Add full continuations, explicit frame stack, env shortcut, constant folding

- Full call/cc: upward continuations via explicit VM frame stack + trampoline.
  Generators now work: (make-gen (lambda (yield) (yield 1) (yield 2)))
- Explicit frame stack: compiled→compiled CALL no longer grows Python stack.
  Non-tail calls use frames list instead of recursive vm_exec.
- Env global shortcut: lookup checks local then global before walking chain,
  O(1) for builtins instead of O(depth).
- Constant folding: arithmetic on literals folded at compile time.
- Auto-compile covers inline lambdas (not just define).
- 522 tests green, 7-19x speedup over interpreter.
This commit is contained in:
russell@unturf.com 2026-04-13 15:14:55 -04:00
parent f584ad76f2
commit e091accd56
2 changed files with 213 additions and 36 deletions

View file

@ -2527,6 +2527,62 @@ class TestBytecodeCompiler(unittest.TestCase):
assert run('(sum-to 100000)', self.g) == 5000050000
run('(auto-compile! #f)', self.g)
def test_full_continuation_upward(self):
"""Upward continuation: capture and invoke later."""
run('(auto-compile! #t)', self.g)
run('''(define saved-k #f)
(define (capture) (call/cc (lambda (k) (set! saved-k k) (quote first))))''', self.g)
assert run('(capture)', self.g) == S('first')
assert run("(saved-k (quote second))", self.g) == S('second')
run('(auto-compile! #f)', self.g)
def test_full_continuation_generator(self):
"""Generator pattern using full continuations."""
run('(auto-compile! #t)', self.g)
run('''
(define (make-gen thunk)
(let ((k #f) (done #f))
(lambda ()
(if done (quote done)
(call/cc
(lambda (return)
(if k (k return)
(begin
(thunk (lambda (val)
(call/cc (lambda (next)
(set! k next)
(return val)))))
(set! done #t)
(return (quote done))))))))))
(define gen (make-gen (lambda (yield)
(yield 10) (yield 20) (yield 30))))
''', self.g)
assert run('(gen)', self.g) == 10
assert run('(gen)', self.g) == 20
assert run('(gen)', self.g) == 30
assert run('(gen)', self.g) == S('done')
run('(auto-compile! #f)', self.g)
def test_constant_folding(self):
"""Compile-time constant folding for arithmetic."""
run('(auto-compile! #t)', self.g)
# (+ 1 2) should fold to 3 at compile time
run('(define (f) (+ 1 2))', self.g)
assert run('(f)', self.g) == 3
# Nested: (+ (* 3 4) 1) — inner folds, outer doesn't (different calls)
run('(define (g x) (+ x (* 3 4)))', self.g)
assert run('(g 1)', self.g) == 13
run('(auto-compile! #f)', self.g)
def test_auto_compile_inline_lambda(self):
"""Inline lambdas should be compiled when auto-compile is on."""
run('(auto-compile! #t)', self.g)
run("(define f (lambda (x) (* x 2)))", self.g)
assert run('(compiled? f)', self.g) == True
# Lambda in expression position
assert run('((lambda (x) (+ x 1)) 5)', self.g) == 6
run('(auto-compile! #f)', self.g)
###############################################################################
# Main

View file

@ -246,7 +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, (CompiledProc, FullCont)): 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
@ -413,11 +413,17 @@ def _body_env(forms, env):
###############################################################################
class Env:
__slots__ = ('b', 'p')
def __init__(self, parent=None): self.b = {}; self.p = parent
__slots__ = ('b', 'p', 'g')
def __init__(self, parent=None):
self.b = {}; self.p = parent
self.g = parent.g if parent else None # global env shortcut
def lookup(self, k):
e = self
b = self.b
if k in b: return b[k]
g = self.g
if g is not None and k in g.b: return g.b[k]
e = self.p
while e:
if k in e.b: return e.b[k]
e = e.p
@ -683,7 +689,11 @@ def leval(expr, env):
a = _L(tail)
if not a: raise LispErr('lambda: empty')
ps, rest = _formals(a[0])
return Proc(ps, rest, a[1:], env)
p = Proc(ps, rest, a[1:], env)
if _auto_compile[0]:
try: p = bc_compile_proc(p, env)
except Exception: pass
return p
if head is S('let'):
a = _L(tail)
@ -946,8 +956,12 @@ def leval(expr, env):
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))
try:
return vm_exec(proc.code,
proc.env.child(proc.params, proc.rest, args))
except _ContInvoked as ci:
if _vm_depth[0] > 0: raise
return _cont_resume(ci)
if isinstance(proc, Proc):
env = proc.env.child(proc.params, proc.rest, args)
@ -956,16 +970,34 @@ def leval(expr, env):
else: expr = Pair(S('begin'), _P(body))
continue
if callable(proc): return proc(args, env)
if callable(proc):
try: return proc(args, env)
except _ContInvoked as ci:
if _vm_depth[0] > 0: raise
return _cont_resume(ci)
raise LispErr(f'not callable: {show(proc)}')
def _cont_resume(ci):
"""Resume an escaped continuation."""
c = ci.cont
frames = [(i, p, e, list(s)) for i, p, e, s in c.frames]
stack = list(c.stack); stack.append(ci.val)
try:
return _vm_loop(c.instrs, c.ip, stack, c.env, frames, c.vm_id)
except _ContInvoked as ci2:
return _cont_resume(ci2)
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))
try:
return vm_exec(proc.code,
proc.env.child(proc.params, proc.rest, args))
except _ContInvoked as ci:
if _vm_depth[0] > 0: raise
return _cont_resume(ci)
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
@ -999,7 +1031,37 @@ 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
OP_CALL_CC = 51 # escape-only call/cc
OP_CALL_CC = 51 # call/cc
# Constant folding tables
def _bc_is_const(expr):
"""Is expr a compile-time constant?"""
if isinstance(expr, (int, float, Fraction)): return True
if isinstance(expr, bool): return True
if isinstance(expr, str) and not isinstance(expr, Symbol): return True
if expr is NIL or expr is VOID or expr is True or expr is False: return True
return False
import operator as _op, functools as _ft
_BC_FOLDABLE = {
S('+'): lambda a: _ft.reduce(_op.add, a, 0),
S('-'): lambda a: -a[0] if len(a) == 1 else _ft.reduce(_op.sub, a[1:], a[0]),
S('*'): lambda a: _ft.reduce(_op.mul, a, 1),
S('='): lambda a: a[0] == a[1],
S('<'): lambda a: a[0] < a[1],
S('>'): lambda a: a[0] > a[1],
S('<='): lambda a: a[0] <= a[1],
S('>='): lambda a: a[0] >= a[1],
S('not'): lambda a: not _truthy(a[0]),
S('zero?'): lambda a: a[0] == 0,
S('positive?'): lambda a: a[0] > 0,
S('negative?'): lambda a: a[0] < 0,
S('abs'): lambda a: abs(a[0]),
S('min'): lambda a: min(a),
S('max'): lambda a: max(a),
S('string-length'): lambda a: len(a[0]) if isinstance(a[0], str) else None,
S('string-append'): lambda a: ''.join(a),
}
class CodeObj:
"""Compiled bytecode chunk."""
@ -1278,9 +1340,17 @@ def _bc(expr, code, env, tail=False):
except LispErr:
pass
# --- Constant folding ---
call_args = _L(args)
if isinstance(head, Symbol) and head in _BC_FOLDABLE and all(_bc_is_const(a) for a in call_args):
try:
result = _BC_FOLDABLE[head]([a for a in call_args])
code.emit(OP_CONST, result); return
except Exception:
pass # fold failed, emit normal call
# --- 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))
@ -1319,9 +1389,46 @@ def _bc_lambda(body, params, rest, env, name=None):
return inner
class _ContInvoked(Exception):
"""Raised when a full continuation is invoked."""
__slots__ = ('cont', 'val')
def __init__(self, cont, val): self.cont = cont; self.val = val
class FullCont:
"""Full continuation (supports upward escape from call/cc)."""
__slots__ = ('frames', 'stack', 'ip', 'instrs', 'env', 'vm_id')
def __init__(self, frames, stack, ip, instrs, env, vm_id):
self.frames = [(i, p, e, list(s)) for i, p, e, s in frames]
self.stack = list(stack); self.ip = ip
self.instrs = instrs; self.env = env; self.vm_id = vm_id
def __call__(self, args, _env):
raise _ContInvoked(self, args[0] if args else VOID)
def __repr__(self): return '#<continuation>'
_vm_depth = [0]
def vm_exec(code, env):
"""Execute compiled bytecode. Optimized with local aliases."""
instrs = code.instrs; ip = 0; stack = []; _ap = stack.append; _po = stack.pop
"""Execute compiled bytecode with explicit frame stack and continuation support."""
vm_id = object() # unique per invocation
_vm_depth[0] += 1
try:
instrs = code.instrs; ip = 0; stack = []; frames = []
while True:
try:
return _vm_loop(instrs, ip, stack, env, frames, vm_id)
except _ContInvoked as ci:
if ci.cont.vm_id is not vm_id:
raise # not our continuation, propagate to outer VM
c = ci.cont
frames = [(i, p, e, list(s)) for i, p, e, s in c.frames]
stack = list(c.stack); stack.append(ci.val)
ip = c.ip; instrs = c.instrs; env = c.env
finally:
_vm_depth[0] -= 1
def _vm_loop(instrs, ip, stack, env, frames, vm_id):
"""Inner VM loop with explicit frame stack."""
_ap = stack.append; _po = stack.pop
_isinstance = isinstance; _CP = CompiledProc; _Pr = Proc
while ip < len(instrs):
op, arg = instrs[ip]; ip += 1
@ -1347,8 +1454,11 @@ def vm_exec(code, env):
else: args_ = []
func = _po()
if _isinstance(func, _CP):
_ap(vm_exec(func.code,
func.env.child(func.params, func.rest, args_)))
frames.append((instrs, ip, env, stack))
env = func.env.child(func.params, func.rest, args_)
instrs = func.code.instrs; ip = 0
stack = []; _ap = stack.append; _po = stack.pop
continue
elif _isinstance(func, _Pr):
_ap(_call(func, args_, env))
elif callable(func):
@ -1361,18 +1471,30 @@ def vm_exec(code, env):
func = _po()
if _isinstance(func, _CP):
env = func.env.child(func.params, func.rest, args_)
instrs = func.code.instrs; ip = 0; stack.clear()
_ap = stack.append; _po = stack.pop
continue
instrs = func.code.instrs; ip = 0
stack.clear(); continue
elif _isinstance(func, _Pr):
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)
ret = leval(body[-1], c)
if not frames: return ret
instrs, ip, env, stack = frames.pop()
_ap = stack.append; _po = stack.pop
_ap(ret); continue
elif callable(func):
return func(args_, env)
ret = func(args_, env)
if not frames: return ret
instrs, ip, env, stack = frames.pop()
_ap = stack.append; _po = stack.pop
_ap(ret); continue
else: raise LispErr(f'not callable: {show(func)}')
elif op == OP_RETURN: return _po() if stack else VOID
elif op == OP_RETURN:
ret = _po() if stack else VOID
if not frames: return ret
instrs, ip, env, stack = frames.pop()
_ap = stack.append; _po = stack.pop
_ap(ret); continue
elif op == OP_MAKE_CLOSURE:
inner_code, params, rest = arg
_ap(_CP(inner_code, params, rest, env, inner_code.name))
@ -1382,20 +1504,18 @@ def vm_exec(code, env):
elif op == OP_EVAL: _ap(leval(arg, env))
elif op == OP_CALL_CC:
proc = _po()
class _Esc(Exception):
__slots__ = ('v',)
def __init__(self, v): self.v = v
def _kont(a, _e): raise _Esc(a[0] if a else VOID)
try:
if _isinstance(proc, _CP):
_ap(vm_exec(proc.code,
proc.env.child(proc.params, proc.rest, [_kont])))
elif _isinstance(proc, _Pr):
_ap(_call(proc, [_kont], env))
elif callable(proc):
_ap(proc([_kont], env))
else: raise LispErr(f'call/cc: not callable: {show(proc)}')
except _Esc as e: _ap(e.v)
cont = FullCont(frames, stack, ip, instrs, env, vm_id)
if _isinstance(proc, _CP):
frames.append((instrs, ip, env, stack))
env = proc.env.child(proc.params, proc.rest, [cont])
instrs = proc.code.instrs; ip = 0
stack = []; _ap = stack.append; _po = stack.pop
continue
elif _isinstance(proc, _Pr):
_ap(_call(proc, [cont], env))
elif callable(proc):
_ap(proc([cont], env))
else: raise LispErr(f'call/cc: not callable: {show(proc)}')
return stack[-1] if stack else VOID
@ -1570,6 +1690,7 @@ def _pprint(x, indent=0, width=72):
def make_global_env():
g = Env()
g.g = g # global env shortcut: children skip directly here for builtins
d = g.define
# ── Arithmetic ───────────────────────────────────────────────────────────