local tokens = { whitespace = { ["\t"] = true, [" "] = true, ["\n"] = true, [","] = true, }, line = { ["\n"] = true, [","] = true, }, label = { [":"] = true, }, indexVar = { ["%"] = true, }, operator = {}, op = { add = { ["+"] = true, }, subtract = { ["-"] = true, }, multiply = { ["*"] = true, }, modulo = { ["%"] = true, }, power = { ["^"] = true, }, divide = { ["/"] = true, }, }, assign = { ["="] = true, }, comparison = {}, comp = { equals = { ["=="] = true, }, notEquals = { ["~="] = true, }, bigger = { [">"] = true, }, biggerOrEquals = { [">="] = true, }, smaller = { ["<"] = true, }, smallerOrEquals = { ["<="] = true, }, }, logic = {}, logicAnd = { ["and"] = true, ["&&"] = true, }, logicOr = { ["or"] = true, ["||"] = true, }, logicNot = { ["not"] = true, ["!"] = true, }, keyword = {}, ifStatement = { ["if"] = true, }, whileStatement = { ["while"] = true, }, forStatement = { ["for"] = true, }, elseStatement = { ["else"] = true, }, localStatement = { ["local"] = true, }, eof = { ["end"] = true, ["exit"] = true, }, boolean = {}, boolTrue = { ["true"] = true, }, boolFalse = { ["false"] = true, }, ident = { ["_"] = true, }, digits = { }, string = { ["\""] = true, ["'"] = true, }, symbol = { ["("] = true, [")"] = true, }, escape = { ["\\"] = true, } } for c=("a"):byte(), ("z"):byte() do tokens.ident[string.char(c)] = true tokens.ident[string.char(c):upper()] = true end for c=("0"):byte(), ("9"):byte() do tokens.ident[string.char(c)] = true tokens.digits[string.char(c)] = true end local function addTo(root, ...) if not root then error("invalid argument #1: expected table, got nil",2) end local tbls = {...} for i,t in ipairs(tbls) do for k,v in pairs(t) do root[k] = v end end end for k,v in pairs(tokens.comp) do addTo(tokens.comparison, v) end for k,v in pairs(tokens.op) do addTo(tokens.operator, v) end addTo(tokens.logic, tokens.logicAnd, tokens.logicOr, tokens.logicNot) addTo(tokens.keyword, tokens.logic, tokens.ifStatement, tokens.whileStatement, tokens.forStatement, tokens.elseStatement, tokens.localStatement, tokens.eof) addTo(tokens.boolean, tokens.boolTrue, tokens.boolFalse) local statementValues = { ["string"] = true, ["number"] = true, ["variable"] = true, ["arg"] = true, ["value"] = true, } local validVarTypes = { ["function"] = true, ["arg"] = true, ["variable"] = true, ["escape"] = true, } local function lex(text) local char = 1 local function get(p) local tPos = p or char return text:sub(tPos,tPos) end local beginline = true local inStatement = false local isLabel = false local data = {{}} local line = data[1] local posOffset = 0 local function newline() posOffset = char table.insert(data,{}) line = data[#data] end local function addData(txt,type) if not txt then error("no txt",2) end if line[#line] and line[#line].type == type then line[#line].data = line[#line].data..txt line[#line].posLast = line[#line].posLast+#txt else table.insert(line,{data=txt,posFirst=(char-posOffset),posLast=(char-posOffset)+(#txt-1),type=type}) end end local function readWhitespace(acceptNewline) local txt = get() local oldchar = char while tokens.whitespace[txt] and (acceptNewline or not tokens.line[txt]) do char = char + 1 addData(txt,"whitespace") isLabel = false txt = get() end if oldchar == char then return false else return true end end while char <= #text do local txt = get() if tokens.whitespace[txt] and not tokens.line[txt] then addData(txt,"whitespace") isLabel = false elseif tokens.escape[txt] then addData(txt..text:sub(char+1,char+1),"escape") char = char+1 if tokens.whitespace[get(char+1)] then isLabel = false beginline = false end elseif tokens.string[txt] then local bchar = txt local echar = bchar local stype = "string" if isLabel then stype = "label" elseif beginline then stype = "function" end isLabel = false beginline = false addData(txt,stype) char = char+1 while char <= #text do local c = text:sub(char,char) if tokens.escape[c] then addData(c..get(char+1),"escape") char = char+1 if tokens.digits[get()] then char = char+1 for i=1,2 do if tokens.digits[get()] then addData(get(),"escape") char = char+1 else break end end else char = char+1 end elseif tokens.indexVar[c] then local b2,e2 = text:find("^%%[A-Za-z0-9_]+%%", char) if b2 then addData(text:sub(b2,e2), "variable") char = e2 else local b3,e3 = text:find("^%%[0-9]+", char) if not b3 then b3,e3 = text:find("^%%~[0-9]+", char) end if b3 then addData(text:sub(b3,e3), "arg") char = e3 else addData(c, stype) end end char = char+1 elseif c == echar then addData(c, stype) break else if c == "\n" then newline() else addData(c, stype) end char = char+1 end end elseif tokens.indexVar[txt] then if inStatement == 1 and line[#line] and line[#line].type == "whitespace" and line[#line-1] and statementValues[line[#line-1].type] then inStatement = false beginline = true end local b2,e2 = text:find("^%%[A-Za-z0-9_]+%%", char) if b2 then if isLabel and beginline then addData(text:sub(b2,e2), "unidentified") else addData(text:sub(b2,e2), "variable") end char = e2 else local b3,e3 = text:find("^%%[0-9]+", char) if not b3 then b3,e3 = text:find("^%%~[0-9]+", char) end if b3 then if isLabel and beginline then addData(text:sub(b3,e3), "unidentified") else addData(text:sub(b3,e3), "arg") end char = e3 elseif isLabel then addData(txt, "label") elseif beginline then addData(txt, "function") elseif not inStatement then addData(txt, "string") elseif tokens.operator[txt] then addData(txt, "operator") else addData(txt, "unidentified") end end if tokens.whitespace[get(char+1)] then beginline = false end elseif tokens.ident[txt] or (not inStatement and not (tokens.line[txt] or tokens.label[txt] or tokens.assign[txt] or tokens.symbol[txt])) then local b,e if inStatement then b,e = text:find("^[A-Za-z0-9_.]+", char) else b,e = char,char+1 local c = get(e) while e <= #text and not (tokens.line[c] or tokens.label[c] or tokens.assign[c] or tokens.indexVar[c] or tokens.whitespace[c] or tokens.symbol[c] or tokens.escape[c]) do e = e + 1 c = get(e) end e = e - 1 end local token = text:sub(b,e) local ltoken = token:lower() local statementValue = false if inStatement then if line[#line] and line[#line].type == "whitespace" and line[#line-1] and statementValues[line[#line-1].type] then if inStatement == 1 then inStatement = 0 beginline = true end statementValue = true end end char = e if isLabel then addData(token, "label") if tokens.whitespace[get(char+1)] then isLabel = false beginline = false end elseif tokens.localStatement[ltoken] and not inStatement then if not beginline then addData(token, "string") else addData(token, "keyword") end elseif tokens.eof[ltoken] and (inStatement == 0 or not inStatement) then addData(token, "keyword") elseif (tokens.ifStatement[ltoken] or tokens.whileStatement[ltoken]) and (inStatement == 0 or not inStatement) then addData(token, "keyword") inStatement = 1 elseif (tokens.elseStatement[ltoken] or tokens.eof[ltoken]) and (inStatement == 0 or not inStatement) then addData(token, "keyword") elseif (tokens.logicAnd[ltoken] or tokens.logicOr[ltoken]) and statementValue then addData(token, "logic") if inStatement == 0 then inStatement = 1 end elseif tokens.logicNot[ltoken] and inStatement and not statementValue then addData(token, "logic") if inStatement == 0 then inStatement = 1 end elseif tokens.forStatement[ltoken] and (inStatement == 0 or not inStatement) then -- other handling elseif tokens.boolean[ltoken] and inStatement and not statementValue then addData(token, "value") elseif beginline and (inStatement == 0 or not inStatement) then addData(token, "function") if tokens.whitespace[get(char+1)] then beginline = false end elseif tonumber(token) then addData(token, "number") if tokens.whitespace[get(char+1)] then beginline = false end else addData(token, "string") end if inStatement == 0 then inStatement = false end elseif tokens.label[txt] then if char+1 > #text or tokens.whitespace[get(char+1)] then addData(txt, "string") else addData(txt, "label") isLabel = true end elseif tokens.line[txt] then beginline = true inStatement = false if txt ~= "\n" then addData(txt, "symbol") end elseif tokens.symbol[txt] then addData(txt, "symbol") elseif txt:find("%p") then local b,e = text:find("^%p+", char) local token = text:sub(b,e) char = e if inStatement then if tokens.operator[token] then addData(token, "operator") elseif tokens.comparison[token] then addData(token, "comparison") elseif tokens.logic[token] then addData(token, "logic") else addData(token, "string") end else if tokens.assign[token] then local v = line[#line] if v and v.type == "whitespace" then v = line[#line-1] end if v and (validVarTypes[v.type]) then local it = #line-1 while v and (validVarTypes[v.type]) do if v.type == "function" then v.type = "variable" end it = it-1 v = line[it] end addData(token, "assign") inStatement = true elseif beginline then addData(token, "unidentified") else addData(token, "string") end elseif beginline then addData(token, "unidentified") else addData(token, "string") end end elseif txt ~= "\n" then addData(txt, "unidentified") end if txt == "\n" then newline() end char = char+1 end return data end local function parse(data, filename) filename = filename or "latch" if type(data) == "string" then data = lex(data) end local function err(lineNum, txt, txtafter) local err = filename..": "..txt.." on line "..lineNum if txtafter then err = err.." ("..txtafter..")" end error(err, 0) end local function unexpectedToken(line, token, ...) _G.debugtoken = token local exp = {...} local expstr if #exp > 0 then expstr = "expected "..table.concat(exp, ", ") end err(line, "unexpected token '"..tostring(token.data).."' ("..tostring(token.type)..")", expstr) end local function locateEntry(tbl, value) for k,v in pairs(tbl) do if v == value then return true end end return false end local function parseToken(line, i, allowed, stopAt) local token = line[i] local data = {} stopAt = stopAt or {"whitespace"} while true do if locateEntry(allowed, token.type) then if token.type == "number" then table.insert(data, {type="number", data=token.data}) elseif token.type == "operator" then table.insert(data, {type="operator", data=token.data}) elseif token.type == "variable" and token.data:sub(1,1) == "%" then table.insert(data, {type="variable", data=token.data:sub(2,#token.data-1)}) elseif token.type == "variable" then table.insert(data, {type="string", data=token.data}) elseif token.type == "arg" then table.insert(data, {type="argument", data=token.data:sub(2)}) elseif token.type == "label" then if tokens.string[token.data:sub(2,2)] then table.insert(data, {type="label", data=token.data:sub(2):gsub(token.data:sub(2,2),"")}) else table.insert(data, {type="label", data=token.data:sub(2)}) end elseif tokens.string[token.data:sub(1,1)] then table.insert(data, {type="string", data=token.data:gsub(token.data:sub(1,1),"")}) else table.insert(data, {type="string", data=token.data}) end elseif locateEntry(stopAt, token.type) then i = i - 1 break else local expected = {} addTo(expected, stopAt) addTo(expected, allowed) unexpectedToken(ln, token, unpack(expected)) end i = i + 1 token = line[i] if not token then break end end return data, i end local function parseExpression(line, i, whitespaceConcat) -- this is all wrong FUCK, value (x+5) > comparison (x+5 == 8) > expression(x+5 == 8 and %potato%) -- expression structure local token = line[i] local conditions = { { {} -- current condition (value) -- more AND conditions } -- more OR conditions } local condition = conditions[1][1] local isValid = false while true do token = line[i] if not token then break end if token.type == "whitespace" then -- optional token elseif token.type == "logic" and tokens.logicNot[token.data] then -- optional token table.insert(condition, token) isValid = false elseif statementValues[token.type] then -- REQUIRED token, expression is invalid without it table.insert(condition, token) isValid = true i = i + 1 token = line[i] if not token then break end if token.type == "whitespace" then i = i + 1 token = line[i] end if not token then break end if token.type == "logic" and tokens.logicAnd[token.data] then condition = {} table.insert(conditions[#conditions], condition) isValid = false elseif token.type == "logic" and tokens.logicOr[token.data] then condition = {} table.insert(conditions, {}) table.insert(conditions[#conditions], condition) isValid = false elseif token.type == "operator" then table.insert(condition, token) elseif whitespaceConcat then i = i - 1 else i = i - 1 break -- end of statement end else unexpectedToken(i, token, "expression") end i = i + 1 end if not isValid then err(i, "invalid statement") else return conditions, i end end local program = { arguments = {}, env = {}, commands = {}, labels = {}, code = {}, } local scope = program.code for ln,line in ipairs(data) do local token local i = 1 local beginline = true local function nextToken(includeWhitespace) if line[i+1] and (includeWhitespace or line[i+1].type ~= "whitespace") then return line[i+1] elseif line[i+2] then return line[i+2] end end local function nextTokenType(includeWhitespace) local nt = nextToken(includeWhitespace) if nt then return nt.type end end while i <= #line do token = line[i] if beginline then if token.type == "function" or token.type == "variable" or token.type == "arg" then local data data, i = parseToken(line, i, {"function", "variable", "arg", "escape"}, {"whitespace", "symbol", "assign"}) local l if nextTokenType() == "assign" then l = {type="assign", data=data, value={}} while nextTokenType() == "assign" do i = i + 1 end i = i + 1 l.value, i = parseExpression(line, i, true) else l = {type="command", data=data, line=ln, params={}} beginline = false end table.insert(scope, l) elseif token.type == "label" then program.labels[token.data] = {scope=scope, commandNum=#scope+1} elseif token.type ~= "whitespace" then --unexpectedToken(ln, token, "whitespace", "function", "variable", "arg") end else if token.type == "label" then if scope[#scope].type == "command" then local data data, i = parseToken(line, i, {"label", "variable", "arg", "escape"}, {"whitespace", "symbol"}) table.insert(scope[#scope].params, data) else unexpectedToken(ln, token) end elseif token.type == "string" or token.type == "number" then if scope[#scope].type == "command" then local data data, i = parseToken(line, i, {"string", "number", "variable", "arg", "escape"}, {"whitespace", "symbol"}) table.insert(scope[#scope].params, data) end elseif token.type ~= "whitespace" then end end i = i + 1 end end return program end return {lex=lex,parse=parse}