#!/usr/bin/lua local debug = false local xdebug = false local stack, while_stack, if_stack, commands = {}, {}, {}, {} local executing = true local look_for_end_while = false -- basic stack manipulation local function push( val ) table.insert( stack, tostring( val ) ) end local function pop() return table.remove( stack ) end local function peek( n ) return stack[ table.getn( stack ) - n ] end -- returns true if every element of tab is true -- uses i and v because a for loop with ipairs() was about 30% slower local function multiand( tab ) local i = 1 local v = tab[ i ] while v ~= nil do if not v then return false end i = i + 1 v = tab[ i ] end return true end -- executes the given function (in the form of an array of ops) local function exec_ops( optable ) local i = 1 local v = optable[ i ] while v do if look_for_end_while then if v == keywords["repeat"] or v == keywords["until"] then look_for_end_while = false end i = i + 1 else if executing then local oldi = i i = v( i ) if i == 0 then return end -- ;, exit while type( i ) == "string" do -- execute if keywords[ i ] then i = keywords[ i ]() elseif userfuncs[ i ] then i = userfuncs[ i ]() else if debug then io.write( "invalid function name: ", i, "\n" ) end break end end if not i then i = oldi + 1 end if xdebug then if optable.name then io.write( optable.name, ": ", oldi, " (", ( commands[ optable.loc + i ] or "" ), "): { " ) for _, v in ipairs( stack ) do io.write( "\"", v, "\" " ) end io.write( "}\n" ) end end if done then return end else if v == keywords["if"] or v == keywords["else"] or v == keywords["then"] then v() end i = i + 1 end end v = optable[ i ] end if debug then io.write( "unterminated function: ", optable.name, "\n" ) end end -- __call metamethod for ops local function op_call( func ) local d, t = func.data, type( func.data ) if t == "table" then return exec_ops( d ) end if t == "string" then if string.sub( d, 1, 1 ) == "\"" then return push( string.sub( d, 2 ) ) elseif keywords[ d ] then return keywords[ d ]() elseif userfuncs[ d ] then return userfuncs[ d ]() end end if t == "number" then return push( d ) end end -- user defined variable table, with some initial definitions local uservars = { ["pi"] = math.pi, ["e"] = math.exp( 1 ), } -- built in keywords for the language keywords = { ["+"] = function() push( pop() + pop() ) end , ["-"] = function() push( -pop() + pop() ) end , ["*"] = function() push( pop() * pop() ) end , ["/"] = function() push( 1 / pop() * pop() ) end , ["%"] = function() local num1, num2 = pop(), pop() push( math.mod( num2, num1 ) ) end , ["^"] = function() local num1, num2 = pop(), pop() push( num2 ^ num1 ) end , ["notify"] = function() io.write( pop(), "\n" ) end , ["read"] = function() push( io.read() ) end , ["pop"] = function() pop() end , ["dup"] = function() push( peek( 0 ) ) end , ["swap"] = function() local val1, val2 = pop(), pop() push( val1 ) push( val2 ) end , ["over"] = function() push( peek( 1 ) ) end , ["rotate"] = function() local num = tonumber( pop() ) if num > 0 then push( table.remove( stack, table.getn( stack ) - num + 1 ) ) elseif num < 0 then table.insert( stack, table.getn( stack ) + num + 1, table.remove( stack ) ) end end , ["pick"] = function() push( peek( pop() - 1 ) ) end , ["="] = function() if tonumber( pop() ) == tonumber( pop() ) then push( 1 ) else push( 0 ) end end , ["!="] = function() if tonumber( pop() ) ~= tonumber( pop() ) then push( 1 ) else push( 0 ) end end , [">"] = function() if tonumber( pop() ) < tonumber( pop() ) then push( 1 ) else push( 0 ) end end , ["<"] = function() if tonumber( pop() ) > tonumber( pop() ) then push( 1 ) else push( 0 ) end end , [">="] = function() if tonumber( pop() ) <= tonumber( pop() ) then push( 1 ) else push( 0 ) end end , ["<="] = function() if tonumber( pop() ) >= tonumber( pop() ) then push( 1 ) else push( 0 ) end end , ["sin"] = function() push( math.sin( pop() ) ) end , ["cos"] = function() push( math.cos( pop() ) ) end , ["tan"] = function() push( math.tan( pop() ) ) end , ["asin"] = function() push( math.asin( pop() ) ) end , ["acos"] = function() push( math.acos( pop() ) ) end , ["atan"] = function() push( math.atan( pop() ) ) end , ["atan2"] = function() local num = pop() push( math.atan2( pop(), num ) ) end , ["abs"] = function() push( math.abs( pop() ) ) end , ["ceil"] = function() push( math.ceil( pop() ) ) end , ["floor"] = function() push( math.floor( pop() ) ) end , ["rad"] = function() push( math.rad( pop() ) ) end , ["deg"] = function() push( math.deg( pop() ) ) end , ["log"] = function() push( math.log10( pop() ) ) end , ["ln"] = function() push( math.log( pop() ) ) end , ["max"] = function() push( math.max( pop(), pop() ) ) end , ["min"] = function() push( math.min( pop(), pop() ) ) end , ["and"] = function() -- locals to avoid short circuiting local num1, num2 = tonumber( pop() ), tonumber( pop() ) if num1 ~= 0 and num2 ~= 0 then push( 1 ) else push( 0 ) end end , ["or"] = function() -- locals to avoid short circuiting local num1, num2 = tonumber( pop() ), tonumber( pop() ) if num1 == 0 and num2 == 0 then push( 0 ) else push( 1 ) end end , ["not"] = function() if tonumber( pop() ) == 0 then push( 1 ) else push( 0 ) end end , ["strcat"] = function() local str = pop() push( pop() .. str ) end , ["spac"] = function() push( " " ) end , ["empty"] = function() while peek( 0 ) do pop() end end , ["stack"] = function() for i = 0, table.getn( stack ) - 1 do io.write( peek( i ), "\n" ) end end , ["depth"] = function() push( table.getn( stack ) ) end , ["explode"] = function() local delim, str, count, ret = pop(), pop(), 0, {} str = str .. delim for match in string.gfind( str, "(.-)" .. delim ) do table.insert( ret, match ) count = count + 1 end -- push in reverse order so they pop off in order for i = table.getn( ret ), 1, -1 do push( ret[ i ] ) end push( count ) end , ["!"] = function() uservars[ pop() ] = pop() end , ["@"] = function() push( uservars[ pop() ] or 0 ) end , ["unlet"] = function() uservars[ pop() ] = nil end , ["begin"] = function( i ) table.insert( while_stack, { loc = i + 1, ifs = 0 } ) end , ["while"] = function( i ) if tonumber( pop() ) ~= 0 then return i + 1 else table.remove( while_stack ) look_for_end_while = true end end , ["until"] = function() if tonumber( pop() ) == 0 then return while_stack[ table.getn( while_stack ) ].loc else table.remove( while_stack ) end end , ["repeat"] = function() return while_stack[ table.getn( while_stack ) ].loc end , ["continue"] = function() -- different from repeat since break ignores it return while_stack[ table.getn( while_stack ) ].loc end , ["if"] = function() if pop() == '0' then table.insert( if_stack, false ) else table.insert( if_stack, true ) end if table.getn( while_stack ) > 0 then while_stack[ table.getn( while_stack ) ].ifs = while_stack[ table.getn( while_stack ) ].ifs + 1 end executing = multiand( if_stack ) end , ["else"] = function() if_stack[ table.getn( if_stack ) ] = not if_stack[ table.getn( if_stack ) ] executing = multiand( if_stack ) end , ["then"] = function() table.remove( if_stack ) if table.getn( while_stack ) > 0 then while_stack[ table.getn( while_stack ) ].ifs = while_stack[ table.getn( while_stack ) ].ifs - 1 end executing = multiand( if_stack ) end , ["break"] = function() local ifs = while_stack[ table.getn( while_stack ) ].ifs while ifs > 0 do table.remove( if_stack ) ifs = ifs - 1 end table.remove( while_stack ) look_for_end_while = true end , ["address?"] = function() local str = pop() if userfuncs[ str ] or keywords[ str ] then push( 1 ) else push( 0 ) end end , ["int?"] = function() local num = tonumber( pop() ) if num and num == math.floor( num ) then push( 1 ) else push( 0 ) end end , ["number?"] = function() if tonumber( pop() ) then push( 1 ) else push( 0 ) end end , ["put"] = function() stack[ table.getn( stack ) - pop() - 1 ] = pop() end , ["random"] = function() push( math.random( pop() ) ) end , ["date"] = function() local date = os.date( "*t" ) push( date.year ) push( date.month ) push( date.day ) end , ["execute"] = function() return pop() end , ["instr"] = function() local find = pop() push( string.find( pop(), find, 1, true ) or 0 ) end , ["rinstr"] = function() local last_found, found local find, str = pop(), pop() found = string.find( str, find, 1, true ) if not found then push( 0 ); return end while found do last_found = found + 1 found = string.find( str, find, last_found, true ) end push( last_found - 1 ) end , ["strcut"] = function() local num, str = pop(), pop() push( string.sub( str, num + 1 ) ) push( string.sub( str, 1, num ) ) end , ["stringpfx"] = function() local pfx, str = pop(), pop() if string.sub( str, 1, string.len( pfx ) ) == pfx then push( 1 ) else push( 0 ) end end , ["strcmp"] = function() local str1, str2 = pop(), pop() local i = 1 while string.byte( str1, i ) == string.byte( str2, i ) do if not string.byte( str1, i ) then push( 0 ) return end i = i + 1 end local char1 = string.byte( str1, i ) local char2 = string.byte( str2, i ) if char1 and char2 then push( char1 - char2 ) else push( char1 or -char2 ) end end , ["strncmp"] = function() local count, str1, str2 = pop(), pop(), pop() local i = 1 for j = 1, count - 1 do if string.byte( str1, i ) ~= string.byte( str2, i ) then break end if not string.byte( str1, i ) then push( 0 ) return end i = i + 1 end local char1 = string.byte( str1, i ) local char2 = string.byte( str2, i ) if char1 and char2 then push( char1 - char2 ) else push( char1 or -char2 ) end end , ["subst"] = function() local old, new, str = pop(), pop(), pop() push( string.gsub( str, old, new ) ) end , ["strlen"] = function() push( string.len( pop() ) ) end , ["tolower"] = function() push( string.lower( pop() ) ) end , ["toupper"] = function() push( string.upper( pop() ) ) end , ["striplead"] = function() push( string.gsub( pop(), "^%s*", "" ) ) end , ["striptail"] = function() push( string.gsub( pop(), "%s*$", "" ) ) end , ["systime"] = function() push( os.time() ) end , ["time"] = function() local date = os.date( "*t" ) push( date.sec ) push( date.min ) push( date.hour ) end , ["timefmt"] = function() local time = pop() push( os.date( pop(), time ) ) end , ["timesplit"] = function() local date = os.date( "*t", pop() ) push( date.sec ) push( date.min ) push( date.hour ) push( date.day ) push( date.month ) push( date.year ) push( date.wday ) push( date.yday ) end , ["debug"] = function() debug = not debug end , ["xdebug"] = function() xdebug = not xdebug end , [";"] = function() return 0 end , ["exit"] = function() return 0 end , [";;"] = function() done = true end } -- user defined functions, with some initial definitions userfuncs = { ["wait"] = setmetatable( { data = { keywords["read"], keywords["pop"], keywords[";"] }, name = "wait" }, { __call = op_call } ) , ["spacecat"] = setmetatable( { data = { keywords["spac"], keywords["swap"], keywords["strcat"], keywords["strcat"], keywords[";"] }, name = "spacecat" }, { __call = op_call } ) , ["incr"] = setmetatable( { data = { setmetatable( { data = 1 }, { __call = op_call } ), keywords["+"], keywords[";"] }, name = "incr" }, { __call = op_call } ) , ["decr"] = setmetatable( { data = { setmetatable( { data = 1 }, { __call = op_call } ), keywords["-"], keywords[";"] }, name = "decr" }, { __call = op_call } ) , ["root"] = setmetatable( { data = { setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords["^"], keywords[";"] }, name = "root" }, { __call = op_call } ) , ["sqrt"] = setmetatable( { data = { setmetatable( { data = .5 }, { __call = op_call } ), keywords["^"], keywords[";"] }, name = "sqrt" }, { __call = op_call } ) , ["csc"] = setmetatable( { data = { keywords["sin"], setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords[";"] }, name = "csc" }, { __call = op_call } ) , ["sec"] = setmetatable( { data = { keywords["cos"], setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords[";"] }, name = "sec" }, { __call = op_call } ) , ["cot"] = setmetatable( { data = { keywords["tan"], setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords[";"] }, name = "cot" }, { __call = op_call } ) , ["acsc"] = setmetatable( { data = { setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords["asin"], keywords[";"] }, name = "acsc" }, { __call = op_call } ) , ["asec"] = setmetatable( { data = { setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords["acos"], keywords[";"] }, name = "asec" }, { __call = op_call } ) , ["acot"] = setmetatable( { data = { setmetatable( { data = 1 }, { __call = op_call } ), keywords["swap"], keywords["/"], keywords["atan"], keywords[";"] }, name = "acot" }, { __call = op_call } ) , ["instring"] = setmetatable( { data = { keywords["tolower"], keywords["swap"], keywords["tolower"], keywords["swap"], keywords["instr"], keywords[";"] }, name = "instring" }, { __call = op_call } ) , ["rinstring"] = setmetatable( { data = { keywords["tolower"], keywords["swap"], keywords["tolower"], keywords["swap"], keywords["rinstr"], keywords[";"] }, name = "rinstring" }, { __call = op_call } ) , ["stringcmp"] = setmetatable( { data = { keywords["tolower"], keywords["swap"], keywords["tolower"], keywords["swap"], keywords["strcmp"], keywords[";"] }, name = "stringcmp" }, { __call = op_call } ) , ["strip"] = setmetatable( { data = { keywords["striplead"], keywords["striptail"], keywords[";"] }, name = "strip" }, { __call = op_call } ) , ["rot"] = setmetatable( { data = { setmetatable( { data = 3 }, { __call = op_call } ), keywords["rotate"], keywords[";"] }, name = "rot" }, { __call = op_call } ) } local from_file, continuing = false, false local script_file = io.stdin if arg[ 1 ] then -- use explicit file descriptors so that read will work properly in files script_file = assert( io.open( arg[ 1 ], "r" ) ) from_file = true end local whiles, ifs = 0, 0 -- main program loop done = false while not done do if not from_file then if not continuing then io.write( "> " ) else io.write( ">> " ) end end local exec_str if from_file then exec_str = script_file:read( "*a" ) else if continuing then exec_str = script_file:read() else exec_str = ": main " .. script_file:read() end end -- split the input string (add stuff from args here for quote parsing) local comment = false for word in string.gfind( exec_str, "%S+" ) do if word == "begin" then whiles = whiles + 1 end if word == "repeat" then whiles = whiles - 1 end if word == "until" then whiles = whiles - 1 end if word == "if" then ifs = ifs + 1 end if word == "then" then ifs = ifs - 1 end if word == "(" then comment = true end if not comment then table.insert( commands, word ) end if word == ")" then comment = false end --io.stdout:write( word .. ": " .. whiles .. " " .. ifs .. " \n" ) end if ifs ~= 0 or whiles ~= 0 then continuing = true else continuing = false -- no need to test for from_file here since extra ;s are ignored table.insert( commands, ";" ) end if not continuing then -- don't execute if command list is incomplete local current_op_table = setmetatable( {}, { __call = op_call } ) -- turn the tokens into opcodes local is_funcname = false for i, v in ipairs( commands ) do if is_funcname then current_op_table.name = v current_op_table.loc = i - 1 userfuncs[ v ] = true -- allow recursive definitions is_funcname = false elseif v == ":" then if current_op_table.name then -- recursion. this stores a table reference, so multiple -- levels of recursion work for i, v in ipairs( current_op_table ) do if v == current_op_table.name then current_op_table[ i ] = setmetatable( { data = current_op_table }, { __call = op_call } ) end end userfuncs[ current_op_table.name ] = setmetatable( { data = current_op_table }, { __call = op_call } ) end current_op_table = {} is_funcname = true -- this allows stray - signs within decimals (like 3.14-15) fix? elseif string.find( v, "^%-?%d*%.?%d+[eE]?%-?%d*$" ) then table.insert( current_op_table, setmetatable( { data = tonumber( v ) }, { __call = op_call } ) ) elseif string.find( v, "^\".+" ) then table.insert( current_op_table, setmetatable( { data = v }, { __call = op_call } ) ) elseif keywords[ v ] then table.insert( current_op_table, keywords[ v ] ) elseif type( userfuncs[ v ] ) == "boolean" then table.insert( current_op_table, v ) -- string for recursion elseif type( userfuncs[ v ] ) == "table" then table.insert( current_op_table, userfuncs[ v ] ) else if debug then io.write( "unknown word: ", v, "\n" ) end break end end -- grab the last function definition userfuncs[ current_op_table.name ] = current_op_table -- execute the main function if not done then exec_ops( userfuncs.main ) end if debug then io.write( "{ " ) for _, v in ipairs( stack ) do io.write( "\"", v, "\" " ) end io.write( "}\n" ) end commands = {} end -- we read the whole file at once, so looping won't get us anymore text if from_file then break end end