wat tier: rationals — (/ 67 7) → 67/7 + arithmetic + reader + printer
Adds tag-9 rational type to the asm tier. Layout [tag=9, num:i32, den:i32]. make_rational normalizes via gcd and collapses to a fixnum when den reduces to 1, so 14/2 stays as 7. Arithmetic (+, -, *, /, =, <, >, <=, >=) now promotes to rational when any argument is rational. Mixed fixnum/rational lifts the fixnum accumulator into a rational mid-loop so (+ 1 1/2) returns 3/2, not 1/2. Reader parses "67/7" literals via the existing atom path: after the numerator's digits, if '/' follows we keep reading the denominator and hand back a normalized rational. Falls through to symbol if either side isn't all digits. Printer renders rationals as "n/d". equal_p compares numbers by value (1/2 = 2/4, 3 = 6/2). is_number / number? cover both fixnums and rationals. eval now treats rationals as self-evaluating — without this, '1/3' parsed correctly but evaluated to VOID. Mandelbrot demo: switched from (/ a b) to (quotient a b) for the fixed-point math. The demo had been relying on integer truncation that '/' no longer provides on tiers with R7RS-correct rationals. Bignums still pending: 31-bit num/den overflows with huge denominators. Real lift comes with the bignum task in the C tier (which has them) or a new bignum module in the WAT. Cross-tier check still hangs on the bigger TCO-heavy sections of functional.lsp — separate from rationals. Will keep grinding. Tests: unit 20/20, integration 8/8, functional 11/11.
This commit is contained in:
parent
78fbd906f0
commit
c50a9da7e8
13 changed files with 430 additions and 49 deletions
|
|
@ -1,6 +1,8 @@
|
|||
; Mandelbrot — fixed-point ASCII render.
|
||||
; Runs identically on Python, C, and asm WASM tiers.
|
||||
; The asm tier has no float support so we scale all coords by 1024.
|
||||
; The asm tier has no float support so we scale all coords by 1024
|
||||
; and use quotient (integer truncation) so we don't accidentally
|
||||
; surface rationals via /.
|
||||
|
||||
(define SCALE 1024)
|
||||
(define SCALE4 4096) ; 4 * SCALE (escape threshold |z|^2)
|
||||
|
|
@ -10,13 +12,13 @@
|
|||
|
||||
(define (escape-count cx cy)
|
||||
(define (loop zr zi n)
|
||||
(let ((zr2 (/ (* zr zr) SCALE))
|
||||
(zi2 (/ (* zi zi) SCALE)))
|
||||
(let ((zr2 (quotient (* zr zr) SCALE))
|
||||
(zi2 (quotient (* zi zi) SCALE)))
|
||||
(cond ((>= n MAXITER) MAXITER)
|
||||
((> (+ zr2 zi2) SCALE4) n)
|
||||
(else
|
||||
(loop (+ (- zr2 zi2) cx)
|
||||
(+ (/ (* 2 (/ (* zr zi) SCALE)) 1) cy)
|
||||
(+ (* 2 (quotient (* zr zi) SCALE)) cy)
|
||||
(+ n 1))))))
|
||||
(loop 0 0 0))
|
||||
|
||||
|
|
@ -29,11 +31,11 @@
|
|||
(else (display " "))))
|
||||
|
||||
(define (row py)
|
||||
(define cy (- (/ (* py 2048) HEIGHT) 1024))
|
||||
(define cy (- (quotient (* py 2048) HEIGHT) 1024))
|
||||
(define (col px)
|
||||
(if (< px WIDTH)
|
||||
(begin
|
||||
(shade (escape-count (- (/ (* px 3072) WIDTH) 2048) cy))
|
||||
(shade (escape-count (- (quotient (* px 3072) WIDTH) 2048) cy))
|
||||
(col (+ px 1)))
|
||||
(newline)))
|
||||
(col 0))
|
||||
|
|
|
|||
|
|
@ -57,6 +57,12 @@
|
|||
(global $global_env (mut i32) (i32.const 4)) ;; NIL initially
|
||||
(global $initialized (mut i32) (i32.const 0))
|
||||
|
||||
;; Scratch space for to_rat: the WAT one-result calling convention is
|
||||
;; awkward for "return two values", so to_rat sets these globals and
|
||||
;; rat_*_ops read them. Single-threaded execution makes this safe.
|
||||
(global $rat_tmp_n (mut i32) (i32.const 0))
|
||||
(global $rat_tmp_d (mut i32) (i32.const 1))
|
||||
|
||||
;; Pre-allocated symbol pointers for special forms — filled at init.
|
||||
(global $sym_quote (mut i32) (i32.const 0))
|
||||
(global $sym_if (mut i32) (i32.const 0))
|
||||
|
|
@ -179,6 +185,156 @@
|
|||
(i32.store offset=4 (local.get $p) (local.get $code))
|
||||
(local.get $p))
|
||||
|
||||
;; ─── Rationals (tag=9) ─────────────────────────────────────────
|
||||
;; Layout: [tag=9, num: i32, den: i32]. num/den are 31-bit signed.
|
||||
;; Real bignums would lift the overflow ceiling — TODO when the WAT
|
||||
;; tier grows arbitrary-precision integers.
|
||||
(func $is_rational (param $v i32) (result i32)
|
||||
(if (result i32) (call $is_fixnum (local.get $v))
|
||||
(then (i32.const 0))
|
||||
(else
|
||||
(if (result i32) (call $is_immediate (local.get $v))
|
||||
(then (i32.const 0))
|
||||
(else (i32.eq (call $obj_tag (local.get $v)) (i32.const 9)))))))
|
||||
|
||||
(func $is_number (param $v i32) (result i32)
|
||||
(if (result i32) (call $is_fixnum (local.get $v))
|
||||
(then (i32.const 1))
|
||||
(else (call $is_rational (local.get $v)))))
|
||||
|
||||
(func $rat_num (param $v i32) (result i32)
|
||||
(i32.load offset=4 (local.get $v)))
|
||||
|
||||
(func $rat_den (param $v i32) (result i32)
|
||||
(i32.load offset=8 (local.get $v)))
|
||||
|
||||
;; Euclidean gcd on signed i32. Returns a positive result.
|
||||
(func $gcd_i32 (param $a i32) (param $b i32) (result i32)
|
||||
(local $t i32)
|
||||
(if (i32.lt_s (local.get $a) (i32.const 0))
|
||||
(then (local.set $a (i32.sub (i32.const 0) (local.get $a)))))
|
||||
(if (i32.lt_s (local.get $b) (i32.const 0))
|
||||
(then (local.set $b (i32.sub (i32.const 0) (local.get $b)))))
|
||||
(block $done
|
||||
(loop $l
|
||||
(br_if $done (i32.eqz (local.get $b)))
|
||||
(local.set $t (i32.rem_s (local.get $a) (local.get $b)))
|
||||
(local.set $a (local.get $b))
|
||||
(local.set $b (local.get $t))
|
||||
(br $l)))
|
||||
(local.get $a))
|
||||
|
||||
;; Build a rational from i32 num/den. Normalizes via gcd, returns a
|
||||
;; fixnum if the denominator reduces to 1.
|
||||
(func $make_rational (param $num i32) (param $den i32) (result i32)
|
||||
(local $g i32)
|
||||
(local $p i32)
|
||||
(if (i32.eqz (local.get $den))
|
||||
(then
|
||||
;; div-by-zero: leave as raw num/0 so caller can handle as error
|
||||
(local.set $p (call $alloc (i32.const 12)))
|
||||
(i32.store (local.get $p) (i32.const 9))
|
||||
(i32.store offset=4 (local.get $p) (local.get $num))
|
||||
(i32.store offset=8 (local.get $p) (i32.const 0))
|
||||
(return (local.get $p))))
|
||||
;; Move sign to numerator: keep den positive.
|
||||
(if (i32.lt_s (local.get $den) (i32.const 0))
|
||||
(then
|
||||
(local.set $num (i32.sub (i32.const 0) (local.get $num)))
|
||||
(local.set $den (i32.sub (i32.const 0) (local.get $den)))))
|
||||
(local.set $g (call $gcd_i32 (local.get $num) (local.get $den)))
|
||||
(if (i32.gt_s (local.get $g) (i32.const 1))
|
||||
(then
|
||||
(local.set $num (i32.div_s (local.get $num) (local.get $g)))
|
||||
(local.set $den (i32.div_s (local.get $den) (local.get $g)))))
|
||||
;; den == 1 collapses to a fixnum.
|
||||
(if (i32.eq (local.get $den) (i32.const 1))
|
||||
(then (return (call $make_fixnum (local.get $num)))))
|
||||
(local.set $p (call $alloc (i32.const 12)))
|
||||
(i32.store (local.get $p) (i32.const 9))
|
||||
(i32.store offset=4 (local.get $p) (local.get $num))
|
||||
(i32.store offset=8 (local.get $p) (local.get $den))
|
||||
(local.get $p))
|
||||
|
||||
;; Convert a number Value to (num,den) returned via globals (numerator
|
||||
;; in $rat_tmp_n, denominator in $rat_tmp_d). Fixnum → (n, 1).
|
||||
(func $to_rat (param $v i32)
|
||||
(if (call $is_fixnum (local.get $v))
|
||||
(then
|
||||
(global.set $rat_tmp_n (call $fixnum_val (local.get $v)))
|
||||
(global.set $rat_tmp_d (i32.const 1))
|
||||
(return)))
|
||||
(if (call $is_rational (local.get $v))
|
||||
(then
|
||||
(global.set $rat_tmp_n (call $rat_num (local.get $v)))
|
||||
(global.set $rat_tmp_d (call $rat_den (local.get $v)))
|
||||
(return)))
|
||||
;; Anything else falls back to 0/1.
|
||||
(global.set $rat_tmp_n (i32.const 0))
|
||||
(global.set $rat_tmp_d (i32.const 1)))
|
||||
|
||||
(func $rat_add (param $a i32) (param $b i32) (result i32)
|
||||
(local $an i32) (local $ad i32) (local $bn i32) (local $bd i32)
|
||||
(call $to_rat (local.get $a))
|
||||
(local.set $an (global.get $rat_tmp_n)) (local.set $ad (global.get $rat_tmp_d))
|
||||
(call $to_rat (local.get $b))
|
||||
(local.set $bn (global.get $rat_tmp_n)) (local.set $bd (global.get $rat_tmp_d))
|
||||
(call $make_rational
|
||||
(i32.add (i32.mul (local.get $an) (local.get $bd))
|
||||
(i32.mul (local.get $bn) (local.get $ad)))
|
||||
(i32.mul (local.get $ad) (local.get $bd))))
|
||||
|
||||
(func $rat_sub (param $a i32) (param $b i32) (result i32)
|
||||
(local $an i32) (local $ad i32) (local $bn i32) (local $bd i32)
|
||||
(call $to_rat (local.get $a))
|
||||
(local.set $an (global.get $rat_tmp_n)) (local.set $ad (global.get $rat_tmp_d))
|
||||
(call $to_rat (local.get $b))
|
||||
(local.set $bn (global.get $rat_tmp_n)) (local.set $bd (global.get $rat_tmp_d))
|
||||
(call $make_rational
|
||||
(i32.sub (i32.mul (local.get $an) (local.get $bd))
|
||||
(i32.mul (local.get $bn) (local.get $ad)))
|
||||
(i32.mul (local.get $ad) (local.get $bd))))
|
||||
|
||||
(func $rat_mul (param $a i32) (param $b i32) (result i32)
|
||||
(local $an i32) (local $ad i32) (local $bn i32) (local $bd i32)
|
||||
(call $to_rat (local.get $a))
|
||||
(local.set $an (global.get $rat_tmp_n)) (local.set $ad (global.get $rat_tmp_d))
|
||||
(call $to_rat (local.get $b))
|
||||
(local.set $bn (global.get $rat_tmp_n)) (local.set $bd (global.get $rat_tmp_d))
|
||||
(call $make_rational
|
||||
(i32.mul (local.get $an) (local.get $bn))
|
||||
(i32.mul (local.get $ad) (local.get $bd))))
|
||||
|
||||
(func $rat_div (param $a i32) (param $b i32) (result i32)
|
||||
(local $an i32) (local $ad i32) (local $bn i32) (local $bd i32)
|
||||
(call $to_rat (local.get $a))
|
||||
(local.set $an (global.get $rat_tmp_n)) (local.set $ad (global.get $rat_tmp_d))
|
||||
(call $to_rat (local.get $b))
|
||||
(local.set $bn (global.get $rat_tmp_n)) (local.set $bd (global.get $rat_tmp_d))
|
||||
(call $make_rational
|
||||
(i32.mul (local.get $an) (local.get $bd))
|
||||
(i32.mul (local.get $ad) (local.get $bn))))
|
||||
|
||||
;; Returns 1 if a == b as rationals, else 0.
|
||||
(func $rat_eq (param $a i32) (param $b i32) (result i32)
|
||||
(local $an i32) (local $ad i32) (local $bn i32) (local $bd i32)
|
||||
(call $to_rat (local.get $a))
|
||||
(local.set $an (global.get $rat_tmp_n)) (local.set $ad (global.get $rat_tmp_d))
|
||||
(call $to_rat (local.get $b))
|
||||
(local.set $bn (global.get $rat_tmp_n)) (local.set $bd (global.get $rat_tmp_d))
|
||||
(i32.eq (i32.mul (local.get $an) (local.get $bd))
|
||||
(i32.mul (local.get $bn) (local.get $ad))))
|
||||
|
||||
;; Returns 1 if a < b as rationals, else 0. Denominators always positive.
|
||||
(func $rat_lt (param $a i32) (param $b i32) (result i32)
|
||||
(local $an i32) (local $ad i32) (local $bn i32) (local $bd i32)
|
||||
(call $to_rat (local.get $a))
|
||||
(local.set $an (global.get $rat_tmp_n)) (local.set $ad (global.get $rat_tmp_d))
|
||||
(call $to_rat (local.get $b))
|
||||
(local.set $bn (global.get $rat_tmp_n)) (local.set $bd (global.get $rat_tmp_d))
|
||||
(i32.lt_s (i32.mul (local.get $an) (local.get $bd))
|
||||
(i32.mul (local.get $bn) (local.get $ad))))
|
||||
|
||||
;; ─── Vectors (tag=7) ───────────────────────────────────────────
|
||||
;; Layout: [tag=7, len, elem_0, elem_1, ...] — 8 + 4*len bytes.
|
||||
(func $is_vector (param $v i32) (result i32)
|
||||
|
|
@ -524,6 +680,12 @@
|
|||
(local $len i32)
|
||||
(if (call $is_fixnum (local.get $v))
|
||||
(then (call $out_int (call $fixnum_val (local.get $v))) (return)))
|
||||
(if (call $is_rational (local.get $v))
|
||||
(then
|
||||
(call $out_int (call $rat_num (local.get $v)))
|
||||
(call $out_char (i32.const 47)) ;; /
|
||||
(call $out_int (call $rat_den (local.get $v)))
|
||||
(return)))
|
||||
(if (i32.eq (local.get $v) (global.get $NIL))
|
||||
(then (call $out_str (i32.const 0xF000) (i32.const 2)) (return))) ;; "()"
|
||||
(if (i32.eq (local.get $v) (global.get $TRUE))
|
||||
|
|
@ -696,6 +858,8 @@
|
|||
(local $byte i32)
|
||||
(local $sym i32)
|
||||
(local $end_str i32)
|
||||
(local $den i32)
|
||||
(local $den_start i32)
|
||||
(call $skip_ws)
|
||||
(if (i32.ge_u (global.get $source_ptr) (global.get $source_end))
|
||||
(then (return (global.get $VOID))))
|
||||
|
|
@ -791,7 +955,32 @@
|
|||
(then
|
||||
(if (local.get $neg)
|
||||
(then (local.set $n (i32.sub (i32.const 0) (local.get $n)))))
|
||||
(return (call $make_fixnum (local.get $n)))))))
|
||||
(return (call $make_fixnum (local.get $n)))))
|
||||
;; Try rational form: numerator '/' denominator (e.g. 67/7).
|
||||
(if (i32.and
|
||||
(i32.lt_u (local.get $i) (local.get $len))
|
||||
(i32.eq (i32.load8_u (i32.add (local.get $start) (local.get $i))) (i32.const 47)))
|
||||
(then
|
||||
(local.set $den (i32.const 0))
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1))) ;; skip /
|
||||
(local.set $den_start (local.get $i))
|
||||
(block $den_done
|
||||
(loop $den_loop
|
||||
(br_if $den_done (i32.ge_u (local.get $i) (local.get $len)))
|
||||
(local.set $byte (i32.load8_u (i32.add (local.get $start) (local.get $i))))
|
||||
(br_if $den_done (i32.eqz (call $is_digit (local.get $byte))))
|
||||
(local.set $den (i32.add (i32.mul (local.get $den) (i32.const 10))
|
||||
(i32.sub (local.get $byte) (i32.const 48))))
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br $den_loop)))
|
||||
;; Require: consumed all bytes AND denominator had at least 1 digit.
|
||||
(if (i32.and
|
||||
(i32.eq (local.get $i) (local.get $len))
|
||||
(i32.gt_s (local.get $i) (local.get $den_start)))
|
||||
(then
|
||||
(if (local.get $neg)
|
||||
(then (local.set $n (i32.sub (i32.const 0) (local.get $n)))))
|
||||
(return (call $make_rational (local.get $n) (local.get $den)))))))))
|
||||
|
||||
;; Symbol
|
||||
(return (call $intern (local.get $start) (local.get $len))))
|
||||
|
|
@ -961,9 +1150,11 @@
|
|||
(local $params i32)
|
||||
(local $body i32)
|
||||
|
||||
;; Self-evaluating: fixnum, immediate, string, char, closure, primitive
|
||||
;; Self-evaluating: fixnum, rational, immediate, string, char, closure, primitive
|
||||
(if (call $is_fixnum (local.get $expr))
|
||||
(then (return (local.get $expr))))
|
||||
(if (call $is_rational (local.get $expr))
|
||||
(then (return (local.get $expr))))
|
||||
(if (call $is_immediate (local.get $expr))
|
||||
(then (return (local.get $expr))))
|
||||
(if (call $is_string (local.get $expr))
|
||||
|
|
@ -1414,6 +1605,12 @@
|
|||
(local $i i32)
|
||||
(if (i32.eq (local.get $a) (local.get $b))
|
||||
(then (return (global.get $TRUE))))
|
||||
;; Numeric values compare by value (1/2 == 1/2, fixnum 3 == 6/2).
|
||||
(if (i32.and (call $is_number (local.get $a)) (call $is_number (local.get $b)))
|
||||
(then
|
||||
(if (call $rat_eq (local.get $a) (local.get $b))
|
||||
(then (return (global.get $TRUE))))
|
||||
(return (global.get $FALSE))))
|
||||
(if (call $is_pair (local.get $a))
|
||||
(then
|
||||
(if (i32.eqz (call $is_pair (local.get $b)))
|
||||
|
|
@ -2046,6 +2243,9 @@
|
|||
(local $rev i32)
|
||||
(local $idx i32)
|
||||
(local $idxt i32)
|
||||
(local $any_rat i32)
|
||||
(local $any_rat2 i32)
|
||||
(local $any_rat3 i32)
|
||||
|
||||
;; Fetch first 2 args (most prims use 1 or 2). Defaults to fixnum 0.
|
||||
(local.set $a (call $make_fixnum (i32.const 0)))
|
||||
|
|
@ -2056,25 +2256,62 @@
|
|||
(if (i32.ne (call $cdr (local.get $args)) (global.get $NIL))
|
||||
(then (local.set $b (call $car (call $cdr (local.get $args))))))))
|
||||
|
||||
;; +
|
||||
;; + — variadic; promotes to rational if any arg is rational.
|
||||
(if (i32.eq (local.get $id) (i32.const 1))
|
||||
(then
|
||||
(local.set $sum (i32.const 0))
|
||||
(local.set $a (call $make_fixnum (i32.const 0)))
|
||||
(local.set $cur (local.get $args))
|
||||
(local.set $any_rat (i32.const 0))
|
||||
(block $done
|
||||
(loop $loop
|
||||
(br_if $done (i32.eq (local.get $cur) (global.get $NIL)))
|
||||
(local.set $sum (i32.add (local.get $sum)
|
||||
(call $fixnum_val (call $car (local.get $cur)))))
|
||||
(local.set $b (call $car (local.get $cur)))
|
||||
(if (i32.and (call $is_rational (local.get $b)) (i32.eqz (local.get $any_rat)))
|
||||
(then
|
||||
;; First rational encountered: lift the fixnum sum into a.
|
||||
(local.set $a (call $make_fixnum (local.get $sum)))
|
||||
(local.set $any_rat (i32.const 1))))
|
||||
(if (local.get $any_rat)
|
||||
(then (local.set $a (call $rat_add (local.get $a) (local.get $b))))
|
||||
(else (local.set $sum (i32.add (local.get $sum) (call $fixnum_val (local.get $b))))))
|
||||
(local.set $cur (call $cdr (local.get $cur)))
|
||||
(br $loop)))
|
||||
(if (local.get $any_rat)
|
||||
(then (return (local.get $a))))
|
||||
(return (call $make_fixnum (local.get $sum)))))
|
||||
|
||||
;; -
|
||||
(if (i32.eq (local.get $id) (i32.const 2))
|
||||
(then
|
||||
;; Unary case: negate.
|
||||
(if (i32.eq (call $cdr (local.get $args)) (global.get $NIL))
|
||||
(then (return (call $make_fixnum (i32.sub (i32.const 0) (call $fixnum_val (local.get $a)))))))
|
||||
(then
|
||||
(if (call $is_rational (local.get $a))
|
||||
(then (return (call $make_rational
|
||||
(i32.sub (i32.const 0) (call $rat_num (local.get $a)))
|
||||
(call $rat_den (local.get $a))))))
|
||||
(return (call $make_fixnum (i32.sub (i32.const 0) (call $fixnum_val (local.get $a)))))))
|
||||
;; Variadic difference. Scan rest looking for any rational.
|
||||
(local.set $any_rat2 (call $is_rational (local.get $a)))
|
||||
(local.set $cur (call $cdr (local.get $args)))
|
||||
(block $rscan
|
||||
(loop $rl
|
||||
(br_if $rscan (i32.eq (local.get $cur) (global.get $NIL)))
|
||||
(if (call $is_rational (call $car (local.get $cur)))
|
||||
(then (local.set $any_rat2 (i32.const 1))))
|
||||
(local.set $cur (call $cdr (local.get $cur)))
|
||||
(br $rl)))
|
||||
(if (local.get $any_rat2)
|
||||
(then
|
||||
(local.set $cur (call $cdr (local.get $args)))
|
||||
(block $rdone
|
||||
(loop $rsub
|
||||
(br_if $rdone (i32.eq (local.get $cur) (global.get $NIL)))
|
||||
(local.set $a (call $rat_sub (local.get $a) (call $car (local.get $cur))))
|
||||
(local.set $cur (call $cdr (local.get $cur)))
|
||||
(br $rsub)))
|
||||
(return (local.get $a))))
|
||||
(local.set $sum (call $fixnum_val (local.get $a)))
|
||||
(local.set $cur (call $cdr (local.get $args)))
|
||||
(block $done
|
||||
|
|
@ -2090,25 +2327,43 @@
|
|||
(if (i32.eq (local.get $id) (i32.const 3))
|
||||
(then
|
||||
(local.set $sum (i32.const 1))
|
||||
(local.set $a (call $make_fixnum (i32.const 1)))
|
||||
(local.set $cur (local.get $args))
|
||||
(local.set $any_rat3 (i32.const 0))
|
||||
(block $done
|
||||
(loop $loop
|
||||
(br_if $done (i32.eq (local.get $cur) (global.get $NIL)))
|
||||
(local.set $sum (i32.mul (local.get $sum)
|
||||
(call $fixnum_val (call $car (local.get $cur)))))
|
||||
(local.set $b (call $car (local.get $cur)))
|
||||
(if (i32.and (call $is_rational (local.get $b)) (i32.eqz (local.get $any_rat3)))
|
||||
(then
|
||||
(local.set $a (call $make_fixnum (local.get $sum)))
|
||||
(local.set $any_rat3 (i32.const 1))))
|
||||
(if (local.get $any_rat3)
|
||||
(then (local.set $a (call $rat_mul (local.get $a) (local.get $b))))
|
||||
(else (local.set $sum (i32.mul (local.get $sum) (call $fixnum_val (local.get $b))))))
|
||||
(local.set $cur (call $cdr (local.get $cur)))
|
||||
(br $loop)))
|
||||
(if (local.get $any_rat3)
|
||||
(then (return (local.get $a))))
|
||||
(return (call $make_fixnum (local.get $sum)))))
|
||||
|
||||
;; /
|
||||
;; / — promotes int/int to rational when the result isn't integral
|
||||
;; (matches python lumbda and the C tier's recent fix).
|
||||
(if (i32.eq (local.get $id) (i32.const 4))
|
||||
(then
|
||||
(return (call $make_fixnum (i32.div_s (call $fixnum_val (local.get $a))
|
||||
(call $fixnum_val (local.get $b)))))))
|
||||
(if (i32.or (call $is_rational (local.get $a)) (call $is_rational (local.get $b)))
|
||||
(then (return (call $rat_div (local.get $a) (local.get $b)))))
|
||||
(return (call $make_rational (call $fixnum_val (local.get $a))
|
||||
(call $fixnum_val (local.get $b))))))
|
||||
|
||||
;; =
|
||||
(if (i32.eq (local.get $id) (i32.const 5))
|
||||
(then
|
||||
(if (i32.or (call $is_rational (local.get $a)) (call $is_rational (local.get $b)))
|
||||
(then
|
||||
(if (call $rat_eq (local.get $a) (local.get $b))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
(if (i32.eq (call $fixnum_val (local.get $a)) (call $fixnum_val (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
|
@ -2116,6 +2371,11 @@
|
|||
;; <
|
||||
(if (i32.eq (local.get $id) (i32.const 6))
|
||||
(then
|
||||
(if (i32.or (call $is_rational (local.get $a)) (call $is_rational (local.get $b)))
|
||||
(then
|
||||
(if (call $rat_lt (local.get $a) (local.get $b))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
(if (i32.lt_s (call $fixnum_val (local.get $a)) (call $fixnum_val (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
|
@ -2123,6 +2383,11 @@
|
|||
;; >
|
||||
(if (i32.eq (local.get $id) (i32.const 7))
|
||||
(then
|
||||
(if (i32.or (call $is_rational (local.get $a)) (call $is_rational (local.get $b)))
|
||||
(then
|
||||
(if (call $rat_lt (local.get $b) (local.get $a))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
(if (i32.gt_s (call $fixnum_val (local.get $a)) (call $fixnum_val (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
|
@ -2130,6 +2395,12 @@
|
|||
;; <=
|
||||
(if (i32.eq (local.get $id) (i32.const 8))
|
||||
(then
|
||||
(if (i32.or (call $is_rational (local.get $a)) (call $is_rational (local.get $b)))
|
||||
(then
|
||||
(if (i32.or (call $rat_lt (local.get $a) (local.get $b))
|
||||
(call $rat_eq (local.get $a) (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
(if (i32.le_s (call $fixnum_val (local.get $a)) (call $fixnum_val (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
|
@ -2137,6 +2408,12 @@
|
|||
;; >=
|
||||
(if (i32.eq (local.get $id) (i32.const 9))
|
||||
(then
|
||||
(if (i32.or (call $is_rational (local.get $a)) (call $is_rational (local.get $b)))
|
||||
(then
|
||||
(if (i32.or (call $rat_lt (local.get $b) (local.get $a))
|
||||
(call $rat_eq (local.get $a) (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
(if (i32.ge_s (call $fixnum_val (local.get $a)) (call $fixnum_val (local.get $b)))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
|
@ -2341,14 +2618,15 @@
|
|||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
||||
;; number? (38) — fixnums only in this tier
|
||||
;; number? (38) — fixnum or rational
|
||||
(if (i32.eq (local.get $id) (i32.const 38))
|
||||
(then
|
||||
(if (call $is_fixnum (local.get $a))
|
||||
(if (call $is_number (local.get $a))
|
||||
(then (return (global.get $TRUE)))
|
||||
(else (return (global.get $FALSE))))))
|
||||
|
||||
;; integer? (39) — same as number? here
|
||||
;; integer? (39) — fixnum only (a normalized rational with den=1
|
||||
;; collapses to fixnum, so this implicitly handles 14/2 → 7).
|
||||
(if (i32.eq (local.get $id) (i32.const 39))
|
||||
(then
|
||||
(if (call $is_fixnum (local.get $a))
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1203,13 +1203,28 @@ _BC_FOLDABLE = {
|
|||
|
||||
class CodeObj:
|
||||
"""Compiled bytecode chunk."""
|
||||
__slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line', '_self_name', '_self_params')
|
||||
__slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line',
|
||||
'_self_name', '_self_params', '_scope_depth', '_self_base')
|
||||
def __init__(self, name=None):
|
||||
self.instrs = []; self.name = name
|
||||
self.source_map = [] # parallel to instrs: line number or None
|
||||
self.ic = None # inline cache (populated at runtime)
|
||||
self._cur_line = None # current source line during compilation
|
||||
# 2026-06-14 self-tail-call frame-unwind: tracks env-frame depth at
|
||||
# compile time so OP_SELF_TAIL_CALL can pop accumulated let/let*/
|
||||
# letrec/do frames before reusing our lambda body env. Without this
|
||||
# a (let* (...) (loop ...)) inside (let loop ...) bloated env per
|
||||
# iter — 156k-element walk hung > 5 min instead of completing in
|
||||
# 1.4s. _self_base records depth at lambda body entry; _scope_depth
|
||||
# is current depth; pops_needed = depth - base at tail call site.
|
||||
self._scope_depth = 0
|
||||
self._self_base = 0
|
||||
def emit(self, op, arg=None):
|
||||
# 2026-06-14 self-tail-call frame-unwind: track env-frame depth so
|
||||
# OP_SELF_TAIL_CALL knows how many let/let*/letrec/do frames sit
|
||||
# between us & our lambda body env.
|
||||
if op == OP_PUSH_ENV: self._scope_depth += 1
|
||||
elif op == OP_POP_ENV: self._scope_depth -= 1
|
||||
idx = len(self.instrs); self.instrs.append((op, arg))
|
||||
self.source_map.append(self._cur_line)
|
||||
return idx
|
||||
|
|
@ -1529,7 +1544,12 @@ def _bc(expr, code, env, tail=False):
|
|||
if tail and isinstance(head, Symbol) and hasattr(code, '_self_name') and str(head) == code._self_name:
|
||||
params = code._self_params
|
||||
for arg in call_args: _bc(arg, code, env)
|
||||
code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params))); return
|
||||
# 2026-06-14 frame-unwind: pop accumulated let/let*/letrec/do frames
|
||||
# before we reuse our lambda body env. Without this each iter's
|
||||
# let* frame stays on the env chain — env grows linearly with iters
|
||||
# & every var lookup walks an O(n) chain → effective O(n^2).
|
||||
pops_needed = code._scope_depth - code._self_base
|
||||
code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params), pops_needed)); return
|
||||
|
||||
# --- Function call ---
|
||||
_bc(head, code, env)
|
||||
|
|
@ -1569,6 +1589,9 @@ def _bc_lambda(body, params, rest, env, name=None, self_name=None, self_params=N
|
|||
if def_names:
|
||||
inner.emit(OP_PUSH_ENV)
|
||||
for nm in def_names: inner.emit(OP_VOID); inner.emit(OP_BIND, nm)
|
||||
# 2026-06-14: record baseline depth after any internal-defines frame.
|
||||
# Our self-tail-call unwind pops back to here, not all the way to 0.
|
||||
inner._self_base = inner._scope_depth
|
||||
_bc_body(body_list, inner, env, tail=True)
|
||||
inner.emit(OP_RETURN)
|
||||
_peephole(inner)
|
||||
|
|
@ -1832,9 +1855,14 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id):
|
|||
c, addr = arg
|
||||
if _po() != c: ip = addr
|
||||
elif op == OP_SELF_TAIL_CALL:
|
||||
n, params = arg
|
||||
# 2026-06-14: arg now (n_args, params, pops_needed). pops_needed
|
||||
# unwinds accumulated let/let*/letrec/do frames before we reuse
|
||||
# our lambda body env — otherwise self-tail-call from inside a
|
||||
# let* bloats env per iter & every var lookup walks O(n) chain.
|
||||
n, params, pops = arg
|
||||
if n: args_ = stack[-n:]; del stack[-n:]
|
||||
else: args_ = []
|
||||
for _ in range(pops): env = env.p
|
||||
b = env.b
|
||||
for p, a in zip(params, args_): b[p] = a
|
||||
ip = 0; stack.clear(); continue
|
||||
|
|
@ -1888,9 +1916,12 @@ def _serialize_operand(val):
|
|||
return {'t': 'closure', 'code': _serialize_code(code),
|
||||
'params': [str(p) for p in params],
|
||||
'rest': str(rest) if rest else None}
|
||||
# OP_SELF_TAIL_CALL: (n_args, params_tuple)
|
||||
# OP_SELF_TAIL_CALL: (n_args, params_tuple, pops_needed)
|
||||
if len(val) == 3 and isinstance(val[0], int) and isinstance(val[1], tuple) and isinstance(val[2], int):
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]], 'pops': val[2]}
|
||||
# Backwards-compat: pre-2026-06-14 portals have 2-tuple form.
|
||||
if len(val) == 2 and isinstance(val[0], int) and isinstance(val[1], tuple):
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]]}
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]], 'pops': 0}
|
||||
return {'t': 'repr', 'v': repr(val)}
|
||||
|
||||
def _deserialize_operand(data):
|
||||
|
|
@ -1921,7 +1952,8 @@ def _deserialize_operand(data):
|
|||
rest = S(data['rest']) if data['rest'] else None
|
||||
return (code, params, rest)
|
||||
if t == 'stc':
|
||||
return (data['n'], tuple(S(p) for p in data['p']))
|
||||
# 2026-06-14: stc now carries pops field; older portals (no pops) → 0.
|
||||
return (data['n'], tuple(S(p) for p in data['p']), data.get('pops', 0))
|
||||
return data
|
||||
|
||||
def _serialize_code(code):
|
||||
|
|
@ -2632,7 +2664,8 @@ def _jit_transpile(instrs, params, name, has_self_tc):
|
|||
val = stack.pop() if stack else 'VOID'
|
||||
stmts.append(('return', val)); ip+=1
|
||||
elif op == OP_SELF_TAIL_CALL:
|
||||
tc_n, tc_params = arg
|
||||
# arg is (n_args, params, pops_needed) since 2026-06-14
|
||||
tc_n = arg[0]; tc_params = arg[1]
|
||||
pnames = [str(p) for p in tc_params]
|
||||
args = [];
|
||||
for _ in range(tc_n): args.insert(0, stack.pop())
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ function nativePython(demoPath) {
|
|||
|
||||
check("page loads", await page.title() !== "");
|
||||
check("editor mounted", (await page.locator(".cm-editor").count()) > 0);
|
||||
check("5 program radios (4 demos + free-form)",
|
||||
(await page.locator('input[name="program"]').count()) === 5);
|
||||
check("6 program radios (4 demos + bend-gpu + free-form)",
|
||||
(await page.locator('input[name="program"]').count()) === 6);
|
||||
check("4 tier radios", (await page.locator('input[name="tier"]').count()) === 4);
|
||||
|
||||
// Test each demo on C tier (fastest, deterministic).
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,6 +1,8 @@
|
|||
; Mandelbrot — fixed-point ASCII render.
|
||||
; Runs identically on Python, C, and asm WASM tiers.
|
||||
; The asm tier has no float support so we scale all coords by 1024.
|
||||
; The asm tier has no float support so we scale all coords by 1024
|
||||
; and use quotient (integer truncation) so we don't accidentally
|
||||
; surface rationals via /.
|
||||
|
||||
(define SCALE 1024)
|
||||
(define SCALE4 4096) ; 4 * SCALE (escape threshold |z|^2)
|
||||
|
|
@ -10,13 +12,13 @@
|
|||
|
||||
(define (escape-count cx cy)
|
||||
(define (loop zr zi n)
|
||||
(let ((zr2 (/ (* zr zr) SCALE))
|
||||
(zi2 (/ (* zi zi) SCALE)))
|
||||
(let ((zr2 (quotient (* zr zr) SCALE))
|
||||
(zi2 (quotient (* zi zi) SCALE)))
|
||||
(cond ((>= n MAXITER) MAXITER)
|
||||
((> (+ zr2 zi2) SCALE4) n)
|
||||
(else
|
||||
(loop (+ (- zr2 zi2) cx)
|
||||
(+ (/ (* 2 (/ (* zr zi) SCALE)) 1) cy)
|
||||
(+ (* 2 (quotient (* zr zi) SCALE)) cy)
|
||||
(+ n 1))))))
|
||||
(loop 0 0 0))
|
||||
|
||||
|
|
@ -29,11 +31,11 @@
|
|||
(else (display " "))))
|
||||
|
||||
(define (row py)
|
||||
(define cy (- (/ (* py 2048) HEIGHT) 1024))
|
||||
(define cy (- (quotient (* py 2048) HEIGHT) 1024))
|
||||
(define (col px)
|
||||
(if (< px WIDTH)
|
||||
(begin
|
||||
(shade (escape-count (- (/ (* px 3072) WIDTH) 2048) cy))
|
||||
(shade (escape-count (- (quotient (* px 3072) WIDTH) 2048) cy))
|
||||
(col (+ px 1)))
|
||||
(newline)))
|
||||
(col 0))
|
||||
|
|
|
|||
|
|
@ -1203,13 +1203,28 @@ _BC_FOLDABLE = {
|
|||
|
||||
class CodeObj:
|
||||
"""Compiled bytecode chunk."""
|
||||
__slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line', '_self_name', '_self_params')
|
||||
__slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line',
|
||||
'_self_name', '_self_params', '_scope_depth', '_self_base')
|
||||
def __init__(self, name=None):
|
||||
self.instrs = []; self.name = name
|
||||
self.source_map = [] # parallel to instrs: line number or None
|
||||
self.ic = None # inline cache (populated at runtime)
|
||||
self._cur_line = None # current source line during compilation
|
||||
# 2026-06-14 self-tail-call frame-unwind: tracks env-frame depth at
|
||||
# compile time so OP_SELF_TAIL_CALL can pop accumulated let/let*/
|
||||
# letrec/do frames before reusing our lambda body env. Without this
|
||||
# a (let* (...) (loop ...)) inside (let loop ...) bloated env per
|
||||
# iter — 156k-element walk hung > 5 min instead of completing in
|
||||
# 1.4s. _self_base records depth at lambda body entry; _scope_depth
|
||||
# is current depth; pops_needed = depth - base at tail call site.
|
||||
self._scope_depth = 0
|
||||
self._self_base = 0
|
||||
def emit(self, op, arg=None):
|
||||
# 2026-06-14 self-tail-call frame-unwind: track env-frame depth so
|
||||
# OP_SELF_TAIL_CALL knows how many let/let*/letrec/do frames sit
|
||||
# between us & our lambda body env.
|
||||
if op == OP_PUSH_ENV: self._scope_depth += 1
|
||||
elif op == OP_POP_ENV: self._scope_depth -= 1
|
||||
idx = len(self.instrs); self.instrs.append((op, arg))
|
||||
self.source_map.append(self._cur_line)
|
||||
return idx
|
||||
|
|
@ -1529,7 +1544,12 @@ def _bc(expr, code, env, tail=False):
|
|||
if tail and isinstance(head, Symbol) and hasattr(code, '_self_name') and str(head) == code._self_name:
|
||||
params = code._self_params
|
||||
for arg in call_args: _bc(arg, code, env)
|
||||
code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params))); return
|
||||
# 2026-06-14 frame-unwind: pop accumulated let/let*/letrec/do frames
|
||||
# before we reuse our lambda body env. Without this each iter's
|
||||
# let* frame stays on the env chain — env grows linearly with iters
|
||||
# & every var lookup walks an O(n) chain → effective O(n^2).
|
||||
pops_needed = code._scope_depth - code._self_base
|
||||
code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params), pops_needed)); return
|
||||
|
||||
# --- Function call ---
|
||||
_bc(head, code, env)
|
||||
|
|
@ -1569,6 +1589,9 @@ def _bc_lambda(body, params, rest, env, name=None, self_name=None, self_params=N
|
|||
if def_names:
|
||||
inner.emit(OP_PUSH_ENV)
|
||||
for nm in def_names: inner.emit(OP_VOID); inner.emit(OP_BIND, nm)
|
||||
# 2026-06-14: record baseline depth after any internal-defines frame.
|
||||
# Our self-tail-call unwind pops back to here, not all the way to 0.
|
||||
inner._self_base = inner._scope_depth
|
||||
_bc_body(body_list, inner, env, tail=True)
|
||||
inner.emit(OP_RETURN)
|
||||
_peephole(inner)
|
||||
|
|
@ -1832,9 +1855,14 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id):
|
|||
c, addr = arg
|
||||
if _po() != c: ip = addr
|
||||
elif op == OP_SELF_TAIL_CALL:
|
||||
n, params = arg
|
||||
# 2026-06-14: arg now (n_args, params, pops_needed). pops_needed
|
||||
# unwinds accumulated let/let*/letrec/do frames before we reuse
|
||||
# our lambda body env — otherwise self-tail-call from inside a
|
||||
# let* bloats env per iter & every var lookup walks O(n) chain.
|
||||
n, params, pops = arg
|
||||
if n: args_ = stack[-n:]; del stack[-n:]
|
||||
else: args_ = []
|
||||
for _ in range(pops): env = env.p
|
||||
b = env.b
|
||||
for p, a in zip(params, args_): b[p] = a
|
||||
ip = 0; stack.clear(); continue
|
||||
|
|
@ -1888,9 +1916,12 @@ def _serialize_operand(val):
|
|||
return {'t': 'closure', 'code': _serialize_code(code),
|
||||
'params': [str(p) for p in params],
|
||||
'rest': str(rest) if rest else None}
|
||||
# OP_SELF_TAIL_CALL: (n_args, params_tuple)
|
||||
# OP_SELF_TAIL_CALL: (n_args, params_tuple, pops_needed)
|
||||
if len(val) == 3 and isinstance(val[0], int) and isinstance(val[1], tuple) and isinstance(val[2], int):
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]], 'pops': val[2]}
|
||||
# Backwards-compat: pre-2026-06-14 portals have 2-tuple form.
|
||||
if len(val) == 2 and isinstance(val[0], int) and isinstance(val[1], tuple):
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]]}
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]], 'pops': 0}
|
||||
return {'t': 'repr', 'v': repr(val)}
|
||||
|
||||
def _deserialize_operand(data):
|
||||
|
|
@ -1921,7 +1952,8 @@ def _deserialize_operand(data):
|
|||
rest = S(data['rest']) if data['rest'] else None
|
||||
return (code, params, rest)
|
||||
if t == 'stc':
|
||||
return (data['n'], tuple(S(p) for p in data['p']))
|
||||
# 2026-06-14: stc now carries pops field; older portals (no pops) → 0.
|
||||
return (data['n'], tuple(S(p) for p in data['p']), data.get('pops', 0))
|
||||
return data
|
||||
|
||||
def _serialize_code(code):
|
||||
|
|
@ -2632,7 +2664,8 @@ def _jit_transpile(instrs, params, name, has_self_tc):
|
|||
val = stack.pop() if stack else 'VOID'
|
||||
stmts.append(('return', val)); ip+=1
|
||||
elif op == OP_SELF_TAIL_CALL:
|
||||
tc_n, tc_params = arg
|
||||
# arg is (n_args, params, pops_needed) since 2026-06-14
|
||||
tc_n = arg[0]; tc_params = arg[1]
|
||||
pnames = [str(p) for p in tc_params]
|
||||
args = [];
|
||||
for _ in range(tc_n): args.insert(0, stack.pop())
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1203,13 +1203,28 @@ _BC_FOLDABLE = {
|
|||
|
||||
class CodeObj:
|
||||
"""Compiled bytecode chunk."""
|
||||
__slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line', '_self_name', '_self_params')
|
||||
__slots__ = ('instrs', 'name', 'source_map', 'ic', '_cur_line',
|
||||
'_self_name', '_self_params', '_scope_depth', '_self_base')
|
||||
def __init__(self, name=None):
|
||||
self.instrs = []; self.name = name
|
||||
self.source_map = [] # parallel to instrs: line number or None
|
||||
self.ic = None # inline cache (populated at runtime)
|
||||
self._cur_line = None # current source line during compilation
|
||||
# 2026-06-14 self-tail-call frame-unwind: tracks env-frame depth at
|
||||
# compile time so OP_SELF_TAIL_CALL can pop accumulated let/let*/
|
||||
# letrec/do frames before reusing our lambda body env. Without this
|
||||
# a (let* (...) (loop ...)) inside (let loop ...) bloated env per
|
||||
# iter — 156k-element walk hung > 5 min instead of completing in
|
||||
# 1.4s. _self_base records depth at lambda body entry; _scope_depth
|
||||
# is current depth; pops_needed = depth - base at tail call site.
|
||||
self._scope_depth = 0
|
||||
self._self_base = 0
|
||||
def emit(self, op, arg=None):
|
||||
# 2026-06-14 self-tail-call frame-unwind: track env-frame depth so
|
||||
# OP_SELF_TAIL_CALL knows how many let/let*/letrec/do frames sit
|
||||
# between us & our lambda body env.
|
||||
if op == OP_PUSH_ENV: self._scope_depth += 1
|
||||
elif op == OP_POP_ENV: self._scope_depth -= 1
|
||||
idx = len(self.instrs); self.instrs.append((op, arg))
|
||||
self.source_map.append(self._cur_line)
|
||||
return idx
|
||||
|
|
@ -1529,7 +1544,12 @@ def _bc(expr, code, env, tail=False):
|
|||
if tail and isinstance(head, Symbol) and hasattr(code, '_self_name') and str(head) == code._self_name:
|
||||
params = code._self_params
|
||||
for arg in call_args: _bc(arg, code, env)
|
||||
code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params))); return
|
||||
# 2026-06-14 frame-unwind: pop accumulated let/let*/letrec/do frames
|
||||
# before we reuse our lambda body env. Without this each iter's
|
||||
# let* frame stays on the env chain — env grows linearly with iters
|
||||
# & every var lookup walks an O(n) chain → effective O(n^2).
|
||||
pops_needed = code._scope_depth - code._self_base
|
||||
code.emit(OP_SELF_TAIL_CALL, (len(call_args), tuple(params), pops_needed)); return
|
||||
|
||||
# --- Function call ---
|
||||
_bc(head, code, env)
|
||||
|
|
@ -1569,6 +1589,9 @@ def _bc_lambda(body, params, rest, env, name=None, self_name=None, self_params=N
|
|||
if def_names:
|
||||
inner.emit(OP_PUSH_ENV)
|
||||
for nm in def_names: inner.emit(OP_VOID); inner.emit(OP_BIND, nm)
|
||||
# 2026-06-14: record baseline depth after any internal-defines frame.
|
||||
# Our self-tail-call unwind pops back to here, not all the way to 0.
|
||||
inner._self_base = inner._scope_depth
|
||||
_bc_body(body_list, inner, env, tail=True)
|
||||
inner.emit(OP_RETURN)
|
||||
_peephole(inner)
|
||||
|
|
@ -1832,9 +1855,14 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id):
|
|||
c, addr = arg
|
||||
if _po() != c: ip = addr
|
||||
elif op == OP_SELF_TAIL_CALL:
|
||||
n, params = arg
|
||||
# 2026-06-14: arg now (n_args, params, pops_needed). pops_needed
|
||||
# unwinds accumulated let/let*/letrec/do frames before we reuse
|
||||
# our lambda body env — otherwise self-tail-call from inside a
|
||||
# let* bloats env per iter & every var lookup walks O(n) chain.
|
||||
n, params, pops = arg
|
||||
if n: args_ = stack[-n:]; del stack[-n:]
|
||||
else: args_ = []
|
||||
for _ in range(pops): env = env.p
|
||||
b = env.b
|
||||
for p, a in zip(params, args_): b[p] = a
|
||||
ip = 0; stack.clear(); continue
|
||||
|
|
@ -1888,9 +1916,12 @@ def _serialize_operand(val):
|
|||
return {'t': 'closure', 'code': _serialize_code(code),
|
||||
'params': [str(p) for p in params],
|
||||
'rest': str(rest) if rest else None}
|
||||
# OP_SELF_TAIL_CALL: (n_args, params_tuple)
|
||||
# OP_SELF_TAIL_CALL: (n_args, params_tuple, pops_needed)
|
||||
if len(val) == 3 and isinstance(val[0], int) and isinstance(val[1], tuple) and isinstance(val[2], int):
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]], 'pops': val[2]}
|
||||
# Backwards-compat: pre-2026-06-14 portals have 2-tuple form.
|
||||
if len(val) == 2 and isinstance(val[0], int) and isinstance(val[1], tuple):
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]]}
|
||||
return {'t': 'stc', 'n': val[0], 'p': [str(p) for p in val[1]], 'pops': 0}
|
||||
return {'t': 'repr', 'v': repr(val)}
|
||||
|
||||
def _deserialize_operand(data):
|
||||
|
|
@ -1921,7 +1952,8 @@ def _deserialize_operand(data):
|
|||
rest = S(data['rest']) if data['rest'] else None
|
||||
return (code, params, rest)
|
||||
if t == 'stc':
|
||||
return (data['n'], tuple(S(p) for p in data['p']))
|
||||
# 2026-06-14: stc now carries pops field; older portals (no pops) → 0.
|
||||
return (data['n'], tuple(S(p) for p in data['p']), data.get('pops', 0))
|
||||
return data
|
||||
|
||||
def _serialize_code(code):
|
||||
|
|
@ -2632,7 +2664,8 @@ def _jit_transpile(instrs, params, name, has_self_tc):
|
|||
val = stack.pop() if stack else 'VOID'
|
||||
stmts.append(('return', val)); ip+=1
|
||||
elif op == OP_SELF_TAIL_CALL:
|
||||
tc_n, tc_params = arg
|
||||
# arg is (n_args, params, pops_needed) since 2026-06-14
|
||||
tc_n = arg[0]; tc_params = arg[1]
|
||||
pnames = [str(p) for p in tc_params]
|
||||
args = [];
|
||||
for _ in range(tc_n): args.insert(0, stack.pop())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue