Skip to content

Instantly share code, notes, and snippets.

@SquidDev
Last active April 13, 2017 09:38
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 SquidDev/e204ea9b6032dabf0cba3f57893125ba to your computer and use it in GitHub Desktop.
Save SquidDev/e204ea9b6032dabf0cba3f57893125ba to your computer and use it in GitHub Desktop.
local loading = {}
local oldRequire, preload, loaded = require, {}, { startup = loading }
local function require(name)
local result = loaded[name]
if result ~= nil then
if result == loading then
error("loop or previous error loading module '" .. name .. "'", 2)
end
return result
end
loaded[name] = loading
local contents = preload[name]
if contents then
result = contents(name)
elseif oldRequire then
result = oldRequire(name)
else
error("cannot load '" .. name .. "'", 2)
end
if result == nil then result = true end
loaded[name] = result
return result
end
preload["bsrocks.lib.parse"] = function(...)
--- Check if a Lua source is either invalid or incomplete
local setmeta = setmetatable
local function createLookup(tbl)
for _, v in ipairs(tbl) do tbl[v] = true end
return tbl
end
--- List of white chars
local whiteChars = createLookup { ' ', '\n', '\t', '\r' }
--- Lookup of escape characters
local escapeLookup = { ['\r'] = '\\r', ['\n'] = '\\n', ['\t'] = '\\t', ['"'] = '\\"', ["'"] = "\\'" }
--- Lookup of lower case characters
local lowerChars = createLookup {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
}
--- Lookup of upper case characters
local upperChars = createLookup {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
}
--- Lookup of digits
local digits = createLookup { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }
--- Lookup of hex digits
local hexDigits = createLookup {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'a', 'B', 'b', 'C', 'c', 'D', 'd', 'E', 'e', 'F', 'f'
}
--- Lookup of valid symbols
local symbols = createLookup { '+', '-', '*', '/', '^', '%', ',', '{', '}', '[', ']', '(', ')', ';', '#' }
--- Lookup of valid keywords
local keywords = createLookup {
'and', 'break', 'do', 'else', 'elseif',
'end', 'false', 'for', 'function', 'goto', 'if',
'in', 'local', 'nil', 'not', 'or', 'repeat',
'return', 'then', 'true', 'until', 'while',
}
--- Keywords that end a block
local statListCloseKeywords = createLookup { 'end', 'else', 'elseif', 'until' }
--- Unary operators
local unops = createLookup { '-', 'not', '#' }
--- Stores a list of tokens
-- @type TokenList
-- @tfield table tokens List of tokens
-- @tfield number pointer Pointer to the current
-- @tfield table savedPointers A save point
local TokenList = {}
do
--- Get this element in the token list
-- @tparam int offset The offset in the token list
function TokenList:Peek(offset)
local tokens = self.tokens
offset = offset or 0
return tokens[math.min(#tokens, self.pointer + offset)]
end
--- Get the next token in the list
-- @tparam table tokenList Add the token onto this table
-- @treturn Token The token
function TokenList:Get(tokenList)
local tokens = self.tokens
local pointer = self.pointer
local token = tokens[pointer]
self.pointer = math.min(pointer + 1, #tokens)
if tokenList then
table.insert(tokenList, token)
end
return token
end
--- Check if the next token is of a type
-- @tparam string type The type to compare it with
-- @treturn bool If the type matches
function TokenList:Is(type)
return self:Peek().Type == type
end
--- Check if the next token is a symbol and return it
-- @tparam string symbol Symbol to check (Optional)
-- @tparam table tokenList Add the token onto this table
-- @treturn [ 0 ] ?|token If symbol is not specified, return the token
-- @treturn [ 1 ] boolean If symbol is specified, return true if it matches
function TokenList:ConsumeSymbol(symbol, tokenList)
local token = self:Peek()
if token.Type == 'Symbol' then
if symbol then
if token.Data == symbol then
self:Get(tokenList)
return true
else
return nil
end
else
self:Get(tokenList)
return token
end
else
return nil
end
end
--- Check if the next token is a keyword and return it
-- @tparam string kw Keyword to check (Optional)
-- @tparam table tokenList Add the token onto this table
-- @treturn [ 0 ] ?|token If kw is not specified, return the token
-- @treturn [ 1 ] boolean If kw is specified, return true if it matches
function TokenList:ConsumeKeyword(kw, tokenList)
local token = self:Peek()
if token.Type == 'Keyword' and token.Data == kw then
self:Get(tokenList)
return true
else
return nil
end
end
--- Check if the next token matches is a keyword
-- @tparam string kw The particular keyword
-- @treturn boolean If it matches or not
function TokenList:IsKeyword(kw)
local token = self:Peek()
return token.Type == 'Keyword' and token.Data == kw
end
--- Check if the next token matches is a symbol
-- @tparam string symbol The particular symbol
-- @treturn boolean If it matches or not
function TokenList:IsSymbol(symbol)
local token = self:Peek()
return token.Type == 'Symbol' and token.Data == symbol
end
--- Check if the next token is an end of file
-- @treturn boolean If the next token is an end of file
function TokenList:IsEof()
return self:Peek().Type == 'Eof'
end
end
--- Create a list of @{Token|tokens} from a Lua source
-- @tparam string src Lua source code
-- @treturn TokenList The list of @{Token|tokens}
local function lex(src)
--token dump
local tokens = {}
do -- Main bulk of the work
--line / char / pointer tracking
local pointer = 1
local line = 1
local char = 1
--get / peek functions
local function get()
local c = src:sub(pointer,pointer)
if c == '\n' then
char = 1
line = line + 1
else
char = char + 1
end
pointer = pointer + 1
return c
end
local function peek(n)
n = n or 0
return src:sub(pointer+n,pointer+n)
end
local function consume(chars)
local c = peek()
for i = 1, #chars do
if c == chars:sub(i,i) then return get() end
end
end
--shared stuff
local function generateError(err, resumable)
if resumable == true then
resumable = 1
else
resumable = 0
end
error(line..":"..char..":"..resumable..":"..err, 0)
end
local function tryGetLongString()
local start = pointer
if peek() == '[' then
local equalsCount = 0
local depth = 1
while peek(equalsCount+1) == '=' do
equalsCount = equalsCount + 1
end
if peek(equalsCount+1) == '[' then
--start parsing the string. Strip the starting bit
for _ = 0, equalsCount+1 do get() end
--get the contents
local contentStart = pointer
while true do
--check for eof
if peek() == '' then
generateError("Expected `]"..string.rep('=', equalsCount).."]` near <eof>.", true)
end
--check for the end
local foundEnd = true
if peek() == ']' then
for i = 1, equalsCount do
if peek(i) ~= '=' then foundEnd = false end
end
if peek(equalsCount+1) ~= ']' then
foundEnd = false
end
else
if peek() == '[' then
-- is there an embedded long string?
local embedded = true
for i = 1, equalsCount do
if peek(i) ~= '=' then
embedded = false
break
end
end
if peek(equalsCount + 1) == '[' and embedded then
-- oh look, there was
depth = depth + 1
for i = 1, (equalsCount + 2) do
get()
end
end
end
foundEnd = false
end
if foundEnd then
depth = depth - 1
if depth == 0 then
break
else
for i = 1, equalsCount + 2 do
get()
end
end
else
get()
end
end
--get the interior string
local contentString = src:sub(contentStart, pointer-1)
--found the end. Get rid of the trailing bit
for i = 0, equalsCount+1 do get() end
--get the exterior string
local longString = src:sub(start, pointer-1)
--return the stuff
return contentString, longString
else
return nil
end
else
return nil
end
end
--main token emitting loop
while true do
--get leading whitespace. The leading whitespace will include any comments
--preceding the token. This prevents the parser needing to deal with comments
--separately.
local longStr = false
while true do
local c = peek()
if c == '#' and peek(1) == '!' and line == 1 then
-- #! shebang for linux scripts
get()
get()
while peek() ~= '\n' and peek() ~= '' do
get()
end
end
if c == ' ' or c == '\t' or c == '\n' or c == '\r' then
get()
elseif c == '-' and peek(1) == '-' then
--comment
get() get()
local _, wholeText = tryGetLongString()
if not wholeText then
while peek() ~= '\n' and peek() ~= '' do
get()
end
end
else
break
end
end
--get the initial char
local thisLine = line
local thisChar = char
local errorAt = ":"..line..":"..char..":> "
local c = peek()
--symbol to emit
local toEmit = nil
--branch on type
if c == '' then
--eof
toEmit = { Type = 'Eof' }
elseif upperChars[c] or lowerChars[c] or c == '_' then
--ident or keyword
local start = pointer
repeat
get()
c = peek()
until not (upperChars[c] or lowerChars[c] or digits[c] or c == '_')
local dat = src:sub(start, pointer-1)
if keywords[dat] then
toEmit = {Type = 'Keyword', Data = dat}
else
toEmit = {Type = 'Ident', Data = dat}
end
elseif digits[c] or (peek() == '.' and digits[peek(1)]) then
--number const
local start = pointer
if c == '0' and peek(1) == 'x' then
get();get()
while hexDigits[peek()] do get() end
if consume('Pp') then
consume('+-')
while digits[peek()] do get() end
end
else
while digits[peek()] do get() end
if consume('.') then
while digits[peek()] do get() end
end
if consume('Ee') then
consume('+-')
if not digits[peek()] then generateError("Expected exponent") end
repeat get() until not digits[peek()]
end
local n = peek():lower()
if (n >= 'a' and n <= 'z') or n == '_' then
generateError("Invalid number format")
end
end
toEmit = {Type = 'Number', Data = src:sub(start, pointer-1)}
elseif c == '\'' or c == '\"' then
local start = pointer
--string const
local delim = get()
local contentStart = pointer
while true do
local c = get()
if c == '\\' then
get() --get the escape char
elseif c == delim then
break
elseif c == '' or c == '\n' then
generateError("Unfinished string near <eof>")
end
end
local content = src:sub(contentStart, pointer-2)
local constant = src:sub(start, pointer-1)
toEmit = {Type = 'String', Data = constant, Constant = content}
elseif c == '[' then
local content, wholetext = tryGetLongString()
if wholetext then
toEmit = {Type = 'String', Data = wholetext, Constant = content}
else
get()
toEmit = {Type = 'Symbol', Data = '['}
end
elseif consume('>=<') then
if consume('=') then
toEmit = {Type = 'Symbol', Data = c..'='}
else
toEmit = {Type = 'Symbol', Data = c}
end
elseif consume('~') then
if consume('=') then
toEmit = {Type = 'Symbol', Data = '~='}
else
generateError("Unexpected symbol `~` in source.")
end
elseif consume('.') then
if consume('.') then
if consume('.') then
toEmit = {Type = 'Symbol', Data = '...'}
else
toEmit = {Type = 'Symbol', Data = '..'}
end
else
toEmit = {Type = 'Symbol', Data = '.'}
end
elseif consume(':') then
if consume(':') then
toEmit = {Type = 'Symbol', Data = '::'}
else
toEmit = {Type = 'Symbol', Data = ':'}
end
elseif symbols[c] then
get()
toEmit = {Type = 'Symbol', Data = c}
else
local contents, all = tryGetLongString()
if contents then
toEmit = {Type = 'String', Data = all, Constant = contents}
else
generateError("Unexpected Symbol `"..c.."` in source.")
end
end
--add the emitted symbol, after adding some common data
toEmit.line = thisLine
toEmit.char = thisChar
tokens[#tokens+1] = toEmit
--halt after eof has been emitted
if toEmit.Type == 'Eof' then break end
end
end
--public interface:
local tokenList = setmetatable({
tokens = tokens,
pointer = 1
}, {__index = TokenList})
return tokenList
end
--- Create a AST tree from a Lua Source
-- @tparam TokenList tok List of tokens from @{lex}
-- @treturn table The AST tree
local function parse(tok)
--- Generate an error
-- @tparam string msg The error message
-- @raise The produces error message
local function GenerateError(msg) error(msg, 0) end
local ParseExpr,
ParseStatementList,
ParseSimpleExpr
--- Parse the function definition and its arguments
-- @tparam Scope.Scope scope The current scope
-- @treturn Node A function Node
local function ParseFunctionArgsAndBody()
if not tok:ConsumeSymbol('(') then
GenerateError("`(` expected.")
end
--arg list
while not tok:ConsumeSymbol(')') do
if tok:Is('Ident') then
tok:Get()
if not tok:ConsumeSymbol(',') then
if tok:ConsumeSymbol(')') then
break
else
GenerateError("`)` expected.")
end
end
elseif tok:ConsumeSymbol('...') then
if not tok:ConsumeSymbol(')') then
GenerateError("`...` must be the last argument of a function.")
end
break
else
GenerateError("Argument name or `...` expected")
end
end
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected after function body")
end
end
--- Parse a simple expression
-- @tparam Scope.Scope scope The current scope
-- @treturn Node the resulting node
local function ParsePrimaryExpr()
if tok:ConsumeSymbol('(') then
ParseExpr()
if not tok:ConsumeSymbol(')') then
GenerateError("`)` Expected.")
end
return { AstType = "Paren" }
elseif tok:Is('Ident') then
tok:Get()
else
GenerateError("primary expression expected")
end
end
--- Parse some table related expressions
-- @tparam boolean onlyDotColon Only allow '.' or ':' nodes
-- @treturn Node The resulting node
function ParseSuffixedExpr(onlyDotColon)
--base primary expression
local prim = ParsePrimaryExpr() or { AstType = ""}
while true do
local tokenList = {}
if tok:ConsumeSymbol('.') or tok:ConsumeSymbol(':') then
if not tok:Is('Ident') then
GenerateError("<Ident> expected.")
end
tok:Get()
prim = { AstType = 'MemberExpr' }
elseif not onlyDotColon and tok:ConsumeSymbol('[') then
ParseExpr()
if not tok:ConsumeSymbol(']') then
GenerateError("`]` expected.")
end
prim = { AstType = 'IndexExpr' }
elseif not onlyDotColon and tok:ConsumeSymbol('(') then
while not tok:ConsumeSymbol(')') do
ParseExpr()
if not tok:ConsumeSymbol(',') then
if tok:ConsumeSymbol(')') then
break
else
GenerateError("`)` Expected.")
end
end
end
prim = { AstType = 'CallExpr' }
elseif not onlyDotColon and tok:Is('String') then
--string call
tok:Get()
prim = { AstType = 'StringCallExpr' }
elseif not onlyDotColon and tok:IsSymbol('{') then
--table call
ParseSimpleExpr()
prim = { AstType = 'TableCallExpr' }
else
break
end
end
return prim
end
--- Parse a simple expression (strings, numbers, booleans, varargs)
-- @treturn Node The resulting node
function ParseSimpleExpr()
if tok:Is('Number') or tok:Is('String') then
tok:Get()
elseif tok:ConsumeKeyword('nil') or tok:ConsumeKeyword('false') or tok:ConsumeKeyword('true') or tok:ConsumeSymbol('...') then
elseif tok:ConsumeSymbol('{') then
while true do
if tok:ConsumeSymbol('[') then
--key
ParseExpr()
if not tok:ConsumeSymbol(']') then
GenerateError("`]` Expected")
end
if not tok:ConsumeSymbol('=') then
GenerateError("`=` Expected")
end
ParseExpr()
elseif tok:Is('Ident') then
--value or key
local lookahead = tok:Peek(1)
if lookahead.Type == 'Symbol' and lookahead.Data == '=' then
--we are a key
local key = tok:Get()
if not tok:ConsumeSymbol('=') then
GenerateError("`=` Expected")
end
ParseExpr()
else
--we are a value
ParseExpr()
end
elseif tok:ConsumeSymbol('}') then
break
else
ParseExpr()
end
if tok:ConsumeSymbol(';') or tok:ConsumeSymbol(',') then
--all is good
elseif tok:ConsumeSymbol('}') then
break
else
GenerateError("`}` or table entry Expected")
end
end
elseif tok:ConsumeKeyword('function') then
return ParseFunctionArgsAndBody()
else
return ParseSuffixedExpr()
end
end
local unopprio = 8
local priority = {
['+'] = {6,6},
['-'] = {6,6},
['%'] = {7,7},
['/'] = {7,7},
['*'] = {7,7},
['^'] = {10,9},
['..'] = {5,4},
['=='] = {3,3},
['<'] = {3,3},
['<='] = {3,3},
['~='] = {3,3},
['>'] = {3,3},
['>='] = {3,3},
['and'] = {2,2},
['or'] = {1,1},
}
--- Parse an expression
-- @tparam int level Current level (Optional)
-- @treturn Node The resulting node
function ParseExpr(level)
level = level or 0
--base item, possibly with unop prefix
if unops[tok:Peek().Data] then
local op = tok:Get().Data
ParseExpr(unopprio)
else
ParseSimpleExpr()
end
--next items in chain
while true do
local prio = priority[tok:Peek().Data]
if prio and prio[1] > level then
local tokenList = {}
tok:Get()
ParseExpr(prio[2])
else
break
end
end
end
--- Parse a statement (if, for, while, etc...)
-- @treturn Node The resulting node
local function ParseStatement()
if tok:ConsumeKeyword('if') then
--clauses
repeat
ParseExpr()
if not tok:ConsumeKeyword('then') then
GenerateError("`then` expected.")
end
ParseStatementList()
until not tok:ConsumeKeyword('elseif')
--else clause
if tok:ConsumeKeyword('else') then
ParseStatementList()
end
--end
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
elseif tok:ConsumeKeyword('while') then
--condition
ParseExpr()
--do
if not tok:ConsumeKeyword('do') then
return GenerateError("`do` expected.")
end
--body
ParseStatementList()
--end
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
elseif tok:ConsumeKeyword('do') then
--do block
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
elseif tok:ConsumeKeyword('for') then
--for block
if not tok:Is('Ident') then
GenerateError("<ident> expected.")
end
tok:Get()
if tok:ConsumeSymbol('=') then
--numeric for
ParseExpr()
if not tok:ConsumeSymbol(',') then
GenerateError("`,` Expected")
end
ParseExpr()
if tok:ConsumeSymbol(',') then
ParseExpr()
end
if not tok:ConsumeKeyword('do') then
GenerateError("`do` expected")
end
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected")
end
else
--generic for
while tok:ConsumeSymbol(',') do
if not tok:Is('Ident') then
GenerateError("for variable expected.")
end
tok:Get(tokenList)
end
if not tok:ConsumeKeyword('in') then
GenerateError("`in` expected.")
end
ParseExpr()
while tok:ConsumeSymbol(',') do
ParseExpr()
end
if not tok:ConsumeKeyword('do') then
GenerateError("`do` expected.")
end
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
end
elseif tok:ConsumeKeyword('repeat') then
ParseStatementList()
if not tok:ConsumeKeyword('until') then
GenerateError("`until` expected.")
end
ParseExpr()
elseif tok:ConsumeKeyword('function') then
if not tok:Is('Ident') then
GenerateError("Function name expected")
end
ParseSuffixedExpr(true) --true => only dots and colons
ParseFunctionArgsAndBody()
elseif tok:ConsumeKeyword('local') then
if tok:Is('Ident') then
tok:Get()
while tok:ConsumeSymbol(',') do
if not tok:Is('Ident') then
GenerateError("local var name expected")
end
tok:Get()
end
if tok:ConsumeSymbol('=') then
repeat
ParseExpr()
until not tok:ConsumeSymbol(',')
end
elseif tok:ConsumeKeyword('function') then
if not tok:Is('Ident') then
GenerateError("Function name expected")
end
tok:Get(tokenList)
ParseFunctionArgsAndBody()
else
GenerateError("local var or function def expected")
end
elseif tok:ConsumeSymbol('::') then
if not tok:Is('Ident') then
GenerateError('Label name expected')
end
tok:Get()
if not tok:ConsumeSymbol('::') then
GenerateError("`::` expected")
end
elseif tok:ConsumeKeyword('return') then
local exList = {}
local token = tok:Peek()
if token.Type == "Eof" or token.Type ~= "Keyword" or not statListCloseKeywords[token.Data] then
ParseExpr()
local token = tok:Peek()
while tok:ConsumeSymbol(',') do
ParseExpr()
end
end
elseif tok:ConsumeKeyword('break') then
elseif tok:ConsumeKeyword('goto') then
if not tok:Is('Ident') then
GenerateError("Label expected")
end
tok:Get(tokenList)
else
--statementParseExpr
local suffixed = ParseSuffixedExpr()
--assignment or call?
if tok:IsSymbol(',') or tok:IsSymbol('=') then
--check that it was not parenthesized, making it not an lvalue
if suffixed.AstType == "Paren" then
GenerateError("Can not assign to parenthesized expression, is not an lvalue")
end
--more processing needed
while tok:ConsumeSymbol(',') do
ParseSuffixedExpr()
end
--equals
if not tok:ConsumeSymbol('=') then
GenerateError("`=` Expected.")
end
--rhs
ParseExpr()
while tok:ConsumeSymbol(',') do
ParseExpr()
end
elseif suffixed.AstType == 'CallExpr' or
suffixed.AstType == 'TableCallExpr' or
suffixed.AstType == 'StringCallExpr'
then
--it's a call statement
else
GenerateError("Assignment Statement Expected")
end
end
tok:ConsumeSymbol(';')
end
--- Parse a a list of statements
-- @tparam Scope.Scope scope The current scope
-- @treturn Node The resulting node
function ParseStatementList()
while not statListCloseKeywords[tok:Peek().Data] and not tok:IsEof() do
ParseStatement()
end
end
return ParseStatementList()
end
return {
lex = lex,
parse = parse,
}
end
preload["bsrocks.lib.dump"] = function(...)
local keywords = {
[ "and" ] = true, [ "break" ] = true, [ "do" ] = true, [ "else" ] = true,
[ "elseif" ] = true, [ "end" ] = true, [ "false" ] = true, [ "for" ] = true,
[ "function" ] = true, [ "if" ] = true, [ "in" ] = true, [ "local" ] = true,
[ "nil" ] = true, [ "not" ] = true, [ "or" ] = true, [ "repeat" ] = true, [ "return" ] = true,
[ "then" ] = true, [ "true" ] = true, [ "until" ] = true, [ "while" ] = true,
}
local function serializeImpl(t, tracking, indent, tupleLength)
local objType = type(t)
if objType == "table" and not tracking[t] then
tracking[t] = true
if next(t) == nil then
if tupleLength then
return "()"
else
return "{}"
end
else
local shouldNewLine = false
local length = tupleLength or #t
local builder = 0
for k,v in pairs(t) do
if type(k) == "table" or type(v) == "table" then
shouldNewLine = true
break
elseif type(k) == "number" and k >= 1 and k <= length and k % 1 == 0 then
builder = builder + #tostring(v) + 2
else
builder = builder + #tostring(v) + #tostring(k) + 2
end
if builder > 30 then
shouldNewLine = true
break
end
end
local newLine, nextNewLine, subIndent = "", ", ", ""
if shouldNewLine then
newLine = "\n"
nextNewLine = ",\n"
subIndent = indent .. " "
end
local result, n = {(tupleLength and "(" or "{") .. newLine}, 1
local seen = {}
local first = true
for k = 1, length do
seen[k] = true
n = n + 1
local entry = subIndent .. serializeImpl(t[k], tracking, subIndent)
if not first then
entry = nextNewLine .. entry
else
first = false
end
result[n] = entry
end
for k,v in pairs(t) do
if not seen[k] then
local entry
if type(k) == "string" and not keywords[k] and string.match( k, "^[%a_][%a%d_]*$" ) then
entry = k .. " = " .. serializeImpl(v, tracking, subIndent)
else
entry = "[" .. serializeImpl(k, tracking, subIndent) .. "] = " .. serializeImpl(v, tracking, subIndent)
end
entry = subIndent .. entry
if not first then
entry = nextNewLine .. entry
else
first = false
end
n = n + 1
result[n] = entry
end
end
n = n + 1
result[n] = newLine .. indent .. (tupleLength and ")" or "}")
return table.concat(result)
end
elseif objType == "string" then
return (string.format("%q", t):gsub("\\\n", "\\n"))
else
return tostring(t)
end
end
local function serialize(t, n)
return serializeImpl(t, {}, "", n)
end
return serialize
end
preload["bsrocks.commands.repl"] = function(...)
local env = require "bsrocks.env"
local serialize = require "bsrocks.lib.dump"
local parse = require "bsrocks.lib.parse"
local function execute(...)
local running = true
local env = env()
local thisEnv = env._G
thisEnv.exit = setmetatable({}, {
__tostring = function() return "Call exit() to exit" end,
__call = function() running = false end,
})
-- We need to pass through a secondary function to prevent tail calls
thisEnv._noTail = function(...) return ... end
thisEnv.arg = { [0] = "repl", ... }
-- As per @demhydraz's suggestion. Because the prompt uses Out[n] as well
local output = {}
thisEnv.Out = output
local inputColour, outputColour, textColour = colours.green, colours.cyan, term.getTextColour()
local codeColour, pointerColour = colours.lightGrey, colours.lightBlue
if not term.isColour() then
inputColour = colours.white
outputColour = colours.white
codeColour = colours.white
pointerColour = colours.white
end
local autocomplete = nil
if not settings or settings.get("lua.autocomplete") then
autocomplete = function(line)
local start = line:find("[a-zA-Z0-9_%.]+$")
if start then
line = line:sub(start)
end
if #line > 0 then
return textutils.complete(line, thisEnv)
end
end
end
local history = {}
local counter = 1
--- Prints an output and sets the output variable
local function setOutput(out, length)
thisEnv._ = out
thisEnv['_' .. counter] = out
output[counter] = out
term.setTextColour(outputColour)
write("Out[" .. counter .. "]: ")
term.setTextColour(textColour)
if type(out) == "table" then
local meta = getmetatable(out)
if type(meta) == "table" and type(meta.__tostring) == "function" then
print(tostring(out))
else
print(serialize(out, length))
end
else
print(serialize(out))
end
end
--- Handle the result of the function
local function handle(forcePrint, success, ...)
if success then
local len = select('#', ...)
if len == 0 then
if forcePrint then
setOutput(nil)
end
elseif len == 1 then
setOutput(...)
else
setOutput({...}, len)
end
else
printError(...)
end
end
local function handleError(lines, line, column, message)
local contents = lines[line]
term.setTextColour(codeColour)
print(" " .. contents)
term.setTextColour(pointerColour)
print((" "):rep(column) .. "^ ")
printError(" " .. message)
end
local function execute(lines, force)
local buffer = table.concat(lines, "\n")
local forcePrint = false
local func, err = load(buffer, "lua", "t", thisEnv)
local func2, err2 = load("return " .. buffer, "lua", "t", thisEnv)
if not func then
if func2 then
func = load("return _noTail(" .. buffer .. ")", "lua", "t", thisEnv)
forcePrint = true
else
local success, tokens = pcall(parse.lex, buffer)
if not success then
local line, column, resumable, message = tokens:match("(%d+):(%d+):([01]):(.+)")
if line then
if line == #lines and column > #lines[line] and resumable == 1 then
return false
else
handleError(lines, tonumber(line), tonumber(column), message)
return true
end
else
printError(tokens)
return true
end
end
local success, message = pcall(parse.parse, tokens)
if not success then
if not force and tokens.pointer >= #tokens.tokens then
return false
else
local token = tokens.tokens[tokens.pointer]
handleError(lines, token.line, token.char, message)
return true
end
end
end
elseif func2 then
func = load("return _noTail(" .. buffer .. ")", "lua", "t", thisEnv)
end
if func then
handle(forcePrint, pcall(func))
counter = counter + 1
else
printError(err)
end
return true
end
local lines = {}
local input = "In [" .. counter .. "]: "
local isEmpty = false
while running do
term.setTextColour(inputColour)
write(input)
term.setTextColour(textColour)
local line = read(nil, history, autocomplete)
if not line then break end
if #line:gsub("%s", "") > 0 then
for i = #history, 1, -1 do
if history[i] == line then
table.remove(history, i)
break
end
end
history[#history + 1] = line
lines[#lines + 1] = line
isEmpty = false
if execute(lines) then
lines = {}
input = "In [" .. counter .. "]: "
else
input = (" "):rep(#tostring(counter) + 3) .. "... "
end
else
execute(lines, true)
lines = {}
isEmpty = false
input = "In [" .. counter .. "]: "
end
end
for _, v in pairs(env.cleanup) do v() end
end
local description = [[
This is almost identical to the built in Lua program with some simple differences.
Scripts are run in an environment similar to the exec command.
The result of the previous outputs are also stored in variables of the form _idx (the last result is also stored in _). For example: if Out[1] = 123 then _1 = 123 and _ = 123
]]
return {
name = "repl",
help = "Run a Lua repl in an emulated environment",
syntax = "",
description = description,
execute = execute,
}
end
preload["bsrocks.bin.repl"] = function(...)
preload['bsrocks.env'] = function()
return function()
return {
cleanup = {},
_G = setmetatable({}, {__index = _ENV})
}
end
end
return require "bsrocks.commands.repl".execute(...)
end
return preload["bsrocks.bin.repl"](...)
local e={}local t,a,o=require,{},{startup=e}
local function i(n)local s=o[n]
if s~=nil then if s==e then
error("loop or previous error loading module '"..n..
"'",2)end;return s end;o[n]=e;local h=a[n]if h then s=h(n)elseif t then s=t(n)else
error("cannot load '"..n.."'",2)end;if s==nil then s=true end;o[n]=s;return s end
a["bsrocks.lib.parse"]=function(...)local n=setmetatable;local function s(g)for k,q in ipairs(g)do g[q]=true end
return g end
local h=s{' ','\n','\t','\r'}
local r={['\r']='\\r',['\n']='\\n',['\t']='\\t',['"']='\\"',["'"]="\\'"}
local d=s{'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'}
local l=s{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'}
local u=s{'0','1','2','3','4','5','6','7','8','9'}
local c=s{'0','1','2','3','4','5','6','7','8','9','A','a','B','b','C','c','D','d','E','e','F','f'}
local m=s{'+','-','*','/','^','%',',','{','}','[',']','(',')',';','#'}
local f=s{'and','break','do','else','elseif','end','false','for','function','goto','if','in','local','nil','not','or','repeat','return','then','true','until','while'}local w=s{'end','else','elseif','until'}
local y=s{'-','not','#'}local p={}
do function p:Peek(g)local k=self.tokens;g=g or 0;return
k[math.min(#k,self.pointer+g)]end
function p:Get(g)
local k=self.tokens;local q=self.pointer;local j=k[q]self.pointer=math.min(q+1,#k)if g then
table.insert(g,j)end;return j end;function p:Is(g)return self:Peek().Type==g end
function p:ConsumeSymbol(g,k)
local q=self:Peek()if q.Type=='Symbol'then if g then
if q.Data==g then self:Get(k)return true else return nil end else self:Get(k)return q end else
return nil end end
function p:ConsumeKeyword(g,k)local q=self:Peek()if q.Type=='Keyword'and q.Data==g then
self:Get(k)return true else return nil end end;function p:IsKeyword(g)local k=self:Peek()
return k.Type=='Keyword'and k.Data==g end
function p:IsSymbol(g)local k=self:Peek()return k.Type==
'Symbol'and k.Data==g end
function p:IsEof()return self:Peek().Type=='Eof'end end
local function v(g)local k={}
do local j=1;local x=1;local z=1;local function _()local I=g:sub(j,j)
if I=='\n'then z=1;x=x+1 else z=z+1 end;j=j+1;return I end;local function E(I)I=I or 0;return
g:sub(j+I,j+I)end;local function T(I)local N=E()for S=1,#I do
if N==I:sub(S,S)then return _()end end end;local function A(I,N)if N==true then
N=1 else N=0 end
error(x..":"..z..":"..N..":"..I,0)end
local function O()local I=j
if E()=='['then local N=0;local S=1;while E(N+1)==
'='do N=N+1 end
if E(N+1)=='['then for L=0,N+1 do _()end;local H=j
while true do if E()==''then
A(
"Expected `]"..string.rep('=',N).."]` near <eof>.",true)end;local L=true
if E()==']'then for U=1,N do
if E(U)~='='then L=false end end;if E(N+1)~=']'then L=false end else
if E()=='['then local U=true;for C=1,N do if
E(C)~='='then U=false;break end end;if
E(N+1)=='['and U then S=S+1;for C=1,(N+2)do _()end end end;L=false end
if L then S=S-1;if S==0 then break else for U=1,N+2 do _()end end else _()end end;local R=g:sub(H,j-1)for L=0,N+1 do _()end;local D=g:sub(I,j-1)return R,D else return nil end else return nil end end
while true do local I=false
while true do local L=E()
if L=='#'and E(1)=='!'and x==1 then _()_()while
E()~='\n'and E()~=''do _()end end
if L==' 'or L=='\t'or L=='\n'or L=='\r'then _()elseif L=='-'and
E(1)=='-'then _()_()local U,C=O()
if not C then while E()~='\n'and E()~=''do _()end end else break end end;local N=x;local S=z;local H=":"..x..":"..z..":> "local R=E()local D=nil
if R==
''then D={Type='Eof'}elseif l[R]or d[R]or R=='_'then local L=j
repeat _()R=E()until not(
l[R]or d[R]or u[R]or R=='_')local U=g:sub(L,j-1)if f[U]then D={Type='Keyword',Data=U}else
D={Type='Ident',Data=U}end elseif
u[R]or(E()=='.'and u[E(1)])then local L=j
if R=='0'and E(1)=='x'then _()_()while c[E()]do _()end;if T('Pp')then T('+-')while
u[E()]do _()end end else while u[E()]do _()end;if T('.')then
while u[E()]do _()end end
if T('Ee')then T('+-')if not u[E()]then
A("Expected exponent")end;repeat _()until not u[E()]end;local U=E():lower()if(U>='a'and U<='z')or U=='_'then
A("Invalid number format")end end;D={Type='Number',Data=g:sub(L,j-1)}elseif R=='\''or R=='\"'then
local L=j;local U=_()local C=j
while true do local R=_()if R=='\\'then _()elseif R==U then break elseif R==''or R=='\n'then
A("Unfinished string near <eof>")end end;local M=g:sub(C,j-2)local F=g:sub(L,j-1)
D={Type='String',Data=F,Constant=M}elseif R=='['then local L,U=O()if U then D={Type='String',Data=U,Constant=L}else _()
D={Type='Symbol',Data='['}end elseif T('>=<')then if T('=')then
D={Type='Symbol',Data=R..'='}else D={Type='Symbol',Data=R}end elseif T('~')then if
T('=')then D={Type='Symbol',Data='~='}else
A("Unexpected symbol `~` in source.")end elseif T('.')then if T('.')then if T('.')then
D={Type='Symbol',Data='...'}else D={Type='Symbol',Data='..'}end else
D={Type='Symbol',Data='.'}end elseif T(':')then if T(':')then
D={Type='Symbol',Data='::'}else D={Type='Symbol',Data=':'}end elseif m[R]then _()
D={Type='Symbol',Data=R}else local L,U=O()
if L then D={Type='String',Data=U,Constant=L}else A("Unexpected Symbol `"..
R.."` in source.")end end;D.line=N;D.char=S;k[#k+1]=D;if D.Type=='Eof'then break end end end;local q=setmetatable({tokens=k,pointer=1},{__index=p})
return q end
local function b(g)local function k(O)error(O,0)end;local q,j,x
local function z()if not g:ConsumeSymbol('(')then
k("`(` expected.")end
while not g:ConsumeSymbol(')')do
if g:Is('Ident')then
g:Get()
if not g:ConsumeSymbol(',')then if g:ConsumeSymbol(')')then break else
k("`)` expected.")end end elseif g:ConsumeSymbol('...')then if not g:ConsumeSymbol(')')then
k("`...` must be the last argument of a function.")end;break else
k("Argument name or `...` expected")end end;j()if not g:ConsumeKeyword('end')then
k("`end` expected after function body")end end
local function _()
if g:ConsumeSymbol('(')then q()if not g:ConsumeSymbol(')')then
k("`)` Expected.")end;return{AstType="Paren"}elseif g:Is('Ident')then g:Get()else
k("primary expression expected")end end
function ParseSuffixedExpr(O)local I=_()or{AstType=""}
while true do local N={}
if g:ConsumeSymbol('.')or
g:ConsumeSymbol(':')then
if not g:Is('Ident')then k("<Ident> expected.")end;g:Get()I={AstType='MemberExpr'}elseif
not O and g:ConsumeSymbol('[')then q()
if not g:ConsumeSymbol(']')then k("`]` expected.")end;I={AstType='IndexExpr'}elseif not O and g:ConsumeSymbol('(')then while not
g:ConsumeSymbol(')')do q()
if not g:ConsumeSymbol(',')then if g:ConsumeSymbol(')')then break else
k("`)` Expected.")end end end
I={AstType='CallExpr'}elseif not O and g:Is('String')then g:Get()I={AstType='StringCallExpr'}elseif
not O and g:IsSymbol('{')then x()I={AstType='TableCallExpr'}else break end end;return I end
function x()
if g:Is('Number')or g:Is('String')then g:Get()elseif
g:ConsumeKeyword('nil')or g:ConsumeKeyword('false')or g:ConsumeKeyword('true')or g:ConsumeSymbol('...')then elseif
g:ConsumeSymbol('{')then
while true do
if g:ConsumeSymbol('[')then q()if not g:ConsumeSymbol(']')then
k("`]` Expected")end;if not g:ConsumeSymbol('=')then
k("`=` Expected")end;q()elseif g:Is('Ident')then local O=g:Peek(1)
if O.Type=='Symbol'and
O.Data=='='then local I=g:Get()if not g:ConsumeSymbol('=')then
k("`=` Expected")end;q()else q()end elseif g:ConsumeSymbol('}')then break else q()end
if g:ConsumeSymbol(';')or g:ConsumeSymbol(',')then elseif
g:ConsumeSymbol('}')then break else k("`}` or table entry Expected")end end elseif g:ConsumeKeyword('function')then return z()else return ParseSuffixedExpr()end end;local E=8
local T={['+']={6,6},['-']={6,6},['%']={7,7},['/']={7,7},['*']={7,7},['^']={10,9},['..']={5,4},['==']={3,3},['<']={3,3},['<=']={3,3},['~=']={3,3},['>']={3,3},['>=']={3,3},['and']={2,2},['or']={1,1}}
function q(O)O=O or 0
if y[g:Peek().Data]then local I=g:Get().Data;q(E)else x()end
while true do local I=T[g:Peek().Data]if I and I[1]>O then local N={}g:Get()
q(I[2])else break end end end
local function A()
if g:ConsumeKeyword('if')then
repeat q()if not g:ConsumeKeyword('then')then
k("`then` expected.")end;j()until not g:ConsumeKeyword('elseif')if g:ConsumeKeyword('else')then j()end;if
not g:ConsumeKeyword('end')then k("`end` expected.")end elseif
g:ConsumeKeyword('while')then q()
if not g:ConsumeKeyword('do')then return k("`do` expected.")end;j()
if not g:ConsumeKeyword('end')then k("`end` expected.")end elseif g:ConsumeKeyword('do')then j()if not g:ConsumeKeyword('end')then
k("`end` expected.")end elseif g:ConsumeKeyword('for')then if not g:Is('Ident')then
k("<ident> expected.")end;g:Get()
if g:ConsumeSymbol('=')then q()if not
g:ConsumeSymbol(',')then k("`,` Expected")end;q()if
g:ConsumeSymbol(',')then q()end;if not g:ConsumeKeyword('do')then
k("`do` expected")end;j()if not g:ConsumeKeyword('end')then
k("`end` expected")end else
while g:ConsumeSymbol(',')do if not g:Is('Ident')then
k("for variable expected.")end;g:Get(tokenList)end
if not g:ConsumeKeyword('in')then k("`in` expected.")end;q()while g:ConsumeSymbol(',')do q()end;if
not g:ConsumeKeyword('do')then k("`do` expected.")end;j()if not
g:ConsumeKeyword('end')then k("`end` expected.")end end elseif g:ConsumeKeyword('repeat')then j()if not g:ConsumeKeyword('until')then
k("`until` expected.")end;q()elseif g:ConsumeKeyword('function')then if
not g:Is('Ident')then k("Function name expected")end
ParseSuffixedExpr(true)z()elseif g:ConsumeKeyword('local')then
if g:Is('Ident')then g:Get()while g:ConsumeSymbol(',')do
if not
g:Is('Ident')then k("local var name expected")end;g:Get()end
if
g:ConsumeSymbol('=')then repeat q()until not g:ConsumeSymbol(',')end elseif g:ConsumeKeyword('function')then if not g:Is('Ident')then
k("Function name expected")end;g:Get(tokenList)z()else
k("local var or function def expected")end elseif g:ConsumeSymbol('::')then if not g:Is('Ident')then
k('Label name expected')end;g:Get()if not g:ConsumeSymbol('::')then
k("`::` expected")end elseif g:ConsumeKeyword('return')then local O={}local I=g:Peek()
if
I.Type=="Eof"or I.Type~="Keyword"or not w[I.Data]then q()local I=g:Peek()while g:ConsumeSymbol(',')do q()end end elseif g:ConsumeKeyword('break')then elseif g:ConsumeKeyword('goto')then if not g:Is('Ident')then
k("Label expected")end;g:Get(tokenList)else
local O=ParseSuffixedExpr()
if g:IsSymbol(',')or g:IsSymbol('=')then if O.AstType=="Paren"then
k("Can not assign to parenthesized expression, is not an lvalue")end;while g:ConsumeSymbol(',')do
ParseSuffixedExpr()end;if not g:ConsumeSymbol('=')then
k("`=` Expected.")end;q()while g:ConsumeSymbol(',')do q()end elseif
O.AstType=='CallExpr'or O.AstType=='TableCallExpr'or O.AstType==
'StringCallExpr'then else
k("Assignment Statement Expected")end end;g:ConsumeSymbol(';')end;function j()
while not w[g:Peek().Data]and not g:IsEof()do A()end end;return j()end;return{lex=v,parse=b}end
a["bsrocks.lib.dump"]=function(...)
local n={["and"]=true,["break"]=true,["do"]=true,["else"]=true,["elseif"]=true,["end"]=true,["false"]=true,["for"]=true,["function"]=true,["if"]=true,["in"]=true,["local"]=true,["nil"]=true,["not"]=true,["or"]=true,["repeat"]=true,["return"]=true,["then"]=true,["true"]=true,["until"]=true,["while"]=true}
local function s(r,d,l,u)local c=type(r)
if c=="table"and not d[r]then d[r]=true
if next(r)==nil then if u then return"()"else
return"{}"end else local m=false;local f=u or#r;local w=0
for j,x in pairs(r)do
if type(j)=="table"or
type(x)=="table"then m=true;break elseif
type(j)=="number"and j>=1 and j<=f and j%1 ==0 then w=w+#tostring(x)+2 else
w=w+#
tostring(x)+#tostring(j)+2 end;if w>30 then m=true;break end end;local y,p,v="",", ",""if m then y="\n"p=",\n"v=l.." "end;local b,g={
(u and"("or"{")..y},1;local k={}local q=true
for j=1,f do k[j]=true;g=g+1;local x=v..
s(r[j],d,v)if not q then x=p..x else q=false end;b[g]=x end
for j,x in pairs(r)do
if not k[j]then local z
if type(j)=="string"and not n[j]and
string.match(j,"^[%a_][%a%d_]*$")then z=j.." = "..s(x,d,v)else z="["..
s(j,d,v).."] = "..s(x,d,v)end;z=v..z;if not q then z=p..z else q=false end;g=g+1;b[g]=z end end;g=g+1;b[g]=y..l.. (u and")"or"}")return
table.concat(b)end elseif c=="string"then return
(string.format("%q",r):gsub("\\\n","\\n"))else return tostring(r)end end;local function h(r,d)return s(r,{},"",d)end;return h end
a["bsrocks.commands.repl"]=function(...)local n=i"bsrocks.env"local s=i"bsrocks.lib.dump"
local h=i"bsrocks.lib.parse"
local function r(...)local l=true;local n=n()local u=n._G
u.exit=setmetatable({},{__tostring=function()return"Call exit() to exit"end,__call=function()
l=false end})u._noTail=function(...)return...end;u.arg={[0]="repl",...}local c={}
u.Out=c;local m,f,w=colours.green,colours.cyan,term.getTextColour()
local y,p=colours.lightGrey,colours.lightBlue;if not term.isColour()then m=colours.white;f=colours.white
y=colours.white;p=colours.white end;local v=nil
if not
settings or settings.get("lua.autocomplete")then
v=function(E)
local T=E:find("[a-zA-Z0-9_%.]+$")if T then E=E:sub(T)end
if#E>0 then return textutils.complete(E,u)end end end;local b={}local g=1
local function k(E,T)u._=E;u['_'..g]=E;c[g]=E;term.setTextColour(f)write(
"Out["..g.."]: ")term.setTextColour(w)
if
type(E)=="table"then local A=getmetatable(E)if type(A)=="table"and type(A.__tostring)==
"function"then print(tostring(E))else
print(s(E,T))end else print(s(E))end end
local function q(E,T,...)
if T then local A=select('#',...)
if A==0 then if E then k(nil)end elseif A==1 then k(...)else k({...},A)end else printError(...)end end;local function j(E,T,A,O)local I=E[T]term.setTextColour(y)print(" "..I)
term.setTextColour(p)print((" "):rep(A).."^ ")
printError(" "..O)end
local function r(E,T)
local A=table.concat(E,"\n")local O=false;local I,N=load(A,"lua","t",u)
local S,H=load("return "..A,"lua","t",u)
if not I then
if S then
I=load("return _noTail("..A..")","lua","t",u)O=true else local R,D=pcall(h.lex,A)
if not R then
local U,C,M,F=D:match("(%d+):(%d+):([01]):(.+)")
if U then if U==#E and C>#E[U]and M==1 then return false else
j(E,tonumber(U),tonumber(C),F)return true end else printError(D)return
true end end;local R,L=pcall(h.parse,D)if not R then
if not T and D.pointer>=#D.tokens then return
false else local U=D.tokens[D.pointer]j(E,U.line,U.char,L)return true end end end elseif S then
I=load("return _noTail("..A..")","lua","t",u)end;if I then q(O,pcall(I))g=g+1 else printError(N)end
return true end;local x={}local z="In ["..g.."]: "local _=false
while l do
term.setTextColour(m)write(z)term.setTextColour(w)local E=read(nil,b,v)
if not E then break end
if#E:gsub("%s","")>0 then for T=#b,1,-1 do
if b[T]==E then table.remove(b,T)break end end;b[#b+1]=E;x[#x+1]=E;_=false;if r(x)then x={}z="In ["..g..
"]: "else
z=(" "):rep(#tostring(g)+3).."... "end else r(x,true)x={}_=false
z="In ["..g.."]: "end end;for E,T in pairs(n.cleanup)do T()end end
local d=[[
This is almost identical to the built in Lua program with some simple differences.
Scripts are run in an environment similar to the exec command.
The result of the previous outputs are also stored in variables of the form _idx (the last result is also stored in _). For example: if Out[1] = 123 then _1 = 123 and _ = 123
]]
return{name="repl",help="Run a Lua repl in an emulated environment",syntax="",description=d,execute=r}end
a["bsrocks.bin.repl"]=function(...)
a['bsrocks.env']=function()
return function()return
{cleanup={},_G=setmetatable({},{__index=_ENV})}end end;return i"bsrocks.commands.repl".execute(...)end;return a["bsrocks.bin.repl"](...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment