Permalink
Browse files

Function/method calls with arguments inside () or without parentheses…

… have different needs for the priority of the call operator; we need to push a variation with a different priority onto the operator stack if we detect a method call without an opening parenthesis (caveat: Ruby has rules regarding whitespace here too, that I believe we're currently *not* taking into account)
  • Loading branch information...
vidarh committed Sep 11, 2014
1 parent 1d54cec commit dbfe0204dae6db4a5186fec7597fac5b99bd62d7
Showing with 53 additions and 33 deletions.
  1. +28 −24 features/shunting.feature
  2. +2 −1 operators.rb
  3. +22 −7 shunting.rb
  4. +1 −1 treeoutput.rb
View
@@ -43,28 +43,28 @@ Feature: Shunting Yard
Then the parse tree should become <tree>
Examples:
| expr | tree |
| "foo(1)" | [:call,:foo,[1]] |
| "foo(1,2)" | [:call,:foo,[1,2]] |
| "foo 1" | [:call,:foo,[1]] |
| "foo 1,2" | [:call,:foo,[1,2]] |
| "self.foo" | [:callm,:self,:foo] |
| "self.foo(1)" | [:callm,:self,:foo,1] |
| "self.foo(1,2)" | [:callm,:self,:foo,[1,2]] |
| "self.foo bar" | [:callm,:self,:foo,:bar] |
| "foo(*arg)" | [:call,:foo,[[:splat, :arg]]] |
| "foo(*arg,bar)" | [:call,:foo,[[:splat, :arg],:bar]] |
| "foo.bar(*arg)" | [:callm, :foo, :bar, [[:splat, :arg]]] |
| "foo.bar(!arg)" | [:callm, :foo, :bar, [[:"!", :arg]]] |
| "foo(1 + arg)" | [:call,:foo,[[:+, 1, :arg]]] |
| "foo(1 * arg,bar)" | [:call,:foo,[[:*, 1, :arg],:bar]] |
| "(ret.flatten).uniq" | [:callm,[:callm,:ret,:flatten],:uniq] |
| "ret.flatten.uniq" | [:callm,[:callm,:ret,:flatten],:uniq] |
| "foo.bar = 123" | [:callm,:foo,:bar=, [123]] |
| "flatten(r[2])" | [:call, :flatten, [[:callm, :r, :[], [2]]]] |
| expr | tree |
| "foo(1)" | [:call,:foo,[1]] |
| "foo(1,2)" | [:call,:foo,[1,2]] |
| "foo 1" | [:call,:foo,[1]] |
| "foo 1,2" | [:call,:foo,[1,2]] |
| "self.foo" | [:callm,:self,:foo] |
| "self.foo(1)" | [:callm,:self,:foo,1] |
| "self.foo(1,2)" | [:callm,:self,:foo,[1,2]] |
| "self.foo bar" | [:callm,:self,:foo,:bar] |
| "foo(*arg)" | [:call,:foo,[[:splat, :arg]]] |
| "foo(*arg,bar)" | [:call,:foo,[[:splat, :arg],:bar]] |
| "foo.bar(*arg)" | [:callm, :foo, :bar, [[:splat, :arg]]] |
| "foo.bar(!arg)" | [:callm, :foo, :bar, [[:"!", :arg]]] |
| "foo(1 + arg)" | [:call,:foo,[[:+, 1, :arg]]] |
| "foo(1 * arg,bar)" | [:call,:foo,[[:*, 1, :arg],:bar]] |
| "(ret.flatten).uniq" | [:callm,[:callm,:ret,:flatten],:uniq] |
| "ret.flatten.uniq" | [:callm,[:callm,:ret,:flatten],:uniq] |
| "foo.bar = 123" | [:callm,:foo,:bar=, [123]] |
| "flatten(r[2])" | [:call, :flatten, [[:callm, :r, :[], [2]]]] |
| "foo.bar(ret[123])" | [:callm, :foo, :bar, [[:callm, :ret, :[], [123]]]] |
| "Foo::bar(baz)" | [:callm, :Foo, :bar, :baz] |
| "foo.bar sym do end" | [:callm, :foo, :bar, :sym, [:block]] |
| "Foo::bar(baz)" | [:callm, :Foo, :bar, :baz] |
| "foo.bar sym do end" | [:callm, :foo, :bar, :sym, [:block]] |
Scenario Outline: Array syntax
Given the expression <expr>
@@ -99,15 +99,19 @@ Feature: Shunting Yard
| "s.foo[0]" | [:callm, [:callm,:s,:foo],:[],[0]] | |
| "foo[1] = 2" | [:callm, :foo, :[]=, [1,2]] | Tree rewrite |
@func
Scenario Outline: Function calls
Given the expression <expr>
When I parse it with the shunting yard parser
Then the parse tree should become <tree>
Examples:
| expr | tree |
| "attr_reader :args,:body" | [:call, :attr_reader, [:":args", :":body"]] |
| expr | tree | |
| "attr_reader :args,:body" | [:call, :attr_reader, [:":args", :":body"]] | |
| "puts 42 == 42" | [:call, :puts, [[:==,42,42]]] | |
| "foo.bar() + 'x'" | [:+, [:callm, :foo, :bar, nil], "x"] | |
| "foo.bar + 'x'" | [:+, [:callm, :foo, :bar], "x"] | |
Scenario Outline: Terminating expressions with keywords
Given the expression <expr>
When I parse it with the shunting yard parser
View
@@ -6,7 +6,7 @@
#
# - Priority (pri)
# - Unique Name / Identifier (sym)
# - Type (prefix, infix or suffix)
# - Type (prefix, infix, suffix, left-parenthesis (:lp) or right-parenthesis (:rp))
# - Arity (how many arguments? Most operators are either unary or binary)
# - Minarity (The minimum arity, for operators with optional arguments)
# - Association: Whether the operator binds to the left or right argument first (the default is right)
@@ -105,6 +105,7 @@ def self.expect(s)
# "Fake" operator for function calls
"#call#" => Oper.new( 99, :call, :prefix,2,1),
"#call2#" => Oper.new( 5, :call, :prefix,2,1),
"," => Oper.new( 99, :comma, :infix, 2,1),
# "Fake" operator for [] following a name
View
@@ -12,6 +12,18 @@ def initialize(output, tokenizer, parser)
# FIXME: Pass this in instead of storing it.
@tokenizer = TokenizerAdapter.new(tokenizer,parser)
@parser = parser
# Tricky hack:
#
# We need a call operator with high priority, but *if* parentheses are not used around
# the arguments, we need to push an operator with *low* priority onto the operator stack to prevent
# binding the call too tightly to the first value encountered. opcall2 below is that second, low priority
# call operator
@opcall = Operators["#call#"]
@opcall2 = Operators["#call2#"]
@opcallm = Operators["."]
end
def keywords
@@ -27,6 +39,7 @@ def reduce(ostack, op = nil)
# As a special rule only :rp's are allowed to reduce past an :lp. This is a bit of a hack - since we recurse for :lp's
# then don't strictly need to go on the stack at all - they could be pushed on after the shunt returns. May
# do that later.
while !ostack.empty? && (ostack.last.pri > pri || (ostack.last.pri == pri && op.assoc == :left) || ostack.last.type == :postfix) && ((op && op.type == :rp) || ostack.last.type != :lp)
o = ostack.pop
@out.oper(o) if o.sym
@@ -43,16 +56,17 @@ def shunt(src, ostack = [], inhibit = [])
# "opstate" is used to handle things like pre-increment and post-increment that
# share the same token.
lp_on_entry = ostack.first && ostack.first.type == :lp
opcall = Operators["#call#"]
opcallm = Operators["."]
lastlp = true
src.each do |token,op,keyword|
# Normally we stop when encountering a keyword, but it's ok to encounter
# one as the second operand for an infix operator
if inhibit.include?(token) or keyword && (opstate != :prefix || !ostack.last || ostack.last.type != :infix || token == :end)
src.unget(token)
break
end
if op
op = op[opstate] if op.is_a?(Hash)
@@ -63,11 +77,12 @@ def shunt(src, ostack = [], inhibit = [])
op = Operators["#,#"] if op == Operators[","] and lp_on_entry
if op.sym == :hash_or_block || op.sym == :block
if possible_func || ostack.last == opcall || ostack.last == opcallm
@out.value([]) if ostack.last != opcall
if possible_func || (ostack.last && ostack.last.sym == :call) || ostack.last == @opcallm
ocall = ostack.last ? ostack.last.sym == :call : false
@out.value([]) if !ocall
@out.value(parse_block(token))
@out.oper(Operators["#flatten#"])
ostack << opcall if ostack.last != opcall
ostack << @opcall if !ocall
elsif op.sym == :hash_or_block
op = Operators["#hash#"]
shunt(src, [op])
@@ -86,7 +101,7 @@ def shunt(src, ostack = [], inhibit = [])
shunt(src, [op])
opstate = :infix_or_postfix
# Handling function calls and a[1] vs [1]
ostack << (op.sym == :array ? Operators["#index#"] : opcall) if possible_func
ostack << (op.sym == :array ? Operators["#index#"] : @opcall) if possible_func
elsif op.type == :rp
break
else
@@ -97,7 +112,7 @@ def shunt(src, ostack = [], inhibit = [])
else
if possible_func
reduce(ostack)
ostack << opcall
ostack << @opcall2
end
@out.value(token)
opstate = :infix_or_postfix # After a non-operator value, any single arity operator would be either postfix,
View
@@ -76,7 +76,7 @@ def oper(o)
else
# FIXME This seemingly fixes issue where single argument function call does not get its arguments wrapped.
# FIXME Need to verify that this doesn't fail any other tests than the ones it should
if o.sym == :call || o.sym == :callm and o.type == :prefix and rightv[0] != :flatten and rightv[0] != :comma
if o.sym == :call || o.sym == :callm and o.type == :prefix and rightv && rightv[0] != :flatten and rightv[0] != :comma
rightv = E[rightv]
end
@vstack << E[o.sym, flatten(leftv), rightv].compact

0 comments on commit dbfe020

Please sign in to comment.