feat: json encode/decode + as, .wob v2 class metadata — log-watcher compiles
docs/examples/log-watcher (1285 lines, 7 files) now compiles clean: 0 diagnostics, a 35KB .wob written. corpus 71/0, woc runtest 565/0, every wovm unit gate green (both dispatch flavors). - .wob v2: each class row gains three u32 per-field arrays — the field's NAME constant, the CLASS it refers to (or the json-raw marker), and a container field's ELEMENT kinds. json is then a runtime service driven by metadata instead of per-type generated code. loader/emitter/disassembler/test assembler all read and write v2; field-name constants are interned with the rest of the pool (interning during serialization silently loses them) - runtime/src/json.c (new): encode by static kind + object headers + class table (nested records need no static knowledge); decode parses and BINDS straight into the target class — keys matched to field names, nested objects built as the field's class, arrays as a multi of the field's element kind, unknown keys skipped, absent keys nil. Malformed input is nil, never a trap - `as`: `json.decode(text) as T` is the one cast this language has (WO-E403 for any other `as`, and for a bare json.decode with no target type). Its result is `?T`, which is why the decode and the target are one instruction - json.Value: a reserved type name for a value the source does not inspect — the raw JSON slice, kind TEXT, re-emitted verbatim by encode - docs: 00-wob-format.md is now the v2 reference (class metadata, TRY/ENDTRY, the whole builtin surface, WO_T_IO); 08-builtin-surface.md documents the text/container builtins, the OS modules with their predeclared records, and json's two documented limits (Bool encodes 0/1, floats truncate) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f59340a1e3
commit
ebf82ca9c0
23 changed files with 1031 additions and 88 deletions
|
|
@ -253,6 +253,13 @@ and expr_kind =
|
||||||
literal without lookahead nothing else needs. *)
|
literal without lookahead nothing else needs. *)
|
||||||
| ListLit of expr list
|
| ListLit of expr list
|
||||||
| MapLit
|
| MapLit
|
||||||
|
(* `expr as Type` — a CHECKED conversion, not a reinterpretation: its only
|
||||||
|
meaning in this language is "decode this JSON text into that type",
|
||||||
|
yielding `?Type` (nil when the text does not fit). Anything else is a
|
||||||
|
WO-E403 at emission: there is no reinterpret-cast in the doctrine (the
|
||||||
|
systems-track spec's reject table lists `cast`), and this form exists
|
||||||
|
only because a decode's result type cannot be inferred. *)
|
||||||
|
| As of expr * field_ty
|
||||||
(* haxe-parity Task 5: `try body catch (ename) handler` — an expression,
|
(* haxe-parity Task 5: `try body catch (ename) handler` — an expression,
|
||||||
like `switch`. `body` is an expression (the workload's only form);
|
like `switch`. `body` is an expression (the workload's only form);
|
||||||
`handler` is a `stmt list` so both arm spellings share one shape,
|
`handler` is a `stmt list` so both arm spellings share one shape,
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ let dump (img : string) : string =
|
||||||
let line fmt = Buffer.add_string out (fmt ^ "\n") in
|
let line fmt = Buffer.add_string out (fmt ^ "\n") in
|
||||||
if u32 img 0 <> magic then raise (Bad "bad magic");
|
if u32 img 0 <> magic then raise (Bad "bad magic");
|
||||||
let ver = u32 img 4 in
|
let ver = u32 img 4 in
|
||||||
if ver <> 1 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
if ver <> 2 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
||||||
let coff = u32 img 8 and ccnt = u32 img 12 in
|
let coff = u32 img 8 and ccnt = u32 img 12 in
|
||||||
let koff = u32 img 16 and kcnt = u32 img 20 in
|
let koff = u32 img 16 and kcnt = u32 img 20 in
|
||||||
let ioff = u32 img 24 and icnt = u32 img 28 in
|
let ioff = u32 img 24 and icnt = u32 img 28 in
|
||||||
|
|
@ -204,10 +204,20 @@ let dump (img : string) : string =
|
||||||
o := !o + 12;
|
o := !o + 12;
|
||||||
let kinds = List.init fcnt (fun j -> kind_name (u8 img (!o + j))) in
|
let kinds = List.init fcnt (fun j -> kind_name (u8 img (!o + j))) in
|
||||||
o := !o + fcnt + ((4 - (fcnt mod 4)) mod 4);
|
o := !o + fcnt + ((4 - (fcnt mod 4)) mod 4);
|
||||||
|
(* v2 per-field metadata: names, referenced class ids, element kinds. The
|
||||||
|
dump shows each field as name:kind — the names are what json.encode
|
||||||
|
renders as keys, so a wrong one is worth seeing. *)
|
||||||
|
let names = List.init fcnt (fun j -> u32 img (!o + (j * 4))) in
|
||||||
|
o := !o + (fcnt * 12);
|
||||||
|
let fields =
|
||||||
|
List.map2
|
||||||
|
(fun nmk k -> if nmk = 0xFFFFFFFF then k else Printf.sprintf "%s:%s" (kname nmk) k)
|
||||||
|
names kinds
|
||||||
|
in
|
||||||
line
|
line
|
||||||
(Printf.sprintf "c%-3d %s flags=%s fields=[%s]" i (kname nm)
|
(Printf.sprintf "c%-3d %s flags=%s fields=[%s]" i (kname nm)
|
||||||
(if flags land 1 <> 0 then "gc" else "-")
|
(if flags land 1 <> 0 then "gc" else "-")
|
||||||
(String.concat ", " kinds))
|
(String.concat ", " fields))
|
||||||
done;
|
done;
|
||||||
(* interfaces + vtable rows *)
|
(* interfaces + vtable rows *)
|
||||||
line "== INTERFACES ==";
|
line "== INTERFACES ==";
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ let kind_label (k : Token.kind) : string =
|
||||||
| Token.KwTry -> "KW_TRY"
|
| Token.KwTry -> "KW_TRY"
|
||||||
| Token.KwCatch -> "KW_CATCH"
|
| Token.KwCatch -> "KW_CATCH"
|
||||||
| Token.KwNil -> "KW_NIL"
|
| Token.KwNil -> "KW_NIL"
|
||||||
|
| Token.KwAs -> "KW_AS"
|
||||||
| Token.LBrace -> "LBRACE"
|
| Token.LBrace -> "LBRACE"
|
||||||
| Token.RBrace -> "RBRACE"
|
| Token.RBrace -> "RBRACE"
|
||||||
| Token.LParen -> "LPAREN"
|
| Token.LParen -> "LPAREN"
|
||||||
|
|
@ -226,6 +227,7 @@ let rec expr_str (e : Ast.expr) : string =
|
||||||
| Ast.ListLit items -> Printf.sprintf "[%s]" (String.concat ", " (List.map expr_str items))
|
| Ast.ListLit items -> Printf.sprintf "[%s]" (String.concat ", " (List.map expr_str items))
|
||||||
| Ast.MapLit -> "{}"
|
| Ast.MapLit -> "{}"
|
||||||
| Ast.NilLit -> "nil"
|
| Ast.NilLit -> "nil"
|
||||||
|
| Ast.As (inner, ty) -> Printf.sprintf "%s as %s" (expr_str inner) (field_ty_str ty)
|
||||||
(* Like SWITCH above: a one-line summary, not a full unparse of the
|
(* Like SWITCH above: a one-line summary, not a full unparse of the
|
||||||
catch arm's statements. *)
|
catch arm's statements. *)
|
||||||
| Ast.Try { body; ename; handler } ->
|
| Ast.Try { body; ename; handler } ->
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ let stdlib_not_linked_code = Diag.emitter_prefix ^ "06"
|
||||||
============================================================ *)
|
============================================================ *)
|
||||||
|
|
||||||
let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
|
let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
|
||||||
let wob_version = 1
|
let wob_version = 2 (* v2: per-field class-table metadata *)
|
||||||
let wob_hdr_size = 44
|
let wob_hdr_size = 44
|
||||||
let wob_none = 0xFFFFFFFF
|
let wob_none = 0xFFFFFFFF
|
||||||
let k_int = 0
|
let k_int = 0
|
||||||
|
|
@ -256,6 +256,11 @@ let b_map_key_at = 37
|
||||||
let b_map_val_at = 38
|
let b_map_val_at = 38
|
||||||
let b_multi_set = 39
|
let b_multi_set = 39
|
||||||
|
|
||||||
|
(* json (runtime/src/json.c): encode takes the value's static kind as its
|
||||||
|
second argument, decode the class id to build as its second. *)
|
||||||
|
let b_json_encode = 57
|
||||||
|
let b_json_decode = 58
|
||||||
|
|
||||||
let ins_abc op a b c = op lor (a lsl 8) lor (b lsl 16) lor (c lsl 24)
|
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_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)
|
let ins_asbx op a sbx = ins_abx op a (sbx + 32768)
|
||||||
|
|
@ -947,6 +952,7 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option
|
||||||
| ListLit [] | MapLit -> None
|
| ListLit [] | MapLit -> None
|
||||||
(* haxe-parity Task 6: contextual on its destination (see owner.ml). *)
|
(* haxe-parity Task 6: contextual on its destination (see owner.ml). *)
|
||||||
| NilLit -> None
|
| NilLit -> None
|
||||||
|
| As (_, ty) -> Some (Nullable ty)
|
||||||
(* haxe-parity Task 5: a `try` yields its try arm's type — types.ml has
|
(* haxe-parity Task 5: a `try` yields its try arm's type — types.ml has
|
||||||
already required the catch arm to agree. *)
|
already required the catch arm to agree. *)
|
||||||
| Try t -> ty_of_expr p f t.body
|
| Try t -> ty_of_expr p f t.body
|
||||||
|
|
@ -1296,6 +1302,34 @@ let check_field_idx (p : pctx) (f : fstate) (pos : Ast.pos) (v : int) : int =
|
||||||
takes a bare identifier), so a fresh container must be created
|
takes a bare identifier), so a fresh container must be created
|
||||||
somewhere its type is declared — otherwise WO-E403 at the creation
|
somewhere its type is declared — otherwise WO-E403 at the creation
|
||||||
site, where the reader can act on it. *)
|
site, where the reader can act on it. *)
|
||||||
|
(* v2 class-table metadata for one field (runtime/src/wob.h):
|
||||||
|
- field_class: the class a field refers to — its own class for an
|
||||||
|
owned/@gc field, its ELEMENT's class for a container of records — or the
|
||||||
|
json-raw marker for a `json.Value` field, else "none".
|
||||||
|
- field_elem: a container field's element kinds (a multi's element kind; a
|
||||||
|
map's key kind and value kind packed as the container_imm immediate),
|
||||||
|
0 for everything else.
|
||||||
|
Both exist so json.encode/json.decode can work off metadata instead of
|
||||||
|
per-type generated code. *)
|
||||||
|
let wob_field_json_raw = 0xFFFFFFFE
|
||||||
|
|
||||||
|
let field_class_meta (p : pctx) (ty : Ast.field_ty) : int =
|
||||||
|
let name_of t = match t with Ast.Scalar n -> Some n | _ -> None in
|
||||||
|
match unwrap ty with
|
||||||
|
| Ast.Scalar n when n = Types.json_value_type -> wob_field_json_raw
|
||||||
|
| Ast.Scalar n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||||
|
| Ast.Multi e | Ast.Map (_, e) -> (
|
||||||
|
match name_of (Ast.Scalar e) with
|
||||||
|
| Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||||
|
| None -> wob_none)
|
||||||
|
| Ast.Ref _ | Ast.Nullable _ -> wob_none
|
||||||
|
|
||||||
|
let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int =
|
||||||
|
match unwrap ty with
|
||||||
|
| Ast.Multi e -> field_kind p (Ast.Scalar e)
|
||||||
|
| Ast.Map (k, v) -> field_kind p (Ast.Scalar k) lor (field_kind p (Ast.Scalar v) lsl 4)
|
||||||
|
| _ -> 0
|
||||||
|
|
||||||
let container_imm (p : pctx) (expected : Ast.field_ty option) (map : bool) : int option =
|
let container_imm (p : pctx) (expected : Ast.field_ty option) (map : bool) : int option =
|
||||||
match expected with
|
match expected with
|
||||||
| Some t -> (
|
| Some t -> (
|
||||||
|
|
@ -1371,6 +1405,34 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
||||||
sync_mask p f v e.id;
|
sync_mask p f v e.id;
|
||||||
put f (ins_abc op_builtin dst imm b_map_new))
|
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
|
| Try t -> emit_try p f v ~dst ?expected e t.body t.ename t.handler
|
||||||
|
(* `json.decode(text) as T` is the ONE `as` this language has: a checked
|
||||||
|
decode, lowered to json_decode(text, class id of T). The decode needs
|
||||||
|
the target class, and the target class is exactly what `as` names — so
|
||||||
|
the two are one instruction, never separable. Any other `as` (and a
|
||||||
|
bare `json.decode(...)` with no `as`) is WO-E403: there is no
|
||||||
|
reinterpret cast in the doctrine. *)
|
||||||
|
| As (inner, ty) -> (
|
||||||
|
let target = match unwrap ty with Scalar n -> class_of_name p n | _ -> None in
|
||||||
|
match (inner.kind, target) with
|
||||||
|
| Call ({ kind = Field ({ kind = Ident "json"; _ }, "decode"); _ }, [ src ]), Some cid ->
|
||||||
|
let base = alloc_temps p f e.pos 2 in
|
||||||
|
let save = f.f_temp in
|
||||||
|
emit_expr p f v ~dst:base src;
|
||||||
|
f.f_temp <- save;
|
||||||
|
put f (ins_abx op_loadk (base + 1) (check_bx p f e.pos "constant" (const_int p cid)));
|
||||||
|
sync_mask p f v e.id;
|
||||||
|
f.f_cur_line <- e.pos.line;
|
||||||
|
put f (ins_abc op_builtin dst base b_json_decode)
|
||||||
|
| Call ({ kind = Field ({ kind = Ident "json"; _ }, "decode"); _ }, _), None ->
|
||||||
|
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||||
|
~message:"`as` needs a declared class or record type to decode into";
|
||||||
|
put f (ins_abx op_loadk dst (const_int p 0))
|
||||||
|
| _ ->
|
||||||
|
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||||
|
~message:
|
||||||
|
"`as` is only a checked JSON decode (`json.decode(text) as T`) — this language has no \
|
||||||
|
reinterpret cast";
|
||||||
|
put f (ins_abx op_loadk dst (const_int p 0)))
|
||||||
| Ident n -> (
|
| Ident n -> (
|
||||||
match lookup_local f n with
|
match lookup_local f n with
|
||||||
| Some (r, _) -> if r <> dst then put f (ins_abc op_move dst r 0)
|
| Some (r, _) -> if r <> dst then put f (ins_abc op_move dst r 0)
|
||||||
|
|
@ -2300,11 +2362,38 @@ and emit_call (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : As
|
||||||
(* the systems stdlib: one builtin per member. A member whose
|
(* the systems stdlib: one builtin per member. A member whose
|
||||||
result is a record takes that record's class id as its last
|
result is a record takes that record's class id as its last
|
||||||
argument, so the VM allocates what it fills (sysio.c). *)
|
argument, so the VM allocates what it fills (sysio.c). *)
|
||||||
match Types.stdlib_member alias mname with
|
match (if alias = "json" then None else Types.stdlib_member alias mname) with
|
||||||
|
| None when alias = "json" && mname = "encode" -> (
|
||||||
|
(* json.encode(x): the VM needs x's STATIC kind, since a register
|
||||||
|
alone cannot say whether it holds an i64 or a pointer.
|
||||||
|
Everything below the top level comes from object headers and
|
||||||
|
the class table (runtime/src/json.c). *)
|
||||||
|
match args with
|
||||||
|
| [ a ] ->
|
||||||
|
let base = alloc_temps p f e.pos 2 in
|
||||||
|
let save = f.f_temp in
|
||||||
|
emit_expr p f v ~dst:base a;
|
||||||
|
f.f_temp <- save;
|
||||||
|
let kind =
|
||||||
|
match ty_of_expr p f a with Some t -> field_kind p t | None -> 3 (* Text *)
|
||||||
|
in
|
||||||
|
put f (ins_abx op_loadk (base + 1) (check_bx p f e.pos "constant" (const_int p kind)));
|
||||||
|
sync_mask p f v e.id;
|
||||||
|
f.f_cur_line <- e.pos.line;
|
||||||
|
put f (ins_abc op_builtin dst base b_json_encode)
|
||||||
|
| _ ->
|
||||||
|
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||||
|
~message:"`json.encode` takes exactly one argument";
|
||||||
|
put f (ins_abx op_loadk dst (const_int p 0)))
|
||||||
|
| None when alias = "json" && mname = "decode" ->
|
||||||
|
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||||
|
~message:
|
||||||
|
"`json.decode(text)` needs a target type — write `json.decode(text) as T`, whose \
|
||||||
|
result is `?T`";
|
||||||
|
put f (ins_abx op_loadk dst (const_int p 0))
|
||||||
| None ->
|
| None ->
|
||||||
err p ~code:stdlib_not_linked_code ~file:f.f_file ~pos:e.pos
|
err p ~code:stdlib_not_linked_code ~file:f.f_file ~pos:e.pos
|
||||||
~message:
|
~message:(Printf.sprintf "stdlib module `%s` has no member `%s`" alias mname);
|
||||||
(Printf.sprintf "stdlib module `%s` has no member `%s`" alias mname);
|
|
||||||
put f (ins_abx op_loadk dst (const_int p 0))
|
put f (ins_abx op_loadk dst (const_int p 0))
|
||||||
| Some sm ->
|
| Some sm ->
|
||||||
if List.length args <> sm.Types.sm_arity then begin
|
if List.length args <> sm.Types.sm_arity then begin
|
||||||
|
|
@ -3572,6 +3661,13 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
||||||
(* names are constants; interning them first keeps the pool's low
|
(* names are constants; interning them first keeps the pool's low
|
||||||
indexes stable and readable in a disassembly *)
|
indexes stable and readable in a disassembly *)
|
||||||
let class_name_k = Array.map (fun c -> const_text p c.cr_name) p_classes in
|
let class_name_k = Array.map (fun c -> const_text p c.cr_name) p_classes in
|
||||||
|
(* Field-name constants are interned HERE, with every other constant, and
|
||||||
|
never during serialization: the constant pool is written before the
|
||||||
|
class table, so a name interned later would be missing from the image
|
||||||
|
(found the hard way — the loader rejected every class). *)
|
||||||
|
let class_field_names =
|
||||||
|
Array.map (fun c -> Array.map (fun ((fname : string), _) -> const_text p fname) c.cr_fields) p_classes
|
||||||
|
in
|
||||||
let iface_name_k = Array.map (fun i -> const_text p i.ir_name) p_ifaces in
|
let iface_name_k = Array.map (fun i -> const_text p i.ir_name) p_ifaces in
|
||||||
let method_name_k = Array.map (fun m -> const_text p m.mr_name) p_methods in
|
let method_name_k = Array.map (fun m -> const_text p m.mr_name) p_methods in
|
||||||
(* ---- pass 2: method bodies ---- *)
|
(* ---- pass 2: method bodies ---- *)
|
||||||
|
|
@ -3644,7 +3740,14 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
||||||
let pad = (4 - (Array.length c.cr_fields mod 4)) mod 4 in
|
let pad = (4 - (Array.length c.cr_fields mod 4)) mod 4 in
|
||||||
for _ = 1 to pad do
|
for _ = 1 to pad do
|
||||||
Buf.u8 cls 0
|
Buf.u8 cls 0
|
||||||
done)
|
done;
|
||||||
|
(* v2 per-field metadata (wob.h's "class-table field metadata"): the
|
||||||
|
names json.encode renders as keys, the classes json.decode has to
|
||||||
|
build for a nested field, and the element kinds a container field
|
||||||
|
needs when decode creates one. *)
|
||||||
|
Array.iter (fun kidx -> Buf.u32 cls kidx) class_field_names.(cid);
|
||||||
|
Array.iter (fun (_, ty) -> Buf.u32 cls (field_class_meta p ty)) c.cr_fields;
|
||||||
|
Array.iter (fun (_, ty) -> Buf.u32 cls (field_elem_meta p ty)) c.cr_fields)
|
||||||
p_classes;
|
p_classes;
|
||||||
let ifs = Buf.create () in
|
let ifs = Buf.create () in
|
||||||
Array.iteri
|
Array.iteri
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ let keyword_kind = function
|
||||||
| "try" -> Some Token.KwTry
|
| "try" -> Some Token.KwTry
|
||||||
| "catch" -> Some Token.KwCatch
|
| "catch" -> Some Token.KwCatch
|
||||||
| "nil" -> Some Token.KwNil
|
| "nil" -> Some Token.KwNil
|
||||||
|
| "as" -> Some Token.KwAs
|
||||||
| "INSERT" -> Some Token.KwInsert
|
| "INSERT" -> Some Token.KwInsert
|
||||||
| "SELECT" -> Some Token.KwSelect
|
| "SELECT" -> Some Token.KwSelect
|
||||||
| _ -> None
|
| _ -> None
|
||||||
|
|
|
||||||
|
|
@ -514,6 +514,9 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option =
|
||||||
(* haxe-parity Task 6: `nil` is the zero word — contextual on its
|
(* haxe-parity Task 6: `nil` is the zero word — contextual on its
|
||||||
destination, and never something this frame owns. *)
|
destination, and never something this frame owns. *)
|
||||||
| NilLit -> None
|
| NilLit -> None
|
||||||
|
(* a checked decode's value is a fresh instance of the named type (or
|
||||||
|
nil) — owned, so the binding that holds it gets its drop *)
|
||||||
|
| As (_, ty) -> Some (Nullable ty)
|
||||||
(* haxe-parity Task 5: a `try` yields its try arm's type (types.ml has
|
(* haxe-parity Task 5: a `try` yields its try arm's type (types.ml has
|
||||||
already required the catch arm to agree). *)
|
already required the catch arm to agree). *)
|
||||||
| Try t -> expr_ty ctx t.body
|
| Try t -> expr_ty ctx t.body
|
||||||
|
|
@ -1069,6 +1072,7 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit =
|
||||||
literal-specific one. *)
|
literal-specific one. *)
|
||||||
| ListLit items -> List.iter (read_expr ctx) items
|
| ListLit items -> List.iter (read_expr ctx) items
|
||||||
| MapLit | NilLit -> ()
|
| MapLit | NilLit -> ()
|
||||||
|
| As (inner, _) -> read_expr ctx inner
|
||||||
| Try t -> analyze_try ctx e t.body t.ename t.handler
|
| Try t -> analyze_try ctx e t.body t.ename t.handler
|
||||||
| DbStub _ ->
|
| DbStub _ ->
|
||||||
(* trap-capable: the frame needs its drop map here *)
|
(* trap-capable: the frame needs its drop map here *)
|
||||||
|
|
|
||||||
|
|
@ -850,6 +850,17 @@ and parse_multiplicative (st : state) : Ast.expr =
|
||||||
done;
|
done;
|
||||||
!lhs
|
!lhs
|
||||||
|
|
||||||
|
(* `as` binds tighter than every binary operator and looser than a call, so
|
||||||
|
`json.decode(raw) as FileConfig` casts the call's result, and
|
||||||
|
`x as T == y` compares the cast value. *)
|
||||||
|
and parse_as (st : state) (e : Ast.expr) : Ast.expr =
|
||||||
|
if peek st <> Token.KwAs then e
|
||||||
|
else begin
|
||||||
|
ignore (advance st);
|
||||||
|
let ty = parse_field_ty st in
|
||||||
|
parse_as st { Ast.id = fresh_id st; pos = e.Ast.pos; kind = Ast.As (e, ty) }
|
||||||
|
end
|
||||||
|
|
||||||
and parse_unary (st : state) : Ast.expr =
|
and parse_unary (st : state) : Ast.expr =
|
||||||
match peek st with
|
match peek st with
|
||||||
| Token.Dash ->
|
| Token.Dash ->
|
||||||
|
|
@ -858,7 +869,7 @@ and parse_unary (st : state) : Ast.expr =
|
||||||
ignore (advance st);
|
ignore (advance st);
|
||||||
let operand = parse_unary st in
|
let operand = parse_unary st in
|
||||||
{ Ast.id; pos; kind = Ast.Unary (Ast.Neg, operand) }
|
{ Ast.id; pos; kind = Ast.Unary (Ast.Neg, operand) }
|
||||||
| _ -> parse_postfix st
|
| _ -> parse_as st (parse_postfix st)
|
||||||
|
|
||||||
and parse_postfix (st : state) : Ast.expr =
|
and parse_postfix (st : state) : Ast.expr =
|
||||||
let base = ref (parse_primary st) in
|
let base = ref (parse_primary st) in
|
||||||
|
|
@ -1767,6 +1778,7 @@ 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.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.ListLit items -> { e with Ast.kind = Ast.ListLit (List.map (subst_expr consts bound) items) }
|
||||||
| Ast.MapLit | Ast.NilLit -> e
|
| Ast.MapLit | Ast.NilLit -> e
|
||||||
|
| Ast.As (inner, ty) -> { e with Ast.kind = Ast.As (subst_expr consts bound inner, ty) }
|
||||||
| Ast.Try { body; ename; handler } ->
|
| Ast.Try { body; ename; handler } ->
|
||||||
{ e with
|
{ e with
|
||||||
Ast.kind =
|
Ast.kind =
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ type kind =
|
||||||
identifier — `nil` appears in the corpus and the driving workload
|
identifier — `nil` appears in the corpus and the driving workload
|
||||||
only ever as this literal. *)
|
only ever as this literal. *)
|
||||||
| KwNil
|
| KwNil
|
||||||
|
(* haxe-parity: `expr as Type` — the checked-decode cast. Only meaningful
|
||||||
|
over `json.decode(text)`, whose result has no type until one is named. *)
|
||||||
|
| KwAs
|
||||||
(* haxe-parity Task 4: `typedef Name = { ... }` structural records. A
|
(* haxe-parity Task 4: `typedef Name = { ... }` structural records. A
|
||||||
real keyword (grepped the corpus/sample first, same discipline as
|
real keyword (grepped the corpus/sample first, same discipline as
|
||||||
every keyword above — `typedef` appears only as this declaration's
|
every keyword above — `typedef` appears only as this declaration's
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,12 @@ let stdlib_modules = [ "fs"; "proc"; "net"; "time"; "json"; "env" ]
|
||||||
|
|
||||||
let is_stdlib_module (name : string) : bool = List.mem name stdlib_modules
|
let is_stdlib_module (name : string) : bool = List.mem name stdlib_modules
|
||||||
|
|
||||||
|
(* The one reserved stdlib TYPE name: `json.Value`, a decoded JSON value.
|
||||||
|
Represented as a Text holding the raw JSON slice (see wob_kind_of_typ),
|
||||||
|
so `json.encode(v)` re-emits it verbatim and nothing has to model a
|
||||||
|
dynamic value tree. *)
|
||||||
|
let json_value_type = "json.Value"
|
||||||
|
|
||||||
(* haxe-parity Task 5: the record `catch (e)` binds — the VM's structured
|
(* haxe-parity Task 5: the record `catch (e)` binds — the VM's structured
|
||||||
trap error, one shape forever (spec §6). Predeclared rather than
|
trap error, one shape forever (spec §6). Predeclared rather than
|
||||||
written: no source declares it, every program that catches gets it, and
|
written: no source declares it, every program that catches gets it, and
|
||||||
|
|
@ -252,7 +258,14 @@ let stdlib_members : stdlib_member list =
|
||||||
m "net" "write" 2 54 None None;
|
m "net" "write" 2 54 None None;
|
||||||
m "net" "close" 1 55 None None;
|
m "net" "close" 1 55 None None;
|
||||||
(* proc *)
|
(* proc *)
|
||||||
m "proc" "run" 2 56 (Some (TNullable (TScalar proc_record_name))) (Some proc_record_name) ]
|
m "proc" "run" 2 56 (Some (TNullable (TScalar proc_record_name))) (Some proc_record_name);
|
||||||
|
(* json — both members are lowered specially (emit.ml): encode needs its
|
||||||
|
argument's static kind, and decode has no type until an `as` names one,
|
||||||
|
so neither goes through the generic builtin path. They are listed here
|
||||||
|
for their SIGNATURES: `json.encode(x) -> Text`, and `json.decode(t)`
|
||||||
|
yielding nothing on its own. *)
|
||||||
|
m "json" "encode" 1 57 (Some (TScalar "Text")) None;
|
||||||
|
m "json" "decode" 1 58 None None ]
|
||||||
|
|
||||||
let stdlib_member (m : string) (name : string) : stdlib_member option =
|
let stdlib_member (m : string) (name : string) : stdlib_member option =
|
||||||
List.find_opt (fun s -> s.sm_module = m && s.sm_name = name) stdlib_members
|
List.find_opt (fun s -> s.sm_module = m && s.sm_name = name) stdlib_members
|
||||||
|
|
@ -339,7 +352,11 @@ let wob_kind_of_typ (syms : symbols) (t : typ) : wob_kind =
|
||||||
WO_K_TEXT. Emitting WO_K_SCALAR here leaked every string a
|
WO_K_TEXT. Emitting WO_K_SCALAR here leaked every string a
|
||||||
class owned. Found by the emitter, this function's first
|
class owned. Found by the emitter, this function's first
|
||||||
caller. *)
|
caller. *)
|
||||||
if name = "Text" then WO_K_TEXT
|
(* `json.Value` is a Text at the representation level: a decoded
|
||||||
|
value the source never inspects, carrying the raw JSON slice it
|
||||||
|
came from, which json.encode emits back verbatim. Kinding it TEXT
|
||||||
|
is what makes it drop correctly and pass through concatenation. *)
|
||||||
|
if name = "Text" || name = json_value_type then WO_K_TEXT
|
||||||
else if is_builtin_scalar name then WO_K_SCALAR
|
else if is_builtin_scalar name then WO_K_SCALAR
|
||||||
else if StringMap.mem name syms.unions then
|
else if StringMap.mem name syms.unions then
|
||||||
(* haxe-parity Task 4: an all-bare union value is a plain
|
(* haxe-parity Task 4: an all-bare union value is a plain
|
||||||
|
|
@ -993,6 +1010,9 @@ let typecheck_program ~file ~(module_of : string -> string)
|
||||||
as an empty container literal — nothing about the literal itself
|
as an empty container literal — nothing about the literal itself
|
||||||
says which `?T` it is the absent value of. *)
|
says which `?T` it is the absent value of. *)
|
||||||
| NilLit -> None
|
| NilLit -> None
|
||||||
|
(* A checked decode yields `?T` — the named type, or nil when the text
|
||||||
|
did not fit it. *)
|
||||||
|
| As (_, ty) -> Some (TNullable (resolve_field_ty ty))
|
||||||
(* haxe-parity Task 5: a `try` expression's type is its try arm's — the
|
(* 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
|
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. *)
|
would answer, and the try arm is the one that always has a value. *)
|
||||||
|
|
@ -1342,6 +1362,9 @@ let typecheck_program ~file ~(module_of : string -> string)
|
||||||
what lets a comparison or a binding treat it as the absent value of
|
what lets a comparison or a binding treat it as the absent value of
|
||||||
whatever `?T` it meets. *)
|
whatever `?T` it meets. *)
|
||||||
| NilLit -> { typ = TScalar "Int"; is_nil = true }
|
| NilLit -> { typ = TScalar "Int"; is_nil = true }
|
||||||
|
| As (inner, ty) ->
|
||||||
|
let _ = typecheck_expr env cenv inner in
|
||||||
|
{ typ = TNullable (resolve_field_ty ty); is_nil = false }
|
||||||
| Try { body; ename; handler } ->
|
| Try { body; ename; handler } ->
|
||||||
let body_res = typecheck_expr env cenv body in
|
let body_res = typecheck_expr env cenv body in
|
||||||
(* The catch arm sees exactly one new name: the error record. *)
|
(* The catch arm sees exactly one new name: the error record. *)
|
||||||
|
|
@ -2024,6 +2047,7 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e
|
||||||
| Interp inner -> walk_expr bound visit inner
|
| Interp inner -> walk_expr bound visit inner
|
||||||
| ListLit items -> List.iter (walk_expr bound visit) items
|
| ListLit items -> List.iter (walk_expr bound visit) items
|
||||||
| MapLit | NilLit -> ()
|
| MapLit | NilLit -> ()
|
||||||
|
| As (inner, _) -> walk_expr bound visit inner
|
||||||
| Try { body; ename; handler } ->
|
| Try { body; ename; handler } ->
|
||||||
walk_expr bound visit body;
|
walk_expr bound visit body;
|
||||||
walk_block (StringSet.add ename bound) visit handler
|
walk_block (StringSet.add ename bound) visit handler
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
== CONSTANTS ==
|
== CONSTANTS ==
|
||||||
k0 TEXT "Cache"
|
k0 TEXT "Cache"
|
||||||
k1 TEXT "Holder"
|
k1 TEXT "Holder"
|
||||||
k2 TEXT "read"
|
k2 TEXT "hits"
|
||||||
k3 TEXT "proven"
|
k3 TEXT "cache"
|
||||||
k4 TEXT "main"
|
k4 TEXT "read"
|
||||||
k5 INT 41
|
k5 TEXT "proven"
|
||||||
k6 INT 1
|
k6 TEXT "main"
|
||||||
|
k7 INT 41
|
||||||
|
k8 INT 1
|
||||||
== CLASSES ==
|
== CLASSES ==
|
||||||
c0 Cache flags=gc fields=[SCALAR]
|
c0 Cache flags=gc fields=[hits:SCALAR]
|
||||||
c1 Holder flags=- fields=[GCREF]
|
c1 Holder flags=- fields=[cache:GCREF]
|
||||||
== INTERFACES ==
|
== INTERFACES ==
|
||||||
== VTABLES ==
|
== VTABLES ==
|
||||||
== METHODS ==
|
== METHODS ==
|
||||||
|
|
@ -31,7 +33,7 @@ m2 main args=0 regs=6 [free fn] [ENTRY]
|
||||||
drops: pc 14 owned={} gc={r0}
|
drops: pc 14 owned={} gc={r0}
|
||||||
drops: pc 15 owned={} gc={}
|
drops: pc 15 owned={} gc={}
|
||||||
0000 NEW r0, c0
|
0000 NEW r0, c0
|
||||||
0001 LOADK r1, k5
|
0001 LOADK r1, k7
|
||||||
0002 SETF r0, f0, r1
|
0002 SETF r0, f0, r1
|
||||||
0003 NEW r1, c1
|
0003 NEW r1, c1
|
||||||
0004 MOVE r2, r0
|
0004 MOVE r2, r0
|
||||||
|
|
@ -40,7 +42,7 @@ m2 main args=0 regs=6 [free fn] [ENTRY]
|
||||||
0007 MOVE r4, r1
|
0007 MOVE r4, r1
|
||||||
0008 CALL r4, m1
|
0008 CALL r4, m1
|
||||||
0009 MOVE r3, r4
|
0009 MOVE r3, r4
|
||||||
0010 LOADK r5, k6
|
0010 LOADK r5, k8
|
||||||
0011 ADD r2, r3, r5
|
0011 ADD r2, r3, r5
|
||||||
0012 BUILTIN r2, r2, print_int
|
0012 BUILTIN r2, r2, print_int
|
||||||
0013 DROP r1
|
0013 DROP r1
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
== CONSTANTS ==
|
== CONSTANTS ==
|
||||||
k0 TEXT "Book"
|
k0 TEXT "Book"
|
||||||
k1 TEXT "Toy"
|
k1 TEXT "Toy"
|
||||||
k2 TEXT "Priced"
|
k2 TEXT "base"
|
||||||
k3 TEXT "current_price"
|
k3 TEXT "Priced"
|
||||||
k4 TEXT "quote"
|
k4 TEXT "current_price"
|
||||||
k5 TEXT "main"
|
k5 TEXT "quote"
|
||||||
k6 INT 2
|
k6 TEXT "main"
|
||||||
k7 INT 3
|
k7 INT 2
|
||||||
k8 INT 10
|
k8 INT 3
|
||||||
k9 INT 5
|
k9 INT 10
|
||||||
|
k10 INT 5
|
||||||
== CLASSES ==
|
== CLASSES ==
|
||||||
c0 Book flags=- fields=[SCALAR]
|
c0 Book flags=- fields=[base:SCALAR]
|
||||||
c1 Toy flags=- fields=[SCALAR]
|
c1 Toy flags=- fields=[base:SCALAR]
|
||||||
== INTERFACES ==
|
== INTERFACES ==
|
||||||
i0 Priced methods=1 slots=s0..s0
|
i0 Priced methods=1 slots=s0..s0
|
||||||
== VTABLES ==
|
== VTABLES ==
|
||||||
|
|
@ -22,14 +23,14 @@ m0 current_price args=1 regs=3 [class c0]
|
||||||
lines: 0->15
|
lines: 0->15
|
||||||
drops: (none)
|
drops: (none)
|
||||||
0000 GETF r1, r0, f0
|
0000 GETF r1, r0, f0
|
||||||
0001 LOADK r2, k6
|
0001 LOADK r2, k7
|
||||||
0002 ADD r1, r1, r2
|
0002 ADD r1, r1, r2
|
||||||
0003 RET r1
|
0003 RET r1
|
||||||
m1 current_price args=1 regs=3 [class c1]
|
m1 current_price args=1 regs=3 [class c1]
|
||||||
lines: 0->23
|
lines: 0->23
|
||||||
drops: (none)
|
drops: (none)
|
||||||
0000 GETF r1, r0, f0
|
0000 GETF r1, r0, f0
|
||||||
0001 LOADK r2, k7
|
0001 LOADK r2, k8
|
||||||
0002 MUL r1, r1, r2
|
0002 MUL r1, r1, r2
|
||||||
0003 RET r1
|
0003 RET r1
|
||||||
m2 quote args=1 regs=2 [free fn]
|
m2 quote args=1 regs=2 [free fn]
|
||||||
|
|
@ -45,10 +46,10 @@ m3 main args=0 regs=4 [free fn] [ENTRY]
|
||||||
drops: pc 19 owned={r0} gc={}
|
drops: pc 19 owned={r0} gc={}
|
||||||
drops: pc 20 owned={} gc={}
|
drops: pc 20 owned={} gc={}
|
||||||
0000 NEW r0, c0
|
0000 NEW r0, c0
|
||||||
0001 LOADK r1, k8
|
0001 LOADK r1, k9
|
||||||
0002 SETF r0, f0, r1
|
0002 SETF r0, f0, r1
|
||||||
0003 NEW r1, c1
|
0003 NEW r1, c1
|
||||||
0004 LOADK r2, k9
|
0004 LOADK r2, k10
|
||||||
0005 SETF r1, f0, r2
|
0005 SETF r1, f0, r2
|
||||||
0006 MOVE r3, r0
|
0006 MOVE r3, r0
|
||||||
0007 CALL r3, m2
|
0007 CALL r3, m2
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
== CONSTANTS ==
|
== CONSTANTS ==
|
||||||
k0 TEXT "Item"
|
k0 TEXT "Item"
|
||||||
k1 TEXT "consume"
|
k1 TEXT "n"
|
||||||
k2 TEXT "twice"
|
k2 TEXT "consume"
|
||||||
k3 TEXT "main"
|
k3 TEXT "twice"
|
||||||
k4 INT 2
|
k4 TEXT "main"
|
||||||
k5 INT 3
|
k5 INT 2
|
||||||
k6 INT 5
|
k6 INT 3
|
||||||
k7 INT 0
|
k7 INT 5
|
||||||
|
k8 INT 0
|
||||||
== CLASSES ==
|
== CLASSES ==
|
||||||
c0 Item flags=- fields=[SCALAR]
|
c0 Item flags=- fields=[n:SCALAR]
|
||||||
== INTERFACES ==
|
== INTERFACES ==
|
||||||
== VTABLES ==
|
== VTABLES ==
|
||||||
== METHODS ==
|
== METHODS ==
|
||||||
|
|
@ -31,12 +32,12 @@ m1 twice args=2 regs=6 [free fn]
|
||||||
drops: pc 14 owned={r2} gc={}
|
drops: pc 14 owned={r2} gc={}
|
||||||
drops: pc 19 owned={} gc={}
|
drops: pc 19 owned={} gc={}
|
||||||
0000 NEW r2, c0
|
0000 NEW r2, c0
|
||||||
0001 LOADK r3, k4
|
0001 LOADK r3, k5
|
||||||
0002 SETF r2, f0, r3
|
0002 SETF r2, f0, r3
|
||||||
0003 MOVE r3, r1
|
0003 MOVE r3, r1
|
||||||
0004 JZ r3, -> 0013
|
0004 JZ r3, -> 0013
|
||||||
0005 NEW r3, c0
|
0005 NEW r3, c0
|
||||||
0006 LOADK r4, k5
|
0006 LOADK r4, k6
|
||||||
0007 SETF r3, f0, r4
|
0007 SETF r3, f0, r4
|
||||||
0008 GETF r4, r3, f0
|
0008 GETF r4, r3, f0
|
||||||
0009 DROP r3
|
0009 DROP r3
|
||||||
|
|
@ -54,9 +55,9 @@ m2 main args=0 regs=4 [free fn] [ENTRY]
|
||||||
lines: 0->24 7->23
|
lines: 0->24 7->23
|
||||||
drops: (none)
|
drops: (none)
|
||||||
0000 NEW r1, c0
|
0000 NEW r1, c0
|
||||||
0001 LOADK r3, k6
|
0001 LOADK r3, k7
|
||||||
0002 SETF r1, f0, r3
|
0002 SETF r1, f0, r3
|
||||||
0003 LOADK r2, k7
|
0003 LOADK r2, k8
|
||||||
0004 CALL r1, m1
|
0004 CALL r1, m1
|
||||||
0005 MOVE r0, r1
|
0005 MOVE r0, r1
|
||||||
0006 BUILTIN r0, r0, print_int
|
0006 BUILTIN r0, r0, print_int
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
== CONSTANTS ==
|
== CONSTANTS ==
|
||||||
k0 TEXT "Item"
|
k0 TEXT "Item"
|
||||||
k1 TEXT "Bag"
|
k1 TEXT "Bag"
|
||||||
k2 TEXT "touch"
|
k2 TEXT "n"
|
||||||
k3 TEXT "pair"
|
k3 TEXT "items"
|
||||||
k4 TEXT "fixed"
|
k4 TEXT "touch"
|
||||||
k5 TEXT "touch3"
|
k5 TEXT "pair"
|
||||||
k6 TEXT "triple"
|
k6 TEXT "fixed"
|
||||||
k7 TEXT "write_through"
|
k7 TEXT "touch3"
|
||||||
k8 TEXT "main"
|
k8 TEXT "triple"
|
||||||
k9 INT 0
|
k9 TEXT "write_through"
|
||||||
k10 INT 1
|
k10 TEXT "main"
|
||||||
k11 INT 5
|
k11 INT 0
|
||||||
k12 INT 2
|
k12 INT 1
|
||||||
k13 INT 3
|
k13 INT 5
|
||||||
|
k14 INT 2
|
||||||
|
k15 INT 3
|
||||||
== CLASSES ==
|
== CLASSES ==
|
||||||
c0 Item flags=- fields=[SCALAR]
|
c0 Item flags=- fields=[n:SCALAR]
|
||||||
c1 Bag flags=- fields=[MULTI]
|
c1 Bag flags=- fields=[items:MULTI]
|
||||||
== INTERFACES ==
|
== INTERFACES ==
|
||||||
== VTABLES ==
|
== VTABLES ==
|
||||||
== METHODS ==
|
== METHODS ==
|
||||||
|
|
@ -48,10 +50,10 @@ m2 fixed args=1 regs=5 [free fn]
|
||||||
lines: 0->27
|
lines: 0->27
|
||||||
drops: (none)
|
drops: (none)
|
||||||
0000 GETF r3, r0, f0
|
0000 GETF r3, r0, f0
|
||||||
0001 LOADK r4, k9
|
0001 LOADK r4, k11
|
||||||
0002 BUILTIN r1, r3, multi_get
|
0002 BUILTIN r1, r3, multi_get
|
||||||
0003 GETF r3, r0, f0
|
0003 GETF r3, r0, f0
|
||||||
0004 LOADK r4, k10
|
0004 LOADK r4, k12
|
||||||
0005 BUILTIN r2, r3, multi_get
|
0005 BUILTIN r2, r3, multi_get
|
||||||
0006 CALL r1, m0
|
0006 CALL r1, m0
|
||||||
0007 RET r1
|
0007 RET r1
|
||||||
|
|
@ -97,7 +99,7 @@ m5 write_through args=3 regs=7 [free fn]
|
||||||
0003 GETF r5, r0, f0
|
0003 GETF r5, r0, f0
|
||||||
0004 MOVE r6, r2
|
0004 MOVE r6, r2
|
||||||
0005 BUILTIN r4, r5, multi_get
|
0005 BUILTIN r4, r5, multi_get
|
||||||
0006 LOADK r5, k11
|
0006 LOADK r5, k13
|
||||||
0007 BORROW_X r4
|
0007 BORROW_X r4
|
||||||
0008 BORROW_S r3
|
0008 BORROW_S r3
|
||||||
0009 SETF r4, f0, r5
|
0009 SETF r4, f0, r5
|
||||||
|
|
@ -114,22 +116,22 @@ m6 main args=0 regs=6 [free fn] [ENTRY]
|
||||||
0002 SETF r0, f0, r1
|
0002 SETF r0, f0, r1
|
||||||
0003 GETF r1, r0, f0
|
0003 GETF r1, r0, f0
|
||||||
0004 NEW r2, c0
|
0004 NEW r2, c0
|
||||||
0005 LOADK r3, k10
|
0005 LOADK r3, k12
|
||||||
0006 SETF r2, f0, r3
|
0006 SETF r2, f0, r3
|
||||||
0007 BUILTIN r1, r1, multi_push
|
0007 BUILTIN r1, r1, multi_push
|
||||||
0008 GETF r1, r0, f0
|
0008 GETF r1, r0, f0
|
||||||
0009 NEW r2, c0
|
0009 NEW r2, c0
|
||||||
0010 LOADK r3, k12
|
0010 LOADK r3, k14
|
||||||
0011 SETF r2, f0, r3
|
0011 SETF r2, f0, r3
|
||||||
0012 BUILTIN r1, r1, multi_push
|
0012 BUILTIN r1, r1, multi_push
|
||||||
0013 GETF r1, r0, f0
|
0013 GETF r1, r0, f0
|
||||||
0014 NEW r2, c0
|
0014 NEW r2, c0
|
||||||
0015 LOADK r3, k13
|
0015 LOADK r3, k15
|
||||||
0016 SETF r2, f0, r3
|
0016 SETF r2, f0, r3
|
||||||
0017 BUILTIN r1, r1, multi_push
|
0017 BUILTIN r1, r1, multi_push
|
||||||
0018 MOVE r2, r0
|
0018 MOVE r2, r0
|
||||||
0019 LOADK r3, k9
|
0019 LOADK r3, k11
|
||||||
0020 LOADK r4, k10
|
0020 LOADK r4, k12
|
||||||
0021 CALL r2, m1
|
0021 CALL r2, m1
|
||||||
0022 MOVE r1, r2
|
0022 MOVE r1, r2
|
||||||
0023 BUILTIN r1, r1, print_int
|
0023 BUILTIN r1, r1, print_int
|
||||||
|
|
@ -138,15 +140,15 @@ m6 main args=0 regs=6 [free fn] [ENTRY]
|
||||||
0026 MOVE r1, r2
|
0026 MOVE r1, r2
|
||||||
0027 BUILTIN r1, r1, print_int
|
0027 BUILTIN r1, r1, print_int
|
||||||
0028 MOVE r2, r0
|
0028 MOVE r2, r0
|
||||||
0029 LOADK r3, k9
|
0029 LOADK r3, k11
|
||||||
0030 LOADK r4, k10
|
0030 LOADK r4, k12
|
||||||
0031 LOADK r5, k12
|
0031 LOADK r5, k14
|
||||||
0032 CALL r2, m4
|
0032 CALL r2, m4
|
||||||
0033 MOVE r1, r2
|
0033 MOVE r1, r2
|
||||||
0034 BUILTIN r1, r1, print_int
|
0034 BUILTIN r1, r1, print_int
|
||||||
0035 MOVE r2, r0
|
0035 MOVE r2, r0
|
||||||
0036 LOADK r3, k9
|
0036 LOADK r3, k11
|
||||||
0037 LOADK r4, k10
|
0037 LOADK r4, k12
|
||||||
0038 CALL r2, m5
|
0038 CALL r2, m5
|
||||||
0039 MOVE r1, r2
|
0039 MOVE r1, r2
|
||||||
0040 BUILTIN r1, r1, print_int
|
0040 BUILTIN r1, r1, print_int
|
||||||
|
|
|
||||||
|
|
@ -2382,7 +2382,7 @@ let validate_image (img : string) : string list =
|
||||||
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
|
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
|
||||||
let none = 0xFFFFFFFF in
|
let none = 0xFFFFFFFF in
|
||||||
if u32 0 <> 0x31424F57 then fail "bad magic";
|
if u32 0 <> 0x31424F57 then fail "bad magic";
|
||||||
if u32 4 <> 1 then fail "unsupported version";
|
if u32 4 <> 2 then fail "unsupported version";
|
||||||
let coff = u32 8 and ccnt = u32 12 in
|
let coff = u32 8 and ccnt = u32 12 in
|
||||||
let koff = u32 16 and kcnt = u32 20 in
|
let koff = u32 16 and kcnt = u32 20 in
|
||||||
let ioff = u32 24 and icnt = u32 28 in
|
let ioff = u32 24 and icnt = u32 28 in
|
||||||
|
|
@ -2423,6 +2423,18 @@ let validate_image (img : string) : string list =
|
||||||
if u8 (!o + j) > 5 then fail (Printf.sprintf "class %d field %d: bad kind" i j)
|
if u8 (!o + j) > 5 then fail (Printf.sprintf "class %d field %d: bad kind" i j)
|
||||||
done;
|
done;
|
||||||
o := !o + fcnt + ((4 - (fcnt mod 4)) mod 4);
|
o := !o + fcnt + ((4 - (fcnt mod 4)) mod 4);
|
||||||
|
(* v2: three u32 arrays of per-field metadata — names (a Text constant or
|
||||||
|
"not recorded"), the referenced class id (or the json-raw marker), and
|
||||||
|
container element kinds. Mirrors runtime/src/loader.c's own checks. *)
|
||||||
|
for j = 0 to fcnt - 1 do
|
||||||
|
let nmk = u32 (!o + (j * 4)) in
|
||||||
|
if nmk <> 0xFFFFFFFF && not (text_const nmk) then
|
||||||
|
fail (Printf.sprintf "class %d field %d: bad name constant" i j);
|
||||||
|
let fc = u32 (!o + ((fcnt + j) * 4)) in
|
||||||
|
if fc <> 0xFFFFFFFF && fc <> 0xFFFFFFFE && fc >= kcnt then
|
||||||
|
fail (Printf.sprintf "class %d field %d: field class out of range" i j)
|
||||||
|
done;
|
||||||
|
o := !o + (fcnt * 12);
|
||||||
if !o > len then fail (Printf.sprintf "class %d: truncated" i)
|
if !o > len then fail (Printf.sprintf "class %d: truncated" i)
|
||||||
done;
|
done;
|
||||||
(* interfaces + vtable rows *)
|
(* interfaces + vtable rows *)
|
||||||
|
|
@ -3294,7 +3306,7 @@ let () =
|
||||||
(count_substring ~needle:"Status.Pending flags" dump = 1
|
(count_substring ~needle:"Status.Pending flags" dump = 1
|
||||||
&& count_substring ~needle:"Status.Failed flags" dump = 1);
|
&& count_substring ~needle:"Status.Failed flags" dump = 1);
|
||||||
check "t4 table: Status.Failed's payload Text is a TEXT slot"
|
check "t4 table: Status.Failed's payload Text is a TEXT slot"
|
||||||
(count_substring ~needle:"Status.Failed flags=- fields=[TEXT]" dump = 1);
|
(count_substring ~needle:"Status.Failed flags=- fields=[reason:TEXT]" dump = 1);
|
||||||
check "t4 table: bare union gets no class entries"
|
check "t4 table: bare union gets no class entries"
|
||||||
(count_substring ~needle:"Kind" dump
|
(count_substring ~needle:"Kind" dump
|
||||||
- count_substring ~needle:"Kind" (String.concat "" [ "" ])
|
- count_substring ~needle:"Kind" (String.concat "" [ "" ])
|
||||||
|
|
@ -3302,7 +3314,7 @@ let () =
|
||||||
&& count_substring ~needle:"Kind flags" dump = 0
|
&& count_substring ~needle:"Kind flags" dump = 0
|
||||||
&& count_substring ~needle:"Kind.Lo" dump = 0);
|
&& count_substring ~needle:"Kind.Lo" dump = 0);
|
||||||
check "t4 table: a bare-union record field is a SCALAR slot, a payload one OWNED"
|
check "t4 table: a bare-union record field is a SCALAR slot, a payload one OWNED"
|
||||||
(count_substring ~needle:"Holder flags=- fields=[SCALAR, OWNED]" dump = 1)
|
(count_substring ~needle:"Holder flags=- fields=[k:SCALAR, st:OWNED]" dump = 1)
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
(* lowering shapes: variant_tag for a payload union's switch only; a
|
(* lowering shapes: variant_tag for a payload union's switch only; a
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# The `.wob` format v1 — normative reference
|
# The `.wob` format v2 — normative reference
|
||||||
|
|
||||||
> Copied verbatim from the normative section of
|
> Copied verbatim from the normative section of
|
||||||
> [`docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md`](../../superpowers/plans/2026-08-01-wob-format-and-vm-core.md)
|
> [`docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md`](../../superpowers/plans/2026-08-01-wob-format-and-vm-core.md)
|
||||||
|
|
@ -11,11 +11,19 @@
|
||||||
|
|
||||||
All integers little-endian; offsets are absolute file offsets.
|
All integers little-endian; offsets are absolute file offsets.
|
||||||
|
|
||||||
**Header (44 bytes):** magic `"WOB1"`, version 1, then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none).
|
**Header (44 bytes):** magic `"WOB1"`, version 2, then offset/count u32 pairs for the constant pool, class table, interface section, and method table, then a u32 entry-method index (all-ones = none).
|
||||||
|
|
||||||
**Constant pool** — sequential entries: one tag byte; tag 0 = i64 follows; tag 1 = text (u32 length + bytes, no NUL).
|
**Constant pool** — sequential entries: one tag byte; tag 0 = i64 follows; tag 1 = text (u32 length + bytes, no NUL).
|
||||||
|
|
||||||
**Class table** — per class: name constant index, flags u32 (bit0 = instances are `@gc`), field count, then one kind byte per field padded to a 4-byte boundary. Field kinds: 0 SCALAR, 1 OWNED, 2 GCREF, 3 TEXT, 4 MULTI, 5 MAP. Runtime object layout: 16-byte header then one 8-byte slot per field, in declaration order.
|
**Class table** — per class: name constant index, flags u32 (bit0 = instances are `@gc`), field count, then one kind byte per field padded to a 4-byte boundary, then **three u32 arrays of per-field metadata** (v2), one entry per field each, in declaration order:
|
||||||
|
|
||||||
|
1. `field_names[i]` — constant index of the field's name, or all-ones for "not recorded" (what a hand-built test image writes).
|
||||||
|
2. `field_class[i]` — the class id the field refers to: its own class for an OWNED/GCREF field, its *element's* class for a container of records; `0xFFFFFFFE` marks a `json.Value` field, whose Text holds a raw JSON slice; all-ones for none.
|
||||||
|
3. `field_elem[i]` — a container field's element kinds: a MULTI's element kind, or a MAP's key kind in the low nibble and value kind in the next; 0 otherwise.
|
||||||
|
|
||||||
|
Field kinds: 0 SCALAR, 1 OWNED, 2 GCREF, 3 TEXT, 4 MULTI, 5 MAP. Runtime object layout: 16-byte header then one 8-byte slot per field, in declaration order.
|
||||||
|
|
||||||
|
The metadata exists for exactly one reason: `json.encode`/`json.decode` are runtime services driven by class metadata (`runtime/src/json.c`) rather than per-type generated code, so the names a JSON object needs and the shapes a decode has to build must live in the image. Every other part of the runtime ignores it.
|
||||||
|
|
||||||
**Interface section** — per interface: name constant index, method count. Global *slot ids* are assigned sequentially across interfaces in declaration order. Then a vtable row count and rows: class id, interface id, one method index per interface method.
|
**Interface section** — per interface: name constant index, method count. Global *slot ids* are assigned sequentially across interfaces in declaration order. Then a vtable row count and rows: class id, interface id, one method index per interface method.
|
||||||
|
|
||||||
|
|
@ -43,10 +51,18 @@ All integers little-endian; offsets are absolute file offsets.
|
||||||
| 29 | BUILTIN A B C | register A = builtin C applied to args starting at register B (fixed arity per builtin; `multi_new`/`map_new` carry kind immediates in B instead) |
|
| 29 | BUILTIN A B C | register A = builtin C applied to args starting at register B (fixed arity per builtin; `multi_new`/`map_new` carry kind immediates in B instead) |
|
||||||
| 30 | DB_STUB | trap T_DB "engine not linked" (spec: SQL-layer statements in milestone 1) |
|
| 30 | DB_STUB | trap T_DB "engine not linked" (spec: SQL-layer statements in milestone 1) |
|
||||||
| 31 | TRAP Bx | explicit trap with code Bx |
|
| 31 | TRAP Bx | explicit trap with code Bx |
|
||||||
|
| 32 | TRY A sBx | push a catch frame for this frame and window: handler at pc + sBx, error record register A (haxe-parity Task 5) |
|
||||||
|
| 33 | ENDTRY | pop the innermost catch frame — the try region completed without trapping |
|
||||||
|
|
||||||
**Builtins:** now (ms), print (text), print_int, words (whitespace token count), multi_new/multi_push/multi_get/count/latest, map_new/map_set/map_get/map_has, int_to_text (haxe-parity Task 2), variant_tag (haxe-parity Task 4 — see "Enum payload variants" below).
|
**try/catch (Task 5).** A trap raised while a catch frame is live unwinds every frame *above* the catching one exactly as an uncaught trap does (drop maps run, registers null), then releases what the try region owned in the catching frame — the difference between the drop entry at the trapping instruction and the one at the handler pc — and resumes at the handler instead of leaving the VM. A frame that returns takes its still-open catch frames with it, so a `return` out of a try region cannot leave a handler pointing at a dead window. With no catch frame live, a trap behaves byte-for-byte as it did before v2. The catch arm's error record is an ordinary compiler-allocated object filled by the `err_fill` builtin (field order: 0 code, 1 line, 2 method, 3 msg).
|
||||||
|
|
||||||
**Trap codes:** DIV0, BORROW, STACK, OOM, DB, BOUNDS, KEY, EXPLICIT.
|
**Builtins:** now (ms), print (text), print_int, words (whitespace token count), multi_new/multi_push/multi_get/count/latest, map_new/map_set/map_get/map_has, int_to_text (haxe-parity Task 2), variant_tag (haxe-parity Task 4 — see "Enum payload variants" below), err_fill (Task 5's catch record), then the systems stdlib:
|
||||||
|
|
||||||
|
- **text/containers** — len, byte_at, print_err, starts_with, ends_with, index_of, last_index_of, substr, trim, to_lower, char_of, parse_int, split, split_ws, join, slice, pop, shift, sort, reverse, remove, key_at, val_at, multi_set. Ids 16–39; `runtime/src/builtin.c`.
|
||||||
|
- **the OS half** — fs.exists/list/stat/read_all/read_at/append, time.sleep/local/iso, env.get/stopping, net.listen/accept/read/write/close, proc.run. Ids 40–56; `runtime/src/sysio.c`. A member that returns a record takes that record's **class id as its last argument**, so the VM allocates what it fills without knowing any source type name.
|
||||||
|
- **json** — encode (value + the value's static kind), decode (text + the class id to build). Ids 57–58; `runtime/src/json.c`. Decode yields the zero word on malformed input rather than trapping, which is what makes `json.decode(t) as T` a checked decode.
|
||||||
|
|
||||||
|
**Trap codes:** DIV0, BORROW, STACK, OOM, DB, BOUNDS, KEY, EXPLICIT, IO (a syscall the source cannot prevent said no — errno's message rides in the error record).
|
||||||
|
|
||||||
## Enum payload variants (haxe-parity compiler Task 4)
|
## Enum payload variants (haxe-parity compiler Task 4)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,31 @@ maps to one `BUILTIN` id of the format doc.
|
||||||
| `set(m, k, v)` | `map_set` | 3 | insert or replace in a `map` |
|
| `set(m, k, v)` | `map_set` | 3 | insert or replace in a `map` |
|
||||||
| `has(m, k)` | `map_has` | 2 | `1`/`0` |
|
| `has(m, k)` | `map_has` | 2 | `1`/`0` |
|
||||||
| `int_to_text(n)` | `int_to_text` | 1 | decimal rendering of an `Int`, as a fresh owned `Text` — haxe-parity Task 2's one fenced VM addition, the type-directed half of string interpolation (below); also directly callable |
|
| `int_to_text(n)` | `int_to_text` | 1 | decimal rendering of an `Int`, as a fresh owned `Text` — haxe-parity Task 2's one fenced VM addition, the type-directed half of string interpolation (below); also directly callable |
|
||||||
|
| `len(x)` | `len` | 1 | byte length of a `Text`, or element/entry count of a container |
|
||||||
|
| `byte_at(t, i)` | `byte_at` | 2 | byte value at an index; out of range traps `BOUNDS` |
|
||||||
|
| `print_err(t)` | `print_err` | 1 | a `Text` to stderr, newline-terminated |
|
||||||
|
| `starts_with(t, p)` / `ends_with(t, s)` | same | 2 | `1`/`0` |
|
||||||
|
| `index_of(t, n)` / `last_index_of(t, n)` | same | 2 | first/last byte offset, `-1` when absent |
|
||||||
|
| `substr(t, start, len)` | `substr` | 3 | fresh `Text`, clamped (never traps) |
|
||||||
|
| `trim(t)` / `to_lower(t)` | same | 1 | fresh `Text` |
|
||||||
|
| `char_of(b)` | `char_of` | 1 | fresh one-byte `Text` |
|
||||||
|
| `parse_int(t)` | `parse_int` | 1 | `?Int` — an unparseable text is `0`, which is how `?Int` spells nil |
|
||||||
|
| `split(t, sep)` / `split_ws(t)` | same | 2 / 1 | fresh `multi Text` |
|
||||||
|
| `join(m, sep)` | `join` | 2 | fresh `Text` from a `multi Text` |
|
||||||
|
| `slice(m, from, to)` | `slice` | 3 | fresh `multi` over `[from, to)`; `Text` elements are COPIED, so slice and source never both own one value |
|
||||||
|
| `pop(m)` / `shift(m)` | same | 1 | removes and returns the last/first element (ownership moves to the caller); empty traps `BOUNDS` |
|
||||||
|
| `sort(m)` / `reverse(m)` | same | 1 | in place; `sort` compares `Text` by content, everything else as signed integers |
|
||||||
|
| `remove(m, k)` | `map_remove` | 2 | `1`/`0`; drops the removed key and value |
|
||||||
|
| `key_at(m, i)` / `val_at(m, i)` | same | 2 | slot-ordered map enumeration — what `for k, v in m` lowers onto |
|
||||||
|
| `m[i] = v` on a `multi` | `multi_set` | 3 | in-place element write, dropping the element it replaces |
|
||||||
|
|
||||||
`get`, `set`, `push`, `count` and `has` resolve on the container they are
|
`get`, `set`, `push`, `count` and `has` resolve on the container they are
|
||||||
given, so one source name covers the `multi` and `map` ids the runtime
|
given, so one source name covers the `multi` and `map` ids the runtime
|
||||||
keeps apart.
|
keeps apart.
|
||||||
|
|
||||||
**Sugar.** `c[i]` is exactly `get(c, i)` and `m[k] = v` is exactly
|
**Sugar.** `c[i]` is exactly `get(c, i)` and `m[k] = v` is exactly
|
||||||
`set(m, k, v)`. There is no element *write* into a `multi` — v1 has
|
`set(m, k, v)` for a `map` and `multi_set(m, i, v)` for a `multi` (the
|
||||||
`multi_push` and `multi_get` and no element store — so `m[i] = v` on a
|
element it replaces is the container's, so the VM drops it).
|
||||||
`multi` is `WO-E403`.
|
|
||||||
|
|
||||||
**Shadowing.** A user-declared free `fn` of the same name always wins. A
|
**Shadowing.** A user-declared free `fn` of the same name always wins. A
|
||||||
declared name is never silently replaced by a builtin.
|
declared name is never silently replaced by a builtin.
|
||||||
|
|
@ -218,3 +234,64 @@ rejects `6`), and it needs none: every per-kind drop plan already ignores
|
||||||
a zero slot. `?T`'s field kind is therefore `T`'s. Note the consequence
|
a zero slot. `?T`'s field kind is therefore `T`'s. Note the consequence
|
||||||
for `@gc`: `?SomeGcClass` is a `GCREF` field like any other, so it
|
for `@gc`: `?SomeGcClass` is a `GCREF` field like any other, so it
|
||||||
participates in refcounting and cycle detection normally.
|
participates in refcounting and cycle detection normally.
|
||||||
|
|
||||||
|
## The systems stdlib's OS half (`fs`, `time`, `env`, `net`, `proc`)
|
||||||
|
|
||||||
|
Reserved module names resolve to one builtin per member
|
||||||
|
(`runtime/src/sysio.c`). Every one is a thin blocking libc call, so the
|
||||||
|
failure surface is uniform: a syscall that fails traps `WO_T_IO` carrying
|
||||||
|
errno's own message, and the source decides with `try ... catch` whether
|
||||||
|
that is fatal. Absence is never a trap — a missing path from `fs.stat` and an
|
||||||
|
unset `env.get` are nil.
|
||||||
|
|
||||||
|
| member | signature | notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `fs.exists(path)` | `-> Bool` | |
|
||||||
|
| `fs.list(dir)` | `-> multi Text` | names only, unsorted; unreadable dir traps `IO` |
|
||||||
|
| `fs.stat(path)` | `-> ?Stat` | `Stat { size, mtime (ms), inode, dir }` |
|
||||||
|
| `fs.read_all(path, cap)` | `-> Text` | truncated at `cap` |
|
||||||
|
| `fs.read_at(path, off, len)` | `-> Text` | short read allowed (a growing file is normal) |
|
||||||
|
| `fs.append(path, text)` | — | creates the file if absent |
|
||||||
|
| `time.now()` | `-> Int` | wall-clock ms; the existing `now` builtin |
|
||||||
|
| `time.sleep(ms)` | — | |
|
||||||
|
| `time.local(ms)` | `-> TimeParts` | `{ year, month, day, hour, minute, second, dow }`, dow 0 = Sunday |
|
||||||
|
| `time.iso(ms)` | `-> Text` | UTC, second precision |
|
||||||
|
| `env.get(name)` | `-> ?Text` | unset is nil |
|
||||||
|
| `env.stopping()` | `-> Bool` | SIGTERM/SIGINT latch, handlers installed on first use |
|
||||||
|
| `net.listen(host, port)` | `-> Int` | IPv4, SO_REUSEADDR, backlog 64; returns an fd |
|
||||||
|
| `net.accept(fd)` | `-> Int` | |
|
||||||
|
| `net.read(fd, max)` | `-> Text` | one read; the empty Text is EOF |
|
||||||
|
| `net.write(fd, text)` | — | writes all of it |
|
||||||
|
| `net.close(fd)` | — | |
|
||||||
|
| `proc.run(cmd, args)` | `-> ?Proc` | `Proc { code, out, err }`; stdout/stderr captured and capped |
|
||||||
|
|
||||||
|
`Stat`, `TimeParts` and `Proc` are **predeclared records**: no source declares
|
||||||
|
them, and their field ORDER is the contract with `sysio.c`, which writes them
|
||||||
|
by index. The compiler passes the record's class id as the member's last
|
||||||
|
argument, so the VM allocates what it fills.
|
||||||
|
|
||||||
|
## `json`
|
||||||
|
|
||||||
|
`json.encode(x) -> Text` and `json.decode(text) as T -> ?T`. Both are
|
||||||
|
metadata-driven (`runtime/src/json.c`): the class table's per-field names,
|
||||||
|
referenced classes and element kinds (`.wob` v2) are what let one
|
||||||
|
implementation encode and decode any record shape, with no per-type generated
|
||||||
|
code.
|
||||||
|
|
||||||
|
- `encode` takes the value's *static* kind alongside it, because a register
|
||||||
|
alone cannot say whether it holds an i64 or a pointer; everything below the
|
||||||
|
top level comes from object headers and the class table.
|
||||||
|
- `decode` parses and binds straight into the target class: keys are matched
|
||||||
|
against field names, a nested object is built as that field's class, an
|
||||||
|
array as a `multi` of that field's element kind, unknown keys are skipped,
|
||||||
|
and absent keys stay nil. Malformed input yields nil — never a trap, which
|
||||||
|
is what makes the `as` form a *checked* decode. `as` exists for no other
|
||||||
|
purpose: there is no reinterpret cast in the doctrine.
|
||||||
|
- `json.Value` is a reserved type name for a decoded value the source does not
|
||||||
|
inspect: it holds the raw JSON slice it came from (kind TEXT) and encodes
|
||||||
|
back verbatim.
|
||||||
|
|
||||||
|
Two documented limits: a `Bool` field is a SCALAR slot like any other integer,
|
||||||
|
so it encodes as `0`/`1` rather than `false`/`true` (the kind byte does not
|
||||||
|
distinguish them); and a JSON number with a fraction or exponent decodes by
|
||||||
|
truncation to `Int`, since the language has no float.
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ static int elem_cmp(uint8_t kind, uint64_t a, uint64_t b) {
|
||||||
int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
||||||
wo_rt *rt = &vm->rt;
|
wo_rt *rt = &vm->rt;
|
||||||
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
|
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
|
||||||
/* the OS half lives in its own translation unit — see sysio.c */
|
/* the OS half and json live in their own translation units */
|
||||||
|
if (C >= WO_B_JSON_ENCODE) return wo_builtin_json(vm, R, ins, msg);
|
||||||
if (C >= WO_B_SYS_FIRST) return wo_builtin_sys(vm, R, ins, msg);
|
if (C >= WO_B_SYS_FIRST) return wo_builtin_sys(vm, R, ins, msg);
|
||||||
switch (C) {
|
switch (C) {
|
||||||
case WO_B_NOW: { /* wall-clock milliseconds */
|
case WO_B_NOW: { /* wall-clock milliseconds */
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,7 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
|
||||||
* wo_builtin dispatches every id at or above WO_B_SYS_FIRST here. */
|
* wo_builtin dispatches every id at or above WO_B_SYS_FIRST here. */
|
||||||
int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
|
int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
|
||||||
|
|
||||||
|
/* json.encode / json.decode (runtime/src/json.c), same contract again. */
|
||||||
|
int wo_builtin_json(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
|
||||||
|
|
||||||
#endif /* WO_BUILTIN_H */
|
#endif /* WO_BUILTIN_H */
|
||||||
|
|
|
||||||
585
runtime/src/json.c
Normal file
585
runtime/src/json.c
Normal file
|
|
@ -0,0 +1,585 @@
|
||||||
|
/* json.c — `json.encode` and `json.decode`, driven by class-table metadata
|
||||||
|
* instead of per-type generated code.
|
||||||
|
*
|
||||||
|
* The .wob class table carries, per field, its NAME, the CLASS it refers to
|
||||||
|
* and a container field's ELEMENT KINDS (wob.h's "class-table field
|
||||||
|
* metadata", format v2). That is everything both directions need:
|
||||||
|
*
|
||||||
|
* encode the top-level value's kind comes from the compiler (a register
|
||||||
|
* alone cannot say whether it holds an i64 or a pointer);
|
||||||
|
* everything below it comes from object headers and the class
|
||||||
|
* table, so a nested record needs no static knowledge at all.
|
||||||
|
* decode parse-and-bind straight into the target class: an object's keys
|
||||||
|
* are matched against field names, each value converted to that
|
||||||
|
* field's kind, a nested object built as that field's class, an
|
||||||
|
* array built as a `multi` of that field's element kind. Unknown
|
||||||
|
* keys are skipped; absent keys stay the zero word, which is how
|
||||||
|
* `?T` spells nil. Malformed input yields nil, never a trap —
|
||||||
|
* that is what makes `json.decode(t) as T` a *checked* decode.
|
||||||
|
*
|
||||||
|
* Two deliberate, documented limits: a `Bool` field is a WO_K_SCALAR slot
|
||||||
|
* like every other integer, so it encodes as 0/1 rather than false/true (the
|
||||||
|
* kind byte does not distinguish them); and a JSON number with a fraction or
|
||||||
|
* an exponent decodes by truncation to i64, since the language has no float.
|
||||||
|
* A `json.Value` field (field_class == WOB_FIELD_JSON_RAW) holds the raw JSON
|
||||||
|
* slice it was decoded from, and encodes back verbatim.
|
||||||
|
*/
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "builtin.h"
|
||||||
|
#include "cont.h"
|
||||||
|
#include "gc.h"
|
||||||
|
|
||||||
|
/* ---- growable output buffer (encode) --------------------------------- */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *p;
|
||||||
|
size_t len, cap;
|
||||||
|
int oom;
|
||||||
|
} jbuf;
|
||||||
|
|
||||||
|
static void jb_reserve(jbuf *b, size_t extra) {
|
||||||
|
if (b->oom) return;
|
||||||
|
if (b->len + extra <= b->cap) return;
|
||||||
|
size_t want = b->cap ? b->cap * 2 : 256;
|
||||||
|
while (want < b->len + extra) want *= 2;
|
||||||
|
char *np = realloc(b->p, want);
|
||||||
|
if (!np) {
|
||||||
|
b->oom = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
b->p = np;
|
||||||
|
b->cap = want;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jb_put(jbuf *b, const char *s, size_t n) {
|
||||||
|
jb_reserve(b, n);
|
||||||
|
if (b->oom) return;
|
||||||
|
memcpy(b->p + b->len, s, n);
|
||||||
|
b->len += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jb_ch(jbuf *b, char c) { jb_put(b, &c, 1); }
|
||||||
|
|
||||||
|
static void jb_int(jbuf *b, int64_t v) {
|
||||||
|
char tmp[24];
|
||||||
|
int n = snprintf(tmp, sizeof tmp, "%lld", (long long)v);
|
||||||
|
jb_put(b, tmp, (size_t)n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON string body: quotes, backslashes and control bytes escaped; every
|
||||||
|
* other byte passes through, so UTF-8 stays UTF-8. */
|
||||||
|
static void jb_text(jbuf *b, const wo_str *s) {
|
||||||
|
jb_ch(b, '"');
|
||||||
|
for (uint32_t i = 0; i < s->len; i++) {
|
||||||
|
unsigned char c = (unsigned char)s->data[i];
|
||||||
|
switch (c) {
|
||||||
|
case '"': jb_put(b, "\\\"", 2); break;
|
||||||
|
case '\\': jb_put(b, "\\\\", 2); break;
|
||||||
|
case '\n': jb_put(b, "\\n", 2); break;
|
||||||
|
case '\r': jb_put(b, "\\r", 2); break;
|
||||||
|
case '\t': jb_put(b, "\\t", 2); break;
|
||||||
|
case '\b': jb_put(b, "\\b", 2); break;
|
||||||
|
case '\f': jb_put(b, "\\f", 2); break;
|
||||||
|
default:
|
||||||
|
if (c < 0x20) {
|
||||||
|
char esc[7];
|
||||||
|
int n = snprintf(esc, sizeof esc, "\\u%04x", c);
|
||||||
|
jb_put(b, esc, (size_t)n);
|
||||||
|
} else
|
||||||
|
jb_ch(b, (char)c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jb_ch(b, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- encode ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void enc_value(jbuf *b, const wo_module *mod, uint64_t v, uint8_t kind, uint32_t fclass);
|
||||||
|
|
||||||
|
static void enc_object(jbuf *b, const wo_module *mod, const wo_hdr *o) {
|
||||||
|
const wo_classdesc *c = &mod->classes[o->class_id];
|
||||||
|
const uint64_t *fs = wo_fields((wo_hdr *)(uintptr_t)o);
|
||||||
|
jb_ch(b, '{');
|
||||||
|
int first = 1;
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
|
uint32_t nm = c->field_names ? c->field_names[i] : WOB_NONE;
|
||||||
|
if (nm == WOB_NONE || nm >= mod->const_cnt) continue; /* unnamed: not encodable */
|
||||||
|
if (!first) jb_ch(b, ',');
|
||||||
|
first = 0;
|
||||||
|
jb_text(b, mod->consts[nm].s);
|
||||||
|
jb_ch(b, ':');
|
||||||
|
enc_value(b, mod, fs[i], c->kinds[i], c->field_class ? c->field_class[i] : WOB_NONE);
|
||||||
|
}
|
||||||
|
jb_ch(b, '}');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void enc_value(jbuf *b, const wo_module *mod, uint64_t v, uint8_t kind, uint32_t fclass) {
|
||||||
|
if (!v && kind != WO_K_SCALAR) {
|
||||||
|
jb_put(b, "null", 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (kind) {
|
||||||
|
case WO_K_SCALAR:
|
||||||
|
jb_int(b, (int64_t)v);
|
||||||
|
return;
|
||||||
|
case WO_K_TEXT: {
|
||||||
|
const wo_str *s = (const wo_str *)(uintptr_t)v;
|
||||||
|
if (fclass == WOB_FIELD_JSON_RAW) jb_put(b, s->data, s->len); /* already JSON */
|
||||||
|
else jb_text(b, s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case WO_K_MULTI: {
|
||||||
|
const wo_multi *m = (const wo_multi *)(uintptr_t)v;
|
||||||
|
jb_ch(b, '[');
|
||||||
|
for (uint32_t i = 0; i < m->len; i++) {
|
||||||
|
if (i) jb_ch(b, ',');
|
||||||
|
enc_value(b, mod, m->items[i], m->elem_kind, fclass);
|
||||||
|
}
|
||||||
|
jb_ch(b, ']');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case WO_K_MAP: {
|
||||||
|
const wo_map *m = (const wo_map *)(uintptr_t)v;
|
||||||
|
jb_ch(b, '{');
|
||||||
|
for (uint32_t i = 0; i < m->len; i++) {
|
||||||
|
if (i) jb_ch(b, ',');
|
||||||
|
if (m->key_kind == WO_K_TEXT && m->keys[i]) jb_text(b, (const wo_str *)(uintptr_t)m->keys[i]);
|
||||||
|
else {
|
||||||
|
/* a non-Text key still has to be a JSON string */
|
||||||
|
jb_ch(b, '"');
|
||||||
|
jb_int(b, (int64_t)m->keys[i]);
|
||||||
|
jb_ch(b, '"');
|
||||||
|
}
|
||||||
|
jb_ch(b, ':');
|
||||||
|
enc_value(b, mod, m->vals[i], m->val_kind, fclass);
|
||||||
|
}
|
||||||
|
jb_ch(b, '}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: { /* OWNED / GCREF: a class object, or a native one */
|
||||||
|
const wo_hdr *o = (const wo_hdr *)(uintptr_t)v;
|
||||||
|
if (o->class_id == WO_CLS_STR) {
|
||||||
|
jb_text(b, (const wo_str *)(uintptr_t)o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o->class_id == WO_CLS_MULTI) {
|
||||||
|
enc_value(b, mod, v, WO_K_MULTI, fclass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o->class_id == WO_CLS_MAP) {
|
||||||
|
enc_value(b, mod, v, WO_K_MAP, fclass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o->class_id < mod->class_cnt) {
|
||||||
|
enc_object(b, mod, o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jb_put(b, "null", 4);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- decode: parse and bind ------------------------------------------ */
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *p, *end;
|
||||||
|
wo_rt *rt;
|
||||||
|
const wo_module *mod;
|
||||||
|
} jp;
|
||||||
|
|
||||||
|
static void jskip_ws(jp *j) {
|
||||||
|
while (j->p < j->end && (*j->p == ' ' || *j->p == '\t' || *j->p == '\n' || *j->p == '\r')) j->p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int jparse_value(jp *j, uint8_t kind, uint32_t fclass, uint32_t felem, uint64_t *out);
|
||||||
|
|
||||||
|
/* Walks one value without building anything — an unknown object key, or a
|
||||||
|
* value whose JSON shape does not fit the field it landed on. */
|
||||||
|
static int jskip_value(jp *j) {
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p >= j->end) return -1;
|
||||||
|
char c = *j->p;
|
||||||
|
if (c == '{' || c == '[') {
|
||||||
|
char close = c == '{' ? '}' : ']';
|
||||||
|
int depth = 0;
|
||||||
|
while (j->p < j->end) {
|
||||||
|
char d = *j->p++;
|
||||||
|
if (d == '"') { /* strings may contain braces */
|
||||||
|
while (j->p < j->end && *j->p != '"') {
|
||||||
|
if (*j->p == '\\' && j->p + 1 < j->end) j->p++;
|
||||||
|
j->p++;
|
||||||
|
}
|
||||||
|
if (j->p < j->end) j->p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (d == '{' || d == '[') depth++;
|
||||||
|
else if (d == '}' || d == ']') {
|
||||||
|
depth--;
|
||||||
|
if (depth == 0) return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(void)close;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (c == '"') {
|
||||||
|
j->p++;
|
||||||
|
while (j->p < j->end && *j->p != '"') {
|
||||||
|
if (*j->p == '\\' && j->p + 1 < j->end) j->p++;
|
||||||
|
j->p++;
|
||||||
|
}
|
||||||
|
if (j->p >= j->end) return -1;
|
||||||
|
j->p++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
while (j->p < j->end && *j->p != ',' && *j->p != '}' && *j->p != ']' &&
|
||||||
|
*j->p != ' ' && *j->p != '\n' && *j->p != '\t' && *j->p != '\r')
|
||||||
|
j->p++;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A JSON string into a fresh Text, applying escapes. \uXXXX becomes UTF-8
|
||||||
|
* (BMP only: a surrogate pair decodes as two replacement-free code units,
|
||||||
|
* which is what every byte-oriented consumer here wants). */
|
||||||
|
static wo_str *jparse_string(jp *j) {
|
||||||
|
if (j->p >= j->end || *j->p != '"') return NULL;
|
||||||
|
const char *start = ++j->p;
|
||||||
|
size_t worst = (size_t)(j->end - start);
|
||||||
|
wo_str *s = wo_str_alloc(j->rt, (uint32_t)worst);
|
||||||
|
if (!s) return NULL;
|
||||||
|
uint32_t n = 0;
|
||||||
|
while (j->p < j->end && *j->p != '"') {
|
||||||
|
char c = *j->p++;
|
||||||
|
if (c != '\\') {
|
||||||
|
s->data[n++] = c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (j->p >= j->end) break;
|
||||||
|
char e = *j->p++;
|
||||||
|
switch (e) {
|
||||||
|
case 'n': s->data[n++] = '\n'; break;
|
||||||
|
case 't': s->data[n++] = '\t'; break;
|
||||||
|
case 'r': s->data[n++] = '\r'; break;
|
||||||
|
case 'b': s->data[n++] = '\b'; break;
|
||||||
|
case 'f': s->data[n++] = '\f'; break;
|
||||||
|
case 'u': {
|
||||||
|
unsigned cp = 0;
|
||||||
|
for (int k = 0; k < 4 && j->p < j->end; k++) {
|
||||||
|
char h = *j->p++;
|
||||||
|
unsigned d = (unsigned)(h >= '0' && h <= '9' ? h - '0'
|
||||||
|
: h >= 'a' && h <= 'f' ? h - 'a' + 10
|
||||||
|
: h >= 'A' && h <= 'F' ? h - 'A' + 10
|
||||||
|
: 0);
|
||||||
|
cp = cp * 16 + d;
|
||||||
|
}
|
||||||
|
if (cp < 0x80) s->data[n++] = (char)cp;
|
||||||
|
else if (cp < 0x800) {
|
||||||
|
s->data[n++] = (char)(0xC0 | (cp >> 6));
|
||||||
|
s->data[n++] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
} else {
|
||||||
|
s->data[n++] = (char)(0xE0 | (cp >> 12));
|
||||||
|
s->data[n++] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||||
|
s->data[n++] = (char)(0x80 | (cp & 0x3F));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: s->data[n++] = e; /* covers \" \\ \/ */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (j->p >= j->end) {
|
||||||
|
wo_str_free(j->rt, s);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
j->p++; /* closing quote */
|
||||||
|
s->len = n;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An object into a fresh instance of [class_id]: keys matched against the
|
||||||
|
* class's field names, values converted to each field's own kind. */
|
||||||
|
static int jparse_object(jp *j, uint32_t class_id, uint64_t *out) {
|
||||||
|
const wo_classdesc *c = &j->mod->classes[class_id];
|
||||||
|
wo_hdr *o = wo_obj_new(j->rt, class_id);
|
||||||
|
if (!o) return -1;
|
||||||
|
uint64_t *fs = wo_fields(o);
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p >= j->end || *j->p != '{') {
|
||||||
|
wo_drop_obj(j->rt, o);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
j->p++;
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p < j->end && *j->p == '}') {
|
||||||
|
j->p++;
|
||||||
|
*out = (uint64_t)(uintptr_t)o;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
jskip_ws(j);
|
||||||
|
wo_str *key = jparse_string(j);
|
||||||
|
if (!key) {
|
||||||
|
wo_drop_obj(j->rt, o);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p >= j->end || *j->p != ':') {
|
||||||
|
wo_str_free(j->rt, key);
|
||||||
|
wo_drop_obj(j->rt, o);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
j->p++;
|
||||||
|
/* which field is this key? */
|
||||||
|
uint32_t idx = c->field_cnt;
|
||||||
|
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||||
|
uint32_t nm = c->field_names ? c->field_names[i] : WOB_NONE;
|
||||||
|
if (nm == WOB_NONE || nm >= j->mod->const_cnt) continue;
|
||||||
|
const wo_str *fname = j->mod->consts[nm].s;
|
||||||
|
if (fname->len == key->len && !memcmp(fname->data, key->data, key->len)) {
|
||||||
|
idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wo_str_free(j->rt, key);
|
||||||
|
if (idx == c->field_cnt) {
|
||||||
|
if (jskip_value(j) != 0) {
|
||||||
|
wo_drop_obj(j->rt, o);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uint64_t val = 0;
|
||||||
|
if (jparse_value(j, c->kinds[idx], c->field_class ? c->field_class[idx] : WOB_NONE,
|
||||||
|
c->field_elem ? c->field_elem[idx] : 0, &val) != 0) {
|
||||||
|
wo_drop_obj(j->rt, o);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fs[idx] = val;
|
||||||
|
}
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p < j->end && *j->p == ',') {
|
||||||
|
j->p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (j->p < j->end && *j->p == '}') {
|
||||||
|
j->p++;
|
||||||
|
*out = (uint64_t)(uintptr_t)o;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
wo_drop_obj(j->rt, o);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int jparse_value(jp *j, uint8_t kind, uint32_t fclass, uint32_t felem, uint64_t *out) {
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p >= j->end) return -1;
|
||||||
|
/* a `json.Value` field keeps the raw slice, whatever shape it is */
|
||||||
|
if (fclass == WOB_FIELD_JSON_RAW) {
|
||||||
|
const char *start = j->p;
|
||||||
|
if (jskip_value(j) != 0) return -1;
|
||||||
|
wo_str *raw = wo_str_new(j->rt, start, (uint32_t)(j->p - start));
|
||||||
|
if (!raw) return -1;
|
||||||
|
*out = (uint64_t)(uintptr_t)raw;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
char c = *j->p;
|
||||||
|
if (c == 'n') { /* null: the zero word, for every kind */
|
||||||
|
return jskip_value(j) == 0 ? (*out = 0, 0) : -1;
|
||||||
|
}
|
||||||
|
if (c == '{') {
|
||||||
|
if ((kind == WO_K_OWNED || kind == WO_K_GCREF) && fclass < j->mod->class_cnt)
|
||||||
|
return jparse_object(j, fclass, out);
|
||||||
|
if (kind == WO_K_MAP) {
|
||||||
|
uint8_t kk = (uint8_t)(felem & 0x0F), vk = (uint8_t)((felem >> 4) & 0x0F);
|
||||||
|
wo_map *m = wo_map_new(j->rt, kk, vk);
|
||||||
|
if (!m) return -1;
|
||||||
|
j->p++;
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p < j->end && *j->p == '}') {
|
||||||
|
j->p++;
|
||||||
|
*out = (uint64_t)(uintptr_t)m;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
jskip_ws(j);
|
||||||
|
wo_str *key = jparse_string(j);
|
||||||
|
if (!key) {
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p >= j->end || *j->p != ':') {
|
||||||
|
wo_str_free(j->rt, key);
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
j->p++;
|
||||||
|
uint64_t val = 0;
|
||||||
|
if (jparse_value(j, vk, fclass, 0, &val) != 0) {
|
||||||
|
wo_str_free(j->rt, key);
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
uint64_t old = 0;
|
||||||
|
if (wo_map_set(m, (uint64_t)(uintptr_t)key, val, &old) < 0) {
|
||||||
|
wo_str_free(j->rt, key);
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p < j->end && *j->p == ',') {
|
||||||
|
j->p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (j->p < j->end && *j->p == '}') {
|
||||||
|
j->p++;
|
||||||
|
*out = (uint64_t)(uintptr_t)m;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* an object where the field wants something else: skip it, leave nil */
|
||||||
|
*out = 0;
|
||||||
|
return jskip_value(j);
|
||||||
|
}
|
||||||
|
if (c == '[') {
|
||||||
|
if (kind != WO_K_MULTI) {
|
||||||
|
*out = 0;
|
||||||
|
return jskip_value(j);
|
||||||
|
}
|
||||||
|
uint8_t ek = (uint8_t)(felem & 0x0F);
|
||||||
|
wo_multi *m = wo_multi_new(j->rt, ek);
|
||||||
|
if (!m) return -1;
|
||||||
|
j->p++;
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p < j->end && *j->p == ']') {
|
||||||
|
j->p++;
|
||||||
|
*out = (uint64_t)(uintptr_t)m;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
uint64_t item = 0;
|
||||||
|
if (jparse_value(j, ek, fclass, 0, &item) != 0 || wo_multi_push(m, item) != 0) {
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
jskip_ws(j);
|
||||||
|
if (j->p < j->end && *j->p == ',') {
|
||||||
|
j->p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (j->p < j->end && *j->p == ']') {
|
||||||
|
j->p++;
|
||||||
|
*out = (uint64_t)(uintptr_t)m;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
wo_drop_obj(j->rt, &m->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c == '"') {
|
||||||
|
wo_str *s = jparse_string(j);
|
||||||
|
if (!s) return -1;
|
||||||
|
if (kind == WO_K_TEXT) {
|
||||||
|
*out = (uint64_t)(uintptr_t)s;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
wo_str_free(j->rt, s); /* a string where a number was declared: nil */
|
||||||
|
*out = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (c == 't' || c == 'f') {
|
||||||
|
int truth = c == 't';
|
||||||
|
if (jskip_value(j) != 0) return -1;
|
||||||
|
*out = kind == WO_K_SCALAR ? (uint64_t)truth : 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* number: i64 by truncation — the language has no float */
|
||||||
|
{
|
||||||
|
int neg = 0;
|
||||||
|
if (*j->p == '-') {
|
||||||
|
neg = 1;
|
||||||
|
j->p++;
|
||||||
|
} else if (*j->p == '+')
|
||||||
|
j->p++;
|
||||||
|
int64_t acc = 0;
|
||||||
|
int digits = 0;
|
||||||
|
while (j->p < j->end && *j->p >= '0' && *j->p <= '9') {
|
||||||
|
acc = acc * 10 + (*j->p++ - '0');
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
if (!digits) return -1;
|
||||||
|
if (j->p < j->end && (*j->p == '.' || *j->p == 'e' || *j->p == 'E')) {
|
||||||
|
/* consume the fraction/exponent; the integer part is the value */
|
||||||
|
if (*j->p == '.') {
|
||||||
|
j->p++;
|
||||||
|
while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++;
|
||||||
|
}
|
||||||
|
if (j->p < j->end && (*j->p == 'e' || *j->p == 'E')) {
|
||||||
|
j->p++;
|
||||||
|
if (j->p < j->end && (*j->p == '-' || *j->p == '+')) j->p++;
|
||||||
|
while (j->p < j->end && *j->p >= '0' && *j->p <= '9') j->p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = kind == WO_K_SCALAR ? (uint64_t)(neg ? -acc : acc) : 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int wo_builtin_json(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
||||||
|
wo_rt *rt = &vm->rt;
|
||||||
|
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
|
||||||
|
switch (C) {
|
||||||
|
case WO_B_JSON_ENCODE: {
|
||||||
|
jbuf b = {NULL, 0, 0, 0};
|
||||||
|
uint8_t kind = (uint8_t)R[B + 1];
|
||||||
|
enc_value(&b, vm->mod, R[B], kind, WOB_NONE);
|
||||||
|
if (b.oom) {
|
||||||
|
free(b.p);
|
||||||
|
*msg = "out of memory";
|
||||||
|
return WO_T_OOM;
|
||||||
|
}
|
||||||
|
wo_str *s = wo_str_new(rt, b.p ? b.p : "", (uint32_t)b.len);
|
||||||
|
free(b.p);
|
||||||
|
if (!s) {
|
||||||
|
*msg = "out of memory";
|
||||||
|
return WO_T_OOM;
|
||||||
|
}
|
||||||
|
R[A] = (uint64_t)(uintptr_t)s;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case WO_B_JSON_DECODE: { /* malformed input is nil, never a trap */
|
||||||
|
if (!R[B]) {
|
||||||
|
R[A] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const wo_str *src = (const wo_str *)(uintptr_t)R[B];
|
||||||
|
if (src->h.class_id != WO_CLS_STR) {
|
||||||
|
*msg = "not a text value";
|
||||||
|
return WO_T_BOUNDS;
|
||||||
|
}
|
||||||
|
uint64_t cls = R[B + 1];
|
||||||
|
if (cls >= vm->mod->class_cnt) {
|
||||||
|
*msg = "decode target is not a class";
|
||||||
|
return WO_T_BOUNDS;
|
||||||
|
}
|
||||||
|
jp j = {src->data, src->data + src->len, rt, vm->mod};
|
||||||
|
uint64_t out = 0;
|
||||||
|
if (jparse_object(&j, (uint32_t)cls, &out) != 0) {
|
||||||
|
R[A] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
R[A] = out;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
*msg = "unknown json builtin";
|
||||||
|
return WO_T_EXPLICIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -59,6 +59,9 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
|
||||||
[WO_B_ENV_GET] = 1, [WO_B_ENV_STOPPING] = 0, [WO_B_NET_LISTEN] = 2,
|
[WO_B_ENV_GET] = 1, [WO_B_ENV_STOPPING] = 0, [WO_B_NET_LISTEN] = 2,
|
||||||
[WO_B_NET_ACCEPT] = 1, [WO_B_NET_READ] = 2, [WO_B_NET_WRITE] = 2,
|
[WO_B_NET_ACCEPT] = 1, [WO_B_NET_READ] = 2, [WO_B_NET_WRITE] = 2,
|
||||||
[WO_B_NET_CLOSE] = 1, [WO_B_PROC_RUN] = 3,
|
[WO_B_NET_CLOSE] = 1, [WO_B_PROC_RUN] = 3,
|
||||||
|
/* json (json.c): encode takes the value's static kind, decode the class
|
||||||
|
id to build */
|
||||||
|
[WO_B_JSON_ENCODE] = 2, [WO_B_JSON_DECODE] = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
static int vtab_cmp(const void *a, const void *b) {
|
static int vtab_cmp(const void *a, const void *b) {
|
||||||
|
|
@ -137,7 +140,7 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
||||||
m->classes = calloc(kcnt, sizeof(wo_classdesc));
|
m->classes = calloc(kcnt, sizeof(wo_classdesc));
|
||||||
if (!m->classes) BAIL("out of memory");
|
if (!m->classes) BAIL("out of memory");
|
||||||
}
|
}
|
||||||
size_t pool_len = 0;
|
size_t pool_len = 0, meta_pool = 0;
|
||||||
for (uint32_t i = 0; i < kcnt; i++) {
|
for (uint32_t i = 0; i < kcnt; i++) {
|
||||||
uint32_t name, flags, fcnt;
|
uint32_t name, flags, fcnt;
|
||||||
if (rd_u32(&k, &name) || rd_u32(&k, &flags) || rd_u32(&k, &fcnt))
|
if (rd_u32(&k, &name) || rd_u32(&k, &flags) || rd_u32(&k, &fcnt))
|
||||||
|
|
@ -159,15 +162,47 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
||||||
uint32_t pad = (4u - fcnt % 4u) % 4u;
|
uint32_t pad = (4u - fcnt % 4u) % 4u;
|
||||||
if (pad > k.len - k.off) BAIL("class %u: truncated pad", (unsigned)i);
|
if (pad > k.len - k.off) BAIL("class %u: truncated pad", (unsigned)i);
|
||||||
k.off += pad;
|
k.off += pad;
|
||||||
|
/* v2: three u32 arrays of per-field metadata (names, referenced
|
||||||
|
class ids, container element kinds) — wob.h's "class-table field
|
||||||
|
metadata" note. A name of WOB_NONE means "not recorded", which is
|
||||||
|
what a hand-built test image writes; any other value must be a
|
||||||
|
real Text constant, since json.encode renders it as a key. */
|
||||||
|
size_t meta_words = (size_t)fcnt * 3u;
|
||||||
|
if (meta_words * 4u > k.len - k.off) BAIL("class %u: truncated field metadata", (unsigned)i);
|
||||||
|
uint32_t *mp = realloc(m->metapool, (meta_pool + (meta_words ? meta_words : 1)) * sizeof(uint32_t));
|
||||||
|
if (!mp) BAIL("out of memory");
|
||||||
|
m->metapool = mp;
|
||||||
|
for (size_t w = 0; w < meta_words; w++) {
|
||||||
|
uint32_t val;
|
||||||
|
if (rd_u32(&k, &val)) BAIL("class %u: truncated field metadata", (unsigned)i);
|
||||||
|
m->metapool[meta_pool + w] = val;
|
||||||
|
}
|
||||||
|
for (uint32_t j = 0; j < fcnt; j++) {
|
||||||
|
uint32_t nm = m->metapool[meta_pool + j];
|
||||||
|
if (nm != WOB_NONE && (nm >= m->const_cnt || m->consts[nm].tag != WOB_K_TEXT))
|
||||||
|
BAIL("class %u field %u: bad name constant", (unsigned)i, (unsigned)j);
|
||||||
|
uint32_t fc = m->metapool[meta_pool + fcnt + j];
|
||||||
|
if (fc != WOB_NONE && fc != WOB_FIELD_JSON_RAW && fc >= kcnt)
|
||||||
|
BAIL("class %u field %u: field class out of range", (unsigned)i, (unsigned)j);
|
||||||
|
}
|
||||||
m->classes[i].name = name;
|
m->classes[i].name = name;
|
||||||
m->classes[i].flags = flags;
|
m->classes[i].flags = flags;
|
||||||
m->classes[i].field_cnt = fcnt;
|
m->classes[i].field_cnt = fcnt;
|
||||||
m->classes[i].kinds = (const uint8_t *)(uintptr_t)pool_len; /* offset */
|
m->classes[i].kinds = (const uint8_t *)(uintptr_t)pool_len; /* offset */
|
||||||
|
/* offsets too; fixed up to pointers once the pool stops moving */
|
||||||
|
m->classes[i].field_names = (const uint32_t *)(uintptr_t)meta_pool;
|
||||||
|
m->classes[i].field_class = (const uint32_t *)(uintptr_t)(meta_pool + fcnt);
|
||||||
|
m->classes[i].field_elem = (const uint32_t *)(uintptr_t)(meta_pool + 2u * (size_t)fcnt);
|
||||||
pool_len += fcnt ? fcnt : 1;
|
pool_len += fcnt ? fcnt : 1;
|
||||||
|
meta_pool += meta_words ? meta_words : 1;
|
||||||
m->class_cnt = i + 1;
|
m->class_cnt = i + 1;
|
||||||
}
|
}
|
||||||
for (uint32_t i = 0; i < m->class_cnt; i++)
|
for (uint32_t i = 0; i < m->class_cnt; i++) {
|
||||||
m->classes[i].kinds = m->kindpool + (uintptr_t)m->classes[i].kinds;
|
m->classes[i].kinds = m->kindpool + (uintptr_t)m->classes[i].kinds;
|
||||||
|
m->classes[i].field_names = m->metapool + (uintptr_t)m->classes[i].field_names;
|
||||||
|
m->classes[i].field_class = m->metapool + (uintptr_t)m->classes[i].field_class;
|
||||||
|
m->classes[i].field_elem = m->metapool + (uintptr_t)m->classes[i].field_elem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- interfaces + vtable rows (expanded to sorted triples) ---- */
|
/* ---- interfaces + vtable rows (expanded to sorted triples) ---- */
|
||||||
cur_t s = {buf, len, ioff};
|
cur_t s = {buf, len, ioff};
|
||||||
|
|
@ -485,6 +520,7 @@ void wo_module_free(wo_module *m) {
|
||||||
free(m->consts);
|
free(m->consts);
|
||||||
free(m->classes);
|
free(m->classes);
|
||||||
free(m->kindpool);
|
free(m->kindpool);
|
||||||
|
free(m->metapool);
|
||||||
free(m->vtabs);
|
free(m->vtabs);
|
||||||
for (uint32_t i = 0; i < m->method_cnt; i++) {
|
for (uint32_t i = 0; i < m->method_cnt; i++) {
|
||||||
free(m->methods[i].code);
|
free(m->methods[i].code);
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ typedef struct wo_module {
|
||||||
wo_classdesc *classes;
|
wo_classdesc *classes;
|
||||||
uint32_t class_cnt;
|
uint32_t class_cnt;
|
||||||
uint8_t *kindpool; /* pooled field-kind bytes the classes point into */
|
uint8_t *kindpool; /* pooled field-kind bytes the classes point into */
|
||||||
|
/* pooled per-field metadata (v2): names, referenced class ids, element
|
||||||
|
kinds — see wob.h's "class-table field metadata" note */
|
||||||
|
uint32_t *metapool;
|
||||||
uint32_t slot_cnt; /* total interface slots across all interfaces */
|
uint32_t slot_cnt; /* total interface slots across all interfaces */
|
||||||
wo_vtabent *vtabs;
|
wo_vtabent *vtabs;
|
||||||
uint32_t vtab_cnt;
|
uint32_t vtab_cnt;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
/* ---- file header (44 bytes, absolute offsets) ---- */
|
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||||
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
||||||
#define WOB_VERSION 1u
|
#define WOB_VERSION 2u /* v2 adds per-field names/types to the class table */
|
||||||
#define WOB_HDR_SIZE 44u
|
#define WOB_HDR_SIZE 44u
|
||||||
#define WOB_OFF_MAGIC 0u
|
#define WOB_OFF_MAGIC 0u
|
||||||
#define WOB_OFF_VERSION 4u
|
#define WOB_OFF_VERSION 4u
|
||||||
|
|
@ -23,6 +23,24 @@
|
||||||
#define WOB_OFF_ENTRY 40u
|
#define WOB_OFF_ENTRY 40u
|
||||||
#define WOB_NONE 0xFFFFFFFFu /* "no entry method" / "free fn" class id */
|
#define WOB_NONE 0xFFFFFFFFu /* "no entry method" / "free fn" class id */
|
||||||
|
|
||||||
|
/* ---- class-table field metadata (v2) ----
|
||||||
|
* Every class row carries, after its kind bytes, three u32 arrays — one
|
||||||
|
* entry per field: the constant index of the field's NAME, the class id the
|
||||||
|
* field REFERS to, and the element kinds of a container field. They exist
|
||||||
|
* for one reason: `json.encode`/`json.decode` are runtime services driven by
|
||||||
|
* class metadata (runtime/src/json.c) instead of per-type generated code, so
|
||||||
|
* the names a JSON object needs and the shapes a decode must build have to
|
||||||
|
* be in the image. Absent metadata is WOB_NONE / 0, which every other part
|
||||||
|
* of the runtime ignores.
|
||||||
|
*
|
||||||
|
* field_class[i]: the class id of an OWNED/GCREF field, or of a container
|
||||||
|
* field's element when that element is a class; WOB_FIELD_JSON_RAW marks a
|
||||||
|
* `json.Value` field, whose Text holds raw JSON that encode emits verbatim
|
||||||
|
* and decode captures unparsed; WOB_NONE otherwise.
|
||||||
|
* field_elem[i]: for a MULTI field, its element kind; for a MAP field, the
|
||||||
|
* key kind in the low byte and the value kind in the next; 0 otherwise. */
|
||||||
|
#define WOB_FIELD_JSON_RAW 0xFFFFFFFEu
|
||||||
|
|
||||||
/* ---- constant pool tags ---- */
|
/* ---- constant pool tags ---- */
|
||||||
#define WOB_K_INT 0u /* tag byte, then i64 */
|
#define WOB_K_INT 0u /* tag byte, then i64 */
|
||||||
#define WOB_K_TEXT 1u /* tag byte, then u32 len + bytes (no NUL) */
|
#define WOB_K_TEXT 1u /* tag byte, then u32 len + bytes (no NUL) */
|
||||||
|
|
@ -235,8 +253,16 @@ enum {
|
||||||
WO_B_NET_WRITE = 54, /* (fd, text) -> 0 */
|
WO_B_NET_WRITE = 54, /* (fd, text) -> 0 */
|
||||||
WO_B_NET_CLOSE = 55, /* (fd) -> 0 */
|
WO_B_NET_CLOSE = 55, /* (fd) -> 0 */
|
||||||
WO_B_PROC_RUN = 56, /* (cmd, multi Text args, cls) -> Proc {code, out, err} */
|
WO_B_PROC_RUN = 56, /* (cmd, multi Text args, cls) -> Proc {code, out, err} */
|
||||||
|
/* ---- json (runtime/src/json.c): metadata-driven, not per-type code.
|
||||||
|
* encode takes the STATIC kind of its argument, because a register alone
|
||||||
|
* cannot say whether it holds an i64 or a pointer; everything below the
|
||||||
|
* top level comes from object headers and the class table. decode takes
|
||||||
|
* the class id to build, and yields nil (0) on malformed input — never a
|
||||||
|
* trap, which is what makes `json.decode(t) as T` a checked decode. ---- */
|
||||||
|
WO_B_JSON_ENCODE = 57, /* (value, kind) -> Text */
|
||||||
|
WO_B_JSON_DECODE = 58, /* (text, cls) -> ?instance of cls */
|
||||||
};
|
};
|
||||||
#define WO_B_MAX 56u
|
#define WO_B_MAX 58u
|
||||||
/* ids at or above this one live in sysio.c, not builtin.c */
|
/* ids at or above this one live in sysio.c, not builtin.c */
|
||||||
#define WO_B_SYS_FIRST WO_B_FS_EXISTS
|
#define WO_B_SYS_FIRST WO_B_FS_EXISTS
|
||||||
|
|
||||||
|
|
@ -264,6 +290,12 @@ typedef struct wo_classdesc {
|
||||||
uint32_t flags; /* bit0: instances are @gc */
|
uint32_t flags; /* bit0: instances are @gc */
|
||||||
uint32_t field_cnt;
|
uint32_t field_cnt;
|
||||||
const uint8_t *kinds; /* field_cnt kind bytes, declaration order */
|
const uint8_t *kinds; /* field_cnt kind bytes, declaration order */
|
||||||
|
/* v2 per-field metadata, field_cnt entries each — see the
|
||||||
|
* "class-table field metadata" note above. Both may be NULL for a
|
||||||
|
* class an image wrote with no metadata at all. */
|
||||||
|
const uint32_t *field_names; /* constant index of each field's name */
|
||||||
|
const uint32_t *field_class; /* referenced class id / JSON_RAW / NONE */
|
||||||
|
const uint32_t *field_elem; /* container element kinds */
|
||||||
} wo_classdesc;
|
} wo_classdesc;
|
||||||
#define WO_CLASSF_GC 0x01u
|
#define WO_CLASSF_GC 0x01u
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@ uint32_t wb_class(wb_t *b, uint32_t name_const, uint32_t flags,
|
||||||
put_u32(&b->classes, field_cnt);
|
put_u32(&b->classes, field_cnt);
|
||||||
put(&b->classes, kinds, field_cnt);
|
put(&b->classes, kinds, field_cnt);
|
||||||
for (uint32_t pad = field_cnt; pad % 4; pad++) put_u8(&b->classes, 0);
|
for (uint32_t pad = field_cnt; pad % 4; pad++) put_u8(&b->classes, 0);
|
||||||
|
/* v2 per-field metadata (wob.h): a hand-built image records no field
|
||||||
|
names and no referenced classes — WOB_NONE reads as "not recorded",
|
||||||
|
which every consumer but json.encode/decode ignores. */
|
||||||
|
for (uint32_t j = 0; j < field_cnt; j++) put_u32(&b->classes, WOB_NONE);
|
||||||
|
for (uint32_t j = 0; j < field_cnt; j++) put_u32(&b->classes, WOB_NONE);
|
||||||
|
for (uint32_t j = 0; j < field_cnt; j++) put_u32(&b->classes, 0);
|
||||||
return b->class_cnt++;
|
return b->class_cnt++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue