Skip to content

Commit fbc3531

Browse files
perf: reserve each function's operand stack on entry (#59)
Third of a three-PR stack on `next` (`785be0e`): 1. #57 perf: grow the value stack out of line 2. #58 perf: inline the fused binop and compare helpers 3. #59 **perf: reserve each function's operand stack on entry** (this PR) This branch includes #58 and #57 below it; the last three commits are new here. The first PR moved stack growth out of the instruction handlers but kept a cold call to it, and that call still costs the handlers their register saves. This PR removes growth from `push` entirely: - The parser already tracks the operand stack per lane (`lane_counts` in `visit.rs`). It now also records the highest point each function body reaches, as `WasmFunction::max_stack`. - `enter_locals` reserves `locals + max_stack` in each lane when a function is entered, and grows a dynamic stack there if needed. It already grew for the locals at that point. - `Stack::push`, used by the instruction handlers, then traps at capacity instead of calling a growth path: a full stack inside a body means the limit. Typed guest operand handlers now use reserved capacity; host value marshaling can still grow. - Host argument and result marshaling through `push_dyn` retains `push_or_grow`, including host calls made by a guest. - Catch landing reservations now include tag payload lanes and optional exception references, including function-label catches and cross-frame throws. Self-calls preserve the actual value-stack overflow trap; the WAST exhaustion assertion accepts either internal stack reaching its limit first. GC field reads and exception payload injection use reserved no-growth pushes. Changes to know about: - **Public API**: `tinywasm_types::WasmFunction` gains a `pub max_stack: ValueCounts` field. - **Archive format**: the layout of `WasmFunction` changed, so `TWASM_VERSION` goes to `06` (the first format change since 0.11.0 shipped with `05`). - **Stack limits**: overflow is detected when a function is entered, not at the push that crosses the limit. A function whose deepest operand stack does not fit traps on entry, even if a particular call would not have reached that depth. Deep recursion runs out slightly earlier, by at most one function's operand stack. - `tests/value_stack_reservation.rs` runs a function with a deep operand stack in all three lanes, recursively, on dynamic stacks that start empty or tiny and on a fixed stack. It also checks that a limit smaller than the body's needs still traps with `ValueStackOverflow`. **iPhone 12 E-cores**, same setup as the first PR, with XCode 27 PMU template for low-level CPU stats. This PR against the second, which ittself is against `next`, and against an upper bound: the second PR plus a `push` with no growth path and no reservation, which behaves the same on fixed stacks (the default) but would trap on dynamic ones. | | cycles | instructions | rows faster | |---|---:|---:|---:| | this PR vs the second | **−2.8 %** | −1.9 % | 15/16 | | the whole stack vs `next` | **−8.0 %** | −7.2 % | 16/16 | | upper bound vs the second | −2.7 % | −2.4 % | 16/16 | | this PR vs the upper bound | −0.1 % | +0.5 % | 8/16 | The reservation keeps the whole upper-bound gain on the geomean. Its extra work on function entry shows up on the most call-heavy rows. The EH parser (exnref) is the one row that is slower than with the first two PRs: +1.3 %, consistently across reps. Against the upper bound, it is +2.3 %, and the tail-call FSM +1.2 %. A few rows come out about 3 % faster than the upper bound (sieve, xmrsplayer), which is most likely code layout rather than this change. Cycles per call on `next`, and every build as a ratio to it (median of 5 launches): | row | `next` Mcycles | first PR | first two | whole stack | upper bound | |---|---:|---:|---:|---:|---:| | fib(30) | 303.8 | 0.964 | 0.931 | 0.913 | 0.905 | | matmul relaxed-simd FMA | 8.17 | 0.960 | 0.951 | 0.889 | 0.879 | | audio DSP | 3089 | 0.960 | 0.926 | 0.881 | 0.888 | | call_indirect | 63.36 | 0.970 | 0.975 | 0.952 | 0.953 | | xmrsplayer | 45.52 | 0.943 | 0.946 | 0.909 | 0.938 | | vtable_poly4 | 128.5 | 0.984 | 0.961 | 0.937 | 0.934 | | graphql-validation (AssemblyScript) | 36.61 | 0.975 | 0.945 | 0.936 | 0.926 | | graphql-validation (Porffor) | 31.03 | 0.970 | 0.958 | 0.957 | 0.952 | | sieve (scalar) | 2.04 | 0.909 | 0.908 | 0.857 | 0.889 | | crc32 (scalar) | 14.75 | 0.915 | 0.904 | 0.866 | 0.869 | | convolution (scalar) | 40.93 | 0.935 | 0.933 | 0.882 | 0.887 | | bulk_memory (scalar) | 30.59 | 0.936 | 0.907 | 0.869 | 0.877 | | tail-call FSM | 14.59 | 0.978 | 0.967 | 0.953 | 0.942 | | EH parser (exnref) | 42.01 | 0.980 | 0.981 | 0.994 | 0.972 | | GC binary trees | 284.4 | 0.989 | 0.990 | 0.984 | 0.984 | | call_ref twin (call_indirect) | 55.03 | 0.983 | 0.970 | 0.962 | 0.954 | | **geomean** | | **0.959** | **0.947** | **0.920** | **0.921** | **Checks** on this branch: tinywasm's CI matrix , run on my local M4 MacBook: `cargo test --workspace` and `cargo test --workspace --examples` . I went the distance and made sure the whole suite passes with the default value stacks switched to dynamic stacks that start empty and grow to exactly each function's reservation (a local stress configuration, not in this PR), where a function whose recorded maximum undercounted its pushes would trap. Last, I did a coverage-driven fuzzer pass overnight while I slept, and didn't get any anomalous exceptions or state tainting. --------- Co-authored-by: Henry Gressmann <mail@henrygressmann.de>
1 parent 6b4dc3f commit fbc3531

12 files changed

Lines changed: 330 additions & 75 deletions

File tree

‎crates/cli/src/wast_runner.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,14 @@ impl WastRunner {
349349
);
350350
continue;
351351
};
352-
if !message.starts_with(trap.message()) && !trap.message().starts_with(message) {
352+
// The core test suite's "call stack exhausted" text denotes stack exhaustion,
353+
// not which internal stack reaches its configured limit first. Function-entry
354+
// operand reservation can exhaust a value lane before the call-frame stack.
355+
if !matches!(trap, tinywasm::Trap::CallStackOverflow | tinywasm::Trap::ValueStackOverflow) {
353356
test_group.add_result(
354357
&format!("AssertExhaustion({i})"),
355358
span.linecol_in(wast_raw),
356-
Err(anyhow!("expected trap: {}, got: {}", message, trap.message())),
359+
Err(anyhow!("expected stack exhaustion ({message}), got: {}", trap.message())),
357360
);
358361
continue;
359362
}

‎crates/parser/src/conversion.rs‎

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -198,34 +198,35 @@ pub(crate) fn convert_module_code(
198198
}
199199

200200
#[cfg(feature = "validate")]
201-
let (body, data, uses_local_memory, validator_allocs, reader_allocs) = match validator {
201+
let (body, data, uses_local_memory, max_stack, validator_allocs, reader_allocs) = match validator {
202202
Some(validator) => {
203-
let (body, data, uses_local_memory, validator_allocs, reader_allocs) = process_operators_and_validate(
204-
validator,
205-
func,
206-
(local_types, local_addr_map),
207-
metadata,
208-
context,
209-
reader_allocs,
210-
options,
211-
)?;
212-
(body, data, uses_local_memory, Some(validator_allocs), reader_allocs)
203+
let (body, data, uses_local_memory, max_stack, validator_allocs, reader_allocs) =
204+
process_operators_and_validate(
205+
validator,
206+
func,
207+
(local_types, local_addr_map),
208+
metadata,
209+
context,
210+
reader_allocs,
211+
options,
212+
)?;
213+
(body, data, uses_local_memory, max_stack, Some(validator_allocs), reader_allocs)
213214
}
214215
None => {
215-
let (body, data, uses_local_memory, reader_allocs) =
216+
let (body, data, uses_local_memory, max_stack, reader_allocs) =
216217
process_operators(func, (local_types, local_addr_map), metadata, context, reader_allocs, options)?;
217-
(body, data, uses_local_memory, None, reader_allocs)
218+
(body, data, uses_local_memory, max_stack, None, reader_allocs)
218219
}
219220
};
220221
#[cfg(not(feature = "validate"))]
221-
let (body, data, uses_local_memory, validator_allocs, reader_allocs) = {
222+
let (body, data, uses_local_memory, max_stack, validator_allocs, reader_allocs) = {
222223
let _ = validator;
223-
let (body, data, uses_local_memory, reader_allocs) =
224+
let (body, data, uses_local_memory, max_stack, reader_allocs) =
224225
process_operators(func, (local_types, local_addr_map), metadata, context, reader_allocs, options)?;
225-
(body, data, uses_local_memory, None, reader_allocs)
226+
(body, data, uses_local_memory, max_stack, None, reader_allocs)
226227
};
227228
Ok((
228-
FunctionCode { instructions: body, data: data.finish(), locals: local_counts, uses_local_memory },
229+
FunctionCode { instructions: body, data: data.finish(), locals: local_counts, max_stack, uses_local_memory },
229230
validator_allocs,
230231
reader_allocs,
231232
))

‎crates/parser/src/module.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) struct FunctionCode {
1414
pub instructions: Vec<Instruction>,
1515
pub data: WasmFunctionData,
1616
pub locals: ValueCounts,
17+
pub max_stack: ValueCounts,
1718
pub uses_local_memory: bool,
1819
}
1920

@@ -553,6 +554,7 @@ impl<'a> ModuleReader<'a> {
553554
locals: code.locals,
554555
params,
555556
results,
557+
max_stack: code.max_stack,
556558
}))
557559
})
558560
.collect::<Result<_>>()?;

‎crates/parser/src/visit.rs‎

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ pub(crate) struct FunctionBuilder<'a> {
185185
control_stack: Vec<ControlFrame<'a>>,
186186
operand_stack: Vec<ValueLane>,
187187
lane_counts: ValueCounts,
188+
max_lane_counts: ValueCounts,
188189
metadata: &'a ModuleMetadata,
189190
local_types: Vec<ValueLane>,
190191
local_addr_map: Vec<u16>,
@@ -223,6 +224,7 @@ impl<'a> FunctionBuilder<'a> {
223224
}],
224225
operand_stack: Vec::new(),
225226
lane_counts: ValueCounts::default(),
227+
max_lane_counts: ValueCounts::default(),
226228
uses_local_memory: false,
227229
}
228230
}
@@ -451,7 +453,7 @@ pub(crate) fn process_operators(
451453
context: FunctionLoweringContext,
452454
allocs: OperatorsReaderAllocations,
453455
options: &ParserOptions,
454-
) -> Result<(Vec<Instruction>, FunctionDataBuilder, bool, OperatorsReaderAllocations)> {
456+
) -> Result<(Vec<Instruction>, FunctionDataBuilder, bool, ValueCounts, OperatorsReaderAllocations)> {
455457
let (local_types, local_addr_map) = locals;
456458
let body_size = body.as_bytes().len();
457459
let reader = body.get_binary_reader_for_operators()?;
@@ -474,7 +476,7 @@ pub(crate) fn process_operators(
474476

475477
reader.finish()?;
476478
let instructions = builder.emitter.finish(&mut builder.data)?;
477-
Ok((instructions, builder.data, builder.uses_local_memory, reader.into_allocations()))
479+
Ok((instructions, builder.data, builder.uses_local_memory, builder.max_lane_counts, reader.into_allocations()))
478480
}
479481

480482
#[cfg(feature = "validate")]
@@ -486,7 +488,14 @@ pub(crate) fn process_operators_and_validate(
486488
context: FunctionLoweringContext,
487489
allocs: OperatorsReaderAllocations,
488490
options: &ParserOptions,
489-
) -> Result<(Vec<Instruction>, FunctionDataBuilder, bool, FuncValidatorAllocations, OperatorsReaderAllocations)> {
491+
) -> Result<(
492+
Vec<Instruction>,
493+
FunctionDataBuilder,
494+
bool,
495+
ValueCounts,
496+
FuncValidatorAllocations,
497+
OperatorsReaderAllocations,
498+
)> {
490499
let (local_types, local_addr_map) = locals;
491500
let body_size = body.as_bytes().len();
492501
let reader = body.get_binary_reader_for_operators()?;
@@ -509,7 +518,14 @@ pub(crate) fn process_operators_and_validate(
509518

510519
reader.finish()?;
511520
let instructions = builder.emitter.finish(&mut builder.data)?;
512-
Ok((instructions, builder.data, builder.uses_local_memory, validator.into_allocations(), reader.into_allocations()))
521+
Ok((
522+
instructions,
523+
builder.data,
524+
builder.uses_local_memory,
525+
builder.max_lane_counts,
526+
validator.into_allocations(),
527+
reader.into_allocations(),
528+
))
513529
}
514530

515531
impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
@@ -952,11 +968,23 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
952968
wasmparser::Catch::All { label } => (None, label, false),
953969
wasmparser::Catch::AllRef { label } => (None, label, true),
954970
};
955-
if let Some(tag) = tag {
956-
self.metadata.tag_signature(tag)?;
957-
}
958971
let target_idx = self.get_ctx_idx(depth)?;
959972
let target_base = self.control_stack[target_idx].base;
973+
// The runtime truncates to the target base, then injects the exception payload and
974+
// optional reference before executing this landing pad. These pushes do not pass
975+
// through the ordinary logical operand stack, but still need function-entry capacity.
976+
let mut landing_counts = target_base;
977+
if let Some(tag) = tag {
978+
for &lane in &self.metadata.tag_signature(tag)?.params {
979+
Self::increment_lane(&mut landing_counts, lane)?;
980+
}
981+
}
982+
if with_ref {
983+
Self::increment_lane(&mut landing_counts, ValueLane::S32)?;
984+
}
985+
self.max_lane_counts.c32 = self.max_lane_counts.c32.max(landing_counts.c32);
986+
self.max_lane_counts.c64 = self.max_lane_counts.c64.max(landing_counts.c64);
987+
self.max_lane_counts.c128 = self.max_lane_counts.c128.max(landing_counts.c128);
960988
let landing_label = self.emitter.new_label();
961989
self.emitter.bind(landing_label)?;
962990
self.emit_branch_jump_or_return(depth)?;
@@ -1525,17 +1553,29 @@ impl<'a> FunctionBuilder<'a> {
15251553
Ok((size, addr))
15261554
}
15271555

1528-
/// Pushes logical operands while maintaining the lane counts used by `DropKeep`.
1556+
/// Increments a physical lane count, rejecting functions too large for the encoded count.
1557+
fn increment_lane(counts: &mut ValueCounts, lane: ValueLane) -> Result<()> {
1558+
let count = match lane {
1559+
ValueLane::S32 => &mut counts.c32,
1560+
ValueLane::S64 => &mut counts.c64,
1561+
ValueLane::S128 => &mut counts.c128,
1562+
};
1563+
*count = count
1564+
.checked_add(1)
1565+
.ok_or_else(|| crate::ParseError::Other("logical operand lane count is too large".into()))?;
1566+
Ok(())
1567+
}
1568+
1569+
/// Pushes logical operands while maintaining the lane counts used by `DropKeep` and their
1570+
/// maximum, which the runtime reserves when it enters the function.
15291571
fn push_sizes(&mut self, sizes: &[ValueLane]) -> Result<()> {
15301572
for &size in sizes {
1531-
let count = match size {
1532-
ValueLane::S32 => &mut self.lane_counts.c32,
1533-
ValueLane::S64 => &mut self.lane_counts.c64,
1534-
ValueLane::S128 => &mut self.lane_counts.c128,
1535-
};
1536-
*count = count
1537-
.checked_add(1)
1538-
.ok_or_else(|| crate::ParseError::Other("logical operand lane count is too large".into()))?;
1573+
Self::increment_lane(&mut self.lane_counts, size)?;
1574+
match size {
1575+
ValueLane::S32 => self.max_lane_counts.c32 = self.max_lane_counts.c32.max(self.lane_counts.c32),
1576+
ValueLane::S64 => self.max_lane_counts.c64 = self.max_lane_counts.c64.max(self.lane_counts.c64),
1577+
ValueLane::S128 => self.max_lane_counts.c128 = self.max_lane_counts.c128.max(self.lane_counts.c128),
1578+
}
15391579
self.operand_stack.push(size);
15401580
}
15411581
Ok(())

‎crates/tinywasm/src/func/mod.rs‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,14 @@ impl Function {
157157
let host = store.state.funcs.host(self.addr()).func.clone();
158158
host.call_values(store, self.module_id, type_addr, params, results)
159159
} else {
160-
let (wasm_params, wasm_locals) = {
160+
let (wasm_params, wasm_locals, wasm_max_stack) = {
161161
let wasm = store.state.funcs.wasm(self.addr());
162-
let wasm_params = wasm.func.params;
163-
let wasm_locals = wasm.func.locals;
164-
(wasm_params, wasm_locals)
162+
(wasm.func.params, wasm.func.locals, wasm.func.max_stack)
165163
};
166164
store.push_wasm_values(params).inspect_err(|_| store.value_stack.truncate_to_base(value_stack_base))?;
167165
let locals_base = store
168166
.value_stack
169-
.enter_locals(&wasm_params, &wasm_locals)
167+
.enter_locals(&wasm_params, &wasm_locals, &wasm_max_stack)
170168
.inspect_err(|_| store.value_stack.truncate_to_base(value_stack_base))?;
171169
let callframe = CallFrame::new(self.addr(), locals_base, wasm_locals);
172170
InterpreterRuntime::exec(store, callframe, call_stack_base).inspect_err(|_| {
@@ -213,14 +211,14 @@ impl Function {
213211
};
214212
}
215213

216-
let (type_addr, wasm_params, wasm_locals) = {
214+
let (type_addr, wasm_params, wasm_locals, wasm_max_stack) = {
217215
let wasm = store.state.funcs.wasm(self.addr());
218-
(wasm.type_addr, wasm.func.params, wasm.func.locals)
216+
(wasm.type_addr, wasm.func.params, wasm.func.locals, wasm.func.max_stack)
219217
};
220218
store.push_typed_values::<false>(type_addr, params, value_stack_base)?;
221219
let locals_base = store
222220
.value_stack
223-
.enter_locals(&wasm_params, &wasm_locals)
221+
.enter_locals(&wasm_params, &wasm_locals, &wasm_max_stack)
224222
.inspect_err(|_| store.value_stack.truncate_to_base(value_stack_base))?;
225223
let callframe = CallFrame::new(self.addr(), locals_base, wasm_locals);
226224
InterpreterRuntime::exec(store, callframe, call_stack_base).inspect_err(|_| {

‎crates/tinywasm/src/func/resume.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ impl Function {
7979
return Ok(ExecState::Completed(Some(CallResult::Written)));
8080
}
8181

82-
let (wasm_params, wasm_locals) = {
82+
let (wasm_params, wasm_locals, wasm_max_stack) = {
8383
let wasm = store.state.funcs.wasm(self.addr());
84-
(wasm.func.params, wasm.func.locals)
84+
(wasm.func.params, wasm.func.locals, wasm.func.max_stack)
8585
};
8686

8787
store.call_stack.clear();
8888
store.value_stack.clear();
8989
store.push_wasm_values(params)?;
90-
let locals_base = store.value_stack.enter_locals(&wasm_params, &wasm_locals)?;
90+
let locals_base = store.value_stack.enter_locals(&wasm_params, &wasm_locals, &wasm_max_stack)?;
9191
let callframe = CallFrame::new(self.addr(), locals_base, wasm_locals);
9292

9393
Ok(ExecState::Running { callframe, root_func_addr: self.addr() })
@@ -224,9 +224,9 @@ impl<P: IntoWasmValues, R: FromWasmValues> FunctionTyped<P, R> {
224224
let execution = ExecutionInner { store, state: ExecState::Completed(None) };
225225
return Ok(FuncExecutionTyped { execution, result: Some(result) });
226226
}
227-
let (type_addr, wasm_params, wasm_locals) = {
227+
let (type_addr, wasm_params, wasm_locals, wasm_max_stack) = {
228228
let wasm = store.state.funcs.wasm(self.func.addr());
229-
(wasm.type_addr, wasm.func.params, wasm.func.locals)
229+
(wasm.type_addr, wasm.func.params, wasm.func.locals, wasm.func.max_stack)
230230
};
231231

232232
store.enter_execution()?;
@@ -236,7 +236,7 @@ impl<P: IntoWasmValues, R: FromWasmValues> FunctionTyped<P, R> {
236236
store.push_typed_values::<false>(type_addr, params.into_wasm_values(), StackBase::default())?;
237237
let locals_base = store
238238
.value_stack
239-
.enter_locals(&wasm_params, &wasm_locals)
239+
.enter_locals(&wasm_params, &wasm_locals, &wasm_max_stack)
240240
.inspect_err(|_| store.value_stack.clear())?;
241241
let callframe = CallFrame::new(self.func.addr(), locals_base, wasm_locals);
242242
Ok(ExecState::Running { callframe, root_func_addr: self.func.addr() })

‎crates/tinywasm/src/interpreter/executor/mod.rs‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ impl<'store> Executor<'store> {
617617
let Store { state, value_stack, .. } = self.store;
618618
let object = state.gc.get(exception).ok_or(Trap::InvalidReference)?;
619619
for value in object.values.iter().copied() {
620-
value_stack.push_dyn(value)?;
620+
value_stack.push_reserved(value)?;
621621
}
622622
}
623623
if with_ref {
@@ -703,9 +703,8 @@ impl<'store> Executor<'store> {
703703

704704
fn exec_call_self(&mut self, return_instr_ptr: usize) -> ExecResult<()> {
705705
self.charge_call_fuel(FUEL_COST_CALL_TOTAL);
706-
let Ok(locals_base) = self.store.value_stack.enter_locals(&self.func.params, &self.func.locals) else {
707-
return cold!(Err(Trap::CallStackOverflow.into()));
708-
};
706+
let locals_base =
707+
self.store.value_stack.enter_locals(&self.func.params, &self.func.locals, &self.func.max_stack)?;
709708
let new = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals);
710709
self.store.call_stack.push(core::mem::replace(&mut self.cf, new), return_instr_ptr)?;
711710
Ok(())
@@ -715,9 +714,8 @@ impl<'store> Executor<'store> {
715714
self.charge_call_fuel(FUEL_COST_CALL_TOTAL);
716715

717716
self.store.value_stack.truncate_keep_counts(self.cf.locals_base, self.func.params);
718-
let Ok(locals_base) = self.store.value_stack.enter_locals(&self.func.params, &self.func.locals) else {
719-
return cold!(Err(Trap::CallStackOverflow.into()));
720-
};
717+
let locals_base =
718+
self.store.value_stack.enter_locals(&self.func.params, &self.func.locals, &self.func.max_stack)?;
721719
self.cf = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals);
722720
Ok(())
723721
}
@@ -778,14 +776,14 @@ impl<'store> Executor<'store> {
778776
return_instr_ptr: usize,
779777
) -> ExecResult<ExecFlow> {
780778
let wasm_func = self.store.state.funcs.wasm(func_addr);
781-
let (params, locals, owner, next_func) = {
779+
let (params, locals, max_stack, owner, next_func) = {
782780
let next_func = (!Shared::ptr_eq(&self.func, &wasm_func.func)).then(|| wasm_func.func.clone());
783-
(wasm_func.func.params, wasm_func.func.locals, wasm_func.owner, next_func)
781+
(wasm_func.func.params, wasm_func.func.locals, wasm_func.func.max_stack, wasm_func.owner, next_func)
784782
};
785783
if TAIL {
786784
self.store.value_stack.truncate_keep_counts(self.cf.locals_base, params);
787785
}
788-
let locals_base = self.store.value_stack.enter_locals(&params, &locals)?;
786+
let locals_base = self.store.value_stack.enter_locals(&params, &locals, &max_stack)?;
789787
if TAIL {
790788
self.cf = CallFrame::new(func_addr, locals_base, locals);
791789
} else {

0 commit comments

Comments
 (0)