Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions lib/typeprof/core/ast/call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def initialize(raw_node, lenv, mid)
else
raise "not supported yet: #{ raw_node.parameters.class }"
end
@body = raw_node.body ? AST.create_node(raw_node.body, nlenv) : DummyNilNode.new(code_range, lenv)
@body = raw_node.body ? AST.create_node(raw_node.body, nlenv) : DummyNilNode.new(code_range, nlenv)
end

attr_reader :tbl, :f_args, :opt_positional_defaults, :body
Expand All @@ -44,8 +44,9 @@ def req_keywords = @params[:req_keywords]
def opt_keywords = @params[:opt_keywords]
def rest_keywords = @params[:rest_keywords]
def opt_keyword_defaults = @params[:opt_keyword_defaults]
def no_keywords = @params[:no_keywords]

def subnodes = { opt_positional_defaults:, body: }
def subnodes = { opt_positional_defaults:, body:, opt_keyword_defaults: }
# f_args covers only the parameters a block binds, so the rest have to be
# compared too or an edit that only touches them looks like no edit at all.
def attrs = { tbl:, f_args:, formal_names: }
Expand Down Expand Up @@ -116,8 +117,12 @@ def install0(genv)
end

# A block is yielded to, and what a yielding method passes is the positional
# list alone; there are no formals to bind beyond it.
def build_formals(genv, blenv, f_args) = nil
# list alone; there are no formals to bind beyond it. Keyword defaults are
# still code in the block, so they are analyzed without being bound.
def build_formals(genv, blenv, f_args)
opt_keyword_defaults.each {|expr| expr.install(genv) }
nil
end

# Block-local variables shadow the outer ones, so writes to them are not
# modifications of the enclosing scope.
Expand Down
2 changes: 0 additions & 2 deletions lib/typeprof/core/ast/value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,6 @@ def initialize(raw_node, lenv)

def lambda? = true

def subnodes = { opt_positional_defaults:, body:, opt_keyword_defaults: }

# A lambda is entered like a method, so every parameter kind binds, not just
# the positionals a block is handed.
def build_formals(genv, blenv, f_args)
Expand Down
11 changes: 11 additions & 0 deletions lib/typeprof/core/env/method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@ def with_keywords_as_last_positional_hash
)
end

# Keywords passed to formals that take none are the last positional hash.
def with_keywords_normalized_for(node)
return self unless keywords
return self if node.no_keywords || node.rest_keywords
return self unless node.req_keywords.empty? && node.opt_keywords.empty?

with_keywords_as_last_positional_hash
end

def prepend_positionals(positionals, splat_flags)
return self if positionals.empty?

Expand Down Expand Up @@ -570,7 +579,9 @@ def initialize(node, f_ary_arg, f_args, next_boxes, formals = nil)
# The arguments of a call that enters this body directly, as Proc#call does.
def pass_arguments(genv, changes, a_args)
if @formals
a_args = a_args.with_keywords_normalized_for(@node)
@formals.pass_arguments(changes, genv, a_args, @node)
changes.add_edge(genv, a_args.block, @formals.block) if @formals.block && a_args.block
else
accept_args(genv, changes, a_args.positionals)
end
Expand Down
12 changes: 2 additions & 10 deletions lib/typeprof/core/graph/box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ def resolve_overload(changes, genv, method_type, node, param_map, a_args, ret, f
a_args.block.each_type do |ty|
case ty
when Type::Proc
ty.block.accept_args(genv, changes, blk_a_args)
ty.block.pass_arguments(genv, changes, ActualArguments.new(blk_a_args, ::Array.new(blk_a_args.size, false), nil, nil))

if ty.block.is_a?(Block)
ty.block.next_boxes.each do |next_box|
Expand Down Expand Up @@ -827,17 +827,9 @@ def run0(genv, changes)
def pass_arguments(changes, genv, a_args)
@f_args.pass_arguments(changes, genv, a_args, @node)
end
def normalize_keyword_hash_argument_for_def(a_args)
return a_args unless a_args.keywords
return a_args if @node.no_keywords
return a_args if @node.rest_keywords
return a_args unless @node.req_keywords.empty? && @node.opt_keywords.empty?

a_args.with_keywords_as_last_positional_hash
end

def call(changes, genv, a_args, ret)
a_args = normalize_keyword_hash_argument_for_def(a_args)
a_args = a_args.with_keywords_normalized_for(@node)
if pass_arguments(changes, genv, a_args)
if @node.is_a?(AST::DefNode)
@node.body.lenv.forward_args&.accept_actual_arguments(genv, changes, a_args)
Expand Down
12 changes: 12 additions & 0 deletions scenario/block/empty_body.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## update
# The parameter of an empty block is its own, not the enclosing x
def outer_local
x = 1
["str"].each {|x| }
x
end

## assert
class Object
def outer_local: -> Integer
end
29 changes: 29 additions & 0 deletions scenario/block/keyword_default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## update
def helper(a, b) = a

# The default value of a block keyword is analyzed as a lambda's is
def foo = [1].each { |x: helper(1)| x }
foo

## diagnostics
(4,25)-(4,31): wrong number of arguments (1 for 2)

## assert
class Object
def helper: (untyped, untyped) -> untyped
def foo: -> Array[Integer]
end

## update
def helper(a, b) = a

def foo = [1].each { |x: helper(1, 2)| x }
foo

## diagnostics

## assert
class Object
def helper: (Integer, Integer) -> Integer
def foo: -> Array[Integer]
end
20 changes: 20 additions & 0 deletions scenario/lambda/block_arg.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## update
# A lambda passed as a block binds like a method, as it does for #call
def rest = [1, 2].map(&->(*a) { a })
rest

def post = [1, 2].map(&->(*a, b) { b })
post

def too_many = [1, 2].map(&->(x, y) { y })
too_many

## diagnostics
(8,22)-(8,25): wrong number of arguments (1 for 2)

## assert
class Object
def rest: -> Array[Array[Integer]]
def post: -> Array[Integer]
def too_many: -> Array[untyped]
end
8 changes: 8 additions & 0 deletions scenario/lambda/call_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,25 @@ def post = ->(x, *y, z) { z }.call(1, 2, :sym)
def keywords = ->(k: 1) { k }.call(k: "str")
keywords

def keyword_from_keyword = ->(k:, j: k) { j }.call(k: 1)
keyword_from_keyword

def rest_keywords = ->(**kw) { kw }.call(a: 1)
rest_keywords

def block_param = ->(&b) { b }.call

def block_given = ->(&b) { b }.call { 1 }
block_given

## assert
class Object
def rest: -> Array[Integer | String]
def lead_and_rest: -> Array[Integer]
def post: -> :sym
def keywords: -> (Integer | String)
def keyword_from_keyword: -> Integer
def rest_keywords: -> { a: Integer }
def block_param: -> untyped
def block_given: -> Proc
end
36 changes: 36 additions & 0 deletions scenario/lambda/empty_body.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## update
# The parameter of an empty lambda is its own, not the enclosing x
def outer_local
x = 1
f = ->(x) { }
f.call("str")
x
end

def check(v) = v

# An empty lambda returns nil, not what the enclosing method returns
def enclosing_return(c)
return :sym if c
f = -> { }
check(f.call)
nil
end

# An empty lambda returns nil, not what the enclosing block breaks with
def enclosing_break
[1].each do
g = -> { }
check(g.call)
break "str"
end
nil
end

## assert
class Object
def outer_local: -> Integer
def check: (nil) -> nil
def enclosing_return: (untyped) -> :sym?
def enclosing_break: -> nil
end
15 changes: 15 additions & 0 deletions scenario/lambda/keyword_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## update
# With no keyword parameters, keywords are passed as a trailing hash
def one_hash = ->(h) { h }.call(k: 1)
one_hash

def trailing_hash = ->(a, b) { b }.call(1, k: 2)
trailing_hash

## diagnostics

## assert
class Object
def one_hash: -> { k: Integer }
def trailing_hash: -> { k: Integer }
end
19 changes: 19 additions & 0 deletions scenario/rbs/super_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## update: test.rbs
class C
def foo: () { (Integer) -> Integer } -> :ok
end

## update: test.rb
class D < C
def foo
super() { |x| "str" }
end
end

## assert
class D < C
def foo: -> :ok
end

## diagnostics
(3,18)-(3,23): expected: Integer; actual: String
Loading