first commit
This commit is contained in:
385
CLAUDE.md
Normal file
385
CLAUDE.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# 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 tagged unions, no closures, no
|
||||
lambdas.** Five 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.
|
||||
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
|
||||
|
||||
Plan 9 model. An error is a string. Empty means OK. Hare uses
|
||||
tagged unions for errors; we don't have unions, so we drop down
|
||||
to the plainer Plan 9 thing.
|
||||
|
||||
```
|
||||
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, tagged unions, closures, lambdas. Ever.
|
||||
See hard rule #7.
|
||||
- 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.
|
||||
369
PLAN.md
Normal file
369
PLAN.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# PLAN
|
||||
|
||||
A phased plan for `ww`. Each phase ends with a green `make test` and
|
||||
a tagged checkpoint. Don't start phase N+1 until phase N is green.
|
||||
|
||||
CLAUDE.md is the rulebook. This file is the schedule.
|
||||
|
||||
## Phase 0 — Skeleton (C bootstrap scaffold)
|
||||
|
||||
Goal: a repo that builds, has a Makefile, and has a `ww` driver
|
||||
that prints its version. No compilation yet.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `Makefile` (POSIX) with `all`, `test`, `clean`, `install` targets.
|
||||
- `cmd/wwc/` C library skeleton (builds `libwwc.a`):
|
||||
- `ww.h` — central typedefs (`Node`, `Sym`, `Type`, `Lex`)
|
||||
- `mem.c` — bump arena allocator (no `malloc` in hot paths)
|
||||
- `err.c` — `errorf`, `fatal`, `warn`
|
||||
- (lex/parse/check stubs added in later phases)
|
||||
- `cmd/ww/` driver skeleton: argv parsing, version print.
|
||||
- `test/run` — shell test harness.
|
||||
- `test/wwc/000_smoke.c` — smoke test that asserts `ww -V` prints
|
||||
the version.
|
||||
|
||||
Per-target binaries (`6c`, `6a`, `6l`) are NOT created here. They
|
||||
ship in their own phases (4, 5, 6).
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `make` produces `out/bin/ww` and `out/lib/libwwc.a`.
|
||||
- `make test` runs the smoke test and passes.
|
||||
|
||||
## Phase 1 — Lexer (in `libwwc.a`)
|
||||
|
||||
Goal: tokenize ww source into a stream of `Tok` structs.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/wwc/lex.c` — hand-rolled DFA. Handles:
|
||||
- identifiers, keywords (`fn`, `let`, `def`, `if`, `else`, `for`,
|
||||
`switch`, `case`, `return`, `use`, `type`, `struct`, `defer`,
|
||||
`break`, `continue`, `export`, `proc`, `chan`, `nil`, `true`,
|
||||
`false`)
|
||||
- integer literals (dec, hex, oct, bin) with suffixes (`u8`, `i32`...)
|
||||
- float literals
|
||||
- rune literals `'x'` and string literals `"..."` (UTF-8)
|
||||
- operators and punctuation, including `@` for attributes
|
||||
(`@symbol("name")`, etc.)
|
||||
- explicit `;` terminators — no automatic insertion (Hare rule)
|
||||
- `//` and `/* */` comments
|
||||
- `cmd/wwc/tok.c` — token names, debug printer.
|
||||
- `test/wwc/100_lex.c` — table-driven: input → token sequence.
|
||||
- `test/lang/lex/*.ww` — small files exercised via a tiny test
|
||||
harness that links against `libwwc.a`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A `lextool` test binary dumps a representative sample's token
|
||||
stream deterministically.
|
||||
- All lex tests green.
|
||||
|
||||
## Phase 2 — Parser & AST (in `libwwc.a`)
|
||||
|
||||
Goal: parse the full grammar into an AST. No types yet.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/wwc/parse.c` — recursive descent. Pratt expression parser for
|
||||
precedence. No yacc.
|
||||
- `cmd/wwc/ast.c` — `Node` constructor helpers; printer.
|
||||
- Grammar coverage:
|
||||
- `use` imports (paths use `.` not `::`)
|
||||
- `type` declarations (struct, alias, fn type) with trailing `=`
|
||||
- `def` constants
|
||||
- `let` declarations (top-level and local)
|
||||
- `fn` definitions with `= { ... };` body. No methods; receivers
|
||||
are just first arguments.
|
||||
- `export` visibility marker
|
||||
- all expressions, statements, control flow
|
||||
- `defer`
|
||||
- body-less `fn` declarations and `@symbol("name")` attribute for
|
||||
FFI imports
|
||||
- `test/wwc/200_parse.c` — table-driven AST shape tests.
|
||||
- `test/lang/parse/*.ww` — programs that should parse but not yet
|
||||
typecheck; harness only checks parser exits 0.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- 50+ parse tests green, drawing shapes from
|
||||
`ref/hare/cmd/hare/main.ha`.
|
||||
- AST printer output is deterministic.
|
||||
|
||||
## Phase 3 — Type checker (in `libwwc.a`)
|
||||
|
||||
Goal: resolve names, infer/check types, produce a typed AST.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/wwc/sym.c` — symbol table. Lexical scopes. Plan 9 style hash.
|
||||
- `cmd/wwc/type.c` — type representation, equality, conversion rules.
|
||||
- `cmd/wwc/check.c` — type checking pass over the AST. Errors carry
|
||||
source locations.
|
||||
- Built-in types: all integer widths, `bool`, `f32`, `f64`, `rune`,
|
||||
`str`, `void`, pointers, slices `[]T`, fixed arrays `[N]T`,
|
||||
function types.
|
||||
- Slice semantics: `{ ptr, len, cap }`, indexing bounds-checked at
|
||||
runtime (debug builds; release may elide via flag).
|
||||
- `test/wwc/300_check.c` — table-driven: src → expected error or OK.
|
||||
- `test/lang/check/*.ww` — both passing and failing cases.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- All built-in types correctly checked.
|
||||
- Negative tests produce stable, useful diagnostics.
|
||||
|
||||
## Phase 4 — `6c` amd64 compiler
|
||||
|
||||
Goal: turn typed AST into Plan 9-style amd64 assembly text. The
|
||||
binary `6c` is a Plan 9 cc analogue for amd64. There is no separate
|
||||
SSA IR file; the only on-disk intermediate is `.s`.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/6c/` (links against `libwwc.a`):
|
||||
- `6.out.h` — amd64 opcode enum, register names, addressing
|
||||
modes. Mirror `ref/plan9front/sys/src/cmd/6c/` shape.
|
||||
- `gc.h` — `Prog`, `Adr`, scratch regs, stack layout.
|
||||
- `cgen.c` — typed AST → `Prog` list (walking, not SSA).
|
||||
- `txt.c` — `Prog` list → textual Plan 9 amd64 asm.
|
||||
- `swt.c`, `peep.c`, `reg.c` — switch lowering, peephole pass,
|
||||
naive register allocation (linear scan or spill-everywhere
|
||||
first; tighten later).
|
||||
- `mkfile` — Plan 9 mkfile next to the Makefile entries.
|
||||
- Output suffix `.s`. Plan 9 amd64 asm syntax (not GAS).
|
||||
- `test/lang/6c/*.ww` — golden tests: src → expected `.s` text.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `6c hello.ww` produces `hello.s`.
|
||||
- 20+ programs round-trip identically across runs.
|
||||
- The output is consumable by phase 5's `6a`.
|
||||
|
||||
## Phase 5 — `6a` amd64 assembler
|
||||
|
||||
Goal: assemble Plan 9 amd64 asm into ELF object files. ELF (not
|
||||
Plan 9 a.out) so we can interop with C archives in phase 8.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/6a/`:
|
||||
- `lex.c` — tokenize Plan 9 asm.
|
||||
- `parse.c` — hand-rolled grammar (Plan 9 uses yacc; we go
|
||||
hand-rolled to keep rule #6 honest).
|
||||
- `asm.c` — encode amd64 instructions (REX, ModR/M, SIB).
|
||||
- `obj.c` — emit ELF64 relocatable objects.
|
||||
- `mkfile`
|
||||
- Pseudoregisters supported: `SP`, `FP`, `SB` (static base) per
|
||||
Plan 9 convention, on top of `AX`, `BX`, ..., `R8`-`R15`.
|
||||
- `test/wwc/500_asm.c` — round-trip known asm to known bytes.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `6a hello.s -o hello.o` produces a valid ELF amd64 object.
|
||||
- `objdump -d hello.o` matches expectations across a 20-program
|
||||
corpus.
|
||||
|
||||
## Phase 6 — `6l` amd64 linker
|
||||
|
||||
Goal: link ELF objects into a static ELF executable.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/6l/`:
|
||||
- `obj.c` — load ELF `.o` files and `.a` archives.
|
||||
- `sym.c` — global symbol table; resolution.
|
||||
- `pass.c` — section layout, relocation application.
|
||||
- `out.c` — emit static ELF executable.
|
||||
- `mkfile`
|
||||
- Static linking only. No dynamic loader. No PT_INTERP segment.
|
||||
- `lib/libwwrt.a` (built later in phase 7) is auto-included.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `6l -o hello hello.o libwwrt.a` produces a static ELF binary.
|
||||
- `ldd hello` reports "not a dynamic executable".
|
||||
- The binary exits 0 with the program's intended semantics.
|
||||
|
||||
## Phase 7 — Runtime + driver wiring
|
||||
|
||||
Goal: a tiny static runtime, plus the `ww` driver wired to call
|
||||
`6c → 6a → 6l` end to end.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `rt/start.s` — entry, sets up argc/argv, calls `main`.
|
||||
- `rt/syscall.s` — Linux syscall trampoline.
|
||||
- `rt/panic.c` — `panic`, `abort`, stack unwind for `defer`.
|
||||
- `rt/mem.c` — small page allocator built on `mmap`. Users get
|
||||
raw pages; higher-level allocators ship in `lib/`.
|
||||
- `rt/slice.c` — slice helpers used by codegen
|
||||
(`bounds_check`, `slice_grow` for explicit user calls).
|
||||
- Build artifact: `out/lib/libwwrt.a`.
|
||||
- `cmd/ww/` driver: reads `ww build foo.ww`, runs `6c` then `6a`
|
||||
then `6l`, links `libwwrt.a` into the output.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A ww program with no libc dependency runs.
|
||||
- Adding `use os` pulls in syscall wrappers; still static.
|
||||
|
||||
## Phase 8 — C FFI + static linking C libs
|
||||
|
||||
Goal: bind to C libraries; produce static binaries containing them.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- Body-less `fn` declarations + `@symbol("name")` attribute parser
|
||||
and checker rules already in phases 2/3 — finalize codegen.
|
||||
- amd64 SysV ABI: formalize struct-by-value and varargs in `6c`.
|
||||
- `6l` learns to slurp static archives from a search path.
|
||||
- `lib/c/libc/` — minimal libc bindings (`malloc`, `free`, `printf`,
|
||||
`read`, `write`, `open`, `close`).
|
||||
- `lib/c/tls/` — bindings to libtls or BearSSL (decide; BearSSL is
|
||||
more aligned with our static-linking story).
|
||||
- `lib/c/crypto/` — bindings to libcrypto subset (sha256, aes, hmac).
|
||||
- `lib/c/curses/` — ncurses bindings (initscr, getch, mvprintw, ...).
|
||||
- `ww` driver: `ww build -lcrypto -ltls -lncurses` resolves these
|
||||
via a `pkg-config`-style probe, hands `.a` paths to `6l`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `examples/tlsclient.ww` connects to https://example.org and
|
||||
prints the response. Statically linked. No `.so` deps in `ldd`.
|
||||
- `examples/menu.ww` runs an ncurses TUI.
|
||||
|
||||
## Phase 9 — Standard library (Hare-shaped)
|
||||
|
||||
Goal: a usable stdlib. Each module ships with tests.
|
||||
|
||||
Order of build (each is its own milestone):
|
||||
|
||||
1. `types` limits, int helpers
|
||||
2. `bytes` slice ops on `[]u8`
|
||||
3. `strings` ops on `str`
|
||||
4. `io` `stream` struct (function-pointer vtable, no interface);
|
||||
`eof` sentinel value
|
||||
5. `bufio` buffered reader/writer (Plan 9 `bio` analogue)
|
||||
6. `fmt` printf-family writing through `io.stream`
|
||||
7. `os` argv, env, stdin/out/err, file ops
|
||||
8. `errors` `error = str` and sentinel constants
|
||||
9. `strconv` itoa, atoi, parse float
|
||||
10. `sort` `sort.slice`, binary search
|
||||
11. `path` filepath ops
|
||||
12. `encoding/utf8`, `encoding/hex`, `encoding/base64`
|
||||
13. `hash/crc32`, `hash/fnv`, `hash/sha256` (pure ww)
|
||||
14. `time` monotonic, wall clock, sleep
|
||||
15. `net` dial/listen TCP, UDP
|
||||
|
||||
Each module follows the Hare layout: one directory, multiple files,
|
||||
`+linux.ww`, `+test.ww` overlays where useful.
|
||||
|
||||
Exit criteria per module:
|
||||
|
||||
- Module compiles standalone with `ww build ./lib/<mod>`.
|
||||
- Module tests pass: `ww test ./lib/<mod>`.
|
||||
- Public API documented in `README` inside the module dir.
|
||||
|
||||
## Phase 10 — Self-host
|
||||
|
||||
Goal: rewrite `libwwc`, `6c`, `6a`, `6l`, and `ww` in ww. Drop the
|
||||
C bootstrap.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Translate `ww.h` and `Node`/`Sym`/`Type`/`Prog`/`Adr` to ww
|
||||
structs.
|
||||
2. Port `lex.c` → `lex.ww`. Diff token streams against C output.
|
||||
3. Port `parse.c` → `parse.ww`. Diff ASTs.
|
||||
4. Port `check.c`. Diff typed ASTs.
|
||||
5. Port `6c` (cgen, txt, peep, reg, swt). Diff `.s` output byte
|
||||
for byte.
|
||||
6. Port `6a` and `6l`. Diff resulting `.o` and final binaries.
|
||||
7. Three-stage bootstrap:
|
||||
`Cstage (gcc-built tools) → ww1 → ww2 → ww3`, with
|
||||
`cmp ww2 ww3` OK for each tool.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `make bootstrap` runs the three-stage build green.
|
||||
- The C `cmd/wwc/`, `cmd/6c/`, `cmd/6a/`, `cmd/6l/` C trees are
|
||||
deleted in this phase's final commit.
|
||||
|
||||
## Phase 11 — CSP
|
||||
|
||||
Goal: channels and lightweight processes, without GC.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- Runtime scheduler: cooperative, M:N, with stack-per-proc (start
|
||||
with fixed-size stacks; segmented stacks are a research item).
|
||||
- `proc f(args)` statement: spawns a process. Fire-and-forget by
|
||||
default. To keep a handle, use the stdlib spawner
|
||||
(`sched.spawn(f, args)` returns a `procval`). No GC means proc
|
||||
lifetimes must be explicit.
|
||||
- `chan T` type. `c <- v` send; `let v = <-c` receive;
|
||||
`select { ... }`.
|
||||
- Channel ownership: closing a channel is a single-writer
|
||||
responsibility (Go rule). Buffered and unbuffered both supported.
|
||||
- `sync` package: `mutex`, `waitgroup`, `once`.
|
||||
|
||||
Concurrency-safety constraints (because no GC):
|
||||
|
||||
- Sending a pointer over a channel transfers ownership unless the
|
||||
type is explicitly marked `shared`. The checker enforces this.
|
||||
- No data races by construction in the safe subset; `unsafe` pkg
|
||||
exists for power users.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Classic CSP examples (ping-pong, prime sieve, fan-in/fan-out)
|
||||
compile and run.
|
||||
- Race detector under `-race` (later sub-phase) catches a planted
|
||||
race in tests.
|
||||
|
||||
## Test cadence
|
||||
|
||||
Every phase has its own test directory (`test/wwc/<phase>_*.c`,
|
||||
`test/lang/<phase-area>/*.ww`). `make test` runs them all in order
|
||||
of phase. CI is just `make test` in a clean tree.
|
||||
|
||||
## Versioning checkpoints
|
||||
|
||||
Tag the tree at each phase boundary:
|
||||
|
||||
- `v0.0` end of phase 0
|
||||
- `v0.1` end of phase 1
|
||||
- ...
|
||||
- `v0.10` end of phase 10 (self-hosted)
|
||||
- `v1.0` end of phase 11 (CSP merged, ABI declared stable enough
|
||||
to start caring)
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- **`Prog`/`Adr` design churn.** Mitigate by reading Plan 9 cc and
|
||||
per-target compilers before we write our own. Don't invent until
|
||||
you've copied.
|
||||
- **Writing a linker is hard.** ELF is documented, static linking
|
||||
is the small subset. Read `ref/plan9front/sys/src/cmd/8l/` and
|
||||
the ELF spec before writing `6l`. Start with hello-world and grow.
|
||||
- **Static linking against C libs.** OpenSSL is a pain to build
|
||||
static; BearSSL or LibreTLS are more pleasant. Pick early.
|
||||
- **Self-host bootstrap divergence.** Diff outputs at every stage,
|
||||
not just the final binary. Token streams, ASTs, and `.s` text
|
||||
must match.
|
||||
- **CSP without GC.** Channel-of-pointer ownership rules are the
|
||||
hard part. Prototype the checker with `unsafe`-allowed first;
|
||||
tighten later.
|
||||
|
||||
## Non-goals (restated)
|
||||
|
||||
- No package manager.
|
||||
- No generics, no interfaces, no tagged unions, no closures, no
|
||||
lambdas.
|
||||
- No async/await.
|
||||
- No `map`.
|
||||
- No GC.
|
||||
- No LLVM, no QBE, no external compiler infrastructure. The
|
||||
toolchain is `cc/8c/8a/8l`-shaped (Plan 9), per target.
|
||||
1
ref/hare
Submodule
1
ref/hare
Submodule
Submodule ref/hare added at b2e3defdd8
1
ref/plan9front
Submodule
1
ref/plan9front
Submodule
Submodule ref/plan9front added at 6dc60ec3a9
Reference in New Issue
Block a user