writeonce/docs/superpowers/specs/2026-08-01-systems-track-design.md
shoney.arickathil a55971d857 docs: status board at docs/00-status.md; gap-closure spec applied; recover lost doc
- Board renamed docs/plan/00-kanban.md -> docs/00-status.md and rebuilt: ▶ NEXT
  PLAN pointer (iteration 4 — emitter, corpus, `woc build`) then six buckets —
  stories, in progress, done, pending, discarded, learnings. It covered only the
  Rust runtime before, so the whole OOP track was invisible. All 16 inbound refs
  repointed; `Kanban:` banners renamed to `Status:`.
- New discarded.md (settled rejections with reasons: inheritance, `abstract`,
  Money/SKU/Float, Dynamic/cast/macro/extern, AOT-to-C, Menhir, shared engine
  state) and learnings.md (plumbed≠enforced, vacuous goldens, exit-0-wrong-
  output, malloc-path ASan trick, deferred checks that never reach the VM).
- RECOVERED docs/plan/exploration/blue-green-vm/00-vision.md — gone from disk,
  never committed (gitignored path), cited by five docs incl. principle 12.
  Root cause was broader: all seven forward-roadmap plans in
  docs/superpowers/plans/ were untracked and ignored, on one disk only. Dropped
  the docs ignore rules with a do-not-re-add note; added __pycache__/*.pyc.
- Repaired broken links across docs/, 270 -> 36: fixes a regression from the
  earlier reference/ -> .dev/reference/ move (relative paths at ../../ and
  deeper were skipped), plus depth and reorg drift. The 36 residual point at
  content that does not exist and need decisions, not paths.
- New spec docs/superpowers/specs/2026-08-10-logwatcher-gap-closure-design.md,
  applied: `and`/`or` verdict row; Part 3 gains `env` (six modules), swaps
  time.mono for iso/local, adds 22 bare core builtins; throw/time.mono/is cut
  (0 uses in the sample). Plan 8: Task 2 gains and/or, Task 5 drops throw,
  abstract+`is` task deleted, 8/9 renumber to 7/8. Plan 9 gains core builtins.
  Plan 10 gains the 307 -> 0 diagnostic gate. WO-E205 re-filed unreachable-by-
  design. types.ml header drops its false satisfaction-set claim. 00-code-
  review.md reduced to a stub — its rival Phase 1-4 roadmap retired.
2026-08-10 23:42:26 +02:00

147 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` | **adopt** | expression-form over the trap system: `catch` binds the structured error `{code, method, line, msg}`; uncaught = existing trap surface. `throw` (explicit raise) is **cut** — 0 uses in the driving workload; parked post-iteration-12 |
| `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` | **cut** | runtime type test restricted to union variants and interface values; a compile error on statically-known types — 0 uses in the driving workload; parked post-iteration-12 |
| `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 |
| `&&` / `\|\|` | **adopt** | spelled `and`/`or` (words, not symbols) — the lexer has no `&` case at all (a bare `&` reports `WO-E001`), so words cost nothing to add as keywords and read better in the sample's conditional-heavy code; one new precedence level below comparison and above assignment (`or` binds loosest, then `and`, then comparison, then the arithmetic ladder); short-circuit; `Bool`-typed operands only, `Bool` result, no truthiness; lowers to compare-and-jump on existing opcodes (`JZ` plus a jump), no VM change |
| `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
Six 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 |
| --- | --- | --- |
| `env` | `args()`; `get(name) -> ?Text`; `exit(code)`; `stopping() -> Bool` | CLI subcommand dispatch and exit-code propagation for `main`; `env.get` reads the MCP API key with an environment fallback (1 use); `env.stopping` drives the poll-loop shutdown check (4 uses) — the daemon idiom's exit condition. |
| `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); `sleep(ms)`; `iso(ms) -> Text`; `local(ms) -> {year, month, day, hour, minute, dow}` | the daemon sleep; `iso` gives JSONL detection timestamps and MCP response fields a stable textual instant; `local` gives cron next-fire computation broken-out calendar fields, including day-of-week. `mono()` is **cut** — 0 uses in the driving workload; parked post-iteration-12. |
| `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. |
### Core builtins
The sample calls **22 unqualified builtin names across ~176 sites**, none of
them in any spec: `len` ×55, `push` ×17, `byte_at` ×11, `starts_with` ×9,
`index_of` ×8, `has` ×7, `split` ×6, `split_ws` ×6, `join` ×5, `parse_int` ×5,
`trim` ×4, `slice` ×4, `substr` ×4, `pop` ×3, `ends_with` ×2,
`last_index_of` ×2, `to_lower` ×2, `sort` ×2, `char_of` ×1, `shift` ×1,
`remove` ×1, `reverse` ×1. `print_err` joins this set — Part 2 already named
it; this table never listed it.
These are **always in scope** — no `use` line, no namespace — the same status
`print`, `print_int`, `now`, `words`, `count`, `latest` already have, and they
share the same flat `WO_B_*` id space in the VM's builtin table as every other
builtin. Grouping into text operations, collection operations, and map
operations is documentation only, not namespaces. Each builtin has a
fixed-arity typed contract, resolved at compile time like every other
builtin; out-of-range indices **trap** (`T_BOUNDS`), never return a sentinel.
**Deliberately not adopted:** iteration/closure builtins (`map`, `filter`,
`reduce`) — the language has no function-value type, and adding higher-order
functions would require one.
## 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; uncaught faults still surface as traps. `throw` (explicit raise) is **cut** — 0 uses in the driving workload; parked post-iteration-12. Optionals (`?T`) handle *expected* absence (missing file stat, failed decode, missing env var) — the stdlib returns nil for those, reserving traps 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 six 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. `throw` (explicit raise), `time.mono`, and `is` are also cut — 0 uses in the driving workload each; parked post-iteration-12.