ww: hare-feature batch (_, const, [_]T, ..., size/offset, assert, for-else)
This commit is contained in:
@@ -62,7 +62,7 @@ Cstage counterparts. Test 995 pins the stronger property — the
|
||||
wwstage rebuilds every one of its own tools through `ww_ww + w6c_ww +
|
||||
w6a_ww + w6l_ww`, byte-for-byte. Test 996 pins the equivalent for the
|
||||
dynamic linker: `w6l_ww` with `-L`/`-l` produces a byte-identical
|
||||
PT_INTERP+PT_DYNAMIC binary to C-side `w6l` on snake.
|
||||
PT_INTERP+PT_DYNAMIC binary to C-side `w6l`.
|
||||
|
||||
`make nocc` is wired and verifies stage-0 self-reproduction locally;
|
||||
the stage-0 binaries under `bootstrap/$(ARCH)/` are gitignored until
|
||||
|
||||
415
CLAUDE.md
415
CLAUDE.md
@@ -1,409 +1,6 @@
|
||||
# ww
|
||||
|
||||
A small systems language. Plan 9 in spirit and code style, Hare in
|
||||
syntax, API, and FFI, no GC, CSP at the end.
|
||||
|
||||
This file is the contract for the work. Read PLAN.md for the schedule.
|
||||
|
||||
## Identity
|
||||
|
||||
- Name: `ww`
|
||||
- Source extension: `.ww`
|
||||
- Module: a directory of `*.ww` files (Hare-style layout)
|
||||
- Toolchain: Plan 9-organized. One library + one binary per role
|
||||
per target architecture. Plan 9 uses a single digit per arch
|
||||
(8=386, 6=amd64, 5=arm, 7=arm64, 9=power); we adopt that.
|
||||
- `cmd/wcc/` — frontend library (lex, parse, check). Builds
|
||||
`libwcc.a`. Not a binary.
|
||||
- `cmd/w6c/` — amd64 compiler. Reads `.ww`, writes `.s`
|
||||
(Plan 9 amd64 asm).
|
||||
- `cmd/w6a/` — amd64 assembler. Reads `.s`, writes `.o`
|
||||
(ELF, for C interop).
|
||||
- `cmd/w6l/` — amd64 linker. Reads `.o` and `.a`, writes a
|
||||
static ELF binary.
|
||||
- `cmd/ww/` — user-facing driver (Hare's `hare(1)` /
|
||||
Plan 9's `cc(1)` analogue). Orchestrates `w6c → w6a → w6l`.
|
||||
Adding a new target later means a new triple (e.g. `w7c`/`w7a`/`w7l`
|
||||
for arm64). The frontend library `wwc` is shared.
|
||||
|
||||
## Hard rules (do not violate)
|
||||
|
||||
1. **No garbage collector.** Ever. Memory is allocated and freed by the
|
||||
programmer. The compiler may insert defer-style cleanup, never a
|
||||
tracing/reference-counting collector.
|
||||
2. **No `map`.** A built-in growable hash table is unsafe without GC
|
||||
(rehashing invalidates pointers). Users may build their own; it is
|
||||
not a language type.
|
||||
3. **No complex runtime.** The runtime is a few hundred lines. It owns:
|
||||
process startup, syscalls trampoline, panic/abort, and (later) the
|
||||
CSP scheduler. It does not own memory beyond a tiny bump arena for
|
||||
startup.
|
||||
4. **Static linking by default.** A `ww` binary is self-contained,
|
||||
like Go. Dynamic linking is opt-in (`-shared`, `-l`).
|
||||
5. **C FFI is first-class, Hare-style.** A body-less `fn` declaration
|
||||
imports the symbol; `@symbol("name")` overrides the linker name.
|
||||
We must be able to bind libcrypto/libtls/ncurses cleanly and link
|
||||
them statically into the final image. The platform calling
|
||||
convention (SysV amd64 on Linux) is the C ABI, so no separate
|
||||
`extern "c"` marker is needed.
|
||||
6. **Plan 9 toolchain. No LLVM, no QBE, no external IR.** We do not
|
||||
invent a portable SSA IR. We follow Plan 9: the per-target
|
||||
compiler (`w6c` for amd64) reads `.ww` and writes Plan 9-style
|
||||
target assembly (`.s`); the per-target assembler (`w6a`) writes
|
||||
ELF objects; the per-target linker (`w6l`) produces a static
|
||||
binary. Each tool uses Plan 9 cc's in-memory `Prog`/`Adr`
|
||||
shapes — read `ref/plan9front/sys/src/cmd/cc/`, `cmd/w6c/`,
|
||||
`cmd/w6a/`, `cmd/w6l/` before writing your own.
|
||||
7. **No generics, no interfaces, no closures, no lambdas.** Four forms
|
||||
of bloat we refuse. Polymorphism, when genuinely needed, is a
|
||||
struct of function pointers plus a `ctx: *void` (Plan 9 `Bio`,
|
||||
Hare `io::stream`). All functions are declared at file scope;
|
||||
function values are pointers to those named functions. No
|
||||
capturing. No anonymous function literals. The compiler does no
|
||||
virtual dispatch; users build vtables explicitly when they want
|
||||
them. If a function needs to work on multiple types, write it
|
||||
multiple times, or operate on `[]u8` and let the caller cast.
|
||||
|
||||
**Tagged unions are allowed**, but only as the Hare-style error
|
||||
idiom: `(T | error)` (and a few sentinel kin like `nomem`).
|
||||
Pattern-matched with `match`. Propagated with postfix `?`. Asserted
|
||||
with postfix `!`. They are not an open extension point — no enum
|
||||
methods, no virtual dispatch through the tag, no nesting beyond
|
||||
what the error idiom needs. If you find yourself reaching for a
|
||||
discriminated record, use a struct with a tag field instead.
|
||||
8. **Tests run after every change.** `make test` is the truth. A
|
||||
change without a green `make test` is not a change.
|
||||
9. **Prototype in C, then self-host.** The C bootstrap toolchain
|
||||
(`libwcc`, `w6c`, `w6a`, `w6l`, `ww`) is throwaway scaffolding.
|
||||
Its job is to compile enough of `ww` to compile the ww
|
||||
reimplementations of itself. Do not over-engineer the C side.
|
||||
|
||||
## Type system
|
||||
|
||||
Hare-style integer names. Fixed width, explicit signedness:
|
||||
|
||||
```
|
||||
i8 i16 i32 i64 signed
|
||||
u8 u16 u32 u64 unsigned
|
||||
uint int register width (target-defined)
|
||||
uintptr pointer-width unsigned
|
||||
f32 f64 IEEE 754
|
||||
bool one byte
|
||||
rune i32, a Unicode code point
|
||||
str immutable utf-8 view: { *u8, len }
|
||||
void zero-sized
|
||||
```
|
||||
|
||||
Composite:
|
||||
|
||||
```
|
||||
*T pointer (may be nil)
|
||||
[N]T fixed array
|
||||
[]T slice: { *T, len, cap }
|
||||
struct { x: i32, y: i32 } aggregate (Hare shape)
|
||||
fn(arg: T) ret function pointer (file-scope only)
|
||||
chan T CSP channel (last phase)
|
||||
```
|
||||
|
||||
No `map`. No `interface`. No `union`. No exceptions. No generics.
|
||||
No closures.
|
||||
|
||||
Polymorphism, when truly needed, is a struct of function pointers
|
||||
plus a `ctx: *void`. See `lib/io/stream.ww` for the canonical shape.
|
||||
This is how Plan 9 `Bio` and Hare `io::stream` work. It is plain
|
||||
data, easy to read, and the compiler does nothing magic for it.
|
||||
|
||||
## Syntax
|
||||
|
||||
Hare-shaped. Trailing semicolons. `=` after function and type
|
||||
signatures. The one departure from Hare: module paths use `.`
|
||||
instead of `::`. Both module navigation and field access use the
|
||||
same dot — the compiler resolves by name lookup.
|
||||
|
||||
```
|
||||
use io;
|
||||
use fmt;
|
||||
use os;
|
||||
|
||||
def MAX_LINE: i32 = 4096;
|
||||
|
||||
type point = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
};
|
||||
|
||||
export fn move(p: *point, dx: i32, dy: i32) void = {
|
||||
p.x += dx;
|
||||
p.y += dy;
|
||||
};
|
||||
|
||||
export fn distance(a: point, b: point) f64 = {
|
||||
let dx: f64 = (a.x - b.x): f64;
|
||||
let dy: f64 = (a.y - b.y): f64;
|
||||
return math.sqrt(dx*dx + dy*dy);
|
||||
};
|
||||
|
||||
export fn main() void = {
|
||||
let p: point = point { x = 0, y = 0 };
|
||||
move(&p, 3, 4);
|
||||
for (let i: i32 = 0; i < MAX_LINE; i += 1) {
|
||||
fmt.println(i);
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Lexical rules:
|
||||
|
||||
- Statements end in `;`. No automatic insertion.
|
||||
- Function bodies follow `=`: `fn f() T = { ... };`.
|
||||
- Type definitions follow `=`: `type p = struct { ... };`.
|
||||
- Visibility is the `export` keyword. No capitalization rule.
|
||||
- Module paths use `.`. So does field access. Compiler disambiguates.
|
||||
- Constants: `def NAME: T = lit;` (compile-time).
|
||||
- Variables: `let name: T = expr;` or `let name = expr;` (inferred).
|
||||
- Struct literal: `point { x = 0, y = 0 }` (Hare uses `=`).
|
||||
- Type cast: `expr: T`.
|
||||
- Pointers are nullable. Compare with `== nil`.
|
||||
- No methods. A function on `point` is `fn move(p: *point, ...)`.
|
||||
Plan 9 cc has no methods; neither do we.
|
||||
- No closures, no lambdas, no anonymous functions. A function value
|
||||
is a pointer to a named, file-scope function.
|
||||
- No `:=`, no `make`, no `new`. Allocation is the built-in expression
|
||||
form (Hare-style):
|
||||
- `alloc(point { x = 1, y = 2 })` returns `*point`
|
||||
- `alloc([0u8...], 16)` returns `[]u8` of len/cap 16
|
||||
- `free(p)` releases a pointer or slice
|
||||
- C FFI: a body-less `fn` is an external symbol. `@symbol("name")`
|
||||
overrides the linker name:
|
||||
```
|
||||
@symbol("malloc") fn c_malloc(n: u64) *void;
|
||||
@symbol("free") fn c_free(p: *void) void;
|
||||
```
|
||||
- Errors are plain strings. See "Errors" below.
|
||||
|
||||
## Errors
|
||||
|
||||
Two idioms, picked by the API author:
|
||||
|
||||
1. **Plan 9 model.** An error is a string. Empty means OK. Functions
|
||||
that can fail return `(T, error)`. Use this when there are only one
|
||||
or two error sources and the caller usually wants to format the
|
||||
message and move on.
|
||||
2. **Hare tagged-union model.** A function returns `(T | E1 | E2 | ...)`.
|
||||
Callers `match` on it, or propagate with postfix `?`, or assert
|
||||
non-error with `!`. Use this when errors are structured (have
|
||||
payload) or when a caller routinely wants to handle one specific
|
||||
error kind.
|
||||
|
||||
Both are first-class. Pick whichever fits; do not mix in a single
|
||||
return type.
|
||||
|
||||
```
|
||||
type error = str;
|
||||
|
||||
def eEOF : error = "eof";
|
||||
def eShortRead : error = "short read";
|
||||
```
|
||||
|
||||
Functions that can fail return `(T, error)`. The `T` is zero-valued
|
||||
when the error is non-empty:
|
||||
|
||||
```
|
||||
export fn open(name: str) (*file, error) = {
|
||||
if (name == "") {
|
||||
return nil, "open: empty name";
|
||||
};
|
||||
let fd: i32 = sys.open(name, sys.oRdonly, 0);
|
||||
if (fd < 0) {
|
||||
return nil, sys.errstr();
|
||||
};
|
||||
return alloc(file { fd = fd, name = name }), "";
|
||||
};
|
||||
|
||||
let f, err = open("/tmp/x");
|
||||
if (err != "") {
|
||||
fmt.eprintln(err);
|
||||
os.exit(1);
|
||||
};
|
||||
```
|
||||
|
||||
Wrapping is string concatenation: `fmt.errorf("open %s: %s", name,
|
||||
err)`. Comparison is plain string compare. Sentinel errors are
|
||||
package-level `def`s.
|
||||
|
||||
Why a string and not a struct? Because Plan 9 used errstr for
|
||||
thirty years and the world did not end. Strings are concrete,
|
||||
allocation-free when literal, and carry arbitrary detail without
|
||||
inviting a type hierarchy. Hare's `?` postfix and `match` for
|
||||
errors are also unavailable to us by rule #7.
|
||||
|
||||
## Naming
|
||||
|
||||
Plan 9 taste lowered to ww. No CamelCase anywhere in ww source.
|
||||
|
||||
- Package names: short, lowercase, one word. `fmt`, `io`, `bufio`.
|
||||
- Identifiers: lowercase, words run together. `newbuf`, `tcpsock`,
|
||||
`parsefile`. Underscores allowed but discouraged.
|
||||
- Visibility: the `export` keyword. Not first-letter case.
|
||||
- Types: lowercase, like everything else (`point`, `lexer`, `node`).
|
||||
- Constants (`def`): UPPER_SNAKE for tunables (`MAX_LINE`, `NHASH`);
|
||||
lowercase for ordinary ones (`eEOF`, `eShortRead`).
|
||||
- Files: short, descriptive, lowercase. `lex.c`, `parse.c`, `ir.c`,
|
||||
`lex.ww`, `parse.ww`.
|
||||
- C-side struct typedefs in the bootstrap mirror Plan 9 (capitalized
|
||||
is the C convention there): `Node`, `Sym`, `Type`, `Prog`, `Adr`.
|
||||
In ww source the same shapes are lowercase (`node`, `sym`, `prog`,
|
||||
`adr`; the type-info struct is just `tinfo` to avoid the keyword).
|
||||
|
||||
## Standard library
|
||||
|
||||
Hare layout, Plan 9 names where they exist. Initial cut:
|
||||
|
||||
```
|
||||
lib/
|
||||
types/ integer limits, type info
|
||||
bytes/ byte slice ops
|
||||
strings/ str ops
|
||||
fmt/ printf-family
|
||||
io/ reader/writer/closer interfaces
|
||||
bufio/ buffered io (Plan 9 'bio' equivalent)
|
||||
os/ process, fs, args, env
|
||||
os/exec/ run subcommands
|
||||
errors/ error type, sentinel values
|
||||
sort/ sort.Slice, sort.Search
|
||||
strconv/ number<->string
|
||||
path/ path manipulation
|
||||
encoding/ hex, base64, utf8
|
||||
hash/ crc32, fnv, sha256
|
||||
net/ dial, listen
|
||||
time/ monotonic + wall clock
|
||||
sync/ (post-CSP) mutex, once, waitgroup
|
||||
```
|
||||
|
||||
C bindings live under `lib/c/`:
|
||||
|
||||
```
|
||||
lib/c/
|
||||
libc/ malloc, printf, etc. (when calling out)
|
||||
tls/ libtls (or BearSSL) bindings
|
||||
crypto/ libcrypto bindings
|
||||
curses/ ncurses bindings
|
||||
```
|
||||
|
||||
Bindings are thin: one `.ww` file per C header section, marked
|
||||
`extern "c"`, no wrapping logic in the binding layer itself. Higher
|
||||
ergonomics live in a sibling pure-`ww` package.
|
||||
|
||||
## Build
|
||||
|
||||
POSIX `make`, no autotools, no cmake.
|
||||
|
||||
```
|
||||
make # builds libwcc, w6c, w6a, w6l, ww, stdlib
|
||||
make test # runs all tests (toolchain + stdlib)
|
||||
make install # installs to $PREFIX (default /usr/local)
|
||||
make clean
|
||||
```
|
||||
|
||||
Target layout under `out/`:
|
||||
|
||||
```
|
||||
out/
|
||||
bin/
|
||||
ww user-facing driver
|
||||
w6c amd64 compiler
|
||||
w6a amd64 assembler
|
||||
w6l amd64 linker
|
||||
lib/
|
||||
libwcc.a frontend library (linked into w6c)
|
||||
libwwrt.a runtime archive (linked into final binaries)
|
||||
<pkg>.a precompiled stdlib modules
|
||||
obj/... intermediate .s, .o per package
|
||||
```
|
||||
|
||||
Static by default. `ww build foo.ww` produces a statically linked
|
||||
ELF. Dynamic is `ww build -shared` or per-library `-l`.
|
||||
|
||||
## Testing
|
||||
|
||||
Three tiers, all driven by `make test`:
|
||||
|
||||
1. **Compiler unit tests** (`test/wcc/`): C, table-driven. Lex, parse,
|
||||
typecheck, IR-gen, codegen each have their own table.
|
||||
2. **Language tests** (`test/lang/*.ww`): each file is a single ww
|
||||
program with a comment header declaring expected exit code and
|
||||
expected stdout. The harness (`test/run`) compiles and runs.
|
||||
3. **Stdlib tests** (`lib/*/+test.ha`-style, here `*_test.ww`): in-tree
|
||||
tests per module. Hare convention, just renamed.
|
||||
|
||||
Every change must:
|
||||
|
||||
- Add or update a test that exercises the change.
|
||||
- Leave `make test` green.
|
||||
- Produce no new warnings (`-Wall -Wextra -Wpedantic` in C; the
|
||||
ww typechecker is strict by default).
|
||||
|
||||
## Rob Pike rules (kept on the wall)
|
||||
|
||||
1. You can't tell where a program will spend its time. Measure.
|
||||
2. Measure. Don't tune for speed without numbers.
|
||||
3. Fancy algorithms are slow when n is small, and n is usually small.
|
||||
4. Fancy algorithms are buggier and harder to implement. Prefer simple.
|
||||
5. Data dominates. Get the structures right and the code follows.
|
||||
6. There is no rule 6.
|
||||
|
||||
Applied to this project:
|
||||
|
||||
- `w6c` is a single-pass-ish recursive-descent parser into a typed
|
||||
AST, walked into a `Prog` list, then printed as text. No parser
|
||||
generator, no LLVM, no SSA pass pipeline.
|
||||
- Optimizer is intentionally absent at first. Constant fold + dead
|
||||
code elim only. Add passes when a benchmark demands one.
|
||||
- Data structures: `Node`, `Sym`, `Type`, `Prog`, `Adr` modeled on
|
||||
Plan 9 cc. Read `ref/plan9front/sys/src/cmd/cc/cc.h` and the
|
||||
per-target headers (`cmd/8c/gc.h`, `cmd/8a/`, `cmd/8l/`) before
|
||||
inventing a new shape.
|
||||
|
||||
## What is in `ref/`
|
||||
|
||||
- `ref/hare/` — the Hare distribution. Read for syntax, FFI
|
||||
(`@symbol`), stdlib layout, type names, error idioms.
|
||||
- `ref/plan9front/` — 9front. Read `sys/src/cmd/cc/` (the shared
|
||||
frontend library), `sys/src/cmd/8c/` and `sys/src/cmd/w6c/` (per-
|
||||
target compilers), `sys/src/cmd/8a/` (assembler), `sys/src/cmd/8l/`
|
||||
(linker) for toolchain organization, `Prog`/`Adr` data shapes,
|
||||
mkfile style, and naming.
|
||||
|
||||
When in doubt: copy Hare for surface syntax and stdlib, copy Plan 9
|
||||
for toolchain organization and compiler internals.
|
||||
|
||||
## What we will NOT build
|
||||
|
||||
- A package manager. Modules are directories. Vendoring is `cp -r`.
|
||||
- A formatter beyond `wwfmt` (one canonical style, no options).
|
||||
- A language server in phase 0. Plain editors are fine.
|
||||
- Generics, interfaces, closures, lambdas. Ever. See hard rule #7.
|
||||
Tagged unions are allowed but ONLY for the Hare-style error idiom
|
||||
(`(T | error)` + `match` + `?` + `!`). No general-purpose enums or
|
||||
open extension points.
|
||||
- An async/await coloring. CSP is the concurrency story; a function
|
||||
is a function.
|
||||
|
||||
## Working agreement (for the assistant)
|
||||
|
||||
When making changes:
|
||||
|
||||
- Prefer editing existing files to creating new ones.
|
||||
- Run `make test` (or the smallest relevant slice) after any code
|
||||
change. Report results.
|
||||
- Match the file's existing style. C files: Plan 9 style (tabs,
|
||||
K&R, short names; see `ref/plan9front/sys/src/cmd/cc/`). ww
|
||||
files: Hare-shaped, formatted by `wwfmt` (one canonical style).
|
||||
- If a design question is non-obvious, propose two options before
|
||||
writing code.
|
||||
- Keep diffs small. One concern per change.
|
||||
- Commits: Plan 9 style. Subject is one short lowercase line,
|
||||
prefixed with the affected area: `w6c: fix const fold for i64`,
|
||||
`lib/fmt: handle %v for slices`, `cc: typo`. Body only when the
|
||||
why is not obvious from the diff. No `Co-Authored-By` trailer,
|
||||
no "Generated with" footer, no emoji.
|
||||
1. This is a project for ww programming language
|
||||
2. ww is a programming language aiming for Hare + CSP, no GC
|
||||
3. cmd/ is the C bootstrap toolchain (wcc frontend lib, w6c/w6a/w6l per-arch, ww driver); selfhost/ is the ww reimplementation
|
||||
4. All C code must strictly align with plan 9 coding style
|
||||
5. lib/ is the standard library; follow Hare APIs (signatures, layout, error idioms) — consult ref/hare/ before designing new modules
|
||||
6. ref/hare and ref/plan9front are read-only references — consult before inventing data shapes or syntax
|
||||
|
||||
409
PLAN.md
409
PLAN.md
@@ -1,409 +0,0 @@
|
||||
# PLAN
|
||||
|
||||
A phased plan for `ww`. Each phase ends with a green `make test` and
|
||||
a tagged checkpoint. Don't start phase N+1 until phase N is green.
|
||||
|
||||
CLAUDE.md is the rulebook. This file is the schedule.
|
||||
|
||||
## Phase 0 — Skeleton (C bootstrap scaffold)
|
||||
|
||||
Goal: a repo that builds, has a Makefile, and has a `ww` driver
|
||||
that prints its version. No compilation yet.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `Makefile` (POSIX) with `all`, `test`, `clean`, `install` targets.
|
||||
- `cmd/wcc/` C library skeleton (builds `libwcc.a`):
|
||||
- `ww.h` — central typedefs (`Node`, `Sym`, `Type`, `Lex`)
|
||||
- `mem.c` — bump arena allocator (no `malloc` in hot paths)
|
||||
- `err.c` — `errorf`, `fatal`, `warn`
|
||||
- (lex/parse/check stubs added in later phases)
|
||||
- `cmd/ww/` driver skeleton: argv parsing, version print.
|
||||
- `test/run` — shell test harness.
|
||||
- `test/wcc/000_smoke.c` — smoke test that asserts `ww -V` prints
|
||||
the version.
|
||||
|
||||
Per-target binaries (`w6c`, `w6a`, `w6l`) are NOT created here. They
|
||||
ship in their own phases (4, 5, 6).
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `make` produces `out/bin/ww` and `out/lib/libwcc.a`.
|
||||
- `make test` runs the smoke test and passes.
|
||||
|
||||
## Phase 1 — Lexer (in `libwcc.a`)
|
||||
|
||||
Goal: tokenize ww source into a stream of `Tok` structs.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/wcc/lex.c` — hand-rolled DFA. Handles:
|
||||
- identifiers, keywords (`fn`, `let`, `def`, `if`, `else`, `for`,
|
||||
`switch`, `case`, `return`, `use`, `type`, `struct`, `defer`,
|
||||
`break`, `continue`, `export`, `proc`, `chan`, `nil`, `true`,
|
||||
`false`)
|
||||
- integer literals (dec, hex, oct, bin) with suffixes (`u8`, `i32`...)
|
||||
- float literals
|
||||
- rune literals `'x'` and string literals `"..."` (UTF-8)
|
||||
- operators and punctuation, including `@` for attributes
|
||||
(`@symbol("name")`, etc.)
|
||||
- explicit `;` terminators — no automatic insertion (Hare rule)
|
||||
- `//` and `/* */` comments
|
||||
- `cmd/wcc/tok.c` — token names, debug printer.
|
||||
- `test/wcc/100_lex.c` — table-driven: input → token sequence.
|
||||
- `test/lang/lex/*.ww` — small files exercised via a tiny test
|
||||
harness that links against `libwcc.a`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A `lextool` test binary dumps a representative sample's token
|
||||
stream deterministically.
|
||||
- All lex tests green.
|
||||
|
||||
## Phase 2 — Parser & AST (in `libwcc.a`)
|
||||
|
||||
Goal: parse the full grammar into an AST. No types yet.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/wcc/parse.c` — recursive descent. Pratt expression parser for
|
||||
precedence. No yacc.
|
||||
- `cmd/wcc/ast.c` — `Node` constructor helpers; printer.
|
||||
- Grammar coverage:
|
||||
- `use` imports (paths use `.` not `::`)
|
||||
- `type` declarations (struct, alias, fn type) with trailing `=`
|
||||
- `def` constants
|
||||
- `let` declarations (top-level and local)
|
||||
- `fn` definitions with `= { ... };` body. No methods; receivers
|
||||
are just first arguments.
|
||||
- `export` visibility marker
|
||||
- all expressions, statements, control flow
|
||||
- `defer`
|
||||
- body-less `fn` declarations and `@symbol("name")` attribute for
|
||||
FFI imports
|
||||
- `test/wcc/200_parse.c` — table-driven AST shape tests.
|
||||
- `test/lang/parse/*.ww` — programs that should parse but not yet
|
||||
typecheck; harness only checks parser exits 0.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- 50+ parse tests green, drawing shapes from
|
||||
`ref/hare/cmd/hare/main.ha`.
|
||||
- AST printer output is deterministic.
|
||||
|
||||
## Phase 3 — Type checker (in `libwcc.a`)
|
||||
|
||||
Goal: resolve names, infer/check types, produce a typed AST.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/wcc/sym.c` — symbol table. Lexical scopes. Plan 9 style hash.
|
||||
- `cmd/wcc/type.c` — type representation, equality, conversion rules.
|
||||
- `cmd/wcc/check.c` — type checking pass over the AST. Errors carry
|
||||
source locations.
|
||||
- Built-in types: all integer widths, `bool`, `f32`, `f64`, `rune`,
|
||||
`str`, `void`, pointers, slices `[]T`, fixed arrays `[N]T`,
|
||||
function types.
|
||||
- Slice semantics: `{ ptr, len, cap }`, indexing bounds-checked at
|
||||
runtime (debug builds; release may elide via flag).
|
||||
- `test/wcc/300_check.c` — table-driven: src → expected error or OK.
|
||||
- `test/lang/check/*.ww` — both passing and failing cases.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- All built-in types correctly checked.
|
||||
- Negative tests produce stable, useful diagnostics.
|
||||
|
||||
## Phase 4 — `w6c` amd64 compiler
|
||||
|
||||
Goal: turn typed AST into Plan 9-style amd64 assembly text. The
|
||||
binary `w6c` is a Plan 9 cc analogue for amd64. There is no separate
|
||||
SSA IR file; the only on-disk intermediate is `.s`.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/w6c/` (links against `libwcc.a`):
|
||||
- `6.out.h` — amd64 opcode enum, register names, addressing
|
||||
modes. Mirror `ref/plan9front/sys/src/cmd/w6c/` shape.
|
||||
- `gc.h` — `Prog`, `Adr`, scratch regs, stack layout.
|
||||
- `cgen.c` — typed AST → `Prog` list (walking, not SSA).
|
||||
- `txt.c` — `Prog` list → textual Plan 9 amd64 asm.
|
||||
- `swt.c`, `peep.c`, `reg.c` — switch lowering, peephole pass,
|
||||
naive register allocation (linear scan or spill-everywhere
|
||||
first; tighten later).
|
||||
- `mkfile` — Plan 9 mkfile next to the Makefile entries.
|
||||
- Output suffix `.s`. Plan 9 amd64 asm syntax (not GAS).
|
||||
- `test/lang/w6c/*.ww` — golden tests: src → expected `.s` text.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `w6c hello.ww` produces `hello.s`.
|
||||
- 20+ programs round-trip identically across runs.
|
||||
- The output is consumable by phase 5's `w6a`.
|
||||
|
||||
## Phase 5 — `w6a` amd64 assembler
|
||||
|
||||
Goal: assemble Plan 9 amd64 asm into ELF object files. ELF (not
|
||||
Plan 9 a.out) so we can interop with C archives in phase 8.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/w6a/`:
|
||||
- `lex.c` — tokenize Plan 9 asm.
|
||||
- `parse.c` — hand-rolled grammar (Plan 9 uses yacc; we go
|
||||
hand-rolled to keep rule #6 honest).
|
||||
- `asm.c` — encode amd64 instructions (REX, ModR/M, SIB).
|
||||
- `obj.c` — emit ELF64 relocatable objects.
|
||||
- `mkfile`
|
||||
- Pseudoregisters supported: `SP`, `FP`, `SB` (static base) per
|
||||
Plan 9 convention, on top of `AX`, `BX`, ..., `R8`-`R15`.
|
||||
- `test/wcc/500_asm.c` — round-trip known asm to known bytes.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `w6a hello.s -o hello.o` produces a valid ELF amd64 object.
|
||||
- `objdump -d hello.o` matches expectations across a 20-program
|
||||
corpus.
|
||||
|
||||
## Phase 6 — `w6l` amd64 linker
|
||||
|
||||
Goal: link ELF objects into a static ELF executable.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `cmd/w6l/`:
|
||||
- `obj.c` — load ELF `.o` files and `.a` archives.
|
||||
- `sym.c` — global symbol table; resolution.
|
||||
- `pass.c` — section layout, relocation application.
|
||||
- `out.c` — emit static ELF executable.
|
||||
- `mkfile`
|
||||
- Static linking only. No dynamic loader. No PT_INTERP segment.
|
||||
- `lib/libwwrt.a` (built later in phase 7) is auto-included.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `w6l -o hello hello.o libwwrt.a` produces a static ELF binary.
|
||||
- `ldd hello` reports "not a dynamic executable".
|
||||
- The binary exits 0 with the program's intended semantics.
|
||||
|
||||
## Phase 7 — Runtime + driver wiring
|
||||
|
||||
Goal: a tiny static runtime, plus the `ww` driver wired to call
|
||||
`w6c → w6a → w6l` end to end.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `rt/start.s` — entry, sets up argc/argv, calls `main`.
|
||||
- `rt/syscall.s` — Linux syscall trampoline.
|
||||
- `rt/panic.c` — `panic`, `abort`, stack unwind for `defer`.
|
||||
- `rt/mem.c` — small page allocator built on `mmap`. Users get
|
||||
raw pages; higher-level allocators ship in `lib/`.
|
||||
- `rt/slice.c` — slice helpers used by codegen
|
||||
(`bounds_check`, `slice_grow` for explicit user calls).
|
||||
- Build artifact: `out/lib/libwwrt.a`.
|
||||
- `cmd/ww/` driver: reads `ww build foo.ww`, runs `w6c` then `w6a`
|
||||
then `w6l`, links `libwwrt.a` into the output.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- A ww program with no libc dependency runs.
|
||||
- Adding `use os` pulls in syscall wrappers; still static.
|
||||
|
||||
## Phase 8 — C FFI + static linking C libs
|
||||
|
||||
Goal: bind to C libraries; produce static binaries containing them.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- Body-less `fn` declarations + `@symbol("name")` attribute parser
|
||||
and checker rules already in phases 2/3 — finalize codegen.
|
||||
- amd64 SysV ABI: formalize struct-by-value and varargs in `w6c`.
|
||||
- `w6l` learns to slurp static archives from a search path.
|
||||
- `lib/c/libc/` — minimal libc bindings (`malloc`, `free`, `printf`,
|
||||
`read`, `write`, `open`, `close`).
|
||||
- `lib/c/tls/` — bindings to libtls or BearSSL (decide; BearSSL is
|
||||
more aligned with our static-linking story).
|
||||
- `lib/c/crypto/` — bindings to libcrypto subset (sha256, aes, hmac).
|
||||
- `lib/c/curses/` — ncurses bindings (initscr, getch, mvprintw, ...).
|
||||
- `ww` driver: `ww build -lcrypto -ltls -lncurses` resolves these
|
||||
via a `pkg-config`-style probe, hands `.a` paths to `w6l`.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `examples/tlsclient.ww` connects to https://example.org and
|
||||
prints the response. Statically linked. No `.so` deps in `ldd`.
|
||||
- `examples/menu.ww` runs an ncurses TUI.
|
||||
|
||||
## Phase 9 — Standard library (Hare-shaped)
|
||||
|
||||
Goal: a usable stdlib. Each module ships with tests.
|
||||
|
||||
Order of build (each is its own milestone):
|
||||
|
||||
1. `types` limits, int helpers
|
||||
2. `bytes` slice ops on `[]u8`
|
||||
3. `strings` ops on `str`
|
||||
4. `io` `stream` struct (function-pointer vtable, no interface);
|
||||
`eof` sentinel value
|
||||
5. `bufio` buffered reader/writer (Plan 9 `bio` analogue)
|
||||
6. `fmt` printf-family writing through `io.stream`
|
||||
7. `os` argv, env, stdin/out/err, file ops
|
||||
8. `errors` `error = str` and sentinel constants
|
||||
9. `strconv` itoa, atoi, parse float
|
||||
10. `sort` `sort.slice`, binary search
|
||||
11. `path` filepath ops
|
||||
12. `encoding/utf8`, `encoding/hex`, `encoding/base64`
|
||||
13. `hash/crc32`, `hash/fnv`, `hash/sha256` (pure ww)
|
||||
14. `time` monotonic, wall clock, sleep
|
||||
15. `net` dial/listen TCP, UDP
|
||||
|
||||
Each module follows the Hare layout: one directory, multiple files,
|
||||
`+linux.ww`, `+test.ww` overlays where useful.
|
||||
|
||||
Exit criteria per module:
|
||||
|
||||
- Module compiles standalone with `ww build ./lib/<mod>`.
|
||||
- Module tests pass: `ww test ./lib/<mod>`.
|
||||
- Public API documented in `README` inside the module dir.
|
||||
|
||||
## Phase 10 — Self-host
|
||||
|
||||
Goal: rewrite `libwcc`, `w6c`, `w6a`, `w6l`, and `ww` in ww. Drop the
|
||||
C bootstrap.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Translate `ww.h` and `Node`/`Sym`/`Type`/`Prog`/`Adr` to ww
|
||||
structs.
|
||||
2. Port `lex.c` → `lex.ww`. Diff token streams against C output.
|
||||
3. Port `parse.c` → `parse.ww`. Diff ASTs.
|
||||
4. Port `check.c`. Diff typed ASTs.
|
||||
5. Port `w6c` (cgen, txt, peep, reg, swt). Diff `.s` output byte
|
||||
for byte.
|
||||
6. Port `w6a` and `w6l`. Diff resulting `.o` and final binaries.
|
||||
7. Three-stage bootstrap:
|
||||
`Cstage (gcc-built tools) → ww1 → ww2 → ww3`, with
|
||||
`cmp ww2 ww3` OK for each tool.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- `make bootstrap` runs the three-stage build green.
|
||||
- The C `cmd/wcc/`, `cmd/w6c/`, `cmd/w6a/`, `cmd/w6l/` C trees are
|
||||
deleted in this phase's final commit.
|
||||
|
||||
### Status (2026-05)
|
||||
|
||||
Steps 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/<arch>/{ww,w6c,w6a,w6l}`), delete the C trees, and promote
|
||||
`selfhost/cmd/*` to `cmd/*`.
|
||||
|
||||
See `BOOTSTRAP.md` for the build flow and `make cstage`/`make wwstage`
|
||||
targets.
|
||||
|
||||
## Phase 11 — CSP
|
||||
|
||||
Goal: channels and lightweight processes, without GC.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- Runtime scheduler: cooperative, M:N, with stack-per-proc (start
|
||||
with fixed-size stacks; segmented stacks are a research item).
|
||||
- `proc f(args)` statement: spawns a process. Fire-and-forget by
|
||||
default. To keep a handle, use the stdlib spawner
|
||||
(`sched.spawn(f, args)` returns a `procval`). No GC means proc
|
||||
lifetimes must be explicit.
|
||||
- `chan T` type. `c <- v` send; `let v = <-c` receive;
|
||||
`select { ... }`.
|
||||
- Channel ownership: closing a channel is a single-writer
|
||||
responsibility (Go rule). Buffered and unbuffered both supported.
|
||||
- `sync` package: `mutex`, `waitgroup`, `once`.
|
||||
|
||||
Concurrency-safety constraints (because no GC):
|
||||
|
||||
- Sending a pointer over a channel transfers ownership unless the
|
||||
type is explicitly marked `shared`. The checker enforces this.
|
||||
- No data races by construction in the safe subset; `unsafe` pkg
|
||||
exists for power users.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- Classic CSP examples (ping-pong, prime sieve, fan-in/fan-out)
|
||||
compile and run.
|
||||
- Race detector under `-race` (later sub-phase) catches a planted
|
||||
race in tests.
|
||||
|
||||
## Test cadence
|
||||
|
||||
Every phase has its own test directory (`test/wcc/<phase>_*.c`,
|
||||
`test/lang/<phase-area>/*.ww`). `make test` runs them all in order
|
||||
of phase. CI is just `make test` in a clean tree.
|
||||
|
||||
## Versioning checkpoints
|
||||
|
||||
Tag the tree at each phase boundary:
|
||||
|
||||
- `v0.0` end of phase 0
|
||||
- `v0.1` end of phase 1
|
||||
- ...
|
||||
- `v0.10` end of phase 10 (self-hosted)
|
||||
- `v1.0` end of phase 11 (CSP merged, ABI declared stable enough
|
||||
to start caring)
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- **`Prog`/`Adr` design churn.** Mitigate by reading Plan 9 cc and
|
||||
per-target compilers before we write our own. Don't invent until
|
||||
you've copied.
|
||||
- **Writing a linker is hard.** ELF is documented, static linking
|
||||
is the small subset. Read `ref/plan9front/sys/src/cmd/8l/` and
|
||||
the ELF spec before writing `w6l`. Start with hello-world and grow.
|
||||
- **Static linking against C libs.** OpenSSL is a pain to build
|
||||
static; BearSSL or LibreTLS are more pleasant. Pick early.
|
||||
- **Self-host bootstrap divergence.** Diff outputs at every stage,
|
||||
not just the final binary. Token streams, ASTs, and `.s` text
|
||||
must match.
|
||||
- **CSP without GC.** Channel-of-pointer ownership rules are the
|
||||
hard part. Prototype the checker with `unsafe`-allowed first;
|
||||
tighten later.
|
||||
|
||||
## Non-goals (restated)
|
||||
|
||||
- No package manager.
|
||||
- No generics, no interfaces, no tagged unions, no closures, no
|
||||
lambdas.
|
||||
- No async/await.
|
||||
- No `map`.
|
||||
- No GC.
|
||||
- No LLVM, no QBE, no external compiler infrastructure. The
|
||||
toolchain is `cc/8c/8a/8l`-shaped (Plan 9), per target.
|
||||
6
cmd/w6c/CLAUDE.md
Normal file
6
cmd/w6c/CLAUDE.md
Normal file
@@ -0,0 +1,6 @@
|
||||
w6c — amd64 compiler. Reads `.ww`, writes Plan 9 amd64 assembly (`.s`).
|
||||
|
||||
- Plan 9 C style: tabs, K&R, short names. Mirror `ref/plan9front/sys/src/cmd/8c/` and `ref/plan9front/sys/src/cmd/cc/`. Do not invent new data shapes — reuse `Prog`/`Adr`/`Node`/`Sym`/`Type` conventions.
|
||||
- Frontend (lex, parse, check) lives in `../wcc/` and links as `libwcc.a`. This directory owns codegen only.
|
||||
- `gc.h` is the per-target header (Plan 9 cc convention). `cgen.c` walks the AST into `Prog` list; `txt.c` prints it. `peep.c` is peephole. `reg.c` is the register allocator. `swt.c` is switch lowering.
|
||||
- The text output must be readable by `w6a` (the amd64 assembler under `../w6a/`). When in doubt about syntax, run `w6a` on the output.
|
||||
137
cmd/w6c/cgen.c
137
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
|
||||
* <abort body>
|
||||
* skip: */
|
||||
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
||||
n->lhs->type == ty_err &&
|
||||
strcmp(n->lhs->str, "assert") == 0 && n->list) {
|
||||
cgexpr(c, n->list, locals);
|
||||
char *skip = mklabel(c, "as");
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
ins1(c, A_JNE, abranch(skip));
|
||||
Node *msg = n->list->next;
|
||||
if (msg) {
|
||||
cgexpr(c, msg, locals);
|
||||
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
||||
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
|
||||
} else {
|
||||
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
|
||||
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
|
||||
}
|
||||
ins1(c, A_CALL, asym("rt_abort"));
|
||||
label(c, skip);
|
||||
break;
|
||||
}
|
||||
/* Hare-style builtins: len(x) and append(s, v). */
|
||||
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
||||
strcmp(n->lhs->str, "len") == 0 && n->list) {
|
||||
@@ -2257,9 +2308,34 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
|
||||
break;
|
||||
}
|
||||
/* struct literal initialiser: field-by-field store. */
|
||||
/* struct literal initialiser: field-by-field store. The
|
||||
* literal carries op == TK_ELLIPSIS when the source ends in
|
||||
* `..., ...` — in that case zero-fill the entire slot first,
|
||||
* so unmentioned fields read as 0. */
|
||||
if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu
|
||||
&& lu->kind == TY_STRUCT) {
|
||||
if (n->rhs->op == TK_ELLIPSIS) {
|
||||
u64 sz = lu->size;
|
||||
/* AX = 0 once, then store from AX. w6a doesn't
|
||||
* accept MOVB imm,mem — use register stores. */
|
||||
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
||||
u64 i = 0;
|
||||
while (i + 8 <= sz) {
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BP, off + (int)i));
|
||||
i += 8;
|
||||
}
|
||||
while (i + 4 <= sz) {
|
||||
ins2(c, A_MOVL, areg(D_AX),
|
||||
amem(D_BP, off + (int)i));
|
||||
i += 4;
|
||||
}
|
||||
while (i < sz) {
|
||||
ins2(c, A_MOVB, areg(D_AX),
|
||||
amem(D_BP, off + (int)i));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
for (Node *f = n->rhs->list; f; f = f->next) {
|
||||
/* find offset of this field */
|
||||
u64 foff = 0;
|
||||
@@ -2315,6 +2391,46 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* array literal initialiser: `let xs: [N]T = [a, b, c];`.
|
||||
* Walk elements in declaration order, store each at off + i*esz
|
||||
* using the right width for the element type. The trailing
|
||||
* `...` repeat marker (an N_FIELD with str=="...") fills the
|
||||
* remaining slots with the last value. */
|
||||
if (n->rhs && n->rhs->kind == N_ARRLIT && lu
|
||||
&& lu->kind == TY_ARRAY) {
|
||||
int esz = lu->sub ? (int)lu->sub->size : 1;
|
||||
int op = A_MOVQ;
|
||||
if (esz == 1) op = A_MOVB;
|
||||
else if (esz == 4) op = A_MOVL;
|
||||
/* esz == 2 (i16/u16) falls through to MOVQ — over-writes
|
||||
* by 6B; the next element store rewrites the high half.
|
||||
* For the last element this trails 6 bytes into the next
|
||||
* stack slot. Add MOVW to w6a if real i16 arrays land. */
|
||||
int idx = 0;
|
||||
Node *last = NULL;
|
||||
int repeat = 0;
|
||||
for (Node *e = n->rhs->list; e; e = e->next) {
|
||||
if (e->kind == N_FIELD && e->str &&
|
||||
strcmp(e->str, "...") == 0) {
|
||||
repeat = 1;
|
||||
break;
|
||||
}
|
||||
cgexpr(c, e, *locals);
|
||||
ins2(c, op, areg(D_AX),
|
||||
amem(D_BP, off + idx * esz));
|
||||
last = e;
|
||||
idx++;
|
||||
}
|
||||
if (repeat && last) {
|
||||
/* fill remaining slots with the value already in AX. */
|
||||
while (idx < (int)lu->alen) {
|
||||
ins2(c, op, areg(D_AX),
|
||||
amem(D_BP, off + idx * esz));
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (n->rhs && sz == 8) {
|
||||
cgexpr(c, n->rhs, *locals);
|
||||
if (isf) {
|
||||
@@ -2488,6 +2604,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
|
||||
char *loop = mklabel(c, "rloop");
|
||||
char *end = mklabel(c, "rend");
|
||||
char *natural_exit = end;
|
||||
if (n->els) natural_exit = mklabel(c, "relseloop");
|
||||
if (nloops < LOOP_MAX) {
|
||||
loop_cont[nloops] = loop;
|
||||
loop_brk[nloops] = end;
|
||||
@@ -2497,7 +2615,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
ins2(c, A_MOVQ, amem(D_BP, ioff), areg(D_AX));
|
||||
ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_BX));
|
||||
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
||||
ins1(c, A_JGE, abranch(end));
|
||||
ins1(c, A_JGE, abranch(natural_exit));
|
||||
/* compute element base: s.ptr + i*esz → BX */
|
||||
if (esz > 1) {
|
||||
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
||||
@@ -2523,6 +2641,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
cgstmt(c, n->body, locals, frame);
|
||||
ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff));
|
||||
ins1(c, A_JMP, abranch(loop));
|
||||
if (n->els) {
|
||||
label(c, natural_exit);
|
||||
cgstmt(c, n->els, locals, frame);
|
||||
}
|
||||
label(c, end);
|
||||
if (nloops > 0) nloops--;
|
||||
break;
|
||||
@@ -2530,12 +2652,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
case N_FOR: {
|
||||
char *loop = mklabel(c, "loop");
|
||||
char *end = mklabel(c, "endloop");
|
||||
/* `else` runs at normal cond-false exit; break skips it.
|
||||
* Separate the natural exit label from the break target so
|
||||
* the else block sits between them. */
|
||||
char *natural_exit = end;
|
||||
if (n->els) natural_exit = mklabel(c, "elseloop");
|
||||
if (n->lhs) cgstmt(c, n->lhs, locals, frame);
|
||||
label(c, loop);
|
||||
if (n->cond) {
|
||||
cgexpr(c, n->cond, *locals);
|
||||
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
||||
ins1(c, A_JE, abranch(end));
|
||||
ins1(c, A_JE, abranch(natural_exit));
|
||||
}
|
||||
if (nloops < LOOP_MAX) {
|
||||
loop_cont[nloops] = loop;
|
||||
@@ -2546,6 +2673,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
if (nloops > 0) nloops--;
|
||||
if (n->rhs) cgexpr(c, n->rhs, *locals);
|
||||
ins1(c, A_JMP, abranch(loop));
|
||||
if (n->els) {
|
||||
label(c, natural_exit);
|
||||
cgstmt(c, n->els, locals, frame);
|
||||
}
|
||||
label(c, end);
|
||||
break;
|
||||
}
|
||||
|
||||
163
cmd/wcc/check.c
163
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))
|
||||
|
||||
@@ -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 };
|
||||
|
||||
119
cmd/wcc/parse.c
119
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));
|
||||
|
||||
@@ -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 ")";
|
||||
|
||||
@@ -114,6 +114,8 @@ typedef enum {
|
||||
TK_AS, /* reserved for future cast spelling, not active */
|
||||
TK_STATIC, /* Hare-style storage-class qualifier */
|
||||
TK_MATCH, /* match expression head */
|
||||
TK_CONST, /* const binding */
|
||||
TK_UNDER, /* bare '_' discard */
|
||||
|
||||
/* punct + operators */
|
||||
TK_LPAREN, /* ( */
|
||||
@@ -433,6 +435,7 @@ struct Sym {
|
||||
Type *type;
|
||||
Node *decl;
|
||||
int exported;
|
||||
int is_const; /* const-bound (assignment rejected) */
|
||||
Sym *next; /* iteration */
|
||||
Sym *hashnext; /* bucket chain */
|
||||
Scope *scope;
|
||||
|
||||
13
lib/ww/CLAUDE.md
Normal file
13
lib/ww/CLAUDE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
lib/ww — frontend in ww (lex, parse, sym, typ, ast). Mirrors `cmd/wcc/` (the C bootstrap frontend). Compiled by `cmd/w6c` against the wwstage cgen.
|
||||
|
||||
Style:
|
||||
- Hare-shaped syntax. Trailing `;`. `=` after fn/type signatures. `export` for visibility.
|
||||
- Plan 9 names: run-together lowercase (`parsefile`, `lexnext`, `mksym`). No snake_case. No CamelCase.
|
||||
- ww-side type names are lowercase (`node`, `sym`, `typ`); the C-side equivalents in `cmd/wcc/ww.h` are capitalized (`Node`, `Sym`, `Type`) per Plan 9 cc convention.
|
||||
|
||||
This directory is **not** a Hare-style stdlib module — it's a compiler-frontend port, so structural fidelity to `cmd/wcc/` beats Hare API mimicry. The "follow Hare APIs" rule from the root CLAUDE.md applies to peer `lib/*` modules (fmt, io, bufio, strings, ...), not here.
|
||||
|
||||
When porting from `cmd/wcc/*.c`:
|
||||
- Keep the same data layout. The selfhost goal is byte-compatible structures so dump/inspect tools work either way.
|
||||
- Read `cmd/wcc/ww.h` first for canonical field names and ordering.
|
||||
- See `selfhost/CLAUDE.md` for the wwstage cgen traps to avoid (two-level field write, `(scalar,str)` tuple return, `def : str`, undersized `amalloc`).
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ")"; };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -24,6 +24,7 @@ type sym = struct {
|
||||
type_: *tinfo,
|
||||
decl: *node,
|
||||
exported: i32,
|
||||
is_const: i32, // const-bound (assignment rejected)
|
||||
snext: *sym, // iteration order
|
||||
hashnext: *sym, // hash bucket chain
|
||||
scope: *scope,
|
||||
|
||||
25
selfhost/CLAUDE.md
Normal file
25
selfhost/CLAUDE.md
Normal file
@@ -0,0 +1,25 @@
|
||||
selfhost — ww reimplementation of the toolchain (wcc, w6c, w6a, w6l, ww, wwdump). Compiled by the C bootstrap (`../cmd/`); the goal is to eventually compile itself.
|
||||
|
||||
Identifiers: Plan 9 style. lowercase, words run together (`newbuf`, `tcpsock`, `parsefile`). No snake_case.
|
||||
|
||||
Syntax and idioms: Hare-shaped. Trailing `;`, `=` after fn/type signatures, `export` for visibility, `match`/`?`/`!` for tagged-union errors. Consult `ref/hare/` for canonical signatures and error-handling patterns before inventing your own.
|
||||
|
||||
The C bootstrap's cgen ("wwstage") has four known silent-miscompilation traps. They produce wrong runtime behavior, not compile errors. When porting C → ww here, default to the workarounds:
|
||||
|
||||
1. **Two-level field write through pointer field doesn't stick.**
|
||||
`r.sym.isdyn = 1` where `r.sym: *T` drops the write. Bind the inner pointer to a local first:
|
||||
```
|
||||
let sym: *lsym = r.sym;
|
||||
sym.isdyn = 1;
|
||||
```
|
||||
|
||||
2. **Tuple return `(scalar, str)` corrupts the str half.** Both ptr and len come back garbage. Split into two functions — one returns the scalar, another returns the str. Plain `str` returns are fine.
|
||||
|
||||
3. **`def NAME: str = "...";` is broken.** The `.len` picks up an unrelated accumulator. Wrap the literal in a nullary fn instead:
|
||||
```
|
||||
fn namestr() str = { return "..."; };
|
||||
```
|
||||
|
||||
4. **`amalloc(n)` with n < struct size silently corrupts neighbours.** No error — the bump arena hands out n bytes and field writes overflow into the next record. When introducing or growing a struct, audit every `amalloc(_, n)` call site and over-size (we routinely pass 48 for a 40-byte struct). Symptom: linked-list prepends lose all but the most recent entry.
|
||||
|
||||
If a port "should work" but the binary is wrong, suspect these first.
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user