Luaxp is a simple arithmetic expression parser for Lua.
Luaxp supports simple mathemtical expressions for addition, subtraction, multiplication, division, modulus, bitwise operations, and logical operations. It has a small library of built-in functions (abs, cos, sin, floor, ceil, round, etc.).
Through a passed-in context table, Luaxp supports named variables, and custom functions. See the documentation below for how to implement these.
Luaxp is offered under MIT License as of October 29, 2018 (beginning with version 0.9.7).
There are three branches in the Github repository:
- master - The current released version; this is the version to use/track if you are incorporating LuaXP into other projects;
- develop - The current development version, which may contain work in progress, partial implementations, debugging code, etc. ("the bleeding edge");
- stable - The current stable development code, which contains only completed and tested functionality, but may still contain debug messages and lack some optimizations and refinement.
Code moves from the develop branch to the stable branch to the master branch. There is no release schedule. Releases are done as needed.
Some day. This is all very new.
Grab it. Put in your shared Lua directory (/usr/share/lua/...?) or keep it where you use it. Try out the free-form test program try_luaxp.lua. This lets you enter expressions and see the results.
TO-DO: Install with LuaRocks
As of version 0.9.7, the following are known issues or enhancement that are currently being considered:
None
I wrote this library as a port of a similar library I wrote for JavaScript called lexp.js (Lightweight Expression Parser). It differs slightly in operation, but the underlying approach is fundamentally the same and it's a very close port. I did this mainly for fun. I use lexp.js in a dashboard system that I wrote (I wanted something simpler to set up and manage than dashing, which is great, but has way too high a setup and learning curve, but I digress), and figured that somebody might make use of it in Lua as well.
I like bug reports. I like help. I like making things better. If you have suggestions or bug reports please use use GitHub Issues. If you have a contribution, have at it! Please try to follow the coding style to keep it consistent, and use spaces rather than tabs (4 space indenting).
Also, if you're making a feature enhancement contribution, consider looking at my lexp project as well, and see if the same enhancement would be appropriate there. Since the Lua implementation is born of the JavaScript one, I think it would be an interesting exercise to try and keep them as close functionally as possible.
This is a very rough BNF for the parser:
<expression> ::= <number> | <string> | <variable-name> | <variable-name> "[" <array-subscript> "]" | <function-name> "(" <argument-list> ")" | <expression> <binary-operator> <expression> | <unary-operator> <expression> | "(" <expression> ")" <argument-list> ::= "" | <expression-list> <expression-list> ::= <expression> [ "," <expression-list> ] <unary-operator> ::= "-" | "+" | "!" <binary-operator> ::= "+" | "-" | "*" | "/" | "%" | "&" | "|" | "^" | "<" | "<=" | ">" | ">=" | "==" | "=" | "<>" | "!=" <array-subscript> :== <number> | <expression> /* must eval to number */ <number> ::= <decimal-integer> | "0x" <hexadecimal-integer> | "0b" <binary-integer> | "0" <octal-integer> | <decimal-rational-number> <string> ::= "'" <characters> "'" | '"' <characters> '"' <variable-name> ::= <letter> { <letter> | <digit> | "_" | "." } <function-name> ::= <letter> { <letter> | <digit> | "_" } This is intentionally simplified and doesn't exhaustively convey the full syntax, which would be too detailed to convey the concept quickly. Specific elements of the syntax such are array and dot notation for traversal of trees/structures is not shown (e.g. expressions forms "weather.current" and "weather['current'], which are equivalent).
To load the library, use a require() statement:
luaxp = require "luaxp" The compile() function accepts a single argument, the string the containing the expression to be parsed. If parsing of the expression succeeds, the function returns a table containing the parse tree that is used as input to run() later. If parsing fails, the function returns two values: nil and a table containing information about the error.
Example:
luaxp = require('luaxp') local parsedExp,err = luaxp.compile("abs(355/113-pi)") if parsedExp == nil then -- Parsing failed print("Expression parsing failed. Reason: " .. luaxp.dump(err)) else -- Parsing succeeded, on to other work... ... end This example uses the LuaXP public function dump() to display the contents of the err table returned.
The run() function executes the parsed expression. It takes an optional executionContext argument, which is a table containing variable names and functions.
run() returns the result of the expression evaluation. If the evaluation succeeds, the first return value will always be non-nil. If it fails, two values are returned: nil and a string containing the error message (i.e. same semantics as compile()). You should always check for evaluation errors, as these are errors that were not or could not be detected in parsing (e.g. a sub-expression used as a divisor evaluates to zero, thus an attempt to divide by zero).
luaxp = require "luaxp" local parsedExp, cerr = luaxp.compile("abs(355 / 113 - pi)" ) if parsedExp == nil then error("Parsing failed: " .. cerr.message) end local context = { pi = math.pi } local resultValue, rerr = luaxp.run( parsedExp, context ) if resultValue == nil then error("Evaluation failed: " .. rerr.message) else print("Result:", luaxp.isNull(resultValue) and "NULL" or tostring(resultValue) ) end In the above example, a context is created to define the value of "pi" that is used in the parsed expression. This context is then passed to run(), which uses it to dereference the value on the fly.
The code also checks the return value for the special "null" value. If the result of an expression results in "no value", LuaXP does not use Lua nil, it has its own indicator, and your code should check for this as shown above.
As of this version, Luaxp does not allow you to modify variables or create new ones during evaluation.
The evaluate() function performs the work of compile() and run() in one step. The function result is the value of the parsed and evaluated expression, unless a parsing or evaluation error occurs, in which case the function will return two values: nil and an error message.
luaxp = require "luaxp" local context = { pi = math.pi } local resultValue,err = luaxp.evaluate("abs(355/113-pi)", context) if resultValue == nil then error("Error in evaluation of expression: " .. err.message) else print("The difference between the two approximations of pi is " .. tostring(result)) end The LuaXP dump() function will return a string containing a safely-printable representation of the passed value. If the value passed is a table, for example, dump() will display it in a Lua-like table initialization syntax (tuned for readability, not for re-use as actual Lua code).
The isNull() function returns a boolean indicating if the passed argument is LuaXP's null value.
The null and NULL constants (synonyms) are the represtations of LuaXP's null value. Thus the test returnValue==luaxp.null in Lua is equivalent to isNull(returnvalue). The constants can also be used to initialize values when creating the execution context.
The words true and false are reserved and evaluate to their respective boolean values. The words null, NULL, and nil evaluate to the LuaXP null value.
The reserved words pi and PI (synonyms) are provided as a convenience and evaluate to the underyling Lua Math library implementation of math.pi.
If a LuaXP call results in an error (nil first return value), the error table (second return value) contains the following elements:
type- Always included, the string "compile" or "evaluation" to indicate the stage at which the error was detected.message- Always included, text describing the error.location- Sometimes included, the character position at which the error was detected, if available.
The try_luaxp.lua example included with LuaXP shows how the location value can be used to provide feedback to the user when errors occur. Try entering "0b2" and "max(1,2,nosuchname)" into this example program.
The context passed to evaluate() and run() is used to define named variables and custom functions that can be used in expressions. We've seen in the above examples for these functions how that works. For variables, it's simple a matter of defining a table element with the value to be used:
local context = {} context.minrange = 0 context.maxrange = 100 -- or the more streamlined: local context = { minrange=0, maxrange=100 } These may be referred to in expressions simply by their names as defined (case sensitive):
$ lua try_luaxp.lua Running with Luaxp version 0.9.2 Context variables defined: minrange=0 maxrange=100 EXP> maxrange Expression result: 100 EXP> (maxrange-minrange)/2 Expression result: 50 EXP> nonsense Expression evaluation failed: Undefined variable: nonsense Variables can also use dotted notation to traverse a tree of values in the context:
context.device = {} context.device.class = "motor" context.device.info = { location="MR1-15-C02", specs={ manufacturer="Danfoss", model="EM5-18-184T", frame="T", voltage="460", hp="5" } } In expressions, the value device.class would therefore be motor. Referring simply to device, however, would return a runtime evaluation error.
The second more complex example shows that dotted notation can be used to traverse more deeply-nested structure. In this example, one could derive the horsepower of the example motor by referring to device.info.specs.hp.
You can define custom functions for your expressions by defining them in the context passed to run() or evaluate().
It's pretty straightforward to do. Your custom function must be implemented by a Lua function that takes a single argument, which I'll call argv simply for example purposes. This is an array of the expression values parsed.
Let's say we want to create a function to convert degrees to radians. The math for that is pretty easy. It's the value in degrees times "pi" and divided by 180. If you wrote that just as a plain Lua function, it would probably look something like this:
function toRadians(degrees) return degrees * math.pi / 180 end To make that a function that your expressions could use, you need to put it into the context that's passed to run(), which is done like this:
local context = {} context.toradians = function( argv ) return args[1] * math.pi / 180 end Now, when you run your expression, you can pass this context, and the evaluator will know what toradians means in the expression:
luaxp = require "luaxp" local context = {} context.toradians = function( argv ) return argv[1] * math.pi / 180 end print("The cosine of 45 degrees is " .. luaxp.evaluate("cos(toradians(45))", context)) Although we have used an anonymous function in this example, there is no reason you could not separately define a named function, and simply use a reference to the function name in the context assignment, like this:
function toRadians(argv) return argv[1] * math.pi / 180 end context.toradians = toRadians The premise here is simple, if it's not already clear enough. The evaluator will simply look in your passed context for any name that it doesn't recognize as one of its predefined functions. If it finds a table element with a key equal to the name, the value is assumed to be a function it can call. The function is called with a single argument, a table (as an array) containing all of the arguments that were parsed in the expression. There is no limit to the number of arguments. Your function is responsible for sanity-checking the number of arguments, their values/type, and supplying defaults if necessary.
Note in the above example that we defined our function with an uppercase letter "R" in the name, but when we made the context assignment, the context element has all lower case. This means that any expression would also need to use all lower case. The name used in evaluation is the name on the context element, not the actual name of the function.
If we run our program (which is available as example1.lua in the repository), here's the output:
$ lua example1.lua The cosine of 45 degrees is 0.70710678118655 The evaluator supports assignment of a value to local variable. If multiple expressions are evaluated using the same context, the local variables defined by earlier expressions are visible to the later ones.
luaxp = require "luaxp" ctx = {} result = luaxp.evaluate( "v=100", ctx ) print(result) -- prints 100 result = luaxp.evaluate( "v=v*2", ctx ) print(result) -- prints 200 result = luaxp.evaluate( "v/5", ctx ) print(result) -- prints 40 The local variables accumulated in the context are stored under the __lvars key. Thus, in this example, ctx.__lvars.v would be defined and have the value 40 in Lua after all three evaluations.
Local variables are in scope before context variables (that is, if a local variable has the same name as a context variable, the local variable will always take precedence):
luaxp = require "luaxp" ctx = {} ctx.alpha = 57 -- context variable definition result = luaxp.evaluate( "alpha", ctx ) print(result) -- prints 57 as expected -- This expression creates a local variable with the same name luaxp.evaluate( "alpha=99", ctx ) -- Now that we've set local alpha, we can't "see" the context variable result = luaxp.evaluate( "alpha", ctx ) print(result) -- prints 99 -- If we print what's in the context, we see two different values, one -- for the original context variable as we defined it, the other for -- the local variable defined by the expression evaluation. print(ctx.alpha) -- prints 57 print(ctx.__lvars.alpha) -- prints 99