Expression Syntax

This section outlines the basic syntax rules that expressions must adhere to.

Numeric values

Integer values, floating-point values, and exponential notation values can be used in expressions.

julia> expr = parse_expr("42")
42.0

julia> typeof(expr)
NumVal{Float64}

julia> eval_expr(expr)
42.0

During parsing, numeric values correspond to the Number type.

Operations on Numbers

The following operations are defined for numeric values:

  • Arithmetic:
    • +: addition
    • -: subtraction
    • *: multiplication
    • /: division
    • ^: exponentiation
julia> expr = parse_expr("1 + 2 * 3")
ExprNode(
  +,
  Union{AbstractExpr, ExprNode}[
    1.0,
    ExprNode(
      *,
      Union{AbstractExpr, ExprNode}[
        2.0,
        3.0
      ],
    ),
  ],
)

julia> typeof(expr)
ExprNode

julia> eval_expr(expr)
7.0
  • Logical:
    • >: greater than
    • <: less than
    • >=: greater than or equal to
    • <=: less than or equal to
    • !=: not equal to
    • ==: equal to
julia> expr = parse_expr("1 < 3")
ExprNode(
  <,
  Union{AbstractExpr, ExprNode}[
    1.0,
    3.0
  ],
)

julia> typeof(expr)
ExprNode

julia> eval_expr(expr)
true
  • Mathematical:
    • abs: absolute value
    • sqrt: square root
    • sin: sine
    • cos: cosine
    • atan: arctangent
    • exp: exponent
    • log: logarithm
julia> expr = parse_expr("abs(-1) + cos(0)")
ExprNode(+
  Union{AbstractExpr, ExprNode}[
    ExprNode(
      abs,
      Union{AbstractExpr, ExprNode}[
        ExprNode(
          -,
          Union{AbstractExpr, ExprNode}[
            1.0
          ],
        ),
      ],
    ), 
    ExprNode(
      cos,
      Union{AbstractExpr, ExprNode}[
        0.0
      ],
    ),
  ],
)

julia> typeof(expr)
ExprNode

julia> eval_expr(expr)
2.0

String values

Expressions can also contain string values by enclosing them with single quotes (') on both sides.

julia> expr = parse_expr(" 'its my string' ")
its my string

julia> typeof(expr)
StrVal

julia> eval_expr(expr)
"its my string"

Inside such a string, no other single quote characters can be present. During parsing, string values correspond to the String type.

Operations on Strings

The following operations are defined for string values:

  • *: concatenation
  • ^: repetition
  • Logical:
    • >: greater than
    • <: less than
    • >=: greater than or equal to
    • <=: less than or equal to
    • !=: not equal to
    • ==: equal to
julia> expr = parse_expr("'Julia' * 'Lang' * '❤️'")
ExprNode(
  *,
  Union{AbstractExpr, ExprNode}[
    Julia,
    Lang,
    ❤️,
  ],
)

julia> eval_expr(expr)
"JuliaLang❤️"

Variables

Finally, the most important type of values are variable values. Referring to them within an expression can consist of two parts:

  • (required) the regular variable name according to the variable naming rules.
  • (optional) tags related to this variable.

Let's take a closer look at the syntax for the tag system:

  • Tags are specified inside curly braces {} or square brackets [] immediately following the variable name.
  • Tags can be specified in any order and are listed with commas without spaces.
  • Tags consist of a key-value pairs in the form of key='value'.
  • The name should contain only letters and numbers without spaces, and the first character of the name should be a letter.
  • The tag value must be a string value.
Note

Tags can be used as an extended variable name.

Variable scopes

For convenience, variables can be classified into local and global ones. The user can define which variables belong to which of such formats.

For the following examples, we will declare two dictionaries that will be responsible for global and local variables:

const local_vars = Dict{String,Float64}(
    "var"              => 1,
    "var{tag='value'}" => 2,
)

const global_vars = Dict{String,Float64}(
    "var"              => 3,
    "var[tag='value']" => 4,
)

As well as a function that will extract the values of variables in the process of calculating expressions

function NumExpr.eval_expr(var::NumExpr.Variable)
    return get(isglobal_scope(var) ? global_vars : local_vars, var[], NaN)
end
Note

Currently, this is done only to facilitate the separation of variables into two formats and is not related to scope. This division is intended for implementing additional functionality.

During parsing, local variables correspond to the type Variable{LocalScope}, and global ones to Variable{GlobalScope}.

Local variables

Local variables specified when using {} brackets for tags or in the absence of a tag.

By default:

julia> expr = parse_expr("var")
var

julia> typeof(expr)
Variable{LocalScope}

julia> var = eval_expr(expr)
1.0

julia> typeof(var)
Float64

By local tag:

julia> expr = parse_expr("var{tag='value'}")
var{tag='value'}

julia> typeof(expr)
Variable{LocalScope}

julia> var = eval_expr(expr)
2.0

Calling a local variable without tags:

julia> expr = parse_expr("{var}")
var

julia> eval_expr(expr)
1.0

Global variables

Global variables specified when using [] brackets for tags.

Calling a global variable without tags:

julia> expr = parse_expr("[var]")
var

julia> typeof(expr)
Variable{GlobalScope}

julia> eval_expr(expr)
3.0

By global tag:

julia> expr = parse_expr("var[tag='value']")
var[tag='value']

julia> typeof(expr)
Variable{GlobalScope}

julia> eval_expr(expr)
4.0

Variable values

To interpret the variables specified in the expression as concrete values during the calculation process, the following steps need to be taken:

  • Determine the data source from which variables can be extracted using their full name (including tags, which should be sorted in alphabetical order). For example, a dictionary:
colors = Dict{String, UInt32}(
    "color"                 => 0xffffff,
    "color[name='red']"     => 0xff0000,
    "color[name='green']"   => 0x00ff00,
    "color[name='blue']"    => 0x0000ff,
    "color[name='yellow']"  => 0xffff00,
    "color[name='cyan']"    => 0x00ffff,
    "color[name='magenta']" => 0xff00ff,
)
Note

The full name of a Variable type variable can be obtained by applying the unary operator [] to the object.

  • Then, it is necessary to overload the function that will extract variable values from their source.

For example, let's request the value for the variable name var from the colors dictionary:

NumExpr.eval_expr(var::NumExpr.Variable) = get(colors, var[], NaN)

Now, during the evaluation of the expression, variables will be interpreted as numbers.

julia> expr = parse_expr("color");

julia> eval_expr(expr)
0x00ffffff

julia> expr = parse_expr("color[name='red'] + color[name='green'] + color[name='blue']");

julia> eval_expr(expr)
0x0000000000ffffff

Custom Functions

In addition to predefined functions, users can define their own. To define a new operation on the elements mentioned earlier, you need to define a new method for the NumExpr.call function.

  • The first argument of such a method should be of type ::NumExpr.Func{:S}, where S is the name of the new function.
  • Subsequent arguments should correspond to the required arguments of the defined function.

For example, for a function max(x::Number, y::Number), you need to define the following method:

function NumExpr.call(::NumExpr.Func{:max}, x::Number, y::Number)
    return max(x, y)
end

Now, expressions containing such a function can be correctly processed.

julia> expr = parse_expr("max(5, 10) + max(20, 3)");

julia> eval_expr(expr)
30.0

For functions with an unknown number of arguments the method can look like this:

function NumExpr.call(::NumExpr.Func{:sum}, x::Number...)
    return sum(x)
end

Now we can call the sum function with any number of arguments.

julia> expr = parse_expr("sum(6, 4) + sum(5, 15, 10)");

julia> eval_expr(expr)
40.0