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
76dacf6985
commit
065ac99224
23 changed files with 1031 additions and 88 deletions
|
|
@ -253,6 +253,13 @@ and expr_kind =
|
|||
literal without lookahead nothing else needs. *)
|
||||
| ListLit of expr list
|
||||
| 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,
|
||||
like `switch`. `body` is an expression (the workload's only form);
|
||||
`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
|
||||
if u32 img 0 <> magic then raise (Bad "bad magic");
|
||||
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 koff = u32 img 16 and kcnt = u32 img 20 in
|
||||
let ioff = u32 img 24 and icnt = u32 img 28 in
|
||||
|
|
@ -204,10 +204,20 @@ let dump (img : string) : string =
|
|||
o := !o + 12;
|
||||
let kinds = List.init fcnt (fun j -> kind_name (u8 img (!o + j))) in
|
||||
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
|
||||
(Printf.sprintf "c%-3d %s flags=%s fields=[%s]" i (kname nm)
|
||||
(if flags land 1 <> 0 then "gc" else "-")
|
||||
(String.concat ", " kinds))
|
||||
(String.concat ", " fields))
|
||||
done;
|
||||
(* interfaces + vtable rows *)
|
||||
line "== INTERFACES ==";
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ let kind_label (k : Token.kind) : string =
|
|||
| Token.KwTry -> "KW_TRY"
|
||||
| Token.KwCatch -> "KW_CATCH"
|
||||
| Token.KwNil -> "KW_NIL"
|
||||
| Token.KwAs -> "KW_AS"
|
||||
| Token.LBrace -> "LBRACE"
|
||||
| Token.RBrace -> "RBRACE"
|
||||
| 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.MapLit -> "{}"
|
||||
| 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
|
||||
catch arm's statements. *)
|
||||
| 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_version = 1
|
||||
let wob_version = 2 (* v2: per-field class-table metadata *)
|
||||
let wob_hdr_size = 44
|
||||
let wob_none = 0xFFFFFFFF
|
||||
let k_int = 0
|
||||
|
|
@ -256,6 +256,11 @@ let b_map_key_at = 37
|
|||
let b_map_val_at = 38
|
||||
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_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)
|
||||
|
|
@ -947,6 +952,7 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option
|
|||
| ListLit [] | MapLit -> None
|
||||
(* haxe-parity Task 6: contextual on its destination (see owner.ml). *)
|
||||
| NilLit -> None
|
||||
| As (_, ty) -> Some (Nullable ty)
|
||||
(* haxe-parity Task 5: a `try` yields its try arm's type — types.ml has
|
||||
already required the catch arm to agree. *)
|
||||
| Try t -> ty_of_expr p f t.body
|
||||
|
|
@ -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
|
||||
somewhere its type is declared — otherwise WO-E403 at the creation
|
||||
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 =
|
||||
match expected with
|
||||
| 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;
|
||||
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
|
||||
(* `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 -> (
|
||||
match lookup_local f n with
|
||||
| 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
|
||||
result is a record takes that record's class id as its last
|
||||
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 ->
|
||||
err p ~code:stdlib_not_linked_code ~file:f.f_file ~pos:e.pos
|
||||
~message:
|
||||
(Printf.sprintf "stdlib module `%s` has no member `%s`" alias mname);
|
||||
~message:(Printf.sprintf "stdlib module `%s` has no member `%s`" alias mname);
|
||||
put f (ins_abx op_loadk dst (const_int p 0))
|
||||
| Some sm ->
|
||||
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
|
||||
indexes stable and readable in a disassembly *)
|
||||
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 method_name_k = Array.map (fun m -> const_text p m.mr_name) p_methods in
|
||||
(* ---- 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
|
||||
for _ = 1 to pad do
|
||||
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;
|
||||
let ifs = Buf.create () in
|
||||
Array.iteri
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ let keyword_kind = function
|
|||
| "try" -> Some Token.KwTry
|
||||
| "catch" -> Some Token.KwCatch
|
||||
| "nil" -> Some Token.KwNil
|
||||
| "as" -> Some Token.KwAs
|
||||
| "INSERT" -> Some Token.KwInsert
|
||||
| "SELECT" -> Some Token.KwSelect
|
||||
| _ -> 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
|
||||
destination, and never something this frame owns. *)
|
||||
| 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
|
||||
already required the catch arm to agree). *)
|
||||
| Try t -> expr_ty ctx t.body
|
||||
|
|
@ -1069,6 +1072,7 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit =
|
|||
literal-specific one. *)
|
||||
| ListLit items -> List.iter (read_expr ctx) items
|
||||
| MapLit | NilLit -> ()
|
||||
| As (inner, _) -> read_expr ctx inner
|
||||
| Try t -> analyze_try ctx e t.body t.ename t.handler
|
||||
| DbStub _ ->
|
||||
(* trap-capable: the frame needs its drop map here *)
|
||||
|
|
|
|||
|
|
@ -850,6 +850,17 @@ and parse_multiplicative (st : state) : Ast.expr =
|
|||
done;
|
||||
!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 =
|
||||
match peek st with
|
||||
| Token.Dash ->
|
||||
|
|
@ -858,7 +869,7 @@ and parse_unary (st : state) : Ast.expr =
|
|||
ignore (advance st);
|
||||
let operand = parse_unary st in
|
||||
{ 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 =
|
||||
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.ListLit items -> { e with Ast.kind = Ast.ListLit (List.map (subst_expr consts bound) items) }
|
||||
| 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 } ->
|
||||
{ e with
|
||||
Ast.kind =
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ type kind =
|
|||
identifier — `nil` appears in the corpus and the driving workload
|
||||
only ever as this literal. *)
|
||||
| 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
|
||||
real keyword (grepped the corpus/sample first, same discipline as
|
||||
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
|
||||
|
||||
(* 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
|
||||
trap error, one shape forever (spec §6). Predeclared rather than
|
||||
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" "close" 1 55 None None;
|
||||
(* 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 =
|
||||
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
|
||||
class owned. Found by the emitter, this function's first
|
||||
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 StringMap.mem name syms.unions then
|
||||
(* 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
|
||||
says which `?T` it is the absent value of. *)
|
||||
| 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
|
||||
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. *)
|
||||
|
|
@ -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
|
||||
whatever `?T` it meets. *)
|
||||
| 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 } ->
|
||||
let body_res = typecheck_expr env cenv body in
|
||||
(* 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
|
||||
| ListLit items -> List.iter (walk_expr bound visit) items
|
||||
| MapLit | NilLit -> ()
|
||||
| As (inner, _) -> walk_expr bound visit inner
|
||||
| Try { body; ename; handler } ->
|
||||
walk_expr bound visit body;
|
||||
walk_block (StringSet.add ename bound) visit handler
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Cache"
|
||||
k1 TEXT "Holder"
|
||||
k2 TEXT "read"
|
||||
k3 TEXT "proven"
|
||||
k4 TEXT "main"
|
||||
k5 INT 41
|
||||
k6 INT 1
|
||||
k2 TEXT "hits"
|
||||
k3 TEXT "cache"
|
||||
k4 TEXT "read"
|
||||
k5 TEXT "proven"
|
||||
k6 TEXT "main"
|
||||
k7 INT 41
|
||||
k8 INT 1
|
||||
== CLASSES ==
|
||||
c0 Cache flags=gc fields=[SCALAR]
|
||||
c1 Holder flags=- fields=[GCREF]
|
||||
c0 Cache flags=gc fields=[hits:SCALAR]
|
||||
c1 Holder flags=- fields=[cache:GCREF]
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
|
|
@ -31,7 +33,7 @@ m2 main args=0 regs=6 [free fn] [ENTRY]
|
|||
drops: pc 14 owned={} gc={r0}
|
||||
drops: pc 15 owned={} gc={}
|
||||
0000 NEW r0, c0
|
||||
0001 LOADK r1, k5
|
||||
0001 LOADK r1, k7
|
||||
0002 SETF r0, f0, r1
|
||||
0003 NEW r1, c1
|
||||
0004 MOVE r2, r0
|
||||
|
|
@ -40,7 +42,7 @@ m2 main args=0 regs=6 [free fn] [ENTRY]
|
|||
0007 MOVE r4, r1
|
||||
0008 CALL r4, m1
|
||||
0009 MOVE r3, r4
|
||||
0010 LOADK r5, k6
|
||||
0010 LOADK r5, k8
|
||||
0011 ADD r2, r3, r5
|
||||
0012 BUILTIN r2, r2, print_int
|
||||
0013 DROP r1
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Book"
|
||||
k1 TEXT "Toy"
|
||||
k2 TEXT "Priced"
|
||||
k3 TEXT "current_price"
|
||||
k4 TEXT "quote"
|
||||
k5 TEXT "main"
|
||||
k6 INT 2
|
||||
k7 INT 3
|
||||
k8 INT 10
|
||||
k9 INT 5
|
||||
k2 TEXT "base"
|
||||
k3 TEXT "Priced"
|
||||
k4 TEXT "current_price"
|
||||
k5 TEXT "quote"
|
||||
k6 TEXT "main"
|
||||
k7 INT 2
|
||||
k8 INT 3
|
||||
k9 INT 10
|
||||
k10 INT 5
|
||||
== CLASSES ==
|
||||
c0 Book flags=- fields=[SCALAR]
|
||||
c1 Toy flags=- fields=[SCALAR]
|
||||
c0 Book flags=- fields=[base:SCALAR]
|
||||
c1 Toy flags=- fields=[base:SCALAR]
|
||||
== INTERFACES ==
|
||||
i0 Priced methods=1 slots=s0..s0
|
||||
== VTABLES ==
|
||||
|
|
@ -22,14 +23,14 @@ m0 current_price args=1 regs=3 [class c0]
|
|||
lines: 0->15
|
||||
drops: (none)
|
||||
0000 GETF r1, r0, f0
|
||||
0001 LOADK r2, k6
|
||||
0001 LOADK r2, k7
|
||||
0002 ADD r1, r1, r2
|
||||
0003 RET r1
|
||||
m1 current_price args=1 regs=3 [class c1]
|
||||
lines: 0->23
|
||||
drops: (none)
|
||||
0000 GETF r1, r0, f0
|
||||
0001 LOADK r2, k7
|
||||
0001 LOADK r2, k8
|
||||
0002 MUL r1, r1, r2
|
||||
0003 RET r1
|
||||
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 20 owned={} gc={}
|
||||
0000 NEW r0, c0
|
||||
0001 LOADK r1, k8
|
||||
0001 LOADK r1, k9
|
||||
0002 SETF r0, f0, r1
|
||||
0003 NEW r1, c1
|
||||
0004 LOADK r2, k9
|
||||
0004 LOADK r2, k10
|
||||
0005 SETF r1, f0, r2
|
||||
0006 MOVE r3, r0
|
||||
0007 CALL r3, m2
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Item"
|
||||
k1 TEXT "consume"
|
||||
k2 TEXT "twice"
|
||||
k3 TEXT "main"
|
||||
k4 INT 2
|
||||
k5 INT 3
|
||||
k6 INT 5
|
||||
k7 INT 0
|
||||
k1 TEXT "n"
|
||||
k2 TEXT "consume"
|
||||
k3 TEXT "twice"
|
||||
k4 TEXT "main"
|
||||
k5 INT 2
|
||||
k6 INT 3
|
||||
k7 INT 5
|
||||
k8 INT 0
|
||||
== CLASSES ==
|
||||
c0 Item flags=- fields=[SCALAR]
|
||||
c0 Item flags=- fields=[n:SCALAR]
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
|
|
@ -31,12 +32,12 @@ m1 twice args=2 regs=6 [free fn]
|
|||
drops: pc 14 owned={r2} gc={}
|
||||
drops: pc 19 owned={} gc={}
|
||||
0000 NEW r2, c0
|
||||
0001 LOADK r3, k4
|
||||
0001 LOADK r3, k5
|
||||
0002 SETF r2, f0, r3
|
||||
0003 MOVE r3, r1
|
||||
0004 JZ r3, -> 0013
|
||||
0005 NEW r3, c0
|
||||
0006 LOADK r4, k5
|
||||
0006 LOADK r4, k6
|
||||
0007 SETF r3, f0, r4
|
||||
0008 GETF r4, r3, f0
|
||||
0009 DROP r3
|
||||
|
|
@ -54,9 +55,9 @@ m2 main args=0 regs=4 [free fn] [ENTRY]
|
|||
lines: 0->24 7->23
|
||||
drops: (none)
|
||||
0000 NEW r1, c0
|
||||
0001 LOADK r3, k6
|
||||
0001 LOADK r3, k7
|
||||
0002 SETF r1, f0, r3
|
||||
0003 LOADK r2, k7
|
||||
0003 LOADK r2, k8
|
||||
0004 CALL r1, m1
|
||||
0005 MOVE r0, r1
|
||||
0006 BUILTIN r0, r0, print_int
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Item"
|
||||
k1 TEXT "Bag"
|
||||
k2 TEXT "touch"
|
||||
k3 TEXT "pair"
|
||||
k4 TEXT "fixed"
|
||||
k5 TEXT "touch3"
|
||||
k6 TEXT "triple"
|
||||
k7 TEXT "write_through"
|
||||
k8 TEXT "main"
|
||||
k9 INT 0
|
||||
k10 INT 1
|
||||
k11 INT 5
|
||||
k12 INT 2
|
||||
k13 INT 3
|
||||
k2 TEXT "n"
|
||||
k3 TEXT "items"
|
||||
k4 TEXT "touch"
|
||||
k5 TEXT "pair"
|
||||
k6 TEXT "fixed"
|
||||
k7 TEXT "touch3"
|
||||
k8 TEXT "triple"
|
||||
k9 TEXT "write_through"
|
||||
k10 TEXT "main"
|
||||
k11 INT 0
|
||||
k12 INT 1
|
||||
k13 INT 5
|
||||
k14 INT 2
|
||||
k15 INT 3
|
||||
== CLASSES ==
|
||||
c0 Item flags=- fields=[SCALAR]
|
||||
c1 Bag flags=- fields=[MULTI]
|
||||
c0 Item flags=- fields=[n:SCALAR]
|
||||
c1 Bag flags=- fields=[items:MULTI]
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
|
|
@ -48,10 +50,10 @@ m2 fixed args=1 regs=5 [free fn]
|
|||
lines: 0->27
|
||||
drops: (none)
|
||||
0000 GETF r3, r0, f0
|
||||
0001 LOADK r4, k9
|
||||
0001 LOADK r4, k11
|
||||
0002 BUILTIN r1, r3, multi_get
|
||||
0003 GETF r3, r0, f0
|
||||
0004 LOADK r4, k10
|
||||
0004 LOADK r4, k12
|
||||
0005 BUILTIN r2, r3, multi_get
|
||||
0006 CALL r1, m0
|
||||
0007 RET r1
|
||||
|
|
@ -97,7 +99,7 @@ m5 write_through args=3 regs=7 [free fn]
|
|||
0003 GETF r5, r0, f0
|
||||
0004 MOVE r6, r2
|
||||
0005 BUILTIN r4, r5, multi_get
|
||||
0006 LOADK r5, k11
|
||||
0006 LOADK r5, k13
|
||||
0007 BORROW_X r4
|
||||
0008 BORROW_S r3
|
||||
0009 SETF r4, f0, r5
|
||||
|
|
@ -114,22 +116,22 @@ m6 main args=0 regs=6 [free fn] [ENTRY]
|
|||
0002 SETF r0, f0, r1
|
||||
0003 GETF r1, r0, f0
|
||||
0004 NEW r2, c0
|
||||
0005 LOADK r3, k10
|
||||
0005 LOADK r3, k12
|
||||
0006 SETF r2, f0, r3
|
||||
0007 BUILTIN r1, r1, multi_push
|
||||
0008 GETF r1, r0, f0
|
||||
0009 NEW r2, c0
|
||||
0010 LOADK r3, k12
|
||||
0010 LOADK r3, k14
|
||||
0011 SETF r2, f0, r3
|
||||
0012 BUILTIN r1, r1, multi_push
|
||||
0013 GETF r1, r0, f0
|
||||
0014 NEW r2, c0
|
||||
0015 LOADK r3, k13
|
||||
0015 LOADK r3, k15
|
||||
0016 SETF r2, f0, r3
|
||||
0017 BUILTIN r1, r1, multi_push
|
||||
0018 MOVE r2, r0
|
||||
0019 LOADK r3, k9
|
||||
0020 LOADK r4, k10
|
||||
0019 LOADK r3, k11
|
||||
0020 LOADK r4, k12
|
||||
0021 CALL r2, m1
|
||||
0022 MOVE r1, r2
|
||||
0023 BUILTIN r1, r1, print_int
|
||||
|
|
@ -138,15 +140,15 @@ m6 main args=0 regs=6 [free fn] [ENTRY]
|
|||
0026 MOVE r1, r2
|
||||
0027 BUILTIN r1, r1, print_int
|
||||
0028 MOVE r2, r0
|
||||
0029 LOADK r3, k9
|
||||
0030 LOADK r4, k10
|
||||
0031 LOADK r5, k12
|
||||
0029 LOADK r3, k11
|
||||
0030 LOADK r4, k12
|
||||
0031 LOADK r5, k14
|
||||
0032 CALL r2, m4
|
||||
0033 MOVE r1, r2
|
||||
0034 BUILTIN r1, r1, print_int
|
||||
0035 MOVE r2, r0
|
||||
0036 LOADK r3, k9
|
||||
0037 LOADK r4, k10
|
||||
0036 LOADK r3, k11
|
||||
0037 LOADK r4, k12
|
||||
0038 CALL r2, m5
|
||||
0039 MOVE r1, r2
|
||||
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 none = 0xFFFFFFFF in
|
||||
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 koff = u32 16 and kcnt = u32 20 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)
|
||||
done;
|
||||
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)
|
||||
done;
|
||||
(* interfaces + vtable rows *)
|
||||
|
|
@ -3294,7 +3306,7 @@ let () =
|
|||
(count_substring ~needle:"Status.Pending flags" dump = 1
|
||||
&& count_substring ~needle:"Status.Failed flags" dump = 1);
|
||||
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"
|
||||
(count_substring ~needle:"Kind" dump
|
||||
- count_substring ~needle:"Kind" (String.concat "" [ "" ])
|
||||
|
|
@ -3302,7 +3314,7 @@ let () =
|
|||
&& count_substring ~needle:"Kind flags" 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"
|
||||
(count_substring ~needle:"Holder flags=- fields=[SCALAR, OWNED]" dump = 1)
|
||||
(count_substring ~needle:"Holder flags=- fields=[k:SCALAR, st:OWNED]" dump = 1)
|
||||
|
||||
let () =
|
||||
(* 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
|
||||
> [`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.
|
||||
|
||||
**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).
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -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) |
|
||||
| 30 | DB_STUB | trap T_DB "engine not linked" (spec: SQL-layer statements in milestone 1) |
|
||||
| 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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
| `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 |
|
||||
| `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
|
||||
given, so one source name covers the `multi` and `map` ids the runtime
|
||||
keeps apart.
|
||||
|
||||
**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
|
||||
`multi_push` and `multi_get` and no element store — so `m[i] = v` on a
|
||||
`multi` is `WO-E403`.
|
||||
`set(m, k, v)` for a `map` and `multi_set(m, i, v)` for a `multi` (the
|
||||
element it replaces is the container's, so the VM drops it).
|
||||
|
||||
**Shadowing.** A user-declared free `fn` of the same name always wins. A
|
||||
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
|
||||
for `@gc`: `?SomeGcClass` is a `GCREF` field like any other, so it
|
||||
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) {
|
||||
wo_rt *rt = &vm->rt;
|
||||
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);
|
||||
switch (C) {
|
||||
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. */
|
||||
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 */
|
||||
|
|
|
|||
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_NET_ACCEPT] = 1, [WO_B_NET_READ] = 2, [WO_B_NET_WRITE] = 2,
|
||||
[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) {
|
||||
|
|
@ -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));
|
||||
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++) {
|
||||
uint32_t name, flags, 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;
|
||||
if (pad > k.len - k.off) BAIL("class %u: truncated pad", (unsigned)i);
|
||||
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].flags = flags;
|
||||
m->classes[i].field_cnt = fcnt;
|
||||
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;
|
||||
meta_pool += meta_words ? meta_words : 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].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) ---- */
|
||||
cur_t s = {buf, len, ioff};
|
||||
|
|
@ -485,6 +520,7 @@ void wo_module_free(wo_module *m) {
|
|||
free(m->consts);
|
||||
free(m->classes);
|
||||
free(m->kindpool);
|
||||
free(m->metapool);
|
||||
free(m->vtabs);
|
||||
for (uint32_t i = 0; i < m->method_cnt; i++) {
|
||||
free(m->methods[i].code);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ typedef struct wo_module {
|
|||
wo_classdesc *classes;
|
||||
uint32_t class_cnt;
|
||||
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 */
|
||||
wo_vtabent *vtabs;
|
||||
uint32_t vtab_cnt;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||
#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_OFF_MAGIC 0u
|
||||
#define WOB_OFF_VERSION 4u
|
||||
|
|
@ -23,6 +23,24 @@
|
|||
#define WOB_OFF_ENTRY 40u
|
||||
#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 ---- */
|
||||
#define WOB_K_INT 0u /* tag byte, then i64 */
|
||||
#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_CLOSE = 55, /* (fd) -> 0 */
|
||||
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 */
|
||||
#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 field_cnt;
|
||||
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;
|
||||
#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(&b->classes, kinds, field_cnt);
|
||||
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++;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue