diff --git a/tests.py b/tests.py index c149163..9cb6015 100644 --- a/tests.py +++ b/tests.py @@ -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 diff --git a/uncommonlisp.py b/uncommonlisp.py index 17de545..5328bf8 100644 --- a/uncommonlisp.py +++ b/uncommonlisp.py @@ -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: