mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
virtualcomputer: use real BASIC
Former-commit-id: 557df532d4c9693b5b57501c0f67e7d95266f9ba Former-commit-id: 974ea7882079fb3902f00ab821a7c8c54390d0b4
This commit is contained in:
@@ -1045,33 +1045,14 @@ require("ROMLIB")
|
||||
speaker.enqueue(80, computer.bellpitch) -- term.bell sometimes get squelched
|
||||
|
||||
-- load bios, if any
|
||||
if fs.exists(computer.bootloader) then fs.dofile(computer.bootloader) end
|
||||
--if fs.exists(computer.bootloader) then fs.dofile(computer.bootloader) end
|
||||
|
||||
-- halt/run luaprompt upon the termination of bios.
|
||||
-- Valid BIOS should load OS and modify 'shell.status' to 'shell.halt' before terminating itself.
|
||||
if shell.status == shell.halt then __haltsystemexplicit__() goto quit end
|
||||
|
||||
-- load Lua prompt, if bios is not found
|
||||
print("Rom basic "..DC2.._VERSION..DC4)
|
||||
print("Lua is copyrighted (C) 1994-2013 Lua.org, PUC-Rio")
|
||||
print()
|
||||
|
||||
while not machine.isHalted() do
|
||||
term.setCursorBlink(true)
|
||||
io.write(computer.prompt)
|
||||
local s = __scanforline__()
|
||||
xpcall(
|
||||
function()
|
||||
if s:byte(1) == 61 then -- print out value
|
||||
s1 = string.sub(s, 2)
|
||||
_G.runscript("print(tostring("..s1.."))\n", "=stdin")
|
||||
else
|
||||
_G.runscript(s, "=stdin")
|
||||
end
|
||||
end,
|
||||
function(err) print(DLE..err) end -- it catches logical errors
|
||||
)
|
||||
end
|
||||
dofile "/net/torvald/terrarum/virtualcomputer/assets/lua/TBASIC.lua"
|
||||
|
||||
::quit::
|
||||
machine.closeInputString()
|
||||
|
||||
525
src/net/torvald/terrarum/virtualcomputer/assets/lua/TBASEXEC.lua
Normal file
525
src/net/torvald/terrarum/virtualcomputer/assets/lua/TBASEXEC.lua
Normal file
@@ -0,0 +1,525 @@
|
||||
--[[
|
||||
TBASIC: Simple BASIC language based on the Commodore BASIC Version 2.
|
||||
|
||||
(C64 rulz? Nope.)
|
||||
|
||||
|
||||
How to use in your program:
|
||||
|
||||
1. load the script by:
|
||||
if you're using ComputerCraft, use:
|
||||
os.loadAPI "TBASEXEC.lua"
|
||||
else, use:
|
||||
require "TBASEXEC"
|
||||
|
||||
2. run:
|
||||
_TBASIC.EXEC(string of whole command)
|
||||
]]
|
||||
|
||||
if os and os.loadAPI then -- ComputerCraft
|
||||
os.loadAPI "TBASINCL.lua"
|
||||
else
|
||||
require "TBASINCL"
|
||||
end
|
||||
|
||||
table.concat = function(t, delimeter)
|
||||
if #t == 0 then return "" end
|
||||
local outstr = t[1]
|
||||
for i = 2, #t do
|
||||
outstr = outstr..delimeter..tostring(t[i])
|
||||
end
|
||||
|
||||
return outstr
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- INTERPRETER STATUS ---------------------------------------------------------
|
||||
|
||||
local programlist = {}
|
||||
|
||||
-- LEXER ----------------------------------------------------------------------
|
||||
|
||||
local function appendcommand(lineno, statement)
|
||||
if lineno > _TBASIC._INTPRTR.MAXLINES then
|
||||
_TBASIC._ERROR.LINETOOBIG()
|
||||
elseif lineno < 0 then
|
||||
_TBASIC._ERROR.NOLINENUM()
|
||||
else
|
||||
programlist[lineno] = statement
|
||||
end
|
||||
end
|
||||
|
||||
do -- Avoid heap allocs for performance
|
||||
local tokens = {" ", "\t"} -- initial obvious tokens
|
||||
local longest_token_len = 0
|
||||
-- build 'tokens' table from list of operators from the language
|
||||
for _, v in ipairs(_TBASIC._OPERATR) do
|
||||
if not v:match("[A-Za-z]") then -- we want non-alphabetic operators as a token
|
||||
table.insert(tokens, v)
|
||||
-- get longest_token_len, will be used for 'lookahead'
|
||||
local tokenlen = #v
|
||||
if longest_token_len < #v then
|
||||
longest_token_len = #v
|
||||
end
|
||||
end
|
||||
end
|
||||
-- sort them out using ther hash for binary search
|
||||
table.sort(tokens, function(a, b) return string.hash(a) < string.hash(b) end)
|
||||
|
||||
|
||||
function parsewords(line)
|
||||
if line == nil then return end
|
||||
|
||||
-----------------------
|
||||
-- check line sanity --
|
||||
-----------------------
|
||||
|
||||
-- filter for IF statement
|
||||
if line:match("[Ii][Ff]") then
|
||||
-- no matching THEN
|
||||
if not line:match("[Tt][Hh][Ee][Nn]") then
|
||||
_TBASIC._ERROR.NOMATCHING("IF", "THEN")
|
||||
-- assignment on IF clause
|
||||
elseif line:match("[Ii][Ff][^\n]+[Tt][Hh][Ee][Nn]"):match("[^=]=[^=]") or
|
||||
line:match("[Ii][Ff][^\n]+[Tt][Hh][Ee][Nn]"):match(":=") then
|
||||
_TBASIC._ERROR.ASGONIF()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- automatically infer and insert some commands --
|
||||
--------------------------------------------------
|
||||
-- (This is starting to get dirty...)
|
||||
|
||||
-- unary minus
|
||||
local matchobj = line:find("%-[0-9]")
|
||||
if matchobj then -- in this pattern, it always returns a number
|
||||
local newline = line:sub(1, matchobj - 1) .. "MINUS " .. line:sub(matchobj + 1, #line)
|
||||
line = newline
|
||||
end
|
||||
-- conditional for IF
|
||||
-- if IF statement has no appended paren
|
||||
if not line:match("[Ii][Ff][ ]*%(") then
|
||||
local newline = line:gsub("[Ii][Ff]", "IF ( "):gsub("[Tt][Hh][Ee][Nn]", " ) THEN")
|
||||
line = newline
|
||||
end
|
||||
-- special treatment for FOR
|
||||
if line:sub(1, 3):upper() == "FOR" then
|
||||
if line:match("[0-9]?%.[0-9]") then -- real number used (e.g. "3.14", ".5")
|
||||
_TBASIC._ERROR.ILLEGALARG()
|
||||
else
|
||||
local varnameintm = line:match(" [^\n]+[ =]")
|
||||
|
||||
if varnameintm then
|
||||
local varname = varnameintm:match("[^= ]+")
|
||||
if varname then
|
||||
local newline = line:gsub(" "..varname.."[ =]", " $"..varname.." "..varname.." = ")
|
||||
line = newline:gsub("= =", "=")
|
||||
else
|
||||
_TBASIC._ERROR.SYNTAX()
|
||||
end
|
||||
end
|
||||
-- basically, "FOR x x = 1 TO 10", which converts to "x x 1 10 TO = FOR",
|
||||
-- which is executed (in RPN) in steps of:
|
||||
-- "x x 1 10 TO = FOR"
|
||||
-- "x x (arr) = FOR"
|
||||
-- "x FOR" -- see this part? we need extra 'x' to feed for the FOR statement to function
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
printdbg("parsing line", line)
|
||||
|
||||
|
||||
|
||||
lextable = {}
|
||||
isquote = false
|
||||
quotemode = false
|
||||
wordbuffer = ""
|
||||
local function flush()
|
||||
if (#wordbuffer > 0) then
|
||||
table.insert(lextable, wordbuffer)
|
||||
wordbuffer = ""
|
||||
end
|
||||
end
|
||||
local function append(char)
|
||||
wordbuffer = wordbuffer..char
|
||||
end
|
||||
local function append_no_whitespace(char)
|
||||
if char ~= " " and char ~= "\t" then
|
||||
wordbuffer = wordbuffer..char
|
||||
end
|
||||
end
|
||||
|
||||
-- return: lookless_count on success, nil on failure
|
||||
local function isdelimeter(string)
|
||||
local cmpval = function(table_elem) return string.hash(table_elem) end
|
||||
local lookless_count = #string
|
||||
local ret = nil
|
||||
repeat
|
||||
ret = table.binsearch(tokens, string:sub(1, lookless_count), cmpval)
|
||||
lookless_count = lookless_count - 1
|
||||
until ret or lookless_count < 1
|
||||
return ret and lookless_count + 1 or false
|
||||
end
|
||||
|
||||
local i = 1 -- Lua Protip: variable in 'for' is immutable, and is different from general variable table, even if they have same name
|
||||
while i <= #line do
|
||||
local c = string.char(line:byte(i))
|
||||
|
||||
local lookahead = line:sub(i, i+longest_token_len)
|
||||
|
||||
if isquote then
|
||||
if c == [["]] then
|
||||
flush()
|
||||
isquote = false
|
||||
else
|
||||
append(c)
|
||||
end
|
||||
else
|
||||
if c == [["]] then
|
||||
isquote = true
|
||||
append_no_whitespace("~")
|
||||
else
|
||||
local delimsize = isdelimeter(lookahead) -- returns nil if no matching delimeter found
|
||||
if delimsize then
|
||||
flush() -- flush buffer
|
||||
append_no_whitespace(lookahead:sub(1, delimsize))
|
||||
flush() -- flush this delimeter
|
||||
i = i + delimsize - 1
|
||||
else
|
||||
append_no_whitespace(c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
flush() -- don't forget this!
|
||||
|
||||
|
||||
return lextable
|
||||
end
|
||||
end
|
||||
|
||||
local function readprogram(program)
|
||||
for line in program:gmatch("[^\n]+") do
|
||||
lineno = line:match("[0-9]+ ", 1)
|
||||
|
||||
if not lineno then
|
||||
_TBASIC._ERROR.NOLINENUM()
|
||||
end
|
||||
|
||||
statement = line:sub(#lineno + 1)
|
||||
|
||||
appendcommand(tonumber(lineno), statement)
|
||||
end
|
||||
end
|
||||
|
||||
do -- Avoid heap allocs for performance
|
||||
local function stackpush(t, v)
|
||||
t[#t + 1] = v
|
||||
end
|
||||
|
||||
local function stackpop(t)
|
||||
local v = t[#t]
|
||||
t[#t] = nil
|
||||
return v
|
||||
end
|
||||
|
||||
local function stackpeek(t)
|
||||
local v = t[#t]
|
||||
return v
|
||||
end
|
||||
|
||||
local function unmark(word)
|
||||
if type(word) == "table" then return word end
|
||||
return word:sub(2, #word)
|
||||
end
|
||||
|
||||
local function isoperator(word)
|
||||
if word == nil then return false end
|
||||
return word:byte(1) == 35
|
||||
end
|
||||
|
||||
local isvariable = _TBASIC.isvariable
|
||||
|
||||
local function isuserfunc(word)
|
||||
if type(word) == "table" then return false end
|
||||
if word == nil then return false end
|
||||
return word:byte(1) == 64
|
||||
end
|
||||
|
||||
local function isbuiltin(word)
|
||||
if type(word) == "table" then return false end
|
||||
if word == nil then return false end
|
||||
return word:byte(1) == 38
|
||||
end
|
||||
|
||||
local function iskeyword(word)
|
||||
if word == nil then return false end
|
||||
return isoperator(word) or isuserfunc(word) or isbuiltin(word)
|
||||
end
|
||||
|
||||
local function isassign(word)
|
||||
if word == nil then return false end
|
||||
return word ~= "==" and word ~= ">=" and word ~= "<=" and word:byte(#word) == 61
|
||||
end
|
||||
|
||||
local function isnoresolvevar(word)
|
||||
local novarresolve = {"NEXT"}
|
||||
|
||||
for _, w in ipairs(novarresolve) do -- linear search, because the array is small
|
||||
if word:upper() == w then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function execword(word, args)
|
||||
if not _TBASIC.__appexit then
|
||||
printdbg("--> execword", word)
|
||||
printdbg("--> inargs", table.unpack(args))
|
||||
|
||||
-- selectively resolve variable (if it's assign func, bottommost var -- target of assignation -- will not be resolved)
|
||||
-- for command "NEXT": DO NOT RESOLVE, pass its name (Call by Name)
|
||||
if not isnoresolvevar(word) then
|
||||
for i = isassign(word) and 2 or 1, #args do
|
||||
arg = args[i]
|
||||
|
||||
printdbg("--> resolvevar arg", arg)
|
||||
|
||||
if isvariable(arg) then
|
||||
var = unmark(arg)
|
||||
|
||||
if type(var) ~= "table" then
|
||||
value = _TBASIC._INTPRTR.CNSTANTS[var:upper()] -- try for pre-def
|
||||
|
||||
if value == nil then
|
||||
value = _TBASIC._INTPRTR.VARTABLE[var:upper()] -- try for user-def
|
||||
end
|
||||
|
||||
if value == nil then
|
||||
_TBASIC._ERROR.NULVAR(var)
|
||||
end
|
||||
|
||||
args[i] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if word == "IF" then
|
||||
printdbg("--> branch statement 'IF'")
|
||||
if not args[1] then -- if condition 'false'
|
||||
printdbg("--> if condition 'false'", table.unpack(args))
|
||||
return "terminate_loop" -- evaluated as 'true' to Lua
|
||||
else
|
||||
printdbg("--> if condition 'true'", table.unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
printdbg("--> execword outarg", table.unpack(args))
|
||||
result = _TBASIC.LUAFN[word][1](table.unpack(args))
|
||||
|
||||
printdbg("--> result", result)
|
||||
stackpush(execstack, result)
|
||||
end
|
||||
end
|
||||
|
||||
function printdbg(...)
|
||||
local debug = false
|
||||
if debug then print("DBG", ...) end
|
||||
end
|
||||
|
||||
|
||||
function interpretline(line)
|
||||
if not _TBASIC.__appexit then
|
||||
--[[
|
||||
impl
|
||||
|
||||
1. (normalise expr using parsewords)
|
||||
2. use _TBASIC.RPNPARSR to convert to RPN
|
||||
3. execute RPN op set like FORTH
|
||||
|
||||
* "&" - internal functions
|
||||
* "@" - user-defined functions
|
||||
* "$" - variables (builtin constants and user-defined) -- familiar, eh?
|
||||
* "#" - operators
|
||||
* "~" - strings
|
||||
* none prepended - data (number or string)
|
||||
]]
|
||||
|
||||
lextable = parsewords(line)
|
||||
local vararg = -13 -- magic
|
||||
|
||||
|
||||
if lextable and lextable[1] ~= nil then
|
||||
if lextable[1]:upper() == "REM" then return nil end
|
||||
|
||||
printdbg("lextable", table.concat(lextable, "|"))
|
||||
|
||||
-- execute expression
|
||||
exprlist = _TBASIC.TORPN(lextable) -- 2 2 #+ &PRINT for "PRINT 2+2"
|
||||
|
||||
printdbg("trying to exec", table.concat(exprlist, " "), "\n--------")
|
||||
|
||||
execstack = {}
|
||||
|
||||
for _, word in ipairs(exprlist) do
|
||||
printdbg("stack before", table.concat(execstack, " "))
|
||||
printdbg("word", word)
|
||||
|
||||
if iskeyword(word) then
|
||||
printdbg("is keyword")
|
||||
|
||||
funcname = unmark(word)
|
||||
args = {}
|
||||
argsize = _TBASIC._GETARGS(funcname)
|
||||
|
||||
printdbg("argsize", argsize)
|
||||
|
||||
if not argsize then
|
||||
_TBASIC._ERROR.DEV_UNIMPL(funcname)
|
||||
else
|
||||
if argsize ~= vararg then
|
||||
-- consume 'argsize' elements from the stack
|
||||
for argcnt = argsize, 1, -1 do
|
||||
if #execstack == 0 then
|
||||
_TBASIC._ERROR.ARGMISSING(funcname)
|
||||
end
|
||||
args[argcnt] = stackpop(execstack)
|
||||
end
|
||||
else
|
||||
-- consume entire stack
|
||||
local reversedargs = {}
|
||||
while #execstack > 0 and isvariable(stackpeek(execstack)) do
|
||||
stackpush(reversedargs, stackpop(execstack))
|
||||
end
|
||||
-- reverse 'args'
|
||||
while #reversedargs > 0 do
|
||||
stackpush(args, stackpop(reversedargs))
|
||||
end
|
||||
end
|
||||
local terminate_loop = execword(funcname, args)
|
||||
if terminate_loop then
|
||||
printdbg("--> termination of loop")
|
||||
printdbg("--------")
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif isvariable(word) then
|
||||
printdbg("is variable")
|
||||
stackpush(execstack, word) -- push raw variable ($ sign retained)
|
||||
else
|
||||
printdbg("is data")
|
||||
stackpush(execstack, word) -- push number or string
|
||||
end
|
||||
|
||||
printdbg("stack after", table.concat(execstack, " "))
|
||||
printdbg("--------")
|
||||
end
|
||||
|
||||
-- if execstack is not empty, something is wrong
|
||||
if #execstack > 0 then
|
||||
_TBASIC._ERROR.SYNTAX() -- cannot reliably pinpoint which statement has error; use generic error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function termination_condition()
|
||||
return terminated or
|
||||
_TBASIC._INTPRTR.GOTOCNTR > _TBASIC._INTPRTR.GOTOLMIT or
|
||||
_TBASIC.__appexit or
|
||||
#_TBASIC._INTPRTR.CALLSTCK > _TBASIC._INTPRTR.STACKMAX
|
||||
end
|
||||
|
||||
local function fetchnextcmd()
|
||||
cmd = nil
|
||||
repeat
|
||||
_TBASIC._INTPRTR.PROGCNTR = _TBASIC._INTPRTR.PROGCNTR + 1
|
||||
cmd = programlist[_TBASIC._INTPRTR.PROGCNTR]
|
||||
|
||||
if _TBASIC._INTPRTR.PROGCNTR > _TBASIC._INTPRTR.MAXLINES then
|
||||
terminated = true
|
||||
break
|
||||
end
|
||||
until cmd ~= nil
|
||||
|
||||
if cmd ~= nil then
|
||||
if _TBASIC._INTPRTR.TRACE then
|
||||
print("PC", _TBASIC._INTPRTR.PROGCNTR)
|
||||
end
|
||||
|
||||
return cmd
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function interpretall()
|
||||
|
||||
terminated = false
|
||||
|
||||
repeat
|
||||
interpretline(fetchnextcmd())
|
||||
until termination_condition()
|
||||
|
||||
if _TBASIC._INTPRTR.GOTOCNTR > _TBASIC._INTPRTR.GOTOLMIT then
|
||||
_TBASIC._ERROR.TOOLONGEXEC()
|
||||
end
|
||||
end
|
||||
|
||||
-- END OF LEXER ---------------------------------------------------------------
|
||||
|
||||
local testprogram = [[
|
||||
10 PRINT "Hello, world!"
|
||||
]]
|
||||
|
||||
|
||||
_G._TBASIC.EXEC = function(cmdstring) -- you can access this interpreter with this global function
|
||||
_TBASIC._INTPRTR.RESET()
|
||||
programlist = {}
|
||||
readprogram(cmdstring)
|
||||
interpretall()
|
||||
end
|
||||
|
||||
|
||||
if testprogram then
|
||||
_TBASIC._INTPRTR.RESET()
|
||||
programlist = {}
|
||||
readprogram(testprogram)
|
||||
interpretall()
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Terran BASIC (TBASIC)
|
||||
Copyright (c) 2016 Torvald (minjaesong) and the contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the Software), to deal in the
|
||||
Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
100
src/net/torvald/terrarum/virtualcomputer/assets/lua/TBASIC.lua
Normal file
100
src/net/torvald/terrarum/virtualcomputer/assets/lua/TBASIC.lua
Normal file
@@ -0,0 +1,100 @@
|
||||
--[[
|
||||
TBASIC shell
|
||||
|
||||
Synopsis: TBASIC (filename)
|
||||
If no file is specified, interactive mode will be started
|
||||
|
||||
]]
|
||||
|
||||
|
||||
if os and os.loadAPI then -- ComputerCraft
|
||||
os.loadAPI "TBASINCL.lua"
|
||||
os.loadAPI "TBASEXEC.lua"
|
||||
else
|
||||
require "TBASINCL"
|
||||
require "TBASEXEC"
|
||||
end
|
||||
|
||||
args = {...}
|
||||
|
||||
print(_G._TBASIC._VERSION)
|
||||
_TBASIC.PROMPT()
|
||||
_TBASIC.SHOWLUAERROR = false
|
||||
|
||||
|
||||
if args[1] then
|
||||
local prog = nil
|
||||
if fs and fs.open then -- ComputerCraft
|
||||
local inp = assert(fs.open(args[1], "r"))
|
||||
prog = inp:readAll()
|
||||
inp:close()
|
||||
else
|
||||
local inp = assert(io.open(args[1], "r"))
|
||||
prog = inp:read("*all")
|
||||
inp:close()
|
||||
end
|
||||
|
||||
_TBASIC.EXEC(prog)
|
||||
else
|
||||
local terminate_app = false
|
||||
|
||||
|
||||
local lines = {}
|
||||
local lineno = 1
|
||||
while not terminate_app do
|
||||
local __read = false
|
||||
line = io.read()
|
||||
|
||||
if line:upper() == "NEW" then
|
||||
lines = {}
|
||||
lineno = 1
|
||||
elseif line:upper() == "RUN" then
|
||||
_TBASIC.EXEC(table.concat(lines, "\n"))
|
||||
elseif line:upper() == "LIST" then
|
||||
print()
|
||||
print(table.concat(lines, "\n"))
|
||||
_TBASIC.PROMPT()
|
||||
__read = true
|
||||
elseif line:upper() == "EXIT" then
|
||||
terminate_app = true
|
||||
break
|
||||
elseif line:match("[0-9]+ ") then
|
||||
table.insert(lines, line)
|
||||
lineno = lineno + 1
|
||||
__read = true
|
||||
elseif #line == 0 and line:byte(1) ~= 10 and line:byte(1) ~= 13 then
|
||||
__read = true
|
||||
else
|
||||
_TBASIC.EXEC("1 "..line)
|
||||
end
|
||||
|
||||
-- reset
|
||||
if not __read then
|
||||
_TBASIC.PROMPT()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Terran BASIC (TBASIC)
|
||||
Copyright (c) 2016 Torvald (minjaesong) and the contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the Software), to deal in the
|
||||
Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
996
src/net/torvald/terrarum/virtualcomputer/assets/lua/TBASINCL.lua
Normal file
996
src/net/torvald/terrarum/virtualcomputer/assets/lua/TBASINCL.lua
Normal file
@@ -0,0 +1,996 @@
|
||||
-- TBASIC includes
|
||||
|
||||
if not _G.bit and not _G.bit32 then
|
||||
error("This lua implementation does not have bit/bit32 library, aborting.")
|
||||
end
|
||||
|
||||
if not _G.unpack and not table.unpack then
|
||||
error("This lua implementation does not have unpack() function, aborting.")
|
||||
end
|
||||
|
||||
if _G.bit32 then _G.bit = bit32 end -- Lua 5.2 and LuaJIT compatibility (which has 'bit32' but no 'bit')
|
||||
if _G.unpack and not _G.table.unpack then _G.table.unpack = unpack end -- LuaJIT compatibility
|
||||
|
||||
|
||||
-- simple binary search stole and improved from Kotlin Language
|
||||
-- @param cmpval: function that returns numerical value of the value used for searching.
|
||||
-- implementation: function(s) return whateverhashornumber(s) end
|
||||
-- e.g. function(s) return string.hash(s) end -- for string values
|
||||
-- you must implement it by yourself!
|
||||
do -- Avoid heap allocs for performance
|
||||
local default_cmp_fn = function(s) return string.hash(tostring(s)) end
|
||||
|
||||
function table.binsearch(t, value, cmpval)
|
||||
local low = 1
|
||||
local high = #t
|
||||
local cmp = cmpval or default_cmp_fn
|
||||
|
||||
local value = cmp(value)
|
||||
|
||||
while low <= high do
|
||||
local mid = bit.rshift((low + high), 1)
|
||||
local midVal = t[mid]
|
||||
|
||||
if value > cmp(midVal) then
|
||||
low = mid + 1
|
||||
elseif value < cmp(midVal) then
|
||||
high = mid - 1
|
||||
else
|
||||
return mid -- key found
|
||||
end
|
||||
end
|
||||
return nil -- key not found
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
_G._TBASIC = {}
|
||||
_G._TBASIC._VERNUM = 0x0004 -- 0.4
|
||||
_G._TBASIC._VERSION = string.format(" **** TERRAN BASIC V%d.%d **** ", bit.rshift(_TBASIC._VERNUM, 8), bit.band(_TBASIC._VERNUM, 0xFF))
|
||||
_G._TBASIC.PROMPT = function() print("\nREADY.") end
|
||||
_G._TBASIC._INVOKEERR = function(msg, msg1)
|
||||
if msg1 then
|
||||
print("?L".._G._TBASIC._INTPRTR.PROGCNTR..": "..msg.." "..msg1)
|
||||
else
|
||||
print("?L".._G._TBASIC._INTPRTR.PROGCNTR..": "..msg, "ERROR")
|
||||
end
|
||||
if _TBASIC.SHOWLUAERROR then error("Error thrown") end
|
||||
--os.exit(1) -- terminate
|
||||
_G._TBASIC.__appexit = true -- duh, computercraft
|
||||
end
|
||||
_G._TBASIC._ERROR = {
|
||||
SYNTAX = function() _TBASIC._INVOKEERR("SYNTAX") end,
|
||||
SYNTAXAT = function(word) _TBASIC._INVOKEERR("SYNTAX ERROR AT", "'"..word.."'") end,
|
||||
TYPE = function() _TBASIC._INVOKEERR("TYPE MISMATCH") end,
|
||||
ILLEGALNAME = function(name, reason)
|
||||
if reason then
|
||||
_TBASIC._INVOKEERR("ILLEGAL NAME: ".."'"..name.."'", "REASON:"..reason)
|
||||
else
|
||||
_TBASIC._INVOKEERR("ILLEGAL NAME:", "'"..name.."'")
|
||||
end
|
||||
end,
|
||||
ILLEGALARG = function(expected, got)
|
||||
if (not expected) and (not got) then
|
||||
_TBASIC._INVOKEERR("ILLEGAL QUANTITY")
|
||||
elseif not got then
|
||||
_TBASIC._INVOKEERR(expected:upper().." EXPECTED")
|
||||
else
|
||||
_TBASIC._INVOKEERR(expected:upper().." EXPECTED,", "GOT "..got:upper())
|
||||
end
|
||||
end,
|
||||
NOSUCHLINE = function(line) _TBASIC._INVOKEERR("NO SUCH LINE:", line) end,
|
||||
NULFN = function(var) _TBASIC._INVOKEERR("UNDEFINED FUNCTION:", "'"..var.."'") end,
|
||||
NULVAR = function(var) _TBASIC._INVOKEERR("UNDEFINED VARIABLE:", "'"..var.."'") end,
|
||||
DIV0 = function() _TBASIC._INVOKEERR("DIVISION BY ZERO") end,
|
||||
NAN = function() _TBASIC._INVOKEERR("NOT A NUMBER") end,
|
||||
INDETERMINANT = function() _TBASIC._INVOKEERR("INDETERMINANT MATH") end, -- 0^0 is NOT indeterminant, it's 1. This is the language spec.
|
||||
STACKOVFL = function() _TBASIC._INVOKEERR("TOO MANY RECURSION") end,
|
||||
LINETOOBIG = function() _TBASIC._INVOKEERR("TOO BIG LINE NUMBER") end,
|
||||
NOLINENUM = function() _TBASIC._INVOKEERR("NO LINE NUMBER") end,
|
||||
ABORT = function(reason)
|
||||
if reason then
|
||||
_TBASIC._INVOKEERR("PROGRAM", "ABORTED: "..reason)
|
||||
else
|
||||
_TBASIC._INVOKEERR("PROGRAM", "ABORTED")
|
||||
end
|
||||
end,
|
||||
ARGMISSING = function(fname, remark)
|
||||
if remark then
|
||||
_TBASIC._INVOKEERR("MISSING ARGUMENT(S) FOR", "'"..fname.."' ("..remark..")")
|
||||
else
|
||||
_TBASIC._INVOKEERR("MISSING ARGUMENT(S) FOR", "'"..fname.."'")
|
||||
end
|
||||
end,
|
||||
NOMATCHING = function(fname, match) _TBASIC._INVOKEERR("'"..fname.."' HAS NO MACTHING", "'"..match.."'") end,
|
||||
TOOLONGEXEC = function() _TBASIC._INVOKEERR("TOO LONG WITHOUT YIELDING") end,
|
||||
RETURNWOSUB = function() _TBASIC._INVOKEERR("RETURN WITHOUT GOSUB") end,
|
||||
NEXTWOFOR = function() _TBASIC._INVOKEERR("NEXT WITHOUT FOR") end,
|
||||
ASGONIF = function() _TBASIC._INVOKEERR("ASSIGNMENT ON IF CLAUSE") end,
|
||||
SHELLCMD = function() _TBASIC._INVOKEERR("THIS IS A SHELL COMMAND") end,
|
||||
|
||||
DEV_FUCKIT = function() _TBASIC._INVOKEERR("FEELING DIRTY") end,
|
||||
DEV_UNIMPL = function(fname) _TBASIC._INVOKEERR("UNIMPLEMENTED SYNTAX:", "'"..fname.."'") end
|
||||
}
|
||||
_G._TBASIC._FNCTION = {
|
||||
-- variable control
|
||||
"CLR", -- deletes all user-defined variables and functions
|
||||
"DIM", -- allocates an array
|
||||
"DEF", -- defines new function. Synopsis "DEF FN FOOBAR(arg)"
|
||||
"FN", -- denotes function
|
||||
-- flow control
|
||||
"GO", "GOTO", -- considered harmful
|
||||
"GOSUB", "RETURN",
|
||||
"FOR", "NEXT",
|
||||
"DO", -- reserved only
|
||||
"IF", "THEN",
|
||||
--"ELSE", "ELSEIF", -- reserved only, will not be implemented
|
||||
"END", -- terminate program cleanly
|
||||
"ABORT", -- break as if an error occured
|
||||
"ABORTM", -- ABORT with message
|
||||
-- stdio
|
||||
"PRINT",
|
||||
"INPUT",
|
||||
"GET", -- read single key
|
||||
"HTAB", "TAB", -- set cursor's X position
|
||||
"VTAB", -- set cursor's Y position
|
||||
"SCROLL",
|
||||
"CLS", -- clear screen
|
||||
"TEXTCOL", -- foreground colour
|
||||
"BACKCOL", -- background colour
|
||||
-- mathematics
|
||||
"ABS", "SIN", "COS", "TAN", "FLOOR", "CEIL", "ROUND", "LOG",
|
||||
"INT", -- integer part of a number (3.78 -> 3, -3.03 -> -3)
|
||||
"RND", -- random number 0.0 <= x < 1.0
|
||||
"SGN", -- sign of a number (-1, 0, 1)
|
||||
"SQRT", -- square root
|
||||
"CBRT", -- cubic root
|
||||
"MAX", "MIN",
|
||||
"INV", -- returns (1.0 / arg)
|
||||
-- string functions
|
||||
"LEN",
|
||||
"LEFT", -- just like in Excel
|
||||
"MID", -- -- just like in Excel (substring)
|
||||
"RIGHT", -- just like in Excel
|
||||
-- type conversion
|
||||
"ASC", -- converts a charactor into its code point
|
||||
"CHR", -- converts an integer into corresponding character
|
||||
"STR", -- number to string
|
||||
"VAL", -- string to number
|
||||
-- misc
|
||||
"REM", -- mark this line as comment
|
||||
"NEW", -- clean up any programs on the buffer (this is a Shell function)
|
||||
-- pc speaker
|
||||
"BEEP", -- beeps. Synopsis: "BEEP", "BEEP [pattern]" (not for CC)
|
||||
"TEMIT", -- emits a tone. Synopsis: "TEMIT [frequency] [seconds]" (not for CC)
|
||||
-- commands
|
||||
"RUN", -- run a program or a line. Synopsis: "RUN", "RUN [line]" (this is a Shell function)
|
||||
"LIST", -- list currently entered program. Synopsis: "LIST", "LIST [line]", "LIST [from "-" to]" (this is a Shell function)
|
||||
-- external IO
|
||||
"LOAD", -- file load. Synopsis: "LOAD [filename]"
|
||||
"SAVE", -- file save. Synopsis: "SAVE [filename]"
|
||||
}
|
||||
_G._TBASIC._OPERATR = {
|
||||
-- operators
|
||||
">>>", "<<", ">>", "|", "&", "XOR", "!", -- bitwise operations
|
||||
";", -- string concatenation
|
||||
"SIZEOF", -- LENGTH OF string/array. This is not C
|
||||
"==", ">", "<", "<=", "=<", ">=", "=>",
|
||||
"!=", "<>", "><", -- not equal
|
||||
"=", ":=", -- assign
|
||||
"AND", "OR", "NOT",
|
||||
"^", -- math.pow, 0^0 should return 1.
|
||||
"*", "/", "+", "-", -- arithmetic operations
|
||||
"%", -- math.fmod
|
||||
"TO", "STEP", -- integer sequence operator
|
||||
"MINUS", -- unary minus
|
||||
}
|
||||
_G._TBASIC._INTPRTR = {}
|
||||
_G._TBASIC._INTPRTR.TRACE = false -- print program counter while execution
|
||||
_G._TBASIC.SHOWLUAERROR = true
|
||||
|
||||
local function stackpush(t, v)
|
||||
t[#t + 1] = v
|
||||
end
|
||||
|
||||
local function stackpop(t)
|
||||
local v = t[#t]
|
||||
t[#t] = nil
|
||||
return v
|
||||
end
|
||||
|
||||
local function stackpeek(t)
|
||||
local v = t[#t]
|
||||
return v
|
||||
end
|
||||
|
||||
function string.hash(str)
|
||||
local hash = 2166136261
|
||||
for i = 1, #str do
|
||||
hash = hash * 16777619
|
||||
hash = bit.bxor(hash, str:byte(i))
|
||||
end
|
||||
return hash
|
||||
end
|
||||
|
||||
--sort builtin keywords list
|
||||
table.sort(_TBASIC._FNCTION, function(a, b) return string.hash(a) < string.hash(b) end)
|
||||
|
||||
_G._TBASIC._INTPRTR.RESET = function()
|
||||
_TBASIC.__appexit = false
|
||||
_G._TBASIC._INTPRTR.PROGCNTR = 0
|
||||
_G._TBASIC._INTPRTR.MAXLINES = 63999
|
||||
_G._TBASIC._INTPRTR.VARTABLE = {} -- table of variables. [NAME] = data
|
||||
_G._TBASIC._INTPRTR.FNCTABLE = {} -- table of functions. [NAME] = array of strings? (TBA)
|
||||
_G._TBASIC._INTPRTR.CALLSTCK = {}
|
||||
_G._TBASIC._INTPRTR.STACKMAX = 200
|
||||
_G._TBASIC._INTPRTR.CNSTANTS = {
|
||||
M_PI = 3.14159265359,
|
||||
M_2PI = 6.28318530718,
|
||||
M_E = 2.71828182846,
|
||||
M_ROOT2 = 1.41421356237,
|
||||
TRUE = true,
|
||||
FALSE = false,
|
||||
NIL = nil
|
||||
}
|
||||
_G._TBASIC._INTPRTR.GOTOCNTR = 0
|
||||
_G._TBASIC._INTPRTR.GOTOLMIT = 16384
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- FUNCTION IMPLEMENTS --------------------------------------------------------
|
||||
|
||||
local function __assert(arg, expected)
|
||||
if type(arg) ~= expected then
|
||||
_TBASIC._ERROR.ILLEGALARG(expected, type(arg))
|
||||
end
|
||||
end
|
||||
|
||||
local function __assertlhand(lval, expected)
|
||||
if type(lval) ~= expected then
|
||||
_TBASIC._ERROR.ILLEGALARG("LHAND: "..expected, type(lval))
|
||||
end
|
||||
end
|
||||
|
||||
local function __assertrhand(rval, expected)
|
||||
if type(rval) ~= expected then
|
||||
_TBASIC._ERROR.ILLEGALARG("RHAND: "..expected, type(rval))
|
||||
end
|
||||
end
|
||||
|
||||
local function __checknumber(arg)
|
||||
if arg == nil then
|
||||
_TBASIC._ERROR.ILLEGALARG("number", type(arg))
|
||||
else
|
||||
if type(arg) == "table" then
|
||||
repeat
|
||||
tval = arg[1]
|
||||
arg = tval
|
||||
until type(tval) ~= "table"
|
||||
end
|
||||
|
||||
n = tonumber(arg)
|
||||
if n == nil then _TBASIC._ERROR.ILLEGALARG("number", type(arg))
|
||||
else return n end
|
||||
end
|
||||
end
|
||||
|
||||
local function __checkstring(arg)
|
||||
if type(arg) == "function" then
|
||||
_TBASIC._ERROR.ILLEGALARG("STRING/NUMBER/BOOL", type(arg))
|
||||
end
|
||||
|
||||
if type(arg) == "table" then
|
||||
repeat
|
||||
tval = arg[1]
|
||||
arg = tval
|
||||
until type(tval) ~= "table"
|
||||
end
|
||||
|
||||
local strarg = tostring(arg)
|
||||
return strarg:byte(1) == 126 and strarg:sub(2, #strarg) or strarg
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function _fnprint(arg)
|
||||
if type(arg) == "function" then
|
||||
_TBASIC._ERROR.SYNTAX()
|
||||
return
|
||||
end
|
||||
|
||||
if type(arg) == "boolean" then
|
||||
if arg then print(" TRUE")
|
||||
else print(" FALSE") end
|
||||
elseif _TBASIC.isstring(arg) then
|
||||
print(__checkstring(arg))
|
||||
elseif _TBASIC.isnumber(arg) then -- if argument can be turned into a number (e.g. 14321, "541")
|
||||
print(" "..arg)
|
||||
elseif type(arg) == "table" then
|
||||
_fnprint(arg[1]) -- recursion
|
||||
else
|
||||
print(tostring(arg))
|
||||
end
|
||||
end
|
||||
|
||||
local function _fngoto(lnum)
|
||||
local linenum = __checknumber(lnum)
|
||||
|
||||
if linenum < 1 then
|
||||
_TBASIC._ERROR.NOSUCHLINE(linenum)
|
||||
return
|
||||
end
|
||||
|
||||
_TBASIC._INTPRTR.GOTOCNTR = _TBASIC._INTPRTR.GOTOCNTR + 1
|
||||
_TBASIC._INTPRTR.PROGCNTR = linenum - 1
|
||||
end
|
||||
|
||||
local function _fnnewvar(varname, value)
|
||||
_TBASIC._INTPRTR.VARTABLE[varname:upper()] = value
|
||||
end
|
||||
|
||||
local function _fngosub(lnum)
|
||||
local linenum = __checknumber(lnum)
|
||||
|
||||
stackpush(_TBASIC._INTPRTR.CALLSTCK, _TBASIC._INTPRTR.PROGCNTR) -- save current line number
|
||||
_fngoto(linenum)
|
||||
end
|
||||
|
||||
local function _fnreturn()
|
||||
if #_TBASIC._INTPRTR.CALLSTCK == 0 then -- nowhere to return
|
||||
_TBASIC._ERROR.RETURNWOSUB()
|
||||
return
|
||||
end
|
||||
|
||||
local return_line = stackpop(_TBASIC._INTPRTR.CALLSTCK) + 1 -- the line has GOSUB, so advance one
|
||||
_fngoto(return_line)
|
||||
end
|
||||
|
||||
local function _fnabort()
|
||||
_TBASIC._ERROR.ABORT()
|
||||
end
|
||||
|
||||
local function _fnabortmsg(reason)
|
||||
_TBASIC._ERROR.ABORT(__checkstring(reason))
|
||||
end
|
||||
|
||||
local function _fnif(bool)
|
||||
__assert(bool, "boolean")
|
||||
|
||||
if bool == nil then
|
||||
_TBASIC._ERROR.ILLEGALARG()
|
||||
end
|
||||
|
||||
if not bool then
|
||||
_TBASIC._INTPRTR.PROGCNTR = _TBASIC._INTPRTR.PROGCNTR + 1
|
||||
end
|
||||
end
|
||||
|
||||
local function _fnnop()
|
||||
return
|
||||
end
|
||||
|
||||
local function _fnfor(seq)
|
||||
--print("TEST: INTEGER SEQUENCE")
|
||||
--print(table.concat(seq, " "))
|
||||
|
||||
stackpush(_TBASIC._INTPRTR.CALLSTCK, _TBASIC._INTPRTR.PROGCNTR)
|
||||
end
|
||||
|
||||
local function _fnnext(...)
|
||||
if #_TBASIC._INTPRTR.CALLSTCK == 0 then -- nowhere to return
|
||||
_TBASIC._ERROR.NEXTWOFOR()
|
||||
end
|
||||
|
||||
local variables = {...} -- array of strings(varname) e.g. "$X, $Y, $Z"
|
||||
local branch = false
|
||||
-- dequeue intsequences
|
||||
for i, v in ipairs(variables) do
|
||||
local t = nil
|
||||
if _TBASIC.isvariable(v) then
|
||||
t = _TBASIC._INTPRTR.VARTABLE[v:sub(2, #v)]
|
||||
|
||||
if type(t) ~= "table" then
|
||||
_TBASIC._ERROR.ILLEGALARG("ARRAY", type(t))
|
||||
end
|
||||
|
||||
table.remove(t, 1)
|
||||
|
||||
-- unassign variable
|
||||
if #t == 0 then
|
||||
_TBASIC._INTPRTR.VARTABLE[v] = nil
|
||||
branch = true
|
||||
end
|
||||
else
|
||||
_TBASIC._ERROR.ILLEGALARG("ARRAY", type(t))
|
||||
end
|
||||
end
|
||||
|
||||
-- branch? or go back?
|
||||
if not branch then
|
||||
_fngoto(stackpeek(_TBASIC._INTPRTR.CALLSTCK) + 1) -- the line has FOR statement
|
||||
else
|
||||
stackpop(_TBASIC._INTPRTR.CALLSTCK) -- dump the stack
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-- OPERATOR IMPLEMENTS --------------------------------------------------------
|
||||
|
||||
local function booleanise(bool)
|
||||
return bool and "$TRUE" or "$FALSE"
|
||||
end
|
||||
|
||||
local function _opconcat(lval, rval)
|
||||
if type(lval) == "function" then _TBASIC._ERROR.ILLEGALARG("VALUE", "FUNCTION") end
|
||||
if type(rval) == "function" then _TBASIC._ERROR.ILLEGALARG("VALUE", "FUNCTION") end
|
||||
|
||||
local l = (type(lval) == "string" and lval:byte(1)) == 126 and lval:sub(2, #lval) or __checkstring(lval)
|
||||
local r = (type(rval) == "string" and rval:byte(1)) == 126 and rval:sub(2, #rval) or __checkstring(rval)
|
||||
|
||||
ret = l..r
|
||||
return ret:byte(1) == 126 and "~"..ret or ret -- re-append missing "~" if applicable
|
||||
end
|
||||
|
||||
local function _opplus(lval, rval)
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return l + r
|
||||
end
|
||||
|
||||
local function _optimes(lval, rval)
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return l * r
|
||||
end
|
||||
|
||||
local function _opminus(lval, rval) return _opplus(lval, -rval) end
|
||||
|
||||
local function _opdiv(lval, rval)
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
if l == 0 and r == 0 then
|
||||
_TBASIC._ERROR.INDETERMINANT()
|
||||
elseif r == 0 then
|
||||
_TBASIC._ERROR.DIV0()
|
||||
else
|
||||
return _optimes(l, 1.0 / r)
|
||||
end
|
||||
end
|
||||
|
||||
local function _opmodulo(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return math.fmod(l, r)
|
||||
end
|
||||
|
||||
local function _oppower(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return math.pow(l, r) -- 0^0 is 1 according to the spec, and so is the Lua's.
|
||||
end
|
||||
|
||||
local function _opassign(var, value)
|
||||
if _TBASIC.isnumber(var) or _TBASIC.isfunction(var) or _TBASIC.isoperator(var) or _TBASIC.isargsep(var) then
|
||||
_TBASIC._ERROR.ILLEGALNAME(var)
|
||||
end
|
||||
|
||||
-- remove missed "$"
|
||||
local varname = var:byte(1) == 36 and var:sub(2, #var) or var
|
||||
|
||||
-- if it still has "$", the programmer just broke the law
|
||||
if varname:byte(1) == 36 then
|
||||
_TBASIC._ERROR.ILLEGALNAME(varname, "HAS ILLEGAL CHARACTER '$'")
|
||||
end
|
||||
|
||||
_TBASIC._INTPRTR.VARTABLE[varname:upper()] = value
|
||||
end
|
||||
|
||||
local function _opeq(lval, rval) return booleanise(__checkstring(lval) == __checkstring(rval)) end
|
||||
local function _opne(lval, rval) return booleanise(__checkstring(lval) ~= __checkstring(rval)) end
|
||||
local function _opgt(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return booleanise(l > r)
|
||||
end
|
||||
|
||||
local function _oplt(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return booleanise(l < r)
|
||||
end
|
||||
|
||||
local function _opge(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return booleanise(l >= r)
|
||||
end
|
||||
|
||||
local function _ople(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return booleanise(l <= r)
|
||||
end
|
||||
|
||||
local function _opband(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.band(l, r)
|
||||
end
|
||||
|
||||
local function _opbor(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.bor(l, r)
|
||||
end
|
||||
|
||||
local function _opbxor(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.bxor(l, r)
|
||||
end
|
||||
|
||||
local function _opbnot(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.bnot(l, r)
|
||||
end
|
||||
|
||||
local function _oplshift(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.lshift(l, r)
|
||||
end
|
||||
|
||||
local function _oprshift(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.arshift(l, r)
|
||||
end
|
||||
|
||||
local function _opurshift(lval, rval)
|
||||
local expected = "number"
|
||||
local l = __checknumber(lval)
|
||||
local r = __checknumber(rval)
|
||||
|
||||
return bit.rshift(l, r)
|
||||
end
|
||||
|
||||
local function _opsizeof(target)
|
||||
if type(target) == "table" then
|
||||
-- TODO return dimensional size
|
||||
return #target
|
||||
else
|
||||
_TBASIC._ERROR.ILLEGALARG("string or array", type(lval))
|
||||
end
|
||||
end
|
||||
|
||||
local function _opland(lhand, rhand)
|
||||
return booleanise(lhand and rhand)
|
||||
end
|
||||
|
||||
local function _oplor(lhand, rhand)
|
||||
return booleanise(lhand or rhand)
|
||||
end
|
||||
|
||||
local function _oplnot(rhand)
|
||||
return booleanise(not rhand)
|
||||
end
|
||||
|
||||
local function _opintrange(x, y) -- x TO y -> {x..y}
|
||||
local from = __checknumber(x)
|
||||
local to = __checknumber(y)
|
||||
|
||||
local seq = {}
|
||||
if from < to then
|
||||
for i = from, to do
|
||||
table.insert(seq, i)
|
||||
end
|
||||
else
|
||||
for i = from, to, -1 do
|
||||
table.insert(seq, i)
|
||||
end
|
||||
end
|
||||
|
||||
return seq
|
||||
end
|
||||
|
||||
local function _opintrangestep(seq, stp) -- i know you can just use "for i = from, to, step"
|
||||
local step = __checknumber(stp) -- but that's just how not this stack machine works...
|
||||
__assert(seq, "table")
|
||||
|
||||
if step == 1 then return seq end
|
||||
if step < 1 then _TBASIC._ERROR.ILLEGALARG() end
|
||||
|
||||
local newseq = {}
|
||||
for i, v in ipairs(seq) do
|
||||
if i % step == 1 then
|
||||
table.insert(newseq, v)
|
||||
end
|
||||
end
|
||||
|
||||
return newseq
|
||||
end
|
||||
|
||||
local function _opunaryminus(n)
|
||||
local num = __checknumber(n)
|
||||
return -num
|
||||
end
|
||||
|
||||
local vararg = -13 -- magic
|
||||
|
||||
_G._TBASIC.LUAFN = {
|
||||
-- variable control
|
||||
CLR = {function() _TBASIC._INTPRTR.VARTABLE = {} end, 0},
|
||||
-- flow control
|
||||
IF = {_fnif, 1},
|
||||
THEN = {_fnnop, 0},
|
||||
GOTO = {_fngoto, 1},
|
||||
GOSUB = {_fngosub, 1},
|
||||
RETURN = {_fnreturn, 0},
|
||||
END = {function() _G._TBASIC.__appexit = true end, 0},
|
||||
ABORT = {_fnabort, 0},
|
||||
ABORTM = {_fnabortmsg, 1},
|
||||
FOR = {_fnfor, 1},
|
||||
NEXT = {_fnnext, vararg},
|
||||
-- stdio
|
||||
PRINT = {_fnprint, 1},
|
||||
---------------
|
||||
-- operators --
|
||||
---------------
|
||||
[";"] = {_opconcat, 2},
|
||||
["+"] = {_opplus, 2},
|
||||
["*"] = {_optimes, 2},
|
||||
["-"] = {_opminus, 2},
|
||||
["/"] = {_opdiv, 2},
|
||||
["%"] = {_opmodulo, 2},
|
||||
["^"] = {_oppower, 2},
|
||||
["=="] = {_opeq, 2},
|
||||
["!="] = {_opne, 2}, {["<>"] = _opne, 2}, {["><"] = _opne, 2},
|
||||
[">="] = {_opge, 2}, {["=>"] = _opge, 2},
|
||||
["<="] = {_ople, 2}, {["=<"] = _ople, 2},
|
||||
[">"] = {_opgt, 2},
|
||||
["<"] = {_oplt, 2},
|
||||
["="] = {_opassign, 2}, {[":="] = _opassign, 2},
|
||||
SIZEOF = {_opsizeof, 1},
|
||||
MINUS = {_opunaryminus, 1},
|
||||
-- logical operators
|
||||
AND = {_opland, 2},
|
||||
OR = {_oplor, 2},
|
||||
NOT = {_oplnot, 1},
|
||||
-- bit operators
|
||||
["<<"] = {_oplshift, 2},
|
||||
[">>"] = {_oprshift, 2}, -- bit.arshift
|
||||
[">>>"] = {_opurshift, 2}, -- bit.rshift
|
||||
["|"] = {_opbor, 2},
|
||||
["&"] = {_opband, 2},
|
||||
["!"] = {_opbnot, 2},
|
||||
XOR = {_opbxor, 2},
|
||||
-- int sequence
|
||||
TO = {_opintrange, 2},
|
||||
STEP = {_opintrangestep, 2},
|
||||
-- misc
|
||||
REM = {function() end, 0}
|
||||
}
|
||||
_G._TBASIC._GETARGS = function(func)
|
||||
local f = _TBASIC.LUAFN[func]
|
||||
if f == nil then return nil end
|
||||
return f[2]
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- PARSER IMPL ----------------------------------------------------------------
|
||||
|
||||
local opprecedence = {
|
||||
{":=", "="}, -- least important
|
||||
{"OR"},
|
||||
{"AND"},
|
||||
{"|"},
|
||||
{"XOR"},
|
||||
{"&"},
|
||||
{"==", "!=", "<>", "><"},
|
||||
{"<=", ">=", "=<", "=>", "<", ">"},
|
||||
{"TO", "STEP"},
|
||||
{">>>", "<<", ">>"},
|
||||
{";"},
|
||||
{"+", "-"},
|
||||
{"*", "/", "%"},
|
||||
{"NOT", "!"},
|
||||
{"^", "SIZEOF"}, -- most important
|
||||
{"MINUS"}
|
||||
}
|
||||
local opassoc = {
|
||||
rtl = {";", "^", "NOT", "!", "SIZEOF"}
|
||||
}
|
||||
local function exprerr(token)
|
||||
_TBASIC._ERROR.SYNTAXAT(token)
|
||||
end
|
||||
local function _op_precd(op)
|
||||
-- take care of prematurely prepended '#'
|
||||
local t1 = op:byte(1) == 35 and op:sub(2, #op) or op
|
||||
op = t1:upper()
|
||||
|
||||
for i = 1, #opprecedence do
|
||||
for _, op_in_quo in ipairs(opprecedence[i]) do
|
||||
if op == op_in_quo then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
exprerr("precedence of "..op)
|
||||
end
|
||||
|
||||
local function _op_isrtl(op)
|
||||
for _, v in ipairs(opassoc.rtl) do
|
||||
if op == v then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function _op_isltr(op)
|
||||
return not _op_isrtl(op)
|
||||
end
|
||||
|
||||
|
||||
|
||||
function _G._TBASIC.isnumber(token)
|
||||
return tonumber(token) and true or false
|
||||
end
|
||||
|
||||
function _G._TBASIC.isoperator(token)
|
||||
if token == nil then return false end
|
||||
|
||||
-- take care of prematurely prepended '#'
|
||||
local t1 = token:byte(1) == 35 and token:sub(2, #token) or token
|
||||
token = t1
|
||||
|
||||
for _, tocheck in ipairs(_TBASIC._OPERATR) do
|
||||
if tocheck == token:upper() then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _G._TBASIC.isvariable(word)
|
||||
if type(word) == "number" then return false end
|
||||
if type(word) == "boolean" then return true end
|
||||
if type(word) == "table" then return true end
|
||||
if word == nil then return false end
|
||||
return word:byte(1) == 36
|
||||
end
|
||||
|
||||
function _G._TBASIC.isargsep(token)
|
||||
return token == ","
|
||||
end
|
||||
|
||||
function _G._TBASIC.isfunction(token)
|
||||
if token == nil then return false end
|
||||
|
||||
-- take care of prematurely prepended '&'
|
||||
local t1 = token:byte(1) == 38 and token:sub(2, #token) or token
|
||||
token = t1
|
||||
|
||||
-- try for builtin
|
||||
local cmpval = function(table_elem) return string.hash(table_elem) end
|
||||
|
||||
local found = table.binsearch(_TBASIC._FNCTION, token, cmpval)
|
||||
|
||||
if found then
|
||||
return true
|
||||
end
|
||||
|
||||
-- try for user-defined functions
|
||||
found = table.binsearch(_TBASIC._INTPRTR.FNCTABLE, token, cmpval)
|
||||
if found then -- found is either Table or Nil. We want boolean value.
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function _G._TBASIC.isstring(token)
|
||||
if type(token) ~= "string" then return false end
|
||||
return token:byte(1) == 126
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function printdbg(...)
|
||||
local debug = false
|
||||
if debug then print("TBASINCL", ...) end
|
||||
end
|
||||
|
||||
|
||||
-- implementation of the Shunting Yard algo
|
||||
_G._TBASIC.TORPN = function(exprarray)
|
||||
local stack = {}
|
||||
local outqueue = {}
|
||||
|
||||
local loophookkeylist = {}
|
||||
local function infloophook(key)
|
||||
if not _G[key] then
|
||||
_G[key] = 0
|
||||
table.insert(loophookkeylist, key)
|
||||
end
|
||||
_G[key] = _G[key] + 1
|
||||
|
||||
if _G[key] > 50000 then
|
||||
error(key..": too long without yielding")
|
||||
end
|
||||
end
|
||||
|
||||
local isfunction = _TBASIC.isfunction
|
||||
local isoperator = _TBASIC.isoperator
|
||||
local isargsep = _TBASIC.isargsep
|
||||
local isnumber = _TBASIC.isnumber
|
||||
|
||||
for _, token in ipairs(exprarray) do--expr:gmatch("[^ ]+") do
|
||||
if token == nil then error("Token is nil!") end
|
||||
|
||||
-- hack: remove single prepended whitespace
|
||||
t1 = token:byte(1) == 32 and token:sub(2, #token) or token
|
||||
token = t1
|
||||
|
||||
printdbg("TOKEN", "'"..token.."'")
|
||||
if isfunction(token:upper()) then
|
||||
printdbg("is function")
|
||||
|
||||
stackpush(stack, "&"..token:upper())
|
||||
elseif isargsep(token) then
|
||||
printdbg("is argument separator")
|
||||
|
||||
if not (stackpeek(stack) == "(" or #stack == 0) then
|
||||
repeat
|
||||
stackpush(outqueue, stackpop(stack))
|
||||
|
||||
infloophook("repeat1")
|
||||
until stackpeek(stack) == "(" or #stack == 0
|
||||
end
|
||||
-- no left paren encountered, ERROR!
|
||||
if #stack == 0 then exprerr(token) end -- misplaces sep or mismatched parens
|
||||
elseif isoperator(token) then
|
||||
printdbg("is operator")
|
||||
|
||||
local o1 = token
|
||||
|
||||
while isoperator(stackpeek(stack)) and (
|
||||
(_op_isltr(o1) and _op_precd(o1) <= _op_precd(stackpeek(stack))) or
|
||||
(_op_isrtl(o1) and _op_precd(o1) < _op_precd(stackpeek(stack)))
|
||||
) do
|
||||
local o2 = stackpeek(stack)
|
||||
|
||||
printdbg("--> push o2 to stack, o2:", o2)
|
||||
|
||||
stackpop(stack) -- drop
|
||||
stackpush(outqueue, (o2:byte(1) == 35) and o2 or "#"..o2:upper()) -- try to rm excess '#'
|
||||
|
||||
infloophook("while")
|
||||
end
|
||||
|
||||
stackpush(stack, "#"..o1:upper())
|
||||
elseif token == "(" then
|
||||
stackpush(stack, token)
|
||||
elseif token == ")" then
|
||||
while stackpeek(stack) ~= "(" do
|
||||
if #stack == 0 then
|
||||
exprerr(token)
|
||||
end
|
||||
|
||||
printdbg("--> stack will pop", stackpeek(stack))
|
||||
|
||||
stackpush(outqueue, stackpop(stack))
|
||||
|
||||
infloophook("")
|
||||
end
|
||||
|
||||
printdbg("--> will drop", stackpeek(stack), "(should be left paren!)")
|
||||
|
||||
--[[found_left_paren = false
|
||||
if stackpeek(stack) ~= "(" then
|
||||
exprerr(token)
|
||||
else
|
||||
found_left_paren = true
|
||||
end]]
|
||||
stackpop(stack) -- drop
|
||||
|
||||
printdbg("--> stack peek after drop", stackpeek(stack))
|
||||
|
||||
if isfunction(stackpeek(stack)) then
|
||||
printdbg("--> will enq fn", stackpeek(stack))
|
||||
stackpush(outqueue, stackpop(stack))
|
||||
end
|
||||
printdbg("--> STACKTRACE_ITMD", table.concat(stack, " "))
|
||||
printdbg("--> OUTPUT_ITMD", table.concat(outqueue, " "))
|
||||
|
||||
-- stack empty without finding left paren, ERROR!
|
||||
--if not found_left_paren and #stack == 0 then exprerr(token) end -- mismatched parens
|
||||
elseif _TBASIC._INTPRTR.VARTABLE[token:upper()] ~= nil or
|
||||
_TBASIC._INTPRTR.CNSTANTS[token:upper()] ~= nil then -- if the token is variable
|
||||
printdbg("is variable")
|
||||
|
||||
stackpush(outqueue, "$"..token:upper())
|
||||
else
|
||||
printdbg("is data")
|
||||
|
||||
stackpush(outqueue, token) -- arbitrary data
|
||||
end
|
||||
printdbg("STACKTRACE", table.concat(stack, " "))
|
||||
printdbg("OUTPUT", table.concat(outqueue, " "))
|
||||
printdbg()
|
||||
end
|
||||
|
||||
while #stack > 0 do
|
||||
if stackpeek(stack) == "(" or stackpeek(stack) == ")" then
|
||||
exprerr("(paren)") -- mismatched parens
|
||||
end
|
||||
stackpush(outqueue, stackpop(stack))
|
||||
|
||||
infloophook("while3")
|
||||
end
|
||||
|
||||
printdbg("FINAL RESULT: "..table.concat(outqueue, " "))
|
||||
|
||||
for _, key in ipairs(loophookkeylist) do
|
||||
_G[key] = nil
|
||||
end
|
||||
|
||||
return outqueue
|
||||
end
|
||||
|
||||
|
||||
-- INIT -----------------------------------------------------------------------
|
||||
|
||||
_G._TBASIC._INTPRTR.RESET()
|
||||
|
||||
|
||||
--[[
|
||||
Terran BASIC (TBASIC)
|
||||
Copyright (c) 2016 Torvald (minjaesong) and the contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the Software), to deal in the
|
||||
Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]]
|
||||
Reference in New Issue
Block a user