-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- local TSM = select(2, ...) ---@type TSM local AST = TSM.Init("Util.CustomStringClasses.AST") ---@class Util.CustomStringClasses.AST local Types = TSM.Include("Util.CustomStringClasses.Types") local Optimizer = TSM.Include("Util.CustomStringClasses.Optimizer") local EnumType = TSM.Include("Util.EnumType") local Money = TSM.Include("Util.Money") local TokenProcessor = TSM.Include("LibTSMClass").DefineClass("TokenProcessor") ---@class TokenProcessor local private = { tokenProcessor = nil, expressionStackTemp = {}, expressionResultTemp = {}, expressionsTemp = {}, dependencyIteratorTemp = {}, } local AST_ACTION = EnumType.New("AST_ACTION", { START_EXPRESSION = EnumType.CreateValue(), END_EXPRESSION = EnumType.CreateValue(), START_FUNCTION = EnumType.CreateValue(), END_FUNCTION = EnumType.CreateValue(), ADD_NODE = EnumType.CreateValue(), NEGATE_NEXT = EnumType.CreateValue(), }) local AST_IGNORE_TOKEN = { [Types.TOKEN.WHITESPACE] = true, [Types.TOKEN.NEWLINE] = true, [Types.TOKEN.COLORCODE] = true, } local INTERNAL_AST_NODE = EnumType.New("INTERNAL_AST_NODE", { ROOT_EXPRESSION = EnumType.CreateValue(), EXPRESSION = EnumType.CreateValue(), OPERATOR = EnumType.CreateValue(), }) -- ============================================================================ -- Module Methods -- ============================================================================ ---Generates an AST from a list of tokens. ---@param tokenList NamedTupleList The list of tokens ---@param tree Tree The empty tree to store the AST in ---@return boolean # Whether or not generating the AST was successful ---@return EnumTypeValue|nil # The error type ---@return number|nil # The error token index function AST.Generate(tokenList, tree) -- Generate the AST private.tokenProcessor = private.tokenProcessor or TokenProcessor() local success, errType, errTokenIndex = private.tokenProcessor:Execute(tokenList, tree) if not success then return false, errType, errTokenIndex end -- Optimize the AST success, errType, errTokenIndex = Optimizer.Execute(tree) if not success then return false, errType, errTokenIndex end return true end ---Iterates over the variable dependencies of the custom string. ---@param tree Tree The empty tree to store the AST in ---@return fun(): number, string, string?, string? @An iterator with fields: `index`, `source`, `itemArg`, `extraArg` function AST.DependencyIterator(tree) assert(not next(private.dependencyIteratorTemp)) local context = private.dependencyIteratorTemp context.tree = tree for node in tree:DepthFirstIterator() do local nodeType = tree:GetData(node, "type") local parentNode = context.tree:GetParent(node) local parentNodeValue = parentNode and context.tree:GetData(parentNode, "value") or nil if nodeType == Types.NODE.VARIABLE and parentNodeValue ~= "convert" then tinsert(context, node) elseif nodeType == Types.NODE.FUNCTION and tree:GetData(node, "value") == "convert" then tinsert(context, node) end end return private.DependencyIteratorHelper, context, 0 end ---Dumps an AST for debugging purposes. ---@param tree Tree The empty tree to store the AST in function AST.Dump(tree) for node in tree:DepthFirstIterator() do local value = tree:GetData(node, "value") value = value ~= "" and value or "?" local depth = tree:GetDepth(node) print(strrep(" ", depth)..value) end end -- ============================================================================ -- TokenProcessor Class -- ============================================================================ function TokenProcessor:__init() self._tokenList = nil self._tree = nil self._index = 0 self._errType = nil self._errTokenIndex = nil self._stack = {} end function TokenProcessor:Execute(tokenList, tree) assert(not self._tokenList and not self._tree and not self._errType and not self._errTokenIndex and not next(self._stack)) self._tokenList = tokenList self._tree = tree self._index = 0 -- Generate the initial AST from the tokens if not self:_ProcessAction() then local errType, errTokenIndex = self._errType, self._errTokenIndex assert(errType and errTokenIndex) self._errType = nil self._errTokenIndex = nil self._tree = nil self._tokenList = nil wipe(self._stack) return false, errType, errTokenIndex end local nextActionResult, nextAction = self:_GetNextAction() assert(nextActionResult and nextAction == nil) -- Remove all expressions and turn operators into functions self:_RemoveExpressions() -- Validate that there are no internal nodes left in the tree and that the number of arguments for functions is correct local isValid = true for node in tree:DepthFirstIterator() do local nodeType = tree:GetData(node, "type") assert(nodeType == Types.NODE.CONSTANT or nodeType == Types.NODE.FUNCTION or nodeType == Types.NODE.VARIABLE) if isValid and nodeType == Types.NODE.FUNCTION then local value = tree:GetData(node, "value") local numChildren = tree:GetNumChildren(node) local info = Types.FUNCTION_INFO[value] if not info and numChildren ~= 1 then -- This is a price source being used as a function which must have exactly 1 argument self:_HandleErrorForNode(Types.ERROR.INVALID_NUM_ARGS, node) isValid = false elseif info and (numChildren < info.minArgs or numChildren > info.maxArgs) then self:_HandleErrorForNode(Types.ERROR.INVALID_NUM_ARGS, node) isValid = false elseif value == "convert" then -- Extra validation for convert() local child1Node, child2Node = tree:GetChildren(node) if tree:GetData(child1Node, "type") ~= Types.NODE.VARIABLE then self:_HandleErrorForNode(Types.ERROR.INVALID_CONVERT_ARG, child1Node) -- First argument is not a variable isValid = false elseif Types.IsItemParam(tree:GetData(child1Node, "value")) then -- First argument is an item self:_HandleErrorForNode(Types.ERROR.INVALID_CONVERT_ARG, child1Node) isValid = false elseif tree:GetData(child1Node, "value") == "matprice" then -- Can't use "matprice" as a source for convert() self:_HandleErrorForNode(Types.ERROR.INVALID_CONVERT_ARG, child1Node) isValid = false elseif child2Node and tree:GetData(child2Node, "type") ~= Types.NODE.VARIABLE then -- Second argument is not a variable self:_HandleErrorForNode(Types.ERROR.INVALID_CONVERT_ARG, child2Node) isValid = false elseif child2Node and not Types.IsItemParam(tree:GetData(child2Node, "value")) then -- Second argument is not an item self:_HandleErrorForNode(Types.ERROR.INVALID_CONVERT_ARG, child2Node) isValid = false end end elseif isValid and nodeType == Types.NODE.VARIABLE and Types.IsItemParam(tree:GetData(node, "value")) and not self._tree:GetParent(node) then -- Item parameter without a parent self:_HandleErrorForNode(Types.ERROR.NO_ITEM_PARAM_PARENT, node) isValid = false elseif isValid and nodeType == Types.NODE.VARIABLE and tree:GetData(node, "value") == "convert" then self:_HandleErrorForNode(Types.ERROR.INVALID_NUM_ARGS, node) isValid = false end end self._tree = nil self._tokenList = nil assert(not next(self._stack)) if isValid then assert(not self._errType) return true else local errType, errTokenIndex = self._errType, self._errTokenIndex assert(errType and errTokenIndex) self._errType = nil self._errTokenIndex = nil return false, errType, errTokenIndex end end function TokenProcessor:_ProcessAction(parent, action, tokenIndex) if not action then local success success, action, tokenIndex = self:_GetNextAction() if not success then return false end end assert(action == AST_ACTION.START_EXPRESSION or action == AST_ACTION.START_FUNCTION) local isExpression = action == AST_ACTION.START_EXPRESSION local nodeType = isExpression and INTERNAL_AST_NODE.EXPRESSION or Types.NODE.FUNCTION local nodeValue = tokenIndex ~= -1 and self._tokenList:GetRowField(tokenIndex, "str") or "" local node = self._tree:Insert(parent, nodeType, nodeValue, tokenIndex) local negateExpressionNode = nil while true do local success success, action, tokenIndex = self:_GetNextAction() if not success then return false end if (isExpression and action == AST_ACTION.END_EXPRESSION) or (not isExpression and action == AST_ACTION.END_FUNCTION) then assert(not negateExpressionNode) break elseif action == AST_ACTION.ADD_NODE then local tokenType, tokenStr = self._tokenList:GetRow(tokenIndex) local childType, childValue = nil, nil if tokenType == Types.TOKEN.MATH_OPERATOR then childType = INTERNAL_AST_NODE.OPERATOR childValue = tokenStr elseif tokenType == Types.TOKEN.MONEY then childType = Types.NODE.CONSTANT local value = Money.FromString(tokenStr) assert(value) childValue = value elseif tokenType == Types.TOKEN.NUMBER then childType = Types.NODE.CONSTANT local value = tonumber(tokenStr) assert(value) childValue = value elseif tokenType == Types.TOKEN.IDENTIFIER then childType = Types.NODE.VARIABLE childValue = tokenStr else error("Invalid token type: "..tostring(tokenType)) end self._tree:Insert(negateExpressionNode or node, childType, childValue, tokenIndex) if negateExpressionNode then if not self:_ProcessExpression(negateExpressionNode) then return false end negateExpressionNode = nil end elseif action == AST_ACTION.NEGATE_NEXT then negateExpressionNode = self._tree:Insert(node, INTERNAL_AST_NODE.EXPRESSION, "", -1) self._tree:Insert(negateExpressionNode, Types.NODE.CONSTANT, 0, -1) self._tree:Insert(negateExpressionNode, INTERNAL_AST_NODE.OPERATOR, "-", -1) elseif action == AST_ACTION.START_FUNCTION or action == AST_ACTION.START_EXPRESSION then if not self:_ProcessAction(negateExpressionNode or node, action, tokenIndex) then return false end if negateExpressionNode then if not self:_ProcessExpression(negateExpressionNode) then return false end negateExpressionNode = nil end else error("Invalid action: "..tostring(action)) end end if isExpression then return self:_ProcessExpression(node) else return true end end function TokenProcessor:_GetNextAction() while true do if self._index == 0 then -- Start our wrapping expression self._index = self._index + 1 assert(self:_StackLen() == 0) self:_StackPush(-1, INTERNAL_AST_NODE.ROOT_EXPRESSION) return true, AST_ACTION.START_EXPRESSION, -1 elseif self._stack.queuedAction then -- We have an extra action queued from the current token, so return that local index = self._index self._index = self._index + 1 local action = self._stack.queuedAction self._stack.queuedAction = nil return true, action, index elseif self._index <= self._tokenList:GetNumRows() then -- Get the next action based on the token local index = self._index local token = self._tokenList:GetRowField(self._index, "token") local isValid, action, queueAction = self:_GetTokenAction(token) if not isValid then return false end if queueAction then self._stack.queuedAction = queueAction else self._index = self._index + 1 end if action then return true, action, index end elseif self._index == self._tokenList:GetNumRows() + 1 then -- End our wrapping expression self._index = self._index + 1 if self:_StackLen() > 1 then local tokenIndex = self:_StackPop() self:_HandleErrorForToken(Types.ERROR.UNBALANCED_PARENS, tokenIndex) return false end assert(select(2, self:_StackPop()) == INTERNAL_AST_NODE.ROOT_EXPRESSION) assert(self:_StackLen() == 0) return true, AST_ACTION.END_EXPRESSION, -1 else -- We're done assert(self:_StackLen() == 0) return true, nil, nil, nil end end end function TokenProcessor:_StackPush(tokenIndex, node) tinsert(self._stack, tokenIndex) tinsert(self._stack, node) end function TokenProcessor:_StackPeekLastTwoNodes() local len = #self._stack return self._stack[len], self._stack[len-2] end function TokenProcessor:_StackPop() assert(#self._stack > 0) local node = tremove(self._stack) local tokenIndex = tremove(self._stack) return tokenIndex, node end function TokenProcessor:_StackLen() return #self._stack / 2 end function TokenProcessor:_GetTokenAction(token) local parentNode, parentParentNode = self:_StackPeekLastTwoNodes() if token == Types.TOKEN.NUMBER or token == Types.TOKEN.MONEY or token == Types.TOKEN.IDENTIFIER or token == Types.TOKEN.MATH_OPERATOR then return true, AST_ACTION.ADD_NODE elseif token == Types.TOKEN.FUNCTION then assert(parentNode == INTERNAL_AST_NODE.EXPRESSION or parentNode == INTERNAL_AST_NODE.ROOT_EXPRESSION) self:_StackPush(self._index, Types.NODE.FUNCTION) return true, AST_ACTION.START_FUNCTION elseif token == Types.TOKEN.LEFT_PAREN then self:_StackPush(self._index, INTERNAL_AST_NODE.EXPRESSION) return true, AST_ACTION.START_EXPRESSION elseif token == Types.TOKEN.COMMA then if parentNode ~= INTERNAL_AST_NODE.EXPRESSION or parentParentNode ~= Types.NODE.FUNCTION then self:_HandleErrorForToken(Types.ERROR.INVALID_TOKEN, self._index) return false end return true, AST_ACTION.END_EXPRESSION, AST_ACTION.START_EXPRESSION elseif token == Types.TOKEN.RIGHT_PAREN then local _, stackNode = self:_StackPop() if stackNode == INTERNAL_AST_NODE.ROOT_EXPRESSION then self:_HandleErrorForToken(Types.ERROR.UNBALANCED_PARENS, self._index) return false end assert(stackNode == INTERNAL_AST_NODE.EXPRESSION) if parentParentNode == Types.NODE.FUNCTION then self:_StackPop() return true, AST_ACTION.END_EXPRESSION, AST_ACTION.END_FUNCTION else return true, AST_ACTION.END_EXPRESSION end elseif token == Types.TOKEN.NEGATIVE_OPERATOR then return true, AST_ACTION.NEGATE_NEXT elseif token == Types.TOKEN.UNKNOWN then self:_HandleErrorForToken(Types.ERROR.INVALID_TOKEN, self._index) return false else assert(AST_IGNORE_TOKEN[token]) return true end end function TokenProcessor:_ProcessExpression(parent) -- Get the children assert(not next(private.expressionResultTemp)) local result = private.expressionResultTemp for child in self._tree:ChildrenIterator(parent) do tinsert(result, child) end if #result == 1 then -- Expression with a single child, so nothing to process wipe(result) return true elseif #result == 0 then -- Empty expression local parentParent = self._tree:GetParent(parent) if parentParent and self._tree:GetData(parentParent, "type") == Types.NODE.FUNCTION then self:_HandleErrorForNode(Types.ERROR.INVALID_NUM_ARGS, parentParent) else self:_HandleErrorForNode(Types.ERROR.INVALID_TOKEN, parent) end return false end -- Convert the list of children from infix notation to postfix assert(not next(private.expressionStackTemp)) local stack = private.expressionStackTemp local numNodes = #result for i = numNodes, 1, -1 do local child = result[i] local childType = self._tree:GetData(child, "type") if childType == Types.NODE.FUNCTION or childType == INTERNAL_AST_NODE.EXPRESSION or childType == Types.NODE.CONSTANT or childType == Types.NODE.VARIABLE then tinsert(result, child) elseif childType == INTERNAL_AST_NODE.OPERATOR then local childPriority = self:_GetOperatorPriority(child, "value") while #stack > 0 and childPriority < self:_GetOperatorPriority(stack[#stack]) do tinsert(result, tremove(stack)) end tinsert(stack, child) else error("Unknown type: "..tostring(childType)) end end while #stack > 0 do tinsert(result, tremove(stack)) end -- We use the end of the `result` list to store the result - shift the result down assert(#result == numNodes * 2) for i = numNodes, 1, -1 do result[i] = tremove(result) end assert(#result == numNodes) -- Convert operators into functions and convert them into a tree if not self:_ProcessOperator(result, parent, tremove(result)) then wipe(result) return false end if #result > 0 then self:_HandleErrorForNode(Types.ERROR.INVALID_TOKEN, result[1]) wipe(result) return false end return true end function TokenProcessor:_GetOperatorPriority(node) local str = self._tree:GetData(node, "value") if str == "-" or str == "+" then return 1 elseif str == "*" or str == "/" or str == "%" then return 2 elseif str == "^" then return 3 else error("Invalid operator: "..tostring(str)) end end function TokenProcessor:_ProcessOperator(nodes, parent, opNode) if self._tree:GetData(opNode, "type") ~= INTERNAL_AST_NODE.OPERATOR then self:_HandleErrorForNode(Types.ERROR.INVALID_TOKEN, opNode) return false end self._tree:SetData(opNode, "type", Types.NODE.FUNCTION) self._tree:SetChildren(parent, opNode) if self._tree:GetData(opNode, "value") == "-" and #nodes == 1 then tinsert(nodes, self._tree:Insert(opNode, Types.NODE.CONSTANT, 0, -1)) end local leftNode = tremove(nodes) if self._tree:GetData(leftNode, "type") == INTERNAL_AST_NODE.OPERATOR then if not self:_ProcessOperator(nodes, opNode, leftNode) then return false end end local rightNode = tremove(nodes) if not rightNode then self:_HandleErrorForNode(Types.ERROR.INVALID_TOKEN, opNode) return false end if self._tree:GetData(rightNode, "type") == INTERNAL_AST_NODE.OPERATOR then if not self:_ProcessOperator(nodes, opNode, rightNode) then return false end end self._tree:SetChildren(opNode, leftNode, rightNode) return true end function TokenProcessor:_RemoveExpressions() local root = self._tree:GetRoot() assert(self._tree:GetData(root, "type") == INTERNAL_AST_NODE.EXPRESSION) -- Get the single child of the root node and promote it to be the new root assert(self._tree:GetNumChildren(root) == 1) local newRoot = self._tree:GetChildren(root) self._tree:MoveUp(newRoot) assert(not next(private.expressionsTemp)) local expressions = private.expressionsTemp for node in self._tree:DepthFirstIterator() do if self._tree:GetData(node, "type") == INTERNAL_AST_NODE.EXPRESSION then assert(self._tree:GetNumChildren(node) == 1) tinsert(expressions, self._tree:GetChildren(node)) end end for _, child in ipairs(expressions) do self._tree:MoveUp(child) end wipe(expressions) end function TokenProcessor:_HandleErrorForNode(errType, node) self:_HandleErrorForToken(errType, self._tree:GetData(node, "tokenIndex")) end function TokenProcessor:_HandleErrorForToken(errType, tokenIndex) assert(tokenIndex and tokenIndex ~= -1) assert(errType) assert(not self._errType and not self._errTokenIndex) self._errType = errType self._errTokenIndex = tokenIndex end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.DependencyIteratorHelper(context, index) index = index + 1 local node = context[index] if not node then wipe(context) return nil end local nodeType = context.tree:GetData(node, "type") local nodeValue = context.tree:GetData(node, "value") if nodeType == Types.NODE.FUNCTION then assert(nodeValue == "convert") local sourceNode, itemNode = context.tree:GetChildren(node) assert(sourceNode and context.tree:GetData(sourceNode, "type") == Types.NODE.VARIABLE) local itemValue = itemNode and context.tree:GetData(itemNode, "value") assert(not itemValue or (context.tree:GetData(itemNode, "type") == Types.NODE.VARIABLE and Types.IsItemParam(itemValue))) return index, nodeValue, itemValue, context.tree:GetData(sourceNode, "value") else assert(nodeType == Types.NODE.VARIABLE) local parentNode = context.tree:GetParent(node) local parentNodeValue = parentNode and context.tree:GetData(parentNode, "value") or nil assert(not parentNode or context.tree:GetData(parentNode, "type") == Types.NODE.FUNCTION) if Types.IsItemParam(nodeValue) then assert(parentNodeValue) return index, parentNodeValue, nodeValue else return index, nodeValue end end end