Catalog · How the catalog works
ExprFpc
A freepascal (https://www.freepascal.org) library to evaluate expressions or simple scripts
Anyone may press Interesting, signed in or not. It helps others find this project.
README
ExprFpc
A library to evaluate expressions or simple scripts
Introduction
exprfpc is a freepascal library used to let a program evaluate/execute user-modifiable expressions (scripts); or, in other words, it is an embeddable scripting language with its virtual machine.
Only numbers (double precision type) are implemented. There is a limited support for strings, useful for logging: the language parses them, but they can only be used as argument for a host implemented function.
Overview
The host (the main program) creates an instance of the virtual machine; then optionally adds special functions and/or variables; finally loads a script and compiles it. From then on, the host can set variables to feed data to the script, execute it, and fetch the result(s).
The scripting language is quite basic and has some limitations, in order to favour simplicity and speed (it is about 250% faster than python, which anyway is not very fast).
Scripting language (or, expression syntax)
Basically, the language calculates expressions and assigns variables.
An example to calculate the hypotenuse of a triangle is:
s1sq = s1 square # square is a special infix operator
s2sq = s2 square
result = s1sq+s2sq root # there is no precedenceAn assignment writes into a variable; if the variable does not exist, it is created (during compilation). Once created, variables survive until the next compilation (of a new script) and retain their value across executions.
For a variable to be read (in an expression), the variable must exist. In the example above the variables s1 and s2 are not declared by the script: they already exist because they are purposely created (either as variable or function) by the host before compilation.
There are a few things to note:
- infixes are "extended"; they need, as usual, a left operand, but can have 0 or
more right operand. square and root are infixes with no right operand,
so
s1 squarecalculates the square of s1.
- there is no precedence.
1+2*3yields 9, not 7, because evaluation is made strictly left to right like simple pocket calculators. And there are no parentheses either: this is why we need to square s1 and s2 as intermediate steps; writings1 square + s2 squarewould calculate "square(square(s1)+s2)". A two line version of the example would be:
s2sq = s2 square
result = s1 square + s2sq root(in the last line, square is applied to _s1_, and _root_ is applied to the sum
of the two squares)- so an expression can be formally defined as: TERM [INFIX ARGS]...
The engine already knows the common infixes (+, -, /, **, etc.), plus others quite special, but the host can add other.
Flow control
The language supports if and while statements. The forms are the following,
where the square brackets enclose optional parts:
if expression then statement # single line
or
if expression [then] # multiline
statement
[else
statement]
endThe while statement is similar:
while expression do statement # single line, orand
while expression [do]
statement
endIn both cases, expression means a numeric expression, which is true if it yields a value different from 0. The comparison operators (<, >, =, <=, <>...) are supported, but they are evaluated like any other operator, so:
if error+2 > 3 then... # ok: sum 2 to error and compare to 3
if error < 2+3 then... # maybe wrong, compiles to "if (error<2)+3..."Interaction between host and script (virtual machine)
To communicate values from host to script there are two ways:
- create variables before compilation, and set them before each invocation
- add functions before compilation, so the script uses them to read values
Another example is the following script which implements a PI (PID without D) controller:
error = sp - pv # sp and pv are host functions
# error is a script private variableprop = error * kp # kp is functionintbuf = intbuf + error # intbuf is another private variable
# preserved across successive executionsout = intbuf * ki + prop # out is instead created by the hostWhen the script is invoked, sp and pv call a host function which return the current value of SP and PV. After an execution of the script, the host reads the out variable (declared by the host itself) to get the result.
The virtual machine keeps variables into an array accessible by the host. As variables are created, they are appended to the array. So, if the host declares a variable just after the instance creation, that variable will be #1 in the array; the second will be #2, and so on. The script can declare more private variables, which will be also appended to the array, but leaving the host variables untouched.
Other things to know
- Comments are introduced by "#" and go up to the end of line
- The pipe character ("|") can be used to separate logical lines (more statements on the same line). In this case, comments finish on the pipe character
- Every instance contain a StringList named HostData at disposal of the host to store and retrieve instance specific information
- the host can declare and add variadic functions
- the host can install a callback to parse declarations from the script
- there is a getVarList to retrieve a list of used variable
- there is a getFuncList to retrieve a compact list of available infixes. This is useful to help the user
- a script can be divided in SECTIONS, invokable separately
- variables which start with an uppercase letter are read-only
How to use the library
Usage is very simple. The following code fragment illustrates how to create an instance; it is taken from the file umain.pas test program from the uexprfpc project (files in this directory; use Lazarus to open):
uses uexprfpc;
...
// optional, assign a procedure to log messages from compiler
exprfpclogproc := @exprlog;
// create an instance
expr := TExprVM.Create;
// optional functions for added facilities
expr.addfunc('ticks', @scripttick, [], 'f', 'get tick count');
// optional, create variables to communicate with script
expr.addfvar('EXPR_VER', EXPRFPC_VER, 'version');
// creates a read-only variable; the script knows the engine version
// this is variable #1The procedure exprlog assigned to exprfpclog has the following signature:
procedure exprlog(st: string);and shows st somewhere.
Once the above is done, it is possible to compile and run the script:
if 0=expr.compile(scriptText) then begin
expr.execute(1); // "1" refers to the start of script
// read output, maybe
theengineversion := expr.fvarvalues[1]; // we know it's there
// otherwise:
for i := 1 to expr.nvar do
writeln(expr.fvarnames[i], '=', expr.fvarvalues[i]);
// we know everything about the variablesBefore execution the host can set variables simply with:
expr.fvarvalues[x] := somenumber; // must know what's thereAfter compilation, there can be many calls to execute(), possibly preceded by some variable assignment, and probably followed by variables reading. Another possibility is to use functions for exchanging data.
Adding functions for the script
Functions can be added to extend the script possibilities. There are three types:
- infixes: They are added with
addfinfix(name, @function), likeaddfinfix('+', @fplus);The addfinfix() is a shorthand for addfunc(), with more parameters
- normal functions: They are added with
addfunc(name, @function, flags, argumentTypes, description). An example is:expr.addfunc('ticks', @scripttick, [], 'f', 'get tick count');
All the functions expect their arguments in then global variable exprfpcregs[x] and return a number value of type double (or real). Infixes use register #0 for left operand, and #1 onward for right operands.
variadic These type of function can use any number of arguments, with different data types. They are invoked for each argument and, if configured, a special procedure is called for finalization of the statement. See main.pas for the implementation of the log() function.
Compilation callbacks
Via callback, the host can examine special lines of the script, like #pragmas. They are useful to implement special facilities, configured by the script. See umain.pas for an example.

