Tags

, ,

Background

Back in 2009, a Stack Overflow user was seeking an easy way to plug general expression evaluation into a C++ program. The question received good answers explaining how to build a parser so that the expression could be parsed and then executed. I provided an answer showing how to instead embed a Lua interpreter and use that to evaluate arbitrary expressions. Inspired by the reference to Lua, the original poster also asked a related question aimed squarely at Lua users to see if there was any other input on the subject.

Since Lua was designed for easy embedding in larger programs, it is straightforward to set up a Lua interpreter instance, and pass it expressions to be evaluated, getting back a function to call that evaluates the expression. You can even let the user have variables, and it is easy to also let the user have access to the whole Lua language.

My LE module

Since that is a bold claim, I’ve written a simple wrapper that makes it so. I have compiled this and tested it lightly, but it certainly should not be trusted in production code without some attention to error handling and so forth. All the usual caveats apply here. I compiled and tested this on Windows using Lua 5.1.4 from Lua for Windows. On other platforms, you’ll have to find Lua from your usual source, or from http://www.lua.org. Note that Lua 5.2 is considered the current mainstream version, and while I don’t expect there are large differences making this code work with 5.2, it has not been tried. Perhaps I’ll do that and post about it sometime “soon”.

This sample uses simple and direct techniques to hide the full power and complexity of the Lua API behind as simple as possible an interface. It is probably useful as-is, but could be improved in a number of ways.

Alternative from LHF

I would encourage readers to look into the much more production-ready ae library by lhf for code that takes advantage of the API to avoid some of the quick and dirty string manipulation I’ve used. His library also promotes the math library into the global name space so that the user can say sin(x) or 2 * pi without having to say math.sin and so forth.

Public interface to LE

Here is the file le.h:

    /* Public API for the LE library.
     */
    int le_init();
    int le_loadexpr(char *expr, char **pmsg);
    double le_eval(int cookie, char **pmsg);
    void le_unref(int cookie);
    void le_setvar(char *name, double value);
    double le_getvar(char *name);

Sample code using LE

Here is the file t-le.c, demonstrating a simple use of this library. It takes its single command-line argument, loads it as an expression, and evaluates it with the global variable x changing from 0.0 to 1.0 in 11 steps:

    #include <stdio.h>
    #include "le.h"

    int main(int argc, char **argv)
    {
        int cookie;
        int i;
        char *msg = NULL;

        if (!le_init()) {
        printf("can't init LEn");
        return 1;
        }
        if (argc<2) {
        printf("Usage: t-le "expression"n");
        return 1;
        }
        cookie = le_loadexpr(argv[1], &msg);
        if (msg) {
        printf("can't load: %sn", msg);
        free(msg);
        return 1;
        }
        printf("  x    %sn"
           "------ --------n", argv[1]);
        for (i=0; i<11; ++i) {
        double x = i/10.;
        double y;

        le_setvar("x",x);
        y = le_eval(cookie, &msg);
        if (msg) {
            printf("can't eval: %sn", msg);
            free(msg);
            return 1;
        }
        printf("%6.2f %.3fn", x,y);
        }
    }

Here is some output from t-le:

E:...>t-le "math.sin(math.pi * x)"
  x    math.sin(math.pi * x)
------ --------
  0.00 0.000
  0.10 0.309
  0.20 0.588
  0.30 0.809
  0.40 0.951
  0.50 1.000
  0.60 0.951
  0.70 0.809
  0.80 0.588
  0.90 0.309
  1.00 0.000

E:...>

Implementation of LE

Here is le.c, implementing the Lua Expression evaluator:

    #include <lua.h>
    #include <lauxlib.h>

    #include <stdlib.h>
    #include <string.h>

    static lua_State *L = NULL;

    /* Initialize the LE library by creating a Lua state.
     *
     * The new Lua interpreter state has the "usual" standard libraries
     * open.
     */
    int le_init()
    {
        L = luaL_newstate();
        if (L) 
        luaL_openlibs(L);
        return !!L;
    }

    /* Load an expression, returning a cookie that can be used later to
     * select this expression for evaluation by le_eval(). Note that
     * le_unref() must eventually be called to free the expression.
     *
     * The cookie is a lua_ref() reference to a function that evaluates the
     * expression when called. Any variables in the expression are assumed
     * to refer to the global environment, which is _G in the interpreter.
     * A refinement might be to isolate the function envioronment from the
     * globals.
     *
     * The implementation rewrites the expr as "return "..expr so that the
     * anonymous function actually produced by lua_load() looks like:
     *
     *     function() return expr end
     *
     *
     * If there is an error and the pmsg parameter is non-NULL, the char *
     * it points to is filled with an error message. The message is
     * allocated by strdup() so the caller is responsible for freeing the
     * storage.
     * 
     * Returns a valid cookie or the constant LUA_NOREF (-2).
     */
    int le_loadexpr(char *expr, char **pmsg)
    {
        int err;
        char *buf;

        if (!L) {
        if (pmsg)
            *pmsg = strdup("LE library not initialized");
        return LUA_NOREF;
        }
        buf = malloc(strlen(expr)+8);
        if (!buf) {
        if (pmsg)
            *pmsg = strdup("Insufficient memory");
        return LUA_NOREF;
        }
        strcpy(buf, "return ");
        strcat(buf, expr);
        err = luaL_loadstring(L,buf);
        free(buf);
        if (err) {
        if (pmsg)
            *pmsg = strdup(lua_tostring(L,-1));
        lua_pop(L,1);
        return LUA_NOREF;
        }
        if (pmsg)
        *pmsg = NULL;
        return luaL_ref(L, LUA_REGISTRYINDEX);
    }

    /* Evaluate the loaded expression.
     * 
     * If there is an error and the pmsg parameter is non-NULL, the char *
     * it points to is filled with an error message. The message is
     * allocated by strdup() so the caller is responsible for freeing the
     * storage.
     * 
     * Returns the result or 0 on error.
     */
    double le_eval(int cookie, char **pmsg)
    {
        int err;
        double ret;

        if (!L) {
        if (pmsg)
            *pmsg = strdup("LE library not initialized");
        return 0;
        }
        lua_rawgeti(L, LUA_REGISTRYINDEX, cookie);
        err = lua_pcall(L,0,1,0);
        if (err) {
        if (pmsg)
            *pmsg = strdup(lua_tostring(L,-1));
        lua_pop(L,1);
        return 0;
        }
        if (pmsg)
        *pmsg = NULL;
        ret = (double)lua_tonumber(L,-1);
        lua_pop(L,1);
        return ret;
    }


    /* Free the loaded expression.
     */
    void le_unref(int cookie)
    {
        if (!L)
        return;
        luaL_unref(L, LUA_REGISTRYINDEX, cookie);    
    }

    /* Set a variable for use in an expression.
     */
    void le_setvar(char *name, double value)
    {
        if (!L)
        return;
        lua_pushnumber(L,value);
        lua_setglobal(L,name);
    }

    /* Retrieve the current value of a variable.
     */
    double le_getvar(char *name)
    {
        double ret;

        if (!L)
        return 0;
        lua_getglobal(L,name);
        ret = (double)lua_tonumber(L,-1);
        lua_pop(L,1);
        return ret;
    }

Remarks

The above sample consists of just 189 lines of code total, including a spattering of comments, blank lines, and the demonstration. Not bad for a quick function evaluator that knows how to evaluate reasonably arbitrary expressions of any number of variables and has rich library of standard math functions at its beck and call. Not to mention the demo that quickly evaluates expressions of one variable.

You have a Turing-complete language underneath it all, and it would be an easy extension to allow the user to define complete functions as well as to evaluate simple expressions.