propane/lib/propane/rule_set.rb

170 lines
5.1 KiB
Ruby

class Propane
# A RuleSet collects all grammar rules of the same name.
class RuleSet
# @return [Array<Hash>]
# tree fields.
attr_reader :tree_fields
# @return [Integer]
# ID of the RuleSet.
attr_reader :id
# @return [String]
# Name of the RuleSet.
attr_reader :name
# @return [String, nil]
# Parser type name.
attr_accessor :ptypename
# @return [Array<Rule>]
# Rules in the RuleSet.
attr_reader :rules
# @return [Boolean]
# Whether the RuleSet could expand to an empty sequence.
attr_writer :could_be_empty
# Construct a RuleSet.
#
# @param name [String]
# Name of the RuleSet.
# @param id [Integer]
# ID of the RuleSet.
def initialize(name, id)
@id = id
@name = name
@rules = []
@could_be_empty = false
end
# Add a Rule to the RuleSet.
#
# @param rule [Rule]
# Rule to add.
def <<(rule)
@rules << rule
end
# Return whether any Rule in the RuleSet is empty.
#
# @return [Boolean]
# Whether any rule in the RuleSet is empty.
def could_be_empty?
@could_be_empty
end
# Return whether this is an optional RuleSet.
#
# @return [Boolean]
# Whether this is an optional RuleSet.
def optional?
@name.end_with?("?")
end
# For optional rule sets, return the underlying component that is optional.
def option_target
@rules.each do |rule|
if rule.components.size > 0
return rule.components[0]
end
end
raise "Optional rule target not found"
end
# Build the start token set for the RuleSet.
#
# @return [Set<Token>]
# Start token set for the RuleSet.
def start_token_set
if @_start_token_set.nil?
@_start_token_set = Set.new
@rules.each do |rule|
rule.components.each do |component|
if component.is_a?(Token)
@_start_token_set << component
break
else
@_start_token_set += component.start_token_set
unless component.could_be_empty?
break
end
end
end
end
end
@_start_token_set
end
# Finalize a RuleSet after adding all Rules to it.
def finalize(grammar)
if grammar.tree
build_tree_fields(grammar)
end
end
private
# Build the set of tree fields for this RuleSet.
#
# This is an Array of Hashes. Each entry in the Array corresponds to a
# field location in the tree node. The entry is a Hash. It could have one or
# two keys. It will always have the field name with a positional suffix as
# a key. It may also have the field name without the positional suffix if
# that field only exists in one position across all Rules in the RuleSet.
#
# @return [void]
def build_tree_fields(grammar)
field_tree_node_indexes = {}
field_indexes_across_all_rules = {}
# Stores the index into @tree_fields by field alias name.
field_aliases = {}
@tree_fields = []
@rules.each do |rule|
rule.components.each_with_index do |component, i|
if component.is_a?(RuleSet) && component.optional?
component = component.option_target
end
if component.is_a?(Token)
node_name = "Token"
else
node_name = component.name
end
struct_name = "#{grammar.tree_prefix}#{node_name}#{grammar.tree_suffix}"
field_name = "p#{node_name}#{i + 1}"
unless field_tree_node_indexes[field_name]
field_tree_node_indexes[field_name] = @tree_fields.size
@tree_fields << {field_name => struct_name}
end
rule.aliases.each do |alias_name, index|
if index == i
alias_tree_fields_index = field_tree_node_indexes[field_name]
if field_aliases[alias_name] && field_aliases[alias_name] != alias_tree_fields_index
raise Error.new("Error: conflicting tree node field positions for alias `#{alias_name}` in rule #{rule.name} defined on line #{rule.line_number}")
end
field_aliases[alias_name] = alias_tree_fields_index
@tree_fields[alias_tree_fields_index][alias_name] = @tree_fields[alias_tree_fields_index].first[1]
end
end
field_indexes_across_all_rules[node_name] ||= Set.new
field_indexes_across_all_rules[node_name] << field_tree_node_indexes[field_name]
rule.rule_set_node_field_index_map[i] = field_tree_node_indexes[field_name]
end
end
field_indexes_across_all_rules.each do |node_name, indexes_across_all_rules|
if indexes_across_all_rules.size == 1
# If this field was only seen in one position across all rules,
# then add an alias to the positional field name that does not
# include the position.
@tree_fields[indexes_across_all_rules.first]["p#{node_name}"] =
"#{grammar.tree_prefix}#{node_name}#{grammar.tree_suffix}"
end
end
end
end
end