Skip to content

Fix: preserve force_interpreter setting after invokeInterpreterOnly (#31)#32

Merged
chaploud merged 2 commits intoclojurewasm:mainfrom
jtakakura:fix/force-interpreter-persistence
Apr 14, 2026
Merged

Fix: preserve force_interpreter setting after invokeInterpreterOnly (#31)#32
chaploud merged 2 commits intoclojurewasm:mainfrom
jtakakura:fix/force-interpreter-persistence

Conversation

@jtakakura
Copy link
Copy Markdown
Contributor

This PR fixes a bug where the force_interpreter option was lost after calling invokeInterpreterOnly (see #31). Now, modules loaded with force_interpreter=true always run in interpreter mode, even after debug calls. Includes a test to verify the fix.

Closes #31

@jtakakura jtakakura force-pushed the fix/force-interpreter-persistence branch 7 times, most recently from e42843f to a1ed281 Compare April 11, 2026 08:50
@jtakakura jtakakura marked this pull request as ready for review April 11, 2026 09:05
@jtakakura jtakakura force-pushed the fix/force-interpreter-persistence branch from a1ed281 to c08d5e6 Compare April 11, 2026 23:18
…ly (clojurewasm#31)

The v1 fix for clojurewasm#31 added persistent module.{fuel,timeout_ms,force_interpreter}
fields and had invoke() apply them unconditionally on every call. That broke the
exact pattern clojurewasm#31's reproduction cites -- `module.vm.force_interpreter = true;
module.invoke(...)` -- because invoke() would overwrite vm.force_interpreter
with the default-null persistent field. The same silent override affected
vm.fuel (wiped to null) and the vm deadline (setDeadlineTimeoutMs(null) clears).

Guard every `self.* -> vm.*` write with `if (self.*) |v|`. A null persistent
field means "leave vm untouched", so callers who mutate self.vm.* directly (the
legacy pattern in zwasm 1.7.x and earlier) continue to work. Vm.reset() does
not touch force_interpreter/fuel/deadline_ns, so the caller's value survives
across invoke() boundaries as it used to.

invokeInterpreterOnly() saves the live vm.force_interpreter and restores it via
defer, instead of restoring the persistent self.force_interpreter which may not
match what the caller configured.

Changes:
- force_interpreter: `bool` -> `?bool` on WasmModule and loadCore
- guard self.* writes in loadCore start-function block, invoke(),
  and invokeInterpreterOnly()
- revert `self.wasi_ctx.?` -> `if (self.wasi_ctx) |*wc|` in loadWasiWith*
  (the unwrap lost a defensive check without resolving a real build error)
- drop redundant module.vm.setDeadlineTimeoutMs(ms) in cli.zig cmdRun (invoke
  now applies it via module.timeout_ms, guarded)
- expand the regression test to cover legacy-vm, new-module-field, and mixed
  patterns for fuel / timeout / force_interpreter

Mac merge gate (local):
  zig build test                                              PASS
  zig build test -Djit=false -Dcomponent=false -Dwat=false    PASS
  python3 test/spec/run_spec.py --build --summary             62263/62263
  bash test/c_api/run_ffi_test.sh --build                     68/68
  bash test/e2e/run_e2e.sh --convert --summary                796/796
  bash bench/run_bench.sh --quick                             no regression

Co-authored-by: Junji Takakura <j.takakura@gmail.com>
@chaploud
Copy link
Copy Markdown
Contributor

Thanks for the fix, @jtakakura 🙏 — the defer-reset in invokeInterpreterOnly was subtle and the fix direction (persistent module settings) is nice ergonomics.

I pushed b99f19b on top of your commit with maintainer-edit permission. Summary of the follow-up:

Regression the v1 fix introduced (caught during review — not blocking anything in the wild yet, since main hasn't shipped this):

The v1 invoke() unconditionally copied self.{fuel,force_interpreter,timeout_ms}self.vm.* on every call. That silently clobbered the exact pattern #31's reproduction steps describe:

module.vm.force_interpreter = true;   // legacy direct-vm pattern (zwasm 1.7.x)
module.invoke(...);                    // <- v1 overwrote vm.force_interpreter
                                       //    with self.force_interpreter (= false)

Same for module.vm.fuel = N (wiped to null) and for deadline (setDeadlineTimeoutMs(null) clears deadline_ns).

Fix approach (minimal delta on top of yours): flip force_interpreter to ?bool and guard every self.* → vm.* write with if (self.*) |v|. Null persistent field ⇒ "don't touch vm", so direct-vm callers keep working. Vm.reset() never clears these fields, so caller state survives across invoke() as it used to. invokeInterpreterOnly now saves the live vm.force_interpreter and restores via defer instead of restoring from the persistent field.

Also: reverted self.wasi_ctx.? back to if (self.wasi_ctx) |*wc| (the unwrap was a defensiveness downgrade without a real Zig 0.15.2 build error behind it), and dropped the now-redundant pre-invoke setDeadlineTimeoutMs in cli.zig.

Merge gate (Mac aarch64 + Ubuntu x86_64 via OrbStack, both):

  • zig build test
  • minimal build (-Djit=false -Dcomponent=false -Dwat=false) ✅
  • spec: 62263/62263 ✅
  • FFI: 68/68 ✅
  • e2e: 796/796 ✅
  • realworld: PASS=50 FAIL=0 CRASH=0 ✅
  • bench quick: no regression ✅

Will merge once CI is green. Keeping you as co-author on the follow-up commit.

@chaploud chaploud merged commit 742362e into clojurewasm:main Apr 14, 2026
5 checks passed
@jtakakura jtakakura deleted the fix/force-interpreter-persistence branch April 14, 2026 22:05
chaploud added a commit that referenced this pull request Apr 15, 2026
Establishes a reference point on main (201d13c, after PRs #32/#33/#35 merged)
for comparison against upcoming changes that touch the VM hot path — notably
PR #28's cancellation checks in consumeInstructionBudget.

Delta vs previous baseline w45-simd-loop-persist (2026-03-26, 1e406db):
major speedups in realworld C benchmarks (rw_c_matrix -82%, rw_c_math -69%,
rw_cpp_sort -28%) from unrelated interim commits; sub-ms regressions on
st_ackermann and st_nestedloop are at-or-below the noise floor and not
attributable to PR #32 (its invoke() change adds three null-branch guards
with no observable cost when persistent fields are unset, which is the
bench configuration).
@jtakakura
Copy link
Copy Markdown
Contributor Author

@chaploud Thank you for the detailed review and the follow-up commit.
I really appreciate the insights into backward compatibility with the legacy pattern. It’s a great learning experience for me.
I’m glad to be a co-author and looking forward to making more contributions to zwasm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

force_interpreter setting is lost after calling invokeInterpreterOnly

2 participants