Quantcast

Implement SimulationCraft profile translator module.

Johnny C. Lam [04-21-14 - 05:03]
Implement SimulationCraft profile translator module.

This does a fairly straightforward translation from SimC action lists into
the equivalent Ovale script language.  It does an incomplete job -- it
only generates functions from the equivalently-named action lists in a
SimC profile, which need hand-editing and inclusion into Ovale icons.

Lexer implemented using coroutines taken from Penlight Lua library.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1312 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
Ovale.toc
OvaleLexer.lua
OvaleSimulationCraft.lua
compiler.pl
diff --git a/Ovale.toc b/Ovale.toc
index 1f3e235..f84e154 100644
--- a/Ovale.toc
+++ b/Ovale.toc
@@ -19,6 +19,7 @@ locales\files.xml
 Ovale.lua

 # Utility modules.
+OvaleLexer.lua
 OvalePool.lua
 OvalePoolGC.lua
 OvalePoolRefCount.lua
@@ -43,6 +44,7 @@ OvalePaperDoll.lua
 OvalePower.lua
 OvaleScore.lua
 OvaleScripts.lua
+OvaleSimulationCraft.lua
 OvaleSpellBook.lua
 OvaleStance.lua
 OvaleState.lua
diff --git a/OvaleLexer.lua b/OvaleLexer.lua
new file mode 100644
index 0000000..06d540a
--- /dev/null
+++ b/OvaleLexer.lua
@@ -0,0 +1,362 @@
+--[[--------------------------------------------------------------------
+  Copyright (C) 2009 Steve Donovan, David Manura.
+
+  Permission is hereby granted, free of charge, to any person obtaining a
+  copy of this software and associated documentation files (the
+  "Software"), to deal in the Software without restriction, including
+  without limitation the rights to use, copy, modify, merge, publish,
+  distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so, subject to
+  the following conditions:
+
+  The above copyright notice and this permission notice shall be included
+  in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+--]]--------------------------------------------------------------------
+--[[--------------------------------------------------------------------
+    Ovale Spell Priority
+    Copyright (C) 2014 Johnny C. Lam
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License in the LICENSE
+    file accompanying this program.
+--]]--------------------------------------------------------------------
+
+-- This file is modified from:
+-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/lexer.lua
+
+--- Lexical scanner for creating a sequence of tokens from text.
+-- `lexer.scan(s)` returns an iterator over all tokens found in the
+-- string `s`. This iterator returns two values, a token type string
+-- (such as 'string' for quoted string, 'iden' for identifier) and the value of the
+-- token.
+--
+-- Versions specialized for Lua and C are available; these also handle block comments
+-- and classify keywords as 'keyword' tokens. For example:
+--
+--    > s = 'for i=1,n do'
+--    > for t,v in lexer.lua(s)  do print(t,v) end
+--    keyword for
+--    iden    i
+--    =       =
+--    number  1
+--    ,       ,
+--    iden    n
+--    keyword do
+--
+-- See the Guide for further @{06-data.md.Lexical_Scanning|discussion}
+-- @module pl.lexer
+
+local _, Ovale = ...
+local OvaleLexer = {}
+Ovale.OvaleLexer = OvaleLexer
+
+--<private-static-properties>
+local append = table.insert
+local error = error
+local strfind = string.find
+local strsub = string.sub
+local type = type
+local yield, wrap = coroutine.yield, coroutine.wrap
+
+local plain_matches = nil
+
+local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+'
+local NUMBER2 = '^[%+%-]?%d+%.?%d*'
+local NUMBER3 = '^0x[%da-fA-F]+'
+local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+'
+local NUMBER5 = '^%d+%.?%d*'
+local IDEN = '^[%a_][%w_]*'
+local WSPACE = '^%s+'
+local STRING0 = [[^(['\"]).-\\%1]]
+local STRING1 = [[^(['\"]).-[^\]%1]]
+local STRING3 = "^((['\"])%2)" -- empty string
+local PREPRO = '^#.-[^\\]\n'
+--</private-static-properties>
+
+--<private-static-methods>
+local function assert_arg(idx,val,tp)
+    if type(val) ~= tp then
+        error("argument "..idx.." must be "..tp, 2)
+    end
+end
+
+local function tdump(tok)
+    return yield(tok,tok)
+end
+
+local function ndump(tok,options)
+    if options and options.number then
+        tok = tonumber(tok)
+    end
+    return yield("number",tok)
+end
+
+-- regular strings, single or double quotes; usually we want them
+-- without the quotes
+local function sdump(tok,options)
+    if options and options.string then
+        tok = strsub(tok,2,-2)
+    end
+    return yield("string",tok)
+end
+
+local function chdump(tok,options)
+    if options and options.string then
+        tok = strsub(tok,2,-2)
+    end
+    return yield("char",tok)
+end
+
+local function cdump(tok)
+    return yield('comment',tok)
+end
+
+local function wsdump (tok)
+    return yield("space",tok)
+end
+
+local function pdump (tok)
+    return yield('prepro',tok)
+end
+
+local function plain_vdump(tok)
+    return yield("iden",tok)
+end
+--<private-static-methods>
+
+--<public-static-methods>
+--- create a plain token iterator from a string or file-like object.
+-- @param s the string
+-- @param matches an optional match table (set of pattern-action pairs)
+-- @param filter a table of token types to exclude, by default {space=true}
+-- @param options a table of options; by default, {number=true,string=true},
+-- which means convert numbers and strip string quotes.
+function OvaleLexer.scan (s,matches,filter,options)
+    --assert_arg(1,s,'string')
+    local file = type(s) ~= 'string' and s
+    filter = filter or {space=true}
+    options = options or {number=true,string=true}
+    if filter then
+        if filter.space then filter[wsdump] = true end
+        if filter.comments then
+            filter[cdump] = true
+        end
+    end
+    if not matches then
+        if not plain_matches then
+            plain_matches = {
+                {WSPACE,wsdump},
+                {NUMBER3,ndump},
+                {IDEN,plain_vdump},
+                {NUMBER1,ndump},
+                {NUMBER2,ndump},
+                {STRING3,sdump},
+                {STRING0,sdump},
+                {STRING1,sdump},
+                {'^.',tdump}
+            }
+        end
+        matches = plain_matches
+    end
+    local function lex ()
+        local i1,i2,idx,res1,res2,tok,pat,fun,capt
+        local line = 1
+        if file then s = file:read()..'\n' end
+        local sz = #s
+        local idx = 1
+        --print('sz',sz)
+        while true do
+            for _,m in ipairs(matches) do
+                pat = m[1]
+                fun = m[2]
+                i1,i2 = strfind(s,pat,idx)
+                if i1 then
+                    tok = strsub(s,i1,i2)
+                    idx = i2 + 1
+                    if not (filter and filter[fun]) then
+                        res1,res2 = fun(tok,options)
+                    end
+                    if res1 then
+                        local tp = type(res1)
+                        -- insert a token list
+                        if tp=='table' then
+                            yield('','')
+                            for _,t in ipairs(res1) do
+                                yield(t[1],t[2])
+                            end
+                        elseif tp == 'string' then -- or search up to some special pattern
+                            i1,i2 = strfind(s,res1,idx)
+                            if i1 then
+                                tok = strsub(s,i1,i2)
+                                idx = i2 + 1
+                                yield('',tok)
+                            else
+                                yield('','')
+                                idx = sz + 1
+                            end
+                            --if idx > sz then return end
+                        else
+                            yield(line,idx)
+                        end
+                    end
+                    if idx > sz then
+                        if file then
+                            --repeat -- next non-empty line
+                                line = line + 1
+                                s = file:read()
+                                if not s then return end
+                            --until not s:match '^%s*$'
+                            s = s .. '\n'
+                            idx ,sz = 1,#s
+                            break
+                        else
+                            return
+                        end
+                    else break end
+                end
+            end
+        end
+    end
+    return wrap(lex)
+end
+
+local function isstring (s)
+    return type(s) == 'string'
+end
+
+--- insert tokens into a stream.
+-- @param tok a token stream
+-- @param a1 a string is the type, a table is a token list and
+-- a function is assumed to be a token-like iterator (returns type & value)
+-- @param a2 a string is the value
+function OvaleLexer.insert (tok,a1,a2)
+    if not a1 then return end
+    local ts
+    if isstring(a1) and isstring(a2) then
+        ts = {{a1,a2}}
+    elseif type(a1) == 'function' then
+        ts = {}
+        for t,v in a1() do
+            append(ts,{t,v})
+        end
+    else
+        ts = a1
+    end
+    tok(ts)
+end
+
+--- get everything in a stream upto a newline.
+-- @param tok a token stream
+-- @return a string
+function OvaleLexer.getline (tok)
+    local t,v = tok('.-\n')
+    return v
+end
+
+--- get current line number. <br>
+-- Only available if the input source is a file-like object.
+-- @param tok a token stream
+-- @return the line number and current column
+function OvaleLexer.lineno (tok)
+    return tok(0)
+end
+
+--- get the rest of the stream.
+-- @param tok a token stream
+-- @return a string
+function OvaleLexer.getrest (tok)
+    local t,v = tok('.+')
+    return v
+end
+
+--- get a list of parameters separated by a delimiter from a stream.
+-- @param tok the token stream
+-- @param endtoken end of list (default ')'). Can be '\n'
+-- @param delim separator (default ',')
+-- @return a list of token lists.
+function OvaleLexer.get_separated_list(tok,endtoken,delim)
+    endtoken = endtoken or ')'
+    delim = delim or ','
+    local parm_values = {}
+    local level = 1 -- used to count ( and )
+    local tl = {}
+    local function tappend (tl,t,val)
+        val = val or t
+        append(tl,{t,val})
+    end
+    local is_end
+    if endtoken == '\n' then
+        is_end = function(t,val)
+            return t == 'space' and val:find '\n'
+        end
+    else
+        is_end = function (t)
+            return t == endtoken
+        end
+    end
+    local token,value
+    while true do
+        token,value=tok()
+        if not token then return nil,'EOS' end -- end of stream is an error!
+        if is_end(token,value) and level == 1 then
+            append(parm_values,tl)
+            break
+        elseif token == '(' then
+            level = level + 1
+            tappend(tl,'(')
+        elseif token == ')' then
+            level = level - 1
+            if level == 0 then -- finished with parm list
+                append(parm_values,tl)
+                break
+            else
+                tappend(tl,')')
+            end
+        elseif token == delim and level == 1 then
+            append(parm_values,tl) -- a new parm
+            tl = {}
+        else
+            tappend(tl,token,value)
+        end
+    end
+    return parm_values,{token,value}
+end
+
+--- get the next non-space token from the stream.
+-- @param tok the token stream.
+function OvaleLexer.skipws (tok)
+    local t,v = tok()
+    while t == 'space' do
+        t,v = tok()
+    end
+    return t,v
+end
+
+local skipws = OvaleLexer.skipws
+
+--- get the next token, which must be of the expected type.
+-- Throws an error if this type does not match!
+-- @param tok the token stream
+-- @param expected_type the token type
+-- @param no_skip_ws whether we should skip whitespace
+function OvaleLexer.expecting (tok,expected_type,no_skip_ws)
+    assert_arg(1,tok,'function')
+    assert_arg(2,expected_type,'string')
+    local t,v
+    if no_skip_ws then
+        t,v = tok()
+    else
+        t,v = skipws(tok)
+    end
+    if t ~= expected_type then error ("expecting "..expected_type,2) end
+    return v
+end
+--</public-static-methods>
diff --git a/OvaleSimulationCraft.lua b/OvaleSimulationCraft.lua
new file mode 100644
index 0000000..751986b
--- /dev/null
+++ b/OvaleSimulationCraft.lua
@@ -0,0 +1,1121 @@
+--[[--------------------------------------------------------------------
+    Ovale Spell Priority
+    Copyright (C) 2014 Johnny C. Lam
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License in the LICENSE
+    file accompanying this program.
+--]]--------------------------------------------------------------------
+
+local _, Ovale = ...
+local OvaleSimulationCraft = {}
+Ovale.OvaleSimulationCraft = OvaleSimulationCraft
+
+--<private-static-properties>
+local OvaleLexer = Ovale.OvaleLexer
+
+local format = string.format
+local gmatch = string.gmatch
+local gsub = string.gsub
+local setmetatable = setmetatable
+local strfind = string.find
+local strlower = string.lower
+local strmatch = string.match
+local strsplit = strsplit
+local strupper = string.upper
+local tconcat = table.concat
+local tinsert = table.insert
+local tonumber = tonumber
+local tremove = table.remove
+local tsort = table.sort
+local type = type
+local wipe = table.wipe
+local yield = coroutine.yield
+local RAID_CLASS_COLORS = RAID_CLASS_COLORS
+
+local INDENTATION = {}
+do
+	INDENTATION[0] = ""
+	local metatable = {
+		__index = function(tbl, key)
+				key = tonumber(key)
+				if key > 0 then
+					local s = tbl[key - 1] .. "	"
+					rawset(tbl, key, s)
+					return s
+				end
+				return INDENTATION[0]
+			end,
+	}
+	setmetatable(INDENTATION, metatable)
+end
+
+local SIMC_CLASS = {}
+do
+	for class in pairs(RAID_CLASS_COLORS) do
+		SIMC_CLASS[strlower(class)] = true
+	end
+end
+--<private-static-properties>
+
+--<public-static-properties>
+OvaleSimulationCraft.simcString = nil
+OvaleSimulationCraft.simcComments = nil
+OvaleSimulationCraft.indent = 0
+OvaleSimulationCraft.profile = {}
+OvaleSimulationCraft.symbols = {}
+OvaleSimulationCraft.script = {}
+OvaleSimulationCraft.__index = OvaleSimulationCraft
+do
+	-- Class constructor
+	setmetatable(OvaleSimulationCraft, { __call = function(self, ...) return self:New(...) end })
+end
+--</public-static-properties>
+
+--<private-static-methods>
+local function NameValuePair(expr)
+	local name, value = strmatch(expr, "^([^=]*)=(.*)")
+	if strmatch(value, "^[%-]?%d+%.?%d*$") then
+		value = tonumber(value)
+	end
+	return name, value
+end
+--</private-static-methods>
+
+--<public-static-methods>
+function OvaleSimulationCraft:New(simcString)
+	local obj = {
+		simcString = nil,
+		simcComments = nil,
+		indent = 0,
+		profile = {},
+		symbols = {},
+		script = {},
+	}
+	setmetatable(obj, self)
+	if simcString then
+		obj:ParseProfile(simcString)
+	end
+	return obj
+end
+
+function OvaleSimulationCraft:Indent()
+	self.indent = self.indent + 1
+end
+
+function OvaleSimulationCraft:UnIndent()
+	self.indent = (self.indent > 0) and (self.indent - 1) or 0
+end
+
+function OvaleSimulationCraft:Append(script, ...)
+	local s = format("%s%s", INDENTATION[self.indent], format(...))
+	tinsert(script, s)
+end
+
+function OvaleSimulationCraft:ParseProfile(simcString)
+	self.simcString = simcString
+	local profile = self.profile
+	for line in gmatch(simcString, "[^\r\n]+") do
+		line = strmatch(line, "^%s*(.-)%s*$")
+		if not (strmatch(line, "^#.*") or strmatch(line, "^$")) then
+			local key, value = strmatch(line, "([^%+=]+)%+?=(.*)")
+			if not profile[key] then
+				profile[key] = value
+			elseif type(profile[key]) == "table" then
+				tinsert(profile[key], value)
+			else
+				local oldValue = profile[key]
+				profile[key] = {}
+				tinsert(profile[key], oldValue)
+				tinsert(profile[key], value)
+			end
+		end
+	end
+	-- Concatenate variables defined over multiple lines using +=
+	for k, v in pairs(profile) do
+		if type(v) == "table" then
+			local value = tconcat(v)
+			profile[k] = value
+		end
+	end
+	for k, v in pairs(profile) do
+		if strmatch(k, "^actions") then
+			local listName = strmatch(k, "^actions[.]([%w_]+)") or "default"
+			profile[k] = nil
+			profile.actionList = profile.actionList or {}
+			local tbl = { strsplit("/", v) }
+			for i, action in ipairs(tbl) do
+				local line = tbl[i]
+				tbl[i] = { strsplit(",", action) }
+				tbl[i].action = tremove(tbl[i], 1)
+				tbl[i].line = line
+			end
+			profile.actionList[listName] = tbl
+		elseif k == "glyphs" then
+			profile[k] = { strsplit("/", v) }
+		elseif k == "professions" then
+			local tbl = { strsplit("/", v) }
+			for i, profession in ipairs(tbl) do
+				local prof, level = NameValuePair(profession)
+				tbl[i] = nil
+				tbl[prof] = level
+			end
+			profile[k] = tbl
+		end
+	end
+end
+
+do
+	local symbols = {}
+
+	function OvaleSimulationCraft:GenerateScript(script)
+		script = script or {}
+		local profile = self.profile
+		for class in pairs(SIMC_CLASS) do
+			local simcName = profile[class]
+			if simcName then
+				profile.class = class
+				self:Append(script, "# Based on SimulationCraft profile %s.", simcName)
+				self:Append(script, "#	class=%s", class)
+			end
+		end
+		if profile.spec then
+			self:Append(script, "#	spec=%s", profile.spec)
+		end
+		if profile.talents then
+			self:Append(script, "#	talents=%s", profile.talents)
+		end
+		if profile.glyphs then
+			self:Append(script, "#	glyphs=%s", tconcat(profile.glyphs, "/"))
+		end
+		if profile.default_pet then
+			self:Append(script, "#	pet=%s", profile.default_pet)
+		end
+		if profile.actionList then
+			for listName, actionList in pairs(profile.actionList) do
+				self:ParseActionList(script, listName, actionList)
+			end
+		end
+		self:Append(script, "")
+
+		tsort(self.symbols)
+		wipe(symbols)
+		for _, v in ipairs(self.symbols) do
+			if not symbols[v] then
+				symbols[v] = true
+				tinsert(symbols, v)
+			end
+		end
+		self:Append(script, "### Pre-defined symbols")
+		for _, v in ipairs(symbols) do
+			self:Append(script, format("# %s", v))
+		end
+		return script
+	end
+end
+
+do
+	local function TitleCase(first, rest)
+		return strupper(first) .. strlower(rest)
+	end
+
+	function OvaleSimulationCraft:FunctionName(listName)
+		if self.profile.spec then
+			listName = format("%s_%s_actions", self.profile.spec, listName)
+		else
+			listName = format("%s_actions", listName)
+		end
+		listName = gsub(listName, "_", " ")
+		listName = gsub(listName, "(%a)(%w*)", TitleCase)
+		listName = gsub(listName, "%s", "")
+		return listName
+	end
+end
+
+function OvaleSimulationCraft:ParseActionList(script, listName, actionList)
+	self:Append(script, "")
+	self:Append(script, "AddFunction %s", self:FunctionName(listName))
+	self:Append(script, "{")
+	self:Indent()
+	local indent = self.indent
+	for i, actionLine in ipairs(actionList) do
+		self:ParseActionLine(script, actionLine)
+	end
+	while self.indent > indent do
+		self:UnIndent()
+		self:Append(script, "}")
+	end
+	self:UnIndent()
+	self:Append(script, "}")
+end
+
+do
+	local TO_NAME = {
+		druid = {
+			balance = {
+				["dream_of_cenarius"] = "dream_of_cenarius_caster",
+				["incarnation"] = "incarnation_caster",
+				["wild_mushroom"] = "wild_mushroom_caster",
+			}
+		},
+	}
+
+	function OvaleSimulationCraft:Name(name)
+		local class, spec = self.profile.class, self.profile.spec
+		if TO_NAME[class] and TO_NAME[class][spec] and TO_NAME[class][spec][name] then
+			return TO_NAME[class][spec][name]
+		end
+		return name
+	end
+end
+
+do
+	local SIMC_ACTION = {
+		["_potion$"] = function(simc, action) return format("Item(%s)", action) end,
+		["^$"] = false,
+		["^auto_attack$"] = false,
+		["^flask$"] = false,
+		["^food$"] = false,
+		["^snapshot_stats$"] = false,
+		-- Death Knight
+		["^blood_presence$"] = "if not Stance(deathknight_blood_presence) Spell(blood_presence)",
+		["^frost_presence$"] = "if not Stance(deathknight_frost_presence) Spell(frost_presence)",
+		["^unholy_presence$"] = "if not Stance(deathknight_unholy_presence) Spell(unholy_presence)",
+		-- Hunter
+		["^aspect_of_the_"] = function(simc, action) return format("if not Stance(hunter_%s) Spell(%s)", action, action) end,
+		-- Mage
+		["^arcane_brilliance$"] = "if BuffExpires(critical_strike any=1) or BuffExpires(spell_power_multiplier any=1) Spell(arcane_brilliance)",
+		["^cancel_buff$"] = false,
+		["^frost_armor$"] = function(simc, action)
+				tinsert(simc.symbols, "frost_armor_buff")
+				return "if BuffExpires(frost_armor_buff) Spell(frost_armor)"
+			end,
+		["^molten_armor$"] = function(simc, action)
+				tinsert(simc.symbols, "molten_armor_buff")
+				return "if BuffExpires(molten_armor_buff) Spell(molten_armor)"
+			end,
+		["^rune_of_power$"] = false, -- XXX
+		-- Monk
+		["^chi_sphere$"] = false,
+		-- Paladin
+		["^rebuke$"] = "if target.IsInterruptible() Spell(rebuke)",
+		["^seal_of_"] = function(simc, action) return format("if not Stance(paladin_%s) Spell(%s)", action, action) end,
+		-- Priest
+		["^inner_fire$"] = function(simc, action)
+				tinsert(simc.symbols, "inner_fire_buff")
+				return "if BuffExpires(inner_fire_buff) Spell(inner_fire)"
+			end,
+		["^shadowform$"] = "if not Stance(priest_shadowform) Spell(shadowform)",
+		-- Rogue
+		["^apply_poison$"] = false,	-- XXX
+		["^kick$"] = "if target.IsInterruptible() Spell(kick)",
+		["^stealth$"] = "if Stealthed(no) Spell(stealth)",
+	}
+
+	local scriptLine = {}
+
+	function OvaleSimulationCraft:ParseActionLine(script, actionLine)
+		wipe(scriptLine)
+		if self.simcComments then
+			self:Append(script, "#%s", actionLine.line)
+		end
+
+		local action = self:Name(actionLine.action)
+		local matchedAction = false
+		for pattern, result in pairs(SIMC_ACTION) do
+			if strmatch(action, pattern) then
+				matchedAction = true
+				scriptLine.action = (type(result) == "function") and result(self, action) or result
+				break
+			end
+		end
+		if not matchedAction then
+			scriptLine.action = format("Spell(%s)", action)
+		end
+
+		if scriptLine.action then
+			local addActionSymbol = false
+			if #actionLine == 0 then
+				addActionSymbol = true
+			else
+				for i, expr in ipairs(actionLine) do
+					local name, value = NameValuePair(expr)
+					if action == "use_item" then
+						if name == "slot" then
+							if value == "hands" then
+								scriptLine.action = "Item(HandsSlot usable=1)"
+							elseif value == "trinket" then
+								scriptLine.action = "{ Item(Trinket0Slot usable=1) Item(Trinket1Slot usable=1) }"
+							end
+						elseif name == "name" then
+							if strmatch(value, "gauntlets") or strmatch(value, "gloves") or strmatch(value, "grips") or strmatch(value, "handguards") then
+								scriptLine.action = "Item(HandsSlot usable=1)"
+							end
+						end
+					elseif action == "wait" then
+						if name == "sec" then
+							if type(value) == "number" then
+								scriptLine.action = nil
+							else
+								local spellName = strmatch(value, "^cooldown%.([%w_]+)%.remains$")
+								if spellName then
+									scriptLine.action = format("wait Spell(%s)", spellName)
+								end
+							end
+						end
+					elseif action == "swap_action_list" then
+						if name == "name" then
+							scriptLine.action = format("%s()", self:FunctionName(value))
+						end
+					elseif action == "run_action_list" then
+						if name == "name" then
+							scriptLine.action = format("%s()", self:FunctionName(value))
+						end
+					elseif action == "pool_resource" then
+						scriptLine.action = nil
+						if name == "for_next" and value == 1 then
+							script.for_next = true
+						end
+					elseif action == "stance" then
+						-- Don't add symbol for "stance".
+					else
+						addActionSymbol = true
+					end
+					if name == "if" then
+						scriptLine.if_expr = self:ParseExpression(action, value, addActionSymbol)
+					else
+						scriptLine[name] = value
+					end
+				end
+			end
+			if addActionSymbol then
+				tinsert(self.symbols, action)
+			end
+
+			if scriptLine.if_expr
+				or scriptLine.moving == 1
+				or scriptLine.sync
+				or action == "focus_fire" and scriptLine.five_stacks == 1
+				or action == "stance" and scriptLine.choose
+				or scriptLine.weapon
+			then
+				local needAnd = false
+				if action == "pool_resource" and not script.for_next then
+					tinsert(scriptLine, "unless")
+				else
+					tinsert(scriptLine, "if")
+				end
+				if scriptLine.moving == 1 then
+					tinsert(scriptLine, "Speed() > 0")
+					needAnd = true
+				end
+				if scriptLine.sync then
+					if needAnd then
+						tinsert(scriptLine, "and")
+					end
+					tinsert(scriptLine, format("Spell(%s)", scriptLine.sync))
+					needAnd = true
+				end
+				if scriptLine.weapon then
+					if needAnd then
+						tinsert(scriptLine, "and")
+					end
+					tinsert(scriptLine, format("WeaponEnchantExpires(%s)", scriptLine.weapon))
+					needAnd = true
+				end
+				if action == "focus_fire" and scriptLine.five_stacks == 1 then
+					if needAnd then
+						tinsert(scriptLine, "and")
+					end
+					tinsert(scriptLine, "BuffStacks(pet_frenzy any=1) == 5")
+					needAnd = true
+				end
+				if action == "stance" and scriptLine.choose then
+					if needAnd then
+						tinsert(scriptLine, "and")
+					end
+					local class = self.profile.class
+					if class == "deathknight" then
+						local spellName = format("%s_presence", scriptLine.choose)
+						scriptLine.action = format("Spell(%s)", spellName)
+						tinsert(scriptLine, format("not Stance(%s_%s)", class, spellName))
+					elseif class == "monk" then
+						local spellName = format("stance_of_the_%s", scriptLine.choose)
+						scriptLine.action = format("Spell(%s)", spellName)
+						tinsert(scriptLine, format("not Stance(%s_%s)", class, spellName))
+					elseif class == "warrior" then
+						local spellName = format("%s_stance", scriptLine.choose)
+						scriptLine.action = format("Spell(%s)", spellName)
+						tinsert(scriptLine, format("not Stance(%s_%s)", class, spellName))
+					else
+						tinsert(scriptLine, format("not Stance(%s_%s)", class, scriptLine.choose))
+					end
+					needAnd = true
+				end
+				if scriptLine.if_expr then
+					if needAnd then
+						tinsert(scriptLine, "and")
+					end
+					if needAnd and strmatch(scriptLine.if_expr, " or ") then
+						tinsert(scriptLine, format("{ %s }", scriptLine.if_expr))
+					else
+						tinsert(scriptLine, scriptLine.if_expr)
+					end
+					needAnd = true
+				end
+			end
+
+			if action ~= "pool_resource" then
+				if script.for_next then
+					tinsert(scriptLine, "wait")
+					script.for_next = nil
+				end
+				tinsert(scriptLine, scriptLine.action)
+			end
+
+			if scriptLine[1] then
+				self:Append(script, tconcat(scriptLine, " "))
+			end
+			if action == "pool_resource" and not script.for_next then
+				self:Append(script, "{")
+				self:Indent()
+			end
+		end
+	end
+end
+
+do
+	-- Table of matching tokens for SimC conditional expressions for OvaleLexer.scan().
+	local MATCHES = {}
+	local function chdump(tok, options)
+		if options and options.string then
+			tok = strsub(tok, 2, -2)
+		end
+		return yield("char", tok)
+	end
+
+	local function ndump(tok, options)
+		if options and options.number then
+			tok = tonumber(tok)
+		end
+		return yield("number", tok)
+	end
+
+	local function tdump(tok)
+		return yield(tok, tok)
+	end
+
+	local function vdump(tok)
+		return yield("iden", tok)
+	end
+
+	local function wsdump(tok)
+		return yield("space", tok)
+	end
+
+	-- whitespace
+	tinsert(MATCHES, { '^%s+', wsdump })
+	-- numbers
+	tinsert(MATCHES, { '^[%-]?%d+%.?%d*', ndump })
+	-- floor/ceil
+	tinsert(MATCHES, { '^floor', tdump })
+	tinsert(MATCHES, { '^ceil', tdump })
+	-- identifiers (foo.bar.baz.etc)
+	tinsert(MATCHES, { '^[%a_][%w_%.]*[%w_]', vdump })
+	-- not-equal
+	tinsert(MATCHES, { '^!=', tdump })
+	-- less-than-equal
+	tinsert(MATCHES, { '^<=', tdump })
+	-- greater-than-equal
+	tinsert(MATCHES, { '^>=', tdump })
+	-- exclusion
+	tinsert(MATCHES, { '^!~', tdump })
+	-- catch-all
+	tinsert(MATCHES, { '^.', tdump })
+
+	--[[
+		The "buff" list also contains special buffs such as:
+			"bleeding" (target is bleeding),
+			"casting" (character is casting),
+			"raid_movement" (character is moving because of a "movement" raid event),
+			"poisoned" (target is poisoned),
+			"self_movement" (character is moving because of a start_move action),
+			"stunned" (character is stunned),
+			"vulnerable" (target is vulnerable),
+			"stealthed" (character is stealthed).
+	--]]
+	local SPECIAL_PROPERTY = {
+		["^debuff%.casting%.react$"] = "IsInterruptible()",
+		["^debuff%.flying%.down$"] = "True(not flying_debuff)",
+		["^buff%.raid_movement%.duration$"] = "0",
+		-- Pretend the target can never be invulnerable.
+		["^debuff%.invulnerable%.react$"] = "InCombat(no)",
+		["^buff%.bloodlust%.react$"] = "BuffPresent(burst_haste any=1)",
+		["^buff%.bloodlust%.up$"] = "BuffPresent(burst_haste any=1)",
+		["^buff%.bloodlust%.down$"] = "BuffExpires(burst_haste any=1)",
+		["^buff%.stealthed%.down$"] = "Stealthed(no)",
+		["^buff%.stealthed%.up$"] = "Stealthed()",
+
+		-- Druid
+		["^buff%.wild_mushroom%.max_stack$"] = function(simc, action)
+				local class, spec = simc.profile.class, simc.profile.spec
+				if class == "druid" and spec == "restoration" then
+					return "1"
+				end
+				return "3"
+			end,
+		["^buff%.wild_mushroom%.stack$"] = "WildMushroomCount()",
+		-- Hunter
+		["^debuff%.ranged_vulnerability%.up$"] = "target.DebuffPresent(ranged_vulnerability any=1)",
+		-- Mage
+		["^buff%.arcane_charge%.stack$"] = function(simc, action)
+				tinsert(simc.symbols, "arcane_charge_debuff")
+				return "DebuffStacks(arcane_charge_debuff)"
+			end,
+		-- Monk
+		["^dot%.zen_sphere%.ticking$"] = function(simc, action)
+				tinsert(simc.symbols, "zen_sphere_buff")
+				return "BuffPresent(zen_sphere_buff)"
+			end,
+		-- Rogue
+		["^buff%.stealth%.down$"] = "Stealthed(no)",
+		["^buff%.stealth%.up$"] = "Stealthed()",
+		-- Shaman
+		["^active_flame_shock$"] = function(simc, action)
+				tinsert(simc.symbols, "flame_shock_debuff")
+				return "DebuffCount(flame_shock_debuff)"
+			end,
+		["^buff%.lightning_shield%.max_stack$"] = "7",
+		["^buff%.lightning_shield%.react$"] = function(simc, action)
+				tinsert(simc.symbols, "lightning_shield_buff")
+				return "BuffStacks(lightning_shield_buff)"
+			end,
+		["^pet%.greater_fire_elemental%.active$"] = "TotemPresent(fire totem=fire_elemental_totem)",
+		["^pet%.primal_fire_elemental%.active$"] = "TotemPresent(fire totem=fire_elemental_totem)",
+	}
+
+	-- totem.<totem_name>.<totem_property>
+	local TOTEM_TYPE = {
+		["capacitor_totem"] = "air",
+		["earth_elemental_totem"] = "earth",
+		["earthbind_totem"] = "earth",
+		["earthgrab_totem"] = "earth",
+		["fire_elemental_totem"] = "fire",
+		["grounding_totem"] = "air",
+		["healing_stream_totem"] = "water",
+		["healing_tide_totem"] = "water",
+		["magma_totem"] = "fire",
+		["mana_tide_totem"] = "water",
+		["searing_totem"] = "fire",
+		["stone_bulwark_totem"] = "earth",
+		["stormlash_totem"] = "air",
+		["tremor_totem"] = "earth",
+		["windwalk_totem"] = "air",
+	}
+	local TOTEM_PROPERTY_PATTERN = "^totem%.([%w_]+)%.([%w_]+)$"
+	local TOTEM_PROPERTY = {
+		["^active$"] = function(simc, totemName)
+				if TOTEM_TYPE[totemName] then
+					return format("TotemPresent(%s totem=%s)", TOTEM_TYPE[totemName], totemName)
+				else
+					return format("TotemPresent(%s)", totemName)
+				end
+			end,
+		["^remains$"] = function(simc, totemName)
+				if TOTEM_TYPE[totemName] then
+					return format("TotemRemains(%s totem=%s)", TOTEM_TYPE[totemName], totemName)
+				else
+					return format("TotemRemains(%s)", totemName)
+				end
+			end,
+	}
+
+	local ACTION_PROPERTY_PATTERN = "^action%.([%w_]+)%.([%w_]+)$"
+	local ACTION_PROPERTY = {
+		-- TODO: Buff?
+		["^active$"] = function(simc, actionName)
+				if strmatch(actionName, "_totem$") then
+					return TOTEM_PROPERTY["^active$"](simc, actionName)
+				else
+					local symbol = format("%s_debuff", actionName)
+					tinsert(simc.symbols, symbol)
+					return format("target.DebuffPresent(%s)", symbol)
+				end
+			end,
+		["^add_ticks$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("TicksAdded(%s)", symbol)
+			end,
+		["^ember_react$"] = "BurningEmbers() >= 1",
+		["^cast_delay$"] = "True(cast_delay)",
+		["^cast_time$"] = function(simc, actionName) return format("CastTime(%s)", actionName) end,
+		["^charges$"] = function(simc, actionName) return format("Charges(%s)", actionName) end,
+		-- TODO: ItemCooldown?
+		["^cooldown$"] = function(simc, actionName) return format("SpellCooldown(%s)", actionName) end,
+		-- TODO: Item?
+		["^cooldown_react$"] = function(simc, actionName) return format("Spell(%s)", actionName) end,
+		["^crit_damage$"] = function(simc, actionName) return format("CritDamage(%s)", actionName) end,
+		-- TODO: Melee/Ranged/Spell crit chance depending on type of attack, or at least class of player.
+		["^crit_pct_current$"] = function(simc, actionName) return format("SpellCritChance(%s)", actionName) end,
+		["^crit_tick_damage$"] = function(simc, actionName) return format("CritDamage(%s)", actionName) end,
+		["^duration$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("SpellData(%s duration)", symbol)
+			end,
+		["^enabled$"] = function(simc, actionName)
+				local symbol = format("%s_talent", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("TalentPoints(%s)", symbol)
+			end,
+		["^gcd$"] = function(simc, actionName) return format("GCD(%s)", actionName) end,
+		["^hit_damage$"] = function(simc, actionName) return format("Damage(%s)", actionName) end,
+		["^in_flight$"] = function(simc, actionName) return format("InFlightToTarget(%s)", actionName) end,
+		["^in_flight_to_target$"] = function(simc, actionName) return format("InFlightToTarget(%s)", actionName) end,
+		["^miss_react$"] = "True(miss_react)",
+		-- TODO: Buff?
+		["^n_ticks$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("target.Ticks(%s)", symbol)
+			end,
+		["^recharge_time$"] = function(simc, actionName) return format("SpellChargeCooldown(%s)", actionName) end,
+		-- TODO: Buff?
+		["^remains$"] = function(simc, actionName)
+				if strmatch(actionName, "_totem$") then
+					return TOTEM_PROPERTY["^remains$"](simc, actionName)
+				else
+					local symbol = format("%s_debuff", actionName)
+					tinsert(simc.symbols, symbol)
+					return format("target.DebuffRemains(%s)", symbol)
+				end
+			end,
+		["^shard_react$"] = "SoulShards() >= 1",
+		["^spell_power$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("target.DebuffSpellpower(%s)", symbol)
+			end,
+		["^tick_damage$"] = function(simc, actionName) return format("Damage(%s)", actionName) end,
+		["^tick_multiplier$"] = function(simc, actionName) return format("target.DamageMultiplier(%s)", actionName) end,
+		-- TODO: Buff?
+		["^tick_time$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("target.TickTime(%s)", symbol)
+			end,
+		-- TODO: Buff?
+		["^ticking$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("target.DebuffPresent(%s)", symbol)
+			end,
+		-- TODO: Buff?
+		["^ticks_remain$"] = function(simc, actionName)
+				local symbol = format("%s_debuff", actionName)
+				tinsert(simc.symbols, symbol)
+				return format("target.TicksRemain(%s)", symbol)
+			end,
+		["^travel_time$"] = function(simc, actionName) return format("TravelTime(%s)", actionName) end,
+	}
+
+	local CHARACTER_PROPERTY = {
+		["^anticipation_charges"] = function(simc, pattern, token)
+				tinsert(simc.symbols, "anticipation_buff")
+				return "BuffStacks(anticipation_buff)"
+			end,
+		["^burning_ember$"] = "BurningEmbers()",
+		["^chi$"] = "Chi()",
+		["^chi%.max$"] = "MaxChi()",
+		["^combo_points$"] = "ComboPoints()",
+		["^demonic_fury$"] = "DemonicFury()",
+		["^eclipse$"] = "Eclipse()",
+		["^eclipse_dir$"] = "EclipseDir()",
+		["^energy$"] = "Energy()",
+		["^energy%.regen$"] = "EnergyRegen()",
+		["^energy%.time_to_max$"] = "TimeToMaxEnergy()",
+		["^focus$"] = "Focus()",
+		["^focus%.regen$"] = "FocusRegen()",
+		["^focus%.time_to_max$"] = "TimeToMaxFocus()",
+		["^health$"] = "Health()",
+		["^health%.deficit$"] = "HealthMissing()",
+		["^health%.max$"] = "MaxHealth()",
+		["^health%.pct$"] = "HealthPercent()",
+		["^holy_power$"] = "HolyPower()",
+		["^incoming_damage_([%d]+)(m?s)$"] = function(simc, pattern, token)
+				local seconds, measure = strmatch(token, pattern)
+				seconds = tonumber(seconds)
+				if measure == "ms" then
+					seconds = seconds / 1000
+				end
+				return format("IncomingDamage(%.3f)", seconds)
+			end,
+		["^in_combat$"] = "InCombat()",
+		["^level$"] = "Level()",
+		["^mana$"] = "Mana()",
+		["^mana%.deficit$"] = "ManaDeficit()",
+		["^mana%.max$"] = "MaxMana()",
+		["^mana%.max_nonproc$"] = "MaxMana()",
+		["^mana%.pct$"] = "ManaPercent()",
+		["^mana%.pct_nonproc$"] = "ManaPercent()",
+		["^mana_gem_charges$"] = function(simc, pattern, token)
+				tinsert(simc.symbols, "mana_gem")
+				return "ItemCharges(mana_gem)"
+			end,
+		["^mastery_value$"] = "MasteryEffect()",
+		["^multiplier$"] = "DamageMultiplier()",
+		["^position_front$"] = "False(position_front)",	-- XXX
+		["^rage$"] = "Rage()",
+		["^rage%.max$"] = "MaxRage()",
+		["^runic_power$"] = "RunicPower()",
+		["^shadow_orb$"] = "ShadowOrbs()",
+		["^soul_shards$"] = "SoulShards()",
+		["^stat%.agility$"] = "Agility()",
+		["^stat%.attack_power$"] = "AttackPower()",
+		["^stat%.crit$"] = "CritRating()",
+		["^stat%.crit_rating$"] = "CritRating()",
+		["^stat%.energy$"] = "Energy()",
+		["^stat%.focus$"] = "Focus()",
+		["^stat%.haste_rating$"] = "HasteRating()",
+		["^stat%.health$"] = "Health()",
+		["^stat%.intellect$"] = "Intellect()",
+		["^stat%.mana$"] = "Mana()",
+		["^stat%.mastery_rating$"] = "MasteryRating()",
+		["^stat%.maximum_energy$"] = "MaxEnergy()",
+		["^stat%.maximum_focus$"] = "MaxFocus()",
+		["^stat%.maximum_health$"] = "MaxHealth()",
+		["^stat%.maximum_mana$"] = "MaxMana()",
+		["^stat%.maximum_runic$"] = "MaxRunicPower()",
+		["^stat%.rage$"] = "Rage()",
+		["^stat%.runic$"] = "RunicPower()",
+		["^stat%.spell_power$"] = "Spellpower()",
+		["^stat%.spirit$"] = "Spirit()",
+		["^stat%.stamina$"] = "Stamina()",
+		["^stat%.strength$"] = "Strength()",
+		["^time_to_die$"] = "TimeToDie()",
+	}
+
+	-- aura.<aura_name>.<aura_property>
+	local AURA_PROPERTY_PATTERN = "^aura%.([%w_]+)%.([%w_]+)$"
+	local AURA_PROPERTY = {
+		["^down$"] = function(simc, name) return format("BuffExpires(%s any=1)", name) end,
+		["^stack$"] = function(simc, name) return format("BuffStacks(%s any=1)", name) end,
+		["^react$"] = function(simc, name) return format("BuffPresent(%s any=1)", name) end,
+		["^remains$"] = function(simc, name) return format("BuffRemains(%s any=1)", name) end,
+		["^up$"] = function(simc, name) return format("BuffPresent(%s any=1)", name) end,
+	}
+
+	-- (buff|debuff).<aura_name>.<aura_property>
+	local function BuffType(buffType) return (buffType == "debuff") and "Debuff" or "Buff" end
+	local BUFF_PROPERTY_PATTERN = "^(d?e?buff)%.([%w_]+)%.([%w_]+)$"
+	local BUFF_PROPERTY = {
+		-- XXX: Assume that the spell and the buff have the same name.
+		["^cooldown_remains$"] = function(simc, buffType, name) return format("SpellCooldown(%s)", name) end,
+		["^down$"] = function(simc, buffType, name) return format("%sExpires(%s_%s)", BuffType(buffType), name, buffType) end,
+		["^duration$"] = function(simc, buffType, name) return format("SpellData(%s_%s duration)", name, buffType) end,
+		["^stack$"] = function(simc, buffType, name) return format("%sStacks(%s_%s)", BuffType(buffType), name, buffType) end,
+		-- "react" is supposed to be a stack count, but it's used in almost every script as "up".
+		["^react$"] = function(simc, buffType, name) return format("%sPresent(%s_%s)", BuffType(buffType), name, buffType) end,
+		["^remains$"] = function(simc, buffType, name) return format("%sRemains(%s_%s)", BuffType(buffType), name, buffType) end,
+		["^up$"] = function(simc, buffType, name) return format("%sPresent(%s_%s)", BuffType(buffType), name, buffType) end,
+		["^value$"] = function(simc, buffType, name) return format("%sAmount(%s_%s)", BuffType(buffType), name, buffType) end,
+	}
+
+	-- cooldown.<spell_name>.<cooldown_property>
+	local COOLDOWN_PROPERTY_PATTERN = "^cooldown%.([%w_]+)%.([%w_]+)$"
+	local COOLDOWN_PROPERTY = {
+		-- TODO: ItemCooldown?
+		["^remains$"] = function(simc, spellName) return format("SpellCooldown(%s)", spellName) end,
+	}
+
+	-- dot.<dot_name>.<dot_property>
+	local DOT_PROPERTY_PATTERN = "^dot%.([%w_]+)%.([%w_]+)$"
+	local DOT_HELPFUL = {
+		["sacred_shield"] = true,
+	}
+	local function DotBuffName(dotName) return DOT_HELPFUL[dotName] and "Buff" or "Debuff" end
+	local function DotBuffSuffix(dotName) return DOT_HELPFUL[dotName] and "buff" or "debuff" end
+	local DOT_PROPERTY = {
+		["^attack_power$"] = function(simc, dotName) return format("%sAttackPower(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		["^crit_pct$"] = function(simc, dotName) return format("%sSpellCritChance(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		["^duration$"] = function(simc, dotName) return format("%sDuration(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		["^multiplier$"] = function(simc, dotName) return format("%sDamageMultiplier(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		["^remains$"] = function(simc, dotName) return format("%sRemains(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		["^spell_power$"] = function(simc, dotName) return format("%sSpellpower(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		-- TODO: Should really implement BuffDamage() that's akin to Damage/LastEstimatedDamage.
+		["^tick_dmg$"] = function(simc, dotName) return format("LastEstimatedDamage(%s_%s)", dotName, DotBuffSuffix(dotName)) end,
+		["^ticking$"] = function(simc, dotName) return format("%sPresent(%s_%s)", DotBuffName(dotName), dotName, DotBuffSuffix(dotName)) end,
+		["^ticks$"] = function(simc, dotName) return format("Ticks(%s_%s)", dotName, DotBuffSuffix(dotName)) end,
+		["^ticks_remain$"] = function(simc, dotName) return format("TicksRemain(%s_%s)", dotName, DotBuffSuffix(dotName)) end,
+	}
+
+	-- talent.<talent_name>.<talent_property>
+	local TALENT_PROPERTY_PATTERN = "^talent%.([%w_]+)%.([%w_]+)$"
+	local TALENT_PROPERTY = {
+		["^enabled$"] = function(simc, talentName) return format("TalentPoints(%s_talent)", talentName) end,
+		["^disabled$"] = function(simc, talentName) return format("not TalentPoints(%s_talent)", talentName) end,
+	}
+
+	-- glyph.<glyph_name>.<glyph_property>
+	local GLYPH_PROPERTY_PATTERN = "^glyph%.([%w_]+)%.([%w_]+)$"
+	local GLYPH_PROPERTY = {
+		["^enabled$"] = function(simc, glyphName) return format("Glyph(glyph_of_%s)", glyphName) end,
+		["^disabled$"] = function(simc, glyphName) return format("not Glyph(glyph_of_%s)", glyphName) end,
+	}
+
+	-- trinket.<proc_type>.<stat>.<trinket_property>
+	local TRINKET_PROPERTY_PATTERN = "^trinket%.([%w_]+)%.([%w_]+)%.([%w_]+)$"
+	local TRINKET_PROPERTY = {
+		["^cooldown_remains"] = function(simc, procType, statName) return ("{ ItemCooldown(Trinket0Slot) + ItemCooldown(Trinket1Slot) }") end,
+		["^down$"] = function(simc, procType, statName) return format("BuffPresent(trinket_%s_%s_buff)", procType, statName) end,
+		["^react$"] = function(simc, procType, statName)
+				if strfind(procType, "stacking") then
+					return format("BuffStacks(trinket_%s_%s_buff)", procType, statName)
+				end
+				return format("BuffPresent(trinket_%s_%s_buff)", procType, statName)
+			end,
+		["^remains$"] = function(simc, procType, statName) return format("BuffRemains(trinket_%s_%s_buff)", procType, statName) end,
+		["^stack$"] = function(simc, procType, statName) return format("BuffStacks(trinket_%s_%s_buff)", procType, statName) end,
+		["^up$"] = function(simc, procType, statName) return format("BuffPresent(trinket_%s_%s_buff)", procType, statName) end,
+	}
+
+	local GENERAL_PROPERTY = {
+		["^active_enemies$"] = "Enemies()",
+		["^adds$"] = "Enemies()",
+		["^ptr$"] = "PTR()",
+		["^time$"] = "TimeInCombat()",
+		["^time_to_bloodlust$"] = "TimeToBloodlust()",
+	}
+
+	local TRANSLATED_TOKEN = {
+		["^!$"] = "not",
+		["^!=$"] = "!=",
+		["^!~$"] = "!~",
+		["^%%$"] = "/",
+		["^%($"] = "{",
+		["^%)$"] = "}",
+		["^%*$"] = "*",
+		["^%+$"] = "+",
+		["^&$"] = "and",
+		["^-$"] = "-",
+		["^<$"] = "<",
+		["^<=$"] = "<=",
+		["^=$"] = "==",
+		["^>$"] = ">",
+		["^>=$"] = ">=",
+		["^|$"] = "or",
+		["^~$"] = "~",
+	}
+
+	function OvaleSimulationCraft:ParseExpression(action, expr, addActionSymbol)
+		local translatedList = {}
+		local tokenIterator = OvaleLexer.scan(expr, MATCHES)
+		local tokenType, token = tokenIterator()
+		while tokenType do
+			local translated
+			if tokenType == "iden" then
+				-- Strip off "target." if present.
+				local isTargetFound = strmatch(token, "^target%.([%w_%.]+)")
+				if isTargetFound then
+					token = isTargetFound
+				end
+				if not translated then
+					-- Handle properties that have a special interpretation.
+					for pattern, result in pairs(SPECIAL_PROPERTY) do
+						if strmatch(token, pattern) then
+							translated = (type(result) == "function") and result(self, action) or result
+							break
+						end
+					end
+				end
+				if not translated then
+					-- Bare action properties.
+					if addActionSymbol then
+						tinsert(self.symbols, action)
+					end
+					for pattern, result in pairs(ACTION_PROPERTY) do
+						if strmatch(token, pattern) then
+							translated = (type(result) == "function") and result(self, action) or result
+							break
+						end
+					end
+				end
+				if not translated then
+					-- Action properties for actions other than the one for this action line.
+					local name, property = strmatch(token, ACTION_PROPERTY_PATTERN)
+					if name then
+						name = self:Name(name)
+						tinsert(self.symbols, name)
+						for pattern, result in pairs(ACTION_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					for pattern, result in pairs(CHARACTER_PROPERTY) do
+						if strmatch(token, pattern) then
+							translated = (type(result) == "function") and result(self, pattern, token) or result
+							break
+						end
+					end
+				end
+				if not translated then
+					local name, property = strmatch(token, AURA_PROPERTY_PATTERN)
+					if name then
+						name = self:Name(name)
+						tinsert(self.symbols, name)
+						for pattern, result in pairs(AURA_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local buffType, name, property = strmatch(token, BUFF_PROPERTY_PATTERN)
+					if buffType then
+						if buffType == "debuff" then
+							-- Debuffs default to checking on the target.
+							isTargetFound = true
+						end
+						name = self:Name(name)
+						tinsert(self.symbols, format("%s_%s", name, buffType))
+						for pattern, result in pairs(BUFF_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, buffType, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local name, property = strmatch(token, TOTEM_PROPERTY_PATTERN)
+					if name then
+						name = self:Name(name)
+						if TOTEM_TYPE[name] then
+							tinsert(self.symbols, name)
+						end
+						for pattern, result in pairs(TOTEM_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local name, property = strmatch(token, COOLDOWN_PROPERTY_PATTERN)
+					if name then
+						name = self:Name(name)
+						tinsert(self.symbols, name)
+						for pattern, result in pairs(COOLDOWN_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local name, property = strmatch(token, TALENT_PROPERTY_PATTERN)
+					if name then
+						tinsert(self.symbols, format("%s_talent", name))
+						for pattern, result in pairs(TALENT_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local name, property = strmatch(token, GLYPH_PROPERTY_PATTERN)
+					if name then
+						tinsert(self.symbols, format("glyph_of_%s", name))
+						for pattern, result in pairs(GLYPH_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local name, property = strmatch(token, DOT_PROPERTY_PATTERN)
+					if name then
+						-- DoTs default to checking on the target.
+						isTargetFound = true
+						name = self:Name(name)
+						tinsert(self.symbols, format("%s_%s", name, DotBuffSuffix(name)))
+						for pattern, result in pairs(DOT_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					local procType, name, property = strmatch(token, TRINKET_PROPERTY_PATTERN)
+					if procType then
+						tinsert(self.symbols, format("trinket_%s_%s_buff", procType, name))
+						for pattern, result in pairs(TRINKET_PROPERTY) do
+							if strmatch(property, pattern) then
+								translated = (type(result) == "function") and result(self, procType, name) or result
+								break
+							end
+						end
+					end
+				end
+				if not translated then
+					-- set_bonus.<set_name>_<N>pc_<role>
+					local name, count, role = strmatch(token, "^set_bonus%.(%w+)_(%d+)pc_(%w+)$")
+					if name and count and role then
+						local tierLevel = strmatch(name, "^tier(%d+)")
+						if tierLevel then
+							name = format("T%s", tierLevel)
+						end
+						translated = format("ArmorSetBonus(%s_%s %s)", name, role, count)
+					end
+				end
+				if not translated then
+					for pattern, result in pairs(GENERAL_PROPERTY) do
+						if strmatch(token, pattern) then
+							translated = (type(result) == "function") and result(self, action) or result
+							break
+						end
+					end
+				end
+				if not translated then
+					translated = format("FIXME_%s", token)
+				end
+				if isTargetFound then
+					translated = "target." .. translated
+				end
+			elseif tokenType == "number" then
+				translated = token
+			end
+			if not translated then
+				for pattern, result in pairs(TRANSLATED_TOKEN) do
+					if strmatch(token, pattern) then
+						translated = (type(result) == "function") and result(self, token) or result
+						break
+					end
+				end
+			end
+			if not translated then
+				translated = format("FIXME_%s", token)
+			end
+			tinsert(translatedList, translated)
+			tokenType, token = tokenIterator()
+		end
+		local translation = tconcat(translatedList, " ")
+		return translation
+	end
+end
+--</public-static-methods>
diff --git a/compiler.pl b/compiler.pl
index 39059c5..24727c9 100644
--- a/compiler.pl
+++ b/compiler.pl
@@ -102,10 +102,12 @@ $m{Skada}{get_player} = true;
 $p{Skada}{current} = true;
 $p{Skada}{total} = true;

+$sp{Ovale}{OvaleLexer} = true;
 $sp{Ovale}{OvalePool} = true;
 $sp{Ovale}{OvalePoolGC} = true;
 $sp{Ovale}{OvalePoolRefCount} = true;
 $sp{Ovale}{OvaleQueue} = true;
+$sp{Ovale}{OvaleSimulationCraft} = true;
 $sp{Ovale}{OvaleSkada} = true;
 $sp{Ovale}{OvaleTimeSpan} = true;