Compare commits

...

7 Commits

Author SHA1 Message Date
c24f323ff0 v1.5.1 2024-07-26 22:30:48 -04:00
fec2c28693 Only calculate lookahead tokens when needed - #28
Lookahead tokens are only need if either:
(1) There is more than one rule that could be reduced in a given parser
state, or
(2) There are shift actions for a state and at least one rule that could
be reduced in the same state (to warn about shift/reduce conflicts).
2024-07-26 22:08:25 -04:00
61339aeae9 Avoid recalculating reduce_rules - #28 2024-07-26 21:36:41 -04:00
95b3dc6550 Cache ItemSet#next_symbols - #28 2024-07-25 20:33:15 -04:00
74d94fef72 Do not build ItemSet follow sets - #28 2024-07-25 20:02:00 -04:00
588c5e21c7 Cache ItemSet#leading_item_sets return values - #28 2024-07-25 10:42:43 -04:00
5f1c306273 Update CLI usage in README 2024-07-22 21:35:32 -04:00
5 changed files with 60 additions and 85 deletions

View File

@ -1,3 +1,9 @@
## v1.5.1
### Improvements
- Improve performance (#28)
## v1.5.0 ## v1.5.0
### New Features ### New Features

View File

@ -31,9 +31,14 @@ Propane is typically invoked from the command-line as `./propane`.
Usage: ./propane [options] <input-file> <output-file> Usage: ./propane [options] <input-file> <output-file>
Options: Options:
--log LOG Write log file -h, --help Show this usage and exit.
--version Show program version and exit --log LOG Write log file. This will show all parser states and their
-h, --help Show this usage and exit associated shifts and reduces. It can be helpful when
debugging a grammar.
--version Show program version and exit.
-w Treat warnings as errors. This option will treat shift/reduce
conflicts as fatal errors and will print them to stderr in
addition to the log file.
The user must specify the path to a Propane input grammar file and a path to an The user must specify the path to a Propane input grammar file and a path to an
output file. output file.

View File

@ -39,7 +39,6 @@ class Propane
end end
build_reduce_actions! build_reduce_actions!
build_follow_sets!
build_tables! build_tables!
write_log! write_log!
if @warnings.size > 0 && @options[:warnings_as_errors] if @warnings.size > 0 && @options[:warnings_as_errors]
@ -66,10 +65,10 @@ class Propane
state_id: state_id, state_id: state_id,
} }
end end
if item_set.reduce_actions unless item_set.reduce_rules.empty?
shift_entries.each do |shift_entry| shift_entries.each do |shift_entry|
token = shift_entry[:symbol] token = shift_entry[:symbol]
if item_set.reduce_actions.include?(token) if get_lookahead_reduce_actions_for_item_set(item_set).include?(token)
rule = item_set.reduce_actions[token] rule = item_set.reduce_actions[token]
@warnings << "Shift/Reduce conflict (state #{item_set.id}) between token #{token.name} and rule #{rule.name} (defined on line #{rule.line_number})" @warnings << "Shift/Reduce conflict (state #{item_set.id}) between token #{token.name} and rule #{rule.name} (defined on line #{rule.line_number})"
end end
@ -115,7 +114,7 @@ class Propane
# @return [void] # @return [void]
def build_reduce_actions! def build_reduce_actions!
@item_sets.each do |item_set| @item_sets.each do |item_set|
item_set.reduce_actions = build_reduce_actions_for_item_set(item_set) build_reduce_actions_for_item_set(item_set)
end end
end end
@ -124,27 +123,36 @@ class Propane
# @param item_set [ItemSet] # @param item_set [ItemSet]
# ItemSet (parser state) # ItemSet (parser state)
# #
# @return [nil, Hash] # @return [void]
# If no reduce actions are possible for the given item set, nil.
# Otherwise, a mapping of lookahead Tokens to the Rules to reduce.
def build_reduce_actions_for_item_set(item_set) def build_reduce_actions_for_item_set(item_set)
# To build the reduce actions, we start by looking at any # To build the reduce actions, we start by looking at any
# "complete" items, i.e., items where the parse position is at the # "complete" items, i.e., items where the parse position is at the
# end of a rule. These are the only rules that are candidates for # end of a rule. These are the only rules that are candidates for
# reduction in the current ItemSet. # reduction in the current ItemSet.
reduce_rules = Set.new(item_set.items.select(&:complete?).map(&:rule)) item_set.reduce_rules = Set.new(item_set.items.select(&:complete?).map(&:rule))
if reduce_rules.size == 1 if item_set.reduce_rules.size == 1
item_set.reduce_rule = reduce_rules.first item_set.reduce_rule = item_set.reduce_rules.first
end end
if reduce_rules.size == 0 if item_set.reduce_rules.size > 1
nil # Force item_set.reduce_actions to be built to store the lookahead
else # tokens for the possible reduce rules if there is more than one.
build_lookahead_reduce_actions_for_item_set(item_set) get_lookahead_reduce_actions_for_item_set(item_set)
end end
end end
# Get the reduce actions for a single item set (parser state).
#
# @param item_set [ItemSet]
# ItemSet (parser state)
#
# @return [Hash]
# Mapping of lookahead Tokens to the Rules to reduce.
def get_lookahead_reduce_actions_for_item_set(item_set)
item_set.reduce_actions ||= build_lookahead_reduce_actions_for_item_set(item_set)
end
# Build the reduce actions for a single item set (parser state). # Build the reduce actions for a single item set (parser state).
# #
# @param item_set [ItemSet] # @param item_set [ItemSet]
@ -153,15 +161,13 @@ class Propane
# @return [Hash] # @return [Hash]
# Mapping of lookahead Tokens to the Rules to reduce. # Mapping of lookahead Tokens to the Rules to reduce.
def build_lookahead_reduce_actions_for_item_set(item_set) def build_lookahead_reduce_actions_for_item_set(item_set)
reduce_rules = Set.new(item_set.items.select(&:complete?).map(&:rule))
# We will be looking for all possible tokens that can follow instances of # We will be looking for all possible tokens that can follow instances of
# these rules. Rather than looking through the entire grammar for the # these rules. Rather than looking through the entire grammar for the
# possible following tokens, we will only look in the item sets leading # possible following tokens, we will only look in the item sets leading
# up to this one. This restriction gives us a more precise lookahead set, # up to this one. This restriction gives us a more precise lookahead set,
# and allows us to parse LALR grammars. # and allows us to parse LALR grammars.
item_sets = Set[item_set] + item_set.leading_item_sets item_sets = Set[item_set] + item_set.leading_item_sets
reduce_rules.reduce({}) do |reduce_actions, reduce_rule| item_set.reduce_rules.reduce({}) do |reduce_actions, reduce_rule|
lookahead_tokens_for_rule = build_lookahead_tokens_to_reduce(reduce_rule, item_sets) lookahead_tokens_for_rule = build_lookahead_tokens_to_reduce(reduce_rule, item_sets)
lookahead_tokens_for_rule.each do |lookahead_token| lookahead_tokens_for_rule.each do |lookahead_token|
if existing_reduce_rule = reduce_actions[lookahead_token] if existing_reduce_rule = reduce_actions[lookahead_token]
@ -233,51 +239,6 @@ class Propane
lookahead_tokens lookahead_tokens
end end
# Build the follow sets for each ItemSet.
#
# @return [void]
def build_follow_sets!
@item_sets.each do |item_set|
item_set.follow_set = build_follow_set_for_item_set(item_set)
end
end
# Build the follow set for the given ItemSet.
#
# @param item_set [ItemSet]
# The ItemSet to build the follow set for.
#
# @return [Set]
# Follow set for the given ItemSet.
def build_follow_set_for_item_set(item_set)
follow_set = Set.new
rule_sets_to_check_after = Set.new
item_set.items.each do |item|
(1..).each do |offset|
case symbol = item.next_symbol(offset)
when nil
rule_sets_to_check_after << item.rule.rule_set
break
when Token
follow_set << symbol
break
when RuleSet
follow_set += symbol.start_token_set
unless symbol.could_be_empty?
break
end
end
end
end
reduce_lookaheads = build_lookahead_reduce_actions_for_item_set(item_set)
reduce_lookaheads.each do |token, rule_set|
if rule_sets_to_check_after.include?(rule_set)
follow_set << token
end
end
follow_set
end
def write_log! def write_log!
@log.puts Util.banner("Parser Rules") @log.puts Util.banner("Parser Rules")
@grammar.rules.each do |rule| @grammar.rules.each do |rule|

View File

@ -2,7 +2,7 @@ class Propane
class Parser class Parser
# Represent a parser "item set", which is a set of possible items that the # Represent a parser "item set", which is a set of possible items that the
# parser could currently be parsing. # parser could currently be parsing. This is equivalent to a parser state.
class ItemSet class ItemSet
# @return [Set<Item>] # @return [Set<Item>]
@ -25,15 +25,15 @@ class Propane
# Rule to reduce if there is only one possibility. # Rule to reduce if there is only one possibility.
attr_accessor :reduce_rule attr_accessor :reduce_rule
# @return [Set<Rule>]
# Set of rules that could be reduced in this parser state.
attr_accessor :reduce_rules
# @return [nil, Hash] # @return [nil, Hash]
# Reduce actions, mapping lookahead tokens to rules, if there is # Reduce actions, mapping lookahead tokens to rules, if there is
# more than one rule that could be reduced. # more than one rule that could be reduced.
attr_accessor :reduce_actions attr_accessor :reduce_actions
# @return [Set<Token>]
# Follow set for the ItemSet.
attr_accessor :follow_set
# Build an ItemSet. # Build an ItemSet.
# #
# @param items [Array<Item>] # @param items [Array<Item>]
@ -50,7 +50,7 @@ class Propane
# @return [Set<Token, RuleSet>] # @return [Set<Token, RuleSet>]
# Set of next symbols for all Items in this ItemSet. # Set of next symbols for all Items in this ItemSet.
def next_symbols def next_symbols
Set.new(@items.map(&:next_symbol).compact) @_next_symbols ||= Set.new(@items.map(&:next_symbol).compact)
end end
# Build a next ItemSet for the given next symbol. # Build a next ItemSet for the given next symbol.
@ -99,21 +99,24 @@ class Propane
# @return [Set<ItemSet>] # @return [Set<ItemSet>]
# Set of all ItemSets that lead up to this ItemSet. # Set of all ItemSets that lead up to this ItemSet.
def leading_item_sets def leading_item_sets
result = Set.new @_leading_item_sets ||=
eval_sets = Set[self] begin
evaled = Set.new result = Set.new
while eval_sets.size > 0 eval_sets = Set[self]
eval_set = eval_sets.first evaled = Set.new
eval_sets.delete(eval_set) while eval_sets.size > 0
evaled << eval_set eval_set = eval_sets.first
eval_set.in_sets.each do |in_set| eval_sets.delete(eval_set)
result << in_set evaled << eval_set
unless evaled.include?(in_set) eval_set.in_sets.each do |in_set|
eval_sets << in_set result << in_set
unless evaled.include?(in_set)
eval_sets << in_set
end
end
end end
result
end end
end
result
end end
# Represent the ItemSet as a String. # Represent the ItemSet as a String.

View File

@ -1,3 +1,3 @@
class Propane class Propane
VERSION = "1.5.0" VERSION = "1.5.1"
end end