virtualcomputer: use real BASIC

Former-commit-id: 557df532d4c9693b5b57501c0f67e7d95266f9ba
Former-commit-id: 974ea7882079fb3902f00ab821a7c8c54390d0b4
This commit is contained in:
Song Minjae
2016-11-14 12:24:03 +09:00
parent 0b20869fa4
commit 36822f95d7
4 changed files with 1623 additions and 21 deletions

View File

@@ -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()

View 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.
]]

View 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.
]]

View 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.
]]