feat: iterations 19 + 17 — Float/Bytes scalars (.wob v5), library kind + internal/
- Float full stack: literals (fraction/exponent; `0..10` still a range), f64
opcodes 34-41, @table column, WAL bit-exact replay, json fractions in and
shortest-round-trip out. IEEE-quiet — FDIV never traps where DIV does.
- Bytes: a wo_str with its own class id, so alloc/free/copy are shared but no
Text builtin accepts one; len/at/slice/eq/concat, base64 both ways, json
boundary as base64; TEXT_COPY preserves the kind.
- No implicit Int/Float mixing (WO-E201 in the typechecker, not the emitter,
which picks the opcode from one side and would misread the other).
- One IEEE deviation: float_cmp total order (NaN last, -0.0 == +0.0) for
indexes and order-by, keys canonicalized to match. `?Float` nil is a
reserved quiet NaN — the zero word is +0.0, WO_NIL_SCALAR's bits are -2.0.
- Renderer prefers fixed over exponential in 1e-6..1e21: pure shortest makes
a price of 900.0 read `9e+02`. One renderer for interp/json/float_to_text.
- Fixed en route: lexer double-counted the leading digit; is_scalar_shaped
took Float/Bytes as Int-shaped; Bytes ownership needed a shared heap-scalar
predicate or temps never dropped; order-by bit-compared negatives backwards.
- Iteration 17: `kind = "library"` (absent = program; bad value = WO-E109),
entry-less check mode retiring the `--emit` workaround, Go's `internal/` as
WO-E108 at the consumer's `use`. Driver-only; VM/.wob/GC untouched.
- Framework reorg: internal/{parse,serve}.wo; http/form.wo split out to keep
media_type/form_values public (parse.wo had grown public surface).
- Docs: link audit (97 -> 88 broken, conflict markers resolved, 2 duplicate
stories removed), 00-code-review verified 26/27, iterations re-sequenced.
- Also carries the pre-staged pub(read)/using/#if work from the index.
- Gates: corpus 103/0, test_wal 156/0, web-app 26/0, oop-accept ALL MET.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5521d21a84
commit
d24c705860
77 changed files with 3352 additions and 423 deletions
|
|
@ -269,6 +269,13 @@ let merge_symbols (syms_list : Woc_lib.Types.symbols list) : Woc_lib.Types.symbo
|
|||
|
||||
let duplicate_symbol_code = Woc_lib.Diag.types_prefix ^ "14" (* WO-E214 *)
|
||||
|
||||
(* iteration 17: WO-E108 — a consumer `use` reached into a dependency's
|
||||
`internal/`. A use-resolution diagnostic, so it lives in the E1xx band with
|
||||
the other path/import errors and rides the normal collector path (exit 1),
|
||||
unlike the manifest errors WO-E106/E107/E109 which print directly and
|
||||
exit 2. *)
|
||||
let dep_internal_code = Woc_lib.Diag.parsing_prefix ^ "08"
|
||||
|
||||
let report_collision (collector : Woc_lib.Diag.Collector.t) ~(kind : string) ~(name : string)
|
||||
~(file : string) ~(pos : Woc_lib.Ast.pos) ~(first_file : string)
|
||||
~(first_pos : Woc_lib.Ast.pos) : unit =
|
||||
|
|
@ -503,7 +510,48 @@ let compile_image ?(deps : (string * string) list = []) path =
|
|||
deps
|
||||
in
|
||||
match owner with
|
||||
| None -> (f, prog)
|
||||
| None ->
|
||||
(* iteration 17: the dependency-privacy boundary. A CONSUMER file
|
||||
may not `use` a dep module whose path contains the segment
|
||||
`internal` — Go's rule, and a pure use-resolution check, which
|
||||
is why it needs no keyword and no syntax. Consumer-only by
|
||||
design (spec §3): the dep's own `use internal` is prefixed by
|
||||
the Some arm below and stays legal, so a library can organize
|
||||
its interior freely.
|
||||
|
||||
Matched on a whole SEGMENT, never a substring: a dep module
|
||||
named `internals` or `my_internal_thing` is ordinary public
|
||||
surface. The root project's own `internal/` directories never
|
||||
fire this either — the first segment has to name a DEP.
|
||||
|
||||
The use is left in place rather than dropped: the collector's
|
||||
has-error path already stops emission, and dropping it would
|
||||
turn one clear diagnostic into a cascade of
|
||||
unknown-type errors from the same file. *)
|
||||
List.iter
|
||||
(fun d ->
|
||||
match d with
|
||||
| Woc_lib.Ast.Use u -> (
|
||||
match u.Woc_lib.Ast.segments with
|
||||
| seg :: rest
|
||||
when List.exists (fun (dname, _) -> dname = seg) deps
|
||||
&& List.exists (fun s -> s = "internal") rest ->
|
||||
let pos = u.Woc_lib.Ast.pos in
|
||||
Woc_lib.Diag.Collector.add collector
|
||||
(Woc_lib.Diag.error ~code:dep_internal_code ~file:f
|
||||
~line:pos.Woc_lib.Ast.line ~col:pos.Woc_lib.Ast.col
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"`%s` is internal to the dependency `%s` — a module under \
|
||||
`internal/` is the library's own business and cannot be \
|
||||
imported across the `[deps]` boundary"
|
||||
(String.concat "/" u.Woc_lib.Ast.segments)
|
||||
seg)
|
||||
())
|
||||
| _ -> ())
|
||||
| _ -> ())
|
||||
prog.Woc_lib.Ast.decls;
|
||||
(f, prog)
|
||||
| Some (dname, _) ->
|
||||
let redecl = function
|
||||
| Woc_lib.Ast.Use u ->
|
||||
|
|
@ -518,6 +566,12 @@ let compile_image ?(deps : (string * string) list = []) path =
|
|||
parsed
|
||||
in
|
||||
let syms, module_syms = typecheck_all collector ~root:path ~deps parsed in
|
||||
(* haxe-parity Task 7: typecheck recorded every `using` extension call;
|
||||
rewrite them into plain free-fn calls (receiver first) so the owner
|
||||
and emit passes below need no using-awareness at all *)
|
||||
let parsed =
|
||||
List.map (fun (f, prog) -> (f, Woc_lib.Types.apply_using_rewrites ~file:f prog)) parsed
|
||||
in
|
||||
let units =
|
||||
List.map
|
||||
(fun (f, prog) ->
|
||||
|
|
@ -629,54 +683,11 @@ let default_runtime_path () : string =
|
|||
if Sys.file_exists sibling && not (Sys.is_directory sibling) then sibling
|
||||
else "runtime/wovm"
|
||||
|
||||
let build_mode ?(deps : (string * string) list = []) ~(runtime : string option)
|
||||
(path : string) (out : string) : unit =
|
||||
let collector, lookup, image = compile_image ~deps path in
|
||||
if Woc_lib.Diag.Collector.has_error collector then finish collector lookup
|
||||
else begin
|
||||
if String.get_int32_le image wob_off_entry = -1l then begin
|
||||
Printf.eprintf
|
||||
"woc: %s: no `main` entry point found; `build` requires a zero-argument free fn named \
|
||||
`main`\n"
|
||||
path;
|
||||
exit 2
|
||||
end;
|
||||
let rt_path = match runtime with Some p -> p | None -> default_runtime_path () in
|
||||
if (not (Sys.file_exists rt_path)) || Sys.is_directory rt_path then begin
|
||||
Printf.eprintf "woc: runtime binary not found at '%s' -- build it with: make -C runtime wovm\n"
|
||||
rt_path;
|
||||
exit 2
|
||||
end;
|
||||
let rt_bytes =
|
||||
match read_source rt_path with
|
||||
| Ok s -> strip_existing_trailer s
|
||||
| Error msg ->
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2
|
||||
in
|
||||
let tmp = out ^ ".woc-build.tmp" in
|
||||
(* stale tmp from an interrupted earlier build must not survive: its
|
||||
permission bits would leak through, since Open_creat on an
|
||||
existing inode does not apply the requested mode *)
|
||||
(try Sys.remove tmp with Sys_error _ -> ());
|
||||
(try
|
||||
let oc = open_out_gen [ Open_wronly; Open_creat; Open_trunc; Open_binary ] 0o755 tmp in
|
||||
output_string oc rt_bytes;
|
||||
output_string oc image;
|
||||
output_bytes oc
|
||||
(trailer_bytes ~payload_off:(String.length rt_bytes) ~payload_len:(String.length image));
|
||||
close_out oc
|
||||
with Sys_error msg ->
|
||||
(try Sys.remove tmp with Sys_error _ -> ());
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2);
|
||||
(try Sys.rename tmp out
|
||||
with Sys_error msg ->
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2);
|
||||
finish collector lookup
|
||||
end
|
||||
|
||||
(* RELOCATED (iteration 17): manifest_parse used to sit below build_mode.
|
||||
build_mode now reads the manifest itself, to tell "you forgot `main`"
|
||||
from "this is a library" in the no-entry error, and OCaml has no
|
||||
forward reference across top-level `let`s. Nothing in the block
|
||||
changed; only its position did. *)
|
||||
(* ---- manifest build ---------------------------------------------------
|
||||
`woc <dir>` where <dir>/wo.toml exists is a BUILD, not a check: the
|
||||
manifest names the application, so pointing woc at the project is enough
|
||||
|
|
@ -807,7 +818,11 @@ let manifest_parse (path : string) : (string * string) list =
|
|||
let v = String.sub v 1 (String.length v - 2) in
|
||||
let known =
|
||||
match (!section, key) with
|
||||
| "", ("name" | "version" | "description") -> true
|
||||
(* iteration 17: `kind` says whether this project is a program or
|
||||
a library. Explicit, not inferred from the presence of `main` —
|
||||
a forgotten entry and a deliberate library must not look the
|
||||
same in an error message. Validated in manifest_build. *)
|
||||
| "", ("name" | "version" | "description" | "kind") -> true
|
||||
| "runtime", "wo" -> true (* minimum toolchain version; enforced in manifest_build *)
|
||||
| "build", ("runtime" | "target") -> true
|
||||
| _ -> false
|
||||
|
|
@ -815,7 +830,8 @@ let manifest_parse (path : string) : (string * string) list =
|
|||
if not known then
|
||||
fail !lineno
|
||||
(if !section = "" then
|
||||
Printf.sprintf "unknown key `%s` (name, version, description exist)" key
|
||||
Printf.sprintf "unknown key `%s` (name, version, description, kind exist)"
|
||||
key
|
||||
else
|
||||
Printf.sprintf "unknown key `%s` in [%s]" key !section);
|
||||
kvs := ((if !section = "" then key else !section ^ "." ^ key), v) :: !kvs
|
||||
|
|
@ -823,6 +839,69 @@ let manifest_parse (path : string) : (string * string) list =
|
|||
with End_of_file -> close_in ic);
|
||||
!kvs
|
||||
|
||||
let build_mode ?(deps : (string * string) list = []) ~(runtime : string option)
|
||||
(path : string) (out : string) : unit =
|
||||
let collector, lookup, image = compile_image ~deps path in
|
||||
if Woc_lib.Diag.Collector.has_error collector then finish collector lookup
|
||||
else begin
|
||||
if String.get_int32_le image wob_off_entry = -1l then begin
|
||||
Printf.eprintf
|
||||
"woc: %s: no `main` entry point found; `build` requires a zero-argument free fn named \
|
||||
`main`\n"
|
||||
path;
|
||||
(* iteration 17: if the project DECLARES itself a library, say so — the
|
||||
two failures are different problems and deserve different answers.
|
||||
Read only when the manifest exists; a malformed one still fails the
|
||||
way it does today, through manifest_parse's own path. *)
|
||||
let mf = Filename.concat path "wo.toml" in
|
||||
if Sys.file_exists mf then begin
|
||||
match List.assoc_opt "kind" (manifest_parse mf) with
|
||||
| Some "library" ->
|
||||
Printf.eprintf
|
||||
"woc: %s: this project declares `kind = \"library\"` — add a `main` for a demo \
|
||||
binary, or check it with `woc %s`\n"
|
||||
mf path
|
||||
| _ -> ()
|
||||
end;
|
||||
exit 2
|
||||
end;
|
||||
let rt_path = match runtime with Some p -> p | None -> default_runtime_path () in
|
||||
if (not (Sys.file_exists rt_path)) || Sys.is_directory rt_path then begin
|
||||
Printf.eprintf "woc: runtime binary not found at '%s' -- build it with: make -C runtime wovm\n"
|
||||
rt_path;
|
||||
exit 2
|
||||
end;
|
||||
let rt_bytes =
|
||||
match read_source rt_path with
|
||||
| Ok s -> strip_existing_trailer s
|
||||
| Error msg ->
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2
|
||||
in
|
||||
let tmp = out ^ ".woc-build.tmp" in
|
||||
(* stale tmp from an interrupted earlier build must not survive: its
|
||||
permission bits would leak through, since Open_creat on an
|
||||
existing inode does not apply the requested mode *)
|
||||
(try Sys.remove tmp with Sys_error _ -> ());
|
||||
(try
|
||||
let oc = open_out_gen [ Open_wronly; Open_creat; Open_trunc; Open_binary ] 0o755 tmp in
|
||||
output_string oc rt_bytes;
|
||||
output_string oc image;
|
||||
output_bytes oc
|
||||
(trailer_bytes ~payload_off:(String.length rt_bytes) ~payload_len:(String.length image));
|
||||
close_out oc
|
||||
with Sys_error msg ->
|
||||
(try Sys.remove tmp with Sys_error _ -> ());
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2);
|
||||
(try Sys.rename tmp out
|
||||
with Sys_error msg ->
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2);
|
||||
finish collector lookup
|
||||
end
|
||||
|
||||
|
||||
(* [runtime] wo = ">= X.Y" — a minimum-toolchain-version constraint. Only `>=`
|
||||
and a bare version are interpreted; any other operator is accepted untouched
|
||||
(forward-compatible — don't hard-fail on a constraint syntax not grokked
|
||||
|
|
@ -1036,6 +1115,32 @@ let manifest_build ?(update_deps = false) (dir : string) : unit =
|
|||
Printf.eprintf "woc: %s: `name` is required\n" mf;
|
||||
exit 2
|
||||
in
|
||||
(* iteration 17: `kind` decides what `woc <dir>` MEANS for this project.
|
||||
A library is checked whole with no entry required; a program builds, as
|
||||
it always has. Absent is "program", so every existing manifest behaves
|
||||
byte-identically. An unrecognized value is a manifest error rather than
|
||||
a silent default — a typo'd kind would otherwise build the wrong thing,
|
||||
the same reasoning manifest_parse's unknown-key rejection follows. *)
|
||||
(match get "kind" with
|
||||
| None | Some "program" -> ()
|
||||
| Some "library" ->
|
||||
(* Check mode. `[deps]` are resolved and the `[runtime]` constraint
|
||||
enforced exactly as a build does — a library must be checkable
|
||||
offline once locked, or "it checks here" means nothing. The full
|
||||
pipeline runs (parse, typecheck, interface satisfaction, ownership,
|
||||
GC inference) because compile_image runs it; the in-memory image is
|
||||
simply discarded, so no target/ appears and no file is written. An
|
||||
entry-less image is already legal on that path — the `--emit`
|
||||
precedent. *)
|
||||
let collector, lookup, _image = compile_image ~deps dir in
|
||||
if Woc_lib.Diag.Collector.has_error collector then finish collector lookup;
|
||||
exit 0
|
||||
| Some other ->
|
||||
Printf.eprintf
|
||||
"woc: %s: error WO-E109: unknown `kind` value `%s` — expected \"program\" (the default) \
|
||||
or \"library\"\n"
|
||||
mf other;
|
||||
exit 2);
|
||||
(* paths in the manifest are the PROJECT's, so they resolve against the
|
||||
manifest's directory — `woc .` inside the project and `woc path/to/it`
|
||||
from anywhere must build the same thing *)
|
||||
|
|
@ -1050,7 +1155,22 @@ let manifest_build ?(update_deps = false) (dir : string) : unit =
|
|||
build_mode ~deps ~runtime dir (Filename.concat target name)
|
||||
|
||||
let () =
|
||||
match Sys.argv with
|
||||
(* haxe-parity Task 8: `-D name` defines a build flag (`#if name` keeps
|
||||
its section). Accepted anywhere on the command line in every mode, so
|
||||
it is peeled off BEFORE the fixed-shape mode match below. *)
|
||||
let rec peel_defines acc = function
|
||||
| "-D" :: name :: rest when name <> "" && name.[0] <> '-' ->
|
||||
Woc_lib.Lexer.defines :=
|
||||
Woc_lib.Lexer.StringSet.add name !Woc_lib.Lexer.defines;
|
||||
peel_defines acc rest
|
||||
| "-D" :: _ ->
|
||||
Printf.eprintf "woc: -D needs a flag name (woc -D portable ...)\n";
|
||||
exit 2
|
||||
| a :: rest -> peel_defines (a :: acc) rest
|
||||
| [] -> List.rev acc
|
||||
in
|
||||
let argv = Array.of_list (peel_defines [] (Array.to_list Sys.argv)) in
|
||||
match argv with
|
||||
| [| _; "--dump-tokens"; path |] -> dump_tokens path
|
||||
| [| _; "--dump-ast"; path |] -> dump_ast path
|
||||
| [| _; "--dump-owner"; path |] -> dump_owner path
|
||||
|
|
|
|||
|
|
@ -147,6 +147,19 @@ crash): emit_return's place test now sees through `Interp` the same way,
|
|||
while bare Ident/Field/Index behavior there is unchanged. Both flavors
|
||||
pinned by `tests/corpus/run/interp-borrowed-field`.
|
||||
|
||||
The iteration-5 strictness closeout (2026-08-20) added three seams worth
|
||||
knowing: `pub(read)` rides the field annotation list as a synthetic
|
||||
"pub_read" marker and is enforced in the Assign case that already resolves
|
||||
the target's class (WO-E219, `current_self` names the checking class —
|
||||
class-owned writes, sibling instances included); `using` extensions are a
|
||||
TYPECHECK-TIME rewrite — `types.ml` records (file, call-id) → fn name in
|
||||
`using_rewrites` and `apply_using_rewrites` rewrites `recv.ext(a)` to
|
||||
`ext(recv, a)` before owner/emit, which therefore carry zero
|
||||
using-awareness (collision with a real method is WO-E220 — never a silent
|
||||
win either way); `#if` is a token-stream filter at the end of
|
||||
`Lexer.tokenize` (`Lexer.defines` filled by `woc -D`, WO-E003 for misuse)
|
||||
— the parser never sees a directive.
|
||||
|
||||
Two rules the measurements imposed, both easy to get backwards:
|
||||
|
||||
- **Never drop an argument register after a `CALL`.** The callee's frame
|
||||
|
|
@ -171,3 +184,79 @@ Two rules the measurements imposed, both easy to get backwards:
|
|||
- `./compiler/_build/default/bin/woc --emit docs/examples/log-watcher -o /tmp/lw.wob`
|
||||
— the acceptance workload. It must compile with zero diagnostics, and
|
||||
`runtime/wovm /tmp/lw.wob watch <file> 2 1` must tail a live file and alert.
|
||||
|
||||
## Float and Bytes (iteration 19)
|
||||
|
||||
- **A digit run is an Int unless a fraction or an exponent follows.** The
|
||||
lexer requires a DIGIT after `.` before committing to a Float, which is what
|
||||
keeps `0..10` a range rather than `Float 0.` followed by `.10`, and checks
|
||||
the exponent form (`e`, optional sign, at least one digit) before consuming
|
||||
anything, so `2eggs` is still `Int 2` then an ident. `c` in that branch is
|
||||
PEEKED, not consumed — the scan loop reads it, and adding it to the buffer
|
||||
first double-counts the leading digit (a real bug this went through).
|
||||
- **The no-mixing rule lives in the typechecker, not the emitter.** The
|
||||
emitter picks the arithmetic opcode from whether EITHER side is a Float, so
|
||||
an unreported `1 + 2.5` would lower to integer ADD over f64 bits and produce
|
||||
a plausible wrong number with no diagnostic. `check_numeric_mix` reports the
|
||||
mix (WO-E201) off confident types only, keeping this file's stay-silent-when-
|
||||
underivable contract; `%` on a Float is rejected outright.
|
||||
- **`Float`/`Bytes` are builtin scalars but not Int-shaped.**
|
||||
`is_scalar_shaped` excludes both by name alongside `Text`, or
|
||||
`print_int(price)` prints f64 bits as a huge integer and `trunc(digest)`
|
||||
reinterprets a pointer — the representation mismatch that predicate exists
|
||||
for.
|
||||
- **Bytes is a heap-owned scalar, so every ownership rule that named `Text` by
|
||||
string had to name a predicate instead.** `Types.is_heap_scalar` is that
|
||||
predicate (owner.ml's four sites) and `is_heap_kind` is its emitter twin
|
||||
(kind 3 or 7, six sites). Miss one and a Bytes temp never drops, or a Bytes
|
||||
stored into a container aliases where a Text would copy.
|
||||
- **`?Float` needs its own nil constant.** `nil_const_for` picks it, and the
|
||||
bit pattern is emitted as a FLOAT pool constant because it is far outside
|
||||
OCaml's 63-bit native int — `const_int` cannot express it at all. Float
|
||||
constants dedupe on BITS, since `0.0` and `-0.0` are `=`-equal in OCaml but
|
||||
must stay distinct, and NaN is not `=`-equal to itself.
|
||||
- **A Float `order by` key uses `float_cmp`, not `op_lt`.** Raw-bit ordering
|
||||
puts negatives backwards (the sign bit makes `-1.0` compare greater than
|
||||
`1.0` as an integer) and leaves NaN wherever the comparison sequence drops
|
||||
it. `float-table-column` in the corpus pins the ascending order that a
|
||||
bit compare gets wrong.
|
||||
- **Three parallel builtin tables must agree**: `Types.builtin_signatures`
|
||||
(arity + arg kinds), `Types.builtin_confident_ret` and its emitter twin
|
||||
`builtin_ret` (a missing entry for a fresh-heap result is a LEAK, not just a
|
||||
lost type), and `is_builtin_name` plus the id mapping. The loader's arity
|
||||
table and the OCaml twin in `compiler/test/runner.ml` are a fourth and fifth.
|
||||
|
||||
## Library kind and the `internal/` boundary (iteration 17)
|
||||
|
||||
Every part of this lives in the driver (`compiler/bin/main.ml`). No lexer,
|
||||
parser, typechecker, VM, `.wob`, or GC change — `internal` is a path shape, not
|
||||
a keyword, and visibility is name resolution at compile time.
|
||||
|
||||
- **`kind` is declared, not inferred.** `wo.toml`'s top-level `kind` is
|
||||
`"program"` (the default, so every existing manifest is byte-identical) or
|
||||
`"library"`; anything else is WO-E109 at exit 2. Go infers library-ness from
|
||||
the absence of `main`, which makes "you forgot the entry" and "this is a
|
||||
library" the same error — the whole reason to spend a manifest key here.
|
||||
- **Check mode reuses `compile_image` whole.** The library branch resolves
|
||||
`[deps]`, enforces the `[runtime]` constraint, runs the full pipeline, and
|
||||
discards the in-memory image; no `target/` is created and no file is written.
|
||||
An entry-less image was already legal on that path (the `--emit` precedent),
|
||||
so "checks clean" means what "builds clean" means.
|
||||
- **`manifest_parse` was RELOCATED above `build_mode`** so the no-entry error
|
||||
can read the manifest and say "this project declares itself a library"
|
||||
instead of only "no `main`". OCaml has no forward reference across top-level
|
||||
`let`s; types.ml solved the same problem the same way. `woc build <dir> -o
|
||||
<out>` never goes through `manifest_build`, so reading it inside `build_mode`
|
||||
is the only placement that covers the explicit-build path.
|
||||
- **WO-E108 is consumer-only, and keys on the FIRST segment naming a dep.**
|
||||
That single condition is what makes the root project's own `internal/`
|
||||
directories immune, and the dep-owned branch (which prefixes `use internal`
|
||||
to `<dep>/internal`) is untouched, so a library imports its own interior
|
||||
freely. The match is on a whole path SEGMENT — a module named `internals` is
|
||||
ordinary public surface.
|
||||
- **The offending `use` is left in the AST, not dropped.** The collector's
|
||||
has-error path already stops emission; removing the use would replace one
|
||||
clear diagnostic with a cascade of unknown-type errors from the same file.
|
||||
- **Exit-code bands stay split**: WO-E108 is a diagnostic through the normal
|
||||
collector path (exit 1); WO-E106/E107/E109 are manifest errors printed
|
||||
directly (exit 2).
|
||||
|
|
|
|||
|
|
@ -201,6 +201,10 @@ type expr = {
|
|||
select)". *)
|
||||
and expr_kind =
|
||||
| IntLit of int
|
||||
(* iteration 19: a Float literal, carried as OCaml's own f64. Separate from
|
||||
IntLit all the way down — there is no implicit coercion anywhere, so the
|
||||
typechecker must be able to tell `1` from `1.0` at every use site. *)
|
||||
| FloatLit of float
|
||||
| StrLit of string
|
||||
| BoolLit of bool
|
||||
(* haxe-parity Task 6: `nil`, the absent value of a `?T`. One
|
||||
|
|
@ -553,6 +557,11 @@ type use_decl = {
|
|||
id : int;
|
||||
pos : pos;
|
||||
segments : string list;
|
||||
(* haxe-parity Task 7: `using shared/textutil` — a use PLUS extension
|
||||
registration: the module's pub free fns whose first parameter matches
|
||||
a receiver's type become callable as methods on it. Compile-time only
|
||||
(types.ml resolves and rewrites); false for a plain `use`. *)
|
||||
is_using : bool;
|
||||
}
|
||||
|
||||
(* haxe-parity Task 4: one variant of a union declaration
|
||||
|
|
|
|||
|
|
@ -149,6 +149,16 @@ let ins_str (i : int) (pc : int) : string =
|
|||
jumps are — absolute, so a disassembly can be read against the pc column. *)
|
||||
| 32 -> Printf.sprintf "TRY r%d, handler -> %04d" a target
|
||||
| 33 -> "ENDTRY"
|
||||
(* iteration 19: the f64 world. Rendered with the same three-register shape
|
||||
as their Int counterparts so a disassembly reads the same. *)
|
||||
| 34 -> Printf.sprintf "FADD r%d, r%d, r%d" a b c
|
||||
| 35 -> Printf.sprintf "FSUB r%d, r%d, r%d" a b c
|
||||
| 36 -> Printf.sprintf "FMUL r%d, r%d, r%d" a b c
|
||||
| 37 -> Printf.sprintf "FDIV r%d, r%d, r%d" a b c
|
||||
| 38 -> Printf.sprintf "FNEG r%d, r%d" a b
|
||||
| 39 -> Printf.sprintf "FEQ r%d, r%d, r%d" a b c
|
||||
| 40 -> Printf.sprintf "FLT r%d, r%d, r%d" a b c
|
||||
| 41 -> Printf.sprintf "FLE r%d, r%d, r%d" a b c
|
||||
| op -> Printf.sprintf "?OP%d" op
|
||||
|
||||
(* ---- the dump ---- *)
|
||||
|
|
@ -156,13 +166,17 @@ let ins_str (i : int) (pc : int) : string =
|
|||
type kconst =
|
||||
| KInt of int64
|
||||
| KText of string
|
||||
| KFloat of float (* iteration 19 *)
|
||||
|
||||
let dump (img : string) : string =
|
||||
let out = Buffer.create 4096 in
|
||||
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 <> 4 then raise (Bad (Printf.sprintf "unsupported version %d" ver));
|
||||
(* iteration 19 bumped the format to v5 (Float constant tag, kinds 6/7,
|
||||
opcodes 34-41). The disassembler tracks the emitter, not a range: an old
|
||||
image is a different format and reading it as this one would misrender. *)
|
||||
if ver <> 5 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
|
||||
|
|
@ -186,17 +200,29 @@ let dump (img : string) : string =
|
|||
consts.(i) <- KText (String.sub img !o n);
|
||||
o := !o + n
|
||||
end
|
||||
else if tag = 2 then begin
|
||||
(* iteration 19: a Float constant. Rendered as OCaml's hex-float so the
|
||||
disassembly names the exact bits — a decimal here would make golden
|
||||
files depend on printf rounding. *)
|
||||
consts.(i) <- KFloat (Int64.float_of_bits (i64 img !o));
|
||||
o := !o + 8
|
||||
end
|
||||
else raise (Bad (Printf.sprintf "constant %d: unknown tag %d" i tag))
|
||||
done;
|
||||
let kname i =
|
||||
if i >= ccnt then Printf.sprintf "<k%d?>" i
|
||||
else match consts.(i) with KText s -> s | KInt n -> Int64.to_string n
|
||||
else
|
||||
match consts.(i) with
|
||||
| KText s -> s
|
||||
| KInt n -> Int64.to_string n
|
||||
| KFloat x -> Printf.sprintf "%h" x
|
||||
in
|
||||
line "== CONSTANTS ==";
|
||||
for i = 0 to ccnt - 1 do
|
||||
match consts.(i) with
|
||||
| KInt n -> line (Printf.sprintf "k%-3d INT %Ld" i n)
|
||||
| KText s -> line (Printf.sprintf "k%-3d TEXT %s" i (quote s))
|
||||
| KFloat x -> line (Printf.sprintf "k%-3d FLT %h" i x) (* iteration 19 *)
|
||||
done;
|
||||
(* classes *)
|
||||
line "== CLASSES ==";
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ let kind_label (k : Token.kind) : string =
|
|||
match k with
|
||||
| Token.Ident s -> Printf.sprintf "IDENT(%s)" s
|
||||
| Token.Int n -> Printf.sprintf "INT(%d)" n
|
||||
(* iteration 19: hex-float, so the golden file records the exact bits and
|
||||
does not depend on decimal formatting *)
|
||||
| Token.Float x -> Printf.sprintf "FLOAT(%h)" x
|
||||
| Token.Str s -> Printf.sprintf "STR(%s)" s
|
||||
| Token.InterpStr segs ->
|
||||
let part_str = function
|
||||
|
|
@ -52,6 +55,7 @@ let kind_label (k : Token.kind) : string =
|
|||
| Token.KwSelect -> "KW_SELECT"
|
||||
| Token.KwUse -> "KW_USE"
|
||||
| Token.KwSpawn -> "KW_SPAWN"
|
||||
| Token.KwUsing -> "KW_USING"
|
||||
| Token.KwPub -> "KW_PUB"
|
||||
| Token.KwBreak -> "KW_BREAK"
|
||||
| Token.KwContinue -> "KW_CONTINUE"
|
||||
|
|
@ -99,6 +103,9 @@ let kind_label (k : Token.kind) : string =
|
|||
| Token.PlusEq -> "PLUSEQ"
|
||||
| Token.MinusEq -> "MINUSEQ"
|
||||
| Token.Newline -> "NEWLINE"
|
||||
| Token.HashIf -> "#if"
|
||||
| Token.HashElse -> "#else"
|
||||
| Token.HashEnd -> "#end"
|
||||
| Token.Eof -> "EOF"
|
||||
|
||||
(* Multi-file dump layout (Task 8, bin/main.ml). Every dump_* function
|
||||
|
|
@ -212,6 +219,10 @@ let dbstub_tokens_str (toks : Token.t list) : string =
|
|||
let rec expr_str (e : Ast.expr) : string =
|
||||
match e.Ast.kind with
|
||||
| Ast.IntLit n -> string_of_int n
|
||||
(* iteration 19: `%h` is OCaml's hex-float — exact, short, and unambiguous
|
||||
in a golden file. A decimal rendering here would make the golden test
|
||||
depend on printf rounding, which is not what these fixtures check. *)
|
||||
| Ast.FloatLit f -> Printf.sprintf "%h" f
|
||||
| Ast.StrLit s -> "\"" ^ s ^ "\""
|
||||
| Ast.BoolLit b -> if b then "true" else "false"
|
||||
| Ast.Ident s -> s
|
||||
|
|
|
|||
|
|
@ -151,11 +151,20 @@ let stdlib_not_linked_code = Diag.emitter_prefix ^ "06"
|
|||
============================================================ *)
|
||||
|
||||
let wob_magic = 0x31424F57 (* "WOB1" read as an LE u32 *)
|
||||
let wob_version = 4 (* v4 (iteration 7b): RC opcodes retired; gc mask = GC roots *)
|
||||
|
||||
(* v5 (iteration 19): the Float constant tag, field kinds 6/7, opcodes 34-41,
|
||||
builtins 70-83. v4 (iteration 7b): RC opcodes retired; gc mask = GC roots *)
|
||||
let wob_version = 5
|
||||
|
||||
let wob_hdr_size = 44
|
||||
let wob_none = 0xFFFFFFFF
|
||||
let k_int = 0
|
||||
let k_text = 1
|
||||
|
||||
(* iteration 19: tag byte then the f64's IEEE bits as an LE u64. OCaml's
|
||||
`float` IS an f64, so Int64.bits_of_float is a bit reinterpretation, not a
|
||||
conversion — a literal reaches the VM exactly as written. *)
|
||||
let k_float = 2
|
||||
let max_regs = 64
|
||||
let classf_gc = 0x01
|
||||
|
||||
|
|
@ -167,6 +176,16 @@ let op_sub = 4
|
|||
let op_mul = 5
|
||||
let op_div = 6
|
||||
let op_neg = 7
|
||||
|
||||
(* iteration 19: the f64 world. Separate opcodes, not a mode bit — see wob.h. *)
|
||||
let op_fadd = 34
|
||||
let op_fsub = 35
|
||||
let op_fmul = 36
|
||||
let op_fdiv = 37
|
||||
let op_fneg = 38
|
||||
let op_feq = 39
|
||||
let op_flt = 40
|
||||
let op_fle = 41
|
||||
let op_concat = 8
|
||||
let op_eq = 9
|
||||
let op_lt = 10
|
||||
|
|
@ -241,6 +260,22 @@ let b_trim = 24
|
|||
let b_to_lower = 25
|
||||
let b_char_of = 26
|
||||
let b_parse_int = 27
|
||||
|
||||
(* iteration 19: Float bridges then Bytes (wob.h ids 70-83) *)
|
||||
let b_float_of_int = 70
|
||||
let b_trunc = 71
|
||||
let b_parse_float = 72
|
||||
let b_float_to_text = 73
|
||||
let b_float_cmp = 74
|
||||
let b_bytes_len = 75
|
||||
let b_bytes_at = 76
|
||||
let b_bytes_slice = 77
|
||||
let b_bytes_eq = 78
|
||||
let b_bytes_concat = 79
|
||||
let b_base64_encode = 80
|
||||
let b_base64_decode = 81
|
||||
let b_bytes_of_text = 82
|
||||
let b_text_of_bytes = 83
|
||||
let b_split = 28
|
||||
let b_split_ws = 29
|
||||
let b_join = 30
|
||||
|
|
@ -432,7 +467,12 @@ type pctx = {
|
|||
(* constant pool, deduplicated *)
|
||||
p_kints : (int, int) Hashtbl.t;
|
||||
p_ktexts : (string, int) Hashtbl.t;
|
||||
mutable p_consts : [ `Int of int | `Text of string ] list; (* rev *)
|
||||
(* iteration 19: Float constants dedupe on BITS, not on value. Two reasons,
|
||||
both load-bearing: 0.0 and -0.0 are `=`-equal in OCaml but must stay
|
||||
distinct constants, and NaN is not `=`-equal to itself, so a value-keyed
|
||||
table would grow one entry per NaN literal forever. *)
|
||||
p_kfloats : (int64, int) Hashtbl.t;
|
||||
mutable p_consts : [ `Int of int | `Text of string | `Float of int64 ] list; (* rev *)
|
||||
mutable p_nconsts : int;
|
||||
}
|
||||
|
||||
|
|
@ -446,6 +486,18 @@ let const_int (p : pctx) (v : int) : int =
|
|||
p.p_nconsts <- i + 1;
|
||||
i
|
||||
|
||||
(* iteration 19 *)
|
||||
let const_float (p : pctx) (v : float) : int =
|
||||
let bits = Int64.bits_of_float v in
|
||||
match Hashtbl.find_opt p.p_kfloats bits with
|
||||
| Some i -> i
|
||||
| None ->
|
||||
let i = p.p_nconsts in
|
||||
Hashtbl.replace p.p_kfloats bits i;
|
||||
p.p_consts <- `Float bits :: p.p_consts;
|
||||
p.p_nconsts <- i + 1;
|
||||
i
|
||||
|
||||
let const_text (p : pctx) (s : string) : int =
|
||||
match Hashtbl.find_opt p.p_ktexts s with
|
||||
| Some i -> i
|
||||
|
|
@ -762,6 +814,8 @@ let kind_byte : Types.wob_kind -> int = function
|
|||
| Types.WO_K_TEXT -> 3
|
||||
| Types.WO_K_MULTI -> 4
|
||||
| Types.WO_K_MAP -> 5
|
||||
| Types.WO_K_FLOAT -> 6 (* iteration 19 *)
|
||||
| Types.WO_K_BYTES -> 7
|
||||
(* `?T` has no kind byte of its own in the v1 format (kinds run 0..5;
|
||||
the loader rejects 6). It needs none: a nullable field stores what
|
||||
T stores and spells nil as 0, and every drop plan in
|
||||
|
|
@ -773,6 +827,16 @@ let kind_byte : Types.wob_kind -> int = function
|
|||
let field_kind (p : pctx) (ft : Ast.field_ty) : int =
|
||||
kind_byte (Types.wob_kind_of_typ p.p_syms (Types.typ_of_field_ty (unwrap ft)))
|
||||
|
||||
(* iteration 19: "is this a str-shaped heap value the holder owns?" — kind 3
|
||||
(Text/json.Value) or kind 7 (Bytes). Every copy-on-boundary and drop-the-
|
||||
fresh-temp site asks this; before Bytes existed the six sites each spelled
|
||||
`= 3` inline, and leaving them that way would have meant a Bytes temp never
|
||||
dropped and a Bytes stored into a container aliased instead of copied.
|
||||
WO_B_TEXT_COPY preserves the kind, so one predicate covers both. *)
|
||||
let is_heap_kind (p : pctx) (ft : Ast.field_ty) : bool =
|
||||
let k = field_kind p ft in
|
||||
k = 3 || k = 7
|
||||
|
||||
let class_of_name (p : pctx) (n : string) : int option = SM.find_opt n p.p_class_id
|
||||
|
||||
(* iteration 9b: a @table class's instances are row ids, so field access on
|
||||
|
|
@ -943,6 +1007,16 @@ let builtin_ret (name : string) (argty : Ast.field_ty option) : Ast.field_ty opt
|
|||
match argty with Some t -> ( match unwrap t with Map (k, _) -> Some (Scalar k) | _ -> None) | None -> None)
|
||||
| "val_at" -> (
|
||||
match argty with Some t -> ( match unwrap t with Map (_, v) -> Some (Scalar v) | _ -> None) | None -> None)
|
||||
(* iteration 19 — mirrors Types.builtin_confident_ret. The fresh-heap
|
||||
results (`float_to_text`, `base64_encode`, `text_of_bytes`, the three
|
||||
that return a fresh Bytes) MUST be listed or their `let` never gets a
|
||||
drop: a missing entry here is a leak, per this table's own contract. *)
|
||||
| "float" | "parse_float" -> Some (Scalar "Float")
|
||||
| "trunc" | "float_cmp" | "bytes_len" | "bytes_at" -> Some (Scalar "Int")
|
||||
| "float_to_text" | "base64_encode" | "text_of_bytes" -> Some (Scalar "Text")
|
||||
| "bytes_eq" -> Some (Scalar "Bool")
|
||||
| "bytes_slice" | "bytes_concat" | "bytes_of_text" -> Some (Scalar "Bytes")
|
||||
| "base64_decode" -> Some (Nullable (Scalar "Bytes"))
|
||||
| _ -> None
|
||||
|
||||
let is_builtin_name (n : string) =
|
||||
|
|
@ -954,7 +1028,11 @@ let is_builtin_name (n : string) =
|
|||
"substr"; "trim"; "to_lower"; "char_of"; "parse_int"; "split"; "split_ws"; "join"; "slice";
|
||||
"pop"; "shift"; "sort"; "reverse"; "remove"; "key_at"; "val_at";
|
||||
(* the concurrency arc *)
|
||||
"send" ]
|
||||
"send";
|
||||
(* iteration 19: Float bridges and Bytes surface *)
|
||||
"float"; "trunc"; "parse_float"; "float_to_text"; "float_cmp"; "bytes_len"; "bytes_at";
|
||||
"bytes_slice"; "bytes_eq"; "bytes_concat"; "base64_encode"; "base64_decode";
|
||||
"bytes_of_text"; "text_of_bytes" ]
|
||||
|
||||
(* ---- unions and variants (haxe-parity Task 4) ------------------------
|
||||
|
||||
|
|
@ -1005,6 +1083,7 @@ let query_elem_scalar (p : pctx) (q : Ast.query) ~(src : string) : string =
|
|||
let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option =
|
||||
match e.kind with
|
||||
| IntLit _ -> Some (Scalar "Int")
|
||||
| FloatLit _ -> Some (Scalar "Float") (* iteration 19 *)
|
||||
| StrLit _ -> Some (Scalar "Text")
|
||||
| BoolLit _ -> Some (Scalar "Bool")
|
||||
(* Same rule as owner.ml's expr_ty: a non-empty list literal knows its
|
||||
|
|
@ -1209,6 +1288,26 @@ and emit_binding_ty_of_arm (p : pctx) (f : fstate) (subject : Ast.expr) (arm : A
|
|||
let is_text (p : pctx) (f : fstate) (e : Ast.expr) : bool =
|
||||
match ty_of_expr p f e with Some t -> ( match unwrap t with Scalar "Text" -> true | _ -> false) | None -> false
|
||||
|
||||
(* iteration 19: does this expression hold f64 bits? Every operator that has
|
||||
both an Int and a Float lowering asks this to pick the opcode. A literal is
|
||||
answered directly because `1.5 + x` has a FloatLit on the left whose
|
||||
`ty_of_expr` may not resolve, and picking integer ADD there would compute
|
||||
garbage silently — the whole failure mode WO-E2xx's no-mixing rule exists to
|
||||
prevent. The typechecker has already rejected genuinely mixed operands, so
|
||||
one Float side is enough to select the Float opcode. *)
|
||||
let is_float (p : pctx) (f : fstate) (e : Ast.expr) : bool =
|
||||
match e.Ast.kind with
|
||||
| Ast.FloatLit _ -> true
|
||||
| _ -> (
|
||||
match ty_of_expr p f e with
|
||||
| Some t -> ( match unwrap t with Scalar "Float" -> true | _ -> false)
|
||||
| None -> false)
|
||||
|
||||
let is_bytes (p : pctx) (f : fstate) (e : Ast.expr) : bool =
|
||||
match ty_of_expr p f e with
|
||||
| Some t -> ( match unwrap t with Scalar "Bytes" -> true | _ -> false)
|
||||
| None -> false
|
||||
|
||||
(* ============================================================
|
||||
Lowering
|
||||
============================================================ *)
|
||||
|
|
@ -1387,6 +1486,16 @@ let wob_field_nil_bool = 0xFFFFFFFB (* a `?Bool` field: NIL_SCALAR nil + bool en
|
|||
WO_NIL_SCALAR exactly. *)
|
||||
let nil_scalar_word = -4611686018427387904
|
||||
|
||||
let wob_field_nil_float = 0xFFFFFFFA (* a `?Float` field: WO_NIL_FLOAT nil *)
|
||||
|
||||
(* iteration 19: nil for a `?Float`. It cannot be the zero word (+0.0) and it
|
||||
cannot be nil_scalar_word (whose bits ARE -2.0), so it is a reserved quiet
|
||||
NaN — see runtime/src/wob.h's WO_NIL_FLOAT for why that costs nothing real.
|
||||
Carried as an Int64 and emitted as a FLOAT constant, because the bit pattern
|
||||
is far outside OCaml's 63-bit native int and could not be written as one. *)
|
||||
let nil_float_bits = 0x7FF8000000000EE1L
|
||||
let nil_float_value = Int64.float_of_bits nil_float_bits
|
||||
|
||||
(* is this a `?scalar` — an optional whose representation is a plain register,
|
||||
so its nil has to be the sentinel rather than the zero word? *)
|
||||
let is_nullable_scalar (p : pctx) (ty : Ast.field_ty) : bool =
|
||||
|
|
@ -1394,9 +1503,28 @@ let is_nullable_scalar (p : pctx) (ty : Ast.field_ty) : bool =
|
|||
| Ast.Nullable inner -> field_kind p inner = 0 (* WO_K_SCALAR *)
|
||||
| _ -> false
|
||||
|
||||
(* iteration 19: a `?Float` is word-shaped like a `?scalar` but takes its own
|
||||
sentinel, so it needs its own predicate rather than widening the one above
|
||||
(whose callers all pair it with nil_scalar_word). *)
|
||||
let is_nullable_float (p : pctx) (ty : Ast.field_ty) : bool =
|
||||
match ty with
|
||||
| Ast.Nullable inner -> field_kind p inner = 6 (* WO_K_FLOAT *)
|
||||
| _ -> false
|
||||
|
||||
(* The constant index of the right nil for a destination type: a `?Float`'s
|
||||
reserved NaN, a `?scalar`'s sentinel, or the zero word for everything else
|
||||
(heap-shaped optionals, where a null pointer is unambiguous). One place, so
|
||||
the four sites that write a nil cannot drift apart. *)
|
||||
let nil_const_for (p : pctx) (dest : Ast.field_ty option) : int =
|
||||
match dest with
|
||||
| Some t when is_nullable_float p t -> const_float p nil_float_value
|
||||
| Some t when is_nullable_scalar p t -> const_int p nil_scalar_word
|
||||
| _ -> const_int p 0
|
||||
|
||||
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
|
||||
if is_nullable_scalar p ty then
|
||||
if is_nullable_float p ty then wob_field_nil_float (* iteration 19 *)
|
||||
else if is_nullable_scalar p ty then
|
||||
(match ty with
|
||||
| Ast.Nullable (Ast.Scalar "Bool") -> wob_field_nil_bool
|
||||
| _ -> wob_field_nil_scalar)
|
||||
|
|
@ -1510,14 +1638,14 @@ let copy_place_text (p : pctx) (f : fstate) (reg : int) (e : Ast.expr) : unit =
|
|||
fresh int_to_text that must not be re-copied *)
|
||||
let is_place = is_borrowed_value_t p f e && not (is_container_read e) in
|
||||
let is_text =
|
||||
match ty_of_expr p f e with Some t -> field_kind p t = 3 (* WO_K_TEXT *) | None -> false
|
||||
match ty_of_expr p f e with Some t -> is_heap_kind p t | None -> false
|
||||
in
|
||||
if is_place && is_text then put f (ins_abc op_builtin reg reg b_text_copy)
|
||||
|
||||
let drop_fresh_text ?keep (p : pctx) (f : fstate) (reg : int) (e : Ast.expr) : unit =
|
||||
let is_place = is_borrowed_value_t p f e && not (is_container_read e) in
|
||||
let is_text =
|
||||
match ty_of_expr p f e with Some t -> field_kind p t = 3 (* WO_K_TEXT *) | None -> false
|
||||
match ty_of_expr p f e with Some t -> is_heap_kind p t | None -> false
|
||||
in
|
||||
if (match keep with Some k -> k <> reg | None -> true) && (not is_place) && is_text then
|
||||
put f (ins_abc op_drop reg 0 0)
|
||||
|
|
@ -1530,6 +1658,10 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
|||
f.f_cur_line <- e.pos.line;
|
||||
(match e.kind with
|
||||
| IntLit n -> put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_int p n)))
|
||||
| FloatLit x ->
|
||||
(* iteration 19: the literal's bits go into the pool and LOADK copies the
|
||||
word. No decimal round-trip anywhere between source and register. *)
|
||||
put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_float p x)))
|
||||
| BoolLit b -> put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_int p (if b then 1 else 0))))
|
||||
| StrLit s -> put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_text p s)))
|
||||
(* haxe-parity Task 6: `nil` is the zero word for a heap-shaped `?T` and the
|
||||
|
|
@ -1539,10 +1671,7 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
|||
is the safe answer (a heap slot). *)
|
||||
| NilLit ->
|
||||
let dest = match expected with Some _ -> expected | None -> f.f_ret in
|
||||
let word =
|
||||
match dest with Some t when is_nullable_scalar p t -> nil_scalar_word | _ -> 0
|
||||
in
|
||||
put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (const_int p word)))
|
||||
put f (ins_abx op_loadk dst (check_bx p f e.pos "constant" (nil_const_for p dest)))
|
||||
(* Container literals lower to exactly what `multi_new()`/`map_new()`
|
||||
lower to — the element kinds are the destination's, never guessed
|
||||
(docs/plan/oop-vm/08-builtin-surface.md) — plus one `multi_push` per
|
||||
|
|
@ -1739,11 +1868,14 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
|||
drop_fresh_text ~keep:dst p f (w + 1) idx;
|
||||
(* a Text read out of a container is COPIED: the container keeps owning
|
||||
its element, the reader owns the copy (see owner.ml's copies_out) *)
|
||||
if (match ty_of_expr p f e with Some t -> field_kind p t = 3 | None -> false) then
|
||||
if (match ty_of_expr p f e with Some t -> is_heap_kind p t | None -> false) then
|
||||
put f (ins_abc op_builtin dst dst b_text_copy))
|
||||
| Unary (Neg, o) ->
|
||||
let b = emit_operand p f v o in
|
||||
put f (ins_abc op_neg dst b 0)
|
||||
(* iteration 19: FNEG flips the sign bit, so `-0.0` is reachable and
|
||||
`-x` on an infinity gives the other infinity. Integer NEG on f64 bits
|
||||
would produce a different number entirely. *)
|
||||
put f (ins_abc (if is_float p f o then op_fneg else op_neg) dst b 0)
|
||||
| Binary (op, l, r) -> emit_binary p f v ~dst op l r
|
||||
| Ctor (cn, fields) -> emit_ctor p f v ~dst e cn fields
|
||||
| Spawn (cn, fields) ->
|
||||
|
|
@ -1815,12 +1947,17 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
|||
match unwrap t with
|
||||
| Scalar "Text" -> emit_expr p f v ~dst inner
|
||||
| Scalar "Int" -> emit_builtin p f v ~dst e "int_to_text" [ inner ]
|
||||
(* iteration 19: `"total: ${price}"` is the first thing anyone writes
|
||||
after adding a Float column, so it renders here rather than forcing
|
||||
an explicit float_to_text at every call site. Same renderer as
|
||||
json.encode, so the two never disagree. *)
|
||||
| Scalar "Float" -> emit_builtin p f v ~dst e "float_to_text" [ inner ]
|
||||
| other ->
|
||||
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"cannot interpolate a value of type `%s` in \"${...}\" — only Text and Int are \
|
||||
supported"
|
||||
"cannot interpolate a value of type `%s` in \"${...}\" — only Text, Int, and \
|
||||
Float are supported"
|
||||
(Dump.field_ty_str other));
|
||||
put f (ins_abx op_loadk dst (const_int p 0)))
|
||||
| None ->
|
||||
|
|
@ -1911,11 +2048,16 @@ and emit_binary (p : pctx) (f : fstate) (v : views) ~(dst : int) (op : Ast.binop
|
|||
f.f_temp <- save
|
||||
in
|
||||
let nil_compare o = nil_compare_into p f v ~dst o l r in
|
||||
(* iteration 19: one Float operand selects the Float opcode. The typechecker
|
||||
has already rejected a genuine Int/Float mix (WO-E201), so reaching here
|
||||
with only one Float side means the other is an underivable expression of
|
||||
the same type — never an Int to be silently reinterpreted. *)
|
||||
let fl = is_float p f l || is_float p f r in
|
||||
match op with
|
||||
| Add -> simple op_add
|
||||
| Sub -> simple op_sub
|
||||
| Mul -> simple op_mul
|
||||
| Div -> simple op_div
|
||||
| Add -> simple (if fl then op_fadd else op_add)
|
||||
| Sub -> simple (if fl then op_fsub else op_sub)
|
||||
| Mul -> simple (if fl then op_fmul else op_mul)
|
||||
| Div -> simple (if fl then op_fdiv else op_div)
|
||||
| Concat ->
|
||||
(* CONCAT allocates a new Text and leaves its operands untouched, so an
|
||||
operand that was itself freshly built — the partial result of a longer
|
||||
|
|
@ -1929,10 +2071,10 @@ and emit_binary (p : pctx) (f : fstate) (v : views) ~(dst : int) (op : Ast.binop
|
|||
put f (ins_abc op_concat dst a b);
|
||||
drop_fresh_text ~keep:dst p f a l;
|
||||
drop_fresh_text ~keep:dst p f b r
|
||||
| Lt -> simple op_lt
|
||||
| Le -> simple op_le
|
||||
| Gt -> swapped op_lt
|
||||
| Ge -> swapped op_le
|
||||
| Lt -> simple (if fl then op_flt else op_lt)
|
||||
| Le -> simple (if fl then op_fle else op_le)
|
||||
| Gt -> swapped (if fl then op_flt else op_lt)
|
||||
| Ge -> swapped (if fl then op_fle else op_le)
|
||||
(* A comparison against `nil` is a WORD compare, never a content compare:
|
||||
EQS would dereference nil as a `wo_str*` (the VM's str_check traps on
|
||||
that, so an `x != nil` guard would trap instead of answering). The literal
|
||||
|
|
@ -1942,6 +2084,10 @@ and emit_binary (p : pctx) (f : fstate) (v : views) ~(dst : int) (op : Ast.binop
|
|||
| Eq ->
|
||||
if is_nil_lit l || is_nil_lit r then nil_compare op_eq
|
||||
else if is_text p f l || is_text p f r then simple op_eqs
|
||||
(* iteration 19: FEQ, not EQ. A word compare would make `NaN == NaN` true
|
||||
(identical bits) and `0.0 == -0.0` false (differing bits) — both
|
||||
backwards from IEEE, which is the stated contract. *)
|
||||
else if fl then simple op_feq
|
||||
else simple op_eq
|
||||
| Ne ->
|
||||
(* no NE opcode in the v1 set: `a != b` is `(a == b) == 0`. The
|
||||
|
|
@ -1956,7 +2102,12 @@ and emit_binary (p : pctx) (f : fstate) (v : views) ~(dst : int) (op : Ast.binop
|
|||
else begin
|
||||
let a = emit_operand p f v l in
|
||||
let b = emit_operand p f v r in
|
||||
put f (ins_abc (if is_text p f l || is_text p f r then op_eqs else op_eq) t a b);
|
||||
put f
|
||||
(ins_abc
|
||||
(if is_text p f l || is_text p f r then op_eqs
|
||||
else if fl then op_feq (* iteration 19: `a != b` on Floats is IEEE *)
|
||||
else op_eq)
|
||||
t a b);
|
||||
(* the same reap `simple` does — `headers["authorization"] !=
|
||||
"Bearer ${key}"` abandoned both sides, once per MCP request *)
|
||||
drop_fresh_owned ~keep:t p f a l;
|
||||
|
|
@ -2647,7 +2798,15 @@ and emit_query (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr)
|
|||
`x.name` sees x unbound, returns None, and a Text key silently falls
|
||||
to the pointer-comparing op_lt (the wrong-order bug) *)
|
||||
let key_is_text =
|
||||
match ty_of_expr p f key with Some t -> field_kind p t = 3 | None -> false
|
||||
match ty_of_expr p f key with Some t -> field_kind p t = 3 | None -> false (* Text keys only: order-by on Bytes is out of scope *)
|
||||
in
|
||||
(* iteration 19: a Float order-by key uses the TOTAL order (float_cmp),
|
||||
not op_lt over the raw bits. Bits get negatives backwards (the sign
|
||||
bit makes -1.0 compare greater than 1.0 as an integer) and leave NaN
|
||||
wherever the comparison sequence happens to drop it; an order-by must
|
||||
be a total order or the result depends on input order. *)
|
||||
let key_is_float =
|
||||
match ty_of_expr p f key with Some t -> field_kind p t = 6 | None -> false
|
||||
in
|
||||
let kj = alloc_temp p f e.pos in
|
||||
emit_expr p f v ~dst:kj key;
|
||||
|
|
@ -2666,6 +2825,18 @@ and emit_query (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr)
|
|||
put f (ins_abc op_builtin cmp w b_str_lt);
|
||||
f.f_temp <- save
|
||||
end
|
||||
else if key_is_float then begin
|
||||
(* cmp = float_cmp(a, b) < 0 *)
|
||||
let save = f.f_temp in
|
||||
let w = alloc_temps p f e.pos 2 in
|
||||
put f (ins_abc op_move w a 0);
|
||||
put f (ins_abc op_move (w + 1) b 0);
|
||||
put f (ins_abc op_builtin cmp w b_float_cmp);
|
||||
let z = alloc_temp p f e.pos in
|
||||
put f (ins_abx op_loadk z (check_bx p f e.pos "constant" (const_int p 0)));
|
||||
put f (ins_abc op_lt cmp cmp z);
|
||||
f.f_temp <- save
|
||||
end
|
||||
else put f (ins_abc op_lt cmp a b)
|
||||
in
|
||||
if desc then lt kb kj else lt kj kb;
|
||||
|
|
@ -2783,7 +2954,8 @@ and emit_insert (p : pctx) (f : fstate) (v : views) ~(dst : int) (e : Ast.expr)
|
|||
f.f_temp <- save
|
||||
| None ->
|
||||
let nil_word =
|
||||
if is_nullable_scalar p fty then const_int p nil_scalar_word
|
||||
if is_nullable_float p fty then const_float p nil_float_value
|
||||
else if is_nullable_scalar p fty then const_int p nil_scalar_word
|
||||
else const_int p 0
|
||||
in
|
||||
put f (ins_abx op_loadk (base + 1 + idx) (check_bx p f e.pos "constant" nil_word))))
|
||||
|
|
@ -2827,7 +2999,7 @@ and emit_default_value (p : pctx) (f : fstate) ~(dst : int) ~(fty : Ast.field_ty
|
|||
put f
|
||||
(ins_abx op_loadk dst
|
||||
(check_bx p f pos "constant"
|
||||
(const_int p (if is_nullable_scalar p fty then nil_scalar_word else 0))))
|
||||
(nil_const_for p (Some fty))))
|
||||
(* `= {}` — a fresh empty container of the field's own declared type,
|
||||
the same rule `[]` above follows *)
|
||||
| [ Token.LBrace; Token.RBrace ] -> (
|
||||
|
|
@ -3233,7 +3405,7 @@ and call_window (p : pctx) (f : fstate) (v : views) (e : Ast.expr) ~(recv : Ast.
|
|||
let fresh_borrowed_value (a : Ast.expr) : bool =
|
||||
is_fresh_owned_temp p f a
|
||||
|| ((not (is_borrowed_value_t p f a) || is_container_read a)
|
||||
&& match ty_of_expr p f a with Some t -> field_kind p t = 3 | None -> false)
|
||||
&& match ty_of_expr p f a with Some t -> is_heap_kind p t | None -> false)
|
||||
in
|
||||
let owned_heap_temp (a : Ast.expr) : bool =
|
||||
(match a.kind with
|
||||
|
|
@ -3353,6 +3525,10 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
|| id = b_len || id = b_print_err || id = b_trim || id = b_to_lower || id = b_char_of
|
||||
|| id = b_parse_int || id = b_split_ws || id = b_pop || id = b_shift || id = b_sort
|
||||
|| id = b_reverse
|
||||
(* iteration 19, one argument *)
|
||||
|| id = b_float_of_int || id = b_trunc || id = b_parse_float || id = b_float_to_text
|
||||
|| id = b_bytes_len || id = b_base64_encode || id = b_base64_decode
|
||||
|| id = b_bytes_of_text || id = b_text_of_bytes
|
||||
then 1
|
||||
else if
|
||||
id = b_multi_push || id = b_multi_get || id = b_map_get || id = b_map_has
|
||||
|
|
@ -3361,8 +3537,10 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
|| id = b_byte_at || id = b_starts_with || id = b_ends_with || id = b_index_of
|
||||
|| id = b_last_index_of || id = b_split || id = b_join || id = b_map_remove
|
||||
|| id = b_map_key_at || id = b_map_val_at
|
||||
(* iteration 19, two arguments *)
|
||||
|| id = b_float_cmp || id = b_bytes_at || id = b_bytes_eq || id = b_bytes_concat
|
||||
then 2
|
||||
else 3
|
||||
else 3 (* b_bytes_slice lands here with substr's shape: (value, start, len) *)
|
||||
in
|
||||
let container_id first_arg on_multi on_map =
|
||||
match ty_of_expr p f first_arg with
|
||||
|
|
@ -3461,6 +3639,21 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
| "remove" -> fixed b_map_remove
|
||||
| "key_at" -> fixed b_map_key_at
|
||||
| "val_at" -> fixed b_map_val_at
|
||||
(* iteration 19 *)
|
||||
| "float" -> fixed b_float_of_int
|
||||
| "trunc" -> fixed b_trunc
|
||||
| "parse_float" -> fixed b_parse_float
|
||||
| "float_to_text" -> fixed b_float_to_text
|
||||
| "float_cmp" -> fixed b_float_cmp
|
||||
| "bytes_len" -> fixed b_bytes_len
|
||||
| "bytes_at" -> fixed b_bytes_at
|
||||
| "bytes_slice" -> fixed b_bytes_slice
|
||||
| "bytes_eq" -> fixed b_bytes_eq
|
||||
| "bytes_concat" -> fixed b_bytes_concat
|
||||
| "base64_encode" -> fixed b_base64_encode
|
||||
| "base64_decode" -> fixed b_base64_decode
|
||||
| "bytes_of_text" -> fixed b_bytes_of_text
|
||||
| "text_of_bytes" -> fixed b_text_of_bytes
|
||||
| "multi_new" | "map_new" ->
|
||||
let is_map = name = "map_new" in
|
||||
if args <> [] then bad (Printf.sprintf "builtin `%s` takes no arguments" name)
|
||||
|
|
@ -3490,7 +3683,7 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
| Some id ->
|
||||
fixed id;
|
||||
if (match builtin_ret name (match args with x :: _ -> ty_of_expr p f x | [] -> None) with
|
||||
| Some t -> field_kind p t = 3
|
||||
| Some t -> is_heap_kind p t
|
||||
| None -> false)
|
||||
then put f (ins_abc op_builtin dst dst b_text_copy)
|
||||
| None -> bad "builtin `get` needs a `multi` or a `map` as its first argument")
|
||||
|
|
@ -4381,7 +4574,12 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
|
|||
let col_of n = ref_index_of_name fnames n in
|
||||
let is_indexable (fl : Ast.field) =
|
||||
match Types.wob_kind_of_typ p_syms_for_indexes (Types.typ_of_field_ty (unwrap fl.Ast.ty)) with
|
||||
| Types.WO_K_SCALAR | Types.WO_K_TEXT -> true
|
||||
(* iteration 19: Float is indexable — the engine keys it on
|
||||
the TOTAL order's canonical bits (table.c's
|
||||
idx_float_key), so -0.0 and +0.0 are one key and all
|
||||
NaNs are one key. Bytes stays out: ordering it beyond
|
||||
equality is out of scope. Mirrors loader.c. *)
|
||||
| Types.WO_K_SCALAR | Types.WO_K_TEXT | Types.WO_K_FLOAT -> true
|
||||
| _ -> false
|
||||
in
|
||||
let table_indexes =
|
||||
|
|
@ -4398,7 +4596,7 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
|
|||
if List.mem "unique" fl.Ast.annotations then begin
|
||||
if not (is_indexable fl) then
|
||||
index_col_err := Some (c.Ast.pos, Printf.sprintf
|
||||
"`@unique` on `%s.%s`: only scalar and Text fields can be indexed"
|
||||
"`@unique` on `%s.%s`: only scalar, Text, and Float fields can be indexed"
|
||||
c.Ast.name fl.Ast.name);
|
||||
[ (true, [| col_of fl.Ast.name |]) ]
|
||||
end
|
||||
|
|
@ -4419,7 +4617,7 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
|
|||
| Some fl ->
|
||||
if not (is_indexable fl) then
|
||||
index_col_err := Some (c.Ast.pos, Printf.sprintf
|
||||
"`@table(index: ...)` on `%s`: `%s` is not a scalar or Text field"
|
||||
"`@table(index: ...)` on `%s`: `%s` is not a scalar, Text, or Float field"
|
||||
c.Ast.name cn))
|
||||
cols)
|
||||
cfg.Ast.indexes
|
||||
|
|
@ -4600,6 +4798,7 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
|
|||
p_iface_id = !iface_id; p_method_id = !method_id; p_methods; p_uses;
|
||||
p_module_syms = module_syms; p_module_of = module_of; p_colliding = colliding;
|
||||
p_kints = Hashtbl.create 32;
|
||||
p_kfloats = Hashtbl.create 16; (* iteration 19 *)
|
||||
p_ktexts = Hashtbl.create 32; p_consts = []; p_nconsts = 0 }
|
||||
in
|
||||
(* names are constants; interning them first keeps the pool's low
|
||||
|
|
@ -4672,7 +4871,11 @@ let emit ?(entry_ok : string -> bool = fun _ -> true) ~(syms : Types.symbols)
|
|||
| `Text s ->
|
||||
Buf.u8 consts k_text;
|
||||
Buf.u32 consts (String.length s);
|
||||
Buf.str consts s)
|
||||
Buf.str consts s
|
||||
| `Float bits ->
|
||||
(* iteration 19: the bits, exactly as OCaml holds them *)
|
||||
Buf.u8 consts k_float;
|
||||
Buf.i64 consts bits)
|
||||
(List.rev p.p_consts);
|
||||
let cls = Buf.create () in
|
||||
Array.iteri
|
||||
|
|
|
|||
|
|
@ -53,6 +53,15 @@ let unknown_char_code = Diag.lexing_prefix ^ "01" (* WO-E001 *)
|
|||
oversight; pinned by the plain-unterminated-string-reports-nothing
|
||||
assertion in compiler/test/runner.ml. *)
|
||||
let unterminated_escape_code = Diag.lexing_prefix ^ "02" (* WO-E002 *)
|
||||
let directive_code = Diag.lexing_prefix ^ "03" (* WO-E003: #if/#else/#end misuse *)
|
||||
|
||||
(* haxe-parity Task 8: build flags. `woc -D name` fills this before any
|
||||
tokenize call; undefined flags are false. A module-level ref because the
|
||||
compiler is a single-shot process — tests that care set it explicitly
|
||||
and reset to empty. *)
|
||||
module StringSet = Set.Make (String)
|
||||
|
||||
let defines : StringSet.t ref = ref StringSet.empty
|
||||
|
||||
type lexer = {
|
||||
src : string;
|
||||
|
|
@ -126,6 +135,7 @@ let keyword_kind = function
|
|||
| "false" -> Some Token.KwFalse
|
||||
| "use" -> Some Token.KwUse
|
||||
| "spawn" -> Some Token.KwSpawn
|
||||
| "using" -> Some Token.KwUsing
|
||||
| "pub" -> Some Token.KwPub
|
||||
| "break" -> Some Token.KwBreak
|
||||
| "continue" -> Some Token.KwContinue
|
||||
|
|
@ -205,6 +215,59 @@ let read_interp_expr lx =
|
|||
done;
|
||||
Buffer.contents buf
|
||||
|
||||
(* haxe-parity Task 8: the #if filter, run over the in-order token list at
|
||||
the end of tokenize. A `#if <flag>` section is kept when the flag is
|
||||
defined AND every enclosing section is kept; `#else` flips the section;
|
||||
`#end` closes it. Nesting allowed; flag NAMES only (no expression
|
||||
language — the spec's limit); undefined flags are false. Misuse is
|
||||
WO-E003: a #if without a flag name, a second #else, a stray #else/#end,
|
||||
or a #if left open at end of file. Eof always survives so the parser
|
||||
still terminates after a reported error. *)
|
||||
let preprocess (collector : Diag.Collector.t) ~(file : string)
|
||||
(toks : Token.t list) : Token.t list =
|
||||
let err line col msg =
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:directive_code ~file ~line ~col ~message:msg ())
|
||||
in
|
||||
(* frame: (emitting, seen_else, opening line, opening col) *)
|
||||
let stack : (bool * bool * int * int) list ref = ref [] in
|
||||
let emitting () = List.for_all (fun (e, _, _, _) -> e) !stack in
|
||||
let out = ref [] in
|
||||
let rec go = function
|
||||
| [] -> (
|
||||
match !stack with
|
||||
| (_, _, l, c) :: _ -> err l c "#if left open — missing #end"
|
||||
| [] -> ())
|
||||
| { Token.kind = Token.HashIf; line; col } :: rest -> (
|
||||
match rest with
|
||||
| { Token.kind = Token.Ident flag; _ } :: rest2 ->
|
||||
stack := (StringSet.mem flag !defines, false, line, col) :: !stack;
|
||||
go rest2
|
||||
| _ ->
|
||||
err line col "#if needs a flag name (`#if portable`)";
|
||||
stack := (false, false, line, col) :: !stack;
|
||||
go rest)
|
||||
| { Token.kind = Token.HashElse; line; col } :: rest ->
|
||||
(match !stack with
|
||||
| (e, false, l, c) :: tl -> stack := (not e, true, l, c) :: tl
|
||||
| (_, true, _, _) :: _ -> err line col "second #else in one #if section"
|
||||
| [] -> err line col "#else outside any #if");
|
||||
go rest
|
||||
| { Token.kind = Token.HashEnd; line; col } :: rest ->
|
||||
(match !stack with
|
||||
| _ :: tl -> stack := tl
|
||||
| [] -> err line col "#end outside any #if");
|
||||
go rest
|
||||
| ({ Token.kind = Token.Eof; _ } as t) :: rest ->
|
||||
out := t :: !out;
|
||||
go rest
|
||||
| t :: rest ->
|
||||
if emitting () then out := t :: !out;
|
||||
go rest
|
||||
in
|
||||
go toks;
|
||||
List.rev !out
|
||||
|
||||
let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) :
|
||||
Token.t list =
|
||||
let lx = make src in
|
||||
|
|
@ -321,16 +384,99 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) :
|
|||
| segs -> emit (Token.InterpStr segs) line col)
|
||||
end
|
||||
else if is_digit c then begin
|
||||
(* iteration 19: one scanner for both numeric worlds. The integer run
|
||||
is scanned into a buffer as well as accumulated, because a fraction
|
||||
or an exponent turns the whole thing into a Float and OCaml's
|
||||
float_of_string wants the original text.
|
||||
|
||||
A digit run stays an Int unless it is followed by:
|
||||
- '.' AND a digit -> `1.5`. The digit requirement is what keeps
|
||||
`0..10` a range (Dot Dot after Int 0) and leaves any future
|
||||
`1.method()` reachable; without it `0..10` would lex as
|
||||
Float 0. followed by `.10`.
|
||||
- 'e'/'E' with an optional sign AND a digit -> `2e10`. Checked
|
||||
before consuming, so `2eggs` is still Int 2 then Ident. *)
|
||||
(* `c` is PEEKED, not consumed — the loop below reads it. Adding it to
|
||||
the buffer here as well would count the first digit twice. *)
|
||||
let buf = Buffer.create 16 in
|
||||
let n = ref 0 in
|
||||
let scanning = ref true in
|
||||
while !scanning do
|
||||
match peek lx with
|
||||
| Some d when is_digit d ->
|
||||
n := (!n * 10) + (Char.code d - Char.code '0');
|
||||
Buffer.add_char buf d;
|
||||
ignore (advance lx)
|
||||
| _ -> scanning := false
|
||||
done;
|
||||
emit (Token.Int !n) line col
|
||||
let is_float = ref false in
|
||||
(match (peek lx, peek_at lx 1) with
|
||||
| Some '.', Some d when is_digit d ->
|
||||
is_float := true;
|
||||
Buffer.add_char buf '.';
|
||||
ignore (advance lx);
|
||||
let frac = ref true in
|
||||
while !frac do
|
||||
match peek lx with
|
||||
| Some d when is_digit d ->
|
||||
Buffer.add_char buf d;
|
||||
ignore (advance lx)
|
||||
| _ -> frac := false
|
||||
done
|
||||
| _ -> ());
|
||||
(* exponent, on an integer run (`2e10`) or after a fraction (`1.5e-3`) *)
|
||||
(match (peek lx, peek_at lx 1, peek_at lx 2) with
|
||||
| Some ('e' | 'E'), Some d, _ when is_digit d -> is_float := true
|
||||
| Some ('e' | 'E'), Some ('+' | '-'), Some d when is_digit d -> is_float := true
|
||||
| _ -> ());
|
||||
if !is_float then begin
|
||||
(match peek lx with
|
||||
| Some (('e' | 'E') as e) ->
|
||||
Buffer.add_char buf e;
|
||||
ignore (advance lx);
|
||||
(match peek lx with
|
||||
| Some (('+' | '-') as s) ->
|
||||
Buffer.add_char buf s;
|
||||
ignore (advance lx)
|
||||
| _ -> ());
|
||||
let ex = ref true in
|
||||
while !ex do
|
||||
match peek lx with
|
||||
| Some d when is_digit d ->
|
||||
Buffer.add_char buf d;
|
||||
ignore (advance lx)
|
||||
| _ -> ex := false
|
||||
done
|
||||
| _ -> ());
|
||||
(* float_of_string cannot fail here: the buffer is a well-formed
|
||||
decimal by construction. Overflow is not an error either — it
|
||||
yields infinity, which is a legitimate Float per IEEE quiet
|
||||
semantics (`1e400` is `inf`, not a compile error). *)
|
||||
emit (Token.Float (float_of_string (Buffer.contents buf))) line col
|
||||
end
|
||||
else emit (Token.Int !n) line col
|
||||
end
|
||||
else if c = '#' then begin
|
||||
(* haxe-parity Task 8: `#if` / `#else` / `#end` build-flag
|
||||
directives. Names only — anything else after '#' is WO-E003. *)
|
||||
ignore (advance lx);
|
||||
let name =
|
||||
match peek lx with
|
||||
| Some d when is_ident_start d -> read_ident_chars lx
|
||||
| _ -> ""
|
||||
in
|
||||
match name with
|
||||
| "if" -> emit Token.HashIf line col
|
||||
| "else" -> emit Token.HashElse line col
|
||||
| "end" -> emit Token.HashEnd line col
|
||||
| other ->
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:directive_code ~file ~line ~col
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"unknown directive `#%s` — the build-flag directives are #if <flag>, #else, #end"
|
||||
other)
|
||||
())
|
||||
end
|
||||
else if is_ident_start c then begin
|
||||
let name = read_ident_chars lx in
|
||||
|
|
@ -446,4 +592,4 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) :
|
|||
report_unknown line col other)
|
||||
done;
|
||||
emit Token.Eof lx.line lx.col;
|
||||
List.rev !out
|
||||
preprocess collector ~file (List.rev !out)
|
||||
|
|
|
|||
|
|
@ -406,7 +406,8 @@ let oclass_of (ctx : ctx) (ft : Ast.field_ty) : oclass =
|
|||
per rescan, which is a plain malloc and so ASan-visible). A Text read out
|
||||
of a PLACE is still a borrow — analyze_let's own place logic decides
|
||||
that, exactly as it does for a record field. *)
|
||||
| Scalar n when n = "Text" || n = Types.json_value_type -> Owned
|
||||
(* iteration 19: Bytes joins Text here — see Types.is_heap_scalar *)
|
||||
| Scalar n when Types.is_heap_scalar n -> Owned
|
||||
| Scalar n ->
|
||||
if Types.is_builtin_scalar n then Copy
|
||||
else if Types.is_gc_class ctx.syms n then Gc
|
||||
|
|
@ -497,6 +498,7 @@ let variant_union_ty (ctx : ctx) (n : string) : Ast.field_ty option =
|
|||
let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option =
|
||||
match e.kind with
|
||||
| IntLit _ -> Some (Scalar "Int")
|
||||
| FloatLit _ -> Some (Scalar "Float") (* iteration 19 *)
|
||||
| StrLit _ -> Some (Scalar "Text")
|
||||
| BoolLit _ -> Some (Scalar "Bool")
|
||||
(* A non-empty list literal knows its element type, so an unannotated
|
||||
|
|
@ -1073,7 +1075,7 @@ let escape (ctx : ctx) (l : local) ~(pos : Ast.pos) ~message
|
|||
let stores_by_copy (ctx : ctx) (p : place) : bool =
|
||||
match place_ty ctx p with
|
||||
| Some t -> ( match unwrap_nullable t with
|
||||
| Scalar n -> n = "Text" || n = Types.json_value_type
|
||||
| Scalar n -> Types.is_heap_scalar n
|
||||
| _ -> false)
|
||||
| None -> false
|
||||
|
||||
|
|
@ -1130,7 +1132,10 @@ type access = {
|
|||
|
||||
let rec read_expr (ctx : ctx) (e : Ast.expr) : unit =
|
||||
match e.kind with
|
||||
| IntLit _ | StrLit _ | BoolLit _ -> ()
|
||||
(* iteration 19: a Float literal owns nothing — it is a word in a register,
|
||||
exactly like an Int. (A Bytes value DOES own its heap object, but Bytes
|
||||
has no literal form, so nothing new lands in this arm.) *)
|
||||
| IntLit _ | FloatLit _ | StrLit _ | BoolLit _ -> ()
|
||||
| Ident _ | Field _ | Index _ ->
|
||||
(match place_of e with Some p -> use_place ctx p | None -> ());
|
||||
read_place_parts ctx e
|
||||
|
|
@ -1652,7 +1657,7 @@ and analyze_stmt (ctx : ctx) (s : Ast.stmt) : unit =
|
|||
let cursor (n : string) (t : Ast.field_ty) : local =
|
||||
let copied =
|
||||
match unwrap_nullable t with
|
||||
| Scalar cn -> cn = "Text" || cn = Types.json_value_type
|
||||
| Scalar cn -> Types.is_heap_scalar cn
|
||||
| _ -> false
|
||||
in
|
||||
if copied then
|
||||
|
|
@ -1788,7 +1793,7 @@ and analyze_let (ctx : ctx) (s : Ast.stmt) (name : string) (ty : Ast.field_ty op
|
|||
| _ -> false
|
||||
in
|
||||
let copies_out = reads_container && (match unwrap_nullable vty with
|
||||
| Scalar n -> n = "Text" || n = Types.json_value_type
|
||||
| Scalar n -> Types.is_heap_scalar n
|
||||
| _ -> false) in
|
||||
let vplace = if copies_out then None else place_of value in
|
||||
(* A `@gc` value read out of a container is neither a copy nor a new
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,15 @@ and parse_primary (st : state) : Ast.expr =
|
|||
let id = fresh_id st in
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.IntLit n }
|
||||
| Token.Float f ->
|
||||
(* iteration 19. Negative literals are Unary(Neg, FloatLit) exactly as
|
||||
they are for Int — and that is what makes -0.0 reachable, since the
|
||||
emitter lowers the negation to WOP_FNEG (a sign flip), not to
|
||||
`0.0 - x` (which would give +0.0). *)
|
||||
let pos = peek_pos st in
|
||||
let id = fresh_id st in
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.FloatLit f }
|
||||
| Token.Str s ->
|
||||
let pos = peek_pos st in
|
||||
let id = fresh_id st in
|
||||
|
|
@ -1597,13 +1606,22 @@ let parse_const_literal (st : state) : Ast.expr =
|
|||
| Token.Int n ->
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.IntLit n }
|
||||
| Token.Float f ->
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.FloatLit f }
|
||||
| Token.Dash -> (
|
||||
ignore (advance st);
|
||||
match peek st with
|
||||
| Token.Int n ->
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.IntLit (-n) }
|
||||
| _ -> unexpected st "an integer literal after '-'")
|
||||
| Token.Float f ->
|
||||
(* a `const` initializer is folded here rather than emitted as a
|
||||
negation, so -0.0 needs the sign to survive the fold: OCaml's unary
|
||||
minus on a float flips the sign bit, which is exactly right. *)
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.FloatLit (-.f) }
|
||||
| _ -> unexpected st "a numeric literal after '-'")
|
||||
| Token.Str s ->
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.StrLit s }
|
||||
|
|
@ -1854,18 +1872,18 @@ let parse_interface ?(pub = false) (st : state) : Ast.interface_decl =
|
|||
statement is (`end_of_stmt`: optional `;`, then newline/EOF — there
|
||||
is no enclosing block at top level, but end_of_stmt's RBrace arm is
|
||||
harmless dead code here, never reached). *)
|
||||
let parse_use_decl (st : state) : Ast.use_decl =
|
||||
let parse_use_decl ?(is_using = false) (st : state) : Ast.use_decl =
|
||||
let pos = peek_pos st in
|
||||
let id = fresh_id st in
|
||||
ignore (advance st);
|
||||
(* 'use' *)
|
||||
(* 'use' or 'using' — `using` is a use PLUS extension registration *)
|
||||
let first = expect_ident st "module name" in
|
||||
let segments = ref [ first ] in
|
||||
while accept st Token.Slash do
|
||||
segments := expect_ident st "module path segment" :: !segments
|
||||
done;
|
||||
end_of_stmt st;
|
||||
{ Ast.id; pos; segments = List.rev !segments }
|
||||
{ Ast.id; pos; segments = List.rev !segments; is_using }
|
||||
|
||||
(* ---- top-level program ---------------------------------------------------
|
||||
|
||||
|
|
@ -1900,6 +1918,7 @@ let parse_program (st : state) : Ast.program =
|
|||
| Token.KwInterface -> decls := Ast.Interface (parse_interface st) :: !decls
|
||||
| Token.KwFn -> decls := Ast.Fn (parse_fn_decl ~pub:false st) :: !decls
|
||||
| Token.KwUse -> decls := Ast.Use (parse_use_decl st) :: !decls
|
||||
| Token.KwUsing -> decls := Ast.Use (parse_use_decl ~is_using:true st) :: !decls
|
||||
| Token.KwConst -> decls := Ast.Const (parse_const_decl st) :: !decls
|
||||
| Token.KwInline ->
|
||||
let ipos = peek_pos st in
|
||||
|
|
@ -1967,7 +1986,7 @@ module StringSet = Set.Make (String)
|
|||
|
||||
let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : Ast.expr) : Ast.expr =
|
||||
match e.Ast.kind with
|
||||
| Ast.IntLit _ | Ast.StrLit _ | Ast.BoolLit _ | Ast.DbStub _ -> e
|
||||
| Ast.IntLit _ | Ast.FloatLit _ | Ast.StrLit _ | Ast.BoolLit _ | Ast.DbStub _ -> e
|
||||
| Ast.Ident name ->
|
||||
if StringSet.mem name bound then e
|
||||
else ( match StringMap.find_opt name consts with Some v -> { e with Ast.kind = v.Ast.kind } | None -> e)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ type kind =
|
|||
(* literals *)
|
||||
| Ident of string
|
||||
| Int of int
|
||||
(* iteration 19: a Float literal. OCaml's `float` is an IEEE f64, the same
|
||||
type the VM's registers hold, so the value is carried unchanged from
|
||||
source to `.wob` (Int64.bits_of_float at emit time). A bare digit run is
|
||||
still Token.Int — only a fraction or an exponent makes a Float, so every
|
||||
pre-existing fixture lexes byte-identically. *)
|
||||
| Float of float
|
||||
| Str of string
|
||||
(* haxe-parity Task 2: a string literal containing at least one
|
||||
`${expr}` interpolation. Alternating text/expr segments, in source
|
||||
|
|
@ -66,6 +72,7 @@ type kind =
|
|||
(* the concurrency arc (iterations 8+11): `spawn Cls { ... }`. `send`
|
||||
is deliberately NOT a keyword — it is a builtin free-fn name. *)
|
||||
| KwSpawn
|
||||
| KwUsing
|
||||
| KwPub
|
||||
(* haxe-parity Task 2 (small control surface): break/continue/do-while,
|
||||
const values, and/or booleans, and inline-fn rejection (the haxe
|
||||
|
|
@ -141,6 +148,12 @@ type kind =
|
|||
| GtEq
|
||||
| PlusEq
|
||||
| MinusEq
|
||||
(* haxe-parity Task 8: `#if name / #else / #end` build-flag directives.
|
||||
They exist only between the scanner and the preprocessor filter at the
|
||||
end of Lexer.tokenize — the parser never sees one. *)
|
||||
| HashIf
|
||||
| HashElse
|
||||
| HashEnd
|
||||
(* meta *)
|
||||
| Newline
|
||||
| Eof
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@ type wob_kind =
|
|||
| WO_K_TEXT (* 3 *)
|
||||
| WO_K_MULTI (* 4 *)
|
||||
| WO_K_MAP (* 5 *)
|
||||
| WO_K_NULLABLE (* 6 *)
|
||||
| WO_K_FLOAT (* 6 — iteration 19 *)
|
||||
| WO_K_BYTES (* 7 — iteration 19 *)
|
||||
(* Not a .wob kind byte: `?T` emits T's kind (emit.ml's kind_byte unwraps
|
||||
first). It kept the value 6 in this list until iteration 19 gave 6 a real
|
||||
meaning; the constructor order here has never been the wire order. *)
|
||||
| WO_K_NULLABLE
|
||||
|
||||
(* ============================================================
|
||||
Symbol tables (Pass 1 output, Pass 2 input)
|
||||
|
|
@ -161,11 +166,36 @@ let static_method_of (syms : symbols) (cls_name : string) (m_name : string) : me
|
|||
| Some cls -> List.find_opt (fun (m : method_info) -> m.name = m_name && m.is_static) cls.methods
|
||||
| None -> None
|
||||
|
||||
(* Builtin scalars *)
|
||||
let builtin_scalars = ["Int"; "Bool"; "Text"; "Timestamp"; "Id"]
|
||||
(* Builtin scalars. `Float` and `Bytes` joined in iteration 19 — see
|
||||
docs/stories/language-runtime-database/19-missing-scalar-types.md. Both are
|
||||
real, distinct types with no implicit conversion to or from anything:
|
||||
`float(i)` / `trunc(f)` bridge the two numeric worlds, and
|
||||
`bytes_of_text` / `text_of_bytes` bridge the two byte carriers. *)
|
||||
let builtin_scalars = ["Int"; "Bool"; "Text"; "Timestamp"; "Id"; "Float"; "Bytes"]
|
||||
|
||||
let is_builtin_scalar name = List.mem name builtin_scalars
|
||||
|
||||
(* The two numeric types. Arithmetic and comparison are legal within each and
|
||||
an error ACROSS them — the check needs one predicate, not scattered string
|
||||
compares (`Timestamp`/`Id` are Int-shaped conveniences, so they answer
|
||||
`Int` here: `t + 1` on a Timestamp has always been legal). *)
|
||||
(* Scalars whose VALUE is a heap object the slot owns: storing one copies, a
|
||||
`let` holding one gets a drop, and reading one out of a container copies it
|
||||
out. `Text` and `json.Value` were the whole list until iteration 19 added
|
||||
`Bytes`, which is a wo_str in every respect but its class id — so every
|
||||
ownership rule that named Text by string had to name this instead, or a
|
||||
Bytes would silently never be dropped. Defined here so owner.ml and emit.ml
|
||||
share one answer. (`json_value_type` is bound further down, so the
|
||||
comparison is spelled out rather than referencing it.) *)
|
||||
let is_heap_scalar (name : string) : bool =
|
||||
name = "Text" || name = "Bytes" || name = "json.Value"
|
||||
|
||||
let numeric_world (t : string) : [ `Int | `Float | `Other ] =
|
||||
match t with
|
||||
| "Int" | "Timestamp" | "Id" -> `Int
|
||||
| "Float" -> `Float
|
||||
| _ -> `Other
|
||||
|
||||
(* haxe-parity Task 1 (modules) / gap-closure amendment: six reserved
|
||||
stdlib namespaces, not the plan text's five — `use fs`/`proc`/`net`/
|
||||
`time`/`json`/`env` must resolve now (their members arrive in plan
|
||||
|
|
@ -354,6 +384,13 @@ let wob_kind_of_typ (syms : symbols) (t : typ) : wob_kind =
|
|||
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
|
||||
(* iteration 19. Float is word-shaped like a scalar but needs its own
|
||||
kind so json, the WAL, and printing know the word is f64 bits and
|
||||
not an integer. Bytes is heap-shaped like Text and MUST have its own
|
||||
kind for the same reason WO_K_TEXT exists — the drop plan frees it —
|
||||
plus the distinct id keeps a Text builtin from accepting one. *)
|
||||
else if name = "Float" then WO_K_FLOAT
|
||||
else if name = "Bytes" then WO_K_BYTES
|
||||
else if is_stdlib_scalar_type name then WO_K_SCALAR
|
||||
else if is_builtin_scalar name then WO_K_SCALAR
|
||||
else if StringMap.mem name syms.unions then
|
||||
|
|
@ -395,6 +432,18 @@ let nullable_used_without_check_code = Diag.types_prefix ^ "11"
|
|||
let nullable_assign_mismatch_code = Diag.types_prefix ^ "12"
|
||||
let spawn_no_receive_code = Diag.types_prefix ^ "21" (* WO-E221: spawn target lacks fn receive(msg: M); E219/E220 are taken on the language-surface-strictness branch *)
|
||||
let traced_send_code = Diag.types_prefix ^ "22" (* WO-E222: traced(-containing) type in an actor message or actor state — aliased graphs cannot cross heap boundaries *)
|
||||
let pub_read_write_code = Diag.types_prefix ^ "19" (* WO-E219: pub(read) field written outside its class *)
|
||||
let using_collision_code = Diag.types_prefix ^ "20" (* WO-E220: using extension collides with a real method *)
|
||||
|
||||
(* haxe-parity Task 7: `using` extension-call rewrites, recorded during
|
||||
typecheck and applied to the AST before the owner/emit passes (which
|
||||
then see a plain free-fn call with the receiver as the first argument
|
||||
and need no using-awareness at all). Keyed (file, call-expr id): expr
|
||||
ids restart per parsed file, so the id alone is ambiguous. The value is
|
||||
the bare extension fn name (the module is `use`d — `using` implies it —
|
||||
so the emitter's unqualified cross-module resolution finds it). A
|
||||
single-shot table for a single-shot compiler process. *)
|
||||
let using_rewrites : (string * int, string) Hashtbl.t = Hashtbl.create 64
|
||||
let missing_nil_check_code = Diag.types_prefix ^ "13"
|
||||
|
||||
(* haxe-parity Task 1 (modules). module_not_imported_code (WO-E210,
|
||||
|
|
@ -456,7 +505,12 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) :
|
|||
List.iter (function
|
||||
| Ast.Class c ->
|
||||
let fields = List.map (fun (f : Ast.field) ->
|
||||
(f.name, f.ty, f.default, f.annotations)
|
||||
(* pub(read) rides the annotation list as a synthetic marker so the
|
||||
symbol shape stays put; the write check (WO-E219) reads it *)
|
||||
( f.name,
|
||||
f.ty,
|
||||
f.default,
|
||||
if f.pub_read then "pub_read" :: f.annotations else f.annotations )
|
||||
) c.fields in
|
||||
let methods = List.map (fun (m : Ast.method_decl) ->
|
||||
{ name = m.name;
|
||||
|
|
@ -702,6 +756,11 @@ let rec typ_equal (syms : symbols) (a : typ) (b : typ) : bool =
|
|||
type builtin_arg_req =
|
||||
| ReqText
|
||||
| ReqInt
|
||||
(* iteration 19: the two new scalars need their own requirements, because
|
||||
"any word" would let `trunc(count)` through and silently reinterpret an
|
||||
integer as f64 bits — the exact mixing this iteration outlaws. *)
|
||||
| ReqFloat
|
||||
| ReqBytes
|
||||
| ReqMulti
|
||||
| ReqMap
|
||||
| ReqContainer (* multi or map, e.g. `count`/`get` resolve on either *)
|
||||
|
|
@ -750,6 +809,27 @@ let builtin_signatures : (string * int * builtin_arg_req list) list =
|
|||
("remove", 2, [ ReqMap; ReqAny ]);
|
||||
("key_at", 2, [ ReqMap; ReqInt ]);
|
||||
("val_at", 2, [ ReqMap; ReqInt ]);
|
||||
(* iteration 19: the Float bridges and surface. `float`/`trunc` are the
|
||||
ONLY way between the numeric worlds; `float_to_text` backs both
|
||||
interpolation and json.encode. *)
|
||||
("float", 1, [ ReqInt ]);
|
||||
("trunc", 1, [ ReqFloat ]);
|
||||
("parse_float", 1, [ ReqText ]);
|
||||
("float_to_text", 1, [ ReqFloat ]);
|
||||
("float_cmp", 2, [ ReqFloat; ReqFloat ]);
|
||||
(* iteration 19: Bytes. `bytes_len`/`bytes_at`/`bytes_slice` are spelled
|
||||
out rather than overloading `len`/`byte_at`/`substr`, because the point
|
||||
of a distinct Bytes type is that a Text builtin never accepts one —
|
||||
overloading would put the two carriers back in one namespace. *)
|
||||
("bytes_len", 1, [ ReqBytes ]);
|
||||
("bytes_at", 2, [ ReqBytes; ReqInt ]);
|
||||
("bytes_slice", 3, [ ReqBytes; ReqInt; ReqInt ]);
|
||||
("bytes_eq", 2, [ ReqBytes; ReqBytes ]);
|
||||
("bytes_concat", 2, [ ReqBytes; ReqBytes ]);
|
||||
("base64_encode", 1, [ ReqBytes ]);
|
||||
("base64_decode", 1, [ ReqText ]);
|
||||
("bytes_of_text", 1, [ ReqText ]);
|
||||
("text_of_bytes", 1, [ ReqBytes ]);
|
||||
]
|
||||
|
||||
let rec unwrap_nullable (t : typ) : typ =
|
||||
|
|
@ -833,21 +913,31 @@ let unnarrow_env (names : string list) ~(orig : typ StringMap.t)
|
|||
chasing builtin return types. `Text` stays its own, narrower case:
|
||||
it is the one builtin scalar with a genuinely different
|
||||
representation. *)
|
||||
let is_scalar_shaped (name : string) : bool = is_builtin_scalar name && name <> "Text"
|
||||
(* iteration 19: `Float` and `Bytes` are builtin scalars but NOT Int-shaped.
|
||||
Leaving them in would have let `print_int(price)` and `trunc(digest)`
|
||||
through — the first prints f64 bits as a huge integer, the second
|
||||
reinterprets a pointer. Both are exactly the representation mismatch this
|
||||
predicate exists to catch, so both are excluded by name alongside Text. *)
|
||||
let is_scalar_shaped (name : string) : bool =
|
||||
is_builtin_scalar name && name <> "Text" && name <> "Float" && name <> "Bytes"
|
||||
|
||||
let matches_req (req : builtin_arg_req) (t : typ) : bool =
|
||||
match req, unwrap_nullable t with
|
||||
| ReqAny, _ -> true
|
||||
| ReqText, TScalar "Text" -> true
|
||||
| ReqInt, TScalar name -> is_scalar_shaped name
|
||||
| ReqFloat, TScalar "Float" -> true
|
||||
| ReqBytes, TScalar "Bytes" -> true
|
||||
| ReqMulti, TMulti _ -> true
|
||||
| ReqMap, TMap _ -> true
|
||||
| ReqContainer, (TMulti _ | TMap _) -> true
|
||||
| (ReqText | ReqInt | ReqMulti | ReqMap | ReqContainer), _ -> false
|
||||
| (ReqText | ReqInt | ReqFloat | ReqBytes | ReqMulti | ReqMap | ReqContainer), _ -> false
|
||||
|
||||
let req_label = function
|
||||
| ReqText -> "Text"
|
||||
| ReqInt -> "Int"
|
||||
| ReqFloat -> "Float" (* iteration 19 *)
|
||||
| ReqBytes -> "Bytes"
|
||||
| ReqMulti -> "a `multi`"
|
||||
| ReqMap -> "a `map`"
|
||||
| ReqContainer -> "a `multi` or `map`"
|
||||
|
|
@ -936,6 +1026,17 @@ let builtin_confident_ret (name : string) (arg0 : typ option) : typ option =
|
|||
| "pop" | "shift" -> ( match arg0 with Some (TMulti e) -> Some e | _ -> None)
|
||||
| "key_at" -> ( match arg0 with Some (TMap (k, _)) -> Some k | _ -> None)
|
||||
| "val_at" -> ( match arg0 with Some (TMap (_, v)) -> Some v | _ -> None)
|
||||
(* iteration 19. `parse_float` returns a plain Float, not a `?Float`:
|
||||
unparseable input is NaN, which already means "not a number" and needs no
|
||||
second absence channel — unlike `parse_int`, where every bit pattern is a
|
||||
valid integer so nil had to be borrowed from `?Int`. *)
|
||||
| "float" | "parse_float" -> Some (TScalar "Float")
|
||||
| "trunc" | "float_cmp" | "bytes_len" | "bytes_at" -> Some (TScalar "Int")
|
||||
| "float_to_text" | "base64_encode" | "text_of_bytes" -> Some (TScalar "Text")
|
||||
| "bytes_eq" -> Some (TScalar "Bool")
|
||||
| "bytes_slice" | "bytes_concat" | "bytes_of_text" -> Some (TScalar "Bytes")
|
||||
(* malformed base64 is nil, not a trap: it arrives from the network *)
|
||||
| "base64_decode" -> Some (TNullable (TScalar "Bytes"))
|
||||
| _ -> None
|
||||
|
||||
(* `use_edge`/`uses_of_program`/`path_str` -- relocated here (hotfix)
|
||||
|
|
@ -949,6 +1050,7 @@ type use_edge = {
|
|||
ue_alias : string;
|
||||
ue_segments : string list;
|
||||
ue_is_stdlib : bool;
|
||||
ue_is_using : bool; (* haxe-parity Task 7: `using` extension import *)
|
||||
}
|
||||
|
||||
let uses_of_program (prog : program) : use_edge list =
|
||||
|
|
@ -959,7 +1061,12 @@ let uses_of_program (prog : program) : use_edge list =
|
|||
let is_stdlib =
|
||||
match u.segments with [ s ] -> is_stdlib_module s | _ -> false
|
||||
in
|
||||
Some { ue_pos = u.pos; ue_alias = alias; ue_segments = u.segments; ue_is_stdlib = is_stdlib }
|
||||
Some
|
||||
{ ue_pos = u.pos;
|
||||
ue_alias = alias;
|
||||
ue_segments = u.segments;
|
||||
ue_is_stdlib = is_stdlib;
|
||||
ue_is_using = u.is_using }
|
||||
| Class _ | Interface _ | Fn _ | Const _ | Union _ -> None)
|
||||
prog.decls
|
||||
|
||||
|
|
@ -1060,6 +1167,25 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
| [ fi ] -> Some fi
|
||||
| _ -> None)
|
||||
in
|
||||
(* haxe-parity Task 7: `using` extension candidates for a method-shaped
|
||||
call — this file's using'd modules' pub free fns named [mname] whose
|
||||
FIRST declared parameter type equals the receiver's type exactly.
|
||||
Compile-time only; the rewrite (using_rewrites) is what the emitter
|
||||
sees, never a dispatch table. *)
|
||||
let usings_resolved =
|
||||
List.filter (fun ((u : use_edge), _) -> u.ue_is_using) uses_resolved
|
||||
in
|
||||
let using_candidates (mname : string) (recv : typ) : free_fn_info list =
|
||||
List.filter_map
|
||||
(fun (_, (msyms : symbols)) ->
|
||||
match StringMap.find_opt mname msyms.free_fns with
|
||||
| Some fi when fi.pub -> (
|
||||
match fi.params with
|
||||
| (_, pty, _) :: _ when typ_of_field_ty pty = recv -> Some fi
|
||||
| _ -> None)
|
||||
| _ -> None)
|
||||
usings_resolved
|
||||
in
|
||||
|
||||
(* WO-E209's own type deriver -- deliberately NOT typecheck_expr's
|
||||
`.typ` below, and deliberately narrower. typecheck_expr hands back
|
||||
|
|
@ -1107,6 +1233,7 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
let rec confident_typ (cenv : typ StringMap.t) (e : expr) : typ option =
|
||||
match e.kind with
|
||||
| IntLit _ -> Some (TScalar "Int")
|
||||
| FloatLit _ -> Some (TScalar "Float") (* iteration 19 *)
|
||||
| StrLit _ -> Some (TScalar "Text")
|
||||
| BoolLit _ -> Some (TScalar "Bool")
|
||||
(* A non-empty list literal is confident about its element type; an
|
||||
|
|
@ -1292,6 +1419,10 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
positive off an underivable expression. `current_ret` is the enclosing
|
||||
fn/method's declared return type, set by each body walk below. *)
|
||||
let current_ret : typ option ref = ref None in
|
||||
(* the class whose method body is being checked — None in a free fn.
|
||||
pub(read) writes are legal only when this names the field's declaring
|
||||
class (Haxe's (default, null): the CLASS owns writes, not the instance) *)
|
||||
let current_self : string option ref = ref None in
|
||||
let report_nullable ~code (pos : pos) (msg : string) : unit =
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code ~file ~line:pos.line ~col:pos.col ~message:msg ())
|
||||
|
|
@ -1314,6 +1445,61 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
"%s is possibly nil (`?T`) — check it against `nil` before reaching through it"
|
||||
what)
|
||||
in
|
||||
(* iteration 19: Int and Float are two worlds with two instruction sets and
|
||||
two failure modes. Mixing them in one operator is always an error, never
|
||||
a promotion — the storefront that prices in cents did so because there
|
||||
was no Float, and silently widening an Int into an f64 is how the next
|
||||
rounding bug gets written. `float(i)` and `trunc(f)` say it out loud.
|
||||
|
||||
Only reports when BOTH sides have confident types: this file's standing
|
||||
contract is to stay silent rather than guess (an underivable operand
|
||||
means no diagnostic, not a false one). *)
|
||||
let check_numeric_mix (cenv : typ StringMap.t) (pos : pos) (opname : string)
|
||||
(left : expr) (right : expr) : unit =
|
||||
let world (e : expr) =
|
||||
match confident_typ cenv e with
|
||||
| Some t -> ( match unwrap_nullable t with TScalar n -> numeric_world n | _ -> `Other)
|
||||
| None -> `Other
|
||||
in
|
||||
match (world left, world right) with
|
||||
| `Int, `Float | `Float, `Int ->
|
||||
let int_side = if world left = `Int then "left" else "right" in
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:type_mismatch_code ~file ~line:pos.line ~col:pos.col
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"`%s` mixes Int and Float — there is no implicit conversion; wrap the \
|
||||
%s side in `float(...)`, or `trunc(...)` the Float side to stay in Int"
|
||||
opname int_side)
|
||||
())
|
||||
| _ -> ()
|
||||
in
|
||||
let mod_on_float (cenv : typ StringMap.t) (pos : pos) (left : expr) (right : expr) : unit =
|
||||
let is_float (e : expr) =
|
||||
match confident_typ cenv e with
|
||||
| Some t -> ( match unwrap_nullable t with TScalar n -> numeric_world n = `Float | _ -> false)
|
||||
| None -> false
|
||||
in
|
||||
if is_float left || is_float right then
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:type_mismatch_code ~file ~line:pos.line ~col:pos.col
|
||||
~message:
|
||||
"`%` has no Float meaning — integer remainder is not IEEE remainder, and this \
|
||||
language has no `fmod`; `trunc(...)` first if integer remainder is what you want"
|
||||
())
|
||||
in
|
||||
let e219 (pos : pos) (fname : string) (cls : string) : unit =
|
||||
report_nullable ~code:pub_read_write_code pos
|
||||
(Printf.sprintf
|
||||
"field `%s` of class `%s` is pub(read) — readable anywhere, writable only inside `%s`"
|
||||
fname cls cls)
|
||||
in
|
||||
let e220 (pos : pos) (mname : string) (recv : string) : unit =
|
||||
report_nullable ~code:using_collision_code pos
|
||||
(Printf.sprintf
|
||||
"`%s` is both a real method of `%s` and a `using` extension — rename one; an extension never overrides a method"
|
||||
mname recv)
|
||||
in
|
||||
(* the boundary test every store/return/argument shares: value flows into a
|
||||
non-nullable slot *)
|
||||
let crosses_boundary ~(target : typ) (v : expr_type_result) : bool =
|
||||
|
|
@ -1356,6 +1542,7 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
expr_type_result =
|
||||
match e.kind with
|
||||
| IntLit _ -> { typ = TScalar "Int"; is_nil = false }
|
||||
| FloatLit _ -> { typ = TScalar "Float"; is_nil = false } (* iteration 19 *)
|
||||
| StrLit _ -> { typ = TScalar "Text"; is_nil = false }
|
||||
| BoolLit _ -> { typ = TScalar "Bool"; is_nil = false }
|
||||
| Ident name ->
|
||||
|
|
@ -1412,11 +1599,25 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
| Field (base, mname) -> (
|
||||
match Option.map unwrap_nullable (confident_typ cenv base) with
|
||||
| Some (TScalar cn) -> (
|
||||
(* haxe-parity Task 7: a method-shaped call may be a `using`
|
||||
extension — a real method always wins AND collides loudly
|
||||
(WO-E220); exactly one candidate on a method-less receiver
|
||||
records the rewrite the emitter will see. *)
|
||||
let cands = using_candidates mname (TScalar cn) in
|
||||
let record_rewrite (fi : free_fn_info) =
|
||||
Hashtbl.replace using_rewrites (file, e.id) fi.name;
|
||||
Some (List.tl fi.params)
|
||||
in
|
||||
match StringMap.find_opt cn syms.classes with
|
||||
| Some cls -> (
|
||||
match List.find_opt (fun (m : method_info) -> m.name = mname) cls.methods with
|
||||
| Some m -> Some m.params
|
||||
| None -> None)
|
||||
| Some m ->
|
||||
(match cands with
|
||||
| _ :: _ -> e220 callee.pos mname cn
|
||||
| [] -> ());
|
||||
Some m.params
|
||||
| None -> (
|
||||
match cands with [ fi ] -> record_rewrite fi | _ -> None))
|
||||
| None -> (
|
||||
match StringMap.find_opt cn syms.interfaces with
|
||||
| Some iface -> (
|
||||
|
|
@ -1424,8 +1625,10 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
List.find_opt (fun (s : method_sig_info) -> s.name = mname) iface.methods
|
||||
with
|
||||
| Some sg -> Some sg.params
|
||||
| None -> None)
|
||||
| None -> None))
|
||||
| None -> (
|
||||
match cands with [ fi ] -> record_rewrite fi | _ -> None))
|
||||
| None -> (
|
||||
match cands with [ fi ] -> record_rewrite fi | _ -> None)))
|
||||
| _ -> (
|
||||
match base.kind with
|
||||
| Ident head -> (
|
||||
|
|
@ -1604,6 +1807,9 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
ul.u_name ur.u_name)
|
||||
())
|
||||
| _ -> ());
|
||||
(* iteration 19: `==` across the divide is explicitly out of scope as
|
||||
an implicit conversion, so it is an error like every other mix. *)
|
||||
check_numeric_mix cenv e.pos "==" left right;
|
||||
{ typ = TScalar "Bool"; is_nil = false }
|
||||
| Binary (((Add | Sub | Mul | Div | Mod) as op), left, right) ->
|
||||
let lres = typecheck_expr env cenv left in
|
||||
|
|
@ -1635,13 +1841,33 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
| Div -> "/"
|
||||
| _ -> "%"))
|
||||
());
|
||||
(* iteration 19: the two numeric worlds never mix implicitly. Caught
|
||||
here rather than left to the emitter because the emitter picks the
|
||||
opcode from the LEFT operand's type — `1 + 2.5` would lower to
|
||||
integer ADD over f64 bits and produce a garbage number with no
|
||||
diagnostic at all. Reported off confident types only, the same
|
||||
stay-silent-when-underivable contract as the Text check above.
|
||||
`%` is rejected outright for Float: fmod is not an operator this
|
||||
language has, and silently meaning integer remainder would be
|
||||
worse than saying no. *)
|
||||
let opname =
|
||||
match op with Add -> "+" | Sub -> "-" | Mul -> "*" | Div -> "/" | _ -> "%"
|
||||
in
|
||||
check_numeric_mix cenv e.pos opname left right;
|
||||
if op = Mod then mod_on_float cenv e.pos left right;
|
||||
{ typ = (match confident_typ cenv left with Some t -> t | None -> TScalar "Int");
|
||||
is_nil = false }
|
||||
| Binary ((Lt | Le | Gt | Ge), left, right) ->
|
||||
| Binary (((Lt | Le | Gt | Ge) as op), left, right) ->
|
||||
let lres = typecheck_expr env cenv left in
|
||||
let rres = typecheck_expr env cenv right in
|
||||
if is_nullable lres.typ || lres.is_nil then e211 left.pos (expr_label left);
|
||||
if is_nullable rres.typ || rres.is_nil then e211 right.pos (expr_label right);
|
||||
(* iteration 19: ordering across the divide is the same error as
|
||||
arithmetic across it — `cents < price` compares an integer against
|
||||
f64 bits and answers nonsense. *)
|
||||
check_numeric_mix cenv e.pos
|
||||
(match op with Lt -> "<" | Le -> "<=" | Gt -> ">" | _ -> ">=")
|
||||
left right;
|
||||
{ typ = TScalar "Bool"; is_nil = false }
|
||||
| Binary (_, left, right) ->
|
||||
let _ = typecheck_expr env cenv left in
|
||||
|
|
@ -1940,10 +2166,19 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
(`type_mismatch_code`) — the same code the arm-unification check
|
||||
below uses — per the review's own instruction ("wire through
|
||||
E201 like arm mismatch"). *)
|
||||
let repr_kind (t : typ) : [ `Text | `Scalar | `Other ] =
|
||||
(* iteration 19: FLOAT and BYTES get their own answers rather than folding
|
||||
into `Scalar`/`Text`. Folding Float into `Scalar` would let a Float
|
||||
subject switch against Int labels with no diagnostic and lower to an
|
||||
integer compare over f64 bits; folding Bytes into `Text` would let a
|
||||
Bytes subject match Text labels. Both are distinct representations to
|
||||
this check, so a mismatch is reported and a same-kind switch is left
|
||||
alone (emit.ml picks FEQ for a Float subject). *)
|
||||
let repr_kind (t : typ) : [ `Text | `Scalar | `Float | `Bytes | `Other ] =
|
||||
match wob_kind_of_typ syms t with
|
||||
| WO_K_TEXT -> `Text
|
||||
| WO_K_SCALAR -> `Scalar
|
||||
| WO_K_FLOAT -> `Float
|
||||
| WO_K_BYTES -> `Bytes
|
||||
| WO_K_OWNED | WO_K_GCREF | WO_K_MULTI | WO_K_MAP | WO_K_NULLABLE -> `Other
|
||||
in
|
||||
(* haxe-parity Task 4: a union-typed subject switches the arms from
|
||||
|
|
@ -2282,7 +2517,12 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
match StringMap.find_opt cn syms.classes with
|
||||
| Some cls -> (
|
||||
match List.find_opt (fun (fn2, _, _, _) -> fn2 = fname) cls.fields with
|
||||
| Some (_, fty, _, _) -> Some (resolve_field_ty fty)
|
||||
| Some (_, fty, _, annots) ->
|
||||
(* WO-E219: a pub(read) field is written only from inside
|
||||
its declaring class's own methods *)
|
||||
if List.mem "pub_read" annots && !current_self <> Some cn then
|
||||
e219 target.pos fname cn;
|
||||
Some (resolve_field_ty fty)
|
||||
| None -> None)
|
||||
| None -> None)
|
||||
| _ -> None)
|
||||
|
|
@ -2388,8 +2628,10 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
StringMap.add name (resolve_field_ty ty) acc)
|
||||
(StringMap.singleton "self" (TScalar self_class)) m.params in
|
||||
current_ret := Option.map resolve_field_ty m.ret;
|
||||
current_self := Some self_class;
|
||||
let _ = List.fold_left typecheck_stmt (env_with_self, cenv_with_self) m.body in
|
||||
current_ret := None;
|
||||
current_self := None;
|
||||
false
|
||||
in
|
||||
|
||||
|
|
@ -2565,7 +2807,7 @@ and walk_stmt (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (s : s
|
|||
and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : expr) : unit =
|
||||
visit bound e;
|
||||
match e.kind with
|
||||
| IntLit _ | StrLit _ | BoolLit _ | Ident _ -> ()
|
||||
| IntLit _ | FloatLit _ | StrLit _ | BoolLit _ | Ident _ -> ()
|
||||
| Field (b, _) -> walk_expr bound visit b
|
||||
| Index (b, i) ->
|
||||
walk_expr bound visit b;
|
||||
|
|
@ -2885,3 +3127,84 @@ let dump_symbols (syms : symbols) : string =
|
|||
) syms.free_fns [] in
|
||||
|
||||
String.concat "\n" (class_lines @ interface_lines @ fn_lines)
|
||||
|
||||
(* haxe-parity Task 7: rewrite `recv.ext(args)` into `ext(recv, args)` for
|
||||
every call typecheck resolved as a `using` extension (using_rewrites).
|
||||
Runs between typecheck and the owner/emit passes (bin/main.ml), so those
|
||||
passes see an ordinary free-fn call — borrowed receiver as the first
|
||||
argument — and carry zero using-awareness of their own. *)
|
||||
let apply_using_rewrites ~(file : string) (prog : program) : program =
|
||||
if Hashtbl.length using_rewrites = 0 then prog
|
||||
else begin
|
||||
let rec rx (e : expr) : expr =
|
||||
let kind =
|
||||
match e.kind with
|
||||
| Call (({ kind = Field (base, _); _ } as callee), args)
|
||||
when Hashtbl.mem using_rewrites (file, e.id) ->
|
||||
let fn = Hashtbl.find using_rewrites (file, e.id) in
|
||||
Call ({ callee with kind = Ident fn }, rx base :: List.map rx args)
|
||||
| Call (callee, args) -> Call (rx callee, List.map rx args)
|
||||
| Field (b, f) -> Field (rx b, f)
|
||||
| Index (b, i) -> Index (rx b, rx i)
|
||||
| Unary (op, o) -> Unary (op, rx o)
|
||||
| Binary (op, l, r) -> Binary (op, rx l, rx r)
|
||||
| Ctor (n, fs) -> Ctor (n, List.map (fun (k, v) -> (k, rx v)) fs)
|
||||
| Spawn (n, fs) -> Spawn (n, List.map (fun (k, v) -> (k, rx v)) fs)
|
||||
| Insert (n, fs) -> Insert (n, List.map (fun (k, v) -> (k, rx v)) fs)
|
||||
| Delete d -> Delete (rx d)
|
||||
| Interp inner -> Interp (rx inner)
|
||||
| Switch (scrut, arms) ->
|
||||
Switch
|
||||
( rx scrut,
|
||||
List.map
|
||||
(fun (a : switch_arm) ->
|
||||
{ a with values = List.map rx a.values; body = List.map rs a.body })
|
||||
arms )
|
||||
| ListLit items -> ListLit (List.map rx items)
|
||||
| As (inner, t) -> As (rx inner, t)
|
||||
| Try { body; ename; handler } ->
|
||||
Try { body = rx body; ename; handler = List.map rs handler }
|
||||
| Query q ->
|
||||
Query
|
||||
{ q with
|
||||
q_wheres = List.map rx q.q_wheres;
|
||||
q_group = Option.map (fun (g, k) -> (g, rx k)) q.q_group;
|
||||
q_order = Option.map (fun (k, d) -> (rx k, d)) q.q_order;
|
||||
q_take = Option.map rx q.q_take;
|
||||
q_select = rx q.q_select }
|
||||
| ( IntLit _ | FloatLit _ | StrLit _ | BoolLit _ | NilLit | Ident _ | MapLit
|
||||
| DbStub _ ) as k ->
|
||||
k
|
||||
in
|
||||
{ e with kind }
|
||||
and rs (s : stmt) : stmt =
|
||||
let k =
|
||||
match s.s_kind with
|
||||
| Let l -> Let { l with value = rx l.value }
|
||||
| Assign { target; value } -> Assign { target = rx target; value = rx value }
|
||||
| If { cond; then_body; else_body } ->
|
||||
If
|
||||
{ cond = rx cond;
|
||||
then_body = List.map rs then_body;
|
||||
else_body = Option.map (fun (p, b) -> (p, List.map rs b)) else_body }
|
||||
| While { cond; body } -> While { cond = rx cond; body = List.map rs body }
|
||||
| For f -> For { f with iter = rx f.iter; body = List.map rs f.body }
|
||||
| DoWhile { cond; body } -> DoWhile { cond = rx cond; body = List.map rs body }
|
||||
| Return e -> Return (Option.map rx e)
|
||||
| ExprStmt e -> ExprStmt (rx e)
|
||||
| (Break | Continue) as k -> k
|
||||
in
|
||||
{ s with s_kind = k }
|
||||
in
|
||||
let rd (d : decl) : decl =
|
||||
match d with
|
||||
| Class c ->
|
||||
Class
|
||||
{ c with
|
||||
methods =
|
||||
List.map (fun (m : method_decl) -> { m with body = List.map rs m.body }) c.methods }
|
||||
| Fn f -> Fn { f with body = List.map rs f.body }
|
||||
| (Interface _ | Use _ | Const _ | Union _) as d -> d
|
||||
in
|
||||
{ decls = List.map rd prog.decls }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1221,14 +1221,21 @@ let typecheck_str ~file src =
|
|||
(syms, collector)
|
||||
|
||||
let () =
|
||||
(* Money/SKU/Float carry no special status -- all three are ordinary
|
||||
unknown types now (WO-E225 fires on them as fields). Float went for
|
||||
the same phantom-scalar reason Money/SKU did: no float-literal syntax
|
||||
in the lexer and no float kind in wob, so no Float value could ever
|
||||
be written or represented. Timestamp stays a real builtin. *)
|
||||
(* Money/SKU carry no special status -- ordinary unknown types (WO-E225
|
||||
fires on them as fields). Float was removed for the same phantom-scalar
|
||||
reason once, and iteration 19 EARNED IT BACK: there is float-literal
|
||||
syntax in the lexer, a WO_K_FLOAT kind in wob, f64 opcodes in the VM, and
|
||||
a WAL slot -- so a Float value can now be written, stored, and replayed.
|
||||
Money stays out (cents-as-Int holds until a workload proves otherwise);
|
||||
Bytes came in with Float. Timestamp stays a real builtin. *)
|
||||
check "Money is no longer a builtin scalar" (not (Types.is_builtin_scalar "Money"));
|
||||
check "SKU is no longer a builtin scalar" (not (Types.is_builtin_scalar "SKU"));
|
||||
check "Float is not a builtin scalar" (not (Types.is_builtin_scalar "Float"));
|
||||
check "Float is a builtin scalar (iteration 19)" (Types.is_builtin_scalar "Float");
|
||||
check "Bytes is a builtin scalar (iteration 19)" (Types.is_builtin_scalar "Bytes");
|
||||
(* the no-mixing rule's own predicate: Float must NOT be Int-shaped, or
|
||||
`print_int(price)` would print f64 bits as a huge integer *)
|
||||
check "Float is not Int-shaped" (not (Types.is_scalar_shaped "Float"));
|
||||
check "Bytes is not Int-shaped" (not (Types.is_scalar_shaped "Bytes"));
|
||||
check "Timestamp is a builtin scalar" (Types.is_builtin_scalar "Timestamp")
|
||||
|
||||
let () =
|
||||
|
|
@ -2271,7 +2278,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 <> 4 then fail "unsupported version";
|
||||
if u32 4 <> 5 then fail "unsupported version"; (* v5: iteration 19 *)
|
||||
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
|
||||
|
|
@ -2287,7 +2294,10 @@ let validate_image (img : string) : string list =
|
|||
let tag = u8 !o in
|
||||
incr o;
|
||||
ctag.(i) <- tag;
|
||||
if tag = 0 then o := !o + 8
|
||||
(* tag 2 = WOB_K_FLOAT (iteration 19): same 8-byte payload as an Int,
|
||||
read as f64 bits. Every bit pattern is a legal f64, so nothing to
|
||||
validate beyond the length. *)
|
||||
if tag = 0 || tag = 2 then o := !o + 8
|
||||
else if tag = 1 then begin
|
||||
let n = u32 !o in
|
||||
o := !o + 4;
|
||||
|
|
@ -2310,7 +2320,8 @@ let validate_image (img : string) : string list =
|
|||
class_fields.(i) <- fcnt;
|
||||
let kco = !o in (* the kind bytes' offset: the v3 index walk re-reads them *)
|
||||
for j = 0 to fcnt - 1 do
|
||||
if u8 (!o + j) > 5 then fail (Printf.sprintf "class %d field %d: bad kind" i j)
|
||||
(* WO_K_MAX is 7 since iteration 19 (6 = FLOAT, 7 = BYTES) *)
|
||||
if u8 (!o + j) > 7 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
|
||||
|
|
@ -2321,8 +2332,12 @@ let validate_image (img : string) : string list =
|
|||
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)
|
||||
(* NONE, JSON_RAW, NIL_SCALAR, BOOL, NIL_BOOL, and (iteration 19)
|
||||
NIL_FLOAT are markers, not class ids — same list as loader.c *)
|
||||
if
|
||||
fc <> 0xFFFFFFFF && fc <> 0xFFFFFFFE && fc <> 0xFFFFFFFD && fc <> 0xFFFFFFFC
|
||||
&& fc <> 0xFFFFFFFB && fc <> 0xFFFFFFFA && fc >= kcnt
|
||||
then fail (Printf.sprintf "class %d field %d: field class out of range" i j)
|
||||
done;
|
||||
o := !o + (fcnt * 12);
|
||||
(* v3: the index tail — flags (bit0 only), col_cnt 1..8, columns in
|
||||
|
|
@ -2340,8 +2355,11 @@ let validate_image (img : string) : string list =
|
|||
o := !o + 4;
|
||||
if col >= fcnt then fail (Printf.sprintf "class %d index %d: column out of range" i x);
|
||||
let kind = u8 (kco + col) in
|
||||
if kind <> 0 && kind <> 3 then
|
||||
fail (Printf.sprintf "class %d index %d: column %d is not scalar or Text" i x c)
|
||||
(* iteration 19: FLOAT (6) is indexable — the engine orders it by the
|
||||
total order (NaN last). BYTES (7) is not, this iteration. *)
|
||||
if kind <> 0 && kind <> 3 && kind <> 6 then
|
||||
fail
|
||||
(Printf.sprintf "class %d index %d: column %d is not scalar, Text, or Float" i x c)
|
||||
done
|
||||
done;
|
||||
if !o > len then fail (Printf.sprintf "class %d: truncated" i)
|
||||
|
|
@ -2493,13 +2511,15 @@ let validate_image (img : string) : string list =
|
|||
golden lowering suite actually emits; 61 = DB_INSERT (arity 1:
|
||||
the class-id slot — field slots are runtime-validated, same as
|
||||
the C loader) *)
|
||||
if c > 12 && (c < 61 || c > 67) then
|
||||
(* iteration 19 widened the accepted band to 70-83 (the Float
|
||||
bridges and the Bytes surface) alongside 61-67 *)
|
||||
if c > 12 && (c < 61 || c > 67) && (c < 70 || c > 83) then
|
||||
fail (Printf.sprintf "method %d pc %d: builtin out of range" i pc)
|
||||
else if c = 4 then begin
|
||||
if b > 5 then fail (Printf.sprintf "method %d pc %d: bad element kind" i pc)
|
||||
if b > 7 then fail (Printf.sprintf "method %d pc %d: bad element kind" i pc)
|
||||
end
|
||||
else if c = 9 then begin
|
||||
if b land 0x0F > 5 || b lsr 4 > 5 then
|
||||
if b land 0x0F > 7 || b lsr 4 > 7 then
|
||||
fail (Printf.sprintf "method %d pc %d: bad key/value kind" i pc)
|
||||
end
|
||||
else begin
|
||||
|
|
@ -2516,6 +2536,11 @@ let validate_image (img : string) : string list =
|
|||
| 65 -> 3
|
||||
| 66 -> 3
|
||||
| 67 -> 2
|
||||
(* iteration 19: float bridges and Bytes, mirroring
|
||||
loader.c's b_arity table *)
|
||||
| 70 | 71 | 72 | 73 | 75 | 80 | 81 | 82 | 83 -> 1
|
||||
| 74 | 76 | 78 | 79 -> 2
|
||||
| 77 -> 3
|
||||
| _ -> 0
|
||||
in
|
||||
if arity > 0 then begin
|
||||
|
|
@ -2524,6 +2549,15 @@ let validate_image (img : string) : string list =
|
|||
end
|
||||
end
|
||||
| 30 | 31 -> ()
|
||||
(* iteration 19: FNEG is two registers, the rest are three — the same
|
||||
shapes as their Int counterparts (opcodes 7 and 3-6/9-11) *)
|
||||
| 38 ->
|
||||
rchk pc a;
|
||||
rchk pc b
|
||||
| 34 | 35 | 36 | 37 | 39 | 40 | 41 ->
|
||||
rchk pc a;
|
||||
rchk pc b;
|
||||
rchk pc c
|
||||
| _ -> fail (Printf.sprintf "method %d pc %d: unknown opcode %d" i pc op))
|
||||
code;
|
||||
if ninstr > 0 then begin
|
||||
|
|
|
|||
|
|
@ -25,8 +25,12 @@ static const wo_classdesc *g_classes;
|
|||
static void db_val_free(uint8_t kind, uint64_t v) {
|
||||
if (!v) return;
|
||||
switch (kind) {
|
||||
case WO_K_SCALAR: return;
|
||||
case WO_K_TEXT: free((db_text *)(uintptr_t)v); return;
|
||||
/* iteration 19: FLOAT is a word in the slot, nothing to free. BYTES is
|
||||
stored in the same db_text blob a Text is, so the same free serves. */
|
||||
case WO_K_SCALAR:
|
||||
case WO_K_FLOAT: return;
|
||||
case WO_K_TEXT:
|
||||
case WO_K_BYTES: free((db_text *)(uintptr_t)v); return;
|
||||
case WO_K_OWNED: db_rec_free((db_rec *)(uintptr_t)v, g_classes); return;
|
||||
case WO_K_MULTI: {
|
||||
db_multi *m = (db_multi *)(uintptr_t)v;
|
||||
|
|
@ -53,8 +57,14 @@ static uint64_t db_val_encode(const wo_classdesc *classes, uint8_t kind, uint64_
|
|||
int *ok, const char **msg) {
|
||||
*ok = 1;
|
||||
switch (kind) {
|
||||
case WO_K_SCALAR: return v;
|
||||
case WO_K_TEXT: {
|
||||
/* iteration 19: the f64's bits go in the slot unexamined. NaN, the
|
||||
infinities, and -0.0 all store and read back bit-exact because nothing
|
||||
here interprets the word — the kind byte is what tells json and the WAL
|
||||
how to read it later. */
|
||||
case WO_K_SCALAR:
|
||||
case WO_K_FLOAT: return v;
|
||||
case WO_K_TEXT:
|
||||
case WO_K_BYTES: {
|
||||
if (!v) return 0;
|
||||
const wo_str *s = (const wo_str *)(uintptr_t)v;
|
||||
db_text *t = malloc(sizeof(db_text) + s->len);
|
||||
|
|
@ -140,11 +150,17 @@ static uint64_t db_val_decode(wo_rt *rt, uint8_t kind, uint64_t v, int *ok,
|
|||
const char **msg) {
|
||||
*ok = 1;
|
||||
switch (kind) {
|
||||
case WO_K_SCALAR: return v;
|
||||
case WO_K_TEXT: {
|
||||
case WO_K_SCALAR:
|
||||
case WO_K_FLOAT: return v; /* iteration 19: bits back out unchanged */
|
||||
case WO_K_TEXT:
|
||||
case WO_K_BYTES: {
|
||||
if (!v) return 0;
|
||||
const db_text *t = (const db_text *)(uintptr_t)v;
|
||||
wo_str *s = wo_str_new(rt, t->bytes, t->len);
|
||||
/* the out-gate decides the KIND: a Bytes column must hand back a
|
||||
Bytes, or a Text builtin would happily accept the row's value and
|
||||
the distinct type would be a fiction at the storage boundary */
|
||||
wo_str *s = kind == WO_K_BYTES ? wo_bytes_new(rt, t->bytes, t->len)
|
||||
: wo_str_new(rt, t->bytes, t->len);
|
||||
if (!s) goto oom;
|
||||
return (uint64_t)(uintptr_t)s;
|
||||
}
|
||||
|
|
@ -268,6 +284,20 @@ static void hdel(db_table *t, uint64_t id) {
|
|||
|
||||
/* ---- secondary indexes (Task 4) ---------------------------------------- */
|
||||
|
||||
/* iteration 19: an index key for a FLOAT column is the value's CANONICAL
|
||||
* bits, not its raw bits. Two values that the total order calls equal must
|
||||
* hash and compare equal, and raw bits break that twice: -0.0 and +0.0 are
|
||||
* equal but differ in the sign bit, and two NaNs with different payloads are
|
||||
* equal (both "last") but differ everywhere. Without this a `unique` Float
|
||||
* column would accept both -0.0 and 0.0, and a probe for one would miss a row
|
||||
* stored as the other. */
|
||||
static uint64_t idx_float_key(uint64_t bits) {
|
||||
double d = wo_f64(bits);
|
||||
if (d != d) return 0x7FF8000000000000ull; /* every NaN -> the canonical one */
|
||||
if (d == 0.0) return 0; /* -0.0 -> +0.0 */
|
||||
return bits;
|
||||
}
|
||||
|
||||
/* hash of one row's index columns: kind-driven, never trusted for equality */
|
||||
static uint64_t idx_hash(const wo_classdesc *c, const db_index *ix, const db_row *r) {
|
||||
uint64_t h = 0x9e3779b97f4a7c15ull;
|
||||
|
|
@ -282,6 +312,8 @@ static uint64_t idx_hash(const wo_classdesc *c, const db_index *ix, const db_row
|
|||
else th = 0;
|
||||
v = th;
|
||||
}
|
||||
else if (c->kinds[col] == WO_K_FLOAT)
|
||||
v = idx_float_key(v); /* iteration 19 */
|
||||
h ^= hmix(v + i);
|
||||
}
|
||||
return h ? h : 1; /* 0 marks an empty bucket */
|
||||
|
|
@ -298,6 +330,9 @@ static int idx_cols_equal(const wo_classdesc *c, const db_index *ix, const db_ro
|
|||
if (x != y) return 0;
|
||||
} else if (x->len != y->len || memcmp(x->bytes, y->bytes, x->len) != 0)
|
||||
return 0;
|
||||
} else if (c->kinds[col] == WO_K_FLOAT) {
|
||||
/* iteration 19: compare canonicalized, matching idx_hash */
|
||||
if (idx_float_key(a->slots[col]) != idx_float_key(b->slots[col])) return 0;
|
||||
} else if (a->slots[col] != b->slots[col])
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,8 +98,15 @@ static uint64_t rd_u64(rbuf *r) {
|
|||
|
||||
static void enc_val(wbuf *w, const wo_classdesc *classes, uint8_t kind, uint64_t v) {
|
||||
switch (kind) {
|
||||
case WO_K_SCALAR: wput_u64(w, v); return;
|
||||
case WO_K_TEXT: {
|
||||
/* iteration 19: a Float is one word on the wire, its raw IEEE bits — no
|
||||
decimal rendering anywhere in the durability path, so replay is
|
||||
bit-exact for NaN, the infinities, and -0.0 alike. A Bytes is the same
|
||||
length-prefixed blob a Text is; the class table's kind byte is what
|
||||
says which one comes back out. */
|
||||
case WO_K_SCALAR:
|
||||
case WO_K_FLOAT: wput_u64(w, v); return;
|
||||
case WO_K_TEXT:
|
||||
case WO_K_BYTES: {
|
||||
if (!v) {
|
||||
wput_u32(w, WAL_NIL_TEXT);
|
||||
return;
|
||||
|
|
@ -160,13 +167,15 @@ static void enc_val(wbuf *w, const wo_classdesc *classes, uint8_t kind, uint64_t
|
|||
static int dec_val(rbuf *r, wo_db *db, uint8_t kind, uint64_t *out) {
|
||||
*out = 0;
|
||||
switch (kind) {
|
||||
case WO_K_SCALAR: {
|
||||
case WO_K_SCALAR:
|
||||
case WO_K_FLOAT: { /* iteration 19: the same word back, uninterpreted */
|
||||
uint64_t v = rd_u64(r);
|
||||
if (r->bad) return -1;
|
||||
*out = v;
|
||||
return 0;
|
||||
}
|
||||
case WO_K_TEXT: {
|
||||
case WO_K_TEXT:
|
||||
case WO_K_BYTES: {
|
||||
uint32_t len = rd_u32(r);
|
||||
if (r->bad) return -1;
|
||||
if (len == WAL_NIL_TEXT) return 0;
|
||||
|
|
|
|||
|
|
@ -7,9 +7,83 @@ and the plans it amends; its Phase 1–4 roadmap is retired in favour of the
|
|||
approved story iterations. See [`00-status.md`](00-status.md) for current
|
||||
status.
|
||||
|
||||
## Standing critique (undated, author unrecorded)
|
||||
|
||||
Native speed — the big one. Everything is interpreted: ~40× behind Go on raw compute, no JIT, no AOT-to-native. The scheduling primitives win benchmarks; a compute-bound handler loses them all back. There is also no Float type at all (the storefront prices in cents for a reason), no SIMD story, and fixed interpreter ceilings (4096 register slots, 256 frames, ~42 KiB per fiber until growable contexts land).
|
||||
|
||||
- Language expressiveness. No generics — the cache stores Text and tells you to json.encode; no function values or closures (doctrine, but it's why every handler is a class with one method); byte-based strings with no Unicode awareness; no Result-style error values (traps + try only); pattern matching is a switch, not destructuring. Some of this is deliberate rejection, but "deliberate" doesn't make the expressiveness appear.
|
||||
- Concurrency holes the arc hasn't closed. send is one-way — no reply/request-response primitive (my own benchmarks couldn't await the actor and had to sleep); no supervision, links, or actor death (actors live until process end); unbounded mailboxes with zero backpressure; no timers beyond sleep; round-robin placement with no work stealing; multi-shard DB access still traps (stage 3 unbuilt); accept lives on one shard.
|
||||
- Production plumbing. No TLS anywhere (proxy-mandated forever), no HTTP/2 or WebSockets yet, no crypto primitives (blocked on the bit-ops-vs-builtins fork), observability is print/stderr — no metrics, tracing, or profiler; no debugger, no LSP (discussed, never built); deps are git-rev-only with no registry, no transitive resolution, no semver; blue-green deploy and schema migrations are recorded futures, not features.
|
||||
- Proof maturity. 22's benchmark battery has never run — every number so far is a scratch measurement on one machine; TSan covers one demo; no fuzzing, no CI beyond local just, and the whole ecosystem is one framework, five samples, and one committed consumer. The honest summary: the architecture is ahead of the product — the doctrine bets (ownership+inference, actors, one binary, io_uring) are landing and measurable, while the surface a developer touches daily (types, tooling, ecosystem) is years behind the languages it benchmarks against.
|
||||
|
||||
## Verification 2026-08-20
|
||||
|
||||
Every claim above was checked against the tree. **26 of 27 hold. One number
|
||||
does not, and two problems are worse than stated.**
|
||||
|
||||
### Rejected: "~40× behind Go on raw compute"
|
||||
|
||||
Unsourced. Nothing in the repo measures compute against Go. The only Go
|
||||
comparison on record runs the other way and on a different workload:
|
||||
[`plan/exploration/c-runtime/00-plan.md`](plan/exploration/c-runtime/00-plan.md)
|
||||
records the C prototype at **908,916 reads/s and 643,250 fsync-acked
|
||||
commits/s on 8 shards vs Go `net/http` at 495k/355k with ~8× worse p99** on a
|
||||
20-core box — I/O-bound serving, not compute. `runtime/bench/goref/` holds a
|
||||
Go reference program, but no compute-bound result from it is written down
|
||||
anywhere.
|
||||
|
||||
The figure also contradicts this document's own closing sentence: the
|
||||
doctrine bets cannot be "measurable" while iteration 22 has never run. Drop
|
||||
the number or produce the benchmark.
|
||||
|
||||
### Understated
|
||||
|
||||
- **`map<K,V>` lookup is a linear scan.** `runtime/src/cont.h`: parallel
|
||||
key/value arrays, "linear scan lookup — deliberate milestone-1 KISS". Every
|
||||
`get`/`has`/`set` is O(n). For a language whose framework routes requests
|
||||
and whose planned cache is keyed, this outranks the missing `Float` as a
|
||||
compute problem — and the compute paragraph never mentions it.
|
||||
- **The multi-shard DB gap is structural, not a missing flag.**
|
||||
`wo_engine_start` (`runtime/src/vm.c`) `memset`s each worker VM to zero, so
|
||||
`rt.db` and `rt.wal` are NULL by construction off the primary;
|
||||
`wo_builtin_db` then returns `WO_T_DB "database engine not initialized"`. It
|
||||
is a clean trap rather than a crash — but any multi-shard program that
|
||||
touches the database is broken today.
|
||||
|
||||
### Confirmed, with corrections to the numbers
|
||||
|
||||
| Claim | Evidence |
|
||||
| --- | --- |
|
||||
| interpreted only, no JIT, no AOT | no `jit` anywhere; `specs/2026-08-01-oop-compiler-vm-design.md` records AOT-to-C as rejected |
|
||||
| no `Float`, no `Bytes` | absent from `compiler/src/types.ml`; no float builtin; `net.read` returns `Text` |
|
||||
| no SIMD | nothing in `runtime/src` or `compiler/src` |
|
||||
| fixed interpreter ceilings | **mislabeled**: 4096 is the value **stack** (`WO_STACK_SLOTS`), registers are 64 per frame (`WO_MAX_REGS`), frames 256 (`WO_MAX_FRAMES`) — all `runtime/src/wob.h`. Unlisted: `WO_MAX_SHARDS 64`, `WO_ARENA_MAX_CLASS 1024`, `WO_MAX_CATCH 64`. ~42 KiB/fiber matches the arc spec |
|
||||
| no generics | `multi T` / `map<K,V>` are runtime-provided native classes by design |
|
||||
| no function values or closures | no such form in the lexer keyword set or typechecker |
|
||||
| byte-based strings, no Unicode | `WO_B_BYTE_AT`, ASCII `WO_B_TO_LOWER`; `json.c` decodes BMP only |
|
||||
| no Result-style errors | traps + `try`/`catch` only |
|
||||
| pattern matching is a switch | `KwSwitch`/`KwCase`; no destructuring form |
|
||||
| `send` is one-way | `WO_B_SEND=69` is the last builtin (`WO_B_MAX 69u`) — no ask/reply opcode |
|
||||
| no supervision, links, actor death | nothing in the runtime |
|
||||
| unbounded mailboxes, no backpressure | `vm.h`: `msgs` is a "FIFO ring, growable"; `mcap` only grows |
|
||||
| no timers beyond sleep | `time.sleep` is the only one; no `timerfd` in the runtime |
|
||||
| round-robin placement, no work stealing | `eng_rr` cursor, `vm.c` |
|
||||
| accept on one shard | one listener, `SO_REUSEADDR` only — no `SO_REUSEPORT` |
|
||||
| no TLS | the only `tls` in the runtime is thread-local storage (`wo_tls_vm`) |
|
||||
| no HTTP/2 or WebSockets | framework `http/` is parse/serve/types/auth/multipart; h2c parked per `00-status.md` |
|
||||
| no crypto primitives | none |
|
||||
| observability is print/stderr | `WO_B_PRINT`, `PRINT_INT`, `PRINT_ERR`; no counters, tracing, or profiler |
|
||||
| no debugger, no LSP | neither exists |
|
||||
| deps git-rev only | no semver, registry, or transitive resolution in the compiler |
|
||||
| blue-green + migrations are futures | iteration 26 still pending |
|
||||
| 22's battery never run | 22 is ⬜ "needs a spec first"; no `bench/baseline.json`, no `just db-bench`; `runtime/bench/` is the retired C prototype's harness |
|
||||
| TSan covers one demo | only `scripts/fibers-accept.sh` builds and runs `wovm_tsan` |
|
||||
| no fuzzing, no CI | no `.github/`, no fuzz target |
|
||||
| one framework, five samples, one consumer | exact: `writeonce-framework`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` |
|
||||
|
||||
### Consequence
|
||||
|
||||
The iteration order in
|
||||
[`stories/language-runtime-database/00-story.md`](stories/language-runtime-database/00-story.md)
|
||||
was re-sequenced against these findings on 2026-08-20 — Seq only, no `#`
|
||||
renumbered, no file moved. See that table's second re-sequencing note.
|
||||
|
|
|
|||
154
docs/00-link-audit.md
Normal file
154
docs/00-link-audit.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Markdown link audit — 2026-08-20
|
||||
|
||||
Scope: every `*.md` in the repo (`.git` excluded).
|
||||
External URLs were not fetched (no network verification performed).
|
||||
|
||||
| | files | relative links | broken paths | bad anchors |
|
||||
|---|---|---|---|---|
|
||||
| first scan | 207 | 574 | 97 | 0 |
|
||||
| after section A fixes | 206 | 569 | **88** | 0 |
|
||||
|
||||
Section A is repaired and verified. Sections B–F are pre-existing rot and
|
||||
still open — every one of the remaining 88 lives there.
|
||||
|
||||
Re-check with `just linkcheck`.
|
||||
|
||||
Tool: `linkcheck.py` — walks the tree, strips fenced/inline code, extracts inline
|
||||
links and reference definitions, resolves each relative target, and validates
|
||||
`#fragment` against GitHub-style heading slugs of the target file.
|
||||
|
||||
---
|
||||
|
||||
## A. Regressions from the in-flight renumber — FIXED 2026-08-20
|
||||
|
||||
All nine broke because files moved in the working tree; each had a known
|
||||
successor. Repaired:
|
||||
|
||||
| Source | Was | Now |
|
||||
|---|---|---|
|
||||
| `docs/00-status.md:167` | `stories/language-runtime-database/05-language-surface.md` | `…/done/05-language-surface.md` |
|
||||
| `docs/00-status.md:187` | `stories/language-runtime-database/18-memory-db-features.md` | `…/hold/18-memory-db-features.md` |
|
||||
| `docs/stories/language-runtime-database/00-story.md:60` | `05-language-surface.md` | `done/05-language-surface.md` |
|
||||
| `docs/stories/language-runtime-database/00-story.md:69` | `18-memory-db-features.md` | `hold/18-memory-db-features.md` |
|
||||
| `docs/stories/language-runtime-database/25-http-service.md:4` | `../00-story.md` | `00-story.md` |
|
||||
| `docs/stories/language-runtime-database/26-blue-green-deploy.md:4` | `../00-story.md` | `00-story.md` |
|
||||
| `.../refine/08-shard-actor-runtime.md:98` | `../hold/09e-durability-throughput-scale.md` | `22-durability-throughput-scale.md` |
|
||||
| `.../refine/08-shard-actor-runtime.md:100` | `09f-io-uring-commit.md` | `23-io-uring-commit.md` |
|
||||
| `.../refine/20-cross-program-tables.md:143` | `../hold/09d-keypair-attach-auth.md` | `21-keypair-attach-auth.md` |
|
||||
|
||||
The `25`/`26` pair used `../00-story.md` while `00-story.md` is a sibling — the
|
||||
`refine/`-relative form pasted into files one level up.
|
||||
|
||||
Link labels were renumbered with their targets, since the old IDs contradicted
|
||||
the new paths: `9e`→`22` and `9f`→`23` in `refine/08` (both the "Gated by the
|
||||
benchmark" note and settled decision 4, "Order: 22 → the 8+11 arc → 23").
|
||||
|
||||
## B. Dead era: the old flat `docs/plan/NN-*.md` numbering (48 links)
|
||||
|
||||
`docs/plan/` now holds only `compiler/`, `exploration/`, `oop-vm/`,
|
||||
`discarded.md`, `learnings.md`. Every flat-numbered plan doc is gone, and no
|
||||
successor path was recorded. Missing targets, by inbound count:
|
||||
|
||||
- `09-concurrency-scaleout.md` — 12
|
||||
- `11-wal-and-recovery.md` — 9
|
||||
- `12-engine-disk-cutover.md` — 8
|
||||
- `10-storage-foundations.md` — 8
|
||||
- `done/02-event-loop-epoll.md` — 4
|
||||
- `13-class-model-live-pricing.md` — 3
|
||||
- `07-inotify-content-watcher.md` — 3
|
||||
- `08-sendfile-static-assets.md` — 2
|
||||
- `15-mcp-streamable-http.md`, `16-postgres-mirror.md`,
|
||||
`done/03-hand-rolled-http.md`, `done/04-cutover-remove-tokio-axum.md` — 1 each
|
||||
|
||||
Inbound from: all of `docs/plan/exploration/{linux,postgresql,c-runtime,assembly}/`,
|
||||
plus `docs/00-principles.md:57,77,78`, `runtime/README.md:47`,
|
||||
`.dev/reference/README.md:55,56,58`.
|
||||
|
||||
**Decision needed** — these exploration docs still cite a plan structure that no
|
||||
longer exists. Either map each to its story successor
|
||||
(e.g. concurrency-scaleout → `stories/.../refine/08-shard-actor-runtime.md`,
|
||||
wal/storage → `refine/22-durability-throughput-scale.md`,
|
||||
io_uring → `refine/23-io-uring-commit.md`) or strip the links and keep prose.
|
||||
|
||||
## C. Dead era: the `docs/runtime/database/` tree (7 links)
|
||||
|
||||
`docs/runtime/` does not exist. Missing targets:
|
||||
|
||||
- `03-inmemory-engine.md` — 5 (incl. one `#recovery` anchor)
|
||||
- `02-wo-language.md` — 2 (incl. one `#concurrency-model` anchor)
|
||||
- `07-wo-seg-migration.md` — 1
|
||||
|
||||
Inbound from `docs/plan/exploration/linux/{07-io_uring,08-mmap,11-memfd_create}.md`,
|
||||
`docs/plan/exploration/{assembly/02-writeonce-stance,c-runtime/02-single-binary}.md`,
|
||||
`runtime/README.md:43`, `.dev/reference/README.md:31`.
|
||||
|
||||
## D. Never-created / removed siblings (5 links)
|
||||
|
||||
| Source | Target | Note |
|
||||
|---|---|---|
|
||||
| `docs/plan/exploration/linux/06-sendfile.md:10` | `./07-splice.md` | slot 07 is `07-io_uring.md`; no splice doc was written |
|
||||
| `docs/plan/exploration/assembly/00-overview.md:19` | `../../../.dev/reference/go/src/runtime/atomic_amd64.s` | wrong depth **and** file absent from the vendored Go tree |
|
||||
| `docs/00-principles.md:87` | `examples/blog/README.md` | `docs/examples/blog/` never existed |
|
||||
| `.dev/reference/rest/README.md:76` | `../../docs/examples/blog/README.md` | same missing example |
|
||||
| `.dev/reference/README.md:41,59` | `../docs/plan/exploration/colibri/00-colibri-and-mixtral.md` | `exploration/colibri/` absent (2 links) |
|
||||
|
||||
## E. `prototypes/` tree gone (4 links)
|
||||
|
||||
`prototypes/` is not in the repo. Referenced as `prototypes/wo-db/` from
|
||||
`docs/plan/exploration/c-runtime/00-plan.md:88`, `02-single-binary.md:83`,
|
||||
`runtime/README.md:5`, and `prototypes/llama-moe-stream` from
|
||||
`.dev/reference/README.md:59`.
|
||||
|
||||
## F. Vendored skill copies — not ours to fix (13 links)
|
||||
|
||||
`.dev/skills/` holds flattened copies of plugin skills. The originals ship as
|
||||
directories with sibling reference files; flattening dropped them.
|
||||
|
||||
- `.dev/skills/context-mode/context-mode.md:297-300` → `./references/{patterns-javascript,patterns-python,patterns-shell,anti-patterns}.md`
|
||||
- `.dev/skills/superpowers/requesting-code-review.md:34,95` → `code-reviewer.md`
|
||||
- `.dev/skills/superpowers/subagent-driven-development.md:232,300,345,400,410` → `implementer-prompt.md`, `task-reviewer-prompt.md`, `re-review-prompt.md` (×2), `../requesting-code-review/code-reviewer.md`
|
||||
- `.dev/skills/superpowers/test-driven-development.md:206` → `writing-good-tests.md`
|
||||
- `.dev/skills/superpowers/writing-skills.md:12,587` → `../using-superpowers/references/{codex,gemini}-tools.md`, `testing-skills-with-subagents.md`
|
||||
|
||||
Leave as-is, or re-vendor the skills with their `references/` subdirectories.
|
||||
|
||||
---
|
||||
|
||||
## Structural problems found alongside the links
|
||||
|
||||
1. **Iteration 19 was double-booked — RESOLVED.**
|
||||
`refine/19-chat-websocket-workload.md` and `refine/24-chat-websocket-workload.md`
|
||||
were the same document, differing only in the `# Iteration NN` heading, while
|
||||
`19-missing-scalar-types.md` also claimed 19. `00-story.md`'s mapping line
|
||||
(`24←19(chat)`) and table row 20 make **24 canonical**, so the 19 copy was
|
||||
deleted. `refine/11-fibers.md:13` had been pointing at the 19 copy — repointed
|
||||
to 24 first, so the delete broke nothing. Prose in `refine/08` that named
|
||||
"iteration 19" for chat now says 24 (4 places).
|
||||
|
||||
2. **`08-shard-actor-runtime.md` existed twice — RESOLVED.**
|
||||
58 lines at the stories root vs 110 in `refine/`. The `refine/` copy supersedes
|
||||
it outright: same acceptance criteria plus the 2026-08-20 settled decisions, the
|
||||
inferred-GC restatement (7b retired `@gc`, which the root copy still required),
|
||||
and the corrected substrate path (the root copy cited `runtime/wo-rt.c`, removed
|
||||
with the Rust runtime). Root copy deleted; the one inbound link,
|
||||
`docs/00-status.md:171`, now points at `refine/`. Six other referrers already did.
|
||||
|
||||
3. **Unresolved merge-conflict markers were committed** into
|
||||
`refine/20-cross-program-tables.md:139-145` — `<<<<<<<< HEAD:… / ======== /
|
||||
>>>>>>>> language-surface-strictness:…/hold/09c-cross-program-tables.md`, from a
|
||||
rename-conflicted merge. This is what produced that file's broken `09d` link:
|
||||
the stale side was still in the file. Resolved in favour of HEAD (the renumbered
|
||||
`21` text). `grep` confirms no other conflict markers under `docs/`.
|
||||
|
||||
## Still open
|
||||
|
||||
- Sections B–F above: 88 broken links, all pre-existing.
|
||||
- `docs/plan/discarded.md` and `docs/plan/learnings.md` are the only survivors of
|
||||
the old flat plan layout, which is why B and C have no successor map. A rename
|
||||
table in one of them would let the exploration docs be repaired mechanically
|
||||
rather than by guesswork.
|
||||
- `docs/00-status.md:171` still shows iteration 8 as ⬜ while `00-story.md:68`
|
||||
records arc stages 1+2 as landed 2026-08-20. Not a link problem — a status
|
||||
disagreement between the two index docs. Left alone.
|
||||
- `refine/23-io-uring-commit.md:26` still quotes the old order as
|
||||
"9e → 8+11 → 9f" in a dated note. No link involved; left as historical record.
|
||||
|
|
@ -164,13 +164,14 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
| 2 | [VM core (`wovm`)](stories/language-runtime-database/done/02-vm-core.md) | ✅ |
|
||||
| 3 | [Compiler front (`woc`)](stories/language-runtime-database/done/03-compiler-front.md) | ✅ (known gaps below) |
|
||||
| 4 | [Single binary end-to-end](stories/language-runtime-database/done/04-single-binary-e2e.md) | ✅ (known gaps below) |
|
||||
| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 grammar done; **`?T` forced handling ✅ + reject rows ✅ + WO-E205 ✅ (2026-08-18)**; `pub(read)`/`using`/`#if` still ⏸ |
|
||||
| 5 | [Language surface](stories/language-runtime-database/done/05-language-surface.md) | 🔄 grammar done; **`?T` forced handling ✅ + reject rows ✅ + WO-E205 ✅ (2026-08-18)**; `pub(read)`/`using`/`#if` still ⏸ |
|
||||
| 6 | [Program mode + stdlib](stories/language-runtime-database/done/06-program-mode-stdlib.md) | ✅ (the surface log-watcher uses) |
|
||||
| 7 | [log-watcher proof](stories/language-runtime-database/done/07-logwatcher-proof.md) | ✅ **landed 2026-08-15** — executable, not merely compilable: zero ASan leaks in all three modes, SIGTERM ends parked syscalls, fds flat, `LW_SOAK` gate; `just log-watcher` 7/0 |
|
||||
| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/done/07b-inferred-gc-mark-sweep.md) | ✅ **landed 2026-08-18** — `@gc` gone (WO-E104), GC-ness inferred, RC replaced by incremental mark-sweep, `.wob` v4; supersedes iteration 2's RC memory model |
|
||||
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
|
||||
| 8 | [Shard-actor runtime](stories/language-runtime-database/refine/08-shard-actor-runtime.md) | ⬜ |
|
||||
| 9 | [Database engine](stories/language-runtime-database/done/09-database-engine.md) | 🔄 engine complete (storage/WAL/indexes/insert-update-delete); reads land with 9b |
|
||||
| 9b | [`@table`, relations, query](stories/language-runtime-database/done/09b-table-relations-query.md) | 🔄 query surface + relations + FK done (branch query-surface); group-by parked |
|
||||
| 19 | [Float + Bytes](stories/language-runtime-database/done/19-missing-scalar-types.md) | ✅ **landed 2026-08-20** — `.wob` v5: Float constant tag, field kinds 6/7, opcodes 34-41 (IEEE-quiet f64), builtins 70-83. Full stack: literals, arithmetic, `@table` column, WAL bit-exact replay, json fractions in / shortest-round-trip out, `?Float` reserved-NaN nil, total-order index (NaN last, `-0.0` == `+0.0`), Bytes + base64. No implicit Int/Float mixing (WO-E201); `float`/`trunc` are the only bridges. Proof: web-app price is a real Float (`{"price":9.99}`), `just web-app` 23/0; corpus 103/0 |
|
||||
| 20 | [Cross-program tables](stories/language-runtime-database/refine/20-cross-program-tables.md) | 🔄 channel done (branch ipc-attach); manifest+binding pending |
|
||||
| 21 | [Keypair attach auth](stories/language-runtime-database/refine/21-keypair-attach-auth.md) | 🔄 crypto+handshake done (branch keypair-auth); manifest pending |
|
||||
| 22 | [Durability, throughput, scale](stories/language-runtime-database/refine/22-durability-throughput-scale.md) | ⬜ needs a spec first |
|
||||
|
|
@ -183,8 +184,8 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
| 14 | [skillhost host workload](stories/language-runtime-database/refine/28-skillhost-host-workload.md) | ⬜ gaps recorded (branch query-grammar found skillhost needs no new query grammar); each gap a candidate iteration |
|
||||
| 15 | [deps: `wo.toml [deps]`](stories/language-runtime-database/done/15-deps-package-manager.md) | ✅ **landed 2026-08-18** (branch web-framework): [deps] inline tables, git-binary fetch, wo.lock pinning, offline-when-locked, --update-deps, WO-E106/E107; `just deps-accept` 8/0 |
|
||||
| 16 | [web framework](stories/language-runtime-database/done/16-web-framework.md) | ✅ **landed 2026-08-19** — writeonce-framework (HTTP/1.1 + router + Handler/Middleware) consumed by web-app through [deps]; h2c parked (§C) behind 8/23/11. **v1 polish landed 2026-08-20** (branch framework-v1): get/post/put/delete_ helpers, 405+Allow, HEAD, Logging middleware, set_header; `just web-app` 16/0; fixed the interp-borrowed-field emitter crash en route. **Auth-in-core landed 2026-08-20**: http/auth.wo (Bearer/Basic, ct_eq, req.principal), web-app dogfoods BearerAuth, gate 17/0 |
|
||||
| 17 | [library projects + `internal/`](stories/language-runtime-database/17-library-projects-internal.md) | ⏸ **PARKED 2026-08-20** (developer directive; framework v1 first) — forks settled, spec + plan approved and ready on branch `library-internal`: kind = "library" key; Go internal/ rule, dep-boundary-only; lib+bin dual; VM/GC untouched by design |
|
||||
| 18 | [framework v2: memory-rich features](stories/language-runtime-database/18-memory-db-features.md) | 🔄 **spec APPROVED 2026-08-20, plan next** ([spec](superpowers/specs/2026-08-20-memory-db-features-design.md)): TTL cache + @table flags + durable job queue (drain-on-request) + `transaction { }` over the WAL's staged batch; pub/sub REJECTED until 8/11 |
|
||||
| 17 | [library projects + `internal/`](stories/language-runtime-database/done/17-library-projects-internal.md) | ✅ **landed 2026-08-20** — `kind = "library"` in `wo.toml` (default `program`, so every existing manifest is byte-identical; unknown value = WO-E109 exit 2); `woc <dir>` on a library runs the FULL pipeline entry-less and writes nothing, retiring iteration 16's `--emit` workaround; the no-entry build error names the kind; lib+bin dual works. Go's `internal/` rule as **WO-E108** at the consumer's own `use`, dep-boundary-only — the library imports its own interior freely. Framework reorganized: `internal/{parse,serve}.wo` behind the line, `http/form.wo` split out to keep `media_type`/`form_values` public. Driver-only change; VM/`.wob`/GC untouched. `just web-app` **26/0** (3 new checks), every standing gate unchanged |
|
||||
| 18 | [framework v2: memory-rich features](stories/language-runtime-database/hold/18-memory-db-features.md) | 🔄 **spec APPROVED 2026-08-20, plan next** ([spec](superpowers/specs/2026-08-20-memory-db-features-design.md)): TTL cache + @table flags + durable job queue (drain-on-request) + `transaction { }` over the WAL's staged batch; pub/sub REJECTED until 8/11 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -192,7 +193,7 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
|
||||
| Track | Item | Where |
|
||||
| -------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| Language | nothing active — the framework v1-polish slice landed 2026-08-20 (branch framework-v1, awaiting merge); next per the order: brainstorm 20/21's forks | [order](#implementation-order-re-sequenced-2026-08-20--framework-goal) |
|
||||
| Language | nothing active — the framework v1-polish slice landed 2026-08-20 (branch framework-v1, awaiting merge); next per the order: brainstorm 20/21's forks | [order](#implementation-order-re-sequenced-2026-08-20--code-review-pass) |
|
||||
|
||||
Off-goal work is parked; the goal (2026-08-20) is the web framework as a
|
||||
polished micro-framework v1 — iteration 17 (library kind + `internal/`) is
|
||||
|
|
@ -359,48 +360,53 @@ The C proving-ground work (`exploration/c-runtime/`, phases A–F: 859k reads/s,
|
|||
|
||||
## Pending
|
||||
|
||||
### Implementation order (re-sequenced 2026-08-20 — framework goal)
|
||||
### Implementation order (re-sequenced 2026-08-20 — code-review pass)
|
||||
|
||||
The goal is the web framework as a first-class library, so the framework
|
||||
line leads and the workload-driven extras (14, 27) demote behind it.
|
||||
Dependency rules that force the shape: 23 explicitly after 8 + 22; 20
|
||||
"precedes iteration 25"; 21's plan folds into 20's; 12 only after 9 + 10
|
||||
(catalog to diff, HTTP to build on); 11 rides 8's shard scheduler; h2c
|
||||
parked behind 8/23/11; the post-12 parked list stays parked by the
|
||||
2026-08-08 scope directive. (Iteration 7 dropped from this list — landed
|
||||
2026-08-15.)
|
||||
Replaces the framework-goal ordering. Basis: the verified findings in
|
||||
[`00-code-review.md`](00-code-review.md) — measure before optimizing, close
|
||||
correctness holes before adding surface, stop stacking features on
|
||||
unmeasured ground. IDs below are post-renumber; the authoritative table with
|
||||
per-row reasoning is
|
||||
[`00-story.md`](stories/language-runtime-database/00-story.md).
|
||||
|
||||
1. ~~**17**~~ — **PARKED 2026-08-20** (developer directive: framework v1
|
||||
first); spec + plan approved, ready on branch `library-internal` for
|
||||
whenever it unparks — slots anywhere after 16. The framework v1 slices
|
||||
took its place and landed the same day (branch framework-v1,
|
||||
`just web-app` 21/0).
|
||||
2. **18** — framework v2 (transaction{} + cache/flags/jobs); spec
|
||||
APPROVED 2026-08-20, the only all-green spec-approved node in the
|
||||
[dependency graph](00-dependency-graph.md) — plan next, then
|
||||
implement.
|
||||
3. **20 then 21** — finish the half-done branches (ipc-attach: manifest +
|
||||
binding; keypair-auth: manifest) before they rot; 21 folds into 20's
|
||||
plan; both must precede 10.
|
||||
4. **22** — the measurement backbone; baseline single-shard BEFORE the
|
||||
runtime restructure so 8/23/11 sign against real numbers.
|
||||
5. **8** — shard-actor; the framework's multi-core serving story; unblocks
|
||||
23, 11, h2c.
|
||||
6. **23** — io_uring group-commit; explicitly after 8 + 22.
|
||||
7. **11** — fibers; retires the framework's disclosed keep-alive limit (an
|
||||
idle connection starving accept forced close-when-idle in iteration 16 —
|
||||
parked fds fix it properly); with 8 + 23 done, **h2c unparks** (spec §C)
|
||||
as the framework's HTTP/2 slice.
|
||||
8. **10** — HTTP service layer; after 20 by its own precedence note;
|
||||
`service` blocks lower onto the framework instead of a parallel stack.
|
||||
9. **12** — blue-green; prerequisites 9 + 10 now exist; completes the
|
||||
framework's deploy story.
|
||||
10. **27 then 14** — demoted with the goal shift: skillhost (28) is no longer the
|
||||
driving workload; 27 likely collapses to "confirm `len(query)` + add
|
||||
`exists`" and precedes 28 when they run.
|
||||
11. **13 + parked drain** — metaprogramming (spec first), then group-by,
|
||||
`pub(read)`/`using`/`#if`, ADT roster, WO-E225 — held behind 26 by the
|
||||
scope directive.
|
||||
Dependency rules that still force the shape: 23 explicitly after 8 + 22;
|
||||
21's plan folds into 20's; 26 only after 9 + 25; 11 rides 8's shard
|
||||
scheduler; h2c parked behind 8/23/11.
|
||||
|
||||
1. **22** — the measurement backbone, and now first: it has never run, so
|
||||
every performance claim on this project is unsourced. No
|
||||
`bench/baseline.json`, no `just db-bench`; `runtime/bench/` is the
|
||||
retired C prototype's harness.
|
||||
2. **8+11 stage 3** — transparent DB RPC, then 22 re-run for the
|
||||
concurrency delta. Reframed as a correctness fix: worker VMs are
|
||||
zero-initialized, so a DB statement off the primary traps `WO_T_DB`.
|
||||
A multi-shard program that touches the database is broken today.
|
||||
3. **30** (new) — observability, CI, fuzz: runtime counters + a profiler
|
||||
hook, 22's harness run per change instead of by hand, a fuzz target on
|
||||
the parser and `.wob` loader. No iteration covered any of this.
|
||||
4. **19** — Float + Bytes; small, and it gates 24 (WS frames) and the
|
||||
crypto fork (digests).
|
||||
5. **31** (new) — actor lifecycle: request/response (`send` is one-way and
|
||||
callers `sleep` to await), bounded mailboxes (the FIFO only grows),
|
||||
actor death/supervision, timers beyond `time.sleep`.
|
||||
6. **24** — chat, the arc's acceptance; honest only after 19 + 31.
|
||||
7. **23** — io_uring group-commit; explicitly after 8 + 22.
|
||||
8. **25** — HTTP service layer; `service` blocks lower onto the framework
|
||||
instead of a parallel stack.
|
||||
9. **18** — framework v2 (transaction{} + cache/flags/jobs); spec APPROVED
|
||||
2026-08-20 but **demoted from first**: more surface on a framework with
|
||||
one consumer, and its cache stores `Text` because there are no generics.
|
||||
10. **27, then 26** — query grammar from corpora (likely collapses to
|
||||
"confirm `len(query)` + add `exists`"), then blue-green.
|
||||
11. **20 then 21** — demoted hard: new distribution surface while there is
|
||||
no TLS, no crypto primitives, and the multi-shard DB still traps. The
|
||||
half-done branches (ipc-attach, keypair-auth) keep their manifests.
|
||||
12. **28, then 29 + parked drain** — skillhost is no longer the driving
|
||||
workload; then metaprogramming (spec first), group-by, ADT roster,
|
||||
WO-E225, held by the 2026-08-08 scope directive.
|
||||
|
||||
✅ **17** — landed 2026-08-20 (unparked and executed): `kind = "library"`,
|
||||
check mode, and the `internal/` dep boundary (WO-E108). Driver-only.
|
||||
|
||||
### Language track — sequenced, on the critical path
|
||||
|
||||
|
|
@ -418,7 +424,7 @@ parked behind 8/23/11; the post-12 parked list stays parked by the
|
|||
| 23 | io_uring group-commit write path — batched durability overlapped on shard threads, fsync fallback | **no spec yet** — brainstorm after iterations 8 + 22 |
|
||||
| 27 | Query grammar from real embedded-DB corpora — whole-query count + correlated exists, driven by the skillhost SQL catalogue; add only what a corpus uses | **no spec yet** — three forks; may collapse to "confirm len(query) + add exists" |
|
||||
| 14 | skillhost host workload — port skillhost (MCP host + confined script runner) to writeonce; drives the missing host capabilities into the open (bounded subprocess, stdin/stdout transport, fs metadata, FFI-vs-out-of-process) | **no spec yet** — gaps recorded in the iteration; each gap brainstormed on demand, bounded-subprocess first |
|
||||
| 17 | library projects + dependency privacy — `wo.toml` kind = "library" (checkable without entry, dual lib+bin) + Go-style `internal/` at the [deps] boundary; framework reorg demonstrates both | **forks settled 2026-08-20** — decisions + framework/compiler/VM/GC impact in the iteration; spec/plan next |
|
||||
| 17 | library projects + dependency privacy — `wo.toml` kind = "library" (checkable without entry, dual lib+bin) + Go-style `internal/` at the [deps] boundary; framework reorg demonstrates both | ✅ **landed 2026-08-20** — [spec](superpowers/specs/2026-08-20-library-kind-internal-design.md) · [plan](superpowers/plans/2026-08-20-library-kind-internal.md) |
|
||||
| 10 | HTTP service layer | [plan 6](superpowers/plans/2026-08-01-http-service-layer.md) |
|
||||
| 11 | Fibers | vision §3, [blue-green exploration](plan/exploration/blue-green-vm/00-vision.md) |
|
||||
| 12 | Blue-green deploy | [spec](superpowers/specs/2026-08-03-blue-green-vm-design.md) — plan authored after iterations 9 + 25 |
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ use framework/http
|
|||
use framework/router
|
||||
|
||||
-- decode target for POST /products, encode shape for every product answer
|
||||
typedef ProductView = { name: Text, price: Int, stock: Int }
|
||||
typedef ProductView = { name: Text, price: Float, stock: Int }
|
||||
typedef NewOrder = { product: Text, qty: Int }
|
||||
|
||||
fn view_json(name: Text, price: Int, stock: Int) -> Text {
|
||||
fn view_json(name: Text, price: Float, stock: Int) -> Text {
|
||||
return json.encode(ProductView { name: name, price: price, stock: stock });
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ class ShowProduct {
|
|||
|
||||
-- Accepts THREE bodies: multipart/form-data (curl -F), a form post
|
||||
-- (application/x-www-form-urlencoded), and JSON — same insert either way.
|
||||
fn create_product(name: Text, price: Int, stock: Int) -> Resp {
|
||||
fn create_product(name: Text, price: Float, stock: Int) -> Resp {
|
||||
let made = try insert Product { name: name, price: price, stock: stock }
|
||||
catch (e) nil;
|
||||
if made == nil { return conflict("product name already exists"); }
|
||||
|
|
@ -63,8 +63,10 @@ class CreateProduct {
|
|||
if pstr == nil { return bad_request("multipart needs name, price, stock"); }
|
||||
let sstr = part_named(ps, "stock");
|
||||
if sstr == nil { return bad_request("multipart needs name, price, stock"); }
|
||||
let price = parse_int(pstr);
|
||||
if price == nil { return bad_request("price must be a number"); }
|
||||
-- parse_float answers NaN for unparseable input rather than a ?Float:
|
||||
-- "not a number" is already a Float value, and NaN != NaN is the test
|
||||
let price = parse_float(pstr);
|
||||
if price != price { return bad_request("price must be a number"); }
|
||||
let stock = parse_int(sstr);
|
||||
if stock == nil { return bad_request("stock must be a number"); }
|
||||
return create_product(name, price, stock);
|
||||
|
|
@ -78,8 +80,8 @@ class CreateProduct {
|
|||
if ps == nil { return bad_request("form needs name, price, stock"); }
|
||||
let ss = f["stock"];
|
||||
if ss == nil { return bad_request("form needs name, price, stock"); }
|
||||
let price = parse_int(ps);
|
||||
if price == nil { return bad_request("price must be a number"); }
|
||||
let price = parse_float(ps);
|
||||
if price != price { return bad_request("price must be a number"); }
|
||||
let stock = parse_int(ss);
|
||||
if stock == nil { return bad_request("stock must be a number"); }
|
||||
return create_product(name, price, stock);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@
|
|||
@table(name: "products", index: [name])
|
||||
class Product {
|
||||
name: Text @unique
|
||||
price: Int -- cents
|
||||
-- iteration 19: a real Float price, not cents-as-Int. This column IS the
|
||||
-- iteration's living proof — `{"price": 9.99}` posted here used to fail the
|
||||
-- whole checked decode because the language had no Float at all.
|
||||
price: Float
|
||||
stock: Int
|
||||
|
||||
orders: backlink Order.product
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework",
|
|||
|
||||
## What it is
|
||||
|
||||
- **HTTP/1.1** server core (`http/`): request parsing (`Content-Length`
|
||||
- **HTTP/1.1** server core: request parsing (`Content-Length`
|
||||
bodies, %-decoded paths and query strings), response serialization, a
|
||||
blocking serve loop that answers 400 to malformed requests, 500 to
|
||||
trapping handlers (and survives), closes every fd, and honors SIGTERM.
|
||||
|
|
@ -143,6 +143,30 @@ first (pure `.wo` cannot express it yet).
|
|||
| SHA-256 · SHA-512 · HMAC · CRC32 | 🔧 the language has NO bitwise operators — these are C runtime builtins (libc-only doctrine permits hand-rolled crypto in the runtime) or the language grows bit ops first; the fork goes to a brainstorm before the slice |
|
||||
| Unlocks (signed cookies, CSRF, session integrity, webhook verification, JWT HS256) | ⬜ framework slices AFTER the hash primitives exist; **hard stop there** — no RS256, no JOSE zoo |
|
||||
|
||||
## Layout and privacy (iteration 17)
|
||||
|
||||
This project declares `kind = "library"` in `wo.toml`, so `woc <dir>` runs the
|
||||
FULL pipeline over it — parse, typecheck, interface satisfaction, ownership, GC
|
||||
inference — with no `fn main` required, and writes nothing. That retired
|
||||
iteration 16's `woc --emit` verification workaround. `woc build` on it fails
|
||||
naming the kind, unless a demo `main` is added (lib+bin is allowed).
|
||||
|
||||
- `http/` — the public surface: `Req`/`Resp` and the response builders
|
||||
(`types.wo`), auth (`auth.wo`), multipart (`multipart.wo`), and the
|
||||
body-inspection pair `media_type`/`form_values` (`form.wo`).
|
||||
- `router/` — the route table and `Logging`.
|
||||
- `app.wo` — `App`, the registration helpers, the dispatch loop.
|
||||
- **`internal/` — not importable by a consumer.** The connection-level request
|
||||
parser and carry-state record (`parse.wo`) and the serve loop, status text,
|
||||
and response serializer (`serve.wo`) live here. A consuming app that writes
|
||||
`use writeonce-framework/internal` gets **WO-E108** at that `use`. The rule
|
||||
is Go's: a path segment named `internal` is refused across the `[deps]`
|
||||
boundary only — the framework's own modules import it freely.
|
||||
|
||||
One honest disclosure: privacy restricts NAMING, not code size. `internal/`
|
||||
modules still compile into the consumer's single image (there is no dead-code
|
||||
elimination); a consumer simply cannot name them.
|
||||
|
||||
## The consuming sample
|
||||
|
||||
`docs/examples/web-app` — a small storefront importing this framework
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
-- app.wo — the assembly: an App holds the middleware chain and the route
|
||||
-- table, satisfies http's Dispatcher interface, and serves.
|
||||
-- table, satisfies internal's Dispatcher interface, and serves.
|
||||
--
|
||||
-- let app = App { middleware: [], routes: [] };
|
||||
-- app.use_mw(Mw { m: Auth { token: t } });
|
||||
|
|
@ -12,6 +12,9 @@
|
|||
-- and the server survives.
|
||||
use http
|
||||
use router
|
||||
-- iteration 17: the serve loop is library-internal now (internal/serve.wo).
|
||||
-- Legal here: the `internal/` boundary refuses CONSUMERS, not the library.
|
||||
use internal
|
||||
|
||||
pub class App {
|
||||
middleware: multi Mw
|
||||
|
|
@ -67,6 +70,6 @@ pub class App {
|
|||
}
|
||||
|
||||
fn serve(host: Text, port: Int) -> Int {
|
||||
return http.serve(host, port, self);
|
||||
return internal.serve(host, port, self);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
docs/examples/writeonce-framework/http/form.wo
Normal file
28
docs/examples/writeonce-framework/http/form.wo
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- http/form.wo — the public body-inspection surface: what media type a
|
||||
-- request carries, and form-encoded bodies as decoded pairs.
|
||||
--
|
||||
-- PUBLIC by design (iteration 17). It sits here rather than in
|
||||
-- `internal/parse.wo` with the rest of the parsing code because these two
|
||||
-- functions are exactly what a consuming app calls; the decoders they lean on
|
||||
-- stay behind the privacy line. `use internal` is legal INSIDE the library —
|
||||
-- the boundary is consumer-only.
|
||||
use internal
|
||||
|
||||
-- The request's media type: the content-type header lowercased with any
|
||||
-- parameters ("; charset=...") stripped; "" when the header is absent.
|
||||
pub fn media_type(req: Req) -> Text {
|
||||
let ct = req.headers["content-type"];
|
||||
if ct == nil { return ""; }
|
||||
let semi = index_of(ct, ";");
|
||||
if semi >= 0 { return to_lower(trim(substr(ct, 0, semi))); }
|
||||
return to_lower(trim("${ct}"));
|
||||
}
|
||||
|
||||
-- Form-encoded body -> decoded pairs ('+' as space, %XX), the body-parsing
|
||||
-- hook for application/x-www-form-urlencoded. nil when the content-type
|
||||
-- says the body is something else — a JSON body is not silently misread
|
||||
-- as one giant form key.
|
||||
pub fn form_values(req: Req) -> ?map<Text, Text> {
|
||||
if media_type(req) != "application/x-www-form-urlencoded" { return nil; }
|
||||
return parse_query(req.body);
|
||||
}
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
-- http/parse.wo — HTTP/1.1 request parsing over a net connection.
|
||||
-- internal/parse.wo — HTTP/1.1 request parsing over a net connection.
|
||||
--
|
||||
-- INTERNAL (iteration 17): a consumer cannot `use` this module — the
|
||||
-- `internal/` segment makes that WO-E108. The connection-level parser, the
|
||||
-- carry-state record, and the %XX/query decoders are the framework's own
|
||||
-- business; `media_type`/`form_values` are the public half and live in
|
||||
-- `http/form.wo`.
|
||||
--
|
||||
-- Bounded reads only (`net.read`), so requests are buffered to the header
|
||||
-- terminator, then the body to exactly Content-Length. Keep-alive means
|
||||
|
|
@ -10,6 +16,7 @@
|
|||
-- ok=false malformed request — answer 400 and close
|
||||
-- ok=true, req non-nil one complete request
|
||||
use net
|
||||
use http -- Req lives in the public module now
|
||||
|
||||
const BODY_MAX = 1048576
|
||||
|
||||
|
|
@ -56,7 +63,7 @@ pub fn url_decode(t: Text, plus_space: Bool) -> Text {
|
|||
}
|
||||
|
||||
-- "a=1&b=hello+world" -> decoded pairs; a bare key maps to ""
|
||||
fn parse_query(qs: Text) -> map<Text, Text> {
|
||||
pub fn parse_query(qs: Text) -> map<Text, Text> {
|
||||
let q: map<Text, Text> = {};
|
||||
if qs == "" { return q; }
|
||||
for pair in split(qs, "&") {
|
||||
|
|
@ -71,25 +78,6 @@ fn parse_query(qs: Text) -> map<Text, Text> {
|
|||
return q;
|
||||
}
|
||||
|
||||
-- The request's media type: the content-type header lowercased with any
|
||||
-- parameters ("; charset=...") stripped; "" when the header is absent.
|
||||
pub fn media_type(req: Req) -> Text {
|
||||
let ct = req.headers["content-type"];
|
||||
if ct == nil { return ""; }
|
||||
let semi = index_of(ct, ";");
|
||||
if semi >= 0 { return to_lower(trim(substr(ct, 0, semi))); }
|
||||
return to_lower(trim("${ct}"));
|
||||
}
|
||||
|
||||
-- Form-encoded body -> decoded pairs ('+' as space, %XX), the body-parsing
|
||||
-- hook for application/x-www-form-urlencoded. nil when the content-type
|
||||
-- says the body is something else — a JSON body is not silently misread
|
||||
-- as one giant form key.
|
||||
pub fn form_values(req: Req) -> ?map<Text, Text> {
|
||||
if media_type(req) != "application/x-www-form-urlencoded" { return nil; }
|
||||
return parse_query(req.body);
|
||||
}
|
||||
|
||||
fn malformed(rest: Text) -> Parsed {
|
||||
return Parsed { closed: false, ok: false, req: nil, rest: rest };
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
-- http/serve.wo — response serialization + the blocking keep-alive serve
|
||||
-- internal/serve.wo — response serialization + the blocking keep-alive serve
|
||||
-- loop. The dispatch seam is the Dispatcher interface (the router's App
|
||||
-- satisfies it, Task 3); it is wrapped in `try`, so a trapping handler
|
||||
-- answers 500 and the loop lives — a bad request must never kill the
|
||||
|
|
@ -7,6 +7,10 @@
|
|||
-- in a blocking call already unwinds cleanly — the runtime's stop story).
|
||||
use net
|
||||
use env
|
||||
-- iteration 17: serve.wo moved under internal/, so Req/Resp and the response
|
||||
-- builders are no longer same-module — they live in the public `http` module.
|
||||
use http
|
||||
use internal
|
||||
|
||||
pub interface Dispatcher {
|
||||
fn dispatch(mut req: Req) -> Resp
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
name = "writeonce-framework"
|
||||
kind = "library"
|
||||
version = "0.1.0"
|
||||
description = "A web framework written in writeonce: HTTP/1.1 keep-alive server core, router with :param captures, Handler/Middleware structural interfaces (iteration 16)"
|
||||
|
||||
[runtime]
|
||||
wo = ">= 0.1"
|
||||
|
||||
# A LIBRARY project: no `fn main` here — the consuming app owns the entry.
|
||||
# A LIBRARY project (declared above since iteration 17): no `fn main` here —
|
||||
# the consuming app owns the entry. `woc <dir>` typechecks the whole project.
|
||||
# Apps import this repo through `wo.toml [deps]` (iteration 15) and
|
||||
# `use writeonce-framework` / `use writeonce-framework/http` / `.../router`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Haxe-Parity Language Adoptions Implementation Plan
|
||||
|
||||
> **Status: ⏸ on hold behind [the executable plan](2026-08-14-logwatcher-executable.md)** (story iteration 5) — the grammar half of this plan is what let the driving workload compile, and the rest of it makes the language *refuse* more rather than making that program *run*, so it waits. Original status follows. — Tasks 1–4 ✅ shipped and review-verified: modules (`use`/`pub`), language surface (`and`/`or`, `${}` interpolation, `const`, break/continue, do-while), switch expressions, typedef records + enum payload variants. Task 5 (try/catch over the trap system) ✅ shipped 2026-08-14 — VM catch frames, expression and block catch arms, the `{code, line, method, msg}` record. Task 6 (`?T`) ✅ complete 2026-08-18 (branch `nullable-enforcement`): forced handling enforced — WO-E211/E212/E213 emit, locals narrow via `!= nil` guards / diverging early-return / `and`-chains / `while`; field places bind to a local first. Boxed scalar cells were superseded by `WO_NIL_SCALAR` before this task ran. Task 7 🔶 half: `static` members and `pub(read)` syntax landed, `using` and the `pub(read)` write check did not. Task 8: reject rows ✅ 2026-08-18 (WO-E105 doctrine diagnostics at class headers, expression heads, top-level heads; `Dynamic`/`untyped` type names via WO-E225's doctrine message; corpus `compile-fail/reject-*`); `#if` build flags still ⬜. Board: [00-status.md](../../00-status.md)
|
||||
> **Status: ✅ COMPLETE 2026-08-20** (story iteration 5, branch `language-surface-strictness`) — every task closed. Tasks 1–4 ✅ (modules, small surface, switch expressions, records+variants); Task 5 ✅ try/catch (2026-08-14); Task 6 ✅ `?T` forced handling (2026-08-18, WO-E211/212/213 + narrowing); Task 7 ✅ statics + `pub(read)` syntax (2026-08-14), write enforcement WO-E219 and `using` extensions with WO-E220 collision (2026-08-20 — compile-time rewrite to a free-fn call, owner/emit untouched); Task 8 ✅ reject rows WO-E105 (2026-08-18) and `#if` build flags (`woc -D name`, token-level, WO-E003 misuse; 2026-08-20). `is`/`throw` cut, `abstract` rejected. Board: [00-status.md](../../00-status.md)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@
|
|||
|
||||
All integers little-endian; offsets are absolute file offsets.
|
||||
|
||||
**Header (44 bytes):** magic `"WOB1"`, version 4, 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 5 (iteration 19; see "v5: Float and Bytes" below), 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); tag 2 = f64 as its IEEE 754 bit pattern in an LE u64 (v5). There is no Bytes tag: Bytes has no literal form.
|
||||
|
||||
**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; `0xFFFFFFFD` a nullable scalar (`WO_NIL_SCALAR` nil); `0xFFFFFFFC` a plain `Bool` (json encodes `true`/`false`); `0xFFFFFFFB` a `?Bool` (both); all-ones for none.
|
||||
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; `0xFFFFFFFD` a nullable scalar (`WO_NIL_SCALAR` nil); `0xFFFFFFFC` a plain `Bool` (json encodes `true`/`false`); `0xFFFFFFFB` a `?Bool` (both); `0xFFFFFFFA` a `?Float` (v5 — nil is `WO_NIL_FLOAT`, not `WO_NIL_SCALAR`); 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.
|
||||
Field kinds: 0 SCALAR, 1 OWNED, 2 GCREF, 3 TEXT, 4 MULTI, 5 MAP, **6 FLOAT, 7 BYTES** (v5). 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.
|
||||
|
||||
|
|
@ -53,6 +53,8 @@ The metadata exists for exactly one reason: `json.encode`/`json.decode` are runt
|
|||
| 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 |
|
||||
| 34–38 | FADD/FSUB/FMUL/FDIV/FNEG | f64 arithmetic on the register's bits (v5). **None of these trap**: IEEE 754 quiet semantics, so `x/0.0` is ±Inf and `0.0/0.0` is NaN. FNEG flips the sign bit, so `-0.0` is reachable |
|
||||
| 39–41 | FEQ/FLT/FLE | f64 IEEE compares, result 0/1 — so any comparison involving NaN is 0, and `0.0 == -0.0` is 1. Not a total order; indexes and order-by use the `float_cmp` builtin instead |
|
||||
|
||||
**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).
|
||||
|
||||
|
|
@ -70,6 +72,72 @@ The metadata exists for exactly one reason: `json.encode`/`json.decode` are runt
|
|||
|
||||
**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).
|
||||
|
||||
## v5: Float and Bytes (iteration 19)
|
||||
|
||||
The version bump is real: an image written before this iteration is rejected,
|
||||
and so is one written after it by an older runtime. Both directions are
|
||||
deliberate — the new kind bytes and the new constant tag would be silently
|
||||
misread otherwise, and a misread f64 is a plausible-looking wrong number
|
||||
rather than a crash.
|
||||
|
||||
**What v5 adds**
|
||||
|
||||
- Constant tag `2` — an f64 as its raw IEEE 754 bits in an LE u64. Bits, not
|
||||
a decimal rendering, so a literal reaches the VM exactly as written and no
|
||||
parse/print round trip sits between source and register.
|
||||
- Field kind `6 FLOAT` — word-shaped like SCALAR (the slot holds f64 bits), but
|
||||
its own kind because three services cannot guess from a register alone: json
|
||||
(a Float field must emit `9.99`, not `4621...`), the WAL (replay must not
|
||||
reinterpret the word), and printing.
|
||||
- Field kind `7 BYTES` — pointer-shaped like TEXT and sharing the `wo_str`
|
||||
object layout byte for byte, differing only in the header's `class_id`
|
||||
(`WO_CLS_BYTES`). That distinct id is what lets a Text builtin refuse a Bytes
|
||||
and vice versa; `WO_B_TEXT_COPY` preserves the kind, so every
|
||||
copy-on-ownership-boundary already handles both.
|
||||
- `field_class` marker `0xFFFFFFFA` — a `?Float`. Nil is `WO_NIL_FLOAT`, a
|
||||
reserved quiet NaN (`0x7FF8000000000EE1`), because neither of the existing
|
||||
sentinels works: the zero word is `+0.0`, and `WO_NIL_SCALAR`'s bit pattern
|
||||
*is* `-2.0`. A computed NaN is the platform's canonical quiet NaN, so it never
|
||||
collides; the cost is one NaN payload out of 2^51, exactly as `?Int` costs one
|
||||
absurd integer.
|
||||
- Opcodes `34–41` — the f64 arithmetic and compare set (table above).
|
||||
- Builtins `70–83` — `float`/`trunc`/`parse_float`/`float_to_text`/`float_cmp`,
|
||||
then the Bytes surface (`bytes_len`/`bytes_at`/`bytes_slice`/`bytes_eq`/
|
||||
`bytes_concat`/`base64_encode`/`base64_decode`/`bytes_of_text`/
|
||||
`text_of_bytes`).
|
||||
|
||||
**Two numeric worlds, no implicit crossing.** Int keeps its DIV0 trap; Float
|
||||
never traps. There is no coercion in either direction — not in arithmetic, not
|
||||
in comparison, not in `==` — and the typechecker reports a mix as WO-E201
|
||||
rather than letting the emitter pick an opcode from one side and misread the
|
||||
other. `float(i)` and `trunc(f)` are the only bridges; `trunc` traps
|
||||
`WO_T_BOUNDS` on NaN, ±Inf, and anything outside i64, because the Int world has
|
||||
no value to hand back and returning 0 is how currency bugs start.
|
||||
|
||||
**One deliberate deviation from IEEE: the total order.** `FLT`/`FLE`/`FEQ` are
|
||||
IEEE, so `NaN < 1.0` is false and `NaN == NaN` is false. But an index and an
|
||||
`order by` *require* a total order — otherwise a sort's result depends on the
|
||||
comparison sequence and a B-tree walk loses rows. The `float_cmp` builtin
|
||||
provides it: `-Inf < finite < +Inf < NaN`, with `-0.0` equal to `+0.0`. Index
|
||||
keys are canonicalized to match (`table.c`'s `idx_float_key`: every NaN maps to
|
||||
the canonical one, `-0.0` maps to `+0.0`), so a `unique` Float column treats
|
||||
`-0.0` and `0.0` as the same key and a probe for one finds a row stored as the
|
||||
other. FLOAT is therefore an indexable kind; BYTES is not, this iteration.
|
||||
|
||||
**Rendering.** One renderer (`wo_float_text`) serves interpolation,
|
||||
`float_to_text`, and `json.encode`, so the three can never disagree. It picks
|
||||
the fewest significant digits that reparse to the same *bits*, then prefers
|
||||
fixed notation over exponential across the range `1e-6 … 1e21` — "shortest"
|
||||
alone would render a price of `900.0` as `9e+02`. A rendering always carries a
|
||||
`.` or an exponent, so a Float never prints as `1` where an Int would.
|
||||
|
||||
**json boundaries.** A Float field accepts the whole JSON number grammar,
|
||||
fractions and exponents included (an Int field's strictness is unchanged — a
|
||||
fraction there still fails the decode whole). A non-finite Float encodes as
|
||||
`null`, because JSON has no `nan`/`inf` literal and emitting one would be
|
||||
invalid JSON. A Bytes field crosses as a base64 string, matching
|
||||
`base64_encode`'s alphabet exactly.
|
||||
|
||||
## Enum payload variants (haxe-parity compiler Task 4)
|
||||
|
||||
`.wob` v1 is unchanged — no new section, no new header field, no version
|
||||
|
|
|
|||
|
|
@ -51,13 +51,33 @@ rows in landing order, then the pending rows in implementation order.
|
|||
File names keep their IDs — every board, spec, and plan references
|
||||
iterations by number, so numbers never renumber.
|
||||
|
||||
RE-SEQUENCED 2026-08-20 (second pass) against the verified findings in
|
||||
[`docs/00-code-review.md`](../../00-code-review.md). **Seq changed; no `#`
|
||||
changed and no file moved** — that is what an immutable ID is for. The
|
||||
rule applied: measure before optimizing, close correctness holes before
|
||||
adding surface, and stop stacking features on unmeasured ground.
|
||||
|
||||
- **22 → first.** It has never run. `bench/baseline.json` does not exist,
|
||||
there is no `just db-bench`, and `runtime/bench/` is the retired C
|
||||
prototype's harness plus a Go reference. Until 22 runs, no performance
|
||||
statement about this project is sourced.
|
||||
- **The arc's stage 3 → second, reframed as correctness.** `wo_engine_start`
|
||||
zero-initializes worker VMs, so `rt.db` is NULL off the primary and any DB
|
||||
statement there traps `WO_T_DB "database engine not initialized"`. A
|
||||
multi-shard program that touches the database is broken today.
|
||||
- **New 30 (observability, CI, fuzz) and new 31 (actor lifecycle).** The
|
||||
review's two largest gaps had no iteration at all: nothing to run the
|
||||
proof automatically, and no request/response, backpressure, supervision,
|
||||
or timers behind `spawn`/`send`.
|
||||
- **18, 20, 21 demoted.** All three add surface; none answer a named gap.
|
||||
|
||||
| Seq | # | Iteration | Delivers |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 1 | [Principles doc](done/01-principles-doc.md) | `docs/00-principles.md` — the doctrine page every later slice links back to |
|
||||
| 2 | 2 | [VM core](done/02-vm-core.md) | `wovm`: `.wob` loader, register interpreter, arena, borrow word, `@gc` collector |
|
||||
| 3 | 3 | [Compiler front](done/03-compiler-front.md) | `woc`: lexer → parser → typechecker → ownership pass, diagnostics |
|
||||
| 4 | 4 | [Single binary end-to-end](done/04-single-binary-e2e.md) | emitter + conformance corpus + `woc build` self-contained binary |
|
||||
| 5 | 5 | [Language surface](05-language-surface.md) | Haxe-parity adoptions, grammar + strictness halves (landed in waves through 2026-08-20) |
|
||||
| 5 | 5 | [Language surface](done/05-language-surface.md) | Haxe-parity adoptions, grammar + strictness halves (landed in waves through 2026-08-20) |
|
||||
| 6 | 6 | [Program mode + stdlib](done/06-program-mode-stdlib.md) | `fn main`, exit codes, `fs`/`proc`/`net`/`time`/`json` builtins |
|
||||
| 7 | 7 | [log-watcher proof](done/07-logwatcher-proof.md) | the driving workload compiled, executable, soak-proven (landed 2026-08-15) |
|
||||
| 8 | 7b | [Inferred GC + mark-sweep](done/07b-inferred-gc-mark-sweep.md) | `@gc` removed; GC-ness inferred; RC replaced by incremental per-shard tri-color mark-sweep |
|
||||
|
|
@ -65,20 +85,22 @@ iterations by number, so numbers never renumber.
|
|||
| 10 | 9b | [`@table`, relations, query](done/09b-table-relations-query.md) | `@table` real storage; `ref`/`backlink`/`multi`; compiler-checked queries |
|
||||
| 11 | 15 | [deps: `wo.toml [deps]`](done/15-deps-package-manager.md) | exact-rev git deps + `wo.lock` + `.wo-deps`; flat-only, offline once locked |
|
||||
| 12 | 16 | [web framework](done/16-web-framework.md) | the `.wo` framework v1 (router, middleware, auth, all three body hooks) consumed via `[deps]` |
|
||||
| 13 | 8+11 | [Shard-actor runtime](08-shard-actor-runtime.md) · [Fibers](refine/11-fibers.md) | THE ARC (stages 1+2 landed 2026-08-20: fibers/budget/actors/io_uring plane; pinned shards, envelope sends, home-routed frees, WO-E222); stage 3 = transparent DB RPC + 22 re-run |
|
||||
| 14 | 18 | [framework v2: memory-rich features](18-memory-db-features.md) | spec+plan approved: TTL cache, @table flags, durable job queue, `transaction { }` over the WAL's staged batch |
|
||||
| 15 | 19 | [Float + Bytes](19-missing-scalar-types.md) | the missing scalars, full stack: IEEE-quiet f64 through literals/VM/@table/WAL/json + Bytes as the binary carrier — feeds 24 (WS frames) and the crypto fork (digests). *(was 20)* |
|
||||
| 16 | 20 | [Cross-program tables](refine/20-cross-program-tables.md) | attach to a running program's database over local IPC; owner stays the single writer (channel half-built). *(was 9c)* |
|
||||
| 17 | 21 | [Keypair attach auth](refine/21-keypair-attach-auth.md) | program identity is a keypair; mutual challenge–response at attach (crypto half-built; plan folds into 20's). *(was 9d)* |
|
||||
| 18 | 22 | [Durability, throughput, scale](refine/22-durability-throughput-scale.md) | restart-persistence proof, benchmarks, ~1M rows — the baseline the arc and 23 sign against. *(was 9e)* |
|
||||
| 13 | 22 | [Durability, throughput, scale](refine/22-durability-throughput-scale.md) | restart-persistence proof, benchmarks, ~1M rows — the baseline the arc and 23 sign against. **Promoted to first pending (was seq 18)**: it has never run, so every performance claim on this project is currently unsourced. *(was 9e)* |
|
||||
| 14 | 8+11 | [Shard-actor runtime](refine/08-shard-actor-runtime.md) · [Fibers](refine/11-fibers.md) | THE ARC (stages 1+2 landed 2026-08-20: fibers/budget/actors/io_uring plane; pinned shards, envelope sends, home-routed frees, WO-E222); stage 3 = transparent DB RPC + 22 re-run. **Stage 3 is a correctness hole, not an optimization**: worker shards are zero-initialized, so a DB statement off the primary traps `WO_T_DB`. |
|
||||
| 15 | 30 | Observability, CI, fuzz *(no story file yet)* | **NEW** — runtime counters + a profiler hook, 22's harness wired to run per change instead of by hand, and a fuzz target on the parser and `.wob` loader. The whole proof-maturity gap had no iteration to point at. |
|
||||
| 16 | 19 | [Float + Bytes](done/19-missing-scalar-types.md) | **LANDED 2026-08-20** — `.wob` v5; the full stack: IEEE-quiet f64 through literals/VM/@table/WAL/json + Bytes as the binary carrier, no implicit mixing, total-order indexes. Unblocks 24 (WS frames) and the crypto fork (digests). *(was 20)* |
|
||||
| 17 | 31 | Actor lifecycle *(no story file yet)* | **NEW** — request/response (today `send` is one-way and callers `sleep` to await), bounded mailboxes with backpressure (today the FIFO just grows), actor death/supervision, and timers beyond `time.sleep`. 24 cannot be written honestly without these. |
|
||||
| 18 | 24 | [chat: WebSocket workload](refine/24-chat-websocket-workload.md) | the arc's acceptance: WS upgrade + frames (SHA-1 via crypto fork, Bytes via 19), rooms/broadcast, 1k clients, drain-clean. *(was 19)* |
|
||||
| 19 | 23 | [io_uring group-commit](refine/23-io-uring-commit.md) | WAL WRITE+FSYNC chains on the arc's per-shard rings; fsync fallback kept (after 22 + the arc). *(was 9f)* |
|
||||
| 20 | 24 | [chat: WebSocket workload](refine/24-chat-websocket-workload.md) | the arc's acceptance: WS upgrade + frames (SHA-1 via crypto fork, Bytes via 19), rooms/broadcast, 1k clients, drain-clean. *(was 19)* |
|
||||
| 21 | 25 | [HTTP service layer](25-http-service.md) | `service` blocks lower onto the framework (after 9b + 20 by their own precedence notes). *(was 10)* |
|
||||
| 22 | 26 | [Blue-green deploy](26-blue-green-deploy.md) | two VM slots, in-runtime compile, atomic switch, resident rollback (plan authored after 9 + 25). *(was 12)* |
|
||||
| 23 | 27 | [Query grammar corpus](refine/27-query-grammar-corpus.md) | grow the query grammar from real corpora; likely collapses to "confirm `len(query)` + add `exists`"; precedes 28. *(was 9g)* |
|
||||
| 24 | 28 | [skillhost host workload](refine/28-skillhost-host-workload.md) | host-shaped driving workload naming runtime gaps — demoted with the framework goal. *(was 14)* |
|
||||
| 25 | 29 | [Compile-time metaprogramming](refine/29-compile-time-metaprogramming.md) | `@derive(...)` from class-table metadata; held with the parked drain by the 2026-08-08 scope directive. *(was 13)* |
|
||||
| ⏸ | 17 | [library projects + `internal/`](17-library-projects-internal.md) | **PARKED** (spec + plan approved, branch `library-internal`) — `wo.toml` kind = "library" + Go's `internal/` rule; slots anywhere after 16 on directive |
|
||||
| 20 | 25 | [HTTP service layer](25-http-service.md) | `service` blocks lower onto the framework (after 9b + 20 by their own precedence notes). *(was 10)* |
|
||||
| 21 | 18 | [framework v2: memory-rich features](hold/18-memory-db-features.md) | spec+plan approved: TTL cache, @table flags, durable job queue, `transaction { }` over the WAL's staged batch. **Demoted from seq 14**: more surface on a framework with one consumer, and the cache still stores `Text` because there are no generics |
|
||||
| 22 | 27 | [Query grammar corpus](refine/27-query-grammar-corpus.md) | grow the query grammar from real corpora; likely collapses to "confirm `len(query)` + add `exists`"; precedes 28. *(was 9g)* |
|
||||
| 23 | 26 | [Blue-green deploy](26-blue-green-deploy.md) | two VM slots, in-runtime compile, atomic switch, resident rollback (plan authored after 9 + 25). *(was 12)* |
|
||||
| 24 | 20 | [Cross-program tables](refine/20-cross-program-tables.md) | attach to a running program's database over local IPC; owner stays the single writer (channel half-built). **Demoted from seq 16**: new distribution surface while there is no TLS, no crypto, and the multi-shard DB still traps. *(was 9c)* |
|
||||
| 25 | 21 | [Keypair attach auth](refine/21-keypair-attach-auth.md) | program identity is a keypair; mutual challenge–response at attach (crypto half-built; plan folds into 20's). **Demoted with 20** — and it needs crypto primitives that do not exist. *(was 9d)* |
|
||||
| 26 | 28 | [skillhost host workload](refine/28-skillhost-host-workload.md) | host-shaped driving workload naming runtime gaps — demoted with the framework goal. *(was 14)* |
|
||||
| 27 | 29 | [Compile-time metaprogramming](refine/29-compile-time-metaprogramming.md) | `@derive(...)` from class-table metadata; held with the parked drain by the 2026-08-08 scope directive. *(was 13)* |
|
||||
| ✅ | 17 | [library projects + `internal/`](done/17-library-projects-internal.md) | **LANDED 2026-08-20** — `kind = "library"` + entry-less check mode (retires the `--emit` workaround) and Go's `internal/` rule as WO-E108 at the consumer's `use`; driver-only, VM/GC untouched. `just web-app` 26/0 |
|
||||
|
||||
|
||||
Review protocol: the developer reads one iteration, approves or amends;
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
# Iteration 8 — shard-actor runtime
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
|
||||
## Goals
|
||||
|
||||
- The runtime scales past one core the doctrine way: pinned thread-per-core
|
||||
shards, each owning its own heap and event loop; cross-shard
|
||||
communication is a message send that **moves ownership** — shared mutable
|
||||
state never exists.
|
||||
- The language grows `spawn` and message send; garbage collection stays
|
||||
per-shard, so no global pause appears at any core count.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- What to achieve?
|
||||
- **Given** a program spawning actors across shards,
|
||||
- **when** an owned object is sent to another shard,
|
||||
- **then** the sender can no longer touch it (compile-time move), the
|
||||
receiver owns it, and its eventual free routes back to its
|
||||
allocation-home arena.
|
||||
- What to achieve?
|
||||
- **Given** debug builds with shard-ownership asserts,
|
||||
- **when** the deterministic actor corpus runs under ASan and TSan,
|
||||
- **then** zero races, zero leaks, and identical output across runs.
|
||||
- What to achieve?
|
||||
- **Given** a `@gc` reference,
|
||||
- **when** code attempts to send it cross-shard,
|
||||
- **then** the compiler rejects it — aliased references cannot cross
|
||||
heap boundaries.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Fibers/green threads (recorded in the blue-green vision §3; extends this
|
||||
scheduler later).
|
||||
- Cross-shard transactions (the database iteration's 2PC concern, later).
|
||||
|
||||
## Info
|
||||
|
||||
- The C reference (`runtime/wo-rt.c`, phases A–F) is the substrate: epoll
|
||||
loops, eventfd mail, the machinery this iteration lifts into `wovm`.
|
||||
- The VM's object header has carried a shard id since iteration 2 — no
|
||||
relayout.
|
||||
- **Gated by the benchmark (2026-08-15):** this is the "optimize
|
||||
multithreading" lever of the performance arc — thread-per-core is a
|
||||
throughput/scale claim, so landing it means re-running iteration
|
||||
[22](refine/22-durability-throughput-scale.md) at the connection/concurrency
|
||||
scale it unlocks and recording the before/after delta. It is also where
|
||||
the io_uring write path ([23](refine/23-io-uring-commit.md)) gets a thread to
|
||||
overlap durability against.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
- Execute the existing plan: `docs/superpowers/plans/2026-08-01-shard-actor-vm-runtime.md`
|
||||
(pinned-worker scheduler, shard-stamped heaps, MPSC mailbox rings + mail
|
||||
eventfds, send-as-move with home-routed frees, gc pacing per tick,
|
||||
spawn/send surface, actor corpus).
|
||||
|
|
@ -1,19 +1,23 @@
|
|||
# Iteration 5 — language surface (Haxe-parity adoptions)
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
> [Story — one language, one runtime, one database, one binary](../00-story.md).
|
||||
|
||||
|
||||
> **Status (2026-08-14):** the *grammar* half landed — modules, `and`/`or`,
|
||||
> interpolation, `const`, loop control, switch expressions, typedef records,
|
||||
> enum payloads, try/catch, `nil`/`?T`, statics, `pub(read)` syntax, container
|
||||
> literals, `for k, v in m`, and `as` — which is what let the driving workload
|
||||
> compile. The *strictness* half (`?T` forced handling `WO-E211`–`E213`,
|
||||
> `pub(read)` write enforcement, `using`, `#if`, reject-row diagnostics) is
|
||||
> **deliberately deferred** behind
|
||||
> [`plan/compiler/2026-08-14-logwatcher-executable.md`](../../plan/compiler/2026-08-14-logwatcher-executable.md):
|
||||
> it makes the language refuse more, not the program run. Plan 8 stays open
|
||||
> for it.
|
||||
> **Status: ✅ COMPLETE 2026-08-20.** The *grammar* half landed 2026-08-14 —
|
||||
> modules, `and`/`or`, interpolation, `const`, loop control, switch
|
||||
> expressions, typedef records, enum payloads, try/catch, `nil`/`?T`,
|
||||
> statics, `pub(read)` syntax, container literals, `for k, v in m`, `as`.
|
||||
> The *strictness* half landed in three waves: `?T` forced handling
|
||||
> (WO-E211/212/213 + narrowing) and reject rows (WO-E105) on 2026-08-18;
|
||||
> the final three on 2026-08-20 (branch `language-surface-strictness`,
|
||||
> developer directive overriding the post-12 park) — **`pub(read)` write
|
||||
> enforcement** (WO-E219, class-owned writes, corpus-pinned),
|
||||
> **`using` static extensions** (compile-time rewrite to a free-fn call,
|
||||
> WO-E220 on method collision, zero owner/emit awareness), and **`#if`
|
||||
> build flags** (`woc -D name`, token-level filter, WO-E003 misuse).
|
||||
> `is`/`throw` stay cut (0 workload uses); `abstract` is a reject row.
|
||||
> Plan 8 is closed.
|
||||
|
||||
## Goals
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Iteration 17 — library projects and dependency privacy (`kind`, `internal/`)
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
> [Story — one language, one runtime, one database, one binary](../00-story.md).
|
||||
>
|
||||
> **Inserted 2026-08-20, needs further refinement** (developer decision: keep
|
||||
> as an iteration, do not implement yet).
|
||||
|
|
@ -10,10 +10,36 @@
|
|||
> changed). See "Settled decisions" and "Impact analysis" below.
|
||||
>
|
||||
> **⏸ PARKED 2026-08-20** (developer directive: framework v1 work first).
|
||||
> The spec and plan were written and approved before parking; both sit ready
|
||||
> on branch `library-internal`
|
||||
> The spec and plan were written and approved before parking
|
||||
> (`docs/superpowers/specs/2026-08-20-library-kind-internal-design.md`,
|
||||
> `docs/superpowers/plans/2026-08-20-library-kind-internal.md`).
|
||||
>
|
||||
> **LANDED 2026-08-20** — unparked and executed against that plan, all six
|
||||
> tasks. Every change is in the driver (`compiler/bin/main.ml`); the VM,
|
||||
> `.wob`, and GC are untouched exactly as the impact analysis predicted.
|
||||
> Reasoning-under-the-code in `compiler/src/CODE-LOGIC.md`.
|
||||
>
|
||||
> Gates: `just web-app` **26/0** (three new checks — library check mode,
|
||||
> WO-E108 at the boundary, WO-E109 on a bad kind), and `just woc-test`,
|
||||
> `just oop-e2e` (103/0), `just deps-accept`, `just log-watcher`,
|
||||
> `just employee` all unchanged.
|
||||
>
|
||||
> Two deviations from the plan, both because the framework grew after the plan
|
||||
> was written:
|
||||
>
|
||||
> 1. **`http/parse.wo` was SPLIT, not moved whole.** The plan said move it
|
||||
> under `internal/`, but the framework-v1 slices had since added
|
||||
> `media_type` and `form_values` to that file and the web-app calls both —
|
||||
> moving the file whole would have put public surface behind the privacy
|
||||
> line and broken the consumer. The parsing plumbing (`Parsed`,
|
||||
> `parse_request`, `url_decode`, `parse_query`) is now `internal/parse.wo`;
|
||||
> the two public functions are `http/form.wo`, which does `use internal`
|
||||
> (legal inside the library).
|
||||
> 2. **The gate is 26/0, not the plan's 17/0.** `just web-app` had grown from
|
||||
> 14 to 23 checks (framework v1 plus iteration 19's Float price) before this
|
||||
> iteration started; the three new checks make 26. The plan's numbers were
|
||||
> written against a 14-check gate. Acceptance criterion 3 below still holds
|
||||
> in substance: no pre-existing check changed.
|
||||
|
||||
## Why this iteration exists
|
||||
|
||||
|
|
@ -1,11 +1,31 @@
|
|||
# Iteration 19 — the missing scalar types: Float and Bytes
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
> [Story — one language, one runtime, one database, one binary](../00-story.md).
|
||||
>
|
||||
> **Inserted 2026-08-20, forks settled the same day** (developer
|
||||
> decisions below). Next: spec, then plan — a new storage kind touches
|
||||
> the `.wob`/WAL formats, so this one earns its written spec.
|
||||
>
|
||||
> **LANDED 2026-08-20.** Both scalars shipped full-stack as `.wob` v5. The
|
||||
> format decisions the story asked a spec for are recorded normatively in
|
||||
> [`docs/plan/oop-vm/00-wob-format.md`](../../../plan/oop-vm/00-wob-format.md)
|
||||
> §"v5: Float and Bytes" — written in the same change as the code, per this
|
||||
> iteration's own last acceptance criterion — with the reasoning-under-the-code
|
||||
> in `runtime/src/CODE-LOGIC.md` and `compiler/src/CODE-LOGIC.md`. No separate
|
||||
> spec document was authored; the deviation is deliberate and noted here.
|
||||
>
|
||||
> Gates: corpus **103/0** (four new fixtures — `float-arithmetic`,
|
||||
> `bytes-carrier`, `float-json-storage`, `float-table-column`),
|
||||
> `test_wal` **156/0** (bit-exact Float/Bytes replay), `just web-app`
|
||||
> **23/0** with the storefront price a real Float, `just oop-accept` ALL
|
||||
> CRITERIA MET, and every other sample gate unchanged.
|
||||
>
|
||||
> One judgment call worth flagging: the shortest-round-trip renderer prefers
|
||||
> FIXED notation over exponential across `1e-6 … 1e21`. Pure "shortest" is
|
||||
> what `%g` does, and it renders a price of `900.0` as `9e+02` — correct and
|
||||
> useless. Both forms carry the same significant digits, so round-tripping is
|
||||
> unaffected.
|
||||
|
||||
## Why this iteration exists
|
||||
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
> **Scope label (2026-08-20): this iteration is FRAMEWORK V2.** Framework
|
||||
> v1 is the transport/routing/body/security surface tracked in the
|
||||
> [framework README's status ledger](../../examples/writeonce-framework/README.md);
|
||||
> [framework README's status ledger](../../../examples/writeonce-framework/README.md);
|
||||
> v2 is what the embedded store adds on top. v1 gaps land before or
|
||||
> alongside v2 as slices, per the ledger.
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
> [Story — one language, one runtime, one database, one binary](../00-story.md).
|
||||
>
|
||||
> **Inserted 2026-08-20, forks settled the same day** (developer decisions
|
||||
> below). Next step: spec + plan, implementation on approval — the
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# Iteration 8 — shard-actor runtime (the 8+11 concurrency arc, part 1)
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](../00-story.md).
|
||||
>
|
||||
> **REFINED 2026-08-20** (developer decisions, no code): iterations 8 and
|
||||
> 11 are **one arc** — the scheduler, fibers on it, then serving — because
|
||||
> the database-ownership decision below makes a fiberless multi-shard
|
||||
> server block a whole thread per cross-shard call. The arc's driving
|
||||
> workload is **iteration 24 (chat: WebSocket pub/sub)**. The pre-existing
|
||||
> plan (`docs/superpowers/plans/2026-08-01-shard-actor-vm-runtime.md`)
|
||||
> predates inferred GC (7b), the unified surface, and the DB decision —
|
||||
> it is a source of ideas, NOT the plan of record; the arc starts with a
|
||||
> fresh brainstorm → spec → plan.
|
||||
|
||||
## Settled decisions (2026-08-20)
|
||||
|
||||
1. **The database is an actor.** The engine (`wo_db` + WAL) lives on one
|
||||
owner shard; every query/write from another shard is a message send,
|
||||
and results come back materialized (queries already copy rows out —
|
||||
the model was built for this). Doctrine-pure: no lock, no shared
|
||||
mutable state. Consequence, accepted deliberately: callers must PARK
|
||||
while the reply travels, which is why 8 and 11 ship as one arc.
|
||||
(Rejected: a coarse engine lock — bends the doctrine and caps write
|
||||
scaling anyway; partitioned tables — the real scale answer, but it
|
||||
waits for a measured need, not v1.)
|
||||
2. **One concurrency surface: everything is an actor address.** `spawn`
|
||||
returns an address whether the spawnee lands on this shard (a fiber)
|
||||
or another (placement policy's call); `send` always moves ownership;
|
||||
a same-heap send skips the ring and is cheap. The language never
|
||||
shows a fiber-vs-actor split. (This dissolves iteration 11's
|
||||
"handle vs address" open question.)
|
||||
3. **Driving workload: chat** (iteration 24) — rooms, broadcast, N
|
||||
concurrent WebSocket clients, one binary. The arc's acceptance is the
|
||||
chat sample's, not only synthetic corpora.
|
||||
4. **Order: 22 → the 8+11 arc → 23.** The io_uring write path waits for
|
||||
the arc (its batch boundary is the shard tick) and for 22's baseline.
|
||||
|
||||
## Goals
|
||||
|
||||
- The runtime scales past one core the doctrine way: pinned
|
||||
thread-per-core shards, each owning its own heap and event loop;
|
||||
cross-shard communication is a message send that **moves ownership** —
|
||||
shared mutable state never exists.
|
||||
- The language grows `spawn`/`send` (the unified address surface);
|
||||
garbage collection stays per-shard (7b's collector is already
|
||||
per-shard by construction), so no global pause appears at any core
|
||||
count.
|
||||
- The database keeps its single-writer truth by BEING an actor on its
|
||||
owner shard.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- What to achieve?
|
||||
- **Given** a program spawning actors across shards,
|
||||
- **when** an owned object is sent to another shard,
|
||||
- **then** the sender can no longer touch it (compile-time move), the
|
||||
receiver owns it, and its eventual free routes back to its
|
||||
allocation-home arena.
|
||||
- What to achieve?
|
||||
- **Given** debug builds with shard-ownership asserts,
|
||||
- **when** the deterministic actor corpus runs under ASan and TSan,
|
||||
- **then** zero races, zero leaks, and identical output across runs.
|
||||
- What to achieve?
|
||||
- **Given** a reference to a TRACED object (GC-ness is inferred since
|
||||
7b — there is no `@gc` to write),
|
||||
- **when** code attempts to send it cross-shard,
|
||||
- **then** the compiler rejects it: aliased references cannot cross
|
||||
heap boundaries, and the diagnostic names the inferred-traced class
|
||||
and why it is traced. (This criterion originally said `@gc`;
|
||||
restated 2026-08-20 in inference terms — same rule, current
|
||||
language.)
|
||||
- What to achieve?
|
||||
- **Given** handlers on serving shards querying and writing through
|
||||
the DB-owner shard,
|
||||
- **when** the employee/web-app matrices run multi-shard,
|
||||
- **then** every answer is byte-identical to the single-shard run and
|
||||
the WAL's ack-after-durable contract is unchanged.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Cross-shard transactions (2PC) — the DB actor serializes writers, so
|
||||
iteration 18's `transaction { }` is unaffected; distributing it is a
|
||||
later story.
|
||||
- Fiber details beyond the shared scheduler substrate — part 2
|
||||
([iteration 11](11-fibers.md)) owns them.
|
||||
- WebSocket framing — the framework's (iteration 24's) job.
|
||||
|
||||
## Info
|
||||
|
||||
- The C proving ground (`docs/plan/exploration/c-runtime/`, phases A–F:
|
||||
epoll loops, eventfd mail) is the substrate this lifts into `wovm`.
|
||||
(Path restated 2026-08-20; the old `runtime/wo-rt.c` reference was
|
||||
stale — that tree was removed with the Rust runtime.)
|
||||
- The VM's object header has carried a shard id since iteration 2 — no
|
||||
relayout.
|
||||
- **Gated by the benchmark:** landing the arc means re-running
|
||||
[22](22-durability-throughput-scale.md) at the concurrency
|
||||
scale it unlocks and recording the before/after delta; it is also
|
||||
where [23](23-io-uring-commit.md) gets a thread to overlap
|
||||
durability against.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Fresh brainstorm → spec → plan for the WHOLE arc (8+11), staged: the
|
||||
pinned-worker scheduler + shard-stamped heaps + MPSC mailboxes + the
|
||||
unified spawn/send surface and the traced-send rejection; fibers on that
|
||||
scheduler (part 2's document); the DB-actor migration; then iteration 24
|
||||
proves it. The 2026-08-01 plan is reference material for the mailbox and
|
||||
heap-stamping shapes only.
|
||||
|
|
@ -1,7 +1,28 @@
|
|||
# Iteration 11 — fibers (green threads on the shard scheduler)
|
||||
# Iteration 11 — fibers (the 8+11 concurrency arc, part 2)
|
||||
|
||||
> Format: `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](../00-story.md).
|
||||
>
|
||||
> **REFINED 2026-08-20** (developer decisions, no code): 8 and 11 ship as
|
||||
> **one arc** — the DB becomes an actor on an owner shard
|
||||
> ([iteration 8](08-shard-actor-runtime.md)'s decision), so serving
|
||||
> shards must PARK on cross-shard replies, which is this iteration.
|
||||
> The spawn-surface question below is SETTLED: one unified actor-address
|
||||
> surface (`spawn` returns an address, fiber or remote alike; `send`
|
||||
> moves ownership; same-heap sends take the cheap path). The arc's
|
||||
> driving workload is [iteration 24: chat](24-chat-websocket-workload.md)
|
||||
> — fiber-per-WebSocket-connection is the serving model that retires the
|
||||
> framework's close-when-idle keep-alive policy.
|
||||
>
|
||||
> **STAGE-1 SUBSTANCE LANDED 2026-08-20** (branch `concurrency-arc`):
|
||||
> reduction-budget fibers (back-edge accounting — the livelock lesson is
|
||||
> in the plan's deviations), `spawn`/`send`/`actor M`, one-message-at-a-
|
||||
> time delivery, main-return reap, fiber-trap isolation, and parked
|
||||
> `net`/`time` builtins on the io_uring-first per-shard I/O plane
|
||||
> (`WO_IO=uring|epoll`, epoll fallback proven). Demonstrated by
|
||||
> `docs/examples/fibers` (`just fibers` 8/0). The shard-context criteria
|
||||
> (TID assertions, cross-shard sends, blue-green drain reuse) close with
|
||||
> the arc's stage 2.
|
||||
|
||||
## Goals
|
||||
|
||||
|
|
@ -42,10 +63,13 @@
|
|||
as cleanly as trapped ones. (Iteration 26's blue-green drain reuses
|
||||
exactly this unwind path.)
|
||||
- What to achieve?
|
||||
- **Given** `@gc` objects referenced only from a parked fiber's frames,
|
||||
- **when** the per-shard cycle collector scans,
|
||||
- **then** fiber stacks are roots — nothing live is collected, nothing
|
||||
dead survives.
|
||||
- **Given** TRACED objects (GC-ness inferred since 7b) referenced only
|
||||
from a parked fiber's frames,
|
||||
- **when** the per-shard mark-sweep collector scans,
|
||||
- **then** parked fibers' frames are roots exactly as the live frame
|
||||
stack is (`vm_gc_roots` grows fiber awareness) — nothing live is
|
||||
collected, nothing dead survives. (Originally said `@gc`; restated
|
||||
2026-08-20 in inference terms.)
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
|
|
@ -66,9 +90,11 @@
|
|||
matches the stdlib posture).
|
||||
- Vision origin: [blue-green vision §3](../../../plan/exploration/blue-green-vm/00-vision.md);
|
||||
iteration 8's scheduler is the substrate this extends.
|
||||
- Open questions to settle in the spec: spawn surface (handle vs actor
|
||||
address), budget size and check granularity, parked-fiber drop
|
||||
semantics, run-queue fairness (FIFO v1).
|
||||
- Open questions to settle in the spec — REDUCED 2026-08-20: the spawn
|
||||
surface is settled (the unified actor address, iteration 8 decision 2).
|
||||
Still open for the arc's spec: budget size and check granularity,
|
||||
parked-fiber drop semantics, run-queue fairness (FIFO v1), and how a
|
||||
parked fiber's borrow state interacts with the shard's GC safepoints.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,27 @@
|
|||
> would optimize a number nobody had measured, against a runtime that
|
||||
> couldn't use it.
|
||||
>
|
||||
> **No spec exists yet.** The forks in *Info* are genuine decisions.
|
||||
> **No spec exists yet.** ~~The forks in *Info* are genuine decisions.~~
|
||||
>
|
||||
> **REFINED 2026-08-20: the four forks are SETTLED as their recorded
|
||||
> leanings** (developer confirmation, no code): (1) drop-in behind
|
||||
> `wo_wal_commit` first, an async variant only if the arc's scheduler
|
||||
> proves the blocking boundary is the bottleneck; (2) raw
|
||||
> `io_uring_setup`/`io_uring_enter` syscalls — libc-only doctrine holds,
|
||||
> ring layout documented normatively; (3) the batch boundary is the shard
|
||||
> tick (the 8+11 arc's quantum), single-writer fallback batches whatever
|
||||
> accumulated; (4) startup auto-probe + an env override so CI proves both
|
||||
> paths on one kernel — AMENDED: the override is the arc-wide
|
||||
> `WO_IO=uring|epoll` (the arc's T4 owns the probe and the per-shard
|
||||
> ring; `WO_WAL_MODE` is subsumed). Position: AFTER the 8+11 arc (order
|
||||
> settled 2026-08-20: 9e → 8+11 → 9f). AMENDED 2026-08-20 (io_uring-first
|
||||
> directive): the WAL's WRITE+FSYNC chains ride the SAME per-shard ring
|
||||
> T4 creates for fiber parking — one event loop per shard, readiness ops
|
||||
> and durability ops together, exactly the linux reference project's
|
||||
> "single event loop" card. One composition
|
||||
> note added since iteration 18: a `transaction { }` already IS a staged
|
||||
> batch — under io_uring it becomes exactly one submission, so the two
|
||||
> features compose without either knowing the other.
|
||||
|
||||
## Goals
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
# Iteration 17 — library kind + `internal/`: implementation plan
|
||||
|
||||
> **Status: ⏸ PARKED 2026-08-20** (developer directive: framework v1 work
|
||||
> proceeds instead; this plan stays ready on branch `library-internal`,
|
||||
> execution not started). Board: [docs/00-status.md](../../00-status.md).
|
||||
> **Status: ✅ LANDED 2026-08-20** — executed in full, all six tasks. Was
|
||||
> parked the same day (developer directive: framework v1 work first), then
|
||||
> unparked and run. Two disclosed deviations, both because the framework grew
|
||||
> after this plan was written: `http/parse.wo` was SPLIT rather than moved
|
||||
> whole (its `media_type`/`form_values` are public surface the web-app calls),
|
||||
> and the gate reads 26/0 rather than 17/0 (`just web-app` was already at 23
|
||||
> before this iteration). Board: [docs/00-status.md](../../00-status.md).
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> superpowers:subagent-driven-development (recommended) or
|
||||
|
|
@ -28,7 +32,7 @@ build path grows a check branch, and the dep-use resolution walk in
|
|||
|
||||
**Spec:** [`../specs/2026-08-20-library-kind-internal-design.md`](../specs/2026-08-20-library-kind-internal-design.md)
|
||||
(normative). Story:
|
||||
[`17-library-projects-internal.md`](../../stories/language-runtime-database/17-library-projects-internal.md).
|
||||
[`17-library-projects-internal.md`](../../stories/language-runtime-database/done/17-library-projects-internal.md).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
# Iteration 17 — library projects and dependency privacy: design
|
||||
|
||||
> **Status: ⏸ PARKED 2026-08-20** (developer directive: framework v1 work
|
||||
> proceeds instead; spec + plan stay ready on branch `library-internal`).
|
||||
> Approved before parking. Decisions were settled in
|
||||
> [the iteration](../../stories/language-runtime-database/17-library-projects-internal.md)
|
||||
> **Status: ✅ LANDED 2026-08-20** — implemented as specified; the impact
|
||||
> analysis held (driver-only, VM/`.wob`/GC untouched). Approved, then parked
|
||||
> the same day by directive, then unparked and executed. Decisions were
|
||||
> settled in
|
||||
> [the iteration](../../stories/language-runtime-database/done/17-library-projects-internal.md)
|
||||
> (four forks + impact analysis); this spec makes them buildable. The plan
|
||||
> follows after review. Board: [docs/00-status.md](../../00-status.md).
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
> **Status: APPROVED 2026-08-20** (developer review). Plan next.
|
||||
> Decisions were settled in
|
||||
> [the iteration](../../stories/language-runtime-database/18-memory-db-features.md);
|
||||
> [the iteration](../../stories/language-runtime-database/hold/18-memory-db-features.md);
|
||||
> this spec makes them buildable. The plan follows after review.
|
||||
> Board: [docs/00-status.md](../../00-status.md).
|
||||
>
|
||||
|
|
|
|||
5
justfile
5
justfile
|
|
@ -1,5 +1,10 @@
|
|||
# writeonce — task runner. `just --list` shows all recipes.
|
||||
|
||||
# docs gate: every relative markdown link resolves, every #anchor exists.
|
||||
# Report lands in docs/00-link-audit.md; this recipe is the re-check.
|
||||
linkcheck:
|
||||
python3 scripts/linkcheck.py .
|
||||
|
||||
# woc compiler front (compiler/): build the executable
|
||||
woc-build:
|
||||
dune build --root compiler
|
||||
|
|
|
|||
|
|
@ -138,3 +138,46 @@ returns, and actor state / queued messages / the in-flight message are GC
|
|||
roots scanned beside the fiber frames. spawn = BUILTIN 68 (instance +
|
||||
receive's method index, compile-time constant); send = BUILTIN 69 (the
|
||||
message is excluded from the emitter's fresh-arg drops — ownership moved).
|
||||
|
||||
## Float and Bytes (iteration 19 — `.wob` v5)
|
||||
|
||||
The registers did not change shape: a Float IS the register's 64 bits read as
|
||||
an f64, converted only by `wo_f64`/`wo_bits` in `wob.h` (memcpy, so
|
||||
strict-aliasing-clean and free at -O1). Nothing else in the runtime knows the
|
||||
difference, which is why the change is opcodes and kind bytes rather than a
|
||||
layout.
|
||||
|
||||
- **Two failure worlds.** `WOP_DIV` traps DIV0; `WOP_FDIV` never traps. That
|
||||
asymmetry is the contract, not an oversight — IEEE quiet semantics mean Inf
|
||||
and NaN flow instead of raising, so a compute-bound handler cannot be killed
|
||||
by data. `WOP_FNEG` flips the sign bit rather than subtracting from zero,
|
||||
which is the only way `-0.0` is reachable.
|
||||
- **IEEE compares are not the index's order.** `FEQ`/`FLT`/`FLE` are IEEE
|
||||
(`NaN == NaN` is 0, `0.0 == -0.0` is 1). Indexes and `order by` need a total
|
||||
order instead, so `wo_float_cmp` (`wob.h`) sorts NaN last and treats the two
|
||||
zeros as equal, and `table.c`'s `idx_float_key` canonicalizes an index
|
||||
column's bits to match. Skip that canonicalization and a `unique` Float
|
||||
column accepts both `-0.0` and `0.0`, and a probe for one misses a row stored
|
||||
as the other — the bug this pairing exists to prevent.
|
||||
- **A `?Float`'s nil is a reserved quiet NaN** (`WO_NIL_FLOAT`), not the zero
|
||||
word (`+0.0`) and not `WO_NIL_SCALAR` (whose bits are `-2.0`). Arithmetic
|
||||
produces the platform's canonical quiet NaN, so a computed NaN never reads as
|
||||
absence. Both json paths that write a nil word — the omitted-key prefill in
|
||||
`jparse_object` and the explicit `null` in `jparse_value` — must know this;
|
||||
either one alone leaves a `null` price reading back as zero.
|
||||
- **Bytes is `wo_str` with a different `class_id`.** Same struct, same
|
||||
allocator, same free (`gc.c` handles both ids), so lifetime handling can
|
||||
never diverge. `WO_B_TEXT_COPY` preserves the id, which is what lets every
|
||||
existing copy-on-ownership-boundary serve both carriers; copying a Bytes as a
|
||||
Text would launder it into the wrong world, and the distinct id exists
|
||||
precisely to stop that.
|
||||
- **One float renderer, three callers.** `wo_float_text` backs
|
||||
`float_to_text`, string interpolation, and `json.encode`. Shortest digits
|
||||
that reparse to the same BITS (bits, not `==`: `-0.0 == 0.0` is true, so a
|
||||
value comparison would let `0` stand in for `-0.0`), then fixed notation
|
||||
preferred over exponential in `1e-6 … 1e21` — pure "shortest" renders a
|
||||
price of 900.0 as `9e+02`.
|
||||
- **The durability path never renders.** `wal.c` writes a Float as its raw
|
||||
word and a Bytes as the same length-prefixed blob a Text uses, so replay is
|
||||
bit-exact for NaN, ±Inf, and `-0.0`. `test_wal`'s `test_float_bytes_replay`
|
||||
asserts on bits for exactly that reason.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "db.h" /* database/src — the engine's statement executors */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h> /* strtod: the round-trip check in wo_float_text */
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
|
|
@ -25,6 +26,106 @@ static void *native_check(uint64_t v, uint32_t cls, const char **msg) {
|
|||
return o;
|
||||
}
|
||||
|
||||
/* ---- iteration 19: Float rendering (contract in builtin.h) ----
|
||||
* Shortest round-trip, found by asking printf for 1..17 significant digits
|
||||
* and stopping at the first rendering that strtod turns back into the SAME
|
||||
* BITS. Bits, not `==`: -0.0 == 0.0 is true, so a value comparison would let
|
||||
* "0" stand in for -0.0 and the iteration's own edge-case gate would fail.
|
||||
*
|
||||
* 17 digits always terminates the loop (%.17g round-trips every double), so
|
||||
* the fallback after the loop is unreachable defensive code, not a policy. */
|
||||
size_t wo_float_text(double d, char *out, size_t cap) {
|
||||
if (d != d) return (size_t)snprintf(out, cap, "nan");
|
||||
if (d > 1.7976931348623157e308) return (size_t)snprintf(out, cap, "inf");
|
||||
if (d < -1.7976931348623157e308) return (size_t)snprintf(out, cap, "-inf");
|
||||
/* Step 1: the fewest significant digits that reparse to the same bits. */
|
||||
char tmp[WO_FLOAT_TEXT_CAP];
|
||||
int sig = 17;
|
||||
for (int prec = 1; prec <= 17; prec++) {
|
||||
int n = snprintf(tmp, sizeof tmp, "%.*g", prec, d);
|
||||
if (n > 0 && (size_t)n < sizeof tmp && wo_bits(strtod(tmp, NULL)) == wo_bits(d)) {
|
||||
sig = prec;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Step 2: fixed or exponential. "Shortest" alone is the wrong rule here —
|
||||
* %g renders 900.0 as `9e+02` because that is two bytes shorter, and a
|
||||
* price of `9e+02` in a JSON body is nobody's idea of a good answer. So
|
||||
* fixed notation wins across the range humans read (and the range JSON
|
||||
* bodies live in), and the exponent is kept only where fixed would be
|
||||
* absurd: a 21-digit integer or twenty leading zeros. Same thresholds
|
||||
* JavaScript's own number formatting uses, for the same reason.
|
||||
* Correctness is unaffected: both forms carry the identical `sig`
|
||||
* significant digits, so both reparse to the same bits. */
|
||||
int exp10;
|
||||
{
|
||||
char e[WO_FLOAT_TEXT_CAP];
|
||||
snprintf(e, sizeof e, "%.*e", sig - 1, d);
|
||||
const char *ep = strchr(e, 'e');
|
||||
exp10 = ep ? (int)strtol(ep + 1, NULL, 10) : 0;
|
||||
}
|
||||
int len;
|
||||
if (exp10 >= -6 && exp10 < 21) {
|
||||
int decimals = sig - 1 - exp10;
|
||||
if (decimals < 1) decimals = 1; /* always one decimal: see below */
|
||||
len = snprintf(tmp, sizeof tmp, "%.*f", decimals, d);
|
||||
/* A Float must not print as `1` where an Int would: the two numeric
|
||||
* worlds never mix implicitly, so the rendering says which one this
|
||||
* is. `900.0` reparses to the same bits as `900`, so the trailing
|
||||
* `.0` costs the round-trip nothing. */
|
||||
}
|
||||
else
|
||||
len = snprintf(tmp, sizeof tmp, "%.*e", sig - 1, d);
|
||||
if (len < 0 || (size_t)len >= sizeof tmp) /* defensive: cannot happen */
|
||||
len = snprintf(tmp, sizeof tmp, "%.17g", d);
|
||||
return (size_t)snprintf(out, cap, "%s", tmp);
|
||||
}
|
||||
|
||||
/* iteration 19: base64, standard alphabet with '=' padding (RFC 4648 §4) —
|
||||
* the alphabet the JSON boundary and every HTTP header this project will meet
|
||||
* uses. No URL-safe variant until a workload needs one. */
|
||||
static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
static int b64_val(char c) {
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||
if (c == '+') return 62;
|
||||
if (c == '/') return 63;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int wo_base64_to_bytes(wo_rt *rt, const char *p, uint32_t len, uint64_t *out) {
|
||||
*out = 0;
|
||||
if (len % 4u != 0u) return -1;
|
||||
uint32_t pad = 0;
|
||||
if (len) {
|
||||
if (p[len - 1] == '=') pad++;
|
||||
if (len >= 2 && p[len - 2] == '=') pad++;
|
||||
}
|
||||
wo_str *o = wo_bytes_alloc(rt, len / 4u * 3u - pad);
|
||||
if (!o) return -2;
|
||||
char *w = o->data;
|
||||
for (uint32_t i = 0; i < len; i += 4u) {
|
||||
uint32_t v = 0;
|
||||
for (uint32_t j = 0; j < 4u; j++) {
|
||||
char c = p[i + j];
|
||||
int d = c == '=' ? 0 : b64_val(c);
|
||||
/* '=' is legal only in the final quad, and only where pad said */
|
||||
if (d < 0 || (c == '=' && i + 4u < len)) {
|
||||
wo_str_free(rt, o);
|
||||
return -1;
|
||||
}
|
||||
v = (v << 6) | (uint32_t)d;
|
||||
}
|
||||
uint32_t have = i + 4u == len ? 3u - pad : 3u;
|
||||
if (have > 0) *w++ = (char)(v >> 16);
|
||||
if (have > 1) *w++ = (char)(v >> 8);
|
||||
if (have > 2) *w++ = (char)v;
|
||||
}
|
||||
*out = (uint64_t)(uintptr_t)o;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- systems-stdlib helpers ------------------------------------------
|
||||
* Text is bytes with an explicit length and no NUL, so every scan below is
|
||||
* length-driven; "whitespace" is the same four bytes WO_B_WORDS already
|
||||
|
|
@ -274,9 +375,21 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
R[A] = 0;
|
||||
return 0;
|
||||
}
|
||||
const wo_str *src = native_check(R[B], WO_CLS_STR, msg);
|
||||
if (!src) return WO_T_BOUNDS;
|
||||
wo_str *cp = wo_str_new(rt, src->data, src->len);
|
||||
/* iteration 19: a copy PRESERVES the kind. Bytes is a wo_str with a
|
||||
* different class id, and every ownership boundary that copies a Text
|
||||
* (into a container, into a field, out of a borrow) copies a Bytes for
|
||||
* the identical reason — so this one builtin serves both rather than
|
||||
* growing a bytes_copy that the six emitter sites would have to choose
|
||||
* between. Copying a Bytes as a Text would silently launder it into
|
||||
* the wrong world, which is exactly what the distinct id prevents. */
|
||||
const wo_hdr *h = (const wo_hdr *)(uintptr_t)R[B];
|
||||
if (h->class_id != WO_CLS_STR && h->class_id != WO_CLS_BYTES) {
|
||||
*msg = "wrong container type";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
const wo_str *src = (const wo_str *)h;
|
||||
wo_str *cp = h->class_id == WO_CLS_BYTES ? wo_bytes_new(rt, src->data, src->len)
|
||||
: wo_str_new(rt, src->data, src->len);
|
||||
if (!cp) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
|
|
@ -307,6 +420,195 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
R[A] = (uint64_t)(uintptr_t)s;
|
||||
return 0;
|
||||
}
|
||||
/* ---- iteration 19: the Float bridges ---- */
|
||||
case WO_B_FLOAT_OF_INT: {
|
||||
R[A] = wo_bits((double)(int64_t)R[B]);
|
||||
return 0;
|
||||
}
|
||||
case WO_B_TRUNC: {
|
||||
double d = wo_f64(R[B]);
|
||||
/* The Int world has no NaN, no Inf, and no 2^63. Yielding 0 or a
|
||||
* wrapped bit pattern for those is exactly the silent-corruption
|
||||
* class this iteration exists to remove, so it traps. Bound checked
|
||||
* with >= 2^63 as a double (9223372036854775808.0 is exact). */
|
||||
if (d != d || !(d > -9223372036854775808.0 && d < 9223372036854775808.0)) {
|
||||
*msg = "trunc: float has no integer value";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
R[A] = (uint64_t)(int64_t)d; /* C truncates toward zero, as specified */
|
||||
return 0;
|
||||
}
|
||||
case WO_B_PARSE_FLOAT: {
|
||||
const wo_str *s = native_check(R[B], WO_CLS_STR, msg);
|
||||
if (!s) return WO_T_BOUNDS;
|
||||
/* strtod needs a NUL and a Text has none. A number never needs more
|
||||
* than a couple of dozen bytes; anything longer is not a number, and
|
||||
* NaN ("not a number") is the honest answer for unparseable input —
|
||||
* no `?Float` needed, which is why parse_float has no nullable form
|
||||
* while parse_int leans on `?Int`'s nil. */
|
||||
char buf[64];
|
||||
if (s->len >= sizeof buf) {
|
||||
R[A] = wo_bits(0.0 / 0.0);
|
||||
return 0;
|
||||
}
|
||||
memcpy(buf, s->data, s->len);
|
||||
buf[s->len] = '\0';
|
||||
char *end = NULL;
|
||||
double d = strtod(buf, &end);
|
||||
/* Trailing garbage is a parse failure, not a prefix match: "1.5kg"
|
||||
* must not silently become 1.5. Empty input fails the same way. */
|
||||
R[A] = wo_bits(end == buf || *end != '\0' ? 0.0 / 0.0 : d);
|
||||
return 0;
|
||||
}
|
||||
case WO_B_FLOAT_TO_TEXT: {
|
||||
char buf[WO_FLOAT_TEXT_CAP];
|
||||
size_t len = wo_float_text(wo_f64(R[B]), buf, sizeof buf);
|
||||
wo_str *s = wo_str_new(rt, buf, (uint32_t)len);
|
||||
if (!s) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
R[A] = (uint64_t)(uintptr_t)s;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_FLOAT_CMP: {
|
||||
R[A] = (uint64_t)(int64_t)wo_float_cmp(wo_f64(R[B]), wo_f64(R[B + 1]));
|
||||
return 0;
|
||||
}
|
||||
/* ---- iteration 19: Bytes ---- */
|
||||
case WO_B_BYTES_LEN: {
|
||||
const wo_str *b = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
if (!b) return WO_T_BOUNDS;
|
||||
R[A] = b->len;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BYTES_AT: {
|
||||
const wo_str *b = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
if (!b) return WO_T_BOUNDS;
|
||||
uint64_t i = R[B + 1];
|
||||
if (i >= b->len) {
|
||||
*msg = "bytes index out of range";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
R[A] = (uint8_t)b->data[i];
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BYTES_SLICE: {
|
||||
const wo_str *b = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
if (!b) return WO_T_BOUNDS;
|
||||
/* Clamped, matching WO_B_SUBSTR: a slice past the end is the empty
|
||||
* slice, not a trap. Signed reads because a computed start can be
|
||||
* negative and must clamp to 0 rather than wrap to 2^64. */
|
||||
int64_t start = (int64_t)R[B + 1], want = (int64_t)R[B + 2];
|
||||
if (start < 0) start = 0;
|
||||
if (start > (int64_t)b->len) start = b->len;
|
||||
if (want < 0) want = 0;
|
||||
if (want > (int64_t)b->len - start) want = (int64_t)b->len - start;
|
||||
wo_str *out = wo_bytes_new(rt, b->data + start, (uint32_t)want);
|
||||
if (!out) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
R[A] = (uint64_t)(uintptr_t)out;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BYTES_EQ: {
|
||||
const wo_str *x = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
const wo_str *y = x ? native_check(R[B + 1], WO_CLS_BYTES, msg) : NULL;
|
||||
if (!y) return WO_T_BOUNDS;
|
||||
R[A] = x->len == y->len && !memcmp(x->data, y->data, x->len) ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BYTES_CONCAT: {
|
||||
const wo_str *x = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
const wo_str *y = x ? native_check(R[B + 1], WO_CLS_BYTES, msg) : NULL;
|
||||
if (!y) return WO_T_BOUNDS;
|
||||
if ((uint64_t)x->len + y->len > 0xFFFFFFFFull) {
|
||||
*msg = "bytes concat overflows length";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
wo_str *out = wo_bytes_alloc(rt, x->len + y->len);
|
||||
if (!out) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
memcpy(out->data, x->data, x->len);
|
||||
memcpy(out->data + x->len, y->data, y->len);
|
||||
R[A] = (uint64_t)(uintptr_t)out;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BASE64_ENCODE: {
|
||||
const wo_str *b = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
if (!b) return WO_T_BOUNDS;
|
||||
if ((uint64_t)b->len > 0xBFFFFFFFull) { /* 4/3 growth must not overflow u32 */
|
||||
*msg = "base64: input too large";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
uint32_t olen = ((b->len + 2u) / 3u) * 4u;
|
||||
wo_str *out = wo_str_alloc(rt, olen);
|
||||
if (!out) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
char *o = out->data;
|
||||
uint32_t i = 0;
|
||||
for (; i + 3u <= b->len; i += 3u) {
|
||||
uint32_t v = ((uint8_t)b->data[i] << 16) | ((uint8_t)b->data[i + 1] << 8) |
|
||||
(uint8_t)b->data[i + 2];
|
||||
*o++ = B64[(v >> 18) & 63];
|
||||
*o++ = B64[(v >> 12) & 63];
|
||||
*o++ = B64[(v >> 6) & 63];
|
||||
*o++ = B64[v & 63];
|
||||
}
|
||||
if (i < b->len) { /* 1 or 2 trailing bytes, '=' padded */
|
||||
uint32_t rem = b->len - i;
|
||||
uint32_t v = (uint32_t)(uint8_t)b->data[i] << 16;
|
||||
if (rem == 2u) v |= (uint32_t)(uint8_t)b->data[i + 1] << 8;
|
||||
*o++ = B64[(v >> 18) & 63];
|
||||
*o++ = B64[(v >> 12) & 63];
|
||||
*o++ = rem == 2u ? B64[(v >> 6) & 63] : '=';
|
||||
*o++ = '=';
|
||||
}
|
||||
R[A] = (uint64_t)(uintptr_t)out;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BASE64_DECODE: {
|
||||
const wo_str *s = native_check(R[B], WO_CLS_STR, msg);
|
||||
if (!s) return WO_T_BOUNDS;
|
||||
uint64_t bytes = 0;
|
||||
int rc = wo_base64_to_bytes(rt, s->data, s->len, &bytes);
|
||||
if (rc == -2) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
/* malformed decodes to nil, not a trap: base64 arrives from the
|
||||
network, so a bad body is expected input — the same contract
|
||||
json.decode keeps. rc == -1 leaves bytes at 0. */
|
||||
R[A] = bytes;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_BYTES_OF_TEXT: {
|
||||
const wo_str *s = native_check(R[B], WO_CLS_STR, msg);
|
||||
if (!s) return WO_T_BOUNDS;
|
||||
wo_str *out = wo_bytes_new(rt, s->data, s->len);
|
||||
if (!out) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
R[A] = (uint64_t)(uintptr_t)out;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_TEXT_OF_BYTES: {
|
||||
const wo_str *b = native_check(R[B], WO_CLS_BYTES, msg);
|
||||
if (!b) return WO_T_BOUNDS;
|
||||
wo_str *out = wo_str_new(rt, b->data, b->len);
|
||||
if (!out) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
R[A] = (uint64_t)(uintptr_t)out;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_VARIANT_TAG: { /* haxe-parity compiler Task 4: enum payload variants */
|
||||
/* the tag IS the header's class_id (wob.h's convention). Null and
|
||||
* native-class receivers trap BOUNDS — same defense ICALL keeps;
|
||||
|
|
|
|||
|
|
@ -31,4 +31,26 @@ 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);
|
||||
|
||||
/* iteration 19: render a Float as text, SHORTEST form that reparses to the
|
||||
* same bits, into `out` (cap must be >= WO_FLOAT_TEXT_CAP); returns the
|
||||
* length. One renderer for three callers — WO_B_FLOAT_TO_TEXT, string
|
||||
* interpolation, and json.encode — because three spellings of the same
|
||||
* number is how a round-trip test starts passing while the product lies.
|
||||
*
|
||||
* The rendering always carries a '.' or an exponent, so a Float never prints
|
||||
* as `1` where an Int would: the two numeric worlds stay visibly distinct,
|
||||
* and `1.0` reparses to exactly the same bits as `1`. Non-finite values
|
||||
* render `nan` / `inf` / `-inf`; json.encode does NOT use those (JSON has no
|
||||
* such literals) and emits `null` instead, which it decides for itself. */
|
||||
#define WO_FLOAT_TEXT_CAP 32u
|
||||
size_t wo_float_text(double d, char *out, size_t cap);
|
||||
|
||||
/* iteration 19: decode base64 into a fresh Bytes. Two callers — the
|
||||
* `base64_decode` builtin and json.decode's Bytes boundary — and they must
|
||||
* agree byte for byte, so there is one implementation.
|
||||
* 0 = ok (*out is the Bytes word), -1 = malformed input, -2 = OOM.
|
||||
* Malformed is a return code rather than a trap because both callers treat
|
||||
* bad base64 as expected input from the network. */
|
||||
int wo_base64_to_bytes(wo_rt *rt, const char *p, uint32_t len, uint64_t *out);
|
||||
|
||||
#endif /* WO_BUILTIN_H */
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ void wo_drop_obj(wo_rt *rt, wo_hdr *o) {
|
|||
if (!o) return;
|
||||
switch (o->class_id) {
|
||||
case WO_CLS_STR:
|
||||
case WO_CLS_BYTES: /* iteration 19: same object shape, same free */
|
||||
wo_str_free(rt, (wo_str *)o);
|
||||
return;
|
||||
case WO_CLS_MULTI:
|
||||
|
|
@ -177,6 +178,7 @@ void wo_gc_scan_root(wo_rt *rt, wo_hdr *o) {
|
|||
}
|
||||
switch (o->class_id) {
|
||||
case WO_CLS_STR:
|
||||
case WO_CLS_BYTES: /* iteration 19: leaf bytes, nothing to scan */
|
||||
return;
|
||||
case WO_CLS_MULTI: {
|
||||
wo_multi *m = (wo_multi *)o;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,36 @@ static void jb_int(jbuf *b, int64_t v) {
|
|||
jb_put(b, tmp, (size_t)n);
|
||||
}
|
||||
|
||||
/* iteration 19: base64 body for a Bytes field, standard alphabet with '='
|
||||
* padding — the same encoding WO_B_BASE64_ENCODE produces, so a Bytes column
|
||||
* that leaves through json.encode comes back through base64_decode bit-exact.
|
||||
* Written out here rather than shared with builtin.c because that one
|
||||
* allocates a wo_str and this one appends to a growing buffer; the alphabet is
|
||||
* the contract, and the corpus fixture compares both against it. */
|
||||
static void jb_b64(jbuf *b, const uint8_t *p, uint32_t len) {
|
||||
static const char A[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
uint32_t i = 0;
|
||||
char q[4];
|
||||
for (; i + 3u <= len; i += 3u) {
|
||||
uint32_t v = ((uint32_t)p[i] << 16) | ((uint32_t)p[i + 1] << 8) | p[i + 2];
|
||||
q[0] = A[(v >> 18) & 63];
|
||||
q[1] = A[(v >> 12) & 63];
|
||||
q[2] = A[(v >> 6) & 63];
|
||||
q[3] = A[v & 63];
|
||||
jb_put(b, q, 4);
|
||||
}
|
||||
if (i < len) {
|
||||
uint32_t rem = len - i;
|
||||
uint32_t v = (uint32_t)p[i] << 16;
|
||||
if (rem == 2u) v |= (uint32_t)p[i + 1] << 8;
|
||||
q[0] = A[(v >> 18) & 63];
|
||||
q[1] = A[(v >> 12) & 63];
|
||||
q[2] = rem == 2u ? A[(v >> 6) & 63] : '=';
|
||||
q[3] = '=';
|
||||
jb_put(b, q, 4);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
|
|
@ -121,7 +151,10 @@ static void enc_object(jbuf *b, const wo_module *mod, const wo_hdr *o) {
|
|||
}
|
||||
|
||||
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) {
|
||||
/* iteration 19: FLOAT joins SCALAR in being exempt here — a zero word is
|
||||
+0.0, a perfectly good value, not absence. A `?Float` spells nil as
|
||||
WO_NIL_FLOAT and is handled in the FLOAT arm below. */
|
||||
if (!v && kind != WO_K_SCALAR && kind != WO_K_FLOAT) {
|
||||
jb_put(b, "null", 4);
|
||||
return;
|
||||
}
|
||||
|
|
@ -136,6 +169,38 @@ static void enc_value(jbuf *b, const wo_module *mod, uint64_t v, uint8_t kind, u
|
|||
else
|
||||
jb_int(b, (int64_t)v);
|
||||
return;
|
||||
case WO_K_FLOAT: {
|
||||
/* iteration 19. Three cases, in order: a `?Float` holding its nil
|
||||
sentinel is JSON null; a non-finite value has NO JSON literal (the
|
||||
grammar has no nan/inf), so it is null too — the same choice every
|
||||
mainstream encoder makes, and the alternative is emitting invalid
|
||||
JSON; anything else is the shortest round-trip rendering, the SAME
|
||||
renderer interpolation uses so the two can never disagree. */
|
||||
if (fclass == WOB_FIELD_NIL_FLOAT && v == WO_NIL_FLOAT) {
|
||||
jb_put(b, "null", 4);
|
||||
return;
|
||||
}
|
||||
double d = wo_f64(v);
|
||||
if (d != d || d > 1.7976931348623157e308 || d < -1.7976931348623157e308) {
|
||||
jb_put(b, "null", 4);
|
||||
return;
|
||||
}
|
||||
char buf[WO_FLOAT_TEXT_CAP];
|
||||
size_t n = wo_float_text(d, buf, sizeof buf);
|
||||
jb_put(b, buf, n);
|
||||
return;
|
||||
}
|
||||
case WO_K_BYTES: {
|
||||
/* iteration 19: JSON has no binary type, so the boundary is base64 —
|
||||
spelled out here as the convention, matching base64_encode's
|
||||
alphabet exactly so a value that goes out through json comes back
|
||||
in through base64_decode unchanged. */
|
||||
const wo_str *s = (const wo_str *)(uintptr_t)v;
|
||||
jb_ch(b, '"');
|
||||
jb_b64(b, (const uint8_t *)s->data, s->len);
|
||||
jb_ch(b, '"');
|
||||
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 */
|
||||
|
|
@ -329,10 +394,16 @@ static int jparse_object(jp *j, uint32_t class_id, uint64_t *out) {
|
|||
/* NEW zeroes every slot, which is nil for a heap-shaped field but a real
|
||||
`0` for a scalar one — so a nullable scalar starts at its own nil word
|
||||
(wob.h's WO_NIL_SCALAR) and stays there if the object omits the key. */
|
||||
for (uint32_t i = 0; i < c->field_cnt; i++)
|
||||
if (c->field_class && (c->field_class[i] == WOB_FIELD_NIL_SCALAR ||
|
||||
c->field_class[i] == WOB_FIELD_NIL_BOOL))
|
||||
for (uint32_t i = 0; i < c->field_cnt; i++) {
|
||||
if (!c->field_class) break;
|
||||
if (c->field_class[i] == WOB_FIELD_NIL_SCALAR || c->field_class[i] == WOB_FIELD_NIL_BOOL)
|
||||
fs[i] = WO_NIL_SCALAR;
|
||||
/* iteration 19: a `?Float`'s nil is its own reserved NaN — the zero
|
||||
word is +0.0, so an omitted key would read back as a real price of
|
||||
zero rather than as absence. */
|
||||
else if (c->field_class[i] == WOB_FIELD_NIL_FLOAT)
|
||||
fs[i] = WO_NIL_FLOAT;
|
||||
}
|
||||
jskip_ws(j);
|
||||
if (j->p >= j->end || *j->p != '{') {
|
||||
wo_drop_obj(j->rt, o);
|
||||
|
|
@ -414,8 +485,8 @@ static int jparse_value(jp *j, uint8_t kind, uint32_t fclass, uint32_t felem, ui
|
|||
}
|
||||
char c = *j->p;
|
||||
if (c == 'n') { /* null: this field's own nil word */
|
||||
uint64_t nilw =
|
||||
(fclass == WOB_FIELD_NIL_SCALAR || fclass == WOB_FIELD_NIL_BOOL)
|
||||
uint64_t nilw = fclass == WOB_FIELD_NIL_FLOAT ? WO_NIL_FLOAT /* iteration 19 */
|
||||
: (fclass == WOB_FIELD_NIL_SCALAR || fclass == WOB_FIELD_NIL_BOOL)
|
||||
? WO_NIL_SCALAR
|
||||
: 0;
|
||||
return jskip_value(j) == 0 ? (*out = nilw, 0) : -1;
|
||||
|
|
@ -520,6 +591,17 @@ static int jparse_value(jp *j, uint8_t kind, uint32_t fclass, uint32_t felem, ui
|
|||
*out = (uint64_t)(uintptr_t)s;
|
||||
return 0;
|
||||
}
|
||||
if (kind == WO_K_BYTES) {
|
||||
/* iteration 19: the Bytes boundary is a base64 STRING (the same
|
||||
convention encode writes). Malformed base64 decodes to nil, not
|
||||
a whole-decode failure, matching base64_decode's own contract —
|
||||
a bad body from the network is expected input. */
|
||||
uint64_t bytes = 0;
|
||||
if (wo_base64_to_bytes(j->rt, s->data, s->len, &bytes) != 0) bytes = 0;
|
||||
wo_str_free(j->rt, s);
|
||||
*out = bytes;
|
||||
return 0;
|
||||
}
|
||||
wo_str_free(j->rt, s); /* a string where a number was declared: nil */
|
||||
*out = 0;
|
||||
return 0;
|
||||
|
|
@ -530,7 +612,47 @@ static int jparse_value(jp *j, uint8_t kind, uint32_t fclass, uint32_t felem, ui
|
|||
*out = kind == WO_K_SCALAR ? (uint64_t)truth : 0;
|
||||
return 0;
|
||||
}
|
||||
/* number: i64 by truncation — the language has no float */
|
||||
/* iteration 19: a FLOAT field takes the whole JSON number grammar —
|
||||
fractions and exponents included. This is the hole the iteration exists
|
||||
to close: `{"price": 9.99}` used to fail the entire decode. strtod does
|
||||
the conversion (correctly rounded), and the span is bounded by the JSON
|
||||
number grammar so a NUL-terminated scratch copy is always enough. */
|
||||
if (kind == WO_K_FLOAT) {
|
||||
const char *start = j->p;
|
||||
if (j->p < j->end && (*j->p == '-' || *j->p == '+')) j->p++;
|
||||
int digits = 0;
|
||||
while (j->p < j->end && *j->p >= '0' && *j->p <= '9') {
|
||||
j->p++;
|
||||
digits++;
|
||||
}
|
||||
if (j->p < j->end && *j->p == '.') {
|
||||
j->p++;
|
||||
while (j->p < j->end && *j->p >= '0' && *j->p <= '9') {
|
||||
j->p++;
|
||||
digits++;
|
||||
}
|
||||
}
|
||||
if (!digits) return -1;
|
||||
if (j->p < j->end && (*j->p == 'e' || *j->p == 'E')) {
|
||||
const char *save = j->p;
|
||||
j->p++;
|
||||
if (j->p < j->end && (*j->p == '-' || *j->p == '+')) j->p++;
|
||||
int edigits = 0;
|
||||
while (j->p < j->end && *j->p >= '0' && *j->p <= '9') {
|
||||
j->p++;
|
||||
edigits++;
|
||||
}
|
||||
if (!edigits) j->p = save; /* `1e` is not an exponent; stop before it */
|
||||
}
|
||||
size_t n = (size_t)(j->p - start);
|
||||
char tmp[64];
|
||||
if (n >= sizeof tmp) return -1; /* no real JSON number is this long */
|
||||
memcpy(tmp, start, n);
|
||||
tmp[n] = '\0';
|
||||
*out = wo_bits(strtod(tmp, NULL));
|
||||
return 0;
|
||||
}
|
||||
/* number: i64 by truncation — the language has no float in an Int slot */
|
||||
{
|
||||
int neg = 0;
|
||||
if (*j->p == '-') {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,12 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
|
|||
id to build */
|
||||
[WO_B_JSON_ENCODE] = 2, [WO_B_JSON_DECODE] = 2, [WO_B_MAP_GET_OPT] = 2,
|
||||
[WO_B_TEXT_COPY] = 1,
|
||||
/* iteration 19: Float bridges, then Bytes */
|
||||
[WO_B_FLOAT_OF_INT] = 1, [WO_B_TRUNC] = 1, [WO_B_PARSE_FLOAT] = 1,
|
||||
[WO_B_FLOAT_TO_TEXT] = 1, [WO_B_FLOAT_CMP] = 2,
|
||||
[WO_B_BYTES_LEN] = 1, [WO_B_BYTES_AT] = 2, [WO_B_BYTES_SLICE] = 3,
|
||||
[WO_B_BYTES_EQ] = 2, [WO_B_BYTES_CONCAT] = 2, [WO_B_BASE64_ENCODE] = 1,
|
||||
[WO_B_BASE64_DECODE] = 1, [WO_B_BYTES_OF_TEXT] = 1, [WO_B_TEXT_OF_BYTES] = 1,
|
||||
};
|
||||
|
||||
static int vtab_cmp(const void *a, const void *b) {
|
||||
|
|
@ -142,6 +148,16 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
c.off += l;
|
||||
m->consts[i].tag = tag;
|
||||
m->consts[i].s = s;
|
||||
} else if (tag == WOB_K_FLOAT) {
|
||||
/* iteration 19: raw f64 bits. Stored in the same slot as an Int
|
||||
constant because a register holds either as one word — the tag
|
||||
is what says how to read it, and LOADK copies the word either
|
||||
way. No validation is possible or wanted: every one of the 2^64
|
||||
patterns is a legal f64 (NaN payloads included). */
|
||||
uint64_t v;
|
||||
if (rd_u64(&c, &v)) BAIL("constant %u: truncated", (unsigned)i);
|
||||
m->consts[i].tag = tag;
|
||||
m->consts[i].i = (int64_t)v;
|
||||
} else {
|
||||
BAIL("constant %u: unknown tag %u", (unsigned)i, (unsigned)tag);
|
||||
}
|
||||
|
|
@ -197,7 +213,8 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
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 != WOB_FIELD_NIL_SCALAR &&
|
||||
fc != WOB_FIELD_BOOL && fc != WOB_FIELD_NIL_BOOL && fc >= kcnt)
|
||||
fc != WOB_FIELD_BOOL && fc != WOB_FIELD_NIL_BOOL &&
|
||||
fc != WOB_FIELD_NIL_FLOAT /* iteration 19 */ && fc >= kcnt)
|
||||
BAIL("class %u field %u: field class out of range", (unsigned)i, (unsigned)j);
|
||||
}
|
||||
m->classes[i].name = name;
|
||||
|
|
@ -234,9 +251,13 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
if (rd_u32(&k, &col)) BAIL("class %u index %u: truncated column", (unsigned)i, (unsigned)x);
|
||||
if (col >= fcnt) BAIL("class %u index %u: column out of range", (unsigned)i, (unsigned)x);
|
||||
uint8_t kind = m->kindpool[(uintptr_t)m->classes[i].kinds + col];
|
||||
if (kind != WO_K_SCALAR && kind != WO_K_TEXT)
|
||||
BAIL("class %u index %u: column %u is not scalar or Text", (unsigned)i,
|
||||
(unsigned)x, (unsigned)col);
|
||||
/* iteration 19: FLOAT joins the indexable kinds — the engine
|
||||
orders it by WO_B_FLOAT_CMP's total order (NaN last), which
|
||||
is precisely what an index requires. BYTES stays out: its
|
||||
ordering beyond equality is out of scope this iteration. */
|
||||
if (kind != WO_K_SCALAR && kind != WO_K_TEXT && kind != WO_K_FLOAT)
|
||||
BAIL("class %u index %u: column %u is not scalar, Text, or Float",
|
||||
(unsigned)i, (unsigned)x, (unsigned)col);
|
||||
m->idxpool[idx_pool++] = col;
|
||||
}
|
||||
}
|
||||
|
|
@ -397,6 +418,7 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
break;
|
||||
case WOP_MOVE:
|
||||
case WOP_NEG:
|
||||
case WOP_FNEG: /* iteration 19 */
|
||||
RCHK(A);
|
||||
RCHK(B);
|
||||
break;
|
||||
|
|
@ -409,6 +431,16 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
|
|||
case WOP_LT:
|
||||
case WOP_LE:
|
||||
case WOP_EQS:
|
||||
/* iteration 19: same shape as their Int counterparts — three
|
||||
register operands, no immediate, nothing to range-check beyond
|
||||
the registers. Every bit pattern is a legal f64. */
|
||||
case WOP_FADD:
|
||||
case WOP_FSUB:
|
||||
case WOP_FMUL:
|
||||
case WOP_FDIV:
|
||||
case WOP_FEQ:
|
||||
case WOP_FLT:
|
||||
case WOP_FLE:
|
||||
RCHK(A);
|
||||
RCHK(B);
|
||||
RCHK(C);
|
||||
|
|
|
|||
|
|
@ -171,9 +171,7 @@ int main(int argc, char **argv) {
|
|||
VM.is_primary = 1;
|
||||
VM.rt.shard_id = 0;
|
||||
VM.wake_efd = eventfd(0, EFD_NONBLOCK);
|
||||
VM.in_mu = calloc(1, sizeof(pthread_mutex_t));
|
||||
if (!VM.in_mu || VM.wake_efd < 0
|
||||
|| pthread_mutex_init((pthread_mutex_t *)VM.in_mu, NULL) != 0) {
|
||||
if (VM.wake_efd < 0 || wo_engine_primary_inbox(VM.wake_efd) != 0) {
|
||||
fprintf(stderr, "wovm: cannot set up the primary shard\n");
|
||||
wo_vm_destroy(&VM);
|
||||
wo_module_free(&mod);
|
||||
|
|
|
|||
|
|
@ -175,6 +175,23 @@ wo_str *wo_str_new(wo_rt *rt, const char *bytes, uint32_t len) {
|
|||
return s;
|
||||
}
|
||||
|
||||
/* iteration 19: Bytes is a wo_str wearing a different class id. Allocating
|
||||
* through wo_str_alloc and restamping is deliberate — one allocator, one
|
||||
* arena accounting path, one free — so a Bytes can never diverge from a Text
|
||||
* in lifetime handling. */
|
||||
wo_str *wo_bytes_alloc(wo_rt *rt, uint32_t len) {
|
||||
wo_str *s = wo_str_alloc(rt, len);
|
||||
if (s) s->h.class_id = WO_CLS_BYTES;
|
||||
return s;
|
||||
}
|
||||
|
||||
wo_str *wo_bytes_new(wo_rt *rt, const char *bytes, uint32_t len) {
|
||||
wo_str *s = wo_bytes_alloc(rt, len);
|
||||
if (!s) return NULL;
|
||||
memcpy(s->data, bytes, len);
|
||||
return s;
|
||||
}
|
||||
|
||||
wo_str *wo_str_concat(wo_rt *rt, const wo_str *a, const wo_str *b) {
|
||||
wo_str *s = wo_str_alloc(rt, a->len + b->len);
|
||||
if (!s) return NULL;
|
||||
|
|
|
|||
|
|
@ -95,4 +95,11 @@ wo_str *wo_str_concat(wo_rt *rt, const wo_str *a, const wo_str *b);
|
|||
int wo_str_eq(const wo_str *a, const wo_str *b); /* content equality */
|
||||
void wo_str_free(wo_rt *rt, wo_str *s); /* no-op on WO_F_CONST */
|
||||
|
||||
/* iteration 19: Bytes. Byte-for-byte the wo_str object — same struct, same
|
||||
* allocator, same free path (gc.c handles both class ids) — differing only in
|
||||
* the header's class_id, which is what every Text builtin checks. There is no
|
||||
* wo_bytes_free: wo_str_free is it. NULL = OOM on both. */
|
||||
wo_str *wo_bytes_alloc(wo_rt *rt, uint32_t len); /* UNINITIALIZED bytes */
|
||||
wo_str *wo_bytes_new(wo_rt *rt, const char *bytes, uint32_t len);
|
||||
|
||||
#endif /* WO_OBJ_H */
|
||||
|
|
|
|||
177
runtime/src/vm.c
177
runtime/src/vm.c
|
|
@ -35,18 +35,30 @@ static _Thread_local wo_vm *tls_vm = NULL;
|
|||
wo_vm *wo_tls_vm(void) { return tls_vm; }
|
||||
void wo_tls_set(wo_vm *vm) { tls_vm = vm; }
|
||||
|
||||
/* The cross-shard inbox lives OUTSIDE wo_vm, in engine-owned storage a
|
||||
* worker's lazy vm-init can never wipe: the second TSan/ASan round found
|
||||
* senders reading vm fields (in_mu, wake_efd, shard_id) through the
|
||||
* late-init memset's zero window. Senders touch ONLY this array; the vm's
|
||||
* own in_* fields are dead weight kept for layout stability. */
|
||||
#define WO_ENG_MAX_SHARDS 64u
|
||||
typedef struct {
|
||||
pthread_mutex_t mu;
|
||||
wo_envelope *head, *tail;
|
||||
int efd; /* duplicate of the shard's wake_efd, sender-visible, never wiped */
|
||||
} wo_inbox;
|
||||
static wo_inbox INBOX[WO_ENG_MAX_SHARDS];
|
||||
static int INBOX_READY[WO_ENG_MAX_SHARDS];
|
||||
|
||||
/* push an envelope into a shard's inbox and wake it (any thread) */
|
||||
static void inbox_push(wo_vm *to, wo_envelope *e) {
|
||||
pthread_mutex_t *mu = (pthread_mutex_t *)to->in_mu;
|
||||
pthread_mutex_lock(mu);
|
||||
static void inbox_push_to(uint32_t shard, wo_envelope *e) {
|
||||
wo_inbox *ib = &INBOX[shard % WO_ENG_MAX_SHARDS];
|
||||
pthread_mutex_lock(&ib->mu);
|
||||
e->next = NULL;
|
||||
if (to->in_tail) to->in_tail->next = e;
|
||||
else to->in_head = e;
|
||||
to->in_tail = e;
|
||||
/* capture the wake fd UNDER the lock: worker_late_init rewrites the
|
||||
* whole vm under this same mutex (TSan caught the unlocked read) */
|
||||
int efd = to->wake_efd;
|
||||
pthread_mutex_unlock(mu);
|
||||
if (ib->tail) ib->tail->next = e;
|
||||
else ib->head = e;
|
||||
ib->tail = e;
|
||||
int efd = ib->efd;
|
||||
pthread_mutex_unlock(&ib->mu);
|
||||
if (efd >= 0) {
|
||||
uint64_t one = 1;
|
||||
ssize_t n = write(efd, &one, sizeof one);
|
||||
|
|
@ -62,10 +74,11 @@ static void fib_reap_all(wo_vm *vm);
|
|||
/* the owning thread drains its inbox: adopt actors, deliver sends,
|
||||
* execute home-routed frees. Returns how many envelopes were handled. */
|
||||
static int wo_vm_adopt(wo_vm *vm) {
|
||||
pthread_mutex_lock((pthread_mutex_t *)vm->in_mu);
|
||||
wo_envelope *e = vm->in_head;
|
||||
vm->in_head = vm->in_tail = NULL;
|
||||
pthread_mutex_unlock((pthread_mutex_t *)vm->in_mu);
|
||||
wo_inbox *ib = &INBOX[vm->shard_id % WO_ENG_MAX_SHARDS];
|
||||
pthread_mutex_lock(&ib->mu);
|
||||
wo_envelope *e = ib->head;
|
||||
ib->head = ib->tail = NULL;
|
||||
pthread_mutex_unlock(&ib->mu);
|
||||
int n = 0;
|
||||
while (e) {
|
||||
wo_envelope *nx = e->next;
|
||||
|
|
@ -93,12 +106,11 @@ static int wo_vm_adopt(wo_vm *vm) {
|
|||
* the header's shard id is not the current thread's) */
|
||||
void wo_route_free(wo_hdr *h) {
|
||||
if (eng_teardown) return; /* arenas are torn down wholesale */
|
||||
wo_vm *to = &wo_eng.shards[h->shard_id];
|
||||
wo_envelope *e = calloc(1, sizeof *e);
|
||||
if (!e) return; /* OOM on the free path: leak rather than crash */
|
||||
e->kind = 2;
|
||||
e->payload = (uint64_t)(uintptr_t)h;
|
||||
inbox_push(to, e);
|
||||
inbox_push_to(h->shard_id, e);
|
||||
}
|
||||
|
||||
/* A worker's whole life in T5: pinned, parked on its wake eventfd until
|
||||
|
|
@ -110,28 +122,20 @@ static size_t eng_heap_cap = 0;
|
|||
* first envelope, not at boot (20 idle shards must stay ~free) */
|
||||
static int worker_late_init(wo_vm *vm) {
|
||||
if (vm->rt.arena.base) return 0;
|
||||
/* under the inbox mutex: wo_vm_init memsets the whole vm, and a
|
||||
* concurrent inbox_push would race the in_head/in_tail wipe (TSan
|
||||
* caught exactly this). The mutex OBJECT is malloc'd and stable;
|
||||
* pushers block on it while the fields are rebuilt. */
|
||||
void *mu = vm->in_mu;
|
||||
pthread_mutex_lock((pthread_mutex_t *)mu);
|
||||
/* the memset here is now HARMLESS to senders: every field they touch
|
||||
* lives in the engine-owned INBOX array, never in the vm (the second
|
||||
* TSan/ASan round found them reading through this wipe's zero window) */
|
||||
const wo_module *mod = vm->mod;
|
||||
uint32_t id = vm->shard_id;
|
||||
int efd = vm->wake_efd;
|
||||
wo_envelope *h = vm->in_head, *t = vm->in_tail;
|
||||
int rc = wo_vm_init(vm, mod, eng_heap_cap);
|
||||
if (rc == 0) {
|
||||
vm->shard_id = id;
|
||||
vm->rt.shard_id = (uint16_t)id;
|
||||
vm->is_primary = 0;
|
||||
vm->wake_efd = efd;
|
||||
vm->in_mu = mu;
|
||||
vm->in_head = h;
|
||||
vm->in_tail = t;
|
||||
tls_vm = vm;
|
||||
}
|
||||
pthread_mutex_unlock((pthread_mutex_t *)mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +178,18 @@ static void *shard_main(void *arg) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
/* register the PRIMARY's inbox row (main.c calls it once its wake fd
|
||||
* exists); workers register theirs in wo_engine_start */
|
||||
int wo_engine_primary_inbox(int wake_efd) {
|
||||
if (!INBOX_READY[0]) {
|
||||
if (pthread_mutex_init(&INBOX[0].mu, NULL) != 0) return -1;
|
||||
INBOX_READY[0] = 1;
|
||||
}
|
||||
INBOX[0].head = INBOX[0].tail = NULL;
|
||||
INBOX[0].efd = wake_efd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wo_engine_start(const wo_module *mod, size_t heap_cap, uint32_t nshards) {
|
||||
wo_eng.nshards = nshards;
|
||||
eng_heap_cap = heap_cap;
|
||||
|
|
@ -193,9 +209,15 @@ int wo_engine_start(const wo_module *mod, size_t heap_cap, uint32_t nshards) {
|
|||
sv->is_primary = 0;
|
||||
sv->wake_efd = eventfd(0, EFD_NONBLOCK);
|
||||
if (sv->wake_efd < 0) return -1;
|
||||
sv->in_mu = calloc(1, sizeof(pthread_mutex_t));
|
||||
if (!sv->in_mu || pthread_mutex_init((pthread_mutex_t *)sv->in_mu, NULL) != 0)
|
||||
return -1;
|
||||
{
|
||||
wo_inbox *ib = &INBOX[i % WO_ENG_MAX_SHARDS];
|
||||
if (!INBOX_READY[i % WO_ENG_MAX_SHARDS]) {
|
||||
if (pthread_mutex_init(&ib->mu, NULL) != 0) return -1;
|
||||
INBOX_READY[i % WO_ENG_MAX_SHARDS] = 1;
|
||||
}
|
||||
ib->head = ib->tail = NULL;
|
||||
ib->efd = sv->wake_efd;
|
||||
}
|
||||
if (pthread_create(&ts[i - 1], NULL, shard_main, sv) != 0) return -1;
|
||||
}
|
||||
(void)heap_cap; /* consumed at lazy init (T6) */
|
||||
|
|
@ -219,11 +241,12 @@ void wo_engine_stop(void) {
|
|||
* raised by the destroys below into a no-op, so no teardown ordering
|
||||
* can lock a freed mutex (the ASan SEGV this replaces). */
|
||||
eng_teardown = 1;
|
||||
for (uint32_t i = 0; i < wo_eng.nshards; i++) {
|
||||
wo_vm *sv = &wo_eng.shards[i];
|
||||
if (!sv->in_mu) continue;
|
||||
wo_envelope *e = sv->in_head;
|
||||
sv->in_head = sv->in_tail = NULL;
|
||||
for (uint32_t i = 0; i < wo_eng.nshards && i < WO_ENG_MAX_SHARDS; i++) {
|
||||
if (!INBOX_READY[i]) continue;
|
||||
wo_inbox *ib = &INBOX[i];
|
||||
wo_envelope *e = ib->head;
|
||||
ib->head = ib->tail = NULL;
|
||||
ib->efd = -1;
|
||||
while (e) {
|
||||
wo_envelope *nx = e->next;
|
||||
if (e->kind == 1 && e->actor) {
|
||||
|
|
@ -238,27 +261,11 @@ void wo_engine_stop(void) {
|
|||
close(wo_eng.shards[i].wake_efd);
|
||||
if (wo_eng.shards[i].rt.arena.base) /* lazily init'ed only */
|
||||
wo_vm_destroy(&wo_eng.shards[i]);
|
||||
free(wo_eng.shards[i].in_mu);
|
||||
wo_eng.shards[i].in_mu = NULL;
|
||||
|
||||
}
|
||||
/* the primary's inbox: same discard (main destroys its vm right after) */
|
||||
/* the primary's wake fd (its inbox row was drained in the loop above) */
|
||||
{
|
||||
wo_vm *pv = &wo_eng.shards[0];
|
||||
if (pv->in_mu) {
|
||||
wo_envelope *e = pv->in_head;
|
||||
pv->in_head = pv->in_tail = NULL;
|
||||
while (e) {
|
||||
wo_envelope *nx = e->next;
|
||||
if (e->kind == 1 && e->actor) {
|
||||
free(e->actor->msgs);
|
||||
free(e->actor);
|
||||
}
|
||||
free(e);
|
||||
e = nx;
|
||||
}
|
||||
free(pv->in_mu);
|
||||
pv->in_mu = NULL;
|
||||
}
|
||||
if (pv->wake_efd >= 0) {
|
||||
close(pv->wake_efd);
|
||||
pv->wake_efd = -1;
|
||||
|
|
@ -455,7 +462,7 @@ int wo_vm_actor_spawn(wo_vm *vm, uint64_t instance, uint32_t method_idx,
|
|||
}
|
||||
e->kind = 1;
|
||||
e->actor = a;
|
||||
inbox_push(&wo_eng.shards[home], e);
|
||||
inbox_push_to(home, e);
|
||||
}
|
||||
*out_addr = (uint64_t)(uintptr_t)a;
|
||||
return 0;
|
||||
|
|
@ -483,7 +490,7 @@ int wo_vm_actor_send(wo_vm *vm, uint64_t addr, uint64_t msg_val, const char **ms
|
|||
e->kind = 0;
|
||||
e->actor = a;
|
||||
e->payload = msg_val;
|
||||
inbox_push(&wo_eng.shards[a->home], e);
|
||||
inbox_push_to(a->home, e);
|
||||
return 0;
|
||||
}
|
||||
if (actor_push(a, msg_val) != 0) {
|
||||
|
|
@ -758,7 +765,7 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
|||
* cur/queued/parked). */
|
||||
#define NEXT_RUNNABLE() \
|
||||
do { \
|
||||
if (vm->in_mu) (void)wo_vm_adopt(vm); \
|
||||
if (INBOX_READY[vm->shard_id % WO_ENG_MAX_SHARDS]) (void)wo_vm_adopt(vm); \
|
||||
vm->cur = fib_dequeue(vm); \
|
||||
while (!vm->cur) { \
|
||||
if (!vm->is_primary && !vm->parked) { \
|
||||
|
|
@ -785,7 +792,7 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
|||
} \
|
||||
return -1; \
|
||||
} \
|
||||
if (vm->in_mu) (void)wo_vm_adopt(vm); \
|
||||
if (INBOX_READY[vm->shard_id % WO_ENG_MAX_SHARDS]) (void)wo_vm_adopt(vm); \
|
||||
vm->cur = fib_dequeue(vm); \
|
||||
} \
|
||||
vm->budget = vm->budget0; \
|
||||
|
|
@ -847,6 +854,11 @@ static int vm_run(wo_vm *vm, uint64_t *ret, wo_err *err) {
|
|||
[WOP_RELEASE_X] = &&L_RELEASE_X, [WOP_BUILTIN] = &&L_BUILTIN,
|
||||
[WOP_DB_STUB] = &&L_DB_STUB, [WOP_TRAP] = &&L_TRAP,
|
||||
[WOP_TRY] = &&L_TRY, [WOP_ENDTRY] = &&L_ENDTRY,
|
||||
/* iteration 19: the f64 world */
|
||||
[WOP_FADD] = &&L_FADD, [WOP_FSUB] = &&L_FSUB,
|
||||
[WOP_FMUL] = &&L_FMUL, [WOP_FDIV] = &&L_FDIV,
|
||||
[WOP_FNEG] = &&L_FNEG, [WOP_FEQ] = &&L_FEQ,
|
||||
[WOP_FLT] = &&L_FLT, [WOP_FLE] = &&L_FLE,
|
||||
};
|
||||
#define CASE(name) L_##name
|
||||
#define NEXT() \
|
||||
|
|
@ -867,8 +879,11 @@ dispatch:
|
|||
|
||||
CASE(LOADK) : {
|
||||
const wo_const *k = &mod->consts[wo_ins_bx(ins)];
|
||||
R[wo_ins_a(ins)] = k->tag == WOB_K_INT ? (uint64_t)k->i
|
||||
: (uint64_t)(uintptr_t)k->s;
|
||||
/* WOB_K_TEXT is the only pointer-shaped constant; INT and (iteration
|
||||
* 19) FLOAT both live in the same word, differing only in how the
|
||||
* ops that read them interpret it. */
|
||||
R[wo_ins_a(ins)] = k->tag == WOB_K_TEXT ? (uint64_t)(uintptr_t)k->s
|
||||
: (uint64_t)k->i;
|
||||
NEXT();
|
||||
}
|
||||
|
||||
|
|
@ -920,6 +935,50 @@ dispatch:
|
|||
NEXT();
|
||||
}
|
||||
|
||||
/* iteration 19: f64 arithmetic. Registers are u64, so each op bitcasts in
|
||||
* and out (wo_f64/wo_bits — memcpy-based, the only strict-aliasing-clean
|
||||
* way). Nothing here traps: IEEE 754 quiet semantics are the contract, so
|
||||
* x/0.0 yields ±Inf and 0.0/0.0 yields NaN instead of raising. The FPU's
|
||||
* own exception flags are left alone — the language never reads them. */
|
||||
CASE(FADD) : {
|
||||
R[wo_ins_a(ins)] = wo_bits(wo_f64(R[wo_ins_b(ins)]) + wo_f64(R[wo_ins_c(ins)]));
|
||||
NEXT();
|
||||
}
|
||||
CASE(FSUB) : {
|
||||
R[wo_ins_a(ins)] = wo_bits(wo_f64(R[wo_ins_b(ins)]) - wo_f64(R[wo_ins_c(ins)]));
|
||||
NEXT();
|
||||
}
|
||||
CASE(FMUL) : {
|
||||
R[wo_ins_a(ins)] = wo_bits(wo_f64(R[wo_ins_b(ins)]) * wo_f64(R[wo_ins_c(ins)]));
|
||||
NEXT();
|
||||
}
|
||||
CASE(FDIV) : {
|
||||
R[wo_ins_a(ins)] = wo_bits(wo_f64(R[wo_ins_b(ins)]) / wo_f64(R[wo_ins_c(ins)]));
|
||||
NEXT();
|
||||
}
|
||||
CASE(FNEG) : {
|
||||
/* sign flip, not 0.0 - x: only this reaches -0.0 from +0.0, and the
|
||||
* iteration's edge-case gate stores -0.0 and reads it back. */
|
||||
R[wo_ins_a(ins)] = wo_bits(-wo_f64(R[wo_ins_b(ins)]));
|
||||
NEXT();
|
||||
}
|
||||
/* IEEE comparisons, NOT the total order: every one of these is false when
|
||||
* either side is NaN, which is what makes `NaN != NaN` true in the
|
||||
* language. Indexes and order-by need a total order instead and call
|
||||
* WO_B_FLOAT_CMP for it. */
|
||||
CASE(FEQ) : {
|
||||
R[wo_ins_a(ins)] = wo_f64(R[wo_ins_b(ins)]) == wo_f64(R[wo_ins_c(ins)]) ? 1 : 0;
|
||||
NEXT();
|
||||
}
|
||||
CASE(FLT) : {
|
||||
R[wo_ins_a(ins)] = wo_f64(R[wo_ins_b(ins)]) < wo_f64(R[wo_ins_c(ins)]) ? 1 : 0;
|
||||
NEXT();
|
||||
}
|
||||
CASE(FLE) : {
|
||||
R[wo_ins_a(ins)] = wo_f64(R[wo_ins_b(ins)]) <= wo_f64(R[wo_ins_c(ins)]) ? 1 : 0;
|
||||
NEXT();
|
||||
}
|
||||
|
||||
CASE(JMP) : {
|
||||
if (wo_ins_sbx(ins) < 0) {
|
||||
GC_SAFEPOINT(); /* loop back-edge */
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ void wo_tls_set(wo_vm *vm);
|
|||
/* Start shards 1..n-1 (0 is the caller's, already init'ed in shards[0]).
|
||||
* 0 ok. Stop joins every worker and destroys their vms. */
|
||||
int wo_engine_start(const wo_module *mod, size_t heap_cap, uint32_t nshards);
|
||||
int wo_engine_primary_inbox(int wake_efd);
|
||||
void wo_engine_stop(void);
|
||||
|
||||
/* Spawn a fiber that will run method_idx(args) — the runtime half the
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@
|
|||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h> /* memcpy: the f64 <-> u64 bitcast (iteration 19) */
|
||||
|
||||
/* ---- file header (44 bytes, absolute offsets) ---- */
|
||||
#define WOB_MAGIC 0x31424F57u /* "WOB1" read as LE u32 */
|
||||
#define WOB_VERSION 4u /* v4 (iteration 7b): opcodes 27-28 (RC_INC/RC_DEC) retired;
|
||||
* the drop table's gc mask now means "GC roots at this pc" */
|
||||
#define WOB_VERSION 5u /* v5 (iteration 19): the two missing scalars. New
|
||||
* constant tag WOB_K_FLOAT, field kinds WO_K_FLOAT/WO_K_BYTES (WO_K_MAX 5->7),
|
||||
* opcodes 34-41 (the f64 arithmetic/compare set), builtins 70-82.
|
||||
* v4 (iteration 7b): opcodes 27-28 (RC_INC/RC_DEC) retired; the drop table's
|
||||
* gc mask now means "GC roots at this pc" */
|
||||
#define WOB_HDR_SIZE 44u
|
||||
#define WOB_OFF_MAGIC 0u
|
||||
#define WOB_OFF_VERSION 4u
|
||||
|
|
@ -56,6 +60,11 @@
|
|||
* (nil spelled WO_NIL_SCALAR, exactly like NIL_SCALAR, plus bool encoding). */
|
||||
#define WOB_FIELD_BOOL 0xFFFFFFFCu
|
||||
#define WOB_FIELD_NIL_BOOL 0xFFFFFFFBu
|
||||
/* iteration 19: a `?Float` field. Marks WHICH nil sentinel the slot uses —
|
||||
* WO_NIL_FLOAT, not WO_NIL_SCALAR. A plain `Float` needs no marker at all
|
||||
* (WO_K_FLOAT is a real kind byte, unlike Bool). `?Bytes` needs none either:
|
||||
* it is pointer-shaped, so the zero word is unambiguous absence. */
|
||||
#define WOB_FIELD_NIL_FLOAT 0xFFFFFFFAu
|
||||
|
||||
/* nil for a nullable scalar: -(2^62). Not INT64_MIN, deliberately — the
|
||||
* compiler's own integers are OCaml's 63-bit native ints, so INT64_MIN is not
|
||||
|
|
@ -63,9 +72,25 @@
|
|||
* inside a `?Int`; that is the whole cost of the choice. */
|
||||
#define WO_NIL_SCALAR ((uint64_t)(int64_t)(-4611686018427387904LL))
|
||||
|
||||
/* nil for a `?Float` (iteration 19). WO_NIL_SCALAR cannot serve: its bit
|
||||
* pattern IS -2.0 as an f64, and -2.0 is an ordinary price delta. Nor can the
|
||||
* zero word: that is +0.0. So nil is a quiet NaN carrying a reserved payload.
|
||||
* Arithmetic on this platform produces the CANONICAL quiet NaN
|
||||
* (0x7FF8000000000000), so a computed NaN never collides with it — the
|
||||
* iteration's own edge-case gate stores NaN and reads NaN back. The whole cost
|
||||
* of the choice is that one NaN payload out of 2^51 is unavailable inside a
|
||||
* `?Float`, exactly as one absurd integer is unavailable inside a `?Int`. */
|
||||
#define WO_NIL_FLOAT 0x7FF8000000000EE1ull
|
||||
|
||||
/* ---- 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) */
|
||||
/* iteration 19: tag byte, then the f64's IEEE 754 bit pattern as an LE u64.
|
||||
* Bits, not a decimal rendering — a literal must reach the VM bit-exact, and
|
||||
* the emitter is OCaml (whose float IS an f64) so no conversion happens at
|
||||
* all. There is no Bytes constant tag: Bytes has no literal form by design
|
||||
* (settled decision 3 — it is built from base64/net/slices). */
|
||||
#define WOB_K_FLOAT 2u
|
||||
|
||||
/* ---- field kinds (one byte per field in the class table) ---- */
|
||||
enum {
|
||||
|
|
@ -75,8 +100,16 @@ enum {
|
|||
WO_K_TEXT = 3,
|
||||
WO_K_MULTI = 4,
|
||||
WO_K_MAP = 5,
|
||||
/* iteration 19. FLOAT is word-shaped like SCALAR — the slot holds f64
|
||||
* bits — but it needs its own kind for three services that cannot guess
|
||||
* from a register: json (a Float field emits 9.99, not 4621...), the WAL
|
||||
* (replay must not reinterpret bits), and printing. BYTES is
|
||||
* pointer-shaped like TEXT and shares the wo_str object layout, with its
|
||||
* own class-id sentinel so no Text builtin silently accepts one. */
|
||||
WO_K_FLOAT = 6,
|
||||
WO_K_BYTES = 7,
|
||||
};
|
||||
#define WO_K_MAX 5u
|
||||
#define WO_K_MAX 7u
|
||||
|
||||
/* ---- loader-enforced limits ---- */
|
||||
#define WO_MAX_REGS 64u
|
||||
|
|
@ -118,6 +151,12 @@ _Static_assert(sizeof(wo_hdr) == 16, "object header must be exactly 16 bytes");
|
|||
#define WO_CLS_STR 0xFFFFFFFCu
|
||||
#define WO_CLS_MAP 0xFFFFFFFDu
|
||||
#define WO_CLS_MULTI 0xFFFFFFFEu
|
||||
/* iteration 19: Bytes reuses the wo_str object layout byte for byte (header,
|
||||
* len, bytes) and differs ONLY in this header class_id. That is deliberate:
|
||||
* every allocation, drop, and copy path already handles the shape, while the
|
||||
* distinct id is what lets a Text builtin refuse a Bytes and vice versa —
|
||||
* settled decision 3, "Text goes back to meaning text". */
|
||||
#define WO_CLS_BYTES 0xFFFFFFFBu
|
||||
|
||||
/* borrow word states */
|
||||
#define WO_BORROW_FREE 0u
|
||||
|
|
@ -193,8 +232,23 @@ enum {
|
|||
* today's surface. */
|
||||
WOP_TRY = 32, /* A sBx: push catch frame, handler at pc + sBx */
|
||||
WOP_ENDTRY = 33, /* pop the innermost catch frame */
|
||||
/* iteration 19: the f64 world. Registers stay u64 — these ops bitcast,
|
||||
* compute, and bitcast back, so there is no layout change anywhere. They
|
||||
* are separate opcodes rather than a mode bit on ADD/DIV because the
|
||||
* compiler always knows the static type and because the two worlds have
|
||||
* different failure semantics: WOP_DIV traps DIV0, WOP_FDIV never traps
|
||||
* (IEEE quiet — Inf and NaN flow). No opcode here mixes an Int operand
|
||||
* with a Float one; `float(i)` and `trunc(f)` are the only bridges. */
|
||||
WOP_FADD = 34, /* A B C: f64 */
|
||||
WOP_FSUB = 35,
|
||||
WOP_FMUL = 36,
|
||||
WOP_FDIV = 37, /* never traps: x/0.0 is ±Inf, 0.0/0.0 is NaN */
|
||||
WOP_FNEG = 38, /* A B — sign flip, so -0.0 is reachable */
|
||||
WOP_FEQ = 39, /* A B C: IEEE equality, so NaN == NaN is 0 */
|
||||
WOP_FLT = 40, /* IEEE ordered <: any comparison with NaN is 0 */
|
||||
WOP_FLE = 41,
|
||||
};
|
||||
#define WOP_MAX 33u
|
||||
#define WOP_MAX 41u
|
||||
|
||||
/* ---- builtin ids (WOP_BUILTIN operand C) ---- */
|
||||
enum {
|
||||
|
|
@ -341,9 +395,44 @@ enum {
|
|||
* order-by on a Text key; scalars use the LT opcode) */
|
||||
WO_B_SPAWN = 68, /* (instance, receive_method_idx) -> actor address (arc) */
|
||||
WO_B_SEND = 69, /* (address, msg) — msg moves to the runtime (arc) */
|
||||
/* ---- iteration 19: the Float bridges and surface. There is NO implicit
|
||||
* coercion anywhere, so every crossing between the two numeric worlds is
|
||||
* one of these calls, visible in the source. ---- */
|
||||
WO_B_FLOAT_OF_INT = 70, /* (i64) -> f64 bits. `float(i)`. Exact below 2^53,
|
||||
* round-to-nearest above — the hardware's rule */
|
||||
WO_B_TRUNC = 71, /* (f64) -> i64 toward zero. `trunc(f)`. NaN, ±Inf,
|
||||
* and anything outside i64 trap WO_T_BOUNDS: the
|
||||
* Int world has no value to give back, and
|
||||
* silently yielding 0 is how currency bugs start */
|
||||
WO_B_PARSE_FLOAT = 72, /* (text) -> f64 bits; unparseable is NaN, which is
|
||||
* exactly "not a number" and needs no ?Float */
|
||||
WO_B_FLOAT_TO_TEXT = 73, /* (f64) -> fresh Text, SHORTEST round-trip form
|
||||
* (%.17g trimmed to the shortest that reparses
|
||||
* equal). Drives interpolation and json.encode */
|
||||
WO_B_FLOAT_CMP = 74, /* (a, b) -> -1/0/1 TOTAL order: -Inf < finite <
|
||||
* +Inf < NaN, and -0.0 == +0.0. Not IEEE — an
|
||||
* index and an order-by REQUIRE a total order, so
|
||||
* this is the one deliberate deviation, and it
|
||||
* lives in its own builtin rather than bending
|
||||
* WOP_FLT (which stays IEEE for the language) */
|
||||
/* ---- iteration 19: Bytes. Length-carrying, content-comparable, no
|
||||
* literal form; every accessor refuses a Text and every Text builtin
|
||||
* refuses a Bytes (WO_T_BOUNDS on the wrong class id). ---- */
|
||||
WO_B_BYTES_LEN = 75, /* (bytes) -> i64 */
|
||||
WO_B_BYTES_AT = 76, /* (bytes, i) -> i64 byte; out of range traps BOUNDS */
|
||||
WO_B_BYTES_SLICE = 77, /* (bytes, start, len) -> fresh Bytes, clamped */
|
||||
WO_B_BYTES_EQ = 78, /* (a, b) -> 1/0 by content */
|
||||
WO_B_BYTES_CONCAT = 79, /* (a, b) -> fresh Bytes */
|
||||
WO_B_BASE64_ENCODE = 80, /* (bytes) -> fresh Text, standard alphabet + pad */
|
||||
WO_B_BASE64_DECODE = 81, /* (text) -> ?Bytes; malformed is nil, not a trap,
|
||||
* because base64 arrives from the network */
|
||||
WO_B_BYTES_OF_TEXT = 82, /* (text) -> fresh Bytes, the bytes as they are */
|
||||
WO_B_TEXT_OF_BYTES = 83, /* (bytes) -> fresh Text, verbatim. The caller
|
||||
* asserts the bytes are text; no validation,
|
||||
* because Unicode is explicitly out of scope */
|
||||
};
|
||||
|
||||
#define WO_B_MAX 69u
|
||||
#define WO_B_MAX 83u
|
||||
/* ids at or above this one live in sysio.c, not builtin.c */
|
||||
#define WO_B_SYS_FIRST WO_B_FS_EXISTS
|
||||
|
||||
|
|
@ -369,6 +458,37 @@ static inline uint8_t wo_ins_c(uint32_t i) { return (uint8_t)((i >> 24) & 0xFFu)
|
|||
static inline uint16_t wo_ins_bx(uint32_t i) { return (uint16_t)(i >> 16); }
|
||||
static inline int32_t wo_ins_sbx(uint32_t i) { return (int32_t)wo_ins_bx(i) - 32768; }
|
||||
|
||||
/* ---- iteration 19: the f64 <-> register bitcast, and the total order ----
|
||||
* A register is a u64 and a Float is an f64 in it. memcpy is the only
|
||||
* strict-aliasing-clean cast; every compiler this project targets folds these
|
||||
* to zero instructions (the value already sits in the right register class or
|
||||
* is one movq away). Do NOT reintroduce a union or a pointer cast here. */
|
||||
static inline double wo_f64(uint64_t bits) {
|
||||
double d;
|
||||
memcpy(&d, &bits, sizeof d);
|
||||
return d;
|
||||
}
|
||||
static inline uint64_t wo_bits(double d) {
|
||||
uint64_t b;
|
||||
memcpy(&b, &d, sizeof b);
|
||||
return b;
|
||||
}
|
||||
|
||||
/* The TOTAL order (WO_B_FLOAT_CMP, indexes, order-by): -Inf < finite < +Inf <
|
||||
* NaN, with -0.0 equal to +0.0. IEEE's own comparisons are not a total order —
|
||||
* NaN is unordered against everything, which would make a sort's result depend
|
||||
* on the comparison sequence and a B-tree walk lose rows. Sorting NaN last is
|
||||
* therefore not a preference but a requirement, and it is the ONE place this
|
||||
* iteration deviates from raw IEEE. The language's own `<` (WOP_FLT) keeps
|
||||
* IEEE semantics, so `NaN < 1.0` is still false in source. */
|
||||
static inline int wo_float_cmp(double a, double b) {
|
||||
int an = a != a, bn = b != b; /* NaN is the only value unequal to itself */
|
||||
if (an || bn) return an && bn ? 0 : (an ? 1 : -1);
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0; /* covers -0.0 vs +0.0: they compare equal, deliberately */
|
||||
}
|
||||
|
||||
/* ---- class descriptor shared by loader and runtime ---- */
|
||||
typedef struct wo_classdesc {
|
||||
uint32_t name; /* constant index of the class name */
|
||||
|
|
|
|||
|
|
@ -264,11 +264,71 @@ static void test_crash_battery(void) {
|
|||
}
|
||||
}
|
||||
|
||||
/* iteration 19: a Float column and a Bytes column survive a WAL round trip
|
||||
* BIT-EXACT. Bit-exact is the whole assertion — the durability path must not
|
||||
* render a float as decimal anywhere, or NaN, the infinities and -0.0 would
|
||||
* each come back as something else. Bytes goes through the same length-
|
||||
* prefixed blob a Text does and must come back as a Bytes, not a Text. */
|
||||
static const uint8_t fb_kinds[] = {WO_K_FLOAT, WO_K_BYTES};
|
||||
static const wo_classdesc FB_CLASSES[] = {
|
||||
{.name = 0, .flags = 0, .field_cnt = 2, .kinds = fb_kinds},
|
||||
};
|
||||
|
||||
static void test_float_bytes_replay(void) {
|
||||
char path[128];
|
||||
snprintf(path, sizeof path, "%s/floatbytes.wal", g_dir);
|
||||
wo_rt rt;
|
||||
T_EQ(wo_rt_init(&rt, 1 << 20, FB_CLASSES, 1), 0);
|
||||
wo_db db;
|
||||
T_EQ(wo_db_init(&db, FB_CLASSES, 1, 0, 1), 0);
|
||||
wo_wal w;
|
||||
T_EQ(wo_wal_open(&w, path, 1 << 16), 0);
|
||||
const char *msg = "";
|
||||
|
||||
/* the values a decimal round trip would destroy, plus a NUL-bearing blob
|
||||
* that a NUL-terminated string path would truncate */
|
||||
const double vals_f[] = {9.99, 0.0 / 0.0, 1.0 / 0.0, -1.0 / 0.0, -0.0, 1e308};
|
||||
const char blob[] = {'a', '\0', 'b'};
|
||||
enum { N = sizeof vals_f / sizeof vals_f[0] };
|
||||
uint64_t ids[N];
|
||||
for (int i = 0; i < N; i++) {
|
||||
wo_str *b = wo_bytes_new(&rt, blob, sizeof blob);
|
||||
T_CHECK(b != NULL);
|
||||
uint64_t vals[2] = {wo_bits(vals_f[i]), (uint64_t)(uintptr_t)b};
|
||||
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
|
||||
T_CHECK(ids[i] != 0);
|
||||
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
|
||||
wo_str_free(&rt, b);
|
||||
}
|
||||
T_EQ(wo_wal_commit(&w), 0);
|
||||
wo_wal_close(&w);
|
||||
wo_db_destroy(&db);
|
||||
|
||||
wo_db db2;
|
||||
T_EQ(wo_db_init(&db2, FB_CLASSES, 1, 0, 1), 0);
|
||||
T_EQ(wo_wal_replay(path, &db2), N);
|
||||
for (int i = 0; i < N; i++) {
|
||||
uint64_t out[2];
|
||||
T_EQ(wo_row_read(&db2, &rt, 0, ids[i], out, &msg), 0);
|
||||
/* BITS, not value: NaN != NaN and -0.0 == 0.0, so a value comparison
|
||||
* would pass while silently having lost the payload or the sign */
|
||||
T_EQ(out[0], wo_bits(vals_f[i]));
|
||||
wo_str *b = (wo_str *)(uintptr_t)out[1];
|
||||
T_CHECK(b != NULL);
|
||||
T_EQ(b->h.class_id, WO_CLS_BYTES); /* a Bytes column yields a Bytes */
|
||||
T_CHECK(b->len == sizeof blob && memcmp(b->data, blob, sizeof blob) == 0);
|
||||
wo_str_free(&rt, b);
|
||||
}
|
||||
wo_db_destroy(&db2);
|
||||
wo_rt_destroy(&rt);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_dir, sizeof g_dir, "/tmp/wo-wal-test-XXXXXX");
|
||||
if (!mkdtemp(g_dir)) return 1;
|
||||
test_roundtrip_replay();
|
||||
test_torn_tail();
|
||||
test_float_bytes_replay();
|
||||
test_crash_battery();
|
||||
/* leave the dir for a failed run's forensics only */
|
||||
if (!t_fail) {
|
||||
|
|
|
|||
111
scripts/linkcheck.py
Normal file
111
scripts/linkcheck.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Scan every .md in repo, extract links, report broken local targets + bad anchors."""
|
||||
import os, re, sys, urllib.parse
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else ".")
|
||||
|
||||
SKIP_DIRS = {".git", "node_modules", "_build", "target", "dist"}
|
||||
|
||||
INLINE = re.compile(r'(?<!!)\[([^\]\n]*)\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)')
|
||||
REFDEF = re.compile(r'^\s{0,3}\[([^\]]+)\]:\s*<?(\S+)>?', re.M)
|
||||
FENCE = re.compile(r'(^```.*?^```|^~~~.*?^~~~)', re.M | re.S)
|
||||
INLINE_CODE = re.compile(r'`[^`\n]*`')
|
||||
ATX = re.compile(r'^(#{1,6})\s+(.*?)\s*#*\s*$', re.M)
|
||||
HTML_ANCHOR = re.compile(r'<a\s+[^>]*(?:name|id)=["\']([^"\']+)["\']', re.I)
|
||||
|
||||
def strip_code(text):
|
||||
text = FENCE.sub(lambda m: "\n" * m.group(0).count("\n"), text)
|
||||
return INLINE_CODE.sub("", text)
|
||||
|
||||
def slugify(heading):
|
||||
# GitHub-style slug
|
||||
h = re.sub(r'<[^>]+>', '', heading)
|
||||
h = re.sub(r'!?\[([^\]]*)\]\([^)]*\)', r'\1', h) # links -> text
|
||||
h = h.replace('`', '').replace('*', '').replace('_', '')
|
||||
h = h.strip().lower()
|
||||
h = re.sub(r'[^\w\- ]', '', h, flags=re.UNICODE)
|
||||
return h.replace(' ', '-')
|
||||
|
||||
md_files = []
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||
for f in filenames:
|
||||
if f.lower().endswith((".md", ".markdown")):
|
||||
md_files.append(os.path.join(dirpath, f))
|
||||
md_files.sort()
|
||||
|
||||
anchors = {}
|
||||
def get_anchors(path):
|
||||
if path in anchors:
|
||||
return anchors[path]
|
||||
try:
|
||||
raw = open(path, encoding="utf-8", errors="replace").read()
|
||||
except OSError:
|
||||
anchors[path] = set()
|
||||
return anchors[path]
|
||||
body = strip_code(raw)
|
||||
s = set()
|
||||
seen = defaultdict(int)
|
||||
for _, head in ATX.findall(body):
|
||||
base = slugify(head)
|
||||
if not base:
|
||||
continue
|
||||
n = seen[base]
|
||||
seen[base] += 1
|
||||
s.add(base if n == 0 else f"{base}-{n}")
|
||||
s |= set(HTML_ANCHOR.findall(raw))
|
||||
anchors[path] = s
|
||||
return s
|
||||
|
||||
broken = []
|
||||
anchor_bad = []
|
||||
stats = defaultdict(int)
|
||||
|
||||
for md in md_files:
|
||||
raw = open(md, encoding="utf-8", errors="replace").read()
|
||||
body = strip_code(raw)
|
||||
lines = body.split("\n")
|
||||
targets = []
|
||||
for m in INLINE.finditer(body):
|
||||
targets.append((body[:m.start()].count("\n") + 1, m.group(2)))
|
||||
for m in REFDEF.finditer(body):
|
||||
targets.append((body[:m.start()].count("\n") + 1, m.group(2)))
|
||||
|
||||
for lineno, target in targets:
|
||||
t = target.strip()
|
||||
if not t:
|
||||
continue
|
||||
low = t.lower()
|
||||
if low.startswith(("http://", "https://", "mailto:", "ftp://", "tel:", "data:")):
|
||||
stats["external"] += 1
|
||||
continue
|
||||
if t.startswith("#"):
|
||||
stats["anchor-local"] += 1
|
||||
frag = urllib.parse.unquote(t[1:])
|
||||
if frag and frag not in get_anchors(md):
|
||||
anchor_bad.append((md, lineno, t))
|
||||
continue
|
||||
stats["local"] += 1
|
||||
pathpart, _, frag = t.partition("#")
|
||||
pathpart = urllib.parse.unquote(pathpart)
|
||||
frag = urllib.parse.unquote(frag)
|
||||
if pathpart.startswith("/"):
|
||||
cand = os.path.join(ROOT, pathpart.lstrip("/"))
|
||||
else:
|
||||
cand = os.path.normpath(os.path.join(os.path.dirname(md), pathpart))
|
||||
if not os.path.exists(cand):
|
||||
broken.append((md, lineno, t))
|
||||
continue
|
||||
if frag and cand.lower().endswith((".md", ".markdown")):
|
||||
if frag not in get_anchors(cand):
|
||||
anchor_bad.append((md, lineno, t))
|
||||
|
||||
rel = lambda p: os.path.relpath(p, ROOT)
|
||||
print(f"files={len(md_files)} links: local={stats['local']} external={stats['external']} same-file-anchor={stats['anchor-local']}")
|
||||
print(f"\n== BROKEN PATHS ({len(broken)}) ==")
|
||||
for md, ln, t in broken:
|
||||
print(f"{rel(md)}:{ln} -> {t}")
|
||||
print(f"\n== BAD ANCHORS ({len(anchor_bad)}) ==")
|
||||
for md, ln, t in anchor_bad:
|
||||
print(f"{rel(md)}:{ln} -> {t}")
|
||||
|
|
@ -3,7 +3,8 @@
|
|||
# chain at run time, network-free: a temp git remote is built from
|
||||
# docs/examples/writeonce-framework, its file:// URL is substituted into a
|
||||
# temp copy of docs/examples/web-app, then: fetch -> lock -> build -> serve ->
|
||||
# the storefront matrix -> SIGTERM -> restart persistence. The repo itself
|
||||
# the storefront matrix -> SIGTERM -> restart persistence, plus iteration 17's
|
||||
# library-kind and internal/-boundary checks. The repo itself
|
||||
# never carries .wo-deps/wo.lock artifacts.
|
||||
set -uo pipefail
|
||||
|
||||
|
|
@ -49,6 +50,37 @@ else
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# ---- iteration 17: library kind + the internal/ dep boundary ----
|
||||
# The framework copy at $W/fw carries `kind = "library"` and has no `fn main`.
|
||||
# Checking it entry-less is what retired iteration 16's `--emit` workaround.
|
||||
if out="$("$WOC" "$W/fw" 2>&1)" && [[ -z "$out" ]]; then
|
||||
ok "library check mode: the framework typechecks entry-less"
|
||||
else
|
||||
bad "library-check" "exit=$? out=$(printf '%s' "$out" | head -1)"
|
||||
fi
|
||||
|
||||
# A consumer reaching past the privacy line is WO-E108 at its own `use`.
|
||||
cp -r "$W/app" "$W/app-internal"
|
||||
rm -rf "$W/app-internal/target" "$W/app-internal/wo.lock" "$W/app-internal/.wo-deps"
|
||||
sed -i '1i use framework/internal' "$W/app-internal/main.wo"
|
||||
out="$("$WOC" "$W/app-internal" 2>&1)"; rc=$?
|
||||
if [[ "$rc" == "1" ]] && printf '%s' "$out" | grep -q 'WO-E108'; then
|
||||
ok "dep boundary: importing framework/internal is WO-E108"
|
||||
else
|
||||
bad "internal-boundary" "exit=$rc out=$(printf '%s' "$out" | head -1)"
|
||||
fi
|
||||
|
||||
# An unknown `kind` is a manifest error (exit 2), not a silent default.
|
||||
cp -r "$W/app" "$W/app-badkind"
|
||||
rm -rf "$W/app-badkind/target" "$W/app-badkind/wo.lock" "$W/app-badkind/.wo-deps"
|
||||
sed -i '1i kind = "junk"' "$W/app-badkind/wo.toml"
|
||||
out="$("$WOC" "$W/app-badkind" 2>&1)"; rc=$?
|
||||
if [[ "$rc" == "2" ]] && printf '%s' "$out" | grep -q 'WO-E109'; then
|
||||
ok "manifest: an unknown kind is WO-E109 at exit 2"
|
||||
else
|
||||
bad "kind-validation" "exit=$rc out=$(printf '%s' "$out" | head -1)"
|
||||
fi
|
||||
|
||||
DATA="$W/data"; mkdir -p "$DATA"
|
||||
WA_TOKEN=s3cr3t WO_DATA="$DATA" "$W/app/target/web-app" "$PORT" >"$W/srv.out" 2>&1 &
|
||||
SRV=$!
|
||||
|
|
@ -105,19 +137,27 @@ PYEOF
|
|||
[[ "$wt" == "401" ]] && ok "401 on a wrong bearer token (ct_eq)" \
|
||||
|| bad "wrong-token" "got $wt"
|
||||
expect "empty list" "$(hit GET /products)" 200 "[]"
|
||||
expect "create product (201)" "$(hit POST /products '{"name":"mug","price":900,"stock":5}')" 201 '"name":"mug"'
|
||||
expect "duplicate name is 409 (@unique)" "$(hit POST /products '{"name":"mug","price":1,"stock":1}')" 409
|
||||
# iteration 19: a REAL decimal price. `{"price": 9.99}` is the acceptance
|
||||
# criterion — before Float existed this body failed the whole checked decode.
|
||||
expect "create product (201, fractional price)" \
|
||||
"$(hit POST /products '{"name":"mug","price":9.99,"stock":5}')" 201 '"price":9.99'
|
||||
expect "duplicate name is 409 (@unique)" "$(hit POST /products '{"name":"mug","price":1.0,"stock":1}')" 409
|
||||
# an integer-shaped JSON number is a legal Float too, and comes back as one
|
||||
expect "integer-shaped price decodes as Float" \
|
||||
"$(hit POST /products '{"name":"plate","price":12,"stock":1}')" 201 '"price":12.0'
|
||||
expect "malformed json is 400" "$(hit POST /products '{oops')" 400
|
||||
expect "form-encoded create (201, + and %XX decoded)" \
|
||||
"$(hit POST /products 'name=form+kettle&price=1250&stock=2' yes 'application/x-www-form-urlencoded; charset=UTF-8')" 201 '"name":"form kettle"'
|
||||
"$(hit POST /products 'name=form+kettle&price=12.50&stock=2' yes 'application/x-www-form-urlencoded; charset=UTF-8')" 201 '"name":"form kettle"'
|
||||
expect "form with a non-numeric price is 400" \
|
||||
"$(hit POST /products 'name=x&price=abc&stock=1' yes 'application/x-www-form-urlencoded')" 400
|
||||
MP=$'--BXB\r\ncontent-disposition: form-data; name="name"\r\n\r\nmp teapot\r\n--BXB\r\ncontent-disposition: form-data; name="price"\r\n\r\n700\r\n--BXB\r\ncontent-disposition: form-data; name="stock"\r\n\r\n3\r\n--BXB--\r\n'
|
||||
MP=$'--BXB\r\ncontent-disposition: form-data; name="name"\r\n\r\nmp teapot\r\n--BXB\r\ncontent-disposition: form-data; name="price"\r\n\r\n7.05\r\n--BXB\r\ncontent-disposition: form-data; name="stock"\r\n\r\n3\r\n--BXB--\r\n'
|
||||
expect "multipart create (201, curl -F shape)" \
|
||||
"$(hit POST /products "$MP" yes 'multipart/form-data; boundary=BXB')" 201 '"name":"mp teapot"'
|
||||
expect "multipart without the closing marker is 400" \
|
||||
"$(hit POST /products $'--BXB\r\ncontent-disposition: form-data; name="name"\r\n\r\nx\r\n' yes 'multipart/form-data; boundary=BXB')" 400
|
||||
expect "list shows the product" "$(hit GET /products)" 200 '"price":900'
|
||||
# the fractional price survives storage and comes back byte-identical
|
||||
expect "list shows the fractional price" "$(hit GET /products)" 200 '"price":9.99'
|
||||
expect "list shows the form price" "$(hit GET /products)" 200 '"price":12.5'
|
||||
expect "show by :name capture" "$(hit GET /products/mug)" 200 '"stock":5'
|
||||
expect "unknown product is 404" "$(hit GET /products/none)" 404
|
||||
expect "create order (FK)" "$(hit POST /orders '{"product":"mug","qty":2}')" 201
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
WO-E003
|
||||
6
tests/corpus/compile-fail/ifdef-unterminated/fixture.wo
Normal file
6
tests/corpus/compile-fail/ifdef-unterminated/fixture.wo
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- a #if left open at end of file is WO-E003
|
||||
|
||||
fn main() -> Int {
|
||||
#if portable
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
WO-E219
|
||||
17
tests/corpus/compile-fail/pub-read-external-write/fixture.wo
Normal file
17
tests/corpus/compile-fail/pub-read-external-write/fixture.wo
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- pub(read): readable anywhere, writable only inside the declaring class.
|
||||
-- A write from outside (here: main) is WO-E219.
|
||||
|
||||
class Counter {
|
||||
pub(read) count: Int
|
||||
label: Text
|
||||
|
||||
fn bump() {
|
||||
self.count = self.count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let c = Counter { count: 0, label: "hits" };
|
||||
c.count = 99;
|
||||
return c.count;
|
||||
}
|
||||
3
tests/corpus/compile-fail/using-collision/ext/fns.wo
Normal file
3
tests/corpus/compile-fail/using-collision/ext/fns.wo
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub fn label(b: Box) -> Text {
|
||||
return "extension";
|
||||
}
|
||||
1
tests/corpus/compile-fail/using-collision/fixture.code
Normal file
1
tests/corpus/compile-fail/using-collision/fixture.code
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E220
|
||||
16
tests/corpus/compile-fail/using-collision/fixture.wo
Normal file
16
tests/corpus/compile-fail/using-collision/fixture.wo
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- a using extension colliding with a real method is WO-E220 — an
|
||||
-- extension never silently overrides (or loses to) a method
|
||||
using ext
|
||||
|
||||
class Box {
|
||||
n: Int
|
||||
fn label() -> Text {
|
||||
return "method";
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let b = Box { n: 1 };
|
||||
print(b.label());
|
||||
return 0;
|
||||
}
|
||||
16
tests/corpus/run/bytes-carrier/fixture.out
Normal file
16
tests/corpus/run/bytes-carrier/fixture.out
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
9
|
||||
119
|
||||
write
|
||||
once
|
||||
0
|
||||
1
|
||||
0
|
||||
write!
|
||||
d3JpdGVvbmNl
|
||||
YWI=
|
||||
YQ==
|
||||
1
|
||||
3
|
||||
0
|
||||
1
|
||||
1
|
||||
43
tests/corpus/run/bytes-carrier/fixture.wo
Normal file
43
tests/corpus/run/bytes-carrier/fixture.wo
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
-- Iteration 19: Bytes, the binary carrier. Length-carrying, content-
|
||||
-- comparable, no literal form — built from text or from base64, which is how
|
||||
-- it will arrive once WebSocket frames and digests exist (iteration 24 and
|
||||
-- the crypto fork). Text goes back to meaning text.
|
||||
fn main() {
|
||||
let raw = bytes_of_text("writeonce")
|
||||
print_int(bytes_len(raw))
|
||||
print_int(bytes_at(raw, 0))
|
||||
|
||||
-- slicing is clamped like substr, and produces a fresh Bytes
|
||||
let head = bytes_slice(raw, 0, 5)
|
||||
print(text_of_bytes(head))
|
||||
print(text_of_bytes(bytes_slice(raw, 5, 99)))
|
||||
print_int(bytes_len(bytes_slice(raw, 99, 3)))
|
||||
|
||||
-- content equality, not identity
|
||||
print_int(bytes_eq(raw, bytes_of_text("writeonce")))
|
||||
print_int(bytes_eq(raw, head))
|
||||
|
||||
-- concat
|
||||
print(text_of_bytes(bytes_concat(head, bytes_of_text("!"))))
|
||||
|
||||
-- base64 round trip, including the two padding shapes
|
||||
print(base64_encode(raw))
|
||||
print(base64_encode(bytes_of_text("ab")))
|
||||
print(base64_encode(bytes_of_text("a")))
|
||||
let back = base64_decode("d3JpdGVvbmNl")
|
||||
if back != nil {
|
||||
print_int(bytes_eq(back, raw))
|
||||
}
|
||||
|
||||
-- binary safety: a NUL byte in the middle survives, which is the whole
|
||||
-- reason Text-as-bytes was a hole
|
||||
let nul = base64_decode("YQBi")
|
||||
if nul != nil {
|
||||
print_int(bytes_len(nul))
|
||||
print_int(bytes_at(nul, 1))
|
||||
}
|
||||
|
||||
-- malformed base64 is nil, not a trap: it arrives from the network
|
||||
print_int(base64_decode("!!!!") == nil)
|
||||
print_int(base64_decode("abc") == nil)
|
||||
}
|
||||
19
tests/corpus/run/float-arithmetic/fixture.out
Normal file
19
tests/corpus/run/float-arithmetic/fixture.out
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
9.99 0.125 20000000000.0 0.0015
|
||||
0.30000000000000004
|
||||
29.97
|
||||
-0.009999999999999787
|
||||
inf -inf nan
|
||||
0
|
||||
1
|
||||
-0.0
|
||||
1
|
||||
3.5
|
||||
9
|
||||
-9
|
||||
3.5 nan
|
||||
1
|
||||
1
|
||||
0
|
||||
-1
|
||||
1
|
||||
0
|
||||
54
tests/corpus/run/float-arithmetic/fixture.wo
Normal file
54
tests/corpus/run/float-arithmetic/fixture.wo
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
-- Iteration 19: Float, the language half. Literal forms, f64 arithmetic,
|
||||
-- IEEE-quiet division (no DIV0 trap where Int would trap), the explicit
|
||||
-- bridges in both directions, and the shortest-round-trip rendering that
|
||||
-- interpolation and json.encode share.
|
||||
fn main() {
|
||||
-- literal forms: fraction, leading zero, exponent (both signs)
|
||||
let price = 9.99
|
||||
let tiny = 0.125
|
||||
let big = 2e10
|
||||
let small = 1.5e-3
|
||||
print("${price} ${tiny} ${big} ${small}")
|
||||
|
||||
-- arithmetic is f64, not integer: 0.1 + 0.2 is famously not 0.3, and
|
||||
-- printing the truth here is the point of a shortest-round-trip renderer
|
||||
print("${0.1 + 0.2}")
|
||||
print("${price * 3.0}")
|
||||
print("${price - 10.0}")
|
||||
|
||||
-- IEEE quiet division: Int would trap DIV0, Float yields infinities and
|
||||
-- NaN and keeps running
|
||||
print("${1.0 / 0.0} ${-1.0 / 0.0} ${0.0 / 0.0}")
|
||||
|
||||
-- NaN is not equal to itself; that is the contract, not a bug
|
||||
let nan = 0.0 / 0.0
|
||||
print_int(nan == nan)
|
||||
print_int(nan != nan)
|
||||
|
||||
-- -0.0 is reachable and distinct in its bits, while IEEE equality says
|
||||
-- it equals +0.0
|
||||
let negzero = -0.0
|
||||
print("${negzero}")
|
||||
print_int(negzero == 0.0)
|
||||
|
||||
-- the explicit bridges: no implicit conversion exists in either direction
|
||||
let n = 7
|
||||
print("${float(n) / 2.0}")
|
||||
print_int(trunc(9.99))
|
||||
print_int(trunc(-9.99))
|
||||
|
||||
-- parse_float: unparseable is NaN, which is what "not a number" means,
|
||||
-- so there is no ?Float to narrow
|
||||
print("${parse_float("3.5")} ${parse_float("nope")}")
|
||||
|
||||
-- ordering uses IEEE comparisons in the language
|
||||
print_int(1.5 < 2.5)
|
||||
print_int(2.5 <= 2.5)
|
||||
print_int(nan < 1.0)
|
||||
|
||||
-- float_cmp is the TOTAL order instead: NaN sorts last, -0.0 == +0.0.
|
||||
-- This is what an index and an order-by use.
|
||||
print_int(float_cmp(1.0, 2.0))
|
||||
print_int(float_cmp(nan, 1.0))
|
||||
print_int(float_cmp(negzero, 0.0))
|
||||
}
|
||||
10
tests/corpus/run/float-json-storage/fixture.out
Normal file
10
tests/corpus/run/float-json-storage/fixture.out
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{"name":"mug","price":9.99,"discount":null,"blob":"aGk="}
|
||||
9.99
|
||||
0.25
|
||||
hi
|
||||
{"name":"mug","price":9.99,"discount":0.25,"blob":"aGk="}
|
||||
0.0025 0
|
||||
null discount read back as nil
|
||||
42.0
|
||||
int fraction still rejected
|
||||
{"name":"inf","price":null,"discount":null,"blob":""}
|
||||
65
tests/corpus/run/float-json-storage/fixture.wo
Normal file
65
tests/corpus/run/float-json-storage/fixture.wo
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
-- Iteration 19, the forcing function: `{"price": 9.99}` used to fail the
|
||||
-- whole checked decode because the language had no Float. Now a Float field
|
||||
-- decodes fractions and exponents, encodes shortest-round-trip, and an Int
|
||||
-- field's strictness is UNCHANGED (a fraction there is still malformed).
|
||||
-- A ?Float carries its own nil sentinel, and a Bytes field crosses the JSON
|
||||
-- boundary as base64.
|
||||
use json
|
||||
|
||||
class Item {
|
||||
name: Text
|
||||
price: Float
|
||||
discount: ?Float
|
||||
blob: Bytes
|
||||
}
|
||||
|
||||
class Counted {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
-- encode: shortest form, a nil ?Float is null, Bytes is base64
|
||||
print(json.encode(Item {
|
||||
name: "mug",
|
||||
price: 9.99,
|
||||
discount: nil,
|
||||
blob: bytes_of_text("hi")
|
||||
}))
|
||||
|
||||
-- decode a fraction into a Float field: the hole this iteration closes
|
||||
let a = json.decode("{\"name\":\"mug\",\"price\":9.99,\"discount\":0.25,\"blob\":\"aGk=\"}") as Item
|
||||
if a != nil {
|
||||
print("${a.price}")
|
||||
let d = a.discount
|
||||
if d != nil { print("${d}") }
|
||||
print(text_of_bytes(a.blob))
|
||||
print(json.encode(a))
|
||||
}
|
||||
|
||||
-- exponent and integer-shaped forms both land in a Float field
|
||||
let b = json.decode("{\"name\":\"x\",\"price\":2.5e-3,\"discount\":null,\"blob\":\"\"}") as Item
|
||||
if b != nil {
|
||||
print("${b.price} ${bytes_len(b.blob)}")
|
||||
-- a JSON null in a ?Float field must read back as nil, which is the whole
|
||||
-- job of the reserved-NaN sentinel: the zero word would be +0.0
|
||||
let bd = b.discount
|
||||
if bd == nil { print("null discount read back as nil") }
|
||||
}
|
||||
let c = json.decode("{\"name\":\"x\",\"price\":42,\"discount\":null,\"blob\":\"\"}") as Item
|
||||
if c != nil { print("${c.price}") }
|
||||
|
||||
-- Int strictness is untouched: a fraction in an Int field is still
|
||||
-- malformed, so the checked decode fails whole
|
||||
let bad = json.decode("{\"n\":3.7}") as Counted
|
||||
if bad == nil { print("int fraction still rejected") }
|
||||
|
||||
-- a non-finite Float has no JSON literal, so it encodes as null rather
|
||||
-- than as invalid JSON
|
||||
print(json.encode(Item {
|
||||
name: "inf",
|
||||
price: 1.0 / 0.0,
|
||||
discount: 0.0 / 0.0,
|
||||
blob: bytes_of_text("")
|
||||
}))
|
||||
return 0
|
||||
}
|
||||
18
tests/corpus/run/float-table-column/fixture.out
Normal file
18
tests/corpus/run/float-table-column/fixture.out
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-2.5 a
|
||||
0.25 c
|
||||
1.5 b
|
||||
--
|
||||
1.5 b
|
||||
0.25 c
|
||||
-2.5 a
|
||||
--
|
||||
-2.5 a
|
||||
-0.0 negzero
|
||||
0.25 c
|
||||
1.5 b
|
||||
inf inf
|
||||
nan nan
|
||||
--
|
||||
1
|
||||
ZERO-SIGN-DUP-REFUSED
|
||||
2
|
||||
40
tests/corpus/run/float-table-column/fixture.wo
Normal file
40
tests/corpus/run/float-table-column/fixture.wo
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
-- Iteration 19, the storage half: a Float is a real @table column. It is
|
||||
-- stored, read back bit-exact, indexed (unique, on the TOTAL order — so
|
||||
-- -0.0 and +0.0 are the same key), and order-by sorts by that total order
|
||||
-- rather than by raw bits, which would put negatives backwards and drop NaN
|
||||
-- wherever the comparison sequence happened to leave it.
|
||||
@table(name: "readings", index: [at])
|
||||
class Reading {
|
||||
at: Float
|
||||
label: Text
|
||||
}
|
||||
|
||||
class Priced {
|
||||
cost: Float @unique
|
||||
}
|
||||
|
||||
fn main() {
|
||||
insert Reading { at: 1.5, label: "b" }
|
||||
insert Reading { at: -2.5, label: "a" }
|
||||
insert Reading { at: 0.25, label: "c" }
|
||||
|
||||
-- ascending: -2.5 < 0.25 < 1.5. Raw-bit ordering would put -2.5 last.
|
||||
for r in from x in Reading order by x.at select x { print("${r.at} ${r.label}") }
|
||||
print("--")
|
||||
for r in from x in Reading order by x.at desc select x { print("${r.at} ${r.label}") }
|
||||
print("--")
|
||||
|
||||
-- edge values survive storage and come back bit-exact
|
||||
insert Reading { at: 1.0 / 0.0, label: "inf" }
|
||||
insert Reading { at: 0.0 / 0.0, label: "nan" }
|
||||
insert Reading { at: -0.0, label: "negzero" }
|
||||
for r in from x in Reading order by x.at select x { print("${r.at} ${r.label}") }
|
||||
print("--")
|
||||
|
||||
-- a unique Float index keys on the total order: -0.0 and +0.0 are the
|
||||
-- SAME key, so the second insert is refused
|
||||
print_int(insert Priced { cost: -0.0 })
|
||||
let dup = try insert Priced { cost: 0.0 } catch (e) nil
|
||||
if dup == nil { print("ZERO-SIGN-DUP-REFUSED") }
|
||||
print_int(insert Priced { cost: 0.5 })
|
||||
}
|
||||
3
tests/corpus/run/ifdef-flags/fixture.out
Normal file
3
tests/corpus/run/ifdef-flags/fixture.out
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
native
|
||||
neither
|
||||
done
|
||||
31
tests/corpus/run/ifdef-flags/fixture.wo
Normal file
31
tests/corpus/run/ifdef-flags/fixture.wo
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- #if build flags: undefined flags are false, #else flips, nesting works.
|
||||
-- The corpus runs woc with NO -D, so only the #else/undefined paths emit.
|
||||
|
||||
fn label() -> Text {
|
||||
#if portable
|
||||
return "portable";
|
||||
#else
|
||||
return "native";
|
||||
#end
|
||||
}
|
||||
|
||||
fn nested() -> Text {
|
||||
#if outer
|
||||
#if inner
|
||||
return "both";
|
||||
#end
|
||||
return "outer only";
|
||||
#else
|
||||
return "neither";
|
||||
#end
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
print(label());
|
||||
print(nested());
|
||||
#if debug
|
||||
print("debug build");
|
||||
#end
|
||||
print("done");
|
||||
return 0;
|
||||
}
|
||||
2
tests/corpus/run/pub-read-accessor/fixture.out
Normal file
2
tests/corpus/run/pub-read-accessor/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
a=2 b=5
|
||||
after reset b=0
|
||||
27
tests/corpus/run/pub-read-accessor/fixture.wo
Normal file
27
tests/corpus/run/pub-read-accessor/fixture.wo
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- pub(read) golden: the declaring class writes (its own instance AND a
|
||||
-- sibling instance — class-owned, not instance-owned), everyone reads.
|
||||
|
||||
class Counter {
|
||||
pub(read) count: Int
|
||||
|
||||
fn bump() {
|
||||
self.count = self.count + 1;
|
||||
}
|
||||
|
||||
fn reset(mut other: Counter) {
|
||||
other.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let a = Counter { count: 0 };
|
||||
let b = Counter { count: 5 };
|
||||
a.bump();
|
||||
a.bump();
|
||||
print("a=${a.count} b=${b.count}");
|
||||
a.reset(b);
|
||||
print("after reset b=${b.count}");
|
||||
if a.count != 2 { return 1; }
|
||||
if b.count != 0 { return 1; }
|
||||
return 0;
|
||||
}
|
||||
4
tests/corpus/run/using-extension/fixture.out
Normal file
4
tests/corpus/run/using-extension/fixture.out
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ha!
|
||||
hahaha
|
||||
plain!
|
||||
box untouched 7
|
||||
18
tests/corpus/run/using-extension/fixture.wo
Normal file
18
tests/corpus/run/using-extension/fixture.wo
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- `using` static extensions (iter 5, task 7): s.shout() rewrites to
|
||||
-- shout(s) at compile time — no dispatch table, receiver borrowed like
|
||||
-- any first argument. The plain call form keeps working.
|
||||
using textutil
|
||||
|
||||
class Box {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let s = "ha";
|
||||
print(s.shout());
|
||||
print(s.reps(3));
|
||||
print(shout("plain"));
|
||||
let b = Box { n: 7 };
|
||||
print("box untouched ${b.n}");
|
||||
return 0;
|
||||
}
|
||||
20
tests/corpus/run/using-extension/textutil/util.wo
Normal file
20
tests/corpus/run/using-extension/textutil/util.wo
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- the extension module: pub free fns whose first parameter names the
|
||||
-- receiver type become methods via `using textutil`
|
||||
pub fn shout(s: Text) -> Text {
|
||||
return "${s}!";
|
||||
}
|
||||
|
||||
pub fn reps(s: Text, n: Int) -> Text {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
while i < n {
|
||||
out = "${out}${s}";
|
||||
i = i + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
-- not pub: never a candidate
|
||||
fn hidden(s: Text) -> Text {
|
||||
return s;
|
||||
}
|
||||
Loading…
Reference in a new issue