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.
This commit is contained in:
russell@unturf.com 2026-04-13 14:39:15 -04:00
parent 52d14a2f59
commit a7ca8e9f89
3 changed files with 728 additions and 3 deletions

118
bench.py
View file

@ -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)')

204
tests.py
View file

@ -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
###############################################################################

View file

@ -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'#<compiled {self.name or "λ"}>'
# 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)