Skip to content

Instantly share code, notes, and snippets.

@jacobstern
Created February 8, 2020 04:34
Show Gist options
  • Save jacobstern/13ebb6794623f38a56c5270580976a62 to your computer and use it in GitHub Desktop.
Save jacobstern/13ebb6794623f38a56c5270580976a62 to your computer and use it in GitHub Desktop.
(function () -- ensure we run in a clean scope
-- Prepare globals
local my_ENV, globfuncs = {}, {}
for k,v in pairs(_ENV) do
my_ENV[k] = v
if (type(v) == "function") globfuncs[k] = true
end
local g_ENV = _ENV
local _ENV = my_ENV -- with this, we segregate ourselves from the running code (all global accesses below use _ENV automagically)
-- get an assert whenever globals are accidentally added
-- KEEP COMMENTED OUT due to ord/chr weak globals
--[[setmetatable(_ENV, {
__index=function(o,k) assert(false,'global get: '..k) end,
__newindex=function(o,k,v) assert(false,'global set: '..k) end
})]]
-- Utils
local function isoneof(ch, str)
for i=1,#str do
if (sub(str,i,i) == ch) return true
end
return false
end
local function isin(obj, tab)
for i=1,#tab do
if (tab[i] == obj) return true
end
return false
end
local function rest(a, ...)
return ...
end
local function _pack_n(...)
local empty = type(btn(...)) == "number"
return empty and 0 or 1 + _pack_n(rest(...))
end
local function pack(...)
local t = {...}
t.n = _pack_n(...)
return t
end
local function unpack(t, i)
i = i or 1
if (i > t.n) return
return t[i], unpack(t, i+1)
end
local function is_pack_empty(t)
return t.n == 0
end
local function pack_self_test()
return (coresume(cocreate(function() assert(_pack_n({},{},'','z',-1,0.5,1.5,0,9999,false,true,nil) == 12) end)))
end
if not pack_self_test() then
printh("falling back to naive packing...")
pack = function(...) -- naive (no trailing nil support)
local t = {...}
local n = 1 -- prefer nil over empty
for k, v in pairs(t) do n = max(n, k) end -- more reliable than #
t.n = n
return t
end
is_pack_empty = function(t)
return t.n <= 1 and t[1] == nil
end
end
local g_error = nil
local function fail(err)
g_error = err
assert(false, "FAIL")
end
local function copy(t)
local ct = {}
for k, v in pairs(t) do ct[k] = v end
return ct
end
local function popkeys()
while (stat(30)) do stat(31) end
end
-- Input : Tokenize
local escapes = {a="\a",b="\b",f="\f",n="\n",r="\r",t="\t",v="\v",["\\"]="\\",['"']='"',["'"]="''"}
local function isalnum(ch)
return ch >= '0' and ch <= '9' or ch >= 'A' and ch <= 'Z' or ch >= 'a' and ch <= 'z' or ch == '_' or ch >= '\x80'
end
local function dequote(str, i, quote)
local rawstr = ''
while i <= #str and sub(str,i,i) != quote do
local ch = sub(str,i,i)
if ch == '\\' then
i += 1
local esch = sub(str,i,i)
ch = escapes[esch] -- normal escapes
if esch == 'x' then -- hex escape
i += 2
esch = tonum('0x'..sub(str,i-1,i))
if (not esch) fail("bad hex escape") esch=0
if chr then ch = chr(esch) else fail("hex escape requires 1.12d+") ch='' end
elseif esch >= '0' and esch <= '9' then -- dec escape
local start = i
while esch >= '0' and esch <= '9' and i < start + 3 do i += 1; esch = sub(str,i,i) end
i -= 1
esch = tonum(sub(str,start,i))
if (not esch or esch >= 256) fail("bad decimal escape") esch=0
if chr then ch = chr(esch) else fail("decimal escape requires 1.12d+") ch='' end
elseif esch == '\n' then -- newline
ch = '\n'
elseif esch == 'z' then -- ignore wspace
repeat i += 1; esch = sub(str,i,i) until not isoneof(esch, ' \r\t\f\v\n')
if (esch == '') fail(false)
ch = ''
i -= 1
elseif esch == '' then fail(false) end
if (not ch) fail("bad escape: " .. esch) ch=''
end
rawstr = rawstr .. ch
i += 1
end
if (i > #str) fail("unterminated string", true)
return rawstr, i+1
end
local function delongbracket(str, i)
if sub(str,i,i) == '[' then i += 1
local eq_start = i
while sub(str,i,i) == '=' do i += 1 end
local end_delim = ']' .. sub(str,eq_start,i-1) .. ']'
local j = #end_delim
if sub(str,i,i) == '[' then i += 1
if (sub(str,i,i) == '\n') i += 1
local start = i
while i <= #str and sub(str,i,i+j-1) != end_delim do i += 1 end
if (i >= #str) fail(false)
return sub(str,start,i-1), i+j
end
end
fail("invalid long brackets")
return str, i
end
local function tokenize(str, lenient)
local i = 1
local line = 1
local tokens = {}
local tlines = {}
local tstarts = {}
local tends = {}
local _fail, err = fail
if (lenient) fail = function(v,ok) err = v and not ok end
while i <= #str do
local start = i
local ch = sub(str,i,i)
local ws, token
if isoneof(ch, ' \r\t\f\v') then i += 1; ws = true
elseif ch == '\n' then i += 1; line += 1; ws = true
elseif ch == '-' and sub(str,i+1,i+1) == '-' then -- comment
i += 2
if sub(str,i,i) == '[' then
token, i = delongbracket(str, i)
else
while i <= #str and sub(str,i,i) != '\n' do i += 1 end
end
if (lenient) add(tokens, true) else ws = true
elseif (ch >= '0' and ch <= '9') or -- number
(ch == '.' and isoneof(sub(str,i+1,i+1), "0123456789")) then
while isoneof(sub(str,i,i), "0123456789aAbBcCdDeEfF") or sub(str,i,i) == '.' or
isoneof(sub(str,i,i), 'xX') and sub(str,i-1,i-1) == '0' and i-1 == start or
isoneof(sub(str,i,i), '-+') and isoneof(sub(str,i-1,i-1), 'eE') do i += 1 end
token = sub(str,start,i-1)
while not tonum(token) do
if isoneof(sub(str,i-1,i-1), "aAbBcCdDeEfF") then i -= 1; token = sub(str,start,i-1)
else fail("bad number: " .. token); token='0' end
end
add(tokens, tonum(token))
elseif isalnum(ch) then -- ident
while isalnum(sub(str,i,i)) do i += 1 end
add(tokens, sub(str,start,i-1))
elseif ch == "'" or ch == '"' then -- string
token, i = dequote(str, i+1, ch)
add(tokens, {str=token})
elseif ch == '[' and isoneof(sub(str,i+1,i+1), "=[") then -- string (long form)
token, i = delongbracket(str, i)
add(tokens, {str=token})
else
i += 1
local ch2,ch3 = sub(str,i,i),sub(str,i+1,i+1)
local eqopt = isoneof(ch,'+-*/%<>=')
local repopt = isoneof(ch,'.:')
if (eqopt or isoneof(ch,'~!')) and ch2 == '=' then i += 1
elseif ch == '.' and ch2 == '.' and ch3 == '.' then i += 2
elseif repopt and ch2 == ch then i += 1
elseif eqopt or repopt or isoneof(ch,'^#(){}[];,?@') then
else fail("bad char: " .. ch) end
add(tokens, sub(str,start,i-1))
end
if (not ws) add(tlines, line); add(tstarts, start); add(tends, i-1)
if (err) tokens[#tokens] = false; err = false;
end
fail = _fail
return tokens, tlines, tstarts, tends
end
-- Input : Parse & Eval
-- a parse returns a (node, setnode, tailcallnode) tuple.
-- each node is a function with e as the first arg.
local keywords = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while"}
local function is_op_assign(token)
return type(token) == "string" and sub(token,2,2) == '='
end
local function eval_nodes(e, nodes)
local results = {}
local n = #nodes
for i=1,n-1 do
results[i] = nodes[i](e)
end
if n > 0 then
local values = pack(nodes[n](e))
for i=1,values.n do
results[n + i - 1] = values[i]
end
n += values.n - 1
end
results.n = n
return results
end
local function handle_break(retval)
if (retval == true) return -- break
return retval
end
local g_results = nil
local g_last_value = nil
local cmd_exec
local function parse(tokens, tlines)
local i = 1
local parse_expr, parse_block
local locals = {}
local depth = 0
local function const_node(value)
return function(e) assert(e, "repl failure"); return value end
end
local function var_node(name)
local i = locals[name]
if i then return function(e) return e[i][name] end
else return function(e) return g_ENV[name] end
end
end
local function vararg_node()
local i = locals['...']
if (not i) fail("unexpected '...'")
return function(e) return unpack(e[i]["..."]) end
end
local function assign_node(name)
local i = locals[name]
if i then return function(e, v) e[i][name] = v end
else return function(e, v) g_ENV[name] = v end
end
end
local function require(expect, retval)
local token = tokens[i]; i += 1
if (token == expect) return retval
if (token == nil) fail(false)
fail("expected: " .. expect)
end
local function require_ident(token)
if (not token) token = tokens[i]; i += 1
if (token == nil) fail(false)
if (type(token) == 'string' and isalnum(sub(token,1,1)) and not isin(token, keywords)) return token
if (type(token) == 'string') fail("invalid identifier: " .. token)
fail("identifier expected")
end
local function parse_list(parser)
local list = {}
add(list, parser())
while tokens[i] == ',' do i += 1
add(list, parser())
end
return list
end
local function parse_call(node, method, arg)
local args = {}
if arg then
add(args, arg)
else
while tokens[i] != ')' do
local token = tokens[i]
if (token == nil) fail(false)
add(args, parse_expr())
if tokens[i] == ')' then break end
require(',')
end
require(')')
end
if arg then
return function(e) return node(e)(arg(e)) end, nil,
function(e) return node(e), pack(arg(e)) end
elseif method then
return function(e)
local obj = node(e)
return obj[method](obj, unpack(eval_nodes(e, args)))
end, nil, function(e)
local obj = node(e)
return obj[method], pack(obj, unpack(eval_nodes(e, args)))
end
else
return function(e)
return node(e)(unpack(eval_nodes(e, args)))
end, nil, function(e)
return node(e), eval_nodes(e, args)
end
end
end
local function parse_table()
local keys, values = {}, {}
local splat_i = 0
local index = 1
while tokens[i] != '}' do
local token = tokens[i]; i += 1
if (token == nil) fail(false)
local key, value
if token == '[' then key = parse_expr(); require(']') require('='); value = parse_expr()
elseif tokens[i] == '=' then key = const_node(require_ident(token)); require('='); value = parse_expr()
else i -= 1; key = const_node(index); value = parse_expr(); index += 1; splat_i = #keys + 1
end
add(keys, key); add(values, value)
if (tokens[i] == '}') break
if (tokens[i] == ';') i += 1 else require(',')
end
require('}')
return function(e)
local table = {}
for i=1,#keys do
if i == splat_i then
local key, values = keys[i](e), pack(values[i](e))
for i=1,values.n do
table[key + i - 1] = values[i]
end
else
table[keys[i](e)] = values[i](e)
end
end
return table
end
end
local function parse_function(is_stmt, is_local)
local name, has_self, setnode
if (is_stmt) then
if is_local then
name = require_ident()
locals[name] = depth
setnode = assign_node(name)
else
name = {require_ident()}
while tokens[i] == '.' do i += 1; add(name, require_ident()) end
if (tokens[i] == ':') i += 1; add(name, require_ident()); has_self = true
if #name == 1 then setnode = assign_node(name[1])
else
local node = var_node(name[1])
for i=2,#name-1 do
local node_i = node -- capture
node = function(e) return node_i(e)[name[i]] end
end
setnode = function(e,v) node(e)[name[#name]] = v end
end
end
end
local params = {}
local vararg = false
if (has_self) add(params, 'self')
require("(")
while tokens[i] != ')' do
if (tokens[i] == '...') i += 1; vararg = true; else add(params, require_ident())
if tokens[i] == ')' then break end
require(',')
end
require(")")
local func_locals = {}
for param in all(params) do func_locals[param] = depth + 1 end
if (vararg) func_locals['...'] = depth + 1
local body = require('end', parse_block(func_locals))
return function(e)
local func_e = copy(e)
local func_e_len = #func_e
local func = function(...)
local args = pack(...)
local func_e = func_e
if #func_e != func_e_len then -- recursion
local new_e = {}
for i=1,func_e_len do new_e[i] = func_e[i] end
func_e = new_e
end
local scope = {}
for i=1,#params do scope[params[i]] = args[i] end
if vararg then
local varargs = {}
for i=#params,args.n do
varargs[i - #params] = args[i]
end
varargs.n = max(args.n - #params, 0)
scope['...'] = varargs
end
local retval = body(func_e, scope)
if (not retval) return
if (type(retval) == "table") return unpack(retval) -- return
if (type(retval) == "function") return retval() -- tailcall
if (retval == true) fail("break outside of loop")
fail("goto out of function")
end
if (is_stmt) setnode(e, func) else return func
end
end
local function parse_core()
local token = tokens[i]; i += 1
local arg
if (token == nil) fail(false)
if (token == "nil") return const_node(nil)
if (token == "true") return const_node(true)
if (token == "false") return const_node(false)
if (type(token) == "number") return const_node(token)
if (type(token) == "table") return const_node(token.str)
if (token == "{") return parse_table()
if token == "(" then arg = require(')', parse_expr()); return function(e) return (arg(e)) end end
if (token == "-") arg = parse_expr(9); return function(e) return -arg(e) end
if (token == "not") arg = parse_expr(9); return function(e) return not arg(e) end
if (token == "#") arg = parse_expr(9); return function(e) return #arg(e) end
if (token == 'function') return parse_function()
if (token == "...") return vararg_node()
if (token == "@") arg = require_ident() return function(e) return cmd_exec(arg) end, function(e,v) cmd_exec(arg,true,v) end
if (require_ident(token)) return var_node(token), assign_node(token)
fail("unexpected token: " .. token)
end
local function parse_expr_more(prec, left)
local token = tokens[i]; i += 1
local right
if token == '.' then right = require_ident(); return function(e) return left(e)[right] end, function(e,v) left(e)[right] = v end
elseif token == '[' then right = require(']', parse_expr()); return function(e) return left(e)[right(e)] end, function(e,v) left(e)[right(e)] = v end
elseif token == "(" then return parse_call(left)
elseif token == ":" then right = require('(', require_ident()); return parse_call(left, right)
elseif token == "{" or type(token) == "table" then i -= 1; right = parse_core(); return parse_call(left, nil, right)
elseif token == "^" and prec <= 10 then right = parse_expr(10); return function(e) return left(e) ^ right(e) end
elseif token == "*" and prec < 8 then right = parse_expr(8); return function(e) return left(e) * right(e) end
elseif token == "/" and prec < 8 then right = parse_expr(8); return function(e) return left(e) / right(e) end
elseif token == "%" and prec < 8 then right = parse_expr(8); return function(e) return left(e) % right(e) end
elseif token == "+" and prec < 7 then right = parse_expr(7); return function(e) return left(e) + right(e) end
elseif token == "-" and prec < 7 then right = parse_expr(7); return function(e) return left(e) - right(e) end
elseif token == ".." and prec <= 6 then right = parse_expr(6); return function(e) return left(e) .. right(e) end
elseif token == "<" and prec < 5 then right = parse_expr(5); return function(e) return left(e) < right(e) end
elseif token == ">" and prec < 5 then right = parse_expr(5); return function(e) return left(e) > right(e) end
elseif token == "<=" and prec < 5 then right = parse_expr(5); return function(e) return left(e) <= right(e) end
elseif token == ">=" and prec < 5 then right = parse_expr(5); return function(e) return left(e) >= right(e) end
elseif token == "==" and prec < 5 then right = parse_expr(5); return function(e) return left(e) == right(e) end
elseif (token == "~=" or token == "!=") and prec < 5 then right = parse_expr(5); return function(e) return left(e) ~= right(e) end
elseif token == "and" and prec < 4 then right = parse_expr(4); return function(e) return left(e) and right(e) end
elseif token == "or" and prec < 3 then right = parse_expr(3); return function(e) return left(e) or right(e) end
else i -= 1; return end
end
parse_expr = function(prec)
if (not prec) prec = 0
local node, setnode, callnode = parse_core()
while true do
local newnode, newsetnode, newcallnode = parse_expr_more(prec, node)
if (not newnode) break
node, setnode, callnode = newnode, newsetnode, newcallnode
end
return node, setnode, callnode
end
local function parse_assign_expr()
local _, assign_expr = parse_expr()
if (not assign_expr) fail("cannot assign to value")
return assign_expr
end
local function parse_assign()
local targets = require("=", parse_list(parse_assign_expr))
local sources = parse_list(parse_expr)
if #targets == 1 and #sources == 1 then return function(e)
targets[1](e, sources[1](e))
end else return function(e)
local values = eval_nodes(e, sources)
for i=1,#targets do targets[i](e, values[i]) end
end end
end
local function parse_op_assign(node, setnode)
local op = sub(tokens[i],1,1); i += 1
local source = parse_expr()
if (op == "+") return function(e) return setnode(e, node(e) + source(e)) end
if (op == "-") return function(e) return setnode(e, node(e) - source(e)) end
if (op == "*") return function(e) return setnode(e, node(e) * source(e)) end
if (op == "/") return function(e) return setnode(e, node(e) / source(e)) end
if (op == "%") return function(e) return setnode(e, node(e) % source(e)) end
fail("invalid compound assignment: " .. op)
end
local function parse_local()
if tokens[i] == 'function' then
i += 1
return parse_function(true, true)
else
local targets = parse_list(require_ident)
local sources
if (tokens[i] == '=') i += 1; sources = parse_list(parse_expr) else sources = {}
for i=1,#targets do locals[targets[i]] = depth end
local d = depth -- capture
if #targets == 1 and #sources == 1 then return function(e)
e[d][targets[1]] = sources[1](e)
end else return function(e)
local values = eval_nodes(e, sources)
for i=1,#targets do e[d][targets[i]] = values[i] end
end end
end
end
local function parse_ifstmt()
local cond = parse_expr()
local then_b, else_b
if tokens[i] != 'then' then
local line = tlines[i]
local endcb = function() return line != tlines[i] end
then_b = parse_block(nil, {'else','end'}, endcb)
if (tokens[i] == 'else' and not endcb()) i += 1; else_b = parse_block(nil, {'end'}, endcb)
else
local need_end = true
require('then')
then_b, else_b = parse_block(nil, {'else','elseif','end'})
if tokens[i] == 'else' then i += 1; else_b = parse_block()
elseif tokens[i] == 'elseif' then i += 1; else_b = parse_ifstmt(); need_end = false
end
if (need_end) require('end')
end
return function(e)
if cond(e) then return then_b(e)
elseif else_b then return else_b(e)
end
end
end
local function parse_while()
local cond = require('do', parse_expr())
local body = require('end', parse_block())
return function(e)
while cond(e) do
local retval = body(e)
if (retval) return handle_break(retval)
end
end
end
local function parse_repeat()
local body = require('until', parse_block(nil, {'until'}))
local cond = parse_expr()
return function(e)
repeat
local retval = body(e)
if (retval) return handle_break(retval)
until cond(e)
end
end
local function parse_for()
if tokens[i + 1] == '=' then
local varb = require('=', require_ident())
local min = require(',', parse_expr())
local max = parse_expr()
local step
if (tokens[i] == ',') i += 1; step = parse_expr() else step = const_node(1)
require('do')
local for_locals = {[varb]=depth + 1}
local body = require('end', parse_block(for_locals))
return function(e)
for i=min(e),max(e),step(e) do
local scope = {}
scope[varb] = i
local retval = body(e, scope)
if (retval) return handle_break(retval)
end
end
else
local targets = require("in", parse_list(require_ident))
local sources = require('do', parse_list(parse_expr))
local for_locals = {}
for target in all(targets) do for_locals[target] = depth + 1 end
local body = require('end', parse_block(for_locals))
return function(e)
local exps = eval_nodes(e, sources)
while true do
local scope = {}
local vars = {exps[1](exps[2], exps[3])}
if (vars[1] == nil) break
exps[3] = vars[1]
for i=1,#targets do scope[targets[i]] = vars[i] end
local retval = body(e, scope)
if (retval) return handle_break(retval)
end
end
end
end
local function parse_return(endcb)
if isin(tokens[i], {';','end','else','elseif','until'}) or (endcb and endcb()) then
return function(e) return {n=0} end
else
local node, _, callnode = parse_expr()
local nodes = {node}
while tokens[i] == ',' do i += 1
add(nodes, parse_expr())
end
if #nodes == 1 and callnode then
return function(e) local func, args = callnode(e);
return function() return func(unpack(args)) end -- tailcall
end
else
return function(e) return eval_nodes(e, nodes) end
end
end
end
local function parse_label(stmt_i)
local label = "::" .. require('::', require_ident()) .. "::"
locals[label] = bor(depth, lshr(stmt_i,16))
return nil
end
local function parse_goto()
local label = "::" .. require_ident() .. "::"
local locals_c = locals -- capture
return function(e)
local target = locals_c[label]
if (not target) fail("invalid label: " .. label)
return target
end
end
local function parse_stmt(stmt_i, endcb)
local token = tokens[i]; i += 1
if (token == ';') return nil
if (token == 'do') return require('end', parse_block())
if (token == 'if') return parse_ifstmt()
if (token == 'while') return parse_while()
if (token == 'repeat') return parse_repeat()
if (token == 'for') return parse_for()
if (token == 'break') return function(e) return true end
if (token == 'return') return parse_return(endcb)
if (token == 'local') return parse_local()
if (token == 'goto') return parse_goto()
if (token == '::') return parse_label(stmt_i)
if token == 'function' and tokens[i] != '(' then return parse_function(true) end
if token == '?' then local nodes = parse_list(parse_expr);
return function (e) g_ENV.print(unpack(eval_nodes(e, nodes))) end end
-- assign/exprs
i -= 1
local start = i
local node, setnode, callnode = parse_expr()
if tokens[i] == ',' or tokens[i] == '=' then
i = start; return parse_assign()
elseif is_op_assign(tokens[i]) then
return parse_op_assign(node, setnode)
elseif depth <= 1 then
return function (e)
local results = pack(node(e))
if (not (callnode and is_pack_empty(results))) add(g_results, results)
g_last_value = results[1]
end
else
return function(e) node(e) end
end
end
parse_block = function(initial_locals, ends, endcb)
if (not ends) ends = {'end'}
local prev_locals = locals
locals = initial_locals or {}
depth += 1
local block_depth = depth
if (prev_locals) setmetatable(locals, {__index=prev_locals})
local block = {}
while i <= #tokens and not isin(tokens[i], ends) do
local stmt = parse_stmt(#block, endcb)
if (stmt) add(block, stmt)
if (endcb and endcb()) break
end
locals = prev_locals
depth -= 1
return function (e, scope)
local retval
add(e, scope or {})
local i,n = 1,#block
while i <= n do
retval = block[i](e)
if retval then
if (type(retval) != "number") break -- return/break
if (band(retval,0xffff) != block_depth) break
i = shl(retval,16) -- goto
retval = nil
end
i += 1
end
e[#e] = nil
return retval
end
end
return parse_block({_=0, _env=0}, {})
end
-- Output
local g_show_max_items = 10
local function requote(str)
local i = 1
while i <= #str do
local ch, nch = sub(str,i,i)
if ch == '\\' or ch == '"' then nch = ch
elseif ch == '\n' then nch = 'n'
elseif ch == '\t' then nch = 't'
elseif ch == '\x7f' then nch = 'x7f'
elseif ch < '\x10' and ord then nch = 'x' .. sub(tostr(ord(ch), 1),5,6) end
if (nch) str = sub(str,1,i-1) .. '\\' .. nch .. sub(str,i+1); i += #nch
i += 1
end
return '"' .. str .. '"'
end
local function is_identifier(key)
if (type(key) != 'string') return false
if (isin(key, keywords)) return false
if (#key == 0 or sub(key,1,1) >= '0' and sub(key,1,1) <= '9') return false
for i=1,#key do
if (not isalnum(sub(key,i,i))) return false
end
return true
end
local function value_to_str(val, depth)
local ty = type(val)
if (ty == 'nil') then
return 'nil'
elseif (ty == 'boolean') then
return val and 'true' or 'false'
elseif (ty == 'number') then
return tostr(val)
elseif (ty == 'string') then
return requote(val)
elseif (ty == 'table' and not depth) then
local res = '{'
local i = 0
local prev = 0
for k,v in next, val do -- pairs uses metamethods
if (i == g_show_max_items) res = res .. ',<...>' break
if (i > 0) res = res .. ','
local vstr = value_to_str(v,1)
if k == prev + 1 then res = res .. vstr; prev = k
elseif is_identifier(k) then res = res .. k .. '=' .. vstr
else res = res .. '[' .. value_to_str(k,1) ..']=' .. vstr end
i += 1
end
return res .. '}'
elseif (type(ty) == 'string') then
return '<' .. ty .. '>'
else
return '<<' .. tostr(ty) .. '>>' -- for when things are badly borken
end
end
local function results_to_str(str, results)
if (results == nil) return str, nil -- no new results
if (not str) str = ''
local count = min(21,#results)
for ir=1, count do
if (#str > 0) str = str .. '\n'
local result = results[ir]
if type(result) == 'table' then
local line = ''
for i=1,result.n do
if (#line > 0) line = line .. ', '
line = line .. value_to_str(result[i])
end
str = str .. line
else
str = str .. result
end
end
local new_results = {}
for i=count+1, #results do new_results[i - count] = results[i] end
return str, new_results
end
-- Console output
poke(0x5f2d,1) -- enable keyboard
cls()
local g_prompt = "> " -- currently must be valid token!
local g_input, g_input_lines = "", 1
local g_cursor_pos, g_cursor_time = 1, 20
local g_str_output, g_error_output
local g_history, g_history_i = {''}, 1
local g_num_output_lines = 0
local g_interrupt = nil
local g_notice = nil
local g_abort = false
local g_line = 1
local g_enable_pause, g_enable_interrupt = false, true
local g_pal = {7,4,3,5,6,8,5,12,14,7,11}
g_ENV.print = function(value, ...) -- override print for better output
if (not is_pack_empty(pack(...)) or not g_enable_interrupt) return print(value, ...)
add(g_results, tostr(value))
end
local function unpause()
if (not g_enable_pause) poke(0x5f30,1) -- suppress pause (e.g. from p, etc.)
end
local function walk_str(str, cb)
local i = 1
local x, y = 0, 0
if (not str) return i, x, y
while i <= #str do
local ch = sub(str,i,i)
local spch = ch >= '\x80'
if (x >= (spch and 31 or 32)) y += 1; x = 0
if (cb) cb(i,ch,x,y)
if ch == '\n' then y += 1; x = 0
else x += (spch and 2 or 1) end
i += 1
end
return i, x, y
end
local function str_i2xy(str, ci)
local cx, cy = 0, 0
local ei, ex, ey = walk_str(str, function(i,ch,x,y)
if (ci == i) cx, cy = x, y
end)
if (ci >= ei) cx, cy = ex, ey
if (ex > 0) ey += 1
return cx, cy, ey
end
local function str_xy2i(str, cx, cy)
local ci = 1
local found = false
local ei, ex, ey = walk_str(str, function(i,ch,x,y)
if (cy == y and cx == x and not found) ci = i; found = true
if ((cy < y or cy == y and cx < x) and not found) ci = i - 1; found = true
end)
if (not found) ci = cy >= ey and ei or ei - 1
if (ex > 0) ey += 1
return ci, ey
end
local function str_print(str, xpos, ypos, color)
local c, func = color, type(color) == "function"
walk_str(str, function(i,ch,x,y)
if (func) c = color(i)
print(ch, xpos + x*4, ypos + y*6, c)
end)
end
local function str_print_input(input, xpos, ypos)
local tokens, _, tstarts, tends = tokenize(input, true) -- tlines not reliable!
local ti = 1
str_print(input, xpos, ypos, function(i)
while ti <= #tends and tends[ti] < i do ti += 1 end
local token
if (ti <= #tends and tstarts[ti] <= i) token = tokens[ti]
local c = g_pal[5]
if token == false then c = g_pal[6] -- error
elseif token == true then c = g_pal[7] -- comment
elseif type(token) != 'string' or isin(token, {"nil","true","false"}) then c = g_pal[8]
elseif isin(token, keywords) then c = g_pal[9]
elseif not isalnum(sub(token,1,1)) then c = g_pal[10]
elseif globfuncs[token] then c = g_pal[11] end
return c
end)
end
local function _draw()
local old_color = peek(0x5f25)
local old_camx, old_camy = peek2(0x5f28), peek2(0x5f2a)
camera()
local function scroll(count)
cursor(0,127)
for i=1,count do
rectfill(0,g_line*6,127,(g_line+1)*6-1,0)
if g_line < 21 then
g_line += 1
else
print("")
end
end
end
local function draw_cursor(x, y)
for i=0,2 do
local c = pget(x+i,y+5)
pset(x+i,y+5,c==0 and 5 or 0)
end
end
local function draw_input(cursor)
local input = g_prompt .. g_input .. ' '
local cx, cy, ilines = str_i2xy(input, #g_prompt + g_cursor_pos) -- ' ' is cursor placeholder
if ilines > g_input_lines then
scroll(ilines - g_input_lines)
g_input_lines = ilines
end
local y = (g_line - g_input_lines)*6
rectfill(0,y,127,y+g_input_lines*6-1,0)
str_print_input(input,0,y)
print(g_prompt,0,y,g_pal[4])
if (g_cursor_time >= 10 and cursor != false and not g_interrupt) draw_cursor(cx*4, y + cy*6)
end
local function page_interrupt(page_olines)
scroll(1)
g_line -= 1
print("[enter] ('esc' to abort)",0,g_line*6,g_pal[3])
while true do
flip(); unpause()
local key = stat(31)
if (key == '\x1b') g_abort = true; g_str_output = ''; g_results = {}; return false
if (key == '\r' or key == '\n') g_num_output_lines += page_olines; return true
popkeys()
end
end
::again::
local ostart, olines
if g_results or g_str_output then
ostart, olines = str_xy2i(g_str_output, 0, g_num_output_lines)
if olines - g_num_output_lines <= 20 and g_results then -- add more output
g_str_output, g_results = results_to_str(g_str_output, g_results)
ostart, olines = str_xy2i(g_str_output, 0, g_num_output_lines)
if (#g_results == 0 and not g_interrupt) g_results = nil
end
end
if (not g_interrupt) camera()
if (g_num_output_lines == 0 and not g_interrupt) draw_input(not g_str_output)
if g_str_output then
local output = sub(g_str_output, ostart)
local page_olines = min(olines - g_num_output_lines, 20)
scroll(page_olines)
str_print(output,0,(g_line - page_olines)*6,g_pal[1])
if page_olines < olines - g_num_output_lines then
if (page_interrupt(page_olines)) goto again
else
local _, _, elines = str_i2xy(g_error_output, 0)
scroll(elines)
str_print(g_error_output,0,(g_line - elines)*6,g_pal[2])
if not g_interrupt then
g_input, g_input_lines, g_cursor_pos, g_str_output, g_error_output, g_num_output_lines =
'', 0, 1, nil, nil, 0
draw_input()
end
end
end
if g_interrupt then
scroll(1)
g_line -= 1
print(g_interrupt,0,g_line*6,g_pal[3])
end
if g_notice then
scroll(1)
g_line -= 1
print(g_notice,0,g_line*6,g_pal[3])
g_notice = nil
end
g_cursor_time -= 1
if (g_cursor_time == 0) g_cursor_time = 20
color(old_color)
camera(old_camx, old_camy)
if (g_line <= 20) cursor(0, g_line * 6)
end
--- Execution loop
local function execute_raw(line, eval)
local root = parse(tokenize(line))
local retval = root({[0]={_=g_last_value, _env=g_ENV}})
if retval then
if (retval == true) fail("break outside of loop")
if (type(retval) == "number") fail("goto out of program")
if (not eval) fail("return outside of function")
return unpack(retval)
end
end
local function execute(line)
g_results, g_error, g_abort = {}, nil, false
local coro = cocreate(function () execute_raw(line) end)
local ok, error
while true do
ok, error = coresume(coro)
if (costatus(coro) == 'dead') break
if g_enable_interrupt then
g_interrupt = "running, press 'esc' to abort"
_draw(); flip(); unpause()
g_interrupt = nil
else
flip(); unpause()
end
local key = stat(31)
if (g_abort or key == '\x1b') error = 'computation aborted'; break
popkeys()
end
if (g_error == false) error, g_results = nil, nil -- incomplete
if (g_error) error, g_error = g_error, nil
g_error_output = error
end
-- Special ('@') commands
local function do_mainloop(env)
-- not very compatible, mind!
if (_set_fps) _set_fps(env._update60 and 60 or 30)
if (env._init) env._init()
while true do
if (_update_buttons) _update_buttons()
if env._update60 then env._update60() elseif env._update then env._update() end
if (env._draw) env._draw()
-- TODO: pausemenu handling; time() freezing
end
end
cmd_exec = function(name, assign, value)
local function trueish(t)
return (t and t != 0) and true or false
end
if isin(name, {"i","interrupt"}) then
if (assign) g_enable_interrupt = trueish(value)
return g_enable_interrupt
elseif isin(name, {"p","pause"}) then
if (assign) g_enable_pause = trueish(value)
return g_enable_pause
elseif isin(name, {"mi","max_items"}) then
if (assign) g_show_max_items = tonum(value) or -1
return g_show_max_items
elseif isin(name, {"c","code"}) and not assign then
local code = {[0]=g_input}
for i=1,#g_history-1 do code[i] = g_history[#g_history-i] end
return code
elseif isin(name, {"cl","colors"}) then
if (assign) g_pal = copy(value)
return g_pal
elseif isin(name, {"x","exec"}) and not assign then
return function(str) execute_raw(str) end
elseif isin(name, {"v","eval"}) and not assign then
return function(str) return execute_raw("return " .. str, true) end
elseif isin(name, {"rst","reset"}) then
run() -- full pico8 reset
elseif isin(name, {"run"}) then
do_mainloop(g_ENV)
else
fail("unknown @-command")
end
end
-- Console input
local g_btn_frames = {}
local g_frame = 0
local g_ideal_x = nil
local g_prev_paste = stat(4)
local g_prev_gpio_paste = ''
local function get_gpio()
if (not chr) return '' -- eh
local addr = 0x5f80
local str = ''
for i=0,0x7f do
local ch = peek(addr+i)
if (ch == 0) break
str = str .. chr(ch)
end
return str
end
local function _update()
local input, pause = false, false
local function go_line(dy)
local cx, cy, h = str_i2xy(g_prompt .. g_input, #g_prompt + g_cursor_pos)
if (g_ideal_x) cx = g_ideal_x
cy += dy
if (not (cy >= 0 and cy < h)) return false
g_cursor_pos = max(str_xy2i(g_prompt .. g_input, cx, cy) - #g_prompt, 1)
g_ideal_x = cx
g_cursor_time = 20 -- setting input clears ideal x
return true
end
local function go_edge(dx)
local cx, cy = str_i2xy(g_prompt .. g_input, #g_prompt + g_cursor_pos)
cx = dx > 0 and 100 or 0
g_cursor_pos = max(str_xy2i(g_prompt .. g_input, cx, cy) - #g_prompt, 1)
input = true
end
local function go_history(di)
g_history[g_history_i] = g_input
g_history_i += di
g_input = g_history[g_history_i]
if di < 0 then
g_cursor_pos = #g_input + 1
else
g_cursor_pos = max(str_xy2i(g_prompt .. g_input, 32, 0) - #g_prompt, 1) -- end of first line
local ch = sub(g_input,g_cursor_pos,g_cursor_pos)
if (ch != '' and ch != '\n') g_cursor_pos -= 1
end
input = true
end
local function push_history()
if #g_input > 0 then
if (#g_history > 50) del(g_history, g_history[1])
g_history[#g_history] = g_input
add(g_history, '')
g_history_i = #g_history
input = true
end
end
local function delchar()
if (g_cursor_pos > 1) then
g_input = sub(g_input,1,g_cursor_pos-2) .. sub(g_input,g_cursor_pos)
g_cursor_pos -= 1
input = true
end
end
local function inschar(key)
g_input = sub(g_input,1,g_cursor_pos-1) .. key .. sub(g_input,g_cursor_pos)
g_cursor_pos += #key
input = true
end
local function btnpfast(i)
if btn(i) then
if (not g_btn_frames[i]) g_btn_frames[i] = g_frame return true
local delta = g_frame - g_btn_frames[i]
return delta > 10 and delta % 2 == 0
else
g_btn_frames[i] = nil; return false
end
end
local keycode = -1
if btnpfast(0) then
if (g_cursor_pos > 1) g_cursor_pos -= 1; input = true
elseif btnpfast(1) then
if (g_cursor_pos <= #g_input) g_cursor_pos += 1; input = true
elseif btnpfast(2) then
if (not go_line(-1) and g_history_i > 1) go_history(-1)
elseif btnpfast(3) then
if (not go_line(1) and g_history_i < #g_history) go_history(1)
else
local key = stat(31)
if (ord) keycode = ord(key)
if key == '\b' then delchar()
elseif key == '\x1b' then -- escape
if #g_input == 0 then extcmd("pause")
else g_results, g_error_output = {}, nil; push_history() end
elseif key == '\r' or key == '\n' then
execute(g_input) -- sets g_results/g_error_output
if (not g_results) inschar('\n') else push_history()
elseif key != '' and keycode < 0x9a then -- ignore ctrl-junk
inschar(key)
elseif keycode == 193 then -- ctrl+b for 'better than nothing'
inschar('\n')
elseif keycode == 192 then -- ctrl+a
go_edge(-1)
elseif keycode == 196 then -- ctrl+e
go_edge(1)
end
end
local paste = stat(4)
if (paste != g_prev_paste or keycode == 213) inschar(paste); g_prev_paste = paste
if keycode == 194 or keycode == 215 then
if g_input != '' and g_input != g_prev_paste then
g_prev_paste = g_input; printh(g_input, "@clip");
if (keycode == 215) g_input = ''; g_cursor_pos = 1;
g_notice = "press again to put in clipboard"
else
g_notice = ''
end
end
local gpio_paste = get_gpio() -- DEPRECATED
if (gpio_paste != g_prev_gpio_paste) inschar(gpio_paste); g_prev_gpio_paste = gpio_paste
if (input) g_cursor_time = 20; g_ideal_x = nil
unpause()
g_frame += 1
end
-- my own crummy mainloop, since time() does not seem to update if the regular mainloop goes "rogue" and flips.
while true do
_update()
_draw()
flip()
end
end)()
-- features: _ is last value, ? prints as usual, esc aborts, _env is all globals, @i command to avoid interruptions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment