Skip to content

Commit c0be647

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 7af50cc commit c0be647

12 files changed

Lines changed: 332 additions & 77 deletions

File tree

‎crates/cli/src/wast_runner.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,14 @@ impl WastRunner {
344344
);
345345
continue;
346346
};
347-
if !message.starts_with(trap.message()) && !trap.message().starts_with(message) {
347+
// The core test suite's "call stack exhausted" text denotes stack exhaustion,
348+
// not which internal stack reaches its configured limit first. Function-entry
349+
// operand reservation can exhaust a value lane before the call-frame stack.
350+
if !matches!(trap, tinywasm::Trap::CallStackOverflow | tinywasm::Trap::ValueStackOverflow) {
348351
test_group.add_result(
349352
&format!("AssertExhaustion({i})"),
350353
span.linecol_in(wast_raw),
351-
Err(anyhow!("expected trap: {}, got: {}", message, trap.message())),
354+
Err(anyhow!("expected stack exhaustion ({message}), got: {}", trap.message())),
352355
);
353356
continue;
354357
}

‎crates/parser/src/conversion.rs‎

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

199199
#[cfg(feature = "validate")]
200-
let (body, data, uses_local_memory, validator_allocs, reader_allocs) = match validator {
200+
let (body, data, uses_local_memory, max_stack, validator_allocs, reader_allocs) = match validator {
201201
Some(validator) => {
202-
let (body, data, uses_local_memory, validator_allocs, reader_allocs) = process_operators_and_validate(
203-
validator,
204-
func,
205-
(local_types, local_addr_map),
206-
metadata,
207-
context,
208-
reader_allocs,
209-
options,
210-
)?;
211-
(body, data, uses_local_memory, Some(validator_allocs), reader_allocs)
202+
let (body, data, uses_local_memory, max_stack, validator_allocs, reader_allocs) =
203+
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, max_stack, Some(validator_allocs), reader_allocs)
212213
}
213214
None => {
214-
let (body, data, uses_local_memory, reader_allocs) =
215+
let (body, data, uses_local_memory, max_stack, reader_allocs) =
215216
process_operators(func, (local_types, local_addr_map), metadata, context, reader_allocs, options)?;
216-
(body, data, uses_local_memory, None, reader_allocs)
217+
(body, data, uses_local_memory, max_stack, None, reader_allocs)
217218
}
218219
};
219220
#[cfg(not(feature = "validate"))]
220-
let (body, data, uses_local_memory, validator_allocs, reader_allocs) = {
221+
let (body, data, uses_local_memory, max_stack, validator_allocs, reader_allocs) = {
221222
let _ = validator;
222-
let (body, data, uses_local_memory, reader_allocs) =
223+
let (body, data, uses_local_memory, max_stack, reader_allocs) =
223224
process_operators(func, (local_types, local_addr_map), metadata, context, reader_allocs, options)?;
224-
(body, data, uses_local_memory, None, reader_allocs)
225+
(body, data, uses_local_memory, max_stack, None, reader_allocs)
225226
};
226227
Ok((
227-
FunctionCode { instructions: body, data: data.finish(), locals: local_counts, uses_local_memory },
228+
FunctionCode { instructions: body, data: data.finish(), locals: local_counts, max_stack, uses_local_memory },
228229
validator_allocs,
229230
reader_allocs,
230231
))

‎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
}
@@ -436,7 +438,7 @@ pub(crate) fn process_operators(
436438
context: FunctionLoweringContext,
437439
allocs: OperatorsReaderAllocations,
438440
options: &ParserOptions,
439-
) -> Result<(Vec<Instruction>, FunctionDataBuilder, bool, OperatorsReaderAllocations)> {
441+
) -> Result<(Vec<Instruction>, FunctionDataBuilder, bool, ValueCounts, OperatorsReaderAllocations)> {
440442
let (local_types, local_addr_map) = locals;
441443
let body_size = body.as_bytes().len();
442444
let reader = body.get_binary_reader_for_operators()?;
@@ -459,7 +461,7 @@ pub(crate) fn process_operators(
459461

460462
reader.finish()?;
461463
let instructions = builder.emitter.finish(&mut builder.data)?;
462-
Ok((instructions, builder.data, builder.uses_local_memory, reader.into_allocations()))
464+
Ok((instructions, builder.data, builder.uses_local_memory, builder.max_lane_counts, reader.into_allocations()))
463465
}
464466

465467
#[cfg(feature = "validate")]
@@ -471,7 +473,14 @@ pub(crate) fn process_operators_and_validate(
471473
context: FunctionLoweringContext,
472474
allocs: OperatorsReaderAllocations,
473475
options: &ParserOptions,
474-
) -> Result<(Vec<Instruction>, FunctionDataBuilder, bool, FuncValidatorAllocations, OperatorsReaderAllocations)> {
476+
) -> Result<(
477+
Vec<Instruction>,
478+
FunctionDataBuilder,
479+
bool,
480+
ValueCounts,
481+
FuncValidatorAllocations,
482+
OperatorsReaderAllocations,
483+
)> {
475484
let (local_types, local_addr_map) = locals;
476485
let body_size = body.as_bytes().len();
477486
let reader = body.get_binary_reader_for_operators()?;
@@ -494,7 +503,14 @@ pub(crate) fn process_operators_and_validate(
494503

495504
reader.finish()?;
496505
let instructions = builder.emitter.finish(&mut builder.data)?;
497-
Ok((instructions, builder.data, builder.uses_local_memory, validator.into_allocations(), reader.into_allocations()))
506+
Ok((
507+
instructions,
508+
builder.data,
509+
builder.uses_local_memory,
510+
builder.max_lane_counts,
511+
validator.into_allocations(),
512+
reader.into_allocations(),
513+
))
498514
}
499515

500516
impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
@@ -859,11 +875,23 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> {
859875
wasmparser::Catch::All { label } => (None, label, false),
860876
wasmparser::Catch::AllRef { label } => (None, label, true),
861877
};
862-
if let Some(tag) = tag {
863-
self.metadata.tag_signature(tag)?;
864-
}
865878
let target_idx = self.get_ctx_idx(depth)?;
866879
let target_base = self.control_stack[target_idx].base;
880+
// The runtime truncates to the target base, then injects the exception payload and
881+
// optional reference before executing this landing pad. These pushes do not pass
882+
// through the ordinary logical operand stack, but still need function-entry capacity.
883+
let mut landing_counts = target_base;
884+
if let Some(tag) = tag {
885+
for &lane in &self.metadata.tag_signature(tag)?.params {
886+
Self::increment_lane(&mut landing_counts, lane)?;
887+
}
888+
}
889+
if with_ref {
890+
Self::increment_lane(&mut landing_counts, ValueLane::S32)?;
891+
}
892+
self.max_lane_counts.c32 = self.max_lane_counts.c32.max(landing_counts.c32);
893+
self.max_lane_counts.c64 = self.max_lane_counts.c64.max(landing_counts.c64);
894+
self.max_lane_counts.c128 = self.max_lane_counts.c128.max(landing_counts.c128);
867895
let landing_label = self.emitter.new_label();
868896
self.emitter.bind(landing_label)?;
869897
self.emit_branch_jump_or_return(depth)?;
@@ -1432,17 +1460,29 @@ impl<'a> FunctionBuilder<'a> {
14321460
Ok((size, addr))
14331461
}
14341462

1435-
/// Pushes logical operands while maintaining the lane counts used by `DropKeep`.
1463+
/// Increments a physical lane count, rejecting functions too large for the encoded count.
1464+
fn increment_lane(counts: &mut ValueCounts, lane: ValueLane) -> Result<()> {
1465+
let count = match lane {
1466+
ValueLane::S32 => &mut counts.c32,
1467+
ValueLane::S64 => &mut counts.c64,
1468+
ValueLane::S128 => &mut counts.c128,
1469+
};
1470+
*count = count
1471+
.checked_add(1)
1472+
.ok_or_else(|| crate::ParseError::Other("logical operand lane count is too large".into()))?;
1473+
Ok(())
1474+
}
1475+
1476+
/// Pushes logical operands while maintaining the lane counts used by `DropKeep` and their
1477+
/// maximum, which the runtime reserves when it enters the function.
14361478
fn push_sizes(&mut self, sizes: &[ValueLane]) -> Result<()> {
14371479
for &size in sizes {
1438-
let count = match size {
1439-
ValueLane::S32 => &mut self.lane_counts.c32,
1440-
ValueLane::S64 => &mut self.lane_counts.c64,
1441-
ValueLane::S128 => &mut self.lane_counts.c128,
1442-
};
1443-
*count = count
1444-
.checked_add(1)
1445-
.ok_or_else(|| crate::ParseError::Other("logical operand lane count is too large".into()))?;
1480+
Self::increment_lane(&mut self.lane_counts, size)?;
1481+
match size {
1482+
ValueLane::S32 => self.max_lane_counts.c32 = self.max_lane_counts.c32.max(self.lane_counts.c32),
1483+
ValueLane::S64 => self.max_lane_counts.c64 = self.max_lane_counts.c64.max(self.lane_counts.c64),
1484+
ValueLane::S128 => self.max_lane_counts.c128 = self.max_lane_counts.c128.max(self.lane_counts.c128),
1485+
}
14461486
self.operand_stack.push(size);
14471487
}
14481488
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
@@ -620,7 +620,7 @@ impl<'store> Executor<'store> {
620620
let Store { state, value_stack, .. } = self.store;
621621
let object = state.gc.get(exception).ok_or(Trap::InvalidReference)?;
622622
for value in object.values.iter().copied() {
623-
value_stack.push_dyn(value)?;
623+
value_stack.push_reserved(value)?;
624624
}
625625
}
626626
if with_ref {
@@ -706,9 +706,8 @@ impl<'store> Executor<'store> {
706706

707707
fn exec_call_self(&mut self, return_instr_ptr: usize) -> ExecResult<()> {
708708
self.charge_call_fuel(FUEL_COST_CALL_TOTAL);
709-
let Ok(locals_base) = self.store.value_stack.enter_locals(&self.func.params, &self.func.locals) else {
710-
return cold!(Err(Trap::CallStackOverflow.into()));
711-
};
709+
let locals_base =
710+
self.store.value_stack.enter_locals(&self.func.params, &self.func.locals, &self.func.max_stack)?;
712711
let new = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals);
713712
self.store.call_stack.push(core::mem::replace(&mut self.cf, new), return_instr_ptr)?;
714713
Ok(())
@@ -718,9 +717,8 @@ impl<'store> Executor<'store> {
718717
self.charge_call_fuel(FUEL_COST_CALL_TOTAL);
719718

720719
self.store.value_stack.truncate_keep_counts(self.cf.locals_base, self.func.params);
721-
let Ok(locals_base) = self.store.value_stack.enter_locals(&self.func.params, &self.func.locals) else {
722-
return cold!(Err(Trap::CallStackOverflow.into()));
723-
};
720+
let locals_base =
721+
self.store.value_stack.enter_locals(&self.func.params, &self.func.locals, &self.func.max_stack)?;
724722
self.cf = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals);
725723
Ok(())
726724
}
@@ -780,14 +778,14 @@ impl<'store> Executor<'store> {
780778
return_instr_ptr: usize,
781779
) -> ExecResult<ExecFlow> {
782780
let wasm_func = self.store.state.funcs.wasm(func_addr);
783-
let (params, locals, owner, next_func) = {
781+
let (params, locals, max_stack, owner, next_func) = {
784782
let next_func = (!Shared::ptr_eq(&self.func, &wasm_func.func)).then(|| wasm_func.func.clone());
785-
(wasm_func.func.params, wasm_func.func.locals, wasm_func.owner, next_func)
783+
(wasm_func.func.params, wasm_func.func.locals, wasm_func.func.max_stack, wasm_func.owner, next_func)
786784
};
787785
if TAIL {
788786
self.store.value_stack.truncate_keep_counts(self.cf.locals_base, params);
789787
}
790-
let locals_base = self.store.value_stack.enter_locals(&params, &locals)?;
788+
let locals_base = self.store.value_stack.enter_locals(&params, &locals, &max_stack)?;
791789
if TAIL {
792790
self.cf = CallFrame::new(func_addr, locals_base, locals);
793791
} else {

0 commit comments

Comments
 (0)