lib/atp/flow.rb in atp-0.8.0 vs lib/atp/flow.rb in atp-1.0.0
- old
+ new
@@ -1,68 +1,211 @@
module ATP
# Implements the main user API for building and interacting
# with an abstract test program
class Flow
attr_reader :program, :name
- # Returns the raw AST
- attr_reader :raw
- attr_accessor :id
+ attr_accessor :source_file, :source_line_number, :description
+
+ CONDITION_KEYS = {
+ if_enabled: :if_enabled,
+ if_enable: :if_enabled,
+ enabled: :if_enabled,
+ enable_flag: :if_enabled,
+ enable: :if_enabled,
+
+ unless_enabled: :unless_enabled,
+ not_enabled: :unless_enabled,
+ disabled: :unless_enabled,
+ disable: :unless_enabled,
+ unless_enable: :unless_enabled,
+
+ if_failed: :if_failed,
+ unless_passed: :if_failed,
+ failed: :if_failed,
+
+ if_passed: :if_passed,
+ unless_failed: :if_passed,
+ passed: :if_passed,
+
+ if_any_failed: :if_any_failed,
+ unless_all_passed: :if_any_failed,
+
+ if_all_failed: :if_all_failed,
+ unless_any_passed: :if_all_failed,
+
+ if_any_passed: :if_any_passed,
+ unless_all_failed: :if_any_passed,
+
+ if_all_passed: :if_all_passed,
+ unless_any_failed: :if_all_passed,
+
+ if_ran: :if_ran,
+ if_executed: :if_ran,
+
+ unless_ran: :unless_ran,
+ unless_executed: :unless_ran,
+
+ job: :if_job,
+ jobs: :if_job,
+ if_job: :if_job,
+ if_jobs: :if_job,
+
+ unless_job: :unless_job,
+ unless_jobs: :unless_job,
+
+ if_flag: :if_flag,
+
+ unless_flag: :unless_flag,
+
+ group: :group
+ }
+
+ CONDITION_NODE_TYPES = CONDITION_KEYS.values.uniq
+
def initialize(program, name = nil, options = {})
name, options = nil, name if name.is_a?(Hash)
extract_meta!(options)
@program = program
@name = name
- @raw = builder.flow(name)
+ @pipeline = [n1(:flow, n1(:name, name))]
end
# @api private
def marshal_dump
- [@name, @program, Processors::Marshal.new.process(@raw)]
+ [@name, @program, Processors::Marshal.new.process(raw)]
end
# @api private
def marshal_load(array)
- @name, @program, @raw = array
+ @name, @program, raw = array
+ @pipeline = [raw]
end
+ # Returns the raw AST
+ def raw
+ n = nil
+ @pipeline.reverse_each do |node|
+ if n
+ n = node.updated(nil, node.children + [n])
+ else
+ n = node
+ end
+ end
+ n
+ end
+
# Returns a processed/optimized AST, this is the one that should be
# used to build and represent the given test flow
- def ast
- ast = Processors::PreCleaner.new.process(raw)
- # File.open("a1.txt", "w") { |f| f.write(ast) }
- ast = Processors::FlowID.new.run(ast, id) if id
- # File.open("a2.txt", "w") { |f| f.write(ast) }
- Validators::DuplicateIDs.new(self).process(ast)
- Validators::MissingIDs.new(self).process(ast)
- ast = Processors::Condition.new.process(ast)
- ast = Processors::Relationship.new.process(ast)
- ast = Processors::PostCleaner.new.process(ast)
- Validators::Jobs.new(self).process(ast)
+ def ast(options = {})
+ options = {
+ apply_relationships: true,
+ # Supply a unique ID to append to all IDs
+ unique_id: nil,
+ # Set to :smt, or :igxl
+ optimization: :runner,
+ # These options are not intended for application use, but provide the ability to
+ # turn off certain processors during test cases
+ add_ids: true,
+ optimize_flags: true,
+ one_flag_per_test: true,
+ implement_continue: true
+ }.merge(options)
+ ###############################################################################
+ ## Common pre-processing and validation
+ ###############################################################################
+ ast = Processors::PreCleaner.new.run(raw)
+ Validators::DuplicateIDs.new(self).run(ast)
+ Validators::MissingIDs.new(self).run(ast)
+ Validators::Jobs.new(self).run(ast)
+ # Ensure everything has an ID, this helps later if condition nodes need to be generated
+ ast = Processors::AddIDs.new.run(ast) if options[:add_ids]
+ ast = Processors::FlowID.new.run(ast, options[:unique_id]) if options[:unique_id]
+
+ ###############################################################################
+ ## Optimization for a C-like flow target, e.g. V93K
+ ###############################################################################
+ if options[:optimization] == :smt || options[:optimization] == :runner
+ # This applies all the relationships by setting flags in the referenced test and
+ # changing all if_passed/failed type nodes to if_flag type nodes
+ ast = Processors::Relationship.new.run(ast) if options[:apply_relationships]
+ ast = Processors::Condition.new.run(ast)
+ unless options[:optimization] == :runner
+ ast = Processors::ContinueImplementer.new.run(ast) if options[:implement_continue]
+ end
+ ast = Processors::FlagOptimizer.new.run(ast) if options[:optimize_flags]
+ ast = Processors::AdjacentIfCombiner.new.run(ast)
+
+ ###############################################################################
+ ## Optimization for a row-based target, e.g. UltraFLEX
+ ###############################################################################
+ elsif options[:optimization] == :igxl
+ # Un-nest everything embedded in else nodes
+ ast = Processors::ElseRemover.new.run(ast)
+ # Un-nest everything embedded in on_pass/fail nodes except for binning and
+ # flag setting
+ ast = Processors::OnPassFailRemover.new.run(ast)
+ # This applies all the relationships by setting flags in the referenced test and
+ # changing all if_passed/failed type nodes to if_flag type nodes
+ ast = Processors::Relationship.new.run(ast) if options[:apply_relationships]
+ ast = Processors::Condition.new.run(ast)
+ ast = Processors::ApplyPostGroupActions.new.run(ast)
+ ast = Processors::OneFlagPerTest.new.run(ast) if options[:one_flag_per_test]
+ ast = Processors::RedundantConditionRemover.new.run(ast)
+
+ ###############################################################################
+ ## Not currently used, more of a test case
+ ###############################################################################
+ elsif options[:optimization] == :flat
+ # Un-nest everything embedded in else nodes
+ ast = Processors::ElseRemover.new.run(ast)
+ # Un-nest everything embedded in on_pass/fail nodes except for binning and
+ # flag setting
+ ast = Processors::OnPassFailRemover.new.run(ast)
+ ast = Processors::Condition.new.run(ast)
+ ast = Processors::Flattener.new.run(ast)
+
+ ###############################################################################
+ ## Default Optimization
+ ###############################################################################
+ else
+ ast = Processors::Condition.new.run(ast)
+ end
+
+ ###############################################################################
+ ## Common cleanup
+ ###############################################################################
+ # Removes any empty on_pass and on_fail branches
+ ast = Processors::EmptyBranchRemover.new.run(ast)
ast
end
# Indicate the that given flags should be considered volatile (can change at any time), which will
# prevent them from been touched by the optimization algorithms
def volatile(*flags)
options = flags.pop if flags.last.is_a?(Hash)
flags = flags.flatten
- @raw = builder.add_volatile_flags(@raw, flags)
+ @pipeline[0] = add_volatile_flags(@pipeline[0], flags)
end
# Group all tests generated within the given block
#
# @example
# flow.group "RAM Tests" do
# flow.test ...
# flow.test ...
# end
def group(name, options = {})
- open_groups.push([])
- yield
extract_meta!(options)
- append builder.group(name, open_groups.pop, options)
+ apply_conditions(options) do
+ children = [n1(:name, name)]
+ children << id(options[:id]) if options[:id]
+ children << on_fail(options[:on_fail]) if options[:on_fail]
+ children << on_pass(options[:on_pass]) if options[:on_pass]
+ g = n(:group, children)
+ append_to(g) { yield }
+ end
end
# Add a test line to the flow
#
# @param [String, Symbol] the name of the test
@@ -72,12 +215,11 @@
# @option options [Hash] :on_fail What action to take if the test fails, e.g. assign a bin
# @option options [Hash] :on_pass What action to take if the test passes
# @option options [Hash] :conditions What conditions must be met to execute the test
def test(instance, options = {})
extract_meta!(options)
- r = options.delete(:return)
- t = apply_open_conditions(options) do |options|
+ apply_conditions(options) do
# Allows any continue, bin, or soft bin argument passed in at the options top-level to be assumed
# to be the action to take if the test fails
if b = options.delete(:bin)
options[:on_fail] ||= {}
options[:on_fail][:bin] = b
@@ -98,238 +240,503 @@
options[:on_fail] ||= {}
options[:on_fail][:continue] = true
end
if f = options.delete(:flag_pass)
options[:on_pass] ||= {}
- options[:on_pass][:set_run_flag] = f
+ options[:on_pass][:set_flag] = f
end
if f = options.delete(:flag_fail)
options[:on_fail] ||= {}
- options[:on_fail][:set_run_flag] = f
+ options[:on_fail][:set_flag] = f
end
- builder.test(instance, options)
+
+ children = [n1(:object, instance)]
+
+ name = (options[:name] || options[:tname] || options[:test_name])
+ unless name
+ [:name, :tname, :test_name].each do |m|
+ name ||= instance.respond_to?(m) ? instance.send(m) : nil
+ end
+ end
+ children << n1(:name, name) if name
+
+ num = (options[:number] || options[:num] || options[:tnum] || options[:test_number])
+ unless num
+ [:number, :num, :tnum, :test_number].each do |m|
+ num ||= instance.respond_to?(m) ? instance.send(m) : nil
+ end
+ end
+ children << number(num) if num
+
+ children << id(options[:id]) if options[:id]
+
+ if levels = options[:level] || options[:levels]
+ levels = [levels] unless levels.is_a?(Array)
+ levels.each do |l|
+ children << level(l[:name], l[:value], l[:unit] || l[:units])
+ end
+ end
+
+ if lims = options[:limit] || options[:limits]
+ lims = [lims] unless lims.is_a?(Array)
+ lims.each do |l|
+ if l.is_a?(Hash)
+ children << limit(l[:value], l[:rule], l[:unit] || l[:units])
+ end
+ end
+ end
+
+ if pins = options[:pin] || options[:pins]
+ pins = [pins] unless pins.is_a?(Array)
+ pins.each do |p|
+ if p.is_a?(Hash)
+ children << pin(p[:name])
+ else
+ children << pin(p)
+ end
+ end
+ end
+
+ if pats = options[:pattern] || options[:patterns]
+ pats = [pats] unless pats.is_a?(Array)
+ pats.each do |p|
+ if p.is_a?(Hash)
+ children << pattern(p[:name], p[:path])
+ else
+ children << pattern(p)
+ end
+ end
+ end
+
+ if options[:meta]
+ attrs = []
+ options[:meta].each { |k, v| attrs << attribute(k, v) }
+ children << n(:meta, attrs)
+ end
+
+ if subs = options[:sub_test] || options[:sub_tests]
+ subs = [subs] unless subs.is_a?(Array)
+ subs.each do |s|
+ children << s.updated(:sub_test, nil)
+ end
+ end
+
+ children << on_fail(options[:on_fail]) if options[:on_fail]
+ children << on_pass(options[:on_pass]) if options[:on_pass]
+
+ save_conditions
+ n(:test, children)
end
- append(t) unless r
- t
end
# Equivalent to calling test, but returns a sub_test node instead of adding it to the flow.
- # It will also ignore any condition nodes that would normally wrap the equivalent flow.test call.
#
# This is a helper to create sub_tests for inclusion in a top-level test node.
def sub_test(instance, options = {})
- options[:return] = true
- options[:ignore_all_conditions] = true
- test(instance, options)
+ temp = append_to(n0(:temp)) { test(instance, options) }
+ temp.children.first.updated(:sub_test, nil)
end
def bin(number, options = {})
+ if number.is_a?(Hash)
+ fail 'The bin number must be passed as the first argument'
+ end
+ options[:bin_description] ||= options.delete(:description)
extract_meta!(options)
- t = apply_open_conditions(options) do |options|
- fail 'A :type option set to :pass or :fail is required when calling bin' unless options[:type]
+ apply_conditions(options) do
+ options[:type] ||= :fail
options[:bin] = number
options[:softbin] ||= options[:soft_bin] || options[:sbin]
- builder.set_result(options[:type], options)
+ set_result(options[:type], options)
end
- append(t)
end
+ def pass(number, options = {})
+ if number.is_a?(Hash)
+ fail 'The bin number must be passed as the first argument'
+ end
+ options[:type] = :pass
+ bin(number, options)
+ end
+
def cz(instance, cz_setup, options = {})
extract_meta!(options)
- t = apply_open_conditions(options) do |options|
- conditions = options.delete(:conditions)
- options[:return] = true
- builder.cz(cz_setup, test(instance, options.merge(dont_apply_conditions: true)), conditions: conditions)
+ apply_conditions(options) do
+ node = n1(:cz, cz_setup)
+ append_to(node) { test(instance, options) }
end
- append(t)
end
alias_method :characterize, :cz
# Append a log message line to the flow
def log(message, options = {})
extract_meta!(options)
- t = apply_open_conditions(options) do |options|
- builder.log(message, options)
+ apply_conditions(options) do
+ n1(:log, message.to_s)
end
- append(t)
end
# Enable a flow control variable
def enable(var, options = {})
extract_meta!(options)
- t = apply_open_conditions(options) do |options|
- builder.enable_flow_flag(var, options)
+ apply_conditions(options) do
+ n1(:enable, var)
end
- append(t)
end
# Disable a flow control variable
def disable(var, options = {})
extract_meta!(options)
- t = apply_open_conditions(options) do |options|
- builder.disable_flow_flag(var, options)
+ apply_conditions(options) do
+ n1(:disable, var)
end
- append(t)
end
+ def set_flag(flag, options = {})
+ extract_meta!(options)
+ apply_conditions(options) do
+ set_flag_node(flag)
+ end
+ end
+
# Insert explicitly rendered content in to the flow
def render(str, options = {})
extract_meta!(options)
- t = apply_open_conditions(options) do |options|
- builder.render(str, options)
+ apply_conditions(options) do
+ n1(:render, str)
end
- append(t)
end
- def with_condition(options)
+ def continue(options = {})
extract_meta!(options)
- open_conditions.push(options)
- yield
- open_conditions.pop
+ apply_conditions(options) do
+ n0(:continue)
+ end
end
- alias_method :with_conditions, :with_condition
# Execute the given flow in the console
def run(options = {})
Formatters::Datalog.run_and_format(ast, options)
nil
end
# Returns true if the test context generated from the supplied options + existing condition
# wrappers, is different from that which was applied to the previous test.
def context_changed?(options)
- a = context
- b = build_context(options)
- !context_equal?(a, b)
+ options[:_dont_delete_conditions_] = true
+ last_conditions != clean_conditions(open_conditions + [extract_conditions(options)])
end
- def context
- builder.context
+ # Define handlers for all of the flow control block methods, unless a custom one has already
+ # been defined above
+ CONDITION_KEYS.keys.each do |method|
+ define_method method do |*flags, &block|
+ if flags.last.is_a?(Hash)
+ options = flags.pop
+ else
+ options = {}
+ end
+ flags = flags.first if flags.size == 1
+ # Legacy option provided by OrigenTesters that permits override of a block enable method by passing
+ # an :or option with a true value
+ if (CONDITION_KEYS[method] == :if_enabled || CONDITION_KEYS[method] || :unless_enabled) && options[:or]
+ block.call
+ else
+ flow_control_method(CONDITION_KEYS[method], flags, options, &block)
+ end
+ end unless method_defined?(method)
end
- def context_equal?(a, b)
- if a.size == b.size
- a = clean_condition(a[:conditions])
- b = clean_condition(b[:conditions])
- if a.keys.sort == b.keys.sort
- a.all? do |key, value|
- value.flatten.uniq.sort == b[key].flatten.uniq.sort
- end
- end
- end
+ def inspect
+ "<ATP::Flow:#{object_id} #{name}>"
end
private
- def clean_condition(h)
- c = {}
- h.each do |hash|
- key, value = hash.first[0], hash.first[1]
- key = clean_key(key)
- value = clean_value(value)
- c[key] ||= []
- c[key] << value unless c[key].include?(value)
+ def flow_control_method(name, flag, options = {}, &block)
+ extract_meta!(options)
+ if flag.is_a?(Array)
+ if name == :if_passed
+ fail 'if_passed only accepts one ID, use if_any_passed or if_all_passed for multiple IDs'
+ end
+ if name == :if_failed
+ fail 'if_failed only accepts one ID, use if_any_failed or if_all_failed for multiple IDs'
+ end
end
- c
+ apply_conditions(options) do
+ if block
+ node = n1(name, flag)
+ open_conditions << [name, flag]
+ node = append_to(node) { block.call }
+ open_conditions.pop
+ else
+ unless options[:then] || options[:else]
+ fail "You must supply a :then or :else option when calling #{name} like this!"
+ end
+ node = n1(name, flag)
+ if options[:then]
+ node = append_to(node) { options[:then].call }
+ end
+ if options[:else]
+ e = n0(:else)
+ e = append_to(e) { options[:else].call }
+ node = node.updated(nil, node.children + [e])
+ end
+ end
+ node
+ end
end
- def clean_value(value)
- if value.is_a?(Array)
- value.map { |v| v.to_s.downcase }.sort
+ def apply_conditions(options, node = nil)
+ # Applying the current context, means to append to the same node as the last time, this
+ # means that the next node will pick up the exact same condition context as the previous one
+ if options[:context] == :current
+ node = yield
+ found = false
+ @pipeline = @pipeline.map do |parent|
+ p = Processors::AppendTo.new
+ n = p.run(parent, node, @last_append.id)
+ found ||= p.succeeded?
+ n
+ end
+ unless found
+ fail 'The request to apply the current context has failed, this is likely a bug in the ATP plugin'
+ end
+ node
else
- value.to_s.downcase
+ conditions = extract_conditions(options)
+ open_conditions << conditions
+ node = yield
+ open_conditions.pop
+
+ update_last_append = !condition_node?(node)
+
+ conditions.each do |key, value|
+ if key == :group
+ node = n2(key, n1(:name, value.to_s), node)
+ else
+ node = n2(key, value, node)
+ end
+ if update_last_append
+ @last_append = node
+ update_last_append = false
+ end
+ end
+
+ append(node)
+ node
end
end
- def clean_key(key)
- case key.to_sym
- when :if_enabled, :enabled, :enable_flag, :enable, :if_enable
- :if_enable
- when :unless_enabled, :not_enabled, :disabled, :disable, :unless_enable
- :unless_enable
- when :if_failed, :unless_passed, :failed
- :if_failed
- when :if_passed, :unless_failed, :passed
- :if_passed
- when :if_any_failed, :unless_all_passed
- :if_any_failed
- when :if_all_failed, :unless_any_passed
- :if_all_failed
- when :if_any_passed, :unless_all_failed
- :if_any_passed
- when :if_all_passed, :unless_any_failed
- :if_all_passed
- when :if_ran, :if_executed
- :if_ran
- when :unless_ran, :unless_executed
- :unless_ran
- when :job, :jobs, :if_job, :if_jobs
- :if_job
- when :unless_job, :unless_jobs
- :unless_job
- else
- fail "Unknown test condition attribute - #{key}"
+ def save_conditions
+ @last_conditions = clean_conditions(open_conditions.dup)
+ end
+
+ def last_conditions
+ @last_conditions || {}
+ end
+
+ def open_conditions
+ @open_conditions ||= []
+ end
+
+ def clean_conditions(conditions)
+ result = {}.with_indifferent_access
+ conditions.each do |cond|
+ if cond.is_a?(Array)
+ if cond.size != 2
+ fail 'Something has gone wrong in ATP!'
+ else
+ result[cond[0]] = cond[1].to_s if cond[1]
+ end
+ else
+ cond.each { |k, v| result[k] = v.to_s if v }
+ end
end
+ result
end
- def build_context(options)
- c = open_conditions.dup
- if options[:conditions]
- options[:conditions].each do |key, value|
- c << { key => value }
+ def extract_conditions(options)
+ conditions = {}
+ delete_from_options = !options.delete(:_dont_delete_conditions_)
+ options.each do |key, value|
+ if CONDITION_KEYS[key]
+ options.delete(key) if delete_from_options
+ key = CONDITION_KEYS[key]
+ if conditions[key] && value
+ fail "Multiple values assigned to flow condition #{key}" unless conditions[key] == value
+ else
+ conditions[key] = value if value
+ end
end
end
- { conditions: c }
+ conditions
end
- def builder
- @builder ||= AST::Builder.new
+ def append(node)
+ @last_append = @pipeline.last unless condition_node?(node)
+ n = @pipeline.pop
+ @pipeline << n.updated(nil, n.children + [node])
+ @pipeline.last
end
- def apply_open_conditions(options)
- if options[:ignore_all_conditions]
- yield(options)
+ # Append all nodes generated within the given block to the given node
+ # instead of the top-level flow node
+ def append_to(node)
+ @pipeline << node
+ yield
+ @pipeline.pop
+ end
+
+ def condition_node?(node)
+ !!CONDITION_KEYS[node.type]
+ end
+
+ def extract_meta!(options)
+ self.source_file = options.delete(:source_file)
+ self.source_line_number = options.delete(:source_line_number)
+ self.description = options.delete(:description)
+ end
+
+ def id(name)
+ n1(:id, name)
+ end
+
+ def on_fail(options = {})
+ if options.is_a?(Proc)
+ node = n0(:on_fail)
+ append_to(node) { options.call }
else
- if options[:context] == :current
- options[:conditions] = builder.context[:conditions]
+ children = []
+ if options[:bin] || options[:softbin]
+ fail_opts = { bin: options[:bin], softbin: options[:softbin] }
+ fail_opts[:bin_description] = options[:bin_description] if options[:bin_description]
+ fail_opts[:softbin_description] = options[:softbin_description] if options[:softbin_description]
+ children << set_result(:fail, fail_opts)
end
- builder.new_context
- t = yield(options)
- unless options[:context] == :current
- unless options[:dont_apply_conditions]
- open_conditions.each do |conditions|
- t = builder.apply_conditions(t, conditions)
- end
- end
+ if options[:set_run_flag] || options[:set_flag]
+ children << set_flag_node(options[:set_run_flag] || options[:set_flag])
end
- t
+ children << n0(:continue) if options[:continue]
+ children << n1(:render, options[:render]) if options[:render]
+ n(:on_fail, children)
end
end
- def extract_meta!(options)
- builder.source_file = options.delete(:source_file)
- builder.source_line_number = options.delete(:source_line_number)
- builder.description = options.delete(:description)
+ def on_pass(options = {})
+ if options.is_a?(Proc)
+ node = n0(:on_pass)
+ append_to(node) { options.call }
+ else
+ children = []
+ if options[:bin] || options[:softbin]
+ pass_opts = { bin: options[:bin], softbin: options[:softbin] }
+ pass_opts[:bin_description] = options[:bin_description] if options[:bin_description]
+ pass_opts[:softbin_description] = options[:softbin_description] if options[:softbin_description]
+ children << set_result(:pass, pass_opts)
+ end
+ if options[:set_run_flag] || options[:set_flag]
+ children << set_flag_node(options[:set_run_flag] || options[:set_flag])
+ end
+ children << n0(:continue) if options[:continue]
+ children << n1(:render, options[:render]) if options[:render]
+ n(:on_pass, children)
+ end
end
- # For testing
- def raw=(ast)
- @raw = ast
+ def pattern(name, path = nil)
+ if path
+ n2(:pattern, name, path)
+ else
+ n1(:pattern, name)
+ end
end
- def open_conditions
- @open_conditions ||= []
+ def attribute(name, value)
+ n2(:attribute, name, value)
end
- def open_groups
- @open_groups ||= []
+ def level(name, value, units = nil)
+ if units
+ n(:level, [name, value, units])
+ else
+ n2(:level, name, value)
+ end
end
- def append(node)
- if open_groups.empty?
- @raw = @raw.updated(nil, @raw.children + [node])
+ def limit(value, rule, units = nil)
+ if units
+ n(:limit, [value, rule, units])
else
- open_groups.last << node
+ n2(:limit, value, rule)
end
+ end
+
+ def pin(name)
+ n1(:pin, name)
+ end
+
+ def set_result(type, options = {})
+ children = []
+ children << type
+ if options[:bin] && options[:bin_description]
+ children << n2(:bin, options[:bin], options[:bin_description])
+ else
+ children << n1(:bin, options[:bin]) if options[:bin]
+ end
+ if options[:softbin] && options[:softbin_description]
+ children << n2(:softbin, options[:softbin], options[:softbin_description])
+ else
+ children << n1(:softbin, options[:softbin]) if options[:softbin]
+ end
+ n(:set_result, children)
+ end
+
+ def number(val)
+ n1(:number, val.to_i)
+ end
+
+ def set_flag_node(flag)
+ n1(:set_flag, flag)
+ end
+
+ # Ensures the flow ast has a volatile node, then adds the
+ # given flags to it
+ def add_volatile_flags(node, flags)
+ name, *nodes = *node
+ if nodes[0] && nodes[0].type == :volatile
+ v = nodes.shift
+ else
+ v = n0(:volatile)
+ end
+ existing = v.children.map { |f| f.type == :flag ? f.value : nil }.compact
+ new = []
+ flags.each do |flag|
+ new << n1(:flag, flag) unless existing.include?(flag)
+ end
+ v = v.updated(nil, v.children + new)
+ node.updated(nil, [name, v] + nodes)
+ end
+
+ def n(type, children, options = {})
+ options[:file] ||= options.delete(:source_file) || source_file
+ options[:line_number] ||= options.delete(:source_line_number) || source_line_number
+ options[:description] ||= options.delete(:description) || description
+ # Guarantee that each node has a unique meta-ID, in case we need to ever search
+ # for it
+ options[:id] = ATP.next_id
+ ATP::AST::Node.new(type, children, options)
+ end
+
+ def n0(type, options = {})
+ n(type, [], options)
+ end
+
+ def n1(type, arg, options = {})
+ n(type, [arg], options)
+ end
+
+ def n2(type, arg1, arg2, options = {})
+ n(type, [arg1, arg2], options)
end
end
end