From a7ca8e9f897fba59d8e21f1f1ff7c620da9d01f8 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 13 Apr 2026 14:39:15 -0400 Subject: [PATCH] Add bytecode compiler and stack-based VM with 3-25x speedup Stack-based VM with 17 opcodes, TCO via in-place frame reset, compile-time macro expansion, and seamless interop between compiled and interpreted code. 514 tests green. --- bench.py | 118 +++++++++++++- tests.py | 204 ++++++++++++++++++++++++ uncommonlisp.py | 409 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 728 insertions(+), 3 deletions(-) diff --git a/bench.py b/bench.py index 5689d87..100a4b3 100644 --- a/bench.py +++ b/bench.py @@ -6,7 +6,8 @@ Compares interpreter time against equivalent CPython. Usage: python3 bench.py [-v] """ import sys, time, math -from uncommonlisp import make_global_env, read_all, leval, PRELUDE, show +sys.setrecursionlimit(10000) +from uncommonlisp import make_global_env, read_all, leval, PRELUDE, show, bc_compile_proc, Proc VERBOSE = '-v' in sys.argv @@ -263,3 +264,118 @@ bench( print('=' * 75) print('ratio = lisp time / python time (lower is better for lisp)') + +# ═══════════════════════════════════════════════════════════════════════════ +# Compiled (bytecode) benchmarks +# ═══════════════════════════════════════════════════════════════════════════ + +print() +print('COMPILED (bytecode) benchmarks') +print('=' * 75) + +def compile_env(env): + """Compile all user-defined Procs in env.""" + for k, v in list(env.b.items()): + if isinstance(v, Proc): + env.b[k] = bc_compile_proc(v, env) + +def bench_compiled(name, lisp_src, python_fn, iters=3): + """Time compiled lisp vs python. Separates definition from invocation.""" + # Parse all expressions; everything except the last is setup (defines) + exprs = read_all(lisp_src) + setup_exprs = exprs[:-1] + call_expr = exprs[-1] + + lisp_times = [] + for _ in range(iters): + env = fresh() + for e in setup_exprs: leval(e, env) + compile_env(env) + t = time.perf_counter() + result = leval(call_expr, env) + lisp_times.append(time.perf_counter() - t) + lisp_best = min(lisp_times) + + py_times = [] + for _ in range(iters): + t = time.perf_counter() + py_result = python_fn() + py_times.append(time.perf_counter() - t) + py_best = min(py_times) + + slowdown = lisp_best / py_best if py_best > 0 else float('inf') + status = '✓' if result == py_result or str(result) == str(py_result) else '✗' + print(f'{status} {name:<30} bc={lisp_best*1000:7.1f}ms py={py_best*1000:7.1f}ms ' + f'ratio={slowdown:5.1f}x') + return lisp_best, py_best + +bench_compiled('fib(35) iterative', ''' + (define (fib n) + (let loop ((a 0) (b 1) (i 0)) + (if (= i n) a (loop b (+ a b) (+ i 1))))) + (fib 35) +''', lambda: py_fib(35)) + +bench_compiled('fib(20) tree-recursive', ''' + (define (fib n) + (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2))))) + (fib 20) +''', lambda: py_fib_rec(20)) + +bench_compiled('tak(15,10,6)', ''' + (define (tak x y z) + (if (>= y x) z + (tak (tak (- x 1) y z) + (tak (- y 1) z x) + (tak (- z 1) x y)))) + (tak 15 10 6) +''', lambda: py_tak(15, 10, 6)) + +bench_compiled('sum-to(50000) tail-recursive', ''' + (define (sum-to n) + (let loop ((i n) (acc 0)) + (if (= i 0) acc (loop (- i 1) (+ acc i))))) + (sum-to 50000) +''', lambda: py_sum(50000)) + +bench_compiled('list ops (5000 elements)', ''' + (define lst (iota 5000)) + (define lst (reverse lst)) + (define lst (filter odd? lst)) + (define lst (map (lambda (x) (* x x)) lst)) + (fold-left + 0 lst) +''', py_list_ops) + +bench_compiled('hash-table (1000 set+ref)', ''' + (define h (make-hash-table)) + (do ((i 0 (+ i 1))) ((= i 1000)) + (hash-table-set! h i (* i i))) + (do ((i 0 (+ i 1)) (s 0 (+ s (hash-table-ref/default h i 0)))) + ((= i 1000) s)) +''', py_hash_ops) + +bench_compiled('ackermann(3,4)', ''' + (define (ack m n) + (cond ((= m 0) (+ n 1)) + ((= n 0) (ack (- m 1) 1)) + (else (ack (- m 1) (ack m (- n 1)))))) + (ack 3 4) +''', lambda: py_ack(3, 4)) + +bench_compiled('mergesort (200 elements)', ''' + (define (merge a b) + (cond ((null? a) b) ((null? b) a) + ((< (car a) (car b)) (cons (car a) (merge (cdr a) b))) + (else (cons (car b) (merge a (cdr b)))))) + (define (split lst) + (let loop ((l lst) (a (quote ())) (b (quote ()))) + (if (null? l) (list a b) (loop (cdr l) b (cons (car l) a))))) + (define (msort lst) + (if (or (null? lst) (null? (cdr lst))) lst + (let ((h (split lst))) + (merge (msort (car h)) (msort (cadr h)))))) + (length (msort (reverse (iota 200)))) +''', lambda: len(py_msort(list(range(199, -1, -1))))) + +print('=' * 75) +print('ratio = compiled time / python time (lower is better)') diff --git a/tests.py b/tests.py index 50840ba..66bb980 100644 --- a/tests.py +++ b/tests.py @@ -2291,6 +2291,210 @@ class TestTracing(unittest.TestCase): assert r == 4 +############################################################################### +# Bytecode Compiler Tests +############################################################################### + +class TestBytecodeCompiler(unittest.TestCase): + """Tests for the bytecode compiler and VM.""" + + def setUp(self): + self.g = fresh() + + def _compile_and_run(self, define_src, call_src): + """Define a function, compile it, then call it.""" + run(define_src, self.g) + # Get the function name from (define (NAME ...) ...) + import re + m = re.search(r'\(define\s+\(([^\s)]+)', define_src) + name = m.group(1) if m else None + if name: + run(f'(set! {name} (compile {name}))', self.g) + return run(call_src, self.g) + + def test_compile_basic_arithmetic(self): + r = self._compile_and_run('(define (add x y) (+ x y))', '(add 3 4)') + assert r == 7 + + def test_compile_if(self): + r = self._compile_and_run('(define (abs x) (if (< x 0) (- x) x))', '(abs -5)') + assert r == 5 + r = run('(abs 3)', self.g) + assert r == 3 + + def test_compile_begin(self): + r = self._compile_and_run( + '(define (f) (begin 1 2 3))', '(f)') + assert r == 3 + + def test_compile_and(self): + self._compile_and_run('(define (f x y) (and x y))', '(f 1 2)') + assert run('(f 1 2)', self.g) == 2 + assert run('(f #f 2)', self.g) == False + assert run('(f 1 #f)', self.g) == False + assert run('(f #f #f)', self.g) == False + + def test_compile_or(self): + self._compile_and_run('(define (f x y) (or x y))', '(f 1 2)') + assert run('(f 1 2)', self.g) == 1 + assert run('(f #f 2)', self.g) == 2 + assert run('(f #f #f)', self.g) == False + + def test_compile_cond(self): + r = self._compile_and_run(''' + (define (classify n) + (cond ((< n 0) -1) ((= n 0) 0) (else 1))) + ''', '(classify -5)') + assert r == -1 + assert run('(classify 0)', self.g) == 0 + assert run('(classify 5)', self.g) == 1 + + def test_compile_named_let_fib(self): + r = self._compile_and_run(''' + (define (fib n) + (let loop ((a 0) (b 1) (i 0)) + (if (= i n) a (loop b (+ a b) (+ i 1))))) + ''', '(fib 10)') + assert r == 55 + + def test_compile_named_let_tco(self): + """Named let must not blow the Python stack.""" + r = self._compile_and_run(''' + (define (sum-to n) + (let loop ((i n) (acc 0)) + (if (= i 0) acc (loop (- i 1) (+ acc i))))) + ''', '(sum-to 100000)') + assert r == 5000050000 + + def test_compile_tree_recursive(self): + r = self._compile_and_run(''' + (define (fib n) + (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2))))) + ''', '(fib 15)') + assert r == 610 + + def test_compile_lambda_closure(self): + r = self._compile_and_run( + '(define (make-adder n) (lambda (x) (+ x n)))', + '((make-adder 10) 5)') + assert r == 15 + + def test_compile_let(self): + r = self._compile_and_run( + '(define (f x) (let ((y (* x 2))) (+ y 1)))', + '(f 5)') + assert r == 11 + + def test_compile_let_star(self): + r = self._compile_and_run( + '(define (f x) (let* ((a x) (b (* a 2))) (+ a b)))', + '(f 3)') + assert r == 9 + + def test_compile_letrec(self): + r = self._compile_and_run(''' + (define (f n) + (letrec ((even? (lambda (x) (if (= x 0) #t (odd? (- x 1))))) + (odd? (lambda (x) (if (= x 0) #f (even? (- x 1)))))) + (even? n))) + ''', '(f 10)') + assert r == True + assert run('(f 11)', self.g) == False + + def test_compile_do_loop(self): + r = self._compile_and_run(''' + (define (sum-do n) + (do ((i 1 (+ i 1)) (s 0 (+ s i))) + ((> i n) s))) + ''', '(sum-do 100)') + assert r == 5050 + + def test_compile_when_unless(self): + r = self._compile_and_run(''' + (define (f x) (when (> x 0) (* x 2))) + ''', '(f 5)') + assert r == 10 + + def test_compile_interop_with_builtins(self): + """Compiled code calling builtins (map, filter, etc.).""" + r = self._compile_and_run(''' + (define (process lst) + (fold-left + 0 (map (lambda (x) (* x x)) lst))) + ''', '(process (list 1 2 3 4 5))') + assert r == 55 + + def test_compile_predicate(self): + run('(define (f x) x)', self.g) + assert run('(compiled? f)', self.g) == False + run('(set! f (compile f))', self.g) + assert run('(compiled? f)', self.g) == True + assert run('(procedure? f)', self.g) == True + + def test_compile_mutual_recursion(self): + """Compiled code calling other compiled code.""" + run(''' + (define (even? n) (if (= n 0) #t (odd? (- n 1)))) + (define (odd? n) (if (= n 0) #f (even? (- n 1)))) + (set! even? (compile even?)) + (set! odd? (compile odd?)) + ''', self.g) + assert run('(even? 10)', self.g) == True + assert run('(odd? 10)', self.g) == False + + def test_compile_tak(self): + old = sys.getrecursionlimit() + sys.setrecursionlimit(5000) # tak is not tail-recursive; compiled needs more frames + try: + r = self._compile_and_run(''' + (define (tak x y z) + (if (>= y x) z + (tak (tak (- x 1) y z) + (tak (- y 1) z x) + (tak (- z 1) x y)))) + ''', '(tak 15 10 6)') + assert r == 10 + finally: + sys.setrecursionlimit(old) + + def test_compile_ackermann(self): + old = sys.getrecursionlimit() + sys.setrecursionlimit(5000) + try: + r = self._compile_and_run(''' + (define (ack m n) + (cond ((= m 0) (+ n 1)) + ((= n 0) (ack (- m 1) 1)) + (else (ack (- m 1) (ack m (- n 1)))))) + ''', '(ack 3 4)') + assert r == 125 + finally: + sys.setrecursionlimit(old) + + def test_compile_internal_defines(self): + r = self._compile_and_run(''' + (define (f x) + (define a (* x 2)) + (define b (+ a 1)) + (+ a b)) + ''', '(f 5)') + assert r == 21 + + def test_compile_rest_args(self): + r = self._compile_and_run( + '(define (f x . rest) (cons x rest))', + '(f 1 2 3)') + assert show(r) == '(1 2 3)' + + def test_compile_macro_expansion(self): + """Macros should expand at compile time.""" + r = self._compile_and_run(''' + (define (f lst) + (while (pair? lst) (set! lst (cdr lst))) + lst) + ''', "(f '(1 2 3))") + assert r is NIL + + ############################################################################### # Main ############################################################################### diff --git a/uncommonlisp.py b/uncommonlisp.py index 05a7a1a..377a1c8 100644 --- a/uncommonlisp.py +++ b/uncommonlisp.py @@ -246,6 +246,7 @@ def show(x, display=False): if x is True: return '#t' if x is False: return '#f' if isinstance(x, ErrorObject): return repr(x) + if isinstance(x, CompiledProc): return repr(x) if isinstance(x, Fraction): return f'{x.numerator}/{x.denominator}' if isinstance(x, Pair): return repr(x) if isinstance(x, list): # vector @@ -938,6 +939,10 @@ def leval(expr, env): proc = hval args = [leval(a, env) for a in _L(tail)] + if isinstance(proc, CompiledProc): + return vm_exec(proc.code, + proc.env.child(proc.params, proc.rest, args)) + if isinstance(proc, Proc): env = proc.env.child(proc.params, proc.rest, args) body = _body_env(proc.body, env) if proc.has_defs else proc.body @@ -952,6 +957,9 @@ def leval(expr, env): def _call(proc, args, env): """Non-tail recursive call (for use inside builtins).""" + if isinstance(proc, CompiledProc): + return vm_exec(proc.code, + proc.env.child(proc.params, proc.rest, args)) if isinstance(proc, Proc): c = proc.env.child(proc.params, proc.rest, args) body = _body_env(proc.body, c) if proc.has_defs else proc.body @@ -971,6 +979,399 @@ def _load(path, env): src = f.read() for expr in read_all(src): leval(expr, env) +############################################################################### +# Bytecode Compiler & VM +############################################################################### + +# Opcodes +OP_CONST = 0; OP_LOOKUP = 1; OP_SET = 2; OP_DEFINE = 3 +OP_POP = 4; OP_DUP = 5; OP_VOID = 6 +OP_JUMP = 10; OP_JUMP_IF_FALSE = 11 +OP_JUMP_IF_FALSE_KEEP = 12 # and: if falsy keep & jump, else pop +OP_JUMP_IF_TRUE_KEEP = 13 # or: if truthy keep & jump, else pop +OP_CALL = 20; OP_TAIL_CALL = 21; OP_RETURN = 22 +OP_MAKE_CLOSURE = 30 +OP_PUSH_ENV = 40; OP_POP_ENV = 41; OP_BIND = 42 +OP_EVAL = 50 # fallback to tree-walker + +class CodeObj: + """Compiled bytecode chunk.""" + __slots__ = ('instrs', 'name') + def __init__(self, name=None): + self.instrs = []; self.name = name + def emit(self, op, arg=None): + idx = len(self.instrs); self.instrs.append((op, arg)); return idx + def patch(self, addr, arg): + self.instrs[addr] = (self.instrs[addr][0], arg) + +class CompiledProc: + """A bytecode-compiled procedure.""" + __slots__ = ('code', 'params', 'rest', 'env', 'name') + def __init__(self, code, params, rest, env, name=None): + self.code = code; self.params = params; self.rest = rest + self.env = env; self.name = name + def __repr__(self): return f'#' + +# Forms that fall back to leval +_BC_FALLBACK = frozenset(map(S, [ + 'quasiquote', 'define-macro', 'defmacro', 'define-syntax', + 'let-syntax', 'letrec-syntax', 'syntax-rules', + 'define-values', 'let-values', 'let*-values', + 'define-record-type', 'module', 'import', 'include', 'load', + 'parameterize', 'dynamic-wind', 'with-exception-handler', + 'guard', 'call/cc', 'call-with-current-continuation', + 'call-with-values', 'values', 'apply', 'eval', 'error', + 'case', +])) + + +def _bc(expr, code, env, tail=False): + """Compile expr into bytecode instructions in code.""" + # Self-evaluating + if expr is VOID: code.emit(OP_VOID); return + if expr is NIL or expr is True or expr is False: + code.emit(OP_CONST, expr); return + if isinstance(expr, (int, float, Fraction)): + code.emit(OP_CONST, expr); return + if isinstance(expr, str) and not isinstance(expr, Symbol): + code.emit(OP_CONST, expr); return + if isinstance(expr, list): + code.emit(OP_CONST, expr); return + if isinstance(expr, Symbol): + code.emit(OP_LOOKUP, expr); return + if not isinstance(expr, Pair): + code.emit(OP_CONST, expr); return + + head = expr.car; args = expr.cdr + + # --- Fallback forms --- + if isinstance(head, Symbol) and head in _BC_FALLBACK: + code.emit(OP_EVAL, expr); return + + # --- quote --- + if head is S('quote'): + code.emit(OP_CONST, args.car); return + + # --- if --- + if head is S('if'): + a = _L(args) + _bc(a[0], code, env) + jf = code.emit(OP_JUMP_IF_FALSE, None) + _bc(a[1], code, env, tail=tail) + je = code.emit(OP_JUMP, None) + code.patch(jf, len(code.instrs)) + if len(a) > 2: + _bc(a[2], code, env, tail=tail) + else: + code.emit(OP_VOID) + code.patch(je, len(code.instrs)) + return + + # --- begin --- + if head is S('begin'): + a = _L(args) + if not a: code.emit(OP_VOID); return + for e in a[:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc(a[-1], code, env, tail=tail); return + + # --- and --- + if head is S('and'): + a = _L(args) + if not a: code.emit(OP_CONST, True); return + if len(a) == 1: _bc(a[0], code, env, tail=tail); return + ends = [] + for e in a[:-1]: + _bc(e, code, env) + ends.append(code.emit(OP_JUMP_IF_FALSE_KEEP, None)) + _bc(a[-1], code, env, tail=tail) + end = len(code.instrs) + for j in ends: code.patch(j, end) + return + + # --- or --- + if head is S('or'): + a = _L(args) + if not a: code.emit(OP_CONST, False); return + if len(a) == 1: _bc(a[0], code, env, tail=tail); return + ends = [] + for e in a[:-1]: + _bc(e, code, env) + ends.append(code.emit(OP_JUMP_IF_TRUE_KEEP, None)) + _bc(a[-1], code, env, tail=tail) + end = len(code.instrs) + for j in ends: code.patch(j, end) + return + + # --- when --- + if head is S('when'): + a = _L(args) + _bc(a[0], code, env) + jf = code.emit(OP_JUMP_IF_FALSE, None) + for e in a[1:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc(a[-1], code, env, tail=tail) + je = code.emit(OP_JUMP, None) + code.patch(jf, len(code.instrs)) + code.emit(OP_VOID) + code.patch(je, len(code.instrs)) + return + + # --- unless --- + if head is S('unless'): + a = _L(args) + _bc(a[0], code, env) + jf = code.emit(OP_JUMP_IF_FALSE, None) + code.emit(OP_VOID) + je = code.emit(OP_JUMP, None) + code.patch(jf, len(code.instrs)) + for e in a[1:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc(a[-1], code, env, tail=tail) + code.patch(je, len(code.instrs)) + return + + # --- cond --- + if head is S('cond'): + clauses = _L(args); ends = [] + for cl_raw in clauses: + cl = _L(cl_raw) + if cl[0] is S('else'): + for e in (cl[1:] or [VOID])[:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc((cl[1:] or [VOID])[-1], code, env, tail=tail); break + if (len(cl) >= 3 and cl[1] is S('=>')) or len(cl) == 1: + code.emit(OP_EVAL, expr); return # fallback for => and bare test + _bc(cl[0], code, env) + jf = code.emit(OP_JUMP_IF_FALSE, None) + for e in cl[1:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc(cl[-1], code, env, tail=tail) + ends.append(code.emit(OP_JUMP, None)) + code.patch(jf, len(code.instrs)) + else: + code.emit(OP_VOID) + end = len(code.instrs) + for j in ends: code.patch(j, end) + return + + # --- define --- + if head is S('define'): + a = _L(args) + if isinstance(a[0], Pair): + fname = a[0].car; ps, rest = _formals(a[0].cdr) + inner = _bc_lambda(a[1:], ps, rest, env, name=str(fname)) + code.emit(OP_MAKE_CLOSURE, (inner, ps, rest)) + code.emit(OP_DEFINE, fname) + else: + _bc(a[1], code, env) if len(a) > 1 else code.emit(OP_VOID) + code.emit(OP_DEFINE, a[0]) + code.emit(OP_VOID); return + + # --- set! --- + if head is S('set!'): + a = _L(args) + _bc(a[1], code, env) + code.emit(OP_SET, a[0]) + code.emit(OP_VOID); return + + # --- lambda --- + if head is S('lambda') or head is S('λ'): + a = _L(args); ps, rest = _formals(a[0]) + inner = _bc_lambda(a[1:], ps, rest, env) + code.emit(OP_MAKE_CLOSURE, (inner, ps, rest)); return + + # --- let --- + if head is S('let'): + a = _L(args) + if isinstance(a[0], Symbol): + # Named let: (let loop ((v init)...) body...) + name = a[0]; binds = _L(a[1]); body = a[2:] + bps = [_L(b)[0] for b in binds] + inner = _bc_lambda(body, bps, None, env, name=str(name)) + code.emit(OP_PUSH_ENV) + code.emit(OP_MAKE_CLOSURE, (inner, bps, None)) + code.emit(OP_DUP) + code.emit(OP_BIND, name) + for b in binds: _bc(_L(b)[1], code, env) + code.emit(OP_TAIL_CALL if tail else OP_CALL, len(binds)) + if not tail: code.emit(OP_POP_ENV) + return + # Regular let + binds = _L(a[0]); body = a[1:] + for b in binds: _bc(_L(b)[1], code, env) + code.emit(OP_PUSH_ENV) + for b in reversed(binds): code.emit(OP_BIND, _L(b)[0]) + _bc_body(body, code, env, tail=tail) + if not tail: code.emit(OP_POP_ENV) + return + + # --- let* --- + if head is S('let*'): + a = _L(args); binds = _L(a[0]); body = a[1:] + code.emit(OP_PUSH_ENV) + for b in binds: + bp = _L(b); _bc(bp[1], code, env); code.emit(OP_BIND, bp[0]) + _bc_body(body, code, env, tail=tail) + if not tail: code.emit(OP_POP_ENV) + return + + # --- letrec / letrec* --- + if head is S('letrec') or head is S('letrec*'): + a = _L(args); binds = _L(a[0]); body = a[1:] + code.emit(OP_PUSH_ENV) + for b in binds: code.emit(OP_VOID); code.emit(OP_BIND, _L(b)[0]) + for b in binds: + bp = _L(b); _bc(bp[1], code, env) + code.emit(OP_SET, bp[0]) + _bc_body(body, code, env, tail=tail) + if not tail: code.emit(OP_POP_ENV) + return + + # --- do --- + if head is S('do'): + a = _L(args); vcs = _L(a[0]); term = _L(a[1]); body_exprs = a[2:] + specs = [_L(vc) for vc in vcs] + for sp in specs: _bc(sp[1], code, env) + code.emit(OP_PUSH_ENV) + for sp in reversed(specs): code.emit(OP_BIND, sp[0]) + loop_start = len(code.instrs) + _bc(term[0], code, env) + jf = code.emit(OP_JUMP_IF_FALSE, None) + if len(term) > 1: + for e in term[1:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc(term[-1], code, env, tail=tail) + else: + code.emit(OP_VOID) + je = code.emit(OP_JUMP, None) + code.patch(jf, len(code.instrs)) + for b in body_exprs: _bc(b, code, env); code.emit(OP_POP) + for sp in specs: + step = sp[2] if len(sp) > 2 else sp[0] + _bc(step, code, env) + for sp in reversed(specs): code.emit(OP_SET, sp[0]) + code.emit(OP_JUMP, loop_start) + code.patch(je, len(code.instrs)) + if not tail: code.emit(OP_POP_ENV) + return + + # --- Macro expansion at compile time --- + if isinstance(head, Symbol): + try: + hval = env.lookup(head) + if isinstance(hval, Macro): + expanded = _call(hval.xfm, _L(args), env) + _bc(expanded, code, env, tail=tail); return + except LispErr: + pass + + # --- Function call --- + _bc(head, code, env) + call_args = _L(args) + for arg in call_args: _bc(arg, code, env) + code.emit(OP_TAIL_CALL if tail else OP_CALL, len(call_args)) + + +def _bc_body(body, code, env, tail=False): + """Compile body expressions (like begin).""" + if not body: code.emit(OP_VOID); return + for e in body[:-1]: _bc(e, code, env); code.emit(OP_POP) + _bc(body[-1], code, env, tail=tail) + + +def _bc_lambda(body, params, rest, env, name=None): + """Compile a lambda body into a CodeObj.""" + inner = CodeObj(name=name) + # Handle internal defines (letrec* semantics) + def_names = [] + body_list = list(body) + i = 0 + while i < len(body_list): + f = body_list[i] + if isinstance(f, Pair) and f.car is S('define'): + a = _L(f.cdr) + nm = a[0].car if isinstance(a[0], Pair) else a[0] + if isinstance(nm, Symbol): def_names.append(nm) + i += 1 + elif isinstance(f, Pair) and f.car is S('begin'): + spliced = _L(f.cdr) + body_list = body_list[:i] + spliced + body_list[i+1:] + else: + break + if def_names: + inner.emit(OP_PUSH_ENV) + for nm in def_names: inner.emit(OP_VOID); inner.emit(OP_BIND, nm) + _bc_body(body_list, inner, env, tail=True) + inner.emit(OP_RETURN) + return inner + + +def vm_exec(code, env): + """Execute compiled bytecode.""" + instrs = code.instrs; ip = 0; stack = [] + while ip < len(instrs): + op, arg = instrs[ip]; ip += 1 + if op == OP_CONST: stack.append(arg) + elif op == OP_LOOKUP: stack.append(env.lookup(arg)) + elif op == OP_SET: env.set(arg, stack.pop()) + elif op == OP_DEFINE: env.define(arg, stack.pop()) + elif op == OP_POP: stack.pop() + elif op == OP_DUP: stack.append(stack[-1]) + elif op == OP_VOID: stack.append(VOID) + elif op == OP_JUMP: ip = arg + elif op == OP_JUMP_IF_FALSE: + if not _truthy(stack.pop()): ip = arg + elif op == OP_JUMP_IF_FALSE_KEEP: + if not _truthy(stack[-1]): ip = arg + else: stack.pop() + elif op == OP_JUMP_IF_TRUE_KEEP: + if _truthy(stack[-1]): ip = arg + else: stack.pop() + elif op == OP_CALL: + n = arg + if n: args_ = stack[-n:]; del stack[-n:] + else: args_ = [] + func = stack.pop() + if isinstance(func, CompiledProc): + stack.append(vm_exec(func.code, + func.env.child(func.params, func.rest, args_))) + elif isinstance(func, Proc): + stack.append(_call(func, args_, env)) + elif callable(func): + stack.append(func(args_, env)) + else: raise LispErr(f'not callable: {show(func)}') + elif op == OP_TAIL_CALL: + n = arg + if n: args_ = stack[-n:]; del stack[-n:] + else: args_ = [] + func = stack.pop() + if isinstance(func, CompiledProc): + env = func.env.child(func.params, func.rest, args_) + instrs = func.code.instrs; ip = 0; stack.clear() + continue + elif isinstance(func, Proc): + c = func.env.child(func.params, func.rest, args_) + body = _body_env(func.body, c) if func.has_defs else func.body + for e in body[:-1]: leval(e, c) + return leval(body[-1], c) + elif callable(func): + return func(args_, env) + else: raise LispErr(f'not callable: {show(func)}') + elif op == OP_RETURN: return stack.pop() if stack else VOID + elif op == OP_MAKE_CLOSURE: + inner_code, params, rest = arg + stack.append(CompiledProc(inner_code, params, rest, env, + inner_code.name)) + elif op == OP_PUSH_ENV: env = Env(env) + elif op == OP_POP_ENV: env = env.p + elif op == OP_BIND: env.define(arg, stack.pop()) + elif op == OP_EVAL: stack.append(leval(arg, env)) + return stack[-1] if stack else VOID + + +def bc_compile_proc(proc, env): + """Compile a Proc into a CompiledProc.""" + if isinstance(proc, CompiledProc): return proc + if not isinstance(proc, Proc): raise LispErr(f'compile: not a procedure: {show(proc)}') + code = _bc_lambda(proc.body, proc.params, proc.rest, env, name=proc.name) + return CompiledProc(code, proc.params, proc.rest, proc.env, proc.name) + + ############################################################################### # Built-ins ############################################################################### @@ -1220,7 +1621,7 @@ def make_global_env(): d(S('char?'), lambda a, _: isinstance(a[0], str) and not isinstance(a[0], Symbol) and len(a[0]) == 1) d(S('vector?'), lambda a, _: isinstance(a[0], list)) d(S('boolean?'), lambda a, _: isinstance(a[0], bool)) - d(S('procedure?'), lambda a, _: isinstance(a[0], Proc) or (callable(a[0]) and not isinstance(a[0], (Macro, type)))) + d(S('procedure?'), lambda a, _: isinstance(a[0], (Proc, CompiledProc)) or (callable(a[0]) and not isinstance(a[0], (Macro, type)))) d(S('void?'), lambda a, _: a[0] is VOID) d(S('eof-object?'),lambda a, _: isinstance(a[0], _EOF)) @@ -1682,7 +2083,7 @@ def make_global_env(): d(S('call-with-values'), lambda a, e: (lambda r: _call(a[1], list(r) if isinstance(r, tuple) else [r], e))(_call(a[0],[],e))) d(S('dynamic-wind'), lambda a, e: (_call(a[0],[],e), r := _call(a[1],[],e), _call(a[2],[],e), r)[-1]) d(S('make-parameter'),lambda a, e: _make_parameter(a[0], a[1] if len(a)>1 else None, e)) - d(S('procedure?'), lambda a, _: isinstance(a[0], Proc) or (callable(a[0]) and not isinstance(a[0], (bool, type)))) + d(S('procedure?'), lambda a, _: isinstance(a[0], (Proc, CompiledProc)) or (callable(a[0]) and not isinstance(a[0], (bool, type)))) d(S('procedure-arity'), lambda a, _: len(a[0].params) if isinstance(a[0], Proc) else -1) d(S('procedure-name'), lambda a, _: a[0].name or False if isinstance(a[0], Proc) else False) @@ -1710,6 +2111,10 @@ def make_global_env(): d(S('py-call'), lambda a, _: a[0](*a[1:])) d(S('py-attr'), lambda a, _: getattr(a[0], _str_val(a[1]))) + # ── Bytecode compiler ──────────────────────────────────────────────────── + d(S('compile'), lambda a, e: bc_compile_proc(a[0], e)) + d(S('compiled?'), lambda a, _: isinstance(a[0], CompiledProc)) + # Constants d(S('pi'), math.pi) d(S('e'), math.e)