feat: try/catch over the trap system (haxe-parity plan 8, Task 5)
VM catch frames + expression-form try/catch in the compiler. Uncaught traps
keep byte-for-byte today's surface. log-watcher parse errors 18 -> 7;
corpus 71/0, woc runtest 565/0, wovm unit gates green (both dispatch flavors).
- wob.h: WOP_TRY (A sBx: push catch frame, handler at pc+sBx) / WOP_ENDTRY;
WO_B_ERR_FILL builtin (fills the catch record: 0 code, 1 line, 2 method,
3 msg — the field-order contract with the compiler)
- vm.h/vm.c: catch stack (depth, handler pc, error reg) + the caught error;
vm_unwind takes a stop depth, so a caught trap kills every frame above the
catching one exactly as an uncaught trap would, then releases only what the
try region owned in the catching frame (drop-entry diff against the handler
pc) and resumes at the handler; RET/RET0 drop the catch frames of the frame
they leave; TRAPF resumes instead of returning when the trap was caught
- builtin.c: err_fill allocates the method/msg Texts into the record the
compiler owns, so the pending error never has to outlive the landing
- loader.c: TRY's handler target validated like a jump, error register like
any register operand; err_fill arity
- lexer/token/ast/parser: `try`/`catch` keywords; `try expr catch (e) expr`
and `catch (e) { block }`, newline allowed before `catch`; try binds looser
than every operator, so `try a / b catch (e) 0` catches the division
- types.ml: predeclared `Error` record (merged table only), catch binding,
arm-type agreement reported only when both arms are confidently typed
- owner.ml: analyze_try — the catch arm is an alternate flow join off the
entry state, the error record is an owned handler-scope local
- emit.ml: TRY/body/ENDTRY/JMP + handler prologue (NEW Error, err_fill),
join drops on both arms, `Error` class entry only for programs that catch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afd2492f05
commit
c43fa388d9
14 changed files with 561 additions and 45 deletions
|
|
@ -330,7 +330,9 @@ let typecheck_all (collector : Woc_lib.Diag.Collector.t) ~(root : string)
|
|||
let module_of = module_of_file ~root in
|
||||
Woc_lib.Types.check_modules collector ~module_of per_file_syms parsed;
|
||||
let module_syms = Woc_lib.Types.module_symbols ~module_of per_file_syms in
|
||||
let syms = merge_symbols (List.map snd per_file_syms) in
|
||||
(* haxe-parity Task 5: the predeclared `Error` record joins the merged
|
||||
table only — see Types.with_builtin_records for why not per-file. *)
|
||||
let syms = Woc_lib.Types.with_builtin_records (merge_symbols (List.map snd per_file_syms)) in
|
||||
(* `~file_syms` (hotfix, multi-file double-report): `per_file_syms` and
|
||||
`parsed` are both `List.map`s over the same original file list, in
|
||||
the same order, so pairing them positionally is exact -- each
|
||||
|
|
|
|||
|
|
@ -247,6 +247,20 @@ and expr_kind =
|
|||
literal without lookahead nothing else needs. *)
|
||||
| ListLit of expr list
|
||||
| MapLit
|
||||
(* haxe-parity Task 5: `try body catch (ename) handler` — an expression,
|
||||
like `switch`. `body` is an expression (the workload's only form);
|
||||
`handler` is a `stmt list` so both arm spellings share one shape,
|
||||
exactly as a switch arm does: `catch (e) nil` parses as a single
|
||||
ExprStmt, `catch (e) { ... }` as its statements, and the arm's value
|
||||
is its trailing ExprStmt (an arm with no trailing expression yields
|
||||
nothing, which is legal in statement position). The error record the
|
||||
handler binds is the structured trap error {code, line, method, msg}
|
||||
— the `Error` record type, predeclared by types.ml. *)
|
||||
| Try of {
|
||||
body : expr;
|
||||
ename : string;
|
||||
handler : stmt list;
|
||||
}
|
||||
|
||||
(* ---- statements (Task 5) ---------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ let kind_label (k : Token.kind) : string =
|
|||
| Token.KwCase -> "KW_CASE"
|
||||
| Token.KwDefault -> "KW_DEFAULT"
|
||||
| Token.KwTypedef -> "KW_TYPEDEF"
|
||||
| Token.KwTry -> "KW_TRY"
|
||||
| Token.KwCatch -> "KW_CATCH"
|
||||
| Token.LBrace -> "LBRACE"
|
||||
| Token.RBrace -> "RBRACE"
|
||||
| Token.LParen -> "LPAREN"
|
||||
|
|
@ -222,6 +224,10 @@ let rec expr_str (e : Ast.expr) : string =
|
|||
| Ast.Interp inner -> Printf.sprintf "INTERP(%s)" (expr_str inner)
|
||||
| Ast.ListLit items -> Printf.sprintf "[%s]" (String.concat ", " (List.map expr_str items))
|
||||
| Ast.MapLit -> "{}"
|
||||
(* Like SWITCH above: a one-line summary, not a full unparse of the
|
||||
catch arm's statements. *)
|
||||
| Ast.Try { body; ename; handler } ->
|
||||
Printf.sprintf "TRY %s CATCH (%s) { %d stmt }" (expr_str body) ename (List.length handler)
|
||||
(* haxe-parity Task 3: arm bodies are `stmt list`, not one `expr` — no
|
||||
golden AST/bc fixture pins a switch (direct assertions instead, see
|
||||
runner.ml, same convention haxe-parity Task 2 used), so this is a
|
||||
|
|
|
|||
|
|
@ -192,6 +192,10 @@ let op_rc_dec = 28
|
|||
let op_builtin = 29
|
||||
let op_db_stub = 30
|
||||
|
||||
(* haxe-parity Task 5: try/catch (runtime/src/wob.h's WOP_TRY/WOP_ENDTRY) *)
|
||||
let op_try = 32
|
||||
let op_endtry = 33
|
||||
|
||||
let b_now = 0
|
||||
let b_print = 1
|
||||
let b_print_int = 2
|
||||
|
|
@ -219,6 +223,13 @@ let b_int_to_text = 13
|
|||
types.ml's builtin_signatures — 08-builtin-surface.md is unchanged). *)
|
||||
let b_variant_tag = 14
|
||||
|
||||
(* haxe-parity Task 5: fills the catch arm's freshly allocated `Error`
|
||||
record from the trap the VM landed with (field order 0 code, 1 line,
|
||||
2 method, 3 msg — Types.error_record_fields). runtime/src/wob.h
|
||||
WO_B_ERR_FILL = 15. Compiler-internal, like b_variant_tag: never a
|
||||
source-callable name. *)
|
||||
let b_err_fill = 15
|
||||
|
||||
let ins_abc op a b c = op lor (a lsl 8) lor (b lsl 16) lor (c lsl 24)
|
||||
let ins_abx op a bx = op lor (a lsl 8) lor (bx lsl 16)
|
||||
let ins_asbx op a sbx = ins_abx op a (sbx + 32768)
|
||||
|
|
@ -867,6 +878,9 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option
|
|||
| ListLit (first :: _) -> (
|
||||
match ty_of_expr p f first with Some (Scalar n) -> Some (Multi n) | _ -> None)
|
||||
| ListLit [] | MapLit -> None
|
||||
(* haxe-parity Task 5: a `try` yields its try arm's type — types.ml has
|
||||
already required the catch arm to agree. *)
|
||||
| Try t -> ty_of_expr p f t.body
|
||||
| Ident n -> (
|
||||
match List.assoc_opt n f.f_env with
|
||||
| Some (_, t) -> Some t
|
||||
|
|
@ -1270,6 +1284,7 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
|||
| Some imm ->
|
||||
sync_mask p f v e.id;
|
||||
put f (ins_abc op_builtin dst imm b_map_new))
|
||||
| Try t -> emit_try p f v ~dst ?expected e t.body t.ename t.handler
|
||||
| Ident n -> (
|
||||
match lookup_local f n with
|
||||
| Some (r, _) -> if r <> dst then put f (ins_abc op_move dst r 0)
|
||||
|
|
@ -1802,6 +1817,100 @@ and emit_switch ?(want_value = true) (p : pctx) (f : fstate) (v : views) (e : As
|
|||
List.iter (fun (o, g, _) -> mask_meet f o g) rest;
|
||||
f.f_div <- div0
|
||||
|
||||
(* haxe-parity Task 5: `try body catch (e) handler`.
|
||||
|
||||
TRY ereg, ->handler register the region; ereg is where the error
|
||||
<body -> dst> record lands if it fires
|
||||
ENDTRY the region completed: pop it
|
||||
JMP ->exit
|
||||
handler:
|
||||
NEW ereg, Error the record is the compiler's allocation, so
|
||||
BUILTIN ereg, err_fill its drop is the ordinary scope-end one
|
||||
<handler -> dst>
|
||||
<scope drops, join drops>
|
||||
exit:
|
||||
|
||||
The two arms are joined exactly like a switch's: each arm's ending
|
||||
owned/gc masks meet, and each drops what the other moved
|
||||
(emit_join_drops with the labels owner.ml's analyze_try recorded). The
|
||||
VM releases whatever the body itself owned before landing — the live
|
||||
mask at the try site is what it reads — so the handler starts from the
|
||||
entry state, which is what the owner pass assumed. *)
|
||||
and emit_try (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : Ast.expr)
|
||||
(body : Ast.expr) (ename : string) (handler : Ast.stmt list) : unit =
|
||||
(* dst is reserved for the whole construct — same reason emit_switch
|
||||
does it: a handler-local `let` must never be handed dst's register *)
|
||||
let saved_nlocals = f.f_nlocals in
|
||||
if f.f_nlocals <= dst then f.f_nlocals <- dst + 1;
|
||||
if f.f_temp <= dst then f.f_temp <- dst + 1;
|
||||
bump f dst;
|
||||
let ereg = alloc_local p f e.pos in
|
||||
f.f_cur_line <- e.pos.line;
|
||||
let try_pc = here f in
|
||||
put f (ins_asbx op_try ereg 0);
|
||||
let entry_owned = f.f_owned and entry_gc = f.f_gc in
|
||||
let div0 = f.f_div in
|
||||
(match expected with
|
||||
| Some t -> emit_expr p f v ~dst ~expected:t body
|
||||
| None -> emit_expr p f v ~dst body);
|
||||
f.f_cur_line <- e.pos.line;
|
||||
put f (ins_abc op_endtry 0 0 0);
|
||||
emit_join_drops p f v ~node:e.id ~label:"TRYBODY";
|
||||
let body_owned = f.f_owned and body_gc = f.f_gc and body_div = f.f_div in
|
||||
let skip_pc = here f in
|
||||
put f (ins_asbx op_jmp 0 0);
|
||||
patch_jump p f ~file:f.f_file ~pos:e.pos try_pc (here f);
|
||||
f.f_owned <- entry_owned;
|
||||
f.f_gc <- entry_gc;
|
||||
f.f_div <- div0;
|
||||
let saved_env = f.f_env and saved_decls = f.f_declared in
|
||||
let saved_locals = f.f_nlocals in
|
||||
(match class_of_name p Types.error_record_name with
|
||||
| Some ecid ->
|
||||
f.f_cur_line <- e.pos.line;
|
||||
put f (ins_abx op_new ereg (check_bx p f e.pos "class" ecid));
|
||||
put f (ins_abc op_builtin ereg ereg b_err_fill)
|
||||
| None ->
|
||||
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||
~message:"no class-table entry for the `Error` record — a `try` cannot bind its error";
|
||||
put f (ins_abx op_loadk ereg (const_int p 0)));
|
||||
f.f_env <- (ename, (ereg, Ast.Scalar Types.error_record_name)) :: f.f_env;
|
||||
Hashtbl.replace f.f_decl e.id ereg;
|
||||
f.f_declared <- e.id :: f.f_declared;
|
||||
mask_set f (match Hashtbl.find_opt v.v_holder e.id with Some k -> k | None -> Owner.LOwned) ereg;
|
||||
(match List.rev handler with
|
||||
| [] -> ()
|
||||
| last :: rev_init -> (
|
||||
List.iter (emit_stmt p f v) (List.rev rev_init);
|
||||
match last.Ast.s_kind with
|
||||
| Ast.ExprStmt ve ->
|
||||
stmt_reset f;
|
||||
f.f_cur_line <- last.Ast.s_pos.line;
|
||||
(match expected with
|
||||
| Some t -> emit_expr p f v ~dst ~expected:t ve
|
||||
| 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;
|
||||
f.f_temp <- saved_locals;
|
||||
emit_join_drops p f v ~node:e.id ~label:"CATCHJOIN";
|
||||
patch_jump p f ~file:f.f_file ~pos:e.pos skip_pc (here f);
|
||||
f.f_nlocals <- saved_nlocals;
|
||||
(* the state after the try is what both arms agree on *)
|
||||
if body_div then ()
|
||||
else if f.f_div then begin
|
||||
f.f_owned <- body_owned;
|
||||
f.f_gc <- body_gc;
|
||||
f.f_div <- div0
|
||||
end
|
||||
else begin
|
||||
mask_meet f body_owned body_gc;
|
||||
f.f_div <- div0
|
||||
end
|
||||
|
||||
and emit_ctor (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr) (cn : string)
|
||||
(fields : (string * Ast.expr) list) : unit =
|
||||
match class_of_name p cn with
|
||||
|
|
@ -2921,6 +3030,16 @@ let emit_method (p : pctx) (v : views) ~(file : string) ~(self_class : (int * st
|
|||
Program assembly
|
||||
============================================================ *)
|
||||
|
||||
(* haxe-parity Task 5: does this program catch anywhere? Only then does the
|
||||
`Error` record earn a class-table entry — so no image that never writes
|
||||
`try` gains a class it does not use. *)
|
||||
let program_uses_try (prog : Ast.program) : bool =
|
||||
let found = ref false in
|
||||
Types.walk_program
|
||||
(fun _ (e : Ast.expr) -> match e.Ast.kind with Ast.Try _ -> found := true | _ -> ())
|
||||
prog;
|
||||
!found
|
||||
|
||||
(* Structural satisfaction, Go-style (spec section 2): a class satisfies
|
||||
an interface when it has a method of the same name and parameter count
|
||||
for every method the interface declares. There is no `implements`
|
||||
|
|
@ -3056,6 +3175,25 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
| Ast.Const _ -> ())
|
||||
u.prog.decls)
|
||||
units;
|
||||
(* haxe-parity Task 5: the record `catch (e)` binds needs a real
|
||||
class-table entry (it is an ordinary heap object with two owned Texts,
|
||||
so the drop plan is the ordinary one). Added only for a program that
|
||||
actually catches — every existing image keeps its exact class table —
|
||||
and only when nothing already claims the name. Its field order is
|
||||
Types.error_record_fields, which is the same order the VM's
|
||||
WO_B_ERR_FILL builtin writes. *)
|
||||
if
|
||||
(not (SM.mem Types.error_record_name !class_id))
|
||||
&& List.exists (fun u -> program_uses_try u.prog) units
|
||||
then begin
|
||||
let cid = !nclasses in
|
||||
class_id := SM.add Types.error_record_name cid !class_id;
|
||||
incr nclasses;
|
||||
classes :=
|
||||
{ cr_name = Types.error_record_name; cr_gc = false;
|
||||
cr_fields = Array.of_list Types.error_record_fields; cr_methods = [] }
|
||||
:: !classes
|
||||
end;
|
||||
let class_id = !class_id in
|
||||
let p_classes = Array.of_list (List.rev !classes) in
|
||||
let p_ifaces = Array.of_list (List.rev !ifaces) in
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ let keyword_kind = function
|
|||
| "case" -> Some Token.KwCase
|
||||
| "default" -> Some Token.KwDefault
|
||||
| "typedef" -> Some Token.KwTypedef
|
||||
| "try" -> Some Token.KwTry
|
||||
| "catch" -> Some Token.KwCatch
|
||||
| "INSERT" -> Some Token.KwInsert
|
||||
| "SELECT" -> Some Token.KwSelect
|
||||
| _ -> None
|
||||
|
|
|
|||
|
|
@ -511,6 +511,9 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option =
|
|||
| ListLit (first :: _) -> (
|
||||
match expr_ty ctx first with Some (Scalar n) -> Some (Multi n) | _ -> None)
|
||||
| ListLit [] | MapLit -> None
|
||||
(* haxe-parity Task 5: a `try` yields its try arm's type (types.ml has
|
||||
already required the catch arm to agree). *)
|
||||
| Try t -> expr_ty ctx t.body
|
||||
| Ident n -> (
|
||||
match find_local ctx n with
|
||||
| Some l -> Some l.l_ty
|
||||
|
|
@ -1063,6 +1066,7 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit =
|
|||
literal-specific one. *)
|
||||
| ListLit items -> List.iter (read_expr ctx) items
|
||||
| MapLit -> ()
|
||||
| Try t -> analyze_try ctx e t.body t.ename t.handler
|
||||
| DbStub _ ->
|
||||
(* trap-capable: the frame needs its drop map here *)
|
||||
record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx))
|
||||
|
|
@ -1556,6 +1560,51 @@ and fixpoint (ctx : ctx) (run : unit -> unit) : unit =
|
|||
else today. Borrows only (l_holds = false), so pop_scope's drop
|
||||
recording never sees them; every pre-existing call site passes
|
||||
nothing and is byte-identical. *)
|
||||
(* haxe-parity Task 5: `try body catch (e) handler`. The two arms are
|
||||
alternate flows joining at one point — the same shape `if`/`switch`
|
||||
already have, so the same join machinery applies: whatever one arm
|
||||
moved out, the arm that still holds it drops at its own end
|
||||
(branch_join_drops), and the state after the whole expression is the
|
||||
join of both.
|
||||
|
||||
What is genuinely different from a branch is WHERE the catch arm starts
|
||||
from: a trap can be raised anywhere inside the body, so the handler may
|
||||
run after *any prefix* of it. Taking the entry state as the handler's
|
||||
starting point is the conservative reading — it never claims the body's
|
||||
moves happened — and the join then makes the surviving path responsible
|
||||
for the drop. The frame also needs a live mask at the try itself (like
|
||||
every other trap-capable site): that mask is what the VM's unwind uses
|
||||
to release the body's own values before landing in the handler.
|
||||
|
||||
`e` is a local of the predeclared `Error` record, owned by the handler
|
||||
scope (the record and its two Texts are freshly allocated at landing),
|
||||
so pop_scope records its drop like any other owned local's. *)
|
||||
and analyze_try (ctx : ctx) (e : Ast.expr) (body : Ast.expr) (ename : string)
|
||||
(handler : Ast.stmt list) : unit =
|
||||
record_drop ctx ~node:e.id ~pos:e.pos ~kind:DLiveMask ~items:(mask_items (live_holders ctx));
|
||||
let entry = snapshot ctx in
|
||||
let div0 = ctx.diverged in
|
||||
read_expr ctx body;
|
||||
let body_sn = snapshot ctx in
|
||||
let body_div = ctx.diverged in
|
||||
restore entry;
|
||||
ctx.diverged <- div0;
|
||||
let ety = Ast.Scalar Types.error_record_name in
|
||||
let ebind =
|
||||
{ l_name = ename; l_ty = ety; l_class = oclass_of ctx ety; l_node = e.id; l_pos = e.pos;
|
||||
l_holds = (match oclass_of ctx ety with Copy -> false | _ -> true); l_src = None;
|
||||
l_bkind = AShared; l_state = Live }
|
||||
in
|
||||
analyze_block ctx ~pre:[ ebind ] ~node:e.id ~pos:e.pos ~label:"CATCH" handler;
|
||||
let catch_sn = snapshot ctx in
|
||||
let catch_div = ctx.diverged in
|
||||
if (not body_div) && not catch_div then begin
|
||||
branch_join_drops ctx ~node:e.id ~label:"TRYBODY" ~pos:e.pos ~moving:catch_sn ~other:body_sn;
|
||||
branch_join_drops ctx ~node:e.id ~label:"CATCHJOIN" ~pos:e.pos ~moving:body_sn ~other:catch_sn
|
||||
end;
|
||||
restore (if body_div then catch_sn else if catch_div then body_sn else join body_sn catch_sn);
|
||||
ctx.diverged <- body_div && catch_div
|
||||
|
||||
and analyze_block (ctx : ctx) ?(pre = []) ~node ~pos ~label (body : Ast.stmt list) : unit =
|
||||
push_scope ctx ~node ~pos ~label;
|
||||
List.iter (declare ctx) pre;
|
||||
|
|
|
|||
|
|
@ -719,7 +719,35 @@ let with_no_brace (st : state) (value : bool) (f : unit -> 'a) : 'a =
|
|||
|
||||
(* ---- expression parsing -------------------------------------------------- *)
|
||||
|
||||
let rec parse_expr (st : state) : Ast.expr = parse_or st
|
||||
let rec parse_expr (st : state) : Ast.expr =
|
||||
match peek st with Token.KwTry -> parse_try st | _ -> parse_or st
|
||||
|
||||
(* haxe-parity Task 5: `try body catch (e) arm`. `try` binds looser than
|
||||
every operator, so the body is a full operator expression and `catch`
|
||||
is what ends it (`try a / b catch (e) 0` catches the division, not just
|
||||
`a`). A newline before `catch` is insignificant — the workload wraps
|
||||
long try bodies (mcp.wo's `try self.dispatch(...)` / `catch (e)
|
||||
err(...)`). The arm is either a braced block or one expression; both
|
||||
become a `stmt list`, so `{}` right after the catch variable is always
|
||||
the empty block, never the empty-map literal. *)
|
||||
and parse_try (st : state) : Ast.expr =
|
||||
let pos = peek_pos st in
|
||||
let id = fresh_id st in
|
||||
ignore (advance st);
|
||||
(* 'try' *)
|
||||
let body = parse_or st in
|
||||
skip_newlines st;
|
||||
expect st Token.KwCatch "`catch` after a `try` expression";
|
||||
expect st Token.LParen "'(' before the catch variable";
|
||||
let ename = expect_ident st "catch variable name" in
|
||||
expect st Token.RParen "')' after the catch variable";
|
||||
let handler =
|
||||
if peek st = Token.LBrace then with_no_brace st false (fun () -> parse_block st)
|
||||
else
|
||||
let e = parse_expr st in
|
||||
[ { Ast.s_id = fresh_id st; s_pos = e.pos; s_kind = Ast.ExprStmt e } ]
|
||||
in
|
||||
{ Ast.id; pos; kind = Ast.Try { body; ename; handler } }
|
||||
|
||||
and parse_or (st : state) : Ast.expr =
|
||||
let lhs = ref (parse_and st) in
|
||||
|
|
@ -1730,6 +1758,13 @@ let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : As
|
|||
| Ast.Interp inner -> { e with Ast.kind = Ast.Interp (subst_expr consts bound inner) }
|
||||
| Ast.ListLit items -> { e with Ast.kind = Ast.ListLit (List.map (subst_expr consts bound) items) }
|
||||
| Ast.MapLit -> e
|
||||
| Ast.Try { body; ename; handler } ->
|
||||
{ e with
|
||||
Ast.kind =
|
||||
Ast.Try
|
||||
{ body = subst_expr consts bound body; ename;
|
||||
handler = subst_block consts (StringSet.add ename bound) handler }
|
||||
}
|
||||
| Ast.Switch (subject, arms) ->
|
||||
{ e with
|
||||
Ast.kind =
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ type kind =
|
|||
| KwSwitch
|
||||
| KwCase
|
||||
| KwDefault
|
||||
(* haxe-parity Task 5: `try expr catch (e) arm` — real keywords, and
|
||||
neither appears as an identifier anywhere in the corpus or the
|
||||
driving workload (grepped, the same discipline every keyword above
|
||||
followed). *)
|
||||
| KwTry
|
||||
| KwCatch
|
||||
(* haxe-parity Task 4: `typedef Name = { ... }` structural records. A
|
||||
real keyword (grepped the corpus/sample first, same discipline as
|
||||
every keyword above — `typedef` appears only as this declaration's
|
||||
|
|
|
|||
|
|
@ -173,6 +173,33 @@ let stdlib_modules = [ "fs"; "proc"; "net"; "time"; "json"; "env" ]
|
|||
|
||||
let is_stdlib_module (name : string) : bool = List.mem name stdlib_modules
|
||||
|
||||
(* haxe-parity Task 5: the record `catch (e)` binds — the VM's structured
|
||||
trap error, one shape forever (spec §6). Predeclared rather than
|
||||
written: no source declares it, every program that catches gets it, and
|
||||
the field ORDER here is the contract with the VM's WO_B_ERR_FILL
|
||||
builtin (runtime/src/wob.h), which writes fields 0..3 by index. *)
|
||||
let error_record_name = "Error"
|
||||
|
||||
let error_record_fields : (string * field_ty) list =
|
||||
[ ("code", Scalar "Int"); ("line", Scalar "Int"); ("method", Scalar "Text");
|
||||
("msg", Scalar "Text") ]
|
||||
|
||||
(* Adds the predeclared records to a symbol table. Applied to the MERGED
|
||||
table only (bin/main.ml), never to a per-file one: one entry per file
|
||||
would read as a cross-file duplicate declaration (WO-E214). A source
|
||||
that declares its own `Error` keeps it — its own fields are then the
|
||||
ones `catch (e)` binds, which is either what it wanted or a type error
|
||||
it will hear about at the use site. *)
|
||||
let with_builtin_records (syms : symbols) : symbols =
|
||||
if StringMap.mem error_record_name syms.classes then syms
|
||||
else
|
||||
let info =
|
||||
{ name = error_record_name; is_class = false; is_record = true; is_gc = false; table = None;
|
||||
fields = List.map (fun (n, t) -> (n, t, None, [])) error_record_fields; methods = [];
|
||||
id = -1; pos = { line = 0; col = 0 }; pub = true }
|
||||
in
|
||||
{ syms with classes = StringMap.add error_record_name info syms.classes }
|
||||
|
||||
let rec has_recursive_structure (cls : class_info) : bool =
|
||||
List.exists (fun (_, ty, _, _) ->
|
||||
match ty with
|
||||
|
|
@ -844,6 +871,10 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
| ListLit (first :: _) -> (
|
||||
match confident_typ cenv first with Some t -> Some (TMulti t) | None -> None)
|
||||
| ListLit [] | MapLit -> None
|
||||
(* haxe-parity Task 5: a `try` expression's type is its try arm's — the
|
||||
handler is checked to agree (typecheck_expr below), so either arm
|
||||
would answer, and the try arm is the one that always has a value. *)
|
||||
| Try { body; _ } -> confident_typ cenv body
|
||||
| Ident name -> StringMap.find_opt name cenv
|
||||
| Field (base, field_name) -> (
|
||||
match confident_typ cenv base with
|
||||
|
|
@ -1169,6 +1200,40 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
| t :: _ -> { typ = TMulti t; is_nil = false }
|
||||
| [] -> { typ = TScalar "Int"; is_nil = false })
|
||||
| MapLit -> { typ = TScalar "Int"; is_nil = false }
|
||||
| Try { body; ename; handler } ->
|
||||
let body_res = typecheck_expr env cenv body in
|
||||
(* The catch arm sees exactly one new name: the error record. *)
|
||||
let herr = TScalar error_record_name in
|
||||
let benv = StringMap.add ename herr env in
|
||||
let bcenv = StringMap.add ename herr cenv in
|
||||
let handler_res =
|
||||
match List.rev handler with
|
||||
| [] -> None
|
||||
| last :: rev_init -> (
|
||||
let env', cenv' = List.fold_left typecheck_stmt (benv, bcenv) (List.rev rev_init) in
|
||||
match last.s_kind with
|
||||
| ExprStmt e -> Some (confident_typ cenv' e, e.pos)
|
||||
| _ ->
|
||||
let _ = typecheck_stmt (env', cenv') last in
|
||||
None)
|
||||
in
|
||||
(* Both arms must yield one type where the value is used. Reported
|
||||
only when BOTH types are confident — the same "stay silent when
|
||||
underivable" contract every other confident-type consumer here
|
||||
follows, which also keeps a `catch (e) nil` arm quiet until
|
||||
optionals land. *)
|
||||
(match (handler_res, confident_typ cenv body) with
|
||||
| Some (Some ht, hpos), Some bt when not (typ_equal syms ht bt) ->
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:type_mismatch_code ~file ~line:hpos.line ~col:hpos.col
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"the `catch` arm yields `%s`, but the `try` arm yields `%s` — both arms of \
|
||||
a `try` expression must have one type"
|
||||
(typ_label ht) (typ_label bt))
|
||||
())
|
||||
| _ -> ());
|
||||
{ typ = body_res.typ; is_nil = false }
|
||||
|
||||
(* haxe-parity Task 3: the one deriver behind both `Switch` call sites
|
||||
-- `typecheck_expr`'s own case above (every "the value is used"
|
||||
|
|
@ -1798,6 +1863,9 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e
|
|||
| Interp inner -> walk_expr bound visit inner
|
||||
| ListLit items -> List.iter (walk_expr bound visit) items
|
||||
| MapLit -> ()
|
||||
| Try { body; ename; handler } ->
|
||||
walk_expr bound visit body;
|
||||
walk_block (StringSet.add ename bound) visit handler
|
||||
| DbStub _ -> ()
|
||||
| Switch (subject, arms) ->
|
||||
walk_expr bound visit subject;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "builtin.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "cont.h"
|
||||
|
|
@ -178,6 +179,43 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
R[A] = o->class_id;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_ERR_FILL: { /* haxe-parity compiler Task 5: try/catch */
|
||||
/* Fills the catch arm's record from the error the VM landed with.
|
||||
* Field order is this builtin's contract with the compiler
|
||||
* (docs/plan/oop-vm/00-wob-format.md): 0 code, 1 line, 2 method,
|
||||
* 3 msg. The record is the compiler's own allocation, so its drop
|
||||
* is the ordinary one and the two fresh Texts belong to it. */
|
||||
if (!R[B]) {
|
||||
*msg = "null error record";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
wo_hdr *o = (wo_hdr *)(uintptr_t)R[B];
|
||||
if (o->class_id >= vm->mod->class_cnt ||
|
||||
vm->mod->classes[o->class_id].field_cnt < 4) {
|
||||
*msg = "error record is not a 4-field class";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
wo_str *meth = wo_str_new(rt, vm->caught.method,
|
||||
(uint32_t)strlen(vm->caught.method));
|
||||
if (!meth) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
wo_str *text = wo_str_new(rt, vm->caught.msg,
|
||||
(uint32_t)strlen(vm->caught.msg));
|
||||
if (!text) {
|
||||
wo_str_free(rt, meth);
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
uint64_t *fs = wo_fields(o);
|
||||
fs[0] = vm->caught.code;
|
||||
fs[1] = vm->caught.line;
|
||||
fs[2] = (uint64_t)(uintptr_t)meth;
|
||||
fs[3] = (uint64_t)(uintptr_t)text;
|
||||
R[A] = R[B];
|
||||
return 0;
|
||||
}
|
||||
default: /* unreachable: loader validated the id */
|
||||
*msg = "unknown builtin";
|
||||
return WO_T_EXPLICIT;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
|
|||
[WO_B_MULTI_GET] = 2, [WO_B_COUNT] = 1, [WO_B_LATEST] = 1,
|
||||
[WO_B_MAP_NEW] = 0, [WO_B_MAP_SET] = 3, [WO_B_MAP_GET] = 2,
|
||||
[WO_B_MAP_HAS] = 2, [WO_B_INT_TO_TEXT] = 1,
|
||||
[WO_B_VARIANT_TAG] = 1,
|
||||
[WO_B_VARIANT_TAG] = 1, [WO_B_ERR_FILL] = 1,
|
||||
};
|
||||
|
||||
static int vtab_cmp(const void *a, const void *b) {
|
||||
|
|
@ -389,6 +389,20 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
case WOP_DB_STUB:
|
||||
case WOP_TRAP:
|
||||
break;
|
||||
/* haxe-parity compiler Task 5: the handler target is validated
|
||||
exactly like a jump (it IS a jump the VM takes on a trap),
|
||||
and A is the register the catch arm's error record lands
|
||||
in, so it has to be inside the frame. */
|
||||
case WOP_TRY: {
|
||||
RCHK(A);
|
||||
int64_t tgt = (int64_t)pc + 1 + wo_ins_sbx(ins);
|
||||
if (tgt < 0 || tgt >= (int64_t)mm->ninstr)
|
||||
BAIL("method %u pc %u: catch handler out of code", (unsigned)i,
|
||||
(unsigned)pc);
|
||||
break;
|
||||
}
|
||||
case WOP_ENDTRY:
|
||||
break;
|
||||
default:
|
||||
BAIL("method %u pc %u: unknown opcode %u", (unsigned)i,
|
||||
(unsigned)pc, (unsigned)op);
|
||||
|
|
|
|||
180
runtime/src/vm.c
180
runtime/src/vm.c
|
|
@ -28,30 +28,60 @@ void wo_vm_destroy(wo_vm *vm) { wo_rt_destroy(&vm->rt); }
|
|||
* register does not block its drop — the borrower IS the dying frame.
|
||||
* Window overlap is safe: a slot dropped by the callee frame is nulled, so
|
||||
* an outer mask covering the same physical slot sees 0 and skips. */
|
||||
static void vm_unwind(wo_vm *vm) {
|
||||
const wo_module *mod = vm->mod;
|
||||
for (uint32_t d = vm->depth; d > 0; d--) {
|
||||
const wo_frame *f = &vm->frames[d - 1];
|
||||
const wo_methodrec *me = &mod->methods[f->method];
|
||||
uint32_t fpc = (d == vm->depth) ? f->pc : f->pc - 1;
|
||||
const wo_dropent *ent = NULL; /* last entry with pc <= fpc */
|
||||
for (uint32_t i = 0; i < me->drop_cnt && me->drops[i].pc <= fpc; i++)
|
||||
ent = &me->drops[i];
|
||||
if (!ent) continue; /* no entry: nothing live in this frame */
|
||||
uint64_t *R = vm->regs + f->base;
|
||||
for (uint32_t r = 0; r < me->reg_cnt; r++) {
|
||||
uint64_t bit = 1ull << r;
|
||||
if ((ent->owned & bit) && R[r]) {
|
||||
wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||
R[r] = 0;
|
||||
}
|
||||
if ((ent->gc & bit) && R[r]) {
|
||||
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||
R[r] = 0;
|
||||
}
|
||||
/* The drop-table entry governing instruction [pc]: the last one recorded
|
||||
* at or before it. NULL = nothing live there. */
|
||||
static const wo_dropent *vm_dropent(const wo_methodrec *me, uint32_t pc) {
|
||||
const wo_dropent *ent = NULL;
|
||||
for (uint32_t i = 0; i < me->drop_cnt && me->drops[i].pc <= pc; i++) ent = &me->drops[i];
|
||||
return ent;
|
||||
}
|
||||
|
||||
/* Release what frame [d-1] owns at [pc] but no longer owns at [keep_pc] —
|
||||
* the values the abandoned region of that frame created. [keep_pc] =
|
||||
* UINT32_MAX means "keep nothing", which is the dying-frame case every
|
||||
* uncaught trap uses. A borrow held by a dying register does not block
|
||||
* its drop — the borrower IS the dying region. */
|
||||
static void vm_release_frame(wo_vm *vm, uint32_t d, uint32_t pc, uint32_t keep_pc) {
|
||||
const wo_frame *f = &vm->frames[d - 1];
|
||||
const wo_methodrec *me = &vm->mod->methods[f->method];
|
||||
const wo_dropent *ent = vm_dropent(me, pc);
|
||||
if (!ent) return; /* no entry: nothing live in this frame */
|
||||
uint64_t keep_owned = 0, keep_gc = 0;
|
||||
if (keep_pc != UINT32_MAX) {
|
||||
const wo_dropent *k = vm_dropent(me, keep_pc);
|
||||
if (k) {
|
||||
keep_owned = k->owned;
|
||||
keep_gc = k->gc;
|
||||
}
|
||||
}
|
||||
vm->depth = 0;
|
||||
uint64_t *R = vm->regs + f->base;
|
||||
for (uint32_t r = 0; r < me->reg_cnt; r++) {
|
||||
uint64_t bit = 1ull << r;
|
||||
if ((ent->owned & bit) && !(keep_owned & bit) && R[r]) {
|
||||
wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||
R[r] = 0;
|
||||
}
|
||||
if ((ent->gc & bit) && !(keep_gc & bit) && R[r]) {
|
||||
wo_rc_dec(&vm->rt, (wo_hdr *)(uintptr_t)R[r]);
|
||||
R[r] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Trap unwinding — the spec's "traps never leak" promise (spec §6). Walk
|
||||
* frames innermost to outermost down to (not including) [stop_depth];
|
||||
* in each, the governing instruction is the trap pc for the innermost
|
||||
* frame and the instruction before the saved resume pc — i.e. the CALL —
|
||||
* for every outer frame. Window overlap is safe: a slot dropped by the
|
||||
* callee frame is nulled, so an outer mask covering the same physical
|
||||
* slot sees 0 and skips. stop_depth is 0 for an uncaught trap (the whole
|
||||
* stack dies) and the catching frame's depth for a caught one. */
|
||||
static void vm_unwind(wo_vm *vm, uint32_t stop_depth) {
|
||||
for (uint32_t d = vm->depth; d > stop_depth; d--) {
|
||||
const wo_frame *f = &vm->frames[d - 1];
|
||||
vm_release_frame(vm, d, (d == vm->depth) ? f->pc : f->pc - 1, UINT32_MAX);
|
||||
}
|
||||
vm->depth = stop_depth;
|
||||
}
|
||||
|
||||
/* Residual runtime checks the loader cannot do statically (registers are
|
||||
|
|
@ -89,25 +119,58 @@ static wo_str *str_check(uint64_t v, const char **why) {
|
|||
return s;
|
||||
}
|
||||
|
||||
/* Fills [out] with the trap's structured error (spec §6): the code, the
|
||||
* source line of the trapping pc, the trapping method's name, and the
|
||||
* message. One shape forever — the CLI prints it, and the catch arm of a
|
||||
* `try` binds exactly the same four fields. */
|
||||
static void vm_fill_err(wo_vm *vm, wo_err *out, uint32_t tcode, const char *fmt, va_list ap) {
|
||||
const wo_module *mod = vm->mod;
|
||||
const wo_frame *f = &vm->frames[vm->depth - 1];
|
||||
const wo_methodrec *me = &mod->methods[f->method];
|
||||
out->code = tcode;
|
||||
out->line = 0; /* last line-table entry with pc <= trapping pc */
|
||||
for (uint32_t i = 0; i < me->line_cnt && me->lines[i].pc <= f->pc; i++)
|
||||
out->line = me->lines[i].line;
|
||||
const wo_str *nm = mod->consts[me->name].s;
|
||||
int nlen = nm->len < 63 ? (int)nm->len : 63;
|
||||
snprintf(out->method, sizeof(out->method), "%.*s", nlen, nm->data);
|
||||
vsnprintf(out->msg, sizeof(out->msg), fmt, ap);
|
||||
}
|
||||
|
||||
/* 0 = the trap was caught: the stack is unwound down to the catching
|
||||
* frame, that frame's pc now points at the handler, and the caller must
|
||||
* reload and keep interpreting. -1 = uncaught: *err is filled and the
|
||||
* stack is fully unwound (depth 0), exactly as before Task 5. */
|
||||
static int vm_trap(wo_vm *vm, wo_err *err, uint32_t tcode, const char *fmt,
|
||||
...) {
|
||||
if (err) {
|
||||
const wo_module *mod = vm->mod;
|
||||
const wo_frame *f = &vm->frames[vm->depth - 1];
|
||||
const wo_methodrec *me = &mod->methods[f->method];
|
||||
err->code = tcode;
|
||||
err->line = 0; /* last line-table entry with pc <= trapping pc */
|
||||
for (uint32_t i = 0; i < me->line_cnt && me->lines[i].pc <= f->pc; i++)
|
||||
err->line = me->lines[i].line;
|
||||
const wo_str *nm = mod->consts[me->name].s;
|
||||
int nlen = nm->len < 63 ? (int)nm->len : 63;
|
||||
snprintf(err->method, sizeof(err->method), "%.*s", nlen, nm->data);
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(err->msg, sizeof(err->msg), fmt, ap);
|
||||
va_end(ap);
|
||||
/* The record the catch arm reads is always filled, even when the
|
||||
* caller passed no err: it is the value `catch (e)` binds. */
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vm_fill_err(vm, &vm->caught, tcode, fmt, ap);
|
||||
va_end(ap);
|
||||
if (vm->ncatch) {
|
||||
const wo_catch *c = &vm->catches[vm->ncatch - 1];
|
||||
uint32_t cdepth = c->depth;
|
||||
uint32_t hpc = c->pc;
|
||||
/* Which instruction governs the catching frame's own live set has
|
||||
* to be decided before unwinding moves the depth: the trapping
|
||||
* instruction when the trap was raised in this very frame, the
|
||||
* CALL (pc - 1, the saved pc is the resume point) when it came
|
||||
* from deeper. */
|
||||
int trapped_here = (cdepth == vm->depth);
|
||||
vm->ncatch--;
|
||||
/* frames above the catching one die whole */
|
||||
vm_unwind(vm, cdepth);
|
||||
/* in the catching frame only the try region's own values die: the
|
||||
* handler's drop entry names what survives into the catch arm */
|
||||
wo_frame *cf = &vm->frames[cdepth - 1];
|
||||
vm_release_frame(vm, cdepth, trapped_here ? cf->pc : cf->pc - 1, hpc);
|
||||
cf->pc = hpc;
|
||||
return 0;
|
||||
}
|
||||
vm_unwind(vm);
|
||||
if (err) *err = vm->caught;
|
||||
vm_unwind(vm, 0);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -127,11 +190,18 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
|||
R = vm->regs + vm->frames[vm->depth - 1].base; \
|
||||
} while (0)
|
||||
|
||||
/* pc is post-incremented at dispatch: the trapping instruction is pc-1 */
|
||||
/* pc is post-incremented at dispatch: the trapping instruction is pc-1.
|
||||
* A caught trap (vm_trap == 0) has already unwound to the handler's frame
|
||||
* and pointed it at the handler, so the interpreter just reloads and
|
||||
* keeps going — the same macro serves both surfaces. */
|
||||
#define TRAPF(tcode, ...) \
|
||||
do { \
|
||||
vm->frames[vm->depth - 1].pc = pc - 1; \
|
||||
return vm_trap(vm, err, tcode, __VA_ARGS__); \
|
||||
if (vm_trap(vm, err, tcode, __VA_ARGS__) == 0) { \
|
||||
RELOAD(); \
|
||||
NEXT(); \
|
||||
} \
|
||||
return -1; \
|
||||
} while (0)
|
||||
|
||||
RELOAD();
|
||||
|
|
@ -157,6 +227,7 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
|||
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_RC_INC] = &&L_RC_INC,
|
||||
[WOP_RC_DEC] = &&L_RC_DEC, [WOP_BUILTIN] = &&L_BUILTIN,
|
||||
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
||||
[WOP_TRY] = &&L_TRY, [WOP_ENDTRY] = &&L_ENDTRY,
|
||||
};
|
||||
#define CASE(name) L_##name
|
||||
#define NEXT() \
|
||||
|
|
@ -262,10 +333,18 @@ dispatch:
|
|||
NEXT();
|
||||
}
|
||||
|
||||
/* A frame leaving takes its still-open try regions with it: a `return`
|
||||
* out of a try region never runs its ENDTRY, and a handler pc in a frame
|
||||
* that no longer exists would land the next trap on a dead window. */
|
||||
#define DROP_CATCHES() \
|
||||
while (vm->ncatch && vm->catches[vm->ncatch - 1].depth > vm->depth) \
|
||||
vm->ncatch--
|
||||
|
||||
CASE(RET) : {
|
||||
uint64_t rv = R[wo_ins_a(ins)];
|
||||
vm->regs[vm->frames[vm->depth - 1].base] = rv;
|
||||
vm->depth--;
|
||||
DROP_CATCHES();
|
||||
if (vm->depth == 0) {
|
||||
*ret = rv;
|
||||
return 0;
|
||||
|
|
@ -276,6 +355,7 @@ dispatch:
|
|||
CASE(RET0) : {
|
||||
vm->regs[vm->frames[vm->depth - 1].base] = 0;
|
||||
vm->depth--;
|
||||
DROP_CATCHES();
|
||||
if (vm->depth == 0) {
|
||||
*ret = 0;
|
||||
return 0;
|
||||
|
|
@ -434,6 +514,24 @@ dispatch:
|
|||
|
||||
CASE(TRAP) : { TRAPF(wo_ins_bx(ins), "explicit trap"); }
|
||||
|
||||
CASE(TRY) : {
|
||||
if (vm->ncatch >= WO_MAX_CATCH)
|
||||
TRAPF(WO_T_STACK, "catch stack overflow (%u regions)",
|
||||
(unsigned)WO_MAX_CATCH);
|
||||
vm->catches[vm->ncatch].depth = vm->depth;
|
||||
vm->catches[vm->ncatch].pc = (uint32_t)((int64_t)pc + wo_ins_sbx(ins));
|
||||
vm->catches[vm->ncatch].reg = wo_ins_a(ins);
|
||||
vm->ncatch++;
|
||||
NEXT();
|
||||
}
|
||||
CASE(ENDTRY) : {
|
||||
/* the try region completed without trapping. Defensive on an
|
||||
* unpaired ENDTRY (a miscompile the loader cannot see): pop
|
||||
* nothing rather than corrupt the stack. */
|
||||
if (vm->ncatch) vm->ncatch--;
|
||||
NEXT();
|
||||
}
|
||||
|
||||
#ifdef WO_ISO_C
|
||||
default:
|
||||
TRAPF(WO_T_EXPLICIT, "unknown opcode"); /* unreachable: loader */
|
||||
|
|
@ -444,6 +542,7 @@ dispatch:
|
|||
#undef NEXT
|
||||
#undef RELOAD
|
||||
#undef TRAPF
|
||||
#undef DROP_CATCHES
|
||||
}
|
||||
|
||||
int wo_vm_call(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
|
||||
|
|
@ -465,6 +564,7 @@ int wo_vm_call(wo_vm *vm, uint32_t method_idx, const uint64_t *args,
|
|||
return -1;
|
||||
}
|
||||
vm->depth = 1;
|
||||
vm->ncatch = 0; /* catch regions never survive a call boundary */
|
||||
vm->frames[0].method = method_idx;
|
||||
vm->frames[0].pc = 0;
|
||||
vm->frames[0].base = 0;
|
||||
|
|
|
|||
|
|
@ -21,12 +21,32 @@ typedef struct wo_frame {
|
|||
uint32_t base; /* register-window base in the value stack */
|
||||
} wo_frame;
|
||||
|
||||
/* One live `try` region (haxe-parity compiler Task 5, WOP_TRY). `depth`
|
||||
* is the frame depth that registered it, so a trap raised deeper unwinds
|
||||
* every frame above that one and lands here; `pc` is the handler's
|
||||
* instruction in that frame's method; `reg` is the window-relative
|
||||
* register the error record is built into. */
|
||||
typedef struct wo_catch {
|
||||
uint32_t depth;
|
||||
uint32_t pc;
|
||||
uint32_t reg;
|
||||
} wo_catch;
|
||||
|
||||
#define WO_MAX_CATCH 64u
|
||||
|
||||
typedef struct wo_vm {
|
||||
const wo_module *mod;
|
||||
wo_rt rt;
|
||||
uint64_t regs[WO_STACK_SLOTS];
|
||||
wo_frame frames[WO_MAX_FRAMES];
|
||||
uint32_t depth;
|
||||
/* the catch stack, innermost last; ncatch = 0 means every trap is
|
||||
* the uncaught kind and behaves exactly as it did before Task 5 */
|
||||
wo_catch catches[WO_MAX_CATCH];
|
||||
uint32_t ncatch;
|
||||
/* the error a caught trap landed with, read by WO_B_ERR_FILL while
|
||||
* the catch arm builds its record */
|
||||
wo_err caught;
|
||||
} wo_vm;
|
||||
|
||||
/* heap_cap = arena byte capacity (the CLI's WO_HEAP_MB feeds this) */
|
||||
|
|
|
|||
|
|
@ -120,8 +120,20 @@ enum {
|
|||
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 */
|
||||
/* haxe-parity compiler Task 5: try/catch over the trap system.
|
||||
* TRY pushes a catch frame {this frame, this window, handler pc =
|
||||
* pc + sBx, error register A}; a trap raised while it is the
|
||||
* innermost one unwinds every frame above this one exactly as an
|
||||
* uncaught trap does (drop maps run, registers null), releases what
|
||||
* the try region itself owned in this frame, and resumes at the
|
||||
* handler instead of leaving the VM. ENDTRY pops it — the try
|
||||
* region completed without trapping. Uncaught behavior is
|
||||
* unchanged: with no catch frame live, a trap is byte-for-byte
|
||||
* today's surface. */
|
||||
WOP_TRY = 32, /* A sBx: push catch frame, handler at pc + sBx */
|
||||
WOP_ENDTRY = 33, /* pop the innermost catch frame */
|
||||
};
|
||||
#define WOP_MAX 31u
|
||||
#define WOP_MAX 33u
|
||||
|
||||
/* ---- builtin ids (WOP_BUILTIN operand C) ---- */
|
||||
enum {
|
||||
|
|
@ -152,8 +164,20 @@ enum {
|
|||
* WO_T_BOUNDS on a null receiver or a native class id — the same
|
||||
* defense ICALL keeps for a miscompiled receiver. */
|
||||
WO_B_VARIANT_TAG = 14,
|
||||
/* haxe-parity compiler Task 5: materialize the caught error. The
|
||||
* catch arm's error record is an ordinary compiler-generated class
|
||||
* whose field order this builtin is the contract for — (record
|
||||
* object) -> the same object, with field 0 = code (i64), 1 = line
|
||||
* (i64), 2 = method (fresh owned Text), 3 = msg (fresh owned Text),
|
||||
* read from the error the VM landed here with. The compiler
|
||||
* allocates and owns the record (so its drop is the ordinary one);
|
||||
* the VM only fills it, which is why the pending error never has to
|
||||
* outlive the landing. Traps WO_T_BOUNDS on a receiver that is not
|
||||
* a 4-field class object, WO_T_OOM if either Text cannot be
|
||||
* allocated. */
|
||||
WO_B_ERR_FILL = 15,
|
||||
};
|
||||
#define WO_B_MAX 14u
|
||||
#define WO_B_MAX 15u
|
||||
|
||||
/* ---- instruction encode/decode: op:8 A:8 then B:8 C:8 or Bx:16 ---- */
|
||||
static inline uint32_t wo_ins_abc(uint8_t op, uint8_t a, uint8_t b, uint8_t c) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue