A LALR(1) parser refers to a Look-Ahead LR parser(with 1 token of lookahead).
Specifically a LALR(1) parser adds lookahead to a LR(0) parser keeping the same set of underlying states. This grants a substantial portion of the additional recognition power of LR(1).
In addition to the parser states being unchanged, the function from the LR(0) parser is also used as is, but with now referring to the lookahead token rather than the shift token.
What we need to compute is thus the values of the function , which are the reductions to perform in a state given that the lookahead token is (where is a terminal).
We compute through a series of steps which are described by the following equations, such that each step depends on the one below it.
This is going to be a lot all at once so it will likely be more useful as something to come back and refer to rather than an overview of what is to come.
To compute we now need to determine values for the , and relations. Like these can be deduced from the LR(0) parser.
We define such that if and only if all of the following are true
This is used in the function above to define the full list of terminals which can be the next terminal read after a given non-terminal transition.
As for what this looks like we can have a look at the graph generated for the following grammar:
S -> E
E -> F C D E
E -> "e"
F ->
C ->
D ->We define such that if and only if all of the following are true.
The relation captures what edges can follow the specified edge after taking into account backtracking from performing a reduction.
The following is an example grammar and parser diagram to illustrate what looks like in practice.
S -> E
E -> "f" F
E -> "e"
F -> "g" G
G -> "h" EThe relation is a bit different from the others because it does not relate transitions to transitions.
Unfortunately that means it doesn't have such a nice graphical representation to help illustrate it, but computing it will be very similar to computing .
We define such that if and only if all of following are true.
To compute and from their respective relations we need to make use of a graph algorithm referred to as Digraph.
The way these two functions are defined, each strongly connected component of the relation graph converges to the same value for each vertex in the component, then the other components have dependencies on that value as if the whole component was a single vertex.
The Digraph algorithm is a modified SCC traversal algorithm which computes a specific form of graph function as it does so. Namely one in the following form:
Where is what we are trying to compute, is some function we know how to compute without the use of , and is a relation.
# &f means that the argument is passed using the ruby block syntax
# i.e. {}
# For now this can be mostly ignored as we use it as if it was
# just another hashmap from edges to some value.
#
# To facilitate this ruby allows you to pass in a hash as a block
# argument using the & operator, and you invoke it using [] rather
# than ().
def digraph(relation, edges, &f)
stack = []
path = []
state = {}
result = {}
edges.each do |starting_point|
# The outer loop starts an iteration with the next unvisited
# edge after the previous starting point has traversed all
# reachable nodes.
unless state.key?(starting_point)
stack.push(starting_point)
state[starting_point] = 0
end
until stack.empty?
edge = stack.pop()
if state[edge] == 0
# Each edge is queued for processing exactly twice. Once
# when traversing downwards in this branch, doing the
# initial setup, then again in the other branch when
# ascending back up to propagate the computed values
# across the component.
#
# Thus the weird step of queueing the same element again
# immediately after popping it.
stack.push(edge)
path.push(edge)
state[edge] = path.length
result[edge] = f[edge]
(relation[edge]).each do |other|
unless state.key?(other)
state[other] = 0
stack.push(other)
end
end
else
(relation[edge]).each do |other|
unless state[other] == 0
state[edge] = [state[edge], state[other]].min
result[edge] = result[edge] + result[other]
end
end
# Given how state[edge] is assigned to the minimum of the
# the current node and its neighbours above, this check
# should only be true for the first node visited in each
# SCC.
if path[state[edge] - 1] == edge
while true
popped = path.pop()
# The state gets set to infinity here so that the edge
# is never considered for being a part of any other
# component later. Otherwise the trick with setting
# the minimum value from neighbours wouldn't work
# properly.
state[popped] = Float::INFINITY
if popped == edge
break
else
result[popped] = result[edge]
end
end
end
end
end
end
result
end
The steps of the algorithm are as follows:
- Compute nullable non-terminals from the grammar
- Compute for each transition from the
LR(0)parser - Compute the from the
LR(0)parser - Apply Digraph to and to compute
- Compute and from the
LR(0)parser - Apply Digraph to and to compute
- Union all of the follow sets for each reduction as defined by to compute
I will include explanations of some of the less obvious elements of the above however, rather than post the whole implementation with little discussion, I will direct you to the full example source in lalr.rb for all of the remaining details.
The process of computing the nullable non-terminals is obvious, but non-trivial. Here is an example of an algorithm to do so making use of something very similar to the digraph algorithm to recursively traverse the tree.
There are certainly some clever tricks that can be used to cut this down.
def nullable_tokens
nullable = Set.new
# The set of non_terminals on the right hand side of each
# production. As the algorithm proceeds we will prune the
# nullable tokens as we find them until it the set is empty at
# which point we know the production is nullable.
production_dependencies = Hash.new { |h, k| h[k] = Set.new }
# This is the inverse mapping for the initial value of
# production_dependencies
has_token = Hash.new { |h, k| h[k] = Set.new }
@production_tokens.each do |production, left|
tokens = production_items(production).map do |item|
@items[item]
end.to_set
if tokens.empty?
nullable.add(left)
else
if (tokens & terminals).empty?
production_dependencies[production] = tokens
tokens.each do |token|
has_token[token].add(production)
end
end
end
end
queue = nullable.to_a
# For each nullable token there must exist a parse tree with
# that token at the root such that every node in the tree is
# also a nullable token.
#
# The implication of that is that we can enumerate all such
# tokens by iteratively computing all of the tokens that can
# be generated with a tree of each depth. That is accomplished
# here by processing them in FIFO order as we find them.
until queue.empty?
token = queue.shift()
(has_token[token]).each do |production|
left_token = @production_tokens[production]
unless nullable.include?(left_token)
production_dependencies[production].delete(token)
if production_dependencies[production].empty?
nullable.add(left_token)
queue.push(left_token)
end
end
end
end
nullable
end
# The lookback and includes relations can be computed efficiently
# at the same time because they both involve the same traversal of
# paths defined by the tokens in some production.
def compute_path_relations
@lookback_relation = Hash.with_default(Set)
@includes_relation = Hash.with_default(Set)
non_terminal_transitions.each do |edge, _|
(productions_for[edge.label]).each do |production|
tokens = tokens_for(production)
state = edge.from
included = Set.new
tokens.each do |token|
next_edge = Edge[state, token]
state = transitions[next_edge]
if state.nil?
# if we don't find a transition for a given token along
# the path described by the production, then state will
# be nil here and that value will be available outside
# the loop as a flag indicating whether the path was
# found.
break
end
# If we find a path from the initial state, then the
# transitions which include the start state are those for
# the nullable non-terminals at the end of the path, and
# at most one non-nullable non-terminal that immediately
# proceeds those.
if non_terminals.include?(token)
unless nullable_tokens.include?(token)
included.clear
end
included.add(next_edge)
else
included.clear
end
end
unless state.nil?
@lookback_relation[Edge[state, production]].add(edge)
included.each do |other_edge|
@includes_relation[other_edge].add(edge)
end
end
end
end
# Here I have marked these sets as immutable because otherwise
# the Hash.with_default will insert new empty sets when they are
# used later.
@includes_relation.freeze
@lookback_relation.freeze
end
Once we've completed generating the relations, computing the and functions is incredibly straightforward. Literally just filling in the arguments as described e.g.
def read
digraph(reads_relation, transitions.keys, &direct_reads)
end
Then the final step of computing is just merging everything together. In the code here this is combined with the step of generating the value of .
def reduce
reduce = Hash.with_default { Hash.with_default(Set) }
lookahead.each do |edge, tokens|
# This is syntax to destructure the `edge` variable and assign
# its two fields to the variables `state` and `production`.
edge => Edge[state, production]
if reduce0[state].include?(production)
tokens.each do |token|
reduce[state][token].add(production)
end
end
end
reduce
end
Like with a LR(0) parser, a grammar is only considered to be in LALR(1) if the parser generated by the described algorithm is unambiguous.
Thus the output needs to be checked to ensure that each edge only corresponds to a single action, either shift or reduce.
Some additional checks can be performed during the steps which make use of the digraph algorithm to identify grammars which are not LR at all. See [1] for more details.
- F. DeRemer and T. Pennello, "Efficient Computation of LALR(1) Look-Ahead sets," ACM Transactions on Programming Languages and Systems, vol. 4, no. 4, pp. 615-649, Oct. 1982, doi: https://doi.org/10.1145/69622.357187.