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
|
||||
production interpreter. Treat the files below as a worked example of
|
||||
"what does and doesn't lower cleanly through the wwstage cgen today",
|
||||
not as a reference Lisp implementation.
|
||||
production interpreter. Drives the wwstage cgen (`out/bin/w6c_ww`) by
|
||||
default; the Makefile sets `WW_W6C` so `make` picks the ww-built
|
||||
backend rather than the C bootstrap one.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -9,7 +9,7 @@ not as a reference Lisp implementation.
|
||||
eval, apply, printer, REPL. Everything the entry
|
||||
point and the test driver consume is `export`-ed.
|
||||
- `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`
|
||||
drops `-I` in single-file mode, so the Makefile
|
||||
runs the binary itself.
|
||||
@@ -24,86 +24,66 @@ test — use the bare names.
|
||||
|
||||
## wwstage cgen workarounds at play
|
||||
|
||||
Every workaround below is documented inline at its use site. The
|
||||
shape of the bug is what matters, not the specific symptom — the
|
||||
same bug class shows up in any new code that hits the same pattern.
|
||||
The wwstage cgen has been brought up to par on the bug classes this
|
||||
demo originally exercised. The remaining workarounds and the
|
||||
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.**
|
||||
`globalarr[i] = …` lowers to `LEAQ (BP), BX` instead of
|
||||
`LEAQ globalarr(SB), BX` — the global address gets treated as a
|
||||
stack frame, corrupting both. Heap-allocate via `os.alloc` into a
|
||||
`*T` and alias to a local at the top of each function before
|
||||
indexing. See `sym_off` / `sym_blob` / `obuf`.
|
||||
1. **(retired) Top-level `[N]T` arrays mis-address inside functions.**
|
||||
`globalarr[i]` used to lower to `LEAQ (BP), BX`. cgindex/cgassign
|
||||
now detect top-level array idents and emit `LEAQ name(SB), BX`,
|
||||
and emitletdataw lays the array bytes into DATAW.
|
||||
|
||||
2. **`global_ptr[i]` mis-addresses too.** Even after switching to a
|
||||
pointer global, `MOVQ global_ptr(SB), BX` is replaced with
|
||||
`MOVQ (BP), BX`. Alias `let p = global_ptr;` at function entry,
|
||||
then `p[i]`. See every helper that touches the symbol interner.
|
||||
2. **(retired) `global_ptr[i]` mis-addresses.** cgindex/cgassign
|
||||
detect top-level `*T` idents and emit `MOVQ name(SB), BX` with
|
||||
the correct element scaling. `lispcore`'s symbol interner is back
|
||||
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.**
|
||||
`L.cur.kind = k` or `L.src.ptr = buf.ptr` (where `cur`/`src` is a
|
||||
non-pointer struct field of a struct reached through a pointer) is
|
||||
silently dropped — the function body emits no store. Either
|
||||
flatten the sub-struct (see `lexer.curkind`/`curival`/…) or build
|
||||
the whole sub-struct as a local and do a single whole-struct
|
||||
assign (`let s: str = …; L.src = s;`).
|
||||
3. **(retired) Two-level field write through a non-pointer
|
||||
sub-struct.** cgassign/cgdot now handle the chained
|
||||
`(*L).cur.kind` shape both as read and write. The lexer keeps the
|
||||
flattened `curkind` / `curival` / … fields for now because every
|
||||
call site uses them; un-flattening is a stylistic improvement, not
|
||||
a correctness fix.
|
||||
|
||||
4. **f64 through every boundary is unreliable.** The cgen routes f64
|
||||
stores via integer registers; in most paths AX gets stored where
|
||||
X0 should have been written.
|
||||
- `alloc(value{ fval = v })`: writes the kind enum (AX still
|
||||
holds it) instead of the f64.
|
||||
- `p.fval = v` through a `*T`: same — stores AX.
|
||||
- `*p = v` for `*f64`: stores AX.
|
||||
- `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.
|
||||
4. **(partially retired) f64 through every boundary.** Struct-field
|
||||
`p.fval = v` through `*T`, `*p = v` for `*f64`, and `alloc(T{…})`
|
||||
sugar for f64 fields all route through X0 now (`vfloat`,
|
||||
`promote_v`, `to_f64` are back to direct `p.fval = …` /
|
||||
`*out = v.fval` form). Function-arg passing of an f64 struct-field
|
||||
value also works (the cgen's `exprfloatkind` now recognises
|
||||
`p.field` whose declared type is f64/f32).
|
||||
|
||||
What does work: `MOVSD X0, global(SB)` (a top-level f64 global)
|
||||
and `MOVSD X0, off(BP)` (a local f64 slot). The interpreter
|
||||
threads f64 through a scratch global (`fbuf`) and byte-copies
|
||||
8 bytes wherever a `*f64` would normally suffice. See `vfloat`,
|
||||
`to_f64`, `copybytes`.
|
||||
5. **(retired) `alloc(value{ text = s })` writes only `s.ptr`.** Both
|
||||
the `alloc(T{…})` builtin and the bare struct-literal init now
|
||||
emit both halves of the str. `vstr` still does manual
|
||||
`p.text = s` (semantically equivalent, no longer required).
|
||||
|
||||
5. **`alloc(value{ text = s })` writes only `s.ptr`.** The cgen sets
|
||||
up `s.ptr` in AX, sets the new-pointer in BX, then needs `s.len`
|
||||
in another reg — but the same BX gets clobbered by the new-pointer
|
||||
reload, so the `.len` store never happens. Manual init via
|
||||
`p.text = s` writes both halves correctly.
|
||||
6. **`(slice | E)` tagged-union returns drop `slice.len`.** Still
|
||||
present. The wwstage return convention is AX=tag, DX=payload1,
|
||||
CX=payload2 — a 24-byte slice header doesn't fit. Inline the
|
||||
slice-building loop into the caller. See `eval`'s argument-eval
|
||||
inline.
|
||||
|
||||
6. **`(slice | E)` tagged-union returns drop `slice.len`.** The
|
||||
wwstage return convention is AX=tag, DX=payload1, CX=payload2. A
|
||||
slice header is 24 bytes (ptr/len/cap); only ptr and cap come
|
||||
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.
|
||||
7. **(retired) `xs[i].kind` drops the trailing field load.** cgdot
|
||||
now handles N_INDEX bases. Builtins are back to
|
||||
`xs[0].kind` / `xs[0].car` directly — no `let p = xs[0];` first.
|
||||
|
||||
8. **f64 compound assigns are mis-lowered to `acc = d` (no OP).**
|
||||
`acc += f` / `acc /= f` etc. on f64 locals drop the operator.
|
||||
Write the explicit form: `acc = acc + f` / `acc = acc / f`.
|
||||
Integer compound assigns work fine, so `acc += i` on `i64`
|
||||
stays as-is.
|
||||
Still present. Write the explicit form: `acc = acc + f`. Integer
|
||||
compound assigns work fine.
|
||||
|
||||
9. **`let r = call(); foreign_call(); return r?;` corrupts `r` when
|
||||
the call returned a tagged union.** The (`tag`, `payload1`,
|
||||
`payload2`) triple sits in AX/DX/CX after the call, and the
|
||||
foreign call between capture and `?`-unwrap clobbers at least one
|
||||
register before the cgen has spilled it to the local slot.
|
||||
Symptom: a `(*value | rterror)` whose `rterror` carries a string
|
||||
literal prints with a `str.len` of tens of thousands. Workaround:
|
||||
`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`).
|
||||
9. **(retired in practice) `let r = call(); foreign_call(); return
|
||||
r?;` corrupts `r`.** The wwstage cgen now spills the AX/DX/CX
|
||||
triple to the local's 24-byte slot at the assignment point,
|
||||
matching what cstage emits — the foreign call in between no longer
|
||||
clobbers an unspilled half. We still pre-match the union inline in
|
||||
eval's BUILTIN apply path so the `os.free` happens *after*
|
||||
classification rather than after the unwrap.
|
||||
|
||||
If a new function "should work but acts weird", the bug is almost
|
||||
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.
|
||||
If a new function "should work but acts weird", check #6 first.
|
||||
|
||||
## 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.
|
||||
- **No bigints.** `i64` wraps silently on overflow. `(fact 21)`
|
||||
rolls over.
|
||||
- **Float printing is fixed `%.6f`.** `1.0` prints as `1.000000`.
|
||||
Strconv has no `ftos` yet.
|
||||
- **Float printing is 6-digit fixed-point.** The printer delegates
|
||||
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
|
||||
source lands as the two bytes `\\` + `n`, not a newline.
|
||||
- **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
|
||||
# `..helper`, which then breaks the assembler).
|
||||
|
||||
WW := $(shell cd ../..; pwd)/out/bin/ww
|
||||
HERE := $(shell pwd)
|
||||
# Drive the wwstage cgen (w6c_ww) by default. The C-stage cgen has
|
||||
# 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
|
||||
$(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
|
||||
# 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.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
|
||||
# the REPL; each prints its results to stdout (errors go to stderr
|
||||
|
||||
@@ -263,22 +263,10 @@ export fn vint(v: i64) *value = {
|
||||
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 = {
|
||||
fbuf = v;
|
||||
let p: *value = arena_alloc(VALUE_SZ): *value;
|
||||
p.kind = valkind.FLOAT;
|
||||
let raw: *u8 = p: *u8;
|
||||
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; };
|
||||
p.fval = v;
|
||||
return p;
|
||||
};
|
||||
|
||||
@@ -371,29 +359,20 @@ let SID_LET: i32 = 0;
|
||||
let SID_BEGIN: 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 = {
|
||||
if (a.len != len) { return false; };
|
||||
let blob: *u8 = sym_blob;
|
||||
let i: i32 = 0;
|
||||
for (i < len) {
|
||||
if (a[i] != blob[off + i]) { return false; };
|
||||
if (a[i] != sym_blob[off + i]) { return false; };
|
||||
i += 1;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
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;
|
||||
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;
|
||||
};
|
||||
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 j: i32 = 0;
|
||||
for (j < s.len) {
|
||||
blob[off + j] = s[j];
|
||||
sym_blob[off + j] = s[j];
|
||||
j += 1;
|
||||
};
|
||||
offt[sym_count] = off;
|
||||
lent[sym_count] = s.len;
|
||||
sym_off[sym_count] = off;
|
||||
sym_len[sym_count] = s.len;
|
||||
let id: i32 = sym_count;
|
||||
sym_count += 1;
|
||||
sym_blobuse += s.len;
|
||||
@@ -413,11 +392,9 @@ export fn intern(s: str) i32 = {
|
||||
};
|
||||
|
||||
fn symname(id: i32) str = {
|
||||
let offt: *i32 = sym_off;
|
||||
let lent: *i32 = sym_len;
|
||||
let r: str;
|
||||
r.ptr = sym_blob + offt[id];
|
||||
r.len = lent[id];
|
||||
r.ptr = sym_blob + sym_off[id];
|
||||
r.len = sym_len[id];
|
||||
return r;
|
||||
};
|
||||
|
||||
@@ -883,15 +860,7 @@ fn promote_v(v: *value) *value = {
|
||||
n.car = p.car;
|
||||
n.cdr = p.cdr;
|
||||
n.envp = p.envp;
|
||||
if (p.kind == valkind.FLOAT) {
|
||||
// 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.FLOAT) { n.fval = p.fval; };
|
||||
if (p.kind == valkind.STR) {
|
||||
// Deep-copy the bytes into perm. vstr put them in trans, and
|
||||
// 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
|
||||
// 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 = {
|
||||
let dst: *u8 = out: *u8;
|
||||
if (v.kind == valkind.INT) {
|
||||
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;
|
||||
};
|
||||
if (v.kind == valkind.INT) { *out = v.ival: f64; return true; };
|
||||
if (v.kind == valkind.FLOAT) { *out = v.fval; return true; };
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -1149,22 +1092,16 @@ fn b_cons(xs: []*value) (*value | rterror) = {
|
||||
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) = {
|
||||
if (xs.len != 1) { return "car: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
if (p.kind != valkind.CONS) { return "car: not a pair": rterror; };
|
||||
return p.car;
|
||||
if (xs[0].kind != valkind.CONS) { return "car: not a pair": rterror; };
|
||||
return xs[0].car;
|
||||
};
|
||||
|
||||
fn b_cdr(xs: []*value) (*value | rterror) = {
|
||||
if (xs.len != 1) { return "cdr: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
if (p.kind != valkind.CONS) { return "cdr: not a pair": rterror; };
|
||||
return p.cdr;
|
||||
if (xs[0].kind != valkind.CONS) { return "cdr: not a pair": rterror; };
|
||||
return xs[0].cdr;
|
||||
};
|
||||
|
||||
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) = {
|
||||
if (xs.len != 1) { return "null?: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
return vbool(p.kind == valkind.NIL);
|
||||
return vbool(xs[0].kind == valkind.NIL);
|
||||
};
|
||||
|
||||
fn b_pairp(xs: []*value) (*value | rterror) = {
|
||||
if (xs.len != 1) { return "pair?: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
return vbool(p.kind == valkind.CONS);
|
||||
return vbool(xs[0].kind == valkind.CONS);
|
||||
};
|
||||
|
||||
fn b_nump(xs: []*value) (*value | rterror) = {
|
||||
if (xs.len != 1) { return "number?: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
let k: valkind = p.kind;
|
||||
let k: valkind = xs[0].kind;
|
||||
return vbool(k == valkind.INT || k == valkind.FLOAT);
|
||||
};
|
||||
|
||||
fn b_symp(xs: []*value) (*value | rterror) = {
|
||||
if (xs.len != 1) { return "symbol?: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
return vbool(p.kind == valkind.SYM);
|
||||
return vbool(xs[0].kind == valkind.SYM);
|
||||
};
|
||||
|
||||
fn b_eqp(xs: []*value) (*value | rterror) = {
|
||||
if (xs.len != 2) { return "eq?: need 2 args": rterror; };
|
||||
let a: *value = xs[0];
|
||||
let b: *value = xs[1];
|
||||
if (a.kind != b.kind) { return vbool(false); };
|
||||
if (a.kind == valkind.NIL) { return vbool(true); };
|
||||
if (a.kind == valkind.BOOL) { return vbool(a.bval == b.bval); };
|
||||
if (a.kind == valkind.INT) { return vbool(a.ival == b.ival); };
|
||||
if (a.kind == valkind.SYM) { return vbool(a.sid == b.sid); };
|
||||
if (xs[0].kind != xs[1].kind) { return vbool(false); };
|
||||
if (xs[0].kind == valkind.NIL) { return vbool(true); };
|
||||
if (xs[0].kind == valkind.BOOL) { return vbool(xs[0].bval == xs[1].bval); };
|
||||
if (xs[0].kind == valkind.INT) { return vbool(xs[0].ival == xs[1].ival); };
|
||||
if (xs[0].kind == valkind.SYM) { return vbool(xs[0].sid == xs[1].sid); };
|
||||
// Reference equality for everything else — matches eq? semantics
|
||||
// in classic Lisps.
|
||||
return vbool(a == b);
|
||||
return vbool(xs[0] == xs[1]);
|
||||
};
|
||||
|
||||
fn b_not(xs: []*value) (*value | rterror) = {
|
||||
if (xs.len != 1) { return "not: need 1 arg": rterror; };
|
||||
let p: *value = xs[0];
|
||||
return vbool(!truthy(p));
|
||||
return vbool(!truthy(xs[0]));
|
||||
};
|
||||
|
||||
fn b_print(xs: []*value) (*value | rterror) = {
|
||||
|
||||
Reference in New Issue
Block a user