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:
parent
f584ad76f2
commit
e091accd56
2 changed files with 213 additions and 36 deletions
56
tests.py
56
tests.py
|
|
@ -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
|
||||
|
|
|
|||
193
uncommonlisp.py
193
uncommonlisp.py
|
|
@ -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 ───────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue