expression_parser

TODO: think of a new name.

Github Pages

Why

  • Library first
  • JSON support (copy paste your json, should be valid code)
  • Immutable
  • Compiled code externaly saveable (using serde)
  • No external calls build-in (you can add those yourself if you want)

Non Goals

  • Speed, speed is nice but not a goal

Examples

Take a look at the expression example:

cargo run --example expression 1 + 12

Or even better use the REPL:

cargo run --example repl

For syntax check the examples page and the rest of the Github Pages

library usage

Simple example:


#![allow(unused)]
fn main() {
use expression_parser::{Environment, ExpressionFile};

let result = ExpressionFile::run("1 + 5 - 2",  &mut Environment::default());

assert_eq!(Ok(4.0.into()), result);
}

Example with variable assignment:


#![allow(unused)]
fn main() {
use expression_parser::{Environment, ExpressionFile, ExpressionValue};

let input = r#"
    a = [1, 2, 3];
    b = [3, 2, 1];
    c = concat(a, b);
    d = concat(b, a);
    concat(c, [4,4], d);
"#;
let file = ExpressionFile::parse(input)?;
let evaluated = ExpressionFile::eval(file, &mut Environment::default());
assert_eq!(
    ExpressionValue::from(vec![
        1, 2, 3, 3, 2, 1, 4,
        4, 3, 2, 1, 1, 2, 3
    ]),
    evaluated.unwrap()
);
}

For better examples take a look at the library usage page

Introduction

Basic types

Types are nearly equal to JSON types:

  • String
"Text";
"More text";
// unicode rust
"ok\u{00e9}"; // prints oké
// also json unicode
"ok\u00e9"; // prints oké as well
"prints\t\tescaped\n\"characters\"\\";
// prints         escaped
// "characters"\
  • Number
123;
12.345;
1.23e6; // 1230000
1.23e-3; // 0.00123
  • Boolean
true;
false;
  • Null
null;
  • List
[ 1, 2, 3 ];
["test", "test"];
// also mixed type lists
[1, "test", false];
  • Map
// just your regular json
{
    "test": 123,
    "list": [1,2,3],
    "map": {
        "nested": true
    }
}

Extra Types:

  • Function
// simple example
{x => x + x}

Variables

Creating new variables can be done just by using the = symbol.

Example:

a = 1;
b = 2;
c = a + b;

Currently there is no garbage collection, but if you want you can manually delete variables with:

a = 1;
unset a;

Built-in Functions

Add

Adds the two arguments together

Usage:

    first = 1 + 1 == 2;
    second = 123 + 321 == 444;
    third = -321 + 321 == 0;

All

Checks if all arguments are truthy

Usage:

    first = all([1, true, [1,2,3], {"test": true}]);
    second = all([1, false, [1,2,3], {"test": true}]) == false;

And

If the first argument is truthy returns the second argument, otherwise returns the first argument

Usage:

    first = true and true;
    second = 1 and true;
    third = [1,2,3] and true;
    fourth = 0 and 1234 == 0;

Any

Checks if any arguments are truthy

Usage:

    first = any([1, false, [1,2,3], {"test": true}]);
    second = any([0.0, true, [], {}]);
    third = any([0.0, false, [], {}]) == false;

Assert

Raises an error with the given message if the first argument is not truthy

Usage:

    one = assert(true == true);
    two = assert([1,2,3]) == [1,2,3];
    # with an optional error message
    three = assert(1, "not truthy") == 1;
    four = try(assert(1 == 0, "panic"), "default") == "default";

Call

Calls the function with given arguments

Usage:

    my_function = {x => x + 25};
    one = my_function.(5) == 30;
    # the same as:
    two = call(my_function, 5) == 30;
    one and two  

Concat

Combines two or more lists into one

Usage:

    first = concat([1,2], [3,4], [5,6]) == [1,2,3,4,5,6];
    second = concat("test", "-", "test") == "test-test";

Contains

Checks if the seconds argument is in the first argument

Usage:

    first = contains("testing", "test");
    second = contains([1,2,3,4], 3);

Cos

Calculates the cosine of the number

Usage:

    first = cos(0) == 1;

Div

Divides the second argument from the first argument

Usage:

    first = 1 / 5 == 0.2;
    second = 50 / 50 == 1;
    third = 150 / 6 == 25;

Equal

Compares if the two arguments are equal

Usage:

    first = 1 == 1; 
    second = [1,2,3] == [1,2,3]; 
    third = {"test": true} == {"test": true}; 

Error

Raises an error with the given message

Usage:

    # an error with "something went wrong" will be catched and returns "default"
    one = try(error("something went wrong"), "default") == "default";

Format

Formats the arguments into the template, only positional arguments are supported.

Usage:

    one = format("{} + {} = {}", 1, 1, 2) == "1 + 1 = 2";
    two = format("{2} - {1} = {0}", 5, 8, 13) == "13 - 8 = 5";
    three = format("{}{}", "abc", "def") == "abcdef";

    one and two and three

Get

Gets the value from a list or a map

Usage:

    first = get([1, 2, 3], 1) == 2;
    // throws error:
    // get([1, 2, 3], -1.23) != 2;

    second = get([1, 2, 3], 1.5 - 0.5) == 2;
    third = get({"test": 12, "another": -1}, "test") == 12;
    // throws error:
    // get({"test": 1}, "another")

Greater

Compares if left is greater than right

Usage:

    first = 2 > 1; 

Help

Prints help message

Usage:

    // prints general help
    help();

    // prints help for specific function
    help(contains);

    // print all the functions
    help(functions);

If

If the first argument is truthy returns the second argument, otherwise returns the third argument

Usage:

    first = if(1 == 1, "success", "failed") == "success";
    
    one = "1";
    two = "2";
    some_test = e != pi;
    second = if(some_test, one, two) == one;

    // if function can also take a function
    third = if(1 == 1, {=>
        1 + 2
    }, {=>
        "error"
    }) == 3;

Join

Combine the first argument into a string with the second argument as is seperator

Usage:

    first = join(["1", "2", "3"], ", ") == "1, 2, 3";
    second = join(["test", "testing", "test"], "-") == "test-testing-test";

Length

Returns the length of map of list

Usage:

    first = length(["1", "2", "3"]) == 3;
    second = length({"test": 1, "another": 3}) == 2;
    third = length(range(15)) == 15;

Lesser

Compares if left is less than right

Usage:

    first = 1 < 2; 

Lower

Converts input to lowercase

Usage:

    first = lower("TESTING") == "testing";

Mul

Multiplies the two arguments together

Usage:

    first = 1 * 1 == 1;
    second = 150 * 0 == 0;
    third = -5 * -5 == 25;

Not equal

Compares if the two arguments are not equal

Usage:

    first = 1 != 2; 
    second = [1,2,3] != [3,2,1]; 
    third = {"test": true} != {"test": false}; 

Now

Returns the unix timestamp of the current time

Usage:

    now();

Or

If the first argument is truthy returns the first argument, otherwise returns the second argument

Usage:

    first = false or true;
    second = 0.0 or true;
    third = [] or true;

Pow

Raises the first argument to the second argument

Usage:

    first = 1 ** 1 == 1;
    second = 2 ^ 3 == 8;
    third = 16 ** 0.5 == 4;

Print

Prints value

Usage:

    // prints the item to configured logger
    one = print({"test": 1}) == {"test": 1};
            
    one

Product

Calculates the product of the arguments

Usage:

    first = product(1,2,3,4,5) == 120;
    second = product([1,2,3,4,5]) == 120;

Push

Push value to the list

Usage:

    first = push([1, 2, 3], 1) == [1,2,3,1];
    second = push([1, 2, 3], [1, 2, 3]) == [1,2,3,[1,2,3]];

Put

Put third argument into the map or list under the second argument

Usage:

    first = put({}, "test", 123) == {"test": 123};
    // overwrites existing key
    second = put({"test": 23}, "test", 5) == {"test": 5};
    third =  put({"test": 23}, "test", put({"nested": 23}, "nested", 5)) == {"test": {"nested": 5}};
    // put replaces the value at the index
    four = put([1, 2, 3], 2, 15) == [1, 2, 15];
    

Random

Generate a random number. Defaults to a number between 0 and 1, but the range can be set as argument

Usage:

    // random() returns a random number between 0 and 1
    first = random() != random();
    second = random(-1, 1) != random(2, 5);
    third = random(-5) != random(5);

Range

Generate a list

Usage:

    // generates a list till the given argument
    first = range(3) == [0,1,2];
    // generates a list from to the given arguments
    second = range(1, 4) == [1,2,3];
    // generate a list from to the given arguments with an additional step argument
    third = range(0, 15, 5) == [0, 5, 10];
    four = range(0, 16, 5) == [0, 5, 10, 15];

    // the last argument can also be a function that returns the next value
    // this will generate the squares of the values in the range
    five = range(0, 4, {x => x*x}) == [0, 1, 4, 9];
    // the last argument is just a function so it can also be used with variables
    filler = 10;
    six = range(0, 4, {=> filler}) == [10, 10, 10, 10];

    multiplier = {multiply => {number => multiply*number}};
    times_three = multiplier.(3);
    times_five = multiplier.(5);
    seven = range(0, 4, times_three) == [0, 3, 6, 9];
    eight = range(0, 4, times_five) == [0, 5, 10, 15];
        

Reduce

Reduce a list to a single value

Usage:

    // sum functionality
    one = reduce([1,2,3,4], 0, {acc, x => acc + x}) == 10;
    // map like functionality
    two = reduce([1,2], [], {acc, x => push(acc, x*3)}) == [3,6];

    // generate a map. This example generates a map with odd numbers
    three = reduce([1,2,3,4], {}, {acc, item => 
        put(acc, format("key-{}", item), item * 2 + 1)
    }) == {"key-1": 3, "key-2": 5, "key-3": 7, "key-4": 9};

Remove

Removes index from the list or key from the map

Usage:

    // for lists
    first = remove([1, 2, 3], 1) == [1, 3];
    second = remove([1, 2, 3], -1) == [1, 2];
    // for maps
    third = remove({"test": 1}, "test") == {};
    four = remove({"test": 1}, "another") == {"test": 1};
    five = remove({"test": 1, "another": 123}, "another") == {"test": 1};

Shuffle

Shuffles the given list

Usage:

    shuffle([1,2,3,4]);
                

Sin

Calculates the sine of the number

Usage:

    first = sin(0) == 0;

Sub

Subtracts the second argument from the first argument

Usage:

    first = 1 - 1 == 0;
    second = 421 - 321 == 100;
    third = -123 - 321 == -444;

Sum

Sums up the arguments

Usage:

    first = sum(1,2,3,4,5) == 15;
    second = sum([1,2,3,4,5]) == 15;

Tan

Calculates the tangent of the number

Usage:

    first = tan(0) == 0;

Trim

Removes the characters at the start and end of the first argument, second argument is an optional argument that contains the character to remove, defaults to ' '

Usage:

    first = trim("   test    ") == "test";
    second = trim("__Testing_Test__", "_") == "Testing_Test";
    third = trim("A sentence.\n\n\n\n\n", "\n") == "A sentence.";

Try

Tries the first argument, if that fails returns the second argument

Usage:

    // will just return the get function
    first = try(get({"test": 10}, "test"), 123) == 10;
    // get function will raise because "test" is not found
    second = try(get({}, "test"), 123) == 123;

    map = {"test": 15};
    invalid_key = 1;
    third = try(put(map, invalid_key, 123), map) == map;

Type

Returns the type of the argument

Usage:

    one = type(1.0) == "number";
    two = type([]) == "list";
    three = type({}) == "map";
    four = type("test") == "string";
    five = type({=> x}) == "function";
    six = type(true) == "boolean";
    seven = type(null) == "null";

Upper

Converts input to uppercase

Usage:

    first = upper("testing") == "TESTING";

User Functions

User functions can be defined as follows:

my_func = {x, y =>
    x + y
};
my_func.(2, 4) // returns 6

You see that the calling of a function is different than built-ins functions. It uses the . operator to call the function.

Variables outside the function can be used inside the function.

GLOBAL = 15;
my_func = {x =>
    GLOBAL + x
};
my_func.(4) // returns 19

Even currying it implemented.

factory = {x =>
    { y => x + y }
};
generated_function = factory.(4);
generated_function.(12) // returns 16

Also simple recursion works:

factorial = {n => 
    if((n == 0) or (n == 1), 1, n * factorial.(n-1))
};
factorial.(8) // returns 40320

Examples

Import

import { value, func } from "./examples/file/import_from.txt";


func.(value)

Import_from

value = {
    "text": "testing",
    "number": 123
};


func = { data => 
    text = get(data, "text");
    number = get(data, "number");
    format("{} {}", text, number)
};

Recursion

createList = {list, index, till => 
    newList = push(list, index);
    newIndex = index+1;
    if(newIndex == till, newList, {=> createList.(newList, newIndex, till)})
};

// using range(150) is ofcourse better, because this will overflow to stack
createList.([], 0, 150);

Script

Test::new = {=> 
    {
        "y": [1,2,3],
        "x": {
            "x": 123
        }
    }
};

Test::get =  {map, key => 
    inner = get(map, key);
    get(inner, key)
};

my_map = Test::new.();

Test::get.(my_map, "x")

Secant_method

// port from the python method used on https://en.wikipedia.org/wiki/Secant_method

secant_method = {f, x0, x1, iterations =>
    result = reduce(range(iterations), [x0, x1], {prev_numbers =>
        x0 = get(prev_numbers, 0);
        x1 = get(prev_numbers, 1);

        x2 = x1 - f.(x1) * (x1 - x0) / (f.(x1) - f.(x0));
        [x1, x2]
    });

    get(result, 1)
};

my_func = {x => 
    x ** 2 - 612
};

secant_method.(my_func, 10, 30, 5) == 24.738633748750722

Sort

MergeSort::sort = {list => 
    if(length(list) < 2, list, {=> 
        lists = MergeSort::split_middle.(list);
        left = get(lists, "left");
        right = get(lists, "right");
        left = MergeSort::sort.(left);
        right = MergeSort::sort.(right);
        MergeSort::merge.(left, right)
    })
};

MergeSort::merge = {left, right =>
    result = reduce(range(length(left) + length(right)), {"left": left, "right": right, "sorted": []}, {acc =>
        left = get(acc, "left");
        right = get(acc, "right");
        sorted = get(acc, "sorted");

        if(length(left) == 0, {"left": [], "right": [], "sorted": concat(sorted, right)}, {=>
            if(length(right) == 0, {"left": [], "right": [], "sorted": concat(sorted, left)}, {=>
                left_first = get(left, 0);
                right_first = get(right, 0);
                if(left_first < right_first, {=>
                    left = remove(left, 0);
                    sorted = push(sorted, left_first);
                    {"left": left, "right": right, "sorted": sorted}
                }, {=>
                    right = remove(right, 0);
                    sorted = push(sorted, right_first);
                    {"left": left, "right": right, "sorted": sorted}
                })
            })
        })     
    });
    get(result, "sorted")
};

MergeSort::split_middle = {list => 
    middle = length(list) / 2;
    lists = reduce(list, {"left": [], "right": []}, {acc, x => 
        left = get(acc, "left");
        right = get(acc, "right");
        if(
            length(left) < middle, 
            {"left": push(left, x), "right": right}, 
            {"left": left, "right": push(right, x)}
        );
    });
    lists
};


data_list = shuffle(range(15));
// check if data is not already sorted
assert(data_list != range(15));
MergeSort::sort.(data_list)

Library Usage

        use expression_parser::{Environment, ExpressionFile};

        let input = r#"
        a = 1 + 1;
        a + 5        
        "#;

        // ofcourse handle this better in you code
        let parsed_expression = ExpressionFile::parse(input).unwrap();

        // you can now decide what to do with the expression

        // we will just evaluate it here with default variables.

        let mut vars = Environment::default();
        let output = ExpressionFile::eval(parsed_expression, &mut vars).unwrap();
        assert_eq!(output, 7.into());

Add extra variables you can use in your code:

        use expression_parser::{Env, Environment, ExpressionFile};

        let input = r#"
        5 * DATA     
        "#;

        let parsed_expression = ExpressionFile::parse(input).unwrap();

        let mut env = Environment::default();
        env.variables_mut().insert("DATA", 1234.into());

        let output = ExpressionFile::eval(parsed_expression, &mut env).unwrap();
        assert_eq!(output, 6170.into());

Define your own functions in Rust like:

        use expression_parser::{
            Closure, Env, Environment, Error, ExpressionFile, ExpressionValue,
        };
        use std::sync::Arc;

        let mut env = Environment::default();

        // a `Closure` struct is just a container for holding the function.
        // `new` takes a list of the arguments used (only for debugging purposes)
        // and an `Arc` with a `Box`ed function with two arguments.
        // the first is a list containing all the arguments given by the user. These need to be validated yourself.
        // the second argument is a `Environment` struct that has methods to access variables outside the function and side effects.
        // the return value is a `Result<ExpressionValue, Error>`
        let closure = Closure::new(
            vec!["x".to_string(), "y".to_string()],
            Arc::new(Box::new(|vars, _| {
                /// validate the arguments or return an error
                fn validate_number(x: Option<&ExpressionValue>) -> Result<f64, Error> {
                    x.ok_or(Error::new_static("missing arguments"))?
                        .as_number()
                        .ok_or(Error::new_static("argument not a number"))
                }

                let x = validate_number(vars.get(0))?;
                let y = validate_number(vars.get(1))?;
                let result = x * y;

                // the `into` casts the rust value into a `ExpressionValue` enum
                Ok(result.into())
            })),
        );
        env.variables_mut().insert("external_func", closure.into());

        let script = r#"
        external_func.(6, 2)
        "#;

        let parsed = ExpressionFile::parse(script).unwrap();
        let result = ExpressionFile::eval(parsed, &mut env);
        assert_eq!(result, Ok(12.into()))
        use expression_parser::{Closure, Env, Environment, Error, Expression, ExpressionFile};
        use std::sync::Arc;

        let closure = Closure::new(
            vec!["map".to_string(), "key".to_string()],
            Arc::new(Box::new(|vars, context| {
                let map = vars
                    .get(0)
                    .ok_or(Error::new_static("expect a map as the first argument"))?
                    // probably do something more smart than just clone
                    .clone()
                    .as_map()
                    .ok_or(Error::new_static("expect a map as the first argument"))?;
                let key = vars
                    .get(1)
                    .ok_or(Error::new_static("expect a key as the second argument"))?
                    .as_string()
                    .ok_or(Error::new_static("expect a key as the second argument"))?;

                // get access to the underlying HashMap
                let result = map.0.get(&key).ok_or(Error::new_static("key not found"))?;

                // the result is an expression, these can include variables ect.
                // We can just match on the value or eval the Expression with the current context.
                let result = Expression::eval(result.clone(), &mut Box::new(context))?;

                Ok(result)
            })),
        );

        let mut env = Environment::default();
        env.variables_mut().insert("map_get", closure.into());

        let script = r#"
        map_get.(
            {"test": "some_value"},
            "test"
        )
        "#;

        let result = ExpressionFile::run(script, &mut env);
        assert_eq!(result, Ok("some_value".into()));

        let script = r#"
        text = "some_variable";
        map_get.(
            {"test": text},
            "test"
        )
        "#;

        let result = ExpressionFile::run(script, &mut env);
        assert_eq!(result, Ok("some_variable".into()));

Use your own variables:

        use expression_parser::{Environment, ExpressionFile, ExpressionValue};
        use std::collections::HashMap;

        // if you like python keywords better than the names I picked
        let mut my_variables = HashMap::new();
        my_variables.insert(String::from("True"), ExpressionValue::Bool(true));
        my_variables.insert(String::from("False"), ExpressionValue::Bool(false));
        my_variables.insert(String::from("None"), ExpressionValue::Null);

        // `Environment::builder` lets you configure your environment yourself.
        let mut env = Environment::builder()
            .with_variables(Box::new(my_variables))
            .build();

        let input = r#"
            a = True and True;
            b = False or True;
            c = None or True;
            all(a, b, c)
        "#;

        let parsed_expression = ExpressionFile::parse(input).unwrap();

        let output = ExpressionFile::eval(parsed_expression, &mut env).unwrap();
        assert_eq!(output, true.into());

Or extend the default variables:

        use expression_parser::{Environment, ExpressionFile, VariableMap, Variables};

        // `Variables::default` returns a `VariableMap` with all the default variables
        let mut my_variables = Variables::default();
        // so this allready exists in the variables.
        assert!(my_variables.insert("true", true.into()).is_some());
        my_variables.insert("DATA", vec![1, 5, 1].into());
        my_variables.insert("SOME", true.into());

        // `Environment::builder` lets you configure your environment yourself.
        let mut env = Environment::builder()
            .with_variables(Box::new(my_variables))
            .build();

        let input = r#"
            // returns DATA because SOME is true 
            if(SOME, DATA, error("data not found"))
        "#;

        let parsed_expression = ExpressionFile::parse(input).unwrap();

        let output = ExpressionFile::eval(parsed_expression, &mut env).unwrap();
        assert_eq!(output, vec![1, 5, 1].into());