From 984502235d23203f22a1e9a5864ccd224492ade6 Mon Sep 17 00:00:00 2001 From: arcusbuilds Date: Wed, 26 Aug 2026 03:23:30 +0530 Subject: [PATCH 1/5] Charge const-expr operators the configured fuel cost Operators inside constant expressions - global initializers, element and data segment offsets, element segment expressions - were each charged a hardcoded 1 fuel unit, so `Config::operator_cost` had no effect on any instantiation-time work. Constant expressions are stored as `ConstOp` rather than `wasmparser::Operator`, so add a `const_op_cost` lookup alongside the existing `cost` lookup and use it in `translate_const_expr`. Default costs are unchanged: every `ConstOp` still costs 1 with the default table. --- crates/cranelift/src/func_environ.rs | 2 +- crates/environ/src/tunables.rs | 50 ++++++++++++++++++++++- crates/wasmtime/src/config.rs | 4 ++ tests/all/fuel.rs | 61 ++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index b722c22b3bac..e8f9529c81dc 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -6145,7 +6145,7 @@ impl FuncEnvironment<'_> { let mut stack = Vec::new(); for op in expr.ops() { if self.tunables.consume_fuel { - self.fuel_consumed += 1; + self.fuel_consumed += self.tunables.operator_cost.const_op_cost(op); } match op { ConstOp::I32Const(i) => { diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index 70ea869d644d..2f468d6df985 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{IndexType, Limits, Memory, TripleExt}; +use crate::{ConstOp, IndexType, Limits, Memory, TripleExt}; use core::num::NonZeroU32; use core::{fmt, str::FromStr}; use serde_derive::{Deserialize, Serialize}; @@ -541,6 +541,18 @@ impl OperatorCostStrategy { } } + /// Get the cost of an operator inside a constant expression. + /// + /// Constant expressions are stored as [`ConstOp`] rather than + /// `wasmparser::Operator`, so they need their own lookup, but the costs + /// come from the same table as [`OperatorCostStrategy::cost`]. + pub fn const_op_cost(&self, op: &ConstOp) -> i64 { + match self { + OperatorCostStrategy::Table(cost) => cost.const_op_cost(op), + OperatorCostStrategy::Default => DEFAULT_OPERATOR_COST.const_op_cost(op), + } + } + /// Get the costs of work whose size is only known at runtime. pub fn variable(&self) -> &VariableOperatorCost { match self { @@ -551,6 +563,7 @@ impl OperatorCostStrategy { } const DEFAULT_VARIABLE_OPERATOR_COST: VariableOperatorCost = VariableOperatorCost::new(); +const DEFAULT_OPERATOR_COST: OperatorCost = OperatorCost::new(); /// Fuel costs for operators whose work is proportional to a runtime operand. /// @@ -728,3 +741,38 @@ macro_rules! define_operator_cost { } wasmparser::for_each_operator!(define_operator_cost); + +impl OperatorCost { + /// Returns the cost of an operator appearing in a constant expression. + /// + /// This mirrors [`OperatorCost::cost`] for the [`ConstOp`] representation + /// used by global initializers, element and data segment offsets, and + /// element segment expressions. + pub fn const_op_cost(&self, op: &ConstOp) -> i64 { + let cost = match op { + ConstOp::I32Const(_) => self.I32Const, + ConstOp::I64Const(_) => self.I64Const, + ConstOp::F32Const(_) => self.F32Const, + ConstOp::F64Const(_) => self.F64Const, + ConstOp::V128Const(_) => self.V128Const, + ConstOp::GlobalGet(_) => self.GlobalGet, + ConstOp::RefI31 => self.RefI31, + ConstOp::RefNull(_) => self.RefNull, + ConstOp::RefFunc(_) => self.RefFunc, + ConstOp::I32Add => self.I32Add, + ConstOp::I32Sub => self.I32Sub, + ConstOp::I32Mul => self.I32Mul, + ConstOp::I64Add => self.I64Add, + ConstOp::I64Sub => self.I64Sub, + ConstOp::I64Mul => self.I64Mul, + ConstOp::StructNew { .. } => self.StructNew, + ConstOp::StructNewDefault { .. } => self.StructNewDefault, + ConstOp::ArrayNew { .. } => self.ArrayNew, + ConstOp::ArrayNewDefault { .. } => self.ArrayNewDefault, + ConstOp::ArrayNewFixed { .. } => self.ArrayNewFixed, + ConstOp::ExternConvertAny => self.ExternConvertAny, + ConstOp::AnyConvertExtern => self.AnyConvertExtern, + }; + i64::from(cost) + } +} diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index f172503ca9de..cf9163c2cf37 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -651,6 +651,10 @@ impl Config { /// configures per-byte, per-element, and per-page costs for operators whose /// work depends on a runtime operand. /// + /// These costs apply both to operators in function bodies and to operators + /// in constant expressions evaluated at instantiation time, such as global + /// initializers and element or data segment offsets. + /// /// This is only relevant when [`Config::consume_fuel`] is enabled. pub fn operator_cost(&mut self, cost: OperatorCost) -> &mut Self { self.tunables.operator_cost = Some(OperatorCostStrategy::table(cost)); diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 1192fdc46d8c..92af433b9e6c 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -1052,3 +1052,64 @@ fn fuel_around_table_grow() -> Result<()> { assert_eq!(trap, Trap::TableOutOfBounds); Ok(()) } + +/// A const-expr operator must be charged its configured cost, not a hardcoded 1. +/// +/// The module's global initializer is a three-op const-expr. Wasmtime does not +/// constant-fold `i32.add` at compile time, so the expression is translated into +/// the synthesized module-startup function and executed at instantiation. The +/// `(start ...)` function is what flushes the buffered charges into the fuel +/// counter, so it must be present for the charges to be observable. +/// +/// Expected accounting with `I32Add = 100` and every other cost left at its +/// default of 1: +/// +/// | module-startup function entry | 1 | +/// | `i32.const 1` | 1 | +/// | `i32.const 2` | 1 | +/// | `i32.add` | 100 | +/// | synthesized `call $start` | 1 | +/// | `$start` function entry | 1 | +/// | total | 105 | +/// +/// With the default table every op costs 1, so the same module totals 6. +#[test] +#[cfg_attr(miri, ignore)] +fn const_expr_honors_operator_cost() -> Result<()> { + const WAT: &str = r#" + (module + (global $g i32 (i32.add (i32.const 1) (i32.const 2))) + (export "g" (global $g)) + (func $start) + (start $start)) + "#; + + fn instantiation_fuel(op_cost: OperatorCost) -> Result { + let mut config = Config::new(); + config.consume_fuel(true).operator_cost(op_cost); + let engine = Engine::new(&config)?; + let module = Module::new(&engine, WAT)?; + + let mut store = Store::new(&engine, ()); + store.set_fuel(10_000)?; + let instance = Instance::new(&mut store, &module, &[])?; + + let g = instance + .get_global(&mut store, "g") + .unwrap() + .get(&mut store); + assert_eq!(g.i32(), Some(3), "global initializer did not run"); + + Ok(10_000 - store.get_fuel()?) + } + + assert_eq!(instantiation_fuel(OperatorCost::default())?, 6); + + let expensive_add = OperatorCost { + I32Add: 100, + ..Default::default() + }; + assert_eq!(instantiation_fuel(expensive_add)?, 105); + + Ok(()) +} From 62663b4b93ed6ee29764ce9775f1edf84cd4ca11 Mon Sep 17 00:00:00 2001 From: arcusbuilds Date: Wed, 26 Aug 2026 03:29:18 +0530 Subject: [PATCH 2/5] Charge the configured Call cost for the synthesized start call `module_start` synthesizes the call to a module's `(start ...)` function and manually replicates the fuel accounting that `fuel_before_op` performs for `Operator::Call`. That replica used a hardcoded 1 rather than the configured cost, so the `Call` entry of `Config::operator_cost` did not apply to the start call. Look the cost up from the table instead. --- crates/cranelift/src/func_environ.rs | 4 ++- tests/all/fuel.rs | 37 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index e8f9529c81dc..da577bf3c922 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -6115,7 +6115,9 @@ impl FuncEnvironment<'_> { // Manuall manage fuel around the call as the `Call` opcode does for // normal wasm to ensure that it's correctly accounted for. if self.tunables.consume_fuel { - self.fuel_consumed += 1; + self.fuel_consumed += self.tunables.operator_cost.cost(&Operator::Call { + function_index: func.as_u32(), + }); self.fuel_increment_var(builder); self.fuel_save_from_var(builder); } diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 92af433b9e6c..5c97bbbe99b0 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -1113,3 +1113,40 @@ fn const_expr_honors_operator_cost() -> Result<()> { Ok(()) } + +/// `module_start` synthesizes the call to the wasm `(start ...)` function and +/// hand-rolls the fuel accounting that `Operator::Call` normally receives from +/// `fuel_before_op`. That accounting must use the configured `Call` cost rather +/// than a hardcoded 1. +/// +/// The module has no globals and no segments, and an empty start function, so +/// the only charges are: +/// +/// | module-startup function entry | 1 | +/// | synthesized `call $start` | 50 | +/// | `$start` function entry | 1 | +/// | total | 52 | +#[test] +#[cfg_attr(miri, ignore)] +fn module_start_call_honors_operator_cost() -> Result<()> { + const WAT: &str = r#" + (module + (func $start) + (start $start)) + "#; + + let mut config = Config::new(); + config.consume_fuel(true).operator_cost(OperatorCost { + Call: 50, + ..Default::default() + }); + let engine = Engine::new(&config)?; + let module = Module::new(&engine, WAT)?; + + let mut store = Store::new(&engine, ()); + store.set_fuel(10_000)?; + Instance::new(&mut store, &module, &[])?; + + assert_eq!(10_000 - store.get_fuel()?, 52); + Ok(()) +} From 74871d8df1ac843693c8d9694fb159fa1f8590e9 Mon Sep 17 00:00:00 2001 From: arcusbuilds Date: Wed, 26 Aug 2026 03:48:22 +0530 Subject: [PATCH 3/5] Note the const-expr fuel change in RELEASES.md and tighten the tests `Config::operator_cost` shipped in 48.0.0, so applying it to const-expr operators and the synthesized start call changes the observable behavior of released public API; note it under 49.0.0's `Changed` section. Also pin a second cost-table entry in the const-expr test so a mis-wired `ConstOp` match arm cannot pass, and document where the two flat per-function entry charges in the start-call test come from. --- RELEASES.md | 6 ++++++ crates/environ/src/tunables.rs | 7 +++++++ tests/all/fuel.rs | 19 ++++++++++++------- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index c152459a49d0..e3d7be69bffc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,12 @@ Unreleased. ### Changed +* `Config::operator_cost` now applies to operators inside constant expressions + (global initializers, element and data segment offsets, element segment + expressions) and to the synthesized call to a module's `start` function. + Previously each of those was charged 1 fuel unit regardless of the configured + cost. + -------------------------------------------------------------------------------- Release notes for previous releases of Wasmtime can be found on the respective diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index 2f468d6df985..ee06da6ee470 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -563,6 +563,13 @@ impl OperatorCostStrategy { } const DEFAULT_VARIABLE_OPERATOR_COST: VariableOperatorCost = VariableOperatorCost::new(); + +/// The default cost table, used to serve [`OperatorCostStrategy::const_op_cost`] +/// for the [`OperatorCostStrategy::Default`] strategy. +/// +/// The `default_cost!` costs baked in here must stay in sync with +/// `default_operator_cost`, which serves the same strategy for +/// [`OperatorCostStrategy::cost`]. const DEFAULT_OPERATOR_COST: OperatorCost = OperatorCost::new(); /// Fuel costs for operators whose work is proportional to a runtime operand. diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index 5c97bbbe99b0..fb04d3a1dd38 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -1061,16 +1061,16 @@ fn fuel_around_table_grow() -> Result<()> { /// `(start ...)` function is what flushes the buffered charges into the fuel /// counter, so it must be present for the charges to be observable. /// -/// Expected accounting with `I32Add = 100` and every other cost left at its -/// default of 1: +/// Expected accounting with `I32Const = 7`, `I32Add = 100`, and every other +/// cost left at its default of 1: /// /// | module-startup function entry | 1 | -/// | `i32.const 1` | 1 | -/// | `i32.const 2` | 1 | +/// | `i32.const 1` | 7 | +/// | `i32.const 2` | 7 | /// | `i32.add` | 100 | /// | synthesized `call $start` | 1 | /// | `$start` function entry | 1 | -/// | total | 105 | +/// | total | 117 | /// /// With the default table every op costs 1, so the same module totals 6. #[test] @@ -1105,11 +1105,12 @@ fn const_expr_honors_operator_cost() -> Result<()> { assert_eq!(instantiation_fuel(OperatorCost::default())?, 6); - let expensive_add = OperatorCost { + let custom = OperatorCost { + I32Const: 7, I32Add: 100, ..Default::default() }; - assert_eq!(instantiation_fuel(expensive_add)?, 105); + assert_eq!(instantiation_fuel(custom)?, 117); Ok(()) } @@ -1126,6 +1127,10 @@ fn const_expr_honors_operator_cost() -> Result<()> { /// | synthesized `call $start` | 50 | /// | `$start` function entry | 1 | /// | total | 52 | +/// +/// The two entry rows are the flat per-function entry charge +/// (`FuncEnvironment::new`'s `fuel_consumed: 1`, flushed by `fuel_check` on +/// function entry), not derived from any operator's cost. #[test] #[cfg_attr(miri, ignore)] fn module_start_call_honors_operator_cost() -> Result<()> { From d85822f85e253c517dce95b8fc53bcac55aacaa2 Mon Sep 17 00:00:00 2001 From: arcusbuilds Date: Sat, 29 Aug 2026 00:43:11 +0530 Subject: [PATCH 4/5] Reuse the operator cost lookup for const-expr operators Translate a `ConstOp` back into the `wasmparser::Operator` it was parsed from and charge it through the existing `OperatorCostStrategy::cost`, rather than carrying a second lookup over the same table. That removes the duplicate cost mapping and the default table that had to be kept in sync with `default_operator_cost` by hand. Only `ConstOp::RefNull` cannot reproduce its immediate, since `TypeConvert::convert_heap_type` has no reverse; a placeholder heap type stands in, which is fine because the cost lookup ignores immediates. Also drop the test doc comments and assert the start-call test under the default cost table as well as a custom one. --- crates/environ/src/tunables.rs | 112 +++++++++++++++++++-------------- tests/all/fuel.rs | 64 ++++++------------- 2 files changed, 84 insertions(+), 92 deletions(-) diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index ee06da6ee470..ce73ea1cc13f 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -544,13 +544,10 @@ impl OperatorCostStrategy { /// Get the cost of an operator inside a constant expression. /// /// Constant expressions are stored as [`ConstOp`] rather than - /// `wasmparser::Operator`, so they need their own lookup, but the costs - /// come from the same table as [`OperatorCostStrategy::cost`]. + /// `wasmparser::Operator`, so translate back and reuse + /// [`OperatorCostStrategy::cost`]. pub fn const_op_cost(&self, op: &ConstOp) -> i64 { - match self { - OperatorCostStrategy::Table(cost) => cost.const_op_cost(op), - OperatorCostStrategy::Default => DEFAULT_OPERATOR_COST.const_op_cost(op), - } + self.cost(&const_op_as_operator(op)) } /// Get the costs of work whose size is only known at runtime. @@ -564,14 +561,6 @@ impl OperatorCostStrategy { const DEFAULT_VARIABLE_OPERATOR_COST: VariableOperatorCost = VariableOperatorCost::new(); -/// The default cost table, used to serve [`OperatorCostStrategy::const_op_cost`] -/// for the [`OperatorCostStrategy::Default`] strategy. -/// -/// The `default_cost!` costs baked in here must stay in sync with -/// `default_operator_cost`, which serves the same strategy for -/// [`OperatorCostStrategy::cost`]. -const DEFAULT_OPERATOR_COST: OperatorCost = OperatorCost::new(); - /// Fuel costs for operators whose work is proportional to a runtime operand. /// /// These costs are charged in addition to the corresponding flat cost in @@ -749,37 +738,68 @@ macro_rules! define_operator_cost { wasmparser::for_each_operator!(define_operator_cost); -impl OperatorCost { - /// Returns the cost of an operator appearing in a constant expression. - /// - /// This mirrors [`OperatorCost::cost`] for the [`ConstOp`] representation - /// used by global initializers, element and data segment offsets, and - /// element segment expressions. - pub fn const_op_cost(&self, op: &ConstOp) -> i64 { - let cost = match op { - ConstOp::I32Const(_) => self.I32Const, - ConstOp::I64Const(_) => self.I64Const, - ConstOp::F32Const(_) => self.F32Const, - ConstOp::F64Const(_) => self.F64Const, - ConstOp::V128Const(_) => self.V128Const, - ConstOp::GlobalGet(_) => self.GlobalGet, - ConstOp::RefI31 => self.RefI31, - ConstOp::RefNull(_) => self.RefNull, - ConstOp::RefFunc(_) => self.RefFunc, - ConstOp::I32Add => self.I32Add, - ConstOp::I32Sub => self.I32Sub, - ConstOp::I32Mul => self.I32Mul, - ConstOp::I64Add => self.I64Add, - ConstOp::I64Sub => self.I64Sub, - ConstOp::I64Mul => self.I64Mul, - ConstOp::StructNew { .. } => self.StructNew, - ConstOp::StructNewDefault { .. } => self.StructNewDefault, - ConstOp::ArrayNew { .. } => self.ArrayNew, - ConstOp::ArrayNewDefault { .. } => self.ArrayNewDefault, - ConstOp::ArrayNewFixed { .. } => self.ArrayNewFixed, - ConstOp::ExternConvertAny => self.ExternConvertAny, - ConstOp::AnyConvertExtern => self.AnyConvertExtern, - }; - i64::from(cost) +/// Translate a [`ConstOp`] back into the `wasmparser::Operator` it was parsed +/// from, so that constant expressions can share the operator cost lookup with +/// function bodies. +/// +/// Every immediate round-trips exactly except `ConstOp::RefNull`'s, which stores +/// a `WasmHeapType` lowered on the way in by `TypeConvert::convert_heap_type` +/// and has no reverse conversion, so a placeholder heap type stands in for it. +/// That is fine here because the cost lookup matches on the operator alone and +/// ignores its immediates. +fn const_op_as_operator(op: &ConstOp) -> Operator<'static> { + use wasmparser::{AbstractHeapType, HeapType, Ieee32, Ieee64, V128}; + match op { + ConstOp::I32Const(value) => Operator::I32Const { value: *value }, + ConstOp::I64Const(value) => Operator::I64Const { value: *value }, + ConstOp::F32Const(bits) => Operator::F32Const { + value: Ieee32::from(f32::from_bits(*bits)), + }, + ConstOp::F64Const(bits) => Operator::F64Const { + value: Ieee64::from(f64::from_bits(*bits)), + }, + ConstOp::V128Const(value) => Operator::V128Const { + value: V128::from(*value as i128), + }, + ConstOp::GlobalGet(index) => Operator::GlobalGet { + global_index: index.as_u32(), + }, + ConstOp::RefI31 => Operator::RefI31, + ConstOp::RefNull(_) => Operator::RefNull { + hty: HeapType::Abstract { + shared: false, + ty: AbstractHeapType::Any, + }, + }, + ConstOp::RefFunc(index) => Operator::RefFunc { + function_index: index.as_u32(), + }, + ConstOp::I32Add => Operator::I32Add, + ConstOp::I32Sub => Operator::I32Sub, + ConstOp::I32Mul => Operator::I32Mul, + ConstOp::I64Add => Operator::I64Add, + ConstOp::I64Sub => Operator::I64Sub, + ConstOp::I64Mul => Operator::I64Mul, + ConstOp::StructNew { struct_type_index } => Operator::StructNew { + struct_type_index: struct_type_index.as_u32(), + }, + ConstOp::StructNewDefault { struct_type_index } => Operator::StructNewDefault { + struct_type_index: struct_type_index.as_u32(), + }, + ConstOp::ArrayNew { array_type_index } => Operator::ArrayNew { + array_type_index: array_type_index.as_u32(), + }, + ConstOp::ArrayNewDefault { array_type_index } => Operator::ArrayNewDefault { + array_type_index: array_type_index.as_u32(), + }, + ConstOp::ArrayNewFixed { + array_type_index, + array_size, + } => Operator::ArrayNewFixed { + array_type_index: array_type_index.as_u32(), + array_size: *array_size, + }, + ConstOp::ExternConvertAny => Operator::ExternConvertAny, + ConstOp::AnyConvertExtern => Operator::AnyConvertExtern, } } diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index fb04d3a1dd38..edc5420adc80 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -1053,26 +1053,6 @@ fn fuel_around_table_grow() -> Result<()> { Ok(()) } -/// A const-expr operator must be charged its configured cost, not a hardcoded 1. -/// -/// The module's global initializer is a three-op const-expr. Wasmtime does not -/// constant-fold `i32.add` at compile time, so the expression is translated into -/// the synthesized module-startup function and executed at instantiation. The -/// `(start ...)` function is what flushes the buffered charges into the fuel -/// counter, so it must be present for the charges to be observable. -/// -/// Expected accounting with `I32Const = 7`, `I32Add = 100`, and every other -/// cost left at its default of 1: -/// -/// | module-startup function entry | 1 | -/// | `i32.const 1` | 7 | -/// | `i32.const 2` | 7 | -/// | `i32.add` | 100 | -/// | synthesized `call $start` | 1 | -/// | `$start` function entry | 1 | -/// | total | 117 | -/// -/// With the default table every op costs 1, so the same module totals 6. #[test] #[cfg_attr(miri, ignore)] fn const_expr_honors_operator_cost() -> Result<()> { @@ -1115,22 +1095,6 @@ fn const_expr_honors_operator_cost() -> Result<()> { Ok(()) } -/// `module_start` synthesizes the call to the wasm `(start ...)` function and -/// hand-rolls the fuel accounting that `Operator::Call` normally receives from -/// `fuel_before_op`. That accounting must use the configured `Call` cost rather -/// than a hardcoded 1. -/// -/// The module has no globals and no segments, and an empty start function, so -/// the only charges are: -/// -/// | module-startup function entry | 1 | -/// | synthesized `call $start` | 50 | -/// | `$start` function entry | 1 | -/// | total | 52 | -/// -/// The two entry rows are the flat per-function entry charge -/// (`FuncEnvironment::new`'s `fuel_consumed: 1`, flushed by `fuel_check` on -/// function entry), not derived from any operator's cost. #[test] #[cfg_attr(miri, ignore)] fn module_start_call_honors_operator_cost() -> Result<()> { @@ -1140,18 +1104,26 @@ fn module_start_call_honors_operator_cost() -> Result<()> { (start $start)) "#; - let mut config = Config::new(); - config.consume_fuel(true).operator_cost(OperatorCost { + fn instantiation_fuel(op_cost: OperatorCost) -> Result { + let mut config = Config::new(); + config.consume_fuel(true).operator_cost(op_cost); + let engine = Engine::new(&config)?; + let module = Module::new(&engine, WAT)?; + + let mut store = Store::new(&engine, ()); + store.set_fuel(10_000)?; + Instance::new(&mut store, &module, &[])?; + + Ok(10_000 - store.get_fuel()?) + } + + assert_eq!(instantiation_fuel(OperatorCost::default())?, 3); + + let custom = OperatorCost { Call: 50, ..Default::default() - }); - let engine = Engine::new(&config)?; - let module = Module::new(&engine, WAT)?; - - let mut store = Store::new(&engine, ()); - store.set_fuel(10_000)?; - Instance::new(&mut store, &module, &[])?; + }; + assert_eq!(instantiation_fuel(custom)?, 52); - assert_eq!(10_000 - store.get_fuel()?, 52); Ok(()) } From 2cc9a2f484aff707ccfe72f2852629146039767b Mon Sep 17 00:00:00 2001 From: arcusbuilds Date: Sat, 29 Aug 2026 02:20:06 +0530 Subject: [PATCH 5/5] Move the const-expr operator translation onto `ConstOp` `ConstOp::to_operator` now lives next to `ConstOp::from_wasmparser`, and `translate_const_expr` charges through `OperatorCostStrategy::cost` directly, so `tunables.rs` no longer carries any const-expr specific lookup. Run both new tests under `#[wasmtime_test]` like the neighbouring cost tests, with `extended_const` enabled for the `i32.add` initializer, and link the release note to the PR. --- RELEASES.md | 1 + crates/cranelift/src/func_environ.rs | 2 +- crates/environ/src/tunables.rs | 77 +--------------------------- crates/environ/src/types.rs | 61 ++++++++++++++++++++++ tests/all/fuel.rs | 26 +++++----- 5 files changed, 76 insertions(+), 91 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index e3d7be69bffc..7ddd1e42cbf3 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,7 @@ Unreleased. expressions) and to the synthesized call to a module's `start` function. Previously each of those was charged 1 fuel unit regardless of the configured cost. + [#14215](https://github.com/bytecodealliance/wasmtime/pull/14215) -------------------------------------------------------------------------------- diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index da577bf3c922..0da899c472d8 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -6147,7 +6147,7 @@ impl FuncEnvironment<'_> { let mut stack = Vec::new(); for op in expr.ops() { if self.tunables.consume_fuel { - self.fuel_consumed += self.tunables.operator_cost.const_op_cost(op); + self.fuel_consumed += self.tunables.operator_cost.cost(&op.to_operator()); } match op { ConstOp::I32Const(i) => { diff --git a/crates/environ/src/tunables.rs b/crates/environ/src/tunables.rs index ce73ea1cc13f..70ea869d644d 100644 --- a/crates/environ/src/tunables.rs +++ b/crates/environ/src/tunables.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{ConstOp, IndexType, Limits, Memory, TripleExt}; +use crate::{IndexType, Limits, Memory, TripleExt}; use core::num::NonZeroU32; use core::{fmt, str::FromStr}; use serde_derive::{Deserialize, Serialize}; @@ -541,15 +541,6 @@ impl OperatorCostStrategy { } } - /// Get the cost of an operator inside a constant expression. - /// - /// Constant expressions are stored as [`ConstOp`] rather than - /// `wasmparser::Operator`, so translate back and reuse - /// [`OperatorCostStrategy::cost`]. - pub fn const_op_cost(&self, op: &ConstOp) -> i64 { - self.cost(&const_op_as_operator(op)) - } - /// Get the costs of work whose size is only known at runtime. pub fn variable(&self) -> &VariableOperatorCost { match self { @@ -737,69 +728,3 @@ macro_rules! define_operator_cost { } wasmparser::for_each_operator!(define_operator_cost); - -/// Translate a [`ConstOp`] back into the `wasmparser::Operator` it was parsed -/// from, so that constant expressions can share the operator cost lookup with -/// function bodies. -/// -/// Every immediate round-trips exactly except `ConstOp::RefNull`'s, which stores -/// a `WasmHeapType` lowered on the way in by `TypeConvert::convert_heap_type` -/// and has no reverse conversion, so a placeholder heap type stands in for it. -/// That is fine here because the cost lookup matches on the operator alone and -/// ignores its immediates. -fn const_op_as_operator(op: &ConstOp) -> Operator<'static> { - use wasmparser::{AbstractHeapType, HeapType, Ieee32, Ieee64, V128}; - match op { - ConstOp::I32Const(value) => Operator::I32Const { value: *value }, - ConstOp::I64Const(value) => Operator::I64Const { value: *value }, - ConstOp::F32Const(bits) => Operator::F32Const { - value: Ieee32::from(f32::from_bits(*bits)), - }, - ConstOp::F64Const(bits) => Operator::F64Const { - value: Ieee64::from(f64::from_bits(*bits)), - }, - ConstOp::V128Const(value) => Operator::V128Const { - value: V128::from(*value as i128), - }, - ConstOp::GlobalGet(index) => Operator::GlobalGet { - global_index: index.as_u32(), - }, - ConstOp::RefI31 => Operator::RefI31, - ConstOp::RefNull(_) => Operator::RefNull { - hty: HeapType::Abstract { - shared: false, - ty: AbstractHeapType::Any, - }, - }, - ConstOp::RefFunc(index) => Operator::RefFunc { - function_index: index.as_u32(), - }, - ConstOp::I32Add => Operator::I32Add, - ConstOp::I32Sub => Operator::I32Sub, - ConstOp::I32Mul => Operator::I32Mul, - ConstOp::I64Add => Operator::I64Add, - ConstOp::I64Sub => Operator::I64Sub, - ConstOp::I64Mul => Operator::I64Mul, - ConstOp::StructNew { struct_type_index } => Operator::StructNew { - struct_type_index: struct_type_index.as_u32(), - }, - ConstOp::StructNewDefault { struct_type_index } => Operator::StructNewDefault { - struct_type_index: struct_type_index.as_u32(), - }, - ConstOp::ArrayNew { array_type_index } => Operator::ArrayNew { - array_type_index: array_type_index.as_u32(), - }, - ConstOp::ArrayNewDefault { array_type_index } => Operator::ArrayNewDefault { - array_type_index: array_type_index.as_u32(), - }, - ConstOp::ArrayNewFixed { - array_type_index, - array_size, - } => Operator::ArrayNewFixed { - array_type_index: array_type_index.as_u32(), - array_size: *array_size, - }, - ConstOp::ExternConvertAny => Operator::ExternConvertAny, - ConstOp::AnyConvertExtern => Operator::AnyConvertExtern, - } -} diff --git a/crates/environ/src/types.rs b/crates/environ/src/types.rs index 2f7782e1ea24..c3c5b1f759ce 100644 --- a/crates/environ/src/types.rs +++ b/crates/environ/src/types.rs @@ -2145,6 +2145,67 @@ impl ConstOp { } }) } + + /// Convert a `ConstOp` back to a `wasmparser::Operator`. + /// + /// `RefNull`'s heap type does not round-trip, so only use this where the + /// immediates do not matter, such as looking up an operator's fuel cost. + pub fn to_operator(&self) -> wasmparser::Operator<'static> { + use wasmparser::{AbstractHeapType, HeapType, Ieee32, Ieee64, Operator, V128}; + match self { + ConstOp::I32Const(value) => Operator::I32Const { value: *value }, + ConstOp::I64Const(value) => Operator::I64Const { value: *value }, + ConstOp::F32Const(bits) => Operator::F32Const { + value: Ieee32::from(f32::from_bits(*bits)), + }, + ConstOp::F64Const(bits) => Operator::F64Const { + value: Ieee64::from(f64::from_bits(*bits)), + }, + ConstOp::V128Const(value) => Operator::V128Const { + value: V128::from(*value as i128), + }, + ConstOp::GlobalGet(index) => Operator::GlobalGet { + global_index: index.as_u32(), + }, + ConstOp::RefI31 => Operator::RefI31, + ConstOp::RefNull(_) => Operator::RefNull { + hty: HeapType::Abstract { + shared: false, + ty: AbstractHeapType::Any, + }, + }, + ConstOp::RefFunc(index) => Operator::RefFunc { + function_index: index.as_u32(), + }, + ConstOp::I32Add => Operator::I32Add, + ConstOp::I32Sub => Operator::I32Sub, + ConstOp::I32Mul => Operator::I32Mul, + ConstOp::I64Add => Operator::I64Add, + ConstOp::I64Sub => Operator::I64Sub, + ConstOp::I64Mul => Operator::I64Mul, + ConstOp::StructNew { struct_type_index } => Operator::StructNew { + struct_type_index: struct_type_index.as_u32(), + }, + ConstOp::StructNewDefault { struct_type_index } => Operator::StructNewDefault { + struct_type_index: struct_type_index.as_u32(), + }, + ConstOp::ArrayNew { array_type_index } => Operator::ArrayNew { + array_type_index: array_type_index.as_u32(), + }, + ConstOp::ArrayNewDefault { array_type_index } => Operator::ArrayNewDefault { + array_type_index: array_type_index.as_u32(), + }, + ConstOp::ArrayNewFixed { + array_type_index, + array_size, + } => Operator::ArrayNewFixed { + array_type_index: array_type_index.as_u32(), + array_size: *array_size, + }, + ConstOp::ExternConvertAny => Operator::ExternConvertAny, + ConstOp::AnyConvertExtern => Operator::AnyConvertExtern, + } + } } /// The type that can be used to index into [Memory] and [Table]. diff --git a/tests/all/fuel.rs b/tests/all/fuel.rs index edc5420adc80..f6a06a21f025 100644 --- a/tests/all/fuel.rs +++ b/tests/all/fuel.rs @@ -1053,9 +1053,9 @@ fn fuel_around_table_grow() -> Result<()> { Ok(()) } -#[test] +#[wasmtime_test(wasm_features(extended_const))] #[cfg_attr(miri, ignore)] -fn const_expr_honors_operator_cost() -> Result<()> { +fn const_expr_honors_operator_cost(config: &mut Config) -> Result<()> { const WAT: &str = r#" (module (global $g i32 (i32.add (i32.const 1) (i32.const 2))) @@ -1064,10 +1064,9 @@ fn const_expr_honors_operator_cost() -> Result<()> { (start $start)) "#; - fn instantiation_fuel(op_cost: OperatorCost) -> Result { - let mut config = Config::new(); + fn instantiation_fuel(config: &mut Config, op_cost: OperatorCost) -> Result { config.consume_fuel(true).operator_cost(op_cost); - let engine = Engine::new(&config)?; + let engine = Engine::new(config)?; let module = Module::new(&engine, WAT)?; let mut store = Store::new(&engine, ()); @@ -1083,31 +1082,30 @@ fn const_expr_honors_operator_cost() -> Result<()> { Ok(10_000 - store.get_fuel()?) } - assert_eq!(instantiation_fuel(OperatorCost::default())?, 6); + assert_eq!(instantiation_fuel(config, OperatorCost::default())?, 6); let custom = OperatorCost { I32Const: 7, I32Add: 100, ..Default::default() }; - assert_eq!(instantiation_fuel(custom)?, 117); + assert_eq!(instantiation_fuel(config, custom)?, 117); Ok(()) } -#[test] +#[wasmtime_test] #[cfg_attr(miri, ignore)] -fn module_start_call_honors_operator_cost() -> Result<()> { +fn module_start_call_honors_operator_cost(config: &mut Config) -> Result<()> { const WAT: &str = r#" (module (func $start) (start $start)) "#; - fn instantiation_fuel(op_cost: OperatorCost) -> Result { - let mut config = Config::new(); + fn instantiation_fuel(config: &mut Config, op_cost: OperatorCost) -> Result { config.consume_fuel(true).operator_cost(op_cost); - let engine = Engine::new(&config)?; + let engine = Engine::new(config)?; let module = Module::new(&engine, WAT)?; let mut store = Store::new(&engine, ()); @@ -1117,13 +1115,13 @@ fn module_start_call_honors_operator_cost() -> Result<()> { Ok(10_000 - store.get_fuel()?) } - assert_eq!(instantiation_fuel(OperatorCost::default())?, 3); + assert_eq!(instantiation_fuel(config, OperatorCost::default())?, 3); let custom = OperatorCost { Call: 50, ..Default::default() }; - assert_eq!(instantiation_fuel(custom)?, 52); + assert_eq!(instantiation_fuel(config, custom)?, 52); Ok(()) }