- 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>
11 KiB
Iteration 17 — library projects and dependency privacy (kind, internal/)
Format: fiberloom
product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.Inserted 2026-08-20, needs further refinement (developer decision: keep as an iteration, do not implement yet).
Refined 2026-08-20: all four forks SETTLED (developer decisions, no code changed). See "Settled decisions" and "Impact analysis" below.
⏸ PARKED 2026-08-20 (developer directive: framework v1 work first). The spec and plan were written and approved before parking (
docs/superpowers/specs/2026-08-20-library-kind-internal-design.md,docs/superpowers/plans/2026-08-20-library-kind-internal.md).LANDED 2026-08-20 — unparked and executed against that plan, all six tasks. Every change is in the driver (
compiler/bin/main.ml); the VM,.wob, and GC are untouched exactly as the impact analysis predicted. Reasoning-under-the-code incompiler/src/CODE-LOGIC.md.Gates:
just web-app26/0 (three new checks — library check mode, WO-E108 at the boundary, WO-E109 on a bad kind), andjust woc-test,just oop-e2e(103/0),just deps-accept,just log-watcher,just employeeall unchanged.Two deviations from the plan, both because the framework grew after the plan was written:
http/parse.wowas SPLIT, not moved whole. The plan said move it underinternal/, but the framework-v1 slices had since addedmedia_typeandform_valuesto that file and the web-app calls both — moving the file whole would have put public surface behind the privacy line and broken the consumer. The parsing plumbing (Parsed,parse_request,url_decode,parse_query) is nowinternal/parse.wo; the two public functions arehttp/form.wo, which doesuse internal(legal inside the library).- The gate is 26/0, not the plan's 17/0.
just web-apphad grown from 14 to 23 checks (framework v1 plus iteration 19's Float price) before this iteration started; the three new checks make 26. The plan's numbers were written against a 14-check gate. Acceptance criterion 3 below still holds in substance: no pre-existing check changed.
Why this iteration exists
Iterations 15/16 made cross-repo libraries real — and exposed two gaps the established ecosystems solved long ago:
- Library-ness is implicit and broken. A project without
fn main(the framework) cannot be checked bywoc <dir>: manifest presence forces build mode, which errors "nomainentry point found". Iteration 16 verified the framework through awoc --emitworkaround — a wart, not a design. - The dependency boundary leaks internals.
pubis module-public with no dep-private tier: the framework's plumbing (parse_request, the hex decoder,route_match) is exactly as importable by the consuming app as its intended surface (Handler,App, the builders). Nothing marks "this module is the library's own business".
The conventions, analyzed (the corpus for the spec)
Go: no manifest marker — the package decides (package main +
func main = executable; anything else = library), cmd/<name>/ hosts
multiple binaries, and internal/ carries all encapsulation: the
compiler refuses any import of a path containing internal/ from outside
the subtree rooted at internal/'s parent. Directory-shaped privacy, zero
keywords — public repo, private API.
Rust: manifest-declared targets ([lib] / [[bin]], src/lib.rs /
src/main.rs conventions; a crate can be both) and keyword-grained
visibility: pub, pub(crate), pub(super), pub(in path). A dependency
sees only what is pub-reachable from the crate root. No internal/
convention — pub(crate) does that job.
Fit to writeonce doctrine: directory-as-module and keyword frugality point at Go's shape — but writeonce has a manifest (Go does not), so the library marker can be explicit where Go infers, giving clearer errors.
Goals (draft — the spec refines)
- A library project is first-class: declared in
wo.toml,woc <dir>typechecks it whole (no entry required),woc buildon it refuses with a message that says what it is. The framework adopts it and loses the--emitworkaround. - A dependency has a private interior: some modules are importable inside
the dep but not across the
[deps]boundary; violations are a named compile diagnostic at the offendinguse. - The framework reorganizes to demonstrate both (its parser plumbing moves
behind the privacy line;
Handler/App/builders stay public).
Settled decisions (2026-08-20 — the former open forks)
- Library-ness is manifest-declared:
kind = "library", a top-levelwo.tomlkey, default"program". Explicit beats inference in error messages both ways: a library refuses the default build with a message naming its kind, and a program missingmainstays a loud error instead of silently becoming a library. (Rejected: a[lib]section — ceremony with nothing to hold yet; Go-style inference — makes forgot-mainand is-a-library indistinguishable.) - Privacy mechanism: Go's
internal/directory rule. A dependency module whose path contains the segmentinternalis not importable across the[deps]boundary; the violation is a named diagnostic at the offendinguse, naming the dependency. Zero new syntax — a pure use-resolution rule, matching the directory-as-module doctrine exactly. (Rejected:pub(lib)keyword — parser + typechecker + catalog surface for granularity nothing needs; manifest export allowlist — a second place that drifts from the code.) - Scope: dep-boundary only.
internal/modules stay importable anywhere INSIDE the dependency; only the consumer is refused. The smaller honest cut; Go's full subtree rule (parent-of-internal/scope, enforced even within one project) is recorded as a possible later tightening — adopting it later only ever rejects more, never breaks a consumer. - Lib+bin duality: allowed. A library MAY carry an entry-shaped
main(demo/self-test binary).kind = "library"changes the DEFAULT action ofwoc <dir>to whole-project typecheck; an explicit build invocation still produces the binary when amainexists. As a dependency itsmainis never the entry — iteration 15 already ships that rule.
Follow-on rule inherited from Go, recorded so the spec doesn't relitigate
it: an internal type MAY appear in a public signature (Go permits exported
functions returning internal types — the consumer can hold and pass the
value but cannot use the module to name the type). No extra check in v1;
it is the library author's own smell to avoid.
Impact analysis (what each layer feels)
Framework (docs/examples/writeonce-framework/). Gains one manifest
line (kind = "library"); woc <dir> then typechecks the whole project
with no entry required — the iteration-16 --emit verification workaround
dies. Reorg: the plumbing the web-app never imports — the request parser
module (parse_request, url_decode, the carry-state record) and the serve
loop internals — moves under internal/; the public surface (Handler,
Middleware, App, Req/Resp, the response builders) stays where it
is. The framework's own use of its internal modules stays legal
(decision 3). The web-app changes nothing: it already imports only the
public modules, so just web-app staying 14/0 is the regression gate, not
a migration.
Compiler (woc) — the only place code would change. Two seams, both in
existing passes: the driver reads the kind key and picks
check-mode-by-default for libraries (build mode already exists; check mode
must still run the FULL pipeline — parse, typecheck, borrow check, GC
inference — so a green check means what a green build means); and the
dep-use resolution (the iteration-15 prefixing step) refuses a consumer
use whose dep-relative path contains internal, with a new WO-E1xx.
No lexer, parser, or type-system syntax changes — internal is a path
shape, not a keyword.
VM (wovm): zero impact, by construction. Visibility is name
resolution at compile time; the .wob format carries no module or
visibility metadata to extend — no new opcodes, no version bump, no loader
change. A library never yields its own .wob at all: iteration 15's model
compiles dependencies whole-program into the consumer's single image, and
that stands. The VM never learns "library" exists. A dual-built demo
binary is an ordinary program image. One honest disclosure: internal/
modules still compile INTO the consumer's image (there is no dead-code
elimination) — privacy restricts naming, it strips nothing.
GC: zero mechanism impact, one semantic note worth pinning. GC-ness is
whole-program inferred (iteration 7b): structural SCC plus demand
promotion run over the app's AND every dep's classes together, AFTER use
resolution — privacy is invisible to inference. So a framework class can
still be promoted to traced by how the APP uses it (an escape in app code
promotes the escaping projection's class, wherever that class lives), and
internal/ does not fence that off. This is correct and intended:
GC-ness stays a per-consumer, whole-program property, not a library
promise — a library author cannot pin "my class is untraced" any more
than before. Library check mode runs the same inference (over the library
alone), so a library-standalone check and an app-embedded compile may
legitimately disagree about traced-ness — that is the design, restated
here so nobody files it as a bug. Runtime GC (header, barrier, safepoints,
budgets) untouched.
Acceptance Criteria (draft)
- Given the framework marked as a library, when
woc <dir>runs, then it typechecks the whole project with no entry required, andwoc buildrefuses with a message naming the kind. - Given a framework module behind the privacy line, when the web-app
uses it, then a named diagnostic points at theuseand names the dependency; inside the framework the same import stays legal. - Given the public surface (
Handler,App, builders), when the web-app builds, then nothing changed —just web-appstays 14/0.
Out Of Scope
Registries/semver (still future), transitive deps (15's flat-only stands),
pub(super)-style fine grains beyond the chosen mechanism, multiple named
binaries per project (cmd/ convention — record, don't build).
Proposed Solution
Forks settled above (2026-08-20). Remaining path: spec + small plan —
manifest kind key + driver check-mode for libraries, the use-resolution
privacy rule + WO-E1xx diagnostic, the framework reorg (plumbing under
internal/), and just web-app as the regression gate. VM and GC are
untouched by design (see Impact analysis).