Add specialized opcodes, mutable strings, --fast flag

- 20 specialized opcodes: ADD, SUB, MUL, NEG, ADD1, SUB1, NUM_EQ, LT, GT,
  LE, GE, CAR, CDR, CONS, NULL?, PAIR?, NOT, ZERO?, VEC_REF, VEC_SET.
  Compiler pattern-matches (+ x 1) → ADD1, (- x 1) → SUB1.
- MutableString class: string-set!, string-fill!, string-copy! now work.
  make-string and string-copy return mutable strings.
- --fast / -f flag: auto-compile mode from command line.
- 529 tests green.
This commit is contained in:
russell@unturf.com 2026-04-13 16:19:27 -04:00
parent e091accd56
commit f89d3bce6f
2 changed files with 166 additions and 6 deletions

View file

@ -2583,6 +2583,58 @@ class TestBytecodeCompiler(unittest.TestCase):
assert run('((lambda (x) (+ x 1)) 5)', self.g) == 6
run('(auto-compile! #f)', self.g)
def test_specialized_opcodes_arithmetic(self):
"""Specialized opcodes for +, -, *, =, <, etc."""
self._compile_and_run('(define (f x y) (+ x y))', '(f 3 4)')
assert run('(f 3 4)', self.g) == 7
assert run('(f 1.5 2.5)', self.g) == 4.0
def test_specialized_opcodes_add1_sub1(self):
"""(+ x 1) → ADD1, (- x 1) → SUB1."""
self._compile_and_run(
'(define (inc x) (+ x 1))', '(inc 41)')
assert run('(inc 41)', self.g) == 42
self._compile_and_run(
'(define (dec x) (- x 1))', '(dec 43)')
assert run('(dec 43)', self.g) == 42
def test_specialized_opcodes_car_cdr_cons(self):
"""Specialized car, cdr, cons."""
self._compile_and_run(
'(define (hd lst) (car lst))', "(hd '(1 2 3))")
assert run("(hd '(1 2 3))", self.g) == 1
self._compile_and_run(
'(define (tl lst) (cdr lst))', "(tl '(1 2 3))")
assert show(run("(tl '(1 2 3))", self.g)) == '(2 3)'
self._compile_and_run(
'(define (pair a b) (cons a b))', '(pair 1 2)')
assert show(run('(pair 1 2)', self.g)) == '(1 . 2)'
def test_specialized_opcodes_predicates(self):
"""null?, pair?, not, zero?."""
self._compile_and_run(
"(define (f x) (null? x))", "(f '())")
assert run("(f '())", self.g) == True
assert run("(f '(1))", self.g) == False
def test_specialized_tail_position(self):
"""Specialized opcodes work in tail position."""
r = self._compile_and_run(
'(define (f x) (+ x 1))', '(f 99)')
assert r == 100
def test_string_set(self):
"""string-set! for mutable strings."""
run('(define s (string-copy "hello"))', self.g)
run('(string-set! s 0 #\\H)', self.g)
assert run('s', self.g) == "Hello"
def test_string_fill(self):
"""string-fill! fills a mutable string."""
run('(define s (make-string 3 #\\x))', self.g)
run('(string-fill! s #\\o)', self.g)
assert run('s', self.g) == "ooo"
###############################################################################
# Main

View file

@ -236,6 +236,38 @@ class StringOutputPort:
def close(self): pass
def getvalue(self): return ''.join(self._buf)
class MutableString:
"""Mutable string for R7RS string-set!, string-copy!, string-fill!."""
__slots__ = ('_c',)
def __init__(self, s):
self._c = list(s) if isinstance(s, str) else list(s._c) if isinstance(s, MutableString) else list(s)
def __len__(self): return len(self._c)
def __getitem__(self, k):
if isinstance(k, slice): return ''.join(self._c[k])
return self._c[k]
def __setitem__(self, k, v): self._c[k] = v
def __str__(self): return ''.join(self._c)
def __repr__(self): return '"' + str(self) + '"'
def __eq__(self, o):
if isinstance(o, MutableString): return self._c == o._c
if isinstance(o, str): return str(self) == o
return NotImplemented
def __hash__(self): return hash(str(self))
def __contains__(self, x): return x in str(self)
def __add__(self, o): return str(self) + (str(o) if isinstance(o, MutableString) else o)
def __radd__(self, o): return o + str(self)
# String-like methods for compatibility
def upper(self): return str(self).upper()
def lower(self): return str(self).lower()
def strip(self): return str(self).strip()
def rstrip(self): return str(self).rstrip()
def split(self, *a): return str(self).split(*a)
def find(self, *a): return str(self).find(*a)
def replace(self, *a): return str(self).replace(*a)
def startswith(self, *a): return str(self).startswith(*a)
def endswith(self, *a): return str(self).endswith(*a)
def join(self, it): return str(self).join(str(x) if isinstance(x, MutableString) else x for x in it)
###############################################################################
# Printer
###############################################################################
@ -251,6 +283,10 @@ def show(x, display=False):
if isinstance(x, Pair): return repr(x)
if isinstance(x, list): # vector
return '#(' + ' '.join(show(e) for e in x) + ')'
if isinstance(x, MutableString):
if display: return str(x)
return ('"' + str(x).replace('\\', '\\\\').replace('"', '\\"')
.replace('\n', '\\n').replace('\t', '\\t') + '"')
if isinstance(x, str):
if isinstance(x, Symbol): return str(x)
if display: return x
@ -1032,6 +1068,24 @@ 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 # call/cc
# Specialized opcodes (avoid LOOKUP+CALL for hot builtins)
OP_ADD = 60; OP_SUB = 61; OP_MUL = 62; OP_NEG = 63
OP_NUM_EQ = 64; OP_LT = 65; OP_GT = 66; OP_LE = 67; OP_GE = 68
OP_ADD1 = 69; OP_SUB1 = 70
OP_CAR = 71; OP_CDR = 72; OP_CONS = 73
OP_NULL_P = 74; OP_PAIR_P = 75; OP_NOT = 76; OP_ZERO_P = 77
OP_VEC_REF = 78; OP_VEC_SET = 79
# Specialization table: {symbol: {arity: opcode}}
_BC_SPECIALIZE = {
S('+'): {2: OP_ADD}, S('-'): {2: OP_SUB, 1: OP_NEG}, S('*'): {2: OP_MUL},
S('='): {2: OP_NUM_EQ}, S('<'): {2: OP_LT}, S('>'): {2: OP_GT},
S('<='): {2: OP_LE}, S('>='): {2: OP_GE},
S('car'): {1: OP_CAR}, S('cdr'): {1: OP_CDR}, S('cons'): {2: OP_CONS},
S('null?'): {1: OP_NULL_P}, S('pair?'): {1: OP_PAIR_P},
S('not'): {1: OP_NOT}, S('zero?'): {1: OP_ZERO_P},
S('vector-ref'): {2: OP_VEC_REF}, S('vector-set!'): {3: OP_VEC_SET},
}
# Constant folding tables
def _bc_is_const(expr):
@ -1347,7 +1401,24 @@ def _bc(expr, code, env, tail=False):
result = _BC_FOLDABLE[head]([a for a in call_args])
code.emit(OP_CONST, result); return
except Exception:
pass # fold failed, emit normal call
pass
# --- Specialized opcodes for hot builtins ---
if isinstance(head, Symbol):
n = len(call_args)
spec = _BC_SPECIALIZE.get(head)
if spec and n in spec:
# Special case: (+ x 1) → ADD1, (- x 1) → SUB1
if head is S('+') and n == 2:
if _bc_is_const(call_args[1]) and call_args[1] == 1:
_bc(call_args[0], code, env); code.emit(OP_ADD1); return
if _bc_is_const(call_args[0]) and call_args[0] == 1:
_bc(call_args[1], code, env); code.emit(OP_ADD1); return
if head is S('-') and n == 2:
if _bc_is_const(call_args[1]) and call_args[1] == 1:
_bc(call_args[0], code, env); code.emit(OP_SUB1); return
for arg in call_args: _bc(arg, code, env)
code.emit(spec[n]); return
# --- Function call ---
_bc(head, code, env)
@ -1516,6 +1587,28 @@ def _vm_loop(instrs, ip, stack, env, frames, vm_id):
elif callable(proc):
_ap(proc([cont], env))
else: raise LispErr(f'call/cc: not callable: {show(proc)}')
# ── Specialized opcodes ──────────────────────────────────────────
elif op == OP_ADD: b = _po(); stack[-1] = stack[-1] + b
elif op == OP_SUB: b = _po(); stack[-1] = stack[-1] - b
elif op == OP_MUL: b = _po(); stack[-1] = stack[-1] * b
elif op == OP_NEG: stack[-1] = -stack[-1]
elif op == OP_ADD1: stack[-1] = stack[-1] + 1
elif op == OP_SUB1: stack[-1] = stack[-1] - 1
elif op == OP_NUM_EQ: b = _po(); stack[-1] = stack[-1] == b
elif op == OP_LT: b = _po(); stack[-1] = stack[-1] < b
elif op == OP_GT: b = _po(); stack[-1] = stack[-1] > b
elif op == OP_LE: b = _po(); stack[-1] = stack[-1] <= b
elif op == OP_GE: b = _po(); stack[-1] = stack[-1] >= b
elif op == OP_CAR: stack[-1] = stack[-1].car
elif op == OP_CDR: stack[-1] = stack[-1].cdr
elif op == OP_CONS: d = _po(); stack[-1] = Pair(stack[-1], d)
elif op == OP_NULL_P: stack[-1] = stack[-1] is NIL
elif op == OP_PAIR_P: stack[-1] = _isinstance(stack[-1], Pair)
elif op == OP_NOT: stack[-1] = stack[-1] is False
elif op == OP_ZERO_P: stack[-1] = stack[-1] == 0
elif op == OP_VEC_REF: i = _po(); stack[-1] = stack[-1][i]
elif op == OP_VEC_SET:
v = _po(); i = _po(); stack[-1][i] = v; stack[-1] = VOID
return stack[-1] if stack else VOID
@ -1547,6 +1640,12 @@ def _disassemble(proc):
OP_MAKE_CLOSURE: 'MAKE_CLOSURE',
OP_PUSH_ENV: 'PUSH_ENV', OP_POP_ENV: 'POP_ENV', OP_BIND: 'BIND',
OP_EVAL: 'EVAL', OP_CALL_CC: 'CALL_CC',
OP_ADD: 'ADD', OP_SUB: 'SUB', OP_MUL: 'MUL', OP_NEG: 'NEG',
OP_ADD1: 'ADD1', OP_SUB1: 'SUB1',
OP_NUM_EQ: 'NUM_EQ', OP_LT: 'LT', OP_GT: 'GT', OP_LE: 'LE', OP_GE: 'GE',
OP_CAR: 'CAR', OP_CDR: 'CDR', OP_CONS: 'CONS',
OP_NULL_P: 'NULL?', OP_PAIR_P: 'PAIR?', OP_NOT: 'NOT', OP_ZERO_P: 'ZERO?',
OP_VEC_REF: 'VEC_REF', OP_VEC_SET: 'VEC_SET',
}
lines = [f'--- {proc.name or "λ"} '
f'({" ".join(str(p) for p in proc.params)}'
@ -1585,6 +1684,7 @@ def _num(x):
return x
def _str_val(x):
if isinstance(x, MutableString): return x
if not isinstance(x, str) or isinstance(x, Symbol):
raise LispErr(f'not a string: {show(x)}')
return x
@ -1814,7 +1914,7 @@ def make_global_env():
d(S('null?'), lambda a, _: a[0] is NIL)
d(S('list?'), lambda a, _: _is_proper_list(a[0]))
d(S('symbol?'), lambda a, _: isinstance(a[0], Symbol))
d(S('string?'), lambda a, _: isinstance(a[0], str) and not isinstance(a[0], Symbol))
d(S('string?'), lambda a, _: isinstance(a[0], MutableString) or (isinstance(a[0], str) and not isinstance(a[0], Symbol)))
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))
@ -2020,13 +2120,16 @@ def make_global_env():
d(S('apply'), lambda a, e: _call(a[0], (_L(a[-1]) if len(a)==2 else [leval(x,e) for x in a[1:-1]] + _L(a[-1])), e))
# ── Strings ───────────────────────────────────────────────────────────────
d(S('make-string'), lambda a, _: (a[1] if len(a) > 1 else ' ') * int(_num(a[0])))
d(S('make-string'), lambda a, _: MutableString((a[1] if len(a) > 1 else ' ') * int(_num(a[0]))))
d(S('string'), lambda a, _: ''.join(a))
d(S('string-length'), lambda a, _: len(_str_val(a[0])))
d(S('string-ref'), lambda a, _: _str_val(a[0])[int(_num(a[1]))])
d(S('substring'), lambda a, _: _str_val(a[0])[int(_num(a[1])): int(_num(a[2])) if len(a) > 2 else None])
d(S('string-append'), lambda a, _: ''.join(_str_val(x) for x in a))
d(S('string-copy'), lambda a, _: _str_val(a[0]))
d(S('substring'), lambda a, _: str(_str_val(a[0]))[int(_num(a[1])): int(_num(a[2])) if len(a) > 2 else None])
d(S('string-append'), lambda a, _: ''.join(str(_str_val(x)) for x in a))
d(S('string-copy'), lambda a, _: MutableString(_str_val(a[0])))
d(S('string-set!'), lambda a, _: (_str_val(a[0]).__setitem__(int(_num(a[1])), a[2]) or VOID) if isinstance(a[0], MutableString) else _raise(LispErr('string-set!: immutable string')))
d(S('string-fill!'), lambda a, _: [_str_val(a[0]).__setitem__(i, a[1]) for i in range(len(a[0]))] and VOID if isinstance(a[0], MutableString) else _raise(LispErr('string-fill!: immutable string')))
d(S('string-copy!'), lambda a, _: [a[0].__setitem__(int(_num(a[1]))+i, c) for i, c in enumerate(str(_str_val(a[2]))[int(_num(a[3])) if len(a)>3 else 0:int(_num(a[4])) if len(a)>4 else None])] and VOID if isinstance(a[0], MutableString) else _raise(LispErr('string-copy!: immutable dest')))
d(S('string->list'), lambda a, _: _P(list(_str_val(a[0]))))
d(S('list->string'), lambda a, _: ''.join(_L(a[0])))
d(S('string->symbol'), lambda a, _: S(_str_val(a[0])))
@ -2506,6 +2609,11 @@ def main():
args = sys.argv[1:]
# --fast / -f: enable auto-compile
if '--fast' in args or '-f' in args:
_auto_compile[0] = True
args = [a for a in args if a not in ('--fast', '-f')]
# -e 'expr' mode
if args and args[0] == '-e':
if len(args) < 2: