Page 1 of 1

evaluate user-defined conditions at runtime using Lua or SQLite

Posted: Tue Apr 18, 2023 8:57 pm
by jassing
I had a need to evaluate user-defined conditions at runtime (ie: unknown to me),
initially, I ended up using Lua.

They seem to be about the same, speed-wise, so it's a matter of what you prefer working with.

(updated to include "SQLite like" statement for Lua matches 2023/04/18 01:41UTC)

Code: Select all

EnableExplicit
XIncludeFile "lua\module_lua.pbi" ;http://forums.purebasic.com/english/viewtopic.php?t=67365

DeclareModule LuaEval
  Declare uate( condition.s )
  Declare done()
EndDeclareModule

Module LuaEval
  Global Lua   = #Null
  Global NewMap regex()
  Declare init() : init()
  #likeStatement = ~"\".*\" like \".*\""
  #likeElements  = ~"(?<=\").*?(?=\")";~"\".*?\""
    
  UseModule Lua
  ;- Exported functions to Lua
  ProcedureC LuaRegEx( *l )
    Protected ParamCount,i.i, str.s, pattern.s, isLike = #False 
    ParamCount = lua_gettop(*L)
    If ParamCount = 2
      If lua_isstring(*l,1) And lua_isstring(*l,2)
        str = lua_tostring(*L, 1)
        pattern = lua_tostring(*l,2)
        If FindMapElement(regex(), pattern)=#False 
          regex(pattern) = CreateRegularExpression(#PB_Any,pattern)
        EndIf 
        If regex(pattern)
          isLike = MatchRegularExpression(regex(pattern),str)
          lua_pushboolean(*l,islike)
        EndIf
      EndIf
    EndIf 
    ProcedureReturn isLike
  EndProcedure
  
  ;- Our functions.
  Procedure uate( condition.s )
    ;Static lua = #Null 
    Protected retval = -1,*script, scriptLength
    Protected Dim like.s(0)
    If lua 
      If MatchRegularExpression(regex(#likeStatement), condition)
        If ExtractRegularExpression(regex(#likeElements), condition, like()) = 3 And Trim(LCase(like(1)))="like"
          condition = "match('"+like(0)+"','"+ReplaceString(like(2),"%",".*")+"')"
          Debug condition
        EndIf
      EndIf 
      
      *script = Ascii(~"function eval1()\n"+
                    ~"return("+condition+~")\n"+
                    ~"end")
      scriptLength = MemorySize(*script)-1 ; Remove final Null
  
      If luaL_loadbuffer(lua,*script,scriptLength,"Eval1") Or lua_pcall(LUA, 0, #LUA_MULTRET, 0)
        Debug "Lua error loading buffer - " + lua_tostring(LUA, -1)
        lua_pop(LUA, 1)
      Else 
        lua_getglobal(lua,"eval1")
        If lua_pcall(lua,0,1,0)
          Debug "Lua error calling eval1() - "+lua_tostring(lua,-1)
          lua_pop(LUA, 1)
        Else
          retval = lua_toboolean(lua,-1)
        EndIf 
      EndIf
      
      FreeMemory(*script)
    EndIf
    ProcedureReturn retval
  EndProcedure
  
  Procedure init()
    If Lua_Initialize(".\")
      lua=luaL_newstate() ; Create a new lua-vm, it is possible to have more than one.
      If lua 
        luaL_openlibs(LUA)  ; initalize the default librarys
        lua_register(LUA, "match", @LuaRegEx()) ; Replace print, so it output to the debug
      EndIf
      regex(#likeStatement) = CreateRegularExpression(#PB_Any, #likeStatement)
      regex(#likeElements)  = CreateRegularExpression(#PB_Any, #likeElements )
    
    EndIf
  EndProcedure
  
  Procedure done()
    Lua_close(lua)
    Lua_Dispose()
    lua=#Null
    ForEach regex()
      FreeRegularExpression(regex())
    Next
    ClearMap(regex())
  EndProcedure
  
  UnuseModule Lua
EndModule

CompilerIf #PB_Compiler_IsMainFile
  Debug "No: "   + Str(LuaEval::uate(~"\"this\" == \"that\"" ))
  Debug "Yes: "  + Str(LuaEval::uate(~"\"this\" == \"this\"" ))
  Debug "No: "   + Str(LuaEval::uate("3 > 4"))
  Debug "Yes: "  + Str(LuaEval::uate("3 < 4"))
  
  ;No such phrasing like in SQLite "this LIKE that"
  Debug "---"
  Debug "Yes: " + Str(LuaEval::uate(~"\"I am josh,and I am here\" like \"%josh%\""))
  Debug "No: "  + Str(LuaEval::uate(~"\"I am joXh,and I am here\" like \"%josh%\""))
  
  Debug  "Yes: " + Str(LuaEval::uate(~"match(\"I am josh, and i am here\",\"josh\")"))
  Debug  "Yes: " + Str(LuaEval::uate(~"match(\"Find the number 423 but not 324\",\"4[0-9]{2}\")"))
  Debug  "NO: "  + Str(LuaEval::uate(~"match(\"I am josh, and i am here\",\"Josh\")"))
  Debug  "yes: " + Str(LuaEval::uate(~"match(\"I am josh, and i am here\",\"(?i)Josh\")"))
  Debug  "No: "  + Str(LuaEval::uate(~"match(\"Won't Find neither the number 4.23 nor not 324\",\"4[0-9]{2}\")"))
  
  ; more realistic example
  Define user.s = "%dow% == 2 and %hour% > 12" ; ie: User entered the condition in a stringgadget.\
  ; now replace the application specific tokens
  user = ReplaceString(user,"%hour%",FormatDate("%hh",Date()))
  user = ReplaceString(user,"%dow%", Str(DayOfWeek(Date())))
  Debug  "Tuesday, after 12? "+Str(LuaEval::uate( user ))
  
  LuaEval::done()  
CompilerEndIf
A potentially better solution as it doesn't require any additional files, not my idea, from viewtopic.php?p=556183#p556183

Code: Select all

UseSQLiteDatabase()
DeclareModule SQLeval
  Declare uate( condition.s )
EndDeclareModule

Module SQLeval  
  ImportC ""
    sqlite3_create_function.i(DatabaseID, zFunctionName.p-utf8, nArg.i, eTextRep.i, *pApp, *xFunc, *xStep, *xFinal)
    sqlite3_aggregate_context(*sqlite3_context, nBytes.i)
    sqlite3_result_int(*sqlite3_value, lVal.l)
    sqlite3_result_int64(*sqlite3_value, qVal.q)
    sqlite3_result_double(*sqlite3_context, dblVal.d)
    sqlite3_result_text(*sqlite3_context, *char, cBytes, *void1, *void2)
    sqlite3_result_text16(*sqlite3_context, *char, cBytes, *void1, *void2)
    sqlite3_value_numeric_type.i(*sqlite3_value)
    sqlite3_value_int.l(*sqlite3_value)
    sqlite3_value_int64.q(*sqlite3_value)
    sqlite3_value_double.d(*sqlite3_value)
    sqlite3_value_text(*sqlite3_value)
    sqlite3_value_text16(*sqlite3_value)
    
    sqlite3_value_type.i(*argv)
    
  EndImport
  
  ;- Constants
  ;{
  #SQLITE_UTF8 = 1  ; IMP: R-37514-35566
  #SQLITE_UTF16LE = 2 ; IMP: R-03371-37637
  #SQLITE_UTF16BE = 3 ; IMP: R-51971-34154
  #SQLITE_UTF16 = 4   ; Use native byte order
  #SQLITE_ANY = 5     ; Deprecated
  #SQLITE_UTF16_ALIGNED = 8 ; sqlite3_create_collation only
  
  
  #SQLITE_INTEGER = 1
  #SQLITE_FLOAT = 2
  #SQLITE_TEXT = 3
  #SQLITE_BLOB = 4
  #SQLITE_NULL = 5
  
  #SQLITE_STATIC = 0
  #SQLITE_TRANSIENT = -1
  ;}
  Structure udtArgv
    *Index[0]
  EndStructure
  Global dbID 
  Procedure Unicode(String.s)
    Protected *mem
    *mem = AllocateMemory(StringByteLength(String) + SizeOf(Character))
    If *mem
      PokeS(*mem, String, -1, #PB_UTF16)
    EndIf
    ProcedureReturn *mem
  EndProcedure
  
  CompilerIf #False ;- Example functions
    ProcedureC sql_sin(*context, argc.i, *argv.udtArgv)
      Protected a.d
      a = sqlite3_value_double(*argv\Index[0])
      a = Sin(a)
      sqlite3_result_double(*context, a)
    EndProcedure
    
    ProcedureC sql_cos(*context, argc.i, *argv.udtArgv)
      Protected a.d
      a = sqlite3_value_double(*argv\Index[0])
      a = Cos(a)
      sqlite3_result_double(*context, a)
    EndProcedure

    ProcedureC sql_tan(*context, argc.i, *argv.udtArgv)
      Protected a.d
      a = sqlite3_value_double(*argv\Index[0])
      a = Tan(a)
      sqlite3_result_double(*context, a)
    EndProcedure
 
    ProcedureC sql_destructor_freememory(*void)
      If *void
        Debug "SQL-Destructor: FreeMemory: " + *void
        FreeMemory(*void)
      EndIf
    EndProcedure

    ProcedureC sql_lcase(*context, argc.i, *argv.udtArgv)
      Protected *string, *result
      *string = sqlite3_value_text(*argv\Index[0])
      *result = UTF8(LCase(PeekS(*string, -1, #PB_UTF8)))
      sqlite3_result_text(*context, *result, -1, @sql_destructor_freememory(), 0)
    EndProcedure
    
    ProcedureC sql_ucase(*context, argc.i, *argv.udtArgv)
      Protected *string, *result
      *string = sqlite3_value_text(*argv\Index[0])
      *result = UTF8(UCase(PeekS(*string, -1, #PB_UTF8)))
      sqlite3_result_text(*context, *result, -1, @sql_destructor_freememory(), 0)
    EndProcedure
  CompilerEndIf 
  
  ;- Our exported functions
  ProcedureC sql_regex( *context, argc.i, *argv.udtArgv )
    Protected *string, *pattern, result
    Protected.s string, pattern
    Static NewMap regex()
    *string = sqlite3_value_text(*argv\Index[0])
    *pattern = sqlite3_value_text(*argv\index[1])
    pattern = PeekS(*pattern,-1,#PB_UTF8)
    string = PeekS(*string,-1,#PB_UTF8)
    If FindMapElement(regex(),pattern)=#False
      regex(pattern)=CreateRegularExpression(#PB_Any,pattern)
    EndIf 
    result = MatchRegularExpression(regex(pattern),string)
    sqlite3_result_int( *context, result)
  EndProcedure

  Procedure uate( condition.s )
    ;Debug condition
    Protected result = -1
    ;ReplaceString(condition,#DQUOTE$,"'",#PB_String_InPlace)
    condition = ReplaceString(condition,"'","''")
    
    If DatabaseQuery(dbid, "select ("+condition+");")
      If NextDatabaseRow(dbid)
        result = GetDatabaseLong(dbid, 0)
      EndIf
      FinishDatabaseQuery(dbid)
    Else : Debug condition  
    EndIf
    ProcedureReturn result
  EndProcedure
  
  Procedure init( )
    ;Protected dataBaseID.i = DatabaseID()
    UseSQLiteDatabase()
    dbID = OpenDatabase(#PB_Any,":memory:","","")
    
    sqlite3_create_function(DatabaseID(dbID), "match",  2, #SQLITE_UTF8, #Null, @sql_regex(), #Null, #Null)
  EndProcedure
  
  init()
EndModule

CompilerIf #PB_Compiler_IsMainFile
  debug  "No: "  + Str(SQLeval::uate(~"\"this\" == \"that\"" ))
  debug  "Yes: " + Str(SQLeval::uate(~"\"this\" == \"this\"" ))
  debug  "No: "  + Str(SQLeval::uate("3 > 4"))
  debug  "Yes: " + Str(SQLeval::uate("3 < 4"))
  ; sqlite specific "like"
  debug  "Yes: " + Str(SQLeval::uate(~"\"I am josh,and I am here\" like \"%josh%\""))
  debug  "No: "  + Str(SQLeval::uate(~"\"I am joXh,and I am here\" like \"%josh%\""))
  debug  "Yes: " + Str(SQLeval::uate(~"match(\"I am josh, and i am here\",\"josh\")"))
  debug  "Yes: " + Str(SQLeval::uate(~"match(\"Find the number 423 but not 324\",\"4[0-9]{2}\")"))
  debug  "NO: "  + Str(SQLeval::uate(~"match(\"I am josh, and i am here\",\"Josh\")"))
  debug  "yes: " + Str(SQLeval::uate(~"match(\"I am josh, and i am here\",\"(?i)Josh\")"))
  debug  "No: "  + Str(SQLeval::uate(~"match(\"Won't Find neither the number 4.23 nor not 324\",\"4[0-9]{2}\")"))
  ; more realistic example
  Define user.s = "%dow% == 2 and %hour% > 12" ; ie: User entered the condition in a stringgadget.\
  ; now replace the application specific tokens
  user = ReplaceString(user,"%hour%",FormatDate("%hh",Date()))
  user = ReplaceString(user,"%dow%", Str(DayOfWeek(Date())))
  debug  "Tuesday, after 12? "+Str(SQLeval::uate( user ))
CompilerEndIf

Re: evaluate user-defined conditions at runtime using Lua or SQLite

Posted: Wed Apr 19, 2023 12:53 am
by Demivec
FYi, the evaluate() procedure that handles things through lua appears to have a memory leak. It doesn't free the memory pointed to by *script but instead just keeps allocating new memory by using Ascii().

Re: evaluate user-defined conditions at runtime using Lua or SQLite

Posted: Wed Apr 19, 2023 1:49 am
by jassing
Demivec wrote: Wed Apr 19, 2023 12:53 am FYi, the evaluate() procedure that handles things through lua appears to have a memory leak. It doesn't free the memory pointed to by *script but instead just keeps allocating new memory by using Ascii().
You're right - fixed; thanks.