Add inline VM opcodes for hot primitives (OP_ADD through OP_DEC)

16 new opcodes (160-175) bypass the CALL_PRIM hashtable lookup for
the most frequently called primitives:

  Arithmetic: OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_INC, OP_DEC, OP_NEG
  Comparison: OP_EQ, OP_LT, OP_GT, OP_NOT
  Collection: OP_LEN, OP_FIRST, OP_REST, OP_NTH, OP_CONS

The compiler (compiler.sx) recognizes these names at compile time and
emits the inline opcode instead of CALL_PRIM. The opcode is self-
contained — no constant pool index, no argc byte. Each primitive is
a single byte in the bytecode stream.

Implementation in all three VMs:
- OCaml (sx_vm.ml): direct pattern match, no allocation
- SX spec (vm.sx): delegates to existing primitives
- JS (transpiled): same as SX spec

66 new tests in spec/tests/vm-inline.sx covering arithmetic, comparison,
collection ops, composition, and edge cases.

Tests: 1314 JS (full), 1114 OCaml, 32 Playwright

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 20:10:48 +00:00
parent 5b2ef0a2af
commit eb4233ff36
6 changed files with 245 additions and 7 deletions

View File

@@ -756,13 +756,40 @@
(not (= (get (scope-resolve scope name) "type") "upvalue"))
(primitive? name))))))
(if is-prim
;; Direct primitive call — no closure overhead
;; Direct primitive call — try inline opcode first
(let ((name (symbol-name head))
(name-idx (pool-add (get em "pool") name)))
(for-each (fn (a) (compile-expr em a scope false)) args)
(emit-op em 52) ;; OP_CALL_PRIM
(emit-u16 em name-idx)
(emit-byte em (len args)))
(argc (len args))
(inline-op
(cond
;; Binary arithmetic/comparison (2 args)
(and (= argc 2) (= name "+")) 160
(and (= argc 2) (= name "-")) 161
(and (= argc 2) (= name "*")) 162
(and (= argc 2) (= name "/")) 163
(and (= argc 2) (= name "=")) 164
(and (= argc 2) (= name "<")) 165
(and (= argc 2) (= name ">")) 166
(and (= argc 2) (= name "nth")) 171
(and (= argc 2) (= name "cons")) 172
;; Unary (1 arg)
(and (= argc 1) (= name "not")) 167
(and (= argc 1) (= name "len")) 168
(and (= argc 1) (= name "first")) 169
(and (= argc 1) (= name "rest")) 170
(and (= argc 1) (= name "inc")) 174
(and (= argc 1) (= name "dec")) 175
:else nil)))
(if inline-op
;; Emit inline opcode — no constant pool lookup, no argc byte
(do
(for-each (fn (a) (compile-expr em a scope false)) args)
(emit-op em inline-op))
;; Fallback: CALL_PRIM with name lookup
(let ((name-idx (pool-add (get em "pool") name)))
(for-each (fn (a) (compile-expr em a scope false)) args)
(emit-op em 52) ;; OP_CALL_PRIM
(emit-u16 em name-idx)
(emit-byte em argc))))
;; General call
(do
(compile-expr em head scope false)