Skip to content

Remove unused typed local opcodes from compiler and VM#74

Merged
frostney merged 3 commits intomainfrom
opt/remove-typed-locals
Mar 13, 2026
Merged

Remove unused typed local opcodes from compiler and VM#74
frostney merged 3 commits intomainfrom
opt/remove-typed-locals

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Mar 12, 2026

Summary

  • Removes 10 typed local opcodes (OP_GET_LOCAL_INT through OP_SET_LOCAL_REF) that had identical implementations to OP_GET_LOCAL/OP_SET_LOCAL — plain register copies with no type-specific behavior.
  • The optimization decision (choosing OP_ADD_FLOAT vs OP_RT_ADD) comes from ExpressionType at compile time, not from which load opcode was used.
  • Simplifies the VM dispatch surface (10 fewer case arms in both ExecuteLoop and ExecuteCoreOp) and removes the TypedGetLocalOp/TypedSetLocalOp compiler helpers.
  • Opcode enum entries preserved in Souffle.Bytecode.pas for .sbc backward compatibility.

Test plan

  • All JS tests pass in interpreter mode
  • All JS tests pass in bytecode mode
  • CI benchmark comparison will confirm no performance change

Made with Cursor

Summary by CodeRabbit

  • Refactor

    • Unified all type-specific local variable loads/stores into single generic local get/set operations across VM, translator, and compiler, removing the specialized variants while preserving behavior.
  • Chores

    • Bumped bytecode/format version to reflect the instruction set change.
  • Documentation

    • Updated docs to describe the unified local access model and where type-specialization now occurs.

The 10 typed local opcodes (OP_GET_LOCAL_INT..OP_SET_LOCAL_REF) had
identical implementations to OP_GET_LOCAL/OP_SET_LOCAL — plain register
copies with no type-specific behavior. The optimization decision
(OP_ADD_FLOAT vs OP_RT_ADD) comes from ExpressionType at compile time,
not from the load opcode. Removing these simplifies the dispatch surface
without changing any optimization behavior.

Changes:
- Remove TypedGetLocalOp/TypedSetLocalOp helpers from compiler
- Compiler now always emits OP_GET_LOCAL/OP_SET_LOCAL
- Remove typed local case arms from VM ExecuteLoop and ExecuteCoreOp
- Remove typed local entries from WASM translator
- Opcode enum entries preserved for .sbc backward compatibility

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

Removes all type-specific local load/store opcodes and related helpers; compiler, Wasm translator, VM, and docs now use only generic OP_GET_LOCAL / OP_SET_LOCAL. Bytecode format version is bumped to v4 to reflect the opcode set change.

Changes

Cohort / File(s) Summary
VM Execution Engine
souffle/Souffle.VM.pas
Deleted case handlers for typed local ops (OP_GET_LOCAL_*, OP_SET_LOCAL_*) from ExecuteLoop / ExecuteCoreOp; VM now handles only generic OP_GET_LOCAL / OP_SET_LOCAL.
Bytecode / Opcode defs
souffle/Souffle.Bytecode.pas
Bumped SOUFFLE_FORMAT_VERSION to 4 and removed the ten typed-local opcodes from TSouffleOpCode (creates a gap/renumbering in prior opcode space).
WASM Translation
souffle/Souffle.Wasm.Translator.pas
Consolidated multiple per-type opcode branches into single OP_GET_LOCAL / OP_SET_LOCAL emission paths in EmitInstruction.
Compiler Expressions
units/Goccia.Compiler.Expressions.pas
Removed TypedSetLocalOp and related typed-get/set selection; CompileIdentifierAccess and CompileAssignment now emit generic local get/set unconditionally.
Documentation
docs/souffle-vm.md
Removed typed-local opcodes from Tier 1 list; updated explanation to state loads/stores are generic and optimizations rely on typed arithmetic/compare opcodes.

Sequence Diagram(s)

sequenceDiagram
  participant Compiler as Compiler
  participant Translator as WasmTranslator
  participant VM as VM
  Compiler->>Translator: emit OP_GET_LOCAL / OP_SET_LOCAL for a local slot
  Translator->>VM: translate to wasm local.get / local.set (or bytecode stream)
  VM->>VM: Execute generic OP_GET_LOCAL / OP_SET_LOCAL\n(access slot without per-type branch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇
I hopped through ops and left a trail,
Five small paths made into one pale.
One gentle hop, a simpler tune,
Moonlit bytes beneath the rune.
Nibble, nest — the VM's new noon.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main change: removal of typed local opcodes (OP_GET_LOCAL_INT, OP_SET_LOCAL_INT, etc.) from the compiler and VM, which is the core objective across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch opt/remove-typed-locals
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
souffle/Souffle.Wasm.Translator.pas (1)

638-647: ⚠️ Potential issue | 🔴 Critical

Keep the legacy typed-local opcodes mapped here.

Souffle.Bytecode.pas:101-111 still declares OP_GET_LOCAL_INT through OP_SET_LOCAL_REF for old .sbc compatibility. After this change, those legacy opcodes are no longer recognized by the Wasm translator and will fall into the default EmitUnreachable path, so translating persisted bytecode that predates this PR will break.

🔧 Minimal fix
-    OP_GET_LOCAL:
+    OP_GET_LOCAL,
+    OP_GET_LOCAL_INT,
+    OP_GET_LOCAL_FLOAT,
+    OP_GET_LOCAL_BOOL,
+    OP_GET_LOCAL_STRING,
+    OP_GET_LOCAL_REF:
     begin
       ABuilder.EmitLocalGet(Bx);
       ABuilder.EmitLocalSet(A);
     end;
-    OP_SET_LOCAL:
+    OP_SET_LOCAL,
+    OP_SET_LOCAL_INT,
+    OP_SET_LOCAL_FLOAT,
+    OP_SET_LOCAL_BOOL,
+    OP_SET_LOCAL_STRING,
+    OP_SET_LOCAL_REF:
     begin
       ABuilder.EmitLocalGet(A);
       ABuilder.EmitLocalSet(Bx);
     end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@souffle/Souffle.Wasm.Translator.pas` around lines 638 - 647, The Wasm
translator removed handling for legacy typed-local opcodes (e.g.,
OP_GET_LOCAL_INT .. OP_SET_LOCAL_REF) so older .sbc files will fall through to
EmitUnreachable; restore support by adding case branches mapping each legacy
opcode to the same actions as the generic handlers (use
EmitLocalGet/EmitLocalSet with the appropriate operands), i.e., implement cases
for OP_GET_LOCAL_INT, OP_GET_LOCAL_REF, OP_SET_LOCAL_INT, OP_SET_LOCAL_REF (and
any other legacy OP_GET/OP_SET variants declared in Souffle.Bytecode.pas) that
call ABuilder.EmitLocalGet(...) and ABuilder.EmitLocalSet(...) the same way
OP_GET_LOCAL and OP_SET_LOCAL do so legacy bytecode is recognized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@souffle/Souffle.Wasm.Translator.pas`:
- Around line 638-647: The Wasm translator removed handling for legacy
typed-local opcodes (e.g., OP_GET_LOCAL_INT .. OP_SET_LOCAL_REF) so older .sbc
files will fall through to EmitUnreachable; restore support by adding case
branches mapping each legacy opcode to the same actions as the generic handlers
(use EmitLocalGet/EmitLocalSet with the appropriate operands), i.e., implement
cases for OP_GET_LOCAL_INT, OP_GET_LOCAL_REF, OP_SET_LOCAL_INT, OP_SET_LOCAL_REF
(and any other legacy OP_GET/OP_SET variants declared in Souffle.Bytecode.pas)
that call ABuilder.EmitLocalGet(...) and ABuilder.EmitLocalSet(...) the same way
OP_GET_LOCAL and OP_SET_LOCAL do so legacy bytecode is recognized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0597decc-fbe2-4459-a801-951d00309e7b

📥 Commits

Reviewing files that changed from the base of the PR and between aaa31fe and 76a85e6.

📒 Files selected for processing (3)
  • souffle/Souffle.VM.pas
  • souffle/Souffle.Wasm.Translator.pas
  • units/Goccia.Compiler.Expressions.pas
💤 Files with no reviewable changes (1)
  • souffle/Souffle.VM.pas

@frostney
Copy link
Copy Markdown
Owner Author

TODO: Remove opcode enums entirely - Backwards compatibility is not a priority at the moment and we should up the SBC version number

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 3463 3463
Tests Passed 3422 ✅ 3463 ✅
Tests Skipped 41 0
Tests Execution 149.7ms 167.9ms
Tests Engine 306.1ms 683.5ms
Benchmarks Total 254 254
Benchmarks Duration 6.66min 10.58min

Measured on ubuntu-latest x64.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 12, 2026

Benchmark Results

254 benchmarks

Interpreted: 🟢 46 improved · 🔴 1 regressed · 207 unchanged · avg +4.7%
Bytecode: 🟢 8 improved · 🔴 2 regressed · 244 unchanged · avg +1.1%

arraybuffer.js — Interp: 🟢 2, 12 unch. · avg +3.9% · Bytecode: 14 unch. · avg -2.1%
Benchmark Interpreted Δ Bytecode Δ
create ArrayBuffer(0) 421,967 → 444,591 +5.4% 146,658 → 144,793 -1.3%
create ArrayBuffer(64) 412,708 → 433,810 +5.1% 142,667 → 142,866 +0.1%
create ArrayBuffer(1024) 317,332 → 330,389 +4.1% 130,112 → 128,653 -1.1%
create ArrayBuffer(8192) 138,729 → 139,907 +0.8% 81,593 → 80,361 -1.5%
slice full buffer (64 bytes) 507,944 → 538,328 +6.0% 384,102 → 365,311 -4.9%
slice half buffer (512 of 1024 bytes) 430,211 → 460,898 🟢 +7.1% 338,592 → 323,968 -4.3%
slice with negative indices 427,289 → 468,260 🟢 +9.6% 364,038 → 350,823 -3.6%
slice empty range 498,101 → 532,289 +6.9% 377,390 → 364,373 -3.4%
byteLength access 1,596,224 → 1,587,867 -0.5% 1,055,008 → 1,036,363 -1.8%
Symbol.toStringTag access 1,192,516 → 1,188,569 -0.3% 545,389 → 540,542 -0.9%
ArrayBuffer.isView 747,110 → 757,938 +1.4% 469,688 → 450,990 -4.0%
clone ArrayBuffer(64) 374,847 → 386,542 +3.1% 318,287 → 311,551 -2.1%
clone ArrayBuffer(1024) 291,518 → 301,207 +3.3% 246,636 → 243,975 -1.1%
clone ArrayBuffer inside object 254,935 → 263,230 +3.3% 156,135 → 155,957 -0.1%
arrays.js — Interp: 🟢 4, 15 unch. · avg +3.3% · Bytecode: 19 unch. · avg +1.2%
Benchmark Interpreted Δ Bytecode Δ
Array.from length 100 14,300 → 14,294 -0.0% 12,842 → 13,137 +2.3%
Array.from 10 elements 220,145 → 237,882 🟢 +8.1% 152,923 → 156,275 +2.2%
Array.of 10 elements 301,512 → 311,977 +3.5% 224,569 → 224,609 +0.0%
spread into new array 328,496 → 353,136 🟢 +7.5% 570,753 → 569,338 -0.2%
map over 50 elements 28,352 → 28,569 +0.8% 22,135 → 22,369 +1.1%
filter over 50 elements 24,269 → 24,438 +0.7% 21,416 → 21,815 +1.9%
reduce sum 50 elements 27,488 → 27,704 +0.8% 19,284 → 19,838 +2.9%
forEach over 50 elements 23,439 → 23,365 -0.3% 23,510 → 23,724 +0.9%
find in 50 elements 35,313 → 35,632 +0.9% 28,036 → 28,445 +1.5%
sort 20 elements 12,140 → 12,514 +3.1% 3,177 → 3,190 +0.4%
flat nested array 108,188 → 122,163 🟢 +12.9% 313,437 → 311,626 -0.6%
flatMap 70,076 → 75,021 🟢 +7.1% 211,685 → 214,567 +1.4%
map inside map (5x5) 20,883 → 22,007 +5.4% 69,693 → 70,007 +0.4%
filter inside map (5x10) 15,877 → 16,371 +3.1% 13,953 → 14,093 +1.0%
reduce inside map (5x10) 19,437 → 19,855 +2.2% 14,014 → 14,176 +1.2%
forEach inside forEach (5x10) 16,597 → 16,718 +0.7% 15,323 → 15,744 +2.8%
find inside some (10x10) 13,895 → 14,070 +1.3% 10,950 → 11,148 +1.8%
map+filter chain nested (5x20) 5,490 → 5,530 +0.7% 4,509 → 4,595 +1.9%
reduce flatten (10x5) 37,914 → 39,618 +4.5% 6,085 → 6,092 +0.1%
async-await.js — Interp: 🟢 3, 3 unch. · avg +6.5% · Bytecode: 6 unch. · avg +0.7%
Benchmark Interpreted Δ Bytecode Δ
single await 365,057 → 383,655 +5.1% 277,944 → 277,833 -0.0%
multiple awaits 161,054 → 172,812 🟢 +7.3% 119,624 → 119,866 +0.2%
await non-Promise value 831,676 → 876,153 +5.3% 901,554 → 933,453 +3.5%
await with try/catch 356,047 → 372,969 +4.8% 271,580 → 270,621 -0.4%
await Promise.all 48,516 → 52,797 🟢 +8.8% 43,669 → 44,185 +1.2%
nested async function call 179,423 → 192,872 🟢 +7.5% 207,981 → 207,761 -0.1%
classes.js — Interp: 31 unch. · avg +3.8% · Bytecode: 31 unch. · avg +0.5%
Benchmark Interpreted Δ Bytecode Δ
simple class new 114,658 → 117,532 +2.5% 338,222 → 343,668 +1.6%
class with defaults 91,529 → 95,029 +3.8% 239,266 → 241,228 +0.8%
50 instances via Array.from 5,440 → 5,553 +2.1% 6,384 → 6,413 +0.5%
instance method call 58,082 → 59,380 +2.2% 153,390 → 152,804 -0.4%
static method call 90,225 → 93,139 +3.2% 359,870 → 353,445 -1.8%
single-level inheritance 45,558 → 46,425 +1.9% 152,229 → 149,949 -1.5%
two-level inheritance 38,540 → 39,399 +2.2% 130,358 → 129,590 -0.6%
private field access 57,580 → 59,262 +2.9% 169,516 → 167,187 -1.4%
private methods 62,574 → 64,551 +3.2% 217,406 → 214,511 -1.3%
getter/setter access 64,616 → 66,387 +2.7% 169,354 → 167,464 -1.1%
class decorator (identity) 79,195 → 82,869 +4.6% 54,083 → 52,233 -3.4%
class decorator (wrapping) 45,868 → 47,569 +3.7% 38,099 → 38,547 +1.2%
identity method decorator 56,426 → 59,071 +4.7% 44,999 → 46,257 +2.8%
wrapping method decorator 46,369 → 48,712 +5.1% 38,834 → 38,847 +0.0%
stacked method decorators (x3) 32,389 → 34,380 +6.1% 28,098 → 28,635 +1.9%
identity field decorator 63,344 → 67,150 +6.0% 46,092 → 47,001 +2.0%
field initializer decorator 53,935 → 56,545 +4.8% 41,788 → 42,128 +0.8%
getter decorator (identity) 55,358 → 57,420 +3.7% 40,699 → 41,444 +1.8%
setter decorator (identity) 46,704 → 48,484 +3.8% 35,093 → 36,049 +2.7%
static method decorator 60,399 → 63,150 +4.6% 62,959 → 63,756 +1.3%
static field decorator 69,683 → 73,352 +5.3% 66,959 → 67,018 +0.1%
private method decorator 46,139 → 48,499 +5.1% 37,455 → 38,165 +1.9%
private field decorator 51,511 → 52,905 +2.7% 38,468 → 39,249 +2.0%
plain auto-accessor (no decorator) 86,783 → 91,039 +4.9% 54,192 → 54,787 +1.1%
auto-accessor with decorator 50,330 → 53,095 +5.5% 37,342 → 38,170 +2.2%
decorator writing metadata 41,537 → 43,971 +5.9% 42,152 → 43,169 +2.4%
static getter read 101,736 → 105,054 +3.3% 383,144 → 380,510 -0.7%
static getter/setter pair 77,786 → 80,060 +2.9% 208,226 → 207,423 -0.4%
inherited static getter 58,866 → 60,233 +2.3% 262,035 → 262,683 +0.2%
inherited static setter 63,650 → 65,135 +2.3% 203,868 → 204,849 +0.5%
inherited static getter with this binding 53,213 → 54,490 +2.4% 154,484 → 154,221 -0.2%
closures.js — Interp: 🟢 1, 10 unch. · avg +4.7% · Bytecode: 11 unch. · avg +0.5%
Benchmark Interpreted Δ Bytecode Δ
closure over single variable 123,589 → 131,541 +6.4% 589,065 → 596,786 +1.3%
closure over multiple variables 117,116 → 121,224 +3.5% 389,441 → 392,828 +0.9%
nested closures 121,010 → 128,581 +6.3% 554,552 → 562,213 +1.4%
function as argument 94,190 → 100,071 +6.2% 491,152 → 498,167 +1.4%
function returning function 117,427 → 122,646 +4.4% 588,813 → 600,365 +2.0%
compose two functions 69,896 → 73,501 +5.2% 353,239 → 357,719 +1.3%
fn.call 147,425 → 154,556 +4.8% 137,278 → 137,802 +0.4%
fn.apply 110,315 → 115,461 +4.7% 94,462 → 94,159 -0.3%
fn.bind 132,232 → 141,815 🟢 +7.2% 143,039 → 144,080 +0.7%
recursive sum to 50 11,978 → 12,169 +1.6% 38,927 → 37,563 -3.5%
recursive tree traversal 19,458 → 19,758 +1.5% 58,306 → 58,562 +0.4%
collections.js — Interp: 🟢 2, 10 unch. · avg +2.7% · Bytecode: 12 unch. · avg +1.4%
Benchmark Interpreted Δ Bytecode Δ
add 50 elements 7,138 → 7,212 +1.0% 5,672 → 5,788 +2.0%
has lookup (50 elements) 89,465 → 89,165 -0.3% 86,313 → 86,656 +0.4%
delete elements 47,513 → 48,297 +1.6% 36,783 → 36,801 +0.0%
forEach iteration 15,686 → 16,214 +3.4% 15,325 → 15,567 +1.6%
spread to array 30,804 → 33,417 🟢 +8.5% 145,515 → 147,574 +1.4%
deduplicate array 40,910 → 42,874 +4.8% 46,658 → 47,415 +1.6%
set 50 entries 5,313 → 5,403 +1.7% 5,789 → 5,772 -0.3%
get lookup (50 entries) 86,692 → 86,393 -0.3% 95,800 → 97,121 +1.4%
has check 129,653 → 128,773 -0.7% 150,478 → 154,162 +2.4%
delete entries 45,408 → 45,760 +0.8% 35,284 → 35,842 +1.6%
forEach iteration 15,502 → 16,265 +4.9% 15,632 → 15,908 +1.8%
keys/values/entries 8,178 → 8,755 🟢 +7.1% 22,373 → 22,890 +2.3%
destructuring.js — Interp: 🟢 10, 12 unch. · avg +6.6% · Bytecode: 22 unch. · avg +1.7%
Benchmark Interpreted Δ Bytecode Δ
simple array destructuring 417,360 → 434,943 +4.2% 608,533 → 642,663 +5.6%
with rest element 272,645 → 310,199 🟢 +13.8% 480,009 → 483,493 +0.7%
with defaults 421,870 → 434,285 +2.9% 658,700 → 684,238 +3.9%
skip elements 424,552 → 469,026 🟢 +10.5% 705,349 → 722,035 +2.4%
nested array destructuring 173,301 → 189,969 🟢 +9.6% 333,888 → 355,449 +6.5%
swap variables 509,172 → 539,246 +5.9% 990,612 → 1,004,117 +1.4%
simple object destructuring 296,906 → 305,234 +2.8% 504,260 → 529,941 +5.1%
with defaults 355,435 → 372,706 +4.9% 316,614 → 322,273 +1.8%
with renaming 313,351 → 330,594 +5.5% 587,366 → 605,572 +3.1%
nested object destructuring 146,887 → 157,940 🟢 +7.5% 248,565 → 249,173 +0.2%
rest properties 183,921 → 198,324 🟢 +7.8% 225,098 → 223,127 -0.9%
object parameter 91,186 → 95,575 +4.8% 183,291 → 192,437 +5.0%
array parameter 121,036 → 129,371 +6.9% 311,744 → 307,817 -1.3%
mixed destructuring in map 34,535 → 35,798 +3.7% 33,901 → 34,846 +2.8%
forEach with array destructuring 64,945 → 69,777 🟢 +7.4% 138,059 → 136,815 -0.9%
map with array destructuring 65,513 → 71,157 🟢 +8.6% 164,348 → 167,222 +1.7%
filter with array destructuring 68,233 → 74,274 🟢 +8.9% 197,837 → 198,996 +0.6%
reduce with array destructuring 73,697 → 79,316 🟢 +7.6% 183,977 → 181,533 -1.3%
map with object destructuring 78,706 → 81,193 +3.2% 75,319 → 75,525 +0.3%
map with nested destructuring 64,380 → 67,562 +4.9% 62,124 → 61,523 -1.0%
map with rest in destructuring 36,243 → 39,894 🟢 +10.1% 22,437 → 22,443 +0.0%
map with defaults in destructuring 58,054 → 59,984 +3.3% 37,982 → 38,302 +0.8%
fibonacci.js — Interp: 8 unch. · avg +3.6% · Bytecode: 🟢 1, 7 unch. · avg +2.6%
Benchmark Interpreted Δ Bytecode Δ
recursive fib(15) 327 → 344 +5.2% 1,135 → 1,136 +0.1%
recursive fib(20) 30 → 31 +2.8% 102 → 102 -0.0%
recursive fib(15) typed 335 → 339 +1.3% 1,432 → 1,435 +0.2%
recursive fib(20) typed 30 → 31 +2.2% 127 → 129 +1.8%
iterative fib(20) via reduce 11,971 → 12,238 +2.2% 8,249 → 8,883 🟢 +7.7%
iterator fib(20) 9,109 → 9,625 +5.7% 14,293 → 14,943 +4.5%
iterator fib(20) via Iterator.from + take 14,240 → 14,939 +4.9% 15,931 → 16,799 +5.4%
iterator fib(20) last value via reduce 10,990 → 11,494 +4.6% 12,412 → 12,572 +1.3%
for-of.js — Interp: 7 unch. · avg +4.5% · Bytecode: 7 unch. · avg -1.1%
Benchmark Interpreted Δ Bytecode Δ
for...of with 10-element array 46,637 → 48,291 +3.5% 151,702 → 147,694 -2.6%
for...of with 100-element array 5,421 → 5,549 +2.3% 21,572 → 20,946 -2.9%
for...of with string (10 chars) 33,808 → 35,885 +6.1% 118,340 → 117,194 -1.0%
for...of with Set (10 elements) 46,767 → 49,489 +5.8% 162,415 → 154,768 -4.7%
for...of with Map entries (10 entries) 29,648 → 31,431 +6.0% 45,352 → 48,237 +6.4%
for...of with destructuring 39,815 → 41,696 +4.7% 69,941 → 70,309 +0.5%
for-await-of with sync array 44,612 → 45,936 +3.0% 128,666 → 124,184 -3.5%
iterators.js — Interp: 🟢 7, 13 unch. · avg +6.8% · Bytecode: 20 unch. · avg +2.2%
Benchmark Interpreted Δ Bytecode Δ
Iterator.from({next}).toArray() — 20 elements 14,380 → 14,903 +3.6% 16,533 → 16,624 +0.6%
Iterator.from({next}).toArray() — 50 elements 6,288 → 6,557 +4.3% 7,139 → 7,416 +3.9%
spread pre-wrapped iterator — 20 elements 10,821 → 11,745 🟢 +8.5% 15,311 → 16,014 +4.6%
Iterator.from({next}).forEach — 50 elements 4,363 → 4,604 +5.5% 5,087 → 5,326 +4.7%
Iterator.from({next}).reduce — 50 elements 4,330 → 4,687 🟢 +8.2% 4,985 → 5,108 +2.5%
wrap array iterator 163,259 → 183,983 🟢 +12.7% 105,774 → 108,595 +2.7%
wrap plain {next()} object 9,798 → 10,576 🟢 +7.9% 12,432 → 12,982 +4.4%
map + toArray (50 elements) 4,444 → 4,823 🟢 +8.5% 5,755 → 5,803 +0.8%
filter + toArray (50 elements) 4,406 → 4,673 +6.1% 5,226 → 5,583 +6.8%
take(10) + toArray (50 element source) 26,078 → 27,577 +5.7% 27,947 → 29,144 +4.3%
drop(40) + toArray (50 element source) 6,260 → 6,612 +5.6% 7,883 → 8,401 +6.6%
chained map + filter + take (100 element source) 8,243 → 8,734 +6.0% 9,382 → 9,642 +2.8%
some + every (50 elements) 2,589 → 2,713 +4.8% 3,358 → 3,346 -0.3%
find (50 elements) 5,654 → 5,948 +5.2% 6,872 → 6,806 -1.0%
array.values().map().filter().toArray() 8,792 → 9,401 +6.9% 9,637 → 9,603 -0.4%
array.values().take(5).toArray() 202,686 → 223,316 🟢 +10.2% 155,073 → 145,468 -6.2%
array.values().drop(45).toArray() 189,768 → 208,947 🟢 +10.1% 141,128 → 146,926 +4.1%
map.entries() chained helpers 10,519 → 11,215 +6.6% 5,262 → 5,322 +1.1%
set.values() chained helpers 18,509 → 19,297 +4.3% 20,166 → 20,760 +2.9%
string iterator map + toArray 13,379 → 14,019 +4.8% 21,524 → 21,530 +0.0%
json.js — Interp: 🟢 1, 19 unch. · avg +3.7% · Bytecode: 🔴 1, 19 unch. · avg -0.0%
Benchmark Interpreted Δ Bytecode Δ
parse simple object 165,485 → 171,930 +3.9% 139,632 → 147,897 +5.9%
parse nested object 103,682 → 106,902 +3.1% 87,670 → 89,177 +1.7%
parse array of objects 54,238 → 56,765 +4.7% 52,507 → 52,320 -0.4%
parse large flat object 48,866 → 49,393 +1.1% 46,177 → 46,788 +1.3%
parse mixed types 69,296 → 72,395 +4.5% 67,792 → 65,886 -2.8%
stringify simple object 193,976 → 199,106 +2.6% 162,717 → 157,935 -2.9%
stringify nested object 107,097 → 115,023 🟢 +7.4% 88,778 → 85,787 -3.4%
stringify array of objects 59,133 → 62,738 +6.1% 54,272 → 53,137 -2.1%
stringify mixed types 83,991 → 88,124 +4.9% 74,745 → 69,211 🔴 -7.4%
reviver doubles numbers 43,866 → 45,322 +3.3% 45,974 → 45,251 -1.6%
reviver filters properties 36,844 → 38,361 +4.1% 46,039 → 46,687 +1.4%
reviver on nested object 48,886 → 51,193 +4.7% 52,646 → 52,517 -0.2%
reviver on array 28,912 → 29,346 +1.5% 29,696 → 29,801 +0.4%
replacer function doubles numbers 46,660 → 48,100 +3.1% 52,750 → 52,937 +0.4%
replacer function excludes properties 58,249 → 59,520 +2.2% 61,075 → 62,791 +2.8%
array replacer (allowlist) 111,218 → 115,905 +4.2% 97,979 → 98,811 +0.8%
stringify with 2-space indent 94,117 → 97,673 +3.8% 78,834 → 81,317 +3.2%
stringify with tab indent 94,428 → 97,265 +3.0% 79,641 → 81,362 +2.2%
parse then stringify 53,160 → 54,942 +3.4% 51,593 → 51,607 +0.0%
stringify then parse 31,622 → 32,399 +2.5% 30,931 → 30,859 -0.2%
jsx.jsx — Interp: 🟢 1, 20 unch. · avg +4.9% · Bytecode: 21 unch. · avg +2.4%
Benchmark Interpreted Δ Bytecode Δ
simple element 197,025 → 206,793 +5.0% 605,346 → 624,992 +3.2%
self-closing element 205,553 → 214,886 +4.5% 635,877 → 666,774 +4.9%
element with string attribute 165,085 → 174,618 +5.8% 442,393 → 451,178 +2.0%
element with multiple attributes 143,704 → 150,053 +4.4% 384,981 → 397,355 +3.2%
element with expression attribute 157,327 → 166,541 +5.9% 427,678 → 436,255 +2.0%
text child 200,218 → 206,912 +3.3% 608,325 → 640,881 +5.4%
expression child 195,291 → 202,887 +3.9% 607,063 → 610,191 +0.5%
mixed text and expression 184,034 → 195,057 +6.0% 553,482 → 570,749 +3.1%
nested elements (3 levels) 74,180 → 77,149 +4.0% 242,235 → 246,028 +1.6%
sibling children 55,637 → 57,100 +2.6% 180,272 → 187,485 +4.0%
component element 139,209 → 148,426 +6.6% 419,867 → 424,312 +1.1%
component with children 86,877 → 89,604 +3.1% 269,652 → 274,362 +1.7%
dotted component 115,823 → 123,318 +6.5% 307,709 → 310,491 +0.9%
empty fragment 205,976 → 218,480 +6.1% 639,262 → 666,250 +4.2%
fragment with children 54,414 → 57,074 +4.9% 181,754 → 183,381 +0.9%
spread attributes 104,404 → 109,945 +5.3% 107,496 → 107,679 +0.2%
spread with overrides 91,515 → 95,474 +4.3% 80,665 → 82,120 +1.8%
shorthand props 152,216 → 159,819 +5.0% 406,761 → 423,625 +4.1%
nav bar structure 24,990 → 26,979 🟢 +8.0% 78,081 → 79,277 +1.5%
card component tree 29,988 → 30,765 +2.6% 84,717 → 85,571 +1.0%
10 list items via Array.from 13,709 → 14,477 +5.6% 23,116 → 23,730 +2.7%
numbers.js — Interp: 11 unch. · avg +2.2% · Bytecode: 🟢 1, 10 unch. · avg +0.9%
Benchmark Interpreted Δ Bytecode Δ
integer arithmetic 565,026 → 525,727 -7.0% 1,415,209 → 1,534,647 🟢 +8.4%
floating point arithmetic 570,650 → 589,851 +3.4% 1,703,496 → 1,675,065 -1.7%
number coercion 181,026 → 190,082 +5.0% 136,645 → 138,290 +1.2%
toFixed 99,498 → 101,903 +2.4% 210,827 → 212,458 +0.8%
toString 151,738 → 156,706 +3.3% 688,101 → 689,562 +0.2%
valueOf 214,218 → 224,099 +4.6% 947,730 → 916,146 -3.3%
toPrecision 138,875 → 145,397 +4.7% 401,864 → 404,265 +0.6%
Number.isNaN 287,739 → 293,758 +2.1% 172,909 → 177,186 +2.5%
Number.isFinite 286,785 → 285,463 -0.5% 170,646 → 170,540 -0.1%
Number.isInteger 277,273 → 278,484 +0.4% 186,714 → 183,356 -1.8%
Number.parseInt and parseFloat 233,096 → 245,362 +5.3% 152,970 → 157,227 +2.8%
objects.js — Interp: 🟢 1, 6 unch. · avg +5.5% · Bytecode: 🟢 1, 6 unch. · avg +3.9%
Benchmark Interpreted Δ Bytecode Δ
create simple object 459,785 → 473,727 +3.0% 817,028 → 846,158 +3.6%
create nested object 210,981 → 224,358 +6.3% 356,929 → 367,926 +3.1%
create 50 objects via Array.from 8,904 → 9,423 +5.8% 8,324 → 8,465 +1.7%
property read 612,233 → 627,553 +2.5% 667,680 → 655,795 -1.8%
Object.keys 282,082 → 297,325 +5.4% 205,089 → 213,481 +4.1%
Object.entries 97,841 → 109,343 🟢 +11.8% 63,293 → 66,476 +5.0%
spread operator 185,021 → 191,497 +3.5% 178,075 → 198,508 🟢 +11.5%
promises.js — Interp: 🟢 12 · avg +11.0% · Bytecode: 🟢 1, 11 unch. · avg +1.9%
Benchmark Interpreted Δ Bytecode Δ
Promise.resolve(value) 534,648 → 580,648 🟢 +8.6% 345,981 → 355,236 +2.7%
new Promise(resolve => resolve(value)) 181,775 → 203,276 🟢 +11.8% 167,776 → 169,678 +1.1%
Promise.reject(reason) 521,616 → 566,023 🟢 +8.5% 344,290 → 347,643 +1.0%
resolve + then (1 handler) 162,375 → 176,222 🟢 +8.5% 148,724 → 158,716 +6.7%
resolve + then chain (3 deep) 62,610 → 69,132 🟢 +10.4% 69,875 → 70,168 +0.4%
resolve + then chain (10 deep) 19,888 → 22,306 🟢 +12.2% 23,712 → 22,206 -6.4%
reject + catch + then 89,403 → 99,077 🟢 +10.8% 87,240 → 93,514 🟢 +7.2%
resolve + finally + then 78,877 → 87,270 🟢 +10.6% 76,892 → 76,902 +0.0%
Promise.all (5 resolved) 29,979 → 34,089 🟢 +13.7% 26,793 → 26,938 +0.5%
Promise.race (5 resolved) 31,810 → 35,910 🟢 +12.9% 28,170 → 28,573 +1.4%
Promise.allSettled (5 mixed) 25,378 → 28,229 🟢 +11.2% 22,043 → 23,458 +6.4%
Promise.any (5 mixed) 29,805 → 33,740 🟢 +13.2% 26,450 → 26,798 +1.3%
strings.js — Interp: 🔴 1, 10 unch. · avg +2.7% · Bytecode: 🟢 2, 9 unch. · avg +2.3%
Benchmark Interpreted Δ Bytecode Δ
string concatenation 415,547 → 413,655 -0.5% 429,996 → 428,276 -0.4%
template literal 700,755 → 640,100 🔴 -8.7% 651,790 → 704,492 🟢 +8.1%
string repeat 394,779 → 410,886 +4.1% 953,332 → 990,507 +3.9%
split and join 133,762 → 136,850 +2.3% 310,333 → 322,407 +3.9%
indexOf and includes 161,214 → 169,873 +5.4% 602,735 → 605,301 +0.4%
toUpperCase and toLowerCase 244,261 → 254,388 +4.1% 623,424 → 710,207 🟢 +13.9%
slice and substring 152,705 → 162,184 +6.2% 674,670 → 660,927 -2.0%
trim operations 183,345 → 188,378 +2.7% 743,080 → 746,297 +0.4%
replace and replaceAll 202,365 → 211,487 +4.5% 668,675 → 663,836 -0.7%
startsWith and endsWith 129,931 → 136,711 +5.2% 512,834 → 502,075 -2.1%
padStart and padEnd 191,810 → 200,234 +4.4% 579,858 → 581,845 +0.3%
typed-arrays.js — Interp: 🟢 2, 20 unch. · avg +3.7% · Bytecode: 🟢 2, 🔴 1, 19 unch. · avg +0.3%
Benchmark Interpreted Δ Bytecode Δ
new Int32Array(0) 311,345 → 333,988 🟢 +7.3% 128,971 → 129,048 +0.1%
new Int32Array(100) 288,309 → 307,709 +6.7% 123,596 → 124,549 +0.8%
new Int32Array(1000) 169,338 → 177,923 +5.1% 65,577 → 57,935 🔴 -11.7%
new Float64Array(100) 259,755 → 276,797 +6.6% 110,870 → 107,321 -3.2%
Int32Array.from([...]) 179,577 → 184,149 +2.5% 155,138 → 153,236 -1.2%
Int32Array.of(1, 2, 3, 4, 5) 308,565 → 319,460 +3.5% 234,078 → 242,991 +3.8%
sequential write 100 elements 3,625 → 3,712 +2.4% 13,213 → 12,925 -2.2%
sequential read 100 elements 3,730 → 3,814 +2.3% 10,228 → 10,173 -0.5%
Float64Array write 100 elements 3,372 → 3,417 +1.3% 12,534 → 12,144 -3.1%
fill(42) 45,601 → 45,905 +0.7% 43,936 → 43,954 +0.0%
slice() 204,057 → 211,834 +3.8% 186,189 → 184,633 -0.8%
map(x => x * 2) 8,199 → 8,486 +3.5% 7,459 → 7,801 +4.6%
filter(x => x > 50) 8,292 → 8,596 +3.7% 7,859 → 7,988 +1.6%
reduce (sum) 7,880 → 8,177 +3.8% 6,744 → 6,954 +3.1%
sort() 177,194 → 172,286 -2.8% 151,059 → 155,240 +2.8%
indexOf() 450,459 → 461,409 +2.4% 361,542 → 354,664 -1.9%
reverse() 332,846 → 341,527 +2.6% 271,068 → 268,327 -1.0%
create view over existing buffer 389,343 → 415,980 +6.8% 138,274 → 137,664 -0.4%
subarray() 449,450 → 483,421 🟢 +7.6% 352,010 → 350,014 -0.6%
set() from array 587,777 → 606,744 +3.2% 233,585 → 250,844 🟢 +7.4%
for-of loop 5,101 → 5,243 +2.8% 18,429 → 17,635 -4.3%
spread into array 17,648 → 18,792 +6.5% 46,499 → 53,163 🟢 +14.3%

Measured on ubuntu-latest x64. Changes within ±7% are considered insignificant.

The typed local opcodes (56-65) are no longer emitted by the compiler
and had no case arms in the VM. Remove the dead enum entries entirely
and bump SOUFFLE_FORMAT_VERSION from 3 to 4. Update documentation to
reflect that local access always uses OP_GET_LOCAL/OP_SET_LOCAL.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
souffle/Souffle.Bytecode.pas (1)

98-106: Consider adding a comment documenting the removed typed local opcodes (56-65).

Similar to the comment on line 104 about opcode 68, a brief note about the removed typed local opcodes would help future maintainers understand the gap between OP_CONCAT = 55 and OP_NEW_BLUEPRINT = 66.

📝 Optional documentation comment
     // ── Core: String ──
     OP_CONCAT        = 55,  // ABC   R[A] := String(R[B]) + String(R[C])
 
+    // opcodes 56-65 removed: were OP_GET_LOCAL_INT through OP_SET_LOCAL_REF (typed local opcodes)
+    // use OP_GET_LOCAL/OP_SET_LOCAL instead — type optimization is via arithmetic/compare opcodes
+
     // ── Core: Blueprint ──
     OP_NEW_BLUEPRINT = 66,  // ABx   R[A] := new Blueprint(name=Constants[Bx])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@souffle/Souffle.Bytecode.pas` around lines 98 - 106, Add a brief comment
between OP_CONCAT and OP_NEW_BLUEPRINT in Souffle.Bytecode.pas documenting that
opcode slots 56–65 were removed (the typed-local opcodes) and why/what replaced
them—mirroring the style of the existing comment for the removed
OP_BLUEPRINT_METHOD (68); reference the surrounding symbols OP_CONCAT,
OP_NEW_BLUEPRINT and the removed range 56-65 so future maintainers understand
the numeric gap.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@souffle/Souffle.Bytecode.pas`:
- Around line 98-106: Add a brief comment between OP_CONCAT and OP_NEW_BLUEPRINT
in Souffle.Bytecode.pas documenting that opcode slots 56–65 were removed (the
typed-local opcodes) and why/what replaced them—mirroring the style of the
existing comment for the removed OP_BLUEPRINT_METHOD (68); reference the
surrounding symbols OP_CONCAT, OP_NEW_BLUEPRINT and the removed range 56-65 so
future maintainers understand the numeric gap.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4ac18b4-dba1-46fb-8d54-825d0a8a1239

📥 Commits

Reviewing files that changed from the base of the PR and between 8963103 and 8afab47.

📒 Files selected for processing (2)
  • docs/souffle-vm.md
  • souffle/Souffle.Bytecode.pas

@frostney frostney merged commit ac221f2 into main Mar 13, 2026
9 checks passed
@frostney frostney deleted the opt/remove-typed-locals branch March 13, 2026 08:06
frostney added a commit that referenced this pull request Mar 18, 2026
- Rewrite optimization-log.md with correct PR numbers for all 22 PRs
  since #73 (previous version had multiple wrong PR-to-description
  mappings)
- Add missing PRs: #73, #74, #75, #76, #77, #79, #80, #81, #82, #83,
  #85, #88, #89, plus infrastructure PRs #90, #92, #98
- Fix GC section: Collect-after-each-file is deliberate, not a
  workaround; any GC revisit needs fresh analysis
- Fix minor items: link to actual issues #103 and #104, correct naming
  convention context (shared units should NOT use Goccia.* prefix)
- Create docs/decision-log.md with 1-3 sentence summaries linking to
  detailed documentation
- Link fpc-generics-performance.md and fpc-dispatch-performance.md
  from AGENTS.md, code-style.md, design-decisions.md, and
  optimization-log.md

Made-with: Cursor
frostney added a commit that referenced this pull request Mar 18, 2026
…105)

* Convert spike PDFs to Markdown, update VM docs, add optimization log

- Convert all 4 PDF spike documents (string performance, hashmap
  performance, generics performance, dispatch performance) to Markdown
  for better AI agent parsing
- Fix stale TSouffleValue documentation: 26 bytes → 16 bytes,
  string[23] → string[13] across souffle-vm.md, design-decisions.md
- Update test count to 3,501 across all docs (AGENTS.md, README.md,
  souffle-vm.md, design-decisions.md)
- Add svkString to TSouffleValue kinds list in AGENTS.md
- Add THashMap optimization details to AGENTS.md component table
- Add docs/optimization-log.md with full VM optimization history,
  experiment results, remaining work, and key insights for fresh
  context pickup
- Add hashmap spike update note documenting TScopeMap removal,
  TOrderedStringMap refactoring, and THashMap hash optimization

Made-with: Cursor

* Fix optimization log PR numbers, add decision log, link all spikes

- Rewrite optimization-log.md with correct PR numbers for all 22 PRs
  since #73 (previous version had multiple wrong PR-to-description
  mappings)
- Add missing PRs: #73, #74, #75, #76, #77, #79, #80, #81, #82, #83,
  #85, #88, #89, plus infrastructure PRs #90, #92, #98
- Fix GC section: Collect-after-each-file is deliberate, not a
  workaround; any GC revisit needs fresh analysis
- Fix minor items: link to actual issues #103 and #104, correct naming
  convention context (shared units should NOT use Goccia.* prefix)
- Create docs/decision-log.md with 1-3 sentence summaries linking to
  detailed documentation
- Link fpc-generics-performance.md and fpc-dispatch-performance.md
  from AGENTS.md, code-style.md, design-decisions.md, and
  optimization-log.md

Made-with: Cursor

* Address PR review comments: fix ratios, bytes/chars, stale refs

- Fix generics spike ratio values (arithmetic errors from original PDF)
- Change "13 characters" to "13 bytes" in souffle-vm.md (FPC string[N]
  is byte-counted), add UTF-8 note
- Mark TScopeMap as removed/historical in hashmap spike document
- Fix Key Insight #4 PR reference (was #88 async/await, not boolean)
- Remove stale GOCCIA_EXT_EVAL_CLASS / FPendingClasses / decorator
  bridge references from AGENTS.md, souffle-vm.md, design-decisions.md
  (decorators are now compiled natively via BEGIN/APPLY/FINISH opcodes)
- Fix "44 second" → "44-second" hyphenation in string spike

Made-with: Cursor

* Expand TScopeMap history in hashmap spike with full timeline

The spike originally recommended TScopeMap for scope bindings based on
micro-benchmarks at N≤10. After implementation (PR #66), real-world
profiling showed the assumption about scope sizes was incorrect — global
and bridged scopes far exceed 20 bindings, causing a 2.7× regression.
The update section now tells the complete timeline: original
recommendation → implementation → profiling discovery → revert.

Made-with: Cursor

* Add grill-me interview skill

Introduce a new 'grill-me' agent skill that relentlessly interviews users about a plan or design, exploring each branch of the decision tree until a shared understanding is reached and dependencies are resolved. The skill prefers to inspect the codebase when that can answer a question. Updated skills lock to include the new skill.
@frostney frostney added the internal Refactoring, CI, tooling, cleanup label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Refactoring, CI, tooling, cleanup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant