Created
February 7, 2018 23:50
-
-
Save inmatarian/1444cfdea410d7d044928cf9261fc9f5 to your computer and use it in GitHub Desktop.
some crap I did years ago for fun
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__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(...) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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