-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathborrow.ml
More file actions
2185 lines (2066 loc) · 94.8 KB
/
Copy pathborrow.ml
File metadata and controls
2185 lines (2066 loc) · 94.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(* SPDX-License-Identifier: MPL-2.0 *)
(* SPDX-FileCopyrightText: 2024-2025 hyperpolymath *)
(** Borrow checker for ownership verification.
This module implements borrow checking to ensure memory safety:
- No use after move
- No conflicting borrows
- Borrows don't outlive owners
[IMPL-DEP: Phase 3]
*)
open Ast
(** A place is an l-value that can be borrowed *)
type place =
| PlaceVar of string * Symbol.symbol_id (** human name, symbol id *)
| PlaceField of place * string
| PlaceIndex of place * int option (** None for dynamic index *)
| PlaceDeref of place
[@@deriving show]
(** Borrow kind *)
type borrow_kind =
| Shared (** Immutable borrow (&) *)
| Exclusive (** Mutable borrow (&mut) *)
[@@deriving show, eq]
(** Human-readable borrow kind for error messages. *)
let borrow_kind_name (k : borrow_kind) : string =
match k with
| Shared -> "shared"
| Exclusive -> "exclusive"
(** A borrow record *)
type borrow = {
b_place : place;
b_kind : borrow_kind;
b_span : Span.t;
b_id : int;
}
[@@deriving show]
(** Move record for tracking move sites *)
type move_record = {
m_place : place;
m_span : Span.t;
}
[@@deriving show]
(** Function signature for ownership tracking *)
type fn_signature = {
fn_name : string;
fn_param_ownerships : ownership option list;
(** Parameter indices whose borrow may flow out through the function's
return value — the function's *return-borrow summary*. Non-empty
exactly when some return position yields a borrow rooted at a
[ref]/[mut] (caller-owned) parameter (directly [&p] / [return p], or
through a let-bound ref-local chain). A call [let r = f(a, b)] where
[i] is in this set means the result [r] borrows argument [i] — the
borrow-graph edge that was previously missing, letting a use of the
moved argument through [r] slip past (#554). Computed syntactically
from the body (name-based) at signature-build time; the
interprocedural-through-call-result case (a return whose origin is
itself another ref-returning call result bound to a local) is the
documented residual that true Polonius origins (ADR-022 / #553)
close. *)
fn_ret_borrow_params : int list;
}
(** Borrow checker context with function signatures *)
type context = {
fn_sigs : (string, fn_signature) Hashtbl.t;
}
(** Borrow checker state *)
type state = {
(** Active borrows *)
mutable borrows : borrow list;
(** Moved places with their move sites *)
mutable moved : move_record list;
(** Next borrow ID *)
mutable next_id : int;
(** Mutable bindings - places that were declared with 'let mut' *)
mutable mutable_bindings : place list;
(** Borrow-graph edges: a reference binder symbol -> the borrow it holds.
Populated when a [let r = &x] / [let r = &mut x] binds a reference to
a place. These borrows are *escaping* (they live as long as the
binder's lexical scope), unlike call-argument borrows which are
temporary and released when the call expression completes. This is
the graph that borrow-graph validation (CORE-01 / #177) walks. *)
mutable ref_bindings : (Symbol.symbol_id * borrow) list;
(** Symbols bound by a [let] in the current block, innermost first.
Used to decide whether a borrowed owner is block-local (so a
reference that escapes the block outlives its owner). *)
mutable block_local_syms : Symbol.symbol_id list;
(** Sym-ids of the current function's *callee-owned* parameters: those
with effective ownership [None] (by-value) or [Some Own]. A reference
rooted at one of these escapes the callee frame if it is *returned*,
exactly like a borrow of a block-local. [ref]/[mut] params are
*caller-owned* referents and are deliberately absent here — returning
a borrow of them is sound. CORE-01 pt2 / #177 (return-escape). *)
mutable callee_owned_params : Symbol.symbol_id list;
(** Sym-ids of bindings declared @linear (`QOne`) in the current
function — surfaced as explicit `@linear` annotations on lets/params
and as inferred `QOne` from a [Cmd _] type annotation. Maintained
alongside the quantity checker's per-block linear-tracking so the
borrow checker can reject *capture* of these bindings by a closure:
a closure can be called 0..N times, so capturing a linear binding
makes its consumption count unprovable at borrow time. The quantity
checker also catches this via [QOmega] scaling of captures, but the
borrow-side error fires earlier in the pipeline and points at the
*lambda* span (the capture site) rather than at a downstream "used
multiple times" diagnostic. CORE-01 pt3 Slice D / #177. *)
mutable linear_bindings : Symbol.symbol_id list;
(** Escaping borrows produced by the most-recently-checked call
expression whose callee has a non-empty return-borrow summary. A call
[f(a)] whose result borrows [a] keeps that argument borrow *live* on
[state.borrows] (not released at call end) and stashes it here so the
immediately-following [record_ref_binding] can claim it as the result
binder's ref-graph edge ([let r = f(a)] → [r] borrows [a]), subjecting
it to the same NLL last-use expiry and return-escape checks as
[let r = &a]. An *unclaimed* escaping borrow (the result flowed into
an aggregate, was dereferenced, or discarded) behaves exactly like an
unclaimed plain `&` borrow: it simply lingers on [state.borrows] until
lexical block exit. This list is only a hand-off pointer for the claim
step; the borrows it names live or die on [state.borrows]. #554. *)
mutable result_borrows : borrow list;
}
(** Borrow checker errors *)
type borrow_error =
| UseAfterMove of place * Span.t * Span.t (** place, use site, move site *)
| ConflictingBorrow of borrow * borrow
| BorrowOutlivesOwner of borrow * Symbol.symbol_id
| MoveWhileBorrowed of place * borrow
| CannotMoveOutOfBorrow of place * borrow
| CannotBorrowAsMutable of place * Span.t
| UseWhileExclusivelyBorrowed of place * borrow * Span.t
(** use of [place] (at the trailing span) while a still-live exclusive
borrow holds it — the shared-XOR-exclusive aliasing rule, enforced
at use sites, not only at borrow creation. CORE-01 / #177. *)
| LinearCapturedByClosure of string * Span.t
(** the named @linear binding has been captured as a closure free
variable at the lambda's span — capturing extends consumption
beyond what the @linear contract can be checked at borrow time
(a closure may be called 0..N times). CORE-01 pt3 Slice D / #177. *)
[@@deriving show]
type 'a result = ('a, borrow_error) Result.t
(* Result bind - define before use *)
let ( let* ) = Result.bind
(** Effective ownership of a parameter.
AffineScript source encodes `own`/`ref`/`mut` on a parameter as the
*type* constructors [TyOwn]/[TyRef]/[TyMut] (the same encoding
[codegen.ml] reads to derive WASM param kinds), not as the legacy
[p_ownership] field — which the parser leaves [None] for surface
`a: mut Int`. Reading only [p_ownership] (the prior behaviour) meant
the owned/ref/mut borrow discipline was effectively *unenforced from
source*. Prefer an explicit [p_ownership]; otherwise derive it from
the parameter type. CORE-01 / #177. *)
let ty_ownership (t : type_expr) : ownership option =
match t with
| TyOwn _ -> Some Own
| TyRef _ -> Some Ref
| TyMut _ -> Some Mut
| _ -> None
let param_ownership (p : param) : ownership option =
match p.p_ownership with
| Some _ as o -> o
| None -> ty_ownership p.p_ty
(** Compute a function's *return-borrow summary*: the set of parameter
indices whose borrow may be returned (see [fn_ret_borrow_params]).
Purely syntactic and name-based (no symbol table needed, so it can run
at signature-build time). A return position contributes parameter [i]
when its value is a borrow rooted at the [ref]/[mut] parameter at index
[i] — either [&p] / [&mut p] for such a [p], the bare [return p] of a
[ref]/[mut] parameter (returning the reference propagates the caller's
borrow), or a let-bound ref-local that was itself seeded from one of
those. Borrows of by-value/[own] parameters or locals are NOT included:
those are return-escapes the in-function [check_return_escape] already
rejects, so the function never type-checks and its summary is moot.
Sound direction: the summary may *over*-approximate origins (e.g. across
branches, or a local shadowing a parameter name) — over-approximation
only ever keeps more argument borrows alive at call sites, never fewer,
so it cannot reintroduce a use-after-move false negative. Bodies of
nested lambdas are skipped (a [return] there is the lambda's, not this
function's). #554. *)
let compute_ret_borrow_params (lookup : string -> int list option)
(fd : fn_decl) : int list =
let param_idx : (string, int) Hashtbl.t = Hashtbl.create 8 in
let ref_param : (string, unit) Hashtbl.t = Hashtbl.create 8 in
List.iteri (fun i (p : param) ->
Hashtbl.replace param_idx p.p_name.name i;
(match param_ownership p with
| Some Ref | Some Mut -> Hashtbl.replace ref_param p.p_name.name ()
| _ -> ())
) fd.fd_params;
(* name -> param-index set a let-bound ref-local may borrow *)
let local_origins : (string, int list) Hashtbl.t = Hashtbl.create 8 in
let result = ref [] in
let add_origins l =
List.iter (fun i -> if not (List.mem i !result) then result := i :: !result) l
in
let rec peel = function ExprSpan (e, _) -> peel e | e -> e in
let rec root_name (e : expr) : string option =
match peel e with
| ExprVar id -> Some id.name
| ExprField (b, _) | ExprIndex (b, _) | ExprTupleIndex (b, _) -> root_name b
| _ -> None
in
(* Param indices a ref-source expression borrows, [] if none. [local_origins]
is consulted BEFORE [ref_param] so a local that shadows a parameter name
(its entry, possibly [], was set by [record_let]) wins — otherwise a
value-local shadowing a ref-param would be spuriously treated as a
returned borrow of that param (false positive). *)
let rec origins_of_ref_source (e : expr) : int list =
match peel e with
| ExprUnary ((OpRef | OpMutRef), inner) ->
(match root_name inner with
| Some n ->
(match Hashtbl.find_opt local_origins n with
| Some l -> l
| None ->
(match Hashtbl.find_opt param_idx n with Some i -> [i] | None -> []))
| None -> [])
| ExprVar id ->
(match Hashtbl.find_opt local_origins id.name with
| Some l -> l
| None ->
if Hashtbl.mem ref_param id.name then
(match Hashtbl.find_opt param_idx id.name with Some i -> [i] | None -> [])
else [])
| ExprApp (f, args) ->
(* Interprocedural: a call whose result borrows the callee's parameter
[i] makes OUR function borrow whatever argument [i] resolves to in our
frame. [lookup] returns the callee's *current* return-borrow summary,
driven to a fixpoint by [build_context], so a function that returns
another ref-returning call's result (e.g.
[wrap(ref x){ let t = pick(x); return t }]) inherits the origin.
#554 residual (a). *)
(match root_name f with
| Some fname ->
(match lookup fname with
| Some callee_sum ->
List.concat_map (fun i ->
match List.nth_opt args i with
| Some arg -> origins_of_ref_source arg
| None -> []) callee_sum
| None -> [])
| None -> [])
| _ -> []
in
(* Always record the binding (even to []) so a shadowing value-local masks
the parameter it shadows for the rest of the scan. *)
let record_let (pat : pattern) (value : expr) : unit =
match pat with
| PatVar id -> Hashtbl.replace local_origins id.name (origins_of_ref_source value)
| _ -> ()
in
(* [walk_tail] visits an expression in *return/tail* position: its value, if
a ref-source, is a return origin, and so are the tails of any [if]/[match]/
block it resolves to. [walk_expr] visits a non-tail expression: it only
harvests explicit [return]s and threads [let]-bindings into [local_origins].
Splitting the two is what lets a borrow returned via a bare [match]/[if]
arm tail (e.g. `match k { _ => &x }`) register as an origin. *)
let rec walk_tail (e : expr) : unit =
add_origins (origins_of_ref_source e);
match e with
| ExprSpan (e, _) -> walk_tail e
| ExprReturn (Some r) -> walk_tail r
| ExprReturn None -> ()
| ExprLet lb ->
walk_expr lb.el_value;
record_let lb.el_pat lb.el_value;
(match lb.el_body with Some b -> walk_tail b | None -> ())
| ExprBlock blk -> walk_block_tail blk
| ExprIf ei ->
walk_expr ei.ei_cond; walk_tail ei.ei_then;
(match ei.ei_else with Some e -> walk_tail e | None -> ())
| ExprMatch em ->
walk_expr em.em_scrutinee;
List.iter (fun arm ->
(match arm.ma_guard with Some g -> walk_expr g | None -> ());
walk_tail arm.ma_body) em.em_arms
| ExprHandle eh ->
walk_expr eh.eh_body;
List.iter (fun arm ->
match arm with HandlerReturn (_, b) | HandlerOp (_, _, b) -> walk_tail b)
eh.eh_handlers
| ExprTry et ->
walk_block_tail et.et_body;
(match et.et_catch with
| Some arms -> List.iter (fun arm -> walk_tail arm.ma_body) arms
| None -> ());
(match et.et_finally with Some b -> walk_block_tail b | None -> ())
| _ -> walk_expr e
and walk_expr (e : expr) : unit =
match e with
| ExprSpan (e, _) -> walk_expr e
(* A returned value is in tail/return position no matter where the
[return] textually sits, so it must be walked as a TAIL — otherwise a
borrow returned via [return if c { &x } else { &x };] (or match/block)
is missed and the #554 stamp is bypassed by the idiomatic spelling.
Found by second-pass adversarial verification. *)
| ExprReturn (Some r) -> walk_tail r
| ExprReturn None -> ()
| ExprLet lb ->
walk_expr lb.el_value;
record_let lb.el_pat lb.el_value;
(match lb.el_body with Some b -> walk_expr b | None -> ())
| ExprBlock blk -> walk_block blk
| ExprIf ei ->
walk_expr ei.ei_cond; walk_expr ei.ei_then;
(match ei.ei_else with Some e -> walk_expr e | None -> ())
| ExprMatch em ->
walk_expr em.em_scrutinee;
List.iter (fun arm ->
(match arm.ma_guard with Some g -> walk_expr g | None -> ());
walk_expr arm.ma_body) em.em_arms
| ExprApp (f, args) -> walk_expr f; List.iter walk_expr args
| ExprBinary (a, _, b) -> walk_expr a; walk_expr b
| ExprUnary (_, e) -> walk_expr e
| ExprField (b, _) | ExprTupleIndex (b, _) | ExprRowRestrict (b, _) -> walk_expr b
| ExprIndex (a, b) -> walk_expr a; walk_expr b
| ExprTuple es | ExprArray es -> List.iter walk_expr es
| ExprRecord r ->
List.iter (fun (_, eo) -> match eo with Some e -> walk_expr e | None -> ()) r.er_fields;
(match r.er_spread with Some e -> walk_expr e | None -> ())
| ExprHandle eh ->
walk_expr eh.eh_body;
List.iter (fun arm ->
match arm with HandlerReturn (_, b) | HandlerOp (_, _, b) -> walk_expr b)
eh.eh_handlers
| ExprTry et ->
walk_block et.et_body;
(match et.et_catch with
| Some arms -> List.iter (fun arm -> walk_expr arm.ma_body) arms
| None -> ());
(match et.et_finally with Some b -> walk_block b | None -> ())
| ExprResume (Some e) -> walk_expr e
(* Lambda bodies are skipped: a return there belongs to the lambda. *)
| _ -> ()
and walk_stmt (s : stmt) : unit =
match s with
| StmtLet sl -> walk_expr sl.sl_value; record_let sl.sl_pat sl.sl_value
| StmtExpr e -> walk_expr e
| StmtAssign (l, _, r) ->
walk_expr l; walk_expr r;
(* A whole-var reassignment may change which parameters a ref-local
borrows. UNION the new origins into the local's set (rather than
replace) so the flow-insensitive summary never *drops* a possible
origin — conservative, so [let mut t = pick(y); t = pick(x); return t]
summarises {x,y} not the stale {y}, closing the reassigned-local
false negative. #554 round-3. *)
(match peel l with
| ExprVar id ->
let prior =
match Hashtbl.find_opt local_origins id.name with Some x -> x | None -> []
in
Hashtbl.replace local_origins id.name
(List.sort_uniq compare (prior @ origins_of_ref_source r))
| _ -> ())
| StmtWhile (c, b) -> walk_expr c; walk_block b
| StmtFor (_, it, b) -> walk_expr it; walk_block b
and walk_block (blk : block) : unit =
(* non-tail block: only explicit returns inside it reach the fn return *)
List.iter walk_stmt blk.blk_stmts;
match blk.blk_expr with Some e -> walk_expr e | None -> ()
and walk_block_tail (blk : block) : unit =
(* block in tail position: its value expression is in tail position *)
List.iter walk_stmt blk.blk_stmts;
match blk.blk_expr with Some e -> walk_tail e | None -> ()
in
(match fd.fd_body with
| FnBlock blk -> walk_block_tail blk
| FnExpr e -> walk_tail e
| FnExtern -> ());
List.sort_uniq compare !result
(** Create a new borrow checker context *)
let create_context () : context =
{
fn_sigs = Hashtbl.create 64;
}
(** Create a new borrow checker state *)
let create () : state =
{
borrows = [];
moved = [];
next_id = 0;
mutable_bindings = [];
ref_bindings = [];
block_local_syms = [];
callee_owned_params = [];
linear_bindings = [];
result_borrows = [];
}
(** Mirror of [Quantity.quantity_of_ty_annotation]: returns [QOne] when the
given type annotation is a [Cmd _] application (linear by construction
per ADR-002 / Stage 11), [QOmega] otherwise. Duplicated here so
[Borrow] does not depend on [Quantity]; the canonical helper still
lives in [quantity.ml]. CORE-01 pt3 Slice D / #177. *)
let borrow_quantity_of_ty (te_opt : type_expr option) : quantity =
match te_opt with
| Some (TyApp ({ name = "Cmd"; _ }, _)) -> QOne
| _ -> QOmega
(** Returns true if a let-binding declared with the given explicit-quantity
annotation and type annotation should be tracked as @linear (QOne) by
the borrow checker. CORE-01 pt3 Slice D / #177. *)
let let_is_linear (q_opt : quantity option) (ty_opt : type_expr option) : bool =
match q_opt with
| Some q -> q = QOne
| None -> borrow_quantity_of_ty ty_opt = QOne
(** Add a function signature to context *)
let add_fn_signature (ctx : context) (fd : fn_decl) : unit =
let sig_ = {
fn_name = fd.fd_name.name;
fn_param_ownerships = List.map param_ownership fd.fd_params;
(* Standalone use: intraprocedural only (no callee summaries available).
[build_context] recomputes interprocedurally via a fixpoint. *)
fn_ret_borrow_params = compute_ret_borrow_params (fun _ -> None) fd;
} in
Hashtbl.replace ctx.fn_sigs fd.fd_name.name sig_
(** Build context from program.
Return-borrow summaries ([fn_ret_borrow_params]) are computed
*interprocedurally* by a monotone fixpoint: each signature is seeded with
an empty summary, then every function's summary is recomputed against the
callees' current summaries until none change. [compute_ret_borrow_params]
resolves a returned call result through the callee's summary, so a
function that returns another ref-returning call's result eventually
inherits the origin. Origins only grow and are bounded by arity, so the
loop terminates. #554 (interprocedural residual (a)). *)
let build_context (program : program) : context =
let ctx = create_context () in
let fds =
List.filter_map (function TopFn fd -> Some fd | _ -> None) program.prog_decls
in
List.iter (fun (fd : fn_decl) ->
Hashtbl.replace ctx.fn_sigs fd.fd_name.name {
fn_name = fd.fd_name.name;
fn_param_ownerships = List.map param_ownership fd.fd_params;
fn_ret_borrow_params = [];
}) fds;
let lookup name =
match Hashtbl.find_opt ctx.fn_sigs name with
| Some s -> Some s.fn_ret_borrow_params
| None -> None
in
let changed = ref true in
while !changed do
changed := false;
List.iter (fun (fd : fn_decl) ->
let new_sum = compute_ret_borrow_params lookup fd in
match Hashtbl.find_opt ctx.fn_sigs fd.fd_name.name with
| Some s when new_sum <> s.fn_ret_borrow_params ->
Hashtbl.replace ctx.fn_sigs fd.fd_name.name
{ s with fn_ret_borrow_params = new_sum };
changed := true
| _ -> ()
) fds
done;
ctx
(** Generate a fresh borrow ID *)
let fresh_id (state : state) : int =
let id = state.next_id in
state.next_id <- id + 1;
id
(** Check if a type is Copy (doesn't need to be moved) *)
let rec is_copy_type (ty_opt : type_expr option) : bool =
match ty_opt with
| None -> false (* Unknown type, assume not Copy *)
| Some ty ->
begin match ty with
| TyCon id when id.name = "Int" || id.name = "Bool" || id.name = "Char" -> true
| TyCon id when id.name = "Unit" -> true
| TyTuple tys -> List.for_all (fun t -> is_copy_type (Some t)) tys
| TyRef _ -> true (* Shared references are Copy *)
| _ -> false (* Records, arrays, owned types, etc. are not Copy *)
end
(** Check if an expression has a Copy type (heuristic for literals) *)
let is_copy_expr (expr : expr) : bool =
match expr with
| ExprLit (LitInt _) -> true
| ExprLit (LitBool _) -> true
| ExprLit (LitChar _) -> true
| ExprLit (LitUnit _) -> true
| ExprUnary ((OpRef | OpMutRef), _) -> true (* Reference creation produces a Copy pointer *)
| _ -> false
(** Walk to the root variable of a place, if any. *)
let rec root_var (p : place) : Symbol.symbol_id option =
match p with
| PlaceVar (_, id) -> Some id
| PlaceField (base, _)
| PlaceIndex (base, _)
| PlaceDeref base -> root_var base
(** Check if two places overlap.
Two places overlap when they share the same root variable. This is
deliberately conservative — [a.x] and [a.y] overlap, [a[0]] and [a[1]]
overlap — which is the safe direction for a borrow checker (it may
report conflicts that a more precise analysis would allow, but never
misses a real conflict). The rule terminates trivially because
[root_var] always descends.
The previous implementation case-split on shape and recursed into
sub-place pairs; that worked for [PlaceVar]/[PlaceField] alone but
produced an infinite recursion once [PlaceIndex] was added to the
cross-shape arms, and silently returned [false] on
[PlaceIndex] vs [PlaceVar] before that — which is why writes through
[mut buf: Array[T]] parameters were spuriously rejected. *)
let places_overlap (p1 : place) (p2 : place) : bool =
match root_var p1, root_var p2 with
| Some r1, Some r2 -> r1 = r2
| _ -> false
(** Check if a place is moved and return the move site if so *)
let find_move (state : state) (place : place) : Span.t option =
List.find_map (fun mr ->
if places_overlap place mr.m_place then Some mr.m_span
else None
) state.moved
(** Check if a place is moved *)
let is_moved (state : state) (place : place) : bool =
Option.is_some (find_move state place)
(** Check if a borrow conflicts with existing borrows *)
let find_conflicting_borrow (state : state) (new_borrow : borrow) : borrow option =
List.find_opt (fun existing ->
places_overlap new_borrow.b_place existing.b_place &&
(new_borrow.b_kind = Exclusive || existing.b_kind = Exclusive)
) state.borrows
(** Record a move *)
let record_move (state : state) (place : place) (span : Span.t) : unit result =
(* Check for active borrows *)
match List.find_opt (fun b -> places_overlap place b.b_place) state.borrows with
| Some borrow -> Error (MoveWhileBorrowed (place, borrow))
| None ->
state.moved <- { m_place = place; m_span = span } :: state.moved;
Ok ()
(** Check if a place is mutable *)
let is_mutable (state : state) (place : place) : bool =
List.exists (fun mut_place -> places_overlap place mut_place) state.mutable_bindings
(** Record a borrow *)
let record_borrow (state : state) (place : place) (kind : borrow_kind)
(span : Span.t) : borrow result =
(* Check if moved and report the original move site *)
match find_move state place with
| Some move_site ->
Error (UseAfterMove (place, span, move_site))
| None ->
(* Check if trying to mutably borrow an immutable place *)
let mut_check = match kind with
| Exclusive ->
if not (is_mutable state place) then
Error (CannotBorrowAsMutable (place, span))
else
Ok ()
| Shared -> Ok ()
in
match mut_check with
| Error _ as err -> err
| Ok () ->
let new_borrow = {
b_place = place;
b_kind = kind;
b_span = span;
b_id = fresh_id state;
} in
match find_conflicting_borrow state new_borrow with
| Some conflict -> Error (ConflictingBorrow (new_borrow, conflict))
| None ->
state.borrows <- new_borrow :: state.borrows;
Ok new_borrow
(** End a borrow *)
let end_borrow (state : state) (borrow : borrow) : unit =
state.borrows <- List.filter (fun b -> b.b_id <> borrow.b_id) state.borrows
(** Find a still-live exclusive borrow that aliases [place], if any.
With call-argument borrows released after their call (see [check_expr]
[ExprApp]), a persistent exclusive borrow overlapping [place] is a real
escaping `&mut` binding — reading or otherwise using the place while it
is exclusively borrowed violates shared-XOR-exclusive. This is the
use-site half of the rule that [find_conflicting_borrow] only enforced
at borrow *creation*. CORE-01 / #177. *)
let find_aliasing_exclusive (state : state) (place : place) : borrow option =
List.find_opt (fun b ->
b.b_kind = Exclusive && places_overlap place b.b_place
) state.borrows
(** Check a use of a place *)
let check_use (state : state) (place : place) (span : Span.t) : unit result =
match find_move state place with
| Some move_site -> Error (UseAfterMove (place, span, move_site))
| None ->
match find_aliasing_exclusive state place with
| Some b -> Error (UseWhileExclusivelyBorrowed (place, b, span))
| None -> Ok ()
(** Format a place for display in error messages. *)
let rec format_place (p : place) : string =
match p with
| PlaceVar (name, _) -> name
| PlaceField (base, f) -> format_place base ^ "." ^ f
| PlaceIndex (base, Some i) -> format_place base ^ "[" ^ string_of_int i ^ "]"
| PlaceIndex (base, None) -> format_place base ^ "[_]"
| PlaceDeref p' -> "*" ^ format_place p'
(** Format a span for display. *)
let format_span (span : Span.t) : string =
Format.asprintf "%a" Span.pp_short span
(** Format a borrow error as a human-readable string. *)
let format_borrow_error (e : borrow_error) : string =
match e with
| UseAfterMove (place, use_span, move_span) ->
Printf.sprintf
"use of moved value: `%s`\n \
value used at %s\n \
value moved at %s"
(format_place place) (format_span use_span) (format_span move_span)
| ConflictingBorrow (b1, b2) ->
Printf.sprintf
"conflicting borrows on `%s`:\n \
%s borrow (id %d) at %s conflicts with earlier %s borrow (id %d) at %s"
(format_place b1.b_place)
(borrow_kind_name b1.b_kind) b1.b_id (format_span b1.b_span)
(borrow_kind_name b2.b_kind) b2.b_id (format_span b2.b_span)
| BorrowOutlivesOwner (b, sym_id) ->
Printf.sprintf
"borrow of `%s` (id %d) outlives its owner (symbol %d)"
(format_place b.b_place) b.b_id sym_id
| MoveWhileBorrowed (place, b) ->
Printf.sprintf
"cannot move `%s` while it is %s-borrowed at %s"
(format_place place) (borrow_kind_name b.b_kind) (format_span b.b_span)
| CannotMoveOutOfBorrow (place, b) ->
Printf.sprintf
"cannot move out of `%s`, which is behind a %s borrow at %s"
(format_place place) (borrow_kind_name b.b_kind) (format_span b.b_span)
| CannotBorrowAsMutable (place, span) ->
Printf.sprintf
"cannot borrow `%s` as mutable — it is not declared with `let mut` (at %s)"
(format_place place) (format_span span)
| UseWhileExclusivelyBorrowed (place, b, use_span) ->
Printf.sprintf
"cannot use `%s` at %s while it is exclusively borrowed:\n \
exclusive borrow (id %d) taken at %s is still live"
(format_place place) (format_span use_span)
b.b_id (format_span b.b_span)
| LinearCapturedByClosure (name, lam_span) ->
Printf.sprintf
"cannot capture @linear binding `%s` in a closure (at %s)\n \
a closure may be called zero or many times, so capturing a \
@linear binding makes its consumption count unprovable. \
Inline the use, or move the binding into the closure body."
name (format_span lam_span)
(** Get span from an expression *)
let rec expr_span (expr : expr) : Span.t =
match expr with
| ExprSpan (_, span) -> span
| ExprLit lit -> lit_span lit
| ExprVar id -> id.span
| ExprLet { el_pat; _ } -> pattern_span el_pat
| ExprIf { ei_cond; _ } -> expr_span ei_cond
| ExprMatch { em_scrutinee; _ } -> expr_span em_scrutinee
| ExprLambda { elam_params; _ } ->
begin match elam_params with
| p :: _ -> p.p_name.span
| [] -> Span.dummy
end
| ExprApp (f, _) -> expr_span f
| ExprField (e, _) -> expr_span e
| ExprTupleIndex (e, _) -> expr_span e
| ExprIndex (e, _) -> expr_span e
| ExprTuple exprs ->
begin match exprs with
| e :: _ -> expr_span e
| [] -> Span.dummy
end
| ExprArray exprs ->
begin match exprs with
| e :: _ -> expr_span e
| [] -> Span.dummy
end
| ExprRecord { er_fields; _ } ->
begin match er_fields with
| (id, _) :: _ -> id.span
| [] -> Span.dummy
end
| ExprRowRestrict (e, _) -> expr_span e
| ExprBinary (e, _, _) -> expr_span e
| ExprUnary (_, e) -> expr_span e
| ExprBlock { blk_stmts; blk_expr } ->
begin match blk_stmts with
| StmtLet { sl_pat; _ } :: _ -> pattern_span sl_pat
| StmtExpr e :: _ -> expr_span e
| StmtAssign (e, _, _) :: _ -> expr_span e
| StmtWhile (e, _) :: _ -> expr_span e
| StmtFor (p, _, _) :: _ -> pattern_span p
| [] -> match blk_expr with Some e -> expr_span e | None -> Span.dummy
end
| ExprReturn _ -> Span.dummy
| ExprBreak sp -> sp
| ExprContinue sp -> sp
| ExprTry _ -> Span.dummy
| ExprHandle { eh_body; _ } -> expr_span eh_body
| ExprResume _ -> Span.dummy
| ExprUnsafe _ -> Span.dummy
| ExprVariant (id, _) -> id.span
and lit_span (lit : literal) : Span.t =
match lit with
| LitInt (_, span) -> span
| LitFloat (_, span) -> span
| LitBool (_, span) -> span
| LitChar (_, span) -> span
| LitString (_, span) -> span
| LitUnit span -> span
and pattern_span (pat : pattern) : Span.t =
match pat with
| PatWildcard span -> span
| PatVar id -> id.span
| PatLit lit -> lit_span lit
| PatCon (id, _) -> id.span
| PatTuple pats ->
begin match pats with
| p :: _ -> pattern_span p
| [] -> Span.dummy
end
| PatRecord ((id, _) :: _, _) -> id.span
| PatRecord ([], _) -> Span.dummy
| PatOr (p1, _) -> pattern_span p1
| PatAs (id, _) -> id.span
(** Lookup a symbol by name, searching all_symbols table *)
(** This is needed because scopes are exited after resolution *)
let lookup_symbol_by_name (symbols : Symbol.t) (name : string) : Symbol.symbol option =
(* Search all_symbols table for most recent symbol with this name *)
let matching_symbols = Hashtbl.fold (fun _id sym acc ->
if sym.Symbol.sym_name = name && sym.Symbol.sym_kind = Symbol.SKVariable then
sym :: acc
else
acc
) symbols.Symbol.all_symbols [] in
(* Sort by symbol ID (descending) to get most recent *)
let sorted_symbols = List.sort (fun a b -> compare b.Symbol.sym_id a.Symbol.sym_id) matching_symbols in
match sorted_symbols with
| sym :: _ -> Some sym
| [] -> None
(** If [expr] creates a reference to a place ([&p] / [&mut p], optionally
wrapped in spans), return that place. Used to build the borrow-graph
edge for [let r = &p]. CORE-01 / #177. *)
let rec ref_target (symbols : Symbol.t) (expr : expr) : place option =
match expr with
| ExprSpan (e, _) -> ref_target symbols e
| ExprUnary ((OpRef | OpMutRef), e) -> expr_to_place symbols e
| _ -> None
(** Convert an expression to a place (if it's an l-value) *)
and expr_to_place (symbols : Symbol.t) (expr : expr) : place option =
match expr with
| ExprVar id ->
begin match lookup_symbol_by_name symbols id.name with
| Some sym -> Some (PlaceVar (id.name, sym.sym_id))
| None -> None
end
| ExprField (base, field) ->
begin match expr_to_place symbols base with
| Some base_place -> Some (PlaceField (base_place, field.name))
| None -> None
end
| ExprIndex (base, _) ->
begin match expr_to_place symbols base with
| Some base_place -> Some (PlaceIndex (base_place, None))
| None -> None
end
| ExprSpan (e, _) ->
expr_to_place symbols e
| _ -> None
(** Look up the live borrow that [expr] denotes, after the expression has
been checked. Handles three cases:
- [&p] / [&mut p]: find the borrow on [p] in [state.borrows];
- [r] where [r] is a ref-binder symbol: return its [state.ref_bindings]
entry (the same borrow it already aliases) — this is ref-to-ref
binding / reborrow through indirection;
- anything else: None.
Used by [record_ref_binding] (let path) and [StmtAssign] (assign path)
so [let r2 = r] and [r2 = r] both extend the borrow-graph correctly.
CORE-01 / #177 ref-to-ref. *)
let rec ref_source_borrow (state : state) (symbols : Symbol.t)
(expr : expr) : borrow option =
match expr with
| ExprSpan (e, _) -> ref_source_borrow state symbols e
| ExprUnary ((OpRef | OpMutRef), _) ->
(match ref_target symbols expr with
| Some target ->
List.find_opt (fun b -> places_overlap b.b_place target) state.borrows
| None -> None)
| ExprVar id ->
(match lookup_symbol_by_name symbols id.name with
| Some sym -> List.assoc_opt sym.Symbol.sym_id state.ref_bindings
| None -> None)
| _ -> None
(** Cheap structural test: would [expr] supply a reborrow source on
[StmtAssign]? Used *before* the RHS check, so it does not consult
live borrow state — only the structural shape of [expr] and the
pre-existing [state.ref_bindings] (for the ref-var path). *)
let rec is_reborrow_source (state : state) (symbols : Symbol.t)
(expr : expr) : bool =
match expr with
| ExprSpan (e, _) -> is_reborrow_source state symbols e
| ExprUnary ((OpRef | OpMutRef), _) -> true
| ExprVar id ->
(match lookup_symbol_by_name symbols id.name with
| Some sym -> List.mem_assoc sym.Symbol.sym_id state.ref_bindings
| None -> false)
| _ -> false
(** Record a borrow-graph edge for [let <pat> = <ref-source>].
[<ref-source>] is either a direct [&p]/[&mut p] *or* another ref-binder
variable [r] (ref-to-ref binding). Call *after* the value expression has
been checked, so the underlying borrow is already on [state.borrows]
(or already aliased via [state.ref_bindings] for the ref-var path).
CORE-01 / #177. *)
let record_ref_binding (state : state) (symbols : Symbol.t)
(pat : pattern) (value : expr) : unit =
match pat with
| PatVar id ->
begin match lookup_symbol_by_name symbols id.name with
| None -> ()
| Some sym ->
let rec peel = function ExprSpan (e, _) -> peel e | e -> e in
begin match peel value with
| ExprApp _ when state.result_borrows <> [] ->
(* [let r = f(a)] where the call result borrows one or more of its
arguments: claim those escaping borrows (left live by the
[ExprApp] handler) as r's ref-graph edges, so NLL last-use expiry
and return-escape treat r exactly like [let r = &a]. #554. *)
List.iter (fun b ->
state.ref_bindings <- (sym.Symbol.sym_id, b) :: state.ref_bindings)
state.result_borrows;
state.result_borrows <- []
| _ ->
begin match ref_source_borrow state symbols value with
| Some b -> state.ref_bindings <- (sym.Symbol.sym_id, b) :: state.ref_bindings
| None -> ()
end
end
end
| _ -> ()
(** The borrow a *returned* expression denotes, if any: either a direct
[&place] / [&mut place], or a reference binder [r] (from [let r = &p])
looked up in the live borrow-graph. Returning a *value* (incl. [*r]) is
not a borrow and yields [None]. CORE-01 pt2 / #177 (return-escape). *)
let returned_borrow (state : state) (symbols : Symbol.t)
(e : expr) : borrow option =
let rec peel = function ExprSpan (x, _) -> peel x | x -> x in
match ref_target symbols e with
| Some target ->
Some (match List.find_opt (fun b ->
places_overlap b.b_place target) state.borrows with
| Some b -> b
| None -> { b_place = target; b_kind = Shared;
b_span = expr_span e; b_id = -1 })
| None ->
(match peel e with
| ExprVar id ->
(match lookup_symbol_by_name symbols id.name with
| Some sym ->
List.assoc_opt sym.Symbol.sym_id state.ref_bindings
| None -> None)
| _ -> None)
(** Return-escape (CORE-01 pt2 / #177): a [return e] (or fn-tail) whose
value is a reference rooted at a *callee-owned* binding — a function
local or a by-value/[own] parameter — dangles once the callee frame is
gone. Mirrors [BorrowOutlivesOwner]'s block-escape rationale, extended
to the return position (the let-only graph in pt1 only saw block tails,
so [return r] slipped through). Sound and non-over-rejecting: returning
a borrow of a dying callee binding is always wrong; valid code returns
the value, or a borrow of a [ref]/[mut] (caller-owned) parameter — the
latter has no callee-owned root and is intentionally not flagged. *)
let check_return_escape (state : state) (symbols : Symbol.t)
(e : expr) : unit result =
match returned_borrow state symbols e with
| None -> Ok ()
| Some b ->
(match root_var b.b_place with
| Some owner
when List.mem owner state.callee_owned_params
|| List.mem owner state.block_local_syms ->
Error (BorrowOutlivesOwner (b, owner))
| _ -> Ok ())
(** NLL last-use pre-pass (CORE-01 pt3 / #177 Slice A).
Returns a map [sym_id -> max statement index at which the symbol is
mentioned anywhere inside [blk] (including nested blocks, lambda
bodies, handler arms, etc.)]. Statement indices are 0-based; the
block's tail expression, if any, is treated as the [n]-th "statement"
(where [n = List.length blk.blk_stmts]).
Used by [check_block] to expire ref-bindings introduced in this block
once their binder is dead, instead of holding them until lexical block
exit. The expiry releases the underlying borrow, so subsequent
statements may legally read or assign through the (no-longer-borrowed)
owner — the canonical Rust-style "non-lexical lifetime" win.
A symbol that never appears is simply absent from the table; callers
treat that as [-1] (last-use precedes the first statement → expire
immediately after the declaration). *)
let compute_last_use_index (symbols : Symbol.t) (blk : block)
: (Symbol.symbol_id, int) Hashtbl.t =
let tbl : (Symbol.symbol_id, int) Hashtbl.t = Hashtbl.create 8 in
let bump (sym : Symbol.symbol_id) (idx : int) : unit =
match Hashtbl.find_opt tbl sym with
| Some cur when cur >= idx -> ()
| _ -> Hashtbl.replace tbl sym idx
in
let mark_var (idx : int) (id : ident) : unit =
match lookup_symbol_by_name symbols id.name with
| Some s -> bump s.Symbol.sym_id idx
| None -> ()
in
let rec visit_expr (idx : int) (e : expr) : unit =
match e with
| ExprVar id -> mark_var idx id
| ExprLit _ | ExprVariant _ -> ()
| ExprSpan (e, _) -> visit_expr idx e
| ExprApp (f, args) ->
visit_expr idx f; List.iter (visit_expr idx) args
| ExprField (b, _) | ExprTupleIndex (b, _) -> visit_expr idx b
| ExprIndex (b, i) -> visit_expr idx b; visit_expr idx i
| ExprTuple es | ExprArray es -> List.iter (visit_expr idx) es
| ExprRecord r ->
List.iter (fun (_, eo) -> match eo with Some e -> visit_expr idx e | None -> ()) r.er_fields;
(match r.er_spread with Some e -> visit_expr idx e | None -> ())
| ExprRowRestrict (e, _) -> visit_expr idx e
| ExprBinary (l, _, r) -> visit_expr idx l; visit_expr idx r
| ExprUnary (_, e) -> visit_expr idx e
| ExprLet lb ->
visit_expr idx lb.el_value;
(match lb.el_body with Some b -> visit_expr idx b | None -> ())
| ExprIf ei ->
visit_expr idx ei.ei_cond; visit_expr idx ei.ei_then;
(match ei.ei_else with Some e -> visit_expr idx e | None -> ())
| ExprMatch em ->
visit_expr idx em.em_scrutinee;
List.iter (fun arm ->
(match arm.ma_guard with Some g -> visit_expr idx g | None -> ());
visit_expr idx arm.ma_body
) em.em_arms
| ExprBlock blk -> visit_block idx blk
| ExprLambda lam -> visit_expr idx lam.elam_body
| ExprReturn (Some e) -> visit_expr idx e
| ExprReturn None -> ()
| ExprBreak _ -> ()
| ExprContinue _ -> ()
| ExprHandle eh ->