The Shunting-Yard Algorithm (2011)
reedbeta.com
reedbeta.com
While researching the area, I came across this exact blog post, and thought it was perfect. It explains things clearly and concisely (without even needing code/pseudocode). Granted, it's a simple algorithm, but this post was just so much nicer than any of the (higher ranked) results that Google gave me. And it has no ads!
It made me sad. Years ago, it seemed like Google helped me find lots of nice little pages like this one. It's much harder to find web articles of this quality anymore.
Back to this article specifically, I particularly appreciated the "Advanced Usage" section, where it briefly (and clearly!) goes over some useful extensions to the algorithm. These were very much on my mind (since I wanted parentheses and function calls). They are simple extensions to a simple algorithm, but it's nice to have them summarized for you before you dive in.
It was a breezy joy to implement (did it even take an hour?). And I can think of other easy ways to extend its capabilities further. This style of arithmetic parsing would be a wonderful thing for beginner programmers to cut their teeth on. It exposes them to a number of important computer/programming ideas, yet is simple to implement while also being directly useful.
* it doesn't mention that operator-precedence (usually: Pratt) parsing is another alternative
* it doesn't mention that you don't have to evaluate the expression directly; it's perfectly possible to output an AST, RPN, or some kind of minimal bytecode.
* unless you do a bit of optimization, you can't reduce the number of "registers" needed à la Schneider. While generally less relevant for interpreters unless you do some careful platform-dependent design, interpreters can easily benefit from the one-register case.
* you're allowed to use an ahead-of-time LR machine generator, you don't have to encode its tables by hand (but for simple grammars it's not actually that hard to work out with comments).
* it doesn't sufficiently mention that you really want to work with IDs in a lot of cases; among other things this avoids further unary/binary problems (though I suppose synthesizing an extra argument also works).
> Note that “applying” an operator can mean a couple of different things in this context. You could actually execute the operators, in which case the operands would be numerical values of all the terms and subexpressions; you could also build a syntax tree, in which case the operands would be subtrees. The algorithm works the same way in either case.
> you could also build a syntax tree, in which case the operands would be subtrees.
1 + if true then 1 else a b + c
as (represented as an s-expr using some weird angle brackets): ⟨+ 1 ⟨if␣then␣else␣ true 1 ⟨+ ⟨a b⟩ c⟩⟩⟩
I thought it was cool and could be useful to a lot of simple use cases.I then proceeded to extend it a little for my purposes (supporting functions iwth variable numbers of arguments) which was fun, and got a surprising amount of people showing up in the blog post comments: https://blog.kallisti.net.nz/2008/02/extension-to-the-shunti...
One of the sections was an implementation of this algorithm, and it made me fall in love with programming and the power of well-designed algorithms.
Since performance is a major concern, we compared a lot of algorithms and frameworks and Shunting Yard really hits the spot being both fast and easy to grasp, producing maintainable code.
For anyone interested in seeing code, we recently open sourced Sonic: https://github.com/adletec/sonic
https://github.com/LoganDark/cpp-calculator/blob/315c3e9814e...
It was sort of butchered to try to handle function calls as well, but it's still kind of shunting-yard. It also doesn't handle unary operators well. But it worked for its original purpose, I suppose.
[0]: https://github.com/nitely/nim-regex/blob/5e447c329ce826deb61...
edit: added permalink
Not only would writing 18 routines be a PITA, it would mean super deep and slow call stacks during expression evaluation. So I used the shunting yard algorithm.
[edit] I've been told that this is only true in Poland, where reverse Polish notation is interpreted literally.
In my case, I used it to build an AST, rather than evaluate the calculation.
Adding my two cents, this is a really great little algo that I personally have implemented for the fun of it many times as a means of comparing languages to one another, really just for my own benefit. the kotlin instantiation was my fave thus far but I am overdue to revisit this in things like mojo and zig, so big +1
I also found that it's super-easy to extend to support parentheses.
Some Ruby code to share if anyone would like to see it.
PRECEDENCES = [
['+', '-'],
['*', '/']
]
def precedence_of(operator)
PRECEDENCES.find_index do |level_ops|
level_ops.include?(operator)
end
end
def lower_precedence(token, than:)
precedence_of(token) < precedence_of(than)
end
def apply(operator, l, r)
case operator
when '+'
l + r
when '-'
l - r
when '*'
l \* r
when '/'
l / r
else
raise Exception.new("Unknown operator #{operator}")
end
end
def collapse(operators, operands, next_operator: nil)
while !operators.empty? &&
(next_operator.nil? ||
lower_precedence(next_operator, than: operators.last))
r = operands.pop()
op = operators.pop()
l = operands.pop()
operands.push(apply(op, l, r))
end
end
def expect_operator(token)
if precedence_of(token).nil?
raise Exception.new("Expected operator, got #{token.inspect}")
end
return token
end
def expect_operand(token)
if token[/^\d+$/]
return token.to_i
end
raise Exception.new(
"Expected value or nested expression beginning with '(', instead got #{token.inspect}"
)
end
def should_cons(str, char)
return !str.nil? && str[/^\d+$/] && char[/^\d+$/]
end
loop do
input = gets
tokens = []
input.chars.each do |char|
if should_cons(tokens.last, char)
tokens.push(tokens.pop() + char)
else
if char.strip.size > 0
tokens.push(char)
end
end
end
puts tokens.inspect
expect_value = true
operators = []
operands = []
parentheses_stack = []
tokens.each do |token|
if expect_value
if token == '('
puts "PUSHING #{operators.inspect} #{operands.inspect}"
parentheses_stack << [operators, operands]
operators = []
operands = []
next
else
operand = expect_operand(token)
operands << operand
end
else
if token == ')'
puts "COLLAPSING #{operators.inspect} #{operands.inspect}"
collapse(operators, operands)
result = operands.last
puts "RESULT #{result.inspect}"
operators, operands = parentheses_stack.pop()
operands.push(result)
puts "POPPING(+ pushing) #{operators.inspect} #{operands.inspect}"
next
else
operator = expect_operator(token)
puts "COLLAPSING #{operators.inspect} #{operands.inspect}"
collapse(operators, operands, next_operator: operator)
operators.push(operator)
end
end
expect_value = !expect_value
end
collapse(operators, operands)
puts operands.last
end