33 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. The lexer operates on bytes; non-ASCII bytes are legal only inside string and rune literals and comments.
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 (tokenMODPATH).//ww:module-reset— reset the current module to the empty string before a package-less bundled file (tokenMODRESET).
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_litis held as au64.nilis 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 \0and\xNN(two hex digits). Rune literals additionally accept the Unicode escapes\uHHHHand\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 bareenum { … }.- 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" 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
.wwfiles share one declared package name. Ordinary production and same-package test sources usep; external test sources may use the relatedp_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. - Directory source eligibility uses Go 1.26.5 filename suffix semantics for
WW's fixed
linux/amd64target. In the basename stem before the first dot, a final_testtoken is ignored for platform matching. A final known OS or architecture token must matchlinuxoramd64; a final known OS followed by a known architecture takes precedence and both must match. The known-name sets are Go 1.26.5'ssyslist.KnownOSandsyslist.KnownArch. Unknown or misplaced suffixes are ordinary, and a platform word without a nonempty underscore prefix is ordinary (linux.wwandplan9_test.wware selected;x_windows.wwandx_plan9_test.wware 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, or persistent invalidation. After eligibility, 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@testvalidation 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. import acme.codec;loads the canonical packageacme.codec. If that package declarespackage wire;, the importing file sees its exported names aswire.Name;codec.Nameis not an additional binding. An explicit alias replaces only that visible qualifier:import stable acme.codec;exposesstable.Name, notwire.Nameorcodec.Name. Both kinds of binding are scoped to that source file. A sibling file must declare its own import. Neither form exposes an imported declaration as a bareName; ordinary unqualified lookup remains limited to lexical, builtin, and same-package declarations.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,
.wwiownership, symbols, artifacts, storage, or diagnostics. Repeated occurrences of the same exact identity remain valid and deduplicate normally. - An executable package is one declared
package mainand containing afn main; path and directory spelling do not classify commands. An ordinary import of a package declaredmainis rejected, except for the toolchain's colocated external-test wiring. - A newly published
ww buildcommand is created with permission0777filtered by the invoking process's umask. A newly published non-command archive, and the adjacent WW interface required to consume it, use0666filtered by that same umask. These inode permissions are output metadata: they do not enter canonical package or action identity, artifact bytes, import binding, symbols,.wwicontents, 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. - 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.
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 spreadxs.... - C-style bare
...— only on a bodiless (extern) declaration, for calling C variadic functions across the FFI.f32arguments in the variadic tail are promoted tof64per 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?— ifexpris an error variant, return it from the current function (propagate); otherwise yield the success value.expr!— ifexpris 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@testfunctions, for testing unexported internals. - Black-box, external:
package <mod>_test;withimport <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.
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, result, or process.
Every test-bearing directory product links one request-private runnable.
-c retains an executable copy and suppresses its execution; -o retains a
copy at the named destination and still executes unless -c is present. With
no 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. 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. It 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. Test execution starts only after that transaction commits, so a
runtime failure leaves an explicitly retained binary. 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.
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. 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.
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,
environment, and three standard descriptors; no package directory, input, or
output policy is embedded or forced by the binary.
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.
-
int/uintare a machine word (8 bytes), not Hare's 32-bitint. All limit constants derive fromsize(int);size/uintptrare 8 bytes. Ref: project memory "int is a machine word"; USER 2026-05-26. -
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. -
Nullability is the union
(*T | void), not anullablequalifier. Plain*Tis non-null; the union fold is one flag, zero-cost for the non-null path. Ref: USER 2026-05-26 nullable-kept ruling. -
No
_unsafesuffix convention. ww is unmanaged (no GC, no safe baseline), so Hare's_unsafeaxis does not apply.bytes→stris the pure reinterpretstrings.frombytes(Hare'sfromutf8_unsafe); UTF-8 validation is opt-in at the IO source viautf8.validate, not wrapped per construction. Ref: CLAUDE.md rule 9; ref/hare/strings/ utf8.ha. -
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 thewccbackend. The internal data shapes still mirrorref/hare/hare. Ref: CLAUDE.md rule 6; USER #74. -
Tests use the Go
foo/foo_testmodel (§10): external black-boxpackage <mod>_test;plus in-package white-box@testfiles. ww hard-rejects self-import. Ref: CLAUDE.md rule 9; task #16. -
No arena / no
freein compiler-side code. Per-nodealloc(value)!; allocation is reclaimed at process exit.alloctakes a value (alloc(T{…})!), never a bare type. Ref: project memory "drop amalloc", "Phase 0 closed", "alloc syntax form". -
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 stampsnode.type_in place as a checker invariant. Ref: project memory "Hare AST has no per-expr result". -
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. -
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.)