diff --git a/res/modules/bitwise/compiler.lua b/res/modules/bitwise/compiler.lua new file mode 100644 index 00000000..ce56c056 --- /dev/null +++ b/res/modules/bitwise/compiler.lua @@ -0,0 +1,113 @@ +local util = require "core:bitwise/util" + +local tokenizer = require "core:bitwise/tokenizer" +local parser = require "core:bitwise/parser" + +local types = tokenizer.types + +local operatorToFunctionName = +{ + [types.lshift] = "bit.lshift", + [types.rshift] = "bit.rshift", + [types.bnot] = "bit.bnot", + [types.band] = "bit.band", + [types.bxor] = "bit.bxor", + [types.bor] = "bit.bor" +} + +local function compile(str, args, asFunction) + if asFunction == nil then + asFunction = true + end + + local tokens = parser(str) + + local stack = { } + + local ids = { } + + for _, token in ipairs(tokens) do + if table.has(tokenizer.numTypes, token.type) or token.type == types.id then + if token.type == tokenizer.types.binNum then + util.stack.push(stack, tonumber(token.value, 2)) + elseif token.type == tokenizer.types.hexNum then + util.stack.push(stack, "0x"..token.value) + else + util.stack.push(stack, token.value) + + if token.type == types.id then + local add = true + + for i = 1, #ids do + if ids[i].value == token.value then + add = false + break + end + end + + if add then table.insert(ids, token) end + end + end + else + local fun = operatorToFunctionName[token.type] + + local a = util.stack.pop(stack) + local b = token.type ~= types.bnot and util.stack.pop(stack) + + if not b then + util.stack.push(stack, fun..'('..a..')') + else + util.stack.push(stack, fun..'('..a..','..b..')') + end + end + end + + local strArgs = "" + + if args then + for _, id in ipairs(ids) do + if not table.has(args, id.value) then + util.throw(id.column, "undefined identifier") + end + end + else + args = { } + + table.sort(ids, function(a, b) return a.value:upper() < b.value:upper() end) + + for _, id in ipairs(ids) do + table.insert(args, id.value) + end + end + + for i = 1, #args do + local str = args[i] + + for j = 1, #str do + local char = str:sub(j,j) + + if + not string.find(tokenizer.idChars, char) or + (j == 1 and string.find(tokenizer.startingDigits, char)) + then error("invalid argument name ("..i..")") end + end + + strArgs = strArgs..str + + if i ~= #args then + strArgs = strArgs..',' + end + end + + local code = stack[1] + + code = "function("..strArgs..") return "..code.." end" + + if asFunction then + return load("return "..code)() + else + return code + end +end + +return compile \ No newline at end of file diff --git a/res/modules/bitwise/executor.lua b/res/modules/bitwise/executor.lua new file mode 100644 index 00000000..40e2afad --- /dev/null +++ b/res/modules/bitwise/executor.lua @@ -0,0 +1,61 @@ +local compiler = require "core:bitwise/compiler" + +local cache = { } + +local cacheLength = 512 + +local function hashOfArgs(args) + local str = "" + + for i = 1, #args do + str = str..args[i] + + if i ~= #args then + str = str..';' + end + end + + return str +end + +local function execute(str, args, ...) + local hasArgs = args and type(args) == 'table' + + local expArgs + + if hasArgs then + expArgs = args + else + expArgs = nil + end + + local argsHash = hasArgs and hashOfArgs(args) or "" + + local func + + if (#str + #argsHash) <= cacheLength then + if cache[argsHash] then + local fun = cache[argsHash][str] + + if fun then return fun end + end + + local comp = compiler(str, expArgs) + + cache[argsHash] = { } + + cache[argsHash][str] = comp + + func = comp + else + func = compiler(str, expArgs) + end + + if hasArgs or not args then + return func(...) + else + return func(args, ...) + end +end + +return execute \ No newline at end of file diff --git a/res/modules/bitwise/parser.lua b/res/modules/bitwise/parser.lua new file mode 100644 index 00000000..8971903e --- /dev/null +++ b/res/modules/bitwise/parser.lua @@ -0,0 +1,61 @@ +local util = require "core:bitwise/util" + +local tokenizer = require "core:bitwise/tokenizer" + +local types = tokenizer.types + +local priorities = +{ + [types.bnot] = 4, + [types.lshift] = 3, + [types.rshift] = 3, + [types.band] = 2, + [types.bxor] = 1, + [types.bor] = 0 +} + +local function parse(str) + local stack = { } + local output = { } + + for _, token in ipairs(tokenizer.get_tokens(str)) do + if table.has(tokenizer.numTypes, token.type) or token.type == types.id then + table.insert(output, token) + elseif table.has(tokenizer.operatorTypes, token.type) then + local firstOp = util.stack.top(stack) + + if firstOp and table.has(tokenizer.operatorTypes, firstOp.type) and priorities[firstOp.type] >= priorities[token.type] then + table.insert(output, util.stack.pop(stack)) + end + + util.stack.push(stack, token) + elseif token.type == types.openingBracket then + util.stack.push(stack, token) + elseif token.type == types.closingBracket then + while true do + local topToken = util.stack.top(stack) + + if not topToken then util.throw(token.column, "unexpected closing bracket") + elseif topToken.type == types.openingBracket then break end + + table.insert(output, util.stack.pop(stack)) + end + + util.stack.pop(stack) + end + end + + while #stack > 0 do + local token = util.stack.pop(stack) + + if token.type == types.openingBracket then + util.throw(token.column, "unclosed bracket") + end + + table.insert(output, token) + end + + return output +end + +return parse \ No newline at end of file diff --git a/res/modules/bitwise/tokenizer.lua b/res/modules/bitwise/tokenizer.lua new file mode 100644 index 00000000..0f30f9ea --- /dev/null +++ b/res/modules/bitwise/tokenizer.lua @@ -0,0 +1,228 @@ +local util = require "core:bitwise/util" + +local operators = "><|&~^()" +local digits = "0123456789ABCDEF" +local startingDigits = "0123456789" +local idChars = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789_" +local spaces = " \t" + +local function esc(x) + return (x:gsub('%%', '%%%%') + :gsub('^%^', '%%^') + :gsub('%$$', '%%$') + :gsub('%(', '%%(') + :gsub('%)', '%%)') + :gsub('%.', '%%.') + :gsub('%[', '%%[') + :gsub('%]', '%%]') + :gsub('%*', '%%*') + :gsub('%+', '%%+') + :gsub('%-', '%%-') + :gsub('%?', '%%?')) +end + +local decNum = "DECIMAL" +local hexNum = "HEXADECIMAL" +local binNum = "BINARY" + +local rshift = "RIGHT SHIFT" +local lshift = "LEFT SHIFT" +local bor = "OR" +local band = "AND" +local bxor = "XOR" +local bnot = "NOT" +local id = "ID" +local openingBracket = "OPENING BRACKET" +local closingBracket = "CLOSING BRACKET" + +local opToType = +{ + ['^'] = bxor, + ['|'] = bor, + ['&'] = band, + ['('] = openingBracket, + [')'] = closingBracket, + ['~'] = bnot +} + +local typeToOp = +{ + [bxor] = '^', + [bor] = '|', + [band] = '&', + [openingBracket] = '(', + [closingBracket] = ')', + [bnot] = '~' +} + +local function contains(str, char) + return string.find(str, esc(char)) ~= nil +end + +local function checkDigitSystem(column, digit, base) + local i = digits:find(digit) + + if not i or i > base then + util.throw(column, "the digit '"..digit.."' does not belong to the "..base.."-based number system") + end +end + +local function isEndOfNumOrId(i, set, next, len) + if i == len or not contains(set, next) then + if i ~= len and not contains(operators, next) and not contains(spaces, next) then + util.throw(i + 1, "operator or space expected") + else + return true + end + else + return false + end +end + +local function tokenize(str) + local tokens = { } + + local buffer = "" + local numType, readingNum, readingId + local numSys + + local i = 1 + + while i <= #str do + local char = str:sub(i, i) + local upChar = string.upper(char) + local next = str:sub(i + 1, i + 1) + + if contains(operators, char) then + local type + + if char == '>' or char == '<' then + if next == char then + type = char == '>' and rshift or lshift + i = i + 1 + else + util.throw(i, "invalid operator") + end + elseif char == '~' then + if contains(idChars, next) then + type = bnot + else + util.throw(i, "number expected") + end + else + type = opToType[char] + end + + table.insert(tokens, { column = i, type = type }) + elseif ((not readingNum and contains(startingDigits, char)) or (readingNum and contains(digits, upChar))) and not readingId then + if not readingNum then + readingNum = true + + if char == '0' and (next == 'x' or next == 'b') then + numType = next == 'x' and hexNum or binNum + numSys = next == 'x' and 16 or 2 + + i = i + 2 + + if not str:sub(i, i) then + util.throw(i, "unexpected end") + end + + char = str:sub(i, i) + upChar = string.upper(char) + next = str:sub(i + 1, i + 1) + else + numType = decNum + numSys = 10 + end + end + + if readingNum then + if numType == hexNum then + char, next = upChar, string.upper(next) + end + + checkDigitSystem(i, char, numSys) + + buffer = buffer..char + + if isEndOfNumOrId(i, digits, next, #str) then + readingNum = false + table.insert(tokens, { column = i, type = numType, value = buffer }) + buffer = "" + end + end + elseif contains(idChars, char) then + if not readingId then + readingId = true + end + + if readingId then + buffer = buffer..char + + if isEndOfNumOrId(i, idChars, next, #str) then + readingId = false + table.insert(tokens, { column = i, type = id, value = buffer }) + buffer = "" + end + end + else + local space + + for j = 1, #spaces do + if spaces:sub(j, j) == char then + space = true + break + end + end + + if not space then + util.throw(i, "undefined token: \""..char.."\"") + end + end + + i = i + 1 + end + + return tokens +end + +local function printTokens(tokens) + for _, token in ipairs(tokens) do + local str = "{ " + + for k, v in pairs(token) do + str = str..k..' = '..v..', ' + end + + print(str:sub(1, #str - 2).." }") + end +end + +return +{ + operators = operators, + digits = digits, + startingDigits = startingDigits, + idChars = idChars, + operatorTypes = { lshift, rshift, bnot, band, bxor, bor }, + numTypes = { decNum, hexNum, binNum }, + types = { + decNum = decNum, + hexNum = hexNum, + binNum = binNum, + rshift = rshift, + lshift = lshift, + bor = bor, + band = band, + bxor = bxor, + bnot = bnot, + id = id, + openingBracket = openingBracket, + closingBracket = closingBracket + }, + opToType = opToType, + typeToOp = typeToOp, + get_tokens = tokenize, + print_tokens = printTokens +} \ No newline at end of file diff --git a/res/modules/bitwise/util.lua b/res/modules/bitwise/util.lua new file mode 100644 index 00000000..41cda5ae --- /dev/null +++ b/res/modules/bitwise/util.lua @@ -0,0 +1,21 @@ +local util = { } + +util.stack = { } + +function util.throw(column, msg) + error("column "..column..": "..msg) +end + +function util.stack.push(stack, value) + table.insert(stack, value) +end + +function util.stack.pop(stack) + return table.remove(stack, #stack) +end + +function util.stack.top(stack) + return stack[#stack] +end + +return util \ No newline at end of file