ww: import toolchain — C bootstrap + ww-side self-host (phases 0-10)

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.
This commit is contained in:
2026-05-11 02:17:47 +09:00
parent 4c8fc59ca1
commit 1657bdeda3
106 changed files with 35654 additions and 15 deletions

12
.gitignore vendored Normal file
View File

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

View File

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

269
Makefile Normal file
View File

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

107
cmd/6a/a.h Normal file
View File

@@ -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 <stddef.h>
#include <stdint.h>
#include <stdio.h>
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

689
cmd/6a/asm.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
#include <stdio.h>
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;
}

31
cmd/6a/lex.c Normal file
View File

@@ -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 <ctype.h>
#include <stdlib.h>
#include <string.h>
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);
}

59
cmd/6a/main.c Normal file
View File

@@ -0,0 +1,59 @@
/*
* 6a — amd64 assembler driver. Read .s, parse, encode, emit ELF .o.
*/
#include "a.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

242
cmd/6a/obj.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
#include <stdio.h>
/* 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;
}

405
cmd/6a/parse.c Normal file
View File

@@ -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 <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
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;
}

107
cmd/6c/6.out.h Normal file
View File

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

2622
cmd/6c/cgen.c Normal file

File diff suppressed because it is too large Load Diff

56
cmd/6c/gc.h Normal file
View File

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

93
cmd/6c/main.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

8
cmd/6c/peep.c Normal file
View File

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

9
cmd/6c/reg.c Normal file
View File

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

8
cmd/6c/swt.c Normal file
View File

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

189
cmd/6c/txt.c Normal file
View File

@@ -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<framesize>
* 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 <string.h>
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);
}
}
}

74
cmd/6l/l.h Normal file
View File

@@ -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 <stddef.h>
#include <stdint.h>
#include <stdio.h>
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

73
cmd/6l/main.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

324
cmd/6l/obj.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
#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 "!<arch>\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 "!<arch>\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, "!<arch>\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;
}

89
cmd/6l/out.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

63
cmd/6l/pass.c Normal file
View File

@@ -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 <stdio.h>
#include <string.h>
#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;
}

27
cmd/6l/sym.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
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;
}

349
cmd/ww/main.c Normal file
View File

@@ -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 <runtime.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 <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <libgen.h>
static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n"
" build <path> compile module to a static binary\n"
" run <path> build then exec\n"
" test <path> build and run module tests\n"
" fmt <path> 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 <dir>/X.ww then <dir>/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;
}

189
cmd/wwc/ast.c Normal file
View File

@@ -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 <string.h>
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);
}

945
cmd/wwc/check.c Normal file
View File

@@ -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 <string.h>
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;
}
}
}

76
cmd/wwc/err.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
Pos noPos = { "<none>", 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++;
}

476
cmd/wwc/lex.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
#include <errno.h>
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;
}

117
cmd/wwc/mem.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
#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;
}
}

1183
cmd/wwc/parse.c Normal file

File diff suppressed because it is too large Load Diff

74
cmd/wwc/sym.c Normal file
View File

@@ -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 <string.h>
#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;
}

199
cmd/wwc/tok.c Normal file
View File

@@ -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 <string.h>
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 "<none>";
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 "<last>";
}
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 : "<none>", 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);
}

370
cmd/wwc/type.c Normal file
View File

@@ -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 <string.h>
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, "<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 "<nil>";
switch (t->kind) {
case TY_NONE: return "<none>";
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 "<err>";
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 : "<named>";
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 "?";
}

462
cmd/wwc/ww.h Normal file
View File

@@ -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 <stddef.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
/* 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 */

119
cmd/wwdump/main.c Normal file
View File

@@ -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: "<file>:<l>:<c> <kind> [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 <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

92
lib/ascii/ascii.ww Normal file
View File

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

82
lib/bufio/bufio.ww Normal file
View File

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

51
lib/bytes/bytes.ww Normal file
View File

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

22
lib/c/libc/libc.ww Normal file
View File

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

19
lib/encoding/hex/hex.ww Normal file
View File

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

14
lib/encoding/utf8/utf8.ww Normal file
View File

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

30
lib/errors/errors.ww Normal file
View File

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

67
lib/fmt/fmt.ww Normal file
View File

@@ -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 "<file>:<line>:<col>: <msg>\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);
};

15
lib/hash/fnv/fnv.ww Normal file
View File

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

28
lib/io/stream.ww Normal file
View File

@@ -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);
};

48
lib/net/net.ww Normal file
View File

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

171
lib/os/os.ww Normal file
View File

@@ -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 <fcntl.h>.
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 <unistd.h> (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;
};

15
lib/path/path.ww Normal file
View File

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

49
lib/slices/slices.ww Normal file
View File

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

27
lib/sort/sort.ww Normal file
View File

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

119
lib/strconv/strconv.ww Normal file
View File

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

95
lib/strings/strings.ww Normal file
View File

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

15
lib/time/time.ww Normal file
View File

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

48
lib/types/types.ww Normal file
View File

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

17
rt/abort.s Normal file
View File

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

24
rt/alloc.s Normal file
View File

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

16
rt/start.s Normal file
View File

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

27
rt/streq.s Normal file
View File

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

16
rt/syscall.s Normal file
View File

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

681
selfhost/cmd/6a/asm.ww Normal file
View File

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

66
selfhost/cmd/6a/lex.ww Normal file
View File

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

File diff suppressed because it is too large Load Diff

110
selfhost/cmd/6a/main.ww Normal file
View File

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

293
selfhost/cmd/6a/obj.ww Normal file
View File

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

583
selfhost/cmd/6a/parse.ww Normal file
View File

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

193
selfhost/cmd/6a/types.ww Normal file
View File

@@ -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,
};

File diff suppressed because it is too large Load Diff

120
selfhost/cmd/6l/main.ww Normal file
View File

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

486
selfhost/cmd/6l/obj.ww Normal file
View File

@@ -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 "!<arch>\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;
};

91
selfhost/cmd/6l/out.ww Normal file
View File

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

64
selfhost/cmd/6l/pass.ww Normal file
View File

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

75
selfhost/cmd/6l/sym.ww Normal file
View File

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

View File

@@ -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 <fcntl.h>.
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 <unistd.h> (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 <dir>/<name>.ww then <dir>/<name>/<name>.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: <dir>/<name>.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: <dir>/<name>/<name>.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: <self_dir>/../../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: <self_dir>/../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 <stem>.s <stem>.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 <stem>.o <stem>.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 <out> <stem>.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] <subcommand> [args...]\n -V print version and exit\n build <path> compile module to a static binary\n run <path> 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 <dir>
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_<pid> 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;
};

695
selfhost/cmd/ww/main.ww Normal file
View File

@@ -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 <dir>/<name>.ww then <dir>/<name>/<name>.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: <dir>/<name>.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: <dir>/<name>/<name>.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: <self_dir>/../../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: <self_dir>/../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 <stem>.s <stem>.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 <stem>.o <stem>.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 <out> <stem>.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] <subcommand> [args...]\n -V print version and exit\n build <path> compile module to a static binary\n run <path> 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 <dir>
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_<pid> 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;
};

335
selfhost/cmd/wwc/ast.ww Normal file
View File

@@ -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);
};

2868
selfhost/cmd/wwc/cgen.ww Normal file

File diff suppressed because it is too large Load Diff

247
selfhost/cmd/wwc/check.ww Normal file
View File

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

37
selfhost/cmd/wwc/err.ww Normal file
View File

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

656
selfhost/cmd/wwc/lex.ww Normal file
View File

@@ -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);
};

106
selfhost/cmd/wwc/mem.ww Normal file
View File

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

960
selfhost/cmd/wwc/parse.ww Normal file
View File

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

113
selfhost/cmd/wwc/sym.ww Normal file
View File

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

394
selfhost/cmd/wwc/tok.ww Normal file
View File

@@ -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 "<none>"; };
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 "<last>"; };
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.
// "<file>:<line>:<col> <kindname>[ <value>]\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, "<none>");
};
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'
};

329
selfhost/cmd/wwc/typ.ww Normal file
View File

@@ -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, "<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
};

File diff suppressed because it is too large Load Diff

156
selfhost/cmd/wwdump/main.ww Normal file
View File

@@ -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)
// "<file>: <resolved>/<resolved+unresolved> 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;
};

View File

@@ -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 <fcntl.h>.
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 <unistd.h> (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;
};

163
selfhost/test/smoke.ww Normal file
View File

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

45
selfhost/test/sym_link.ww Normal file
View File

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

29
selfhost/test/uses.ww Normal file
View File

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

66
test/run Executable file
View File

@@ -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/<NNN>_<name>.c → out/bin/test_<name> binary, run it.
# test/lang/<phase>/*.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 <NNN>_
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/<phase>/*.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: <N>` 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"

87
test/wwc/000_smoke.c Normal file
View File

@@ -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 <stdlib.h>
#include <string.h>
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;
}

142
test/wwc/100_lex.c Normal file
View File

@@ -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 <string.h>
#include <stdlib.h>
#include <stdio.h>
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, "<test>", 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;
}

190
test/wwc/200_parse.c Normal file
View File

@@ -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 <stdio.h>
#include <string.h>
#include <stdlib.h>
static int
must_parse(const char *src)
{
Arena *a = newarena();
Lex l;
Parser p;
lexinit(&l, a, "<test>", 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, "<test>", 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;
}

126
test/wwc/300_check.c Normal file
View File

@@ -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 <stdio.h>
#include <string.h>
#include <stdlib.h>
static int
runrow(const char *src, const char *expect)
{
Arena *a = newarena();
Lex l;
Parser p;
Checker c;
lexinit(&l, a, "<test>", 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;
}

90
test/wwc/400_6c.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;
}

62
test/wwc/500_6a.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;
}

67
test/wwc/600_6l.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;
}

96
test/wwc/610_arch.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}

863
test/wwc/700_e2e.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}

Some files were not shown because too many files have changed in this diff Show More