C bootstrap (phases 0-9):
cmd/wwc, cmd/6c, cmd/6a, cmd/6l, cmd/ww, rt, lib/*.
ww-side self-host (phase 10):
selfhost/cmd/wwc — ww-cgen frontend; bootstrap fixed point.
selfhost/cmd/6a — assembler; byte-identical to C 6a (test 991).
selfhost/cmd/6l — linker w/ archive (.a) support; byte-identical
to C 6l (test 992).
selfhost/cmd/ww — driver (build/run/version); byte-identical to
C ww (test 993).
make test: 15/15. make bootstrap: ww2.s == ww3.s, ww2.o == ww3.o,
ww2 == ww3 byte-identical, with the full ww-tooled chain.
411 lines
15 KiB
Markdown
411 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/wwc/` — frontend library (lex, parse, check). Builds
|
|
`libwwc.a`. Not a binary.
|
|
- `cmd/6c/` — amd64 compiler. Reads `.ww`, writes `.s`
|
|
(Plan 9 amd64 asm).
|
|
- `cmd/6a/` — amd64 assembler. Reads `.s`, writes `.o`
|
|
(ELF, for C interop).
|
|
- `cmd/6l/` — 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 `6c → 6a → 6l`.
|
|
Adding a new target later means a new triple (e.g. `7c`/`7a`/`7l`
|
|
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 (`6c` for amd64) reads `.ww` and writes Plan 9-style
|
|
target assembly (`.s`); the per-target assembler (`6a`) writes
|
|
ELF objects; the per-target linker (`6l`) produces a static
|
|
binary. Each tool uses Plan 9 cc's in-memory `Prog`/`Adr`
|
|
shapes — read `ref/plan9front/sys/src/cmd/cc/`, `cmd/6c/`,
|
|
`cmd/6a/`, `cmd/6l/` 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
|
|
(`libwwc`, `6c`, `6a`, `6l`, `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`, `bio`.
|
|
- 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)
|
|
bio/ alias of bufio for Plan 9 muscle memory
|
|
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 libwwc, 6c, 6a, 6l, 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
|
|
6c amd64 compiler
|
|
6a amd64 assembler
|
|
6l amd64 linker
|
|
lib/
|
|
libwwc.a frontend library (linked into 6c)
|
|
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/wwc/`): 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:
|
|
|
|
- `6c` 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/6c/` (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: `6c: 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.
|