writeonce/docs/superpowers/specs/2026-08-01-systems-track-design.md
shoney.arickathil 2de724df11 feat(compiler): MVS ownership pass + woc driver; drop Money/SKU/Float, reject abstract
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).
2026-08-10 21:24:50 +02:00

12 KiB

writeonce systems track — Haxe keyword study, program mode, systems stdlib (design)

Date: 2026-08-01 Status: approved design, pre-implementation Companion spec: 2026-08-01-oop-compiler-vm-design.md (the OOP core this track extends) Driving workload: ~/projects/log-watcher — ~1,200 lines of Haxe compiled to C++ (--cpp), a single-binary systems daemon: log-tail watcher, cron.d supervisor, flock/pgrep probes, hand-rolled MCP-over-HTTP server, JSONL detection sink.

Motivation

writeonce today can only be a full-stack server. log-watcher is the counterexample class: a CLI daemon that reads files, probes processes, serves a small TCP protocol, and sleeps in a poll loop. The language should build such applications too. Method: study the language log-watcher is written in — Haxe — keyword by keyword, adopt broadly what fits, reject explicitly what breaks doctrine, and prove the result by re-expressing log-watcher in .wo.

Decisions locked during brainstorming

Question Decision
"Hexa" meaning Haxe — log-watcher's language.
Deliverable Spec + .wo sample workload. Repo pattern: samples force the grammar (blog/ecommerce/pricing precedent).
Adoption stance Broad Haxe parity MINUS doctrine breakers. No inheritance (extends/override/super), no Dynamic/untyped, no macros — plan-13 doctrine and the OOP spec stay locked. Everything else adopts liberally.
Systems access Program mode + safe builtin stdlib. No FFI/extern — capabilities are typed builtins implemented in the C runtime.

Part 1 — The Haxe keyword verdict table

Every Haxe keyword (plus the contextual ones), one verdict each: have (writeonce equivalent exists), adopt (new surface this track adds), reject (with the reason). This table is normative for the plan documents.

Haxe keyword Verdict writeonce mapping / reason
class have class (state + methods, no hierarchy)
interface have structural interfaces (OOP spec section 3)
function have fn
var (locals) have let; mutability via MVS rules, no second keyword
this have self (identifier, positionally bound)
if / else have same
for / in have same
while have same
return have same
true / false have same
enum have+adopt tagged unions exist; adopt payload variants (Pending | Failed(reason: Text)) with exhaustive switch
final have MVS immutability by default; const for named constants
new have constructor brace literal Type { … }; no keyword
switch / case / default adopt expression-form switch, exhaustive over unions; default optional when exhaustive
typedef adopt structural record aliases with optional fields (?field) — the SupConfig/TailState pattern
null / Null<T> adopt ?T optional types; forced handling before use (no nil deref trap possible); bare null only assignable to ?T
try / catch / throw adopt expression-form over the trap system: catch binds the structured error {code, method, line, msg}; throw value raises an EXPLICIT trap carrying the value; uncaught = existing trap surface
break / continue adopt loop control
do (do-while) adopt parity, trivial
static adopt class-level fn/const — namespaced functions without instances (Flock.held, Pgrep.alive pattern)
abstract reject a distinct scalar type adds a conversion surface without buying safety this language needs; domain scalars are plain Int/Text
using adopt static extension methods — doctrine-safe reuse (composition sugar, the inheritance substitute)
import / package adopt use + directory-as-module; stdlib namespaces (fs, proc, net, time, env, json)
is adopt runtime type test restricted to union variants and interface values; a compile error on statically-known types
inline adopt (values) const compile-time values; inline functions rejected — optimization is the compiler's job
public / private adopt (as pub) default private; pub exports; property-accessor pattern (default, null) becomes pub(read) — public read, owner-only write
#if / #else / #end adopt build-flag conditional compilation only (the -D portable pattern); flags from the build command, no expression language beyond flag names
string interpolation '${}' adopt in string literals
extends reject no-inheritance doctrine (plan 13, OOP spec) — is-a via unions, has-a via composition
super reject no hierarchy to call up
override reject nothing to override
overload reject one name, one signature; keeps dispatch and diagnostics simple
implements reject (keyword) satisfaction is structural and implicit; declaring it adds a lie surface
dynamic / Dynamic reject static typing is the VM's foundation (untagged registers); typed json.decode covers the real use
untyped reject no escape hatch from the type system
macro reject kills the fast-compile promise; codegen belongs to wo gen tooling
extern reject no FFI hole in the memory-safety story; capabilities are audited builtins
cast reject no unsafe casts; conversions are typed (abstract from/to, explicit builtins)
operator reject no operator overloading; KISS

Part 2 — Program mode

Entry. A project containing a free fn main(args: multi Text) -> Int compiles as a program; the return value is the exit code. wo run <dir> executes it. woc build produces the single binary (plan-3 trailer mechanics unchanged). A project with service blocks and no main remains a server. Both present: main runs first and decides what to start — exactly log-watcher's shape (watch/run/mcp subcommands selecting the daemon flavor).

Blocking model — the load-bearing decision. Program mode runs one shard and blocking builtins are legal (time.sleep, proc.run, blocking net.accept): the Haxe original is a poll loop around Sys.sleep, and writeonce expresses that directly. Server shards keep the never-block doctrine — the same stdlib calls are loop-integrated (io_uring) there. One API, two execution disciplines, selected by mode. No async/await keyword exists in either mode.

CLI surface. env.args() -> multi Text, env.get(name) -> ?Text, env.exit(code) (never returns), print/print_int (exist) plus print_err; output flushes on newline (the reason for log-watcher's Util.say disappears).

Daemon idiom. while true { …; time.sleep(ms) } is supported and expected. Signals: the runtime owns signalfd (doctrine); SIGTERM/SIGINT set a shutdown flag programs poll via env.stopping() -> Bool. No signal callbacks.

Part 3 — Systems stdlib

Five builtin modules, scoped to what log-watcher's code actually uses. Every handle (file, socket, process) is an owned object whose drop closes it — MVS deterministic destruction is RAII: no close bookkeeping, no leaked fds by construction, and a handle sent nowhere dies at scope end.

Module Surface log-watcher use it covers
fs exists(path); stat(path) -> ?{size, inode, mtime}; read_at(path, offset, max) -> Text; read_all(path, cap); append(path, text); list(dir) -> multi Text rotation detection needs the inode; bounded tail-chunk reads (never front-to-back scans); JSONL detection sink (open-append-close); cron.d directory scan. No write/truncate/delete in v1 — the read-only posture is the default posture.
proc run(cmd, args: multi Text) -> {code: Int, out: Text, err: Text}, bounded capture the flock -n exit-code probe and pgrep -f. Args-array only — no shell-string form, command injection unrepresentable.
net listen(addr, port) -> Listener; accept(listener) -> Conn; read(conn, max) -> Text; write(conn, text) the hand-rolled MCP HTTP subset (127.0.0.1 accept loop, one request per connection). TCP only in v1.
time now() wall ms (exists); mono() monotonic ms; sleep(ms) poll-interval math on a monotonic clock; the daemon sleep.
json json.decode(text) as RecordType -> ?RecordType; json.encode(value) -> Text config loading and JSON-RPC — typed, replacing Haxe's Dynamic idiom: missing optional fields are fine, shape mismatches yield nil, never a trap. The as here is the decode-target position only — a checked conversion returning ?T, not a cast; it exists nowhere else (the cast rejection stands). Reuses the HTTP plan's C codec as builtins.

Part 4 — The sample workload

docs/examples/log-watcher/ — the Haxe original re-expressed in .wo, file-for-file:

.wo file .hx sibling carries
main.wo Main.hx subcommand dispatch, config decode into a typedef record with ?fields
watcher.wo, logtail.wo Watcher.hx, LogTail.hx TailState record, bounded tail reads, rotation-by-inode, quiet-period rule
cron.wo Cron.hx cron.d parse, next-fire computation
probes.wo Flock.hx, Pgrep.hx proc.run exit-code probes as static fns
mcp.wo Mcp.hx, Tools.hx typed request/response records; pure handle(req) -> resp kept socket-free (the original's best design decision, preserved); serve loop over net

A README table records the mapping and what (if anything) each file could not express — an empty "could not express" column is this track's acceptance criterion.

Error handling

One system, two surfaces. Traps remain the runtime truth (OOP spec section 6). This track adds the language surface: try expr catch (e) fallback-expr — e is the structured error record; throw value raises EXPLICIT with the value attached. Optionals (?T) handle expected absence (missing file stat, failed decode, missing env var) — the stdlib returns nil for those, reserving traps/throw for genuine faults. The Haxe original's try … catch (e:Dynamic) return false probes become optional-returning calls — clearer than the original.

Testing

  • Language adoptions: each feature lands with conformance-corpus fixtures — golden (runs, expected stdout) and must-fail (expected WO-E###) — extending the OOP track's corpus and error catalog.
  • Stdlib: corpus fixtures against real resources — tempdir files (stat/inode/rotation simulation via rename), spawned /bin/true-class processes, loopback sockets. Handle-drop RAII proven under ASan (a leaked fd test: open many handles in a loop, assert no fd growth).
  • Acceptance: the log-watcher sample compiles; its testable cores (tail state machine, cron next-fire, MCP handle) pass fixtures ported from the Haxe test suite's cases; the sample binary runs watch against a growing tempfile and detects an error-final quiet period.

Success criteria

  1. The keyword table is fully implemented: every adopt row parses, typechecks, and executes with corpus coverage; every reject row has a diagnostic or a documented absence.
  2. fn main program mode: wo run executes a CLI program; exit codes propagate; woc build produces a self-contained binary for it.
  3. All five stdlib modules pass their corpus fixtures; handle RAII is ASan-proven.
  4. docs/examples/log-watcher/ compiles and its README mapping table has an empty "could not express" column.
  5. The sample's watch mode detects a silent death (error-final + quiet period) end to end on a real tempfile.

Out of scope (named)

Threads/worker pools in program mode (the shard-actor track owns concurrency); UDP/TLS; fs mutation beyond append; signal callbacks; sqlite-equivalent embedded SQL over RAM (that is the DB engine's job — a future sample can wire MiniLog's idea to select); Haxe macro-based reflection idioms.