Last active
April 13, 2017 09:38
-
-
Save SquidDev/e204ea9b6032dabf0cba3f57893125ba to your computer and use it in GitHub Desktop.
A fancy Lua REPL (originally from Blue-Shiny-Rocks: http://www.computercraft.info/forums2/index.php?/topic/26032- and https://github.com/SquidDev-CC/Blue-Shiny-Rocks)
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
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"](...) |
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
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