Files
ww/docs/spec.md

1435 lines
75 KiB
Markdown

# 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).
- `enum``enum 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.
```ww
@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. `bytes``str` 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.)*