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
841cb41c6b
commit
092b528081
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
|
||||
| 25 -> Printf.sprintf "RELEASE_S 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 ->
|
||||
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)
|
||||
|
|
@ -164,7 +162,7 @@ let dump (img : string) : string =
|
|||
let line fmt = Buffer.add_string out (fmt ^ "\n") in
|
||||
if u32 img 0 <> magic then raise (Bad "bad magic");
|
||||
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 koff = u32 img 16 and kcnt = u32 img 20 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
|
||||
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>" —
|
||||
the sites static proof could not settle, so the
|
||||
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.DContinue -> Printf.sprintf "%s CONTINUE %s" pos (drop_items_str d.Owner.dr_items)
|
||||
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) =
|
||||
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
|
||||
|
|
@ -537,7 +526,6 @@ let dump_owner (t : Owner.tables) : string =
|
|||
let lines =
|
||||
section "== MOVES ==" (List.map move_line t.Owner.moves)
|
||||
@ 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)
|
||||
in
|
||||
String.concat "\n" lines ^ "\n"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
Owner.tables the four ownership tables, verbatim:
|
||||
moves -> which MOVEs are real transfers
|
||||
drops -> DROP placement + drop-table masks
|
||||
rcs -> RC_INC / RC_DEC, minus ELIDED pairs
|
||||
residuals -> the ONLY places borrow ops appear
|
||||
|
||||
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_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_none = 0xFFFFFFFF
|
||||
let k_int = 0
|
||||
|
|
@ -187,8 +186,7 @@ let op_borrow_s = 23
|
|||
let op_borrow_x = 24
|
||||
let op_release_s = 25
|
||||
let op_release_x = 26
|
||||
let op_rc_inc = 27
|
||||
let op_rc_dec = 28
|
||||
(* opcodes 27-28 (RC_INC/RC_DEC) retired in v4 — reserved, never emitted *)
|
||||
let op_builtin = 29
|
||||
let op_db_stub = 30
|
||||
|
||||
|
|
@ -567,7 +565,6 @@ type views = {
|
|||
This is how the emitter learns which locals the frame destroys
|
||||
without re-deriving owner.ml's own "holds" decision. *)
|
||||
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
|
||||
guards *)
|
||||
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_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_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 }
|
||||
in
|
||||
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.DLiveMask -> Hashtbl.replace v.v_mask d.Owner.dr_node items)
|
||||
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
|
||||
(region, operand) with the strongest access kind, never one pair per
|
||||
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);
|
||||
mask_clear f r
|
||||
| Owner.LGc ->
|
||||
(* a @gc handle's release is an rc site, never a DROP: the RC
|
||||
table carries it (with its own ELIDED decision) *)
|
||||
()))
|
||||
(* a traced handle's death emits nothing — tracing owns the
|
||||
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
|
||||
|
||||
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
|
||||
| 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
|
||||
authoritative here (it is taken after the call's own argument
|
||||
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)
|
||||
| Switch (subject, arms) -> emit_switch p f v e ~dst subject arms);
|
||||
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
|
||||
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
|
||||
|
|
@ -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 ->
|
||||
let r = match lookup_local f n with Some (r, _) -> r | None -> 0 in
|
||||
Hashtbl.replace f.f_node e.id r;
|
||||
emit_rc p f v ~node:e.id ~acquire:true ();
|
||||
r
|
||||
| _ ->
|
||||
(* 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 -> ())
|
||||
| _ -> emit_stmt p f v last));
|
||||
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_env <- saved_env;
|
||||
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)
|
||||
| _ -> emit_stmt p f v last));
|
||||
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_env <- saved_env;
|
||||
f.f_declared <- saved_decls;
|
||||
|
|
@ -3560,8 +3509,7 @@ and emit_stmt_body (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) : unit =
|
|||
| None -> ());
|
||||
(match Hashtbl.find_opt v.v_holder s.s_id with
|
||||
| Some kind -> mask_set f kind r
|
||||
| None -> ());
|
||||
emit_rc p f v ~node:s.s_id ~acquire:true ()
|
||||
| None -> ())
|
||||
| Assign { target; value } -> emit_assign p f v s target value
|
||||
| ExprStmt ({ kind = Ast.Switch (subj, arms); _ } as e) ->
|
||||
(* 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;
|
||||
emit_switch ~want_value:false p f v e ~dst:t subj arms;
|
||||
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
|
||||
| Some place -> ( match lookup_local f place with Some (sr, _) -> mask_clear f sr | 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)
|
||||
(value : Ast.expr) : unit =
|
||||
let overwrite = Hashtbl.mem v.v_overwrite s.s_id in
|
||||
(* a @gc value the assignment displaces is released, not dropped: the
|
||||
RC table carries that RELEASE at the assignment's own node *)
|
||||
let releases =
|
||||
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
|
||||
(* iteration 7b: a traced value an assignment displaces needs nothing —
|
||||
tracing owns its lifetime (the VM's store barrier shades it while a
|
||||
mark is live). Only OVERWRITE (owned) entries lower to a drop. *)
|
||||
match target.kind with
|
||||
| Ident n -> (
|
||||
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)
|
||||
| Some (r, ty) ->
|
||||
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
|
||||
RELEASE entry); compute the new one into a temporary first so
|
||||
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. *)
|
||||
copy_place_text p f t value;
|
||||
f.f_cur_line <- s.s_pos.line;
|
||||
if overwrite then begin
|
||||
put f (ins_abc op_drop r 0 0);
|
||||
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_drop r 0 0);
|
||||
mask_clear f r;
|
||||
put f (ins_abc op_move r t 0)
|
||||
end
|
||||
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
|
||||
Hashtbl.replace f.f_node target.id b;
|
||||
let idx = check_field_idx p f target.pos idx in
|
||||
if overwrite || releases then begin
|
||||
(* SETF never auto-drops (format doc): the compiler emits
|
||||
the destruction of the field's previous value — a DROP
|
||||
for an owned field, an rc release for a @gc one *)
|
||||
if overwrite then begin
|
||||
(* SETF never auto-drops (format doc): the compiler emits the
|
||||
destruction of the field's previous OWNED value. A traced
|
||||
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
|
||||
f.f_cur_line <- s.s_pos.line;
|
||||
put f (ins_abc op_getf old b idx);
|
||||
if overwrite then 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
|
||||
put f (ins_abc op_drop old 0 0)
|
||||
end;
|
||||
let t = alloc_temp p f value.pos in
|
||||
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 =
|
||||
match opt with
|
||||
| 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 -> ());
|
||||
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;
|
||||
put f (ins_abc op_ret0 0 0 0);
|
||||
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
|
||||
| Some place -> ( match lookup_local f place with Some (sr, _) -> mask_clear f sr | 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 -> ());
|
||||
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;
|
||||
put f (ins_abc op_ret t 0 0);
|
||||
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"
|
||||
| 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 -> ());
|
||||
emit_rc p f v ~node:s.s_id ~acquire:false ();
|
||||
f.f_cur_line <- s.s_pos.line;
|
||||
let pc = here f in
|
||||
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
|
||||
~message:"`continue` outside of a loop"
|
||||
| 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 -> ());
|
||||
emit_rc p f v ~node:s.s_id ~acquire:false ();
|
||||
f.f_cur_line <- s.s_pos.line;
|
||||
let pc = here f in
|
||||
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_decls = f.f_declared in
|
||||
List.iter (emit_stmt p f v) body;
|
||||
(* scope end: the owner table's DROPs first, then the @gc releases for
|
||||
the handles this block declared — owner.ml's own pop_scope order *)
|
||||
(* scope end: the owner table's DROPs, owner.ml's own pop_scope order *)
|
||||
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_env <- saved_env;
|
||||
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;
|
||||
f.f_loops <- List.tl f.f_loops;
|
||||
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
|
||||
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;
|
||||
|
|
@ -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;
|
||||
f.f_loops <- List.tl f.f_loops;
|
||||
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
|
||||
iteration's own scope-end cleanup (a `continue` already ran the
|
||||
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;
|
||||
f.f_cur_line <- m.pos.line;
|
||||
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
|
||||
instruction is not one, and the implicit void return is what
|
||||
control falling off the end means *)
|
||||
|
|
|
|||
|
|
@ -277,22 +277,9 @@ type drop_site = {
|
|||
dr_items : drop_item list;
|
||||
}
|
||||
|
||||
type rc_op =
|
||||
| RcAcquire
|
||||
| RcRelease
|
||||
|
||||
(* 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;
|
||||
}
|
||||
(* iteration 7b: the rc-site machinery (acquire/release pairs, elision
|
||||
groups, the clobber rule) is gone with reference counting itself — a
|
||||
traced value's aliases need no bookkeeping, tracing owns the lifetime. *)
|
||||
|
||||
type acc_kind =
|
||||
| AShared
|
||||
|
|
@ -319,7 +306,6 @@ type residual_site = {
|
|||
type tables = {
|
||||
moves : move_site list;
|
||||
drops : drop_site list;
|
||||
rcs : rc_site list;
|
||||
residuals : residual_site list;
|
||||
}
|
||||
|
||||
|
|
@ -368,7 +354,6 @@ type scope = {
|
|||
type sink = {
|
||||
mutable s_moves : move_site list;
|
||||
mutable s_drops : drop_site list;
|
||||
mutable s_rcs : rc_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
|
||||
no scope-end drops and drops out of if/else joins *)
|
||||
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
|
||||
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
|
||||
|
|
@ -860,13 +837,6 @@ let record_drop (ctx : ctx) ~node ~(pos : Ast.pos) ~kind ~items : unit =
|
|||
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 :
|
||||
unit =
|
||||
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 }
|
||||
:: ctx.sink.s_res
|
||||
|
||||
let clobber (ctx : ctx) (root : string) : unit = Hashtbl.replace ctx.clobbered root ()
|
||||
|
||||
(* ============================================================
|
||||
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 })
|
||||
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 =
|
||||
match ctx.scopes with
|
||||
| [] -> ()
|
||||
|
|
@ -944,8 +904,7 @@ let pop_scope (ctx : ctx) : unit =
|
|||
if not ctx.diverged then begin
|
||||
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)
|
||||
~items:(owned_items live);
|
||||
release_gc ctx ~node:sc.sc_node ~pos:sc.sc_pos live
|
||||
~items:(owned_items live)
|
||||
end;
|
||||
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
|
||||
~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
|
||||
positive half of `transfer`'s decision below, needed one step earlier by
|
||||
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 =
|
||||
match place_class ctx p with
|
||||
| Copy -> false
|
||||
| Gc ->
|
||||
gc_escape ctx p;
|
||||
false
|
||||
| Gc -> false (* traced values alias freely; tracing owns the lifetime *)
|
||||
| Owned -> (
|
||||
match is_borrow_root ctx p with
|
||||
| Some l ->
|
||||
|
|
@ -1155,7 +1102,6 @@ let transfer (ctx : ctx) (p : place) ~(what : string) : bool =
|
|||
else begin
|
||||
check_against_borrows ctx ~node:p.pnode ~pos:p.ppos p AMove;
|
||||
l.l_state <- Moved p.ppos;
|
||||
clobber ctx p.root;
|
||||
true
|
||||
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
|
||||
(match place_of base with
|
||||
| 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
|
||||
Some { ac_place = p; ac_kind = (if excl then AExcl else AShared) }
|
||||
else None
|
||||
|
|
@ -1329,13 +1272,6 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
|||
| None -> []
|
||||
| Some p -> (
|
||||
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
|
||||
| Copy, _ | Gc, _ -> []
|
||||
| Owned, Take ->
|
||||
|
|
@ -1366,27 +1302,10 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
|||
a.ac_place AExcl)
|
||||
accesses;
|
||||
(* transfers last *)
|
||||
(* `push`'s value argument (builtin `multi_push`) stores a @gc reference
|
||||
inside the container permanently — an escape exactly like a ctor
|
||||
field or a `take` argument. `push` is never a resolved callee (it has
|
||||
no declared params), so `conv_of` defaults it to Borrow and the
|
||||
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
|
||||
(* iteration 7b deleted the `push`-of-a-gc-value special case (an RC_INC
|
||||
escape site): a traced value stored into a container needs no
|
||||
bookkeeping — tracing finds it through the container. The bug class the
|
||||
old special case guarded against cannot recur without RC. *)
|
||||
List.iteri
|
||||
(fun i a ->
|
||||
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 transfer ctx p ~what:(Printf.sprintf "cannot be passed to `take %s`" pname) then
|
||||
record_move ctx p (MvArg pname))
|
||||
else if is_push_gc_value i && place_class ctx p = Gc then gc_escape ctx p)
|
||||
)
|
||||
args;
|
||||
record_drop ctx ~node:call_e.id ~pos:call_e.pos ~kind:DLiveMask
|
||||
~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)
|
||||
end
|
||||
else (false, Some p, Borrowed s.s_pos)))
|
||||
| Gc, None ->
|
||||
(* fresh allocation: its release is the allocation's own, never
|
||||
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)
|
||||
| Gc, None -> (true, None, Live) (* traced: no bookkeeping (7b) *)
|
||||
| Gc, Some p -> (true, Some p, Live)
|
||||
in
|
||||
declare ctx
|
||||
{ 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
|
||||
| None -> read_expr ctx target
|
||||
| Some p ->
|
||||
clobber ctx p.root;
|
||||
check_against_borrows ctx ~node:s.s_id ~pos:p.ppos p AExcl;
|
||||
if p.projs = [] then begin
|
||||
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
|
||||
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 } ]
|
||||
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
|
||||
else begin
|
||||
|
|
@ -1924,7 +1831,7 @@ and analyze_assign (ctx : ctx) (s : Ast.stmt) (target : Ast.expr) (value : Ast.e
|
|||
| Owned ->
|
||||
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 } ]
|
||||
| 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 -> ()
|
||||
end);
|
||||
(* 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));
|
||||
let live = live_holders ctx in
|
||||
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
|
||||
|
||||
(* 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 :: _ ->
|
||||
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);
|
||||
release_gc ctx ~node:s.s_id ~pos:s.s_pos live);
|
||||
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DBreak ~items:(owned_items live));
|
||||
ctx.diverged <- true
|
||||
|
||||
and analyze_continue (ctx : ctx) (s : Ast.stmt) : unit =
|
||||
|
|
@ -1997,8 +1902,7 @@ and analyze_continue (ctx : ctx) (s : Ast.stmt) : unit =
|
|||
| [] -> ()
|
||||
| loop_node :: _ ->
|
||||
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);
|
||||
release_gc ctx ~node:s.s_id ~pos:s.s_pos live);
|
||||
record_drop ctx ~node:s.s_id ~pos:s.s_pos ~kind:DContinue ~items:(owned_items live));
|
||||
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_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)
|
||||
(syms : Types.symbols) (coll : Diag.Collector.t) (sink : sink)
|
||||
~(self_class : string option) (m : Ast.method_decl) : unit =
|
||||
let ctx =
|
||||
{ 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;
|
||||
clobbered = Hashtbl.create 8; promote }
|
||||
diverged = false; promote }
|
||||
in
|
||||
push_scope ctx ~node:m.id ~pos:m.pos ~label:"BODY";
|
||||
(* `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) });
|
||||
List.iter (fun p -> declare ctx (param_local ctx p)) m.params;
|
||||
List.iter (analyze_stmt ctx) m.body;
|
||||
pop_scope ctx;
|
||||
resolve_rc ctx
|
||||
pop_scope ctx
|
||||
|
||||
(* ============================================================
|
||||
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)
|
||||
(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
|
||||
(function
|
||||
| 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
|
||||
{ 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;
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -28,26 +28,23 @@ m1 proven args=1 regs=3 [free fn]
|
|||
0002 CALL r2, m0
|
||||
0003 RET r2
|
||||
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 7 owned={r1} gc={r0}
|
||||
drops: pc 14 owned={} gc={r0}
|
||||
drops: pc 15 owned={} gc={}
|
||||
drops: pc 6 owned={r1} gc={r0}
|
||||
drops: pc 13 owned={} gc={r0}
|
||||
0000 NEW r0, c0
|
||||
0001 LOADK r1, k8
|
||||
0002 SETF r0, f0, r1
|
||||
0003 NEW r1, c1
|
||||
0004 MOVE r2, r0
|
||||
0005 RC_INC r2
|
||||
0006 SETF r1, f0, r2
|
||||
0007 MOVE r4, r1
|
||||
0008 CALL r4, m1
|
||||
0009 MOVE r3, r4
|
||||
0010 LOADK r5, k9
|
||||
0011 ADD r2, r3, r5
|
||||
0012 BUILTIN r2, r2, print_int
|
||||
0013 DROP r1
|
||||
0014 RC_DEC r0
|
||||
0015 RET0
|
||||
0005 SETF r1, f0, r2
|
||||
0006 MOVE r4, r1
|
||||
0007 CALL r4, m1
|
||||
0008 MOVE r3, r4
|
||||
0009 LOADK r5, k9
|
||||
0010 ADD r2, r3, r5
|
||||
0011 BUILTIN r2, r2, print_int
|
||||
0012 DROP r1
|
||||
0013 RET0
|
||||
== ENTRY ==
|
||||
m2
|
||||
|
|
|
|||
|
|
@ -19,5 +19,4 @@
|
|||
50:3 JOIN-DROP ELSE [a]
|
||||
58:3 RETURN [a]
|
||||
64:3 RETURN [a]
|
||||
== RC ==
|
||||
== RESIDUAL ==
|
||||
|
|
|
|||
|
|
@ -7,5 +7,4 @@
|
|||
10:3 RETURN [it]
|
||||
16:16 LIVE-MASK [bag, held]
|
||||
17:3 RETURN [held]
|
||||
== RC ==
|
||||
== RESIDUAL ==
|
||||
|
|
|
|||
|
|
@ -3,5 +3,4 @@
|
|||
== DROPS ==
|
||||
18:5 OVERWRITE self.name
|
||||
22:5 OVERWRITE self.prices
|
||||
== RC ==
|
||||
== RESIDUAL ==
|
||||
|
|
|
|||
|
|
@ -4,13 +4,4 @@
|
|||
26:15 LIVE-MASK [c:gc]
|
||||
35:18 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 ==
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
== MOVES ==
|
||||
== DROPS ==
|
||||
== RC ==
|
||||
== RESIDUAL ==
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -1965,13 +1965,6 @@ let () =
|
|||
(fun (d : Owner.drop_site) ->
|
||||
List.for_all (fun (i : Owner.drop_item) -> i.Owner.di_node > 0) d.Owner.dr_items)
|
||||
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_tables, _ = owner_str ~file:res_path (read_file res_path) in
|
||||
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)"
|
||||
(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 path = "golden/owner/moves.wo" 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 none = 0xFFFFFFFF in
|
||||
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 koff = u32 16 and kcnt = u32 20 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)
|
||||
|
||||
let () =
|
||||
(* The spec's zero-cost promise, as an assertion and not only a pinned
|
||||
dump: a method whose ownership is fully proven contains no borrow op
|
||||
and no rc op. golden/bc/elision.wo's `proven` aliases a @gc
|
||||
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. *)
|
||||
(* The zero-cost promise, iteration 7b edition: a proven method emits no
|
||||
borrow op, and NO method anywhere emits an rc op — reference counting
|
||||
is gone from the instruction stream entirely (opcodes 27/28 reserved). *)
|
||||
let path = "golden/bc/elision.wo" 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 <> "");
|
||||
List.iter
|
||||
(fun op ->
|
||||
check
|
||||
(Printf.sprintf "elision: `proven` emits no %s (zero-cost when provable)" op)
|
||||
(find_substring ~needle:op block = None))
|
||||
[ "BORROW_S"; "BORROW_X"; "RELEASE_S"; "RELEASE_X"; "RC_INC"; "RC_DEC" ];
|
||||
(* the contrast, so the fixture cannot pass by emitting nothing anywhere:
|
||||
main stores the @gc value into a field, which is a KEPT acquire *)
|
||||
let main_block = method_block (Disasm.dump image) "main" in
|
||||
check "elision: the escaping acquire in `main` is still emitted (fixture is not vacuous)"
|
||||
(find_substring ~needle:"RC_INC" main_block <> None)
|
||||
[ "BORROW_S"; "BORROW_X"; "RELEASE_S"; "RELEASE_X" ];
|
||||
List.iter
|
||||
(fun op ->
|
||||
check
|
||||
(Printf.sprintf "rc retired: the whole image contains no %s" op)
|
||||
(find_substring ~needle:op dump = None))
|
||||
[ "RC_INC"; "RC_DEC" ]
|
||||
|
||||
let () =
|
||||
(* 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)
|
||||
|
||||
(* 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
|
||||
to appear in the emitted code exactly once — and nothing else may. A
|
||||
count identity over a whole file is the cheapest way to state that, and
|
||||
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). *)
|
||||
table asks for has to appear in the emitted code exactly once — and
|
||||
nothing else may (rc ops don't exist since iteration 7b). A count
|
||||
identity over a whole file is the cheapest way to state that. *)
|
||||
let () =
|
||||
let count_op needle dump =
|
||||
String.split_on_char '\n' dump
|
||||
|
|
@ -2817,23 +2790,14 @@ let () =
|
|||
d.Owner.dr_items))
|
||||
0 tables.Owner.drops
|
||||
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 dump = Disasm.dump image in
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one DROP per owned drop-table item" path)
|
||||
~expected:want_drops ~actual:(count_op "DROP " dump) string_of_int;
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one RC_INC per KEPT acquire" path)
|
||||
~expected:(kept Owner.RcAcquire) ~actual:(count_op "RC_INC" 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;
|
||||
(Printf.sprintf "table contract %s: rc ops never appear (7b)" path)
|
||||
~expected:0 ~actual:(count_op "RC_INC" dump + count_op "RC_DEC" dump) string_of_int;
|
||||
(* The residual table is both the only licence to emit a borrow
|
||||
op and an obligation to emit one per *operand*: guards are
|
||||
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_RELEASE_S:
|
||||
case WOP_RELEASE_X:
|
||||
case WOP_RC_INC:
|
||||
case WOP_RC_DEC:
|
||||
RCHK(A);
|
||||
break;
|
||||
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_DROP] = &&L_DROP, [WOP_BORROW_S] = &&L_BORROW_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_RC_DEC] = &&L_RC_DEC, [WOP_BUILTIN] = &&L_BUILTIN,
|
||||
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_BUILTIN] = &&L_BUILTIN,
|
||||
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
||||
[WOP_TRY] = &&L_TRY, [WOP_ENDTRY] = &&L_ENDTRY,
|
||||
};
|
||||
|
|
@ -488,14 +487,6 @@ dispatch:
|
|||
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) : {
|
||||
const char *why;
|
||||
wo_str *x = str_check(R[wo_ins_b(ins)], &why);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
|
||||
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||
#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_OFF_MAGIC 0u
|
||||
#define WOB_OFF_VERSION 4u
|
||||
|
|
@ -168,8 +169,8 @@ enum {
|
|||
WOP_BORROW_X = 24,
|
||||
WOP_RELEASE_S = 25,
|
||||
WOP_RELEASE_X = 26,
|
||||
WOP_RC_INC = 27,
|
||||
WOP_RC_DEC = 28,
|
||||
/* 27-28 were RC_INC/RC_DEC — retired with reference counting (v4,
|
||||
iteration 7b). Reserved: the loader rejects them. */
|
||||
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_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) */
|
||||
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();
|
||||
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), /* rc 1 */
|
||||
wo_ins_abc(WOP_RC_INC, 0, 0, 0), /* rc 2 */
|
||||
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_abx(WOP_NEW, 0, 0),
|
||||
wo_ins_abc(27 /* retired RC_INC */, 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;
|
||||
uint8_t *img = wb_finish(b, &len);
|
||||
uint64_t ret = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_borrow_violation_frees_owned();
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue