diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ffeec22..04b3be8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,7 @@ jobs: - "--features sync" - "--features no_optimize" - "--features no_float" + - "--features f32_float" - "--tests --features only_i32" - "--features only_i64" - "--features no_index" diff --git a/Cargo.toml b/Cargo.toml index 11dc351b..6311ac79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ unchecked = [] # unchecked arithmetic sync = [] # restrict to only types that implement Send + Sync no_optimize = [] # no script optimizer no_float = [] # no floating-point +f32_float = [] # set FLOAT=f32 only_i32 = [] # set INT=i32 (useful for 32-bit systems) only_i64 = [] # set INT=i64 (default) and disable support for all other integer types no_index = [] # no arrays and indexing diff --git a/RELEASES.md b/RELEASES.md index c849496b..d70fcdfd 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -24,6 +24,7 @@ Breaking changes New features ------------ +* `f32_float` feature to set `FLOAT` to `f32`. * Low-level API for custom syntax allowing more flexibility in designing the syntax. * `Module::fill_with` to poly-fill a module with another. diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 84a9e233..101c631d 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -25,6 +25,7 @@ Fast * Fairly low compile-time overhead. * Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). + An unofficial Fibonacci benchmark puts Rhai somewhere between Wren and Python. * Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations. diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 897efbfa..a8b06ea8 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -6,35 +6,45 @@ What Rhai Isn't Rhai's purpose is to provide a dynamic layer over Rust code, in the same spirit of _zero cost abstractions_. It doesn't attempt to be a new language. For example: -* No classes. Well, Rust doesn't either. On the other hand... +* **No classes**. Well, Rust doesn't either. On the other hand... -* No traits... so it is also not Rust. Do your Rusty stuff in Rust. +* **No traits**... so it is also not Rust. Do your Rusty stuff in Rust. -* No structures/records/tuples - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. +* **No structures/records/tuples** - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. There is, however, a built-in [object map] type which is adequate for most uses. It is possible to simulate [object-oriented programming (OOP)][OOP] by storing [function pointers] or [closures] in [object map] properties, turning them into _methods_. -* No first-class functions - Code your functions in Rust instead, and register them with Rhai. +* **No first-class functions** - Code your functions in Rust instead, and register them with Rhai. There is, however, support for simple [function pointers] to allow runtime dispatch by function name. -* No garbage collection - this should be expected, so... +* **No garbage collection** - this should be expected, so... -* No first-class closures - do your closure magic in Rust instead: [turn a Rhai scripted function into a Rust closure]({{rootUrl}}/engine/call-fn.md). +* **No first-class closures** - do your closure magic in Rust instead: [turn a Rhai scripted function into a Rust closure]({{rootUrl}}/engine/call-fn.md). There is, however, support for simulated [closures] via [currying] a [function pointer] with captured shared variables. -* No byte-codes/JIT - Rhai has an AST-walking interpreter which will not win any speed races. - The purpose of Rhai is not to be extremely _fast_, but to make it as easy as possible to +* **No byte-codes/JIT** - Rhai has an optimized AST-walking interpreter which is fast enough for most usage scenarios. + Essential AST data structures are packed and kept together to maximize cache friendliness. + + Functions are dispatched based on pre-calculated hashes and accessing variables are mostly through pre-calculated + offsets to the variables file (a [`Scope`]), so it is seldom necessary to look something up by text name. + + In addition, Rhai's design deliberately avoids maintaining a _scope chain_ so function scopes do not + pay any speed penalty. This particular design also allows variables data to be kept together in a contiguous + block, avoiding allocations and fragmentation while being cache-friendly. In a typical script evaluation run, + no data is shared and nothing is locked. + + Still, the purpose of Rhai is not to be super _fast_, but to make it as easy and versatile as possible to integrate with native Rust applications. -* No formal language grammar - Rhai uses a hand-coded lexer, a hand-coded top-down recursive-descent parser +* **No formal language grammar** - Rhai uses a hand-coded lexer, a hand-coded top-down recursive-descent parser for statements, and a hand-coded Pratt parser for expressions. - This lack of formalism allows the parser itself to be exposed as a service in order to support + This lack of formalism allows the _parser_ itself to be exposed as a service in order to support [disabling keywords/operators][disable keywords and operators], adding [custom operators], and defining [custom syntax]. @@ -45,6 +55,7 @@ Do Not Write The Next 4D VR Game in Rhai Due to this intended usage, Rhai deliberately keeps the language simple and small by omitting advanced language features such as classes, inheritance, interfaces, generics, first-class functions/closures, pattern matching, concurrency, byte-codes VM, JIT etc. +Focus is on _flexibility_ and _ease of use_ instead of raw speed. Avoid the temptation to write full-fledge application logic entirely in Rhai - that use case is best fulfilled by more complete languages such as JavaScript or Lua. diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index f7446913..c8fe9181 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -8,7 +8,7 @@ The following primitive types are supported natively: | Category | Equivalent Rust types | [`type_of()`] | `to_string()` | | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | ----------------------- | | **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | -| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | +| **Floating-point number** (disabled with [`no_float`]) | `f32` (default for [`f32_float`]), `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | | **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | | **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | | **Immutable Unicode [string]** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | diff --git a/doc/src/links.md b/doc/src/links.md index 5093c412..8c2cc211 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -3,6 +3,7 @@ [`sync`]: {{rootUrl}}/start/features.md [`no_optimize`]: {{rootUrl}}/start/features.md [`no_float`]: {{rootUrl}}/start/features.md +[`f32_float`]: {{rootUrl}}/start/features.md [`only_i32`]: {{rootUrl}}/start/features.md [`only_i64`]: {{rootUrl}}/start/features.md [`no_index`]: {{rootUrl}}/start/features.md diff --git a/doc/src/start/features.md b/doc/src/start/features.md index e82b1df1..60508ed4 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -17,6 +17,7 @@ more control over what a script can (or cannot) do. | `sync` | no | restricts all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync` | | `no_optimize` | no | disables [script optimization] | | `no_float` | no | disables floating-point numbers and math | +| `f32_float` | no | sets the system floating-point type to `f32` instead of `f64` | | `only_i32` | no | sets the system integer type to `i32` and disable all other integer types. `INT` is set to `i32` | | `only_i64` | no | sets the system integer type to `i64` and disable all other integer types. `INT` is set to `i64` | | `no_index` | no | disables [arrays] and indexing features | diff --git a/src/ast.rs b/src/ast.rs index 4a82fb67..bb99d2f3 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -539,7 +539,7 @@ impl AsRef for AST { } /// An identifier containing a string name and a position. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Ident { pub name: String, pub pos: Position, @@ -553,7 +553,7 @@ impl Ident { } /// An identifier containing an immutable name and a position. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct IdentX { pub name: ImmutableString, pub pos: Position, @@ -866,7 +866,7 @@ pub struct FnCallInfo { /// and the function names are predictable, so no need to allocate a new `String`. pub name: Cow<'static, str>, /// Namespace of the function, if any. - pub namespace: Option>, + pub namespace: Option, /// Call native functions only? Set to `true` to skip searching for script-defined function overrides /// when it is certain that the function must be native (e.g. an operator). pub native_only: bool, @@ -903,8 +903,8 @@ pub enum Expr { StringConstant(Box), /// FnPtr constant. FnPointer(Box), - /// Variable access - ((variable name, position), optional modules, hash, optional index) - Variable(Box<(Ident, Option>, u64, Option)>), + /// Variable access - (variable name, optional modules, hash, optional index) + Variable(Box<(Ident, Option, u64, Option)>), /// Property access. Property(Box<(IdentX, (String, String))>), /// { stmt } diff --git a/src/engine.rs b/src/engine.rs index b08164ba..ee635ed8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -589,9 +589,9 @@ fn default_print(_s: &str) { pub fn search_imports<'s>( mods: &'s Imports, state: &mut State, - modules: &Box, + modules: &ModuleRef, ) -> Result<&'s Module, Box> { - let (root, root_pos) = &modules[0]; + let Ident { name: root, pos } = &modules[0]; // Qualified - check if the root module is directly indexed let index = if state.always_search { @@ -608,7 +608,7 @@ pub fn search_imports<'s>( .rev() .find(|(n, _)| n == root) .map(|(_, m)| m) - .ok_or_else(|| EvalAltResult::ErrorModuleNotFound(root.to_string(), *root_pos))? + .ok_or_else(|| EvalAltResult::ErrorModuleNotFound(root.to_string(), *pos))? }) } @@ -617,9 +617,9 @@ pub fn search_imports<'s>( pub fn search_imports_mut<'s>( mods: &'s mut Imports, state: &mut State, - modules: &Box, + modules: &ModuleRef, ) -> Result<&'s mut Module, Box> { - let (root, root_pos) = &modules[0]; + let Ident { name: root, pos } = &modules[0]; // Qualified - check if the root module is directly indexed let index = if state.always_search { @@ -636,7 +636,7 @@ pub fn search_imports_mut<'s>( .rev() .find(|(n, _)| n == root) .map(|(_, m)| m) - .ok_or_else(|| EvalAltResult::ErrorModuleNotFound(root.to_string(), *root_pos))? + .ok_or_else(|| EvalAltResult::ErrorModuleNotFound(root.to_string(), *pos))? }) } diff --git a/src/fn_call.rs b/src/fn_call.rs index 78c381a3..0ae48900 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -1094,7 +1094,7 @@ impl Engine { state: &mut State, lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, - modules: &Option>, + modules: &Option, name: &str, args_expr: impl AsRef<[Expr]>, def_val: Option, diff --git a/src/lib.rs b/src/lib.rs index 7889c553..621aea86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,8 +99,16 @@ pub type INT = i32; /// /// Not available under the `no_float` feature. #[cfg(not(feature = "no_float"))] +#[cfg(not(feature = "f32_float"))] pub type FLOAT = f64; +/// The system floating-point type. +/// +/// Not available under the `no_float` feature. +#[cfg(not(feature = "no_float"))] +#[cfg(feature = "f32_float")] +pub type FLOAT = f32; + pub use ast::AST; pub use dynamic::Dynamic; pub use engine::{Engine, EvalContext}; diff --git a/src/module/mod.rs b/src/module/mod.rs index 907db6f0..79dbe625 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1,6 +1,6 @@ //! Module defining external-loaded modules for Rhai. -use crate::ast::FnAccess; +use crate::ast::{FnAccess, Ident}; use crate::dynamic::{Dynamic, Variant}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, NativeCallContext, SendSync}; use crate::fn_register::by_value as cast_arg; @@ -1502,7 +1502,7 @@ impl Module { /// /// This type is volatile and may change. #[derive(Clone, Eq, PartialEq, Default, Hash)] -pub struct ModuleRef(StaticVec<(String, Position)>, Option); +pub struct ModuleRef(StaticVec, Option); impl fmt::Debug for ModuleRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -1517,7 +1517,7 @@ impl fmt::Debug for ModuleRef { } impl Deref for ModuleRef { - type Target = StaticVec<(String, Position)>; + type Target = StaticVec; fn deref(&self) -> &Self::Target { &self.0 @@ -1532,15 +1532,15 @@ impl DerefMut for ModuleRef { impl fmt::Display for ModuleRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (m, _) in self.0.iter() { - write!(f, "{}{}", m, Token::DoubleColon.syntax())?; + for Ident { name, .. } in self.0.iter() { + write!(f, "{}{}", name, Token::DoubleColon.syntax())?; } Ok(()) } } -impl From> for ModuleRef { - fn from(modules: StaticVec<(String, Position)>) -> Self { +impl From> for ModuleRef { + fn from(modules: StaticVec) -> Self { Self(modules, None) } } diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 3f04b50d..d061ceeb 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -224,25 +224,32 @@ gen_signed_functions!(signed_num_128 => i128); #[cfg(not(feature = "no_float"))] #[export_module] mod f32_functions { - #[rhai_fn(name = "+")] - pub fn add(x: f32, y: f32) -> f32 { - x + y - } - #[rhai_fn(name = "-")] - pub fn subtract(x: f32, y: f32) -> f32 { - x - y - } - #[rhai_fn(name = "*")] - pub fn multiply(x: f32, y: f32) -> f32 { - x * y - } - #[rhai_fn(name = "/")] - pub fn divide(x: f32, y: f32) -> f32 { - x / y - } - #[rhai_fn(name = "%")] - pub fn modulo(x: f32, y: f32) -> f32 { - x % y + #[cfg(not(feature = "f32_float"))] + pub mod basic_arithmetic { + #[rhai_fn(name = "+")] + pub fn add(x: f32, y: f32) -> f32 { + x + y + } + #[rhai_fn(name = "-")] + pub fn subtract(x: f32, y: f32) -> f32 { + x - y + } + #[rhai_fn(name = "*")] + pub fn multiply(x: f32, y: f32) -> f32 { + x * y + } + #[rhai_fn(name = "/")] + pub fn divide(x: f32, y: f32) -> f32 { + x / y + } + #[rhai_fn(name = "%")] + pub fn modulo(x: f32, y: f32) -> f32 { + x % y + } + #[rhai_fn(name = "~", return_raw)] + pub fn pow_f_f(x: f32, y: f32) -> Result> { + Ok(Dynamic::from(x.powf(y))) + } } #[rhai_fn(name = "-")] pub fn neg(x: f32) -> f32 { @@ -261,10 +268,6 @@ mod f32_functions { } } #[rhai_fn(name = "~", return_raw)] - pub fn pow_f_f(x: f32, y: f32) -> Result> { - Ok(Dynamic::from(x.powf(y))) - } - #[rhai_fn(name = "~", return_raw)] pub fn pow_f_i(x: f32, y: INT) -> Result> { if cfg!(not(feature = "unchecked")) && y > (i32::MAX as INT) { Err(make_err(format!( @@ -280,6 +283,33 @@ mod f32_functions { #[cfg(not(feature = "no_float"))] #[export_module] mod f64_functions { + #[cfg(feature = "f32_float")] + pub mod basic_arithmetic { + #[rhai_fn(name = "+")] + pub fn add(x: f64, y: f64) -> f64 { + x + y + } + #[rhai_fn(name = "-")] + pub fn subtract(x: f64, y: f64) -> f64 { + x - y + } + #[rhai_fn(name = "*")] + pub fn multiply(x: f64, y: f64) -> f64 { + x * y + } + #[rhai_fn(name = "/")] + pub fn divide(x: f64, y: f64) -> f64 { + x / y + } + #[rhai_fn(name = "%")] + pub fn modulo(x: f64, y: f64) -> f64 { + x % y + } + #[rhai_fn(name = "~", return_raw)] + pub fn pow_f_f(x: f64, y: f64) -> Result> { + Ok(Dynamic::from(x.powf(y))) + } + } #[rhai_fn(name = "-")] pub fn neg(x: f64) -> f64 { -x diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index c723d853..ef55765f 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -215,10 +215,6 @@ mod float_functions { Ok((x.trunc() as INT).into()) } } - #[rhai_fn(name = "to_float")] - pub fn f32_to_float(x: f32) -> FLOAT { - x as FLOAT - } #[rhai_fn(name = "to_int", return_raw)] pub fn f64_to_int(x: f64) -> Result> { if cfg!(not(feature = "unchecked")) && x > (MAX_INT as f64) { @@ -244,6 +240,13 @@ mod float_functions { .into() }) } + #[cfg(not(feature = "f32_float"))] + pub mod f32_f64 { + #[rhai_fn(name = "to_float")] + pub fn f32_to_f64(x: f32) -> f64 { + x as f64 + } + } } #[cfg(not(feature = "no_float"))] diff --git a/src/parser.rs b/src/parser.rs index ebdbe625..7acef0d1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -265,7 +265,7 @@ fn parse_fn_call( lib: &mut FunctionsLib, id: String, capture: bool, - mut namespace: Option>, + mut namespace: Option, settings: ParseSettings, ) -> Result { let (token, token_pos) = input.peek().unwrap(); @@ -292,7 +292,7 @@ fn parse_fn_call( let hash_script = if let Some(modules) = namespace.as_mut() { #[cfg(not(feature = "no_module"))] - modules.set_index(state.find_module(&modules[0].0)); + modules.set_index(state.find_module(&modules[0].name)); // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -300,7 +300,7 @@ fn parse_fn_call( // 2) Calculate a second hash with no qualifiers, empty function name, // zero number of arguments, and the actual list of argument `TypeId`'s. // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + let qualifiers = modules.iter().map(|m| m.name.as_str()); calc_script_fn_hash(qualifiers, &id, 0) } else { // Qualifiers (none) + function name + no parameters. @@ -339,7 +339,7 @@ fn parse_fn_call( let hash_script = if let Some(modules) = namespace.as_mut() { #[cfg(not(feature = "no_module"))] - modules.set_index(state.find_module(&modules[0].0)); + modules.set_index(state.find_module(&modules[0].name)); // Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -347,7 +347,7 @@ fn parse_fn_call( // 2) Calculate a second hash with no qualifiers, empty function name, // zero number of arguments, and the actual list of argument `TypeId`'s. // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + let qualifiers = modules.iter().map(|m| m.name.as_str()); calc_script_fn_hash(qualifiers, &id, args.len()) } else { // Qualifiers (none) + function name + number of arguments. @@ -891,14 +891,14 @@ fn parse_primary( // module access (Expr::Variable(x), Token::DoubleColon) => match input.next().unwrap() { (Token::Identifier(id2), pos2) => { - let (Ident { name, pos }, mut modules, _, index) = *x; + let (var_name_def, mut modules, _, index) = *x; if let Some(ref mut modules) = modules { - modules.push((name, pos)); + modules.push(var_name_def); } else { let mut m: ModuleRef = Default::default(); - m.push((name, pos)); - modules = Some(Box::new(m)); + m.push(var_name_def); + modules = Some(m); } Expr::Variable(Box::new((Ident::new(id2, pos2), modules, 0, index))) @@ -929,10 +929,10 @@ fn parse_primary( let modules = modules.as_mut().unwrap(); // Qualifiers + variable name - *hash = calc_script_fn_hash(modules.iter().map(|(v, _)| v.as_str()), name, 0); + *hash = calc_script_fn_hash(modules.iter().map(|v| v.name.as_str()), name, 0); #[cfg(not(feature = "no_module"))] - modules.set_index(state.find_module(&modules[0].0)); + modules.set_index(state.find_module(&modules[0].name)); } _ => (), } @@ -1206,7 +1206,7 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - return Err(PERR::PropertyExpected.into_err(x.1.unwrap()[0].1)); + return Err(PERR::PropertyExpected.into_err(x.1.unwrap()[0].pos)); } // lhs.prop (lhs, prop @ Expr::Property(_)) => { diff --git a/tests/float.rs b/tests/float.rs index 534115e3..1e973e29 100644 --- a/tests/float.rs +++ b/tests/float.rs @@ -34,19 +34,19 @@ fn test_float_parse() -> Result<(), Box> { fn test_struct_with_float() -> Result<(), Box> { #[derive(Clone)] struct TestStruct { - x: f64, + x: FLOAT, } impl TestStruct { fn update(&mut self) { - self.x += 5.789_f64; + self.x += 5.789; } - fn get_x(&mut self) -> f64 { + fn get_x(&mut self) -> FLOAT { self.x } - fn set_x(&mut self, new_x: f64) { + fn set_x(&mut self, new_x: FLOAT) { self.x = new_x; } diff --git a/tests/modules.rs b/tests/modules.rs index e43b8e71..ab51f4e4 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -81,7 +81,7 @@ fn test_module_resolver() -> Result<(), Box> { #[cfg(not(feature = "no_float"))] module.set_fn_4_mut( "sum_of_three_args".to_string(), - |target: &mut INT, a: INT, b: INT, c: f64| { + |target: &mut INT, a: INT, b: INT, c: rhai::FLOAT| { *target = a + b + c as INT; Ok(()) }, diff --git a/tests/power_of.rs b/tests/power_of.rs index a3b6fd43..2843c37b 100644 --- a/tests/power_of.rs +++ b/tests/power_of.rs @@ -4,7 +4,7 @@ use rhai::{Engine, EvalAltResult, INT}; use rhai::FLOAT; #[cfg(not(feature = "no_float"))] -const EPSILON: FLOAT = 0.000_000_000_1; +const EPSILON: FLOAT = 0.000_001; #[test] fn test_power_of() -> Result<(), Box> { diff --git a/tests/types.rs b/tests/types.rs index d5a5928e..df72afdd 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -16,7 +16,13 @@ fn test_type_of() -> Result<(), Box> { assert_eq!(engine.eval::("type_of(60 + 5)")?, "i32"); #[cfg(not(feature = "no_float"))] - assert_eq!(engine.eval::("type_of(1.0 + 2.0)")?, "f64"); + { + #[cfg(not(feature = "f32_float"))] + assert_eq!(engine.eval::("type_of(1.0 + 2.0)")?, "f64"); + + #[cfg(feature = "f32_float")] + assert_eq!(engine.eval::("type_of(1.0 + 2.0)")?, "f32"); + } #[cfg(not(feature = "no_index"))] assert_eq!(