@@ -202,7 +202,8 @@ let param_ownership (p : param) : ownership option =
202202 so it cannot reintroduce a use-after-move false negative. Bodies of
203203 nested lambdas are skipped (a [return] there is the lambda's, not this
204204 function's). #554. *)
205- let compute_ret_borrow_params (fd : fn_decl ) : int list =
205+ let compute_ret_borrow_params (lookup : string -> int list option )
206+ (fd : fn_decl ) : int list =
206207 let param_idx : (string, int) Hashtbl.t = Hashtbl. create 8 in
207208 let ref_param : (string, unit) Hashtbl.t = Hashtbl. create 8 in
208209 List. iteri (fun i (p : param ) ->
@@ -229,7 +230,7 @@ let compute_ret_borrow_params (fd : fn_decl) : int list =
229230 (its entry, possibly [], was set by [record_let]) wins — otherwise a
230231 value-local shadowing a ref-param would be spuriously treated as a
231232 returned borrow of that param (false positive). *)
232- let origins_of_ref_source (e : expr ) : int list =
233+ let rec origins_of_ref_source (e : expr ) : int list =
233234 match peel e with
234235 | ExprUnary ((OpRef | OpMutRef ), inner ) ->
235236 (match root_name inner with
@@ -246,6 +247,24 @@ let compute_ret_borrow_params (fd : fn_decl) : int list =
246247 if Hashtbl. mem ref_param id.name then
247248 (match Hashtbl. find_opt param_idx id.name with Some i -> [i] | None -> [] )
248249 else [] )
250+ | ExprApp (f , args ) ->
251+ (* Interprocedural: a call whose result borrows the callee's parameter
252+ [i] makes OUR function borrow whatever argument [i] resolves to in our
253+ frame. [lookup] returns the callee's *current* return-borrow summary,
254+ driven to a fixpoint by [build_context], so a function that returns
255+ another ref-returning call's result (e.g.
256+ [wrap(ref x){ let t = pick(x); return t }]) inherits the origin.
257+ #554 residual (a). *)
258+ (match root_name f with
259+ | Some fname ->
260+ (match lookup fname with
261+ | Some callee_sum ->
262+ List. concat_map (fun i ->
263+ match List. nth_opt args i with
264+ | Some arg -> origins_of_ref_source arg
265+ | None -> [] ) callee_sum
266+ | None -> [] )
267+ | None -> [] )
249268 | _ -> []
250269 in
251270 (* Always record the binding (even to []) so a shadowing value-local masks
@@ -403,18 +422,51 @@ let add_fn_signature (ctx : context) (fd : fn_decl) : unit =
403422 let sig_ = {
404423 fn_name = fd.fd_name.name;
405424 fn_param_ownerships = List. map param_ownership fd.fd_params;
406- fn_ret_borrow_params = compute_ret_borrow_params fd;
425+ (* Standalone use: intraprocedural only (no callee summaries available).
426+ [build_context] recomputes interprocedurally via a fixpoint. *)
427+ fn_ret_borrow_params = compute_ret_borrow_params (fun _ -> None ) fd;
407428 } in
408429 Hashtbl. replace ctx.fn_sigs fd.fd_name.name sig_
409430
410- (* * Build context from program *)
431+ (* * Build context from program.
432+
433+ Return-borrow summaries ([fn_ret_borrow_params]) are computed
434+ *interprocedurally* by a monotone fixpoint: each signature is seeded with
435+ an empty summary, then every function's summary is recomputed against the
436+ callees' current summaries until none change. [compute_ret_borrow_params]
437+ resolves a returned call result through the callee's summary, so a
438+ function that returns another ref-returning call's result eventually
439+ inherits the origin. Origins only grow and are bounded by arity, so the
440+ loop terminates. #554 (interprocedural residual (a)). *)
411441let build_context (program : program ) : context =
412442 let ctx = create_context () in
413- List. iter (fun decl ->
414- match decl with
415- | TopFn fd -> add_fn_signature ctx fd
416- | _ -> ()
417- ) program.prog_decls;
443+ let fds =
444+ List. filter_map (function TopFn fd -> Some fd | _ -> None ) program.prog_decls
445+ in
446+ List. iter (fun (fd : fn_decl ) ->
447+ Hashtbl. replace ctx.fn_sigs fd.fd_name.name {
448+ fn_name = fd.fd_name.name;
449+ fn_param_ownerships = List. map param_ownership fd.fd_params;
450+ fn_ret_borrow_params = [] ;
451+ }) fds;
452+ let lookup name =
453+ match Hashtbl. find_opt ctx.fn_sigs name with
454+ | Some s -> Some s.fn_ret_borrow_params
455+ | None -> None
456+ in
457+ let changed = ref true in
458+ while ! changed do
459+ changed := false ;
460+ List. iter (fun (fd : fn_decl ) ->
461+ let new_sum = compute_ret_borrow_params lookup fd in
462+ match Hashtbl. find_opt ctx.fn_sigs fd.fd_name.name with
463+ | Some s when new_sum <> s.fn_ret_borrow_params ->
464+ Hashtbl. replace ctx.fn_sigs fd.fd_name.name
465+ { s with fn_ret_borrow_params = new_sum };
466+ changed := true
467+ | _ -> ()
468+ ) fds
469+ done ;
418470 ctx
419471
420472(* * Generate a fresh borrow ID *)
@@ -1761,6 +1813,31 @@ and check_stmt (ctx : context) (state : state) (symbols : Symbol.t) (stmt : stmt
17611813 (binder_sym, new_b) :: state.ref_bindings
17621814 | None -> () )
17631815 | None -> () );
1816+ (* #554 residual (c): [r = f(b)] reassigning an existing ref-binder
1817+ to a call whose result borrows [b]. The `&p`/ref-var reborrow
1818+ path above does not fire ([is_reborrow_source] only matches those
1819+ shapes), so release the binder's OLD loan here (ref-counted by
1820+ [b_id] so a borrow another binder still aliases is kept) and
1821+ rebind it to the escaping call-result borrows — mirroring
1822+ [record_ref_binding]'s claim and the Slice-B `&` reborrow, so a
1823+ later use of the old target is no longer spuriously rejected. *)
1824+ (match root_var place with
1825+ | Some binder_sym ->
1826+ let rec peel = function ExprSpan (x , _ ) -> peel x | x -> x in
1827+ (match peel rhs with
1828+ | ExprApp _ when state.result_borrows <> [] ->
1829+ let old, remaining =
1830+ List. partition (fun (s , _ ) -> s = binder_sym) state.ref_bindings
1831+ in
1832+ List. iter (fun (_ , ob ) ->
1833+ if not (List. exists (fun (_ , b' ) -> b'.b_id = ob.b_id) remaining)
1834+ then end_borrow state ob) old;
1835+ state.ref_bindings < -
1836+ List. fold_left (fun acc b -> (binder_sym, b) :: acc)
1837+ remaining state.result_borrows;
1838+ state.result_borrows < - []
1839+ | _ -> () )
1840+ | None -> () );
17641841 Ok ()
17651842 end
17661843 | None ->
@@ -2023,6 +2100,18 @@ let check_program (symbols : Symbol.t) (program : program) : unit result =
20232100 multi-arg, and aggregates (the aggregate + assign-path + branch-tail-
20242101 summary holes a first cut missed were all found and closed).
20252102
2103+ Follow-up (also #554) closed two of the three named residuals:
2104+ - *interprocedural-through-call-result* — [compute_ret_borrow_params] now
2105+ resolves a returned call result through the callee's summary, and
2106+ [build_context] drives all summaries to a monotone fixpoint, so
2107+ [fn wrap(x: ref Int) -> ref Int { let t = pick(x); return t; }] inherits
2108+ pick's origin and [let r = wrap(a); consume(a); *r] is rejected
2109+ (transitively, through any depth of wrappers).
2110+ - *reassignment precision* — [r = f(b)] now releases the binder's prior
2111+ loan (ref-counted by [b_id]) and rebinds to the escaping call result,
2112+ so a later use of the old target is no longer spuriously rejected —
2113+ symmetric with the plain-`&` Slice-B reborrow.
2114+
20262115 Still deferred:
20272116 - Origin/region variables (true Polonius surface) — a region
20282117 var on each [TyRef]/[TyMut] with subset constraints and a
@@ -2031,33 +2120,19 @@ let check_program (symbols : Symbol.t) (program : program) : unit result =
20312120 (docs/decisions/0022-polonius-origin-variables.adoc) for the
20322121 M1-M4 migration plan; lexical checker is the merge oracle
20332122 through M3.
2034- - #554 residuals (all strictly smaller than the original hole; closed
2035- properly by the Polonius origins above, #553):
2036- (a) *interprocedural-through-call-result* — the summary is
2037- intraprocedural and does not chase a returned borrow whose origin
2038- is itself ANOTHER ref-returning call result bound to a local,
2039- e.g. [fn wrap(x: ref Int) -> ref Int { let t = pick(x); return t; }];
2040- so [let r = wrap(a); consume(a); *r] slips through. A summary
2041- fixpoint over the call graph would close it.
2042- (b) *branch-merged / copied-out claim* — a borrow threaded through an
2043- [if]/[match] arm or block tail at the BIND site
2044- ([let r = if c { pick(a) } else { pick(b) }], [let r = { pick(a) }]),
2045- or copied out of its binder into an aggregate before the binder's
2046- last use ([let r = pick(a); let t = (r, 0); …]), is not protected,
2047- because the arm-merge / block-exit drops the branch-local borrow
2048- and NLL expires the binder at the copy site. This is NOT a new
2049- asymmetry: the lexical checker treats the byte-identical plain-`&`
2050- programs ([let r = if c { &a } else { &b }], [let t = (r, 0)] with
2051- [r = &a]) identically (both accept) — it is the same pre-existing
2052- through-branch / copy-out lexical limitation, inherited by the
2053- call-result path, not introduced by it.
2054- (c) *reassignment precision* (over-rejection, not a soundness hole):
2055- [r = pick(b)] reassigning an existing ref-binder does not release
2056- the binder's OLD loan (the [&p] reborrow path does, via
2057- [is_reborrow_source]); a later use of the old target may be
2058- spuriously rejected. Rare; conservative direction.
2059- - Tighter integration with the quantity checker for captured
2060- linears (Slice D).
2123+ - #554 remaining residual (closed properly by the Polonius origins
2124+ above, #553): *branch-merged / copied-out claim* — a borrow threaded
2125+ through an [if]/[match] arm or block tail at the BIND site
2126+ ([let r = if c { pick(a) } else { pick(b) }], [let r = { pick(a) }]),
2127+ or copied out of its binder into an aggregate before the binder's last
2128+ use ([let r = pick(a); let t = (r, 0); …]), is not protected, because
2129+ the arm-merge / block-exit drops the branch-local borrow and NLL
2130+ expires the binder at the copy site. This is NOT a new asymmetry: the
2131+ byte-identical plain-`&` programs ([let r = if c { &a } else { &b }],
2132+ [let t = (r, 0)] with [r = &a]) behave identically (both accept) — the
2133+ same pre-existing through-branch / copy-out lexical limitation,
2134+ inherited by the call-result path, not introduced by it. A true
2135+ flow-sensitive borrow graph (Polonius) discharges it uniformly.
20612136 - Tighter integration with the quantity checker for captured
20622137 linears (Slice D).
20632138*)
0 commit comments