Merge pull request #278 from schungx/master

f32_float feature.
This commit is contained in:
Stephen Chung 2020-11-01 16:11:38 +08:00 committed by GitHub
commit 8fc0f53c84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 140 additions and 76 deletions

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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`, <br/>`u32`, `i32` (default for [`only_i32`]),<br/>`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<String>` or `Arc<String>`) | `"string"` | `"hello"` etc. |

View File

@ -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

View File

@ -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 |

View File

@ -539,7 +539,7 @@ impl AsRef<Module> 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<Box<ModuleRef>>,
pub namespace: Option<ModuleRef>,
/// 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<IdentX>),
/// FnPtr constant.
FnPointer(Box<IdentX>),
/// Variable access - ((variable name, position), optional modules, hash, optional index)
Variable(Box<(Ident, Option<Box<ModuleRef>>, u64, Option<NonZeroUsize>)>),
/// Variable access - (variable name, optional modules, hash, optional index)
Variable(Box<(Ident, Option<ModuleRef>, u64, Option<NonZeroUsize>)>),
/// Property access.
Property(Box<(IdentX, (String, String))>),
/// { stmt }

View File

@ -589,9 +589,9 @@ fn default_print(_s: &str) {
pub fn search_imports<'s>(
mods: &'s Imports,
state: &mut State,
modules: &Box<ModuleRef>,
modules: &ModuleRef,
) -> Result<&'s Module, Box<EvalAltResult>> {
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<ModuleRef>,
modules: &ModuleRef,
) -> Result<&'s mut Module, Box<EvalAltResult>> {
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))?
})
}

View File

@ -1094,7 +1094,7 @@ impl Engine {
state: &mut State,
lib: &[&Module],
this_ptr: &mut Option<&mut Dynamic>,
modules: &Option<Box<ModuleRef>>,
modules: &Option<ModuleRef>,
name: &str,
args_expr: impl AsRef<[Expr]>,
def_val: Option<bool>,

View File

@ -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};

View File

@ -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<NonZeroUsize>);
pub struct ModuleRef(StaticVec<Ident>, Option<NonZeroUsize>);
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<Ident>;
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<StaticVec<(String, Position)>> for ModuleRef {
fn from(modules: StaticVec<(String, Position)>) -> Self {
impl From<StaticVec<Ident>> for ModuleRef {
fn from(modules: StaticVec<Ident>) -> Self {
Self(modules, None)
}
}

View File

@ -224,6 +224,8 @@ gen_signed_functions!(signed_num_128 => i128);
#[cfg(not(feature = "no_float"))]
#[export_module]
mod f32_functions {
#[cfg(not(feature = "f32_float"))]
pub mod basic_arithmetic {
#[rhai_fn(name = "+")]
pub fn add(x: f32, y: f32) -> f32 {
x + y
@ -244,6 +246,11 @@ mod f32_functions {
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<Dynamic, Box<EvalAltResult>> {
Ok(Dynamic::from(x.powf(y)))
}
}
#[rhai_fn(name = "-")]
pub fn neg(x: f32) -> f32 {
-x
@ -261,10 +268,6 @@ mod f32_functions {
}
}
#[rhai_fn(name = "~", return_raw)]
pub fn pow_f_f(x: f32, y: f32) -> Result<Dynamic, Box<EvalAltResult>> {
Ok(Dynamic::from(x.powf(y)))
}
#[rhai_fn(name = "~", return_raw)]
pub fn pow_f_i(x: f32, y: INT) -> Result<Dynamic, Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
Ok(Dynamic::from(x.powf(y)))
}
}
#[rhai_fn(name = "-")]
pub fn neg(x: f64) -> f64 {
-x

View File

@ -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<Dynamic, Box<EvalAltResult>> {
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"))]

View File

@ -265,7 +265,7 @@ fn parse_fn_call(
lib: &mut FunctionsLib,
id: String,
capture: bool,
mut namespace: Option<Box<ModuleRef>>,
mut namespace: Option<ModuleRef>,
settings: ParseSettings,
) -> Result<Expr, ParseError> {
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<Expr, ParseEr
}
// lhs.module::id - syntax error
(_, Expr::Variable(x)) if x.1.is_some() => {
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(_)) => {

View File

@ -34,19 +34,19 @@ fn test_float_parse() -> Result<(), Box<EvalAltResult>> {
fn test_struct_with_float() -> Result<(), Box<EvalAltResult>> {
#[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;
}

View File

@ -81,7 +81,7 @@ fn test_module_resolver() -> Result<(), Box<EvalAltResult>> {
#[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(())
},

View File

@ -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<EvalAltResult>> {

View File

@ -16,8 +16,14 @@ fn test_type_of() -> Result<(), Box<EvalAltResult>> {
assert_eq!(engine.eval::<String>("type_of(60 + 5)")?, "i32");
#[cfg(not(feature = "no_float"))]
{
#[cfg(not(feature = "f32_float"))]
assert_eq!(engine.eval::<String>("type_of(1.0 + 2.0)")?, "f64");
#[cfg(feature = "f32_float")]
assert_eq!(engine.eval::<String>("type_of(1.0 + 2.0)")?, "f32");
}
#[cfg(not(feature = "no_index"))]
assert_eq!(
engine.eval::<String>(r#"type_of([true, 2, "hello"])"#)?,