ww: hare-feature batch (_, const, [_]T, ..., size/offset, assert, for-else)

This commit is contained in:
2026-05-11 19:38:12 +09:00
parent 579cc39f9b
commit 6219a47c6f
25 changed files with 1496 additions and 1049 deletions

View File

@@ -62,7 +62,7 @@ Cstage counterparts. Test 995 pins the stronger property — the
wwstage rebuilds every one of its own tools through `ww_ww + w6c_ww +
w6a_ww + w6l_ww`, byte-for-byte. Test 996 pins the equivalent for the
dynamic linker: `w6l_ww` with `-L`/`-l` produces a byte-identical
PT_INTERP+PT_DYNAMIC binary to C-side `w6l` on snake.
PT_INTERP+PT_DYNAMIC binary to C-side `w6l`.
`make nocc` is wired and verifies stage-0 self-reproduction locally;
the stage-0 binaries under `bootstrap/$(ARCH)/` are gitignored until

415
CLAUDE.md
View File

@@ -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

409
PLAN.md
View File

@@ -1,409 +0,0 @@
# 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/wcc/` C library skeleton (builds `libwcc.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/wcc/000_smoke.c` — smoke test that asserts `ww -V` prints
the version.
Per-target binaries (`w6c`, `w6a`, `w6l`) are NOT created here. They
ship in their own phases (4, 5, 6).
Exit criteria:
- `make` produces `out/bin/ww` and `out/lib/libwcc.a`.
- `make test` runs the smoke test and passes.
## Phase 1 — Lexer (in `libwcc.a`)
Goal: tokenize ww source into a stream of `Tok` structs.
Deliverables:
- `cmd/wcc/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/wcc/tok.c` — token names, debug printer.
- `test/wcc/100_lex.c` — table-driven: input → token sequence.
- `test/lang/lex/*.ww` — small files exercised via a tiny test
harness that links against `libwcc.a`.
Exit criteria:
- A `lextool` test binary dumps a representative sample's token
stream deterministically.
- All lex tests green.
## Phase 2 — Parser & AST (in `libwcc.a`)
Goal: parse the full grammar into an AST. No types yet.
Deliverables:
- `cmd/wcc/parse.c` — recursive descent. Pratt expression parser for
precedence. No yacc.
- `cmd/wcc/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/wcc/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 `libwcc.a`)
Goal: resolve names, infer/check types, produce a typed AST.
Deliverables:
- `cmd/wcc/sym.c` — symbol table. Lexical scopes. Plan 9 style hash.
- `cmd/wcc/type.c` — type representation, equality, conversion rules.
- `cmd/wcc/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/wcc/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 — `w6c` amd64 compiler
Goal: turn typed AST into Plan 9-style amd64 assembly text. The
binary `w6c` is a Plan 9 cc analogue for amd64. There is no separate
SSA IR file; the only on-disk intermediate is `.s`.
Deliverables:
- `cmd/w6c/` (links against `libwcc.a`):
- `6.out.h` — amd64 opcode enum, register names, addressing
modes. Mirror `ref/plan9front/sys/src/cmd/w6c/` 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/w6c/*.ww` — golden tests: src → expected `.s` text.
Exit criteria:
- `w6c hello.ww` produces `hello.s`.
- 20+ programs round-trip identically across runs.
- The output is consumable by phase 5's `w6a`.
## Phase 5 — `w6a` 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/w6a/`:
- `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/wcc/500_asm.c` — round-trip known asm to known bytes.
Exit criteria:
- `w6a hello.s -o hello.o` produces a valid ELF amd64 object.
- `objdump -d hello.o` matches expectations across a 20-program
corpus.
## Phase 6 — `w6l` amd64 linker
Goal: link ELF objects into a static ELF executable.
Deliverables:
- `cmd/w6l/`:
- `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:
- `w6l -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
`w6c → w6a → w6l` 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 `w6c` then `w6a`
then `w6l`, 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 `w6c`.
- `w6l` 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 `w6l`.
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 `libwcc`, `w6c`, `w6a`, `w6l`, 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 `w6c` (cgen, txt, peep, reg, swt). Diff `.s` output byte
for byte.
6. Port `w6a` and `w6l`. 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/wcc/`, `cmd/w6c/`, `cmd/w6a/`, `cmd/w6l/` C trees are
deleted in this phase's final commit.
### Status (2026-05)
Steps 16 done. The five ww-side tools live in `selfhost/cmd/`:
- `wwc/` — ww-native lex/parse/check/cgen (the frontend library)
- `w6c/` — ww-native compiler binary; thin driver over wwc's cgen
- `w6a/` — ww-native amd64 assembler
- `w6l/` — ww-native amd64 linker
- `ww/` — ww-native driver, shells to `w6c_ww/w6a_ww/w6l_ww`
Step 7 reaches its fixed point: `make bootstrap` produces ww1 → ww2 →
ww3 with `cmp ww2 ww3` byte-identical. Tests 990994 confirm each
selfhost tool is byte-identical to its Cstage counterpart on the
wwdump-corpus (and `ww_ww build` is byte-identical to `ww build` on
test 993's hello + wwdump corpus, despite using a different
toolchain underneath).
`ww_ww build foo.ww` is now C-free at runtime: the driver invokes
the wwstage tools end-to-end. Cstage is still required at first-
checkout time (no checked-in stage-0 binary yet). ET_DYN dynamic
linking is also fully ported to the ww side (test 996 confirms
`w6l_ww -L ... -l ...` is byte-identical to `w6l` on snake); the one
remaining gap is that `ww_ww build`'s argv parser does not yet
forward `-L`/`-l` to the linker — `w6l_ww` accepts them when invoked
directly, just not through the ww-side driver. Snake's Makefile
still drives the C `ww` for that reason; switching it to `w6l_ww` +
explicit args works today.
Exit criterion 1 (bootstrap green) is met. Exit criterion 2 (delete
the C trees) is **deferred to v1.0**: removing `cmd/wcc/`, `cmd/w6c/`,
`cmd/w6a/`, `cmd/w6l/`, `cmd/ww/` is a one-way door. Until the compiler
stops churning we keep Cstage as the canonical fresh-checkout entry
point and treat `selfhost/cmd/*` as the path forward. At v1.0, the
plan is to ship a checked-in stage-0 binary archive
(`bootstrap/<arch>/{ww,w6c,w6a,w6l}`), delete the C trees, and promote
`selfhost/cmd/*` to `cmd/*`.
See `BOOTSTRAP.md` for the build flow and `make cstage`/`make wwstage`
targets.
## 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/wcc/<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 `w6l`. 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.

6
cmd/w6c/CLAUDE.md Normal file
View File

@@ -0,0 +1,6 @@
w6c — amd64 compiler. Reads `.ww`, writes Plan 9 amd64 assembly (`.s`).
- Plan 9 C style: tabs, K&R, short names. Mirror `ref/plan9front/sys/src/cmd/8c/` and `ref/plan9front/sys/src/cmd/cc/`. Do not invent new data shapes — reuse `Prog`/`Adr`/`Node`/`Sym`/`Type` conventions.
- Frontend (lex, parse, check) lives in `../wcc/` and links as `libwcc.a`. This directory owns codegen only.
- `gc.h` is the per-target header (Plan 9 cc convention). `cgen.c` walks the AST into `Prog` list; `txt.c` prints it. `peep.c` is peephole. `reg.c` is the register allocator. `swt.c` is switch lowering.
- The text output must be readable by `w6a` (the amd64 assembler under `../w6a/`). When in doubt about syntax, run `w6a` on the output.

View File

@@ -856,6 +856,14 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
case N_ASSIGN: {
/* Discard lvalue `_ = expr;` — evaluate rhs for side effects,
* write nothing. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && n->lhs->str[0] == '\0' &&
n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
break;
}
/* p.x = v or p.x += v where p.x is a struct field
* (direct or via *struct). For compound ops we read-modify-
* write the field; for plain `=` we just write. */
@@ -1234,6 +1242,49 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
case N_CALL: {
/* abort([msg]) — call rt_abort. Empty msg becomes (NULL, 0).
* Only fires when the checker tagged the callee as a builtin
* (lhs->type == ty_err); a user-declared `abort` in scope is
* resolved through the regular call path. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
n->lhs->type == ty_err &&
strcmp(n->lhs->str, "abort") == 0) {
if (n->list) {
cgexpr(c, n->list, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
} else {
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
}
ins1(c, A_CALL, asym("rt_abort"));
break;
}
/* assert(cond[, msg]) — if !cond, call rt_abort. Compiles to:
* CMPQ $0, AX
* JNE skip
* <abort body>
* skip: */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
n->lhs->type == ty_err &&
strcmp(n->lhs->str, "assert") == 0 && n->list) {
cgexpr(c, n->list, locals);
char *skip = mklabel(c, "as");
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(skip));
Node *msg = n->list->next;
if (msg) {
cgexpr(c, msg, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
} else {
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
}
ins1(c, A_CALL, asym("rt_abort"));
label(c, skip);
break;
}
/* Hare-style builtins: len(x) and append(s, v). */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "len") == 0 && n->list) {
@@ -2257,9 +2308,34 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
break;
}
/* struct literal initialiser: field-by-field store. */
/* struct literal initialiser: field-by-field store. The
* literal carries op == TK_ELLIPSIS when the source ends in
* `..., ...` — in that case zero-fill the entire slot first,
* so unmentioned fields read as 0. */
if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu
&& lu->kind == TY_STRUCT) {
if (n->rhs->op == TK_ELLIPSIS) {
u64 sz = lu->size;
/* AX = 0 once, then store from AX. w6a doesn't
* accept MOVB imm,mem — use register stores. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
u64 i = 0;
while (i + 8 <= sz) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + (int)i));
i += 8;
}
while (i + 4 <= sz) {
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, off + (int)i));
i += 4;
}
while (i < sz) {
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, off + (int)i));
i += 1;
}
}
for (Node *f = n->rhs->list; f; f = f->next) {
/* find offset of this field */
u64 foff = 0;
@@ -2315,6 +2391,46 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
}
break;
}
/* array literal initialiser: `let xs: [N]T = [a, b, c];`.
* Walk elements in declaration order, store each at off + i*esz
* using the right width for the element type. The trailing
* `...` repeat marker (an N_FIELD with str=="...") fills the
* remaining slots with the last value. */
if (n->rhs && n->rhs->kind == N_ARRLIT && lu
&& lu->kind == TY_ARRAY) {
int esz = lu->sub ? (int)lu->sub->size : 1;
int op = A_MOVQ;
if (esz == 1) op = A_MOVB;
else if (esz == 4) op = A_MOVL;
/* esz == 2 (i16/u16) falls through to MOVQ — over-writes
* by 6B; the next element store rewrites the high half.
* For the last element this trails 6 bytes into the next
* stack slot. Add MOVW to w6a if real i16 arrays land. */
int idx = 0;
Node *last = NULL;
int repeat = 0;
for (Node *e = n->rhs->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str &&
strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
cgexpr(c, e, *locals);
ins2(c, op, areg(D_AX),
amem(D_BP, off + idx * esz));
last = e;
idx++;
}
if (repeat && last) {
/* fill remaining slots with the value already in AX. */
while (idx < (int)lu->alen) {
ins2(c, op, areg(D_AX),
amem(D_BP, off + idx * esz));
idx++;
}
}
break;
}
if (n->rhs && sz == 8) {
cgexpr(c, n->rhs, *locals);
if (isf) {
@@ -2488,6 +2604,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
char *loop = mklabel(c, "rloop");
char *end = mklabel(c, "rend");
char *natural_exit = end;
if (n->els) natural_exit = mklabel(c, "relseloop");
if (nloops < LOOP_MAX) {
loop_cont[nloops] = loop;
loop_brk[nloops] = end;
@@ -2497,7 +2615,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins2(c, A_MOVQ, amem(D_BP, ioff), areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_BX));
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
ins1(c, A_JGE, abranch(end));
ins1(c, A_JGE, abranch(natural_exit));
/* compute element base: s.ptr + i*esz → BX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
@@ -2523,6 +2641,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
cgstmt(c, n->body, locals, frame);
ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff));
ins1(c, A_JMP, abranch(loop));
if (n->els) {
label(c, natural_exit);
cgstmt(c, n->els, locals, frame);
}
label(c, end);
if (nloops > 0) nloops--;
break;
@@ -2530,12 +2652,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
case N_FOR: {
char *loop = mklabel(c, "loop");
char *end = mklabel(c, "endloop");
/* `else` runs at normal cond-false exit; break skips it.
* Separate the natural exit label from the break target so
* the else block sits between them. */
char *natural_exit = end;
if (n->els) natural_exit = mklabel(c, "elseloop");
if (n->lhs) cgstmt(c, n->lhs, locals, frame);
label(c, loop);
if (n->cond) {
cgexpr(c, n->cond, *locals);
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JE, abranch(end));
ins1(c, A_JE, abranch(natural_exit));
}
if (nloops < LOOP_MAX) {
loop_cont[nloops] = loop;
@@ -2546,6 +2673,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
if (nloops > 0) nloops--;
if (n->rhs) cgexpr(c, n->rhs, *locals);
ins1(c, A_JMP, abranch(loop));
if (n->els) {
label(c, natural_exit);
cgstmt(c, n->els, locals, frame);
}
label(c, end);
break;
}

View File

@@ -91,10 +91,15 @@ resolve_type(Checker *c, Node *n)
return type_slice(c->a, resolve_type(c, n->lhs));
case N_TARRAY: {
u64 len = 0;
if (n->rhs && n->rhs->kind == N_INTLIT)
if (n->rhs == NULL) {
/* `[_]T` — length inferred at the use site (currently
* only `let x: [_]T = arrlit;`). Leave alen=0 as a
* sentinel; clet patches it from the initialiser. */
} else if (n->rhs->kind == N_INTLIT) {
len = n->rhs->uval;
else
} else {
err(c, n->pos, "array length must be an integer literal");
}
return type_array(c->a, resolve_type(c, n->lhs), len);
}
case N_TCHAN:
@@ -304,6 +309,9 @@ cexpr(Checker *c, Node *n)
case N_FALSE: n->type = ty_untyped_bool; return n->type;
case N_NIL: n->type = ty_untyped_nil; return n->type;
case N_IDENT: {
if (n->str && n->str[0] == '\0')
return n->type = err(c, n->pos,
"`_` is only valid as a binding or discard lvalue");
Sym *s = scope_lookup(c->cur, n->str);
if (s == NULL)
return n->type = err(c, n->pos, "undefined: %s", n->str);
@@ -417,6 +425,59 @@ cexpr(Checker *c, Node *n)
n->lhs->type = ty_err; /* mark builtin: no real symbol */
return n->type;
}
/* size(T) / align(T): fold to an integer literal. The arg is
* a type-expr node (planted by the parser, not a regular
* expression). */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
(strcmp(n->lhs->str, "size") == 0 ||
strcmp(n->lhs->str, "align") == 0) &&
n->list != NULL) {
int is_size = strcmp(n->lhs->str, "size") == 0;
Type *t = resolve_type(c, n->list);
u64 v = 0;
if (t && t != ty_err) v = is_size ? t->size : t->align;
n->kind = N_INTLIT;
n->uval = v;
n->str = aprintf(c->a, "%llu", (unsigned long long)v);
n->strlen = strlen(n->str);
n->lhs = NULL;
n->list = NULL;
n->tsuffix = NULL;
n->type = ty_untyped_int;
return n->type;
}
/* offset(e.f): the byte offset of `f` inside the struct type of
* `e`. Folded to an integer literal at check-time. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "offset") == 0 &&
n->list != NULL && n->list->next == NULL &&
n->list->kind == N_DOT) {
Node *dot = n->list;
Type *bt = cexpr(c, dot->lhs);
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
if (u && u->kind == TY_PTR) u = u->sub;
if (u && u->kind == TY_NAMED) u = u->under;
u64 off = 0;
int found = 0;
if (u && u->kind == TY_STRUCT) {
for (Tfield *f = u->fields; f; f = f->next)
if (strcmp(f->name, dot->str) == 0) {
off = f->offset; found = 1; break;
}
}
if (!found)
err(c, n->pos, "offset: no field '%s'",
dot->str ? dot->str : "?");
n->kind = N_INTLIT;
n->uval = off;
n->str = aprintf(c->a, "%llu", (unsigned long long)off);
n->strlen = strlen(n->str);
n->lhs = NULL;
n->list = NULL;
n->tsuffix = NULL;
n->type = ty_untyped_int;
return n->type;
}
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && strcmp(n->lhs->str, "append") == 0 &&
n->list != NULL && n->list->next != NULL) {
@@ -443,6 +504,43 @@ cexpr(Checker *c, Node *n)
n->lhs->type = ty_err;
return n->type;
}
/* assert(cond[, msg]) / abort([msg]) — runtime checks that
* call into rt_abort. msg must be a str when present.
* Only treated as builtins when no user symbol shadows the
* name; existing code that declares its own `abort`/`assert`
* (e.g. lib/os/os.ww) keeps working unchanged. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "abort") == 0 &&
scope_lookup(c->cur, "abort") == NULL) {
if (n->list) {
Type *mt = cexpr(c, n->list);
if (mt != ty_err && !type_assignable(ty_str, mt))
err(c, n->pos, "abort: message must be str");
if (n->list->next)
err(c, n->pos, "abort: at most one arg");
}
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "assert") == 0 &&
n->list != NULL &&
scope_lookup(c->cur, "assert") == NULL) {
Type *ct = cexpr(c, n->list);
if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool)
err(c, n->pos, "assert: cond must be bool");
if (n->list->next) {
Type *mt = cexpr(c, n->list->next);
if (mt != ty_err && !type_assignable(ty_str, mt))
err(c, n->pos, "assert: message must be str");
if (n->list->next->next)
err(c, n->pos, "assert: at most two args");
}
n->type = ty_void;
n->lhs->type = ty_err;
return n->type;
}
/* alloc([], n) — Hare-style fresh slice with cap n. We pin
* the element type to u8 by default; the caller's declared
* slice type drives the actual element size at codegen. */
@@ -488,6 +586,19 @@ cexpr(Checker *c, Node *n)
return n->type = u->ret ? u->ret : ty_void;
}
case N_ASSIGN: {
/* `_` lvalue: discard the rhs. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && n->lhs->str[0] == '\0') {
(void)cexpr(c, n->rhs);
return n->type = ty_void;
}
/* Reject assignment to a const-bound name. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str) {
Sym *s = scope_lookup(c->cur, n->lhs->str);
if (s && s->is_const)
err(c, n->pos, "cannot assign to const `%s`",
n->lhs->str);
}
Type *l = cexpr(c, n->lhs);
Type *r = cexpr(c, n->rhs);
if (l != ty_err && r != ty_err && !type_assignable(l, r))
@@ -641,19 +752,45 @@ clet(Checker *c, Node *n)
Type *declared = n->lhs ? resolve_type(c, n->lhs) : NULL;
Type *initt = NULL;
if (n->rhs) initt = cexpr(c, n->rhs);
/* `let xs: [_]T = arrlit;` — fill in the inferred length from the
* initialiser. `resolve_type` left alen=0 as a sentinel. */
if (declared && declared->kind == TY_ARRAY && declared->alen == 0 &&
initt) {
Type *iu = (initt->kind == TY_NAMED) ? initt->under : initt;
if (iu && iu->kind == TY_ARRAY)
declared = type_array(c->a, declared->sub, iu->alen);
else
err(c, n->pos, "[_]T needs an array-literal initialiser");
}
Type *t = declared;
if (t == NULL && initt) t = type_default(initt);
if (t == NULL) {
err(c, n->pos, "let needs a type or initialiser");
t = ty_err;
}
if (declared && initt && initt != ty_err &&
/* An array literal with a trailing `...` repeat marker has
* "flexible" length — the last value fills the remaining slots.
* The literal's type carries the explicit-element count, which
* may not match the declared length. Trust the declared type
* when the marker is present. */
int has_arr_repeat = 0;
if (n->rhs && n->rhs->kind == N_ARRLIT) {
for (Node *e = n->rhs->list; e; e = e->next)
if (e->kind == N_FIELD && e->str &&
strcmp(e->str, "...") == 0) {
has_arr_repeat = 1;
break;
}
}
if (declared && initt && initt != ty_err && !has_arr_repeat &&
!type_assignable(declared, initt))
err(c, n->pos, "init %s not assignable to declared %s",
type_name(c->a, initt), type_name(c->a, declared));
n->type = t;
if (n->str && n->str[0])
scope_define(c->cur, n->str, SK_VAR, t, n);
if (n->str && n->str[0]) {
Sym *s = scope_define(c->cur, n->str, SK_VAR, t, n);
if (s && n->op == TK_CONST) s->is_const = 1;
}
}
static void
@@ -721,6 +858,7 @@ cstmt(Checker *c, Node *n)
}
cstmt(c, n->body);
c->loops--;
if (n->els) cstmt(c, n->els);
c->cur = saved;
break;
}
@@ -738,6 +876,10 @@ cstmt(Checker *c, Node *n)
if (n->rhs) (void)cexpr(c, n->rhs);
cstmt(c, n->body);
c->loops--;
/* `else` block: runs at normal cond-false exit (skipped by
* break). Outside the loop count — break/continue inside the
* else target an enclosing loop, not this one. */
if (n->els) cstmt(c, n->els);
c->cur = saved;
break;
}
@@ -759,8 +901,10 @@ cstmt(Checker *c, Node *n)
l->str, type_name(c->a, elem),
type_name(c->a, declared));
l->type = t;
if (l->str && l->str[0])
scope_define(c->cur, l->str, SK_VAR, t, l);
if (l->str && l->str[0]) {
Sym *s = scope_define(c->cur, l->str, SK_VAR, t, l);
if (s && n->op == TK_CONST) s->is_const = 1;
}
if (tp) tp = tp->next;
}
if (u && tp != NULL)
@@ -776,6 +920,11 @@ cstmt(Checker *c, Node *n)
}
Tparam *tp = u ? u->params : NULL;
for (Node *lv = n->list; lv; lv = lv->next) {
/* `_` lvalue: skip type check, advance the tuple cursor. */
if (lv->kind == N_IDENT && lv->str && lv->str[0] == '\0') {
if (tp) tp = tp->next;
continue;
}
Type *lt = cexpr(c, lv);
Type *elem = tp ? tp->type : NULL;
if (lt && elem && !type_assignable(lt, elem))

View File

@@ -317,6 +317,11 @@ lexident(Lex *l, Pos start)
lget(l);
u64 n = l->pos - begin;
const char *p = l->src + begin;
/* bare '_' is the discard marker. `_x`, `_1` are normal idents. */
if (n == 1 && p[0] == '_') {
Tok t = (Tok){ TK_UNDER, start, astrndup(l->a, p, n), n, {0}, TK_NONE };
return t;
}
Tkind k = kwlookup(p, n);
Tok t = (Tok){ k != TK_NONE ? k : TK_IDENT, start,
astrndup(l->a, p, n), n, {0}, TK_NONE };

View File

@@ -81,6 +81,19 @@ expectident(Parser *p)
return s;
}
/* Like expectident but also accepts a bare `_` discard marker. The
* returned string is the empty string "" so the checker skips
* scope_define. Callers that care can detect this with `s[0] == '\0'`. */
static const char *
expectbindname(Parser *p)
{
if (p->cur.kind == TK_UNDER) {
advance(p);
return "";
}
return expectident(p);
}
static Node *parseexpr(Parser *p);
static Node *parseunary(Parser *p);
static Node *parsetype(Parser *p);
@@ -108,11 +121,13 @@ parseparams(Parser *p)
break;
}
Node *n = newnode(p->a, N_PARAM, pp);
/* IDENT ':' type OR type-only (fn-type params).
* Disambiguate: if current is IDENT and next is ':' it's named.
* Otherwise treat as anonymous (type-only). */
if (p->cur.kind == TK_IDENT && peek(p).kind == TK_COLON) {
n->str = expectident(p);
/* IDENT ':' type OR `_' ':' type OR type-only.
* Disambiguate: if current is IDENT or '_' and next is ':',
* it's a named param. Otherwise treat as anonymous. */
int named = (p->cur.kind == TK_IDENT || p->cur.kind == TK_UNDER)
&& peek(p).kind == TK_COLON;
if (named) {
n->str = expectbindname(p);
expect(p, TK_COLON);
n->lhs = parsetype(p);
} else {
@@ -150,7 +165,11 @@ parsetype(Parser *p)
return n;
}
Node *n = newnode(p->a, N_TARRAY, pp);
n->rhs = parseexpr(p);
/* `[_]T` — length inferred from the initialiser. Marked by
* leaving n->rhs == NULL; check.c fills in the length from
* the array literal's element count. */
if (!accept(p, TK_UNDER))
n->rhs = parseexpr(p);
expect(p, TK_RBRACK);
n->lhs = parsetype(p);
return n;
@@ -332,6 +351,14 @@ parsestructlit(Parser *p, Node *typeref)
Node *head = NULL, *tail = NULL;
while (p->cur.kind != TK_RBRACE && p->cur.kind != TK_EOF) {
Pos fp = p->cur.pos;
/* Trailing `...` after the last comma (or as the only entry)
* means "zero-init all unmentioned fields". Marked on the
* literal node via op = TK_ELLIPSIS; cgen consumes it. */
if (p->cur.kind == TK_ELLIPSIS) {
advance(p);
n->op = TK_ELLIPSIS;
break;
}
const char *name = expectident(p);
expect(p, TK_ASSIGN);
Node *val = parseexpr(p);
@@ -416,6 +443,16 @@ parseprimary(Parser *p)
case TK_TRUE: advance(p); return newnode(p->a, N_TRUE, pp);
case TK_FALSE: advance(p); return newnode(p->a, N_FALSE, pp);
case TK_NIL: advance(p); return newnode(p->a, N_NIL, pp);
case TK_UNDER: {
/* Bare `_` — valid only as a discard lvalue. We yield an N_IDENT
* with empty str; the checker rejects it outside assignment
* lvalue positions. */
advance(p);
Node *n = newnode(p->a, N_IDENT, pp);
n->str = "";
n->strlen = 0;
return n;
}
case TK_LPAREN: {
advance(p);
Node *e = parseexpr(p);
@@ -535,7 +572,16 @@ parsepostfix(Parser *p, Node *lhs)
advance(p);
Node *n = newnode(p->a, N_CALL, pp);
n->lhs = lhs;
n->list = parsearglist(p, TK_RPAREN);
/* size(T)/align(T): the single arg is a type expression,
* not a regular expression — types like []u8 cannot parse
* as expressions. Special-case at the parser. */
if (lhs->kind == N_IDENT && lhs->str &&
(strcmp(lhs->str, "size") == 0 ||
strcmp(lhs->str, "align") == 0)) {
n->list = parsetype(p);
} else {
n->list = parsearglist(p, TK_RPAREN);
}
expect(p, TK_RPAREN);
lhs = n;
break;
@@ -715,7 +761,13 @@ static Node *
parselet(Parser *p, int top)
{
Pos pp = p->cur.pos;
expect(p, TK_LET);
int is_const = 0;
if (p->cur.kind == TK_CONST) {
is_const = 1;
advance(p);
} else {
expect(p, TK_LET);
}
/* Hare-style tuple destructure: `let (a, b) = expr;` */
if (p->cur.kind == TK_LPAREN) {
@@ -725,7 +777,7 @@ parselet(Parser *p, int top)
for (;;) {
Pos lpp = p->cur.pos;
Node *l = newnode(p->a, N_LET, lpp);
l->str = expectident(p);
l->str = expectbindname(p);
if (accept(p, TK_COLON)) l->lhs = parsetype(p);
if (head == NULL) head = l;
else tail->next = l;
@@ -737,6 +789,10 @@ parselet(Parser *p, int top)
m->rhs = parseexpr(p);
expect(p, TK_SEMI);
m->list = head;
if (is_const) {
m->op = TK_CONST;
for (Node *l = head; l; l = l->next) l->op = TK_CONST;
}
(void)top;
return m;
}
@@ -744,7 +800,7 @@ parselet(Parser *p, int top)
/* parse first binding */
Pos lp = p->cur.pos;
Node *first = newnode(p->a, N_LET, lp);
first->str = expectident(p);
first->str = expectbindname(p);
if (accept(p, TK_COLON))
first->lhs = parsetype(p);
@@ -755,7 +811,7 @@ parselet(Parser *p, int top)
while (accept(p, TK_COMMA)) {
Pos lpp = p->cur.pos;
Node *l = newnode(p->a, N_LET, lpp);
l->str = expectident(p);
l->str = expectbindname(p);
if (accept(p, TK_COLON))
l->lhs = parsetype(p);
tail->next = l;
@@ -765,6 +821,10 @@ parselet(Parser *p, int top)
m->rhs = parseexpr(p);
expect(p, TK_SEMI);
m->list = head;
if (is_const) {
m->op = TK_CONST;
for (Node *l = head; l; l = l->next) l->op = TK_CONST;
}
(void)top;
return m;
}
@@ -772,6 +832,7 @@ parselet(Parser *p, int top)
if (accept(p, TK_ASSIGN))
first->rhs = parseexpr(p);
expect(p, TK_SEMI);
if (is_const) first->op = TK_CONST;
(void)top;
return first;
}
@@ -795,6 +856,17 @@ parseif(Parser *p)
return n;
}
static Node *
parse_for_else(Parser *p, Node *n)
{
/* `for (cond) { body } else { else_body }` — the else block runs
* when the loop exits normally (cond → false) and is skipped by
* `break`. Hare-style "did the loop find anything" idiom. */
if (accept(p, TK_ELSE))
n->els = parseblock(p);
return n;
}
static Node *
parsefor(Parser *p)
{
@@ -803,7 +875,7 @@ parsefor(Parser *p)
Node *n = newnode(p->a, N_FOR, pp);
if (p->cur.kind == TK_LBRACE) {
n->body = parseblock(p);
return n;
return parse_for_else(p, n);
}
expect(p, TK_LPAREN);
if (p->cur.kind == TK_RPAREN) {
@@ -826,7 +898,7 @@ parsefor(Parser *p)
for (;;) {
Pos np = p->cur.pos;
Node *e = newnode(p->a, N_IDENT, np);
e->str = expectident(p);
e->str = expectbindname(p);
if (names == NULL) names = e;
else tail->next = e;
tail = e;
@@ -840,29 +912,30 @@ parsefor(Parser *p)
rng->lhs = parseexpr(p);
expect(p, TK_RPAREN);
rng->body = parseblock(p);
return rng;
return parse_for_else(p, rng);
}
if (p->cur.kind == TK_IDENT) {
if (p->cur.kind == TK_IDENT || p->cur.kind == TK_UNDER) {
Pos ip = p->cur.pos;
const char *nm = p->cur.text;
int isunder = p->cur.kind == TK_UNDER;
Tok la = peek(p);
if (la.kind == TK_DOTDOT) {
advance(p); /* consume IDENT */
advance(p); /* consume IDENT/UNDER */
advance(p); /* consume DOTDOT */
Node *rng = newnode(p->a, N_FORRANGE, pp);
rng->str = nm;
rng->str = isunder ? "" : nm;
rng->lhs = parseexpr(p);
expect(p, TK_RPAREN);
rng->body = parseblock(p);
(void)ip;
return rng;
return parse_for_else(p, rng);
}
}
/* Not a range. Build a synthetic LET stmt manually
* since we already consumed `let`. */
Pos lp = p->cur.pos;
Node *first = newnode(p->a, N_LET, lp);
first->str = expectident(p);
first->str = expectbindname(p);
if (accept(p, TK_COLON))
first->lhs = parsetype(p);
if (accept(p, TK_ASSIGN))
@@ -885,7 +958,7 @@ parsefor(Parser *p)
}
expect(p, TK_RPAREN);
n->body = parseblock(p);
return n;
return parse_for_else(p, n);
}
static Node *
@@ -953,7 +1026,8 @@ parsestmt(Parser *p)
expect(p, TK_SEMI);
return b;
}
case TK_LET: return parselet(p, 0);
case TK_LET:
case TK_CONST: return parselet(p, 0);
case TK_IF: {
Node *n = parseif(p);
expect(p, TK_SEMI);
@@ -1166,7 +1240,8 @@ parsefile(Parser *p)
case TK_DEF: d = parsedef(p, exp); break;
case TK_TYPE: d = parsetypedecl(p, exp); break;
case TK_FN: d = parsefn(p, exp, attrs); break;
case TK_LET: d = parselet(p, 1); d->export = exp; break;
case TK_LET:
case TK_CONST: d = parselet(p, 1); d->export = exp; break;
default:
errorf(p->cur.pos, "expected top-level decl, got %s",
tokname(p->cur.kind));

View File

@@ -19,6 +19,7 @@ static const struct kwent kwtab[] = {
{ "break", TK_BREAK },
{ "case", TK_CASE },
{ "chan", TK_CHAN },
{ "const", TK_CONST },
{ "continue", TK_CONTINUE },
{ "def", TK_DEF },
{ "defer", TK_DEFER },
@@ -90,6 +91,8 @@ tokname(Tkind k)
case TK_AS: return "as";
case TK_STATIC: return "static";
case TK_MATCH: return "match";
case TK_CONST: return "const";
case TK_UNDER: return "_";
case TK_LPAREN: return "(";
case TK_RPAREN: return ")";

View File

@@ -114,6 +114,8 @@ typedef enum {
TK_AS, /* reserved for future cast spelling, not active */
TK_STATIC, /* Hare-style storage-class qualifier */
TK_MATCH, /* match expression head */
TK_CONST, /* const binding */
TK_UNDER, /* bare '_' discard */
/* punct + operators */
TK_LPAREN, /* ( */
@@ -433,6 +435,7 @@ struct Sym {
Type *type;
Node *decl;
int exported;
int is_const; /* const-bound (assignment rejected) */
Sym *next; /* iteration */
Sym *hashnext; /* bucket chain */
Scope *scope;

13
lib/ww/CLAUDE.md Normal file
View File

@@ -0,0 +1,13 @@
lib/ww — frontend in ww (lex, parse, sym, typ, ast). Mirrors `cmd/wcc/` (the C bootstrap frontend). Compiled by `cmd/w6c` against the wwstage cgen.
Style:
- Hare-shaped syntax. Trailing `;`. `=` after fn/type signatures. `export` for visibility.
- Plan 9 names: run-together lowercase (`parsefile`, `lexnext`, `mksym`). No snake_case. No CamelCase.
- ww-side type names are lowercase (`node`, `sym`, `typ`); the C-side equivalents in `cmd/wcc/ww.h` are capitalized (`Node`, `Sym`, `Type`) per Plan 9 cc convention.
This directory is **not** a Hare-style stdlib module — it's a compiler-frontend port, so structural fidelity to `cmd/wcc/` beats Hare API mimicry. The "follow Hare APIs" rule from the root CLAUDE.md applies to peer `lib/*` modules (fmt, io, bufio, strings, ...), not here.
When porting from `cmd/wcc/*.c`:
- Keep the same data layout. The selfhost goal is byte-compatible structures so dump/inspect tools work either way.
- Read `cmd/wcc/ww.h` first for canonical field names and ordering.
- See `selfhost/CLAUDE.md` for the wwstage cgen traps to avoid (two-level field write, `(scalar,str)` tuple return, `def : str`, undersized `amalloc`).

View File

@@ -444,10 +444,18 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
};
let n: u64 = l.lpos - begin;
let p: *u8 = l.src + begin;
let k: i32 = kwlookup(p, n: i32);
out.file = start.file;
out.line = start.line;
out.col = start.col;
// Bare '_' is the discard marker. `_x`, `_1` are normal idents.
if (n == 1u64) {
if (p[0] == 95u8) {
out.kind = TK_UNDER;
out.text = astrndup(l.a, p, n);
return;
};
};
let k: i32 = kwlookup(p, n: i32);
if (k != TK_NONE) {
out.kind = k;
} else {

View File

@@ -48,62 +48,64 @@ def TK_FALSE: i32 = 28;
def TK_AS: i32 = 29;
def TK_STATIC: i32 = 30;
def TK_MATCH: i32 = 31;
def TK_CONST: i32 = 32;
def TK_UNDER: i32 = 33;
def TK_LPAREN: i32 = 32;
def TK_RPAREN: i32 = 33;
def TK_LBRACE: i32 = 34;
def TK_RBRACE: i32 = 35;
def TK_LBRACK: i32 = 36;
def TK_RBRACK: i32 = 37;
def TK_COMMA: i32 = 38;
def TK_SEMI: i32 = 39;
def TK_COLON: i32 = 40;
def TK_DOT: i32 = 41;
def TK_ELLIPSIS: i32 = 42;
def TK_DOTDOT: i32 = 43;
def TK_AT: i32 = 44;
def TK_QUESTION: i32 = 45;
def TK_LPAREN: i32 = 34;
def TK_RPAREN: i32 = 35;
def TK_LBRACE: i32 = 36;
def TK_RBRACE: i32 = 37;
def TK_LBRACK: i32 = 38;
def TK_RBRACK: i32 = 39;
def TK_COMMA: i32 = 40;
def TK_SEMI: i32 = 41;
def TK_COLON: i32 = 42;
def TK_DOT: i32 = 43;
def TK_ELLIPSIS: i32 = 44;
def TK_DOTDOT: i32 = 45;
def TK_AT: i32 = 46;
def TK_QUESTION: i32 = 47;
def TK_ASSIGN: i32 = 46;
def TK_PLUSEQ: i32 = 47;
def TK_MINUSEQ: i32 = 48;
def TK_STAREQ: i32 = 49;
def TK_SLASHEQ: i32 = 50;
def TK_PERCENTEQ: i32 = 51;
def TK_AMPEQ: i32 = 52;
def TK_PIPEEQ: i32 = 53;
def TK_CARETEQ: i32 = 54;
def TK_LSHIFTEQ: i32 = 55;
def TK_RSHIFTEQ: i32 = 56;
def TK_ASSIGN: i32 = 48;
def TK_PLUSEQ: i32 = 49;
def TK_MINUSEQ: i32 = 50;
def TK_STAREQ: i32 = 51;
def TK_SLASHEQ: i32 = 52;
def TK_PERCENTEQ: i32 = 53;
def TK_AMPEQ: i32 = 54;
def TK_PIPEEQ: i32 = 55;
def TK_CARETEQ: i32 = 56;
def TK_LSHIFTEQ: i32 = 57;
def TK_RSHIFTEQ: i32 = 58;
def TK_PLUS: i32 = 57;
def TK_MINUS: i32 = 58;
def TK_STAR: i32 = 59;
def TK_SLASH: i32 = 60;
def TK_PERCENT: i32 = 61;
def TK_AMP: i32 = 62;
def TK_PIPE: i32 = 63;
def TK_CARET: i32 = 64;
def TK_TILDE: i32 = 65;
def TK_LSHIFT: i32 = 66;
def TK_RSHIFT: i32 = 67;
def TK_PLUS: i32 = 59;
def TK_MINUS: i32 = 60;
def TK_STAR: i32 = 61;
def TK_SLASH: i32 = 62;
def TK_PERCENT: i32 = 63;
def TK_AMP: i32 = 64;
def TK_PIPE: i32 = 65;
def TK_CARET: i32 = 66;
def TK_TILDE: i32 = 67;
def TK_LSHIFT: i32 = 68;
def TK_RSHIFT: i32 = 69;
def TK_EQ: i32 = 68;
def TK_NEQ: i32 = 69;
def TK_LT: i32 = 70;
def TK_LE: i32 = 71;
def TK_GT: i32 = 72;
def TK_GE: i32 = 73;
def TK_EQ: i32 = 70;
def TK_NEQ: i32 = 71;
def TK_LT: i32 = 72;
def TK_LE: i32 = 73;
def TK_GT: i32 = 74;
def TK_GE: i32 = 75;
def TK_AND: i32 = 74;
def TK_OR: i32 = 75;
def TK_NOT: i32 = 76;
def TK_AND: i32 = 76;
def TK_OR: i32 = 77;
def TK_NOT: i32 = 78;
def TK_LARROW: i32 = 77;
def TK_ARROW: i32 = 78;
def TK_FATARROW: i32 = 79;
def TK_LARROW: i32 = 79;
def TK_ARROW: i32 = 80;
def TK_FATARROW: i32 = 81;
def TK_LAST: i32 = 80;
def TK_LAST: i32 = 82;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -152,6 +154,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "break", n)) { return TK_BREAK; };
if (streqn(p, "case", n)) { return TK_CASE; };
if (streqn(p, "chan", n)) { return TK_CHAN; };
if (streqn(p, "const", n)) { return TK_CONST; };
if (streqn(p, "continue", n)) { return TK_CONTINUE; };
if (streqn(p, "def", n)) { return TK_DEF; };
if (streqn(p, "defer", n)) { return TK_DEFER; };
@@ -214,6 +217,8 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
if (k == TK_UNDER) { return "_"; };
if (k == TK_LPAREN) { return "("; };
if (k == TK_RPAREN) { return ")"; };

View File

@@ -40,11 +40,15 @@ fn parselet(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// Accept `let` or `const`. Const-bound bindings are marked via
// n.op = TK_CONST so the checker can reject reassignment.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -54,6 +58,7 @@ fn parselet(p: *parser, exported: i32) *node = {
};
expecttok(p, TK_SEMI, "expected ';' after let");
n.exported = exported;
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -89,10 +94,10 @@ fn parseparams(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let n: *node = newnode(p.a, N_PARAM, pf, pl, pc);
// Param form: IDENT ':' type. Anonymous-type-only params (used
// in fn type expressions) aren't yet wired here.
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
// (used in fn type expressions) aren't yet wired here.
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in parameter");
n.lhs = parsetype(p);

View File

@@ -4,6 +4,18 @@ use os;
use mem;
use tok;
// streq_local — str-to-str compare. Inlined here to avoid a cross-
// module `use sym;` for one call site.
fn streq_local(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn parseprimary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
@@ -40,6 +52,43 @@ fn parseprimary(p: *parser) *node = {
advance(p);
return newnode(p.a, N_NIL, pf, pl, pc);
};
if (p.curkind == TK_UNDER) {
// Bare `_` — valid only as a discard lvalue. Emit an N_IDENT
// with empty str; the checker rejects it outside lvalue
// positions.
advance(p);
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
let empty: str;
n.str = empty;
return n;
};
if (p.curkind == TK_LBRACK) {
// Array literal `[a, b, c]` or `[v, w...]` (repeat suffix).
// The repeat marker is an N_FIELD node with str = "..."
// appended to the element list so cgen can detect it.
advance(p);
let n: *node = newnode(p.a, N_ARRLIT, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_RBRACK) {
if (p.curkind == TK_EOF) { break; };
let e: *node = parseexpr(p);
if (head == nil) { head = e; tail = e; }
else { tail.next = e; tail = e; };
if (accepttok(p, TK_ELLIPSIS)) {
let rep: *node = newnode(p.a, N_FIELD,
p.curfile, p.curline, p.curcol);
rep.str = "...";
tail.next = rep;
tail = rep;
break;
};
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACK, "expected ']' after array literal");
n.list = head;
return n;
};
if (p.curkind == TK_LPAREN) {
advance(p);
let e: *node = parseexpr(p);
@@ -79,6 +128,13 @@ fn parseprimary(p: *parser) *node = {
let tail: *node = nil;
for (p.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
// Trailing `...` autofill marker. Stash on s.op so
// cgen can zero-fill the slot before per-field stores.
if (p.curkind == TK_ELLIPSIS) {
advance(p);
s.op = TK_ELLIPSIS;
break;
};
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
@@ -164,9 +220,20 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
advance(p);
let n: *node = newnode(p.a, N_CALL, pf, pl, pc);
n.lhs = cur;
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
// size(T)/align(T): the single arg is a type expression,
// not a regular expression. Special-case at the parser.
let is_typeop: i32 = 0;
if (cur.kind == N_IDENT) {
if (streq_local(cur.str, "size")) { is_typeop = 1; };
if (streq_local(cur.str, "align")) { is_typeop = 1; };
};
if (is_typeop != 0) {
n.list = parsetype(p);
} else {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
};
expecttok(p, TK_RPAREN, "expected ')' after args");
cur = n;
continue;

View File

@@ -85,6 +85,19 @@ fn expectident(p: *parser, into: *str) bool = {
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == TK_UNDER) {
let empty: str;
*into = empty;
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
@@ -112,7 +125,14 @@ fn parsetype(p: *parser) *node = {
return n;
};
let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc);
n.rhs = parseexpr(p);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for N_LET fills it from the
// array literal's element count.
if (p.curkind == TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
@@ -274,6 +294,8 @@ export fn parsefile(p: *parser) *node = {
d = parsetypedecl(p, exported);
} else { if (p.curkind == TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == TK_FN) {
d = parsefn(p, exported, attrs);
} else {
@@ -300,7 +322,7 @@ export fn parsefile(p: *parser) *node = {
advance(p);
};
if (p.curkind == TK_SEMI) { advance(p); };
};};};};};
};};};};};};
if (d != nil) {
if (head == nil) {

View File

@@ -8,10 +8,13 @@ fn parseletlocal(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// `let` or `const`. Const-bound locals are marked via n.op = TK_CONST.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -20,6 +23,7 @@ fn parseletlocal(p: *parser) *node = {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -98,6 +102,11 @@ fn parsefor(p: *parser) *node = {
};
expecttok(p, TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
// by break. Hare's "did the loop find it?" idiom.
if (accepttok(p, TK_ELSE)) {
n.els = parseblock(p);
};
return n;
};
@@ -116,6 +125,7 @@ fn parsestmt(p: *parser) *node = {
return b;
};
if (p.curkind == TK_LET) { return parseletlocal(p); };
if (p.curkind == TK_CONST) { return parseletlocal(p); };
if (p.curkind == TK_IF) {
let n: *node = parseif(p);
expecttok(p, TK_SEMI, "expected ';' after if");

View File

@@ -24,6 +24,7 @@ type sym = struct {
type_: *tinfo,
decl: *node,
exported: i32,
is_const: i32, // const-bound (assignment rejected)
snext: *sym, // iteration order
hashnext: *sym, // hash bucket chain
scope: *scope,

25
selfhost/CLAUDE.md Normal file
View File

@@ -0,0 +1,25 @@
selfhost — ww reimplementation of the toolchain (wcc, w6c, w6a, w6l, ww, wwdump). Compiled by the C bootstrap (`../cmd/`); the goal is to eventually compile itself.
Identifiers: Plan 9 style. lowercase, words run together (`newbuf`, `tcpsock`, `parsefile`). No snake_case.
Syntax and idioms: Hare-shaped. Trailing `;`, `=` after fn/type signatures, `export` for visibility, `match`/`?`/`!` for tagged-union errors. Consult `ref/hare/` for canonical signatures and error-handling patterns before inventing your own.
The C bootstrap's cgen ("wwstage") has four known silent-miscompilation traps. They produce wrong runtime behavior, not compile errors. When porting C → ww here, default to the workarounds:
1. **Two-level field write through pointer field doesn't stick.**
`r.sym.isdyn = 1` where `r.sym: *T` drops the write. Bind the inner pointer to a local first:
```
let sym: *lsym = r.sym;
sym.isdyn = 1;
```
2. **Tuple return `(scalar, str)` corrupts the str half.** Both ptr and len come back garbage. Split into two functions — one returns the scalar, another returns the str. Plain `str` returns are fine.
3. **`def NAME: str = "...";` is broken.** The `.len` picks up an unrelated accumulator. Wrap the literal in a nullary fn instead:
```
fn namestr() str = { return "..."; };
```
4. **`amalloc(n)` with n < struct size silently corrupts neighbours.** No error — the bump arena hands out n bytes and field writes overflow into the next record. When introducing or growing a struct, audit every `amalloc(_, n)` call site and over-size (we routinely pass 48 for a 40-byte struct). Symptom: linked-list prepends lose all but the most recent entry.
If a port "should work" but the binary is wrong, suspect these first.

View File

@@ -488,62 +488,64 @@ def TK_FALSE: i32 = 28;
def TK_AS: i32 = 29;
def TK_STATIC: i32 = 30;
def TK_MATCH: i32 = 31;
def TK_CONST: i32 = 32;
def TK_UNDER: i32 = 33;
def TK_LPAREN: i32 = 32;
def TK_RPAREN: i32 = 33;
def TK_LBRACE: i32 = 34;
def TK_RBRACE: i32 = 35;
def TK_LBRACK: i32 = 36;
def TK_RBRACK: i32 = 37;
def TK_COMMA: i32 = 38;
def TK_SEMI: i32 = 39;
def TK_COLON: i32 = 40;
def TK_DOT: i32 = 41;
def TK_ELLIPSIS: i32 = 42;
def TK_DOTDOT: i32 = 43;
def TK_AT: i32 = 44;
def TK_QUESTION: i32 = 45;
def TK_LPAREN: i32 = 34;
def TK_RPAREN: i32 = 35;
def TK_LBRACE: i32 = 36;
def TK_RBRACE: i32 = 37;
def TK_LBRACK: i32 = 38;
def TK_RBRACK: i32 = 39;
def TK_COMMA: i32 = 40;
def TK_SEMI: i32 = 41;
def TK_COLON: i32 = 42;
def TK_DOT: i32 = 43;
def TK_ELLIPSIS: i32 = 44;
def TK_DOTDOT: i32 = 45;
def TK_AT: i32 = 46;
def TK_QUESTION: i32 = 47;
def TK_ASSIGN: i32 = 46;
def TK_PLUSEQ: i32 = 47;
def TK_MINUSEQ: i32 = 48;
def TK_STAREQ: i32 = 49;
def TK_SLASHEQ: i32 = 50;
def TK_PERCENTEQ: i32 = 51;
def TK_AMPEQ: i32 = 52;
def TK_PIPEEQ: i32 = 53;
def TK_CARETEQ: i32 = 54;
def TK_LSHIFTEQ: i32 = 55;
def TK_RSHIFTEQ: i32 = 56;
def TK_ASSIGN: i32 = 48;
def TK_PLUSEQ: i32 = 49;
def TK_MINUSEQ: i32 = 50;
def TK_STAREQ: i32 = 51;
def TK_SLASHEQ: i32 = 52;
def TK_PERCENTEQ: i32 = 53;
def TK_AMPEQ: i32 = 54;
def TK_PIPEEQ: i32 = 55;
def TK_CARETEQ: i32 = 56;
def TK_LSHIFTEQ: i32 = 57;
def TK_RSHIFTEQ: i32 = 58;
def TK_PLUS: i32 = 57;
def TK_MINUS: i32 = 58;
def TK_STAR: i32 = 59;
def TK_SLASH: i32 = 60;
def TK_PERCENT: i32 = 61;
def TK_AMP: i32 = 62;
def TK_PIPE: i32 = 63;
def TK_CARET: i32 = 64;
def TK_TILDE: i32 = 65;
def TK_LSHIFT: i32 = 66;
def TK_RSHIFT: i32 = 67;
def TK_PLUS: i32 = 59;
def TK_MINUS: i32 = 60;
def TK_STAR: i32 = 61;
def TK_SLASH: i32 = 62;
def TK_PERCENT: i32 = 63;
def TK_AMP: i32 = 64;
def TK_PIPE: i32 = 65;
def TK_CARET: i32 = 66;
def TK_TILDE: i32 = 67;
def TK_LSHIFT: i32 = 68;
def TK_RSHIFT: i32 = 69;
def TK_EQ: i32 = 68;
def TK_NEQ: i32 = 69;
def TK_LT: i32 = 70;
def TK_LE: i32 = 71;
def TK_GT: i32 = 72;
def TK_GE: i32 = 73;
def TK_EQ: i32 = 70;
def TK_NEQ: i32 = 71;
def TK_LT: i32 = 72;
def TK_LE: i32 = 73;
def TK_GT: i32 = 74;
def TK_GE: i32 = 75;
def TK_AND: i32 = 74;
def TK_OR: i32 = 75;
def TK_NOT: i32 = 76;
def TK_AND: i32 = 76;
def TK_OR: i32 = 77;
def TK_NOT: i32 = 78;
def TK_LARROW: i32 = 77;
def TK_ARROW: i32 = 78;
def TK_FATARROW: i32 = 79;
def TK_LARROW: i32 = 79;
def TK_ARROW: i32 = 80;
def TK_FATARROW: i32 = 81;
def TK_LAST: i32 = 80;
def TK_LAST: i32 = 82;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -592,6 +594,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "break", n)) { return TK_BREAK; };
if (streqn(p, "case", n)) { return TK_CASE; };
if (streqn(p, "chan", n)) { return TK_CHAN; };
if (streqn(p, "const", n)) { return TK_CONST; };
if (streqn(p, "continue", n)) { return TK_CONTINUE; };
if (streqn(p, "def", n)) { return TK_DEF; };
if (streqn(p, "defer", n)) { return TK_DEFER; };
@@ -654,6 +657,8 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
if (k == TK_UNDER) { return "_"; };
if (k == TK_LPAREN) { return "("; };
if (k == TK_RPAREN) { return ")"; };
@@ -1374,10 +1379,18 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
};
let n: u64 = l.lpos - begin;
let p: *u8 = l.src + begin;
let k: i32 = kwlookup(p, n: i32);
out.file = start.file;
out.line = start.line;
out.col = start.col;
// Bare '_' is the discard marker. `_x`, `_1` are normal idents.
if (n == 1u64) {
if (p[0] == 95u8) {
out.kind = TK_UNDER;
out.text = astrndup(l.a, p, n);
return;
};
};
let k: i32 = kwlookup(p, n: i32);
if (k != TK_NONE) {
out.kind = k;
} else {
@@ -1961,6 +1974,18 @@ use os;
use mem;
use tok;
// streq_local — str-to-str compare. Inlined here to avoid a cross-
// module `use sym;` for one call site.
fn streq_local(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn parseprimary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
@@ -1997,6 +2022,43 @@ fn parseprimary(p: *parser) *node = {
advance(p);
return newnode(p.a, N_NIL, pf, pl, pc);
};
if (p.curkind == TK_UNDER) {
// Bare `_` — valid only as a discard lvalue. Emit an N_IDENT
// with empty str; the checker rejects it outside lvalue
// positions.
advance(p);
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
let empty: str;
n.str = empty;
return n;
};
if (p.curkind == TK_LBRACK) {
// Array literal `[a, b, c]` or `[v, w...]` (repeat suffix).
// The repeat marker is an N_FIELD node with str = "..."
// appended to the element list so cgen can detect it.
advance(p);
let n: *node = newnode(p.a, N_ARRLIT, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_RBRACK) {
if (p.curkind == TK_EOF) { break; };
let e: *node = parseexpr(p);
if (head == nil) { head = e; tail = e; }
else { tail.next = e; tail = e; };
if (accepttok(p, TK_ELLIPSIS)) {
let rep: *node = newnode(p.a, N_FIELD,
p.curfile, p.curline, p.curcol);
rep.str = "...";
tail.next = rep;
tail = rep;
break;
};
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACK, "expected ']' after array literal");
n.list = head;
return n;
};
if (p.curkind == TK_LPAREN) {
advance(p);
let e: *node = parseexpr(p);
@@ -2036,6 +2098,13 @@ fn parseprimary(p: *parser) *node = {
let tail: *node = nil;
for (p.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
// Trailing `...` autofill marker. Stash on s.op so
// cgen can zero-fill the slot before per-field stores.
if (p.curkind == TK_ELLIPSIS) {
advance(p);
s.op = TK_ELLIPSIS;
break;
};
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
@@ -2121,9 +2190,20 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
advance(p);
let n: *node = newnode(p.a, N_CALL, pf, pl, pc);
n.lhs = cur;
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
// size(T)/align(T): the single arg is a type expression,
// not a regular expression. Special-case at the parser.
let is_typeop: i32 = 0;
if (cur.kind == N_IDENT) {
if (streq_local(cur.str, "size")) { is_typeop = 1; };
if (streq_local(cur.str, "align")) { is_typeop = 1; };
};
if (is_typeop != 0) {
n.list = parsetype(p);
} else {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
};
expecttok(p, TK_RPAREN, "expected ')' after args");
cur = n;
continue;
@@ -2290,10 +2370,13 @@ fn parseletlocal(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// `let` or `const`. Const-bound locals are marked via n.op = TK_CONST.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2302,6 +2385,7 @@ fn parseletlocal(p: *parser) *node = {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2380,6 +2464,11 @@ fn parsefor(p: *parser) *node = {
};
expecttok(p, TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
// by break. Hare's "did the loop find it?" idiom.
if (accepttok(p, TK_ELSE)) {
n.els = parseblock(p);
};
return n;
};
@@ -2398,6 +2487,7 @@ fn parsestmt(p: *parser) *node = {
return b;
};
if (p.curkind == TK_LET) { return parseletlocal(p); };
if (p.curkind == TK_CONST) { return parseletlocal(p); };
if (p.curkind == TK_IF) {
let n: *node = parseif(p);
expecttok(p, TK_SEMI, "expected ';' after if");
@@ -2522,11 +2612,15 @@ fn parselet(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// Accept `let` or `const`. Const-bound bindings are marked via
// n.op = TK_CONST so the checker can reject reassignment.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2536,6 +2630,7 @@ fn parselet(p: *parser, exported: i32) *node = {
};
expecttok(p, TK_SEMI, "expected ';' after let");
n.exported = exported;
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2571,10 +2666,10 @@ fn parseparams(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let n: *node = newnode(p.a, N_PARAM, pf, pl, pc);
// Param form: IDENT ':' type. Anonymous-type-only params (used
// in fn type expressions) aren't yet wired here.
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
// (used in fn type expressions) aren't yet wired here.
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in parameter");
n.lhs = parsetype(p);
@@ -2722,6 +2817,19 @@ fn expectident(p: *parser, into: *str) bool = {
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == TK_UNDER) {
let empty: str;
*into = empty;
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
@@ -2749,7 +2857,14 @@ fn parsetype(p: *parser) *node = {
return n;
};
let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc);
n.rhs = parseexpr(p);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for N_LET fills it from the
// array literal's element count.
if (p.curkind == TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
@@ -2911,6 +3026,8 @@ export fn parsefile(p: *parser) *node = {
d = parsetypedecl(p, exported);
} else { if (p.curkind == TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == TK_FN) {
d = parsefn(p, exported, attrs);
} else {
@@ -2937,7 +3054,7 @@ export fn parsefile(p: *parser) *node = {
advance(p);
};
if (p.curkind == TK_SEMI) { advance(p); };
};};};};};
};};};};};};
if (d != nil) {
if (head == nil) {
@@ -3311,6 +3428,7 @@ type sym = struct {
type_: *tinfo,
decl: *node,
exported: i32,
is_const: i32, // const-bound (assignment rejected)
snext: *sym, // iteration order
hashnext: *sym, // hash bucket chain
scope: *scope,
@@ -4270,6 +4388,52 @@ fn primsize(name: str) i32 = {
return 0;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;
@@ -5253,6 +5417,19 @@ fn cgcall(c: *cgen, n: *node) void = {
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
@@ -5746,7 +5923,7 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
@@ -5807,10 +5984,84 @@ fn cglet(c: *cgen, n: *node) void = {
c.lastwasreturn = 0;
return;
};
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
// Walk elements in declaration order, store each at off + i*esz
// using the right width for the element type. Trailing `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry.
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
@@ -5821,6 +6072,29 @@ fn cglet(c: *cgen, n: *node) void = {
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
@@ -5912,6 +6186,11 @@ fn cgfor(c: *cgen, n: *node) void = {
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
@@ -5919,7 +6198,7 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(endl); emitline("\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
@@ -5932,6 +6211,10 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
@@ -6029,7 +6312,7 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// of the caller. Same-name re-declarations share the first
// slot (see scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;

View File

@@ -33,7 +33,7 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// of the caller. Same-name re-declarations share the first
// slot (see scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;

View File

@@ -736,6 +736,19 @@ fn cgcall(c: *cgen, n: *node) void = {
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.

View File

@@ -126,7 +126,7 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
@@ -187,10 +187,84 @@ fn cglet(c: *cgen, n: *node) void = {
c.lastwasreturn = 0;
return;
};
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
// Walk elements in declaration order, store each at off + i*esz
// using the right width for the element type. Trailing `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry.
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
@@ -201,6 +275,29 @@ fn cglet(c: *cgen, n: *node) void = {
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
@@ -292,6 +389,11 @@ fn cgfor(c: *cgen, n: *node) void = {
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
@@ -299,7 +401,7 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(endl); emitline("\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
@@ -312,6 +414,10 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;

View File

@@ -619,6 +619,52 @@ fn primsize(name: str) i32 = {
return 0;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;

View File

@@ -488,62 +488,64 @@ def TK_FALSE: i32 = 28;
def TK_AS: i32 = 29;
def TK_STATIC: i32 = 30;
def TK_MATCH: i32 = 31;
def TK_CONST: i32 = 32;
def TK_UNDER: i32 = 33;
def TK_LPAREN: i32 = 32;
def TK_RPAREN: i32 = 33;
def TK_LBRACE: i32 = 34;
def TK_RBRACE: i32 = 35;
def TK_LBRACK: i32 = 36;
def TK_RBRACK: i32 = 37;
def TK_COMMA: i32 = 38;
def TK_SEMI: i32 = 39;
def TK_COLON: i32 = 40;
def TK_DOT: i32 = 41;
def TK_ELLIPSIS: i32 = 42;
def TK_DOTDOT: i32 = 43;
def TK_AT: i32 = 44;
def TK_QUESTION: i32 = 45;
def TK_LPAREN: i32 = 34;
def TK_RPAREN: i32 = 35;
def TK_LBRACE: i32 = 36;
def TK_RBRACE: i32 = 37;
def TK_LBRACK: i32 = 38;
def TK_RBRACK: i32 = 39;
def TK_COMMA: i32 = 40;
def TK_SEMI: i32 = 41;
def TK_COLON: i32 = 42;
def TK_DOT: i32 = 43;
def TK_ELLIPSIS: i32 = 44;
def TK_DOTDOT: i32 = 45;
def TK_AT: i32 = 46;
def TK_QUESTION: i32 = 47;
def TK_ASSIGN: i32 = 46;
def TK_PLUSEQ: i32 = 47;
def TK_MINUSEQ: i32 = 48;
def TK_STAREQ: i32 = 49;
def TK_SLASHEQ: i32 = 50;
def TK_PERCENTEQ: i32 = 51;
def TK_AMPEQ: i32 = 52;
def TK_PIPEEQ: i32 = 53;
def TK_CARETEQ: i32 = 54;
def TK_LSHIFTEQ: i32 = 55;
def TK_RSHIFTEQ: i32 = 56;
def TK_ASSIGN: i32 = 48;
def TK_PLUSEQ: i32 = 49;
def TK_MINUSEQ: i32 = 50;
def TK_STAREQ: i32 = 51;
def TK_SLASHEQ: i32 = 52;
def TK_PERCENTEQ: i32 = 53;
def TK_AMPEQ: i32 = 54;
def TK_PIPEEQ: i32 = 55;
def TK_CARETEQ: i32 = 56;
def TK_LSHIFTEQ: i32 = 57;
def TK_RSHIFTEQ: i32 = 58;
def TK_PLUS: i32 = 57;
def TK_MINUS: i32 = 58;
def TK_STAR: i32 = 59;
def TK_SLASH: i32 = 60;
def TK_PERCENT: i32 = 61;
def TK_AMP: i32 = 62;
def TK_PIPE: i32 = 63;
def TK_CARET: i32 = 64;
def TK_TILDE: i32 = 65;
def TK_LSHIFT: i32 = 66;
def TK_RSHIFT: i32 = 67;
def TK_PLUS: i32 = 59;
def TK_MINUS: i32 = 60;
def TK_STAR: i32 = 61;
def TK_SLASH: i32 = 62;
def TK_PERCENT: i32 = 63;
def TK_AMP: i32 = 64;
def TK_PIPE: i32 = 65;
def TK_CARET: i32 = 66;
def TK_TILDE: i32 = 67;
def TK_LSHIFT: i32 = 68;
def TK_RSHIFT: i32 = 69;
def TK_EQ: i32 = 68;
def TK_NEQ: i32 = 69;
def TK_LT: i32 = 70;
def TK_LE: i32 = 71;
def TK_GT: i32 = 72;
def TK_GE: i32 = 73;
def TK_EQ: i32 = 70;
def TK_NEQ: i32 = 71;
def TK_LT: i32 = 72;
def TK_LE: i32 = 73;
def TK_GT: i32 = 74;
def TK_GE: i32 = 75;
def TK_AND: i32 = 74;
def TK_OR: i32 = 75;
def TK_NOT: i32 = 76;
def TK_AND: i32 = 76;
def TK_OR: i32 = 77;
def TK_NOT: i32 = 78;
def TK_LARROW: i32 = 77;
def TK_ARROW: i32 = 78;
def TK_FATARROW: i32 = 79;
def TK_LARROW: i32 = 79;
def TK_ARROW: i32 = 80;
def TK_FATARROW: i32 = 81;
def TK_LAST: i32 = 80;
def TK_LAST: i32 = 82;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -592,6 +594,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "break", n)) { return TK_BREAK; };
if (streqn(p, "case", n)) { return TK_CASE; };
if (streqn(p, "chan", n)) { return TK_CHAN; };
if (streqn(p, "const", n)) { return TK_CONST; };
if (streqn(p, "continue", n)) { return TK_CONTINUE; };
if (streqn(p, "def", n)) { return TK_DEF; };
if (streqn(p, "defer", n)) { return TK_DEFER; };
@@ -654,6 +657,8 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
if (k == TK_UNDER) { return "_"; };
if (k == TK_LPAREN) { return "("; };
if (k == TK_RPAREN) { return ")"; };
@@ -1374,10 +1379,18 @@ fn lexident(l: *lex, start: *pos, out: *tok) void = {
};
let n: u64 = l.lpos - begin;
let p: *u8 = l.src + begin;
let k: i32 = kwlookup(p, n: i32);
out.file = start.file;
out.line = start.line;
out.col = start.col;
// Bare '_' is the discard marker. `_x`, `_1` are normal idents.
if (n == 1u64) {
if (p[0] == 95u8) {
out.kind = TK_UNDER;
out.text = astrndup(l.a, p, n);
return;
};
};
let k: i32 = kwlookup(p, n: i32);
if (k != TK_NONE) {
out.kind = k;
} else {
@@ -1961,6 +1974,18 @@ use os;
use mem;
use tok;
// streq_local — str-to-str compare. Inlined here to avoid a cross-
// module `use sym;` for one call site.
fn streq_local(a: str, b: str) bool = {
if (a.len != b.len) { return false; };
let i: i32 = 0;
for (i < a.len) {
if (a[i] != b[i]) { return false; };
i += 1;
};
return true;
};
fn parseprimary(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
@@ -1997,6 +2022,43 @@ fn parseprimary(p: *parser) *node = {
advance(p);
return newnode(p.a, N_NIL, pf, pl, pc);
};
if (p.curkind == TK_UNDER) {
// Bare `_` — valid only as a discard lvalue. Emit an N_IDENT
// with empty str; the checker rejects it outside lvalue
// positions.
advance(p);
let n: *node = newnode(p.a, N_IDENT, pf, pl, pc);
let empty: str;
n.str = empty;
return n;
};
if (p.curkind == TK_LBRACK) {
// Array literal `[a, b, c]` or `[v, w...]` (repeat suffix).
// The repeat marker is an N_FIELD node with str = "..."
// appended to the element list so cgen can detect it.
advance(p);
let n: *node = newnode(p.a, N_ARRLIT, pf, pl, pc);
let head: *node = nil;
let tail: *node = nil;
for (p.curkind != TK_RBRACK) {
if (p.curkind == TK_EOF) { break; };
let e: *node = parseexpr(p);
if (head == nil) { head = e; tail = e; }
else { tail.next = e; tail = e; };
if (accepttok(p, TK_ELLIPSIS)) {
let rep: *node = newnode(p.a, N_FIELD,
p.curfile, p.curline, p.curcol);
rep.str = "...";
tail.next = rep;
tail = rep;
break;
};
if (!accepttok(p, TK_COMMA)) { break; };
};
expecttok(p, TK_RBRACK, "expected ']' after array literal");
n.list = head;
return n;
};
if (p.curkind == TK_LPAREN) {
advance(p);
let e: *node = parseexpr(p);
@@ -2036,6 +2098,13 @@ fn parseprimary(p: *parser) *node = {
let tail: *node = nil;
for (p.curkind != TK_RBRACE) {
if (p.curkind == TK_EOF) { break; };
// Trailing `...` autofill marker. Stash on s.op so
// cgen can zero-fill the slot before per-field stores.
if (p.curkind == TK_ELLIPSIS) {
advance(p);
s.op = TK_ELLIPSIS;
break;
};
let fpf: str = p.curfile;
let fpl: i32 = p.curline;
let fpc: i32 = p.curcol;
@@ -2121,9 +2190,20 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
advance(p);
let n: *node = newnode(p.a, N_CALL, pf, pl, pc);
n.lhs = cur;
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
// size(T)/align(T): the single arg is a type expression,
// not a regular expression. Special-case at the parser.
let is_typeop: i32 = 0;
if (cur.kind == N_IDENT) {
if (streq_local(cur.str, "size")) { is_typeop = 1; };
if (streq_local(cur.str, "align")) { is_typeop = 1; };
};
if (is_typeop != 0) {
n.list = parsetype(p);
} else {
let arghead: *node = nil;
parsearglist(p, TK_RPAREN, &arghead);
n.list = arghead;
};
expecttok(p, TK_RPAREN, "expected ')' after args");
cur = n;
continue;
@@ -2290,10 +2370,13 @@ fn parseletlocal(p: *parser) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// `let` or `const`. Const-bound locals are marked via n.op = TK_CONST.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2302,6 +2385,7 @@ fn parseletlocal(p: *parser) *node = {
n.rhs = parseexpr(p);
};
expecttok(p, TK_SEMI, "expected ';' after let");
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2380,6 +2464,11 @@ fn parsefor(p: *parser) *node = {
};
expecttok(p, TK_RPAREN, "expected ')' after for");
n.body = parseblock(p);
// Optional `else { ... }` — runs at normal cond-false exit; skipped
// by break. Hare's "did the loop find it?" idiom.
if (accepttok(p, TK_ELSE)) {
n.els = parseblock(p);
};
return n;
};
@@ -2398,6 +2487,7 @@ fn parsestmt(p: *parser) *node = {
return b;
};
if (p.curkind == TK_LET) { return parseletlocal(p); };
if (p.curkind == TK_CONST) { return parseletlocal(p); };
if (p.curkind == TK_IF) {
let n: *node = parseif(p);
expecttok(p, TK_SEMI, "expected ';' after if");
@@ -2522,11 +2612,15 @@ fn parselet(p: *parser, exported: i32) *node = {
let pf: str = p.curfile;
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
advance(p); // past `let`
// Accept `let` or `const`. Const-bound bindings are marked via
// n.op = TK_CONST so the checker can reject reassignment.
let is_const: i32 = 0;
if (p.curkind == TK_CONST) { is_const = 1; };
advance(p);
let n: *node = newnode(p.a, N_LET, pf, pl, pc);
n.module = p.l.module;
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
if (accepttok(p, TK_COLON)) {
n.lhs = parsetype(p);
@@ -2536,6 +2630,7 @@ fn parselet(p: *parser, exported: i32) *node = {
};
expecttok(p, TK_SEMI, "expected ';' after let");
n.exported = exported;
if (is_const != 0) { n.op = TK_CONST; };
return n;
};
@@ -2571,10 +2666,10 @@ fn parseparams(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
let n: *node = newnode(p.a, N_PARAM, pf, pl, pc);
// Param form: IDENT ':' type. Anonymous-type-only params (used
// in fn type expressions) aren't yet wired here.
// Param form: (IDENT|'_') ':' type. Anonymous-type-only params
// (used in fn type expressions) aren't yet wired here.
let id: str;
expectident(p, &id);
expectbindname(p, &id);
n.str = id;
expecttok(p, TK_COLON, "expected ':' in parameter");
n.lhs = parsetype(p);
@@ -2722,6 +2817,19 @@ fn expectident(p: *parser, into: *str) bool = {
return true;
};
// expectbindname — like expectident but also accepts a bare `_`
// discard marker. On `_`, returns "" so the checker skips
// scope_define for the binding.
fn expectbindname(p: *parser, into: *str) bool = {
if (p.curkind == TK_UNDER) {
let empty: str;
*into = empty;
advance(p);
return true;
};
return expectident(p, into);
};
// ---- type expressions ------------------------------------------------
//
// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`).
@@ -2749,7 +2857,14 @@ fn parsetype(p: *parser) *node = {
return n;
};
let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc);
n.rhs = parseexpr(p);
// `[_]T` — length inferred from initialiser. n.rhs stays nil
// as the sentinel; the cgen path for N_LET fills it from the
// array literal's element count.
if (p.curkind == TK_UNDER) {
advance(p);
} else {
n.rhs = parseexpr(p);
};
expecttok(p, TK_RBRACK, "expected ']' in array type");
n.lhs = parsetype(p);
return n;
@@ -2911,6 +3026,8 @@ export fn parsefile(p: *parser) *node = {
d = parsetypedecl(p, exported);
} else { if (p.curkind == TK_LET) {
d = parselet(p, exported);
} else { if (p.curkind == TK_CONST) {
d = parselet(p, exported);
} else { if (p.curkind == TK_FN) {
d = parsefn(p, exported, attrs);
} else {
@@ -2937,7 +3054,7 @@ export fn parsefile(p: *parser) *node = {
advance(p);
};
if (p.curkind == TK_SEMI) { advance(p); };
};};};};};
};};};};};};
if (d != nil) {
if (head == nil) {
@@ -3311,6 +3428,7 @@ type sym = struct {
type_: *tinfo,
decl: *node,
exported: i32,
is_const: i32, // const-bound (assignment rejected)
snext: *sym, // iteration order
hashnext: *sym, // hash bucket chain
scope: *scope,
@@ -4270,6 +4388,52 @@ fn primsize(name: str) i32 = {
return 0;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
return slotsize(c, n.lhs);
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: i32 = typn.kind;
@@ -5253,6 +5417,19 @@ fn cgcall(c: *cgen, n: *node) void = {
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an N_IDENT with empty str
// (planted by parseprimary on the TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
@@ -5746,7 +5923,7 @@ fn cgexprstmt(c: *cgen, n: *node) void = {
fn cglet(c: *cgen, n: *node) void = {
let nm: str = n.str;
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
let off: i32 = localadd(c, nm, sz, n.lhs);
if (n.rhs != nil) {
let rhs: *node = n.rhs;
@@ -5807,10 +5984,84 @@ fn cglet(c: *cgen, n: *node) void = {
c.lastwasreturn = 0;
return;
};
// Array literal init: `let xs: [N]T = [a, b, c];` (or [_]T).
// Walk elements in declaration order, store each at off + i*esz
// using the right width for the element type. Trailing `...`
// after the last value (an N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
if (rhs.kind == N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = "MOVQ";
if (esz == 1) { mop = "MOVB"; }
else { if (esz == 4) { mop = "MOVL"; }; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *node = rhs.list;
for (e != nil) {
if (e.kind == N_FIELD) {
if (streq(e.str, "...")) {
repeat = true;
e = nil;
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
} else {
cgexpr(c, e);
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
e = e.next;
};
};
// AX still holds the last stored value; fill remaining
// slots up to the declared length with it.
if (repeat) {
let total: i32 = idx;
if (n.lhs != nil) {
if (n.lhs.kind == N_TARRAY) {
if (n.lhs.rhs != nil) {
if (n.lhs.rhs.kind == N_INTLIT) {
total = n.lhs.rhs.uval: i32;
};
};
};
};
for (idx < total) {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
idx += 1;
};
};
c.lastwasreturn = 0;
return;
};
// Struct literal init: `let p: point = point{x=..., y=...};`.
// For each field in the lit, evaluate its value and store at
// the field's offset within the slot. Field-name → offset
// from the struct registry.
// from the struct registry. When the literal carries
// op == TK_ELLIPSIS (autofill marker from the parser), the
// entire slot is zero-filled first so unmentioned fields
// read as 0.
if (rhs.kind == N_STRUCTLIT) {
let trefn: *node = rhs.lhs;
let sname: str;
@@ -5821,6 +6072,29 @@ fn cglet(c: *cgen, n: *node) void = {
};
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
if (rhs.op == TK_ELLIPSIS) {
let total: i32 = si.totsize;
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= total) {
emitline("\tMOVQ\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= total) {
emitline("\tMOVL\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < total) {
emitline("\tMOVB\tAX, ");
emitoff((off + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fieldnode: *node = rhs.list;
for (fieldnode != nil) {
if (fieldnode.kind == N_FIELD) {
@@ -5912,6 +6186,11 @@ fn cgfor(c: *cgen, n: *node) void = {
// label when there's no post-expression.
let topl: str = mklabel(c, "loop");
let endl: str = mklabel(c, "endloop");
// `else` runs at natural cond-false exit; break skips it. When
// present, branch the cond-fail edge to a separate natural_exit
// label so the else body sits between it and the break target.
let naturall: str = endl;
if (n.els != nil) { naturall = mklabel(c, "elseloop"); };
if (n.lhs != nil) { cgstmt(c, n.lhs); };
@@ -5919,7 +6198,7 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.cond != nil) {
cgexpr(c, n.cond);
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(endl); emitline("\n");
emitline("\tJE\t"); emitline(naturall); emitline("\n");
};
c.loopendbuf[c.looptop] = endl;
@@ -5932,6 +6211,10 @@ fn cgfor(c: *cgen, n: *node) void = {
if (n.rhs != nil) { cgexpr(c, n.rhs); };
emitline("\tJMP\t"); emitline(topl); emitline("\n");
if (n.els != nil) {
emitlabel(naturall);
cgstmt(c, n.els);
};
emitlabel(endl);
c.lastwasreturn = 0;
return;
@@ -6029,7 +6312,7 @@ fn scanlocals(c: *cgen, n: *node) i32 = {
// of the caller. Same-name re-declarations share the first
// slot (see scanseenmark / localadd).
if (!scanseenmark(c, n.str)) {
let sz: i32 = slotsize(c, n.lhs);
let sz: i32 = letslotsize(c, n);
if (sz < 8) { sz = 8; };
if ((sz & 7) != 0) { sz = (sz + 7) & ~7; };
total += sz;