diff --git a/tests.py b/tests.py index 09b2d7a..c149163 100644 --- a/tests.py +++ b/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 diff --git a/uncommonlisp.py b/uncommonlisp.py index 853f01b..17de545 100644 --- a/uncommonlisp.py +++ b/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 '#' + +_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 ───────────────────────────────────────────────────────────