diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 36ffc43e..3db0b6b1 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index fc52d337..48827457 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,409 +1,6 @@ -# ww - -A small systems language. Plan 9 in spirit and code style, Hare in -syntax, API, and FFI, no GC, CSP at the end. - -This file is the contract for the work. Read PLAN.md for the schedule. - -## Identity - -- Name: `ww` -- Source extension: `.ww` -- Module: a directory of `*.ww` files (Hare-style layout) -- Toolchain: Plan 9-organized. One library + one binary per role - per target architecture. Plan 9 uses a single digit per arch - (8=386, 6=amd64, 5=arm, 7=arm64, 9=power); we adopt that. - - `cmd/wcc/` — frontend library (lex, parse, check). Builds - `libwcc.a`. Not a binary. - - `cmd/w6c/` — amd64 compiler. Reads `.ww`, writes `.s` - (Plan 9 amd64 asm). - - `cmd/w6a/` — amd64 assembler. Reads `.s`, writes `.o` - (ELF, for C interop). - - `cmd/w6l/` — amd64 linker. Reads `.o` and `.a`, writes a - static ELF binary. - - `cmd/ww/` — user-facing driver (Hare's `hare(1)` / - Plan 9's `cc(1)` analogue). Orchestrates `w6c → w6a → w6l`. - Adding a new target later means a new triple (e.g. `w7c`/`w7a`/`w7l` - for arm64). The frontend library `wwc` is shared. - -## Hard rules (do not violate) - -1. **No garbage collector.** Ever. Memory is allocated and freed by the - programmer. The compiler may insert defer-style cleanup, never a - tracing/reference-counting collector. -2. **No `map`.** A built-in growable hash table is unsafe without GC - (rehashing invalidates pointers). Users may build their own; it is - not a language type. -3. **No complex runtime.** The runtime is a few hundred lines. It owns: - process startup, syscalls trampoline, panic/abort, and (later) the - CSP scheduler. It does not own memory beyond a tiny bump arena for - startup. -4. **Static linking by default.** A `ww` binary is self-contained, - like Go. Dynamic linking is opt-in (`-shared`, `-l`). -5. **C FFI is first-class, Hare-style.** A body-less `fn` declaration - imports the symbol; `@symbol("name")` overrides the linker name. - We must be able to bind libcrypto/libtls/ncurses cleanly and link - them statically into the final image. The platform calling - convention (SysV amd64 on Linux) is the C ABI, so no separate - `extern "c"` marker is needed. -6. **Plan 9 toolchain. No LLVM, no QBE, no external IR.** We do not - invent a portable SSA IR. We follow Plan 9: the per-target - compiler (`w6c` for amd64) reads `.ww` and writes Plan 9-style - target assembly (`.s`); the per-target assembler (`w6a`) writes - ELF objects; the per-target linker (`w6l`) produces a static - binary. Each tool uses Plan 9 cc's in-memory `Prog`/`Adr` - shapes — read `ref/plan9front/sys/src/cmd/cc/`, `cmd/w6c/`, - `cmd/w6a/`, `cmd/w6l/` before writing your own. -7. **No generics, no interfaces, no closures, no lambdas.** Four forms - of bloat we refuse. Polymorphism, when genuinely needed, is a - struct of function pointers plus a `ctx: *void` (Plan 9 `Bio`, - Hare `io::stream`). All functions are declared at file scope; - function values are pointers to those named functions. No - capturing. No anonymous function literals. The compiler does no - virtual dispatch; users build vtables explicitly when they want - them. If a function needs to work on multiple types, write it - multiple times, or operate on `[]u8` and let the caller cast. - - **Tagged unions are allowed**, but only as the Hare-style error - idiom: `(T | error)` (and a few sentinel kin like `nomem`). - Pattern-matched with `match`. Propagated with postfix `?`. Asserted - with postfix `!`. They are not an open extension point — no enum - methods, no virtual dispatch through the tag, no nesting beyond - what the error idiom needs. If you find yourself reaching for a - discriminated record, use a struct with a tag field instead. -8. **Tests run after every change.** `make test` is the truth. A - change without a green `make test` is not a change. -9. **Prototype in C, then self-host.** The C bootstrap toolchain - (`libwcc`, `w6c`, `w6a`, `w6l`, `ww`) is throwaway scaffolding. - Its job is to compile enough of `ww` to compile the ww - reimplementations of itself. Do not over-engineer the C side. - -## Type system - -Hare-style integer names. Fixed width, explicit signedness: - -``` -i8 i16 i32 i64 signed -u8 u16 u32 u64 unsigned -uint int register width (target-defined) -uintptr pointer-width unsigned -f32 f64 IEEE 754 -bool one byte -rune i32, a Unicode code point -str immutable utf-8 view: { *u8, len } -void zero-sized -``` - -Composite: - -``` -*T pointer (may be nil) -[N]T fixed array -[]T slice: { *T, len, cap } -struct { x: i32, y: i32 } aggregate (Hare shape) -fn(arg: T) ret function pointer (file-scope only) -chan T CSP channel (last phase) -``` - -No `map`. No `interface`. No `union`. No exceptions. No generics. -No closures. - -Polymorphism, when truly needed, is a struct of function pointers -plus a `ctx: *void`. See `lib/io/stream.ww` for the canonical shape. -This is how Plan 9 `Bio` and Hare `io::stream` work. It is plain -data, easy to read, and the compiler does nothing magic for it. - -## Syntax - -Hare-shaped. Trailing semicolons. `=` after function and type -signatures. The one departure from Hare: module paths use `.` -instead of `::`. Both module navigation and field access use the -same dot — the compiler resolves by name lookup. - -``` -use io; -use fmt; -use os; - -def MAX_LINE: i32 = 4096; - -type point = struct { - x: i32, - y: i32, -}; - -export fn move(p: *point, dx: i32, dy: i32) void = { - p.x += dx; - p.y += dy; -}; - -export fn distance(a: point, b: point) f64 = { - let dx: f64 = (a.x - b.x): f64; - let dy: f64 = (a.y - b.y): f64; - return math.sqrt(dx*dx + dy*dy); -}; - -export fn main() void = { - let p: point = point { x = 0, y = 0 }; - move(&p, 3, 4); - for (let i: i32 = 0; i < MAX_LINE; i += 1) { - fmt.println(i); - }; -}; -``` - -Lexical rules: - -- Statements end in `;`. No automatic insertion. -- Function bodies follow `=`: `fn f() T = { ... };`. -- Type definitions follow `=`: `type p = struct { ... };`. -- Visibility is the `export` keyword. No capitalization rule. -- Module paths use `.`. So does field access. Compiler disambiguates. -- Constants: `def NAME: T = lit;` (compile-time). -- Variables: `let name: T = expr;` or `let name = expr;` (inferred). -- Struct literal: `point { x = 0, y = 0 }` (Hare uses `=`). -- Type cast: `expr: T`. -- Pointers are nullable. Compare with `== nil`. -- No methods. A function on `point` is `fn move(p: *point, ...)`. - Plan 9 cc has no methods; neither do we. -- No closures, no lambdas, no anonymous functions. A function value - is a pointer to a named, file-scope function. -- No `:=`, no `make`, no `new`. Allocation is the built-in expression - form (Hare-style): - - `alloc(point { x = 1, y = 2 })` returns `*point` - - `alloc([0u8...], 16)` returns `[]u8` of len/cap 16 - - `free(p)` releases a pointer or slice -- C FFI: a body-less `fn` is an external symbol. `@symbol("name")` - overrides the linker name: - ``` - @symbol("malloc") fn c_malloc(n: u64) *void; - @symbol("free") fn c_free(p: *void) void; - ``` -- Errors are plain strings. See "Errors" below. - -## Errors - -Two idioms, picked by the API author: - -1. **Plan 9 model.** An error is a string. Empty means OK. Functions - that can fail return `(T, error)`. Use this when there are only one - or two error sources and the caller usually wants to format the - message and move on. -2. **Hare tagged-union model.** A function returns `(T | E1 | E2 | ...)`. - Callers `match` on it, or propagate with postfix `?`, or assert - non-error with `!`. Use this when errors are structured (have - payload) or when a caller routinely wants to handle one specific - error kind. - -Both are first-class. Pick whichever fits; do not mix in a single -return type. - -``` -type error = str; - -def eEOF : error = "eof"; -def eShortRead : error = "short read"; -``` - -Functions that can fail return `(T, error)`. The `T` is zero-valued -when the error is non-empty: - -``` -export fn open(name: str) (*file, error) = { - if (name == "") { - return nil, "open: empty name"; - }; - let fd: i32 = sys.open(name, sys.oRdonly, 0); - if (fd < 0) { - return nil, sys.errstr(); - }; - return alloc(file { fd = fd, name = name }), ""; -}; - -let f, err = open("/tmp/x"); -if (err != "") { - fmt.eprintln(err); - os.exit(1); -}; -``` - -Wrapping is string concatenation: `fmt.errorf("open %s: %s", name, -err)`. Comparison is plain string compare. Sentinel errors are -package-level `def`s. - -Why a string and not a struct? Because Plan 9 used errstr for -thirty years and the world did not end. Strings are concrete, -allocation-free when literal, and carry arbitrary detail without -inviting a type hierarchy. Hare's `?` postfix and `match` for -errors are also unavailable to us by rule #7. - -## Naming - -Plan 9 taste lowered to ww. No CamelCase anywhere in ww source. - -- Package names: short, lowercase, one word. `fmt`, `io`, `bufio`. -- Identifiers: lowercase, words run together. `newbuf`, `tcpsock`, - `parsefile`. Underscores allowed but discouraged. -- Visibility: the `export` keyword. Not first-letter case. -- Types: lowercase, like everything else (`point`, `lexer`, `node`). -- Constants (`def`): UPPER_SNAKE for tunables (`MAX_LINE`, `NHASH`); - lowercase for ordinary ones (`eEOF`, `eShortRead`). -- Files: short, descriptive, lowercase. `lex.c`, `parse.c`, `ir.c`, - `lex.ww`, `parse.ww`. -- C-side struct typedefs in the bootstrap mirror Plan 9 (capitalized - is the C convention there): `Node`, `Sym`, `Type`, `Prog`, `Adr`. - In ww source the same shapes are lowercase (`node`, `sym`, `prog`, - `adr`; the type-info struct is just `tinfo` to avoid the keyword). - -## Standard library - -Hare layout, Plan 9 names where they exist. Initial cut: - -``` -lib/ - types/ integer limits, type info - bytes/ byte slice ops - strings/ str ops - fmt/ printf-family - io/ reader/writer/closer interfaces - bufio/ buffered io (Plan 9 'bio' equivalent) - os/ process, fs, args, env - os/exec/ run subcommands - errors/ error type, sentinel values - sort/ sort.Slice, sort.Search - strconv/ number<->string - path/ path manipulation - encoding/ hex, base64, utf8 - hash/ crc32, fnv, sha256 - net/ dial, listen - time/ monotonic + wall clock - sync/ (post-CSP) mutex, once, waitgroup -``` - -C bindings live under `lib/c/`: - -``` -lib/c/ - libc/ malloc, printf, etc. (when calling out) - tls/ libtls (or BearSSL) bindings - crypto/ libcrypto bindings - curses/ ncurses bindings -``` - -Bindings are thin: one `.ww` file per C header section, marked -`extern "c"`, no wrapping logic in the binding layer itself. Higher -ergonomics live in a sibling pure-`ww` package. - -## Build - -POSIX `make`, no autotools, no cmake. - -``` -make # builds libwcc, w6c, w6a, w6l, ww, stdlib -make test # runs all tests (toolchain + stdlib) -make install # installs to $PREFIX (default /usr/local) -make clean -``` - -Target layout under `out/`: - -``` -out/ - bin/ - ww user-facing driver - w6c amd64 compiler - w6a amd64 assembler - w6l amd64 linker - lib/ - libwcc.a frontend library (linked into w6c) - libwwrt.a runtime archive (linked into final binaries) - .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 diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 6c8abb4a..00000000 --- a/PLAN.md +++ /dev/null @@ -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/`. -- Module tests pass: `ww test ./lib/`. -- 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 1–6 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 990–994 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//{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/_*.c`, -`test/lang//*.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. diff --git a/cmd/w6c/CLAUDE.md b/cmd/w6c/CLAUDE.md new file mode 100644 index 00000000..fa712cd5 --- /dev/null +++ b/cmd/w6c/CLAUDE.md @@ -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. diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index ffb074d4..9151e048 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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 + * + * 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; } diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 353549f7..425b224d 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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)) diff --git a/cmd/wcc/lex.c b/cmd/wcc/lex.c index 4f3ae509..e682db7d 100644 --- a/cmd/wcc/lex.c +++ b/cmd/wcc/lex.c @@ -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 }; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index fc879044..baba84c9 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -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)); diff --git a/cmd/wcc/tok.c b/cmd/wcc/tok.c index ff1627df..18312785 100644 --- a/cmd/wcc/tok.c +++ b/cmd/wcc/tok.c @@ -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 ")"; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index cd7f90b4..9d8f5e07 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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; diff --git a/lib/ww/CLAUDE.md b/lib/ww/CLAUDE.md new file mode 100644 index 00000000..a1fba70b --- /dev/null +++ b/lib/ww/CLAUDE.md @@ -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`). diff --git a/lib/ww/lex/lex.ww b/lib/ww/lex/lex.ww index f3ea3e69..55cc919b 100644 --- a/lib/ww/lex/lex.ww +++ b/lib/ww/lex/lex.ww @@ -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 { diff --git a/lib/ww/lex/tok.ww b/lib/ww/lex/tok.ww index a531adc9..c5a81f6c 100644 --- a/lib/ww/lex/tok.ww +++ b/lib/ww/lex/tok.ww @@ -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 ")"; }; diff --git a/lib/ww/parse/decl.ww b/lib/ww/parse/decl.ww index 352964c2..7357fe12 100644 --- a/lib/ww/parse/decl.ww +++ b/lib/ww/parse/decl.ww @@ -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); diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index e88c222c..bd7bb298 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -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; diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index a30761e2..cf23bdd4 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -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) { diff --git a/lib/ww/parse/stmt.ww b/lib/ww/parse/stmt.ww index 2aa98f56..f560fa4a 100644 --- a/lib/ww/parse/stmt.ww +++ b/lib/ww/parse/stmt.ww @@ -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"); diff --git a/lib/ww/sym.ww b/lib/ww/sym.ww index 27478234..add8fe6d 100644 --- a/lib/ww/sym.ww +++ b/lib/ww/sym.ww @@ -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, diff --git a/selfhost/CLAUDE.md b/selfhost/CLAUDE.md new file mode 100644 index 00000000..518449ca --- /dev/null +++ b/selfhost/CLAUDE.md @@ -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. diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index ae5460ee..ff8fd48c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 50335fb2..05b25f9c 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -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; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 38b41254..836bb118 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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. diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 77dda175..4160d7cb 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 1a359e9c..cb6c8130 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 946ad956..b51f2f85 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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;