Files
ww/docs/spec.md

75 KiB

The ww Language Specification

Version 0.1 (2026-06-21). Status: normative-intent, in progress.

ww is a systems language: Hare semantics + CSP concurrency, no garbage collector, with a Rob-Pike / Plan-9 sensibility and Go-style explicit package/import. This document is the language contract. Where the two compiler stages disagree with this text, the text is wrong and is a bug to be filed (CLAUDE.md rule 10 requires the stages to agree with each other; this spec requires them to agree with it).

Two implementations track this spec and must stay byte-identical (rule 10): the C bootstrap frontend cmd/wcc (the reference) and the ww reimplementation lib/ww/syntax. The token table in §2 is kept numerically identical between them on purpose.

This is not a tutorial. It is terse by design, modelled on the Go language specification (golang.org/ref/spec): EBNF where a grammar is worth pinning, prose for semantics, and a normative appendix (§12) that consolidates every sanctioned divergence from Hare. The appendix is the highest-value part of this document — it is the single normative home for rules currently scattered across CLAUDE.md, .ai/, and project memory.

Sections marked (reserved) describe surface that is tokenised but not yet given meaning; they are written when the feature lands.


1. Notation

The grammar uses Extended Backus-Naur Form, following the Go spec's conventions:

Production  = production_name "=" [ Expression ] "." .
Expression  = Term { "|" Term } .
Term        = Factor { Factor } .
Factor      = production_name | token | Group | Option | Repetition .
Group       = "(" Expression ")" .
Option      = "[" Expression "]" .
Repetition  = "{" Expression "}" .

| is alternation, {} is zero-or-more, [] is zero-or-one, () is grouping. Terminal tokens are in "quotes" or are the named token classes from §2 (ident, int_lit, …).


2. Lexical elements

2.1 Source representation

Source is UTF-8. One UTF-8-encoded byte order mark (U+FEFF, bytes EF BB BF) is ignored when it is the first code point of a physical source file. Its three encoded bytes still count in source positions, so a following token on the first line begins at column 4. U+FEFF is invalid at every other source position, including inside string and rune literals and comments. Apart from that marker rule, the lexer operates on bytes and non-ASCII bytes are legal only inside string and rune literals and comments.

Every malformed UTF-8 byte in an eligible selected physical source produces one positioned invalid UTF-8 encoding error at its 1-based physical line and raw-byte column. The byte is consumed and omitted from the lexer's logical character stream before token recovery. A malformed multi-byte spelling is therefore diagnosed once for each byte that decodes as U+FFFD with width one; a correctly encoded U+FFFD is valid. Malformed bytes cannot split an identifier, number or suffix, operator, escape, comment delimiter, package keyword, or import spelling into different tokens. Filename and test-role eligibility precede validation, so an excluded physical file contributes no UTF-8 diagnostic. This rule is independent of the leading-BOM and raw-NUL rules below and does not make source bytes package, import, graph, action, artifact, publication, or persistence identity.

Each raw byte 00 (U+0000) is invalid at every physical source position, including in comments and string or rune literal text. It produces one positioned invalid NUL character error at that byte's source position and is omitted from the lexer's logical character stream before token recovery. It therefore cannot split an identifier, number, operator, escape, or comment boundary into different tokens. This is a source-representation rule, before package/import interpretation; it does not make U+0000 a package, import, or artifact identity component. An escape spelling such as "\\x00" is not a raw source byte and remains a legal literal value.

2.2 Comments

Line comments only, introduced by // and running to end of line. There are no block comments (Hare lineage; /* */ is not recognised).

Two comment forms are driver directives, not ordinary comments — they carry module-bundle structure and lex to dedicated tokens:

  • //ww:module <dotted.path> — set the mangling module path for the decls that follow (token MODPATH).
  • //ww:module-reset — reset the current module to the empty string before a package-less bundled file (token MODRESET).

These exist for the build driver's source bundling and are not written by hand in ordinary source.

2.3 Tokens

Tokens are identifiers, keywords, operators/punctuation, and literals. Whitespace (space, tab, CR, LF) separates tokens and is otherwise ignored. A semicolon ; terminates declarations and statements and is written explicitly (no automatic insertion).

2.4 Keywords

The following 30 identifiers are reserved and may not be used as ordinary identifiers:

as       break    case     chan     const    continue
def      defer    else     enum     export   false
fn       for      if       is       import   let
match    nil      package  proc     return   static
struct   switch   true     type     void     yield

chan and proc are reserved for the concurrency surface (§11, reserved). package is the package-clause keyword; import introduces an import.

2.5 Operators and punctuation

Assignment:  =  +=  -=  *=  /=  %=  &=  |=  ^=  <<=  >>=
Arithmetic:  +  -  *  /  %
Bitwise:     &  |  ^  ~  <<  >>
Comparison:  ==  !=  <  <=  >  >=
Logical:     &&  ||  !
Grouping:    (  )  {  }  [  ]
Punctuation: ,  ;  :  .  ..  ...  @  ?
Arrows:      <-  ->  =>

<- (channel send/receive) and -> are tokenised but reserved (§11). => separates a match arm pattern from its body. ? and ! are the error operators (§9). : introduces a type ascription / cast (§8) and a field/binding type. .. is the range operator (for range form, §7); ... is the variadic / spread marker (§6.3). @ introduces an attribute (§5.7).

2.6 Identifiers

ident = letter { letter | digit } .
letter = "A"…"Z" | "a"…"z" | "_" .
digit  = "0"…"9" .

A lone _ is the discard identifier: legal as a binding name to mean "ignore this", never as a reference.

2.7 Literals

int_lit   = decimal | hex | octal | binary [ suffix ] .
float_lit = digits "." digits [ exponent ] [ suffix ] .
rune_lit  = "'" ( byte | escape ) "'" .
str_lit   = '"' { byte | escape } '"' .
bool_lit  = "true" | "false" .
nil_lit   = "nil" .
  • int_lit is held as a u64. nil is the null pointer value (§3.6).
  • A numeric literal may carry a type suffix naming its type directly: 42u8, 0u64, 1i64, 3.0f32. Suffixes are the fixed-width type names (i8 i16 i32 i64 u8 u16 u32 u64 f32 f64). Without a suffix a literal is an untyped constant (§3.5) whose type is inferred from context.
  • Escapes in rune and string literals: \\ \" \' \n \t \r \0 and \xNN (two hex digits). Rune literals additionally accept the Unicode escapes \uHHHH and \UHHHHHHHH, yielding a codepoint.

3. Types

A type describes the layout and operations of a value. Every type has a size and an alignment, both queryable with size(T) (§8). No size is ever written as a bare literal in size-computing code — it is routed through the type table (CLAUDE.md rule 13).

3.1 Boolean and void

bool (1 byte; true/false). void (size 0) is the empty type, used as a fn result and as the second arm of a nullable pointer (§3.6).

3.2 Numeric types

Fixed-width integers: i8 i16 i32 i64 (signed), u8 u16 u32 u64 (unsigned). Floats: f32 f64.

Machine-word types (8 bytes on amd64): int, uint, uintptr, size. int and uint are a full machine word — Go-style, not Hare's 32-bit int. The limit constants (INT_MIN, INT_MAX, UINT_MAX) are derived from size(int), never hardcoded to a width (§12).

rune is a 32-bit Unicode codepoint (4 bytes).

3.3 String and slice

str and []T (slice of T) are both 24-byte three-word headers: { ptr, len, cap }. Their .ptr, .len, and .cap pseudo-fields are readable (§8.4). A str is UTF-8 bytes; reinterpreting []u8 as str is strings.frombytes (a pure cast — there is no validation; §12).

Array: [N]T is N elements inline (size N * size(T)). [_]T infers N from the initialiser's element count.

3.4 Aggregate and named types

type S = struct { f0: T0, f1: T1, … };   // struct
type Name = T;                            // named type / alias
  • struct — named fields laid out in declaration order with natural alignment padding. Duplicate field names are rejected.
  • tuple(T0, T1, …). Each element occupies an 8-byte slot (size((u32, u32)) == 16), diverging from Hare's packed tuples (§12).
  • enumenum i32 { A = 0, B, … } (a named integer type with named constants) or bare enum { … }.
  • A named type / alias introduces a new spelling for an underlying type.

TODO(spec): structs carry an internal packed property (no padding); surface its syntax (an @packed attribute? a struct modifier?) once confirmed against cmd/wcc. Likewise pin the static let qualifier spelling and whether static applies outside function bodies.

3.5 Untyped constants

A literal without a suffix has an untyped type (untyped int, untyped float, untyped str, untyped rune, untyped bool, untyped nil). An untyped constant takes a concrete type from its use context (assignment target, operand of a typed operation, fn argument). This is the inference that lets let x: u8 = 5; work without a suffix.

3.6 Pointers and nullability

*T is a pointer to T. A plain *T is non-null; dereferencing it is unconditional and free. A nullable pointer is written as the tagged union (*T | void); nil inhabits it. This is a one-flag fold, zero extra cost for the non-null case, and it is the only optionality mechanism — there is no separate nullable qualifier keyword (§12). ww has no runtime null-check net, so a non-null *T carries the no-null obligation the way C does.

3.7 Tagged unions and the error tag

TaggedType = "(" Type { "|" Type } ")" .

A tagged union (A | B | …) holds one of its variants plus a runtime tag. Variants are discriminated with match (§7) or tested with is and narrowed with as (§8). A variant may carry the error mark !T, which participates in the error model (§9): (i64 | invalid | overflow) is a typical fallible result.

3.8 Function and channel types

fn(P0, P1, …) R is a function type. chan T is a channel of T (reserved, §11). never is the bottom type of an expression that does not return (e.g. a call to abort).


4. Packages and imports

SourceFile = PackageClause { ImportDecl } { TopDecl } .
PackageClause = "package" PackageName ";" .
PackageName   = ident .
ImportDecl    = "import" ( ImportPath | ImportName ImportPath ) ";" .
ImportName    = ident .
ImportPath    = ident { "." ident } .
  • Every source file begins with a package clause. Within one semantic source action, eligible .ww files share one declared package name. Ordinary production and same-package test sources use p; external test sources may use the related p_test, and the actions remain separate even though one canonical directory owns their test product. The declared name need not equal the directory name or the final component of its canonical import identity.

  • The discard identifier _ is syntactically valid as PackageName, but it is never a valid declared package name. A complete source with package _; reaches checker initialization, which reports exactly invalid package name _ at the underscore token and continues checking the retained file. The package-clause parser admits the discard token only in this grammar slot; no other identifier position is broadened. A malformed or missing package name remains a parser error, and any complete-file syntax error prevents this checker diagnostic.

    Loading may retain _ transiently to compare declared source families, record imports, and construct the applicable action, but it is not canonical package, import, graph, action, symbol, .wwi, artifact, publication, or persistence identity. Direct sources retain __root; dotted directories and providers retain their dotted identities. Selected files with distinct declared names remain a loader/family conflict. In an all-blank action the checker emits one blank-name diagnostic per retained package marker in deterministic source order.

    Observable ordering is source eligibility and loader-visible header/family validation, recursive import loading, the ww run main-package check, eligible dependency producers, complete parent-source parsing, then the blank-name, reached imported-package, and later checker diagnostics. Thus missing or invalid imports may precede the parent check; a blank run root is not main and starts no producer; and a full-source syntax error suppresses the blank-name error. An ordinary blank source provider cannot publish an interface or archive for an importer. A supplied or caller-corrupted .wwi can nevertheless contain that spelling and is defensively validated when reached.

    Import interfaces are read, owner-checked, and syntax-parsed as separate lists before the primary source is parsed. Interface structural errors keep their existing precedence, but a primary syntax error returns before imported-package semantic checking and therefore suppresses every blank-provider import diagnostic. After successful primary syntax, compiler test mode first materializes its required test or collision-safe __wwtest support occurrence unless an equivalent primary occurrence exists. With no matching interface that occurrence retains the external support fallback; with a matching interface its provider name is validated, and the reserved spelling does not bypass the check.

    Each represented canonical interface package is classified as valid, missing, conflicting, or invalid, where invalid means one nonconflicting real declared name _. Imported-interface reachability is rooted only at canonical uses in the primary and compiler-required lists. A reached valid interface owner may contribute its imported uses transitively; an invalid, missing, conflicting, unreachable, or ownerless section may not. Before interface facts are bound or merged, every declaration and use whose owner is not both reached and valid is discarded. An unused invalid interface is therefore wholly inert, even when it embeds a valid-origin section that imports the invalid path: it emits no diagnostic, installs no scope or declaration, changes no output, and is byte-equivalent to supplying no such interface. If primary source separately reaches that valid origin, its retained edge may legitimately reach and diagnose the invalid provider.

    A retained use of an invalid provider is marked used. A nonblank use receives a fake empty-scope package binding under its explicit alias or, without one, the canonical path leaf; a blank use creates no visible binding. This recovery prevents qualified values, calls, and types from producing missing-member, unknown-type, export, or calling-nonfunction cascades. Ordinary lexical shadowing of a nonblank recovery alias still applies.

    Immediately after primary blank-name diagnostics, the first retained use of each invalid canonical path reports exactly could not import PATH (invalid package name: "_"); later uses of that path are deduplicated, distinct paths retain occurrence order, and independent checker errors continue. Default, explicit, and blank import forms all position this error at the path's first identifier rather than at the alias. Canonical path, source spelling, alias, declared provider name, owner marker, placeholder, edge, and binding remain separate facts. A physical .wwi path is observation metadata and a same-named .ww file remains an import decoy.

    Ordinary ww build still omits a valid visible literal *_test.ww after header loading, so package _; in that omitted role has no action or diagnostic. Under ww test, production plus a same-package blank test forms one augmented internal-test action; the test recompile substitutes for the separate production node and one compiler invocation diagnoses every retained blank marker. A test-only blank source has one test-package action. Mixed valid/blank production and test names retain family-mismatch precedence, and no failed blank product reaches generated main, link, or runtime.

    Normal blank-package failure publishes no assembly, interface, object, archive, executable, retained test, or new semantic generation. Existing request rollback removes owned stages and preserves prior public and committed bytes; a public build or test consuming a reached corrupted committed interface likewise publishes no replacement consumer generation, retained test, or downstream artifact. Exact valid-interface restoration follows ordinary reuse. Independent requests share no blank-name, reachability, deduplication, or fake-binding state. Cstage and WWstage have the same path-positioned diagnostic stream and artifact outcome. The private in-memory AST adds only path-position fields; AST enum/printing, .wwi schema, build workdir format 18, test workdir format 19, and semantic storage format 3 do not change. External driver interruption is unchanged; the fixed .new residue and later persistent request poisoning remain open.

  • Each source file has one contiguous import section immediately after its package clause. Once a non-import top-level declaration begins, a later import is rejected as imports must appear before other declarations. Parsing continues for recovery: consecutive imports in that late section produce one ordering diagnostic, while another ordinary declaration followed by another import starts a separately diagnosed late section. Existing aggregate module/reset boundaries and constituent package clauses reset this parser state per source; they do not relax the rule within a source or become package/import identity. A filename excluded by the target-selection rule below reaches no parser and therefore cannot contribute an ordering diagnostic or import edge.

  • Directory source eligibility uses Go 1.26.5 filename suffix semantics for WW's fixed linux/amd64 target. In the basename stem before the first dot, a final _test token is ignored for platform matching. A final known OS or architecture token must match linux or amd64; a final known OS followed by a known architecture takes precedence and both must match. The known-name sets are Go 1.26.5's syslist.KnownOS and syslist.KnownArch. Unknown or misplaced suffixes are ordinary, and a platform word without a nonempty underscore prefix is ordinary (linux.ww and plan9_test.ww are selected; x_windows.ww and x_plan9_test.ww are not). Leading-dot/underscore entries are ignored. Eligible names are byte-sorted before source validation. Production excludes selected *_test.ww; test variants classify only those selected test files. An excluded file contributes no declarations, imports, filename collision, package edge, action, export, artifact, initialization, test, diagnostic, or persistent invalidation. After eligibility, each selected physical source is validated for malformed UTF-8 and raw NUL before package-clause or import interpretation, in the existing byte-sorted file order. After that source preflight, two distinct selected basenames in one canonical directory that are equal under Go 1.26.5 Unicode simple folding are rejected after the coordinator's required package-clause classification and production @test validation parses, but before the delegated graph-import scan or tools. An ordinary build compares production names only; one test product compares its production, same-package test, and external-test selections without merging their units.

  • After existing filename, target, kind, and test-role eligibility, a selected source whose package/import header successfully declares the exact name documentation is omitted before semantic source membership and import scanning. The loader accepts the ordinary leading BOM, whitespace, comments, internal line directives, and package-clause trivia when recognizing this exact candidate. documentation_test, documentationx, and all other names remain ordinary sources.

    Candidate recognition is silent. A malformed package clause, malformed contiguous import, reached header NUL, or unterminated header-trivia comment retains its existing positioned load error before omission. After a valid exact package/import prefix, a non-i first raw byte establishes the body boundary and the remaining bytes are not parsed. A first raw i that does not form the exact import token is instead diagnosed at that byte as expected top-level decl; exact import followed by malformed import syntax retains the ordinary import-parser diagnostic. Thus syntax, types, late imports, imports missing only in the body, declarations, initializers, tests, and runtime behavior after a successful non-i boundary are not observed.

    An omitted documentation source creates no package member or family, source-file import occurrence, binding or graph edge, qualifier, initializer, declaration, symbol, .wwi, action, publication destination, or persistence identity. A mixed package is semantically identical to the same source set without that file. A doc-only dotted directory provider has no package sources and cannot satisfy an import. A same-named .ww file is still not an import provider under the directory-only import rule; when such a file is selected as a logical CLI root, it receives the established __root identity only if retained as ordinary. The rule applies equally to selected literal and logical roots, directory members, raw tests, recursive discovery, and dotted directory providers.

    Named-file documentation preflight applies only after symlink-following stat classifies the literal or resolved source as regular, including a symlink to a regular file. It retains one exact read buffer: documentation bytes are suppressed from that buffer, while ordinary bytes from the same preflight are reused by graph loading rather than reopened. FIFO and other nonregular named-source routes keep their prior behavior and are not touched by this preflight.

    Coordinator directory discovery records metadata only. Selected paths are canonicalized and deduplicated before source reads; each unique source is then classified exactly once. For an ordinary result, the same buffered bytes feed existing coordinator source validation. Direct-root failure and recursive pattern/group accounting are performed per request after classification, without rereading or reclassifying a source reached by more than one spelling or pattern. Independently, one delegated driver request enumerates each canonical directory once and lazily snapshots each role-eligible regular source once; that observation supplies classification, package/test attestation, import scanning, copied test actions, and unit emission across every variant in the request. A production-only request does not open excluded *_test.ww entries. Coordinator and delegated-driver observations remain separate process boundaries; concurrent rewriting between them gains no new atomic-snapshot guarantee.

    Test-role precedence remains route-specific. A visible literal named _test.ww passed to ww build validates its package/import header and then follows the test-only empty-selection rule below without documentation classification. A production directory build leaves *_test.ww unopened. Raw ww test and selected directory test variants do classify documentation sources. A logical request whose resolved provider merely has an _test.ww physical basename is not the literal named-build special case. Directory ww test -S without its required -o rejects as ww test: -S needs -o; with a valid -o, a directory rejects as ww test: -S needs a single test file. Both checks precede classification.

    A doc-only direct build or run root takes its route's existing directory contains no WW package sources result; recursive doc-only directories do not become packages, while mixed roots are exactly the source-absent control. No compiler, assembler, archiver, linker, generated test support/main, test child, initializer, or program runtime is attributable to the omitted source, and it creates no unit, interface, assembly, object, archive, executable, output, transaction, capture, or retained result. Changing only an ignored documentation body cannot invalidate or replace a committed semantic generation. Cold failure publishes nothing; warm no-source or header failure preserves prior public and semantic bytes. Classification state is ephemeral: a process-local named-root preload is cleared at the build boundary, and directory observations die with the request graph. No state is shared or persisted across requests, and no lock, schema, cache, or serialized identity is added. Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3.

  • A single existing raw .ww operand is also subject to the unconditional leading-name rule: if its final requested basename begins . or _, it is ignored before the source is opened. Named raw sources otherwise retain their existing all-files behavior, so a visible wrong-platform suffix remains eligible. Only the requested basename participates: a hidden parent does not hide visible main.ww, a hidden symlink spelling stays hidden for any existing non-directory target, and a visible symlink spelling stays eligible. The prefix-excluded operand creates no package, declaration, import binding/edge, action, artifact, initialization, test, publication, or persistent state. ww build reports the existing directory contains no WW package sources condition; an explicit running raw ww test also emits its command-owned FAIL, while -c and -S do not.

  • A visible single raw operand passed to ww build whose requested final basename ends exactly _test.ww is test-only after its package clause and contiguous initial import section have been read. Prefix exclusion remains first, so a bare _test.ww is never opened. Header read, package-clause, and initial-import syntax errors retain their ordinary precedence; after a valid header the sole root is omitted. Its dotted imports are not resolved, and a late import, body parse/type error, or runtime behavior is not observed. The first ordinary body token is not lexed, so an immediately following malformed UTF-8 byte or non-leading BOM is outside the header. A raw NUL reached while locating that token and an unterminated comment in header trivia remain load errors. Go's byte reader probes a following i as a possible import; the corresponding WW boundary is the exact lexical import token, so an ordinary WW identifier merely beginning with i is body syntax. This named-source rule retains all-files platform behavior: a visible x_windows_test.ww is still test-only. It classifies the requested basename of a visible symlink, not its target name; a symlink whose target is a directory remains a directory request.

  • Omission creates no canonical package, command-line package node, import binding or edge, graph/action, symbol, initializer, executable, archive, interface, publication, transaction, default .sepwork, or work-state mutation. It does not alter ww test, ww test -c, ww test -S, logical operands, directory/recursive selection, multiple named-source support, imports, or ww run. Physical directory and symlink-target data remain observation metadata, never package, import, graph, action, artifact, symbol, .wwi, publication, or persistence identity. Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3.

  • A local literal passed to ww build whose requested final basename ends .ww remains a directory request when ordinary stat reports a directory, including when the spelling is a symlink or ends _test.ww. The directory target therefore bypasses raw named-source prefix and test-source classification and enters ordinary directory enumeration. Actual eligible production entries, package declarations, imports, graph actions, producers, output policy, publication, persistence, invalidation, rollback, and cleanup are exactly those of the existing directory package route. The requested spelling and physical target are loader/presentation metadata only and create no new package, dotted-import, graph, action, symbol, artifact, .wwi, publication, or persistence identity. Raw ww test, ww test -c, and ww test -S already use the same directory classification and are unchanged. ww run retains its separate named-operand front door, with its .ww-spelled directory rule specified next. Multiple operands, recursive and logical requests, regular and non-regular source targets, and dangling symlinks are not changed. Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3.

  • For ww run, an existing selected target whose exact requested spelling ends .ww is a named-source request when symlink-following stat reports a directory. The command rejects it before shared resolution, directory enumeration, source/import loading, graph/action construction, private run scratch, producers, or runtime. A requested suffix _test.ww has the earlier exact diagnostic ww: cannot run *_test.ww files (OPERAND)\n; every other such directory has exact diagnostic OPERAND is a directory, should be a WW file\n. Both forms return status 1 with empty stdout and reproduce the requested operand bytes unchanged. The rule follows a terminal symlink, includes .hidden.ww and _hidden.ww directory spellings, and precedes any malformed package or missing import inside the directory. A trailing separator does not end .ww; ordinary non-.ww directories, dotted logical requests, regular and missing operands, multiple named operands, and non-directory non-regular targets retain their established routes. The rejected spelling and followed target are diagnostic metadata only and create no package/import identity, graph/action, symbol, artifact, .wwi, publication, transaction, persistence, process, or filesystem owner. Build and all test forms retain their distinct stat-first directory behavior. Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3.

  • A public operand is eligible for direct named-source adoption only when its exact requested spelling ends .ww and that command's existing file-kind rule admits it. An existing non-directory object with another suffix is not source: its bytes, package clause, imports, syntax, test declarations, runtime behavior, mode, and timestamp are not read as source, and ordinary dotted resolution continues exactly as though the colliding object were absent. Thus request foo.bar may resolve foo/bar.ww or foo/bar/ even while a physical non-directory foo.bar exists. A wrong-suffix symlink to a non-directory is the same ignored collision; a symlink to a directory remains an ordinary stat-first directory request. A visible .ww symlink to a regular source remains eligible, and the established stage-specific handling of visible .ww special files is not broadened by this rule.

    A resolved logical single file retains the established __root command-line package/action/artifact identity. A resolved logical directory retains its dotted package, import, graph, action, symbol, .wwi, initializer, artifact, publication, and persistence identity. The ignored physical pathname/object creates no membership, binding, edge, action, key, or alternate identity. Build and run use only the logical provider's production/import/initializer closure and runtime. Raw running test, its historical second-positional test-name filter, test -c, and test -S use only the provider's test package, descriptors, support closure, binary, and assembly.

    When no provider exists, collision-present behavior is byte-for-byte the existing collision-absent behavior. An ordinary single target retains its direct build/run/test cannot-find result and creates no producer action. A second positional retains the established package-coordinator route, diagnostics, status, selection lifecycle, and cleanup; this source gate does not reinterpret that positional form. Mutation invariance covers only absent and stat-successful non-directory collision states. A transition to a directory leaves this rule and follows ordinary directory routing, with no new atomic-snapshot guarantee for concurrent kind changes.

    Provider compilation failure preserves the prior public and semantic generation. A retained running-test runtime failure occurs after the complete logical build generation commits: it preserves prior retained public bytes by skipping deferred installation, while that semantic generation remains committed and reusable. Restoring prior source bytes requires a later successful rebuild and commit, not runtime-failure rollback. Controlled- failure cleanup otherwise remains unchanged. External signal interruption after action start is unchanged, including the verified-open fixed .new residue and later persistent-request poisoning. Multiple named sources, remaining suffix-first run behavior, finite .ww FIFOs, test process topology, and -run regular expressions are not completed here. Build workdir format remains 18, test workdir format remains 19, and semantic storage format remains 3.

  • import acme.codec; loads the canonical package acme.codec. If that package declares package wire;, the importing file sees its exported names as wire.Name; codec.Name is not an additional binding. An explicit alias replaces only that visible qualifier: import stable acme.codec; exposes stable.Name, not wire.Name or codec.Name. Each effective default or explicit named binding is a file-local package-name object usable only as the left qualifier of a selector. A bare occurrence in a value context is rejected as use of package BINDING not in selector; a bare occurrence in a type context is rejected as BINDING (package name) is not a type. Such a rejected bare occurrence does not count as use of the import for unused-import accounting. A legal selector does count as use, but does not excuse any separate bare occurrence. Both kinds of named binding are scoped to that source file. A sibling file must declare its own import; without one, its otherwise equal spelling follows ordinary undefined-name or unknown-type lookup rather than package-name diagnostics. Builtin spelling does not alter the object: an import bound as len, size, align, or another builtin name remains a selector-only package-name object. An ordinary lexical binding may shadow that file-local package-name object. Lookup at each occurrence chooses the nearest enclosing binding: a selector before a later local declaration can use and count the import, while the same spelling after that declaration denotes the local. Parameters bind for their whole function body; local let bindings begin after their declared type and initializer have been checked; tuple bindings begin after their right-hand side and declared types; and loop, range, and match-arm bindings begin only after their respective initializer/iterable or pattern/type has been checked. Nested blocks and loop scopes restore the imported package-name object on exit. These declaration-point rules apply equally to default, explicit, and builtin-spelled qualifiers. A selector counts as an import use only when its receiver resolves to that package-name object; a selector on a closer local, including a field or pseudo-field selector, does not. Go has no range-loop else clause; that WW-only extension is not assigned a Go-derived scope rule by this paragraph. Neither form exposes an imported declaration as a bare Name; ordinary unqualified lookup remains limited to lexical, builtin, and same-package declarations. A blank import creates no package-name object, an effective init import is rejected before installing one, and a missing target fails during import resolution, so none of those cases acquires the bare-package diagnostics or satisfies a named import's unused accounting.

  • An import whose effective file-local qualifier is init is invalid. This includes both import init acme.codec; and an unaliased import whose target declares package init;. Each resolved occurrence is rejected at its first import-spec token (the explicit alias when present, otherwise the first path token) as cannot import package as init - init must be a func. The rejected qualifier is never installed and does not participate in unused-import, duplicate-binding, or declaration/import-collision recovery; consequently a later init.Name independently reports an undefined init. Repeated invalid occurrences each report the core error. Import resolution retains precedence, so a missing target fails as missing without an additional effective-init error. A resolved rejected occurrence remains source and graph provenance for its exact dotted target: only its file-local qualifier binding is absent. Canonical package, import, graph, action, artifact, symbol, .wwi, publication, and persistence identity never derives from the rejected qualifier or from the target's declared name.

  • import _ acme.codec; is a blank side-effect import. The lone _ creates no qualifier, exposes no bare declaration, and is never diagnosed as unused. It is nevertheless a real import occurrence: resolution and all missing, self-import, cycle, internal, vendor, and imported-command checks use the dotted path, and the dependency participates in executable and test package initialization. Repeated blank occurrences and blank plus default/explicit named occurrences of one path are valid; every named occurrence remains independently subject to duplicate-binding and unused checks. Each semantic action's package dependency graph is the sorted, deduplicated union of real imports in that action's eligible source set. Occurrences retain their owning file and position, but equal canonical targets create one graph edge. Test-only occurrences never enter ordinary production. Self-import is rejected, except that toolchain-owned external-test self wiring is rebound to the effective augmented package action after ordinary per-site validation.

  • Canonical import identity is exact and case-sensitive. After contextual local or vendor expansion, two distinct effective identities that are equal under Go 1.26.5 simple folding are a request-wide structural error. Folding is only a temporary collision key: it never changes lookup, action identity, .wwi ownership, symbols, artifacts, storage, or diagnostics. Repeated occurrences of the same exact identity remain valid and deduplicate normally.

  • An executable package is one declared package main and containing a fn main; path and directory spelling do not classify commands. Within a package declared main, a package-scope declaration named main must be a function: let, const, def, and type forms reject as cannot declare main - must be func and do not enter package scope. The restriction depends only on the declared package name. A package with any other declared name may use or export main regardless of its dotted import path, path leaf, physical directory, or selection role. WW retains its established program-entry ABI, so a function main may carry WW's supported arguments and result; this rule does not adopt Go's source signature. An ordinary import of a package declared main is rejected, except for the toolchain's colocated external-test wiring.

  • For ww build, the output-option name is exactly o. The accepted forms are -o VALUE, --o VALUE, -o=VALUE, and --o=VALUE. An equals form splits at its first = and preserves every later byte, including further = characters; an empty value is valid. Repetition is last-value-wins, and a final empty value means that there is no effective explicit output, so the ordinary default-output or no-public-output rule applies. A concatenated spelling such as -oVALUE or --oVALUE is an unknown flag, not an output option. Build option parsing stops at the first package or source operand; later flag-like arguments remain operands and are not reparsed as -o. These spelling and placement rules select only caller-visible output disposition and never supply package, import, graph, action, symbol, artifact, .wwi, or persistence identity. A nonempty effective -o names an output directory when ordinary stat reports an existing directory (following symlinks) or its spelling ends in /. This classification is independent of whether one or many package roots were requested. Each selected command is published beneath that directory using the final component of its requested contextual import name, with the selected local directory leaf as the fallback when no such identity exists. Independently selected non-main roots load but do not enter the action list; non-main command dependencies retain ordinary actions, and a selection containing no command rejects before tools or output creation. A raw command-line .ww source uses its source basename without .ww, and a raw non-main source is the same no-command rejection. An existing directory whose requested basename ends .ww is not such a raw source: it retains directory-package output naming. With no -o, its requested leaf is used verbatim (for example, directory_test.ww); with an existing output-directory -o, publication instead uses the canonical selected target leaf (for example, directory-target). Missing trailing-slash hierarchies are created transactionally from 0777, filtered by the caller's umask. Loading and package/import diagnostics precede no-main, derived-path, duplicate-destination, implicit-default, and other output preflight; all of those checks precede creation. Producer failure and coordinator-owned interruption preserve existing directory contents and remove only request-created prefixes and stages. Direct external SIGTERM of a build driver is a verified-open exception: both stages preserve public and committed work bytes, but leave the directly spawned compiler alive and exactly three fixed-name .new staging files; the existing .unit.new makes a later persistent-work request reject. A non-directory output retains the single-product file/archive rule. If a lone command's synthesized default basename already names a directory, loading and graph validation complete and the build rejects before tools without changing that directory; a non-main package synthesizes no default public output. With no path operand, the current directory is selected as if . had been supplied. For a lone literal current-directory command and no explicit -o, ., ./, and equivalent single-dot-component spellings derive the default executable name from the canonical selected directory's final component; cold adjacent scratch uses that same leaf plus .sepwork. The dot spelling itself is never an output name. A current-directory non-main package uses the corrected scratch presentation but still links and publishes nothing. This physical leaf is fallback presentation metadata only: it never displaces a requested contextual dotted identity or becomes package, import, graph, action, artifact, symbol, .wwi, or persistence identity. Output paths and directory metadata never become package, import, graph, action, symbol, artifact, .wwi, or persistence identity.

    A valid omitted single raw *_test.ww build has an empty selection. With no effective -o, including an assembly-only request, and with exact -o /dev/null, it succeeds silently. A non-directory effective output fails with ww: no packages to build; an output-directory effective output fails with ww: no main packages to build. These empty-selection outcomes occur only after the raw test source's header has loaded successfully, create no output path or parent, and preserve every pre-existing output and work-state byte unchanged.

    Every caller-visible build installation checks its destination after all applicable compile, assemble, archive, and link producers finish. Ordinary stat follows symlinks. An existing directory rejects as ww: build output "PATH" already exists and is a directory; an existing nonempty regular file rejects as ... is not an object file unless its leading bytes identify a Go 1.26.5 object/output form. The recognized table is archive, ELF, Mach-O, PE, Plan 9, WASM, and XCOFF magic; WW additionally recognizes its compiler-owned //ww:module interface prefix. An absent path, an empty regular reservation, or a non-directory non-regular path may be replaced. A published non-main archive and its .wwi sidecar are checked as one WW request transaction, so arbitrary caller text in either destination preserves both old outputs and the committed persistent generation. This safety check is output disposition only: it does not enter package/import loading, graph or action identity, artifact bytes, or invalidation. Assembly-only -S retains the directory form's command-action selection and no-main rejection, but it reaches no install action: destination length, duplicate publication names, implicit destination collision, and output parent creation are therefore inapplicable.

  • A newly published ww build command is created with permission 0777 filtered by the invoking process's umask. A newly published non-command archive, and the adjacent WW interface required to consume it, use 0666 filtered by that same umask. These inode permissions are output metadata: they do not enter canonical package or action identity, artifact bytes, import binding, symbols, .wwi contents, or persistent invalidation. A warm build therefore reuses unchanged semantic actions while refreshing the caller-visible output with the current invocation's mode. Assembly-only builds publish no executable or archive.

  • On the Unix target, exact -o /dev/null is a build-output discard request, not an ordinary output filename. Package loading, import validation, graph construction, compilation, assembly, archiving, command linking, failure, and persistent invalidation proceed normally; only installation of a caller-visible command, archive, interface, default basename, or adjacent scratch tree is omitted. One such request may select any number of command and non-command packages, and an empty recursive match is successful after its normal warning. -S likewise needs no caller workdir merely to retain discarded assembly. Raw-file and directory roots use the same rule. A non-exact spelling is an ordinary output and keeps all established fan-out, rejection, permission, scratch, and publication behavior. Output discard is request metadata; it does not change package, import, graph, action, artifact, symbol, .wwi, or persistence identity.

  • Only names marked export (§5) are visible across module boundaries.

Import paths remain unquoted and dotted. Grouped imports, quoted import paths, and dot imports are not implemented. _ is reserved here for the blank form; it is not an ordinary alias.


5. Declarations

TopDecl = [ "export" ] ( FnDecl | TypeDecl | LetDecl | ConstDecl
                       | DefDecl ) .

5.1 Functions

FnDecl = "fn" ident "(" [ Params ] ")" Result "=" Block .
       | "fn" ident "(" [ Params ] ")" Result ";" .   // bodiless: extern/FFI
Params = Param { "," Param } .
Param  = ident ":" Type | ident ":" Type "..." | "..." .
Result = Type | "void" .

The = between signature and body is required. A bodiless fn (; terminator) declares an external symbol (C FFI); the bare ... C-style variadic is legal only on a bodiless declaration (§6.3).

At package scope, exactly fn init() void = Block; declares a special package initializer. It must have a body, no parameters, no result, no export, and no attribute. Multiple init declarations are permitted and retain owner-file and source order. init is not inserted into ordinary package scope: it cannot be called as init(), selected as pkg.init, exported, or used by another kind of declaration. Package-variable initialization completes before these functions run.

In a package whose declared name is main, only a function declaration may claim the package-scope name main. A rejected non-function declaration is not installed and cannot satisfy the executable entry. This is independent of canonical import identity and physical location. The accepted function shape continues to use WW's entry ABI, including its supported argument and result forms.

5.2 let

LetDecl = "let" ident ":" Type [ "=" Expr ] ";" .

A mutable binding. Without an initialiser, package-level storage is zero-backed; an uninitialized function-local binding retains the existing uninitialized-local rule. A static qualifier (static let …) gives function-local storage static lifetime.

For a package-level let with an initializer, values representable by the static-data emitter are installed statically. Every other otherwise valid initializer is evaluated exactly once at runtime. Checked references through package functions contribute variable-dependency edges; dependencies precede dependents, and source declaration order breaks ready ties. Initialization cycles are errors. Runtime package lets execute after imported package tasks and before the package's init functions. const and def are not broadened by this runtime path.

5.3 const

const introduces an immutable storage binding of the same declaration shape as let; assignment to it is rejected. At package scope its initializer stays within the existing static-data forms. A const is not entered into the runtime package-variable schedule described above; use mutable let when an otherwise valid initializer requires runtime evaluation.

5.4 def

DefDecl = "def" ident ":" Type "=" ConstExpr ";" .

A compile-time constant definition, typically at module level (def AF_INET: i32 = 2;). Hare's def.

5.5 type

TypeDecl = "type" ident "=" Type ";" .

5.6 Visibility

export on a top-level declaration makes the name visible to importing modules. Unmarked names are module-private.

5.7 Attributes

@name prefixes a declaration with an attribute. The defined attribute is @test: it marks a function as a test (§10).


6. Functions and calls

6.1 Blocks and results

A function body is a Block (§7). The body's value is its result; an explicit return Expr; returns early.

6.2 Calls

Call = Expr "(" [ Args ] ")" .
Args = Arg { "," Arg } [ "..." ] .

Arguments are passed by value (a str/[]T/struct copy is a shallow header/field copy). Aggregates larger than a register use the aggregate/sret ABI.

6.3 Variadic

Two distinct variadic forms:

  • Hare-style args: T... — the callee receives a []T. A call either gathers loose arguments (fmt.println(1, "x")) or forwards an existing slice with the spread xs....
  • C-style bare ... — only on a bodiless (extern) declaration, for calling C variadic functions across the FFI. f32 arguments in the variadic tail are promoted to f64 per the C ABI.

7. Statements

Stmt = Block | LetDecl | ConstDecl | Assign | If | For | Switch
     | Match | Return | Break | Continue | Defer | Yield | ExprStmt .
Block = "{" { Stmt } "}" .

7.1 Assignment

Assign = Expr AssignOp Expr ";" .
AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%="
         | "&=" | "|=" | "^=" | "<<=" | ">>=" .

The compound forms apply to scalar, indexed, and pointer-field targets.

7.2 if

If = "if" "(" Expr ")" Block [ "else" ( If | Block ) ] .

7.3 for

For = "for" "(" Expr ")" Block                          // condition
    | "for" "(" LetDecl Expr ";" Expr ")" Block          // 3-clause
    | "for" "(" "let" ident ".." Expr ")" Block .         // range

The 3-clause form is for (let i: size = 0; i < n; i += 1) { … }. The range form iterates the elements of a slice or array (for (let x .. xs) { … }). break and continue apply to the nearest enclosing loop; continue runs the post-clause / advances the range.

TODO(spec): does the range form bind the element or an index, and is the bound name a copy or a view? Confirm against the checker's N_FORRANGE lowering; the continue-post behaviour was a fixed miscompile (project memory #138), so the desugaring is worth stating precisely.

7.4 switch

Switch  = "switch" "(" Expr ")" "{" { SwCase } "}" .
SwCase  = "case" ExprList ":" { Stmt } | "case" ":" { Stmt } .

Value-matching on a scalar. A case may list comma-separated values; case: is the default arm. Arms do not fall through.

7.5 match

Match    = "match" "(" Expr ")" "{" { MatchArm } "}" .
MatchArm = "case" [ "let" ident ":" ] Type "=>" ArmBody
         | "case" "=>" ArmBody .
ArmBody  = Stmt | Block .

Discriminates a tagged union (§3.7). case let v: T => binds the narrowed value to v; case T => matches without binding; case => is the default. A match used as an expression produces a value via yield (§7.7) from each arm.

7.6 return, break, continue, defer

return [Expr]; break; continue;. defer Stmt; schedules a statement to run when the enclosing block exits.

7.7 yield

yield Expr; produces the value of an enclosing block- or match- expression.


8. Expressions

8.1 Operands

Identifiers, qualified names (mod.name), literals (§2.7), struct/array/ tuple literals (§8.5), and parenthesised expressions.

8.2 Operators and precedence

From loosest to tightest binding:

||
&&
==  !=  <  <=  >  >=
|   ^
&
<<  >>
+   -
*   /   %
unary:  -  !  ~  &(addr-of)  *(deref)
postfix: call()  index[]  slice[:]  field.  ?  !

&e takes the address of an addressable operand; *p dereferences.

TODO(spec): the precedence ladder is transcribed from the parser's bprec (parse.ww); verify each tier against cmd/wcc/parse.c and state associativity per tier. ww has no generics — no type parameters anywhere in the grammar; call this out explicitly once confirmed.

8.3 Casts, as, is

Cast = Expr ":" Type .          // type ascription / conversion
As   = Expr "as" Type .         // tagged-union narrowing
Is   = Expr "is" Type .         // tagged-union variant test (bool)

expr: T converts/ascribes (len(xs): size, t.line: i64). An integer cast does not silently truncate beyond the target width. e as T narrows a tagged union to variant T; e is T tests membership.

8.4 Index, slice, field, pseudo-fields

a[i] indexes an array/slice/str (index operand must be an integer). a[lo:hi] produces a sub-slice. e.f selects a struct field or a qualified name. .ptr, .len, .cap read the header words of a str or slice.

8.5 Composite literals

StructLit = TypeName "{" [ FieldInit { "," FieldInit } ] "}" .
FieldInit = ident ":" Expr .
ArrayLit  = "[" [ Expr { "," Expr } ] "]" .
TupleLit  = "(" Expr "," Expr { "," Expr } ")" .

S{} is the zero value of S.

8.6 Builtins

size(T) (type size), len(x) (str/slice/array length), alloc(v)! (heap-allocate a value, yield *T; §9), append(s, x) (grow a slice), delete(…), abort(msg) (terminate; type never).

alloc takes a value: alloc(T{…})!, alloc(T{})!, or alloc(expr)!. Bare alloc(T)! is invalid (it parses T as a value reference) (§12). There is no free: ww has no GC and no manual reclamation in compiler-side code — allocation lives until process exit (§12). lib/* modules that genuinely own a heap buffer free it explicitly via os.free.


9. Errors

ww has no exceptions. A fallible operation returns a tagged union whose error variants carry the !T mark (§3.7). Two postfix operators consume them:

  • expr? — if expr is an error variant, return it from the current function (propagate); otherwise yield the success value.
  • expr! — if expr is an error variant, abort; otherwise yield the success value.

Error variants are ordinary sentinel types in the union (strconv.invalid, strconv.overflow, …), discriminated with match. This mirrors Hare's error idiom, spelled with ww's ! tag.


10. Tests

A function marked @test is a test, run by the test harness, not part of the program. Two placements, following the Go foo / foo_test model:

  • White-box, in-package: a colocated file package <mod>; with @test functions, for testing unexported internals.
  • Black-box, external: package <mod>_test; with import <mod>;, exercising only the public surface.
@test fn adds() void = {
	if (1 + 1 != 2) { abort("math broke"); };
};

Production, production-plus-white-box-test, external black-box-test, test support, recompiled-for-test dependencies, and generated test main are distinct package actions. One canonical selected directory owns one test product and one generated main, which imports every applicable same-package and external target and produces one binary/result. The external action's import of the package under test binds to the augmented white-box action when it exists; affected transitive importers are copied and rewired so ordinary and augmented package state do not coexist in the linked closure.

A non-function package-scope main in production or same-package test source of a package declared main is a package-checker failure. Generated test-main ownership does not hide or replace it: the product is not linked or executed, no test accounting or package ok result is emitted, and ordinary build/test failure presentation and rollback apply.

Those variants are action distinctions over exact package representatives, not new ordinary package identities for case-fold comparison. Production, same-package test, external test, and recompiled copies of one exact canonical package therefore do not collide with each other. Their selected source units remain separate, while the directory-owned filename preflight spans the production/internal/external selections applicable to that test product.

Before a test function runs, the one generated product initializes its exact effective graph dependency-first and once per canonical action. Imports, runtime lets, and init declarations found only in *_test.ww never enter an ordinary production build. Test-only same-package, external-package, and valid mixed directories are accepted from their selected test files. A directory with no selected test file validates ordinary production but creates no test support, generated main, link, binary, captured runtime result, or process. The coordinator reports that successful validation exactly as ? <package> [no test files]\n.

An exact package documentation source that reaches a test selection is suppressed by the package rule in §4 before production, same-package, or external-test family construction. It contributes no @test, dependency, initializer, support action, generated-main input, binary, process, captured result, or retained output. In a mixed directory the test product is exactly the product of the ordinary sources alone; a doc-only directory is not the successful production-with-no-tests case. A direct running directory request instead fails with FAIL\n and the coordinator's directory contains no WW package sources diagnostic, while -c omits FAIL. A selected named or logical raw running request likewise emits FAIL and its physical parent's driver no-source diagnostic; raw -c and -S omit the marker. Directory -S without the required -o retains ww test: -S needs -o; after a valid -o, it retains ww test: -S needs a single test file. Both precede source classification. Recursive doc-only matches are omitted as packages: running test reports the per-pattern no-match warning followed by ww test: no packages to test, while mixed recursive selections run only ordinary groups. A documentation-header error retains precedence over every no-source result; an explicit running test adds FAIL, but compile-only and assembly-only routes do not.

Every test-bearing directory product links one request-private runnable. The test output-option name is exactly o, with the accepted forms -o VALUE, --o VALUE, -o=VALUE, and --o=VALUE. Equals forms split only at their first = and preserve an empty value or any additional = bytes. Repeated occurrences are last-value-wins. A final empty value means no effective explicit output and therefore requests no running-test retention; with -c, the ordinary default retained name applies. A concatenated -oVALUE or --oVALUE is unknown. Unlike build option parsing, known test options, including these exact output forms, are recognized before or after package operands. Invalid output-option names reject before package loading, product construction, execution, or publication. -c retains an executable copy and suppresses its execution; a nonempty effective -o retains a copy at the named destination and still executes unless -c is present. With no effective explicit output, -c writes <import-leaf>.test in the invocation directory. An output ending in / or naming an existing directory receives that basename and may have missing parent directories created. One non-directory output may name only one package. Multiple packages whose visible import leaves would produce the same test-binary name reject before tools or output creation; exact /dev/null is the discard exception. It keeps the ordinary private link and, unless -c is present, the ordinary execution, but installs no retained copy. The raw single-file compatibility route uses the same private-output rule and never treats /dev/null as an explicit artifact or adjacent-scratch stem. Declared package names, test variants, source filenames, physical directories, output paths, and retained binary names remain presentation or loader metadata and do not become canonical package or action identity.

The retained file is an executable, byte-identical copy of the private runnable. A compile-only retained binary joins package artifacts and statuses in the request-wide atomic publication transaction. Build, link, stage, or install failure preserves old destinations and removes temporary stages and invocation-created output prefixes. For a running -o request, the private binary executes first. Only a successful run enters the guarded install action; a failed, signalled, timed-out, interrupted, or unstartable run publishes no new copy and preserves any prior destination. The post-run guard uses the same directory/nonempty-regular/object-magic rule as ww build. Successful products in a multi-package running request install independently; their visible result order remains package order. A no-test product publishes no binary and does not create a directory solely for one. -w may persist the unchanged semantic actions for either -c or running retention without changing publication identity or introducing a test-result cache.

When ww test executes a directory-owned product, the child process working directory is that product's canonical absolute physical package source directory. Its per-run environment has exactly one effective uppercase PWD, with that same value. The physical source directory is execution context, not canonical package identity: it does not enter dotted identity, import binding, action identity, mangled symbols, .wwi, artifacts, or persistence keys. Production, internal-test, external-test, recompiled-for-test, support, and generated-main actions in the product share the one process context while remaining separate actions. Reachable dependency initialization consequently observes the tested product's directory; a dependency tested as its own product observes its own directory.

Every test binary actually started by ww test receives one effective uppercase PATH whose first element is the canonical absolute directory of the selected WW driver. An absent or empty caller PATH yields only that directory; a nonempty effective caller value follows it after :. Normal duplicate PATH= entries collapse to that one value, using the caller's first effective value as the suffix. This applies to directory products, their dependency initialization, filters and list mode, running retained tests, and the raw single-file compatibility route. It does not apply to build tools, ww build, ww run, compile-only or assembly-only test requests, no-test products, or later direct execution of a retained binary.

An executed directory product otherwise receives the caller's original Unix environment, not the build plan's tool environment. The snapshot keeps the first occurrence of each normal case-sensitive key=value, omits later normal duplicates and raw empty entries, and retains every nonempty malformed entry in order. It then applies only the effective PATH above and the package PWD. Consequently caller LC_ALL, TMPDIR, empty-valued variables, case-distinct keys, and arbitrary variables remain visible to production or test-only initialization and test bodies. The compiler, assembler, archiver, linker, and coordinator scratch continue to use their separate build-plan locale and temporary directory.

The same coordinator-executed product reads standard input from the null device. Its first read observes EOF regardless of the terminal, pipe, or file connected to the invoking command. Each parallel product owns a separate null descriptor, and production or test-only dependency initialization, filtering, listing, no-match execution, failure, and timeout all retain that boundary. Standard input is runtime process metadata and contributes no package, action, artifact, or persistence identity.

The product's standard output and standard error refer to one product-local capture. Bytes from either descriptor retain the order in which their writes reach that shared open output, and the coordinator emits the completed capture on its standard output. Immediately before an existing directory-product ok or run-status FAIL trailer, a nonempty capture whose final byte is not newline receives exactly one newline; an empty or already newline-terminated capture receives none. The capture itself is unchanged, and raw single-file or later direct retained-binary execution has no coordinator trailer and no such separator. A runtime failure or child-setup failure appends the product status diagnostic to standard output as well. Loader, source, compiler, assembler, linker, and other build failures retain their diagnostic standard-error channel; build-action stdout and stderr remain separate. Parallel products own independent captures and are still emitted in canonical product order.

List mode starts the same directory-owned product and therefore performs its ordinary package initialization, but it does not start any selected test child. The harness emits exactly one newline-terminated qualified name for each test selected by the command's filters, in descriptor order. If the selected set is empty, the harness emits no list payload, warning, or accounting and returns success; the coordinator still emits the ordinary successful package result without a no-tests suffix. Ordinary non-list execution is distinct: a valid filter selecting no tests writes exactly testing: warning: no tests to run\n through the harness's standard error, preserves the discovered/selected/started/completed accounting and successful exit, and causes a successful directory package result to end in [no tests to run]. The coordinator recognizes that exact warning only at capture byte zero or after a newline. Directory execution combines the warning and accounting in its product-local capture; a retained binary invoked directly keeps the warning on standard error, its accounting on standard output, and has no coordinator package result.

Calling test.skip(reason) from an active test records a successful skip and stops that test body. The empty string is a valid reason: test.skip("") is not a malformed harness result. Its ordinary result line is exactly the qualified test name, ... SKIP:, and a newline, with no reason bytes after the space. It increments skipped accounting once, increments neither failed nor harness-error accounting, and permits later selected tests to run. The same classification applies to raw single-file, same-package, external-package, and honest test-only descriptors; an active-test call reached through production package code; filtered execution; a coordinator-run retained binary; and later direct execution of a retained binary. Production files do not acquire a test descriptor. A nonempty reason keeps the same presentation with its bytes after SKIP: . An oversized reason remains an invalid control result, and test.skip outside an active test still aborts. This result rule does not change the current per-test process boundary, list mode, filtering language, package initialization, or package/import/action identity.

After those ordered results, an ordinary explicit ww test request that records an attributable setup, build, or execution failure emits exactly one standalone FAIL\n on standard output. The line is command-owned: it follows even a later successful package result, and a single explicit directory, recursive or dotted directory, or raw source file is sufficient. Runtime nonzero exit, signal, timeout, or executable-start failure all set that status. Filters, list mode, and the private execution of a retained test keep the same rule. Bare implicit current-directory ww test, -c, -S, build requests, CLI usage/shape or output preflight, publication-only failure, capture-only failure, cleanup-only failure, and later direct execution of a retained binary do not emit it. The marker changes no capture, diagnostic, executable, package/import/action identity, publication decision, or persistent byte.

The coordinator does not change its own cwd or environment. Parallel products receive independent child environments and each uses its own source directory. Relative ordinary files, testdata, and writes resolve there for every executing filter or list path. ww build, directory ww test -c (including -c -o), and a no-selected-test directory execute no test child and receive no execution-directory effect. A published test binary invoked directly, and the raw single-file compatibility route, inherit the user's invocation cwd and three standard descriptors; no package directory, input, or output policy is embedded or forced by the binary. Directly invoked retained binaries inherit the entire caller environment. Raw single-file tests preserve every caller environment field except the test-only PATH transformation above.


11. Concurrency (reserved)

The headline +CSP surface — chan T, proc/spawn, channel send/ receive <-, and the alt select construct — is tokenised (chan, proc, <-, ->) but not yet given semantics. This section is written when the feature lands; until then, use of these tokens is rejected by the checker.

Meta-ruling (normative). CSP is the sanctioned departure from the Hare-fidelity rules (CLAUDE.md rules 5 and 9) — Hare has no channels, and the +CSP mandate is the project's reason to add them. The concurrency surface therefore cites the Newsqueak → Limbo → Go lineage, not Hare, and that citation is correct by construction, not a rule-9 violation. The design of record is .ai/csp-design-input.md (library-first: spawn, unbuffered bidirectional chan T via mkchan(T), <- send/recv, recv → (T | closed) error idiom, alt as v2). See §12 entry 9.

TODO(spec): fill in syntax + semantics (channel construction, send/recv typing, spawn arg-copy ABI, alt arm grammar, the closed sentinel) once the MVP lands and the USER meta-ruling is explicitly on record.


12. Divergences from Hare (normative)

ww follows Hare's semantics and lib/ API shapes (CLAUDE.md rules 5, 9) except for the deliberate, USER-blessed departures below. Each is normative; the cited reference is the rationale of record.

  1. int/uint are a machine word (8 bytes), not Hare's 32-bit int. All limit constants derive from size(int); size/uintptr are 8 bytes. Ref: project memory "int is a machine word"; USER 2026-05-26.

  2. Tuples use 8-byte slots, not Hare's packed layout. size((u32, u32)) == 16. Routed through the type table only. Ref: USER 2026-06-04 tuple slot ruling; parity task #60.

  3. Nullability is the union (*T | void), not a nullable qualifier. Plain *T is non-null; the union fold is one flag, zero-cost for the non-null path. Ref: USER 2026-05-26 nullable-kept ruling.

  4. No _unsafe suffix convention. ww is unmanaged (no GC, no safe baseline), so Hare's _unsafe axis does not apply. bytesstr is the pure reinterpret strings.frombytes (Hare's fromutf8_unsafe); UTF-8 validation is opt-in at the IO source via utf8.validate, not wrapped per construction. Ref: CLAUDE.md rule 9; ref/hare/strings/ utf8.ha.

  5. The compiler frontend is one package lib/ww/syntax (tok + lex + ast + sym + typ + parse), a Go-over-Hare consolidation of Hare's {ast, lex, parse} split. Its single consumer is the wcc backend. The internal data shapes still mirror ref/hare/hare. Ref: CLAUDE.md rule 6; USER #74.

  6. Tests use the Go foo / foo_test model (§10): external black-box package <mod>_test; plus in-package white-box @test files. ww hard-rejects self-import. Ref: CLAUDE.md rule 9; task #16.

  7. No arena / no free in compiler-side code. Per-node alloc(value)!; allocation is reclaimed at process exit. alloc takes a value (alloc(T{…})!), never a bare type. Ref: project memory "drop amalloc", "Phase 0 closed", "alloc syntax form".

  8. The checked AST carries a per-node type_ stamp. Hare's parse AST is untyped and harec keeps a separate checked AST in C; ww stamps node.type_ in place as a checker invariant. Ref: project memory "Hare AST has no per-expr result".

  9. CSP (§11) is the sanctioned departure from rule-5/9 Hare fidelity — Hare has no channels. The surface cites Newsqueak/Limbo/Go, not Hare. Ref: .ai/csp-design-input.md; pending explicit USER meta-ruling.

  10. The two compiler stages emit byte-identical asm for the same input (CLAUDE.md rule 10); when inference power differs, the richer stage is aligned down to the leaner one. This is a project invariant, listed here because it is the contract this spec's "the text is the bug if stages disagree with it" rule rests on.


Appendix A. Anti-rot

This spec is hand-maintained. To keep it from drifting against the parser, every fenced ```ww example is intended to be extracted and compiled through w6c, asserting it parses (or rejects, for negative examples), wired as a make spec-check dependency of make test. The EBNF productions are not auto-verified; the compiling examples are the drift alarm. (Check not yet implemented; filed as a follow-up.)