From 5f74d5bc5cd0870a42342210c62d7257a0dd8458 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 27 Jun 2026 13:39:36 +0200 Subject: feat: blocking host reentrant calls Signed-off-by: Henry --- crates/tinywasm/src/error.rs | 6 + crates/tinywasm/src/func.rs | 251 ++++++++++++++------- crates/tinywasm/src/interpreter/executor.rs | 9 +- crates/tinywasm/src/interpreter/mod.rs | 8 +- .../tinywasm/src/interpreter/stack/call_stack.rs | 14 +- .../tinywasm/src/interpreter/stack/value_stack.rs | 9 + crates/tinywasm/src/reference.rs | 2 +- crates/tinywasm/src/store/mod.rs | 35 ++- 8 files changed, 227 insertions(+), 107 deletions(-) (limited to 'crates') diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index b26da42..4f187e5 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -96,6 +96,12 @@ impl LinkingError { } } +impl Error { + pub(crate) fn other(message: impl Into) -> Self { + Self::Other(message.into()) + } +} + /// A WebAssembly trap /// /// See diff --git a/crates/tinywasm/src/func.rs b/crates/tinywasm/src/func.rs index 82b329d..cb95945 100644 --- a/crates/tinywasm/src/func.rs +++ b/crates/tinywasm/src/func.rs @@ -1,7 +1,7 @@ use crate::interpreter::stack::{CallFrame, ValueStack}; use crate::reference::StoreItem; use crate::{Error, FunctionInstance, InterpreterRuntime, Result, Store, unlikely}; -use alloc::{boxed::Box, format, rc::Rc, string::ToString, sync::Arc, vec, vec::Vec}; +use alloc::{boxed::Box, format, rc::Rc, sync::Arc, vec, vec::Vec}; use tinywasm_types::{ExternRef, FuncRef, FuncType, ModuleInstanceAddr, WasmType, WasmValue}; impl Function { @@ -10,26 +10,36 @@ impl Function { /// See #[inline] pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result> { + #[inline] + fn call_inner(func: &Function, store: &mut Store, params: &[WasmValue]) -> Result> { + let func_instance = store.state.get_func(func.addr); + let wasm_func = match func_instance { + FunctionInstance::Host(host_func) => { + return host_func.clone().call(FuncContext { store, module_addr: func.module_addr }, params); + } + FunctionInstance::Wasm(wasm_func) => wasm_func, + }; + + // Reset stack, push args, allocate locals, create entry frame. + store.call_stack.clear(); + store.value_stack.clear(); + store.value_stack.extend_from_wasmvalues(params)?; + let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals)?; + let callframe = CallFrame::new(func.addr, locals_base, wasm_func.func.locals); + + // Execute until completion and then collect result values from the stack. + InterpreterRuntime::exec(store, callframe, 0)?; + collect_call_results(&mut store.value_stack, &func.ty) + } + self.item.validate_store(store)?; validate_call_params(&self.ty, params)?; - let wasm_func = match store.state.get_func(self.addr) { - FunctionInstance::Host(host_func) => { - return host_func.clone().call(FuncContext { store, module_addr: self.module_addr }, params); - } - FunctionInstance::Wasm(wasm_func) => wasm_func, - }; - - // Reset stack, push args, allocate locals, create entry frame. - store.call_stack.clear(); - store.value_stack.clear(); - store.value_stack.extend_from_wasmvalues(params)?; - let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals)?; - let callframe = CallFrame::new(self.addr, locals_base, wasm_func.func.locals); + store.enter_execution()?; + let result = call_inner(self, store, params); + store.exit_execution(); - // Execute until completion and then collect result values from the stack. - InterpreterRuntime::exec(store, callframe)?; - collect_call_results(&mut store.value_stack, &self.ty) + result } /// Call a function and return a resumable execution handle. @@ -42,30 +52,41 @@ impl Function { store: &'store mut Store, params: &[WasmValue], ) -> Result> { - self.item.validate_store(store)?; - validate_call_params(&self.ty, params)?; - - match store.state.get_func(self.addr) { - FunctionInstance::Host(host_func) => { - let result = host_func.clone().call(FuncContext { store, module_addr: self.module_addr }, params)?; - Ok(FuncExecution { store, state: FuncExecutionState::Completed { result: Some(result) } }) - } - FunctionInstance::Wasm(wasm_func) => { - store.call_stack.clear(); - store.value_stack.clear(); - store.value_stack.extend_from_wasmvalues(params)?; - let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals)?; - let callframe = CallFrame::new(self.addr, locals_base, wasm_func.func.locals); - - Ok(FuncExecution { - store, - state: FuncExecutionState::Running { + #[inline] + fn call_resumable_inner( + func: &Function, + store: &mut Store, + params: &[WasmValue], + ) -> Result { + let func_instance = store.state.get_func(func.addr); + match func_instance { + FunctionInstance::Host(host_func) => host_func + .clone() + .call(FuncContext { store, module_addr: func.module_addr }, params) + .map(|result| FuncExecutionState::Completed { result: Some(result) }), + FunctionInstance::Wasm(wasm_func) => { + store.call_stack.clear(); + store.value_stack.clear(); + store.value_stack.extend_from_wasmvalues(params)?; + let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals)?; + let callframe = CallFrame::new(func.addr, locals_base, wasm_func.func.locals); + + Ok(FuncExecutionState::Running { exec_state: ExecutionState { callframe }, - root_func_addr: self.addr, - }, - }) + root_func_addr: func.addr, + }) + } } } + + self.item.validate_store(store)?; + validate_call_params(&self.ty, params)?; + + store.enter_execution()?; + let result = call_resumable_inner(self, store, params); + store.exit_execution(); + + Ok(FuncExecution { store, state: result? }) } } @@ -157,27 +178,24 @@ impl HostFunction { func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result> + 'static, ) -> Function { let ty = Arc::new(ty.clone()); - let ty_inner = ty.clone(); + let host_ty = ty.clone(); + let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result> { - let ty = ty_inner.clone(); let result = func(ctx, args)?; + let expected = host_ty.results(); - if result.len() != ty.results().len() { - return Err(crate::Error::InvalidHostFnReturn { expected: ty.clone(), actual: result }); - }; + let valid = result.len() == expected.len() + && result.iter().zip(expected).all(|(val, ty)| WasmType::from(val) == *ty); - result.iter().zip(ty.results().iter()).try_for_each(|(val, res_ty)| { - if WasmType::from(val) != *res_ty { - return Err(crate::Error::InvalidHostFnReturn { expected: ty.clone(), actual: result.clone() }); - } - Ok(()) - })?; + if !valid { + return Err(crate::Error::InvalidHostFnReturn { expected: Arc::clone(&host_ty), actual: result }); + } Ok(result) }; let addr = store.add_func(FunctionInstance::Host(Rc::new(Self { func: Box::new(inner_func), ty: ty.clone() }))); - Function { item: crate::StoreItem::new(store.id(), addr), module_addr: 0, addr, ty: ty.clone() } + Function { item: crate::StoreItem::new(store.id(), addr), module_addr: 0, addr, ty } } /// Create a new typed host function import. @@ -211,9 +229,7 @@ impl HostFunction { R: IntoWasmValues + ToWasmTypes, { let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result> { - let args = P::from_wasm_values(args)?; - let result = func(ctx, args)?; - Ok(result.into_wasm_values()) + Ok(func(ctx, P::from_wasm_values(args)?)?.into_wasm_values()) }; let ty = Arc::new(tinywasm_types::FuncType::new(&P::wasm_types(), &R::wasm_types())); @@ -293,6 +309,66 @@ impl FuncContext<'_> { pub fn remaining_fuel(&self) -> u32 { self.store.execution_fuel } + + /// Call a function from within the current host-function invocation. + /// + /// This is the safe way for host functions to perform blocking reentrant + /// calls into Wasm. Unlike [`Function::call`], it preserves the active + /// invocation's stacks and resumes the host caller after the nested call + /// completes. + /// + /// Nested calls are currently blocking only. If the surrounding invocation + /// is resumed with fuel or a time budget, this method does not suspend and + /// later continue the host function in the middle of the nested call. + pub fn call_untyped(&mut self, func: &Function, args: &[WasmValue]) -> Result> { + if !self.store.execution_active { + return Err(Error::other("FuncContext::call requires an active host-function invocation")); + } + + func.item.validate_store(self.store)?; + validate_call_params(&func.ty, args)?; + + let func_instance = self.store.state.get_func(func.addr).clone(); + match func_instance { + FunctionInstance::Host(host_func) => { + host_func.call(FuncContext { store: &mut *self.store, module_addr: func.module_addr }, args) + } + FunctionInstance::Wasm(wasm_func) => { + let call_stack_base = self.store.call_stack.len(); + let value_stack_base = self.store.value_stack.base(); + + self.store.value_stack.extend_from_wasmvalues(args).inspect_err(|_| { + self.store.value_stack.truncate_to_base(value_stack_base); + })?; + + let locals_base = self + .store + .value_stack + .enter_locals(&wasm_func.func.params, &wasm_func.func.locals) + .inspect_err(|_| self.store.value_stack.truncate_to_base(value_stack_base))?; + + let callframe = CallFrame::new(func.addr, locals_base, wasm_func.func.locals); + InterpreterRuntime::exec(self.store, callframe, call_stack_base).inspect_err(|_| { + self.store.call_stack.truncate_to(call_stack_base); + self.store.value_stack.truncate_to_base(value_stack_base); + })?; + + collect_call_results(&mut self.store.value_stack, &func.ty) + } + } + } + + /// Call a typed function from within the current host-function invocation. + /// + /// See [`Self::call_untyped`] for reentrancy and resumable-execution + /// limitations. + pub fn call(&mut self, func: &FunctionTyped, params: P) -> Result + where + P: IntoWasmValues, + R: FromWasmValues, + { + R::from_wasm_values(&self.call_untyped(&func.func, ¶ms.into_wasm_values())?) + } } impl core::ops::Deref for FuncContext<'_> { @@ -352,23 +428,30 @@ impl<'store> FuncExecution<'store> { /// Returns [`ExecProgress::Suspended`] when fuel is exhausted, or /// [`ExecProgress::Completed`] with the final values once the invocation /// returns. + /// + /// Reentrant calls made by host functions through [`FuncContext::call`] are + /// currently blocking. They do not suspend and later resume the host + /// function in the middle of the nested call. pub fn resume_with_fuel(&mut self, fuel: u32) -> Result>> { let FuncExecutionState::Running { exec_state, root_func_addr } = &mut self.state else { let FuncExecutionState::Completed { result } = &mut self.state else { unreachable!("invalid function execution state") }; - return result - .take() - .map(ExecProgress::Completed) - .ok_or_else(|| Error::Other("execution already completed".to_string())); + return match result.take() { + Some(res) => Ok(ExecProgress::Completed(res)), + None => Err(Error::other("execution already completed")), + }; }; - match InterpreterRuntime::exec_with_fuel(self.store, exec_state.callframe, fuel)? { + self.store.enter_execution()?; + let result = InterpreterRuntime::exec_with_fuel(self.store, exec_state.callframe, fuel); + self.store.exit_execution(); + + match result? { crate::interpreter::ExecState::Completed => { let result_ty = self.store.state.get_func(*root_func_addr).ty().clone(); - let result = collect_call_results(&mut self.store.value_stack, &result_ty)?; self.state = FuncExecutionState::Completed { result: None }; - Ok(ExecProgress::Completed(result)) + Ok(ExecProgress::Completed(collect_call_results(&mut self.store.value_stack, &result_ty)?)) } crate::interpreter::ExecState::Suspended(callframe) => { exec_state.callframe = callframe; @@ -386,6 +469,10 @@ impl<'store> FuncExecution<'store> { /// Returns [`ExecProgress::Suspended`] when the budget is exhausted, or /// [`ExecProgress::Completed`] with the final values once the invocation /// returns. + /// + /// Reentrant calls made by host functions through [`FuncContext::call`] are + /// currently blocking. They do not suspend and later resume the host + /// function in the middle of the nested call. pub fn resume_with_time_budget( &mut self, time_budget: crate::std::time::Duration, @@ -394,18 +481,21 @@ impl<'store> FuncExecution<'store> { let FuncExecutionState::Completed { result } = &mut self.state else { unreachable!("invalid function execution state") }; - return result - .take() - .map(ExecProgress::Completed) - .ok_or_else(|| Error::Other("execution already completed".to_string())); + return match result.take() { + Some(res) => Ok(ExecProgress::Completed(res)), + None => Err(Error::other("execution already completed")), + }; }; - match InterpreterRuntime::exec_with_time_budget(self.store, exec_state.callframe, time_budget)? { + self.store.enter_execution()?; + let result = InterpreterRuntime::exec_with_time_budget(self.store, exec_state.callframe, time_budget); + self.store.exit_execution(); + + match result? { crate::interpreter::ExecState::Completed => { let result_ty = self.store.state.get_func(*root_func_addr).ty().clone(); - let result = collect_call_results(&mut self.store.value_stack, &result_ty)?; self.state = FuncExecutionState::Completed { result: None }; - Ok(ExecProgress::Completed(result)) + Ok(ExecProgress::Completed(collect_call_results(&mut self.store.value_stack, &result_ty)?)) } crate::interpreter::ExecState::Suspended(callframe) => { exec_state.callframe = callframe; @@ -425,7 +515,7 @@ fn validate_call_params(func_ty: &FuncType, params: &[WasmValue]) -> Result<()> } if !(func_ty.params().iter().zip(params).all(|(ty, param)| ty == ¶m.into())) { - return Err(Error::Other("Type mismatch".into())); + return Err(Error::other("Type mismatch")); } Ok(()) @@ -533,14 +623,9 @@ macro_rules! impl_scalar_wasm_traits { impl FromWasmValues for $T { #[inline] fn from_wasm_values(values: &[WasmValue]) -> Result { - let value = *values - .first() - .ok_or(Error::Other("Not enough values in WasmValue vector".to_string()))?; + let value = *values.first().ok_or(Error::other("Not enough elemennts in &[WasmValue]"))?; <$T>::try_from(value).map_err(|e| { - Error::Other(format!( - "FromWasmValues: Could not convert WasmValue to expected type: {:?}", - e - )) + Error::Other(format!("FromWasmValues: Could not convert WasmValue to expected type: {e:?}")) }) } } @@ -580,18 +665,10 @@ macro_rules! impl_tuple_traits { fn from_wasm_values(values: &[WasmValue]) -> Result { let mut iter = values.iter(); - Ok(( - $( - $T::try_from( - *iter.next() - .ok_or(Error::Other("Not enough values in WasmValue vector".to_string()))? - ) - .map_err(|e| Error::Other(format!( - "FromWasmValues: Could not convert WasmValue to expected type: {:?}", - e, - )))?, - )+ - )) + Ok(($( + $T::try_from(*iter.next().ok_or(Error::other("Not enough values in WasmValue vector"))?) + .map_err(|e| Error::Other(format!("FromWasmValues: Could not convert WasmValue to expected type: {e:?}")))?, + )+)) } } } diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index dd45f74..f0d995d 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -27,13 +27,14 @@ pub(crate) struct Executor<'store, const BUDGETED: bool> { func: Arc, module: ModuleInstance, store: &'store mut Store, + call_stack_base: u32, } impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { - pub(crate) fn new(store: &'store mut Store, cf: CallFrame) -> Self { + pub(crate) fn new(store: &'store mut Store, cf: CallFrame, call_stack_base: u32) -> Self { let wasm_func = store.state.get_wasm_func(cf.func_addr); let module = store.get_module_instance_internal(wasm_func.owner); - Self { module, cf, func: wasm_func.func.clone(), store } + Self { module, cf, func: wasm_func.func.clone(), store, call_stack_base } } #[inline(always)] @@ -1078,7 +1079,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { fn exec_return(&mut self) -> bool { self.store.value_stack.truncate_keep_counts(self.cf.locals_base, self.func.results); - let Some(caller) = self.store.call_stack.pop() else { + let Some(caller) = self.store.call_stack.pop_frame(self.call_stack_base) else { return true; }; if caller.func_addr == self.cf.func_addr { @@ -1096,7 +1097,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { #[inline(always)] fn finish_return(&mut self) -> bool { - let Some(caller) = self.store.call_stack.pop() else { + let Some(caller) = self.store.call_stack.pop_frame(self.call_stack_base) else { return true; }; if caller.func_addr == self.cf.func_addr { diff --git a/crates/tinywasm/src/interpreter/mod.rs b/crates/tinywasm/src/interpreter/mod.rs index db094e0..cece36a 100644 --- a/crates/tinywasm/src/interpreter/mod.rs +++ b/crates/tinywasm/src/interpreter/mod.rs @@ -26,12 +26,12 @@ pub(crate) enum ExecState { pub(crate) struct InterpreterRuntime; impl InterpreterRuntime { - pub(crate) fn exec(store: &mut Store, cf: CallFrame) -> Result<(), Trap> { - executor::Executor::::new(store, cf).run_to_completion() + pub(crate) fn exec(store: &mut Store, cf: CallFrame, call_stack_base: u32) -> Result<(), Trap> { + executor::Executor::::new(store, cf, call_stack_base).run_to_completion() } pub(crate) fn exec_with_fuel(store: &mut Store, cf: CallFrame, fuel: u32) -> Result { - executor::Executor::::new(store, cf).run_with_fuel(fuel) + executor::Executor::::new(store, cf, 0).run_with_fuel(fuel) } #[cfg(feature = "std")] @@ -40,6 +40,6 @@ impl InterpreterRuntime { cf: CallFrame, time_budget: core::time::Duration, ) -> Result { - executor::Executor::::new(store, cf).run_with_time_budget(time_budget) + executor::Executor::::new(store, cf, 0).run_with_time_budget(time_budget) } } diff --git a/crates/tinywasm/src/interpreter/stack/call_stack.rs b/crates/tinywasm/src/interpreter/stack/call_stack.rs index a307b50..ad71302 100644 --- a/crates/tinywasm/src/interpreter/stack/call_stack.rs +++ b/crates/tinywasm/src/interpreter/stack/call_stack.rs @@ -22,8 +22,18 @@ impl CallStack { } #[inline(always)] - pub(crate) fn pop(&mut self) -> Option { - self.stack.pop() + pub(crate) fn len(&self) -> u32 { + self.stack.len() as u32 + } + + pub(crate) fn truncate_to(&mut self, len: u32) { + debug_assert!(len as usize <= self.stack.len()); + self.stack.truncate(len as usize); + } + + #[inline(always)] + pub(crate) fn pop_frame(&mut self, base: u32) -> Option { + if self.len() == base { None } else { self.stack.pop() } } #[inline(always)] diff --git a/crates/tinywasm/src/interpreter/stack/value_stack.rs b/crates/tinywasm/src/interpreter/stack/value_stack.rs index 014f7a3..ad16467 100644 --- a/crates/tinywasm/src/interpreter/stack/value_stack.rs +++ b/crates/tinywasm/src/interpreter/stack/value_stack.rs @@ -194,6 +194,15 @@ impl ValueStack { self.stack_128.clear(); } + #[inline(always)] + pub(crate) fn base(&self) -> StackBase { + StackBase { + s32: self.stack_32.len() as u32, + s64: self.stack_64.len() as u32, + s128: self.stack_128.len() as u32, + } + } + #[inline(always)] pub(crate) fn len(&self) -> usize { self.stack_32.len() + self.stack_64.len() + self.stack_128.len() diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index 51e0cee..036eaf1 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -353,7 +353,7 @@ impl Table { let init = match (ty.element_type, init) { (WasmType::RefFunc, WasmValue::RefFunc(func_ref)) => TableElement::from(func_ref.addr()), (WasmType::RefExtern, WasmValue::RefExtern(extern_ref)) => TableElement::from(extern_ref.addr()), - _ => return Err(Error::Other("invalid table init value".to_string())), + _ => return Err(Error::other("invalid table init value")), }; let addr = store.state.tables.len() as TableAddr; store.state.tables.push(TableInstance::new_with_init(ty, init)); diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index 446c9ed..1599d81 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -1,5 +1,5 @@ use alloc::sync::Arc; -use alloc::{boxed::Box, format, string::ToString, vec::Vec}; +use alloc::{boxed::Box, format, vec::Vec}; use core::hint::cold_path; use core::sync::atomic::{AtomicUsize, Ordering}; use tinywasm_types::*; @@ -45,6 +45,7 @@ pub struct Store { pub(crate) engine: Engine, pub(crate) execution_fuel: u32, + pub(crate) execution_active: bool, pub(crate) state: State, pub(crate) call_stack: CallStack, pub(crate) value_stack: ValueStack, @@ -73,6 +74,7 @@ impl Store { value_stack: ValueStack::new(engine.config()), engine, execution_fuel: 0, + execution_active: false, } } @@ -91,6 +93,21 @@ impl Store { } } } + + pub(crate) fn enter_execution(&mut self) -> Result<()> { + if self.execution_active { + return Err(Trap::Other( + "cannot call a function while another invocation is active; use FuncContext::call from host functions", + ) + .into()); + } + self.execution_active = true; + Ok(()) + } + + pub(crate) fn exit_execution(&mut self) { + self.execution_active = false; + } } impl PartialEq for Store { @@ -569,7 +586,7 @@ impl Store { RefFunc(Some(idx)) => TinyWasmValue::ValueRef(ValueRef::from_addr(Some(resolve_func(*idx)?))), _ => { cold_path(); - return Err(Error::Other("unsupported const instruction".to_string())); + return Err(Error::other("unsupported const instruction")); } }; @@ -591,14 +608,14 @@ impl Store { } RefExtern(Some(_)) => { cold_path(); - return Err(Error::Other("ref.extern constants are not supported in init expressions".to_string())); + return Err(Error::other("ref.extern constants are not supported in init expressions")); } I32Add | I32Sub | I32Mul => { - let rhs = stack.pop().ok_or_else(|| Error::Other("const stack underflow".to_string()))?; - let lhs = stack.pop().ok_or_else(|| Error::Other("const stack underflow".to_string()))?; + let rhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; + let lhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; let (TinyWasmValue::Value32(lhs), TinyWasmValue::Value32(rhs)) = (lhs, rhs) else { cold_path(); - return Err(Error::Other("type mismatch in const i32 op".to_string())); + return Err(Error::other("type mismatch in const i32 op")); }; let lhs = lhs as i32; let rhs = rhs as i32; @@ -615,7 +632,7 @@ impl Store { let lhs = stack.pop(); let (Some(TinyWasmValue::Value64(lhs)), Some(TinyWasmValue::Value64(rhs))) = (lhs, rhs) else { cold_path(); - return Err(Error::Other("type mismatch in const i64 op".to_string())); + return Err(Error::other("type mismatch in const i64 op")); }; let lhs = lhs as i64; @@ -636,12 +653,12 @@ impl Store { let Some(value) = stack.pop() else { cold_path(); - return Err(Error::Other("empty const expression".to_string())); + return Err(Error::other("empty const expression")); }; if !stack.is_empty() { cold_path(); - return Err(Error::Other("const expression did not reduce to single value".to_string())); + return Err(Error::other("const expression did not reduce to single value")); } Ok(value) } -- cgit v1.3.1