Skip to content

Instantly share code, notes, and snippets.

@inmatarian
Created February 7, 2018 23:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inmatarian/1444cfdea410d7d044928cf9261fc9f5 to your computer and use it in GitHub Desktop.
Save inmatarian/1444cfdea410d7d044928cf9261fc9f5 to your computer and use it in GitHub Desktop.
some crap I did years ago for fun
__DEBUG_BASIC = true
local function NULLFUNC() end
local old_print, print = print, NULLFUNC
if __DEBUG_BASIC then
print = function(...) old_print(("%i"):format(debug.getinfo(2, 'l').currentline), ...) end
end
local unpack = unpack or table.unpack
local class
do
local function new(cls, ...)
local inst = setmetatable({}, cls)
inst:init(...)
return inst
end
class = function(parent)
local cls = { new=new, init=NULLFUNC }
cls.__index = cls
if parent then setmetatable(cls, parent) end
return cls
end
end
local Tokenizer = class()
do
function Tokenizer:init(line)
self.line = line
self.pos = 1
self.current = nil
self.value = nil
end
function Tokenizer:token()
return self.current
end
function Tokenizer:tokenValue()
return self.value
end
local special_tokens = {
{ 'NOTEQUAL', '<>' }, { 'LTE', '<=' }, { 'GTE', '>=' },
{ 'SEMICOLON', ';' }, { 'COMMA', ',' }, { 'PLUS', '+' }, { 'EOL', '\n', },
{ 'ASTERISK', '*' }, { 'SLASH', '/' }, { 'MINUS', '-' }, { 'EOL', '\r', },
{ 'LEFTPAREN', '(' }, { 'COLON', ':' }, { 'HASH', '#' }, { 'LT', '<' },
{ 'RIGHTPAREN', ')' }, { 'EQUALS', '=' }, { 'POWER', '^' }, { 'GT', '>', },
{ 'QUESTION', '?' }, { 'PERCENT', '%' };
}
local token_keywords = {
'LET', 'PRINT', 'IF', 'THEN', 'ELSE', 'FOR', 'TO', 'NEXT', 'STEP', 'GOTO',
'GOSUB', 'RETURN', 'CALL', 'REM', 'PEEK', 'POKE', 'END', 'LIST', 'RUN',
'AND', 'OR', 'NOT'
}
local function get_next_token(self)
do -- numbers
local st, en, cap = string.find(self.line, '^([0-9]*%.[0-9]+)', self.pos)
if st then
self.current, self.value, self.pos = 'NUMBER', tonumber(cap), en+1
return
end
st, en, cap = string.find(self.line, '^([0-9]+)', self.pos)
if st then
self.current, self.value, self.pos = 'NUMBER', tonumber(cap), en+1
return
end
end
do -- special character operators or separators
for i = 1, #special_tokens do
local st, en = string.find(self.line, special_tokens[i][2], self.pos, true)
if st == self.pos then
self.current, self.value, self.pos = special_tokens[i][1], special_tokens[i][2], en+1
return
end
end
end
do -- strings
local st, en, cap = string.find(self.line, '^"([^"]*)"', self.pos)
if st then
self.current, self.value, self.pos = 'STRING', cap, en+1
return
end
end
do -- keywords
local st, en, cap = string.find(self.line, "^([A-Za-z]+)", self.pos)
if st and ((en >= #self.line) or (string.find(self.line, '^[^0-9A-Za-z_]', en+1))) then
for i = 1, #token_keywords do
local ucap = string.upper(cap)
if ucap == token_keywords[i] then
self.current, self.value, self.pos = ucap, cap, en+1
return
end
end
end
end
do -- variables
local st, en, cap = string.find(self.line, "^([A-Za-z_]+[0-9A-Za-z_]*)", self.pos)
if st then
self.current, self.value, self.pos = 'VARIABLE', cap, en+1
return
end
end
self.current, self.value = nil, nil
end
function Tokenizer:next()
if self.pos > #self.line then
self.current, self.value = nil, nil
return
end
self.pos = string.find(self.line, '[^ \t]', self.pos)
if not self.pos then
self.pos, self.current, self.value = #self.line+1, nil, nil
return
end
get_next_token(self)
-- print("NEXT TOKEN", self.current, self.value, self.pos)
if self.current == 'REM' then
self.current, self.value = nil, nil
self.pos = #self.line + 1
end
end
end
local Listing = class()
do
-- returns first index thats gte line number
local function bsearch(t, num)
local st, en, mid, cmp = 1, #t, 0
while st ~= en do
mid = math.floor((st+en)/2)
if t[mid].num < num then st = mid + 1 else en = mid end
end
return st
end
function Listing:addLine(num, line)
assert(num == math.floor(num), "Line numbers must be integer")
local idx = 1
if #self > 0 then
if num < self[1].num then
table.insert(self, 1, {})
elseif num > self[#self].num then
idx = #self + 1
else
idx = bsearch(self, num)
if self[idx].num > num then
table.insert(self, idx, {})
end
end
end
if self[idx]==nil then self[idx] = {} end
self[idx].line=line
self[idx].num=num
end
function Listing:getLine(idx)
return self[idx].line
end
function Listing:getLineNumber(idx)
return self[idx].num
end
function Listing:getIndexByLabel(num)
assert(num == math.floor(num), "Line numbers must be integer")
if #self > 0 then
local idx = bsearch(self, num)
if self[idx].num == num then
return idx
end
end
return nil
end
end
local Interpreter = class()
do
local pr = {}
function pr.accept(self, token)
assert(token == self.tokenizer:token(), "Expecting token "..token)
local value = self.tokenizer:tokenValue()
self.tokenizer:next()
return value
end
local function boolint(a) if a then return 1 else return 0 end end
local order_of_operators = {
['POWER'] = { p=1, r=true, a=2, f=function(a, b) return a^b end },
['NEGATIVE'] = { p=2, r=false, a=1, f=function(a) return -a end },
['ASTERISK'] = { p=3, r=false, a=2, f=function(a, b) return a*b end },
['SLASH'] = { p=3, r=false, a=2, f=function(a, b) return a/b end },
['PERCENT'] = { p=3, r=false, a=2, f=function(a, b) return a%b end },
['PLUS'] = { p=4, r=false, a=2, f=function(a, b) return a+b end },
['MINUS'] = { p=4, r=false, a=2, f=function(a, b) return a-b end },
['NOT'] = { p=5, r=false, a=1, f=function(a) return boolint(a==0) end },
['EQUALS'] = { p=6, r=false, a=2, f=function(a, b) return boolint(a==b) end },
['NOTEQUAL'] = { p=6, r=false, a=2, f=function(a, b) return boolint(a~=b) end },
['LTE'] = { p=6, r=false, a=2, f=function(a, b) return boolint(a<=b) end },
['GTE'] = { p=6, r=false, a=2, f=function(a, b) return boolint(a>=b) end },
['LT'] = { p=6, r=false, a=2, f=function(a, b) return boolint(a<b) end },
['GT'] = { p=6, r=false, a=2, f=function(a, b) return boolint(a>b) end },
['AND'] = { p=7, r=false, a=2, f=function(a, b) return boolint((a~=0) and (b~=0)) end },
['OR'] = { p=8, r=false, a=2, f=function(a, b) return boolint((a~=0) or (b~=0)) end },
}
-- shunting yard order of operations, converts to post-fix
function pr.shunting_yard(self)
local list, stack, arity = {}, {}, {}
local negative, next_negative = true, true -- special case for unary-minus
for max_loop = 2048, 1, -1 do
assert(max_loop>1, "max loop in expression parser")
negative = next_negative; next_negative = false
local token = self.tokenizer:token()
if token == 'NUMBER' then
list[#list+1] = pr.accept(self, 'NUMBER')
next_negative = false
elseif token == 'VARIABLE' then
local identifier = pr.accept(self, 'VARIABLE')
local fn = self.library[identifier]
if fn then
stack[#stack+1] = fn
arity[#arity+1] = 1
pr.accept(self, 'LEFTPAREN') -- MUST follow function with call.
stack[#stack+1] = 'LEFTPAREN'
next_negative = true
else
local val = self.variables[identifier]
assert(type(val)=='number', "non-numeric value in expression")
list[#list+1] = val
next_negative = false
end
elseif token == 'COMMA' then
pr.accept(self, 'COMMA')
while #stack > 0 and stack[#stack] ~= 'LEFTPAREN' do
list[#list+1] = table.remove(stack)
end
assert(#stack>0 and stack[#stack]=='LEFTPAREN', "unexpected comma in expression")
assert(#stack>1 and type(stack[#stack-1])=='function', "unexpected comma, not a function call")
assert(#arity>1, "arity count was lost")
arity[#arity] = arity[#arity]+1
next_negative = true
elseif token == 'LEFTPAREN' then
pr.accept(self, 'LEFTPAREN')
stack[#stack+1] = 'LEFTPAREN'
next_negative = true
elseif token == 'RIGHTPAREN' then
pr.accept(self, 'RIGHTPAREN')
while #stack > 0 and stack[#stack] ~= 'LEFTPAREN' do
list[#list+1] = table.remove(stack)
end
assert(#stack > 0, "missing left parenthesis")
table.remove(stack) -- remove lparen
if (#stack > 0) and (type(stack[#stack]) == 'function') then
list[#list+1] = table.remove(arity)
list[#list+1] = table.remove(stack)
end
next_negative = false
elseif order_of_operators[token] then
pr.accept(self, token)
if token == 'MINUS' and negative then token = 'NEGATIVE' end
local order = order_of_operators[token]
local p1 = order.p
while (#stack>0) and (order_of_operators[stack[#stack]]) do
local p2 = order_of_operators[stack[#stack]].p
if (p1 > p2) or ((not order.r) and p1 == p2) then
list[#list+1] = table.remove(stack)
else
break
end
end
stack[#stack+1]=token
next_negative = true
else
break-- end of expression at first unrecognized token
end
end
while #stack > 0 do list[#list+1] = table.remove(stack) end
return list, stack
end
function pr.expression(self)
local list, stack = pr.shunting_yard(self)
for i = 1, #list do
if type(list[i])=='number' then
stack[#stack+1]=list[i]
elseif order_of_operators[list[i]] then
local op = order_of_operators[list[i]]
local a, b
if op.a == 2 then
b = table.remove(stack)
end
a = table.remove(stack)
stack[#stack+1]=op.f(a, b)
elseif type(list[i])=='function' then
local arity = table.remove(stack)
local params = {}
for i = 1, arity do
table.insert(params, 1, table.remove(stack))
end
stack[#stack+1]=list[i](unpack(params))
end
end
assert(#stack==1, "imbalanced evaluation stack in expression")
return stack[1]
end
pr.library = {}
pr.library.SIN = math.sin
function pr.copy_library(self)
for k, v in pairs(pr.library) do
self.library[k] = v
end
end
pr.statement={}
function pr.statement.LIST(self)
assert(self.interactive, "LIST statement disabled in non-interactive mode")
local start = 0
if self.tokenizer:token() == 'NUMBER' then
start = pr.accept(self, "NUMBER")
end
for i = 1, #self.listing do
if self.listing[i].num >= start then
self.write(self.listing[i].line, '\n')
end
end
end
function pr.statement.LET(self)
local identifier = pr.accept(self, 'VARIABLE')
assert(self.library[identifier]==nil, "Function already exists")
pr.accept(self, 'EQUALS')
if self.tokenizer:token() == 'STRING' then
self.variables[identifier] = pr.accept(self, 'STRING')
else
self.variables[identifier] = pr.expression(self)
end
end
function pr.statement.PRINT(self)
local output = {}
local token = self.tokenizer:token()
local max_loop = 4096
while true do
max_loop = max_loop - 1
assert(max_loop > 1, "max loop in print statement")
if token == 'STRING' then
output[#output+1] = pr.accept(self, 'STRING')
else
output[#output+1] = pr.expression(self)
end
token = self.tokenizer:token()
while token == 'COMMA' or token == 'SEMICOLON' do
if token == 'COMMA' then
pr.accept(self, 'COMMA')
output[#output+1] = ' '
else
pr.accept(self, 'SEMICOLON')
end
token = self.tokenizer:token()
end
if (token == 'COLON') or (token==nil) then
break
end
end
output[#output+1]='\n'
-- self.write(self:currentLineNumber(), ': ')
self.write(unpack(output))
end
function pr.statement.END(self)
assert(self.running, "Not executing")
self.running = false
return 'DONE'
end
function pr.statement.GOTO(self)
assert(self.running, "Not executing")
self.lineIdx = self.listing:getIndexByLabel(pr.accept(self, 'NUMBER'))
return 'DONE'
end
function pr.statement.GOSUB(self)
assert(self.running, "Not executing")
assert(#self.callStack < 65536, "Stack Overflow")
self.callStack[#self.callStack+1] = { type='RETURN', idx=self.lineIdx }
self.lineIdx = self.listing:getIndexByLabel(pr.accept(self, 'NUMBER'))
return 'DONE'
end
function pr.statement.RETURN(self)
assert(self.running, "Not executing")
assert((#self.callStack>0) and (self.callStack[#self.callStack].type=='RETURN'),
"Call stack top must be return address")
self.lineIdx = (table.remove(self.callStack)).idx
return 'DONE'
end
function pr.statement.FOR(self)
local idx, col = self.lineIdx, 1
local var = pr.accept(self, 'VARIABLE')
pr.accept(self, 'EQUALS')
local start = pr.expression(self)
pr.accept(self, 'TO')
local stop = pr.expression(self)
local step = 1
if self.tokenizer:token()=='STEP' then
pr.accept(self, 'STEP')
step = pr.expression(self)
end
if self.tokenizer:token()=='COLON' then
idx, col = self.currLineIdx, self.tokenizer.pos
end
assert(#self.callStack < 65536, "Stack Overflow")
self.callStack[#self.callStack+1] = {
type='FORNEXT', var=var, idx=idx, col=col, stop=stop, step=step
}
self.variables[var]=start
end
function pr.statement.NEXT(self)
assert(#self.callStack>0, "Call stack top must be for loop")
local peek = self.callStack[#self.callStack]
assert(peek.type=='FORNEXT', "Call stack top must be for loop")
if self.tokenizer:token()=='VARIABLE' then
local var = pr.accept(self, 'VARIABLE')
while true do
if peek.var == var then break end
table.remove(self.callStack)
assert(#self.callStack>0, "Call stack top must be for loop")
peek = self.callStack[#self.callStack]
assert(peek.type=='FORNEXT', "Call stack top must be for loop")
end
end
local v = self.variables[peek.var]
v = v + peek.step
self.variables[peek.var]=v
if ((peek.step > 0) and (v <= peek.stop)) or ((peek.step <= 0) and (v >= peek.stop)) then
self.lineIdx, self.charIdx = peek.idx, peek.col
else
table.remove(self.callStack)
end
end
function pr.statement.IF(self)
if pr.expression(self) ~= 0 then
local token = self.tokenizer:token()
if token == 'GOTO' then
pr.accept(self, 'GOTO')
return pr.statement.GOTO(self)
else
pr.accept(self, 'THEN')
if self.tokenizer:token() == 'NUMBER' then
return pr.statement.GOTO(self)
else
return 'THEN'
end
end
end
return 'DONE'
end
function pr.execute(self)
local token = self.tokenizer:token()
while token ~= nil do
local mode
if token == 'VARIABLE' then
mode = pr.statement.LET(self)
elseif token == 'QUESTION' then
pr.accept(self, token)
mode = pr.statement.PRINT(self)
elseif pr.statement[token] then
pr.accept(self, token)
mode = pr.statement[token](self)
else
error("Unknown statement "..token)
end
token = self.tokenizer:token()
if mode ~= 'DONE' and token ~= nil then
if mode ~= 'THEN' then
pr.accept(self, 'COLON')
end
token = self.tokenizer:token()
else
break
end
end
end
function Interpreter:init()
self.interactive = true
self.listing = Listing:new()
self.variables = {}
self.library = {}
self.lineIdx = 0
self.currLineIdx = 0
self.charIdx = 0
self.running = false
pr.copy_library(self)
end
function Interpreter:setInteractiveMode(toggle)
self.interactive = toggle
end
function Interpreter:setInputStream(fn)
self.read=fn
end
function Interpreter:setOutputStream(fn)
self.write=fn
end
function Interpreter:addFunction(name, fn)
self.library[name]=fn
end
function Interpreter:eval(line)
self.tokenizer = Tokenizer:new(line)
self.tokenizer:next()
local token = self.tokenizer:token()
if token == "NUMBER" then
self.listing:addLine(self.tokenizer:tokenValue(), line)
elseif token == "RUN" then
assert(self.interactive, "RUN disabled in non-interactive mode")
self:run()
elseif token ~= nil then
assert(self.interactive, "Numbered lines required in non-interactive mode")
pr.execute(self, line)
end
end
function Interpreter:start()
self.lineIdx = 1
self.currLineIdx = 1
self.charIdx = 1
self.running = true
self.callStack = {}
end
function Interpreter:step()
if not self.running then error("Execution has stopped") end
self.currLineIdx = self.lineIdx
local line = self.listing:getLine(self.lineIdx)
self.tokenizer = Tokenizer:new(string.sub(line, self.charIdx))
self.tokenizer:next()
if self.tokenizer:token() == 'NUMBER' then -- skip line number
pr.accept(self, 'NUMBER')
end
self.lineIdx, self.charIdx = self.lineIdx + 1, 1 -- default next line
pr.execute(self)
if self.lineIdx > #self.listing then
self.running = false
end
end
function Interpreter:currentLineNumber()
return self.listing:getLineNumber(self.currLineIdx)
end
function Interpreter:run()
self:start()
while self.running do
self:step()
end
end
end
------------------------------------------------------------------------------
function repl()
print("Interactive Mode\nSo many bytes free\n")
local program = Interpreter:new()
program:setInputStream(function(...) io.read(...) end)
program:setOutputStream(function(...) io.write(...) end)
local input
local function safe_run() return program:eval(input) end
local function err_hand(msg)
io.stderr:write(msg, '\n',
('Interpreter Line: %s\n'):format(tostring(program:currentLineNumber())),
debug.traceback(), '\n')
end
while true do
io.write('> ')
input = io.read('*l')
if input == null then break end
local good, output = xpcall(safe_run, err_hand)
if good == true then
if output then
print(output)
end
end
end
end
------------------------------------------------------------------------------
function runfile(filename, args)
local program = Interpreter:new()
program:setInputStream(function(...) io.read(...) end)
program:setOutputStream(function(...) io.write(...) end)
program:setInteractiveMode(false)
local input
local function safe_run() return program:eval(input) end
local function err_hand(msg)
io.stderr:write(msg, '\n',
('Interpreter Line: %s\n'):format(tostring(program:currentLineNumber())),
debug.traceback(), '\n')
end
local FILE, msg = io.open(filename, 'r')
if FILE == nil then
io.stderr:write(msg)
return 1
end
while true do
input = FILE:read('*l')
if input == nil then break end
local good, output = xpcall(safe_run, err_hand)
if good ~= true then
FILE:close()
return 1
end
end
local exit = xpcall(function() return program:run() end, err_hand)
if exit == false then return 1 end
return 0
end
------------------------------------------------------------------------------
function main(...)
local N = select('#', ...)
local filename
local argslist = {}
if N > 0 then
for i = 1, N do
local arg = select(i, ...)
if string.sub(arg, 1, 1) == '-' then
local k, v = string.match(arg, '%-%-?([^-=]+)=?(.*)')
if k then
if v == nil then v = true end
argslist[k] = v
end
elseif filename == nil then
filename = arg
end
end
end
if filename == nil then
return repl(argslist)
else
local exit = runfile(filename, argslist)
io.read()
return exit
end
end
return main(...)
10 PRINT "HELLO" : ? "WHATS UP?"
20 X=0
30 GOSUB 100
40 PRINT "GOOD BYE"
50 END
100 REM LOOP
110 X=X+1
120 IF X%2=0 THEN PRINT X
130 IF X<10 THEN 100
140 FOR J = 2 TO 6 STEP 2
150 ? "FOR J =", J
160 FOR I = 5 TO 1 STEP -2: ? "FOR I =", I : NEXT I
170 NEXT
180 RETURN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment