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).
123 lines
12 KiB
Markdown
123 lines
12 KiB
Markdown
# 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`](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 fn`s |
|
|
| `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.
|