Quantcast

Add OvaleAST module to generate an abstract syntax tree from a script.

Johnny C. Lam [07-13-14 - 11:31]
Add OvaleAST module to generate an abstract syntax tree from a script.

This module implements a recursive descent parser for the Ovale script
language and generates an AST for further manipulation.

There are some ambiguities in the script language:

    * Unary negation versus binary subtraction.
    * '{' and '}' can enclose either a statement group or an expression.

The public methods for OvaleAST are:

    ParseCode(code) to generate an AST from a block of code.
    ParseScript(name) to generate an AST from a named script.
    Unparse(node) to generate a block of code from an AST node.
    Optimize(ast) to make optimization passes through the AST.

Unparse(ParseCode(Code)) should be roughly idempotent in that the original
code and the resulting code should have the same AST representation.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1542 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
Ovale.toc
OvaleAST.lua
tests/ast.t
diff --git a/Ovale.toc b/Ovale.toc
index 719a772..fef99ac 100644
--- a/Ovale.toc
+++ b/Ovale.toc
@@ -29,6 +29,7 @@ OvaleQueue.lua
 OvaleTimeSpan.lua

 # Core modules.
+OvaleAST.lua
 OvaleActionBar.lua
 OvaleAura.lua
 OvaleBestAction.lua
diff --git a/OvaleAST.lua b/OvaleAST.lua
new file mode 100644
index 0000000..6e21879
--- /dev/null
+++ b/OvaleAST.lua
@@ -0,0 +1,2349 @@
+--[[--------------------------------------------------------------------
+    Ovale Spell Priority
+    Copyright (C) 2014 Johnny C. Lam
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License in the LICENSE
+    file accompanying this program.
+--]]--------------------------------------------------------------------
+
+--[[----------------------------------------------------------------------------
+	This module implements a parser that generates an abstract syntax tree (AST)
+	from an Ovale script.
+
+	An AST data structure is a table with the following public properties:
+
+		ast.annotation
+		ast.annotation.customFunction
+		ast.annotation.definition
+		ast.annotation.functionCall
+		ast.child
+--]]----------------------------------------------------------------------------
+
+local _, Ovale = ...
+local OvaleAST = Ovale:NewModule("OvaleAST")
+Ovale.OvaleAST = OvaleAST
+
+--<private-static-properties>
+local L = Ovale.L
+local OvalePool = Ovale.OvalePool
+
+-- Forward declarations for module dependencies.
+local OvaleCondition = nil
+local OvaleLexer = nil
+local OvaleScripts = nil
+
+local format = string.format
+local gsub = string.gsub
+local next = next
+local pairs = pairs
+local rawset = rawset
+local setmetatable = setmetatable
+local strlower = string.lower
+local strmatch = string.match
+local strsub = string.sub
+local tconcat = table.concat
+local tinsert = table.insert
+local tonumber = tonumber
+local tostring = tostring
+local type = type
+local wipe = table.wipe
+local yield = coroutine.yield
+local API_GetItemInfo = GetItemInfo
+local API_GetSpellInfo = GetSpellInfo
+
+-- Profiling set-up.
+local Profiler = Ovale.Profiler
+local profiler = nil
+do
+	local group = OvaleAST:GetName()
+
+	local function EnableProfiling()
+		API_GetItemInfo = Profiler:Wrap(group, "OvaleAST_API_GetItemInfo", GetItemInfo)
+		API_GetSpellInfo = Profiler:Wrap(group, "OvaleAST_API_GetSpellInfo", GetSpellInfo)
+	end
+
+	local function DisableProfiling()
+		API_GetItemInfo = GetItemInfo
+		API_GetSpellInfo = GetSpellInfo
+	end
+
+	Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling)
+	profiler = Profiler:GetProfilingGroup(group)
+end
+
+-- Keywords for the Ovale script language.
+local KEYWORD = {
+	["and"] = true,
+	["if"] = true,
+	["not"] = true,
+	["or"] = true,
+	["unless"] = true,
+	["wait"] = true,
+}
+
+local DECLARATION_KEYWORD = {
+	["AddActionIcon"] = true,
+	["AddCheckBox"] = true,
+	["AddFunction"] = true,
+	["AddIcon"] = true,
+	["AddListItem"] = true,
+	["Define"] = true,
+	["Include"] = true,
+	["ItemInfo"] = true,
+	["ItemList"] = true,
+	["ScoreSpells"] = true,
+	["SpellInfo"] = true,
+	["SpellList"] = true,
+}
+
+local PARAMETER_KEYWORD = {
+	["checkbox"] = true,
+	["checkboxoff"] = true,
+	["checkboxon"] = true,
+	["glyph"] = true,
+	["if_spell"] = true,
+	["if_stance"] = true,
+	["item"] = true,
+	["itemcount"] = true,
+	["itemset"] = true,
+	["list"] = true,
+	["mastery"] = true,
+	["stance"] = true,
+	["talent"] = true,
+	["wait"] = true,
+}
+
+local SPELL_AURA_KEYWORD = {
+	["SpellAddBuff"] = true,
+	["SpellAddDebuff"] = true,
+	["SpellAddTargetBuff"] = true,
+	["SpellAddTargetDebuff"] = true,
+	["SpellDamageBuff"] = true,
+	["SpellDamageDebuff"] = true,
+}
+
+do
+	-- SpellAuraList keywords are declaration keywords.
+	for keyword, value in pairs(SPELL_AURA_KEYWORD) do
+		DECLARATION_KEYWORD[keyword] = value
+	end
+	-- All keywords are Ovale script keywords.
+	for keyword, value in pairs(DECLARATION_KEYWORD) do
+		KEYWORD[keyword] = value
+	end
+	for keyword, value in pairs(PARAMETER_KEYWORD) do
+		KEYWORD[keyword] = value
+	end
+end
+
+-- Table of pattern/tokenizer pairs for the Ovale script language.
+local MATCHES = nil
+
+-- Functions that are actions.
+local ACTION = {
+	["item"] = true,
+	["macro"] = true,
+	["spell"] = true,
+	["texture"] = true,
+}
+
+-- Functions for accessing string databases.
+local STRING_LOOKUP_FUNCTION = {
+	["ItemName"] = true,
+	["L"] = true,
+	["SpellName"] = true,
+}
+
+-- Unary and binary operators with precedence.
+local UNARY_OPERATOR = {
+	["not"] = { "logical", 50 },
+	["-"]   = { "arithmetic", 50 },
+}
+local BINARY_OPERATOR = {
+	-- logical
+	["and"] = { "logical", 10 },
+	["or"]  = { "logical", 10 },
+	-- comparison
+	["!="]  = { "compare", 20 },
+	["<"]   = { "compare", 20 },
+	["<="]  = { "compare", 20 },
+	["=="]  = { "compare", 20 },
+	[">"]   = { "compare", 20 },
+	[">="]  = { "compare", 20 },
+	-- addition, subtraction
+	["+"]   = { "arithmetic", 30 },
+	["-"]   = { "arithmetic", 30 },
+	-- multiplication, division, modulus
+	["%"]   = { "arithmetic", 40 },
+	["*"]   = { "arithmetic", 40 },
+	["/"]   = { "arithmetic", 40 },
+	-- exponentiation
+	["^"]   = { "arithmetic", 100 },
+}
+
+-- INDENT[k] is a string of k concatenated tabs.
+local INDENT = {}
+do
+	INDENT[0] = ""
+	local metatable = {
+		__index = function(tbl, key)
+			key = tonumber(key)
+			if key > 0 then
+				local s = tbl[key - 1] .. "\t"
+				rawset(tbl, key, s)
+				return s
+			end
+			return INDENT[0]
+		end,
+	}
+	setmetatable(INDENT, metatable)
+end
+
+local self_indent = 0
+local self_outputPool = OvalePool("OvaleAST_outputPool")
+
+local self_parametersPool = OvalePool("OvaleAST_parametersPool")
+local self_childrenPool = OvalePool("OvaleAST_childrenPool")
+local self_pool = OvalePool("OvaleAST_pool")
+do
+	self_pool.Clean = function(self, node)
+		if node.child then
+			self_childrenPool:Release(node.child)
+			node.child = nil
+		end
+	end
+end
+--</private-static-properties>
+
+--<public-static-properties>
+-- Export list of parameters keywords.
+OvaleAST.PARAMETER_KEYWORD = PARAMETER_KEYWORD
+--</public-static-properties>
+
+--<private-static-methods>
+-- Implementation of PHP-like print_r() taken from http://lua-users.org/wiki/TableSerialization.
+-- This is used to print out a table, but has been modified to print out an AST.
+local function print_r(node, indent, done, output)
+	done = done or {}
+	output = output or {}
+	indent = indent or ''
+	for key, value in pairs(node) do
+		if type(value) == "table" then
+			if done[value] then
+				tinsert(output, indent .. "[" .. tostring(key) .. "] => (self_reference)")
+			else
+				-- Shortcut conditional allocation
+				done[value] = true
+				if value.type then
+					tinsert(output, indent .. "[" .. tostring(key) .. "] =>")
+				else
+					tinsert(output, indent .. "[" .. tostring(key) .. "] => {")
+				end
+				print_r(value, indent .. "    ", done, output)
+				if not value.type then
+					tinsert(output, indent .. "}")
+				end
+			end
+		else
+			tinsert(output, indent .. "[" .. tostring(key) .. "] => " .. tostring(value))
+		end
+	end
+	return output
+end
+
+-- Follow the flyweight pattern for number nodes.
+local function GetNumberNode(value, nodeList, annotation)
+	-- Check for a flyweight node with this exact numerical value.
+	annotation.numberFlyweight = annotation.numberFlyweight or {}
+	local node = annotation.numberFlyweight[value]
+	if not node then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "value"
+		node.value = value
+		node.origin = 0
+		node.rate = 0
+		-- Store the first node with this exact numerical value in numberFlyweight.
+		annotation.numberFlyweight[value] = node
+	end
+	return node
+end
+
+--[[---------------------------------------------
+	Lexer functions (for use with OvaleLexer)
+--]]---------------------------------------------
+local function TokenizeComment(token)
+	return yield("comment", token)
+end
+
+local function TokenizeLua(token, options)
+	-- Strip off leading [[ and trailing ]].
+	token = strsub(token, 3, -3)
+	return yield("lua", token)
+end
+
+local function TokenizeName(token)
+	if KEYWORD[token] then
+		return yield("keyword", token)
+	else
+		return yield("name", token)
+	end
+end
+
+local function TokenizeNumber(token, options)
+	if options and options.number then
+		token = tonumber(token)
+	end
+	return yield("number", token)
+end
+
+local function TokenizeString(token, options)
+	-- Strip leading and trailing quote characters.
+	if options and options.string then
+		token = strsub(token, 2, -2)
+	end
+	return yield("string", token)
+end
+
+local function TokenizeWhitespace(token)
+	return yield("space", token)
+end
+
+local function Tokenize(token)
+	return yield(token, token)
+end
+
+local function NoToken()
+	return yield(nil)
+end
+
+do
+	MATCHES = {
+		{ "^%s+", TokenizeWhitespace },
+		{ "^%d+%.?%d*", TokenizeNumber },
+		{ "^[%a_][%w_]*", TokenizeName },
+		{ "^((['\"])%2)", TokenizeString },	-- empty string
+		{ [[^(['\"]).-\\%1]], TokenizeString },
+		{ [[^(['\"]).-[^\]%1]], TokenizeString },
+		{ "^#.-\n", TokenizeComment },
+		{ "^!=", Tokenize },
+		{ "^==", Tokenize },
+		{ "^<=", Tokenize },
+		{ "^>=", Tokenize },
+		{ "^.", Tokenize },
+		{ "^$", NoToken },
+	}
+end
+
+local function GetTokenIterator(s)
+	local exclude = { space = true, comments = true }
+	do
+		-- Fix some API brokenness in the Penlight lexer.
+		if exclude.space then
+			exclude[TokenizeWhitespace] = true
+		end
+		if exclude.comments then
+			exclude[TokenizeComment] = true
+		end
+	end
+	return OvaleLexer.scan(s, MATCHES, exclude)
+end
+
+--[[------------------------
+	"Unparser" functions
+--]]------------------------
+
+-- Forward declarations of functions needed to implement the recursive unparser.
+local UNPARSE_VISITOR = nil
+local Unparse = nil
+local UnparseAddCheckBox = nil
+local UnparseAddFunction = nil
+local UnparseAddIcon = nil
+local UnparseAddListItem = nil
+local UnparseComment = nil
+local UnparseDefine = nil
+local UnparseExpression = nil
+local UnparseFunction = nil
+local UnparseGroup = nil
+local UnparseIf = nil
+local UnparseItemInfo = nil
+local UnparseList = nil
+local UnparseNumber = nil
+local UnparseParameterValue = nil
+local UnparseParameters = nil
+local UnparseScoreSpells = nil
+local UnparseScript = nil
+local UnparseSpellAuraList = nil
+local UnparseSpellInfo = nil
+local UnparseString = nil
+local UnparseUnless = nil
+local UnparseVariable = nil
+local UnparseWait = nil
+
+Unparse = function(node)
+	local visitor
+	if node.previousType then
+		visitor = UNPARSE_VISITOR[node.previousType]
+	else
+		visitor = UNPARSE_VISITOR[node.type]
+	end
+	if not visitor then
+		Ovale:FormatPrint("Unable to unparse node of type '%s'.", node.type)
+	else
+		return visitor(node)
+	end
+end
+
+UnparseAddCheckBox = function(node)
+	local s
+	if node.rawParams and next(node.rawParams) then
+		s = format("AddCheckBox(%s %s %s)", node.name, Unparse(node.description), UnparseParameters(node.rawParams))
+	else
+		s = format("AddCheckBox(%s %s)", node.name, Unparse(node.description))
+	end
+	return s
+end
+
+UnparseAddFunction = function(node)
+	local s
+	if node.rawParams and next(node.rawParams) then
+		s = format("AddFunction %s %s %s", node.name, UnparseParameters(node.rawParams), Unparse(node.child[1]))
+	else
+		s = format("AddFunction %s %s", node.name, Unparse(node.child[1]))
+	end
+	return s
+end
+
+UnparseAddIcon = function(node)
+	local s
+	if node.rawParams and next(node.rawParams) then
+		s = format("AddIcon %s %s", UnparseParameters(node.rawParams), Unparse(node.child[1]))
+	else
+		s = format("AddIcon %s", Unparse(node.child[1]))
+	end
+	return s
+end
+
+UnparseAddListItem = function(node)
+	local s
+	if node.rawParams and next(node.rawParams) then
+		s = format("AddListItem(%s %s %s %s)", node.name, node.item, Unparse(node.description), UnparseParameters(node.rawParams))
+	else
+		s = format("AddListItem(%s %s %s)", node.name, node.item, Unparse(node.description))
+	end
+	return s
+end
+
+UnparseComment = function(node)
+	return "#" .. node.comment
+end
+
+UnparseDefine = function(node)
+	return format("Define(%s %s)", node.name, node.value)
+end
+
+UnparseExpression = function(node)
+	local expression
+	if node.expressionType == "unary" then
+		if node.operator == "-" then
+			expression = "-" .. Unparse(node.child[1])
+		else
+			expression = format("%s %s", node.operator, Unparse(node.child[1]))
+		end
+	elseif node.expressionType == "binary" then
+		expression = format("%s %s %s", Unparse(node.child[1]), node.operator, Unparse(node.child[2]))
+	end
+	if node.left and node.right then
+		local left, right = node.left, node.right
+		local left = (node.left == "{") and "{ " or node.left
+		local right = (node.right == "}") and " }" or node.right
+		return left .. expression .. right
+	else
+		return expression
+	end
+end
+
+UnparseFunction = function(node)
+	local s
+	if node.rawParams and next(node.rawParams) then
+		local name
+		local filter = node.rawParams.filter
+		if filter == "debuff" then
+			name = gsub(node.name, "^Buff", "Debuff")
+		else
+			name = node.name
+		end
+		local target = node.rawParams.target
+		if target then
+			s = format("%s.%s(%s)", target, name, UnparseParameters(node.rawParams))
+		else
+			s = format("%s(%s)", name, UnparseParameters(node.rawParams))
+		end
+	else
+		s = format("%s()", node.name)
+	end
+	return s
+end
+
+UnparseGroup = function(node)
+	local output = self_outputPool:Get()
+	output[#output + 1] = ""
+	output[#output + 1] = INDENT[self_indent] .. "{"
+	self_indent = self_indent + 1
+	for _, statementNode in ipairs(node.child) do
+		output[#output + 1] = INDENT[self_indent] .. Unparse(statementNode)
+	end
+	self_indent = self_indent - 1
+	output[#output + 1] = INDENT[self_indent] .. "}"
+
+	local outputString = tconcat(output, "\n")
+	self_outputPool:Release(output)
+	return outputString
+end
+
+UnparseIf = function(node)
+	return format("if %s %s", Unparse(node.child[1]), Unparse(node.child[2]))
+end
+
+UnparseItemInfo = function(node)
+	return format("ItemInfo(%s %s)", node.name, UnparseParameters(node.rawParams))
+end
+
+UnparseList = function(node)
+	return format("%s(%s %s)", node.keyword, node.name, UnparseParameters(node.rawParams))
+end
+
+UnparseNumber = function(node)
+	return tostring(node.value)
+end
+
+UnparseParameterValue = function(node)
+	return "!" .. Unparse(node.child[1])
+end
+
+UnparseParameters = function(parameters)
+	local output = self_outputPool:Get()
+	local N = #parameters
+	for k = 1, N do
+		output[#output + 1] = Unparse(parameters[k])
+	end
+	for k, v in pairs(parameters) do
+		if type(k) == "number" and k <= N then
+			-- Already output in previous loop.
+		elseif type(v) == "table" then
+			output[#output + 1] = format("%s=%s", k, Unparse(v))
+		elseif k == "filter" or k == "target" then
+			-- Skip output of "filter" or "target".
+		else
+			output[#output + 1] = format("%s=%s", k, v)
+		end
+	end
+	local outputString = tconcat(output, " ")
+	self_outputPool:Release(output)
+	return outputString
+end
+
+UnparseScoreSpells = function(node)
+	return format("ScoreSpells(%s)", UnparseParameters(node.rawParams))
+end
+
+UnparseScript = function(node)
+	local output = self_outputPool:Get()
+	for _, declarationNode in ipairs(node.child) do
+		if declarationNode.type == "item_info" or declarationNode.type == "spell_aura_list" or declarationNode.type == "spell_info" then
+			output[#output + 1] = INDENT[self_indent + 1] .. Unparse(declarationNode)
+		else
+			-- Add an extra blank line preceding "AddFunction" or "AddIcon".
+			if declarationNode.type == "add_function" or declarationNode.type == "icon" then
+				output[#output + 1] = ""
+			end
+			output[#output + 1] = Unparse(declarationNode)
+		end
+	end
+	local outputString = tconcat(output, "\n")
+	self_outputPool:Release(output)
+	return outputString
+end
+
+UnparseSpellAuraList = function(node)
+	return format("%s(%s %s)", node.keyword, node.name, UnparseParameters(node.rawParams))
+end
+
+UnparseSpellInfo = function(node)
+	return format("SpellInfo(%s %s)", node.name, UnparseParameters(node.rawParams))
+end
+
+UnparseString = function(node)
+	return '"' .. node.value .. '"'
+end
+
+UnparseUnless = function(node)
+	return format("unless %s %s", Unparse(node.child[1]), Unparse(node.child[2]))
+end
+
+UnparseVariable = function(node)
+	return node.name
+end
+
+UnparseWait = function(node)
+	return format("wait %s", Unparse(node.child[1]))
+end
+
+do
+	UNPARSE_VISITOR = {
+		["action"] = UnparseFunction,
+		["add_function"] = UnparseAddFunction,
+		["arithmetic"] = UnparseExpression,
+		["bang_value"] = UnparseParameterValue,
+		["checkbox"] = UnparseAddCheckBox,
+		["compare"] = UnparseExpression,
+		["comment"] = UnparseComment,
+		["custom_function"] = UnparseFunction,
+		["define"] = UnparseDefine,
+		["function"] = UnparseFunction,
+		["group"] = UnparseGroup,
+		["icon"] = UnparseAddIcon,
+		["if"] = UnparseIf,
+		["item_info"] = UnparseItemInfo,
+		["list"] = UnparseList,
+		["list_item"] = UnparseAddListItem,
+		["logical"] = UnparseExpression,
+		["score_spells"] = UnparseScoreSpells,
+		["script"] = UnparseScript,
+		["spell_aura_list"] = UnparseSpellAuraList,
+		["spell_info"] = UnparseSpellInfo,
+		["string"] = UnparseString,
+		["unless"] = UnparseUnless,
+		["value"] = UnparseNumber,
+		["variable"] = UnparseVariable,
+		["wait"] = UnparseWait,
+	}
+end
+
+--[[--------------------
+	Parser functions
+--]]--------------------
+
+-- Prints the error message and the next 20 tokens from tokenStream.
+local function SyntaxError(tokenStream, ...)
+	Ovale:FormatPrint(...)
+	local context = { "Next tokens:" }
+	for i = 1, 20 do
+		local tokenType, token = tokenStream:Peek(i)
+		if tokenType then
+			context[#context + 1] = token
+		else
+			context[#context + 1] = "<EOS>"
+			break
+		end
+	end
+	Ovale:Print(tconcat(context, " "))
+end
+
+-- Forward declarations of parser functions needed to implement a recursive descent parser.
+local PARSE_VISITOR = nil
+local Parse = nil
+local ParseAddCheckBox = nil
+local ParseAddFunction = nil
+local ParseAddIcon = nil
+local ParseAddListItem = nil
+local ParseDeclaration = nil
+local ParseDefine = nil
+local ParseExpression = nil
+local ParseFunction = nil
+local ParseGroup = nil
+local ParseIf = nil
+local ParseInclude = nil
+local ParseItemInfo = nil
+local ParseList = nil
+local ParseNumber = nil
+local ParseParameterValue = nil
+local ParseParameters = nil
+local ParseParentheses = nil
+local ParseScoreSpells = nil
+local ParseScript = nil
+local ParseSimpleExpression = nil
+local ParseSpellAuraList = nil
+local ParseSpellInfo = nil
+local ParseString = nil
+local ParseStatement = nil
+local ParseUnless = nil
+local ParseVariable = nil
+local ParseWait = nil
+
+Parse = function(nodeType, tokenStream, nodeList, annotation)
+	local visitor = PARSE_VISITOR[nodeType]
+	if not visitor then
+		Ovale:FormatPrint("Unable to parse node of type '%s'.", nodeType)
+	else
+		return visitor(tokenStream, nodeList, annotation)
+	end
+end
+
+ParseAddCheckBox = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'AddCheckBox' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "AddCheckBox") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDCHECKBOX; 'AddCheckBox' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDCHECKBOX; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the checkbox name.
+	local name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDCHECKBOX; name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the description string.
+	local descriptionNode
+	if ok then
+		ok, descriptionNode = ParseString(tokenStream, nodeList, annotation)
+	end
+	-- Consume any parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDCHECKBOX; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "checkbox"
+		node.name = name
+		node.description = descriptionNode
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseAddFunction = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'AddFunction' token.
+	local tokenType, token = tokenStream:Consume()
+	if not (tokenType == "keyword" and token == "AddFunction") then
+		SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDFUNCTION; 'AddFunction' expected.", token)
+		ok = false
+	end
+	-- Consume the function name.
+	local name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDFUNCTION; name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume any parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the body.
+	local bodyNode
+	if ok then
+		ok, bodyNode = ParseGroup(tokenStream, nodeList, annotation)
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList, true)
+		node.type = "add_function"
+		node.name = name
+		node.child[1] = bodyNode
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+		annotation.customFunction = annotation.customFunction or {}
+		annotation.customFunction[name] = node
+	end
+	return ok, node
+end
+
+ParseAddIcon = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'AddIcon' token.
+	local tokenType, token = tokenStream:Consume()
+	if not (tokenType == "keyword" and token == "AddIcon") then
+		SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDICON; 'AddIcon' expected.", token)
+		ok = false
+	end
+	-- Consume any parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the body.
+	local bodyNode
+	if ok then
+		ok, bodyNode = ParseGroup(tokenStream, nodeList, annotation)
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList, true)
+		node.type = "icon"
+		node.child[1] = bodyNode
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseAddListItem = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'AddListItem' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "AddListItem") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDLISTITEM; 'AddListItem' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDLISTITEM; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the list name.
+	local name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDLISTITEM; name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the item name.
+	local item
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			item = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDLISTITEM; name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the description string.
+	local descriptionNode
+	if ok then
+		ok, descriptionNode = ParseString(tokenStream, nodeList, annotation)
+	end
+	-- Consume any parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ADDLISTITEM; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "list_item"
+		node.name = name
+		node.item = item
+		node.description = descriptionNode
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseDeclaration = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local node
+	local tokenType, token = tokenStream:Peek()
+	if tokenType == "keyword" and DECLARATION_KEYWORD[token] then
+		if token == "AddCheckBox" then
+			ok, node = ParseAddCheckBox(tokenStream, nodeList, annotation)
+		elseif token == "AddFunction" then
+			ok, node = ParseAddFunction(tokenStream, nodeList, annotation)
+		elseif token == "AddIcon" then
+			ok, node = ParseAddIcon(tokenStream, nodeList, annotation)
+		elseif token == "AddListItem" then
+			ok, node = ParseAddListItem(tokenStream, nodeList, annotation)
+		elseif token == "Define" then
+			ok, node = ParseDefine(tokenStream, nodeList, annotation)
+		elseif token == "Include" then
+			ok, node = ParseInclude(tokenStream, nodeList, annotation)
+		elseif token == "ItemInfo" then
+			ok, node = ParseItemInfo(tokenStream, nodeList, annotation)
+		elseif token == "ItemList" then
+			ok, node = ParseList(tokenStream, nodeList, annotation)
+		elseif token == "ScoreSpells" then
+			ok, node = ParseScoreSpells(tokenStream, nodeList, annotation)
+		elseif SPELL_AURA_KEYWORD[token] then
+			ok, node = ParseSpellAuraList(tokenStream, nodeList, annotation)
+		elseif token == "SpellInfo" then
+			ok, node = ParseSpellInfo(tokenStream, nodeList, annotation)
+		elseif token == "SpellList" then
+			ok, node = ParseList(tokenStream, nodeList, annotation)
+		end
+	else
+		SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DECLARATION; declaration keyword expected.", token)
+		tokenStream:Consume()
+		ok = false
+	end
+	return ok, node
+end
+
+ParseDefine = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'Define' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "Define") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DEFINE; 'Define' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DEFINE; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the variable name.
+	local name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DEFINE; name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the value.
+	local value
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "-" then
+			-- Negative number.
+			tokenType, token = tokenStream:Consume()
+			if tokenType == "number" then
+				-- Elide the unary negation operator into the number.
+				value = -1 * tonumber(token)
+			else
+				SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DEFINE; number expected after '-'.", token)
+				ok = false
+			end
+		elseif tokenType == "number" then
+			value = tonumber(token)
+		elseif tokenType == "string" then
+			value = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DEFINE; number or string expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing DEFINE; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "define"
+		node.name = name
+		node.value = value
+		annotation.definition = annotation.definition or {}
+		annotation.definition[name] = value
+	end
+	return ok, node
+end
+
+--[[
+	Operator-precedence parser for logical and arithmetic expressions.
+	Implementation taken from Wikipedia:
+		http://en.wikipedia.org/wiki/Operator-precedence_parser
+--]]
+ParseExpression = function(tokenStream, nodeList, annotation, minPrecedence)
+	minPrecedence = minPrecedence or 0
+	local ok = true
+	local node
+
+	-- Check for unary operator expressions first as they decorate the underlying expression.
+	do
+		local tokenType, token = tokenStream:Peek()
+		if tokenType then
+			local opInfo = UNARY_OPERATOR[token]
+			if opInfo then
+				local opType, precedence = opInfo[1], opInfo[2]
+				tokenStream:Consume()
+				local operator = token
+				local rhsNode
+				ok, rhsNode = ParseExpression(tokenStream, nodeList, annotation, precedence)
+				if ok then
+					if operator == "-" and rhsNode.type == "value" then
+						-- Elide the unary negation operator into the number.
+						local value = -1 * rhsNode.value
+						node = GetNumberNode(value, nodeList, annotation)
+					else
+						node = OvaleAST:NewNode(nodeList, true)
+						node.type = opType
+						node.expressionType = "unary"
+						node.operator = operator
+						node.precedence = precedence
+						node.child[1] = rhsNode
+					end
+				end
+			else
+				ok, node = ParseSimpleExpression(tokenStream, nodeList, annotation)
+			end
+		end
+	end
+
+	-- Peek at the next token to see if it is a binary operator.
+	while ok do
+		local keepScanning = false
+		local tokenType, token = tokenStream:Peek()
+		if tokenType then
+			local opInfo = BINARY_OPERATOR[token]
+			if opInfo then
+				local opType, precedence = opInfo[1], opInfo[2]
+				if precedence and precedence > minPrecedence then
+					keepScanning = true
+					tokenStream:Consume()
+					local operator = token
+					local lhsNode = node
+					local rhsNode
+					ok, rhsNode = ParseExpression(tokenStream, nodeList, annotation, precedence)
+					if ok then
+						node = OvaleAST:NewNode(nodeList, true)
+						node.type = opType
+						node.expressionType = "binary"
+						node.operator = operator
+						node.precedence = precedence
+						node.child[1] = lhsNode
+						node.child[2] = rhsNode
+					end
+				end
+			end
+		end
+		if not keepScanning then
+			break
+		end
+	end
+
+	return ok, node
+end
+
+ParseFunction = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local name, lowername
+	-- Consume the name.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+			lowername = strlower(name)
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; name expected.", token)
+			ok = false
+		end
+	end
+	-- Check for <target>.<function>.
+	local target
+	if ok then
+		local tokenType, token = tokenStream:Peek()
+		if tokenType == "." then
+			target = name
+			tokenType, token = tokenStream:Consume(2)
+			if tokenType == "name" then
+				name = token
+				lowername = strlower(name)
+			else
+				SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; name expected.", token)
+				ok = false
+			end
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume any function parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Verify that an action has at least one fixed parameter.
+	if ok and ACTION[lowername] and not parameters[1] then
+		SyntaxError(tokenStream, "Syntax error: action '%s' requires at least one fixed parameter.", name)
+		ok = false
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing FUNCTION; ')' expected.", token)
+			ok = false
+		end
+	end
+	if ok then
+		-- Parse the function name.
+		if not parameters.target then
+			-- Auto-set the target if the function name starts with "Target".
+			if strsub(lowername, 1, 6) == "target" then
+				parameters.target = "target"
+				lowername = strsub(lowername, 7)
+				name = strsub(name, 7)
+			end
+		end
+		if not parameters.filter then
+			-- Auto-set the aura filter if the function name starts with "Debuff" or "Buff".
+			if strsub(lowername, 1, 6) == "debuff" then
+				parameters.filter = "debuff"
+			elseif strsub(lowername, 1, 4) == "buff" then
+				parameters.filter = "buff"
+			elseif strsub(lowername, 1, 11) == "otherdebuff" then
+				parameters.filter = "debuff"
+			elseif strsub(lowername, 1, 9) == "otherbuff" then
+				parameters.filter = "buff"
+			end
+		end
+		-- Set the target if given in a prefix.
+		if target then
+			parameters.target = target
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.name = name
+		node.lowername = lowername
+		if ACTION[lowername] then
+			node.type = "action"
+			-- Built-in functions are case-insensitive.
+			node.func = lowername
+		elseif STRING_LOOKUP_FUNCTION[name] then
+			node.type = "function"
+			-- String-lookup functions are case-sensitive.
+			node.func = name
+		elseif OvaleCondition:IsCondition(lowername) then
+			node.type = "function"
+			-- Built-in functions are case-insensitive.
+			node.func = lowername
+		else
+			node.type = "custom_function"
+			-- Script-defined functions are case-sensitive.
+			node.func = name
+		end
+		node.rawParams = parameters
+		node.asString = UnparseFunction(node)
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+		annotation.functionCall = annotation.functionCall or {}
+		annotation.functionCall[node.func] = true
+		annotation.functionReference = annotation.functionReference or {}
+		annotation.functionReference[#annotation.functionReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseGroup = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the left brace.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "{" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing GROUP; '{' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume any statements up to the matching right brace.
+	local child = self_childrenPool:Get()
+	local tokenType, token = tokenStream:Peek()
+	while ok and tokenType and tokenType ~= "}" do
+		local statementNode
+		ok, statementNode = ParseStatement(tokenStream, nodeList, annotation)
+		if ok then
+			child[#child + 1] = statementNode
+			tokenType, token = tokenStream:Peek()
+		else
+			break
+		end
+	end
+	-- Consume the right brace.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "}" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing GROUP; '}' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "group"
+		node.child = child
+	else
+		self_childrenPool:Release(child)
+	end
+	return ok, node
+end
+
+ParseIf = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'if' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "if") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing IF; 'if' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the condition and body.
+	local conditionNode, bodyNode
+	if ok then
+		ok, conditionNode = ParseExpression(tokenStream, nodeList, annotation)
+	end
+	if ok then
+		ok, bodyNode = ParseStatement(tokenStream, nodeList, annotation)
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList, true)
+		node.type = "if"
+		node.child[1] = conditionNode
+		node.child[2] = bodyNode
+	end
+	return ok, node
+end
+
+ParseInclude = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'Include' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "Include") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing INCLUDE; 'Include' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing INCLUDE; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the script name.
+	local name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing INCLUDE; script name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing INCLUDE; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Get the code associated with the script name.
+	local code = OvaleScripts.script[name] and OvaleScripts.script[name].code
+	if not code then
+		Ovale:FormatPrint("Script '%s' not found when parsing INCLUDE.", name)
+		ok = false
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		local includeTokenStream = OvaleLexer(name, GetTokenIterator(code))
+		ok, node = ParseScript(includeTokenStream, nodeList, annotation)
+		includeTokenStream:Release()
+	end
+	return ok, node
+end
+
+ParseItemInfo = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local name, lowername
+	-- Consume the 'ItemInfo' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "ItemInfo") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ITEMINFO; 'ItemInfo' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ITEMINFO; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the item ID.
+	local itemId, name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "number" then
+			spellId = token
+		elseif tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ITEMINFO; number or name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume any ItemInfo parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing ITEMINFO; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "item_info"
+		node.itemId = itemId
+		node.name = name
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+		if name then
+			annotation.nameReference = annotation.nameReference or {}
+			annotation.nameReference[#annotation.nameReference + 1] = node
+		end
+	end
+	return ok, node
+end
+
+ParseList = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the list token.
+	local keyword
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "keyword" and (token == "ItemList" or token == "SpellList") then
+			keyword = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing LIST; keyword expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing LIST; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the list name.
+	local name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing LIST; name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the list.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation, true)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing LIST; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "list"
+		node.keyword = keyword
+		node.name = name
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseNumber = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local value
+	-- Consume the number.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "number" then
+			value = tonumber(token)
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing NUMBER; number expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = GetNumberNode(value, nodeList, annotation)
+	end
+	return ok, node
+end
+
+ParseParameterValue = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local isBang = false
+	local tokenType, token = tokenStream:Peek()
+	if tokenType == "!" then
+		isBang = true
+		-- Consume the '!' token.
+		tokenStream:Consume()
+	end
+	local expressionNode
+	tokenType, token = tokenStream:Peek()
+	if tokenType == "(" or tokenType == "-" then
+		ok, expressionNode = ParseExpression(tokenStream, nodeList, annotation)
+	else
+		ok, expressionNode = ParseSimpleExpression(tokenStream, nodeList, annotation)
+	end
+	local node
+	if isBang then
+		node = OvaleAST:NewNode(nodeList, true)
+		node.type = "bang_value"
+		node.child[1] = expressionNode
+	else
+		node = expressionNode
+	end
+	return ok, node
+end
+
+ParseParameters = function(tokenStream, nodeList, annotation, isList)
+	local ok = true
+	local parameters = self_parametersPool:Get()
+	while ok do
+		local tokenType, token = tokenStream:Peek()
+		if tokenType then
+			local name, node
+			if tokenType == "name" then
+				ok, node = ParseVariable(tokenStream, nodeList, annotation)
+				if ok then
+					name = node.name
+				end
+			elseif tokenType == "number" then
+				ok, node = ParseNumber(tokenStream, nodeList, annotation)
+				if ok then
+					name = node.value
+				end
+			elseif PARAMETER_KEYWORD[token] then
+				if isList then
+					SyntaxError(tokenStream, "Syntax error: unexpected keyword '%s' when parsing PARAMETERS; simple expression expected.", token)
+					ok = false
+				else
+					tokenStream:Consume()
+					name = token
+				end
+			else
+				break
+			end
+			-- Check if this is a bare value or the start of a "name=value" pair.
+			if ok and name then
+				tokenType, token = tokenStream:Peek()
+				if tokenType == "=" then
+					-- Consume the '=' token.
+					tokenStream:Consume()
+					-- Get the value.
+					ok, node = ParseParameterValue(tokenStream, nodeList, annotation)
+					if ok then
+						parameters[name] = node
+					end
+				elseif PARAMETER_KEYWORD[name] then
+					SyntaxError(tokenStream, "Syntax error: unexpected keyword '%s' when parsing PARAMETERS; simple expression expected.", name)
+					ok = false
+				else
+					parameters[#parameters + 1] = node
+				end
+			end
+		else
+			break
+		end
+	end
+	if ok then
+		annotation.parametersList = annotation.parametersList or {}
+		annotation.parametersList[#annotation.parametersList + 1] = parameters
+	else
+		parameters = nil
+	end
+	return ok, parameters
+end
+
+ParseParentheses = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local leftToken, rightToken
+	-- Consume the left parenthesis.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "(" then
+			leftToken, rightToken = "(", ")"
+		elseif tokenType == "{" then
+			leftToken, rightToken = "{", "}"
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing PARENTHESES; '(' or '{' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the inner expression.
+	local node
+	if ok then
+		ok, node = ParseExpression(tokenStream, nodeList, annotation)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= rightToken then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing PARENTHESES; '%s' expected.", token, rightToken)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	if ok then
+		node.left = leftToken
+		node.right = rightToken
+	end
+	return ok, node
+end
+
+ParseScoreSpells = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'ScoreSpells' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "ScoreSpells") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SCORESPELLS; 'ScoreSpells' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SCORESPELLS; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the list of spells.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation, true)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SCORESPELLS; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "score_spells"
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseScript = function(tokenStream, nodeList, annotation)
+	profiler.Start("OvaleAST_ParseScript")
+	local ok = true
+	-- Consume each declaration.
+	local child = self_childrenPool:Get()
+	while ok do
+		local tokenType, token = tokenStream:Peek()
+		if tokenType then
+			local declarationNode
+			ok, declarationNode = ParseDeclaration(tokenStream, nodeList, annotation)
+			if ok then
+				if declarationNode.type == "script" then
+					for _, node in ipairs(declarationNode.child) do
+						child[#child + 1] = node
+					end
+					-- All "script" nodes are standalone and need to be explicitly released.
+					self_pool:Release(declarationNode)
+				else
+					child[#child + 1] = declarationNode
+				end
+			end
+		else
+			break
+		end
+	end
+	-- Create the AST node.
+	local ast
+	if ok then
+		-- Create a standalone AST node.
+		ast = OvaleAST:NewNode()
+		ast.type = "script"
+		ast.child = child
+	else
+		self_childrenPool:Release(child)
+	end
+	profiler.Stop("OvaleAST_ParseScript")
+	return ok, ast
+end
+
+ParseSimpleExpression = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local node
+	local tokenType, token = tokenStream:Peek()
+	if tokenType == "number" then
+		ok, node = ParseNumber(tokenStream, nodeList, annotation)
+	elseif tokenType == "name" then
+		tokenType, token = tokenStream:Peek(2)
+		if tokenType == "." or tokenType == "(" then
+			ok, node = ParseFunction(tokenStream, nodeList, annotation)
+		else
+			ok, node = ParseVariable(tokenStream, nodeList, annotation)
+		end
+	elseif tokenType == "(" or tokenType == "{" then
+		ok, node = ParseParentheses(tokenStream, nodeList, annotation)
+	else
+		SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SIMPLE EXPRESSION", token)
+		tokenStream:Consume()
+		ok = false
+	end
+	return ok, node
+end
+
+ParseSpellAuraList = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the keyword token.
+	local keyword
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "keyword" and SPELL_AURA_KEYWORD[token] then
+			keyword = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLAURALIST; keyword expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLAURALIST; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the spell ID.
+	local spellId, name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "number" then
+			spellId = token
+		elseif tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLAURALIST; number or name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume any parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLAURALIST; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "spell_aura_list"
+		node.keyword = keyword
+		node.spellId = spellId
+		node.name = name
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+		if name then
+			annotation.nameReference = annotation.nameReference or {}
+			annotation.nameReference[#annotation.nameReference + 1] = node
+		end
+	end
+	return ok, node
+end
+
+ParseSpellInfo = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local name, lowername
+	-- Consume the 'SpellInfo' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "SpellInfo") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLINFO; 'SpellInfo' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the left parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= "(" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLINFO; '(' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the spell ID.
+	local spellId, name
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "number" then
+			spellId = token
+		elseif tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLINFO; number or name expected.", token)
+			ok = false
+		end
+	end
+	-- Consume any SpellInfo parameters.
+	local parameters
+	if ok then
+		ok, parameters = ParseParameters(tokenStream, nodeList, annotation)
+	end
+	-- Consume the right parenthesis.
+	if ok then
+		local tokenType, token = tokenStream:Consume()
+		if tokenType ~= ")" then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing SPELLINFO; ')' expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "spell_info"
+		node.spellId = spellId
+		node.name = name
+		node.rawParams = parameters
+		annotation.parametersReference = annotation.parametersReference or {}
+		annotation.parametersReference[#annotation.parametersReference + 1] = node
+		if name then
+			annotation.nameReference = annotation.nameReference or {}
+			annotation.nameReference[#annotation.nameReference + 1] = node
+		end
+	end
+	return ok, node
+end
+
+ParseStatement = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local node
+	local tokenType, token = tokenStream:Peek()
+	if tokenType then
+		local parser
+		if token == "{" then
+			-- Find the matching '}' and inspect the next token to see if this is an expression or a group.
+			local i = 1
+			local count = 0
+			while tokenType do
+				if token == "{" then
+					count = count + 1
+				elseif token == "}" then
+					count = count - 1
+				end
+				i = i + 1
+				tokenType, token = tokenStream:Peek(i)
+				if count == 0 then
+					break
+				end
+			end
+			if tokenType then
+				if BINARY_OPERATOR[token] then
+					ok, node = ParseExpression(tokenStream, nodeList, annotation)
+				else
+					ok, node = ParseGroup(tokenStream, nodeList, annotation)
+				end
+			else
+				SyntaxError(tokenStream, "Syntax error: unexpected end of script.")
+			end
+		elseif token == "if" then
+			ok, node = ParseIf(tokenStream, nodeList, annotation)
+		elseif token == "unless" then
+			ok, node = ParseUnless(tokenStream, nodeList, annotation)
+		elseif token == "wait" then
+			ok, node = ParseWait(tokenStream, nodeList, annotation)
+		else
+			ok, node = ParseExpression(tokenStream, nodeList, annotation)
+		end
+	end
+	return ok, node
+end
+
+ParseString = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local node
+	local value
+	if ok then
+		local tokenType, token = tokenStream:Peek()
+		if tokenType == "string" then
+			value = token
+			tokenStream:Consume()
+		elseif tokenType == "name" then
+			if STRING_LOOKUP_FUNCTION[token] then
+				ok, node = ParseFunction(tokenStream, nodeList, annotation)
+			else
+				value = token
+				tokenStream:Consume()
+			end
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing STRING; string, variable, or function expected.", token)
+			tokenStream:Consume()
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	if ok then
+		if not node then
+			node = OvaleAST:NewNode(nodeList)
+			node.type = "string"
+			node.value = value
+		end
+		annotation.stringReference = annotation.stringReference or {}
+		annotation.stringReference[#annotation.stringReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseUnless = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'unless' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "unless") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing UNLESS; 'unless' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the condition and body.
+	local conditionNode, bodyNode
+	if ok then
+		ok, conditionNode = ParseExpression(tokenStream, nodeList, annotation)
+	end
+	if ok then
+		ok, bodyNode = ParseStatement(tokenStream, nodeList, annotation)
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList, true)
+		node.type = "unless"
+		node.child[1] = conditionNode
+		node.child[2] = bodyNode
+	end
+	return ok, node
+end
+
+ParseVariable = function(tokenStream, nodeList, annotation)
+	local ok = true
+	local name
+	-- Consume the variable name.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if tokenType == "name" then
+			name = token
+		else
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing VARIABLE; name expected.", token)
+			ok = false
+		end
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList)
+		node.type = "variable"
+		node.name = name
+		annotation.nameReference = annotation.nameReference or {}
+		annotation.nameReference[#annotation.nameReference + 1] = node
+	end
+	return ok, node
+end
+
+ParseWait = function(tokenStream, nodeList, annotation)
+	local ok = true
+	-- Consume the 'wait' token.
+	do
+		local tokenType, token = tokenStream:Consume()
+		if not (tokenType == "keyword" and token == "wait") then
+			SyntaxError(tokenStream, "Syntax error: unexpected token '%s' when parsing WAIT; 'wait' expected.", token)
+			ok = false
+		end
+	end
+	-- Consume the statement body.
+	local bodyNode
+	if ok then
+		ok, bodyNode = ParseStatement(tokenStream, nodeList, annotation)
+	end
+	-- Create the AST node.
+	local node
+	if ok then
+		node = OvaleAST:NewNode(nodeList, true)
+		node.type = "wait"
+		node.child[1] = bodyNode
+	end
+	return ok, node
+end
+
+do
+	PARSE_VISITOR = {
+		["action"] = ParseFunction,
+		["add_function"] = ParseAddFunction,
+		["arithmetic"] = ParseExpression,
+		["bang_value"] = ParseParameterValue,
+		["checkbox"] = ParseAddCheckBox,
+		["compare"] = ParseExpression,
+		["comment"] = ParseComment,
+		["custom_function"] = ParseFunction,
+		["define"] = ParseDefine,
+		["expression"] = ParseExpression,
+		["function"] = ParseFunction,
+		["group"] = ParseGroup,
+		["icon"] = ParseAddIcon,
+		["if"] = ParseIf,
+		["item_info"] = ParseItemInfo,
+		["list"] = ParseList,
+		["list_item"] = ParseAddListItem,
+		["logical"] = ParseExpression,
+		["score_spells"] = ParseScoreSpells,
+		["script"] = ParseScript,
+		["spell_aura_list"] = ParseSpellAuraList,
+		["spell_info"] = ParseSpellInfo,
+		["string"] = ParseString,
+		["unless"] = ParseUnless,
+		["value"] = ParseNumber,
+		["variable"] = ParseVariable,
+		["wait"] = ParseWait,
+	}
+end
+--</private-static-methods>
+
+--<public-static-methods>
+function OvaleAST:OnInitialize()
+	-- Resolve module dependencies.
+	OvaleCondition = Ovale.OvaleCondition
+	OvaleLexer = Ovale.OvaleLexer
+	OvaleScripts = Ovale.OvaleScripts
+end
+
+function OvaleAST:Debug()
+	self_pool:Debug()
+	self_parametersPool:Debug()
+	self_childrenPool:Debug()
+	self_outputPool:Debug()
+end
+
+-- Get a new node from the pool and save it in the nodes array.
+function OvaleAST:NewNode(nodeList, hasChild)
+	local node = self_pool:Get()
+	if nodeList then
+		local nodeId = #nodeList + 1
+		node.nodeId = nodeId
+		nodeList[nodeId] = node
+	end
+	if hasChild then
+		node.child = self_childrenPool:Get()
+	end
+	return node
+end
+
+function OvaleAST:NodeToString(node)
+	local output = print_r(node)
+	return tconcat(output, "\n")
+end
+
+function OvaleAST:ReleaseAnnotation(annotation)
+	if annotation.parametersList then
+		for _, parameters in ipairs(annotation.parametersList) do
+			self_parametersPool:Release(parameters)
+		end
+	end
+	if annotation.nodeList then
+		for _, node in ipairs(annotation.nodeList) do
+			self_pool:Release(node)
+		end
+	end
+	for key, value in pairs(annotation) do
+		if type(value) == "table" then
+			wipe(value)
+		end
+		annotation[key] = nil
+	end
+end
+
+function OvaleAST:Release(ast)
+	if ast.annotation then
+		self:ReleaseAnnotation(ast.annotation)
+		ast.annotation = nil
+	end
+	self_pool:Release(ast)
+end
+
+function OvaleAST:ParseCode(nodeType, code, nodeList, annotation)
+	nodeList = nodeList or {}
+	annotation = annotation or {}
+	local tokenStream = OvaleLexer("Ovale", GetTokenIterator(code))
+	local ok, node = Parse(nodeType, tokenStream, nodeList, annotation)
+	tokenStream:Release()
+	return node, nodeList, annotation
+end
+
+function OvaleAST:ParseScript(name, options)
+	-- Get the code associated with the script name.
+	local code = OvaleScripts.script[name] and OvaleScripts.script[name].code
+	local ast
+	if code then
+		options = options or { verify = true }
+		-- Annotation table for the AST.
+		local annotation = {
+			nodeList = {},
+			verify = options.verify,
+		}
+		ast = self:ParseCode("script", code, annotation.nodeList, annotation)
+		if ast then
+			ast.annotation = annotation
+			self:PropagateConstants(ast)
+			self:PropagateStrings(ast)
+			self:FlattenParameters(ast)
+			self:VerifyFunctionCalls(ast)
+		else
+			-- Create a dummy node to properly release resources.
+			ast = self:NewNode()
+			ast.annotation = annotation
+			self:Release(ast)
+			ast = nil
+		end
+	end
+	return ast
+end
+
+function OvaleAST:Unparse(node)
+	return Unparse(node)
+end
+
+-- Replaces variables with their defined values.
+function OvaleAST:PropagateConstants(ast)
+	profiler.Start("OvaleAST_PropagateConstants")
+	if ast.annotation then
+		local dictionary = ast.annotation.definition
+		if dictionary and ast.annotation.nameReference then
+			for _, node in ipairs(ast.annotation.nameReference) do
+				if node.type == "item_info" and node.name then
+					local itemId = dictionary[node.name]
+					if itemId then
+						node.itemId = itemId
+					end
+				elseif (node.type == "spell_aura_list" or node.type == "spell_info") and node.name then
+					local spellId = dictionary[node.name]
+					if spellId then
+						node.spellId = spellId
+					end
+				elseif node.type == "variable" then
+					local name = node.name
+					local value = dictionary[name]
+					if value then
+						-- Convert to a value node.
+						node.previousType = "variable"
+						node.type = "value"
+						node.value = value
+						node.origin = 0
+						node.rate = 0
+					end
+				end
+			end
+		end
+	end
+	profiler.Stop("OvaleAST_PropagateConstants")
+end
+
+-- Replaces variables and string-lookup function calls with string values.
+function OvaleAST:PropagateStrings(ast)
+	profiler.Start("OvaleAST_PropagateStrings")
+	if ast.annotation and ast.annotation.stringReference then
+		for _, node in ipairs(ast.annotation.stringReference) do
+			if node.type == "string" then
+				-- do nothing
+			elseif node.type == "variable" then
+				local value = node.name
+				-- Convert to a string node.
+				node.previousType = "variable"
+				node.type = "string"
+				node.value = value
+			elseif node.type == "function" then
+				-- Get the lookup key for the string database.
+				local key = node.rawParams[1]
+				if type(key) == "table" then
+					if key.type == "value" then
+						key = key.value
+					elseif key.type == "variable" then
+						key = key.name
+					elseif key.type == "string" then
+						key = key.value
+					end
+				end
+				local value
+				if key then
+					local name = node.name
+					if name == "ItemName" then
+						value = API_GetItemInfo(key)
+					elseif name == "L" then
+						value = L[key]
+					elseif name == "SpellName" then
+						value = API_GetSpellInfo(key)
+					end
+				end
+				if value then
+					-- Convert to a string node.
+					node.previousType = "function"
+					node.type = "string"
+					node.value = value
+					node.key = key
+				end
+			end
+		end
+	end
+	profiler.Stop("OvaleAST_PropagateStrings")
+end
+
+-- "Flattens" parameter tables by replacing table values with the bare numerical or string values
+-- so that the parameter table can be used directly by script conditions.
+function OvaleAST:FlattenParameters(ast)
+	profiler.Start("OvaleAST_FlattenParameters")
+	local annotation = ast.annotation
+	if annotation and annotation.parametersReference then
+		local dictionary = annotation.definition
+		for _, node in ipairs(annotation.parametersReference) do
+			if node.rawParams then
+				local parameters = self_parametersPool:Get()
+				for key, value in pairs(node.rawParams) do
+					-- Lookup the key.
+					if type(key) ~= "number" and dictionary and dictionary[key] then
+						key = dictionary[key]
+					end
+					-- Evaluate the value.
+					if type(value) == "table" then
+						local node = value
+						local isBang = false
+						if node.type == "bang_value" then
+							isBang = true
+							node = node.child[1]
+						end
+						if node.type == "value" then
+							value = node.value
+						elseif node.type == "variable" then
+							value = node.name
+						elseif node.type == "string" then
+							value = node.value
+						end
+						if isBang then
+							value = "!" .. tostring(value)
+						end
+					end
+					parameters[key] = value
+				end
+				node.params = parameters
+				annotation.parametersList = annotation.parametersList or {}
+				annotation.parametersList[#annotation.parametersList + 1] = parameters
+
+				-- Save a flattened string representation of the parameters.
+				local output = self_outputPool:Get()
+				local N = #parameters
+				for k = 1, N do
+					output[k] = parameters[k]
+				end
+				for k, v in pairs(parameters) do
+					if type(k) == "number" and k <= N then
+						-- Already output in previous loop.
+					else
+						output[#output + 1] = format("%s=%s", k, v)
+					end
+				end
+				node.paramsAsString = tconcat(output, " ")
+				self_outputPool:Release(output)
+			end
+		end
+	end
+	profiler.Stop("OvaleAST_FlattenParameters")
+end
+
+-- Verify that all functions called within the script are known.
+function OvaleAST:VerifyFunctionCalls(ast)
+	profiler.Start("OvaleAST_VerifyFunctionCalls")
+	if ast.annotation and ast.annotation.verify then
+		local customFunction = ast.annotation.customFunction
+		local functionCall = ast.annotation.functionCall
+		if functionCall then
+			for name in pairs(functionCall) do
+				if ACTION[name] then
+					-- Function call is an action.
+				elseif STRING_LOOKUP_FUNCTION[name] then
+					-- Function call is a string-lookup function.
+				elseif OvaleCondition:IsCondition(name) then
+					-- Function call is a registered script condition.
+				elseif customFunction and customFunction[name] then
+					-- Function call is a script-defined function (via AddFunction).
+				else
+					Ovale:Errorf("unknown function '%s'.", name)
+				end
+			end
+		end
+	end
+	profiler.Stop("OvaleAST_VerifyFunctionCalls")
+end
+
+function OvaleAST:Optimize(ast)
+	self:CommonFunctionElimination(ast)
+end
+
+--[[----------------------------------------------------------------------------
+	Common Function Elimination
+
+	This is an optimizing transformation of the AST that globally replaces
+	references to function nodes to the node of the first function call made
+	with identical parameters.
+--]]----------------------------------------------------------------------------
+function OvaleAST:CommonFunctionElimination(ast)
+	profiler.Start("OvaleAST_CommonFunctionElimination")
+	if ast.annotation then
+		-- Hash all of the function calls.
+		if ast.annotation.functionReference then
+			local functionHash = ast.annotation.functionHash or {}
+			for _, node in ipairs(ast.annotation.functionReference) do
+				if node.params then
+					local parameters = node.params
+					local N = #parameters
+
+					local output = self_outputPool:Get()
+					output[#output + 1] = node.name
+					output[#output + 1] = "("
+					for k, v in ipairs(parameters) do
+						output[#output + 1] = v
+					end
+					for k, v in pairs(parameters) do
+						if type(k) == "number" and k <= N then
+							-- Already output in previous loop.
+						else
+							output[#output + 1] = format("%s=%s", k, v)
+						end
+					end
+					output[#output + 1] = ")"
+					local hash = tconcat(output, " ")
+					self_outputPool:Release(output)
+
+					node.functionHash = hash
+					functionHash[hash] = functionHash[hash] or node
+				end
+			end
+			ast.annotation.functionHash = functionHash
+		end
+
+		-- Walk the AST and search for child nodes that are function nodes and
+		-- replace with a reference to the hashed node.
+		if ast.annotation.functionHash and ast.annotation.nodeList then
+			local functionHash = ast.annotation.functionHash
+			for _, node in ipairs(ast.annotation.nodeList) do
+				if node.child then
+					for k, childNode in ipairs(node.child) do
+						if childNode.functionHash then
+							node.child[k] = functionHash[childNode.functionHash]
+						end
+					end
+				end
+			end
+		end
+	end
+	profiler.Stop("OvaleAST_CommonFunctionElimination")
+end
+--</public-static-methods>
diff --git a/tests/ast.t b/tests/ast.t
new file mode 100644
index 0000000..195c9f6
--- /dev/null
+++ b/tests/ast.t
@@ -0,0 +1,55 @@
+--[[------------------------------
+	Load fake WoW environment.
+--]]------------------------------
+local root = "../"
+do
+	local state = {
+		class = "SHAMAN",
+		level = 90,
+	}
+	dofile(root .. "WoWAPI.lua")
+	WoWAPI:Initialize("Ovale", state)
+	WoWAPI:ExportSymbols()
+end
+
+--[[-----------------------------------------------
+	Fake loading via file order from Ovale.toc.
+--]]-----------------------------------------------
+do
+	local addonFiles = {
+		"Ovale.lua",
+		-- Profiling module.
+		"Profiler.lua",
+		-- Utility modules.
+		"OvalePool.lua",
+		"OvaleQueue.lua",
+		-- Core modules.
+		"OvaleAST.lua",
+		"OvaleCondition.lua",
+		"OvaleLexer.lua",
+		"OvaleScripts.lua",
+		-- Additional modules.
+		"scripts/files.xml",
+	}
+	for _, file in ipairs(addonFiles) do
+		WoWAPI:LoadAddonFile(file, root)
+	end
+
+	local AceAddon = LibStub("AceAddon-3.0")
+	AceAddon:ADDON_LOADED()
+end
+
+local OvaleAST = Ovale.OvaleAST
+local separator = string.rep("-", 80)
+
+-- Parse the default Ovale script for the class.
+local ast = OvaleAST:ParseScript("Ovale", { verify = false })
+if ast then
+	OvaleAST:Optimize(ast)
+	Ovale:Print(OvaleAST:NodeToString(ast))
+	Ovale:Print(separator)
+	Ovale:Print(OvaleAST:Unparse(ast))
+	OvaleAST:Release(ast)
+end
+Ovale:Print(separator)
+OvaleAST:Debug()