- Float full stack: literals (fraction/exponent; `0..10` still a range), f64
opcodes 34-41, @table column, WAL bit-exact replay, json fractions in and
shortest-round-trip out. IEEE-quiet — FDIV never traps where DIV does.
- Bytes: a wo_str with its own class id, so alloc/free/copy are shared but no
Text builtin accepts one; len/at/slice/eq/concat, base64 both ways, json
boundary as base64; TEXT_COPY preserves the kind.
- No implicit Int/Float mixing (WO-E201 in the typechecker, not the emitter,
which picks the opcode from one side and would misread the other).
- One IEEE deviation: float_cmp total order (NaN last, -0.0 == +0.0) for
indexes and order-by, keys canonicalized to match. `?Float` nil is a
reserved quiet NaN — the zero word is +0.0, WO_NIL_SCALAR's bits are -2.0.
- Renderer prefers fixed over exponential in 1e-6..1e21: pure shortest makes
a price of 900.0 read `9e+02`. One renderer for interp/json/float_to_text.
- Fixed en route: lexer double-counted the leading digit; is_scalar_shaped
took Float/Bytes as Int-shaped; Bytes ownership needed a shared heap-scalar
predicate or temps never dropped; order-by bit-compared negatives backwards.
- Iteration 17: `kind = "library"` (absent = program; bad value = WO-E109),
entry-less check mode retiring the `--emit` workaround, Go's `internal/` as
WO-E108 at the consumer's `use`. Driver-only; VM/.wob/GC untouched.
- Framework reorg: internal/{parse,serve}.wo; http/form.wo split out to keep
media_type/form_values public (parse.wo had grown public surface).
- Docs: link audit (97 -> 88 broken, conflict markers resolved, 2 duplicate
stories removed), 00-code-review verified 26/27, iterations re-sequenced.
- Also carries the pre-staged pub(read)/using/#if work from the index.
- Gates: corpus 103/0, test_wal 156/0, web-app 26/0, oop-accept ALL MET.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- docs/examples/web-app: Product (@unique name, backlink orders) / Order
(ref Product) as @table classes; handlers as Handler classes —
ListProducts (ordered query -> JSON array), ShowProduct (unique-index
probe, 404), CreateProduct (checked json.decode -> 400; @unique trap ->
409), CreateOrder (FK insert), DeleteProduct (FK restrict trap -> 409);
Auth middleware reads WA_TOKEN. Entry validates port + token honestly.
- The [deps] KEY is the module name `use` imports: hyphens are not
identifier characters, so the app keys the dep `framework` while the
repository keeps its long name (recorded in the manifest comment).
- driver fix (real gap the chain exposed): a dependency's INTERNAL `use`
paths are written against its own root (`use http` inside the framework)
but compile under `<depname>/...` — compile_image now prefixes dep files'
use paths with the dep name (stdlib namespaces stay bare; a path already
starting with the dep name is untouched).
Verified end to end through the full chain (temp git remote of the
framework, file:// substituted, fetch -> lock -> build -> serve): 401
without the token; [] empty list; 201 create; 409 duplicate (@unique);
400 malformed json; list/show payloads exact; 404 unknown product; 201
order; 409 delete-while-referenced (FK restrict) with the server still
serving; SIGTERM clean; the product survives a process restart (WAL
replay). Gates: woc-test 540/0, oop-e2e 88/0, deps-accept 8/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[deps] entries now resolve to fetched, locked checkouts and compile as
module roots.
- resolve_deps (driver): .wo-deps/<name>/ cache beside wo.toml, wo.lock
pinning name -> commit SHA. Cold+unlocked: clone at the manifest rev,
record HEAD. Cold+locked: clone and checkout the LOCKED sha — a moved tag
cannot change the build. Warm+locked: HEAD==lock -> zero network.
Divergence is WO-E106 "lock drift" naming both SHAs and pointing at
--update-deps; every git failure (missing binary, bad URL, bad rev,
missing locked commit) is WO-E106 naming the dep and step. Guard rails:
fetched dep must be a writeonce project; a dep with its own [deps] is
refused (flat-only); dep name colliding with a local module dir is
WO-E107. All git via the git binary (Sys.command; output reads through a
temp file) — no network code in the compiler. Full clone, not --depth 1
(a locked SHA must be reachable regardless of tag movement) — recorded
deviation from the plan's clone sketch.
- woc --update-deps <dir>: re-fetch at manifest revs, rewrite the lock.
- Multi-root compile: compile_image gains ~deps; app files first then deps
sorted by name; module_of_multi maps a dep file to `<name>` /
`<name>/<sub>`, so existing use/pub/collision machinery works across the
boundary unchanged. The app root's walk skips .wo-deps via the existing
dot-rule.
- Entry restriction: Emit.emit gains ?entry_ok (default true — test helpers
untouched); the driver excludes dep files, so a dependency's fn main is
never the entry.
Verified end to end against local file:// remotes: cold fetch + lock; `use
niceframework` + `use niceframework/strutil` build and run; offline rebuild
with the remote deleted; moved tag -> cold rebuild stays at the locked SHA;
--update-deps follows the tag and rewrites the lock; cache/lock drift,
transitive [deps], and name collision each produce their named diagnostic;
the dep's fn main (returning 99) never becomes the entry. woc-test 540/0;
oop-e2e 87/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The manifest grows [deps]: `name = { git = "...", rev = "..." }` — a
one-line inline table accepted ONLY under [deps] (a tiny scanner, not
split-on-comma: URLs may contain any character). git+rev both mandatory and
non-empty; duplicate dep names, unknown table keys, unquoted values, and
inline tables outside [deps] each keep/get a named diagnostic. Parse-only:
no fetch yet (Task 2).
Verified: well-formed parses; missing-rev / bare-value / outside-deps each
diagnose; woc-test 540/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Extract the structural+demand classification out of main.ml's typecheck_all
into `Gcinfer.infer : parsed -> symbols -> symbols`, so a single library entry
point runs the whole pass. The driver calls it once (before typecheck); the
unit-test helpers can call the same function, so is_gc_class classifies
identically in the binary and in tests (prerequisite for removing the @gc
keyword, whose tests read the golden fixtures through the library directly).
- dune: gcinfer moved after owner (it now runs ownership in collect mode).
- main.ml typecheck_all: the split structural-inject + post-typecheck demand
loop become one `Gcinfer.infer parsed syms` call.
- Documented the known demand-promotion imprecision (promotes the escaping root
local's class, over-promoting the container) to refine with Phase 3.
Behavior-neutral: woc-test 566/0, test_diag 14/0, oop-e2e 79/0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Structural inference (2a) covers cyclic classes; this adds the DEMAND half for
the acyclic-but-aliased case, so @gc is now redundant everywhere.
- owner.ml: a `promote` sink on ctx. In collect mode, a class value that would
raise WO-E304 (escape) records its class instead of erroring — because a
class that MUST escape cannot be owned (second-class borrows can't be stored
or returned), so it must be traced. `class_of_ty` extracts the class from the
escaping local's type. analyze/analyze_fn take ?promote.
- main.ml typecheck_all: after structural injection, a fixpoint runs ownership
in collect mode over every file, unioning promotions into syms.traced (and
every module table), before owner/emit see it. Terminates (promotions only
grow, bounded by class count).
- gcinfer.render_final: --dump-gc now reads the authoritative is_gc_class
(structural + demand + the remaining @gc bridge), with the reason.
Effect: a class that escapes is inferred `gc` with no annotation — e.g. `Cache`
(rc.wo sans @gc) shows `gc (alias escape (demand))`; `Box` returned out of
`leak` is promoted and the program is valid. No false positives: employee's
Department/Employee stay owned; the 566 goldens + 14 test_diag unchanged.
Corpus: compile-fail/borrow-escape-return reclassified to run/ (prints 1) —
returning a borrowed class is now legal under demand promotion; the fixture
encoded pre-7b behavior.
Verified: woc-test 566/0 + test_diag 14/0; oop-e2e 79/0; employee 8/0;
log-watcher 7/0.
NOT in this slice: removing the `@gc` KEYWORD (parser rejection + rewriting the
RC/@gc golden + test_diag assertions + moving the inference injection into the
library so unit tests see it) — coupled to Phase 3, which deletes the RC
machinery those tests cover. The ring still needs Phase 3 to RUN (nullable
`?Node` gcref path).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GC-ness now comes from the inference pass, not only the annotation. A class is
traced if the structural SCC put it in `syms.traced`, OR (temporary bridge
until demand-promotion lands) it still carries `@gc`.
- Types.symbols gains a `traced : StringSet.t`; is_gc_class reads it (union'd
with the surviving @gc annotation). All symbols literals + both merges carry
the field.
- typecheck_all injects the classification once (Gcinfer.classify -> traced)
into the merged table AND every module table, before typecheck/owner/emit.
- emit.ml routes the class gc-flag and the union/drop decision through
is_gc_class instead of the raw `.is_gc`, so structurally-inferred gc classes
get the runtime flag. Field-kind derivation already routed through is_gc_class.
- gcinfer.traced_names exposes the traced set for injection.
Effect: docs/examples/gc-cycle now COMPILES with no annotation (the WO-E301
use-after-move at the ring-closing store is gone) — traced classes alias
freely. Bytecode is byte-identical to writing `@gc class Node`.
Verified: woc-test 566/0 (goldens unchanged — every current @gc class stays gc
via the annotation branch, and no golden has a structural-gc-non-annotated
class); oop-e2e 79/0 (gc corpus green).
Not in this slice: demand-promotion (the acyclic-aliased PriceCache case still
needs the @gc bridge) and @gc-in-source-as-error (Phase 2b); the ring RUNNING
(the RC runtime doesn't implement nullable-gcref `?Node` fields — Phase 3).
WO-W201 still fires on gc-cycle (retired in Phase 4).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Structural half of the inference pass, additive — it backs `woc --dump-gc`
and does NOT yet feed field-kind derivation (that is Phase 2 at the
Types.is_gc_class seam), so no emitted bytecode or golden changes.
- compiler/src/gcinfer.ml: build the class-reference graph (edges from
Scalar/Multi/Map fields, unwrapping ?; ref and backlink contribute NO edge),
run Tarjan SCC, classify any class in a non-trivial SCC or with a self-loop
as `gc`, else `owned`; carry a cycle-path reason.
- --dump-gc mode in bin/main.ml (mirrors --dump-owner) + usage line + dune.
Verified:
- docs/examples/gc-cycle -> `Node gc (cycle Node -> Node)`, `Segment owned`.
- docs/examples/employee -> Department/Employee both `owned` (ref/backlink
make no edge, so no false cycle) — the load-bearing correctness case.
- just woc-test 566/0 (goldens untouched, build clean both flavors).
Remaining Phase 1: a --dump-gc golden fixture (deferred — verified manually to
avoid golden-harness churn this slice). Phases 2-4 per the plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Close the 3 gaps between "builds in the repo" and "installs from a tarball
like Go", so writeonce can ship to other developers.
- VERSION file at repo root single-sources the toolchain version (0.1.0).
- `woc version` -> "writeonce 0.1.0 linux/amd64"; `wovm --version` -> "wovm
0.1.0" (stamped by the Makefile from VERSION; --version handled only in the
plain-wovm path so a built app never shadows its own `version` arg).
- wo.toml `[runtime] wo = ">= X.Y"` is now ENFORCED: woc refuses a project
requiring a newer toolchain than itself (>= and bare version parsed;
unknown operators accepted forward-compatibly). Was parsed-and-ignored.
- woc self-locates wovm: --runtime > [build] runtime > $WO_RUNTIME > a `wovm`
beside the woc binary (Sys.executable_name) > runtime/wovm rel CWD. An
installed woc in <prefix>/bin finds its sibling wovm from any cwd.
- `just dist` (scripts/mkdist.sh) packages writeonce-<ver>-linux-amd64.tar.gz,
Go-shaped (archive root writeonce/, bin/{woc,wovm}, README, VERSION), with a
drift guard asserting VERSION == woc == wovm. dist/ gitignored.
- `just install-accept` (scripts/install-accept.sh) is the gate: extract, PATH,
version, build+run a project from an unrelated cwd, constraint refusal — 6/0.
Verified: install-accept 6/0; woc-test 565/0; wovm suites + cli_smoke;
log-watcher 7/0. Linux-amd64 only (a cross matrix is future work).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- FK restrict: deleting a row a non-nullable `ref` still points at traps
WO_T_FK (11), catchable. The compiler now records a `ref` field's
target class in the class-table field_class metadata; the engine
(wo_row_has_referrers) scans referencing scalar columns before a
delete. Correctness-first full scan; the backlink-index optimization
is recorded for later
- docs/examples/employee now COMPILES AND RUNS all six modes against a
WAL-durable database: seed (+@unique trap across restart), report
(per-dept aggregates + payroll), staff (unique probe + backlink +
ref nav), raise (update-through-row), drop (FK restrict), and
persistence via replay
- group-by SYNTAX parked to a future iteration (user decision): the
report mode is hand-rolled from the shipped primitives meanwhile
(same numbers). "table relations and FK" is complete
- scripts/employee-accept.sh (8 checks) + a `just employee` module;
manifest parser tolerates iteration 9c's [share]/[[share.clients]]
sections so `woc .` builds the sample on this branch
- fixtures trap/db-fk-restrict (code 11) + run/db-fk-restrict-catch;
oop-e2e 79/0, woc-test 566/0, 15 runtime suites, log-watcher 7/0,
employee-accept 8/0
- 9b story + status board updated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- a directory carrying wo.toml is a PROJECT: `woc .` inside it (or
`woc path/to/project` from anywhere) reads the manifest and produces
<target>/<name>, exactly what `woc build <dir> -o ...` produces
- schema is the one the sample already carried -- top-level name/
version/description, [runtime] wo (accepted, not yet enforced) --
plus a new [build] section: runtime (wovm to prepend) and target
(output dir, default "target"), both relative to the manifest's own
directory so the build is invocation-point independent
- unknown keys and sections are hard errors: a typo'd key silently
ignored would build the wrong thing
- directories WITHOUT wo.toml keep check-only semantics -- the corpus
is full of those; oop-e2e 71/0, woc-test 565/0, log-watcher 7/0
- sample's wo.toml gains the [build] section; target/ gitignored
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
log-watcher diagnostics 55 -> 48 (all json-rooted now). corpus 71/0, woc 565/0.
- emit.ml: a field default may now be `nil`, `{}` (a fresh empty container of
the field's declared type) or `Rec {}` (a record built from its own field
defaults) — the workload's `st: TailState = TailState {}` and
`scheduled: map<Text, CronWatch> = {}` shapes
- parser.ml/main.ml: a top-level `const` is visible to every file of its
module, not just its own file — files in a directory are one module by the
discovery contract, and the workload reads logtail.wo's `const CHUNK` from
mcp.wo. A file's own const still wins on a name collision
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VM catch frames + expression-form try/catch in the compiler. Uncaught traps
keep byte-for-byte today's surface. log-watcher parse errors 18 -> 7;
corpus 71/0, woc runtest 565/0, wovm unit gates green (both dispatch flavors).
- wob.h: WOP_TRY (A sBx: push catch frame, handler at pc+sBx) / WOP_ENDTRY;
WO_B_ERR_FILL builtin (fills the catch record: 0 code, 1 line, 2 method,
3 msg — the field-order contract with the compiler)
- vm.h/vm.c: catch stack (depth, handler pc, error reg) + the caught error;
vm_unwind takes a stop depth, so a caught trap kills every frame above the
catching one exactly as an uncaught trap would, then releases only what the
try region owned in the catching frame (drop-entry diff against the handler
pc) and resumes at the handler; RET/RET0 drop the catch frames of the frame
they leave; TRAPF resumes instead of returning when the trap was caught
- builtin.c: err_fill allocates the method/msg Texts into the record the
compiler owns, so the pending error never has to outlive the landing
- loader.c: TRY's handler target validated like a jump, error register like
any register operand; err_fill arity
- lexer/token/ast/parser: `try`/`catch` keywords; `try expr catch (e) expr`
and `catch (e) { block }`, newline allowed before `catch`; try binds looser
than every operator, so `try a / b catch (e) 0` catches the division
- types.ml: predeclared `Error` record (merged table only), catch binding,
arm-type agreement reported only when both arms are confidently typed
- owner.ml: analyze_try — the catch arm is an alternate flow join off the
entry state, the error record is an owned handler-scope local
- emit.ml: TRY/body/ENDTRY/JMP + handler prologue (NEW Error, err_fill),
join drops on both arms, `Error` class entry only for programs that catch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Modules: `use`/`pub`, directory-as-module, per-module symbol resolution (a
flat first-wins merge silently ran the wrong `pub fn` body), six reserved
stdlib namespaces typed UNKNOWN-BUT-RESERVED.
- Surface: `and`/`or` (own precedence tier, short-circuit, Bool-only), `${}`
interpolation desugared at parse time, `const`, break/continue with
drop-correct exits, do-while, inline-fn rejection.
- switch expr/stmt: required `default` over scalars/Text, arm unification,
EQ/EQS+JZ lowering, per-arm drop scopes with N-way JOIN-DROP; `default`
sorted last by a shared lowering order (textual order made arms dead).
- typedef records: structural, same shape = one class entry; `?name: T`
nullable-by-shape; emit_ctor fills omitted defaults; `type` as field name.
- Enum variants: all-bare unions = int ordinals; any-payload = one class
entry per variant, tag IS the header class_id (no header field, no format
bump); exhaustive switch without `default`; arity checked both directions.
- Payload escape modeled as move-out (pointer-kind fields only — a scalar
escape is a copy); caller reaps owned heap temps passed by borrow: two
unbounded LSan-blind leaks, 10.5 MB -> 1.5 MB flat over 300k iterations.
- Fixed en route, each with a RED repro: dead E209 builtin-arg check and
`int_to_text` missing from both types.ml builtin tables (both segfaulted
wovm), multi-file phantom double-report, emit_ctor's field temp clobbering
dst in tail position (pre-existing), warnings swallowed without an error.
- Two fenced VM builtins: `int_to_text` (13), `variant_tag` (14).
- 14+565 unit (was 14+401), corpus 71 (was 32) plain and under wovm_asan,
wovm-test + cli_smoke green. Log-watcher 307 -> 93 diagnostics (85 E101 /
4 E207 / 1 E208 / 3 W202); the 5 non-E101 residuals await Task 7 grammar.
- `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.
Completes plan 2 Tasks 7-8. owner.ml: mutable-value-semantics flow analysis
producing the four plan-3 emitter tables (moves, drops incl. LIVE-MASK for trap
unwinding, rc with elision, residual borrow sites) plus WO-E301-304 two-site
diagnostics. Alias questions run over canonicalized places, so a double-mut
reached through let-bound aliases lands in the residual table like the direct
form; dump.ml's contract notes the emitter must coalesce guards per operand.
main.ml: directory discovery, cross-file programs (symbols merge before bodies
check), diagnostics ordered by (file,line,col), new WO-E214 for a name declared
in two files. New docs/plan/oop-vm/01-error-catalog.md (14 emitted + 10 reserved
codes), un-ignored so both plan tracks can cite it; justfile regains woc-*.
builtin_scalars is now the five that work: Int, Bool, Text, Timestamp, Id.
Money/SKU/Float and the abstract_types allowlist are gone — `abstract` never
lexed, and Float had no literal syntax and no wob kind, so no value could exist.
Fixtures and samples retype Money->Int, SKU->Text. The abstract newtype feature
is rejected outright (verdict row adopt->reject); haxe-parity Task 7 keeps `is`.
nullable-types-implementation.md corrected: ?T is plumbed but UNENFORCED
(E211-213 declared, never emitted; probe exits 0), handed to haxe-parity Task 6
as next work item. Records all 10 dead codes incl. E205 — interface satisfaction
is unchecked. crates/rt keeps its Money/SKU fixtures (opaque strings, Stage 2).
Gate: build warning-clean, 14 + 264 checks 0 failures, pricing golden exit 0,
docs/examples histograms unchanged (13/70, zero WO-E225).