Skip to content
Open
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
14 changes: 14 additions & 0 deletions lib/typeprof/core/ast/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ def modified_vars(tbl, vars)
end
end

# Where a diagnostic about the value this node yields is placed: a node
# with a body points at the body's last statement, the rest at itself.
def ret_code_range = code_range

def install_multi_targets(genv, multi_targets, positionals, lenv)
multi_targets.each do |idx, raw_multi_target|
param_vtx = positionals[idx]
lefts = raw_multi_target.lefts.map do |n|
lenv.new_var(n.is_a?(Prism::MultiTargetNode) ? nil : n.name, self)
end
@changes.add_masgn_box(genv, param_vtx, lefts, nil, nil)
end
end

def pretty_print_instance_variables
super() - [:@raw_node, :@lenv, :@prev_node, :@static_ret, :@changes]
end
Expand Down
284 changes: 144 additions & 140 deletions lib/typeprof/core/ast/call.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,139 @@
module TypeProf::Core
class AST
# @lenv is the scope the block closes over; the body has its own LocalEnv.
class BlockNode < Node
def initialize(raw_node, lenv, mid)
super(raw_node, lenv)

@tbl = raw_node.locals
ncref = CRef.new(lenv.cref.cpath, :instance, mid, lenv.cref)
# A `return` in a block exits the enclosing method, so the body writes into
# its return boxes. A lambda's `return` exits the lambda, so it gets its own.
nlenv = LocalEnv.new(lenv.file_context, ncref, {}, lambda? ? [] : lenv.return_boxes)

# parse_params with no parameters returns the canonical empty set, so the
# readers below never have to ask whether there were any.
@params = AST.parse_params(@tbl, nil, nlenv)
@f_args = []
@multi_targets = {}
@opt_positional_defaults = []
case raw_node.parameters
when Prism::BlockParametersNode
# `{ || ... }` (empty pipes) and `{ |; x| ... }` (block-local-only)
# yield BlockParametersNode whose inner `parameters` is nil.
@params = AST.parse_params(@tbl, raw_node.parameters.parameters, nlenv)
@f_args = @params[:req_positionals] + @params[:opt_positionals]
@multi_targets = @params[:req_multi_targets]
@opt_positional_defaults = @params[:opt_positional_defaults]
when Prism::NumberedParametersNode
@f_args = 1.upto(raw_node.parameters.maximum).map {|n| :"_#{n}" }
when Prism::ItParametersNode
@f_args = [:it]
when nil
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)
end

attr_reader :tbl, :f_args, :opt_positional_defaults, :body

# FormalArguments carries vertices; the keyword names stay on the node, which
# is where FormalArguments#pass_arguments looks them up.
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 subnodes = { opt_positional_defaults:, body: }
# 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: }

def formal_names
@params.values_at(
:req_positionals, :opt_positionals, :rest_positionals, :post_positionals,
:req_keywords, :opt_keywords, :rest_keywords, :block,
)
end

def install0(genv)
blenv = @body.lenv
blenv.forward_args = @lenv.forward_args
@lenv.locals.each {|var, vtx| blenv.locals[var] = vtx }
@tbl.each {|var| blenv.locals[var] = Source.new(genv.nil_type) }
blenv.locals[:"*self"] = blenv.cref.get_self(genv)

f_args = @f_args.map {|arg| blenv.new_var(arg, self) }

req_count = f_args.size - @opt_positional_defaults.size
@opt_positional_defaults.each_with_index do |expr, i|
@changes.add_edge(genv, expr.install(genv), f_args[req_count + i])
end

install_multi_targets(genv, @multi_targets, f_args, blenv)
formals = build_formals(genv, blenv, f_args)

@lenv.locals.each do |var, vtx|
blenv.set_var(var, vtx)
end
vars = []
@body.modified_vars(@lenv.locals.keys - @tbl, vars)
vars.uniq!
vars.each do |var|
vtx = @lenv.get_var(var)
nvtx = vtx.new_vertex(genv, self)
@lenv.set_var(var, nvtx)
blenv.set_var(var, nvtx)
end

blenv.locals[:"*expected_block_ret"] = Vertex.new(self)
# Present already when the lambda sits in a method; a top-level one still
# needs it, or ReturnNode drops the returned value on the floor.
blenv.locals[:"*expected_method_ret"] ||= Vertex.new(self) if lambda?
@body.install(genv)
blenv.add_next_box(@changes.add_escape_box(genv, @body.ret))

if lambda?
# `return` and `break` leave the lambda itself, so they reach the caller
# of #call the same way the body's own value does.
blenv.return_boxes.each {|box| blenv.add_next_box(box) }
blenv.add_next_box(@changes.add_escape_box(genv, blenv.break_vtx)) if blenv.break_vtx
end

vars.each do |var|
@changes.add_edge(genv, blenv.get_var(var), @lenv.get_var(var))
end

f_ary_arg = Vertex.new(self)
# TODO: support splat "do |a, *b, c|"
f_args.each_with_index do |f_arg, i|
elem_vtx = @changes.add_splat_box(genv, f_ary_arg, i).ret
@changes.add_edge(genv, elem_vtx, f_arg)
end
block = Block.new(self, f_ary_arg, f_args, blenv.next_boxes, formals)
Source.new(Type::Proc.new(genv, block))
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

# Block-local variables shadow the outer ones, so writes to them are not
# modifications of the enclosing scope.
def modified_vars(tbl, vars)
super(tbl - @tbl, vars)
end

# A block's `break` leaves the method that yielded, so the call it belongs to
# takes the value; a lambda's `break` leaves the lambda and is wired above.
def lambda? = false

def break_vtx = lambda? ? nil : @body.lenv.break_vtx

def ret_code_range = @body.ret_code_range
end

class CallBaseNode < Node
def initialize(raw_node, recv, mid, mid_code_range_loc, raw_args, last_arg, raw_block, lenv, forwarding_arguments: false)
super(raw_node, lenv)
Expand All @@ -14,10 +148,7 @@ def initialize(raw_node, recv, mid, mid_code_range_loc, raw_args, last_arg, raw_
@keyword_args = nil

@block_pass = nil
@block_tbl = nil
@block_f_args = nil
@block_opt_positional_defaults = nil
@block_body = nil
@block = nil
@safe_navigation = raw_node.respond_to?(:safe_navigation?) && raw_node.safe_navigation?
@anonymous_block_forwarding = false
@forwarding_arguments = forwarding_arguments
Expand Down Expand Up @@ -55,47 +186,7 @@ def initialize(raw_node, recv, mid, mid_code_range_loc, raw_args, last_arg, raw_
@anonymous_block_forwarding = true
end
else
@block_pass = nil
@block_tbl = raw_block.locals
@block_multi_targets = {}
@block_f_args = case raw_block.parameters
when Prism::BlockParametersNode
# `{ || ... }` (empty pipes) and `{ |; x| ... }`
# (block-local-only) yield BlockParametersNode
# whose inner `parameters` is nil.
params = raw_block.parameters.parameters
if params
req = params.requireds.each_with_index.map do |n, i|
if n.is_a?(Prism::MultiTargetNode)
@block_multi_targets[i] = n
nil
else
n.name
end
end
opt = params.optionals.map {|n| n.name }
req + opt
else
[]
end
when Prism::NumberedParametersNode
1.upto(raw_block.parameters.maximum).map { |n| :"_#{n}" }
when Prism::ItParametersNode
[:it]
when nil
[]
else
raise "not supported yet: #{ raw_block.parameters.class }"
end
ncref = CRef.new(lenv.cref.cpath, :instance, @mid, lenv.cref)
nlenv = LocalEnv.new(@lenv.file_context, ncref, {}, @lenv.return_boxes)
@block_opt_positional_defaults = []
if raw_block.parameters.is_a?(Prism::BlockParametersNode) && raw_block.parameters.parameters
raw_block.parameters.parameters.optionals.each do |n|
@block_opt_positional_defaults << AST.create_node(n.value, nlenv)
end
end
@block_body = raw_block.body ? AST.create_node(raw_block.body, nlenv) : DummyNilNode.new(code_range, lenv)
@block = BlockNode.new(raw_block, lenv, @mid)
end
end

Expand All @@ -108,12 +199,11 @@ def mid_code_range
@mid_code_range ||= @lenv.code_range_from_node(@mid_code_range_loc) if @mid_code_range_loc
end
attr_reader :positional_args, :splat_flags, :keyword_args
attr_reader :block_tbl, :block_f_args, :block_opt_positional_defaults, :block_body, :block_pass, :anonymous_block_forwarding
attr_reader :block_multi_targets
attr_reader :block, :block_pass, :anonymous_block_forwarding
attr_reader :safe_navigation, :forwarding_arguments

def subnodes = { recv:, positional_args:, keyword_args:, block_opt_positional_defaults:, block_body:, block_pass: }
def attrs = { mid:, splat_flags:, block_tbl:, block_f_args:, yield:, safe_navigation:, anonymous_block_forwarding:, forwarding_arguments: }
def subnodes = { recv:, positional_args:, keyword_args:, block:, block_pass: }
def attrs = { mid:, splat_flags:, yield:, safe_navigation:, anonymous_block_forwarding:, forwarding_arguments: }

def install0(genv)
recv = @recv ? @recv.install(genv) : @yield ? @lenv.get_var(:"*given_block") : @lenv.get_var(:"*self")
Expand Down Expand Up @@ -146,67 +236,8 @@ def install0(genv)
a_args = ActualArguments.new(positional_args, @splat_flags, @keyword_args ? @keyword_args.install(genv) : nil, nil)
end

if @block_body
block_body = @block_body # kinda type annotationty
block_tbl = @block_tbl || raise
block_body.lenv.forward_args = @lenv.forward_args
@lenv.locals.each {|var, vtx| block_body.lenv.locals[var] = vtx }
block_tbl.each {|var| block_body.lenv.locals[var] = Source.new(genv.nil_type) }
block_body.lenv.locals[:"*self"] = block_body.lenv.cref.get_self(genv)

blk_f_args = []
if @block_f_args
@block_f_args.each do |arg|
blk_f_args << block_body.lenv.new_var(arg, self)
end
end

if @block_opt_positional_defaults && !@block_opt_positional_defaults.empty?
req_count = blk_f_args.size - @block_opt_positional_defaults.size
@block_opt_positional_defaults.each_with_index do |expr, i|
@changes.add_edge(genv, expr.install(genv), blk_f_args[req_count + i])
end
end

if @block_multi_targets
@block_multi_targets.each do |idx, raw_multi_target|
param_vtx = blk_f_args[idx]
lefts = raw_multi_target.lefts.map do |n|
block_body.lenv.new_var(n.is_a?(Prism::MultiTargetNode) ? nil : n.name, self)
end
@changes.add_masgn_box(genv, param_vtx, lefts, nil, nil)
end
end

@lenv.locals.each do |var, vtx|
block_body.lenv.set_var(var, vtx)
end
vars = []
block_body.modified_vars(@lenv.locals.keys - block_tbl, vars)
vars.uniq!
vars.each do |var|
vtx = @lenv.get_var(var)
nvtx = vtx.new_vertex(genv, self)
@lenv.set_var(var, nvtx)
block_body.lenv.set_var(var, nvtx)
end

block_body.lenv.locals[:"*expected_block_ret"] = Vertex.new(self)
block_body.install(genv)
block_body.lenv.add_next_box(@changes.add_escape_box(genv, block_body.ret))

vars.each do |var|
@changes.add_edge(genv, block_body.lenv.get_var(var), @lenv.get_var(var))
end

blk_f_ary_arg = Vertex.new(self)
# TODO: support splat "do |a, *b, c|"
blk_f_args.each_with_index do |f_arg, i|
elem_vtx = @changes.add_splat_box(genv, blk_f_ary_arg, i).ret
@changes.add_edge(genv, elem_vtx, f_arg)
end
block = Block.new(self, blk_f_ary_arg, blk_f_args, block_body.lenv.next_boxes)
blk_ty = Source.new(Type::Proc.new(genv, block))
if @block
blk_ty = @block.install(genv)
elsif @block_pass
blk_ty = @block_pass.install(genv)
elsif @anonymous_block_forwarding
Expand All @@ -216,17 +247,16 @@ def install0(genv)
end

if @forwarding_arguments
a_args = a_args.with_block(blk_ty, omittable: !@block_body && !@block_pass && !@anonymous_block_forwarding)
a_args = a_args.with_block(blk_ty, omittable: !@block && !@block_pass && !@anonymous_block_forwarding)
else
a_args = a_args.with_block(blk_ty)
end
box = @changes.add_method_call_box(genv, recv, @mid, a_args, !@recv)

block_body = @block_body
if block_body && block_body.lenv.break_vtx
if @block && @block.break_vtx
ret = Vertex.new(self)
@changes.add_edge(genv, box.ret, ret)
@changes.add_edge(genv, block_body.lenv.break_vtx, ret)
@changes.add_edge(genv, @block.break_vtx, ret)
else
ret = box.ret
end
Expand All @@ -238,18 +268,6 @@ def install0(genv)
ret
end

def block_last_stmt_code_range
if @block_body
if @block_body.is_a?(AST::StatementsNode)
@block_body.stmts.last.code_range
else
@block_body.code_range
end
else
nil
end
end

def retrieve_at(pos, &blk)
yield self if mid_code_range&.include?(pos)
each_subnode do |subnode|
Expand All @@ -258,20 +276,6 @@ def retrieve_at(pos, &blk)
end
end

def modified_vars(tbl, vars)
subnodes.each do |key, subnode|
next unless subnode
if subnode.is_a?(AST::Node)
if key == :block_body
subnode.modified_vars(tbl - self.block_tbl, vars)
else
subnode.modified_vars(tbl, vars)
end
else
subnode.each {|n| n&.modified_vars(tbl, vars) }
end
end
end
end

class CallNode < CallBaseNode
Expand Down
Loading
Loading