From eda0bc063c5ced4447aaed8d5080fd6ca72dd441 Mon Sep 17 00:00:00 2001 From: Matthew Schneider Date: Thu, 15 Oct 2026 19:16:15 -0400 Subject: [PATCH] Support frame variables in the SPC without deopt --- src/engine/compiler/SinglePassCompiler.v3 | 17 ++- src/engine/native/NativeFrames.v3 | 1 - test/unittest/ExeProbeTest.v3 | 36 +++++- .../x86-64-linux/X86_64LinuxExeTest.v3 | 112 +++++++++++++++++- 4 files changed, 159 insertions(+), 7 deletions(-) diff --git a/src/engine/compiler/SinglePassCompiler.v3 b/src/engine/compiler/SinglePassCompiler.v3 index 2cc16eb49..9939df77a 100644 --- a/src/engine/compiler/SinglePassCompiler.v3 +++ b/src/engine/compiler/SinglePassCompiler.v3 @@ -178,10 +178,7 @@ class SinglePassCompiler(xenv: SpcExecEnv, masm: MacroAssembler, regAlloc: RegAl // Visit all local declarations. it.dispatchLocalDecls(this); - - if (!FeatureDisable.frameVariables && func.frame_var_tags != null) { - for (t in func.frame_var_tags) state.push(tagToKindFlags(t) | IS_CONST, NO_REG, 0); - } + pushFrameVars(func); // Emit function entry probe, if any. if (!FeatureDisable.entryProbes && func.entry_probed) { @@ -894,6 +891,7 @@ class SinglePassCompiler(xenv: SpcExecEnv, masm: MacroAssembler, regAlloc: RegAl // Allocate callee's non-parameter locals it.dispatchLocalDecls(this); + pushFrameVars(callee_func); // Emit function entry probe, if any. if (whamm == null && !FeatureDisable.entryProbes && func.entry_probed) { @@ -921,6 +919,17 @@ class SinglePassCompiler(xenv: SpcExecEnv, masm: MacroAssembler, regAlloc: RegAl // Restore caller spc context popSpcFrame(); } + // Allocates frame variables above the function locals. + // XXX does not support dynamically changing frame_var_tags in a function + def pushFrameVars(func: FuncDecl) { + if (FeatureDisable.frameVariables || func.frame_var_tags == null) return; + for (t in func.frame_var_tags) { + def slot = state.sp; + masm.emit_mov_m_q(masm.slotAddr(slot), 0, 0); + if (masm.valuerep.tagged) masm.emit_mov_m_l(masm.tagAddr(slot), t); + state.push(tagToKindFlags(t) | IS_STORED | TAG_STORED, NO_REG, 0); + } + } def emitCallToReg(sig: SigDecl, func_reg: Reg, vsp_reg: Reg, tmp: Reg, checkHostCall: bool, tailCall: bool) { var retpt = masm.newLabel(it.pc), wasmcall_label = masm.newLabel(it.pc); // Handle the current stack state. diff --git a/src/engine/native/NativeFrames.v3 b/src/engine/native/NativeFrames.v3 index 5ff51702e..835cce2ea 100644 --- a/src/engine/native/NativeFrames.v3 +++ b/src/engine/native/NativeFrames.v3 @@ -355,6 +355,5 @@ private class NativeFrameWriter extends FrameWriter { accessor.checkNotUnwound(); accessor.checkFrameVarBounds(i); accessor.stack.overwriteValue(accessor.handle.vfp(), i + accessor.decl.num_locals, v); - if (accessor.isSpc()) accessor.deoptToInterpreter(); } } diff --git a/test/unittest/ExeProbeTest.v3 b/test/unittest/ExeProbeTest.v3 index 1bd64ddbb..c5b5ff847 100644 --- a/test/unittest/ExeProbeTest.v3 +++ b/test/unittest/ExeProbeTest.v3 @@ -9,7 +9,8 @@ def X_ = TestTiers.addTests([ ("entry_probe0", test_entry_probe0), ("entry_probe1", test_entry_probe1), ("probe_write_local", test_probe_write_local), - ("gc_after_deopt", test_gc_after_deopt) + ("gc_after_deopt", test_gc_after_deopt), + ("frame_var_legacy_catch", test_frame_var_legacy_catch) ]); def test_force_gc_callback(args: Range) -> HostResult { @@ -209,3 +210,36 @@ def test_gc_after_deopt(i: ExeTester) { Instrumentation.insertLocalProbe(i.module, i.func.func_index, 3, DeoptThenGcProbe.new()); i.args_i(0).assert2_i(22); } + +// Reads every frame variable of the frame it fires on. +private class FrameVarReader(num: int) extends Probe { + def got = Array.new(num); + def fire(dynamicLoc: DynamicLoc) -> ProbeAction { + def accessor = dynamicLoc.frame.getFrameAccessor(); + for (i < num) got[i] = accessor.getFrameVar(i); + return ProbeAction.Continue; + } +} + +// A legacy catch keeps the caught exception in the frame variable the validator gives it. +def test_frame_var_legacy_catch(i: ExeTester) { + i.extensions |= Extension.LEGACY_EH; + def sig = i.newSig(SigCache.arr_v, SigCache.arr_v); + def tag = TagDecl.new(sig.heaptype_index); + i.module.addImport("", "t", tag); + i.imports = [Tag.new(sig, tag)]; + i.sig(SigCache.v_v); + i.codev([ + u8.!(Opcode.TRY.code), BpTypeCode.EmptyBlock.code, + u8.!(Opcode.THROW.code), byte.!(tag.tag_index), + u8.!(Opcode.CATCH_ALL.code), + u8.!(Opcode.NOP.code), + u8.!(Opcode.END.code)]); + def fv = FrameVarReader.new(1); + Instrumentation.insertLocalProbe(i.module, i.func.func_index, 6, fv); + i.noargs().assert2_none(); + def got = fv.got[0]; + if (!Value.Ref.?(got) || !Exception.?(Value.Ref.!(got).val)) { + i.t.fail1("expected the caught exception in frame var 0, got %q", Values.render(got, _)); + } +} diff --git a/test/unittest/x86-64-linux/X86_64LinuxExeTest.v3 b/test/unittest/x86-64-linux/X86_64LinuxExeTest.v3 index 259c4002d..a5aaa5e65 100644 --- a/test/unittest/x86-64-linux/X86_64LinuxExeTest.v3 +++ b/test/unittest/x86-64-linux/X86_64LinuxExeTest.v3 @@ -16,7 +16,10 @@ component X86_64LinuxExeTest { ("load_oob0", test_load_oob0), ("call1005", test_call1005), // intended to trigger OSR ("osr_accessor", test_osr_accessor), - ("recompile_after_probe", test_recompile_after_probe) + ("recompile_after_probe", test_recompile_after_probe), + ("frame_var_write", test_frame_var_write), + ("inlined_frame_var_write", test_inlined_frame_var_write), + ("frame_var_deopt", test_frame_var_deopt) ]) { // register strategy/test pair UnitTests.registerT(strategy.0, test.0, newTester, test.1); @@ -130,3 +133,110 @@ def test_recompile_after_probe(i: ExeTester) { Instrumentation.insertLocalProbe(i.module, i.func.func_index, 10, inserter); i.args_i(5).assert2_i(66); } + +// Checks that frame variable 0 holds the number of earlier firings, then increments it. Also +// records whether a write turned a compiled frame into an interpreter frame. +private class FrameVarCounter extends Probe { + var fires: int; + var compiled_fires: int; + var wrong: bool; + var deopted: bool; + def fire(dynamicLoc: DynamicLoc) -> ProbeAction { + def accessor = NativeFrameAccessor.!(dynamicLoc.frame.getFrameAccessor()); + def compiled = accessor.isSpc(); + if (compiled) compiled_fires++; + if (Values.unbox_i(accessor.getFrameVar(0)) != fires) wrong = true; + accessor.getWriter().setFrameVar(0, Values.box_i(++fires)); + if (compiled && !accessor.isSpc()) deopted = true; + return ProbeAction.Continue; + } +} + +// A frame variable keeps every value written to it for the whole activation, in compiled code +// without a deopt, and across the tier-up part way through this loop under int:dyn. +def test_frame_var_write(i: ExeTester) { + def p = FrameVarCounter.new(); + i.sig(SigCache.i_i); + i.codev([ + u8.!(Opcode.LOOP.code), NONE, + u8.!(Opcode.LOCAL_GET.code), 0, + u8.!(Opcode.I32_CONST.code), 1, + u8.!(Opcode.I32_SUB.code), + u8.!(Opcode.LOCAL_TEE.code), 0, + u8.!(Opcode.BR_IF.code), 0, + u8.!(Opcode.END.code), + u8.!(Opcode.I32_CONST.code), 7 + ]); + i.func.frame_var_tags = [BpTypeCode.I32.code]; + Instrumentation.insertLocalProbe(i.module, i.func.func_index, 3, p); + + i.args_i(20).assert2_i(7); + i.t.assert_eq(20, p.fires); + i.t.assert_eq(false, p.wrong); + i.t.assert_eq(false, p.deopted); + i.t.assert_eq(true, p.compiled_fires > 0); +} + +// An inlined callee's frame variable starts at 0 and is written without a deopt or clobbering the +// operand above it. Here g(x) = x is probed at its nop, with x live. +def test_inlined_frame_var_write(i: ExeTester) { + def depth_before = SpcTuning.maxInlineDepth; + SpcTuning.maxInlineDepth = 1; + def g = i.newFunction(SigCache.i_i, [u8.!(Opcode.LOCAL_GET.code), 0, u8.!(Opcode.NOP.code)]); + g.frame_var_tags = [BpTypeCode.I32.code]; + i.sig(SigCache.i_i); + i.code([u8.!(Opcode.LOCAL_GET.code), 0, + u8.!(Opcode.CALL.code), byte.!(g.func_index)]); + def p = FrameVarCounter.new(); + Instrumentation.insertLocalProbe(i.module, g.func_index, 3, p); + i.args_i(5).assert2_i(5); + SpcTuning.maxInlineDepth = depth_before; + i.t.assert_eq(1, p.fires); + i.t.assert_eq(false, p.wrong); + i.t.assert_eq(false, p.deopted); +} + +// Writes frame variable 0 and then local 0, which deopts a compiled frame. +private class FrameVarThenLocalWriter extends Probe { + def fire(dynamicLoc: DynamicLoc) -> ProbeAction { + def writer = dynamicLoc.frame.getFrameAccessor().getWriter(); + writer.setFrameVar(0, Values.box_i(42)); + writer.setLocal(0, Values.box_i(9)); + return ProbeAction.Continue; + } +} +// Records whether frame variable 0's slot holds the I32 42 in every byte the interpreter writes, +// which are an 8-byte tag followed by the value zero-extended to 16 bytes. +private class FrameVarSlotChecker extends Probe { + var same: bool; + def fire(dynamicLoc: DynamicLoc) -> ProbeAction { + def accessor = NativeFrameAccessor.!(dynamicLoc.frame.getFrameAccessor()); + def rep = Target.tagging; + def tp = accessor.handle.vfp() + accessor.decl.num_locals * rep.slot_size; + def vp = tp + rep.tag_size; + same = tp.load() == u64.view(BpTypeCode.I32.code) && vp.load() == 42; + if (rep.value_size > 8) same = same && (vp + 8).load() == 0; + return ProbeAction.Continue; + } +} + +// A frame variable written by compiled code is laid out as the interpreter lays it out, when the +// interpreter reads it after a deopt. {dirty} first leaves a v128 in its slot. +def test_frame_var_deopt(i: ExeTester) { + def dirty_sig = i.newSig([ValueType.I32, ValueType.V128], SigCache.arr_v); + def dirty = i.newFunction(dirty_sig, []); + def f = i.newFunction(SigCache.i_i, [u8.!(Opcode.NOP.code), u8.!(Opcode.NOP.code), u8.!(Opcode.LOCAL_GET.code), 0]); + f.frame_var_tags = [BpTypeCode.I32.code]; + def writer = FrameVarThenLocalWriter.new(); + def checker = FrameVarSlotChecker.new(); + Instrumentation.insertLocalProbe(i.module, f.func_index, 1, writer); + Instrumentation.insertLocalProbe(i.module, f.func_index, 2, checker); + i.sig(SigCache.v_i); + i.code([u8.!(Opcode.I32_CONST.code), 1, + Opcode.V128_CONST.prefix, u8.!(Opcode.V128_CONST.code), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + u8.!(Opcode.CALL.code), byte.!(dirty.func_index), + u8.!(Opcode.I32_CONST.code), 5, + u8.!(Opcode.CALL.code), byte.!(f.func_index)]); + i.noargs().assert2_i(9); + i.t.assert_eq(true, checker.same); +}