14 releases

0.8.1 Mar 13, 2023
0.7.3 Nov 3, 2022
0.7.2 Jul 18, 2022
0.5.2 Mar 28, 2022

#1905 in Parser implementations

32 downloads per month

MIT/Apache

310KB
6K SLoC

Rust 5K SLoC // 0.0% comments JavaScript 1K SLoC // 0.1% comments Pest 66 SLoC

lavendeux-parser

Lavendeux Parser - Extensible inline parser engine

Crates.io Build Status License

lavendeux-parser is an exensible parsing engine for mathematical expressions. It supports variable and function assignments, a variety of datatypes, and can be extended easily at runtime through extensions written in javascript.

Extensions are run in a sandboxed environment with no host or network access. This project is the engine behind Lavendeux.

Getting Started

To use it, create a ParserState object, and use it to tokenize input with Token::new:

use lavendeux_parser::{ParserState, ParserError, Token, Value};

fn main() -> Result<(), ParserError> {
    // Create a new parser, and tokenize 2 lines
    let mut state : ParserState = ParserState::new();
    let lines = Token::new("x=9\nsqrt(x) @bin", &mut state)?;

    // The resulting token contains the resulting values and text
    assert_eq!(lines.text(), "9\n0b11");
    assert_eq!(lines.child(1).unwrap().value(), Value::Float(3.0));

    Ok(())
}

The result will be a Token object:

use lavendeux_parser::{ParserState, ParserError, Token, Value};

fn main() -> Result<(), ParserError> {
    let mut state : ParserState = ParserState::new();
    let lines = Token::new("x=9\nsqrt(x) @bin", &mut state)?;

    // String representation of the full result
    assert_eq!(lines.text(), "9\n0b11");

    // String representation of the first line's result
    assert_eq!(lines.child(0).unwrap().text(), "9");

    // Actual value of the first line's result
    // Values are integers, floats, booleans or strings
    let value = lines.child(0).unwrap().value();
    assert_eq!(value.as_int().unwrap(), 9);
    assert_eq!(true, matches!(value, Value::Integer(_)));

    Ok(())
}

A number of functions and @decorators are available for expressions to use - add more using the state:

use lavendeux_parser::{ParserState, ParserError, DecoratorDefinition, FunctionDefinition, FunctionArgument, Value};
use lavendeux_parser::errors::*;

let mut state : ParserState = ParserState::new();
state.decorators.register(DecoratorDefinition {
    name: &["upper", "uppercase"],
    description: "Outputs an uppercase version of the input",
    argument: ExpectedTypes::Any,
    handler: |_, input| Ok(input.as_string().to_uppercase())
});

// Functions take in an array of values, and return a single value
state.functions.register(FunctionDefinition {
    name: "echo",
    category: None,
    description: "Echo back the provided input",
    arguments: || vec![
        FunctionArgument::new_required("input", ExpectedTypes::String),
    ],
    handler: |_function, _state, args| {
        Ok(Value::String(args.get("input").required().as_string()))
    }
});

// Expressions being parsed can now call new_function(), and use the @new_decorator

Javascript extensions give a flexible way of adding functionality at runtime. Extensions are run in a sandboxed environment, with no network or host access. An extension must implement an extension() function taking no arguments and returning an object describing the extension - see example below

/**
* This function tells Lavendeux about this extension.
* It must return an object similar to the one below.
* @returns Object
*/
function extension() }
    return {
        name: "Extension Name",
        author: "Author's name",
        version: "0.0.0",

        functions: {,
            "callable_name": "js_function_name"
        },

        decorators: {,
            "callable_name": "js_decorator_name"
        },
    }
}

/**
* This function can be called from Lavendeux as callable_name(...)
* args is an array of value objects with either the key Integer, Float or String
* It must also return an object of that kind, or throw an exception
* @returns Object
*/
function js_function_name(args) }
    return {
        "Integer": 5,
    };
}

/**
* This decorator can be called from Lavendeux as @callable_name
* arg is a value object with either the key Integer, Float or String
* It must return a string, or throw an exception
* @returns String
*/
function js_decorator_name(arg) {
    return "formatted value";
}

Using Extensions

Extensions are enabled by default, and can be excluded by disabling the crate's "extensions" feature

Extensions can be loaded as follows:

use lavendeux_parser::{ParserState, ParserError, Value, Token};

fn main() -> Result<(), ParserError> {
    let mut state : ParserState = ParserState::new();

    // Load one extension
    state.extensions.load("example_extensions/colour_utils.js")?;

    // Load a whole directory
    state.extensions.load_all("./example_extensions")?;

    // Once loaded, functions and @decorators decribed in the extensions
    // can be called in expressions being parsed
    let token = Token::new("complement(0xFF0000) @colour", &mut state)?;
    assert_eq!(token.text(), "#ffff00");
    Ok(())
}

Syntax

Expressions can be composed of integers, floats, strings, as well as numbers of various bases:

// Integer, floating point or scientific notation numbers
5 + 5.56 + .2e+3

// Currency values
// Note that no exchange rate is being applied automatically
$1,000.00 == ¥1,000.00

// Scientific numbers can be represented a number of ways
5.6e+7 - .6E7 + .2e-3

// Booleans
in_range = 5 > 3 && 5 < 10
true || false

// Integers can also be represented in base 2, 8 or 16
0xFFA & 0b110 & 0777

// Strings are also supported
concat("foo", "bar")

// Arrays can be composed of any combination of types
[10, 12] + [1.2, 1.3]
2 * [10, 5] // Operations can also be applied between scalar values and arrays
[false, 0, true] == true // An array evaluates to true if any element is true

Beyond the simpler operators, the following operations are supported:

5 ** 2 // Exponentiation
6 % 2 // Modulo
3! // Factorial

// Bitwise operators AND, OR, and XOR:
0xF & 0xA | 0x2 ^ 0xF

// Bitwise SHIFT, and NOT
0xF << 1
0x1 >> 2
~0xA

// Boolean operators
true || false && true
1 < 2 > 5 // true

You can also assign values to variables to be used later: They are case sensitive, and can be composed of underscores or alphanumeric characters

// You can also assign values to variables to be used later
x = 0xFFA & 0xFF0
x - 55 // The result will be 200

// A few constants are also pre-defined
value = pi * e * tau

// You can also define functions
f(x) = 2*x**2 + 3*x + 5
f(2.3)

// Functions work well with arrays
sum(a) = element(a, 0) + ( len(a)>1 ? sum(dequeue(a)) : 0 )
sum([10, 10, 11])

// Recursive functions work too!
factorial(x) = x==0 ? 1 : (x * factorial(x - 1) )
factorial(5)

Decorators can be put at the end of a line to change the output format. Valid decorators include:

255 @hex // The result will be 0xFF
8 @oct // The result will be 0o10
5 @float // The result will be 5.0
5 @usd // Also works with @dollars @cad, @aud, @yen, @pounds, or @euros
1647950086 @utc // 2022-03-22 11:54:46

The following functions are supported by default:

    Math Functions
    ===============
    abs(n): Returns the absolute value of n
    acos(n): Calculate the arccosine of n
    array(n): Returns a value as an array
    asin(n): Calculate the arcsine of n
    atan(n): Calculate the arctangent of n
    bool(n): Returns a value as a boolean
    ceil(n): Returns the nearest whole integer larger than n
    cos(n): Calculate the cosine of n
    cosh(n): Calculate the hyperbolic cosine of n
    float(n): Returns a value as a float
    floor(n): Returns the nearest whole integer smaller than n
    int(n): Returns a value as an integer
    ln(n): Returns the natural log of n
    log(n, base): Returns the logarithm of n in any base
    log10(n): Returns the base 10 log of n
    max(n1, n2): Returns the largest numeric value from the supplied arguments
    min(n1, n2): Returns the smallest numeric value from the supplied arguments
    root(n, base): Returns a root of n of any base
    round(n, [precision]): Returns n, rounded to [precision] decimal places
    sin(n): Calculate the sine of n
    sinh(n): Calculate the hyperbolic sine of n
    sqrt(n): Returns the square root of n
    tan(n): Calculate the tangent of n
    tanh(n): Calculate the hyperbolic tangent of n
    to_degrees(n): Convert the given radian value into degrees
    to_radians(n): Convert the given degree value into radians

    Misc Functions
    ===============
    call(filename): Run the contents of a file as a script
    help([function_name]): Display a help message
    run(expression): Run a string as an expression
    tail(filename, [lines]): Returns the last [lines] lines from a given file
    time(): Returns a unix timestamp for the current system time

    Network Functions
    ===============
    api(name, [endpoint]): Make a call to a registered API
    api_delete(name): Remove a registered API from the list
    api_list(): List all registered APIs
    api_register(name, base_url, [api_key]): Register a new API for quick usage
    get(url, [headers1, headers2]): Return the resulting text-format body of an HTTP GET call
    post(url, body, [header-name=value1, header-name=value2]): Return the resulting text-format body of an HTTP POST call
    resolve(hostname): Returns the IP address associated to a given hostname

    Cryptography Functions
    ===============
    choose(option1, option2): Returns any one of the provided arguments at random
    md5(input1, input2): Returns the MD5 hash of a given string
    rand([m], [n]): With no arguments, return a float from 0 to 1. Otherwise return an integer from 0 to m, or m to n
    sha256(input1, input2): Returns the SHA256 hash of a given string

    Arrays Functions
    ===============
    dequeue(array): Remove the first element from an array
    element(array, index): Return an element from a location in an array
    enqueue(array, element): Add an element to the end of an array
    is_empty(array): Returns true if the given array is empty
    len(array): Returns the length of the given array
    merge(arrays1, arrays2): Merge all given arrays
    pop(array): Remove the last element from an array
    push(array, element): Add an element to the end of an array
    remove(array, index): Removes an element from an array

    Strings Functions
    ===============
    concat([s1, s2]): Concatenate a set of strings
    contains(source, s): Returns true if array or string [source] contains [s]
    lowercase(s): Converts the string s to lowercase
    regex(pattern, subject, [group]): Returns a regular expression match from [subject], or false
    strlen(s): Returns the length of the string s
    substr(s, start, [length]): Returns a substring from s, beginning at [start], and going to the end, or for [length] characters
    trim(s): Trim whitespace from a string
    uppercase(s): Converts the string s to uppercase

    Built-in Decorators
    ===============
    @array: Format a number as an array
    @aud: Format a number as a dollar amount
    @bin: Base 2 number formatting, such as 0b11
    @bool: Format a number as a boolean
    @boolean: Format a number as a boolean
    @cad: Format a number as a dollar amount
    @default: Default formatter, type dependent
    @dollar: Format a number as a dollar amount
    @dollars: Format a number as a dollar amount
    @euro: Format a number as a euro amount
    @euros: Format a number as a euro amount
    @float: Format a number as floating point
    @hex: Base 16 number formatting, such as 0xFF
    @int: Format a number as an integer
    @integer: Format a number as an integer
    @oct: Base 8 number formatting, such as 0b77
    @pound: Format a number as a pound amount
    @pounds: Format a number as a pound amount
    @sci: Scientific number formatting, such as 1.2Ee-3
    @usd: Format a number as a dollar amount
    @utc: Interprets an integer as a timestamp, and formats it in UTC standard
    @yen: Format a number as a yen amount

License: MIT OR Apache-2.0

Dependencies

~10–30MB
~496K SLoC