Expand description
Simple interpreter for ASTs produced by arithmetic-parser.
§How it works
- A
Blockof statements is compiled into anExecutableModule. Internally, compilation processes the AST of the block and transforms it into a non-recusrive form. AnExecutableModulemay require imports (such asNativeFns or constantValues), which can be taken from aVariableMap(e.g., anEnvironment). ExecutableModulecan then be executed, for the return value and/or for the changes at the top-level variable scope. There are two major variables influencing the execution outcome. An arithmetic is used to define arithmetic ops (+, unary and binary-,*,/,^) and comparisons (==,!=,>,<,>=,<=). Imports may be redefined at this stage as well.
§Type system
Values have 5 major types:
- Primitive values corresponding to literals in the parsed
Block - Boolean values
- Functions, which are further subdivided into native functions (defined in the Rust code) and interpreted ones (defined within a module)
- Tuples / arrays.
- Objects.
Besides these types, there is an auxiliary one: OpaqueRef, which represents a
reference-counted native value, which can be returned from native functions or provided to
them as an arg, but is otherwise opaque from the point of view of the interpreted code
(cf. anyref in WASM).
§Semantics
- All variables are immutable. Re-declaring a var shadows the previous declaration.
- Functions are first-class (in fact, a function is just a variant of the
Valueenum). - Functions can capture variables (including other functions). All captures are by value.
- Arithmetic operations are defined on primitive values, tuples and objects. Ops on primitives are defined
via an
Arithmetic. With tuples and objects, operations are performed per element / field. Binary operations require tuples of the same size / objects of the same shape, or a tuple / object and a primitive value. As an example,(1, 2) + 3and#{ x: 2, y: 3 } / #{ x: 4, y: 5 }are valid, but(1, 2) * (3, 4, 5)isn’t. - Methods are considered syntactic sugar for functions, with the method receiver considered
the first function argument. For example,
(1, 2).map(sin)is equivalent tomap((1, 2), sin). - No type checks are performed before evaluation.
- Type annotations and type casts are completely ignored. This means that the interpreter may execute code that is incorrect with annotations (e.g., assignment of a tuple to a variable which is annotated to have a numeric type).
§Value comparisons
Equality comparisons (==, !=) are defined on all types of values.
- For bool values, the comparisons work as expected.
- For functions, the equality is determined by the pointer (2 functions are equal iff they alias each other).
OpaqueRefs either use thePartialEqimpl of the underlying type or the pointer equality, depending on how the reference was created; seeOpaqueRefdocs for more details.- Equality for primitive values is determined by the
Arithmetic. - Tuples are equal if they contain the same number of elements and elements are pairwise equal.
- Different types of values are always non-equal.
Order comparisons (>, <, >=, <=) are defined for primitive values only and use
OrdArithmetic.
§Tuples
- Tuples are created using a
Tupleexpression, e.g.,(x, 1, 5). - Indexing for tuples is performed via
FieldAccesswith a numeric field name:xs.0. Thus, the index is always a “compile-time” constant. An error is raised if the index is out of bounds or the receiver is not a tuple. - Tuples can be destructured using a
DestructureLHS of an assignment, e.g.,(x, y, ...) = (1, 2, 3, 4). An error will be raised if the destructured value is not a tuple, or has an incompatible length.
§Objects
- Objects can be created using object expressions, which are similar to ones in JavaScript.
For example,
#{ x: 1, y: (2, 3) }will create an object with two fields:xequal to 1 andyequal to(2, 3). Similar to Rust / modern JavaScript, shortcut field initialization is available:#{ x, y }will take varsxandyfrom the context. - Object fields can be accessed via
FieldAccesswith a field name that is a valid variable name. No other values have such fields. An error will be raised if the object does not have the specified field. - Objects can be destructured using an
ObjectDestructureLHS of an assignment, e.g.,{ x, y } = obj. An error will be raised if the destructured value is not an object or does not have the specified fields. Destructuring is not exhaustive; i.e., the destructured object may have extra fields. - Functional fields are permitted. Similar to Rust, to call a function field, it must
be enclosed in parentheses:
(obj.run)(arg0, arg1).
§Crate features
std. Enables support of types fromstd, such as theErrortrait, and propagates to dependencies. Importantly,stdis necessary for floating-point arithmetics.complex. ImplementsNumberfor floating-point complex numbers from thenum-complexcrate (i.e.,Complex32andComplex64). Enables complex number parsing inarithmetic-parser.bigint. ImplementsNumberand a couple of other helpers for big integers from thenum-bigintcrate (i.e.,BigIntandBigUint). Enables big integer parsing inarithmetic-parser.
§Examples
use arithmetic_parser::grammars::{F32Grammar, Parse, Untyped};
use arithmetic_eval::{
Assertions, Comparisons, Environment, Prelude, Value, VariableMap,
};
let program = r#"
// The interpreter supports all parser features, including
// function definitions, tuples and blocks.
order = |x, y| (min(x, y), max(x, y));
assert_eq(order(0.5, -1), (-1, 0.5));
(_, M) = order(3^2, { x = 3; x + 5 });
M
"#;
let program = Untyped::<F32Grammar>::parse_statements(program)?;
let mut env = Environment::new();
// Add some native functions to the environment.
env.extend(Prelude.iter());
env.extend(Assertions.iter());
env.extend(Comparisons.iter());
// To execute statements, we first compile them into a module.
let module = env.compile_module("test", &program)?;
// Then, the module can be run.
assert_eq!(module.run()?, Value::Prim(9.0));Using objects:
let program = r#"
minmax = |xs| xs.fold(#{ min: INF, max: -INF }, |acc, x| #{
min: if(x < acc.min, x, acc.min),
max: if(x > acc.max, x, acc.max),
});
assert_eq((3, 7, 2, 4).minmax().min, 2);
assert_eq((5, -4, 6, 9, 1).minmax(), #{ min: -4, max: 9 });
"#;
let program = Untyped::<F32Grammar>::parse_statements(program)?;
let mut env = Environment::new();
env.extend(Prelude.iter());
env.extend(Assertions.iter());
let module = env.compile_module("minmax", &program)?;
module.run()?;More complex examples are available in the examples directory of the crate.
Re-exports§
pub use self::error::Error;pub use self::error::ErrorKind;pub use self::error::EvalResult;
Modules§
- arith
Arithmetictrait and its implementations.- error
- Evaluation errors.
- fns
- Standard functions for the interpreter, and the tools to define new native functions.
Macros§
- wrap_fn
- An alternative for
wrapfunction which works for arguments / return results with non-'staticlifetime. - wrap_
fn_ with_ context - Analogue of
wrap_fnmacro that injects theCallContextas the first argument. This can be used to call functions within the implementation.
Structs§
- Assertions
- Container for assertion functions:
assertandassert_eq. - Call
Context - Context for native function calls.
- Comparisons
- Container with the comparison functions:
cmp,minandmax. - Environment
- Environment containing named
Values. - Executable
Module - Executable module together with its imports.
- Executable
Module Builder - Builder for an
ExecutableModule. - Indexed
Id - Indexed module ID containing a prefix part (e.g.,
snippet). - Interpreted
Fn - Function defined within the interpreter.
- Module
Imports - Imports of an
ExecutableModule. - Opaque
Ref - Opaque reference to a native value.
- Prelude
- Commonly used constants and functions from the
fnsmodule. - Wildcard
Id - Module identifier that has a single possible value, which is displayed as
*. - With
Arithmetic - Container for an
ExecutableModuletogether with anOrdArithmetic.
Enums§
- Function
- Function definition. Functions can be either native (defined in the Rust code) or defined in the interpreter.
- Value
- Values produced by expressions during their interpretation.
- Value
Type - Possible high-level types of
Values.
Traits§
- Compiler
Ext - Compiler extensions defined for some AST nodes, most notably,
Block. - Module
Id - Identifier of an
ExecutableModule. This is usually a “small” type, such as an integer or a string. - Native
Fn - Function on zero or more
Values. - Number
- Marker trait for possible literals.
- Variable
Map - Encapsulates read access to named variables.
Type Aliases§
- Spanned
Value - Value together with a span that has produced it.