From 1657bdeda3e7c295be3c73635fdb73a4063437d7 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 11 May 2026 02:17:47 +0900 Subject: [PATCH] =?UTF-8?q?ww:=20import=20toolchain=20=E2=80=94=20C=20boot?= =?UTF-8?q?strap=20+=20ww-side=20self-host=20(phases=200-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C bootstrap (phases 0-9): cmd/wwc, cmd/6c, cmd/6a, cmd/6l, cmd/ww, rt, lib/*. ww-side self-host (phase 10): selfhost/cmd/wwc — ww-cgen frontend; bootstrap fixed point. selfhost/cmd/6a — assembler; byte-identical to C 6a (test 991). selfhost/cmd/6l — linker w/ archive (.a) support; byte-identical to C 6l (test 992). selfhost/cmd/ww — driver (build/run/version); byte-identical to C ww (test 993). make test: 15/15. make bootstrap: ww2.s == ww3.s, ww2.o == ww3.o, ww2 == ww3 byte-identical, with the full ww-tooled chain. --- .gitignore | 12 + CLAUDE.md | 55 +- Makefile | 269 ++ cmd/6a/a.h | 107 + cmd/6a/asm.c | 689 +++ cmd/6a/lex.c | 31 + cmd/6a/main.c | 59 + cmd/6a/obj.c | 242 + cmd/6a/parse.c | 405 ++ cmd/6c/6.out.h | 107 + cmd/6c/cgen.c | 2622 ++++++++++ cmd/6c/gc.h | 56 + cmd/6c/main.c | 93 + cmd/6c/peep.c | 8 + cmd/6c/reg.c | 9 + cmd/6c/swt.c | 8 + cmd/6c/txt.c | 189 + cmd/6l/l.h | 74 + cmd/6l/main.c | 73 + cmd/6l/obj.c | 324 ++ cmd/6l/out.c | 89 + cmd/6l/pass.c | 63 + cmd/6l/sym.c | 27 + cmd/ww/main.c | 349 ++ cmd/wwc/ast.c | 189 + cmd/wwc/check.c | 945 ++++ cmd/wwc/err.c | 76 + cmd/wwc/lex.c | 476 ++ cmd/wwc/mem.c | 117 + cmd/wwc/parse.c | 1183 +++++ cmd/wwc/sym.c | 74 + cmd/wwc/tok.c | 199 + cmd/wwc/type.c | 370 ++ cmd/wwc/ww.h | 462 ++ cmd/wwdump/main.c | 119 + lib/ascii/ascii.ww | 92 + lib/bufio/bufio.ww | 82 + lib/bytes/bytes.ww | 51 + lib/c/libc/libc.ww | 22 + lib/encoding/hex/hex.ww | 19 + lib/encoding/utf8/utf8.ww | 14 + lib/errors/errors.ww | 30 + lib/fmt/fmt.ww | 67 + lib/hash/fnv/fnv.ww | 15 + lib/io/stream.ww | 28 + lib/net/net.ww | 48 + lib/os/os.ww | 171 + lib/path/path.ww | 15 + lib/slices/slices.ww | 49 + lib/sort/sort.ww | 27 + lib/strconv/strconv.ww | 119 + lib/strings/strings.ww | 95 + lib/time/time.ww | 15 + lib/types/types.ww | 48 + rt/abort.s | 17 + rt/alloc.s | 24 + rt/start.s | 16 + rt/streq.s | 27 + rt/syscall.s | 16 + selfhost/cmd/6a/asm.ww | 681 +++ selfhost/cmd/6a/lex.ww | 66 + selfhost/cmd/6a/main.combined.ww | 2168 +++++++++ selfhost/cmd/6a/main.ww | 110 + selfhost/cmd/6a/obj.ww | 293 ++ selfhost/cmd/6a/parse.ww | 583 +++ selfhost/cmd/6a/types.ww | 193 + selfhost/cmd/6l/main.combined.ww | 1077 +++++ selfhost/cmd/6l/main.ww | 120 + selfhost/cmd/6l/obj.ww | 486 ++ selfhost/cmd/6l/out.ww | 91 + selfhost/cmd/6l/pass.ww | 64 + selfhost/cmd/6l/sym.ww | 75 + selfhost/cmd/ww/main.combined.ww | 975 ++++ selfhost/cmd/ww/main.ww | 695 +++ selfhost/cmd/wwc/ast.ww | 335 ++ selfhost/cmd/wwc/cgen.ww | 2868 +++++++++++ selfhost/cmd/wwc/check.ww | 247 + selfhost/cmd/wwc/err.ww | 37 + selfhost/cmd/wwc/lex.ww | 656 +++ selfhost/cmd/wwc/mem.ww | 106 + selfhost/cmd/wwc/parse.ww | 960 ++++ selfhost/cmd/wwc/sym.ww | 113 + selfhost/cmd/wwc/tok.ww | 394 ++ selfhost/cmd/wwc/typ.ww | 329 ++ selfhost/cmd/wwdump/main.combined.ww | 6559 ++++++++++++++++++++++++++ selfhost/cmd/wwdump/main.ww | 156 + selfhost/test/smoke.combined.ww | 549 +++ selfhost/test/smoke.ww | 163 + selfhost/test/sym_link.ww | 45 + selfhost/test/uses.ww | 29 + test/run | 66 + test/wwc/000_smoke.c | 87 + test/wwc/100_lex.c | 142 + test/wwc/200_parse.c | 190 + test/wwc/300_check.c | 126 + test/wwc/400_6c.c | 90 + test/wwc/500_6a.c | 62 + test/wwc/600_6l.c | 67 + test/wwc/610_arch.c | 96 + test/wwc/700_e2e.c | 863 ++++ test/wwc/800_ffi.c | 80 + test/wwc/900_stdlib.c | 65 + test/wwc/990_selfhost.c | 786 +++ test/wwc/991_6a_ww.c | 135 + test/wwc/992_6l_ww.c | 137 + test/wwc/993_ww_ww.c | 177 + 106 files changed, 35654 insertions(+), 15 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/6a/a.h create mode 100644 cmd/6a/asm.c create mode 100644 cmd/6a/lex.c create mode 100644 cmd/6a/main.c create mode 100644 cmd/6a/obj.c create mode 100644 cmd/6a/parse.c create mode 100644 cmd/6c/6.out.h create mode 100644 cmd/6c/cgen.c create mode 100644 cmd/6c/gc.h create mode 100644 cmd/6c/main.c create mode 100644 cmd/6c/peep.c create mode 100644 cmd/6c/reg.c create mode 100644 cmd/6c/swt.c create mode 100644 cmd/6c/txt.c create mode 100644 cmd/6l/l.h create mode 100644 cmd/6l/main.c create mode 100644 cmd/6l/obj.c create mode 100644 cmd/6l/out.c create mode 100644 cmd/6l/pass.c create mode 100644 cmd/6l/sym.c create mode 100644 cmd/ww/main.c create mode 100644 cmd/wwc/ast.c create mode 100644 cmd/wwc/check.c create mode 100644 cmd/wwc/err.c create mode 100644 cmd/wwc/lex.c create mode 100644 cmd/wwc/mem.c create mode 100644 cmd/wwc/parse.c create mode 100644 cmd/wwc/sym.c create mode 100644 cmd/wwc/tok.c create mode 100644 cmd/wwc/type.c create mode 100644 cmd/wwc/ww.h create mode 100644 cmd/wwdump/main.c create mode 100644 lib/ascii/ascii.ww create mode 100644 lib/bufio/bufio.ww create mode 100644 lib/bytes/bytes.ww create mode 100644 lib/c/libc/libc.ww create mode 100644 lib/encoding/hex/hex.ww create mode 100644 lib/encoding/utf8/utf8.ww create mode 100644 lib/errors/errors.ww create mode 100644 lib/fmt/fmt.ww create mode 100644 lib/hash/fnv/fnv.ww create mode 100644 lib/io/stream.ww create mode 100644 lib/net/net.ww create mode 100644 lib/os/os.ww create mode 100644 lib/path/path.ww create mode 100644 lib/slices/slices.ww create mode 100644 lib/sort/sort.ww create mode 100644 lib/strconv/strconv.ww create mode 100644 lib/strings/strings.ww create mode 100644 lib/time/time.ww create mode 100644 lib/types/types.ww create mode 100644 rt/abort.s create mode 100644 rt/alloc.s create mode 100644 rt/start.s create mode 100644 rt/streq.s create mode 100644 rt/syscall.s create mode 100644 selfhost/cmd/6a/asm.ww create mode 100644 selfhost/cmd/6a/lex.ww create mode 100644 selfhost/cmd/6a/main.combined.ww create mode 100644 selfhost/cmd/6a/main.ww create mode 100644 selfhost/cmd/6a/obj.ww create mode 100644 selfhost/cmd/6a/parse.ww create mode 100644 selfhost/cmd/6a/types.ww create mode 100644 selfhost/cmd/6l/main.combined.ww create mode 100644 selfhost/cmd/6l/main.ww create mode 100644 selfhost/cmd/6l/obj.ww create mode 100644 selfhost/cmd/6l/out.ww create mode 100644 selfhost/cmd/6l/pass.ww create mode 100644 selfhost/cmd/6l/sym.ww create mode 100644 selfhost/cmd/ww/main.combined.ww create mode 100644 selfhost/cmd/ww/main.ww create mode 100644 selfhost/cmd/wwc/ast.ww create mode 100644 selfhost/cmd/wwc/cgen.ww create mode 100644 selfhost/cmd/wwc/check.ww create mode 100644 selfhost/cmd/wwc/err.ww create mode 100644 selfhost/cmd/wwc/lex.ww create mode 100644 selfhost/cmd/wwc/mem.ww create mode 100644 selfhost/cmd/wwc/parse.ww create mode 100644 selfhost/cmd/wwc/sym.ww create mode 100644 selfhost/cmd/wwc/tok.ww create mode 100644 selfhost/cmd/wwc/typ.ww create mode 100644 selfhost/cmd/wwdump/main.combined.ww create mode 100644 selfhost/cmd/wwdump/main.ww create mode 100644 selfhost/test/smoke.combined.ww create mode 100644 selfhost/test/smoke.ww create mode 100644 selfhost/test/sym_link.ww create mode 100644 selfhost/test/uses.ww create mode 100755 test/run create mode 100644 test/wwc/000_smoke.c create mode 100644 test/wwc/100_lex.c create mode 100644 test/wwc/200_parse.c create mode 100644 test/wwc/300_check.c create mode 100644 test/wwc/400_6c.c create mode 100644 test/wwc/500_6a.c create mode 100644 test/wwc/600_6l.c create mode 100644 test/wwc/610_arch.c create mode 100644 test/wwc/700_e2e.c create mode 100644 test/wwc/800_ffi.c create mode 100644 test/wwc/900_stdlib.c create mode 100644 test/wwc/990_selfhost.c create mode 100644 test/wwc/991_6a_ww.c create mode 100644 test/wwc/992_6l_ww.c create mode 100644 test/wwc/993_ww_ww.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..32691fe4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Build outputs. +/out/ + +# Stray executables produced by ad-hoc `ww build` runs in the project +# root. Real source for these lives under selfhost/test/ as .ww files. +/loop +/sym_link + +# Per-module build artifacts. The .combined.ww files under selfhost/ +# are intentionally tracked — they're frozen bootstrap inputs. +selfhost/**/*.o +selfhost/**/*.s diff --git a/CLAUDE.md b/CLAUDE.md index 6dc008b0..256a0593 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,16 +54,23 @@ This file is the contract for the work. Read PLAN.md for the schedule. binary. Each tool uses Plan 9 cc's in-memory `Prog`/`Adr` shapes — read `ref/plan9front/sys/src/cmd/cc/`, `cmd/6c/`, `cmd/6a/`, `cmd/6l/` before writing your own. -7. **No generics, no interfaces, no tagged unions, no closures, no - lambdas.** Five forms of bloat we refuse. Polymorphism, when - genuinely needed, is a struct of function pointers plus a - `ctx: *void` (Plan 9 `Bio`, Hare `io::stream`). All functions are - declared at file scope; function values are pointers to those - named functions. No capturing. No anonymous function literals. - The compiler does no virtual dispatch; users build vtables - explicitly when they want them. If a function needs to work on - multiple types, write it multiple times, or operate on `[]u8` - and let the caller cast. +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 @@ -176,9 +183,20 @@ Lexical rules: ## Errors -Plan 9 model. An error is a string. Empty means OK. Hare uses -tagged unions for errors; we don't have unions, so we drop down -to the plainer Plan 9 thing. +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; @@ -365,8 +383,10 @@ for toolchain organization and compiler internals. - A package manager. Modules are directories. Vendoring is `cp -r`. - A formatter beyond `wwfmt` (one canonical style, no options). - A language server in phase 0. Plain editors are fine. -- Generics, interfaces, tagged unions, closures, lambdas. Ever. - See hard rule #7. +- 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. @@ -383,3 +403,8 @@ When making changes: - 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: `6c: 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. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..566616f5 --- /dev/null +++ b/Makefile @@ -0,0 +1,269 @@ +# ww — Plan 9-organised toolchain. POSIX make. +# +# Build a small static frontend library libwwc.a and the user-facing +# driver `ww`. Per-target tools (6c, 6a, 6l) and the runtime are added +# in their own phases. Everything lands under out/. + +PREFIX ?= /usr/local +CC ?= cc +AR ?= ar +CFLAGS ?= -O0 -g -Wall -Wextra -Wpedantic -Wno-unused-parameter +CFLAGS += -std=c99 -fno-strict-aliasing -D_POSIX_C_SOURCE=200809L +INCS = -Icmd/wwc + +OUT = out +BIN = $(OUT)/bin +LIB = $(OUT)/lib +OBJ = $(OUT)/obj + +WWC_SRC = cmd/wwc/mem.c cmd/wwc/err.c cmd/wwc/tok.c cmd/wwc/lex.c \ + cmd/wwc/ast.c cmd/wwc/parse.c cmd/wwc/sym.c cmd/wwc/type.c \ + cmd/wwc/check.c +WWC_OBJ = $(WWC_SRC:cmd/wwc/%.c=$(OBJ)/wwc/%.o) + +WW_SRC = cmd/ww/main.c +WW_OBJ = $(WW_SRC:cmd/ww/%.c=$(OBJ)/ww/%.o) + +WD_SRC = cmd/wwdump/main.c +WD_OBJ = $(WD_SRC:cmd/wwdump/%.c=$(OBJ)/wwdump/%.o) + +C6_SRC = cmd/6c/main.c cmd/6c/cgen.c cmd/6c/txt.c cmd/6c/swt.c \ + cmd/6c/peep.c cmd/6c/reg.c +C6_OBJ = $(C6_SRC:cmd/6c/%.c=$(OBJ)/6c/%.o) + +A6_SRC = cmd/6a/main.c cmd/6a/lex.c cmd/6a/parse.c cmd/6a/asm.c cmd/6a/obj.c +A6_OBJ = $(A6_SRC:cmd/6a/%.c=$(OBJ)/6a/%.o) + +L6_SRC = cmd/6l/main.c cmd/6l/obj.c cmd/6l/sym.c cmd/6l/pass.c cmd/6l/out.c +L6_OBJ = $(L6_SRC:cmd/6l/%.c=$(OBJ)/6l/%.o) + +RT_S = rt/start.s rt/syscall.s rt/alloc.s rt/streq.s rt/abort.s +RT_OBJ = $(RT_S:rt/%.s=$(OBJ)/rt/%.o) + +BINS = $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l $(BIN)/wwdump \ + $(BIN)/wwdump_ww $(BIN)/6a_ww $(BIN)/6l_ww $(BIN)/ww_ww +LIBS = $(LIB)/libwwc.a $(LIB)/libwwrt.a + +all: $(BINS) $(LIBS) + +# ---- libwwc.a ---------------------------------------------------------- +$(LIB)/libwwc.a: $(WWC_OBJ) | $(LIB) + $(AR) rcs $@ $(WWC_OBJ) + +$(OBJ)/wwc/%.o: cmd/wwc/%.c cmd/wwc/ww.h | $(OBJ)/wwc + $(CC) $(CFLAGS) $(INCS) -c -o $@ $< + +# ---- ww driver --------------------------------------------------------- +$(BIN)/ww: $(WW_OBJ) $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $(WW_OBJ) -L$(LIB) -lwwc + +$(OBJ)/ww/%.o: cmd/ww/%.c cmd/wwc/ww.h | $(OBJ)/ww + $(CC) $(CFLAGS) $(INCS) -c -o $@ $< + +# ---- wwdump (lex/AST diff anchor) -------------------------------------- +$(BIN)/wwdump: $(WD_OBJ) $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $(WD_OBJ) -L$(LIB) -lwwc + +$(OBJ)/wwdump/%.o: cmd/wwdump/%.c cmd/wwc/ww.h | $(OBJ)/wwdump + $(CC) $(CFLAGS) $(INCS) -c -o $@ $< + +# ---- 6c amd64 compiler ------------------------------------------------- +$(BIN)/6c: $(C6_OBJ) $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $(C6_OBJ) -L$(LIB) -lwwc + +$(OBJ)/6c/%.o: cmd/6c/%.c cmd/6c/gc.h cmd/6c/6.out.h cmd/wwc/ww.h | $(OBJ)/6c + $(CC) $(CFLAGS) $(INCS) -Icmd/6c -c -o $@ $< + +# ---- 6a amd64 assembler ------------------------------------------------ +$(BIN)/6a: $(A6_OBJ) | $(BIN) + $(CC) $(CFLAGS) -o $@ $(A6_OBJ) + +$(OBJ)/6a/%.o: cmd/6a/%.c cmd/6a/a.h cmd/6c/6.out.h | $(OBJ)/6a + $(CC) $(CFLAGS) -Icmd/6a -Icmd/6c -c -o $@ $< + +# ---- 6l amd64 linker --------------------------------------------------- +$(BIN)/6l: $(L6_OBJ) | $(BIN) + $(CC) $(CFLAGS) -o $@ $(L6_OBJ) + +$(OBJ)/6l/%.o: cmd/6l/%.c cmd/6l/l.h | $(OBJ)/6l + $(CC) $(CFLAGS) -Icmd/6l -c -o $@ $< + +# ---- ww-side wwdump (the lexer port, exercised by 990_selfhost) -------- +# Built via the user-facing ww driver, with -I selfhost/cmd/wwc so it +# can find the lex/tok/mem ports. Output named wwdump_ww to avoid +# colliding with the C-side wwdump in $(BIN). +$(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ + selfhost/cmd/wwc/lex.ww selfhost/cmd/wwc/tok.ww \ + selfhost/cmd/wwc/mem.ww selfhost/cmd/wwc/ast.ww \ + selfhost/cmd/wwc/parse.ww selfhost/cmd/wwc/typ.ww \ + selfhost/cmd/wwc/sym.ww selfhost/cmd/wwc/check.ww \ + selfhost/cmd/wwc/cgen.ww \ + $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(LIB)/libwwrt.a | $(BIN) + cd $(BIN) && ./ww build -I $$PWD/../../selfhost/cmd/wwc \ + $$PWD/../../selfhost/cmd/wwdump/main.ww + mv $(BIN)/main $@ + +# ---- ww-side 6a (assembler port, exercised by 991_6a_ww) --------------- +# Built like wwdump_ww. Needs -I selfhost/cmd/6a for the local types/lex/ +# parse/asm/obj modules and -I selfhost/cmd/wwc to find `mem`. +$(BIN)/6a_ww: selfhost/cmd/6a/main.ww selfhost/cmd/6a/types.ww \ + selfhost/cmd/6a/lex.ww selfhost/cmd/6a/parse.ww \ + selfhost/cmd/6a/asm.ww selfhost/cmd/6a/obj.ww \ + selfhost/cmd/wwc/mem.ww \ + $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(LIB)/libwwrt.a | $(BIN) + cd $(BIN) && ./ww build \ + -I $$PWD/../../selfhost/cmd/6a \ + -I $$PWD/../../selfhost/cmd/wwc \ + $$PWD/../../selfhost/cmd/6a/main.ww + mv $(BIN)/main $@ + +# ---- ww-side 6l (linker port, exercised by 992_6l_ww) ------------------ +# Built like 6a_ww. Needs -I selfhost/cmd/6l for the local sym/obj/pass/ +# out modules and -I selfhost/cmd/wwc to find `mem`. +$(BIN)/6l_ww: selfhost/cmd/6l/main.ww selfhost/cmd/6l/sym.ww \ + selfhost/cmd/6l/obj.ww selfhost/cmd/6l/pass.ww \ + selfhost/cmd/6l/out.ww selfhost/cmd/wwc/mem.ww \ + $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(LIB)/libwwrt.a | $(BIN) + cd $(BIN) && ./ww build \ + -I $$PWD/../../selfhost/cmd/6l \ + -I $$PWD/../../selfhost/cmd/wwc \ + $$PWD/../../selfhost/cmd/6l/main.ww + mv $(BIN)/main $@ + +# ---- ww-side ww driver (exercised by 993_ww_ww) ------------------------ +# The driver pulls in lib/os (default search path) and selfhost/cmd/wwc +# (for the bump arena). It then orchestrates 6c/6a/6l like the C driver. +$(BIN)/ww_ww: selfhost/cmd/ww/main.ww selfhost/cmd/wwc/mem.ww lib/os/os.ww \ + $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(LIB)/libwwrt.a | $(BIN) + cd $(BIN) && ./ww build \ + -I $$PWD/../../selfhost/cmd/wwc \ + $$PWD/../../selfhost/cmd/ww/main.ww + mv $(BIN)/main $@ + +# ---- runtime (libwwrt.a, assembled by our own 6a) ---------------------- +$(OBJ)/rt/%.o: rt/%.s $(BIN)/6a | $(OBJ)/rt + $(BIN)/6a -o $@ $< + +$(LIB)/libwwrt.a: $(RT_OBJ) | $(LIB) + $(AR) rcs $@ $(RT_OBJ) + +# ---- directories ------------------------------------------------------- +$(BIN) $(LIB) $(OBJ)/wwc $(OBJ)/ww $(OBJ)/wwdump $(OBJ)/6c $(OBJ)/6a $(OBJ)/6l $(OBJ)/rt: + mkdir -p $@ + +# ---- tests ------------------------------------------------------------- +# Each phase adds a $(BIN)/test_ target; the runner walks them. +TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ + $(BIN)/test_6c $(BIN)/test_6a $(BIN)/test_6l $(BIN)/test_arch \ + $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_stdlib $(BIN)/test_selfhost \ + $(BIN)/test_6a_ww $(BIN)/test_6l_ww $(BIN)/test_ww_ww + +$(BIN)/test_smoke: test/wwc/000_smoke.c $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc + +$(BIN)/test_lex: test/wwc/100_lex.c $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc + +$(BIN)/test_parse: test/wwc/200_parse.c $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc + +$(BIN)/test_check: test/wwc/300_check.c $(LIB)/libwwc.a | $(BIN) + $(CC) $(CFLAGS) $(INCS) -o $@ $< -L$(LIB) -lwwc + +$(BIN)/test_6c: test/wwc/400_6c.c $(BIN)/6c | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_6a: test/wwc/500_6a.c $(BIN)/6c $(BIN)/6a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_6l: test/wwc/600_6l.c $(BIN)/6c $(BIN)/6a $(BIN)/6l | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_arch: test/wwc/610_arch.c $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(OBJ)/rt/start.o | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_e2e: test/wwc/700_e2e.c $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(LIB)/libwwrt.a $(OBJ)/rt/start.o | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_ffi: test/wwc/800_ffi.c $(BIN)/6c | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_stdlib: test/wwc/900_stdlib.c $(BIN)/6c | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_selfhost: test/wwc/990_selfhost.c $(BIN)/6c $(BIN)/6a $(BIN)/6l \ + $(BIN)/ww $(BIN)/wwdump $(BIN)/wwdump_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_6a_ww: test/wwc/991_6a_ww.c $(BIN)/6a $(BIN)/6a_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_6l_ww: test/wwc/992_6l_ww.c $(BIN)/6l $(BIN)/6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +$(BIN)/test_ww_ww: test/wwc/993_ww_ww.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/6c $(BIN)/6a $(BIN)/6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + +test: all $(TESTS) + @WW=$(BIN)/ww BIN=$(BIN) sh test/run + +install: all + mkdir -p $(PREFIX)/bin $(PREFIX)/lib + cp $(BIN)/ww $(PREFIX)/bin/ + cp $(LIB)/libwwc.a $(PREFIX)/lib/ + +clean: + rm -rf $(OUT) + +# ---- bootstrap (phase 10) ---------------------------------------------- +# Three-stage self-host: Cstage (these C tools) → ww1 → ww2 → ww3 with +# cmp ww2 == ww3. Stage 1 is wwdump_ww (the ww-built frontend); stages +# 2 and 3 assemble with 6a_ww and link with 6l_ww — every tool below +# the driver is now in ww. The C `ww` driver still orchestrates the +# pipeline (porting it is the remaining phase-10 work). +BS = $(OUT)/bootstrap +bootstrap: all + @echo "=== stage 0 (Cstage): C-built tools ===" + @echo " $(BIN)/ww $(BIN)/6c $(BIN)/6a $(BIN)/6l" + @echo + @echo "=== stage 1: ww-built tools, used to drive stages 2 and 3 ===" + @ls -l $(BIN)/wwdump_ww $(BIN)/6a_ww $(BIN)/6l_ww $(BIN)/ww_ww + @rm -rf $(BS) && mkdir -p $(BS) + @echo + @echo "=== stage 2: ww2 = ww1 self-compiles main.combined.ww ===" + @$(BIN)/wwdump_ww -c selfhost/cmd/wwdump/main.combined.ww > $(BS)/ww2.s + @$(BIN)/6a_ww -o $(BS)/ww2.o $(BS)/ww2.s + @$(BIN)/6l_ww -o $(BS)/ww2 $(BS)/ww2.o $(LIB)/libwwrt.a + @ls -l $(BS)/ww2 + @echo + @echo "=== stage 3: ww3 = ww2 self-compiles main.combined.ww ===" + @$(BS)/ww2 -c selfhost/cmd/wwdump/main.combined.ww > $(BS)/ww3.s + @$(BIN)/6a_ww -o $(BS)/ww3.o $(BS)/ww3.s + @$(BIN)/6l_ww -o $(BS)/ww3 $(BS)/ww3.o $(LIB)/libwwrt.a + @ls -l $(BS)/ww3 + @echo + @echo "=== fixed-point gates ===" + @cmp $(BS)/ww2.s $(BS)/ww3.s && echo " ww2.s == ww3.s ✓" + @cmp $(BS)/ww2.o $(BS)/ww3.o && echo " ww2.o == ww3.o ✓ (byte-identical .o)" + @cmp $(BS)/ww2 $(BS)/ww3 && echo " ww2 == ww3 ✓ (byte-identical exe)" + @echo + @echo "BOOTSTRAP OK: ww-cgen + 6a_ww + 6l_ww reach a fixed point." + @echo + @echo "Phase 10 remaining work:" + @echo " - delete cmd/wwc/ cmd/6c/ cmd/6a/ cmd/6l/ cmd/ww/ in the final commit" + @echo + @echo "Already in selfhost/:" + @echo " - cmd/wwc/ : ww-cgen (lex/parse/check/cgen) — bootstrap fixed point" + @echo " - cmd/6a/ : ww-6a (lex/parse/asm/obj) — byte-identical to C 6a (test 991)" + @echo " - cmd/6l/ : ww-6l (linker w/ archive support) — byte-identical to C 6l (test 992)" + @echo " - cmd/ww/ : ww-driver (build/run/version) — byte-identical to C ww (test 993)" + +.PHONY: all test install clean bootstrap diff --git a/cmd/6a/a.h b/cmd/6a/a.h new file mode 100644 index 00000000..5e66d053 --- /dev/null +++ b/cmd/6a/a.h @@ -0,0 +1,107 @@ +/* + * a.h — 6a-private header. Modelled on Plan 9 cmd/6a/a.h, trimmed + * to the instruction subset that 6c emits. + * + * 6a is line-oriented and has no preprocessor: each non-blank, non- + * label line is one instruction. We read the whole file into a list + * of `Aprog`s, then encode and emit ELF64. + */ +#ifndef SIX_A_H +#define SIX_A_H + +#include "6.out.h" +#include +#include +#include + +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +typedef struct Aoperand Aoperand; +typedef struct Aprog Aprog; +typedef struct Asym Asym; +typedef struct Areloc Areloc; +typedef struct Asm Asm; + +struct Aoperand { + int type; /* D_NONE, D_AX..D_R15, D_CONST, D_INDIR, D_EXTERN, D_BRANCH */ + int reg; /* base register for D_INDIR */ + i64 offset; /* immediate or displacement */ + const char *sym; +}; + +struct Aprog { + int as; /* opcode (A_*) */ + Aoperand from; + Aoperand to; + int line; + const char *label; /* label preceding this prog, if any */ + Aprog *link; + /* for A_DATA: raw payload bytes interned by the parser */ + u8 *bytes; + u64 nbytes; +}; + +struct Asym { + const char *name; + int defined; /* 1 if we own its address */ + int is_text; /* if 1, address is in .text */ + int is_global; /* exported (TEXT) */ + u64 addr; /* offset within section if defined */ + int idx; /* ELF symtab index, filled at emit time */ + Asym *next; +}; + +struct Areloc { + u64 off; /* offset within .text where relocation lands */ + int kind; /* R_X86_64_PLT32 (4), R_X86_64_PC32 (2) */ + Asym *sym; + i64 addend; + Areloc *next; +}; + +struct Asm { + /* parser state */ + const char *file; + const char *src; + u64 srclen; + u64 pos; + int line; + + /* program list */ + Aprog *head, *tail; + + /* output text section */ + u8 *text; + u64 textcap, textlen; + + /* symbols */ + Asym *syms; + Areloc *relocs; + + int errs; +}; + +/* lex.c / parse.c */ +void a_init(Asm*, const char *file, const char *src, u64 len); +int a_parse(Asm*); + +/* asm.c */ +int a_encode(Asm*); + +/* obj.c */ +int a_emit_elf(Asm*, FILE *out); + +/* helpers */ +Asym *a_intern(Asm*, const char *name); +void a_emit_byte(Asm*, u8); +void a_emit_u32(Asm*, u32); +void a_addreloc(Asm*, u64 off, int kind, Asym *s, i64 add); + +#endif diff --git a/cmd/6a/asm.c b/cmd/6a/asm.c new file mode 100644 index 00000000..f88c1521 --- /dev/null +++ b/cmd/6a/asm.c @@ -0,0 +1,689 @@ +/* + * asm.c — encode the parsed Aprog list into amd64 machine bytes, + * appending to Asm.text. Relocations for CALL/branch targets that + * resolve to externals are queued in Asm.relocs. + * + * Encoding subset: the instructions cgen emits today. Operand shapes + * we accept: + * MOVQ $imm, reg — C7 /0 imm32 (REX.W) [imm fits in i32] + * MOVQ reg, reg — 89 /r (REX.W) + * MOVQ off(reg), reg — 8B /r (REX.W) + * MOVQ reg, off(reg) — 89 /r (REX.W) + * ADDQ/SUBQ/AND/OR/XOR — 01/29/21/09/31 /r (REX.W) [reg→reg] + * ADDQ $imm, reg — 81 /0 imm32 (REX.W) + * SUBQ $imm, reg — 81 /5 imm32 (REX.W) (likewise CMPQ) + * IMULQ reg, reg — 0F AF /r (REX.W) + * IDIVQ reg — F7 /7 (REX.W) + * DIVQ reg — F7 /6 (REX.W) (unsigned) + * NEGQ/NOTQ reg — F7 /3, F7 /2 (REX.W) + * SHLQ/SHRQ CL, reg — D3 /4, D3 /5 (REX.W) + * CMPQ reg, reg — 39 /r (REX.W) + * CMPQ $imm, reg — 81 /7 imm32 (REX.W) + * PUSHQ reg — 50+rd (REX.B for high) + * POPQ reg — 58+rd (REX.B for high) + * LEAQ name(SB), reg — 48 8D /r RIP-relative; reloc PC32 + * LEAQ off(reg), reg — 48 8D /r + * CALL name(SB) — E8 cd reloc PLT32 + * CALL reg — FF /2 (REX.W not strictly needed) + * RET — C3 + * JMP/Jcc label — E9 cd / 0F 8x cd rel32 to local label + * SYSCALL — 0F 05 + */ +#include "a.h" +#include +#include +#include + +void +a_emit_byte(Asm *a, u8 b) +{ + if (a->textlen + 1 > a->textcap) { + u64 nc = a->textcap ? a->textcap * 2 : 4096; + a->text = realloc(a->text, nc); + a->textcap = nc; + } + a->text[a->textlen++] = b; +} + +void +a_emit_u32(Asm *a, u32 v) +{ + a_emit_byte(a, (u8)(v & 0xff)); + a_emit_byte(a, (u8)((v >> 8) & 0xff)); + a_emit_byte(a, (u8)((v >> 16) & 0xff)); + a_emit_byte(a, (u8)((v >> 24) & 0xff)); +} + +void +a_addreloc(Asm *a, u64 off, int kind, Asym *s, i64 add) +{ + Areloc *r = calloc(1, sizeof *r); + r->off = off; + r->kind = kind; + r->sym = s; + r->addend = add; + r->next = a->relocs; + a->relocs = r; +} + +/* ------ register codes ------------------------------------------- */ + +/* low 3 bits of register encoding */ +static int +rcode(int r) +{ + switch (r) { + case D_AX: return 0; case D_CX: return 1; + case D_DX: return 2; case D_BX: return 3; + case D_SP: return 4; case D_BP: return 5; + case D_SI: return 6; case D_DI: return 7; + case D_R8: return 0; case D_R9: return 1; + case D_R10:return 2; case D_R11:return 3; + case D_R12:return 4; case D_R13:return 5; + case D_R14:return 6; case D_R15:return 7; + case D_X0: return 0; case D_X1: return 1; + case D_X2: return 2; case D_X3: return 3; + case D_X4: return 4; case D_X5: return 5; + case D_X6: return 6; case D_X7: return 7; + case D_X8: return 0; case D_X9: return 1; + case D_X10:return 2; case D_X11:return 3; + case D_X12:return 4; case D_X13:return 5; + case D_X14:return 6; case D_X15:return 7; + } + return 0; +} + +/* 1 if r needs the high bit (REX.R or REX.B) */ +static int +rhi(int r) +{ + if (r >= D_R8 && r <= D_R15) return 1; + if (r >= D_X8 && r <= D_X15) return 1; + return 0; +} + +static int +is_xmm(int r) +{ + return r >= D_X0 && r <= D_X15; +} + +/* ModR/M byte */ +static u8 +modrm(int mod, int reg, int rm) +{ + return (u8)(((mod & 3) << 6) | ((reg & 7) << 3) | (rm & 7)); +} + +/* emit REX with W=1 plus optional R/B for high regs */ +static void +emit_rex(Asm *a, int regbit, int rmbit, int w) +{ + u8 b = 0x40; + if (w) b |= 0x08; + if (regbit) b |= 0x04; + if (rmbit) b |= 0x01; + if (b != 0x40 || w) a_emit_byte(a, b); +} + +/* encode mod/disp for [base+disp]; returns 0 on ok. + * Special-cases SP (needs SIB) and BP (forces disp). + */ +static void +emit_modrm_mem(Asm *a, int reg_field, int base, i64 disp) +{ + int rm = rcode(base); + int mod; + int needsib = (rm == 4); /* SP requires SIB */ + int forced_disp = (rm == 5 && disp == 0); /* BP needs explicit disp8 */ + + if (disp == 0 && !forced_disp) mod = 0; + else if (disp >= -128 && disp <= 127) mod = 1; + else mod = 2; + + a_emit_byte(a, modrm(mod, reg_field, rm)); + if (needsib) + a_emit_byte(a, (u8)(0x24)); /* SIB: scale=0 idx=4(none) base=4 */ + if (mod == 1) + a_emit_byte(a, (u8)(disp & 0xff)); + else if (mod == 2) + a_emit_u32(a, (u32)disp); +} + +/* Plan 9 op order: src, dst. Generic two-reg encoding for ops that + * use the standard "reg, r/m" form (89 /r, 01 /r, etc.) — opcode + * implies the REX.W and the direction; we emit "src register goes + * into reg field, dst register into rm field". */ +static void +encode_rr(Asm *a, u8 opcode, int src, int dst) +{ + emit_rex(a, rhi(src), rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm(3, rcode(src), rcode(dst))); +} + +/* MOVQ src reg → mem(base, disp). opcode = 0x89 */ +static void +encode_rm(Asm *a, u8 opcode, int src_reg, int base, i64 disp) +{ + emit_rex(a, rhi(src_reg), rhi(base), 1); + a_emit_byte(a, opcode); + emit_modrm_mem(a, rcode(src_reg), base, disp); +} + +/* MOVQ mem(base, disp) → reg. opcode = 0x8B */ +static void +encode_mr(Asm *a, u8 opcode, int dst_reg, int base, i64 disp) +{ + emit_rex(a, rhi(dst_reg), rhi(base), 1); + a_emit_byte(a, opcode); + emit_modrm_mem(a, rcode(dst_reg), base, disp); +} + +/* OPCODE /n imm32 reg form. E.g. ADDQ $imm, reg */ +static void +encode_ri_imm32(Asm *a, u8 opcode, int subop, int dst, i32 imm) +{ + emit_rex(a, 0, rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm(3, subop, rcode(dst))); + a_emit_u32(a, (u32)imm); +} + +/* unary-on-reg: F7 /n reg, etc. */ +static void +encode_unary(Asm *a, u8 opcode, int subop, int dst) +{ + emit_rex(a, 0, rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm(3, subop, rcode(dst))); +} + +/* SSE2 helpers. Plan 9 syntax: source first, destination second. + * For ADDSD-style ops we put dst in the reg field, src in r/m. */ +static void +sse_rr(Asm *a, u8 prefix, u8 op2, int reg_op, int rm_op) +{ + if (prefix) a_emit_byte(a, prefix); + emit_rex(a, rhi(reg_op), rhi(rm_op), 0); + a_emit_byte(a, 0x0F); + a_emit_byte(a, op2); + a_emit_byte(a, modrm(3, rcode(reg_op), rcode(rm_op))); +} + +static void +sse_mr_load(Asm *a, u8 prefix, u8 op2, int reg_op, int base, i64 disp) +{ + if (prefix) a_emit_byte(a, prefix); + emit_rex(a, rhi(reg_op), rhi(base), 0); + a_emit_byte(a, 0x0F); + a_emit_byte(a, op2); + emit_modrm_mem(a, rcode(reg_op), base, disp); +} + +/* like sse_mr_load but encoded with REX.W (used by CVTTSD2SI / CVTSI2SD + * which target/source 64-bit integer regs) */ +static void +sse_rr_w(Asm *a, u8 prefix, u8 op2, int reg_op, int rm_op) +{ + if (prefix) a_emit_byte(a, prefix); + emit_rex(a, rhi(reg_op), rhi(rm_op), 1); + a_emit_byte(a, 0x0F); + a_emit_byte(a, op2); + a_emit_byte(a, modrm(3, rcode(reg_op), rcode(rm_op))); +} + +/* ------ second-pass helper: resolve labels to addresses ---------- */ + +static u64 +resolve_label(Asm *a, const char *name) +{ + for (Asym *s = a->syms; s; s = s->next) + if (s->defined && strcmp(s->name, name) == 0) + return s->addr; + return 0; +} + +static int +label_defined(Asm *a, const char *name) +{ + for (Asym *s = a->syms; s; s = s->next) + if (s->defined && strcmp(s->name, name) == 0) return 1; + return 0; +} + +/* ------ first pass: encode ---------------------------------------- */ + +/* For local labels, we record a "fixup" — an offset in .text that + * needs to be patched once the label is resolved at end of pass. */ +typedef struct Fixup Fixup; +struct Fixup { + u64 off; /* where the rel32 lands */ + const char *label; + Fixup *next; +}; +static Fixup *fixups; + +static void +add_fixup(u64 off, const char *label) +{ + Fixup *f = calloc(1, sizeof *f); + f->off = off; + f->label = strdup(label); + f->next = fixups; + fixups = f; +} + +int +a_encode(Asm *a) +{ + fixups = NULL; + const char *cur_text = NULL; /* current TEXT name */ + (void)cur_text; + for (Aprog *p = a->head; p; p = p->link) { + /* Define any pending label at the current PC */ + if (p->label) { + Asym *s = a_intern(a, p->label); + s->defined = 1; + s->is_text = 1; + s->addr = a->textlen; + } + switch (p->as) { + case A_NOP: + break; + case A_TEXT: { + Asym *s = a_intern(a, p->to.sym); + s->defined = 1; + s->is_text = 1; + s->is_global = 1; + s->addr = a->textlen; + cur_text = p->to.sym; + break; + } + case A_DATA: { + Asym *s = a_intern(a, p->to.sym); + s->defined = 1; + s->is_text = 1; /* we lay it out at the end of .text */ + s->is_global = 1; + s->addr = a->textlen; + for (u64 i = 0; i < p->nbytes; i++) + a_emit_byte(a, p->bytes[i]); + break; + } + case A_RET: + a_emit_byte(a, 0xC3); + break; + case A_SYSCALL: + a_emit_byte(a, 0x0F); a_emit_byte(a, 0x05); + break; + case A_PUSHQ: + if (rhi(p->to.type)) a_emit_byte(a, 0x41); + a_emit_byte(a, (u8)(0x50 + rcode(p->to.type))); + break; + case A_POPQ: + if (rhi(p->to.type)) a_emit_byte(a, 0x41); + a_emit_byte(a, (u8)(0x58 + rcode(p->to.type))); + break; + case A_NEGQ: + encode_unary(a, 0xF7, 3, p->to.type); break; + case A_NOTQ: + encode_unary(a, 0xF7, 2, p->to.type); break; + case A_IDIVQ: + encode_unary(a, 0xF7, 7, p->to.type); break; + case A_DIVQ: + /* unsigned divide; shares the F7 group with IDIVQ but + * uses /6 instead of /7. */ + encode_unary(a, 0xF7, 6, p->to.type); break; + + case A_MOVQ: + if (p->from.type == D_CONST && p->to.type >= D_AX && p->to.type <= D_R15) { + i64 v = p->from.offset; + if (v >= -2147483648LL && v <= 2147483647LL) { + /* C7 /0 imm32, sign-extended */ + encode_ri_imm32(a, 0xC7, 0, p->to.type, (i32)v); + } else { + /* movabs r64, imm64: REX.W B8+rd imm64 */ + emit_rex(a, 0, rhi(p->to.type), 1); + a_emit_byte(a, (u8)(0xB8 + rcode(p->to.type))); + for (int k = 0; k < 8; k++) + a_emit_byte(a, (u8)((v >> (k * 8)) & 0xff)); + } + } else if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type >= D_AX && p->to.type <= D_R15) { + encode_rr(a, 0x89, p->from.type, p->to.type); + } else if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) { + encode_mr(a, 0x8B, p->to.type, p->from.reg, p->from.offset); + } else if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type == D_INDIR) { + encode_rm(a, 0x89, p->from.type, p->to.reg, p->to.offset); + } else if (p->from.type == D_CONST + && p->to.type == D_INDIR) { + /* MOVQ $imm32, r/m64 — C7 /0 (REX.W) imm32. + * The CPU sign-extends imm32 into 64 bits, so + * any value within i32 range works. */ + emit_rex(a, 0, rhi(p->to.reg), 1); + a_emit_byte(a, 0xC7); + emit_modrm_mem(a, 0, p->to.reg, p->to.offset); + a_emit_u32(a, (u32)(i32)p->from.offset); + } else if (p->from.type == D_EXTERN + && p->to.type >= D_AX && p->to.type <= D_R15) { + /* RIP-relative load: 48 8B /r mod=00 rm=5 disp32 */ + emit_rex(a, rhi(p->to.type), 0, 1); + a_emit_byte(a, 0x8B); + a_emit_byte(a, modrm(0, rcode(p->to.type), 5)); + u64 reloff = a->textlen; + a_emit_u32(a, 0); + Asym *s = a_intern(a, p->from.sym); + a_addreloc(a, reloff, 2, s, -4); + } else if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type == D_EXTERN) { + /* RIP-relative store: 48 89 /r mod=00 rm=5 disp32 */ + emit_rex(a, rhi(p->from.type), 0, 1); + a_emit_byte(a, 0x89); + a_emit_byte(a, modrm(0, rcode(p->from.type), 5)); + u64 reloff = a->textlen; + a_emit_u32(a, 0); + Asym *s = a_intern(a, p->to.sym); + a_addreloc(a, reloff, 2, s, -4); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVQ shape\n", p->line); + a->errs++; + } + break; + case A_MOVB: + /* MOV r/m8, r8 — 88 /r. No REX.W. We always emit REX + * to allow access to SIL/DIL/BPL/SPL. */ + if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type == D_INDIR) { + emit_rex(a, rhi(p->from.type), rhi(p->to.reg), 0); + a_emit_byte(a, 0x88); + emit_modrm_mem(a, rcode(p->from.type), + p->to.reg, p->to.offset); + } else if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) { + emit_rex(a, rhi(p->to.type), rhi(p->from.reg), 0); + a_emit_byte(a, 0x8A); /* MOV r8, r/m8 */ + emit_modrm_mem(a, rcode(p->to.type), + p->from.reg, p->from.offset); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVB shape\n", p->line); + a->errs++; + } + break; + case A_MOVZBQ: + /* MOVZX r64, r/m8 — 0F B6 /r with REX.W */ + if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) { + emit_rex(a, rhi(p->to.type), rhi(p->from.reg), 1); + a_emit_byte(a, 0x0F); + a_emit_byte(a, 0xB6); + emit_modrm_mem(a, rcode(p->to.type), + p->from.reg, p->from.offset); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVZBQ shape\n", p->line); + a->errs++; + } + break; + case A_MOVL: + /* MOV r/m32, r32 (89 /r) and MOV r32, r/m32 (8B /r), + * both without REX.W. The CPU zero-extends 32-bit ops + * into the 64-bit reg, so reads of u32 fields are safe. + * Sign-extension lives in MOVSXD. */ + if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type == D_INDIR) { + emit_rex(a, rhi(p->from.type), rhi(p->to.reg), 0); + a_emit_byte(a, 0x89); + emit_modrm_mem(a, rcode(p->from.type), + p->to.reg, p->to.offset); + } else if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) { + emit_rex(a, rhi(p->to.type), rhi(p->from.reg), 0); + a_emit_byte(a, 0x8B); + emit_modrm_mem(a, rcode(p->to.type), + p->from.reg, p->from.offset); + } else if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type >= D_AX && p->to.type <= D_R15) { + emit_rex(a, rhi(p->from.type), rhi(p->to.type), 0); + a_emit_byte(a, 0x89); + a_emit_byte(a, modrm(3, + rcode(p->from.type), rcode(p->to.type))); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVL shape\n", p->line); + a->errs++; + } + break; + case A_MOVSXD: + /* MOVSXD r64, r/m32 — 63 /r with REX.W */ + if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) { + emit_rex(a, rhi(p->to.type), rhi(p->from.reg), 1); + a_emit_byte(a, 0x63); + emit_modrm_mem(a, rcode(p->to.type), + p->from.reg, p->from.offset); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVSXD shape\n", p->line); + a->errs++; + } + break; + case A_MOVSD: + /* xmm←mem (load): F2 0F 10 /r */ + /* xmm←xmm: F2 0F 10 /r */ + /* mem←xmm (store):F2 0F 11 /r */ + if (is_xmm(p->from.type) && is_xmm(p->to.type)) { + sse_rr(a, 0xF2, 0x10, p->to.type, p->from.type); + } else if (p->from.type == D_INDIR && is_xmm(p->to.type)) { + sse_mr_load(a, 0xF2, 0x10, p->to.type, + p->from.reg, p->from.offset); + } else if (is_xmm(p->from.type) && p->to.type == D_INDIR) { + sse_mr_load(a, 0xF2, 0x11, p->from.type, + p->to.reg, p->to.offset); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVSD shape\n", p->line); + a->errs++; + } + break; + case A_ADDSD: + sse_rr(a, 0xF2, 0x58, p->to.type, p->from.type); + break; + case A_SUBSD: + sse_rr(a, 0xF2, 0x5C, p->to.type, p->from.type); + break; + case A_MULSD: + sse_rr(a, 0xF2, 0x59, p->to.type, p->from.type); + break; + case A_DIVSD: + sse_rr(a, 0xF2, 0x5E, p->to.type, p->from.type); + break; + case A_UCOMISD: + sse_rr(a, 0x66, 0x2E, p->to.type, p->from.type); + break; + case A_CVTTSD2SI: + /* int_reg ← xmm: F2 REX.W 0F 2C /r ; reg=int rm=xmm */ + sse_rr_w(a, 0xF2, 0x2C, p->to.type, p->from.type); + break; + case A_CVTSI2SD: + /* xmm ← int_reg: F2 REX.W 0F 2A /r ; reg=xmm rm=int */ + sse_rr_w(a, 0xF2, 0x2A, p->to.type, p->from.type); + break; + case A_MOVSS: + if (is_xmm(p->from.type) && is_xmm(p->to.type)) { + sse_rr(a, 0xF3, 0x10, p->to.type, p->from.type); + } else if (p->from.type == D_INDIR && is_xmm(p->to.type)) { + sse_mr_load(a, 0xF3, 0x10, p->to.type, + p->from.reg, p->from.offset); + } else if (is_xmm(p->from.type) && p->to.type == D_INDIR) { + sse_mr_load(a, 0xF3, 0x11, p->from.type, + p->to.reg, p->to.offset); + } else { + fprintf(stderr, "6a: line %d: unsupported MOVSS shape\n", p->line); + a->errs++; + } + break; + case A_ADDSS: + sse_rr(a, 0xF3, 0x58, p->to.type, p->from.type); break; + case A_SUBSS: + sse_rr(a, 0xF3, 0x5C, p->to.type, p->from.type); break; + case A_MULSS: + sse_rr(a, 0xF3, 0x59, p->to.type, p->from.type); break; + case A_DIVSS: + sse_rr(a, 0xF3, 0x5E, p->to.type, p->from.type); break; + case A_UCOMISS: + sse_rr(a, 0x00, 0x2E, p->to.type, p->from.type); break; + case A_CVTTSS2SI: + sse_rr_w(a, 0xF3, 0x2C, p->to.type, p->from.type); break; + case A_CVTSI2SS: + sse_rr_w(a, 0xF3, 0x2A, p->to.type, p->from.type); break; + case A_CVTSD2SS: + /* xmm←xmm: F2 0F 5A /r ; reg=dst rm=src */ + sse_rr(a, 0xF2, 0x5A, p->to.type, p->from.type); break; + case A_CVTSS2SD: + sse_rr(a, 0xF3, 0x5A, p->to.type, p->from.type); break; + case A_ADDQ: + if (p->from.type == D_CONST + && p->to.type >= D_AX && p->to.type <= D_R15) + encode_ri_imm32(a, 0x81, 0, p->to.type, (i32)p->from.offset); + else if (p->from.type == D_CONST && p->to.type == D_INDIR) { + /* ADD r/m64, imm32 — 81 /0 (REX.W) */ + emit_rex(a, 0, rhi(p->to.reg), 1); + a_emit_byte(a, 0x81); + emit_modrm_mem(a, 0, p->to.reg, p->to.offset); + a_emit_u32(a, (u32)(i32)p->from.offset); + } else if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type == D_INDIR) + encode_rm(a, 0x01, p->from.type, p->to.reg, p->to.offset); + else if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) + encode_mr(a, 0x03, p->to.type, p->from.reg, p->from.offset); + else + encode_rr(a, 0x01, p->from.type, p->to.type); + break; + case A_SUBQ: + if (p->from.type == D_CONST + && p->to.type >= D_AX && p->to.type <= D_R15) + encode_ri_imm32(a, 0x81, 5, p->to.type, (i32)p->from.offset); + else if (p->from.type == D_CONST && p->to.type == D_INDIR) { + emit_rex(a, 0, rhi(p->to.reg), 1); + a_emit_byte(a, 0x81); + emit_modrm_mem(a, 5, p->to.reg, p->to.offset); + a_emit_u32(a, (u32)(i32)p->from.offset); + } else if (p->from.type >= D_AX && p->from.type <= D_R15 + && p->to.type == D_INDIR) + encode_rm(a, 0x29, p->from.type, p->to.reg, p->to.offset); + else if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) + encode_mr(a, 0x2B, p->to.type, p->from.reg, p->from.offset); + else + encode_rr(a, 0x29, p->from.type, p->to.type); + break; + case A_ANDQ: encode_rr(a, 0x21, p->from.type, p->to.type); break; + case A_ORQ: encode_rr(a, 0x09, p->from.type, p->to.type); break; + case A_XORQ: + if (p->from.type == D_CONST + && p->to.type >= D_AX && p->to.type <= D_R15) + encode_ri_imm32(a, 0x81, 6, p->to.type, (i32)p->from.offset); + else + encode_rr(a, 0x31, p->from.type, p->to.type); + break; + case A_IMULQ: + emit_rex(a, rhi(p->to.type), rhi(p->from.type), 1); + a_emit_byte(a, 0x0F); a_emit_byte(a, 0xAF); + a_emit_byte(a, modrm(3, rcode(p->to.type), rcode(p->from.type))); + break; + case A_SHLQ: + encode_unary(a, 0xD3, 4, p->to.type); break; + case A_SHRQ: + encode_unary(a, 0xD3, 5, p->to.type); break; + case A_CMPQ: + if (p->from.type == D_CONST && p->to.type >= D_AX && p->to.type <= D_R15) + encode_ri_imm32(a, 0x81, 7, p->to.type, (i32)p->from.offset); + else + encode_rr(a, 0x39, p->from.type, p->to.type); + break; + case A_LEAQ: + if (p->from.type == D_INDIR + && p->to.type >= D_AX && p->to.type <= D_R15) { + encode_mr(a, 0x8D, p->to.type, p->from.reg, p->from.offset); + } else if (p->from.type == D_EXTERN + && p->to.type >= D_AX && p->to.type <= D_R15) { + /* RIP-relative: 48 8D /r mod=00 rm=5 disp32 */ + emit_rex(a, rhi(p->to.type), 0, 1); + a_emit_byte(a, 0x8D); + a_emit_byte(a, modrm(0, rcode(p->to.type), 5)); + u64 reloff = a->textlen; + a_emit_u32(a, 0); + Asym *s = a_intern(a, p->from.sym); + /* R_X86_64_PC32 (2) with addend -4 */ + a_addreloc(a, reloff, 2, s, -4); + } + break; + case A_CALL: + if (p->to.type == D_EXTERN) { + a_emit_byte(a, 0xE8); + u64 reloff = a->textlen; + a_emit_u32(a, 0); + Asym *s = a_intern(a, p->to.sym); + /* R_X86_64_PLT32 (4); addend -4 */ + a_addreloc(a, reloff, 4, s, -4); + } else if (p->to.type == D_BRANCH) { + /* local call to a label */ + a_emit_byte(a, 0xE8); + add_fixup(a->textlen, p->to.sym); + a_emit_u32(a, 0); + } else if (p->to.type >= D_AX && p->to.type <= D_R15) { + if (rhi(p->to.type)) a_emit_byte(a, 0x41); + a_emit_byte(a, 0xFF); + a_emit_byte(a, modrm(3, 2, rcode(p->to.type))); + } + break; + case A_JMP: + a_emit_byte(a, 0xE9); + add_fixup(a->textlen, p->to.sym); + a_emit_u32(a, 0); + break; + case A_JE: case A_JNE: case A_JL: case A_JLE: + case A_JG: case A_JGE: case A_JB: case A_JBE: + case A_JA: case A_JAE: case A_JZ: case A_JNZ: { + u8 cc = 0; + switch (p->as) { + case A_JE: case A_JZ: cc = 0x84; break; + case A_JNE: case A_JNZ: cc = 0x85; break; + case A_JL: cc = 0x8C; break; + case A_JLE: cc = 0x8E; break; + case A_JG: cc = 0x8F; break; + case A_JGE: cc = 0x8D; break; + case A_JB: cc = 0x82; break; + case A_JBE: cc = 0x86; break; + case A_JA: cc = 0x87; break; + case A_JAE: cc = 0x83; break; + default: break; + } + a_emit_byte(a, 0x0F); + a_emit_byte(a, cc); + add_fixup(a->textlen, p->to.sym); + a_emit_u32(a, 0); + break; + } + default: + fprintf(stderr, "6a: unsupported opcode %d on line %d\n", p->as, p->line); + a->errs++; + } + } + + /* second pass: patch fixups */ + for (Fixup *f = fixups; f; f = f->next) { + if (!label_defined(a, f->label)) { + fprintf(stderr, "6a: undefined label '%s'\n", f->label); + a->errs++; + continue; + } + u64 target = resolve_label(a, f->label); + i64 rel = (i64)target - ((i64)f->off + 4); + i32 rel32 = (i32)rel; + a->text[f->off + 0] = (u8)(rel32 & 0xff); + a->text[f->off + 1] = (u8)((rel32 >> 8) & 0xff); + a->text[f->off + 2] = (u8)((rel32 >> 16) & 0xff); + a->text[f->off + 3] = (u8)((rel32 >> 24) & 0xff); + } + return a->errs; +} diff --git a/cmd/6a/lex.c b/cmd/6a/lex.c new file mode 100644 index 00000000..ec9f7fdf --- /dev/null +++ b/cmd/6a/lex.c @@ -0,0 +1,31 @@ +/* + * lex.c — character-level helpers for 6a's line-oriented parser. + * The parser itself lives in parse.c; here we keep the tokenisers + * for identifiers and numbers so parse.c stays focused on syntax. + */ +#include "a.h" +#include +#include +#include + +int +a_isidstart(int c) +{ + return c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} + +int +a_isidcont(int c) +{ + return a_isidstart(c) || (c >= '0' && c <= '9') || c == '.'; +} + +i64 +a_parsenum(const char *s, char **end) +{ + /* Let strtoll handle the sign itself: hand-stripping '-' then + * negating the result fails for LLONG_MIN because the positive + * magnitude (2^63) doesn't fit in long long, strtoll clamps to + * LLONG_MAX, and the negation lands one short. */ + return (i64)strtoll(s, end, 0); +} diff --git a/cmd/6a/main.c b/cmd/6a/main.c new file mode 100644 index 00000000..34bdcbb1 --- /dev/null +++ b/cmd/6a/main.c @@ -0,0 +1,59 @@ +/* + * 6a — amd64 assembler driver. Read .s, parse, encode, emit ELF .o. + */ +#include "a.h" +#include +#include +#include + +static int +slurp(const char *path, char **buf, u64 *len) +{ + FILE *f = fopen(path, "rb"); + if (f == NULL) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = 0; + fclose(f); + *buf = b; + *len = (u64)n; + return 0; +} + +int +main(int argc, char **argv) +{ + const char *src = NULL; + const char *out = NULL; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) out = argv[++i]; + else if (argv[i][0] == '-') { + fprintf(stderr, "6a: unknown flag %s\n", argv[i]); return 2; + } else if (src == NULL) src = argv[i]; + else { fprintf(stderr, "6a: only one input\n"); return 2; } + } + if (src == NULL || out == NULL) { + fputs("usage: 6a -o file.o file.s\n", stderr); + return 2; + } + char *buf; + u64 len; + if (slurp(src, &buf, &len) < 0) { + fprintf(stderr, "6a: cannot read %s\n", src); + return 1; + } + Asm a; + a_init(&a, src, buf, len); + if (a_parse(&a) != 0) return 1; + if (a_encode(&a) != 0) return 1; + FILE *f = fopen(out, "wb"); + if (f == NULL) { fprintf(stderr, "6a: cannot open %s\n", out); return 1; } + int rc = a_emit_elf(&a, f); + fclose(f); + free(buf); + return rc; +} diff --git a/cmd/6a/obj.c b/cmd/6a/obj.c new file mode 100644 index 00000000..351941f6 --- /dev/null +++ b/cmd/6a/obj.c @@ -0,0 +1,242 @@ +/* + * obj.c — emit a tiny ELF64 relocatable object. + * + * Layout (in file order): + * [0] ELF header + * [1] Section .text (program bytes) + * [2] Section .rela.text (relocations) + * [3] Section .symtab + * [4] Section .strtab + * [5] Section .shstrtab + * [6] Section header table + * + * Symtab indices: 0 = STN_UNDEF, 1 = file (skipped), 2.. = our syms. + * For simplicity we emit GLOBAL symbols only (no LOCAL ordering rules + * to worry about). + */ +#include "a.h" +#include +#include +#include + +/* ELF constants */ +#define ELFMAG "\x7f""ELF" +#define ELFCLASS64 2 +#define ELFDATA2LSB 1 +#define EV_CURRENT 1 +#define ET_REL 1 +#define EM_X86_64 62 + +#define SHT_NULL 0 +#define SHT_PROGBITS 1 +#define SHT_SYMTAB 2 +#define SHT_STRTAB 3 +#define SHT_RELA 4 + +#define SHF_ALLOC 0x2 +#define SHF_EXECINSTR 0x4 +#define SHF_INFO_LINK 0x40 + +#define STB_LOCAL 0 +#define STB_GLOBAL 1 +#define STT_NOTYPE 0 +#define STT_FUNC 2 +#define ELF64_ST_INFO(b,t) (((b) << 4) + ((t) & 0xf)) + +#define R_X86_64_PC32 2 +#define R_X86_64_PLT32 4 +#define ELF64_R_INFO(s,t) (((u64)(s) << 32) | ((u64)(t) & 0xffffffff)) + +/* growable byte buffer */ +typedef struct Buf Buf; +struct Buf { u8 *p; size_t n, cap; }; + +static void +bput(Buf *b, const void *src, size_t n) +{ + if (b->n + n > b->cap) { + size_t nc = b->cap ? b->cap * 2 : 256; + while (nc < b->n + n) nc *= 2; + b->p = realloc(b->p, nc); + b->cap = nc; + } + memcpy(b->p + b->n, src, n); + b->n += n; +} + +static u32 stput(Buf *st, const char *s) { + u32 off = (u32)st->n; + bput(st, s, strlen(s) + 1); + return off; +} + +#pragma pack(push, 1) +typedef struct { + u8 e_ident[16]; + u16 e_type, e_machine; + u32 e_version; + u64 e_entry, e_phoff, e_shoff; + u32 e_flags; + u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx; +} Ehdr; + +typedef struct { + u32 sh_name, sh_type; + u64 sh_flags, sh_addr, sh_offset, sh_size; + u32 sh_link, sh_info; + u64 sh_addralign, sh_entsize; +} Shdr; + +typedef struct { + u32 st_name; + u8 st_info, st_other; + u16 st_shndx; + u64 st_value, st_size; +} Sym64; + +typedef struct { + u64 r_offset; + u64 r_info; + i64 r_addend; +} Rela64; +#pragma pack(pop) + +int +a_emit_elf(Asm *a, FILE *f) +{ + Buf shstr = {0}, str = {0}, sym = {0}, rela = {0}; + stput(&shstr, ""); /* idx 0 = empty */ + stput(&str, ""); + + /* Section name offsets */ + u32 shn_text = stput(&shstr, ".text"); + u32 shn_rela = stput(&shstr, ".rela.text"); + u32 shn_symtab = stput(&shstr, ".symtab"); + u32 shn_strtab = stput(&shstr, ".strtab"); + u32 shn_shstrtab = stput(&shstr, ".shstrtab"); + + /* Symbol 0 — STN_UNDEF */ + { + Sym64 z = {0}; + bput(&sym, &z, sizeof z); + } + + /* Section indices: 1=.text, 2=.rela.text, 3=.symtab, 4=.strtab, 5=.shstrtab */ + const u16 SH_TEXT = 1; + + /* Build symbols (defined = global; undefined = global UND) */ + int idx = 1; + for (Asym *s = a->syms; s; s = s->next) { + Sym64 e = {0}; + e.st_name = stput(&str, s->name); + if (s->defined) { + e.st_info = ELF64_ST_INFO(STB_GLOBAL, STT_FUNC); + e.st_shndx = SH_TEXT; + e.st_value = s->addr; + e.st_size = 0; + } else { + e.st_info = ELF64_ST_INFO(STB_GLOBAL, STT_NOTYPE); + e.st_shndx = 0; + } + bput(&sym, &e, sizeof e); + s->idx = idx++; + } + + /* Build relocations */ + for (Areloc *r = a->relocs; r; r = r->next) { + Rela64 re; + re.r_offset = r->off; + re.r_info = ELF64_R_INFO((u64)r->sym->idx, (u64)r->kind); + re.r_addend = r->addend; + bput(&rela, &re, sizeof re); + } + + /* Layout offsets in the file */ + u64 off = sizeof(Ehdr); + u64 off_text = off; off += a->textlen; + u64 off_rela = off; off += rela.n; + u64 off_sym = off; off += sym.n; + u64 off_str = off; off += str.n; + u64 off_shstr= off; off += shstr.n; + /* align to 8 */ + while (off % 8) off++; + u64 off_shdr = off; + const int NSECT = 6; /* null + 5 real */ + + Ehdr eh = {0}; + memcpy(eh.e_ident, "\x7f""ELF", 4); + eh.e_ident[4] = ELFCLASS64; + eh.e_ident[5] = ELFDATA2LSB; + eh.e_ident[6] = EV_CURRENT; + eh.e_type = ET_REL; + eh.e_machine = EM_X86_64; + eh.e_version = EV_CURRENT; + eh.e_shoff = off_shdr; + eh.e_ehsize = sizeof(Ehdr); + eh.e_shentsize = sizeof(Shdr); + eh.e_shnum = NSECT; + eh.e_shstrndx = 5; + + fwrite(&eh, 1, sizeof eh, f); + if (a->textlen) fwrite(a->text, 1, a->textlen, f); + fwrite(rela.p, 1, rela.n, f); + fwrite(sym.p, 1, sym.n, f); + fwrite(str.p, 1, str.n, f); + fwrite(shstr.p, 1, shstr.n, f); + while ((u64)ftell(f) % 8) fputc(0, f); + + /* section header table */ + Shdr sh = {0}; + fwrite(&sh, 1, sizeof sh, f); /* SHT_NULL */ + + memset(&sh, 0, sizeof sh); + sh.sh_name = shn_text; + sh.sh_type = SHT_PROGBITS; + sh.sh_flags = SHF_ALLOC | SHF_EXECINSTR; + sh.sh_offset = off_text; + sh.sh_size = a->textlen; + sh.sh_addralign = 1; + fwrite(&sh, 1, sizeof sh, f); + + memset(&sh, 0, sizeof sh); + sh.sh_name = shn_rela; + sh.sh_type = SHT_RELA; + sh.sh_flags = SHF_INFO_LINK; + sh.sh_offset = off_rela; + sh.sh_size = rela.n; + sh.sh_link = 3; /* symtab */ + sh.sh_info = 1; /* applies to .text */ + sh.sh_addralign = 8; + sh.sh_entsize = sizeof(Rela64); + fwrite(&sh, 1, sizeof sh, f); + + memset(&sh, 0, sizeof sh); + sh.sh_name = shn_symtab; + sh.sh_type = SHT_SYMTAB; + sh.sh_offset = off_sym; + sh.sh_size = sym.n; + sh.sh_link = 4; /* strtab */ + sh.sh_info = 1; /* one local: STN_UNDEF */ + sh.sh_addralign = 8; + sh.sh_entsize = sizeof(Sym64); + fwrite(&sh, 1, sizeof sh, f); + + memset(&sh, 0, sizeof sh); + sh.sh_name = shn_strtab; + sh.sh_type = SHT_STRTAB; + sh.sh_offset = off_str; + sh.sh_size = str.n; + sh.sh_addralign = 1; + fwrite(&sh, 1, sizeof sh, f); + + memset(&sh, 0, sizeof sh); + sh.sh_name = shn_shstrtab; + sh.sh_type = SHT_STRTAB; + sh.sh_offset = off_shstr; + sh.sh_size = shstr.n; + sh.sh_addralign = 1; + fwrite(&sh, 1, sizeof sh, f); + + free(shstr.p); free(str.p); free(sym.p); free(rela.p); + return 0; +} diff --git a/cmd/6a/parse.c b/cmd/6a/parse.c new file mode 100644 index 00000000..f3786e75 --- /dev/null +++ b/cmd/6a/parse.c @@ -0,0 +1,405 @@ +/* + * parse.c — line-oriented parser for the asm subset emitted by 6c. + * + * Grammar: + * line := blank | comment | label | text | instr + * blank := /^\s*$/ + * comment := /^\s*\/\/.*$/ + * label := /^IDENT:$/ + * text := TEXT name,$framesize + * instr := \tMNEM\t[OP1[, OP2]] + * OP := $NUM | REG | NUM(REG) | (REG) | name(SB) | label + * + * Identifiers may include '.' and '_'. Whitespace inside operands + * (between '$' and a number, etc.) is rejected for sanity. + */ +#include "a.h" +#include +#include +#include +#include + +extern int a_isidstart(int); +extern int a_isidcont(int); +extern i64 a_parsenum(const char *, char **); + +void +a_init(Asm *a, const char *file, const char *src, u64 len) +{ + memset(a, 0, sizeof *a); + a->file = file; + a->src = src; + a->srclen = len; + a->line = 1; +} + +static void +err(Asm *a, const char *msg) +{ + fprintf(stderr, "6a: %s:%d: %s\n", a->file, a->line, msg); + a->errs++; +} + +Asym * +a_intern(Asm *a, const char *name) +{ + for (Asym *s = a->syms; s; s = s->next) + if (strcmp(s->name, name) == 0) return s; + Asym *s = calloc(1, sizeof *s); + s->name = strdup(name); + s->next = a->syms; + a->syms = s; + return s; +} + +/* ------------------------------------------------------------------ */ +/* line iterator: returns the next line as a NUL-terminated buffer in + * line/llen pointers, advances pos. Returns 0 on EOF. + */ +static int +nextline(Asm *a, char **line, size_t *llen, char *buf, size_t bufsz) +{ + if (a->pos >= a->srclen) return 0; + size_t n = 0; + while (a->pos < a->srclen && a->src[a->pos] != '\n' && n + 1 < bufsz) + buf[n++] = a->src[a->pos++]; + buf[n] = '\0'; + if (a->pos < a->srclen && a->src[a->pos] == '\n') a->pos++; + *line = buf; + *llen = n; + return 1; +} + +/* skip leading whitespace */ +static const char * +skipws(const char *p) +{ + while (*p == ' ' || *p == '\t') p++; + return p; +} + +static int +opcode_lookup(const char *m) +{ + struct { const char *m; int op; } tab[] = { + { "MOVQ", A_MOVQ }, { "MOVL", A_MOVL }, + { "MOVB", A_MOVB }, { "MOVZBQ", A_MOVZBQ }, + { "MOVSXD", A_MOVSXD }, + { "MOVSD", A_MOVSD }, + { "ADDSD", A_ADDSD },{ "SUBSD", A_SUBSD }, + { "MULSD", A_MULSD },{ "DIVSD", A_DIVSD }, + { "UCOMISD", A_UCOMISD }, + { "CVTTSD2SI", A_CVTTSD2SI }, + { "CVTSI2SD", A_CVTSI2SD }, + { "MOVSS", A_MOVSS }, + { "ADDSS", A_ADDSS },{ "SUBSS", A_SUBSS }, + { "MULSS", A_MULSS },{ "DIVSS", A_DIVSS }, + { "UCOMISS", A_UCOMISS }, + { "CVTTSS2SI", A_CVTTSS2SI }, + { "CVTSI2SS", A_CVTSI2SS }, + { "CVTSD2SS", A_CVTSD2SS }, + { "CVTSS2SD", A_CVTSS2SD }, + { "ADDQ", A_ADDQ }, { "SUBQ", A_SUBQ }, + { "IMULQ",A_IMULQ},{ "IDIVQ",A_IDIVQ}, + { "DIVQ", A_DIVQ }, + { "NEGQ", A_NEGQ },{ "NOTQ", A_NOTQ }, + { "ANDQ", A_ANDQ },{ "ORQ", A_ORQ }, + { "XORQ", A_XORQ }, + { "SHLQ", A_SHLQ },{ "SHRQ", A_SHRQ }, + { "CMPQ", A_CMPQ }, + { "PUSHQ",A_PUSHQ},{ "POPQ", A_POPQ }, + { "LEAQ", A_LEAQ }, + { "CALL", A_CALL },{ "RET", A_RET }, + { "JMP", A_JMP }, + { "JE", A_JE },{ "JNE", A_JNE }, + { "JL", A_JL },{ "JLE", A_JLE }, + { "JG", A_JG },{ "JGE", A_JGE }, + { "JB", A_JB },{ "JBE", A_JBE }, + { "JA", A_JA },{ "JAE", A_JAE }, + { "JZ", A_JZ },{ "JNZ", A_JNZ }, + { "SYSCALL", A_SYSCALL }, + { "TEXT", A_TEXT }, + { "DATA", A_DATA }, + { NULL, 0 } + }; + for (int i = 0; tab[i].m; i++) + if (strcmp(tab[i].m, m) == 0) return tab[i].op; + return 0; +} + +static int +reg_lookup(const char *r) +{ + struct { const char *m; int reg; } tab[] = { + { "AX", D_AX }, { "BX", D_BX }, { "CX", D_CX }, { "DX", D_DX }, + { "SP", D_SP }, { "BP", D_BP }, { "SI", D_SI }, { "DI", D_DI }, + { "R8", D_R8 }, { "R9", D_R9 }, { "R10", D_R10 }, + { "R11", D_R11 }, { "R12", D_R12 }, { "R13", D_R13 }, + { "R14", D_R14 }, { "R15", D_R15 }, + { "X0", D_X0 }, { "X1", D_X1 }, { "X2", D_X2 }, { "X3", D_X3 }, + { "X4", D_X4 }, { "X5", D_X5 }, { "X6", D_X6 }, { "X7", D_X7 }, + { "X8", D_X8 }, { "X9", D_X9 }, { "X10", D_X10 }, + { "X11", D_X11 }, { "X12", D_X12 }, { "X13", D_X13 }, + { "X14", D_X14 }, { "X15", D_X15 }, + { "SB", D_PSB }, { "FP", D_PFP }, + { NULL, 0 } + }; + for (int i = 0; tab[i].m; i++) + if (strcmp(tab[i].m, r) == 0) return tab[i].reg; + return 0; +} + +static int +parse_operand(Asm *a, const char *s, Aoperand *out) +{ + while (*s == ' ' || *s == '\t') s++; + if (*s == '\0') { out->type = D_NONE; return 0; } + + if (*s == '$') { + s++; + char *end; + out->type = D_CONST; + out->offset = a_parsenum(s, &end); + return 0; + } + + /* (REG) form */ + if (*s == '(') { + s++; + char rbuf[8] = {0}; + int n = 0; + while (*s && *s != ')' && n < 7) rbuf[n++] = *s++; + if (*s != ')') { err(a, "missing ')' in indirect"); return -1; } + int r = reg_lookup(rbuf); + if (r == 0) { err(a, "bad register in indirect"); return -1; } + out->type = D_INDIR; + out->reg = r; + out->offset = 0; + return 0; + } + + /* number(REG) form, or label form, or REG */ + const char *p = s; + int sign = 1; + if (*p == '-') { sign = -1; p++; } + if (isdigit((unsigned char)*p)) { + char *end; + i64 off = a_parsenum(s, &end); + if (*end == '(') { + char rbuf[8] = {0}; + int n = 0; + end++; + while (*end && *end != ')' && n < 7) rbuf[n++] = *end++; + if (*end != ')') { err(a, "missing ')'"); return -1; } + int r = reg_lookup(rbuf); + if (r == 0) { err(a, "bad register"); return -1; } + out->type = D_INDIR; + out->reg = r; + out->offset = off; + return 0; + } + out->type = D_CONST; + out->offset = off * sign; + return 0; + } + + /* IDENT — register or symbol-or-label */ + if (a_isidstart((unsigned char)*s)) { + char buf[64] = {0}; + int n = 0; + while (a_isidcont((unsigned char)*s) && n < 63) buf[n++] = *s++; + buf[n] = 0; + + /* ID(SB) means external symbol */ + if (*s == '(') { + char rbuf[8] = {0}; + int rn = 0; + s++; + while (*s && *s != ')' && rn < 7) rbuf[rn++] = *s++; + if (*s != ')') { err(a, "missing ')'"); return -1; } + s++; + int r = reg_lookup(rbuf); + if (r == D_PSB) { + out->type = D_EXTERN; + out->sym = strdup(buf); + return 0; + } + out->type = D_INDIR; + out->reg = r; + out->offset = 0; + /* unusual case: name(REG) with named offset; not used */ + return 0; + } + + int r = reg_lookup(buf); + if (r != 0) { + out->type = r; + return 0; + } + /* otherwise it's a branch target */ + out->type = D_BRANCH; + out->sym = strdup(buf); + return 0; + } + + err(a, "unrecognised operand"); + return -1; +} + +int +a_parse(Asm *a) +{ + char buf[1024]; + char *line; + size_t len; + const char *pending_label = NULL; + + while (nextline(a, &line, &len, buf, sizeof buf)) { + const char *p = skipws(line); + if (*p == '\0' || (*p == '/' && p[1] == '/')) { + a->line++; + continue; + } + + /* label? */ + if (a_isidstart((unsigned char)*p) && line[0] != '\t') { + const char *q = p; + while (a_isidcont((unsigned char)*q)) q++; + if (*q == ':') { + size_t nl = q - p; + char *name = malloc(nl + 1); + memcpy(name, p, nl); + name[nl] = '\0'; + /* If a label is already pending we'd lose it + * by overwriting; flush it onto a NOP prog so + * each label still pins to a real address. */ + if (pending_label) { + Aprog *prg = calloc(1, sizeof *prg); + prg->as = A_NOP; + prg->label = pending_label; + prg->line = a->line; + if (a->head == NULL) a->head = prg; + else a->tail->link = prg; + a->tail = prg; + } + pending_label = name; + a->line++; + continue; + } + } + + /* TEXT or instruction */ + const char *m = p; + char mnem[16] = {0}; + int n = 0; + while (*m && *m != ' ' && *m != '\t' && n < 15) mnem[n++] = *m++; + mnem[n] = '\0'; + + int op = opcode_lookup(mnem); + if (op == 0) { + err(a, "unknown opcode"); + a->line++; + continue; + } + + Aprog *prg = calloc(1, sizeof *prg); + prg->as = op; + prg->line = a->line; + prg->label = pending_label; + pending_label = NULL; + + while (*m == ' ' || *m == '\t') m++; + const char *rest = m; + + if (op == A_TEXT) { + /* TEXT name,$framesize */ + char nbuf[64] = {0}; + int nn = 0; + while (*m && *m != ',' && nn < 63) nbuf[nn++] = *m++; + prg->to.type = D_EXTERN; + prg->to.sym = strdup(nbuf); + if (*m == ',') { + m++; + while (*m == ' ' || *m == '$') m++; + prg->from.type = D_CONST; + prg->from.offset = a_parsenum(m, NULL); + } + } else if (op == A_DATA) { + /* DATA name(SB),"escaped bytes" */ + char nbuf[128] = {0}; + int nn = 0; + while (*m && *m != '(' && nn < 127) nbuf[nn++] = *m++; + prg->to.type = D_EXTERN; + prg->to.sym = strdup(nbuf); + if (*m == '(') { + while (*m && *m != ')') m++; + if (*m == ')') m++; + } + while (*m == ' ' || *m == ',' || *m == '\t') m++; + if (*m != '"') { + err(a, "DATA expects \"...\""); + prg->bytes = NULL; + prg->nbytes = 0; + } else { + m++; + /* parse escapes into a fresh buffer */ + size_t cap = 32, len = 0; + u8 *buf = malloc(cap); + while (*m && *m != '"') { + int c = (unsigned char)*m++; + if (c == '\\' && *m) { + int e = (unsigned char)*m++; + switch (e) { + case 'n': c = '\n'; break; + case 't': c = '\t'; break; + case 'r': c = '\r'; break; + case '\\': c = '\\'; break; + case '"': c = '"'; break; + case '0': c = 0; break; + case 'x': { + int hi = (unsigned char)*m++; + int lo = (unsigned char)*m++; + int h = (hi<='9'?hi-'0':(hi|0x20)-'a'+10); + int l = (lo<='9'?lo-'0':(lo|0x20)-'a'+10); + c = (h << 4) | l; + break; + } + default: c = e; break; + } + } + if (len + 1 > cap) { + cap *= 2; + buf = realloc(buf, cap); + } + buf[len++] = (u8)c; + } + prg->bytes = buf; + prg->nbytes = len; + } + } else { + /* split rest at top-level comma */ + const char *comma = NULL; + for (const char *q = rest; *q; q++) + if (*q == ',' && comma == NULL) comma = q; + if (comma) { + char op1[256], op2[256]; + size_t l1 = comma - rest; + if (l1 >= sizeof op1) l1 = sizeof op1 - 1; + memcpy(op1, rest, l1); op1[l1] = '\0'; + size_t l2 = strlen(comma + 1); + if (l2 >= sizeof op2) l2 = sizeof op2 - 1; + memcpy(op2, comma + 1, l2); op2[l2] = '\0'; + parse_operand(a, op1, &prg->from); + parse_operand(a, op2, &prg->to); + } else if (*rest) { + parse_operand(a, rest, &prg->to); + } + } + + if (a->head == NULL) a->head = prg; + else a->tail->link = prg; + a->tail = prg; + a->line++; + } + return a->errs; +} diff --git a/cmd/6c/6.out.h b/cmd/6c/6.out.h new file mode 100644 index 00000000..dac3f68a --- /dev/null +++ b/cmd/6c/6.out.h @@ -0,0 +1,107 @@ +/* + * 6.out.h — amd64 instruction enum + register names. Mirrors the + * Plan 9 6c shape (cmd/6c/6.out.h) but trimmed to the subset that + * 6c emits and 6a consumes in this bootstrap. Each new opcode added + * here must also gain encoding support in cmd/6a/asm.c. + */ +#ifndef SIX_OUT_H +#define SIX_OUT_H + +/* registers — Plan 9 names; lowercase = 8-bit, etc. We use 64-bit. */ +enum { + D_NONE = 0, + + /* general purpose 64-bit */ + D_AX, D_CX, D_DX, D_BX, + D_SP, D_BP, D_SI, D_DI, + D_R8, D_R9, D_R10, D_R11, + D_R12, D_R13, D_R14, D_R15, + + /* SSE/XMM 64-bit float regs */ + D_X0, D_X1, D_X2, D_X3, + D_X4, D_X5, D_X6, D_X7, + D_X8, D_X9, D_X10, D_X11, + D_X12, D_X13, D_X14, D_X15, + + /* pseudo regs (Plan 9) */ + D_PSP, /* SP pseudo (frame-relative) */ + D_PFP, /* FP pseudo (incoming args) */ + D_PSB, /* SB pseudo (static base) */ + + /* operand kinds; not registers but share the slot */ + D_CONST, /* $N immediate */ + D_BRANCH, /* label reference */ + D_EXTERN, /* external symbol */ + D_INDIR /* offset(reg) memory */ +}; + +/* opcodes — the small set we currently emit & encode */ +enum { + A_NOP = 0, + A_TEXT, + A_DATA, + A_GLOBL, + A_END, + + A_MOVQ, + A_MOVL, + A_MOVB, + A_MOVZBQ, /* movzx r64, r/m8 — load byte zero-extended */ + A_MOVSXD, /* movsxd r64, r/m32 — load i32 sign-extended */ + + /* SSE2 scalar double-precision float */ + A_MOVSD, /* xmm/m → xmm and xmm → m */ + A_ADDSD, + A_SUBSD, + A_MULSD, + A_DIVSD, + A_UCOMISD, + A_CVTTSD2SI, /* truncate f64 → i64 */ + A_CVTSI2SD, /* convert i64 → f64 */ + + /* SSE scalar single-precision float (f32). Same xmm regs. */ + A_MOVSS, + A_ADDSS, + A_SUBSS, + A_MULSS, + A_DIVSS, + A_UCOMISS, + A_CVTTSS2SI, + A_CVTSI2SS, + A_CVTSD2SS, /* f64 → f32 truncate */ + A_CVTSS2SD, /* f32 → f64 widen */ + A_ADDQ, + A_SUBQ, + A_IMULQ, + A_IDIVQ, + A_DIVQ, /* unsigned 64-bit divide; sibling of IDIVQ */ + A_NEGQ, + A_NOTQ, + A_ANDQ, + A_ORQ, + A_XORQ, + A_SHLQ, + A_SHRQ, + A_CMPQ, + + A_PUSHQ, + A_POPQ, + A_LEAQ, + + A_CALL, + A_RET, + A_JMP, + A_JE, A_JNE, + A_JL, A_JLE, A_JG, A_JGE, + A_JB, A_JBE, A_JA, A_JAE, + A_JZ, A_JNZ, + + A_SYSCALL, + + A_LAST +}; + +const char *anames(int); /* opcode -> mnemonic */ +const char *rnames(int); /* register -> name */ + +#endif diff --git a/cmd/6c/cgen.c b/cmd/6c/cgen.c new file mode 100644 index 00000000..dedcf1f2 --- /dev/null +++ b/cmd/6c/cgen.c @@ -0,0 +1,2622 @@ +/* + * cgen.c — typed AST → Prog list, expressed as Plan 9-flavoured + * amd64 assembly text. This is the simplest thing that works: + * + * - Every function gets a stack frame sized for spilled locals + a + * 16-byte alignment pad. + * - Expressions are evaluated stack-machine style: result in AX, + * intermediate stuff pushed on the hardware stack via PUSHQ AX. + * - The first six integer args go in DI, SI, DX, CX, R8, R9 + * (SysV amd64 ABI). We don't yet handle struct-by-value or + * floats; floats and slices are deferred. + * + * Calling our own functions: emit CALL (SB), let 6a/6l resolve. + * Calling C externs: same — extern symbols are just unresolved CALLs. + */ +#include "gc.h" +#include +#include + +static const int sysv_argregs[] = { D_DI, D_SI, D_DX, D_CX, D_R8, D_R9 }; +static const int sysv_fargregs[] = { D_X0, D_X1, D_X2, D_X3, D_X4, D_X5, D_X6, D_X7 }; + +/* per-fn cursor, reset before each cgfn: counts how many 8-byte + * stack-arg slots above BP have been claimed. */ +int cg_stack_arg_cursor; + +/* return type of the current function, set by cgfn before walking + * the body. Drives tagged-union return construction and the `?` / + * `!` propagation paths. */ +static Type *cg_ret_type; +/* Pointer to the current function's frame size accumulator. cgexpr + * needs this to allocate scratch slots (e.g. match bindings) without + * threading it through every signature. */ +static int *cg_frame; + +/* Per-fn defer stack: pushed in registration order, popped (emitted) + * in reverse at each return. */ +#define DEFER_MAX 32 +static Node *defers[DEFER_MAX]; +static int ndefers; + +/* Loop stack: each `for` records the labels its `break`/`continue` + * target. The continue label is where the iterator step + cond test + * happens; the end label sits past the loop. */ +#define LOOP_MAX 16 +static const char *loop_cont[LOOP_MAX]; +static const char *loop_brk[LOOP_MAX]; +static int nloops; + +static int +cg_isfloat(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + if (t == NULL) return 0; + return t->kind == TY_F32 || t->kind == TY_F64 + || t->kind == TY_UNTYPED_FLOAT; +} + +static int +node_isfloat(Node *n) +{ + return n && cg_isfloat(n->type); +} + +static int +type_isstr(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + if (t == NULL) return 0; + return t->kind == TY_STR || t->kind == TY_UNTYPED_STR; +} + +static int +node_isstr(Node *n) +{ + return n && type_isstr(n->type); +} + +static int +type_isslice(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + return t && t->kind == TY_SLICE; +} + +static int +node_isslice(Node *n) +{ + return n && type_isslice(n->type); +} + +static int +type_isf32(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + return t && t->kind == TY_F32; +} + +static int +node_isf32(Node *n) +{ + return n && type_isf32(n->type); +} + +/* struct ≤16B all-INTEGER: 1 or 2 eightbyte regs. + * Returns 0 if not a struct or too large. */ +static int +struct_arg_size(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + if (t == NULL || t->kind != TY_STRUCT) return 0; + return (int)t->size; +} + +/* Tagged-union arg byte size: 16 (8B variants) or 24 (16B variants). + * Both fit in our 3-register classification. Returns 0 if not a + * tagged union or too large to pass in registers. */ +static int +tagged_arg_size(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + if (t == NULL || t->kind != TY_TAGGED) return 0; + if (t->size > 24) return 0; + return (int)t->size; +} + +static int +node_istaggedarg(Node *n) +{ + return n && tagged_arg_size(n->type) > 0; +} + +static int +node_isstructarg(Node *n) +{ + if (n == NULL) return 0; + int sz = struct_arg_size(n->type); + return sz > 0 && sz <= 16; +} + +/* Pick the appropriate scalar SSE opcode (SS vs SD) for a node's + * float type. Untyped float defaults to SD. */ +static int +op_for(Node *n, int sd_op, int ss_op) +{ + return node_isf32(n) ? ss_op : sd_op; +} + +/* Strict variant matcher. Returns 1 iff a value of `src` should be + * tagged as variant `vt` in a tagged-union dispatch: + * - untyped src: first variant whose type can hold it (type_assignable) + * - both NAMED: pointer-identical (same `type` declaration node) + * - one NAMED, the other not: no match (different nominal types) + * - otherwise: structural type_eq + * The pointer-identity rule is what keeps `(str | linerr)` distinguishable + * even though linerr unwraps to str. */ +static int +cg_variant_match(Type *vt, Type *src) +{ + if (vt == NULL || src == NULL) return 0; + if (type_isuntyped(src)) return type_assignable(vt, src); + if (vt->kind == TY_NAMED && src->kind == TY_NAMED) return vt == src; + if (vt->kind == TY_NAMED || src->kind == TY_NAMED) return 0; + return type_eq(vt, src); +} + +/* Find the variant-tag index of `vt` inside the tagged-union type `t`. + * Returns -1 if `t` is not tagged or `vt` does not match a variant. + * Used by N_MATCH dispatch and by the let/assign/return tag synthesis. */ +static int +cg_tag_for_variant(Type *t, Type *vt) +{ + if (t == NULL || vt == NULL) return -1; + if (t->kind == TY_NAMED) t = t->under; + if (t == NULL || t->kind != TY_TAGGED) return -1; + int idx = 0; + for (Tparam *p = t->params; p; p = p->next, idx++) { + if (cg_variant_match(p->type, vt)) return idx; + } + return -1; +} + +static int +type_istagged(Type *t) +{ + if (t == NULL) return 0; + if (t->kind == TY_NAMED) t = t->under; + return t && t->kind == TY_TAGGED; +} + +/* FFI map: ww-side ident name → linker-side symbol name. Built from + * @symbol("real_name") attributes on fn declarations. */ +typedef struct Ffi Ffi; +struct Ffi { + const char *ident; + const char *symbol; + Ffi *next; +}; +static Ffi *ffi_map; + +/* Def-as-string-literal map. `def NAME: str = "lit"` doesn't materialise + * as a real linker symbol; instead, references to NAME load the same + * (ptr, len) pair that the literal would. Avoids needing relocations + * inside DATA blocks for the ptr field of a str header. */ +typedef struct Sdef Sdef; +struct Sdef { + const char *name; + const char *bytes; + u64 len; + Sdef *next; +}; +static Sdef *sdefs; + +/* Interned string literals — emitted as DATA directives after all + * function bodies, so the linker lays them out alongside .text. */ +typedef struct Strlit Strlit; +struct Strlit { + const char *label; + const char *bytes; + u64 len; + Strlit *next; +}; +static Strlit *strlits; +static int strlit_seq; + +static const char * +intern_strlit(Cg *c, const char *bytes, u64 len) +{ + for (Strlit *s = strlits; s; s = s->next) + if (s->len == len && memcmp(s->bytes, bytes, len) == 0) + return s->label; + Strlit *s = amalloc(c->a, sizeof *s); + s->label = aprintf(c->a, "_S_%d", strlit_seq++); + s->bytes = bytes; + s->len = len; + s->next = strlits; + strlits = s; + return s->label; +} + +static void +emit_data(Cg *c, FILE *out) +{ + for (Strlit *s = strlits; s; s = s->next) { + fprintf(out, "DATA %s(SB),\"", s->label); + for (u64 i = 0; i < s->len; i++) { + unsigned char b = (unsigned char)s->bytes[i]; + switch (b) { + case '"': fputs("\\\"", out); break; + case '\\': fputs("\\\\", out); break; + case '\n': fputs("\\n", out); break; + case '\t': fputs("\\t", out); break; + case '\r': fputs("\\r", out); break; + default: + if (b < 0x20 || b >= 0x7f) + fprintf(out, "\\x%02x", b); + else + fputc(b, out); + } + } + /* Trailing NUL: lets `.ptr` be passed to libc / syscalls + * that expect a C string. The `len` field still excludes + * this byte, so iteration semantics are unchanged. */ + fputs("\\x00", out); + fputs("\"\n", out); + } + (void)c; +} + +static const char * +ffi_resolve(const char *ident) +{ + for (Ffi *f = ffi_map; f; f = f->next) + if (strcmp(f->ident, ident) == 0) return f->symbol; + return ident; +} + +static void +ffi_collect(Cg *c, Node *file) +{ + ffi_map = NULL; + if (file == NULL) return; + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_FNDECL) continue; + for (Node *a = d->attr; a; a = a->next) { + if (a->kind != N_ATTR) continue; + if (strcmp(a->str, "symbol") != 0) continue; + if (a->list == NULL || a->list->kind != N_STRLIT) continue; + Ffi *f = amalloc(c->a, sizeof *f); + f->ident = d->str; + f->symbol = a->list->str; + f->next = ffi_map; + ffi_map = f; + } + } +} + +void +cg_init(Cg *c, Arena *a) +{ + memset(c, 0, sizeof *c); + c->a = a; +} + +Prog * +newprog(Cg *c, int op) +{ + Prog *p = amalloc(c->a, sizeof *p); + p->as = op; + return p; +} + +void +emit(Cg *c, Prog *p) +{ + if (c->head == NULL) c->head = p; + else c->tail->link = p; + c->tail = p; +} + +static Adr +areg(int r) +{ + Adr a = { 0 }; + a.type = r; + return a; +} + +static Adr +aimm(long long v) +{ + Adr a = { 0 }; + a.type = D_CONST; + a.offset = v; + return a; +} + +static Adr +amem(int r, long long off) +{ + Adr a = { 0 }; + a.type = D_INDIR; + a.reg = r; + a.offset = off; + return a; +} + +static Adr +asym(const char *s) +{ + Adr a = { 0 }; + a.type = D_EXTERN; + a.sym = s; + return a; +} + +static Adr +abranch(const char *s) +{ + Adr a = { 0 }; + a.type = D_BRANCH; + a.sym = s; + return a; +} + +static char * +mklabel(Cg *c, const char *prefix) +{ + return aprintf(c->a, "%s_%s_%d", c->fnname ? c->fnname : "_", prefix, + c->labelseq++); +} + +static void +ins2(Cg *c, int op, Adr from, Adr to) +{ + Prog *p = newprog(c, op); + p->from = from; + p->to = to; + emit(c, p); +} + +static void +ins1(Cg *c, int op, Adr to) +{ + Prog *p = newprog(c, op); + p->to = to; + emit(c, p); +} + +static void +ins0(Cg *c, int op) +{ + emit(c, newprog(c, op)); +} + +static void +label(Cg *c, const char *s) +{ + Prog *p = newprog(c, A_NOP); + p->label = s; + emit(c, p); +} + +/* ------------------------------------------------------------------ */ +/* per-fn local table: name → stack offset (positive = below FP) */ + +typedef struct Local Local; +struct Local { + const char *name; + int off; /* relative to BP; negative for locals */ + Local *next; +}; + +static int +localoff(Cg *c, Local **head, const char *name, int size, int *frame) +{ + for (Local *l = *head; l; l = l->next) + if (strcmp(l->name, name) == 0) return l->off; + int al = 8; + *frame = (*frame + size + al - 1) & ~(al - 1); + int off = -*frame; + Local *l = amalloc(c->a, sizeof *l); + l->name = name; + l->off = off; + l->next = *head; + *head = l; + return off; +} + +static int +localfind(Local *head, const char *name) +{ + for (Local *l = head; l; l = l->next) + if (strcmp(l->name, name) == 0) return l->off; + return 0; /* 0 = not found (caller must verify) */ +} + +/* ------------------------------------------------------------------ */ +/* expressions: result lands in AX. Returns 1 on success. */ + +static void cgexpr(Cg*, Node*, Local*); +static void cgstmt(Cg*, Node*, Local**, int*); + +static void +cgexpr_int(Cg *c, long long v) +{ + ins2(c, A_MOVQ, aimm(v), areg(D_AX)); +} + +static void +cgexpr(Cg *c, Node *n, Local *locals) +{ + if (n == NULL) { + cgexpr_int(c, 0); + return; + } + switch (n->kind) { + case N_INTLIT: + case N_RUNELIT: + cgexpr_int(c, (long long)n->uval); + break; + case N_FLOATLIT: { + union { double d; u64 u; } x; + x.d = n->fval; + ins2(c, A_MOVQ, aimm((long long)x.u), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVSD, amem(D_SP, 0), areg(D_X0)); + ins2(c, A_ADDQ, aimm(8), areg(D_SP)); + break; + } + case N_STRLIT: { + /* result lives as the (ptr, len) pair: ptr in AX, len in BX. + * Call sites that pass a str arg pick these up directly. */ + const char *lab = intern_strlit(c, n->str, n->strlen); + ins2(c, A_LEAQ, asym(lab), areg(D_AX)); + ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_BX)); + break; + } + case N_TRUE: cgexpr_int(c, 1); break; + case N_FALSE: + case N_NIL: cgexpr_int(c, 0); break; + case N_IDENT: { + int off = localfind(locals, n->str); + if (off != 0) { + if (node_isfloat(n)) { + int op = op_for(n, A_MOVSD, A_MOVSS); + ins2(c, op, amem(D_BP, off), areg(D_X0)); + } else if (node_isstr(n)) { + /* str values flow as (AX=ptr, BX=len) so they + * can be returned in AX:DX or pushed to the + * call-arg stack uniformly. */ + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX)); + } else { + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); + } + } else { + /* Non-local: function symbols load by address (LEAQ), + * str-typed `def`s expand to (ptr, len) of the literal, + * other globals (def constants) load by value (MOVQ). */ + Type *t = n->type; + Type *u = (t && t->kind == TY_NAMED) ? t->under : t; + if (u && u->kind == TY_FN) { + ins2(c, A_LEAQ, asym(n->str), areg(D_AX)); + break; + } + for (Sdef *s = sdefs; s; s = s->next) { + if (strcmp(s->name, n->str) != 0) continue; + const char *lab = intern_strlit(c, s->bytes, + s->len); + ins2(c, A_LEAQ, asym(lab), areg(D_AX)); + ins2(c, A_MOVQ, aimm((long long)s->len), + areg(D_BX)); + goto ident_done; + } + ins2(c, A_MOVQ, asym(n->str), areg(D_AX)); + } + ident_done: + break; + } + case N_UN: + cgexpr(c, n->lhs, locals); + switch (n->op) { + case TK_MINUS: ins1(c, A_NEGQ, areg(D_AX)); break; + case TK_TILDE: ins1(c, A_NOTQ, areg(D_AX)); break; + case TK_NOT: { + ins2(c, A_CMPQ, aimm(0), areg(D_AX)); + char *t = mklabel(c, "tt"); + char *e = mklabel(c, "te"); + ins1(c, A_JE, abranch(t)); + ins2(c, A_MOVQ, aimm(0), areg(D_AX)); + ins1(c, A_JMP, abranch(e)); + label(c, t); + ins2(c, A_MOVQ, aimm(1), areg(D_AX)); + label(c, e); + break; + } + case TK_AMP: { + /* address-of for an N_IDENT local */ + if (n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); + } + break; + } + case TK_STAR: /* deref */ + ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); + break; + default: break; + } + break; + case N_BIN: { + /* str == str / str != str — delegate to rt_streq, which + * does the byte-by-byte compare. */ + if ((n->op == TK_EQ || n->op == TK_NEQ) && + node_isstr(n->lhs) && node_isstr(n->rhs)) { + /* Push rhs (len, then ptr top) */ + if (n->rhs->kind == N_IDENT) { + int off = localfind(locals, n->rhs->str); + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } else { + cgexpr(c, n->rhs, locals); /* AX=ptr, BX=len */ + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + /* Push lhs */ + if (n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } else { + cgexpr(c, n->lhs, locals); + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + ins1(c, A_POPQ, areg(D_DI)); + ins1(c, A_POPQ, areg(D_SI)); + ins1(c, A_POPQ, areg(D_DX)); + ins1(c, A_POPQ, areg(D_CX)); + ins1(c, A_CALL, asym("rt_streq")); + if (n->op == TK_NEQ) + ins2(c, A_XORQ, aimm(1), areg(D_AX)); + break; + } + if (node_isfloat(n)) { + int isf32 = node_isf32(n); + int mov = isf32 ? A_MOVSS : A_MOVSD; + cgexpr(c, n->rhs, locals); /* X0 */ + ins2(c, A_SUBQ, aimm(8), areg(D_SP)); + ins2(c, mov, areg(D_X0), amem(D_SP, 0)); + cgexpr(c, n->lhs, locals); /* X0 */ + ins2(c, mov, amem(D_SP, 0), areg(D_X1)); + ins2(c, A_ADDQ, aimm(8), areg(D_SP)); + switch (n->op) { + case TK_PLUS: + ins2(c, isf32 ? A_ADDSS : A_ADDSD, areg(D_X1), areg(D_X0)); + break; + case TK_MINUS: + ins2(c, isf32 ? A_SUBSS : A_SUBSD, areg(D_X1), areg(D_X0)); + break; + case TK_STAR: + ins2(c, isf32 ? A_MULSS : A_MULSD, areg(D_X1), areg(D_X0)); + break; + case TK_SLASH: + ins2(c, isf32 ? A_DIVSS : A_DIVSD, areg(D_X1), areg(D_X0)); + break; + default: break; + } + break; + } + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs, locals); + ins1(c, A_POPQ, areg(D_BX)); + switch (n->op) { + case TK_PLUS: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; + case TK_MINUS: ins2(c, A_SUBQ, areg(D_BX), areg(D_AX)); break; + case TK_STAR: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break; + case TK_SLASH: { + /* Use DIV (unsigned) when either operand is an unsigned + * integer type — IDIV would sign-extend a u64 with high + * bit set into a negative i64 and produce wrong results + * (see strconv.u64toa with v = 1 << 63). */ + int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) + || (n->rhs && type_isunsigned(n->rhs->type)); + ins2(c, A_MOVQ, aimm(0), areg(D_DX)); + ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX)); + break; + } + case TK_PERCENT: { + int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) + || (n->rhs && type_isunsigned(n->rhs->type)); + ins2(c, A_MOVQ, aimm(0), areg(D_DX)); + ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX)); + ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); + break; + } + case TK_AMP: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break; + case TK_PIPE: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break; + case TK_CARET: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break; + case TK_LSHIFT: case TK_RSHIFT: + /* shift amount must be in CL */ + ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); + ins2(c, n->op == TK_LSHIFT ? A_SHLQ : A_SHRQ, + areg(D_CX), areg(D_AX)); + break; + case TK_EQ: case TK_NEQ: case TK_LT: case TK_LE: + case TK_GT: case TK_GE: { + /* For ordered comparisons on unsigned operands we must + * use the JA/JAE/JB/JBE family — signed Jcc would treat + * a u64 with the high bit set as negative (e.g. the + * loop guard `n > 0` in strconv.u64toa with n=1<<63). */ + int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) + || (n->rhs && type_isunsigned(n->rhs->type)); + ins2(c, A_CMPQ, areg(D_BX), areg(D_AX)); + int op = A_JE; + switch (n->op) { + case TK_EQ: op = A_JE; break; + case TK_NEQ:op = A_JNE; break; + case TK_LT: op = unsignd ? A_JB : A_JL; break; + case TK_LE: op = unsignd ? A_JBE : A_JLE; break; + case TK_GT: op = unsignd ? A_JA : A_JG; break; + case TK_GE: op = unsignd ? A_JAE : A_JGE; break; + default: break; + } + char *t = mklabel(c, "ct"); + char *e = mklabel(c, "ce"); + ins1(c, op, abranch(t)); + ins2(c, A_MOVQ, aimm(0), areg(D_AX)); + ins1(c, A_JMP, abranch(e)); + label(c, t); + ins2(c, A_MOVQ, aimm(1), areg(D_AX)); + label(c, e); + break; + } + case TK_AND: case TK_OR: { + /* short-circuit not yet — eager evaluation. */ + if (n->op == TK_AND) + ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); + else + ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); + break; + } + default: break; + } + break; + } + case N_ASSIGN: { + /* 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. */ + if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && + n->lhs->lhs->kind == N_IDENT) { + Node *base = n->lhs->lhs; + Type *bt = base->type; + Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + int via_ptr = 0; + if (u && u->kind == TY_PTR) { + via_ptr = 1; + u = u->sub; + if (u && u->kind == TY_NAMED) u = u->under; + } + /* slice/str pseudo-field write (.ptr/.len/.cap) */ + if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) { + const char *fld = n->lhs->str; + int delta = -1; + if (strcmp(fld, "ptr") == 0) delta = 0; + else if (strcmp(fld, "len") == 0) delta = 8; + else if (strcmp(fld, "cap") == 0) delta = 16; + if (delta < 0) goto after_dot_assign; + int boff = localfind(locals, base->str); + if (n->op != TK_ASSIGN) { + if (via_ptr) { + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); + ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_BX)); + } else { + ins2(c, A_MOVQ, amem(D_BP, boff + delta), areg(D_BX)); + } + ins1(c, A_PUSHQ, areg(D_BX)); + } + cgexpr(c, n->rhs, locals); + if (n->op != TK_ASSIGN) { + ins1(c, A_POPQ, areg(D_BX)); + switch (n->op) { + case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; + case TK_MINUSEQ: + /* old in BX, rhs in AX; want AX = old-rhs. + * SUBQ src,dst is dst -= src in Plan 9. */ + ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + break; + default: break; + } + } + if (via_ptr) { + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, delta)); + } else { + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + delta)); + } + break; + } + after_dot_assign: + if (u && u->kind == TY_STRUCT) { + /* find field metadata */ + Tfield *f = NULL; + for (Tfield *fl = u->fields; fl; fl = fl->next) + if (strcmp(fl->name, n->lhs->str) == 0) + { f = fl; break; } + if (f == NULL) break; + /* Tagged-union field: synthesise tag and store + * value bytes. Compound ops on tagged fields are + * not meaningful, so only plain `=` is wired. */ + Type *ft = f->type; + Type *fu = (ft && ft->kind == TY_NAMED) + ? ft->under : ft; + if (fu && fu->kind == TY_TAGGED + && n->op == TK_ASSIGN) { + int boff = localfind(locals, base->str); + int foff = (int)f->offset; + Type *vt = n->rhs ? n->rhs->type : NULL; + int tag = cg_tag_for_variant(fu, vt); + /* Resolve the slot's base address + * (either struct local or *struct). */ + int addr_in_bx = via_ptr; + if (via_ptr) + ins2(c, A_MOVQ, amem(D_BP, boff), + areg(D_BX)); + cgexpr(c, n->rhs, locals); + if (type_isstr(vt)) { + if (addr_in_bx) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BX, foff + 8)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BX, foff + 16)); + } else { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, boff + foff + 8)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, boff + foff + 16)); + } + } else { + if (addr_in_bx) + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BX, foff + 8)); + else + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, boff + foff + 8)); + } + if (addr_in_bx) + ins2(c, A_MOVQ, + aimm(tag < 0 ? 0 : tag), + amem(D_BX, foff + 0)); + else + ins2(c, A_MOVQ, + aimm(tag < 0 ? 0 : tag), + amem(D_BP, boff + foff + 0)); + break; + } + int fsz = (int)(f->type ? f->type->size : 8); + int signed_field = f->type && ( + f->type->kind == TY_I8 || + f->type->kind == TY_I16 || + f->type->kind == TY_I32); + int load_op = A_MOVQ, store_op = A_MOVQ; + if (fsz == 1) { load_op = A_MOVZBQ; store_op = A_MOVB; } + else if (fsz == 4) { load_op = signed_field ? A_MOVSXD : A_MOVL; store_op = A_MOVL; } + int boff = localfind(locals, base->str); + int foff = (int)f->offset; + /* str-typed field: rhs cgexpr leaves (AX=ptr, BX=len); + * store both halves at field+0 and field+8. The 8/16 + * trailing-padding bytes are left untouched, which + * matches the let-init shape elsewhere in cgen. Only + * plain `=` is wired; compound on a str field is not + * meaningful. */ + Type *str_fu = (f->type && f->type->kind == TY_NAMED) + ? f->type->under : f->type; + if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STR) { + cgexpr(c, n->rhs, locals); + if (via_ptr) { + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_CX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); + } else { + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8)); + } + break; + } + /* compound: load current value into BX */ + if (n->op != TK_ASSIGN) { + if (via_ptr) { + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); + ins2(c, load_op, amem(D_BX, foff), areg(D_BX)); + } else { + ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX)); + } + ins1(c, A_PUSHQ, areg(D_BX)); + } + cgexpr(c, n->rhs, locals); /* AX = rhs */ + if (n->op != TK_ASSIGN) { + ins1(c, A_POPQ, areg(D_BX)); + switch (n->op) { + case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; + case TK_MINUSEQ: + /* old in BX, rhs in AX; want AX=old-rhs */ + ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); + ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); + break; + default: break; /* others rare */ + } + } + /* now store AX into target */ + if (via_ptr) { + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); + ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); + } else { + ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff)); + } + break; + } + } + /* float assignment to a local */ + if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) { + cgexpr(c, n->rhs, locals); /* X0 */ + int off = localfind(locals, n->lhs->str); + if (off != 0) { + int op = op_for(n, A_MOVSD, A_MOVSS); + ins2(c, op, areg(D_X0), amem(D_BP, off)); + } + break; + } + /* arr[i] = v store. Base may be a simple ident (array/slice/ + * ptr local) or a more complex expression like s.ptr where + * s: *[]u8. We compute the base address, scale the index by + * elem size, and store with the right size. */ + if (n->lhs->kind == N_INDEX && n->lhs->lhs) { + Node *base = n->lhs->lhs; + Type *bt = base->type; + Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + int is_arr = u && u->kind == TY_ARRAY; + int is_sl = u && u->kind == TY_SLICE; + int is_ptr = u && u->kind == TY_PTR; + int esz = (u && u->sub) ? (int)u->sub->size : 1; + int elem_is_str = u && u->sub && type_isstr(u->sub); + if (is_arr || is_sl || is_ptr) { + cgexpr(c, n->rhs, locals); /* AX (and BX if str) */ + /* str element: also stash len so we can store both */ + if (elem_is_str) + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs->rhs, locals); /* idx → AX */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */ + /* base address → BX */ + if (base->kind == N_IDENT && is_arr) { + int off = localfind(locals, base->str); + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); + } else if (base->kind == N_IDENT) { + int off = localfind(locals, base->str); + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); + } else { + cgexpr(c, base, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + } + ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */ + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */ + if (elem_is_str) { + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0)); + ins1(c, A_POPQ, areg(D_CX)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8)); + break; + } + int store_op = A_MOVQ; + if (esz == 1) store_op = A_MOVB; + else if (esz == 4) store_op = A_MOVL; + ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); + break; + } + } + /* Plain `r = expr;` where r is a tagged-union local. Mirrors + * the let-init path: forward AX:DX[:CX] when rhs is itself + * tagged, else synthesise the tag from rhs's static type. */ + if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN + && n->lhs->type) { + Type *lt = n->lhs->type; + Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; + if (lu && lu->kind == TY_TAGGED) { + int off = localfind(locals, n->lhs->str); + if (off == 0) break; + Type *rt = n->rhs ? n->rhs->type : NULL; + if (type_istagged(rt)) { + cgexpr(c, n->rhs, locals); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_DX), + amem(D_BP, off + 8)); + if (lu->size > 16) + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, off + 16)); + } else { + int tag = cg_tag_for_variant(lu, rt); + cgexpr(c, n->rhs, locals); + if (type_isstr(rt)) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, off + 16)); + } else { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + 8)); + } + ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), + amem(D_BP, off + 0)); + } + break; + } + } + /* Deref-target assignment `*p = v;`. The size of the store is + * determined by the type *p points at; the pointer expression + * is evaluated after the value so we don't need to spill BX. */ + if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR + && n->op == TK_ASSIGN) { + Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL; + Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt; + Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL; + if (vt && vt->kind == TY_NAMED) vt = vt->under; + cgexpr(c, n->rhs, locals); /* AX = value (BX too if str) */ + ins1(c, A_PUSHQ, areg(D_AX)); + if (vt && vt->kind == TY_STR) ins1(c, A_PUSHQ, areg(D_BX)); + cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */ + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + if (vt && vt->kind == TY_STR) { + ins1(c, A_POPQ, areg(D_CX)); /* len */ + ins1(c, A_POPQ, areg(D_AX)); /* ptr */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0)); + ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8)); + } else { + ins1(c, A_POPQ, areg(D_AX)); + int sz = vt ? (int)vt->size : 8; + int signed_field = vt && (vt->kind == TY_I8 || + vt->kind == TY_I16 || vt->kind == TY_I32); + (void)signed_field; + int store_op = A_MOVQ; + if (sz == 1) store_op = A_MOVB; + else if (sz == 4) store_op = A_MOVL; + ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); + } + break; + } + /* Plain `name = strexpr;` for a str-typed local. cgexpr leaves + * (AX=ptr, BX=len); store both halves at off+0 and off+8. + * Mirrors the let-init shape so reassignment doesn't truncate. */ + if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN + && n->lhs->type) { + Type *lt = n->lhs->type; + Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; + if (lu && lu->kind == TY_STR) { + int off = localfind(locals, n->lhs->str); + if (off == 0) break; + cgexpr(c, n->rhs, locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); + break; + } + } + if (n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + /* No local match: drop the whole assignment, including + * the RHS evaluation. Top-level let mutation isn't + * supported (no writable .data) and we don't want to + * leak a dead `MOVQ $rhs, AX` into the output. */ + if (off == 0) break; + cgexpr(c, n->rhs, locals); + if (n->op == TK_ASSIGN) { + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + break; + } + /* Compound: load → combine into BX → store. The two + * direct mem-form combines (ADDQ/SUBQ) are kept for + * the simple cases; the rest go through the generic + * register form. */ + if (n->op == TK_PLUSEQ) { + ins2(c, A_ADDQ, areg(D_AX), amem(D_BP, off)); + break; + } + if (n->op == TK_MINUSEQ) { + ins2(c, A_SUBQ, areg(D_AX), amem(D_BP, off)); + break; + } + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); + switch (n->op) { + case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break; + case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break; + case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break; + case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break; + case TK_LSHIFTEQ: + ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); + ins2(c, A_SHLQ, areg(D_CX), areg(D_BX)); + break; + case TK_RSHIFTEQ: + ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); + ins2(c, A_SHRQ, areg(D_CX), areg(D_BX)); + break; + case TK_SLASHEQ: + /* BX = BX / AX. IDIV uses DX:AX/RAX. Move + * BX→AX first, sign-extend via CQO would be + * cleanest; skip for now and fall back to + * MOVQ to avoid emitting wrong code. */ + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off)); + goto skip_assign_store; + default: + /* unknown: just store rhs (legacy fallback) */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + goto skip_assign_store; + } + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off)); + skip_assign_store: ; + } + break; + } + case N_CALL: { + /* 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) { + Node *a = n->list; + Type *at = a->type; + Type *u = (at && at->kind == TY_NAMED) ? at->under : at; + if (u && (u->kind == TY_SLICE || u->kind == TY_STR) + && a->kind == N_IDENT) { + int off = localfind(locals, a->str); + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); + } else if (u && u->kind == TY_ARRAY) { + ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX)); + } else { + /* fall back: load via .len pseudo-field */ + cgexpr(c, a, locals); + } + break; + } + if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && + strcmp(n->lhs->str, "free") == 0 && n->list && + n->list->next == NULL) { + Node *p = n->list; + Type *pt = p->type; + Type *u = (pt && pt->kind == TY_NAMED) ? pt->under : pt; + if (u && u->kind == TY_PTR && u->sub) { + int sz = (int)u->sub->size; + if (sz == 0) sz = 8; + cgexpr(c, p, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); + ins2(c, A_MOVQ, aimm(sz), areg(D_SI)); + ins1(c, A_CALL, asym(ffi_resolve("free"))); + } else if (u && u->kind == TY_SLICE + && p->kind == N_IDENT) { + int off = localfind(locals, p->str); + int esz = (int)(u->sub ? u->sub->size : 1); + ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX)); + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_BX)); + ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); + } + ins2(c, A_MOVQ, areg(D_AX), areg(D_SI)); + ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_DI)); + ins1(c, A_CALL, asym(ffi_resolve("free"))); + } + break; + } + if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && + strcmp(n->lhs->str, "alloc") == 0 && n->list) { + /* alloc(value): heap-init a fresh *T with the value's + * bytes. Size comes from the value's static type. */ + Node *v = n->list; + Type *t = v->type; + Type *u = (t && t->kind == TY_NAMED) ? t->under : t; + Type *def = type_default(t); + int sz = def ? (int)def->size : 8; + if (sz == 0) sz = 8; + /* call os.alloc(sz) */ + ins2(c, A_MOVQ, aimm(sz), areg(D_DI)); + ins1(c, A_CALL, asym(ffi_resolve("alloc"))); + ins1(c, A_PUSHQ, areg(D_AX)); /* save ptr */ + if (v->kind == N_STRUCTLIT && u && u->kind == TY_STRUCT) { + for (Node *f = v->list; f; f = f->next) { + u64 foff = 0; + int fsz = 8; + for (Tfield *fl = u->fields; fl; fl = fl->next) { + if (strcmp(fl->name, f->str) == 0) { + foff = fl->offset; + fsz = (int)(fl->type ? fl->type->size : 8); + break; + } + } + cgexpr(c, f->lhs, locals); /* AX = field val */ + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX)); + int op = A_MOVQ; + if (fsz == 1) op = A_MOVB; + else if (fsz == 4) op = A_MOVL; + ins2(c, op, areg(D_AX), amem(D_BX, (int)foff)); + } + } else { + cgexpr(c, v, locals); /* AX = value */ + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX)); + int op = A_MOVQ; + if (sz == 1) op = A_MOVB; + else if (sz == 4) op = A_MOVL; + ins2(c, op, areg(D_AX), amem(D_BX, 0)); + } + ins1(c, A_POPQ, areg(D_AX)); /* return the ptr */ + break; + } + if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && + strcmp(n->lhs->str, "append") == 0 && n->list && + n->list->next) { + /* append(s, v1, v2, ...) — one CALL per item. + * append(s, items...) — runtime loop over items. */ + Node *sn = n->list; + Type *st = sn->type; + Type *su = (st && st->kind == TY_NAMED) ? st->under : st; + int esz = (su && su->sub) ? (int)su->sub->size : 1; + const char *fn = (esz == 1) ? "appendu8" : "appendi64"; + int sn_off = (sn->kind == N_IDENT) + ? localfind(locals, sn->str) : 0; + for (Node *vn = sn->next; vn; vn = vn->next) { + if (vn->kind == N_SPREAD && + vn->lhs && vn->lhs->kind == N_IDENT) { + /* iterate items; items_off is the + * spread-source slice's BP offset. */ + int it_off = localfind(locals, vn->lhs->str); + int load_op = (esz == 1) ? A_MOVZBQ : A_MOVQ; + /* push counter on stack */ + ins2(c, A_SUBQ, aimm(8), areg(D_SP)); + ins2(c, A_MOVQ, aimm(0), amem(D_SP, 0)); + char *ll = mklabel(c, "spr_l"); + char *le = mklabel(c, "spr_e"); + label(c, ll); + ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX)); + ins2(c, A_CMPQ, areg(D_DX), areg(D_CX)); + ins1(c, A_JGE, abranch(le)); + ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX)); + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_AX)); + ins2(c, A_IMULQ, areg(D_AX), areg(D_CX)); + } + ins2(c, A_ADDQ, areg(D_CX), areg(D_BX)); + ins2(c, load_op, amem(D_BX, 0), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_AX)); + ins1(c, A_POPQ, areg(D_SI)); + ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); + ins1(c, A_CALL, asym(fn)); + ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0)); + ins1(c, A_JMP, abranch(ll)); + label(c, le); + ins2(c, A_ADDQ, aimm(8), areg(D_SP)); + continue; + } + cgexpr(c, vn, locals); /* val → AX */ + ins1(c, A_PUSHQ, areg(D_AX)); + if (sn->kind == N_IDENT) + ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_AX)); + ins1(c, A_POPQ, areg(D_SI)); + ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); + ins1(c, A_CALL, asym(fn)); + } + break; + } + /* up to 6 integer + 8 float args via SysV registers. + * str args occupy two integer eightbytes (ptr, len). */ + int argcount = 0; + Node *args[16] = {0}; + for (Node *a = n->list; a; a = a->next) + if (argcount < 16) args[argcount++] = a; + /* eval right-to-left, push to stack */ + for (int i = argcount - 1; i >= 0; i--) { + if (node_isslice(args[i]) && args[i]->kind == N_IDENT) { + int off = localfind(locals, args[i]->str); + /* push cap, len, ptr (top) so pops give ptr,len,cap */ + ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + continue; + } + if (args[i]->kind == N_SLICE) { + Node *base = args[i]->lhs; + Node *lo = args[i]->rhs; + Node *hi = args[i]->cond; + Type *bt = base ? base->type : NULL; + Type *bu = (bt && bt->kind == TY_NAMED) ? + bt->under : bt; + /* base addr → push */ + if (base->kind == N_IDENT) { + int boff = localfind(locals, base->str); + if (bu && bu->kind == TY_ARRAY) { + ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX)); + } else { + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); + } + } else { + cgexpr(c, base, locals); + } + ins1(c, A_PUSHQ, areg(D_AX)); + /* hi (default base length) → push */ + if (hi) cgexpr(c, hi, locals); + else if (bu && bu->kind == TY_ARRAY) + cgexpr_int(c, (long long)bu->alen); + else if (base->kind == N_IDENT && bu && + (bu->kind == TY_SLICE || bu->kind == TY_STR)) { + int boff = localfind(locals, base->str); + ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX)); + } else { + cgexpr_int(c, 0); + } + ins1(c, A_PUSHQ, areg(D_AX)); + /* lo (default 0) → AX */ + if (lo) cgexpr(c, lo, locals); + else cgexpr_int(c, 0); + ins1(c, A_POPQ, areg(D_BX)); /* hi */ + ins1(c, A_POPQ, areg(D_CX)); /* base */ + /* len = hi - lo (DX) */ + ins2(c, A_MOVQ, areg(D_BX), areg(D_DX)); + ins2(c, A_SUBQ, areg(D_AX), areg(D_DX)); + /* ptr = base + lo */ + ins2(c, A_ADDQ, areg(D_AX), areg(D_CX)); + /* push cap, len, ptr (top) */ + ins1(c, A_PUSHQ, areg(D_DX)); /* cap */ + ins1(c, A_PUSHQ, areg(D_DX)); /* len */ + ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */ + continue; + } + if (node_isstructarg(args[i]) && args[i]->kind == N_IDENT) { + /* load qword(s) directly from the struct's slot */ + int off = localfind(locals, args[i]->str); + int sz = struct_arg_size(args[i]->type); + if (sz > 8) { + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + continue; + } + if (node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) { + /* Tagged-union: push each 8B word from the slot. + * High word goes first so the popper drains them + * in low→high order into the arg-register class. */ + int off = localfind(locals, args[i]->str); + int sz = tagged_arg_size(args[i]->type); + int nwords = sz / 8; + for (int k = nwords - 1; k >= 0; k--) { + ins2(c, A_MOVQ, amem(D_BP, off + k*8), + areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + continue; + } + cgexpr(c, args[i], locals); + if (node_isfloat(args[i])) { + ins2(c, A_SUBQ, aimm(8), areg(D_SP)); + ins2(c, A_MOVSD, areg(D_X0), amem(D_SP, 0)); + } else if (node_isstr(args[i])) { + ins1(c, A_PUSHQ, areg(D_BX)); /* len */ + ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */ + } else { + ins1(c, A_PUSHQ, areg(D_AX)); + } + } + /* pop forward into the right register class. Args that + * don't fit in regs stay on the stack and are reached by + * the callee via positive offsets from BP. The caller is + * responsible for cleaning them up after CALL. */ + int ii = 0, fi = 0, stackslots = 0; + for (int i = 0; i < argcount; i++) { + if (node_isfloat(args[i])) { + if (fi < 8) { + ins2(c, A_MOVSD, amem(D_SP, 0), + areg(sysv_fargregs[fi])); + ins2(c, A_ADDQ, aimm(8), areg(D_SP)); + fi++; + } else { + stackslots++; /* leave on stack */ + } + } else if (node_isstr(args[i])) { + for (int k = 0; k < 2; k++) { + if (ii < 6) + ins1(c, A_POPQ, areg(sysv_argregs[ii++])); + else + stackslots++; + } + } else if (node_isslice(args[i])) { + for (int k = 0; k < 3; k++) { + if (ii < 6) + ins1(c, A_POPQ, areg(sysv_argregs[ii++])); + else + stackslots++; + } + } else if (node_isstructarg(args[i])) { + int sz = struct_arg_size(args[i]->type); + int eb = (sz > 8) ? 2 : 1; + for (int k = 0; k < eb; k++) { + if (ii < 6) + ins1(c, A_POPQ, areg(sysv_argregs[ii++])); + else + stackslots++; + } + } else if (node_istaggedarg(args[i])) { + int sz = tagged_arg_size(args[i]->type); + int eb = sz / 8; + for (int k = 0; k < eb; k++) { + if (ii < 6) + ins1(c, A_POPQ, areg(sysv_argregs[ii++])); + else + stackslots++; + } + } else { + if (ii < 6) { + ins1(c, A_POPQ, areg(sysv_argregs[ii])); + ii++; + } else { + stackslots++; + } + } + } + /* SysV: variadic callees require AL to hold the count of + * XMM regs used in the variable portion. We don't pass + * floats yet, so AL=0 covers every case we emit. */ + Type *callee_t = n->lhs ? n->lhs->type : NULL; + Type *cu = (callee_t && callee_t->kind == TY_NAMED) ? + callee_t->under : callee_t; + if (cu && cu->kind == TY_FN && cu->variadic) + ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + if (n->lhs->kind == N_IDENT) { + ins1(c, A_CALL, asym(ffi_resolve(n->lhs->str))); + } else if (n->lhs->kind == N_DOT && n->lhs->lhs && + n->lhs->lhs->kind == N_IDENT) { + /* `m.fn()` is module-qualified iff the ident has no + * concrete type (SK_USE leaves it ty_err). For a real + * type — typically a struct or *struct holding a + * function pointer — we load the field and indirect. */ + Type *bt = n->lhs->lhs->type; + if (bt == NULL || bt == ty_err) { + ins1(c, A_CALL, asym(ffi_resolve(n->lhs->str))); + } else { + cgexpr(c, n->lhs, locals); /* AX = fn ptr */ + ins1(c, A_CALL, areg(D_AX)); + } + } else { + cgexpr(c, n->lhs, locals); + ins1(c, A_CALL, areg(D_AX)); + } + /* SysV: caller cleans stack args. */ + if (stackslots > 0) + ins2(c, A_ADDQ, aimm(stackslots * 8), areg(D_SP)); + /* If callee returns a str (16B → AX:DX per SysV), shuffle + * len from DX into BX so str values stay in (AX, BX). */ + if (node_isstr(n)) ins2(c, A_MOVQ, areg(D_DX), areg(D_BX)); + break; + } + case N_MATCH: { + /* match on a tagged-union scrutinee. Read tag and value from + * the slot. Dispatch by the resolved variant index of each + * case's type pattern — case order is independent of variant + * declaration order. A case with no pattern (`case =>`) is a + * default arm; its body always runs. + * + * Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third + * word is only meaningful for variants whose payload is >8B + * (e.g. str). Bindings sized 16B (str) copy two words. */ + Node *s = n->lhs; + Type *st = s ? s->type : NULL; + Type *su = (st && st->kind == TY_NAMED) ? st->under : st; + int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16; + int sl_off; + if (s->kind == N_IDENT) { + sl_off = localfind(locals, s->str); + } else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT + && s->lhs->type) { + /* `match (p.field)` — point sl_off at the field's slot + * inside the parent struct. The slot layout (tag at +0, + * value words at +8/+16) is contiguous within the struct, + * so no spill is needed. */ + Type *bt = s->lhs->type; + Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + Tfield *f = NULL; + if (bu && bu->kind == TY_STRUCT) { + for (Tfield *fl = bu->fields; fl; fl = fl->next) { + if (strcmp(fl->name, s->str) == 0) { + f = fl; break; + } + } + } + if (f) { + int boff = localfind(locals, s->lhs->str); + sl_off = boff + (int)f->offset; + } else { + /* fall back to spill */ + sl_off = localoff(c, &locals, "@match_spill", + slot_size, cg_frame); + cgexpr(c, s, locals); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, sl_off + 0)); + } + } else { + /* Spill non-ident scrutinees (e.g. `match (foo()?)`) into + * a scratch slot so we can index out the tag/value. The + * call ABI for tagged returns is AX=tag, DX=value0, + * CX=value1 — copy each word into the slot. */ + sl_off = localoff(c, &locals, "@match_spill", slot_size, + cg_frame); + cgexpr(c, s, locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, sl_off + 8)); + if (slot_size > 16) + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, sl_off + 16)); + } + char *end = mklabel(c, "match_end"); + for (Node *cs = n->list; cs; cs = cs->next) { + char *next = mklabel(c, "match_next"); + if (cs->type != NULL) { + int tag = cg_tag_for_variant(su, cs->type); + ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); + if (cs->list != NULL) { + /* Multi-pattern `case T1 | T2 | ... =>`: + * if the tag matches any of the alts, + * jump to body; otherwise to the next + * case. */ + char *body = mklabel(c, "match_body"); + ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), + areg(D_AX)); + ins1(c, A_JE, abranch(body)); + for (Node *alt = cs->list; alt; + alt = alt->next) { + int atag = cg_tag_for_variant( + su, alt->type); + ins2(c, A_CMPQ, + aimm(atag < 0 ? 0 : atag), + areg(D_AX)); + ins1(c, A_JE, abranch(body)); + } + ins1(c, A_JMP, abranch(next)); + label(c, body); + } else { + ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), + areg(D_AX)); + ins1(c, A_JNE, abranch(next)); + } + } + if (cs->str && cs->str[0] && cs->type) { + Type *bt = cs->type; + Type *bu = (bt && bt->kind == TY_NAMED) + ? bt->under : bt; + int bsz = (bu && bu->kind == TY_STR) ? 16 : 8; + int voff = localoff(c, &locals, cs->str, bsz, + cg_frame); + int nwords = (bsz + 7) / 8; + for (int w = 0; w < nwords; w++) { + ins2(c, A_MOVQ, + amem(D_BP, sl_off + 8 + 8*w), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, voff + 8*w)); + } + } + cgstmt(c, cs->body, &locals, cg_frame); + ins1(c, A_JMP, abranch(end)); + label(c, next); + } + label(c, end); + break; + } + case N_TRYPROP: { + /* Evaluate tagged value: AX=tag, DX=value0[, CX=value1]. + * If tag != 0, propagate as the current function's return. + * On success, unwrap to the success-variant ABI: ≤8B values + * land in AX; str values land in (AX=ptr, BX=len). */ + cgexpr(c, n->lhs, locals); + Type *u = n->lhs ? n->lhs->type : NULL; + if (u && u->kind == TY_NAMED) u = u->under; + Type *first = (u && u->kind == TY_TAGGED && u->params) + ? u->params->type : NULL; + int success_is_str = type_isstr(first); + char *cont = mklabel(c, "tryprop_ok"); + ins2(c, A_CMPQ, aimm(0), areg(D_AX)); + ins1(c, A_JE, abranch(cont)); + ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); + ins1(c, A_POPQ, areg(D_BP)); + ins0(c, A_RET); + label(c, cont); + if (success_is_str) + ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); + ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); + break; + } + case N_TRYUNW: { + /* On error variant: exit(1) directly via the syscall. */ + cgexpr(c, n->lhs, locals); + Type *u = n->lhs ? n->lhs->type : NULL; + if (u && u->kind == TY_NAMED) u = u->under; + Type *first = (u && u->kind == TY_TAGGED && u->params) + ? u->params->type : NULL; + int success_is_str = type_isstr(first); + char *cont = mklabel(c, "tryunw_ok"); + ins2(c, A_CMPQ, aimm(0), areg(D_AX)); + ins1(c, A_JE, abranch(cont)); + ins2(c, A_MOVQ, aimm(1), areg(D_DI)); + ins2(c, A_MOVQ, aimm(60), areg(D_AX)); + ins0(c, A_SYSCALL); + label(c, cont); + if (success_is_str) + ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); + ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); + break; + } + case N_CAST: { + int from_f = node_isfloat(n->lhs); + int to_f = cg_isfloat(n->type); + int from_f32 = node_isf32(n->lhs); + int to_f32 = type_isf32(n->type); + cgexpr(c, n->lhs, locals); /* AX or X0 depending */ + if (from_f && !to_f) { + int op = from_f32 ? A_CVTTSS2SI : A_CVTTSD2SI; + ins2(c, op, areg(D_X0), areg(D_AX)); + } else if (!from_f && to_f) { + int op = to_f32 ? A_CVTSI2SS : A_CVTSI2SD; + ins2(c, op, areg(D_AX), areg(D_X0)); + } else if (from_f && to_f && from_f32 != to_f32) { + int op = to_f32 ? A_CVTSD2SS : A_CVTSS2SD; + ins2(c, op, areg(D_X0), areg(D_X0)); + } + break; + } + case N_DOT: { + /* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16). + * Arrays don't carry a header; .len uses the static size and + * .ptr is the address of the first element. */ + Type *bt = n->lhs ? n->lhs->type : NULL; + Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + /* Module-qualified value reference: `mod.name`. The checker + * leaves SK_USE idents untyped (NULL/ty_err); detect that and + * look up the leaf in the flat (driver-concatenated) sym/def + * maps the same way a bare N_IDENT would. */ + if (n->lhs && n->lhs->kind == N_IDENT + && (bt == NULL || bt == ty_err)) { + Type *t = n->type; + Type *tu = (t && t->kind == TY_NAMED) ? t->under : t; + if (tu && tu->kind == TY_FN) { + ins2(c, A_LEAQ, asym(n->str), areg(D_AX)); + break; + } + for (Sdef *s = sdefs; s; s = s->next) { + if (strcmp(s->name, n->str) != 0) continue; + const char *lab = intern_strlit(c, s->bytes, + s->len); + ins2(c, A_LEAQ, asym(lab), areg(D_AX)); + ins2(c, A_MOVQ, aimm((long long)s->len), + areg(D_BX)); + goto dot_done; + } + ins2(c, A_MOVQ, asym(n->str), areg(D_AX)); + goto dot_done; + } + int lenfld = (n->str && strcmp(n->str, "len") == 0); + int capfld = (n->str && strcmp(n->str, "cap") == 0); + int ptrfld = (n->str && strcmp(n->str, "ptr") == 0); + if (u && (u->kind == TY_SLICE || u->kind == TY_STR) + && (lenfld || capfld || ptrfld)) { + if (n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + int delta = ptrfld ? 0 : (lenfld ? 8 : 16); + ins2(c, A_MOVQ, amem(D_BP, off + delta), + areg(D_AX)); + } else { + /* Evaluate the str/slice expression — leaves + * (AX=ptr, BX=len) for str. .ptr returns AX, + * .len shuffles BX→AX. */ + cgexpr(c, n->lhs, locals); + if (lenfld) + ins2(c, A_MOVQ, areg(D_BX), + areg(D_AX)); + } + break; + } + if (u && u->kind == TY_ARRAY && n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + if (lenfld) { + ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX)); + break; + } + if (ptrfld) { + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); + break; + } + } + /* tuple positional field access: t.0, t.1, ... */ + if (u && u->kind == TY_TUPLE && n->lhs->kind == N_IDENT && n->str) { + int idx = 0; + for (const char *q = n->str; *q; q++) idx = idx * 10 + (*q - '0'); + Tparam *tp = u->params; + int foff = 0; + while (idx > 0 && tp) { + if (tp->type) foff += (int)tp->type->size; + tp = tp->next; + idx--; + } + if (tp != NULL) { + int fsz = (int)(tp->type ? tp->type->size : 8); + int signed_field = tp->type && ( + tp->type->kind == TY_I8 || + tp->type->kind == TY_I16 || + tp->type->kind == TY_I32); + int op = A_MOVQ; + if (fsz == 1) op = A_MOVZBQ; + else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; + int off = localfind(locals, n->lhs->str); + ins2(c, op, amem(D_BP, off + foff), areg(D_AX)); + } + break; + } + /* real struct field: load at struct_off + field_off */ + if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + for (Tfield *f = u->fields; f; f = f->next) { + if (strcmp(f->name, n->str) != 0) continue; + /* str field: load (ptr, len) into (AX, BX) so the + * value flows through the str-rhs convention. */ + Type *str_fu = (f->type && f->type->kind == TY_NAMED) + ? f->type->under : f->type; + if (str_fu && str_fu->kind == TY_STR) { + ins2(c, A_MOVQ, + amem(D_BP, off + (int)f->offset + 0), + areg(D_AX)); + ins2(c, A_MOVQ, + amem(D_BP, off + (int)f->offset + 8), + areg(D_BX)); + break; + } + int fsz = (int)(f->type ? f->type->size : 8); + int signed_field = f->type && ( + f->type->kind == TY_I8 || + f->type->kind == TY_I16 || + f->type->kind == TY_I32); + int op = A_MOVQ; + if (fsz == 1) op = A_MOVZBQ; + else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; + ins2(c, op, + amem(D_BP, off + (int)f->offset), + areg(D_AX)); + break; + } + break; + } + /* pointer-to-slice/str field: deref and read pseudo-field. + * Used by helpers like rt_appendu8(s: *[]u8, v: u8). */ + if (u && u->kind == TY_PTR && u->sub) { + Type *inner = u->sub; + if (inner->kind == TY_NAMED) inner = inner->under; + if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR) + && (lenfld || capfld || ptrfld) + && n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); + int delta = ptrfld ? 0 : (lenfld ? 8 : 16); + ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX)); + break; + } + } + /* pointer-to-struct field: deref and load. Common pattern: + * fn move(p: *point) ... { p.x += dx; ... } */ + if (u && u->kind == TY_PTR && u->sub) { + Type *inner = u->sub; + if (inner->kind == TY_NAMED) inner = inner->under; + if (inner && inner->kind == TY_STRUCT + && n->lhs->kind == N_IDENT) { + int off = localfind(locals, n->lhs->str); + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); + for (Tfield *f = inner->fields; f; f = f->next) { + if (strcmp(f->name, n->str) != 0) continue; + /* str field through *struct: read len into a + * scratch first (it's at +8) so loading ptr + * into AX last leaves (AX=ptr, BX=len). We + * use CX as the scratch, then move CX→BX. */ + Type *str_fu = (f->type && f->type->kind == TY_NAMED) + ? f->type->under : f->type; + if (str_fu && str_fu->kind == TY_STR) { + ins2(c, A_MOVQ, + amem(D_BX, (int)f->offset + 8), + areg(D_CX)); + ins2(c, A_MOVQ, + amem(D_BX, (int)f->offset + 0), + areg(D_AX)); + ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); + break; + } + int fsz = (int)(f->type ? f->type->size : 8); + int signed_field = f->type && ( + f->type->kind == TY_I8 || + f->type->kind == TY_I16 || + f->type->kind == TY_I32); + int op = A_MOVQ; + if (fsz == 1) op = A_MOVZBQ; + else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; + ins2(c, op, + amem(D_BX, (int)f->offset), + areg(D_AX)); + break; + } + break; + } + } + /* Chained N_DOT through a *struct field. cgexpr lhs leaves + * AX = the inner *struct pointer; load the requested field + * with a single MOVQ. Without this, returning `o.p.val` + * silently leaves AX = o.p (the pointer) and the outer + * cast/use sees the pointer instead of the dereferenced + * field. (Surfaced building ww-6l.) */ + if (n->lhs->kind == N_DOT) { + Type *lt = n->lhs->type; + Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; + if (lu && lu->kind == TY_PTR && lu->sub) { + Type *inner = lu->sub; + if (inner->kind == TY_NAMED) inner = inner->under; + if (inner && inner->kind == TY_STRUCT) { + for (Tfield *f = inner->fields; f; f = f->next) { + if (strcmp(f->name, n->str) != 0) continue; + cgexpr(c, n->lhs, locals); /* AX = inner ptr */ + Type *ft = f->type; + Type *fu = (ft && ft->kind == TY_NAMED) + ? ft->under : ft; + /* str field: load (ptr, len) into (AX, BX). */ + if (fu && fu->kind == TY_STR) { + ins2(c, A_MOVQ, + amem(D_AX, (int)f->offset + 8), + areg(D_BX)); + ins2(c, A_MOVQ, + amem(D_AX, (int)f->offset + 0), + areg(D_AX)); + goto dot_done; + } + int fsz = (int)(ft ? ft->size : 8); + int signed_field = ft && ( + ft->kind == TY_I8 || + ft->kind == TY_I16 || + ft->kind == TY_I32); + int op = A_MOVQ; + if (fsz == 1) op = A_MOVZBQ; + else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; + ins2(c, op, amem(D_AX, (int)f->offset), + areg(D_AX)); + goto dot_done; + } + } + } + } + /* fall through to base evaluation; result placeholder */ + cgexpr(c, n->lhs, locals); + dot_done: + break; + } + case N_INDEX: { + /* Scaled indexing for slice/array/str/ptr-to-T. + * Element size is 1 for u8/str, otherwise type's natural size. */ + Type *bt = n->lhs ? n->lhs->type : NULL; + Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + int esz = 1; + if (u && u->sub) esz = (int)u->sub->size; + if (u && u->kind == TY_STR) esz = 1; + + if (n->lhs->kind == N_IDENT && u) { + int off = localfind(locals, n->lhs->str); + cgexpr(c, n->rhs, locals); /* idx → AX */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + /* base address into BX */ + if (u->kind == TY_ARRAY) { + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); + } else { + /* slice/str/ptr: ptr field is at off+0 */ + ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); + } + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + /* str element: load (ptr, len) into (AX, BX) so the + * value flows through the str-rhs convention. */ + if (u->sub && type_isstr(u->sub)) { + ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_CX)); + ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); + break; + } + int signed_elem = u && u->sub && ( + u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || + u->sub->kind == TY_I32); + int load_op = A_MOVQ; + if (esz == 1) load_op = A_MOVZBQ; + else if (esz == 4) load_op = signed_elem ? A_MOVSXD : A_MOVL; + ins2(c, load_op, amem(D_BX, 0), areg(D_AX)); + break; + } + /* Fallback: evaluate base (treat as plain pointer) and + * dereference at base+idx. Pick the load opcode by element + * size — `b.data[i]` on a *u8 must read 1 byte, not 8. + * + * Scale the index in a register before pushing, because + * IMULQ on a memory operand isn't currently encoded by 6a + * (modrm bits use mod=3 register form). */ + cgexpr(c, n->rhs, locals); + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs, locals); + ins1(c, A_POPQ, areg(D_BX)); + ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); + /* str element: load (ptr, len) into (AX, BX). */ + if (u && u->sub && type_isstr(u->sub)) { + ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_BX)); + ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); + break; + } + { + int signed_elem = u && u->sub && ( + u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || + u->sub->kind == TY_I32); + int load_op = A_MOVQ; + if (esz == 1) load_op = A_MOVZBQ; + else if (esz == 4) + load_op = signed_elem ? A_MOVSXD : A_MOVL; + ins2(c, load_op, amem(D_AX, 0), areg(D_AX)); + } + break; + } + default: + cgexpr_int(c, 0); + break; + } +} + +static void +cgstmt(Cg *c, Node *n, Local **locals, int *frame) +{ + if (n == NULL) return; + switch (n->kind) { + case N_BLOCK: + for (Node *s = n->list; s; s = s->next) + cgstmt(c, s, locals, frame); + break; + case N_EXPRSTMT: + cgexpr(c, n->lhs, *locals); + break; + case N_LET: { + Type *lt = n->type; + Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; + int sz = 8; + if (lu && lu->kind == TY_ARRAY) sz = (int)lu->size; + else if (lu && lu->kind == TY_SLICE) sz = 24; + else if (lu && lu->kind == TY_STR) sz = 16; + else if (lu && lu->kind == TY_STRUCT) sz = (int)lu->size; + else if (lu && lu->kind == TY_TUPLE) sz = (int)lu->size; + else if (lu && lu->kind == TY_TAGGED) sz = (int)lu->size; + int off = localoff(c, locals, n->str, sz, frame); + int isf = cg_isfloat(lt); + int isf32 = type_isf32(lt); + /* alloc([], n) initialiser for a slice local: allocate + * n*esize bytes, build the {ptr, 0, n} header in the slot. + * Element size comes from the declared slice type. */ + if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24 + && n->rhs->kind == N_CALL && n->rhs->lhs + && n->rhs->lhs->kind == N_IDENT + && strcmp(n->rhs->lhs->str, "alloc") == 0 + && n->rhs->list && n->rhs->list->kind == N_ARRLIT + && n->rhs->list->list == NULL + && n->rhs->list->next && n->rhs->list->next->next == NULL) { + Node *count = n->rhs->list->next; + int esz = (lu->sub) ? (int)lu->sub->size : 1; + cgexpr(c, count, *locals); /* AX = n */ + ins1(c, A_PUSHQ, areg(D_AX)); /* save count */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_BX)); + ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); + } + ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); + ins1(c, A_CALL, asym(ffi_resolve("alloc"))); + ins1(c, A_POPQ, areg(D_BX)); /* count */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); + break; + } + /* str initialiser: cgexpr produces (AX=ptr, BX=len). */ + if (n->rhs && type_isstr(lt) && sz == 16) { + cgexpr(c, n->rhs, *locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); + break; + } + /* 2-tuple initialiser from a function call: SysV returns + * a 16-byte aggregate in (AX, DX). Store both into the + * tuple slot. */ + if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 16) { + cgexpr(c, n->rhs, *locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); + break; + } + /* Tagged-union initialiser. Two shapes: + * 1) rhs already produces a tagged-union value (e.g. a fn + * call returning (T | E)). cgexpr leaves AX=tag, + * DX=value0[, CX=value1] — copy each into the slot. + * 2) rhs is a bare variant value (e.g. `let r: (i64|i32) = 7`). + * Synthesise the tag from rhs's static type and store it + * alongside the value. str-typed rhs flows as + * (AX=ptr, BX=len) so we store both halves. */ + if (n->rhs && lu && lu->kind == TY_TAGGED) { + Type *rt = n->rhs->type; + if (type_istagged(rt)) { + cgexpr(c, n->rhs, *locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); + if (lu->size > 16) + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, off + 16)); + } else { + int tag = cg_tag_for_variant(lu, rt); + cgexpr(c, n->rhs, *locals); + if (type_isstr(rt)) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, off + 16)); + } else { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + 8)); + } + ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), + amem(D_BP, off + 0)); + } + break; + } + /* slice expression initialiser: build a {ptr, len, cap} header + * referring to the source. Element size assumed to be 1 + * (u8) for now; real element-size scaling is a future TODO. */ + if (n->rhs && n->rhs->kind == N_SLICE && lu + && lu->kind == TY_SLICE) { + Node *base = n->rhs->lhs; + Node *lo = n->rhs->rhs; + Node *hi = n->rhs->cond; + Type *bt = base ? base->type : NULL; + Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; + /* load base address */ + if (base->kind == N_IDENT) { + int boff = localfind(*locals, base->str); + if (bu && bu->kind == TY_ARRAY) { + ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX)); + } else { + /* slice/str/ptr: load .ptr */ + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); + } + } else { + cgexpr(c, base, *locals); + } + ins1(c, A_PUSHQ, areg(D_AX)); /* save base addr */ + /* lo (default 0) */ + if (lo) cgexpr(c, lo, *locals); + else cgexpr_int(c, 0); + ins1(c, A_PUSHQ, areg(D_AX)); /* save lo */ + /* hi (default base length) */ + if (hi) { + cgexpr(c, hi, *locals); + } else if (bu && bu->kind == TY_ARRAY) { + cgexpr_int(c, (long long)bu->alen); + } else if (base->kind == N_IDENT && bu && + (bu->kind == TY_SLICE || bu->kind == TY_STR)) { + int boff = localfind(*locals, base->str); + ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX)); + } else { + cgexpr_int(c, 0); + } + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); /* BX = hi */ + ins1(c, A_POPQ, areg(D_CX)); /* CX = lo */ + ins1(c, A_POPQ, areg(D_AX)); /* AX = base */ + ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); /* AX = base+lo */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); + ins2(c, A_SUBQ, areg(D_CX), areg(D_BX)); /* BX = hi-lo */ + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); + ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); + break; + } + /* struct literal initialiser: field-by-field store. */ + if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu + && lu->kind == TY_STRUCT) { + for (Node *f = n->rhs->list; f; f = f->next) { + /* find offset of this field */ + u64 foff = 0; + int fsz = 8; + Type *ft = NULL; + for (Tfield *fl = lu->fields; fl; fl = fl->next) { + if (strcmp(fl->name, f->str) == 0) { + foff = fl->offset; + fsz = (int)(fl->type ? fl->type->size : 8); + ft = fl->type; + break; + } + } + /* Tagged-union field: synthesise tag from + * f->lhs's static type and store value bytes + * (1 or 2 words for ≤8B/str variants). */ + Type *fu = (ft && ft->kind == TY_NAMED) + ? ft->under : ft; + if (fu && fu->kind == TY_TAGGED) { + Type *vt = f->lhs ? f->lhs->type : NULL; + int tag = cg_tag_for_variant(fu, vt); + cgexpr(c, f->lhs, *locals); + if (type_isstr(vt)) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + (int)foff + 8)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, off + (int)foff + 16)); + } else if (type_istagged(vt)) { + /* forwarding a tagged value */ + ins2(c, A_MOVQ, areg(D_DX), + amem(D_BP, off + (int)foff + 8)); + if (fu->size > 16) + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, off + (int)foff + 16)); + /* AX already holds the tag */ + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + (int)foff + 0)); + continue; + } else { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + (int)foff + 8)); + } + ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), + amem(D_BP, off + (int)foff + 0)); + continue; + } + cgexpr(c, f->lhs, *locals); + int op = A_MOVQ; + if (fsz == 1) op = A_MOVB; + else if (fsz == 4) op = A_MOVL; + ins2(c, op, areg(D_AX), + amem(D_BP, off + (int)foff)); + } + break; + } + if (n->rhs && sz == 8) { + cgexpr(c, n->rhs, *locals); + if (isf) { + int mov = isf32 ? A_MOVSS : A_MOVSD; + ins2(c, mov, areg(D_X0), amem(D_BP, off)); + } else { + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + } + } else if (sz == 8) { + ins2(c, A_MOVQ, aimm(0), amem(D_BP, off)); + } + /* arrays/slices left uninitialised — caller writes via index */ + break; + } + case N_RETURN: + /* run all defers in reverse before the actual return */ + for (int di = ndefers - 1; di >= 0; di--) + cgexpr(c, defers[di], *locals); + /* If the function returns a tagged union and the value is + * one of the variant types, wrap into (tag, value). If rhs + * already produces a tagged union (e.g. forwarding another + * fallible call), pass it through unchanged. + * + * Tagged-return ABI: AX=tag, DX=value0[, CX=value1]. CX is + * only meaningful when the union has a >8B variant (e.g. + * str, where ptr→DX and len→CX). */ + if (n->lhs && cg_ret_type) { + Type *rt = cg_ret_type; + if (rt->kind == TY_NAMED) rt = rt->under; + if (rt && rt->kind == TY_TAGGED) { + Type *vt = n->lhs->type; + cgexpr(c, n->lhs, *locals); + if (!type_istagged(vt)) { + int tag = cg_tag_for_variant(rt, vt); + if (type_isstr(vt)) { + /* AX=ptr, BX=len → DX=ptr, + * CX=len, AX=tag. Move BX + * before AX since the tag + * MOVQ trashes AX. */ + ins2(c, A_MOVQ, areg(D_BX), + areg(D_CX)); + ins2(c, A_MOVQ, areg(D_AX), + areg(D_DX)); + } else { + ins2(c, A_MOVQ, areg(D_AX), + areg(D_DX)); + } + ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), + areg(D_AX)); + } + ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); + ins1(c, A_POPQ, areg(D_BP)); + ins0(c, A_RET); + break; + } + } + if (n->lhs && node_isstr(n->lhs)) { + cgexpr(c, n->lhs, *locals); /* AX=ptr, BX=len */ + ins2(c, A_MOVQ, areg(D_BX), areg(D_DX)); + ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); + ins1(c, A_POPQ, areg(D_BP)); + ins0(c, A_RET); + break; + } + if (n->lhs && n->lhs->kind == N_TUPLE) { + /* small tuples (≤2 elems, ≤8B each) return in AX:DX */ + Node *e0 = n->lhs->list; + Node *e1 = e0 ? e0->next : NULL; + if (e1 && e1->next == NULL) { + cgexpr(c, e1, *locals); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, e0, *locals); + ins1(c, A_POPQ, areg(D_DX)); + } else { + /* >2-tuple not yet implemented; fall back to first elem */ + if (e0) cgexpr(c, e0, *locals); + else cgexpr_int(c, 0); + } + } else if (n->lhs) { + cgexpr(c, n->lhs, *locals); + } else { + cgexpr_int(c, 0); + } + ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); + ins1(c, A_POPQ, areg(D_BP)); + ins0(c, A_RET); + break; + case N_IF: { + char *els = mklabel(c, "else"); + char *end = mklabel(c, "end"); + cgexpr(c, n->cond, *locals); + ins2(c, A_CMPQ, aimm(0), areg(D_AX)); + ins1(c, A_JE, abranch(n->els ? els : end)); + cgstmt(c, n->body, locals, frame); + if (n->els) { + ins1(c, A_JMP, abranch(end)); + label(c, els); + cgstmt(c, n->els, locals, frame); + } + label(c, end); + break; + } + case N_FORRANGE: { + /* Lower `for (let x .. s) body` (and its tuple-destructure + * cousin `for (let (a, b) .. s)`). The body is wrapped in a + * counted loop driven by stack-spilled `_i`/`_len`. Each + * iteration computes the element address `s.ptr + i*esz` + * and either loads the whole element into the named local + * or pulls each tuple field into its respective local. */ + Node *slc = n->lhs; + Type *st = slc ? slc->type : NULL; + Type *u = (st && st->kind == TY_NAMED) ? st->under : st; + int esz = (u && u->sub) ? (int)u->sub->size : 1; + Type *etu = (u && u->sub && u->sub->kind == TY_NAMED) + ? u->sub->under : (u ? u->sub : NULL); + int destruct = (n->list != NULL); + + /* allocate temp slots: _i (8B), _len (8B) */ + char *iname = aprintf(c->a, ".rgi_%d", c->labelseq++); + char *lname = aprintf(c->a, ".rgl_%d", c->labelseq++); + int ioff = localoff(c, locals, iname, 8, frame); + int loff = localoff(c, locals, lname, 8, frame); + + /* allocate per-name slots */ + struct { int off, sz, foff; int signed_field; } binds[8] = {0}; + int nbinds = 0; + if (destruct) { + Tparam *tp = (etu && etu->kind == TY_TUPLE) ? + etu->params : NULL; + int field_off = 0; + for (Node *nm = n->list; nm && nbinds < 8; nm = nm->next) { + int fsz = tp && tp->type ? (int)tp->type->size : 8; + int slot_sz = (fsz < 8) ? 8 : fsz; + binds[nbinds].sz = fsz; + binds[nbinds].foff = field_off; + binds[nbinds].signed_field = tp && tp->type && ( + tp->type->kind == TY_I8 || + tp->type->kind == TY_I16 || + tp->type->kind == TY_I32); + binds[nbinds].off = localoff(c, locals, + nm->str, slot_sz, frame); + field_off += fsz; + nbinds++; + if (tp) tp = tp->next; + } + } else { + int slot_sz = (esz < 8) ? 8 : esz; + binds[0].off = localoff(c, locals, n->str, slot_sz, frame); + binds[0].sz = esz; + binds[0].foff = 0; + binds[0].signed_field = u && u->sub && ( + u->sub->kind == TY_I8 || + u->sub->kind == TY_I16 || + u->sub->kind == TY_I32); + nbinds = 1; + } + + ins2(c, A_MOVQ, aimm(0), amem(D_BP, ioff)); + if (u && (u->kind == TY_SLICE || u->kind == TY_STR) + && slc->kind == N_IDENT) { + int boff = localfind(*locals, slc->str); + ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff)); + } else if (u && u->kind == TY_ARRAY) { + ins2(c, A_MOVQ, aimm((long long)u->alen), + amem(D_BP, loff)); + } else { + cgexpr(c, slc, *locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff)); + } + + char *loop = mklabel(c, "rloop"); + char *end = mklabel(c, "rend"); + if (nloops < LOOP_MAX) { + loop_cont[nloops] = loop; + loop_brk[nloops] = end; + nloops++; + } + label(c, loop); + 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)); + /* compute element base: s.ptr + i*esz → BX */ + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + if (slc->kind == N_IDENT && u && u->kind == TY_ARRAY) { + int boff = localfind(*locals, slc->str); + ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX)); + } else if (slc->kind == N_IDENT) { + int boff = localfind(*locals, slc->str); + ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); + } + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + /* load each binding from BX + foff into its slot */ + for (int b = 0; b < nbinds; b++) { + int op = A_MOVQ; + if (binds[b].sz == 1) op = A_MOVZBQ; + else if (binds[b].sz == 4) + op = binds[b].signed_field ? A_MOVSXD : A_MOVL; + ins2(c, op, amem(D_BX, binds[b].foff), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, binds[b].off)); + } + cgstmt(c, n->body, locals, frame); + ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff)); + ins1(c, A_JMP, abranch(loop)); + label(c, end); + if (nloops > 0) nloops--; + break; + } + case N_FOR: { + char *loop = mklabel(c, "loop"); + char *end = mklabel(c, "endloop"); + 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)); + } + if (nloops < LOOP_MAX) { + loop_cont[nloops] = loop; + loop_brk[nloops] = end; + nloops++; + } + cgstmt(c, n->body, locals, frame); + if (nloops > 0) nloops--; + if (n->rhs) cgexpr(c, n->rhs, *locals); + ins1(c, A_JMP, abranch(loop)); + label(c, end); + break; + } + case N_MLET: { + /* eval rhs; first result in AX, second in DX. Bind both. */ + cgexpr(c, n->rhs, *locals); + ins1(c, A_PUSHQ, areg(D_DX)); /* save 2nd while we store 1st */ + Node *l0 = n->list; + Node *l1 = l0 ? l0->next : NULL; + if (l0) { + int off = localoff(c, locals, l0->str, 8, frame); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + } + ins1(c, A_POPQ, areg(D_DX)); + if (l1) { + int off = localoff(c, locals, l1->str, 8, frame); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off)); + } + break; + } + case N_MASSIGN: { + cgexpr(c, n->rhs, *locals); + ins1(c, A_PUSHQ, areg(D_DX)); + Node *l0 = n->list; + Node *l1 = l0 ? l0->next : NULL; + if (l0 && l0->kind == N_IDENT) { + int off = localfind(*locals, l0->str); + if (off != 0) + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); + } + ins1(c, A_POPQ, areg(D_DX)); + if (l1 && l1->kind == N_IDENT) { + int off = localfind(*locals, l1->str); + if (off != 0) + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off)); + } + break; + } + case N_DEFER: + if (ndefers < DEFER_MAX) { + defers[ndefers++] = n->lhs; + } + break; + case N_BREAK: + if (nloops > 0) + ins1(c, A_JMP, abranch(loop_brk[nloops - 1])); + break; + case N_CONTINUE: + if (nloops > 0) + ins1(c, A_JMP, abranch(loop_cont[nloops - 1])); + break; + case N_SWITCH: { + /* Lower to a chain of compares. Scrutinee lands in a fresh + * local slot so case bodies can spill through SP without + * losing it. Cases are tried top-to-bottom; a `case:` arm + * with no exprs is the default and runs after all named + * arms fail. */ + char *swname = aprintf(c->a, ".sw_%d", c->labelseq++); + int sloff = localoff(c, locals, swname, 8, frame); + cgexpr(c, n->lhs, *locals); /* AX = scrutinee */ + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sloff)); + char *end = mklabel(c, "swend"); + Node *defcase = NULL; + for (Node *cs = n->list; cs; cs = cs->next) { + if (cs->list == NULL) { + defcase = cs; /* save for last */ + continue; + } + char *body = mklabel(c, "swcase"); + char *next = mklabel(c, "swnext"); + for (Node *e = cs->list; e; e = e->next) { + cgexpr(c, e, *locals); /* AX = case-expr */ + ins2(c, A_MOVQ, amem(D_BP, sloff), areg(D_BX)); + ins2(c, A_CMPQ, areg(D_BX), areg(D_AX)); + ins1(c, A_JE, abranch(body)); + } + ins1(c, A_JMP, abranch(next)); + label(c, body); + cgstmt(c, cs->body, locals, frame); + ins1(c, A_JMP, abranch(end)); + label(c, next); + } + if (defcase) + cgstmt(c, defcase->body, locals, frame); + label(c, end); + break; + } + default: + break; + } +} + +static void +cgfn(Cg *c, FILE *out, Node *fn) +{ + if (fn->body == NULL) return; /* extern decl, no body */ + + /* fresh per-fn state */ + c->head = c->tail = NULL; + c->fnname = fn->str; + c->labelseq = 0; + cg_stack_arg_cursor = 0; + ndefers = 0; + nloops = 0; + cg_ret_type = fn->type ? fn->type->ret : NULL; + + int frame = 0; + Local *locals = NULL; + cg_frame = &frame; + + /* TEXT directive comes first; framesize is filled at the end. */ + Prog *text = newprog(c, A_TEXT); + text->to = asym(fn->str); + text->from.offset = 0; /* framesize patched below */ + emit(c, text); + + /* prologue */ + ins1(c, A_PUSHQ, areg(D_BP)); + ins2(c, A_MOVQ, areg(D_SP), areg(D_BP)); + Prog *subsp = newprog(c, A_SUBQ); + subsp->from = aimm(0); + subsp->to = areg(D_SP); + emit(c, subsp); + + /* spill incoming arg registers to local slots. Slice params + * occupy 24 bytes; float params land in XMM0..7 (counted + * separately from integer DI/SI/DX/CX/R8/R9). */ + int argi = 0, fargi = 0; + Tparam *tp = fn->type ? fn->type->params : NULL; + for (Node *p = fn->list; p; p = p->next) { + if (p->str == NULL || strcmp(p->str, "...") == 0) { + if (tp) tp = tp->next; + continue; + } + Type *pt = tp ? tp->type : NULL; + Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt; + int slice = (pu && pu->kind == TY_SLICE); + int is_str = type_isstr(pt); + int is_struct = pu && pu->kind == TY_STRUCT && pu->size <= 16; + int tagged_sz = tagged_arg_size(pt); + int is_tagged = tagged_sz > 0; + int isf = cg_isfloat(pt); + + /* Args overflowing register classes live at positive offsets + * from BP (16 + i*8). We register them as Locals at those + * offsets, no spill needed. */ + int struct_eb = is_struct ? ((pu->size > 8) ? 2 : 1) : 0; + int tagged_eb = is_tagged ? (tagged_sz / 8) : 0; + int eightbytes = slice ? 3 : + (is_str ? 2 : + (is_struct ? struct_eb : + (is_tagged ? tagged_eb : 1))); + int regs_left = isf ? (8 - fargi) : (6 - argi); + if (regs_left >= eightbytes) { + int sz = slice ? 24 : (is_str ? 16 : + (is_struct ? (int)pu->size : + (is_tagged ? tagged_sz : 8))); + int off = localoff(c, &locals, p->str, sz, &frame); + if (slice || is_str || is_struct || is_tagged) { + for (int k = 0; k < eightbytes; k++, argi++) + ins2(c, A_MOVQ, + areg(sysv_argregs[argi]), + amem(D_BP, off + k * 8)); + } else if (isf) { + int mov = type_isf32(pt) ? A_MOVSS : A_MOVSD; + ins2(c, mov, + areg(sysv_fargregs[fargi]), + amem(D_BP, off)); + fargi++; + } else { + ins2(c, A_MOVQ, + areg(sysv_argregs[argi]), + amem(D_BP, off)); + argi++; + } + } else { + /* stack-spilled. Access in place via positive BP offset. */ + static int stack_arg_off; + (void)stack_arg_off; + Local *l = amalloc(c->a, sizeof *l); + l->name = p->str; + /* spilled args layout: each takes 8B (ptr/len/etc); we + * only support the simple case of plain int/float here. */ + extern int cg_stack_arg_cursor; + l->off = 16 + cg_stack_arg_cursor * 8; + cg_stack_arg_cursor += eightbytes; + l->next = locals; + locals = l; + } + if (tp) tp = tp->next; + } + + cgstmt(c, fn->body, &locals, &frame); + + /* implicit return for void functions */ + if (c->tail->as != A_RET) { + for (int di = ndefers - 1; di >= 0; di--) + cgexpr(c, defers[di], locals); + ins2(c, A_MOVQ, aimm(0), areg(D_AX)); + ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); + ins1(c, A_POPQ, areg(D_BP)); + ins0(c, A_RET); + } + + /* round frame to 16; patch SUBQ */ + if (frame & 15) frame = (frame + 15) & ~15; + subsp->from.offset = frame; + text->from.offset = frame; + + txt_emit(out, c->head); +} + +/* Emit DATA directives for top-level `def` constants whose value is + * an integer/rune literal. The 6a side stores the bytes inside .text + * and accesses are RIP-relative. + */ +static void +emit_defs(Cg *c, FILE *out, Node *file) +{ + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_DEF || d->rhs == NULL) continue; + u64 v = 0; + if (d->rhs->kind == N_INTLIT || d->rhs->kind == N_RUNELIT) { + v = d->rhs->uval; + } else if (d->rhs->kind == N_TRUE) { + v = 1; + } else { + continue; /* skip non-integer-literal defs */ + } + fprintf(out, "DATA %s(SB),\"", d->str); + for (int i = 0; i < 8; i++) { + unsigned b = (unsigned)((v >> (i * 8)) & 0xff); + if (b == '"' || b == '\\') + fprintf(out, "\\%c", b); + else if (b < 0x20 || b >= 0x7f) + fprintf(out, "\\x%02x", b); + else + fputc(b, out); + } + fputs("\"\n", out); + } + (void)c; +} + +/* Collect str-typed `def`s so cgexpr N_IDENT can splice them inline. + * Walks past any leading cast on the rhs (e.g. `def x: error = "x": error;` + * shows up as N_CAST wrapping an N_STRLIT). */ +static void +sdef_collect(Cg *c, Node *file) +{ + (void)c; + sdefs = NULL; + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_DEF || d->rhs == NULL) continue; + Node *r = d->rhs; + while (r && r->kind == N_CAST) r = r->lhs; + if (r == NULL || r->kind != N_STRLIT) continue; + Sdef *s = amalloc(c->a, sizeof *s); + s->name = d->str; + s->bytes = r->str; + s->len = r->strlen; + s->next = sdefs; + sdefs = s; + } +} + +void +cg_file(Cg *c, FILE *out, Node *file) +{ + if (file == NULL || file->kind != N_FILE) return; + ffi_collect(c, file); + sdef_collect(c, file); + strlits = NULL; + strlit_seq = 0; + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_FNDECL) continue; + cgfn(c, out, d); + } + emit_data(c, out); + emit_defs(c, out, file); +} + +void peephole(Cg *c) { (void)c; } +void regalloc_init(Cg *c) { (void)c; } diff --git a/cmd/6c/gc.h b/cmd/6c/gc.h new file mode 100644 index 00000000..62eb9ced --- /dev/null +++ b/cmd/6c/gc.h @@ -0,0 +1,56 @@ +/* + * gc.h — 6c-private header: Prog/Adr structs, scratch register set, + * stack-frame state. Plan 9 cmd/6c/gc.h shape, trimmed. + */ +#ifndef SIX_GC_H +#define SIX_GC_H + +#include "ww.h" +#include "6.out.h" + +typedef struct Prog Prog; +typedef struct Adr Adr; + +/* one operand: register, immediate, indirect, or symbolic. */ +struct Adr { + int type; /* D_AX, D_CONST, D_INDIR, ... */ + int reg; /* base register for D_INDIR */ + long long offset; /* immediate value or memory displacement */ + const char *sym; /* symbol name for D_EXTERN/D_BRANCH */ +}; + +struct Prog { + int as; /* opcode (A_MOVQ, ...) */ + Adr from; /* source operand */ + Adr to; /* destination operand (Plan 9 order) */ + int line; + const char *label; /* if non-NULL, this prog is preceded by label: */ + Prog *link; +}; + +/* per-fn codegen state */ +typedef struct Cg Cg; +struct Cg { + Arena *a; + Prog *head, *tail; + const char *fnname; + int framesize; /* bytes of locals; 16-byte aligned */ + int curoff; /* current top of locals */ + Scope *locals; /* (name → offset) tracked via Sym */ + int labelseq; +}; + +/* cgen.c */ +void cg_init(Cg*, Arena*); +void cg_file(Cg*, FILE *out, Node *file); +Prog *newprog(Cg*, int op); +void emit(Cg*, Prog*); + +/* txt.c */ +void txt_emit(FILE*, Prog *head); + +/* swt.c, peep.c, reg.c — placeholders for now */ +void peephole(Cg*); +void regalloc_init(Cg*); + +#endif diff --git a/cmd/6c/main.c b/cmd/6c/main.c new file mode 100644 index 00000000..edaccb30 --- /dev/null +++ b/cmd/6c/main.c @@ -0,0 +1,93 @@ +/* + * 6c — amd64 compiler driver. Reads a .ww source file, runs the + * libwwc frontend (lex → parse → check), then walks the typed AST + * via cgen.c and writes Plan 9-flavoured amd64 asm to stdout (or + * the file given by -o). + */ +#include "gc.h" +#include +#include +#include + +static int +slurp(const char *path, char **outbuf, u64 *outlen) +{ + FILE *f = fopen(path, "rb"); + if (f == NULL) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (b == NULL) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (u64)n; + return 0; +} + +int +main(int argc, char **argv) +{ + const char *src = NULL; + const char *out = NULL; + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (strcmp(a, "-o") == 0 && i + 1 < argc) { + out = argv[++i]; + } else if (a[0] == '-') { + fprintf(stderr, "6c: unknown flag %s\n", a); + return 2; + } else if (src == NULL) { + src = a; + } else { + fprintf(stderr, "6c: only one input supported\n"); + return 2; + } + } + if (src == NULL) { + fputs("usage: 6c [-o out.s] file.ww\n", stderr); + return 2; + } + + char *buf; + u64 len; + if (slurp(src, &buf, &len) < 0) { + fprintf(stderr, "6c: %s: cannot read\n", src); + return 1; + } + + Arena *a = newarena(); + Lex l; + Parser p; + Checker c; + Cg cg; + + lexinit(&l, a, src, buf, len); + parserinit(&p, a, &l); + Node *file = parsefile(&p); + if (l.errs || p.errs) return 1; + + check_init(&c, a); + check_file(&c, file); + if (c.errs) return 1; + + FILE *of = stdout; + if (out) { + of = fopen(out, "wb"); + if (of == NULL) { + fprintf(stderr, "6c: cannot open %s\n", out); + return 1; + } + } + + cg_init(&cg, a); + cg_file(&cg, of, file); + + if (of != stdout) fclose(of); + freearena(a); + free(buf); + return 0; +} diff --git a/cmd/6c/peep.c b/cmd/6c/peep.c new file mode 100644 index 00000000..837a2f52 --- /dev/null +++ b/cmd/6c/peep.c @@ -0,0 +1,8 @@ +/* + * peep.c — peephole pass. Currently a no-op; reserved for the kind of + * cleanup Plan 9 6c does (folding adjacent moves, removing redundant + * compares). Wire in `peephole(c)` from cgen.c after the main walk. + */ +#include "gc.h" + +void peep_run(Cg *c) { (void)c; } diff --git a/cmd/6c/reg.c b/cmd/6c/reg.c new file mode 100644 index 00000000..48bd95ba --- /dev/null +++ b/cmd/6c/reg.c @@ -0,0 +1,9 @@ +/* + * reg.c — register allocator. The current cgen pins everything to AX + * with BX as a scratch top-of-stack — no real allocation. This file + * is the seam where a linear-scan or graph-colouring pass would land + * later; today it's empty. + */ +#include "gc.h" + +void reg_run(Cg *c) { (void)c; } diff --git a/cmd/6c/swt.c b/cmd/6c/swt.c new file mode 100644 index 00000000..df8fe5d8 --- /dev/null +++ b/cmd/6c/swt.c @@ -0,0 +1,8 @@ +/* + * swt.c — switch-statement lowering. Stub for now: cgen falls + * through to a no-op for N_SWITCH. When we add a real lowering, it + * will live here, mirroring Plan 9 6c's pswt.c. + */ +#include "gc.h" + +void swt_lower(Cg *c, Node *n) { (void)c; (void)n; } diff --git a/cmd/6c/txt.c b/cmd/6c/txt.c new file mode 100644 index 00000000..471ad045 --- /dev/null +++ b/cmd/6c/txt.c @@ -0,0 +1,189 @@ +/* + * txt.c — print a Prog list as Plan 9-flavoured amd64 asm text. + * + * Format we emit (and that 6a expects): + * TEXT name + * MOVQ $1, AX + * MOVQ AX, name(SB) ; extern symbol + * MOVQ off(BP), AX ; local + * CMPQ AX, $0 + * JE label + * RET + * label: + * + * Operand order is Plan 9-ish: source first, dest second. (Same as + * AT&T; the convention diverges from Plan 9 only on a few items we + * don't yet emit.) + */ +#include "gc.h" +#include + +const char * +anames(int op) +{ + switch (op) { + case A_NOP: return "NOP"; + case A_TEXT: return "TEXT"; + case A_DATA: return "DATA"; + case A_GLOBL: return "GLOBL"; + case A_END: return "END"; + case A_MOVQ: return "MOVQ"; + case A_MOVL: return "MOVL"; + case A_MOVB: return "MOVB"; + case A_MOVZBQ: return "MOVZBQ"; + case A_MOVSXD: return "MOVSXD"; + case A_MOVSD: return "MOVSD"; + case A_ADDSD: return "ADDSD"; + case A_SUBSD: return "SUBSD"; + case A_MULSD: return "MULSD"; + case A_DIVSD: return "DIVSD"; + case A_UCOMISD: return "UCOMISD"; + case A_CVTTSD2SI: return "CVTTSD2SI"; + case A_CVTSI2SD:return "CVTSI2SD"; + case A_MOVSS: return "MOVSS"; + case A_ADDSS: return "ADDSS"; + case A_SUBSS: return "SUBSS"; + case A_MULSS: return "MULSS"; + case A_DIVSS: return "DIVSS"; + case A_UCOMISS: return "UCOMISS"; + case A_CVTTSS2SI: return "CVTTSS2SI"; + case A_CVTSI2SS:return "CVTSI2SS"; + case A_CVTSD2SS:return "CVTSD2SS"; + case A_CVTSS2SD:return "CVTSS2SD"; + case A_ADDQ: return "ADDQ"; + case A_SUBQ: return "SUBQ"; + case A_IMULQ: return "IMULQ"; + case A_IDIVQ: return "IDIVQ"; + case A_DIVQ: return "DIVQ"; + case A_NEGQ: return "NEGQ"; + case A_NOTQ: return "NOTQ"; + case A_ANDQ: return "ANDQ"; + case A_ORQ: return "ORQ"; + case A_XORQ: return "XORQ"; + case A_SHLQ: return "SHLQ"; + case A_SHRQ: return "SHRQ"; + case A_CMPQ: return "CMPQ"; + case A_PUSHQ: return "PUSHQ"; + case A_POPQ: return "POPQ"; + case A_LEAQ: return "LEAQ"; + case A_CALL: return "CALL"; + case A_RET: return "RET"; + case A_JMP: return "JMP"; + case A_JE: return "JE"; + case A_JNE: return "JNE"; + case A_JL: return "JL"; + case A_JLE: return "JLE"; + case A_JG: return "JG"; + case A_JGE: return "JGE"; + case A_JB: return "JB"; + case A_JBE: return "JBE"; + case A_JA: return "JA"; + case A_JAE: return "JAE"; + case A_JZ: return "JZ"; + case A_JNZ: return "JNZ"; + case A_SYSCALL: return "SYSCALL"; + } + return "??"; +} + +const char * +rnames(int r) +{ + switch (r) { + case D_AX: return "AX"; case D_CX: return "CX"; + case D_DX: return "DX"; case D_BX: return "BX"; + case D_SP: return "SP"; case D_BP: return "BP"; + case D_SI: return "SI"; case D_DI: return "DI"; + case D_R8: return "R8"; case D_R9: return "R9"; + case D_R10: return "R10"; case D_R11: return "R11"; + case D_R12: return "R12"; case D_R13: return "R13"; + case D_R14: return "R14"; case D_R15: return "R15"; + case D_X0: return "X0"; case D_X1: return "X1"; + case D_X2: return "X2"; case D_X3: return "X3"; + case D_X4: return "X4"; case D_X5: return "X5"; + case D_X6: return "X6"; case D_X7: return "X7"; + case D_X8: return "X8"; case D_X9: return "X9"; + case D_X10: return "X10"; case D_X11: return "X11"; + case D_X12: return "X12"; case D_X13: return "X13"; + case D_X14: return "X14"; case D_X15: return "X15"; + case D_PSP: return "SP"; case D_PFP: return "FP"; case D_PSB: return "SB"; + } + return "?"; +} + +static void +prAdr(FILE *f, Adr a) +{ + switch (a.type) { + case D_NONE: fputs("?", f); break; + case D_CONST: + fprintf(f, "$%lld", a.offset); + break; + case D_INDIR: + if (a.offset) + fprintf(f, "%lld(%s)", a.offset, rnames(a.reg)); + else + fprintf(f, "(%s)", rnames(a.reg)); + break; + case D_BRANCH: + fputs(a.sym ? a.sym : "?", f); + break; + case D_EXTERN: + fprintf(f, "%s(SB)", a.sym ? a.sym : "?"); + break; + default: + fputs(rnames(a.type), f); + } +} + +void +txt_emit(FILE *f, Prog *head) +{ + for (Prog *p = head; p; p = p->link) { + if (p->label) { + fprintf(f, "%s:\n", p->label); + if (p->as == A_NOP) continue; + } + switch (p->as) { + case A_NOP: + break; + case A_TEXT: + fprintf(f, "TEXT %s,$%lld\n", + p->to.sym ? p->to.sym : "?", + p->from.offset); + break; + case A_RET: + fputs("\tRET\n", f); + break; + case A_SYSCALL: + fputs("\tSYSCALL\n", f); + break; + case A_NEGQ: + case A_NOTQ: + case A_PUSHQ: + case A_POPQ: + case A_IDIVQ: + case A_DIVQ: + fprintf(f, "\t%s\t", anames(p->as)); + prAdr(f, p->to); + fputc('\n', f); + break; + case A_CALL: + case A_JMP: + case A_JE: case A_JNE: + case A_JL: case A_JLE: case A_JG: case A_JGE: + case A_JB: case A_JBE: case A_JA: case A_JAE: + case A_JZ: case A_JNZ: + fprintf(f, "\t%s\t", anames(p->as)); + prAdr(f, p->to); + fputc('\n', f); + break; + default: + fprintf(f, "\t%s\t", anames(p->as)); + prAdr(f, p->from); + fputs(", ", f); + prAdr(f, p->to); + fputc('\n', f); + } + } +} diff --git a/cmd/6l/l.h b/cmd/6l/l.h new file mode 100644 index 00000000..a3c86f83 --- /dev/null +++ b/cmd/6l/l.h @@ -0,0 +1,74 @@ +/* + * l.h — 6l-private header. Loads relocatable ELF64 .o files (the + * format produced by 6a) and links them into a static executable. + * + * No archives yet (phase 8). No dynamic linking ever. + */ +#ifndef SIX_L_H +#define SIX_L_H + +#include +#include +#include + +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +typedef struct Lsym Lsym; +typedef struct Lrel Lrel; +typedef struct Lobj Lobj; +typedef struct Lnk Lnk; + +struct Lsym { + const char *name; + u64 val; /* offset within combined .text once linked */ + int defined; /* 1 if a Lobj defines this symbol */ + Lobj *owner; + int idx_in_owner; + Lsym *next; +}; + +struct Lrel { + u64 off; /* offset within combined .text */ + int kind; /* R_X86_64_* */ + Lsym *sym; + i64 addend; + Lrel *next; +}; + +struct Lobj { + const char *path; + u8 *buf; /* mmapped or read-in object bytes */ + u64 len; + u64 text_off; /* offset of .text in combined output */ + u64 text_size; + Lobj *next; +}; + +struct Lnk { + Lobj *objs; + Lsym *syms; + Lrel *rels; + u8 *text; /* combined .text */ + u64 textcap, textlen; + int errs; +}; + +/* obj.c */ +int l_load(Lnk*, const char *path); +/* sym.c */ +Lsym *l_intern(Lnk*, const char *name); +Lsym *l_lookup(Lnk*, const char *name); +/* pass.c */ +int l_resolve(Lnk*); +int l_relocate(Lnk*, u64 base); +/* out.c */ +int l_emit_elf(Lnk*, FILE *out, u64 base, u64 entry); + +#endif diff --git a/cmd/6l/main.c b/cmd/6l/main.c new file mode 100644 index 00000000..b03b3075 --- /dev/null +++ b/cmd/6l/main.c @@ -0,0 +1,73 @@ +/* + * 6l — amd64 static linker. Reads relocatable ELF .o files, resolves, + * relocates, writes a static ELF executable. + * + * 6l -o out file1.o file2.o ... + * + * The first symbol named "_start" defined among the inputs becomes + * the entry point. If none is found, fall back to "main". + */ +#include "l.h" +#include +#include +#include + +int +main(int argc, char **argv) +{ + const char *out = NULL; + const char **inputs = calloc(argc, sizeof *inputs); + int ninputs = 0; + u64 base = 0x400000; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { + out = argv[++i]; + } else if (argv[i][0] == '-') { + fprintf(stderr, "6l: unknown flag %s\n", argv[i]); + return 2; + } else { + inputs[ninputs++] = argv[i]; + } + } + if (out == NULL || ninputs == 0) { + fputs("usage: 6l -o exe file1.o [file2.o...]\n", stderr); + return 2; + } + + Lnk l = {0}; + /* Seed the symbol table with the entry point so archive pulls + * include the .o that defines it. Without this, a libwwrt.a + * containing start.o is silently skipped if no user .o + * references _start, and the entry falls back to main — which + * has no proper exit path. */ + (void)l_intern(&l, "_start"); + for (int i = 0; i < ninputs; i++) { + if (l_load(&l, inputs[i]) != 0) return 1; + } + if (l_resolve(&l) != 0) return 1; + if (l_relocate(&l, base + 0x1000) != 0) return 1; + + Lsym *entry = l_lookup(&l, "_start"); + if (entry == NULL || !entry->defined) entry = l_lookup(&l, "main"); + if (entry == NULL || !entry->defined) { + fprintf(stderr, "6l: no _start or main symbol defined\n"); + return 1; + } + + FILE *f = fopen(out, "wb"); + if (f == NULL) { + fprintf(stderr, "6l: cannot open %s\n", out); + return 1; + } + int rc = l_emit_elf(&l, f, base, base + 0x1000 + entry->val); + fclose(f); + if (rc == 0) { + /* chmod +x */ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "chmod +x %s", out); + (void)system(cmd); + } + free(inputs); + return rc; +} diff --git a/cmd/6l/obj.c b/cmd/6l/obj.c new file mode 100644 index 00000000..3ecfadae --- /dev/null +++ b/cmd/6l/obj.c @@ -0,0 +1,324 @@ +/* + * obj.c — load an ELF64 relocatable object emitted by 6a, append its + * .text bytes to the combined image, and pull its symbols and + * relocations into the global tables (with offsets adjusted to the + * combined section). + */ +#include "l.h" +#include +#include + +#define ET_REL 1 +#define EM_X86_64 62 + +#define SHT_PROGBITS 1 +#define SHT_SYMTAB 2 +#define SHT_STRTAB 3 +#define SHT_RELA 4 + +#pragma pack(push, 1) +typedef struct { + u8 e_ident[16]; + u16 e_type, e_machine; + u32 e_version; + u64 e_entry, e_phoff, e_shoff; + u32 e_flags; + u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx; +} Ehdr; + +typedef struct { + u32 sh_name, sh_type; + u64 sh_flags, sh_addr, sh_offset, sh_size; + u32 sh_link, sh_info; + u64 sh_addralign, sh_entsize; +} Shdr; + +typedef struct { + u32 st_name; + u8 st_info, st_other; + u16 st_shndx; + u64 st_value, st_size; +} Sym64; + +typedef struct { + u64 r_offset; + u64 r_info; + i64 r_addend; +} Rela64; +#pragma pack(pop) + +#define ELF64_R_SYM(i) ((u32)((i) >> 32)) +#define ELF64_R_TYPE(i) ((u32)((i) & 0xffffffff)) +#define ELF64_ST_TYPE(i) ((i) & 0xf) +#define ELF64_ST_BIND(i) ((i) >> 4) + +static int +read_all(const char *path, u8 **out, u64 *len) +{ + FILE *f = fopen(path, "rb"); + if (f == NULL) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + u8 *b = malloc((size_t)n); + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + fclose(f); + *out = b; + *len = (u64)n; + return 0; +} + +static void +emit_text(Lnk *l, const u8 *src, u64 n) +{ + if (l->textlen + n > l->textcap) { + u64 nc = l->textcap ? l->textcap * 2 : 4096; + while (nc < l->textlen + n) nc *= 2; + l->text = realloc(l->text, nc); + l->textcap = nc; + } + memcpy(l->text + l->textlen, src, n); + l->textlen += n; +} + +/* Internal: load a single ELF .o image already in memory. The caller + * gives us the bytes (we own them) and a path tag for diagnostics. + * If the bytes look like an archive (magic "!\n") we recurse + * over each member instead. + */ +static int load_image(Lnk *l, const char *path, u8 *buf, u64 len); + +static u64 +ar_field(const u8 *p, int n) +{ + /* decimal field, space-padded */ + u64 v = 0; + for (int i = 0; i < n; i++) { + if (p[i] >= '0' && p[i] <= '9') v = v * 10 + (p[i] - '0'); + else if (p[i] == ' ') break; + else if (p[i] == 0) break; + } + return v; +} + +/* Read an ELF .o image's globally-defined symbol names without + * actually appending it to the link. Returns a heap-allocated + * NULL-terminated array; caller frees the array (not the strings, + * which point into the .o image and must remain alive). + */ +static char ** +elf_globals(const u8 *buf, u64 len) +{ + if (len < sizeof(Ehdr)) return NULL; + Ehdr *eh = (Ehdr *)buf; + if (memcmp(eh->e_ident, "\x7f""ELF", 4) != 0) return NULL; + Shdr *sh = (Shdr *)(buf + eh->e_shoff); + int idx_text = -1, idx_symtab = -1; + const char *shstr = (const char *)(buf + sh[eh->e_shstrndx].sh_offset); + for (u16 i = 0; i < eh->e_shnum; i++) { + if (sh[i].sh_type == SHT_PROGBITS && + strcmp(shstr + sh[i].sh_name, ".text") == 0) + idx_text = i; + else if (sh[i].sh_type == SHT_SYMTAB) + idx_symtab = i; + } + if (idx_text < 0 || idx_symtab < 0) return NULL; + int idx_strtab = sh[idx_symtab].sh_link; + Sym64 *symtab = (Sym64 *)(buf + sh[idx_symtab].sh_offset); + u64 nsyms = sh[idx_symtab].sh_size / sizeof(Sym64); + const char *str = (const char *)(buf + sh[idx_strtab].sh_offset); + + char **out = calloc(nsyms + 1, sizeof *out); + int n = 0; + for (u64 i = 1; i < nsyms; i++) { + if (symtab[i].st_shndx == 0) continue; + if ((symtab[i].st_info >> 4) != 1) continue; /* STB_GLOBAL */ + if ((int)symtab[i].st_shndx != idx_text) continue; + out[n++] = strdup(str + symtab[i].st_name); + } + out[n] = NULL; + return out; +} + +typedef struct ArMember ArMember; +struct ArMember { + u8 *data; /* heap copy; freed if never loaded */ + u64 size; + char **defs; /* NULL-terminated list of defined globals */ + int loaded; + ArMember *next; +}; + +static int +member_defines_undef(Lnk *l, ArMember *m) +{ + if (m->defs == NULL) return 0; + for (int i = 0; m->defs[i]; i++) { + Lsym *s = l_lookup(l, m->defs[i]); + if (s != NULL && !s->defined) return 1; + } + return 0; +} + +static int +load_archive(Lnk *l, const char *path, u8 *buf, u64 len) +{ + /* Pass 1: index members. We copy each member's bytes (cheap; few + * tens of KB per stdlib module) so the archive buffer can be + * freed once we're done indexing. */ + ArMember *head = NULL, *tail = NULL; + u64 pos = 8; /* past "!\n" */ + while (pos + 60 <= len) { + const u8 *hdr = buf + pos; + u64 size = ar_field(hdr + 48, 10); + u64 hdr_end = pos + 60; + if (hdr_end + size > len) break; + if (hdr[0] != '/' && hdr[0] != 0 && hdr[0] != ' ') { + ArMember *m = calloc(1, sizeof *m); + m->size = size; + m->data = malloc((size_t)size); + memcpy(m->data, buf + hdr_end, (size_t)size); + m->defs = elf_globals(m->data, size); + if (head == NULL) head = m; + else tail->next = m; + tail = m; + } + pos = hdr_end + size; + if (size & 1) pos++; + } + free(buf); + + /* Pass 2: iteratively pull members that define a currently- + * undefined symbol. Each pull may introduce new undefs, so loop. */ + int changed = 1; + while (changed) { + changed = 0; + for (ArMember *m = head; m; m = m->next) { + if (m->loaded) continue; + if (!member_defines_undef(l, m)) continue; + u8 *copy = malloc((size_t)m->size); + memcpy(copy, m->data, m->size); + if (load_image(l, path, copy, m->size) == 0) { + m->loaded = 1; + changed = 1; + } + } + } + + /* Free unloaded members; loaded ones had their bytes consumed + * by load_image (which took the copy). */ + while (head) { + ArMember *next = head->next; + free(head->data); + if (head->defs) { + for (int i = 0; head->defs[i]; i++) free(head->defs[i]); + free(head->defs); + } + free(head); + head = next; + } + return 0; +} + +int +l_load(Lnk *l, const char *path) +{ + u8 *buf; + u64 len; + if (read_all(path, &buf, &len) < 0) return -1; + if (len >= 8 && memcmp(buf, "!\n", 8) == 0) + return load_archive(l, path, buf, len); + return load_image(l, path, buf, len); +} + +static int +load_image(Lnk *l, const char *path, u8 *buf, u64 len) +{ + if (len < sizeof(Ehdr)) { free(buf); return -1; } + Ehdr *eh = (Ehdr *)buf; + if (memcmp(eh->e_ident, "\x7f""ELF", 4) != 0 || eh->e_ident[4] != 2 + || eh->e_machine != EM_X86_64 || eh->e_type != ET_REL) { + fprintf(stderr, "6l: %s: not an amd64 ELF64 relocatable\n", path); + free(buf); + return -1; + } + Shdr *sh = (Shdr *)(buf + eh->e_shoff); + if (eh->e_shstrndx >= eh->e_shnum) { free(buf); return -1; } + const char *shstr = (const char *)(buf + sh[eh->e_shstrndx].sh_offset); + + /* find .text, .symtab, .strtab, .rela.text */ + int idx_text = -1, idx_symtab = -1, idx_strtab = -1, idx_rela = -1; + for (u16 i = 0; i < eh->e_shnum; i++) { + const char *nm = shstr + sh[i].sh_name; + if (sh[i].sh_type == SHT_PROGBITS && strcmp(nm, ".text") == 0) + idx_text = i; + else if (sh[i].sh_type == SHT_SYMTAB) + idx_symtab = i; + else if (sh[i].sh_type == SHT_RELA && strcmp(nm, ".rela.text") == 0) + idx_rela = i; + } + if (idx_text < 0 || idx_symtab < 0) { + fprintf(stderr, "6l: %s: missing .text or .symtab\n", path); + free(buf); + return -1; + } + idx_strtab = sh[idx_symtab].sh_link; + + Lobj *ob = calloc(1, sizeof *ob); + ob->path = strdup(path); + ob->buf = buf; + ob->len = len; + ob->text_off = l->textlen; + ob->text_size = sh[idx_text].sh_size; + ob->next = l->objs; + l->objs = ob; + + /* append .text */ + emit_text(l, buf + sh[idx_text].sh_offset, sh[idx_text].sh_size); + + /* per-object: load symbols */ + Sym64 *symtab = (Sym64 *)(buf + sh[idx_symtab].sh_offset); + u64 nsyms = sh[idx_symtab].sh_size / sizeof(Sym64); + const char *str = (const char *)(buf + sh[idx_strtab].sh_offset); + + /* map per-object sym index → global Lsym */ + Lsym **map = calloc(nsyms, sizeof *map); + for (u64 i = 1; i < nsyms; i++) { + const char *nm = str + symtab[i].st_name; + if (nm[0] == '\0') continue; + Lsym *gs = l_intern(l, nm); + if (symtab[i].st_shndx != 0 /* SHN_UNDEF */ + && symtab[i].st_shndx == idx_text) { + if (gs->defined) { + fprintf(stderr, "6l: %s: duplicate symbol %s\n", + path, nm); + l->errs++; + } else { + gs->defined = 1; + gs->owner = ob; + gs->idx_in_owner = (int)i; + gs->val = ob->text_off + symtab[i].st_value; + } + } + map[i] = gs; + } + + /* per-object: collect relocations */ + if (idx_rela >= 0) { + Rela64 *rt = (Rela64 *)(buf + sh[idx_rela].sh_offset); + u64 nrel = sh[idx_rela].sh_size / sizeof(Rela64); + for (u64 i = 0; i < nrel; i++) { + Lrel *r = calloc(1, sizeof *r); + r->off = ob->text_off + rt[i].r_offset; + r->kind = (int)ELF64_R_TYPE(rt[i].r_info); + u32 sidx = ELF64_R_SYM(rt[i].r_info); + r->sym = (sidx < nsyms) ? map[sidx] : NULL; + r->addend = rt[i].r_addend; + r->next = l->rels; + l->rels = r; + } + } + free(map); + return 0; +} diff --git a/cmd/6l/out.c b/cmd/6l/out.c new file mode 100644 index 00000000..0cadcf25 --- /dev/null +++ b/cmd/6l/out.c @@ -0,0 +1,89 @@ +/* + * out.c — emit a static ELF64 executable. + * + * Layout (file order): + * [0..64) ELF header + * [64..120) program header (one PT_LOAD) + * [120..0x1000) zero pad + * [0x1000..) .text bytes + * + * The single PT_LOAD covers the whole file, R+X. No interpreter, + * no dynamic, no .bss yet. Entry point is the address of the + * symbol named "_start" (or whatever main supplies via -e). + */ +#include "l.h" +#include +#include +#include + +#define ET_EXEC 2 +#define EM_X86_64 62 +#define EV_CURRENT 1 +#define ELFCLASS64 2 +#define ELFDATA2LSB 1 + +#define PT_LOAD 1 +#define PF_X 1 +#define PF_W 2 +#define PF_R 4 + +#pragma pack(push, 1) +typedef struct { + u8 e_ident[16]; + u16 e_type, e_machine; + u32 e_version; + u64 e_entry, e_phoff, e_shoff; + u32 e_flags; + u16 e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx; +} Ehdr; + +typedef struct { + u32 p_type, p_flags; + u64 p_offset, p_vaddr, p_paddr; + u64 p_filesz, p_memsz, p_align; +} Phdr; +#pragma pack(pop) + +int +l_emit_elf(Lnk *l, FILE *f, u64 base, u64 entry) +{ + const u64 text_off = 0x1000; + const u64 text_va = base + text_off; + const u64 filesz = text_off + l->textlen; + + Ehdr eh = {0}; + memcpy(eh.e_ident, "\x7f""ELF", 4); + eh.e_ident[4] = ELFCLASS64; + eh.e_ident[5] = ELFDATA2LSB; + eh.e_ident[6] = EV_CURRENT; + eh.e_type = ET_EXEC; + eh.e_machine = EM_X86_64; + eh.e_version = EV_CURRENT; + eh.e_entry = entry; + eh.e_phoff = sizeof(Ehdr); + eh.e_ehsize = sizeof(Ehdr); + eh.e_phentsize = sizeof(Phdr); + eh.e_phnum = 1; + (void)text_va; + + Phdr ph = {0}; + ph.p_type = PT_LOAD; + ph.p_flags = PF_R | PF_X; + ph.p_offset = 0; + ph.p_vaddr = base; + ph.p_paddr = base; + ph.p_filesz = filesz; + ph.p_memsz = filesz; + ph.p_align = 0x1000; + + fwrite(&eh, 1, sizeof eh, f); + fwrite(&ph, 1, sizeof ph, f); + + /* pad to text_off */ + long here = ftell(f); + for (long i = here; i < (long)text_off; i++) fputc(0, f); + + if (l->textlen) fwrite(l->text, 1, l->textlen, f); + + return 0; +} diff --git a/cmd/6l/pass.c b/cmd/6l/pass.c new file mode 100644 index 00000000..c4b35cec --- /dev/null +++ b/cmd/6l/pass.c @@ -0,0 +1,63 @@ +/* + * pass.c — resolution + relocation. After all objects are loaded: + * + * l_resolve : check that every symbol referenced by a relocation + * is defined somewhere. Errors get logged. + * l_relocate: with the final virtual base address known, walk the + * relocation list and patch the .text bytes in place. + * + * Supported relocation kinds: PC32 (2), PLT32 (4). Both are PC-relative + * 32-bit displacements; for static linking PLT32 collapses to PC32. + */ +#include "l.h" +#include +#include + +#define R_X86_64_PC32 2 +#define R_X86_64_PLT32 4 + +int +l_resolve(Lnk *l) +{ + for (Lrel *r = l->rels; r; r = r->next) { + if (r->sym == NULL) continue; + if (!r->sym->defined) { + fprintf(stderr, "6l: undefined reference to '%s'\n", + r->sym->name); + l->errs++; + } + } + return l->errs; +} + +static void +patch_u32(u8 *p, u32 v) +{ + p[0] = (u8)(v & 0xff); + p[1] = (u8)((v >> 8) & 0xff); + p[2] = (u8)((v >> 16) & 0xff); + p[3] = (u8)((v >> 24) & 0xff); +} + +int +l_relocate(Lnk *l, u64 base) +{ + for (Lrel *r = l->rels; r; r = r->next) { + if (r->sym == NULL || !r->sym->defined) continue; + switch (r->kind) { + case R_X86_64_PC32: + case R_X86_64_PLT32: { + u64 site = base + r->off; + i64 target = (i64)(base + r->sym->val); + i64 rel = target - (i64)site + r->addend; + patch_u32(l->text + r->off, (u32)(i32)rel); + break; + } + default: + fprintf(stderr, "6l: unsupported reloc kind %d\n", + r->kind); + l->errs++; + } + } + return l->errs; +} diff --git a/cmd/6l/sym.c b/cmd/6l/sym.c new file mode 100644 index 00000000..5f467348 --- /dev/null +++ b/cmd/6l/sym.c @@ -0,0 +1,27 @@ +/* + * sym.c — global symbol table for the linker. Plain singly-linked + * list; usually a few hundred entries, hashing isn't worth it yet. + */ +#include "l.h" +#include +#include + +Lsym * +l_intern(Lnk *l, const char *name) +{ + for (Lsym *s = l->syms; s; s = s->next) + if (strcmp(s->name, name) == 0) return s; + Lsym *s = calloc(1, sizeof *s); + s->name = strdup(name); + s->next = l->syms; + l->syms = s; + return s; +} + +Lsym * +l_lookup(Lnk *l, const char *name) +{ + for (Lsym *s = l->syms; s; s = s->next) + if (strcmp(s->name, name) == 0) return s; + return NULL; +} diff --git a/cmd/ww/main.c b/cmd/ww/main.c new file mode 100644 index 00000000..c31c05e2 --- /dev/null +++ b/cmd/ww/main.c @@ -0,0 +1,349 @@ +/* + * ww — the user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue. + * + * Pipeline: + * ww build foo.ww → 6c foo.ww > foo.s ; 6a foo.s > foo.o ; + * 6l -o foo foo.o + * ww run foo.ww → build then exec ./foo + * + * Tool paths default to siblings of $0 (so a fresh build runs out of + * out/bin/), and can be overridden with WW_6C / WW_6A / WW_6L. + */ +#include "ww.h" +#include +#include +#include +#include +#include + +static const char *usage = +"usage: ww [-V] [args...]\n" +" -V print version and exit\n" +" build compile module to a static binary\n" +" run build then exec\n" +" test build and run module tests\n" +" fmt reformat ww source\n" +" version print version and exit\n"; + +static char *self_dir; /* directory containing this binary */ + +static const char * +toolpath(const char *envvar, const char *name) +{ + const char *p = getenv(envvar); + if (p && p[0]) return p; + static char buf[1024]; + snprintf(buf, sizeof buf, "%s/%s", self_dir, name); + return strdup(buf); +} + +static int +run(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +/* Set of imported module paths, kept on the heap. Used to break + * cycles in `use` resolution. Linear because typical imports are + * a handful per build. */ +struct ImportSet { + char **paths; + int n, cap; +}; + +static int +import_seen(struct ImportSet *s, const char *path) +{ + for (int i = 0; i < s->n; i++) + if (strcmp(s->paths[i], path) == 0) return 1; + return 0; +} + +static void +import_add(struct ImportSet *s, const char *path) +{ + if (s->n + 1 > s->cap) { + s->cap = s->cap ? s->cap * 2 : 8; + s->paths = realloc(s->paths, s->cap * sizeof *s->paths); + } + s->paths[s->n++] = strdup(path); +} + +/* try /X.ww then /X/X.ww; return resolved path in `out` or 0. */ +static int +locate_import_in(const char *dir, const char *name, char *out, size_t outsz) +{ + snprintf(out, outsz, "%s/%s.ww", dir, name); + if (access(out, 0) == 0) return 1; + snprintf(out, outsz, "%s/%s/%s.ww", dir, name, name); + if (access(out, 0) == 0) return 1; + return 0; +} + +/* Walk a colon-separated dirlist trying to resolve `name`. Returns 1 + * on the first hit. */ +static int +locate_import(const char *dirs, const char *name, char *out, size_t outsz) +{ + const char *p = dirs; + while (*p) { + const char *e = strchr(p, ':'); + size_t n = e ? (size_t)(e - p) : strlen(p); + if (n > 0 && n < outsz) { + char dir[1024]; + if (n >= sizeof dir) n = sizeof dir - 1; + memcpy(dir, p, n); + dir[n] = '\0'; + if (locate_import_in(dir, name, out, outsz)) return 1; + } + if (!e) break; + p = e + 1; + } + return 0; +} + +/* Recursively expand `path`: for each top-level `use IDENT;` we find, + * resolve the import and expand it first, then append our own bytes. + * Already-visited paths are skipped. */ +static void +expand(FILE *out, const char *path, struct ImportSet *visited, + const char *libdir) +{ + /* Use the path as-is for cycle detection. Different syntactic + * paths to the same file would re-import, which is harmless given + * our flat-scope concatenation (duplicate decls would fail at + * check time, surfacing the issue). */ + if (import_seen(visited, path)) return; + import_add(visited, path); + + FILE *in = fopen(path, "rb"); + if (in == NULL) { + fprintf(stderr, "ww: cannot read %s\n", path); + return; + } + /* Scan once for `use X;` clauses, expand each. We keep the line + * format simple — leading whitespace + "use" + IDENT + optional + * dotted suffix + ";". Inside-comment occurrences would slip + * through, but ww source rarely puts that pattern in a comment. */ + char line[2048]; + while (fgets(line, sizeof line, in)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "use ", 4) != 0 && strncmp(p, "use\t", 4) != 0) + continue; + p += 4; + while (*p == ' ' || *p == '\t') p++; + char name[256] = {0}; + int j = 0; + while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') + || *p == '_' || *p == '.' || (*p >= '0' && *p <= '9')) + if (j + 1 < (int)sizeof name) name[j++] = *p++; + if (j == 0) continue; + char ipath[1024]; + if (!locate_import(libdir, name, ipath, sizeof ipath)) + continue; /* silently skip if not found */ + expand(out, ipath, visited, libdir); + } + + rewind(in); + int ch; + while ((ch = fgetc(in)) != EOF) fputc(ch, out); + fputc('\n', out); + fclose(in); +} + +static int +build_one(const char *src, const char *out, const char *extra_includes) +{ + const char *c6 = toolpath("WW_6C", "6c"); + const char *a6 = toolpath("WW_6A", "6a"); + const char *l6 = toolpath("WW_6L", "6l"); + const char *libdir = getenv("WW_LIB"); + if (libdir == NULL || libdir[0] == 0) { + static char libbuf[1024]; + snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir); + libdir = libbuf; + } + const char *srcdir = getenv("WW_SRCLIB"); + static char srcbuf[1024]; + if (srcdir == NULL || srcdir[0] == 0) { + /* in-tree default: ../../lib relative to bin/ */ + snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir); + if (access(srcbuf, 0) == 0) srcdir = srcbuf; + else if (access("lib", 0) == 0) srcdir = "lib"; + else srcdir = libdir; + } + + /* Compose the search path: any -I dirs first, then srcdir. + * locate_import walks them left-to-right. */ + static char searchpath[4096]; + if (extra_includes && extra_includes[0]) + snprintf(searchpath, sizeof searchpath, "%s:%s", extra_includes, srcdir); + else + snprintf(searchpath, sizeof searchpath, "%s", srcdir); + srcdir = searchpath; + + /* Strip extension to derive a stem; e.g. /tmp/foo.ww → /tmp/foo */ + char stem[1024]; + snprintf(stem, sizeof stem, "%s", src); + char *dot = strrchr(stem, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + char asmf[1024], obj[1024], combined[1024]; + snprintf(asmf, sizeof asmf, "%s.s", stem); + snprintf(obj, sizeof obj, "%s.o", stem); + snprintf(combined, sizeof combined, "%s.combined.ww", stem); + + /* Resolve `use X;` imports by concatenating sources into a temp + * file. The compiler then sees one flat source. */ + { + FILE *cf = fopen(combined, "wb"); + if (cf == NULL) { + fprintf(stderr, "ww: cannot open %s\n", combined); + return 1; + } + struct ImportSet visited = {0}; + expand(cf, src, &visited, srcdir); + fclose(cf); + for (int i = 0; i < visited.n; i++) free(visited.paths[i]); + free(visited.paths); + } + + char cmd[4096]; + snprintf(cmd, sizeof cmd, "%s -o %s %s", c6, asmf, combined); + if (run(cmd) != 0) { + fprintf(stderr, "ww: 6c failed\n"); + return 1; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf); + if (run(cmd) != 0) { + fprintf(stderr, "ww: 6a failed\n"); + return 1; + } + /* Link runtime: prefer libwwrt.a (selective archive pull) but + * fall back to start.o + syscall.o in the in-tree obj/ dir if + * we're running uninstalled. */ + char rtargs[2048] = {0}; + char path[1024]; + snprintf(path, sizeof path, "%s/libwwrt.a", libdir); + if (access(path, 0) == 0) { + snprintf(rtargs, sizeof rtargs, "%s", path); + } else { + char a1[1024], a2[1024]; + snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir); + snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir); + snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2); + } + snprintf(cmd, sizeof cmd, "%s -o %s %s %s", l6, out, obj, rtargs); + if (run(cmd) != 0) { + fprintf(stderr, "ww: 6l failed\n"); + return 1; + } + return 0; +} + +static int +do_version(void) +{ + printf("ww %s\n", WW_VERSION); + return 0; +} + +static int +do_build(int argc, char **argv) +{ + const char *src = NULL; + char libs[2048] = {0}; + char incs[2048] = {0}; + for (int i = 0; i < argc; i++) { + if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { + char libpath[512]; + const char *libdir = getenv("WW_LIB"); + if (libdir == NULL) { + static char def[1024]; + snprintf(def, sizeof def, "%s/../lib", self_dir); + libdir = def; + } + snprintf(libpath, sizeof libpath, "%s/lib%s.a", + libdir, argv[i] + 2); + size_t n = strlen(libs); + snprintf(libs + n, sizeof libs - n, " %s", libpath); + } else if (strcmp(argv[i], "-I") == 0 && i + 1 < argc) { + size_t n = strlen(incs); + snprintf(incs + n, sizeof incs - n, + "%s%s", n ? ":" : "", argv[++i]); + } else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) { + size_t n = strlen(incs); + snprintf(incs + n, sizeof incs - n, + "%s%s", n ? ":" : "", argv[i] + 2); + } else if (src == NULL) { + src = argv[i]; + } + } + if (src == NULL) { fputs("ww build: missing source\n", stderr); return 2; } + char out[1024]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(out, sizeof out, "%s", base); + char *dot = strrchr(out, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + (void)libs; /* libs string is gathered; build_one currently + * always links libwwrt.a; -l support pending more + * glue between driver and 6l invocation. */ + return build_one(src, out, incs); +} + +static int +do_run(int argc, char **argv) +{ + if (argc < 1) { fputs("ww run: missing source\n", stderr); return 2; } + char tmp[1024]; + snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid()); + if (build_one(argv[0], tmp, "") != 0) return 1; + int rc = run(tmp); + unlink(tmp); + return rc; +} + +static int +do_test(int argc, char **argv) +{ + (void)argc; (void)argv; + fputs("ww: test: not implemented in this phase\n", stderr); + return 1; +} + +static int +do_fmt(int argc, char **argv) +{ + (void)argc; (void)argv; + fputs("ww: fmt: not implemented in this phase\n", stderr); + return 1; +} + +int +main(int argc, char **argv) +{ + if (argc >= 1) { + char buf[1024]; + snprintf(buf, sizeof buf, "%s", argv[0]); + self_dir = strdup(dirname(buf)); + } + if (argc < 2) { fputs(usage, stderr); return 2; } + const char *cmd = argv[1]; + if (strcmp(cmd, "-V") == 0 || strcmp(cmd, "version") == 0) + return do_version(); + if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) { + fputs(usage, stdout); return 0; + } + if (strcmp(cmd, "build") == 0) return do_build(argc - 2, argv + 2); + if (strcmp(cmd, "run") == 0) return do_run(argc - 2, argv + 2); + if (strcmp(cmd, "test") == 0) return do_test(argc - 2, argv + 2); + if (strcmp(cmd, "fmt") == 0) return do_fmt(argc - 2, argv + 2); + fprintf(stderr, "ww: unknown subcommand: %s\n", cmd); + fputs(usage, stderr); + return 2; +} diff --git a/cmd/wwc/ast.c b/cmd/wwc/ast.c new file mode 100644 index 00000000..3c4ef184 --- /dev/null +++ b/cmd/wwc/ast.c @@ -0,0 +1,189 @@ +/* + * ast.c — Node constructor + s-expression printer. + * + * Constructor zeroes everything past kind/pos. Printer is rigid and + * deterministic so golden tests can diff. One node per logical line, + * children indented by 2 spaces. + */ +#include "ww.h" +#include + +Node * +newnode(Arena *a, Nkind k, Pos p) +{ + Node *n = amalloc(a, sizeof *n); + n->kind = k; + n->pos = p; + return n; +} + +static const char * +nkname(Nkind k) +{ + switch (k) { + case N_NONE: return "none"; + case N_INTLIT: return "int"; + case N_FLOATLIT: return "float"; + case N_STRLIT: return "str"; + case N_RUNELIT: return "rune"; + case N_TRUE: return "true"; + case N_FALSE: return "false"; + case N_NIL: return "nil"; + case N_IDENT: return "id"; + case N_BIN: return "bin"; + case N_UN: return "un"; + case N_CALL: return "call"; + case N_INDEX: return "index"; + case N_DOT: return "dot"; + case N_CAST: return "cast"; + case N_STRUCTLIT: return "structlit"; + case N_ARRLIT: return "arrlit"; + case N_FIELD: return "field"; + case N_ASSIGN: return "assign"; + case N_ALLOC: return "alloc"; + case N_FREE: return "free"; + case N_RECV: return "recv"; + case N_SLICE: return "slice"; + case N_SPREAD: return "spread"; + case N_BLOCK: return "block"; + case N_EXPRSTMT: return "exprstmt"; + case N_LET: return "let"; + case N_RETURN: return "return"; + case N_IF: return "if"; + case N_FOR: return "for"; + case N_FORRANGE: return "forrange"; + case N_DEFER: return "defer"; + case N_BREAK: return "break"; + case N_CONTINUE: return "continue"; + case N_SWITCH: return "switch"; + case N_CASE: return "case"; + case N_FILE: return "file"; + case N_USE: return "use"; + case N_DEF: return "def"; + case N_TYPEDECL: return "typedecl"; + case N_FNDECL: return "fn"; + case N_PARAM: return "param"; + case N_TNAME: return "tname"; + case N_TPTR: return "tptr"; + case N_TSLICE: return "tslice"; + case N_TARRAY: return "tarray"; + case N_TFN: return "tfn"; + case N_TSTRUCT: return "tstruct"; + case N_TFIELD: return "tfield"; + case N_TCHAN: return "tchan"; + case N_ATTR: return "attr"; + case N_TTUPLE: return "ttuple"; + case N_TTAGGED: return "ttagged"; + case N_TUPLE: return "tuple"; + case N_MATCH: return "match"; + case N_MCASE: return "mcase"; + case N_TRYPROP: return "tryprop"; + case N_TRYUNW: return "tryunw"; + case N_MLET: return "mlet"; + case N_MASSIGN: return "massign"; + case N_LAST: return "last"; + } + return "?"; +} + +static void +indent(FILE *f, int d) +{ + for (int i = 0; i < d; i++) fputs(" ", f); +} + +static void +printq(FILE *f, const char *s) +{ + fputc('"', f); + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + switch (c) { + case '"': fputs("\\\"", f); break; + case '\\': fputs("\\\\", f); break; + case '\n': fputs("\\n", f); break; + case '\t': fputs("\\t", f); break; + default: + if (c < 0x20) fprintf(f, "\\x%02x", c); + else fputc(c, f); + } + } + fputc('"', f); +} + +static void pr(FILE*, Node*, int); + +static void +prlist(FILE *f, const char *tag, Node *head, int d) +{ + indent(f, d); + fprintf(f, "(%s\n", tag); + for (Node *n = head; n; n = n->next) + pr(f, n, d + 1); + indent(f, d); + fputs(")\n", f); +} + +static void +pr(FILE *f, Node *n, int d) +{ + if (n == NULL) { + indent(f, d); fputs("()\n", f); return; + } + indent(f, d); + fprintf(f, "(%s", nkname(n->kind)); + switch (n->kind) { + case N_INTLIT: + fprintf(f, " %llu", (unsigned long long)n->uval); + break; + case N_FLOATLIT: + fprintf(f, " %g", n->fval); + break; + case N_RUNELIT: + fprintf(f, " %llu", (unsigned long long)n->uval); + break; + case N_STRLIT: + case N_IDENT: + case N_USE: + case N_DOT: + case N_DEF: + case N_TYPEDECL: + case N_FNDECL: + case N_PARAM: + case N_LET: + case N_TNAME: + case N_TFIELD: + case N_FIELD: + case N_ATTR: + if (n->str) { fputc(' ', f); printq(f, n->str); } + break; + case N_BIN: + case N_UN: + case N_ASSIGN: + fprintf(f, " %s", tokname(n->op)); + break; + default: break; + } + if (n->kind == N_FNDECL && n->export) + fputs(" export", f); + if (n->kind == N_DEF && n->export) + fputs(" export", f); + if (n->kind == N_TYPEDECL && n->export) + fputs(" export", f); + fputc('\n', f); + if (n->attr) + prlist(f, "@", n->attr, d + 1); + if (n->lhs) pr(f, n->lhs, d + 1); + if (n->rhs) pr(f, n->rhs, d + 1); + if (n->cond) pr(f, n->cond, d + 1); + if (n->body) pr(f, n->body, d + 1); + if (n->els) pr(f, n->els, d + 1); + if (n->list) prlist(f, "list", n->list, d + 1); + indent(f, d); fputs(")\n", f); +} + +void +astprint(FILE *f, Node *n) +{ + pr(f, n, 0); +} diff --git a/cmd/wwc/check.c b/cmd/wwc/check.c new file mode 100644 index 00000000..c0e31515 --- /dev/null +++ b/cmd/wwc/check.c @@ -0,0 +1,945 @@ +/* + * check.c — name resolution + type checking pass. + * + * Two-stage: + * 1) collect: walk top-level decls and install Syms with stub types. + * 2) resolve: expand types, check fn bodies and def initialisers. + * + * Errors do not stop the walk — we keep going so the user gets many + * diagnostics from one run. Nodes get their resolved Type attached. + */ +#include "ww.h" +#include + +static void cstmt(Checker*, Node*); +static Type *cexpr(Checker*, Node*); +static Type *resolve_type(Checker*, Node*); + +static Type * +err(Checker *c, Pos p, const char *fmt, ...) +{ + (void)c; + va_list ap; + fprintf(errout ? errout : stderr, + "%s:%d:%d: error: ", p.file ? p.file : "?", p.line, p.col); + va_start(ap, fmt); + vfprintf(errout ? errout : stderr, fmt, ap); + va_end(ap); + fputc('\n', errout ? errout : stderr); + c->errs++; + return ty_err; +} + +static Type * +lookup_builtin(const char *name) +{ + if (strcmp(name, "void") == 0) return ty_void; + if (strcmp(name, "bool") == 0) return ty_bool; + if (strcmp(name, "rune") == 0) return ty_rune; + if (strcmp(name, "i8") == 0) return ty_i8; + if (strcmp(name, "i16") == 0) return ty_i16; + if (strcmp(name, "i32") == 0) return ty_i32; + if (strcmp(name, "i64") == 0) return ty_i64; + if (strcmp(name, "u8") == 0) return ty_u8; + if (strcmp(name, "u16") == 0) return ty_u16; + if (strcmp(name, "u32") == 0) return ty_u32; + if (strcmp(name, "u64") == 0) return ty_u64; + if (strcmp(name, "int") == 0) return ty_int; + if (strcmp(name, "uint") == 0) return ty_uint; + if (strcmp(name, "uintptr") == 0) return ty_uintptr; + if (strcmp(name, "f32") == 0) return ty_f32; + if (strcmp(name, "f64") == 0) return ty_f64; + if (strcmp(name, "str") == 0) return ty_str; + return NULL; +} + +static Type * +resolve_typename(Checker *c, Node *n) +{ + const char *nm = n->str; + Type *bi = lookup_builtin(nm); + if (bi) return bi; + Sym *s = scope_lookup(c->cur, nm); + if (s == NULL && nm) { + /* module-qualified: io.stream → strip the last dot prefix + * and look up the leaf if `io` is a `use`-imported name. */ + const char *dot = strrchr(nm, '.'); + if (dot) { + char head[128] = {0}; + size_t hl = (size_t)(dot - nm); + if (hl < sizeof head) memcpy(head, nm, hl); + Sym *m = scope_lookup(c->cur, head); + if (m && m->kind == SK_USE) + s = scope_lookup(c->cur, dot + 1); + } + } + if (s == NULL || s->kind != SK_TYPE) + return err(c, n->pos, "unknown type '%s'", nm); + return s->type; +} + +static Type * +resolve_type(Checker *c, Node *n) +{ + if (n == NULL) return ty_void; + switch (n->kind) { + case N_TNAME: + return resolve_typename(c, n); + case N_TPTR: + return type_ptr(c->a, resolve_type(c, n->lhs)); + case N_TSLICE: + return type_slice(c->a, resolve_type(c, n->lhs)); + case N_TARRAY: { + u64 len = 0; + if (n->rhs && n->rhs->kind == N_INTLIT) + len = n->rhs->uval; + 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: + return type_chan(c->a, resolve_type(c, n->lhs)); + case N_TTUPLE: { + Type *t = newtype(c->a, TY_TUPLE); + Tparam *head = NULL, *tail = NULL; + u64 sz = 0, al = 1; + for (Node *e = n->list; e; e = e->next) { + Tparam *tp = amalloc(c->a, sizeof *tp); + tp->type = resolve_type(c, e); + if (tp->type && tp->type->align > al) al = tp->type->align; + if (tp->type) sz += tp->type->size; + if (head == NULL) head = tp; + else tail->next = tp; + tail = tp; + } + t->params = head; + t->size = sz; + t->align = al; + return t; + } + case N_TTAGGED: { + /* (T1 | T2 | ...) — tag (8B) followed by the largest variant. */ + Type *t = newtype(c->a, TY_TAGGED); + Tparam *head = NULL, *tail = NULL; + u64 maxsz = 0, al = 8; + for (Node *e = n->list; e; e = e->next) { + Tparam *tp = amalloc(c->a, sizeof *tp); + tp->type = resolve_type(c, e); + if (tp->type && tp->type->size > maxsz) maxsz = tp->type->size; + if (tp->type && tp->type->align > al) al = tp->type->align; + if (head == NULL) head = tp; + else tail->next = tp; + tail = tp; + } + t->params = head; + t->size = 8 + maxsz; + t->align = al; + return t; + } + case N_TFN: { + Type *t = newtype(c->a, TY_FN); + t->ret = resolve_type(c, n->lhs); + t->size = 8; + t->align = 8; + Tparam *head = NULL, *tail = NULL; + for (Node *p = n->list; p; p = p->next) { + if (strcmp(p->str ? p->str : "", "...") == 0) { + t->variadic = 1; + continue; + } + Tparam *tp = amalloc(c->a, sizeof *tp); + tp->name = p->str; + tp->type = resolve_type(c, p->lhs); + if (head == NULL) head = tp; + else tail->next = tp; + tail = tp; + } + t->params = head; + return t; + } + case N_TSTRUCT: { + Type *t = newtype(c->a, TY_STRUCT); + Tfield *head = NULL, *tail = NULL; + u64 off = 0, maxalign = 1; + for (Node *f = n->list; f; f = f->next) { + Tfield *tf = amalloc(c->a, sizeof *tf); + tf->name = f->str; + tf->type = resolve_type(c, f->lhs); + if (tf->type->align > maxalign) maxalign = tf->type->align; + off = (off + tf->type->align - 1) & ~(tf->type->align - 1); + tf->offset = off; + off += tf->type->size; + if (head == NULL) head = tf; + else tail->next = tf; + tail = tf; + } + t->fields = head; + t->align = maxalign; + t->size = (off + maxalign - 1) & ~(maxalign - 1); + return t; + } + default: + return err(c, n->pos, "expected type expression"); + } +} + +/* ---- expressions -------------------------------------------------- */ + +static Type * +unify_arith(Checker *c, Pos p, Type *a, Type *b) +{ + if (a == ty_err || b == ty_err) return ty_err; + /* untyped + untyped → untyped (prefer float over int) */ + if (type_isuntyped(a) && type_isuntyped(b)) { + if (a->kind == TY_UNTYPED_FLOAT || b->kind == TY_UNTYPED_FLOAT) + return ty_untyped_float; + return ty_untyped_int; + } + /* untyped + typed → typed (if assignable) */ + if (type_isuntyped(a) && type_assignable(b, a)) return b; + if (type_isuntyped(b) && type_assignable(a, b)) return a; + if (type_eq(a, b)) return a; + return err(c, p, "operands have differing types %s and %s", + type_name(c->a, a), type_name(c->a, b)); +} + +static Type * +cbinop(Checker *c, Node *n) +{ + Type *l = cexpr(c, n->lhs); + Type *r = cexpr(c, n->rhs); + switch (n->op) { + case TK_PLUS: case TK_MINUS: case TK_STAR: case TK_SLASH: + case TK_PERCENT: + /* pointer arithmetic: ptr ± int → ptr; ptr - ptr → int */ + if ((n->op == TK_PLUS || n->op == TK_MINUS) + && l && l->kind == TY_PTR && type_isint(r)) + return l; + if (n->op == TK_PLUS && type_isint(l) && r && r->kind == TY_PTR) + return r; + if (n->op == TK_MINUS && l && r && l->kind == TY_PTR + && r->kind == TY_PTR) + return ty_i64; + if (!type_isnum(l) || !type_isnum(r)) + return err(c, n->pos, "arithmetic on non-numeric type"); + return unify_arith(c, n->pos, l, r); + case TK_AMP: case TK_PIPE: case TK_CARET: case TK_LSHIFT: + case TK_RSHIFT: + if (!type_isint(l) || !type_isint(r)) + return err(c, n->pos, "bitwise on non-integer type"); + return unify_arith(c, n->pos, l, r); + case TK_EQ: case TK_NEQ: + (void)unify_arith(c, n->pos, l, r); + return ty_bool; + case TK_LT: case TK_LE: case TK_GT: case TK_GE: + if (!type_isnum(l) || !type_isnum(r)) + err(c, n->pos, "ordered comparison on non-numeric"); + (void)unify_arith(c, n->pos, l, r); + return ty_bool; + case TK_AND: case TK_OR: + if (!(l == ty_bool || l == ty_untyped_bool || l == ty_err)) + err(c, n->pos, "left of %s is not bool", tokname(n->op)); + if (!(r == ty_bool || r == ty_untyped_bool || r == ty_err)) + err(c, n->pos, "right of %s is not bool", tokname(n->op)); + return ty_bool; + default: + return err(c, n->pos, "unsupported binary op %s", tokname(n->op)); + } +} + +static Type * +cunop(Checker *c, Node *n) +{ + Type *t = cexpr(c, n->lhs); + switch (n->op) { + case TK_MINUS: case TK_PLUS: + if (!type_isnum(t)) + return err(c, n->pos, "%s on non-numeric", tokname(n->op)); + return t; + case TK_NOT: + if (!(t == ty_bool || t == ty_untyped_bool || t == ty_err)) + err(c, n->pos, "! on non-bool"); + return ty_bool; + case TK_TILDE: + if (!type_isint(t)) + return err(c, n->pos, "~ on non-integer"); + return t; + case TK_STAR: /* deref */ + if (t == ty_err) return ty_err; + if (t->kind != TY_PTR) + return err(c, n->pos, "cannot deref non-pointer %s", + type_name(c->a, t)); + return t->sub; + case TK_AMP: /* address-of */ + return type_ptr(c->a, t); + default: + return err(c, n->pos, "unsupported unary %s", tokname(n->op)); + } +} + +static Type * +cexpr(Checker *c, Node *n) +{ + if (n == NULL) return ty_err; + switch (n->kind) { + case N_INTLIT: + if (n->tsuffix) { + Type *t = lookup_builtin(n->tsuffix); + n->type = t ? t : ty_untyped_int; + } else { + n->type = ty_untyped_int; + } + return n->type; + case N_FLOATLIT: + if (n->tsuffix) { + Type *t = lookup_builtin(n->tsuffix); + n->type = t ? t : ty_untyped_float; + } else { + n->type = ty_untyped_float; + } + return n->type; + case N_STRLIT: n->type = ty_untyped_str; return n->type; + case N_RUNELIT: n->type = ty_untyped_rune; return n->type; + case N_TRUE: + 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: { + Sym *s = scope_lookup(c->cur, n->str); + if (s == NULL) + return n->type = err(c, n->pos, "undefined: %s", n->str); + /* SK_USE has no concrete value type; the only legal use is + * as the lhs of a DOT (module-qualified ref). Surface ty_err + * here; the DOT case below resolves the qualified symbol. */ + if (s->kind == SK_USE) + return n->type = ty_err; + n->type = s->type; + return s->type; + } + case N_PARAM: + return n->type = ty_err; /* shouldn't appear in expr ctx */ + case N_BIN: n->type = cbinop(c, n); return n->type; + case N_UN: n->type = cunop(c, n); return n->type; + case N_CAST: { + (void)cexpr(c, n->lhs); + n->type = resolve_type(c, n->rhs); + return n->type; + } + case N_DOT: { + /* module-qualified: lhs is an N_IDENT bound as SK_USE. + * Resolve to the symbol with the same leaf name. With + * driver-side concatenation, all symbols live in flat + * scope, so we lookup `n->str` directly. */ + if (n->lhs && n->lhs->kind == N_IDENT) { + Sym *ms = scope_lookup(c->cur, n->lhs->str); + if (ms && ms->kind == SK_USE) { + Sym *fs = scope_lookup(c->cur, n->str); + if (fs) + return n->type = fs->type; + /* Leaf isn't in scope here — treat as an + * external declaration. The codegen will + * still emit CALL/MOVQ by the leaf name; the + * linker fails if the symbol is truly + * missing. */ + return n->type = ty_err; + } + } + Type *base = cexpr(c, n->lhs); + if (base == NULL || base == ty_err) return n->type = ty_err; + Type *u = (base->kind == TY_NAMED) ? base->under : base; + if (u && u->kind == TY_PTR) u = u->sub; + if (u && u->kind == TY_NAMED) u = u->under; + /* built-in pseudo-fields on slice/str/array: .len, .cap, .ptr */ + if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY || + u->kind == TY_STR)) { + if (strcmp(n->str, "len") == 0) return n->type = ty_i32; + if (strcmp(n->str, "cap") == 0) return n->type = ty_i32; + if (strcmp(n->str, "ptr") == 0) { + Type *elem = (u->kind == TY_STR) ? ty_u8 : u->sub; + return n->type = type_ptr(c->a, elem); + } + } + if (u && u->kind == TY_STRUCT) { + for (Tfield *f = u->fields; f; f = f->next) + if (strcmp(f->name, n->str) == 0) + return n->type = f->type; + return n->type = err(c, n->pos, "no field '%s' in %s", + n->str, type_name(c->a, base)); + } + /* tuple positional access: t.0, t.1, ... */ + if (u && u->kind == TY_TUPLE && n->str) { + int idx = 0; + for (const char *q = n->str; *q; q++) { + if (*q < '0' || *q > '9') { idx = -1; break; } + idx = idx * 10 + (*q - '0'); + } + if (idx < 0) + return n->type = err(c, n->pos, + "tuple field must be numeric"); + Tparam *tp = u->params; + while (idx > 0 && tp) { tp = tp->next; idx--; } + if (tp == NULL) + return n->type = err(c, n->pos, + "tuple index out of range"); + return n->type = tp->type; + } + /* module-qualified: lhs is IDENT bound as SK_USE */ + return n->type = ty_err; + } + case N_INDEX: { + Type *base = cexpr(c, n->lhs); + Type *idx = cexpr(c, n->rhs); + if (idx != ty_err && !type_isint(idx)) + err(c, n->pos, "index must be integer"); + if (base == ty_err) return n->type = ty_err; + Type *u = (base->kind == TY_NAMED) ? base->under : base; + if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY)) + return n->type = u->sub; + if (u && u->kind == TY_STR) + return n->type = ty_u8; + if (u && u->kind == TY_PTR && u->sub && + (u->sub->kind == TY_ARRAY || u->sub->kind == TY_SLICE)) + return n->type = u->sub->sub; + /* C-style pointer indexing: p[i] → *(p+i) */ + if (u && u->kind == TY_PTR && u->sub) + return n->type = u->sub; + return n->type = err(c, n->pos, "indexing non-indexable %s", + type_name(c->a, base)); + } + case N_CALL: { + /* Hare-style builtins: len(x), append(s, v), alloc(...). + * Recognised by name with no scope binding; we type-check + * the args ourselves and skip the normal call resolution. */ + if (n->lhs && n->lhs->kind == N_IDENT && + n->lhs->str && strcmp(n->lhs->str, "len") == 0 && + n->list != NULL && n->list->next == NULL) { + (void)cexpr(c, n->list); + n->type = ty_i32; + n->lhs->type = ty_err; /* mark builtin: no real symbol */ + 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) { + for (Node *a = n->list; a; a = a->next) + (void)cexpr(c, a); + 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, "alloc") == 0 && + n->list != NULL && n->list->next == NULL) { + Type *t = cexpr(c, n->list); + Type *def = type_default(t); + n->type = type_ptr(c->a, def ? def : 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, "free") == 0 && + n->list != NULL && n->list->next == NULL) { + (void)cexpr(c, n->list); + 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. */ + if (n->lhs && n->lhs->kind == N_IDENT && + n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 && + n->list && n->list->kind == N_ARRLIT && + n->list->list == NULL && + n->list->next && n->list->next->next == NULL) { + (void)cexpr(c, n->list->next); + n->type = type_slice(c->a, ty_u8); + n->lhs->type = ty_err; + return n->type; + } + Type *ft = cexpr(c, n->lhs); + if (ft == ty_err) { + /* Walk args anyway so cgen sees real types. The + * common case is a module-qualified call whose leaf + * isn't in this scope (raw 6c on a single file with + * `use mod;` but no driver concatenation). */ + for (Node *a = n->list; a; a = a->next) + (void)cexpr(c, a); + return n->type = ty_err; + } + Type *u = (ft->kind == TY_NAMED) ? ft->under : ft; + if (u == NULL || u->kind != TY_FN) + return n->type = err(c, n->pos, "calling non-function %s", + type_name(c->a, ft)); + Tparam *p = u->params; + for (Node *a = n->list; a; a = a->next) { + Type *at = cexpr(c, a); + if (p == NULL) { + if (!u->variadic) + err(c, n->pos, "too many arguments"); + continue; + } + if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err) + err(c, a->pos, "argument type %s not assignable to %s", + type_name(c->a, at), type_name(c->a, p->type)); + p = p->next; + } + if (p != NULL) + err(c, n->pos, "not enough arguments"); + return n->type = u->ret ? u->ret : ty_void; + } + case N_ASSIGN: { + Type *l = cexpr(c, n->lhs); + Type *r = cexpr(c, n->rhs); + if (l != ty_err && r != ty_err && !type_assignable(l, r)) + err(c, n->pos, "cannot assign %s to %s", + type_name(c->a, r), type_name(c->a, l)); + return n->type = l; + } + case N_STRUCTLIT: { + /* lhs may be an N_IDENT (the bare type name) or a real type + * expression. Resolve via name lookup first; fall back to + * resolve_type for the synthetic-type-expr case. */ + Type *t = NULL; + if (n->lhs && n->lhs->kind == N_IDENT) { + Sym *s = scope_lookup(c->cur, n->lhs->str); + if (s == NULL || s->kind != SK_TYPE) + t = err(c, n->pos, "unknown struct type '%s'", + n->lhs->str); + else + t = s->type; + } else { + t = resolve_type(c, n->lhs); + } + Type *u = (t && t->kind == TY_NAMED) ? t->under : t; + for (Node *f = n->list; f; f = f->next) { + Type *vt = cexpr(c, f->lhs); + if (u && u->kind == TY_STRUCT) { + Tfield *match = NULL; + for (Tfield *fl = u->fields; fl; fl = fl->next) + if (strcmp(fl->name, f->str) == 0) { + match = fl; break; + } + if (match == NULL) + err(c, f->pos, "no field '%s' in %s", + f->str, type_name(c->a, t)); + else if (vt != ty_err && + !type_assignable(match->type, vt)) + err(c, f->pos, "field %s: %s not assignable to %s", + f->str, type_name(c->a, vt), + type_name(c->a, match->type)); + } + } + return n->type = t; + } + case N_ARRLIT: { + Type *elt = NULL; + u64 count = 0; + for (Node *e = n->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str && + strcmp(e->str, "...") == 0) + continue; + Type *t = cexpr(c, e); + if (elt == NULL) elt = type_default(t); + count++; + } + if (elt == NULL) elt = ty_i32; + return n->type = type_array(c->a, elt, count); + } + case N_SPREAD: + return n->type = cexpr(c, n->lhs); + case N_SLICE: { + Type *base = cexpr(c, n->lhs); + if (n->rhs) (void)cexpr(c, n->rhs); + if (n->cond) (void)cexpr(c, n->cond); + Type *u = (base && base->kind == TY_NAMED) ? base->under : base; + if (u && u->kind == TY_ARRAY) + return n->type = type_slice(c->a, u->sub); + if (u && u->kind == TY_SLICE) + return n->type = base; + if (u && u->kind == TY_STR) + return n->type = ty_str; + if (u && u->kind == TY_PTR && u->sub) + return n->type = type_slice(c->a, u->sub); + return n->type = err(c, n->pos, "cannot slice %s", + type_name(c->a, base)); + } + case N_RECV: { + Type *t = cexpr(c, n->lhs); + Type *u = (t && t->kind == TY_NAMED) ? t->under : t; + if (u && u->kind == TY_CHAN) return n->type = u->sub; + return n->type = err(c, n->pos, "<- expects chan, got %s", + type_name(c->a, t)); + } + case N_MATCH: { + Type *st = cexpr(c, n->lhs); + Type *u = (st && st->kind == TY_NAMED) ? st->under : st; + if (u == NULL || u->kind != TY_TAGGED) { + return n->type = err(c, n->pos, + "match on non-tagged-union %s", type_name(c->a, st)); + } + for (Node *cs = n->list; cs; cs = cs->next) { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + /* Resolve the case pattern's type so codegen can map it + * to the variant tag. Both `case T =>` and `case let v: T + * =>` get this — `case =>` (default) leaves cs->type NULL. + * For multi-pattern `case T1 | T2 =>` each alternative in + * cs->list also gets its type resolved in place. */ + if (cs->lhs) { + Type *vt = resolve_type(c, cs->lhs); + cs->type = vt; + for (Node *alt = cs->list; alt; alt = alt->next) + alt->type = resolve_type(c, alt); + if (cs->str && cs->str[0]) + scope_define(c->cur, cs->str, SK_VAR, vt, cs); + } + cstmt(c, cs->body); + c->cur = saved; + } + n->type = ty_void; + return n->type; + } + case N_TRYPROP: case N_TRYUNW: { + Type *t = cexpr(c, n->lhs); + Type *u = (t && t->kind == TY_NAMED) ? t->under : t; + if (u == NULL || u->kind != TY_TAGGED) { + return n->type = err(c, n->pos, + "%s on non-tagged-union %s", + n->kind == N_TRYPROP ? "?" : "!", + type_name(c->a, t)); + } + /* Convention: first variant is the success type. */ + Tparam *first = u->params; + return n->type = first ? first->type : ty_err; + } + case N_TUPLE: { + /* keep untyped element types; assignability is checked + * element-wise at the consumer (return / mlet / massign). */ + Type *t = newtype(c->a, TY_TUPLE); + Tparam *head = NULL, *tail = NULL; + for (Node *e = n->list; e; e = e->next) { + Tparam *tp = amalloc(c->a, sizeof *tp); + tp->type = cexpr(c, e); + if (head == NULL) head = tp; + else tail->next = tp; + tail = tp; + } + t->params = head; + return n->type = t; + } + default: + return n->type = err(c, n->pos, "internal: unhandled expr kind %d", + n->kind); + } +} + +/* ---- statements --------------------------------------------------- */ + +static void +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); + 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 && + !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); +} + +static void +cstmt(Checker *c, Node *n) +{ + if (n == NULL) return; + switch (n->kind) { + case N_BLOCK: { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + for (Node *s = n->list; s; s = s->next) + cstmt(c, s); + c->cur = saved; + break; + } + case N_EXPRSTMT: (void)cexpr(c, n->lhs); break; + case N_LET: clet(c, n); break; + case N_RETURN: { + Type *rt = n->lhs ? cexpr(c, n->lhs) : ty_void; + if (c->ret == NULL) { + err(c, n->pos, "return outside function"); + break; + } + if (c->ret == ty_void && n->lhs) + err(c, n->pos, "return value in void function"); + else if (c->ret != ty_void && rt != ty_err && c->ret != ty_err + && !type_assignable(c->ret, rt)) + err(c, n->pos, "return %s not assignable to %s", + type_name(c->a, rt), type_name(c->a, c->ret)); + break; + } + case N_IF: { + Type *ct = cexpr(c, n->cond); + if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool) + err(c, n->pos, "if condition must be bool, got %s", + type_name(c->a, ct)); + cstmt(c, n->body); + cstmt(c, n->els); + break; + } + case N_FORRANGE: { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + c->loops++; + Type *st = cexpr(c, n->lhs); + Type *u = (st && st->kind == TY_NAMED) ? st->under : st; + Type *elem = NULL; + if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY)) elem = u->sub; + else if (u && u->kind == TY_STR) elem = ty_u8; + else err(c, n->pos, "for-range needs slice/array/str"); + if (n->list != NULL) { + /* tuple destructure: each name binds to a tuple field */ + Type *etu = (elem && elem->kind == TY_NAMED) ? elem->under : elem; + Tparam *tp = (etu && etu->kind == TY_TUPLE) ? etu->params : NULL; + for (Node *nm = n->list; nm; nm = nm->next) { + Type *ft = tp ? tp->type : ty_err; + if (nm->str && nm->str[0]) + scope_define(c->cur, nm->str, + SK_VAR, ft, nm); + if (tp) tp = tp->next; + } + } else if (n->str && n->str[0]) { + scope_define(c->cur, n->str, SK_VAR, + elem ? elem : ty_err, n); + } + cstmt(c, n->body); + c->loops--; + c->cur = saved; + break; + } + case N_FOR: { + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + c->loops++; + if (n->lhs) cstmt(c, n->lhs); /* init may be a let or expr */ + if (n->cond) { + Type *ct = cexpr(c, n->cond); + if (ct != ty_err && ct != ty_bool && ct != ty_untyped_bool) + err(c, n->pos, "for condition must be bool, got %s", + type_name(c->a, ct)); + } + if (n->rhs) (void)cexpr(c, n->rhs); + cstmt(c, n->body); + c->loops--; + c->cur = saved; + break; + } + case N_MLET: { + Type *rt = cexpr(c, n->rhs); + Type *u = (rt && rt->kind == TY_TUPLE) ? rt : NULL; + if (u == NULL) { + err(c, n->pos, "multi-let rhs is not a tuple (got %s)", + type_name(c->a, rt)); + } + Tparam *tp = u ? u->params : NULL; + for (Node *l = n->list; l; l = l->next) { + Type *declared = l->lhs ? resolve_type(c, l->lhs) : NULL; + Type *elem = tp ? tp->type : NULL; + Type *t = declared ? declared : + (elem ? type_default(elem) : ty_err); + if (declared && elem && !type_assignable(declared, elem)) + err(c, l->pos, "let %s: %s not assignable from %s", + 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 (tp) tp = tp->next; + } + if (u && tp != NULL) + err(c, n->pos, "tuple has extra elements"); + break; + } + case N_MASSIGN: { + Type *rt = cexpr(c, n->rhs); + Type *u = (rt && rt->kind == TY_TUPLE) ? rt : NULL; + if (u == NULL) { + err(c, n->pos, "multi-assign rhs is not a tuple (got %s)", + type_name(c->a, rt)); + } + Tparam *tp = u ? u->params : NULL; + for (Node *lv = n->list; lv; lv = lv->next) { + Type *lt = cexpr(c, lv); + Type *elem = tp ? tp->type : NULL; + if (lt && elem && !type_assignable(lt, elem)) + err(c, lv->pos, "cannot assign %s to %s", + type_name(c->a, elem), type_name(c->a, lt)); + if (tp) tp = tp->next; + } + break; + } + case N_DEFER: (void)cexpr(c, n->lhs); break; + case N_BREAK: + case N_CONTINUE: + if (c->loops == 0) + err(c, n->pos, "%s outside loop", + n->kind == N_BREAK ? "break" : "continue"); + break; + case N_SWITCH: { + Type *st = cexpr(c, n->lhs); + (void)st; + for (Node *cs = n->list; cs; cs = cs->next) { + for (Node *e = cs->list; e; e = e->next) + (void)cexpr(c, e); + cstmt(c, cs->body); + } + break; + } + default: + err(c, n->pos, "internal: unhandled stmt kind %d", n->kind); + } +} + +/* ---- top-level ---------------------------------------------------- */ + +static Type * +build_fn_type(Checker *c, Node *fn) +{ + Type *t = newtype(c->a, TY_FN); + t->size = 8; t->align = 8; + t->ret = fn->lhs ? resolve_type(c, fn->lhs) : ty_void; + Tparam *head = NULL, *tail = NULL; + for (Node *p = fn->list; p; p = p->next) { + if (p->str && strcmp(p->str, "...") == 0) { + t->variadic = 1; + continue; + } + Tparam *tp = amalloc(c->a, sizeof *tp); + tp->name = p->str; + tp->type = resolve_type(c, p->lhs); + if (head == NULL) head = tp; + else tail->next = tp; + tail = tp; + } + t->params = head; + return t; +} + +void +check_init(Checker *c, Arena *a) +{ + memset(c, 0, sizeof *c); + c->a = a; + typesinit(a); + c->top = newscope(a, NULL); + c->cur = c->top; +} + +void +check_file(Checker *c, Node *file) +{ + if (file == NULL || file->kind != N_FILE) return; + + /* pass 1: install names (types first, then defs/fns). + * For self-referential types we install the named-type placeholder + * BEFORE resolving its body; the body may legitimately mention + * the type itself (`type stream = struct { read: fn(*stream)... }`). + */ + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_TYPEDECL) continue; + Type *named = type_named(c->a, d->str, NULL); + if (!scope_define(c->cur, d->str, SK_TYPE, named, d)) + err(c, d->pos, "duplicate type %s", d->str); + d->type = named; + } + for (Node *d = file->list; d; d = d->next) { + if (d->kind != N_TYPEDECL) continue; + Type *under = resolve_type(c, d->lhs); + d->type->under = under; + if (under) { + d->type->size = under->size; + d->type->align = under->align; + } + } + for (Node *d = file->list; d; d = d->next) { + switch (d->kind) { + case N_USE: + scope_define(c->cur, d->str, SK_USE, NULL, d); + break; + case N_DEF: { + Type *t = resolve_type(c, d->lhs); + d->type = t; + if (!scope_define(c->cur, d->str, SK_DEF, t, d)) + err(c, d->pos, "duplicate def %s", d->str); + break; + } + case N_FNDECL: { + Type *t = build_fn_type(c, d); + d->type = t; + if (!scope_define(c->cur, d->str, SK_FN, t, d)) + err(c, d->pos, "duplicate fn %s", d->str); + break; + } + case N_LET: { + Type *t = d->lhs ? resolve_type(c, d->lhs) : NULL; + d->type = t; + if (d->str && d->str[0]) + scope_define(c->cur, d->str, SK_VAR, t, d); + break; + } + default: break; + } + } + + /* pass 2: check def initialisers and fn bodies */ + for (Node *d = file->list; d; d = d->next) { + switch (d->kind) { + case N_DEF: { + if (d->rhs) { + Type *rt = cexpr(c, d->rhs); + if (d->type && rt != ty_err && d->type != ty_err + && !type_assignable(d->type, rt)) + err(c, d->pos, "def %s init %s not assignable to %s", + d->str, type_name(c->a, rt), + type_name(c->a, d->type)); + } + break; + } + case N_FNDECL: { + if (d->body == NULL) break; /* extern decl */ + Scope *saved = c->cur; + c->cur = newscope(c->a, saved); + Type *fnt = d->type; + for (Tparam *p = fnt->params; p; p = p->next) { + if (p->name && p->name[0]) + scope_define(c->cur, p->name, SK_PARAM, p->type, d); + } + Type *prev = c->ret; + c->ret = fnt->ret; + cstmt(c, d->body); + c->ret = prev; + c->cur = saved; + break; + } + case N_LET: { + if (d->rhs) { + Type *rt = cexpr(c, d->rhs); + if (d->type == NULL) d->type = type_default(rt); + if (d->type && rt != ty_err && d->type != ty_err + && !type_assignable(d->type, rt)) + err(c, d->pos, "let %s init not assignable", + d->str); + } + break; + } + default: break; + } + } +} diff --git a/cmd/wwc/err.c b/cmd/wwc/err.c new file mode 100644 index 00000000..97ffb735 --- /dev/null +++ b/cmd/wwc/err.c @@ -0,0 +1,76 @@ +/* + * err.c — diagnostics. + * + * fatal prints, sets exit(1). + * errorf prints with source location, increments nerrors. + * warnf prints with source location, increments nwarnings. + * + * Plan 9 style: short, no levels beyond fatal/error/warn, no colour. + */ +#include "ww.h" +#include +#include + +Pos noPos = { "", 0, 0 }; +int nerrors; +int nwarnings; +FILE *errout; /* set by main; defaults to stderr */ + +static FILE * +out(void) +{ + return errout ? errout : stderr; +} + +static void +prefix(Pos p) +{ + FILE *f = out(); + if (p.file == NULL) + p = noPos; + if (p.line > 0) + fprintf(f, "%s:%d:%d: ", p.file, p.line, p.col); + else + fprintf(f, "%s: ", p.file); +} + +void +fatal(const char *fmt, ...) +{ + FILE *f = out(); + va_list ap; + fprintf(f, "ww: "); + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + fprintf(f, "\n"); + exit(1); +} + +void +errorf(Pos p, const char *fmt, ...) +{ + FILE *f = out(); + va_list ap; + prefix(p); + fprintf(f, "error: "); + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + fprintf(f, "\n"); + nerrors++; +} + +void +warnf(Pos p, const char *fmt, ...) +{ + FILE *f = out(); + va_list ap; + prefix(p); + fprintf(f, "warning: "); + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + fprintf(f, "\n"); + nwarnings++; +} diff --git a/cmd/wwc/lex.c b/cmd/wwc/lex.c new file mode 100644 index 00000000..71dc5102 --- /dev/null +++ b/cmd/wwc/lex.c @@ -0,0 +1,476 @@ +/* + * lex.c — hand-rolled DFA. UTF-8 source, ASCII operators. + * + * Comments: //... and (slash-star ... star-slash). Both stripped. + * Whitespace: space, tab, CR, NL. + * Identifiers: [A-Za-z_][A-Za-z0-9_]* — also matches keywords; we + * look up the kw table after lexing the run. + * Integer: 0x[0-9a-fA-F_]+, 0o[0-7_]+, 0b[01_]+, [0-9][0-9_]* + * Float: [0-9]+'.'[0-9]+([eE][+-]?[0-9]+)? + * Rune: 'x' with C-like escapes + * String: "..." with C-like escapes + * Operators: longest match. + * + * No automatic semicolon insertion (Hare rule). The lexer only emits + * what is in the source; the parser is responsible for non-empty rules. + */ +#include "ww.h" +#include +#include +#include + +void +lexinit(Lex *l, Arena *a, const char *file, const char *src, u64 len) +{ + memset(l, 0, sizeof *l); + l->file = file; + l->src = src; + l->srclen = len; + l->line = 1; + l->col = 1; + l->a = a; +} + +static int +lpeek(Lex *l, u64 ahead) +{ + u64 p = l->pos + ahead; + if (p >= l->srclen) + return -1; + return (unsigned char)l->src[p]; +} + +static int +lget(Lex *l) +{ + if (l->pos >= l->srclen) + return -1; + int c = (unsigned char)l->src[l->pos++]; + if (c == '\n') { + l->line++; + l->col = 1; + } else { + l->col++; + } + return c; +} + +static Pos +lpos(Lex *l) +{ + Pos p = { l->file, l->line, l->col }; + return p; +} + +static int +isidstart(int c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +static int +isidcont(int c) +{ + return isidstart(c) || (c >= '0' && c <= '9'); +} + +static int +ishex(int c) +{ + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); +} + +/* skip whitespace and comments. returns 0 on EOF, else 1. */ +static int +skipws(Lex *l) +{ + for (;;) { + int c = lpeek(l, 0); + if (c < 0) + return 0; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + lget(l); + continue; + } + if (c == '/' && lpeek(l, 1) == '/') { + while ((c = lpeek(l, 0)) >= 0 && c != '\n') + lget(l); + continue; + } + if (c == '/' && lpeek(l, 1) == '*') { + lget(l); lget(l); + int prev = -1; + for (;;) { + int x = lget(l); + if (x < 0) { + Pos p = lpos(l); + errorf(p, "unterminated /* comment"); + l->errs++; + return 0; + } + if (prev == '*' && x == '/') + break; + prev = x; + } + continue; + } + return 1; + } +} + +static u64 +parseint(const char *s, u64 n, int base, int *ok) +{ + u64 v = 0; + int got = 0; + for (u64 i = 0; i < n; i++) { + int c = (unsigned char)s[i]; + if (c == '_') + continue; + int d; + if (c >= '0' && c <= '9') d = c - '0'; + else if (c >= 'a' && c <= 'f') d = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') d = c - 'A' + 10; + else { *ok = 0; return 0; } + if (d >= base) { *ok = 0; return 0; } + /* overflow? cheap check */ + if (v > (u64)~0ULL / (u64)base) { *ok = 0; return 0; } + v = v * (u64)base + (u64)d; + got = 1; + } + *ok = got; + return v; +} + +static int +escape(Lex *l, int *out) +{ + int c = lget(l); + if (c < 0) return -1; + switch (c) { + case 'n': *out = '\n'; return 0; + case 't': *out = '\t'; return 0; + case 'r': *out = '\r'; return 0; + case '\\': *out = '\\'; return 0; + case '\'': *out = '\''; return 0; + case '"': *out = '"'; return 0; + case '0': *out = '\0'; return 0; + case 'a': *out = '\a'; return 0; + case 'b': *out = '\b'; return 0; + case 'f': *out = '\f'; return 0; + case 'v': *out = '\v'; return 0; + case 'x': { + int hi = lget(l), lo = lget(l); + if (!ishex(hi) || !ishex(lo)) { + Pos p = lpos(l); + errorf(p, "bad \\x escape"); + l->errs++; + return -1; + } + int h = (hi <= '9' ? hi - '0' : (hi | 0x20) - 'a' + 10); + int o = (lo <= '9' ? lo - '0' : (lo | 0x20) - 'a' + 10); + *out = (h << 4) | o; + return 0; + } + } + { Pos p = lpos(l); errorf(p, "bad escape \\%c", c); l->errs++; } + return -1; +} + +static Tok +lexnum(Lex *l, Pos start) +{ + Tok t = (Tok){ TK_INT, start, NULL, 0, {0}, TK_NONE }; + u64 begin = l->pos; + int base = 10; + int isfloat = 0; + int c = lpeek(l, 0); + + if (c == '0' && (lpeek(l, 1) == 'x' || lpeek(l, 1) == 'X')) { + lget(l); lget(l); + base = 16; + while ((c = lpeek(l, 0)) >= 0 && (ishex(c) || c == '_')) + lget(l); + } else if (c == '0' && (lpeek(l, 1) == 'b' || lpeek(l, 1) == 'B')) { + lget(l); lget(l); + base = 2; + while ((c = lpeek(l, 0)) >= 0 && (c == '0' || c == '1' || c == '_')) + lget(l); + } else if (c == '0' && (lpeek(l, 1) == 'o' || lpeek(l, 1) == 'O')) { + lget(l); lget(l); + base = 8; + while ((c = lpeek(l, 0)) >= 0 && ((c >= '0' && c <= '7') || c == '_')) + lget(l); + } else { + while ((c = lpeek(l, 0)) >= 0 && ((c >= '0' && c <= '9') || c == '_')) + lget(l); + if (lpeek(l, 0) == '.' && lpeek(l, 1) >= '0' && lpeek(l, 1) <= '9') { + isfloat = 1; + lget(l); + while ((c = lpeek(l, 0)) >= 0 && ((c >= '0' && c <= '9') || c == '_')) + lget(l); + c = lpeek(l, 0); + if (c == 'e' || c == 'E') { + lget(l); + if (lpeek(l, 0) == '+' || lpeek(l, 0) == '-') + lget(l); + while ((c = lpeek(l, 0)) >= 0 && c >= '0' && c <= '9') + lget(l); + } + } + } + + u64 n = l->pos - begin; + t.text = astrndup(l->a, l->src + begin, n); + t.tlen = n; + + if (isfloat) { + t.kind = TK_FLOAT; + /* strdup with underscores stripped before strtod */ + char *clean = amalloc(l->a, n + 1); + u64 j = 0; + for (u64 i = 0; i < n; i++) + if (l->src[begin + i] != '_') + clean[j++] = l->src[begin + i]; + clean[j] = '\0'; + errno = 0; + t.v.fval = strtod(clean, NULL); + if (errno) { + errorf(start, "bad float literal '%s'", t.text); + l->errs++; + } + } else { + const char *digs = l->src + begin; + u64 dn = n; + if (base != 10) { + digs += 2; + dn -= 2; + } + int ok = 0; + t.v.uval = parseint(digs, dn, base, &ok); + if (!ok) { + errorf(start, "bad integer literal '%s'", t.text); + l->errs++; + t.kind = TK_ERR; + } + } + + /* Typed suffix: i8/i16/i32/i64, u8/u16/u32/u64, f32/f64. + * Must be glued (no whitespace) to the digits. We grab the + * adjacent identifier-like run and accept it only if it's one + * of the recognised type names. */ + if (isidstart(lpeek(l, 0))) { + u64 sb = l->pos; + while (isidcont(lpeek(l, 0))) lget(l); + u64 sl = l->pos - sb; + const char *names[] = { + "i8", "i16", "i32", "i64", + "u8", "u16", "u32", "u64", + "f32", "f64", NULL + }; + const char *match = NULL; + for (int i = 0; names[i]; i++) { + u64 nl = strlen(names[i]); + if (nl == sl && memcmp(names[i], l->src + sb, nl) == 0) { + match = names[i]; + break; + } + } + if (match) { + t.tsuffix = astrndup(l->a, l->src + sb, sl); + } else { + /* not a known suffix — rewind so the run becomes a + * separate token. */ + l->pos = sb; + } + } + return t; +} + +static Tok +lexident(Lex *l, Pos start) +{ + u64 begin = l->pos; + while (isidcont(lpeek(l, 0))) + lget(l); + u64 n = l->pos - begin; + const char *p = l->src + begin; + Tkind k = kwlookup(p, n); + Tok t = (Tok){ k != TK_NONE ? k : TK_IDENT, start, + astrndup(l->a, p, n), n, {0}, TK_NONE }; + return t; +} + +static Tok +lexstr(Lex *l, Pos start) +{ + /* opening quote already consumed by caller */ + u64 cap = 32, n = 0; + char *buf = amalloc(l->a, cap); + for (;;) { + int c = lpeek(l, 0); + if (c < 0) { + errorf(start, "unterminated string"); + l->errs++; + Tok t = (Tok){ TK_ERR, start, astrndup(l->a, "", 0), 0, {0}, TK_NONE }; + return t; + } + if (c == '"') { lget(l); break; } + int ch; + if (c == '\\') { + lget(l); + if (escape(l, &ch) < 0) + ch = 0; + } else { + ch = lget(l); + } + if (n + 1 >= cap) { + u64 ncap = cap * 2; + char *nb = amalloc(l->a, ncap); + memcpy(nb, buf, n); + buf = nb; + cap = ncap; + } + buf[n++] = (char)ch; + } + buf[n] = '\0'; + Tok t = (Tok){ TK_STR, start, buf, n, {0}, TK_NONE }; + return t; +} + +static Tok +lexrune(Lex *l, Pos start) +{ + int ch; + int c = lpeek(l, 0); + if (c < 0) { + errorf(start, "unterminated rune"); + l->errs++; + return (Tok){ TK_ERR, start, "", 0, {0}, TK_NONE }; + } + if (c == '\\') { + lget(l); + if (escape(l, &ch) < 0) + ch = 0; + } else { + ch = lget(l); + } + if (lpeek(l, 0) != '\'') { + errorf(start, "rune literal missing closing '"); + l->errs++; + return (Tok){ TK_ERR, start, "", 0, {0}, TK_NONE }; + } + lget(l); + Tok t = (Tok){ TK_RUNE, start, NULL, 0, {0}, TK_NONE }; + t.v.uval = (u64)(u32)ch; + t.text = aprintf(l->a, "%d", ch); + t.tlen = strlen(t.text); + return t; +} + +#define EMIT(K) do { Tok _t = (Tok){ (K), start, NULL, 0, {0}, TK_NONE }; \ + _t.text = tokname(K); _t.tlen = strlen(_t.text); return _t; } while (0) + +Tok +lexnext(Lex *l) +{ + if (!skipws(l)) { + Pos p = lpos(l); + Tok t = (Tok){ TK_EOF, p, "", 0, {0}, TK_NONE }; + return t; + } + Pos start = lpos(l); + int c = lpeek(l, 0); + + if (isidstart(c)) + return lexident(l, start); + if (c >= '0' && c <= '9') + return lexnum(l, start); + + if (c == '"') { lget(l); return lexstr(l, start); } + if (c == '\'') { lget(l); return lexrune(l, start); } + + lget(l); + switch (c) { + case '(': EMIT(TK_LPAREN); + case ')': EMIT(TK_RPAREN); + case '{': EMIT(TK_LBRACE); + case '}': EMIT(TK_RBRACE); + case '[': EMIT(TK_LBRACK); + case ']': EMIT(TK_RBRACK); + case ',': EMIT(TK_COMMA); + case ';': EMIT(TK_SEMI); + case ':': EMIT(TK_COLON); + case '@': EMIT(TK_AT); + case '?': EMIT(TK_QUESTION); + case '~': EMIT(TK_TILDE); + case '.': + if (lpeek(l, 0) == '.' && lpeek(l, 1) == '.') { + lget(l); lget(l); + EMIT(TK_ELLIPSIS); + } + if (lpeek(l, 0) == '.') { + lget(l); + EMIT(TK_DOTDOT); + } + EMIT(TK_DOT); + case '+': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_PLUSEQ); } + EMIT(TK_PLUS); + case '-': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_MINUSEQ); } + if (lpeek(l, 0) == '>') { lget(l); EMIT(TK_ARROW); } + EMIT(TK_MINUS); + case '*': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_STAREQ); } + EMIT(TK_STAR); + case '/': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_SLASHEQ); } + EMIT(TK_SLASH); + case '%': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_PERCENTEQ); } + EMIT(TK_PERCENT); + case '&': + if (lpeek(l, 0) == '&') { lget(l); EMIT(TK_AND); } + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_AMPEQ); } + EMIT(TK_AMP); + case '|': + if (lpeek(l, 0) == '|') { lget(l); EMIT(TK_OR); } + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_PIPEEQ); } + EMIT(TK_PIPE); + case '^': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_CARETEQ); } + EMIT(TK_CARET); + case '=': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_EQ); } + if (lpeek(l, 0) == '>') { lget(l); EMIT(TK_FATARROW); } + EMIT(TK_ASSIGN); + case '!': + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_NEQ); } + EMIT(TK_NOT); + case '<': + if (lpeek(l, 0) == '<') { + lget(l); + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_LSHIFTEQ); } + EMIT(TK_LSHIFT); + } + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_LE); } + if (lpeek(l, 0) == '-') { lget(l); EMIT(TK_LARROW); } + EMIT(TK_LT); + case '>': + if (lpeek(l, 0) == '>') { + lget(l); + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_RSHIFTEQ); } + EMIT(TK_RSHIFT); + } + if (lpeek(l, 0) == '=') { lget(l); EMIT(TK_GE); } + EMIT(TK_GT); + } + errorf(start, "unexpected character 0x%02x", c); + l->errs++; + Tok t = (Tok){ TK_ERR, start, NULL, 0, {0}, TK_NONE }; + t.text = astrndup(l->a, (const char[]){ (char)c }, 1); + t.tlen = 1; + return t; +} diff --git a/cmd/wwc/mem.c b/cmd/wwc/mem.c new file mode 100644 index 00000000..cd3a5caa --- /dev/null +++ b/cmd/wwc/mem.c @@ -0,0 +1,117 @@ +/* + * mem.c — arena allocator. No free per allocation; freearena releases + * the whole chain. Aligned to 16 so structs with 8-byte fields and + * doubles are happy. + * + * Hot allocations in the compiler land in arenas: tokens, AST nodes, + * symbols, types. The chunk size doubles up to a cap so we don't + * fragment on huge inputs. + */ +#include "ww.h" +#include +#include + +#define ALIGN 16 +#define INIT_CHUNK (64 * 1024) +#define MAX_CHUNK (4 * 1024 * 1024) + +static u64 +roundup(u64 n, u64 a) +{ + return (n + a - 1) & ~(a - 1); +} + +Arena * +newarena(void) +{ + Arena *a = calloc(1, sizeof *a); + if (a == NULL) + fatal("newarena: out of memory"); + a->buf = malloc(INIT_CHUNK); + if (a->buf == NULL) + fatal("newarena: out of memory"); + a->cap = INIT_CHUNK; + return a; +} + +static void +grow(Arena *a, u64 need) +{ + u64 ncap = a->cap * 2; + if (ncap > MAX_CHUNK) + ncap = MAX_CHUNK; + if (ncap < need) + ncap = roundup(need, ALIGN); + + /* push current chunk onto chain, allocate fresh head */ + Arena *old = malloc(sizeof *old); + if (old == NULL) + fatal("arena: oom"); + *old = *a; + a->next = old; + a->buf = malloc(ncap); + if (a->buf == NULL) + fatal("arena: oom (chunk=%llu)", (unsigned long long)ncap); + a->off = 0; + a->cap = ncap; +} + +void * +amalloc(Arena *a, u64 n) +{ + n = roundup(n, ALIGN); + if (n > a->cap - a->off) + grow(a, n); + void *p = a->buf + a->off; + a->off += n; + a->total += n; + memset(p, 0, n); + return p; +} + +char * +astrdup(Arena *a, const char *s) +{ + u64 n = strlen(s); + char *p = amalloc(a, n + 1); + memcpy(p, s, n); + return p; +} + +char * +astrndup(Arena *a, const char *s, u64 n) +{ + char *p = amalloc(a, n + 1); + memcpy(p, s, n); + return p; +} + +char * +aprintf(Arena *a, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (n < 0) + fatal("aprintf: vsnprintf failed"); + char *p = amalloc(a, (u64)n + 1); + va_start(ap, fmt); + vsnprintf(p, (size_t)n + 1, fmt, ap); + va_end(ap); + return p; +} + +void +freearena(Arena *a) +{ + while (a) { + Arena *next = a->next; + free(a->buf); + /* The head Arena was returned by newarena() and is the only + * one we should free as a struct; the linked older ones were + * allocated by grow() and are also freeable. */ + free(a); + a = next; + } +} diff --git a/cmd/wwc/parse.c b/cmd/wwc/parse.c new file mode 100644 index 00000000..9ded6210 --- /dev/null +++ b/cmd/wwc/parse.c @@ -0,0 +1,1183 @@ +/* + * parse.c — recursive descent + Pratt expression parser. + * + * Plan 9 style: hand-rolled, no yacc, errors are reported via errorf + * but we keep going where possible so the user gets multiple + * diagnostics from one run. + * + * Module navigation and field access both use '.'. We disambiguate + * structurally: the parser eats `IDENT('.'IDENT)*` greedily for type + * names and `use` paths; in expressions, postfix `.IDENT` becomes + * N_DOT and the checker decides if it's a module-qualified ref or a + * struct field. + */ +#include "ww.h" +#include +#include + +void +parserinit(Parser *p, Arena *a, Lex *l) +{ + memset(p, 0, sizeof *p); + p->l = l; + p->a = a; + p->cur = lexnext(l); +} + +static void +advance(Parser *p) +{ + if (p->hasla) { + p->cur = p->la; + p->hasla = 0; + } else { + p->cur = lexnext(p->l); + } +} + +static Tok +peek(Parser *p) +{ + if (!p->hasla) { + p->la = lexnext(p->l); + p->hasla = 1; + } + return p->la; +} + +static int +accept(Parser *p, Tkind k) +{ + if (p->cur.kind == k) { + advance(p); + return 1; + } + return 0; +} + +static int +expect(Parser *p, Tkind k) +{ + if (p->cur.kind == k) { + advance(p); + return 1; + } + errorf(p->cur.pos, "expected %s, got %s", tokname(k), tokname(p->cur.kind)); + p->errs++; + return 0; +} + +static const char * +expectident(Parser *p) +{ + if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "expected identifier, got %s", + tokname(p->cur.kind)); + p->errs++; + return ""; + } + const char *s = p->cur.text; + advance(p); + return s; +} + +static Node *parseexpr(Parser *p); +static Node *parseunary(Parser *p); +static Node *parsetype(Parser *p); +static Node *parseblock(Parser *p); +static Node *parsestmt(Parser *p); + +/* ------- type expressions ------------------------------------------ */ + +static Node * +parseparams(Parser *p) +{ + Node *head = NULL, *tail = NULL; + if (p->cur.kind == TK_RPAREN) + return NULL; + for (;;) { + Pos pp = p->cur.pos; + if (p->cur.kind == TK_ELLIPSIS) { + advance(p); + Node *n = newnode(p->a, N_PARAM, pp); + n->str = "..."; + n->strlen = 3; + if (head == NULL) head = n; + else tail->next = n; + tail = n; + 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); + expect(p, TK_COLON); + n->lhs = parsetype(p); + } else { + n->str = ""; + n->strlen = 0; + n->lhs = parsetype(p); + } + if (head == NULL) head = n; + else tail->next = n; + tail = n; + if (!accept(p, TK_COMMA)) + break; + if (p->cur.kind == TK_RPAREN) /* trailing comma */ + break; + } + return head; +} + +static Node * +parsetype(Parser *p) +{ + Pos pp = p->cur.pos; + switch (p->cur.kind) { + case TK_STAR: { + advance(p); + Node *n = newnode(p->a, N_TPTR, pp); + n->lhs = parsetype(p); + return n; + } + case TK_LBRACK: { + advance(p); + if (accept(p, TK_RBRACK)) { + Node *n = newnode(p->a, N_TSLICE, pp); + n->lhs = parsetype(p); + return n; + } + Node *n = newnode(p->a, N_TARRAY, pp); + n->rhs = parseexpr(p); + expect(p, TK_RBRACK); + n->lhs = parsetype(p); + return n; + } + case TK_FN: { + advance(p); + expect(p, TK_LPAREN); + Node *n = newnode(p->a, N_TFN, pp); + n->list = parseparams(p); + expect(p, TK_RPAREN); + n->lhs = parsetype(p); + return n; + } + case TK_STRUCT: { + advance(p); + expect(p, TK_LBRACE); + Node *n = newnode(p->a, N_TSTRUCT, pp); + Node *head = NULL, *tail = NULL; + while (p->cur.kind != TK_RBRACE && p->cur.kind != TK_EOF) { + Pos fp = p->cur.pos; + Node *f = newnode(p->a, N_TFIELD, fp); + f->str = expectident(p); + expect(p, TK_COLON); + f->lhs = parsetype(p); + if (head == NULL) head = f; + else tail->next = f; + tail = f; + if (!accept(p, TK_COMMA)) + break; + } + expect(p, TK_RBRACE); + n->list = head; + return n; + } + case TK_CHAN: { + advance(p); + Node *n = newnode(p->a, N_TCHAN, pp); + n->lhs = parsetype(p); + return n; + } + case TK_IDENT: { + Node *n = newnode(p->a, N_TNAME, pp); + n->str = p->cur.text; + n->strlen = p->cur.tlen; + advance(p); + /* dotted path: pkg.Type — collapse into a single TNAME */ + while (accept(p, TK_DOT)) { + if (p->cur.kind != TK_IDENT) { + errorf(p->cur.pos, "expected identifier after '.'"); + p->errs++; + break; + } + n->str = aprintf(p->a, "%s.%s", n->str, p->cur.text); + n->strlen = strlen(n->str); + advance(p); + } + return n; + } + case TK_LPAREN: { + /* Three forms inside the parens: + * (T) — parenthesised single type + * (T, T2, ...) — tuple type + * (T | T2 | ...) — tagged-union type (Hare-style sum) */ + advance(p); + Node *first = parsetype(p); + if (accept(p, TK_PIPE)) { + Node *t = newnode(p->a, N_TTAGGED, pp); + Node *head = first, *tail = first; + for (;;) { + Node *e = parsetype(p); + tail->next = e; + tail = e; + if (!accept(p, TK_PIPE)) break; + } + expect(p, TK_RPAREN); + t->list = head; + return t; + } + if (!accept(p, TK_COMMA)) { + expect(p, TK_RPAREN); + return first; + } + Node *t = newnode(p->a, N_TTUPLE, pp); + Node *head = first, *tail = first; + for (;;) { + Node *e = parsetype(p); + tail->next = e; + tail = e; + if (!accept(p, TK_COMMA)) break; + if (p->cur.kind == TK_RPAREN) break; + } + expect(p, TK_RPAREN); + t->list = head; + return t; + } + default: + errorf(pp, "expected type, got %s", tokname(p->cur.kind)); + p->errs++; + advance(p); + return newnode(p->a, N_TNAME, pp); + } +} + +/* ------- expressions (Pratt) ---------------------------------------- */ + +/* binary precedence; 0 = not a binary op */ +static int +bprec(Tkind k) +{ + switch (k) { + case TK_OR: return 1; + case TK_AND: return 2; + case TK_EQ: + case TK_NEQ: return 3; + case TK_LT: + case TK_LE: + case TK_GT: + case TK_GE: return 4; + case TK_PIPE: return 5; + case TK_CARET: return 6; + case TK_AMP: return 7; + case TK_LSHIFT: + case TK_RSHIFT: return 8; + case TK_PLUS: + case TK_MINUS: return 9; + case TK_STAR: + case TK_SLASH: + case TK_PERCENT: return 10; + default: return 0; + } +} + +static int +isassignop(Tkind k) +{ + switch (k) { + case TK_ASSIGN: case TK_PLUSEQ: case TK_MINUSEQ: + case TK_STAREQ: case TK_SLASHEQ: case TK_PERCENTEQ: + case TK_AMPEQ: case TK_PIPEEQ: case TK_CARETEQ: + case TK_LSHIFTEQ: case TK_RSHIFTEQ: + return 1; + default: return 0; + } +} + +static Node * +parsearglist(Parser *p, Tkind close) +{ + Node *head = NULL, *tail = NULL; + if (p->cur.kind == close) + return NULL; + for (;;) { + Node *e = parseexpr(p); + /* Hare-style spread: `expr...` in an arg slot becomes a + * marker node that the callee/builtin can iterate. */ + if (accept(p, TK_ELLIPSIS)) { + Node *sp = newnode(p->a, N_SPREAD, e->pos); + sp->lhs = e; + e = sp; + } + if (head == NULL) head = e; + else tail->next = e; + tail = e; + if (!accept(p, TK_COMMA)) + break; + if (p->cur.kind == close) + break; + } + return head; +} + +static Node * +parsestructlit(Parser *p, Node *typeref) +{ + Pos pp = p->cur.pos; + expect(p, TK_LBRACE); + Node *n = newnode(p->a, N_STRUCTLIT, pp); + n->lhs = typeref; + Node *head = NULL, *tail = NULL; + while (p->cur.kind != TK_RBRACE && p->cur.kind != TK_EOF) { + Pos fp = p->cur.pos; + const char *name = expectident(p); + expect(p, TK_ASSIGN); + Node *val = parseexpr(p); + Node *f = newnode(p->a, N_FIELD, fp); + f->str = name; + f->lhs = val; + if (head == NULL) head = f; + else tail->next = f; + tail = f; + if (!accept(p, TK_COMMA)) + break; + } + expect(p, TK_RBRACE); + n->list = head; + return n; +} + +static Node * +parsearrlit(Parser *p) +{ + Pos pp = p->cur.pos; + expect(p, TK_LBRACK); + Node *n = newnode(p->a, N_ARRLIT, pp); + Node *head = NULL, *tail = NULL; + while (p->cur.kind != TK_RBRACK && p->cur.kind != TK_EOF) { + Node *e = parseexpr(p); + if (head == NULL) head = e; + else tail->next = e; + tail = e; + if (accept(p, TK_ELLIPSIS)) { + /* repeat suffix marker; encode as a flag node */ + Node *rep = newnode(p->a, N_FIELD, p->cur.pos); + rep->str = "..."; + rep->strlen = 3; + tail->next = rep; + tail = rep; + break; + } + if (!accept(p, TK_COMMA)) + break; + } + expect(p, TK_RBRACK); + n->list = head; + return n; +} + +static Node * +parseprimary(Parser *p) +{ + Pos pp = p->cur.pos; + Tok t = p->cur; + switch (t.kind) { + case TK_INT: { + Node *n = newnode(p->a, N_INTLIT, pp); + n->uval = t.v.uval; + n->str = t.text; + n->tsuffix = t.tsuffix; + advance(p); + return n; + } + case TK_FLOAT: { + Node *n = newnode(p->a, N_FLOATLIT, pp); + n->fval = t.v.fval; + n->str = t.text; + n->tsuffix = t.tsuffix; + advance(p); + return n; + } + case TK_STR: { + Node *n = newnode(p->a, N_STRLIT, pp); + n->str = t.text; + n->strlen = t.tlen; + advance(p); + return n; + } + case TK_RUNE: { + Node *n = newnode(p->a, N_RUNELIT, pp); + n->uval = t.v.uval; + advance(p); + return n; + } + 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_LPAREN: { + advance(p); + Node *e = parseexpr(p); + /* tuple literal: (e1, e2, ...) — at least 2 elements; + * a single (e) is a parenthesised expression */ + if (accept(p, TK_COMMA)) { + Node *t = newnode(p->a, N_TUPLE, pp); + t->list = e; + Node *tt = e; + for (;;) { + Node *en = parseexpr(p); + tt->next = en; + tt = en; + if (!accept(p, TK_COMMA)) break; + if (p->cur.kind == TK_RPAREN) break; + } + expect(p, TK_RPAREN); + return t; + } + expect(p, TK_RPAREN); + return e; + } + case TK_LBRACK: + return parsearrlit(p); + case TK_MATCH: { + /* match (e) { + * case let v: T => stmt; + * case T => stmt; + * case => stmt; — default + * }; + */ + advance(p); + expect(p, TK_LPAREN); + Node *m = newnode(p->a, N_MATCH, pp); + m->lhs = parseexpr(p); + expect(p, TK_RPAREN); + expect(p, TK_LBRACE); + Node *head = NULL, *tail = NULL; + while (p->cur.kind == TK_CASE) { + Pos cp = p->cur.pos; + advance(p); + Node *cs = newnode(p->a, N_MCASE, cp); + if (p->cur.kind == TK_LET) { + /* `case let v: T =>` — bind variant to v. + * Single-type only: multi-pattern with a + * binding would force the binding to take the + * union type, which we don't support. */ + advance(p); + cs->str = expectident(p); + expect(p, TK_COLON); + cs->lhs = parsetype(p); + } else if (p->cur.kind == TK_FATARROW) { + /* `case =>` — default arm (cs->lhs left NULL) */ + } else if (p->cur.kind != TK_ELSE && p->cur.kind != TK_LBRACE) { + /* `case T =>` — match variant by type. + * `case T1 | T2 | T3 =>` — match if scrutinee + * is any of the listed variants. The first type + * goes on cs->lhs (so single-pattern callers + * still work); additional types chain through + * cs->list. */ + cs->lhs = parsetype(p); + Node *tail = NULL; + while (accept(p, TK_PIPE)) { + Node *more = parsetype(p); + if (cs->list == NULL) cs->list = more; + else tail->next = more; + tail = more; + } + } + expect(p, TK_FATARROW); + cs->body = parsestmt(p); + if (head == NULL) head = cs; + else tail->next = cs; + tail = cs; + } + expect(p, TK_RBRACE); + m->list = head; + return m; + } + case TK_IDENT: { + Node *n = newnode(p->a, N_IDENT, pp); + n->str = t.text; + n->strlen = t.tlen; + advance(p); + /* dotted ident chain folded into one IDENT for type-ish refs */ + while (p->cur.kind == TK_DOT && peek(p).kind == TK_IDENT) { + advance(p); + n = (Node*)n; /* keep stable */ + Node *mr = newnode(p->a, N_DOT, pp); + mr->lhs = n; + mr->str = p->cur.text; + mr->strlen = p->cur.tlen; + advance(p); + n = mr; + } + /* struct literal: ident '{' ... '}' (only if ident-shaped) */ + if (p->cur.kind == TK_LBRACE) + return parsestructlit(p, n); + return n; + } + default: + errorf(pp, "unexpected token in expression: %s", + tokname(t.kind)); + p->errs++; + advance(p); + return newnode(p->a, N_NONE, pp); + } +} + +static Node * +parsepostfix(Parser *p, Node *lhs) +{ + for (;;) { + Pos pp = p->cur.pos; + switch (p->cur.kind) { + case TK_LPAREN: { + advance(p); + Node *n = newnode(p->a, N_CALL, pp); + n->lhs = lhs; + n->list = parsearglist(p, TK_RPAREN); + expect(p, TK_RPAREN); + lhs = n; + break; + } + case TK_LBRACK: { + advance(p); + /* Three forms inside the brackets: + * [hi] — slice of [0:hi] (rare) + * [lo:hi] — slice + * [expr] — index + * We disambiguate by checking for ':' after the first + * expr (suppressing cast there since ':' is the slicer). */ + if (p->cur.kind == TK_COLON) { + advance(p); + Node *n = newnode(p->a, N_SLICE, pp); + n->lhs = lhs; + if (p->cur.kind != TK_RBRACK) + n->cond = parseexpr(p); + expect(p, TK_RBRACK); + lhs = n; + break; + } + int prev_nocast = p->nocast; + p->nocast = 1; + Node *e = parseexpr(p); + p->nocast = prev_nocast; + if (p->cur.kind == TK_COLON) { + advance(p); + Node *n = newnode(p->a, N_SLICE, pp); + n->lhs = lhs; + n->rhs = e; + if (p->cur.kind != TK_RBRACK) + n->cond = parseexpr(p); + expect(p, TK_RBRACK); + lhs = n; + break; + } + Node *n = newnode(p->a, N_INDEX, pp); + n->lhs = lhs; + n->rhs = e; + expect(p, TK_RBRACK); + lhs = n; + break; + } + case TK_DOT: { + advance(p); + Node *n = newnode(p->a, N_DOT, pp); + n->lhs = lhs; + if (p->cur.kind == TK_INT) { + /* Hare-style tuple field access: t.0, t.1 */ + n->str = aprintf(p->a, "%llu", + (unsigned long long)p->cur.v.uval); + advance(p); + } else { + n->str = expectident(p); + } + lhs = n; + break; + } + case TK_COLON: { + /* cast: expr ':' type — suppressed inside case-selectors, + * where ':' separates the selector from the body. */ + if (p->nocast) + return lhs; + advance(p); + Node *n = newnode(p->a, N_CAST, pp); + n->lhs = lhs; + n->rhs = parsetype(p); + lhs = n; + break; + } + case TK_QUESTION: { + advance(p); + Node *n = newnode(p->a, N_TRYPROP, pp); + n->lhs = lhs; + lhs = n; + break; + } + case TK_NOT: { + /* Could be unary '!' starting a new expression, but as + * a *postfix* it's the Hare error-unwrap. We're inside + * parsepostfix, so it's postfix. */ + advance(p); + Node *n = newnode(p->a, N_TRYUNW, pp); + n->lhs = lhs; + lhs = n; + break; + } + default: + return lhs; + } + } +} + +static Node * +parseunary(Parser *p) +{ + Pos pp = p->cur.pos; + switch (p->cur.kind) { + case TK_MINUS: + case TK_PLUS: + case TK_NOT: + case TK_TILDE: + case TK_STAR: /* deref */ + case TK_AMP: { /* address-of */ + Tkind op = p->cur.kind; + advance(p); + Node *n = newnode(p->a, N_UN, pp); + n->op = op; + n->lhs = parseunary(p); + return n; + } + case TK_LARROW: { /* chan recv */ + advance(p); + Node *n = newnode(p->a, N_RECV, pp); + n->lhs = parseunary(p); + return n; + } + default: + return parsepostfix(p, parseprimary(p)); + } +} + +static Node * +parsebin(Parser *p, Node *lhs, int min) +{ + for (;;) { + Tkind op = p->cur.kind; + int pr = bprec(op); + if (pr == 0 || pr < min) + return lhs; + Pos pp = p->cur.pos; + advance(p); + Node *rhs = parseunary(p); + while (bprec(p->cur.kind) > pr) + rhs = parsebin(p, rhs, bprec(p->cur.kind)); + Node *n = newnode(p->a, N_BIN, pp); + n->op = op; + n->lhs = lhs; + n->rhs = rhs; + lhs = n; + } +} + +static Node * +parseexpr_noassign(Parser *p) +{ + return parsebin(p, parseunary(p), 1); +} + +static Node * +parseexpr(Parser *p) +{ + Node *e = parsebin(p, parseunary(p), 1); + if (isassignop(p->cur.kind)) { + Pos pp = p->cur.pos; + Tkind op = p->cur.kind; + advance(p); + Node *n = newnode(p->a, N_ASSIGN, pp); + n->op = op; + n->lhs = e; + n->rhs = parseexpr(p); /* right-assoc */ + return n; + } + return e; +} + +Node * +parseexpr_top(Parser *p) +{ + return parseexpr(p); +} + +/* ------- statements ------------------------------------------------- */ + +static Node * +parselet(Parser *p, int top) +{ + Pos pp = p->cur.pos; + expect(p, TK_LET); + + /* Hare-style tuple destructure: `let (a, b) = expr;` */ + if (p->cur.kind == TK_LPAREN) { + advance(p); + Node *m = newnode(p->a, N_MLET, pp); + Node *head = NULL, *tail = NULL; + for (;;) { + Pos lpp = p->cur.pos; + Node *l = newnode(p->a, N_LET, lpp); + l->str = expectident(p); + if (accept(p, TK_COLON)) l->lhs = parsetype(p); + if (head == NULL) head = l; + else tail->next = l; + tail = l; + if (!accept(p, TK_COMMA)) break; + } + expect(p, TK_RPAREN); + expect(p, TK_ASSIGN); + m->rhs = parseexpr(p); + expect(p, TK_SEMI); + m->list = head; + (void)top; + return m; + } + + /* parse first binding */ + Pos lp = p->cur.pos; + Node *first = newnode(p->a, N_LET, lp); + first->str = expectident(p); + if (accept(p, TK_COLON)) + first->lhs = parsetype(p); + + if (p->cur.kind == TK_COMMA) { + /* multi-let: collect (name, type) pairs, then '=' rhs */ + Node *m = newnode(p->a, N_MLET, pp); + Node *head = first, *tail = first; + while (accept(p, TK_COMMA)) { + Pos lpp = p->cur.pos; + Node *l = newnode(p->a, N_LET, lpp); + l->str = expectident(p); + if (accept(p, TK_COLON)) + l->lhs = parsetype(p); + tail->next = l; + tail = l; + } + expect(p, TK_ASSIGN); + m->rhs = parseexpr(p); + expect(p, TK_SEMI); + m->list = head; + (void)top; + return m; + } + + if (accept(p, TK_ASSIGN)) + first->rhs = parseexpr(p); + expect(p, TK_SEMI); + (void)top; + return first; +} + +static Node * +parseif(Parser *p) +{ + Pos pp = p->cur.pos; + expect(p, TK_IF); + expect(p, TK_LPAREN); + Node *n = newnode(p->a, N_IF, pp); + n->cond = parseexpr(p); + expect(p, TK_RPAREN); + n->body = parseblock(p); + if (accept(p, TK_ELSE)) { + if (p->cur.kind == TK_IF) + n->els = parseif(p); + else + n->els = parseblock(p); + } + return n; +} + +static Node * +parsefor(Parser *p) +{ + Pos pp = p->cur.pos; + expect(p, TK_FOR); + Node *n = newnode(p->a, N_FOR, pp); + if (p->cur.kind == TK_LBRACE) { + n->body = parseblock(p); + return n; + } + expect(p, TK_LPAREN); + if (p->cur.kind == TK_RPAREN) { + advance(p); + n->body = parseblock(p); + return n; + } + /* First clause may be 'let' init or expr. If 'let' or terminated + * by ';', it's a C-for. Otherwise it's just `for (cond)`. */ + if (p->cur.kind == TK_LET) { + /* Disambiguate range form: `let IDENT .. expr`. We peek + * for IDENT then DOTDOT before committing to parselet. + * Tuple destructure: `let (a, b) .. expr`. */ + Tok save_cur = p->cur; + (void)save_cur; + advance(p); /* consume LET */ + if (p->cur.kind == TK_LPAREN) { + advance(p); + Node *names = NULL, *tail = NULL; + for (;;) { + Pos np = p->cur.pos; + Node *e = newnode(p->a, N_IDENT, np); + e->str = expectident(p); + if (names == NULL) names = e; + else tail->next = e; + tail = e; + if (!accept(p, TK_COMMA)) break; + } + expect(p, TK_RPAREN); + expect(p, TK_DOTDOT); + Node *rng = newnode(p->a, N_FORRANGE, pp); + rng->str = NULL; + rng->list = names; + rng->lhs = parseexpr(p); + expect(p, TK_RPAREN); + rng->body = parseblock(p); + return rng; + } + if (p->cur.kind == TK_IDENT) { + Pos ip = p->cur.pos; + const char *nm = p->cur.text; + Tok la = peek(p); + if (la.kind == TK_DOTDOT) { + advance(p); /* consume IDENT */ + advance(p); /* consume DOTDOT */ + Node *rng = newnode(p->a, N_FORRANGE, pp); + rng->str = nm; + rng->lhs = parseexpr(p); + expect(p, TK_RPAREN); + rng->body = parseblock(p); + (void)ip; + return 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); + if (accept(p, TK_COLON)) + first->lhs = parsetype(p); + if (accept(p, TK_ASSIGN)) + first->rhs = parseexpr(p); + expect(p, TK_SEMI); + n->lhs = first; + if (p->cur.kind != TK_SEMI && p->cur.kind != TK_RPAREN) + n->cond = parseexpr(p); + if (accept(p, TK_SEMI)) + n->rhs = parseexpr(p); + } else { + n->cond = parseexpr(p); + if (accept(p, TK_SEMI)) { + /* second clause: just consumed ';' but actually + * we need cond/init pattern. Simplify: only support + * `for (cond)` and `for (let init; cond; post)`. + * Treat the first form as having no init. + */ + } + } + expect(p, TK_RPAREN); + n->body = parseblock(p); + return n; +} + +static Node * +parseswitch(Parser *p) +{ + Pos pp = p->cur.pos; + expect(p, TK_SWITCH); + expect(p, TK_LPAREN); + Node *n = newnode(p->a, N_SWITCH, pp); + n->lhs = parseexpr(p); + expect(p, TK_RPAREN); + expect(p, TK_LBRACE); + Node *head = NULL, *tail = NULL; + while (p->cur.kind == TK_CASE) { + Pos cp = p->cur.pos; + advance(p); + Node *c = newnode(p->a, N_CASE, cp); + Node *eh = NULL, *et = NULL; + if (p->cur.kind != TK_COLON) { + p->nocast = 1; + for (;;) { + Node *e = parseexpr(p); + if (eh == NULL) eh = e; + else et->next = e; + et = e; + if (!accept(p, TK_COMMA)) + break; + } + p->nocast = 0; + } + c->list = eh; + expect(p, TK_COLON); + Node *bh = NULL, *bt = NULL; + while (p->cur.kind != TK_CASE && p->cur.kind != TK_RBRACE + && p->cur.kind != TK_EOF) { + Node *s = parsestmt(p); + if (bh == NULL) bh = s; + else bt->next = s; + bt = s; + } + Node *blk = newnode(p->a, N_BLOCK, cp); + blk->list = bh; + c->body = blk; + if (head == NULL) head = c; + else tail->next = c; + tail = c; + } + expect(p, TK_RBRACE); + n->list = head; + return n; +} + +static Node * +parsestmt(Parser *p) +{ + /* Hare's `static` qualifies a statement to mean "the storage + * grown by this statement comes from the function's stack + * frame, not the heap". We don't optimise around it yet, so + * parse-and-ignore: the inner statement is unchanged. */ + if (p->cur.kind == TK_STATIC) advance(p); + Pos pp = p->cur.pos; + switch (p->cur.kind) { + case TK_LBRACE: { + Node *b = parseblock(p); + expect(p, TK_SEMI); + return b; + } + case TK_LET: return parselet(p, 0); + case TK_IF: { + Node *n = parseif(p); + expect(p, TK_SEMI); + return n; + } + case TK_FOR: { + Node *n = parsefor(p); + expect(p, TK_SEMI); + return n; + } + case TK_SWITCH: { + Node *n = parseswitch(p); + expect(p, TK_SEMI); + return n; + } + case TK_RETURN: { + advance(p); + Node *n = newnode(p->a, N_RETURN, pp); + if (p->cur.kind != TK_SEMI) { + Node *first = parseexpr(p); + if (p->cur.kind == TK_COMMA) { + Node *t = newnode(p->a, N_TUPLE, first->pos); + t->list = first; + Node *tail = first; + while (accept(p, TK_COMMA)) { + Node *e = parseexpr(p); + tail->next = e; + tail = e; + } + n->lhs = t; + } else { + n->lhs = first; + } + } + expect(p, TK_SEMI); + return n; + } + case TK_DEFER: { + advance(p); + Node *n = newnode(p->a, N_DEFER, pp); + n->lhs = parseexpr(p); + expect(p, TK_SEMI); + return n; + } + case TK_BREAK: { + advance(p); + Node *n = newnode(p->a, N_BREAK, pp); + expect(p, TK_SEMI); + return n; + } + case TK_CONTINUE: { + advance(p); + Node *n = newnode(p->a, N_CONTINUE, pp); + expect(p, TK_SEMI); + return n; + } + default: { + Node *e = parseexpr(p); + if (p->cur.kind == TK_COMMA) { + /* multi-assign: a, b, ... = expr; + * lvalues must not consume the trailing '=' as an + * assignment-chain (parseexpr would). */ + Node *m = newnode(p->a, N_MASSIGN, pp); + Node *head = e, *tail = e; + while (accept(p, TK_COMMA)) { + Node *lv = parseexpr_noassign(p); + tail->next = lv; + tail = lv; + } + expect(p, TK_ASSIGN); + m->rhs = parseexpr(p); + m->list = head; + expect(p, TK_SEMI); + return m; + } + Node *n = newnode(p->a, N_EXPRSTMT, pp); + n->lhs = e; + expect(p, TK_SEMI); + return n; + } + } +} + +static Node * +parseblock(Parser *p) +{ + Pos pp = p->cur.pos; + expect(p, TK_LBRACE); + Node *n = newnode(p->a, N_BLOCK, pp); + Node *head = NULL, *tail = NULL; + while (p->cur.kind != TK_RBRACE && p->cur.kind != TK_EOF) { + Node *s = parsestmt(p); + if (head == NULL) head = s; + else tail->next = s; + tail = s; + } + expect(p, TK_RBRACE); + n->list = head; + return n; +} + +/* ------- top-level decls ------------------------------------------- */ + +static Node * +parseuse(Parser *p) +{ + Pos pp = p->cur.pos; + expect(p, TK_USE); + Node *n = newnode(p->a, N_USE, pp); + const char *base = expectident(p); + while (accept(p, TK_DOT)) + base = aprintf(p->a, "%s.%s", base, expectident(p)); + n->str = base; + n->strlen = strlen(base); + expect(p, TK_SEMI); + return n; +} + +static Node * +parsedef(Parser *p, int exp) +{ + Pos pp = p->cur.pos; + expect(p, TK_DEF); + Node *n = newnode(p->a, N_DEF, pp); + n->str = expectident(p); + expect(p, TK_COLON); + n->lhs = parsetype(p); + expect(p, TK_ASSIGN); + n->rhs = parseexpr(p); + expect(p, TK_SEMI); + n->export = exp; + return n; +} + +static Node * +parsetypedecl(Parser *p, int exp) +{ + Pos pp = p->cur.pos; + expect(p, TK_TYPE); + Node *n = newnode(p->a, N_TYPEDECL, pp); + n->str = expectident(p); + expect(p, TK_ASSIGN); + n->lhs = parsetype(p); + expect(p, TK_SEMI); + n->export = exp; + return n; +} + +static Node * +parseattrs(Parser *p) +{ + Node *head = NULL, *tail = NULL; + while (p->cur.kind == TK_AT) { + Pos pp = p->cur.pos; + advance(p); + Node *a = newnode(p->a, N_ATTR, pp); + a->str = expectident(p); + expect(p, TK_LPAREN); + a->list = parsearglist(p, TK_RPAREN); + expect(p, TK_RPAREN); + if (head == NULL) head = a; + else tail->next = a; + tail = a; + } + return head; +} + +static Node * +parsefn(Parser *p, int exp, Node *attrs) +{ + Pos pp = p->cur.pos; + expect(p, TK_FN); + Node *n = newnode(p->a, N_FNDECL, pp); + n->str = expectident(p); + expect(p, TK_LPAREN); + n->list = parseparams(p); + expect(p, TK_RPAREN); + /* return type is required: void if absent only via explicit name */ + if (p->cur.kind != TK_ASSIGN && p->cur.kind != TK_SEMI) + n->lhs = parsetype(p); + if (accept(p, TK_ASSIGN)) { + n->body = parseblock(p); + expect(p, TK_SEMI); + } else { + expect(p, TK_SEMI); + } + n->export = exp; + n->attr = attrs; + return n; +} + +Node * +parsefile(Parser *p) +{ + Pos pp = { p->l->file, 1, 1 }; + Node *file = newnode(p->a, N_FILE, pp); + Node *head = NULL, *tail = NULL; + while (p->cur.kind != TK_EOF) { + Node *attrs = parseattrs(p); + int exp = accept(p, TK_EXPORT); + Node *d = NULL; + switch (p->cur.kind) { + case TK_USE: + if (attrs || exp) { + errorf(p->cur.pos, "use cannot be exported or attributed"); + p->errs++; + } + d = parseuse(p); + break; + 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; + default: + errorf(p->cur.pos, "expected top-level decl, got %s", + tokname(p->cur.kind)); + p->errs++; + advance(p); + continue; + } + if (head == NULL) head = d; + else tail->next = d; + tail = d; + } + file->list = head; + return file; +} diff --git a/cmd/wwc/sym.c b/cmd/wwc/sym.c new file mode 100644 index 00000000..9da8f68d --- /dev/null +++ b/cmd/wwc/sym.c @@ -0,0 +1,74 @@ +/* + * sym.c — symbol table. Plan 9-flavoured: a per-scope hashtable + * chained to the parent scope. Lookup walks up. Duplicate definitions + * within the same scope are flagged by the caller (we just refuse the + * insert and return the first one). + */ +#include "ww.h" +#include + +#define INIT_BUCKETS 16 + +static u64 +hashstr(const char *s) +{ + /* FNV-1a 64-bit; small fixed footprint, decent distribution */ + u64 h = 0xcbf29ce484222325ULL; + for (; *s; s++) { + h ^= (unsigned char)*s; + h *= 0x100000001b3ULL; + } + return h; +} + +Scope * +newscope(Arena *a, Scope *parent) +{ + Scope *s = amalloc(a, sizeof *s); + s->parent = parent; + s->a = a; + s->nbuckets = INIT_BUCKETS; + s->buckets = amalloc(a, s->nbuckets * sizeof(Sym *)); + return s; +} + +Sym * +scope_lookup_local(Scope *s, const char *name) +{ + if (s == NULL) return NULL; + u64 h = hashstr(name) % s->nbuckets; + for (Sym *b = s->buckets[h]; b; b = b->hashnext) + if (strcmp(b->name, name) == 0) + return b; + return NULL; +} + +Sym * +scope_lookup(Scope *s, const char *name) +{ + for (; s; s = s->parent) { + Sym *r = scope_lookup_local(s, name); + if (r) return r; + } + return NULL; +} + +Sym * +scope_define(Scope *s, const char *name, Skind k, Type *t, Node *decl) +{ + if (scope_lookup_local(s, name) != NULL) + return NULL; + Sym *sy = amalloc(s->a, sizeof *sy); + sy->name = name; + sy->kind = k; + sy->type = t; + sy->decl = decl; + sy->scope = s; + u64 h = hashstr(name) % s->nbuckets; + sy->hashnext = s->buckets[h]; + s->buckets[h] = sy; + if (s->first == NULL) s->first = sy; + else s->last->next = sy; + s->last = sy; + return sy; +} diff --git a/cmd/wwc/tok.c b/cmd/wwc/tok.c new file mode 100644 index 00000000..ff1627df --- /dev/null +++ b/cmd/wwc/tok.c @@ -0,0 +1,199 @@ +/* + * tok.c — token names, keyword lookup, debug printer. + * + * One table-of-records keyed by kind. The keyword subset is also + * scanned linearly during lexing — fewer than 25 entries, a hash + * isn't worth it. + */ +#include "ww.h" +#include + +struct kwent { + const char *s; + Tkind kind; +}; + +/* keep alphabetised, so kwlookup is easy to read. */ +static const struct kwent kwtab[] = { + { "as", TK_AS }, + { "break", TK_BREAK }, + { "case", TK_CASE }, + { "chan", TK_CHAN }, + { "continue", TK_CONTINUE }, + { "def", TK_DEF }, + { "defer", TK_DEFER }, + { "else", TK_ELSE }, + { "export", TK_EXPORT }, + { "false", TK_FALSE }, + { "fn", TK_FN }, + { "for", TK_FOR }, + { "if", TK_IF }, + { "let", TK_LET }, + { "match", TK_MATCH }, + { "nil", TK_NIL }, + { "proc", TK_PROC }, + { "return", TK_RETURN }, + { "static", TK_STATIC }, + { "struct", TK_STRUCT }, + { "switch", TK_SWITCH }, + { "true", TK_TRUE }, + { "type", TK_TYPE }, + { "use", TK_USE } +}; + +Tkind +kwlookup(const char *s, u64 n) +{ + /* linear scan: small N, predictable, branchy fall-through is fine. */ + for (u64 i = 0; i < nelem(kwtab); i++) { + const char *k = kwtab[i].s; + if (strlen(k) == n && memcmp(k, s, n) == 0) + return kwtab[i].kind; + } + return TK_NONE; +} + +const char * +tokname(Tkind k) +{ + switch (k) { + case TK_NONE: return ""; + case TK_EOF: return "EOF"; + case TK_ERR: return "ERR"; + case TK_IDENT: return "IDENT"; + case TK_INT: return "INT"; + case TK_FLOAT: return "FLOAT"; + case TK_RUNE: return "RUNE"; + case TK_STR: return "STR"; + + case TK_FN: return "fn"; + case TK_LET: return "let"; + case TK_DEF: return "def"; + case TK_IF: return "if"; + case TK_ELSE: return "else"; + case TK_FOR: return "for"; + case TK_SWITCH: return "switch"; + case TK_CASE: return "case"; + case TK_RETURN: return "return"; + case TK_USE: return "use"; + case TK_TYPE: return "type"; + case TK_STRUCT: return "struct"; + case TK_DEFER: return "defer"; + case TK_BREAK: return "break"; + case TK_CONTINUE: return "continue"; + case TK_EXPORT: return "export"; + case TK_PROC: return "proc"; + case TK_CHAN: return "chan"; + case TK_NIL: return "nil"; + case TK_TRUE: return "true"; + case TK_FALSE: return "false"; + case TK_AS: return "as"; + case TK_STATIC: return "static"; + case TK_MATCH: return "match"; + + case TK_LPAREN: return "("; + case TK_RPAREN: return ")"; + case TK_LBRACE: return "{"; + case TK_RBRACE: return "}"; + case TK_LBRACK: return "["; + case TK_RBRACK: return "]"; + case TK_COMMA: return ","; + case TK_SEMI: return ";"; + case TK_COLON: return ":"; + case TK_DOT: return "."; + case TK_ELLIPSIS: return "..."; + case TK_DOTDOT: return ".."; + case TK_AT: return "@"; + case TK_QUESTION: return "?"; + + case TK_ASSIGN: return "="; + case TK_PLUSEQ: return "+="; + case TK_MINUSEQ: return "-="; + case TK_STAREQ: return "*="; + case TK_SLASHEQ: return "/="; + case TK_PERCENTEQ: return "%="; + case TK_AMPEQ: return "&="; + case TK_PIPEEQ: return "|="; + case TK_CARETEQ: return "^="; + case TK_LSHIFTEQ: return "<<="; + case TK_RSHIFTEQ: return ">>="; + + case TK_PLUS: return "+"; + case TK_MINUS: return "-"; + case TK_STAR: return "*"; + case TK_SLASH: return "/"; + case TK_PERCENT: return "%"; + case TK_AMP: return "&"; + case TK_PIPE: return "|"; + case TK_CARET: return "^"; + case TK_TILDE: return "~"; + case TK_LSHIFT: return "<<"; + case TK_RSHIFT: return ">>"; + + case TK_EQ: return "=="; + case TK_NEQ: return "!="; + case TK_LT: return "<"; + case TK_LE: return "<="; + case TK_GT: return ">"; + case TK_GE: return ">="; + + case TK_AND: return "&&"; + case TK_OR: return "||"; + case TK_NOT: return "!"; + + case TK_LARROW: return "<-"; + case TK_ARROW: return "->"; + case TK_FATARROW: return "=>"; + + case TK_LAST: return ""; + } + return ""; +} + +static void +fputq(FILE *f, const char *s, u64 n) +{ + fputc('"', f); + for (u64 i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '\\': fputs("\\\\", f); break; + case '"': fputs("\\\"", f); break; + case '\n': fputs("\\n", f); break; + case '\t': fputs("\\t", f); break; + case '\r': fputs("\\r", f); break; + default: + if (c < 0x20 || c == 0x7f) + fprintf(f, "\\x%02x", c); + else + fputc(c, f); + } + } + fputc('"', f); +} + +void +tokprint(FILE *f, Tok t) +{ + fprintf(f, "%s:%d:%d %s", + t.pos.file ? t.pos.file : "", t.pos.line, t.pos.col, + tokname(t.kind)); + switch (t.kind) { + case TK_IDENT: + case TK_STR: + case TK_ERR: + fputc(' ', f); + fputq(f, t.text, t.tlen); + break; + case TK_INT: + case TK_RUNE: + fprintf(f, " %llu", (unsigned long long)t.v.uval); + break; + case TK_FLOAT: + fprintf(f, " %g", t.v.fval); + break; + default: + break; + } + fputc('\n', f); +} diff --git a/cmd/wwc/type.c b/cmd/wwc/type.c new file mode 100644 index 00000000..76402cd4 --- /dev/null +++ b/cmd/wwc/type.c @@ -0,0 +1,370 @@ +/* + * type.c — Type values and structural equality. + * + * Built-in types are constructed once and exposed as globals so the + * rest of the compiler can `==`-compare them. Compound types (ptr, + * slice, array, fn, struct, chan) are constructed on demand and + * de-duplicated when equality is cheap (only ptr/slice for now). + */ +#include "ww.h" +#include + +Type *ty_void, *ty_bool, *ty_rune; +Type *ty_i8, *ty_i16, *ty_i32, *ty_i64; +Type *ty_u8, *ty_u16, *ty_u32, *ty_u64; +Type *ty_int, *ty_uint, *ty_uintptr; +Type *ty_f32, *ty_f64, *ty_str; +Type *ty_err; +Type *ty_untyped_int, *ty_untyped_float, *ty_untyped_str; +Type *ty_untyped_rune, *ty_untyped_bool, *ty_untyped_nil; + +Type * +newtype(Arena *a, TypeKind k) +{ + Type *t = amalloc(a, sizeof *t); + t->kind = k; + return t; +} + +static Type * +prim(Arena *a, TypeKind k, const char *nm, u64 sz, u64 al) +{ + Type *t = newtype(a, k); + t->name = nm; + t->size = sz; + t->align = al ? al : sz; + return t; +} + +void +typesinit(Arena *a) +{ + /* Always re-init: callers create a fresh arena per compilation unit + * and free it; old globals point at freed memory. */ + ty_void = prim(a, TY_VOID, "void", 0, 1); + ty_bool = prim(a, TY_BOOL, "bool", 1, 1); + ty_rune = prim(a, TY_RUNE, "rune", 4, 4); + + ty_i8 = prim(a, TY_I8, "i8", 1, 1); + ty_i16 = prim(a, TY_I16, "i16", 2, 2); + ty_i32 = prim(a, TY_I32, "i32", 4, 4); + ty_i64 = prim(a, TY_I64, "i64", 8, 8); + ty_u8 = prim(a, TY_U8, "u8", 1, 1); + ty_u16 = prim(a, TY_U16, "u16", 2, 2); + ty_u32 = prim(a, TY_U32, "u32", 4, 4); + ty_u64 = prim(a, TY_U64, "u64", 8, 8); + ty_int = prim(a, TY_INT, "int", 8, 8); /* amd64 */ + ty_uint = prim(a, TY_UINT, "uint", 8, 8); + ty_uintptr= prim(a, TY_UINTPTR,"uintptr", 8, 8); + ty_f32 = prim(a, TY_F32, "f32", 4, 4); + ty_f64 = prim(a, TY_F64, "f64", 8, 8); + /* str is { *u8, len } — 16 bytes on amd64. ABI: pointer + u64. */ + ty_str = prim(a, TY_STR, "str", 16, 8); + ty_err = prim(a, TY_ERR, "", 0, 1); + + ty_untyped_int = prim(a, TY_UNTYPED_INT, "untyped_int", 0, 1); + ty_untyped_float = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0, 1); + ty_untyped_str = prim(a, TY_UNTYPED_STR, "untyped_str", 0, 1); + ty_untyped_rune = prim(a, TY_UNTYPED_RUNE, "untyped_rune", 0, 1); + ty_untyped_bool = prim(a, TY_UNTYPED_BOOL, "untyped_bool", 0, 1); + ty_untyped_nil = prim(a, TY_UNTYPED_NIL, "untyped_nil", 0, 1); +} + +Type * +type_ptr(Arena *a, Type *sub) +{ + Type *t = newtype(a, TY_PTR); + t->sub = sub; + t->size = 8; + t->align = 8; + return t; +} + +Type * +type_slice(Arena *a, Type *sub) +{ + Type *t = newtype(a, TY_SLICE); + t->sub = sub; + t->size = 24; /* { *T, len, cap } */ + t->align = 8; + return t; +} + +Type * +type_array(Arena *a, Type *sub, u64 len) +{ + Type *t = newtype(a, TY_ARRAY); + t->sub = sub; + t->alen = len; + t->size = sub ? sub->size * len : 0; + t->align = sub ? sub->align : 1; + return t; +} + +Type * +type_chan(Arena *a, Type *sub) +{ + Type *t = newtype(a, TY_CHAN); + t->sub = sub; + t->size = 8; /* opaque ptr */ + t->align = 8; + return t; +} + +Type * +type_named(Arena *a, const char *name, Type *under) +{ + Type *t = newtype(a, TY_NAMED); + t->name = name; + t->under = under; + if (under) { + t->size = under->size; + t->align = under->align; + } + return t; +} + +int +type_isint(Type *t) +{ + if (t == NULL) return 0; + switch (t->kind) { + case TY_I8: case TY_I16: case TY_I32: case TY_I64: + case TY_U8: case TY_U16: case TY_U32: case TY_U64: + case TY_INT: case TY_UINT: case TY_UINTPTR: + case TY_RUNE: + case TY_UNTYPED_INT: + case TY_UNTYPED_RUNE: + return 1; + case TY_NAMED: return type_isint(t->under); + default: return 0; + } +} + +int +type_isfloat(Type *t) +{ + if (t == NULL) return 0; + switch (t->kind) { + case TY_F32: case TY_F64: case TY_UNTYPED_FLOAT: + return 1; + case TY_NAMED: return type_isfloat(t->under); + default: return 0; + } +} + +int +type_isnum(Type *t) +{ + return type_isint(t) || type_isfloat(t); +} + +int +type_isunsigned(Type *t) +{ + if (t == NULL) return 0; + switch (t->kind) { + case TY_U8: case TY_U16: case TY_U32: case TY_U64: + case TY_UINT: case TY_UINTPTR: + return 1; + case TY_NAMED: return type_isunsigned(t->under); + default: return 0; + } +} + +int +type_isuntyped(Type *t) +{ + if (t == NULL) return 0; + switch (t->kind) { + case TY_UNTYPED_INT: case TY_UNTYPED_FLOAT: case TY_UNTYPED_STR: + case TY_UNTYPED_RUNE: case TY_UNTYPED_BOOL: case TY_UNTYPED_NIL: + return 1; + default: return 0; + } +} + +Type * +type_default(Type *t) +{ + if (t == NULL) return NULL; + switch (t->kind) { + case TY_UNTYPED_INT: return ty_i32; + case TY_UNTYPED_FLOAT: return ty_f64; + case TY_UNTYPED_STR: return ty_str; + case TY_UNTYPED_RUNE: return ty_rune; + case TY_UNTYPED_BOOL: return ty_bool; + case TY_UNTYPED_NIL: return NULL; /* needs context */ + default: return t; + } +} + +int +type_eq(Type *a, Type *b) +{ + if (a == b) return 1; + if (a == NULL || b == NULL) return 0; + if (a->kind != b->kind) return 0; + switch (a->kind) { + case TY_PTR: case TY_SLICE: case TY_CHAN: + return type_eq(a->sub, b->sub); + case TY_ARRAY: + return a->alen == b->alen && type_eq(a->sub, b->sub); + case TY_FN: { + if (a->variadic != b->variadic) return 0; + if (!type_eq(a->ret, b->ret)) return 0; + Tparam *pa = a->params, *pb = b->params; + while (pa && pb) { + if (!type_eq(pa->type, pb->type)) return 0; + pa = pa->next; pb = pb->next; + } + return pa == NULL && pb == NULL; + } + case TY_STRUCT: { + Tfield *fa = a->fields, *fb = b->fields; + while (fa && fb) { + if (strcmp(fa->name, fb->name) != 0) return 0; + if (!type_eq(fa->type, fb->type)) return 0; + fa = fa->next; fb = fb->next; + } + return fa == NULL && fb == NULL; + } + case TY_NAMED: + return a == b; /* nominally equal only when same node */ + case TY_TUPLE: { + Tparam *pa = a->params, *pb = b->params; + while (pa && pb) { + if (!type_eq(pa->type, pb->type)) return 0; + pa = pa->next; pb = pb->next; + } + return pa == NULL && pb == NULL; + } + default: return 1; /* primitives */ + } +} + +int +type_assignable(Type *dst, Type *src) +{ + if (dst == NULL || src == NULL) return 0; + if (dst == ty_err || src == ty_err) return 1; /* swallow */ + if (type_eq(dst, src)) return 1; + + /* Tagged-union variant inclusion: src is one of dst's variants. + * Checked before the untyped branch so untyped literals (e.g. + * 0, "msg") flow through to a variant's typed slot. Unwraps a + * named alias on either side so `type result = (T | E);` also + * accepts variants and the inverse. */ + { + Type *du = (dst->kind == TY_NAMED) ? dst->under : dst; + Type *su = (src->kind == TY_NAMED) ? src->under : src; + if (du && du->kind == TY_TAGGED && + !(su && su->kind == TY_TAGGED)) { + for (Tparam *p = du->params; p; p = p->next) + if (type_assignable(p->type, src)) return 1; + return 0; + } + } + + /* Untyped → typed: only if the typed kind can hold the value. */ + if (type_isuntyped(src)) { + if (src->kind == TY_UNTYPED_INT && type_isnum(dst)) return 1; + if (src->kind == TY_UNTYPED_FLOAT && type_isfloat(dst)) return 1; + if (src->kind == TY_UNTYPED_STR && (dst->kind == TY_STR || + (dst->kind == TY_NAMED && dst->under && dst->under->kind == TY_STR))) return 1; + if (src->kind == TY_UNTYPED_RUNE && (type_isint(dst) || dst->kind == TY_RUNE)) return 1; + if (src->kind == TY_UNTYPED_BOOL && (dst->kind == TY_BOOL || + (dst->kind == TY_NAMED && dst->under && dst->under->kind == TY_BOOL))) return 1; + if (src->kind == TY_UNTYPED_NIL) { + Type *du = (dst->kind == TY_NAMED) ? dst->under : dst; + if (du && (du->kind == TY_PTR || du->kind == TY_SLICE || + du->kind == TY_CHAN || du->kind == TY_FN)) + return 1; + } + return 0; + } + + /* Named on either side: compare to the underlying. NAMED is a + * distinct type from its under; but assignment from under to + * named (and vice-versa) is allowed in this minimal checker. */ + if (dst->kind == TY_NAMED && type_eq(dst->under, src)) return 1; + if (src->kind == TY_NAMED && type_eq(dst, src->under)) return 1; + + /* Tuple-to-tuple: element-wise assignable. */ + if (dst->kind == TY_TUPLE && src->kind == TY_TUPLE) { + Tparam *pa = dst->params, *pb = src->params; + while (pa && pb) { + if (!type_assignable(pa->type, pb->type)) return 0; + pa = pa->next; pb = pb->next; + } + return pa == NULL && pb == NULL; + } + + return 0; +} + +const char * +type_name(Arena *a, Type *t) +{ + if (t == NULL) return ""; + switch (t->kind) { + case TY_NONE: return ""; + case TY_VOID: return "void"; + case TY_BOOL: return "bool"; + case TY_RUNE: return "rune"; + case TY_I8: return "i8"; + case TY_I16: return "i16"; + case TY_I32: return "i32"; + case TY_I64: return "i64"; + case TY_U8: return "u8"; + case TY_U16: return "u16"; + case TY_U32: return "u32"; + case TY_U64: return "u64"; + case TY_INT: return "int"; + case TY_UINT: return "uint"; + case TY_UINTPTR: return "uintptr"; + case TY_F32: return "f32"; + case TY_F64: return "f64"; + case TY_STR: return "str"; + case TY_ERR: return ""; + case TY_UNTYPED_INT: return "untyped_int"; + case TY_UNTYPED_FLOAT: return "untyped_float"; + case TY_UNTYPED_STR: return "untyped_str"; + case TY_UNTYPED_RUNE: return "untyped_rune"; + case TY_UNTYPED_BOOL: return "untyped_bool"; + case TY_UNTYPED_NIL: return "untyped_nil"; + case TY_PTR: return aprintf(a, "*%s", type_name(a, t->sub)); + case TY_SLICE: return aprintf(a, "[]%s", type_name(a, t->sub)); + case TY_ARRAY: return aprintf(a, "[%llu]%s", + (unsigned long long)t->alen, type_name(a, t->sub)); + case TY_CHAN: return aprintf(a, "chan %s", type_name(a, t->sub)); + case TY_FN: { + const char *r = t->ret ? type_name(a, t->ret) : "void"; + const char *acc = ""; + for (Tparam *p = t->params; p; p = p->next) { + const char *pn = type_name(a, p->type); + acc = acc[0] ? aprintf(a, "%s, %s", acc, pn) : pn; + } + return aprintf(a, "fn(%s) %s", acc, r); + } + case TY_STRUCT: return t->name ? t->name : "struct{...}"; + case TY_NAMED: return t->name ? t->name : ""; + case TY_TUPLE: { + const char *acc = ""; + for (Tparam *p = t->params; p; p = p->next) { + const char *pn = type_name(a, p->type); + acc = acc[0] ? aprintf(a, "%s, %s", acc, pn) : pn; + } + return aprintf(a, "(%s)", acc); + } + case TY_TAGGED: { + const char *acc = ""; + for (Tparam *p = t->params; p; p = p->next) { + const char *pn = type_name(a, p->type); + acc = acc[0] ? aprintf(a, "%s | %s", acc, pn) : pn; + } + return aprintf(a, "(%s)", acc); + } + } + return "?"; +} diff --git a/cmd/wwc/ww.h b/cmd/wwc/ww.h new file mode 100644 index 00000000..4b91b60c --- /dev/null +++ b/cmd/wwc/ww.h @@ -0,0 +1,462 @@ +/* + * ww.h — central header for libwwc.a (the ww frontend library). + * + * Plan 9 in spirit. This file mirrors cc/cc.h's role: one shared + * header that declares everything every translation unit in the + * frontend cares about. + * + * Phases add to this file (lexer/parser/checker), they do not branch + * a sibling header. There is one frontend; there is one ww.h. + */ +#ifndef WW_H +#define WW_H + +#include +#include +#include +#include + +/* version banner — printed by `ww -V` */ +#define WW_VERSION "0.0" + +/* short integer aliases, Plan 9 / Hare-flavoured */ +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +/* forward decls — concrete shapes appear in their phases. */ +typedef struct Tok Tok; +typedef struct Lex Lex; +typedef struct Node Node; +typedef struct Sym Sym; +typedef struct Type Type; +typedef struct Scope Scope; +typedef struct Arena Arena; + +/* mem.c — bump arena (no free; reset/destroy at end of phase) */ +struct Arena { + u8 *buf; /* base of current chunk */ + u64 off; /* bytes used in current chunk */ + u64 cap; /* capacity of current chunk */ + struct Arena *next; /* older chunks (linked list, head = current) */ + u64 total; /* across all chunks, debug only */ +}; + +Arena *newarena(void); +void *amalloc(Arena*, u64); /* zeroed, aligned to 16 */ +char *astrdup(Arena*, const char*); +char *astrndup(Arena*, const char*, u64); +char *aprintf(Arena*, const char*, ...); +void freearena(Arena*); + +/* err.c — diagnostics. Phase 0 has only fatal/warn; later phases add + * source-location-bearing variants. */ +typedef struct Pos Pos; +struct Pos { + const char *file; + i32 line; + i32 col; +}; + +extern Pos noPos; +extern int nerrors; +extern int nwarnings; +extern FILE *errout; + +void fatal(const char*, ...) __attribute__((noreturn, format(printf, 1, 2))); +void errorf(Pos, const char*, ...) __attribute__((format(printf, 2, 3))); +void warnf(Pos, const char*, ...) __attribute__((format(printf, 2, 3))); + +/* tiny helpers */ +#define nelem(a) ((sizeof(a) / sizeof((a)[0]))) + +/* ---- lexer (lex.c, tok.c) ----------------------------------------- */ +typedef enum { + /* zero is "no token" so memset-zero structs read sane */ + TK_NONE = 0, + + /* trivial */ + TK_EOF, + TK_ERR, + TK_IDENT, + TK_INT, + TK_FLOAT, + TK_RUNE, + TK_STR, + + /* keywords — stay grouped, used by tok.c kwtab */ + TK_FN, + TK_LET, + TK_DEF, + TK_IF, + TK_ELSE, + TK_FOR, + TK_SWITCH, + TK_CASE, + TK_RETURN, + TK_USE, + TK_TYPE, + TK_STRUCT, + TK_DEFER, + TK_BREAK, + TK_CONTINUE, + TK_EXPORT, + TK_PROC, + TK_CHAN, + TK_NIL, + TK_TRUE, + TK_FALSE, + TK_AS, /* reserved for future cast spelling, not active */ + TK_STATIC, /* Hare-style storage-class qualifier */ + TK_MATCH, /* match expression head */ + + /* punct + operators */ + TK_LPAREN, /* ( */ + TK_RPAREN, /* ) */ + TK_LBRACE, /* { */ + TK_RBRACE, /* } */ + TK_LBRACK, /* [ */ + TK_RBRACK, /* ] */ + TK_COMMA, /* , */ + TK_SEMI, /* ; */ + TK_COLON, /* : */ + TK_DOT, /* . */ + TK_ELLIPSIS, /* ... */ + TK_DOTDOT, /* .. (range op) */ + TK_AT, /* @ */ + TK_QUESTION, /* ? */ + + TK_ASSIGN, /* = */ + TK_PLUSEQ, /* += */ + TK_MINUSEQ, /* -= */ + TK_STAREQ, /* *= */ + TK_SLASHEQ, /* /= */ + TK_PERCENTEQ, /* %= */ + TK_AMPEQ, /* &= */ + TK_PIPEEQ, /* |= */ + TK_CARETEQ, /* ^= */ + TK_LSHIFTEQ, /* <<= */ + TK_RSHIFTEQ, /* >>= */ + + TK_PLUS, /* + */ + TK_MINUS, /* - */ + TK_STAR, /* * */ + TK_SLASH, /* / */ + TK_PERCENT, /* % */ + TK_AMP, /* & */ + TK_PIPE, /* | */ + TK_CARET, /* ^ */ + TK_TILDE, /* ~ */ + TK_LSHIFT, /* << */ + TK_RSHIFT, /* >> */ + + TK_EQ, /* == */ + TK_NEQ, /* != */ + TK_LT, /* < */ + TK_LE, /* <= */ + TK_GT, /* > */ + TK_GE, /* >= */ + + TK_AND, /* && */ + TK_OR, /* || */ + TK_NOT, /* ! */ + + TK_LARROW, /* <- (chan recv) */ + TK_ARROW, /* -> (reserved) */ + TK_FATARROW, /* => (match arms) */ + + TK_LAST /* sentinel for tables */ +} Tkind; + +struct Tok { + Tkind kind; + Pos pos; + const char *text; /* lexeme (arena-owned, NUL-terminated) */ + u64 tlen; /* byte length of lexeme (sans NUL) */ + /* numeric values pre-parsed; string/rune unescaped */ + union { + u64 uval; /* TK_INT, TK_RUNE */ + double fval; /* TK_FLOAT */ + } v; + /* for typed numeric literals: "i32", "u8", "f64", ... or NULL. */ + const char *tsuffix; +}; + +struct Lex { + const char *file; + const char *src; /* full source, NUL-terminated */ + u64 srclen; + u64 pos; /* current byte offset */ + i32 line; + i32 col; + Arena *a; /* token-text arena */ + int errs; +}; + +void lexinit(Lex*, Arena*, const char *file, const char *src, u64 len); +Tok lexnext(Lex*); +const char *tokname(Tkind); /* canonical spelling, e.g. "fn", "+=" */ +void tokprint(FILE*, Tok); /* one line, "%s:%d:%d: %s %q" */ +Tkind kwlookup(const char *s, u64 n); /* TK_NONE if not a keyword */ + +/* ---- AST (ast.c, parse.c) ----------------------------------------- */ +typedef enum { + N_NONE = 0, + + /* literals */ + N_INTLIT, + N_FLOATLIT, + N_STRLIT, + N_RUNELIT, + N_TRUE, + N_FALSE, + N_NIL, + N_IDENT, + + /* expressions */ + N_BIN, /* op, lhs, rhs */ + N_UN, /* op, lhs */ + N_CALL, /* lhs=callee, list=args */ + N_INDEX, /* lhs=base, rhs=index */ + N_DOT, /* lhs=base, str=field */ + N_CAST, /* lhs=expr, rhs=type-expr */ + N_STRUCTLIT, /* lhs=type-expr, list=N_FIELD */ + N_ARRLIT, /* list=elements (for [a,b,...]) */ + N_FIELD, /* str=name, lhs=value */ + N_ASSIGN, /* op, lhs, rhs */ + N_ALLOC, /* lhs=expr, rhs=size-or-null */ + N_FREE, /* lhs=expr */ + N_RECV, /* lhs (chan recv: <-c) */ + N_SLICE, /* lhs=base, rhs=lo or NULL, cond=hi or NULL */ + N_SPREAD, /* lhs (variadic spread in arg position: e...) */ + + /* statements */ + N_BLOCK, /* list=stmts */ + N_EXPRSTMT, /* lhs=expr */ + N_LET, /* str=name, lhs=type-expr|NULL, rhs=init|NULL */ + N_RETURN, /* lhs=expr|NULL */ + N_IF, /* cond, body, els */ + N_FOR, /* lhs=init, cond, rhs=post, body */ + N_FORRANGE, /* str=elem name, lhs=slice expr, body=block */ + N_DEFER, /* lhs=expr */ + N_BREAK, + N_CONTINUE, + N_SWITCH, /* lhs=scrutinee, list=cases */ + N_CASE, /* list=exprs (empty=default), body */ + + /* declarations */ + N_FILE, /* list=top decls */ + N_USE, /* str=path */ + N_DEF, /* str=name, lhs=type|NULL, rhs=init */ + N_TYPEDECL, /* str=name, lhs=type-expr */ + N_FNDECL, /* str=name, list=params, lhs=ret-type, body|NULL */ + N_PARAM, /* str=name, lhs=type-expr */ + + /* type expressions */ + N_TNAME, /* str */ + N_TPTR, /* lhs=inner */ + N_TSLICE, /* lhs=inner */ + N_TARRAY, /* lhs=element, rhs=len-expr */ + N_TFN, /* list=params, lhs=ret */ + N_TSTRUCT, /* list=fields */ + N_TFIELD, /* str=name, lhs=type */ + N_TCHAN, /* lhs=element */ + + /* attribute on a decl */ + N_ATTR, /* str=name, list=args */ + + /* multi-value (tuple) plumbing */ + N_TTUPLE, /* type expr: (T1, T2, ...). list = element type exprs */ + N_TTAGGED, /* type expr: (T1 | T2 | ...). list = variant type exprs */ + N_TUPLE, /* expr: (e1, e2, ...). list = element exprs */ + N_MATCH, /* match (lhs) { list of cases }; cases are N_MCASE */ + N_MCASE, /* str=binding name (or NULL), lhs=variant type expr or NULL, body */ + N_TRYPROP, /* lhs? — propagate error variant */ + N_TRYUNW, /* lhs! — abort on error variant */ + N_MLET, /* let a, b = expr; list = N_LET stubs (str, lhs=type), rhs = expr */ + N_MASSIGN, /* a, b = expr; list = lvalue exprs, rhs = expr */ + + N_LAST +} Nkind; + +struct Node { + Nkind kind; + Pos pos; + Tkind op; /* for N_BIN/N_UN/N_ASSIGN */ + const char *str; /* identifier/literal/name/path */ + u64 strlen; + u64 uval; /* int/rune lit */ + double fval; /* float lit */ + Node *lhs; + Node *rhs; + Node *cond; + Node *body; + Node *els; + Node *list; /* head of singly-linked sibling chain */ + Node *next; /* sibling link inside `list` */ + Node *attr; /* @attribute chain (N_ATTR list) */ + int export; + Type *type; /* filled in by checker */ + const char *tsuffix; /* typed numeric literal suffix */ +}; + +Node *newnode(Arena*, Nkind, Pos); +void astprint(FILE*, Node*); /* s-expr, deterministic, one-line per node */ + +typedef struct Parser Parser; +struct Parser { + Lex *l; + Arena *a; + Tok cur; + Tok la; /* one-token lookahead buffer */ + int hasla; + int errs; + int nocast; /* in case-selector ctx, ':' is a separator */ +}; + +void parserinit(Parser*, Arena*, Lex*); +Node *parsefile(Parser*); +Node *parseexpr_top(Parser*); /* for testing: parse one expression */ + +/* ---- types (type.c) ----------------------------------------------- */ +typedef enum { + TY_NONE = 0, + TY_VOID, + TY_BOOL, + TY_RUNE, + TY_I8, TY_I16, TY_I32, TY_I64, + TY_U8, TY_U16, TY_U32, TY_U64, + TY_UINT, TY_INT, + TY_UINTPTR, + TY_F32, TY_F64, + TY_STR, + TY_PTR, + TY_SLICE, + TY_ARRAY, + TY_STRUCT, + TY_FN, + TY_CHAN, + TY_NAMED, + TY_TUPLE, + TY_TAGGED, /* (T1 | T2 | ...) — Hare-style sum type */ + TY_ERR, + /* untyped constants (not surfaced to users; checker-internal) */ + TY_UNTYPED_INT, + TY_UNTYPED_FLOAT, + TY_UNTYPED_STR, + TY_UNTYPED_RUNE, + TY_UNTYPED_BOOL, + TY_UNTYPED_NIL +} TypeKind; + +typedef struct Tfield Tfield; +struct Tfield { + const char *name; + Type *type; + u64 offset; + Tfield *next; +}; + +typedef struct Tparam Tparam; +struct Tparam { + const char *name; + Type *type; + Tparam *next; +}; + +struct Type { + TypeKind kind; + u64 size; + u64 align; + Type *sub; /* ptr/slice/array/chan element */ + u64 alen; /* array length */ + Tfield *fields;/* struct */ + Tparam *params;/* fn */ + Type *ret; /* fn */ + int variadic; + const char *name; /* named alias / debug */ + Type *under; /* underlying resolved type for NAMED */ +}; + +extern Type *ty_void, *ty_bool, *ty_rune; +extern Type *ty_i8, *ty_i16, *ty_i32, *ty_i64; +extern Type *ty_u8, *ty_u16, *ty_u32, *ty_u64; +extern Type *ty_int, *ty_uint, *ty_uintptr; +extern Type *ty_f32, *ty_f64, *ty_str; +extern Type *ty_err; +extern Type *ty_untyped_int, *ty_untyped_float, *ty_untyped_str; +extern Type *ty_untyped_rune, *ty_untyped_bool, *ty_untyped_nil; + +void typesinit(Arena*); +Type *newtype(Arena*, TypeKind); +Type *type_ptr(Arena*, Type *sub); +Type *type_slice(Arena*, Type *sub); +Type *type_array(Arena*, Type *sub, u64 len); +Type *type_chan(Arena*, Type *sub); +Type *type_named(Arena*, const char *name, Type *under); +const char *type_name(Arena*, Type*); /* arena'd debug string */ +int type_eq(Type *a, Type *b); /* structural equality */ +int type_isint(Type *t); +int type_isfloat(Type *t); +int type_isnum(Type *t); +int type_isunsigned(Type *t); +int type_isuntyped(Type *t); +int type_assignable(Type *dst, Type *src); +Type *type_default(Type *t); /* untyped → default concrete */ + +/* ---- symbols (sym.c) ---------------------------------------------- */ +typedef enum { + SK_NONE = 0, + SK_VAR, + SK_PARAM, + SK_DEF, + SK_TYPE, + SK_FN, + SK_USE, + SK_FIELD /* not stored in scope; used by check internally */ +} Skind; + +struct Sym { + const char *name; + Skind kind; + Type *type; + Node *decl; + int exported; + Sym *next; /* iteration */ + Sym *hashnext; /* bucket chain */ + Scope *scope; +}; + +struct Scope { + Scope *parent; + Sym *first, *last; + Sym **buckets; + u64 nbuckets; + Arena *a; +}; + +Scope *newscope(Arena*, Scope *parent); +Sym *scope_define(Scope*, const char *name, Skind, Type*, Node *decl); +Sym *scope_lookup(Scope*, const char *name); /* walk up parents */ +Sym *scope_lookup_local(Scope*, const char *name); + +/* ---- checker (check.c) -------------------------------------------- */ +typedef struct Checker Checker; +struct Checker { + Arena *a; + Scope *top; /* file scope */ + Scope *cur; /* current scope */ + Type *ret; /* expected return type of current fn (or NULL) */ + int loops; /* nesting count for break/continue */ + int errs; +}; + +void check_init(Checker*, Arena*); +void check_file(Checker*, Node *file); + +#endif /* WW_H */ diff --git a/cmd/wwdump/main.c b/cmd/wwdump/main.c new file mode 100644 index 00000000..2c699f41 --- /dev/null +++ b/cmd/wwdump/main.c @@ -0,0 +1,119 @@ +/* + * wwdump — deterministic dump tool for ww source. + * + * Reads a .ww file and writes either a token stream or an AST + * s-expression to stdout, byte-for-byte stable across runs. It is the + * diff anchor for self-host: the C-side libwwc and the future ww-side + * frontend must produce the same dump for the same input. + * + * wwdump -t file.ww tokens, one per line: ":: [val]" + * wwdump -a file.ww AST as s-expr, one node per line + * + * No newlines or trailing whitespace varies by phase of the moon. If + * the bytes differ, somebody changed the front end. + */ +#include "ww.h" +#include +#include +#include + +static int +slurp(const char *path, char **outbuf, u64 *outlen) +{ + FILE *f = fopen(path, "rb"); + if (f == NULL) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (b == NULL) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (u64)n; + return 0; +} + +static int +dump_tokens(const char *src, char *buf, u64 len, FILE *out) +{ + Arena *a = newarena(); + Lex l; + lexinit(&l, a, src, buf, len); + for (;;) { + Tok t = lexnext(&l); + tokprint(out, t); + if (t.kind == TK_EOF || t.kind == TK_ERR) break; + } + int errs = l.errs; + freearena(a); + return errs ? 1 : 0; +} + +static int +dump_ast(const char *src, char *buf, u64 len, FILE *out) +{ + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, src, buf, len); + parserinit(&p, a, &l); + Node *file = parsefile(&p); + int errs = l.errs || p.errs; + if (file) astprint(out, file); + freearena(a); + return errs ? 1 : 0; +} + +int +main(int argc, char **argv) +{ + int mode = 't'; /* tokens by default */ + const char *src = NULL; + const char *out = NULL; + for (int i = 1; i < argc; i++) { + const char *a = argv[i]; + if (strcmp(a, "-t") == 0) mode = 't'; + else if (strcmp(a, "-a") == 0) mode = 'a'; + else if (strcmp(a, "-o") == 0 && i + 1 < argc) out = argv[++i]; + else if (a[0] == '-') { + fprintf(stderr, "wwdump: unknown flag %s\n", a); + return 2; + } + else if (src == NULL) src = a; + else { + fputs("wwdump: only one input supported\n", stderr); + return 2; + } + } + if (src == NULL) { + fputs("usage: wwdump [-t|-a] [-o out] file.ww\n", stderr); + return 2; + } + + char *buf; + u64 len; + if (slurp(src, &buf, &len) < 0) { + fprintf(stderr, "wwdump: %s: cannot read\n", src); + return 1; + } + + FILE *of = stdout; + if (out) { + of = fopen(out, "wb"); + if (of == NULL) { + fprintf(stderr, "wwdump: cannot open %s\n", out); + return 1; + } + } + + int rc; + if (mode == 'a') rc = dump_ast(src, buf, len, of); + else rc = dump_tokens(src, buf, len, of); + + if (of != stdout) fclose(of); + free(buf); + return rc; +} diff --git a/lib/ascii/ascii.ww b/lib/ascii/ascii.ww new file mode 100644 index 00000000..b99cbf4d --- /dev/null +++ b/lib/ascii/ascii.ww @@ -0,0 +1,92 @@ +// ascii — byte-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family. Bytes outside 0..127 always +// answer `false`. The lexer hot path uses these inline; they are +// expected to inline to a couple of compares. + +export fn isdigit(c: u8) bool = { + if (c < 48u8) { return false; }; + if (c > 57u8) { return false; }; + return true; +}; + +export fn isupper(c: u8) bool = { + if (c < 65u8) { return false; }; + if (c > 90u8) { return false; }; + return true; +}; + +export fn islower(c: u8) bool = { + if (c < 97u8) { return false; }; + if (c > 122u8) { return false; }; + return true; +}; + +export fn isalpha(c: u8) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: u8) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: u8) bool = { + if (c == 32u8) { return true; }; // ' ' + if (c == 9u8) { return true; }; // '\t' + if (c == 10u8) { return true; }; // '\n' + if (c == 11u8) { return true; }; // '\v' + if (c == 12u8) { return true; }; // '\f' + if (c == 13u8) { return true; }; // '\r' + return false; +}; + +export fn ishex(c: u8) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65u8) { + if (c <= 70u8) { return true; }; // 'A'..'F' + }; + if (c >= 97u8) { + if (c <= 102u8) { return true; }; // 'a'..'f' + }; + return false; +}; + +// digitval — value of `c` as a hex/decimal digit, or -1 if not one. +// Useful when scanning numeric literals. +export fn digitval(c: u8) i32 = { + if (isdigit(c)) { return (c - 48u8): i32; }; + if (c >= 65u8) { + if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; + }; + if (c >= 97u8) { + if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; + }; + return -1; +}; + +// isidstart / isidpart — identifier classes used by the lexer. +// Alpha or '_' starts; alnum or '_' continues. +export fn isidstart(c: u8) bool = { + if (isalpha(c)) { return true; }; + if (c == 95u8) { return true; }; // '_' + return false; +}; + +export fn isidpart(c: u8) bool = { + if (isalnum(c)) { return true; }; + if (c == 95u8) { return true; }; + return false; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: u8) u8 = { + if (isupper(c)) { return c + 32u8; }; + return c; +}; + +export fn toupper(c: u8) u8 = { + if (islower(c)) { return c - 32u8; }; + return c; +}; diff --git a/lib/bufio/bufio.ww b/lib/bufio/bufio.ww new file mode 100644 index 00000000..bf31b084 --- /dev/null +++ b/lib/bufio/bufio.ww @@ -0,0 +1,82 @@ +// bufio — buffered reader/writer over io.stream. Plan 9 'bio' +// analogue, lowered to ww. The buffer is owned by the caller and +// passed in at init time; we don't allocate. +// +// We keep the stream pointer as *void here to avoid a cross-module +// type name (the module-import system isn't online yet); a later +// revision will replace it with *io.stream once `use` resolves +// types from imported modules. + +type stream_p = *void; + +type buf = struct { + s: stream_p, + data: *u8, + cap: i32, + r: i32, // read cursor + w: i32, // write cursor (for writers) +}; + +export fn rinit(b: *buf, s: stream_p, data: *u8, cap: i32) void = { + b.s = s; + b.data = data; + b.cap = cap; + b.r = 0; + b.w = 0; +}; + +// peek1 — look at the next byte without consuming. Returns -1 on +// empty buffer; the caller is responsible for refilling via the +// stream when this happens. +export fn peek1(b: *buf) i32 = { + if (b.r < b.w) { + return b.data[b.r]: i32; + }; + return -1; +}; + +// take1 — pop one byte. -1 if empty. +export fn take1(b: *buf) i32 = { + if (b.r < b.w) { + let c: u8 = b.data[b.r]; + b.r += 1; + return c: i32; + }; + return -1; +}; + +// avail — bytes left to read out of the buffer. +export fn avail(b: *buf) i32 = { + return b.w - b.r; +}; + +// Distinct alias so `(str | linerr)` has two variant types the +// tagged-union machinery can keep apart at the tag level. The error +// variant carries a short description; callers compare with errors.is +// or just inspect by length. +type linerr = str; + +// takeline — Hare-style fallible line read. Drains the buffer up to +// (but not including) the next '\n' and advances the cursor past the +// newline. Returns the line as a borrowed str on success, or a linerr +// describing why no line was available: +// - "eof" when the buffer is empty +// - "no newline" when the buffer contains data but no '\n' +// +// The returned str borrows from the underlying buffer; callers must +// consume it (or copy) before refilling. +export fn takeline(b: *buf) (str | linerr) = { + if (b.r >= b.w) { return "eof": linerr; }; + let i: i32 = b.r; + for (i < b.w) { + if (b.data[i] == 10u8) { + let s: str; + s.ptr = b.data + b.r; + s.len = i - b.r; + b.r = i + 1; + return s; + }; + i += 1; + }; + return "no newline": linerr; +}; diff --git a/lib/bytes/bytes.ww b/lib/bytes/bytes.ww new file mode 100644 index 00000000..91167652 --- /dev/null +++ b/lib/bytes/bytes.ww @@ -0,0 +1,51 @@ +// bytes — slice operations over []u8. + +export fn equal(a: []u8, b: []u8) bool = { + let i: i32 = 0; + for (i < a.len) { + if (i >= b.len) { return false; }; + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return i == b.len; +}; + +export fn indexbyte(s: []u8, c: u8) i32 = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] == c) { return i; }; + i += 1; + }; + return -1; +}; + +export fn copy(dst: []u8, src: []u8) i32 = { + let n: i32 = dst.len; + if (src.len < n) { n = src.len; }; + let i: i32 = 0; + for (i < n) { + dst[i] = src[i]; + i += 1; + }; + return n; +}; + +// indexsub — first index of `sub` in `s`, or -1. Mirrors +// strings.index but on []u8. Empty `sub` matches at 0. +export fn indexsub(s: []u8, sub: []u8) i32 = { + if (sub.len == 0) { return 0; }; + if (sub.len > s.len) { return -1; }; + let last: i32 = s.len - sub.len; + let i: i32 = 0; + for (i <= last) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i += 1; + }; + return -1; +}; diff --git a/lib/c/libc/libc.ww b/lib/c/libc/libc.ww new file mode 100644 index 00000000..c65aa644 --- /dev/null +++ b/lib/c/libc/libc.ww @@ -0,0 +1,22 @@ +// lib/c/libc — minimal libc bindings. Each declaration is body-less, +// imported from the C side at link time. The @symbol attribute pins +// the linker name; without it, the binding name itself is used. +// +// These declarations cover the small surface the bootstrap wants: +// process exit, three io syscalls, and the malloc/free pair. Higher +// ergonomics live in sibling pure-ww packages. + +@symbol("malloc") fn malloc(n: u64) *void; +@symbol("free") fn free(p: *void) void; +@symbol("calloc") fn calloc(n: u64, sz: u64) *void; + +@symbol("read") fn read(fd: i32, buf: *u8, n: u64) i64; +@symbol("write") fn write(fd: i32, buf: *u8, n: u64) i64; +@symbol("open") fn open(path: *u8, flags: i32, mode: i32) i32; +@symbol("close") fn close(fd: i32) i32; + +@symbol("exit") fn exit(code: i32) void; + +@symbol("strlen") fn strlen(s: *u8) u64; +@symbol("memcpy") fn memcpy(dst: *void, src: *void, n: u64) *void; +@symbol("memset") fn memset(p: *void, b: i32, n: u64) *void; diff --git a/lib/encoding/hex/hex.ww b/lib/encoding/hex/hex.ww new file mode 100644 index 00000000..5eaaf6c7 --- /dev/null +++ b/lib/encoding/hex/hex.ww @@ -0,0 +1,19 @@ +// encoding/hex — encode/decode hexadecimal pairs. + +export fn encode(dst: []u8, src: []u8) i32 = { + let i: i32 = 0; + let j: i32 = 0; + for (i < src.len) { + let b: u8 = src[i]; + let hi: u8 = (b: i32 >> 4): u8 & ('\x0f': u8); + let lo: u8 = b & ('\x0f': u8); + if (hi < 10) { dst[j] = hi + ('0': u8); } + else { dst[j] = hi - 10 + ('a': u8); }; + j += 1; + if (lo < 10) { dst[j] = lo + ('0': u8); } + else { dst[j] = lo - 10 + ('a': u8); }; + j += 1; + i += 1; + }; + return j; +}; diff --git a/lib/encoding/utf8/utf8.ww b/lib/encoding/utf8/utf8.ww new file mode 100644 index 00000000..deb7eeb5 --- /dev/null +++ b/lib/encoding/utf8/utf8.ww @@ -0,0 +1,14 @@ +// encoding/utf8 — UTF-8 helpers. RFC 3629; we only handle the legal +// subset (no over-long encodings, no surrogates). + +def MAX: rune = 1114111; // 0x10FFFF +def BAD: rune = -1; + +export fn runelen(r: rune) i32 = { + if (r < 0) { return -1; }; + if (r < 128) { return 1; }; + if (r < 2048) { return 2; }; + if (r < 65536) { return 3; }; + if (r <= MAX) { return 4; }; + return -1; +}; diff --git a/lib/errors/errors.ww b/lib/errors/errors.ww new file mode 100644 index 00000000..d37273b8 --- /dev/null +++ b/lib/errors/errors.ww @@ -0,0 +1,30 @@ +// errors — error type (a string) and a few sentinels. Plan 9 model: +// the empty string means OK, a non-empty string is the message. + +type error = str; + +def eEOF: error = "eof"; +def eShortRead: error = "short read"; +def eShortWrite: error = "short write"; +def eClosed: error = "closed"; +def eInvalid: error = "invalid argument"; +def ePerm: error = "permission denied"; +def eNotFound: error = "not found"; +def eExists: error = "already exists"; + +export fn isnil(e: error) bool = { + return e.len == 0; +}; + +// is — compare an error against a sentinel (or any other error). Pure +// byte equality; same shape as strings.equal but kept here so callers +// don't have to pull in strings just to compare. +export fn is(e: error, want: error) bool = { + if (e.len != want.len) { return false; }; + let i: i32 = 0; + for (i < e.len) { + if (e[i] != want[i]) { return false; }; + i += 1; + }; + return true; +}; diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww new file mode 100644 index 00000000..fd1df5f5 --- /dev/null +++ b/lib/fmt/fmt.ww @@ -0,0 +1,67 @@ +// fmt — minimal formatting writers. All output goes through os.write +// to fd 1 (stdout). No printf-family yet — we don't have varargs in +// the language proper — but the typed entry points cover the common +// cases. + +use os; +use strconv; + +export fn print(s: str) i64 = { + return os.write(1, s.ptr, s.len: u64); +}; + +export fn println(s: str) void = { + os.write(1, s.ptr, s.len: u64); + os.write(1, "\n".ptr, 1u64); +}; + +export fn printint(v: i64) void = { + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +export fn printlnint(v: i64) void = { + printint(v); + os.write(1, "\n".ptr, 1u64); +}; + +// errln — write a message to stderr with a trailing newline. +export fn errln(s: str) void = { + os.write(2, s.ptr, s.len: u64); + os.write(2, "\n".ptr, 1u64); +}; + +// fprint / fprintln — same as print/println but on an arbitrary fd. +// Used by the compiler to write to its -o output file. +export fn fprint(fd: i32, s: str) i64 = { + return os.write(fd, s.ptr, s.len: u64); +}; + +export fn fprintln(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); + os.write(fd, "\n".ptr, 1u64); +}; + +export fn fprintint(fd: i32, v: i64) void = { + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], v); + os.write(fd, buf.ptr, n: u64); +}; + +// errpos — write "::: \n" to fd 2. The shape +// every compiler diagnostic uses; centralised so the format stays +// consistent across phases. +export fn errpos(file: str, line: i32, col: i32, msg: str) void = { + os.write(2, file.ptr, file.len: u64); + os.write(2, ":".ptr, 1u64); + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], line: i64); + os.write(2, buf.ptr, n: u64); + os.write(2, ":".ptr, 1u64); + n = strconv.i64toa(buf[0:32], col: i64); + os.write(2, buf.ptr, n: u64); + os.write(2, ": ".ptr, 2u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); +}; diff --git a/lib/hash/fnv/fnv.ww b/lib/hash/fnv/fnv.ww new file mode 100644 index 00000000..6067f0c2 --- /dev/null +++ b/lib/hash/fnv/fnv.ww @@ -0,0 +1,15 @@ +// hash/fnv — FNV-1a 64-bit. Pure ww. No dependencies. + +def OFFSET: u64 = 14695981039346656037; +def PRIME: u64 = 1099511628211; + +export fn fnv1a(buf: []u8) u64 = { + let h: u64 = OFFSET; + let i: i32 = 0; + for (i < buf.len) { + h = h ^ (buf[i]: u64); + h = h * PRIME; + i += 1; + }; + return h; +}; diff --git a/lib/io/stream.ww b/lib/io/stream.ww new file mode 100644 index 00000000..9b6d7092 --- /dev/null +++ b/lib/io/stream.ww @@ -0,0 +1,28 @@ +// io — stream interface (Plan 9 Bio / Hare io::stream shape). +// +// No closures, no methods. A `stream` is a struct of function +// pointers plus a `ctx: *void`. The error channel is the return +// value of read/write/close. Negative i32 = errno-style code, +// non-negative = bytes transferred. + +type stream = struct { + ctx: *void, + read: fn(s: *stream, buf: []u8) i32, + write: fn(s: *stream, buf: []u8) i32, + close: fn(s: *stream) i32, +}; + +def eof: i32 = -1; +def closed: i32 = -2; + +export fn stream_read(s: *stream, buf: []u8) i32 = { + return s.read(s, buf); +}; + +export fn stream_write(s: *stream, buf: []u8) i32 = { + return s.write(s, buf); +}; + +export fn stream_close(s: *stream) i32 = { + return s.close(s); +}; diff --git a/lib/net/net.ww b/lib/net/net.ww new file mode 100644 index 00000000..8db6c7e6 --- /dev/null +++ b/lib/net/net.ww @@ -0,0 +1,48 @@ +// net — minimal TCP. Sketched against the Linux syscall numbers +// 41 (socket), 42 (connect), 43 (accept), 49 (bind), 50 (listen). +// Real applications will want addrinfo + DNS; we leave that to +// higher layers. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; + +def AF_INET: i32 = 2; +def SOCK_STREAM:i32 = 1; +def IPPROTO_TCP:i32 = 6; + +def SYS_SOCKET: i64 = 41; +def SYS_CONNECT: i64 = 42; +def SYS_ACCEPT: i64 = 43; +def SYS_BIND: i64 = 49; +def SYS_LISTEN: i64 = 50; + +// sockaddr_in is laid out by the kernel: family u16, port u16 (BE), +// addr u32 (BE), padding 8B = 16B total. Caller fills it. +type sockaddr_in = struct { + family: u16, + port: u16, + addr: u32, + pad0: u64, +}; + +export fn socket() i32 = { + return syscall3(SYS_SOCKET, AF_INET: i64, SOCK_STREAM: i64, + IPPROTO_TCP: i64): i32; +}; + +export fn connect(fd: i32, sa: *sockaddr_in) i32 = { + return syscall3(SYS_CONNECT, fd: i64, sa: i64, 16): i32; +}; + +export fn bind(fd: i32, sa: *sockaddr_in) i32 = { + return syscall3(SYS_BIND, fd: i64, sa: i64, 16): i32; +}; + +export fn listen(fd: i32, backlog: i32) i32 = { + return syscall3(SYS_LISTEN, fd: i64, backlog: i64, 0): i32; +}; + +// htons-equivalent: byte-swap a 16-bit port into network order. +export fn htons(p: u16) u16 = { + return ((p << 8) | (p >> 8)) & 0xffff; +}; diff --git a/lib/os/os.ww b/lib/os/os.ww new file mode 100644 index 00000000..71dc5150 --- /dev/null +++ b/lib/os/os.ww @@ -0,0 +1,171 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_ACCESS: i64 = 21; +def SYS_GETPID: i64 = 39; +def SYS_FORK: i64 = 57; +def SYS_EXECVE: i64 = 59; +def SYS_EXIT: i64 = 60; +def SYS_WAIT4: i64 = 61; +def SYS_UNLINK: i64 = 87; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// ---- process and filesystem helpers used by the `ww` driver ---------- + +// access(2): returns 0 if the file is reachable, negative errno +// otherwise. mode is the bitset described in (F_OK=0). +export fn access(path: *u8, mode: i32) i32 = { + return syscall2(SYS_ACCESS, path: i64, mode: i64): i32; +}; + +// unlink(2). +export fn unlink(path: *u8) i32 = { + return syscall1(SYS_UNLINK, path: i64): i32; +}; + +// getpid(2). Used by the driver to mint unique scratch paths. +export fn getpid() i32 = { + return syscall0(SYS_GETPID): i32; +}; + +// fork(2): 0 in the child, child pid in the parent, negative errno +// on failure. +export fn fork() i32 = { + return syscall0(SYS_FORK): i32; +}; + +// execve(2): on success, does not return. +export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { + return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32; +}; + +// wait4(2): wait for `pid` (or any child if -1), store status in +// `*status_out`, return the pid that ended (or negative errno). +export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = { + return syscall4(SYS_WAIT4, pid: i64, status_out: i64, + options: i64, rusage: i64): i32; +}; diff --git a/lib/path/path.ww b/lib/path/path.ww new file mode 100644 index 00000000..c9f02a98 --- /dev/null +++ b/lib/path/path.ww @@ -0,0 +1,15 @@ +// path — filesystem path manipulation. UTF-8 paths, '/' separator. + +export fn isabs(p: str) bool = { + if (p.len == 0) { return false; }; + return p[0] == ('/': u8); +}; + +export fn lastindex(p: str, c: u8) i32 = { + let i: i32 = p.len - 1; + for (i >= 0) { + if (p[i] == c) { return i; }; + i -= 1; + }; + return -1; +}; diff --git a/lib/slices/slices.ww b/lib/slices/slices.ww new file mode 100644 index 00000000..2a8d763c --- /dev/null +++ b/lib/slices/slices.ww @@ -0,0 +1,49 @@ +// slices — generic slice helpers, written without generics. +// +// CLAUDE.md forbids generics, so we mint per-element-type variants. +// Hare's `append` builtin is what these stand in for: each takes a +// *[]T plus an item, grows the storage if needed, and updates the +// slice header in place. The user passes `&s` because we mutate +// through the pointer. + +use os; + +export fn appendu8(s: *[]u8, v: u8) void = { + if (s.len >= s.cap) { + let nc: i32 = s.cap * 2; + if (nc < 8) { nc = 8; }; + let np: *u8 = os.alloc(nc: u64): *u8; + let i: i32 = 0; + for (i < s.len) { + np[i] = s.ptr[i]; + i += 1; + }; + if (s.cap > 0) { + os.free(s.ptr: *void, s.cap: u64); + }; + s.ptr = np; + s.cap = nc; + }; + s.ptr[s.len] = v; + s.len += 1; +}; + +export fn appendi64(s: *[]i64, v: i64) void = { + if (s.len >= s.cap) { + let nc: i32 = s.cap * 2; + if (nc < 8) { nc = 8; }; + let np: *i64 = os.alloc((nc * 8): u64): *i64; + let i: i32 = 0; + for (i < s.len) { + np[i] = s.ptr[i]; + i += 1; + }; + if (s.cap > 0) { + os.free(s.ptr: *void, (s.cap * 8): u64); + }; + s.ptr = np; + s.cap = nc; + }; + s.ptr[s.len] = v; + s.len += 1; +}; diff --git a/lib/sort/sort.ww b/lib/sort/sort.ww new file mode 100644 index 00000000..94cdd042 --- /dev/null +++ b/lib/sort/sort.ww @@ -0,0 +1,27 @@ +// sort — sorting helpers. The data is reached through a vtable so the +// algorithm stays generic without language-level generics. + +type slice = struct { + ctx: *void, + len: i32, + less: fn(s: *slice, i: i32, j: i32) bool, + swap: fn(s: *slice, i: i32, j: i32) void, +}; + +// Insertion sort, fine for small inputs and stable. We'll grow into +// quicksort later when we have heavier tests. +export fn sort(s: *slice) void = { + let i: i32 = 1; + for (i < s.len) { + let j: i32 = i; + for (j > 0) { + if (s.less(s, j, j - 1)) { + s.swap(s, j, j - 1); + j -= 1; + } else { + j = 0; + }; + }; + i += 1; + }; +}; diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww new file mode 100644 index 00000000..44a9c966 --- /dev/null +++ b/lib/strconv/strconv.ww @@ -0,0 +1,119 @@ +// strconv — number↔string conversions. Decimal i64 to/from a fixed +// buffer. Two error idioms ship side by side: +// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the +// tagged-union work; kept for callers that already use it. +// - Hare style (parse64/parseu64): `(value | str)`. The error +// variant carries a short, allocation-free message describing +// why the parse failed. Prefer this for new code. + +// u64toa — write `v` in decimal into `buf` and return the byte count. +// Unsigned-only so callers don't have to think about wraparound when +// printing a u64 that happens to have the high bit set. +export fn u64toa(buf: []u8, v: u64) i32 = { + let tmp: [32]u8; + let i: i32 = 0; + let n: u64 = v; + for (n > 0u64) { + tmp[i] = ((n % 10u64) + 48u64): u8; + n = n / 10u64; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn i64toa(buf: []u8, v: i64) i32 = { + let neg: bool = false; + let n: i64 = v; + if (n < 0) { + neg = true; + n = -n; + }; + let tmp: [32]u8; + let i: i32 = 0; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + if (neg) { + buf[out] = 45u8; // '-' + out += 1; + }; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn atoi64(s: str) (i64, bool) = { + let v: i64 = 0; + let i: i32 = 0; + let neg: bool = false; + if (s.len > 0) { + if (s[0] == 45u8) { neg = true; i = 1; }; + }; + if (i >= s.len) { return 0, false; }; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return 0, false; }; + if (c > 57u8) { return 0, false; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v, true; +}; + +// parse64 — Hare-style fallible signed decimal parser. The value +// variant is i64; the error variant is a short str describing the +// reason. No locale, no whitespace, no underscores: a leading '-' is +// the only non-digit accepted, and only at position 0. +export fn parse64(s: str) (i64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let i: i32 = 0; + let neg: bool = false; + if (s[0] == 45u8) { neg = true; i = 1; }; + if (i >= s.len) { return "parse: lone sign"; }; + let v: i64 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v; +}; + +// parseu64 — fallible unsigned decimal parser. No leading sign. +export fn parseu64(s: str) (u64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let v: u64 = 0u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10u64 + ((c: u64) - 48u64); + i += 1; + }; + return v; +}; diff --git a/lib/strings/strings.ww b/lib/strings/strings.ww new file mode 100644 index 00000000..55ed9d70 --- /dev/null +++ b/lib/strings/strings.ww @@ -0,0 +1,95 @@ +// strings — operations over the immutable str type ({ *u8, len }). + +use os; + +export fn len(s: str) i32 = { + return s.len; +}; + +export fn isempty(s: str) bool = { + return s.len == 0; +}; + +export fn equal(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; +}; + +export fn hasprefix(s: str, p: str) bool = { + if (p.len > s.len) { return false; }; + let i: i32 = 0; + for (i < p.len) { + if (s[i] != p[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn hassuffix(s: str, suf: str) bool = { + if (suf.len > s.len) { return false; }; + let off: i32 = s.len - suf.len; + let i: i32 = 0; + for (i < suf.len) { + if (s[off + i] != suf[i]) { return false; }; + i += 1; + }; + return true; +}; + +// indexbyte — first index of `c` in `s`, or -1 if absent. Plan 9- +// style sentinel return; callers that prefer a fallible shape can +// wrap this in their own (i32 | str). No allocation. +export fn indexbyte(s: str, c: u8) i32 = { + let i: i32 = 0; + for (i < s.len) { + if (s[i] == c) { return i; }; + i += 1; + }; + return -1; +}; + +// index — first index of `sub` in `s`, or -1. Naive scan; fine for +// short patterns and small strings, which dominate config and CLI +// parsing. Empty `sub` matches at 0. +export fn index(s: str, sub: str) i32 = { + if (sub.len == 0) { return 0; }; + if (sub.len > s.len) { return -1; }; + let last: i32 = s.len - sub.len; + let i: i32 = 0; + for (i <= last) { + let j: i32 = 0; + let ok: bool = true; + for (j < sub.len) { + if (s[i + j] != sub[j]) { ok = false; j = sub.len; } + else { j += 1; }; + }; + if (ok) { return i; }; + i += 1; + }; + return -1; +}; + +export fn contains(s: str, sub: str) bool = { + return index(s, sub) >= 0; +}; + +// concat — joins two strings into a fresh str. Caller owns the +// returned str's storage; release via `os.free(r.ptr, r.len)`. Mirrors +// Hare's strings::concat shape. +export fn concat(a: str, b: str) str = { + let total: i32 = a.len + b.len; + let buf: *u8 = os.alloc(total: u64): *u8; + let i: i32 = 0; + for (i < a.len) { buf[i] = a[i]; i += 1; }; + let j: i32 = 0; + for (j < b.len) { buf[a.len + j] = b[j]; j += 1; }; + let r: str; + r.ptr = buf; + r.len = total; + return r; +}; diff --git a/lib/time/time.ww b/lib/time/time.ww new file mode 100644 index 00000000..c848f546 --- /dev/null +++ b/lib/time/time.ww @@ -0,0 +1,15 @@ +// time — clocks. We expose monotonic-ns via a syscall trampoline. +// Linux clock_gettime is syscall 228; clock id 1 is CLOCK_MONOTONIC. +// Until we can pass struct-by-value the user supplies a buffer. + +@symbol("rt_syscall") fn syscall(num: i64, a: i64, b: i64, c: i64) i64; + +def CLOCK_MONOTONIC: i64 = 1; +def SYS_CLOCK_GETTIME: i64 = 228; + +// timespec is {sec, nsec} — 16 bytes. Caller passes a pointer. +type timespec = struct { sec: i64, nsec: i64 }; + +export fn monotonic(ts: *timespec) i32 = { + return syscall(SYS_CLOCK_GETTIME, CLOCK_MONOTONIC, ts: i64, 0): i32; +}; diff --git a/lib/types/types.ww b/lib/types/types.ww new file mode 100644 index 00000000..2d2c6c0d --- /dev/null +++ b/lib/types/types.ww @@ -0,0 +1,48 @@ +// types — integer limits and helpers, the seed module that the +// rest of the stdlib depends on. Plan 9-flavoured: the names are +// short and the constants are platform-fixed (we are amd64 only). + +def I8_MAX: i8 = 127; +def I16_MAX: i16 = 32767; +def I32_MAX: i32 = 2147483647; +def I64_MAX: i64 = 9223372036854775807; + +def I8_MIN: i8 = -128; +def I16_MIN: i16 = -32768; +def I32_MIN: i32 = -2147483648; +def I64_MIN: i64 = -9223372036854775808; + +def U8_MAX: u8 = 255; +def U16_MAX: u16 = 65535; +def U32_MAX: u32 = 4294967295; +def U64_MAX: u64 = 18446744073709551615; + +export fn min_i32(a: i32, b: i32) i32 = { + if (a < b) { return a; }; + return b; +}; + +export fn max_i32(a: i32, b: i32) i32 = { + if (a > b) { return a; }; + return b; +}; + +export fn min_i64(a: i64, b: i64) i64 = { + if (a < b) { return a; }; + return b; +}; + +export fn max_i64(a: i64, b: i64) i64 = { + if (a > b) { return a; }; + return b; +}; + +export fn abs_i32(x: i32) i32 = { + if (x < 0) { return -x; }; + return x; +}; + +export fn abs_i64(x: i64) i64 = { + if (x < 0) { return -x; }; + return x; +}; diff --git a/rt/abort.s b/rt/abort.s new file mode 100644 index 00000000..f5650b60 --- /dev/null +++ b/rt/abort.s @@ -0,0 +1,17 @@ +// rt/abort.s — write a string to stderr and exit(1). +// +// Args (SysV str-by-value): DI = msg.ptr, SI = msg.len. +// We never return. + +TEXT rt_abort,$0 + // reorder args for the write syscall: rdi=fd, rsi=ptr, rdx=len. + MOVQ SI, DX // DX = len + MOVQ DI, SI // SI = ptr + MOVQ $2, DI // DI = stderr + MOVQ $1, AX // AX = sys_write + SYSCALL + // exit(1) + MOVQ $1, DI + MOVQ $60, AX // sys_exit + SYSCALL + RET diff --git a/rt/alloc.s b/rt/alloc.s new file mode 100644 index 00000000..a1c3de1c --- /dev/null +++ b/rt/alloc.s @@ -0,0 +1,24 @@ +// rt/alloc.s — page allocator via the mmap syscall. +// +// rt_alloc(n: u64) returns a *void aligned at a page boundary, sized +// to the next page multiple. Pair with rt_free(p, n). +// +// We pin to PROT_READ|PROT_WRITE and MAP_PRIVATE|MAP_ANONYMOUS so +// callers never have to plumb file descriptors through. + +TEXT rt_alloc,$0 + MOVQ DI, SI // arg 1: length = caller's n + MOVQ $0, DI // arg 0: addr = NULL (kernel chooses) + MOVQ $3, DX // arg 2: prot = R|W + MOVQ $34, R10 // arg 3: flags = MAP_PRIVATE|MAP_ANON + MOVQ $-1, R8 // arg 4: fd = -1 + MOVQ $0, R9 // arg 5: offset = 0 + MOVQ $9, AX // syscall: mmap + SYSCALL + RET + +TEXT rt_free,$0 + // DI already holds ptr, SI already holds length + MOVQ $11, AX // syscall: munmap + SYSCALL + RET diff --git a/rt/start.s b/rt/start.s new file mode 100644 index 00000000..7604cce9 --- /dev/null +++ b/rt/start.s @@ -0,0 +1,16 @@ +// rt/start.s — process entry. Linux sets up the stack so that on +// entry [SP] holds argc, [SP+8] starts argv (NULL-terminated array +// of *u8), [SP+8+(argc+1)*8] starts envp. +// +// We pull argc into DI and the argv pointer into SI, then jump to +// `main`. ww programs that declare `fn main(argc: i32, argv: **u8) +// i32` see them; programs declaring `fn main() i32` simply ignore +// the regs. Either way, main's return value is fed to the exit +// syscall. +TEXT _start,$0 + MOVQ (SP), DI + LEAQ 8(SP), SI + CALL main(SB) + MOVQ AX, DI + MOVQ $60, AX + SYSCALL diff --git a/rt/streq.s b/rt/streq.s new file mode 100644 index 00000000..4889879d --- /dev/null +++ b/rt/streq.s @@ -0,0 +1,27 @@ +// rt/streq.s — string equality at the runtime layer. +// +// Called from cgen for `a == b` / `a != b` when both operands are str. +// SysV ABI: a.ptr in DI, a.len in SI, b.ptr in DX, b.len in CX. +// Returns 1 in AX if equal, 0 otherwise. Caller flips for !=. + +TEXT rt_streq,$0 + CMPQ CX, SI // lengths must match + JNE rt_streq_no + // SI now holds the (shared) length; iterate while SI > 0 +rt_streq_loop: + CMPQ $0, SI + JE rt_streq_yes + MOVZBQ (DI), AX + MOVZBQ (DX), R8 + CMPQ R8, AX + JNE rt_streq_no + ADDQ $1, DI + ADDQ $1, DX + SUBQ $1, SI + JMP rt_streq_loop +rt_streq_yes: + MOVQ $1, AX + RET +rt_streq_no: + MOVQ $0, AX + RET diff --git a/rt/syscall.s b/rt/syscall.s new file mode 100644 index 00000000..0d739b9e --- /dev/null +++ b/rt/syscall.s @@ -0,0 +1,16 @@ +// rt/syscall.s — Linux amd64 syscall trampoline. Args go in +// DI, SI, DX, R10, R8, R9 (the kernel ABI; userspace 4th is CX). We +// keep things minimal: rt_syscall(num, a1, a2, a3, a4, a5) → result. +// +// The first ww arg lands in DI (caller side); to use it as the +// syscall number we move it into AX. The 4th C-ABI arg comes in CX +// and must be moved to R10 for the kernel. +TEXT rt_syscall,$0 + MOVQ DI, AX + MOVQ SI, DI + MOVQ DX, SI + MOVQ CX, DX + MOVQ R8, R10 + MOVQ R9, R8 + SYSCALL + RET diff --git a/selfhost/cmd/6a/asm.ww b/selfhost/cmd/6a/asm.ww new file mode 100644 index 00000000..fba81cc9 --- /dev/null +++ b/selfhost/cmd/6a/asm.ww @@ -0,0 +1,681 @@ +// selfhost/cmd/6a/asm.ww — port of cmd/6a/asm.c. +// +// Encode the parsed aprog list into amd64 machine bytes, appending to +// asm_.text. Relocations for CALL/branch targets that resolve to +// externals are queued in asm_.relocs. +// +// Encoding subset matches what 6c emits — see cmd/6a/asm.c for the +// authoritative list. Helpers (rcode/rhi/modrm/emit_rex etc.) are +// fully ported; a_encode itself is still a stub pending the full +// switch over A_*. + +use os; +use mem; +use types; + +// ---- text buffer growth ------------------------------------------------ + +export fn a_emit_byte(a: *asm_, b: u8) void = { + if (a.textlen + 1u64 > a.textcap) { + let nc: u64 = a.textcap; + if (nc == 0u64) { nc = 4096u64; }; + nc = nc * 2u64; + let nb: *u8 = os.alloc(nc): *u8; + let i: u64 = 0u64; + for (i < a.textlen) { nb[i] = a.text[i]; i += 1u64; }; + a.text = nb; + a.textcap = nc; + }; + a.text[a.textlen] = b; + a.textlen += 1u64; +}; + +export fn a_emit_u32(a: *asm_, v: u32) void = { + a_emit_byte(a, (v & 255u32): u8); + a_emit_byte(a, ((v >> 8u32) & 255u32): u8); + a_emit_byte(a, ((v >> 16u32) & 255u32): u8); + a_emit_byte(a, ((v >> 24u32) & 255u32): u8); +}; + +export fn a_addreloc(a: *asm_, off: u64, kind: i32, s: *asym, add: i64) void = { + let r: *areloc = amalloc(a.a, 48u64): *areloc; + r.off = off; + r.kind = kind; + r.asy = s; + r.addend = add; + r.rnext = a.relocs; + a.relocs = r; +}; + +// ---- register codes ---------------------------------------------------- + +// Low 3 bits of register encoding. +fn rcode(r: i32) i32 = { + if (r == D_AX) { return 0; }; if (r == D_CX) { return 1; }; + if (r == D_DX) { return 2; }; if (r == D_BX) { return 3; }; + if (r == D_SP) { return 4; }; if (r == D_BP) { return 5; }; + if (r == D_SI) { return 6; }; if (r == D_DI) { return 7; }; + if (r == D_R8) { return 0; }; if (r == D_R9) { return 1; }; + if (r == D_R10) { return 2; }; if (r == D_R11) { return 3; }; + if (r == D_R12) { return 4; }; if (r == D_R13) { return 5; }; + if (r == D_R14) { return 6; }; if (r == D_R15) { return 7; }; + if (r == D_X0) { return 0; }; if (r == D_X1) { return 1; }; + if (r == D_X2) { return 2; }; if (r == D_X3) { return 3; }; + if (r == D_X4) { return 4; }; if (r == D_X5) { return 5; }; + if (r == D_X6) { return 6; }; if (r == D_X7) { return 7; }; + if (r == D_X8) { return 0; }; if (r == D_X9) { return 1; }; + if (r == D_X10) { return 2; }; if (r == D_X11) { return 3; }; + if (r == D_X12) { return 4; }; if (r == D_X13) { return 5; }; + if (r == D_X14) { return 6; }; if (r == D_X15) { return 7; }; + return 0; +}; + +// 1 if r needs the REX high bit (R8..R15 or X8..X15). +fn rhi(r: i32) i32 = { + if (r >= D_R8) { if (r <= D_R15) { return 1; }; }; + if (r >= D_X8) { if (r <= D_X15) { return 1; }; }; + return 0; +}; + +fn is_xmm(r: i32) bool = { + if (r >= D_X0) { if (r <= D_X15) { return true; }; }; + return false; +}; + +// ModR/M byte builder. +fn modrm_byte(mod: i32, reg: i32, rm: i32) u8 = { + return (((mod & 3) << 6) | ((reg & 7) << 3) | (rm & 7)): u8; +}; + +// REX prefix; W=1 for 64-bit operand size. +fn emit_rex(a: *asm_, regbit: i32, rmbit: i32, w: i32) void = { + let b: u8 = 64u8; // 0x40 + if (w != 0) { b = b | 8u8; }; + if (regbit != 0) { b = b | 4u8; }; + if (rmbit != 0) { b = b | 1u8; }; + if (b != 64u8) { a_emit_byte(a, b); } + else { if (w != 0) { a_emit_byte(a, b); }; }; +}; + +// ModR/M + (optional) SIB + displacement for [base+disp]. +// Special-cases SP (needs SIB) and BP (forces explicit disp). +fn emit_modrm_mem(a: *asm_, reg_field: i32, base: i32, disp: i64) void = { + let rm: i32 = rcode(base); + let needsib: bool = (rm == 4); + let forced_disp: bool = false; + if (rm == 5) { if (disp == 0i64) { forced_disp = true; }; }; + + let mod: i32 = 2; + if (disp == 0i64) { + if (!forced_disp) { mod = 0; } + else { mod = 1; }; + } else { + if (disp >= -128i64) { if (disp <= 127i64) { mod = 1; }; }; + }; + + a_emit_byte(a, modrm_byte(mod, reg_field, rm)); + if (needsib) { + a_emit_byte(a, 36u8); // 0x24: scale=0 idx=4(none) base=4 + }; + if (mod == 1) { + a_emit_byte(a, (disp: u64 & 255u64): u8); + } else { if (mod == 2) { + a_emit_u32(a, disp: u32); + };}; +}; + +// reg→reg "src, dst" generic encoding (89 /r, 01 /r, etc.). +fn encode_rr(a: *asm_, opcode: u8, src: i32, dst: i32) void = { + emit_rex(a, rhi(src), rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm_byte(3, rcode(src), rcode(dst))); +}; + +// reg→mem(base, disp) (e.g. MOVQ src reg into mem; opcode = 0x89). +fn encode_rm(a: *asm_, opcode: u8, src_reg: i32, base: i32, disp: i64) void = { + emit_rex(a, rhi(src_reg), rhi(base), 1); + a_emit_byte(a, opcode); + emit_modrm_mem(a, rcode(src_reg), base, disp); +}; + +// mem(base, disp) → reg (e.g. MOVQ mem into reg; opcode = 0x8B). +fn encode_mr(a: *asm_, opcode: u8, dst_reg: i32, base: i32, disp: i64) void = { + emit_rex(a, rhi(dst_reg), rhi(base), 1); + a_emit_byte(a, opcode); + emit_modrm_mem(a, rcode(dst_reg), base, disp); +}; + +// OPCODE /n imm32 reg form (e.g. ADDQ $imm, reg). +fn encode_ri_imm32(a: *asm_, opcode: u8, subop: i32, dst: i32, imm: i32) void = { + emit_rex(a, 0, rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm_byte(3, subop, rcode(dst))); + a_emit_u32(a, imm: u32); +}; + +// Unary on reg: F7 /n reg, etc. +fn encode_unary(a: *asm_, opcode: u8, subop: i32, dst: i32) void = { + emit_rex(a, 0, rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm_byte(3, subop, rcode(dst))); +}; + +// SSE2 helpers. Plan 9 syntax: source first, destination second. +// For ADDSD-style ops we put dst in the reg field, src in r/m. +fn sse_rr(a: *asm_, prefix: u8, op2: u8, reg_op: i32, rm_op: i32) void = { + if (prefix != 0u8) { a_emit_byte(a, prefix); }; + emit_rex(a, rhi(reg_op), rhi(rm_op), 0); + a_emit_byte(a, 15u8); // 0x0F + a_emit_byte(a, op2); + a_emit_byte(a, modrm_byte(3, rcode(reg_op), rcode(rm_op))); +}; + +fn sse_mr_load(a: *asm_, prefix: u8, op2: u8, reg_op: i32, base: i32, disp: i64) void = { + if (prefix != 0u8) { a_emit_byte(a, prefix); }; + emit_rex(a, rhi(reg_op), rhi(base), 0); + a_emit_byte(a, 15u8); + a_emit_byte(a, op2); + emit_modrm_mem(a, rcode(reg_op), base, disp); +}; + +// REX.W variant of sse_rr (CVTTSD2SI / CVTSI2SD). +fn sse_rr_w(a: *asm_, prefix: u8, op2: u8, reg_op: i32, rm_op: i32) void = { + if (prefix != 0u8) { a_emit_byte(a, prefix); }; + emit_rex(a, rhi(reg_op), rhi(rm_op), 1); + a_emit_byte(a, 15u8); + a_emit_byte(a, op2); + a_emit_byte(a, modrm_byte(3, rcode(reg_op), rcode(rm_op))); +}; + +// ---- label resolution / fixups ---------------------------------------- + +fn streq(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 resolve_label(a: *asm_, name: str) u64 = { + let s: *asym = a.syms; + for (s != nil) { + if (s.defined != 0) { if (streq(s.name, name)) { return s.addr; }; }; + s = s.snext; + }; + return 0u64; +}; + +fn label_defined(a: *asm_, name: str) bool = { + let s: *asym = a.syms; + for (s != nil) { + if (s.defined != 0) { if (streq(s.name, name)) { return true; }; }; + s = s.snext; + }; + return false; +}; + +// ---- fixup helper ----------------------------------------------------- + +fn add_fixup(a: *asm_, off: u64, label: str) void = { + let f: *afixup = amalloc(a.a, 48u64): *afixup; + f.off = off; + f.label = label; + f.fnext = a.fixups; + a.fixups = f; +}; + +fn is_gpr(t: i32) bool = { + if (t >= D_AX) { if (t <= D_R15) { return true; }; }; + return false; +}; + +// `a_intern` lives in parse.ww — flat-scope concat lets us call it +// directly without an @symbol declaration here. + +// ---- a_encode --------------------------------------------------------- + +export fn a_encode(a: *asm_) i32 = { + let p: *aprog = a.head; + for (p != nil) { + // Define any pending label at the current PC. + if (p.label.len > 0) { + let s: *asym = a_intern(a, p.label); + s.defined = 1; + s.is_text = 1; + s.addr = a.textlen; + }; + let op: i32 = p.as_; + + if (op == A_NOP) { + p = p.link; continue; + }; + if (op == A_TEXT) { + let s: *asym = a_intern(a, p.to.asym); + s.defined = 1; + s.is_text = 1; + s.is_global = 1; + s.addr = a.textlen; + p = p.link; continue; + }; + if (op == A_DATA) { + let s: *asym = a_intern(a, p.to.asym); + s.defined = 1; + s.is_text = 1; + s.is_global = 1; + s.addr = a.textlen; + let i: u64 = 0u64; + for (i < p.nbytes) { a_emit_byte(a, p.bytes[i]); i += 1u64; }; + p = p.link; continue; + }; + if (op == A_RET) { + a_emit_byte(a, 195u8); // 0xC3 + p = p.link; continue; + }; + if (op == A_SYSCALL) { + a_emit_byte(a, 15u8); + a_emit_byte(a, 5u8); + p = p.link; continue; + }; + if (op == A_PUSHQ) { + if (rhi(p.to.atype) != 0) { a_emit_byte(a, 65u8); }; // 0x41 + a_emit_byte(a, (80 + rcode(p.to.atype)): u8); // 0x50 + p = p.link; continue; + }; + if (op == A_POPQ) { + if (rhi(p.to.atype) != 0) { a_emit_byte(a, 65u8); }; + a_emit_byte(a, (88 + rcode(p.to.atype)): u8); // 0x58 + p = p.link; continue; + }; + if (op == A_NEGQ) { encode_unary(a, 247u8, 3, p.to.atype); p = p.link; continue; }; + if (op == A_NOTQ) { encode_unary(a, 247u8, 2, p.to.atype); p = p.link; continue; }; + if (op == A_IDIVQ) { encode_unary(a, 247u8, 7, p.to.atype); p = p.link; continue; }; + if (op == A_DIVQ) { encode_unary(a, 247u8, 6, p.to.atype); p = p.link; continue; }; + + if (op == A_MOVQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + let v: i64 = p.from.offset; + if (v >= -2147483648i64) { if (v <= 2147483647i64) { + encode_ri_imm32(a, 199u8, 0, tt, v: i32); + p = p.link; continue; + };}; + // movabs r64, imm64: REX.W B8+rd imm64 + emit_rex(a, 0, rhi(tt), 1); + a_emit_byte(a, (184 + rcode(tt)): u8); + let k: i32 = 0; + for (k < 8) { + a_emit_byte(a, ((v: u64 >> (k: u64 * 8u64)) & 255u64): u8); + k += 1; + }; + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (is_gpr(tt)) { + encode_rr(a, 137u8, ft, tt); // 0x89 + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 139u8, tt, p.from.reg, p.from.offset); // 0x8B + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_INDIR) { + encode_rm(a, 137u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_CONST) { if (tt == D_INDIR) { + emit_rex(a, 0, rhi(p.to.reg), 1); + a_emit_byte(a, 199u8); + emit_modrm_mem(a, 0, p.to.reg, p.to.offset); + a_emit_u32(a, p.from.offset: u32); + p = p.link; continue; + };}; + if (ft == D_EXTERN) { if (is_gpr(tt)) { + // RIP-relative load: 48 8B /r mod=00 rm=5 disp32 + emit_rex(a, rhi(tt), 0, 1); + a_emit_byte(a, 139u8); + a_emit_byte(a, modrm_byte(0, rcode(tt), 5)); + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.from.asym); + a_addreloc(a, reloff, 2, s, -4i64); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_EXTERN) { + // RIP-relative store: 48 89 /r mod=00 rm=5 disp32 + emit_rex(a, rhi(ft), 0, 1); + a_emit_byte(a, 137u8); + a_emit_byte(a, modrm_byte(0, rcode(ft), 5)); + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.to.asym); + a_addreloc(a, reloff, 2, s, -4i64); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVQ shape\n".ptr, 27u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVB) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_gpr(ft)) { if (tt == D_INDIR) { + emit_rex(a, rhi(ft), rhi(p.to.reg), 0); + a_emit_byte(a, 136u8); // 0x88 + emit_modrm_mem(a, rcode(ft), p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 0); + a_emit_byte(a, 138u8); // 0x8A + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVB shape\n".ptr, 27u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVZBQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 1); + a_emit_byte(a, 15u8); + a_emit_byte(a, 182u8); // 0xB6 + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVZBQ shape\n".ptr, 29u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVL) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_gpr(ft)) { if (tt == D_INDIR) { + emit_rex(a, rhi(ft), rhi(p.to.reg), 0); + a_emit_byte(a, 137u8); + emit_modrm_mem(a, rcode(ft), p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 0); + a_emit_byte(a, 139u8); + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (is_gpr(tt)) { + emit_rex(a, rhi(ft), rhi(tt), 0); + a_emit_byte(a, 137u8); + a_emit_byte(a, modrm_byte(3, rcode(ft), rcode(tt))); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVL shape\n".ptr, 27u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVSXD) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 1); + a_emit_byte(a, 99u8); // 0x63 + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVSXD shape\n".ptr, 29u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVSD) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_xmm(ft)) { if (is_xmm(tt)) { + sse_rr(a, 242u8, 16u8, tt, ft); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_xmm(tt)) { + sse_mr_load(a, 242u8, 16u8, tt, p.from.reg, p.from.offset); + p = p.link; continue; + };}; + if (is_xmm(ft)) { if (tt == D_INDIR) { + sse_mr_load(a, 242u8, 17u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVSD shape\n".ptr, 28u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_ADDSD) { sse_rr(a, 242u8, 88u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_SUBSD) { sse_rr(a, 242u8, 92u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_MULSD) { sse_rr(a, 242u8, 89u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_DIVSD) { sse_rr(a, 242u8, 94u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_UCOMISD) { sse_rr(a, 102u8, 46u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTTSD2SI) { sse_rr_w(a, 242u8, 44u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSI2SD) { sse_rr_w(a, 242u8, 42u8, p.to.atype, p.from.atype); p = p.link; continue; }; + + if (op == A_MOVSS) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_xmm(ft)) { if (is_xmm(tt)) { + sse_rr(a, 243u8, 16u8, tt, ft); p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_xmm(tt)) { + sse_mr_load(a, 243u8, 16u8, tt, p.from.reg, p.from.offset); + p = p.link; continue; + };}; + if (is_xmm(ft)) { if (tt == D_INDIR) { + sse_mr_load(a, 243u8, 17u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVSS shape\n".ptr, 28u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_ADDSS) { sse_rr(a, 243u8, 88u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_SUBSS) { sse_rr(a, 243u8, 92u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_MULSS) { sse_rr(a, 243u8, 89u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_DIVSS) { sse_rr(a, 243u8, 94u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_UCOMISS) { sse_rr(a, 0u8, 46u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTTSS2SI) { sse_rr_w(a, 243u8, 44u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSI2SS) { sse_rr_w(a, 243u8, 42u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSD2SS) { sse_rr(a, 242u8, 90u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSS2SD) { sse_rr(a, 243u8, 90u8, p.to.atype, p.from.atype); p = p.link; continue; }; + + if (op == A_ADDQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 0, tt, p.from.offset: i32); // 0x81 + p = p.link; continue; + };}; + if (ft == D_CONST) { if (tt == D_INDIR) { + emit_rex(a, 0, rhi(p.to.reg), 1); + a_emit_byte(a, 129u8); + emit_modrm_mem(a, 0, p.to.reg, p.to.offset); + a_emit_u32(a, p.from.offset: u32); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_INDIR) { + encode_rm(a, 1u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 3u8, tt, p.from.reg, p.from.offset); + p = p.link; continue; + };}; + encode_rr(a, 1u8, ft, tt); + p = p.link; continue; + }; + + if (op == A_SUBQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 5, tt, p.from.offset: i32); + p = p.link; continue; + };}; + if (ft == D_CONST) { if (tt == D_INDIR) { + emit_rex(a, 0, rhi(p.to.reg), 1); + a_emit_byte(a, 129u8); + emit_modrm_mem(a, 5, p.to.reg, p.to.offset); + a_emit_u32(a, p.from.offset: u32); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_INDIR) { + encode_rm(a, 41u8, ft, p.to.reg, p.to.offset); // 0x29 + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 43u8, tt, p.from.reg, p.from.offset); // 0x2B + p = p.link; continue; + };}; + encode_rr(a, 41u8, ft, tt); + p = p.link; continue; + }; + + if (op == A_ANDQ) { encode_rr(a, 33u8, p.from.atype, p.to.atype); p = p.link; continue; }; // 0x21 + if (op == A_ORQ) { encode_rr(a, 9u8, p.from.atype, p.to.atype); p = p.link; continue; }; // 0x09 + if (op == A_XORQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 6, tt, p.from.offset: i32); + p = p.link; continue; + };}; + encode_rr(a, 49u8, ft, tt); // 0x31 + p = p.link; continue; + }; + if (op == A_IMULQ) { + emit_rex(a, rhi(p.to.atype), rhi(p.from.atype), 1); + a_emit_byte(a, 15u8); + a_emit_byte(a, 175u8); // 0xAF + a_emit_byte(a, modrm_byte(3, rcode(p.to.atype), rcode(p.from.atype))); + p = p.link; continue; + }; + if (op == A_SHLQ) { encode_unary(a, 211u8, 4, p.to.atype); p = p.link; continue; }; // 0xD3 + if (op == A_SHRQ) { encode_unary(a, 211u8, 5, p.to.atype); p = p.link; continue; }; + if (op == A_CMPQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 7, tt, p.from.offset: i32); + p = p.link; continue; + };}; + encode_rr(a, 57u8, ft, tt); // 0x39 + p = p.link; continue; + }; + + if (op == A_LEAQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 141u8, tt, p.from.reg, p.from.offset); // 0x8D + p = p.link; continue; + };}; + if (ft == D_EXTERN) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), 0, 1); + a_emit_byte(a, 141u8); + a_emit_byte(a, modrm_byte(0, rcode(tt), 5)); + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.from.asym); + a_addreloc(a, reloff, 2, s, -4i64); + p = p.link; continue; + };}; + p = p.link; continue; + }; + + if (op == A_CALL) { + let tt: i32 = p.to.atype; + if (tt == D_EXTERN) { + a_emit_byte(a, 232u8); // 0xE8 + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.to.asym); + a_addreloc(a, reloff, 4, s, -4i64); + p = p.link; continue; + }; + if (tt == D_BRANCH) { + a_emit_byte(a, 232u8); + add_fixup(a, a.textlen, p.to.asym); + a_emit_u32(a, 0u32); + p = p.link; continue; + }; + if (is_gpr(tt)) { + if (rhi(tt) != 0) { a_emit_byte(a, 65u8); }; + a_emit_byte(a, 255u8); // 0xFF + a_emit_byte(a, modrm_byte(3, 2, rcode(tt))); + p = p.link; continue; + }; + p = p.link; continue; + }; + + if (op == A_JMP) { + a_emit_byte(a, 233u8); // 0xE9 + add_fixup(a, a.textlen, p.to.asym); + a_emit_u32(a, 0u32); + p = p.link; continue; + }; + + // Conditional jumps. 0x0F + cc + rel32. + let cc: u8 = 0u8; + let is_jcc: bool = true; + if (op == A_JE) { cc = 132u8; } // 0x84 + else { if (op == A_JZ) { cc = 132u8; } + else { if (op == A_JNE) { cc = 133u8; } + else { if (op == A_JNZ) { cc = 133u8; } + else { if (op == A_JL) { cc = 140u8; } + else { if (op == A_JLE) { cc = 142u8; } + else { if (op == A_JG) { cc = 143u8; } + else { if (op == A_JGE) { cc = 141u8; } + else { if (op == A_JB) { cc = 130u8; } + else { if (op == A_JBE) { cc = 134u8; } + else { if (op == A_JA) { cc = 135u8; } + else { if (op == A_JAE) { cc = 131u8; } + else { is_jcc = false; };};};};};};};};};};};}; + if (is_jcc) { + a_emit_byte(a, 15u8); + a_emit_byte(a, cc); + add_fixup(a, a.textlen, p.to.asym); + a_emit_u32(a, 0u32); + p = p.link; continue; + }; + + os.write(2, "6a: unsupported opcode\n".ptr, 23u64); + a.errs += 1; + p = p.link; + }; + + // Second pass: patch fixups (forward label refs). + let f: *afixup = a.fixups; + for (f != nil) { + if (!label_defined(a, f.label)) { + os.write(2, "6a: undefined label '".ptr, 21u64); + let lbl: str = f.label; + os.write(2, lbl.ptr, lbl.len: u64); + os.write(2, "'\n".ptr, 2u64); + a.errs += 1; + f = f.fnext; + continue; + }; + let target: u64 = resolve_label(a, f.label); + let rel: i64 = target: i64 - (f.off: i64 + 4i64); + let rel32: u32 = rel: u32; + a.text[f.off] = (rel32 & 255u32): u8; + a.text[f.off + 1u64] = ((rel32 >> 8u32) & 255u32): u8; + a.text[f.off + 2u64] = ((rel32 >> 16u32) & 255u32): u8; + a.text[f.off + 3u64] = ((rel32 >> 24u32) & 255u32): u8; + f = f.fnext; + }; + return a.errs; +}; diff --git a/selfhost/cmd/6a/lex.ww b/selfhost/cmd/6a/lex.ww new file mode 100644 index 00000000..793aa71e --- /dev/null +++ b/selfhost/cmd/6a/lex.ww @@ -0,0 +1,66 @@ +// selfhost/cmd/6a/lex.ww — port of cmd/6a/lex.c. +// +// Character-level helpers for 6a's line-oriented parser. The parser +// itself is in parse.ww; here we keep tokenisers for identifiers and +// numbers so parse.ww stays focused on syntax. + +export fn a_isidstart(c: i32) bool = { + if (c == 95) { return true; }; + if (c >= 65) { if (c <= 90) { return true; }; }; // A-Z + if (c >= 97) { if (c <= 122) { return true; }; }; // a-z + return false; +}; + +export fn a_isidcont(c: i32) bool = { + if (a_isidstart(c)) { return true; }; + if (c >= 48) { if (c <= 57) { return true; }; }; // 0-9 + if (c == 46) { return true; }; // . + return false; +}; + +// a_parsenum — read a leading [+-]?[0x|0X|0]?digits from p[0..n-1]. +// Returns (value, consumed). Stops at first non-digit. +// Plain Plan 9-style: $123 / $0x1f / $-7. Decimal default; 0x prefix +// for hex; 0 prefix for octal when followed by a digit (else just 0). +export fn a_parsenum(p: *u8, n: u64) (i64, u64) = { + let i: u64 = 0u64; + let neg: bool = false; + if (i < n) { + if (p[i] == 45u8) { neg = true; i += 1u64; } + else { if (p[i] == 43u8) { i += 1u64; }; }; + }; + let base: i64 = 10i64; + if (i + 1u64 < n) { + if (p[i] == 48u8) { + if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } + else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } + else { if (p[i + 1u64] >= 48u8) { if (p[i + 1u64] <= 55u8) { + base = 8i64; i += 1u64; + };};};}; + }; + }; + let v: i64 = 0i64; + let scan: bool = true; + for (scan) { + if (i >= n) { scan = false; } + else { + let c: u8 = p[i]; + let d: i64 = -1i64; + if (c >= 48u8) { if (c <= 57u8) { d = (c - 48u8): i64; }; }; + if (d < 0i64) { + if (base == 16i64) { + if (c >= 97u8) { if (c <= 102u8) { d = (c - 97u8): i64 + 10i64; }; }; + if (c >= 65u8) { if (c <= 70u8) { d = (c - 65u8): i64 + 10i64; }; }; + }; + }; + if (d < 0i64) { scan = false; } + else { if (d >= base) { scan = false; } + else { + v = v * base + d; + i += 1u64; + }; }; + }; + }; + if (neg) { v = -v; }; + return v, i; +}; diff --git a/selfhost/cmd/6a/main.combined.ww b/selfhost/cmd/6a/main.combined.ww new file mode 100644 index 00000000..9955400c --- /dev/null +++ b/selfhost/cmd/6a/main.combined.ww @@ -0,0 +1,2168 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_EXIT: i64 = 60; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c. +// +// Bump arena allocator. Backed by the runtime page allocator +// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the +// current chunk runs out we link a fresh one. Freeing the arena +// unmaps the chain. +// +// Memory handed out is 16-byte aligned. The C version under +// cmd/wwc/ is retained until the three-stage bootstrap diffs clean. + +use os; + +def ALIGN: u64 = 16u64; +def INIT_CHUNK: u64 = 65536u64; +def MAX_CHUNK: u64 = 4194304u64; +def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, + next: *arena, + total: u64, +}; + +fn roundup(n: u64, a: u64) u64 = { + return (n + a - 1u64) & ~(a - 1u64); +}; + +export fn newarena() *arena = { + let a: *arena = os.alloc(ARENA_SZ): *arena; + a.buf = os.alloc(INIT_CHUNK): *u8; + a.off = 0u64; + a.cap = INIT_CHUNK; + a.next = nil; + a.total = 0u64; + return a; +}; + +// Grow: link a fresh chunk in front of the head. We push the old +// chunk into `next` so the head always describes the current bump +// region. Chunk size doubles up to MAX_CHUNK. +fn grow(a: *arena, need: u64) bool = { + let want: u64 = a.cap * 2u64; + if (want < need) { want = need; }; + if (want > MAX_CHUNK) { want = MAX_CHUNK; }; + if (want < need) { return false; }; // single allocation too big + + let old: *arena = os.alloc(ARENA_SZ): *arena; + old.buf = a.buf; + old.off = a.off; + old.cap = a.cap; + old.next = a.next; + old.total = 0u64; + + a.buf = os.alloc(want): *u8; + a.off = 0u64; + a.cap = want; + a.next = old; + return true; +}; + +export fn amalloc(a: *arena, n: u64) *void = { + let need: u64 = roundup(n, ALIGN); + if (need > a.cap - a.off) { + if (!grow(a, need)) { return nil; }; + }; + let p: *u8 = a.buf + a.off; + a.off += need; + a.total += need; + // Zero the region. Plan 9 amalloc zeroes; we mirror that here so + // the checker can assume freshly allocated nodes start at 0. + let i: u64 = 0u64; + for (i < need) { + p[i] = 0u8; + i += 1u64; + }; + return p: *void; +}; + +// astrndup — copy `n` bytes into the arena and produce a NUL-terminated +// view. Returns a `str` whose ptr is arena-owned and whose len is `n` +// (the trailing NUL is past `len`, so callers reading exactly n bytes +// see no padding). Used by the lexer to capture token text. +export fn astrndup(a: *arena, src: *u8, n: u64) str = { + let p: *u8 = amalloc(a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { + p[i] = src[i]; + i += 1u64; + }; + p[n] = 0u8; + let r: str; + r.ptr = p; + r.len = n: i32; + return r; +}; + +export fn freearena(a: *arena) void = { + for (a != nil) { + let next: *arena = a.next; + os.free(a.buf: *void, a.cap); + os.free(a: *void, ARENA_SZ); + a = next; + }; +}; + +// selfhost/cmd/6a/types.ww — types + constants shared across the +// 6a port. Mirrors cmd/6a/a.h and cmd/6c/6.out.h. + +use mem; + +// ---- registers + operand kinds (from 6.out.h) ------------------------- +// These must stay numerically aligned with the C enum so that ww-cgen +// output (which reads them via `D_AX(SB)` etc.) lands on the same +// integers when read by ww-6a. +def D_NONE: i32 = 0; + +def D_AX: i32 = 1; +def D_CX: i32 = 2; +def D_DX: i32 = 3; +def D_BX: i32 = 4; +def D_SP: i32 = 5; +def D_BP: i32 = 6; +def D_SI: i32 = 7; +def D_DI: i32 = 8; +def D_R8: i32 = 9; +def D_R9: i32 = 10; +def D_R10: i32 = 11; +def D_R11: i32 = 12; +def D_R12: i32 = 13; +def D_R13: i32 = 14; +def D_R14: i32 = 15; +def D_R15: i32 = 16; + +def D_X0: i32 = 17; +def D_X1: i32 = 18; +def D_X2: i32 = 19; +def D_X3: i32 = 20; +def D_X4: i32 = 21; +def D_X5: i32 = 22; +def D_X6: i32 = 23; +def D_X7: i32 = 24; +def D_X8: i32 = 25; +def D_X9: i32 = 26; +def D_X10: i32 = 27; +def D_X11: i32 = 28; +def D_X12: i32 = 29; +def D_X13: i32 = 30; +def D_X14: i32 = 31; +def D_X15: i32 = 32; + +def D_PSP: i32 = 33; +def D_PFP: i32 = 34; +def D_PSB: i32 = 35; + +def D_CONST: i32 = 36; +def D_BRANCH: i32 = 37; +def D_EXTERN: i32 = 38; +def D_INDIR: i32 = 39; + +// ---- opcodes ---------------------------------------------------------- +def A_NOP: i32 = 0; +def A_TEXT: i32 = 1; +def A_DATA: i32 = 2; +def A_GLOBL: i32 = 3; +def A_END: i32 = 4; + +def A_MOVQ: i32 = 5; +def A_MOVL: i32 = 6; +def A_MOVB: i32 = 7; +def A_MOVZBQ: i32 = 8; +def A_MOVSXD: i32 = 9; + +def A_MOVSD: i32 = 10; +def A_ADDSD: i32 = 11; +def A_SUBSD: i32 = 12; +def A_MULSD: i32 = 13; +def A_DIVSD: i32 = 14; +def A_UCOMISD: i32 = 15; +def A_CVTTSD2SI: i32 = 16; +def A_CVTSI2SD: i32 = 17; + +def A_MOVSS: i32 = 18; +def A_ADDSS: i32 = 19; +def A_SUBSS: i32 = 20; +def A_MULSS: i32 = 21; +def A_DIVSS: i32 = 22; +def A_UCOMISS: i32 = 23; +def A_CVTTSS2SI: i32 = 24; +def A_CVTSI2SS: i32 = 25; +def A_CVTSD2SS: i32 = 26; +def A_CVTSS2SD: i32 = 27; + +def A_ADDQ: i32 = 28; +def A_SUBQ: i32 = 29; +def A_IMULQ: i32 = 30; +def A_IDIVQ: i32 = 31; +def A_DIVQ: i32 = 32; +def A_NEGQ: i32 = 33; +def A_NOTQ: i32 = 34; +def A_ANDQ: i32 = 35; +def A_ORQ: i32 = 36; +def A_XORQ: i32 = 37; +def A_SHLQ: i32 = 38; +def A_SHRQ: i32 = 39; +def A_CMPQ: i32 = 40; + +def A_PUSHQ: i32 = 41; +def A_POPQ: i32 = 42; +def A_LEAQ: i32 = 43; + +def A_CALL: i32 = 44; +def A_RET: i32 = 45; +def A_JMP: i32 = 46; +def A_JE: i32 = 47; +def A_JNE: i32 = 48; +def A_JL: i32 = 49; +def A_JLE: i32 = 50; +def A_JG: i32 = 51; +def A_JGE: i32 = 52; +def A_JB: i32 = 53; +def A_JBE: i32 = 54; +def A_JA: i32 = 55; +def A_JAE: i32 = 56; +def A_JZ: i32 = 57; +def A_JNZ: i32 = 58; + +def A_SYSCALL: i32 = 59; + +// ---- structs (mirror cmd/6a/a.h) -------------------------------------- + +type aoperand = struct { + atype: i32, // D_NONE / D_AX..D_R15 / D_CONST / D_INDIR / D_EXTERN / D_BRANCH + reg: i32, + offset: i64, + asym: str, +}; + +// `from` and `to` are pointer-to-aoperand (rather than embedded). The +// C cgen we currently bootstrap on doesn't support chained-dot through +// embedded structs (e.g. p.to.atype where `to` is a value field), but +// it does support chained-dot through pointer fields. Allocating each +// operand once per prog lets us write `p.to.atype` straightforwardly. +type aprog = struct { + as_: i32, + from: *aoperand, + to: *aoperand, + line: i32, + label: str, + link: *aprog, + bytes: *u8, // payload for A_DATA + nbytes: u64, +}; + +type asym = struct { + name: str, + defined: i32, + is_text: i32, + is_global: i32, + addr: u64, + idx: i32, + snext: *asym, +}; + +type areloc = struct { + off: u64, + kind: i32, + asy: *asym, + addend: i64, + rnext: *areloc, +}; + +type afixup = struct { + off: u64, // where the rel32 lands in .text + label: str, + fnext: *afixup, +}; + +type asm_ = struct { + a: *arena, + file: str, + src: *u8, + srclen: u64, + pos: u64, + line: i32, + + head: *aprog, + tail: *aprog, + + text: *u8, + textcap: u64, + textlen: u64, + + syms: *asym, + relocs: *areloc, + fixups: *afixup, + + errs: i32, +}; + +// selfhost/cmd/6a/lex.ww — port of cmd/6a/lex.c. +// +// Character-level helpers for 6a's line-oriented parser. The parser +// itself is in parse.ww; here we keep tokenisers for identifiers and +// numbers so parse.ww stays focused on syntax. + +export fn a_isidstart(c: i32) bool = { + if (c == 95) { return true; }; + if (c >= 65) { if (c <= 90) { return true; }; }; // A-Z + if (c >= 97) { if (c <= 122) { return true; }; }; // a-z + return false; +}; + +export fn a_isidcont(c: i32) bool = { + if (a_isidstart(c)) { return true; }; + if (c >= 48) { if (c <= 57) { return true; }; }; // 0-9 + if (c == 46) { return true; }; // . + return false; +}; + +// a_parsenum — read a leading [+-]?[0x|0X|0]?digits from p[0..n-1]. +// Returns (value, consumed). Stops at first non-digit. +// Plain Plan 9-style: $123 / $0x1f / $-7. Decimal default; 0x prefix +// for hex; 0 prefix for octal when followed by a digit (else just 0). +export fn a_parsenum(p: *u8, n: u64) (i64, u64) = { + let i: u64 = 0u64; + let neg: bool = false; + if (i < n) { + if (p[i] == 45u8) { neg = true; i += 1u64; } + else { if (p[i] == 43u8) { i += 1u64; }; }; + }; + let base: i64 = 10i64; + if (i + 1u64 < n) { + if (p[i] == 48u8) { + if (p[i + 1u64] == 120u8) { base = 16i64; i += 2u64; } + else { if (p[i + 1u64] == 88u8) { base = 16i64; i += 2u64; } + else { if (p[i + 1u64] >= 48u8) { if (p[i + 1u64] <= 55u8) { + base = 8i64; i += 1u64; + };};};}; + }; + }; + let v: i64 = 0i64; + let scan: bool = true; + for (scan) { + if (i >= n) { scan = false; } + else { + let c: u8 = p[i]; + let d: i64 = -1i64; + if (c >= 48u8) { if (c <= 57u8) { d = (c - 48u8): i64; }; }; + if (d < 0i64) { + if (base == 16i64) { + if (c >= 97u8) { if (c <= 102u8) { d = (c - 97u8): i64 + 10i64; }; }; + if (c >= 65u8) { if (c <= 70u8) { d = (c - 65u8): i64 + 10i64; }; }; + }; + }; + if (d < 0i64) { scan = false; } + else { if (d >= base) { scan = false; } + else { + v = v * base + d; + i += 1u64; + }; }; + }; + }; + if (neg) { v = -v; }; + return v, i; +}; + +// selfhost/cmd/6a/parse.ww — port of cmd/6a/parse.c. +// +// Line-oriented parser for the asm subset emitted by 6c. +// Grammar: +// line := blank | comment | label | text | instr +// blank := /^\s*$/ +// comment := /^\s*\/\/.*$/ +// label := /^IDENT:$/ +// text := TEXT name,$framesize +// instr := \tMNEM\t[OP1[, OP2]] +// OP := $NUM | REG | NUM(REG) | (REG) | name(SB) | label + +use os; +use mem; +use lex; +use types; + +fn streq_lit(p: *u8, n: u64, lit: str) bool = { + if (n != lit.len: u64) { return false; }; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (p[i] != lit[li]) { return false; }; + i += 1u64; + }; + return true; +}; + +// opcode_lookup — name (length-bounded *u8) → A_*. Returns 0 (A_NOP) +// if not found. +fn opcode_lookup(p: *u8, n: u64) i32 = { + if (streq_lit(p, n, "MOVQ")) { return A_MOVQ; }; + if (streq_lit(p, n, "MOVL")) { return A_MOVL; }; + if (streq_lit(p, n, "MOVB")) { return A_MOVB; }; + if (streq_lit(p, n, "MOVZBQ")) { return A_MOVZBQ; }; + if (streq_lit(p, n, "MOVSXD")) { return A_MOVSXD; }; + if (streq_lit(p, n, "MOVSD")) { return A_MOVSD; }; + if (streq_lit(p, n, "ADDSD")) { return A_ADDSD; }; + if (streq_lit(p, n, "SUBSD")) { return A_SUBSD; }; + if (streq_lit(p, n, "MULSD")) { return A_MULSD; }; + if (streq_lit(p, n, "DIVSD")) { return A_DIVSD; }; + if (streq_lit(p, n, "UCOMISD")) { return A_UCOMISD; }; + if (streq_lit(p, n, "CVTTSD2SI")) { return A_CVTTSD2SI; }; + if (streq_lit(p, n, "CVTSI2SD")) { return A_CVTSI2SD; }; + if (streq_lit(p, n, "MOVSS")) { return A_MOVSS; }; + if (streq_lit(p, n, "ADDSS")) { return A_ADDSS; }; + if (streq_lit(p, n, "SUBSS")) { return A_SUBSS; }; + if (streq_lit(p, n, "MULSS")) { return A_MULSS; }; + if (streq_lit(p, n, "DIVSS")) { return A_DIVSS; }; + if (streq_lit(p, n, "UCOMISS")) { return A_UCOMISS; }; + if (streq_lit(p, n, "CVTTSS2SI")) { return A_CVTTSS2SI; }; + if (streq_lit(p, n, "CVTSI2SS")) { return A_CVTSI2SS; }; + if (streq_lit(p, n, "CVTSD2SS")) { return A_CVTSD2SS; }; + if (streq_lit(p, n, "CVTSS2SD")) { return A_CVTSS2SD; }; + if (streq_lit(p, n, "ADDQ")) { return A_ADDQ; }; + if (streq_lit(p, n, "SUBQ")) { return A_SUBQ; }; + if (streq_lit(p, n, "IMULQ")) { return A_IMULQ; }; + if (streq_lit(p, n, "IDIVQ")) { return A_IDIVQ; }; + if (streq_lit(p, n, "DIVQ")) { return A_DIVQ; }; + if (streq_lit(p, n, "NEGQ")) { return A_NEGQ; }; + if (streq_lit(p, n, "NOTQ")) { return A_NOTQ; }; + if (streq_lit(p, n, "ANDQ")) { return A_ANDQ; }; + if (streq_lit(p, n, "ORQ")) { return A_ORQ; }; + if (streq_lit(p, n, "XORQ")) { return A_XORQ; }; + if (streq_lit(p, n, "SHLQ")) { return A_SHLQ; }; + if (streq_lit(p, n, "SHRQ")) { return A_SHRQ; }; + if (streq_lit(p, n, "CMPQ")) { return A_CMPQ; }; + if (streq_lit(p, n, "PUSHQ")) { return A_PUSHQ; }; + if (streq_lit(p, n, "POPQ")) { return A_POPQ; }; + if (streq_lit(p, n, "LEAQ")) { return A_LEAQ; }; + if (streq_lit(p, n, "CALL")) { return A_CALL; }; + if (streq_lit(p, n, "RET")) { return A_RET; }; + if (streq_lit(p, n, "JMP")) { return A_JMP; }; + if (streq_lit(p, n, "JE")) { return A_JE; }; + if (streq_lit(p, n, "JNE")) { return A_JNE; }; + if (streq_lit(p, n, "JL")) { return A_JL; }; + if (streq_lit(p, n, "JLE")) { return A_JLE; }; + if (streq_lit(p, n, "JG")) { return A_JG; }; + if (streq_lit(p, n, "JGE")) { return A_JGE; }; + if (streq_lit(p, n, "JB")) { return A_JB; }; + if (streq_lit(p, n, "JBE")) { return A_JBE; }; + if (streq_lit(p, n, "JA")) { return A_JA; }; + if (streq_lit(p, n, "JAE")) { return A_JAE; }; + if (streq_lit(p, n, "JZ")) { return A_JZ; }; + if (streq_lit(p, n, "JNZ")) { return A_JNZ; }; + if (streq_lit(p, n, "SYSCALL")) { return A_SYSCALL; }; + if (streq_lit(p, n, "TEXT")) { return A_TEXT; }; + if (streq_lit(p, n, "DATA")) { return A_DATA; }; + return A_NOP; +}; + +// reg_lookup — name → D_*. Returns D_NONE if not found. +fn reg_lookup(p: *u8, n: u64) i32 = { + if (streq_lit(p, n, "AX")) { return D_AX; }; + if (streq_lit(p, n, "BX")) { return D_BX; }; + if (streq_lit(p, n, "CX")) { return D_CX; }; + if (streq_lit(p, n, "DX")) { return D_DX; }; + if (streq_lit(p, n, "SP")) { return D_SP; }; + if (streq_lit(p, n, "BP")) { return D_BP; }; + if (streq_lit(p, n, "SI")) { return D_SI; }; + if (streq_lit(p, n, "DI")) { return D_DI; }; + if (streq_lit(p, n, "R8")) { return D_R8; }; + if (streq_lit(p, n, "R9")) { return D_R9; }; + if (streq_lit(p, n, "R10")) { return D_R10; }; + if (streq_lit(p, n, "R11")) { return D_R11; }; + if (streq_lit(p, n, "R12")) { return D_R12; }; + if (streq_lit(p, n, "R13")) { return D_R13; }; + if (streq_lit(p, n, "R14")) { return D_R14; }; + if (streq_lit(p, n, "R15")) { return D_R15; }; + if (streq_lit(p, n, "X0")) { return D_X0; }; + if (streq_lit(p, n, "X1")) { return D_X1; }; + if (streq_lit(p, n, "X2")) { return D_X2; }; + if (streq_lit(p, n, "X3")) { return D_X3; }; + if (streq_lit(p, n, "X4")) { return D_X4; }; + if (streq_lit(p, n, "X5")) { return D_X5; }; + if (streq_lit(p, n, "X6")) { return D_X6; }; + if (streq_lit(p, n, "X7")) { return D_X7; }; + if (streq_lit(p, n, "X8")) { return D_X8; }; + if (streq_lit(p, n, "X9")) { return D_X9; }; + if (streq_lit(p, n, "X10")) { return D_X10; }; + if (streq_lit(p, n, "X11")) { return D_X11; }; + if (streq_lit(p, n, "X12")) { return D_X12; }; + if (streq_lit(p, n, "X13")) { return D_X13; }; + if (streq_lit(p, n, "X14")) { return D_X14; }; + if (streq_lit(p, n, "X15")) { return D_X15; }; + if (streq_lit(p, n, "SB")) { return D_PSB; }; + if (streq_lit(p, n, "FP")) { return D_PFP; }; + return D_NONE; +}; + +export fn a_init(a: *asm_, ar: *arena, file: str, src: *u8, len: u64) void = { + a.a = ar; + a.file = file; + a.src = src; + a.srclen = len; + a.pos = 0u64; + a.line = 1; + a.head = nil; + a.tail = nil; + a.text = nil; + a.textcap = 0u64; + a.textlen = 0u64; + a.syms = nil; + a.relocs = nil; + a.fixups = nil; + a.errs = 0; +}; + +fn streq_str(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; +}; + +export fn a_intern(a: *asm_, name: str) *asym = { + let s: *asym = a.syms; + for (s != nil) { + if (streq_str(s.name, name)) { return s; }; + s = s.snext; + }; + let n: *asym = amalloc(a.a, 64u64): *asym; + n.name = name; + n.snext = a.syms; + a.syms = n; + return n; +}; + +fn perr(a: *asm_, msg: str) void = { + os.write(2, "6a: ".ptr, 4u64); + let f: str = a.file; + os.write(2, f.ptr, f.len: u64); + os.write(2, ": ".ptr, 2u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + a.errs += 1; +}; + +// dup_str — copy n bytes from p into a fresh heap str. +fn dup_str(a: *arena, p: *u8, n: u64) str = { + return astrndup(a, p, n); +}; + +// ---- line iteration & whitespace -------------------------------------- + +// Read next line into a fresh heap buffer; returns (ptr, len) or (nil,0) +// at EOF. Advances a.pos past the newline. +fn next_line(a: *asm_) (*u8, u64) = { + if (a.pos >= a.srclen) { return nil, 0u64; }; + let start: u64 = a.pos; + for (a.pos < a.srclen) { + if (a.src[a.pos] == 10u8) { a.pos = a.pos; a.pos += 0u64; } // no-op; explicit break via condition + else { a.pos += 1u64; continue; }; + // hit newline + let n: u64 = a.pos - start; + let buf: *u8 = amalloc(a.a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { buf[i] = a.src[start + i]; i += 1u64; }; + buf[n] = 0u8; + a.pos += 1u64; // skip newline + return buf, n; + }; + // EOF without trailing newline + let n: u64 = a.pos - start; + if (n == 0u64) { return nil, 0u64; }; + let buf: *u8 = amalloc(a.a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { buf[i] = a.src[start + i]; i += 1u64; }; + buf[n] = 0u8; + return buf, n; +}; + +fn skip_ws(p: *u8, off: u64, n: u64) u64 = { + let i: u64 = off; + for (i < n) { + if (p[i] != 32u8) { if (p[i] != 9u8) { return i; }; }; + i += 1u64; + }; + return i; +}; + +// parse_operand — parse one operand from p[off..n), populate out. +// Returns new offset (clamped to n on error). +fn parse_operand(a: *asm_, p: *u8, off_in: u64, n: u64, out: *aoperand) u64 = { + let off: u64 = skip_ws(p, off_in, n); + out.atype = D_NONE; + out.reg = 0; + out.offset = 0i64; + let empty_str: str; + empty_str.ptr = nil; empty_str.len = 0; + out.asym = empty_str; + if (off >= n) { return off; }; + let c0: u8 = p[off]; + + // $NUM + if (c0 == 36u8) { // '$' + off += 1u64; + let v: i64; + let used: u64; + v, used = a_parsenum(p + off, n - off); + out.atype = D_CONST; + out.offset = v; + return off + used; + }; + + // (REG) + if (c0 == 40u8) { // '(' + off += 1u64; + let rstart: u64 = off; + for (off < n) { + if (p[off] == 41u8) { off = off; off += 0u64; } // no-op marker + else { off += 1u64; continue; }; + let rn: u64 = off - rstart; + let r: i32 = reg_lookup(p + rstart, rn); + if (r == 0) { perr(a, "bad register in indirect"); return n; }; + out.atype = D_INDIR; + out.reg = r; + out.offset = 0i64; + return off + 1u64; // past ')' + }; + perr(a, "missing ')' in indirect"); + return n; + }; + + // number(REG) — possibly signed — or bare $NUM-less constant + let cur: u64 = off; + let is_num: bool = false; + if (cur < n) { + if (p[cur] == 45u8) { is_num = true; } + else { if (p[cur] >= 48u8) { if (p[cur] <= 57u8) { is_num = true; }; }; }; + }; + if (is_num) { + let v: i64; + let used: u64; + v, used = a_parsenum(p + off, n - off); + let after: u64 = off + used; + if (after < n) { if (p[after] == 40u8) { // '(' + let rstart: u64 = after + 1u64; + let cur2: u64 = rstart; + for (cur2 < n) { + if (p[cur2] == 41u8) { cur2 = cur2; cur2 += 0u64; } + else { cur2 += 1u64; continue; }; + let rn: u64 = cur2 - rstart; + let r: i32 = reg_lookup(p + rstart, rn); + if (r == 0) { perr(a, "bad register"); return n; }; + out.atype = D_INDIR; + out.reg = r; + out.offset = v; + return cur2 + 1u64; + }; + perr(a, "missing ')'"); + return n; + };}; + out.atype = D_CONST; + out.offset = v; + return after; + }; + + // IDENT — register, symbol(SB), or branch label + if (a_isidstart(c0: i32)) { + let istart: u64 = off; + for (off < n) { + if (a_isidcont(p[off]: i32)) { off += 1u64; continue; }; + off = off; off += 0u64; // loop break + let in_: u64 = off - istart; + // IDENT(SB) — external + if (off < n) { if (p[off] == 40u8) { // '(' + let rstart: u64 = off + 1u64; + let cur2: u64 = rstart; + for (cur2 < n) { + if (p[cur2] == 41u8) { cur2 = cur2; cur2 += 0u64; } + else { cur2 += 1u64; continue; }; + let rn: u64 = cur2 - rstart; + let r: i32 = reg_lookup(p + rstart, rn); + if (r == D_PSB) { + out.atype = D_EXTERN; + out.asym = dup_str(a.a, p + istart, in_); + } else { + out.atype = D_INDIR; + out.reg = r; + out.offset = 0i64; + }; + return cur2 + 1u64; + }; + perr(a, "missing ')'"); + return n; + };}; + let r: i32 = reg_lookup(p + istart, in_); + if (r != D_NONE) { + out.atype = r; + return off; + }; + out.atype = D_BRANCH; + out.asym = dup_str(a.a, p + istart, in_); + return off; + }; + // EOF inside ident + let in_: u64 = off - istart; + let r: i32 = reg_lookup(p + istart, in_); + if (r != D_NONE) { out.atype = r; return off; }; + out.atype = D_BRANCH; + out.asym = dup_str(a.a, p + istart, in_); + return off; + }; + + perr(a, "unrecognised operand"); + return n; +}; + +// Append a fresh aprog to the list with given opcode and label. +fn add_prog(a: *asm_, opc: i32, lbl: str) *aprog = { + let pr: *aprog = amalloc(a.a, 96u64): *aprog; + pr.as_ = opc; + pr.line = a.line; + pr.label = lbl; + pr.link = nil; + pr.bytes = nil; + pr.nbytes = 0u64; + pr.from = amalloc(a.a, 48u64): *aoperand; + pr.to = amalloc(a.a, 48u64): *aoperand; + if (a.head == nil) { a.head = pr; } + else { + // `a.tail.link = pr` would be a chained-dot write through a + // pointer field, which the C cgen we bootstrap on doesn't + // support (silently drops the store). Bind a local first. + let tail: *aprog = a.tail; + tail.link = pr; + }; + a.tail = pr; + return pr; +}; + +export fn a_parse(a: *asm_) i32 = { + let pending: str; + pending.ptr = nil; pending.len = 0; + + for (true) { + let line: *u8; + let n: u64; + line, n = next_line(a); + if (line == nil) { return a.errs; }; + + // skip leading ws + let i: u64 = skip_ws(line, 0u64, n); + // blank or //-comment + if (i >= n) { a.line += 1; continue; }; + if (i + 1u64 < n) { + if (line[i] == 47u8) { if (line[i + 1u64] == 47u8) { + a.line += 1; continue; + };}; + }; + + // Label? IDENT: starting at column 0 (no leading tab). + // Only if the identifier is followed by ':'. Otherwise, fall + // through to mnemonic parsing so e.g. `TEXT foo,$0` (which + // also starts with an idchar in column 0) gets parsed. + if (line[0u64] != 9u8) { + if (a_isidstart(line[i]: i32)) { + let q: u64 = i; + let scan_id: bool = true; + for (scan_id) { + if (q >= n) { scan_id = false; } + else { if (a_isidcont(line[q]: i32)) { q += 1u64; } + else { scan_id = false; }; }; + }; + if (q < n) { if (line[q] == 58u8) { // ':' + let nm: str = dup_str(a.a, line + i, q - i); + // Pending label gets a NOP prog so addresses pin. + if (pending.len > 0) { + let np: *aprog = add_prog(a, A_NOP, pending); + }; + pending = nm; + a.line += 1; + continue; + };}; + // not a label — fall through to mnemonic parse + }; + }; + + // MNEMONIC at the start of the rest. Scan to first ws/EOL. + let mstart: u64 = i; + let m: u64 = mstart; + let scan: bool = true; + for (scan) { + if (m >= n) { scan = false; } + else { if (line[m] == 32u8) { scan = false; } + else { if (line[m] == 9u8) { scan = false; } + else { m += 1u64; }; }; }; + }; + let mlen: u64 = m - mstart; + let opc: i32 = opcode_lookup(line + mstart, mlen); + if (opc == 0) { + if (mlen > 0u64) { + perr(a, "unknown opcode"); + }; + pending.ptr = nil; pending.len = 0; + a.line += 1; continue; + }; + + let pr: *aprog = add_prog(a, opc, pending); + pending.ptr = nil; pending.len = 0; + + // Skip ws after mnemonic + let r0: u64 = skip_ws(line, m, n); + + if (opc == A_TEXT) { + // TEXT name,$framesize — find first ',' as the end of name. + let q: u64 = r0; + let comma_pos: u64 = n; + let scan_t: bool = true; + for (scan_t) { + if (q >= n) { scan_t = false; } + else { if (line[q] == 44u8) { comma_pos = q; scan_t = false; } + else { q += 1u64; }; }; + }; + let to_op: *aoperand = pr.to; + to_op.atype = D_EXTERN; + to_op.asym = dup_str(a.a, line + r0, comma_pos - r0); + if (comma_pos < n) { + let p2: u64 = comma_pos + 1u64; + p2 = skip_ws(line, p2, n); + if (p2 < n) { if (line[p2] == 36u8) { p2 += 1u64; }; }; + let v: i64; + let used: u64; + v, used = a_parsenum(line + p2, n - p2); + let from_op: *aoperand = pr.from; + from_op.atype = D_CONST; + from_op.offset = v; + }; + a.line += 1; continue; + }; + + if (opc == A_DATA) { + // DATA name(SB),"escaped bytes" — find first '(' as end of name. + let q: u64 = r0; + let lparen: u64 = n; + let scan_d: bool = true; + for (scan_d) { + if (q >= n) { scan_d = false; } + else { if (line[q] == 40u8) { lparen = q; scan_d = false; } + else { q += 1u64; }; }; + }; + let to_op: *aoperand = pr.to; + to_op.atype = D_EXTERN; + to_op.asym = dup_str(a.a, line + r0, lparen - r0); + // Skip past `(SB)` to land just after ')'. + let p2: u64 = lparen; + let scan_d2: bool = true; + for (scan_d2) { + if (p2 >= n) { scan_d2 = false; } + else { if (line[p2] == 41u8) { p2 += 1u64; scan_d2 = false; } + else { p2 += 1u64; }; }; + }; + // Skip ws / ',' / tab between `)` and the `"`. + let scan_d3: bool = true; + for (scan_d3) { + if (p2 >= n) { scan_d3 = false; } + else { if (line[p2] == 32u8) { p2 += 1u64; } + else { if (line[p2] == 44u8) { p2 += 1u64; } + else { if (line[p2] == 9u8) { p2 += 1u64; } + else { scan_d3 = false; }; }; }; }; + }; + if (p2 >= n) { perr(a, "DATA missing payload"); a.line += 1; continue; }; + if (line[p2] != 34u8) { perr(a, "DATA expects \"...\""); a.line += 1; continue; }; + p2 += 1u64; // past opening " + // Parse escape sequence into a fresh growable buffer. + let cap: u64 = 32u64; + let blen: u64 = 0u64; + let dbuf: *u8 = amalloc(a.a, cap): *u8; + for (p2 < n) { + if (line[p2] == 34u8) { p2 = p2; p2 += 0u64; p2 = n + 1u64; } + else { + let ch: u8 = line[p2]; + p2 += 1u64; + if (ch == 92u8) { // '\' + if (p2 < n) { + let e: u8 = line[p2]; + p2 += 1u64; + if (e == 110u8) { ch = 10u8; } // 'n' + else { if (e == 116u8) { ch = 9u8; } + else { if (e == 114u8) { ch = 13u8; } + else { if (e == 92u8) { ch = 92u8; } + else { if (e == 34u8) { ch = 34u8; } + else { if (e == 48u8) { ch = 0u8; } + else { if (e == 120u8) { // 'x' + if (p2 + 1u64 < n) { + let hi: u8 = line[p2]; + let lo: u8 = line[p2 + 1u64]; + p2 += 2u64; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi <= 57u8) { h = hi - 48u8; } + else { h = (hi | 32u8) - 97u8 + 10u8; }; + if (lo <= 57u8) { l = lo - 48u8; } + else { l = (lo | 32u8) - 97u8 + 10u8; }; + ch = (h << 4u8) | l; + }; + } + else { ch = e; };};};};};};}; + }; + }; + if (blen + 1u64 > cap) { + let ncap: u64 = cap * 2u64; + let nb: *u8 = amalloc(a.a, ncap): *u8; + let bi: u64 = 0u64; + for (bi < blen) { nb[bi] = dbuf[bi]; bi += 1u64; }; + dbuf = nb; + cap = ncap; + }; + dbuf[blen] = ch; + blen += 1u64; + }; + }; + pr.bytes = dbuf; + pr.nbytes = blen; + a.line += 1; continue; + }; + + // Generic instruction: 0/1/2 operands separated by ','. + // Find top-level comma. + let comma: i64 = -1i64; + let q: u64 = r0; + for (q < n) { + if (line[q] == 44u8) { + if (comma < 0i64) { comma = q: i64; }; + }; + q += 1u64; + }; + if (comma >= 0i64) { + let cu: u64 = comma: u64; + parse_operand(a, line, r0, cu, pr.from); + parse_operand(a, line + (cu + 1u64), 0u64, n - (cu + 1u64), pr.to); + } else { if (r0 < n) { + parse_operand(a, line, r0, n, pr.to); + };}; + + a.line += 1; + }; + return a.errs; +}; + +// selfhost/cmd/6a/asm.ww — port of cmd/6a/asm.c. +// +// Encode the parsed aprog list into amd64 machine bytes, appending to +// asm_.text. Relocations for CALL/branch targets that resolve to +// externals are queued in asm_.relocs. +// +// Encoding subset matches what 6c emits — see cmd/6a/asm.c for the +// authoritative list. Helpers (rcode/rhi/modrm/emit_rex etc.) are +// fully ported; a_encode itself is still a stub pending the full +// switch over A_*. + +use os; +use mem; +use types; + +// ---- text buffer growth ------------------------------------------------ + +export fn a_emit_byte(a: *asm_, b: u8) void = { + if (a.textlen + 1u64 > a.textcap) { + let nc: u64 = a.textcap; + if (nc == 0u64) { nc = 4096u64; }; + nc = nc * 2u64; + let nb: *u8 = os.alloc(nc): *u8; + let i: u64 = 0u64; + for (i < a.textlen) { nb[i] = a.text[i]; i += 1u64; }; + a.text = nb; + a.textcap = nc; + }; + a.text[a.textlen] = b; + a.textlen += 1u64; +}; + +export fn a_emit_u32(a: *asm_, v: u32) void = { + a_emit_byte(a, (v & 255u32): u8); + a_emit_byte(a, ((v >> 8u32) & 255u32): u8); + a_emit_byte(a, ((v >> 16u32) & 255u32): u8); + a_emit_byte(a, ((v >> 24u32) & 255u32): u8); +}; + +export fn a_addreloc(a: *asm_, off: u64, kind: i32, s: *asym, add: i64) void = { + let r: *areloc = amalloc(a.a, 48u64): *areloc; + r.off = off; + r.kind = kind; + r.asy = s; + r.addend = add; + r.rnext = a.relocs; + a.relocs = r; +}; + +// ---- register codes ---------------------------------------------------- + +// Low 3 bits of register encoding. +fn rcode(r: i32) i32 = { + if (r == D_AX) { return 0; }; if (r == D_CX) { return 1; }; + if (r == D_DX) { return 2; }; if (r == D_BX) { return 3; }; + if (r == D_SP) { return 4; }; if (r == D_BP) { return 5; }; + if (r == D_SI) { return 6; }; if (r == D_DI) { return 7; }; + if (r == D_R8) { return 0; }; if (r == D_R9) { return 1; }; + if (r == D_R10) { return 2; }; if (r == D_R11) { return 3; }; + if (r == D_R12) { return 4; }; if (r == D_R13) { return 5; }; + if (r == D_R14) { return 6; }; if (r == D_R15) { return 7; }; + if (r == D_X0) { return 0; }; if (r == D_X1) { return 1; }; + if (r == D_X2) { return 2; }; if (r == D_X3) { return 3; }; + if (r == D_X4) { return 4; }; if (r == D_X5) { return 5; }; + if (r == D_X6) { return 6; }; if (r == D_X7) { return 7; }; + if (r == D_X8) { return 0; }; if (r == D_X9) { return 1; }; + if (r == D_X10) { return 2; }; if (r == D_X11) { return 3; }; + if (r == D_X12) { return 4; }; if (r == D_X13) { return 5; }; + if (r == D_X14) { return 6; }; if (r == D_X15) { return 7; }; + return 0; +}; + +// 1 if r needs the REX high bit (R8..R15 or X8..X15). +fn rhi(r: i32) i32 = { + if (r >= D_R8) { if (r <= D_R15) { return 1; }; }; + if (r >= D_X8) { if (r <= D_X15) { return 1; }; }; + return 0; +}; + +fn is_xmm(r: i32) bool = { + if (r >= D_X0) { if (r <= D_X15) { return true; }; }; + return false; +}; + +// ModR/M byte builder. +fn modrm_byte(mod: i32, reg: i32, rm: i32) u8 = { + return (((mod & 3) << 6) | ((reg & 7) << 3) | (rm & 7)): u8; +}; + +// REX prefix; W=1 for 64-bit operand size. +fn emit_rex(a: *asm_, regbit: i32, rmbit: i32, w: i32) void = { + let b: u8 = 64u8; // 0x40 + if (w != 0) { b = b | 8u8; }; + if (regbit != 0) { b = b | 4u8; }; + if (rmbit != 0) { b = b | 1u8; }; + if (b != 64u8) { a_emit_byte(a, b); } + else { if (w != 0) { a_emit_byte(a, b); }; }; +}; + +// ModR/M + (optional) SIB + displacement for [base+disp]. +// Special-cases SP (needs SIB) and BP (forces explicit disp). +fn emit_modrm_mem(a: *asm_, reg_field: i32, base: i32, disp: i64) void = { + let rm: i32 = rcode(base); + let needsib: bool = (rm == 4); + let forced_disp: bool = false; + if (rm == 5) { if (disp == 0i64) { forced_disp = true; }; }; + + let mod: i32 = 2; + if (disp == 0i64) { + if (!forced_disp) { mod = 0; } + else { mod = 1; }; + } else { + if (disp >= -128i64) { if (disp <= 127i64) { mod = 1; }; }; + }; + + a_emit_byte(a, modrm_byte(mod, reg_field, rm)); + if (needsib) { + a_emit_byte(a, 36u8); // 0x24: scale=0 idx=4(none) base=4 + }; + if (mod == 1) { + a_emit_byte(a, (disp: u64 & 255u64): u8); + } else { if (mod == 2) { + a_emit_u32(a, disp: u32); + };}; +}; + +// reg→reg "src, dst" generic encoding (89 /r, 01 /r, etc.). +fn encode_rr(a: *asm_, opcode: u8, src: i32, dst: i32) void = { + emit_rex(a, rhi(src), rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm_byte(3, rcode(src), rcode(dst))); +}; + +// reg→mem(base, disp) (e.g. MOVQ src reg into mem; opcode = 0x89). +fn encode_rm(a: *asm_, opcode: u8, src_reg: i32, base: i32, disp: i64) void = { + emit_rex(a, rhi(src_reg), rhi(base), 1); + a_emit_byte(a, opcode); + emit_modrm_mem(a, rcode(src_reg), base, disp); +}; + +// mem(base, disp) → reg (e.g. MOVQ mem into reg; opcode = 0x8B). +fn encode_mr(a: *asm_, opcode: u8, dst_reg: i32, base: i32, disp: i64) void = { + emit_rex(a, rhi(dst_reg), rhi(base), 1); + a_emit_byte(a, opcode); + emit_modrm_mem(a, rcode(dst_reg), base, disp); +}; + +// OPCODE /n imm32 reg form (e.g. ADDQ $imm, reg). +fn encode_ri_imm32(a: *asm_, opcode: u8, subop: i32, dst: i32, imm: i32) void = { + emit_rex(a, 0, rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm_byte(3, subop, rcode(dst))); + a_emit_u32(a, imm: u32); +}; + +// Unary on reg: F7 /n reg, etc. +fn encode_unary(a: *asm_, opcode: u8, subop: i32, dst: i32) void = { + emit_rex(a, 0, rhi(dst), 1); + a_emit_byte(a, opcode); + a_emit_byte(a, modrm_byte(3, subop, rcode(dst))); +}; + +// SSE2 helpers. Plan 9 syntax: source first, destination second. +// For ADDSD-style ops we put dst in the reg field, src in r/m. +fn sse_rr(a: *asm_, prefix: u8, op2: u8, reg_op: i32, rm_op: i32) void = { + if (prefix != 0u8) { a_emit_byte(a, prefix); }; + emit_rex(a, rhi(reg_op), rhi(rm_op), 0); + a_emit_byte(a, 15u8); // 0x0F + a_emit_byte(a, op2); + a_emit_byte(a, modrm_byte(3, rcode(reg_op), rcode(rm_op))); +}; + +fn sse_mr_load(a: *asm_, prefix: u8, op2: u8, reg_op: i32, base: i32, disp: i64) void = { + if (prefix != 0u8) { a_emit_byte(a, prefix); }; + emit_rex(a, rhi(reg_op), rhi(base), 0); + a_emit_byte(a, 15u8); + a_emit_byte(a, op2); + emit_modrm_mem(a, rcode(reg_op), base, disp); +}; + +// REX.W variant of sse_rr (CVTTSD2SI / CVTSI2SD). +fn sse_rr_w(a: *asm_, prefix: u8, op2: u8, reg_op: i32, rm_op: i32) void = { + if (prefix != 0u8) { a_emit_byte(a, prefix); }; + emit_rex(a, rhi(reg_op), rhi(rm_op), 1); + a_emit_byte(a, 15u8); + a_emit_byte(a, op2); + a_emit_byte(a, modrm_byte(3, rcode(reg_op), rcode(rm_op))); +}; + +// ---- label resolution / fixups ---------------------------------------- + +fn streq(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 resolve_label(a: *asm_, name: str) u64 = { + let s: *asym = a.syms; + for (s != nil) { + if (s.defined != 0) { if (streq(s.name, name)) { return s.addr; }; }; + s = s.snext; + }; + return 0u64; +}; + +fn label_defined(a: *asm_, name: str) bool = { + let s: *asym = a.syms; + for (s != nil) { + if (s.defined != 0) { if (streq(s.name, name)) { return true; }; }; + s = s.snext; + }; + return false; +}; + +// ---- fixup helper ----------------------------------------------------- + +fn add_fixup(a: *asm_, off: u64, label: str) void = { + let f: *afixup = amalloc(a.a, 48u64): *afixup; + f.off = off; + f.label = label; + f.fnext = a.fixups; + a.fixups = f; +}; + +fn is_gpr(t: i32) bool = { + if (t >= D_AX) { if (t <= D_R15) { return true; }; }; + return false; +}; + +// `a_intern` lives in parse.ww — flat-scope concat lets us call it +// directly without an @symbol declaration here. + +// ---- a_encode --------------------------------------------------------- + +export fn a_encode(a: *asm_) i32 = { + let p: *aprog = a.head; + for (p != nil) { + // Define any pending label at the current PC. + if (p.label.len > 0) { + let s: *asym = a_intern(a, p.label); + s.defined = 1; + s.is_text = 1; + s.addr = a.textlen; + }; + let op: i32 = p.as_; + + if (op == A_NOP) { + p = p.link; continue; + }; + if (op == A_TEXT) { + let s: *asym = a_intern(a, p.to.asym); + s.defined = 1; + s.is_text = 1; + s.is_global = 1; + s.addr = a.textlen; + p = p.link; continue; + }; + if (op == A_DATA) { + let s: *asym = a_intern(a, p.to.asym); + s.defined = 1; + s.is_text = 1; + s.is_global = 1; + s.addr = a.textlen; + let i: u64 = 0u64; + for (i < p.nbytes) { a_emit_byte(a, p.bytes[i]); i += 1u64; }; + p = p.link; continue; + }; + if (op == A_RET) { + a_emit_byte(a, 195u8); // 0xC3 + p = p.link; continue; + }; + if (op == A_SYSCALL) { + a_emit_byte(a, 15u8); + a_emit_byte(a, 5u8); + p = p.link; continue; + }; + if (op == A_PUSHQ) { + if (rhi(p.to.atype) != 0) { a_emit_byte(a, 65u8); }; // 0x41 + a_emit_byte(a, (80 + rcode(p.to.atype)): u8); // 0x50 + p = p.link; continue; + }; + if (op == A_POPQ) { + if (rhi(p.to.atype) != 0) { a_emit_byte(a, 65u8); }; + a_emit_byte(a, (88 + rcode(p.to.atype)): u8); // 0x58 + p = p.link; continue; + }; + if (op == A_NEGQ) { encode_unary(a, 247u8, 3, p.to.atype); p = p.link; continue; }; + if (op == A_NOTQ) { encode_unary(a, 247u8, 2, p.to.atype); p = p.link; continue; }; + if (op == A_IDIVQ) { encode_unary(a, 247u8, 7, p.to.atype); p = p.link; continue; }; + if (op == A_DIVQ) { encode_unary(a, 247u8, 6, p.to.atype); p = p.link; continue; }; + + if (op == A_MOVQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + let v: i64 = p.from.offset; + if (v >= -2147483648i64) { if (v <= 2147483647i64) { + encode_ri_imm32(a, 199u8, 0, tt, v: i32); + p = p.link; continue; + };}; + // movabs r64, imm64: REX.W B8+rd imm64 + emit_rex(a, 0, rhi(tt), 1); + a_emit_byte(a, (184 + rcode(tt)): u8); + let k: i32 = 0; + for (k < 8) { + a_emit_byte(a, ((v: u64 >> (k: u64 * 8u64)) & 255u64): u8); + k += 1; + }; + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (is_gpr(tt)) { + encode_rr(a, 137u8, ft, tt); // 0x89 + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 139u8, tt, p.from.reg, p.from.offset); // 0x8B + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_INDIR) { + encode_rm(a, 137u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_CONST) { if (tt == D_INDIR) { + emit_rex(a, 0, rhi(p.to.reg), 1); + a_emit_byte(a, 199u8); + emit_modrm_mem(a, 0, p.to.reg, p.to.offset); + a_emit_u32(a, p.from.offset: u32); + p = p.link; continue; + };}; + if (ft == D_EXTERN) { if (is_gpr(tt)) { + // RIP-relative load: 48 8B /r mod=00 rm=5 disp32 + emit_rex(a, rhi(tt), 0, 1); + a_emit_byte(a, 139u8); + a_emit_byte(a, modrm_byte(0, rcode(tt), 5)); + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.from.asym); + a_addreloc(a, reloff, 2, s, -4i64); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_EXTERN) { + // RIP-relative store: 48 89 /r mod=00 rm=5 disp32 + emit_rex(a, rhi(ft), 0, 1); + a_emit_byte(a, 137u8); + a_emit_byte(a, modrm_byte(0, rcode(ft), 5)); + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.to.asym); + a_addreloc(a, reloff, 2, s, -4i64); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVQ shape\n".ptr, 27u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVB) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_gpr(ft)) { if (tt == D_INDIR) { + emit_rex(a, rhi(ft), rhi(p.to.reg), 0); + a_emit_byte(a, 136u8); // 0x88 + emit_modrm_mem(a, rcode(ft), p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 0); + a_emit_byte(a, 138u8); // 0x8A + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVB shape\n".ptr, 27u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVZBQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 1); + a_emit_byte(a, 15u8); + a_emit_byte(a, 182u8); // 0xB6 + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVZBQ shape\n".ptr, 29u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVL) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_gpr(ft)) { if (tt == D_INDIR) { + emit_rex(a, rhi(ft), rhi(p.to.reg), 0); + a_emit_byte(a, 137u8); + emit_modrm_mem(a, rcode(ft), p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 0); + a_emit_byte(a, 139u8); + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (is_gpr(tt)) { + emit_rex(a, rhi(ft), rhi(tt), 0); + a_emit_byte(a, 137u8); + a_emit_byte(a, modrm_byte(3, rcode(ft), rcode(tt))); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVL shape\n".ptr, 27u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVSXD) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_INDIR) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), rhi(p.from.reg), 1); + a_emit_byte(a, 99u8); // 0x63 + emit_modrm_mem(a, rcode(tt), p.from.reg, p.from.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVSXD shape\n".ptr, 29u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_MOVSD) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_xmm(ft)) { if (is_xmm(tt)) { + sse_rr(a, 242u8, 16u8, tt, ft); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_xmm(tt)) { + sse_mr_load(a, 242u8, 16u8, tt, p.from.reg, p.from.offset); + p = p.link; continue; + };}; + if (is_xmm(ft)) { if (tt == D_INDIR) { + sse_mr_load(a, 242u8, 17u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVSD shape\n".ptr, 28u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_ADDSD) { sse_rr(a, 242u8, 88u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_SUBSD) { sse_rr(a, 242u8, 92u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_MULSD) { sse_rr(a, 242u8, 89u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_DIVSD) { sse_rr(a, 242u8, 94u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_UCOMISD) { sse_rr(a, 102u8, 46u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTTSD2SI) { sse_rr_w(a, 242u8, 44u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSI2SD) { sse_rr_w(a, 242u8, 42u8, p.to.atype, p.from.atype); p = p.link; continue; }; + + if (op == A_MOVSS) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (is_xmm(ft)) { if (is_xmm(tt)) { + sse_rr(a, 243u8, 16u8, tt, ft); p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_xmm(tt)) { + sse_mr_load(a, 243u8, 16u8, tt, p.from.reg, p.from.offset); + p = p.link; continue; + };}; + if (is_xmm(ft)) { if (tt == D_INDIR) { + sse_mr_load(a, 243u8, 17u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + os.write(2, "6a: unsupported MOVSS shape\n".ptr, 28u64); + a.errs += 1; + p = p.link; continue; + }; + + if (op == A_ADDSS) { sse_rr(a, 243u8, 88u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_SUBSS) { sse_rr(a, 243u8, 92u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_MULSS) { sse_rr(a, 243u8, 89u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_DIVSS) { sse_rr(a, 243u8, 94u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_UCOMISS) { sse_rr(a, 0u8, 46u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTTSS2SI) { sse_rr_w(a, 243u8, 44u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSI2SS) { sse_rr_w(a, 243u8, 42u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSD2SS) { sse_rr(a, 242u8, 90u8, p.to.atype, p.from.atype); p = p.link; continue; }; + if (op == A_CVTSS2SD) { sse_rr(a, 243u8, 90u8, p.to.atype, p.from.atype); p = p.link; continue; }; + + if (op == A_ADDQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 0, tt, p.from.offset: i32); // 0x81 + p = p.link; continue; + };}; + if (ft == D_CONST) { if (tt == D_INDIR) { + emit_rex(a, 0, rhi(p.to.reg), 1); + a_emit_byte(a, 129u8); + emit_modrm_mem(a, 0, p.to.reg, p.to.offset); + a_emit_u32(a, p.from.offset: u32); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_INDIR) { + encode_rm(a, 1u8, ft, p.to.reg, p.to.offset); + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 3u8, tt, p.from.reg, p.from.offset); + p = p.link; continue; + };}; + encode_rr(a, 1u8, ft, tt); + p = p.link; continue; + }; + + if (op == A_SUBQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 5, tt, p.from.offset: i32); + p = p.link; continue; + };}; + if (ft == D_CONST) { if (tt == D_INDIR) { + emit_rex(a, 0, rhi(p.to.reg), 1); + a_emit_byte(a, 129u8); + emit_modrm_mem(a, 5, p.to.reg, p.to.offset); + a_emit_u32(a, p.from.offset: u32); + p = p.link; continue; + };}; + if (is_gpr(ft)) { if (tt == D_INDIR) { + encode_rm(a, 41u8, ft, p.to.reg, p.to.offset); // 0x29 + p = p.link; continue; + };}; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 43u8, tt, p.from.reg, p.from.offset); // 0x2B + p = p.link; continue; + };}; + encode_rr(a, 41u8, ft, tt); + p = p.link; continue; + }; + + if (op == A_ANDQ) { encode_rr(a, 33u8, p.from.atype, p.to.atype); p = p.link; continue; }; // 0x21 + if (op == A_ORQ) { encode_rr(a, 9u8, p.from.atype, p.to.atype); p = p.link; continue; }; // 0x09 + if (op == A_XORQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 6, tt, p.from.offset: i32); + p = p.link; continue; + };}; + encode_rr(a, 49u8, ft, tt); // 0x31 + p = p.link; continue; + }; + if (op == A_IMULQ) { + emit_rex(a, rhi(p.to.atype), rhi(p.from.atype), 1); + a_emit_byte(a, 15u8); + a_emit_byte(a, 175u8); // 0xAF + a_emit_byte(a, modrm_byte(3, rcode(p.to.atype), rcode(p.from.atype))); + p = p.link; continue; + }; + if (op == A_SHLQ) { encode_unary(a, 211u8, 4, p.to.atype); p = p.link; continue; }; // 0xD3 + if (op == A_SHRQ) { encode_unary(a, 211u8, 5, p.to.atype); p = p.link; continue; }; + if (op == A_CMPQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_CONST) { if (is_gpr(tt)) { + encode_ri_imm32(a, 129u8, 7, tt, p.from.offset: i32); + p = p.link; continue; + };}; + encode_rr(a, 57u8, ft, tt); // 0x39 + p = p.link; continue; + }; + + if (op == A_LEAQ) { + let ft: i32 = p.from.atype; + let tt: i32 = p.to.atype; + if (ft == D_INDIR) { if (is_gpr(tt)) { + encode_mr(a, 141u8, tt, p.from.reg, p.from.offset); // 0x8D + p = p.link; continue; + };}; + if (ft == D_EXTERN) { if (is_gpr(tt)) { + emit_rex(a, rhi(tt), 0, 1); + a_emit_byte(a, 141u8); + a_emit_byte(a, modrm_byte(0, rcode(tt), 5)); + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.from.asym); + a_addreloc(a, reloff, 2, s, -4i64); + p = p.link; continue; + };}; + p = p.link; continue; + }; + + if (op == A_CALL) { + let tt: i32 = p.to.atype; + if (tt == D_EXTERN) { + a_emit_byte(a, 232u8); // 0xE8 + let reloff: u64 = a.textlen; + a_emit_u32(a, 0u32); + let s: *asym = a_intern(a, p.to.asym); + a_addreloc(a, reloff, 4, s, -4i64); + p = p.link; continue; + }; + if (tt == D_BRANCH) { + a_emit_byte(a, 232u8); + add_fixup(a, a.textlen, p.to.asym); + a_emit_u32(a, 0u32); + p = p.link; continue; + }; + if (is_gpr(tt)) { + if (rhi(tt) != 0) { a_emit_byte(a, 65u8); }; + a_emit_byte(a, 255u8); // 0xFF + a_emit_byte(a, modrm_byte(3, 2, rcode(tt))); + p = p.link; continue; + }; + p = p.link; continue; + }; + + if (op == A_JMP) { + a_emit_byte(a, 233u8); // 0xE9 + add_fixup(a, a.textlen, p.to.asym); + a_emit_u32(a, 0u32); + p = p.link; continue; + }; + + // Conditional jumps. 0x0F + cc + rel32. + let cc: u8 = 0u8; + let is_jcc: bool = true; + if (op == A_JE) { cc = 132u8; } // 0x84 + else { if (op == A_JZ) { cc = 132u8; } + else { if (op == A_JNE) { cc = 133u8; } + else { if (op == A_JNZ) { cc = 133u8; } + else { if (op == A_JL) { cc = 140u8; } + else { if (op == A_JLE) { cc = 142u8; } + else { if (op == A_JG) { cc = 143u8; } + else { if (op == A_JGE) { cc = 141u8; } + else { if (op == A_JB) { cc = 130u8; } + else { if (op == A_JBE) { cc = 134u8; } + else { if (op == A_JA) { cc = 135u8; } + else { if (op == A_JAE) { cc = 131u8; } + else { is_jcc = false; };};};};};};};};};};};}; + if (is_jcc) { + a_emit_byte(a, 15u8); + a_emit_byte(a, cc); + add_fixup(a, a.textlen, p.to.asym); + a_emit_u32(a, 0u32); + p = p.link; continue; + }; + + os.write(2, "6a: unsupported opcode\n".ptr, 23u64); + a.errs += 1; + p = p.link; + }; + + // Second pass: patch fixups (forward label refs). + let f: *afixup = a.fixups; + for (f != nil) { + if (!label_defined(a, f.label)) { + os.write(2, "6a: undefined label '".ptr, 21u64); + let lbl: str = f.label; + os.write(2, lbl.ptr, lbl.len: u64); + os.write(2, "'\n".ptr, 2u64); + a.errs += 1; + f = f.fnext; + continue; + }; + let target: u64 = resolve_label(a, f.label); + let rel: i64 = target: i64 - (f.off: i64 + 4i64); + let rel32: u32 = rel: u32; + a.text[f.off] = (rel32 & 255u32): u8; + a.text[f.off + 1u64] = ((rel32 >> 8u32) & 255u32): u8; + a.text[f.off + 2u64] = ((rel32 >> 16u32) & 255u32): u8; + a.text[f.off + 3u64] = ((rel32 >> 24u32) & 255u32): u8; + f = f.fnext; + }; + return a.errs; +}; + +// selfhost/cmd/6a/obj.ww — port of cmd/6a/obj.c. +// +// Emit a tiny ELF64 relocatable object. Layout (in file order): +// [0] ELF header +// [1] Section .text (program bytes) +// [2] Section .rela.text (relocations) +// [3] Section .symtab +// [4] Section .strtab +// [5] Section .shstrtab +// [6] Section header table +// +// Symtab indices: 0 = STN_UNDEF, 1.. = our syms. Only GLOBAL symbols. + +use os; +use mem; +use types; + +// ---- ELF constants ---------------------------------------------------- +def ELFCLASS64: u8 = 2u8; +def ELFDATA2LSB: u8 = 1u8; +def EV_CURRENT_W: u32 = 1u32; +def ET_REL_W: u16 = 1u16; +def EM_X86_64_W: u16 = 62u16; + +def SHT_NULL_C: u32 = 0u32; +def SHT_PROGBITS_C: u32 = 1u32; +def SHT_SYMTAB_C: u32 = 2u32; +def SHT_STRTAB_C: u32 = 3u32; +def SHT_RELA_C: u32 = 4u32; + +def SHF_ALLOC: u64 = 2u64; +def SHF_EXECINSTR: u64 = 4u64; +def SHF_INFO_LINK: u64 = 64u64; // 0x40 + +def STB_GLOBAL: u8 = 1u8; +def STT_NOTYPE: u8 = 0u8; +def STT_FUNC: u8 = 2u8; + +// Sizes of fixed structures. +def EHDR_SZ: u64 = 64u64; +def SHDR_SZ: u64 = 64u64; +def SYM_SZ: u64 = 24u64; +def RELA_SZ: u64 = 24u64; + +// ---- LE byte writers (own the bytes — write into a *u8 + offset) ---- + +fn wr_u8(p: *u8, off: u64, v: u8) void = { p[off] = v; }; +fn wr_u16(p: *u8, off: u64, v: u16) void = { + p[off] = (v & 255u16): u8; + p[off + 1u64] = ((v >> 8u16) & 255u16): u8; +}; +fn wr_u32(p: *u8, off: u64, v: u32) void = { + p[off] = (v & 255u32): u8; + p[off + 1u64] = ((v >> 8u32) & 255u32): u8; + p[off + 2u64] = ((v >> 16u32) & 255u32): u8; + p[off + 3u64] = ((v >> 24u32) & 255u32): u8; +}; +fn wr_u64(p: *u8, off: u64, v: u64) void = { + wr_u32(p, off, (v & 4294967295u64): u32); + wr_u32(p, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); +}; + +// ---- growable byte buffer --------------------------------------------- + +type buf = struct { + a: *arena, + p: *u8, + n: u64, + cap: u64, +}; + +fn buf_init(b: *buf, a: *arena) void = { + b.a = a; + b.cap = 256u64; + b.n = 0u64; + b.p = amalloc(a, b.cap): *u8; +}; + +fn buf_grow(b: *buf, need: u64) void = { + if (b.n + need <= b.cap) { return; }; + let nc: u64 = b.cap; + for (nc < b.n + need) { nc = nc * 2u64; }; + let np: *u8 = amalloc(b.a, nc): *u8; + let i: u64 = 0u64; + for (i < b.n) { np[i] = b.p[i]; i += 1u64; }; + b.p = np; + b.cap = nc; +}; + +fn buf_putb(b: *buf, src: *u8, n: u64) void = { + buf_grow(b, n); + let i: u64 = 0u64; + for (i < n) { b.p[b.n + i] = src[i]; i += 1u64; }; + b.n += n; +}; + +// Write a NUL-terminated C-string copy of `s` into b. Returns offset +// where it started (suitable for st_name / sh_name fields). +fn buf_put_cstr(b: *buf, s: str) u32 = { + let off: u32 = b.n: u32; + buf_grow(b, s.len: u64 + 1u64); + let i: i32 = 0; + for (i < s.len) { b.p[b.n] = s[i]; b.n += 1u64; i += 1; }; + b.p[b.n] = 0u8; + b.n += 1u64; + return off; +}; + +// ---- emit_elf --------------------------------------------------------- + +export fn a_emit_elf(a: *asm_, fd: i32) i32 = { + let shstr: buf; buf_init(&shstr, a.a); + let str_: buf; buf_init(&str_, a.a); + let sym: buf; buf_init(&sym, a.a); + let rela: buf; buf_init(&rela, a.a); + + // Index 0 = empty. + let zero: u8 = 0u8; + buf_putb(&shstr, &zero, 1u64); + buf_putb(&str_, &zero, 1u64); + + // Section name offsets. + let shn_text: u32 = buf_put_cstr(&shstr, ".text"); + let shn_rela: u32 = buf_put_cstr(&shstr, ".rela.text"); + let shn_symtab: u32 = buf_put_cstr(&shstr, ".symtab"); + let shn_strtab: u32 = buf_put_cstr(&shstr, ".strtab"); + let shn_shstrtab: u32 = buf_put_cstr(&shstr, ".shstrtab"); + + // Symbol 0 — STN_UNDEF (24 zero bytes). + let zsym: [24]u8; + let zi: i32 = 0; + for (zi < 24) { zsym[zi] = 0u8; zi += 1; }; + buf_putb(&sym, zsym.ptr, 24u64); + + let SH_TEXT: u16 = 1u16; + + // Build symbols. + let idx: i32 = 1; + let s: *asym = a.syms; + for (s != nil) { + let entry: [24]u8; + let ei: i32 = 0; + for (ei < 24) { entry[ei] = 0u8; ei += 1; }; + let st_name: u32 = buf_put_cstr(&str_, s.name); + wr_u32(entry.ptr, 0u64, st_name); + if (s.defined != 0) { + wr_u8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_FUNC)); + wr_u16(entry.ptr, 6u64, SH_TEXT); + wr_u64(entry.ptr, 8u64, s.addr); + } else { + wr_u8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_NOTYPE)); + wr_u16(entry.ptr, 6u64, 0u16); + }; + buf_putb(&sym, entry.ptr, 24u64); + s.idx = idx; + idx += 1; + s = s.snext; + }; + + // Build relocations. + let r: *areloc = a.relocs; + for (r != nil) { + let entry: [24]u8; + wr_u64(entry.ptr, 0u64, r.off); + let r_info: u64 = (r.asy.idx: u64 << 32u64) | (r.kind: u64 & 4294967295u64); + wr_u64(entry.ptr, 8u64, r_info); + wr_u64(entry.ptr, 16u64, r.addend: u64); + buf_putb(&rela, entry.ptr, 24u64); + r = r.rnext; + }; + + // File offsets. + let off: u64 = EHDR_SZ; + let off_text: u64 = off; off = off + a.textlen; + let off_rela: u64 = off; off = off + rela.n; + let off_sym: u64 = off; off = off + sym.n; + let off_str: u64 = off; off = off + str_.n; + let off_shstr: u64 = off; off = off + shstr.n; + for ((off & 7u64) != 0u64) { off += 1u64; }; + let off_shdr: u64 = off; + let NSECT: u16 = 6u16; + + // ---- Ehdr ---- + let eh: [64]u8; + let i: i32 = 0; + for (i < 64) { eh[i] = 0u8; i += 1; }; + eh[0] = 127u8; // 0x7f + eh[1] = 69u8; // 'E' + eh[2] = 76u8; // 'L' + eh[3] = 70u8; // 'F' + eh[4] = ELFCLASS64; + eh[5] = ELFDATA2LSB; + eh[6] = EV_CURRENT_W: u8; + wr_u16(eh.ptr, 16u64, ET_REL_W); + wr_u16(eh.ptr, 18u64, EM_X86_64_W); + wr_u32(eh.ptr, 20u64, EV_CURRENT_W); + wr_u64(eh.ptr, 24u64, 0u64); // e_entry + wr_u64(eh.ptr, 32u64, 0u64); // e_phoff + wr_u64(eh.ptr, 40u64, off_shdr); // e_shoff + wr_u32(eh.ptr, 48u64, 0u32); // e_flags + wr_u16(eh.ptr, 52u64, 64u16); // e_ehsize + wr_u16(eh.ptr, 54u64, 0u16); // e_phentsize + wr_u16(eh.ptr, 56u64, 0u16); // e_phnum + wr_u16(eh.ptr, 58u64, 64u16); // e_shentsize + wr_u16(eh.ptr, 60u64, NSECT); // e_shnum + wr_u16(eh.ptr, 62u64, 5u16); // e_shstrndx + + if (os.writefull(fd, eh.ptr, 64u64) != 64i64) { return -1; }; + if (a.textlen > 0u64) { + if (os.writefull(fd, a.text, a.textlen) != a.textlen: i64) { return -1; }; + }; + if (rela.n > 0u64) { + if (os.writefull(fd, rela.p, rela.n) != rela.n: i64) { return -1; }; + }; + if (sym.n > 0u64) { + if (os.writefull(fd, sym.p, sym.n) != sym.n: i64) { return -1; }; + }; + if (str_.n > 0u64) { + if (os.writefull(fd, str_.p, str_.n) != str_.n: i64) { return -1; }; + }; + if (shstr.n > 0u64) { + if (os.writefull(fd, shstr.p, shstr.n) != shstr.n: i64) { return -1; }; + }; + + // Pad to 8 before shdrs. + let written: u64 = EHDR_SZ + a.textlen + rela.n + sym.n + str_.n + shstr.n; + for ((written & 7u64) != 0u64) { + os.writefull(fd, &zero, 1u64); + written += 1u64; + }; + + // Section header table — 6 headers of 64 bytes each = 384 bytes. + let shbuf: [64]u8; + // SHT_NULL + let sn: i32 = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + os.writefull(fd, shbuf.ptr, 64u64); + // .text + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_text); + wr_u32(shbuf.ptr, 4u64, SHT_PROGBITS_C); + wr_u64(shbuf.ptr, 8u64, SHF_ALLOC | SHF_EXECINSTR); + wr_u64(shbuf.ptr, 24u64, off_text); + wr_u64(shbuf.ptr, 32u64, a.textlen); + wr_u64(shbuf.ptr, 48u64, 1u64); // sh_addralign + os.writefull(fd, shbuf.ptr, 64u64); + // .rela.text + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_rela); + wr_u32(shbuf.ptr, 4u64, SHT_RELA_C); + wr_u64(shbuf.ptr, 8u64, SHF_INFO_LINK); + wr_u64(shbuf.ptr, 24u64, off_rela); + wr_u64(shbuf.ptr, 32u64, rela.n); + wr_u32(shbuf.ptr, 40u64, 3u32); // sh_link = symtab idx + wr_u32(shbuf.ptr, 44u64, 1u32); // sh_info = .text idx + wr_u64(shbuf.ptr, 48u64, 8u64); + wr_u64(shbuf.ptr, 56u64, RELA_SZ); + os.writefull(fd, shbuf.ptr, 64u64); + // .symtab + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_symtab); + wr_u32(shbuf.ptr, 4u64, SHT_SYMTAB_C); + wr_u64(shbuf.ptr, 24u64, off_sym); + wr_u64(shbuf.ptr, 32u64, sym.n); + wr_u32(shbuf.ptr, 40u64, 4u32); // sh_link = strtab + wr_u32(shbuf.ptr, 44u64, 1u32); // sh_info = one local (STN_UNDEF) + wr_u64(shbuf.ptr, 48u64, 8u64); + wr_u64(shbuf.ptr, 56u64, SYM_SZ); + os.writefull(fd, shbuf.ptr, 64u64); + // .strtab + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_strtab); + wr_u32(shbuf.ptr, 4u64, SHT_STRTAB_C); + wr_u64(shbuf.ptr, 24u64, off_str); + wr_u64(shbuf.ptr, 32u64, str_.n); + wr_u64(shbuf.ptr, 48u64, 1u64); + os.writefull(fd, shbuf.ptr, 64u64); + // .shstrtab + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_shstrtab); + wr_u32(shbuf.ptr, 4u64, SHT_STRTAB_C); + wr_u64(shbuf.ptr, 24u64, off_shstr); + wr_u64(shbuf.ptr, 32u64, shstr.n); + wr_u64(shbuf.ptr, 48u64, 1u64); + os.writefull(fd, shbuf.ptr, 64u64); + + return 0; +}; + +// selfhost/cmd/6a/main.ww — port of cmd/6a/main.c. +// +// 6a = amd64 assembler. Read .s, parse, encode, emit ELF .o. +// +// 6a_ww -o file.o file.s + +use os; +use mem; +use types; +use lex; +use parse; +use asm; +use obj; + +fn streq_cs(a: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (a[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (a[i] != 0u8) { return false; }; + return true; +}; + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +// Slurp the whole file into a fresh buffer. +fn slurp(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let nz: u64 = n: u64; + let buf: *u8 = os.alloc(nz + 1u64): *u8; + let got: i64 = os.readfull(fd, buf, nz); + os.close(fd); + if (got != n) { return nil, 0u64; }; + buf[nz] = 0u8; + return buf, nz; +}; + +export fn main(argc: i32, argv: **u8) i32 = { + let src_cs: *u8 = nil; + let out_cs: *u8 = nil; + + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_cs(a, "-o")) { + i += 1; + if (i >= argc) { + os.write(2, "6a: -o requires arg\n".ptr, 20u64); + return 2; + }; + out_cs = argv[i]; + } else { if (a[0u64] == 45u8) { + os.write(2, "6a: unknown flag\n".ptr, 17u64); + return 2; + } else { + if (src_cs != nil) { + os.write(2, "6a: only one input\n".ptr, 19u64); + return 2; + }; + src_cs = a; + }; }; + i += 1; + }; + + if (src_cs == nil) { + os.write(2, "usage: 6a_ww -o file.o file.s\n".ptr, 30u64); + return 2; + }; + if (out_cs == nil) { + os.write(2, "6a: missing -o\n".ptr, 15u64); + return 2; + }; + + let buf: *u8; + let blen: u64; + buf, blen = slurp(src_cs); + if (buf == nil) { + os.write(2, "6a: cannot read input\n".ptr, 22u64); + return 1; + }; + + let ar: *arena = newarena(); + let asm: asm_; + let nlen: u64 = cstrlen(src_cs); + let fname: str = astrndup(ar, src_cs, nlen); + a_init(&asm, ar, fname, buf, blen); + + if (a_parse(&asm) != 0) { return 1; }; + if (a_encode(&asm) != 0) { return 1; }; + + // Open output for write. + let fd: i32 = os.open(out_cs, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 420i32); // 0o644 + if (fd < 0) { + os.write(2, "6a: cannot open output\n".ptr, 23u64); + return 1; + }; + let rc: i32 = a_emit_elf(&asm, fd); + os.close(fd); + return rc; +}; + diff --git a/selfhost/cmd/6a/main.ww b/selfhost/cmd/6a/main.ww new file mode 100644 index 00000000..cd7a9d1e --- /dev/null +++ b/selfhost/cmd/6a/main.ww @@ -0,0 +1,110 @@ +// selfhost/cmd/6a/main.ww — port of cmd/6a/main.c. +// +// 6a = amd64 assembler. Read .s, parse, encode, emit ELF .o. +// +// 6a_ww -o file.o file.s + +use os; +use mem; +use types; +use lex; +use parse; +use asm; +use obj; + +fn streq_cs(a: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (a[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (a[i] != 0u8) { return false; }; + return true; +}; + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +// Slurp the whole file into a fresh buffer. +fn slurp(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let nz: u64 = n: u64; + let buf: *u8 = os.alloc(nz + 1u64): *u8; + let got: i64 = os.readfull(fd, buf, nz); + os.close(fd); + if (got != n) { return nil, 0u64; }; + buf[nz] = 0u8; + return buf, nz; +}; + +export fn main(argc: i32, argv: **u8) i32 = { + let src_cs: *u8 = nil; + let out_cs: *u8 = nil; + + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_cs(a, "-o")) { + i += 1; + if (i >= argc) { + os.write(2, "6a: -o requires arg\n".ptr, 20u64); + return 2; + }; + out_cs = argv[i]; + } else { if (a[0u64] == 45u8) { + os.write(2, "6a: unknown flag\n".ptr, 17u64); + return 2; + } else { + if (src_cs != nil) { + os.write(2, "6a: only one input\n".ptr, 19u64); + return 2; + }; + src_cs = a; + }; }; + i += 1; + }; + + if (src_cs == nil) { + os.write(2, "usage: 6a_ww -o file.o file.s\n".ptr, 30u64); + return 2; + }; + if (out_cs == nil) { + os.write(2, "6a: missing -o\n".ptr, 15u64); + return 2; + }; + + let buf: *u8; + let blen: u64; + buf, blen = slurp(src_cs); + if (buf == nil) { + os.write(2, "6a: cannot read input\n".ptr, 22u64); + return 1; + }; + + let ar: *arena = newarena(); + let asm: asm_; + let nlen: u64 = cstrlen(src_cs); + let fname: str = astrndup(ar, src_cs, nlen); + a_init(&asm, ar, fname, buf, blen); + + if (a_parse(&asm) != 0) { return 1; }; + if (a_encode(&asm) != 0) { return 1; }; + + // Open output for write. + let fd: i32 = os.open(out_cs, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 420i32); // 0o644 + if (fd < 0) { + os.write(2, "6a: cannot open output\n".ptr, 23u64); + return 1; + }; + let rc: i32 = a_emit_elf(&asm, fd); + os.close(fd); + return rc; +}; diff --git a/selfhost/cmd/6a/obj.ww b/selfhost/cmd/6a/obj.ww new file mode 100644 index 00000000..dbd740f7 --- /dev/null +++ b/selfhost/cmd/6a/obj.ww @@ -0,0 +1,293 @@ +// selfhost/cmd/6a/obj.ww — port of cmd/6a/obj.c. +// +// Emit a tiny ELF64 relocatable object. Layout (in file order): +// [0] ELF header +// [1] Section .text (program bytes) +// [2] Section .rela.text (relocations) +// [3] Section .symtab +// [4] Section .strtab +// [5] Section .shstrtab +// [6] Section header table +// +// Symtab indices: 0 = STN_UNDEF, 1.. = our syms. Only GLOBAL symbols. + +use os; +use mem; +use types; + +// ---- ELF constants ---------------------------------------------------- +def ELFCLASS64: u8 = 2u8; +def ELFDATA2LSB: u8 = 1u8; +def EV_CURRENT_W: u32 = 1u32; +def ET_REL_W: u16 = 1u16; +def EM_X86_64_W: u16 = 62u16; + +def SHT_NULL_C: u32 = 0u32; +def SHT_PROGBITS_C: u32 = 1u32; +def SHT_SYMTAB_C: u32 = 2u32; +def SHT_STRTAB_C: u32 = 3u32; +def SHT_RELA_C: u32 = 4u32; + +def SHF_ALLOC: u64 = 2u64; +def SHF_EXECINSTR: u64 = 4u64; +def SHF_INFO_LINK: u64 = 64u64; // 0x40 + +def STB_GLOBAL: u8 = 1u8; +def STT_NOTYPE: u8 = 0u8; +def STT_FUNC: u8 = 2u8; + +// Sizes of fixed structures. +def EHDR_SZ: u64 = 64u64; +def SHDR_SZ: u64 = 64u64; +def SYM_SZ: u64 = 24u64; +def RELA_SZ: u64 = 24u64; + +// ---- LE byte writers (own the bytes — write into a *u8 + offset) ---- + +fn wr_u8(p: *u8, off: u64, v: u8) void = { p[off] = v; }; +fn wr_u16(p: *u8, off: u64, v: u16) void = { + p[off] = (v & 255u16): u8; + p[off + 1u64] = ((v >> 8u16) & 255u16): u8; +}; +fn wr_u32(p: *u8, off: u64, v: u32) void = { + p[off] = (v & 255u32): u8; + p[off + 1u64] = ((v >> 8u32) & 255u32): u8; + p[off + 2u64] = ((v >> 16u32) & 255u32): u8; + p[off + 3u64] = ((v >> 24u32) & 255u32): u8; +}; +fn wr_u64(p: *u8, off: u64, v: u64) void = { + wr_u32(p, off, (v & 4294967295u64): u32); + wr_u32(p, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); +}; + +// ---- growable byte buffer --------------------------------------------- + +type buf = struct { + a: *arena, + p: *u8, + n: u64, + cap: u64, +}; + +fn buf_init(b: *buf, a: *arena) void = { + b.a = a; + b.cap = 256u64; + b.n = 0u64; + b.p = amalloc(a, b.cap): *u8; +}; + +fn buf_grow(b: *buf, need: u64) void = { + if (b.n + need <= b.cap) { return; }; + let nc: u64 = b.cap; + for (nc < b.n + need) { nc = nc * 2u64; }; + let np: *u8 = amalloc(b.a, nc): *u8; + let i: u64 = 0u64; + for (i < b.n) { np[i] = b.p[i]; i += 1u64; }; + b.p = np; + b.cap = nc; +}; + +fn buf_putb(b: *buf, src: *u8, n: u64) void = { + buf_grow(b, n); + let i: u64 = 0u64; + for (i < n) { b.p[b.n + i] = src[i]; i += 1u64; }; + b.n += n; +}; + +// Write a NUL-terminated C-string copy of `s` into b. Returns offset +// where it started (suitable for st_name / sh_name fields). +fn buf_put_cstr(b: *buf, s: str) u32 = { + let off: u32 = b.n: u32; + buf_grow(b, s.len: u64 + 1u64); + let i: i32 = 0; + for (i < s.len) { b.p[b.n] = s[i]; b.n += 1u64; i += 1; }; + b.p[b.n] = 0u8; + b.n += 1u64; + return off; +}; + +// ---- emit_elf --------------------------------------------------------- + +export fn a_emit_elf(a: *asm_, fd: i32) i32 = { + let shstr: buf; buf_init(&shstr, a.a); + let str_: buf; buf_init(&str_, a.a); + let sym: buf; buf_init(&sym, a.a); + let rela: buf; buf_init(&rela, a.a); + + // Index 0 = empty. + let zero: u8 = 0u8; + buf_putb(&shstr, &zero, 1u64); + buf_putb(&str_, &zero, 1u64); + + // Section name offsets. + let shn_text: u32 = buf_put_cstr(&shstr, ".text"); + let shn_rela: u32 = buf_put_cstr(&shstr, ".rela.text"); + let shn_symtab: u32 = buf_put_cstr(&shstr, ".symtab"); + let shn_strtab: u32 = buf_put_cstr(&shstr, ".strtab"); + let shn_shstrtab: u32 = buf_put_cstr(&shstr, ".shstrtab"); + + // Symbol 0 — STN_UNDEF (24 zero bytes). + let zsym: [24]u8; + let zi: i32 = 0; + for (zi < 24) { zsym[zi] = 0u8; zi += 1; }; + buf_putb(&sym, zsym.ptr, 24u64); + + let SH_TEXT: u16 = 1u16; + + // Build symbols. + let idx: i32 = 1; + let s: *asym = a.syms; + for (s != nil) { + let entry: [24]u8; + let ei: i32 = 0; + for (ei < 24) { entry[ei] = 0u8; ei += 1; }; + let st_name: u32 = buf_put_cstr(&str_, s.name); + wr_u32(entry.ptr, 0u64, st_name); + if (s.defined != 0) { + wr_u8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_FUNC)); + wr_u16(entry.ptr, 6u64, SH_TEXT); + wr_u64(entry.ptr, 8u64, s.addr); + } else { + wr_u8(entry.ptr, 4u64, ((STB_GLOBAL << 4u8) | STT_NOTYPE)); + wr_u16(entry.ptr, 6u64, 0u16); + }; + buf_putb(&sym, entry.ptr, 24u64); + s.idx = idx; + idx += 1; + s = s.snext; + }; + + // Build relocations. + let r: *areloc = a.relocs; + for (r != nil) { + let entry: [24]u8; + wr_u64(entry.ptr, 0u64, r.off); + let r_info: u64 = (r.asy.idx: u64 << 32u64) | (r.kind: u64 & 4294967295u64); + wr_u64(entry.ptr, 8u64, r_info); + wr_u64(entry.ptr, 16u64, r.addend: u64); + buf_putb(&rela, entry.ptr, 24u64); + r = r.rnext; + }; + + // File offsets. + let off: u64 = EHDR_SZ; + let off_text: u64 = off; off = off + a.textlen; + let off_rela: u64 = off; off = off + rela.n; + let off_sym: u64 = off; off = off + sym.n; + let off_str: u64 = off; off = off + str_.n; + let off_shstr: u64 = off; off = off + shstr.n; + for ((off & 7u64) != 0u64) { off += 1u64; }; + let off_shdr: u64 = off; + let NSECT: u16 = 6u16; + + // ---- Ehdr ---- + let eh: [64]u8; + let i: i32 = 0; + for (i < 64) { eh[i] = 0u8; i += 1; }; + eh[0] = 127u8; // 0x7f + eh[1] = 69u8; // 'E' + eh[2] = 76u8; // 'L' + eh[3] = 70u8; // 'F' + eh[4] = ELFCLASS64; + eh[5] = ELFDATA2LSB; + eh[6] = EV_CURRENT_W: u8; + wr_u16(eh.ptr, 16u64, ET_REL_W); + wr_u16(eh.ptr, 18u64, EM_X86_64_W); + wr_u32(eh.ptr, 20u64, EV_CURRENT_W); + wr_u64(eh.ptr, 24u64, 0u64); // e_entry + wr_u64(eh.ptr, 32u64, 0u64); // e_phoff + wr_u64(eh.ptr, 40u64, off_shdr); // e_shoff + wr_u32(eh.ptr, 48u64, 0u32); // e_flags + wr_u16(eh.ptr, 52u64, 64u16); // e_ehsize + wr_u16(eh.ptr, 54u64, 0u16); // e_phentsize + wr_u16(eh.ptr, 56u64, 0u16); // e_phnum + wr_u16(eh.ptr, 58u64, 64u16); // e_shentsize + wr_u16(eh.ptr, 60u64, NSECT); // e_shnum + wr_u16(eh.ptr, 62u64, 5u16); // e_shstrndx + + if (os.writefull(fd, eh.ptr, 64u64) != 64i64) { return -1; }; + if (a.textlen > 0u64) { + if (os.writefull(fd, a.text, a.textlen) != a.textlen: i64) { return -1; }; + }; + if (rela.n > 0u64) { + if (os.writefull(fd, rela.p, rela.n) != rela.n: i64) { return -1; }; + }; + if (sym.n > 0u64) { + if (os.writefull(fd, sym.p, sym.n) != sym.n: i64) { return -1; }; + }; + if (str_.n > 0u64) { + if (os.writefull(fd, str_.p, str_.n) != str_.n: i64) { return -1; }; + }; + if (shstr.n > 0u64) { + if (os.writefull(fd, shstr.p, shstr.n) != shstr.n: i64) { return -1; }; + }; + + // Pad to 8 before shdrs. + let written: u64 = EHDR_SZ + a.textlen + rela.n + sym.n + str_.n + shstr.n; + for ((written & 7u64) != 0u64) { + os.writefull(fd, &zero, 1u64); + written += 1u64; + }; + + // Section header table — 6 headers of 64 bytes each = 384 bytes. + let shbuf: [64]u8; + // SHT_NULL + let sn: i32 = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + os.writefull(fd, shbuf.ptr, 64u64); + // .text + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_text); + wr_u32(shbuf.ptr, 4u64, SHT_PROGBITS_C); + wr_u64(shbuf.ptr, 8u64, SHF_ALLOC | SHF_EXECINSTR); + wr_u64(shbuf.ptr, 24u64, off_text); + wr_u64(shbuf.ptr, 32u64, a.textlen); + wr_u64(shbuf.ptr, 48u64, 1u64); // sh_addralign + os.writefull(fd, shbuf.ptr, 64u64); + // .rela.text + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_rela); + wr_u32(shbuf.ptr, 4u64, SHT_RELA_C); + wr_u64(shbuf.ptr, 8u64, SHF_INFO_LINK); + wr_u64(shbuf.ptr, 24u64, off_rela); + wr_u64(shbuf.ptr, 32u64, rela.n); + wr_u32(shbuf.ptr, 40u64, 3u32); // sh_link = symtab idx + wr_u32(shbuf.ptr, 44u64, 1u32); // sh_info = .text idx + wr_u64(shbuf.ptr, 48u64, 8u64); + wr_u64(shbuf.ptr, 56u64, RELA_SZ); + os.writefull(fd, shbuf.ptr, 64u64); + // .symtab + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_symtab); + wr_u32(shbuf.ptr, 4u64, SHT_SYMTAB_C); + wr_u64(shbuf.ptr, 24u64, off_sym); + wr_u64(shbuf.ptr, 32u64, sym.n); + wr_u32(shbuf.ptr, 40u64, 4u32); // sh_link = strtab + wr_u32(shbuf.ptr, 44u64, 1u32); // sh_info = one local (STN_UNDEF) + wr_u64(shbuf.ptr, 48u64, 8u64); + wr_u64(shbuf.ptr, 56u64, SYM_SZ); + os.writefull(fd, shbuf.ptr, 64u64); + // .strtab + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_strtab); + wr_u32(shbuf.ptr, 4u64, SHT_STRTAB_C); + wr_u64(shbuf.ptr, 24u64, off_str); + wr_u64(shbuf.ptr, 32u64, str_.n); + wr_u64(shbuf.ptr, 48u64, 1u64); + os.writefull(fd, shbuf.ptr, 64u64); + // .shstrtab + sn = 0; + for (sn < 64) { shbuf[sn] = 0u8; sn += 1; }; + wr_u32(shbuf.ptr, 0u64, shn_shstrtab); + wr_u32(shbuf.ptr, 4u64, SHT_STRTAB_C); + wr_u64(shbuf.ptr, 24u64, off_shstr); + wr_u64(shbuf.ptr, 32u64, shstr.n); + wr_u64(shbuf.ptr, 48u64, 1u64); + os.writefull(fd, shbuf.ptr, 64u64); + + return 0; +}; diff --git a/selfhost/cmd/6a/parse.ww b/selfhost/cmd/6a/parse.ww new file mode 100644 index 00000000..3a551fb0 --- /dev/null +++ b/selfhost/cmd/6a/parse.ww @@ -0,0 +1,583 @@ +// selfhost/cmd/6a/parse.ww — port of cmd/6a/parse.c. +// +// Line-oriented parser for the asm subset emitted by 6c. +// Grammar: +// line := blank | comment | label | text | instr +// blank := /^\s*$/ +// comment := /^\s*\/\/.*$/ +// label := /^IDENT:$/ +// text := TEXT name,$framesize +// instr := \tMNEM\t[OP1[, OP2]] +// OP := $NUM | REG | NUM(REG) | (REG) | name(SB) | label + +use os; +use mem; +use lex; +use types; + +fn streq_lit(p: *u8, n: u64, lit: str) bool = { + if (n != lit.len: u64) { return false; }; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (p[i] != lit[li]) { return false; }; + i += 1u64; + }; + return true; +}; + +// opcode_lookup — name (length-bounded *u8) → A_*. Returns 0 (A_NOP) +// if not found. +fn opcode_lookup(p: *u8, n: u64) i32 = { + if (streq_lit(p, n, "MOVQ")) { return A_MOVQ; }; + if (streq_lit(p, n, "MOVL")) { return A_MOVL; }; + if (streq_lit(p, n, "MOVB")) { return A_MOVB; }; + if (streq_lit(p, n, "MOVZBQ")) { return A_MOVZBQ; }; + if (streq_lit(p, n, "MOVSXD")) { return A_MOVSXD; }; + if (streq_lit(p, n, "MOVSD")) { return A_MOVSD; }; + if (streq_lit(p, n, "ADDSD")) { return A_ADDSD; }; + if (streq_lit(p, n, "SUBSD")) { return A_SUBSD; }; + if (streq_lit(p, n, "MULSD")) { return A_MULSD; }; + if (streq_lit(p, n, "DIVSD")) { return A_DIVSD; }; + if (streq_lit(p, n, "UCOMISD")) { return A_UCOMISD; }; + if (streq_lit(p, n, "CVTTSD2SI")) { return A_CVTTSD2SI; }; + if (streq_lit(p, n, "CVTSI2SD")) { return A_CVTSI2SD; }; + if (streq_lit(p, n, "MOVSS")) { return A_MOVSS; }; + if (streq_lit(p, n, "ADDSS")) { return A_ADDSS; }; + if (streq_lit(p, n, "SUBSS")) { return A_SUBSS; }; + if (streq_lit(p, n, "MULSS")) { return A_MULSS; }; + if (streq_lit(p, n, "DIVSS")) { return A_DIVSS; }; + if (streq_lit(p, n, "UCOMISS")) { return A_UCOMISS; }; + if (streq_lit(p, n, "CVTTSS2SI")) { return A_CVTTSS2SI; }; + if (streq_lit(p, n, "CVTSI2SS")) { return A_CVTSI2SS; }; + if (streq_lit(p, n, "CVTSD2SS")) { return A_CVTSD2SS; }; + if (streq_lit(p, n, "CVTSS2SD")) { return A_CVTSS2SD; }; + if (streq_lit(p, n, "ADDQ")) { return A_ADDQ; }; + if (streq_lit(p, n, "SUBQ")) { return A_SUBQ; }; + if (streq_lit(p, n, "IMULQ")) { return A_IMULQ; }; + if (streq_lit(p, n, "IDIVQ")) { return A_IDIVQ; }; + if (streq_lit(p, n, "DIVQ")) { return A_DIVQ; }; + if (streq_lit(p, n, "NEGQ")) { return A_NEGQ; }; + if (streq_lit(p, n, "NOTQ")) { return A_NOTQ; }; + if (streq_lit(p, n, "ANDQ")) { return A_ANDQ; }; + if (streq_lit(p, n, "ORQ")) { return A_ORQ; }; + if (streq_lit(p, n, "XORQ")) { return A_XORQ; }; + if (streq_lit(p, n, "SHLQ")) { return A_SHLQ; }; + if (streq_lit(p, n, "SHRQ")) { return A_SHRQ; }; + if (streq_lit(p, n, "CMPQ")) { return A_CMPQ; }; + if (streq_lit(p, n, "PUSHQ")) { return A_PUSHQ; }; + if (streq_lit(p, n, "POPQ")) { return A_POPQ; }; + if (streq_lit(p, n, "LEAQ")) { return A_LEAQ; }; + if (streq_lit(p, n, "CALL")) { return A_CALL; }; + if (streq_lit(p, n, "RET")) { return A_RET; }; + if (streq_lit(p, n, "JMP")) { return A_JMP; }; + if (streq_lit(p, n, "JE")) { return A_JE; }; + if (streq_lit(p, n, "JNE")) { return A_JNE; }; + if (streq_lit(p, n, "JL")) { return A_JL; }; + if (streq_lit(p, n, "JLE")) { return A_JLE; }; + if (streq_lit(p, n, "JG")) { return A_JG; }; + if (streq_lit(p, n, "JGE")) { return A_JGE; }; + if (streq_lit(p, n, "JB")) { return A_JB; }; + if (streq_lit(p, n, "JBE")) { return A_JBE; }; + if (streq_lit(p, n, "JA")) { return A_JA; }; + if (streq_lit(p, n, "JAE")) { return A_JAE; }; + if (streq_lit(p, n, "JZ")) { return A_JZ; }; + if (streq_lit(p, n, "JNZ")) { return A_JNZ; }; + if (streq_lit(p, n, "SYSCALL")) { return A_SYSCALL; }; + if (streq_lit(p, n, "TEXT")) { return A_TEXT; }; + if (streq_lit(p, n, "DATA")) { return A_DATA; }; + return A_NOP; +}; + +// reg_lookup — name → D_*. Returns D_NONE if not found. +fn reg_lookup(p: *u8, n: u64) i32 = { + if (streq_lit(p, n, "AX")) { return D_AX; }; + if (streq_lit(p, n, "BX")) { return D_BX; }; + if (streq_lit(p, n, "CX")) { return D_CX; }; + if (streq_lit(p, n, "DX")) { return D_DX; }; + if (streq_lit(p, n, "SP")) { return D_SP; }; + if (streq_lit(p, n, "BP")) { return D_BP; }; + if (streq_lit(p, n, "SI")) { return D_SI; }; + if (streq_lit(p, n, "DI")) { return D_DI; }; + if (streq_lit(p, n, "R8")) { return D_R8; }; + if (streq_lit(p, n, "R9")) { return D_R9; }; + if (streq_lit(p, n, "R10")) { return D_R10; }; + if (streq_lit(p, n, "R11")) { return D_R11; }; + if (streq_lit(p, n, "R12")) { return D_R12; }; + if (streq_lit(p, n, "R13")) { return D_R13; }; + if (streq_lit(p, n, "R14")) { return D_R14; }; + if (streq_lit(p, n, "R15")) { return D_R15; }; + if (streq_lit(p, n, "X0")) { return D_X0; }; + if (streq_lit(p, n, "X1")) { return D_X1; }; + if (streq_lit(p, n, "X2")) { return D_X2; }; + if (streq_lit(p, n, "X3")) { return D_X3; }; + if (streq_lit(p, n, "X4")) { return D_X4; }; + if (streq_lit(p, n, "X5")) { return D_X5; }; + if (streq_lit(p, n, "X6")) { return D_X6; }; + if (streq_lit(p, n, "X7")) { return D_X7; }; + if (streq_lit(p, n, "X8")) { return D_X8; }; + if (streq_lit(p, n, "X9")) { return D_X9; }; + if (streq_lit(p, n, "X10")) { return D_X10; }; + if (streq_lit(p, n, "X11")) { return D_X11; }; + if (streq_lit(p, n, "X12")) { return D_X12; }; + if (streq_lit(p, n, "X13")) { return D_X13; }; + if (streq_lit(p, n, "X14")) { return D_X14; }; + if (streq_lit(p, n, "X15")) { return D_X15; }; + if (streq_lit(p, n, "SB")) { return D_PSB; }; + if (streq_lit(p, n, "FP")) { return D_PFP; }; + return D_NONE; +}; + +export fn a_init(a: *asm_, ar: *arena, file: str, src: *u8, len: u64) void = { + a.a = ar; + a.file = file; + a.src = src; + a.srclen = len; + a.pos = 0u64; + a.line = 1; + a.head = nil; + a.tail = nil; + a.text = nil; + a.textcap = 0u64; + a.textlen = 0u64; + a.syms = nil; + a.relocs = nil; + a.fixups = nil; + a.errs = 0; +}; + +fn streq_str(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; +}; + +export fn a_intern(a: *asm_, name: str) *asym = { + let s: *asym = a.syms; + for (s != nil) { + if (streq_str(s.name, name)) { return s; }; + s = s.snext; + }; + let n: *asym = amalloc(a.a, 64u64): *asym; + n.name = name; + n.snext = a.syms; + a.syms = n; + return n; +}; + +fn perr(a: *asm_, msg: str) void = { + os.write(2, "6a: ".ptr, 4u64); + let f: str = a.file; + os.write(2, f.ptr, f.len: u64); + os.write(2, ": ".ptr, 2u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + a.errs += 1; +}; + +// dup_str — copy n bytes from p into a fresh heap str. +fn dup_str(a: *arena, p: *u8, n: u64) str = { + return astrndup(a, p, n); +}; + +// ---- line iteration & whitespace -------------------------------------- + +// Read next line into a fresh heap buffer; returns (ptr, len) or (nil,0) +// at EOF. Advances a.pos past the newline. +fn next_line(a: *asm_) (*u8, u64) = { + if (a.pos >= a.srclen) { return nil, 0u64; }; + let start: u64 = a.pos; + for (a.pos < a.srclen) { + if (a.src[a.pos] == 10u8) { a.pos = a.pos; a.pos += 0u64; } // no-op; explicit break via condition + else { a.pos += 1u64; continue; }; + // hit newline + let n: u64 = a.pos - start; + let buf: *u8 = amalloc(a.a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { buf[i] = a.src[start + i]; i += 1u64; }; + buf[n] = 0u8; + a.pos += 1u64; // skip newline + return buf, n; + }; + // EOF without trailing newline + let n: u64 = a.pos - start; + if (n == 0u64) { return nil, 0u64; }; + let buf: *u8 = amalloc(a.a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { buf[i] = a.src[start + i]; i += 1u64; }; + buf[n] = 0u8; + return buf, n; +}; + +fn skip_ws(p: *u8, off: u64, n: u64) u64 = { + let i: u64 = off; + for (i < n) { + if (p[i] != 32u8) { if (p[i] != 9u8) { return i; }; }; + i += 1u64; + }; + return i; +}; + +// parse_operand — parse one operand from p[off..n), populate out. +// Returns new offset (clamped to n on error). +fn parse_operand(a: *asm_, p: *u8, off_in: u64, n: u64, out: *aoperand) u64 = { + let off: u64 = skip_ws(p, off_in, n); + out.atype = D_NONE; + out.reg = 0; + out.offset = 0i64; + let empty_str: str; + empty_str.ptr = nil; empty_str.len = 0; + out.asym = empty_str; + if (off >= n) { return off; }; + let c0: u8 = p[off]; + + // $NUM + if (c0 == 36u8) { // '$' + off += 1u64; + let v: i64; + let used: u64; + v, used = a_parsenum(p + off, n - off); + out.atype = D_CONST; + out.offset = v; + return off + used; + }; + + // (REG) + if (c0 == 40u8) { // '(' + off += 1u64; + let rstart: u64 = off; + for (off < n) { + if (p[off] == 41u8) { off = off; off += 0u64; } // no-op marker + else { off += 1u64; continue; }; + let rn: u64 = off - rstart; + let r: i32 = reg_lookup(p + rstart, rn); + if (r == 0) { perr(a, "bad register in indirect"); return n; }; + out.atype = D_INDIR; + out.reg = r; + out.offset = 0i64; + return off + 1u64; // past ')' + }; + perr(a, "missing ')' in indirect"); + return n; + }; + + // number(REG) — possibly signed — or bare $NUM-less constant + let cur: u64 = off; + let is_num: bool = false; + if (cur < n) { + if (p[cur] == 45u8) { is_num = true; } + else { if (p[cur] >= 48u8) { if (p[cur] <= 57u8) { is_num = true; }; }; }; + }; + if (is_num) { + let v: i64; + let used: u64; + v, used = a_parsenum(p + off, n - off); + let after: u64 = off + used; + if (after < n) { if (p[after] == 40u8) { // '(' + let rstart: u64 = after + 1u64; + let cur2: u64 = rstart; + for (cur2 < n) { + if (p[cur2] == 41u8) { cur2 = cur2; cur2 += 0u64; } + else { cur2 += 1u64; continue; }; + let rn: u64 = cur2 - rstart; + let r: i32 = reg_lookup(p + rstart, rn); + if (r == 0) { perr(a, "bad register"); return n; }; + out.atype = D_INDIR; + out.reg = r; + out.offset = v; + return cur2 + 1u64; + }; + perr(a, "missing ')'"); + return n; + };}; + out.atype = D_CONST; + out.offset = v; + return after; + }; + + // IDENT — register, symbol(SB), or branch label + if (a_isidstart(c0: i32)) { + let istart: u64 = off; + for (off < n) { + if (a_isidcont(p[off]: i32)) { off += 1u64; continue; }; + off = off; off += 0u64; // loop break + let in_: u64 = off - istart; + // IDENT(SB) — external + if (off < n) { if (p[off] == 40u8) { // '(' + let rstart: u64 = off + 1u64; + let cur2: u64 = rstart; + for (cur2 < n) { + if (p[cur2] == 41u8) { cur2 = cur2; cur2 += 0u64; } + else { cur2 += 1u64; continue; }; + let rn: u64 = cur2 - rstart; + let r: i32 = reg_lookup(p + rstart, rn); + if (r == D_PSB) { + out.atype = D_EXTERN; + out.asym = dup_str(a.a, p + istart, in_); + } else { + out.atype = D_INDIR; + out.reg = r; + out.offset = 0i64; + }; + return cur2 + 1u64; + }; + perr(a, "missing ')'"); + return n; + };}; + let r: i32 = reg_lookup(p + istart, in_); + if (r != D_NONE) { + out.atype = r; + return off; + }; + out.atype = D_BRANCH; + out.asym = dup_str(a.a, p + istart, in_); + return off; + }; + // EOF inside ident + let in_: u64 = off - istart; + let r: i32 = reg_lookup(p + istart, in_); + if (r != D_NONE) { out.atype = r; return off; }; + out.atype = D_BRANCH; + out.asym = dup_str(a.a, p + istart, in_); + return off; + }; + + perr(a, "unrecognised operand"); + return n; +}; + +// Append a fresh aprog to the list with given opcode and label. +fn add_prog(a: *asm_, opc: i32, lbl: str) *aprog = { + let pr: *aprog = amalloc(a.a, 96u64): *aprog; + pr.as_ = opc; + pr.line = a.line; + pr.label = lbl; + pr.link = nil; + pr.bytes = nil; + pr.nbytes = 0u64; + pr.from = amalloc(a.a, 48u64): *aoperand; + pr.to = amalloc(a.a, 48u64): *aoperand; + if (a.head == nil) { a.head = pr; } + else { + // `a.tail.link = pr` would be a chained-dot write through a + // pointer field, which the C cgen we bootstrap on doesn't + // support (silently drops the store). Bind a local first. + let tail: *aprog = a.tail; + tail.link = pr; + }; + a.tail = pr; + return pr; +}; + +export fn a_parse(a: *asm_) i32 = { + let pending: str; + pending.ptr = nil; pending.len = 0; + + for (true) { + let line: *u8; + let n: u64; + line, n = next_line(a); + if (line == nil) { return a.errs; }; + + // skip leading ws + let i: u64 = skip_ws(line, 0u64, n); + // blank or //-comment + if (i >= n) { a.line += 1; continue; }; + if (i + 1u64 < n) { + if (line[i] == 47u8) { if (line[i + 1u64] == 47u8) { + a.line += 1; continue; + };}; + }; + + // Label? IDENT: starting at column 0 (no leading tab). + // Only if the identifier is followed by ':'. Otherwise, fall + // through to mnemonic parsing so e.g. `TEXT foo,$0` (which + // also starts with an idchar in column 0) gets parsed. + if (line[0u64] != 9u8) { + if (a_isidstart(line[i]: i32)) { + let q: u64 = i; + let scan_id: bool = true; + for (scan_id) { + if (q >= n) { scan_id = false; } + else { if (a_isidcont(line[q]: i32)) { q += 1u64; } + else { scan_id = false; }; }; + }; + if (q < n) { if (line[q] == 58u8) { // ':' + let nm: str = dup_str(a.a, line + i, q - i); + // Pending label gets a NOP prog so addresses pin. + if (pending.len > 0) { + let np: *aprog = add_prog(a, A_NOP, pending); + }; + pending = nm; + a.line += 1; + continue; + };}; + // not a label — fall through to mnemonic parse + }; + }; + + // MNEMONIC at the start of the rest. Scan to first ws/EOL. + let mstart: u64 = i; + let m: u64 = mstart; + let scan: bool = true; + for (scan) { + if (m >= n) { scan = false; } + else { if (line[m] == 32u8) { scan = false; } + else { if (line[m] == 9u8) { scan = false; } + else { m += 1u64; }; }; }; + }; + let mlen: u64 = m - mstart; + let opc: i32 = opcode_lookup(line + mstart, mlen); + if (opc == 0) { + if (mlen > 0u64) { + perr(a, "unknown opcode"); + }; + pending.ptr = nil; pending.len = 0; + a.line += 1; continue; + }; + + let pr: *aprog = add_prog(a, opc, pending); + pending.ptr = nil; pending.len = 0; + + // Skip ws after mnemonic + let r0: u64 = skip_ws(line, m, n); + + if (opc == A_TEXT) { + // TEXT name,$framesize — find first ',' as the end of name. + let q: u64 = r0; + let comma_pos: u64 = n; + let scan_t: bool = true; + for (scan_t) { + if (q >= n) { scan_t = false; } + else { if (line[q] == 44u8) { comma_pos = q; scan_t = false; } + else { q += 1u64; }; }; + }; + let to_op: *aoperand = pr.to; + to_op.atype = D_EXTERN; + to_op.asym = dup_str(a.a, line + r0, comma_pos - r0); + if (comma_pos < n) { + let p2: u64 = comma_pos + 1u64; + p2 = skip_ws(line, p2, n); + if (p2 < n) { if (line[p2] == 36u8) { p2 += 1u64; }; }; + let v: i64; + let used: u64; + v, used = a_parsenum(line + p2, n - p2); + let from_op: *aoperand = pr.from; + from_op.atype = D_CONST; + from_op.offset = v; + }; + a.line += 1; continue; + }; + + if (opc == A_DATA) { + // DATA name(SB),"escaped bytes" — find first '(' as end of name. + let q: u64 = r0; + let lparen: u64 = n; + let scan_d: bool = true; + for (scan_d) { + if (q >= n) { scan_d = false; } + else { if (line[q] == 40u8) { lparen = q; scan_d = false; } + else { q += 1u64; }; }; + }; + let to_op: *aoperand = pr.to; + to_op.atype = D_EXTERN; + to_op.asym = dup_str(a.a, line + r0, lparen - r0); + // Skip past `(SB)` to land just after ')'. + let p2: u64 = lparen; + let scan_d2: bool = true; + for (scan_d2) { + if (p2 >= n) { scan_d2 = false; } + else { if (line[p2] == 41u8) { p2 += 1u64; scan_d2 = false; } + else { p2 += 1u64; }; }; + }; + // Skip ws / ',' / tab between `)` and the `"`. + let scan_d3: bool = true; + for (scan_d3) { + if (p2 >= n) { scan_d3 = false; } + else { if (line[p2] == 32u8) { p2 += 1u64; } + else { if (line[p2] == 44u8) { p2 += 1u64; } + else { if (line[p2] == 9u8) { p2 += 1u64; } + else { scan_d3 = false; }; }; }; }; + }; + if (p2 >= n) { perr(a, "DATA missing payload"); a.line += 1; continue; }; + if (line[p2] != 34u8) { perr(a, "DATA expects \"...\""); a.line += 1; continue; }; + p2 += 1u64; // past opening " + // Parse escape sequence into a fresh growable buffer. + let cap: u64 = 32u64; + let blen: u64 = 0u64; + let dbuf: *u8 = amalloc(a.a, cap): *u8; + for (p2 < n) { + if (line[p2] == 34u8) { p2 = p2; p2 += 0u64; p2 = n + 1u64; } + else { + let ch: u8 = line[p2]; + p2 += 1u64; + if (ch == 92u8) { // '\' + if (p2 < n) { + let e: u8 = line[p2]; + p2 += 1u64; + if (e == 110u8) { ch = 10u8; } // 'n' + else { if (e == 116u8) { ch = 9u8; } + else { if (e == 114u8) { ch = 13u8; } + else { if (e == 92u8) { ch = 92u8; } + else { if (e == 34u8) { ch = 34u8; } + else { if (e == 48u8) { ch = 0u8; } + else { if (e == 120u8) { // 'x' + if (p2 + 1u64 < n) { + let hi: u8 = line[p2]; + let lo: u8 = line[p2 + 1u64]; + p2 += 2u64; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi <= 57u8) { h = hi - 48u8; } + else { h = (hi | 32u8) - 97u8 + 10u8; }; + if (lo <= 57u8) { l = lo - 48u8; } + else { l = (lo | 32u8) - 97u8 + 10u8; }; + ch = (h << 4u8) | l; + }; + } + else { ch = e; };};};};};};}; + }; + }; + if (blen + 1u64 > cap) { + let ncap: u64 = cap * 2u64; + let nb: *u8 = amalloc(a.a, ncap): *u8; + let bi: u64 = 0u64; + for (bi < blen) { nb[bi] = dbuf[bi]; bi += 1u64; }; + dbuf = nb; + cap = ncap; + }; + dbuf[blen] = ch; + blen += 1u64; + }; + }; + pr.bytes = dbuf; + pr.nbytes = blen; + a.line += 1; continue; + }; + + // Generic instruction: 0/1/2 operands separated by ','. + // Find top-level comma. + let comma: i64 = -1i64; + let q: u64 = r0; + for (q < n) { + if (line[q] == 44u8) { + if (comma < 0i64) { comma = q: i64; }; + }; + q += 1u64; + }; + if (comma >= 0i64) { + let cu: u64 = comma: u64; + parse_operand(a, line, r0, cu, pr.from); + parse_operand(a, line + (cu + 1u64), 0u64, n - (cu + 1u64), pr.to); + } else { if (r0 < n) { + parse_operand(a, line, r0, n, pr.to); + };}; + + a.line += 1; + }; + return a.errs; +}; diff --git a/selfhost/cmd/6a/types.ww b/selfhost/cmd/6a/types.ww new file mode 100644 index 00000000..72797a58 --- /dev/null +++ b/selfhost/cmd/6a/types.ww @@ -0,0 +1,193 @@ +// selfhost/cmd/6a/types.ww — types + constants shared across the +// 6a port. Mirrors cmd/6a/a.h and cmd/6c/6.out.h. + +use mem; + +// ---- registers + operand kinds (from 6.out.h) ------------------------- +// These must stay numerically aligned with the C enum so that ww-cgen +// output (which reads them via `D_AX(SB)` etc.) lands on the same +// integers when read by ww-6a. +def D_NONE: i32 = 0; + +def D_AX: i32 = 1; +def D_CX: i32 = 2; +def D_DX: i32 = 3; +def D_BX: i32 = 4; +def D_SP: i32 = 5; +def D_BP: i32 = 6; +def D_SI: i32 = 7; +def D_DI: i32 = 8; +def D_R8: i32 = 9; +def D_R9: i32 = 10; +def D_R10: i32 = 11; +def D_R11: i32 = 12; +def D_R12: i32 = 13; +def D_R13: i32 = 14; +def D_R14: i32 = 15; +def D_R15: i32 = 16; + +def D_X0: i32 = 17; +def D_X1: i32 = 18; +def D_X2: i32 = 19; +def D_X3: i32 = 20; +def D_X4: i32 = 21; +def D_X5: i32 = 22; +def D_X6: i32 = 23; +def D_X7: i32 = 24; +def D_X8: i32 = 25; +def D_X9: i32 = 26; +def D_X10: i32 = 27; +def D_X11: i32 = 28; +def D_X12: i32 = 29; +def D_X13: i32 = 30; +def D_X14: i32 = 31; +def D_X15: i32 = 32; + +def D_PSP: i32 = 33; +def D_PFP: i32 = 34; +def D_PSB: i32 = 35; + +def D_CONST: i32 = 36; +def D_BRANCH: i32 = 37; +def D_EXTERN: i32 = 38; +def D_INDIR: i32 = 39; + +// ---- opcodes ---------------------------------------------------------- +def A_NOP: i32 = 0; +def A_TEXT: i32 = 1; +def A_DATA: i32 = 2; +def A_GLOBL: i32 = 3; +def A_END: i32 = 4; + +def A_MOVQ: i32 = 5; +def A_MOVL: i32 = 6; +def A_MOVB: i32 = 7; +def A_MOVZBQ: i32 = 8; +def A_MOVSXD: i32 = 9; + +def A_MOVSD: i32 = 10; +def A_ADDSD: i32 = 11; +def A_SUBSD: i32 = 12; +def A_MULSD: i32 = 13; +def A_DIVSD: i32 = 14; +def A_UCOMISD: i32 = 15; +def A_CVTTSD2SI: i32 = 16; +def A_CVTSI2SD: i32 = 17; + +def A_MOVSS: i32 = 18; +def A_ADDSS: i32 = 19; +def A_SUBSS: i32 = 20; +def A_MULSS: i32 = 21; +def A_DIVSS: i32 = 22; +def A_UCOMISS: i32 = 23; +def A_CVTTSS2SI: i32 = 24; +def A_CVTSI2SS: i32 = 25; +def A_CVTSD2SS: i32 = 26; +def A_CVTSS2SD: i32 = 27; + +def A_ADDQ: i32 = 28; +def A_SUBQ: i32 = 29; +def A_IMULQ: i32 = 30; +def A_IDIVQ: i32 = 31; +def A_DIVQ: i32 = 32; +def A_NEGQ: i32 = 33; +def A_NOTQ: i32 = 34; +def A_ANDQ: i32 = 35; +def A_ORQ: i32 = 36; +def A_XORQ: i32 = 37; +def A_SHLQ: i32 = 38; +def A_SHRQ: i32 = 39; +def A_CMPQ: i32 = 40; + +def A_PUSHQ: i32 = 41; +def A_POPQ: i32 = 42; +def A_LEAQ: i32 = 43; + +def A_CALL: i32 = 44; +def A_RET: i32 = 45; +def A_JMP: i32 = 46; +def A_JE: i32 = 47; +def A_JNE: i32 = 48; +def A_JL: i32 = 49; +def A_JLE: i32 = 50; +def A_JG: i32 = 51; +def A_JGE: i32 = 52; +def A_JB: i32 = 53; +def A_JBE: i32 = 54; +def A_JA: i32 = 55; +def A_JAE: i32 = 56; +def A_JZ: i32 = 57; +def A_JNZ: i32 = 58; + +def A_SYSCALL: i32 = 59; + +// ---- structs (mirror cmd/6a/a.h) -------------------------------------- + +type aoperand = struct { + atype: i32, // D_NONE / D_AX..D_R15 / D_CONST / D_INDIR / D_EXTERN / D_BRANCH + reg: i32, + offset: i64, + asym: str, +}; + +// `from` and `to` are pointer-to-aoperand (rather than embedded). The +// C cgen we currently bootstrap on doesn't support chained-dot through +// embedded structs (e.g. p.to.atype where `to` is a value field), but +// it does support chained-dot through pointer fields. Allocating each +// operand once per prog lets us write `p.to.atype` straightforwardly. +type aprog = struct { + as_: i32, + from: *aoperand, + to: *aoperand, + line: i32, + label: str, + link: *aprog, + bytes: *u8, // payload for A_DATA + nbytes: u64, +}; + +type asym = struct { + name: str, + defined: i32, + is_text: i32, + is_global: i32, + addr: u64, + idx: i32, + snext: *asym, +}; + +type areloc = struct { + off: u64, + kind: i32, + asy: *asym, + addend: i64, + rnext: *areloc, +}; + +type afixup = struct { + off: u64, // where the rel32 lands in .text + label: str, + fnext: *afixup, +}; + +type asm_ = struct { + a: *arena, + file: str, + src: *u8, + srclen: u64, + pos: u64, + line: i32, + + head: *aprog, + tail: *aprog, + + text: *u8, + textcap: u64, + textlen: u64, + + syms: *asym, + relocs: *areloc, + fixups: *afixup, + + errs: i32, +}; diff --git a/selfhost/cmd/6l/main.combined.ww b/selfhost/cmd/6l/main.combined.ww new file mode 100644 index 00000000..fbc77b4f --- /dev/null +++ b/selfhost/cmd/6l/main.combined.ww @@ -0,0 +1,1077 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_EXIT: i64 = 60; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c. +// +// Bump arena allocator. Backed by the runtime page allocator +// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the +// current chunk runs out we link a fresh one. Freeing the arena +// unmaps the chain. +// +// Memory handed out is 16-byte aligned. The C version under +// cmd/wwc/ is retained until the three-stage bootstrap diffs clean. + +use os; + +def ALIGN: u64 = 16u64; +def INIT_CHUNK: u64 = 65536u64; +def MAX_CHUNK: u64 = 4194304u64; +def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, + next: *arena, + total: u64, +}; + +fn roundup(n: u64, a: u64) u64 = { + return (n + a - 1u64) & ~(a - 1u64); +}; + +export fn newarena() *arena = { + let a: *arena = os.alloc(ARENA_SZ): *arena; + a.buf = os.alloc(INIT_CHUNK): *u8; + a.off = 0u64; + a.cap = INIT_CHUNK; + a.next = nil; + a.total = 0u64; + return a; +}; + +// Grow: link a fresh chunk in front of the head. We push the old +// chunk into `next` so the head always describes the current bump +// region. Chunk size doubles up to MAX_CHUNK. +fn grow(a: *arena, need: u64) bool = { + let want: u64 = a.cap * 2u64; + if (want < need) { want = need; }; + if (want > MAX_CHUNK) { want = MAX_CHUNK; }; + if (want < need) { return false; }; // single allocation too big + + let old: *arena = os.alloc(ARENA_SZ): *arena; + old.buf = a.buf; + old.off = a.off; + old.cap = a.cap; + old.next = a.next; + old.total = 0u64; + + a.buf = os.alloc(want): *u8; + a.off = 0u64; + a.cap = want; + a.next = old; + return true; +}; + +export fn amalloc(a: *arena, n: u64) *void = { + let need: u64 = roundup(n, ALIGN); + if (need > a.cap - a.off) { + if (!grow(a, need)) { return nil; }; + }; + let p: *u8 = a.buf + a.off; + a.off += need; + a.total += need; + // Zero the region. Plan 9 amalloc zeroes; we mirror that here so + // the checker can assume freshly allocated nodes start at 0. + let i: u64 = 0u64; + for (i < need) { + p[i] = 0u8; + i += 1u64; + }; + return p: *void; +}; + +// astrndup — copy `n` bytes into the arena and produce a NUL-terminated +// view. Returns a `str` whose ptr is arena-owned and whose len is `n` +// (the trailing NUL is past `len`, so callers reading exactly n bytes +// see no padding). Used by the lexer to capture token text. +export fn astrndup(a: *arena, src: *u8, n: u64) str = { + let p: *u8 = amalloc(a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { + p[i] = src[i]; + i += 1u64; + }; + p[n] = 0u8; + let r: str; + r.ptr = p; + r.len = n: i32; + return r; +}; + +export fn freearena(a: *arena) void = { + for (a != nil) { + let next: *arena = a.next; + os.free(a.buf: *void, a.cap); + os.free(a: *void, ARENA_SZ); + a = next; + }; +}; + +// selfhost/cmd/6l/sym.ww — port of cmd/6l/sym.c. +// +// Linker symbol table. Singly-linked list, usually a few hundred +// entries; hashing isn't worth it yet. + +use mem; + +type lsym = struct { + name: str, + val: u64, // offset within combined .text once linked + defined: i32, // 1 if some lobj defines this symbol + owner: *lobj, + idx_in_owner: i32, + snext: *lsym, +}; + +type lrel = struct { + off: u64, // offset within combined .text + kind: i32, // R_X86_64_* + sym: *lsym, + addend: i64, + rnext: *lrel, +}; + +type lobj = struct { + path: str, + buf: *u8, // object bytes + len: u64, + text_off: u64, // offset of .text in combined output + text_size: u64, + onext: *lobj, +}; + +type lnk = struct { + a: *arena, + objs: *lobj, + syms: *lsym, + rels: *lrel, + text: *u8, // combined .text + textcap: u64, + textlen: u64, + errs: i32, +}; + +fn streq(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; +}; + +export fn l_intern(l: *lnk, name: str) *lsym = { + let s: *lsym = l.syms; + for (s != nil) { + if (streq(s.name, name)) { return s; }; + s = s.snext; + }; + let n: *lsym = amalloc(l.a, 64u64): *lsym; + n.name = name; + n.snext = l.syms; + l.syms = n; + return n; +}; + +export fn l_lookup(l: *lnk, name: str) *lsym = { + let s: *lsym = l.syms; + for (s != nil) { + if (streq(s.name, name)) { return s; }; + s = s.snext; + }; + return nil; +}; + +// selfhost/cmd/6l/obj.ww — port of cmd/6l/obj.c. +// +// Loads relocatable ELF64 .o files emitted by 6a, appends .text to +// the combined image, and pulls in symbols + relocations with +// offsets adjusted to the combined section. +// +// Also handles SysV `ar` archives (libwwrt.a). The two-pass loader +// indexes members on the first pass and iteratively pulls members +// that define currently-undefined symbols on subsequent passes. + +use os; +use mem; +use sym; + +def ET_REL: i32 = 1; +def EM_X86_64: i32 = 62; +def SHT_PROGBITS: i32 = 1; +def SHT_SYMTAB: i32 = 2; +def SHT_STRTAB: i32 = 3; +def SHT_RELA: i32 = 4; + +// ---- little-endian byte readers ---------------------------------------- +// 6a/6l use straight LE on amd64. Reading via byte offsets keeps us off +// the cgen's u16 field-load story for now (MOVZBQ exists; MOVZWQ doesn't). + +fn rd_u16(p: *u8, off: u64) u16 = { + let b0: u16 = p[off]: u16; + let b1: u16 = p[off + 1u64]: u16; + return b0 | (b1 << 8u16); +}; + +fn rd_u32(p: *u8, off: u64) u32 = { + let b0: u32 = p[off]: u32; + let b1: u32 = p[off + 1u64]: u32; + let b2: u32 = p[off + 2u64]: u32; + let b3: u32 = p[off + 3u64]: u32; + return b0 | (b1 << 8u32) | (b2 << 16u32) | (b3 << 24u32); +}; + +fn rd_u64(p: *u8, off: u64) u64 = { + let lo: u64 = rd_u32(p, off): u64; + let hi: u64 = rd_u32(p, off + 4u64): u64; + return lo | (hi << 32u64); +}; + +// ---- ELF64 section header offsets (40 bytes total) -------------------- +def SHDR_SIZE: u64 = 64u64; // sizeof(Shdr) per ELF64 spec +def SHDR_NAME: u64 = 0u64; +def SHDR_TYPE: u64 = 4u64; +def SHDR_OFFSET: u64 = 24u64; +def SHDR_SIZE_F: u64 = 32u64; +def SHDR_LINK: u64 = 40u64; + +// ELF64 ehdr field offsets +def EHDR_SIZE: u64 = 64u64; +def EHDR_TYPE: u64 = 16u64; +def EHDR_MACHINE: u64 = 18u64; +def EHDR_SHOFF: u64 = 40u64; +def EHDR_SHENTSIZE: u64 = 58u64; +def EHDR_SHNUM: u64 = 60u64; +def EHDR_SHSTRNDX: u64 = 62u64; + +// ELF64 sym entry: 24 bytes +def SYM_SIZE: u64 = 24u64; +def SYM_NAME: u64 = 0u64; +def SYM_INFO: u64 = 4u64; +def SYM_SHNDX: u64 = 6u64; +def SYM_VALUE: u64 = 8u64; + +// ELF64 RELA entry: 24 bytes +def RELA_SIZE: u64 = 24u64; +def RELA_OFFSET: u64 = 0u64; +def RELA_INFO: u64 = 8u64; +def RELA_ADDEND: u64 = 16u64; + +// ---- file slurp -------------------------------------------------------- + +fn read_all(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let buf: *u8 = os.alloc(n: u64): *u8; + let got: i64 = os.readfull(fd, buf, n: u64); + os.close(fd); + if (got != n) { return nil, 0u64; }; + return buf, n: u64; +}; + +// ---- text buffer growth ------------------------------------------------ + +fn emit_text(l: *lnk, src: *u8, n: u64) void = { + if (l.textlen + n > l.textcap) { + let nc: u64 = l.textcap; + if (nc == 0u64) { nc = 4096u64; }; + for (nc < l.textlen + n) { nc = nc * 2u64; }; + // Grow by mmap'ing a fresh region and copying. The old buffer + // is leaked into the page allocator; for a linker run this is + // trivial waste. + let nb: *u8 = os.alloc(nc): *u8; + let i: u64 = 0u64; + for (i < l.textlen) { + nb[i] = l.text[i]; + i += 1u64; + }; + l.text = nb; + l.textcap = nc; + }; + let i: u64 = 0u64; + for (i < n) { + l.text[l.textlen + i] = src[i]; + i += 1u64; + }; + l.textlen += n; +}; + +// ---- C-string helpers -------------------------------------------------- + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +fn cstr_eq(p: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (p[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (p[i] != 0u8) { return false; }; + return true; +}; + +// Build a ww str from a NUL-terminated *u8 (for passing to l_intern). +fn cstr_to_str(a: *arena, p: *u8) str = { + let n: u64 = cstrlen(p); + return astrndup(a, p, n); +}; + +// ---- archive (SysV ar) types and helpers ------------------------------- +// +// Each archive member starts with a 60-byte ar_hdr. The fields we care +// about are the first byte (member type) and the size at offset 48 (a +// 10-byte, space-padded decimal). Member bodies are 2-byte aligned. + +type defent = struct { + name: str, + dnext: *defent, +}; + +type armember = struct { + data: *u8, // arena copy of the member's ELF bytes + size: u64, + defs: *defent, // linked list of defined globals + loaded: i32, + mnext: *armember, +}; + +fn is_archive(p: *u8, len: u64) bool = { + if (len < 8u64) { return false; }; + if (p[0u64] != 33u8) { return false; }; // '!' + if (p[1u64] != 60u8) { return false; }; // '<' + if (p[2u64] != 97u8) { return false; }; // 'a' + if (p[3u64] != 114u8) { return false; }; // 'r' + if (p[4u64] != 99u8) { return false; }; // 'c' + if (p[5u64] != 104u8) { return false; }; // 'h' + if (p[6u64] != 62u8) { return false; }; // '>' + if (p[7u64] != 10u8) { return false; }; // '\n' + return true; +}; + +// ar_field — parse a space-padded decimal integer of width n. +fn ar_field(p: *u8, n: u64) u64 = { + let v: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + let c: u8 = p[i]; + if (c < 48u8) { return v; }; // space, NUL, etc. + if (c > 57u8) { return v; }; + v = v * 10u64 + ((c - 48u8): u64); + i += 1u64; + }; + return v; +}; + +// elf_globals — return a linked list of names of globally-defined +// (STB_GLOBAL) symbols whose section is `.text`. Names are arena +// copies, so the source ELF buffer can be freed afterward. +fn elf_globals(a: *arena, buf: *u8, len: u64) *defent = { + if (len < EHDR_SIZE) { return nil; }; + if (buf[0u64] != 127u8) { return nil; }; + if (buf[1u64] != 69u8) { return nil; }; + if (buf[2u64] != 76u8) { return nil; }; + if (buf[3u64] != 70u8) { return nil; }; + + let shoff: u64 = rd_u64(buf, EHDR_SHOFF); + let shnum: u32 = rd_u16(buf, EHDR_SHNUM): u32; + let shstrndx: u32 = rd_u16(buf, EHDR_SHSTRNDX): u32; + + let shstr_sh_off: u64 = rd_u64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET); + let shstr: *u8 = buf + shstr_sh_off; + + let idx_text: i32 = -1; + let idx_symtab: i32 = -1; + let i: u32 = 0u32; + for (i < shnum) { + let sh_off: u64 = shoff + (i: u64) * SHDR_SIZE; + let sh_type: u32 = rd_u32(buf, sh_off + SHDR_TYPE); + let sh_name: u32 = rd_u32(buf, sh_off + SHDR_NAME); + let nm: *u8 = shstr + (sh_name: u64); + if (sh_type == SHT_PROGBITS: u32) { + if (cstr_eq(nm, ".text")) { idx_text = i: i32; }; + }; + if (sh_type == SHT_SYMTAB: u32) { idx_symtab = i: i32; }; + i += 1u32; + }; + if (idx_text < 0) { return nil; }; + if (idx_symtab < 0) { return nil; }; + + let sym_sh: u64 = shoff + (idx_symtab: u64) * SHDR_SIZE; + let sym_off: u64 = rd_u64(buf, sym_sh + SHDR_OFFSET); + let sym_size: u64 = rd_u64(buf, sym_sh + SHDR_SIZE_F); + let sym_link: u32 = rd_u32(buf, sym_sh + SHDR_LINK); + let nsyms: u64 = sym_size / SYM_SIZE; + + let str_sh: u64 = shoff + (sym_link: u64) * SHDR_SIZE; + let str_off: u64 = rd_u64(buf, str_sh + SHDR_OFFSET); + let strtab: *u8 = buf + str_off; + + let head: *defent = nil; + let si: u64 = 1u64; + for (si < nsyms) { + let sym_p: u64 = sym_off + si * SYM_SIZE; + let st_name: u32 = rd_u32(buf, sym_p + SYM_NAME); + let st_info: u8 = buf[sym_p + SYM_INFO]; + let st_shndx: u16 = rd_u16(buf, sym_p + SYM_SHNDX); + let bind: u32 = (st_info: u32) >> 4u32; + // STB_GLOBAL = 1; defined in .text. + if (bind == 1u32) { + if (st_shndx != 0u16) { + if ((st_shndx: i32) == idx_text) { + let nm_p: *u8 = strtab + (st_name: u64); + if (nm_p[0u64] != 0u8) { + let nm: str = cstr_to_str(a, nm_p); + let de: *defent = amalloc(a, 32u64): *defent; + de.name = nm; + de.dnext = head; + head = de; + }; + }; + }; + }; + si += 1u64; + }; + return head; +}; + +// member_defines_undef — true if any of m's defined globals matches a +// currently-undefined symbol in the linker's symbol table. Names not +// already interned are uninteresting (the link doesn't need them yet). +fn member_defines_undef(l: *lnk, m: *armember) bool = { + let de: *defent = m.defs; + for (de != nil) { + let s: *lsym = l_lookup(l, de.name); + if (s != nil) { + if (s.defined == 0) { return true; }; + }; + de = de.dnext; + }; + return false; +}; + +// load_archive — port of cmd/6l/obj.c:load_archive. +// +// Pass 1 indexes every regular member. Pass 2 iteratively pulls in any +// member that supplies a currently-undefined symbol; each pull may +// introduce fresh undefs, so we loop until quiescent. +fn load_archive(l: *lnk, path_cs: *u8, buf: *u8, len: u64) i32 = { + let head: *armember = nil; + let tail: *armember = nil; + let pos: u64 = 8u64; // past "!\n" + for (pos + 60u64 <= len) { + let hdr_size: u64 = ar_field(buf + pos + 48u64, 10u64); + let hdr_end: u64 = pos + 60u64; + if (hdr_end + hdr_size > len) { break; }; + let first: u8 = buf[pos]; + // Skip the symbol table ('/'), long-name table ('//'), and + // any padding entries (NUL or space leading byte). + if (first != 47u8) { if (first != 0u8) { if (first != 32u8) { + let m: *armember = amalloc(l.a, 48u64): *armember; + m.size = hdr_size; + let mb: *u8 = amalloc(l.a, hdr_size): *u8; + let i: u64 = 0u64; + for (i < hdr_size) { + mb[i] = buf[hdr_end + i]; + i += 1u64; + }; + m.data = mb; + m.defs = elf_globals(l.a, mb, hdr_size); + m.loaded = 0; + m.mnext = nil; + if (head == nil) { head = m; } + else { tail.mnext = m; }; + tail = m; + }; }; }; + pos = hdr_end + hdr_size; + if ((hdr_size & 1u64) != 0u64) { pos = pos + 1u64; }; + }; + + let changed: i32 = 1; + for (changed != 0) { + changed = 0; + let m: *armember = head; + for (m != nil) { + if (m.loaded == 0) { + if (member_defines_undef(l, m)) { + if (load_image(l, path_cs, m.data, m.size) == 0) { + m.loaded = 1; + changed = 1; + }; + }; + }; + m = m.mnext; + }; + }; + return 0; +}; + +// ---- main loader ------------------------------------------------------- + +export fn l_load(l: *lnk, path_cs: *u8) i32 = { + let bufp: *u8; + let buflen: u64; + bufp, buflen = read_all(path_cs); + if (bufp == nil) { + os.write(2, "6l: cannot read object\n".ptr, 23u64); + return -1; + }; + if (is_archive(bufp, buflen)) { + return load_archive(l, path_cs, bufp, buflen); + }; + return load_image(l, path_cs, bufp, buflen); +}; + +fn load_image(l: *lnk, path_cs: *u8, buf: *u8, len: u64) i32 = { + if (len < EHDR_SIZE) { return -1; }; + // magic: 0x7f, 'E', 'L', 'F' + if (buf[0u64] != 127u8) { return -1; }; + if (buf[1u64] != 69u8) { return -1; }; + if (buf[2u64] != 76u8) { return -1; }; + if (buf[3u64] != 70u8) { return -1; }; + if (buf[4u64] != 2u8) { return -1; }; // ELFCLASS64 + if (rd_u16(buf, EHDR_TYPE) != ET_REL: u16) { return -1; }; + if (rd_u16(buf, EHDR_MACHINE) != EM_X86_64: u16) { return -1; }; + + let shoff: u64 = rd_u64(buf, EHDR_SHOFF); + let shnum: u32 = rd_u16(buf, EHDR_SHNUM): u32; + let shstrndx: u32 = rd_u16(buf, EHDR_SHSTRNDX): u32; + + let shstr_sh_off: u64 = rd_u64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET); + let shstr: *u8 = buf + shstr_sh_off; + + // find .text, .symtab, .rela.text + let idx_text: i32 = -1; + let idx_symtab: i32 = -1; + let idx_rela: i32 = -1; + let i: u32 = 0u32; + for (i < shnum) { + let sh_off: u64 = shoff + (i: u64) * SHDR_SIZE; + let sh_type: u32 = rd_u32(buf, sh_off + SHDR_TYPE); + let sh_name: u32 = rd_u32(buf, sh_off + SHDR_NAME); + let nm: *u8 = shstr + (sh_name: u64); + if (sh_type == SHT_PROGBITS: u32) { + if (cstr_eq(nm, ".text")) { idx_text = i: i32; }; + }; + if (sh_type == SHT_SYMTAB: u32) { idx_symtab = i: i32; }; + if (sh_type == SHT_RELA: u32) { + if (cstr_eq(nm, ".rela.text")) { idx_rela = i: i32; }; + }; + i += 1u32; + }; + if (idx_text < 0) { + os.write(2, "6l: missing .text\n".ptr, 18u64); + return -1; + }; + if (idx_symtab < 0) { + os.write(2, "6l: missing .symtab\n".ptr, 20u64); + return -1; + }; + + let text_sh: u64 = shoff + (idx_text: u64) * SHDR_SIZE; + let text_off: u64 = rd_u64(buf, text_sh + SHDR_OFFSET); + let text_size: u64 = rd_u64(buf, text_sh + SHDR_SIZE_F); + + let sym_sh: u64 = shoff + (idx_symtab: u64) * SHDR_SIZE; + let sym_off: u64 = rd_u64(buf, sym_sh + SHDR_OFFSET); + let sym_size: u64 = rd_u64(buf, sym_sh + SHDR_SIZE_F); + let sym_link: u32 = rd_u32(buf, sym_sh + SHDR_LINK); + let nsyms: u64 = sym_size / SYM_SIZE; + + let str_sh: u64 = shoff + (sym_link: u64) * SHDR_SIZE; + let str_off: u64 = rd_u64(buf, str_sh + SHDR_OFFSET); + let strtab: *u8 = buf + str_off; + + // Track this object. + let ob: *lobj = amalloc(l.a, 64u64): *lobj; + ob.path = cstr_to_str(l.a, path_cs); + ob.buf = buf; + ob.len = len; + ob.text_off = l.textlen; + ob.text_size = text_size; + ob.onext = l.objs; + l.objs = ob; + + // Append .text bytes to the combined image. + emit_text(l, buf + text_off, text_size); + + // Walk symbols. We don't keep a per-object map[] of *lsym. Instead + // the reloc loop re-walks symtab and re-interns by name. Simpler + // than dancing around the cgen's u64-shift gaps. + let si: u64 = 1u64; // skip index 0 (always undef sentinel) + for (si < nsyms) { + let sym_p: u64 = sym_off + si * SYM_SIZE; + let st_name: u32 = rd_u32(buf, sym_p + SYM_NAME); + let st_shndx: u16 = rd_u16(buf, sym_p + SYM_SHNDX); + let st_value: u64 = rd_u64(buf, sym_p + SYM_VALUE); + let nm_p: *u8 = strtab + (st_name: u64); + if (nm_p[0u64] != 0u8) { + let nm: str = cstr_to_str(l.a, nm_p); + let gs: *lsym = l_intern(l, nm); + if (st_shndx != 0u16) { + if ((st_shndx: i32) == idx_text) { + if (gs.defined != 0) { + os.write(2, "6l: duplicate symbol\n".ptr, 21u64); + l.errs += 1; + } else { + gs.defined = 1; + gs.owner = ob; + gs.idx_in_owner = si: i32; + gs.val = ob.text_off + st_value; + }; + }; + }; + }; + si += 1u64; + }; + + // Per-object relocation collection. + if (idx_rela >= 0) { + let rela_sh: u64 = shoff + (idx_rela: u64) * SHDR_SIZE; + let rela_off: u64 = rd_u64(buf, rela_sh + SHDR_OFFSET); + let rela_size: u64 = rd_u64(buf, rela_sh + SHDR_SIZE_F); + let nrel: u64 = rela_size / RELA_SIZE; + let ri: u64 = 0u64; + for (ri < nrel) { + let rp: u64 = rela_off + ri * RELA_SIZE; + let r_off: u64 = rd_u64(buf, rp + RELA_OFFSET); + let r_info: u64 = rd_u64(buf, rp + RELA_INFO); + let r_addend: u64 = rd_u64(buf, rp + RELA_ADDEND); + let r_sym_idx: u32 = (r_info >> 32u64): u32; + let r_kind: i32 = ((r_info & 4294967295u64): u32): i32; + let nr: *lrel = amalloc(l.a, 48u64): *lrel; + nr.off = ob.text_off + r_off; + nr.kind = r_kind; + nr.addend = r_addend: i64; + // Look up the referenced sym by name (re-walk symtab). + if ((r_sym_idx: u64) < nsyms) { + let s_p: u64 = sym_off + (r_sym_idx: u64) * SYM_SIZE; + let s_name: u32 = rd_u32(buf, s_p + SYM_NAME); + let s_nm: *u8 = strtab + (s_name: u64); + if (s_nm[0u64] != 0u8) { + let nm: str = cstr_to_str(l.a, s_nm); + nr.sym = l_intern(l, nm); + }; + }; + nr.rnext = l.rels; + l.rels = nr; + ri += 1u64; + }; + }; + + return 0; +}; + +// selfhost/cmd/6l/pass.ww — port of cmd/6l/pass.c. +// +// Resolution + relocation. l_resolve flags every undefined symbol +// referenced by a relocation. l_relocate walks the rel list and +// patches the .text bytes in place once the final virtual base is +// known. Supported relocation kinds: PC32 (=2), PLT32 (=4); both +// are 32-bit PC-relative displacements (PLT32 == PC32 for static). + +use os; +use sym; + +def R_X86_64_PC32: i32 = 2; +def R_X86_64_PLT32: i32 = 4; + +export fn l_resolve(l: *lnk) i32 = { + let r: *lrel = l.rels; + for (r != nil) { + if (r.sym != nil) { + if (r.sym.defined == 0) { + os.write(2, "6l: undefined reference to '".ptr, 28u64); + let nm: str = r.sym.name; + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "'\n".ptr, 2u64); + l.errs += 1; + }; + }; + r = r.rnext; + }; + return l.errs; +}; + +fn patch_u32(p: *u8, v: u32) void = { + p[0] = (v & 255u32): u8; + p[1] = ((v >> 8u32) & 255u32): u8; + p[2] = ((v >> 16u32) & 255u32): u8; + p[3] = ((v >> 24u32) & 255u32): u8; +}; + +export fn l_relocate(l: *lnk, base: u64) i32 = { + let r: *lrel = l.rels; + for (r != nil) { + if (r.sym != nil) { + if (r.sym.defined != 0) { + let k: i32 = r.kind; + if (k == R_X86_64_PC32) { + let site: u64 = base + r.off; + let target: i64 = (base + r.sym.val): i64; + let rel: i64 = (target - site: i64) + r.addend; + patch_u32(l.text + r.off, rel: u32); + } else { if (k == R_X86_64_PLT32) { + let site: u64 = base + r.off; + let target: i64 = (base + r.sym.val): i64; + let rel: i64 = (target - site: i64) + r.addend; + patch_u32(l.text + r.off, rel: u32); + } else { + os.write(2, "6l: unsupported reloc kind\n".ptr, 27u64); + l.errs += 1; + };}; + }; + }; + r = r.rnext; + }; + return l.errs; +}; + +// selfhost/cmd/6l/out.ww — port of cmd/6l/out.c. +// +// Emit a static ELF64 executable. File layout (per the C original): +// [0..64) Ehdr +// [64..120) Phdr (one PT_LOAD) +// [120..0x1000) zero pad +// [0x1000..) .text bytes +// Single PT_LOAD covers the whole file, R+X. No interpreter, no .bss. + +use os; +use sym; + +def ET_EXEC: u16 = 2u16; +def EM_X86_64_W: u16 = 62u16; +def EV_CURRENT: u32 = 1u32; +def ELFCLASS64: u8 = 2u8; +def ELFDATA2LSB: u8 = 1u8; +def PT_LOAD: u32 = 1u32; +def PF_X: u32 = 1u32; +def PF_R: u32 = 4u32; + +def TEXT_OFF: u64 = 4096u64; // 0x1000 + +// ---- little-endian byte writers ---------------------------------------- + +fn wr_u16(buf: *u8, off: u64, v: u16) void = { + buf[off] = (v & 255u16): u8; + buf[off + 1u64] = ((v >> 8u16) & 255u16): u8; +}; + +fn wr_u32(buf: *u8, off: u64, v: u32) void = { + buf[off] = (v & 255u32): u8; + buf[off + 1u64] = ((v >> 8u32) & 255u32): u8; + buf[off + 2u64] = ((v >> 16u32) & 255u32): u8; + buf[off + 3u64] = ((v >> 24u32) & 255u32): u8; +}; + +fn wr_u64(buf: *u8, off: u64, v: u64) void = { + wr_u32(buf, off, (v & 4294967295u64): u32); + wr_u32(buf, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); +}; + +// ---- emit --------------------------------------------------------------- + +export fn l_emit_elf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { + let filesz: u64 = TEXT_OFF + l.textlen; + + // One contiguous header buffer covering [0..0x1000), then .text. + let hdr: *u8 = os.alloc(TEXT_OFF): *u8; // zero-initialised by mmap + + // --- Ehdr (64 bytes) --- + hdr[0u64] = 127u8; // 0x7f + hdr[1u64] = 69u8; // 'E' + hdr[2u64] = 76u8; // 'L' + hdr[3u64] = 70u8; // 'F' + hdr[4u64] = ELFCLASS64; + hdr[5u64] = ELFDATA2LSB; + hdr[6u64] = EV_CURRENT: u8; + wr_u16(hdr, 16u64, ET_EXEC); // e_type + wr_u16(hdr, 18u64, EM_X86_64_W); // e_machine + wr_u32(hdr, 20u64, EV_CURRENT); // e_version + wr_u64(hdr, 24u64, entry); // e_entry + wr_u64(hdr, 32u64, 64u64); // e_phoff = sizeof(Ehdr) + wr_u64(hdr, 40u64, 0u64); // e_shoff + wr_u32(hdr, 48u64, 0u32); // e_flags + wr_u16(hdr, 52u64, 64u16); // e_ehsize + wr_u16(hdr, 54u64, 56u16); // e_phentsize + wr_u16(hdr, 56u64, 1u16); // e_phnum + wr_u16(hdr, 58u64, 0u16); // e_shentsize + wr_u16(hdr, 60u64, 0u16); // e_shnum + wr_u16(hdr, 62u64, 0u16); // e_shstrndx + + // --- Phdr (56 bytes) at offset 64 --- + wr_u32(hdr, 64u64, PT_LOAD); // p_type + wr_u32(hdr, 68u64, PF_R | PF_X); // p_flags + wr_u64(hdr, 72u64, 0u64); // p_offset + wr_u64(hdr, 80u64, base); // p_vaddr + wr_u64(hdr, 88u64, base); // p_paddr + wr_u64(hdr, 96u64, filesz); // p_filesz + wr_u64(hdr, 104u64, filesz); // p_memsz + wr_u64(hdr, 112u64, TEXT_OFF); // p_align + + // Write [0..0x1000) then .text. + let n1: i64 = os.writefull(fd, hdr, TEXT_OFF); + if (n1 != TEXT_OFF: i64) { return -1; }; + if (l.textlen > 0u64) { + let n2: i64 = os.writefull(fd, l.text, l.textlen); + if (n2 != l.textlen: i64) { return -1; }; + }; + return 0; +}; + +// selfhost/cmd/6l/main.ww — port of cmd/6l/main.c. +// +// 6l = amd64 static linker. Reads relocatable ELF .o files and +// SysV `ar` archives, resolves symbols, applies relocations, writes +// a static ELF executable. +// +// 6l_ww -o out file1.o file2.o libwwrt.a ... + +use os; +use mem; +use sym; +use obj; +use pass; +use out; + +def BASE: u64 = 4194304u64; // 0x400000 +def CODE_VA_OFF: u64 = 4096u64; // .text starts at base + 0x1000 + +// Linker context lives in main's frame; arena gets passed in. +fn make_lnk(a: *arena) *lnk = { + let l: *lnk = amalloc(a, 96u64): *lnk; + l.a = a; + return l; +}; + +fn streq_cs(a: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (a[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (a[i] != 0u8) { return false; }; + return true; +}; + +export fn main(argc: i32, argv: **u8) i32 = { + + let out_path: *u8 = nil; + // Inputs: store as **u8 (heap'd from a fixed-size buffer). + let max_inputs: i32 = 64; + let inputs: **u8 = os.alloc((max_inputs: u64) * 8u64): **u8; + let ninputs: i32 = 0; + + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_cs(a, "-o")) { + i += 1; + if (i >= argc) { + os.write(2, "6l: -o requires argument\n".ptr, 25u64); + return 2; + }; + out_path = argv[i]; + } else { if (a[0u64] == 45u8) { + os.write(2, "6l: unknown flag\n".ptr, 17u64); + return 2; + } else { + if (ninputs >= max_inputs) { + os.write(2, "6l: too many inputs\n".ptr, 20u64); + return 2; + }; + inputs[ninputs] = a; + ninputs += 1; + }; }; + i += 1; + }; + + if (out_path == nil) { + os.write(2, "usage: 6l_ww -o exe file1.o [file2.o...]\n".ptr, 41u64); + return 2; + }; + if (ninputs == 0) { + os.write(2, "6l: no inputs\n".ptr, 14u64); + return 2; + }; + + let a: *arena = newarena(); + let l: *lnk = make_lnk(a); + + // Seed _start so a libwwrt-style start.o is recognised as wanted. + l_intern(l, "_start"); + + let k: i32 = 0; + for (k < ninputs) { + if (l_load(l, inputs[k]) != 0) { + return 1; + }; + k += 1; + }; + + if (l_resolve(l) != 0) { return 1; }; + if (l_relocate(l, BASE + CODE_VA_OFF) != 0) { return 1; }; + + let entry_sym: *lsym = l_lookup(l, "_start"); + if (entry_sym == nil) { entry_sym = l_lookup(l, "main"); } + else { if (entry_sym.defined == 0) { entry_sym = l_lookup(l, "main"); }; }; + if (entry_sym == nil) { + os.write(2, "6l: no _start or main symbol\n".ptr, 29u64); + return 1; + }; + if (entry_sym.defined == 0) { + os.write(2, "6l: no _start or main symbol\n".ptr, 29u64); + return 1; + }; + + // Open output: O_WRONLY|O_CREAT|O_TRUNC, mode 0755. + let flags: i32 = os.O_WRONLY | os.O_CREAT | os.O_TRUNC; + let fd: i32 = os.open(out_path, flags, 493i32); // 0o755 + if (fd < 0) { + os.write(2, "6l: cannot open output\n".ptr, 23u64); + return 1; + }; + + let entry_va: u64 = BASE + CODE_VA_OFF + entry_sym.val; + let rc: i32 = l_emit_elf(l, fd, BASE, entry_va); + os.close(fd); + return rc; +}; + diff --git a/selfhost/cmd/6l/main.ww b/selfhost/cmd/6l/main.ww new file mode 100644 index 00000000..937ef841 --- /dev/null +++ b/selfhost/cmd/6l/main.ww @@ -0,0 +1,120 @@ +// selfhost/cmd/6l/main.ww — port of cmd/6l/main.c. +// +// 6l = amd64 static linker. Reads relocatable ELF .o files and +// SysV `ar` archives, resolves symbols, applies relocations, writes +// a static ELF executable. +// +// 6l_ww -o out file1.o file2.o libwwrt.a ... + +use os; +use mem; +use sym; +use obj; +use pass; +use out; + +def BASE: u64 = 4194304u64; // 0x400000 +def CODE_VA_OFF: u64 = 4096u64; // .text starts at base + 0x1000 + +// Linker context lives in main's frame; arena gets passed in. +fn make_lnk(a: *arena) *lnk = { + let l: *lnk = amalloc(a, 96u64): *lnk; + l.a = a; + return l; +}; + +fn streq_cs(a: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (a[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (a[i] != 0u8) { return false; }; + return true; +}; + +export fn main(argc: i32, argv: **u8) i32 = { + + let out_path: *u8 = nil; + // Inputs: store as **u8 (heap'd from a fixed-size buffer). + let max_inputs: i32 = 64; + let inputs: **u8 = os.alloc((max_inputs: u64) * 8u64): **u8; + let ninputs: i32 = 0; + + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_cs(a, "-o")) { + i += 1; + if (i >= argc) { + os.write(2, "6l: -o requires argument\n".ptr, 25u64); + return 2; + }; + out_path = argv[i]; + } else { if (a[0u64] == 45u8) { + os.write(2, "6l: unknown flag\n".ptr, 17u64); + return 2; + } else { + if (ninputs >= max_inputs) { + os.write(2, "6l: too many inputs\n".ptr, 20u64); + return 2; + }; + inputs[ninputs] = a; + ninputs += 1; + }; }; + i += 1; + }; + + if (out_path == nil) { + os.write(2, "usage: 6l_ww -o exe file1.o [file2.o...]\n".ptr, 41u64); + return 2; + }; + if (ninputs == 0) { + os.write(2, "6l: no inputs\n".ptr, 14u64); + return 2; + }; + + let a: *arena = newarena(); + let l: *lnk = make_lnk(a); + + // Seed _start so a libwwrt-style start.o is recognised as wanted. + l_intern(l, "_start"); + + let k: i32 = 0; + for (k < ninputs) { + if (l_load(l, inputs[k]) != 0) { + return 1; + }; + k += 1; + }; + + if (l_resolve(l) != 0) { return 1; }; + if (l_relocate(l, BASE + CODE_VA_OFF) != 0) { return 1; }; + + let entry_sym: *lsym = l_lookup(l, "_start"); + if (entry_sym == nil) { entry_sym = l_lookup(l, "main"); } + else { if (entry_sym.defined == 0) { entry_sym = l_lookup(l, "main"); }; }; + if (entry_sym == nil) { + os.write(2, "6l: no _start or main symbol\n".ptr, 29u64); + return 1; + }; + if (entry_sym.defined == 0) { + os.write(2, "6l: no _start or main symbol\n".ptr, 29u64); + return 1; + }; + + // Open output: O_WRONLY|O_CREAT|O_TRUNC, mode 0755. + let flags: i32 = os.O_WRONLY | os.O_CREAT | os.O_TRUNC; + let fd: i32 = os.open(out_path, flags, 493i32); // 0o755 + if (fd < 0) { + os.write(2, "6l: cannot open output\n".ptr, 23u64); + return 1; + }; + + let entry_va: u64 = BASE + CODE_VA_OFF + entry_sym.val; + let rc: i32 = l_emit_elf(l, fd, BASE, entry_va); + os.close(fd); + return rc; +}; diff --git a/selfhost/cmd/6l/obj.ww b/selfhost/cmd/6l/obj.ww new file mode 100644 index 00000000..f0bff6b3 --- /dev/null +++ b/selfhost/cmd/6l/obj.ww @@ -0,0 +1,486 @@ +// selfhost/cmd/6l/obj.ww — port of cmd/6l/obj.c. +// +// Loads relocatable ELF64 .o files emitted by 6a, appends .text to +// the combined image, and pulls in symbols + relocations with +// offsets adjusted to the combined section. +// +// Also handles SysV `ar` archives (libwwrt.a). The two-pass loader +// indexes members on the first pass and iteratively pulls members +// that define currently-undefined symbols on subsequent passes. + +use os; +use mem; +use sym; + +def ET_REL: i32 = 1; +def EM_X86_64: i32 = 62; +def SHT_PROGBITS: i32 = 1; +def SHT_SYMTAB: i32 = 2; +def SHT_STRTAB: i32 = 3; +def SHT_RELA: i32 = 4; + +// ---- little-endian byte readers ---------------------------------------- +// 6a/6l use straight LE on amd64. Reading via byte offsets keeps us off +// the cgen's u16 field-load story for now (MOVZBQ exists; MOVZWQ doesn't). + +fn rd_u16(p: *u8, off: u64) u16 = { + let b0: u16 = p[off]: u16; + let b1: u16 = p[off + 1u64]: u16; + return b0 | (b1 << 8u16); +}; + +fn rd_u32(p: *u8, off: u64) u32 = { + let b0: u32 = p[off]: u32; + let b1: u32 = p[off + 1u64]: u32; + let b2: u32 = p[off + 2u64]: u32; + let b3: u32 = p[off + 3u64]: u32; + return b0 | (b1 << 8u32) | (b2 << 16u32) | (b3 << 24u32); +}; + +fn rd_u64(p: *u8, off: u64) u64 = { + let lo: u64 = rd_u32(p, off): u64; + let hi: u64 = rd_u32(p, off + 4u64): u64; + return lo | (hi << 32u64); +}; + +// ---- ELF64 section header offsets (40 bytes total) -------------------- +def SHDR_SIZE: u64 = 64u64; // sizeof(Shdr) per ELF64 spec +def SHDR_NAME: u64 = 0u64; +def SHDR_TYPE: u64 = 4u64; +def SHDR_OFFSET: u64 = 24u64; +def SHDR_SIZE_F: u64 = 32u64; +def SHDR_LINK: u64 = 40u64; + +// ELF64 ehdr field offsets +def EHDR_SIZE: u64 = 64u64; +def EHDR_TYPE: u64 = 16u64; +def EHDR_MACHINE: u64 = 18u64; +def EHDR_SHOFF: u64 = 40u64; +def EHDR_SHENTSIZE: u64 = 58u64; +def EHDR_SHNUM: u64 = 60u64; +def EHDR_SHSTRNDX: u64 = 62u64; + +// ELF64 sym entry: 24 bytes +def SYM_SIZE: u64 = 24u64; +def SYM_NAME: u64 = 0u64; +def SYM_INFO: u64 = 4u64; +def SYM_SHNDX: u64 = 6u64; +def SYM_VALUE: u64 = 8u64; + +// ELF64 RELA entry: 24 bytes +def RELA_SIZE: u64 = 24u64; +def RELA_OFFSET: u64 = 0u64; +def RELA_INFO: u64 = 8u64; +def RELA_ADDEND: u64 = 16u64; + +// ---- file slurp -------------------------------------------------------- + +fn read_all(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let buf: *u8 = os.alloc(n: u64): *u8; + let got: i64 = os.readfull(fd, buf, n: u64); + os.close(fd); + if (got != n) { return nil, 0u64; }; + return buf, n: u64; +}; + +// ---- text buffer growth ------------------------------------------------ + +fn emit_text(l: *lnk, src: *u8, n: u64) void = { + if (l.textlen + n > l.textcap) { + let nc: u64 = l.textcap; + if (nc == 0u64) { nc = 4096u64; }; + for (nc < l.textlen + n) { nc = nc * 2u64; }; + // Grow by mmap'ing a fresh region and copying. The old buffer + // is leaked into the page allocator; for a linker run this is + // trivial waste. + let nb: *u8 = os.alloc(nc): *u8; + let i: u64 = 0u64; + for (i < l.textlen) { + nb[i] = l.text[i]; + i += 1u64; + }; + l.text = nb; + l.textcap = nc; + }; + let i: u64 = 0u64; + for (i < n) { + l.text[l.textlen + i] = src[i]; + i += 1u64; + }; + l.textlen += n; +}; + +// ---- C-string helpers -------------------------------------------------- + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +fn cstr_eq(p: *u8, lit: str) bool = { + let n: u64 = lit.len: u64; + let i: u64 = 0u64; + for (i < n) { + let li: i32 = i: i32; + if (p[i] != lit[li]) { return false; }; + i += 1u64; + }; + if (p[i] != 0u8) { return false; }; + return true; +}; + +// Build a ww str from a NUL-terminated *u8 (for passing to l_intern). +fn cstr_to_str(a: *arena, p: *u8) str = { + let n: u64 = cstrlen(p); + return astrndup(a, p, n); +}; + +// ---- archive (SysV ar) types and helpers ------------------------------- +// +// Each archive member starts with a 60-byte ar_hdr. The fields we care +// about are the first byte (member type) and the size at offset 48 (a +// 10-byte, space-padded decimal). Member bodies are 2-byte aligned. + +type defent = struct { + name: str, + dnext: *defent, +}; + +type armember = struct { + data: *u8, // arena copy of the member's ELF bytes + size: u64, + defs: *defent, // linked list of defined globals + loaded: i32, + mnext: *armember, +}; + +fn is_archive(p: *u8, len: u64) bool = { + if (len < 8u64) { return false; }; + if (p[0u64] != 33u8) { return false; }; // '!' + if (p[1u64] != 60u8) { return false; }; // '<' + if (p[2u64] != 97u8) { return false; }; // 'a' + if (p[3u64] != 114u8) { return false; }; // 'r' + if (p[4u64] != 99u8) { return false; }; // 'c' + if (p[5u64] != 104u8) { return false; }; // 'h' + if (p[6u64] != 62u8) { return false; }; // '>' + if (p[7u64] != 10u8) { return false; }; // '\n' + return true; +}; + +// ar_field — parse a space-padded decimal integer of width n. +fn ar_field(p: *u8, n: u64) u64 = { + let v: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + let c: u8 = p[i]; + if (c < 48u8) { return v; }; // space, NUL, etc. + if (c > 57u8) { return v; }; + v = v * 10u64 + ((c - 48u8): u64); + i += 1u64; + }; + return v; +}; + +// elf_globals — return a linked list of names of globally-defined +// (STB_GLOBAL) symbols whose section is `.text`. Names are arena +// copies, so the source ELF buffer can be freed afterward. +fn elf_globals(a: *arena, buf: *u8, len: u64) *defent = { + if (len < EHDR_SIZE) { return nil; }; + if (buf[0u64] != 127u8) { return nil; }; + if (buf[1u64] != 69u8) { return nil; }; + if (buf[2u64] != 76u8) { return nil; }; + if (buf[3u64] != 70u8) { return nil; }; + + let shoff: u64 = rd_u64(buf, EHDR_SHOFF); + let shnum: u32 = rd_u16(buf, EHDR_SHNUM): u32; + let shstrndx: u32 = rd_u16(buf, EHDR_SHSTRNDX): u32; + + let shstr_sh_off: u64 = rd_u64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET); + let shstr: *u8 = buf + shstr_sh_off; + + let idx_text: i32 = -1; + let idx_symtab: i32 = -1; + let i: u32 = 0u32; + for (i < shnum) { + let sh_off: u64 = shoff + (i: u64) * SHDR_SIZE; + let sh_type: u32 = rd_u32(buf, sh_off + SHDR_TYPE); + let sh_name: u32 = rd_u32(buf, sh_off + SHDR_NAME); + let nm: *u8 = shstr + (sh_name: u64); + if (sh_type == SHT_PROGBITS: u32) { + if (cstr_eq(nm, ".text")) { idx_text = i: i32; }; + }; + if (sh_type == SHT_SYMTAB: u32) { idx_symtab = i: i32; }; + i += 1u32; + }; + if (idx_text < 0) { return nil; }; + if (idx_symtab < 0) { return nil; }; + + let sym_sh: u64 = shoff + (idx_symtab: u64) * SHDR_SIZE; + let sym_off: u64 = rd_u64(buf, sym_sh + SHDR_OFFSET); + let sym_size: u64 = rd_u64(buf, sym_sh + SHDR_SIZE_F); + let sym_link: u32 = rd_u32(buf, sym_sh + SHDR_LINK); + let nsyms: u64 = sym_size / SYM_SIZE; + + let str_sh: u64 = shoff + (sym_link: u64) * SHDR_SIZE; + let str_off: u64 = rd_u64(buf, str_sh + SHDR_OFFSET); + let strtab: *u8 = buf + str_off; + + let head: *defent = nil; + let si: u64 = 1u64; + for (si < nsyms) { + let sym_p: u64 = sym_off + si * SYM_SIZE; + let st_name: u32 = rd_u32(buf, sym_p + SYM_NAME); + let st_info: u8 = buf[sym_p + SYM_INFO]; + let st_shndx: u16 = rd_u16(buf, sym_p + SYM_SHNDX); + let bind: u32 = (st_info: u32) >> 4u32; + // STB_GLOBAL = 1; defined in .text. + if (bind == 1u32) { + if (st_shndx != 0u16) { + if ((st_shndx: i32) == idx_text) { + let nm_p: *u8 = strtab + (st_name: u64); + if (nm_p[0u64] != 0u8) { + let nm: str = cstr_to_str(a, nm_p); + let de: *defent = amalloc(a, 32u64): *defent; + de.name = nm; + de.dnext = head; + head = de; + }; + }; + }; + }; + si += 1u64; + }; + return head; +}; + +// member_defines_undef — true if any of m's defined globals matches a +// currently-undefined symbol in the linker's symbol table. Names not +// already interned are uninteresting (the link doesn't need them yet). +fn member_defines_undef(l: *lnk, m: *armember) bool = { + let de: *defent = m.defs; + for (de != nil) { + let s: *lsym = l_lookup(l, de.name); + if (s != nil) { + if (s.defined == 0) { return true; }; + }; + de = de.dnext; + }; + return false; +}; + +// load_archive — port of cmd/6l/obj.c:load_archive. +// +// Pass 1 indexes every regular member. Pass 2 iteratively pulls in any +// member that supplies a currently-undefined symbol; each pull may +// introduce fresh undefs, so we loop until quiescent. +fn load_archive(l: *lnk, path_cs: *u8, buf: *u8, len: u64) i32 = { + let head: *armember = nil; + let tail: *armember = nil; + let pos: u64 = 8u64; // past "!\n" + for (pos + 60u64 <= len) { + let hdr_size: u64 = ar_field(buf + pos + 48u64, 10u64); + let hdr_end: u64 = pos + 60u64; + if (hdr_end + hdr_size > len) { break; }; + let first: u8 = buf[pos]; + // Skip the symbol table ('/'), long-name table ('//'), and + // any padding entries (NUL or space leading byte). + if (first != 47u8) { if (first != 0u8) { if (first != 32u8) { + let m: *armember = amalloc(l.a, 48u64): *armember; + m.size = hdr_size; + let mb: *u8 = amalloc(l.a, hdr_size): *u8; + let i: u64 = 0u64; + for (i < hdr_size) { + mb[i] = buf[hdr_end + i]; + i += 1u64; + }; + m.data = mb; + m.defs = elf_globals(l.a, mb, hdr_size); + m.loaded = 0; + m.mnext = nil; + if (head == nil) { head = m; } + else { tail.mnext = m; }; + tail = m; + }; }; }; + pos = hdr_end + hdr_size; + if ((hdr_size & 1u64) != 0u64) { pos = pos + 1u64; }; + }; + + let changed: i32 = 1; + for (changed != 0) { + changed = 0; + let m: *armember = head; + for (m != nil) { + if (m.loaded == 0) { + if (member_defines_undef(l, m)) { + if (load_image(l, path_cs, m.data, m.size) == 0) { + m.loaded = 1; + changed = 1; + }; + }; + }; + m = m.mnext; + }; + }; + return 0; +}; + +// ---- main loader ------------------------------------------------------- + +export fn l_load(l: *lnk, path_cs: *u8) i32 = { + let bufp: *u8; + let buflen: u64; + bufp, buflen = read_all(path_cs); + if (bufp == nil) { + os.write(2, "6l: cannot read object\n".ptr, 23u64); + return -1; + }; + if (is_archive(bufp, buflen)) { + return load_archive(l, path_cs, bufp, buflen); + }; + return load_image(l, path_cs, bufp, buflen); +}; + +fn load_image(l: *lnk, path_cs: *u8, buf: *u8, len: u64) i32 = { + if (len < EHDR_SIZE) { return -1; }; + // magic: 0x7f, 'E', 'L', 'F' + if (buf[0u64] != 127u8) { return -1; }; + if (buf[1u64] != 69u8) { return -1; }; + if (buf[2u64] != 76u8) { return -1; }; + if (buf[3u64] != 70u8) { return -1; }; + if (buf[4u64] != 2u8) { return -1; }; // ELFCLASS64 + if (rd_u16(buf, EHDR_TYPE) != ET_REL: u16) { return -1; }; + if (rd_u16(buf, EHDR_MACHINE) != EM_X86_64: u16) { return -1; }; + + let shoff: u64 = rd_u64(buf, EHDR_SHOFF); + let shnum: u32 = rd_u16(buf, EHDR_SHNUM): u32; + let shstrndx: u32 = rd_u16(buf, EHDR_SHSTRNDX): u32; + + let shstr_sh_off: u64 = rd_u64(buf, shoff + (shstrndx: u64) * SHDR_SIZE + SHDR_OFFSET); + let shstr: *u8 = buf + shstr_sh_off; + + // find .text, .symtab, .rela.text + let idx_text: i32 = -1; + let idx_symtab: i32 = -1; + let idx_rela: i32 = -1; + let i: u32 = 0u32; + for (i < shnum) { + let sh_off: u64 = shoff + (i: u64) * SHDR_SIZE; + let sh_type: u32 = rd_u32(buf, sh_off + SHDR_TYPE); + let sh_name: u32 = rd_u32(buf, sh_off + SHDR_NAME); + let nm: *u8 = shstr + (sh_name: u64); + if (sh_type == SHT_PROGBITS: u32) { + if (cstr_eq(nm, ".text")) { idx_text = i: i32; }; + }; + if (sh_type == SHT_SYMTAB: u32) { idx_symtab = i: i32; }; + if (sh_type == SHT_RELA: u32) { + if (cstr_eq(nm, ".rela.text")) { idx_rela = i: i32; }; + }; + i += 1u32; + }; + if (idx_text < 0) { + os.write(2, "6l: missing .text\n".ptr, 18u64); + return -1; + }; + if (idx_symtab < 0) { + os.write(2, "6l: missing .symtab\n".ptr, 20u64); + return -1; + }; + + let text_sh: u64 = shoff + (idx_text: u64) * SHDR_SIZE; + let text_off: u64 = rd_u64(buf, text_sh + SHDR_OFFSET); + let text_size: u64 = rd_u64(buf, text_sh + SHDR_SIZE_F); + + let sym_sh: u64 = shoff + (idx_symtab: u64) * SHDR_SIZE; + let sym_off: u64 = rd_u64(buf, sym_sh + SHDR_OFFSET); + let sym_size: u64 = rd_u64(buf, sym_sh + SHDR_SIZE_F); + let sym_link: u32 = rd_u32(buf, sym_sh + SHDR_LINK); + let nsyms: u64 = sym_size / SYM_SIZE; + + let str_sh: u64 = shoff + (sym_link: u64) * SHDR_SIZE; + let str_off: u64 = rd_u64(buf, str_sh + SHDR_OFFSET); + let strtab: *u8 = buf + str_off; + + // Track this object. + let ob: *lobj = amalloc(l.a, 64u64): *lobj; + ob.path = cstr_to_str(l.a, path_cs); + ob.buf = buf; + ob.len = len; + ob.text_off = l.textlen; + ob.text_size = text_size; + ob.onext = l.objs; + l.objs = ob; + + // Append .text bytes to the combined image. + emit_text(l, buf + text_off, text_size); + + // Walk symbols. We don't keep a per-object map[] of *lsym. Instead + // the reloc loop re-walks symtab and re-interns by name. Simpler + // than dancing around the cgen's u64-shift gaps. + let si: u64 = 1u64; // skip index 0 (always undef sentinel) + for (si < nsyms) { + let sym_p: u64 = sym_off + si * SYM_SIZE; + let st_name: u32 = rd_u32(buf, sym_p + SYM_NAME); + let st_shndx: u16 = rd_u16(buf, sym_p + SYM_SHNDX); + let st_value: u64 = rd_u64(buf, sym_p + SYM_VALUE); + let nm_p: *u8 = strtab + (st_name: u64); + if (nm_p[0u64] != 0u8) { + let nm: str = cstr_to_str(l.a, nm_p); + let gs: *lsym = l_intern(l, nm); + if (st_shndx != 0u16) { + if ((st_shndx: i32) == idx_text) { + if (gs.defined != 0) { + os.write(2, "6l: duplicate symbol\n".ptr, 21u64); + l.errs += 1; + } else { + gs.defined = 1; + gs.owner = ob; + gs.idx_in_owner = si: i32; + gs.val = ob.text_off + st_value; + }; + }; + }; + }; + si += 1u64; + }; + + // Per-object relocation collection. + if (idx_rela >= 0) { + let rela_sh: u64 = shoff + (idx_rela: u64) * SHDR_SIZE; + let rela_off: u64 = rd_u64(buf, rela_sh + SHDR_OFFSET); + let rela_size: u64 = rd_u64(buf, rela_sh + SHDR_SIZE_F); + let nrel: u64 = rela_size / RELA_SIZE; + let ri: u64 = 0u64; + for (ri < nrel) { + let rp: u64 = rela_off + ri * RELA_SIZE; + let r_off: u64 = rd_u64(buf, rp + RELA_OFFSET); + let r_info: u64 = rd_u64(buf, rp + RELA_INFO); + let r_addend: u64 = rd_u64(buf, rp + RELA_ADDEND); + let r_sym_idx: u32 = (r_info >> 32u64): u32; + let r_kind: i32 = ((r_info & 4294967295u64): u32): i32; + let nr: *lrel = amalloc(l.a, 48u64): *lrel; + nr.off = ob.text_off + r_off; + nr.kind = r_kind; + nr.addend = r_addend: i64; + // Look up the referenced sym by name (re-walk symtab). + if ((r_sym_idx: u64) < nsyms) { + let s_p: u64 = sym_off + (r_sym_idx: u64) * SYM_SIZE; + let s_name: u32 = rd_u32(buf, s_p + SYM_NAME); + let s_nm: *u8 = strtab + (s_name: u64); + if (s_nm[0u64] != 0u8) { + let nm: str = cstr_to_str(l.a, s_nm); + nr.sym = l_intern(l, nm); + }; + }; + nr.rnext = l.rels; + l.rels = nr; + ri += 1u64; + }; + }; + + return 0; +}; diff --git a/selfhost/cmd/6l/out.ww b/selfhost/cmd/6l/out.ww new file mode 100644 index 00000000..1439e995 --- /dev/null +++ b/selfhost/cmd/6l/out.ww @@ -0,0 +1,91 @@ +// selfhost/cmd/6l/out.ww — port of cmd/6l/out.c. +// +// Emit a static ELF64 executable. File layout (per the C original): +// [0..64) Ehdr +// [64..120) Phdr (one PT_LOAD) +// [120..0x1000) zero pad +// [0x1000..) .text bytes +// Single PT_LOAD covers the whole file, R+X. No interpreter, no .bss. + +use os; +use sym; + +def ET_EXEC: u16 = 2u16; +def EM_X86_64_W: u16 = 62u16; +def EV_CURRENT: u32 = 1u32; +def ELFCLASS64: u8 = 2u8; +def ELFDATA2LSB: u8 = 1u8; +def PT_LOAD: u32 = 1u32; +def PF_X: u32 = 1u32; +def PF_R: u32 = 4u32; + +def TEXT_OFF: u64 = 4096u64; // 0x1000 + +// ---- little-endian byte writers ---------------------------------------- + +fn wr_u16(buf: *u8, off: u64, v: u16) void = { + buf[off] = (v & 255u16): u8; + buf[off + 1u64] = ((v >> 8u16) & 255u16): u8; +}; + +fn wr_u32(buf: *u8, off: u64, v: u32) void = { + buf[off] = (v & 255u32): u8; + buf[off + 1u64] = ((v >> 8u32) & 255u32): u8; + buf[off + 2u64] = ((v >> 16u32) & 255u32): u8; + buf[off + 3u64] = ((v >> 24u32) & 255u32): u8; +}; + +fn wr_u64(buf: *u8, off: u64, v: u64) void = { + wr_u32(buf, off, (v & 4294967295u64): u32); + wr_u32(buf, off + 4u64, ((v >> 32u64) & 4294967295u64): u32); +}; + +// ---- emit --------------------------------------------------------------- + +export fn l_emit_elf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { + let filesz: u64 = TEXT_OFF + l.textlen; + + // One contiguous header buffer covering [0..0x1000), then .text. + let hdr: *u8 = os.alloc(TEXT_OFF): *u8; // zero-initialised by mmap + + // --- Ehdr (64 bytes) --- + hdr[0u64] = 127u8; // 0x7f + hdr[1u64] = 69u8; // 'E' + hdr[2u64] = 76u8; // 'L' + hdr[3u64] = 70u8; // 'F' + hdr[4u64] = ELFCLASS64; + hdr[5u64] = ELFDATA2LSB; + hdr[6u64] = EV_CURRENT: u8; + wr_u16(hdr, 16u64, ET_EXEC); // e_type + wr_u16(hdr, 18u64, EM_X86_64_W); // e_machine + wr_u32(hdr, 20u64, EV_CURRENT); // e_version + wr_u64(hdr, 24u64, entry); // e_entry + wr_u64(hdr, 32u64, 64u64); // e_phoff = sizeof(Ehdr) + wr_u64(hdr, 40u64, 0u64); // e_shoff + wr_u32(hdr, 48u64, 0u32); // e_flags + wr_u16(hdr, 52u64, 64u16); // e_ehsize + wr_u16(hdr, 54u64, 56u16); // e_phentsize + wr_u16(hdr, 56u64, 1u16); // e_phnum + wr_u16(hdr, 58u64, 0u16); // e_shentsize + wr_u16(hdr, 60u64, 0u16); // e_shnum + wr_u16(hdr, 62u64, 0u16); // e_shstrndx + + // --- Phdr (56 bytes) at offset 64 --- + wr_u32(hdr, 64u64, PT_LOAD); // p_type + wr_u32(hdr, 68u64, PF_R | PF_X); // p_flags + wr_u64(hdr, 72u64, 0u64); // p_offset + wr_u64(hdr, 80u64, base); // p_vaddr + wr_u64(hdr, 88u64, base); // p_paddr + wr_u64(hdr, 96u64, filesz); // p_filesz + wr_u64(hdr, 104u64, filesz); // p_memsz + wr_u64(hdr, 112u64, TEXT_OFF); // p_align + + // Write [0..0x1000) then .text. + let n1: i64 = os.writefull(fd, hdr, TEXT_OFF); + if (n1 != TEXT_OFF: i64) { return -1; }; + if (l.textlen > 0u64) { + let n2: i64 = os.writefull(fd, l.text, l.textlen); + if (n2 != l.textlen: i64) { return -1; }; + }; + return 0; +}; diff --git a/selfhost/cmd/6l/pass.ww b/selfhost/cmd/6l/pass.ww new file mode 100644 index 00000000..2dc5a118 --- /dev/null +++ b/selfhost/cmd/6l/pass.ww @@ -0,0 +1,64 @@ +// selfhost/cmd/6l/pass.ww — port of cmd/6l/pass.c. +// +// Resolution + relocation. l_resolve flags every undefined symbol +// referenced by a relocation. l_relocate walks the rel list and +// patches the .text bytes in place once the final virtual base is +// known. Supported relocation kinds: PC32 (=2), PLT32 (=4); both +// are 32-bit PC-relative displacements (PLT32 == PC32 for static). + +use os; +use sym; + +def R_X86_64_PC32: i32 = 2; +def R_X86_64_PLT32: i32 = 4; + +export fn l_resolve(l: *lnk) i32 = { + let r: *lrel = l.rels; + for (r != nil) { + if (r.sym != nil) { + if (r.sym.defined == 0) { + os.write(2, "6l: undefined reference to '".ptr, 28u64); + let nm: str = r.sym.name; + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "'\n".ptr, 2u64); + l.errs += 1; + }; + }; + r = r.rnext; + }; + return l.errs; +}; + +fn patch_u32(p: *u8, v: u32) void = { + p[0] = (v & 255u32): u8; + p[1] = ((v >> 8u32) & 255u32): u8; + p[2] = ((v >> 16u32) & 255u32): u8; + p[3] = ((v >> 24u32) & 255u32): u8; +}; + +export fn l_relocate(l: *lnk, base: u64) i32 = { + let r: *lrel = l.rels; + for (r != nil) { + if (r.sym != nil) { + if (r.sym.defined != 0) { + let k: i32 = r.kind; + if (k == R_X86_64_PC32) { + let site: u64 = base + r.off; + let target: i64 = (base + r.sym.val): i64; + let rel: i64 = (target - site: i64) + r.addend; + patch_u32(l.text + r.off, rel: u32); + } else { if (k == R_X86_64_PLT32) { + let site: u64 = base + r.off; + let target: i64 = (base + r.sym.val): i64; + let rel: i64 = (target - site: i64) + r.addend; + patch_u32(l.text + r.off, rel: u32); + } else { + os.write(2, "6l: unsupported reloc kind\n".ptr, 27u64); + l.errs += 1; + };}; + }; + }; + r = r.rnext; + }; + return l.errs; +}; diff --git a/selfhost/cmd/6l/sym.ww b/selfhost/cmd/6l/sym.ww new file mode 100644 index 00000000..41051c4d --- /dev/null +++ b/selfhost/cmd/6l/sym.ww @@ -0,0 +1,75 @@ +// selfhost/cmd/6l/sym.ww — port of cmd/6l/sym.c. +// +// Linker symbol table. Singly-linked list, usually a few hundred +// entries; hashing isn't worth it yet. + +use mem; + +type lsym = struct { + name: str, + val: u64, // offset within combined .text once linked + defined: i32, // 1 if some lobj defines this symbol + owner: *lobj, + idx_in_owner: i32, + snext: *lsym, +}; + +type lrel = struct { + off: u64, // offset within combined .text + kind: i32, // R_X86_64_* + sym: *lsym, + addend: i64, + rnext: *lrel, +}; + +type lobj = struct { + path: str, + buf: *u8, // object bytes + len: u64, + text_off: u64, // offset of .text in combined output + text_size: u64, + onext: *lobj, +}; + +type lnk = struct { + a: *arena, + objs: *lobj, + syms: *lsym, + rels: *lrel, + text: *u8, // combined .text + textcap: u64, + textlen: u64, + errs: i32, +}; + +fn streq(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; +}; + +export fn l_intern(l: *lnk, name: str) *lsym = { + let s: *lsym = l.syms; + for (s != nil) { + if (streq(s.name, name)) { return s; }; + s = s.snext; + }; + let n: *lsym = amalloc(l.a, 64u64): *lsym; + n.name = name; + n.snext = l.syms; + l.syms = n; + return n; +}; + +export fn l_lookup(l: *lnk, name: str) *lsym = { + let s: *lsym = l.syms; + for (s != nil) { + if (streq(s.name, name)) { return s; }; + s = s.snext; + }; + return nil; +}; diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww new file mode 100644 index 00000000..53679a3c --- /dev/null +++ b/selfhost/cmd/ww/main.combined.ww @@ -0,0 +1,975 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_ACCESS: i64 = 21; +def SYS_GETPID: i64 = 39; +def SYS_FORK: i64 = 57; +def SYS_EXECVE: i64 = 59; +def SYS_EXIT: i64 = 60; +def SYS_WAIT4: i64 = 61; +def SYS_UNLINK: i64 = 87; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// ---- process and filesystem helpers used by the `ww` driver ---------- + +// access(2): returns 0 if the file is reachable, negative errno +// otherwise. mode is the bitset described in (F_OK=0). +export fn access(path: *u8, mode: i32) i32 = { + return syscall2(SYS_ACCESS, path: i64, mode: i64): i32; +}; + +// unlink(2). +export fn unlink(path: *u8) i32 = { + return syscall1(SYS_UNLINK, path: i64): i32; +}; + +// getpid(2). Used by the driver to mint unique scratch paths. +export fn getpid() i32 = { + return syscall0(SYS_GETPID): i32; +}; + +// fork(2): 0 in the child, child pid in the parent, negative errno +// on failure. +export fn fork() i32 = { + return syscall0(SYS_FORK): i32; +}; + +// execve(2): on success, does not return. +export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { + return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32; +}; + +// wait4(2): wait for `pid` (or any child if -1), store status in +// `*status_out`, return the pid that ended (or negative errno). +export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = { + return syscall4(SYS_WAIT4, pid: i64, status_out: i64, + options: i64, rusage: i64): i32; +}; + +// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c. +// +// Bump arena allocator. Backed by the runtime page allocator +// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the +// current chunk runs out we link a fresh one. Freeing the arena +// unmaps the chain. +// +// Memory handed out is 16-byte aligned. The C version under +// cmd/wwc/ is retained until the three-stage bootstrap diffs clean. + +use os; + +def ALIGN: u64 = 16u64; +def INIT_CHUNK: u64 = 65536u64; +def MAX_CHUNK: u64 = 4194304u64; +def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, + next: *arena, + total: u64, +}; + +fn roundup(n: u64, a: u64) u64 = { + return (n + a - 1u64) & ~(a - 1u64); +}; + +export fn newarena() *arena = { + let a: *arena = os.alloc(ARENA_SZ): *arena; + a.buf = os.alloc(INIT_CHUNK): *u8; + a.off = 0u64; + a.cap = INIT_CHUNK; + a.next = nil; + a.total = 0u64; + return a; +}; + +// Grow: link a fresh chunk in front of the head. We push the old +// chunk into `next` so the head always describes the current bump +// region. Chunk size doubles up to MAX_CHUNK. +fn grow(a: *arena, need: u64) bool = { + let want: u64 = a.cap * 2u64; + if (want < need) { want = need; }; + if (want > MAX_CHUNK) { want = MAX_CHUNK; }; + if (want < need) { return false; }; // single allocation too big + + let old: *arena = os.alloc(ARENA_SZ): *arena; + old.buf = a.buf; + old.off = a.off; + old.cap = a.cap; + old.next = a.next; + old.total = 0u64; + + a.buf = os.alloc(want): *u8; + a.off = 0u64; + a.cap = want; + a.next = old; + return true; +}; + +export fn amalloc(a: *arena, n: u64) *void = { + let need: u64 = roundup(n, ALIGN); + if (need > a.cap - a.off) { + if (!grow(a, need)) { return nil; }; + }; + let p: *u8 = a.buf + a.off; + a.off += need; + a.total += need; + // Zero the region. Plan 9 amalloc zeroes; we mirror that here so + // the checker can assume freshly allocated nodes start at 0. + let i: u64 = 0u64; + for (i < need) { + p[i] = 0u8; + i += 1u64; + }; + return p: *void; +}; + +// astrndup — copy `n` bytes into the arena and produce a NUL-terminated +// view. Returns a `str` whose ptr is arena-owned and whose len is `n` +// (the trailing NUL is past `len`, so callers reading exactly n bytes +// see no padding). Used by the lexer to capture token text. +export fn astrndup(a: *arena, src: *u8, n: u64) str = { + let p: *u8 = amalloc(a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { + p[i] = src[i]; + i += 1u64; + }; + p[n] = 0u8; + let r: str; + r.ptr = p; + r.len = n: i32; + return r; +}; + +export fn freearena(a: *arena) void = { + for (a != nil) { + let next: *arena = a.next; + os.free(a.buf: *void, a.cap); + os.free(a: *void, ARENA_SZ); + a = next; + }; +}; + +// selfhost/cmd/ww/main.ww — port of cmd/ww/main.c. +// +// The user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue: +// +// ww build foo.ww → 6c foo.ww > foo.s ; 6a foo.s > foo.o ; +// 6l -o foo foo.o libwwrt.a +// ww run foo.ww → build then exec +// ww version → print version +// +// Tool paths default to siblings of $0 so a fresh build runs out of +// out/bin/. Env-var overrides (WW_6C / WW_6A / WW_6L / WW_LIB) are +// not yet supported in this port; the bootstrap doesn't need them. + +use os; +use mem; + +// All path/string scratch buffers go on the runtime page allocator. +// One page is plenty for any path we build. +def PATH_MAX: u64 = 4096u64; +def CMD_MAX: u64 = 8192u64; + +// ---- C-string helpers -------------------------------------------------- + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +fn cstreq(a: *u8, b: *u8) bool = { + let i: u64 = 0u64; + for (a[i] == b[i]) { + if (a[i] == 0u8) { return true; }; + i += 1u64; + }; + return false; +}; + +// cstreq_lit — compare a NUL-terminated *u8 to a ww string literal. +fn cstreq_lit(a: *u8, lit: str) bool = { + let n: i32 = lit.len; + let i: i32 = 0; + for (i < n) { + if (a[i] != lit[i]) { return false; }; + i += 1; + }; + return a[n] == 0u8; +}; + +// startswith — does a have b as a prefix? +fn cstr_startswith(a: *u8, b: *u8) bool = { + let i: u64 = 0u64; + for (b[i] != 0u8) { + if (a[i] != b[i]) { return false; }; + i += 1u64; + }; + return true; +}; + +// memcpy +fn bytecpy(dst: *u8, src: *u8, n: u64) void = { + let i: u64 = 0u64; + for (i < n) { + dst[i] = src[i]; + i += 1u64; + }; +}; + +// Copy a NUL-terminated *u8 into dst starting at off; return the new +// offset (without writing a NUL). +fn cstr_into(dst: *u8, off: u64, src: *u8) u64 = { + let i: u64 = 0u64; + for (src[i] != 0u8) { + dst[off + i] = src[i]; + i += 1u64; + }; + return off + i; +}; + +// Same, but for a ww `str` (no NUL on the source side; we copy len bytes). +fn str_into(dst: *u8, off: u64, src: str) u64 = { + let n: i32 = src.len; + let i: i32 = 0; + for (i < n) { + let iu: u64 = i: u64; + dst[off + iu] = src[i]; + i += 1; + }; + let nu: u64 = n: u64; + return off + nu; +}; + +// Write a single byte, return new offset. +fn byte_into(dst: *u8, off: u64, c: u8) u64 = { + dst[off] = c; + return off + 1u64; +}; + +// NUL-terminate at off and return the same off (handy when passing the +// buffer to a syscall that expects a C-string). +fn cstr_seal(dst: *u8, off: u64) void = { + dst[off] = 0u8; +}; + +// ---- Tool-path resolution --------------------------------------------- + +// dirname-equivalent: copy argv[0] up to (but not including) the last +// '/' into dst, NUL-terminated. If no slash, write ".". +fn self_dir_into(dst: *u8, dstsz: u64, argv0: *u8) void = { + let n: u64 = cstrlen(argv0); + let cut: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + if (argv0[i] == 47u8) { cut = i; }; // '/' + i += 1u64; + }; + if (cut == 0u64) { + dst[0u64] = 46u8; // '.' + dst[1u64] = 0u8; + return; + }; + if (cut + 1u64 >= dstsz) { cut = dstsz - 2u64; }; + bytecpy(dst, argv0, cut); + dst[cut] = 0u8; +}; + +// Build "$dir/$name" (NUL-terminated) into a fresh page-sized buffer. +fn join_path(dir: *u8, name: *u8) *u8 = { + let buf: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = cstr_into(buf, 0u64, dir); + off = byte_into(buf, off, 47u8); + off = cstr_into(buf, off, name); + cstr_seal(buf, off); + return buf; +}; + +// Same, but the second component is a ww `str` literal. +fn join_path_lit(dir: *u8, name: str) *u8 = { + let buf: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = cstr_into(buf, 0u64, dir); + off = byte_into(buf, off, 47u8); + off = str_into(buf, off, name); + cstr_seal(buf, off); + return buf; +}; + +// ---- Subprocess plumbing ---------------------------------------------- + +// proc_run — fork, execve `path` with `argv` (NULL-terminated), wait. +// Returns 0 on clean exit-0, 1 on any non-zero exit or signal kill, +// -1 on fork/wait failure. +fn proc_run(path: *u8, argv: **u8) i32 = { + let pid: i32 = os.fork(); + if (pid < 0) { + os.write(2, "ww: fork failed\n".ptr, 16u64); + return -1; + }; + if (pid == 0) { + os.execve(path, argv, nil: **u8); + os.write(2, "ww: execve failed\n".ptr, 18u64); + os.exit(127); + }; + let status: i32 = 0; + let r: i32 = os.wait4(pid, &status, 0i32, nil: *void); + if (r < 0) { + os.write(2, "ww: wait4 failed\n".ptr, 17u64); + return -1; + }; + // Linux wait status: low byte = signal (0 if exited cleanly), + // next byte = exit code. + if ((status & 127i32) != 0) { return 1; }; + let code: i32 = (status >> 8i32) & 255i32; + if (code != 0) { return 1; }; + return 0; +}; + +// ---- `use` resolution + source concatenation -------------------------- +// +// Recursive expansion: for each `use IDENT;` we find at the top of +// `path`, resolve via the colon-separated `dirs`, expand the imported +// file first, then append our own bytes. Already-visited paths are +// skipped (linear scan; typical builds visit a handful of modules). + +type strnode = struct { + s: str, + snext: *strnode, +}; + +type expctx = struct { + a: *arena, // arena for path strings + the visited list + out: i32, // fd we're writing the combined source to + dirs: *u8, // ":"-separated search path (NUL-terminated) + visit: *strnode, +}; + +fn visit_seen(c: *expctx, path: str) bool = { + let n: *strnode = c.visit; + for (n != nil) { + if (n.s.len == path.len) { + let i: i32 = 0; + let eq: bool = true; + for (i < path.len) { + if (n.s[i] != path[i]) { eq = false; i = path.len; } + else { i += 1; }; + }; + if (eq) { return true; }; + }; + n = n.snext; + }; + return false; +}; + +fn visit_add(c: *expctx, path: str) void = { + let n: *strnode = amalloc(c.a, 32u64): *strnode; + n.s = path; + n.snext = c.visit; + c.visit = n; +}; + +// Try /.ww then //.ww. Returns NUL-terminated +// arena-resident path if found, else nil. +fn locate_in(a: *arena, dir: *u8, dir_len: u64, name: *u8, name_len: u64) *u8 = { + // candidate 1: /.ww + let buf: *u8 = amalloc(a, PATH_MAX): *u8; + let off: u64 = 0u64; + let i: u64 = 0u64; + for (i < dir_len) { buf[off + i] = dir[i]; i += 1u64; }; + off += dir_len; + buf[off] = 47u8; off += 1u64; // '/' + i = 0u64; + for (i < name_len) { buf[off + i] = name[i]; i += 1u64; }; + off += name_len; + buf[off] = 46u8; off += 1u64; // '.' + buf[off] = 119u8; off += 1u64; // 'w' + buf[off] = 119u8; off += 1u64; // 'w' + buf[off] = 0u8; + if (os.access(buf, 0i32) == 0) { return buf; }; + + // candidate 2: //.ww + let buf2: *u8 = amalloc(a, PATH_MAX): *u8; + off = 0u64; + i = 0u64; + for (i < dir_len) { buf2[off + i] = dir[i]; i += 1u64; }; + off += dir_len; + buf2[off] = 47u8; off += 1u64; + i = 0u64; + for (i < name_len) { buf2[off + i] = name[i]; i += 1u64; }; + off += name_len; + buf2[off] = 47u8; off += 1u64; + i = 0u64; + for (i < name_len) { buf2[off + i] = name[i]; i += 1u64; }; + off += name_len; + buf2[off] = 46u8; off += 1u64; + buf2[off] = 119u8; off += 1u64; + buf2[off] = 119u8; off += 1u64; + buf2[off] = 0u8; + if (os.access(buf2, 0i32) == 0) { return buf2; }; + return nil; +}; + +// Walk a colon-separated dirlist, return first hit or nil. +fn locate_import(a: *arena, dirs: *u8, name: *u8, name_len: u64) *u8 = { + let total: u64 = cstrlen(dirs); + let p: u64 = 0u64; + for (p < total) { + let q: u64 = p; + for (q < total) { + if (dirs[q] == 58u8) { break; }; // ':' + q += 1u64; + }; + let seg_len: u64 = q - p; + if (seg_len > 0u64) { + let hit: *u8 = locate_in(a, dirs + p, seg_len, name, name_len); + if (hit != nil) { return hit; }; + }; + p = q + 1u64; + }; + return nil; +}; + +// ---- file slurp ------------------------------------------------------- + +fn read_all(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let nu: u64 = n: u64; + let buf: *u8 = os.alloc(nu + 1u64): *u8; + let got: i64 = os.readfull(fd, buf, nu); + os.close(fd); + if (got != n) { return nil, 0u64; }; + buf[nu] = 0u8; + return buf, nu; +}; + +fn is_ident_byte(c: u8) bool = { + if (c >= 97u8) { if (c <= 122u8) { return true; }; }; // a..z + if (c >= 65u8) { if (c <= 90u8) { return true; }; }; // A..Z + if (c >= 48u8) { if (c <= 57u8) { return true; }; }; // 0..9 + if (c == 95u8) { return true; }; // _ + if (c == 46u8) { return true; }; // . + return false; +}; + +// Scan one `use IDENT;` line out of [start, end). Returns the start of +// the ident and its length, or (nil, 0) if no `use` here. The caller +// passes a slice of the source: src points at the line start. +fn scan_use(src: *u8, len: u64) (*u8, u64) = { + let i: u64 = 0u64; + // skip leading whitespace + for (i < len) { + if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; }; + i += 1u64; + }; + if (i + 4u64 > len) { return nil, 0u64; }; + if (src[i] != 117u8) { return nil, 0u64; }; // 'u' + if (src[i + 1u64] != 115u8) { return nil, 0u64; }; // 's' + if (src[i + 2u64] != 101u8) { return nil, 0u64; }; // 'e' + let sep: u8 = src[i + 3u64]; + if (sep != 32u8) { if (sep != 9u8) { return nil, 0u64; }; }; + i += 4u64; + for (i < len) { + if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; }; + i += 1u64; + }; + let id_start: u64 = i; + for (i < len) { + if (!is_ident_byte(src[i])) { break; }; + i += 1u64; + }; + let id_len: u64 = i - id_start; + if (id_len == 0u64) { return nil, 0u64; }; + return src + id_start, id_len; +}; + +// Recursively expand `path` into c.out. Imported files are emitted +// before their importer; cycles are broken via the visited set. +fn expand(c: *expctx, path_cs: *u8) void = { + let plen: u64 = cstrlen(path_cs); + let path_str: str = astrndup(c.a, path_cs, plen); + if (visit_seen(c, path_str)) { return; }; + visit_add(c, path_str); + + let bufp: *u8; + let blen: u64; + bufp, blen = read_all(path_cs); + if (bufp == nil) { + os.write(2, "ww: cannot read source\n".ptr, 23u64); + return; + }; + + // Pass 1: scan top-of-file `use X;` lines, recursively expand. + let i: u64 = 0u64; + for (i < blen) { + // Find the end of the current line. + let j: u64 = i; + for (j < blen) { + if (bufp[j] == 10u8) { break; }; // '\n' + j += 1u64; + }; + let id_p: *u8; + let id_n: u64; + id_p, id_n = scan_use(bufp + i, j - i); + if (id_p != nil) { + let ipath: *u8 = locate_import(c.a, c.dirs, id_p, id_n); + if (ipath != nil) { + expand(c, ipath); + }; + }; + i = j + 1u64; + }; + + // Pass 2: emit our own bytes, then a trailing newline. + os.writefull(c.out, bufp, blen); + os.writefull(c.out, "\n".ptr, 1u64); +}; + +// ---- Build pipeline --------------------------------------------------- + +// Strip the trailing ".ww" off `src` (a NUL-terminated path) into +// `stem`, NUL-terminated. If there's no .ww, the stem is the whole +// path. +fn make_stem(stem: *u8, src: *u8) void = { + let n: u64 = cstrlen(src); + let stop: u64 = n; + if (n >= 3u64) { + if (src[n - 3u64] == 46u8) { // '.' + if (src[n - 2u64] == 119u8) { // 'w' + if (src[n - 1u64] == 119u8) { // 'w' + stop = n - 3u64; + }; + }; + }; + }; + let i: u64 = 0u64; + for (i < stop) { stem[i] = src[i]; i += 1u64; }; + stem[stop] = 0u8; +}; + +// Append a literal suffix to `stem` (which already lives in a buffer). +fn append_lit(stem: *u8, suffix: str) *u8 = { + let buf: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = cstr_into(buf, 0u64, stem); + off = str_into(buf, off, suffix); + cstr_seal(buf, off); + return buf; +}; + +// build_one — compile `src` into the executable named `out`. +// self_dir: NUL-terminated dir containing this driver (and 6c/6a/6l) +// src: NUL-terminated path to the .ww file +// out: NUL-terminated desired output path +// incs: NUL-terminated colon-list of -I dirs (may be empty) +fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = { + let a: *arena = newarena(); + + let c6: *u8 = join_path_lit(self_dir, "6c"); + let a6: *u8 = join_path_lit(self_dir, "6a"); + let l6: *u8 = join_path_lit(self_dir, "6l"); + + // Default lib search path: /../../lib + let dotdot_lib: *u8 = os.alloc(PATH_MAX): *u8; + { + let off: u64 = cstr_into(dotdot_lib, 0u64, self_dir); + off = str_into(dotdot_lib, off, "/../../lib"); + cstr_seal(dotdot_lib, off); + }; + + // Compose searchpath: incs + ':' + dotdot_lib (or just dotdot_lib). + let searchpath: *u8 = os.alloc(PATH_MAX * 2u64): *u8; + { + let off: u64 = 0u64; + if (incs[0u64] != 0u8) { + off = cstr_into(searchpath, off, incs); + off = byte_into(searchpath, off, 58u8); // ':' + }; + off = cstr_into(searchpath, off, dotdot_lib); + cstr_seal(searchpath, off); + }; + + // stem, .s, .o, .combined.ww, libwwrt.a + let stem: *u8 = os.alloc(PATH_MAX): *u8; + make_stem(stem, src); + let asmf: *u8 = append_lit(stem, ".s"); + let objf: *u8 = append_lit(stem, ".o"); + let combined: *u8 = append_lit(stem, ".combined.ww"); + + // libwwrt.a path: /../lib/libwwrt.a + let libwwrt: *u8 = os.alloc(PATH_MAX): *u8; + { + let off: u64 = cstr_into(libwwrt, 0u64, self_dir); + off = str_into(libwwrt, off, "/../lib/libwwrt.a"); + cstr_seal(libwwrt, off); + }; + + // Step 1: expand `use`s into the combined file. + let cf: i32 = os.open(combined, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 420i32); // 0o644 + if (cf < 0) { + os.write(2, "ww: cannot open combined\n".ptr, 25u64); + return 1; + }; + { + let c: expctx; + c.a = a; + c.out = cf; + c.dirs = searchpath; + c.visit = nil; + expand(&c, src); + }; + os.close(cf); + + // Step 2: 6c -o .s .combined.ww + { + let argv: **u8 = os.alloc(40u64): **u8; + argv[0] = "6c\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = asmf; + argv[3] = combined; + argv[4] = nil; + if (proc_run(c6, argv) != 0) { + os.write(2, "ww: 6c failed\n".ptr, 14u64); + return 1; + }; + }; + + // Step 3: 6a -o .o .s + { + let argv: **u8 = os.alloc(40u64): **u8; + argv[0] = "6a\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = objf; + argv[3] = asmf; + argv[4] = nil; + if (proc_run(a6, argv) != 0) { + os.write(2, "ww: 6a failed\n".ptr, 14u64); + return 1; + }; + }; + + // Step 4: 6l -o .o libwwrt.a + { + let argv: **u8 = os.alloc(48u64): **u8; + argv[0] = "6l\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = out; + argv[3] = objf; + argv[4] = libwwrt; + argv[5] = nil; + if (proc_run(l6, argv) != 0) { + os.write(2, "ww: 6l failed\n".ptr, 14u64); + return 1; + }; + }; + return 0; +}; + +// ---- Subcommand handlers ---------------------------------------------- + +fn write_usage(fd: i32) void = { + let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build compile module to a static binary\n run build then exec\n version print version and exit\n"; + os.write(fd, s.ptr, s.len: u64); +}; + +fn do_version() i32 = { + os.write(1, "ww 0.0\n".ptr, 7u64); + return 0; +}; + +// Compute the basename of src (without trailing ".ww") into a fresh +// buffer. Used as the default output path for `ww build`. +fn default_out_path(src: *u8) *u8 = { + let n: u64 = cstrlen(src); + let start: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + if (src[i] == 47u8) { start = i + 1u64; }; // '/' + i += 1u64; + }; + let out: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = 0u64; + let j: u64 = start; + for (j < n) { + out[off] = src[j]; + off += 1u64; + j += 1u64; + }; + // Strip ".ww" if present. + if (off >= 3u64) { + if (out[off - 3u64] == 46u8) { + if (out[off - 2u64] == 119u8) { + if (out[off - 1u64] == 119u8) { + off -= 3u64; + }; + }; + }; + }; + cstr_seal(out, off); + return out; +}; + +fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = { + let src: *u8 = nil; + let incs: *u8 = os.alloc(PATH_MAX * 2u64): *u8; + let inc_off: u64 = 0u64; + cstr_seal(incs, 0u64); + + let i: i32 = start; + for (i < argc) { + let p: *u8 = argv[i]; + // -I + if (p[0u64] == 45u8) { + if (p[1u64] == 73u8) { // '-I' + let dir: *u8 = nil; + if (p[2u64] != 0u8) { + dir = p + 2u64; + } else { + if (i + 1 >= argc) { + os.write(2, "ww build: -I needs an argument\n".ptr, 31u64); + return 2; + }; + i += 1; + dir = argv[i]; + }; + if (inc_off > 0u64) { + incs[inc_off] = 58u8; // ':' + inc_off += 1u64; + }; + inc_off = cstr_into(incs, inc_off, dir); + cstr_seal(incs, inc_off); + } else { + // -lLIB silently ignored for now (driver doesn't yet + // pass extra archives to 6l). + if (p[1u64] != 108u8) { + os.write(2, "ww build: unknown flag\n".ptr, 23u64); + return 2; + }; + }; + } else { + if (src == nil) { src = p; }; + }; + i += 1; + }; + + if (src == nil) { + os.write(2, "ww build: missing source\n".ptr, 25u64); + return 2; + }; + let out: *u8 = default_out_path(src); + return build_one(self_dir, src, out, incs); +}; + +// Format the scratch path /tmp/ww_run_ into buf. Returns NUL- +// terminated buf. Pid is folded in decimal manually since we don't +// import strconv. +fn make_run_tmp(buf: *u8) void = { + let off: u64 = 0u64; + off = str_into(buf, off, "/tmp/ww_run_"); + let pid: i32 = os.getpid(); + // itoa for non-negative pid + let dig: [16]u8; + let n: i32 = 0; + if (pid <= 0) { + dig[n] = 48u8; // '0' + n += 1; + } else { + let v: i32 = pid; + for (v > 0) { + dig[n] = ((v % 10) + 48): u8; + n += 1; + v = v / 10; + }; + }; + let k: i32 = n - 1; + for (k >= 0) { + buf[off] = dig[k]; + off += 1u64; + k -= 1; + }; + cstr_seal(buf, off); +}; + +fn do_run(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = { + if (start >= argc) { + os.write(2, "ww run: missing source\n".ptr, 23u64); + return 2; + }; + let tmp: *u8 = os.alloc(PATH_MAX): *u8; + make_run_tmp(tmp); + if (build_one(self_dir, argv[start], tmp, "\0".ptr) != 0) { return 1; }; + let exec_argv: **u8 = os.alloc(16u64): **u8; + exec_argv[0] = tmp; + exec_argv[1] = nil; + let rc: i32 = proc_run(tmp, exec_argv); + os.unlink(tmp); + return rc; +}; + +// ---- Entry ------------------------------------------------------------- + +export fn main(argc: i32, argv: **u8) i32 = { + if (argc < 1) { + write_usage(2); + return 2; + }; + + // self_dir = dirname(argv[0]) + let self_dir: *u8 = os.alloc(PATH_MAX): *u8; + self_dir_into(self_dir, PATH_MAX, argv[0]); + + if (argc < 2) { + write_usage(2); + return 2; + }; + let cmd: *u8 = argv[1]; + if (cstreq_lit(cmd, "-V")) { return do_version(); }; + if (cstreq_lit(cmd, "version")) { return do_version(); }; + if (cstreq_lit(cmd, "-h")) { + write_usage(1); + return 0; + }; + if (cstreq_lit(cmd, "--help")) { + write_usage(1); + return 0; + }; + if (cstreq_lit(cmd, "build")) { + return do_build(self_dir, argv, argc, 2); + }; + if (cstreq_lit(cmd, "run")) { + return do_run(self_dir, argv, argc, 2); + }; + os.write(2, "ww: unknown subcommand\n".ptr, 23u64); + write_usage(2); + return 2; +}; + diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww new file mode 100644 index 00000000..16de55f9 --- /dev/null +++ b/selfhost/cmd/ww/main.ww @@ -0,0 +1,695 @@ +// selfhost/cmd/ww/main.ww — port of cmd/ww/main.c. +// +// The user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue: +// +// ww build foo.ww → 6c foo.ww > foo.s ; 6a foo.s > foo.o ; +// 6l -o foo foo.o libwwrt.a +// ww run foo.ww → build then exec +// ww version → print version +// +// Tool paths default to siblings of $0 so a fresh build runs out of +// out/bin/. Env-var overrides (WW_6C / WW_6A / WW_6L / WW_LIB) are +// not yet supported in this port; the bootstrap doesn't need them. + +use os; +use mem; + +// All path/string scratch buffers go on the runtime page allocator. +// One page is plenty for any path we build. +def PATH_MAX: u64 = 4096u64; +def CMD_MAX: u64 = 8192u64; + +// ---- C-string helpers -------------------------------------------------- + +fn cstrlen(p: *u8) u64 = { + let n: u64 = 0u64; + for (p[n] != 0u8) { n += 1u64; }; + return n; +}; + +fn cstreq(a: *u8, b: *u8) bool = { + let i: u64 = 0u64; + for (a[i] == b[i]) { + if (a[i] == 0u8) { return true; }; + i += 1u64; + }; + return false; +}; + +// cstreq_lit — compare a NUL-terminated *u8 to a ww string literal. +fn cstreq_lit(a: *u8, lit: str) bool = { + let n: i32 = lit.len; + let i: i32 = 0; + for (i < n) { + if (a[i] != lit[i]) { return false; }; + i += 1; + }; + return a[n] == 0u8; +}; + +// startswith — does a have b as a prefix? +fn cstr_startswith(a: *u8, b: *u8) bool = { + let i: u64 = 0u64; + for (b[i] != 0u8) { + if (a[i] != b[i]) { return false; }; + i += 1u64; + }; + return true; +}; + +// memcpy +fn bytecpy(dst: *u8, src: *u8, n: u64) void = { + let i: u64 = 0u64; + for (i < n) { + dst[i] = src[i]; + i += 1u64; + }; +}; + +// Copy a NUL-terminated *u8 into dst starting at off; return the new +// offset (without writing a NUL). +fn cstr_into(dst: *u8, off: u64, src: *u8) u64 = { + let i: u64 = 0u64; + for (src[i] != 0u8) { + dst[off + i] = src[i]; + i += 1u64; + }; + return off + i; +}; + +// Same, but for a ww `str` (no NUL on the source side; we copy len bytes). +fn str_into(dst: *u8, off: u64, src: str) u64 = { + let n: i32 = src.len; + let i: i32 = 0; + for (i < n) { + let iu: u64 = i: u64; + dst[off + iu] = src[i]; + i += 1; + }; + let nu: u64 = n: u64; + return off + nu; +}; + +// Write a single byte, return new offset. +fn byte_into(dst: *u8, off: u64, c: u8) u64 = { + dst[off] = c; + return off + 1u64; +}; + +// NUL-terminate at off and return the same off (handy when passing the +// buffer to a syscall that expects a C-string). +fn cstr_seal(dst: *u8, off: u64) void = { + dst[off] = 0u8; +}; + +// ---- Tool-path resolution --------------------------------------------- + +// dirname-equivalent: copy argv[0] up to (but not including) the last +// '/' into dst, NUL-terminated. If no slash, write ".". +fn self_dir_into(dst: *u8, dstsz: u64, argv0: *u8) void = { + let n: u64 = cstrlen(argv0); + let cut: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + if (argv0[i] == 47u8) { cut = i; }; // '/' + i += 1u64; + }; + if (cut == 0u64) { + dst[0u64] = 46u8; // '.' + dst[1u64] = 0u8; + return; + }; + if (cut + 1u64 >= dstsz) { cut = dstsz - 2u64; }; + bytecpy(dst, argv0, cut); + dst[cut] = 0u8; +}; + +// Build "$dir/$name" (NUL-terminated) into a fresh page-sized buffer. +fn join_path(dir: *u8, name: *u8) *u8 = { + let buf: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = cstr_into(buf, 0u64, dir); + off = byte_into(buf, off, 47u8); + off = cstr_into(buf, off, name); + cstr_seal(buf, off); + return buf; +}; + +// Same, but the second component is a ww `str` literal. +fn join_path_lit(dir: *u8, name: str) *u8 = { + let buf: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = cstr_into(buf, 0u64, dir); + off = byte_into(buf, off, 47u8); + off = str_into(buf, off, name); + cstr_seal(buf, off); + return buf; +}; + +// ---- Subprocess plumbing ---------------------------------------------- + +// proc_run — fork, execve `path` with `argv` (NULL-terminated), wait. +// Returns 0 on clean exit-0, 1 on any non-zero exit or signal kill, +// -1 on fork/wait failure. +fn proc_run(path: *u8, argv: **u8) i32 = { + let pid: i32 = os.fork(); + if (pid < 0) { + os.write(2, "ww: fork failed\n".ptr, 16u64); + return -1; + }; + if (pid == 0) { + os.execve(path, argv, nil: **u8); + os.write(2, "ww: execve failed\n".ptr, 18u64); + os.exit(127); + }; + let status: i32 = 0; + let r: i32 = os.wait4(pid, &status, 0i32, nil: *void); + if (r < 0) { + os.write(2, "ww: wait4 failed\n".ptr, 17u64); + return -1; + }; + // Linux wait status: low byte = signal (0 if exited cleanly), + // next byte = exit code. + if ((status & 127i32) != 0) { return 1; }; + let code: i32 = (status >> 8i32) & 255i32; + if (code != 0) { return 1; }; + return 0; +}; + +// ---- `use` resolution + source concatenation -------------------------- +// +// Recursive expansion: for each `use IDENT;` we find at the top of +// `path`, resolve via the colon-separated `dirs`, expand the imported +// file first, then append our own bytes. Already-visited paths are +// skipped (linear scan; typical builds visit a handful of modules). + +type strnode = struct { + s: str, + snext: *strnode, +}; + +type expctx = struct { + a: *arena, // arena for path strings + the visited list + out: i32, // fd we're writing the combined source to + dirs: *u8, // ":"-separated search path (NUL-terminated) + visit: *strnode, +}; + +fn visit_seen(c: *expctx, path: str) bool = { + let n: *strnode = c.visit; + for (n != nil) { + if (n.s.len == path.len) { + let i: i32 = 0; + let eq: bool = true; + for (i < path.len) { + if (n.s[i] != path[i]) { eq = false; i = path.len; } + else { i += 1; }; + }; + if (eq) { return true; }; + }; + n = n.snext; + }; + return false; +}; + +fn visit_add(c: *expctx, path: str) void = { + let n: *strnode = amalloc(c.a, 32u64): *strnode; + n.s = path; + n.snext = c.visit; + c.visit = n; +}; + +// Try /.ww then //.ww. Returns NUL-terminated +// arena-resident path if found, else nil. +fn locate_in(a: *arena, dir: *u8, dir_len: u64, name: *u8, name_len: u64) *u8 = { + // candidate 1: /.ww + let buf: *u8 = amalloc(a, PATH_MAX): *u8; + let off: u64 = 0u64; + let i: u64 = 0u64; + for (i < dir_len) { buf[off + i] = dir[i]; i += 1u64; }; + off += dir_len; + buf[off] = 47u8; off += 1u64; // '/' + i = 0u64; + for (i < name_len) { buf[off + i] = name[i]; i += 1u64; }; + off += name_len; + buf[off] = 46u8; off += 1u64; // '.' + buf[off] = 119u8; off += 1u64; // 'w' + buf[off] = 119u8; off += 1u64; // 'w' + buf[off] = 0u8; + if (os.access(buf, 0i32) == 0) { return buf; }; + + // candidate 2: //.ww + let buf2: *u8 = amalloc(a, PATH_MAX): *u8; + off = 0u64; + i = 0u64; + for (i < dir_len) { buf2[off + i] = dir[i]; i += 1u64; }; + off += dir_len; + buf2[off] = 47u8; off += 1u64; + i = 0u64; + for (i < name_len) { buf2[off + i] = name[i]; i += 1u64; }; + off += name_len; + buf2[off] = 47u8; off += 1u64; + i = 0u64; + for (i < name_len) { buf2[off + i] = name[i]; i += 1u64; }; + off += name_len; + buf2[off] = 46u8; off += 1u64; + buf2[off] = 119u8; off += 1u64; + buf2[off] = 119u8; off += 1u64; + buf2[off] = 0u8; + if (os.access(buf2, 0i32) == 0) { return buf2; }; + return nil; +}; + +// Walk a colon-separated dirlist, return first hit or nil. +fn locate_import(a: *arena, dirs: *u8, name: *u8, name_len: u64) *u8 = { + let total: u64 = cstrlen(dirs); + let p: u64 = 0u64; + for (p < total) { + let q: u64 = p; + for (q < total) { + if (dirs[q] == 58u8) { break; }; // ':' + q += 1u64; + }; + let seg_len: u64 = q - p; + if (seg_len > 0u64) { + let hit: *u8 = locate_in(a, dirs + p, seg_len, name, name_len); + if (hit != nil) { return hit; }; + }; + p = q + 1u64; + }; + return nil; +}; + +// ---- file slurp ------------------------------------------------------- + +fn read_all(path_cs: *u8) (*u8, u64) = { + let fd: i32 = os.open(path_cs, os.O_RDONLY, 0i32); + if (fd < 0) { return nil, 0u64; }; + let n: i64 = os.filesize(fd); + if (n < 0i64) { os.close(fd); return nil, 0u64; }; + let nu: u64 = n: u64; + let buf: *u8 = os.alloc(nu + 1u64): *u8; + let got: i64 = os.readfull(fd, buf, nu); + os.close(fd); + if (got != n) { return nil, 0u64; }; + buf[nu] = 0u8; + return buf, nu; +}; + +fn is_ident_byte(c: u8) bool = { + if (c >= 97u8) { if (c <= 122u8) { return true; }; }; // a..z + if (c >= 65u8) { if (c <= 90u8) { return true; }; }; // A..Z + if (c >= 48u8) { if (c <= 57u8) { return true; }; }; // 0..9 + if (c == 95u8) { return true; }; // _ + if (c == 46u8) { return true; }; // . + return false; +}; + +// Scan one `use IDENT;` line out of [start, end). Returns the start of +// the ident and its length, or (nil, 0) if no `use` here. The caller +// passes a slice of the source: src points at the line start. +fn scan_use(src: *u8, len: u64) (*u8, u64) = { + let i: u64 = 0u64; + // skip leading whitespace + for (i < len) { + if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; }; + i += 1u64; + }; + if (i + 4u64 > len) { return nil, 0u64; }; + if (src[i] != 117u8) { return nil, 0u64; }; // 'u' + if (src[i + 1u64] != 115u8) { return nil, 0u64; }; // 's' + if (src[i + 2u64] != 101u8) { return nil, 0u64; }; // 'e' + let sep: u8 = src[i + 3u64]; + if (sep != 32u8) { if (sep != 9u8) { return nil, 0u64; }; }; + i += 4u64; + for (i < len) { + if (src[i] != 32u8) { if (src[i] != 9u8) { break; }; }; + i += 1u64; + }; + let id_start: u64 = i; + for (i < len) { + if (!is_ident_byte(src[i])) { break; }; + i += 1u64; + }; + let id_len: u64 = i - id_start; + if (id_len == 0u64) { return nil, 0u64; }; + return src + id_start, id_len; +}; + +// Recursively expand `path` into c.out. Imported files are emitted +// before their importer; cycles are broken via the visited set. +fn expand(c: *expctx, path_cs: *u8) void = { + let plen: u64 = cstrlen(path_cs); + let path_str: str = astrndup(c.a, path_cs, plen); + if (visit_seen(c, path_str)) { return; }; + visit_add(c, path_str); + + let bufp: *u8; + let blen: u64; + bufp, blen = read_all(path_cs); + if (bufp == nil) { + os.write(2, "ww: cannot read source\n".ptr, 23u64); + return; + }; + + // Pass 1: scan top-of-file `use X;` lines, recursively expand. + let i: u64 = 0u64; + for (i < blen) { + // Find the end of the current line. + let j: u64 = i; + for (j < blen) { + if (bufp[j] == 10u8) { break; }; // '\n' + j += 1u64; + }; + let id_p: *u8; + let id_n: u64; + id_p, id_n = scan_use(bufp + i, j - i); + if (id_p != nil) { + let ipath: *u8 = locate_import(c.a, c.dirs, id_p, id_n); + if (ipath != nil) { + expand(c, ipath); + }; + }; + i = j + 1u64; + }; + + // Pass 2: emit our own bytes, then a trailing newline. + os.writefull(c.out, bufp, blen); + os.writefull(c.out, "\n".ptr, 1u64); +}; + +// ---- Build pipeline --------------------------------------------------- + +// Strip the trailing ".ww" off `src` (a NUL-terminated path) into +// `stem`, NUL-terminated. If there's no .ww, the stem is the whole +// path. +fn make_stem(stem: *u8, src: *u8) void = { + let n: u64 = cstrlen(src); + let stop: u64 = n; + if (n >= 3u64) { + if (src[n - 3u64] == 46u8) { // '.' + if (src[n - 2u64] == 119u8) { // 'w' + if (src[n - 1u64] == 119u8) { // 'w' + stop = n - 3u64; + }; + }; + }; + }; + let i: u64 = 0u64; + for (i < stop) { stem[i] = src[i]; i += 1u64; }; + stem[stop] = 0u8; +}; + +// Append a literal suffix to `stem` (which already lives in a buffer). +fn append_lit(stem: *u8, suffix: str) *u8 = { + let buf: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = cstr_into(buf, 0u64, stem); + off = str_into(buf, off, suffix); + cstr_seal(buf, off); + return buf; +}; + +// build_one — compile `src` into the executable named `out`. +// self_dir: NUL-terminated dir containing this driver (and 6c/6a/6l) +// src: NUL-terminated path to the .ww file +// out: NUL-terminated desired output path +// incs: NUL-terminated colon-list of -I dirs (may be empty) +fn build_one(self_dir: *u8, src: *u8, out: *u8, incs: *u8) i32 = { + let a: *arena = newarena(); + + let c6: *u8 = join_path_lit(self_dir, "6c"); + let a6: *u8 = join_path_lit(self_dir, "6a"); + let l6: *u8 = join_path_lit(self_dir, "6l"); + + // Default lib search path: /../../lib + let dotdot_lib: *u8 = os.alloc(PATH_MAX): *u8; + { + let off: u64 = cstr_into(dotdot_lib, 0u64, self_dir); + off = str_into(dotdot_lib, off, "/../../lib"); + cstr_seal(dotdot_lib, off); + }; + + // Compose searchpath: incs + ':' + dotdot_lib (or just dotdot_lib). + let searchpath: *u8 = os.alloc(PATH_MAX * 2u64): *u8; + { + let off: u64 = 0u64; + if (incs[0u64] != 0u8) { + off = cstr_into(searchpath, off, incs); + off = byte_into(searchpath, off, 58u8); // ':' + }; + off = cstr_into(searchpath, off, dotdot_lib); + cstr_seal(searchpath, off); + }; + + // stem, .s, .o, .combined.ww, libwwrt.a + let stem: *u8 = os.alloc(PATH_MAX): *u8; + make_stem(stem, src); + let asmf: *u8 = append_lit(stem, ".s"); + let objf: *u8 = append_lit(stem, ".o"); + let combined: *u8 = append_lit(stem, ".combined.ww"); + + // libwwrt.a path: /../lib/libwwrt.a + let libwwrt: *u8 = os.alloc(PATH_MAX): *u8; + { + let off: u64 = cstr_into(libwwrt, 0u64, self_dir); + off = str_into(libwwrt, off, "/../lib/libwwrt.a"); + cstr_seal(libwwrt, off); + }; + + // Step 1: expand `use`s into the combined file. + let cf: i32 = os.open(combined, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 420i32); // 0o644 + if (cf < 0) { + os.write(2, "ww: cannot open combined\n".ptr, 25u64); + return 1; + }; + { + let c: expctx; + c.a = a; + c.out = cf; + c.dirs = searchpath; + c.visit = nil; + expand(&c, src); + }; + os.close(cf); + + // Step 2: 6c -o .s .combined.ww + { + let argv: **u8 = os.alloc(40u64): **u8; + argv[0] = "6c\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = asmf; + argv[3] = combined; + argv[4] = nil; + if (proc_run(c6, argv) != 0) { + os.write(2, "ww: 6c failed\n".ptr, 14u64); + return 1; + }; + }; + + // Step 3: 6a -o .o .s + { + let argv: **u8 = os.alloc(40u64): **u8; + argv[0] = "6a\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = objf; + argv[3] = asmf; + argv[4] = nil; + if (proc_run(a6, argv) != 0) { + os.write(2, "ww: 6a failed\n".ptr, 14u64); + return 1; + }; + }; + + // Step 4: 6l -o .o libwwrt.a + { + let argv: **u8 = os.alloc(48u64): **u8; + argv[0] = "6l\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = out; + argv[3] = objf; + argv[4] = libwwrt; + argv[5] = nil; + if (proc_run(l6, argv) != 0) { + os.write(2, "ww: 6l failed\n".ptr, 14u64); + return 1; + }; + }; + return 0; +}; + +// ---- Subcommand handlers ---------------------------------------------- + +fn write_usage(fd: i32) void = { + let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build compile module to a static binary\n run build then exec\n version print version and exit\n"; + os.write(fd, s.ptr, s.len: u64); +}; + +fn do_version() i32 = { + os.write(1, "ww 0.0\n".ptr, 7u64); + return 0; +}; + +// Compute the basename of src (without trailing ".ww") into a fresh +// buffer. Used as the default output path for `ww build`. +fn default_out_path(src: *u8) *u8 = { + let n: u64 = cstrlen(src); + let start: u64 = 0u64; + let i: u64 = 0u64; + for (i < n) { + if (src[i] == 47u8) { start = i + 1u64; }; // '/' + i += 1u64; + }; + let out: *u8 = os.alloc(PATH_MAX): *u8; + let off: u64 = 0u64; + let j: u64 = start; + for (j < n) { + out[off] = src[j]; + off += 1u64; + j += 1u64; + }; + // Strip ".ww" if present. + if (off >= 3u64) { + if (out[off - 3u64] == 46u8) { + if (out[off - 2u64] == 119u8) { + if (out[off - 1u64] == 119u8) { + off -= 3u64; + }; + }; + }; + }; + cstr_seal(out, off); + return out; +}; + +fn do_build(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = { + let src: *u8 = nil; + let incs: *u8 = os.alloc(PATH_MAX * 2u64): *u8; + let inc_off: u64 = 0u64; + cstr_seal(incs, 0u64); + + let i: i32 = start; + for (i < argc) { + let p: *u8 = argv[i]; + // -I + if (p[0u64] == 45u8) { + if (p[1u64] == 73u8) { // '-I' + let dir: *u8 = nil; + if (p[2u64] != 0u8) { + dir = p + 2u64; + } else { + if (i + 1 >= argc) { + os.write(2, "ww build: -I needs an argument\n".ptr, 31u64); + return 2; + }; + i += 1; + dir = argv[i]; + }; + if (inc_off > 0u64) { + incs[inc_off] = 58u8; // ':' + inc_off += 1u64; + }; + inc_off = cstr_into(incs, inc_off, dir); + cstr_seal(incs, inc_off); + } else { + // -lLIB silently ignored for now (driver doesn't yet + // pass extra archives to 6l). + if (p[1u64] != 108u8) { + os.write(2, "ww build: unknown flag\n".ptr, 23u64); + return 2; + }; + }; + } else { + if (src == nil) { src = p; }; + }; + i += 1; + }; + + if (src == nil) { + os.write(2, "ww build: missing source\n".ptr, 25u64); + return 2; + }; + let out: *u8 = default_out_path(src); + return build_one(self_dir, src, out, incs); +}; + +// Format the scratch path /tmp/ww_run_ into buf. Returns NUL- +// terminated buf. Pid is folded in decimal manually since we don't +// import strconv. +fn make_run_tmp(buf: *u8) void = { + let off: u64 = 0u64; + off = str_into(buf, off, "/tmp/ww_run_"); + let pid: i32 = os.getpid(); + // itoa for non-negative pid + let dig: [16]u8; + let n: i32 = 0; + if (pid <= 0) { + dig[n] = 48u8; // '0' + n += 1; + } else { + let v: i32 = pid; + for (v > 0) { + dig[n] = ((v % 10) + 48): u8; + n += 1; + v = v / 10; + }; + }; + let k: i32 = n - 1; + for (k >= 0) { + buf[off] = dig[k]; + off += 1u64; + k -= 1; + }; + cstr_seal(buf, off); +}; + +fn do_run(self_dir: *u8, argv: **u8, argc: i32, start: i32) i32 = { + if (start >= argc) { + os.write(2, "ww run: missing source\n".ptr, 23u64); + return 2; + }; + let tmp: *u8 = os.alloc(PATH_MAX): *u8; + make_run_tmp(tmp); + if (build_one(self_dir, argv[start], tmp, "\0".ptr) != 0) { return 1; }; + let exec_argv: **u8 = os.alloc(16u64): **u8; + exec_argv[0] = tmp; + exec_argv[1] = nil; + let rc: i32 = proc_run(tmp, exec_argv); + os.unlink(tmp); + return rc; +}; + +// ---- Entry ------------------------------------------------------------- + +export fn main(argc: i32, argv: **u8) i32 = { + if (argc < 1) { + write_usage(2); + return 2; + }; + + // self_dir = dirname(argv[0]) + let self_dir: *u8 = os.alloc(PATH_MAX): *u8; + self_dir_into(self_dir, PATH_MAX, argv[0]); + + if (argc < 2) { + write_usage(2); + return 2; + }; + let cmd: *u8 = argv[1]; + if (cstreq_lit(cmd, "-V")) { return do_version(); }; + if (cstreq_lit(cmd, "version")) { return do_version(); }; + if (cstreq_lit(cmd, "-h")) { + write_usage(1); + return 0; + }; + if (cstreq_lit(cmd, "--help")) { + write_usage(1); + return 0; + }; + if (cstreq_lit(cmd, "build")) { + return do_build(self_dir, argv, argc, 2); + }; + if (cstreq_lit(cmd, "run")) { + return do_run(self_dir, argv, argc, 2); + }; + os.write(2, "ww: unknown subcommand\n".ptr, 23u64); + write_usage(2); + return 2; +}; diff --git a/selfhost/cmd/wwc/ast.ww b/selfhost/cmd/wwc/ast.ww new file mode 100644 index 00000000..6fa28bd4 --- /dev/null +++ b/selfhost/cmd/wwc/ast.ww @@ -0,0 +1,335 @@ +// selfhost/cmd/wwc/ast.ww — port of cmd/wwc/ast.c (Node defs + printer). +// +// Status: AST printer is fully ported. Constructor `newnode` is here. +// The parser (parse.ww) is currently minimal — see its file header. +// +// Calling-convention shim: same as tok/lex — `node` is too big to pass +// by value (8 *node pointers + 2 strs + a few ints), so callers always +// hand around `*node`. Only `newnode` allocates and returns a *node. + +use os; +use strconv; +use mem; +use tok; + +// ---- Nkind ------------------------------------------------------------ +// +// Mirror of cmd/wwc/ww.h Nkind. Values must stay numerically equal so +// the AST diff probe in 990_selfhost works. + +def N_NONE: i32 = 0; + +def N_INTLIT: i32 = 1; +def N_FLOATLIT: i32 = 2; +def N_STRLIT: i32 = 3; +def N_RUNELIT: i32 = 4; +def N_TRUE: i32 = 5; +def N_FALSE: i32 = 6; +def N_NIL: i32 = 7; +def N_IDENT: i32 = 8; + +def N_BIN: i32 = 9; +def N_UN: i32 = 10; +def N_CALL: i32 = 11; +def N_INDEX: i32 = 12; +def N_DOT: i32 = 13; +def N_CAST: i32 = 14; +def N_STRUCTLIT:i32 = 15; +def N_ARRLIT: i32 = 16; +def N_FIELD: i32 = 17; +def N_ASSIGN: i32 = 18; +def N_ALLOC: i32 = 19; +def N_FREE: i32 = 20; +def N_RECV: i32 = 21; +def N_SLICE: i32 = 22; +def N_SPREAD: i32 = 23; + +def N_BLOCK: i32 = 24; +def N_EXPRSTMT: i32 = 25; +def N_LET: i32 = 26; +def N_RETURN: i32 = 27; +def N_IF: i32 = 28; +def N_FOR: i32 = 29; +def N_FORRANGE: i32 = 30; +def N_DEFER: i32 = 31; +def N_BREAK: i32 = 32; +def N_CONTINUE: i32 = 33; +def N_SWITCH: i32 = 34; +def N_CASE: i32 = 35; + +def N_FILE: i32 = 36; +def N_USE: i32 = 37; +def N_DEF: i32 = 38; +def N_TYPEDECL: i32 = 39; +def N_FNDECL: i32 = 40; +def N_PARAM: i32 = 41; + +def N_TNAME: i32 = 42; +def N_TPTR: i32 = 43; +def N_TSLICE: i32 = 44; +def N_TARRAY: i32 = 45; +def N_TFN: i32 = 46; +def N_TSTRUCT: i32 = 47; +def N_TFIELD: i32 = 48; +def N_TCHAN: i32 = 49; + +def N_ATTR: i32 = 50; +def N_TTUPLE: i32 = 51; +def N_TTAGGED: i32 = 52; +def N_TUPLE: i32 = 53; +def N_MATCH: i32 = 54; +def N_MCASE: i32 = 55; +def N_TRYPROP: i32 = 56; +def N_TRYUNW: i32 = 57; +def N_MLET: i32 = 58; +def N_MASSIGN: i32 = 59; + +def N_LAST: i32 = 60; + +// ---- Node ------------------------------------------------------------- + +type node = struct { + kind: i32, + file: str, + line: i32, + col: i32, + op: i32, // for N_BIN / N_UN / N_ASSIGN + str: str, + uval: u64, + fval: f64, + lhs: *node, + rhs: *node, + cond: *node, + body: *node, + els: *node, + list: *node, + next: *node, + attr: *node, + exported: i32, // bool — `export` keyword present + type_: *void, // filled in by checker; type.ww treats it as *tinfo + tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) +}; + +export fn newnode(a: *arena, k: i32, file: str, line: i32, col: i32) *node = { + let n: *node = amalloc(a, 192u64): *node; // 192 ≥ struct size + n.kind = k; + n.file = file; + n.line = line; + n.col = col; + return n; +}; + +// ---- printer ---------------------------------------------------------- + +fn nkname(k: i32) str = { + if (k == N_NONE) { return "none"; }; + if (k == N_INTLIT) { return "int"; }; + if (k == N_FLOATLIT) { return "float"; }; + if (k == N_STRLIT) { return "str"; }; + if (k == N_RUNELIT) { return "rune"; }; + if (k == N_TRUE) { return "true"; }; + if (k == N_FALSE) { return "false"; }; + if (k == N_NIL) { return "nil"; }; + if (k == N_IDENT) { return "id"; }; + if (k == N_BIN) { return "bin"; }; + if (k == N_UN) { return "un"; }; + if (k == N_CALL) { return "call"; }; + if (k == N_INDEX) { return "index"; }; + if (k == N_DOT) { return "dot"; }; + if (k == N_CAST) { return "cast"; }; + if (k == N_STRUCTLIT) { return "structlit"; }; + if (k == N_ARRLIT) { return "arrlit"; }; + if (k == N_FIELD) { return "field"; }; + if (k == N_ASSIGN) { return "assign"; }; + if (k == N_ALLOC) { return "alloc"; }; + if (k == N_FREE) { return "free"; }; + if (k == N_RECV) { return "recv"; }; + if (k == N_SLICE) { return "slice"; }; + if (k == N_SPREAD) { return "spread"; }; + if (k == N_BLOCK) { return "block"; }; + if (k == N_EXPRSTMT) { return "exprstmt"; }; + if (k == N_LET) { return "let"; }; + if (k == N_RETURN) { return "return"; }; + if (k == N_IF) { return "if"; }; + if (k == N_FOR) { return "for"; }; + if (k == N_FORRANGE) { return "forrange"; }; + if (k == N_DEFER) { return "defer"; }; + if (k == N_BREAK) { return "break"; }; + if (k == N_CONTINUE) { return "continue"; }; + if (k == N_SWITCH) { return "switch"; }; + if (k == N_CASE) { return "case"; }; + if (k == N_FILE) { return "file"; }; + if (k == N_USE) { return "use"; }; + if (k == N_DEF) { return "def"; }; + if (k == N_TYPEDECL) { return "typedecl"; }; + if (k == N_FNDECL) { return "fn"; }; + if (k == N_PARAM) { return "param"; }; + if (k == N_TNAME) { return "tname"; }; + if (k == N_TPTR) { return "tptr"; }; + if (k == N_TSLICE) { return "tslice"; }; + if (k == N_TARRAY) { return "tarray"; }; + if (k == N_TFN) { return "tfn"; }; + if (k == N_TSTRUCT) { return "tstruct"; }; + if (k == N_TFIELD) { return "tfield"; }; + if (k == N_TCHAN) { return "tchan"; }; + if (k == N_ATTR) { return "attr"; }; + if (k == N_TTUPLE) { return "ttuple"; }; + if (k == N_TTAGGED) { return "ttagged"; }; + if (k == N_TUPLE) { return "tuple"; }; + if (k == N_MATCH) { return "match"; }; + if (k == N_MCASE) { return "mcase"; }; + if (k == N_TRYPROP) { return "tryprop"; }; + if (k == N_TRYUNW) { return "tryunw"; }; + if (k == N_MLET) { return "mlet"; }; + if (k == N_MASSIGN) { return "massign"; }; + if (k == N_LAST) { return "last"; }; + return "?"; +}; + +fn ind(fd: i32, d: i32) void = { + let i: i32 = 0; + for (i < d) { + os.write(fd, " ".ptr, 2u64); + i += 1; + }; +}; + +fn putc1(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn putq(fd: i32, s: str) void = { + putc1(fd, 34u8); // '"' + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c == 34u8) { // '"' + os.write(fd, "\\\"".ptr, 2u64); + } else { if (c == 92u8) { // '\\' + os.write(fd, "\\\\".ptr, 2u64); + } else { if (c == 10u8) { // '\n' + os.write(fd, "\\n".ptr, 2u64); + } else { if (c == 9u8) { // '\t' + os.write(fd, "\\t".ptr, 2u64); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + putc1(fd, c); + };};};};}; + i += 1; + }; + putc1(fd, 34u8); +}; + +fn pr(fd: i32, n: *node, d: i32) void = { + if (n == nil) { + ind(fd, d); + os.write(fd, "()\n".ptr, 3u64); + return; + }; + ind(fd, d); + putc1(fd, 40u8); // '(' + let nm: str = nkname(n.kind); + os.write(fd, nm.ptr, nm.len: u64); + + if (n.kind == N_INTLIT) { + putc1(fd, 32u8); + let buf: [32]u8; + let m: i32 = strconv.u64toa(buf[0:32], n.uval); + os.write(fd, buf.ptr, m: u64); + } else { if (n.kind == N_RUNELIT) { + putc1(fd, 32u8); + let buf: [32]u8; + let m: i32 = strconv.u64toa(buf[0:32], n.uval); + os.write(fd, buf.ptr, m: u64); + } else { if ( + n.kind == N_STRLIT || + n.kind == N_IDENT || + n.kind == N_USE || + n.kind == N_DOT || + n.kind == N_DEF || + n.kind == N_TYPEDECL || + n.kind == N_FNDECL || + n.kind == N_PARAM || + n.kind == N_LET || + n.kind == N_TNAME || + n.kind == N_TFIELD || + n.kind == N_FIELD || + n.kind == N_ATTR + ) { + // Match C ast.c: print the str field whenever it's non-nil, + // even if its length is zero (e.g. an empty STRLIT prints + // `(str ""`). + let s: str = n.str; + if (s.ptr != nil) { + putc1(fd, 32u8); + putq(fd, s); + }; + } else { if ( + n.kind == N_BIN || + n.kind == N_UN || + n.kind == N_ASSIGN + ) { + putc1(fd, 32u8); + let on: str = tokname(n.op); + os.write(fd, on.ptr, on.len: u64); + };};};}; + + if (n.kind == N_FNDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == N_DEF) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == N_TYPEDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + putc1(fd, 10u8); // '\n' + + if (n.attr != nil) { + ind(fd, d + 1); + os.write(fd, "(@\n".ptr, 3u64); + let m: *node = n.attr; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; + if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; + if (n.cond != nil) { pr(fd, n.cond, d + 1); }; + if (n.body != nil) { pr(fd, n.body, d + 1); }; + if (n.els != nil) { pr(fd, n.els, d + 1); }; + if (n.list != nil) { + ind(fd, d + 1); + os.write(fd, "(list\n".ptr, 6u64); + let m: *node = n.list; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + ind(fd, d); + os.write(fd, ")\n".ptr, 2u64); +}; + +export fn astprint(fd: i32, n: *node) void = { + pr(fd, n, 0); +}; diff --git a/selfhost/cmd/wwc/cgen.ww b/selfhost/cmd/wwc/cgen.ww new file mode 100644 index 00000000..76239d4c --- /dev/null +++ b/selfhost/cmd/wwc/cgen.ww @@ -0,0 +1,2868 @@ +// selfhost/cmd/wwc/cgen.ww — port of cmd/6c/cgen.c. +// +// Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c` +// producing byte-identical output to C-side `6c` for the same source, +// then by assembling + linking + running the result. +// +// Current coverage: +// - decls: N_FILE, N_FNDECL (params, frame for locals, prologue +// + dual-epilogue suppression; FFI body-less fn skipped) +// - stmts: N_BLOCK, N_RETURN, N_EXPRSTMT, N_LET (no init), +// N_LET (int-literal / ident / call / N_BIN init), +// N_IF (with optional else), N_FOR (cond-only and full +// init/cond/post), N_BREAK, N_CONTINUE +// - exprs: N_INTLIT, N_IDENT (local/param), N_BIN with full op +// coverage (+/-/*/// %, &/|/^, <>, comparisons with +// signed-vs-unsigned dispatch, &&/||), N_UN (- ! ~ & *), +// N_CALL (recursive R-to-L push, pop into argregs L-to-R), +// N_ASSIGN to local idents (plain and compound +=/-=) +// +// Type info is shallow — frame slots are 8 bytes per local, all loads +// /stores are MOVQ. Programs that mix i8/i32/i64 locals work but spill +// 8 bytes per local. Float, str, slice, struct, match, defer, alloc, +// tagged-union return — none of those are wired yet. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// ---- typedef alias registry ----------------------------------------- +// +// `type error = str;` makes `error` a struct-shape alias. We track +// alias→target so is_str_type / is_slice_type / struct_lookup can +// resolve through the chain. Only direct N_TNAME aliases are mapped; +// `type p = struct {...}` is handled by collect_structs. + +type alias_ent = struct { + aname: str, + target: *node, // the rhs type expr + aanext: *alias_ent, +}; + +fn collect_aliases(c: *cgen, file: *node) void = { + c.aliases = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind != N_TSTRUCT) { + let a: *alias_ent = amalloc(c.a, 32u64): *alias_ent; + a.aname = d.str; + a.target = body; + a.aanext = c.aliases; + c.aliases = a; + }; + }; + }; + d = d.next; + }; +}; + +fn alias_lookup(c: *cgen, name: str) *node = { + let a: *alias_ent = c.aliases; + for (a != nil) { + let an: str = a.aname; + if (streq(an, name)) { return a.target; }; + a = a.aanext; + }; + return nil; +}; + +// resolve_type — follow typedef alias chains to a "canonical" type +// expr (str/slice/array/struct/...). Stops on cycles via depth limit. +fn resolve_type(c: *cgen, t: *node) *node = { + let cur: *node = t; + let depth: i32 = 0; + for (depth < 16) { + if (cur == nil) { return nil; }; + if (cur.kind != N_TNAME) { return cur; }; + let nm: str = cur.str; + let next: *node = alias_lookup(c, nm); + if (next == nil) { return cur; }; + cur = next; + depth += 1; + }; + return cur; +}; + +// ---- struct registry ------------------------------------------------ +// +// Per-file map from struct name → list of fields with computed offsets +// and sizes. Built when cg_file walks N_TYPEDECL with N_TSTRUCT lhs. +// N_DOT and N_ASSIGN consult this to resolve `s.field` for struct or +// *struct bases. + +type field_info = struct { + fname: str, + foff: i32, + fsz: i32, + tnode: *node, // the field type expr, for nested struct lookups + finext: *field_info, +}; + +type struct_info = struct { + sname: str, + fields: *field_info, + tot_size: i32, + sinext: *struct_info, +}; + +// ---- locals / frame -------------------------------------------------- + +type local = struct { + name: str, + off: i32, + tnode: *node, // declared type expr (N_TNAME / N_TPTR / ...) or nil + lnext: *local, +}; + +// strlit — interned string literal record. Emitted as a DATA directive +// after all functions; cgexpr N_STRLIT loads (LEAQ ptr, MOVQ len). +type strlit = struct { + label: str, // "_S_" + bytes: str, + slnext: *strlit, +}; + +// ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr +// gets its CALL target rewritten to `name`. +type ffi = struct { + ident: str, + symbol: str, + fnext: *ffi, +}; + +def LOOP_MAX: i32 = 16; + +type cgen = struct { + a: *arena, + locals: *local, + frame: i32, + last_was_return: i32, + labelseq: i32, + strlit_seq: i32, + strlits: *strlit, + ffis: *ffi, + defs: *def_ent, + fnrets: *fnret, + aliases: *alias_ent, + structs: *struct_info, + fn_name: str, + fn_ret: *node, // declared return type of current fn (or nil) + loop_top: i32, + loop_end_buf: *str, // stack of end labels for break + loop_cont_buf: *str, // stack of cont labels for continue +}; + +fn cgen_init(c: *cgen, a: *arena) void = { + c.a = a; + c.locals = nil; + c.frame = 0; + c.last_was_return = 0; + c.labelseq = 0; + // Note: strlit_seq, strlits, ffis are *not* reset here; they + // persist across cgfn calls within one file. cg_file resets them + // at the start of each compilation unit. + c.loop_top = 0; + c.loop_end_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str; + c.loop_cont_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str; +}; + +fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { + // Round slot size up to 8-byte multiple so all locals stay + // 8-aligned (C cgen does the same — buf[4]u8 still occupies an + // 8-byte slot). + let asz: i32 = sz; + if (asz < 8) { asz = 8; }; + if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; + c.frame += asz; + let off: i32 = 0 - c.frame; + let l: *local = amalloc(c.a, 48u64): *local; + l.name = name; + l.off = off; + l.tnode = tnode; + l.lnext = c.locals; + c.locals = l; + return off; +}; + +fn local_find_node(c: *cgen, name: str) *local = { + let l: *local = c.locals; + for (l != nil) { + let ln: str = l.name; + if (streq(ln, name)) { return l; }; + l = l.lnext; + }; + return nil; +}; + +fn local_find(c: *cgen, name: str) i32 = { + let l: *local = c.locals; + for (l != nil) { + let ln: str = l.name; + if (ln.len == name.len) { + let i: i32 = 0; + let eq: bool = true; + for (i < name.len) { + if (ln[i] != name[i]) { eq = false; i = name.len; } + else { i += 1; }; + }; + if (eq) { return l.off; }; + }; + l = l.lnext; + }; + return 0; +}; + +// ---- emit helpers --------------------------------------------------- + +fn emit_line(s: str) void = { os.write(1, s.ptr, s.len: u64); }; + +fn emit_int(v: i64) void = { + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +fn emit_uint(v: u64) void = { + let buf: [32]u8; + let n: i32 = strconv.u64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +// emit_disp_reg — print "disp(reg)" or "(reg)" when disp == 0, the +// way Plan 9 6c/6a do. +fn emit_disp_reg(off: i64, reg: str) void = { + if (off != 0i64) { emit_int(off); }; + emit_line("("); + emit_line(reg); + emit_line(")"); +}; + +// emit_off — print an integer offset, suppressing it entirely when 0. +// Use before any emit_line("(BP)...") or emit_line("(SB)...") sequence. +// Plan 9 cc convention: "(BP)" not "0(BP)". +fn emit_off(v: i64) void = { + if (v != 0i64) { emit_int(v); }; +}; + +// mklabel — fresh label "__". Returns an +// arena-owned str. Mirrors C cgen's mklabel so diffs match. +fn mklabel(c: *cgen, base: str) str = { + let buf: [128]u8; + let i: i32 = 0; + let fname: str = c.fn_name; + let j: i32 = 0; + for (j < fname.len) { + buf[i] = fname[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + j = 0; + for (j < base.len) { + buf[i] = base[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + let n: i32 = strconv.i64toa(buf[i:128], c.labelseq: i64); + c.labelseq += 1; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { + p[k] = buf[k]; + k += 1; + }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + +fn emit_label(s: str) void = { + os.write(1, s.ptr, s.len: u64); + emit_line(":\n"); +}; + +// ---- string interning ------------------------------------------------ +// +// streq is provided by sym.ww and reused here. + +// intern_strlit — return a stable label for `bytes`. Dedups by content +// so identical literals share storage. +fn intern_strlit(c: *cgen, bytes: str) str = { + let s: *strlit = c.strlits; + for (s != nil) { + let bs: str = s.bytes; + if (streq(bs, bytes)) { + return s.label; + }; + s = s.slnext; + }; + // New label "_S_". + let buf: [32]u8; + buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_" + let n: i32 = strconv.i64toa(buf[3:32], c.strlit_seq: i64); + c.strlit_seq += 1; + let total: i32 = 3 + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let i: i32 = 0; + for (i < total) { p[i] = buf[i]; i += 1; }; + p[total] = 0u8; + let lab: str; + lab.ptr = p; + lab.len = total; + let nw: *strlit = amalloc(c.a, 48u64): *strlit; + nw.label = lab; + nw.bytes = bytes; + nw.slnext = c.strlits; + c.strlits = nw; + return lab; +}; + +// emit_def_constants — DATA directive per top-level int-literal `def`. +// 8 bytes little-endian to match what the C cgen emits. +fn emit_def_constants(c: *cgen, file: *node) void = { + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_DEF) { + let r: *node = d.rhs; + let v: u64 = 0u64; + let ok: bool = false; + if (r != nil) { + if (r.kind == N_INTLIT) { v = r.uval; ok = true; }; + if (r.kind == N_RUNELIT) { v = r.uval; ok = true; }; + if (r.kind == N_TRUE) { v = 1u64; ok = true; }; + if (r.kind == N_FALSE) { v = 0u64; ok = true; }; + if (r.kind == N_NIL) { v = 0u64; ok = true; }; + }; + if (ok) { + emit_line("DATA "); + let nm: str = d.str; + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB),\""); + let i: i32 = 0; + let n: u64 = v; + for (i < 8) { + let b: u8 = (n & 255u64): u8; + n = n >> 8u64; + // C emit_defs only special-cases " and \; + // every other non-printable goes as \xHH. + if (b == 34u8) { emit_line("\\\""); } + else { if (b == 92u8) { emit_line("\\\\"); } + else { + if (b < 32u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + if (b >= 127u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };}; + i += 1; + }; + emit_line("\"\n"); + }; + }; + d = d.next; + }; +}; + +// emit_data_section — DATA directives for every interned strlit. +// Trailing NUL appended so .ptr can be used as a C string by syscalls. +fn emit_data_section(c: *cgen) void = { + let s: *strlit = c.strlits; + for (s != nil) { + emit_line("DATA "); + let lab: str = s.label; + os.write(1, lab.ptr, lab.len: u64); + emit_line("(SB),\""); + let bs: str = s.bytes; + let i: i32 = 0; + for (i < bs.len) { + let b: u8 = bs[i]; + if (b == 34u8) { emit_line("\\\""); } // " + else { if (b == 92u8) { emit_line("\\\\"); } // \ + else { if (b == 10u8) { emit_line("\\n"); } + else { if (b == 9u8) { emit_line("\\t"); } + else { if (b == 13u8) { emit_line("\\r"); } + else { + if (b < 32u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + if (b >= 127u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };};};};}; + i += 1; + }; + emit_line("\\x00\"\n"); + s = s.slnext; + }; +}; + +// ---- fn return-type map --------------------------------------------- +// +// Per-file: ident → ret-type-node. Used to decide whether to shuffle +// (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so +// the value flows through cgen as the canonical (AX, BX) str pair. + +type fnret = struct { + fname: str, + rtype: *node, + frnext: *fnret, +}; + +fn collect_fnrets(c: *cgen, file: *node) void = { + c.fnrets = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + let f: *fnret = amalloc(c.a, 32u64): *fnret; + f.fname = d.str; + f.rtype = d.lhs; + f.frnext = c.fnrets; + c.fnrets = f; + }; + d = d.next; + }; +}; + +fn fnret_lookup(c: *cgen, name: str) *node = { + let f: *fnret = c.fnrets; + for (f != nil) { + let fn_: str = f.fname; + if (streq(fn_, name)) { return f.rtype; }; + f = f.frnext; + }; + return nil; +}; + +// ---- def-constant registry ------------------------------------------ +// +// `def NAME: T = LIT;` becomes a DATA symbol the C-side 6c emits; an +// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at +// file load and consult on N_IDENT lookup. + +type def_ent = struct { + dname: str, + dnext: *def_ent, +}; + +fn collect_defs(c: *cgen, file: *node) void = { + c.defs = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_DEF) { + let e: *def_ent = amalloc(c.a, 32u64): *def_ent; + e.dname = d.str; + e.dnext = c.defs; + c.defs = e; + }; + d = d.next; + }; +}; + +fn def_lookup(c: *cgen, name: str) bool = { + let e: *def_ent = c.defs; + for (e != nil) { + let dn: str = e.dname; + if (streq(dn, name)) { return true; }; + e = e.dnext; + }; + return false; +}; + +// ---- FFI map --------------------------------------------------------- + +fn ffi_collect(c: *cgen, file: *node) void = { + c.ffis = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + let a: *node = d.attr; + for (a != nil) { + if (a.kind == N_ATTR) { + let aname: str = a.str; + if (streq(aname, "symbol")) { + let sym_node: *node = a.list; + if (sym_node != nil) { + if (sym_node.kind == N_STRLIT) { + let f: *ffi = amalloc(c.a, 48u64): *ffi; + f.ident = d.str; + f.symbol = sym_node.str; + f.fnext = c.ffis; + c.ffis = f; + }; + }; + }; + }; + a = a.next; + }; + }; + d = d.next; + }; +}; + +fn ffi_resolve(c: *cgen, ident: str) str = { + let f: *ffi = c.ffis; + for (f != nil) { + let id: str = f.ident; + if (streq(id, ident)) { return f.symbol; }; + f = f.fnext; + }; + return ident; +}; + +// ---- ABI argreg helpers --------------------------------------------- + +fn argreg_name(i: i32) str = { + if (i == 0) { return "DI"; }; + if (i == 1) { return "SI"; }; + if (i == 2) { return "DX"; }; + if (i == 3) { return "CX"; }; + if (i == 4) { return "R8"; }; + if (i == 5) { return "R9"; }; + return "?"; +}; + +// ---- expression cgen ------------------------------------------------- + +// push_args_rev — recursively walks the arg list, evaluates rightmost +// first, and pushes. str args take two slots (ptr in AX, len in BX); +// the order on the stack so a left-to-right pop into argregs lands +// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the +// pop sequence then yields AX, then BX. +fn push_args_rev(c: *cgen, arg: *node) i32 = { + if (arg == nil) { return 0; }; + let rest: i32 = push_args_rev(c, arg.next); + // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header + // on the stack matching C cgen's sequence — push base, push hi, + // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). + if (arg.kind == N_SLICE) { + let base: *node = arg.lhs; + let lo: *node = arg.rhs; + let hi: *node = arg.cond; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + }; + }; + // base address → push + if (base_local != nil) { + let tn: *node = base_local.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + }; + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + }; + } else { + cgexpr(c, base); + }; + emit_line("\tPUSHQ\tAX\n"); + // hi (default base length) → push + if (hi != nil) { + cgexpr(c, hi); + } else { if (base_local != nil) { + let tn: *node = base_local.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + let len_n: *node = tn.rhs; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { + emit_line("\tMOVQ\t$"); + emit_uint(len_n.uval); + emit_line(", AX\n"); + }; + }; + } else { if (tn.kind == N_TSLICE) { + emit_line("\tMOVQ\t"); + emit_off((base_local.off + 8): i64); + emit_line("(BP), AX\n"); + } else { if (tn.kind == N_TNAME) { + if (streq(tn.str, "str")) { + emit_line("\tMOVQ\t"); + emit_off((base_local.off + 8): i64); + emit_line("(BP), AX\n"); + }; + };};}; + }; + } else { + emit_line("\tMOVQ\t$0, AX\n"); + };}; + emit_line("\tPUSHQ\tAX\n"); + // lo (default 0) → AX + if (lo != nil) { cgexpr(c, lo); } + else { emit_line("\tMOVQ\t$0, AX\n"); }; + emit_line("\tPOPQ\tBX\n"); // hi + emit_line("\tPOPQ\tCX\n"); // base + emit_line("\tMOVQ\tBX, DX\n"); // DX = hi + emit_line("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len + emit_line("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr + emit_line("\tPUSHQ\tDX\n"); // cap + emit_line("\tPUSHQ\tDX\n"); // len + emit_line("\tPUSHQ\tCX\n"); // ptr (top) + return rest + 3; + }; + // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in + // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop + // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). + if (arg.kind == N_IDENT) { + let nm: str = arg.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + if (is_slice_type(c, lc.tnode) || is_tagged_type(lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 16): i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 3; + }; + }; + }; + cgexpr(c, arg); + if (node_isslice(c, arg)) { + emit_line("\tPUSHQ\tCX\n"); + emit_line("\tPUSHQ\tBX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 3; + }; + if (node_isstr(c, arg)) { + emit_line("\tPUSHQ\tBX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 2; + }; + emit_line("\tPUSHQ\tAX\n"); + return rest + 1; +}; + +fn node_isslice(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { return is_slice_type(c, lc.tnode); }; + return false; + }; + if (k == N_SLICE) { return true; }; + return false; +}; + +// node_isstr — best-effort surface check: does this expression +// evaluate to a str value? Used to drive the call-arg push convention +// (str args take two slots: ptr + len). +fn node_isstr(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_STRLIT) { return true; }; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { + let tnm: str = tn.str; + if (streq(tnm, "str")) { return true; }; + }; + }; + }; + return false; + }; + if (k == N_CALL) { + let callee: *node = n.lhs; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cnm: str = callee.str; + let rt: *node = fnret_lookup(c, cnm); + return is_str_type(c, rt); + }; + }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + // `.ptr` is *u8 not str; `.len` is i32 not str. + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return is_str_type(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { + return is_str_type(c, n.rhs); + }; + return false; +}; + +// type_name_isunsigned — true for u8/u16/u32/u64/uint/uintptr. +fn type_name_isunsigned(nm: str) bool = { + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// type_node_isunsigned — recurse through TNAME / TPTR / TSLICE etc. +fn type_node_isunsigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { return type_name_isunsigned(t.str); }; + return false; +}; + +// type_name_issigned — true for i8/i16/i32/i64/int/rune. +fn type_name_issigned(nm: str) bool = { + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "rune")) { return true; }; + return false; +}; + +// field_load_op — pick the load instruction for a non-str struct +// field by its declared size + signedness. Mirrors the C cgen op +// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ +// for 8-byte). f might be nil for fields outside our struct registry. +fn field_load_op(f: *field_info) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVZBQ"; }; + if (sz == 4) { + let t: *node = f.tnode; + if (t != nil) { + if (t.kind == N_TNAME) { + if (type_name_issigned(t.str)) { return "MOVSXD"; }; + }; + }; + return "MOVL"; + }; + return "MOVQ"; +}; + +// field_store_op — pick the store instruction for a non-str struct +// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. +fn field_store_op(f: *field_info) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVB"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + +// index_base_esz — element size for `arr[i]` where the base is a +// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). +// For str the element is one byte; for `[]T` / `*[]T` we drill into +// the slice element type. +fn index_base_esz(c: *cgen, base: *node) i32 = { + if (base == nil) { return 8; }; + if (base.kind != N_DOT) { return 8; }; + let fld: str = base.str; + let inner: *node = base.lhs; + if (inner == nil) { return 8; }; + if (inner.kind != N_IDENT) { return 8; }; + let nm: str = inner.str; + let lc: *local = local_find_node(c, nm); + if (lc == nil) { return 8; }; + let tn: *node = lc.tnode; + if (tn == nil) { return 8; }; + + // `.ptr` pseudo-field on str/slice → element of the str/slice. + if (streq(fld, "ptr")) { + let inner_t: *node = tn; + if (tn.kind == N_TPTR) { inner_t = tn.lhs; }; + if (inner_t == nil) { return 8; }; + if (inner_t.kind == N_TNAME) { + if (streq(inner_t.str, "str")) { return 1; }; + }; + if (inner_t.kind == N_TSLICE) { return elem_size_of(inner_t); }; + return 8; + }; + + // Generic struct field: if it's *T, element size is T's size. + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == N_TNAME) { sname = pinner.str; }; + }; + }; + if (sname.len == 0) { return 8; }; + let si: *struct_info = struct_lookup(c, sname); + if (si == nil) { return 8; }; + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return 8; }; + if (ft.kind == N_TPTR) { + let elem: *node = ft.lhs; + if (elem != nil) { + if (elem.kind == N_TNAME) { + if (streq(elem.str, "str")) { return 16; }; + let ps: i32 = prim_size(elem.str); + if (ps > 0) { return ps; }; + }; + }; + return 8; + }; + if (ft.kind == N_TSLICE) { return elem_size_of(ft); }; + return 8; + }; + fi = fi.finext; + }; + return 8; +}; + +// dot_inner_struct_ptr — for an N_DOT whose lhs is a chain of dots +// or an N_IDENT, walk the chain and return the N_TNAME tnode of the +// struct that the chain dereferences to (i.e., for `r.sym` where +// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain +// doesn't resolve to a *struct. +// +// Used by the chained-DOT cgen path so `r.sym.val` knows the outer +// is a field of `lsym`. +fn dot_inner_struct_ptr(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + + // Resolve base's struct tnode. + let base_t: *node = nil; + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (tn == nil) { return nil; }; + // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). + if (tn.kind == N_TNAME) { base_t = tn; }; + if (tn.kind == N_TPTR) { base_t = tn.lhs; }; + } else { if (base.kind == N_DOT) { + base_t = dot_inner_struct_ptr(c, base); + };}; + if (base_t == nil) { return nil; }; + if (base_t.kind != N_TNAME) { return nil; }; + + // Look up the struct, find the field, return the field's *struct. + let si: *struct_info = struct_lookup(c, base_t.str); + if (si == nil) { return nil; }; + let fi: *field_info = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return nil; }; + if (ft.kind != N_TPTR) { return nil; }; + let inner: *node = ft.lhs; + if (inner == nil) { return nil; }; + if (inner.kind != N_TNAME) { return nil; }; + return inner; + }; + fi = fi.finext; + }; + return nil; +}; + +// elem_size_of — given the type node of an indexable (`*T`, `[]T`, +// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ +// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). +fn elem_size_of(t: *node) i32 = { + if (t == nil) { return 1; }; + let k: i32 = t.kind; + let elem: *node = nil; + if (k == N_TPTR) { elem = t.lhs; }; + if (k == N_TSLICE) { elem = t.lhs; }; + if (k == N_TARRAY) { elem = t.lhs; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 1; }; + // Indexing a primitive name (rare): element size = the prim. + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + return 1; + }; + if (elem == nil) { return 1; }; + if (elem.kind == N_TNAME) { + let nm: str = elem.str; + // str element is 16B (ptr+len). prim_size returns 0 for it. + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// node_isunsigned — best-effort cgen-time inference from the AST. We +// don't have a typed AST yet, so we walk surface nodes: +// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) +// N_IDENT — look up the local's declared type +// N_DOT — look up the field's declared type via struct reg +// N_BIN / N_UN — recurse: unsigned if either operand is unsigned +// N_CAST — use the cast target type +// +// Conservative: if we can't tell, return false (signed). The cost of +// being wrong here is byte-different asm vs C, not bad runtime. +fn node_isunsigned(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { return type_node_isunsigned(lc.tnode); }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return type_node_isunsigned(fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { return type_node_isunsigned(n.rhs); }; + if (k == N_BIN) { + if (node_isunsigned(c, n.lhs)) { return true; }; + return node_isunsigned(c, n.rhs); + }; + if (k == N_UN) { return node_isunsigned(c, n.lhs); }; + return false; +}; + +fn cgexpr(c: *cgen, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + if (k == N_INTLIT) { + emit_line("\tMOVQ\t$"); + emit_uint(n.uval); + emit_line(", AX\n"); + return; + }; + if (k == N_RUNELIT) { + emit_line("\tMOVQ\t$"); + emit_uint(n.uval); + emit_line(", AX\n"); + return; + }; + if (k == N_STRLIT) { + // Result is the (ptr, len) pair: ptr in AX, len in BX. Call + // sites that expect a str arg pick these up directly. + let nstr: str = n.str; + let lab: str = intern_strlit(c, nstr); + emit_line("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emit_line("(SB), AX\n"); + emit_line("\tMOVQ\t$"); + emit_int(nstr.len: i64); + emit_line(", BX\n"); + return; + }; + if (k == N_TRUE) { + emit_line("\tMOVQ\t$1, AX\n"); + return; + }; + if (k == N_FALSE) { + emit_line("\tMOVQ\t$0, AX\n"); + return; + }; + if (k == N_NIL) { + emit_line("\tMOVQ\t$0, AX\n"); + return; + }; + + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + // str local: also load the len half into BX. + if (is_str_type(c, lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), BX\n"); + }; + // slice local: load (ptr, len, cap) into (AX, BX, CX). + if (is_slice_type(c, lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_off((off + 16): i64); + emit_line("(BP), CX\n"); + }; + return; + }; + // Top-level `def` constant — load from its DATA symbol. + if (def_lookup(c, nm)) { + emit_line("\tMOVQ\t"); + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB), AX\n"); + return; + }; + // Fn-name used as a value (e.g. `let f = some_fn;` or + // `... = some_fn;`). LEAQ the symbol address into AX. + let rt: *node = fnret_lookup(c, nm); + if (rt != nil) { + emit_line("\tLEAQ\t"); + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB), AX\n"); + return; + }; + return; + }; + + if (k == N_INDEX) { + // Element-size-aware load: u8-element bases use MOVZBQ, + // everything else MOVQ. Fast path when the base is a bare + // ident (mem.ww shape). + let base: *node = n.lhs; + let idx: *node = n.rhs; + let esz: i32 = 8; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + if (base_local != nil) { esz = elem_size_of(base_local.tnode); }; + } else { if (base.kind == N_DOT) { + esz = index_base_esz(c, base); + };}; + }; + cgexpr(c, idx); + if (esz > 1) { + emit_line("\tMOVQ\t$"); + emit_int(esz: i64); + emit_line(", CX\n"); + emit_line("\tIMULQ\tCX, AX\n"); + }; + if (base_local != nil) { + let tn: *node = base_local.tnode; + let is_array: bool = false; + if (tn != nil) { if (tn.kind == N_TARRAY) { is_array = true; }; }; + if (is_array) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + }; + emit_line("\tADDQ\tAX, BX\n"); + // str element (16B): load (ptr, len) into (AX, BX) so + // the value flows through the str-rhs convention. + if (esz == 16) { + emit_line("\tMOVQ\t8(BX), CX\n"); + emit_line("\tMOVQ\t(BX), AX\n"); + emit_line("\tMOVQ\tCX, BX\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVZBQ\t(BX), AX\n"); } + else { emit_line("\tMOVQ\t(BX), AX\n"); }; + return; + }; + // Generic fallback when base isn't a plain ident. + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emit_line("\tPOPQ\tBX\n"); + emit_line("\tADDQ\tBX, AX\n"); + if (esz == 16) { + emit_line("\tMOVQ\t8(AX), BX\n"); + emit_line("\tMOVQ\t(AX), AX\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVZBQ\t(AX), AX\n"); } + else { emit_line("\tMOVQ\t(AX), AX\n"); }; + return; + }; + + if (k == N_MATCH) { + // match (e) { case let v: T => stmt; ... } + // + // Read the tagged-union slot and dispatch by tag. Slot + // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings + // (`case let v: T =>`) get a fresh local slot loaded from + // slot+8 (and slot+16 for str-typed payload). + let scrut: *node = n.lhs; + let scrut_off: i32 = 0; + let scrut_t: *node = nil; + if (scrut != nil) { + if (scrut.kind == N_IDENT) { + let lc: *local = local_find_node(c, scrut.str); + if (lc != nil) { + scrut_off = lc.off; + scrut_t = resolve_type(c, lc.tnode); + }; + }; + }; + let endl: str = mklabel(c, "match_end"); + let cs: *node = n.list; + for (cs != nil) { + let nxt: str = mklabel(c, "match_next"); + let pat: *node = cs.lhs; + // Compute the variant tag for this arm. Default arm + // (no pattern) skips the tag check. + if (pat != nil) { + let want: i32 = 0; + if (scrut_t != nil) { + if (scrut_t.kind == N_TTAGGED) { + let pat_name: str; + pat_name.ptr = nil; pat_name.len = 0; + if (pat.kind == N_TNAME) { pat_name = pat.str; }; + let v: *node = scrut_t.list; + let idx: i32 = 0; + let found: bool = false; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, pat_name)) { + want = idx; + found = true; + v = nil; + }; + }; + if (v != nil) { + v = v.next; + idx += 1; + }; + }; + if (!found) { want = 0; }; + }; + }; + emit_line("\tMOVQ\t"); + emit_off(scrut_off: i64); + emit_line("(BP), AX\n"); + emit_line("\tCMPQ\t$"); + emit_int(want: i64); + emit_line(", AX\n"); + emit_line("\tJNE\t"); + emit_line(nxt); + emit_line("\n"); + }; + // Bind `let v: T` from the slot, if requested. + let bn: str = cs.str; + if (bn.len > 0) { + if (pat != nil) { + let bsz: i32 = 8; + if (is_str_type(c, pat)) { bsz = 16; }; + let voff: i32 = local_add(c, bn, bsz, pat); + emit_line("\tMOVQ\t"); + emit_off((scrut_off + 8): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\tAX, "); + emit_off(voff: i64); + emit_line("(BP)\n"); + if (bsz == 16) { + emit_line("\tMOVQ\t"); + emit_off((scrut_off + 16): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\tAX, "); + emit_off((voff + 8): i64); + emit_line("(BP)\n"); + }; + }; + }; + // Body. Match arms are statements; we cgstmt them. + if (cs.body != nil) { cgstmt(c, cs.body); }; + emit_line("\tJMP\t"); + emit_line(endl); + emit_line("\n"); + emit_label(nxt); + cs = cs.next; + }; + emit_label(endl); + return; + }; + + if (k == N_CAST) { + // Type casts are mostly no-ops at the asm level for our + // integer-shaped operands. Evaluate the source; AX holds + // the bits unchanged. (Sign- or zero-extending narrow loads + // to wider types is the loader's job, not cast's, in this + // minimal cgen.) + cgexpr(c, n.lhs); + return; + }; + + if (k == N_DOT) { + let lhs: *node = n.lhs; + let fld: str = n.str; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let nm: str = lhs.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + // Pointer-to-struct: deref then field load. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == N_TNAME) { + sname = inner.str; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + // str field via *struct: load len into a + // scratch first (so loading ptr into AX + // last leaves (AX=ptr, BX=len)). + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_disp_reg((fi.foff + 8): i64, "BX"); + emit_line(", CX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", AX\n"); + emit_line("\tMOVQ\tCX, BX\n"); + } else { + let op: str = field_load_op(fi); + emit_line("\t"); + emit_line(op); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + }; + // Direct struct local: field load at off+foff. + if (lkind == N_TNAME) { + let sname: str = tn.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + // str field: load both halves so chained + // `.ptr` / `.len` see (AX=ptr, BX=len). + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\t"); + emit_off((lc.off + fi.foff + 8): i64); + emit_line("(BP), BX\n"); + } else { + let op: str = field_load_op(fi); + emit_line("\t"); + emit_line(op); + emit_line("\t"); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP), AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + // Array pseudo-fields: `.ptr` is the array's + // address (LEAQ); `.len` is the static element + // count (immediate). + if (lkind == N_TARRAY) { + if (streq(fld, "ptr")) { + emit_line("\tLEAQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), AX\n"); + return; + }; + if (streq(fld, "len")) { + let len_n: *node = tn.rhs; + let alen: i64 = 0i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { alen = len_n.uval: i64; }; + }; + emit_line("\tMOVQ\t$"); + emit_int(alen); + emit_line(", AX\n"); + return; + }; + }; + // str/slice pseudo-fields .ptr/.len/.cap on a + // direct local: load at slot+delta. + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + // Pointer to str/slice (`*[]u8`, `*str`): + // deref, then load at delta within the + // pointed-to header. C cgen does the same. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let inner_kind: i32 = -1; + if (inner != nil) { inner_kind = inner.kind; }; + let inner_str: bool = false; + if (inner_kind == N_TNAME) { + if (streq(inner.str, "str")) { inner_str = true; }; + }; + if (inner_kind == N_TSLICE) { inner_str = true; }; + if (inner_str) { + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(delta: i64, "BX"); + emit_line(", AX\n"); + return; + }; + }; + emit_line("\tMOVQ\t"); + emit_off((lc.off + delta): i64); + emit_line("(BP), AX\n"); + return; + }; + }; + }; + }; + // Module-qualified value reference: `mod.name` where `mod` + // is N_IDENT bound as SK_USE and the leaf isn't a local. + // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback + // the C cgen takes when bt is NULL/ty_err. + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + emit_line("\tMOVQ\t"); + emit_line(fld); + emit_line("(SB), AX\n"); + return; + }; + }; + // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. + // Evaluate the str-producing expression — that leaves + // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` + // shuffles BX→AX. Mirrors what C cgen does (it just evaluates + // the literal and picks the half it wants). + if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; + if (streq(fld, "len")) { + cgexpr(c, lhs); + emit_line("\tMOVQ\tBX, AX\n"); + return; + }; + // Chained struct-field-via-ptr-via-ptr access: + // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 + // Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct + // field). Outer DOT dereferences and reads `val`. Without this + // path the cgen falls through and AX retains whatever the + // inner expression left there — typically the *struct pointer + // itself, so reads silently get the pointer value instead of + // the field. (Showed up porting 6l/pass.ww.) + if (lhs != nil) { + if (lhs.kind == N_DOT) { + let inner_t: *node = dot_inner_struct_ptr(c, lhs); + if (inner_t != nil) { + let sname: str = inner_t.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + cgexpr(c, lhs); // AX = ptr to inner struct + let lop: str = field_load_op(fi); + // str field: load both halves. + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_disp_reg((fi.foff + 8): i64, "AX"); + emit_line(", BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(fi.foff: i64, "AX"); + emit_line(", AX\n"); + return; + }; + emit_line("\t"); + emit_line(lop); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "AX"); + emit_line(", AX\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + return; + }; + + if (k == N_UN) { + // Match C cgen ordering: evaluate operand first (load into AX), + // then apply the unary op. AMP / STAR override AX with the + // address / deref. The wasted load before AMP keeps our asm + // byte-identical to the C version. + cgexpr(c, n.lhs); + if (n.op == TK_MINUS) { emit_line("\tNEGQ\tAX\n"); return; }; + if (n.op == TK_TILDE) { emit_line("\tNOTQ\tAX\n"); return; }; + if (n.op == TK_STAR) { emit_line("\tMOVQ\t(AX), AX\n"); return; }; + if (n.op == TK_AMP) { + let opnd: *node = n.lhs; + if (opnd != nil) { + if (opnd.kind == N_IDENT) { + let nm: str = opnd.str; + let off: i32 = local_find(c, nm); + if (off != 0) { + emit_line("\tLEAQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + return; + }; + }; + }; + return; + }; + if (n.op == TK_NOT) { + let t: str = mklabel(c, "tt"); + let e: str = mklabel(c, "te"); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); emit_line(t); emit_line("\n"); + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tJMP\t"); emit_line(e); emit_line("\n"); + emit_label(t); + emit_line("\tMOVQ\t$1, AX\n"); + emit_label(e); + return; + }; + return; + }; + + if (k == N_BIN) { + let unsignd: bool = node_isunsigned(c, n.lhs); + if (!unsignd) { unsignd = node_isunsigned(c, n.rhs); }; + + cgexpr(c, n.rhs); + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, n.lhs); + emit_line("\tPOPQ\tBX\n"); + if (n.op == TK_PLUS) { emit_line("\tADDQ\tBX, AX\n"); return; }; + if (n.op == TK_MINUS) { emit_line("\tSUBQ\tBX, AX\n"); return; }; + if (n.op == TK_STAR) { emit_line("\tIMULQ\tBX, AX\n"); return; }; + if (n.op == TK_SLASH) { + emit_line("\tMOVQ\t$0, DX\n"); + if (unsignd) { emit_line("\tDIVQ\tBX\n"); } + else { emit_line("\tIDIVQ\tBX\n"); }; + return; + }; + if (n.op == TK_PERCENT) { + emit_line("\tMOVQ\t$0, DX\n"); + if (unsignd) { emit_line("\tDIVQ\tBX\n"); } + else { emit_line("\tIDIVQ\tBX\n"); }; + emit_line("\tMOVQ\tDX, AX\n"); + return; + }; + if (n.op == TK_AMP) { emit_line("\tANDQ\tBX, AX\n"); return; }; + if (n.op == TK_PIPE) { emit_line("\tORQ\tBX, AX\n"); return; }; + if (n.op == TK_CARET) { emit_line("\tXORQ\tBX, AX\n"); return; }; + if (n.op == TK_LSHIFT) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tSHLQ\tCX, AX\n"); + return; + }; + if (n.op == TK_RSHIFT) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tSHRQ\tCX, AX\n"); + return; + }; + if (n.op == TK_AND) { emit_line("\tANDQ\tBX, AX\n"); return; }; + if (n.op == TK_OR) { emit_line("\tORQ\tBX, AX\n"); return; }; + + // Comparison: emit CMPQ, jump on signed/unsigned variant, + // materialise 0/1 in AX. Same shape as the C cgen. + let is_cmp: bool = false; + let jcc: str = ""; + if (n.op == TK_EQ) { is_cmp = true; jcc = "JE"; }; + if (n.op == TK_NEQ) { is_cmp = true; jcc = "JNE"; }; + if (n.op == TK_LT) { is_cmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; + if (n.op == TK_LE) { is_cmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; + if (n.op == TK_GT) { is_cmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; + if (n.op == TK_GE) { is_cmp = true; if (unsignd) { jcc = "JAE"; } else { jcc = "JGE"; }; }; + if (is_cmp) { + let t: str = mklabel(c, "ct"); + let e: str = mklabel(c, "ce"); + emit_line("\tCMPQ\tBX, AX\n"); + emit_line("\t"); emit_line(jcc); emit_line("\t"); emit_line(t); emit_line("\n"); + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tJMP\t"); emit_line(e); emit_line("\n"); + emit_label(t); + emit_line("\tMOVQ\t$1, AX\n"); + emit_label(e); + return; + }; + return; + }; + + if (k == N_CALL) { + let nargs: i32 = push_args_rev(c, n.list); + let i: i32 = 0; + for (i < nargs) { + emit_line("\tPOPQ\t"); + emit_line(argreg_name(i)); + emit_line("\n"); + i += 1; + }; + let callee: *node = n.lhs; + let callee_name: str; + callee_name.ptr = nil; callee_name.len = 0; + // Detect fn-pointer field call: `w.emit(args)` where `w` is + // a struct local and `emit` is an N_TFN field. Load the + // field value into AX and CALL through it. + let is_fnptr_call: bool = false; + if (callee != nil) { + if (callee.kind == N_DOT) { + let base: *node = callee.lhs; + let fld: str = callee.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft != nil) { + if (ft.kind == N_TFN) { + is_fnptr_call = true; + }; + }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + if (is_fnptr_call) { + // Load fn-ptr field value into AX; CALL AX. We emit the + // load AFTER the args have been popped (so AX/BX/etc + // don't get clobbered by the field load before the pops). + // `popped args` left DI/SI/etc set; AX is free. + cgexpr(c, callee); + emit_line("\tCALL\tAX\n"); + } else { + emit_line("\tCALL\t"); + if (callee != nil) { + if (callee.kind == N_IDENT) { + callee_name = callee.str; + let resolved: str = ffi_resolve(c, callee_name); + os.write(1, resolved.ptr, resolved.len: u64); + } else { if (callee.kind == N_DOT) { + callee_name = callee.str; + let resolved: str = ffi_resolve(c, callee_name); + os.write(1, resolved.ptr, resolved.len: u64); + };}; + }; + emit_line("(SB)\n"); + }; + // SysV returns 16-byte aggregates in (AX, DX). Our str + // convention is (AX, BX), so shuffle for str-returning calls. + if (callee_name.len > 0) { + let rt: *node = fnret_lookup(c, callee_name); + if (is_str_type(c, rt)) { + emit_line("\tMOVQ\tDX, BX\n"); + }; + }; + return; + }; + + if (k == N_ASSIGN) { + let lhs: *node = n.lhs; + // `*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. + // We default to MOVQ (8B) since most fixtures use it; for + // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. + if (lhs != nil) { + if (lhs.kind == N_UN) { + if (lhs.op == TK_STAR) { + if (n.op == TK_ASSIGN) { + let inner: *node = lhs.lhs; + let elem_str: bool = false; + let store_op: str = "MOVQ"; + if (inner != nil) { + if (inner.kind == N_IDENT) { + let lc: *local = local_find_node(c, inner.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { + if (pe.kind == N_TNAME) { + if (streq(pe.str, "str")) { elem_str = true; } + else { + let ps: i32 = prim_size(pe.str); + if (ps == 1) { store_op = "MOVB"; } + else { if (ps == 4) { store_op = "MOVL"; }; }; + }; + }; + }; + }; + }; + }; + }; + }; + cgexpr(c, n.rhs); + if (elem_str) { emit_line("\tPUSHQ\tBX\n"); }; + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, inner); + emit_line("\tMOVQ\tAX, BX\n"); + emit_line("\tPOPQ\tAX\n"); + if (elem_str) { + emit_line("\tMOVQ\tAX, (BX)\n"); + emit_line("\tPOPQ\tCX\n"); + emit_line("\tMOVQ\tCX, 8(BX)\n"); + return; + }; + emit_line("\t"); + emit_line(store_op); + emit_line("\tAX, (BX)\n"); + return; + }; + }; + }; + }; + // Array/slice/ptr index store: `arr[i] = v;`. Element size + // from base.tnode picks MOVB vs MOVQ. + if (lhs != nil) { + if (lhs.kind == N_INDEX) { + if (n.op == TK_ASSIGN) { + let base: *node = lhs.lhs; + let idx: *node = lhs.rhs; + let esz: i32 = 8; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + if (base_local != nil) { + esz = elem_size_of(base_local.tnode); + }; + } else { if (base.kind == N_DOT) { + esz = index_base_esz(c, base); + };}; + }; + cgexpr(c, n.rhs); // value → AX + if (esz == 16) { emit_line("\tPUSHQ\tBX\n"); }; + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, idx); // idx → AX + if (esz > 1) { + emit_line("\tMOVQ\t$"); + emit_int(esz: i64); + emit_line(", CX\n"); + emit_line("\tIMULQ\tCX, AX\n"); + }; + emit_line("\tPUSHQ\tAX\n"); // scaled idx + if (base_local != nil) { + let tn: *node = base_local.tnode; + let is_array: bool = false; + if (tn != nil) { if (tn.kind == N_TARRAY) { is_array = true; }; }; + if (is_array) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + }; + } else { + cgexpr(c, base); + emit_line("\tMOVQ\tAX, BX\n"); + }; + emit_line("\tPOPQ\tAX\n"); // scaled idx + emit_line("\tADDQ\tAX, BX\n"); + emit_line("\tPOPQ\tAX\n"); // value + if (esz == 16) { + emit_line("\tMOVQ\tAX, (BX)\n"); + emit_line("\tPOPQ\tCX\n"); + emit_line("\tMOVQ\tCX, 8(BX)\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVB\tAX, (BX)\n"); } + else { emit_line("\tMOVQ\tAX, (BX)\n"); }; + return; + }; + }; + }; + // Struct/ptr-to-struct field assignment: `s.f = expr;` or + // `p.f = expr;`. Only plain `=` is wired (compound on field + // is rare and not yet needed by our fixtures). + if (lhs != nil) { + if (lhs.kind == N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + // Pointer-to-struct: deref then store. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + if (n.op != TK_ASSIGN) { + // compound: load current value + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + let lop: str = field_load_op(fi); + emit_line("\t"); + emit_line(lop); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", BX\n"); + emit_line("\tPUSHQ\tBX\n"); + }; + cgexpr(c, n.rhs); + if (n.op != TK_ASSIGN) { + emit_line("\tPOPQ\tBX\n"); + // PLUSEQ is commutative; MINUSEQ + // needs lhs - rhs (BX is old lhs, + // AX is rhs). + if (n.op == TK_PLUSEQ) { emit_line("\tADDQ\tBX, AX\n"); }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, BX\n"); + emit_line("\tMOVQ\tBX, AX\n"); + }; + }; + // str field via *struct: rhs left + // (AX=ptr, BX=len). Use CX as the + // address scratch so we don't clobber + // the len half before storing it. + if (n.op == TK_ASSIGN) { + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), CX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(fi.foff: i64, "CX"); + emit_line("\n"); + emit_line("\tMOVQ\tBX, "); + emit_disp_reg((fi.foff + 8): i64, "CX"); + emit_line("\n"); + return; + }; + }; + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line("\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + // Direct struct local: store at off+foff. + if (lkind == N_TNAME) { + let sname: str = tn.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + cgexpr(c, n.rhs); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP)\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + // str/slice pseudo-field assignment. + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let inner_kind: i32 = -1; + if (inner != nil) { inner_kind = inner.kind; }; + let inner_str: bool = false; + if (inner_kind == N_TNAME) { + if (streq(inner.str, "str")) { inner_str = true; }; + }; + if (inner_kind == N_TSLICE) { inner_str = true; }; + if (inner_str) { + if (n.op != TK_ASSIGN) { + // Compound on `(*str|*slice).field`: load + // current → push → eval rhs → combine → store. + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(delta: i64, "BX"); + emit_line(", BX\n"); + emit_line("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emit_line("\tPOPQ\tBX\n"); + // PLUSEQ is commutative; MINUSEQ + // needs lhs - rhs. + if (n.op == TK_PLUSEQ) { emit_line("\tADDQ\tBX, AX\n"); }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, BX\n"); + emit_line("\tMOVQ\tBX, AX\n"); + }; + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(delta: i64, "BX"); + emit_line("\n"); + return; + }; + cgexpr(c, n.rhs); + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(delta: i64, "BX"); + emit_line("\n"); + return; + }; + }; + cgexpr(c, n.rhs); + emit_line("\tMOVQ\tAX, "); + emit_off((lc.off + delta): i64); + emit_line("(BP)\n"); + return; + }; + }; + }; + }; + }; + }; + // Local-ident target — plain `=` and the simple compound + // forms (+= -= *= /=); other compounds fall back to + // "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path. + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let nm: str = lhs.str; + let off: i32 = local_find(c, nm); + if (off == 0) { return; }; + // Detect str-typed local — assignment must store both + // halves (AX=ptr at +0, BX=len at +8). + let lc_str: bool = false; + let lcn: *local = local_find_node(c, nm); + if (lcn != nil) { lc_str = is_str_type(c, lcn.tnode); }; + cgexpr(c, n.rhs); + if (n.op == TK_ASSIGN) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + if (lc_str) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + return; + }; + if (n.op == TK_PLUSEQ) { + emit_line("\tADDQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + // Generic compound: load → combine in BX → store. + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), BX\n"); + if (n.op == TK_STAREQ) { emit_line("\tIMULQ\tAX, BX\n"); }; + if (n.op == TK_AMPEQ) { emit_line("\tANDQ\tAX, BX\n"); }; + if (n.op == TK_PIPEEQ) { emit_line("\tORQ\tAX, BX\n"); }; + if (n.op == TK_CARETEQ) { emit_line("\tXORQ\tAX, BX\n"); }; + if (n.op == TK_LSHIFTEQ) { + emit_line("\tMOVQ\tAX, CX\n"); + emit_line("\tSHLQ\tCX, BX\n"); + }; + if (n.op == TK_RSHIFTEQ) { + emit_line("\tMOVQ\tAX, CX\n"); + emit_line("\tSHRQ\tCX, BX\n"); + }; + emit_line("\tMOVQ\tBX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + }; + return; + }; +}; + +// ---- type-driven slot sizing ---------------------------------------- + +fn struct_lookup(c: *cgen, name: str) *struct_info = { + let s: *struct_info = c.structs; + for (s != nil) { + let sn: str = s.sname; + if (streq(sn, name)) { return s; }; + s = s.sinext; + }; + return nil; +}; + +// prim_size — size in bytes of a primitive type name (or 0 if not +// recognised as a primitive — the caller falls back to other paths). +fn prim_size(name: str) i32 = { + if (streq(name, "u8")) { return 1; }; + if (streq(name, "i8")) { return 1; }; + if (streq(name, "bool")) { return 1; }; + if (streq(name, "u16")) { return 2; }; + if (streq(name, "i16")) { return 2; }; + if (streq(name, "u32")) { return 4; }; + if (streq(name, "i32")) { return 4; }; + if (streq(name, "f32")) { return 4; }; + if (streq(name, "u64")) { return 8; }; + if (streq(name, "i64")) { return 8; }; + if (streq(name, "uint")) { return 8; }; + if (streq(name, "int")) { return 8; }; + if (streq(name, "uintptr")) { return 8; }; + if (streq(name, "f64")) { return 8; }; + if (streq(name, "rune")) { return 4; }; + if (streq(name, "void")) { return 0; }; + return 0; +}; + +fn slot_size(c: *cgen, typ_n: *node) i32 = { + if (typ_n == nil) { return 8; }; + let k: i32 = typ_n.kind; + if (k == N_TPTR) { return 8; }; + if (k == N_TFN) { return 8; }; + if (k == N_TCHAN) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TTUPLE) { return 16; }; + if (k == N_TTAGGED){ return 24; }; + if (k == N_TNAME) { + let nm: str = typ_n.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { + // Pad to 8 for stack slots — matches C cgen which spills + // every primitive into an 8-byte slot. + return 8; + }; + // Named struct lookup. + let si: *struct_info = struct_lookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TARRAY) { + let len_n: *node = typ_n.rhs; + let elem_n: *node = typ_n.lhs; + let elen: i64 = 1i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { elen = len_n.uval: i64; }; + }; + let esz: i32 = 8; + if (elem_n != nil) { + if (elem_n.kind == N_TNAME) { + let en: str = elem_n.str; + let ps: i32 = prim_size(en); + if (ps > 0) { esz = ps; }; + }; + }; + return (esz: i64 * elen): i32; + }; + if (k == N_TSTRUCT) { + // Inline anonymous struct — sum of field sizes. + let f: *node = typ_n.list; + let total: i32 = 0; + for (f != nil) { + if (f.kind == N_TFIELD) { + total += slot_size(c, f.lhs); + }; + f = f.next; + }; + return total; + }; + return 8; +}; + +// register_struct — compute field offsets + total size for a struct +// type-decl, store in c.structs. Field type sizes use the same +// slot_size logic (with primitives kept at their natural width — we +// only round to 8 for stack slots, not struct interiors). +fn field_size(c: *cgen, tnode: *node) i32 = { + if (tnode == nil) { return 8; }; + let k: i32 = tnode.kind; + if (k == N_TNAME) { + let nm: str = tnode.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + let si: *struct_info = struct_lookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TPTR) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TARRAY) { + // Same shape as slot_size's TARRAY branch. + let len_n: *node = tnode.rhs; + let elem_n: *node = tnode.lhs; + let elen: i64 = 1i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { elen = len_n.uval: i64; }; + }; + let esz: i32 = field_size(c, elem_n); + return (esz: i64 * elen): i32; + }; + return 8; +}; + +fn register_struct(c: *cgen, name: str, tstruct: *node) void = { + let si: *struct_info = amalloc(c.a, 64u64): *struct_info; + si.sname = name; + si.fields = nil; + si.tot_size = 0; + let head: *field_info = nil; + let tail: *field_info = nil; + let off: i32 = 0; + let f: *node = tstruct.list; + for (f != nil) { + if (f.kind == N_TFIELD) { + let sz: i32 = field_size(c, f.lhs); + // Align to 8 for any field >= 4 bytes (matches our other + // cgen choices). i8/u8/bool may sit on odd byte offsets; + // the C cgen does similar best-effort packing. + let aln: i32 = 1; + if (sz >= 8) { aln = 8; } + else { if (sz >= 4) { aln = 4; } + else { if (sz >= 2) { aln = 2; }; }; }; + if ((off & (aln - 1)) != 0) { + off = (off + aln - 1) & ~(aln - 1); + }; + let fi: *field_info = amalloc(c.a, 48u64): *field_info; + fi.fname = f.str; + fi.foff = off; + fi.fsz = sz; + fi.tnode = f.lhs; + if (head == nil) { head = fi; tail = fi; } + else { tail.finext = fi; tail = fi; }; + off += sz; + }; + f = f.next; + }; + // Round total to 8 for stack-slot use. + if ((off & 7) != 0) { off = (off + 7) & ~7; }; + si.fields = head; + si.tot_size = off; + si.sinext = c.structs; + c.structs = si; +}; + +fn collect_structs(c: *cgen, file: *node) void = { + c.structs = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind == N_TSTRUCT) { + register_struct(c, d.str, body); + }; + }; + }; + d = d.next; + }; +}; + +// ---- frame pre-scan -------------------------------------------------- +// +// Recursively walks the body to count every local `let`. Each gets a +// slot sized by slot_size(typ); 8-byte default. Match-bindings + for- +// init lets count too. Params are added by the cgfn driver. + +fn scan_locals(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let total: i32 = 0; + if (n.kind == N_LET) { + // Match local_add's rounding: < 8 bumps to 8, then 8-align. + // scan_locals must agree with local_add or the prologue + // SUBQ undersizes the frame and lets overflow into the + // caller's stack — corrupting whatever's at -frameSize..-1 + // of the caller. + let sz: i32 = slot_size(c, n.lhs); + if (sz < 8) { sz = 8; }; + if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; + total += sz; + }; + // Match-arm binding (`case let v: T => ...`) gets a slot too. + if (n.kind == N_MCASE) { + let bn: str = n.str; + if (bn.len > 0) { + let pat: *node = n.lhs; + if (pat != nil) { + if (is_str_type(c, pat)) { total += 16; } + else { total += 8; }; + }; + }; + }; + if (n.lhs != nil) { total += scan_locals(c, n.lhs); }; + if (n.rhs != nil) { total += scan_locals(c, n.rhs); }; + if (n.cond != nil) { total += scan_locals(c, n.cond); }; + if (n.body != nil) { total += scan_locals(c, n.body); }; + if (n.els != nil) { total += scan_locals(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + total += scan_locals(c, m); + m = m.next; + }; + }; + return total; +}; + +// ---- statement cgen -------------------------------------------------- + +fn cgstmt(c: *cgen, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + if (k == N_BLOCK) { + let s: *node = n.list; + for (s != nil) { + cgstmt(c, s); + s = s.next; + }; + return; + }; + + if (k == N_RETURN) { + let rhs: *node = n.lhs; + if (rhs != nil) { + // Tuple return `return a, b;` — pack as (AX=v0, DX=v1). + // Matches C cgen: evaluate v1 first (PUSHQ), then v0 + // into AX, then POPQ DX. End state: AX = v0, DX = v1. + if (rhs.kind == N_TUPLE) { + let v: *node = rhs.list; + if (v != nil) { + let v2: *node = v.next; + if (v2 != nil) { + cgexpr(c, v2); + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, v); + emit_line("\tPOPQ\tDX\n"); + } else { + cgexpr(c, v); + }; + }; + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + // Tagged-union return: pack as (AX=tag, DX=value0, CX=value1). + // For str variant, cgexpr leaves (AX=ptr, BX=len), so we + // shuffle DX←AX (ptr) and CX←BX (len), then load tag. + // For other variants, cgexpr leaves AX, shuffle DX←AX. + if (is_tagged_type(c.fn_ret)) { + cgexpr(c, rhs); + let idx: i32 = tagged_variant_index(c, c.fn_ret, rhs); + if (node_isstr(c, rhs)) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tMOVQ\tAX, DX\n"); + } else { + emit_line("\tMOVQ\tAX, DX\n"); + }; + emit_line("\tMOVQ\t$"); + if (idx < 0) { idx = 0; }; + emit_int(idx: i64); + emit_line(", AX\n"); + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + cgexpr(c, rhs); + }; + // SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX). + // cgexpr leaves str in (AX, BX); shuffle BX→DX. + if (is_str_type(c, c.fn_ret)) { + emit_line("\tMOVQ\tBX, DX\n"); + }; + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + + if (k == N_EXPRSTMT) { + if (n.lhs != nil) { cgexpr(c, n.lhs); }; + c.last_was_return = 0; + return; + }; + + if (k == N_LET) { + let nm: str = n.str; + let sz: i32 = slot_size(c, n.lhs); + let off: i32 = local_add(c, nm, sz, n.lhs); + if (n.rhs != nil) { + let rhs: *node = n.rhs; + // Tagged-union init: `let r: (T | E) = expr;`. + // - If rhs is a CALL to a fn returning tagged-union, + // the result is already in (AX=tag, DX=v0, CX=v1); + // just spill all three. + // - Otherwise rhs is a bare variant value: pack tag + + // value(s). + if (is_tagged_type(n.lhs)) { + let rhs_returns_tagged: bool = false; + if (rhs.kind == N_CALL) { + let callee: *node = rhs.lhs; + if (callee != nil) { + let callee_name: str; + callee_name.ptr = nil; callee_name.len = 0; + if (callee.kind == N_IDENT) { callee_name = callee.str; }; + if (callee.kind == N_DOT) { callee_name = callee.str; }; + if (callee_name.len > 0) { + let rt: *node = fnret_lookup(c, callee_name); + if (is_tagged_type(rt)) { rhs_returns_tagged = true; }; + }; + }; + }; + cgexpr(c, rhs); + if (rhs_returns_tagged) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tDX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tCX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + c.last_was_return = 0; + return; + }; + let tag_idx: i32 = tagged_variant_index(c, n.lhs, rhs); + if (tag_idx < 0) { tag_idx = 0; }; + if (node_isstr(c, rhs)) { + emit_line("\tMOVQ\tAX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tBX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + } else { + emit_line("\tMOVQ\tAX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + emit_line("\tMOVQ\t$"); + emit_int(tag_idx: i64); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + c.last_was_return = 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. + if (rhs.kind == N_STRUCTLIT) { + let trefn: *node = rhs.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (trefn != nil) { + if (trefn.kind == N_IDENT) { sname = trefn.str; } + else { if (trefn.kind == N_TNAME) { sname = trefn.str; }; }; + }; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let field_node: *node = rhs.list; + for (field_node != nil) { + if (field_node.kind == N_FIELD) { + let fname: str = field_node.str; + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + cgexpr(c, field_node.lhs); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_off((off + fi.foff): i64); + emit_line("(BP)\n"); + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + field_node = field_node.next; + }; + c.last_was_return = 0; + return; + }; + }; + cgexpr(c, rhs); + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + // str init: cgexpr also leaves len in BX; store both. + if (sz == 16) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + // slice init: ptr/len/cap in AX/BX/CX. + if (sz == 24) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tCX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + }; + }; + c.last_was_return = 0; + return; + }; + + if (k == N_IF) { + let els: str = mklabel(c, "else"); + let endl: str = mklabel(c, "end"); + cgexpr(c, n.cond); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); + if (n.els != nil) { emit_line(els); } + else { emit_line(endl); }; + emit_line("\n"); + if (n.body != nil) { cgstmt(c, n.body); }; + if (n.els != nil) { + emit_line("\tJMP\t"); emit_line(endl); emit_line("\n"); + emit_label(els); + cgstmt(c, n.els); + }; + emit_label(endl); + c.last_was_return = 0; + return; + }; + + if (k == N_FOR) { + // Match C cgen's label scheme: _loop_N for the top, + // _endloop_N for the post-body merge. No separate cont + // label when there's no post-expression. + let topl: str = mklabel(c, "loop"); + let endl: str = mklabel(c, "endloop"); + + if (n.lhs != nil) { cgstmt(c, n.lhs); }; + + emit_label(topl); + if (n.cond != nil) { + cgexpr(c, n.cond); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); emit_line(endl); emit_line("\n"); + }; + + c.loop_end_buf[c.loop_top] = endl; + c.loop_cont_buf[c.loop_top] = topl; + c.loop_top += 1; + + if (n.body != nil) { cgstmt(c, n.body); }; + + c.loop_top -= 1; + + if (n.rhs != nil) { cgexpr(c, n.rhs); }; + emit_line("\tJMP\t"); emit_line(topl); emit_line("\n"); + emit_label(endl); + c.last_was_return = 0; + return; + }; + + if (k == N_BREAK) { + if (c.loop_top > 0) { + let lbl: str = c.loop_end_buf[c.loop_top - 1]; + emit_line("\tJMP\t"); emit_line(lbl); emit_line("\n"); + }; + c.last_was_return = 0; + return; + }; + if (k == N_CONTINUE) { + if (c.loop_top > 0) { + let lbl: str = c.loop_cont_buf[c.loop_top - 1]; + emit_line("\tJMP\t"); emit_line(lbl); emit_line("\n"); + }; + c.last_was_return = 0; + return; + }; + + c.last_was_return = 0; +}; + +// ---- function-level cgen --------------------------------------------- + +// is_str_type — true when the type expr resolves (through any +// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the +// alias chain registered at file load. +fn is_str_type_raw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + }; + return false; +}; + +fn is_str_type(c: *cgen, t: *node) bool = { + if (is_str_type_raw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolve_type(c, t); + return is_str_type_raw(r); +}; + +fn is_slice_type_raw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TSLICE) { return true; }; + return false; +}; + +fn is_slice_type(c: *cgen, t: *node) bool = { + if (is_slice_type_raw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolve_type(c, t); + return is_slice_type_raw(r); +}; + +fn is_tagged_type(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TTAGGED) { return true; }; + return false; +}; + +// rhs_target_name — for a returned value, what's its declared (or +// surface-inferred) type name? `expr: T` casts dictate T directly; +// bare strlit/intlit fall back to a primitive name. +fn rhs_target_name(c: *cgen, rhs: *node) str = { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (rhs == nil) { return nm; }; + if (rhs.kind == N_CAST) { + let t: *node = rhs.rhs; + if (t != nil) { + if (t.kind == N_TNAME) { return t.str; }; + }; + return nm; + }; + if (rhs.kind == N_STRLIT) { return "str"; }; + if (rhs.kind == N_IDENT) { + let lc: *local = local_find_node(c, rhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { return tn.str; }; + }; + }; + }; + return nm; +}; + +// tagged_variant_index — given the tagged-union type expr and the +// returned value's surface type, find the matching variant's 0-based +// index. Compare by exact type name first; if no match, fall back to +// "any str-shape variant matches an str-typed value". +fn tagged_variant_index(c: *cgen, tagged: *node, rhs: *node) i32 = { + if (tagged == nil) { return -1; }; + if (rhs == nil) { return -1; }; + let want_name: str = rhs_target_name(c, rhs); + if (want_name.len > 0) { + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, want_name)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + }; + // Fallback: by str-shape (resolves aliases). + let want_str: bool = node_isstr(c, rhs); + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let v_is_str: bool = false; + if (v.kind == N_TNAME) { + if (is_str_type(c, v)) { v_is_str = true; }; + }; + if (v_is_str == want_str) { return idx; }; + v = v.next; + idx += 1; + }; + return -1; +}; + +fn cgfn_params(c: *cgen, params: *node) void = { + let p: *node = params; + let idx: i32 = 0; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (is_tagged_type(p.lhs)) { + // tagged-union param: passed in 3 regs (tag, v0, v1), + // 24-byte slot. + let off: i32 = local_add(c, nm, 24, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + idx += 1; + } else { if (is_slice_type(c, p.lhs)) { + // slice param: 3 regs (ptr, len, cap), 24-byte slot. + let off: i32 = local_add(c, nm, 24, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + idx += 1; + } else { if (is_str_type(c, p.lhs)) { + // str param: passed in two regs (ptr, len). + // Slot is 16 bytes; ptr at off+0, len at off+8. + let off: i32 = local_add(c, nm, 16, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + } else { + let off: i32 = local_add(c, nm, 8, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + };};}; + }; + p = p.next; + }; +}; + +fn cgfn(c: *cgen, fn_: *node) void = { + cgen_init(c, c.a); + c.fn_name = fn_.str; + c.fn_ret = fn_.lhs; + + emit_line("TEXT "); + let nm: str = fn_.str; + os.write(1, nm.ptr, nm.len: u64); + emit_line(",$"); + + // Pre-scan total frame: 24 bytes per slice param, 16 per str + // param, 8 per other param, plus per-let from scan_locals. + let scan_p: *node = fn_.list; + let frame: i32 = 0; + for (scan_p != nil) { + if (scan_p.kind == N_PARAM) { + if (is_tagged_type(scan_p.lhs)) { frame += 24; } + else { if (is_slice_type(c, scan_p.lhs)) { frame += 24; } + else { if (is_str_type(c, scan_p.lhs)) { frame += 16; } + else { frame += 8; }; }; }; + }; + scan_p = scan_p.next; + }; + if (fn_.body != nil) { frame += scan_locals(c, fn_.body); }; + if ((frame & 15) != 0) { + frame = (frame + 15) & ~15; + }; + emit_int(frame: i64); + emit_line("\n"); + + emit_line("\tPUSHQ\tBP\n"); + emit_line("\tMOVQ\tSP, BP\n"); + emit_line("\tSUBQ\t$"); + emit_int(frame: i64); + emit_line(", SP\n"); + + cgfn_params(c, fn_.list); + c.last_was_return = 0; + if (fn_.body != nil) { cgstmt(c, fn_.body); }; + + if (c.last_was_return == 0) { + // Zero AX before the fall-through return — matches C cgen, + // which always emits this so void-returning fns don't leak + // a stale callee value to their caller. + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + }; +}; + +// ---- file-level entry ------------------------------------------------ + +export fn cg_file(c: *cgen, file: *node) void = { + if (file == nil) { return; }; + c.strlits = nil; + c.strlit_seq = 0; + collect_aliases(c, file); + collect_structs(c, file); + collect_defs(c, file); + collect_fnrets(c, file); + ffi_collect(c, file); + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + if (d.body != nil) { + cgfn(c, d); + }; + }; + d = d.next; + }; + emit_data_section(c); + emit_def_constants(c, file); +}; diff --git a/selfhost/cmd/wwc/check.ww b/selfhost/cmd/wwc/check.ww new file mode 100644 index 00000000..db04e51b --- /dev/null +++ b/selfhost/cmd/wwc/check.ww @@ -0,0 +1,247 @@ +// selfhost/cmd/wwc/check.ww — minimal port of cmd/wwc/check.c. +// +// Status: name-resolution + primitive-type seeding only. Full type +// inference, conversion rules, tagged-union dispatch typing, return- +// type checking, etc. all live in cmd/wwc/check.c (937 lines) and +// will land here in subsequent commits. +// +// What this version does: +// 1. Creates a top scope and seeds it with primitive type names so +// `i32`, `str`, `*u8` etc. resolve. +// 2. Walks the file's top-level decls (use/def/type/fn/let) and +// installs Sym entries for each. +// 3. Recursively walks fn bodies; for every N_IDENT used as an +// expression or as a type name, looks it up and counts the +// resolved vs. unresolved. +// 4. Returns a summary the caller (wwdump -r) prints; the test +// asserts unresolved == 0 on every selfhost fixture, which is +// the floor signal that the frontend can name-resolve real ww. + +use os; +use mem; +use tok; + +type checker = struct { + a: *arena, + tc: *tctx, + top: *scope, + cur: *scope, + nresolved: i32, + nunresolved: i32, + errs: i32, + verbose: i32, // when non-zero, log each unresolved name +}; + +// seed_primitives — install the built-in type names so `i32`, `str`, +// etc. can be looked up like ordinary symbols. +fn seed_primitives(c: *checker) void = { + scope_define(c.top, "void", SK_TYPE, c.tc.ty_void, nil); + scope_define(c.top, "bool", SK_TYPE, c.tc.ty_bool, nil); + scope_define(c.top, "rune", SK_TYPE, c.tc.ty_rune, nil); + scope_define(c.top, "i8", SK_TYPE, c.tc.ty_i8, nil); + scope_define(c.top, "i16", SK_TYPE, c.tc.ty_i16, nil); + scope_define(c.top, "i32", SK_TYPE, c.tc.ty_i32, nil); + scope_define(c.top, "i64", SK_TYPE, c.tc.ty_i64, nil); + scope_define(c.top, "u8", SK_TYPE, c.tc.ty_u8, nil); + scope_define(c.top, "u16", SK_TYPE, c.tc.ty_u16, nil); + scope_define(c.top, "u32", SK_TYPE, c.tc.ty_u32, nil); + scope_define(c.top, "u64", SK_TYPE, c.tc.ty_u64, nil); + scope_define(c.top, "int", SK_TYPE, c.tc.ty_int, nil); + scope_define(c.top, "uint", SK_TYPE, c.tc.ty_uint, nil); + scope_define(c.top, "uintptr", SK_TYPE, c.tc.ty_uintptr, nil); + scope_define(c.top, "f32", SK_TYPE, c.tc.ty_f32, nil); + scope_define(c.top, "f64", SK_TYPE, c.tc.ty_f64, nil); + scope_define(c.top, "str", SK_TYPE, c.tc.ty_str, nil); + // `nil`, `true`, `false` are keywords — handled at the lex/parser + // level, no symbol needed. + // `len`, `alloc`, `free` are pseudo-builtins; scope_define them so + // their use sites resolve. The actual semantics live in cgen. + scope_define(c.top, "len", SK_FN, nil, nil); + scope_define(c.top, "alloc", SK_FN, nil, nil); + scope_define(c.top, "free", SK_FN, nil, nil); +}; + +// install_decl — install the top-level decl's name into the top scope. +// We don't compute its type yet (that's the resolve pass) — just bind +// the name so forward references resolve. +fn install_decl(c: *checker, d: *node) void = { + if (d == nil) { return; }; + let k: i32 = d.kind; + let nm: str = d.str; + if (k == N_USE) { scope_define(c.top, nm, SK_USE, nil, d); return; }; + if (k == N_DEF) { scope_define(c.top, nm, SK_DEF, nil, d); return; }; + if (k == N_TYPEDECL) { scope_define(c.top, nm, SK_TYPE, nil, d); return; }; + if (k == N_FNDECL) { scope_define(c.top, nm, SK_FN, nil, d); return; }; + if (k == N_LET) { scope_define(c.top, nm, SK_VAR, nil, d); return; }; +}; + +// resolve_walk — recursive AST walk that, for every N_IDENT and +// N_TNAME seen, looks up the name and bumps the resolved/unresolved +// counters. Local lets are installed in the current scope as soon as +// their init/type expressions have been walked (forward use of a let +// before its declaration would resolve to nothing — same semantics as +// the C checker's collect-then-resolve flow within a function). +fn resolve_walk(c: *checker, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + // `use IDENT;` — name is a module label, not a free ident. + if (k == N_USE) { return; }; + + if (k == N_IDENT) { + let nm: str = n.str; + if (nm.len > 0) { + let s: *sym = scope_lookup(c.cur, nm); + if (s == nil) { + c.nunresolved += 1; + if (c.verbose != 0) { + os.write(2, " unresolved id: ".ptr, 17u64); + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "\n".ptr, 1u64); + }; + } else { c.nresolved += 1; }; + }; + }; + + if (k == N_TNAME) { + let nm: str = n.str; + if (nm.len > 0) { + let s: *sym = scope_lookup(c.cur, nm); + if (s == nil) { + c.nunresolved += 1; + if (c.verbose != 0) { + os.write(2, " unresolved tname: ".ptr, 20u64); + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "\n".ptr, 1u64); + }; + } else { c.nresolved += 1; }; + }; + }; + + // `match (e) { case let v: T => stmt; ... }` — the binding `v` + // is declared by the case arm and visible inside its body. + if (k == N_MCASE) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + let nm: str = n.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_VAR, nil, n); + }; + if (n.body != nil) { resolve_walk(c, n.body); }; + return; + }; + + if (k == N_DOT) { + // Walk only the base; the .field name is a member, not a + // free identifier. + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + if (k == N_FIELD) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + if (k == N_TFIELD) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + // Walk children (mirroring ast.ww's printer descent order). + if (n.attr != nil) { resolve_walk(c, n.attr); }; + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + if (n.rhs != nil) { resolve_walk(c, n.rhs); }; + if (n.cond != nil) { resolve_walk(c, n.cond); }; + if (n.body != nil) { resolve_walk(c, n.body); }; + if (n.els != nil) { resolve_walk(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + resolve_walk(c, m); + m = m.next; + }; + }; + + // After walking children: a local `let X: T = init;` registers + // `X` so subsequent statements can resolve it. Top-level lets + // are installed in install_decl, so this duplicate install at + // the file scope just no-ops (scope_define returns nil on dup). + if (k == N_LET) { + let nm: str = n.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_VAR, nil, n); + }; + }; +}; + +// install_param — when entering a fn body, define its params in a +// fresh local scope. +fn install_params(c: *checker, params: *node) void = { + let p: *node = params; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_PARAM, nil, p); + }; + }; + p = p.next; + }; +}; + +// resolve_fnbody — open a child scope for the fn, install its params, +// then walk the body. Local lets installed by walk_stmt (a future +// extension); for the current pass we just resolve-walk without +// per-statement scopes. +fn resolve_fnbody(c: *checker, fnnode: *node) void = { + let outer: *scope = c.cur; + c.cur = newscope(c.a, c.cur); + install_params(c, fnnode.list); + if (fnnode.body != nil) { + resolve_walk(c, fnnode.body); + }; + c.cur = outer; +}; + +export fn check_init(c: *checker, a: *arena, tc: *tctx) void = { + c.a = a; + c.tc = tc; + c.top = newscope(a, nil); + c.cur = c.top; + c.nresolved = 0; + c.nunresolved = 0; + c.errs = 0; + c.verbose = 0; + seed_primitives(c); +}; + +export fn check_file(c: *checker, file: *node) void = { + if (file == nil) { return; }; + if (file.kind != N_FILE) { return; }; + + // Pass 1: install all top-level names. + let d: *node = file.list; + for (d != nil) { + install_decl(c, d); + d = d.next; + }; + + // Pass 2: walk decl bodies/types and resolve identifiers. + d = file.list; + for (d != nil) { + let k: i32 = d.kind; + if (k == N_FNDECL) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; // return type + resolve_fnbody(c, d); + } else { if (k == N_DEF) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + if (d.rhs != nil) { resolve_walk(c, d.rhs); }; + } else { if (k == N_TYPEDECL) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + } else { if (k == N_LET) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + if (d.rhs != nil) { resolve_walk(c, d.rhs); }; + };};};}; + d = d.next; + }; +}; diff --git a/selfhost/cmd/wwc/err.ww b/selfhost/cmd/wwc/err.ww new file mode 100644 index 00000000..e5fff225 --- /dev/null +++ b/selfhost/cmd/wwc/err.ww @@ -0,0 +1,37 @@ +// selfhost/cmd/wwc/err.ww — port of cmd/wwc/err.c. +// +// Diagnostics. Plan 9 style: short, no levels beyond fatal/error/warn. +// Output goes through os.write so we don't pull in libc stdio. + +use os; +use fmt; + +type pos = struct { + file: str, + line: i32, + col: i32, +}; + +let nerrors: i32 = 0; +let nwarnings: i32 = 0; + +export fn fatal(msg: str) void = { + fmt.errln(msg); + os.exit(1); +}; + +export fn errorf(p: pos, msg: str) void = { + os.write(2, p.file.ptr, p.file.len: u64); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + nerrors += 1; +}; + +export fn warnf(p: pos, msg: str) void = { + os.write(2, p.file.ptr, p.file.len: u64); + os.write(2, ": warning: ".ptr, 11u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + nwarnings += 1; +}; diff --git a/selfhost/cmd/wwc/lex.ww b/selfhost/cmd/wwc/lex.ww new file mode 100644 index 00000000..5737a911 --- /dev/null +++ b/selfhost/cmd/wwc/lex.ww @@ -0,0 +1,656 @@ +// selfhost/cmd/wwc/lex.ww — port of cmd/wwc/lex.c. +// +// The DFA, the helpers, and the order of decisions all mirror the C +// version exactly. The 990_selfhost test diffs the resulting token +// stream against the C-side wwdump byte-for-byte; any divergence is +// a port bug. +// +// Calling-convention note: 6c can't yet pass or return structs >16 +// bytes by value, so `tok` and `pos` are passed by pointer (out +// params). The C version passes `Tok` by value; we differ here only +// in shape, not in observable behaviour. Token kind values stay +// numerically identical. + +use os; +use ascii; +use mem; +use tok; + +type lex = struct { + file: str, + src: *u8, // raw bytes; not necessarily NUL-terminated + srclen: u64, + lpos: u64, + line: i32, + col: i32, + a: *arena, + errs: i32, +}; + +export fn lexinit(l: *lex, a: *arena, file: str, src: *u8, len: u64) void = { + l.file = file; + l.src = src; + l.srclen = len; + l.lpos = 0u64; + l.line = 1; + l.col = 1; + l.a = a; + l.errs = 0; +}; + +// srcb — byte at offset; helper that lifts the cast out of indexing. +fn srcb(l: *lex, off: u64) i32 = { + let i: i32 = off: i32; + let b: u8 = l.src[i]; + return b: i32; +}; + +fn lpeek(l: *lex, ahead: u64) i32 = { + let p: u64 = l.lpos + ahead; + if (p >= l.srclen) { return -1; }; + return srcb(l, p); +}; + +fn lget(l: *lex) i32 = { + if (l.lpos >= l.srclen) { return -1; }; + let c: i32 = srcb(l, l.lpos); + l.lpos += 1u64; + if (c == 10) { // '\n' + l.line += 1; + l.col = 1; + } else { + l.col += 1; + }; + return c; +}; + +fn cur_pos(l: *lex, out: *pos) void = { + out.file = l.file; + out.line = l.line; + out.col = l.col; +}; + +// putuint — write `v` (signed, but always non-negative here) to fd 2 +// in decimal. Standalone so err_at doesn't drag in fmt and create a +// dependency cycle with strconv. +fn putuint(fd: i32, v: i32) void = { + let tmp: [16]u8; + let i: i32 = 0; + let n: i32 = v; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { tmp[0] = 48u8; i = 1; }; + let buf: [16]u8; + let m: i32 = 0; + for (i > 0) { i -= 1; buf[m] = tmp[i]; m += 1; }; + os.write(fd, buf.ptr, m: u64); +}; + +fn err_at(l: *lex, p: *pos, msg: str) void = { + let pf: str = p.file; + os.write(2, pf.ptr, pf.len: u64); + os.write(2, ":".ptr, 1u64); + putuint(2, p.line); + os.write(2, ":".ptr, 1u64); + putuint(2, p.col); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + l.errs += 1; +}; + +fn skipws(l: *lex) bool = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { return false; }; + if (c == 32) { lget(l); continue; }; + if (c == 9) { lget(l); continue; }; + if (c == 13) { lget(l); continue; }; + if (c == 10) { lget(l); continue; }; + if (c == 47) { // '/' + let c2: i32 = lpeek(l, 1u64); + if (c2 == 47) { + for (true) { + let cx: i32 = lpeek(l, 0u64); + if (cx < 0) { return false; }; + if (cx == 10) { break; }; + lget(l); + }; + continue; + }; + if (c2 == 42) { // '*' + lget(l); lget(l); + let prev: i32 = -1; + for (true) { + let x: i32 = lget(l); + if (x < 0) { + let cp: pos; + cur_pos(l, &cp); + err_at(l, &cp, "unterminated /* comment"); + return false; + }; + if (prev == 42) { + if (x == 47) { break; }; + }; + prev = x; + }; + continue; + }; + }; + return true; + }; + return false; +}; + +fn parseint(p: *u8, n: u64, base: i32, ok: *bool) u64 = { + let v: u64 = 0u64; + let got: bool = false; + let i: u64 = 0u64; + for (i < n) { + let ix: i32 = i: i32; + let c: u8 = p[ix]; + if (c == 95u8) { // '_' + i += 1u64; + continue; + }; + let d: i32 = -1; + if (c >= 48u8) { + if (c <= 57u8) { d = (c - 48u8): i32; }; + }; + if (d < 0) { + if (c >= 97u8) { + if (c <= 102u8) { d = ((c - 97u8) + 10u8): i32; }; + }; + }; + if (d < 0) { + if (c >= 65u8) { + if (c <= 70u8) { d = ((c - 65u8) + 10u8): i32; }; + }; + }; + if (d < 0) { *ok = false; return 0u64; }; + if (d >= base) { *ok = false; return 0u64; }; + v = v * (base: u64) + (d: u64); + got = true; + i += 1u64; + }; + *ok = got; + return v; +}; + +fn escape(l: *lex, out: *i32) bool = { + let c: i32 = lget(l); + if (c < 0) { return false; }; + if (c == 110) { *out = 10; return true; }; + if (c == 116) { *out = 9; return true; }; + if (c == 114) { *out = 13; return true; }; + if (c == 92) { *out = 92; return true; }; + if (c == 39) { *out = 39; return true; }; + if (c == 34) { *out = 34; return true; }; + if (c == 48) { *out = 0; return true; }; + if (c == 97) { *out = 7; return true; }; + if (c == 98) { *out = 8; return true; }; + if (c == 102) { *out = 12; return true; }; + if (c == 118) { *out = 11; return true; }; + if (c == 120) { + let hi: i32 = lget(l); + let lo: i32 = lget(l); + if (hi < 0) { return false; }; + if (lo < 0) { return false; }; + if (!ascii.ishex(hi: u8)) { + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad \\x escape"); + return false; + }; + if (!ascii.ishex(lo: u8)) { + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad \\x escape"); + return false; + }; + let h: i32 = ascii.digitval(hi: u8); + let lv: i32 = ascii.digitval(lo: u8); + *out = (h << 4) | lv; + return true; + }; + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad escape"); + return false; +}; + +// scan_decimal_run — consume a run of decimal digits and underscores. +fn scan_decimal_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isdigit(c: u8)) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +fn scan_hex_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.ishex(c: u8)) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +fn scan_bin_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c == 48) { lget(l); continue; }; + if (c == 49) { lget(l); continue; }; + if (c == 95) { lget(l); continue; }; + break; + }; +}; + +fn scan_oct_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 48) { break; }; + if (c > 55) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +// scan_exp — consume the [eE][+-]?[0-9]+ tail of a float, if present. +fn scan_exp(l: *lex) void = { + let e: i32 = lpeek(l, 0u64); + if (e != 101) { if (e != 69) { return; }; }; // 'e' or 'E' + lget(l); + let s: i32 = lpeek(l, 0u64); + if (s == 43) { lget(l); } + else { if (s == 45) { lget(l); }; }; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isdigit(c: u8)) { break; }; + lget(l); + }; +}; + +fn lexnum(l: *lex, start: *pos, out: *tok) void = { + out.kind = TK_INT; + out.file = start.file; + out.line = start.line; + out.col = start.col; + let begin: u64 = l.lpos; + let base: i32 = 10; + let isfloat: bool = false; + + let c0: i32 = lpeek(l, 0u64); + let c1: i32 = lpeek(l, 1u64); + + if (c0 == 48) { // '0' + if (c1 == 120) { // 'x' + lget(l); lget(l); base = 16; scan_hex_run(l); + } else { if (c1 == 88) { // 'X' + lget(l); lget(l); base = 16; scan_hex_run(l); + } else { if (c1 == 98) { // 'b' + lget(l); lget(l); base = 2; scan_bin_run(l); + } else { if (c1 == 66) { // 'B' + lget(l); lget(l); base = 2; scan_bin_run(l); + } else { if (c1 == 111) { // 'o' + lget(l); lget(l); base = 8; scan_oct_run(l); + } else { if (c1 == 79) { // 'O' + lget(l); lget(l); base = 8; scan_oct_run(l); + } else { + scan_decimal_run(l); + if (lpeek(l, 0u64) == 46) { + let after: i32 = lpeek(l, 1u64); + if (after >= 48) { + if (after <= 57) { + isfloat = true; + lget(l); + scan_decimal_run(l); + scan_exp(l); + }; + }; + }; + };};};};};}; + } else { + scan_decimal_run(l); + if (lpeek(l, 0u64) == 46) { + let after: i32 = lpeek(l, 1u64); + if (after >= 48) { + if (after <= 57) { + isfloat = true; + lget(l); + scan_decimal_run(l); + scan_exp(l); + }; + }; + }; + }; + + let n: u64 = l.lpos - begin; + out.text = astrndup(l.a, l.src + begin, n); + + if (isfloat) { + // out.fval is already 0 from the top-of-lexnext clear. + // We don't strtod the literal yet — the diff fixtures we + // care about are float-free; any TK_FLOAT seen in source + // gets a placeholder value until we wire a real parser. + out.kind = TK_FLOAT; + } else { + let digs: *u8 = l.src + begin; + let dn: u64 = n; + if (base != 10) { + digs = digs + 2u64; + dn -= 2u64; + }; + let ok: bool = false; + out.uval = parseint(digs, dn, base, &ok); + if (!ok) { + err_at(l, start, "bad integer literal"); + out.kind = TK_ERR; + }; + }; + + let pc: i32 = lpeek(l, 0u64); + if (pc >= 0) { + if (ascii.isidstart(pc: u8)) { + let sb: u64 = l.lpos; + for (true) { + let cc: i32 = lpeek(l, 0u64); + if (cc < 0) { break; }; + if (!ascii.isidpart(cc: u8)) { break; }; + lget(l); + }; + let sl: u64 = l.lpos - sb; + let p: *u8 = l.src + sb; + let isok: bool = false; + if (sl == 2u64) { + if (p[0] == 105u8) { + if (p[1] == 56u8) { isok = true; }; // i8 + }; + if (p[0] == 117u8) { + if (p[1] == 56u8) { isok = true; }; // u8 + }; + }; + if (sl == 3u64) { + if (p[0] == 105u8) { + if (p[1] == 49u8) { if (p[2] == 54u8) { isok = true; }; }; // i16 + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; // i32 + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; // i64 + }; + if (p[0] == 117u8) { + if (p[1] == 49u8) { if (p[2] == 54u8) { isok = true; }; }; + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; + }; + if (p[0] == 102u8) { + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; // f32 + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; // f64 + }; + }; + if (isok) { + out.tsuffix = astrndup(l.a, p, sl); + } else { + l.lpos = sb; + }; + }; + }; +}; + +fn lexident(l: *lex, start: *pos, out: *tok) void = { + let begin: u64 = l.lpos; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isidpart(c: u8)) { break; }; + lget(l); + }; + 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; + if (k != TK_NONE) { + out.kind = k; + } else { + out.kind = TK_IDENT; + }; + out.text = astrndup(l.a, p, n); +}; + +fn lexstr(l: *lex, start: *pos, out: *tok) void = { + let cap: u64 = 32u64; + let nb: u64 = 0u64; + let buf: *u8 = amalloc(l.a, cap): *u8; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { + err_at(l, start, "unterminated string"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + if (c == 34) { lget(l); break; }; + let ch: i32 = 0; + if (c == 92) { + lget(l); + if (!escape(l, &ch)) { ch = 0; }; + } else { + ch = lget(l); + }; + if (nb + 1u64 >= cap) { + let ncap: u64 = cap * 2u64; + let nb2: *u8 = amalloc(l.a, ncap): *u8; + let i: u64 = 0u64; + for (i < nb) { + let ix: i32 = i: i32; + nb2[ix] = buf[ix]; + i += 1u64; + }; + buf = nb2; + cap = ncap; + }; + let nbi: i32 = nb: i32; + buf[nbi] = ch: u8; + nb += 1u64; + }; + out.kind = TK_STR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + let s: str; + s.ptr = buf; + s.len = nb: i32; + out.text = s; +}; + +fn lexrune(l: *lex, start: *pos, out: *tok) void = { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { + err_at(l, start, "unterminated rune"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + let ch: i32 = 0; + if (c == 92) { + lget(l); + if (!escape(l, &ch)) { ch = 0; }; + } else { + ch = lget(l); + }; + if (lpeek(l, 0u64) != 39) { + err_at(l, start, "rune literal missing closing '"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + lget(l); + out.kind = TK_RUNE; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.uval = ch: u64; +}; + +fn emit_simple(start: *pos, k: i32, out: *tok) void = { + out.kind = k; + out.file = start.file; + out.line = start.line; + out.col = start.col; +}; + +// set_pos_from — copy file/line/col from a *pos into a tok. Used by +// the err-token path where we already have a pos. +fn set_pos_from(out: *tok, p: *pos) void = { + out.file = p.file; + out.line = p.line; + out.col = p.col; +}; + +export fn lexnext(l: *lex, out: *tok) void = { + // Reset the out token so callers can rely on stale fields being + // cleared (they only inspect kind, pos, text, uval, fval, tsuffix + // per kind). + out.kind = TK_NONE; + out.uval = 0u64; + // out.fval starts cleared by the caller's stack-local init (lex.ww + // allocates the tok with `let t: tok;` which zeroes). We avoid + // writing a 0.0 literal here so this file itself stays float-free + // and the C/ww wwdump diff over it is byte-identical. + let empty: str; + empty.ptr = nil; + empty.len = 0; + out.text = empty; + out.tsuffix = empty; + + if (!skipws(l)) { + let p: pos; cur_pos(l, &p); + emit_simple(&p, TK_EOF, out); + return; + }; + let start: pos; cur_pos(l, &start); + let c: i32 = lpeek(l, 0u64); + + if (c >= 0) { + if (ascii.isidstart(c: u8)) { lexident(l, &start, out); return; }; + if (ascii.isdigit(c: u8)) { lexnum(l, &start, out); return; }; + }; + + if (c == 34) { lget(l); lexstr(l, &start, out); return; }; + if (c == 39) { lget(l); lexrune(l, &start, out); return; }; + + lget(l); + + if (c == 40) { emit_simple(&start, TK_LPAREN, out); return; }; + if (c == 41) { emit_simple(&start, TK_RPAREN, out); return; }; + if (c == 123) { emit_simple(&start, TK_LBRACE, out); return; }; + if (c == 125) { emit_simple(&start, TK_RBRACE, out); return; }; + if (c == 91) { emit_simple(&start, TK_LBRACK, out); return; }; + if (c == 93) { emit_simple(&start, TK_RBRACK, out); return; }; + if (c == 44) { emit_simple(&start, TK_COMMA, out); return; }; + if (c == 59) { emit_simple(&start, TK_SEMI, out); return; }; + if (c == 58) { emit_simple(&start, TK_COLON, out); return; }; + if (c == 64) { emit_simple(&start, TK_AT, out); return; }; + if (c == 63) { emit_simple(&start, TK_QUESTION, out); return; }; + if (c == 126) { emit_simple(&start, TK_TILDE, out); return; }; + + if (c == 46) { // '.' + if (lpeek(l, 0u64) == 46) { + if (lpeek(l, 1u64) == 46) { + lget(l); lget(l); + emit_simple(&start, TK_ELLIPSIS, out); return; + }; + lget(l); + emit_simple(&start, TK_DOTDOT, out); return; + }; + emit_simple(&start, TK_DOT, out); return; + }; + + if (c == 43) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PLUSEQ, out); return; }; + emit_simple(&start, TK_PLUS, out); return; + }; + if (c == 45) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_MINUSEQ, out); return; }; + if (lpeek(l, 0u64) == 62) { lget(l); emit_simple(&start, TK_ARROW, out); return; }; + emit_simple(&start, TK_MINUS, out); return; + }; + if (c == 42) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_STAREQ, out); return; }; + emit_simple(&start, TK_STAR, out); return; + }; + if (c == 47) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_SLASHEQ, out); return; }; + emit_simple(&start, TK_SLASH, out); return; + }; + if (c == 37) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PERCENTEQ, out); return; }; + emit_simple(&start, TK_PERCENT, out); return; + }; + if (c == 38) { + if (lpeek(l, 0u64) == 38) { lget(l); emit_simple(&start, TK_AND, out); return; }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_AMPEQ, out); return; }; + emit_simple(&start, TK_AMP, out); return; + }; + if (c == 124) { + if (lpeek(l, 0u64) == 124) { lget(l); emit_simple(&start, TK_OR, out); return; }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PIPEEQ, out); return; }; + emit_simple(&start, TK_PIPE, out); return; + }; + if (c == 94) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_CARETEQ, out); return; }; + emit_simple(&start, TK_CARET, out); return; + }; + if (c == 61) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_EQ, out); return; }; + if (lpeek(l, 0u64) == 62) { lget(l); emit_simple(&start, TK_FATARROW, out); return; }; + emit_simple(&start, TK_ASSIGN, out); return; + }; + if (c == 33) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_NEQ, out); return; }; + emit_simple(&start, TK_NOT, out); return; + }; + if (c == 60) { + if (lpeek(l, 0u64) == 60) { + lget(l); + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_LSHIFTEQ, out); return; }; + emit_simple(&start, TK_LSHIFT, out); return; + }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_LE, out); return; }; + if (lpeek(l, 0u64) == 45) { lget(l); emit_simple(&start, TK_LARROW, out); return; }; + emit_simple(&start, TK_LT, out); return; + }; + if (c == 62) { + if (lpeek(l, 0u64) == 62) { + lget(l); + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_RSHIFTEQ, out); return; }; + emit_simple(&start, TK_RSHIFT, out); return; + }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_GE, out); return; }; + emit_simple(&start, TK_GT, out); return; + }; + + err_at(l, &start, "unexpected character"); + out.kind = TK_ERR; + set_pos_from(out, &start); + let one: [1]u8; + one[0] = c: u8; + out.text = astrndup(l.a, one.ptr, 1u64); +}; diff --git a/selfhost/cmd/wwc/mem.ww b/selfhost/cmd/wwc/mem.ww new file mode 100644 index 00000000..9788933c --- /dev/null +++ b/selfhost/cmd/wwc/mem.ww @@ -0,0 +1,106 @@ +// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c. +// +// Bump arena allocator. Backed by the runtime page allocator +// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the +// current chunk runs out we link a fresh one. Freeing the arena +// unmaps the chain. +// +// Memory handed out is 16-byte aligned. The C version under +// cmd/wwc/ is retained until the three-stage bootstrap diffs clean. + +use os; + +def ALIGN: u64 = 16u64; +def INIT_CHUNK: u64 = 65536u64; +def MAX_CHUNK: u64 = 4194304u64; +def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, + next: *arena, + total: u64, +}; + +fn roundup(n: u64, a: u64) u64 = { + return (n + a - 1u64) & ~(a - 1u64); +}; + +export fn newarena() *arena = { + let a: *arena = os.alloc(ARENA_SZ): *arena; + a.buf = os.alloc(INIT_CHUNK): *u8; + a.off = 0u64; + a.cap = INIT_CHUNK; + a.next = nil; + a.total = 0u64; + return a; +}; + +// Grow: link a fresh chunk in front of the head. We push the old +// chunk into `next` so the head always describes the current bump +// region. Chunk size doubles up to MAX_CHUNK. +fn grow(a: *arena, need: u64) bool = { + let want: u64 = a.cap * 2u64; + if (want < need) { want = need; }; + if (want > MAX_CHUNK) { want = MAX_CHUNK; }; + if (want < need) { return false; }; // single allocation too big + + let old: *arena = os.alloc(ARENA_SZ): *arena; + old.buf = a.buf; + old.off = a.off; + old.cap = a.cap; + old.next = a.next; + old.total = 0u64; + + a.buf = os.alloc(want): *u8; + a.off = 0u64; + a.cap = want; + a.next = old; + return true; +}; + +export fn amalloc(a: *arena, n: u64) *void = { + let need: u64 = roundup(n, ALIGN); + if (need > a.cap - a.off) { + if (!grow(a, need)) { return nil; }; + }; + let p: *u8 = a.buf + a.off; + a.off += need; + a.total += need; + // Zero the region. Plan 9 amalloc zeroes; we mirror that here so + // the checker can assume freshly allocated nodes start at 0. + let i: u64 = 0u64; + for (i < need) { + p[i] = 0u8; + i += 1u64; + }; + return p: *void; +}; + +// astrndup — copy `n` bytes into the arena and produce a NUL-terminated +// view. Returns a `str` whose ptr is arena-owned and whose len is `n` +// (the trailing NUL is past `len`, so callers reading exactly n bytes +// see no padding). Used by the lexer to capture token text. +export fn astrndup(a: *arena, src: *u8, n: u64) str = { + let p: *u8 = amalloc(a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { + p[i] = src[i]; + i += 1u64; + }; + p[n] = 0u8; + let r: str; + r.ptr = p; + r.len = n: i32; + return r; +}; + +export fn freearena(a: *arena) void = { + for (a != nil) { + let next: *arena = a.next; + os.free(a.buf: *void, a.cap); + os.free(a: *void, ARENA_SZ); + a = next; + }; +}; diff --git a/selfhost/cmd/wwc/parse.ww b/selfhost/cmd/wwc/parse.ww new file mode 100644 index 00000000..b012dbff --- /dev/null +++ b/selfhost/cmd/wwc/parse.ww @@ -0,0 +1,960 @@ +// selfhost/cmd/wwc/parse.ww — port of cmd/wwc/parse.c. +// +// Status: GROWING stub. Currently handles top-level `use IDENT;`, +// `def NAME: TYPE = LIT;`, `type NAME = TYPE;`, and `fn NAME(params) +// RET;` (header-only — bodies are recovered past). Unknown decls are +// chewed token-by-token until the next ';' so the diff probe can +// still anchor on partial fixtures. +// +// The full port is multi-session work — parse.c is 1,183 lines of +// hand-rolled recursive descent + Pratt expression parser. Each +// surface form lands here gradually so the AST diff in 990_selfhost +// grows toward whole-language coverage one increment at a time. +// +// Calling-convention shim: 6c can't yet pass a sub-struct field +// (e.g. p.cur.line where p.cur is a `tok` of size 76). The parser +// stores the current token as flat primitive fields rather than a +// nested `tok` struct; `refill` copies a freshly lexed token in. + +use os; +use mem; +use tok; + +type parser = struct { + l: *lex, + a: *arena, + errs: i32, + // nocast: while inside `[...]` we treat ':' as the slice + // separator, not the cast operator. Mirrors parse.c's flag. + nocast: i32, + cur_kind: i32, + cur_file: str, + cur_line: i32, + cur_col: i32, + cur_text: str, + cur_uval: u64, +}; + +fn refill(p: *parser) void = { + let t: tok; + lexnext(p.l, &t); + p.cur_kind = t.kind; + p.cur_file = t.file; + p.cur_line = t.line; + p.cur_col = t.col; + p.cur_text = t.text; + p.cur_uval = t.uval; +}; + +export fn parserinit(p: *parser, a: *arena, l: *lex) void = { + p.l = l; + p.a = a; + p.errs = 0; + p.nocast = 0; + refill(p); +}; + +fn advance(p: *parser) void = { refill(p); }; + +fn accept_tok(p: *parser, k: i32) bool = { + if (p.cur_kind == k) { advance(p); return true; }; + return false; +}; + +fn err_msg(p: *parser, msg: str) void = { + let pre: str = "parse: "; + os.write(2, pre.ptr, pre.len: u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + p.errs += 1; +}; + +fn expect_tok(p: *parser, k: i32, what: str) bool = { + if (p.cur_kind == k) { advance(p); return true; }; + err_msg(p, what); + return false; +}; + +// expectident — consume the current TK_IDENT and return its text. +// Returns the empty str on error (and advances to make progress). +fn expectident(p: *parser, into: *str) bool = { + if (p.cur_kind != TK_IDENT) { + err_msg(p, "expected identifier"); + advance(p); + return false; + }; + *into = p.cur_text; + advance(p); + return true; +}; + +// ---- type expressions ------------------------------------------------ +// +// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`). +// Other forms (slice, array, struct, fn, chan, tuple, tagged) will +// land in subsequent commits. + +fn parsetype(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + if (p.cur_kind == TK_STAR) { + advance(p); + let n: *node = newnode(p.a, N_TPTR, pf, pl, pc); + n.lhs = parsetype(p); + return n; + }; + + if (p.cur_kind == TK_LBRACK) { + advance(p); + if (p.cur_kind == TK_RBRACK) { + advance(p); + let n: *node = newnode(p.a, N_TSLICE, pf, pl, pc); + n.lhs = parsetype(p); + return n; + }; + let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc); + n.rhs = parseexpr(p); + expect_tok(p, TK_RBRACK, "expected ']' in array type"); + n.lhs = parsetype(p); + return n; + }; + + if (p.cur_kind == TK_STRUCT) { + advance(p); + expect_tok(p, TK_LBRACE, "expected '{' after struct"); + let n: *node = newnode(p.a, N_TSTRUCT, pf, pl, pc); + let fhead: *node = nil; + let ftail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let fpf: str = p.cur_file; + let fpl: i32 = p.cur_line; + let fpc: i32 = p.cur_col; + let f: *node = newnode(p.a, N_TFIELD, fpf, fpl, fpc); + let fid: str; + expectident(p, &fid); + f.str = fid; + expect_tok(p, TK_COLON, "expected ':' in field"); + f.lhs = parsetype(p); + if (fhead == nil) { fhead = f; ftail = f; } + else { ftail.next = f; ftail = f; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after struct fields"); + n.list = fhead; + return n; + }; + + if (p.cur_kind == TK_IDENT) { + let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); + n.str = p.cur_text; + advance(p); + // Dotted path collapse (pkg.Type) deferred — fixtures don't + // need it yet. + return n; + }; + + if (p.cur_kind == TK_LPAREN) { + // (T) or (T, T, ...) or (T | T | ...) + advance(p); + let first: *node = parsetype(p); + if (accept_tok(p, TK_PIPE)) { + let n: *node = newnode(p.a, N_TTAGGED, pf, pl, pc); + let head: *node = first; + let tail: *node = first; + for (true) { + let e: *node = parsetype(p); + tail.next = e; + tail = e; + if (!accept_tok(p, TK_PIPE)) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tagged-union type"); + n.list = head; + return n; + }; + if (!accept_tok(p, TK_COMMA)) { + expect_tok(p, TK_RPAREN, "expected ')' after parenthesised type"); + return first; + }; + let n: *node = newnode(p.a, N_TTUPLE, pf, pl, pc); + let head: *node = first; + let tail: *node = first; + for (true) { + let e: *node = parsetype(p); + tail.next = e; + tail = e; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == TK_RPAREN) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tuple type"); + n.list = head; + return n; + }; + + if (p.cur_kind == TK_FN) { + advance(p); + expect_tok(p, TK_LPAREN, "expected '(' after fn in type"); + let n: *node = newnode(p.a, N_TFN, pf, pl, pc); + // Anonymous-or-named params: parseparams handles named only; + // for fn-type expressions the C parser allows IDENT-less + // (anonymous) params. Stub: only named params for now. + n.list = parseparams(p); + expect_tok(p, TK_RPAREN, "expected ')' after fn type params"); + n.lhs = parsetype(p); + return n; + }; + + err_msg(p, "expected type"); + advance(p); + return newnode(p.a, N_TNAME, pf, pl, pc); +}; + +// ---- expressions (Pratt) --------------------------------------------- +// +// Forwards: parseexpr → parsebin → parseunary → parsepostfix(parseprimary). +// Tuple literals, match expressions, struct literals, slice [lo:hi], +// and the ?/! try operators are not yet wired — they'll arrive as the +// AST diff fixture grows to need them. + +fn bprec(k: i32) i32 = { + if (k == TK_OR) { return 1; }; + if (k == TK_AND) { return 2; }; + if (k == TK_EQ) { return 3; }; + if (k == TK_NEQ) { return 3; }; + if (k == TK_LT) { return 4; }; + if (k == TK_LE) { return 4; }; + if (k == TK_GT) { return 4; }; + if (k == TK_GE) { return 4; }; + if (k == TK_PIPE) { return 5; }; + if (k == TK_CARET) { return 6; }; + if (k == TK_AMP) { return 7; }; + if (k == TK_LSHIFT) { return 8; }; + if (k == TK_RSHIFT) { return 8; }; + if (k == TK_PLUS) { return 9; }; + if (k == TK_MINUS) { return 9; }; + if (k == TK_STAR) { return 10; }; + if (k == TK_SLASH) { return 10; }; + if (k == TK_PERCENT) { return 10; }; + return 0; +}; + +fn isassignop(k: i32) bool = { + if (k == TK_ASSIGN) { return true; }; + if (k == TK_PLUSEQ) { return true; }; + if (k == TK_MINUSEQ) { return true; }; + if (k == TK_STAREQ) { return true; }; + if (k == TK_SLASHEQ) { return true; }; + if (k == TK_PERCENTEQ) { return true; }; + if (k == TK_AMPEQ) { return true; }; + if (k == TK_PIPEEQ) { return true; }; + if (k == TK_CARETEQ) { return true; }; + if (k == TK_LSHIFTEQ) { return true; }; + if (k == TK_RSHIFTEQ) { return true; }; + return false; +}; + +// Forward references between parseunary/parseexpr/parsebin/parsepostfix +// are resolved by the two-pass checker — no body-less prototypes needed. + +fn parseprimary(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + if (p.cur_kind == TK_INT) { + let n: *node = newnode(p.a, N_INTLIT, pf, pl, pc); + n.uval = p.cur_uval; + n.str = p.cur_text; + advance(p); + return n; + }; + if (p.cur_kind == TK_STR) { + let n: *node = newnode(p.a, N_STRLIT, pf, pl, pc); + n.str = p.cur_text; + advance(p); + return n; + }; + if (p.cur_kind == TK_RUNE) { + let n: *node = newnode(p.a, N_RUNELIT, pf, pl, pc); + n.uval = p.cur_uval; + advance(p); + return n; + }; + if (p.cur_kind == TK_TRUE) { + advance(p); + return newnode(p.a, N_TRUE, pf, pl, pc); + }; + if (p.cur_kind == TK_FALSE) { + advance(p); + return newnode(p.a, N_FALSE, pf, pl, pc); + }; + if (p.cur_kind == TK_NIL) { + advance(p); + return newnode(p.a, N_NIL, pf, pl, pc); + }; + if (p.cur_kind == TK_LPAREN) { + advance(p); + let e: *node = parseexpr(p); + // Tuple literal: (a, b, ...) + if (accept_tok(p, TK_COMMA)) { + let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc); + t.list = e; + let tail: *node = e; + for (true) { + if (p.cur_kind == TK_RPAREN) { break; }; + let en: *node = parseexpr(p); + tail.next = en; + tail = en; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tuple"); + return t; + }; + expect_tok(p, TK_RPAREN, "expected ')'"); + return e; + }; + if (p.cur_kind == TK_IDENT) { + let n: *node = newnode(p.a, N_IDENT, pf, pl, pc); + n.str = p.cur_text; + advance(p); + // `IDENT {` — struct literal. Disambiguate: only consume as a + // struct lit when we're not in a context where '{' starts a + // block (e.g. `if (cond) {`). The parser is called from + // expressions, never directly from cond contexts that need a + // block; in stmt parsing, the for/if drivers consume their + // own paren/cond, so this is safe. + if (p.cur_kind == TK_LBRACE) { + advance(p); + let s: *node = newnode(p.a, N_STRUCTLIT, pf, pl, pc); + s.lhs = n; + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let fpf: str = p.cur_file; + let fpl: i32 = p.cur_line; + let fpc: i32 = p.cur_col; + let id: str; + expectident(p, &id); + expect_tok(p, TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(p.a, N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + return s; + }; + return n; + }; + if (p.cur_kind == TK_MATCH) { + // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; + advance(p); + expect_tok(p, TK_LPAREN, "expected '(' after match"); + let m: *node = newnode(p.a, N_MATCH, pf, pl, pc); + m.lhs = parseexpr(p); + expect_tok(p, TK_RPAREN, "expected ')' after match scrutinee"); + expect_tok(p, TK_LBRACE, "expected '{' to open match body"); + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind == TK_CASE) { + let cf: str = p.cur_file; + let cl: i32 = p.cur_line; + let cc: i32 = p.cur_col; + advance(p); // past `case` + let mc: *node = newnode(p.a, N_MCASE, cf, cl, cc); + if (p.cur_kind == TK_LET) { + advance(p); + let id: str; + expectident(p, &id); + mc.str = id; + expect_tok(p, TK_COLON, "expected ':' after match binding"); + mc.lhs = parsetype(p); + } else { if (p.cur_kind != TK_FATARROW) { + mc.lhs = parsetype(p); + };}; + expect_tok(p, TK_FATARROW, "expected '=>' in match arm"); + mc.body = parsestmt(p); + if (head == nil) { head = mc; tail = mc; } + else { tail.next = mc; tail = mc; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after match body"); + m.list = head; + return m; + }; + err_msg(p, "expected expression"); + advance(p); + return newnode(p.a, N_NONE, pf, pl, pc); +}; + +fn parsearglist(p: *parser, close_kind: i32, head_out: **node) void = { + *head_out = nil; + if (p.cur_kind == close_kind) { return; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let e: *node = parseexpr(p); + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == close_kind) { break; }; + }; + *head_out = head; +}; + +fn parsepostfix(p: *parser, lhs: *node) *node = { + let cur: *node = lhs; + for (true) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + if (p.cur_kind == TK_LPAREN) { + 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; + expect_tok(p, TK_RPAREN, "expected ')' after args"); + cur = n; + continue; + }; + if (p.cur_kind == TK_LBRACK) { + advance(p); + // `[ : hi ]` — slice with implicit lo = 0. + if (p.cur_kind == TK_COLON) { + advance(p); + let n: *node = newnode(p.a, N_SLICE, pf, pl, pc); + n.lhs = cur; + if (p.cur_kind != TK_RBRACK) { + n.cond = parseexpr(p); + }; + expect_tok(p, TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + // Suppress cast inside `[...]` so ':' parses as slice + // separator rather than the postfix cast operator. + let prev: i32 = p.nocast; + p.nocast = 1; + let e: *node = parseexpr(p); + p.nocast = prev; + if (p.cur_kind == TK_COLON) { + advance(p); + let n: *node = newnode(p.a, N_SLICE, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + if (p.cur_kind != TK_RBRACK) { + n.cond = parseexpr(p); + }; + expect_tok(p, TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + let n: *node = newnode(p.a, N_INDEX, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + expect_tok(p, TK_RBRACK, "expected ']' after index"); + cur = n; + continue; + }; + if (p.cur_kind == TK_DOT) { + advance(p); + let n: *node = newnode(p.a, N_DOT, pf, pl, pc); + n.lhs = cur; + let id: str; + expectident(p, &id); + n.str = id; + cur = n; + continue; + }; + if (p.cur_kind == TK_COLON) { + if (p.nocast != 0) { + return cur; + }; + advance(p); + let n: *node = newnode(p.a, N_CAST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + break; + }; + return cur; +}; + +fn parseunary(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + let k: i32 = p.cur_kind; + if (k == TK_MINUS) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_MINUS; n.lhs = parseunary(p); + return n; + }; + if (k == TK_PLUS) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_PLUS; n.lhs = parseunary(p); + return n; + }; + if (k == TK_NOT) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_NOT; n.lhs = parseunary(p); + return n; + }; + if (k == TK_TILDE) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_TILDE; n.lhs = parseunary(p); + return n; + }; + if (k == TK_STAR) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_STAR; n.lhs = parseunary(p); + return n; + }; + if (k == TK_AMP) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_AMP; n.lhs = parseunary(p); + return n; + }; + return parsepostfix(p, parseprimary(p)); +}; + +fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { + let cur: *node = lhs; + for (true) { + let op: i32 = p.cur_kind; + let pr: i32 = bprec(op); + if (pr == 0) { return cur; }; + if (pr < minp) { return cur; }; + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); + let rhs: *node = parseunary(p); + for (true) { + let np: i32 = bprec(p.cur_kind); + if (np <= pr) { break; }; + rhs = parsebin(p, rhs, np); + }; + let n: *node = newnode(p.a, N_BIN, pf, pl, pc); + n.op = op; n.lhs = cur; n.rhs = rhs; + cur = n; + }; + return cur; +}; + +fn parseexpr(p: *parser) *node = { + let e: *node = parsebin(p, parseunary(p), 1); + if (isassignop(p.cur_kind)) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + let op: i32 = p.cur_kind; + advance(p); + let n: *node = newnode(p.a, N_ASSIGN, pf, pl, pc); + n.op = op; + n.lhs = e; + n.rhs = parseexpr(p); // right-associative + return n; + }; + return e; +}; + +// ---- statements ------------------------------------------------------ +// +// Subset wired today: block, let, return, if (no else-if chain), for +// (single-cond C-style), expr-stmt, defer, break, continue. Switch +// and match arms are not yet wired; tuple-let / multi-let neither. + +fn parselet_local(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `let` + let n: *node = newnode(p.a, N_LET, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + if (accept_tok(p, TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accept_tok(p, TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expect_tok(p, TK_SEMI, "expected ';' after let"); + return n; +}; + +fn parseblock(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + expect_tok(p, TK_LBRACE, "expected '{' to open block"); + let blk: *node = newnode(p.a, N_BLOCK, pf, pl, pc); + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let s: *node = parsestmt(p); + if (s != nil) { + if (head == nil) { head = s; tail = s; } + else { tail.next = s; tail = s; }; + }; + }; + expect_tok(p, TK_RBRACE, "expected '}' to close block"); + blk.list = head; + return blk; +}; + +fn parseif(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `if` + expect_tok(p, TK_LPAREN, "expected '(' after if"); + let n: *node = newnode(p.a, N_IF, pf, pl, pc); + n.cond = parseexpr(p); + expect_tok(p, TK_RPAREN, "expected ')' after if condition"); + n.body = parseblock(p); + if (accept_tok(p, TK_ELSE)) { + if (p.cur_kind == TK_IF) { + n.els = parseif(p); + } else { + n.els = parseblock(p); + }; + }; + return n; +}; + +fn parsefor(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `for` + expect_tok(p, TK_LPAREN, "expected '(' after for"); + let n: *node = newnode(p.a, N_FOR, pf, pl, pc); + // Three forms (matching C parser): + // for (cond) — only cond + // for (init; cond; post) — full + // for (true) — infinite (cond is N_TRUE) + // Distinguish by counting ';'. Look at first chunk: if it's a + // `let` stmt that's the init. Otherwise, parse expr; if next is + // ';' it was cond. If we see two ';' total after init, post is + // next. Simpler: peek for `let` to decide init form. + if (p.cur_kind == TK_LET) { + n.lhs = parselet_local(p); // init (consumes its own ';') + n.cond = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after for cond"); + n.rhs = parseexpr(p); + } else { + // Parse one expr. If next is ';', it's a 3-clause without init. + let first: *node = parseexpr(p); + if (accept_tok(p, TK_SEMI)) { + // cond ; post + n.cond = first; + n.rhs = parseexpr(p); + } else { + // just (cond) + n.cond = first; + }; + }; + expect_tok(p, TK_RPAREN, "expected ')' after for"); + n.body = parseblock(p); + return n; +}; + +fn parsestmt(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + // `static` is allowed on local lets per Hare; we accept and skip + // it (it doesn't change the AST shape). + if (p.cur_kind == TK_STATIC) { advance(p); }; + + if (p.cur_kind == TK_LBRACE) { + let b: *node = parseblock(p); + expect_tok(p, TK_SEMI, "expected ';' after block"); + return b; + }; + if (p.cur_kind == TK_LET) { return parselet_local(p); }; + if (p.cur_kind == TK_IF) { + let n: *node = parseif(p); + expect_tok(p, TK_SEMI, "expected ';' after if"); + return n; + }; + if (p.cur_kind == TK_FOR) { + let n: *node = parsefor(p); + expect_tok(p, TK_SEMI, "expected ';' after for"); + return n; + }; + if (p.cur_kind == TK_RETURN) { + advance(p); + let n: *node = newnode(p.a, N_RETURN, pf, pl, pc); + if (p.cur_kind != TK_SEMI) { + let first: *node = parseexpr(p); + // Hare-style multi-value: `return a, b;` becomes a + // tuple expression so codegen sees one rvalue. + if (p.cur_kind == TK_COMMA) { + let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc); + t.list = first; + let tail: *node = first; + for (accept_tok(p, TK_COMMA)) { + let e: *node = parseexpr(p); + tail.next = e; + tail = e; + }; + n.lhs = t; + } else { + n.lhs = first; + }; + }; + expect_tok(p, TK_SEMI, "expected ';' after return"); + return n; + }; + if (p.cur_kind == TK_DEFER) { + advance(p); + let n: *node = newnode(p.a, N_DEFER, pf, pl, pc); + n.lhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after defer"); + return n; + }; + if (p.cur_kind == TK_BREAK) { + advance(p); + expect_tok(p, TK_SEMI, "expected ';' after break"); + return newnode(p.a, N_BREAK, pf, pl, pc); + }; + if (p.cur_kind == TK_CONTINUE) { + advance(p); + expect_tok(p, TK_SEMI, "expected ';' after continue"); + return newnode(p.a, N_CONTINUE, pf, pl, pc); + }; + // expression statement + let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc); + n.lhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after expression statement"); + return n; +}; + +// ---- top-level decl parsers ------------------------------------------ + +fn parseuse(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `use` + let n: *node = newnode(p.a, N_USE, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_SEMI, "expected ';' after use"); + return n; +}; + +fn parsedef(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `def` + let n: *node = newnode(p.a, N_DEF, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_COLON, "expected ':' in def"); + n.lhs = parsetype(p); + expect_tok(p, TK_ASSIGN, "expected '=' in def"); + n.rhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after def"); + n.exported = exported; + return n; +}; + +fn parselet(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `let` + let n: *node = newnode(p.a, N_LET, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + if (accept_tok(p, TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accept_tok(p, TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expect_tok(p, TK_SEMI, "expected ';' after let"); + n.exported = exported; + return n; +}; + +fn parseattrs(p: *parser) *node = { + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind == TK_AT) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); + let a: *node = newnode(p.a, N_ATTR, pf, pl, pc); + let id: str; + expectident(p, &id); + a.str = id; + expect_tok(p, TK_LPAREN, "expected '(' after attribute name"); + let arghead: *node = nil; + parsearglist(p, TK_RPAREN, &arghead); + a.list = arghead; + expect_tok(p, TK_RPAREN, "expected ')' after attribute args"); + if (head == nil) { head = a; tail = a; } + else { tail.next = a; tail = a; }; + }; + return head; +}; + +fn parseparams(p: *parser) *node = { + if (p.cur_kind == TK_RPAREN) { return nil; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + 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. + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_COLON, "expected ':' in parameter"); + n.lhs = parsetype(p); + if (head == nil) { head = n; tail = n; } + else { tail.next = n; tail = n; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == TK_RPAREN) { break; }; + }; + return head; +}; + +fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `fn` + let n: *node = newnode(p.a, N_FNDECL, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_LPAREN, "expected '(' after fn name"); + n.list = parseparams(p); + expect_tok(p, TK_RPAREN, "expected ')' after params"); + if (p.cur_kind != TK_ASSIGN) { + if (p.cur_kind != TK_SEMI) { + n.lhs = parsetype(p); + }; + }; + if (accept_tok(p, TK_ASSIGN)) { + n.body = parseblock(p); + expect_tok(p, TK_SEMI, "expected ';' after fn body"); + } else { + // Body-less fn: FFI declaration (`fn name(args) ret;`). + expect_tok(p, TK_SEMI, "expected ';' after fn header"); + }; + n.exported = exported; + n.attr = attrs; + return n; +}; + +fn parsetypedecl(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `type` + let n: *node = newnode(p.a, N_TYPEDECL, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_ASSIGN, "expected '=' in type decl"); + n.lhs = parsetype(p); + expect_tok(p, TK_SEMI, "expected ';' after type decl"); + n.exported = exported; + return n; +}; + +// ---- file-level loop ------------------------------------------------- + +export fn parsefile(p: *parser) *node = { + let f: *node = newnode(p.a, N_FILE, p.cur_file, p.cur_line, p.cur_col); + let head: *node = nil; + let tail: *node = nil; + + for (p.cur_kind != TK_EOF) { + let attrs: *node = parseattrs(p); + let exported: i32 = 0; + if (p.cur_kind == TK_EXPORT) { exported = 1; advance(p); }; + + let d: *node = nil; + if (p.cur_kind == TK_USE) { + d = parseuse(p); + } else { if (p.cur_kind == TK_DEF) { + d = parsedef(p, exported); + } else { if (p.cur_kind == TK_TYPE) { + d = parsetypedecl(p, exported); + } else { if (p.cur_kind == TK_LET) { + d = parselet(p, exported); + } else { if (p.cur_kind == TK_FN) { + d = parsefn(p, exported, attrs); + } else { + // Recovery: chew tokens until next ';' or EOF, balancing + // '{' '}' pairs so internal ';'s in unfamiliar forms don't + // derail us. + for (p.cur_kind != TK_SEMI) { + if (p.cur_kind == TK_EOF) { break; }; + if (p.cur_kind == TK_LBRACE) { + let depth: i32 = 0; + for (true) { + if (p.cur_kind == TK_EOF) { break; }; + if (p.cur_kind == TK_LBRACE) { depth += 1; advance(p); continue; }; + if (p.cur_kind == TK_RBRACE) { + depth -= 1; + advance(p); + if (depth == 0) { break; }; + continue; + }; + advance(p); + }; + continue; + }; + advance(p); + }; + if (p.cur_kind == TK_SEMI) { advance(p); }; + };};};};}; + + if (d != nil) { + if (head == nil) { + head = d; + tail = d; + } else { + tail.next = d; + tail = d; + }; + }; + }; + f.list = head; + return f; +}; diff --git a/selfhost/cmd/wwc/sym.ww b/selfhost/cmd/wwc/sym.ww new file mode 100644 index 00000000..f3484c00 --- /dev/null +++ b/selfhost/cmd/wwc/sym.ww @@ -0,0 +1,113 @@ +// selfhost/cmd/wwc/sym.ww — port of cmd/wwc/sym.c. +// +// Per-scope hashtable, chained to the parent. Lookup walks up. +// Plan 9 / Hare flavoured. Duplicate definitions in the same scope +// return nil; the caller flags the error. + +use mem; +use typ; +use ast; + +// Symbol kinds — must stay numerically aligned with cmd/wwc/ww.h Skind. +def SK_NONE: i32 = 0; +def SK_VAR: i32 = 1; +def SK_PARAM: i32 = 2; +def SK_DEF: i32 = 3; +def SK_TYPE: i32 = 4; +def SK_FN: i32 = 5; +def SK_USE: i32 = 6; +def SK_FIELD: i32 = 7; + +type sym = struct { + name: str, + skind: i32, + type_: *tinfo, + decl: *node, + exported: i32, + snext: *sym, // iteration order + hashnext: *sym, // hash bucket chain + scope: *scope, +}; + +def NBUCKETS: i32 = 16; + +type scope = struct { + parent: *scope, + first: *sym, + last: *sym, + buckets: **sym, // length = NBUCKETS + nbuckets: i32, + a: *arena, +}; + +// FNV-1a 64 — same hash the C side uses, so bucket distribution is +// identical when both walk a scope in declaration order. +fn hashstr(s: str) u64 = { + let h: u64 = 14695981039346656037u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + h = h ^ (c: u64); + h = h * 1099511628211u64; + i += 1; + }; + return h; +}; + +export fn newscope(a: *arena, parent: *scope) *scope = { + let s: *scope = amalloc(a, 64u64): *scope; + s.parent = parent; + s.a = a; + s.nbuckets = NBUCKETS; + s.buckets = amalloc(a, (NBUCKETS: u64) * 8u64): **sym; + return s; +}; + +export fn streq(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; +}; + +export fn scope_lookup_local(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + let bn: str = b.name; + if (streq(bn, name)) { return b; }; + b = b.hashnext; + }; + return nil; +}; + +export fn scope_lookup(s: *scope, name: str) *sym = { + for (s != nil) { + let r: *sym = scope_lookup_local(s, name); + if (r != nil) { return r; }; + s = s.parent; + }; + return nil; +}; + +export fn scope_define(s: *scope, name: str, k: i32, t: *tinfo, decl: *node) *sym = { + if (scope_lookup_local(s, name) != nil) { return nil; }; + let sy: *sym = amalloc(s.a, 80u64): *sym; + sy.name = name; + sy.skind = k; + sy.type_ = t; + sy.decl = decl; + sy.scope = s; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + sy.hashnext = s.buckets[bi]; + s.buckets[bi] = sy; + if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; + s.last = sy; + return sy; +}; diff --git a/selfhost/cmd/wwc/tok.ww b/selfhost/cmd/wwc/tok.ww new file mode 100644 index 00000000..6150bbc9 --- /dev/null +++ b/selfhost/cmd/wwc/tok.ww @@ -0,0 +1,394 @@ +// selfhost/cmd/wwc/tok.ww — port of cmd/wwc/tok.c plus the Tkind / +// Tok / Pos shapes from cmd/wwc/ww.h. +// +// Token kind values must stay numerically equal to the C side: the +// 990_selfhost test diffs ww-side wwdump output against C-side +// wwdump output, byte-for-byte. Reordering this list shifts the +// integers and breaks the diff. +// +// Bottom of file: tokprint, which emits one token per line in a +// format identical to cmd/wwc/tok.c:tokprint(). + +use os; +use strconv; + +// ---- Tkind ------------------------------------------------------------ +// Mirror of the C enum in cmd/wwc/ww.h. Don't reorder. + +def TK_NONE: i32 = 0; +def TK_EOF: i32 = 1; +def TK_ERR: i32 = 2; +def TK_IDENT: i32 = 3; +def TK_INT: i32 = 4; +def TK_FLOAT: i32 = 5; +def TK_RUNE: i32 = 6; +def TK_STR: i32 = 7; + +def TK_FN: i32 = 8; +def TK_LET: i32 = 9; +def TK_DEF: i32 = 10; +def TK_IF: i32 = 11; +def TK_ELSE: i32 = 12; +def TK_FOR: i32 = 13; +def TK_SWITCH: i32 = 14; +def TK_CASE: i32 = 15; +def TK_RETURN: i32 = 16; +def TK_USE: i32 = 17; +def TK_TYPE: i32 = 18; +def TK_STRUCT: i32 = 19; +def TK_DEFER: i32 = 20; +def TK_BREAK: i32 = 21; +def TK_CONTINUE: i32 = 22; +def TK_EXPORT: i32 = 23; +def TK_PROC: i32 = 24; +def TK_CHAN: i32 = 25; +def TK_NIL: i32 = 26; +def TK_TRUE: i32 = 27; +def TK_FALSE: i32 = 28; +def TK_AS: i32 = 29; +def TK_STATIC: i32 = 30; +def TK_MATCH: i32 = 31; + +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_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_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_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_AND: i32 = 74; +def TK_OR: i32 = 75; +def TK_NOT: i32 = 76; + +def TK_LARROW: i32 = 77; +def TK_ARROW: i32 = 78; +def TK_FATARROW: i32 = 79; + +def TK_LAST: i32 = 80; + +// ---- Pos / Tok -------------------------------------------------------- +// +// `pos` is used at error-reporting boundaries; we always pass it via +// *pos so the value never gets struct-copied (6c can't yet copy a +// 24-byte struct). +// +// `tok` is flat — file/line/col live directly on the token rather than +// nested inside a `pos` field. Same reason: nested struct field +// assignment isn't supported, and flat primitives are. + +type pos = struct { + file: str, + line: i32, + col: i32, +}; + +type tok = struct { + kind: i32, + file: str, // path of the source the token came from + line: i32, + col: i32, + text: str, // arena-owned token text (TK_IDENT, TK_STR, TK_ERR) + uval: u64, // TK_INT, TK_RUNE + fval: f64, // TK_FLOAT + tsuffix: str, // typed numeric literal suffix or empty +}; + +// ---- keyword lookup --------------------------------------------------- + +fn streq_n(a: *u8, b: str, n: i32) bool = { + if (b.len != n) { return false; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// kwlookup — returns the matching TK_* keyword kind for a byte run, +// or TK_NONE if it's an ordinary identifier. Linear search over a +// small alphabetised list, matching cmd/wwc/tok.c. +export fn kwlookup(p: *u8, n: i32) i32 = { + if (streq_n(p, "as", n)) { return TK_AS; }; + if (streq_n(p, "break", n)) { return TK_BREAK; }; + if (streq_n(p, "case", n)) { return TK_CASE; }; + if (streq_n(p, "chan", n)) { return TK_CHAN; }; + if (streq_n(p, "continue", n)) { return TK_CONTINUE; }; + if (streq_n(p, "def", n)) { return TK_DEF; }; + if (streq_n(p, "defer", n)) { return TK_DEFER; }; + if (streq_n(p, "else", n)) { return TK_ELSE; }; + if (streq_n(p, "export", n)) { return TK_EXPORT; }; + if (streq_n(p, "false", n)) { return TK_FALSE; }; + if (streq_n(p, "fn", n)) { return TK_FN; }; + if (streq_n(p, "for", n)) { return TK_FOR; }; + if (streq_n(p, "if", n)) { return TK_IF; }; + if (streq_n(p, "let", n)) { return TK_LET; }; + if (streq_n(p, "match", n)) { return TK_MATCH; }; + if (streq_n(p, "nil", n)) { return TK_NIL; }; + if (streq_n(p, "proc", n)) { return TK_PROC; }; + if (streq_n(p, "return", n)) { return TK_RETURN; }; + if (streq_n(p, "static", n)) { return TK_STATIC; }; + if (streq_n(p, "struct", n)) { return TK_STRUCT; }; + if (streq_n(p, "switch", n)) { return TK_SWITCH; }; + if (streq_n(p, "true", n)) { return TK_TRUE; }; + if (streq_n(p, "type", n)) { return TK_TYPE; }; + if (streq_n(p, "use", n)) { return TK_USE; }; + return TK_NONE; +}; + +// ---- tokname ---------------------------------------------------------- +// +// Returns the canonical printable spelling for a token kind. Matches +// the C tokname()'s output exactly so wwdump output diffs cleanly. + +export fn tokname(k: i32) str = { + if (k == TK_NONE) { return ""; }; + if (k == TK_EOF) { return "EOF"; }; + if (k == TK_ERR) { return "ERR"; }; + if (k == TK_IDENT) { return "IDENT"; }; + if (k == TK_INT) { return "INT"; }; + if (k == TK_FLOAT) { return "FLOAT"; }; + if (k == TK_RUNE) { return "RUNE"; }; + if (k == TK_STR) { return "STR"; }; + + if (k == TK_FN) { return "fn"; }; + if (k == TK_LET) { return "let"; }; + if (k == TK_DEF) { return "def"; }; + if (k == TK_IF) { return "if"; }; + if (k == TK_ELSE) { return "else"; }; + if (k == TK_FOR) { return "for"; }; + if (k == TK_SWITCH) { return "switch"; }; + if (k == TK_CASE) { return "case"; }; + if (k == TK_RETURN) { return "return"; }; + if (k == TK_USE) { return "use"; }; + if (k == TK_TYPE) { return "type"; }; + if (k == TK_STRUCT) { return "struct"; }; + if (k == TK_DEFER) { return "defer"; }; + if (k == TK_BREAK) { return "break"; }; + if (k == TK_CONTINUE) { return "continue"; }; + if (k == TK_EXPORT) { return "export"; }; + if (k == TK_PROC) { return "proc"; }; + if (k == TK_CHAN) { return "chan"; }; + if (k == TK_NIL) { return "nil"; }; + if (k == TK_TRUE) { return "true"; }; + if (k == TK_FALSE) { return "false"; }; + if (k == TK_AS) { return "as"; }; + if (k == TK_STATIC) { return "static"; }; + if (k == TK_MATCH) { return "match"; }; + + if (k == TK_LPAREN) { return "("; }; + if (k == TK_RPAREN) { return ")"; }; + if (k == TK_LBRACE) { return "{"; }; + if (k == TK_RBRACE) { return "}"; }; + if (k == TK_LBRACK) { return "["; }; + if (k == TK_RBRACK) { return "]"; }; + if (k == TK_COMMA) { return ","; }; + if (k == TK_SEMI) { return ";"; }; + if (k == TK_COLON) { return ":"; }; + if (k == TK_DOT) { return "."; }; + if (k == TK_ELLIPSIS) { return "..."; }; + if (k == TK_DOTDOT) { return ".."; }; + if (k == TK_AT) { return "@"; }; + if (k == TK_QUESTION) { return "?"; }; + + if (k == TK_ASSIGN) { return "="; }; + if (k == TK_PLUSEQ) { return "+="; }; + if (k == TK_MINUSEQ) { return "-="; }; + if (k == TK_STAREQ) { return "*="; }; + if (k == TK_SLASHEQ) { return "/="; }; + if (k == TK_PERCENTEQ) { return "%="; }; + if (k == TK_AMPEQ) { return "&="; }; + if (k == TK_PIPEEQ) { return "|="; }; + if (k == TK_CARETEQ) { return "^="; }; + if (k == TK_LSHIFTEQ) { return "<<="; }; + if (k == TK_RSHIFTEQ) { return ">>="; }; + + if (k == TK_PLUS) { return "+"; }; + if (k == TK_MINUS) { return "-"; }; + if (k == TK_STAR) { return "*"; }; + if (k == TK_SLASH) { return "/"; }; + if (k == TK_PERCENT) { return "%"; }; + if (k == TK_AMP) { return "&"; }; + if (k == TK_PIPE) { return "|"; }; + if (k == TK_CARET) { return "^"; }; + if (k == TK_TILDE) { return "~"; }; + if (k == TK_LSHIFT) { return "<<"; }; + if (k == TK_RSHIFT) { return ">>"; }; + + if (k == TK_EQ) { return "=="; }; + if (k == TK_NEQ) { return "!="; }; + if (k == TK_LT) { return "<"; }; + if (k == TK_LE) { return "<="; }; + if (k == TK_GT) { return ">"; }; + if (k == TK_GE) { return ">="; }; + + if (k == TK_AND) { return "&&"; }; + if (k == TK_OR) { return "||"; }; + if (k == TK_NOT) { return "!"; }; + + if (k == TK_LARROW) { return "<-"; }; + if (k == TK_ARROW) { return "->"; }; + if (k == TK_FATARROW) { return "=>"; }; + + if (k == TK_LAST) { return ""; }; + return ""; +}; + +// ---- writer for tokprint ---------------------------------------------- +// +// fputq mirrors cmd/wwc/tok.c:fputq — quote the string with C-style +// escapes for \, ", \n, \t, \r and \xNN for other non-printables. + +fn fputc_byte(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn fputs_str(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn hexchar(n: u8) u8 = { + if (n < 10u8) { return n + 48u8; }; // '0'..'9' + return (n - 10u8) + 97u8; // 'a'..'f' +}; + +fn fputhex2(fd: i32, b: u8) void = { + let out: [4]u8; + out[0] = 92u8; // '\\' + out[1] = 120u8; // 'x' + out[2] = hexchar(b >> 4u8); + out[3] = hexchar(b & 15u8); + os.write(fd, out.ptr, 4u64); +}; + +fn fputq(fd: i32, p: *u8, n: i32) void = { + fputc_byte(fd, 34u8); // '"' + let i: i32 = 0; + for (i < n) { + let c: u8 = p[i]; + if (c == 92u8) { // '\\' + fputs_str(fd, "\\\\"); + } else { + if (c == 34u8) { // '"' + fputs_str(fd, "\\\""); + } else { + if (c == 10u8) { // '\n' + fputs_str(fd, "\\n"); + } else { + if (c == 9u8) { // '\t' + fputs_str(fd, "\\t"); + } else { + if (c == 13u8) { // '\r' + fputs_str(fd, "\\r"); + } else { + if (c < 32u8) { + fputhex2(fd, c); + } else { + if (c == 127u8) { + fputhex2(fd, c); + } else { + fputc_byte(fd, c); + }; + }; + }; + }; + }; + }; + }; + i += 1; + }; + fputc_byte(fd, 34u8); +}; + +// tokprint — write one token line to fd. Format must match +// cmd/wwc/tok.c:tokprint() byte-for-byte: that's the diff anchor. +// ":: [ ]\n" +// +// Takes `t` by pointer because 6c can't yet pass a >16-byte struct +// by value; the C version takes Tok by value. +export fn tokprint(fd: i32, t: *tok) void = { + // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet + // reduced by 6c — `t.x.y` returns the whole str. Lift the str + // fields into locals so we can use the str pseudo-field path. + let tfile: str = t.file; + let ttext: str = t.text; + if (tfile.len > 0) { + fputs_str(fd, tfile); + } else { + fputs_str(fd, ""); + }; + fputc_byte(fd, 58u8); // ':' + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], t.line: i64); + os.write(fd, buf.ptr, n: u64); + fputc_byte(fd, 58u8); + n = strconv.i64toa(buf[0:32], t.col: i64); + os.write(fd, buf.ptr, n: u64); + fputc_byte(fd, 32u8); // ' ' + fputs_str(fd, tokname(t.kind)); + + if (t.kind == TK_IDENT) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_STR) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_ERR) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_INT) { + fputc_byte(fd, 32u8); + n = strconv.u64toa(buf[0:32], t.uval); + os.write(fd, buf.ptr, n: u64); + } else { if (t.kind == TK_RUNE) { + fputc_byte(fd, 32u8); + n = strconv.u64toa(buf[0:32], t.uval); + os.write(fd, buf.ptr, n: u64); + };};};};}; + // TK_FLOAT is intentionally not handled here — %g formatting + // won't byte-match across implementations. Diff fixtures must + // be float-free until we implement a stable float formatter. + + fputc_byte(fd, 10u8); // '\n' +}; diff --git a/selfhost/cmd/wwc/typ.ww b/selfhost/cmd/wwc/typ.ww new file mode 100644 index 00000000..09e17abf --- /dev/null +++ b/selfhost/cmd/wwc/typ.ww @@ -0,0 +1,329 @@ +// selfhost/cmd/wwc/type.ww — port of cmd/wwc/type.c. +// +// Status: full structural port. The C version uses module-globals for +// the primitive types (ty_void, ty_i32, …); ww doesn't have writable +// global storage yet, so we bundle the primitives into a `tctx` that +// the checker passes around explicitly. typesinit fills the tctx +// once per arena. + +use os; +use mem; + +// ---- TypeKind --------------------------------------------------------- +// Numeric values must stay aligned with cmd/wwc/ww.h TypeKind so the +// next diff signal (typed-AST printer / cgen) can compare across the +// two implementations. + +def TY_NONE: i32 = 0; +def TY_VOID: i32 = 1; +def TY_BOOL: i32 = 2; +def TY_RUNE: i32 = 3; +def TY_I8: i32 = 4; +def TY_I16: i32 = 5; +def TY_I32: i32 = 6; +def TY_I64: i32 = 7; +def TY_U8: i32 = 8; +def TY_U16: i32 = 9; +def TY_U32: i32 = 10; +def TY_U64: i32 = 11; +def TY_UINT: i32 = 12; +def TY_INT: i32 = 13; +def TY_UINTPTR: i32 = 14; +def TY_F32: i32 = 15; +def TY_F64: i32 = 16; +def TY_STR: i32 = 17; +def TY_PTR: i32 = 18; +def TY_SLICE: i32 = 19; +def TY_ARRAY: i32 = 20; +def TY_STRUCT: i32 = 21; +def TY_FN: i32 = 22; +def TY_CHAN: i32 = 23; +def TY_NAMED: i32 = 24; +def TY_TUPLE: i32 = 25; +def TY_TAGGED: i32 = 26; +def TY_ERR: i32 = 27; +def TY_UNTYPED_INT: i32 = 28; +def TY_UNTYPED_FLOAT: i32 = 29; +def TY_UNTYPED_STR: i32 = 30; +def TY_UNTYPED_RUNE: i32 = 31; +def TY_UNTYPED_BOOL: i32 = 32; +def TY_UNTYPED_NIL: i32 = 33; + +// ---- tinfo / tfield / tparam ----------------------------------------- + +type tfield = struct { + name: str, + type_: *tinfo, + offset: u64, + tnext: *tfield, +}; + +type tparam = struct { + name: str, + type_: *tinfo, + tnext: *tparam, +}; + +type tinfo = struct { + kind: i32, + size: u64, + align: u64, + sub: *tinfo, // ptr/slice/array/chan element + alen: u64, + fields: *tfield, + params: *tparam, + ret: *tinfo, + variadic: i32, + name: str, + under: *tinfo, +}; + +// ---- tctx — the box of primitive types ------------------------------- + +type tctx = struct { + a: *arena, + ty_void: *tinfo, + ty_bool: *tinfo, + ty_rune: *tinfo, + ty_i8: *tinfo, + ty_i16: *tinfo, + ty_i32: *tinfo, + ty_i64: *tinfo, + ty_u8: *tinfo, + ty_u16: *tinfo, + ty_u32: *tinfo, + ty_u64: *tinfo, + ty_int: *tinfo, + ty_uint: *tinfo, + ty_uintptr: *tinfo, + ty_f32: *tinfo, + ty_f64: *tinfo, + ty_str: *tinfo, + ty_err: *tinfo, + ty_untyped_int: *tinfo, + ty_untyped_float: *tinfo, + ty_untyped_str: *tinfo, + ty_untyped_rune: *tinfo, + ty_untyped_bool: *tinfo, + ty_untyped_nil: *tinfo, +}; + +// ---- constructors ----------------------------------------------------- + +export fn newtype(a: *arena, k: i32) *tinfo = { + let t: *tinfo = amalloc(a, 96u64): *tinfo; + t.kind = k; + return t; +}; + +fn prim(a: *arena, k: i32, nm: str, sz: u64, al: u64) *tinfo = { + let t: *tinfo = newtype(a, k); + t.name = nm; + t.size = sz; + if (al > 0u64) { t.align = al; } else { t.align = sz; }; + return t; +}; + +export fn typesinit(c: *tctx, a: *arena) void = { + c.a = a; + c.ty_void = prim(a, TY_VOID, "void", 0u64, 1u64); + c.ty_bool = prim(a, TY_BOOL, "bool", 1u64, 1u64); + c.ty_rune = prim(a, TY_RUNE, "rune", 4u64, 4u64); + c.ty_i8 = prim(a, TY_I8, "i8", 1u64, 1u64); + c.ty_i16 = prim(a, TY_I16, "i16", 2u64, 2u64); + c.ty_i32 = prim(a, TY_I32, "i32", 4u64, 4u64); + c.ty_i64 = prim(a, TY_I64, "i64", 8u64, 8u64); + c.ty_u8 = prim(a, TY_U8, "u8", 1u64, 1u64); + c.ty_u16 = prim(a, TY_U16, "u16", 2u64, 2u64); + c.ty_u32 = prim(a, TY_U32, "u32", 4u64, 4u64); + c.ty_u64 = prim(a, TY_U64, "u64", 8u64, 8u64); + c.ty_int = prim(a, TY_INT, "int", 8u64, 8u64); + c.ty_uint = prim(a, TY_UINT, "uint", 8u64, 8u64); + c.ty_uintptr= prim(a, TY_UINTPTR, "uintptr", 8u64, 8u64); + c.ty_f32 = prim(a, TY_F32, "f32", 4u64, 4u64); + c.ty_f64 = prim(a, TY_F64, "f64", 8u64, 8u64); + c.ty_str = prim(a, TY_STR, "str", 16u64, 8u64); + c.ty_err = prim(a, TY_ERR, "", 0u64, 1u64); + + c.ty_untyped_int = prim(a, TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); + c.ty_untyped_float = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); + c.ty_untyped_str = prim(a, TY_UNTYPED_STR, "untyped_str", 0u64, 1u64); + c.ty_untyped_rune = prim(a, TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); + c.ty_untyped_bool = prim(a, TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); + c.ty_untyped_nil = prim(a, TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); +}; + +export fn type_ptr(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_PTR); + t.sub = sub; + t.size = 8u64; + t.align = 8u64; + return t; +}; + +export fn type_slice(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_SLICE); + t.sub = sub; + t.size = 24u64; + t.align = 8u64; + return t; +}; + +export fn type_array(a: *arena, sub: *tinfo, n: u64) *tinfo = { + let t: *tinfo = newtype(a, TY_ARRAY); + t.sub = sub; + t.alen = n; + if (sub != nil) { + t.size = sub.size * n; + t.align = sub.align; + } else { + t.align = 1u64; + }; + return t; +}; + +export fn type_chan(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_CHAN); + t.sub = sub; + t.size = 8u64; + t.align = 8u64; + return t; +}; + +export fn type_named(a: *arena, name: str, under: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_NAMED); + t.name = name; + t.under = under; + if (under != nil) { + t.size = under.size; + t.align = under.align; + }; + return t; +}; + +// ---- predicates ------------------------------------------------------- + +export fn type_isint(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_I8) { return true; }; + if (k == TY_I16) { return true; }; + if (k == TY_I32) { return true; }; + if (k == TY_I64) { return true; }; + if (k == TY_U8) { return true; }; + if (k == TY_U16) { return true; }; + if (k == TY_U32) { return true; }; + if (k == TY_U64) { return true; }; + if (k == TY_INT) { return true; }; + if (k == TY_UINT){ return true; }; + if (k == TY_UINTPTR) { return true; }; + if (k == TY_RUNE){ return true; }; + if (k == TY_UNTYPED_INT) { return true; }; + if (k == TY_UNTYPED_RUNE) { return true; }; + if (k == TY_NAMED) { return type_isint(t.under); }; + return false; +}; + +export fn type_isfloat(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_F32) { return true; }; + if (k == TY_F64) { return true; }; + if (k == TY_UNTYPED_FLOAT) { return true; }; + if (k == TY_NAMED) { return type_isfloat(t.under); }; + return false; +}; + +export fn type_isnum(t: *tinfo) bool = { + if (type_isint(t)) { return true; }; + return type_isfloat(t); +}; + +export fn type_isunsigned(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_U8) { return true; }; + if (k == TY_U16) { return true; }; + if (k == TY_U32) { return true; }; + if (k == TY_U64) { return true; }; + if (k == TY_UINT){ return true; }; + if (k == TY_UINTPTR) { return true; }; + if (k == TY_NAMED) { return type_isunsigned(t.under); }; + return false; +}; + +export fn type_isuntyped(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_UNTYPED_INT) { return true; }; + if (k == TY_UNTYPED_FLOAT) { return true; }; + if (k == TY_UNTYPED_STR) { return true; }; + if (k == TY_UNTYPED_RUNE) { return true; }; + if (k == TY_UNTYPED_BOOL) { return true; }; + if (k == TY_UNTYPED_NIL) { return true; }; + return false; +}; + +// type_eq — structural equality. Named types compare nominally. +export fn type_eq(a: *tinfo, b: *tinfo) bool = { + if (a == b) { return true; }; + if (a == nil) { return false; }; + if (b == nil) { return false; }; + if (a.kind != b.kind) { return false; }; + let k: i32 = a.kind; + if (k == TY_PTR) { return type_eq(a.sub, b.sub); }; + if (k == TY_SLICE) { return type_eq(a.sub, b.sub); }; + if (k == TY_CHAN) { return type_eq(a.sub, b.sub); }; + if (k == TY_ARRAY) { + if (a.alen != b.alen) { return false; }; + return type_eq(a.sub, b.sub); + }; + if (k == TY_FN) { + if (a.variadic != b.variadic) { return false; }; + if (!type_eq(a.ret, b.ret)) { return false; }; + let pa: *tparam = a.params; + let pb: *tparam = b.params; + for (true) { + if (pa == nil) { if (pb == nil) { return true; }; return false; }; + if (pb == nil) { return false; }; + if (!type_eq(pa.type_, pb.type_)) { return false; }; + pa = pa.tnext; + pb = pb.tnext; + }; + return true; + }; + if (k == TY_STRUCT) { + let fa: *tfield = a.fields; + let fb: *tfield = b.fields; + for (true) { + if (fa == nil) { if (fb == nil) { return true; }; return false; }; + if (fb == nil) { return false; }; + let na: str = fa.name; + let nb: str = fb.name; + if (na.len != nb.len) { return false; }; + let i: i32 = 0; + for (i < na.len) { + if (na[i] != nb[i]) { return false; }; + i += 1; + }; + if (!type_eq(fa.type_, fb.type_)) { return false; }; + fa = fa.tnext; + fb = fb.tnext; + }; + return true; + }; + if (k == TY_NAMED) { return false; }; // nominal: only same ptr + if (k == TY_TUPLE) { + let pa: *tparam = a.params; + let pb: *tparam = b.params; + for (true) { + if (pa == nil) { if (pb == nil) { return true; }; return false; }; + if (pb == nil) { return false; }; + if (!type_eq(pa.type_, pb.type_)) { return false; }; + pa = pa.tnext; + pb = pb.tnext; + }; + return true; + }; + return true; // primitives match by kind alone +}; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww new file mode 100644 index 00000000..c13fdfe6 --- /dev/null +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -0,0 +1,6559 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_ACCESS: i64 = 21; +def SYS_GETPID: i64 = 39; +def SYS_FORK: i64 = 57; +def SYS_EXECVE: i64 = 59; +def SYS_EXIT: i64 = 60; +def SYS_WAIT4: i64 = 61; +def SYS_UNLINK: i64 = 87; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// ---- process and filesystem helpers used by the `ww` driver ---------- + +// access(2): returns 0 if the file is reachable, negative errno +// otherwise. mode is the bitset described in (F_OK=0). +export fn access(path: *u8, mode: i32) i32 = { + return syscall2(SYS_ACCESS, path: i64, mode: i64): i32; +}; + +// unlink(2). +export fn unlink(path: *u8) i32 = { + return syscall1(SYS_UNLINK, path: i64): i32; +}; + +// getpid(2). Used by the driver to mint unique scratch paths. +export fn getpid() i32 = { + return syscall0(SYS_GETPID): i32; +}; + +// fork(2): 0 in the child, child pid in the parent, negative errno +// on failure. +export fn fork() i32 = { + return syscall0(SYS_FORK): i32; +}; + +// execve(2): on success, does not return. +export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { + return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32; +}; + +// wait4(2): wait for `pid` (or any child if -1), store status in +// `*status_out`, return the pid that ended (or negative errno). +export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = { + return syscall4(SYS_WAIT4, pid: i64, status_out: i64, + options: i64, rusage: i64): i32; +}; + +// selfhost/cmd/wwc/mem.ww — port of cmd/wwc/mem.c. +// +// Bump arena allocator. Backed by the runtime page allocator +// (rt_alloc / rt_free), no libc. Each chunk is mmap'd; when the +// current chunk runs out we link a fresh one. Freeing the arena +// unmaps the chain. +// +// Memory handed out is 16-byte aligned. The C version under +// cmd/wwc/ is retained until the three-stage bootstrap diffs clean. + +use os; + +def ALIGN: u64 = 16u64; +def INIT_CHUNK: u64 = 65536u64; +def MAX_CHUNK: u64 = 4194304u64; +def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, + next: *arena, + total: u64, +}; + +fn roundup(n: u64, a: u64) u64 = { + return (n + a - 1u64) & ~(a - 1u64); +}; + +export fn newarena() *arena = { + let a: *arena = os.alloc(ARENA_SZ): *arena; + a.buf = os.alloc(INIT_CHUNK): *u8; + a.off = 0u64; + a.cap = INIT_CHUNK; + a.next = nil; + a.total = 0u64; + return a; +}; + +// Grow: link a fresh chunk in front of the head. We push the old +// chunk into `next` so the head always describes the current bump +// region. Chunk size doubles up to MAX_CHUNK. +fn grow(a: *arena, need: u64) bool = { + let want: u64 = a.cap * 2u64; + if (want < need) { want = need; }; + if (want > MAX_CHUNK) { want = MAX_CHUNK; }; + if (want < need) { return false; }; // single allocation too big + + let old: *arena = os.alloc(ARENA_SZ): *arena; + old.buf = a.buf; + old.off = a.off; + old.cap = a.cap; + old.next = a.next; + old.total = 0u64; + + a.buf = os.alloc(want): *u8; + a.off = 0u64; + a.cap = want; + a.next = old; + return true; +}; + +export fn amalloc(a: *arena, n: u64) *void = { + let need: u64 = roundup(n, ALIGN); + if (need > a.cap - a.off) { + if (!grow(a, need)) { return nil; }; + }; + let p: *u8 = a.buf + a.off; + a.off += need; + a.total += need; + // Zero the region. Plan 9 amalloc zeroes; we mirror that here so + // the checker can assume freshly allocated nodes start at 0. + let i: u64 = 0u64; + for (i < need) { + p[i] = 0u8; + i += 1u64; + }; + return p: *void; +}; + +// astrndup — copy `n` bytes into the arena and produce a NUL-terminated +// view. Returns a `str` whose ptr is arena-owned and whose len is `n` +// (the trailing NUL is past `len`, so callers reading exactly n bytes +// see no padding). Used by the lexer to capture token text. +export fn astrndup(a: *arena, src: *u8, n: u64) str = { + let p: *u8 = amalloc(a, n + 1u64): *u8; + let i: u64 = 0u64; + for (i < n) { + p[i] = src[i]; + i += 1u64; + }; + p[n] = 0u8; + let r: str; + r.ptr = p; + r.len = n: i32; + return r; +}; + +export fn freearena(a: *arena) void = { + for (a != nil) { + let next: *arena = a.next; + os.free(a.buf: *void, a.cap); + os.free(a: *void, ARENA_SZ); + a = next; + }; +}; + +// strconv — number↔string conversions. Decimal i64 to/from a fixed +// buffer. Two error idioms ship side by side: +// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the +// tagged-union work; kept for callers that already use it. +// - Hare style (parse64/parseu64): `(value | str)`. The error +// variant carries a short, allocation-free message describing +// why the parse failed. Prefer this for new code. + +// u64toa — write `v` in decimal into `buf` and return the byte count. +// Unsigned-only so callers don't have to think about wraparound when +// printing a u64 that happens to have the high bit set. +export fn u64toa(buf: []u8, v: u64) i32 = { + let tmp: [32]u8; + let i: i32 = 0; + let n: u64 = v; + for (n > 0u64) { + tmp[i] = ((n % 10u64) + 48u64): u8; + n = n / 10u64; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn i64toa(buf: []u8, v: i64) i32 = { + let neg: bool = false; + let n: i64 = v; + if (n < 0) { + neg = true; + n = -n; + }; + let tmp: [32]u8; + let i: i32 = 0; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + if (neg) { + buf[out] = 45u8; // '-' + out += 1; + }; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn atoi64(s: str) (i64, bool) = { + let v: i64 = 0; + let i: i32 = 0; + let neg: bool = false; + if (s.len > 0) { + if (s[0] == 45u8) { neg = true; i = 1; }; + }; + if (i >= s.len) { return 0, false; }; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return 0, false; }; + if (c > 57u8) { return 0, false; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v, true; +}; + +// parse64 — Hare-style fallible signed decimal parser. The value +// variant is i64; the error variant is a short str describing the +// reason. No locale, no whitespace, no underscores: a leading '-' is +// the only non-digit accepted, and only at position 0. +export fn parse64(s: str) (i64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let i: i32 = 0; + let neg: bool = false; + if (s[0] == 45u8) { neg = true; i = 1; }; + if (i >= s.len) { return "parse: lone sign"; }; + let v: i64 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v; +}; + +// parseu64 — fallible unsigned decimal parser. No leading sign. +export fn parseu64(s: str) (u64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let v: u64 = 0u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10u64 + ((c: u64) - 48u64); + i += 1; + }; + return v; +}; + +// selfhost/cmd/wwc/tok.ww — port of cmd/wwc/tok.c plus the Tkind / +// Tok / Pos shapes from cmd/wwc/ww.h. +// +// Token kind values must stay numerically equal to the C side: the +// 990_selfhost test diffs ww-side wwdump output against C-side +// wwdump output, byte-for-byte. Reordering this list shifts the +// integers and breaks the diff. +// +// Bottom of file: tokprint, which emits one token per line in a +// format identical to cmd/wwc/tok.c:tokprint(). + +use os; +use strconv; + +// ---- Tkind ------------------------------------------------------------ +// Mirror of the C enum in cmd/wwc/ww.h. Don't reorder. + +def TK_NONE: i32 = 0; +def TK_EOF: i32 = 1; +def TK_ERR: i32 = 2; +def TK_IDENT: i32 = 3; +def TK_INT: i32 = 4; +def TK_FLOAT: i32 = 5; +def TK_RUNE: i32 = 6; +def TK_STR: i32 = 7; + +def TK_FN: i32 = 8; +def TK_LET: i32 = 9; +def TK_DEF: i32 = 10; +def TK_IF: i32 = 11; +def TK_ELSE: i32 = 12; +def TK_FOR: i32 = 13; +def TK_SWITCH: i32 = 14; +def TK_CASE: i32 = 15; +def TK_RETURN: i32 = 16; +def TK_USE: i32 = 17; +def TK_TYPE: i32 = 18; +def TK_STRUCT: i32 = 19; +def TK_DEFER: i32 = 20; +def TK_BREAK: i32 = 21; +def TK_CONTINUE: i32 = 22; +def TK_EXPORT: i32 = 23; +def TK_PROC: i32 = 24; +def TK_CHAN: i32 = 25; +def TK_NIL: i32 = 26; +def TK_TRUE: i32 = 27; +def TK_FALSE: i32 = 28; +def TK_AS: i32 = 29; +def TK_STATIC: i32 = 30; +def TK_MATCH: i32 = 31; + +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_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_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_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_AND: i32 = 74; +def TK_OR: i32 = 75; +def TK_NOT: i32 = 76; + +def TK_LARROW: i32 = 77; +def TK_ARROW: i32 = 78; +def TK_FATARROW: i32 = 79; + +def TK_LAST: i32 = 80; + +// ---- Pos / Tok -------------------------------------------------------- +// +// `pos` is used at error-reporting boundaries; we always pass it via +// *pos so the value never gets struct-copied (6c can't yet copy a +// 24-byte struct). +// +// `tok` is flat — file/line/col live directly on the token rather than +// nested inside a `pos` field. Same reason: nested struct field +// assignment isn't supported, and flat primitives are. + +type pos = struct { + file: str, + line: i32, + col: i32, +}; + +type tok = struct { + kind: i32, + file: str, // path of the source the token came from + line: i32, + col: i32, + text: str, // arena-owned token text (TK_IDENT, TK_STR, TK_ERR) + uval: u64, // TK_INT, TK_RUNE + fval: f64, // TK_FLOAT + tsuffix: str, // typed numeric literal suffix or empty +}; + +// ---- keyword lookup --------------------------------------------------- + +fn streq_n(a: *u8, b: str, n: i32) bool = { + if (b.len != n) { return false; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// kwlookup — returns the matching TK_* keyword kind for a byte run, +// or TK_NONE if it's an ordinary identifier. Linear search over a +// small alphabetised list, matching cmd/wwc/tok.c. +export fn kwlookup(p: *u8, n: i32) i32 = { + if (streq_n(p, "as", n)) { return TK_AS; }; + if (streq_n(p, "break", n)) { return TK_BREAK; }; + if (streq_n(p, "case", n)) { return TK_CASE; }; + if (streq_n(p, "chan", n)) { return TK_CHAN; }; + if (streq_n(p, "continue", n)) { return TK_CONTINUE; }; + if (streq_n(p, "def", n)) { return TK_DEF; }; + if (streq_n(p, "defer", n)) { return TK_DEFER; }; + if (streq_n(p, "else", n)) { return TK_ELSE; }; + if (streq_n(p, "export", n)) { return TK_EXPORT; }; + if (streq_n(p, "false", n)) { return TK_FALSE; }; + if (streq_n(p, "fn", n)) { return TK_FN; }; + if (streq_n(p, "for", n)) { return TK_FOR; }; + if (streq_n(p, "if", n)) { return TK_IF; }; + if (streq_n(p, "let", n)) { return TK_LET; }; + if (streq_n(p, "match", n)) { return TK_MATCH; }; + if (streq_n(p, "nil", n)) { return TK_NIL; }; + if (streq_n(p, "proc", n)) { return TK_PROC; }; + if (streq_n(p, "return", n)) { return TK_RETURN; }; + if (streq_n(p, "static", n)) { return TK_STATIC; }; + if (streq_n(p, "struct", n)) { return TK_STRUCT; }; + if (streq_n(p, "switch", n)) { return TK_SWITCH; }; + if (streq_n(p, "true", n)) { return TK_TRUE; }; + if (streq_n(p, "type", n)) { return TK_TYPE; }; + if (streq_n(p, "use", n)) { return TK_USE; }; + return TK_NONE; +}; + +// ---- tokname ---------------------------------------------------------- +// +// Returns the canonical printable spelling for a token kind. Matches +// the C tokname()'s output exactly so wwdump output diffs cleanly. + +export fn tokname(k: i32) str = { + if (k == TK_NONE) { return ""; }; + if (k == TK_EOF) { return "EOF"; }; + if (k == TK_ERR) { return "ERR"; }; + if (k == TK_IDENT) { return "IDENT"; }; + if (k == TK_INT) { return "INT"; }; + if (k == TK_FLOAT) { return "FLOAT"; }; + if (k == TK_RUNE) { return "RUNE"; }; + if (k == TK_STR) { return "STR"; }; + + if (k == TK_FN) { return "fn"; }; + if (k == TK_LET) { return "let"; }; + if (k == TK_DEF) { return "def"; }; + if (k == TK_IF) { return "if"; }; + if (k == TK_ELSE) { return "else"; }; + if (k == TK_FOR) { return "for"; }; + if (k == TK_SWITCH) { return "switch"; }; + if (k == TK_CASE) { return "case"; }; + if (k == TK_RETURN) { return "return"; }; + if (k == TK_USE) { return "use"; }; + if (k == TK_TYPE) { return "type"; }; + if (k == TK_STRUCT) { return "struct"; }; + if (k == TK_DEFER) { return "defer"; }; + if (k == TK_BREAK) { return "break"; }; + if (k == TK_CONTINUE) { return "continue"; }; + if (k == TK_EXPORT) { return "export"; }; + if (k == TK_PROC) { return "proc"; }; + if (k == TK_CHAN) { return "chan"; }; + if (k == TK_NIL) { return "nil"; }; + if (k == TK_TRUE) { return "true"; }; + if (k == TK_FALSE) { return "false"; }; + if (k == TK_AS) { return "as"; }; + if (k == TK_STATIC) { return "static"; }; + if (k == TK_MATCH) { return "match"; }; + + if (k == TK_LPAREN) { return "("; }; + if (k == TK_RPAREN) { return ")"; }; + if (k == TK_LBRACE) { return "{"; }; + if (k == TK_RBRACE) { return "}"; }; + if (k == TK_LBRACK) { return "["; }; + if (k == TK_RBRACK) { return "]"; }; + if (k == TK_COMMA) { return ","; }; + if (k == TK_SEMI) { return ";"; }; + if (k == TK_COLON) { return ":"; }; + if (k == TK_DOT) { return "."; }; + if (k == TK_ELLIPSIS) { return "..."; }; + if (k == TK_DOTDOT) { return ".."; }; + if (k == TK_AT) { return "@"; }; + if (k == TK_QUESTION) { return "?"; }; + + if (k == TK_ASSIGN) { return "="; }; + if (k == TK_PLUSEQ) { return "+="; }; + if (k == TK_MINUSEQ) { return "-="; }; + if (k == TK_STAREQ) { return "*="; }; + if (k == TK_SLASHEQ) { return "/="; }; + if (k == TK_PERCENTEQ) { return "%="; }; + if (k == TK_AMPEQ) { return "&="; }; + if (k == TK_PIPEEQ) { return "|="; }; + if (k == TK_CARETEQ) { return "^="; }; + if (k == TK_LSHIFTEQ) { return "<<="; }; + if (k == TK_RSHIFTEQ) { return ">>="; }; + + if (k == TK_PLUS) { return "+"; }; + if (k == TK_MINUS) { return "-"; }; + if (k == TK_STAR) { return "*"; }; + if (k == TK_SLASH) { return "/"; }; + if (k == TK_PERCENT) { return "%"; }; + if (k == TK_AMP) { return "&"; }; + if (k == TK_PIPE) { return "|"; }; + if (k == TK_CARET) { return "^"; }; + if (k == TK_TILDE) { return "~"; }; + if (k == TK_LSHIFT) { return "<<"; }; + if (k == TK_RSHIFT) { return ">>"; }; + + if (k == TK_EQ) { return "=="; }; + if (k == TK_NEQ) { return "!="; }; + if (k == TK_LT) { return "<"; }; + if (k == TK_LE) { return "<="; }; + if (k == TK_GT) { return ">"; }; + if (k == TK_GE) { return ">="; }; + + if (k == TK_AND) { return "&&"; }; + if (k == TK_OR) { return "||"; }; + if (k == TK_NOT) { return "!"; }; + + if (k == TK_LARROW) { return "<-"; }; + if (k == TK_ARROW) { return "->"; }; + if (k == TK_FATARROW) { return "=>"; }; + + if (k == TK_LAST) { return ""; }; + return ""; +}; + +// ---- writer for tokprint ---------------------------------------------- +// +// fputq mirrors cmd/wwc/tok.c:fputq — quote the string with C-style +// escapes for \, ", \n, \t, \r and \xNN for other non-printables. + +fn fputc_byte(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn fputs_str(fd: i32, s: str) void = { + os.write(fd, s.ptr, s.len: u64); +}; + +fn hexchar(n: u8) u8 = { + if (n < 10u8) { return n + 48u8; }; // '0'..'9' + return (n - 10u8) + 97u8; // 'a'..'f' +}; + +fn fputhex2(fd: i32, b: u8) void = { + let out: [4]u8; + out[0] = 92u8; // '\\' + out[1] = 120u8; // 'x' + out[2] = hexchar(b >> 4u8); + out[3] = hexchar(b & 15u8); + os.write(fd, out.ptr, 4u64); +}; + +fn fputq(fd: i32, p: *u8, n: i32) void = { + fputc_byte(fd, 34u8); // '"' + let i: i32 = 0; + for (i < n) { + let c: u8 = p[i]; + if (c == 92u8) { // '\\' + fputs_str(fd, "\\\\"); + } else { + if (c == 34u8) { // '"' + fputs_str(fd, "\\\""); + } else { + if (c == 10u8) { // '\n' + fputs_str(fd, "\\n"); + } else { + if (c == 9u8) { // '\t' + fputs_str(fd, "\\t"); + } else { + if (c == 13u8) { // '\r' + fputs_str(fd, "\\r"); + } else { + if (c < 32u8) { + fputhex2(fd, c); + } else { + if (c == 127u8) { + fputhex2(fd, c); + } else { + fputc_byte(fd, c); + }; + }; + }; + }; + }; + }; + }; + i += 1; + }; + fputc_byte(fd, 34u8); +}; + +// tokprint — write one token line to fd. Format must match +// cmd/wwc/tok.c:tokprint() byte-for-byte: that's the diff anchor. +// ":: [ ]\n" +// +// Takes `t` by pointer because 6c can't yet pass a >16-byte struct +// by value; the C version takes Tok by value. +export fn tokprint(fd: i32, t: *tok) void = { + // Chained-dot field reads (`t.x.y`) on str sub-fields aren't yet + // reduced by 6c — `t.x.y` returns the whole str. Lift the str + // fields into locals so we can use the str pseudo-field path. + let tfile: str = t.file; + let ttext: str = t.text; + if (tfile.len > 0) { + fputs_str(fd, tfile); + } else { + fputs_str(fd, ""); + }; + fputc_byte(fd, 58u8); // ':' + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], t.line: i64); + os.write(fd, buf.ptr, n: u64); + fputc_byte(fd, 58u8); + n = strconv.i64toa(buf[0:32], t.col: i64); + os.write(fd, buf.ptr, n: u64); + fputc_byte(fd, 32u8); // ' ' + fputs_str(fd, tokname(t.kind)); + + if (t.kind == TK_IDENT) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_STR) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_ERR) { + fputc_byte(fd, 32u8); + fputq(fd, ttext.ptr, ttext.len); + } else { if (t.kind == TK_INT) { + fputc_byte(fd, 32u8); + n = strconv.u64toa(buf[0:32], t.uval); + os.write(fd, buf.ptr, n: u64); + } else { if (t.kind == TK_RUNE) { + fputc_byte(fd, 32u8); + n = strconv.u64toa(buf[0:32], t.uval); + os.write(fd, buf.ptr, n: u64); + };};};};}; + // TK_FLOAT is intentionally not handled here — %g formatting + // won't byte-match across implementations. Diff fixtures must + // be float-free until we implement a stable float formatter. + + fputc_byte(fd, 10u8); // '\n' +}; + +// ascii — byte-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family. Bytes outside 0..127 always +// answer `false`. The lexer hot path uses these inline; they are +// expected to inline to a couple of compares. + +export fn isdigit(c: u8) bool = { + if (c < 48u8) { return false; }; + if (c > 57u8) { return false; }; + return true; +}; + +export fn isupper(c: u8) bool = { + if (c < 65u8) { return false; }; + if (c > 90u8) { return false; }; + return true; +}; + +export fn islower(c: u8) bool = { + if (c < 97u8) { return false; }; + if (c > 122u8) { return false; }; + return true; +}; + +export fn isalpha(c: u8) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: u8) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: u8) bool = { + if (c == 32u8) { return true; }; // ' ' + if (c == 9u8) { return true; }; // '\t' + if (c == 10u8) { return true; }; // '\n' + if (c == 11u8) { return true; }; // '\v' + if (c == 12u8) { return true; }; // '\f' + if (c == 13u8) { return true; }; // '\r' + return false; +}; + +export fn ishex(c: u8) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65u8) { + if (c <= 70u8) { return true; }; // 'A'..'F' + }; + if (c >= 97u8) { + if (c <= 102u8) { return true; }; // 'a'..'f' + }; + return false; +}; + +// digitval — value of `c` as a hex/decimal digit, or -1 if not one. +// Useful when scanning numeric literals. +export fn digitval(c: u8) i32 = { + if (isdigit(c)) { return (c - 48u8): i32; }; + if (c >= 65u8) { + if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; + }; + if (c >= 97u8) { + if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; + }; + return -1; +}; + +// isidstart / isidpart — identifier classes used by the lexer. +// Alpha or '_' starts; alnum or '_' continues. +export fn isidstart(c: u8) bool = { + if (isalpha(c)) { return true; }; + if (c == 95u8) { return true; }; // '_' + return false; +}; + +export fn isidpart(c: u8) bool = { + if (isalnum(c)) { return true; }; + if (c == 95u8) { return true; }; + return false; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: u8) u8 = { + if (isupper(c)) { return c + 32u8; }; + return c; +}; + +export fn toupper(c: u8) u8 = { + if (islower(c)) { return c - 32u8; }; + return c; +}; + +// selfhost/cmd/wwc/lex.ww — port of cmd/wwc/lex.c. +// +// The DFA, the helpers, and the order of decisions all mirror the C +// version exactly. The 990_selfhost test diffs the resulting token +// stream against the C-side wwdump byte-for-byte; any divergence is +// a port bug. +// +// Calling-convention note: 6c can't yet pass or return structs >16 +// bytes by value, so `tok` and `pos` are passed by pointer (out +// params). The C version passes `Tok` by value; we differ here only +// in shape, not in observable behaviour. Token kind values stay +// numerically identical. + +use os; +use ascii; +use mem; +use tok; + +type lex = struct { + file: str, + src: *u8, // raw bytes; not necessarily NUL-terminated + srclen: u64, + lpos: u64, + line: i32, + col: i32, + a: *arena, + errs: i32, +}; + +export fn lexinit(l: *lex, a: *arena, file: str, src: *u8, len: u64) void = { + l.file = file; + l.src = src; + l.srclen = len; + l.lpos = 0u64; + l.line = 1; + l.col = 1; + l.a = a; + l.errs = 0; +}; + +// srcb — byte at offset; helper that lifts the cast out of indexing. +fn srcb(l: *lex, off: u64) i32 = { + let i: i32 = off: i32; + let b: u8 = l.src[i]; + return b: i32; +}; + +fn lpeek(l: *lex, ahead: u64) i32 = { + let p: u64 = l.lpos + ahead; + if (p >= l.srclen) { return -1; }; + return srcb(l, p); +}; + +fn lget(l: *lex) i32 = { + if (l.lpos >= l.srclen) { return -1; }; + let c: i32 = srcb(l, l.lpos); + l.lpos += 1u64; + if (c == 10) { // '\n' + l.line += 1; + l.col = 1; + } else { + l.col += 1; + }; + return c; +}; + +fn cur_pos(l: *lex, out: *pos) void = { + out.file = l.file; + out.line = l.line; + out.col = l.col; +}; + +// putuint — write `v` (signed, but always non-negative here) to fd 2 +// in decimal. Standalone so err_at doesn't drag in fmt and create a +// dependency cycle with strconv. +fn putuint(fd: i32, v: i32) void = { + let tmp: [16]u8; + let i: i32 = 0; + let n: i32 = v; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { tmp[0] = 48u8; i = 1; }; + let buf: [16]u8; + let m: i32 = 0; + for (i > 0) { i -= 1; buf[m] = tmp[i]; m += 1; }; + os.write(fd, buf.ptr, m: u64); +}; + +fn err_at(l: *lex, p: *pos, msg: str) void = { + let pf: str = p.file; + os.write(2, pf.ptr, pf.len: u64); + os.write(2, ":".ptr, 1u64); + putuint(2, p.line); + os.write(2, ":".ptr, 1u64); + putuint(2, p.col); + os.write(2, ": error: ".ptr, 9u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + l.errs += 1; +}; + +fn skipws(l: *lex) bool = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { return false; }; + if (c == 32) { lget(l); continue; }; + if (c == 9) { lget(l); continue; }; + if (c == 13) { lget(l); continue; }; + if (c == 10) { lget(l); continue; }; + if (c == 47) { // '/' + let c2: i32 = lpeek(l, 1u64); + if (c2 == 47) { + for (true) { + let cx: i32 = lpeek(l, 0u64); + if (cx < 0) { return false; }; + if (cx == 10) { break; }; + lget(l); + }; + continue; + }; + if (c2 == 42) { // '*' + lget(l); lget(l); + let prev: i32 = -1; + for (true) { + let x: i32 = lget(l); + if (x < 0) { + let cp: pos; + cur_pos(l, &cp); + err_at(l, &cp, "unterminated /* comment"); + return false; + }; + if (prev == 42) { + if (x == 47) { break; }; + }; + prev = x; + }; + continue; + }; + }; + return true; + }; + return false; +}; + +fn parseint(p: *u8, n: u64, base: i32, ok: *bool) u64 = { + let v: u64 = 0u64; + let got: bool = false; + let i: u64 = 0u64; + for (i < n) { + let ix: i32 = i: i32; + let c: u8 = p[ix]; + if (c == 95u8) { // '_' + i += 1u64; + continue; + }; + let d: i32 = -1; + if (c >= 48u8) { + if (c <= 57u8) { d = (c - 48u8): i32; }; + }; + if (d < 0) { + if (c >= 97u8) { + if (c <= 102u8) { d = ((c - 97u8) + 10u8): i32; }; + }; + }; + if (d < 0) { + if (c >= 65u8) { + if (c <= 70u8) { d = ((c - 65u8) + 10u8): i32; }; + }; + }; + if (d < 0) { *ok = false; return 0u64; }; + if (d >= base) { *ok = false; return 0u64; }; + v = v * (base: u64) + (d: u64); + got = true; + i += 1u64; + }; + *ok = got; + return v; +}; + +fn escape(l: *lex, out: *i32) bool = { + let c: i32 = lget(l); + if (c < 0) { return false; }; + if (c == 110) { *out = 10; return true; }; + if (c == 116) { *out = 9; return true; }; + if (c == 114) { *out = 13; return true; }; + if (c == 92) { *out = 92; return true; }; + if (c == 39) { *out = 39; return true; }; + if (c == 34) { *out = 34; return true; }; + if (c == 48) { *out = 0; return true; }; + if (c == 97) { *out = 7; return true; }; + if (c == 98) { *out = 8; return true; }; + if (c == 102) { *out = 12; return true; }; + if (c == 118) { *out = 11; return true; }; + if (c == 120) { + let hi: i32 = lget(l); + let lo: i32 = lget(l); + if (hi < 0) { return false; }; + if (lo < 0) { return false; }; + if (!ascii.ishex(hi: u8)) { + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad \\x escape"); + return false; + }; + if (!ascii.ishex(lo: u8)) { + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad \\x escape"); + return false; + }; + let h: i32 = ascii.digitval(hi: u8); + let lv: i32 = ascii.digitval(lo: u8); + *out = (h << 4) | lv; + return true; + }; + let cp: pos; cur_pos(l, &cp); + err_at(l, &cp, "bad escape"); + return false; +}; + +// scan_decimal_run — consume a run of decimal digits and underscores. +fn scan_decimal_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isdigit(c: u8)) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +fn scan_hex_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.ishex(c: u8)) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +fn scan_bin_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c == 48) { lget(l); continue; }; + if (c == 49) { lget(l); continue; }; + if (c == 95) { lget(l); continue; }; + break; + }; +}; + +fn scan_oct_run(l: *lex) void = { + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 48) { break; }; + if (c > 55) { + if (c != 95) { break; }; + }; + lget(l); + }; +}; + +// scan_exp — consume the [eE][+-]?[0-9]+ tail of a float, if present. +fn scan_exp(l: *lex) void = { + let e: i32 = lpeek(l, 0u64); + if (e != 101) { if (e != 69) { return; }; }; // 'e' or 'E' + lget(l); + let s: i32 = lpeek(l, 0u64); + if (s == 43) { lget(l); } + else { if (s == 45) { lget(l); }; }; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isdigit(c: u8)) { break; }; + lget(l); + }; +}; + +fn lexnum(l: *lex, start: *pos, out: *tok) void = { + out.kind = TK_INT; + out.file = start.file; + out.line = start.line; + out.col = start.col; + let begin: u64 = l.lpos; + let base: i32 = 10; + let isfloat: bool = false; + + let c0: i32 = lpeek(l, 0u64); + let c1: i32 = lpeek(l, 1u64); + + if (c0 == 48) { // '0' + if (c1 == 120) { // 'x' + lget(l); lget(l); base = 16; scan_hex_run(l); + } else { if (c1 == 88) { // 'X' + lget(l); lget(l); base = 16; scan_hex_run(l); + } else { if (c1 == 98) { // 'b' + lget(l); lget(l); base = 2; scan_bin_run(l); + } else { if (c1 == 66) { // 'B' + lget(l); lget(l); base = 2; scan_bin_run(l); + } else { if (c1 == 111) { // 'o' + lget(l); lget(l); base = 8; scan_oct_run(l); + } else { if (c1 == 79) { // 'O' + lget(l); lget(l); base = 8; scan_oct_run(l); + } else { + scan_decimal_run(l); + if (lpeek(l, 0u64) == 46) { + let after: i32 = lpeek(l, 1u64); + if (after >= 48) { + if (after <= 57) { + isfloat = true; + lget(l); + scan_decimal_run(l); + scan_exp(l); + }; + }; + }; + };};};};};}; + } else { + scan_decimal_run(l); + if (lpeek(l, 0u64) == 46) { + let after: i32 = lpeek(l, 1u64); + if (after >= 48) { + if (after <= 57) { + isfloat = true; + lget(l); + scan_decimal_run(l); + scan_exp(l); + }; + }; + }; + }; + + let n: u64 = l.lpos - begin; + out.text = astrndup(l.a, l.src + begin, n); + + if (isfloat) { + // out.fval is already 0 from the top-of-lexnext clear. + // We don't strtod the literal yet — the diff fixtures we + // care about are float-free; any TK_FLOAT seen in source + // gets a placeholder value until we wire a real parser. + out.kind = TK_FLOAT; + } else { + let digs: *u8 = l.src + begin; + let dn: u64 = n; + if (base != 10) { + digs = digs + 2u64; + dn -= 2u64; + }; + let ok: bool = false; + out.uval = parseint(digs, dn, base, &ok); + if (!ok) { + err_at(l, start, "bad integer literal"); + out.kind = TK_ERR; + }; + }; + + let pc: i32 = lpeek(l, 0u64); + if (pc >= 0) { + if (ascii.isidstart(pc: u8)) { + let sb: u64 = l.lpos; + for (true) { + let cc: i32 = lpeek(l, 0u64); + if (cc < 0) { break; }; + if (!ascii.isidpart(cc: u8)) { break; }; + lget(l); + }; + let sl: u64 = l.lpos - sb; + let p: *u8 = l.src + sb; + let isok: bool = false; + if (sl == 2u64) { + if (p[0] == 105u8) { + if (p[1] == 56u8) { isok = true; }; // i8 + }; + if (p[0] == 117u8) { + if (p[1] == 56u8) { isok = true; }; // u8 + }; + }; + if (sl == 3u64) { + if (p[0] == 105u8) { + if (p[1] == 49u8) { if (p[2] == 54u8) { isok = true; }; }; // i16 + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; // i32 + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; // i64 + }; + if (p[0] == 117u8) { + if (p[1] == 49u8) { if (p[2] == 54u8) { isok = true; }; }; + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; + }; + if (p[0] == 102u8) { + if (p[1] == 51u8) { if (p[2] == 50u8) { isok = true; }; }; // f32 + if (p[1] == 54u8) { if (p[2] == 52u8) { isok = true; }; }; // f64 + }; + }; + if (isok) { + out.tsuffix = astrndup(l.a, p, sl); + } else { + l.lpos = sb; + }; + }; + }; +}; + +fn lexident(l: *lex, start: *pos, out: *tok) void = { + let begin: u64 = l.lpos; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { break; }; + if (!ascii.isidpart(c: u8)) { break; }; + lget(l); + }; + 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; + if (k != TK_NONE) { + out.kind = k; + } else { + out.kind = TK_IDENT; + }; + out.text = astrndup(l.a, p, n); +}; + +fn lexstr(l: *lex, start: *pos, out: *tok) void = { + let cap: u64 = 32u64; + let nb: u64 = 0u64; + let buf: *u8 = amalloc(l.a, cap): *u8; + for (true) { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { + err_at(l, start, "unterminated string"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + if (c == 34) { lget(l); break; }; + let ch: i32 = 0; + if (c == 92) { + lget(l); + if (!escape(l, &ch)) { ch = 0; }; + } else { + ch = lget(l); + }; + if (nb + 1u64 >= cap) { + let ncap: u64 = cap * 2u64; + let nb2: *u8 = amalloc(l.a, ncap): *u8; + let i: u64 = 0u64; + for (i < nb) { + let ix: i32 = i: i32; + nb2[ix] = buf[ix]; + i += 1u64; + }; + buf = nb2; + cap = ncap; + }; + let nbi: i32 = nb: i32; + buf[nbi] = ch: u8; + nb += 1u64; + }; + out.kind = TK_STR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + let s: str; + s.ptr = buf; + s.len = nb: i32; + out.text = s; +}; + +fn lexrune(l: *lex, start: *pos, out: *tok) void = { + let c: i32 = lpeek(l, 0u64); + if (c < 0) { + err_at(l, start, "unterminated rune"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + let ch: i32 = 0; + if (c == 92) { + lget(l); + if (!escape(l, &ch)) { ch = 0; }; + } else { + ch = lget(l); + }; + if (lpeek(l, 0u64) != 39) { + err_at(l, start, "rune literal missing closing '"); + out.kind = TK_ERR; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.text = astrndup(l.a, "".ptr, 0u64); + return; + }; + lget(l); + out.kind = TK_RUNE; + out.file = start.file; + out.line = start.line; + out.col = start.col; + out.uval = ch: u64; +}; + +fn emit_simple(start: *pos, k: i32, out: *tok) void = { + out.kind = k; + out.file = start.file; + out.line = start.line; + out.col = start.col; +}; + +// set_pos_from — copy file/line/col from a *pos into a tok. Used by +// the err-token path where we already have a pos. +fn set_pos_from(out: *tok, p: *pos) void = { + out.file = p.file; + out.line = p.line; + out.col = p.col; +}; + +export fn lexnext(l: *lex, out: *tok) void = { + // Reset the out token so callers can rely on stale fields being + // cleared (they only inspect kind, pos, text, uval, fval, tsuffix + // per kind). + out.kind = TK_NONE; + out.uval = 0u64; + // out.fval starts cleared by the caller's stack-local init (lex.ww + // allocates the tok with `let t: tok;` which zeroes). We avoid + // writing a 0.0 literal here so this file itself stays float-free + // and the C/ww wwdump diff over it is byte-identical. + let empty: str; + empty.ptr = nil; + empty.len = 0; + out.text = empty; + out.tsuffix = empty; + + if (!skipws(l)) { + let p: pos; cur_pos(l, &p); + emit_simple(&p, TK_EOF, out); + return; + }; + let start: pos; cur_pos(l, &start); + let c: i32 = lpeek(l, 0u64); + + if (c >= 0) { + if (ascii.isidstart(c: u8)) { lexident(l, &start, out); return; }; + if (ascii.isdigit(c: u8)) { lexnum(l, &start, out); return; }; + }; + + if (c == 34) { lget(l); lexstr(l, &start, out); return; }; + if (c == 39) { lget(l); lexrune(l, &start, out); return; }; + + lget(l); + + if (c == 40) { emit_simple(&start, TK_LPAREN, out); return; }; + if (c == 41) { emit_simple(&start, TK_RPAREN, out); return; }; + if (c == 123) { emit_simple(&start, TK_LBRACE, out); return; }; + if (c == 125) { emit_simple(&start, TK_RBRACE, out); return; }; + if (c == 91) { emit_simple(&start, TK_LBRACK, out); return; }; + if (c == 93) { emit_simple(&start, TK_RBRACK, out); return; }; + if (c == 44) { emit_simple(&start, TK_COMMA, out); return; }; + if (c == 59) { emit_simple(&start, TK_SEMI, out); return; }; + if (c == 58) { emit_simple(&start, TK_COLON, out); return; }; + if (c == 64) { emit_simple(&start, TK_AT, out); return; }; + if (c == 63) { emit_simple(&start, TK_QUESTION, out); return; }; + if (c == 126) { emit_simple(&start, TK_TILDE, out); return; }; + + if (c == 46) { // '.' + if (lpeek(l, 0u64) == 46) { + if (lpeek(l, 1u64) == 46) { + lget(l); lget(l); + emit_simple(&start, TK_ELLIPSIS, out); return; + }; + lget(l); + emit_simple(&start, TK_DOTDOT, out); return; + }; + emit_simple(&start, TK_DOT, out); return; + }; + + if (c == 43) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PLUSEQ, out); return; }; + emit_simple(&start, TK_PLUS, out); return; + }; + if (c == 45) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_MINUSEQ, out); return; }; + if (lpeek(l, 0u64) == 62) { lget(l); emit_simple(&start, TK_ARROW, out); return; }; + emit_simple(&start, TK_MINUS, out); return; + }; + if (c == 42) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_STAREQ, out); return; }; + emit_simple(&start, TK_STAR, out); return; + }; + if (c == 47) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_SLASHEQ, out); return; }; + emit_simple(&start, TK_SLASH, out); return; + }; + if (c == 37) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PERCENTEQ, out); return; }; + emit_simple(&start, TK_PERCENT, out); return; + }; + if (c == 38) { + if (lpeek(l, 0u64) == 38) { lget(l); emit_simple(&start, TK_AND, out); return; }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_AMPEQ, out); return; }; + emit_simple(&start, TK_AMP, out); return; + }; + if (c == 124) { + if (lpeek(l, 0u64) == 124) { lget(l); emit_simple(&start, TK_OR, out); return; }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_PIPEEQ, out); return; }; + emit_simple(&start, TK_PIPE, out); return; + }; + if (c == 94) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_CARETEQ, out); return; }; + emit_simple(&start, TK_CARET, out); return; + }; + if (c == 61) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_EQ, out); return; }; + if (lpeek(l, 0u64) == 62) { lget(l); emit_simple(&start, TK_FATARROW, out); return; }; + emit_simple(&start, TK_ASSIGN, out); return; + }; + if (c == 33) { + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_NEQ, out); return; }; + emit_simple(&start, TK_NOT, out); return; + }; + if (c == 60) { + if (lpeek(l, 0u64) == 60) { + lget(l); + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_LSHIFTEQ, out); return; }; + emit_simple(&start, TK_LSHIFT, out); return; + }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_LE, out); return; }; + if (lpeek(l, 0u64) == 45) { lget(l); emit_simple(&start, TK_LARROW, out); return; }; + emit_simple(&start, TK_LT, out); return; + }; + if (c == 62) { + if (lpeek(l, 0u64) == 62) { + lget(l); + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_RSHIFTEQ, out); return; }; + emit_simple(&start, TK_RSHIFT, out); return; + }; + if (lpeek(l, 0u64) == 61) { lget(l); emit_simple(&start, TK_GE, out); return; }; + emit_simple(&start, TK_GT, out); return; + }; + + err_at(l, &start, "unexpected character"); + out.kind = TK_ERR; + set_pos_from(out, &start); + let one: [1]u8; + one[0] = c: u8; + out.text = astrndup(l.a, one.ptr, 1u64); +}; + +// selfhost/cmd/wwc/ast.ww — port of cmd/wwc/ast.c (Node defs + printer). +// +// Status: AST printer is fully ported. Constructor `newnode` is here. +// The parser (parse.ww) is currently minimal — see its file header. +// +// Calling-convention shim: same as tok/lex — `node` is too big to pass +// by value (8 *node pointers + 2 strs + a few ints), so callers always +// hand around `*node`. Only `newnode` allocates and returns a *node. + +use os; +use strconv; +use mem; +use tok; + +// ---- Nkind ------------------------------------------------------------ +// +// Mirror of cmd/wwc/ww.h Nkind. Values must stay numerically equal so +// the AST diff probe in 990_selfhost works. + +def N_NONE: i32 = 0; + +def N_INTLIT: i32 = 1; +def N_FLOATLIT: i32 = 2; +def N_STRLIT: i32 = 3; +def N_RUNELIT: i32 = 4; +def N_TRUE: i32 = 5; +def N_FALSE: i32 = 6; +def N_NIL: i32 = 7; +def N_IDENT: i32 = 8; + +def N_BIN: i32 = 9; +def N_UN: i32 = 10; +def N_CALL: i32 = 11; +def N_INDEX: i32 = 12; +def N_DOT: i32 = 13; +def N_CAST: i32 = 14; +def N_STRUCTLIT:i32 = 15; +def N_ARRLIT: i32 = 16; +def N_FIELD: i32 = 17; +def N_ASSIGN: i32 = 18; +def N_ALLOC: i32 = 19; +def N_FREE: i32 = 20; +def N_RECV: i32 = 21; +def N_SLICE: i32 = 22; +def N_SPREAD: i32 = 23; + +def N_BLOCK: i32 = 24; +def N_EXPRSTMT: i32 = 25; +def N_LET: i32 = 26; +def N_RETURN: i32 = 27; +def N_IF: i32 = 28; +def N_FOR: i32 = 29; +def N_FORRANGE: i32 = 30; +def N_DEFER: i32 = 31; +def N_BREAK: i32 = 32; +def N_CONTINUE: i32 = 33; +def N_SWITCH: i32 = 34; +def N_CASE: i32 = 35; + +def N_FILE: i32 = 36; +def N_USE: i32 = 37; +def N_DEF: i32 = 38; +def N_TYPEDECL: i32 = 39; +def N_FNDECL: i32 = 40; +def N_PARAM: i32 = 41; + +def N_TNAME: i32 = 42; +def N_TPTR: i32 = 43; +def N_TSLICE: i32 = 44; +def N_TARRAY: i32 = 45; +def N_TFN: i32 = 46; +def N_TSTRUCT: i32 = 47; +def N_TFIELD: i32 = 48; +def N_TCHAN: i32 = 49; + +def N_ATTR: i32 = 50; +def N_TTUPLE: i32 = 51; +def N_TTAGGED: i32 = 52; +def N_TUPLE: i32 = 53; +def N_MATCH: i32 = 54; +def N_MCASE: i32 = 55; +def N_TRYPROP: i32 = 56; +def N_TRYUNW: i32 = 57; +def N_MLET: i32 = 58; +def N_MASSIGN: i32 = 59; + +def N_LAST: i32 = 60; + +// ---- Node ------------------------------------------------------------- + +type node = struct { + kind: i32, + file: str, + line: i32, + col: i32, + op: i32, // for N_BIN / N_UN / N_ASSIGN + str: str, + uval: u64, + fval: f64, + lhs: *node, + rhs: *node, + cond: *node, + body: *node, + els: *node, + list: *node, + next: *node, + attr: *node, + exported: i32, // bool — `export` keyword present + type_: *void, // filled in by checker; type.ww treats it as *tinfo + tsuffix: str, // typed numeric literal suffix ("i32", "u64", ...) +}; + +export fn newnode(a: *arena, k: i32, file: str, line: i32, col: i32) *node = { + let n: *node = amalloc(a, 192u64): *node; // 192 ≥ struct size + n.kind = k; + n.file = file; + n.line = line; + n.col = col; + return n; +}; + +// ---- printer ---------------------------------------------------------- + +fn nkname(k: i32) str = { + if (k == N_NONE) { return "none"; }; + if (k == N_INTLIT) { return "int"; }; + if (k == N_FLOATLIT) { return "float"; }; + if (k == N_STRLIT) { return "str"; }; + if (k == N_RUNELIT) { return "rune"; }; + if (k == N_TRUE) { return "true"; }; + if (k == N_FALSE) { return "false"; }; + if (k == N_NIL) { return "nil"; }; + if (k == N_IDENT) { return "id"; }; + if (k == N_BIN) { return "bin"; }; + if (k == N_UN) { return "un"; }; + if (k == N_CALL) { return "call"; }; + if (k == N_INDEX) { return "index"; }; + if (k == N_DOT) { return "dot"; }; + if (k == N_CAST) { return "cast"; }; + if (k == N_STRUCTLIT) { return "structlit"; }; + if (k == N_ARRLIT) { return "arrlit"; }; + if (k == N_FIELD) { return "field"; }; + if (k == N_ASSIGN) { return "assign"; }; + if (k == N_ALLOC) { return "alloc"; }; + if (k == N_FREE) { return "free"; }; + if (k == N_RECV) { return "recv"; }; + if (k == N_SLICE) { return "slice"; }; + if (k == N_SPREAD) { return "spread"; }; + if (k == N_BLOCK) { return "block"; }; + if (k == N_EXPRSTMT) { return "exprstmt"; }; + if (k == N_LET) { return "let"; }; + if (k == N_RETURN) { return "return"; }; + if (k == N_IF) { return "if"; }; + if (k == N_FOR) { return "for"; }; + if (k == N_FORRANGE) { return "forrange"; }; + if (k == N_DEFER) { return "defer"; }; + if (k == N_BREAK) { return "break"; }; + if (k == N_CONTINUE) { return "continue"; }; + if (k == N_SWITCH) { return "switch"; }; + if (k == N_CASE) { return "case"; }; + if (k == N_FILE) { return "file"; }; + if (k == N_USE) { return "use"; }; + if (k == N_DEF) { return "def"; }; + if (k == N_TYPEDECL) { return "typedecl"; }; + if (k == N_FNDECL) { return "fn"; }; + if (k == N_PARAM) { return "param"; }; + if (k == N_TNAME) { return "tname"; }; + if (k == N_TPTR) { return "tptr"; }; + if (k == N_TSLICE) { return "tslice"; }; + if (k == N_TARRAY) { return "tarray"; }; + if (k == N_TFN) { return "tfn"; }; + if (k == N_TSTRUCT) { return "tstruct"; }; + if (k == N_TFIELD) { return "tfield"; }; + if (k == N_TCHAN) { return "tchan"; }; + if (k == N_ATTR) { return "attr"; }; + if (k == N_TTUPLE) { return "ttuple"; }; + if (k == N_TTAGGED) { return "ttagged"; }; + if (k == N_TUPLE) { return "tuple"; }; + if (k == N_MATCH) { return "match"; }; + if (k == N_MCASE) { return "mcase"; }; + if (k == N_TRYPROP) { return "tryprop"; }; + if (k == N_TRYUNW) { return "tryunw"; }; + if (k == N_MLET) { return "mlet"; }; + if (k == N_MASSIGN) { return "massign"; }; + if (k == N_LAST) { return "last"; }; + return "?"; +}; + +fn ind(fd: i32, d: i32) void = { + let i: i32 = 0; + for (i < d) { + os.write(fd, " ".ptr, 2u64); + i += 1; + }; +}; + +fn putc1(fd: i32, b: u8) void = { + let buf: [1]u8; + buf[0] = b; + os.write(fd, buf.ptr, 1u64); +}; + +fn putq(fd: i32, s: str) void = { + putc1(fd, 34u8); // '"' + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c == 34u8) { // '"' + os.write(fd, "\\\"".ptr, 2u64); + } else { if (c == 92u8) { // '\\' + os.write(fd, "\\\\".ptr, 2u64); + } else { if (c == 10u8) { // '\n' + os.write(fd, "\\n".ptr, 2u64); + } else { if (c == 9u8) { // '\t' + os.write(fd, "\\t".ptr, 2u64); + } else { if (c < 32u8) { + let hi: u8 = c >> 4u8; + let lo: u8 = c & 15u8; + let h: u8 = 0u8; + let l: u8 = 0u8; + if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; }; + let buf: [4]u8; + buf[0] = 92u8; + buf[1] = 120u8; + buf[2] = h; + buf[3] = l; + os.write(fd, buf.ptr, 4u64); + } else { + putc1(fd, c); + };};};};}; + i += 1; + }; + putc1(fd, 34u8); +}; + +fn pr(fd: i32, n: *node, d: i32) void = { + if (n == nil) { + ind(fd, d); + os.write(fd, "()\n".ptr, 3u64); + return; + }; + ind(fd, d); + putc1(fd, 40u8); // '(' + let nm: str = nkname(n.kind); + os.write(fd, nm.ptr, nm.len: u64); + + if (n.kind == N_INTLIT) { + putc1(fd, 32u8); + let buf: [32]u8; + let m: i32 = strconv.u64toa(buf[0:32], n.uval); + os.write(fd, buf.ptr, m: u64); + } else { if (n.kind == N_RUNELIT) { + putc1(fd, 32u8); + let buf: [32]u8; + let m: i32 = strconv.u64toa(buf[0:32], n.uval); + os.write(fd, buf.ptr, m: u64); + } else { if ( + n.kind == N_STRLIT || + n.kind == N_IDENT || + n.kind == N_USE || + n.kind == N_DOT || + n.kind == N_DEF || + n.kind == N_TYPEDECL || + n.kind == N_FNDECL || + n.kind == N_PARAM || + n.kind == N_LET || + n.kind == N_TNAME || + n.kind == N_TFIELD || + n.kind == N_FIELD || + n.kind == N_ATTR + ) { + // Match C ast.c: print the str field whenever it's non-nil, + // even if its length is zero (e.g. an empty STRLIT prints + // `(str ""`). + let s: str = n.str; + if (s.ptr != nil) { + putc1(fd, 32u8); + putq(fd, s); + }; + } else { if ( + n.kind == N_BIN || + n.kind == N_UN || + n.kind == N_ASSIGN + ) { + putc1(fd, 32u8); + let on: str = tokname(n.op); + os.write(fd, on.ptr, on.len: u64); + };};};}; + + if (n.kind == N_FNDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == N_DEF) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + if (n.kind == N_TYPEDECL) { + if (n.exported != 0) { os.write(fd, " export".ptr, 7u64); }; + }; + putc1(fd, 10u8); // '\n' + + if (n.attr != nil) { + ind(fd, d + 1); + os.write(fd, "(@\n".ptr, 3u64); + let m: *node = n.attr; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + if (n.lhs != nil) { pr(fd, n.lhs, d + 1); }; + if (n.rhs != nil) { pr(fd, n.rhs, d + 1); }; + if (n.cond != nil) { pr(fd, n.cond, d + 1); }; + if (n.body != nil) { pr(fd, n.body, d + 1); }; + if (n.els != nil) { pr(fd, n.els, d + 1); }; + if (n.list != nil) { + ind(fd, d + 1); + os.write(fd, "(list\n".ptr, 6u64); + let m: *node = n.list; + for (m != nil) { + pr(fd, m, d + 2); + m = m.next; + }; + ind(fd, d + 1); + os.write(fd, ")\n".ptr, 2u64); + }; + ind(fd, d); + os.write(fd, ")\n".ptr, 2u64); +}; + +export fn astprint(fd: i32, n: *node) void = { + pr(fd, n, 0); +}; + +// selfhost/cmd/wwc/parse.ww — port of cmd/wwc/parse.c. +// +// Status: GROWING stub. Currently handles top-level `use IDENT;`, +// `def NAME: TYPE = LIT;`, `type NAME = TYPE;`, and `fn NAME(params) +// RET;` (header-only — bodies are recovered past). Unknown decls are +// chewed token-by-token until the next ';' so the diff probe can +// still anchor on partial fixtures. +// +// The full port is multi-session work — parse.c is 1,183 lines of +// hand-rolled recursive descent + Pratt expression parser. Each +// surface form lands here gradually so the AST diff in 990_selfhost +// grows toward whole-language coverage one increment at a time. +// +// Calling-convention shim: 6c can't yet pass a sub-struct field +// (e.g. p.cur.line where p.cur is a `tok` of size 76). The parser +// stores the current token as flat primitive fields rather than a +// nested `tok` struct; `refill` copies a freshly lexed token in. + +use os; +use mem; +use tok; + +type parser = struct { + l: *lex, + a: *arena, + errs: i32, + // nocast: while inside `[...]` we treat ':' as the slice + // separator, not the cast operator. Mirrors parse.c's flag. + nocast: i32, + cur_kind: i32, + cur_file: str, + cur_line: i32, + cur_col: i32, + cur_text: str, + cur_uval: u64, +}; + +fn refill(p: *parser) void = { + let t: tok; + lexnext(p.l, &t); + p.cur_kind = t.kind; + p.cur_file = t.file; + p.cur_line = t.line; + p.cur_col = t.col; + p.cur_text = t.text; + p.cur_uval = t.uval; +}; + +export fn parserinit(p: *parser, a: *arena, l: *lex) void = { + p.l = l; + p.a = a; + p.errs = 0; + p.nocast = 0; + refill(p); +}; + +fn advance(p: *parser) void = { refill(p); }; + +fn accept_tok(p: *parser, k: i32) bool = { + if (p.cur_kind == k) { advance(p); return true; }; + return false; +}; + +fn err_msg(p: *parser, msg: str) void = { + let pre: str = "parse: "; + os.write(2, pre.ptr, pre.len: u64); + os.write(2, msg.ptr, msg.len: u64); + os.write(2, "\n".ptr, 1u64); + p.errs += 1; +}; + +fn expect_tok(p: *parser, k: i32, what: str) bool = { + if (p.cur_kind == k) { advance(p); return true; }; + err_msg(p, what); + return false; +}; + +// expectident — consume the current TK_IDENT and return its text. +// Returns the empty str on error (and advances to make progress). +fn expectident(p: *parser, into: *str) bool = { + if (p.cur_kind != TK_IDENT) { + err_msg(p, "expected identifier"); + advance(p); + return false; + }; + *into = p.cur_text; + advance(p); + return true; +}; + +// ---- type expressions ------------------------------------------------ +// +// Currently: TNAME (single ident, no dotted path yet) and TPTR (`*T`). +// Other forms (slice, array, struct, fn, chan, tuple, tagged) will +// land in subsequent commits. + +fn parsetype(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + if (p.cur_kind == TK_STAR) { + advance(p); + let n: *node = newnode(p.a, N_TPTR, pf, pl, pc); + n.lhs = parsetype(p); + return n; + }; + + if (p.cur_kind == TK_LBRACK) { + advance(p); + if (p.cur_kind == TK_RBRACK) { + advance(p); + let n: *node = newnode(p.a, N_TSLICE, pf, pl, pc); + n.lhs = parsetype(p); + return n; + }; + let n: *node = newnode(p.a, N_TARRAY, pf, pl, pc); + n.rhs = parseexpr(p); + expect_tok(p, TK_RBRACK, "expected ']' in array type"); + n.lhs = parsetype(p); + return n; + }; + + if (p.cur_kind == TK_STRUCT) { + advance(p); + expect_tok(p, TK_LBRACE, "expected '{' after struct"); + let n: *node = newnode(p.a, N_TSTRUCT, pf, pl, pc); + let fhead: *node = nil; + let ftail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let fpf: str = p.cur_file; + let fpl: i32 = p.cur_line; + let fpc: i32 = p.cur_col; + let f: *node = newnode(p.a, N_TFIELD, fpf, fpl, fpc); + let fid: str; + expectident(p, &fid); + f.str = fid; + expect_tok(p, TK_COLON, "expected ':' in field"); + f.lhs = parsetype(p); + if (fhead == nil) { fhead = f; ftail = f; } + else { ftail.next = f; ftail = f; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after struct fields"); + n.list = fhead; + return n; + }; + + if (p.cur_kind == TK_IDENT) { + let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); + n.str = p.cur_text; + advance(p); + // Dotted path collapse (pkg.Type) deferred — fixtures don't + // need it yet. + return n; + }; + + if (p.cur_kind == TK_LPAREN) { + // (T) or (T, T, ...) or (T | T | ...) + advance(p); + let first: *node = parsetype(p); + if (accept_tok(p, TK_PIPE)) { + let n: *node = newnode(p.a, N_TTAGGED, pf, pl, pc); + let head: *node = first; + let tail: *node = first; + for (true) { + let e: *node = parsetype(p); + tail.next = e; + tail = e; + if (!accept_tok(p, TK_PIPE)) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tagged-union type"); + n.list = head; + return n; + }; + if (!accept_tok(p, TK_COMMA)) { + expect_tok(p, TK_RPAREN, "expected ')' after parenthesised type"); + return first; + }; + let n: *node = newnode(p.a, N_TTUPLE, pf, pl, pc); + let head: *node = first; + let tail: *node = first; + for (true) { + let e: *node = parsetype(p); + tail.next = e; + tail = e; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == TK_RPAREN) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tuple type"); + n.list = head; + return n; + }; + + if (p.cur_kind == TK_FN) { + advance(p); + expect_tok(p, TK_LPAREN, "expected '(' after fn in type"); + let n: *node = newnode(p.a, N_TFN, pf, pl, pc); + // Anonymous-or-named params: parseparams handles named only; + // for fn-type expressions the C parser allows IDENT-less + // (anonymous) params. Stub: only named params for now. + n.list = parseparams(p); + expect_tok(p, TK_RPAREN, "expected ')' after fn type params"); + n.lhs = parsetype(p); + return n; + }; + + err_msg(p, "expected type"); + advance(p); + return newnode(p.a, N_TNAME, pf, pl, pc); +}; + +// ---- expressions (Pratt) --------------------------------------------- +// +// Forwards: parseexpr → parsebin → parseunary → parsepostfix(parseprimary). +// Tuple literals, match expressions, struct literals, slice [lo:hi], +// and the ?/! try operators are not yet wired — they'll arrive as the +// AST diff fixture grows to need them. + +fn bprec(k: i32) i32 = { + if (k == TK_OR) { return 1; }; + if (k == TK_AND) { return 2; }; + if (k == TK_EQ) { return 3; }; + if (k == TK_NEQ) { return 3; }; + if (k == TK_LT) { return 4; }; + if (k == TK_LE) { return 4; }; + if (k == TK_GT) { return 4; }; + if (k == TK_GE) { return 4; }; + if (k == TK_PIPE) { return 5; }; + if (k == TK_CARET) { return 6; }; + if (k == TK_AMP) { return 7; }; + if (k == TK_LSHIFT) { return 8; }; + if (k == TK_RSHIFT) { return 8; }; + if (k == TK_PLUS) { return 9; }; + if (k == TK_MINUS) { return 9; }; + if (k == TK_STAR) { return 10; }; + if (k == TK_SLASH) { return 10; }; + if (k == TK_PERCENT) { return 10; }; + return 0; +}; + +fn isassignop(k: i32) bool = { + if (k == TK_ASSIGN) { return true; }; + if (k == TK_PLUSEQ) { return true; }; + if (k == TK_MINUSEQ) { return true; }; + if (k == TK_STAREQ) { return true; }; + if (k == TK_SLASHEQ) { return true; }; + if (k == TK_PERCENTEQ) { return true; }; + if (k == TK_AMPEQ) { return true; }; + if (k == TK_PIPEEQ) { return true; }; + if (k == TK_CARETEQ) { return true; }; + if (k == TK_LSHIFTEQ) { return true; }; + if (k == TK_RSHIFTEQ) { return true; }; + return false; +}; + +// Forward references between parseunary/parseexpr/parsebin/parsepostfix +// are resolved by the two-pass checker — no body-less prototypes needed. + +fn parseprimary(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + if (p.cur_kind == TK_INT) { + let n: *node = newnode(p.a, N_INTLIT, pf, pl, pc); + n.uval = p.cur_uval; + n.str = p.cur_text; + advance(p); + return n; + }; + if (p.cur_kind == TK_STR) { + let n: *node = newnode(p.a, N_STRLIT, pf, pl, pc); + n.str = p.cur_text; + advance(p); + return n; + }; + if (p.cur_kind == TK_RUNE) { + let n: *node = newnode(p.a, N_RUNELIT, pf, pl, pc); + n.uval = p.cur_uval; + advance(p); + return n; + }; + if (p.cur_kind == TK_TRUE) { + advance(p); + return newnode(p.a, N_TRUE, pf, pl, pc); + }; + if (p.cur_kind == TK_FALSE) { + advance(p); + return newnode(p.a, N_FALSE, pf, pl, pc); + }; + if (p.cur_kind == TK_NIL) { + advance(p); + return newnode(p.a, N_NIL, pf, pl, pc); + }; + if (p.cur_kind == TK_LPAREN) { + advance(p); + let e: *node = parseexpr(p); + // Tuple literal: (a, b, ...) + if (accept_tok(p, TK_COMMA)) { + let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc); + t.list = e; + let tail: *node = e; + for (true) { + if (p.cur_kind == TK_RPAREN) { break; }; + let en: *node = parseexpr(p); + tail.next = en; + tail = en; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RPAREN, "expected ')' in tuple"); + return t; + }; + expect_tok(p, TK_RPAREN, "expected ')'"); + return e; + }; + if (p.cur_kind == TK_IDENT) { + let n: *node = newnode(p.a, N_IDENT, pf, pl, pc); + n.str = p.cur_text; + advance(p); + // `IDENT {` — struct literal. Disambiguate: only consume as a + // struct lit when we're not in a context where '{' starts a + // block (e.g. `if (cond) {`). The parser is called from + // expressions, never directly from cond contexts that need a + // block; in stmt parsing, the for/if drivers consume their + // own paren/cond, so this is safe. + if (p.cur_kind == TK_LBRACE) { + advance(p); + let s: *node = newnode(p.a, N_STRUCTLIT, pf, pl, pc); + s.lhs = n; + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let fpf: str = p.cur_file; + let fpl: i32 = p.cur_line; + let fpc: i32 = p.cur_col; + let id: str; + expectident(p, &id); + expect_tok(p, TK_ASSIGN, "expected '=' in struct lit field"); + let v: *node = parseexpr(p); + let f: *node = newnode(p.a, N_FIELD, fpf, fpl, fpc); + f.str = id; + f.lhs = v; + if (head == nil) { head = f; tail = f; } + else { tail.next = f; tail = f; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after struct literal"); + s.list = head; + return s; + }; + return n; + }; + if (p.cur_kind == TK_MATCH) { + // match (e) { case let v: T => stmt; case T => stmt; case => stmt; }; + advance(p); + expect_tok(p, TK_LPAREN, "expected '(' after match"); + let m: *node = newnode(p.a, N_MATCH, pf, pl, pc); + m.lhs = parseexpr(p); + expect_tok(p, TK_RPAREN, "expected ')' after match scrutinee"); + expect_tok(p, TK_LBRACE, "expected '{' to open match body"); + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind == TK_CASE) { + let cf: str = p.cur_file; + let cl: i32 = p.cur_line; + let cc: i32 = p.cur_col; + advance(p); // past `case` + let mc: *node = newnode(p.a, N_MCASE, cf, cl, cc); + if (p.cur_kind == TK_LET) { + advance(p); + let id: str; + expectident(p, &id); + mc.str = id; + expect_tok(p, TK_COLON, "expected ':' after match binding"); + mc.lhs = parsetype(p); + } else { if (p.cur_kind != TK_FATARROW) { + mc.lhs = parsetype(p); + };}; + expect_tok(p, TK_FATARROW, "expected '=>' in match arm"); + mc.body = parsestmt(p); + if (head == nil) { head = mc; tail = mc; } + else { tail.next = mc; tail = mc; }; + }; + expect_tok(p, TK_RBRACE, "expected '}' after match body"); + m.list = head; + return m; + }; + err_msg(p, "expected expression"); + advance(p); + return newnode(p.a, N_NONE, pf, pl, pc); +}; + +fn parsearglist(p: *parser, close_kind: i32, head_out: **node) void = { + *head_out = nil; + if (p.cur_kind == close_kind) { return; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let e: *node = parseexpr(p); + if (head == nil) { head = e; tail = e; } + else { tail.next = e; tail = e; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == close_kind) { break; }; + }; + *head_out = head; +}; + +fn parsepostfix(p: *parser, lhs: *node) *node = { + let cur: *node = lhs; + for (true) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + if (p.cur_kind == TK_LPAREN) { + 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; + expect_tok(p, TK_RPAREN, "expected ')' after args"); + cur = n; + continue; + }; + if (p.cur_kind == TK_LBRACK) { + advance(p); + // `[ : hi ]` — slice with implicit lo = 0. + if (p.cur_kind == TK_COLON) { + advance(p); + let n: *node = newnode(p.a, N_SLICE, pf, pl, pc); + n.lhs = cur; + if (p.cur_kind != TK_RBRACK) { + n.cond = parseexpr(p); + }; + expect_tok(p, TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + // Suppress cast inside `[...]` so ':' parses as slice + // separator rather than the postfix cast operator. + let prev: i32 = p.nocast; + p.nocast = 1; + let e: *node = parseexpr(p); + p.nocast = prev; + if (p.cur_kind == TK_COLON) { + advance(p); + let n: *node = newnode(p.a, N_SLICE, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + if (p.cur_kind != TK_RBRACK) { + n.cond = parseexpr(p); + }; + expect_tok(p, TK_RBRACK, "expected ']' in slice"); + cur = n; + continue; + }; + let n: *node = newnode(p.a, N_INDEX, pf, pl, pc); + n.lhs = cur; + n.rhs = e; + expect_tok(p, TK_RBRACK, "expected ']' after index"); + cur = n; + continue; + }; + if (p.cur_kind == TK_DOT) { + advance(p); + let n: *node = newnode(p.a, N_DOT, pf, pl, pc); + n.lhs = cur; + let id: str; + expectident(p, &id); + n.str = id; + cur = n; + continue; + }; + if (p.cur_kind == TK_COLON) { + if (p.nocast != 0) { + return cur; + }; + advance(p); + let n: *node = newnode(p.a, N_CAST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + break; + }; + return cur; +}; + +fn parseunary(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + let k: i32 = p.cur_kind; + if (k == TK_MINUS) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_MINUS; n.lhs = parseunary(p); + return n; + }; + if (k == TK_PLUS) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_PLUS; n.lhs = parseunary(p); + return n; + }; + if (k == TK_NOT) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_NOT; n.lhs = parseunary(p); + return n; + }; + if (k == TK_TILDE) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_TILDE; n.lhs = parseunary(p); + return n; + }; + if (k == TK_STAR) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_STAR; n.lhs = parseunary(p); + return n; + }; + if (k == TK_AMP) { + advance(p); + let n: *node = newnode(p.a, N_UN, pf, pl, pc); + n.op = TK_AMP; n.lhs = parseunary(p); + return n; + }; + return parsepostfix(p, parseprimary(p)); +}; + +fn parsebin(p: *parser, lhs: *node, minp: i32) *node = { + let cur: *node = lhs; + for (true) { + let op: i32 = p.cur_kind; + let pr: i32 = bprec(op); + if (pr == 0) { return cur; }; + if (pr < minp) { return cur; }; + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); + let rhs: *node = parseunary(p); + for (true) { + let np: i32 = bprec(p.cur_kind); + if (np <= pr) { break; }; + rhs = parsebin(p, rhs, np); + }; + let n: *node = newnode(p.a, N_BIN, pf, pl, pc); + n.op = op; n.lhs = cur; n.rhs = rhs; + cur = n; + }; + return cur; +}; + +fn parseexpr(p: *parser) *node = { + let e: *node = parsebin(p, parseunary(p), 1); + if (isassignop(p.cur_kind)) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + let op: i32 = p.cur_kind; + advance(p); + let n: *node = newnode(p.a, N_ASSIGN, pf, pl, pc); + n.op = op; + n.lhs = e; + n.rhs = parseexpr(p); // right-associative + return n; + }; + return e; +}; + +// ---- statements ------------------------------------------------------ +// +// Subset wired today: block, let, return, if (no else-if chain), for +// (single-cond C-style), expr-stmt, defer, break, continue. Switch +// and match arms are not yet wired; tuple-let / multi-let neither. + +fn parselet_local(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `let` + let n: *node = newnode(p.a, N_LET, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + if (accept_tok(p, TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accept_tok(p, TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expect_tok(p, TK_SEMI, "expected ';' after let"); + return n; +}; + +fn parseblock(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + expect_tok(p, TK_LBRACE, "expected '{' to open block"); + let blk: *node = newnode(p.a, N_BLOCK, pf, pl, pc); + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind != TK_RBRACE) { + if (p.cur_kind == TK_EOF) { break; }; + let s: *node = parsestmt(p); + if (s != nil) { + if (head == nil) { head = s; tail = s; } + else { tail.next = s; tail = s; }; + }; + }; + expect_tok(p, TK_RBRACE, "expected '}' to close block"); + blk.list = head; + return blk; +}; + +fn parseif(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `if` + expect_tok(p, TK_LPAREN, "expected '(' after if"); + let n: *node = newnode(p.a, N_IF, pf, pl, pc); + n.cond = parseexpr(p); + expect_tok(p, TK_RPAREN, "expected ')' after if condition"); + n.body = parseblock(p); + if (accept_tok(p, TK_ELSE)) { + if (p.cur_kind == TK_IF) { + n.els = parseif(p); + } else { + n.els = parseblock(p); + }; + }; + return n; +}; + +fn parsefor(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `for` + expect_tok(p, TK_LPAREN, "expected '(' after for"); + let n: *node = newnode(p.a, N_FOR, pf, pl, pc); + // Three forms (matching C parser): + // for (cond) — only cond + // for (init; cond; post) — full + // for (true) — infinite (cond is N_TRUE) + // Distinguish by counting ';'. Look at first chunk: if it's a + // `let` stmt that's the init. Otherwise, parse expr; if next is + // ';' it was cond. If we see two ';' total after init, post is + // next. Simpler: peek for `let` to decide init form. + if (p.cur_kind == TK_LET) { + n.lhs = parselet_local(p); // init (consumes its own ';') + n.cond = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after for cond"); + n.rhs = parseexpr(p); + } else { + // Parse one expr. If next is ';', it's a 3-clause without init. + let first: *node = parseexpr(p); + if (accept_tok(p, TK_SEMI)) { + // cond ; post + n.cond = first; + n.rhs = parseexpr(p); + } else { + // just (cond) + n.cond = first; + }; + }; + expect_tok(p, TK_RPAREN, "expected ')' after for"); + n.body = parseblock(p); + return n; +}; + +fn parsestmt(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + + // `static` is allowed on local lets per Hare; we accept and skip + // it (it doesn't change the AST shape). + if (p.cur_kind == TK_STATIC) { advance(p); }; + + if (p.cur_kind == TK_LBRACE) { + let b: *node = parseblock(p); + expect_tok(p, TK_SEMI, "expected ';' after block"); + return b; + }; + if (p.cur_kind == TK_LET) { return parselet_local(p); }; + if (p.cur_kind == TK_IF) { + let n: *node = parseif(p); + expect_tok(p, TK_SEMI, "expected ';' after if"); + return n; + }; + if (p.cur_kind == TK_FOR) { + let n: *node = parsefor(p); + expect_tok(p, TK_SEMI, "expected ';' after for"); + return n; + }; + if (p.cur_kind == TK_RETURN) { + advance(p); + let n: *node = newnode(p.a, N_RETURN, pf, pl, pc); + if (p.cur_kind != TK_SEMI) { + let first: *node = parseexpr(p); + // Hare-style multi-value: `return a, b;` becomes a + // tuple expression so codegen sees one rvalue. + if (p.cur_kind == TK_COMMA) { + let t: *node = newnode(p.a, N_TUPLE, pf, pl, pc); + t.list = first; + let tail: *node = first; + for (accept_tok(p, TK_COMMA)) { + let e: *node = parseexpr(p); + tail.next = e; + tail = e; + }; + n.lhs = t; + } else { + n.lhs = first; + }; + }; + expect_tok(p, TK_SEMI, "expected ';' after return"); + return n; + }; + if (p.cur_kind == TK_DEFER) { + advance(p); + let n: *node = newnode(p.a, N_DEFER, pf, pl, pc); + n.lhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after defer"); + return n; + }; + if (p.cur_kind == TK_BREAK) { + advance(p); + expect_tok(p, TK_SEMI, "expected ';' after break"); + return newnode(p.a, N_BREAK, pf, pl, pc); + }; + if (p.cur_kind == TK_CONTINUE) { + advance(p); + expect_tok(p, TK_SEMI, "expected ';' after continue"); + return newnode(p.a, N_CONTINUE, pf, pl, pc); + }; + // expression statement + let n: *node = newnode(p.a, N_EXPRSTMT, pf, pl, pc); + n.lhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after expression statement"); + return n; +}; + +// ---- top-level decl parsers ------------------------------------------ + +fn parseuse(p: *parser) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `use` + let n: *node = newnode(p.a, N_USE, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_SEMI, "expected ';' after use"); + return n; +}; + +fn parsedef(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `def` + let n: *node = newnode(p.a, N_DEF, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_COLON, "expected ':' in def"); + n.lhs = parsetype(p); + expect_tok(p, TK_ASSIGN, "expected '=' in def"); + n.rhs = parseexpr(p); + expect_tok(p, TK_SEMI, "expected ';' after def"); + n.exported = exported; + return n; +}; + +fn parselet(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `let` + let n: *node = newnode(p.a, N_LET, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + if (accept_tok(p, TK_COLON)) { + n.lhs = parsetype(p); + }; + if (accept_tok(p, TK_ASSIGN)) { + n.rhs = parseexpr(p); + }; + expect_tok(p, TK_SEMI, "expected ';' after let"); + n.exported = exported; + return n; +}; + +fn parseattrs(p: *parser) *node = { + let head: *node = nil; + let tail: *node = nil; + for (p.cur_kind == TK_AT) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); + let a: *node = newnode(p.a, N_ATTR, pf, pl, pc); + let id: str; + expectident(p, &id); + a.str = id; + expect_tok(p, TK_LPAREN, "expected '(' after attribute name"); + let arghead: *node = nil; + parsearglist(p, TK_RPAREN, &arghead); + a.list = arghead; + expect_tok(p, TK_RPAREN, "expected ')' after attribute args"); + if (head == nil) { head = a; tail = a; } + else { tail.next = a; tail = a; }; + }; + return head; +}; + +fn parseparams(p: *parser) *node = { + if (p.cur_kind == TK_RPAREN) { return nil; }; + let head: *node = nil; + let tail: *node = nil; + for (true) { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + 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. + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_COLON, "expected ':' in parameter"); + n.lhs = parsetype(p); + if (head == nil) { head = n; tail = n; } + else { tail.next = n; tail = n; }; + if (!accept_tok(p, TK_COMMA)) { break; }; + if (p.cur_kind == TK_RPAREN) { break; }; + }; + return head; +}; + +fn parsefn(p: *parser, exported: i32, attrs: *node) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `fn` + let n: *node = newnode(p.a, N_FNDECL, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_LPAREN, "expected '(' after fn name"); + n.list = parseparams(p); + expect_tok(p, TK_RPAREN, "expected ')' after params"); + if (p.cur_kind != TK_ASSIGN) { + if (p.cur_kind != TK_SEMI) { + n.lhs = parsetype(p); + }; + }; + if (accept_tok(p, TK_ASSIGN)) { + n.body = parseblock(p); + expect_tok(p, TK_SEMI, "expected ';' after fn body"); + } else { + // Body-less fn: FFI declaration (`fn name(args) ret;`). + expect_tok(p, TK_SEMI, "expected ';' after fn header"); + }; + n.exported = exported; + n.attr = attrs; + return n; +}; + +fn parsetypedecl(p: *parser, exported: i32) *node = { + let pf: str = p.cur_file; + let pl: i32 = p.cur_line; + let pc: i32 = p.cur_col; + advance(p); // past `type` + let n: *node = newnode(p.a, N_TYPEDECL, pf, pl, pc); + let id: str; + expectident(p, &id); + n.str = id; + expect_tok(p, TK_ASSIGN, "expected '=' in type decl"); + n.lhs = parsetype(p); + expect_tok(p, TK_SEMI, "expected ';' after type decl"); + n.exported = exported; + return n; +}; + +// ---- file-level loop ------------------------------------------------- + +export fn parsefile(p: *parser) *node = { + let f: *node = newnode(p.a, N_FILE, p.cur_file, p.cur_line, p.cur_col); + let head: *node = nil; + let tail: *node = nil; + + for (p.cur_kind != TK_EOF) { + let attrs: *node = parseattrs(p); + let exported: i32 = 0; + if (p.cur_kind == TK_EXPORT) { exported = 1; advance(p); }; + + let d: *node = nil; + if (p.cur_kind == TK_USE) { + d = parseuse(p); + } else { if (p.cur_kind == TK_DEF) { + d = parsedef(p, exported); + } else { if (p.cur_kind == TK_TYPE) { + d = parsetypedecl(p, exported); + } else { if (p.cur_kind == TK_LET) { + d = parselet(p, exported); + } else { if (p.cur_kind == TK_FN) { + d = parsefn(p, exported, attrs); + } else { + // Recovery: chew tokens until next ';' or EOF, balancing + // '{' '}' pairs so internal ';'s in unfamiliar forms don't + // derail us. + for (p.cur_kind != TK_SEMI) { + if (p.cur_kind == TK_EOF) { break; }; + if (p.cur_kind == TK_LBRACE) { + let depth: i32 = 0; + for (true) { + if (p.cur_kind == TK_EOF) { break; }; + if (p.cur_kind == TK_LBRACE) { depth += 1; advance(p); continue; }; + if (p.cur_kind == TK_RBRACE) { + depth -= 1; + advance(p); + if (depth == 0) { break; }; + continue; + }; + advance(p); + }; + continue; + }; + advance(p); + }; + if (p.cur_kind == TK_SEMI) { advance(p); }; + };};};};}; + + if (d != nil) { + if (head == nil) { + head = d; + tail = d; + } else { + tail.next = d; + tail = d; + }; + }; + }; + f.list = head; + return f; +}; + +// selfhost/cmd/wwc/type.ww — port of cmd/wwc/type.c. +// +// Status: full structural port. The C version uses module-globals for +// the primitive types (ty_void, ty_i32, …); ww doesn't have writable +// global storage yet, so we bundle the primitives into a `tctx` that +// the checker passes around explicitly. typesinit fills the tctx +// once per arena. + +use os; +use mem; + +// ---- TypeKind --------------------------------------------------------- +// Numeric values must stay aligned with cmd/wwc/ww.h TypeKind so the +// next diff signal (typed-AST printer / cgen) can compare across the +// two implementations. + +def TY_NONE: i32 = 0; +def TY_VOID: i32 = 1; +def TY_BOOL: i32 = 2; +def TY_RUNE: i32 = 3; +def TY_I8: i32 = 4; +def TY_I16: i32 = 5; +def TY_I32: i32 = 6; +def TY_I64: i32 = 7; +def TY_U8: i32 = 8; +def TY_U16: i32 = 9; +def TY_U32: i32 = 10; +def TY_U64: i32 = 11; +def TY_UINT: i32 = 12; +def TY_INT: i32 = 13; +def TY_UINTPTR: i32 = 14; +def TY_F32: i32 = 15; +def TY_F64: i32 = 16; +def TY_STR: i32 = 17; +def TY_PTR: i32 = 18; +def TY_SLICE: i32 = 19; +def TY_ARRAY: i32 = 20; +def TY_STRUCT: i32 = 21; +def TY_FN: i32 = 22; +def TY_CHAN: i32 = 23; +def TY_NAMED: i32 = 24; +def TY_TUPLE: i32 = 25; +def TY_TAGGED: i32 = 26; +def TY_ERR: i32 = 27; +def TY_UNTYPED_INT: i32 = 28; +def TY_UNTYPED_FLOAT: i32 = 29; +def TY_UNTYPED_STR: i32 = 30; +def TY_UNTYPED_RUNE: i32 = 31; +def TY_UNTYPED_BOOL: i32 = 32; +def TY_UNTYPED_NIL: i32 = 33; + +// ---- tinfo / tfield / tparam ----------------------------------------- + +type tfield = struct { + name: str, + type_: *tinfo, + offset: u64, + tnext: *tfield, +}; + +type tparam = struct { + name: str, + type_: *tinfo, + tnext: *tparam, +}; + +type tinfo = struct { + kind: i32, + size: u64, + align: u64, + sub: *tinfo, // ptr/slice/array/chan element + alen: u64, + fields: *tfield, + params: *tparam, + ret: *tinfo, + variadic: i32, + name: str, + under: *tinfo, +}; + +// ---- tctx — the box of primitive types ------------------------------- + +type tctx = struct { + a: *arena, + ty_void: *tinfo, + ty_bool: *tinfo, + ty_rune: *tinfo, + ty_i8: *tinfo, + ty_i16: *tinfo, + ty_i32: *tinfo, + ty_i64: *tinfo, + ty_u8: *tinfo, + ty_u16: *tinfo, + ty_u32: *tinfo, + ty_u64: *tinfo, + ty_int: *tinfo, + ty_uint: *tinfo, + ty_uintptr: *tinfo, + ty_f32: *tinfo, + ty_f64: *tinfo, + ty_str: *tinfo, + ty_err: *tinfo, + ty_untyped_int: *tinfo, + ty_untyped_float: *tinfo, + ty_untyped_str: *tinfo, + ty_untyped_rune: *tinfo, + ty_untyped_bool: *tinfo, + ty_untyped_nil: *tinfo, +}; + +// ---- constructors ----------------------------------------------------- + +export fn newtype(a: *arena, k: i32) *tinfo = { + let t: *tinfo = amalloc(a, 96u64): *tinfo; + t.kind = k; + return t; +}; + +fn prim(a: *arena, k: i32, nm: str, sz: u64, al: u64) *tinfo = { + let t: *tinfo = newtype(a, k); + t.name = nm; + t.size = sz; + if (al > 0u64) { t.align = al; } else { t.align = sz; }; + return t; +}; + +export fn typesinit(c: *tctx, a: *arena) void = { + c.a = a; + c.ty_void = prim(a, TY_VOID, "void", 0u64, 1u64); + c.ty_bool = prim(a, TY_BOOL, "bool", 1u64, 1u64); + c.ty_rune = prim(a, TY_RUNE, "rune", 4u64, 4u64); + c.ty_i8 = prim(a, TY_I8, "i8", 1u64, 1u64); + c.ty_i16 = prim(a, TY_I16, "i16", 2u64, 2u64); + c.ty_i32 = prim(a, TY_I32, "i32", 4u64, 4u64); + c.ty_i64 = prim(a, TY_I64, "i64", 8u64, 8u64); + c.ty_u8 = prim(a, TY_U8, "u8", 1u64, 1u64); + c.ty_u16 = prim(a, TY_U16, "u16", 2u64, 2u64); + c.ty_u32 = prim(a, TY_U32, "u32", 4u64, 4u64); + c.ty_u64 = prim(a, TY_U64, "u64", 8u64, 8u64); + c.ty_int = prim(a, TY_INT, "int", 8u64, 8u64); + c.ty_uint = prim(a, TY_UINT, "uint", 8u64, 8u64); + c.ty_uintptr= prim(a, TY_UINTPTR, "uintptr", 8u64, 8u64); + c.ty_f32 = prim(a, TY_F32, "f32", 4u64, 4u64); + c.ty_f64 = prim(a, TY_F64, "f64", 8u64, 8u64); + c.ty_str = prim(a, TY_STR, "str", 16u64, 8u64); + c.ty_err = prim(a, TY_ERR, "", 0u64, 1u64); + + c.ty_untyped_int = prim(a, TY_UNTYPED_INT, "untyped_int", 0u64, 1u64); + c.ty_untyped_float = prim(a, TY_UNTYPED_FLOAT, "untyped_float", 0u64, 1u64); + c.ty_untyped_str = prim(a, TY_UNTYPED_STR, "untyped_str", 0u64, 1u64); + c.ty_untyped_rune = prim(a, TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); + c.ty_untyped_bool = prim(a, TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); + c.ty_untyped_nil = prim(a, TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); +}; + +export fn type_ptr(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_PTR); + t.sub = sub; + t.size = 8u64; + t.align = 8u64; + return t; +}; + +export fn type_slice(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_SLICE); + t.sub = sub; + t.size = 24u64; + t.align = 8u64; + return t; +}; + +export fn type_array(a: *arena, sub: *tinfo, n: u64) *tinfo = { + let t: *tinfo = newtype(a, TY_ARRAY); + t.sub = sub; + t.alen = n; + if (sub != nil) { + t.size = sub.size * n; + t.align = sub.align; + } else { + t.align = 1u64; + }; + return t; +}; + +export fn type_chan(a: *arena, sub: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_CHAN); + t.sub = sub; + t.size = 8u64; + t.align = 8u64; + return t; +}; + +export fn type_named(a: *arena, name: str, under: *tinfo) *tinfo = { + let t: *tinfo = newtype(a, TY_NAMED); + t.name = name; + t.under = under; + if (under != nil) { + t.size = under.size; + t.align = under.align; + }; + return t; +}; + +// ---- predicates ------------------------------------------------------- + +export fn type_isint(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_I8) { return true; }; + if (k == TY_I16) { return true; }; + if (k == TY_I32) { return true; }; + if (k == TY_I64) { return true; }; + if (k == TY_U8) { return true; }; + if (k == TY_U16) { return true; }; + if (k == TY_U32) { return true; }; + if (k == TY_U64) { return true; }; + if (k == TY_INT) { return true; }; + if (k == TY_UINT){ return true; }; + if (k == TY_UINTPTR) { return true; }; + if (k == TY_RUNE){ return true; }; + if (k == TY_UNTYPED_INT) { return true; }; + if (k == TY_UNTYPED_RUNE) { return true; }; + if (k == TY_NAMED) { return type_isint(t.under); }; + return false; +}; + +export fn type_isfloat(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_F32) { return true; }; + if (k == TY_F64) { return true; }; + if (k == TY_UNTYPED_FLOAT) { return true; }; + if (k == TY_NAMED) { return type_isfloat(t.under); }; + return false; +}; + +export fn type_isnum(t: *tinfo) bool = { + if (type_isint(t)) { return true; }; + return type_isfloat(t); +}; + +export fn type_isunsigned(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_U8) { return true; }; + if (k == TY_U16) { return true; }; + if (k == TY_U32) { return true; }; + if (k == TY_U64) { return true; }; + if (k == TY_UINT){ return true; }; + if (k == TY_UINTPTR) { return true; }; + if (k == TY_NAMED) { return type_isunsigned(t.under); }; + return false; +}; + +export fn type_isuntyped(t: *tinfo) bool = { + if (t == nil) { return false; }; + let k: i32 = t.kind; + if (k == TY_UNTYPED_INT) { return true; }; + if (k == TY_UNTYPED_FLOAT) { return true; }; + if (k == TY_UNTYPED_STR) { return true; }; + if (k == TY_UNTYPED_RUNE) { return true; }; + if (k == TY_UNTYPED_BOOL) { return true; }; + if (k == TY_UNTYPED_NIL) { return true; }; + return false; +}; + +// type_eq — structural equality. Named types compare nominally. +export fn type_eq(a: *tinfo, b: *tinfo) bool = { + if (a == b) { return true; }; + if (a == nil) { return false; }; + if (b == nil) { return false; }; + if (a.kind != b.kind) { return false; }; + let k: i32 = a.kind; + if (k == TY_PTR) { return type_eq(a.sub, b.sub); }; + if (k == TY_SLICE) { return type_eq(a.sub, b.sub); }; + if (k == TY_CHAN) { return type_eq(a.sub, b.sub); }; + if (k == TY_ARRAY) { + if (a.alen != b.alen) { return false; }; + return type_eq(a.sub, b.sub); + }; + if (k == TY_FN) { + if (a.variadic != b.variadic) { return false; }; + if (!type_eq(a.ret, b.ret)) { return false; }; + let pa: *tparam = a.params; + let pb: *tparam = b.params; + for (true) { + if (pa == nil) { if (pb == nil) { return true; }; return false; }; + if (pb == nil) { return false; }; + if (!type_eq(pa.type_, pb.type_)) { return false; }; + pa = pa.tnext; + pb = pb.tnext; + }; + return true; + }; + if (k == TY_STRUCT) { + let fa: *tfield = a.fields; + let fb: *tfield = b.fields; + for (true) { + if (fa == nil) { if (fb == nil) { return true; }; return false; }; + if (fb == nil) { return false; }; + let na: str = fa.name; + let nb: str = fb.name; + if (na.len != nb.len) { return false; }; + let i: i32 = 0; + for (i < na.len) { + if (na[i] != nb[i]) { return false; }; + i += 1; + }; + if (!type_eq(fa.type_, fb.type_)) { return false; }; + fa = fa.tnext; + fb = fb.tnext; + }; + return true; + }; + if (k == TY_NAMED) { return false; }; // nominal: only same ptr + if (k == TY_TUPLE) { + let pa: *tparam = a.params; + let pb: *tparam = b.params; + for (true) { + if (pa == nil) { if (pb == nil) { return true; }; return false; }; + if (pb == nil) { return false; }; + if (!type_eq(pa.type_, pb.type_)) { return false; }; + pa = pa.tnext; + pb = pb.tnext; + }; + return true; + }; + return true; // primitives match by kind alone +}; + +// selfhost/cmd/wwc/sym.ww — port of cmd/wwc/sym.c. +// +// Per-scope hashtable, chained to the parent. Lookup walks up. +// Plan 9 / Hare flavoured. Duplicate definitions in the same scope +// return nil; the caller flags the error. + +use mem; +use typ; +use ast; + +// Symbol kinds — must stay numerically aligned with cmd/wwc/ww.h Skind. +def SK_NONE: i32 = 0; +def SK_VAR: i32 = 1; +def SK_PARAM: i32 = 2; +def SK_DEF: i32 = 3; +def SK_TYPE: i32 = 4; +def SK_FN: i32 = 5; +def SK_USE: i32 = 6; +def SK_FIELD: i32 = 7; + +type sym = struct { + name: str, + skind: i32, + type_: *tinfo, + decl: *node, + exported: i32, + snext: *sym, // iteration order + hashnext: *sym, // hash bucket chain + scope: *scope, +}; + +def NBUCKETS: i32 = 16; + +type scope = struct { + parent: *scope, + first: *sym, + last: *sym, + buckets: **sym, // length = NBUCKETS + nbuckets: i32, + a: *arena, +}; + +// FNV-1a 64 — same hash the C side uses, so bucket distribution is +// identical when both walk a scope in declaration order. +fn hashstr(s: str) u64 = { + let h: u64 = 14695981039346656037u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + h = h ^ (c: u64); + h = h * 1099511628211u64; + i += 1; + }; + return h; +}; + +export fn newscope(a: *arena, parent: *scope) *scope = { + let s: *scope = amalloc(a, 64u64): *scope; + s.parent = parent; + s.a = a; + s.nbuckets = NBUCKETS; + s.buckets = amalloc(a, (NBUCKETS: u64) * 8u64): **sym; + return s; +}; + +export fn streq(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; +}; + +export fn scope_lookup_local(s: *scope, name: str) *sym = { + if (s == nil) { return nil; }; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + let b: *sym = s.buckets[bi]; + for (b != nil) { + let bn: str = b.name; + if (streq(bn, name)) { return b; }; + b = b.hashnext; + }; + return nil; +}; + +export fn scope_lookup(s: *scope, name: str) *sym = { + for (s != nil) { + let r: *sym = scope_lookup_local(s, name); + if (r != nil) { return r; }; + s = s.parent; + }; + return nil; +}; + +export fn scope_define(s: *scope, name: str, k: i32, t: *tinfo, decl: *node) *sym = { + if (scope_lookup_local(s, name) != nil) { return nil; }; + let sy: *sym = amalloc(s.a, 80u64): *sym; + sy.name = name; + sy.skind = k; + sy.type_ = t; + sy.decl = decl; + sy.scope = s; + let h: u64 = hashstr(name); + let bi: i32 = (h % (s.nbuckets: u64)): i32; + sy.hashnext = s.buckets[bi]; + s.buckets[bi] = sy; + if (s.first == nil) { s.first = sy; } else { s.last.snext = sy; }; + s.last = sy; + return sy; +}; + +// selfhost/cmd/wwc/check.ww — minimal port of cmd/wwc/check.c. +// +// Status: name-resolution + primitive-type seeding only. Full type +// inference, conversion rules, tagged-union dispatch typing, return- +// type checking, etc. all live in cmd/wwc/check.c (937 lines) and +// will land here in subsequent commits. +// +// What this version does: +// 1. Creates a top scope and seeds it with primitive type names so +// `i32`, `str`, `*u8` etc. resolve. +// 2. Walks the file's top-level decls (use/def/type/fn/let) and +// installs Sym entries for each. +// 3. Recursively walks fn bodies; for every N_IDENT used as an +// expression or as a type name, looks it up and counts the +// resolved vs. unresolved. +// 4. Returns a summary the caller (wwdump -r) prints; the test +// asserts unresolved == 0 on every selfhost fixture, which is +// the floor signal that the frontend can name-resolve real ww. + +use os; +use mem; +use tok; + +type checker = struct { + a: *arena, + tc: *tctx, + top: *scope, + cur: *scope, + nresolved: i32, + nunresolved: i32, + errs: i32, + verbose: i32, // when non-zero, log each unresolved name +}; + +// seed_primitives — install the built-in type names so `i32`, `str`, +// etc. can be looked up like ordinary symbols. +fn seed_primitives(c: *checker) void = { + scope_define(c.top, "void", SK_TYPE, c.tc.ty_void, nil); + scope_define(c.top, "bool", SK_TYPE, c.tc.ty_bool, nil); + scope_define(c.top, "rune", SK_TYPE, c.tc.ty_rune, nil); + scope_define(c.top, "i8", SK_TYPE, c.tc.ty_i8, nil); + scope_define(c.top, "i16", SK_TYPE, c.tc.ty_i16, nil); + scope_define(c.top, "i32", SK_TYPE, c.tc.ty_i32, nil); + scope_define(c.top, "i64", SK_TYPE, c.tc.ty_i64, nil); + scope_define(c.top, "u8", SK_TYPE, c.tc.ty_u8, nil); + scope_define(c.top, "u16", SK_TYPE, c.tc.ty_u16, nil); + scope_define(c.top, "u32", SK_TYPE, c.tc.ty_u32, nil); + scope_define(c.top, "u64", SK_TYPE, c.tc.ty_u64, nil); + scope_define(c.top, "int", SK_TYPE, c.tc.ty_int, nil); + scope_define(c.top, "uint", SK_TYPE, c.tc.ty_uint, nil); + scope_define(c.top, "uintptr", SK_TYPE, c.tc.ty_uintptr, nil); + scope_define(c.top, "f32", SK_TYPE, c.tc.ty_f32, nil); + scope_define(c.top, "f64", SK_TYPE, c.tc.ty_f64, nil); + scope_define(c.top, "str", SK_TYPE, c.tc.ty_str, nil); + // `nil`, `true`, `false` are keywords — handled at the lex/parser + // level, no symbol needed. + // `len`, `alloc`, `free` are pseudo-builtins; scope_define them so + // their use sites resolve. The actual semantics live in cgen. + scope_define(c.top, "len", SK_FN, nil, nil); + scope_define(c.top, "alloc", SK_FN, nil, nil); + scope_define(c.top, "free", SK_FN, nil, nil); +}; + +// install_decl — install the top-level decl's name into the top scope. +// We don't compute its type yet (that's the resolve pass) — just bind +// the name so forward references resolve. +fn install_decl(c: *checker, d: *node) void = { + if (d == nil) { return; }; + let k: i32 = d.kind; + let nm: str = d.str; + if (k == N_USE) { scope_define(c.top, nm, SK_USE, nil, d); return; }; + if (k == N_DEF) { scope_define(c.top, nm, SK_DEF, nil, d); return; }; + if (k == N_TYPEDECL) { scope_define(c.top, nm, SK_TYPE, nil, d); return; }; + if (k == N_FNDECL) { scope_define(c.top, nm, SK_FN, nil, d); return; }; + if (k == N_LET) { scope_define(c.top, nm, SK_VAR, nil, d); return; }; +}; + +// resolve_walk — recursive AST walk that, for every N_IDENT and +// N_TNAME seen, looks up the name and bumps the resolved/unresolved +// counters. Local lets are installed in the current scope as soon as +// their init/type expressions have been walked (forward use of a let +// before its declaration would resolve to nothing — same semantics as +// the C checker's collect-then-resolve flow within a function). +fn resolve_walk(c: *checker, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + // `use IDENT;` — name is a module label, not a free ident. + if (k == N_USE) { return; }; + + if (k == N_IDENT) { + let nm: str = n.str; + if (nm.len > 0) { + let s: *sym = scope_lookup(c.cur, nm); + if (s == nil) { + c.nunresolved += 1; + if (c.verbose != 0) { + os.write(2, " unresolved id: ".ptr, 17u64); + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "\n".ptr, 1u64); + }; + } else { c.nresolved += 1; }; + }; + }; + + if (k == N_TNAME) { + let nm: str = n.str; + if (nm.len > 0) { + let s: *sym = scope_lookup(c.cur, nm); + if (s == nil) { + c.nunresolved += 1; + if (c.verbose != 0) { + os.write(2, " unresolved tname: ".ptr, 20u64); + os.write(2, nm.ptr, nm.len: u64); + os.write(2, "\n".ptr, 1u64); + }; + } else { c.nresolved += 1; }; + }; + }; + + // `match (e) { case let v: T => stmt; ... }` — the binding `v` + // is declared by the case arm and visible inside its body. + if (k == N_MCASE) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + let nm: str = n.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_VAR, nil, n); + }; + if (n.body != nil) { resolve_walk(c, n.body); }; + return; + }; + + if (k == N_DOT) { + // Walk only the base; the .field name is a member, not a + // free identifier. + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + if (k == N_FIELD) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + if (k == N_TFIELD) { + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + return; + }; + + // Walk children (mirroring ast.ww's printer descent order). + if (n.attr != nil) { resolve_walk(c, n.attr); }; + if (n.lhs != nil) { resolve_walk(c, n.lhs); }; + if (n.rhs != nil) { resolve_walk(c, n.rhs); }; + if (n.cond != nil) { resolve_walk(c, n.cond); }; + if (n.body != nil) { resolve_walk(c, n.body); }; + if (n.els != nil) { resolve_walk(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + resolve_walk(c, m); + m = m.next; + }; + }; + + // After walking children: a local `let X: T = init;` registers + // `X` so subsequent statements can resolve it. Top-level lets + // are installed in install_decl, so this duplicate install at + // the file scope just no-ops (scope_define returns nil on dup). + if (k == N_LET) { + let nm: str = n.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_VAR, nil, n); + }; + }; +}; + +// install_param — when entering a fn body, define its params in a +// fresh local scope. +fn install_params(c: *checker, params: *node) void = { + let p: *node = params; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (nm.len > 0) { + scope_define(c.cur, nm, SK_PARAM, nil, p); + }; + }; + p = p.next; + }; +}; + +// resolve_fnbody — open a child scope for the fn, install its params, +// then walk the body. Local lets installed by walk_stmt (a future +// extension); for the current pass we just resolve-walk without +// per-statement scopes. +fn resolve_fnbody(c: *checker, fnnode: *node) void = { + let outer: *scope = c.cur; + c.cur = newscope(c.a, c.cur); + install_params(c, fnnode.list); + if (fnnode.body != nil) { + resolve_walk(c, fnnode.body); + }; + c.cur = outer; +}; + +export fn check_init(c: *checker, a: *arena, tc: *tctx) void = { + c.a = a; + c.tc = tc; + c.top = newscope(a, nil); + c.cur = c.top; + c.nresolved = 0; + c.nunresolved = 0; + c.errs = 0; + c.verbose = 0; + seed_primitives(c); +}; + +export fn check_file(c: *checker, file: *node) void = { + if (file == nil) { return; }; + if (file.kind != N_FILE) { return; }; + + // Pass 1: install all top-level names. + let d: *node = file.list; + for (d != nil) { + install_decl(c, d); + d = d.next; + }; + + // Pass 2: walk decl bodies/types and resolve identifiers. + d = file.list; + for (d != nil) { + let k: i32 = d.kind; + if (k == N_FNDECL) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; // return type + resolve_fnbody(c, d); + } else { if (k == N_DEF) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + if (d.rhs != nil) { resolve_walk(c, d.rhs); }; + } else { if (k == N_TYPEDECL) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + } else { if (k == N_LET) { + if (d.lhs != nil) { resolve_walk(c, d.lhs); }; + if (d.rhs != nil) { resolve_walk(c, d.rhs); }; + };};};}; + d = d.next; + }; +}; + +// selfhost/cmd/wwc/cgen.ww — port of cmd/6c/cgen.c. +// +// Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c` +// producing byte-identical output to C-side `6c` for the same source, +// then by assembling + linking + running the result. +// +// Current coverage: +// - decls: N_FILE, N_FNDECL (params, frame for locals, prologue +// + dual-epilogue suppression; FFI body-less fn skipped) +// - stmts: N_BLOCK, N_RETURN, N_EXPRSTMT, N_LET (no init), +// N_LET (int-literal / ident / call / N_BIN init), +// N_IF (with optional else), N_FOR (cond-only and full +// init/cond/post), N_BREAK, N_CONTINUE +// - exprs: N_INTLIT, N_IDENT (local/param), N_BIN with full op +// coverage (+/-/*/// %, &/|/^, <>, comparisons with +// signed-vs-unsigned dispatch, &&/||), N_UN (- ! ~ & *), +// N_CALL (recursive R-to-L push, pop into argregs L-to-R), +// N_ASSIGN to local idents (plain and compound +=/-=) +// +// Type info is shallow — frame slots are 8 bytes per local, all loads +// /stores are MOVQ. Programs that mix i8/i32/i64 locals work but spill +// 8 bytes per local. Float, str, slice, struct, match, defer, alloc, +// tagged-union return — none of those are wired yet. + +use os; +use mem; +use ast; +use tok; +use typ; +use sym; +use strconv; + +// ---- typedef alias registry ----------------------------------------- +// +// `type error = str;` makes `error` a struct-shape alias. We track +// alias→target so is_str_type / is_slice_type / struct_lookup can +// resolve through the chain. Only direct N_TNAME aliases are mapped; +// `type p = struct {...}` is handled by collect_structs. + +type alias_ent = struct { + aname: str, + target: *node, // the rhs type expr + aanext: *alias_ent, +}; + +fn collect_aliases(c: *cgen, file: *node) void = { + c.aliases = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind != N_TSTRUCT) { + let a: *alias_ent = amalloc(c.a, 32u64): *alias_ent; + a.aname = d.str; + a.target = body; + a.aanext = c.aliases; + c.aliases = a; + }; + }; + }; + d = d.next; + }; +}; + +fn alias_lookup(c: *cgen, name: str) *node = { + let a: *alias_ent = c.aliases; + for (a != nil) { + let an: str = a.aname; + if (streq(an, name)) { return a.target; }; + a = a.aanext; + }; + return nil; +}; + +// resolve_type — follow typedef alias chains to a "canonical" type +// expr (str/slice/array/struct/...). Stops on cycles via depth limit. +fn resolve_type(c: *cgen, t: *node) *node = { + let cur: *node = t; + let depth: i32 = 0; + for (depth < 16) { + if (cur == nil) { return nil; }; + if (cur.kind != N_TNAME) { return cur; }; + let nm: str = cur.str; + let next: *node = alias_lookup(c, nm); + if (next == nil) { return cur; }; + cur = next; + depth += 1; + }; + return cur; +}; + +// ---- struct registry ------------------------------------------------ +// +// Per-file map from struct name → list of fields with computed offsets +// and sizes. Built when cg_file walks N_TYPEDECL with N_TSTRUCT lhs. +// N_DOT and N_ASSIGN consult this to resolve `s.field` for struct or +// *struct bases. + +type field_info = struct { + fname: str, + foff: i32, + fsz: i32, + tnode: *node, // the field type expr, for nested struct lookups + finext: *field_info, +}; + +type struct_info = struct { + sname: str, + fields: *field_info, + tot_size: i32, + sinext: *struct_info, +}; + +// ---- locals / frame -------------------------------------------------- + +type local = struct { + name: str, + off: i32, + tnode: *node, // declared type expr (N_TNAME / N_TPTR / ...) or nil + lnext: *local, +}; + +// strlit — interned string literal record. Emitted as a DATA directive +// after all functions; cgexpr N_STRLIT loads (LEAQ ptr, MOVQ len). +type strlit = struct { + label: str, // "_S_" + bytes: str, + slnext: *strlit, +}; + +// ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr +// gets its CALL target rewritten to `name`. +type ffi = struct { + ident: str, + symbol: str, + fnext: *ffi, +}; + +def LOOP_MAX: i32 = 16; + +type cgen = struct { + a: *arena, + locals: *local, + frame: i32, + last_was_return: i32, + labelseq: i32, + strlit_seq: i32, + strlits: *strlit, + ffis: *ffi, + defs: *def_ent, + fnrets: *fnret, + aliases: *alias_ent, + structs: *struct_info, + fn_name: str, + fn_ret: *node, // declared return type of current fn (or nil) + loop_top: i32, + loop_end_buf: *str, // stack of end labels for break + loop_cont_buf: *str, // stack of cont labels for continue +}; + +fn cgen_init(c: *cgen, a: *arena) void = { + c.a = a; + c.locals = nil; + c.frame = 0; + c.last_was_return = 0; + c.labelseq = 0; + // Note: strlit_seq, strlits, ffis are *not* reset here; they + // persist across cgfn calls within one file. cg_file resets them + // at the start of each compilation unit. + c.loop_top = 0; + c.loop_end_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str; + c.loop_cont_buf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str; +}; + +fn local_add(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { + // Round slot size up to 8-byte multiple so all locals stay + // 8-aligned (C cgen does the same — buf[4]u8 still occupies an + // 8-byte slot). + let asz: i32 = sz; + if (asz < 8) { asz = 8; }; + if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; + c.frame += asz; + let off: i32 = 0 - c.frame; + let l: *local = amalloc(c.a, 48u64): *local; + l.name = name; + l.off = off; + l.tnode = tnode; + l.lnext = c.locals; + c.locals = l; + return off; +}; + +fn local_find_node(c: *cgen, name: str) *local = { + let l: *local = c.locals; + for (l != nil) { + let ln: str = l.name; + if (streq(ln, name)) { return l; }; + l = l.lnext; + }; + return nil; +}; + +fn local_find(c: *cgen, name: str) i32 = { + let l: *local = c.locals; + for (l != nil) { + let ln: str = l.name; + if (ln.len == name.len) { + let i: i32 = 0; + let eq: bool = true; + for (i < name.len) { + if (ln[i] != name[i]) { eq = false; i = name.len; } + else { i += 1; }; + }; + if (eq) { return l.off; }; + }; + l = l.lnext; + }; + return 0; +}; + +// ---- emit helpers --------------------------------------------------- + +fn emit_line(s: str) void = { os.write(1, s.ptr, s.len: u64); }; + +fn emit_int(v: i64) void = { + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +fn emit_uint(v: u64) void = { + let buf: [32]u8; + let n: i32 = strconv.u64toa(buf[0:32], v); + os.write(1, buf.ptr, n: u64); +}; + +// emit_disp_reg — print "disp(reg)" or "(reg)" when disp == 0, the +// way Plan 9 6c/6a do. +fn emit_disp_reg(off: i64, reg: str) void = { + if (off != 0i64) { emit_int(off); }; + emit_line("("); + emit_line(reg); + emit_line(")"); +}; + +// emit_off — print an integer offset, suppressing it entirely when 0. +// Use before any emit_line("(BP)...") or emit_line("(SB)...") sequence. +// Plan 9 cc convention: "(BP)" not "0(BP)". +fn emit_off(v: i64) void = { + if (v != 0i64) { emit_int(v); }; +}; + +// mklabel — fresh label "__". Returns an +// arena-owned str. Mirrors C cgen's mklabel so diffs match. +fn mklabel(c: *cgen, base: str) str = { + let buf: [128]u8; + let i: i32 = 0; + let fname: str = c.fn_name; + let j: i32 = 0; + for (j < fname.len) { + buf[i] = fname[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + j = 0; + for (j < base.len) { + buf[i] = base[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + let n: i32 = strconv.i64toa(buf[i:128], c.labelseq: i64); + c.labelseq += 1; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { + p[k] = buf[k]; + k += 1; + }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + +fn emit_label(s: str) void = { + os.write(1, s.ptr, s.len: u64); + emit_line(":\n"); +}; + +// ---- string interning ------------------------------------------------ +// +// streq is provided by sym.ww and reused here. + +// intern_strlit — return a stable label for `bytes`. Dedups by content +// so identical literals share storage. +fn intern_strlit(c: *cgen, bytes: str) str = { + let s: *strlit = c.strlits; + for (s != nil) { + let bs: str = s.bytes; + if (streq(bs, bytes)) { + return s.label; + }; + s = s.slnext; + }; + // New label "_S_". + let buf: [32]u8; + buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_" + let n: i32 = strconv.i64toa(buf[3:32], c.strlit_seq: i64); + c.strlit_seq += 1; + let total: i32 = 3 + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let i: i32 = 0; + for (i < total) { p[i] = buf[i]; i += 1; }; + p[total] = 0u8; + let lab: str; + lab.ptr = p; + lab.len = total; + let nw: *strlit = amalloc(c.a, 48u64): *strlit; + nw.label = lab; + nw.bytes = bytes; + nw.slnext = c.strlits; + c.strlits = nw; + return lab; +}; + +// emit_def_constants — DATA directive per top-level int-literal `def`. +// 8 bytes little-endian to match what the C cgen emits. +fn emit_def_constants(c: *cgen, file: *node) void = { + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_DEF) { + let r: *node = d.rhs; + let v: u64 = 0u64; + let ok: bool = false; + if (r != nil) { + if (r.kind == N_INTLIT) { v = r.uval; ok = true; }; + if (r.kind == N_RUNELIT) { v = r.uval; ok = true; }; + if (r.kind == N_TRUE) { v = 1u64; ok = true; }; + if (r.kind == N_FALSE) { v = 0u64; ok = true; }; + if (r.kind == N_NIL) { v = 0u64; ok = true; }; + }; + if (ok) { + emit_line("DATA "); + let nm: str = d.str; + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB),\""); + let i: i32 = 0; + let n: u64 = v; + for (i < 8) { + let b: u8 = (n & 255u64): u8; + n = n >> 8u64; + // C emit_defs only special-cases " and \; + // every other non-printable goes as \xHH. + if (b == 34u8) { emit_line("\\\""); } + else { if (b == 92u8) { emit_line("\\\\"); } + else { + if (b < 32u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + if (b >= 127u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };}; + i += 1; + }; + emit_line("\"\n"); + }; + }; + d = d.next; + }; +}; + +// emit_data_section — DATA directives for every interned strlit. +// Trailing NUL appended so .ptr can be used as a C string by syscalls. +fn emit_data_section(c: *cgen) void = { + let s: *strlit = c.strlits; + for (s != nil) { + emit_line("DATA "); + let lab: str = s.label; + os.write(1, lab.ptr, lab.len: u64); + emit_line("(SB),\""); + let bs: str = s.bytes; + let i: i32 = 0; + for (i < bs.len) { + let b: u8 = bs[i]; + if (b == 34u8) { emit_line("\\\""); } // " + else { if (b == 92u8) { emit_line("\\\\"); } // \ + else { if (b == 10u8) { emit_line("\\n"); } + else { if (b == 9u8) { emit_line("\\t"); } + else { if (b == 13u8) { emit_line("\\r"); } + else { + if (b < 32u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + if (b >= 127u8) { + emit_line("\\x"); + let hi: u8 = b >> 4u8; + let lo: u8 = b & 15u8; + let bb: [2]u8; + if (hi < 10u8) { bb[0] = hi + 48u8; } + else { bb[0] = (hi - 10u8) + 97u8; }; + if (lo < 10u8) { bb[1] = lo + 48u8; } + else { bb[1] = (lo - 10u8) + 97u8; }; + os.write(1, bb.ptr, 2u64); + } else { + let bb: [1]u8; + bb[0] = b; + os.write(1, bb.ptr, 1u64); + }; + }; + };};};};}; + i += 1; + }; + emit_line("\\x00\"\n"); + s = s.slnext; + }; +}; + +// ---- fn return-type map --------------------------------------------- +// +// Per-file: ident → ret-type-node. Used to decide whether to shuffle +// (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so +// the value flows through cgen as the canonical (AX, BX) str pair. + +type fnret = struct { + fname: str, + rtype: *node, + frnext: *fnret, +}; + +fn collect_fnrets(c: *cgen, file: *node) void = { + c.fnrets = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + let f: *fnret = amalloc(c.a, 32u64): *fnret; + f.fname = d.str; + f.rtype = d.lhs; + f.frnext = c.fnrets; + c.fnrets = f; + }; + d = d.next; + }; +}; + +fn fnret_lookup(c: *cgen, name: str) *node = { + let f: *fnret = c.fnrets; + for (f != nil) { + let fn_: str = f.fname; + if (streq(fn_, name)) { return f.rtype; }; + f = f.frnext; + }; + return nil; +}; + +// ---- def-constant registry ------------------------------------------ +// +// `def NAME: T = LIT;` becomes a DATA symbol the C-side 6c emits; an +// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at +// file load and consult on N_IDENT lookup. + +type def_ent = struct { + dname: str, + dnext: *def_ent, +}; + +fn collect_defs(c: *cgen, file: *node) void = { + c.defs = nil; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_DEF) { + let e: *def_ent = amalloc(c.a, 32u64): *def_ent; + e.dname = d.str; + e.dnext = c.defs; + c.defs = e; + }; + d = d.next; + }; +}; + +fn def_lookup(c: *cgen, name: str) bool = { + let e: *def_ent = c.defs; + for (e != nil) { + let dn: str = e.dname; + if (streq(dn, name)) { return true; }; + e = e.dnext; + }; + return false; +}; + +// ---- FFI map --------------------------------------------------------- + +fn ffi_collect(c: *cgen, file: *node) void = { + c.ffis = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + let a: *node = d.attr; + for (a != nil) { + if (a.kind == N_ATTR) { + let aname: str = a.str; + if (streq(aname, "symbol")) { + let sym_node: *node = a.list; + if (sym_node != nil) { + if (sym_node.kind == N_STRLIT) { + let f: *ffi = amalloc(c.a, 48u64): *ffi; + f.ident = d.str; + f.symbol = sym_node.str; + f.fnext = c.ffis; + c.ffis = f; + }; + }; + }; + }; + a = a.next; + }; + }; + d = d.next; + }; +}; + +fn ffi_resolve(c: *cgen, ident: str) str = { + let f: *ffi = c.ffis; + for (f != nil) { + let id: str = f.ident; + if (streq(id, ident)) { return f.symbol; }; + f = f.fnext; + }; + return ident; +}; + +// ---- ABI argreg helpers --------------------------------------------- + +fn argreg_name(i: i32) str = { + if (i == 0) { return "DI"; }; + if (i == 1) { return "SI"; }; + if (i == 2) { return "DX"; }; + if (i == 3) { return "CX"; }; + if (i == 4) { return "R8"; }; + if (i == 5) { return "R9"; }; + return "?"; +}; + +// ---- expression cgen ------------------------------------------------- + +// push_args_rev — recursively walks the arg list, evaluates rightmost +// first, and pushes. str args take two slots (ptr in AX, len in BX); +// the order on the stack so a left-to-right pop into argregs lands +// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the +// pop sequence then yields AX, then BX. +fn push_args_rev(c: *cgen, arg: *node) i32 = { + if (arg == nil) { return 0; }; + let rest: i32 = push_args_rev(c, arg.next); + // N_SLICE expression as arg: `buf[lo:hi]` builds a slice header + // on the stack matching C cgen's sequence — push base, push hi, + // compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr). + if (arg.kind == N_SLICE) { + let base: *node = arg.lhs; + let lo: *node = arg.rhs; + let hi: *node = arg.cond; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + }; + }; + // base address → push + if (base_local != nil) { + let tn: *node = base_local.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + }; + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), AX\n"); + }; + } else { + cgexpr(c, base); + }; + emit_line("\tPUSHQ\tAX\n"); + // hi (default base length) → push + if (hi != nil) { + cgexpr(c, hi); + } else { if (base_local != nil) { + let tn: *node = base_local.tnode; + if (tn != nil) { + if (tn.kind == N_TARRAY) { + let len_n: *node = tn.rhs; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { + emit_line("\tMOVQ\t$"); + emit_uint(len_n.uval); + emit_line(", AX\n"); + }; + }; + } else { if (tn.kind == N_TSLICE) { + emit_line("\tMOVQ\t"); + emit_off((base_local.off + 8): i64); + emit_line("(BP), AX\n"); + } else { if (tn.kind == N_TNAME) { + if (streq(tn.str, "str")) { + emit_line("\tMOVQ\t"); + emit_off((base_local.off + 8): i64); + emit_line("(BP), AX\n"); + }; + };};}; + }; + } else { + emit_line("\tMOVQ\t$0, AX\n"); + };}; + emit_line("\tPUSHQ\tAX\n"); + // lo (default 0) → AX + if (lo != nil) { cgexpr(c, lo); } + else { emit_line("\tMOVQ\t$0, AX\n"); }; + emit_line("\tPOPQ\tBX\n"); // hi + emit_line("\tPOPQ\tCX\n"); // base + emit_line("\tMOVQ\tBX, DX\n"); // DX = hi + emit_line("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len + emit_line("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr + emit_line("\tPUSHQ\tDX\n"); // cap + emit_line("\tPUSHQ\tDX\n"); // len + emit_line("\tPUSHQ\tCX\n"); // ptr (top) + return rest + 3; + }; + // Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in + // reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop + // into argregs lands the canonical (ptr/tag, len/v0, cap/v1). + if (arg.kind == N_IDENT) { + let nm: str = arg.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + if (is_slice_type(c, lc.tnode) || is_tagged_type(lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 16): i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 3; + }; + }; + }; + cgexpr(c, arg); + if (node_isslice(c, arg)) { + emit_line("\tPUSHQ\tCX\n"); + emit_line("\tPUSHQ\tBX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 3; + }; + if (node_isstr(c, arg)) { + emit_line("\tPUSHQ\tBX\n"); + emit_line("\tPUSHQ\tAX\n"); + return rest + 2; + }; + emit_line("\tPUSHQ\tAX\n"); + return rest + 1; +}; + +fn node_isslice(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { return is_slice_type(c, lc.tnode); }; + return false; + }; + if (k == N_SLICE) { return true; }; + return false; +}; + +// node_isstr — best-effort surface check: does this expression +// evaluate to a str value? Used to drive the call-arg push convention +// (str args take two slots: ptr + len). +fn node_isstr(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_STRLIT) { return true; }; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { + let tnm: str = tn.str; + if (streq(tnm, "str")) { return true; }; + }; + }; + }; + return false; + }; + if (k == N_CALL) { + let callee: *node = n.lhs; + if (callee != nil) { + if (callee.kind == N_IDENT) { + let cnm: str = callee.str; + let rt: *node = fnret_lookup(c, cnm); + return is_str_type(c, rt); + }; + }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + // `.ptr` is *u8 not str; `.len` is i32 not str. + if (streq(fld, "ptr")) { return false; }; + if (streq(fld, "len")) { return false; }; + if (streq(fld, "cap")) { return false; }; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return is_str_type(c, fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { + return is_str_type(c, n.rhs); + }; + return false; +}; + +// type_name_isunsigned — true for u8/u16/u32/u64/uint/uintptr. +fn type_name_isunsigned(nm: str) bool = { + if (streq(nm, "u8")) { return true; }; + if (streq(nm, "u16")) { return true; }; + if (streq(nm, "u32")) { return true; }; + if (streq(nm, "u64")) { return true; }; + if (streq(nm, "uint")) { return true; }; + if (streq(nm, "uintptr")) { return true; }; + return false; +}; + +// type_node_isunsigned — recurse through TNAME / TPTR / TSLICE etc. +fn type_node_isunsigned(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { return type_name_isunsigned(t.str); }; + return false; +}; + +// type_name_issigned — true for i8/i16/i32/i64/int/rune. +fn type_name_issigned(nm: str) bool = { + if (streq(nm, "i8")) { return true; }; + if (streq(nm, "i16")) { return true; }; + if (streq(nm, "i32")) { return true; }; + if (streq(nm, "i64")) { return true; }; + if (streq(nm, "int")) { return true; }; + if (streq(nm, "rune")) { return true; }; + return false; +}; + +// field_load_op — pick the load instruction for a non-str struct +// field by its declared size + signedness. Mirrors the C cgen op +// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ +// for 8-byte). f might be nil for fields outside our struct registry. +fn field_load_op(f: *field_info) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVZBQ"; }; + if (sz == 4) { + let t: *node = f.tnode; + if (t != nil) { + if (t.kind == N_TNAME) { + if (type_name_issigned(t.str)) { return "MOVSXD"; }; + }; + }; + return "MOVL"; + }; + return "MOVQ"; +}; + +// field_store_op — pick the store instruction for a non-str struct +// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8. +fn field_store_op(f: *field_info) str = { + if (f == nil) { return "MOVQ"; }; + let sz: i32 = f.fsz; + if (sz == 1) { return "MOVB"; }; + if (sz == 4) { return "MOVL"; }; + return "MOVQ"; +}; + +// index_base_esz — element size for `arr[i]` where the base is a +// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice). +// For str the element is one byte; for `[]T` / `*[]T` we drill into +// the slice element type. +fn index_base_esz(c: *cgen, base: *node) i32 = { + if (base == nil) { return 8; }; + if (base.kind != N_DOT) { return 8; }; + let fld: str = base.str; + let inner: *node = base.lhs; + if (inner == nil) { return 8; }; + if (inner.kind != N_IDENT) { return 8; }; + let nm: str = inner.str; + let lc: *local = local_find_node(c, nm); + if (lc == nil) { return 8; }; + let tn: *node = lc.tnode; + if (tn == nil) { return 8; }; + + // `.ptr` pseudo-field on str/slice → element of the str/slice. + if (streq(fld, "ptr")) { + let inner_t: *node = tn; + if (tn.kind == N_TPTR) { inner_t = tn.lhs; }; + if (inner_t == nil) { return 8; }; + if (inner_t.kind == N_TNAME) { + if (streq(inner_t.str, "str")) { return 1; }; + }; + if (inner_t.kind == N_TSLICE) { return elem_size_of(inner_t); }; + return 8; + }; + + // Generic struct field: if it's *T, element size is T's size. + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let pinner: *node = tn.lhs; + if (pinner != nil) { + if (pinner.kind == N_TNAME) { sname = pinner.str; }; + }; + }; + if (sname.len == 0) { return 8; }; + let si: *struct_info = struct_lookup(c, sname); + if (si == nil) { return 8; }; + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return 8; }; + if (ft.kind == N_TPTR) { + let elem: *node = ft.lhs; + if (elem != nil) { + if (elem.kind == N_TNAME) { + if (streq(elem.str, "str")) { return 16; }; + let ps: i32 = prim_size(elem.str); + if (ps > 0) { return ps; }; + }; + }; + return 8; + }; + if (ft.kind == N_TSLICE) { return elem_size_of(ft); }; + return 8; + }; + fi = fi.finext; + }; + return 8; +}; + +// dot_inner_struct_ptr — for an N_DOT whose lhs is a chain of dots +// or an N_IDENT, walk the chain and return the N_TNAME tnode of the +// struct that the chain dereferences to (i.e., for `r.sym` where +// .sym is *lsym, return N_TNAME("lsym")). Returns nil if the chain +// doesn't resolve to a *struct. +// +// Used by the chained-DOT cgen path so `r.sym.val` knows the outer +// is a field of `lsym`. +fn dot_inner_struct_ptr(c: *cgen, n: *node) *node = { + if (n == nil) { return nil; }; + if (n.kind != N_DOT) { return nil; }; + let base: *node = n.lhs; + let fld: str = n.str; + if (base == nil) { return nil; }; + + // Resolve base's struct tnode. + let base_t: *node = nil; + if (base.kind == N_IDENT) { + let lc: *local = local_find_node(c, base.str); + if (lc == nil) { return nil; }; + let tn: *node = lc.tnode; + if (tn == nil) { return nil; }; + // base could be either struct-by-value (N_TNAME) or *struct (N_TPTR). + if (tn.kind == N_TNAME) { base_t = tn; }; + if (tn.kind == N_TPTR) { base_t = tn.lhs; }; + } else { if (base.kind == N_DOT) { + base_t = dot_inner_struct_ptr(c, base); + };}; + if (base_t == nil) { return nil; }; + if (base_t.kind != N_TNAME) { return nil; }; + + // Look up the struct, find the field, return the field's *struct. + let si: *struct_info = struct_lookup(c, base_t.str); + if (si == nil) { return nil; }; + let fi: *field_info = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + let ft: *node = fi.tnode; + if (ft == nil) { return nil; }; + if (ft.kind != N_TPTR) { return nil; }; + let inner: *node = ft.lhs; + if (inner == nil) { return nil; }; + if (inner.kind != N_TNAME) { return nil; }; + return inner; + }; + fi = fi.finext; + }; + return nil; +}; + +// elem_size_of — given the type node of an indexable (`*T`, `[]T`, +// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/ +// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback). +fn elem_size_of(t: *node) i32 = { + if (t == nil) { return 1; }; + let k: i32 = t.kind; + let elem: *node = nil; + if (k == N_TPTR) { elem = t.lhs; }; + if (k == N_TSLICE) { elem = t.lhs; }; + if (k == N_TARRAY) { elem = t.lhs; }; + if (k == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return 1; }; + // Indexing a primitive name (rare): element size = the prim. + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + return 1; + }; + if (elem == nil) { return 1; }; + if (elem.kind == N_TNAME) { + let nm: str = elem.str; + // str element is 16B (ptr+len). prim_size returns 0 for it. + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + }; + return 8; +}; + +// node_isunsigned — best-effort cgen-time inference from the AST. We +// don't have a typed AST yet, so we walk surface nodes: +// N_INTLIT — never marked unsigned (no tsuffix plumbing yet) +// N_IDENT — look up the local's declared type +// N_DOT — look up the field's declared type via struct reg +// N_BIN / N_UN — recurse: unsigned if either operand is unsigned +// N_CAST — use the cast target type +// +// Conservative: if we can't tell, return false (signed). The cost of +// being wrong here is byte-different asm vs C, not bad runtime. +fn node_isunsigned(c: *cgen, n: *node) bool = { + if (n == nil) { return false; }; + let k: i32 = n.kind; + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { return type_node_isunsigned(lc.tnode); }; + return false; + }; + if (k == N_DOT) { + let base: *node = n.lhs; + let fld: str = n.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (lkind == N_TNAME) { sname = tn.str; }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + return type_node_isunsigned(fi.tnode); + }; + fi = fi.finext; + }; + }; + }; + }; + }; + }; + return false; + }; + if (k == N_CAST) { return type_node_isunsigned(n.rhs); }; + if (k == N_BIN) { + if (node_isunsigned(c, n.lhs)) { return true; }; + return node_isunsigned(c, n.rhs); + }; + if (k == N_UN) { return node_isunsigned(c, n.lhs); }; + return false; +}; + +fn cgexpr(c: *cgen, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + if (k == N_INTLIT) { + emit_line("\tMOVQ\t$"); + emit_uint(n.uval); + emit_line(", AX\n"); + return; + }; + if (k == N_RUNELIT) { + emit_line("\tMOVQ\t$"); + emit_uint(n.uval); + emit_line(", AX\n"); + return; + }; + if (k == N_STRLIT) { + // Result is the (ptr, len) pair: ptr in AX, len in BX. Call + // sites that expect a str arg pick these up directly. + let nstr: str = n.str; + let lab: str = intern_strlit(c, nstr); + emit_line("\tLEAQ\t"); + os.write(1, lab.ptr, lab.len: u64); + emit_line("(SB), AX\n"); + emit_line("\tMOVQ\t$"); + emit_int(nstr.len: i64); + emit_line(", BX\n"); + return; + }; + if (k == N_TRUE) { + emit_line("\tMOVQ\t$1, AX\n"); + return; + }; + if (k == N_FALSE) { + emit_line("\tMOVQ\t$0, AX\n"); + return; + }; + if (k == N_NIL) { + emit_line("\tMOVQ\t$0, AX\n"); + return; + }; + + if (k == N_IDENT) { + let nm: str = n.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let off: i32 = lc.off; + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + // str local: also load the len half into BX. + if (is_str_type(c, lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), BX\n"); + }; + // slice local: load (ptr, len, cap) into (AX, BX, CX). + if (is_slice_type(c, lc.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((off + 8): i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_off((off + 16): i64); + emit_line("(BP), CX\n"); + }; + return; + }; + // Top-level `def` constant — load from its DATA symbol. + if (def_lookup(c, nm)) { + emit_line("\tMOVQ\t"); + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB), AX\n"); + return; + }; + // Fn-name used as a value (e.g. `let f = some_fn;` or + // `... = some_fn;`). LEAQ the symbol address into AX. + let rt: *node = fnret_lookup(c, nm); + if (rt != nil) { + emit_line("\tLEAQ\t"); + os.write(1, nm.ptr, nm.len: u64); + emit_line("(SB), AX\n"); + return; + }; + return; + }; + + if (k == N_INDEX) { + // Element-size-aware load: u8-element bases use MOVZBQ, + // everything else MOVQ. Fast path when the base is a bare + // ident (mem.ww shape). + let base: *node = n.lhs; + let idx: *node = n.rhs; + let esz: i32 = 8; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + if (base_local != nil) { esz = elem_size_of(base_local.tnode); }; + } else { if (base.kind == N_DOT) { + esz = index_base_esz(c, base); + };}; + }; + cgexpr(c, idx); + if (esz > 1) { + emit_line("\tMOVQ\t$"); + emit_int(esz: i64); + emit_line(", CX\n"); + emit_line("\tIMULQ\tCX, AX\n"); + }; + if (base_local != nil) { + let tn: *node = base_local.tnode; + let is_array: bool = false; + if (tn != nil) { if (tn.kind == N_TARRAY) { is_array = true; }; }; + if (is_array) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + }; + emit_line("\tADDQ\tAX, BX\n"); + // str element (16B): load (ptr, len) into (AX, BX) so + // the value flows through the str-rhs convention. + if (esz == 16) { + emit_line("\tMOVQ\t8(BX), CX\n"); + emit_line("\tMOVQ\t(BX), AX\n"); + emit_line("\tMOVQ\tCX, BX\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVZBQ\t(BX), AX\n"); } + else { emit_line("\tMOVQ\t(BX), AX\n"); }; + return; + }; + // Generic fallback when base isn't a plain ident. + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emit_line("\tPOPQ\tBX\n"); + emit_line("\tADDQ\tBX, AX\n"); + if (esz == 16) { + emit_line("\tMOVQ\t8(AX), BX\n"); + emit_line("\tMOVQ\t(AX), AX\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVZBQ\t(AX), AX\n"); } + else { emit_line("\tMOVQ\t(AX), AX\n"); }; + return; + }; + + if (k == N_MATCH) { + // match (e) { case let v: T => stmt; ... } + // + // Read the tagged-union slot and dispatch by tag. Slot + // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings + // (`case let v: T =>`) get a fresh local slot loaded from + // slot+8 (and slot+16 for str-typed payload). + let scrut: *node = n.lhs; + let scrut_off: i32 = 0; + let scrut_t: *node = nil; + if (scrut != nil) { + if (scrut.kind == N_IDENT) { + let lc: *local = local_find_node(c, scrut.str); + if (lc != nil) { + scrut_off = lc.off; + scrut_t = resolve_type(c, lc.tnode); + }; + }; + }; + let endl: str = mklabel(c, "match_end"); + let cs: *node = n.list; + for (cs != nil) { + let nxt: str = mklabel(c, "match_next"); + let pat: *node = cs.lhs; + // Compute the variant tag for this arm. Default arm + // (no pattern) skips the tag check. + if (pat != nil) { + let want: i32 = 0; + if (scrut_t != nil) { + if (scrut_t.kind == N_TTAGGED) { + let pat_name: str; + pat_name.ptr = nil; pat_name.len = 0; + if (pat.kind == N_TNAME) { pat_name = pat.str; }; + let v: *node = scrut_t.list; + let idx: i32 = 0; + let found: bool = false; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, pat_name)) { + want = idx; + found = true; + v = nil; + }; + }; + if (v != nil) { + v = v.next; + idx += 1; + }; + }; + if (!found) { want = 0; }; + }; + }; + emit_line("\tMOVQ\t"); + emit_off(scrut_off: i64); + emit_line("(BP), AX\n"); + emit_line("\tCMPQ\t$"); + emit_int(want: i64); + emit_line(", AX\n"); + emit_line("\tJNE\t"); + emit_line(nxt); + emit_line("\n"); + }; + // Bind `let v: T` from the slot, if requested. + let bn: str = cs.str; + if (bn.len > 0) { + if (pat != nil) { + let bsz: i32 = 8; + if (is_str_type(c, pat)) { bsz = 16; }; + let voff: i32 = local_add(c, bn, bsz, pat); + emit_line("\tMOVQ\t"); + emit_off((scrut_off + 8): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\tAX, "); + emit_off(voff: i64); + emit_line("(BP)\n"); + if (bsz == 16) { + emit_line("\tMOVQ\t"); + emit_off((scrut_off + 16): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\tAX, "); + emit_off((voff + 8): i64); + emit_line("(BP)\n"); + }; + }; + }; + // Body. Match arms are statements; we cgstmt them. + if (cs.body != nil) { cgstmt(c, cs.body); }; + emit_line("\tJMP\t"); + emit_line(endl); + emit_line("\n"); + emit_label(nxt); + cs = cs.next; + }; + emit_label(endl); + return; + }; + + if (k == N_CAST) { + // Type casts are mostly no-ops at the asm level for our + // integer-shaped operands. Evaluate the source; AX holds + // the bits unchanged. (Sign- or zero-extending narrow loads + // to wider types is the loader's job, not cast's, in this + // minimal cgen.) + cgexpr(c, n.lhs); + return; + }; + + if (k == N_DOT) { + let lhs: *node = n.lhs; + let fld: str = n.str; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let nm: str = lhs.str; + let lc: *local = local_find_node(c, nm); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + // Pointer-to-struct: deref then field load. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == N_TNAME) { + sname = inner.str; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + // str field via *struct: load len into a + // scratch first (so loading ptr into AX + // last leaves (AX=ptr, BX=len)). + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_disp_reg((fi.foff + 8): i64, "BX"); + emit_line(", CX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", AX\n"); + emit_line("\tMOVQ\tCX, BX\n"); + } else { + let op: str = field_load_op(fi); + emit_line("\t"); + emit_line(op); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + }; + // Direct struct local: field load at off+foff. + if (lkind == N_TNAME) { + let sname: str = tn.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + // str field: load both halves so chained + // `.ptr` / `.len` see (AX=ptr, BX=len). + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP), AX\n"); + emit_line("\tMOVQ\t"); + emit_off((lc.off + fi.foff + 8): i64); + emit_line("(BP), BX\n"); + } else { + let op: str = field_load_op(fi); + emit_line("\t"); + emit_line(op); + emit_line("\t"); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP), AX\n"); + }; + return; + }; + fi = fi.finext; + }; + }; + }; + // Array pseudo-fields: `.ptr` is the array's + // address (LEAQ); `.len` is the static element + // count (immediate). + if (lkind == N_TARRAY) { + if (streq(fld, "ptr")) { + emit_line("\tLEAQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), AX\n"); + return; + }; + if (streq(fld, "len")) { + let len_n: *node = tn.rhs; + let alen: i64 = 0i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { alen = len_n.uval: i64; }; + }; + emit_line("\tMOVQ\t$"); + emit_int(alen); + emit_line(", AX\n"); + return; + }; + }; + // str/slice pseudo-fields .ptr/.len/.cap on a + // direct local: load at slot+delta. + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + // Pointer to str/slice (`*[]u8`, `*str`): + // deref, then load at delta within the + // pointed-to header. C cgen does the same. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let inner_kind: i32 = -1; + if (inner != nil) { inner_kind = inner.kind; }; + let inner_str: bool = false; + if (inner_kind == N_TNAME) { + if (streq(inner.str, "str")) { inner_str = true; }; + }; + if (inner_kind == N_TSLICE) { inner_str = true; }; + if (inner_str) { + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(delta: i64, "BX"); + emit_line(", AX\n"); + return; + }; + }; + emit_line("\tMOVQ\t"); + emit_off((lc.off + delta): i64); + emit_line("(BP), AX\n"); + return; + }; + }; + }; + }; + // Module-qualified value reference: `mod.name` where `mod` + // is N_IDENT bound as SK_USE and the leaf isn't a local. + // Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback + // the C cgen takes when bt is NULL/ty_err. + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + emit_line("\tMOVQ\t"); + emit_line(fld); + emit_line("(SB), AX\n"); + return; + }; + }; + // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. + // Evaluate the str-producing expression — that leaves + // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` + // shuffles BX→AX. Mirrors what C cgen does (it just evaluates + // the literal and picks the half it wants). + if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; + if (streq(fld, "len")) { + cgexpr(c, lhs); + emit_line("\tMOVQ\tBX, AX\n"); + return; + }; + // Chained struct-field-via-ptr-via-ptr access: + // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 + // Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct + // field). Outer DOT dereferences and reads `val`. Without this + // path the cgen falls through and AX retains whatever the + // inner expression left there — typically the *struct pointer + // itself, so reads silently get the pointer value instead of + // the field. (Showed up porting 6l/pass.ww.) + if (lhs != nil) { + if (lhs.kind == N_DOT) { + let inner_t: *node = dot_inner_struct_ptr(c, lhs); + if (inner_t != nil) { + let sname: str = inner_t.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + if (streq(fi.fname, fld)) { + cgexpr(c, lhs); // AX = ptr to inner struct + let lop: str = field_load_op(fi); + // str field: load both halves. + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_disp_reg((fi.foff + 8): i64, "AX"); + emit_line(", BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(fi.foff: i64, "AX"); + emit_line(", AX\n"); + return; + }; + emit_line("\t"); + emit_line(lop); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "AX"); + emit_line(", AX\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + }; + return; + }; + + if (k == N_UN) { + // Match C cgen ordering: evaluate operand first (load into AX), + // then apply the unary op. AMP / STAR override AX with the + // address / deref. The wasted load before AMP keeps our asm + // byte-identical to the C version. + cgexpr(c, n.lhs); + if (n.op == TK_MINUS) { emit_line("\tNEGQ\tAX\n"); return; }; + if (n.op == TK_TILDE) { emit_line("\tNOTQ\tAX\n"); return; }; + if (n.op == TK_STAR) { emit_line("\tMOVQ\t(AX), AX\n"); return; }; + if (n.op == TK_AMP) { + let opnd: *node = n.lhs; + if (opnd != nil) { + if (opnd.kind == N_IDENT) { + let nm: str = opnd.str; + let off: i32 = local_find(c, nm); + if (off != 0) { + emit_line("\tLEAQ\t"); + emit_off(off: i64); + emit_line("(BP), AX\n"); + return; + }; + }; + }; + return; + }; + if (n.op == TK_NOT) { + let t: str = mklabel(c, "tt"); + let e: str = mklabel(c, "te"); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); emit_line(t); emit_line("\n"); + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tJMP\t"); emit_line(e); emit_line("\n"); + emit_label(t); + emit_line("\tMOVQ\t$1, AX\n"); + emit_label(e); + return; + }; + return; + }; + + if (k == N_BIN) { + let unsignd: bool = node_isunsigned(c, n.lhs); + if (!unsignd) { unsignd = node_isunsigned(c, n.rhs); }; + + cgexpr(c, n.rhs); + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, n.lhs); + emit_line("\tPOPQ\tBX\n"); + if (n.op == TK_PLUS) { emit_line("\tADDQ\tBX, AX\n"); return; }; + if (n.op == TK_MINUS) { emit_line("\tSUBQ\tBX, AX\n"); return; }; + if (n.op == TK_STAR) { emit_line("\tIMULQ\tBX, AX\n"); return; }; + if (n.op == TK_SLASH) { + emit_line("\tMOVQ\t$0, DX\n"); + if (unsignd) { emit_line("\tDIVQ\tBX\n"); } + else { emit_line("\tIDIVQ\tBX\n"); }; + return; + }; + if (n.op == TK_PERCENT) { + emit_line("\tMOVQ\t$0, DX\n"); + if (unsignd) { emit_line("\tDIVQ\tBX\n"); } + else { emit_line("\tIDIVQ\tBX\n"); }; + emit_line("\tMOVQ\tDX, AX\n"); + return; + }; + if (n.op == TK_AMP) { emit_line("\tANDQ\tBX, AX\n"); return; }; + if (n.op == TK_PIPE) { emit_line("\tORQ\tBX, AX\n"); return; }; + if (n.op == TK_CARET) { emit_line("\tXORQ\tBX, AX\n"); return; }; + if (n.op == TK_LSHIFT) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tSHLQ\tCX, AX\n"); + return; + }; + if (n.op == TK_RSHIFT) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tSHRQ\tCX, AX\n"); + return; + }; + if (n.op == TK_AND) { emit_line("\tANDQ\tBX, AX\n"); return; }; + if (n.op == TK_OR) { emit_line("\tORQ\tBX, AX\n"); return; }; + + // Comparison: emit CMPQ, jump on signed/unsigned variant, + // materialise 0/1 in AX. Same shape as the C cgen. + let is_cmp: bool = false; + let jcc: str = ""; + if (n.op == TK_EQ) { is_cmp = true; jcc = "JE"; }; + if (n.op == TK_NEQ) { is_cmp = true; jcc = "JNE"; }; + if (n.op == TK_LT) { is_cmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; + if (n.op == TK_LE) { is_cmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; + if (n.op == TK_GT) { is_cmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; + if (n.op == TK_GE) { is_cmp = true; if (unsignd) { jcc = "JAE"; } else { jcc = "JGE"; }; }; + if (is_cmp) { + let t: str = mklabel(c, "ct"); + let e: str = mklabel(c, "ce"); + emit_line("\tCMPQ\tBX, AX\n"); + emit_line("\t"); emit_line(jcc); emit_line("\t"); emit_line(t); emit_line("\n"); + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tJMP\t"); emit_line(e); emit_line("\n"); + emit_label(t); + emit_line("\tMOVQ\t$1, AX\n"); + emit_label(e); + return; + }; + return; + }; + + if (k == N_CALL) { + let nargs: i32 = push_args_rev(c, n.list); + let i: i32 = 0; + for (i < nargs) { + emit_line("\tPOPQ\t"); + emit_line(argreg_name(i)); + emit_line("\n"); + i += 1; + }; + let callee: *node = n.lhs; + let callee_name: str; + callee_name.ptr = nil; callee_name.len = 0; + // Detect fn-pointer field call: `w.emit(args)` where `w` is + // a struct local and `emit` is an N_TFN field. Load the + // field value into AX and CALL through it. + let is_fnptr_call: bool = false; + if (callee != nil) { + if (callee.kind == N_DOT) { + let base: *node = callee.lhs; + let fld: str = callee.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + let lkind: i32 = tn.kind; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (lkind == N_TNAME) { sname = tn.str; }; + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + let ft: *node = fi.tnode; + if (ft != nil) { + if (ft.kind == N_TFN) { + is_fnptr_call = true; + }; + }; + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; + if (is_fnptr_call) { + // Load fn-ptr field value into AX; CALL AX. We emit the + // load AFTER the args have been popped (so AX/BX/etc + // don't get clobbered by the field load before the pops). + // `popped args` left DI/SI/etc set; AX is free. + cgexpr(c, callee); + emit_line("\tCALL\tAX\n"); + } else { + emit_line("\tCALL\t"); + if (callee != nil) { + if (callee.kind == N_IDENT) { + callee_name = callee.str; + let resolved: str = ffi_resolve(c, callee_name); + os.write(1, resolved.ptr, resolved.len: u64); + } else { if (callee.kind == N_DOT) { + callee_name = callee.str; + let resolved: str = ffi_resolve(c, callee_name); + os.write(1, resolved.ptr, resolved.len: u64); + };}; + }; + emit_line("(SB)\n"); + }; + // SysV returns 16-byte aggregates in (AX, DX). Our str + // convention is (AX, BX), so shuffle for str-returning calls. + if (callee_name.len > 0) { + let rt: *node = fnret_lookup(c, callee_name); + if (is_str_type(c, rt)) { + emit_line("\tMOVQ\tDX, BX\n"); + }; + }; + return; + }; + + if (k == N_ASSIGN) { + let lhs: *node = n.lhs; + // `*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. + // We default to MOVQ (8B) since most fixtures use it; for + // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. + if (lhs != nil) { + if (lhs.kind == N_UN) { + if (lhs.op == TK_STAR) { + if (n.op == TK_ASSIGN) { + let inner: *node = lhs.lhs; + let elem_str: bool = false; + let store_op: str = "MOVQ"; + if (inner != nil) { + if (inner.kind == N_IDENT) { + let lc: *local = local_find_node(c, inner.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TPTR) { + let pe: *node = tn.lhs; + if (pe != nil) { + if (pe.kind == N_TNAME) { + if (streq(pe.str, "str")) { elem_str = true; } + else { + let ps: i32 = prim_size(pe.str); + if (ps == 1) { store_op = "MOVB"; } + else { if (ps == 4) { store_op = "MOVL"; }; }; + }; + }; + }; + }; + }; + }; + }; + }; + cgexpr(c, n.rhs); + if (elem_str) { emit_line("\tPUSHQ\tBX\n"); }; + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, inner); + emit_line("\tMOVQ\tAX, BX\n"); + emit_line("\tPOPQ\tAX\n"); + if (elem_str) { + emit_line("\tMOVQ\tAX, (BX)\n"); + emit_line("\tPOPQ\tCX\n"); + emit_line("\tMOVQ\tCX, 8(BX)\n"); + return; + }; + emit_line("\t"); + emit_line(store_op); + emit_line("\tAX, (BX)\n"); + return; + }; + }; + }; + }; + // Array/slice/ptr index store: `arr[i] = v;`. Element size + // from base.tnode picks MOVB vs MOVQ. + if (lhs != nil) { + if (lhs.kind == N_INDEX) { + if (n.op == TK_ASSIGN) { + let base: *node = lhs.lhs; + let idx: *node = lhs.rhs; + let esz: i32 = 8; + let base_local: *local = nil; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + base_local = local_find_node(c, bn); + if (base_local != nil) { + esz = elem_size_of(base_local.tnode); + }; + } else { if (base.kind == N_DOT) { + esz = index_base_esz(c, base); + };}; + }; + cgexpr(c, n.rhs); // value → AX + if (esz == 16) { emit_line("\tPUSHQ\tBX\n"); }; + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, idx); // idx → AX + if (esz > 1) { + emit_line("\tMOVQ\t$"); + emit_int(esz: i64); + emit_line(", CX\n"); + emit_line("\tIMULQ\tCX, AX\n"); + }; + emit_line("\tPUSHQ\tAX\n"); // scaled idx + if (base_local != nil) { + let tn: *node = base_local.tnode; + let is_array: bool = false; + if (tn != nil) { if (tn.kind == N_TARRAY) { is_array = true; }; }; + if (is_array) { + emit_line("\tLEAQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + } else { + emit_line("\tMOVQ\t"); + emit_off(base_local.off: i64); + emit_line("(BP), BX\n"); + }; + } else { + cgexpr(c, base); + emit_line("\tMOVQ\tAX, BX\n"); + }; + emit_line("\tPOPQ\tAX\n"); // scaled idx + emit_line("\tADDQ\tAX, BX\n"); + emit_line("\tPOPQ\tAX\n"); // value + if (esz == 16) { + emit_line("\tMOVQ\tAX, (BX)\n"); + emit_line("\tPOPQ\tCX\n"); + emit_line("\tMOVQ\tCX, 8(BX)\n"); + return; + }; + if (esz == 1) { emit_line("\tMOVB\tAX, (BX)\n"); } + else { emit_line("\tMOVQ\tAX, (BX)\n"); }; + return; + }; + }; + }; + // Struct/ptr-to-struct field assignment: `s.f = expr;` or + // `p.f = expr;`. Only plain `=` is wired (compound on field + // is rare and not yet needed by our fixtures). + if (lhs != nil) { + if (lhs.kind == N_DOT) { + let base: *node = lhs.lhs; + let fld: str = lhs.str; + if (base != nil) { + if (base.kind == N_IDENT) { + let bn: str = base.str; + let lc: *local = local_find_node(c, bn); + if (lc != nil) { + let tn: *node = lc.tnode; + let lkind: i32 = -1; + if (tn != nil) { lkind = tn.kind; }; + // Pointer-to-struct: deref then store. + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (inner != nil) { + if (inner.kind == N_TNAME) { sname = inner.str; }; + }; + if (sname.len > 0) { + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + if (n.op != TK_ASSIGN) { + // compound: load current value + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + let lop: str = field_load_op(fi); + emit_line("\t"); + emit_line(lop); + emit_line("\t"); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line(", BX\n"); + emit_line("\tPUSHQ\tBX\n"); + }; + cgexpr(c, n.rhs); + if (n.op != TK_ASSIGN) { + emit_line("\tPOPQ\tBX\n"); + // PLUSEQ is commutative; MINUSEQ + // needs lhs - rhs (BX is old lhs, + // AX is rhs). + if (n.op == TK_PLUSEQ) { emit_line("\tADDQ\tBX, AX\n"); }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, BX\n"); + emit_line("\tMOVQ\tBX, AX\n"); + }; + }; + // str field via *struct: rhs left + // (AX=ptr, BX=len). Use CX as the + // address scratch so we don't clobber + // the len half before storing it. + if (n.op == TK_ASSIGN) { + if (is_str_type(c, fi.tnode)) { + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), CX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(fi.foff: i64, "CX"); + emit_line("\n"); + emit_line("\tMOVQ\tBX, "); + emit_disp_reg((fi.foff + 8): i64, "CX"); + emit_line("\n"); + return; + }; + }; + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_disp_reg(fi.foff: i64, "BX"); + emit_line("\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + }; + // Direct struct local: store at off+foff. + if (lkind == N_TNAME) { + let sname: str = tn.str; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fld)) { + cgexpr(c, n.rhs); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_off((lc.off + fi.foff): i64); + emit_line("(BP)\n"); + return; + }; + fi = fi.finext; + }; + }; + }; + // str/slice pseudo-field assignment. + let delta: i32 = -1; + if (streq(fld, "ptr")) { delta = 0; }; + if (streq(fld, "len")) { delta = 8; }; + if (streq(fld, "cap")) { delta = 16; }; + if (delta >= 0) { + if (lkind == N_TPTR) { + let inner: *node = tn.lhs; + let inner_kind: i32 = -1; + if (inner != nil) { inner_kind = inner.kind; }; + let inner_str: bool = false; + if (inner_kind == N_TNAME) { + if (streq(inner.str, "str")) { inner_str = true; }; + }; + if (inner_kind == N_TSLICE) { inner_str = true; }; + if (inner_str) { + if (n.op != TK_ASSIGN) { + // Compound on `(*str|*slice).field`: load + // current → push → eval rhs → combine → store. + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\t"); + emit_disp_reg(delta: i64, "BX"); + emit_line(", BX\n"); + emit_line("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emit_line("\tPOPQ\tBX\n"); + // PLUSEQ is commutative; MINUSEQ + // needs lhs - rhs. + if (n.op == TK_PLUSEQ) { emit_line("\tADDQ\tBX, AX\n"); }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, BX\n"); + emit_line("\tMOVQ\tBX, AX\n"); + }; + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(delta: i64, "BX"); + emit_line("\n"); + return; + }; + cgexpr(c, n.rhs); + emit_line("\tMOVQ\t"); + emit_off(lc.off: i64); + emit_line("(BP), BX\n"); + emit_line("\tMOVQ\tAX, "); + emit_disp_reg(delta: i64, "BX"); + emit_line("\n"); + return; + }; + }; + cgexpr(c, n.rhs); + emit_line("\tMOVQ\tAX, "); + emit_off((lc.off + delta): i64); + emit_line("(BP)\n"); + return; + }; + }; + }; + }; + }; + }; + // Local-ident target — plain `=` and the simple compound + // forms (+= -= *= /=); other compounds fall back to + // "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path. + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let nm: str = lhs.str; + let off: i32 = local_find(c, nm); + if (off == 0) { return; }; + // Detect str-typed local — assignment must store both + // halves (AX=ptr at +0, BX=len at +8). + let lc_str: bool = false; + let lcn: *local = local_find_node(c, nm); + if (lcn != nil) { lc_str = is_str_type(c, lcn.tnode); }; + cgexpr(c, n.rhs); + if (n.op == TK_ASSIGN) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + if (lc_str) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + return; + }; + if (n.op == TK_PLUSEQ) { + emit_line("\tADDQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + if (n.op == TK_MINUSEQ) { + emit_line("\tSUBQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + // Generic compound: load → combine in BX → store. + emit_line("\tMOVQ\t"); + emit_off(off: i64); + emit_line("(BP), BX\n"); + if (n.op == TK_STAREQ) { emit_line("\tIMULQ\tAX, BX\n"); }; + if (n.op == TK_AMPEQ) { emit_line("\tANDQ\tAX, BX\n"); }; + if (n.op == TK_PIPEEQ) { emit_line("\tORQ\tAX, BX\n"); }; + if (n.op == TK_CARETEQ) { emit_line("\tXORQ\tAX, BX\n"); }; + if (n.op == TK_LSHIFTEQ) { + emit_line("\tMOVQ\tAX, CX\n"); + emit_line("\tSHLQ\tCX, BX\n"); + }; + if (n.op == TK_RSHIFTEQ) { + emit_line("\tMOVQ\tAX, CX\n"); + emit_line("\tSHRQ\tCX, BX\n"); + }; + emit_line("\tMOVQ\tBX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + return; + }; + }; + return; + }; +}; + +// ---- type-driven slot sizing ---------------------------------------- + +fn struct_lookup(c: *cgen, name: str) *struct_info = { + let s: *struct_info = c.structs; + for (s != nil) { + let sn: str = s.sname; + if (streq(sn, name)) { return s; }; + s = s.sinext; + }; + return nil; +}; + +// prim_size — size in bytes of a primitive type name (or 0 if not +// recognised as a primitive — the caller falls back to other paths). +fn prim_size(name: str) i32 = { + if (streq(name, "u8")) { return 1; }; + if (streq(name, "i8")) { return 1; }; + if (streq(name, "bool")) { return 1; }; + if (streq(name, "u16")) { return 2; }; + if (streq(name, "i16")) { return 2; }; + if (streq(name, "u32")) { return 4; }; + if (streq(name, "i32")) { return 4; }; + if (streq(name, "f32")) { return 4; }; + if (streq(name, "u64")) { return 8; }; + if (streq(name, "i64")) { return 8; }; + if (streq(name, "uint")) { return 8; }; + if (streq(name, "int")) { return 8; }; + if (streq(name, "uintptr")) { return 8; }; + if (streq(name, "f64")) { return 8; }; + if (streq(name, "rune")) { return 4; }; + if (streq(name, "void")) { return 0; }; + return 0; +}; + +fn slot_size(c: *cgen, typ_n: *node) i32 = { + if (typ_n == nil) { return 8; }; + let k: i32 = typ_n.kind; + if (k == N_TPTR) { return 8; }; + if (k == N_TFN) { return 8; }; + if (k == N_TCHAN) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TTUPLE) { return 16; }; + if (k == N_TTAGGED){ return 24; }; + if (k == N_TNAME) { + let nm: str = typ_n.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { + // Pad to 8 for stack slots — matches C cgen which spills + // every primitive into an 8-byte slot. + return 8; + }; + // Named struct lookup. + let si: *struct_info = struct_lookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TARRAY) { + let len_n: *node = typ_n.rhs; + let elem_n: *node = typ_n.lhs; + let elen: i64 = 1i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { elen = len_n.uval: i64; }; + }; + let esz: i32 = 8; + if (elem_n != nil) { + if (elem_n.kind == N_TNAME) { + let en: str = elem_n.str; + let ps: i32 = prim_size(en); + if (ps > 0) { esz = ps; }; + }; + }; + return (esz: i64 * elen): i32; + }; + if (k == N_TSTRUCT) { + // Inline anonymous struct — sum of field sizes. + let f: *node = typ_n.list; + let total: i32 = 0; + for (f != nil) { + if (f.kind == N_TFIELD) { + total += slot_size(c, f.lhs); + }; + f = f.next; + }; + return total; + }; + return 8; +}; + +// register_struct — compute field offsets + total size for a struct +// type-decl, store in c.structs. Field type sizes use the same +// slot_size logic (with primitives kept at their natural width — we +// only round to 8 for stack slots, not struct interiors). +fn field_size(c: *cgen, tnode: *node) i32 = { + if (tnode == nil) { return 8; }; + let k: i32 = tnode.kind; + if (k == N_TNAME) { + let nm: str = tnode.str; + if (streq(nm, "str")) { return 16; }; + let ps: i32 = prim_size(nm); + if (ps > 0) { return ps; }; + let si: *struct_info = struct_lookup(c, nm); + if (si != nil) { return si.tot_size; }; + return 8; + }; + if (k == N_TPTR) { return 8; }; + if (k == N_TSLICE) { return 24; }; + if (k == N_TARRAY) { + // Same shape as slot_size's TARRAY branch. + let len_n: *node = tnode.rhs; + let elem_n: *node = tnode.lhs; + let elen: i64 = 1i64; + if (len_n != nil) { + if (len_n.kind == N_INTLIT) { elen = len_n.uval: i64; }; + }; + let esz: i32 = field_size(c, elem_n); + return (esz: i64 * elen): i32; + }; + return 8; +}; + +fn register_struct(c: *cgen, name: str, tstruct: *node) void = { + let si: *struct_info = amalloc(c.a, 64u64): *struct_info; + si.sname = name; + si.fields = nil; + si.tot_size = 0; + let head: *field_info = nil; + let tail: *field_info = nil; + let off: i32 = 0; + let f: *node = tstruct.list; + for (f != nil) { + if (f.kind == N_TFIELD) { + let sz: i32 = field_size(c, f.lhs); + // Align to 8 for any field >= 4 bytes (matches our other + // cgen choices). i8/u8/bool may sit on odd byte offsets; + // the C cgen does similar best-effort packing. + let aln: i32 = 1; + if (sz >= 8) { aln = 8; } + else { if (sz >= 4) { aln = 4; } + else { if (sz >= 2) { aln = 2; }; }; }; + if ((off & (aln - 1)) != 0) { + off = (off + aln - 1) & ~(aln - 1); + }; + let fi: *field_info = amalloc(c.a, 48u64): *field_info; + fi.fname = f.str; + fi.foff = off; + fi.fsz = sz; + fi.tnode = f.lhs; + if (head == nil) { head = fi; tail = fi; } + else { tail.finext = fi; tail = fi; }; + off += sz; + }; + f = f.next; + }; + // Round total to 8 for stack-slot use. + if ((off & 7) != 0) { off = (off + 7) & ~7; }; + si.fields = head; + si.tot_size = off; + si.sinext = c.structs; + c.structs = si; +}; + +fn collect_structs(c: *cgen, file: *node) void = { + c.structs = nil; + if (file == nil) { return; }; + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_TYPEDECL) { + let body: *node = d.lhs; + if (body != nil) { + if (body.kind == N_TSTRUCT) { + register_struct(c, d.str, body); + }; + }; + }; + d = d.next; + }; +}; + +// ---- frame pre-scan -------------------------------------------------- +// +// Recursively walks the body to count every local `let`. Each gets a +// slot sized by slot_size(typ); 8-byte default. Match-bindings + for- +// init lets count too. Params are added by the cgfn driver. + +fn scan_locals(c: *cgen, n: *node) i32 = { + if (n == nil) { return 0; }; + let total: i32 = 0; + if (n.kind == N_LET) { + // Match local_add's rounding: < 8 bumps to 8, then 8-align. + // scan_locals must agree with local_add or the prologue + // SUBQ undersizes the frame and lets overflow into the + // caller's stack — corrupting whatever's at -frameSize..-1 + // of the caller. + let sz: i32 = slot_size(c, n.lhs); + if (sz < 8) { sz = 8; }; + if ((sz & 7) != 0) { sz = (sz + 7) & ~7; }; + total += sz; + }; + // Match-arm binding (`case let v: T => ...`) gets a slot too. + if (n.kind == N_MCASE) { + let bn: str = n.str; + if (bn.len > 0) { + let pat: *node = n.lhs; + if (pat != nil) { + if (is_str_type(c, pat)) { total += 16; } + else { total += 8; }; + }; + }; + }; + if (n.lhs != nil) { total += scan_locals(c, n.lhs); }; + if (n.rhs != nil) { total += scan_locals(c, n.rhs); }; + if (n.cond != nil) { total += scan_locals(c, n.cond); }; + if (n.body != nil) { total += scan_locals(c, n.body); }; + if (n.els != nil) { total += scan_locals(c, n.els); }; + if (n.list != nil) { + let m: *node = n.list; + for (m != nil) { + total += scan_locals(c, m); + m = m.next; + }; + }; + return total; +}; + +// ---- statement cgen -------------------------------------------------- + +fn cgstmt(c: *cgen, n: *node) void = { + if (n == nil) { return; }; + let k: i32 = n.kind; + + if (k == N_BLOCK) { + let s: *node = n.list; + for (s != nil) { + cgstmt(c, s); + s = s.next; + }; + return; + }; + + if (k == N_RETURN) { + let rhs: *node = n.lhs; + if (rhs != nil) { + // Tuple return `return a, b;` — pack as (AX=v0, DX=v1). + // Matches C cgen: evaluate v1 first (PUSHQ), then v0 + // into AX, then POPQ DX. End state: AX = v0, DX = v1. + if (rhs.kind == N_TUPLE) { + let v: *node = rhs.list; + if (v != nil) { + let v2: *node = v.next; + if (v2 != nil) { + cgexpr(c, v2); + emit_line("\tPUSHQ\tAX\n"); + cgexpr(c, v); + emit_line("\tPOPQ\tDX\n"); + } else { + cgexpr(c, v); + }; + }; + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + // Tagged-union return: pack as (AX=tag, DX=value0, CX=value1). + // For str variant, cgexpr leaves (AX=ptr, BX=len), so we + // shuffle DX←AX (ptr) and CX←BX (len), then load tag. + // For other variants, cgexpr leaves AX, shuffle DX←AX. + if (is_tagged_type(c.fn_ret)) { + cgexpr(c, rhs); + let idx: i32 = tagged_variant_index(c, c.fn_ret, rhs); + if (node_isstr(c, rhs)) { + emit_line("\tMOVQ\tBX, CX\n"); + emit_line("\tMOVQ\tAX, DX\n"); + } else { + emit_line("\tMOVQ\tAX, DX\n"); + }; + emit_line("\tMOVQ\t$"); + if (idx < 0) { idx = 0; }; + emit_int(idx: i64); + emit_line(", AX\n"); + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + cgexpr(c, rhs); + }; + // SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX). + // cgexpr leaves str in (AX, BX); shuffle BX→DX. + if (is_str_type(c, c.fn_ret)) { + emit_line("\tMOVQ\tBX, DX\n"); + }; + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + c.last_was_return = 1; + return; + }; + + if (k == N_EXPRSTMT) { + if (n.lhs != nil) { cgexpr(c, n.lhs); }; + c.last_was_return = 0; + return; + }; + + if (k == N_LET) { + let nm: str = n.str; + let sz: i32 = slot_size(c, n.lhs); + let off: i32 = local_add(c, nm, sz, n.lhs); + if (n.rhs != nil) { + let rhs: *node = n.rhs; + // Tagged-union init: `let r: (T | E) = expr;`. + // - If rhs is a CALL to a fn returning tagged-union, + // the result is already in (AX=tag, DX=v0, CX=v1); + // just spill all three. + // - Otherwise rhs is a bare variant value: pack tag + + // value(s). + if (is_tagged_type(n.lhs)) { + let rhs_returns_tagged: bool = false; + if (rhs.kind == N_CALL) { + let callee: *node = rhs.lhs; + if (callee != nil) { + let callee_name: str; + callee_name.ptr = nil; callee_name.len = 0; + if (callee.kind == N_IDENT) { callee_name = callee.str; }; + if (callee.kind == N_DOT) { callee_name = callee.str; }; + if (callee_name.len > 0) { + let rt: *node = fnret_lookup(c, callee_name); + if (is_tagged_type(rt)) { rhs_returns_tagged = true; }; + }; + }; + }; + cgexpr(c, rhs); + if (rhs_returns_tagged) { + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tDX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tCX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + c.last_was_return = 0; + return; + }; + let tag_idx: i32 = tagged_variant_index(c, n.lhs, rhs); + if (tag_idx < 0) { tag_idx = 0; }; + if (node_isstr(c, rhs)) { + emit_line("\tMOVQ\tAX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tBX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + } else { + emit_line("\tMOVQ\tAX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + emit_line("\tMOVQ\t$"); + emit_int(tag_idx: i64); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + c.last_was_return = 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. + if (rhs.kind == N_STRUCTLIT) { + let trefn: *node = rhs.lhs; + let sname: str; + sname.ptr = nil; sname.len = 0; + if (trefn != nil) { + if (trefn.kind == N_IDENT) { sname = trefn.str; } + else { if (trefn.kind == N_TNAME) { sname = trefn.str; }; }; + }; + let si: *struct_info = struct_lookup(c, sname); + if (si != nil) { + let field_node: *node = rhs.list; + for (field_node != nil) { + if (field_node.kind == N_FIELD) { + let fname: str = field_node.str; + let fi: *field_info = si.fields; + for (fi != nil) { + let fn_: str = fi.fname; + if (streq(fn_, fname)) { + cgexpr(c, field_node.lhs); + let sop: str = field_store_op(fi); + emit_line("\t"); + emit_line(sop); + emit_line("\tAX, "); + emit_off((off + fi.foff): i64); + emit_line("(BP)\n"); + fi = nil; + } else { + fi = fi.finext; + }; + }; + }; + field_node = field_node.next; + }; + c.last_was_return = 0; + return; + }; + }; + cgexpr(c, rhs); + emit_line("\tMOVQ\tAX, "); + emit_off(off: i64); + emit_line("(BP)\n"); + // str init: cgexpr also leaves len in BX; store both. + if (sz == 16) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + }; + // slice init: ptr/len/cap in AX/BX/CX. + if (sz == 24) { + emit_line("\tMOVQ\tBX, "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + emit_line("\tMOVQ\tCX, "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + }; + }; + c.last_was_return = 0; + return; + }; + + if (k == N_IF) { + let els: str = mklabel(c, "else"); + let endl: str = mklabel(c, "end"); + cgexpr(c, n.cond); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); + if (n.els != nil) { emit_line(els); } + else { emit_line(endl); }; + emit_line("\n"); + if (n.body != nil) { cgstmt(c, n.body); }; + if (n.els != nil) { + emit_line("\tJMP\t"); emit_line(endl); emit_line("\n"); + emit_label(els); + cgstmt(c, n.els); + }; + emit_label(endl); + c.last_was_return = 0; + return; + }; + + if (k == N_FOR) { + // Match C cgen's label scheme: _loop_N for the top, + // _endloop_N for the post-body merge. No separate cont + // label when there's no post-expression. + let topl: str = mklabel(c, "loop"); + let endl: str = mklabel(c, "endloop"); + + if (n.lhs != nil) { cgstmt(c, n.lhs); }; + + emit_label(topl); + if (n.cond != nil) { + cgexpr(c, n.cond); + emit_line("\tCMPQ\t$0, AX\n"); + emit_line("\tJE\t"); emit_line(endl); emit_line("\n"); + }; + + c.loop_end_buf[c.loop_top] = endl; + c.loop_cont_buf[c.loop_top] = topl; + c.loop_top += 1; + + if (n.body != nil) { cgstmt(c, n.body); }; + + c.loop_top -= 1; + + if (n.rhs != nil) { cgexpr(c, n.rhs); }; + emit_line("\tJMP\t"); emit_line(topl); emit_line("\n"); + emit_label(endl); + c.last_was_return = 0; + return; + }; + + if (k == N_BREAK) { + if (c.loop_top > 0) { + let lbl: str = c.loop_end_buf[c.loop_top - 1]; + emit_line("\tJMP\t"); emit_line(lbl); emit_line("\n"); + }; + c.last_was_return = 0; + return; + }; + if (k == N_CONTINUE) { + if (c.loop_top > 0) { + let lbl: str = c.loop_cont_buf[c.loop_top - 1]; + emit_line("\tJMP\t"); emit_line(lbl); emit_line("\n"); + }; + c.last_was_return = 0; + return; + }; + + c.last_was_return = 0; +}; + +// ---- function-level cgen --------------------------------------------- + +// is_str_type — true when the type expr resolves (through any +// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the +// alias chain registered at file load. +fn is_str_type_raw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TNAME) { + let nm: str = t.str; + if (streq(nm, "str")) { return true; }; + }; + return false; +}; + +fn is_str_type(c: *cgen, t: *node) bool = { + if (is_str_type_raw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolve_type(c, t); + return is_str_type_raw(r); +}; + +fn is_slice_type_raw(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TSLICE) { return true; }; + return false; +}; + +fn is_slice_type(c: *cgen, t: *node) bool = { + if (is_slice_type_raw(t)) { return true; }; + if (c == nil) { return false; }; + let r: *node = resolve_type(c, t); + return is_slice_type_raw(r); +}; + +fn is_tagged_type(t: *node) bool = { + if (t == nil) { return false; }; + if (t.kind == N_TTAGGED) { return true; }; + return false; +}; + +// rhs_target_name — for a returned value, what's its declared (or +// surface-inferred) type name? `expr: T` casts dictate T directly; +// bare strlit/intlit fall back to a primitive name. +fn rhs_target_name(c: *cgen, rhs: *node) str = { + let nm: str; + nm.ptr = nil; nm.len = 0; + if (rhs == nil) { return nm; }; + if (rhs.kind == N_CAST) { + let t: *node = rhs.rhs; + if (t != nil) { + if (t.kind == N_TNAME) { return t.str; }; + }; + return nm; + }; + if (rhs.kind == N_STRLIT) { return "str"; }; + if (rhs.kind == N_IDENT) { + let lc: *local = local_find_node(c, rhs.str); + if (lc != nil) { + let tn: *node = lc.tnode; + if (tn != nil) { + if (tn.kind == N_TNAME) { return tn.str; }; + }; + }; + }; + return nm; +}; + +// tagged_variant_index — given the tagged-union type expr and the +// returned value's surface type, find the matching variant's 0-based +// index. Compare by exact type name first; if no match, fall back to +// "any str-shape variant matches an str-typed value". +fn tagged_variant_index(c: *cgen, tagged: *node, rhs: *node) i32 = { + if (tagged == nil) { return -1; }; + if (rhs == nil) { return -1; }; + let want_name: str = rhs_target_name(c, rhs); + if (want_name.len > 0) { + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, want_name)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + }; + // Fallback: by str-shape (resolves aliases). + let want_str: bool = node_isstr(c, rhs); + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + let v_is_str: bool = false; + if (v.kind == N_TNAME) { + if (is_str_type(c, v)) { v_is_str = true; }; + }; + if (v_is_str == want_str) { return idx; }; + v = v.next; + idx += 1; + }; + return -1; +}; + +fn cgfn_params(c: *cgen, params: *node) void = { + let p: *node = params; + let idx: i32 = 0; + for (p != nil) { + if (p.kind == N_PARAM) { + let nm: str = p.str; + if (is_tagged_type(p.lhs)) { + // tagged-union param: passed in 3 regs (tag, v0, v1), + // 24-byte slot. + let off: i32 = local_add(c, nm, 24, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + idx += 1; + } else { if (is_slice_type(c, p.lhs)) { + // slice param: 3 regs (ptr, len, cap), 24-byte slot. + let off: i32 = local_add(c, nm, 24, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 16): i64); + emit_line("(BP)\n"); + idx += 1; + } else { if (is_str_type(c, p.lhs)) { + // str param: passed in two regs (ptr, len). + // Slot is 16 bytes; ptr at off+0, len at off+8. + let off: i32 = local_add(c, nm, 16, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off((off + 8): i64); + emit_line("(BP)\n"); + idx += 1; + } else { + let off: i32 = local_add(c, nm, 8, p.lhs); + emit_line("\tMOVQ\t"); + emit_line(argreg_name(idx)); + emit_line(", "); + emit_off(off: i64); + emit_line("(BP)\n"); + idx += 1; + };};}; + }; + p = p.next; + }; +}; + +fn cgfn(c: *cgen, fn_: *node) void = { + cgen_init(c, c.a); + c.fn_name = fn_.str; + c.fn_ret = fn_.lhs; + + emit_line("TEXT "); + let nm: str = fn_.str; + os.write(1, nm.ptr, nm.len: u64); + emit_line(",$"); + + // Pre-scan total frame: 24 bytes per slice param, 16 per str + // param, 8 per other param, plus per-let from scan_locals. + let scan_p: *node = fn_.list; + let frame: i32 = 0; + for (scan_p != nil) { + if (scan_p.kind == N_PARAM) { + if (is_tagged_type(scan_p.lhs)) { frame += 24; } + else { if (is_slice_type(c, scan_p.lhs)) { frame += 24; } + else { if (is_str_type(c, scan_p.lhs)) { frame += 16; } + else { frame += 8; }; }; }; + }; + scan_p = scan_p.next; + }; + if (fn_.body != nil) { frame += scan_locals(c, fn_.body); }; + if ((frame & 15) != 0) { + frame = (frame + 15) & ~15; + }; + emit_int(frame: i64); + emit_line("\n"); + + emit_line("\tPUSHQ\tBP\n"); + emit_line("\tMOVQ\tSP, BP\n"); + emit_line("\tSUBQ\t$"); + emit_int(frame: i64); + emit_line(", SP\n"); + + cgfn_params(c, fn_.list); + c.last_was_return = 0; + if (fn_.body != nil) { cgstmt(c, fn_.body); }; + + if (c.last_was_return == 0) { + // Zero AX before the fall-through return — matches C cgen, + // which always emits this so void-returning fns don't leak + // a stale callee value to their caller. + emit_line("\tMOVQ\t$0, AX\n"); + emit_line("\tMOVQ\tBP, SP\n"); + emit_line("\tPOPQ\tBP\n"); + emit_line("\tRET\n"); + }; +}; + +// ---- file-level entry ------------------------------------------------ + +export fn cg_file(c: *cgen, file: *node) void = { + if (file == nil) { return; }; + c.strlits = nil; + c.strlit_seq = 0; + collect_aliases(c, file); + collect_structs(c, file); + collect_defs(c, file); + collect_fnrets(c, file); + ffi_collect(c, file); + let d: *node = file.list; + for (d != nil) { + if (d.kind == N_FNDECL) { + if (d.body != nil) { + cgfn(c, d); + }; + }; + d = d.next; + }; + emit_data_section(c); + emit_def_constants(c, file); +}; + +// selfhost/cmd/wwdump/main.ww — ww-side port of cmd/wwdump/main.c. +// +// Reads a .ww file, runs the ww-side lexer, prints tokens through +// the ww-side tokprint. The 990_selfhost test diffs this output +// byte-for-byte against the C-side wwdump on the same file. Any +// divergence is a port bug in lex.ww or tok.ww. +// +// Modes: +// wwdump -t file.ww tokens (default) +// wwdump -a file.ww AST (not yet implemented; reserved) + +use os; +use mem; +use tok; +use lex; +use ast; +use parse; +use typ; +use sym; +use check; +use cgen; +use strconv; + +// ---- argv helpers ----------------------------------------------------- + +// argstrlen — strlen on a NUL-terminated *u8. argv strings are always +// NUL-terminated (kernel-supplied) so this is safe. +fn argstrlen(s: *u8) i32 = { + let n: i32 = 0; + for (s[n] != 0u8) { n += 1; }; + return n; +}; + +fn argstr(p: *u8) str = { + let s: str; + s.ptr = p; + s.len = argstrlen(p); + return s; +}; + +// streq_lit — compare a NUL-terminated argv entry to a string literal. +fn streq_lit(p: *u8, lit: str) bool = { + let i: i32 = 0; + for (i < lit.len) { + if (p[i] != lit[i]) { return false; }; + i += 1; + }; + return p[i] == 0u8; +}; + +// ---- main ------------------------------------------------------------- + +export fn main(argc: i32, argv: **u8) i32 = { + let mode: i32 = 116; // 't' + let path: *u8 = nil; + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_lit(a, "-t")) { + mode = 116; + } else { if (streq_lit(a, "-a")) { + mode = 97; // 'a' + } else { if (streq_lit(a, "-r")) { + mode = 114; // 'r' — resolve / name-check + } else { if (streq_lit(a, "-c")) { + mode = 99; // 'c' — codegen / emit asm + } else { if (path == nil) { + path = a; + };};};};}; + i += 1; + }; + if (path == nil) { + os.write(2, "usage: wwdump [-t|-a] file.ww\n".ptr, 30u64); + return 2; + }; + + let fd_or_err: (i32 | str) = os.tryopen(path, os.O_RDONLY, 0i32); + let fd: i32 = -1; + match (fd_or_err) { + case let v: i32 => fd = v; + case let e: str => { + os.write(2, "wwdump: cannot open ".ptr, 20u64); + os.write(2, path, argstrlen(path): u64); + os.write(2, "\n".ptr, 1u64); + return 1; + }; + }; + + let sz: i64 = os.filesize(fd); + if (sz < 0i64) { + os.write(2, "wwdump: filesize failed\n".ptr, 24u64); + os.close(fd); + return 1; + }; + + let a: *arena = newarena(); + let buf: *u8 = amalloc(a, sz: u64): *u8; + let r: i64 = os.readfull(fd, buf, sz: u64); + os.close(fd); + if (r != sz) { + os.write(2, "wwdump: short read\n".ptr, 19u64); + return 1; + }; + + let l: lex; + lexinit(&l, a, argstr(path), buf, sz: u64); + + if (mode == 116) { // '-t' + for (true) { + let t: tok; + lexnext(&l, &t); + tokprint(1i32, &t); + if (t.kind == TK_EOF) { break; }; + if (t.kind == TK_ERR) { break; }; + }; + } else { if (mode == 97) { // '-a' + let ps: parser; + parserinit(&ps, a, &l); + let f: *node = parsefile(&ps); + astprint(1i32, f); + } else { if (mode == 114) { // '-r' — name resolve report + let ps: parser; + parserinit(&ps, a, &l); + let f: *node = parsefile(&ps); + let tc: tctx; + typesinit(&tc, a); + let ck: checker; + check_init(&ck, a, &tc); + // Quiet by default; flip to 1 when debugging missing names. + ck.verbose = 0; + check_file(&ck, f); + // (close out the if-else chain — we'll close all braces below) + // ": / resolved" + os.write(1, argstr(path).ptr, argstrlen(path): u64); + os.write(1, ": ".ptr, 2u64); + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], ck.nresolved: i64); + os.write(1, buf.ptr, n: u64); + os.write(1, "/".ptr, 1u64); + let total: i32 = ck.nresolved + ck.nunresolved; + n = strconv.i64toa(buf[0:32], total: i64); + os.write(1, buf.ptr, n: u64); + os.write(1, " resolved\n".ptr, 10u64); + if (ck.nunresolved > 0) { return 1; }; + } else { if (mode == 99) { // '-c' — codegen / emit asm + let ps: parser; + parserinit(&ps, a, &l); + let f: *node = parsefile(&ps); + let cg: cgen; + cgen_init(&cg, a); + cg_file(&cg, f); + };};};}; + + if (l.errs > 0) { return 1; }; + return 0; +}; + diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww new file mode 100644 index 00000000..5aaa7c22 --- /dev/null +++ b/selfhost/cmd/wwdump/main.ww @@ -0,0 +1,156 @@ +// selfhost/cmd/wwdump/main.ww — ww-side port of cmd/wwdump/main.c. +// +// Reads a .ww file, runs the ww-side lexer, prints tokens through +// the ww-side tokprint. The 990_selfhost test diffs this output +// byte-for-byte against the C-side wwdump on the same file. Any +// divergence is a port bug in lex.ww or tok.ww. +// +// Modes: +// wwdump -t file.ww tokens (default) +// wwdump -a file.ww AST (not yet implemented; reserved) + +use os; +use mem; +use tok; +use lex; +use ast; +use parse; +use typ; +use sym; +use check; +use cgen; +use strconv; + +// ---- argv helpers ----------------------------------------------------- + +// argstrlen — strlen on a NUL-terminated *u8. argv strings are always +// NUL-terminated (kernel-supplied) so this is safe. +fn argstrlen(s: *u8) i32 = { + let n: i32 = 0; + for (s[n] != 0u8) { n += 1; }; + return n; +}; + +fn argstr(p: *u8) str = { + let s: str; + s.ptr = p; + s.len = argstrlen(p); + return s; +}; + +// streq_lit — compare a NUL-terminated argv entry to a string literal. +fn streq_lit(p: *u8, lit: str) bool = { + let i: i32 = 0; + for (i < lit.len) { + if (p[i] != lit[i]) { return false; }; + i += 1; + }; + return p[i] == 0u8; +}; + +// ---- main ------------------------------------------------------------- + +export fn main(argc: i32, argv: **u8) i32 = { + let mode: i32 = 116; // 't' + let path: *u8 = nil; + let i: i32 = 1; + for (i < argc) { + let a: *u8 = argv[i]; + if (streq_lit(a, "-t")) { + mode = 116; + } else { if (streq_lit(a, "-a")) { + mode = 97; // 'a' + } else { if (streq_lit(a, "-r")) { + mode = 114; // 'r' — resolve / name-check + } else { if (streq_lit(a, "-c")) { + mode = 99; // 'c' — codegen / emit asm + } else { if (path == nil) { + path = a; + };};};};}; + i += 1; + }; + if (path == nil) { + os.write(2, "usage: wwdump [-t|-a] file.ww\n".ptr, 30u64); + return 2; + }; + + let fd_or_err: (i32 | str) = os.tryopen(path, os.O_RDONLY, 0i32); + let fd: i32 = -1; + match (fd_or_err) { + case let v: i32 => fd = v; + case let e: str => { + os.write(2, "wwdump: cannot open ".ptr, 20u64); + os.write(2, path, argstrlen(path): u64); + os.write(2, "\n".ptr, 1u64); + return 1; + }; + }; + + let sz: i64 = os.filesize(fd); + if (sz < 0i64) { + os.write(2, "wwdump: filesize failed\n".ptr, 24u64); + os.close(fd); + return 1; + }; + + let a: *arena = newarena(); + let buf: *u8 = amalloc(a, sz: u64): *u8; + let r: i64 = os.readfull(fd, buf, sz: u64); + os.close(fd); + if (r != sz) { + os.write(2, "wwdump: short read\n".ptr, 19u64); + return 1; + }; + + let l: lex; + lexinit(&l, a, argstr(path), buf, sz: u64); + + if (mode == 116) { // '-t' + for (true) { + let t: tok; + lexnext(&l, &t); + tokprint(1i32, &t); + if (t.kind == TK_EOF) { break; }; + if (t.kind == TK_ERR) { break; }; + }; + } else { if (mode == 97) { // '-a' + let ps: parser; + parserinit(&ps, a, &l); + let f: *node = parsefile(&ps); + astprint(1i32, f); + } else { if (mode == 114) { // '-r' — name resolve report + let ps: parser; + parserinit(&ps, a, &l); + let f: *node = parsefile(&ps); + let tc: tctx; + typesinit(&tc, a); + let ck: checker; + check_init(&ck, a, &tc); + // Quiet by default; flip to 1 when debugging missing names. + ck.verbose = 0; + check_file(&ck, f); + // (close out the if-else chain — we'll close all braces below) + // ": / resolved" + os.write(1, argstr(path).ptr, argstrlen(path): u64); + os.write(1, ": ".ptr, 2u64); + let buf: [32]u8; + let n: i32 = strconv.i64toa(buf[0:32], ck.nresolved: i64); + os.write(1, buf.ptr, n: u64); + os.write(1, "/".ptr, 1u64); + let total: i32 = ck.nresolved + ck.nunresolved; + n = strconv.i64toa(buf[0:32], total: i64); + os.write(1, buf.ptr, n: u64); + os.write(1, " resolved\n".ptr, 10u64); + if (ck.nunresolved > 0) { return 1; }; + } else { if (mode == 99) { // '-c' — codegen / emit asm + let ps: parser; + parserinit(&ps, a, &l); + let f: *node = parsefile(&ps); + let cg: cgen; + cgen_init(&cg, a); + cg_file(&cg, f); + };};};}; + + if (l.errs > 0) { return 1; }; + return 0; +}; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww new file mode 100644 index 00000000..fe2a82a6 --- /dev/null +++ b/selfhost/test/smoke.combined.ww @@ -0,0 +1,549 @@ +// os — process and filesystem facade. The body of each call lands +// either in libwwrt.a (rt_syscall trampoline) or libc bindings, +// depending on how the program was linked. + +@symbol("rt_syscall") fn syscall0(num: i64) i64; +@symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; +@symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; +@symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64; + +@symbol("rt_alloc") fn alloc(n: u64) *void; +@symbol("rt_free") fn free(p: *void, n: u64) void; +@symbol("rt_abort") fn abort(msg: str) void; + +// Hare-style runtime check. Caller passes a message that's printed +// to stderr before exit(1). +export fn assert(cond: bool, msg: str) void = { + if (!cond) { abort(msg); }; +}; + +def SYS_READ: i64 = 0; +def SYS_WRITE: i64 = 1; +def SYS_OPEN: i64 = 2; +def SYS_CLOSE: i64 = 3; +def SYS_LSEEK: i64 = 8; +def SYS_ACCESS: i64 = 21; +def SYS_GETPID: i64 = 39; +def SYS_FORK: i64 = 57; +def SYS_EXECVE: i64 = 59; +def SYS_EXIT: i64 = 60; +def SYS_WAIT4: i64 = 61; +def SYS_UNLINK: i64 = 87; + +// open(2) flags. Linux values, matching . +def O_RDONLY: i32 = 0; +def O_WRONLY: i32 = 1; +def O_RDWR: i32 = 2; +def O_CREAT: i32 = 64; // 0x40 +def O_TRUNC: i32 = 512; // 0x200 + +// lseek(2) whence. +def SEEK_SET: i32 = 0; +def SEEK_CUR: i32 = 1; +def SEEK_END: i32 = 2; + +export fn exit(code: i32) void = { + syscall1(SYS_EXIT, code: i64); +}; + +// Raw, non-fallible primitives. These return Linux's int conventions +// (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a +// Hare-style fallible API use the wrappers below. +export fn write(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); +}; + +export fn read(fd: i32, buf: *u8, n: u64) i64 = { + return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); +}; + +export fn close(fd: i32) i32 = { + return syscall1(SYS_CLOSE, fd: i64): i32; +}; + +// Fallible wrappers. The error variant is a plain str (Plan 9 errstr +// model, see lib/errors); the sum type makes success/failure explicit +// without overloading length-zero. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = read(fd, buf, n); + if (r < 0) { return "read failed"; }; + return r; +}; + +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { + let r: i64 = write(fd, buf, n); + if (r < 0) { return "write failed"; }; + return r; +}; + +// open — Linux open(2). Path must be NUL-terminated; callers using ww +// `str` must ensure the bytes are followed by a 0 byte (literals are, +// arena-copied paths usually are by construction). Returns -errno on +// failure, fd otherwise. Higher-level callers prefer `tryopen`. +export fn open(path: *u8, flags: i32, mode: i32) i32 = { + return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; +}; + +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { + let fd: i32 = open(path, flags, mode); + if (fd < 0) { return "open failed"; }; + return fd; +}; + +// lseek — set/inspect the fd's position. Returns the new offset or +// a negative errno. We use this for fstat-free file-size discovery +// (open ⇒ lseek to end ⇒ lseek back). +export fn lseek(fd: i32, off: i64, whence: i32) i64 = { + return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); +}; + +// filesize — convenience: returns the byte length of an open fd by +// seeking to the end and back. -1 on error. +export fn filesize(fd: i32) i64 = { + let end: i64 = lseek(fd, 0i64, SEEK_END); + if (end < 0) { return -1i64; }; + let r: i64 = lseek(fd, 0i64, SEEK_SET); + if (r < 0) { return -1i64; }; + return end; +}; + +// readfull — keep reading until `n` bytes have arrived or the fd +// closes early. Returns bytes read (0..=n) or -1 on read error. +export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { + let got: u64 = 0u64; + for (got < n) { + let r: i64 = read(fd, buf + got, n - got); + if (r < 0) { return -1i64; }; + if (r == 0) { return got: i64; }; // short read: caller decides + got += r: u64; + }; + return got: i64; +}; + +// writefull — keep writing until `n` bytes have been accepted or the +// fd refuses progress. Returns bytes written or -1. +export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { + let sent: u64 = 0u64; + for (sent < n) { + let r: i64 = write(fd, buf + sent, n - sent); + if (r < 0) { return -1i64; }; + if (r == 0) { return sent: i64; }; + sent += r: u64; + }; + return sent: i64; +}; + +// ---- process and filesystem helpers used by the `ww` driver ---------- + +// access(2): returns 0 if the file is reachable, negative errno +// otherwise. mode is the bitset described in (F_OK=0). +export fn access(path: *u8, mode: i32) i32 = { + return syscall2(SYS_ACCESS, path: i64, mode: i64): i32; +}; + +// unlink(2). +export fn unlink(path: *u8) i32 = { + return syscall1(SYS_UNLINK, path: i64): i32; +}; + +// getpid(2). Used by the driver to mint unique scratch paths. +export fn getpid() i32 = { + return syscall0(SYS_GETPID): i32; +}; + +// fork(2): 0 in the child, child pid in the parent, negative errno +// on failure. +export fn fork() i32 = { + return syscall0(SYS_FORK): i32; +}; + +// execve(2): on success, does not return. +export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { + return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32; +}; + +// wait4(2): wait for `pid` (or any child if -1), store status in +// `*status_out`, return the pid that ended (or negative errno). +export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = { + return syscall4(SYS_WAIT4, pid: i64, status_out: i64, + options: i64, rusage: i64): i32; +}; + +// strconv — number↔string conversions. Decimal i64 to/from a fixed +// buffer. Two error idioms ship side by side: +// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the +// tagged-union work; kept for callers that already use it. +// - Hare style (parse64/parseu64): `(value | str)`. The error +// variant carries a short, allocation-free message describing +// why the parse failed. Prefer this for new code. + +// u64toa — write `v` in decimal into `buf` and return the byte count. +// Unsigned-only so callers don't have to think about wraparound when +// printing a u64 that happens to have the high bit set. +export fn u64toa(buf: []u8, v: u64) i32 = { + let tmp: [32]u8; + let i: i32 = 0; + let n: u64 = v; + for (n > 0u64) { + tmp[i] = ((n % 10u64) + 48u64): u8; + n = n / 10u64; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn i64toa(buf: []u8, v: i64) i32 = { + let neg: bool = false; + let n: i64 = v; + if (n < 0) { + neg = true; + n = -n; + }; + let tmp: [32]u8; + let i: i32 = 0; + for (n > 0) { + tmp[i] = ((n % 10) + 48): u8; + n = n / 10; + i += 1; + }; + if (i == 0) { + tmp[0] = 48u8; + i = 1; + }; + let out: i32 = 0; + if (neg) { + buf[out] = 45u8; // '-' + out += 1; + }; + for (i > 0) { + i -= 1; + buf[out] = tmp[i]; + out += 1; + }; + return out; +}; + +export fn atoi64(s: str) (i64, bool) = { + let v: i64 = 0; + let i: i32 = 0; + let neg: bool = false; + if (s.len > 0) { + if (s[0] == 45u8) { neg = true; i = 1; }; + }; + if (i >= s.len) { return 0, false; }; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return 0, false; }; + if (c > 57u8) { return 0, false; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v, true; +}; + +// parse64 — Hare-style fallible signed decimal parser. The value +// variant is i64; the error variant is a short str describing the +// reason. No locale, no whitespace, no underscores: a leading '-' is +// the only non-digit accepted, and only at position 0. +export fn parse64(s: str) (i64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let i: i32 = 0; + let neg: bool = false; + if (s[0] == 45u8) { neg = true; i = 1; }; + if (i >= s.len) { return "parse: lone sign"; }; + let v: i64 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10 + ((c: i64) - 48); + i += 1; + }; + if (neg) { v = -v; }; + return v; +}; + +// parseu64 — fallible unsigned decimal parser. No leading sign. +export fn parseu64(s: str) (u64 | str) = { + if (s.len == 0) { return "parse: empty"; }; + let v: u64 = 0u64; + let i: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c < 48u8) { return "parse: invalid digit"; }; + if (c > 57u8) { return "parse: invalid digit"; }; + v = v * 10u64 + ((c: u64) - 48u64); + i += 1; + }; + return v; +}; + +// ascii — byte-class predicates and case folding for the ASCII range. +// Matches Hare's ascii::isdigit family. Bytes outside 0..127 always +// answer `false`. The lexer hot path uses these inline; they are +// expected to inline to a couple of compares. + +export fn isdigit(c: u8) bool = { + if (c < 48u8) { return false; }; + if (c > 57u8) { return false; }; + return true; +}; + +export fn isupper(c: u8) bool = { + if (c < 65u8) { return false; }; + if (c > 90u8) { return false; }; + return true; +}; + +export fn islower(c: u8) bool = { + if (c < 97u8) { return false; }; + if (c > 122u8) { return false; }; + return true; +}; + +export fn isalpha(c: u8) bool = { + if (isupper(c)) { return true; }; + return islower(c); +}; + +export fn isalnum(c: u8) bool = { + if (isalpha(c)) { return true; }; + return isdigit(c); +}; + +// isspace — the C/Hare set: space, tab, NL, VT, FF, CR. +export fn isspace(c: u8) bool = { + if (c == 32u8) { return true; }; // ' ' + if (c == 9u8) { return true; }; // '\t' + if (c == 10u8) { return true; }; // '\n' + if (c == 11u8) { return true; }; // '\v' + if (c == 12u8) { return true; }; // '\f' + if (c == 13u8) { return true; }; // '\r' + return false; +}; + +export fn ishex(c: u8) bool = { + if (isdigit(c)) { return true; }; + if (c >= 65u8) { + if (c <= 70u8) { return true; }; // 'A'..'F' + }; + if (c >= 97u8) { + if (c <= 102u8) { return true; }; // 'a'..'f' + }; + return false; +}; + +// digitval — value of `c` as a hex/decimal digit, or -1 if not one. +// Useful when scanning numeric literals. +export fn digitval(c: u8) i32 = { + if (isdigit(c)) { return (c - 48u8): i32; }; + if (c >= 65u8) { + if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; + }; + if (c >= 97u8) { + if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; + }; + return -1; +}; + +// isidstart / isidpart — identifier classes used by the lexer. +// Alpha or '_' starts; alnum or '_' continues. +export fn isidstart(c: u8) bool = { + if (isalpha(c)) { return true; }; + if (c == 95u8) { return true; }; // '_' + return false; +}; + +export fn isidpart(c: u8) bool = { + if (isalnum(c)) { return true; }; + if (c == 95u8) { return true; }; + return false; +}; + +// tolower / toupper — fold ASCII case. Non-letters pass through. +export fn tolower(c: u8) u8 = { + if (isupper(c)) { return c + 32u8; }; + return c; +}; + +export fn toupper(c: u8) u8 = { + if (islower(c)) { return c - 32u8; }; + return c; +}; + +// selfhost/test/smoke.ww — end-to-end smoke for the selfhost path. +// +// Exercises the patterns the real ww-side compiler port will use: +// - bump arena allocator (mem.ww shape) +// - error idiom (T | str) +// - struct of fn pointers + ctx pointer (the io.stream-style +// polymorphism we use instead of interfaces) +// - byte-level scanning that mirrors the hot path inside lex.ww +// - strconv round-trip via the real stdlib +// +// `main` returns 42 when every check passes, 1..N on failure +// indicating which probe broke. The 990_selfhost test asserts 42. +// +// Note: only stack-local mutable state. Top-level `let` mutation +// requires a writable .data segment in 6l, which is a separate +// task; until then we exercise polymorphism via ctx pointers, which +// is what the real port wants anyway. + +use os; +use strconv; +use ascii; + +// --- bump arena --------------------------------------------------------- + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, +}; + +// In-place init. Returning a 24-byte struct by value isn't yet +// supported in 6c (SysV requires a hidden return-slot pointer for +// structs >16 bytes), so we initialize through a pointer like the +// real compiler does today. +fn arena_init(a: *arena, buf: *u8, cap: u64) void = { + a.buf = buf; + a.off = 0u64; + a.cap = cap; +}; + +fn arena_alloc(a: *arena, n: u64) *u8 = { + if (n > a.cap - a.off) { return nil; }; + let p: *u8 = a.buf + a.off; + a.off += n; + return p; +}; + +// --- (i32 | str) error idiom ------------------------------------------- + +fn checked_div(num: i32, den: i32) (i32 | str) = { + if (den == 0) { return "div by zero"; }; + return num / den; +}; + +// --- struct-of-fn-pointer polymorphism --------------------------------- +// +// A trivial "writer" abstraction: a function pointer plus a context. +// This mirrors how io.stream / Plan 9 Bio work. The ctx pointer lets +// the implementation own its own state without a global. + +type counter = struct { + n: i32, +}; + +type writer = struct { + ctx: *void, + emit: fn(ctx: *void, b: u8) void, +}; + +fn count_emit(ctx: *void, b: u8) void = { + let c: *counter = ctx: *counter; + c.n += 1; +}; + +// --- byte scanner like lex.ww's hot path ------------------------------- + +fn count_digits(s: str) i32 = { + let i: i32 = 0; + let n: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c >= 48u8) { + if (c <= 57u8) { n += 1; }; + }; + i += 1; + }; + return n; +}; + +// --- entry -------------------------------------------------------------- + +export fn main() i32 = { + // Probe 1 — arena hands out distinct pointers, refuses oversize. + let buf: [256]u8; + let a: arena; + arena_init(&a, buf.ptr, 256u64); + let p1: *u8 = arena_alloc(&a, 32u64); + let p2: *u8 = arena_alloc(&a, 32u64); + if (p1 == nil) { return 1; }; + if (p2 == nil) { return 2; }; + if (p1 == p2) { return 3; }; + let p3: *u8 = arena_alloc(&a, 1024u64); + if (p3 != nil) { return 4; }; + + // Probe 2 — error union both ways. + let r_ok: (i32 | str) = checked_div(84, 2); + let r_bad: (i32 | str) = checked_div(1, 0); + let acc: i32 = 0; + match (r_ok) { + case let v: i32 => acc = v; + case let e: str => return 5; + }; + if (acc != 42) { return 6; }; + match (r_bad) { + case let v: i32 => return 7; + case let e: str => acc = e.len: i32; + }; + if (acc != 11) { return 8; }; // len("div by zero") == 11 + + // Probe 3 — struct-of-fn-pointer dispatch via ctx pointer. + let c: counter = counter { n = 0 }; + let w: writer = writer { ctx = (&c): *void, emit = count_emit }; + w.emit(w.ctx, 65u8); + w.emit(w.ctx, 66u8); + w.emit(w.ctx, 67u8); + if (c.n != 3) { return 9; }; + + // Probe 4 — byte scan over a literal. + let dn: i32 = count_digits("ww123abc"); + if (dn != 3) { return 10; }; + + // Probe 5 — strconv round-trip via the real stdlib. + let outbuf: [32]u8; + let nb: i32 = strconv.i64toa(outbuf[0:32], 4242i64); + if (nb != 4) { return 11; }; + if (outbuf[0] != 52u8) { return 12; }; // '4' + if (outbuf[3] != 50u8) { return 13; }; // '2' + + // Probe 6 — ascii classifications. + if (!ascii.isdigit(53u8)) { return 14; }; // '5' + if (ascii.isdigit(65u8)) { return 15; }; // 'A' is not a digit + if (!ascii.isalpha(122u8)) { return 16; }; // 'z' + if (!ascii.isidstart(95u8)) { return 17; }; // '_' + if (!ascii.isidpart(48u8)) { return 18; }; // '0' is part + if (ascii.digitval(70u8) != 15) { return 19; }; // 'F' = 15 + if (ascii.tolower(65u8) != 97u8) { return 20; }; // 'A' -> 'a' + + // Probe 7 — file open/read via the new os APIs. /proc/self/cmdline + // always exists on Linux, no write side, and is non-empty. + let path: str = "/proc/self/cmdline"; + let fd_or_err: (i32 | str) = os.tryopen(path.ptr, os.O_RDONLY, 0i32); + let fd: i32 = 0; + match (fd_or_err) { + case let v: i32 => fd = v; + case let e: str => return 21; + }; + let rbuf: [128]u8; + let n: i64 = os.readfull(fd, rbuf.ptr, 128u64); + os.close(fd); + if (n <= 0i64) { return 22; }; + + return 42; +}; + diff --git a/selfhost/test/smoke.ww b/selfhost/test/smoke.ww new file mode 100644 index 00000000..a2203bb7 --- /dev/null +++ b/selfhost/test/smoke.ww @@ -0,0 +1,163 @@ +// selfhost/test/smoke.ww — end-to-end smoke for the selfhost path. +// +// Exercises the patterns the real ww-side compiler port will use: +// - bump arena allocator (mem.ww shape) +// - error idiom (T | str) +// - struct of fn pointers + ctx pointer (the io.stream-style +// polymorphism we use instead of interfaces) +// - byte-level scanning that mirrors the hot path inside lex.ww +// - strconv round-trip via the real stdlib +// +// `main` returns 42 when every check passes, 1..N on failure +// indicating which probe broke. The 990_selfhost test asserts 42. +// +// Note: only stack-local mutable state. Top-level `let` mutation +// requires a writable .data segment in 6l, which is a separate +// task; until then we exercise polymorphism via ctx pointers, which +// is what the real port wants anyway. + +use os; +use strconv; +use ascii; + +// --- bump arena --------------------------------------------------------- + +type arena = struct { + buf: *u8, + off: u64, + cap: u64, +}; + +// In-place init. Returning a 24-byte struct by value isn't yet +// supported in 6c (SysV requires a hidden return-slot pointer for +// structs >16 bytes), so we initialize through a pointer like the +// real compiler does today. +fn arena_init(a: *arena, buf: *u8, cap: u64) void = { + a.buf = buf; + a.off = 0u64; + a.cap = cap; +}; + +fn arena_alloc(a: *arena, n: u64) *u8 = { + if (n > a.cap - a.off) { return nil; }; + let p: *u8 = a.buf + a.off; + a.off += n; + return p; +}; + +// --- (i32 | str) error idiom ------------------------------------------- + +fn checked_div(num: i32, den: i32) (i32 | str) = { + if (den == 0) { return "div by zero"; }; + return num / den; +}; + +// --- struct-of-fn-pointer polymorphism --------------------------------- +// +// A trivial "writer" abstraction: a function pointer plus a context. +// This mirrors how io.stream / Plan 9 Bio work. The ctx pointer lets +// the implementation own its own state without a global. + +type counter = struct { + n: i32, +}; + +type writer = struct { + ctx: *void, + emit: fn(ctx: *void, b: u8) void, +}; + +fn count_emit(ctx: *void, b: u8) void = { + let c: *counter = ctx: *counter; + c.n += 1; +}; + +// --- byte scanner like lex.ww's hot path ------------------------------- + +fn count_digits(s: str) i32 = { + let i: i32 = 0; + let n: i32 = 0; + for (i < s.len) { + let c: u8 = s[i]; + if (c >= 48u8) { + if (c <= 57u8) { n += 1; }; + }; + i += 1; + }; + return n; +}; + +// --- entry -------------------------------------------------------------- + +export fn main() i32 = { + // Probe 1 — arena hands out distinct pointers, refuses oversize. + let buf: [256]u8; + let a: arena; + arena_init(&a, buf.ptr, 256u64); + let p1: *u8 = arena_alloc(&a, 32u64); + let p2: *u8 = arena_alloc(&a, 32u64); + if (p1 == nil) { return 1; }; + if (p2 == nil) { return 2; }; + if (p1 == p2) { return 3; }; + let p3: *u8 = arena_alloc(&a, 1024u64); + if (p3 != nil) { return 4; }; + + // Probe 2 — error union both ways. + let r_ok: (i32 | str) = checked_div(84, 2); + let r_bad: (i32 | str) = checked_div(1, 0); + let acc: i32 = 0; + match (r_ok) { + case let v: i32 => acc = v; + case let e: str => return 5; + }; + if (acc != 42) { return 6; }; + match (r_bad) { + case let v: i32 => return 7; + case let e: str => acc = e.len: i32; + }; + if (acc != 11) { return 8; }; // len("div by zero") == 11 + + // Probe 3 — struct-of-fn-pointer dispatch via ctx pointer. + let c: counter = counter { n = 0 }; + let w: writer = writer { ctx = (&c): *void, emit = count_emit }; + w.emit(w.ctx, 65u8); + w.emit(w.ctx, 66u8); + w.emit(w.ctx, 67u8); + if (c.n != 3) { return 9; }; + + // Probe 4 — byte scan over a literal. + let dn: i32 = count_digits("ww123abc"); + if (dn != 3) { return 10; }; + + // Probe 5 — strconv round-trip via the real stdlib. + let outbuf: [32]u8; + let nb: i32 = strconv.i64toa(outbuf[0:32], 4242i64); + if (nb != 4) { return 11; }; + if (outbuf[0] != 52u8) { return 12; }; // '4' + if (outbuf[3] != 50u8) { return 13; }; // '2' + + // Probe 6 — ascii classifications. + if (!ascii.isdigit(53u8)) { return 14; }; // '5' + if (ascii.isdigit(65u8)) { return 15; }; // 'A' is not a digit + if (!ascii.isalpha(122u8)) { return 16; }; // 'z' + if (!ascii.isidstart(95u8)) { return 17; }; // '_' + if (!ascii.isidpart(48u8)) { return 18; }; // '0' is part + if (ascii.digitval(70u8) != 15) { return 19; }; // 'F' = 15 + if (ascii.tolower(65u8) != 97u8) { return 20; }; // 'A' -> 'a' + + // Probe 7 — file open/read via the new os APIs. /proc/self/cmdline + // always exists on Linux, no write side, and is non-empty. + let path: str = "/proc/self/cmdline"; + let fd_or_err: (i32 | str) = os.tryopen(path.ptr, os.O_RDONLY, 0i32); + let fd: i32 = 0; + match (fd_or_err) { + case let v: i32 => fd = v; + case let e: str => return 21; + }; + let rbuf: [128]u8; + let n: i64 = os.readfull(fd, rbuf.ptr, 128u64); + os.close(fd); + if (n <= 0i64) { return 22; }; + + return 42; +}; diff --git a/selfhost/test/sym_link.ww b/selfhost/test/sym_link.ww new file mode 100644 index 00000000..1e18a8c7 --- /dev/null +++ b/selfhost/test/sym_link.ww @@ -0,0 +1,45 @@ +// selfhost/test/sym_link.ww — link-and-run probe for the ww-cgen +// against the sym/typ/ast/mem dep stack. Exercises arena (mem), +// hashtable scope (sym), and pulls in typ/ast as type carriers. +// Returns 42 on success; smaller values name the probe that broke. + +use mem; +use typ; +use ast; +use sym; + +export fn main() i32 = { + let a: *arena = newarena(); + if (a == nil) { return 1; }; + + let s: *scope = newscope(a, nil); + if (s == nil) { return 2; }; + + let n1: str = "foo"; + let r1: *sym = scope_define(s, n1, SK_VAR, nil, nil); + if (r1 == nil) { return 3; }; + + let n2: str = "bar"; + let r2: *sym = scope_define(s, n2, SK_TYPE, nil, nil); + if (r2 == nil) { return 4; }; + + // Duplicate define in same scope must fail. + let r3: *sym = scope_define(s, n1, SK_VAR, nil, nil); + if (r3 != nil) { return 5; }; + + let l1: *sym = scope_lookup(s, n1); + if (l1 == nil) { return 6; }; + if (l1.skind != SK_VAR) { return 7; }; + + let l2: *sym = scope_lookup(s, n2); + if (l2 == nil) { return 8; }; + if (l2.skind != SK_TYPE) { return 9; }; + + // Not-found lookup returns nil. + let n3: str = "baz"; + let l3: *sym = scope_lookup(s, n3); + if (l3 != nil) { return 10; }; + + freearena(a); + return 42; +}; diff --git a/selfhost/test/uses.ww b/selfhost/test/uses.ww new file mode 100644 index 00000000..f9adc3c3 --- /dev/null +++ b/selfhost/test/uses.ww @@ -0,0 +1,29 @@ +// selfhost/test/uses.ww — AST-diff fixture. Grows as parse.ww does. +// Currently exercises: `use IDENT;`, `def NAME: TYPE = LIT;`, +// `type NAME = TYPE;` (alias + struct), top-level `let NAME: TYPE = LIT;`. +// +// Function declarations are still recovered past — the body parser +// is the next major chunk. See selfhost/cmd/wwc/parse.ww header. + +use os; +use mem; +use fmt; + +def MAX_LINE: i32 = 4096; +def NAME: str = "ww"; +def READY: bool = true; + +type byte = u8; +type rune = i32; +type pos = struct { + file: str, + line: i32, + col: i32, +}; +type buffer = [4096]u8; +type bytes = []u8; +type linkptr = *byte; + +let nerrors: i32 = 0; +let nwarnings: i32 = 0; +let prog_name: str = "ww"; diff --git a/test/run b/test/run new file mode 100755 index 00000000..393366a8 --- /dev/null +++ b/test/run @@ -0,0 +1,66 @@ +#!/bin/sh +# test/run — driver for `make test`. Plan 9 rc-flavoured but plain sh. +# +# Walks the test/ tree: +# test/wwc/_.c → out/bin/test_ binary, run it. +# test/lang//*.ww → compile/run via $WW, compare stdout/exit +# with header comments (// expected: ...). +# Exit non-zero on first failure. + +set -e + +BIN=${BIN:-out/bin} +WW=${WW:-$BIN/ww} + +fail=0 +ran=0 + +# ---- C-side unit tests -------------------------------------------------- +for t in test/wwc/*.c; do + [ -f "$t" ] || continue + name=${t##*/} + name=${name%.c} + # strip leading _ + short=${name#[0-9][0-9][0-9]_} + bin=$BIN/test_$short + if [ ! -x "$bin" ]; then + echo "SKIP $name (no binary $bin)" + continue + fi + if "$bin" >"$BIN/.$short.out" 2>"$BIN/.$short.err"; then + echo "ok $name" + else + rc=$? + echo "FAIL $name (rc=$rc)" + echo "--- stdout ---" + cat "$BIN/.$short.out" + echo "--- stderr ---" + cat "$BIN/.$short.err" + fail=$((fail + 1)) + fi + ran=$((ran + 1)) +done + +# ---- ww source-level tests ---------------------------------------------- +# Each test/lang//*.ww has a header: +# // expected-exit: 0 +# // expected-stdout: hello world +# These run with `ww run`; phases that don't compile yet skip entries +# that begin with `// phase: ` if N > current. +if [ -d test/lang ]; then + for f in test/lang/*/*.ww; do + [ -f "$f" ] || continue + # Skip until ww run exists; placeholder for phase 7+ + echo "skip $f (ww run not online yet)" + done +fi + +if [ $ran -eq 0 ]; then + echo "no tests were run" + exit 1 +fi +if [ $fail -gt 0 ]; then + echo "$fail test(s) failed" + exit 1 +fi +echo "all $ran tests passed" diff --git a/test/wwc/000_smoke.c b/test/wwc/000_smoke.c new file mode 100644 index 00000000..c64f37f7 --- /dev/null +++ b/test/wwc/000_smoke.c @@ -0,0 +1,87 @@ +/* + * 000_smoke — Phase 0 smoke test. + * + * Asserts: + * - libwwc symbols (newarena/amalloc/freearena, fatal/errorf/warnf) + * are linkable. + * - `ww -V` exits 0 and prints the configured version. + */ +#include "ww.h" +#include +#include + +static void +test_arena(void) +{ + Arena *a = newarena(); + if (a == NULL) { + fprintf(stderr, "smoke: newarena returned NULL\n"); + exit(1); + } + int *p = amalloc(a, sizeof *p); + *p = 42; + if (*p != 42) { + fprintf(stderr, "smoke: arena alloc bad\n"); + exit(1); + } + /* force several growths */ + for (int i = 0; i < 10000; i++) { + char *s = aprintf(a, "hello-%d", i); + if (s == NULL || strncmp(s, "hello-", 6) != 0) { + fprintf(stderr, "smoke: aprintf bad\n"); + exit(1); + } + } + freearena(a); +} + +static void +test_version(void) +{ + if (WW_VERSION == NULL || WW_VERSION[0] == '\0') { + fprintf(stderr, "smoke: empty WW_VERSION\n"); + exit(1); + } +} + +static void +test_ww_minus_v(void) +{ + const char *bin = getenv("BIN"); + if (bin == NULL) + bin = "out/bin"; + char cmd[512]; + snprintf(cmd, sizeof cmd, "%s/ww -V", bin); + FILE *p = popen(cmd, "r"); + if (p == NULL) { + fprintf(stderr, "smoke: popen %s\n", cmd); + exit(1); + } + char buf[128]; + size_t n = fread(buf, 1, sizeof buf - 1, p); + buf[n] = '\0'; + int rc = pclose(p); + if (rc != 0) { + fprintf(stderr, "smoke: ww -V exit %d\n", rc); + exit(1); + } + if (strstr(buf, "ww ") != buf) { + fprintf(stderr, "smoke: ww -V output not 'ww ...': %s\n", buf); + exit(1); + } + if (strstr(buf, WW_VERSION) == NULL) { + fprintf(stderr, "smoke: ww -V missing version %s in: %s\n", + WW_VERSION, buf); + exit(1); + } +} + +int +main(void) +{ + test_arena(); + test_version(); + test_ww_minus_v(); + puts("smoke: ok"); + return 0; +} diff --git a/test/wwc/100_lex.c b/test/wwc/100_lex.c new file mode 100644 index 00000000..72f916aa --- /dev/null +++ b/test/wwc/100_lex.c @@ -0,0 +1,142 @@ +/* + * 100_lex — table-driven lexer tests. + * + * Each row is a (src, expected) pair. The expected string is the + * concatenation of token names, space-separated. For literals we + * also encode the value: e.g. INT(42), STR("hi"), IDENT(foo). + * + * EOF is implicit: the harness checks that lexnext returns TK_EOF + * after the last expected token. + */ +#include "ww.h" +#include +#include +#include + +static char * +toklit(Arena *a, Tok t) +{ + switch (t.kind) { + case TK_IDENT: return aprintf(a, "IDENT(%s)", t.text); + case TK_INT: return aprintf(a, "INT(%llu)", (unsigned long long)t.v.uval); + case TK_FLOAT: return aprintf(a, "FLOAT(%g)", t.v.fval); + case TK_RUNE: return aprintf(a, "RUNE(%llu)", (unsigned long long)t.v.uval); + case TK_STR: return aprintf(a, "STR(%s)", t.text); + case TK_ERR: return aprintf(a, "ERR(%s)", t.text); + default: return (char *)tokname(t.kind); + } +} + +static int +runrow(const char *src, const char *expect) +{ + Arena *a = newarena(); + Lex l; + lexinit(&l, a, "", src, strlen(src)); + + char *got = amalloc(a, 1); + got[0] = '\0'; + u64 cap = 1, n = 0; + + for (;;) { + Tok t = lexnext(&l); + if (t.kind == TK_EOF) + break; + const char *piece = toklit(a, t); + u64 plen = strlen(piece); + u64 need = n + plen + 2; + if (need >= cap) { + u64 nc = need * 2; + char *nb = amalloc(a, nc); + memcpy(nb, got, n); + got = nb; + cap = nc; + } + if (n) got[n++] = ' '; + memcpy(got + n, piece, plen); + n += plen; + got[n] = '\0'; + } + + int ok = strcmp(got, expect) == 0; + if (!ok) { + fprintf(stderr, "lex mismatch:\n src: %s\n" + " want: %s\n got: %s\n", src, expect, got); + } + freearena(a); + return ok; +} + +struct row { const char *src, *expect; }; + +static const struct row rows[] = { + { "", "" }, + { " \t\n ", "" }, + { "// comment\n", "" }, + { "/* a /b/ c */", "" }, + + /* identifiers + keywords */ + { "foo", "IDENT(foo)" }, + { "fn", "fn" }, + { "fn main", "fn IDENT(main)" }, + { "let x: i32 = 0;", "let IDENT(x) : IDENT(i32) = INT(0) ;" }, + { "export fn", "export fn" }, + { "if else for switch case return use type struct defer break continue proc chan nil true false", + "if else for switch case return use type struct defer break continue proc chan nil true false" }, + + /* numbers */ + { "0", "INT(0)" }, + { "42", "INT(42)" }, + { "1_000_000", "INT(1000000)" }, + { "0xff", "INT(255)" }, + { "0xDE_AD_BE_EF", "INT(3735928559)" }, + { "0b1010", "INT(10)" }, + { "0o777", "INT(511)" }, + { "3.14", "FLOAT(3.14)" }, + { "1.5e3", "FLOAT(1500)" }, + + /* strings & runes */ + { "\"hello\"", "STR(hello)" }, + { "\"a\\nb\"", "STR(a\nb)" }, + { "'A'", "RUNE(65)" }, + { "'\\n'", "RUNE(10)" }, + { "'\\x7f'", "RUNE(127)" }, + + /* operators & punct */ + { "+ - * / % == != < > <= >= && || !", + "+ - * / % == != < > <= >= && || !" }, + { "= += -= *= /= %= &= |= ^= <<= >>=", + "= += -= *= /= %= &= |= ^= <<= >>=" }, + { "<< >> & | ^ ~ ?", + "<< >> & | ^ ~ ?" }, + { "( ) { } [ ] , ; : . ... @", + "( ) { } [ ] , ; : . ... @" }, + { "<- ->", + "<- ->" }, + { "@symbol(\"malloc\")", + "@ IDENT(symbol) ( STR(malloc) )" }, + + /* mixed */ + { "fn add(a: i32, b: i32) i32 = { return a + b; };", + "fn IDENT(add) ( IDENT(a) : IDENT(i32) , IDENT(b) : IDENT(i32) ) IDENT(i32) = { return IDENT(a) + IDENT(b) ; } ;" }, +}; + +int +main(void) +{ + int fail = 0; + for (size_t i = 0; i < sizeof rows / sizeof rows[0]; i++) { + if (!runrow(rows[i].src, rows[i].expect)) { + fprintf(stderr, "row %zu failed\n", i); + fail++; + } + } + if (fail) { + fprintf(stderr, "%d/%zu lex tests failed\n", fail, + sizeof rows / sizeof rows[0]); + return 1; + } + printf("lex: %zu/%zu ok\n", sizeof rows / sizeof rows[0], + sizeof rows / sizeof rows[0]); + return 0; +} diff --git a/test/wwc/200_parse.c b/test/wwc/200_parse.c new file mode 100644 index 00000000..fa81b5df --- /dev/null +++ b/test/wwc/200_parse.c @@ -0,0 +1,190 @@ +/* + * 200_parse — parser tests. + * + * Two flavours: + * 1) "must parse": source must produce a Node and no errors. + * 2) "shape match": parsed AST printed via astprint, compared against + * expected substring (so tests stay readable without binding to + * every position field). + */ +#include "ww.h" +#include +#include +#include + +static int +must_parse(const char *src) +{ + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, "", src, strlen(src)); + parserinit(&p, a, &l); + Node *n = parsefile(&p); + int ok = (n != NULL && p.errs == 0 && l.errs == 0); + freearena(a); + return ok; +} + +static char * +parse_to_str(const char *src, int *errs) +{ + Arena *a = newarena(); + Lex l; + Parser p; + lexinit(&l, a, "", src, strlen(src)); + parserinit(&p, a, &l); + Node *n = parsefile(&p); + *errs = p.errs + l.errs; + + char *buf = NULL; + size_t len = 0; + FILE *f = open_memstream(&buf, &len); + if (f == NULL) { + freearena(a); + return NULL; + } + astprint(f, n); + fclose(f); + + char *out = malloc(len + 1); + memcpy(out, buf, len); + out[len] = '\0'; + free(buf); + freearena(a); + return out; +} + +static int +must_contain(const char *src, const char *needle) +{ + int errs; + char *got = parse_to_str(src, &errs); + if (got == NULL || errs > 0) { + fprintf(stderr, "parse errs=%d:\n%s\n", errs, src); + free(got); + return 0; + } + if (strstr(got, needle) == NULL) { + fprintf(stderr, "shape mismatch:\n src: %s\n" + " want: %s\n got:\n%s\n", src, needle, got); + free(got); + return 0; + } + free(got); + return 1; +} + +int +main(void) +{ + int fail = 0; + + const char *parses[] = { + "use io;", + "use io.bufio;", + "def MAX: i32 = 4096;", + "export def MAX: i32 = 4096;", + "type point = struct { x: i32, y: i32 };", + "type stream = fn(b: []u8) i32;", + "type chans = chan i32;", + "type box = struct { p: *point, n: i32, items: []i32 };", + "type arr = [16]u8;", + + "fn nop() void = {};", + "export fn id(x: i32) i32 = { return x; };", + "fn add(a: i32, b: i32) i32 = { return a + b; };", + "@symbol(\"malloc\") fn cmalloc(n: u64) *void;", + "fn varadic(a: i32, ...) void;", + + "fn f() void = { let x: i32 = 0; let y = 1.5; let s: str = \"hi\"; };", + "fn f() void = { if (x > 0) { return; } else { x += 1; }; };", + "fn f() void = { for (let i: i32 = 0; i < 10; i += 1) { x += i; }; };", + "fn f() void = { for (i < 10) { i += 1; }; };", + "fn f() void = { for () { i += 1; }; };", + "fn f() void = { defer free(p); };", + "fn f() void = { switch (x) { case 1, 2: y = 1; case: y = 0; }; };", + + "fn ptr(p: *point) i32 = { return p.x; };", + "fn cast() void = { let x = (5 + 1): i64; };", + "fn deref(p: *i32) i32 = { return *p; };", + "fn addr(x: i32) *i32 = { return &x; };", + "fn lit() void = { let p: point = point { x = 1, y = 2 }; };", + "fn pkg() void = { fmt.println(1); };", + "fn arr() void = { let a = [1, 2, 3]; };", + "fn idx() i32 = { return a[3]; };", + "fn neg() i32 = { return -a + ~b * !c; };", + + "fn ops() void = { x = 1; x += 1; x -= 1; x *= 2; x /= 2; x %= 2; " + "x &= 1; x |= 1; x ^= 1; x <<= 1; x >>= 1; };", + "fn cmp() bool = { return a == b && c != d || e < f && g <= h; };", + "fn bits() i32 = { return (a & b) | (c ^ d); };", + "fn shift() i32 = { return a << 2 | b >> 1; };", + + "fn nested() void = { if (a > 0) { if (b > 0) { c = 1; }; }; };", + "fn many(a: i32, b: i32, c: i32, d: i32, e: i32) i32 = " + "{ return a + b * c - d / e; };", + "fn slc(s: []u8) []u8 = { return s; };", + "fn ssn(p: **point) i32 = { return (*p).x; };", + "fn arrptr(p: *[16]u8) u8 = { return p[0]; };", + "fn fnt(f: fn(i32) i32, x: i32) i32 = { return f(x); };", + "fn anontype() void = { let f: fn(i32) i32 = id; };", + + /* multiple decls */ + "use io;\nuse fmt;\ndef N: i32 = 8;\ntype p = struct{x:i32};\nfn f() void = {};", + /* attribute on FFI decl */ + "@symbol(\"strlen\") fn cstrlen(s: *u8) u64;", + /* trailing comma */ + "fn f(a: i32, b: i32,) void = {};", + /* nil/true/false */ + "fn f() void = { let p: *i32 = nil; let x: bool = true; let y: bool = false; };", + /* chan */ + "fn ch() void = { let c: chan i32; let v = <-c; };", + /* nested struct lit */ + "fn lit2() void = { let q = box { p = nil, n = 0, items = [1, 2] }; };", + /* defer with call */ + "fn f() void = { defer close(fd); return; };", + /* break / continue */ + "fn f() void = { for () { if (x) { break; }; continue; }; };", + /* deeply nested expr */ + "fn deep() i32 = { return ((((((1 + 2) * 3) - 4) / 5) % 6) << 7); };", + /* index chain */ + "fn ix() i32 = { return a[b][c[d]]; };", + /* dot chain */ + "fn dot() i32 = { return a.b.c.d; };", + /* call chain */ + "fn cc() i32 = { return f()()(); };", + /* mixed postfix */ + "fn mx() i32 = { return obj.method(arg)[idx].field; };", + + /* multi-return tuples */ + "fn divmod(a: i64, b: i64) (i64, i64) = { return a / b, a % b; };", + "fn try() (i32, str) = { return 0, \"\"; };", + "fn use_tuple() void = { let q, r = divmod(10, 3); };", + "fn assign_tuple() void = { q, r = divmod(10, 3); };", + }; + int n = sizeof parses / sizeof parses[0]; + for (int i = 0; i < n; i++) { + if (!must_parse(parses[i])) { + fprintf(stderr, "parse fail [%d]: %s\n", i, parses[i]); + fail++; + } + } + + if (!must_contain("use io;", "(use \"io\"")) fail++; + if (!must_contain("def N: i32 = 4;", "(def \"N\"")) fail++; + if (!must_contain("def N: i32 = 4;", "(int 4")) fail++; + if (!must_contain("export fn f() void = {};", "(fn \"f\" export")) fail++; + if (!must_contain("type p = struct { x: i32 };", "(typedecl \"p\"")) fail++; + if (!must_contain("fn f() i32 = { return 1; };", "(return")) fail++; + if (!must_contain("fn f() void = { x = 1; };", "(assign =")) fail++; + if (!must_contain("fn f() i32 = { return a + b; };", "(bin +")) fail++; + if (!must_contain("@symbol(\"x\") fn f() void;", "(attr \"symbol\""))fail++; + + if (fail) { + fprintf(stderr, "%d parse tests failed\n", fail); + return 1; + } + printf("parse: %d/%d ok + 9 shape ok\n", n, n); + return 0; +} diff --git a/test/wwc/300_check.c b/test/wwc/300_check.c new file mode 100644 index 00000000..c3f09427 --- /dev/null +++ b/test/wwc/300_check.c @@ -0,0 +1,126 @@ +/* + * 300_check — type-checker tests. + * + * Each row is (src, expect) where expect is "ok" (no errors) or a + * substring that must appear in the captured stderr. + */ +#include "ww.h" +#include +#include +#include + +static int +runrow(const char *src, const char *expect) +{ + Arena *a = newarena(); + Lex l; + Parser p; + Checker c; + lexinit(&l, a, "", src, strlen(src)); + parserinit(&p, a, &l); + Node *file = parsefile(&p); + + char *errbuf = NULL; + size_t errlen = 0; + FILE *prev = errout; + errout = open_memstream(&errbuf, &errlen); + + check_init(&c, a); + check_file(&c, file); + + fclose(errout); + errout = prev; + + int ok = 0; + if (strcmp(expect, "ok") == 0) { + ok = (c.errs == 0 && p.errs == 0 && l.errs == 0); + if (!ok) + fprintf(stderr, "expected ok but got %d errors:\n%s" + " src: %s\n", c.errs + p.errs + l.errs, errbuf, src); + } else { + ok = (errbuf && strstr(errbuf, expect) != NULL); + if (!ok) + fprintf(stderr, "expected substring '%s' in errs:\n%s" + " src: %s\n", expect, errbuf, src); + } + free(errbuf); + freearena(a); + return ok; +} + +struct row { const char *src, *expect; }; + +static const struct row rows[] = { + /* OK cases */ + { "fn main() void = {};", "ok" }, + { "fn id(x: i32) i32 = { return x; };", "ok" }, + { "fn add(a: i32, b: i32) i32 = { return a + b; };", "ok" }, + { "def MAX: i32 = 4096;", "ok" }, + { "type point = struct { x: i32, y: i32 };", "ok" }, + { "type point = struct { x: i32, y: i32 };\n" + "fn move(p: *point, dx: i32) void = { p.x += dx; };", "ok" }, + { "fn nums() void = { let x: i32 = 1; let y: i64 = 2; };", "ok" }, + { "fn cond(x: i32) i32 = { if (x > 0) { return 1; }; return 0; };", "ok" }, + { "fn lo() void = { for (let i: i32 = 0; i < 10; i += 1) { }; };", "ok" }, + { "fn ptr(p: *i32) i32 = { return *p; };", "ok" }, + { "fn addr(x: i32) *i32 = { return &x; };", "ok" }, + { "fn cmp(a: i32, b: i32) bool = { return a == b; };", "ok" }, + { "fn b() bool = { return true && false || !true; };", "ok" }, + { "fn cast() void = { let x = (5 + 1): i64; };", "ok" }, + { "fn slc(s: []u8) []u8 = { return s; };", "ok" }, + { "fn arr() void = { let a: [16]u8; };", "ok" }, + { "fn lit() void = { let p = point { x = 1, y = 2 }; };\n" + "type point = struct { x: i32, y: i32 };", "ok" }, + { "fn idx(a: []i32) i32 = { return a[0]; };", "ok" }, + { "fn nilptr(p: *i32) bool = { return p == nil; };", "ok" }, + { "fn untyped() void = { let x: i64 = 42; };", "ok" }, + { "fn fld(p: *point) i32 = { return p.x; };\n" + "type point = struct { x: i32 };", "ok" }, + { "fn loops() void = { for () { break; }; for () { continue; }; };", "ok" }, + + /* multi-return tuples */ + { "fn divmod(a: i64, b: i64) (i64, i64) = { return a / b, a % b; };", "ok" }, + { "fn dm(a: i64, b: i64) (i64, i64) = { return a, b; };\n" + "fn caller() i64 = { let q, r = dm(10, 3); return q + r; };", "ok" }, + { "fn dm() (i64, i64) = { return 1, 2; };\n" + "fn ass() void = { let q: i64 = 0; let r: i64 = 0; q, r = dm(); };", "ok" }, + + /* error cases */ + { "fn f() void = { return 1; };", "return value in void" }, + { "fn f() i32 = { return; };", "not assignable" }, + { "fn f() void = { x = 1; };", "undefined" }, + { "fn f() void = { let x: nope = 1; };", "unknown type" }, + { "fn f() void = { let x: bool = 1; };", "not assignable" }, + { "fn f() i32 = { return \"hi\"; };", "not assignable" }, + { "fn f() void = { 1 + true; };", "non-numeric" }, + { "fn f() void = { -true; };", "non-numeric" }, + { "fn f() void = { *5; };", "deref non-pointer" }, + { "fn f() void = { let x: i32 = 1; let x: i32 = 2; };", + "ok" }, /* shadowing in inner scope; same scope flagged */ + { "fn f() void = { break; };", "break outside loop" }, + { "fn f() void = { continue; };", "continue outside loop" }, + { "fn f(x: i32) void = { x[0]; };", "indexing non-indexable" }, + { "fn f() i32 = { return 1; }; fn g() i32 = { return f(1); };", + "too many arguments" }, + { "fn f(a: i32) i32 = { return a; }; fn g() i32 = { return f(); };", + "not enough arguments" }, + { "fn f() void = { if (1) { }; };", "if condition" }, +}; + +int +main(void) +{ + int n = sizeof rows / sizeof rows[0]; + int fail = 0; + for (int i = 0; i < n; i++) + if (!runrow(rows[i].src, rows[i].expect)) { + fprintf(stderr, "row %d failed\n", i); + fail++; + } + if (fail) { + fprintf(stderr, "%d/%d check tests failed\n", fail, n); + return 1; + } + printf("check: %d/%d ok\n", n, n); + return 0; +} diff --git a/test/wwc/400_6c.c b/test/wwc/400_6c.c new file mode 100644 index 00000000..d281ce9b --- /dev/null +++ b/test/wwc/400_6c.c @@ -0,0 +1,90 @@ +/* + * 400_6c — codegen smoke tests. Each row supplies a tiny ww source + * and a list of substrings expected in the emitted .s. We don't + * golden-match the whole file (too brittle); we just verify that + * key opcodes and operand shapes show up. + */ +#include +#include +#include +#include + +static int +run6c(const char *src, char **out) +{ + char path[64]; + snprintf(path, sizeof path, "/tmp/wwt_%d.ww", getpid()); + FILE *f = fopen(path, "wb"); + if (f == NULL) return -1; + fputs(src, f); + fclose(f); + + char cmd[256]; + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + snprintf(cmd, sizeof cmd, "%s/6c %s 2>&1", bin, path); + FILE *p = popen(cmd, "r"); + if (p == NULL) { unlink(path); return -1; } + size_t cap = 4096, n = 0; + char *buf = malloc(cap); + int c; + while ((c = fgetc(p)) != EOF) { + if (n + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); } + buf[n++] = (char)c; + } + buf[n] = '\0'; + int rc = pclose(p); + unlink(path); + *out = buf; + return rc; +} + +static int +contains(const char *hay, const char *needle) +{ + return strstr(hay, needle) != NULL; +} + +struct row { const char *src; const char *needle; }; + +static const struct row rows[] = { + { "fn main() i32 = { return 42; };", "MOVQ\t$42, AX" }, + { "fn main() i32 = { return 42; };", "RET" }, + { "fn id(x: i32) i32 = { return x; };","MOVQ\tDI, -8(BP)" }, + { "fn add(a: i32, b: i32) i32 = { return a + b; };", "ADDQ\tBX, AX" }, + { "fn sub(a: i32, b: i32) i32 = { return a - b; };", "SUBQ\tBX, AX" }, + { "fn mul(a: i32, b: i32) i32 = { return a * b; };", "IMULQ\tBX, AX" }, + { "fn neg(x: i32) i32 = { return -x; };", "NEGQ\tAX" }, + { "fn cmp(a: i32, b: i32) bool = { return a == b; };", "CMPQ\tBX, AX" }, + { "fn cmp(a: i32, b: i32) bool = { return a == b; };", "JE" }, + { "fn cond(x: i32) i32 = { if (x > 0) { return 1; }; return 0; };", + "CMPQ\t$0, AX" }, + { "fn lo() void = { for (let i: i32 = 0; i < 10; i += 1) { }; };", "JMP" }, + { "fn callit() i32 = { return 1; };", "TEXT callit,$0" }, +}; + +int +main(void) +{ + int n = sizeof rows / sizeof rows[0]; + int fail = 0; + for (int i = 0; i < n; i++) { + char *out = NULL; + int rc = run6c(rows[i].src, &out); + if (rc != 0) { + fprintf(stderr, "row %d: 6c rc=%d\n", i, rc); + fail++; + free(out); + continue; + } + if (!contains(out, rows[i].needle)) { + fprintf(stderr, "row %d: missing '%s' in:\n%s\n", + i, rows[i].needle, out); + fail++; + } + free(out); + } + if (fail) { fprintf(stderr, "%d/%d 6c tests failed\n", fail, n); return 1; } + printf("6c: %d/%d ok\n", n, n); + return 0; +} diff --git a/test/wwc/500_6a.c b/test/wwc/500_6a.c new file mode 100644 index 00000000..4a259301 --- /dev/null +++ b/test/wwc/500_6a.c @@ -0,0 +1,62 @@ +/* + * 500_asm — assembler smoke. Drive 6c on a tiny program, feed the + * output to 6a, then read back the .o magic to confirm we produced + * a valid ELF64 relocatable object. + */ +#include +#include +#include +#include + +static int +run(const char *cmd) +{ + return system(cmd); +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + + char src[64], asmf[64], obj[64]; + snprintf(src, sizeof src, "/tmp/wwt_%d.ww", getpid()); + snprintf(asmf, sizeof asmf, "/tmp/wwt_%d.s", getpid()); + snprintf(obj, sizeof obj, "/tmp/wwt_%d.o", getpid()); + + const char *programs[] = { + "fn main() i32 = { return 42; };", + "fn add(a: i32, b: i32) i32 = { return a + b; };", + "fn loop() i32 = { let i: i32 = 0; for (i < 10) { i += 1; }; return i; };", + "fn cmp(a: i32, b: i32) bool = { return a < b; };", + NULL + }; + int n = 0, fail = 0; + for (int i = 0; programs[i]; i++, n++) { + FILE *f = fopen(src, "wb"); + fputs(programs[i], f); + fclose(f); + + char cmd[512]; + snprintf(cmd, sizeof cmd, "%s/6c -o %s %s", bin, asmf, src); + if (run(cmd) != 0) { fprintf(stderr, "6c fail: %s\n", programs[i]); fail++; continue; } + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s", bin, obj, asmf); + if (run(cmd) != 0) { fprintf(stderr, "6a fail: %s\n", programs[i]); fail++; continue; } + + FILE *of = fopen(obj, "rb"); + if (of == NULL) { fail++; continue; } + unsigned char hdr[16]; + size_t r = fread(hdr, 1, sizeof hdr, of); + fclose(of); + if (r != 16 || memcmp(hdr, "\x7f""ELF", 4) != 0 + || hdr[4] != 2 /* ELFCLASS64 */) { + fprintf(stderr, "not an ELF64: %s\n", programs[i]); + fail++; + } + } + unlink(src); unlink(asmf); unlink(obj); + if (fail) { fprintf(stderr, "%d/%d 6a tests failed\n", fail, n); return 1; } + printf("6a: %d/%d ok\n", n, n); + return 0; +} diff --git a/test/wwc/600_6l.c b/test/wwc/600_6l.c new file mode 100644 index 00000000..ef90abca --- /dev/null +++ b/test/wwc/600_6l.c @@ -0,0 +1,67 @@ +/* + * 600_6l — linker smoke. Drive 6c → 6a → 6l on a tiny program, + * confirm the result is a static ELF executable with no PT_INTERP. + */ +#include +#include +#include +#include + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + + char src[64], asmf[64], obj[64], exe[64]; + snprintf(src, sizeof src, "/tmp/wwt_%d.ww", getpid()); + snprintf(asmf, sizeof asmf, "/tmp/wwt_%d.s", getpid()); + snprintf(obj, sizeof obj, "/tmp/wwt_%d.o", getpid()); + snprintf(exe, sizeof exe, "/tmp/wwt_%d.x", getpid()); + + FILE *f = fopen(src, "wb"); + fputs("fn main() i32 = { return 42; };", f); + fclose(f); + + char cmd[1024]; + snprintf(cmd, sizeof cmd, "%s/6c -o %s %s", bin, asmf, src); + if (system(cmd) != 0) { fprintf(stderr, "6c failed\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s", bin, obj, asmf); + if (system(cmd) != 0) { fprintf(stderr, "6a failed\n"); return 1; } + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s", bin, exe, obj); + if (system(cmd) != 0) { fprintf(stderr, "6l failed\n"); return 1; } + + /* validate ELF magic + e_type=EXEC */ + FILE *of = fopen(exe, "rb"); + if (of == NULL) { fprintf(stderr, "exe missing\n"); return 1; } + unsigned char hdr[20]; + if (fread(hdr, 1, sizeof hdr, of) != sizeof hdr) { fprintf(stderr, "short exe\n"); fclose(of); return 1; } + fclose(of); + + if (memcmp(hdr, "\x7f""ELF", 4) != 0 || hdr[4] != 2) { + fprintf(stderr, "not an ELF64\n"); return 1; + } + /* e_type at offset 16, little-endian u16; ET_EXEC = 2 */ + unsigned short etype = (unsigned short)hdr[16] | ((unsigned short)hdr[17] << 8); + if (etype != 2) { + fprintf(stderr, "not ET_EXEC, got %u\n", etype); return 1; + } + + /* ldd "not a dynamic executable" — proxy: check that file is not + * dynamic by looking for PT_INTERP. We have none, so ldd reports + * "not a dynamic executable" or similar. */ + snprintf(cmd, sizeof cmd, "ldd %s 2>&1", exe); + FILE *p = popen(cmd, "r"); + char buf[256] = {0}; + fread(buf, 1, sizeof buf - 1, p); + pclose(p); + if (strstr(buf, "not a dynamic executable") == NULL + && strstr(buf, "statically linked") == NULL) { + fprintf(stderr, "ldd says not static: %s\n", buf); + return 1; + } + + unlink(src); unlink(asmf); unlink(obj); unlink(exe); + printf("6l: ok\n"); + return 0; +} diff --git a/test/wwc/610_arch.c b/test/wwc/610_arch.c new file mode 100644 index 00000000..aad64d3b --- /dev/null +++ b/test/wwc/610_arch.c @@ -0,0 +1,96 @@ +/* + * 610_arch — archive selectivity. Build two .o files into an archive + * where one defines a symbol main needs, and the other references an + * undefined external. Linking should pull only the needed member; + * the other one's bad reference must NOT cause a link error. + * + * If 6l were still pulling all members, this test would fail with + * "undefined reference to 'this_symbol_does_not_exist'". + */ +#include +#include +#include +#include +#include +#include + +static int run(const char *cmd) { return system(cmd); } + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char dir[64]; + snprintf(dir, sizeof dir, "/tmp/wwarch_%d", getpid()); + mkdir(dir, 0755); + + /* lib_good.ww — defines f1() */ + char path[256]; + snprintf(path, sizeof path, "%s/lib_good.ww", dir); + FILE *f = fopen(path, "wb"); + fputs("export fn f1() i32 = { return 7; };", f); + fclose(f); + + /* lib_bad.ww — defines f2() but also references an undefined extern */ + snprintf(path, sizeof path, "%s/lib_bad.ww", dir); + f = fopen(path, "wb"); + fputs("@symbol(\"this_symbol_does_not_exist\") fn bogus() i32;\n" + "export fn f2() i32 = { return bogus(); };", f); + fclose(f); + + /* main.ww — calls f1, NOT f2 */ + snprintf(path, sizeof path, "%s/m.ww", dir); + f = fopen(path, "wb"); + fputs("@symbol(\"f1\") fn f1() i32;\n" + "fn main() i32 = { return f1(); };", f); + fclose(f); + + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "set -e; cd %s && %s/6c -o lib_good.s lib_good.ww && " + "%s/6c -o lib_bad.s lib_bad.ww && " + "%s/6a -o lib_good.o lib_good.s && %s/6a -o lib_bad.o lib_bad.s && " + "ar rcs libfoo.a lib_good.o lib_bad.o && " + "%s/6c -o m.s m.ww && %s/6a -o m.o m.s", + dir, bin, bin, bin, bin, bin, bin); + if (run(cmd) != 0) { fprintf(stderr, "build failed\n"); return 1; } + + /* Find start.o for the runtime */ + char startobj[256]; + snprintf(startobj, sizeof startobj, "%s/../obj/rt/start.o", bin); + + snprintf(cmd, sizeof cmd, + "%s/6l -o %s/m %s/m.o %s %s/libfoo.a 2>%s/link.err", + bin, dir, dir, startobj, dir, dir); + if (run(cmd) != 0) { + FILE *ef = fopen("/dev/null", "r"); + (void)ef; + char errpath[300]; + snprintf(errpath, sizeof errpath, "%s/link.err", dir); + FILE *e = fopen(errpath, "r"); + if (e) { + char buf[512]; size_t r = fread(buf, 1, sizeof buf - 1, e); buf[r]='\0'; fclose(e); + fprintf(stderr, "link failed:\n%s\n", buf); + } + return 1; + } + + char exepath[256]; + snprintf(exepath, sizeof exepath, "%s/m", dir); + int rc = run(exepath); + if (WIFEXITED(rc) && WEXITSTATUS(rc) == 7) { + printf("arch: ok (only needed archive member pulled)\n"); + return 0; + } + fprintf(stderr, "arch: unexpected exit\n"); + return 1; +} diff --git a/test/wwc/700_e2e.c b/test/wwc/700_e2e.c new file mode 100644 index 00000000..eb3808f6 --- /dev/null +++ b/test/wwc/700_e2e.c @@ -0,0 +1,863 @@ +/* + * 700_e2e — end-to-end. Drive `ww build` on a small source program, + * run the produced binary, check the exit status. This is the real + * user-facing happy path. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +struct row { const char *src; int want_exit; }; + +static const struct row rows[] = { + { "fn main() i32 = { return 42; };", 42 }, + { "fn add(a: i32, b: i32) i32 = { return a + b; };\n" + "fn main() i32 = { return add(7, 35); };", 42 }, + { "fn main() i32 = {\n" + " let i: i32 = 0;\n" + " let s: i32 = 0;\n" + " for (i < 10) { s += i; i += 1; };\n" + " return s;\n" + "};", 45 }, + { "fn main() i32 = {\n" + " let x: i32 = 100;\n" + " if (x > 50) { return 1; };\n" + " return 0;\n" + "};", 1 }, + { "fn main() i32 = {\n" + " let a: i32 = 6;\n" + " let b: i32 = 7;\n" + " return a * b;\n" + "};", 42 }, + /* multi-return tuple, divmod */ + { "fn divmod(a: i64, b: i64) (i64, i64) = { return a / b, a % b; };\n" + "fn main() i32 = {\n" + " let q, r = divmod(17, 5);\n" + " return (q + r): i32;\n" + "};", 5 }, + /* fixed array, byte-wise read/write */ + { "fn main() i32 = {\n" + " let buf: [4]u8;\n" + " buf[0] = 1: u8;\n" + " buf[1] = 2: u8;\n" + " buf[2] = 3: u8;\n" + " buf[3] = 4: u8;\n" + " let sum: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 4) { sum += buf[i]: i32; i += 1; };\n" + " return sum;\n" + "};", 10 }, + /* float: arg, arith, literal, cast back to int */ + { "fn area(r: f64) f64 = { return 3.14 * r * r; };\n" + "fn main() i32 = { let a: f64 = area(5.0); return a: i32; };", 78 }, + /* string literal via syscall — exit code = bytes written */ + { "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n" + "fn print(s: str) i64 = { return rt_syscall(1, 1, s.ptr: i64, s.len: i64); };\n" + "fn main() i32 = { return print(\"hello, world\\n\"): i32; };", 13 }, + /* 9 args — 3 spill to the stack */ + { "fn s9(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32, i: i32) i32 = {\n" + " return a + b + c + d + e + f + g + h + i;\n" + "};\n" + "fn main() i32 = { return s9(1,2,3,4,5,6,7,8,9); };", 45 }, + /* defer: LIFO at function return */ + { "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n" + "fn out(c: i32) void = { rt_syscall(1, 1, (&c): i64, 1); };\n" + "fn main() i32 = {\n" + " let c1: i32 = 0;\n" + " let c2: i32 = 0;\n" + " c1 = 65;\n" /* 'A' */ + " c2 = 66;\n" /* 'B' */ + " defer out(c1);\n" + " defer out(c2);\n" + " return 0;\n" + "};", 0 }, + /* struct-by-value: 16B all-int passed by value */ + { "type pair = struct { a: i64, b: i64 };\n" + "fn sum(p: pair) i64 = { return p.a + p.b; };\n" + "fn main() i32 = {\n" + " let p: pair = pair { a = 10, b = 32 };\n" + " return sum(p): i32;\n" + "};", 42 }, + /* f32 cast + arithmetic */ + { "fn add32(a: f32, b: f32) f32 = { return a + b; };\n" + "fn main() i32 = {\n" + " let r: f32 = add32(2.5: f32, 7.5: f32);\n" + " return r: i32;\n" + "};", 10 }, + /* slice from array: build header, iterate via index/len */ + { "fn main() i32 = {\n" + " let arr: [4]u8;\n" + " arr[0] = 10: u8; arr[1] = 20: u8;\n" + " arr[2] = 30: u8; arr[3] = 99: u8;\n" + " let s: []u8 = arr[0:3];\n" + " let sum: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < s.len) { sum += s[i]: i32; i += 1; };\n" + " return sum;\n" + "};", 60 }, + /* module imports: use os and call os.write; exit code = bytes */ + { "use os;\n" + "fn main() i32 = { return os.write(1, \"ok\\n\".ptr, 3): i32; };", 3 }, + /* typed integer literals */ + { "fn main() i32 = {\n" + " let buf: [4]u8;\n" + " buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 0u8;\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 3) { s += buf[i]: i32; i += 1; };\n" + " return s;\n" + "};", 198 }, + /* full stdlib stack: use os + strconv, slice-arg call, write + * the formatted number to stdout. exit code = number length. */ + { "use os;\n" + "use strconv;\n" + "fn main() i32 = {\n" + " let buf: [32]u8;\n" + " let s: []u8 = buf[0:32];\n" + " let n: i32 = strconv.i64toa(s, 12345);\n" + " os.write(1, buf.ptr, n: u64);\n" + " os.write(1, \"\\n\".ptr, 1u64);\n" + " return n;\n" + "};", 5 }, + /* alloc + free via mmap-backed runtime — write through allocated + * memory and free it. exit = 0 if the allocation succeeded. */ + { "use os;\n" + "fn main() i32 = {\n" + " let p: *void = os.alloc(4096u64);\n" + " if (p == nil) { return 1; };\n" + " let bp: *u8 = p: *u8;\n" + " bp[0] = 65u8;\n" + " os.write(1, bp, 1u64);\n" + " os.free(p, 4096u64);\n" + " return 0;\n" + "};", 0 }, + /* argv: kernel passes argc in DI, argv in SI. */ + { "fn main(argc: i32, argv: **u8) i32 = { return argc; };", 1 }, + /* i64 array: scaled indexing (elem size 8) */ + { "fn main() i32 = {\n" + " let arr: [4]i64;\n" + " arr[0] = 10; arr[1] = 20; arr[2] = 30; arr[3] = 40;\n" + " let sum: i64 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 4) { sum += arr[i]; i += 1; };\n" + " return sum: i32;\n" + "};", 100 }, + /* break out of an infinite loop early */ + { "fn main() i32 = {\n" + " let i: i32 = 0;\n" + " for () {\n" + " i += 1;\n" + " if (i == 7) { break; };\n" + " };\n" + " return i;\n" + "};", 7 }, + /* switch with multi-expr cases + default */ + { "fn classify(x: i32) i32 = {\n" + " switch (x) {\n" + " case 1, 2, 3: return 10;\n" + " case 10: return 99;\n" + " case: return 50;\n" + " };\n" + " return -1;\n" + "};\n" + "fn main() i32 = {\n" + " return classify(2) + classify(10) + classify(99);\n" + "};", 159 }, /* 10+99+50=159 */ + /* fmt module: stdlib formatter for ints + strings */ + { "use fmt;\n" + "fn main() i32 = {\n" + " fmt.println(\"ww\");\n" + " fmt.printlnint(42);\n" + " fmt.printlnint(-7);\n" + " return 0;\n" + "};", 0 }, + /* struct with i32 fields: MOVL/MOVSXD avoids clobbering neighbors */ + { "type point = struct { x: i32, y: i32 };\n" + "fn distsq(p: point) i32 = { return p.x * p.x + p.y * p.y; };\n" + "fn main() i32 = {\n" + " let p: point = point { x = 3, y = 4 };\n" + " return distsq(p);\n" + "};", 25 }, + /* str equality via == and != */ + { "fn main() i32 = {\n" + " let a: str = \"hello\";\n" + " let b: str = \"hello\";\n" + " let c: str = \"world\";\n" + " let n: i32 = 0;\n" + " if (a == b) { n += 10; };\n" + " if (a != c) { n += 20; };\n" + " return n;\n" + "};", 30 }, + /* CLAUDE.md's move pattern: ptr-to-struct compound field write */ + { "type point = struct { x: i32, y: i32 };\n" + "fn move(p: *point, dx: i32, dy: i32) void = {\n" + " p.x += dx; p.y += dy;\n" + "};\n" + "fn main() i32 = {\n" + " let pt: point = point { x = 0, y = 0 };\n" + " move(&pt, 3, 4);\n" + " return pt.x + pt.y;\n" + "};", 7 }, + /* vtable polymorphism: struct of fn pointers, indirect call */ + { "type ops = struct { add: fn(a: i32, b: i32) i32 };\n" + "fn plus(a: i32, b: i32) i32 = { return a + b; };\n" + "fn main() i32 = {\n" + " let v: ops = ops { add = plus };\n" + " return v.add(20, 22);\n" + "};", 42 }, + /* compound bitwise/shift assigns */ + { "fn main() i32 = {\n" + " let x: i32 = 100;\n" + " x &= 0x3f; x |= 0x80; x ^= 0xc4;\n" + " x *= 2; x <<= 1; x >>= 2;\n" + " return x;\n" + "};", 96 }, + /* user-defined slice append via *[]u8: stdlib slices.appendu8. + * Demonstrates allocator + ptr-to-slice fields + scaled index write. */ + { "use os;\n" + "use slices;\n" + "fn main() i32 = {\n" + " let s: []u8;\n" + " s.ptr = nil; s.len = 0; s.cap = 0;\n" + " slices.appendu8(&s, 65u8);\n" + " slices.appendu8(&s, 66u8);\n" + " slices.appendu8(&s, 67u8);\n" + " os.write(1, s.ptr, s.len: u64);\n" + " os.write(1, \"\\n\".ptr, 1u64);\n" + " return s.len;\n" + "};", 3 }, + /* Hare-style builtins: append(s, v) and len(s). The compiler + * lowers these to slices.appendu8 / s.len access. */ + { "use os;\n" + "use slices;\n" + "fn main() i32 = {\n" + " let s: []u8;\n" + " s.ptr = nil; s.len = 0; s.cap = 0;\n" + " append(s, 88u8); append(s, 89u8); append(s, 90u8);\n" + " os.write(1, s.ptr, len(s): u64);\n" + " os.write(1, \"\\n\".ptr, 1u64);\n" + " return len(s);\n" + "};", 3 }, + /* str-returning function: 16-byte return via AX:DX (SysV). The + * caller's str slot is filled from those two regs. */ + { "use strings;\n" + "use fmt;\n" + "fn main() i32 = {\n" + " let r: str = strings.concat(\"hello, \", \"world\");\n" + " fmt.println(r);\n" + " return r.len;\n" + "};", 12 }, + /* variadic append + static qualifier (Hare idiom) */ + { "use os;\n" + "use slices;\n" + "fn main() i32 = {\n" + " let s: []u8;\n" + " s.ptr = nil; s.len = 0; s.cap = 0;\n" + " static append(s, 72u8, 105u8, 33u8, 10u8);\n" + " os.write(1, s.ptr, len(s): u64);\n" + " return len(s);\n" + "};", 4 }, + /* alloc() builtin: heap-allocate a struct, init from struct-lit */ + { "use os;\n" + "type point = struct { x: i32, y: i32 };\n" + "fn main() i32 = {\n" + " let p: *point = alloc(point { x = 3, y = 4 });\n" + " return p.x * p.x + p.y * p.y;\n" + "};", 25 }, + /* Hare-style range loop: for (let x .. slice) iterates elements */ + { "use os;\n" + "use slices;\n" + "fn main() i32 = {\n" + " let s: []u8;\n" + " s.ptr = nil; s.len = 0; s.cap = 0;\n" + " append(s, 10u8, 20u8, 30u8, 40u8);\n" + " let total: i32 = 0;\n" + " for (let b .. s) { total += b: i32; };\n" + " return total;\n" + "};", 100 }, + /* alloc([], n): fresh empty slice with cap n */ + { "use os;\n" + "use slices;\n" + "fn main() i32 = {\n" + " let s: []u8 = alloc([], 16);\n" + " append(s, 72u8, 105u8);\n" + " return s.cap;\n" + "};", 16 }, + /* variadic spread: append(dst, src...) iterates src */ + { "use os;\n" + "use slices;\n" + "fn main() i32 = {\n" + " let src: []u8;\n" + " src.ptr = nil; src.len = 0; src.cap = 0;\n" + " append(src, 65u8, 66u8, 67u8);\n" + " let dst: []u8;\n" + " dst.ptr = nil; dst.len = 0; dst.cap = 0;\n" + " append(dst, src...);\n" + " os.write(1, dst.ptr, dst.len: u64);\n" + " os.write(1, \"\\n\".ptr, 1u64);\n" + " return dst.len;\n" + "};", 3 }, + /* Hare-style tuple destructure in let */ + { "fn divmod(a: i64, b: i64) (i64, i64) = { return a / b, a % b; };\n" + "fn main() i32 = {\n" + " let (q, r) = divmod(17, 5);\n" + " return (q + r): i32;\n" + "};", 5 }, + /* Hare-style tuple destructure in for-range */ + { "fn main() i32 = {\n" + " let buf: [4]i64;\n" + " buf[0] = 1; buf[1] = 10; buf[2] = 2; buf[3] = 20;\n" + " let s: [](i64, i64);\n" + " s.ptr = buf.ptr: *(i64, i64);\n" + " s.len = 2; s.cap = 2;\n" + " let total: i64 = 0;\n" + " for (let (k, v) .. s) { total += k + v; };\n" + " return total: i32;\n" + "};", 33 }, + /* Hare-style tuple positional access: t.0, t.1 */ + { "fn pair() (i64, i64) = { return 10, 32; };\n" + "fn main() i32 = {\n" + " let t: (i64, i64) = pair();\n" + " return (t.0 + t.1): i32;\n" + "};", 42 }, + /* Hare-style abort/assert + free() builtin */ + { "use os;\n" + "type point = struct { x: i64, y: i64 };\n" + "fn main() i32 = {\n" + " let p: *point = alloc(point { x = 7, y = 35 });\n" + " let r: i64 = p.x + p.y;\n" + " free(p);\n" + " os.assert(r == 42, \"sum mismatch\\n\");\n" + " return r: i32;\n" + "};", 42 }, + /* 3-field tuple destructure in for-range */ + { "fn main() i32 = {\n" + " let buf: [3]i64;\n" + " buf[0] = 5; buf[1] = 7; buf[2] = 30;\n" + " let s: [](i64, i64, i64);\n" + " s.ptr = buf.ptr: *(i64, i64, i64);\n" + " s.len = 1; s.cap = 1;\n" + " let total: i64 = 0;\n" + " for (let (a, b, c) .. s) { total += a + b + c; };\n" + " return total: i32;\n" + "};", 42 }, + /* Hare-style tagged union + match */ + { "fn parse(n: i64) (i64 | i32) = {\n" + " if (n < 0) { return 1: i32; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | i32) = parse(40);\n" + " let s: i64 = 0;\n" + " match (r) {\n" + " case let v: i64 => s = v;\n" + " case let e: i32 => s = -1;\n" + " };\n" + " return (s + 2): i32;\n" + "};", 42 }, + /* ? propagation up the stack */ + { "fn try1(n: i64) (i64 | i32) = {\n" + " if (n < 0) { return 99: i32; };\n" + " return n;\n" + "};\n" + "fn try2(n: i64) (i64 | i32) = {\n" + " let v: i64 = try1(n)?;\n" + " return v + 100;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | i32) = try2(-1);\n" + " let s: i64 = 0;\n" + " match (r) {\n" + " case let v: i64 => s = v;\n" + " case let e: i32 => s = e: i64;\n" + " };\n" + " return s: i32;\n" + "};", 99 }, + /* match cases written in reverse variant order — dispatch must + * use the variant tag, not the case position */ + { "fn parse(n: i64) (i64 | i32) = {\n" + " if (n < 0) { return 7: i32; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | i32) = parse(-1);\n" + " match (r) {\n" + " case let e: i32 => return e;\n" + " case let v: i64 => return (v + 1000): i32;\n" + " };\n" + " return 0;\n" + "};", 7 }, + /* let-init from a bare variant value: tag must be synthesised */ + { "fn main() i32 = {\n" + " let r: (i64 | i32) = 7: i32;\n" + " match (r) {\n" + " case let v: i64 => return 1;\n" + " case let e: i32 => return e;\n" + " };\n" + " return 0;\n" + "};", 7 }, + /* let-init from an untyped literal: variant inclusion must let + * the assignability check through, and the default variant wins */ + { "fn main() i32 = {\n" + " let r: (i64 | i32) = 5;\n" + " match (r) {\n" + " case let v: i64 => return v: i32;\n" + " case let e: i32 => return 99;\n" + " };\n" + " return 0;\n" + "};", 5 }, + /* assignment to a tagged-union local: same tag synthesis */ + { "fn main() i32 = {\n" + " let r: (i64 | i32) = 0;\n" + " r = 9: i32;\n" + " match (r) {\n" + " case let v: i64 => return 1;\n" + " case let e: i32 => return e;\n" + " };\n" + " return 0;\n" + "};", 9 }, + /* default arm `case =>` */ + { "fn parse(n: i64) (i64 | i32) = {\n" + " if (n < 0) { return 1: i32; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | i32) = parse(-1);\n" + " match (r) {\n" + " case let v: i64 => return 1;\n" + " case => return 7;\n" + " };\n" + " return 0;\n" + "};", 7 }, + /* `case T =>` without binding still dispatches by tag */ + { "fn parse(n: i64) (i64 | i32) = {\n" + " if (n < 0) { return 1: i32; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | i32) = parse(40);\n" + " match (r) {\n" + " case i32 => return 1;\n" + " case let v: i64 => return v: i32;\n" + " };\n" + " return 0;\n" + "};", 40 }, + /* str-typed variant payload: let-init with a string literal, + * match-binding loads ptr+len from the slot */ + { "fn main() i32 = {\n" + " let r: (i64 | str) = \"hello, world\";\n" + " match (r) {\n" + " case let n: i64 => return 1;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 12 }, + /* assigning a string into a tagged-union local */ + { "fn main() i32 = {\n" + " let r: (i64 | str) = 0;\n" + " r = \"abc\";\n" + " match (r) {\n" + " case let n: i64 => return 1;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* fn returning (T | str) — wide return ABI */ + { "fn parse(n: i64) (i64 | str) = {\n" + " if (n < 0) { return \"negative number\"; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | str) = parse(-1);\n" + " match (r) {\n" + " case let n: i64 => return 1;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 15 }, + /* ? propagating a str-typed error all the way up */ + { "fn try1(n: i64) (i64 | str) = {\n" + " if (n < 0) { return \"fail\"; };\n" + " return n;\n" + "};\n" + "fn try2(n: i64) (i64 | str) = {\n" + " let v: i64 = try1(n)?;\n" + " return v + 100;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | str) = try2(-1);\n" + " match (r) {\n" + " case let n: i64 => return n: i32;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 4 }, + /* ? success unwrap when the first variant is itself str */ + { "fn make() (str | i64) = {\n" + " return \"ok\";\n" + "};\n" + "fn main() i32 = {\n" + " let r: (str | i64) = make();\n" + " let v: str = r?;\n" + " return v.len: i32;\n" + "};", 2 }, + /* type error = str; named-alias variant works through the + * full happy/error path */ + { "type error = str;\n" + "fn read(n: i64) (i64 | error) = {\n" + " if (n < 0) { return \"eof\": error; };\n" + " return n + 1;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | error) = read(-1);\n" + " match (r) {\n" + " case let v: i64 => return v: i32;\n" + " case let e: error => return e.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* multi-pattern arm: `case T1 | T2 =>` matches either tag */ + { "fn pick(n: i64) (i64 | i32 | u32) = {\n" + " if (n < 0) { return 1: i32; };\n" + " if (n == 0) { return 2: u32; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r1: (i64 | i32 | u32) = pick(0);\n" + " let r2: (i64 | i32 | u32) = pick(-1);\n" + " let r3: (i64 | i32 | u32) = pick(7);\n" + " let acc: i32 = 0;\n" + " match (r1) {\n" + " case let v: i64 => acc += 100;\n" + " case i32 | u32 => acc += 1;\n" + " };\n" + " match (r2) {\n" + " case let v: i64 => acc += 100;\n" + " case i32 | u32 => acc += 10;\n" + " };\n" + " match (r3) {\n" + " case let v: i64 => acc += v: i32;\n" + " case i32 | u32 => acc += 100;\n" + " };\n" + " return acc;\n" + "};", 18 }, + /* Named-alias tagged union as fn arg + ≤16B variants */ + { "type result = (i64 | i32);\n" + "fn unwrap(r: result) i64 = {\n" + " match (r) {\n" + " case let v: i64 => return v;\n" + " case let e: i32 => return e: i64;\n" + " };\n" + " return -1;\n" + "};\n" + "fn main() i32 = {\n" + " let r1: result = 100;\n" + " let r2: result = 7: i32;\n" + " return (unwrap(r1) + unwrap(r2)): i32;\n" + "};", 107 }, + /* 24B tagged-union arg with str variant */ + { "type result = (i64 | str);\n" + "fn classify(r: result) i32 = {\n" + " match (r) {\n" + " case let v: i64 => return 1;\n" + " case let e: str => return e.len: i32;\n" + " };\n" + " return -1;\n" + "};\n" + "fn main() i32 = {\n" + " let r1: result = \"hello\";\n" + " let r2: result = 42;\n" + " return classify(r1) + classify(r2);\n" + "};", 6 }, + /* Tagged union as struct field — both literal init and assign, + * and match-on-field reads from the field's slot in place */ + { "type point = struct {\n" + " x: i32,\n" + " err: (i64 | str),\n" + "};\n" + "fn main() i32 = {\n" + " let p: point = point { x = 1, err = 0 };\n" + " p.err = \"updated\";\n" + " match (p.err) {\n" + " case let v: i64 => return 0;\n" + " case let e: str => return e.len: i32;\n" + " };\n" + " return -1;\n" + "};", 7 }, + /* Pointer variant in a tagged union */ + { "type point = struct { x: i32, y: i32 };\n" + "fn main() i32 = {\n" + " let p: point = point { x = 3, y = 4 };\n" + " let r: (*point | str) = &p;\n" + " match (r) {\n" + " case let pp: *point => return pp.x + pp.y;\n" + " case let e: str => return -1;\n" + " };\n" + " return 0;\n" + "};", 7 }, + /* Forwarding `return inner(n)` when both fns share a tagged- + * union return type — value passes through unwrapped */ + { "type result = (i64 | str);\n" + "fn inner(n: i64) result = {\n" + " if (n < 0) { return \"neg\"; };\n" + " return n + 1;\n" + "};\n" + "fn outer(n: i64) result = {\n" + " return inner(n);\n" + "};\n" + "fn main() i32 = {\n" + " let r: result = outer(-1);\n" + " match (r) {\n" + " case let v: i64 => return v: i32;\n" + " case let e: str => return e.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* match directly on a call expression (no intermediate let) */ + { "fn make(n: i64) (i64 | str) = {\n" + " if (n < 0) { return \"neg\"; };\n" + " return n + 1;\n" + "};\n" + "fn main() i32 = {\n" + " match (make(-1)) {\n" + " case let v: i64 => return v: i32;\n" + " case let e: str => return e.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* Plan 9-style sentinel error idiom: `def NAME: error = "lit"` + * inlines as the (ptr, len) pair at use sites. */ + { "type error = str;\n" + "def eEOF: error = \"eof\";\n" + "def eShortRead: error = \"short read\";\n" + "fn read(n: i64) (i64 | error) = {\n" + " if (n < 0) { return eEOF; };\n" + " if (n == 0) { return eShortRead; };\n" + " return n + 1;\n" + "};\n" + "fn main() i32 = {\n" + " let r0: (i64 | error) = read(0);\n" + " let r1: (i64 | error) = read(-1);\n" + " let r2: (i64 | error) = read(5);\n" + " let acc: i32 = 0;\n" + " match (r0) {\n" + " case let v: i64 => acc += 100;\n" + " case let e: error => acc += e.len: i32;\n" + " };\n" + " match (r1) {\n" + " case let v: i64 => acc += 100;\n" + " case let e: error => acc += e.len: i32;\n" + " };\n" + " match (r2) {\n" + " case let v: i64 => acc += v: i32;\n" + " case let e: error => acc += 100;\n" + " };\n" + " return acc;\n" + "};", 19 }, + /* End-to-end stdlib usage: pull in lib/os and exercise the + * fallible API tryread/trywrite returning (i64 | str) over a + * real syscall. Validates that imported tagged-union returns + * survive the linker as well as the call ABI. */ + { "use os;\n" + "fn main() i32 = {\n" + " let buf: [3]u8;\n" + " buf[0] = 88: u8;\n" + " let ok: (i64 | str) = os.trywrite(1, buf.ptr, 1u64);\n" + " let bad: (i64 | str) = os.trywrite(999: i32, buf.ptr, 1u64);\n" + " let acc: i32 = 0;\n" + " match (ok) {\n" + " case let n: i64 => acc += n: i32;\n" + " case let e: str => acc += -100;\n" + " };\n" + " match (bad) {\n" + " case let n: i64 => acc += -100;\n" + " case let e: str => acc += e.len: i32;\n" + " };\n" + " return acc;\n" + "};", 13 }, /* 1 byte written to fd 1, plus len(\"write failed\")=12 */ + /* strconv.parse64: fallible signed decimal. Two successful + * parses contribute their values; one bad parse contributes + * the error message length (20 = len(\"parse: invalid digit\")). */ + { "use strconv;\n" + "fn main() i32 = {\n" + " let r1: (i64 | str) = strconv.parse64(\"42\");\n" + " let r2: (i64 | str) = strconv.parse64(\"-7\");\n" + " let r3: (i64 | str) = strconv.parse64(\"abc\");\n" + " let acc: i32 = 0;\n" + " match (r1) {\n" + " case let v: i64 => acc += v: i32;\n" + " case let e: str => acc += -100;\n" + " };\n" + " match (r2) {\n" + " case let v: i64 => acc += v: i32;\n" + " case let e: str => acc += -100;\n" + " };\n" + " match (r3) {\n" + " case let v: i64 => acc += -100;\n" + " case let e: str => acc += e.len: i32;\n" + " };\n" + " return acc;\n" + "};", 55 }, /* 42 + (-7) + 20 */ + /* strconv.parseu64: success path 123, error path captures + * len(\"parse: invalid digit\") = 20 for the leading-sign reject. */ + { "use strconv;\n" + "fn main() i32 = {\n" + " let r1: (u64 | str) = strconv.parseu64(\"123\");\n" + " let r2: (u64 | str) = strconv.parseu64(\"-1\");\n" + " let acc: i32 = 0;\n" + " match (r1) {\n" + " case let v: u64 => acc += v: i32;\n" + " case let e: str => acc += -100;\n" + " };\n" + " match (r2) {\n" + " case let v: u64 => acc += -100;\n" + " case let e: str => acc += e.len: i32;\n" + " };\n" + " return acc;\n" + "};", 143 }, /* 123 + 20 */ + /* strings.indexbyte (Plan 9 -1) and strings.index (substring). */ + { "use strings;\n" + "fn main() i32 = {\n" + " let s: str = \"hello, world\";\n" + " let i1: i32 = strings.indexbyte(s, 44u8);\n" + " let i2: i32 = strings.indexbyte(s, 122u8);\n" + " let i3: i32 = strings.index(s, \"world\");\n" + " let i4: i32 = strings.index(s, \"nope\");\n" + " return i1 + i2 + i3 + i4;\n" + "};", 10 }, /* 5 + (-1) + 7 + (-1) */ + /* bytes.indexsub: substring search over []u8. */ + { "use bytes;\n" + "fn main() i32 = {\n" + " let buf: [12]u8;\n" + " buf[0] = 104u8; buf[1] = 101u8; buf[2] = 108u8; buf[3] = 108u8;\n" + " buf[4] = 111u8; buf[5] = 44u8; buf[6] = 32u8; buf[7] = 119u8;\n" + " buf[8] = 111u8; buf[9] = 114u8; buf[10] = 108u8; buf[11] = 100u8;\n" + " let needle: [3]u8;\n" + " needle[0] = 119u8; needle[1] = 111u8; needle[2] = 114u8;\n" + " return bytes.indexsub(buf[0:12], needle[0:3]);\n" + "};", 7 }, + /* errors.is — sentinel comparison through a (T | error) union. + * Sets up two errors, dispatches each, and confirms the matching + * sentinel detection. */ + { "use errors;\n" + "fn parse(n: i64) (i64 | errors.error) = {\n" + " if (n < 0) { return errors.eEOF; };\n" + " if (n == 0) { return errors.eShortRead; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let r1: (i64 | errors.error) = parse(-1);\n" + " let r2: (i64 | errors.error) = parse(0);\n" + " let acc: i32 = 0;\n" + " match (r1) {\n" + " case let v: i64 => acc += -100;\n" + " case let e: errors.error =>\n" + " if (errors.is(e, errors.eEOF)) { acc += 1; }\n" + " else { acc += -100; };\n" + " };\n" + " match (r2) {\n" + " case let v: i64 => acc += -100;\n" + " case let e: errors.error =>\n" + " if (errors.is(e, errors.eShortRead)) { acc += 10; }\n" + " else { acc += -100; };\n" + " };\n" + " return acc;\n" + "};", 11 }, + /* bufio.takeline: drain successive '\\n'-terminated lines from a + * pre-filled buffer, then a trailing fragment that returns the + * `linerr` variant carrying \"no newline\" (10 chars). */ + { "use bufio;\n" + "fn main() i32 = {\n" + " let raw: [11]u8;\n" + " raw[0] = 102u8; raw[1] = 111u8; raw[2] = 111u8; raw[3] = 10u8;\n" + " raw[4] = 98u8; raw[5] = 97u8; raw[6] = 114u8; raw[7] = 10u8;\n" + " raw[8] = 98u8; raw[9] = 97u8; raw[10] = 122u8;\n" + " let b: bufio.buf;\n" + " b.s = nil; b.data = raw.ptr; b.cap = 11; b.r = 0; b.w = 11;\n" + " let acc: i32 = 0;\n" + " let l1: (str | bufio.linerr) = bufio.takeline(&b);\n" + " match (l1) {\n" + " case let s: str => acc += s.len: i32;\n" + " case let e: bufio.linerr => acc += -100;\n" + " };\n" + " let l2: (str | bufio.linerr) = bufio.takeline(&b);\n" + " match (l2) {\n" + " case let s: str => acc += s.len: i32;\n" + " case let e: bufio.linerr => acc += -100;\n" + " };\n" + " let l3: (str | bufio.linerr) = bufio.takeline(&b);\n" + " match (l3) {\n" + " case let s: str => acc += -100;\n" + " case let e: bufio.linerr => acc += e.len: i32;\n" + " };\n" + " return acc;\n" + "};", 16 }, /* 3 + 3 + len(\"no newline\")=10 */ + { NULL, 0 } +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + /* Resolve to absolute path: tests chdir into /tmp/... */ + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64], exe[64]; + snprintf(src, sizeof src, "/tmp/wwe2e_%d_%d.ww", getpid(), i); + snprintf(exe, sizeof exe, "/tmp/wwe2e_%d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + fputs(rows[i].src, f); + fclose(f); + + char cmd[1024]; + /* ww build writes the binary to the current working dir, + * named after the source basename. We override by chdir. */ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwe2e_%d_d_%d", getpid(), i); + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { fail++; continue; } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row %d: exit %d, want %d\n src: %s\n", + i, got, rows[i].want_exit, rows[i].src); + fail++; + } + unlink(src); unlink(outbin); rmdir(tmpdir); + (void)exe; + } + if (fail) { fprintf(stderr, "%d/%d e2e tests failed\n", fail, n); return 1; } + printf("e2e: %d/%d ok\n", n, n); + return 0; +} diff --git a/test/wwc/800_ffi.c b/test/wwc/800_ffi.c new file mode 100644 index 00000000..e46c1e83 --- /dev/null +++ b/test/wwc/800_ffi.c @@ -0,0 +1,80 @@ +/* + * 800_ffi — verify the @symbol("X") attribute redirects CALLs at + * the codegen layer. The ww source declares a body-less fn with an + * external symbol name, calls it; the emitted .s must reference the + * external name, not the ww-side ident. + * + * Also smoke-tests building lib/c/libc/libc.ww (parse + check + cgen + * succeed; nothing to run). + */ +#include +#include +#include +#include + +static char * +run6c(const char *bin, const char *src) +{ + char path[64]; + snprintf(path, sizeof path, "/tmp/wwffi_%d.ww", getpid()); + FILE *f = fopen(path, "wb"); + fputs(src, f); + fclose(f); + + char cmd[512]; + snprintf(cmd, sizeof cmd, "%s/6c %s 2>&1", bin, path); + FILE *p = popen(cmd, "r"); + size_t cap = 4096, n = 0; + char *buf = malloc(cap); + int c; + while ((c = fgetc(p)) != EOF) { + if (n + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); } + buf[n++] = (char)c; + } + buf[n] = '\0'; + pclose(p); + unlink(path); + return buf; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + + int fail = 0; + + /* @symbol redirects ident name to symbol name */ + { + char *out = run6c(bin, + "@symbol(\"cout\") fn writeit(b: *u8, n: u64) i64;\n" + "fn main() i32 = { writeit(nil, 0); return 0; };"); + if (strstr(out, "CALL\tcout(SB)") == NULL) { + fprintf(stderr, "ffi: missing CALL cout(SB) in:\n%s\n", out); + fail++; + } + if (strstr(out, "CALL\twriteit(SB)") != NULL) { + fprintf(stderr, "ffi: should not call by ident name:\n%s\n", out); + fail++; + } + free(out); + } + + /* lib/c/libc parses + cgens (no defined fns means no TEXT directives) */ + { + char path[256]; + snprintf(path, sizeof path, "%s/../../lib/c/libc/libc.ww", bin); + char cmd[512]; + snprintf(cmd, sizeof cmd, "%s/6c %s > /dev/null 2>&1", bin, path); + int rc = system(cmd); + if (rc != 0) { + fprintf(stderr, "ffi: 6c failed on libc.ww (rc=%d)\n", rc); + fail++; + } + } + + if (fail) return 1; + puts("ffi: ok"); + return 0; +} diff --git a/test/wwc/900_stdlib.c b/test/wwc/900_stdlib.c new file mode 100644 index 00000000..2e81d8be --- /dev/null +++ b/test/wwc/900_stdlib.c @@ -0,0 +1,65 @@ +/* + * 900_stdlib — verify each stdlib module parses, type-checks, and + * codegens. We don't link or run them; they exist for downstream + * users to import. As real applications come online they will start + * exercising the bodies through the e2e harness. + */ +#include +#include +#include +#include + +static const char *modules[] = { + "lib/types/types.ww", + "lib/ascii/ascii.ww", + "lib/bytes/bytes.ww", + "lib/strings/strings.ww", + "lib/io/stream.ww", + "lib/errors/errors.ww", + "lib/os/os.ww", + "lib/strconv/strconv.ww", + "lib/sort/sort.ww", + "lib/path/path.ww", + "lib/encoding/utf8/utf8.ww", + "lib/encoding/hex/hex.ww", + "lib/hash/fnv/fnv.ww", + "lib/time/time.ww", + "lib/c/libc/libc.ww", + "lib/bufio/bufio.ww", + "lib/fmt/fmt.ww", + "lib/net/net.ww", + "lib/slices/slices.ww", + NULL +}; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + int n = 0, fail = 0; + for (int i = 0; modules[i]; i++, n++) { + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, modules[i]); + snprintf(cmd, sizeof cmd, "%s/6c %s > /dev/null 2>&1", + bin, path); + int rc = system(cmd); + if (rc != 0) { + fprintf(stderr, "stdlib FAIL: %s (rc=%d)\n", modules[i], rc); + fail++; + } + } + if (fail) { fprintf(stderr, "%d/%d stdlib modules failed\n", fail, n); return 1; } + printf("stdlib: %d/%d ok\n", n, n); + return 0; +} diff --git a/test/wwc/990_selfhost.c b/test/wwc/990_selfhost.c new file mode 100644 index 00000000..d74d5ee2 --- /dev/null +++ b/test/wwc/990_selfhost.c @@ -0,0 +1,786 @@ +/* + * 990_selfhost — phase-10 marker test. Three probes: + * + * 1. The selfhost stubs (mem.ww, err.ww) must parse, typecheck and + * codegen via C-side `6c`. + * + * 2. selfhost/test/smoke.ww — an actual ww program exercising the + * patterns the real port will use (bump arena, error union, + * struct-of-fn-pointer dispatch, byte scanner, strconv) — must + * build through `ww build` and exit 42 when run. Any non-42 exit + * names which probe broke (1..N). + * + * 3. `wwdump` is deterministic: running it twice on the same input + * must produce byte-identical output. This is the diff anchor for + * self-host. When the ww-side wwdump lands (task 3), the same + * bytes will have to come out of the ww frontend. + * + * The ceiling — `cmp ww2 ww3` — is gated on the full frontend port. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +/* Probe 1: each file must compile through `6c`. */ +static int +probe_codegen(const char *bin) +{ + static const char *files[] = { + "selfhost/cmd/wwc/mem.ww", + "selfhost/cmd/wwc/err.ww", + NULL, + }; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + int fail = 0; + for (int i = 0; files[i]; i++) { + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "%s/6c %s/%s > /dev/null 2>&1", bin, cwd, files[i]); + if (runwait(cmd) != 0) { + fprintf(stderr, "codegen FAIL: %s\n", files[i]); + fail++; + } + } + return fail ? -1 : 0; +} + +/* Probe 2: end-to-end build+run of selfhost/test/smoke.ww. Exit 42 = ok. */ +static int +probe_smoke(const char *bin) +{ + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwsh_%d", getpid()); + mkdir(tmpdir, 0755); + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww build %s/selfhost/test/smoke.ww >/dev/null 2>&1", + tmpdir, bin, cwd); + if (runwait(cmd) != 0) { + fprintf(stderr, "smoke FAIL: ww build did not succeed\n"); + return -1; + } + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/smoke", tmpdir); + int rc = runwait(outbin); + unlink(outbin); + char tmp[1024]; + snprintf(tmp, sizeof tmp, "%s/smoke.s", tmpdir); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s/smoke.o", tmpdir); unlink(tmp); + snprintf(tmp, sizeof tmp, "%s/smoke.combined.ww", tmpdir); unlink(tmp); + rmdir(tmpdir); + if (rc != 42) { + fprintf(stderr, "smoke FAIL: exit=%d (want 42; the number " + "names which probe in selfhost/test/smoke.ww broke)\n", rc); + return -1; + } + return 0; +} + +/* Probe 3a: ww-side wwdump_ww must produce byte-identical output to + * the C-side wwdump on every fixture. The token-stream diff (-t) is + * the strongest signal: the lex.ww port has converged when this is + * empty across realistic source. The AST diff (-a) is a partial + * signal — the ww-side parser is currently a stub that handles only + * `use` clauses, so we exercise it on a stripped fixture. As parse.ww + * grows, more files will be added to the -a list. */ +static int +probe_dump_diff_mode(const char *bin, const char *flag, const char **inputs) +{ + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + int fail = 0; + for (int i = 0; inputs[i]; i++) { + char a[256], b[256], cmd[2048]; + snprintf(a, sizeof a, "/tmp/wwd_diff_%d_c", getpid()); + snprintf(b, sizeof b, "/tmp/wwd_diff_%d_w", getpid()); + snprintf(cmd, sizeof cmd, "%s/wwdump %s %s/%s > %s 2>/dev/null", + bin, flag, cwd, inputs[i], a); + runwait(cmd); + snprintf(cmd, sizeof cmd, "%s/wwdump_ww %s %s/%s > %s 2>/dev/null", + bin, flag, cwd, inputs[i], b); + runwait(cmd); + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + int rc1 = slurp(a, &ba, &na); + int rc2 = slurp(b, &bb, &nb); + if (rc1 < 0 || rc2 < 0) { + fprintf(stderr, "diff FAIL (%s): cannot read dumps for %s\n", + flag, inputs[i]); + fail++; + } else if (na != nb || memcmp(ba, bb, na) != 0) { + fprintf(stderr, "diff FAIL (%s): %s — C %zu bytes vs ww %zu bytes\n", + flag, inputs[i], na, nb); + fail++; + } + free(ba); free(bb); + unlink(a); unlink(b); + } + return fail ? -1 : 0; +} + +static int +probe_dump_diff(const char *bin) +{ + const char *tok_inputs[] = { + "selfhost/cmd/wwc/mem.ww", + "selfhost/cmd/wwc/err.ww", + "selfhost/cmd/wwc/lex.ww", + "selfhost/cmd/wwc/tok.ww", + "selfhost/cmd/wwc/ast.ww", + "selfhost/cmd/wwc/parse.ww", + "selfhost/cmd/wwdump/main.ww", + "selfhost/test/smoke.ww", + NULL, + }; + /* AST diff coverage. The ww-side parser handles use/def/type/let/fn + * (with bodies), expressions with full Pratt precedence, statements + * (block/let/return/if/for/defer/break/continue), match arms, + * tagged unions, struct literals, attributes, and tuples. We diff + * against the C parser on every selfhost file plus the stdlib. */ + const char *ast_inputs[] = { + "selfhost/test/uses.ww", + "selfhost/cmd/wwc/err.ww", + "selfhost/cmd/wwc/mem.ww", + "selfhost/cmd/wwc/lex.ww", + "selfhost/cmd/wwc/tok.ww", + "selfhost/cmd/wwc/ast.ww", + "selfhost/cmd/wwc/parse.ww", + "selfhost/cmd/wwc/typ.ww", + "selfhost/cmd/wwc/sym.ww", + "selfhost/cmd/wwc/check.ww", + "selfhost/cmd/wwc/cgen.ww", + "selfhost/cmd/wwdump/main.ww", + "selfhost/test/smoke.ww", + "lib/strconv/strconv.ww", + "lib/os/os.ww", + "lib/ascii/ascii.ww", + "lib/fmt/fmt.ww", + "lib/strings/strings.ww", + "lib/bytes/bytes.ww", + "lib/io/stream.ww", + "lib/errors/errors.ww", + "lib/bufio/bufio.ww", + "lib/types/types.ww", + "lib/sort/sort.ww", + "lib/path/path.ww", + "lib/slices/slices.ww", + "lib/net/net.ww", + "lib/encoding/utf8/utf8.ww", + "lib/encoding/hex/hex.ww", + "lib/hash/fnv/fnv.ww", + "lib/time/time.ww", + "lib/c/libc/libc.ww", + NULL, + }; + int fail = 0; + if (probe_dump_diff_mode(bin, "-t", tok_inputs) != 0) fail++; + if (probe_dump_diff_mode(bin, "-a", ast_inputs) != 0) fail++; + return fail ? -1 : 0; +} + +/* Probe 5: ww-side cgen (wwdump_ww -c) emits Plan 9 amd64 asm. For + * each fixture we (a) diff the ww asm against C-side 6c and (b) + * assemble + link + run the ww output and compare exit codes. Both + * must succeed. The cgen port handles literals, +/-/star/slash, + * compound assignment, idents, calls, if/else, for loops, and + * comparisons; fixtures stay within that surface. */ +struct cprog { const char *src; int want_exit; }; + +static int +probe_ww_compile(const char *bin) +{ + static const struct cprog progs[] = { + { "fn main() i32 = { return 42; };", 42 }, + { "fn add(a: i32, b: i32) i32 = { return a + b; };\n" + "fn main() i32 = { return add(7, 35); };", 42 }, + { "fn sum(n: i32) i32 = {\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < n) { s += i; i += 1; };\n" + " return s;\n" + "};\n" + "fn main() i32 = { return sum(10); };", 45 }, + { "fn check(x: i32) i32 = {\n" + " if (x > 0) { return 1; };\n" + " if (x < 0) { return 100; };\n" + " return 0;\n" + "};\n" + "fn main() i32 = { return check(7); };", 1 }, + { "fn fact(n: i32) i32 = {\n" + " if (n <= 1) { return 1; };\n" + " return n * fact(n - 1);\n" + "};\n" + "fn main() i32 = { return fact(5); };", 120 }, + /* hello-world via syscall: exercises @symbol FFI, string + * literal interning, DATA emission, str struct slot (16B), + * str pseudo-fields (.ptr/.len), N_CAST. write(1,"hi\n",3) + * returns 3 — we cast that down to the exit code. */ + { "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n" + "fn main() i32 = {\n" + " let s: str = \"hi\\n\";\n" + " let r: i64 = rt_syscall(1, 1, s.ptr: i64, s.len: i64);\n" + " return r: i32;\n" + "};", 3 }, + /* struct fields (local + via *struct), &local, struct-name + * registry: distance squared returns 25 = 3*3 + 4*4. */ + { "type point = struct { x: i64, y: i64 };\n" + "fn distance_sq(p: *point) i64 = { return p.x * p.x + p.y * p.y; };\n" + "fn main() i32 = {\n" + " let p: point;\n" + " p.x = 3i64;\n" + " p.y = 4i64;\n" + " return distance_sq(&p): i32;\n" + "};", 25 }, + /* array indexing — element-size scaling for u8 arrays. + * Sets buf[0..3] to 7,8,9,10 then sums them: 34. */ + { "fn main() i32 = {\n" + " let buf: [4]u8;\n" + " buf[0] = 7u8;\n" + " buf[1] = 8u8;\n" + " buf[2] = 9u8;\n" + " buf[3] = 10u8;\n" + " let s: i32 = 0;\n" + " let i: i32 = 0;\n" + " for (i < 4) { s += buf[i]: i32; i += 1; };\n" + " return s;\n" + "};", 34 }, + /* struct literal init — `let p: point = point { x=..., y=...};`. + * Each field stored at its struct offset in the slot. */ + { "type point = struct { x: i64, y: i64 };\n" + "fn main() i32 = {\n" + " let p: point = point { x = 7i64, y = 35i64 };\n" + " return (p.x + p.y): i32;\n" + "};", 42 }, + /* fn-returning-str — exercises the SysV (AX, DX) → (AX, BX) + * shuffle so str values flow through cgen as the canonical + * pair. write(\"hello world\\n\") → exit 12 (length). */ + { "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n" + "fn greeting() str = { return \"hello world\\n\"; };\n" + "fn main() i32 = {\n" + " let g: str = greeting();\n" + " rt_syscall(1, 1, g.ptr: i64, g.len: i64);\n" + " return g.len: i32;\n" + "};", 12 }, + /* fn-pointer field call — `w.emit(b)` where w is a struct + * with an emit: fn(b: u8) i32 field. Loads the field value + * into AX and CALLs through it. The polymorphism shape ww + * uses instead of interfaces. */ + { "type writer = struct { emit: fn(b: u8) i32 };\n" + "fn count_emit(b: u8) i32 = { return b: i32 + 1; };\n" + "fn main() i32 = {\n" + " let w: writer = writer { emit = count_emit };\n" + " return w.emit(41u8);\n" + "};", 42 }, + /* Match expression — tagged union dispatch. classify(ok)=42, + * classify(err)=-1, sum 41. Exercises tagged init, tagged-arg + * push (3 regs), match arm tag-compare + payload bind. */ + { "fn classify(r: (i32 | str)) i32 = {\n" + " match (r) {\n" + " case let v: i32 => return v;\n" + " case let e: str => return -1;\n" + " };\n" + " return 0;\n" + "};\n" + "fn main() i32 = {\n" + " let ok: (i32 | str) = 42;\n" + " let err: (i32 | str) = \"fail\";\n" + " return classify(ok) + classify(err);\n" + "};", 41 }, + /* Compound subtract via *struct field — regression for + * the bug that made wwdump_ww segfault on its own source. + * c.x = 5; dec; dec; dec → expect 2. Pre-fix, both C and + * ww emitted SUBQ in the wrong direction (computing rhs-old + * instead of old-rhs) and this returned -4 (= 252 as u8). */ + { "type ctx = struct { x: i32 };\n" + "fn dec(c: *ctx) void = { c.x -= 1; };\n" + "fn main() i32 = {\n" + " let c: ctx;\n" + " c.x = 5;\n" + " dec(&c); dec(&c); dec(&c);\n" + " return c.x;\n" + "};", 2 }, + /* Chained N_DOT: o.ptr_to_inner.val (read field through a + * pointer-to-struct field). Pre-fix the cgen fell through + * silently and returned the inner pointer instead of the + * dereferenced field. Surfaced porting 6l/pass.ww. */ + { "@symbol(\"rt_alloc\") fn rt_alloc(n: u64) *void;\n" + "type inner = struct { val: u64 };\n" + "type outer = struct { p: *inner };\n" + "fn main() i32 = {\n" + " let in_: *inner = rt_alloc(8u64): *inner;\n" + " in_.val = 42u64;\n" + " let o: *outer = rt_alloc(8u64): *outer;\n" + " o.p = in_;\n" + " return o.p.val: i32;\n" + "};", 42 }, + { NULL, 0 }, + }; + char rt[1024]; + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); + int fail = 0; + for (int i = 0; progs[i].src; i++) { + char src[64], wws[64], cs[64], obj[64], exe[64], cmd[2048]; + snprintf(src, sizeof src, "/tmp/wwc_%d_%d.ww", getpid(), i); + snprintf(wws, sizeof wws, "/tmp/wwc_%d_%d_w.s", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/wwc_%d_%d_c.s", getpid(), i); + snprintf(obj, sizeof obj, "/tmp/wwc_%d_%d.o", getpid(), i); + snprintf(exe, sizeof exe, "/tmp/wwc_%d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) { fail++; continue; } + fputs(progs[i].src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/wwdump_ww -c %s > %s 2>/dev/null", + bin, src, wws); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-compile prog %d: wwdump_ww -c errored\n", i); + fail++; goto cleanup; + } + snprintf(cmd, sizeof cmd, "%s/6c %s > %s 2>/dev/null", bin, src, cs); + runwait(cmd); + + char *bw = NULL, *bc = NULL; + size_t nw = 0, nc = 0; + if (slurp(wws, &bw, &nw) >= 0 && slurp(cs, &bc, &nc) >= 0) { + if (nw != nc || memcmp(bw, bc, nw) != 0) { + fprintf(stderr, "ww-compile prog %d: asm differs (C %zu, ww %zu)\n", + i, nc, nw); + fail++; + } + } + free(bw); free(bc); + + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s 2>/dev/null", bin, obj, wws); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-compile prog %d: 6a failed\n", i); + fail++; goto cleanup; + } + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s %s 2>/dev/null", + bin, exe, obj, rt); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-compile prog %d: 6l failed\n", i); + fail++; goto cleanup; + } + int rc = runwait(exe); + if (rc != progs[i].want_exit) { + fprintf(stderr, "ww-compile prog %d: exit=%d, want %d\n", + i, rc, progs[i].want_exit); + fail++; + } +cleanup: + unlink(src); unlink(wws); unlink(cs); unlink(obj); unlink(exe); + } + return fail ? -1 : 0; +} + +/* Probe 4: ww-side checker (wwdump_ww -r) runs name resolution on + * each "complete" selfhost fixture and reports zero unresolved. The + * complete set is the ones whose identifiers are all locally defined + * — they don't import other selfhost modules. (Files that DO import + * still resolve most names but have some unresolved type/sym refs; + * those are exercised by the broader -t and -a diff probes.) */ +static int +probe_resolve(const char *bin) +{ + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + const char *complete[] = { + "selfhost/cmd/wwc/err.ww", + "selfhost/cmd/wwc/mem.ww", + "selfhost/cmd/wwc/tok.ww", + "selfhost/test/smoke.ww", + NULL, + }; + int fail = 0; + for (int i = 0; complete[i]; i++) { + char a[256], cmd[2048]; + snprintf(a, sizeof a, "/tmp/wwd_r_%d", getpid()); + snprintf(cmd, sizeof cmd, "%s/wwdump_ww -r %s/%s > %s 2>/dev/null", + bin, cwd, complete[i], a); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "resolve FAIL: %s exited %d (unresolved names)\n", + complete[i], rc); + fail++; + } + unlink(a); + } + return fail ? -1 : 0; +} + +/* Probe 3: wwdump must be deterministic across two runs. */ +static int +probe_dump_stable(const char *bin) +{ + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + const char *inputs[] = { + "selfhost/cmd/wwc/mem.ww", + "selfhost/cmd/wwc/err.ww", + "selfhost/test/smoke.ww", + NULL, + }; + const char *modes[] = { "-t", "-a", NULL }; + int fail = 0; + for (int i = 0; inputs[i]; i++) { + for (int m = 0; modes[m]; m++) { + char a[256], b[256]; + snprintf(a, sizeof a, "/tmp/wwd_%d_a", getpid()); + snprintf(b, sizeof b, "/tmp/wwd_%d_b", getpid()); + char cmd[2048]; + snprintf(cmd, sizeof cmd, "%s/wwdump %s %s/%s > %s 2>/dev/null", + bin, modes[m], cwd, inputs[i], a); + if (runwait(cmd) != 0) { + fprintf(stderr, "wwdump FAIL: %s %s\n", modes[m], inputs[i]); + unlink(a); + fail++; + continue; + } + snprintf(cmd, sizeof cmd, "%s/wwdump %s %s/%s > %s 2>/dev/null", + bin, modes[m], cwd, inputs[i], b); + runwait(cmd); + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + fprintf(stderr, "wwdump FAIL: cannot read dumps\n"); + free(ba); free(bb); + unlink(a); unlink(b); + fail++; + continue; + } + if (na == 0) { + fprintf(stderr, "wwdump FAIL: empty dump for %s %s\n", + modes[m], inputs[i]); + fail++; + } else if (na != nb || memcmp(ba, bb, na) != 0) { + fprintf(stderr, "wwdump FAIL: nondeterministic %s %s\n", + modes[m], inputs[i]); + fail++; + } + free(ba); free(bb); + unlink(a); unlink(b); + } + } + return fail ? -1 : 0; +} + +/* Probe 9: full bootstrap fixed-point. ww1 → ww2 → ww3 with + * cmp ww2.s == ww3.s and cmp ww2 == ww3 (byte-identical binaries). + * This is the textbook self-host gate: the compiler must reach a + * fixed point under iterated self-compilation. */ +static int +probe_bootstrap_fixed_point(const char *bin) +{ + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwfp_%d", getpid()); + mkdir(tmpdir, 0755); + char ww2s[512], ww2o[512], ww2e[512]; + char ww3s[512], ww3o[512], ww3e[512]; + char rt[1024], cmd[4096]; + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); + snprintf(ww2s, sizeof ww2s, "%s/ww2.s", tmpdir); + snprintf(ww2o, sizeof ww2o, "%s/ww2.o", tmpdir); + snprintf(ww2e, sizeof ww2e, "%s/ww2", tmpdir); + snprintf(ww3s, sizeof ww3s, "%s/ww3.s", tmpdir); + snprintf(ww3o, sizeof ww3o, "%s/ww3.o", tmpdir); + snprintf(ww3e, sizeof ww3e, "%s/ww3", tmpdir); + int fail = 0; + /* ww1 -> ww2 */ + snprintf(cmd, sizeof cmd, + "%s/wwdump_ww -c %s/selfhost/cmd/wwdump/main.combined.ww > %s 2>/dev/null", + bin, cwd, ww2s); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww1 self-compile\n"); fail++; goto cleanup; } + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s 2>/dev/null", bin, ww2o, ww2s); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: 6a ww2.s\n"); fail++; goto cleanup; } + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s %s 2>/dev/null", bin, ww2e, ww2o, rt); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: 6l ww2\n"); fail++; goto cleanup; } + /* ww2 -> ww3 */ + snprintf(cmd, sizeof cmd, + "%s -c %s/selfhost/cmd/wwdump/main.combined.ww > %s 2>/dev/null", + ww2e, cwd, ww3s); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww2 self-compile\n"); fail++; goto cleanup; } + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s 2>/dev/null", bin, ww3o, ww3s); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: 6a ww3.s\n"); fail++; goto cleanup; } + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s %s 2>/dev/null", bin, ww3e, ww3o, rt); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: 6l ww3\n"); fail++; goto cleanup; } + /* The fixed-point check: ww2.s == ww3.s, ww2 == ww3. */ + snprintf(cmd, sizeof cmd, "cmp -s %s %s", ww2s, ww3s); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww2.s != ww3.s (no fixed point)\n"); fail++; } + snprintf(cmd, sizeof cmd, "cmp -s %s %s", ww2e, ww3e); + if (runwait(cmd) != 0) { fprintf(stderr, "fp FAIL: ww2 != ww3 binaries\n"); fail++; } +cleanup: + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + return fail ? -1 : 0; +} + +/* Probe 8: ww1 → ww2 ladder. wwdump_ww (= ww1, built by C tools) + * compiles its own concatenated source. The output assembles + links + * via 6a/6l into ww2 (a fresh ww-built compiler binary). Confirms + * the full ww-cgen pipeline is functionally correct end-to-end on + * the entire frontend (lex+parse+check+cgen+ast+typ+sym+mem+err+ + * driver). Pre-fix-set this segfaulted at 5919 lines of asm; post- + * fix-set it produces 43K+ lines that assemble/link cleanly. */ +static int +probe_ww1_to_ww2(const char *bin) +{ + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/ww12_%d", getpid()); + mkdir(tmpdir, 0755); + char asm_p[512], obj_p[512], exe_p[512], cmd[4096]; + snprintf(asm_p, sizeof asm_p, "%s/ww2.s", tmpdir); + snprintf(obj_p, sizeof obj_p, "%s/ww2.o", tmpdir); + snprintf(exe_p, sizeof exe_p, "%s/ww2", tmpdir); + int fail = 0; + snprintf(cmd, sizeof cmd, + "%s/wwdump_ww -c %s/selfhost/cmd/wwdump/main.combined.ww > %s 2>/dev/null", + bin, cwd, asm_p); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww1->ww2 FAIL: wwdump_ww segfaulted on its own source\n"); + fail++; + goto cleanup; + } + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s 2>/dev/null", bin, obj_p, asm_p); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww1->ww2 FAIL: 6a errored on ww1 output\n"); + fail++; + goto cleanup; + } + char rt[1024]; + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s %s 2>/dev/null", + bin, exe_p, obj_p, rt); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww1->ww2 FAIL: 6l errored\n"); + fail++; + goto cleanup; + } + struct stat st; + if (stat(exe_p, &st) != 0 || !(st.st_mode & 0111)) { + fprintf(stderr, "ww1->ww2 FAIL: ww2 binary not executable\n"); + fail++; + } +cleanup: + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + return fail ? -1 : 0; +} + +/* Probe 7: end-to-end via wwdump_ww. Use `ww build` to produce the + * concatenated source as a side effect, then drive ww-cgen → 6a → + * 6l → run, comparing the binary's exit code to the fixture's + * declared expectation. This is the bootstrap-grade signal: it + * doesn't require ww-cgen output to be byte-identical to C-cgen, + * only to be functionally correct. The byte-match probe above is + * useful while it lasts but layout choices (slot allocation order, + * spill placement) eventually diverge between cgens; correctness is + * the test that scales. */ +static int +probe_ww_links(const char *bin) +{ + const char *fixtures[][2] = { + { "selfhost/test/sym_link.ww", "42" }, /* sym/typ/ast/mem */ + { NULL, NULL }, + }; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + int fail = 0; + for (int i = 0; fixtures[i][0]; i++) { + const char *fix = fixtures[i][0]; + int want = atoi(fixtures[i][1]); + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwlnk_%d_%d", getpid(), i); + mkdir(tmpdir, 0755); + const char *base = strrchr(fix, '/'); + base = base ? base + 1 : fix; + char stem[256]; + snprintf(stem, sizeof stem, "%s/%s", tmpdir, base); + char *dot = strrchr(stem, '.'); + if (dot) *dot = '\0'; + char tmpsrc[512]; + snprintf(tmpsrc, sizeof tmpsrc, "%s.ww", stem); + char cmd[4096]; + snprintf(cmd, sizeof cmd, "cp %s/%s %s", cwd, fix, tmpsrc); + runwait(cmd); + /* ww build to get the .combined.ww as a side effect. */ + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww build -I %s/selfhost/cmd/wwc %s >/dev/null 2>&1", + tmpdir, bin, cwd, tmpsrc); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-links FAIL: ww build %s\n", fix); + fail++; + goto cleanup; + } + /* Now compile the combined source via the ww frontend. */ + char combinedp[512], wsp[512], wop[512], wexep[512]; + snprintf(combinedp, sizeof combinedp, "%s.combined.ww", stem); + snprintf(wsp, sizeof wsp, "%s_w.s", stem); + snprintf(wop, sizeof wop, "%s_w.o", stem); + snprintf(wexep, sizeof wexep, "%s_w", stem); + snprintf(cmd, sizeof cmd, + "%s/wwdump_ww -c %s > %s 2>/dev/null", bin, combinedp, wsp); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-links FAIL: wwdump_ww -c %s\n", fix); + fail++; + goto cleanup; + } + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s 2>/dev/null", + bin, wop, wsp); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-links FAIL: 6a %s\n", fix); + fail++; + goto cleanup; + } + char rt[1024]; + snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s %s 2>/dev/null", + bin, wexep, wop, rt); + if (runwait(cmd) != 0) { + fprintf(stderr, "ww-links FAIL: 6l %s\n", fix); + fail++; + goto cleanup; + } + int rc = runwait(wexep); + if (rc != want) { + fprintf(stderr, "ww-links FAIL: %s exit=%d want=%d\n", + fix, rc, want); + fail++; + } +cleanup: + /* Sweep tmpdir contents, then remove. ww build also drops a + * C-built binary at — clean it too. */ + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + } + return fail ? -1 : 0; +} + +/* Probe 6: ww-cgen byte-matches C-cgen on every selfhost file that + * raw 6c can compile in isolation (no driver concatenation). The list + * is the convergence set: when this stays empty, the cmp ww2 ww3 + * ceiling becomes feasible. */ +static int +probe_cgen_match(const char *bin) +{ + const char *files[] = { + "selfhost/cmd/wwc/mem.ww", + "selfhost/cmd/wwc/err.ww", + "selfhost/cmd/wwc/tok.ww", + "selfhost/test/smoke.ww", + NULL, + }; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return -1; + int fail = 0; + for (int i = 0; files[i]; i++) { + char a[256], b[256], cmd[2048]; + snprintf(a, sizeof a, "/tmp/wwd_cm_%d_c", getpid()); + snprintf(b, sizeof b, "/tmp/wwd_cm_%d_w", getpid()); + snprintf(cmd, sizeof cmd, "%s/6c %s/%s > %s 2>/dev/null", + bin, cwd, files[i], a); + runwait(cmd); + snprintf(cmd, sizeof cmd, "%s/wwdump_ww -c %s/%s > %s 2>/dev/null", + bin, cwd, files[i], b); + runwait(cmd); + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + int rc1 = slurp(a, &bc, &nc); + int rc2 = slurp(b, &bw, &nw); + if (rc1 < 0 || rc2 < 0) { + fprintf(stderr, "cgen-match FAIL: read %s\n", files[i]); + fail++; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "cgen-match FAIL: %s — C %zu vs ww %zu bytes\n", + files[i], nc, nw); + fail++; + } + free(bc); free(bw); + unlink(a); unlink(b); + } + return fail ? -1 : 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + int fail = 0; + if (probe_codegen(bin) != 0) fail++; + if (probe_smoke(bin) != 0) fail++; + if (probe_dump_stable(bin) != 0) fail++; + if (probe_dump_diff(bin) != 0) fail++; + if (probe_resolve(bin) != 0) fail++; + if (probe_ww_compile(bin) != 0) fail++; + if (probe_cgen_match(bin) != 0) fail++; + if (probe_ww_links(bin) != 0) fail++; + if (probe_ww1_to_ww2(bin) != 0) fail++; + if (probe_bootstrap_fixed_point(bin) != 0) fail++; + + if (fail) { + fprintf(stderr, "selfhost: %d probe(s) failed\n", fail); + return 1; + } + printf("selfhost: codegen ok; smoke=42; wwdump stable; " + "ww lexer matches C on 8 fixtures (-t); " + "ww parser matches C on 32 fixtures (-a); " + "ww checker resolves 4 self-contained fixtures (-r); " + "ww cgen compiles + runs 13 ww programs (incl. compound -= regression); " + "ww cgen byte-matches C on 4 selfhost fixtures (smoke/mem/err/tok); " + "ww cgen builds runnable binaries from sym/typ/ast/mem stack; " + "ww1 -> ww2 -> ww3 fixed-point reached: ww2.s == ww3.s, ww2 == ww3 byte-identical\n"); + return 0; +} diff --git a/test/wwc/991_6a_ww.c b/test/wwc/991_6a_ww.c new file mode 100644 index 00000000..aeb1c308 --- /dev/null +++ b/test/wwc/991_6a_ww.c @@ -0,0 +1,135 @@ +/* + * 991_6a_ww — phase-10 marker for the ww-side 6a port. + * + * Diffs the C-built `6a` against the ww-built `6a_ww` on a corpus of + * .s files. Both must produce byte-identical ELF .o output. The corpus + * spans: + * - rt/X.s hand-written runtime; small, varied operands + * - generated .s from the existing selfhost cgen pipeline + * (wwdump/6a/6l main.s) - large, exercises + * every encoding path 6c emits + * + * Any divergence is a port bug in selfhost/cmd/6a/. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +diff_one(const char *bin, const char *src) +{ + char co[64], wo[64], cmd[2048]; + snprintf(co, sizeof co, "/tmp/wwa_%d_c.o", getpid()); + snprintf(wo, sizeof wo, "/tmp/wwa_%d_w.o", getpid()); + + snprintf(cmd, sizeof cmd, "%s/6a -o %s %s 2>/dev/null", bin, co, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "6a_ww FAIL: C 6a errored on %s\n", src); + unlink(co); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/6a_ww -o %s %s 2>/dev/null", bin, wo, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "6a_ww FAIL: ww 6a errored on %s\n", src); + unlink(co); unlink(wo); + return -1; + } + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + int rc = 0; + if (slurp(co, &bc, &nc) < 0 || slurp(wo, &bw, &nw) < 0) { + fprintf(stderr, "6a_ww FAIL: cannot read .o for %s\n", src); + rc = -1; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "6a_ww FAIL: %s — C %zu vs ww %zu bytes\n", + src, nc, nw); + rc = -1; + } + free(bc); free(bw); + unlink(co); unlink(wo); + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *files[] = { + /* hand-written runtime — small, covers MOVQ/LEAQ/CALL/SYSCALL/RET. */ + "rt/start.s", + "rt/syscall.s", + "rt/abort.s", + "rt/alloc.s", + "rt/streq.s", + /* generated .s from the existing selfhost stack — large, + * exercises every encoding 6c emits (the only way to hit + * the long tail of operand shapes). */ + "selfhost/cmd/wwdump/main.s", + "selfhost/cmd/6l/main.s", + "selfhost/cmd/6a/main.s", + NULL, + }; + int fail = 0; + int n = 0; + for (int i = 0; files[i]; i++) { + char p[2048]; + snprintf(p, sizeof p, "%s/%s", cwd, files[i]); + if (diff_one(bin, p) != 0) fail++; + n++; + } + if (fail) { + fprintf(stderr, "6a_ww: %d/%d diff(s) failed\n", fail, n); + return 1; + } + printf("6a_ww: byte-identical to C 6a on %d corpus files " + "(rt/*.s + selfhost generated main.s)\n", n); + return 0; +} diff --git a/test/wwc/992_6l_ww.c b/test/wwc/992_6l_ww.c new file mode 100644 index 00000000..1b0389b1 --- /dev/null +++ b/test/wwc/992_6l_ww.c @@ -0,0 +1,137 @@ +/* + * 992_6l_ww — phase-10 marker for the ww-side 6l port. + * + * Diffs the C-built `6l` against the ww-built `6l_ww` on a corpus of + * link inputs. Both must produce byte-identical static ELF binaries. + * The corpus mixes: + * - a single .o input (no archive resolution) + * - a .o + libwwrt.a link, exercising the SysV `ar` two-pass loader + * + * Any divergence is a port bug in selfhost/cmd/6l/. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +/* link `inputs` (space-separated, the caller's responsibility to quote) + * with both linkers, compare bytes. */ +static int +diff_one(const char *bin, const char *label, const char *inputs) +{ + char co[64], wo[64], cmd[4096]; + snprintf(co, sizeof co, "/tmp/wwl_%d_c", getpid()); + snprintf(wo, sizeof wo, "/tmp/wwl_%d_w", getpid()); + + snprintf(cmd, sizeof cmd, "%s/6l -o %s %s 2>/dev/null", bin, co, inputs); + if (runwait(cmd) != 0) { + fprintf(stderr, "6l_ww FAIL: C 6l errored on %s\n", label); + unlink(co); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/6l_ww -o %s %s 2>/dev/null", bin, wo, inputs); + if (runwait(cmd) != 0) { + fprintf(stderr, "6l_ww FAIL: ww 6l errored on %s\n", label); + unlink(co); unlink(wo); + return -1; + } + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + int rc = 0; + if (slurp(co, &bc, &nc) < 0 || slurp(wo, &bw, &nw) < 0) { + fprintf(stderr, "6l_ww FAIL: cannot read output for %s\n", label); + rc = -1; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "6l_ww FAIL: %s — C %zu vs ww %zu bytes\n", + label, nc, nw); + rc = -1; + } + free(bc); free(bw); + unlink(co); unlink(wo); + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + struct { + const char *label; + const char *inputs_fmt; /* %s = cwd */ + } cases[] = { + /* Real bootstrap-style links: every selfhost main.o paired + * with libwwrt.a. Exercises both the .o code path and the + * archive two-pass loader (rt_alloc / rt_syscall / rt_abort + * are pulled from libwwrt.a as undefs are encountered). */ + { "wwdump+libwwrt", + "%s/selfhost/cmd/wwdump/main.o %s/out/lib/libwwrt.a" }, + { "6a+libwwrt", + "%s/selfhost/cmd/6a/main.o %s/out/lib/libwwrt.a" }, + { "6l+libwwrt", + "%s/selfhost/cmd/6l/main.o %s/out/lib/libwwrt.a" }, + { NULL, NULL }, + }; + + int fail = 0; + int n = 0; + for (int i = 0; cases[i].label; i++) { + char inputs[2048]; + snprintf(inputs, sizeof inputs, cases[i].inputs_fmt, cwd, cwd); + if (diff_one(bin, cases[i].label, inputs) != 0) fail++; + n++; + } + if (fail) { + fprintf(stderr, "6l_ww: %d/%d diff(s) failed\n", fail, n); + return 1; + } + printf("6l_ww: byte-identical to C 6l on %d corpus links " + "(.o + libwwrt.a archive resolution)\n", n); + return 0; +} diff --git a/test/wwc/993_ww_ww.c b/test/wwc/993_ww_ww.c new file mode 100644 index 00000000..a7406032 --- /dev/null +++ b/test/wwc/993_ww_ww.c @@ -0,0 +1,177 @@ +/* + * 993_ww_ww — phase-10 marker for the ww-side driver port. + * + * Diffs the C-built `ww` against the ww-built `ww_ww`. Both drivers + * orchestrate the same 6c/6a/6l, so the resulting binaries must be + * byte-identical for every program in the corpus. The corpus mixes: + * - a tiny single-file program (no -I flags) + * - a multi-import build (selfhost/cmd/wwdump/main.ww), exercising + * `use` resolution + the -I search path + the libwwrt.a link. + * + * Each build runs from a per-driver scratch directory so the + * intermediate .combined.ww/.s/.o files don't collide. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +/* Build `src` via the named driver, expecting output binary `out` in + * the build directory. Returns 0 on success. */ +static int +build_via(const char *bin, const char *driver, const char *src, + const char *workdir, const char *includes_a, const char *includes_b) +{ + char cmd[4096]; + if (includes_b && includes_b[0]) { + snprintf(cmd, sizeof cmd, + "cd %s && %s/%s build -I %s -I %s %s 2>/dev/null", + workdir, bin, driver, includes_a, includes_b, src); + } else if (includes_a && includes_a[0]) { + snprintf(cmd, sizeof cmd, + "cd %s && %s/%s build -I %s %s 2>/dev/null", + workdir, bin, driver, includes_a, src); + } else { + snprintf(cmd, sizeof cmd, + "cd %s && %s/%s build %s 2>/dev/null", + workdir, bin, driver, src); + } + return runwait(cmd); +} + +static int +diff_one(const char *bin, const char *cwd, const char *label, + const char *src, const char *out_basename, + const char *inc_a, const char *inc_b) +{ + char dc[64], dw[64]; + snprintf(dc, sizeof dc, "/tmp/ww_d_%d_c", getpid()); + snprintf(dw, sizeof dw, "/tmp/ww_d_%d_w", getpid()); + + char cmd[256]; + snprintf(cmd, sizeof cmd, "rm -rf %s %s && mkdir -p %s %s", dc, dw, dc, dw); + if (runwait(cmd) != 0) return -1; + + if (build_via(bin, "ww", src, dc, inc_a, inc_b) != 0) { + fprintf(stderr, "ww_ww FAIL: C ww errored on %s\n", label); + return -1; + } + if (build_via(bin, "ww_ww", src, dw, inc_a, inc_b) != 0) { + fprintf(stderr, "ww_ww FAIL: ww ww errored on %s\n", label); + return -1; + } + + char co[1024], wo[1024]; + snprintf(co, sizeof co, "%s/%s", dc, out_basename); + snprintf(wo, sizeof wo, "%s/%s", dw, out_basename); + + int rc = 0; + if (slurp_eq(co, wo) != 0) { + fprintf(stderr, "ww_ww FAIL: %s — driver outputs differ\n", label); + rc = -1; + } + snprintf(cmd, sizeof cmd, "rm -rf %s %s", dc, dw); + runwait(cmd); + (void)cwd; + return rc; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + /* Stage a tiny program in a place both drivers can reach. */ + { + FILE *f = fopen("/tmp/ww_d_hello.ww", "w"); + if (!f) return 1; + fputs("use os;\n\n" + "export fn main() i32 = {\n" + "\tos.write(1, \"hi\\n\".ptr, 3u64);\n" + "\treturn 0;\n" + "};\n", f); + fclose(f); + } + + struct { + const char *label; + const char *src; /* may be relative to cwd */ + const char *out; /* basename of expected output */ + const char *inc_a; + const char *inc_b; + } cases[] = { + { "hello", "/tmp/ww_d_hello.ww", "ww_d_hello", "", "" }, + { "wwdump", NULL, "main", NULL, NULL }, /* filled in below */ + { NULL, NULL, NULL, NULL, NULL }, + }; + + /* wwdump case: absolute paths so the driver finds the imports + * regardless of the per-driver workdir. */ + static char wwdump_src[2048], wwdump_inc_a[2048], wwdump_inc_b[2048]; + snprintf(wwdump_src, sizeof wwdump_src, "%s/selfhost/cmd/wwdump/main.ww", cwd); + snprintf(wwdump_inc_a, sizeof wwdump_inc_a, "%s/selfhost/cmd/wwc", cwd); + wwdump_inc_b[0] = '\0'; + cases[1].src = wwdump_src; + cases[1].inc_a = wwdump_inc_a; + cases[1].inc_b = wwdump_inc_b; + + int fail = 0; + int n = 0; + for (int i = 0; cases[i].label; i++) { + if (diff_one(bin, cwd, cases[i].label, cases[i].src, + cases[i].out, cases[i].inc_a, cases[i].inc_b) != 0) + fail++; + n++; + } + unlink("/tmp/ww_d_hello.ww"); + if (fail) { + fprintf(stderr, "ww_ww: %d/%d diff(s) failed\n", fail, n); + return 1; + } + printf("ww_ww: byte-identical to C ww on %d corpus builds " + "(single-file + selfhost/wwdump multi-import)\n", n); + return 0; +}