feat: milestone 1 complete — .wob emitter, conformance corpus, single binary; GC redesign specced
- `woc` now emits `.wob` that `wovm` runs: emit.ml lowers the typed, owner-annotated AST (scope-stack registers with a >64 WO-E401 diagnostic, Lua-style call windows, ICALL by slot, dedup const pool, drop maps, line tables, implicit terminators); disasm.ml backs `--dump-bc` goldens. - Ownership lowering consumes the four owner tables verbatim; RESIDUAL is the only source of borrow ops, coalesced per operand. Review caught the emitter consuming only 2 of owner.ml's 4 residual producers — an assignment-anchored aliasing violation ran to exit 0 instead of trapping; fixed, plus a backstop raising WO-E404 for any residual region left unconsumed. - Conformance harness `scripts/oop-e2e.sh` (`just oop-e2e`): four fixture kinds with exact outcomes — byte-exact stdout, one WO-E### anchored on `error CODE:`, numeric trap code, gc trace. 25 fixtures incl. pricing-demo logic, the ownership suite, and DB_STUB's parse-but-trap. `tests/` un-ignored so the corpus is actually tracked. - `woc build` produces a self-contained binary: wovm copy + appended image + 20-byte trailer, self-exec via /proc/self/exe. Verified relocated outside the repo, argless, and against adversarial trailer corruption. - Milestone 1's five spec criteria all MET (`just oop-accept`). Criterion 3 closed by WO-E405 — the entry must return `Int`, since program mode already says its return value is the exit code — which deletes the leak class without adding return-type metadata to the format. `gc/held-cycle` retired: an externally-held cycle is not expressible in a post-exit pump. - New spec: inferred GC + incremental per-shard tri-color mark-sweep, retiring `@gc` and reference counting. Story gains iterations 7b (that work) and 9b (`@table`, relations, compiler-checked query); `.dev/reference` gains a sparse System.Linq checkout. Priority: 5→6→7 (log-watcher) then 7b, 8, 9, 9b.
This commit is contained in:
parent
d7304f23ec
commit
641a41e25c
96 changed files with 6545 additions and 102 deletions
|
|
@ -35,6 +35,7 @@ Study-tree notes:
|
|||
| Link | Points at | Why it's a reference |
|
||||
| --- | --- | --- |
|
||||
| `reference/llvm-project/` | `~/projects/llvm-project` (shallow clone) | Compiler-architecture study for the OCaml `woc` compiler: pass pipelines (`llvm/lib/Passes/`), IR design (`llvm/docs/LangRef.md`), Clang's lexer/parser/sema layering (`clang/lib/{Lex,Parse,Sema}/`), diagnostics machinery (`clang/include/clang/Basic/Diagnostic*.td`). Study-only — writeonce does NOT link against LLVM (zero-dep doctrine; `woc` emits `.wob` bytecode, no LLVM backend). |
|
||||
| `reference/dotnet-runtime/` | `~/projects/dotnet-runtime` (shallow + **sparse**: only `src/libraries/System.Linq`, 15 MB instead of multi-GB) | Query-surface study for story iteration 9b (`@table` relations + language-integrated query). Read `src/libraries/System.Linq/src/System/Linq/` for the operator set and how each is specified (`Where.cs`, `Select.cs`, `Join.cs`, `GroupBy.cs`, `OrderBy.cs`), and the `*.SpeedOpt.cs` files for how LINQ specializes when the source's shape is known. Study-only, and note the deliberate divergence: LINQ-to-Objects is *runtime* iterator composition over `IEnumerable`, while writeonce has no function values and forbids reflection — so writeonce takes the operator vocabulary and semantics, not the delegate/expression-tree machinery. |
|
||||
|
||||
(The former separate `references/` directory was merged into `reference/`
|
||||
on 2026-08-08 — one home for all study trees.)
|
||||
|
|
|
|||
18
.gitignore
vendored
18
.gitignore
vendored
|
|
@ -45,6 +45,12 @@
|
|||
# ln -s <path-to-colibri> .dev/reference/colibri
|
||||
# ln -s <path-to-llama.cpp> .dev/reference/llama-cpp
|
||||
# ln -s <path-to-llvm-project> .dev/reference/llvm-project
|
||||
# ln -s <path-to-dotnet-runtime> .dev/reference/dotnet-runtime
|
||||
# (sparse clone -- only src/libraries/System.Linq:
|
||||
# git clone --filter=blob:none --no-checkout --depth 1 \
|
||||
# https://github.com/dotnet/runtime.git ~/projects/dotnet-runtime
|
||||
# cd ~/projects/dotnet-runtime && git sparse-checkout init --cone \
|
||||
# && git sparse-checkout set src/libraries/System.Linq && git checkout)
|
||||
|
||||
# Agent-orchestration scratch (SDD ledgers, briefs, review packages)
|
||||
/.superpowers/
|
||||
|
|
@ -65,12 +71,12 @@
|
|||
prototypes/llama-moe-stream/
|
||||
prototypes/wo-db/
|
||||
|
||||
# NOTE (2026-08-10): `tests/` stays ignored, but story iteration 4 (plan 3)
|
||||
# lands the conformance corpus under `tests/corpus/` — un-ignore it in that
|
||||
# change, or the corpus will be invisible to git exactly as the docs below
|
||||
# were. See docs/plan/learnings.md, "check that a new document is actually
|
||||
# tracked".
|
||||
tests/
|
||||
# `tests/` un-ignored 2026-08-11 (plan 3 Task 2): the conformance corpus
|
||||
# lands under `tests/corpus/` and must be tracked, not invisible to git
|
||||
# the way the docs below already were once. See docs/plan/learnings.md,
|
||||
# "check that a new document is actually tracked". No build artifacts
|
||||
# land under `tests/` — the harness's own scratch files use mktemp
|
||||
# outside the repo — so nothing needs re-ignoring beneath it.
|
||||
|
||||
# Documentation is version-controlled — repo doctrine puts docs under `docs/`,
|
||||
# and ignoring them there defeats the point. These directories were ignored
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# compiler/ — the OCaml `woc` compiler
|
||||
|
||||
Lexer → parser → typechecker → ownership pass, for `.wo`. OCaml stdlib only (no Menhir, no ppx); dune is the build runner. Sibling of the C `wovm` bytecode VM (`runtime/`) — the two halves of the OOP track's spec (`docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`) meet at plan 3 (`.wob` emission; not built yet).
|
||||
Lexer → parser → typechecker → ownership pass → bytecode emitter, for `.wo`. OCaml stdlib only (no Menhir, no ppx); dune is the build runner. Sibling of the C `wovm` bytecode VM ([`runtime/`](../runtime/README.md)) — the two halves of the OOP track's spec (`docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`) meet at plan 3, where `woc`'s emitted `.wob` runs on `wovm`.
|
||||
|
||||
**Stage: plan 2 (`docs/plan/compiler/2026-08-01-woc-compiler-front.md`) complete, Tasks 1–8.** Diagnostics, lexer, parser (declarations + statements/expressions), typechecker (symbols, field kinds, structural interfaces, `?T` nullable), and the MVS ownership pass are all implemented and wired into the `woc` executable. Bytecode emission and `.wob` output are plan 3 — not started.
|
||||
**Stage: plan 3 (`docs/plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md`) complete, Tasks 1–6 + 8** (Task 7, a parity harness against the Rust runtime, was deferred by explicit decision — the two stacks now diverge by design). `.wo` source compiles to `.wob` bytecode (`--emit`) and to a single self-contained executable (`build`) that runs `wovm` with no arguments and no repo-relative dependency. Milestone 1's acceptance gate — compile-time budget, the full conformance corpus under ASan, the single-binary smoke, both unit suites — is `just oop-accept`. Plan 2 (lexer through ownership pass) shipped first and is unchanged.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -23,10 +23,15 @@ just woc-test # same, from the repo root
|
|||
## Running `woc`
|
||||
|
||||
```
|
||||
woc <path> # compile (lex, parse, typecheck, ownership-check); nothing prints on success
|
||||
woc --dump-tokens <path> # stdout: one line per lexed token
|
||||
woc --dump-ast <path> # stdout: the declaration + body AST, indented
|
||||
woc --dump-owner <path> # stdout: the ownership pass's four tables (moves, drops, rc, residual)
|
||||
woc <path> # compile (lex, parse, typecheck, ownership-check); nothing prints on success
|
||||
woc --dump-tokens <path> # stdout: one line per lexed token
|
||||
woc --dump-ast <path> # stdout: the declaration + body AST, indented
|
||||
woc --dump-owner <path> # stdout: the ownership pass's four tables (moves, drops, rc, residual)
|
||||
woc --dump-bc <path> # stdout: disassembled bytecode for every emitted method
|
||||
woc --emit <path> -o <out.wob> # compile through to a .wob bytecode module, runnable by wovm
|
||||
woc build <dir> -o <app> [--runtime <path>]
|
||||
# compile + append the .wob image to a copy of wovm (default
|
||||
# runtime/wovm, or --runtime) into one self-contained <app>
|
||||
```
|
||||
|
||||
`<path>` is a single `.wo` file or a directory. A directory is discovered recursively for every `.wo` file under it — same contract as `wo run` (`crates/rt/src/lib.rs::discover`): dot-prefixed entries and `target`/`data`/`node_modules` are skipped, results are sorted by path. Every discovered file compiles as one program (declarations in one file resolve for bodies in another, regardless of discovery order); diagnostics from every file and every stage print sorted by `(file, line, col)`. For multi-file `--dump-*` output, each file's dump is preceded by a `=== path ===` header line (`compiler/src/dump.ml`'s `file_header`) — a single-file run never prints one.
|
||||
|
|
@ -35,8 +40,8 @@ Diagnostics render as `file:line:col: severity CODE: message` plus a source exce
|
|||
|
||||
## Layout
|
||||
|
||||
- `src/` — one module per stage: `diag` (diagnostics, collector, exit-code decision), `token`/`lexer`, `ast`/`parser`, `types` (typechecker), `owner` (MVS ownership pass), `dump` (stable text dumps for all of the above)
|
||||
- `bin/` — the `woc` executable: CLI parsing, file discovery, the multi-file/cross-file driver
|
||||
- `test/` — `runner.ml` (golden runner + CLI smoke) and `test_diag.ml` (diag.ml unit checks); `test/golden/<stage>/` holds one-file-per-fixture goldens (`tokens`, `ast`, `owner`, `owner-err`); `test/fixtures/driver/` holds the multi-file CLI-smoke fixtures (directory discovery, cross-file symbols, diagnostic ordering) that don't fit the one-`.wo`-file-per-fixture golden shape
|
||||
- `src/` — one module per stage: `diag` (diagnostics, collector, exit-code decision), `token`/`lexer`, `ast`/`parser`, `types` (typechecker), `owner` (MVS ownership pass), `emit` (bytecode emitter, consumes `owner`'s four tables), `disasm` (bytecode disassembler, backs `--dump-bc`), `dump` (stable text dumps for all of the above)
|
||||
- `bin/` — the `woc` executable: CLI parsing, file discovery, the multi-file/cross-file driver, `--emit`/`build` output
|
||||
- `test/` — `runner.ml` (golden runner + CLI smoke) and `test_diag.ml` (diag.ml unit checks); `test/golden/<stage>/` holds one-file-per-fixture goldens (`tokens`, `ast`, `owner`, `owner-err`, `bc`); `test/fixtures/driver/` holds the multi-file CLI-smoke fixtures (directory discovery, cross-file symbols, diagnostic ordering) that don't fit the one-`.wo`-file-per-fixture golden shape
|
||||
|
||||
Governing docs (all under `docs/`, not here — this file stays an orientation README): spec `docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`; plan `docs/plan/compiler/2026-08-01-woc-compiler-front.md` (+ `architecture.md`, `nullable-types-implementation.md`, `2026-08-01-haxe-parity-language.md`, `2026-08-01-wob-emit-e2e-single-binary.md` in the same directory). Format contract: `docs/plan/oop-vm/00-wob-format.md`. Error catalog: `docs/plan/oop-vm/01-error-catalog.md`.
|
||||
Governing docs (all under `docs/`, not here — this file stays an orientation README): spec `docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`; plans `docs/plan/compiler/2026-08-01-woc-compiler-front.md` and `2026-08-01-wob-emit-e2e-single-binary.md` (+ `architecture.md`, `nullable-types-implementation.md`, `2026-08-01-haxe-parity-language.md` in the same directory). Format contract: `docs/plan/oop-vm/00-wob-format.md`. Error catalog: `docs/plan/oop-vm/01-error-catalog.md`. Conformance corpus contract (fixture layout `woc`'s golden output feeds into): `docs/plan/oop-vm/02-corpus.md`; source-language builtin surface `woc` accepts: `docs/plan/oop-vm/08-builtin-surface.md`. Runtime sibling: [`runtime/README.md`](../runtime/README.md).
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@
|
|||
|
||||
let usage_msg =
|
||||
"usage: woc <path>\n\
|
||||
usage: woc --emit <path> -o <out.wob>\n\
|
||||
usage: woc build <dir> -o <app> [--runtime <path>]\n\
|
||||
usage: woc --dump-tokens <path>\n\
|
||||
usage: woc --dump-ast <path>\n\
|
||||
usage: woc --dump-owner <path>\n\
|
||||
usage: woc --dump-bc <path>\n\
|
||||
\n\
|
||||
Compiles writeonce (.wo) source. <path> is a single .wo file or a\n\
|
||||
directory: a directory is discovered recursively for every .wo file\n\
|
||||
|
|
@ -70,6 +73,28 @@ let usage_msg =
|
|||
sites — see compiler/src/dump.ml for the format); lexing, parsing,\n\
|
||||
type and ownership (WO-E3xx) diagnostics print to stderr.\n\
|
||||
\n\
|
||||
--emit runs the whole pipeline and writes the `.wob` v1 image named\n\
|
||||
by -o (docs/plan/oop-vm/00-wob-format.md). Every discovered file\n\
|
||||
contributes to one image; the entry point is the zero-argument free\n\
|
||||
fn `main`, if the program declares one. Nothing is written when any\n\
|
||||
diagnostic is an error — bytecode for a program that does not compile\n\
|
||||
is never produced.\n\
|
||||
\n\
|
||||
--dump-bc emits the same image and prints its disassembly to stdout\n\
|
||||
(compiler/src/disasm.ml). Unlike the other dumps it prints nothing\n\
|
||||
when the compile is not clean: a disassembly of a program that failed\n\
|
||||
to compile would be describing bytecode nobody may run.\n\
|
||||
\n\
|
||||
build compiles <dir> like --emit, then produces one self-contained\n\
|
||||
executable at -o: the wovm runtime binary (--runtime <path>, or\n\
|
||||
runtime/wovm relative to the current directory when omitted) with the\n\
|
||||
compiled .wob image and a fixed-size trailer appended, so the result\n\
|
||||
runs standalone with no separate .wob file or argument (wovm finds the\n\
|
||||
embedded image via /proc/self/exe -- see docs/plan/oop-vm/00-wob-format.md's\n\
|
||||
\"single-binary trailer\" section). Nothing is written when the compile\n\
|
||||
has diagnostics, when the program declares no zero-argument free fn\n\
|
||||
named `main`, or when the runtime binary cannot be found.\n\
|
||||
\n\
|
||||
For a directory (or otherwise multi-file) path, every --dump-* flag\n\
|
||||
prints each file's own dump in turn, separated by a header line — see\n\
|
||||
compiler/src/dump.ml's file_header doc comment.\n\
|
||||
|
|
@ -311,11 +336,161 @@ let check_only path =
|
|||
List.iter (fun (f, prog) -> ignore (Woc_lib.Owner.analyze ~file:f prog syms collector)) parsed;
|
||||
finish collector (build_lookup sources)
|
||||
|
||||
(* ---- emit mode (plan 3, Task 1) --------------------------------------
|
||||
|
||||
The whole pipeline plus the emitter. Every discovered file feeds one
|
||||
`.wob` image: class ids, interface slot ids and method indexes are
|
||||
assigned in discovery-then-declaration order, and each file keeps its
|
||||
own owner tables because node ids are minted per parse (unique within
|
||||
a file, not across files). *)
|
||||
|
||||
let compile_image path =
|
||||
let sources = discover_and_read path in
|
||||
let collector = Woc_lib.Diag.Collector.create () in
|
||||
let parsed = parse_all collector sources in
|
||||
let syms = typecheck_all collector parsed in
|
||||
let units =
|
||||
List.map
|
||||
(fun (f, prog) ->
|
||||
{ Woc_lib.Emit.file = f; prog; tables = Woc_lib.Owner.analyze ~file:f prog syms collector })
|
||||
parsed
|
||||
in
|
||||
let image = Woc_lib.Emit.emit ~syms collector units in
|
||||
(collector, build_lookup sources, image)
|
||||
|
||||
let write_file path contents =
|
||||
try
|
||||
let oc = open_out_bin path in
|
||||
output_string oc contents;
|
||||
close_out oc
|
||||
with Sys_error msg ->
|
||||
Printf.eprintf "woc: %s\n" msg;
|
||||
exit 2
|
||||
|
||||
let emit_mode path out =
|
||||
let collector, lookup, image = compile_image path in
|
||||
if Woc_lib.Diag.Collector.has_error collector then finish collector lookup
|
||||
else begin
|
||||
write_file out image;
|
||||
finish collector lookup
|
||||
end
|
||||
|
||||
let dump_bc path =
|
||||
let collector, lookup, image = compile_image path in
|
||||
if not (Woc_lib.Diag.Collector.has_error collector) then
|
||||
print_string (Woc_lib.Disasm.dump image);
|
||||
finish collector lookup
|
||||
|
||||
(* ---- build mode (plan 3, Task 6): the single self-contained binary ---
|
||||
|
||||
`woc build <dir> -o app` compiles like --emit, then glues together a
|
||||
runnable executable: the wovm runtime binary, the freshly compiled
|
||||
.wob image, and a fixed-size trailer so wovm's own startup
|
||||
(runtime/src/main.c) can find the embedded image via /proc/self/exe
|
||||
and ignore argv. Trailer layout is docs/plan/oop-vm/00-wob-format.md's
|
||||
"single-binary trailer" section -- this writer and main.c's reader
|
||||
must never disagree about it.
|
||||
|
||||
Edge cases, decided and documented alongside the trailer format:
|
||||
- output path already exists: overwritten, but atomically (build to a
|
||||
temp file next to -o, then rename over it) so a failed build never
|
||||
clobbers a working binary with a partial one.
|
||||
- a directory with no `main`: unlike --emit (where a .wob with no
|
||||
entry is a legitimate artifact), `build`'s whole point is something
|
||||
you can run, so this is a build-time error, not deferred to wovm's
|
||||
own "module has no entry method" at run time.
|
||||
- the --runtime binary is itself already a built single binary (has
|
||||
its own trailer): its embedded payload is stripped before copying,
|
||||
so rebuilding from a built binary doesn't chain payloads/trailers. *)
|
||||
|
||||
let trailer_magic = 0x31544257l (* "WBT1" read as LE u32 (mirrors WOB_MAGIC's "WOB1") *)
|
||||
let trailer_size = 20 (* payload_off u64, payload_len u64, magic u32 *)
|
||||
let wob_off_entry = 40 (* WOB_OFF_ENTRY, runtime/src/wob.h *)
|
||||
|
||||
let trailer_bytes ~(payload_off : int) ~(payload_len : int) : bytes =
|
||||
let t = Bytes.create trailer_size in
|
||||
Bytes.set_int64_le t 0 (Int64.of_int payload_off);
|
||||
Bytes.set_int64_le t 8 (Int64.of_int payload_len);
|
||||
Bytes.set_int32_le t 16 trailer_magic;
|
||||
t
|
||||
|
||||
(* If `rt` already carries a valid trailer of our own (i.e. it's itself
|
||||
the output of a previous `woc build`), its embedded payload is dead
|
||||
weight for a fresh build: return just the pristine runtime prefix.
|
||||
Anything that doesn't look unambiguously like our own trailer (wrong
|
||||
magic, or offsets that don't exactly account for every trailing byte)
|
||||
is returned untouched -- the safe default when it's not certain. *)
|
||||
let strip_existing_trailer (rt : string) : string =
|
||||
let n = String.length rt in
|
||||
if n < trailer_size then rt
|
||||
else if String.get_int32_le rt (n - 4) <> trailer_magic then rt
|
||||
else
|
||||
let payload_off = Int64.to_int (String.get_int64_le rt (n - trailer_size)) in
|
||||
let payload_len = Int64.to_int (String.get_int64_le rt (n - trailer_size + 8)) in
|
||||
if payload_off >= 0 && payload_off <= n - trailer_size
|
||||
&& payload_len = n - trailer_size - payload_off
|
||||
then String.sub rt 0 payload_off
|
||||
else rt
|
||||
|
||||
let default_runtime_path = "runtime/wovm"
|
||||
|
||||
let build_mode ~(runtime : string option) (path : string) (out : string) : unit =
|
||||
let collector, lookup, image = compile_image 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
|
||||
|
||||
let () =
|
||||
match Sys.argv with
|
||||
| [| _; "--dump-tokens"; path |] -> dump_tokens path
|
||||
| [| _; "--dump-ast"; path |] -> dump_ast path
|
||||
| [| _; "--dump-owner"; path |] -> dump_owner path
|
||||
| [| _; "--dump-bc"; path |] -> dump_bc path
|
||||
| [| _; "--emit"; path; "-o"; out |] -> emit_mode path out
|
||||
| [| _; "build"; path; "-o"; out |] -> build_mode ~runtime:None path out
|
||||
| [| _; "build"; path; "-o"; out; "--runtime"; rt |] -> build_mode ~runtime:(Some rt) path out
|
||||
| [| _; "build"; path; "--runtime"; rt; "-o"; out |] -> build_mode ~runtime:(Some rt) path out
|
||||
| [| _; path |] -> check_only path
|
||||
| _ ->
|
||||
prerr_string usage_msg;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@
|
|||
WO-E1xx parsing (Task 4, 5)
|
||||
WO-E2xx types (Task 6)
|
||||
WO-E3xx ownership (Task 7)
|
||||
WO-E4xx emitter (plan 3 Task 1: bytecode/format limits)
|
||||
|
||||
No codes are minted in this module — it only reserves the ranges.
|
||||
The prefixes below are the single documented source later stages
|
||||
|
|
@ -58,6 +59,7 @@ let lexing_prefix = "WO-E0"
|
|||
let parsing_prefix = "WO-E1"
|
||||
let types_prefix = "WO-E2"
|
||||
let ownership_prefix = "WO-E3"
|
||||
let emitter_prefix = "WO-E4"
|
||||
let warning_prefix = "WO-W"
|
||||
|
||||
type severity =
|
||||
|
|
|
|||
282
compiler/src/disasm.ml
Normal file
282
compiler/src/disasm.ml
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
(* disasm.ml — renders a `.wob` image back to readable mnemonics.
|
||||
|
||||
This is what `woc --dump-bc` prints and what the golden fixtures
|
||||
under compiler/test/golden/bc/ pin. Two reasons it decodes the
|
||||
*bytes* rather than reading the emitter's in-memory tables:
|
||||
|
||||
- a pinned dump then covers serialization too, so a header offset,
|
||||
a pad byte or a table count that goes wrong shows up as a golden
|
||||
diff instead of surviving to the loader;
|
||||
- the decoder is written against the same normative documents the
|
||||
emitter is (docs/plan/oop-vm/00-wob-format.md and
|
||||
runtime/src/wob.h), so the two halves disagree loudly.
|
||||
|
||||
Format of the dump (a stable test contract, same doctrine as
|
||||
dump.ml's): fixed sections in a fixed order; one line per constant,
|
||||
class, interface, vtable row and instruction; a method's line and
|
||||
drop tables printed as their own lines before its code, because
|
||||
those tables *are* the deliverable for the drop-map and trap-line
|
||||
goldens. Registers print as rN, constants kN, classes cN, methods
|
||||
mN, interface slots sN, field indexes fN; jumps print their absolute
|
||||
target pc, which is what a reader wants and what stays stable when
|
||||
an unrelated instruction is inserted before the jump. *)
|
||||
|
||||
let magic = 0x31424F57
|
||||
let hdr_size = 44
|
||||
let none = 0xFFFFFFFF
|
||||
|
||||
exception Bad of string
|
||||
|
||||
(* ---- little-endian readers (bounds-checked: a dump must never read
|
||||
past a truncated image, however it got truncated) ---- *)
|
||||
|
||||
let u8 (s : string) (o : int) : int =
|
||||
if o + 1 > String.length s then raise (Bad "truncated");
|
||||
String.get_uint8 s o
|
||||
|
||||
let u16 (s : string) (o : int) : int =
|
||||
if o + 2 > String.length s then raise (Bad "truncated");
|
||||
String.get_uint16_le s o
|
||||
|
||||
let u32 (s : string) (o : int) : int =
|
||||
if o + 4 > String.length s then raise (Bad "truncated");
|
||||
Int32.to_int (String.get_int32_le s o) land 0xFFFFFFFF
|
||||
|
||||
let i64 (s : string) (o : int) : int64 =
|
||||
if o + 8 > String.length s then raise (Bad "truncated");
|
||||
String.get_int64_le s o
|
||||
|
||||
let op_of i = i land 0xFF
|
||||
let a_of i = (i lsr 8) land 0xFF
|
||||
let b_of i = (i lsr 16) land 0xFF
|
||||
let c_of i = (i lsr 24) land 0xFF
|
||||
let bx_of i = (i lsr 16) land 0xFFFF
|
||||
let sbx_of i = bx_of i - 32768
|
||||
|
||||
let builtin_name = function
|
||||
| 0 -> "now"
|
||||
| 1 -> "print"
|
||||
| 2 -> "print_int"
|
||||
| 3 -> "words"
|
||||
| 4 -> "multi_new"
|
||||
| 5 -> "multi_push"
|
||||
| 6 -> "multi_get"
|
||||
| 7 -> "count"
|
||||
| 8 -> "latest"
|
||||
| 9 -> "map_new"
|
||||
| 10 -> "map_set"
|
||||
| 11 -> "map_get"
|
||||
| 12 -> "map_has"
|
||||
| n -> Printf.sprintf "builtin%d" n
|
||||
|
||||
let kind_name = function
|
||||
| 0 -> "SCALAR"
|
||||
| 1 -> "OWNED"
|
||||
| 2 -> "GCREF"
|
||||
| 3 -> "TEXT"
|
||||
| 4 -> "MULTI"
|
||||
| 5 -> "MAP"
|
||||
| n -> Printf.sprintf "KIND%d" n
|
||||
|
||||
(* text constants render with the few escapes a one-line dump needs;
|
||||
anything else would let a fixture's newline break the line format *)
|
||||
let quote (s : string) : string =
|
||||
let b = Buffer.create (String.length s + 2) in
|
||||
Buffer.add_char b '"';
|
||||
String.iter
|
||||
(fun ch ->
|
||||
match ch with
|
||||
| '"' -> Buffer.add_string b "\\\""
|
||||
| '\\' -> Buffer.add_string b "\\\\"
|
||||
| '\n' -> Buffer.add_string b "\\n"
|
||||
| '\t' -> Buffer.add_string b "\\t"
|
||||
| c when Char.code c < 32 -> Buffer.add_string b (Printf.sprintf "\\x%02x" (Char.code c))
|
||||
| c -> Buffer.add_char b c)
|
||||
s;
|
||||
Buffer.add_char b '"';
|
||||
Buffer.contents b
|
||||
|
||||
let mask_str (m : int64) : string =
|
||||
if m = 0L then "{}"
|
||||
else begin
|
||||
let regs = ref [] in
|
||||
for r = 63 downto 0 do
|
||||
if Int64.logand m (Int64.shift_left 1L r) <> 0L then regs := Printf.sprintf "r%d" r :: !regs
|
||||
done;
|
||||
"{" ^ String.concat "," !regs ^ "}"
|
||||
end
|
||||
|
||||
let ins_str (i : int) (pc : int) : string =
|
||||
let a = a_of i and b = b_of i and c = c_of i in
|
||||
let bx = bx_of i in
|
||||
let target = pc + 1 + sbx_of i in
|
||||
match op_of i with
|
||||
| 0 -> "NOP"
|
||||
| 1 -> Printf.sprintf "LOADK r%d, k%d" a bx
|
||||
| 2 -> Printf.sprintf "MOVE r%d, r%d" a b
|
||||
| 3 -> Printf.sprintf "ADD r%d, r%d, r%d" a b c
|
||||
| 4 -> Printf.sprintf "SUB r%d, r%d, r%d" a b c
|
||||
| 5 -> Printf.sprintf "MUL r%d, r%d, r%d" a b c
|
||||
| 6 -> Printf.sprintf "DIV r%d, r%d, r%d" a b c
|
||||
| 7 -> Printf.sprintf "NEG r%d, r%d" a b
|
||||
| 8 -> Printf.sprintf "CONCAT r%d, r%d, r%d" a b c
|
||||
| 9 -> Printf.sprintf "EQ r%d, r%d, r%d" a b c
|
||||
| 10 -> Printf.sprintf "LT r%d, r%d, r%d" a b c
|
||||
| 11 -> Printf.sprintf "LE r%d, r%d, r%d" a b c
|
||||
| 12 -> Printf.sprintf "EQS r%d, r%d, r%d" a b c
|
||||
| 13 -> Printf.sprintf "JMP -> %04d" target
|
||||
| 14 -> Printf.sprintf "JZ r%d, -> %04d" a target
|
||||
| 15 -> Printf.sprintf "CALL r%d, m%d" a bx
|
||||
| 16 -> Printf.sprintf "ICALL r%d, s%d" a bx
|
||||
| 17 -> Printf.sprintf "RET r%d" a
|
||||
| 18 -> "RET0"
|
||||
| 19 -> Printf.sprintf "NEW r%d, c%d" a bx
|
||||
| 20 -> Printf.sprintf "GETF r%d, r%d, f%d" a b c
|
||||
| 21 -> Printf.sprintf "SETF r%d, f%d, r%d" a b c
|
||||
| 22 -> Printf.sprintf "DROP r%d" a
|
||||
| 23 -> Printf.sprintf "BORROW_S r%d" a
|
||||
| 24 -> Printf.sprintf "BORROW_X r%d" a
|
||||
| 25 -> Printf.sprintf "RELEASE_S r%d" a
|
||||
| 26 -> Printf.sprintf "RELEASE_X r%d" a
|
||||
| 27 -> Printf.sprintf "RC_INC r%d" a
|
||||
| 28 -> Printf.sprintf "RC_DEC r%d" a
|
||||
| 29 ->
|
||||
if c = 4 || c = 9 then Printf.sprintf "BUILTIN r%d, kinds=0x%02x, %s" a b (builtin_name c)
|
||||
else Printf.sprintf "BUILTIN r%d, r%d, %s" a b (builtin_name c)
|
||||
| 30 -> "DB_STUB"
|
||||
| 31 -> Printf.sprintf "TRAP %d" bx
|
||||
| op -> Printf.sprintf "?OP%d" op
|
||||
|
||||
(* ---- the dump ---- *)
|
||||
|
||||
type kconst =
|
||||
| KInt of int64
|
||||
| KText of string
|
||||
|
||||
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 <> 1 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
|
||||
let moff = u32 img 32 and mcnt = u32 img 36 in
|
||||
let entry = u32 img 40 in
|
||||
ignore hdr_size;
|
||||
(* constants *)
|
||||
let consts = Array.make (max ccnt 1) (KInt 0L) in
|
||||
let o = ref coff in
|
||||
for i = 0 to ccnt - 1 do
|
||||
let tag = u8 img !o in
|
||||
incr o;
|
||||
if tag = 0 then begin
|
||||
consts.(i) <- KInt (i64 img !o);
|
||||
o := !o + 8
|
||||
end
|
||||
else if tag = 1 then begin
|
||||
let n = u32 img !o in
|
||||
o := !o + 4;
|
||||
if !o + n > String.length img then raise (Bad "text constant overruns image");
|
||||
consts.(i) <- KText (String.sub img !o n);
|
||||
o := !o + n
|
||||
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
|
||||
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))
|
||||
done;
|
||||
(* classes *)
|
||||
line "== CLASSES ==";
|
||||
let o = ref koff in
|
||||
for i = 0 to kcnt - 1 do
|
||||
let nm = u32 img !o and flags = u32 img (!o + 4) and fcnt = u32 img (!o + 8) in
|
||||
o := !o + 12;
|
||||
let kinds = List.init fcnt (fun j -> kind_name (u8 img (!o + j))) in
|
||||
o := !o + fcnt + ((4 - (fcnt mod 4)) mod 4);
|
||||
line
|
||||
(Printf.sprintf "c%-3d %s flags=%s fields=[%s]" i (kname nm)
|
||||
(if flags land 1 <> 0 then "gc" else "-")
|
||||
(String.concat ", " kinds))
|
||||
done;
|
||||
(* interfaces + vtable rows *)
|
||||
line "== INTERFACES ==";
|
||||
let o = ref ioff in
|
||||
let slot_base = Array.make (max icnt 1) 0 in
|
||||
let imcnt = Array.make (max icnt 1) 0 in
|
||||
let slots = ref 0 in
|
||||
for i = 0 to icnt - 1 do
|
||||
let nm = u32 img !o and mc = u32 img (!o + 4) in
|
||||
o := !o + 8;
|
||||
slot_base.(i) <- !slots;
|
||||
imcnt.(i) <- mc;
|
||||
line (Printf.sprintf "i%-3d %s methods=%d slots=s%d..s%d" i (kname nm) mc !slots (!slots + mc - 1));
|
||||
slots := !slots + mc
|
||||
done;
|
||||
let vrows = u32 img !o in
|
||||
o := !o + 4;
|
||||
line "== VTABLES ==";
|
||||
for _ = 1 to vrows do
|
||||
let cid = u32 img !o and iid = u32 img (!o + 4) in
|
||||
o := !o + 8;
|
||||
let mc = if iid < icnt then imcnt.(iid) else 0 in
|
||||
let ms = List.init mc (fun j -> Printf.sprintf "m%d" (u32 img (!o + (4 * j)))) in
|
||||
o := !o + (4 * mc);
|
||||
line
|
||||
(Printf.sprintf "c%d i%d slots s%d.. -> [%s]" cid iid
|
||||
(if iid < icnt then slot_base.(iid) else 0)
|
||||
(String.concat ", " ms))
|
||||
done;
|
||||
(* methods *)
|
||||
line "== METHODS ==";
|
||||
let o = ref moff in
|
||||
for i = 0 to mcnt - 1 do
|
||||
let nm = u32 img !o and cid = u32 img (!o + 4) in
|
||||
let argc = u8 img (!o + 8) and regc = u8 img (!o + 9) in
|
||||
let reserved = u16 img (!o + 10) in
|
||||
if reserved <> 0 then raise (Bad "reserved method field is not zero");
|
||||
let clen = u32 img (!o + 12) in
|
||||
o := !o + 16;
|
||||
if clen mod 4 <> 0 then raise (Bad "code length is not a multiple of 4");
|
||||
let ninstr = clen / 4 in
|
||||
let code = Array.init ninstr (fun j -> u32 img (!o + (4 * j))) in
|
||||
o := !o + clen;
|
||||
let lcnt = u32 img !o in
|
||||
o := !o + 4;
|
||||
let lines = List.init lcnt (fun j -> (u32 img (!o + (8 * j)), u32 img (!o + (8 * j) + 4))) in
|
||||
o := !o + (8 * lcnt);
|
||||
let dcnt = u32 img !o in
|
||||
o := !o + 4;
|
||||
let drops =
|
||||
List.init dcnt (fun j ->
|
||||
let base = !o + (20 * j) in
|
||||
(u32 img base, i64 img (base + 4), i64 img (base + 12)))
|
||||
in
|
||||
o := !o + (20 * dcnt);
|
||||
line
|
||||
(Printf.sprintf "m%-3d %s args=%d regs=%d %s%s" i (kname nm) argc regc
|
||||
(if cid = none then "[free fn]" else Printf.sprintf "[class c%d]" cid)
|
||||
(if entry = i then " [ENTRY]" else ""));
|
||||
line
|
||||
(Printf.sprintf " lines: %s"
|
||||
(if lines = [] then "(none)"
|
||||
else String.concat " " (List.map (fun (pc, l) -> Printf.sprintf "%d->%d" pc l) lines)));
|
||||
if drops = [] then line " drops: (none)"
|
||||
else
|
||||
List.iter
|
||||
(fun (pc, ow, gc) ->
|
||||
line (Printf.sprintf " drops: pc %d owned=%s gc=%s" pc (mask_str ow) (mask_str gc)))
|
||||
drops;
|
||||
Array.iteri (fun pc ins -> line (Printf.sprintf " %04d %s" pc (ins_str ins pc))) code
|
||||
done;
|
||||
line "== ENTRY ==";
|
||||
line (if entry = none then "(none)" else Printf.sprintf "m%d" entry);
|
||||
Buffer.contents out
|
||||
|
|
@ -2,4 +2,4 @@
|
|||
; OCaml stdlib only: no Menhir, no ppx, no opam libraries.
|
||||
(library
|
||||
(name woc_lib)
|
||||
(modules diag token ast lexer parser types owner dump))
|
||||
(modules diag token ast lexer parser types owner emit disasm dump))
|
||||
|
|
|
|||
2022
compiler/src/emit.ml
Normal file
2022
compiler/src/emit.ml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1021,6 +1021,19 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
|||
a.ac_place AExcl)
|
||||
accesses;
|
||||
(* transfers last *)
|
||||
(* `push`'s value argument (builtin `multi_push`) stores a @gc reference
|
||||
inside the container permanently — an escape exactly like a ctor
|
||||
field or a `take` argument. `push` is never a resolved callee (it has
|
||||
no declared params), so `conv_of` defaults it to Borrow and the
|
||||
ordinary Take-gated transfer above never fires for it; without this
|
||||
the container holds the reference with no matching RC_INC, and the
|
||||
collector frees the value out from under the container it still sits
|
||||
in. Narrow to `push`'s own value slot (index 1) and to Gc places only
|
||||
— an Owned element's move-on-push is a separate, pre-existing gap
|
||||
this task does not touch. *)
|
||||
let is_push_gc_value i =
|
||||
resolved = None && i = 1 && match callee.kind with Ident "push" -> true | _ -> false
|
||||
in
|
||||
List.iteri
|
||||
(fun i a ->
|
||||
match place_of a with
|
||||
|
|
@ -1028,8 +1041,9 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
|||
| Some p ->
|
||||
let pname, conv = conv_of i in
|
||||
if conv = Take then
|
||||
if transfer ctx p ~what:(Printf.sprintf "cannot be passed to `take %s`" pname) then
|
||||
record_move ctx p (MvArg pname))
|
||||
(if transfer ctx p ~what:(Printf.sprintf "cannot be passed to `take %s`" pname) then
|
||||
record_move ctx p (MvArg pname))
|
||||
else if is_push_gc_value i && place_class ctx p = Gc then gc_escape ctx p)
|
||||
args;
|
||||
record_drop ctx ~node:call_e.id ~pos:call_e.pos ~kind:DLiveMask
|
||||
~items:(mask_items (live_holders ctx))
|
||||
|
|
|
|||
|
|
@ -142,11 +142,30 @@ let suggest_gc_annotation ~file (cls : class_info) (collector : Diag.Collector.t
|
|||
(Diag.warning ~code:gc_suggestion_code ~file ~line:cls.pos.line ~col:cls.pos.col
|
||||
~message:(Printf.sprintf "%s has recursive/shared structure that borrow checker cannot prove. Consider adding @gc if this is an ephemeral in-memory cache. If this maps to a database table, keep owned (default)." cls.name) ())
|
||||
|
||||
(* Ast.field_ty -> the internal resolved typ. Hoisted out of
|
||||
typecheck_program (where it was a local closure) so the .wob emitter
|
||||
can reach the same mapping instead of keeping a second copy of it;
|
||||
the check pass still calls it under its old local name. *)
|
||||
let rec typ_of_field_ty (ft : field_ty) : typ =
|
||||
match ft with
|
||||
| Scalar name -> TScalar name
|
||||
| Ref name -> TRef name
|
||||
| Multi inner_name -> TMulti (TScalar inner_name)
|
||||
| Map (k_name, v_name) -> TMap (TScalar k_name, TScalar v_name)
|
||||
| Nullable inner -> TNullable (typ_of_field_ty inner)
|
||||
|
||||
(* wob_kind_of_typ: maps internal typ to .wob field kind *)
|
||||
let wob_kind_of_typ (syms : symbols) (t : typ) : wob_kind =
|
||||
let kind_of = function
|
||||
| TScalar name ->
|
||||
if is_builtin_scalar name then WO_K_SCALAR
|
||||
(* Text is NOT a plain scalar slot: a Text field holds a heap
|
||||
string, and the runtime's per-kind drop plan
|
||||
(runtime/src/gc.c wo_drop_kind) only frees it under
|
||||
WO_K_TEXT. Emitting WO_K_SCALAR here leaked every string a
|
||||
class owned. Found by the emitter, this function's first
|
||||
caller. *)
|
||||
if name = "Text" then WO_K_TEXT
|
||||
else if is_builtin_scalar name then WO_K_SCALAR
|
||||
else if is_gc_class syms name then WO_K_GCREF
|
||||
else WO_K_OWNED
|
||||
| TNullable _inner -> WO_K_NULLABLE
|
||||
|
|
@ -177,10 +196,31 @@ let missing_nil_check_code = Diag.types_prefix ^ "13"
|
|||
|
||||
let unknown_type_name_code = Diag.types_prefix ^ "25" (* WO-E225 *)
|
||||
|
||||
(* Same-file counterpart to main.ml's cross-file WO-E214 (Task 8 review):
|
||||
a duplicate class/interface/fn name declared twice *within one file*
|
||||
was silently dropped by collect_declarations's StringMap.add (Task 1
|
||||
review, "Known limitations" #6 -- no diagnostic at all). *)
|
||||
let duplicate_decl_code = Diag.types_prefix ^ "15" (* WO-E215 *)
|
||||
|
||||
(* ============================================================
|
||||
Pass 1: Declaration Collection
|
||||
============================================================ *)
|
||||
|
||||
(* Reported at the *later* declaration, with the first as the related
|
||||
site -- exactly WO-E214's shape. The map keeps the first declaration
|
||||
(a duplicate is never added), matching WO-E214's own first-wins rule
|
||||
for the merged cross-file table. *)
|
||||
let report_duplicate_decl (collector : Diag.Collector.t) ~file ~(kind : string) ~(name : string)
|
||||
~(pos : pos) ~(first_pos : pos) : unit =
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:duplicate_decl_code ~file ~line:pos.line ~col:pos.col
|
||||
~message:(Printf.sprintf "%s `%s` already declared" kind name)
|
||||
~related:
|
||||
[ Diag.related_site ~file ~line:first_pos.line ~col:first_pos.col
|
||||
~label:(Printf.sprintf "`%s` first declared here" name)
|
||||
]
|
||||
())
|
||||
|
||||
let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) : symbols =
|
||||
let classes = ref StringMap.empty in
|
||||
let interfaces = ref StringMap.empty in
|
||||
|
|
@ -213,8 +253,13 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) :
|
|||
id = c.id;
|
||||
pos = c.pos;
|
||||
} in
|
||||
classes := StringMap.add c.name info !classes;
|
||||
suggest_gc_annotation ~file info collector
|
||||
(match StringMap.find_opt c.name !classes with
|
||||
| Some (existing : class_info) ->
|
||||
report_duplicate_decl collector ~file ~kind:"class" ~name:c.name ~pos:c.pos
|
||||
~first_pos:existing.pos
|
||||
| None ->
|
||||
classes := StringMap.add c.name info !classes;
|
||||
suggest_gc_annotation ~file info collector)
|
||||
| Ast.Interface i ->
|
||||
let methods = List.map (fun (m : Ast.method_sig) ->
|
||||
{ name = m.name;
|
||||
|
|
@ -229,7 +274,11 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) :
|
|||
id = i.id;
|
||||
pos = i.pos;
|
||||
} in
|
||||
interfaces := StringMap.add i.name info !interfaces
|
||||
(match StringMap.find_opt i.name !interfaces with
|
||||
| Some (existing : interface_info) ->
|
||||
report_duplicate_decl collector ~file ~kind:"interface" ~name:i.name ~pos:i.pos
|
||||
~first_pos:existing.pos
|
||||
| None -> interfaces := StringMap.add i.name info !interfaces)
|
||||
| Ast.Fn f ->
|
||||
let info = {
|
||||
name = f.name;
|
||||
|
|
@ -240,7 +289,11 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) :
|
|||
id = f.id;
|
||||
pos = f.pos;
|
||||
} in
|
||||
free_fns := StringMap.add f.name info !free_fns
|
||||
(match StringMap.find_opt f.name !free_fns with
|
||||
| Some (existing : free_fn_info) ->
|
||||
report_duplicate_decl collector ~file ~kind:"fn" ~name:f.name ~pos:f.pos
|
||||
~first_pos:existing.pos
|
||||
| None -> free_fns := StringMap.add f.name info !free_fns)
|
||||
) prog.decls;
|
||||
|
||||
{ classes = !classes; interfaces = !interfaces; free_fns = !free_fns;
|
||||
|
|
@ -293,14 +346,7 @@ let check_field_types ~file (syms : symbols) (collector : Diag.Collector.t)
|
|||
|
||||
let typecheck_program ~file (prog : program) (syms : symbols) (collector : Diag.Collector.t) : unit =
|
||||
check_field_types ~file syms collector prog;
|
||||
let rec resolve_field_ty (ft : field_ty) : typ =
|
||||
match ft with
|
||||
| Scalar name -> TScalar name
|
||||
| Ref name -> TRef name
|
||||
| Multi inner_name -> TMulti (TScalar inner_name)
|
||||
| Map (k_name, v_name) -> TMap (TScalar k_name, TScalar v_name)
|
||||
| Nullable inner -> TNullable (resolve_field_ty inner)
|
||||
in
|
||||
let resolve_field_ty = typ_of_field_ty in
|
||||
|
||||
let rec typecheck_expr (env : typ StringMap.t) (e : expr) : expr_type_result =
|
||||
match e.kind with
|
||||
|
|
|
|||
50
compiler/test/golden/bc/arith.expected
Normal file
50
compiler/test/golden/bc/arith.expected
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "compute"
|
||||
k1 TEXT "main"
|
||||
k2 INT 0
|
||||
k3 INT 7
|
||||
k4 INT 3
|
||||
k5 TEXT "done"
|
||||
== CLASSES ==
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
m0 compute args=2 regs=11 [free fn]
|
||||
lines: 0->7 1->8 2->9 3->10 4->11 7->12 8->13 10->14 11->16 14->17 16->18 17->17 18->20 20->21 22->23
|
||||
drops: (none)
|
||||
0000 ADD r2, r0, r1
|
||||
0001 SUB r3, r0, r1
|
||||
0002 MUL r4, r2, r3
|
||||
0003 DIV r5, r4, r1
|
||||
0004 DIV r7, r4, r1
|
||||
0005 MUL r7, r7, r1
|
||||
0006 SUB r6, r4, r7
|
||||
0007 NEG r7, r6
|
||||
0008 EQ r8, r2, r3
|
||||
0009 JZ r8, -> 0011
|
||||
0010 RET r7
|
||||
0011 EQ r9, r2, r3
|
||||
0012 LOADK r10, k2
|
||||
0013 EQ r8, r9, r10
|
||||
0014 MOVE r9, r8
|
||||
0015 JZ r9, -> 0018
|
||||
0016 LOADK r8, k2
|
||||
0017 JMP -> 0014
|
||||
0018 LT r9, r3, r2
|
||||
0019 JZ r9, -> 0022
|
||||
0020 ADD r9, r5, r6
|
||||
0021 RET r9
|
||||
0022 RET r7
|
||||
m1 main args=0 regs=3 [free fn] [ENTRY]
|
||||
lines: 0->27 5->28 7->26
|
||||
drops: (none)
|
||||
0000 LOADK r1, k3
|
||||
0001 LOADK r2, k4
|
||||
0002 CALL r1, m0
|
||||
0003 MOVE r0, r1
|
||||
0004 BUILTIN r0, r0, print_int
|
||||
0005 LOADK r0, k5
|
||||
0006 BUILTIN r0, r0, print
|
||||
0007 RET0
|
||||
== ENTRY ==
|
||||
m1
|
||||
29
compiler/test/golden/bc/arith.wo
Normal file
29
compiler/test/golden/bc/arith.wo
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- Arithmetic, comparison and control-flow lowering.
|
||||
-- Covers the two operators the v1 instruction set has no opcode for and
|
||||
-- that the emitter therefore lowers rather than inventing: `%` becomes
|
||||
-- a - (a / b) * b, and `!=` becomes (a == b) == 0. `>` and `>=` reuse
|
||||
-- LT/LE with the operands swapped.
|
||||
fn compute(a: Int, b: Int) -> Int {
|
||||
let sum = a + b
|
||||
let diff = a - b
|
||||
let prod = sum * diff
|
||||
let quot = prod / b
|
||||
let rem = prod % b
|
||||
let neg = -rem
|
||||
if sum == diff {
|
||||
return neg
|
||||
}
|
||||
let changing = sum != diff
|
||||
while changing {
|
||||
changing = false
|
||||
}
|
||||
if sum > diff {
|
||||
return quot + rem
|
||||
}
|
||||
return neg
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_int(compute(7, 3))
|
||||
print("done")
|
||||
}
|
||||
50
compiler/test/golden/bc/elision.expected
Normal file
50
compiler/test/golden/bc/elision.expected
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Cache"
|
||||
k1 TEXT "Holder"
|
||||
k2 TEXT "read"
|
||||
k3 TEXT "proven"
|
||||
k4 TEXT "main"
|
||||
k5 INT 41
|
||||
k6 INT 1
|
||||
== CLASSES ==
|
||||
c0 Cache flags=gc fields=[SCALAR]
|
||||
c1 Holder flags=- fields=[GCREF]
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
m0 read args=1 regs=2 [free fn]
|
||||
lines: 0->20
|
||||
drops: (none)
|
||||
0000 GETF r1, r0, f0
|
||||
0001 RET r1
|
||||
m1 proven args=1 regs=3 [free fn]
|
||||
lines: 0->24 1->25
|
||||
drops: pc 1 owned={} gc={r1}
|
||||
0000 GETF r1, r0, f0
|
||||
0001 MOVE r2, r1
|
||||
0002 CALL r2, m0
|
||||
0003 RET r2
|
||||
m2 main args=0 regs=6 [free fn] [ENTRY]
|
||||
lines: 0->29 3->30 7->31 13->28
|
||||
drops: pc 3 owned={} gc={r0}
|
||||
drops: pc 7 owned={r1} gc={r0}
|
||||
drops: pc 14 owned={} gc={r0}
|
||||
drops: pc 15 owned={} gc={}
|
||||
0000 NEW r0, c0
|
||||
0001 LOADK r1, k5
|
||||
0002 SETF r0, f0, r1
|
||||
0003 NEW r1, c1
|
||||
0004 MOVE r2, r0
|
||||
0005 RC_INC r2
|
||||
0006 SETF r1, f0, r2
|
||||
0007 MOVE r4, r1
|
||||
0008 CALL r4, m1
|
||||
0009 MOVE r3, r4
|
||||
0010 LOADK r5, k6
|
||||
0011 ADD r2, r3, r5
|
||||
0012 BUILTIN r2, r2, print_int
|
||||
0013 DROP r1
|
||||
0014 RC_DEC r0
|
||||
0015 RET0
|
||||
== ENTRY ==
|
||||
m2
|
||||
32
compiler/test/golden/bc/elision.wo
Normal file
32
compiler/test/golden/bc/elision.wo
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- The zero-cost-when-provable promise, as a pinned dump.
|
||||
--
|
||||
-- `proven` aliases a @gc reference out of a field and hands it to a
|
||||
-- function that only reads it. The owner pass proves the acquire and its
|
||||
-- release balanced inside one scope (compiler/test/golden/owner/rc.wo
|
||||
-- pins that as ELIDED), and nothing about the access is unprovable, so
|
||||
-- the emitted body must contain NO borrow op and NO rc op at all — the
|
||||
-- disassembly below is the evidence. `main` is the contrast: an escape
|
||||
-- into a field is a KEPT acquire, so RC_INC does appear there.
|
||||
@gc
|
||||
class Cache {
|
||||
hits: Int
|
||||
}
|
||||
|
||||
class Holder {
|
||||
cache: Cache
|
||||
}
|
||||
|
||||
fn read(c: Cache) -> Int {
|
||||
return c.hits
|
||||
}
|
||||
|
||||
fn proven(h: Holder) -> Int {
|
||||
let c = h.cache
|
||||
return read(c)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cache = Cache { hits: 41 }
|
||||
let h = Holder { cache: cache }
|
||||
print_int(proven(h) + 1)
|
||||
}
|
||||
69
compiler/test/golden/bc/iface.expected
Normal file
69
compiler/test/golden/bc/iface.expected
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Book"
|
||||
k1 TEXT "Toy"
|
||||
k2 TEXT "Priced"
|
||||
k3 TEXT "current_price"
|
||||
k4 TEXT "quote"
|
||||
k5 TEXT "main"
|
||||
k6 INT 2
|
||||
k7 INT 3
|
||||
k8 INT 10
|
||||
k9 INT 5
|
||||
== CLASSES ==
|
||||
c0 Book flags=- fields=[SCALAR]
|
||||
c1 Toy flags=- fields=[SCALAR]
|
||||
== INTERFACES ==
|
||||
i0 Priced methods=1 slots=s0..s0
|
||||
== VTABLES ==
|
||||
c0 i0 slots s0.. -> [m0]
|
||||
c1 i0 slots s0.. -> [m1]
|
||||
== METHODS ==
|
||||
m0 current_price args=1 regs=3 [class c0]
|
||||
lines: 0->15
|
||||
drops: (none)
|
||||
0000 GETF r1, r0, f0
|
||||
0001 LOADK r2, k6
|
||||
0002 ADD r1, r1, r2
|
||||
0003 RET r1
|
||||
m1 current_price args=1 regs=3 [class c1]
|
||||
lines: 0->23
|
||||
drops: (none)
|
||||
0000 GETF r1, r0, f0
|
||||
0001 LOADK r2, k7
|
||||
0002 MUL r1, r1, r2
|
||||
0003 RET r1
|
||||
m2 quote args=1 regs=2 [free fn]
|
||||
lines: 0->28
|
||||
drops: (none)
|
||||
0000 MOVE r1, r0
|
||||
0001 ICALL r1, s0
|
||||
0002 RET r1
|
||||
m3 main args=0 regs=4 [free fn] [ENTRY]
|
||||
lines: 0->32 3->33 6->34 10->35 14->36 18->31
|
||||
drops: pc 3 owned={r0} gc={}
|
||||
drops: pc 6 owned={r0,r1} gc={}
|
||||
drops: pc 19 owned={r0} gc={}
|
||||
drops: pc 20 owned={} gc={}
|
||||
0000 NEW r0, c0
|
||||
0001 LOADK r1, k8
|
||||
0002 SETF r0, f0, r1
|
||||
0003 NEW r1, c1
|
||||
0004 LOADK r2, k9
|
||||
0005 SETF r1, f0, r2
|
||||
0006 MOVE r3, r0
|
||||
0007 CALL r3, m2
|
||||
0008 MOVE r2, r3
|
||||
0009 BUILTIN r2, r2, print_int
|
||||
0010 MOVE r3, r1
|
||||
0011 CALL r3, m2
|
||||
0012 MOVE r2, r3
|
||||
0013 BUILTIN r2, r2, print_int
|
||||
0014 MOVE r3, r0
|
||||
0015 CALL r3, m0
|
||||
0016 MOVE r2, r3
|
||||
0017 BUILTIN r2, r2, print_int
|
||||
0018 DROP r1
|
||||
0019 DROP r0
|
||||
0020 RET0
|
||||
== ENTRY ==
|
||||
m3
|
||||
37
compiler/test/golden/bc/iface.wo
Normal file
37
compiler/test/golden/bc/iface.wo
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- Structural interface dispatch: the interface section, the global slot
|
||||
-- numbering, and one vtable row per satisfying (class, interface) pair.
|
||||
-- Satisfaction is structural and Go-style (no `implements` keyword by
|
||||
-- doctrine), so Book and Toy each get a row purely by having the
|
||||
-- method. A call through an interface-typed parameter is ICALL by global
|
||||
-- slot id; a call on a known class is a direct CALL by method index.
|
||||
interface Priced {
|
||||
fn current_price() -> Int
|
||||
}
|
||||
|
||||
class Book {
|
||||
base: Int
|
||||
|
||||
fn current_price() -> Int {
|
||||
return self.base + 2
|
||||
}
|
||||
}
|
||||
|
||||
class Toy {
|
||||
base: Int
|
||||
|
||||
fn current_price() -> Int {
|
||||
return self.base * 3
|
||||
}
|
||||
}
|
||||
|
||||
fn quote(p: Priced) -> Int {
|
||||
return p.current_price()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let b = Book { base: 10 }
|
||||
let t = Toy { base: 5 }
|
||||
print_int(quote(b))
|
||||
print_int(quote(t))
|
||||
print_int(b.current_price())
|
||||
}
|
||||
65
compiler/test/golden/bc/owned.expected
Normal file
65
compiler/test/golden/bc/owned.expected
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Item"
|
||||
k1 TEXT "consume"
|
||||
k2 TEXT "twice"
|
||||
k3 TEXT "main"
|
||||
k4 INT 2
|
||||
k5 INT 3
|
||||
k6 INT 5
|
||||
k7 INT 0
|
||||
== CLASSES ==
|
||||
c0 Item flags=- fields=[SCALAR]
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
m0 consume args=1 regs=2 [free fn]
|
||||
lines: 0->11
|
||||
drops: pc 0 owned={r0} gc={}
|
||||
drops: pc 2 owned={} gc={}
|
||||
0000 GETF r1, r0, f0
|
||||
0001 DROP r0
|
||||
0002 RET r1
|
||||
m1 twice args=2 regs=6 [free fn]
|
||||
lines: 0->15 3->16 5->17 8->18 13->20
|
||||
drops: pc 0 owned={r0} gc={}
|
||||
drops: pc 3 owned={r0,r2} gc={}
|
||||
drops: pc 8 owned={r0,r2,r3} gc={}
|
||||
drops: pc 10 owned={r0,r2} gc={}
|
||||
drops: pc 11 owned={r0} gc={}
|
||||
drops: pc 12 owned={} gc={}
|
||||
drops: pc 13 owned={r0,r2} gc={}
|
||||
drops: pc 14 owned={r2} gc={}
|
||||
drops: pc 19 owned={} gc={}
|
||||
0000 NEW r2, c0
|
||||
0001 LOADK r3, k4
|
||||
0002 SETF r2, f0, r3
|
||||
0003 MOVE r3, r1
|
||||
0004 JZ r3, -> 0013
|
||||
0005 NEW r3, c0
|
||||
0006 LOADK r4, k5
|
||||
0007 SETF r3, f0, r4
|
||||
0008 GETF r4, r3, f0
|
||||
0009 DROP r3
|
||||
0010 DROP r2
|
||||
0011 DROP r0
|
||||
0012 RET r4
|
||||
0013 MOVE r4, r0
|
||||
0014 CALL r4, m0
|
||||
0015 MOVE r3, r4
|
||||
0016 GETF r5, r2, f0
|
||||
0017 ADD r3, r3, r5
|
||||
0018 DROP r2
|
||||
0019 RET r3
|
||||
m2 main args=0 regs=4 [free fn] [ENTRY]
|
||||
lines: 0->24 7->23
|
||||
drops: (none)
|
||||
0000 NEW r1, c0
|
||||
0001 LOADK r3, k6
|
||||
0002 SETF r1, f0, r3
|
||||
0003 LOADK r2, k7
|
||||
0004 CALL r1, m1
|
||||
0005 MOVE r0, r1
|
||||
0006 BUILTIN r0, r0, print_int
|
||||
0007 RET0
|
||||
== ENTRY ==
|
||||
m2
|
||||
25
compiler/test/golden/bc/owned.wo
Normal file
25
compiler/test/golden/bc/owned.wo
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- Owned locals: the DROP placement and the drop-table masks the owner
|
||||
-- pass's DROPS table dictates. `twice` has an early return out of a
|
||||
-- nested scope, so its DROPs appear on both paths, and the drop table
|
||||
-- shows the frame's live owned registers at every call site (which is
|
||||
-- what makes a trap unwind without leaking).
|
||||
class Item {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn consume(take it: Item) -> Int {
|
||||
return it.n
|
||||
}
|
||||
|
||||
fn twice(take a: Item, flag: Bool) -> Int {
|
||||
let extra = Item { n: 2 }
|
||||
if flag {
|
||||
let inner = Item { n: 3 }
|
||||
return inner.n
|
||||
}
|
||||
return consume(a) + extra.n
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_int(twice(Item { n: 5 }, false))
|
||||
}
|
||||
156
compiler/test/golden/bc/residual.expected
Normal file
156
compiler/test/golden/bc/residual.expected
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
== CONSTANTS ==
|
||||
k0 TEXT "Item"
|
||||
k1 TEXT "Bag"
|
||||
k2 TEXT "touch"
|
||||
k3 TEXT "pair"
|
||||
k4 TEXT "fixed"
|
||||
k5 TEXT "touch3"
|
||||
k6 TEXT "triple"
|
||||
k7 TEXT "write_through"
|
||||
k8 TEXT "main"
|
||||
k9 INT 0
|
||||
k10 INT 1
|
||||
k11 INT 5
|
||||
k12 INT 2
|
||||
k13 INT 3
|
||||
== CLASSES ==
|
||||
c0 Item flags=- fields=[SCALAR]
|
||||
c1 Bag flags=- fields=[MULTI]
|
||||
== INTERFACES ==
|
||||
== VTABLES ==
|
||||
== METHODS ==
|
||||
m0 touch args=2 regs=4 [free fn]
|
||||
lines: 0->19
|
||||
drops: (none)
|
||||
0000 GETF r2, r0, f0
|
||||
0001 GETF r3, r1, f0
|
||||
0002 ADD r2, r2, r3
|
||||
0003 RET r2
|
||||
m1 pair args=3 regs=9 [free fn]
|
||||
lines: 0->23
|
||||
drops: (none)
|
||||
0000 GETF r7, r0, f0
|
||||
0001 MOVE r8, r1
|
||||
0002 BUILTIN r5, r7, multi_get
|
||||
0003 GETF r7, r0, f0
|
||||
0004 MOVE r8, r2
|
||||
0005 BUILTIN r6, r7, multi_get
|
||||
0006 MOVE r3, r5
|
||||
0007 MOVE r4, r6
|
||||
0008 BORROW_X r3
|
||||
0009 BORROW_X r4
|
||||
0010 CALL r5, m0
|
||||
0011 RELEASE_X r4
|
||||
0012 RELEASE_X r3
|
||||
0013 MOVE r3, r5
|
||||
0014 RET r3
|
||||
m2 fixed args=1 regs=5 [free fn]
|
||||
lines: 0->27
|
||||
drops: (none)
|
||||
0000 GETF r3, r0, f0
|
||||
0001 LOADK r4, k9
|
||||
0002 BUILTIN r1, r3, multi_get
|
||||
0003 GETF r3, r0, f0
|
||||
0004 LOADK r4, k10
|
||||
0005 BUILTIN r2, r3, multi_get
|
||||
0006 CALL r1, m0
|
||||
0007 RET r1
|
||||
m3 touch3 args=3 regs=7 [free fn]
|
||||
lines: 0->37
|
||||
drops: (none)
|
||||
0000 GETF r4, r0, f0
|
||||
0001 GETF r5, r1, f0
|
||||
0002 ADD r3, r4, r5
|
||||
0003 GETF r6, r2, f0
|
||||
0004 ADD r3, r3, r6
|
||||
0005 RET r3
|
||||
m4 triple args=4 regs=12 [free fn]
|
||||
lines: 0->41
|
||||
drops: (none)
|
||||
0000 GETF r10, r0, f0
|
||||
0001 MOVE r11, r1
|
||||
0002 BUILTIN r7, r10, multi_get
|
||||
0003 GETF r10, r0, f0
|
||||
0004 MOVE r11, r2
|
||||
0005 BUILTIN r8, r10, multi_get
|
||||
0006 GETF r10, r0, f0
|
||||
0007 MOVE r11, r3
|
||||
0008 BUILTIN r9, r10, multi_get
|
||||
0009 MOVE r4, r7
|
||||
0010 MOVE r5, r8
|
||||
0011 MOVE r6, r9
|
||||
0012 BORROW_X r4
|
||||
0013 BORROW_X r5
|
||||
0014 BORROW_X r6
|
||||
0015 CALL r7, m3
|
||||
0016 RELEASE_X r6
|
||||
0017 RELEASE_X r5
|
||||
0018 RELEASE_X r4
|
||||
0019 MOVE r4, r7
|
||||
0020 RET r4
|
||||
m5 write_through args=3 regs=7 [free fn]
|
||||
lines: 0->49 3->50 6->51 12->52
|
||||
drops: (none)
|
||||
0000 GETF r4, r0, f0
|
||||
0001 MOVE r5, r1
|
||||
0002 BUILTIN r3, r4, multi_get
|
||||
0003 GETF r5, r0, f0
|
||||
0004 MOVE r6, r2
|
||||
0005 BUILTIN r4, r5, multi_get
|
||||
0006 LOADK r5, k11
|
||||
0007 BORROW_X r4
|
||||
0008 BORROW_S r3
|
||||
0009 SETF r4, f0, r5
|
||||
0010 RELEASE_S r3
|
||||
0011 RELEASE_X r4
|
||||
0012 GETF r5, r3, f0
|
||||
0013 RET r5
|
||||
m6 main args=0 regs=6 [free fn] [ENTRY]
|
||||
lines: 0->56 3->57 8->58 13->59 18->60 24->61 28->62 35->63 41->55
|
||||
drops: pc 3 owned={r0} gc={}
|
||||
drops: pc 42 owned={} gc={}
|
||||
0000 NEW r0, c1
|
||||
0001 BUILTIN r1, kinds=0x01, multi_new
|
||||
0002 SETF r0, f0, r1
|
||||
0003 GETF r1, r0, f0
|
||||
0004 NEW r2, c0
|
||||
0005 LOADK r3, k10
|
||||
0006 SETF r2, f0, r3
|
||||
0007 BUILTIN r1, r1, multi_push
|
||||
0008 GETF r1, r0, f0
|
||||
0009 NEW r2, c0
|
||||
0010 LOADK r3, k12
|
||||
0011 SETF r2, f0, r3
|
||||
0012 BUILTIN r1, r1, multi_push
|
||||
0013 GETF r1, r0, f0
|
||||
0014 NEW r2, c0
|
||||
0015 LOADK r3, k13
|
||||
0016 SETF r2, f0, r3
|
||||
0017 BUILTIN r1, r1, multi_push
|
||||
0018 MOVE r2, r0
|
||||
0019 LOADK r3, k9
|
||||
0020 LOADK r4, k10
|
||||
0021 CALL r2, m1
|
||||
0022 MOVE r1, r2
|
||||
0023 BUILTIN r1, r1, print_int
|
||||
0024 MOVE r2, r0
|
||||
0025 CALL r2, m2
|
||||
0026 MOVE r1, r2
|
||||
0027 BUILTIN r1, r1, print_int
|
||||
0028 MOVE r2, r0
|
||||
0029 LOADK r3, k9
|
||||
0030 LOADK r4, k10
|
||||
0031 LOADK r5, k12
|
||||
0032 CALL r2, m4
|
||||
0033 MOVE r1, r2
|
||||
0034 BUILTIN r1, r1, print_int
|
||||
0035 MOVE r2, r0
|
||||
0036 LOADK r3, k9
|
||||
0037 LOADK r4, k10
|
||||
0038 CALL r2, m5
|
||||
0039 MOVE r1, r2
|
||||
0040 BUILTIN r1, r1, print_int
|
||||
0041 DROP r0
|
||||
0042 RET0
|
||||
== ENTRY ==
|
||||
m6
|
||||
64
compiler/test/golden/bc/residual.wo
Normal file
64
compiler/test/golden/bc/residual.wo
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
-- The other half of the borrow story: where static proof fails, and only
|
||||
-- there, the emitter wraps the region in runtime borrow ops.
|
||||
--
|
||||
-- `pair` takes two exclusive borrows of elements reached through runtime
|
||||
-- indices, so `i == j` is unprovable (the canonical residual case from
|
||||
-- the spec's section 4). One BORROW_X / RELEASE_X pair per operand —
|
||||
-- coalesced per operand, never one pair per residual-table entry.
|
||||
-- `fixed` is the control: literal indices are provably distinct, so it
|
||||
-- gets no guards at all.
|
||||
class Item {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Bag {
|
||||
items: multi Item
|
||||
}
|
||||
|
||||
fn touch(mut a: Item, mut b: Item) -> Int {
|
||||
return a.n + b.n
|
||||
}
|
||||
|
||||
fn pair(mut bag: Bag, i: Int, j: Int) -> Int {
|
||||
return touch(bag.items[i], bag.items[j])
|
||||
}
|
||||
|
||||
fn fixed(mut bag: Bag) -> Int {
|
||||
return touch(bag.items[0], bag.items[1])
|
||||
}
|
||||
|
||||
-- Three exclusive aliases in one region: the pairwise check produces
|
||||
-- THREE residual entries (a-b, a-c, b-c) over THREE distinct operands.
|
||||
-- Per-operand coalescing must emit 3 guard pairs; a regression to one
|
||||
-- pair per table entry would emit 6 and self-trap by asking for two
|
||||
-- exclusive borrows of the same object. `pair` above cannot tell those
|
||||
-- two apart (one entry, two operands, 2 guards either way) — this can.
|
||||
fn touch3(mut a: Item, mut b: Item, mut c: Item) -> Int {
|
||||
return a.n + b.n + c.n
|
||||
}
|
||||
|
||||
fn triple(mut bag: Bag, i: Int, j: Int, k: Int) -> Int {
|
||||
return touch3(bag.items[i], bag.items[j], bag.items[k])
|
||||
}
|
||||
|
||||
-- An assignment is its own region: owner.ml anchors the residual sites
|
||||
-- it produces on the statement, not on a call. `s.n = 5` writes through
|
||||
-- one alias while another is live over a runtime index, so the SETF
|
||||
-- itself must be guarded.
|
||||
fn write_through(mut bag: Bag, i: Int, j: Int) -> Int {
|
||||
let r = bag.items[i]
|
||||
let s = bag.items[j]
|
||||
s.n = 5
|
||||
return r.n
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let bag = Bag { items: multi_new() }
|
||||
push(bag.items, Item { n: 1 })
|
||||
push(bag.items, Item { n: 2 })
|
||||
push(bag.items, Item { n: 3 })
|
||||
print_int(pair(bag, 0, 1))
|
||||
print_int(fixed(bag))
|
||||
print_int(triple(bag, 0, 1, 2))
|
||||
print_int(write_through(bag, 0, 1))
|
||||
}
|
||||
|
|
@ -39,6 +39,8 @@ module Parser = Woc_lib.Parser
|
|||
module Dump = Woc_lib.Dump
|
||||
module Types = Woc_lib.Types
|
||||
module Owner = Woc_lib.Owner
|
||||
module Emit = Woc_lib.Emit
|
||||
module Disasm = Woc_lib.Disasm
|
||||
|
||||
let read_file path =
|
||||
let ic = open_in_bin path in
|
||||
|
|
@ -1058,6 +1060,59 @@ let () =
|
|||
| [ d ] -> check "unknown type inside ?T: code is WO-E225" (d.Diag.code = "WO-E225")
|
||||
| _ -> check "unknown type inside ?T: exactly one diagnostic" false
|
||||
|
||||
let () =
|
||||
(* Same-file duplicate declarations (Task 1 review -> Task 2 fix,
|
||||
WO-E215): collect_declarations folded one file's decls into a
|
||||
StringMap keyed by name via a bare StringMap.add, so a second
|
||||
`class`/`interface`/`fn` of the same name in the SAME file was
|
||||
silently dropped -- no diagnostic at all (Task 1 report, "Known
|
||||
limitations" #6). This is the front-end's own-file counterpart to
|
||||
the driver's cross-file WO-E214: reported at the *later*
|
||||
declaration, with the first declaration as the related site,
|
||||
same WO-E2xx range, same "later primary / first related" shape. *)
|
||||
let check_duplicate tag ~src ~kind ~name =
|
||||
let _, collector = typecheck_str ~file:(tag ^ ".wo") src in
|
||||
let diags = Diag.Collector.diagnostics collector in
|
||||
check_eq (tag ^ ": exactly one diagnostic (WO-E215)") ~expected:1
|
||||
~actual:(List.length diags) string_of_int;
|
||||
(match diags with
|
||||
| [ d ] ->
|
||||
check (tag ^ ": code is WO-E215") (d.Diag.code = "WO-E215");
|
||||
check (tag ^ ": severity is Error") (d.Diag.severity = Diag.Error);
|
||||
check (tag ^ ": reported at the later declaration (4:1)")
|
||||
(d.Diag.site.Diag.line = 4 && d.Diag.site.Diag.col = 1);
|
||||
check (tag ^ ": message names the kind and the name")
|
||||
(find_substring ~needle:(kind ^ " `" ^ name ^ "` already declared") d.Diag.message
|
||||
<> None);
|
||||
(match d.Diag.related with
|
||||
| [ r ] ->
|
||||
check (tag ^ ": related site points at the first declaration (1:1)")
|
||||
(r.Diag.site.Diag.line = 1 && r.Diag.site.Diag.col = 1);
|
||||
check (tag ^ ": related label names the first declaration")
|
||||
(find_substring ~needle:"first declared here" r.Diag.label <> None)
|
||||
| _ -> check (tag ^ ": exactly one related site") false)
|
||||
| _ -> check (tag ^ ": exactly one diagnostic") false);
|
||||
check_eq (tag ^ ": an error run exits 1") ~expected:1
|
||||
~actual:(Diag.Collector.exit_code collector) string_of_int
|
||||
in
|
||||
check_duplicate "duplicate class" ~kind:"class" ~name:"Dup"
|
||||
~src:"class Dup {\n n: Int\n}\nclass Dup {\n s: Text\n}\n";
|
||||
check_duplicate "duplicate interface" ~kind:"interface" ~name:"Shape"
|
||||
~src:"interface Shape {\n fn area() -> Int\n}\ninterface Shape {\n fn perimeter() -> Int\n}\n";
|
||||
check_duplicate "duplicate fn" ~kind:"fn" ~name:"double"
|
||||
~src:"fn double(x: Int) -> Int {\n return x + x\n}\nfn double(y: Int) -> Int {\n return y * 2\n}\n"
|
||||
|
||||
let () =
|
||||
(* Control: a class and a fn sharing a name are different namespaces
|
||||
(collect_declarations keeps them in separate StringMaps) -- must
|
||||
NOT trip WO-E215. *)
|
||||
let _, collector =
|
||||
typecheck_str ~file:"cross-namespace.wo"
|
||||
"class Widget {\n n: Int\n}\nfn Widget() -> Int {\n return 1\n}\n"
|
||||
in
|
||||
check_eq "class/fn name sharing across namespaces: reports nothing" ~expected:0
|
||||
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
|
||||
|
||||
(* ---- direct ownership-pass assertions (Task 7) ------------------------
|
||||
|
||||
golden/owner-err/ already pins the *rendered* text of every must-fail
|
||||
|
|
@ -1077,6 +1132,19 @@ let owner_str ~file src =
|
|||
let tables = Owner.analyze ~file prog syms collector in
|
||||
(tables, collector)
|
||||
|
||||
(* The whole pipeline through the emitter, single-file (every golden
|
||||
fixture is one file). Returns the serialized image plus the collector,
|
||||
so a test can assert on the bytes, on the disassembly, or on the
|
||||
diagnostics. *)
|
||||
let emit_str ~file src =
|
||||
let collector = Diag.Collector.create () in
|
||||
let toks = Lexer.tokenize collector ~file src in
|
||||
let prog = Parser.parse collector ~file toks in
|
||||
let syms, () = Types.typecheck ~file prog collector in
|
||||
let tables = Owner.analyze ~file prog syms collector in
|
||||
let image = Emit.emit ~syms collector [ { Emit.file; prog; tables } ] in
|
||||
(image, collector)
|
||||
|
||||
let is_ownership_code (code : string) =
|
||||
String.length code >= 5 && String.sub code 0 5 = Diag.ownership_prefix
|
||||
|
||||
|
|
@ -1469,6 +1537,681 @@ let () =
|
|||
check "cli smoke: stdout still carries the tables on exit 1"
|
||||
(Option.is_some (find_substring ~needle:"== RESIDUAL ==" stdout))
|
||||
|
||||
(* ---- the loader's validation battery, re-implemented -------------------
|
||||
|
||||
The plan's round-trip rule: an image `woc` produces that `wovm`'s
|
||||
loader rejects is always an emitter bug. Running the C binary from
|
||||
here would make this suite depend on the runtime being built, so the
|
||||
rule is enforced by a third, independent decoder of the same format
|
||||
(runtime/test/wob_build.c is the second: an independent encoder of the
|
||||
same format).
|
||||
Every check below is the OCaml twin of a BAIL in
|
||||
runtime/src/loader.c's wo_load_buf, in the same order, so a divergence
|
||||
between the emitter and the loader surfaces in `dune runtest` rather
|
||||
than in the corpus. Returns the list of violations; [] means the
|
||||
loader would accept the image. *)
|
||||
|
||||
let validate_image (img : string) : string list =
|
||||
let bad = ref [] in
|
||||
let fail fmt = bad := fmt :: !bad in
|
||||
let len = String.length img in
|
||||
let ok n o = o >= 0 && o + n <= len in
|
||||
let u8 o = if ok 1 o then String.get_uint8 img o else -1 in
|
||||
let u16 o = if ok 2 o then String.get_uint16_le img o else -1 in
|
||||
let u32 o = if ok 4 o then Int32.to_int (String.get_int32_le img o) land 0xFFFFFFFF else -1 in
|
||||
let u64 o = if ok 8 o then String.get_int64_le img o else 0L in
|
||||
let none = 0xFFFFFFFF in
|
||||
if u32 0 <> 0x31424F57 then fail "bad magic";
|
||||
if u32 4 <> 1 then fail "unsupported version";
|
||||
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
|
||||
let moff = u32 32 and mcnt = u32 36 in
|
||||
let entry = u32 40 in
|
||||
List.iter
|
||||
(fun o -> if o > len then fail "section offset out of range")
|
||||
[ coff; koff; ioff; moff ];
|
||||
(* constants *)
|
||||
let ctag = Array.make (max ccnt 1) (-1) in
|
||||
let o = ref coff in
|
||||
for i = 0 to ccnt - 1 do
|
||||
let tag = u8 !o in
|
||||
incr o;
|
||||
ctag.(i) <- tag;
|
||||
if tag = 0 then o := !o + 8
|
||||
else if tag = 1 then begin
|
||||
let n = u32 !o in
|
||||
o := !o + 4;
|
||||
if not (ok n !o) then fail (Printf.sprintf "constant %d: text overruns" i);
|
||||
o := !o + n
|
||||
end
|
||||
else fail (Printf.sprintf "constant %d: unknown tag %d" i tag)
|
||||
done;
|
||||
if !o > len then fail "constant pool overruns image";
|
||||
let text_const i = i >= 0 && i < ccnt && ctag.(i) = 1 in
|
||||
(* classes *)
|
||||
let class_fields = Array.make (max kcnt 1) 0 in
|
||||
let o = ref koff in
|
||||
for i = 0 to kcnt - 1 do
|
||||
let nm = u32 !o and flags = u32 (!o + 4) and fcnt = u32 (!o + 8) in
|
||||
o := !o + 12;
|
||||
if not (text_const nm) then fail (Printf.sprintf "class %d: bad name constant" i);
|
||||
if flags land lnot 0x01 <> 0 then fail (Printf.sprintf "class %d: unknown flags" i);
|
||||
if fcnt > 65535 then fail (Printf.sprintf "class %d: too many fields" i);
|
||||
class_fields.(i) <- fcnt;
|
||||
for j = 0 to fcnt - 1 do
|
||||
if u8 (!o + j) > 5 then fail (Printf.sprintf "class %d field %d: bad kind" i j)
|
||||
done;
|
||||
o := !o + fcnt + ((4 - (fcnt mod 4)) mod 4);
|
||||
if !o > len then fail (Printf.sprintf "class %d: truncated" i)
|
||||
done;
|
||||
(* interfaces + vtable rows *)
|
||||
let slot_base = Array.make (max icnt 1) 0 in
|
||||
let imcnt = Array.make (max icnt 1) 0 in
|
||||
let slots = ref 0 in
|
||||
let o = ref ioff in
|
||||
for i = 0 to icnt - 1 do
|
||||
let nm = u32 !o and mc = u32 (!o + 4) in
|
||||
o := !o + 8;
|
||||
if not (text_const nm) then fail (Printf.sprintf "interface %d: bad name constant" i);
|
||||
if mc = 0 || mc > 1024 then fail (Printf.sprintf "interface %d: bad method count" i);
|
||||
slot_base.(i) <- !slots;
|
||||
imcnt.(i) <- mc;
|
||||
slots := !slots + mc
|
||||
done;
|
||||
let vrows = u32 !o in
|
||||
o := !o + 4;
|
||||
let seen_rows = Hashtbl.create 8 in
|
||||
let vmethods = ref [] in
|
||||
for r = 0 to vrows - 1 do
|
||||
let cid = u32 !o and iid = u32 (!o + 4) in
|
||||
o := !o + 8;
|
||||
if cid >= kcnt then fail (Printf.sprintf "vtable row %d: bad class" r);
|
||||
if iid >= icnt then fail (Printf.sprintf "vtable row %d: bad interface" r)
|
||||
else
|
||||
for j = 0 to imcnt.(iid) - 1 do
|
||||
let m = u32 (!o + (4 * j)) in
|
||||
vmethods := m :: !vmethods;
|
||||
let key = (cid, slot_base.(iid) + j) in
|
||||
if Hashtbl.mem seen_rows key then
|
||||
fail (Printf.sprintf "duplicate vtable entry for class %d" cid);
|
||||
Hashtbl.replace seen_rows key ()
|
||||
done;
|
||||
if iid < icnt then o := !o + (4 * imcnt.(iid))
|
||||
done;
|
||||
(* methods *)
|
||||
let margc = Array.make (max mcnt 1) 0 in
|
||||
let mregc = Array.make (max mcnt 1) 0 in
|
||||
let mclass = Array.make (max mcnt 1) 0 in
|
||||
let mcode = Array.make (max mcnt 1) [||] in
|
||||
let o = ref moff in
|
||||
for i = 0 to mcnt - 1 do
|
||||
let nm = u32 !o and cid = u32 (!o + 4) in
|
||||
let argc = u8 (!o + 8) and regc = u8 (!o + 9) and reserved = u16 (!o + 10) in
|
||||
let clen = u32 (!o + 12) in
|
||||
o := !o + 16;
|
||||
if not (text_const nm) then fail (Printf.sprintf "method %d: bad name constant" i);
|
||||
if cid <> none && cid >= kcnt then fail (Printf.sprintf "method %d: bad class" i);
|
||||
if reserved <> 0 then fail (Printf.sprintf "method %d: reserved field not zero" i);
|
||||
if regc < 1 || regc > 64 then fail (Printf.sprintf "method %d: register count out of range" i);
|
||||
if argc > regc then fail (Printf.sprintf "method %d: more args than registers" i);
|
||||
if clen = 0 || clen mod 4 <> 0 then fail (Printf.sprintf "method %d: bad code length" i);
|
||||
let ninstr = clen / 4 in
|
||||
let code = Array.init (max ninstr 0) (fun j -> u32 (!o + (4 * j))) in
|
||||
o := !o + clen;
|
||||
margc.(i) <- argc;
|
||||
mregc.(i) <- regc;
|
||||
mclass.(i) <- cid;
|
||||
mcode.(i) <- code;
|
||||
let lcnt = u32 !o in
|
||||
o := !o + 4;
|
||||
if lcnt > ninstr then fail (Printf.sprintf "method %d: line table too long" i);
|
||||
let prev = ref (-1) in
|
||||
for j = 0 to lcnt - 1 do
|
||||
let pc = u32 (!o + (8 * j)) in
|
||||
if pc >= ninstr || pc <= !prev then
|
||||
fail (Printf.sprintf "method %d: line table not ascending" i);
|
||||
prev := pc
|
||||
done;
|
||||
o := !o + (8 * lcnt);
|
||||
let dcnt = u32 !o in
|
||||
o := !o + 4;
|
||||
if dcnt > ninstr then fail (Printf.sprintf "method %d: drop table too long" i);
|
||||
let prev = ref (-1) in
|
||||
for j = 0 to dcnt - 1 do
|
||||
let base = !o + (20 * j) in
|
||||
let pc = u32 base in
|
||||
let owned = u64 (base + 4) and gc = u64 (base + 12) in
|
||||
if pc >= ninstr || pc <= !prev then
|
||||
fail (Printf.sprintf "method %d: drop table not ascending" i);
|
||||
prev := pc;
|
||||
if regc < 64 && Int64.shift_right_logical (Int64.logor owned gc) regc <> 0L then
|
||||
fail (Printf.sprintf "method %d: drop mask out of range" i)
|
||||
done;
|
||||
o := !o + (20 * dcnt)
|
||||
done;
|
||||
if !o > len then fail "method table overruns image";
|
||||
(* static instruction validation *)
|
||||
for i = 0 to mcnt - 1 do
|
||||
let regc = mregc.(i) in
|
||||
let code = mcode.(i) in
|
||||
let ninstr = Array.length code in
|
||||
let rchk pc r =
|
||||
if r < 0 || r >= regc then
|
||||
fail (Printf.sprintf "method %d pc %d: register out of range" i pc)
|
||||
in
|
||||
Array.iteri
|
||||
(fun pc ins ->
|
||||
let op = ins land 0xFF in
|
||||
let a = (ins lsr 8) land 0xFF in
|
||||
let b = (ins lsr 16) land 0xFF in
|
||||
let c = (ins lsr 24) land 0xFF in
|
||||
let bx = (ins lsr 16) land 0xFFFF in
|
||||
let sbx = bx - 32768 in
|
||||
match op with
|
||||
| 0 -> ()
|
||||
| 1 ->
|
||||
rchk pc a;
|
||||
if bx >= ccnt then fail (Printf.sprintf "method %d pc %d: constant out of range" i pc)
|
||||
| 2 | 7 ->
|
||||
rchk pc a;
|
||||
rchk pc b
|
||||
| 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 ->
|
||||
rchk pc a;
|
||||
rchk pc b;
|
||||
rchk pc c
|
||||
| 13 | 14 ->
|
||||
if op = 14 then rchk pc a;
|
||||
let tgt = pc + 1 + sbx in
|
||||
if tgt < 0 || tgt >= ninstr then
|
||||
fail (Printf.sprintf "method %d pc %d: jump out of code" i pc)
|
||||
| 15 ->
|
||||
rchk pc a;
|
||||
if bx >= mcnt then fail (Printf.sprintf "method %d pc %d: callee out of range" i pc)
|
||||
else if a + margc.(bx) > regc then
|
||||
fail (Printf.sprintf "method %d pc %d: call window exceeds frame" i pc)
|
||||
| 16 ->
|
||||
rchk pc a;
|
||||
if bx >= !slots then
|
||||
fail (Printf.sprintf "method %d pc %d: interface slot out of range" i pc)
|
||||
| 17 -> rchk pc a
|
||||
| 18 -> ()
|
||||
| 19 ->
|
||||
rchk pc a;
|
||||
if bx >= kcnt then fail (Printf.sprintf "method %d pc %d: class out of range" i pc)
|
||||
| 20 ->
|
||||
rchk pc a;
|
||||
rchk pc b
|
||||
| 21 ->
|
||||
rchk pc a;
|
||||
rchk pc c
|
||||
| 22 | 23 | 24 | 25 | 26 | 27 | 28 -> rchk pc a
|
||||
| 29 ->
|
||||
rchk pc a;
|
||||
if c > 12 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)
|
||||
end
|
||||
else if c = 9 then begin
|
||||
if b land 0x0F > 5 || b lsr 4 > 5 then
|
||||
fail (Printf.sprintf "method %d pc %d: bad key/value kind" i pc)
|
||||
end
|
||||
else begin
|
||||
let arity =
|
||||
match c with
|
||||
| 0 -> 0
|
||||
| 1 | 2 | 3 | 7 | 8 -> 1
|
||||
| 5 | 6 | 11 | 12 -> 2
|
||||
| 10 -> 3
|
||||
| _ -> 0
|
||||
in
|
||||
if arity > 0 then begin
|
||||
rchk pc b;
|
||||
rchk pc (b + arity - 1)
|
||||
end
|
||||
end
|
||||
| 30 | 31 -> ()
|
||||
| _ -> fail (Printf.sprintf "method %d pc %d: unknown opcode %d" i pc op))
|
||||
code;
|
||||
if ninstr > 0 then begin
|
||||
let last = code.(ninstr - 1) land 0xFF in
|
||||
if not (last = 17 || last = 18 || last = 31 || last = 30 || last = 13) then
|
||||
fail (Printf.sprintf "method %d: last instruction is not a terminator" i)
|
||||
end
|
||||
done;
|
||||
List.iter
|
||||
(fun m -> if m >= mcnt then fail "vtable entry: method out of range")
|
||||
!vmethods;
|
||||
if entry <> none then begin
|
||||
if entry >= mcnt then fail "entry method out of range"
|
||||
else if margc.(entry) <> 0 || mclass.(entry) <> none then
|
||||
fail "entry must be a zero-arg free fn"
|
||||
end;
|
||||
List.rev !bad
|
||||
|
||||
(* ---- emitter assertions (Task 1, not golden-diffed) --------------------
|
||||
|
||||
golden/bc/*.wo pin the disassembly; these pin what a dump cannot show:
|
||||
that every emitted image satisfies the loader's contract, that the
|
||||
elision fixture really contains no borrow/rc op at all, that a
|
||||
residual guard never lives in a call window, and that an over-budget
|
||||
method diagnoses instead of truncating. *)
|
||||
|
||||
let bc_fixtures () =
|
||||
let dir = "golden/bc" in
|
||||
Sys.readdir dir |> Array.to_list
|
||||
|> List.filter (fun n -> Filename.check_suffix n ".wo")
|
||||
|> List.sort compare
|
||||
|> List.map (fun n -> (dir ^ "/" ^ n, read_file (Filename.concat dir n)))
|
||||
|
||||
let () =
|
||||
List.iter
|
||||
(fun (path, src) ->
|
||||
let image, collector = emit_str ~file:path src in
|
||||
check_eq (Printf.sprintf "emit %s: compiles clean" path) ~expected:0
|
||||
~actual:(List.length (Diag.Collector.diagnostics collector))
|
||||
string_of_int;
|
||||
let violations = validate_image image in
|
||||
check_eq
|
||||
(Printf.sprintf "round trip %s: the loader's battery accepts the image (%s)" path
|
||||
(String.concat "; " violations))
|
||||
~expected:0 ~actual:(List.length violations) string_of_int)
|
||||
(bc_fixtures ())
|
||||
|
||||
(* Every method's block of a disassembly, keyed by the method name as the
|
||||
dump writes it ("m3 pair args=..."). *)
|
||||
let method_block (dump : string) (name : string) : string =
|
||||
let lines = String.split_on_char '\n' dump in
|
||||
let is_header l =
|
||||
String.length l > 1 && l.[0] = 'm' && l.[1] >= '0' && l.[1] <= '9'
|
||||
in
|
||||
let wanted l = is_header l && find_substring ~needle:(" " ^ name ^ " args=") l <> None in
|
||||
let rec collect acc inside = function
|
||||
| [] -> List.rev acc
|
||||
| l :: tl ->
|
||||
if wanted l then collect (l :: acc) true tl
|
||||
else if inside && (is_header l || (String.length l > 1 && l.[0] = '=')) then List.rev acc
|
||||
else if inside then collect (l :: acc) true tl
|
||||
else collect acc false tl
|
||||
in
|
||||
String.concat "\n" (collect [] false lines)
|
||||
|
||||
let () =
|
||||
(* The spec's zero-cost promise, as an assertion and not only a pinned
|
||||
dump: a method whose ownership is fully proven contains no borrow op
|
||||
and no rc op. golden/bc/elision.wo's `proven` aliases a @gc
|
||||
reference and passes it to a reader; owner.ml marks the pair ELIDED
|
||||
(golden/owner/rc.wo pins that), so nothing may be emitted for it. *)
|
||||
let path = "golden/bc/elision.wo" in
|
||||
let image, _ = emit_str ~file:path (read_file path) in
|
||||
let block = method_block (Disasm.dump image) "proven" in
|
||||
check "elision: `proven` was found in the disassembly" (block <> "");
|
||||
List.iter
|
||||
(fun op ->
|
||||
check
|
||||
(Printf.sprintf "elision: `proven` emits no %s (zero-cost when provable)" op)
|
||||
(find_substring ~needle:op block = None))
|
||||
[ "BORROW_S"; "BORROW_X"; "RELEASE_S"; "RELEASE_X"; "RC_INC"; "RC_DEC" ];
|
||||
(* the contrast, so the fixture cannot pass by emitting nothing anywhere:
|
||||
main stores the @gc value into a field, which is a KEPT acquire *)
|
||||
let main_block = method_block (Disasm.dump image) "main" in
|
||||
check "elision: the escaping acquire in `main` is still emitted (fixture is not vacuous)"
|
||||
(find_substring ~needle:"RC_INC" main_block <> None)
|
||||
|
||||
let () =
|
||||
(* Residual guards: one coalesced pair per operand, and — the
|
||||
regression this pins — never on a register inside the call window.
|
||||
The callee's frame overlaps that window (it may assign to its own
|
||||
parameters) and the call's return value lands on the window base, so
|
||||
releasing a window register hands wo_release_excl whatever now sits
|
||||
there. *)
|
||||
let path = "golden/bc/residual.wo" in
|
||||
let image, _ = emit_str ~file:path (read_file path) in
|
||||
let dump = Disasm.dump image in
|
||||
let block = method_block dump "pair" in
|
||||
let count needle s =
|
||||
let rec go i n =
|
||||
if i >= String.length s then n
|
||||
else
|
||||
match find_substring ~needle (String.sub s i (String.length s - i)) with
|
||||
| None -> n
|
||||
| Some k -> go (i + k + String.length needle) (n + 1)
|
||||
in
|
||||
go 0 0
|
||||
in
|
||||
check_eq "residual: `pair` acquires exactly two exclusive guards" ~expected:2
|
||||
~actual:(count "BORROW_X" block) string_of_int;
|
||||
check_eq "residual: and releases exactly two" ~expected:2
|
||||
~actual:(count "RELEASE_X" block) string_of_int;
|
||||
check "residual: `fixed` (literal indexes, provably distinct) gets no guard at all"
|
||||
(find_substring ~needle:"BORROW" (method_block dump "fixed") = None);
|
||||
(* the guard registers and the CALL's window base must be disjoint *)
|
||||
let regs_of prefix =
|
||||
String.split_on_char '\n' block
|
||||
|> List.filter_map (fun l ->
|
||||
match find_substring ~needle:prefix l with
|
||||
| None -> None
|
||||
| Some _ -> (
|
||||
match find_substring ~needle:"r" (String.trim l) with
|
||||
| None -> None
|
||||
| Some _ ->
|
||||
let l = String.trim l in
|
||||
let i = ref 0 in
|
||||
while !i < String.length l && l.[!i] <> 'r' do
|
||||
incr i
|
||||
done;
|
||||
(* skip the mnemonic's own letters up to the operand *)
|
||||
let rec next_reg j =
|
||||
if j >= String.length l then None
|
||||
else if l.[j] = 'r' && j + 1 < String.length l && l.[j + 1] >= '0'
|
||||
&& l.[j + 1] <= '9' then begin
|
||||
let k = ref (j + 1) in
|
||||
while !k < String.length l && l.[!k] >= '0' && l.[!k] <= '9' do
|
||||
incr k
|
||||
done;
|
||||
Some (int_of_string (String.sub l (j + 1) (!k - j - 1)))
|
||||
end
|
||||
else next_reg (j + 1)
|
||||
in
|
||||
next_reg (String.length prefix)))
|
||||
in
|
||||
let guards = regs_of "RELEASE_X" and windows = regs_of "CALL" in
|
||||
check "residual: no guard register is the call window's base register"
|
||||
(List.for_all (fun g -> not (List.mem g windows)) guards)
|
||||
|
||||
let () =
|
||||
(* Over-budget: 70 owned locals cannot fit the VM's 64-register window,
|
||||
so the method must diagnose WO-E401 rather than emit a truncated
|
||||
frame. Generated rather than a fixture file: the point is the count,
|
||||
and 70 hand-written lines would pin nothing extra. *)
|
||||
let buf = Buffer.create 1024 in
|
||||
Buffer.add_string buf "class Item {\n n: Int\n}\n\nfn wide() -> Int {\n";
|
||||
for i = 0 to 69 do
|
||||
Buffer.add_string buf (Printf.sprintf " let v%d = Item { n: %d }\n" i i)
|
||||
done;
|
||||
Buffer.add_string buf " return 0\n}\n";
|
||||
let _, collector = emit_str ~file:"wide.wo" (Buffer.contents buf) in
|
||||
let diags = Diag.Collector.diagnostics collector in
|
||||
check_eq "register budget: exactly one diagnostic (reported once per method)" ~expected:1
|
||||
~actual:(List.length diags) string_of_int;
|
||||
match diags with
|
||||
| [ d ] ->
|
||||
check "register budget: the code is WO-E401" (d.Diag.code = "WO-E401");
|
||||
check "register budget: it is an error, not a warning" (d.Diag.severity = Diag.Error)
|
||||
| _ -> check "register budget: diagnostic shape" false
|
||||
|
||||
let () =
|
||||
(* The emitter's own view of liveness must agree with the owner pass's
|
||||
LIVE-MASK entries: for every call site the table names, the drop
|
||||
table must carry an entry at some pc whose owned/gc masks hold
|
||||
exactly as many registers as the table listed items. A drift here
|
||||
means the emitter stopped consuming the table it is contracted to. *)
|
||||
let path = "golden/bc/owned.wo" in
|
||||
let src = read_file path in
|
||||
let tables, _ = owner_str ~file:path src in
|
||||
let masks =
|
||||
List.filter_map
|
||||
(fun (d : Owner.drop_site) ->
|
||||
match d.Owner.dr_kind with
|
||||
| Owner.DLiveMask -> Some (List.length d.Owner.dr_items)
|
||||
| _ -> None)
|
||||
tables.Owner.drops
|
||||
in
|
||||
let image, _ = emit_str ~file:path src in
|
||||
let dump = Disasm.dump image in
|
||||
let popcounts =
|
||||
String.split_on_char '\n' dump
|
||||
|> List.filter_map (fun l ->
|
||||
if find_substring ~needle:" drops: pc" l = None then None
|
||||
else
|
||||
Some
|
||||
(List.length
|
||||
(List.filter
|
||||
(fun part -> String.length part > 0 && part.[0] = 'r')
|
||||
(String.split_on_char ','
|
||||
(String.concat ""
|
||||
(String.split_on_char '{'
|
||||
(String.concat "" (String.split_on_char '}' l))))))))
|
||||
in
|
||||
check "live masks: the owner table lists at least one call-site mask for owned.wo"
|
||||
(masks <> []);
|
||||
check "live masks: the emitted drop table carries entries whose widest mask matches the \
|
||||
table's widest LIVE-MASK"
|
||||
(List.fold_left max 0 masks <= List.fold_left max 0 popcounts)
|
||||
|
||||
(* The ownership tables are a contract, not a hint: every DROP the DROPS
|
||||
table asks for, and every rc op the RC table does not mark ELIDED, has
|
||||
to appear in the emitted code exactly once — and nothing else may. A
|
||||
count identity over a whole file is the cheapest way to state that, and
|
||||
it is what caught a missing constructor-field @gc acquire (the escape
|
||||
increment is anchored on the value's expression node, so lowering it
|
||||
per statement kind silently skipped one of the four escapes). *)
|
||||
let () =
|
||||
let count_op needle dump =
|
||||
String.split_on_char '\n' dump
|
||||
|> List.filter (fun l -> find_substring ~needle:(" " ^ needle) l <> None)
|
||||
|> List.length
|
||||
in
|
||||
List.iter
|
||||
(fun path ->
|
||||
let src = read_file path in
|
||||
let tables, coll = owner_str ~file:path src in
|
||||
if not (Diag.Collector.has_error coll) then begin
|
||||
let want_drops =
|
||||
List.fold_left
|
||||
(fun n (d : Owner.drop_site) ->
|
||||
match d.Owner.dr_kind with
|
||||
| Owner.DLiveMask -> n
|
||||
| Owner.DScope _ | Owner.DReturn | Owner.DOverwrite | Owner.DBranchJoin _ ->
|
||||
n
|
||||
+ List.length
|
||||
(List.filter
|
||||
(fun (i : Owner.drop_item) -> i.Owner.di_kind = Owner.LOwned)
|
||||
d.Owner.dr_items))
|
||||
0 tables.Owner.drops
|
||||
in
|
||||
let kept op =
|
||||
List.length
|
||||
(List.filter
|
||||
(fun (r : Owner.rc_site) -> r.Owner.rc_op = op && not r.Owner.rc_elided)
|
||||
tables.Owner.rcs)
|
||||
in
|
||||
let image, _ = emit_str ~file:path src in
|
||||
let dump = Disasm.dump image in
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one DROP per owned drop-table item" path)
|
||||
~expected:want_drops ~actual:(count_op "DROP " dump) string_of_int;
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one RC_INC per KEPT acquire" path)
|
||||
~expected:(kept Owner.RcAcquire) ~actual:(count_op "RC_INC" dump) string_of_int;
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one RC_DEC per KEPT release" path)
|
||||
~expected:(kept Owner.RcRelease) ~actual:(count_op "RC_DEC" dump) string_of_int;
|
||||
(* The residual table is both the only licence to emit a borrow
|
||||
op and an obligation to emit one per *operand*: guards are
|
||||
coalesced per operand, never per entry (asking twice for an
|
||||
exclusive borrow of one object self-traps on legal code). The
|
||||
identity is computed here from the raw table, independently of
|
||||
emit.ml's own coalescing — a region the emitter forgot to
|
||||
consume, or one it expanded per entry, both fail it. *)
|
||||
let operands =
|
||||
let by_region = Hashtbl.create 8 in
|
||||
List.iter
|
||||
(fun (r : Owner.residual_site) ->
|
||||
let cur = try Hashtbl.find by_region r.Owner.rs_node with Not_found -> [] in
|
||||
let cur =
|
||||
List.sort_uniq compare (r.Owner.rs_a_node :: r.Owner.rs_b_node :: cur)
|
||||
in
|
||||
Hashtbl.replace by_region r.Owner.rs_node cur)
|
||||
tables.Owner.residuals;
|
||||
Hashtbl.fold (fun _ ops n -> n + List.length ops) by_region 0
|
||||
in
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one borrow acquire per coalesced residual operand"
|
||||
path)
|
||||
~expected:operands
|
||||
~actual:(count_op "BORROW_S" dump + count_op "BORROW_X" dump)
|
||||
string_of_int;
|
||||
check_eq
|
||||
(Printf.sprintf "table contract %s: one release per coalesced residual operand" path)
|
||||
~expected:operands
|
||||
~actual:(count_op "RELEASE_S" dump + count_op "RELEASE_X" dump)
|
||||
string_of_int
|
||||
end)
|
||||
[ "golden/bc/owned.wo"; "golden/bc/elision.wo"; "golden/bc/residual.wo";
|
||||
"golden/bc/iface.wo"; "golden/owner/moves.wo"; "golden/owner/drops.wo";
|
||||
"golden/owner/rc.wo"; "golden/owner/residual.wo" ]
|
||||
|
||||
(* Shapes that produce a loadable image, one per lowering the goldens do
|
||||
not already cover, plus the two round-trip regressions found while
|
||||
building this task: a forward jump out of the *last* `if`/`while` of a
|
||||
body targets the position after the final instruction (the loader
|
||||
reads that as "jump out of code", so the implicit return has to be
|
||||
appended for that reason too), and a call whose argument count differs
|
||||
from the callee's reserves the wrong window (the loader's "call window
|
||||
exceeds frame"). Each case is emitted and run through the loader's
|
||||
battery — the cheap way to keep the round-trip rule honest for
|
||||
lowerings no fixture file happens to exercise. *)
|
||||
let () =
|
||||
let cases =
|
||||
[ ( "jump target at end of code",
|
||||
"fn f(flag: Bool) {\n if flag {\n return\n }\n}\n" );
|
||||
("while at end of body", "fn f(flag: Bool) {\n while flag {\n flag = false\n }\n}\n");
|
||||
( "for over a multi",
|
||||
"class Item {\n n: Int\n}\n\nclass Bag {\n items: multi Item\n}\n\n\
|
||||
fn total(bag: Bag) -> Int {\n let sum = 0\n for it in bag.items {\n\
|
||||
\ sum = sum + it.n\n }\n return sum\n}\n" );
|
||||
( "map builtins",
|
||||
"class Index {\n by_name: map<Text, Int>\n}\n\nfn f() -> Int {\n\
|
||||
\ let idx = Index { by_name: map_new() }\n set(idx.by_name, \"a\", 1)\n\
|
||||
\ if has(idx.by_name, \"a\") {\n return get(idx.by_name, \"a\")\n }\n\
|
||||
\ return 0\n}\n" );
|
||||
( "text: concat, equality, words",
|
||||
"fn f(a: Text, b: Text) -> Int {\n let joined = a .. b\n\
|
||||
\ if joined == a {\n return 1\n }\n return words(joined)\n}\n" );
|
||||
("db stub statement", "fn f() -> Int {\n insert into rows values (1)\n return 0\n}\n");
|
||||
( "nested calls in arguments",
|
||||
"fn one() -> Int {\n return 1\n}\n\nfn add(a: Int, b: Int) -> Int {\n\
|
||||
\ return a + b\n}\n\nfn f() -> Int {\n return add(add(one(), one()), one())\n}\n" );
|
||||
( "method call on a class instance",
|
||||
"class Counter {\n n: Int\n\n fn bump(by: Int) -> Int {\n\
|
||||
\ return self.n + by\n }\n}\n\nfn f() -> Int {\n\
|
||||
\ let c = Counter { n: 1 }\n return c.bump(2)\n}\n" )
|
||||
]
|
||||
in
|
||||
List.iter
|
||||
(fun (name, src) ->
|
||||
let file = name ^ ".wo" in
|
||||
let image, collector = emit_str ~file src in
|
||||
let diags = Diag.Collector.diagnostics collector in
|
||||
check
|
||||
(Printf.sprintf "lowering %s: compiles clean (%s)" name
|
||||
(String.concat ", " (List.map (fun (d : Diag.t) -> d.Diag.code ^ ": " ^ d.Diag.message) diags)))
|
||||
(diags = []);
|
||||
let violations = validate_image image in
|
||||
check
|
||||
(Printf.sprintf "lowering %s: the loader's battery accepts the image (%s)" name
|
||||
(String.concat "; " violations))
|
||||
(violations = []))
|
||||
cases
|
||||
|
||||
let () =
|
||||
(* Arity is the emitter's business because nothing upstream checks it:
|
||||
types.ml declares WO-E203 and never raises it. An unchecked call
|
||||
would reserve a window the callee does not read — the loader rejects
|
||||
it, which by the round-trip rule would be an emitter bug. *)
|
||||
let _, collector =
|
||||
emit_str ~file:"arity.wo"
|
||||
"fn add3(a: Int, b: Int, c: Int) -> Int {\n return a + b + c\n}\n\n\
|
||||
fn main() {\n print_int(add3(1))\n}\n"
|
||||
in
|
||||
let diags = Diag.Collector.diagnostics collector in
|
||||
check_eq "arity: exactly one diagnostic" ~expected:1 ~actual:(List.length diags) string_of_int;
|
||||
match diags with
|
||||
| [ d ] ->
|
||||
check "arity: reported as WO-E403 (a call the emitter cannot lower)" (d.Diag.code = "WO-E403");
|
||||
check "arity: the message names both counts"
|
||||
(find_substring ~needle:"takes 3 argument(s), given 1" d.Diag.message <> None)
|
||||
| _ -> check "arity: diagnostic shape" false
|
||||
|
||||
let () =
|
||||
(* WO-E405: the entry (`fn main()`, zero args) must declare `Int` or
|
||||
nothing at all -- the systems-track spec makes its return value the
|
||||
process exit code. A `@gc` return escaping through it is exactly
|
||||
the leak this diagnostic exists to close (docs/plan/oop-vm's
|
||||
error-catalog entry): the driver has no way to release a pointer
|
||||
it receives with no return-kind metadata to consult. *)
|
||||
let _, collector =
|
||||
emit_str ~file:"entry-not-int.wo"
|
||||
"class Widget {\n n: Int\n}\n\nfn main() -> Widget {\n return Widget { n: 1 }\n}\n"
|
||||
in
|
||||
let diags = Diag.Collector.diagnostics collector in
|
||||
check_eq "entry return type: exactly one diagnostic" ~expected:1 ~actual:(List.length diags)
|
||||
string_of_int;
|
||||
(match diags with
|
||||
| [ d ] ->
|
||||
check "entry return type: reported as WO-E405" (d.Diag.code = "WO-E405");
|
||||
check "entry return type: the message names the declared type and the exit-code contract"
|
||||
(find_substring ~needle:"`Widget`" d.Diag.message <> None
|
||||
&& find_substring ~needle:"process exit code" d.Diag.message <> None
|
||||
&& find_substring ~needle:"must return `Int`" d.Diag.message <> None)
|
||||
| _ -> check "entry return type: diagnostic shape" false);
|
||||
(* control: no return annotation at all is not "anything other than
|
||||
Int" -- it is the shape every other fixture in this suite uses,
|
||||
and must stay clean. *)
|
||||
let _, clean_collector =
|
||||
emit_str ~file:"entry-no-annotation.wo" "fn main() {\n print_int(0)\n}\n"
|
||||
in
|
||||
check "entry return type: `main` with no return annotation compiles clean"
|
||||
(Diag.Collector.diagnostics clean_collector = [])
|
||||
|
||||
(* ---- CLI smoke: emit mode and --dump-bc ------------------------------ *)
|
||||
|
||||
let () =
|
||||
let path = "golden/bc/arith.wo" in
|
||||
let exit_code, stdout, stderr = run_cli [ "--dump-bc"; path ] in
|
||||
check "cli smoke: --dump-bc on a clean file exits 0" (exit_code = 0);
|
||||
check "cli smoke: clean-file --dump-bc writes nothing to stderr" (stderr = "");
|
||||
let image, _ = emit_str ~file:path (read_file path) in
|
||||
check "cli smoke: --dump-bc stdout matches the in-process disassembly exactly"
|
||||
(stdout = Disasm.dump image)
|
||||
|
||||
let () =
|
||||
(* Unlike the other dumps, --dump-bc prints nothing when the compile is
|
||||
not clean: a disassembly of a program that failed to compile
|
||||
describes bytecode nobody is allowed to run. *)
|
||||
let exit_code, stdout, stderr = run_cli [ "--dump-bc"; "golden/owner-err/use-after-move.wo" ] in
|
||||
check "cli smoke: --dump-bc on a failing compile exits 1" (exit_code = 1);
|
||||
check "cli smoke: the WO-E301 diagnostic still goes to stderr"
|
||||
(find_substring ~needle:"WO-E301" stderr <> None);
|
||||
check "cli smoke: --dump-bc prints no bytecode for a program that did not compile"
|
||||
(stdout = "")
|
||||
|
||||
let () =
|
||||
let out = Filename.temp_file "woc_emit" ".wob" in
|
||||
let exit_code, stdout, stderr = run_cli [ "--emit"; "golden/bc/iface.wo"; "-o"; out ] in
|
||||
check "cli smoke: --emit exits 0 on a clean program" (exit_code = 0);
|
||||
check "cli smoke: --emit prints nothing on stdout" (stdout = "");
|
||||
check "cli smoke: --emit prints nothing on stderr" (stderr = "");
|
||||
let image = read_file out in
|
||||
check "cli smoke: the written image is a WOB1 v1 file"
|
||||
(String.length image > 44 && String.sub image 0 4 = "WOB1");
|
||||
check_eq "cli smoke: the written image passes the loader's battery" ~expected:0
|
||||
~actual:(List.length (validate_image image)) string_of_int;
|
||||
(try Sys.remove out with Sys_error _ -> ());
|
||||
(* a failing compile must leave no image behind *)
|
||||
let out2 = Filename.temp_file "woc_emit" ".wob" in
|
||||
Sys.remove out2;
|
||||
let exit_code, _, _ = run_cli [ "--emit"; "golden/owner-err/use-after-move.wo"; "-o"; out2 ] in
|
||||
check "cli smoke: --emit on a failing compile exits 1" (exit_code = 1);
|
||||
check "cli smoke: and writes no image at all" (not (Sys.file_exists out2));
|
||||
(try Sys.remove out2 with Sys_error _ -> ())
|
||||
|
||||
let () =
|
||||
let exit_code, _, stderr = run_cli [ "--emit"; "golden/bc/arith.wo" ] in
|
||||
check "cli smoke: --emit without -o is a usage error (exit 2)" (exit_code = 2);
|
||||
check "cli smoke: usage goes to stderr" (stderr <> "")
|
||||
|
||||
(* ---- golden-directory walk ------------------------------------------ *)
|
||||
|
||||
(* Each stage directory under golden/ names one `woc --dump-*` flag.
|
||||
|
|
@ -1499,6 +2242,17 @@ let run_stage ~stage ~file ~src : string =
|
|||
let _, collector = owner_str ~file src in
|
||||
let lookup f = if f = file then Some src else None in
|
||||
Diag.Collector.render_all collector lookup ^ "\n"
|
||||
| "bc" ->
|
||||
(* the emitter's own stage: the whole front end, then the `.wob`
|
||||
image, then its disassembly. The dump is produced from the
|
||||
serialized bytes (compiler/src/disasm.ml), so a golden here pins
|
||||
the emitted layout, not just the emitter's intentions. *)
|
||||
let image, collector = emit_str ~file src in
|
||||
if Diag.Collector.has_error collector then begin
|
||||
let lookup f = if f = file then Some src else None in
|
||||
"EMIT FAILED\n" ^ Diag.Collector.render_all collector lookup ^ "\n"
|
||||
end
|
||||
else Disasm.dump image
|
||||
| other ->
|
||||
failwith (Printf.sprintf "runner: unknown golden stage directory %S" other)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,19 @@ Statuses: ✅ **done** · 🔄 **in progress** · ⬜ **pending** · ⏸ **parke
|
|||
|
||||
## ▶ NEXT PLAN
|
||||
|
||||
**Story iteration 4 — single binary end-to-end.**
|
||||
Plan: [`compiler/plan/2026-08-01-wob-emit-e2e-single-binary.md`](plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md) ·
|
||||
Story slice: [`docs/stories/language-runtime-database/04-single-binary-e2e.md`](stories/language-runtime-database/04-single-binary-e2e.md)
|
||||
**Story iteration 5 — language surface (Haxe-parity adoptions).**
|
||||
Plan: [`plan/compiler/2026-08-01-haxe-parity-language.md`](plan/compiler/2026-08-01-haxe-parity-language.md) ·
|
||||
Story slice: [`docs/stories/language-runtime-database/05-language-surface.md`](stories/language-runtime-database/05-language-surface.md)
|
||||
|
||||
The bytecode emitter, the three-kind conformance corpus, and `woc build`. This
|
||||
is the milestone where `.wo` source becomes a running self-contained binary —
|
||||
compiler front (iteration 3) and VM core (iteration 2) both shipped, so it is
|
||||
unblocked. Its inputs are the four ownership tables `owner.ml` now produces;
|
||||
`dump.ml`'s format-contract comments are normative for it, **including the
|
||||
requirement to coalesce borrow guards per operand**.
|
||||
|
||||
Iteration 4 proves the pipeline on the **milestone grammar only** — the
|
||||
emitter, corpus, and `woc build` exercise the grammar iteration 3 already
|
||||
compiles, not the log-watcher sample, which still needs ~200 constructs the
|
||||
front end cannot yet parse (iterations 5–6 work). Iteration 4 must not be
|
||||
judged against the sample (gap-closure spec, §6).
|
||||
The language grows from milestone grammar to a daily-driver surface: every
|
||||
**adopt** row of the systems-track verdict table (switch expressions, typedef
|
||||
records, `?T` optionals, enum payloads, try/catch, statics, `using`, modules,
|
||||
`is`, `pub(read)`, `#if`) lands with a golden + must-fail fixture pair; every
|
||||
**reject** row refuses with a doctrine-citing diagnostic. **First task: `?T`
|
||||
forced handling (plan 8 Task 6)** — plumbed since iteration 3 but unenforced
|
||||
(`WO-E211`–`E213` dead), and the log-watcher port (iterations 6–7) uses
|
||||
optionals throughout in place of the Haxe original's sentinel values, so
|
||||
nothing else in this plan can land ahead of it.
|
||||
|
||||
Two tracks run in this repo. The critical path is the **language track**:
|
||||
iterations 3 → 4 → 5 → 6 → 7, ending at *compile and run log-watcher*. The
|
||||
|
|
@ -50,12 +47,14 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
| 1 | [Principles doc](stories/language-runtime-database/01-principles-doc.md) | ✅ |
|
||||
| 2 | [VM core (`wovm`)](stories/language-runtime-database/02-vm-core.md) | ✅ |
|
||||
| 3 | [Compiler front (`woc`)](stories/language-runtime-database/03-compiler-front.md) | ✅ (known gaps below) |
|
||||
| 4 | [Single binary end-to-end](stories/language-runtime-database/04-single-binary-e2e.md) | 🔄 **next** |
|
||||
| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | ⬜ |
|
||||
| 4 | [Single binary end-to-end](stories/language-runtime-database/04-single-binary-e2e.md) | ✅ (known gaps below) |
|
||||
| 5 | [Language surface](stories/language-runtime-database/05-language-surface.md) | 🔄 **next** |
|
||||
| 6 | [Program mode + stdlib](stories/language-runtime-database/06-program-mode-stdlib.md) | ⬜ |
|
||||
| 7 | [log-watcher proof](stories/language-runtime-database/07-logwatcher-proof.md) | ⬜ acceptance |
|
||||
| 7b | [Inferred GC + mark-sweep](stories/language-runtime-database/07b-inferred-gc-mark-sweep.md) | ⬜ closes iteration 4's gate |
|
||||
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
|
||||
| 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | ⬜ |
|
||||
| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ needs a spec first |
|
||||
| 10 | [HTTP service layer](stories/language-runtime-database/10-http-service.md) | ⬜ |
|
||||
| 11 | [Fibers](stories/language-runtime-database/11-fibers.md) | ⬜ |
|
||||
| 12 | [Blue-green deploy](stories/language-runtime-database/12-blue-green-deploy.md) | ⬜ |
|
||||
|
|
@ -66,9 +65,9 @@ that sequences its tasks. Read one, approve, then the next starts.
|
|||
|
||||
| Track | Item | Where |
|
||||
| --- | --- | --- |
|
||||
| Language | Iteration 4 — emitter, conformance corpus, `woc build` | [plan 3](plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md) |
|
||||
| Language | Iteration 5 — Haxe-parity language surface, `?T` forced handling first | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) |
|
||||
|
||||
Nothing else should be started until iteration 4 lands. Off-critical-path work
|
||||
Nothing else should be started until iteration 5 lands. Off-critical-path work
|
||||
is parked by explicit scope directive (2026-08-08).
|
||||
|
||||
---
|
||||
|
|
@ -86,6 +85,7 @@ is parked by explicit scope directive (2026-08-08).
|
|||
| ✅ | Error catalog | [`oop-vm/01-error-catalog.md`](plan/oop-vm/01-error-catalog.md) | 14 emitted codes + 10 reserved, each with the reason it is not yet emitted |
|
||||
| ✅ | log-watcher `.wo` sample | [`../examples/log-watcher/`](examples/log-watcher/README.md) | Eight-file port authored docs-first with its `.hx` mapping table; compiles for real in iteration 7 |
|
||||
| ✅ | Scalar cleanup | [`discarded.md`](plan/discarded.md) | `Money`/`SKU`/`Float` and the abstract allowlist removed; `abstract` flipped adopt → reject |
|
||||
| ✅ | `woc` emitter, corpus, single binary | [plan 3](plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md) | Tasks 1–6 + 8 (Task 7, a parity harness against the Rust runtime, **deferred by explicit user decision** — the two stacks diverge by design). Bytecode emitter (`emit.ml`) + disassembler (`disasm.ml`, `--dump-bc`); three-kind conformance harness (`scripts/oop-e2e.sh`, `just oop-e2e`) over `tests/corpus/{run,compile-fail,trap,gc}`; pricing-demo + ownership/trap corpora (19 fixtures); `@gc` cycle collector's post-exit pump (`WO_GC_BUDGET`/`WO_GC_TRACE`) + 2 gc fixtures (`gc/held-cycle` retired — see criterion-3 closure below); `woc build` single-binary output + relocation/corrupt-trailer smoke; `WO-E405` closing criterion 3's ASan leak (entry must return `Int`); `just oop-accept` wiring all five spec criteria + both unit gates into one command. 14 + 399 compiler checks; `oop-e2e` 25/25 against the release `wovm`. **Milestone-1 acceptance gate is fully green — all five criteria met** (see the dated acceptance note in `docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`) |
|
||||
|
||||
**Known gaps carried out of iteration 3** — recorded, not silently owed:
|
||||
|
||||
|
|
@ -102,6 +102,46 @@ is parked by explicit scope directive (2026-08-08).
|
|||
unresolved-callee drops, RC table ordering, residual b-side role) are listed
|
||||
in the plan-2 SDD ledger and in the affected files' own comments.
|
||||
|
||||
**Known gaps carried out of iteration 4** — recorded, not silently owed:
|
||||
|
||||
- **`WO-E205` (unsatisfied interface) is reachable but unenforced — a real
|
||||
hybrid-boundary inversion, not just a dead code path.** A class that does
|
||||
not structurally satisfy an interface it's passed as compiles clean (exit
|
||||
0, zero diagnostics) even though the violation is statically provable, and
|
||||
the mismatched call reaches `wovm` as an `ICALL` with no matching vtable
|
||||
entry, trapping `WO_T_BOUNDS` (6) at runtime instead of failing at compile
|
||||
time. Pinned by `tests/corpus/trap/unsatisfied-interface/`; when `WO-E205`
|
||||
is wired, that fixture must move to `compile-fail/` in the same change.
|
||||
- **`set(m, k, v)`'s `@gc` retention gap on map keys/values is open** — the
|
||||
twin of the `push` bug Task 5 fixed for `multi`. `set` has no equivalent
|
||||
special case in `owner.ml`'s `analyze_call`, so a `@gc` key or value handed
|
||||
to `set` is under-counted and the collector can free it while the map still
|
||||
points at it. Nothing in the corpus exercises this yet. See
|
||||
[`oop-vm/08-builtin-surface.md`](plan/oop-vm/08-builtin-surface.md).
|
||||
- **E201/E203 and seven other `WO-E2xx` codes remain declared but unemitted**
|
||||
— see [`oop-vm/01-error-catalog.md`](plan/oop-vm/01-error-catalog.md).
|
||||
- **CLOSED — milestone-1's ASan gate (`just oop-accept`) failing on
|
||||
`gc/held-cycle`.** Root cause (Task 8's finding, restated): `main.c`'s
|
||||
entry-method return value (`uint64_t ret`, `src/main.c:158`) is stored
|
||||
but never released, so `gc/held-cycle`'s "permanent external hold" was
|
||||
actually a permanent refcount inflation — LeakSanitizer's "definite
|
||||
leak" (1184 bytes / 3 allocations) was correctly reporting exactly
|
||||
that, not a false positive. Fixing it by releasing `ret` was rejected:
|
||||
the `.wob` method table carries no return-type/kind metadata, so
|
||||
`main.c` has no way to know `ret` is a pointer rather than a scalar,
|
||||
and adding that metadata is a format change out of scope here. Fixed
|
||||
instead at the source: the systems-track spec already requires the
|
||||
entry to return `Int` (its return value is the process exit code), so
|
||||
a class-returning `main` was never legal — `WO-E405`
|
||||
(`compiler/src/emit.ml`, `01-error-catalog.md`) now rejects it at
|
||||
compile time, and `gc/held-cycle` is retired because its premise (an
|
||||
externally-held cycle survives a *post-exit* pump) is no longer
|
||||
expressible — see `oop-vm/02-corpus.md`'s "Retired" note for why, and
|
||||
for where the scenario it meant to cover is actually proven
|
||||
(`runtime/test/test_cycle.c`, plus a proper in-flight fixture scheduled
|
||||
for story iteration 7b). Spec success criterion 3 is now **MET**;
|
||||
`just oop-accept` passes all five criteria.
|
||||
|
||||
### Rust runtime track — Stage 2 shipped, maintained
|
||||
|
||||
| Status | Phase | Doc | Notes |
|
||||
|
|
@ -135,8 +175,10 @@ Ecommerce sample (verified 2026-06-13): `api.rest` 17/17 expected statuses pass.
|
|||
| 5 | Haxe-parity language surface — **`?T` forced handling first**, then switch expressions, records, enum payloads, try/catch, statics, `using`, modules, `is`, `pub(read)`, `#if` | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) |
|
||||
| 6 | Program mode + systems stdlib — `fn main`, exit codes, `fs`/`proc`/`net`/`time`/`json` | [plan 9](superpowers/plans/2026-08-01-program-mode-stdlib.md) |
|
||||
| 7 | log-watcher proof — the sample compiles and detects a silent death live | [plan 10](superpowers/plans/2026-08-01-log-watcher-sample.md) |
|
||||
| 7b | Inferred GC + incremental mark-sweep — `@gc` removed, GC-ness inferred, RC retired | [spec](superpowers/specs/2026-08-11-inferred-gc-mark-sweep-design.md) — plan to be written |
|
||||
| 8 | Shard-actor runtime | [plan 4](superpowers/plans/2026-08-01-shard-actor-vm-runtime.md) |
|
||||
| 9 | Database engine binding | [plan 5](superpowers/plans/2026-08-01-db-engine-binding.md) |
|
||||
| 9b | `@table` + relations + language-integrated query | **no spec yet** — three open forks recorded in the iteration; brainstorm before planning |
|
||||
| 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–10 |
|
||||
|
|
|
|||
|
|
@ -47,3 +47,80 @@ All integers little-endian; offsets are absolute file offsets.
|
|||
**Builtins:** now (ms), print (text), print_int, words (whitespace token count), multi_new/multi_push/multi_get/count/latest, map_new/map_set/map_get/map_has.
|
||||
|
||||
**Trap codes:** DIV0, BORROW, STACK, OOM, DB, BOUNDS, KEY, EXPLICIT.
|
||||
|
||||
## Single-binary trailer (`woc build`, plan 3 Task 6)
|
||||
|
||||
This section is **not part of the `.wob` format above** — `.wob` v1 is unchanged.
|
||||
It documents the wrapper a *deployable executable* carries: `woc build <dir> -o
|
||||
app` makes `app` by copying the `wovm` runtime binary and appending the
|
||||
compiled `.wob` image plus a small fixed-size trailer. `wovm`'s own startup
|
||||
(`runtime/src/main.c`) looks for this trailer in its own executable
|
||||
(`/proc/self/exe`) before falling back to the classic `wovm file.wob` argv
|
||||
contract, so the result runs standalone with no separate `.wob` file. Writer:
|
||||
`compiler/bin/main.ml`. Reader: `runtime/src/main.c`'s `load_self_embedded`.
|
||||
Append-based only, deliberately — no ELF section manipulation.
|
||||
|
||||
**Layout** — the trailer is the fixed **last 20 bytes** of the file, all
|
||||
integers little-endian, found by seeking from the end (no scanning):
|
||||
|
||||
```
|
||||
byte offset from EOF size field
|
||||
-20 8 payload_off -- absolute file offset where the embedded .wob image starts
|
||||
-12 8 payload_len -- length in bytes of the embedded .wob image
|
||||
-4 4 magic -- 0x31544257 ("WBT1" read as LE u32, mirrors WOB_MAGIC's "WOB1")
|
||||
|
||||
[ wovm runtime bytes (payload_off bytes) ][ .wob image (payload_len bytes) ][ trailer: payload_off | payload_len | magic ]
|
||||
^ byte 0 ^ byte payload_off ^ byte payload_off+payload_len == file_size-20
|
||||
file_size ^
|
||||
```
|
||||
|
||||
**Reader algorithm** (`load_self_embedded`): open `/proc/self/exe`; if the
|
||||
file is shorter than 20 bytes, or its last 4 bytes don't equal the magic,
|
||||
there is no trailer — fall back to the argv `.wob` path unchanged. If the
|
||||
magic matches, `payload_off` and `payload_len` are validated to account for
|
||||
*every* trailing byte exactly (`payload_off + payload_len == file_size -
|
||||
20`, checked via a bounds-safe subtraction so a corrupt/huge value can't
|
||||
wrap the arithmetic and slip past); any mismatch is reported as a clear
|
||||
"corrupt trailer" error (exit 2) rather than a crash or silent
|
||||
misbehavior. On success, the executable is mmap'd and `wo_load_buf` parses
|
||||
the embedded region exactly as `wo_load_file` parses a standalone `.wob`
|
||||
today — argv is never consulted.
|
||||
|
||||
**Runtime location (writer side):** `--runtime <path>` wins when given;
|
||||
otherwise the default is `runtime/wovm` resolved relative to the current
|
||||
working directory (the same repo-root-relative assumption every other
|
||||
`just`/build-tooling entry point in this repo already makes). A missing
|
||||
runtime binary is a build-time error naming the recipe: `make -C runtime
|
||||
wovm`. `woc build` never invokes or inspects the runtime binary beyond
|
||||
reading its bytes — it does not need to be executable *as run by woc*, only
|
||||
as run by whoever runs the produced artifact.
|
||||
|
||||
**Edge cases decided for `woc build`** (each implemented deliberately, not
|
||||
left to fall out accidentally):
|
||||
|
||||
- **Output path already exists:** overwritten, but atomically — the new
|
||||
binary is assembled in a temp file (`<out>.woc-build.tmp`, freshly
|
||||
created with mode `0755` each time so a stale temp file's permissions
|
||||
can never leak through) next to `-o`, then renamed over it. A failed
|
||||
build (bad compile, missing runtime, disk-full mid-write) never
|
||||
clobbers a previously-working binary with a partial one.
|
||||
- **A directory with no `main`:** unlike `--emit` (where a `.wob` with no
|
||||
entry method is a legitimate, already-specified artifact), `build`'s
|
||||
entire purpose is something runnable, so a clean compile with no
|
||||
zero-argument free fn named `main` is a **build-time error, no output
|
||||
written** — not deferred to `wovm`'s own "module has no entry method"
|
||||
message at run time. Detected by reading the compiled image's own
|
||||
entry field (`WOB_OFF_ENTRY`, offset 40) rather than plumbing a new
|
||||
return value through the emitter.
|
||||
- **`--runtime` itself already carries a trailer** (rebuilding from a
|
||||
previously-built single binary): its embedded payload is *stripped*
|
||||
before copying — the writer recognizes its own trailer on the input
|
||||
runtime binary the same way the C reader does, and keeps only the
|
||||
pristine runtime prefix (`payload_off` bytes). This makes `woc build
|
||||
... --runtime already-built-app -o new-app` produce a binary
|
||||
byte-identical in size to building fresh from `runtime/wovm` directly,
|
||||
instead of chaining stale payloads and bloating on every rebuild. Any
|
||||
input that doesn't unambiguously look like our own trailer (wrong
|
||||
magic, or offsets that don't exactly account for every trailing byte)
|
||||
is left untouched and copied as-is — the safe default when it's not
|
||||
certain.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
# The `woc` diagnostic catalog — normative reference
|
||||
|
||||
Every `WO-E###`/`WO-W###` code the `woc` front end (`compiler/`) actually
|
||||
emits, as of plan 2 tasks 2–8. Code ranges are reserved per stage
|
||||
(`compiler/src/diag.ml`): `WO-E0xx` lexing, `WO-E1xx` parsing, `WO-E2xx`
|
||||
types, `WO-E3xx` ownership, `WO-W2xx` warnings from the types stage. This
|
||||
emits, as of plan 2 tasks 2–8 and plan 3 tasks 1–2. Code ranges are reserved
|
||||
per stage (`compiler/src/diag.ml`): `WO-E0xx` lexing, `WO-E1xx` parsing,
|
||||
`WO-E2xx` types, `WO-E3xx` ownership, `WO-E4xx` the bytecode emitter,
|
||||
`WO-W2xx` warnings from the types stage. This
|
||||
is an enumeration of codes already in use, not an archaeology dig — see
|
||||
"Completeness method" below for how that was verified, "Reserved,
|
||||
not yet emitted" for codes the source declares but no check yet raises,
|
||||
and "Unreachable by design" for the one code (WO-E205) that isn't merely
|
||||
unimplemented — it has no legal call site in the milestone grammar.
|
||||
and "Reachable but unenforced" for the one code (WO-E205) whose check
|
||||
site the milestone grammar *does* exercise, unlike the codes above it —
|
||||
see that section for why this is a live gap, not a scope boundary.
|
||||
One code (WO-E214) is emitted by the driver (`compiler/bin/main.ml`),
|
||||
not one of the four stage modules — a Task 8 review finding — see its
|
||||
row in the types table below for why it still uses that range.
|
||||
|
|
@ -32,7 +34,7 @@ half of the story ("moved here" / "borrowed here" / etc.).
|
|||
| WO-E101 | generic syntax error: an unexpected token where the grammar expected something else, including running off the end of the file inside an unclosed block/type/interface body. Declaration-level recovery syncs to the next top-level keyword so one bad declaration yields one diagnostic, not a cascade. | `expected ')' or ',', got NEWLINE` |
|
||||
| WO-E102 | an invalid `@table(...)` configuration: `name` given twice, an `index` with no columns, or an argument key other than `name`/`index`. | `@table(name: ...) given twice` |
|
||||
|
||||
## WO-E2xx / WO-W2xx — types (Task 6, `compiler/src/types.ml`; WO-E214 Task 8, `compiler/bin/main.ml`)
|
||||
## WO-E2xx / WO-W2xx — types (Task 6, `compiler/src/types.ml`; WO-E214 Task 8, `compiler/bin/main.ml`; WO-E215 plan 3 Task 2, `compiler/src/types.ml`)
|
||||
|
||||
| code | meaning | example message |
|
||||
| --- | --- | --- |
|
||||
|
|
@ -41,6 +43,7 @@ half of the story ("moved here" / "borrowed here" / etc.).
|
|||
| WO-E206 | a constructor literal (`ClassName { ... }`) omits a field the class declares (no default). | `missing field \`sku\` in constructor of \`Product\`` |
|
||||
| WO-E207 | a constructor literal names a class that isn't declared anywhere in the (possibly multi-file) program. | `unknown type \`Widget\` in constructor` |
|
||||
| WO-E214 | a class or interface name is declared more than once across the files a directory discovers (one program, multiple files — Task 8). Reported at the *later*-discovered declaration (sorted by path), with the first declaration as the related site; the merged symbol table keeps the first one, so this is what stops that silent keep from also hiding a real shape conflict. Driver-level, not `types.ml` — reuses the `types_prefix` range because it's a symbol-table concern, not a lexing/parsing/ownership one. | `class \`Dup\` already declared in \`a_first.wo\`` |
|
||||
| WO-E215 | a class, interface, or free `fn` name is declared more than once in the *same file* (`collect_declarations`'s own `StringMap.add` silently dropped the earlier one — Task 1 review, found while building the plan-3 emitter, fixed in Task 2). Reported at the later declaration, with the first as the related site — the same shape as WO-E214, one file instead of two; the symbol table keeps the first declaration. Class/interface names and free-fn names are separate namespaces, so a class and a fn sharing a name never collide here. | `class \`Dup\` already declared` |
|
||||
| WO-E225 | a field's declared type name isn't a builtin scalar, a declared class, or a declared interface. Checked once per field declaration, at the field's own position. | `unknown type \`Wdiget\`` |
|
||||
|
||||
### Reserved, not yet emitted
|
||||
|
|
@ -59,20 +62,40 @@ conformance fixture (plan 3) or a future reader doesn't assume one of
|
|||
these codes is reachable today; move a code up into the table above in
|
||||
the same commit that wires its first real emission site.
|
||||
|
||||
### Unreachable by design
|
||||
### Reachable but unenforced
|
||||
|
||||
`unsatisfied_interface_code` (WO-E205) is declared in `types.ml` but does not
|
||||
belong in the list above — it is not a pending implementation, it is
|
||||
unreachable by design given the milestone grammar. Structural interface
|
||||
satisfaction has exactly one legal home: a site where a value is used at an
|
||||
interface-typed position (a field, parameter, or return typed as an
|
||||
interface). There is no `implements` keyword by doctrine — satisfaction is
|
||||
structural, checked where the value is used, not declared — and the
|
||||
milestone grammar declares no interfaces and exercises no interface-typed
|
||||
positions, so the check has nowhere to fire. This is not a gap in shipped
|
||||
work; it costs the milestone nothing. The check starts firing the moment a
|
||||
future milestone introduces an interface-typed position — no interim
|
||||
workaround is owed before then.
|
||||
belong in the "Reserved, not yet emitted" list above either. An earlier
|
||||
revision of this doc claimed WO-E205 was *unreachable by design* — that
|
||||
was wrong, caught and corrected in the plan-3 Task 4 review (2026-08-11).
|
||||
Structural interface satisfaction's one legal check site is where a value
|
||||
is used at an interface-typed position (a field, parameter, or return
|
||||
typed as an interface) — and the milestone grammar does exercise that
|
||||
position today. This compiles with exit 0 and zero diagnostics:
|
||||
|
||||
```wo
|
||||
interface Priced { fn current_price() -> Int }
|
||||
class Rock { n: Int }
|
||||
fn quote(p: Priced) -> Int { return p.current_price() }
|
||||
fn main() { let r = Rock { n: 1 }
|
||||
print_int(quote(r)) }
|
||||
```
|
||||
|
||||
`Rock` has no `current_price` method, so it does not structurally satisfy
|
||||
`Priced` passed to `quote`'s interface-typed parameter — and `Rock`'s
|
||||
method set is fully known at compile time, so this is a *statically
|
||||
provable* violation, exactly the shape WO-E205 exists to catch. `woc
|
||||
--emit` accepts it anyway. The unchecked call reaches `wovm` as an
|
||||
`ICALL` with no matching vtable slot, which traps `WO_T_BOUNDS` (6, "no
|
||||
vtable entry for receiver class") at runtime instead of failing to
|
||||
compile. That inverts the hybrid boundary WO-E3xx pins elsewhere
|
||||
(provable violation → compile-time diagnostic, unprovable → runtime
|
||||
trap): here a provable violation resolves as a trap. This is an owed
|
||||
gap, not a design decision, currently pinned as the known-gap fixture
|
||||
`tests/corpus/trap/unsatisfied-interface/` (plan 3, Task 4) — its own
|
||||
comment says it must move to `compile-fail/` with `fixture.code
|
||||
WO-E205` in the same change that implements this check, rather than
|
||||
silently going stale.
|
||||
|
||||
## WO-E3xx — ownership / MVS (Task 7, `compiler/src/owner.ml`)
|
||||
|
||||
|
|
@ -88,6 +111,24 @@ renders indented beneath it.
|
|||
| WO-E303 | two exclusive (`mut`) accesses of the same place, or two accesses the analysis can *prove* overlap, conflict in one region (e.g. two `mut` element accesses through the same provable index, or the same place borrowed and then mutated). Cases the analysis can't prove either way become a residual site for the VM to guard at runtime, not this diagnostic. | `cannot borrow \`bag.items[i]\` as \`mut\` twice in the same call` |
|
||||
| WO-E304 | a borrow is returned or stored somewhere that outlives the scope it borrowed from. `@gc`-typed values are exempt (freely aliased by design). | `borrow of \`x\` returned — borrows cannot outlive their scope` |
|
||||
|
||||
## WO-E4xx — emitter (plan 3 Task 1, `compiler/src/emit.ml`)
|
||||
|
||||
The emitter's range covers the two boundaries nothing upstream can see:
|
||||
the `.wob` format's own encoding limits, and the milestone-1 instruction
|
||||
set's edge — surface the front end accepts but the VM has no operation
|
||||
for. Both are reported, never worked around: an over-budget method is a
|
||||
diagnostic rather than a truncated frame, and a construct with no
|
||||
lowering is a diagnostic rather than invented bytecode. No image is
|
||||
written when any of these fire (`woc --emit` writes nothing on exit 1).
|
||||
|
||||
| code | meaning | example message |
|
||||
| --- | --- | --- |
|
||||
| WO-E401 | the method needs more than 64 registers — the VM's register window (`runtime/src/wob.h` `WO_MAX_REGS`, enforced by the loader). Reported once per method, at the method's own position. | `` `wide` needs more than 64 registers — the VM's register window is 64 slots; split the method or reduce the number of live locals `` |
|
||||
| WO-E402 | a value that does not fit an instruction field: a constant/class/method/interface-slot index above 65535 (`LOADK`/`NEW`/`CALL`/`ICALL` carry a 16-bit operand), a field index above 255 (`GETF`/`SETF` carry a byte), or a jump farther than the signed 16-bit displacement. | `field index 300 exceeds the 8-bit GETF/SETF field` |
|
||||
| WO-E403 | a construct the v1 instruction set cannot express, or a call the emitter cannot lower correctly. The full source-surface contract is [`08-builtin-surface.md`](08-builtin-surface.md); the cases raised here are: an unresolved name; a call to something that is neither a declared `fn` nor a builtin; a wrong argument count (nothing upstream checks arity — WO-E203 is declared and never raised — and a mismatched call reserves a window the callee does not read, which the loader rejects); a field/method on a type that is not a declared class; `multi_new()`/`map_new()` with no destination of declared type (the element kinds are the container's runtime drop plan and cannot be guessed); an element write into a `multi` (v1 has `multi_push`/`multi_get`, no element store); a `for` over a `map` (v1 exposes no key enumeration). Several of these are cases the typechecker's placeholder types let through — the emitter is the first stage that must be exact. | `` `for` can only iterate a `multi` — the v1 builtins expose no key enumeration for a `map` `` |
|
||||
| WO-E404 | an ownership-table entry the emitter could not honor: a residual borrow site whose operand has no register at the guarded region, a residual region no lowering wrapped at all (checked at the end of every compilation unit — owner.ml anchors regions on several different node kinds, and one nobody consumed would ship the aliasing check silently disabled), or a drop/rc site naming a local that has no register. Emitting such a region unguarded would drop the single enforcement a residual site exists for, so it fails instead. | `` residual borrow site in `shuffle` names an operand with no live register — the runtime guard cannot be placed `` |
|
||||
| WO-E405 | the program entry (the zero-arg free fn `main`, selected by name) declares a return type other than `Int`. The systems-track spec (`docs/superpowers/specs/2026-08-01-systems-track-design.md:70`) makes the entry's return value the process exit code, so any other declared return type was never legal — this is the check that finally says so. `main` with no return annotation at all is unaffected (nothing declared to contradict `Int`); every other milestone-1 fixture uses that form. Reported once, at `main`'s own position. | `` entry `main` declares return type `Node` — the entry's return value is the process exit code, so it must return `Int` `` |
|
||||
|
||||
## Completeness method
|
||||
|
||||
Every code in this catalog was found the same way: grep every
|
||||
|
|
@ -100,7 +141,9 @@ constant it names. Every constant with at least one such call site is
|
|||
in the table above; every constant with zero call sites is listed under
|
||||
"Reserved, not yet emitted" instead of silently omitted. `parser.ml`'s
|
||||
`fail`/`unexpected`, `owner.ml`'s `report`/`escape`/`check_against_borrows`,
|
||||
and `main.ml`'s `report_collision` are the only indirection layers
|
||||
`emit.ml`'s `err`/`over_budget`/`check_bx`/`check_field_idx`, `main.ml`'s
|
||||
`report_collision`, and `types.ml`'s `report_duplicate_decl` (plan 3 Task 2)
|
||||
are the only indirection layers
|
||||
between a bare `~code:` argument and the `Diag.error` call — each was
|
||||
read to confirm which named constant ultimately reaches the collector,
|
||||
not just the arity-generic wrapper name. This is why the catalog is an
|
||||
|
|
|
|||
222
docs/plan/oop-vm/02-corpus.md
Normal file
222
docs/plan/oop-vm/02-corpus.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# `tests/corpus/` — how to add a conformance fixture
|
||||
|
||||
> The contribution path every later sub-project's corpus (`actor/`,
|
||||
> `db/`, `lang/`, `sys/`, `sample-logwatcher/`) follows, and the one
|
||||
> `gc/` (below) already uses. Enforced by
|
||||
> [`scripts/oop-e2e.sh`](../../../scripts/oop-e2e.sh) (plan 3, Task 2), run
|
||||
> via `just oop-e2e`. What a `.wo` fixture may actually say is
|
||||
> [`08-builtin-surface.md`](08-builtin-surface.md)'s contract, not this
|
||||
> doc's — read that first, or you will write fixtures against the
|
||||
> compiler's internals instead of its source-language contract and waste
|
||||
> time chasing `WO-E403`s that were never about your fixture's intent.
|
||||
|
||||
## Layout: one directory per fixture, fixed filenames
|
||||
|
||||
Every fixture is its own directory under its kind (`run/`, `compile-fail/`,
|
||||
`trap/`, `gc/`), named for what it exercises (kebab-case, e.g.
|
||||
`interface-dispatch`, not `test3`). Inside, filenames are fixed so the
|
||||
harness can walk every kind the same way:
|
||||
|
||||
```
|
||||
tests/corpus/run/<name>/fixture.wo
|
||||
tests/corpus/run/<name>/fixture.out
|
||||
|
||||
tests/corpus/compile-fail/<name>/fixture.wo
|
||||
tests/corpus/compile-fail/<name>/fixture.code
|
||||
|
||||
tests/corpus/trap/<name>/fixture.wo
|
||||
tests/corpus/trap/<name>/fixture.trap
|
||||
|
||||
tests/corpus/gc/<name>/fixture.wo
|
||||
tests/corpus/gc/<name>/fixture.out
|
||||
tests/corpus/gc/<name>/fixture.trace
|
||||
tests/corpus/gc/<name>/fixture.gc_budget -- optional
|
||||
```
|
||||
|
||||
`scripts/oop-e2e.sh` globs `tests/corpus/<kind>/*/`, so a stray `.wo` file
|
||||
placed directly inside a kind directory (not in its own subdirectory) is
|
||||
never picked up — no error, no run, it just silently does not exist as a
|
||||
fixture. If a fixture stops appearing in the tally, check that first.
|
||||
|
||||
## `run/` — compiles, runs, exact stdout
|
||||
|
||||
**Files:** `fixture.wo`, `fixture.out`.
|
||||
|
||||
**Rule:** `woc --emit fixture.wo -o <scratch>.wob` must exit 0, then
|
||||
`wovm <scratch>.wob` must exit 0 with stdout **byte-for-byte identical**
|
||||
to `fixture.out` — trailing newline included, since `print`/`print_int`
|
||||
are newline-terminated (`08-builtin-surface.md`). No substring match, no
|
||||
trimming. Generate `fixture.out` by actually running the fixture, not by
|
||||
hand-typing what you expect the output to be:
|
||||
|
||||
```sh
|
||||
just woc-build # compiler/_build/default/bin/woc
|
||||
make -C runtime wovm
|
||||
compiler/_build/default/bin/woc --emit tests/corpus/run/<name>/fixture.wo -o /tmp/f.wob
|
||||
runtime/wovm /tmp/f.wob > tests/corpus/run/<name>/fixture.out
|
||||
```
|
||||
|
||||
Then read `fixture.out` back and sanity-check it says what you meant —
|
||||
a byte-exact copy of a wrong run is still wrong, just consistently so.
|
||||
|
||||
The four seed fixtures (`hello`, `arithmetic`, `methods`, `interface`)
|
||||
cover: `print`/`print_int`; arithmetic and control flow, including the
|
||||
two operators the v1 instruction set lowers rather than gives an opcode
|
||||
(`%`, `!=`); a direct method call (`CALL` by method index, receiver's
|
||||
declared type is a concrete class); and structural interface dispatch
|
||||
(`ICALL` by vtable slot, receiver's declared type is an interface, no
|
||||
`implements` keyword). Look at these before writing a new one — they are
|
||||
proof that a given construct actually round-trips through the real
|
||||
`wovm`, not just through `--dump-bc`.
|
||||
|
||||
## `compile-fail/` — must fail with exactly one code
|
||||
|
||||
**Files:** `fixture.wo`, `fixture.code`.
|
||||
|
||||
**Rule:** `fixture.code` names exactly one diagnostic code (whitespace is
|
||||
stripped, so `WO-E215` on its own line is enough). `woc --emit fixture.wo
|
||||
-o <scratch>.wob` must exit 1 with that code appearing in stderr. Exit 0
|
||||
(compiled clean), exit 2 (a usage/IO failure, not a diagnostic), or exit 1
|
||||
with a *different* code are all failures — the harness names which.
|
||||
|
||||
Use `--emit`, not the bare `woc <path>` check-only form, when hand-testing
|
||||
a fixture: `--emit` runs the full pipeline including the emitter, so it
|
||||
also catches `WO-E4xx` cases (register budget, unlowerable constructs)
|
||||
that check-only mode never reaches. `scripts/oop-e2e.sh` always uses
|
||||
`--emit` for this kind for the same reason.
|
||||
|
||||
Every code in [`01-error-catalog.md`](01-error-catalog.md)'s main tables
|
||||
is a legitimate `compile-fail/` target; the codes under "Reserved, not yet
|
||||
emitted" are not — there is no call site to trigger them yet.
|
||||
|
||||
## `trap/` — must compile, then trap with exactly one code
|
||||
|
||||
**Files:** `fixture.wo`, `fixture.trap`.
|
||||
|
||||
**Rule:** `fixture.trap` names exactly one integer trap code (again,
|
||||
whitespace-stripped). `woc --emit` must exit 0 (a `trap/` fixture that
|
||||
fails to *compile* is a `compile-fail/` fixture wearing the wrong hat —
|
||||
move it). Then `wovm <scratch>.wob` must exit 1, with stderr's one fixed
|
||||
line
|
||||
|
||||
```
|
||||
trap CODE in METHOD at line L: MESSAGE
|
||||
```
|
||||
|
||||
giving exactly the `CODE` named in `fixture.trap`. Exit 0 (ran to
|
||||
completion instead of trapping), exit 2 (a loader rejection — the image
|
||||
was malformed, not merely trapped at runtime), or exit 1 with a different
|
||||
`CODE` are all failures.
|
||||
|
||||
## `gc/` — must compile, run to completion, and drive the collector exactly
|
||||
|
||||
**Files:** `fixture.wo`, `fixture.out`, `fixture.trace`, optionally
|
||||
`fixture.gc_budget`.
|
||||
|
||||
**Rule:** `woc --emit` must exit 0, then `wovm <scratch>.wob` must exit 0
|
||||
with `WO_GC_TRACE=1` set (and `WO_GC_BUDGET` set from `fixture.gc_budget`
|
||||
if the fixture has one). Two things are then checked exactly, both
|
||||
generated by actually running the fixture, never hand-typed:
|
||||
|
||||
- **stdout**, byte-for-byte against `fixture.out` — same rule as `run/`.
|
||||
This is the fixture proving it executed the intended shape (e.g. a
|
||||
container's element count) before anything is abandoned.
|
||||
- **the gc pump's stderr trace**, against `fixture.trace`. The pump
|
||||
(`runtime/src/main.c`) prints one `gc: step N budget=B freed=F
|
||||
visited=V remaining=R` line per collection step; `fixture.trace` names
|
||||
the exact total step count and the exact total freed count across every
|
||||
step, as two `key=value` lines:
|
||||
|
||||
```
|
||||
steps=1
|
||||
freed=2
|
||||
```
|
||||
|
||||
The harness counts `^gc: step ` lines in stderr for `steps=`, and sums
|
||||
every step's `freed=` value for `freed=`. A wrong count either way — an
|
||||
object freed that should have survived, one that should have been
|
||||
freed but wasn't, or a sweep that didn't slice the way the fixture's
|
||||
budget says it should — is a named failure, exactly like a wrong
|
||||
`WO-E###` or trap code.
|
||||
|
||||
**Why not assert via ASan/LeakSanitizer instead:** a sanitizer *is* how
|
||||
each `gc/` fixture was actually verified (see below) and is the right
|
||||
tool for proving a freed object was genuinely freed, not recycled inside
|
||||
the arena's own freelist where nothing external can observe it. But
|
||||
LeakSanitizer's leak scan is conservative — it can find a stray bit
|
||||
pattern in the VM's own register file that happens to alias a live heap
|
||||
address and treat an object as "reachable" that the collector's own
|
||||
bookkeeping would not — so its *exact* output is not stable enough to
|
||||
assert byte-for-byte in an automated regression gate. The trace's
|
||||
`steps=`/`freed=` counts come straight from the collector's own
|
||||
accounting (`wo_gc_step`'s return value and the public cycle-candidate
|
||||
buffer length in `runtime/src/obj.h`), so they are exactly reproducible;
|
||||
running the whole corpus under an ASan+UBSan `wovm` (`make -C runtime
|
||||
wovm-asan`) is a supplementary, manual check, not something
|
||||
`scripts/oop-e2e.sh` automates.
|
||||
|
||||
**Retired: `gc/held-cycle` (milestone-1 criterion-3 closure).** An earlier
|
||||
fixture returned a `@gc` cycle from `main` to model an *externally held*
|
||||
cycle — a root the pump must not collect. It could never actually prove
|
||||
that: the program entry's return value is the process exit code
|
||||
(`docs/superpowers/specs/2026-08-01-systems-track-design.md:70`), and
|
||||
`runtime/src/main.c` never releases it, so the fixture's "hold" was
|
||||
really a permanent, un-freeable refcount inflation — indistinguishable
|
||||
from a leak, and confirmed as exactly that: `runtime/build/wovm_asan`
|
||||
reported it as a genuine LeakSanitizer definite leak. `WO-E405`
|
||||
(`compiler/src/emit.ml`, `01-error-catalog.md`) now rejects a non-`Int`
|
||||
entry return type at compile time, which makes the fixture's own
|
||||
premise inexpressible — a post-exit pump has no live roots once the
|
||||
entry returns, by construction, so an *externally held* cycle cannot be
|
||||
modeled from inside a `.wo` program at all. The scenario this fixture
|
||||
meant to cover — a cycle kept alive by a real external root — is
|
||||
already covered properly by
|
||||
`test_externally_held_cycle_survives_then_dies` in
|
||||
`runtime/test/test_cycle.c`, which holds its root the honest way (a C
|
||||
local variable, not a leaked return value). Story iteration 7b (tracing
|
||||
GC design) schedules a proper in-flight fixture for this shape once the
|
||||
runtime has a way to express an external root without going through
|
||||
`main`'s return. `gc/abandoned-cycle` and `gc/budget-steps` are
|
||||
unaffected — neither depends on an externally-held root.
|
||||
|
||||
**Why a `multi` field, not a plain `@gc`-typed field, closes the cycle:**
|
||||
milestone-1 has no nil literal and a constructor literal requires every
|
||||
field, so two classes that mandatorily reference each other can never
|
||||
be built — whichever is constructed first needs an instance of the
|
||||
other that does not exist yet. A `multi` field sidesteps this: it starts
|
||||
empty (`multi_new()`), so both objects can be constructed *before*
|
||||
either references the other, and `push` closes the cycle afterward. This
|
||||
is also why fixture classes carry ~130 `Int` filler fields alongside the
|
||||
one `multi` field that matters — an object under 1024 bytes
|
||||
(`WO_ARENA_MAX_CLASS`, `runtime/src/obj.h`) allocates through the arena's
|
||||
own bump/freelist, where a sanitizer can never observe its free; over
|
||||
that size, `wo_arena_alloc` routes to plain `malloc`, which is what lets
|
||||
ASan prove the frees `runtime/test/test_cycle.c` already relies on the
|
||||
same way (`BIG = 130`).
|
||||
|
||||
## Why exact-match, not substring or "any failure"
|
||||
|
||||
A fixture that merely checks "did *something* go wrong" degrades silently
|
||||
the day the front end starts failing for the *wrong* reason — the fixture
|
||||
stays green while the bug it was written for comes back under a different
|
||||
code path. Naming the exact code (`WO-E###` or trap `N`) means a
|
||||
regression that changes *which* diagnostic fires is caught exactly as
|
||||
reliably as one that stops firing at all.
|
||||
|
||||
## Running the harness
|
||||
|
||||
```sh
|
||||
just woc-build # builds compiler/_build/default/bin/woc
|
||||
make -C runtime wovm # builds runtime/wovm
|
||||
just oop-e2e # walks the corpus, one line per fixture, a final tally
|
||||
|
||||
make -C runtime wovm-asan # optional: builds runtime/build/wovm_asan, for
|
||||
# manually re-running gc/ fixtures under ASan+UBSan
|
||||
```
|
||||
|
||||
`just oop-e2e` fails loudly and names the missing binary (and the command
|
||||
to build it) if either prerequisite above hasn't been built — it does not
|
||||
build them for you. `wovm-asan` is not one of those prerequisites: the
|
||||
automated harness runs every kind, `gc/` included, against the plain
|
||||
`wovm`; the sanitizer build is a manual supplementary check (see `gc/`
|
||||
above).
|
||||
136
docs/plan/oop-vm/08-builtin-surface.md
Normal file
136
docs/plan/oop-vm/08-builtin-surface.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Milestone-1 source surface the emitter lowers — normative reference
|
||||
|
||||
> What a `.wo` program may say and have `woc` produce bytecode for.
|
||||
> The `.wob` format doc ([`00-wob-format.md`](00-wob-format.md)) names the
|
||||
> BUILTIN *ids*; this names their **source spellings** and the handful of
|
||||
> rules that have no other home. Landed with the emitter
|
||||
> (`compiler/src/emit.ml`, plan 3 task 1). Diagnostics referenced here are
|
||||
> catalogued in [`01-error-catalog.md`](01-error-catalog.md).
|
||||
>
|
||||
> Anything on this page is a contract for corpus fixtures and for every
|
||||
> later sub-project's `.wo` code — not an emitter implementation detail.
|
||||
|
||||
## Builtins
|
||||
|
||||
Containers and runtime services are free functions, never methods. Each
|
||||
maps to one `BUILTIN` id of the format doc.
|
||||
|
||||
| source | `.wob` builtin | arity | meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `now()` | `now` | 0 | wall-clock milliseconds (`Int`) |
|
||||
| `print(t)` | `print` | 1 | a `Text`, newline-terminated |
|
||||
| `print_int(n)` | `print_int` | 1 | an `Int`, newline-terminated |
|
||||
| `words(t)` | `words` | 1 | whitespace token count of a `Text` |
|
||||
| `multi_new()` | `multi_new` | 0 | a fresh `multi T` — see the destination rule below |
|
||||
| `map_new()` | `map_new` | 0 | a fresh `map<K, V>` — see the destination rule below |
|
||||
| `push(m, v)` | `multi_push` | 2 | append to a `multi` |
|
||||
| `count(c)` | `count` | 1 | length of a `multi` or a `map` |
|
||||
| `latest(m)` | `latest` | 1 | last element of a `multi` (traps `BOUNDS` when empty) |
|
||||
| `get(c, k)` | `multi_get` / `map_get` | 2 | element by index, or value by key (a missing key traps `KEY`) |
|
||||
| `set(m, k, v)` | `map_set` | 3 | insert or replace in a `map` |
|
||||
| `has(m, k)` | `map_has` | 2 | `1`/`0` |
|
||||
|
||||
`get`, `set`, `push`, `count` and `has` resolve on the container they are
|
||||
given, so one source name covers the `multi` and `map` ids the runtime
|
||||
keeps apart.
|
||||
|
||||
**Sugar.** `c[i]` is exactly `get(c, i)` and `m[k] = v` is exactly
|
||||
`set(m, k, v)`. There is no element *write* into a `multi` — v1 has
|
||||
`multi_push` and `multi_get` and no element store — so `m[i] = v` on a
|
||||
`multi` is `WO-E403`.
|
||||
|
||||
**Shadowing.** A user-declared free `fn` of the same name always wins. A
|
||||
declared name is never silently replaced by a builtin.
|
||||
|
||||
**`push` and `@gc` elements.** `push(m, v)`'s value argument is never a
|
||||
resolved callee parameter (`push` has no declared signature), so the
|
||||
owner pass's ordinary Take-gated transfer never reaches it; a `@gc` value
|
||||
pushed into a `multi` is special-cased in `owner.ml`'s `analyze_call`
|
||||
(the value escapes into the container exactly like a ctor field, RC_INC
|
||||
included) specifically so a `multi`-mediated `@gc` cycle can be built
|
||||
and later collected (`tests/corpus/gc/`, plan 3 task 5). **`set(m, k, v)`
|
||||
has no equivalent special case** — a `@gc` key or value handed to `set`
|
||||
is not retained, so a `map<_, SomeGcClass>` (or a `@gc`-keyed map) built
|
||||
this way will under-count its element's refcount and the collector will
|
||||
free it while the map still points at it. Nothing in the corpus
|
||||
exercises this yet; treat it as an open gap, not a proven-safe pattern,
|
||||
until `set` gets the same fix `push` did.
|
||||
|
||||
## A fresh container needs a destination of declared type
|
||||
|
||||
`multi_new()` and `map_new()` carry their element (and key/value) kinds as
|
||||
an instruction immediate, and those kinds **are** the container's drop
|
||||
plan at runtime (`runtime/src/gc.c`). They cannot be guessed: assuming
|
||||
`SCALAR` for a `multi Item` leaks every element, and for a
|
||||
`map<Text, _>` leaks every key. Milestone-1 `let` has no container type
|
||||
annotation — its optional annotation is a bare identifier — so a fresh
|
||||
container must be created where its type is declared:
|
||||
|
||||
```wo
|
||||
class Store {
|
||||
items: multi Item
|
||||
by_name: map<Text, Int>
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let s = Store { items: multi_new(), by_name: map_new() } -- kinds from the fields
|
||||
push(s.items, Item { n: 7 })
|
||||
set(s.by_name, "seven", 7)
|
||||
}
|
||||
```
|
||||
|
||||
A bare `let m = map_new()` is `WO-E403`, reported at the creation site.
|
||||
The same rule applies to a `take`/`mut` parameter of declared container
|
||||
type, which is also a typed destination.
|
||||
|
||||
## Calls
|
||||
|
||||
- Argument count must match the callee's parameter count (`WO-E403`).
|
||||
Nothing upstream checks arity — `types.ml` declares `WO-E203` and never
|
||||
raises it — and a mismatched call reserves a register window the callee
|
||||
does not read, which the loader rejects outright.
|
||||
- A method is called as `receiver.method(args)`. When the receiver's
|
||||
declared type is an **interface**, the call is dispatched by vtable
|
||||
(`ICALL`); satisfaction is structural (same method name, same parameter
|
||||
count), Go-style, with no `implements` keyword.
|
||||
- `self` occupies the callee's `r0`, so a method's argument count is
|
||||
`1 + parameters`.
|
||||
|
||||
## Program entry
|
||||
|
||||
The entry point is the **zero-argument free `fn main`**. A `main` that
|
||||
takes parameters is not an entry (the format's own rule is a zero-argument
|
||||
free fn), and `wovm` will report `module has no entry method`.
|
||||
|
||||
## Operators with no dedicated opcode
|
||||
|
||||
Lowered by the emitter, not added to the format:
|
||||
|
||||
| source | lowering |
|
||||
| --- | --- |
|
||||
| `a % b` | `a - (a / b) * b` — exact for the VM's truncating `DIV`, which traps on `0` and on `INT64_MIN / -1`, both correct for `%` too |
|
||||
| `a != b` | `(a == b) == 0` |
|
||||
| `a > b`, `a >= b` | `LT` / `LE` with the operands swapped |
|
||||
| `a == b` on `Text` | `EQS` (content equality); `EQ` otherwise |
|
||||
| `a .. b` | `CONCAT` — `+` is arithmetic only, never string addition |
|
||||
|
||||
## Not lowerable in milestone 1
|
||||
|
||||
Each is `WO-E403` at the offending site, never invented bytecode:
|
||||
|
||||
- an element write into a `multi` (no element-store instruction);
|
||||
- `for` over a `map` (v1 exposes no key enumeration);
|
||||
- a name that is neither a local, a parameter, `self`, a declared `fn`,
|
||||
nor a builtin;
|
||||
- a field or method on a type that is not a declared class — including a
|
||||
class named only inside `multi T` / `ref T`, which `types.ml`'s
|
||||
unknown-type check (`WO-E225`) does not look inside.
|
||||
|
||||
## `?T`
|
||||
|
||||
A nullable field stores exactly what `T` stores and spells nil as `0`.
|
||||
The v1 format has no kind byte for it (field kinds run `0..5`; the loader
|
||||
rejects `6`), and it needs none: every per-kind drop plan already ignores
|
||||
a zero slot. `?T`'s field kind is therefore `T`'s. Note the consequence
|
||||
for `@gc`: `?SomeGcClass` is a `GCREF` field like any other, so it
|
||||
participates in refcounting and cycle detection normally.
|
||||
|
|
@ -12,5 +12,6 @@ The normative contract documents both stacks cite. Landed by their named plan ta
|
|||
| `05-http-service.md` | route section, trap→HTTP table, JSON subset | 6 |
|
||||
| `06-ui-live.md` | delta frames, subscribe protocol, wo:live | 7 |
|
||||
| `07-systems-stdlib.md` | per-function nil-vs-trap contracts | 9 |
|
||||
| `08-builtin-surface.md` | builtin source names, container/call/entry rules the emitter enforces | 3 |
|
||||
|
||||
Specs and plans: `docs/superpowers/{specs,plans}/`. Repo map: `docs/08-project-structure.md`.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
**AS** a developer building and operating my own products end to end
|
||||
|
||||
**I WANT** a new programming language — with arithmetic, ownership-based memory safety, and garbage collection where I opt in — whose compiler, runtime, and database ship as a single never-stopping Linux binary that can update its own code in place
|
||||
**I WANT** a new programming language — with arithmetic, ownership-based memory safety, and garbage collection applied automatically wherever ownership alone cannot express the shape — whose compiler, runtime, and database ship as a single never-stopping Linux binary that can update its own code in place
|
||||
|
||||
**TO** write an application once and run it forever: no external stack to assemble, no database server to operate, and deployments that swap code inside the running process with instant rollback.
|
||||
|
||||
|
|
@ -15,8 +15,10 @@
|
|||
serves the API, and carries its own source — the "which commit is prod
|
||||
running?" class of questions disappears.
|
||||
- Memory safety without a GC tax: Rust-shaped borrowing (single owner,
|
||||
second-class borrows) checked mostly at compile time, with per-class `@gc`
|
||||
opt-in collected per shard — no global pause exists by construction.
|
||||
second-class borrows) checked mostly at compile time, and where ownership
|
||||
cannot express the shape the compiler decides — no annotation to write, and
|
||||
collection stays per-shard so no global pause exists by construction
|
||||
(iteration 7b; iterations 1–7 shipped a per-class `@gc` opt-in instead).
|
||||
- Updates are blue-green **inside** the runtime: propose, approve, compile
|
||||
in-process, atomic switch, previous version resident for instant rollback.
|
||||
- The runtime is a recipe box: once language + runtime + database exist, a
|
||||
|
|
@ -47,8 +49,10 @@ iterations); no commits by agents — drafts go to `.dev/commit.md`.
|
|||
| 5 | [Language surface](05-language-surface.md) | Haxe-parity adoptions: switch, records, optionals, try/catch, statics, modules… |
|
||||
| 6 | [Program mode + stdlib](06-program-mode-stdlib.md) | `fn main`, exit codes, `fs`/`proc`/`net`/`time`/`json` builtins |
|
||||
| 7 | [log-watcher proof](07-logwatcher-proof.md) | the driving workload compiled and detecting silent deaths live |
|
||||
| 7b | [Inferred GC + mark-sweep](07b-inferred-gc-mark-sweep.md) | `@gc` removed from the language; compiler infers GC-ness; RC replaced by incremental per-shard tri-color mark-sweep |
|
||||
| 8 | [Shard-actor runtime](08-shard-actor-runtime.md) | thread-per-core shards, per-shard heaps, ownership-move messaging |
|
||||
| 9 | [Database engine](09-database-engine.md) | class-shaped tables, typed WAL + recovery, `insert`/`select` execute |
|
||||
| 9b | [`@table`, relations, query](09b-table-relations-query.md) | `@table` becomes real storage; typed `ref`/`backlink`/`multi` relations; compiler-checked LINQ-shaped queries lowered to engine ops |
|
||||
| 10 | [HTTP service layer](10-http-service.md) | `service` blocks route to VM methods; REST parity with Stage 2 |
|
||||
| 11 | [Fibers](11-fibers.md) | green threads on the shard scheduler: reduction-budget preemption, park on I/O |
|
||||
| 12 | [Blue-green deploy](12-blue-green-deploy.md) | two VM slots, in-runtime compile, atomic switch, resident rollback |
|
||||
|
|
@ -74,11 +78,17 @@ list, and a pointer to the plan document that already sequences its tasks.
|
|||
outcome.
|
||||
- **Critical path (locked 2026-08-08): compile and run log-watcher.**
|
||||
Iterations 3 → 4 → 5 → 6 → 7 are the committed line; nothing off that
|
||||
line lands before iteration 7's acceptance. Iterations 8–11 follow.
|
||||
line lands before iteration 7's acceptance. Then 7b, then 8–12 (with 9b after the database engine).
|
||||
- **Iteration 7b (inserted 2026-08-11)** sits after the critical path
|
||||
deliberately: it delays nothing on the log-watcher line, and it must precede
|
||||
iteration 8 because the collector should be settled before shards multiply.
|
||||
It also closes iteration 4's one open gate clause and supersedes part of
|
||||
iteration 2's memory model — neither is renumbered; both point here.
|
||||
- **Future iterations, after iteration 11** (parked 2026-08-08 — recorded,
|
||||
not scheduled):
|
||||
- WO-W201 `@gc`-suggestion diagnostic refinement (self-reference-only
|
||||
heuristic shipped; shared-structure analysis deferred).
|
||||
- ~~WO-W201 `@gc`-suggestion diagnostic refinement~~ — **superseded by
|
||||
iteration 7b**: the diagnostic is retired outright, because inference
|
||||
replaces the suggestion it existed to make.
|
||||
- WO-E225 unknown-type validation broadened to `ref`/`multi`/`map`
|
||||
element types and method/fn signatures.
|
||||
- ADT container roster adoption (Stack, Queue, Set, Tree, Graph, … —
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
# Iteration 7b — inferred GC + incremental mark-sweep
|
||||
|
||||
> Format: fiberloom `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
>
|
||||
> **Inserted 2026-08-11**, after the plan was first drawn — hence `7b` rather
|
||||
> than a renumber. It sits here because the log-watcher critical path
|
||||
> (iterations 3–7) must not be delayed, and because the collector should be
|
||||
> settled before iteration 8 multiplies shards.
|
||||
|
||||
## Goals
|
||||
|
||||
- **The developer stops deciding which types are garbage collected.** `@gc`
|
||||
disappears from the language; the compiler infers GC-ness and reports every
|
||||
decision with its reason.
|
||||
- Reference counting is replaced by an incremental per-shard tri-color
|
||||
mark-sweep collector, so the compiler no longer has to emit a balanced
|
||||
acquire/release at every alias site — the source of every recorded `@gc`
|
||||
defect.
|
||||
- Milestone 1's acceptance criterion 3 (ASan-clean across the corpus) closes,
|
||||
because the leak blocking it is one of the defects this deletes.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- What to achieve?
|
||||
- **Given** a class whose declaration can form a reference cycle, and a
|
||||
separate class that is only ever shared through a long-lived alias,
|
||||
- **when** the program is compiled,
|
||||
- **then** both are classified GC-managed without any annotation, and
|
||||
`--dump-gc` names the reason for each — a cycle path for the first, the
|
||||
escaping alias site for the second.
|
||||
- What to achieve?
|
||||
- **Given** any `.wo` source containing `@gc`,
|
||||
- **when** it is compiled,
|
||||
- **then** it is a diagnostic pointing at inference and `--dump-gc`, not a
|
||||
silently accepted no-op.
|
||||
- What to achieve?
|
||||
- **Given** a program that hides a traced object from the collector —
|
||||
storing it into an already-blackened object between marking slices and
|
||||
dropping the original reference,
|
||||
- **when** the collector completes,
|
||||
- **then** the object is still alive; and the same fixture fails loudly if
|
||||
the write barrier is compiled out.
|
||||
- What to achieve?
|
||||
- **Given** an abandoned cycle and a cycle still rooted from a live frame,
|
||||
- **when** collection runs,
|
||||
- **then** the abandoned one is freed within budgeted slices with no slice
|
||||
exceeding the configured budget, and the rooted one survives.
|
||||
- What to achieve?
|
||||
- **Given** the whole conformance corpus, run repeatedly so several
|
||||
collection cycles occur,
|
||||
- **when** it runs under ASan,
|
||||
- **then** zero leaks and zero errors — the clause that currently fails.
|
||||
- What to achieve?
|
||||
- **Given** any emitted `.wob` image,
|
||||
- **when** it is disassembled,
|
||||
- **then** no `RC_INC` or `RC_DEC` appears, and the format doc records
|
||||
opcodes 27–28 as reserved behind a version bump.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Cross-shard tracing — ownership moves mean no traced object spans shards.
|
||||
- Generational collection and compaction. Non-moving is load-bearing: no
|
||||
forwarding pointers, no read barrier. Go's collector is not generational
|
||||
either.
|
||||
- Scheduler-integrated pacing beyond the heap-goal trigger; that stays
|
||||
iteration 8's concern, which is part of why this lands first.
|
||||
- `ref T` semantics, unchanged — it is an id, not a pointer, and creates no
|
||||
edge in the inference graph.
|
||||
|
||||
## Info
|
||||
|
||||
- Governing spec: [`docs/superpowers/specs/2026-08-11-inferred-gc-mark-sweep-design.md`](../../superpowers/specs/2026-08-11-inferred-gc-mark-sweep-design.md).
|
||||
- **Why the annotation was insufficient, not merely inconvenient:** the OOP
|
||||
spec's own example, `@gc class PriceCache { entries: map<SKU, Money> }`, is
|
||||
acyclic. It needs GC because it is shared, and second-class borrows cannot
|
||||
be stored or returned. So the developer was being asked to reason about type
|
||||
shape *and* whole-program aliasing at once — hence the hybrid rule
|
||||
(structural SCC plus reported demand promotion).
|
||||
- **Why tracing rather than better reference counting:** all four recorded
|
||||
`@gc` defects are RC bookkeeping failures — `push` missing an increment,
|
||||
`set` still missing one, the `mut`-`@gc` clobber, and the held-cycle leak.
|
||||
Inferring GC-ness would widen that population and so widen that bug class.
|
||||
Tracing emits no per-alias bookkeeping at all.
|
||||
- **Most of what tracing needs already exists.** The emitter already produces
|
||||
precise per-pc pointer masks (the drop table's gc mask) and the class table
|
||||
already carries per-field kinds — the two pieces Go gets from stack maps and
|
||||
type maps. Go's dependence on OS threads is incidental; the algorithm needs
|
||||
only per-frame PC→map lookup and the ability to suspend one stack.
|
||||
- **The one real runtime addition:** the arena cannot enumerate objects — bump
|
||||
allocation plus size-class free lists, with large objects on bare `malloc`
|
||||
and no size headers anywhere. Sweep needs its own list. Retiring `rc`, plus
|
||||
the `borrow` word that traced objects never use, frees exactly eight
|
||||
contiguous bytes for an intrusive link, so the 16-byte header survives.
|
||||
- This iteration **supersedes part of iteration 2's memory model** (§4 of the
|
||||
OOP spec) and closes iteration 4's open gate clause. Neither is renumbered;
|
||||
both carry pointers here.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
- Write the implementation plan from the approved spec, then execute it: the
|
||||
`gcinfer.ml` pass (Tarjan SCC over the class-reference graph, then demand
|
||||
promotion to a fixpoint, with a note per decision), the `@gc` removal and
|
||||
its diagnostic, retiring `RC_INC`/`RC_DEC` and the rc machinery from
|
||||
`owner.ml`/`emit.ml`, the per-shard traced list and sweep, incremental
|
||||
tri-color marking with roots read from the existing pc masks, the Yuasa
|
||||
deletion barrier inside the VM's store paths, and the doc/golden migration
|
||||
the spec's §8 table enumerates.
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# Iteration 9b — `@table`, relations, and language-integrated query
|
||||
|
||||
> Format: fiberloom `product/story-iteration-template`. Part of
|
||||
> [Story — one language, one runtime, one database, one binary](00-story.md).
|
||||
>
|
||||
> **Inserted 2026-08-11**, hence `9b` rather than a renumber. It follows
|
||||
> iteration 9 because a query surface needs tables that actually execute, and
|
||||
> precedes iteration 10 because `service` blocks will want to return query
|
||||
> results.
|
||||
>
|
||||
> **No spec exists yet.** This iteration frames the outcome and records the
|
||||
> open questions; the design must be brainstormed before a plan is written.
|
||||
> The three questions in *Info* are genuine forks, not details.
|
||||
|
||||
## Goals
|
||||
|
||||
- `@table` graduates from a parsed-but-inert annotation into the declaration
|
||||
that makes a class persistent: named storage, declared indexes, and a
|
||||
primary identity.
|
||||
- Relations become first-class and typed — `ref T` foreign keys, `backlink`
|
||||
inverses, and `multi` collections — so a developer navigates their data by
|
||||
following fields rather than by hand-writing joins.
|
||||
- Queries are **written in the language, checked by the compiler**: a
|
||||
LINQ-shaped operator vocabulary (filter, project, join, group, order,
|
||||
aggregate) over tables and relations, with the result's type inferred and
|
||||
every column reference resolved at compile time. A typo in a field name is
|
||||
a compile error, not a runtime one.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- What to achieve?
|
||||
- **Given** a class annotated `@table` with a declared index,
|
||||
- **when** the program is compiled and run,
|
||||
- **then** its instances persist through the engine, the index is built,
|
||||
and a query that could use the index does use it — demonstrated, not
|
||||
assumed.
|
||||
- What to achieve?
|
||||
- **Given** two classes related by `ref` with a `backlink` inverse,
|
||||
- **when** a query navigates the relation in either direction,
|
||||
- **then** it typechecks with the related class's field set in scope, and
|
||||
navigating a field that does not exist is a compile error naming it.
|
||||
- What to achieve?
|
||||
- **Given** a query whose result shape is a projection rather than a whole
|
||||
row,
|
||||
- **when** it is assigned or returned,
|
||||
- **then** its type is the projected shape — so a later use of a column
|
||||
the projection dropped is a compile error.
|
||||
- What to achieve?
|
||||
- **Given** a query written against tables,
|
||||
- **when** the compiler lowers it,
|
||||
- **then** it becomes engine operations, **not** a string handed to a
|
||||
parser at runtime — provable by disassembly, and by the absence of any
|
||||
SQL-text construction in the emitted image.
|
||||
- What to achieve?
|
||||
- **Given** the ecommerce sample's existing relational shapes
|
||||
(`Order.user: ref User`, `User.orders: backlink Order.user`, line-item
|
||||
collections),
|
||||
- **when** they are expressed as queries in this surface,
|
||||
- **then** each produces the same results as the equivalent hand-written
|
||||
query, and the sample's README records anything that could not be
|
||||
expressed.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Cross-shard queries and distributed joins — iteration 8 owns ownership
|
||||
movement, and a query spanning shards is a 2PC concern recorded with the
|
||||
database track.
|
||||
- `LIVE` subscriptions over queries. The subscription registry is the
|
||||
HTTP/UI track's; a query that pushes updates is a later composition of the
|
||||
two.
|
||||
- Migrations. Changing a `@table` class's shape is the blue-green spec's
|
||||
additive-only differ (iteration 12), not this iteration's problem.
|
||||
- Query optimisation beyond index selection. A cost-based planner is a
|
||||
separate, much later concern; this iteration must only prove that declared
|
||||
indexes are used.
|
||||
|
||||
## Info
|
||||
|
||||
Three open forks the spec has to settle. Each is a real decision, and I have a
|
||||
leaning on all three but no mandate.
|
||||
|
||||
**1. Where does this leave the existing SQL + Cypher query layer?**
|
||||
`docs/runtime/database/02-wo-language.md` specifies a two-layer design — a
|
||||
schema layer plus a query layer of literal SQL and Cypher with five
|
||||
"fixed-glue" rules. A language-integrated surface either replaces that layer,
|
||||
sits beside it, or becomes the only surface with SQL retained purely as an
|
||||
export format. Replacing it is the coherent choice and also the most
|
||||
disruptive, because that document is normative and the `wo-db` C++ prototype
|
||||
implements the SQL/Cypher grammar it describes.
|
||||
|
||||
**2. There are no function values, so what is the syntax?**
|
||||
LINQ-to-Objects is built on delegates: `.Where(x => x.Age > 18)` passes a
|
||||
lambda. writeonce has **no function-value type**, and the systems-track spec
|
||||
deliberately rejected closure builtins (`map`/`filter`/`reduce`) for exactly
|
||||
this reason. So the surface cannot be method-chaining-with-lambdas as written
|
||||
in C#. The realistic options are a comprehension syntax the compiler desugars
|
||||
(`from o in orders where o.total > 100 select o.id`), or method chaining whose
|
||||
"lambda" argument is a compiler-recognised expression form rather than a
|
||||
value. Either way the predicate is **compile-time syntax, never a runtime
|
||||
closure** — which is also what lets the whole query lower to engine ops.
|
||||
|
||||
**3. What does the reference actually contribute?**
|
||||
`.dev/reference/dotnet-runtime/src/libraries/System.Linq/src/System/Linq/`
|
||||
(sparse checkout, added with this iteration) is the operator catalogue: read
|
||||
`Where.cs`, `Select.cs`, `Join.cs`, `GroupBy.cs`, `OrderBy.cs` for what each
|
||||
operator means and which edge cases it has to answer, and the `*.SpeedOpt.cs`
|
||||
files for how LINQ specialises when the source's shape is known statically —
|
||||
directly relevant, since writeonce knows every shape statically. What does
|
||||
**not** transfer is the machinery: `IEnumerable` iterator composition,
|
||||
delegates, and `IQueryable`'s runtime expression trees, the last of which
|
||||
depends on reflection that principle 13 forbids outright. Take the vocabulary
|
||||
and the semantics; leave the plumbing.
|
||||
|
||||
Also relevant: `@table(name:, index:)` already parses today with known-key
|
||||
validation (`WO-E102`), the Rust runtime already ships secondary indexes and
|
||||
`find_by` behind that annotation, and `ref T` already classifies as a scalar
|
||||
id rather than a pointer — so the relational vocabulary partly exists and this
|
||||
iteration makes it mean something in the C stack.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
- **Brainstorm a spec first**, settling the three forks above; only then write
|
||||
the plan. This iteration deliberately ships no plan pointer, because
|
||||
choosing between "replace the SQL layer" and "sit beside it" changes what
|
||||
the plan contains.
|
||||
- Study `.dev/reference/dotnet-runtime`'s `System.Linq` operator set for the
|
||||
vocabulary, and `docs/runtime/database/02-wo-language.md` plus
|
||||
`prototypes/wo-db/` for the semantics already committed to.
|
||||
- Expect the work to span the front end (query syntax, relation typing,
|
||||
projection types), the emitter (lowering to engine operations rather than
|
||||
text), and the engine (index selection, relation traversal) — which is why
|
||||
it follows iteration 9 rather than preceding it.
|
||||
|
|
@ -230,3 +230,35 @@ Milestone 1 is done when:
|
|||
3. The ownership corpus passes: every must-fail program fails with the expected `WO-E###`; every must-trap program traps with the expected code; ASan/Valgrind report zero leaks and zero errors across the suite.
|
||||
4. `@gc` cycle test: a cyclic `@gc` graph is collected within budgeted ticks with no pause longer than the configured slice.
|
||||
5. `woc build` produces a single self-contained binary that runs with no arguments.
|
||||
|
||||
## Milestone 1 acceptance — 2026-08-11
|
||||
|
||||
Gate: `just oop-accept` (plan 3, Task 8), run for real against this working tree. Full output archived in `.superpowers/sdd/2026-08-01-wob-emit-e2e-single-binary/task-8-report.md`. Per-criterion result:
|
||||
|
||||
- [x] **1. Compile time.** `woc --emit` over the pricing-demo logic subset (`tests/corpus/{run/pricing-containers,run/pricing-current-price,run/pricing-discounted,run/pricing-text,trap/pricing-set-price-db-stub}/fixture.wo` — the five fixtures Task 3 derived from `docs/examples/pricing/`; the demo's original `.wo` files use surface milestone 1 doesn't have, so these are milestone 1's "logic subset" in fact, not the directory named in the spec's prose), measured over 20 runs: min 10.8 ms, avg 13.1–13.8 ms, max 16.9–17.6 ms. Worst observed run is under a fifth of the 100 ms budget. **MET.**
|
||||
- [x] **2. Pricing output.** All four `run/pricing-*` fixtures compile and run under `wovm`, stdout byte-exact against `fixture.out`, under both the release binary and `runtime/build/wovm_asan`. **MET.**
|
||||
- [ ] **3. Ownership corpus + ASan/Valgrind zero leaks.** The error-code half is fully met: all 7 `compile-fail/` fixtures fail with exactly their named `WO-E###`, all 5 `trap/` fixtures trap with exactly their named code. The ASan half is **not met**: running the full corpus (`run/`, `compile-fail/`, `trap/`, `gc/`, single-binary smoke — 26 checks) against `runtime/build/wovm_asan` (`make -C runtime wovm-asan`, Task 5's target), `gc/held-cycle` fails — LeakSanitizer reports a definite leak (1184 bytes / 3 allocations, `wo_multi_push`/`wo_obj_new` via `main.c:160`'s `wo_vm_call`) instead of exit 0. This is not a false positive to suppress: `runtime/src/main.c`'s entry-method return value (`uint64_t ret`, line 158) is stored and never used again, so the "permanent external hold" the fixture's own comment claims is not actually realized in the C driver — nothing keeps that pointer live for a precise scanner to find. The other 25 checks, including the other two `gc/` fixtures, are ASan-clean. **NOT MET** — tracked in `docs/00-status.md`'s "Known gaps carried out of iteration 4" and the SDD ledger; a `runtime/src/main.c` fix, out of scope for the task that found it.
|
||||
- [x] **4. `@gc` cycle collection.** `gc/abandoned-cycle` (unreachable 2-cycle, collected step 1, `freed=2`) and `gc/budget-steps` (budget-sliced collection, `freed=4`) both pass byte-exact stdout plus exact `WO_GC_TRACE` step/freed counts, clean under ASan. `gc/held-cycle` demonstrates the complementary correctness property — an externally-held cycle is correctly *not* collected (`freed=0`, matching `fixture.trace`) — which is the GC decision criterion 4 asks about; its ASan failure is a criterion-3 (leak-detector) concern, not a collection-correctness one. **MET.**
|
||||
- [x] **5. Single-binary.** `scripts/single-binary-smoke.sh` against the release `runtime/wovm`: `woc build` produces an executable; copied to a directory outside the repo and run with no arguments, stdout is byte-exact; a corrupted trailer fails clearly on exit 2 (never a crash or hang). All 3 checks pass. **MET.**
|
||||
|
||||
**4 of 5 criteria met; criterion 3 is not**, specifically its ASan-zero-leaks clause. `just oop-accept` fails loudly at that stage and does not proceed to the remaining stages (single-binary smoke, both unit gates) in the same run by design — those were verified to pass independently (see the Task 8 report) but are gated behind fixing this finding in a real `oop-accept` run.
|
||||
|
||||
### Update — 2026-08-11: criterion 3 closed, all five criteria MET
|
||||
|
||||
Root cause was already pinned above and did not change on inspection: `gc/held-cycle`'s "permanent external hold" was `runtime/src/main.c`'s entry-return value never being released — a genuine refcount leak, not a false positive. Adding release-on-return to `main.c` was rejected as the fix: the `.wob` method table carries no return-type/kind metadata, so the driver has no way to tell a pointer return from a scalar one, and adding that metadata is a format change out of scope for this closure.
|
||||
|
||||
Fixed at the source instead: this milestone's own spec (Section 4/Success criteria; the systems-track spec, `2026-08-01-systems-track-design.md:70`) makes the program entry's return value the process exit code, so an entry declaring a class return type was never legal — it just went unchecked. `compiler/src/emit.ml` now raises `WO-E405` for a free-fn entry (`main`, zero args) whose declared return type is anything but `Int`; a `main` with no return annotation is unaffected. Catalog entry in `01-error-catalog.md`; a `compiler/test/runner.ml` case (`entry return type: ...`, 4 checks) pins both the positive (fires on `-> Widget`) and negative (silent on no annotation) cases so it cannot regress silently.
|
||||
|
||||
With the entry contract enforced at compile time, `gc/held-cycle`'s premise — an externally-held cycle surviving a *post-exit* pump — is no longer expressible: nothing can hold a root past the point `main` returns, by construction. The fixture is retired (`oop-vm/02-corpus.md`, "Retired" note has the full reasoning); the scenario it meant to demonstrate remains covered, just one layer down, by `test_externally_held_cycle_survives_then_dies` in `runtime/test/test_cycle.c`, which holds its root the honest way (a real C local, not a leaked VM return value). This also means **criterion 4's evidence above is now partly stale**: `gc/held-cycle` no longer exists to "demonstrate the complementary correctness property" it's credited with — criterion 4 remains MET on the strength of `gc/abandoned-cycle` and `gc/budget-steps` alone (both untouched, both still ASan-clean), with the externally-held-cycle property now proven at the runtime-test layer instead of the corpus layer. Story iteration 7b (tracing GC) is scheduled to give the corpus a proper in-flight externally-held fixture once there is a root that doesn't route through `main`'s return.
|
||||
|
||||
Fresh `just oop-accept` run against this working tree, full output archived alongside this note in `.superpowers/sdd/2026-08-01-wob-emit-e2e-single-binary/m1-criterion3-closure-report.md`:
|
||||
|
||||
- [x] **1. Compile time.** 20 runs over the same 5-fixture pricing subset: min 6.464 ms, avg 6.825 ms, max 7.590 ms — under 8% of the 100 ms budget. **MET.**
|
||||
- [x] **2. Pricing output.** Unchanged from the Task 8 report; still byte-exact under both `wovm` and `wovm_asan`. **MET.**
|
||||
- [x] **3. Ownership corpus + ASan/Valgrind zero leaks.** Full corpus (`run/`×8, `compile-fail/`×7, `trap/`×5, `gc/`×2, single-binary-smoke×3 = 25 checks, one fewer than the Task 8 report's 26 because `gc/held-cycle` is retired) against `runtime/build/wovm_asan`: `oop-e2e: 25 checks, 0 failures`. Zero LeakSanitizer reports. **MET.**
|
||||
- [x] **4. `@gc` cycle collection.** `gc/abandoned-cycle` (`freed=2`) and `gc/budget-steps` (`freed=4`) both pass byte-exact stdout and exact `WO_GC_TRACE` counts, clean under ASan — see the note above on `gc/held-cycle`'s retirement and where its property now lives. **MET.**
|
||||
- [x] **5. Single-binary.** `scripts/single-binary-smoke.sh` against the release `runtime/wovm`: 3/3 checks pass. **MET.**
|
||||
|
||||
Both unit gates also ran green in the same `oop-accept` invocation: runtime (`make -C runtime test` + `test-iso` + `cli_smoke.sh`, 13 suites × 2 dispatch flavors, all ASan/UBSan-clean) and compiler (`dune runtest --root compiler`: 14 + 399 checks, up from 14 + 395 — the 4 new `WO-E405` cases). `oop-accept` printed `oop-accept: ALL CRITERIA MET`.
|
||||
|
||||
**5 of 5 criteria met.** Milestone 1's acceptance gate is fully green.
|
||||
|
|
|
|||
|
|
@ -120,18 +120,23 @@ gains an `env` row and the count becomes six.
|
|||
Both were asserted in the review doc; both are corrected at their real source
|
||||
rather than in the retired file.
|
||||
|
||||
**Structural interface satisfaction is not implemented — and is unreachable by
|
||||
design, not owed.** `WO-E205` is declared and never emitted. The review listed
|
||||
satisfaction checking as *implemented*, which is false; but calling it a gap in
|
||||
shipped work is equally wrong. Satisfaction is structural — there is no
|
||||
`implements` keyword by doctrine — so the check has exactly one home: sites
|
||||
where a value is used at an interface-typed position. The milestone grammar has
|
||||
no such positions (no interface-typed fields, parameters, or returns are
|
||||
exercised), so the check cannot fire yet and its absence costs nothing. The
|
||||
error catalog re-files `WO-E205` as **unreachable until interface-typed
|
||||
positions exist**, and `types.ml`'s module header stops claiming it produces a
|
||||
satisfaction set. The log-watcher sample declares no interfaces, so this stays
|
||||
off the critical path.
|
||||
**Structural interface satisfaction is not implemented.** `WO-E205` is declared
|
||||
and never emitted. The review listed satisfaction checking as *implemented*,
|
||||
which is false. This amendment originally also called it *unreachable by
|
||||
design, not owed* — that half is wrong, and corrected here (plan 3 Task 4
|
||||
review, 2026-08-11): the check's one legal home, a site where a value is used
|
||||
at an interface-typed position, **is** exercised by the milestone grammar. An
|
||||
interface-typed parameter accepting a concrete class that doesn't structurally
|
||||
satisfy it compiles clean today, then reaches `wovm` as an `ICALL` with no
|
||||
matching vtable slot, which traps `WO_T_BOUNDS` at runtime instead of failing
|
||||
to compile — even though the violation is statically provable (the class's
|
||||
method set is fully known). See
|
||||
[`01-error-catalog.md`](../../plan/oop-vm/01-error-catalog.md)'s "Reachable but
|
||||
unenforced" section for the repro and
|
||||
`tests/corpus/trap/unsatisfied-interface/` for the pinned current behavior.
|
||||
This is an owed gap, not a design decision — it stays off the log-watcher
|
||||
critical path only because the sample declares no interfaces, not because the
|
||||
gap doesn't exist.
|
||||
|
||||
**`?T` is plumbed, not enforced.** The review listed "Nullable types `?T`" as
|
||||
implemented — the same conflation already corrected in
|
||||
|
|
@ -160,7 +165,7 @@ against the sample.
|
|||
| [plan 9](../plans/2026-08-01-program-mode-stdlib.md) | Gains a core-builtins task covering the 22 bare globals plus `print_err`. `time` task gains `iso` and `local`, loses `mono`. `env` is named as a module rather than loose Part-2 prose. |
|
||||
| [plan 3](../../plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md) | Unchanged. |
|
||||
| [plan 10](../plans/2026-08-01-log-watcher-sample.md) | Acceptance gains the diagnostic-count gate: 307 → 0. |
|
||||
| [`01-error-catalog.md`](../../plan/oop-vm/01-error-catalog.md) | `WO-E205` re-filed as unreachable-by-design with its reason; `WO-E208`/`E210`/`E211`–`E213` keep their existing reserved entries. |
|
||||
| [`01-error-catalog.md`](../../plan/oop-vm/01-error-catalog.md) | `WO-E205` re-filed as **reachable but unenforced** — corrected 2026-08-11, see § 5 — with its repro; `WO-E208`/`E210`/`E211`–`E213` keep their existing reserved entries. |
|
||||
| [`docs/00-status.md`](../../00-status.md) | NEXT PLAN gains the milestone-grammar-only note; pending list gains the three cuts under the parked section. |
|
||||
| `docs/00-code-review.md` | Reduced to a stub: one paragraph saying its findings landed here and in the plans, pointing at `docs/00-status.md`. |
|
||||
|
||||
|
|
@ -191,8 +196,9 @@ uncaught-trap surface exactly as it is today.
|
|||
1. The systems-track spec's Part 1 has a boolean-operator row and its Part 3
|
||||
lists six modules plus a core-builtins section covering all 22 names.
|
||||
2. Plans 8 and 9 reflect every addition and cut; plan 8 Task 7 is gone.
|
||||
3. `WO-E205` is documented as unreachable-by-design, and `types.ml`'s header no
|
||||
longer claims a satisfaction set is produced.
|
||||
3. `WO-E205` is documented as reachable but unenforced (corrected 2026-08-11,
|
||||
see § 5 — an earlier "unreachable-by-design" claim here was wrong), and
|
||||
`types.ml`'s header no longer claims a satisfaction set is produced.
|
||||
4. `docs/00-code-review.md` is a stub; no second roadmap exists in the repo.
|
||||
5. The 307-diagnostic baseline is recorded in plan 10 as its acceptance gate.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
# Inferred GC + incremental mark-sweep — design spec
|
||||
|
||||
**Date:** 2026-08-11
|
||||
**Status:** approved design, pre-implementation
|
||||
**Scope:** removing `@gc` as a developer-facing annotation, inferring GC-ness in the
|
||||
compiler, and replacing reference counting with an incremental per-shard
|
||||
tri-color mark-sweep collector
|
||||
**Amends:** [`2026-08-01-oop-compiler-vm-design.md`](2026-08-01-oop-compiler-vm-design.md)
|
||||
(decision table's GC-granularity row, §3 rule 5, §4 memory model),
|
||||
[`../../00-principles.md`](../../00-principles.md) (principle 3),
|
||||
[`../../plan/oop-vm/00-wob-format.md`](../../plan/oop-vm/00-wob-format.md) (opcodes 27–28, drop-table
|
||||
contract, class flags), [`../../plan/oop-vm/01-error-catalog.md`](../../plan/oop-vm/01-error-catalog.md)
|
||||
(WO-W201, WO-E304)
|
||||
**Reference studied:** Go's collector, `.dev/reference/go/src/runtime/mgc.go` and neighbours
|
||||
|
||||
## Motivation
|
||||
|
||||
`@gc` asks the developer to answer a question the compiler is better placed to
|
||||
answer: does this type need tracing? Worse, the annotation is not even
|
||||
sufficient — the OOP spec's own example, `@gc class PriceCache { entries:
|
||||
map<SKU, Money> }`, is acyclic. It needs GC because it is a shared cache
|
||||
aliased from many places, and second-class borrows cannot be stored or
|
||||
returned, so a long-lived shared alias has nowhere else to live. So the
|
||||
developer is being asked to reason about two different things at once: the
|
||||
shape of their types *and* how those values will be aliased across the whole
|
||||
program.
|
||||
|
||||
The reference-counting implementation makes this worse rather than better.
|
||||
RC requires the compiler to emit a balanced acquire/release at **every** alias
|
||||
site, and every recorded `@gc` defect in this repo is exactly that failure:
|
||||
|
||||
| Defect | Cause |
|
||||
| --- | --- |
|
||||
| `push(multi, gcVal)` emitted no `RC_INC` | unresolved builtin skipped the transfer path — use-after-free |
|
||||
| `set(m, k, v)` still emits none (open) | same gap, never closed |
|
||||
| `mut`-`@gc` argument didn't clobber its root | stale rc elision — use-after-free |
|
||||
| `gc/held-cycle` leaks 1184 bytes | the entry return value's reference is never released, inflating rc forever |
|
||||
|
||||
Inferring GC-ness would *widen* that population and therefore widen that bug
|
||||
class. Tracing deletes the category outright: the compiler emits no per-alias
|
||||
bookkeeping at all.
|
||||
|
||||
And most of what tracing needs already exists. Go's collector depends on nine
|
||||
pieces of compiler metadata; the load-bearing ones are precise per-PC pointer
|
||||
maps and per-frame unwinding. The emitter already produces exactly that — the
|
||||
drop table carries an owned-register mask *and* a gc-register mask at every
|
||||
trap-capable pc, and the class table carries per-field kinds. Go's dependence
|
||||
on OS threads is incidental, not load-bearing: what the algorithm actually
|
||||
requires is the ability to suspend one stack and look up a PC→pointer map per
|
||||
frame, which a bytecode VM with an explicit value stack satisfies without
|
||||
native stacks. Non-moving matches the arena. The per-P coordination machinery
|
||||
(ragged barrier, write-barrier buffers, work stealing, assist credit) exists
|
||||
to run N mutators over one heap and is deletable in a per-shard design.
|
||||
|
||||
## Decisions locked during brainstorming
|
||||
|
||||
| Question | Decision |
|
||||
| --- | --- |
|
||||
| Scope | **One spec: infer *and* replace the collector.** Dropping `@gc` without replacing RC would widen the exact bug class that has already bitten four times. |
|
||||
| What decides GC-ness | **Hybrid: structural + reported promotion.** Cycles from a whole-program SCC over the class-reference graph; long-lived aliasing promotes on demand at the site that would otherwise error. Every promotion is reported. |
|
||||
| Collector cadence | **Incremental tri-color, budgeted slices, with a Yuasa deletion barrier.** Bounded pause regardless of heap size — which principle 3 already promises and the `budget-steps` fixture already asserts. |
|
||||
| Barrier scope | Pointer stores into traced objects only, and only while marking is active. Owned objects, scalars and text pay nothing. |
|
||||
| `@gc` in source | **Errors**, with a diagnostic pointing at inference. Accepting a now-meaningless annotation would be a lie surface. |
|
||||
|
||||
Rejected: structural-only inference (leaves `PriceCache`-shaped types still
|
||||
needing an annotation); demand-only inference (a distant edit silently flips a
|
||||
type's memory strategy with nothing reporting it); stop-the-shard full trace
|
||||
(no barrier, but the pause grows with the live set and principle 3's
|
||||
"budgeted" wording would have to weaken); post-exit-only collection
|
||||
(contradicts principle 6 — a service that never stops never collects);
|
||||
sequencing the collector before inference (defensible, and rejected only
|
||||
because the coupling argument above makes one coherent landing cheaper than
|
||||
two).
|
||||
|
||||
## 1. Inference — `compiler/src/gcinfer.ml`
|
||||
|
||||
A new pass between `types` and `owner`.
|
||||
|
||||
**Structural half.** Build a class-reference graph: an edge from A to B when A
|
||||
has a field whose type is B, `multi B`, `map<B, _>`, `map<_, B>`, or any of
|
||||
those under `?`. **`ref B` creates no edge** — it is an id, not a pointer, and
|
||||
already classifies as `Copy`. Run Tarjan's SCC. Every class in a non-trivial
|
||||
SCC, or with a self-loop, is traced. This is decidable from declarations
|
||||
alone, so it is stable under edits elsewhere in the program.
|
||||
|
||||
**Demand half.** Run the ownership analysis in a collect-promotions mode: at
|
||||
each site where it would report an escape or aliasing error (today's
|
||||
`WO-E304` and the long-lived-alias cases), record the class rather than the
|
||||
error. Promote all recorded classes, then re-run ownership. The promoted set
|
||||
only grows and is bounded by the class count, so this terminates; two owner
|
||||
passes is the worst case in practice because promotion removes errors and
|
||||
never creates them.
|
||||
|
||||
**Everything is reported.** Each promotion emits a note stating the reason —
|
||||
the cycle path for a structural promotion, the escape site for a demand
|
||||
promotion. `--dump-gc` renders the whole classification, which is the
|
||||
golden-testable artifact:
|
||||
|
||||
```
|
||||
Cache gc (alias escape, cache.wo:12)
|
||||
Node gc (cycle Node -> Node)
|
||||
Product owned
|
||||
Price owned
|
||||
```
|
||||
|
||||
**Field kinds follow.** A field whose type is a traced class derives
|
||||
`WO_K_GCREF` automatically; `types.ml`'s existing derivation reads the
|
||||
inferred set instead of `is_gc_class`.
|
||||
|
||||
**The annotation is removed.** The parser's `| "gc" -> is_gc := true` arm
|
||||
becomes a diagnostic in the WO-E1xx range naming the inference pass and
|
||||
`--dump-gc`. `class_info.is_gc` and `Types.is_gc_class` are replaced by the
|
||||
inferred set; `dump.ml` stops rendering ` @gc`.
|
||||
|
||||
## 2. What the compiler emits
|
||||
|
||||
- **`RC_INC` / `RC_DEC` are no longer emitted.** Opcodes 27–28 become
|
||||
reserved. This is a `.wob` version bump, recorded in the format doc.
|
||||
- **No barrier opcode and no emitter barrier.** `SETF` already resolves the
|
||||
field kind from the class table, so the barrier lives inside the VM's store
|
||||
paths (`SETF`, `map_set`, `push`). Zero new opcodes, zero emitter change for
|
||||
the barrier — a deliberate contrast with Go, which must insert barrier calls
|
||||
because it compiles to machine code.
|
||||
- **The drop table's gc mask keeps its bits and changes contract.** It stops
|
||||
meaning "`rc_dec` these registers while unwinding" and starts meaning
|
||||
"these registers are GC roots at this pc". The owned mask is unchanged, and
|
||||
`DROP` placement for owned values is unchanged.
|
||||
- **`wo_drop_kind` for `WO_K_GCREF` becomes a no-op** — tracing owns the
|
||||
lifetime of traced objects, so an owned object dying never frees them.
|
||||
- The class table's `@gc` bit survives with the same encoding; only its
|
||||
*source* changes from annotation to inference.
|
||||
|
||||
Everything else the emitter does — register allocation, window calls, moves,
|
||||
owned drops, residual borrow guards, line tables — is untouched. Inference
|
||||
changes which classes are traced, not how anything is lowered.
|
||||
|
||||
## 3. Runtime — header and the sweep list
|
||||
|
||||
**The arena cannot enumerate objects.** It is bump allocation plus 16-byte
|
||||
size-class free lists, with anything over 1024 bytes falling through to bare
|
||||
`malloc`; `wo_arena_free` requires the caller to pass the size back, and no
|
||||
size headers exist anywhere. Sweep therefore needs its own object list. This
|
||||
is the single largest runtime addition in this spec.
|
||||
|
||||
**The header stays exactly 16 bytes.** Retiring `rc` frees four bytes, and
|
||||
traced objects are exempt from borrow rules so their `borrow` word is dead
|
||||
too. Those two adjacent words give exactly eight contiguous bytes — one
|
||||
64-bit intrusive list link. The `_Static_assert(sizeof(wo_hdr) == 16)` and the
|
||||
format doc's layout size both survive unchanged.
|
||||
|
||||
**A per-shard traced list.** `wo_rt` gains a list head; every traced
|
||||
allocation links itself in. Sweep walks the list, recovers each object's size
|
||||
from the class table via `wo_obj_size`, frees the unmarked, and unlinks. The
|
||||
existing cycle-candidate buffer (`cycbuf`) and the `WO_F_BUF` flag are
|
||||
retired — they exist only to serve trial deletion.
|
||||
|
||||
## 4. Runtime — incremental marking and the barrier
|
||||
|
||||
**Colors.** The two existing color bits (`WO_F_COLOR`) carry white/grey/black.
|
||||
No header growth.
|
||||
|
||||
**Roots.** The VM's value stack and frame stack, read through the per-pc gc
|
||||
masks the emitter already emits — one mask lookup per live frame, exactly the
|
||||
mechanism Go gets from `FUNCDATA_LocalsPointerMaps` but already present here.
|
||||
|
||||
**Owned objects are traversed, never freed.** An owned object can hold a
|
||||
`GCREF` field, so tracing must walk through owned subtrees to find traced
|
||||
objects. To stop that costing the whole owned graph, the class table gains a
|
||||
precomputed **"transitively contains a gcref"** bit, so owned subtrees that
|
||||
cannot reach a traced object are skipped outright. This bit is derivable in
|
||||
the same pass that computes the SCCs.
|
||||
|
||||
**Safepoints** go at loop back-edges and calls — the pcs that already carry
|
||||
drop-table entries, so no new metadata is needed.
|
||||
|
||||
**Barrier.** Yuasa deletion barrier, active only while marking: on a pointer
|
||||
store into a traced slot, shade the *old* value before overwriting it. This is
|
||||
the half of Go's hybrid barrier that eliminates stack rescanning; the
|
||||
Dijkstra insertion half is unnecessary because a shard's own stack is
|
||||
re-read from its masks at each slice rather than being scanned once and
|
||||
trusted.
|
||||
|
||||
**Budget and trigger.** `WO_GC_BUDGET` keeps its name and meaning — objects
|
||||
marked per slice. The cycle starts on a heap goal over the shard's
|
||||
traced-bytes since the last cycle. `WO_GC_TRACE` keeps its stderr trace, with
|
||||
freed/marked counts per slice.
|
||||
|
||||
## 5. Error handling
|
||||
|
||||
No new error mechanism. The barrier and the collector cannot fail: allocation
|
||||
failure already traps `WO_T_OOM`, and a sweep-list allocation failure does not
|
||||
exist because the link is inside the object. Inference reports notes, never
|
||||
traps. The one genuinely new failure mode is a **barrier bug**, which
|
||||
manifests as silent corruption rather than a diagnostic — §7 addresses it with
|
||||
a dedicated adversarial test rather than a runtime check.
|
||||
|
||||
## 6. What this deletes
|
||||
|
||||
Recording this plainly, because it is the design's main argument:
|
||||
|
||||
- `gc.c`'s trial deletion — `mark_gray`, `scan_black`, `scan_`,
|
||||
`collect_white`, `white_free`, the candidate buffer, the zombie guard.
|
||||
- `owner.ml`'s rc machinery — the rc table, elision groups, `rc_escaped`,
|
||||
`gc_escape`, `resolve_rc`, `release_gc`, and the clobber rule that exists
|
||||
only to invalidate elision.
|
||||
- `emit.ml`'s `emit_rc` and the escape-acquire anchor.
|
||||
- The `RC_INC`/`RC_DEC` interpreter cases.
|
||||
- **All four recorded `@gc` defects**, by construction: the `set` gap has no
|
||||
`RC_INC` to omit; the held-cycle leak was rc inflation from an unreleased
|
||||
return value, and with tracing that value simply is not a root; the
|
||||
`mut`-`@gc` clobber protected an elision that no longer exists; the `push`
|
||||
bug cannot recur.
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- **The barrier is the load-bearing test.** An adversarial fixture where the
|
||||
mutator hides a traced object between marking slices — store it into an
|
||||
already-blackened object and drop the original reference — must not free it.
|
||||
A barrier bug is silent corruption, so this test is the design's safety net
|
||||
and must fail loudly if the barrier is compiled out.
|
||||
- **Inference:** unit tests for SCC classification (self-loop, mutual
|
||||
recursion, `multi Self`, `map<_, Self>`, and the `ref T`-creates-no-edge
|
||||
case), promotion cases, and golden `--dump-gc` output including the note
|
||||
text.
|
||||
- **Collector:** `runtime/test/test_cycle.c` and `test_rc.c` are rewritten —
|
||||
they currently assert rc values, which cease to exist. New assertions: an
|
||||
abandoned cycle is freed, a rooted cycle survives, slices are bounded, and
|
||||
the sweep list has no leak after N cycles.
|
||||
- **Corpus:** `tests/corpus/gc/abandoned-cycle` and `budget-steps` survive
|
||||
with re-blessed traces. `held-cycle` is **redefined** — with tracing, a
|
||||
post-exit heap has no roots at all, so the honest fixture is "a cycle rooted
|
||||
from a live frame survives a slice", tested from inside a running program
|
||||
rather than after the entry returns.
|
||||
- **ASan across the corpus**, as today, plus a leak-free assertion after
|
||||
repeated collection cycles.
|
||||
- The milestone-1 acceptance gate's criterion 4 is restated in terms of
|
||||
tracing; criterion 3's ASan clause is expected to go green, since the
|
||||
held-cycle leak is one of the defects this deletes.
|
||||
|
||||
## 8. Migration — normative claims to amend
|
||||
|
||||
| Where | Change |
|
||||
| --- | --- |
|
||||
| `docs/00-principles.md` principle 3 | "`@gc` is a per-class opt-in" → GC-ness is inferred; keep "no global pause exists by construction" (still true — per-shard, and other shards never stop) |
|
||||
| OOP spec decision table, GC-granularity row | per-class annotation → inferred, with the hybrid rule named |
|
||||
| OOP spec §3 rule 5 | "`@gc` class instances alias freely" → traced classes alias freely, and which classes those are is inferred |
|
||||
| OOP spec §4 memory model | replace the RC + Bacon–Rajan paragraph with tracing; header `rc` → sweep-list link; drop `IN_CYCLE_BUF` |
|
||||
| `00-wob-format.md` | opcodes 27–28 reserved; version bump; drop-table gc-mask contract restated as GC roots; class-flag provenance |
|
||||
| `01-error-catalog.md` | WO-W201 (`@gc` suggestion) retired — inference supersedes it; WO-E304's `@gc`-exemption wording updated; new WO-E1xx for `@gc` in source |
|
||||
| `08-builtin-surface.md` | the `push` special case and the `set` gap both deleted — neither exists without RC |
|
||||
| Goldens | every fixture rendering ` @gc`, `flags=gc`, `gc={rN}`, `RC_INC`/`RC_DEC`, or the `== RC ==` table section re-blessed |
|
||||
| `docs/00-status.md` | records this as the iteration that supersedes part of iteration 2's memory model |
|
||||
|
||||
## Success criteria
|
||||
|
||||
1. No `.wo` file in the repo contains `@gc`, and using it is a diagnostic.
|
||||
2. `--dump-gc` classifies every class in the pricing and corpus samples, and
|
||||
every traced class's reason is either a cycle path or a named escape site.
|
||||
3. `RC_INC`/`RC_DEC` appear in no emitted image; the opcodes are reserved in
|
||||
the format doc.
|
||||
4. The adversarial barrier fixture fails when the barrier is compiled out and
|
||||
passes when it is in.
|
||||
5. An abandoned cycle is collected within budgeted slices with no slice
|
||||
exceeding the configured budget; a rooted cycle survives.
|
||||
6. The whole corpus is ASan-clean, including after repeated collection cycles
|
||||
— closing milestone-1 criterion 3.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Cross-shard tracing (ownership moves mean no traced object spans shards);
|
||||
generational collection (no remembered set, no age bits — Go's isn't
|
||||
generational either); compaction (non-moving is load-bearing: no forwarding
|
||||
pointers, no read barrier); scheduler-integrated pacing beyond the heap-goal
|
||||
trigger, which remains sub-project 2's concern; and `ref T` semantics, which
|
||||
are unchanged.
|
||||
89
justfile
89
justfile
|
|
@ -34,6 +34,95 @@ woc-build:
|
|||
woc-test:
|
||||
dune runtest --root compiler
|
||||
|
||||
# wovm (runtime/): build the plain, optimized binary — the release build,
|
||||
# no sanitizer (wovm-test is the ASan-clean gate; `make -C runtime wovm-asan`
|
||||
# builds a separate sanitized binary for the corpus, see oop-accept)
|
||||
wovm-build:
|
||||
make -C runtime wovm
|
||||
|
||||
# wovm gate: unit suites (both dispatch flavors: computed-goto + ISO switch
|
||||
# under -DWO_ISO_C, so neither rots) + CLI smoke, all ASan+UBSan
|
||||
wovm-test:
|
||||
make -C runtime test
|
||||
make -C runtime test-iso
|
||||
bash runtime/test/cli_smoke.sh
|
||||
|
||||
# conformance harness (plan 3): walks tests/corpus/{run,compile-fail,trap},
|
||||
# exact outcome per fixture kind — see docs/plan/oop-vm/02-corpus.md.
|
||||
# Fails loudly (and names the recipe to run) if woc or wovm isn't built.
|
||||
oop-e2e:
|
||||
./scripts/oop-e2e.sh
|
||||
|
||||
# milestone-1 acceptance gate (docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md
|
||||
# "Success criteria"): the five spec criteria plus both unit gates, one
|
||||
# command, in the spec's order. Fails loudly on the first failing stage,
|
||||
# names the criterion, exits nonzero — a measurement that only prints is
|
||||
# not a gate.
|
||||
oop-accept:
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
ROOT="$(pwd)"
|
||||
fail() { echo "oop-accept: FAILED -- $1" >&2; exit 1; }
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/oop-accept.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
echo "=== criterion 1: woc compile time, pricing subset (budget: under 100ms) ==="
|
||||
dune build --root compiler || fail "criterion 1: dune build --root compiler"
|
||||
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||
PRICING="tests/corpus/run/pricing-containers/fixture.wo tests/corpus/run/pricing-current-price/fixture.wo tests/corpus/run/pricing-discounted/fixture.wo tests/corpus/run/pricing-text/fixture.wo tests/corpus/trap/pricing-set-price-db-stub/fixture.wo"
|
||||
N=20
|
||||
total_ns=0; max_ns=0; min_ns=""
|
||||
for i in $(seq 1 "$N"); do
|
||||
start=$(date +%s%N)
|
||||
for f in $PRICING; do
|
||||
"$WOC" --emit "$f" -o "$WORK/pricing.wob" || fail "criterion 1: woc --emit $f"
|
||||
done
|
||||
end=$(date +%s%N)
|
||||
elapsed=$((end - start))
|
||||
total_ns=$((total_ns + elapsed))
|
||||
[[ -z "$min_ns" || elapsed -lt min_ns ]] && min_ns=$elapsed
|
||||
[[ elapsed -gt max_ns ]] && max_ns=$elapsed
|
||||
done
|
||||
avg_ms=$(awk -v t="$total_ns" -v n="$N" 'BEGIN{printf "%.3f", t/n/1000000}')
|
||||
min_ms=$(awk -v t="$min_ns" 'BEGIN{printf "%.3f", t/1000000}')
|
||||
max_ms=$(awk -v t="$max_ns" 'BEGIN{printf "%.3f", t/1000000}')
|
||||
echo " woc --emit over all 5 pricing-subset fixtures, $N runs: min ${min_ms}ms avg ${avg_ms}ms max ${max_ms}ms"
|
||||
[[ "$max_ns" -lt 100000000 ]] || fail "criterion 1: worst case ${max_ms}ms meets/exceeds the 100ms budget"
|
||||
echo " criterion 1: MET"
|
||||
|
||||
echo "=== criteria 2-4: full conformance corpus under ASan (runtime/build/wovm_asan) ==="
|
||||
make -C runtime wovm || fail "criteria 2-4: make -C runtime wovm"
|
||||
make -C runtime wovm-asan || fail "criteria 2-4: make -C runtime wovm-asan"
|
||||
cp "$ROOT/runtime/wovm" "$WORK/wovm.release"
|
||||
restore_wovm() { cp "$WORK/wovm.release" "$ROOT/runtime/wovm"; }
|
||||
echo " swapping runtime/wovm -> runtime/build/wovm_asan for this stage only (oop-e2e.sh has no --wovm override)"
|
||||
cp "$ROOT/runtime/build/wovm_asan" "$ROOT/runtime/wovm"
|
||||
if ./scripts/oop-e2e.sh; then
|
||||
restore_wovm
|
||||
else
|
||||
restore_wovm
|
||||
fail "criteria 2-4: conformance corpus failed under ASan (see output above)"
|
||||
fi
|
||||
echo " runtime/wovm restored to the release binary"
|
||||
echo " criteria 2-4: MET"
|
||||
|
||||
echo "=== criterion 5: single-binary smoke (runtime/wovm, release binary) ==="
|
||||
./scripts/single-binary-smoke.sh || fail "criterion 5: single-binary smoke"
|
||||
echo " criterion 5: MET"
|
||||
|
||||
echo "=== unit gate: runtime (both dispatch flavors + cli_smoke) ==="
|
||||
make -C runtime test || fail "runtime unit gate: make -C runtime test"
|
||||
make -C runtime test-iso || fail "runtime unit gate: make -C runtime test-iso"
|
||||
bash runtime/test/cli_smoke.sh || fail "runtime unit gate: cli_smoke"
|
||||
echo " runtime unit gate: MET"
|
||||
|
||||
echo "=== unit gate: compiler ==="
|
||||
dune runtest --root compiler || fail "compiler unit gate: dune runtest --root compiler"
|
||||
echo " compiler unit gate: MET"
|
||||
|
||||
echo
|
||||
echo "oop-accept: ALL CRITERIA MET"
|
||||
|
||||
# phase-F benchmark: reads, durable writes, 10k idle conns (scaled geometry)
|
||||
rt-c-bench port="8085" threads="8" conns="64":
|
||||
#!/usr/bin/env bash
|
||||
|
|
|
|||
|
|
@ -40,6 +40,14 @@ test-iso: $(ISOBIN)
|
|||
wovm: src/main.c $(VMSRC) $(VMHDR)
|
||||
$(CC) $(CFLAGS) -Isrc -o $@ src/main.c $(VMSRC)
|
||||
|
||||
# ASan+UBSan wovm, same flags as the unit tests, for corpus fixtures that
|
||||
# need a sanitizer to prove a free actually happened (gc/ cycle fixtures) —
|
||||
# tasks 3/4 hand-built this each time because it didn't exist yet
|
||||
build/wovm_asan: src/main.c $(VMSRC) $(VMHDR) | build
|
||||
$(CC) $(TCFLAGS) -Isrc -o $@ src/main.c $(VMSRC)
|
||||
|
||||
wovm-asan: build/wovm_asan
|
||||
|
||||
# fixture generator for the CLI smoke test
|
||||
build/mkwob: test/mkwob.c test/wob_build.c $(VMHDR) | build
|
||||
$(CC) $(CFLAGS) -Isrc -Itest -o $@ test/mkwob.c test/wob_build.c
|
||||
|
|
@ -56,4 +64,4 @@ clean:
|
|||
rm -f wo-rt bench/bench wovm
|
||||
rm -rf build
|
||||
|
||||
.PHONY: bench run clean test test-iso
|
||||
.PHONY: bench run clean test test-iso wovm-asan
|
||||
|
|
|
|||
|
|
@ -82,19 +82,19 @@ Single-shot RECV re-armed per request (multishot recv + buffer rings are a phase
|
|||
|
||||
## The wovm bytecode VM (runtime/src/)
|
||||
|
||||
Milestone 1 of the OOP track (spec: [`docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`](../docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md), plan 1: [`docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md`](../docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md)) — a register VM that executes `.wob` bytecode (format: [`docs/plan/oop-vm/00-wob-format.md`](../docs/plan/oop-vm/00-wob-format.md)) with the full milestone-1 memory model. C11, libc only, same doctrine as `wo-rt.c`.
|
||||
Milestone 1 of the OOP track (spec: [`docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`](../docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md), plan 1: [`docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md`](../docs/superpowers/plans/2026-08-01-wob-format-and-vm-core.md)) — a register VM that executes `.wob` bytecode (format: [`docs/plan/oop-vm/00-wob-format.md`](../docs/plan/oop-vm/00-wob-format.md)) with the full milestone-1 memory model. C11, libc only, same doctrine as `wo-rt.c`. `wovm` doesn't produce `.wob` itself — that's the OCaml `woc` front end's job ([`compiler/README.md`](../compiler/README.md), plan 3); this directory is the VM, not the compiler.
|
||||
|
||||
**Shipped features:**
|
||||
|
||||
- **Register interpreter** — fixed 32-bit instructions, Lua-style window-overlap calls (callee r0 = caller slot A), dual dispatch: computed goto under GNU C, `switch` under `-DWO_ISO_C` (both flavors gated in CI so neither rots).
|
||||
- **Owned objects with a runtime borrow word** — shared-reader count / exclusive sentinel in every 16-byte header; violations trap `T_BORROW`. The compiler elides provable sites; the VM enforces the residual ones (hybrid model, spec §4).
|
||||
- **`@gc` reference counting + budgeted cycle collection** — rc at zero frees immediately; possible cycles buffer as candidates (Bacon–Rajan trial deletion), collected in budgeted epochs per shard — no stop-the-world by construction.
|
||||
- **`@gc` reference counting + budgeted cycle collection** — rc at zero frees immediately; possible cycles buffer as candidates (Bacon–Rajan trial deletion), collected in budgeted epochs per shard — no stop-the-world by construction. The `wovm` CLI pumps the collector to quiescence after the entry method returns (`WO_GC_BUDGET` steps per call, default 64; `WO_GC_TRACE=1` prints one stderr line per step) — scheduler-paced stepping between requests is sub-project 2, not this milestone.
|
||||
- **Deterministic drops** — kind-directed drop plans (scalar/owned/gcref/text/multi/map), recursive over class fields and container elements.
|
||||
- **Trap unwinding that never leaks** — per-method drop tables (pc → owned/gc register masks); a trap walks every frame and frees what was live; structured error `{code, method, line, message}` via line tables.
|
||||
- **Validating loader** — bounds-checked parse, aligned copies, const-string interning, full static validation (opcodes, registers, indexes, jump targets, terminators, builtin arity, call windows, sorted vtables); what the loader accepts, the interpreter trusts — no UB on any input.
|
||||
- **Structural interfaces** — `ICALL` binary-searches sorted (class, slot, method) vtable triples by receiver class.
|
||||
- **Native containers + builtins** — `multi`/`map` with element-kind tags; `now/print/print_int/words/multi_*/map_*`; `DB_STUB` traps "engine not linked" until the DB engine binds (plan 5).
|
||||
- **CLI contract** — `wovm app.wob`: exit 0 = ran; exit 1 = trap, one stderr line `trap CODE in METHOD at line N: MESSAGE`; exit 2 = usage/load failure. `WO_HEAP_MB` overrides the 64 MiB arena.
|
||||
- **CLI contract** — `wovm app.wob`: exit 0 = ran; exit 1 = trap, one stderr line `trap CODE in METHOD at line N: MESSAGE`; exit 2 = usage/load failure. `WO_HEAP_MB` overrides the 64 MiB arena. Run with no `.wob` argument, `wovm` also checks its own trailer for an appended image (`woc build`'s single-binary output, [`docs/plan/oop-vm/00-wob-format.md`](../docs/plan/oop-vm/00-wob-format.md)'s "single-binary trailer" section) — a recognized-but-corrupt trailer fails clearly on exit 2, never a crash.
|
||||
|
||||
**File map:**
|
||||
|
||||
|
|
@ -108,14 +108,14 @@ Milestone 1 of the OOP track (spec: [`docs/superpowers/specs/2026-08-01-oop-comp
|
|||
| `src/loader.h/.c` | `.wob` parse + full static validation + mmap file path |
|
||||
| `src/vm.h/.c` | the interpreter: dispatch, frames, traps, drop-map unwinding, `ICALL` |
|
||||
| `src/builtin.h/.c` | builtin dispatcher |
|
||||
| `src/main.c` | the `wovm` CLI |
|
||||
| `src/main.c` | the `wovm` CLI: arg parsing, self-embedded-trailer detection, the post-exit gc pump |
|
||||
| `test/t.h` | 20-line assert harness (no framework) |
|
||||
| `test/wob_build.h/.c` | in-memory `.wob` assembler — the second, independent encoding of the format; builder/loader disagreements fail tests |
|
||||
| `test/test_*.c` | 13 suites, one binary each, ASan+UBSan |
|
||||
| `test/mkwob.c` | fixture generator for the CLI smoke |
|
||||
| `test/cli_smoke.sh` | end-to-end exit-code/stderr-shape check |
|
||||
|
||||
**Gates:** `just wovm-build` · `just wovm-test` (unit suites + ISO flavor + CLI smoke, all ASan-clean) · in `runtime/`: `make test`, `make test-iso`, `make wovm`.
|
||||
**Gates:** `just wovm-build` · `just wovm-test` (unit suites + ISO flavor + CLI smoke, all ASan-clean) · in `runtime/`: `make test`, `make test-iso`, `make wovm`, `make wovm-asan` (sanitized binary for corpus fixtures that need a leak/UB proof, e.g. `tests/corpus/gc/`). Across both halves: `just oop-e2e` (the `woc` + `wovm` conformance corpus, [`compiler/README.md`](../compiler/README.md)) and `just oop-accept` (milestone 1's full acceptance gate — spec success criteria + both unit suites, one command).
|
||||
|
||||
## Debugging
|
||||
|
||||
|
|
|
|||
|
|
@ -3,24 +3,141 @@
|
|||
* 1 = trap; one stderr line: "trap CODE in METHOD at line N: MESSAGE"
|
||||
* 2 = usage or load failure (loader's message on stderr)
|
||||
* Heap cap defaults to 64 MiB, overridable via WO_HEAP_MB. */
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "gc.h"
|
||||
#include "vm.h"
|
||||
|
||||
static wo_vm VM; /* 32K value stack: keep it off the C stack */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: wovm <file.wob>\n");
|
||||
return 2;
|
||||
/* ---- self-exec detection (Task 6, plan 3) -------------------------------
|
||||
* `woc build` makes a single executable by copying wovm and appending the
|
||||
* .wob image plus a fixed-size trailer; docs/plan/oop-vm/00-wob-format.md's
|
||||
* "single-binary trailer" section is the normative layout (writer:
|
||||
* compiler/bin/main.ml) -- keep this reader in lock-step with it. Reading
|
||||
* this executable's own path via /proc/self/exe is Linux-only, matching
|
||||
* wob.h's own platform note. */
|
||||
#define WO_TRAILER_MAGIC 0x31544257u /* "WBT1" read as LE u32 */
|
||||
#define WO_TRAILER_SIZE 20u /* payload_off u64, payload_len u64, magic u32 */
|
||||
|
||||
/* 1 = embedded image found and loaded into *mod (caller must ignore argv);
|
||||
* 0 = no trailer (plain wovm binary; caller falls back to argv[1] as
|
||||
* today); -1 = a trailer is present but corrupt (err filled; caller must
|
||||
* report and exit -- never guess or run something unintended). */
|
||||
static int load_self_embedded(wo_module *mod, char *err, size_t errlen) {
|
||||
int fd = open("/proc/self/exe", O_RDONLY);
|
||||
if (fd < 0) return 0;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0 || st.st_size < 0) {
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
size_t size = (size_t)st.st_size;
|
||||
if (size < WO_TRAILER_SIZE) {
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
uint8_t tail[WO_TRAILER_SIZE];
|
||||
if (lseek(fd, (off_t)(size - WO_TRAILER_SIZE), SEEK_SET) < 0 ||
|
||||
read(fd, tail, WO_TRAILER_SIZE) != (ssize_t)WO_TRAILER_SIZE) {
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
uint32_t magic;
|
||||
memcpy(&magic, tail + 16, 4);
|
||||
if (magic != WO_TRAILER_MAGIC) {
|
||||
close(fd);
|
||||
return 0; /* plain wovm binary, nothing embedded */
|
||||
}
|
||||
uint64_t payload_off, payload_len;
|
||||
memcpy(&payload_off, tail + 0, 8);
|
||||
memcpy(&payload_len, tail + 8, 8);
|
||||
/* payload must exactly fill everything between its offset and the
|
||||
* trailer -- no gap, no overlap. Bounding payload_off first makes the
|
||||
* subtraction below safe (no unsigned wraparound on a corrupt value). */
|
||||
if (payload_off > size - WO_TRAILER_SIZE) {
|
||||
close(fd);
|
||||
snprintf(err, errlen, "corrupt trailer (bad payload offset)");
|
||||
return -1;
|
||||
}
|
||||
if (payload_len != size - WO_TRAILER_SIZE - payload_off) {
|
||||
close(fd);
|
||||
snprintf(err, errlen, "corrupt trailer (bad payload length)");
|
||||
return -1;
|
||||
}
|
||||
void *p = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
close(fd);
|
||||
if (p == MAP_FAILED) {
|
||||
snprintf(err, errlen, "cannot mmap self");
|
||||
return -1;
|
||||
}
|
||||
int rc = wo_load_buf(mod, (const uint8_t *)p + payload_off, (size_t)payload_len, err, errlen);
|
||||
munmap(p, size);
|
||||
return rc == 0 ? 1 : -1;
|
||||
}
|
||||
|
||||
/* ---- gc pump -----------------------------------------------------------
|
||||
* Deliberately the simplest possible driver over the plan-1 collector's
|
||||
* already-budgeted step interface (wo_gc_step, gc.h): after the entry
|
||||
* method returns, drain the cycle-candidate buffer in bounded slices until
|
||||
* it is empty. This is post-exit-only pacing and nothing more — real
|
||||
* scheduler-integrated pacing (stepping between turns of live work while
|
||||
* the program keeps running) is sub-project 2's job; this milestone only
|
||||
* proves the budgeted-step interface end to end and makes it observable.
|
||||
* WO_GC_TRACE prints one stderr line per step (never stdout — the corpus
|
||||
* harness diffs stdout byte-for-byte) so a fixture can assert "collection
|
||||
* happened in bounded slices", not just "the leak is gone". */
|
||||
static void gc_pump(wo_vm *vm) {
|
||||
size_t budget = 64; /* candidates per step: no prior art to size this
|
||||
against (post-exit draining is new), so picked
|
||||
to mirror WO_HEAP_MB's default 64 — small
|
||||
enough that a deliberately oversized abandoned
|
||||
structure visibly takes more than one step,
|
||||
large enough that ordinary programs clear in
|
||||
one or two */
|
||||
const char *benv = getenv("WO_GC_BUDGET");
|
||||
if (benv && benv[0]) {
|
||||
char *end = NULL;
|
||||
unsigned long v = strtoul(benv, &end, 10);
|
||||
if (end && *end == '\0' && v >= 1 && v <= 1000000) budget = v;
|
||||
}
|
||||
int trace = getenv("WO_GC_TRACE") != NULL;
|
||||
size_t step = 0;
|
||||
while (vm->rt.cycbuf.len > 0) {
|
||||
size_t before = vm->rt.cycbuf.len;
|
||||
size_t freed = wo_gc_step(&vm->rt, budget);
|
||||
step++;
|
||||
if (trace)
|
||||
fprintf(stderr, "gc: step %zu budget=%zu freed=%zu visited=%zu remaining=%zu\n", step,
|
||||
budget, freed, before - vm->rt.cycbuf.len, vm->rt.cycbuf.len);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
wo_module mod;
|
||||
char err[256];
|
||||
if (wo_load_file(&mod, argv[1], err, sizeof err) != 0) {
|
||||
int self_rc = load_self_embedded(&mod, err, sizeof err);
|
||||
if (self_rc < 0) {
|
||||
fprintf(stderr, "wovm: %s\n", err);
|
||||
return 2;
|
||||
}
|
||||
if (self_rc == 0) {
|
||||
/* no embedded image: today's contract, unchanged */
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: wovm <file.wob>\n");
|
||||
return 2;
|
||||
}
|
||||
if (wo_load_file(&mod, argv[1], err, sizeof err) != 0) {
|
||||
fprintf(stderr, "wovm: %s\n", err);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (mod.entry == WOB_NONE) {
|
||||
fprintf(stderr, "wovm: module has no entry method\n");
|
||||
wo_module_free(&mod);
|
||||
|
|
@ -44,6 +161,7 @@ int main(int argc, char **argv) {
|
|||
if (rc != 0)
|
||||
fprintf(stderr, "trap %u in %s at line %u: %s\n", (unsigned)terr.code,
|
||||
terr.method, (unsigned)terr.line, terr.msg);
|
||||
gc_pump(&VM);
|
||||
wo_vm_destroy(&VM);
|
||||
wo_module_free(&mod);
|
||||
return rc == 0 ? 0 : 1;
|
||||
|
|
|
|||
334
scripts/oop-e2e.sh
Executable file
334
scripts/oop-e2e.sh
Executable file
|
|
@ -0,0 +1,334 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/oop-e2e.sh — the conformance harness (plan 3, Task 2).
|
||||
#
|
||||
# Walks tests/corpus/{run,compile-fail,trap,gc}/*/ and enforces one exact
|
||||
# outcome per fixture kind (docs/plan/oop-vm/02-corpus.md has the
|
||||
# contribution contract this script is the enforcement of):
|
||||
#
|
||||
# run/ fixture.wo compiles with woc and runs with wovm;
|
||||
# stdout must equal fixture.out BYTE-FOR-BYTE.
|
||||
# compile-fail/ fixture.wo must fail to compile with exactly the
|
||||
# WO-E### named in fixture.code.
|
||||
# trap/ fixture.wo must compile, then wovm must trap with
|
||||
# exactly the code named in fixture.trap, parsed from
|
||||
# wovm's fixed stderr line
|
||||
# ("trap N in METHOD at line L: MESSAGE").
|
||||
# gc/ fixture.wo compiles and runs with wovm under
|
||||
# WO_GC_TRACE=1 (and WO_GC_BUDGET from the optional
|
||||
# fixture.gc_budget); stdout must equal fixture.out
|
||||
# byte-for-byte, and the gc pump's stderr trace must
|
||||
# match fixture.trace's step count and total freed
|
||||
# count exactly.
|
||||
#
|
||||
# Any other outcome — wrong code, unexpected success, a loader
|
||||
# rejection, a crash, a hang — fails and names the fixture. One line
|
||||
# per fixture, a final tally, nonzero exit if anything failed.
|
||||
|
||||
set -uo pipefail # no -e: a failing fixture is handled explicitly, one at a time
|
||||
shopt -s nullglob
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||
WOVM="$ROOT/runtime/wovm"
|
||||
CORPUS="$ROOT/tests/corpus"
|
||||
|
||||
# A hang in either binary must not block `just oop-e2e`/CI forever with no
|
||||
# diagnostic — every invocation below runs under `timeout`, and a kill
|
||||
# (exit 124, timeout(1)'s own signal for "I killed it") is reported as its
|
||||
# own named failure, never folded into "exited nonzero". Overridable for a
|
||||
# slower box; fixtures in this corpus are small enough that the default is
|
||||
# generous, not tight.
|
||||
TIMEOUT="${OOP_E2E_TIMEOUT:-10}"
|
||||
|
||||
if [[ ! -x "$WOC" ]]; then
|
||||
echo "oop-e2e: woc is not built ($WOC) — run: just woc-build" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$WOVM" ]]; then
|
||||
echo "oop-e2e: wovm is not built ($WOVM) — run: make -C runtime wovm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/oop-e2e.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
ok() {
|
||||
echo "ok $1"
|
||||
pass=$((pass + 1))
|
||||
}
|
||||
|
||||
bad() {
|
||||
echo "FAIL $1 -- $2"
|
||||
fail=$((fail + 1))
|
||||
}
|
||||
|
||||
# One scratch-file prefix per fixture (kind+name is unique across the
|
||||
# whole corpus), so no per-file mktemp calls are needed inside the loop.
|
||||
tmp_prefix() {
|
||||
echo "$WORK/$(echo "$1" | tr '/' '-')"
|
||||
}
|
||||
|
||||
# Every WO-E### a run actually reported, anchored on the diagnostic
|
||||
# renderer's own text (diag.ml's `render`: "file:line:col: error CODE:
|
||||
# message") -- not a bare substring search. This is what makes matching
|
||||
# exact both ways: an expected "WO-E21" cannot match a reported "WO-E215"
|
||||
# (Critical 1, review round 1), and an empty/garbled fixture.code cannot
|
||||
# match a stray occurrence of the right digits inside a message body.
|
||||
extract_error_codes() {
|
||||
grep -oE 'error (WO-E[0-9]+):' "$1" | sed -E 's/error (WO-E[0-9]+):/\1/'
|
||||
}
|
||||
|
||||
run_fixture() {
|
||||
local dir="$1" name="run/$(basename "$1")"
|
||||
local prefix wob out err rc
|
||||
|
||||
if [[ ! -f "$dir/fixture.wo" ]]; then bad "$name" "missing fixture.wo"; return; fi
|
||||
if [[ ! -f "$dir/fixture.out" ]]; then bad "$name" "missing fixture.out"; return; fi
|
||||
|
||||
prefix="$(tmp_prefix "$name")"
|
||||
wob="$prefix.wob" out="$prefix.out" err="$prefix.err"
|
||||
|
||||
timeout "$TIMEOUT" "$WOC" --emit "$dir/fixture.wo" -o "$wob" >/dev/null 2>"$err"
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "woc timed out after ${TIMEOUT}s"
|
||||
return
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "woc crashed (signal $((rc - 128)))"
|
||||
return
|
||||
elif [[ $rc -ne 0 ]]; then
|
||||
bad "$name" "compile failed (exit $rc): $(head -1 "$err")"
|
||||
return
|
||||
fi
|
||||
|
||||
timeout "$TIMEOUT" "$WOVM" "$wob" >"$out" 2>"$err"
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "wovm timed out after ${TIMEOUT}s"
|
||||
return
|
||||
elif [[ $rc -eq 1 ]]; then
|
||||
bad "$name" "unexpected trap: $(head -1 "$err")"
|
||||
return
|
||||
elif [[ $rc -eq 2 ]]; then
|
||||
bad "$name" "loader rejection: $(head -1 "$err")"
|
||||
return
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "wovm crashed (signal $((rc - 128)))"
|
||||
return
|
||||
elif [[ $rc -ne 0 ]]; then
|
||||
bad "$name" "wovm exited $rc: $(head -1 "$err")"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! diff -q "$dir/fixture.out" "$out" >/dev/null 2>&1; then
|
||||
bad "$name" "stdout mismatch"
|
||||
diff -u "$dir/fixture.out" "$out" | sed 's/^/ /'
|
||||
return
|
||||
fi
|
||||
ok "$name"
|
||||
}
|
||||
|
||||
gc_fixture() {
|
||||
local dir="$1" name="gc/$(basename "$1")"
|
||||
local prefix wob out err rc exp_steps exp_freed got_steps got_freed budget
|
||||
|
||||
if [[ ! -f "$dir/fixture.wo" ]]; then bad "$name" "missing fixture.wo"; return; fi
|
||||
if [[ ! -f "$dir/fixture.out" ]]; then bad "$name" "missing fixture.out"; return; fi
|
||||
if [[ ! -f "$dir/fixture.trace" ]]; then bad "$name" "missing fixture.trace"; return; fi
|
||||
|
||||
prefix="$(tmp_prefix "$name")"
|
||||
wob="$prefix.wob" out="$prefix.out" err="$prefix.err"
|
||||
|
||||
timeout "$TIMEOUT" "$WOC" --emit "$dir/fixture.wo" -o "$wob" >/dev/null 2>"$err"
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "woc timed out after ${TIMEOUT}s"
|
||||
return
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "woc crashed (signal $((rc - 128)))"
|
||||
return
|
||||
elif [[ $rc -ne 0 ]]; then
|
||||
bad "$name" "compile failed (exit $rc): $(head -1 "$err")"
|
||||
return
|
||||
fi
|
||||
|
||||
exp_steps="$(grep -oE 'steps=[0-9]+' "$dir/fixture.trace" | head -1 | cut -d= -f2)"
|
||||
exp_freed="$(grep -oE 'freed=[0-9]+' "$dir/fixture.trace" | head -1 | cut -d= -f2)"
|
||||
if [[ -z "$exp_steps" || -z "$exp_freed" ]]; then
|
||||
bad "$name" "fixture.trace missing steps=/freed="
|
||||
return
|
||||
fi
|
||||
|
||||
# WO_GC_TRACE is always on so the pump's stderr trace can be checked;
|
||||
# WO_GC_BUDGET is only set when the fixture names one (fixture.gc_budget
|
||||
# is optional — absent means "use the pump's own default").
|
||||
if [[ -f "$dir/fixture.gc_budget" ]]; then
|
||||
budget="$(tr -d '[:space:]' <"$dir/fixture.gc_budget")"
|
||||
timeout "$TIMEOUT" env WO_GC_TRACE=1 WO_GC_BUDGET="$budget" "$WOVM" "$wob" >"$out" 2>"$err"
|
||||
else
|
||||
timeout "$TIMEOUT" env WO_GC_TRACE=1 "$WOVM" "$wob" >"$out" 2>"$err"
|
||||
fi
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "wovm timed out after ${TIMEOUT}s"
|
||||
return
|
||||
elif [[ $rc -eq 1 ]]; then
|
||||
bad "$name" "unexpected trap: $(head -1 "$err")"
|
||||
return
|
||||
elif [[ $rc -eq 2 ]]; then
|
||||
bad "$name" "loader rejection: $(head -1 "$err")"
|
||||
return
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "wovm crashed (signal $((rc - 128)))"
|
||||
return
|
||||
elif [[ $rc -ne 0 ]]; then
|
||||
bad "$name" "wovm exited $rc: $(head -1 "$err")"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! diff -q "$dir/fixture.out" "$out" >/dev/null 2>&1; then
|
||||
bad "$name" "stdout mismatch"
|
||||
diff -u "$dir/fixture.out" "$out" | sed 's/^/ /'
|
||||
return
|
||||
fi
|
||||
|
||||
got_steps="$(grep -c '^gc: step ' "$err")"
|
||||
got_freed="$(grep -oE 'freed=[0-9]+' "$err" | cut -d= -f2 | awk '{s += $1} END {print s + 0}')"
|
||||
if [[ "$got_steps" != "$exp_steps" || "$got_freed" != "$exp_freed" ]]; then
|
||||
bad "$name" "gc trace mismatch: expected steps=$exp_steps freed=$exp_freed, got steps=$got_steps freed=$got_freed"
|
||||
return
|
||||
fi
|
||||
|
||||
ok "$name"
|
||||
}
|
||||
|
||||
compile_fail_fixture() {
|
||||
local dir="$1" name="compile-fail/$(basename "$1")"
|
||||
local prefix wob err rc expected
|
||||
|
||||
if [[ ! -f "$dir/fixture.wo" ]]; then bad "$name" "missing fixture.wo"; return; fi
|
||||
if [[ ! -f "$dir/fixture.code" ]]; then bad "$name" "missing fixture.code"; return; fi
|
||||
|
||||
prefix="$(tmp_prefix "$name")"
|
||||
wob="$prefix.wob" err="$prefix.err"
|
||||
expected="$(tr -d '[:space:]' <"$dir/fixture.code")"
|
||||
|
||||
if [[ -z "$expected" ]]; then
|
||||
bad "$name" "fixture.code is empty"
|
||||
return
|
||||
fi
|
||||
|
||||
timeout "$TIMEOUT" "$WOC" --emit "$dir/fixture.wo" -o "$wob" >/dev/null 2>"$err"
|
||||
rc=$?
|
||||
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "expected $expected, woc timed out after ${TIMEOUT}s"
|
||||
elif [[ $rc -eq 0 ]]; then
|
||||
bad "$name" "expected $expected, compiled clean (unexpected success)"
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "expected $expected, woc crashed (signal $((rc - 128)))"
|
||||
elif [[ $rc -eq 2 ]]; then
|
||||
bad "$name" "expected $expected, got a usage/IO error: $(head -1 "$err")"
|
||||
elif [[ $rc -ne 1 ]]; then
|
||||
bad "$name" "expected $expected, woc exited $rc"
|
||||
elif ! extract_error_codes "$err" | grep -qxF "$expected"; then
|
||||
bad "$name" "expected $expected, got: $(head -1 "$err")"
|
||||
else
|
||||
ok "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
trap_fixture() {
|
||||
local dir="$1" name="trap/$(basename "$1")"
|
||||
local prefix wob out err rc expected got
|
||||
|
||||
if [[ ! -f "$dir/fixture.wo" ]]; then bad "$name" "missing fixture.wo"; return; fi
|
||||
if [[ ! -f "$dir/fixture.trap" ]]; then bad "$name" "missing fixture.trap"; return; fi
|
||||
|
||||
prefix="$(tmp_prefix "$name")"
|
||||
wob="$prefix.wob" out="$prefix.out" err="$prefix.err"
|
||||
expected="$(tr -d '[:space:]' <"$dir/fixture.trap")"
|
||||
|
||||
if [[ -z "$expected" ]]; then
|
||||
bad "$name" "fixture.trap is empty"
|
||||
return
|
||||
fi
|
||||
|
||||
timeout "$TIMEOUT" "$WOC" --emit "$dir/fixture.wo" -o "$wob" >/dev/null 2>"$err"
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "expected trap $expected, woc timed out after ${TIMEOUT}s"
|
||||
return
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "expected trap $expected, woc crashed (signal $((rc - 128)))"
|
||||
return
|
||||
elif [[ $rc -ne 0 ]]; then
|
||||
bad "$name" "expected trap $expected, compile failed: $(head -1 "$err")"
|
||||
return
|
||||
fi
|
||||
|
||||
timeout "$TIMEOUT" "$WOVM" "$wob" >"$out" 2>"$err"
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "$name" "expected trap $expected, wovm timed out after ${TIMEOUT}s"
|
||||
return
|
||||
elif [[ $rc -eq 0 ]]; then
|
||||
bad "$name" "expected trap $expected, ran to completion"
|
||||
return
|
||||
elif [[ $rc -eq 2 ]]; then
|
||||
bad "$name" "expected trap $expected, got a loader rejection: $(head -1 "$err")"
|
||||
return
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "$name" "expected trap $expected, wovm crashed (signal $((rc - 128)))"
|
||||
return
|
||||
elif [[ $rc -ne 1 ]]; then
|
||||
bad "$name" "expected trap $expected, wovm exited $rc"
|
||||
return
|
||||
fi
|
||||
|
||||
# wovm's fixed stderr line: "trap N in METHOD at line L: MESSAGE"
|
||||
got="$(sed -n 's/^trap \([0-9][0-9]*\) in .*/\1/p' "$err" | head -1)"
|
||||
if [[ -z "$got" ]]; then
|
||||
bad "$name" "exit 1 but no parseable trap line: $(head -1 "$err")"
|
||||
elif [[ "$got" != "$expected" ]]; then
|
||||
bad "$name" "expected trap $expected, got trap $got"
|
||||
else
|
||||
ok "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
walk() {
|
||||
local kind="$1" fn="$2" dir
|
||||
for dir in "$CORPUS/$kind"/*/; do
|
||||
[[ -d "$dir" ]] || continue
|
||||
"$fn" "${dir%/}"
|
||||
done
|
||||
}
|
||||
|
||||
walk run run_fixture
|
||||
walk compile-fail compile_fail_fixture
|
||||
walk trap trap_fixture
|
||||
walk gc gc_fixture
|
||||
|
||||
# ---- single-binary smoke (Task 6, plan 3) -----------------------------
|
||||
# `woc build` end to end (relocate outside the repo, run, diff; corrupt
|
||||
# the trailer, confirm a clear failure) doesn't fit the fixture-walk
|
||||
# shape above, so it's a dedicated script -- its ok/FAIL lines fold into
|
||||
# this harness's own tally the same way a fixture's would.
|
||||
SB_LOG="$WORK/single-binary-smoke.log"
|
||||
"$ROOT/scripts/single-binary-smoke.sh" | tee "$SB_LOG"
|
||||
pass=$((pass + $(grep -c '^ok ' "$SB_LOG")))
|
||||
fail=$((fail + $(grep -c '^FAIL ' "$SB_LOG")))
|
||||
|
||||
echo
|
||||
total=$((pass + fail))
|
||||
printf 'oop-e2e: %d checks, %d failures\n' "$total" "$fail"
|
||||
|
||||
if [[ $total -eq 0 ]]; then
|
||||
echo "oop-e2e: no fixtures found under $CORPUS/{run,compile-fail,trap,gc} — harness misconfigured?" >&2
|
||||
exit 1
|
||||
fi
|
||||
[[ $fail -eq 0 ]]
|
||||
118
scripts/single-binary-smoke.sh
Executable file
118
scripts/single-binary-smoke.sh
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/single-binary-smoke.sh — Task 6 (plan 3): proves `woc build`
|
||||
# produces a genuinely self-contained executable, and that a corrupted
|
||||
# trailer (docs/plan/oop-vm/00-wob-format.md, "single-binary trailer"
|
||||
# section) fails clearly instead of crashing or misbehaving silently.
|
||||
#
|
||||
# Steps: build the hello fixture into one file; copy it to a temp
|
||||
# directory OUTSIDE the repo (hardcoded under /tmp, not $TMPDIR — the
|
||||
# whole point is proving nothing repo-relative is needed at run time);
|
||||
# run it there with no arguments; diff stdout byte-for-byte. Then corrupt
|
||||
# the trailer's payload_len field and confirm a clear exit-2 error, not a
|
||||
# crash or a hang.
|
||||
#
|
||||
# Called by scripts/oop-e2e.sh (its ok/FAIL lines fold into that
|
||||
# harness's tally); also runnable standalone. Same output shape as
|
||||
# oop-e2e.sh: "ok NAME" / "FAIL NAME -- reason", one line per check.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WOC="$ROOT/compiler/_build/default/bin/woc"
|
||||
WOVM="$ROOT/runtime/wovm"
|
||||
FIXTURE="$ROOT/tests/corpus/run/hello/fixture.wo"
|
||||
EXPECTED="$ROOT/tests/corpus/run/hello/fixture.out"
|
||||
TIMEOUT="${OOP_E2E_TIMEOUT:-10}"
|
||||
|
||||
if [[ ! -x "$WOC" ]]; then
|
||||
echo "single-binary-smoke: woc is not built ($WOC) — run: just woc-build" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$WOVM" ]]; then
|
||||
echo "single-binary-smoke: wovm is not built ($WOVM) — run: make -C runtime wovm" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
ok() { echo "ok $1"; pass=$((pass + 1)); }
|
||||
bad() { echo "FAIL $1 -- $2"; fail=$((fail + 1)); }
|
||||
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/wo-single-binary-smoke.XXXXXX")"
|
||||
# Deliberately NOT under $WORK / $TMPDIR: this directory is the actual
|
||||
# proof of self-containment, so it must be unambiguously outside the repo
|
||||
# regardless of how TMPDIR is set in the environment running the harness.
|
||||
ELSEWHERE="$(mktemp -d /tmp/wo-single-binary-elsewhere.XXXXXX)"
|
||||
trap 'rm -rf "$WORK" "$ELSEWHERE"' EXIT
|
||||
echo "single-binary-smoke: relocation dir = $ELSEWHERE"
|
||||
|
||||
# ---- build the hello fixture into one file --------------------------------
|
||||
SRC="$WORK/src"
|
||||
mkdir -p "$SRC"
|
||||
cp "$FIXTURE" "$SRC/fixture.wo"
|
||||
APP="$WORK/app"
|
||||
|
||||
if ! timeout "$TIMEOUT" "$WOC" build "$SRC" -o "$APP" --runtime "$WOVM" \
|
||||
>"$WORK/build.out" 2>"$WORK/build.err"; then
|
||||
rc=$?
|
||||
bad "single-binary/build" "woc build exited $rc: $(head -1 "$WORK/build.err")"
|
||||
elif [[ ! -x "$APP" ]]; then
|
||||
bad "single-binary/build" "output missing or not executable: $APP"
|
||||
else
|
||||
ok "single-binary/build"
|
||||
fi
|
||||
|
||||
# ---- relocate outside the repo, run with no args, diff --------------------
|
||||
if [[ -x "$APP" ]]; then
|
||||
cp "$APP" "$ELSEWHERE/app"
|
||||
chmod +x "$ELSEWHERE/app"
|
||||
( cd / && timeout "$TIMEOUT" "$ELSEWHERE/app" ) >"$WORK/relocated.out" 2>"$WORK/relocated.err"
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]]; then
|
||||
bad "single-binary/relocated-run" "exit $rc: $(head -1 "$WORK/relocated.err")"
|
||||
elif ! diff -q "$EXPECTED" "$WORK/relocated.out" >/dev/null 2>&1; then
|
||||
bad "single-binary/relocated-run" "stdout mismatch"
|
||||
diff -u "$EXPECTED" "$WORK/relocated.out" | sed 's/^/ /'
|
||||
else
|
||||
ok "single-binary/relocated-run"
|
||||
fi
|
||||
else
|
||||
bad "single-binary/relocated-run" "skipped: no app to relocate"
|
||||
fi
|
||||
|
||||
# ---- corrupt the trailer's payload_len field, confirm a clear failure -----
|
||||
# Trailer is the last 20 bytes (payload_off u64, payload_len u64, magic
|
||||
# u32): payload_len sits 12 bytes from EOF. Overwriting it with all-0xFF
|
||||
# breaks the reader's "payload_off + payload_len == file_size - 20" check
|
||||
# without touching the magic, so this exercises the "recognized trailer,
|
||||
# bad bounds" path specifically (not the "no trailer at all" fallback).
|
||||
if [[ -f "$ELSEWHERE/app" ]]; then
|
||||
CORRUPT="$WORK/corrupt-app"
|
||||
cp "$ELSEWHERE/app" "$CORRUPT"
|
||||
chmod +x "$CORRUPT"
|
||||
size=$(stat -c%s "$CORRUPT")
|
||||
printf '\xff\xff\xff\xff\xff\xff\xff\xff' \
|
||||
| dd of="$CORRUPT" bs=1 seek=$((size - 12)) count=8 conv=notrunc status=none
|
||||
|
||||
timeout "$TIMEOUT" "$CORRUPT" >"$WORK/corrupt.out" 2>"$WORK/corrupt.err"
|
||||
rc=$?
|
||||
if [[ $rc -eq 124 ]]; then
|
||||
bad "single-binary/corrupt-trailer" "timed out after ${TIMEOUT}s instead of failing cleanly"
|
||||
elif [[ $rc -gt 128 ]]; then
|
||||
bad "single-binary/corrupt-trailer" "crashed (signal $((rc - 128))) instead of failing cleanly"
|
||||
elif [[ $rc -ne 2 ]]; then
|
||||
bad "single-binary/corrupt-trailer" "expected exit 2, got $rc: $(head -1 "$WORK/corrupt.err")"
|
||||
elif [[ -s "$WORK/corrupt.out" ]]; then
|
||||
bad "single-binary/corrupt-trailer" "exit 2 but stdout was not empty (partial run before failing?)"
|
||||
elif ! grep -qi "corrupt trailer" "$WORK/corrupt.err"; then
|
||||
bad "single-binary/corrupt-trailer" "exit 2 but no clear message: $(head -1 "$WORK/corrupt.err")"
|
||||
else
|
||||
ok "single-binary/corrupt-trailer"
|
||||
fi
|
||||
else
|
||||
bad "single-binary/corrupt-trailer" "skipped: no relocated app to corrupt"
|
||||
fi
|
||||
|
||||
total=$((pass + fail))
|
||||
printf 'single-binary-smoke: %d checks, %d failures\n' "$total" "$fail"
|
||||
[[ $fail -eq 0 ]]
|
||||
17
tests/corpus/README.md
Normal file
17
tests/corpus/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# tests/corpus/ — the conformance spine
|
||||
|
||||
Fixture kinds, exact-outcome matching (byte-equal stdout / exact `WO-E###` / exact trap code / exact gc step+freed counts):
|
||||
|
||||
| dir | kind | landed by plan |
|
||||
| --- | --- | --- |
|
||||
| `run/` | compiles + runs, expected stdout | 3 |
|
||||
| `compile-fail/` | must fail with expected `WO-E###` | 3–4 |
|
||||
| `trap/` | must trap with expected code | 3–4 |
|
||||
| `gc/` | cycle collection scenarios | 3 |
|
||||
| `actor/` | shard/spawn/send (ASan+TSan) | 4 |
|
||||
| `db/` | insert/select + crash/replay | 5 |
|
||||
| `lang/` | Haxe-parity adoptions | 8 |
|
||||
| `sys/` | fs/proc/net/time/json stdlib | 9 |
|
||||
| `sample-logwatcher/` | ported log-watcher fixtures | 10 |
|
||||
|
||||
Runner: `scripts/oop-e2e.sh` (plan 3, task 2 — includes the how-to-add-a-fixture doc pointer `docs/plan/oop-vm/02-corpus.md`).
|
||||
0
tests/corpus/actor/.gitkeep
Normal file
0
tests/corpus/actor/.gitkeep
Normal file
0
tests/corpus/compile-fail/.gitkeep
Normal file
0
tests/corpus/compile-fail/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E304
|
||||
22
tests/corpus/compile-fail/borrow-escape-return/fixture.wo
Normal file
22
tests/corpus/compile-fail/borrow-escape-return/fixture.wo
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- Ownership suite (plan 2), as an end-user program: `h` is a plain
|
||||
-- (borrowed) parameter, so returning `h.box` out of `leak` would let the
|
||||
-- borrow outlive the scope it was borrowed from. Mirrors the `leak` case
|
||||
-- of compiler/test/golden/owner-err/borrow-escape.wo, given a `main` that
|
||||
-- actually calls it.
|
||||
class Box {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Holder {
|
||||
box: Box
|
||||
}
|
||||
|
||||
fn leak(h: Holder) -> Box {
|
||||
return h.box
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let h = Holder { box: Box { n: 1 } }
|
||||
let b = leak(h)
|
||||
print_int(b.n)
|
||||
}
|
||||
1
tests/corpus/compile-fail/double-mut-alias/fixture.code
Normal file
1
tests/corpus/compile-fail/double-mut-alias/fixture.code
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E303
|
||||
26
tests/corpus/compile-fail/double-mut-alias/fixture.wo
Normal file
26
tests/corpus/compile-fail/double-mut-alias/fixture.wo
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- Ownership suite (plan 2), as an end-user program: `bag.items[0]` passed
|
||||
-- twice as a `mut` parameter in the same call is a *provable* alias --
|
||||
-- same runtime index, same call -- so it fails at compile time. Contrast
|
||||
-- with tests/corpus/trap/exclusive-borrow-alias, the same shape with a
|
||||
-- runtime-variable index (i == j): the analysis can't prove that one
|
||||
-- either way, so it becomes a residual check the VM traps BORROW on
|
||||
-- instead. This is the hybrid boundary: provable violations fail here;
|
||||
-- unprovable ones trap there. Mirrors the `same_index` case of
|
||||
-- compiler/test/golden/owner-err/double-mut.wo.
|
||||
class Item {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Bag {
|
||||
items: multi Item
|
||||
}
|
||||
|
||||
fn swap(mut a: Item, mut b: Item) -> Int {
|
||||
return a.n + b.n
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let bag = Bag { items: multi_new() }
|
||||
push(bag.items, Item { n: 1 })
|
||||
print_int(swap(bag.items[0], bag.items[0]))
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
WO-E215
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-- Canary fixture (Task 2 review, Important 2): pins the front-end fix
|
||||
-- landed in this same task. A second `class Dup` in one file used to be
|
||||
-- silently dropped by collect_declarations's bare StringMap.add; now it
|
||||
-- is WO-E215, reported at the later declaration.
|
||||
class Dup {
|
||||
n: Int
|
||||
}
|
||||
class Dup {
|
||||
s: Text
|
||||
}
|
||||
|
||||
fn main() {
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
WO-E206
|
||||
16
tests/corpus/compile-fail/incomplete-constructor/fixture.wo
Normal file
16
tests/corpus/compile-fail/incomplete-constructor/fixture.wo
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- E2xx-range substitute for the briefed "unsatisfied interface": WO-E205
|
||||
-- is unreachable by design in the milestone grammar (no interface-typed
|
||||
-- position exists for structural satisfaction to check against --
|
||||
-- docs/plan/oop-vm/01-error-catalog.md's "Unreachable by design") -- a
|
||||
-- fixture expecting it could never pass, by construction, not merely by
|
||||
-- omission. This exercises the constructor check that does fire instead:
|
||||
-- a class literal that omits a declared field with no default.
|
||||
class Item {
|
||||
n: Int
|
||||
label: Text
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let it = Item { n: 1 }
|
||||
print_int(it.n)
|
||||
}
|
||||
1
tests/corpus/compile-fail/move-after-use/fixture.code
Normal file
1
tests/corpus/compile-fail/move-after-use/fixture.code
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E301
|
||||
22
tests/corpus/compile-fail/move-after-use/fixture.wo
Normal file
22
tests/corpus/compile-fail/move-after-use/fixture.wo
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- Ownership suite (plan 2), as an end-user program: `b` is moved into
|
||||
-- `consume` on the first call, so the second call reads it after the
|
||||
-- move. Mirrors compiler/test/golden/owner-err/use-after-move.wo, the
|
||||
-- unit-test-shaped original this fixture gives a `main` and turns into a
|
||||
-- program the corpus harness actually runs through `woc --emit`.
|
||||
class Box {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn consume(take b: Box) -> Int {
|
||||
return b.n
|
||||
}
|
||||
|
||||
fn run(take b: Box) -> Int {
|
||||
let first = consume(b)
|
||||
let second = consume(b)
|
||||
return first + second
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_int(run(Box { n: 1 }))
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
WO-E302
|
||||
25
tests/corpus/compile-fail/move-while-borrowed/fixture.wo
Normal file
25
tests/corpus/compile-fail/move-while-borrowed/fixture.wo
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- Ownership suite (plan 2), as an end-user program: `alias` borrows
|
||||
-- `b.inner` while `b` itself is moved into `consume` -- a move while a
|
||||
-- live borrow of it still exists. Mirrors
|
||||
-- compiler/test/golden/owner-err/move-while-borrowed.wo, given a `main`
|
||||
-- that actually calls it.
|
||||
class Inner {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Box {
|
||||
inner: Inner
|
||||
}
|
||||
|
||||
fn consume(take b: Box) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
fn run(take b: Box) -> Int {
|
||||
let alias = b.inner
|
||||
return consume(b)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_int(run(Box { inner: Inner { n: 1 } }))
|
||||
}
|
||||
1
tests/corpus/compile-fail/unknown-field/fixture.code
Normal file
1
tests/corpus/compile-fail/unknown-field/fixture.code
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E202
|
||||
14
tests/corpus/compile-fail/unknown-field/fixture.wo
Normal file
14
tests/corpus/compile-fail/unknown-field/fixture.wo
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- E2xx-range substitute for the briefed "type error": WO-E201
|
||||
-- (type-mismatch) is declared in types.ml but has no emission site in the
|
||||
-- milestone front end (docs/plan/oop-vm/01-error-catalog.md's "Reserved,
|
||||
-- not yet emitted") -- it can never fire, so a fixture expecting it could
|
||||
-- never pass. This exercises the type-adjacent check that does fire
|
||||
-- instead: a `.field` access naming a field its class doesn't declare.
|
||||
class Item {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let it = Item { n: 1 }
|
||||
print_int(it.price)
|
||||
}
|
||||
0
tests/corpus/db/.gitkeep
Normal file
0
tests/corpus/db/.gitkeep
Normal file
0
tests/corpus/gc/.gitkeep
Normal file
0
tests/corpus/gc/.gitkeep
Normal file
1
tests/corpus/gc/abandoned-cycle/fixture.out
Normal file
1
tests/corpus/gc/abandoned-cycle/fixture.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
1
|
||||
2
tests/corpus/gc/abandoned-cycle/fixture.trace
Normal file
2
tests/corpus/gc/abandoned-cycle/fixture.trace
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
steps=1
|
||||
freed=2
|
||||
153
tests/corpus/gc/abandoned-cycle/fixture.wo
Normal file
153
tests/corpus/gc/abandoned-cycle/fixture.wo
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
-- 129 padding Int fields (p1..p129) push Node's instance size past the
|
||||
-- arena's 1024-byte size-class ceiling (runtime/src/obj.h), so instances
|
||||
-- allocate through plain malloc/free instead of the bump arena -- the
|
||||
-- same trick runtime/test/test_cycle.c uses (BIG=130) so ASan can prove
|
||||
-- a Node is actually freed, not just recycled inside the arena's own
|
||||
-- freelist where a sanitizer can never see it.
|
||||
@gc
|
||||
class Node {
|
||||
peers: multi Node
|
||||
p1: Int
|
||||
p2: Int
|
||||
p3: Int
|
||||
p4: Int
|
||||
p5: Int
|
||||
p6: Int
|
||||
p7: Int
|
||||
p8: Int
|
||||
p9: Int
|
||||
p10: Int
|
||||
p11: Int
|
||||
p12: Int
|
||||
p13: Int
|
||||
p14: Int
|
||||
p15: Int
|
||||
p16: Int
|
||||
p17: Int
|
||||
p18: Int
|
||||
p19: Int
|
||||
p20: Int
|
||||
p21: Int
|
||||
p22: Int
|
||||
p23: Int
|
||||
p24: Int
|
||||
p25: Int
|
||||
p26: Int
|
||||
p27: Int
|
||||
p28: Int
|
||||
p29: Int
|
||||
p30: Int
|
||||
p31: Int
|
||||
p32: Int
|
||||
p33: Int
|
||||
p34: Int
|
||||
p35: Int
|
||||
p36: Int
|
||||
p37: Int
|
||||
p38: Int
|
||||
p39: Int
|
||||
p40: Int
|
||||
p41: Int
|
||||
p42: Int
|
||||
p43: Int
|
||||
p44: Int
|
||||
p45: Int
|
||||
p46: Int
|
||||
p47: Int
|
||||
p48: Int
|
||||
p49: Int
|
||||
p50: Int
|
||||
p51: Int
|
||||
p52: Int
|
||||
p53: Int
|
||||
p54: Int
|
||||
p55: Int
|
||||
p56: Int
|
||||
p57: Int
|
||||
p58: Int
|
||||
p59: Int
|
||||
p60: Int
|
||||
p61: Int
|
||||
p62: Int
|
||||
p63: Int
|
||||
p64: Int
|
||||
p65: Int
|
||||
p66: Int
|
||||
p67: Int
|
||||
p68: Int
|
||||
p69: Int
|
||||
p70: Int
|
||||
p71: Int
|
||||
p72: Int
|
||||
p73: Int
|
||||
p74: Int
|
||||
p75: Int
|
||||
p76: Int
|
||||
p77: Int
|
||||
p78: Int
|
||||
p79: Int
|
||||
p80: Int
|
||||
p81: Int
|
||||
p82: Int
|
||||
p83: Int
|
||||
p84: Int
|
||||
p85: Int
|
||||
p86: Int
|
||||
p87: Int
|
||||
p88: Int
|
||||
p89: Int
|
||||
p90: Int
|
||||
p91: Int
|
||||
p92: Int
|
||||
p93: Int
|
||||
p94: Int
|
||||
p95: Int
|
||||
p96: Int
|
||||
p97: Int
|
||||
p98: Int
|
||||
p99: Int
|
||||
p100: Int
|
||||
p101: Int
|
||||
p102: Int
|
||||
p103: Int
|
||||
p104: Int
|
||||
p105: Int
|
||||
p106: Int
|
||||
p107: Int
|
||||
p108: Int
|
||||
p109: Int
|
||||
p110: Int
|
||||
p111: Int
|
||||
p112: Int
|
||||
p113: Int
|
||||
p114: Int
|
||||
p115: Int
|
||||
p116: Int
|
||||
p117: Int
|
||||
p118: Int
|
||||
p119: Int
|
||||
p120: Int
|
||||
p121: Int
|
||||
p122: Int
|
||||
p123: Int
|
||||
p124: Int
|
||||
p125: Int
|
||||
p126: Int
|
||||
p127: Int
|
||||
p128: Int
|
||||
p129: Int
|
||||
}
|
||||
|
||||
-- Two @gc Nodes reference each other through "peers" (a multi -- the
|
||||
-- only way v1 can build a cycle at all: there is no nil literal, so a
|
||||
-- direct GCREF field can never be the first edge of a pair that needs
|
||||
-- the other to exist first). Both locals go out of scope at the end of
|
||||
-- main with nothing else referencing either: an abandoned cycle. Only
|
||||
-- the post-exit gc pump (runtime/src/main.c) frees them.
|
||||
fn main() {
|
||||
let a = Node { peers: multi_new(), p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0, p21: 0, p22: 0, p23: 0, p24: 0, p25: 0, p26: 0, p27: 0, p28: 0, p29: 0, p30: 0, p31: 0, p32: 0, p33: 0, p34: 0, p35: 0, p36: 0, p37: 0, p38: 0, p39: 0, p40: 0, p41: 0, p42: 0, p43: 0, p44: 0, p45: 0, p46: 0, p47: 0, p48: 0, p49: 0, p50: 0, p51: 0, p52: 0, p53: 0, p54: 0, p55: 0, p56: 0, p57: 0, p58: 0, p59: 0, p60: 0, p61: 0, p62: 0, p63: 0, p64: 0, p65: 0, p66: 0, p67: 0, p68: 0, p69: 0, p70: 0, p71: 0, p72: 0, p73: 0, p74: 0, p75: 0, p76: 0, p77: 0, p78: 0, p79: 0, p80: 0, p81: 0, p82: 0, p83: 0, p84: 0, p85: 0, p86: 0, p87: 0, p88: 0, p89: 0, p90: 0, p91: 0, p92: 0, p93: 0, p94: 0, p95: 0, p96: 0, p97: 0, p98: 0, p99: 0, p100: 0, p101: 0, p102: 0, p103: 0, p104: 0, p105: 0, p106: 0, p107: 0, p108: 0, p109: 0, p110: 0, p111: 0, p112: 0, p113: 0, p114: 0, p115: 0, p116: 0, p117: 0, p118: 0, p119: 0, p120: 0, p121: 0, p122: 0, p123: 0, p124: 0, p125: 0, p126: 0, p127: 0, p128: 0, p129: 0 }
|
||||
let b = Node { peers: multi_new(), p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0, p21: 0, p22: 0, p23: 0, p24: 0, p25: 0, p26: 0, p27: 0, p28: 0, p29: 0, p30: 0, p31: 0, p32: 0, p33: 0, p34: 0, p35: 0, p36: 0, p37: 0, p38: 0, p39: 0, p40: 0, p41: 0, p42: 0, p43: 0, p44: 0, p45: 0, p46: 0, p47: 0, p48: 0, p49: 0, p50: 0, p51: 0, p52: 0, p53: 0, p54: 0, p55: 0, p56: 0, p57: 0, p58: 0, p59: 0, p60: 0, p61: 0, p62: 0, p63: 0, p64: 0, p65: 0, p66: 0, p67: 0, p68: 0, p69: 0, p70: 0, p71: 0, p72: 0, p73: 0, p74: 0, p75: 0, p76: 0, p77: 0, p78: 0, p79: 0, p80: 0, p81: 0, p82: 0, p83: 0, p84: 0, p85: 0, p86: 0, p87: 0, p88: 0, p89: 0, p90: 0, p91: 0, p92: 0, p93: 0, p94: 0, p95: 0, p96: 0, p97: 0, p98: 0, p99: 0, p100: 0, p101: 0, p102: 0, p103: 0, p104: 0, p105: 0, p106: 0, p107: 0, p108: 0, p109: 0, p110: 0, p111: 0, p112: 0, p113: 0, p114: 0, p115: 0, p116: 0, p117: 0, p118: 0, p119: 0, p120: 0, p121: 0, p122: 0, p123: 0, p124: 0, p125: 0, p126: 0, p127: 0, p128: 0, p129: 0 }
|
||||
push(a.peers, b)
|
||||
push(b.peers, a)
|
||||
print_int(count(a.peers))
|
||||
}
|
||||
1
tests/corpus/gc/budget-steps/fixture.gc_budget
Normal file
1
tests/corpus/gc/budget-steps/fixture.gc_budget
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
1
tests/corpus/gc/budget-steps/fixture.out
Normal file
1
tests/corpus/gc/budget-steps/fixture.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
2
tests/corpus/gc/budget-steps/fixture.trace
Normal file
2
tests/corpus/gc/budget-steps/fixture.trace
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
steps=2
|
||||
freed=4
|
||||
157
tests/corpus/gc/budget-steps/fixture.wo
Normal file
157
tests/corpus/gc/budget-steps/fixture.wo
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
-- 129 padding Int fields (p1..p129) push Node's instance size past the
|
||||
-- arena's 1024-byte size-class ceiling (runtime/src/obj.h), so instances
|
||||
-- allocate through plain malloc/free instead of the bump arena -- the
|
||||
-- same trick runtime/test/test_cycle.c uses (BIG=130) so ASan can prove
|
||||
-- a Node is actually freed, not just recycled inside the arena's own
|
||||
-- freelist where a sanitizer can never see it.
|
||||
@gc
|
||||
class Node {
|
||||
peers: multi Node
|
||||
p1: Int
|
||||
p2: Int
|
||||
p3: Int
|
||||
p4: Int
|
||||
p5: Int
|
||||
p6: Int
|
||||
p7: Int
|
||||
p8: Int
|
||||
p9: Int
|
||||
p10: Int
|
||||
p11: Int
|
||||
p12: Int
|
||||
p13: Int
|
||||
p14: Int
|
||||
p15: Int
|
||||
p16: Int
|
||||
p17: Int
|
||||
p18: Int
|
||||
p19: Int
|
||||
p20: Int
|
||||
p21: Int
|
||||
p22: Int
|
||||
p23: Int
|
||||
p24: Int
|
||||
p25: Int
|
||||
p26: Int
|
||||
p27: Int
|
||||
p28: Int
|
||||
p29: Int
|
||||
p30: Int
|
||||
p31: Int
|
||||
p32: Int
|
||||
p33: Int
|
||||
p34: Int
|
||||
p35: Int
|
||||
p36: Int
|
||||
p37: Int
|
||||
p38: Int
|
||||
p39: Int
|
||||
p40: Int
|
||||
p41: Int
|
||||
p42: Int
|
||||
p43: Int
|
||||
p44: Int
|
||||
p45: Int
|
||||
p46: Int
|
||||
p47: Int
|
||||
p48: Int
|
||||
p49: Int
|
||||
p50: Int
|
||||
p51: Int
|
||||
p52: Int
|
||||
p53: Int
|
||||
p54: Int
|
||||
p55: Int
|
||||
p56: Int
|
||||
p57: Int
|
||||
p58: Int
|
||||
p59: Int
|
||||
p60: Int
|
||||
p61: Int
|
||||
p62: Int
|
||||
p63: Int
|
||||
p64: Int
|
||||
p65: Int
|
||||
p66: Int
|
||||
p67: Int
|
||||
p68: Int
|
||||
p69: Int
|
||||
p70: Int
|
||||
p71: Int
|
||||
p72: Int
|
||||
p73: Int
|
||||
p74: Int
|
||||
p75: Int
|
||||
p76: Int
|
||||
p77: Int
|
||||
p78: Int
|
||||
p79: Int
|
||||
p80: Int
|
||||
p81: Int
|
||||
p82: Int
|
||||
p83: Int
|
||||
p84: Int
|
||||
p85: Int
|
||||
p86: Int
|
||||
p87: Int
|
||||
p88: Int
|
||||
p89: Int
|
||||
p90: Int
|
||||
p91: Int
|
||||
p92: Int
|
||||
p93: Int
|
||||
p94: Int
|
||||
p95: Int
|
||||
p96: Int
|
||||
p97: Int
|
||||
p98: Int
|
||||
p99: Int
|
||||
p100: Int
|
||||
p101: Int
|
||||
p102: Int
|
||||
p103: Int
|
||||
p104: Int
|
||||
p105: Int
|
||||
p106: Int
|
||||
p107: Int
|
||||
p108: Int
|
||||
p109: Int
|
||||
p110: Int
|
||||
p111: Int
|
||||
p112: Int
|
||||
p113: Int
|
||||
p114: Int
|
||||
p115: Int
|
||||
p116: Int
|
||||
p117: Int
|
||||
p118: Int
|
||||
p119: Int
|
||||
p120: Int
|
||||
p121: Int
|
||||
p122: Int
|
||||
p123: Int
|
||||
p124: Int
|
||||
p125: Int
|
||||
p126: Int
|
||||
p127: Int
|
||||
p128: Int
|
||||
p129: Int
|
||||
}
|
||||
|
||||
-- Two INDEPENDENT 2-cycles (a<->b, c<->d), both abandoned. With
|
||||
-- WO_GC_BUDGET=2 (fixture.gc_budget) each step's whole-component budget
|
||||
-- covers exactly one 2-node cycle (test_budget_one_cycle_per_step in
|
||||
-- runtime/test/test_cycle.c proves this at the collector level) -- the
|
||||
-- pump must take two bounded steps to clear four objects, never one
|
||||
-- unbounded sweep.
|
||||
fn main() {
|
||||
let a = Node { peers: multi_new(), p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0, p21: 0, p22: 0, p23: 0, p24: 0, p25: 0, p26: 0, p27: 0, p28: 0, p29: 0, p30: 0, p31: 0, p32: 0, p33: 0, p34: 0, p35: 0, p36: 0, p37: 0, p38: 0, p39: 0, p40: 0, p41: 0, p42: 0, p43: 0, p44: 0, p45: 0, p46: 0, p47: 0, p48: 0, p49: 0, p50: 0, p51: 0, p52: 0, p53: 0, p54: 0, p55: 0, p56: 0, p57: 0, p58: 0, p59: 0, p60: 0, p61: 0, p62: 0, p63: 0, p64: 0, p65: 0, p66: 0, p67: 0, p68: 0, p69: 0, p70: 0, p71: 0, p72: 0, p73: 0, p74: 0, p75: 0, p76: 0, p77: 0, p78: 0, p79: 0, p80: 0, p81: 0, p82: 0, p83: 0, p84: 0, p85: 0, p86: 0, p87: 0, p88: 0, p89: 0, p90: 0, p91: 0, p92: 0, p93: 0, p94: 0, p95: 0, p96: 0, p97: 0, p98: 0, p99: 0, p100: 0, p101: 0, p102: 0, p103: 0, p104: 0, p105: 0, p106: 0, p107: 0, p108: 0, p109: 0, p110: 0, p111: 0, p112: 0, p113: 0, p114: 0, p115: 0, p116: 0, p117: 0, p118: 0, p119: 0, p120: 0, p121: 0, p122: 0, p123: 0, p124: 0, p125: 0, p126: 0, p127: 0, p128: 0, p129: 0 }
|
||||
let b = Node { peers: multi_new(), p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0, p21: 0, p22: 0, p23: 0, p24: 0, p25: 0, p26: 0, p27: 0, p28: 0, p29: 0, p30: 0, p31: 0, p32: 0, p33: 0, p34: 0, p35: 0, p36: 0, p37: 0, p38: 0, p39: 0, p40: 0, p41: 0, p42: 0, p43: 0, p44: 0, p45: 0, p46: 0, p47: 0, p48: 0, p49: 0, p50: 0, p51: 0, p52: 0, p53: 0, p54: 0, p55: 0, p56: 0, p57: 0, p58: 0, p59: 0, p60: 0, p61: 0, p62: 0, p63: 0, p64: 0, p65: 0, p66: 0, p67: 0, p68: 0, p69: 0, p70: 0, p71: 0, p72: 0, p73: 0, p74: 0, p75: 0, p76: 0, p77: 0, p78: 0, p79: 0, p80: 0, p81: 0, p82: 0, p83: 0, p84: 0, p85: 0, p86: 0, p87: 0, p88: 0, p89: 0, p90: 0, p91: 0, p92: 0, p93: 0, p94: 0, p95: 0, p96: 0, p97: 0, p98: 0, p99: 0, p100: 0, p101: 0, p102: 0, p103: 0, p104: 0, p105: 0, p106: 0, p107: 0, p108: 0, p109: 0, p110: 0, p111: 0, p112: 0, p113: 0, p114: 0, p115: 0, p116: 0, p117: 0, p118: 0, p119: 0, p120: 0, p121: 0, p122: 0, p123: 0, p124: 0, p125: 0, p126: 0, p127: 0, p128: 0, p129: 0 }
|
||||
push(a.peers, b)
|
||||
push(b.peers, a)
|
||||
let c = Node { peers: multi_new(), p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0, p21: 0, p22: 0, p23: 0, p24: 0, p25: 0, p26: 0, p27: 0, p28: 0, p29: 0, p30: 0, p31: 0, p32: 0, p33: 0, p34: 0, p35: 0, p36: 0, p37: 0, p38: 0, p39: 0, p40: 0, p41: 0, p42: 0, p43: 0, p44: 0, p45: 0, p46: 0, p47: 0, p48: 0, p49: 0, p50: 0, p51: 0, p52: 0, p53: 0, p54: 0, p55: 0, p56: 0, p57: 0, p58: 0, p59: 0, p60: 0, p61: 0, p62: 0, p63: 0, p64: 0, p65: 0, p66: 0, p67: 0, p68: 0, p69: 0, p70: 0, p71: 0, p72: 0, p73: 0, p74: 0, p75: 0, p76: 0, p77: 0, p78: 0, p79: 0, p80: 0, p81: 0, p82: 0, p83: 0, p84: 0, p85: 0, p86: 0, p87: 0, p88: 0, p89: 0, p90: 0, p91: 0, p92: 0, p93: 0, p94: 0, p95: 0, p96: 0, p97: 0, p98: 0, p99: 0, p100: 0, p101: 0, p102: 0, p103: 0, p104: 0, p105: 0, p106: 0, p107: 0, p108: 0, p109: 0, p110: 0, p111: 0, p112: 0, p113: 0, p114: 0, p115: 0, p116: 0, p117: 0, p118: 0, p119: 0, p120: 0, p121: 0, p122: 0, p123: 0, p124: 0, p125: 0, p126: 0, p127: 0, p128: 0, p129: 0 }
|
||||
let d = Node { peers: multi_new(), p1: 0, p2: 0, p3: 0, p4: 0, p5: 0, p6: 0, p7: 0, p8: 0, p9: 0, p10: 0, p11: 0, p12: 0, p13: 0, p14: 0, p15: 0, p16: 0, p17: 0, p18: 0, p19: 0, p20: 0, p21: 0, p22: 0, p23: 0, p24: 0, p25: 0, p26: 0, p27: 0, p28: 0, p29: 0, p30: 0, p31: 0, p32: 0, p33: 0, p34: 0, p35: 0, p36: 0, p37: 0, p38: 0, p39: 0, p40: 0, p41: 0, p42: 0, p43: 0, p44: 0, p45: 0, p46: 0, p47: 0, p48: 0, p49: 0, p50: 0, p51: 0, p52: 0, p53: 0, p54: 0, p55: 0, p56: 0, p57: 0, p58: 0, p59: 0, p60: 0, p61: 0, p62: 0, p63: 0, p64: 0, p65: 0, p66: 0, p67: 0, p68: 0, p69: 0, p70: 0, p71: 0, p72: 0, p73: 0, p74: 0, p75: 0, p76: 0, p77: 0, p78: 0, p79: 0, p80: 0, p81: 0, p82: 0, p83: 0, p84: 0, p85: 0, p86: 0, p87: 0, p88: 0, p89: 0, p90: 0, p91: 0, p92: 0, p93: 0, p94: 0, p95: 0, p96: 0, p97: 0, p98: 0, p99: 0, p100: 0, p101: 0, p102: 0, p103: 0, p104: 0, p105: 0, p106: 0, p107: 0, p108: 0, p109: 0, p110: 0, p111: 0, p112: 0, p113: 0, p114: 0, p115: 0, p116: 0, p117: 0, p118: 0, p119: 0, p120: 0, p121: 0, p122: 0, p123: 0, p124: 0, p125: 0, p126: 0, p127: 0, p128: 0, p129: 0 }
|
||||
push(c.peers, d)
|
||||
push(d.peers, c)
|
||||
print_int(count(a.peers) + count(c.peers))
|
||||
}
|
||||
0
tests/corpus/lang/.gitkeep
Normal file
0
tests/corpus/lang/.gitkeep
Normal file
0
tests/corpus/run/.gitkeep
Normal file
0
tests/corpus/run/.gitkeep
Normal file
2
tests/corpus/run/arithmetic/fixture.out
Normal file
2
tests/corpus/run/arithmetic/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
14
|
||||
done
|
||||
28
tests/corpus/run/arithmetic/fixture.wo
Normal file
28
tests/corpus/run/arithmetic/fixture.wo
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- Seed fixture: arithmetic (+ - * / %, unary -) and control flow (if,
|
||||
-- while, comparisons including the two operators the v1 instruction set
|
||||
-- has no opcode for: `%` lowers to `a - (a / b) * b`, `!=` lowers to
|
||||
-- `(a == b) == 0` -- see docs/plan/oop-vm/08-builtin-surface.md.
|
||||
fn compute(a: Int, b: Int) -> Int {
|
||||
let sum = a + b
|
||||
let diff = a - b
|
||||
let prod = sum * diff
|
||||
let quot = prod / b
|
||||
let rem = prod % b
|
||||
let neg = -rem
|
||||
if sum == diff {
|
||||
return neg
|
||||
}
|
||||
let changed = sum != diff
|
||||
while changed {
|
||||
changed = false
|
||||
}
|
||||
if sum > diff {
|
||||
return quot + rem
|
||||
}
|
||||
return neg
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_int(compute(7, 3))
|
||||
print("done")
|
||||
}
|
||||
2
tests/corpus/run/hello/fixture.out
Normal file
2
tests/corpus/run/hello/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
hello
|
||||
42
|
||||
5
tests/corpus/run/hello/fixture.wo
Normal file
5
tests/corpus/run/hello/fixture.wo
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-- Seed fixture: print/print_int, the smallest possible run/ case.
|
||||
fn main() {
|
||||
print("hello")
|
||||
print_int(42)
|
||||
}
|
||||
2
tests/corpus/run/interface/fixture.out
Normal file
2
tests/corpus/run/interface/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
12
|
||||
15
|
||||
34
tests/corpus/run/interface/fixture.wo
Normal file
34
tests/corpus/run/interface/fixture.wo
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- Seed fixture: structural interface dispatch. Book and Toy satisfy
|
||||
-- Priced purely by having the method (no `implements` keyword by
|
||||
-- doctrine); `quote`'s call through the interface-typed parameter is
|
||||
-- ICALL by global vtable slot, not a direct method-index CALL.
|
||||
interface Priced {
|
||||
fn current_price() -> Int
|
||||
}
|
||||
|
||||
class Book {
|
||||
base: Int
|
||||
|
||||
fn current_price() -> Int {
|
||||
return self.base + 2
|
||||
}
|
||||
}
|
||||
|
||||
class Toy {
|
||||
base: Int
|
||||
|
||||
fn current_price() -> Int {
|
||||
return self.base * 3
|
||||
}
|
||||
}
|
||||
|
||||
fn quote(p: Priced) -> Int {
|
||||
return p.current_price()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let b = Book { base: 10 }
|
||||
let t = Toy { base: 5 }
|
||||
print_int(quote(b))
|
||||
print_int(quote(t))
|
||||
}
|
||||
2
tests/corpus/run/methods/fixture.out
Normal file
2
tests/corpus/run/methods/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
15
|
||||
42
|
||||
16
tests/corpus/run/methods/fixture.wo
Normal file
16
tests/corpus/run/methods/fixture.wo
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- Seed fixture: a direct method call (receiver's declared type is a
|
||||
-- concrete class, so the emitter dispatches by CALL/method index, not
|
||||
-- ICALL/vtable -- contrast with run/interface).
|
||||
class Counter {
|
||||
n: Int
|
||||
|
||||
fn add(amount: Int) -> Int {
|
||||
return self.n + amount
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let c = Counter { n: 10 }
|
||||
print_int(c.add(5))
|
||||
print_int(c.add(32))
|
||||
}
|
||||
6
tests/corpus/run/pricing-containers/fixture.out
Normal file
6
tests/corpus/run/pricing-containers/fixture.out
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
3
|
||||
20
|
||||
2
|
||||
499
|
||||
1
|
||||
0
|
||||
31
tests/corpus/run/pricing-containers/fixture.wo
Normal file
31
tests/corpus/run/pricing-containers/fixture.wo
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- Pricing-demo corpus (plan 3, Task 3): container round-trips over the
|
||||
-- two container shapes the pricing demo's classes use -- a `multi`
|
||||
-- (docs/examples/pricing/types/product.wo's `prices: multi Price`) and a
|
||||
-- Text-keyed `map` (a SKU->price catalog, using `Text` directly since
|
||||
-- `SKU` was removed from the language on 2026-08-10). Exercises
|
||||
-- multi push/count/get and map set/get/has, per docs/plan/oop-vm/
|
||||
-- 08-builtin-surface.md's builtin table.
|
||||
class Item {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Catalog {
|
||||
items: multi Item
|
||||
by_name: map<Text, Int>
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let c = Catalog { items: multi_new(), by_name: map_new() }
|
||||
push(c.items, Item { n: 10 })
|
||||
push(c.items, Item { n: 20 })
|
||||
push(c.items, Item { n: 30 })
|
||||
print_int(count(c.items))
|
||||
print_int(c.items[1].n)
|
||||
|
||||
set(c.by_name, "mug", 499)
|
||||
set(c.by_name, "shirt", 1999)
|
||||
print_int(count(c.by_name))
|
||||
print_int(c.by_name["mug"])
|
||||
print_int(has(c.by_name, "shirt"))
|
||||
print_int(has(c.by_name, "hat"))
|
||||
}
|
||||
1
tests/corpus/run/pricing-current-price/fixture.out
Normal file
1
tests/corpus/run/pricing-current-price/fixture.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
150
|
||||
31
tests/corpus/run/pricing-current-price/fixture.wo
Normal file
31
tests/corpus/run/pricing-current-price/fixture.wo
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-- Pricing-demo corpus (plan 3, Task 3): mirrors
|
||||
-- docs/examples/pricing/types/product.wo's `current_price` --
|
||||
-- `latest(self.prices).amount` through a `multi Price` field. Trimmed for
|
||||
-- milestone-1 grammar: `@unique` on `sku`, the `id`/`sku`/`name` fields
|
||||
-- (only `prices` is what `current_price` reads), `in txn` (no txn
|
||||
-- keyword in this grammar), and the `service rest` block. `Money`/`SKU`
|
||||
-- were removed from the language on 2026-08-10; nothing here needed
|
||||
-- them. (There is no `@table`, `owner`, or `Customer` in the original --
|
||||
-- Product has exactly id/sku/name/prices; an earlier draft of this
|
||||
-- comment wrongly borrowed fields from the unrelated, non-emittable
|
||||
-- compiler/test/golden/owner/pricing-demo.wo instead.)
|
||||
class Price {
|
||||
amount: Int
|
||||
}
|
||||
|
||||
class Product {
|
||||
prices: multi Price
|
||||
|
||||
fn current_price() -> Int {
|
||||
return latest(self.prices).amount
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
-- Fresh-container destination rule (08-builtin-surface.md): multi_new()
|
||||
-- must land directly in a field of declared container type.
|
||||
let prod = Product { prices: multi_new() }
|
||||
push(prod.prices, Price { amount: 100 })
|
||||
push(prod.prices, Price { amount: 150 })
|
||||
print_int(prod.current_price())
|
||||
}
|
||||
2
tests/corpus/run/pricing-discounted/fixture.out
Normal file
2
tests/corpus/run/pricing-discounted/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
150
|
||||
200
|
||||
22
tests/corpus/run/pricing-discounted/fixture.wo
Normal file
22
tests/corpus/run/pricing-discounted/fixture.wo
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- Pricing-demo corpus (plan 3, Task 3): mirrors
|
||||
-- docs/examples/pricing/types/price.wo's `discounted` -- pure arithmetic,
|
||||
-- no txn machinery. Trimmed for milestone-1 grammar (docs/plan/oop-vm/
|
||||
-- 08-builtin-surface.md): `@table`, `id`/`product: ref Product`/
|
||||
-- `currency`/`at: Timestamp = now()` fields and the `service rest` block
|
||||
-- are all schema/runtime-layer surface the milestone-1 parser has no
|
||||
-- lowering for -- only the field `discounted` actually reads (`amount`)
|
||||
-- is kept. `Money` was removed from the language on 2026-08-10, so
|
||||
-- `amount` is a plain `Int` (minor units), same value the original used.
|
||||
class Price {
|
||||
amount: Int
|
||||
|
||||
fn discounted(pct: Int) -> Int {
|
||||
return self.amount * (100 - pct) / 100
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let p = Price { amount: 200 }
|
||||
print_int(p.discounted(25))
|
||||
print_int(p.discounted(0))
|
||||
}
|
||||
2
tests/corpus/run/pricing-text/fixture.out
Normal file
2
tests/corpus/run/pricing-text/fixture.out
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
writeonce ceramic mug
|
||||
3
|
||||
12
tests/corpus/run/pricing-text/fixture.wo
Normal file
12
tests/corpus/run/pricing-text/fixture.wo
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Pricing-demo corpus (plan 3, Task 3): text builtins -- `words`
|
||||
-- (whitespace token count) and `..` (CONCAT; `+` is arithmetic-only,
|
||||
-- never string addition -- docs/plan/oop-vm/08-builtin-surface.md).
|
||||
-- Mirrors the product name text docs/examples/pricing/types/product.wo
|
||||
-- carries (`name: Text`), reduced to what the two builtins need.
|
||||
fn main() {
|
||||
let brand = "writeonce"
|
||||
let noun = "ceramic mug"
|
||||
let full = brand .. " " .. noun
|
||||
print(full)
|
||||
print_int(words(full))
|
||||
}
|
||||
0
tests/corpus/sample-logwatcher/.gitkeep
Normal file
0
tests/corpus/sample-logwatcher/.gitkeep
Normal file
0
tests/corpus/sys/.gitkeep
Normal file
0
tests/corpus/sys/.gitkeep
Normal file
0
tests/corpus/trap/.gitkeep
Normal file
0
tests/corpus/trap/.gitkeep
Normal file
1
tests/corpus/trap/div-by-zero/fixture.trap
Normal file
1
tests/corpus/trap/div-by-zero/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
1
|
||||
21
tests/corpus/trap/div-by-zero/fixture.wo
Normal file
21
tests/corpus/trap/div-by-zero/fixture.wo
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
-- Runtime trap: division by zero. DIV traps unconditionally on a zero
|
||||
-- divisor (docs/plan/oop-vm/08-builtin-surface.md, docs/plan/oop-vm/
|
||||
-- 00-wob-format.md) -- unprovable at compile time in general (the
|
||||
-- divisor is a runtime value; milestone-1 does no constant-folding
|
||||
-- either), so it traps rather than failing to compile.
|
||||
--
|
||||
-- Line assertion (harness's fixture.trap only carries the numeric code,
|
||||
-- not the line -- verified manually per this task's brief): wovm must
|
||||
-- report this trap at line 12, the `a / b` below. Proves line tables
|
||||
-- survive emission. The machine-enforced coverage for pc->line survival
|
||||
-- lives elsewhere, not in this corpus fixture: compiler/test/golden/bc's
|
||||
-- goldens pin the emitted line table, and runtime/test/test_vm.c's
|
||||
-- test_div_zero_traps_with_line asserts wovm reports the exact line for
|
||||
-- this same DIV0 case.
|
||||
fn divide(a: Int, b: Int) -> Int {
|
||||
return a / b
|
||||
}
|
||||
|
||||
fn main() {
|
||||
print_int(divide(10, 0))
|
||||
}
|
||||
1
tests/corpus/trap/exclusive-borrow-alias/fixture.trap
Normal file
1
tests/corpus/trap/exclusive-borrow-alias/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
28
tests/corpus/trap/exclusive-borrow-alias/fixture.wo
Normal file
28
tests/corpus/trap/exclusive-borrow-alias/fixture.wo
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- Canary fixture (Task 2 review, Important 2): a real runtime trap,
|
||||
-- proving the trap/ path end to end. `pair` takes two exclusive borrows
|
||||
-- through runtime indices (docs/plan/oop-vm/08-builtin-surface.md's
|
||||
-- residual-borrow case); calling it with i == j aliases the same
|
||||
-- element under two exclusive borrows, which the VM's runtime guard
|
||||
-- traps rather than silently corrupting.
|
||||
class Item {
|
||||
n: Int
|
||||
}
|
||||
|
||||
class Bag {
|
||||
items: multi Item
|
||||
}
|
||||
|
||||
fn touch(mut a: Item, mut b: Item) -> Int {
|
||||
return a.n + b.n
|
||||
}
|
||||
|
||||
fn pair(mut bag: Bag, i: Int, j: Int) -> Int {
|
||||
return touch(bag.items[i], bag.items[j])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let bag = Bag { items: multi_new() }
|
||||
push(bag.items, Item { n: 1 })
|
||||
push(bag.items, Item { n: 2 })
|
||||
print_int(pair(bag, 0, 0))
|
||||
}
|
||||
1
tests/corpus/trap/missing-map-key/fixture.trap
Normal file
1
tests/corpus/trap/missing-map-key/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
7
|
||||
14
tests/corpus/trap/missing-map-key/fixture.wo
Normal file
14
tests/corpus/trap/missing-map-key/fixture.wo
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- Runtime residual check: `get` on a `map` with a key that isn't present
|
||||
-- traps KEY (docs/plan/oop-vm/08-builtin-surface.md's `get` row: "a
|
||||
-- missing key traps KEY"). Unprovable at compile time -- the key is a
|
||||
-- runtime `Text` value -- so it traps rather than failing to compile:
|
||||
-- the hybrid boundary's runtime half.
|
||||
class Catalog {
|
||||
prices: map<Text, Int>
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let c = Catalog { prices: map_new() }
|
||||
set(c.prices, "mug", 499)
|
||||
print_int(c.prices["shirt"])
|
||||
}
|
||||
1
tests/corpus/trap/pricing-set-price-db-stub/fixture.trap
Normal file
1
tests/corpus/trap/pricing-set-price-db-stub/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
5
|
||||
29
tests/corpus/trap/pricing-set-price-db-stub/fixture.wo
Normal file
29
tests/corpus/trap/pricing-set-price-db-stub/fixture.wo
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- Pricing-demo corpus (plan 3, Task 3): mirrors
|
||||
-- docs/examples/pricing/types/product.wo's `set_price` -- the write path
|
||||
-- whose `insert Price { ... }` is the SQL sublanguage's one opaque node
|
||||
-- (compiler/src/parser.ml's `insert`/`select` "parses but traps"
|
||||
-- contract) and lowers to a single DB_STUB opcode (docs/plan/oop-vm/
|
||||
-- 00-wob-format.md), which traps WO_T_DB unconditionally ("engine not
|
||||
-- linked") -- the spec's parse-but-trap story, proven end to end.
|
||||
-- Trimmed for milestone-1 grammar: `in txn`, `assert ... otherwise
|
||||
-- abort` (no assert/otherwise/abort keywords in this grammar), and the
|
||||
-- `service rest` block -- only the `insert` statement `set_price`
|
||||
-- actually needs to prove the trap is kept. `self.id` became plain
|
||||
-- `self` since the `id` field itself was trimmed; it doesn't matter to
|
||||
-- the trap -- `insert`'s body is never re-parsed, only captured verbatim.
|
||||
class Price {
|
||||
amount: Int
|
||||
}
|
||||
|
||||
class Product {
|
||||
prices: multi Price
|
||||
|
||||
fn set_price(amount: Int) {
|
||||
insert Price { product: self, amount: amount }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let prod = Product { prices: multi_new() }
|
||||
prod.set_price(4999)
|
||||
}
|
||||
1
tests/corpus/trap/unsatisfied-interface/fixture.trap
Normal file
1
tests/corpus/trap/unsatisfied-interface/fixture.trap
Normal file
|
|
@ -0,0 +1 @@
|
|||
6
|
||||
34
tests/corpus/trap/unsatisfied-interface/fixture.wo
Normal file
34
tests/corpus/trap/unsatisfied-interface/fixture.wo
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- KNOWN GAP, pinned deliberately (Task 4 review, Important 2). `Rock`
|
||||
-- does not have a `current_price` method, so it does not structurally
|
||||
-- satisfy `Priced` -- and its full method set is known at compile time,
|
||||
-- so this violation IS provable statically. By the hybrid-boundary
|
||||
-- doctrine (provable -> compile-time, unprovable -> runtime) this ought
|
||||
-- to be WO-E205 (unsatisfied-interface) at the `quote(r)` call site.
|
||||
--
|
||||
-- It isn't: WO-E205 is declared in types.ml but has no call site today
|
||||
-- (docs/plan/oop-vm/01-error-catalog.md). This compiles clean (exit 0,
|
||||
-- zero diagnostics) and the unsatisfied call instead reaches `wovm` as
|
||||
-- an `ICALL` with no matching vtable entry, which traps `WO_T_BOUNDS`
|
||||
-- (6) -- "no vtable entry for receiver class". That is the CURRENT,
|
||||
-- observed behavior this fixture pins, not the desired one.
|
||||
--
|
||||
-- When WO-E205 is implemented, this exact program must start failing to
|
||||
-- *compile* instead -- move this fixture to compile-fail/ with
|
||||
-- fixture.code WO-E205 in the same change that wires the check, rather
|
||||
-- than leaving a stale trap/ fixture silently describing dead behavior.
|
||||
interface Priced {
|
||||
fn current_price() -> Int
|
||||
}
|
||||
|
||||
class Rock {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn quote(p: Priced) -> Int {
|
||||
return p.current_price()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let r = Rock { n: 1 }
|
||||
print_int(quote(r))
|
||||
}
|
||||
Loading…
Reference in a new issue