@@ -777,6 +777,234 @@ pub fn analyze_rc_elision(ir: &[TypedIRInstruction]) -> RcElisionReport {
777777 r
778778}
779779
780+ /// A function parameter's ownership mode under the borrow analysis (plans/RC_ELISION.md). Borrowed
781+ /// means the callee only reads the parameter, so a caller could pass it without a retain and keep
782+ /// its own drop. Owned means the parameter escapes (stored, returned, reassigned, spawned, aliased,
783+ /// or handed to an Owned position elsewhere) and keeps the callee-owns convention. Non-reference
784+ /// parameters carry no count and report as Borrowed trivially. This is analysis and report only, no
785+ /// codegen consults it yet.
786+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
787+ pub enum ParamMode {
788+ Borrowed ,
789+ Owned ,
790+ }
791+
792+ /// The argument positions a builtin stores or keeps, making the value escape. Exhaustive over the
793+ /// `Builtin` enum with no wildcard, so adding a builtin forces a decision here rather than silently
794+ /// classifying it as read-only. `CtxNew` is handled at its call site since every argument past the
795+ /// function id escapes into the task.
796+ fn builtin_escaping_positions ( builtin : & Builtin ) -> & ' static [ usize ] {
797+ match builtin {
798+ Builtin :: Push => & [ 1 ] ,
799+ Builtin :: Set => & [ 2 ] ,
800+ Builtin :: MapSet => & [ 1 , 2 ] ,
801+ Builtin :: GlobalSet | Builtin :: GlobalSetList => & [ 1 ] ,
802+ Builtin :: CtxNew => & [ ] ,
803+ Builtin :: Print
804+ | Builtin :: ReadLine
805+ | Builtin :: ReadInt
806+ | Builtin :: Len
807+ | Builtin :: ArrayGet
808+ | Builtin :: ListGet
809+ | Builtin :: Pop
810+ | Builtin :: IntToStr
811+ | Builtin :: FloatToStr
812+ | Builtin :: BoolToStr
813+ | Builtin :: MapNew
814+ | Builtin :: MapGet
815+ | Builtin :: MapHas
816+ | Builtin :: MapLen
817+ | Builtin :: WriteStr
818+ | Builtin :: WriteByte
819+ | Builtin :: ByteAt
820+ | Builtin :: BufNew
821+ | Builtin :: BufPush
822+ | Builtin :: BufPushByte
823+ | Builtin :: BufToStr
824+ | Builtin :: BufLen
825+ | Builtin :: ArrayClone
826+ | Builtin :: StrConcat2
827+ | Builtin :: Substring
828+ | Builtin :: ParseInt
829+ | Builtin :: Status
830+ | Builtin :: FileOpen
831+ | Builtin :: FileRead
832+ | Builtin :: FileWrite
833+ | Builtin :: FileClose
834+ | Builtin :: FileExists
835+ | Builtin :: Poll
836+ | Builtin :: LastOp
837+ | Builtin :: OpResultStr
838+ | Builtin :: OpResultInt
839+ | Builtin :: TcpListen
840+ | Builtin :: TcpAccept
841+ | Builtin :: TcpRecv
842+ | Builtin :: TcpSend
843+ | Builtin :: TcpClose
844+ | Builtin :: CtxSwitch
845+ | Builtin :: CtxLive
846+ | Builtin :: GlobalGet
847+ | Builtin :: GlobalGetList => & [ ] ,
848+ }
849+ }
850+
851+ /// Classify every function's parameters as Borrowed or Owned (plans/RC_ELISION.md). Seeds each
852+ /// parameter's escape from its own body (returned, stored into an aggregate or collection or global,
853+ /// reassigned, aliased under the conservative first cut, spawned), records a dependency edge when a
854+ /// parameter is passed on to another function's parameter position, marks spawn-target functions
855+ /// all-Owned (the task outlives the spawner's frame, so no caller lifetime guarantee exists), and
856+ /// propagates escapes over the static call graph to a fixpoint. Escape facts only grow and the graph
857+ /// is finite, so it terminates, and recursion needs no special case. Unknown callees and unresolvable
858+ /// spawn targets degrade conservatively to Owned.
859+ pub fn analyze_param_modes ( ir_functions : & [ TypedIrFunction ] ) -> HashMap < String , Vec < ParamMode > > {
860+ // Escape flags per function, indexed like its params. Only reference-typed params matter, the
861+ // rest stay false and report Borrowed trivially.
862+ let mut escapes: HashMap < String , Vec < bool > > = ir_functions
863+ . iter ( )
864+ . map ( |f| ( f. name . clone ( ) , vec ! [ false ; f. params. len( ) ] ) )
865+ . collect ( ) ;
866+ // (dependent function, param index) escapes when (callee, callee param index) does.
867+ let mut deps: Vec < ( ( String , usize ) , ( String , usize ) ) > = Vec :: new ( ) ;
868+ let by_raw_id: HashMap < u64 , & TypedIrFunction > = ir_functions
869+ . iter ( )
870+ . map ( |f| ( f. id . raw ( ) as u64 , f) )
871+ . collect ( ) ;
872+
873+ for f in ir_functions {
874+ let param_index: HashMap < usize , usize > = f
875+ . params
876+ . iter ( )
877+ . enumerate ( )
878+ . map ( |( i, p) | ( p. id , i) )
879+ . collect ( ) ;
880+ let mut escape = |escapes : & mut HashMap < String , Vec < bool > > , var : usize | {
881+ if let Some ( & i) = param_index. get ( & var) {
882+ escapes. get_mut ( & f. name ) . expect ( "seeded above" ) [ i] = true ;
883+ }
884+ } ;
885+ for ( idx, instr) in f. body . iter ( ) . enumerate ( ) {
886+ match & instr. kind {
887+ TypedIRStmt :: Assign { target, value } => {
888+ // Reassigning a param means its end-of-scope drop releases the new value, and
889+ // aliasing a param is the conservative first cut for alias-tracked escape.
890+ escape ( & mut escapes, target. id ( ) ) ;
891+ if let TypedIRExpr :: Var ( src) = value {
892+ escape ( & mut escapes, src. id ( ) ) ;
893+ }
894+ }
895+ TypedIRStmt :: Return { value : Some ( v) } => escape ( & mut escapes, v. id ( ) ) ,
896+ TypedIRStmt :: FieldSet { value, .. } => escape ( & mut escapes, value. id ( ) ) ,
897+ TypedIRStmt :: StructNew { args, .. } => {
898+ for a in args {
899+ escape ( & mut escapes, a. id ( ) ) ;
900+ }
901+ }
902+ TypedIRStmt :: ArrayNew { elements, .. } => {
903+ for e in elements {
904+ escape ( & mut escapes, e. id ( ) ) ;
905+ }
906+ }
907+ TypedIRStmt :: Call { function_name, args, .. } => {
908+ for ( j, a) in args. iter ( ) . enumerate ( ) {
909+ if let Some ( & i) = param_index. get ( & a. id ( ) ) {
910+ if escapes. contains_key ( function_name) {
911+ deps. push ( ( ( f. name . clone ( ) , i) , ( function_name. clone ( ) , j) ) ) ;
912+ } else {
913+ escapes. get_mut ( & f. name ) . expect ( "seeded above" ) [ i] = true ;
914+ }
915+ }
916+ }
917+ }
918+ TypedIRStmt :: CallBuiltin { builtin, args, .. } => {
919+ if * builtin == Builtin :: CtxNew {
920+ // Every argument past the function id escapes into the task, and the target
921+ // function itself must keep owned params (the task runtime drops them and
922+ // the spawner's frame may be gone before the task runs).
923+ for a in args. iter ( ) . skip ( 1 ) {
924+ escape ( & mut escapes, a. id ( ) ) ;
925+ }
926+ let target_fid = args. first ( ) . and_then ( |a| {
927+ f. body [ ..idx] . iter ( ) . rev ( ) . find_map ( |prev| match & prev. kind {
928+ TypedIRStmt :: Assign {
929+ target,
930+ value : TypedIRExpr :: Int ( k) ,
931+ } if target. id ( ) == a. id ( ) => Some ( * k as u64 ) ,
932+ _ => None ,
933+ } )
934+ } ) ;
935+ match target_fid. and_then ( |fid| by_raw_id. get ( & fid) ) {
936+ Some ( task) => {
937+ if let Some ( flags) = escapes. get_mut ( & task. name ) {
938+ flags. iter_mut ( ) . for_each ( |e| * e = true ) ;
939+ }
940+ }
941+ None => {
942+ // Unresolvable spawn target: every function might be the task, so
943+ // everything degrades to Owned rather than guessing.
944+ for flags in escapes. values_mut ( ) {
945+ flags. iter_mut ( ) . for_each ( |e| * e = true ) ;
946+ }
947+ }
948+ }
949+ } else {
950+ for & pos in builtin_escaping_positions ( builtin) {
951+ if let Some ( a) = args. get ( pos) {
952+ escape ( & mut escapes, a. id ( ) ) ;
953+ }
954+ }
955+ }
956+ }
957+ TypedIRStmt :: Drop { .. }
958+ | TypedIRStmt :: Jump { .. }
959+ | TypedIRStmt :: JumpIf { .. }
960+ | TypedIRStmt :: JumpIfNot { .. }
961+ | TypedIRStmt :: Label ( _)
962+ | TypedIRStmt :: FieldGet { .. }
963+ | TypedIRStmt :: Return { value : None } => { }
964+ }
965+ }
966+ }
967+
968+ // Propagate escapes through the call graph until stable.
969+ let mut changed = true ;
970+ while changed {
971+ changed = false ;
972+ for ( ( fname, i) , ( gname, j) ) in & deps {
973+ let callee_escapes = escapes
974+ . get ( gname)
975+ . and_then ( |flags| flags. get ( * j) . copied ( ) )
976+ . unwrap_or ( true ) ;
977+ if callee_escapes {
978+ let flags = escapes. get_mut ( fname) . expect ( "seeded above" ) ;
979+ if !flags[ * i] {
980+ flags[ * i] = true ;
981+ changed = true ;
982+ }
983+ }
984+ }
985+ }
986+
987+ ir_functions
988+ . iter ( )
989+ . map ( |f| {
990+ let flags = & escapes[ & f. name ] ;
991+ let modes = f
992+ . params
993+ . iter ( )
994+ . zip ( flags)
995+ . map ( |( p, & esc) | {
996+ if is_rc ( & p. typ ) && esc {
997+ ParamMode :: Owned
998+ } else {
999+ ParamMode :: Borrowed
1000+ }
1001+ } )
1002+ . collect ( ) ;
1003+ ( f. name . clone ( ) , modes)
1004+ } )
1005+ . collect ( )
1006+ }
1007+
7801008/// The variable a statement defines (writes a fresh value into a variable), if any. Used to emit a
7811009/// store-back when that variable is memory-resident. Statements that only read (drops, jumps,
7821010/// returns) or write into an object field rather than a variable define nothing.
@@ -1029,7 +1257,7 @@ fn lower_expr_to_bytecode_stage_one(
10291257/// Whether a value of this type is a reference-counted heap object: nominal structs, strings, and
10301258/// arrays/lists. All carry the shared object header and are managed by IncRef/DecRef, and a
10311259/// container's descriptor traces its elements on free.
1032- fn is_rc ( typ : & ProcessedType ) -> bool {
1260+ pub fn is_rc ( typ : & ProcessedType ) -> bool {
10331261 matches ! (
10341262 typ,
10351263 ProcessedType :: Custom { .. }
@@ -1964,6 +2192,96 @@ mod codegen_tests {
19642192
19652193 const RC_SINK : & str = "def sink(s: String) -> Int:\n return len(s)\n \n " ;
19662194
2195+ /// The borrow analysis's parameter modes for one function (mangled names match by prefix).
2196+ fn param_modes_for ( src : & str , fn_name : & str ) -> Vec < super :: ParamMode > {
2197+ let ctx = analyze_source ( src) ;
2198+ assert ! ( !ctx. errors( ) . is_fatal( ) , "compile errors: {:?}" , ctx. errors( ) ) ;
2199+ let mut ir_functions = vec ! [ ctx. main_function( ) ] ;
2200+ ir_functions. extend ( ctx. functions ( ) . to_vec ( ) ) ;
2201+ let modes = super :: analyze_param_modes ( & ir_functions) ;
2202+ let mangled = format ! ( "{fn_name}$" ) ;
2203+ let name = ir_functions
2204+ . iter ( )
2205+ . map ( |f| f. name . clone ( ) )
2206+ . find ( |n| n == fn_name || n. starts_with ( & mangled) )
2207+ . unwrap_or_else ( || panic ! ( "no function named {fn_name}" ) ) ;
2208+ modes[ & name] . clone ( )
2209+ }
2210+
2211+ use super :: ParamMode :: { Borrowed , Owned } ;
2212+
2213+ #[ test]
2214+ fn borrow_read_only_param_is_borrowed ( ) {
2215+ let m = param_modes_for ( "def f(s: String) -> Int:\n return len(s)\n " , "f" ) ;
2216+ assert_eq ! ( m, vec![ Borrowed ] ) ;
2217+ }
2218+
2219+ #[ test]
2220+ fn borrow_returned_param_is_owned ( ) {
2221+ // Ownership rides out on the return, so the caller cannot keep the only reference.
2222+ let m = param_modes_for ( "def f(s: String) -> String:\n return s\n " , "f" ) ;
2223+ assert_eq ! ( m, vec![ Owned ] ) ;
2224+ }
2225+
2226+ #[ test]
2227+ fn borrow_field_stored_param_is_owned ( ) {
2228+ let m = param_modes_for (
2229+ "type Box:\n v: String\n \n def f(b: Box, s: String):\n b.v = s\n " ,
2230+ "f" ,
2231+ ) ;
2232+ assert_eq ! ( m, vec![ Borrowed , Owned ] , "the box is read, the stored value escapes" ) ;
2233+ }
2234+
2235+ #[ test]
2236+ fn borrow_reassigned_param_is_owned ( ) {
2237+ // The end-of-scope drop releases the reassigned value, so the param cannot be borrowed.
2238+ let m = param_modes_for (
2239+ "def f(s: String) -> Int:\n s = \" other\" \n return len(s)\n " ,
2240+ "f" ,
2241+ ) ;
2242+ assert_eq ! ( m, vec![ Owned ] ) ;
2243+ }
2244+
2245+ #[ test]
2246+ fn borrow_list_pushed_param_is_owned_buf_pushed_is_borrowed ( ) {
2247+ // The list stores its element (escape), the buffer only copies bytes out (read).
2248+ let m = param_modes_for (
2249+ "def f(xs: List[String], s: String, b: StrBuf, t: String):\n push(xs, s)\n buf_push(b, t)\n " ,
2250+ "f" ,
2251+ ) ;
2252+ assert_eq ! ( m, vec![ Borrowed , Owned , Borrowed , Borrowed ] ) ;
2253+ }
2254+
2255+ #[ test]
2256+ fn borrow_escape_propagates_transitively ( ) {
2257+ // g stores its param, f only forwards to g, so f's param is owned through the fixpoint,
2258+ // while a forward to a read-only callee stays borrowed.
2259+ let src = "type Box:\n v: String\n \n def store(b: Box, s: String):\n b.v = s\n \n def f(b: Box, s: String):\n store(b, s)\n \n def peek(s: String) -> Int:\n return len(s)\n \n def g(s: String) -> Int:\n return peek(s)\n " ;
2260+ assert_eq ! ( param_modes_for( src, "f" ) , vec![ Borrowed , Owned ] ) ;
2261+ assert_eq ! ( param_modes_for( src, "g" ) , vec![ Borrowed ] ) ;
2262+ }
2263+
2264+ #[ test]
2265+ fn borrow_recursive_read_only_param_stays_borrowed ( ) {
2266+ // The fixpoint terminates on the self-edge with no escape seed, so recursion alone does not
2267+ // pessimize.
2268+ let m = param_modes_for (
2269+ "def f(s: String, n: Int) -> Int:\n if n <= 0:\n return len(s)\n return f(s, n - 1)\n " ,
2270+ "f" ,
2271+ ) ;
2272+ assert_eq ! ( m, vec![ Borrowed , Borrowed ] , "the string param and the trivially non-ref Int" ) ;
2273+ }
2274+
2275+ #[ test]
2276+ fn borrow_spawn_target_params_are_owned ( ) {
2277+ // The task outlives the spawner's frame, so no caller lifetime guarantee exists.
2278+ let m = param_modes_for (
2279+ "def worker(s: String):\n print(s)\n \n let x = \" hi\" \n spawn worker(x)\n scheduler_run()\n " ,
2280+ "worker" ,
2281+ ) ;
2282+ assert_eq ! ( m, vec![ Owned ] ) ;
2283+ }
2284+
19672285 #[ test]
19682286 fn rc_call_arg_last_use_is_movable ( ) {
19692287 // `x`'s only use is the call, so passing it is a move: the retain is elidable.
0 commit comments