feat: retire RC from the emitter and the format — .wob v4 (7b Phase 3b)
The compiler no longer emits reference-counting ops anywhere, and the format reserves them. With Phase 3a's collector this completes the runtime half of iteration 7b: spec success criteria 3 (no RC ops in any image, opcodes reserved) and 6 (corpus ASan-clean) are met — `just oop-accept` is fully green. - owner.ml: the rc machinery is deleted outright — rc_site/rc_op types, the rcs table, fn_rcs/rc_groups/rc_escaped, record_rc, release_gc, gc_escape, resolve_rc, and the clobber rule (its only consumer was elision). The `push`-of-a-gc-value RC_INC special case is gone (the bug class cannot recur without RC). Drop tables (owned + LGc kinds) are untouched — the gc mask is what feeds the collector's root maps. - emit.ml: emit_rc, the v_rc view, the escape-acquire anchor, and every caller deleted; assignment displacing a traced value emits nothing (the VM's store barrier owns it); scope-ended LGc handles clear their gc-mask bit so root maps stay precise. - .wob v4: WOB_VERSION 3 -> 4 in wob.h + emit.ml + disasm.ml + the runner's loader battery; opcodes 27-28 removed from the enum/jump table/interpreter and REJECTED by the loader like any unknown opcode. - dump.ml: the == RC == owner-dump section is gone; 6 goldens re-blessed (owner dumps lose the section, elision.wo's bc dump loses its RC ops). - runner.ml: rc-table/ELIDED assertions deleted; the elision test now asserts the WHOLE image contains no RC op; the table-contract sweep asserts rc ops never appear. - test_unwind.c: the rc-opcodes test becomes two — the loader rejects reserved opcode 27, and an abandoned traced instance is freed by rt_destroy (ASan-proven). Verified: woc-test 540/0 + test_diag 14/0; runtime test + test-iso all suites ASan/UBSan (test_unwind 12/0); cli_smoke; oop-e2e 79/0 (v4 images end to end); employee 8/0; log-watcher 7/0; ring runs + reclaimed (freed=3) with zero RC ops in its image; `just oop-accept` ALL CRITERIA MET. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2c825843f
commit
4a62488bc6
15 changed files with 104 additions and 356 deletions
|
|
@ -140,8 +140,6 @@ let ins_str (i : int) (pc : int) : string =
|
||||||
| 24 -> Printf.sprintf "BORROW_X r%d" a
|
| 24 -> Printf.sprintf "BORROW_X r%d" a
|
||||||
| 25 -> Printf.sprintf "RELEASE_S r%d" a
|
| 25 -> Printf.sprintf "RELEASE_S r%d" a
|
||||||
| 26 -> Printf.sprintf "RELEASE_X r%d" a
|
| 26 -> Printf.sprintf "RELEASE_X r%d" a
|
||||||
| 27 -> Printf.sprintf "RC_INC r%d" a
|
|
||||||
| 28 -> Printf.sprintf "RC_DEC r%d" a
|
|
||||||
| 29 ->
|
| 29 ->
|
||||||
if c = 4 || c = 9 then Printf.sprintf "BUILTIN r%d, kinds=0x%02x, %s" a b (builtin_name c)
|
if c = 4 || c = 9 then Printf.sprintf "BUILTIN r%d, kinds=0x%02x, %s" a b (builtin_name c)
|
||||||
else Printf.sprintf "BUILTIN r%d, r%d, %s" a b (builtin_name c)
|
else Printf.sprintf "BUILTIN r%d, r%d, %s" a b (builtin_name c)
|
||||||
|
|
@ -164,7 +162,7 @@ let dump (img : string) : string =
|
||||||
let line fmt = Buffer.add_string out (fmt ^ "\n") in
|
let line fmt = Buffer.add_string out (fmt ^ "\n") in
|
||||||
if u32 img 0 <> magic then raise (Bad "bad magic");
|
if u32 img 0 <> magic then raise (Bad "bad magic");
|
||||||
let ver = u32 img 4 in
|
let ver = u32 img 4 in
|
||||||
if ver <> 3 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
if ver <> 4 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
||||||
let coff = u32 img 8 and ccnt = u32 img 12 in
|
let coff = u32 img 8 and ccnt = u32 img 12 in
|
||||||
let koff = u32 img 16 and kcnt = u32 img 20 in
|
let koff = u32 img 16 and kcnt = u32 img 20 in
|
||||||
let ioff = u32 img 24 and icnt = u32 img 28 in
|
let ioff = u32 img 24 and icnt = u32 img 28 in
|
||||||
|
|
|
||||||
|
|
@ -446,11 +446,6 @@ let dump_ast (prog : Ast.program) : string =
|
||||||
block; source order is only here to keep the
|
block; source order is only here to keep the
|
||||||
rendering deterministic.
|
rendering deterministic.
|
||||||
|
|
||||||
== RC == "LINE:COL ACQUIRE|RELEASE <place> ELIDED|KEPT" for
|
|
||||||
@gc reference counting. ELIDED marks a pair the
|
|
||||||
emitter may skip because increment and decrement
|
|
||||||
are provably balanced inside one scope.
|
|
||||||
|
|
||||||
== RESIDUAL == "LINE:COL RESIDUAL <op> <place> vs <op> <place>" —
|
== RESIDUAL == "LINE:COL RESIDUAL <op> <place> vs <op> <place>" —
|
||||||
the sites static proof could not settle, so the
|
the sites static proof could not settle, so the
|
||||||
emitter wraps them in runtime borrow ops
|
emitter wraps them in runtime borrow ops
|
||||||
|
|
@ -523,12 +518,6 @@ let dump_owner (t : Owner.tables) : string =
|
||||||
| Owner.DBreak -> Printf.sprintf "%s BREAK %s" pos (drop_items_str d.Owner.dr_items)
|
| Owner.DBreak -> Printf.sprintf "%s BREAK %s" pos (drop_items_str d.Owner.dr_items)
|
||||||
| Owner.DContinue -> Printf.sprintf "%s CONTINUE %s" pos (drop_items_str d.Owner.dr_items)
|
| Owner.DContinue -> Printf.sprintf "%s CONTINUE %s" pos (drop_items_str d.Owner.dr_items)
|
||||||
in
|
in
|
||||||
let rc_line (r : Owner.rc_site) =
|
|
||||||
Printf.sprintf "%s %s %s %s" (owner_pos_str r.Owner.rc_pos)
|
|
||||||
(match r.Owner.rc_op with Owner.RcAcquire -> "ACQUIRE" | Owner.RcRelease -> "RELEASE")
|
|
||||||
r.Owner.rc_place
|
|
||||||
(if r.Owner.rc_elided then "ELIDED" else "KEPT")
|
|
||||||
in
|
|
||||||
let res_line (r : Owner.residual_site) =
|
let res_line (r : Owner.residual_site) =
|
||||||
Printf.sprintf "%s RESIDUAL %s %s vs %s %s" (owner_pos_str r.Owner.rs_pos)
|
Printf.sprintf "%s RESIDUAL %s %s vs %s %s" (owner_pos_str r.Owner.rs_pos)
|
||||||
(acc_kind_str r.Owner.rs_a_kind) r.Owner.rs_a (acc_kind_str r.Owner.rs_b_kind) r.Owner.rs_b
|
(acc_kind_str r.Owner.rs_a_kind) r.Owner.rs_a (acc_kind_str r.Owner.rs_b_kind) r.Owner.rs_b
|
||||||
|
|
@ -537,7 +526,6 @@ let dump_owner (t : Owner.tables) : string =
|
||||||
let lines =
|
let lines =
|
||||||
section "== MOVES ==" (List.map move_line t.Owner.moves)
|
section "== MOVES ==" (List.map move_line t.Owner.moves)
|
||||||
@ section "== DROPS ==" (List.map drop_line t.Owner.drops)
|
@ section "== DROPS ==" (List.map drop_line t.Owner.drops)
|
||||||
@ section "== RC ==" (List.map rc_line t.Owner.rcs)
|
|
||||||
@ section "== RESIDUAL ==" (List.map res_line t.Owner.residuals)
|
@ section "== RESIDUAL ==" (List.map res_line t.Owner.residuals)
|
||||||
in
|
in
|
||||||
String.concat "\n" lines ^ "\n"
|
String.concat "\n" lines ^ "\n"
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
Owner.tables the four ownership tables, verbatim:
|
Owner.tables the four ownership tables, verbatim:
|
||||||
moves -> which MOVEs are real transfers
|
moves -> which MOVEs are real transfers
|
||||||
drops -> DROP placement + drop-table masks
|
drops -> DROP placement + drop-table masks
|
||||||
rcs -> RC_INC / RC_DEC, minus ELIDED pairs
|
|
||||||
residuals -> the ONLY places borrow ops appear
|
residuals -> the ONLY places borrow ops appear
|
||||||
|
|
||||||
Where the drop map is synced is worth stating once: the owner table is
|
Where the drop map is synced is worth stating once: the owner table is
|
||||||
|
|
@ -152,7 +151,7 @@ let stdlib_not_linked_code = Diag.emitter_prefix ^ "06"
|
||||||
============================================================ *)
|
============================================================ *)
|
||||||
|
|
||||||
let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
|
let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
|
||||||
let wob_version = 3 (* v3: v2 + per-class secondary-index metadata *)
|
let wob_version = 4 (* v4 (iteration 7b): RC opcodes retired; gc mask = GC roots *)
|
||||||
let wob_hdr_size = 44
|
let wob_hdr_size = 44
|
||||||
let wob_none = 0xFFFFFFFF
|
let wob_none = 0xFFFFFFFF
|
||||||
let k_int = 0
|
let k_int = 0
|
||||||
|
|
@ -187,8 +186,7 @@ let op_borrow_s = 23
|
||||||
let op_borrow_x = 24
|
let op_borrow_x = 24
|
||||||
let op_release_s = 25
|
let op_release_s = 25
|
||||||
let op_release_x = 26
|
let op_release_x = 26
|
||||||
let op_rc_inc = 27
|
(* opcodes 27-28 (RC_INC/RC_DEC) retired in v4 — reserved, never emitted *)
|
||||||
let op_rc_dec = 28
|
|
||||||
let op_builtin = 29
|
let op_builtin = 29
|
||||||
let op_db_stub = 30
|
let op_db_stub = 30
|
||||||
|
|
||||||
|
|
@ -567,7 +565,6 @@ type views = {
|
||||||
This is how the emitter learns which locals the frame destroys
|
This is how the emitter learns which locals the frame destroys
|
||||||
without re-deriving owner.ml's own "holds" decision. *)
|
without re-deriving owner.ml's own "holds" decision. *)
|
||||||
v_holder : (int, Owner.local_kind) Hashtbl.t;
|
v_holder : (int, Owner.local_kind) Hashtbl.t;
|
||||||
v_rc : (int, Owner.rc_site list) Hashtbl.t; (* by rc_node *)
|
|
||||||
(* region node -> the region's position and its per-operand coalesced
|
(* region node -> the region's position and its per-operand coalesced
|
||||||
guards *)
|
guards *)
|
||||||
v_res : (int, Ast.pos * (int * Owner.acc_kind) list) Hashtbl.t;
|
v_res : (int, Ast.pos * (int * Owner.acc_kind) list) Hashtbl.t;
|
||||||
|
|
@ -585,7 +582,7 @@ let build_views (t : Owner.tables) : views =
|
||||||
{ v_move = Hashtbl.create 16; v_scope = Hashtbl.create 16; v_join = Hashtbl.create 16;
|
{ v_move = Hashtbl.create 16; v_scope = Hashtbl.create 16; v_join = Hashtbl.create 16;
|
||||||
v_return = Hashtbl.create 16; v_break = Hashtbl.create 16; v_continue = Hashtbl.create 16;
|
v_return = Hashtbl.create 16; v_break = Hashtbl.create 16; v_continue = Hashtbl.create 16;
|
||||||
v_overwrite = Hashtbl.create 16; v_mask = Hashtbl.create 16;
|
v_overwrite = Hashtbl.create 16; v_mask = Hashtbl.create 16;
|
||||||
v_holder = Hashtbl.create 16; v_rc = Hashtbl.create 16; v_res = Hashtbl.create 16;
|
v_holder = Hashtbl.create 16; v_res = Hashtbl.create 16;
|
||||||
v_res_used = Hashtbl.create 16 }
|
v_res_used = Hashtbl.create 16 }
|
||||||
in
|
in
|
||||||
List.iter
|
List.iter
|
||||||
|
|
@ -606,11 +603,6 @@ let build_views (t : Owner.tables) : views =
|
||||||
| Owner.DOverwrite -> Hashtbl.replace v.v_overwrite d.Owner.dr_node ()
|
| Owner.DOverwrite -> Hashtbl.replace v.v_overwrite d.Owner.dr_node ()
|
||||||
| Owner.DLiveMask -> Hashtbl.replace v.v_mask d.Owner.dr_node items)
|
| Owner.DLiveMask -> Hashtbl.replace v.v_mask d.Owner.dr_node items)
|
||||||
t.Owner.drops;
|
t.Owner.drops;
|
||||||
List.iter
|
|
||||||
(fun (r : Owner.rc_site) ->
|
|
||||||
let prev = try Hashtbl.find v.v_rc r.Owner.rc_node with Not_found -> [] in
|
|
||||||
Hashtbl.replace v.v_rc r.Owner.rc_node (prev @ [ r ]))
|
|
||||||
t.Owner.rcs;
|
|
||||||
(* Guard coalescing, per dump.ml's normative note: one entry per
|
(* Guard coalescing, per dump.ml's normative note: one entry per
|
||||||
(region, operand) with the strongest access kind, never one pair per
|
(region, operand) with the strongest access kind, never one pair per
|
||||||
table entry. AExcl outranks AShared; a move is never a residual
|
table entry. AExcl outranks AShared; a move is never a residual
|
||||||
|
|
@ -1235,9 +1227,10 @@ let emit_drops (p : pctx) (f : fstate) (items : Owner.drop_item list) : unit =
|
||||||
put f (ins_abc op_drop r 0 0);
|
put f (ins_abc op_drop r 0 0);
|
||||||
mask_clear f r
|
mask_clear f r
|
||||||
| Owner.LGc ->
|
| Owner.LGc ->
|
||||||
(* a @gc handle's release is an rc site, never a DROP: the RC
|
(* a traced handle's death emits nothing — tracing owns the
|
||||||
table carries it (with its own ELIDED decision) *)
|
lifetime (7b). Clearing its gc-mask bit keeps the root maps
|
||||||
()))
|
precise: a scope-ended handle must not pin garbage. *)
|
||||||
|
mask_clear f r))
|
||||||
items
|
items
|
||||||
|
|
||||||
let emit_scope_drops (p : pctx) (f : fstate) (v : views) ~(node : int) ~(label : string) : unit =
|
let emit_scope_drops (p : pctx) (f : fstate) (v : views) ~(node : int) ~(label : string) : unit =
|
||||||
|
|
@ -1258,37 +1251,6 @@ let emit_join_drops (p : pctx) (f : fstate) (v : views) ~(node : int) ~(label :
|
||||||
| Some items -> emit_drops p f items
|
| Some items -> emit_drops p f items
|
||||||
| None -> ()
|
| None -> ()
|
||||||
|
|
||||||
(* RC sites, minus the ELIDED ones — the spec's zero-cost promise lives
|
|
||||||
here and in the residual-only borrow rule. `which` selects acquires or
|
|
||||||
releases; a return site carries both plus the returned value's own
|
|
||||||
escape increment, and acquires must precede releases or a balanced
|
|
||||||
pair could momentarily reach rc 0. *)
|
|
||||||
let emit_rc (p : pctx) (f : fstate) (v : views) ~(node : int) ~(acquire : bool)
|
|
||||||
?(groups : int list option) () : unit =
|
|
||||||
match Hashtbl.find_opt v.v_rc node with
|
|
||||||
| None -> ()
|
|
||||||
| Some sites ->
|
|
||||||
List.iter
|
|
||||||
(fun (r : Owner.rc_site) ->
|
|
||||||
let want = match r.Owner.rc_op with Owner.RcAcquire -> true | Owner.RcRelease -> false in
|
|
||||||
let in_scope =
|
|
||||||
match groups with None -> true | Some gs -> List.mem r.Owner.rc_group gs
|
|
||||||
in
|
|
||||||
if want = acquire && in_scope && not r.Owner.rc_elided then
|
|
||||||
let reg =
|
|
||||||
if r.Owner.rc_group >= 0 then Hashtbl.find_opt f.f_decl r.Owner.rc_group
|
|
||||||
else Hashtbl.find_opt f.f_node r.Owner.rc_node
|
|
||||||
in
|
|
||||||
match reg with
|
|
||||||
| Some g ->
|
|
||||||
put f (ins_abc (if acquire then op_rc_inc else op_rc_dec) g 0 0);
|
|
||||||
if not acquire then mask_clear f g
|
|
||||||
| None ->
|
|
||||||
err p ~code:unguardable_code ~file:f.f_file ~pos:r.Owner.rc_pos
|
|
||||||
~message:
|
|
||||||
(Printf.sprintf "rc site for `%s` has no register in `%s`" r.Owner.rc_place f.f_fn))
|
|
||||||
sites
|
|
||||||
|
|
||||||
(* The frame's drop map at a call / DB_STUB site. The owner table is
|
(* The frame's drop map at a call / DB_STUB site. The owner table is
|
||||||
authoritative here (it is taken after the call's own argument
|
authoritative here (it is taken after the call's own argument
|
||||||
transfers, so a value moved in is the callee's responsibility); an
|
transfers, so a value moved in is the callee's responsibility); an
|
||||||
|
|
@ -1827,16 +1789,6 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
||||||
put f (ins_abc op_db_stub 0 0 0)
|
put f (ins_abc op_db_stub 0 0 0)
|
||||||
| Switch (subject, arms) -> emit_switch p f v e ~dst subject arms);
|
| Switch (subject, arms) -> emit_switch p f v e ~dst subject arms);
|
||||||
Hashtbl.replace f.f_node e.id dst;
|
Hashtbl.replace f.f_node e.id dst;
|
||||||
(* An escaping @gc value takes its increment right where the value
|
|
||||||
lands. owner.ml's gc_escape anchors that acquire on the *place
|
|
||||||
expression's* own node and gives it group -1 (never elidable), and
|
|
||||||
it fires for all four escapes alike: a constructor field, an
|
|
||||||
assignment into a field, a `take` argument, and a return. Emitting
|
|
||||||
it here — once, at the one place every expression passes through —
|
|
||||||
is what keeps all four in step; anchoring it per statement kind is
|
|
||||||
how the constructor-field case went missing. *)
|
|
||||||
emit_rc p f v ~node:e.id ~acquire:true ()
|
|
||||||
|
|
||||||
(* An operand that only needs to *be* in some register: a place already
|
(* An operand that only needs to *be* in some register: a place already
|
||||||
living in one is used where it is, everything else lands in a fresh
|
living in one is used where it is, everything else lands in a fresh
|
||||||
temporary. This is what keeps a proven method's disassembly free of
|
temporary. This is what keeps a proven method's disassembly free of
|
||||||
|
|
@ -1861,7 +1813,6 @@ and emit_tail (p : pctx) (f : fstate) (v : views) (e : Ast.expr) : int =
|
||||||
| Ident n when lookup_local f n <> None ->
|
| Ident n when lookup_local f n <> None ->
|
||||||
let r = match lookup_local f n with Some (r, _) -> r | None -> 0 in
|
let r = match lookup_local f n with Some (r, _) -> r | None -> 0 in
|
||||||
Hashtbl.replace f.f_node e.id r;
|
Hashtbl.replace f.f_node e.id r;
|
||||||
emit_rc p f v ~node:e.id ~acquire:true ();
|
|
||||||
r
|
r
|
||||||
| _ ->
|
| _ ->
|
||||||
(* allocate (so the register counts towards the budget and the
|
(* allocate (so the register counts towards the budget and the
|
||||||
|
|
@ -2300,7 +2251,6 @@ and emit_switch ?(want_value = true) (p : pctx) (f : fstate) (v : views) (e : As
|
||||||
| None -> ())
|
| None -> ())
|
||||||
| _ -> emit_stmt p f v last));
|
| _ -> emit_stmt p f v last));
|
||||||
emit_scope_drops p f v ~node:e.id ~label;
|
emit_scope_drops p f v ~node:e.id ~label;
|
||||||
emit_rc p f v ~node:e.id ~acquire:false ~groups:(declared_since f saved_decls) ();
|
|
||||||
f.f_nlocals <- saved_locals;
|
f.f_nlocals <- saved_locals;
|
||||||
f.f_env <- saved_env;
|
f.f_env <- saved_env;
|
||||||
f.f_declared <- saved_decls;
|
f.f_declared <- saved_decls;
|
||||||
|
|
@ -2400,7 +2350,6 @@ and emit_try (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : Ast
|
||||||
| None -> emit_expr p f v ~dst ve)
|
| None -> emit_expr p f v ~dst ve)
|
||||||
| _ -> emit_stmt p f v last));
|
| _ -> emit_stmt p f v last));
|
||||||
emit_scope_drops p f v ~node:e.id ~label:"CATCH";
|
emit_scope_drops p f v ~node:e.id ~label:"CATCH";
|
||||||
emit_rc p f v ~node:e.id ~acquire:false ~groups:(declared_since f saved_decls) ();
|
|
||||||
f.f_nlocals <- saved_locals;
|
f.f_nlocals <- saved_locals;
|
||||||
f.f_env <- saved_env;
|
f.f_env <- saved_env;
|
||||||
f.f_declared <- saved_decls;
|
f.f_declared <- saved_decls;
|
||||||
|
|
@ -3560,8 +3509,7 @@ and emit_stmt_body (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit =
|
||||||
| None -> ());
|
| None -> ());
|
||||||
(match Hashtbl.find_opt v.v_holder s.s_id with
|
(match Hashtbl.find_opt v.v_holder s.s_id with
|
||||||
| Some kind -> mask_set f kind r
|
| Some kind -> mask_set f kind r
|
||||||
| None -> ());
|
| None -> ())
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:true ()
|
|
||||||
| Assign { target; value } -> emit_assign p f v s target value
|
| Assign { target; value } -> emit_assign p f v s target value
|
||||||
| ExprStmt ({ kind = Ast.Switch (subj, arms); _ } as e) ->
|
| ExprStmt ({ kind = Ast.Switch (subj, arms); _ } as e) ->
|
||||||
(* Task 4 fix round 1: the one place a switch's value is DISCARDED —
|
(* Task 4 fix round 1: the one place a switch's value is DISCARDED —
|
||||||
|
|
@ -3577,7 +3525,6 @@ and emit_stmt_body (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit =
|
||||||
f.f_cur_line <- e.pos.line;
|
f.f_cur_line <- e.pos.line;
|
||||||
emit_switch ~want_value:false p f v e ~dst:t subj arms;
|
emit_switch ~want_value:false p f v e ~dst:t subj arms;
|
||||||
Hashtbl.replace f.f_node e.id t;
|
Hashtbl.replace f.f_node e.id t;
|
||||||
emit_rc p f v ~node:e.id ~acquire:true ();
|
|
||||||
(match Hashtbl.find_opt v.v_move e.id with
|
(match Hashtbl.find_opt v.v_move e.id with
|
||||||
| Some place -> ( match lookup_local f place with Some (sr, _) -> mask_clear f sr | None -> ())
|
| Some place -> ( match lookup_local f place with Some (sr, _) -> mask_clear f sr | None -> ())
|
||||||
| None -> ())
|
| None -> ())
|
||||||
|
|
@ -3616,17 +3563,9 @@ and emit_stmt_body (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit =
|
||||||
and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast.expr)
|
and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast.expr)
|
||||||
(value : Ast.expr) : unit =
|
(value : Ast.expr) : unit =
|
||||||
let overwrite = Hashtbl.mem v.v_overwrite s.s_id in
|
let overwrite = Hashtbl.mem v.v_overwrite s.s_id in
|
||||||
(* a @gc value the assignment displaces is released, not dropped: the
|
(* iteration 7b: a traced value an assignment displaces needs nothing —
|
||||||
RC table carries that RELEASE at the assignment's own node *)
|
tracing owns its lifetime (the VM's store barrier shades it while a
|
||||||
let releases =
|
mark is live). Only OVERWRITE (owned) entries lower to a drop. *)
|
||||||
match Hashtbl.find_opt v.v_rc s.s_id with
|
|
||||||
| None -> false
|
|
||||||
| Some sites ->
|
|
||||||
List.exists
|
|
||||||
(fun (r : Owner.rc_site) ->
|
|
||||||
r.Owner.rc_op = Owner.RcRelease && not r.Owner.rc_elided)
|
|
||||||
sites
|
|
||||||
in
|
|
||||||
match target.kind with
|
match target.kind with
|
||||||
| Ident n -> (
|
| Ident n -> (
|
||||||
match lookup_local f n with
|
match lookup_local f n with
|
||||||
|
|
@ -3635,7 +3574,7 @@ and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast
|
||||||
~message:(Printf.sprintf "assignment to `%s`, which is not a local or parameter" n)
|
~message:(Printf.sprintf "assignment to `%s`, which is not a local or parameter" n)
|
||||||
| Some (r, ty) ->
|
| Some (r, ty) ->
|
||||||
Hashtbl.replace f.f_node target.id r;
|
Hashtbl.replace f.f_node target.id r;
|
||||||
if overwrite || releases then begin
|
if overwrite then begin
|
||||||
(* the replaced value dies here (the owner table's OVERWRITE or
|
(* the replaced value dies here (the owner table's OVERWRITE or
|
||||||
RELEASE entry); compute the new one into a temporary first so
|
RELEASE entry); compute the new one into a temporary first so
|
||||||
destroying the old one cannot destroy what is about to be
|
destroying the old one cannot destroy what is about to be
|
||||||
|
|
@ -3649,14 +3588,8 @@ and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast
|
||||||
the source can live inside what is about to be dropped. *)
|
the source can live inside what is about to be dropped. *)
|
||||||
copy_place_text p f t value;
|
copy_place_text p f t value;
|
||||||
f.f_cur_line <- s.s_pos.line;
|
f.f_cur_line <- s.s_pos.line;
|
||||||
if overwrite then begin
|
put f (ins_abc op_drop r 0 0);
|
||||||
put f (ins_abc op_drop r 0 0);
|
mask_clear f r;
|
||||||
mask_clear f r
|
|
||||||
end;
|
|
||||||
if releases then begin
|
|
||||||
Hashtbl.replace f.f_node s.s_id r;
|
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ()
|
|
||||||
end;
|
|
||||||
put f (ins_abc op_move r t 0)
|
put f (ins_abc op_move r t 0)
|
||||||
end
|
end
|
||||||
else begin
|
else begin
|
||||||
|
|
@ -3715,18 +3648,15 @@ and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast
|
||||||
let b = emit_operand p f v base in
|
let b = emit_operand p f v base in
|
||||||
Hashtbl.replace f.f_node target.id b;
|
Hashtbl.replace f.f_node target.id b;
|
||||||
let idx = check_field_idx p f target.pos idx in
|
let idx = check_field_idx p f target.pos idx in
|
||||||
if overwrite || releases then begin
|
if overwrite then begin
|
||||||
(* SETF never auto-drops (format doc): the compiler emits
|
(* SETF never auto-drops (format doc): the compiler emits the
|
||||||
the destruction of the field's previous value — a DROP
|
destruction of the field's previous OWNED value. A traced
|
||||||
for an owned field, an rc release for a @gc one *)
|
old value needs nothing here — tracing owns its lifetime
|
||||||
|
(the VM's SETF barrier shades it while a mark is live). *)
|
||||||
let old = alloc_temp p f target.pos in
|
let old = alloc_temp p f target.pos in
|
||||||
f.f_cur_line <- s.s_pos.line;
|
f.f_cur_line <- s.s_pos.line;
|
||||||
put f (ins_abc op_getf old b idx);
|
put f (ins_abc op_getf old b idx);
|
||||||
if overwrite then put f (ins_abc op_drop old 0 0);
|
put f (ins_abc op_drop old 0 0)
|
||||||
if releases then begin
|
|
||||||
Hashtbl.replace f.f_node s.s_id old;
|
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ()
|
|
||||||
end
|
|
||||||
end;
|
end;
|
||||||
let t = alloc_temp p f value.pos in
|
let t = alloc_temp p f value.pos in
|
||||||
emit_expr p f v ~dst:t ~expected:fty value;
|
emit_expr p f v ~dst:t ~expected:fty value;
|
||||||
|
|
@ -3803,10 +3733,8 @@ and emit_assign (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (target : Ast
|
||||||
and emit_return (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (opt : Ast.expr option) : unit =
|
and emit_return (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (opt : Ast.expr option) : unit =
|
||||||
match opt with
|
match opt with
|
||||||
| None ->
|
| None ->
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:true ();
|
|
||||||
(match Hashtbl.find_opt v.v_return s.s_id with Some items -> emit_drops p f items | None -> ());
|
(match Hashtbl.find_opt v.v_return s.s_id with Some items -> emit_drops p f items | None -> ());
|
||||||
List.iter (fun r -> put f (ins_abc op_drop r 0 0)) f.f_esc_drops;
|
List.iter (fun r -> put f (ins_abc op_drop r 0 0)) f.f_esc_drops;
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ();
|
|
||||||
f.f_cur_line <- s.s_pos.line;
|
f.f_cur_line <- s.s_pos.line;
|
||||||
put f (ins_abc op_ret0 0 0 0);
|
put f (ins_abc op_ret0 0 0 0);
|
||||||
f.f_div <- true
|
f.f_div <- true
|
||||||
|
|
@ -3833,13 +3761,8 @@ and emit_return (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (opt : Ast.ex
|
||||||
(match Hashtbl.find_opt v.v_move e.id with
|
(match Hashtbl.find_opt v.v_move e.id with
|
||||||
| Some place -> ( match lookup_local f place with Some (sr, _) -> mask_clear f sr | None -> ())
|
| Some place -> ( match lookup_local f place with Some (sr, _) -> mask_clear f sr | None -> ())
|
||||||
| None -> ());
|
| None -> ());
|
||||||
(* the escaping value's own increment was emitted where the value
|
|
||||||
landed (emit_expr / emit_tail), which is before the frame's
|
|
||||||
releases below — a balanced pair must never reach rc 0 in between *)
|
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:true ();
|
|
||||||
(match Hashtbl.find_opt v.v_return s.s_id with Some items -> emit_drops p f items | None -> ());
|
(match Hashtbl.find_opt v.v_return s.s_id with Some items -> emit_drops p f items | None -> ());
|
||||||
List.iter (fun r -> if r <> t then put f (ins_abc op_drop r 0 0)) f.f_esc_drops;
|
List.iter (fun r -> if r <> t then put f (ins_abc op_drop r 0 0)) f.f_esc_drops;
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ();
|
|
||||||
f.f_cur_line <- s.s_pos.line;
|
f.f_cur_line <- s.s_pos.line;
|
||||||
put f (ins_abc op_ret t 0 0);
|
put f (ins_abc op_ret t 0 0);
|
||||||
f.f_div <- true
|
f.f_div <- true
|
||||||
|
|
@ -3861,9 +3784,7 @@ and emit_break (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit =
|
||||||
| [] ->
|
| [] ->
|
||||||
err p ~code:cannot_lower_code ~file:f.f_file ~pos:s.s_pos ~message:"`break` outside of a loop"
|
err p ~code:cannot_lower_code ~file:f.f_file ~pos:s.s_pos ~message:"`break` outside of a loop"
|
||||||
| lf :: _ ->
|
| lf :: _ ->
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:true ();
|
|
||||||
(match Hashtbl.find_opt v.v_break s.s_id with Some items -> emit_drops p f items | None -> ());
|
(match Hashtbl.find_opt v.v_break s.s_id with Some items -> emit_drops p f items | None -> ());
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ();
|
|
||||||
f.f_cur_line <- s.s_pos.line;
|
f.f_cur_line <- s.s_pos.line;
|
||||||
let pc = here f in
|
let pc = here f in
|
||||||
put f (ins_asbx op_jmp 0 0);
|
put f (ins_asbx op_jmp 0 0);
|
||||||
|
|
@ -3876,9 +3797,7 @@ and emit_continue (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit =
|
||||||
err p ~code:cannot_lower_code ~file:f.f_file ~pos:s.s_pos
|
err p ~code:cannot_lower_code ~file:f.f_file ~pos:s.s_pos
|
||||||
~message:"`continue` outside of a loop"
|
~message:"`continue` outside of a loop"
|
||||||
| lf :: _ ->
|
| lf :: _ ->
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:true ();
|
|
||||||
(match Hashtbl.find_opt v.v_continue s.s_id with Some items -> emit_drops p f items | None -> ());
|
(match Hashtbl.find_opt v.v_continue s.s_id with Some items -> emit_drops p f items | None -> ());
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ();
|
|
||||||
f.f_cur_line <- s.s_pos.line;
|
f.f_cur_line <- s.s_pos.line;
|
||||||
let pc = here f in
|
let pc = here f in
|
||||||
put f (ins_asbx op_jmp 0 0);
|
put f (ins_asbx op_jmp 0 0);
|
||||||
|
|
@ -3891,10 +3810,8 @@ and emit_block (p : pctx) (f : fstate) (v : views) ~(node : int) ~(label : strin
|
||||||
let saved_env = f.f_env in
|
let saved_env = f.f_env in
|
||||||
let saved_decls = f.f_declared in
|
let saved_decls = f.f_declared in
|
||||||
List.iter (emit_stmt p f v) body;
|
List.iter (emit_stmt p f v) body;
|
||||||
(* scope end: the owner table's DROPs first, then the @gc releases for
|
(* scope end: the owner table's DROPs, owner.ml's own pop_scope order *)
|
||||||
the handles this block declared — owner.ml's own pop_scope order *)
|
|
||||||
emit_scope_drops p f v ~node ~label;
|
emit_scope_drops p f v ~node ~label;
|
||||||
emit_rc p f v ~node ~acquire:false ~groups:(declared_since f saved_decls) ();
|
|
||||||
f.f_nlocals <- saved_locals;
|
f.f_nlocals <- saved_locals;
|
||||||
f.f_env <- saved_env;
|
f.f_env <- saved_env;
|
||||||
f.f_declared <- saved_decls;
|
f.f_declared <- saved_decls;
|
||||||
|
|
@ -4049,7 +3966,6 @@ and emit_for (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (var : string)
|
||||||
List.iter (emit_stmt p f v) body;
|
List.iter (emit_stmt p f v) body;
|
||||||
f.f_loops <- List.tl f.f_loops;
|
f.f_loops <- List.tl f.f_loops;
|
||||||
emit_scope_drops p f v ~node:s.s_id ~label:"FOR";
|
emit_scope_drops p f v ~node:s.s_id ~label:"FOR";
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ~groups:(declared_since f saved_decls) ();
|
|
||||||
let continue_target = here f in
|
let continue_target = here f in
|
||||||
List.iter (fun pc -> patch_jump p f ~file:f.f_file ~pos:s.s_pos pc continue_target) lf.lf_continues;
|
List.iter (fun pc -> patch_jump p f ~file:f.f_file ~pos:s.s_pos pc continue_target) lf.lf_continues;
|
||||||
f.f_temp <- f.f_nlocals;
|
f.f_temp <- f.f_nlocals;
|
||||||
|
|
@ -4112,7 +4028,6 @@ and emit_for (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (var : string)
|
||||||
List.iter (emit_stmt p f v) body;
|
List.iter (emit_stmt p f v) body;
|
||||||
f.f_loops <- List.tl f.f_loops;
|
f.f_loops <- List.tl f.f_loops;
|
||||||
emit_scope_drops p f v ~node:s.s_id ~label:"FOR";
|
emit_scope_drops p f v ~node:s.s_id ~label:"FOR";
|
||||||
emit_rc p f v ~node:s.s_id ~acquire:false ~groups:(declared_since f saved_decls) ();
|
|
||||||
(* haxe-parity Task 2: `continue` re-enters right here — after this
|
(* haxe-parity Task 2: `continue` re-enters right here — after this
|
||||||
iteration's own scope-end cleanup (a `continue` already ran the
|
iteration's own scope-end cleanup (a `continue` already ran the
|
||||||
equivalent of it at its own site, from v_continue — see
|
equivalent of it at its own site, from v_continue — see
|
||||||
|
|
@ -4249,7 +4164,6 @@ let emit_method (p : pctx) (v : views) ~(file : string) ~(self_class : (int * st
|
||||||
stmt_reset f;
|
stmt_reset f;
|
||||||
f.f_cur_line <- m.pos.line;
|
f.f_cur_line <- m.pos.line;
|
||||||
emit_scope_drops p f v ~node:m.id ~label:"BODY";
|
emit_scope_drops p f v ~node:m.id ~label:"BODY";
|
||||||
emit_rc p f v ~node:m.id ~acquire:false ~groups:(declared_since f []) ();
|
|
||||||
(* the terminator rule: the loader rejects a method whose last
|
(* the terminator rule: the loader rejects a method whose last
|
||||||
instruction is not one, and the implicit void return is what
|
instruction is not one, and the implicit void return is what
|
||||||
control falling off the end means *)
|
control falling off the end means *)
|
||||||
|
|
|
||||||
|
|
@ -277,22 +277,9 @@ type drop_site = {
|
||||||
dr_items : drop_item list;
|
dr_items : drop_item list;
|
||||||
}
|
}
|
||||||
|
|
||||||
type rc_op =
|
(* iteration 7b: the rc-site machinery (acquire/release pairs, elision
|
||||||
| RcAcquire
|
groups, the clobber rule) is gone with reference counting itself — a
|
||||||
| RcRelease
|
traced value's aliases need no bookkeeping, tracing owns the lifetime. *)
|
||||||
|
|
||||||
(* rc_group ties a binding's ACQUIRE to its RELEASE(s): both are elided
|
|
||||||
together when the pair is provably balanced inside one scope. Escape
|
|
||||||
sites (a @gc value returned, stored, or moved into a `take`) use group
|
|
||||||
-1 — an increment that outlives the scope can never be elided. *)
|
|
||||||
type rc_site = {
|
|
||||||
rc_node : int;
|
|
||||||
rc_pos : Ast.pos;
|
|
||||||
rc_op : rc_op;
|
|
||||||
rc_place : string;
|
|
||||||
rc_group : int;
|
|
||||||
mutable rc_elided : bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
type acc_kind =
|
type acc_kind =
|
||||||
| AShared
|
| AShared
|
||||||
|
|
@ -319,7 +306,6 @@ type residual_site = {
|
||||||
type tables = {
|
type tables = {
|
||||||
moves : move_site list;
|
moves : move_site list;
|
||||||
drops : drop_site list;
|
drops : drop_site list;
|
||||||
rcs : rc_site list;
|
|
||||||
residuals : residual_site list;
|
residuals : residual_site list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,7 +354,6 @@ type scope = {
|
||||||
type sink = {
|
type sink = {
|
||||||
mutable s_moves : move_site list;
|
mutable s_moves : move_site list;
|
||||||
mutable s_drops : drop_site list;
|
mutable s_drops : drop_site list;
|
||||||
mutable s_rcs : rc_site list;
|
|
||||||
mutable s_res : residual_site list;
|
mutable s_res : residual_site list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,14 +378,6 @@ type ctx = {
|
||||||
(* set when the current path has returned; a diverged path contributes
|
(* set when the current path has returned; a diverged path contributes
|
||||||
no scope-end drops and drops out of if/else joins *)
|
no scope-end drops and drops out of if/else joins *)
|
||||||
mutable diverged : bool;
|
mutable diverged : bool;
|
||||||
(* rc bookkeeping, resolved into rc_elided at the end of the function *)
|
|
||||||
mutable fn_rcs : rc_site list;
|
|
||||||
rc_groups : (int, string option) Hashtbl.t; (* group -> source root, None = fresh allocation *)
|
|
||||||
rc_escaped : (int, unit) Hashtbl.t;
|
|
||||||
(* roots that are assigned to, or exclusively borrowed, anywhere in
|
|
||||||
this function — an rc pair whose source root is clobbered cannot be
|
|
||||||
elided, because the original reference may die inside the scope *)
|
|
||||||
clobbered : (string, unit) Hashtbl.t;
|
|
||||||
(* iteration 7b demand-promotion (Gcinfer): when Some, the pass runs in
|
(* iteration 7b demand-promotion (Gcinfer): when Some, the pass runs in
|
||||||
collect mode — a class value that would fail the escape rule records its
|
collect mode — a class value that would fail the escape rule records its
|
||||||
class here instead of raising WO-E304, so inference can promote it to
|
class here instead of raising WO-E304, so inference can promote it to
|
||||||
|
|
@ -860,13 +837,6 @@ let record_drop (ctx : ctx) ~node ~(pos : Ast.pos) ~kind ~items : unit =
|
||||||
ctx.sink.s_drops <-
|
ctx.sink.s_drops <-
|
||||||
{ dr_node = node; dr_pos = pos; dr_kind = kind; dr_items = items } :: ctx.sink.s_drops
|
{ dr_node = node; dr_pos = pos; dr_kind = kind; dr_items = items } :: ctx.sink.s_drops
|
||||||
|
|
||||||
let record_rc (ctx : ctx) ~node ~(pos : Ast.pos) ~op ~place ~group : unit =
|
|
||||||
if ctx.recording then
|
|
||||||
ctx.fn_rcs <-
|
|
||||||
{ rc_node = node; rc_pos = pos; rc_op = op; rc_place = place; rc_group = group;
|
|
||||||
rc_elided = false }
|
|
||||||
:: ctx.fn_rcs
|
|
||||||
|
|
||||||
let record_residual (ctx : ctx) ~node ~(pos : Ast.pos) ~(a : place) ~a_kind ~(b : place) ~b_kind :
|
let record_residual (ctx : ctx) ~node ~(pos : Ast.pos) ~(a : place) ~a_kind ~(b : place) ~b_kind :
|
||||||
unit =
|
unit =
|
||||||
if ctx.recording then
|
if ctx.recording then
|
||||||
|
|
@ -879,8 +849,6 @@ let record_residual (ctx : ctx) ~node ~(pos : Ast.pos) ~(a : place) ~a_kind ~(b
|
||||||
rs_b_node = b.pnode }
|
rs_b_node = b.pnode }
|
||||||
:: ctx.sink.s_res
|
:: ctx.sink.s_res
|
||||||
|
|
||||||
let clobber (ctx : ctx) (root : string) : unit = Hashtbl.replace ctx.clobbered root ()
|
|
||||||
|
|
||||||
(* ============================================================
|
(* ============================================================
|
||||||
Scopes, live sets, snapshots
|
Scopes, live sets, snapshots
|
||||||
============================================================ *)
|
============================================================ *)
|
||||||
|
|
@ -929,14 +897,6 @@ let mask_items (ls : local list) : drop_item list =
|
||||||
{ di_name = l.l_name; di_kind = (if l.l_class = Gc then LGc else LOwned); di_node = l.l_node })
|
{ di_name = l.l_name; di_kind = (if l.l_class = Gc then LGc else LOwned); di_node = l.l_node })
|
||||||
ls
|
ls
|
||||||
|
|
||||||
(* Releases for the @gc handles a scope exit destroys. *)
|
|
||||||
let release_gc (ctx : ctx) ~node ~pos (ls : local list) : unit =
|
|
||||||
List.iter
|
|
||||||
(fun l ->
|
|
||||||
if l.l_class = Gc then
|
|
||||||
record_rc ctx ~node ~pos ~op:RcRelease ~place:l.l_name ~group:l.l_node)
|
|
||||||
ls
|
|
||||||
|
|
||||||
let pop_scope (ctx : ctx) : unit =
|
let pop_scope (ctx : ctx) : unit =
|
||||||
match ctx.scopes with
|
match ctx.scopes with
|
||||||
| [] -> ()
|
| [] -> ()
|
||||||
|
|
@ -944,8 +904,7 @@ let pop_scope (ctx : ctx) : unit =
|
||||||
if not ctx.diverged then begin
|
if not ctx.diverged then begin
|
||||||
let live = List.filter is_live_holder sc.sc_locals in
|
let live = List.filter is_live_holder sc.sc_locals in
|
||||||
record_drop ctx ~node:sc.sc_node ~pos:sc.sc_pos ~kind:(DScope sc.sc_label)
|
record_drop ctx ~node:sc.sc_node ~pos:sc.sc_pos ~kind:(DScope sc.sc_label)
|
||||||
~items:(owned_items live);
|
~items:(owned_items live)
|
||||||
release_gc ctx ~node:sc.sc_node ~pos:sc.sc_pos live
|
|
||||||
end;
|
end;
|
||||||
ctx.scopes <- rest
|
ctx.scopes <- rest
|
||||||
|
|
||||||
|
|
@ -1087,16 +1046,6 @@ let escape (ctx : ctx) (l : local) ~(pos : Ast.pos) ~message
|
||||||
report ctx ~code:borrow_escape_code ~pos ~message ~rel:l.l_pos
|
report ctx ~code:borrow_escape_code ~pos ~message ~rel:l.l_pos
|
||||||
~label:(borrow_label l)
|
~label:(borrow_label l)
|
||||||
|
|
||||||
(* A @gc value reaching a location that outlives this scope: one
|
|
||||||
increment at the escape site, never elidable. If the escaping value is
|
|
||||||
a whole local, its own binding pair can no longer be elided either. *)
|
|
||||||
let gc_escape (ctx : ctx) (p : place) : unit =
|
|
||||||
record_rc ctx ~node:p.pnode ~pos:p.ppos ~op:RcAcquire ~place:(place_text p) ~group:(-1);
|
|
||||||
if p.projs = [] then
|
|
||||||
match root_local ctx p with
|
|
||||||
| Some l -> Hashtbl.replace ctx.rc_escaped l.l_node ()
|
|
||||||
| None -> ()
|
|
||||||
|
|
||||||
(* Whether passing/assigning this place would be a *real* transfer — the
|
(* Whether passing/assigning this place would be a *real* transfer — the
|
||||||
positive half of `transfer`'s decision below, needed one step earlier by
|
positive half of `transfer`'s decision below, needed one step earlier by
|
||||||
analyze_call: an argument that cannot transfer (a borrow, a projection,
|
analyze_call: an argument that cannot transfer (a borrow, a projection,
|
||||||
|
|
@ -1130,9 +1079,7 @@ let is_real_transfer (ctx : ctx) (p : place) : bool =
|
||||||
let transfer (ctx : ctx) (p : place) ~(what : string) : bool =
|
let transfer (ctx : ctx) (p : place) ~(what : string) : bool =
|
||||||
match place_class ctx p with
|
match place_class ctx p with
|
||||||
| Copy -> false
|
| Copy -> false
|
||||||
| Gc ->
|
| Gc -> false (* traced values alias freely; tracing owns the lifetime *)
|
||||||
gc_escape ctx p;
|
|
||||||
false
|
|
||||||
| Owned -> (
|
| Owned -> (
|
||||||
match is_borrow_root ctx p with
|
match is_borrow_root ctx p with
|
||||||
| Some l ->
|
| Some l ->
|
||||||
|
|
@ -1155,7 +1102,6 @@ let transfer (ctx : ctx) (p : place) ~(what : string) : bool =
|
||||||
else begin
|
else begin
|
||||||
check_against_borrows ctx ~node:p.pnode ~pos:p.ppos p AMove;
|
check_against_borrows ctx ~node:p.pnode ~pos:p.ppos p AMove;
|
||||||
l.l_state <- Moved p.ppos;
|
l.l_state <- Moved p.ppos;
|
||||||
clobber ctx p.root;
|
|
||||||
true
|
true
|
||||||
end)))
|
end)))
|
||||||
|
|
||||||
|
|
@ -1302,9 +1248,6 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
||||||
let excl = match resolved with Some c -> c.ce_recv_excl | None -> false in
|
let excl = match resolved with Some c -> c.ce_recv_excl | None -> false in
|
||||||
(match place_of base with
|
(match place_of base with
|
||||||
| Some p ->
|
| Some p ->
|
||||||
(* same ordering as the argument case below: a receiver a method
|
|
||||||
writes to is clobbered whatever its class *)
|
|
||||||
if excl then clobber ctx p.root;
|
|
||||||
if place_class ctx p = Owned then
|
if place_class ctx p = Owned then
|
||||||
Some { ac_place = p; ac_kind = (if excl then AExcl else AShared) }
|
Some { ac_place = p; ac_kind = (if excl then AExcl else AShared) }
|
||||||
else None
|
else None
|
||||||
|
|
@ -1329,13 +1272,6 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
||||||
| None -> []
|
| None -> []
|
||||||
| Some p -> (
|
| Some p -> (
|
||||||
let _, conv = conv_of i in
|
let _, conv = conv_of i in
|
||||||
(* Clobbering is decided *before* the ownership class, because
|
|
||||||
it is not an ownership question: a `mut` argument means the
|
|
||||||
callee may replace what the place holds, and for a @gc place
|
|
||||||
that is exactly what invalidates rc elision (the alias would
|
|
||||||
be the last reference and its increment was elided). Getting
|
|
||||||
this order wrong is a use-after-free, not an imprecision. *)
|
|
||||||
if conv = Mut then clobber ctx p.root;
|
|
||||||
match (place_class ctx p, conv) with
|
match (place_class ctx p, conv) with
|
||||||
| Copy, _ | Gc, _ -> []
|
| Copy, _ | Gc, _ -> []
|
||||||
| Owned, Take ->
|
| Owned, Take ->
|
||||||
|
|
@ -1366,27 +1302,10 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
||||||
a.ac_place AExcl)
|
a.ac_place AExcl)
|
||||||
accesses;
|
accesses;
|
||||||
(* transfers last *)
|
(* transfers last *)
|
||||||
(* `push`'s value argument (builtin `multi_push`) stores a @gc reference
|
(* iteration 7b deleted the `push`-of-a-gc-value special case (an RC_INC
|
||||||
inside the container permanently — an escape exactly like a ctor
|
escape site): a traced value stored into a container needs no
|
||||||
field or a `take` argument. `push` is never a resolved callee (it has
|
bookkeeping — tracing finds it through the container. The bug class the
|
||||||
no declared params), so `conv_of` defaults it to Borrow and the
|
old special case guarded against cannot recur without RC. *)
|
||||||
ordinary Take-gated transfer above never fires for it; without this
|
|
||||||
the container holds the reference with no matching RC_INC, and the
|
|
||||||
collector frees the value out from under the container it still sits
|
|
||||||
in. Narrow to `push`'s own value slot (index 1) and to Gc places only
|
|
||||||
— an Owned element's move-on-push is a separate, pre-existing gap
|
|
||||||
this task does not touch. *)
|
|
||||||
(* Keyed on "`push` is not a user-declared fn", NOT on "the callee did not
|
|
||||||
resolve": since 2026-08-14 resolve_callee answers for builtins too (their
|
|
||||||
return types are what give a `split`/`slice` binding its drop), and the
|
|
||||||
old `resolved = None` test silently stopped firing — the pushed @gc value
|
|
||||||
lost its RC_INC, the collector freed it while the container still held it,
|
|
||||||
and both `gc/` fixtures died with a use-after-free. *)
|
|
||||||
let is_push_gc_value i =
|
|
||||||
i = 1
|
|
||||||
&& Types.StringMap.find_opt "push" ctx.syms.Types.free_fns = None
|
|
||||||
&& match callee.kind with Ident "push" -> true | _ -> false
|
|
||||||
in
|
|
||||||
List.iteri
|
List.iteri
|
||||||
(fun i a ->
|
(fun i a ->
|
||||||
match place_of a with
|
match place_of a with
|
||||||
|
|
@ -1396,7 +1315,7 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
||||||
if conv = Take then
|
if conv = Take then
|
||||||
(if transfer ctx p ~what:(Printf.sprintf "cannot be passed to `take %s`" pname) then
|
(if transfer ctx p ~what:(Printf.sprintf "cannot be passed to `take %s`" pname) then
|
||||||
record_move ctx p (MvArg pname))
|
record_move ctx p (MvArg pname))
|
||||||
else if is_push_gc_value i && place_class ctx p = Gc then gc_escape ctx p)
|
)
|
||||||
args;
|
args;
|
||||||
record_drop ctx ~node:call_e.id ~pos:call_e.pos ~kind:DLiveMask
|
record_drop ctx ~node:call_e.id ~pos:call_e.pos ~kind:DLiveMask
|
||||||
~items:(mask_items (live_holders ctx))
|
~items:(mask_items (live_holders ctx))
|
||||||
|
|
@ -1860,15 +1779,8 @@ and analyze_let (ctx : ctx) (s : Ast.stmt) (name : string) (ty : Ast.field_ty op
|
||||||
(true, None, Live)
|
(true, None, Live)
|
||||||
end
|
end
|
||||||
else (false, Some p, Borrowed s.s_pos)))
|
else (false, Some p, Borrowed s.s_pos)))
|
||||||
| Gc, None ->
|
| Gc, None -> (true, None, Live) (* traced: no bookkeeping (7b) *)
|
||||||
(* fresh allocation: its release is the allocation's own, never
|
| Gc, Some p -> (true, Some p, Live)
|
||||||
elidable *)
|
|
||||||
Hashtbl.replace ctx.rc_groups s.s_id None;
|
|
||||||
(true, None, Live)
|
|
||||||
| Gc, Some p ->
|
|
||||||
Hashtbl.replace ctx.rc_groups s.s_id (Some p.root);
|
|
||||||
record_rc ctx ~node:s.s_id ~pos:s.s_pos ~op:RcAcquire ~place:(place_text p) ~group:s.s_id;
|
|
||||||
(true, Some p, Live)
|
|
||||||
in
|
in
|
||||||
declare ctx
|
declare ctx
|
||||||
{ l_name = name; l_ty = vty; l_class = cls; l_node = s.s_id; l_pos = s.s_pos; l_holds = holds;
|
{ l_name = name; l_ty = vty; l_class = cls; l_node = s.s_id; l_pos = s.s_pos; l_holds = holds;
|
||||||
|
|
@ -1902,7 +1814,6 @@ and analyze_assign (ctx : ctx) (s : Ast.stmt) (target : Ast.expr) (value : Ast.e
|
||||||
(match tplace with
|
(match tplace with
|
||||||
| None -> read_expr ctx target
|
| None -> read_expr ctx target
|
||||||
| Some p ->
|
| Some p ->
|
||||||
clobber ctx p.root;
|
|
||||||
check_against_borrows ctx ~node:s.s_id ~pos:p.ppos p AExcl;
|
check_against_borrows ctx ~node:s.s_id ~pos:p.ppos p AExcl;
|
||||||
if p.projs = [] then begin
|
if p.projs = [] then begin
|
||||||
match root_local ctx p with
|
match root_local ctx p with
|
||||||
|
|
@ -1910,10 +1821,6 @@ and analyze_assign (ctx : ctx) (s : Ast.stmt) (target : Ast.expr) (value : Ast.e
|
||||||
if l.l_class = Owned then
|
if l.l_class = Owned then
|
||||||
record_drop ctx ~node:s.s_id ~pos:p.ppos ~kind:DOverwrite
|
record_drop ctx ~node:s.s_id ~pos:p.ppos ~kind:DOverwrite
|
||||||
~items:[ { di_name = place_text p; di_kind = LOwned; di_node = p.pnode } ]
|
~items:[ { di_name = place_text p; di_kind = LOwned; di_node = p.pnode } ]
|
||||||
else if l.l_class = Gc then begin
|
|
||||||
Hashtbl.replace ctx.rc_escaped l.l_node ();
|
|
||||||
record_rc ctx ~node:s.s_id ~pos:p.ppos ~op:RcRelease ~place:(place_text p) ~group:(-1)
|
|
||||||
end
|
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
end
|
end
|
||||||
else begin
|
else begin
|
||||||
|
|
@ -1924,7 +1831,7 @@ and analyze_assign (ctx : ctx) (s : Ast.stmt) (target : Ast.expr) (value : Ast.e
|
||||||
| Owned ->
|
| Owned ->
|
||||||
record_drop ctx ~node:s.s_id ~pos:p.ppos ~kind:DOverwrite
|
record_drop ctx ~node:s.s_id ~pos:p.ppos ~kind:DOverwrite
|
||||||
~items:[ { di_name = place_text p; di_kind = LOwned; di_node = p.pnode } ]
|
~items:[ { di_name = place_text p; di_kind = LOwned; di_node = p.pnode } ]
|
||||||
| Gc -> record_rc ctx ~node:s.s_id ~pos:p.ppos ~op:RcRelease ~place:(place_text p) ~group:(-1)
|
| Gc -> () (* traced: tracing owns the old value's lifetime (7b) *)
|
||||||
| Copy -> ()
|
| Copy -> ()
|
||||||
end);
|
end);
|
||||||
(* the incoming value *)
|
(* the incoming value *)
|
||||||
|
|
@ -1966,7 +1873,6 @@ and analyze_return (ctx : ctx) (s : Ast.stmt) (opt : Ast.expr option) : unit =
|
||||||
record_move ctx p MvReturn));
|
record_move ctx p MvReturn));
|
||||||
let live = live_holders ctx in
|
let live = live_holders ctx in
|
||||||
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DReturn ~items:(owned_items live);
|
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DReturn ~items:(owned_items live);
|
||||||
release_gc ctx ~node:s.s_id ~pos:s.s_pos live;
|
|
||||||
ctx.diverged <- true
|
ctx.diverged <- true
|
||||||
|
|
||||||
(* haxe-parity Task 2: `break`/`continue` reuse analyze_return's own
|
(* haxe-parity Task 2: `break`/`continue` reuse analyze_return's own
|
||||||
|
|
@ -1988,8 +1894,7 @@ and analyze_break (ctx : ctx) (s : Ast.stmt) : unit =
|
||||||
| [] -> ()
|
| [] -> ()
|
||||||
| loop_node :: _ ->
|
| loop_node :: _ ->
|
||||||
let live = live_holders_upto ctx loop_node in
|
let live = live_holders_upto ctx loop_node in
|
||||||
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DBreak ~items:(owned_items live);
|
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DBreak ~items:(owned_items live));
|
||||||
release_gc ctx ~node:s.s_id ~pos:s.s_pos live);
|
|
||||||
ctx.diverged <- true
|
ctx.diverged <- true
|
||||||
|
|
||||||
and analyze_continue (ctx : ctx) (s : Ast.stmt) : unit =
|
and analyze_continue (ctx : ctx) (s : Ast.stmt) : unit =
|
||||||
|
|
@ -1997,8 +1902,7 @@ and analyze_continue (ctx : ctx) (s : Ast.stmt) : unit =
|
||||||
| [] -> ()
|
| [] -> ()
|
||||||
| loop_node :: _ ->
|
| loop_node :: _ ->
|
||||||
let live = live_holders_upto ctx loop_node in
|
let live = live_holders_upto ctx loop_node in
|
||||||
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DContinue ~items:(owned_items live);
|
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DContinue ~items:(owned_items live));
|
||||||
release_gc ctx ~node:s.s_id ~pos:s.s_pos live);
|
|
||||||
ctx.diverged <- true
|
ctx.diverged <- true
|
||||||
|
|
||||||
(* ============================================================
|
(* ============================================================
|
||||||
|
|
@ -2022,25 +1926,12 @@ let param_local (ctx : ctx) (p : Ast.param) : local =
|
||||||
{ l_name = p.name; l_ty = p.ty; l_class = cls; l_node = p.id; l_pos = p.pos; l_holds = false;
|
{ l_name = p.name; l_ty = p.ty; l_class = cls; l_node = p.id; l_pos = p.pos; l_holds = false;
|
||||||
l_src = None; l_bkind = (if p.conv = Mut then AExcl else AShared); l_state = state })
|
l_src = None; l_bkind = (if p.conv = Mut then AExcl else AShared); l_state = state })
|
||||||
|
|
||||||
let resolve_rc (ctx : ctx) : unit =
|
|
||||||
List.iter
|
|
||||||
(fun r ->
|
|
||||||
if r.rc_group >= 0 then
|
|
||||||
r.rc_elided <-
|
|
||||||
(not (Hashtbl.mem ctx.rc_escaped r.rc_group))
|
|
||||||
&& (match Hashtbl.find_opt ctx.rc_groups r.rc_group with
|
|
||||||
| Some (Some root) -> not (Hashtbl.mem ctx.clobbered root)
|
|
||||||
| _ -> false))
|
|
||||||
ctx.fn_rcs;
|
|
||||||
ctx.sink.s_rcs <- ctx.fn_rcs @ ctx.sink.s_rcs
|
|
||||||
|
|
||||||
let analyze_fn ~(file : string) ?(promote : (string -> unit) option = None)
|
let analyze_fn ~(file : string) ?(promote : (string -> unit) option = None)
|
||||||
(syms : Types.symbols) (coll : Diag.Collector.t) (sink : sink)
|
(syms : Types.symbols) (coll : Diag.Collector.t) (sink : sink)
|
||||||
~(self_class : string option) (m : Ast.method_decl) : unit =
|
~(self_class : string option) (m : Ast.method_decl) : unit =
|
||||||
let ctx =
|
let ctx =
|
||||||
{ file; syms; coll; sink; fn_name = m.name; scopes = []; loop_stack = []; recording = true;
|
{ file; syms; coll; sink; fn_name = m.name; scopes = []; loop_stack = []; recording = true;
|
||||||
diverged = false; fn_rcs = []; rc_groups = Hashtbl.create 8; rc_escaped = Hashtbl.create 8;
|
diverged = false; promote }
|
||||||
clobbered = Hashtbl.create 8; promote }
|
|
||||||
in
|
in
|
||||||
push_scope ctx ~node:m.id ~pos:m.pos ~label:"BODY";
|
push_scope ctx ~node:m.id ~pos:m.pos ~label:"BODY";
|
||||||
(* `self` is always a borrow (spec rule 6) *)
|
(* `self` is always a borrow (spec rule 6) *)
|
||||||
|
|
@ -2055,8 +1946,7 @@ let analyze_fn ~(file : string) ?(promote : (string -> unit) option = None)
|
||||||
l_state = (if cls = Gc then Live else Borrowed m.pos) });
|
l_state = (if cls = Gc then Live else Borrowed m.pos) });
|
||||||
List.iter (fun p -> declare ctx (param_local ctx p)) m.params;
|
List.iter (fun p -> declare ctx (param_local ctx p)) m.params;
|
||||||
List.iter (analyze_stmt ctx) m.body;
|
List.iter (analyze_stmt ctx) m.body;
|
||||||
pop_scope ctx;
|
pop_scope ctx
|
||||||
resolve_rc ctx
|
|
||||||
|
|
||||||
(* ============================================================
|
(* ============================================================
|
||||||
Entry point
|
Entry point
|
||||||
|
|
@ -2066,7 +1956,7 @@ let pos_key (p : Ast.pos) = (p.line, p.col)
|
||||||
|
|
||||||
let analyze ~(file : string) ?(promote : (string -> unit) option = None)
|
let analyze ~(file : string) ?(promote : (string -> unit) option = None)
|
||||||
(prog : Ast.program) (syms : Types.symbols) (coll : Diag.Collector.t) : tables =
|
(prog : Ast.program) (syms : Types.symbols) (coll : Diag.Collector.t) : tables =
|
||||||
let sink = { s_moves = []; s_drops = []; s_rcs = []; s_res = [] } in
|
let sink = { s_moves = []; s_drops = []; s_res = [] } in
|
||||||
List.iter
|
List.iter
|
||||||
(function
|
(function
|
||||||
| Ast.Class c ->
|
| Ast.Class c ->
|
||||||
|
|
@ -2086,5 +1976,4 @@ let analyze ~(file : string) ?(promote : (string -> unit) option = None)
|
||||||
let sort_by key l = List.stable_sort (fun a b -> compare (key a) (key b)) (List.rev l) in
|
let sort_by key l = List.stable_sort (fun a b -> compare (key a) (key b)) (List.rev l) in
|
||||||
{ moves = sort_by (fun m -> pos_key m.mv_pos) sink.s_moves;
|
{ moves = sort_by (fun m -> pos_key m.mv_pos) sink.s_moves;
|
||||||
drops = sort_by (fun d -> pos_key d.dr_pos) sink.s_drops;
|
drops = sort_by (fun d -> pos_key d.dr_pos) sink.s_drops;
|
||||||
rcs = sort_by (fun r -> pos_key r.rc_pos) sink.s_rcs;
|
|
||||||
residuals = sort_by (fun r -> pos_key r.rs_pos) sink.s_res }
|
residuals = sort_by (fun r -> pos_key r.rs_pos) sink.s_res }
|
||||||
|
|
|
||||||
|
|
@ -28,26 +28,23 @@ m1 proven args=1 regs=3 [free fn]
|
||||||
0002 CALL r2, m0
|
0002 CALL r2, m0
|
||||||
0003 RET r2
|
0003 RET r2
|
||||||
m2 main args=0 regs=6 [free fn] [ENTRY]
|
m2 main args=0 regs=6 [free fn] [ENTRY]
|
||||||
lines: 0->29 3->30 7->31 13->28
|
lines: 0->29 3->30 6->31 12->28
|
||||||
drops: pc 3 owned={} gc={r0}
|
drops: pc 3 owned={} gc={r0}
|
||||||
drops: pc 7 owned={r1} gc={r0}
|
drops: pc 6 owned={r1} gc={r0}
|
||||||
drops: pc 14 owned={} gc={r0}
|
drops: pc 13 owned={} gc={r0}
|
||||||
drops: pc 15 owned={} gc={}
|
|
||||||
0000 NEW r0, c0
|
0000 NEW r0, c0
|
||||||
0001 LOADK r1, k8
|
0001 LOADK r1, k8
|
||||||
0002 SETF r0, f0, r1
|
0002 SETF r0, f0, r1
|
||||||
0003 NEW r1, c1
|
0003 NEW r1, c1
|
||||||
0004 MOVE r2, r0
|
0004 MOVE r2, r0
|
||||||
0005 RC_INC r2
|
0005 SETF r1, f0, r2
|
||||||
0006 SETF r1, f0, r2
|
0006 MOVE r4, r1
|
||||||
0007 MOVE r4, r1
|
0007 CALL r4, m1
|
||||||
0008 CALL r4, m1
|
0008 MOVE r3, r4
|
||||||
0009 MOVE r3, r4
|
0009 LOADK r5, k9
|
||||||
0010 LOADK r5, k9
|
0010 ADD r2, r3, r5
|
||||||
0011 ADD r2, r3, r5
|
0011 BUILTIN r2, r2, print_int
|
||||||
0012 BUILTIN r2, r2, print_int
|
0012 DROP r1
|
||||||
0013 DROP r1
|
0013 RET0
|
||||||
0014 RC_DEC r0
|
|
||||||
0015 RET0
|
|
||||||
== ENTRY ==
|
== ENTRY ==
|
||||||
m2
|
m2
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,4 @@
|
||||||
50:3 JOIN-DROP ELSE [a]
|
50:3 JOIN-DROP ELSE [a]
|
||||||
58:3 RETURN [a]
|
58:3 RETURN [a]
|
||||||
64:3 RETURN [a]
|
64:3 RETURN [a]
|
||||||
== RC ==
|
|
||||||
== RESIDUAL ==
|
== RESIDUAL ==
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,4 @@
|
||||||
10:3 RETURN [it]
|
10:3 RETURN [it]
|
||||||
16:16 LIVE-MASK [bag, held]
|
16:16 LIVE-MASK [bag, held]
|
||||||
17:3 RETURN [held]
|
17:3 RETURN [held]
|
||||||
== RC ==
|
|
||||||
== RESIDUAL ==
|
== RESIDUAL ==
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,4 @@
|
||||||
== DROPS ==
|
== DROPS ==
|
||||||
18:5 OVERWRITE self.name
|
18:5 OVERWRITE self.name
|
||||||
22:5 OVERWRITE self.prices
|
22:5 OVERWRITE self.prices
|
||||||
== RC ==
|
|
||||||
== RESIDUAL ==
|
== RESIDUAL ==
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,4 @@
|
||||||
26:15 LIVE-MASK [c:gc]
|
26:15 LIVE-MASK [c:gc]
|
||||||
35:18 LIVE-MASK [c:gc]
|
35:18 LIVE-MASK [c:gc]
|
||||||
36:15 LIVE-MASK [c:gc]
|
36:15 LIVE-MASK [c:gc]
|
||||||
== RC ==
|
|
||||||
15:3 ACQUIRE h.cache ELIDED
|
|
||||||
16:3 RELEASE c ELIDED
|
|
||||||
20:3 ACQUIRE h.cache KEPT
|
|
||||||
21:3 RELEASE c KEPT
|
|
||||||
21:10 ACQUIRE c KEPT
|
|
||||||
26:3 RELEASE c KEPT
|
|
||||||
34:3 ACQUIRE h.cache KEPT
|
|
||||||
36:3 RELEASE c KEPT
|
|
||||||
== RESIDUAL ==
|
== RESIDUAL ==
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
== MOVES ==
|
== MOVES ==
|
||||||
== DROPS ==
|
== DROPS ==
|
||||||
== RC ==
|
|
||||||
== RESIDUAL ==
|
== RESIDUAL ==
|
||||||
22:22 RESIDUAL BORROW_X bag.items[i] vs BORROW_X bag.items[j]
|
22:22 RESIDUAL BORROW_X bag.items[i] vs BORROW_X bag.items[j]
|
||||||
24:24 RESIDUAL BORROW_X bag.items[i] vs BORROW_S bag.items[j]
|
24:24 RESIDUAL BORROW_X bag.items[i] vs BORROW_S bag.items[j]
|
||||||
|
|
|
||||||
|
|
@ -1965,13 +1965,6 @@ let () =
|
||||||
(fun (d : Owner.drop_site) ->
|
(fun (d : Owner.drop_site) ->
|
||||||
List.for_all (fun (i : Owner.drop_item) -> i.Owner.di_node > 0) d.Owner.dr_items)
|
List.for_all (fun (i : Owner.drop_item) -> i.Owner.di_node > 0) d.Owner.dr_items)
|
||||||
tables.Owner.drops);
|
tables.Owner.drops);
|
||||||
let rc_path = "golden/owner/rc.wo" in
|
|
||||||
let rc_tables, _ = owner_str ~file:rc_path (read_file rc_path) in
|
|
||||||
check "rc table: every entry carries a real AST node id"
|
|
||||||
(List.for_all (fun (r : Owner.rc_site) -> r.Owner.rc_node > 0) rc_tables.Owner.rcs);
|
|
||||||
check "rc table: the balanced pair is elided, the escaping one kept"
|
|
||||||
(List.exists (fun (r : Owner.rc_site) -> r.Owner.rc_elided) rc_tables.Owner.rcs
|
|
||||||
&& List.exists (fun (r : Owner.rc_site) -> not r.Owner.rc_elided) rc_tables.Owner.rcs);
|
|
||||||
let res_path = "golden/owner/residual.wo" in
|
let res_path = "golden/owner/residual.wo" in
|
||||||
let res_tables, _ = owner_str ~file:res_path (read_file res_path) in
|
let res_tables, _ = owner_str ~file:res_path (read_file res_path) in
|
||||||
check_eq "residual table: only the runtime-index pairs are residual" ~expected:3
|
check_eq "residual table: only the runtime-index pairs are residual" ~expected:3
|
||||||
|
|
@ -2235,23 +2228,6 @@ let () =
|
||||||
check "switch let-class: `w` is dropped at the return (classified Owned, not silently Copy)"
|
check "switch let-class: `w` is dropped at the return (classified Owned, not silently Copy)"
|
||||||
(List.exists (fun d -> names_of d = [ "w" ]) returns)
|
(List.exists (fun d -> names_of d = [ "w" ]) returns)
|
||||||
|
|
||||||
let () =
|
|
||||||
(* IMPORTANT: a `mut` argument means the callee may replace what the place
|
|
||||||
holds. For a @gc place that invalidates rc elision — the elided
|
|
||||||
increment would leave the alias as the last reference to a freed
|
|
||||||
object. The clobber therefore has to happen before the ownership class
|
|
||||||
is consulted, since @gc arguments create no access entry at all. *)
|
|
||||||
let path = "golden/owner/rc.wo" in
|
|
||||||
let tables, _ = owner_str ~file:path (read_file path) in
|
|
||||||
let at line =
|
|
||||||
List.filter (fun (r : Owner.rc_site) -> r.Owner.rc_pos.Ast.line = line) tables.Owner.rcs
|
|
||||||
in
|
|
||||||
single_site "rc: `balanced` has one ACQUIRE" (at 15) (fun r ->
|
|
||||||
check "rc: an alias whose source is never clobbered is ELIDED" r.Owner.rc_elided);
|
|
||||||
single_site "rc: `clobbered` has one ACQUIRE" (at 34) (fun r ->
|
|
||||||
check "rc: an alias whose source root is passed `mut` is KEPT"
|
|
||||||
(not r.Owner.rc_elided))
|
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
let path = "golden/owner/moves.wo" in
|
let path = "golden/owner/moves.wo" in
|
||||||
let exit_code, stdout, stderr = run_cli [ "--dump-owner"; path ] in
|
let exit_code, stdout, stderr = run_cli [ "--dump-owner"; path ] in
|
||||||
|
|
@ -2295,7 +2271,7 @@ let validate_image (img : string) : string list =
|
||||||
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
|
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
|
||||||
let none = 0xFFFFFFFF in
|
let none = 0xFFFFFFFF in
|
||||||
if u32 0 <> 0x31424F57 then fail "bad magic";
|
if u32 0 <> 0x31424F57 then fail "bad magic";
|
||||||
if u32 4 <> 3 then fail "unsupported version";
|
if u32 4 <> 4 then fail "unsupported version";
|
||||||
let coff = u32 8 and ccnt = u32 12 in
|
let coff = u32 8 and ccnt = u32 12 in
|
||||||
let koff = u32 16 and kcnt = u32 20 in
|
let koff = u32 16 and kcnt = u32 20 in
|
||||||
let ioff = u32 24 and icnt = u32 28 in
|
let ioff = u32 24 and icnt = u32 28 in
|
||||||
|
|
@ -2614,26 +2590,26 @@ let method_block (dump : string) (name : string) : string =
|
||||||
String.concat "\n" (collect [] false lines)
|
String.concat "\n" (collect [] false lines)
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
(* The spec's zero-cost promise, as an assertion and not only a pinned
|
(* The zero-cost promise, iteration 7b edition: a proven method emits no
|
||||||
dump: a method whose ownership is fully proven contains no borrow op
|
borrow op, and NO method anywhere emits an rc op — reference counting
|
||||||
and no rc op. golden/bc/elision.wo's `proven` aliases a @gc
|
is gone from the instruction stream entirely (opcodes 27/28 reserved). *)
|
||||||
reference and passes it to a reader; owner.ml marks the pair ELIDED
|
|
||||||
(golden/owner/rc.wo pins that), so nothing may be emitted for it. *)
|
|
||||||
let path = "golden/bc/elision.wo" in
|
let path = "golden/bc/elision.wo" in
|
||||||
let image, _ = emit_str ~file:path (read_file path) in
|
let image, _ = emit_str ~file:path (read_file path) in
|
||||||
let block = method_block (Disasm.dump image) "proven" in
|
let dump = Disasm.dump image in
|
||||||
|
let block = method_block dump "proven" in
|
||||||
check "elision: `proven` was found in the disassembly" (block <> "");
|
check "elision: `proven` was found in the disassembly" (block <> "");
|
||||||
List.iter
|
List.iter
|
||||||
(fun op ->
|
(fun op ->
|
||||||
check
|
check
|
||||||
(Printf.sprintf "elision: `proven` emits no %s (zero-cost when provable)" op)
|
(Printf.sprintf "elision: `proven` emits no %s (zero-cost when provable)" op)
|
||||||
(find_substring ~needle:op block = None))
|
(find_substring ~needle:op block = None))
|
||||||
[ "BORROW_S"; "BORROW_X"; "RELEASE_S"; "RELEASE_X"; "RC_INC"; "RC_DEC" ];
|
[ "BORROW_S"; "BORROW_X"; "RELEASE_S"; "RELEASE_X" ];
|
||||||
(* the contrast, so the fixture cannot pass by emitting nothing anywhere:
|
List.iter
|
||||||
main stores the @gc value into a field, which is a KEPT acquire *)
|
(fun op ->
|
||||||
let main_block = method_block (Disasm.dump image) "main" in
|
check
|
||||||
check "elision: the escaping acquire in `main` is still emitted (fixture is not vacuous)"
|
(Printf.sprintf "rc retired: the whole image contains no %s" op)
|
||||||
(find_substring ~needle:"RC_INC" main_block <> None)
|
(find_substring ~needle:op dump = None))
|
||||||
|
[ "RC_INC"; "RC_DEC" ]
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
(* haxe-parity Task 3: the compare-and-jump chain lowers onto the
|
(* haxe-parity Task 3: the compare-and-jump chain lowers onto the
|
||||||
|
|
@ -2786,12 +2762,9 @@ let () =
|
||||||
(List.fold_left max 0 masks <= List.fold_left max 0 popcounts)
|
(List.fold_left max 0 masks <= List.fold_left max 0 popcounts)
|
||||||
|
|
||||||
(* The ownership tables are a contract, not a hint: every DROP the DROPS
|
(* The ownership tables are a contract, not a hint: every DROP the DROPS
|
||||||
table asks for, and every rc op the RC table does not mark ELIDED, has
|
table asks for has to appear in the emitted code exactly once — and
|
||||||
to appear in the emitted code exactly once — and nothing else may. A
|
nothing else may (rc ops don't exist since iteration 7b). A count
|
||||||
count identity over a whole file is the cheapest way to state that, and
|
identity over a whole file is the cheapest way to state that. *)
|
||||||
it is what caught a missing constructor-field @gc acquire (the escape
|
|
||||||
increment is anchored on the value's expression node, so lowering it
|
|
||||||
per statement kind silently skipped one of the four escapes). *)
|
|
||||||
let () =
|
let () =
|
||||||
let count_op needle dump =
|
let count_op needle dump =
|
||||||
String.split_on_char '\n' dump
|
String.split_on_char '\n' dump
|
||||||
|
|
@ -2817,23 +2790,14 @@ let () =
|
||||||
d.Owner.dr_items))
|
d.Owner.dr_items))
|
||||||
0 tables.Owner.drops
|
0 tables.Owner.drops
|
||||||
in
|
in
|
||||||
let kept op =
|
|
||||||
List.length
|
|
||||||
(List.filter
|
|
||||||
(fun (r : Owner.rc_site) -> r.Owner.rc_op = op && not r.Owner.rc_elided)
|
|
||||||
tables.Owner.rcs)
|
|
||||||
in
|
|
||||||
let image, _ = emit_str ~file:path src in
|
let image, _ = emit_str ~file:path src in
|
||||||
let dump = Disasm.dump image in
|
let dump = Disasm.dump image in
|
||||||
check_eq
|
check_eq
|
||||||
(Printf.sprintf "table contract %s: one DROP per owned drop-table item" path)
|
(Printf.sprintf "table contract %s: one DROP per owned drop-table item" path)
|
||||||
~expected:want_drops ~actual:(count_op "DROP " dump) string_of_int;
|
~expected:want_drops ~actual:(count_op "DROP " dump) string_of_int;
|
||||||
check_eq
|
check_eq
|
||||||
(Printf.sprintf "table contract %s: one RC_INC per KEPT acquire" path)
|
(Printf.sprintf "table contract %s: rc ops never appear (7b)" path)
|
||||||
~expected:(kept Owner.RcAcquire) ~actual:(count_op "RC_INC" dump) string_of_int;
|
~expected:0 ~actual:(count_op "RC_INC" dump + count_op "RC_DEC" dump) string_of_int;
|
||||||
check_eq
|
|
||||||
(Printf.sprintf "table contract %s: one RC_DEC per KEPT release" path)
|
|
||||||
~expected:(kept Owner.RcRelease) ~actual:(count_op "RC_DEC" dump) string_of_int;
|
|
||||||
(* The residual table is both the only licence to emit a borrow
|
(* The residual table is both the only licence to emit a borrow
|
||||||
op and an obligation to emit one per *operand*: guards are
|
op and an obligation to emit one per *operand*: guards are
|
||||||
coalesced per operand, never per entry (asking twice for an
|
coalesced per operand, never per entry (asking twice for an
|
||||||
|
|
|
||||||
|
|
@ -461,8 +461,6 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
||||||
case WOP_BORROW_X:
|
case WOP_BORROW_X:
|
||||||
case WOP_RELEASE_S:
|
case WOP_RELEASE_S:
|
||||||
case WOP_RELEASE_X:
|
case WOP_RELEASE_X:
|
||||||
case WOP_RC_INC:
|
|
||||||
case WOP_RC_DEC:
|
|
||||||
RCHK(A);
|
RCHK(A);
|
||||||
break;
|
break;
|
||||||
case WOP_BUILTIN: {
|
case WOP_BUILTIN: {
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,7 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
||||||
[WOP_GETF] = &&L_GETF, [WOP_SETF] = &&L_SETF,
|
[WOP_GETF] = &&L_GETF, [WOP_SETF] = &&L_SETF,
|
||||||
[WOP_DROP] = &&L_DROP, [WOP_BORROW_S] = &&L_BORROW_S,
|
[WOP_DROP] = &&L_DROP, [WOP_BORROW_S] = &&L_BORROW_S,
|
||||||
[WOP_BORROW_X] = &&L_BORROW_X, [WOP_RELEASE_S] = &&L_RELEASE_S,
|
[WOP_BORROW_X] = &&L_BORROW_X, [WOP_RELEASE_S] = &&L_RELEASE_S,
|
||||||
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_RC_INC] = &&L_RC_INC,
|
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_BUILTIN] = &&L_BUILTIN,
|
||||||
[WOP_RC_DEC] = &&L_RC_DEC, [WOP_BUILTIN] = &&L_BUILTIN,
|
|
||||||
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
||||||
[WOP_TRY] = &&L_TRY, [WOP_ENDTRY] = &&L_ENDTRY,
|
[WOP_TRY] = &&L_TRY, [WOP_ENDTRY] = &&L_ENDTRY,
|
||||||
};
|
};
|
||||||
|
|
@ -488,14 +487,6 @@ dispatch:
|
||||||
NEXT();
|
NEXT();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iteration 7b: reference counting is retired — tracing owns traced
|
|
||||||
* lifetimes, so alias bookkeeping means nothing. The opcodes stay
|
|
||||||
* accepted as no-ops until the emitter stops producing them and the
|
|
||||||
* format reserves 27–28 (the .wob version bump); a no-op is also what
|
|
||||||
* deletes the old RC_DEC-on-nil trap that broke `?Node` field stores. */
|
|
||||||
CASE(RC_INC) : NEXT();
|
|
||||||
CASE(RC_DEC) : NEXT();
|
|
||||||
|
|
||||||
CASE(CONCAT) : {
|
CASE(CONCAT) : {
|
||||||
const char *why;
|
const char *why;
|
||||||
wo_str *x = str_check(R[wo_ins_b(ins)], &why);
|
wo_str *x = str_check(R[wo_ins_b(ins)], &why);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
|
|
||||||
/* ---- file header (44 bytes, absolute offsets) ---- */
|
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||||
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
||||||
#define WOB_VERSION 3u /* v3: v2's field metadata + per-class index metadata */
|
#define WOB_VERSION 4u /* v4 (iteration 7b): opcodes 27-28 (RC_INC/RC_DEC) retired;
|
||||||
|
* the drop table's gc mask now means "GC roots at this pc" */
|
||||||
#define WOB_HDR_SIZE 44u
|
#define WOB_HDR_SIZE 44u
|
||||||
#define WOB_OFF_MAGIC 0u
|
#define WOB_OFF_MAGIC 0u
|
||||||
#define WOB_OFF_VERSION 4u
|
#define WOB_OFF_VERSION 4u
|
||||||
|
|
@ -168,8 +169,8 @@ enum {
|
||||||
WOP_BORROW_X = 24,
|
WOP_BORROW_X = 24,
|
||||||
WOP_RELEASE_S = 25,
|
WOP_RELEASE_S = 25,
|
||||||
WOP_RELEASE_X = 26,
|
WOP_RELEASE_X = 26,
|
||||||
WOP_RC_INC = 27,
|
/* 27-28 were RC_INC/RC_DEC — retired with reference counting (v4,
|
||||||
WOP_RC_DEC = 28,
|
iteration 7b). Reserved: the loader rejects them. */
|
||||||
WOP_BUILTIN = 29, /* A B C: r[A] = builtin C, args from r[B] */
|
WOP_BUILTIN = 29, /* A B C: r[A] = builtin C, args from r[B] */
|
||||||
WOP_DB_STUB = 30, /* traps WO_T_DB "engine not linked" */
|
WOP_DB_STUB = 30, /* traps WO_T_DB "engine not linked" */
|
||||||
WOP_TRAP = 31, /* Bx: explicit trap */
|
WOP_TRAP = 31, /* Bx: explicit trap */
|
||||||
|
|
|
||||||
|
|
@ -104,30 +104,51 @@ static void test_two_frame_unwind_frees_both(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* rc inc/dec through opcodes frees exactly at zero (@gc malloc-path class) */
|
/* rc inc/dec through opcodes frees exactly at zero (@gc malloc-path class) */
|
||||||
static void test_rc_opcodes_free_at_zero(void) {
|
/* iteration 7b: opcodes 27-28 (the old RC_INC/RC_DEC) are reserved in .wob
|
||||||
|
* v4 — the loader must reject an image that carries one, exactly like any
|
||||||
|
* other unknown opcode. A traced instance abandoned by a clean return is
|
||||||
|
* rt_destroy's to free (ASan proves it). */
|
||||||
|
static void test_reserved_rc_opcode_rejected(void) {
|
||||||
wb_t *b = wb_new();
|
wb_t *b = wb_new();
|
||||||
uint32_t kc = wb_const_text(b, "GBig");
|
uint32_t kc = wb_const_text(b, "GBig");
|
||||||
uint32_t kf = wb_const_text(b, "main");
|
uint32_t kf = wb_const_text(b, "main");
|
||||||
wb_class(b, kc, WO_CLASSF_GC, big_kinds, BIG);
|
wb_class(b, kc, WO_CLASSF_GC, big_kinds, BIG);
|
||||||
uint32_t code[] = {
|
uint32_t code[] = {
|
||||||
wo_ins_abx(WOP_NEW, 0, 0), /* rc 1 */
|
wo_ins_abx(WOP_NEW, 0, 0),
|
||||||
wo_ins_abc(WOP_RC_INC, 0, 0, 0), /* rc 2 */
|
wo_ins_abc(27 /* retired RC_INC */, 0, 0, 0),
|
||||||
wo_ins_abc(WOP_RC_DEC, 0, 0, 0), /* rc 1 */
|
|
||||||
wo_ins_abc(WOP_RC_DEC, 0, 0, 0), /* rc 0: freed */
|
|
||||||
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
};
|
};
|
||||||
wb_method(b, kf, WOB_NONE, 0, 1, code, 5, NULL, 0, NULL, 0);
|
wb_method(b, kf, WOB_NONE, 0, 1, code, 3, NULL, 0, NULL, 0);
|
||||||
size_t len;
|
size_t len;
|
||||||
uint8_t *img = wb_finish(b, &len);
|
uint8_t *img = wb_finish(b, &len);
|
||||||
uint64_t ret = 0;
|
uint64_t ret = 0;
|
||||||
wo_err err;
|
wo_err err;
|
||||||
T_EQ(run_img(img, len, &ret, &err), 0); /* no trap; ASan proves the free */
|
T_EQ(run_img(img, len, &ret, &err), -2); /* loader rejection */
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_abandoned_traced_freed_at_destroy(void) {
|
||||||
|
wb_t *b = wb_new();
|
||||||
|
uint32_t kc = wb_const_text(b, "GBig");
|
||||||
|
uint32_t kf = wb_const_text(b, "main");
|
||||||
|
wb_class(b, kc, WO_CLASSF_GC, big_kinds, BIG);
|
||||||
|
uint32_t code[] = {
|
||||||
|
wo_ins_abx(WOP_NEW, 0, 0), /* traced, linked; never dropped */
|
||||||
|
wo_ins_abc(WOP_RET0, 0, 0, 0),
|
||||||
|
};
|
||||||
|
wb_method(b, kf, WOB_NONE, 0, 1, code, 2, NULL, 0, NULL, 0);
|
||||||
|
size_t len;
|
||||||
|
uint8_t *img = wb_finish(b, &len);
|
||||||
|
uint64_t ret = 0;
|
||||||
|
wo_err err;
|
||||||
|
T_EQ(run_img(img, len, &ret, &err), 0); /* ASan: rt_destroy frees it */
|
||||||
free(img);
|
free(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_borrow_violation_frees_owned();
|
test_borrow_violation_frees_owned();
|
||||||
test_two_frame_unwind_frees_both();
|
test_two_frame_unwind_frees_both();
|
||||||
test_rc_opcodes_free_at_zero();
|
test_reserved_rc_opcode_rejected();
|
||||||
|
test_abandoned_traced_freed_at_destroy();
|
||||||
return t_report("test_unwind");
|
return t_report("test_unwind");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue