Quantcast

Update Ovale to use abstract syntax trees.

Johnny C. Lam [07-13-14 - 11:31]
Update Ovale to use abstract syntax trees.

OvaleCompile has been restructured to only evaluate the declarations in
the script and populate any databases used by other modules.  It makes a
distinction between compiling the script (using OvaleAST) and evaluating
the script.  Compiling the script only needs to happen when the script
code changes.  All other events only require re-evaluating the script.

OvaleBestAction has been restructured to more readily cache computed
results of nodes during each update cycle.  Combined with the common
function elimination optimization in OvaleAST, this allows for all
function calls to be computed only once per AddIcon group.

Other miscellaenous changes to modules were made to update to correct
method names and to remove unused code.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1543 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
OvaleBestAction.lua
OvaleCompile.lua
OvaleFrame.lua
OvaleOptions.lua
conditions/Damage.lua
conditions/LastEstimatedDamage.lua
diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua
index 3fcfa05..29d1824 100644
--- a/OvaleBestAction.lua
+++ b/OvaleBestAction.lua
@@ -9,7 +9,7 @@
 --]]--------------------------------------------------------------------

 local _, Ovale = ...
-local OvaleBestAction = Ovale:NewModule("OvaleBestAction")
+local OvaleBestAction = Ovale:NewModule("OvaleBestAction", "AceEvent-3.0")
 Ovale.OvaleBestAction = OvaleBestAction

 --<private-static-properties>
@@ -17,7 +17,9 @@ local OvalePool = Ovale.OvalePool
 local OvaleTimeSpan = Ovale.OvaleTimeSpan

 -- Forward declarations for module dependencies.
+local OvaleAST = nil
 local OvaleActionBar = nil
+local OvaleCompile = nil
 local OvaleCondition = nil
 local OvaleCooldown = nil
 local OvaleData = nil
@@ -103,423 +105,718 @@ end

 local OVALE_DEFAULT_PRIORITY = 3

+-- Table of node types to visitor methods.
+local COMPUTE_VISITOR = {
+	["action"] = "ComputeAction",
+	["arithmetic"] = "ComputeArithmetic",
+	["compare"] = "ComputeCompare",
+	["custom_function"] = "ComputeCustomFunction",
+	["function"] = "ComputeFunction",
+	["group"] = "ComputeGroup",
+	["if"] = "ComputeIf",
+	["logical"] = "ComputeLogical",
+	["lua"] = "ComputeLua",
+	["unless"] = "ComputeIf",
+	["value"] = "ComputeValue",
+	["wait"] = "ComputeWait",
+}
+
 -- Age of the current computation.
 local self_serial = 0
+
 -- Pool of time-span tables.
-local self_pool = OvalePool("OvaleBestAction_pool")
+local self_timeSpanPool = OvalePool("OvaleBestAction_timeSpanPool")
+-- timeSpan[node] = computed time span for that node.
+local self_timeSpan = {}
+
+-- Pool of value nodes for results.
+local self_valuePool = OvalePool("OvaleBestAction_valuePool")
+-- value[node] = result node of that node.
+local self_value = {}
 --</private-static-properties>

 --<private-static-methods>
-local function PutValue(element, value, origin, rate)
-	if not element.result then
-		element.result = { type = "value" }
+local function SetValue(node, value, origin, rate)
+	-- Re-use existing result.
+	local result = self_value[node]
+	if not result then
+		result = self_valuePool:Get()
+		self_value[node] = result
 	end
-	local result = element.result
-	result.value = value
-	result.origin = origin
-	result.rate = rate
+	-- Overwrite any pre-existing values.
+	result.type = "value"
+	result.value = value or 0
+	result.origin = origin or 0
+	result.rate = rate or 0
 	return result
 end

-local function ComputeAction(element, state)
-	profiler.Start("OvaleBestAction_ComputeAction")
-	local self = OvaleBestAction
-	local action = element.params[1]
+local function GetTimeSpan(node)
+	local timeSpan = self_timeSpan[node]
+	if timeSpan then
+		timeSpan:Reset()
+	else
+		timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
+		self_timeSpan[node] = timeSpan
+	end
+	return timeSpan
+end
+
+local function GetActionItemInfo(element, state, target)
+	profiler.Start("OvaleBestAction_GetActionItemInfo")
+
 	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
-		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId = self:GetActionInfo(element, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId

-	if not actionTexture then
-		Ovale:Logf("Action %s not found", action)
-		profiler.Stop("OvaleBestAction_ComputeAction")
-		return timeSpan
-	elseif not (actionEnable and actionEnable > 0) then
-		Ovale:Logf("Action %s not enabled", action)
-		profiler.Stop("OvaleBestAction_ComputeAction")
-		return timeSpan
-	elseif element.params.usable == 1 and not actionUsable then
-		Ovale:Logf("Action %s not usable", action)
-		profiler.Stop("OvaleBestAction_ComputeAction")
-		return timeSpan
+	local itemId = element.params[1]
+	if type(itemId) ~= "number" then
+		itemId = OvaleEquipement:GetEquippedItem(itemId)
 	end
+	if not itemId then
+		Ovale:Logf("Unknown item '%s'.", element.params[1])
+	else
+		Ovale:Logf("Item ID '%s'", itemId)
+		local action = OvaleActionBar:GetForItem(itemId)
+		local spellName = API_GetItemSpell(itemId)

-	-- Set the cast time of the action.
-	if actionType == "spell" then
-		local spellId = actionId
-		local si = spellId and OvaleData.spellInfo[spellId]
-		if si and si.casttime then
-			element.castTime = si.casttime
-		else
-			local _, _, _, _, _, _, castTime = API_GetSpellInfo(spellId)
-			if castTime then
-				element.castTime = castTime / 1000
-			else
-				element.castTime = nil
-			end
+		-- Use texture specified in the action if given.
+		if element.params.texture then
+			actionTexture = "Interface\\Icons\\" .. element.params.texture
 		end
-	else
-		element.castTime = 0
+		actionTexture = actionTexture or API_GetItemIcon(itemId)
+		actionInRange = API_IsItemInRange(itemId, target)
+		actionCooldownStart, actionCooldownDuration, actionEnable = API_GetItemCooldown(itemId)
+		actionUsable = spellName and API_IsUsableItem(itemId)
+		if action then
+			actionShortcut = OvaleActionBar:GetBinding(action)
+			actionIsCurrent = API_IsCurrentAction(action)
+		end
+		actionType = "item"
+		actionId = itemId
 	end

-	-- If the action is not on cooldown, then treat it like it's immediately ready.
-	local start
-	if actionCooldownDuration and actionCooldownStart and actionCooldownStart > 0 then
-		start = actionCooldownDuration + actionCooldownStart
+	profiler.Stop("OvaleBestAction_GetActionItemInfo")
+	return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target
+end
+
+local function GetActionMacroInfo(element, state, target)
+	profiler.Start("OvaleBestAction_GetActionMacroInfo")
+
+	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId
+
+	local macro = element.params[1]
+	local action = OvaleActionBar:GetForMacro(macro)
+	if not action then
+		Ovale:Logf("Unknown macro '%s'.", macro)
 	else
-		start = state.currentTime
+		-- Use texture specified in the action if given.
+		if element.params.texture then
+			actionTexture = "Interface\\Icons\\" .. element.params.texture
+		end
+		actionTexture = actionTexture or API_GetActionTexture(action)
+		actionInRange = API_IsActionInRange(action, target)
+		actionCooldownStart, actionCooldownDuration, actionEnable = API_GetActionCooldown(action)
+		actionUsable = API_IsUsableAction(action)
+		actionShortcut = OvaleActionBar:GetBinding(action)
+		actionIsCurrent = API_IsCurrentAction(action)
+		actionType = "macro"
+		actionId = macro
 	end

-	Ovale:Logf("start=%f nextCast=%s [%d]", start, state.nextCast, element.nodeId)
+	profiler.Stop("OvaleBestAction_GetActionMacroInfo")
+	return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target
+end

-	-- If the action is available before the end of the current spellcast, then wait until we can first cast the action.
-	if start < state.nextCast then
-		-- Default to starting at next available cast time.
-		local newStart = state.nextCast
-		-- If we are currently channeling a spellcast, then see if it is interruptible.
-		-- If we are allowed to interrupt it, then start after the next tick of the channel.
-		if state.isChanneling then
-			local spellId = state.currentSpellId
-			local si = spellId and OvaleData.spellInfo[spellId]
-			if si then
-				-- "channel=N" means that the channel has N total ticks and can be interrupted.
-				local channel = si.channel or si.canStopChannelling
-				if channel then
-					local hasteMultiplier = 1
-					if si.haste == "spell" then
-						hasteMultiplier = state:GetSpellHasteMultiplier()
-					elseif si.haste == "melee" then
-						hasteMultiplier = state:GetMeleeHasteMultiplier()
-					end
-					local numTicks = floor(channel * hasteMultiplier + 0.5)
-					local tick = (state.nextCast - state.startCast) / numTicks
-					local tickTime = state.startCast
-					for i = 1, numTicks do
-						tickTime = tickTime + tick
-						if start <= tickTime then
+local function GetActionSpellInfo(element, state, target)
+	profiler.Start("OvaleBestAction_GetActionSpellInfo")
+
+	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId
+
+	local spellId = element.params[1]
+	local action = OvaleActionBar:GetForSpell(spellId)
+	if not OvaleSpellBook:IsKnownSpell(spellId) and not action then
+		Ovale:Logf("Unknown spell ID '%s'.", spellId)
+	else
+		-- Use texture specified in the action if given.
+		if element.params.texture then
+			actionTexture = "Interface\\Icons\\" .. element.params.texture
+		end
+		actionTexture = actionTexture or API_GetSpellTexture(spellId)
+		actionInRange = API_IsSpellInRange(OvaleSpellBook:GetSpellName(spellId), target)
+		actionCooldownStart, actionCooldownDuration, actionEnable = state:GetSpellCooldown(spellId)
+		actionUsable = OvaleSpellBook:IsUsableSpell(spellId)
+		if action then
+			actionShortcut = OvaleActionBar:GetBinding(action)
+			actionIsCurrent = API_IsCurrentAction(action)
+		end
+		actionType = "spell"
+		actionId = spellId
+
+		local si = OvaleData.spellInfo[spellId]
+		if si then
+			-- Verify that the spell may be cast given restrictions specified in SpellInfo().
+			local meetsRequirements = true
+			if si.stance and not OvaleStance:IsStance(si.stance) then
+				Ovale:Logf("Spell ID '%s' requires the player to be in stance '%s'", spellId, si.stance)
+				meetsRequirements = false
+			elseif si.combo then
+				-- Spell requires combo points.
+				local cost = state:ComboPointCost(spellId)
+				if state.combo < cost then
+					Ovale:Logf("Spell ID '%s' requires at least %d combo points.", spellId, cost)
+					meetsRequirements = false
+				end
+			else
+				for powerType in pairs(OvalePower.SECONDARY_POWER) do
+					if si[powerType] then
+						-- Spell requires "secondary" resources, e.g., chi, focus, rage, etc.,
+						local cost = state:PowerCost(spellId, powerType)
+						if state[powerType] < cost then
+							Ovale:Logf("Spell ID '%s' requires at least %d %s.", spellId, cost, powerType)
+							meetsRequirements = false
 							break
 						end
 					end
-					newStart = tickTime
-					Ovale:Logf("%s start=%f, numTicks=%d, tick=%f, tickTime=%f", spellId, newStart, numTicks, tick, tickTime)
 				end
 			end
+
+			-- Fix spell cooldown information using primary resource requirements specified in SpellInfo().
+			if actionCooldownStart and actionCooldownDuration then
+				-- Get the maximum time before all "primary" resources are ready.
+				local atTime = state.currentTime
+				for powerType in pairs(OvalePower.PRIMARY_POWER) do
+					if si[powerType] then
+						local t = state.currentTime + state:TimeToPower(spellId, powerType)
+						if atTime < t then
+							atTime = t
+						end
+					end
+				end
+				if actionCooldownStart > 0 then
+					if atTime > actionCooldownStart + actionCooldownDuration then
+						Ovale:Logf("Delaying spell ID '%s' for primary resource.", spellId)
+						actionCooldownDuration = atTime - actionCooldownStart
+					end
+				else
+					actionCooldownStart = state.currentTime
+					actionCooldownDuration = atTime - actionCooldownStart
+				end
+
+				if si.blood or si.frost or si.unholy or si.death then
+					-- Spell requires runes.
+					local needRunes = true
+					-- "buff_runes_none" is the spell ID of the buff that makes casting the spell cost no runes.
+					local buffNoRunes = si.buff_runes_none
+					if buffNoRunes then
+						local aura = state:GetAura("player", buffNoRunes)
+						if state:IsActiveAura(aura) then
+							needRunes = false
+						end
+					end
+					if needRunes then
+						local ending = state.currentTime + state:GetRunesCooldown(si.blood, si.unholy, si.frost, si.death)
+						if ending > actionCooldownStart + actionCooldownDuration then
+							actionCooldownDuration = ending - actionCooldownStart
+						end
+					end
+				end
+			end
+
+			-- Use texture specified in the SpellInfo() if given.
+			if si.texture then
+				actionTexture = "Interface\\Icons\\" .. si.texture
+			end
+
+			if not meetsRequirements then
+				-- Assign all return values to nil.
+				actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+					actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target = nil
+			end
 		end
-		start = newStart
 	end
-	Ovale:Logf("Action %s can start at %f", action, start)
-	timeSpan[1], timeSpan[2] = start, math.huge

-	--[[
-		Allow for the return value of an to be "typecast" to a constant value by specifying
-		asValue=1 as a parameter.
+	profiler.Stop("OvaleBestAction_GetActionSpellInfo")
+	return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target
+end

-		Return 1 if the action is off of cooldown, or 0 if it is on cooldown.
-	--]]
-	local value
-	if element.params.asValue and element.params.asValue == 1 then
-		local atTime = state.currentTime
-		if HasTime(timeSpan, atTime) then
-			value = 1
+local function GetActionTextureInfo(element, state, target)
+	profiler.Start("OvaleBestAction_GetActionTextureInfo")
+
+	local texture = element.params[1]
+	local actionTexture = "Interface\\Icons\\" .. texture
+	local actionInRange = nil
+	local actionCooldownStart = 0
+	local actionCooldownDuration = 0
+	local actionEnable = 1
+	local actionUsable = true
+	local actionShortcut = nil
+	local actionIsCurrent = nil
+	local actionType = "texture"
+	local actionId = texture
+
+	profiler.Stop("OvaleBestAction_GetActionTextureInfo")
+	return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target
+end
+--</private-static-methods>
+
+--<public-static-methods>
+function OvaleBestAction:OnInitialize()
+	-- Resolve module dependencies.
+	OvaleAST = Ovale.OvaleAST
+	OvaleActionBar = Ovale.OvaleActionBar
+	OvaleCompile = Ovale.OvaleCompile
+	OvaleCondition = Ovale.OvaleCondition
+	OvaleCooldown = Ovale.OvaleCooldown
+	OvaleData = Ovale.OvaleData
+	OvaleEquipement = Ovale.OvaleEquipement
+	OvaleFuture = Ovale.OvaleFuture
+	OvalePower = Ovale.OvalePower
+	OvaleSpellBook = Ovale.OvaleSpellBook
+	OvaleStance = Ovale.OvaleStance
+end
+
+function OvaleBestAction:OnEnable()
+	self:RegisterMessage("Ovale_ScriptChanged")
+end
+
+function OvaleBestAction:OnDisable()
+	self:UnregisterMessage("Ovale_ScriptChanged")
+end
+
+function OvaleBestAction:Ovale_ScriptChanged()
+	-- Clean-up tables that are referenced using obsolete nodes as keys.
+	for node, timeSpan in pairs(self_timeSpan) do
+		self_timeSpanPool:Release(timeSpan)
+		self_timeSpan[node] = nil
+	end
+	for node, value in pairs(self_value) do
+		self_valuePool:Release(value)
+		self_value[node] = nil
+	end
+end
+
+function OvaleBestAction:StartNewAction(state)
+	state:Reset()
+	OvaleFuture:ApplyInFlightSpells(state)
+	self_serial = self_serial + 1
+end
+
+function OvaleBestAction:GetActionInfo(element, state)
+	if element and element.type == "action" then
+		local target = element.params.target or OvaleCondition.defaultTarget
+		if element.lowername == "item" then
+			return GetActionItemInfo(element, state, target)
+		elseif element.lowername == "macro" then
+			return GetActionMacroInfo(element, state, target)
+		elseif element.lowername == "spell" then
+			return GetActionSpellInfo(element, state, target)
+		elseif element.lowername == "texture" then
+			return GetActionTextureInfo(element, state, target)
+		end
+	end
+	return nil
+end
+
+function OvaleBestAction:Compute(element, state)
+	local timeSpan, priority, result
+	if element then
+		if element.asString then
+			Ovale:Logf("[%d] >>> Computing '%s': %s", element.nodeId, element.type, element.asString)
 		else
-			value = 0
+			Ovale:Logf("[%d] >>> Computing '%s'", element.nodeId, element.type)
+		end
+		-- Check for recently cached computation results.
+		if element.serial and element.serial >= self_serial then
+			timeSpan = element.timeSpan
+			priority = element.priority
+			result = element.result
+			Ovale:Logf("[%d]    using cached result (age = %d)", element.nodeId, element.serial)
+		else
+			local visitor = COMPUTE_VISITOR[element.type]
+			if visitor and self[visitor] then
+				timeSpan, priority, result = self[visitor](self, element, state)
+				element.serial = self_serial
+				element.timeSpan = timeSpan
+				element.priority = priority
+				element.result = result
+			else
+				Ovale:Logf("[%d] Runtime error: unable to compute node of type '%s'.", element.nodeId, element.type)
+			end
+		end
+		if result and result.type == "value" then
+			local value, origin, rate = result.value, result.origin, result.rate
+			Ovale:Logf("[%d] <<< '%s' returns %s with value = %f, %f, %f", element.nodeId, element.type, tostring(timeSpan), value, origin, rate)
+		elseif result and result.nodeId then
+			Ovale:Logf("[%d] <<< '%s' returns [%d] %s", element.nodeId, element.type, result.nodeId, tostring(timeSpan))
+		else
+			Ovale:Logf("[%d] <<< '%s' returns %s", element.nodeId, element.type, tostring(timeSpan))
 		end
-		timeSpan[1], timeSpan[2] = 0, math.huge
 	end
+	return timeSpan, priority, result
+end

-	local priority = element.params.priority or OVALE_DEFAULT_PRIORITY
-	if value then
-		local result = PutValue(element, value, 0, 0)
-		profiler.Stop("OvaleBestAction_ComputeAction")
-		return timeSpan, priority, result
+function OvaleBestAction:ComputeBool(element, state)
+	local timeSpan, _, newElement = self:Compute(element, state)
+	-- Match SimulationCraft: 0 is false, non-zero is true.
+	--	(https://code.google.com/p/simulationcraft/wiki/ActionLists#Logical_operators)
+	if newElement and newElement.type == "value" and newElement.value == 0 and newElement.rate == 0 then
+		return nil
 	else
-		profiler.Stop("OvaleBestAction_ComputeAction")
-		return timeSpan, priority, element
+		return timeSpan
 	end
 end

-local function ComputeAnd(element, state)
-	profiler.Start("OvaleBestAction_Compute")
-	Ovale:Logf("%s [%d]", element.type, element.nodeId)
-	local self = OvaleBestAction
-	local timeSpanA = self:ComputeBool(element.a, state)
-	local timeSpan = element.timeSpan
-
-	-- Short-circuit evaluation of left argument to AND.
-	if Measure(timeSpanA) == 0 then
-		timeSpan:Reset(timeSpanA)
+function OvaleBestAction:ComputeAction(element, state)
+	profiler.Start("OvaleBestAction_ComputeAction")
+	local nodeId = element.nodeId
+	local timeSpan = GetTimeSpan(element)
+	local priority, result
+
+	Ovale:Logf("[%d]    evaluating action: %s(%s)", element.nodeId, element.name, element.paramsAsString)
+	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
+		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId = self:GetActionInfo(element, state)
+
+	local action = element.params[1]
+	if not actionTexture then
+		Ovale:Logf("[%s]    Action %s not found.", nodeId, action)
+	elseif not (actionEnable and actionEnable > 0) then
+		Ovale:Logf("[%s]    Action %s not enabled.", nodeId, action)
+	elseif element.params.usable == 1 and not actionUsable then
+		Ovale:Logf("[%s]    Action %s not usable.", nodeId, action)
 	else
-		local timeSpanB = self:ComputeBool(element.b, state)
-		-- Take intersection of A and B.
-		timeSpan:Reset()
-		Intersect(timeSpanA, timeSpanB, timeSpan)
+		-- Set the cast time of the action.
+		if actionType == "spell" then
+			local spellId = actionId
+			local si = spellId and OvaleData.spellInfo[spellId]
+			if si and si.casttime then
+				element.castTime = si.casttime
+			else
+				local _, _, _, _, _, _, castTime = API_GetSpellInfo(spellId)
+				if castTime then
+					element.castTime = castTime / 1000
+				else
+					element.castTime = nil
+				end
+			end
+		else
+			element.castTime = 0
+		end
+
+		-- If the action is not on cooldown, then treat it like it's immediately ready.
+		local start
+		if actionCooldownDuration and actionCooldownStart and actionCooldownStart > 0 then
+			start = actionCooldownDuration + actionCooldownStart
+		else
+			start = state.currentTime
+		end
+
+		Ovale:Logf("[%d]    start=%f nextCast=%s", nodeId, start, state.nextCast)
+
+		-- If the action is available before the end of the current spellcast, then wait until we can first cast the action.
+		if start < state.nextCast then
+			-- Default to starting at next available cast time.
+			local newStart = state.nextCast
+			-- If we are currently channeling a spellcast, then see if it is interruptible.
+			-- If we are allowed to interrupt it, then start after the next tick of the channel.
+			if state.isChanneling then
+				local spellId = state.currentSpellId
+				local si = spellId and OvaleData.spellInfo[spellId]
+				if si then
+					-- "channel=N" means that the channel has N total ticks and can be interrupted.
+					local channel = si.channel or si.canStopChannelling
+					if channel then
+						local hasteMultiplier = 1
+						if si.haste == "spell" then
+							hasteMultiplier = state:GetSpellHasteMultiplier()
+						elseif si.haste == "melee" then
+							hasteMultiplier = state:GetMeleeHasteMultiplier()
+						end
+						local numTicks = floor(channel * hasteMultiplier + 0.5)
+						local tick = (state.nextCast - state.startCast) / numTicks
+						local tickTime = state.startCast
+						for i = 1, numTicks do
+							tickTime = tickTime + tick
+							if start <= tickTime then
+								break
+							end
+						end
+						newStart = tickTime
+						Ovale:Logf("[%d]    %s start=%f, numTicks=%d, tick=%f, tickTime=%f", nodeId, spellId, newStart, numTicks, tick, tickTime)
+					end
+				end
+			end
+			start = newStart
+		end
+		Ovale:Logf("[%d]    Action %s can start at %f.", nodeId, action, start)
+		timeSpan[1], timeSpan[2] = start, math.huge
+
+		--[[
+			Allow for the return value of an to be "typecast" to a constant value by specifying
+			asValue=1 as a parameter.
+
+			Return 1 if the action is off of cooldown, or 0 if it is on cooldown.
+		--]]
+		local value
+		if element.params.asValue == 1 then
+			local atTime = state.currentTime
+			local value = HasTime(timeSpan, atTime) and 1 or 0
+			result = SetValue(element, value)
+			timeSpan[1], timeSpan[2] = 0, math.huge
+			Ovale:Logf("[%d]    Action %s typecast to value %f.", nodeId, action, value)
+		else
+			result = element
+		end
+		priority = element.params.priority or OVALE_DEFAULT_PRIORITY
 	end
-	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
-	profiler.Stop("OvaleBestAction_Compute")
-	return timeSpan
+
+	profiler.Stop("OvaleBestAction_ComputeAction")
+	return timeSpan, priority, result
 end

-local function ComputeArithmetic(element, state)
+function OvaleBestAction:ComputeArithmetic(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	local self = OvaleBestAction
-	local timeSpanA, _, elementA = self:Compute(element.a, state)
-	local timeSpanB, _, elementB = self:Compute(element.b, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
+	local timeSpanA, _, elementA = self:Compute(element.child[1], state)
+	local timeSpanB, _, elementB = self:Compute(element.child[2], state)
+	local timeSpan = GetTimeSpan(element)
+	local result

 	-- Take intersection of A and B.
 	Intersect(timeSpanA, timeSpanB, timeSpan)
 	if Measure(timeSpan) == 0 then
-		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
-		local result = PutValue(element, 0, 0, 0)
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan, OVALE_DEFAULT_PRIORITY, result
-	end
-
-	--[[
-		A(t) = a + (t - b)*c
-		B(t) = x + (t - y)*z
-
-		Silently "typecast" non-values to a constant value of 0.
-	--]]
-	local a = elementA and elementA.value or 0
-	local b = elementA and elementA.origin or 0
-	local c = elementA and elementA.rate or 0
-	local x = elementB and elementB.value or 0
-	local y = elementB and elementB.origin or 0
-	local z = elementB and elementB.rate or 0
-	local atTime = state.currentTime
+		Ovale:Logf("[%d]    arithmetic '%s' returns %s with zero measure", element.nodeId, element.operator, tostring(timeSpan))
+		result = SetValue(element, 0)
+	else
+		--[[
+			A(t) = a + (t - b)*c
+			B(t) = x + (t - y)*z

-	Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, element.operator, x, y, z, element.nodeId)
+			Silently "typecast" non-values to a constant value of 0.
+		--]]
+		local a = elementA and elementA.value or 0
+		local b = elementA and elementA.origin or 0
+		local c = elementA and elementA.rate or 0
+		local x = elementB and elementB.value or 0
+		local y = elementB and elementB.origin or 0
+		local z = elementB and elementB.rate or 0
+		local operator = element.operator
+		local atTime = state.currentTime

-	-- result(t) = l + (t - m) * n
-	local l, m, n
+		Ovale:Logf("[%d]    %f+(t-%f)*%f %s %f+(t-%f)*%f", element.nodeId, a, b, c, operator, x, y, z)

-	--[[
-		A(t) = a + (t - b)*c = a + (t - t0 + t0 - b)*c = [a + (t0 - b)*c] + (t - t0)*c = A(t0) + (t - t0)*c
-		B(t) = x + (t - y)*z = x + (t - t0 + t0 - y)*z = [x + (t0 - y)*z] + (t - t0)*z = B(t0) + (t - t0)*z
-	--]]
-	local A = a + (atTime - b)*c
-	local B = x + (atTime - y)*z
+		-- result(t) = l + (t - m)*n
+		local l, m, n

-	if element.operator == "+" then
 		--[[
-			A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
-			B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
-
-			A(t) + B(t) = (A + B) + (t - t0)*(c + z)
+			A(t) = a + (t - b)*c = a + (t - t0 + t0 - b)*c = [a + (t0 - b)*c] + (t - t0)*c = A(t0) + (t - t0)*c
+			B(t) = x + (t - y)*z = x + (t - t0 + t0 - y)*z = [x + (t0 - y)*z] + (t - t0)*z = B(t0) + (t - t0)*z
 		--]]
-		l = A + B
-		m = atTime
-		n = c + z
-	elseif element.operator == "-" then
-		--[[
-			A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
-			B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
+		local A = a + (atTime - b)*c
+		local B = x + (atTime - y)*z

-			A(t) - B(t) = (A - B) + (t - t0)*(c - z)
-		--]]
-		l = A - B
-		m = atTime
-		n = c - z
-	elseif element.operator == "*" then
-		--[[
-				 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
-				 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
-			A(t)*B(t) = A*B + (t - t0)*[A*z + B*c] + [(t - t0)^2]*(c*z)
-					  = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere.
-		--]]
-			l = A*B
+		if operator == "+" then
+			--[[
+				A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
+				B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
+
+				A(t) + B(t) = (A + B) + (t - t0)*(c + z)
+			--]]
+			l = A + B
 			m = atTime
-			n = A*z + B*c
-	elseif element.operator == "/" then
-		--[[
-				 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
-				 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
-			A(t)/B(t) = A/B + (t - t0)*[(B*c - A*z)/B^2] + O(t^2) converges when |t - t0| < |B/z|.
-		--]]
-		l = A/B
-		m = atTime
-		n = (B*c - A*z)/(B^2)
-		local bound
-		if z == 0 then
-			bound = math.huge
-		else
-			bound = abs(B/z)
-		end
-		local scratch = OvaleTimeSpan(self_pool:Get())
-		scratch:Reset(timeSpan)
-		timeSpan:Reset()
-		IntersectInterval(scratch, atTime - bound, atTime + bound, timeSpan)
-		self_pool:Release(scratch)
-	elseif element.operator == "%" then
-		if c == 0 and z == 0 then
-			l = A % B
+			n = c + z
+		elseif operator == "-" then
+			--[[
+				A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
+				B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
+
+				A(t) - B(t) = (A - B) + (t - t0)*(c - z)
+			--]]
+			l = A - B
 			m = atTime
-			n = 0
-		else
-			Ovale:Error("Parameters of % must be constants")
-			l = 0
-			m = 0
-			n = 0
+			n = c - z
+		elseif operator == "*" then
+			--[[
+					 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
+					 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
+				A(t)*B(t) = A*B + (t - t0)*[A*z + B*c] + [(t - t0)^2]*(c*z)
+						  = A*B + (t - t0)*[A*z + B*c] + O(t^2) converges everywhere.
+			--]]
+				l = A*B
+				m = atTime
+				n = A*z + B*c
+		elseif operator == "/" then
+			--[[
+					 A(t) = A(t0) + (t - t0)*c = A + (t - t0)*c
+					 B(t) = B(t0) + (t - t0)*z = B + (t - t0)*z
+				A(t)/B(t) = A/B + (t - t0)*[(B*c - A*z)/B^2] + O(t^2) converges when |t - t0| < |B/z|.
+			--]]
+			l = A/B
+			m = atTime
+			n = (B*c - A*z)/(B^2)
+			local bound
+			if z == 0 then
+				bound = math.huge
+			else
+				bound = abs(B/z)
+			end
+			local scratch = OvaleTimeSpan(self_timeSpanPool:Get())
+			scratch:Reset(timeSpan)
 			timeSpan:Reset()
+			IntersectInterval(scratch, atTime - bound, atTime + bound, timeSpan)
+			self_timeSpanPool:Release(scratch)
+		elseif operator == "%" then
+			if c == 0 and z == 0 then
+				l = A % B
+				m = atTime
+				n = 0
+			else
+				Ovale:Errorf("[%d]    Parameters of modulus operator '%' must be constants.", element.nodeId)
+				l = 0
+				m = 0
+				n = 0
+			end
 		end
+		Ovale:Logf("[%d]    arithmetic '%s' returns %f+(t-%f)*%f", element.nodeId, operator, l, m, n)
+		result = SetValue(element, l, m, n)
 	end
-	Ovale:Logf("result = %f+(t-%f)*%f [%d]", l, m, n, element.nodeId)
-	local result = PutValue(element, l, m, n)
 	profiler.Stop("OvaleBestAction_Compute")
 	return timeSpan, OVALE_DEFAULT_PRIORITY, result
 end

-local function ComputeCompare(element, state)
+function OvaleBestAction:ComputeCompare(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	local self = OvaleBestAction
-	local timeSpanA, _, elementA = self:Compute(element.a, state)
-	local timeSpanB, _, elementB = self:Compute(element.b, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
+	local timeSpanA, _, elementA = self:Compute(element.child[1], state)
+	local timeSpanB, _, elementB = self:Compute(element.child[2], state)
+	local timeSpan = GetTimeSpan(element)

 	-- Take intersection of A and B.
 	Intersect(timeSpanA, timeSpanB, timeSpan)
 	if Measure(timeSpan) == 0 then
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan
-	end
-
-	--[[
-		A(t) = a + (t - b)*c
-		B(t) = x + (t - y)*z
+		Ovale:Logf("[%d]    compare '%s' returns %s with zero measure", element.nodeId, element.operator, tostring(timeSpan))
+	else
+		--[[
+			A(t) = a + (t - b)*c
+			B(t) = x + (t - y)*z

-		Silently "typecast" non-values to a constant value of 0.
-	--]]
-	local a = elementA and elementA.value or 0
-	local b = elementA and elementA.origin or 0
-	local c = elementA and elementA.rate or 0
-	local x = elementB and elementB.value or 0
-	local y = elementB and elementB.origin or 0
-	local z = elementB and elementB.rate or 0
-	local operator = element.operator
+			Silently "typecast" non-values to a constant value of 0.
+		--]]
+		local a = elementA and elementA.value or 0
+		local b = elementA and elementA.origin or 0
+		local c = elementA and elementA.rate or 0
+		local x = elementB and elementB.value or 0
+		local y = elementB and elementB.origin or 0
+		local z = elementB and elementB.rate or 0
+		local operator = element.operator

-	Ovale:Logf("%f+(t-%f)*%f %s %f+(t-%f)*%f [%d]", a, b, c, operator, x, y, z, element.nodeId)
+		Ovale:Logf("[%d]    %f+(t-%f)*%f %s %f+(t-%f)*%f", element.nodeId, a, b, c, operator, x, y, z)

-	--[[
-				 A(t) = B(t)
-		a + (t - b)*c = x + (t - y)*z
-		a + t*c - b*c = x + t*z - y*z
-			t*c - t*z = (x - y*z) - (a - b*c)
-			t*(c - z) = B(0) - A(0)
-	--]]
-	local A = a - b*c
-	local B = x - y*z
-	if c == z then
-		if not ((operator == "==" and A == B)
-				or (operator == "<" and A < B)
-				or (operator == "<=" and A <= B)
-				or (operator == ">" and A > B)
-				or (operator == ">=" and A >= B)) then
+		--[[
+					 A(t) = B(t)
+			a + (t - b)*c = x + (t - y)*z
+			a + t*c - b*c = x + t*z - y*z
+				t*c - t*z = (x - y*z) - (a - b*c)
+				t*(c - z) = B(0) - A(0)
+		--]]
+		local A = a - b*c
+		local B = x - y*z
+		if c == z then
+			if not ((operator == "==" and A == B)
+					or (operator == "!=" and A ~= B)
+					or (operator == "<" and A < B)
+					or (operator == "<=" and A <= B)
+					or (operator == ">" and A > B)
+					or (operator == ">=" and A >= B)) then
+				timeSpan:Reset()
+			end
+		else
+			local scratch = OvaleTimeSpan(self_timeSpanPool:Get())
+			scratch:Reset(timeSpan)
 			timeSpan:Reset()
+			local t = (B - A)/(c - z)
+			t = (t > 0) and t or 0
+			Ovale:Logf("[%d]    intersection at t = %f", element.nodeId, t)
+			if (c > z and operator == "<")
+					or (c > z and operator == "<=")
+					or (c < z and operator == ">")
+					or (c < z and operator == ">=") then
+				IntersectInterval(scratch, 0, t, timeSpan)
+			elseif (c < z and operator == "<")
+					or (c < z and operator == "<=")
+					or (c > z and operator == ">")
+					or (c > z and operator == ">=") then
+				IntersectInterval(scratch, t, math.huge, timeSpan)
+			end
+			self_timeSpanPool:Release(scratch)
 		end
-	else
-		local scratch = OvaleTimeSpan(self_pool:Get())
-		scratch:Reset(timeSpan)
-		timeSpan:Reset()
-		local t = (B - A)/(c - z)
-		t = (t > 0) and t or 0
-		Ovale:Logf("t = %f", t)
-		if (c > z and operator == "<")
-				or (c > z and operator == "<=")
-				or (c < z and operator == ">")
-				or (c < z and operator == ">=") then
-			IntersectInterval(scratch, 0, t, timeSpan)
-		elseif (c < z and operator == "<")
-				or (c < z and operator == "<=")
-				or (c > z and operator == ">")
-				or (c > z and operator == ">=") then
-			IntersectInterval(scratch, t, math.huge, timeSpan)
-		end
-		self_pool:Release(scratch)
+		Ovale:Logf("[%d]    compare '%s' returns %s", element.nodeId, operator, tostring(timeSpan))
 	end
-	Ovale:Logf("compare %s returns %s [%d]", operator, tostring(timeSpan), element.nodeId)
 	profiler.Stop("OvaleBestAction_Compute")
 	return timeSpan
 end

-local function ComputeCustomFunction(element, state)
+function OvaleBestAction:ComputeCustomFunction(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	Ovale:Logf("custom function %s", element.name)
-	local self = OvaleBestAction
-	if not element.serial or element.serial < self_serial then
-		-- Cache new values in element.
-		element.timeSpanA, element.priorityA, element.elementA = self:Compute(element.a, state)
-		element.serial = self_serial
-	else
-		Ovale:Logf("Using cached values for %s", element.name)
-	end
-
-	local timeSpanA, priorityA, elementA = element.timeSpanA, element.priorityA, element.elementA
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
-	if element.params.asValue and element.params.asValue == 1 then
-		--[[
-			Allow for the return value of a custom function to be "typecast" to a constant value.
-
-			If the return value is a time span (a "boolean" value), then if the current time of
-			the simulation is within the time span, then return 1, or 0 otherwise.
-
-			If the return value is a linear function, then if the current time of the simulation
-			is within the function's domain, then the function is simply evaluated at the current
-			time, or 0 otherwise.
-
-			If the return value is an action, then return 1 if the action is off of cooldown, or
-			0 if it is on cooldown.
-		--]]
-		local atTime = state.currentTime
-		local value = 0
-		if HasTime(timeSpanA, atTime) then
-			if not elementA then	-- boolean
-				value = 1
-			elseif elementA.type == "value" then
-				value = elementA.value + (atTime - elementA.origin) * elementA.rate
-			elseif elementA.type == "action" then
-				value = 1
+	local timeSpan = GetTimeSpan(element)
+	local priority, result
+
+	local node = OvaleCompile:GetFunctionNode(element.name)
+	if node then
+		Ovale:Logf("[%d]    evaluating function: %s(%s)", element.nodeId, node.name, node.paramsAsString)
+		local timeSpanA, priorityA, elementA = self:Compute(node.child[1], state)
+		if element.params.asValue == 1 or node.params.asValue == 1 then
+			--[[
+				Allow for the return value of a custom function to be "typecast" to a constant value.
+
+				If the return value is a time span (a "boolean" value), then if the current time of
+				the simulation is within the time span, then return 1, or 0 otherwise.
+
+				If the return value is a linear function, then if the current time of the simulation
+				is within the function's domain, then the function is simply evaluated at the current
+				time, or 0 otherwise.
+
+				If the return value is an action, then return 1 if the action is off of cooldown, or
+				0 if it is on cooldown.
+			--]]
+			local atTime = state.currentTime
+			local value = 0
+			if HasTime(timeSpanA, atTime) then
+				if not elementA then	-- boolean
+					value = 1
+				elseif elementA.type == "value" then
+					value = elementA.value + (atTime - elementA.origin) * elementA.rate
+				elseif elementA.type == "action" then
+					value = 1
+				end
 			end
+			Ovale:Logf("[%d]    function '%s' typecast to value %f", element.nodeId, element.name, value)
+			timeSpan[1], timeSpan[2] = 0, math.huge
+			result = SetValue(element, value)
+		else
+			CopyTimeSpan(timeSpanA, timeSpan)
+			result = elementA
 		end
-		timeSpan[1], timeSpan[2] = 0, math.huge
-		local result = PutValue(element, value, 0, 0)
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan, priorityA, result
-	else
-		CopyTimeSpan(timeSpanA, timeSpan)
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan, priorityA, elementA
 	end
+
+	profiler.Stop("OvaleBestAction_Compute")
+	return timeSpan, priority, result
 end

-local function ComputeFunction(element, state)
+function OvaleBestAction:ComputeFunction(element, state)
 	profiler.Start("OvaleBestAction_ComputeFunction")
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
-	if not OvaleCondition:IsCondition(element.func) then
-		Ovale:Errorf("Condition %s not found", element.func)
-		profiler.Stop("OvaleBestAction_ComputeFunction")
-		return timeSpan
-	end
+	local timeSpan = GetTimeSpan(element)
+	local priority, result

+	Ovale:Logf("[%d]    evaluating condition: %s(%s)", element.nodeId, element.name, element.paramsAsString)
 	local start, ending, value, origin, rate = OvaleCondition:EvaluateCondition(element.func, element.params)
 	if start and ending then
 		timeSpan[1], timeSpan[2] = start, ending
 	end
-
-	if Ovale.trace then
-		local conditionCall = element.func .. "("
-		for k, v in pairs(element.params) do
-			conditionCall = conditionCall .. k .. "=" .. v .. ","
-		end
-		conditionCall = conditionCall .. ")"
-		Ovale:FormatPrint("Condition %s returned %s, %s, %s, %s, %s", conditionCall, start, ending, value, origin, rate)
-	end
+	Ovale:Logf("[%d]    condition '%s' returns %s, %s, %s, %s, %s", element.nodeId, element.name, start, ending, value, origin, rate)

 	--[[
 		Allow for the return value of a script condition to be "typecast" to a constant value
@@ -532,7 +829,7 @@ local function ComputeFunction(element, state)
 		is within the function's domain, then the function is simply evaluated at the current
 		time, or 0 otherwise.
 	--]]
-	if element.params.asValue and element.params.asValue == 1 then
+	if element.params.asValue == 1 then
 		local atTime = state.currentTime
 		if HasTime(timeSpan, atTime) then
 			if value then
@@ -543,44 +840,33 @@ local function ComputeFunction(element, state)
 		else
 			value = 0
 		end
-		origin, rate = 0, 0
+		result = SetValue(element, value)
 		timeSpan[1], timeSpan[2] = 0, math.huge
+		priority = OVALE_DEFAULT_PRIORITY
+		Ovale:Logf("[%d]    condition '%s' typecast to value %f", element.nodeId, element.name, value)
+	elseif value then
+		result = SetValue(element, value, origin, rate)
 	end

-	if value then
-		local result = PutValue(element, value, origin, rate)
-		profiler.Stop("OvaleBestAction_ComputeFunction")
-		return timeSpan, OVALE_DEFAULT_PRIORITY, result
-	else
-		profiler.Stop("OvaleBestAction_ComputeFunction")
-		return timeSpan
-	end
+	profiler.Stop("OvaleBestAction_ComputeFunction")
+	return timeSpan, priority, result
 end

-local function ComputeGroup(element, state)
+function OvaleBestAction:ComputeGroup(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	local self = OvaleBestAction
 	local bestTimeSpan, bestPriority, bestElement, bestCastTime
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
+	local timeSpan = GetTimeSpan(element)

-	Ovale:Logf("%s [%d]", element.type, element.nodeId)
-
-	if #element.nodes == 1 then
-		profiler.Stop("OvaleBestAction_Compute")
-		return self:Compute(element.nodes[1], state)
-	end
+	local best = OvaleTimeSpan(self_timeSpanPool:Get())
+	local current = OvaleTimeSpan(self_timeSpanPool:Get())

-	local best = OvaleTimeSpan(self_pool:Get())
-	local current = OvaleTimeSpan(self_pool:Get())
-
-	for k, v in ipairs(element.nodes) do
-		local currentTimeSpan, currentPriority, currentElement = self:Compute(v, state)
+	for _, node in ipairs(element.child) do
+		local currentTimeSpan, currentPriority, currentElement = self:Compute(node, state)
 		-- We only care about actions that are available at time t > state.currentTime.
 		current:Reset()
 		IntersectInterval(currentTimeSpan, state.currentTime, math.huge, current)
 		if Measure(current) > 0 then
-			Ovale:Logf("    group checking %s [%d]", tostring(current), element.nodeId)
+			Ovale:Logf("[%d]    group checking %s", element.nodeId, tostring(current))
 			local currentCastTime
 			if currentElement then
 				currentCastTime = currentElement.castTime
@@ -590,24 +876,24 @@ local function ComputeGroup(element, state)
 				currentCastTime = gcd
 			end

-			local replace = false
+			local currentIsBetter = false
 			if Measure(best) == 0 then
-				Ovale:Logf("    group first best %s [%d]", tostring(current), element.nodeId)
-				replace = true
+				Ovale:Logf("[%d]    group first best is %s", element.nodeId, tostring(current))
+				currentIsBetter = true
 			elseif not currentPriority or not bestPriority or currentPriority == bestPriority then
 				-- If the spells have the same priority, then pick the one with an earlier cast time.
 				local threshold = (bestElement and bestElement.params) and bestElement.params.wait or 0
 				if best[1] - current[1] > threshold then
-					Ovale:Logf("    group new best %s [%d]", tostring(current), element.nodeId)
-					replace = true
+					Ovale:Logf("[%d]    group new best is %s", element.nodeId, tostring(current))
+					currentIsBetter = true
 				end
 			elseif currentPriority > bestPriority then
 				-- If the current spell has a higher priority than the best one found, then choose the
 				-- higher priority spell if its cast is pushed back too far by the lower priority one.
 				local threshold = (currentElement and currentElement.params) and currentElement.params.wait or (bestCastTime * 0.75)
 				if current[1] - best[1] < threshold then
-					Ovale:Logf("    group new best (lower prio) %s [%d]", tostring(current), element.nodeId)
-					replace = true
+					Ovale:Logf("[%d]    group new best (lower prio) is %s", element.nodeId, tostring(current))
+					currentIsBetter = true
 				end
 			elseif currentPriority < bestPriority then
 				-- If the current spell has a lower priority than the best one found, then choose the
@@ -615,11 +901,11 @@ local function ComputeGroup(element, state)
 				-- one by too much.
 				local threshold = (bestElement and bestElement.params) and bestElement.params.wait or (currentCastTime * 0.75)
 				if best[1] - current[1] > threshold then
-					Ovale:Logf("    group new best (higher prio) %s [%d]", tostring(current), element.nodeId)
-					replace = true
+					Ovale:Logf("[%d]    group new best (higher prio) is %s", element.nodeId, tostring(current))
+					currentIsBetter = true
 				end
 			end
-			if replace then
+			if currentIsBetter then
 				best:Reset(current)
 				bestTimeSpan = currentTimeSpan
 				bestPriority = currentPriority
@@ -631,39 +917,30 @@ local function ComputeGroup(element, state)
 		end
 	end

-	self_pool:Release(best)
-	self_pool:Release(current)
+	self_timeSpanPool:Release(best)
+	self_timeSpanPool:Release(current)

-	if not bestTimeSpan then
-		Ovale:Logf("group return %s [%d]", tostring(timeSpan), element.nodeId)
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan
-	else
-		CopyTimeSpan(bestTimeSpan, timeSpan)
-		if bestElement then
-			local id = bestElement.value
-			if bestElement.params then
-				id = bestElement.params[1]
-			end
-			Ovale:Logf("group best action %s remains %s [%d]", id, tostring(timeSpan), element.nodeId)
-		else
-			Ovale:Logf("group no best action returns %s [%d]", tostring(timeSpan), element.nodeId)
+	CopyTimeSpan(bestTimeSpan, timeSpan)
+	if bestElement then
+		local id = bestElement.value
+		if bestElement.params then
+			id = bestElement.params[1]
 		end
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan, bestPriority, bestElement
+		Ovale:Logf("[%d]    group best action %s remains %s", element.nodeId, id, tostring(timeSpan))
+	else
+		Ovale:Logf("[%d]    group no best action returns %s", element.nodeId, tostring(timeSpan))
 	end
+
+	profiler.Stop("OvaleBestAction_Compute")
+	return timeSpan, bestPriority, bestElement
 end

-local function ComputeIf(element, state)
+function OvaleBestAction:ComputeIf(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	Ovale:Logf("%s [%d]", element.type, element.nodeId)
-	local self = OvaleBestAction
-
-	local timeSpanA = self:ComputeBool(element.a, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
-	local conditionTimeSpan = OvaleTimeSpan(self_pool:Get())
+	local timeSpanA = self:ComputeBool(element.child[1], state)
+	local timeSpan = GetTimeSpan(element)
+	local priority, result
+	local conditionTimeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
 	if element.type == "if" then
 		conditionTimeSpan:Reset(timeSpanA)
 	elseif element.type == "unless" then
@@ -673,323 +950,102 @@ local function ComputeIf(element, state)
 	-- Short-circuit evaluation of left argument to IF.
 	if Measure(conditionTimeSpan) == 0 then
 		timeSpan:Reset(conditionTimeSpan)
-		self_pool:Release(conditionTimeSpan)
-		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
-		local result = PutValue(element, 0, 0, 0)
-		profiler.Stop("OvaleBestAction_Compute")
-		return timeSpan, OVALE_DEFAULT_PRIORITY, result
-	end
-
-	local timeSpanB, priorityB, elementB = self:Compute(element.b, state)
-	-- If the "then" clause is a "wait" node, then only wait if the conditions are true.
-	if elementB and elementB.wait and not HasTime(conditionTimeSpan, state.currentTime) then
-		elementB.wait = nil
+		Ovale:Logf("[%d]    '%s' returns %s with zero measure", element.nodeId, element.type, tostring(timeSpan))
+		priority = OVALE_DEFAULT_PRIORITY
+		result = SetValue(element, 0)
+	else
+		local timeSpanB, priorityB, elementB = self:Compute(element.child[2], state)
+		-- If the "then" clause is a "wait" node, then only wait if the conditions are true.
+		if elementB and elementB.wait and not HasTime(conditionTimeSpan, state.currentTime) then
+			elementB.wait = nil
+		end
+		-- Take intersection of the condition and B.
+		Intersect(conditionTimeSpan, timeSpanB, timeSpan)
+		Ovale:Logf("[%d]    '%s' returns %s", element.nodeId, element.type, tostring(timeSpan))
+		priority = priorityB
+		result = elementB
 	end
-	-- Take intersection of the condition and B.
-	Intersect(conditionTimeSpan, timeSpanB, timeSpan)
-	self_pool:Release(conditionTimeSpan)
+	self_timeSpanPool:Release(conditionTimeSpan)

-	Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
 	profiler.Stop("OvaleBestAction_Compute")
-	return timeSpan, priorityB, elementB
+	return timeSpan, priority, result
 end

-local function ComputeLua(element, state)
-	profiler.Start("OvaleBestAction_ComputeLua")
-	local ret = loadstring(element.lua)()
-	Ovale:Logf("lua %s [%d]", ret, element.nodeId)
-
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
-	timeSpan[1], timeSpan[2] = 0, math.huge
-	local result = PutValue(element, ret, 0, 0)
-	profiler.Stop("OvaleBestAction_ComputeLua")
-	return timeSpan, OVALE_DEFAULT_PRIORITY, result
-end
-
-local function ComputeNot(element, state)
+function OvaleBestAction:ComputeLogical(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	Ovale:Logf("%s [%d]", element.type, element.nodeId)
-	local self = OvaleBestAction
-	local timeSpanA = self:ComputeBool(element.a, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
-	Complement(timeSpanA, timeSpan)
-	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+	local timeSpanA = self:ComputeBool(element.child[1], state)
+	local timeSpan = GetTimeSpan(element)
+
+	if element.operator == "and" then
+		-- Short-circuit evaluation of left argument to AND.
+		if Measure(timeSpanA) == 0 then
+			timeSpan:Reset(timeSpanA)
+			Ovale:Logf("[%d]    logical '%s' short-circuits with zero measure left argument", element.nodeId, element.operator, tostring(timeSpan))
+		else
+			local timeSpanB = self:ComputeBool(element.child[2], state)
+			-- Take intersection of A and B.
+			Intersect(timeSpanA, timeSpanB, timeSpan)
+		end
+	elseif element.operator == "not" then
+		Complement(timeSpanA, timeSpan)
+	elseif element.operator == "or" then
+		-- Short-circuit evaluation of left argument to OR.
+		if timeSpanA[1] == 0 and timeSpanA[1] == math.huge then
+			timeSpan:Reset(timeSpanA)
+			Ovale:Logf("[%d]    logical '%s' short-circuits with universe as left argument", element.nodeId, element.operator, tostring(timeSpan))
+		else
+			local timeSpanB = self:ComputeBool(element.child[2], state)
+			-- Take union of A and B.
+			Union(timeSpanA, timeSpanB, timeSpan)
+		end
+	end
+
+	Ovale:Logf("[%d]    logical '%s' returns %s", element.nodeId, element.operator, tostring(timeSpan))
 	profiler.Stop("OvaleBestAction_Compute")
 	return timeSpan
 end

-local function ComputeOr(element, state)
-	profiler.Start("OvaleBestAction_Compute")
-	Ovale:Logf("%s [%d]", element.type, element.nodeId)
-	local self = OvaleBestAction
-	local timeSpanA = self:ComputeBool(element.a, state)
-	local timeSpanB = self:ComputeBool(element.b, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
-	-- Take union of A and B.
-	Union(timeSpanA, timeSpanB, timeSpan)
-	Ovale:Logf("%s returns %s [%d]", element.type, tostring(timeSpan), element.nodeId)
-	profiler.Stop("OvaleBestAction_Compute")
-	return timeSpan
+function OvaleBestAction:ComputeLua(element, state)
+	profiler.Start("OvaleBestAction_ComputeLua")
+	local value = loadstring(element.lua)()
+	Ovale:Logf("[%d]    lua returns %s", element.nodeId, value)
+
+	local timeSpan = GetTimeSpan(element)
+	local priority, result
+	if value then
+		timeSpan[1], timeSpan[2] = 0, math.huge
+		result = SetValue(element, value)
+		priority = OVALE_DEFAULT_PRIORITY
+	end
+	profiler.Stop("OvaleBestAction_ComputeLua")
+	return timeSpan, priority, result
 end

-local function ComputeValue(element, state)
+function OvaleBestAction:ComputeValue(element, state)
 	profiler.Start("OvaleBestAction_ComputeValue")
-	Ovale:Logf("value %s", element.value)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
-
+	Ovale:Logf("[%d]    value is %s", element.nodeId, element.value)
+	local timeSpan = GetTimeSpan(element)
 	timeSpan[1], timeSpan[2] = 0, math.huge
 	profiler.Stop("OvaleBestAction_ComputeValue")
 	return timeSpan, OVALE_DEFAULT_PRIORITY, element
 end

-local function ComputeWait(element, state)
+function OvaleBestAction:ComputeWait(element, state)
 	profiler.Start("OvaleBestAction_Compute")
-	Ovale:Logf("%s [%d]", element.type, element.nodeId)
-	local self = OvaleBestAction
-	local timeSpanA, priorityA, elementA = self:Compute(element.a, state)
-	local timeSpan = element.timeSpan
-	timeSpan:Reset()
+	local timeSpanA, priorityA, elementA = self:Compute(element.child[1], state)
+	local timeSpan = GetTimeSpan(element)

 	if elementA then
 		elementA.wait = true
 		CopyTimeSpan(timeSpanA, timeSpan)
-		Ovale:Logf("%s return %s [%d]", element.type, tostring(timeSpan), element.nodeId)
+		Ovale:Logf("[%d]    '%s' returns %s", element.nodeId, element.type, tostring(timeSpan))
 	end
 	profiler.Stop("OvaleBestAction_Compute")
 	return timeSpan, priorityA, elementA
 end
---</private-static-methods>
-
---<private-static-properties>
-local OVALE_COMPUTE_VISITOR =
-{
-	["action"] = ComputeAction,
-	["and"] = ComputeAnd,
-	["arithmetic"] = ComputeArithmetic,
-	["compare"] = ComputeCompare,
-	["customfunction"] = ComputeCustomFunction,
-	["function"] = ComputeFunction,
-	["group"] = ComputeGroup,
-	["if"] = ComputeIf,
-	["lua"] = ComputeLua,
-	["not"] = ComputeNot,
-	["or"] = ComputeOr,
-	["unless"] = ComputeIf,
-	["value"] = ComputeValue,
-	["wait"] = ComputeWait,
-}
---</private-static-properties>
-
---<public-static-methods>
-function OvaleBestAction:OnInitialize()
-	-- Resolve module dependencies.
-	OvaleActionBar = Ovale.OvaleActionBar
-	OvaleCooldown = Ovale.OvaleCooldown
-	OvaleCondition = Ovale.OvaleCondition
-	OvaleData = Ovale.OvaleData
-	OvaleEquipement = Ovale.OvaleEquipement
-	OvaleFuture = Ovale.OvaleFuture
-	OvalePower = Ovale.OvalePower
-	OvaleSpellBook = Ovale.OvaleSpellBook
-	OvaleStance = Ovale.OvaleStance
-end
-
-function OvaleBestAction:StartNewAction(state)
-	state:Reset()
-	OvaleFuture:ApplyInFlightSpells(state)
-	self_serial = self_serial + 1
-end
-
-function OvaleBestAction:GetActionInfo(element, state)
-	if not element then
-		return nil
-	end
-
-	profiler.Start("OvaleBestAction_GetActionInfo")
-	local target = element.params.target or OvaleCondition.defaultTarget
-	local action
-	local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
-		actionUsable, actionShortcut, actionIsCurrent, actionEnable,
-		actionType, actionId
-
-	if element.func == "spell" then
-		local spellId = element.params[1]
-		action = OvaleActionBar:GetForSpell(spellId)
-		if not OvaleSpellBook:IsKnownSpell(spellId) and not action then
-			Ovale:Logf("Spell %s not learnt", spellId)
-			profiler.Stop("OvaleBestAction_GetActionInfo")
-			return nil
-		end
-
-		actionTexture = actionTexture or API_GetSpellTexture(spellId)
-		actionInRange = API_IsSpellInRange(OvaleSpellBook:GetSpellName(spellId), target)
-		actionCooldownStart, actionCooldownDuration, actionEnable = state:GetSpellCooldown(spellId)
-		actionType = "spell"
-		actionId = spellId

-		-- Verify that the spell may be cast given restrictions specified in SpellInfo().
-		local si = OvaleData.spellInfo[spellId]
-		if si then
-			if si.stance and not OvaleStance:IsStance(si.stance) then
-				-- Spell requires a stance that player is not in.
-				profiler.Stop("OvaleBestAction_GetActionInfo")
-				return nil
-			end
-			if si.combo then
-				-- Spell requires combo points.
-				local cost = state:ComboPointCost(spellId)
-				if state.combo < cost then
-					profiler.Stop("OvaleBestAction_GetActionInfo")
-					return nil
-				end
-			end
-			for powerType in pairs(OvalePower.SECONDARY_POWER) do
-				if si[powerType] then
-					-- Spell requires "secondary" resources, e.g., chi, focus, rage, etc.,
-					local cost = state:PowerCost(spellId, powerType)
-					if state[powerType] < cost then
-						profiler.Stop("OvaleBestAction_GetActionInfo")
-						return nil
-					end
-				end
-			end
-
-			if actionCooldownStart and actionCooldownDuration then
-				-- Get the maximum time before all "primary" resources are ready.
-				local atTime = state.currentTime
-				for powerType in pairs(OvalePower.PRIMARY_POWER) do
-					if si[powerType] then
-						local t = state.currentTime + state:TimeToPower(spellId, powerType)
-						if atTime < t then
-							atTime = t
-						end
-					end
-				end
-				if actionCooldownStart > 0 then
-					if atTime > actionCooldownStart + actionCooldownDuration then
-						Ovale:Logf("Delaying spell %s for primary resource.", spellId)
-						actionCooldownDuration = atTime - actionCooldownStart
-					end
-				else
-					actionCooldownStart = state.currentTime
-					actionCooldownDuration = atTime - actionCooldownStart
-				end
-
-				if si.blood or si.frost or si.unholy or si.death then
-					-- Spell requires runes.
-					local needRunes = true
-					-- "buff_runes_none" is the spell ID of the buff that makes casting the spell cost no runes.
-					local buffNoRunes = si.buff_runes_none
-					if buffNoRunes then
-						local aura = state:GetAura("player", buffNoRunes)
-						if state:IsActiveAura(aura) then
-							needRunes = false
-						end
-					end
-					if needRunes then
-						local ending = state.currentTime + state:GetRunesCooldown(si.blood, si.unholy, si.frost, si.death)
-						if ending > actionCooldownStart + actionCooldownDuration then
-							actionCooldownDuration = ending - actionCooldownStart
-						end
-					end
-				end
-			end
-			-- Use a custom texture if given.
-			if si.texture then
-				actionTexture = "Interface\\Icons\\" .. si.texture
-			end
-		end
-
-		actionUsable = OvaleSpellBook:IsUsableSpell(spellId)
-
-	elseif element.func == "macro" then
-		local macro = element.params[1]
-		action = OvaleActionBar:GetForMacro(macro)
-		if not action then
-			Ovale:Logf("Unknown macro %s", macro)
-			profiler.Stop("OvaleBestAction_GetActionInfo")
-			return nil
-		end
-		actionTexture = API_GetActionTexture(action)
-		actionInRange = API_IsActionInRange(action, target)
-		actionCooldownStart, actionCooldownDuration, actionEnable = API_GetActionCooldown(action)
-		actionUsable = API_IsUsableAction(action)
-		actionType = "macro"
-		actionId = macro
-
-	elseif element.func == "item" then
-		local itemId = element.params[1]
-		if itemId and type(itemId) ~= "number" then
-			itemId = OvaleEquipement:GetEquippedItem(itemId)
-		end
-		if not itemId then
-			Ovale:Logf("Unknown item %s", element.params[1])
-			profiler.Stop("OvaleBestAction_GetActionInfo")
-			return nil
-		end
-		Ovale:Logf("Item %s", itemId)
-		action = OvaleActionBar:GetForItem(itemId)
-
-		actionTexture = API_GetItemIcon(itemId)
-		actionInRange = API_IsItemInRange(itemId, target)
-		actionCooldownStart, actionCooldownDuration, actionEnable = API_GetItemCooldown(itemId)
-
-		local spellName = API_GetItemSpell(itemId)
-		actionUsable = spellName and API_IsUsableItem(action)
-		actionType = "item"
-		actionId = itemId
-
-	elseif element.func == "texture" then
-		local texture = element.params[1]
-		actionTexture = "Interface\\Icons\\" .. texture
-		actionInRange = nil
-		actionCooldownStart = API_GetTime()
-		actionCooldownDuration = 0
-		actionEnable = 1
-		actionUsable = true
-		actionType = "texture"
-		actionId = texture
-	end
-
-	if action then
-		actionShortcut = OvaleActionBar:GetBinding(action)
-		actionIsCurrent = API_IsCurrentAction(action)
-	end
-
-	profiler.Stop("OvaleBestAction_GetActionInfo")
-	return actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
-		actionUsable, actionShortcut, actionIsCurrent, actionEnable, actionType, actionId, target, element.params.nored
-end
-
-function OvaleBestAction:Compute(element, state)
-	if not element or (Ovale.bug and not Ovale.trace) then
-		return nil
-	end
-
-	local visitor = OVALE_COMPUTE_VISITOR[element.type]
-	if visitor then
-		return visitor(element, state)
-	end
-
-	Ovale:Logf("unknown element %s, return nil", element.type)
-	return nil
-end
-
-function OvaleBestAction:ComputeBool(element, state)
-	local timeSpan, _, newElement = self:Compute(element, state)
-	-- Match SimC: 0 is false, non-zero is true.
-	--	(https://code.google.com/p/simulationcraft/wiki/ActionLists#Logical_operators)
-	if newElement and newElement.type == "value" and newElement.value == 0 and newElement.rate == 0 then
-		return nil
-	else
-		return timeSpan
-	end
+function OvaleBestAction:Debug()
+	self_timeSpanPool:Debug()
+	self_valuePool:Debug()
 end
 --</public-static-methods>
diff --git a/OvaleCompile.lua b/OvaleCompile.lua
index 4456bd4..f5efc2b 100644
--- a/OvaleCompile.lua
+++ b/OvaleCompile.lua
@@ -13,11 +13,8 @@ local OvaleCompile = Ovale:NewModule("OvaleCompile", "AceEvent-3.0")
 Ovale.OvaleCompile = OvaleCompile

 --<private-static-properties>
-local L = Ovale.L
-local OvalePool = Ovale.OvalePool
-local OvaleTimeSpan = Ovale.OvaleTimeSpan
-
 -- Forward declarations for module dependencies.
+local OvaleAST = nil
 local OvaleCondition = nil
 local OvaleCooldown = nil
 local OvaleData = nil
@@ -32,913 +29,345 @@ local OvaleStance = nil
 local ipairs = ipairs
 local pairs = pairs
 local tonumber = tonumber
-local strgmatch = string.gmatch
-local strgsub = string.gsub
-local strlen = string.len
-local strlower = string.lower
+local strfind = string.find
 local strmatch = string.match
 local strsub = string.sub
-local tinsert = table.insert
 local wipe = table.wipe
-local API_GetItemInfo = GetItemInfo
-local API_GetSpellInfo = GetSpellInfo

 -- Profiling set-up.
 local Profiler = Ovale.Profiler
 local profiler = nil
 do
 	local group = OvaleCompile:GetName()
-
-	local function EnableProfiling()
-		API_GetItemInfo = Profiler:Wrap(group, "OvaleCompile_API_GetItemInfo", GetItemInfo)
-		API_GetSpellInfo = Profiler:Wrap(group, "OvaleCompile_API_GetSpellInfo", GetSpellInfo)
-	end
-
-	local function DisableProfiling()
-		API_GetItemInfo = GetItemInfo
-		API_GetSpellInfo = GetSpellInfo
-	end
-
-	Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling)
+	Profiler:RegisterProfilingGroup(group)
 	profiler = Profiler:GetProfilingGroup(group)
 end

-local self_node = {}
-local self_pool = OvalePool("OvaleCompile_pool")
-local self_timeSpanPool = OvalePool("OvaleCompile_timeSpanPool")
-local self_defines = {}
-local self_sharedCooldownNames = {}
-local self_customFunctions = {}
-local self_missingSpellList = {}
--- table of functions called within the script: self_functionCalls[functionName] = node
-local self_functionCalls = {}
-
 -- Whether to trigger a script compilation if items or stances change.
 local self_compileOnItems = false
 local self_compileOnStances = false

 -- This module needs the information in other modules to be preloaded and ready for use.
-local self_canCompile = false
+local self_canEvaluate = false
 local self_requirePreload = { "OvaleEquipement", "OvaleSpellBook", "OvaleStance" }

--- Current age of compilation state.
+-- Current age of the script evaluation state.
+-- This advances every time an event occurs that requires re-evaluating the script.
 local self_serial = 0
--- Number of times the script has been compiled.
-local self_compileCount = 0
--- Master nodes of the current script (one node for each icon)
-local self_masterNodes = {}
+-- Number of times the script has been evaluated.
+local self_timesEvaluated = 0
+-- Icon nodes of the current script (one node for each icon)
+local self_icon = {}

--- Lua pattern to match a key=value pair, returning key and value.
-local KEY_VALUE_PATTERN = "([%w_]+)=(!?[-%w\\_%.]+)"
 -- Lua pattern to match a floating-point number that may start with a minus sign.
 local NUMBER_PATTERN = "^%-?%d+%.?%d*$"

 local OVALE_COMPILE_DEBUG = "compile"
-local OVALE_MISSING_SPELL_DEBUG = "missing_spells"
-local OVALE_UNKNOWN_SPELL_DEBUG = "unknown_spells"
-
--- Parameters used as conditionals in script declarations.
-local OVALE_PARAMETER = {
-	checkboxoff = true,
-	checkboxon = true,
-	glyph = true,
-	if_spell = true,
-	if_stance = true,
-	item = true,
-	itemcount = true,
-	itemset = true,
-	list = true,
-	mastery = true,
-	talent = true,
-}
-
--- Known script functions other than conditions.
-local OVALE_FUNCTIONS = {
-	item = true,
-	macro = true,
-	spell = true,
-	texture = true,
-}
 --</private-static-properties>

 --<public-static-properties>
--- Current age of the current compiled script.
+-- Current age of the script; this advances every time the script is evaluated.
 OvaleCompile.serial = nil
-OvaleCompile.customFunctionNode = {}
+-- AST for the current script.
+OvaleCompile.ast = nil
 --</public-static-properties>

 --<private-static-methods>
-local function AddNode(node)
-	tinsert(self_node, node)
-	node.nodeId = #self_node
-	return "node" .. #self_node
-end
-
--- Parse params string into key=value pairs and positional arguments stored in paramList table.
-local function ParseParameters(params, paramList)
-	profiler.Start("OvaleCompile_ParseParameters")
-	paramList = paramList or {}
-	if params then
-		-- Handle key=value pairs.
-		for key, value in strgmatch(params, KEY_VALUE_PATTERN) do
-			if strmatch(key, NUMBER_PATTERN) then
-				key = tonumber(key)
-			end
-			if strmatch(value, NUMBER_PATTERN) then
-				value = tonumber(value)
-			end
-			paramList[key] = value
-		end
-		-- Strip out all key=value pairs and handle positional arguments.
-		params = strgsub(params, KEY_VALUE_PATTERN, "")
-		local k = 1
-		for word in strgmatch(params, "[-%w_\\%.]+") do
-			if strmatch(word, NUMBER_PATTERN) then
-				word = tonumber(word)
-			end
-			paramList[k] = word
-			k = k + 1
-		end
-	end
-	profiler.Stop("OvaleCompile_ParseParameters")
-	return paramList
-end
-
 local function HasTalent(talentId)
 	if OvaleSpellBook:IsKnownTalent(talentId) then
 		return OvaleSpellBook:GetTalentPoints(talentId) > 0
 	else
-		Ovale:FormatPrint("Unknown talent %s", talentId)
+		Ovale:FormatPrint("Warning: unknown talent ID '%s'", talentId)
 		return false
 	end
 end

 local function RequireValue(value)
-	local requireValue = (strsub(value, 1, 1) ~= "!")
-	if not requireValue then
+	local required = (strsub(tostring(value), 1, 1) ~= "!")
+	if not required then
 		value = strsub(value, 2)
 		if strmatch(value, NUMBER_PATTERN) then
 			value = tonumber(value)
 		end
 	end
-	return value, requireValue
+	return value, required
 end

-local function TestConditions(paramList)
+local function TestConditions(parameters)
 	profiler.Start("OvaleCompile_TestConditions")
 	local boolean = true
-	if boolean and paramList.glyph then
-		local glyph, requireGlyph = RequireValue(paramList.glyph)
+	if boolean and parameters.glyph then
+		local glyph, required = RequireValue(parameters.glyph)
 		local hasGlyph = OvaleSpellBook:IsActiveGlyph(glyph)
-		if (requireGlyph and not hasGlyph) or (not requireGlyph and hasGlyph) then
-			boolean = false
-		end
+		boolean = (required and hasGlyph) or (not required and not hasGlyph)
 	end
-	if boolean and paramList.mastery then
-		local spec, requireSpec = RequireValue(paramList.mastery)
+	if boolean and parameters.mastery then
+		local spec, required = RequireValue(parameters.mastery)
 		local isSpec = OvalePaperDoll:IsSpecialization(spec)
-		if (requireSpec and not isSpec) or (not requireSpec and isSpec) then
-			boolean = false
-		end
+		boolean = (required and isSpec) or (not required and not isSpec)
 	end
-	if boolean and paramList.if_stance then
+	if boolean and parameters.if_stance then
 		self_compileOnStances = true
-		local stance, requireStance = RequireValue(paramList.if_stance)
+		local stance, required = RequireValue(parameters.if_stance)
 		local isStance = OvaleStance:IsStance(stance)
-		if (requireStance and not isStance) or (not requireStance and isStance) then
-			boolean = false
-		end
+		boolean = (required and isStance) or (not required and not isStance)
 	end
-	if boolean and paramList.if_spell then
-		local spell, requireSpell = RequireValue(paramList.if_spell)
+	if boolean and parameters.if_spell then
+		local spell, required = RequireValue(parameters.if_spell)
 		local hasSpell = OvaleSpellBook:IsKnownSpell(spell)
-		if (requireSpell and not hasSpell) or (not requireSpell and hasSpell) then
-			boolean = false
-		end
+		boolean = (required and hasSpell) or (not required and not hasSpell)
 	end
-	if boolean and paramList.talent then
-		local talent, requireTalent = RequireValue(paramList.talent)
+	if boolean and parameters.talent then
+		local talent, required = RequireValue(parameters.talent)
 		local hasTalent = HasTalent(talent)
-		if (requireTalent and not hasTalent) or (not requireTalent and hasTalent) then
-			boolean = false
-		end
+		boolean = (required and hasTalent) or (not required and not hasTalent)
 	end
-	if boolean and paramList.checkboxon then
-		local cb = paramList.checkboxon
-		if not Ovale.casesACocher[cb] then
-			Ovale.casesACocher[cb] = {}
-		end
-		Ovale.casesACocher[cb].compile = true
-		if not OvaleOptions:GetProfile().check[cb] then
-			boolean = false
-		end
-	end
-	if boolean and paramList.checkboxoff then
-		local cb = paramList.checkboxoff
-		if not Ovale.casesACocher[cb] then
-			Ovale.casesACocher[cb] = {}
-		end
-		Ovale.casesACocher[cb].compile = true
-		if OvaleOptions:GetProfile().check[cb] then
-			boolean = false
-		end
-	end
-	if boolean and paramList.list and paramList.item then
-		local list = paramList.list
-		local key = paramList.item
-		if not Ovale.listes[list] then
-			Ovale.listes[list] = { items = {}}
-		end
-		Ovale.listes[list].compile = true
-		if OvaleOptions:GetProfile().list[list] ~= key then
-			boolean = false
-		end
-	end
-	if boolean and paramList.itemset and paramList.itemcount then
-		local equippedCount = OvaleEquipement:GetArmorSetCount(paramList.itemset)
+	if boolean and parameters.itemset and parameters.itemcount then
+		local equippedCount = OvaleEquipement:GetArmorSetCount(parameters.itemset)
 		self_compileOnItems = true
-		if equippedCount < paramList.itemcount then
-			boolean = false
+		boolean = (equippedCount >= parameters.itemcount)
+	end
+	do
+		local profile
+		if boolean and parameters.checkbox then
+			local name, required = RequireValue(parameters.checkbox)
+			local checkBox = Ovale.casesACocher[name] or {}
+			checkBox.compile = true
+			Ovale.casesACocher[name] = checkBox
+			-- Check the value of the checkbox.
+			profile = profile or OvaleOptions:GetProfile()
+			local isChecked = (profile.check[name] ~= nil)
+			boolean = (required and isChecked) or (not required and not isChecked)
+		end
+		-- XXX Deprecated: checkboxon
+		if boolean and parameters.checkboxon then
+			-- Flag this checkbox as triggering a script evaluation.
+			local name = parameters.checkboxon
+			local checkBox = Ovale.casesACocher[name] or {}
+			checkBox.compile = true
+			Ovale.casesACocher[name] = checkBox
+			-- Check the value of the checkbox.
+			profile = profile or OvaleOptions:GetProfile()
+			boolean = (profile.check[name] ~= nil)
+		end
+		-- XXX Deprecated: checkboxoff
+		if boolean and parameters.checkboxoff then
+			-- Flag this checkbox as triggering a script evaluation.
+			local name = parameters.checkboxon
+			local checkBox = Ovale.casesACocher[name] or {}
+			checkBox.compile = true
+			Ovale.casesACocher[name] = checkBox
+			-- Check the value of the checkbox.
+			profile = profile or OvaleOptions:GetProfile()
+			boolean = (profile.check[name] == nil)
+		end
+		if boolean and parameters.list and parameters.item then
+			-- Flag this list as triggering a script evaluation.
+			local name = parameters.list
+			local item, required = RequireValue(parameters.item)
+			local list = Ovale.listes[name] or { items = {}, default = nil }
+			list.compile = true
+			Ovale.listes[name] = list
+			-- Check the selected item in the list.
+			profile = profile or OvaleOptions:GetProfile()
+			local isSelected = (profile.list[name] == item)
+			boolean = (required and isSelected) or (not required and not isSelected)
 		end
 	end
 	profiler.Stop("OvaleCompile_TestConditions")
 	return boolean
 end

-local function ParseNumber(dummy, value)
-	local node = self_pool:Get()
-	node.type = "value"
-	node.value = tonumber(value)
-	node.origin = 0
-	node.rate = 0
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return dummy..AddNode(node)
-end
-
-local function ParseFunction(prefix, func, params)
-	local paramList = ParseParameters(params)
-	if func ~= "" then
-		paramList.target = prefix
-	else
-		func = prefix
-	end
-
-	if not paramList.target then
-		if strsub(func, 1, 6) == "Target" then
-			paramList.target = "target"
-			func = strsub(func, 7)
-		end
-	end
-
-	if self_customFunctions[func] then
-		self_functionCalls[func] = self_customFunctions[func]
-		return self_customFunctions[func]
-	end
-
-	func = strlower(func)
-
-	-- "debuff" and "buff" conditions implicitly set their aura filter.
-	if not paramList.filter then
-		if strsub(func, 1, 6) == "debuff" then
-			paramList.filter = "debuff"
-		elseif strsub(func, 1, 4) == "buff" then
-			paramList.filter = "buff"
-		elseif strsub(func, 1, 11) == "otherdebuff" then
-			paramList.filter = "debuff"
-		elseif strsub(func, 1, 9) == "otherbuff" then
-			paramList.filter = "buff"
-		end
-	end
-
-	local node = self_pool:Get()
-	if func == "spell" or func == "macro" or func == "item" or func == "texture" then
-		node.type = "action"
-	else
-		node.type = "function"
-	end
-	node.func = func
-	node.params = paramList
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	local nodeName = AddNode(node)
-	self_functionCalls[func] = node
-
-	local mine = true
-	if paramList.any then
-		mine = false
-	end
-
-	local spellId = paramList[1]
-	if spellId then
-		-- For the conditions that refer to player's spells, check if the spell ID
-		-- is a variant of a spell with the same name as one already in the
-		-- spellbook.  If it is, then add that variant spell ID to our spellList.
-		if OvaleCondition:IsSpellbookCondition(func) then
-			if not OvaleSpellBook:IsKnownSpell(spellId) and not self_missingSpellList[spellId] and not self_sharedCooldownNames[spellId] then
-				local spellName
-				if type(spellId) == "number" then
-					spellName = API_GetSpellInfo(spellId)
-				end
-				if spellName then
-					if spellName == API_GetSpellInfo(spellName) then
-						Ovale:DebugPrintf(OVALE_MISSING_SPELL_DEBUG, "Learning spell %s with ID %d", spellName, spellId)
-						self_missingSpellList[spellId] = spellName
-					end
-				else
-					Ovale:DebugPrintf(OVALE_UNKNOWN_SPELL_DEBUG, "Unknown spell with ID %s", spellId)
-				end
-			end
-		end
-	end
-
-	return nodeName
-end
-
---[[
-	Parse the various Spell*{Buff,Debuff}() declarations.
-	Check for test conditions to see whether this declaration is active.
-	Filter out then test conditions and copy the rest of the key=value pairs
-	into the aura table.
---]]
-local function ParseSpellAuraList(auraTable, filter, paramList)
-	if TestConditions(paramList) then
-		paramList[1] = nil
-		if not auraTable[filter] then
-			for k, v in pairs(paramList) do
-				if OVALE_PARAMETER[k] then
-					paramList[k] = nil
-				end
-			end
-			auraTable[filter] = paramList
-		else
-			local tbl = auraTable[filter]
-			for k, v in pairs(paramList) do
-				if not OVALE_PARAMETER[k] then
-					tbl[k] = v
-				end
+local function EvaluateAddCheckBox(node)
+	local ok = true
+	local name, parameters = node.name, node.params
+	if TestConditions(parameters) then
+		--[[
+			If this control was not previously existing, then age the  script evaluation state
+			so that anything that checks the value of this control are re-evaluated after the
+			current evaluation cycle.
+		--]]
+		if not Ovale.casesACocher[name] then
+			self_serial = self_serial + 1
+		end
+		local checkBox = Ovale.casesACocher[name] or {}
+		checkBox.text = node.description.value
+		for _, v in ipairs(parameters) do
+			if v == "default" then
+				checkBox.checked = true
+				break
 			end
 		end
+		Ovale.casesACocher[name] = checkBox
 	end
-	return ""
-end
-
-local function ParseSpellAddBuff(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	local si = OvaleData:SpellInfo(spellId)
-	return ParseSpellAuraList(si.aura.player, "HELPFUL", paramList)
-end
-
-local function ParseSpellAddDebuff(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	local si = OvaleData:SpellInfo(spellId)
-	return ParseSpellAuraList(si.aura.player, "HARMFUL", paramList)
-end
-
-local function ParseSpellAddTargetBuff(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	local si = OvaleData:SpellInfo(spellId)
-	return ParseSpellAuraList(si.aura.target, "HELPFUL", paramList)
+	return ok
 end

-local function ParseSpellAddTargetDebuff(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	local si = OvaleData:SpellInfo(spellId)
-	return ParseSpellAuraList(si.aura.target, "HARMFUL", paramList)
-end
-
-local function ParseSpellDamageBuff(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	local si = OvaleData:SpellInfo(spellId)
-	return ParseSpellAuraList(si.aura.damage, "HELPFUL", paramList)
-end
-
-local function ParseSpellDamageDebuff(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	local si = OvaleData:SpellInfo(spellId)
-	return ParseSpellAuraList(si.aura.damage, "HARMFUL", paramList)
-end
-
-local function ParseSpellInfo(params)
-	local paramList = ParseParameters(params)
-	local spellId = paramList[1]
-	if spellId and TestConditions(paramList) then
-		local si = OvaleData:SpellInfo(spellId)
-		for k,v in pairs(paramList) do
-			if k == "addduration" then
-				si.duration = si.duration + v
-			elseif k == "addcd" then
-				si.cd = si.cd + v
-			elseif k == "addlist" then
-				-- Add this buff to the named spell list.
-				if not OvaleData.buffSpellList[v] then
-					OvaleData.buffSpellList[v] = {}
-				end
-				OvaleData.buffSpellList[v][spellId] = true
-			elseif k == "sharedcd" then
-				OvaleCooldown:AddSharedCooldown(v, spellId)
-				self_sharedCooldownNames[v] = true
-			else
-				si[k] = v
-			end
-		end
+local function EvaluateAddIcon(node)
+	local ok = true
+	if TestConditions(node.params) then
+		self_icon[#self_icon + 1] = node
 	end
-	return ""
+	return ok
 end

-local function ParseScoreSpells(params)
-	for v in strgmatch(params, "(%d+)") do
-		local spellId = tonumber(v)
-		if spellId then
-			OvaleScore:AddSpell(spellId)
-		else
-			Ovale:FormatPrint("ScoreSpell with unknown spell %s", v)
+local function EvaluateAddListItem(node)
+	local ok = true
+	local name, item, parameters = node.name, node.item, node.params
+	if TestConditions(parameters) then
+		--[[
+			If this control was not previously existing, then age the  script evaluation state
+			so that anything that checks the value of this control are re-evaluated after the
+			current evaluation cycle.
+		--]]
+		if not (Ovale.listes[name] and Ovale.listes[name][item]) then
+			self_serial = self_serial + 1
 		end
-	end
-end
-
-local function ParseSpellList(name, params)
-	OvaleData.buffSpellList[name] = {}
-	for v in strgmatch(params, "(%d+)") do
-		v = tonumber(v)
-		if v then
-			OvaleData.buffSpellList[name][v] = true
+		local list = Ovale.listes[name] or { items = {}, default = nil }
+		list.items[item] = node.description.value
+		for _, v in ipairs(parameters) do
+			if v == "default" then
+				list.default = item
+				break
+			end
 		end
+		Ovale.listes[name] = list
 	end
+	return ok
 end

-local function ParseItemInfo(params)
-	local paramList = ParseParameters(params)
-	local itemId = paramList[1]
-	if itemId and TestConditions(paramList) then
-		for k, v in pairs(paramList) do
+local function EvaluateItemInfo(node)
+	local ok = true
+	local itemId, parameters = node.itemId, node.params
+	if itemId and TestConditions(parameters) then
+		for k, v in pairs(parameters) do
 			if k == "proc" then
 				-- Add the buff for this item proc to the spell list "item_proc_<proc>".
-				local buff = tonumber(paramList.buff)
+				local buff = tonumber(parameters.buff)
 				if buff then
-					local listName = "item_proc_" .. v
-					if not OvaleData.buffSpellList[listName] then
-						OvaleData.buffSpellList[listName] = {}
-					end
-					OvaleData.buffSpellList[listName][buff] = true
+					local name = "item_proc_" .. v
+					local list = OvaleData.buffSpellList[name] or {}
+					list[buff] = true
+					OvaleData.buffSpellList[name] = list
+				else
+					ok = false
+					break
 				end
 			end
 		end
 	end
-	return ""
-end
-
-local function ParseItemList(name, params)
-	OvaleData.itemList[name] = {}
-	local i = 1
-	for v in strgmatch(params, "(%d+)") do
-		OvaleData.itemList[name][i] = tonumber(v)
-		i = i + 1
-	end
-end
-
-local function ParseIf(a, b)
-	local node = self_pool:Get()
-	node.type = "if"
-	node.a = self_node[tonumber(a)]
-	node.b = self_node[tonumber(b)]
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseUnless(a, b)
-	local node = self_pool:Get()
-	node.type = "unless"
-	node.a = self_node[tonumber(a)]
-	node.b = self_node[tonumber(b)]
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseWait(a)
-	local node = self_pool:Get()
-	node.type = "wait"
-	node.a = self_node[tonumber(a)]
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseAnd(a,b)
-	local node = self_pool:Get()
-	node.type = "and"
-	node.a = self_node[tonumber(a)]
-	node.b = self_node[tonumber(b)]
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseNot(a)
-	local node = self_pool:Get()
-	node.type = "not"
-	node.a = self_node[tonumber(a)]
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseOr(a,b)
-	local node = self_pool:Get()
-	node.type = "or"
-	node.a = self_node[tonumber(a)]
-	node.b = self_node[tonumber(b)]
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local ParseOp
-do
-	local operator = {
-		["+"] = "arithmetic",
-		["-"] = "arithmetic",
-		["*"] = "arithmetic",
-		["/"] = "arithmetic",
-		["%"] = "arithmetic",
-		["<"] = "compare",
-		["<="] = "compare",
-		["=="] = "compare",
-		[">="] = "compare",
-		[">"] = "compare",
-	}
-
-	function ParseOp(a, op, b)
-		local node = self_pool:Get()
-		node.type = operator[op]
-		node.operator = op
-		node.a = self_node[tonumber(a)]
-		node.b = self_node[tonumber(b)]
-		node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-		return AddNode(node)
-	end
-end
-
-local function ParseGroup(text)
-	local nodes = {}
-
-	for w in strgmatch(text, "node(%d+)") do
-		tinsert(nodes, self_node[tonumber(w)])
-	end
-
-	text = strgsub(text, "node%d+", "")
-
-	if (strmatch(text,"[^ ]")) then
-		Ovale:FormatPrint("syntax error: %s", text)
-		return nil
-	end
-
-	local node = self_pool:Get()
-	node.type = "group"
-	node.nodes = nodes
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseAddListItem(list, item, text, params)
-	local paramList = ParseParameters(params)
-	if not TestConditions(paramList) then
-		return ""
-	end
-	if (not Ovale.listes[list]) then
-		Ovale.listes[list] = {items={},default=nil}
-	end
-	Ovale.listes[list].items[item] = text
-	if paramList[1] and paramList[1] == "default" then
-		Ovale.listes[list].default=item
-	end
-	return ""
-end
-
-local function ParseAddCheckBox(item, text, params)
-	local paramList = ParseParameters(params)
-	if not TestConditions(paramList) then
-		return ""
-	end
-	if not Ovale.casesACocher[item] then
-		Ovale.casesACocher[item] = {}
-	end
-	Ovale.casesACocher[item].text = text
-	if  paramList[1] and paramList[1]=="default" then
-		Ovale.casesACocher[item].checked = true
-	end
-	return ""
+	return ok
 end

-local function ParseDefine(key, value)
-	self_defines[key] = value
-	return ""
-end
-
-local function ReplaceDefine(key)
-	return self_defines[key]
-end
-
-local function ParseLua(text)
-	local node = self_pool:Get()
-	node.type = "lua"
-	node.lua = strsub(text, 2, strlen(text)-1)
-	node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-	return AddNode(node)
-end
-
-local function ParseInclude(name)
-	local code
-	local script = OvaleScripts.script[name]
-	if script then
-		code = script.code
-	end
-	if not code then
-		Ovale:FormatPrint("Cannot Include(...): script named \"%s\" not found", name)
-	end
-	return code or ""
-end
-
-local function ParseCommands(text)
-	local original = text
-	text = strgsub(text,"(%b[])", ParseLua)
-
-	while true do
-		local was = text
-		text = strgsub(text, "(%w+)%.?(%w*)%s*%((.-)%)", ParseFunction)
-		text = strgsub(text, "([^%w])(%d+%.?%d*)", ParseNumber)
-		text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-		if was == text then
-			break
-		end
-	end
-
-	while true do
-		local was = text
-		while true do
-			local was = text
-			text = strgsub(text, "node(%d+)%s*([%*%/%%])%s*node(%d+)", ParseOp)
-			text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-			if was == text then
-				break
-			end
-		end
-		while true do
-			local was = text
-			text = strgsub(text, "node(%d+)%s*([%+%-])%s*node(%d+)", ParseOp)
-			text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-			if was == text then
-				break
-			end
-		end
-		if was == text then
-			break
-		end
+local function EvaluateList(node)
+	local ok = true
+	local name, parameters = node.name, node.params
+	local listDB
+	if node.keyword == "ItemList" then
+		listDB = "itemList"
+	else -- if node.keyword == "SpellList" then
+		listDB = "buffSpellList"
 	end
-
-	while true do
-		local was = text
-		text = strgsub(text, "node(%d+)%s*([%>%<]=?)%s*node(%d+)", ParseOp)
-		text = strgsub(text, "node(%d+)%s*(==)%s*node(%d+)", ParseOp)
-		text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-		if was == text then
-			break
-		end
-	end
-
-	while true do
-		local was = text
-		while true do
-			local was = text
-			text = strgsub(text, "not%s+node(%d+)", ParseNot)
-			text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-			if was == text then
-				break
-			end
-		end
-		while true do
-			local was = text
-			text = strgsub(text, "node(%d+)%s+and%s+node(%d+)", ParseAnd)
-			text = strgsub(text, "node(%d+)%s+or%s+node(%d+)", ParseOr)
-			text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-			if was == text then
-				break
-			end
-		end
-		if was == text then
+	local list = OvaleData[listDB][name] or {}
+	for i, id in ipairs(parameters) do
+		id = tonumber(id)
+		if id then
+			list[id] = true
+		else
+			ok = false
 			break
 		end
 	end
+	OvaleData[listDB][name] = list
+	return ok
+end

-	while true do
-		local was = text
-		text = strgsub(text, "if%s+node(%d+)%s+node(%d+)",ParseIf)
-		text = strgsub(text, "unless%s+node(%d+)%s+node(%d+)",ParseUnless)
-		text = strgsub(text, "wait%s+node(%d+)",ParseWait)
-		text = strgsub(text, "{([node%d ]*)}", ParseGroup)
-		if was == text then
+local function EvaluateScoreSpells(node)
+	local ok = true
+	for _, spellId in ipairs(node.params) do
+		spellId = tonumber(spellId)
+		if spellId then
+			OvaleScore:AddSpell(tonumber(spellId))
+		else
+			ok = false
 			break
 		end
 	end
-
-	local nodeId
-	if text then
-		nodeId = tonumber(strmatch(text, "node(%d+)"))
-	end
-	if nodeId then
-		-- If there is anything other than spaces, this is a syntax error.
-		text = strgsub(text, "node%d+", "", 1)
-		if strmatch(text,"[^ ]") then
-			Ovale:FormatPrint("Group: %s", original)
-			Ovale:FormatPrint("syntax error: %s", text)
-			nodeId = nil
-		end
-	else
-		Ovale:Print("no master node")
-	end
-	return nodeId
+	return ok
 end

-local function ParseAddFunction(name, params, text)
-	local paramList = ParseParameters(params)
-	if TestConditions(paramList) then
-		local nodeId = ParseCommands(text)
-		if nodeId then
-			local node = self_pool:Get()
-			node.type = "customfunction"
-			node.name = name
-			node.params = paramList
-			node.a = self_node[nodeId]
-			node.timeSpan = OvaleTimeSpan(self_timeSpanPool:Get())
-			return AddNode(node)
+local function EvaluateSpellAuraList(node)
+	local ok = true
+	local spellId, parameters = node.spellId, node.params
+	if TestConditions(parameters) then
+		local keyword = node.keyword
+		local si = OvaleData:SpellInfo(spellId)
+		local auraTable
+		if strfind(keyword, "^SpellAddTarget") then
+			auraTable = si.aura.target
+		elseif strfind(keyword, "^SpellDamage") then
+			auraTable = si.aura.damage
+		else
+			auraTable = si.aura.player
+		end
+		local filter = strfind(node.keyword, "Debuff") and "HARMFUL" or "HELPFUL"
+		local tbl = auraTable[filter] or {}
+		local count = 0
+		for k, v in pairs(parameters) do
+			if not OvaleAST.PARAMETER_KEYWORD[k] then
+				tbl[k] = v
+				count = count + 1
+			end
 		end
-	end
-end
-
-local function ParseAddIcon(params, text, secure)
-	local paramList = ParseParameters(params)
-	if TestConditions(paramList) then
-		local masterNodeId = ParseCommands(text)
-		if masterNodeId then
-			local masterNode = self_node[masterNodeId]
-			masterNode.params = paramList
-			masterNode.secure = secure
-			return masterNode
+		if count > 0 then
+			auraTable[filter] = tbl
 		end
 	end
+	return ok
 end

-local function ParseItemName(text)
-	local itemId = tonumber(text)
-	if itemId then
-		local item = API_GetItemInfo(itemId) or "Item " .. itemId
-		return '"' .. item .. '"'
-	else
-		Ovale:FormatPrint("ItemName of %s unknown\n", text)
-		return nil
-	end
-end
-
-local function ParseSpellName(text)
-	local spellId = tonumber(text)
-	local spell = OvaleSpellBook:GetSpellName(spellId)
-	if spell then
-		return '"' .. spell .. '"'
-	else
-		Ovale:FormatPrint("SpellName of %s unknown", text)
-		return nil
-	end
-end
-
-local function ParseL(text)
-	return '"'..L[text]..'"'
-end
-
--- On compile les AddCheckBox et AddListItem
-local function CompileInputs(text)
-	Ovale.casesACocher = {}
-	Ovale.listes = {}
-
-	text = strgsub(text, "AddListItem%s*%(%s*([%w_]+)%s+([%w_]+)%s+\"(.-)\"%s*(.-)%s*%)", ParseAddListItem)
-	text = strgsub(text, "AddCheckBox%s*%(%s*([%w_]+)%s+\"(.-)\"%s*(.-)%s*%)", ParseAddCheckBox)
-	return text
-end
-
--- Compile non-function and non-icon declarations.
-local function CompileDeclarations(text)
-	-- Define(CONSTANTE valeur)
-	text = strgsub(text, "Define%s*%(%s*([%w_]+)%s+([%w_.=]+)%s*%)", ParseDefine)
-	-- On remplace les constantes par leur valeur
-	text = strgsub(text, "([%w_]+)", ReplaceDefine)
-
-	-- Fonctions
-	text = strgsub(text, "ItemName%s*%(%s*(%w+)%s*%)", ParseItemName)
-	text = strgsub(text, "SpellName%s*%(%s*(%w+)%s*%)", ParseSpellName)
-	text = strgsub(text, "L%s*%(%s*(%w+)%s*%)", ParseL)
-
-	-- Options diverses
-	OvaleData:ResetSpellInfo()
-	text = strgsub(text, "SpellAddBuff%s*%((.-)%)", ParseSpellAddBuff)
-	text = strgsub(text, "SpellAddDebuff%s*%((.-)%)", ParseSpellAddDebuff)
-	text = strgsub(text, "SpellAddTargetBuff%s*%((.-)%)", ParseSpellAddTargetBuff)
-	text = strgsub(text, "SpellAddTargetDebuff%s*%((.-)%)", ParseSpellAddTargetDebuff)
-	text = strgsub(text, "SpellDamageBuff%s*%((.-)%)", ParseSpellDamageBuff)
-	text = strgsub(text, "SpellDamageDebuff%s*%((.-)%)", ParseSpellDamageDebuff)
-	text = strgsub(text, "SpellInfo%s*%((.-)%)", ParseSpellInfo)
-	text = strgsub(text, "ItemInfo%s*%((.-)%)", ParseItemInfo)
-	text = strgsub(text, "ScoreSpells%s*%((.-)%)", ParseScoreSpells)
-	text = strgsub(text, "SpellList%s*%(%s*([%w_]+)%s*(.-)%)", ParseSpellList)
-	text = strgsub(text, "ItemList%s*%(%s*([%w_]+)%s*(.-)%)", ParseItemList)
-
-	-- On vire les espaces en trop
-	text = strgsub(text, "\n", " ")
-	text = strgsub(text, "%s+", " ")
-
-	return text
-end
-
-local function CompileScript(text)
-	profiler.Start("OvaleCompile_CompileScript")
-	local self = OvaleCompile
-	self_compileOnItems = false
-	self_compileOnStances = false
-	Ovale.bug = false
-
-	wipe(self_defines)
-	wipe(self_sharedCooldownNames)
-	wipe(self_customFunctions)
-	wipe(self_missingSpellList)
-	wipe(self_functionCalls)
-	wipe(self.customFunctionNode)
-	OvaleCooldown:ResetSharedCooldowns()
-
-	-- Return all existing nodes to the node pool.
-	for i, node in pairs(self_node) do
-		self_node[i] = nil
-		self_timeSpanPool:Release(node.timeSpan)
-		self_pool:Release(node)
-	end
-	wipe(self_node)
-
-	-- Loop and strip out comments and replace Include() directives until there
-	-- are no more inclusions to make.
-	while true do
-		local was = text
-		text = strgsub(text, "#.-\n","")
-		text = strgsub(text, "#.*$","")
-		text = strgsub(text, "Include%s*%(%s*([%w_]+)%s*%)", ParseInclude)
-		if was == text then
-			break
-		end
-	end
-
-	text = CompileDeclarations(text)
-	text = CompileInputs(text)
-
-	for name, p, t in strgmatch(text, "AddFunction%s+(%w+)%s*(.-)%s*(%b{})") do
-		local node = ParseAddFunction(name, p, t)
-		if node then
-			self_customFunctions[name] = node
-			local nodeId = strmatch(node, "node(%d+)")
-			self.customFunctionNode[name] = self_node[tonumber(nodeId)]
-		end
-	end
-
-	-- On compile les AddIcon
-	wipe(self_masterNodes)
-
-	for p,t in strgmatch(text, "AddActionIcon%s*(.-)%s*(%b{})") do
-		local node = ParseAddIcon(p,t,true)
-		if node then
-			tinsert(self_masterNodes, node)
-		end
-	end
-
-	for p,t in strgmatch(text, "AddIcon%s*(.-)%s*(%b{})") do
-		local node = ParseAddIcon(p,t)
-		if node then
-			tinsert(self_masterNodes, node)
-		end
-	end
-
-	-- Verify that all the functions called within the script are defined.
-	for p, v in pairs(self_functionCalls) do
-		if not (OVALE_FUNCTIONS[p] or self_customFunctions[p] or OvaleCondition:IsCondition(p)) then
-			Ovale:Errorf("Unknown function call: %s (node%s)", p, v.nodeId)
+local function EvaluateSpellInfo(node)
+	local ok = true
+	local spellId, parameters = node.spellId, node.params
+	if spellId and TestConditions(parameters) then
+		local si = OvaleData:SpellInfo(spellId)
+		for k, v in pairs(parameters) do
+			if k == "addduration" then
+				local value = tonumber(v)
+				if value then
+					si.duration = si.duration + value
+				else
+					ok = false
+					break
+				end
+			elseif k == "addcd" then
+				local value = tonumber(v)
+				if value then
+					si.cd = si.cd + value
+				else
+					ok = false
+					break
+				end
+			elseif k == "addlist" then
+				-- Add this buff to the named spell list.
+				local list = OvaleData.buffSpellList[v] or {}
+				list[spellId] = true
+				OvaleData.buffSpellList[v] = list
+			elseif k == "sharedcd" then
+				OvaleCooldown:AddSharedCooldown(v, spellId)
+			elseif not OvaleAST.PARAMETER_KEYWORD[k] then
+				si[k] = v
+			end
 		end
 	end
-
-	-- Add any missing spells found while compiling the script into the spellbook.
-	for k, v in pairs(self_missingSpellList) do
-		OvaleSpellBook:AddSpell(k, v)
-	end
-	profiler.Stop("OvaleCompile_CompileScript")
+	return ok
 end
 --</private-static-methods>

 --<public-static-methods>
 function OvaleCompile:OnInitialize()
 	-- Resolve module dependencies.
+	OvaleAST = Ovale.OvaleAST
 	OvaleCondition = Ovale.OvaleCondition
 	OvaleCooldown = Ovale.OvaleCooldown
 	OvaleData = Ovale.OvaleData
@@ -952,15 +381,15 @@ function OvaleCompile:OnInitialize()
 end

 function OvaleCompile:OnEnable()
-	self:RegisterMessage("PLAYER_REGEN_ENABLED")
 	self:RegisterMessage("Ovale_CheckBoxValueChanged", "EventHandler")
 	self:RegisterMessage("Ovale_EquipmentChanged")
 	self:RegisterMessage("Ovale_GlyphsChanged", "EventHandler")
 	self:RegisterMessage("Ovale_ListValueChanged", "EventHandler")
-	self:RegisterMessage("Ovale_ScriptChanged", "EventHandler")
+	self:RegisterMessage("Ovale_ScriptChanged", "CompileScript")
 	self:RegisterMessage("Ovale_SpellsChanged", "EventHandler")
 	self:RegisterMessage("Ovale_StanceChanged")
 	self:RegisterMessage("Ovale_TalentsChanged", "EventHandler")
+	self:SendMessage("Ovale_ScriptChanged")
 end

 function OvaleCompile:OnDisable()
@@ -972,11 +401,6 @@ function OvaleCompile:OnDisable()
 	self:UnregisterMessage("Ovale_SpellsChanged")
 	self:UnregisterMessage("Ovale_StanceChanged")
 	self:UnregisterMessage("Ovale_TalentsChanged")
-	self_pool:Drain()
-end
-
-function OvaleCompile:PLAYER_REGEN_ENABLED(event)
-	self_pool:Drain()
 end

 function OvaleCompile:Ovale_EquipmentChanged(event)
@@ -992,93 +416,92 @@ function OvaleCompile:Ovale_StanceChanged(event)
 end

 function OvaleCompile:EventHandler(event)
-	Ovale:DebugPrint(OVALE_COMPILE_DEBUG, event)
-	-- Advance age of current compilation state.
+	-- Advance age of the script evaluation state.
 	self_serial = self_serial + 1
+	Ovale:DebugPrintf(OVALE_COMPILE_DEBUG, "%s: advance age to %d.", event, self_serial)
 	Ovale.refreshNeeded["player"] = true
 end

-function OvaleCompile:Compile()
-	self_canCompile = self_canCompile or Ovale:IsPreloaded(self_requirePreload)
-	if self_canCompile then
-		local profile = OvaleOptions:GetProfile()
-		local source = profile.source
-		local code
-		if source and OvaleScripts.script[source] then
-			code = OvaleScripts.script[source].code
-		else
-			code = ""
+function OvaleCompile:CompileScript(event)
+	local profile = OvaleOptions:GetProfile()
+	local source = profile.source
+	Ovale:DebugPrintf(OVALE_COMPILE_DEBUG, "Compiling script '%s'.", source)
+	if self.ast then
+		OvaleAST:Release(self.ast)
+		self.ast = nil
+	end
+	local ast = OvaleAST:ParseScript(source)
+	if ast then
+		OvaleAST:Optimize(ast)
+		self.ast = ast
+	end
+	self:EventHandler(event)
+end
+
+function OvaleCompile:EvaluateScript()
+	profiler.Start("OvaleCompile_EvaluateScript")
+	self_canEvaluate = self_canEvaluate or Ovale:IsPreloaded(self_requirePreload)
+	if self_canEvaluate and self.ast then
+		Ovale:DebugPrint(OVALE_COMPILE_DEBUG, "Evaluating script.")
+		-- Reset compilation state.
+		local ok = true
+		self_compileOnItems = false
+		self_compileOnStances = false
+		wipe(self_icon)
+		OvaleCooldown:ResetSharedCooldowns()
+		self_timesEvaluated = self_timesEvaluated + 1
+
+		-- Evaluate every declaration node of the script.
+		for _, node in ipairs(self.ast.child) do
+			local nodeType = node.type
+			if nodeType == "checkbox" then
+				ok = EvaluateAddCheckBox(node)
+			elseif nodeType == "icon" then
+				ok = EvaluateAddIcon(node)
+			elseif nodeType == "list_item" then
+				ok = EvaluateAddListItem(node)
+			elseif nodeType == "item_info" then
+				ok = EvaluateItemInfo(node)
+			elseif nodeType == "list" then
+				ok = EvaluateList(node)
+			elseif nodeType == "score_spells" then
+				ok = EvaluateScoreSpells(node)
+			elseif nodeType == "spell_aura_list" then
+				ok = EvaluateSpellAuraList(node)
+			elseif nodeType == "spell_info" then
+				ok = EvaluateSpellInfo(node)
+			else
+				-- Any other top-level node types are no-ops when evaluating the script.
+			end
+			if not ok then
+				break
+			end
+		end
+		if ok then
+			Ovale:UpdateFrame()
 		end
-		CompileScript(code)
-		self_compileCount = self_compileCount + 1
-		Ovale:UpdateFrame()
 	end
+	profiler.Stop("OvaleCompile_EvaluateScript")
 end

 function OvaleCompile:GetFunctionNode(name)
-	return self.customFunctionNode[name]
+	local node
+	if self.ast and self.ast.annotation and self.ast.annotation.customFunction then
+		node = self.ast.annotation.customFunction[name]
+	end
+	return node
 end

-function OvaleCompile:GetMasterNodes()
-	-- Compile the script if it is outdated.
+function OvaleCompile:GetIconNodes()
+	-- Evaluate the script if it is outdated.
 	if not self.serial or self.serial < self_serial then
 		self.serial = self_serial
-		self:Compile()
+		self:EvaluateScript()
 	end
-	return self_masterNodes
+	return self_icon
 end

-function OvaleCompile:Debug(iconNumber)
-	iconNumber = iconNumber or 1
-	self_pool:Debug()
-	local masterNodes = self:GetMasterNodes()
-	Ovale:Print(self:DebugNode(masterNodes[iconNumber]))
-	Ovale:FormatPrint("Total number of script compilations: %d", self_compileCount)
-end
-
-function OvaleCompile:DebugNode(node)
-	local text
-	if (not node) then
-		return "#nil"
-	end
-	if (node.type == "group") then
-		text = "{"
-		for k,n in ipairs(node.nodes) do
-			text = text .. self:DebugNode(n) .. " "
-		end
-		text = text .. "}\n"
-	elseif (node.type == "action" or node.type == "function") then
-		text = node.func.."("
-		for k,p in pairs(node.params) do
-			text = text .. k.."=" .. p .. " "
-		end
-		text = text .. ")"
-	elseif (node.type == "customfunction") then
-		text = self:DebugNode(node.a)
-	elseif (node.type == "if") then
-		text = "if "..self:DebugNode(node.a).." "..self:DebugNode(node.b)
-	elseif (node.type == "unless") then
-		text = "unless "..self:DebugNode(node.a).." "..self:DebugNode(node.b)
-	elseif (node.type == "wait") then
-		text = "wait "..self:DebugNode(node.a)
-	elseif (node.type == "and") then
-		text = self:DebugNode(node.a).." and "..self:DebugNode(node.b)
-	elseif (node.type == "or") then
-		text = self:DebugNode(node.a).." or "..self:DebugNode(node.b)
-	elseif (node.type == "not") then
-		text = "not "..self:DebugNode(node.a)
-	elseif node.type == "compare" then
-		text = self:DebugNode(node.a)..node.operator..self:DebugNode(node.b)
-	elseif node.type == "arithmetic" then
-		text = self:DebugNode(node.a)..node.operator..self:DebugNode(node.b)
-	elseif node.type == "lua" then
-		text = "["..node.lua.."]"
-	elseif node.type == "value" then
-		text = node.value
-	else
-		text = "#unknown node type "..node.type.."#"
-	end
-
-	return text
+function OvaleCompile:Debug()
+	Ovale:FormatPrint("Total number of times the script was evaluated: %d", self_timesEvaluated)
 end
 --</public-static-methods>
diff --git a/OvaleFrame.lua b/OvaleFrame.lua
index b5cebd0..ec62251 100644
--- a/OvaleFrame.lua
+++ b/OvaleFrame.lua
@@ -171,26 +171,25 @@ do
 		local refresh = forceRefresh or next(Ovale.refreshNeeded)
 		if not refresh then return end

-		local masterNodes = OvaleCompile:GetMasterNodes()
-		if not masterNodes then return end
+		local iconNodes = OvaleCompile:GetIconNodes()
+		if not iconNodes then return end

 		self.lastUpdate = now

 		local state = OvaleState.state
 		state:Initialize()
-		for k,node in pairs(masterNodes) do
-			local target
+		for k, node in ipairs(iconNodes) do
+			-- Set the true target of "target" references in the icon's body.
 			if node.params and node.params.target then
-				target = node.params.target
+				OvaleCondition.defaultTarget = node.params.target
 			else
-				target = "target"
+				OvaleCondition.defaultTarget = "target"
 			end
-			OvaleCondition.defaultTarget = target

 			if refresh then
-				Ovale:Logf("****Master Node %d", k)
+				Ovale:Logf("+++ Icon %d", k)
 				OvaleBestAction:StartNewAction(state)
-				local timeSpan, _, element = OvaleBestAction:Compute(node, state)
+				local timeSpan, _, element = OvaleBestAction:Compute(node.child[1], state)
 				local start = NextTime(timeSpan, state.currentTime)
 				if start then
 					Ovale:Logf("Compute start = %f", start)
@@ -213,8 +212,11 @@ do
 				else
 					local actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
 						actionUsable, actionShortcut, actionIsCurrent, actionEnable,
-						actionType, actionId, actionTarget, noRed = OvaleBestAction:GetActionInfo(element, state)
-					if noRed then
+						actionType, actionId, actionTarget = OvaleBestAction:GetActionInfo(element, state)
+
+					-- Use the start time of the best action instead of the intersection of its start time
+					-- with any conditions used to determine the best action.
+					if element and element.params and element.params.nored == 1 then
 						start = actionCooldownStart + actionCooldownDuration
 						if start < state.currentTime then
 							start = state.currentTime
@@ -238,9 +240,7 @@ do
 						action.spellId = nil
 					end
 					if start and start <= now and actionUsable then
-						if not action.waitStart then
-							action.waitStart = now
-						end
+						action.waitStart = action.waitStart or now
 					else
 						action.waitStart = nil
 					end
@@ -269,7 +269,7 @@ do
 								spellTarget = OvaleCondition.defaultTarget
 							end
 							state:ApplySpell(spellId, OvaleGUID:GetGUID(spellTarget))
-							timeSpan, _, element = OvaleBestAction:Compute(node, state)
+							timeSpan, _, element = OvaleBestAction:Compute(node.child[1], state)
 							start = NextTime(timeSpan, state.currentTime)
 							icons[2]:Update(element, start, OvaleBestAction:GetActionInfo(element, state))
 						else
@@ -313,14 +313,14 @@ do
 		local maxWidth = 0
 		local top = 0

-		local masterNodes = OvaleCompile:GetMasterNodes()
-		if not masterNodes then return end
+		local iconNodes = OvaleCompile:GetIconNodes()
+		if not iconNodes then return end

 		local BARRE = 8

 		local margin = profile.apparence.margin

-		for k,node in pairs(masterNodes) do
+		for k, node in ipairs(iconNodes) do
 			if not self.actions[k] then
 				self.actions[k] = {icons={}, secureIcons={}}
 			end
diff --git a/OvaleOptions.lua b/OvaleOptions.lua
index 8da2c4d..742d031 100644
--- a/OvaleOptions.lua
+++ b/OvaleOptions.lua
@@ -544,12 +544,6 @@ local self_options =
 							desc = L["Debug GUID"],
 							type = "toggle",
 						},
-						missing_spells =
-						{
-							name = "Missing spells",
-							desc = L["Debug missing spells"],
-							type = "toggle",
-						},
 						paper_doll =
 						{
 							name = "Paper doll updates",
@@ -562,12 +556,6 @@ local self_options =
 							desc = L["Debug stat snapshots"],
 							type = "toggle",
 						},
-						unknown_spells =
-						{
-							name = "Unknown spells",
-							desc = L["Debug unknown spells"],
-							type = "toggle",
-						},
 					},
 					get = function(info) return OvaleOptions.db.profile.debug[info[#info]] end,
 					set = function(info, value) OvaleOptions.db.profile.debug[info[#info]] = value end,
diff --git a/conditions/Damage.lua b/conditions/Damage.lua
index 42e5242..890eea3 100644
--- a/conditions/Damage.lua
+++ b/conditions/Damage.lua
@@ -27,7 +27,7 @@ do
 			local name = si[paramName]
 			local node = OvaleCompile:GetFunctionNode(name)
 			if node then
-				local timeSpan, priority, element = OvaleBestAction:Compute(node, state)
+				local timeSpan, priority, element = OvaleBestAction:Compute(node.child[1], state)
 				if element and element.type == "value" then
 					local value = element.value + (state.currentTime - element.origin) * element.rate
 					return value
diff --git a/conditions/LastEstimatedDamage.lua b/conditions/LastEstimatedDamage.lua
index 0c7d623..fe9d95f 100644
--- a/conditions/LastEstimatedDamage.lua
+++ b/conditions/LastEstimatedDamage.lua
@@ -29,7 +29,7 @@ do
 			local name = si[paramName]
 			local node = OvaleCompile:GetFunctionNode(name)
 			if node then
-				local timeSpan, priority, element = OvaleBestAction:Compute(node, state)
+				local timeSpan, priority, element = OvaleBestAction:Compute(node.child[1], state)
 				if element and element.type == "value" then
 					local value = element.value + (state.currentTime - element.origin) * element.rate
 					return value