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:
parent
52d14a2f59
commit
a7ca8e9f89
3 changed files with 728 additions and 3 deletions
118
bench.py
118
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)')
|
||||
|
|
|
|||
204
tests.py
204
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
|
||||
###############################################################################
|
||||
|
|
|
|||
409
uncommonlisp.py
409
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'#<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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue