writeonce/docs/stories/language-runtime-database/done/17-library-projects-internal.md
shoney.arickathil d24c705860 feat: iterations 19 + 17 — Float/Bytes scalars (.wob v5), library kind + internal/
- 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>
2026-08-20 19:24:15 +02:00

11 KiB

Iteration 17 — library projects and dependency privacy (kind, internal/)

Format: 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 in compiler/src/CODE-LOGIC.md.

Gates: just web-app 26/0 (three new checks — library check mode, WO-E108 at the boundary, WO-E109 on a bad kind), and just woc-test, just oop-e2e (103/0), just deps-accept, just log-watcher, just employee all unchanged.

Two deviations from the plan, both because the framework grew after the plan was written:

  1. http/parse.wo was SPLIT, not moved whole. The plan said move it under internal/, but the framework-v1 slices had since added media_type and form_values to 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 now internal/parse.wo; the two public functions are http/form.wo, which does use internal (legal inside the library).
  2. The gate is 26/0, not the plan's 17/0. just web-app had 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:

  1. Library-ness is implicit and broken. A project without fn main (the framework) cannot be checked by woc <dir>: manifest presence forces build mode, which errors "no main entry point found". Iteration 16 verified the framework through a woc --emit workaround — a wart, not a design.
  2. The dependency boundary leaks internals. pub is 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 build on it refuses with a message that says what it is. The framework adopts it and loses the --emit workaround.
  • 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 offending use.
  • 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)

  1. Library-ness is manifest-declared: kind = "library", a top-level wo.toml key, 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 missing main stays a loud error instead of silently becoming a library. (Rejected: a [lib] section — ceremony with nothing to hold yet; Go-style inference — makes forgot-main and is-a-library indistinguishable.)
  2. Privacy mechanism: Go's internal/ directory rule. A dependency module whose path contains the segment internal is not importable across the [deps] boundary; the violation is a named diagnostic at the offending use, 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.)
  3. 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.
  4. Lib+bin duality: allowed. A library MAY carry an entry-shaped main (demo/self-test binary). kind = "library" changes the DEFAULT action of woc <dir> to whole-project typecheck; an explicit build invocation still produces the binary when a main exists. As a dependency its main is 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, and woc build refuses 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 the use and 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-app stays 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).