examples: lisp — drive wwstage by default; retire stale workarounds
Makefile sets WW_W6C=$(BIN)/w6c_ww so `make`, `make test`, and `make demo` all use the ww-built backend. With the wwstage cgen fixes in selfhost/cmd/wcc/ the demo no longer needs to dodge: - bug #1+#2 (global addressing): symbol interner indexes sym_off / sym_len / sym_blob directly. No `let blob = sym_blob;` aliasing. - bug #3 (chained non-pointer sub-struct field): not retired here (lexer.cur is still flattened) but the cgen now handles the shape; un-flattening is cosmetic. - bug #4 (f64 through every boundary): vfloat writes p.fval = v directly; promote_v's FLOAT branch is one assign; to_f64 reads v.ival as f64 / v.fval directly. Drops fbuf, FVAL_OFF, copybytes. - bug #7 (xs[i].field): builtins write xs[0].kind / xs[0].car directly; no `let p = xs[0];` first. lisp_test still 101/101. CLAUDE.md marks each historical bug as retired or still load- bearing; #6 (slice-len in tagged-union return) and #8 (f64 compound assign) are the remaining shapes to avoid.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
examples/lisp — tiny Lisp interpreter in pure ww. Demo program; not a
|
examples/lisp — tiny Lisp interpreter in pure ww. Demo program; not a
|
||||||
production interpreter. Treat the files below as a worked example of
|
production interpreter. Drives the wwstage cgen (`out/bin/w6c_ww`) by
|
||||||
"what does and doesn't lower cleanly through the wwstage cgen today",
|
default; the Makefile sets `WW_W6C` so `make` picks the ww-built
|
||||||
not as a reference Lisp implementation.
|
backend rather than the C bootstrap one.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ not as a reference Lisp implementation.
|
|||||||
eval, apply, printer, REPL. Everything the entry
|
eval, apply, printer, REPL. Everything the entry
|
||||||
point and the test driver consume is `export`-ed.
|
point and the test driver consume is `export`-ed.
|
||||||
- `lisp.ww` entry point; `use lispcore;` + `main()`.
|
- `lisp.ww` entry point; `use lispcore;` + `main()`.
|
||||||
- `lisp_test.ww` in-process test driver (72 probes). Built as a
|
- `lisp_test.ww` in-process test driver (101 probes). Built as a
|
||||||
standalone binary, exec'd directly — `ww test`
|
standalone binary, exec'd directly — `ww test`
|
||||||
drops `-I` in single-file mode, so the Makefile
|
drops `-I` in single-file mode, so the Makefile
|
||||||
runs the binary itself.
|
runs the binary itself.
|
||||||
@@ -24,86 +24,66 @@ test — use the bare names.
|
|||||||
|
|
||||||
## wwstage cgen workarounds at play
|
## wwstage cgen workarounds at play
|
||||||
|
|
||||||
Every workaround below is documented inline at its use site. The
|
The wwstage cgen has been brought up to par on the bug classes this
|
||||||
shape of the bug is what matters, not the specific symptom — the
|
demo originally exercised. The remaining workarounds and the
|
||||||
same bug class shows up in any new code that hits the same pattern.
|
historical bugs they sidestepped are listed below; "(retired)" marks
|
||||||
|
items the cgen now handles natively, kept here only as a record of
|
||||||
|
the shape so a regression is easy to recognise.
|
||||||
|
|
||||||
1. **Top-level `[N]T` arrays mis-address inside functions.**
|
1. **(retired) Top-level `[N]T` arrays mis-address inside functions.**
|
||||||
`globalarr[i] = …` lowers to `LEAQ (BP), BX` instead of
|
`globalarr[i]` used to lower to `LEAQ (BP), BX`. cgindex/cgassign
|
||||||
`LEAQ globalarr(SB), BX` — the global address gets treated as a
|
now detect top-level array idents and emit `LEAQ name(SB), BX`,
|
||||||
stack frame, corrupting both. Heap-allocate via `os.alloc` into a
|
and emitletdataw lays the array bytes into DATAW.
|
||||||
`*T` and alias to a local at the top of each function before
|
|
||||||
indexing. See `sym_off` / `sym_blob` / `obuf`.
|
|
||||||
|
|
||||||
2. **`global_ptr[i]` mis-addresses too.** Even after switching to a
|
2. **(retired) `global_ptr[i]` mis-addresses.** cgindex/cgassign
|
||||||
pointer global, `MOVQ global_ptr(SB), BX` is replaced with
|
detect top-level `*T` idents and emit `MOVQ name(SB), BX` with
|
||||||
`MOVQ (BP), BX`. Alias `let p = global_ptr;` at function entry,
|
the correct element scaling. `lispcore`'s symbol interner is back
|
||||||
then `p[i]`. See every helper that touches the symbol interner.
|
to plain `sym_blob[off + i]` style; no `let blob: *u8 = sym_blob`
|
||||||
|
aliasing.
|
||||||
|
|
||||||
3. **Two-level field write through a non-pointer sub-struct.**
|
3. **(retired) Two-level field write through a non-pointer
|
||||||
`L.cur.kind = k` or `L.src.ptr = buf.ptr` (where `cur`/`src` is a
|
sub-struct.** cgassign/cgdot now handle the chained
|
||||||
non-pointer struct field of a struct reached through a pointer) is
|
`(*L).cur.kind` shape both as read and write. The lexer keeps the
|
||||||
silently dropped — the function body emits no store. Either
|
flattened `curkind` / `curival` / … fields for now because every
|
||||||
flatten the sub-struct (see `lexer.curkind`/`curival`/…) or build
|
call site uses them; un-flattening is a stylistic improvement, not
|
||||||
the whole sub-struct as a local and do a single whole-struct
|
a correctness fix.
|
||||||
assign (`let s: str = …; L.src = s;`).
|
|
||||||
|
|
||||||
4. **f64 through every boundary is unreliable.** The cgen routes f64
|
4. **(partially retired) f64 through every boundary.** Struct-field
|
||||||
stores via integer registers; in most paths AX gets stored where
|
`p.fval = v` through `*T`, `*p = v` for `*f64`, and `alloc(T{…})`
|
||||||
X0 should have been written.
|
sugar for f64 fields all route through X0 now (`vfloat`,
|
||||||
- `alloc(value{ fval = v })`: writes the kind enum (AX still
|
`promote_v`, `to_f64` are back to direct `p.fval = …` /
|
||||||
holds it) instead of the f64.
|
`*out = v.fval` form). Function-arg passing of an f64 struct-field
|
||||||
- `p.fval = v` through a `*T`: same — stores AX.
|
value also works (the cgen's `exprfloatkind` now recognises
|
||||||
- `*p = v` for `*f64`: stores AX.
|
`p.field` whose declared type is f64/f32).
|
||||||
- `func(f64_arg)` where the source is a struct field: cgen does
|
|
||||||
`MOVQ off(BX), AX` (integer load) and never puts the bits in X0.
|
|
||||||
|
|
||||||
What does work: `MOVSD X0, global(SB)` (a top-level f64 global)
|
5. **(retired) `alloc(value{ text = s })` writes only `s.ptr`.** Both
|
||||||
and `MOVSD X0, off(BP)` (a local f64 slot). The interpreter
|
the `alloc(T{…})` builtin and the bare struct-literal init now
|
||||||
threads f64 through a scratch global (`fbuf`) and byte-copies
|
emit both halves of the str. `vstr` still does manual
|
||||||
8 bytes wherever a `*f64` would normally suffice. See `vfloat`,
|
`p.text = s` (semantically equivalent, no longer required).
|
||||||
`to_f64`, `copybytes`.
|
|
||||||
|
|
||||||
5. **`alloc(value{ text = s })` writes only `s.ptr`.** The cgen sets
|
6. **`(slice | E)` tagged-union returns drop `slice.len`.** Still
|
||||||
up `s.ptr` in AX, sets the new-pointer in BX, then needs `s.len`
|
present. The wwstage return convention is AX=tag, DX=payload1,
|
||||||
in another reg — but the same BX gets clobbered by the new-pointer
|
CX=payload2 — a 24-byte slice header doesn't fit. Inline the
|
||||||
reload, so the `.len` store never happens. Manual init via
|
slice-building loop into the caller. See `eval`'s argument-eval
|
||||||
`p.text = s` writes both halves correctly.
|
inline.
|
||||||
|
|
||||||
6. **`(slice | E)` tagged-union returns drop `slice.len`.** The
|
7. **(retired) `xs[i].kind` drops the trailing field load.** cgdot
|
||||||
wwstage return convention is AX=tag, DX=payload1, CX=payload2. A
|
now handles N_INDEX bases. Builtins are back to
|
||||||
slice header is 24 bytes (ptr/len/cap); only ptr and cap come
|
`xs[0].kind` / `xs[0].car` directly — no `let p = xs[0];` first.
|
||||||
through in DX/CX. The intermediate `BX = s.len` is loaded but
|
|
||||||
never moved into a return register. Inline the slice-building
|
|
||||||
loop into the caller instead of factoring it into a helper. See
|
|
||||||
`eval`'s argument-eval inline.
|
|
||||||
|
|
||||||
7. **`xs[i].kind` drops the trailing field load.** Field access on a
|
|
||||||
slice element gives back only the bytes at `&xs[i]` — the cgen
|
|
||||||
doesn't chain the dereference. Bind to a local first:
|
|
||||||
`let p = xs[i]; if (p.kind …)`. See every builtin.
|
|
||||||
|
|
||||||
8. **f64 compound assigns are mis-lowered to `acc = d` (no OP).**
|
8. **f64 compound assigns are mis-lowered to `acc = d` (no OP).**
|
||||||
`acc += f` / `acc /= f` etc. on f64 locals drop the operator.
|
Still present. Write the explicit form: `acc = acc + f`. Integer
|
||||||
Write the explicit form: `acc = acc + f` / `acc = acc / f`.
|
compound assigns work fine.
|
||||||
Integer compound assigns work fine, so `acc += i` on `i64`
|
|
||||||
stays as-is.
|
|
||||||
|
|
||||||
9. **`let r = call(); foreign_call(); return r?;` corrupts `r` when
|
9. **(retired in practice) `let r = call(); foreign_call(); return
|
||||||
the call returned a tagged union.** The (`tag`, `payload1`,
|
r?;` corrupts `r`.** The wwstage cgen now spills the AX/DX/CX
|
||||||
`payload2`) triple sits in AX/DX/CX after the call, and the
|
triple to the local's 24-byte slot at the assignment point,
|
||||||
foreign call between capture and `?`-unwrap clobbers at least one
|
matching what cstage emits — the foreign call in between no longer
|
||||||
register before the cgen has spilled it to the local slot.
|
clobbers an unspilled half. We still pre-match the union inline in
|
||||||
Symptom: a `(*value | rterror)` whose `rterror` carries a string
|
eval's BUILTIN apply path so the `os.free` happens *after*
|
||||||
literal prints with a `str.len` of tens of thousands. Workaround:
|
classification rather than after the unwrap.
|
||||||
`match` the union inline before the foreign call and let each arm
|
|
||||||
return its own typed result. See eval's BUILTIN apply path
|
|
||||||
(where we free the args slice after `apply_builtin`).
|
|
||||||
|
|
||||||
If a new function "should work but acts weird", the bug is almost
|
If a new function "should work but acts weird", check #6 first.
|
||||||
always one of the above and shows up under valgrind/gdb the same
|
|
||||||
way it did the first time: silently dropped store, missing field
|
|
||||||
read, garbage payload after a tagged-union return.
|
|
||||||
|
|
||||||
## Tail-call optimization
|
## Tail-call optimization
|
||||||
|
|
||||||
@@ -144,8 +124,12 @@ env where `od?` is unbound; the tie-back only adds the self-binding.
|
|||||||
`(fib 25)` would still grow trans linearly until the form returns.
|
`(fib 25)` would still grow trans linearly until the form returns.
|
||||||
- **No bigints.** `i64` wraps silently on overflow. `(fact 21)`
|
- **No bigints.** `i64` wraps silently on overflow. `(fact 21)`
|
||||||
rolls over.
|
rolls over.
|
||||||
- **Float printing is fixed `%.6f`.** `1.0` prints as `1.000000`.
|
- **Float printing is 6-digit fixed-point.** The printer delegates
|
||||||
Strconv has no `ftos` yet.
|
to `strconv.f64tos`, which trims trailing zeros and the trailing
|
||||||
|
'.' (`1.0` → `1`, `1.5` → `1.5`, `0.1` → `0.1`). It does not yet
|
||||||
|
emit scientific notation or detect NaN/Inf — magnitudes ≥ 9e18
|
||||||
|
print as `huge`. Graduate-to-Ryū requires `f64`↔`u64` bit-
|
||||||
|
reinterpret in cgen.
|
||||||
- **String escapes are accepted but not translated.** `"\n"` in
|
- **String escapes are accepted but not translated.** `"\n"` in
|
||||||
source lands as the two bytes `\\` + `n`, not a newline.
|
source lands as the two bytes `\\` + `n`, not a newline.
|
||||||
- **No `(load)` / file I/O builtins.** Programs come in via stdin.
|
- **No `(load)` / file I/O builtins.** Programs come in via stdin.
|
||||||
|
|||||||
@@ -15,11 +15,18 @@
|
|||||||
# (otherwise `-I.` collapses to ".", and the cgen mangles symbols as
|
# (otherwise `-I.` collapses to ".", and the cgen mangles symbols as
|
||||||
# `..helper`, which then breaks the assembler).
|
# `..helper`, which then breaks the assembler).
|
||||||
|
|
||||||
WW := $(shell cd ../..; pwd)/out/bin/ww
|
# Drive the wwstage cgen (w6c_ww) by default. The C-stage cgen has
|
||||||
HERE := $(shell pwd)
|
# unfixed silent-miscompilation traps that lispcore used to dodge by
|
||||||
|
# hand; with the wwstage fixes in place we can write the natural
|
||||||
|
# shape (no flattened sub-structs, no aliased global indexers, no
|
||||||
|
# `let p = xs[i];` per builtin) and lisp_test still passes.
|
||||||
|
WW := $(shell cd ../..; pwd)/out/bin/ww
|
||||||
|
W6CWW := $(shell cd ../..; pwd)/out/bin/w6c_ww
|
||||||
|
HERE := $(shell pwd)
|
||||||
|
WWENV := WW_W6C=$(W6CWW)
|
||||||
|
|
||||||
lisp: lisp.ww lispcore.ww
|
lisp: lisp.ww lispcore.ww
|
||||||
$(WW) build lisp.ww -I $(HERE)
|
$(WWENV) $(WW) build lisp.ww -I $(HERE)
|
||||||
|
|
||||||
# `ww test <file.ww>` in single-file mode discards extra args, so we
|
# `ww test <file.ww>` in single-file mode discards extra args, so we
|
||||||
# can't pass `-I` through it. Build the test as a normal binary and
|
# can't pass `-I` through it. Build the test as a normal binary and
|
||||||
@@ -28,7 +35,7 @@ test: lisp_test lisp
|
|||||||
./lisp_test
|
./lisp_test
|
||||||
|
|
||||||
lisp_test: lisp_test.ww lispcore.ww
|
lisp_test: lisp_test.ww lispcore.ww
|
||||||
$(WW) build lisp_test.ww -I $(HERE)
|
$(WWENV) $(WW) build lisp_test.ww -I $(HERE)
|
||||||
|
|
||||||
# Demo programs in tree. `make demo` runs every test_*.lisp through
|
# Demo programs in tree. `make demo` runs every test_*.lisp through
|
||||||
# the REPL; each prints its results to stdout (errors go to stderr
|
# the REPL; each prints its results to stdout (errors go to stderr
|
||||||
|
|||||||
@@ -263,22 +263,10 @@ export fn vint(v: i64) *value = {
|
|||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
// f64 → struct-field via a *T pointer is mis-lowered (stores AX instead
|
|
||||||
// of MOVSD from X0). MOVSD into a top-level f64 global works, so route
|
|
||||||
// the bits through a scratch global and copy them as bytes.
|
|
||||||
def FVAL_OFF: u64 = 16u64;
|
|
||||||
let fbuf: f64 = 0.0;
|
|
||||||
|
|
||||||
fn vfloat(v: f64) *value = {
|
fn vfloat(v: f64) *value = {
|
||||||
fbuf = v;
|
|
||||||
let p: *value = arena_alloc(VALUE_SZ): *value;
|
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||||
p.kind = valkind.FLOAT;
|
p.kind = valkind.FLOAT;
|
||||||
let raw: *u8 = p: *u8;
|
p.fval = v;
|
||||||
let dst: *u8 = raw + FVAL_OFF;
|
|
||||||
let srcp: *f64 = &fbuf;
|
|
||||||
let src: *u8 = srcp: *u8;
|
|
||||||
let i: u64 = 0u64;
|
|
||||||
for (i < 8u64) { dst[i] = src[i]; i += 1u64; };
|
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -371,29 +359,20 @@ let SID_LET: i32 = 0;
|
|||||||
let SID_BEGIN: i32 = 0;
|
let SID_BEGIN: i32 = 0;
|
||||||
let SID_SETBANG: i32 = 0;
|
let SID_SETBANG: i32 = 0;
|
||||||
|
|
||||||
// streqid / intern / symname all alias the global *T pointers into
|
|
||||||
// locals before indexing. The wwstage cgen mis-lowers `global[i]` when
|
|
||||||
// `global` is a pointer-typed top-level let: it reads BP+0 instead of
|
|
||||||
// `LEAQ global(SB)`. Through a local the address path is correct.
|
|
||||||
|
|
||||||
fn streqid(a: str, off: i32, len: i32) bool = {
|
fn streqid(a: str, off: i32, len: i32) bool = {
|
||||||
if (a.len != len) { return false; };
|
if (a.len != len) { return false; };
|
||||||
let blob: *u8 = sym_blob;
|
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
for (i < len) {
|
for (i < len) {
|
||||||
if (a[i] != blob[off + i]) { return false; };
|
if (a[i] != sym_blob[off + i]) { return false; };
|
||||||
i += 1;
|
i += 1;
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export fn intern(s: str) i32 = {
|
export fn intern(s: str) i32 = {
|
||||||
let offt: *i32 = sym_off;
|
|
||||||
let lent: *i32 = sym_len;
|
|
||||||
let blob: *u8 = sym_blob;
|
|
||||||
let i: i32 = 0;
|
let i: i32 = 0;
|
||||||
for (i < sym_count) {
|
for (i < sym_count) {
|
||||||
if (streqid(s, offt[i], lent[i])) { return i; };
|
if (streqid(s, sym_off[i], sym_len[i])) { return i; };
|
||||||
i += 1;
|
i += 1;
|
||||||
};
|
};
|
||||||
os.assert(sym_count < SYM_CAP, "sym table full");
|
os.assert(sym_count < SYM_CAP, "sym table full");
|
||||||
@@ -401,11 +380,11 @@ export fn intern(s: str) i32 = {
|
|||||||
let off: i32 = sym_blobuse;
|
let off: i32 = sym_blobuse;
|
||||||
let j: i32 = 0;
|
let j: i32 = 0;
|
||||||
for (j < s.len) {
|
for (j < s.len) {
|
||||||
blob[off + j] = s[j];
|
sym_blob[off + j] = s[j];
|
||||||
j += 1;
|
j += 1;
|
||||||
};
|
};
|
||||||
offt[sym_count] = off;
|
sym_off[sym_count] = off;
|
||||||
lent[sym_count] = s.len;
|
sym_len[sym_count] = s.len;
|
||||||
let id: i32 = sym_count;
|
let id: i32 = sym_count;
|
||||||
sym_count += 1;
|
sym_count += 1;
|
||||||
sym_blobuse += s.len;
|
sym_blobuse += s.len;
|
||||||
@@ -413,11 +392,9 @@ export fn intern(s: str) i32 = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn symname(id: i32) str = {
|
fn symname(id: i32) str = {
|
||||||
let offt: *i32 = sym_off;
|
|
||||||
let lent: *i32 = sym_len;
|
|
||||||
let r: str;
|
let r: str;
|
||||||
r.ptr = sym_blob + offt[id];
|
r.ptr = sym_blob + sym_off[id];
|
||||||
r.len = lent[id];
|
r.len = sym_len[id];
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -883,15 +860,7 @@ fn promote_v(v: *value) *value = {
|
|||||||
n.car = p.car;
|
n.car = p.car;
|
||||||
n.cdr = p.cdr;
|
n.cdr = p.cdr;
|
||||||
n.envp = p.envp;
|
n.envp = p.envp;
|
||||||
if (p.kind == valkind.FLOAT) {
|
if (p.kind == valkind.FLOAT) { n.fval = p.fval; };
|
||||||
// f64 byte copy — `n.fval = p.fval` through *value lowers
|
|
||||||
// to an integer-reg store at the f64 offset, same trap
|
|
||||||
// vfloat sidesteps via the `fbuf` scratch global.
|
|
||||||
let psrc: *u8 = (p: *u8) + FVAL_OFF;
|
|
||||||
let ndst: *u8 = (n: *u8) + FVAL_OFF;
|
|
||||||
let i: u64 = 0u64;
|
|
||||||
for (i < 8u64) { ndst[i] = psrc[i]; i += 1u64; };
|
|
||||||
};
|
|
||||||
if (p.kind == valkind.STR) {
|
if (p.kind == valkind.STR) {
|
||||||
// Deep-copy the bytes into perm. vstr put them in trans, and
|
// Deep-copy the bytes into perm. vstr put them in trans, and
|
||||||
// the trans reset that ends every top-level form would drop
|
// the trans reset that ends every top-level form would drop
|
||||||
@@ -983,35 +952,9 @@ fn any_float(xs: []*value) bool = {
|
|||||||
// X0 — so the caller sees garbage. An out-pointer sidesteps the
|
// X0 — so the caller sees garbage. An out-pointer sidesteps the
|
||||||
// boundary entirely.
|
// boundary entirely.
|
||||||
|
|
||||||
// f64 movement through the wwstage cgen is fragile: only MOVSD into a
|
|
||||||
// top-level global emits the right MOVSD. Field-of-pointer stores,
|
|
||||||
// struct-literal stores, and function-arg passing for f64 all run
|
|
||||||
// through integer registers and lose the value. We treat every
|
|
||||||
// "f64 stored at address A" as a byte-copy from a known-good source.
|
|
||||||
//
|
|
||||||
// copybytes(d, s) — pure 8-byte memcpy.
|
|
||||||
// to_f64 — INT branch routes through `fbuf` (global f64 store works
|
|
||||||
// for the CVTSI2SD-produced X0), then byte-copies; FLOAT
|
|
||||||
// branch is a direct bit-copy from `v.fval`.
|
|
||||||
|
|
||||||
fn copybytes(d: *u8, s: *u8) void = {
|
|
||||||
let i: u64 = 0u64;
|
|
||||||
for (i < 8u64) { d[i] = s[i]; i += 1u64; };
|
|
||||||
};
|
|
||||||
|
|
||||||
fn to_f64(v: *value, out: *f64) bool = {
|
fn to_f64(v: *value, out: *f64) bool = {
|
||||||
let dst: *u8 = out: *u8;
|
if (v.kind == valkind.INT) { *out = v.ival: f64; return true; };
|
||||||
if (v.kind == valkind.INT) {
|
if (v.kind == valkind.FLOAT) { *out = v.fval; return true; };
|
||||||
fbuf = v.ival: f64;
|
|
||||||
let sp: *f64 = &fbuf;
|
|
||||||
copybytes(dst, sp: *u8);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
if (v.kind == valkind.FLOAT) {
|
|
||||||
let raw: *u8 = v: *u8;
|
|
||||||
copybytes(dst, raw + FVAL_OFF);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1149,22 +1092,16 @@ fn b_cons(xs: []*value) (*value | rterror) = {
|
|||||||
return vcons(xs[0], xs[1]);
|
return vcons(xs[0], xs[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Field access on a slice element (xs[i].kind) is mis-lowered by the
|
|
||||||
// wwstage cgen: it emits only the slice index, drops the trailing
|
|
||||||
// field load. Workaround: bind xs[i] to a local *value first.
|
|
||||||
|
|
||||||
fn b_car(xs: []*value) (*value | rterror) = {
|
fn b_car(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "car: need 1 arg": rterror; };
|
if (xs.len != 1) { return "car: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
if (xs[0].kind != valkind.CONS) { return "car: not a pair": rterror; };
|
||||||
if (p.kind != valkind.CONS) { return "car: not a pair": rterror; };
|
return xs[0].car;
|
||||||
return p.car;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_cdr(xs: []*value) (*value | rterror) = {
|
fn b_cdr(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "cdr: need 1 arg": rterror; };
|
if (xs.len != 1) { return "cdr: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
if (xs[0].kind != valkind.CONS) { return "cdr: not a pair": rterror; };
|
||||||
if (p.kind != valkind.CONS) { return "cdr: not a pair": rterror; };
|
return xs[0].cdr;
|
||||||
return p.cdr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_list(xs: []*value) (*value | rterror) = {
|
fn b_list(xs: []*value) (*value | rterror) = {
|
||||||
@@ -1179,47 +1116,40 @@ fn b_list(xs: []*value) (*value | rterror) = {
|
|||||||
|
|
||||||
fn b_nullp(xs: []*value) (*value | rterror) = {
|
fn b_nullp(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "null?: need 1 arg": rterror; };
|
if (xs.len != 1) { return "null?: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
return vbool(xs[0].kind == valkind.NIL);
|
||||||
return vbool(p.kind == valkind.NIL);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_pairp(xs: []*value) (*value | rterror) = {
|
fn b_pairp(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "pair?: need 1 arg": rterror; };
|
if (xs.len != 1) { return "pair?: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
return vbool(xs[0].kind == valkind.CONS);
|
||||||
return vbool(p.kind == valkind.CONS);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_nump(xs: []*value) (*value | rterror) = {
|
fn b_nump(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "number?: need 1 arg": rterror; };
|
if (xs.len != 1) { return "number?: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
let k: valkind = xs[0].kind;
|
||||||
let k: valkind = p.kind;
|
|
||||||
return vbool(k == valkind.INT || k == valkind.FLOAT);
|
return vbool(k == valkind.INT || k == valkind.FLOAT);
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_symp(xs: []*value) (*value | rterror) = {
|
fn b_symp(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "symbol?: need 1 arg": rterror; };
|
if (xs.len != 1) { return "symbol?: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
return vbool(xs[0].kind == valkind.SYM);
|
||||||
return vbool(p.kind == valkind.SYM);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_eqp(xs: []*value) (*value | rterror) = {
|
fn b_eqp(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 2) { return "eq?: need 2 args": rterror; };
|
if (xs.len != 2) { return "eq?: need 2 args": rterror; };
|
||||||
let a: *value = xs[0];
|
if (xs[0].kind != xs[1].kind) { return vbool(false); };
|
||||||
let b: *value = xs[1];
|
if (xs[0].kind == valkind.NIL) { return vbool(true); };
|
||||||
if (a.kind != b.kind) { return vbool(false); };
|
if (xs[0].kind == valkind.BOOL) { return vbool(xs[0].bval == xs[1].bval); };
|
||||||
if (a.kind == valkind.NIL) { return vbool(true); };
|
if (xs[0].kind == valkind.INT) { return vbool(xs[0].ival == xs[1].ival); };
|
||||||
if (a.kind == valkind.BOOL) { return vbool(a.bval == b.bval); };
|
if (xs[0].kind == valkind.SYM) { return vbool(xs[0].sid == xs[1].sid); };
|
||||||
if (a.kind == valkind.INT) { return vbool(a.ival == b.ival); };
|
|
||||||
if (a.kind == valkind.SYM) { return vbool(a.sid == b.sid); };
|
|
||||||
// Reference equality for everything else — matches eq? semantics
|
// Reference equality for everything else — matches eq? semantics
|
||||||
// in classic Lisps.
|
// in classic Lisps.
|
||||||
return vbool(a == b);
|
return vbool(xs[0] == xs[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_not(xs: []*value) (*value | rterror) = {
|
fn b_not(xs: []*value) (*value | rterror) = {
|
||||||
if (xs.len != 1) { return "not: need 1 arg": rterror; };
|
if (xs.len != 1) { return "not: need 1 arg": rterror; };
|
||||||
let p: *value = xs[0];
|
return vbool(!truthy(xs[0]));
|
||||||
return vbool(!truthy(p));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn b_print(xs: []*value) (*value | rterror) = {
|
fn b_print(xs: []*value) (*value | rterror) = {
|
||||||
|
|||||||
Reference in New Issue
Block a user