diff --git a/wasm/app/index.html b/wasm/app/index.html index 648ccef..c2bc429 100644 --- a/wasm/app/index.html +++ b/wasm/app/index.html @@ -71,11 +71,11 @@ diff --git a/wasm/app/repl.html b/wasm/app/repl.html new file mode 100644 index 0000000..c0f2935 --- /dev/null +++ b/wasm/app/repl.html @@ -0,0 +1,60 @@ + + + + + +lumbda repl — three tiers, one prompt + + + + +
+ + +

lumbda

+
+

three tiers, one prompt

+

+ interactive repl. type a lisp expression at the prompt below; each tier + keeps its own persistent state across evals. compare side by side, + measure tier-by-tier elapsed, share a link to the + playground for static demos. +

+
+ +
+
+ send to + + + + +
+ + + + +
+ +
+
+
+ +
+ λ> + + +
+ + + + + + diff --git a/wasm/asm/lumbda.wat b/wasm/asm/lumbda.wat index fb841ea..fa136b3 100644 --- a/wasm/asm/lumbda.wat +++ b/wasm/asm/lumbda.wat @@ -61,6 +61,12 @@ (global $sym_and (mut i32) (i32.const 0)) (global $sym_or (mut i32) (i32.const 0)) (global $sym_set (mut i32) (i32.const 0)) + (global $sym_letstar (mut i32) (i32.const 0)) + (global $sym_letrec (mut i32) (i32.const 0)) + (global $sym_when (mut i32) (i32.const 0)) + (global $sym_unless (mut i32) (i32.const 0)) + (global $sym_case (mut i32) (i32.const 0)) + (global $sym_do (mut i32) (i32.const 0)) ;; Constants (immediate value addresses) (global $NIL i32 (i32.const 4)) @@ -689,6 +695,21 @@ (if (i32.eq (local.get $head) (global.get $sym_or)) (then (return (call $eval_or (local.get $rest) (local.get $env))))) + (if (i32.eq (local.get $head) (global.get $sym_letstar)) + (then (return (call $eval_letstar (local.get $rest) (local.get $env))))) + + (if (i32.eq (local.get $head) (global.get $sym_letrec)) + (then (return (call $eval_letrec (local.get $rest) (local.get $env))))) + + (if (i32.eq (local.get $head) (global.get $sym_when)) + (then (return (call $eval_when (local.get $rest) (local.get $env))))) + + (if (i32.eq (local.get $head) (global.get $sym_unless)) + (then (return (call $eval_unless (local.get $rest) (local.get $env))))) + + (if (i32.eq (local.get $head) (global.get $sym_case)) + (then (return (call $eval_case (local.get $rest) (local.get $env))))) + ;; Function application (return (call $apply (call $eval (local.get $head) (local.get $env)) @@ -752,8 +773,99 @@ (br $loop))) (global.get $VOID)) - ;; (let ((x e) ...) body) — non-recursive form + ;; (let ((x e) ...) body) — non-recursive form (also supports named let) + ;; Named let: (let name ((x e) ...) body) -> a self-referential procedure. (func $eval_let (param $rest i32) (param $env i32) (result i32) + (local $first i32) + (local $bindings i32) + (local $body i32) + (local $new_env i32) + (local $b i32) + (local $sym i32) + (local $val i32) + (local $name i32) + (local $params i32) + (local $args i32) + (local $closure i32) + (local.set $first (call $car (local.get $rest))) + ;; Named let if first arg is a symbol. + (if (call $is_symbol (local.get $first)) + (then + (local.set $name (local.get $first)) + (local.set $bindings (call $car (call $cdr (local.get $rest)))) + (local.set $body (call $cdr (call $cdr (local.get $rest)))) + ;; Build params list (the binding names) and args list (their inits). + (local.set $params (call $extract_let_params (local.get $bindings))) + (local.set $args (call $extract_let_inits (local.get $bindings) (local.get $env))) + ;; Create closure capturing CURRENT env (so the body can refer to name). + (local.set $closure (call $make_closure (local.get $params) (local.get $body) (local.get $env))) + ;; Bind the closure to name in a fresh local env and patch its env to include itself. + (local.set $new_env (call $env_define (local.get $env) (local.get $name) (local.get $closure))) + ;; Patch closure.env to the new env so name resolves to it. + (i32.store offset=12 (local.get $closure) (local.get $new_env)) + (return (call $apply (local.get $closure) (local.get $args))))) + (local.set $bindings (local.get $first)) + (local.set $body (call $cdr (local.get $rest))) + (local.set $new_env (local.get $env)) + (block $done + (loop $loop + (br_if $done (i32.eq (local.get $bindings) (global.get $NIL))) + (local.set $b (call $car (local.get $bindings))) + (local.set $sym (call $car (local.get $b))) + (local.set $val (call $eval (call $car (call $cdr (local.get $b))) (local.get $env))) + (local.set $new_env (call $env_define (local.get $new_env) (local.get $sym) (local.get $val))) + (local.set $bindings (call $cdr (local.get $bindings))) + (br $loop))) + (call $eval_begin (local.get $body) (local.get $new_env))) + + ;; Helper: build params list from let bindings (a list of (sym init)). + (func $extract_let_params (param $bindings i32) (result i32) + (local $head i32) + (local $tail i32) + (local $new i32) + (local.set $head (global.get $NIL)) + (local.set $tail (global.get $NIL)) + (block $done + (loop $l + (br_if $done (i32.eq (local.get $bindings) (global.get $NIL))) + (local.set $new + (call $make_pair + (call $car (call $car (local.get $bindings))) + (global.get $NIL))) + (if (i32.eq (local.get $head) (global.get $NIL)) + (then (local.set $head (local.get $new)) + (local.set $tail (local.get $new))) + (else (call $set_cdr (local.get $tail) (local.get $new)) + (local.set $tail (local.get $new)))) + (local.set $bindings (call $cdr (local.get $bindings))) + (br $l))) + (local.get $head)) + + ;; Helper: evaluate each init expression and return the resulting list. + (func $extract_let_inits (param $bindings i32) (param $env i32) (result i32) + (local $head i32) + (local $tail i32) + (local $new i32) + (local.set $head (global.get $NIL)) + (local.set $tail (global.get $NIL)) + (block $done + (loop $l + (br_if $done (i32.eq (local.get $bindings) (global.get $NIL))) + (local.set $new + (call $make_pair + (call $eval (call $car (call $cdr (call $car (local.get $bindings)))) (local.get $env)) + (global.get $NIL))) + (if (i32.eq (local.get $head) (global.get $NIL)) + (then (local.set $head (local.get $new)) + (local.set $tail (local.get $new))) + (else (call $set_cdr (local.get $tail) (local.get $new)) + (local.set $tail (local.get $new)))) + (local.set $bindings (call $cdr (local.get $bindings))) + (br $l))) + (local.get $head)) + + ;; (let* ((x e1) (y e2)) body) — each binding sees previous bindings. + (func $eval_letstar (param $rest i32) (param $env i32) (result i32) (local $bindings i32) (local $body i32) (local $new_env i32) @@ -768,12 +880,94 @@ (br_if $done (i32.eq (local.get $bindings) (global.get $NIL))) (local.set $b (call $car (local.get $bindings))) (local.set $sym (call $car (local.get $b))) - (local.set $val (call $eval (call $car (call $cdr (local.get $b))) (local.get $env))) + (local.set $val (call $eval (call $car (call $cdr (local.get $b))) (local.get $new_env))) (local.set $new_env (call $env_define (local.get $new_env) (local.get $sym) (local.get $val))) (local.set $bindings (call $cdr (local.get $bindings))) (br $loop))) (call $eval_begin (local.get $body) (local.get $new_env))) + ;; (letrec ((f (lambda ...))) body) — each binding visible to all others. + ;; First create the env with placeholder bindings, then evaluate inits in + ;; the new env (so lambdas capture it), then patch values. + (func $eval_letrec (param $rest i32) (param $env i32) (result i32) + (local $bindings i32) + (local $body i32) + (local $new_env i32) + (local $b i32) + (local $sym i32) + (local $val i32) + (local $cur i32) + (local $bind i32) + (local.set $bindings (call $car (local.get $rest))) + (local.set $body (call $cdr (local.get $rest))) + (local.set $new_env (local.get $env)) + (local.set $cur (local.get $bindings)) + ;; Pass 1: bind every name to VOID in the new env. + (block $done1 + (loop $l1 + (br_if $done1 (i32.eq (local.get $cur) (global.get $NIL))) + (local.set $b (call $car (local.get $cur))) + (local.set $sym (call $car (local.get $b))) + (local.set $new_env (call $env_define (local.get $new_env) (local.get $sym) (global.get $VOID))) + (local.set $cur (call $cdr (local.get $cur))) + (br $l1))) + ;; Pass 2: evaluate each init in new_env (so lambdas see each other), + ;; mutate the binding pair's cdr to the real value. + (local.set $cur (local.get $bindings)) + (block $done2 + (loop $l2 + (br_if $done2 (i32.eq (local.get $cur) (global.get $NIL))) + (local.set $b (call $car (local.get $cur))) + (local.set $sym (call $car (local.get $b))) + (local.set $val (call $eval (call $car (call $cdr (local.get $b))) (local.get $new_env))) + (call $env_set (local.get $new_env) (local.get $sym) (local.get $val)) + (local.set $cur (call $cdr (local.get $cur))) + (br $l2))) + (call $eval_begin (local.get $body) (local.get $new_env))) + + ;; (when test body...) -> if test true, eval body, else void + (func $eval_when (param $rest i32) (param $env i32) (result i32) + (if (i32.ne (call $eval (call $car (local.get $rest)) (local.get $env)) (global.get $FALSE)) + (then (return (call $eval_begin (call $cdr (local.get $rest)) (local.get $env))))) + (global.get $VOID)) + + ;; (unless test body...) -> if test false, eval body, else void + (func $eval_unless (param $rest i32) (param $env i32) (result i32) + (if (i32.eq (call $eval (call $car (local.get $rest)) (local.get $env)) (global.get $FALSE)) + (then (return (call $eval_begin (call $cdr (local.get $rest)) (local.get $env))))) + (global.get $VOID)) + + ;; (case key ((1 2) "small") ((3 4) "med") (else "big")) + ;; Each clause: (datum-list body) where datum-list is either a list of + ;; literals to match via eqv?, or the symbol `else`. + (func $eval_case (param $rest i32) (param $env i32) (result i32) + (local $key i32) + (local $clauses i32) + (local $clause i32) + (local $data i32) + (local $body i32) + (local.set $key (call $eval (call $car (local.get $rest)) (local.get $env))) + (local.set $clauses (call $cdr (local.get $rest))) + (block $done + (loop $l + (br_if $done (i32.eq (local.get $clauses) (global.get $NIL))) + (local.set $clause (call $car (local.get $clauses))) + (local.set $data (call $car (local.get $clause))) + (local.set $body (call $cdr (local.get $clause))) + (if (i32.eq (local.get $data) (global.get $sym_else)) + (then (return (call $eval_begin (local.get $body) (local.get $env))))) + ;; data is a list of literal values; match via eqv? + (block $clausedone + (loop $datal + (br_if $clausedone (i32.eq (local.get $data) (global.get $NIL))) + (if (i32.eq (call $car (local.get $data)) (local.get $key)) + (then (return (call $eval_begin (local.get $body) (local.get $env))))) + (local.set $data (call $cdr (local.get $data))) + (br $datal))) + (local.set $clauses (call $cdr (local.get $clauses))) + (br $l))) + (global.get $VOID)) + (func $eval_and (param $rest i32) (param $env i32) (result i32) (local $val i32) (local.set $val (global.get $TRUE)) @@ -865,12 +1059,128 @@ (br $loop))) (local.get $new)) + ;; ─── Equality helpers ────────────────────────────────────────── + ;; Deep structural equality. Returns TRUE/FALSE immediate. + (func $equal_p (param $a i32) (param $b i32) (result i32) + (local $alen i32) + (local $blen i32) + (local $i i32) + (if (i32.eq (local.get $a) (local.get $b)) + (then (return (global.get $TRUE)))) + (if (call $is_pair (local.get $a)) + (then + (if (i32.eqz (call $is_pair (local.get $b))) + (then (return (global.get $FALSE)))) + (if (i32.eq (call $equal_p (call $car (local.get $a)) (call $car (local.get $b))) (global.get $FALSE)) + (then (return (global.get $FALSE)))) + (return (call $equal_p (call $cdr (local.get $a)) (call $cdr (local.get $b)))))) + (if (call $is_string (local.get $a)) + (then + (if (i32.eqz (call $is_string (local.get $b))) + (then (return (global.get $FALSE)))) + (local.set $alen (i32.load offset=4 (local.get $a))) + (local.set $blen (i32.load offset=4 (local.get $b))) + (if (i32.ne (local.get $alen) (local.get $blen)) + (then (return (global.get $FALSE)))) + (local.set $i (i32.const 0)) + (block $done + (loop $l + (br_if $done (i32.ge_u (local.get $i) (local.get $alen))) + (if (i32.ne + (i32.load8_u (i32.add (i32.add (local.get $a) (i32.const 8)) (local.get $i))) + (i32.load8_u (i32.add (i32.add (local.get $b) (i32.const 8)) (local.get $i)))) + (then (return (global.get $FALSE)))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $l))) + (return (global.get $TRUE)))) + ;; Fall through: not equal (different types / not pair) + (global.get $FALSE)) + + ;; (append a b) returns a new list with b appended after a. + (func $append2 (param $a i32) (param $b i32) (result i32) + (local $head i32) + (local $tail i32) + (local $new i32) + (local $cur i32) + (local.set $head (global.get $NIL)) + (local.set $tail (global.get $NIL)) + (local.set $cur (local.get $a)) + (block $done + (loop $l + (br_if $done (i32.eqz (call $is_pair (local.get $cur)))) + (local.set $new (call $make_pair (call $car (local.get $cur)) (global.get $NIL))) + (if (i32.eq (local.get $head) (global.get $NIL)) + (then (local.set $head (local.get $new)) + (local.set $tail (local.get $new))) + (else (call $set_cdr (local.get $tail) (local.get $new)) + (local.set $tail (local.get $new)))) + (local.set $cur (call $cdr (local.get $cur))) + (br $l))) + (if (i32.eq (local.get $head) (global.get $NIL)) + (then (return (local.get $b)))) + (call $set_cdr (local.get $tail) (local.get $b)) + (local.get $head)) + + ;; (member key list) — use_equal=1 → equal?; else eq? + (func $member_eq (param $key i32) (param $list i32) (param $use_equal i32) (result i32) + (local $cur i32) + (local $match i32) + (local.set $cur (local.get $list)) + (block $done + (loop $l + (br_if $done (i32.eqz (call $is_pair (local.get $cur)))) + (if (local.get $use_equal) + (then + (local.set $match + (i32.eq (call $equal_p (local.get $key) (call $car (local.get $cur))) (global.get $TRUE)))) + (else + (local.set $match + (i32.eq (local.get $key) (call $car (local.get $cur)))))) + (if (local.get $match) + (then (return (local.get $cur)))) + (local.set $cur (call $cdr (local.get $cur))) + (br $l))) + (global.get $FALSE)) + + ;; (assoc key alist) — use_equal=1 → equal? on car; else eq? + (func $assoc_eq (param $key i32) (param $alist i32) (param $use_equal i32) (result i32) + (local $cur i32) + (local $pair i32) + (local $match i32) + (local.set $cur (local.get $alist)) + (block $done + (loop $l + (br_if $done (i32.eqz (call $is_pair (local.get $cur)))) + (local.set $pair (call $car (local.get $cur))) + (if (call $is_pair (local.get $pair)) + (then + (if (local.get $use_equal) + (then + (local.set $match + (i32.eq (call $equal_p (local.get $key) (call $car (local.get $pair))) (global.get $TRUE)))) + (else + (local.set $match + (i32.eq (local.get $key) (call $car (local.get $pair)))))) + (if (local.get $match) + (then (return (local.get $pair)))))) + (local.set $cur (call $cdr (local.get $cur))) + (br $l))) + (global.get $FALSE)) + ;; ─── Primitives ──────────────────────────────────────────────── (func $apply_primitive (param $id i32) (param $args i32) (result i32) (local $a i32) (local $b i32) (local $sum i32) (local $cur i32) + (local $vmin i32) + (local $vmax i32) + (local $base i32) + (local $exp_n i32) + (local $result i32) + (local $rev i32) + (local $idx i32) + (local $idxt i32) ;; Fetch first 2 args (most prims use 1 or 2). Defaults to fixnum 0. (local.set $a (call $make_fixnum (i32.const 0))) @@ -1059,6 +1369,255 @@ (then (return (global.get $TRUE))) (else (return (global.get $FALSE)))))) + ;; quotient (25): integer truncating division + (if (i32.eq (local.get $id) (i32.const 25)) + (then + (return (call $make_fixnum (i32.div_s (call $fixnum_val (local.get $a)) + (call $fixnum_val (local.get $b))))))) + + ;; remainder (26): integer remainder (sign of dividend) + (if (i32.eq (local.get $id) (i32.const 26)) + (then + (return (call $make_fixnum (i32.rem_s (call $fixnum_val (local.get $a)) + (call $fixnum_val (local.get $b))))))) + + ;; min (27): variadic + (if (i32.eq (local.get $id) (i32.const 27)) + (then + (local.set $sum (call $fixnum_val (local.get $a))) + (local.set $cur (call $cdr (local.get $args))) + (block $done + (loop $loop + (br_if $done (i32.eq (local.get $cur) (global.get $NIL))) + (local.set $vmin (call $fixnum_val (call $car (local.get $cur)))) + (if (i32.lt_s (local.get $vmin) (local.get $sum)) + (then (local.set $sum (local.get $vmin)))) + (local.set $cur (call $cdr (local.get $cur))) + (br $loop))) + (return (call $make_fixnum (local.get $sum))))) + + ;; max (28): variadic + (if (i32.eq (local.get $id) (i32.const 28)) + (then + (local.set $sum (call $fixnum_val (local.get $a))) + (local.set $cur (call $cdr (local.get $args))) + (block $done + (loop $loop + (br_if $done (i32.eq (local.get $cur) (global.get $NIL))) + (local.set $vmax (call $fixnum_val (call $car (local.get $cur)))) + (if (i32.gt_s (local.get $vmax) (local.get $sum)) + (then (local.set $sum (local.get $vmax)))) + (local.set $cur (call $cdr (local.get $cur))) + (br $loop))) + (return (call $make_fixnum (local.get $sum))))) + + ;; expt (29): integer exponent (positive) + (if (i32.eq (local.get $id) (i32.const 29)) + (then + (local.set $base (call $fixnum_val (local.get $a))) + (local.set $exp_n (call $fixnum_val (local.get $b))) + (local.set $result (i32.const 1)) + (block $done + (loop $loop + (br_if $done (i32.le_s (local.get $exp_n) (i32.const 0))) + (local.set $result (i32.mul (local.get $result) (local.get $base))) + (local.set $exp_n (i32.sub (local.get $exp_n) (i32.const 1))) + (br $loop))) + (return (call $make_fixnum (local.get $result))))) + + ;; even? (30) + (if (i32.eq (local.get $id) (i32.const 30)) + (then + (if (i32.eqz (i32.and (call $fixnum_val (local.get $a)) (i32.const 1))) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; odd? (31) + (if (i32.eq (local.get $id) (i32.const 31)) + (then + (if (i32.and (call $fixnum_val (local.get $a)) (i32.const 1)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; positive? (32) + (if (i32.eq (local.get $id) (i32.const 32)) + (then + (if (i32.gt_s (call $fixnum_val (local.get $a)) (i32.const 0)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; negative? (33) + (if (i32.eq (local.get $id) (i32.const 33)) + (then + (if (i32.lt_s (call $fixnum_val (local.get $a)) (i32.const 0)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; set-car! (34) + (if (i32.eq (local.get $id) (i32.const 34)) + (then + (call $set_car (local.get $a) (local.get $b)) + (return (global.get $VOID)))) + + ;; set-cdr! (35) + (if (i32.eq (local.get $id) (i32.const 35)) + (then + (call $set_cdr (local.get $a) (local.get $b)) + (return (global.get $VOID)))) + + ;; equal? (36): deep structural equality + (if (i32.eq (local.get $id) (i32.const 36)) + (then (return (call $equal_p (local.get $a) (local.get $b))))) + + ;; eqv? (37): identity for our value types + (if (i32.eq (local.get $id) (i32.const 37)) + (then + (if (i32.eq (local.get $a) (local.get $b)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; number? (38) — fixnums only in this tier + (if (i32.eq (local.get $id) (i32.const 38)) + (then + (if (call $is_fixnum (local.get $a)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; integer? (39) — same as number? here + (if (i32.eq (local.get $id) (i32.const 39)) + (then + (if (call $is_fixnum (local.get $a)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; symbol? (40) + (if (i32.eq (local.get $id) (i32.const 40)) + (then + (if (call $is_symbol (local.get $a)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; string? (41) + (if (i32.eq (local.get $id) (i32.const 41)) + (then + (if (call $is_string (local.get $a)) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; procedure? (42) — closure or primitive + (if (i32.eq (local.get $id) (i32.const 42)) + (then + (if (i32.or (call $is_closure (local.get $a)) (call $is_primitive (local.get $a))) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; boolean? (43) + (if (i32.eq (local.get $id) (i32.const 43)) + (then + (if (i32.or (i32.eq (local.get $a) (global.get $TRUE)) + (i32.eq (local.get $a) (global.get $FALSE))) + (then (return (global.get $TRUE))) + (else (return (global.get $FALSE)))))) + + ;; caar (44) + (if (i32.eq (local.get $id) (i32.const 44)) + (then (return (call $car (call $car (local.get $a)))))) + + ;; cadr (45) + (if (i32.eq (local.get $id) (i32.const 45)) + (then (return (call $car (call $cdr (local.get $a)))))) + + ;; cdar (46) + (if (i32.eq (local.get $id) (i32.const 46)) + (then (return (call $cdr (call $car (local.get $a)))))) + + ;; cddr (47) + (if (i32.eq (local.get $id) (i32.const 47)) + (then (return (call $cdr (call $cdr (local.get $a)))))) + + ;; caddr (48) + (if (i32.eq (local.get $id) (i32.const 48)) + (then (return (call $car (call $cdr (call $cdr (local.get $a))))))) + + ;; cadddr (49) + (if (i32.eq (local.get $id) (i32.const 49)) + (then (return (call $car (call $cdr (call $cdr (call $cdr (local.get $a)))))))) + + ;; reverse (50) + (if (i32.eq (local.get $id) (i32.const 50)) + (then + (local.set $rev (global.get $NIL)) + (local.set $cur (local.get $a)) + (block $done + (loop $loop + (br_if $done (i32.eqz (call $is_pair (local.get $cur)))) + (local.set $rev (call $make_pair (call $car (local.get $cur)) (local.get $rev))) + (local.set $cur (call $cdr (local.get $cur))) + (br $loop))) + (return (local.get $rev)))) + + ;; append (51) — 2-arg + (if (i32.eq (local.get $id) (i32.const 51)) + (then (return (call $append2 (local.get $a) (local.get $b))))) + + ;; apply (52) — (apply f args) + (if (i32.eq (local.get $id) (i32.const 52)) + (then (return (call $apply (local.get $a) (local.get $b))))) + + ;; error (53) — emit message + raise (we just write to output for now) + (if (i32.eq (local.get $id) (i32.const 53)) + (then + (call $out_str (i32.const 0xF030) (i32.const 6)) ;; "

- asm tier note: the wat implementation ships a minimal lisp - subset (special forms, arithmetic, list ops, recursion) — enough for the - four demos above. symbol lookup is linear; would be moad-0001 at scale, - documented in asm/lumbda.wat. - see lumbda. + same lisp source, three implementations — verify yourself by + eyeballing each tier's output, or by running + make wasm-test against the cross-tier suite. + asm tier (asm/lumbda.wat) is growing toward feature parity + with asm/lumbda.s; see lumbda.