Files
ww/CLAUDE.md

410 lines
15 KiB
Markdown

# ww
A small systems language. Plan 9 in spirit and code style, Hare in
syntax, API, and FFI, no GC, CSP at the end.
This file is the contract for the work. Read PLAN.md for the schedule.
## Identity
- Name: `ww`
- Source extension: `.ww`
- Module: a directory of `*.ww` files (Hare-style layout)
- Toolchain: Plan 9-organized. One library + one binary per role
per target architecture. Plan 9 uses a single digit per arch
(8=386, 6=amd64, 5=arm, 7=arm64, 9=power); we adopt that.
- `cmd/wcc/` — frontend library (lex, parse, check). Builds
`libwcc.a`. Not a binary.
- `cmd/w6c/` — amd64 compiler. Reads `.ww`, writes `.s`
(Plan 9 amd64 asm).
- `cmd/w6a/` — amd64 assembler. Reads `.s`, writes `.o`
(ELF, for C interop).
- `cmd/w6l/` — amd64 linker. Reads `.o` and `.a`, writes a
static ELF binary.
- `cmd/ww/` — user-facing driver (Hare's `hare(1)` /
Plan 9's `cc(1)` analogue). Orchestrates `w6c → w6a → w6l`.
Adding a new target later means a new triple (e.g. `w7c`/`w7a`/`w7l`
for arm64). The frontend library `wwc` is shared.
## Hard rules (do not violate)
1. **No garbage collector.** Ever. Memory is allocated and freed by the
programmer. The compiler may insert defer-style cleanup, never a
tracing/reference-counting collector.
2. **No `map`.** A built-in growable hash table is unsafe without GC
(rehashing invalidates pointers). Users may build their own; it is
not a language type.
3. **No complex runtime.** The runtime is a few hundred lines. It owns:
process startup, syscalls trampoline, panic/abort, and (later) the
CSP scheduler. It does not own memory beyond a tiny bump arena for
startup.
4. **Static linking by default.** A `ww` binary is self-contained,
like Go. Dynamic linking is opt-in (`-shared`, `-l`).
5. **C FFI is first-class, Hare-style.** A body-less `fn` declaration
imports the symbol; `@symbol("name")` overrides the linker name.
We must be able to bind libcrypto/libtls/ncurses cleanly and link
them statically into the final image. The platform calling
convention (SysV amd64 on Linux) is the C ABI, so no separate
`extern "c"` marker is needed.
6. **Plan 9 toolchain. No LLVM, no QBE, no external IR.** We do not
invent a portable SSA IR. We follow Plan 9: the per-target
compiler (`w6c` for amd64) reads `.ww` and writes Plan 9-style
target assembly (`.s`); the per-target assembler (`w6a`) writes
ELF objects; the per-target linker (`w6l`) produces a static
binary. Each tool uses Plan 9 cc's in-memory `Prog`/`Adr`
shapes — read `ref/plan9front/sys/src/cmd/cc/`, `cmd/w6c/`,
`cmd/w6a/`, `cmd/w6l/` before writing your own.
7. **No generics, no interfaces, no closures, no lambdas.** Four forms
of bloat we refuse. Polymorphism, when genuinely needed, is a
struct of function pointers plus a `ctx: *void` (Plan 9 `Bio`,
Hare `io::stream`). All functions are declared at file scope;
function values are pointers to those named functions. No
capturing. No anonymous function literals. The compiler does no
virtual dispatch; users build vtables explicitly when they want
them. If a function needs to work on multiple types, write it
multiple times, or operate on `[]u8` and let the caller cast.
**Tagged unions are allowed**, but only as the Hare-style error
idiom: `(T | error)` (and a few sentinel kin like `nomem`).
Pattern-matched with `match`. Propagated with postfix `?`. Asserted
with postfix `!`. They are not an open extension point — no enum
methods, no virtual dispatch through the tag, no nesting beyond
what the error idiom needs. If you find yourself reaching for a
discriminated record, use a struct with a tag field instead.
8. **Tests run after every change.** `make test` is the truth. A
change without a green `make test` is not a change.
9. **Prototype in C, then self-host.** The C bootstrap toolchain
(`libwcc`, `w6c`, `w6a`, `w6l`, `ww`) is throwaway scaffolding.
Its job is to compile enough of `ww` to compile the ww
reimplementations of itself. Do not over-engineer the C side.
## Type system
Hare-style integer names. Fixed width, explicit signedness:
```
i8 i16 i32 i64 signed
u8 u16 u32 u64 unsigned
uint int register width (target-defined)
uintptr pointer-width unsigned
f32 f64 IEEE 754
bool one byte
rune i32, a Unicode code point
str immutable utf-8 view: { *u8, len }
void zero-sized
```
Composite:
```
*T pointer (may be nil)
[N]T fixed array
[]T slice: { *T, len, cap }
struct { x: i32, y: i32 } aggregate (Hare shape)
fn(arg: T) ret function pointer (file-scope only)
chan T CSP channel (last phase)
```
No `map`. No `interface`. No `union`. No exceptions. No generics.
No closures.
Polymorphism, when truly needed, is a struct of function pointers
plus a `ctx: *void`. See `lib/io/stream.ww` for the canonical shape.
This is how Plan 9 `Bio` and Hare `io::stream` work. It is plain
data, easy to read, and the compiler does nothing magic for it.
## Syntax
Hare-shaped. Trailing semicolons. `=` after function and type
signatures. The one departure from Hare: module paths use `.`
instead of `::`. Both module navigation and field access use the
same dot — the compiler resolves by name lookup.
```
use io;
use fmt;
use os;
def MAX_LINE: i32 = 4096;
type point = struct {
x: i32,
y: i32,
};
export fn move(p: *point, dx: i32, dy: i32) void = {
p.x += dx;
p.y += dy;
};
export fn distance(a: point, b: point) f64 = {
let dx: f64 = (a.x - b.x): f64;
let dy: f64 = (a.y - b.y): f64;
return math.sqrt(dx*dx + dy*dy);
};
export fn main() void = {
let p: point = point { x = 0, y = 0 };
move(&p, 3, 4);
for (let i: i32 = 0; i < MAX_LINE; i += 1) {
fmt.println(i);
};
};
```
Lexical rules:
- Statements end in `;`. No automatic insertion.
- Function bodies follow `=`: `fn f() T = { ... };`.
- Type definitions follow `=`: `type p = struct { ... };`.
- Visibility is the `export` keyword. No capitalization rule.
- Module paths use `.`. So does field access. Compiler disambiguates.
- Constants: `def NAME: T = lit;` (compile-time).
- Variables: `let name: T = expr;` or `let name = expr;` (inferred).
- Struct literal: `point { x = 0, y = 0 }` (Hare uses `=`).
- Type cast: `expr: T`.
- Pointers are nullable. Compare with `== nil`.
- No methods. A function on `point` is `fn move(p: *point, ...)`.
Plan 9 cc has no methods; neither do we.
- No closures, no lambdas, no anonymous functions. A function value
is a pointer to a named, file-scope function.
- No `:=`, no `make`, no `new`. Allocation is the built-in expression
form (Hare-style):
- `alloc(point { x = 1, y = 2 })` returns `*point`
- `alloc([0u8...], 16)` returns `[]u8` of len/cap 16
- `free(p)` releases a pointer or slice
- C FFI: a body-less `fn` is an external symbol. `@symbol("name")`
overrides the linker name:
```
@symbol("malloc") fn c_malloc(n: u64) *void;
@symbol("free") fn c_free(p: *void) void;
```
- Errors are plain strings. See "Errors" below.
## Errors
Two idioms, picked by the API author:
1. **Plan 9 model.** An error is a string. Empty means OK. Functions
that can fail return `(T, error)`. Use this when there are only one
or two error sources and the caller usually wants to format the
message and move on.
2. **Hare tagged-union model.** A function returns `(T | E1 | E2 | ...)`.
Callers `match` on it, or propagate with postfix `?`, or assert
non-error with `!`. Use this when errors are structured (have
payload) or when a caller routinely wants to handle one specific
error kind.
Both are first-class. Pick whichever fits; do not mix in a single
return type.
```
type error = str;
def eEOF : error = "eof";
def eShortRead : error = "short read";
```
Functions that can fail return `(T, error)`. The `T` is zero-valued
when the error is non-empty:
```
export fn open(name: str) (*file, error) = {
if (name == "") {
return nil, "open: empty name";
};
let fd: i32 = sys.open(name, sys.oRdonly, 0);
if (fd < 0) {
return nil, sys.errstr();
};
return alloc(file { fd = fd, name = name }), "";
};
let f, err = open("/tmp/x");
if (err != "") {
fmt.eprintln(err);
os.exit(1);
};
```
Wrapping is string concatenation: `fmt.errorf("open %s: %s", name,
err)`. Comparison is plain string compare. Sentinel errors are
package-level `def`s.
Why a string and not a struct? Because Plan 9 used errstr for
thirty years and the world did not end. Strings are concrete,
allocation-free when literal, and carry arbitrary detail without
inviting a type hierarchy. Hare's `?` postfix and `match` for
errors are also unavailable to us by rule #7.
## Naming
Plan 9 taste lowered to ww. No CamelCase anywhere in ww source.
- Package names: short, lowercase, one word. `fmt`, `io`, `bufio`.
- Identifiers: lowercase, words run together. `newbuf`, `tcpsock`,
`parsefile`. Underscores allowed but discouraged.
- Visibility: the `export` keyword. Not first-letter case.
- Types: lowercase, like everything else (`point`, `lexer`, `node`).
- Constants (`def`): UPPER_SNAKE for tunables (`MAX_LINE`, `NHASH`);
lowercase for ordinary ones (`eEOF`, `eShortRead`).
- Files: short, descriptive, lowercase. `lex.c`, `parse.c`, `ir.c`,
`lex.ww`, `parse.ww`.
- C-side struct typedefs in the bootstrap mirror Plan 9 (capitalized
is the C convention there): `Node`, `Sym`, `Type`, `Prog`, `Adr`.
In ww source the same shapes are lowercase (`node`, `sym`, `prog`,
`adr`; the type-info struct is just `tinfo` to avoid the keyword).
## Standard library
Hare layout, Plan 9 names where they exist. Initial cut:
```
lib/
types/ integer limits, type info
bytes/ byte slice ops
strings/ str ops
fmt/ printf-family
io/ reader/writer/closer interfaces
bufio/ buffered io (Plan 9 'bio' equivalent)
os/ process, fs, args, env
os/exec/ run subcommands
errors/ error type, sentinel values
sort/ sort.Slice, sort.Search
strconv/ number<->string
path/ path manipulation
encoding/ hex, base64, utf8
hash/ crc32, fnv, sha256
net/ dial, listen
time/ monotonic + wall clock
sync/ (post-CSP) mutex, once, waitgroup
```
C bindings live under `lib/c/`:
```
lib/c/
libc/ malloc, printf, etc. (when calling out)
tls/ libtls (or BearSSL) bindings
crypto/ libcrypto bindings
curses/ ncurses bindings
```
Bindings are thin: one `.ww` file per C header section, marked
`extern "c"`, no wrapping logic in the binding layer itself. Higher
ergonomics live in a sibling pure-`ww` package.
## Build
POSIX `make`, no autotools, no cmake.
```
make # builds libwcc, w6c, w6a, w6l, ww, stdlib
make test # runs all tests (toolchain + stdlib)
make install # installs to $PREFIX (default /usr/local)
make clean
```
Target layout under `out/`:
```
out/
bin/
ww user-facing driver
w6c amd64 compiler
w6a amd64 assembler
w6l amd64 linker
lib/
libwcc.a frontend library (linked into w6c)
libwwrt.a runtime archive (linked into final binaries)
<pkg>.a precompiled stdlib modules
obj/... intermediate .s, .o per package
```
Static by default. `ww build foo.ww` produces a statically linked
ELF. Dynamic is `ww build -shared` or per-library `-l`.
## Testing
Three tiers, all driven by `make test`:
1. **Compiler unit tests** (`test/wcc/`): C, table-driven. Lex, parse,
typecheck, IR-gen, codegen each have their own table.
2. **Language tests** (`test/lang/*.ww`): each file is a single ww
program with a comment header declaring expected exit code and
expected stdout. The harness (`test/run`) compiles and runs.
3. **Stdlib tests** (`lib/*/+test.ha`-style, here `*_test.ww`): in-tree
tests per module. Hare convention, just renamed.
Every change must:
- Add or update a test that exercises the change.
- Leave `make test` green.
- Produce no new warnings (`-Wall -Wextra -Wpedantic` in C; the
ww typechecker is strict by default).
## Rob Pike rules (kept on the wall)
1. You can't tell where a program will spend its time. Measure.
2. Measure. Don't tune for speed without numbers.
3. Fancy algorithms are slow when n is small, and n is usually small.
4. Fancy algorithms are buggier and harder to implement. Prefer simple.
5. Data dominates. Get the structures right and the code follows.
6. There is no rule 6.
Applied to this project:
- `w6c` is a single-pass-ish recursive-descent parser into a typed
AST, walked into a `Prog` list, then printed as text. No parser
generator, no LLVM, no SSA pass pipeline.
- Optimizer is intentionally absent at first. Constant fold + dead
code elim only. Add passes when a benchmark demands one.
- Data structures: `Node`, `Sym`, `Type`, `Prog`, `Adr` modeled on
Plan 9 cc. Read `ref/plan9front/sys/src/cmd/cc/cc.h` and the
per-target headers (`cmd/8c/gc.h`, `cmd/8a/`, `cmd/8l/`) before
inventing a new shape.
## What is in `ref/`
- `ref/hare/` — the Hare distribution. Read for syntax, FFI
(`@symbol`), stdlib layout, type names, error idioms.
- `ref/plan9front/` — 9front. Read `sys/src/cmd/cc/` (the shared
frontend library), `sys/src/cmd/8c/` and `sys/src/cmd/w6c/` (per-
target compilers), `sys/src/cmd/8a/` (assembler), `sys/src/cmd/8l/`
(linker) for toolchain organization, `Prog`/`Adr` data shapes,
mkfile style, and naming.
When in doubt: copy Hare for surface syntax and stdlib, copy Plan 9
for toolchain organization and compiler internals.
## What we will NOT build
- A package manager. Modules are directories. Vendoring is `cp -r`.
- A formatter beyond `wwfmt` (one canonical style, no options).
- A language server in phase 0. Plain editors are fine.
- Generics, interfaces, closures, lambdas. Ever. See hard rule #7.
Tagged unions are allowed but ONLY for the Hare-style error idiom
(`(T | error)` + `match` + `?` + `!`). No general-purpose enums or
open extension points.
- An async/await coloring. CSP is the concurrency story; a function
is a function.
## Working agreement (for the assistant)
When making changes:
- Prefer editing existing files to creating new ones.
- Run `make test` (or the smallest relevant slice) after any code
change. Report results.
- Match the file's existing style. C files: Plan 9 style (tabs,
K&R, short names; see `ref/plan9front/sys/src/cmd/cc/`). ww
files: Hare-shaped, formatted by `wwfmt` (one canonical style).
- If a design question is non-obvious, propose two options before
writing code.
- Keep diffs small. One concern per change.
- Commits: Plan 9 style. Subject is one short lowercase line,
prefixed with the affected area: `w6c: fix const fold for i64`,
`lib/fmt: handle %v for slices`, `cc: typo`. Body only when the
why is not obvious from the diff. No `Co-Authored-By` trailer,
no "Generated with" footer, no emoji.