ww: hare-feature batch (_, const, [_]T, ..., size/offset, assert, for-else)
This commit is contained in:
415
CLAUDE.md
415
CLAUDE.md
@@ -1,409 +1,6 @@
|
||||
# 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.
|
||||
1. This is a project for ww programming language
|
||||
2. ww is a programming language aiming for Hare + CSP, no GC
|
||||
3. cmd/ is the C bootstrap toolchain (wcc frontend lib, w6c/w6a/w6l per-arch, ww driver); selfhost/ is the ww reimplementation
|
||||
4. All C code must strictly align with plan 9 coding style
|
||||
5. lib/ is the standard library; follow Hare APIs (signatures, layout, error idioms) — consult ref/hare/ before designing new modules
|
||||
6. ref/hare and ref/plan9front are read-only references — consult before inventing data shapes or syntax
|
||||
|
||||
Reference in New Issue
Block a user