217 lines
5.9 KiB
Crystal
217 lines
5.9 KiB
Crystal
module Ameba::AST
|
|
# Represents the existence of the local variable.
|
|
# Holds the var node and variable assignments.
|
|
class Variable
|
|
# List of the assignments of this variable.
|
|
getter assignments = [] of Assignment
|
|
|
|
# List of the references of this variable.
|
|
getter references = [] of Reference
|
|
|
|
# The actual var node.
|
|
getter node : Crystal::Var
|
|
|
|
# Scope of this variable.
|
|
getter scope : Scope
|
|
|
|
# Node of the first assignment which can be available before any reference.
|
|
getter assign_before_reference : Crystal::ASTNode?
|
|
|
|
delegate location, to: @node
|
|
delegate end_location, to: @node
|
|
delegate name, to: @node
|
|
delegate to_s, to: @node
|
|
|
|
# Creates a new variable(in the scope).
|
|
#
|
|
# ```
|
|
# Variable.new(node, scope)
|
|
# ```
|
|
def initialize(@node, @scope)
|
|
end
|
|
|
|
# Returns `true` if it is a special variable, i.e `$?`.
|
|
def special?
|
|
@node.special_var?
|
|
end
|
|
|
|
# Assigns the variable (creates a new assignment).
|
|
# Variable may have multiple assignments.
|
|
#
|
|
# ```
|
|
# variable = Variable.new(node, scope)
|
|
# variable.assign(node1)
|
|
# variable.assign(node2)
|
|
# variable.assignment.size # => 2
|
|
# ```
|
|
def assign(node, scope)
|
|
assignments << Assignment.new(node, self, scope)
|
|
|
|
update_assign_reference!
|
|
end
|
|
|
|
# Returns `true` if variable has any reference.
|
|
#
|
|
# ```
|
|
# variable = Variable.new(node, scope)
|
|
# variable.reference(var_node)
|
|
# variable.referenced? # => true
|
|
# ```
|
|
def referenced?
|
|
!references.empty?
|
|
end
|
|
|
|
# Creates a reference to this variable in some scope.
|
|
#
|
|
# ```
|
|
# variable = Variable.new(node, scope)
|
|
# variable.reference(var_node, some_scope)
|
|
# ```
|
|
def reference(node : Crystal::Var, scope : Scope)
|
|
Reference.new(node, scope).tap do |reference|
|
|
references << reference
|
|
scope.references << reference
|
|
end
|
|
end
|
|
|
|
# Reference variable's assignments.
|
|
#
|
|
# ```
|
|
# variable = Variable.new(node, scope)
|
|
# variable.assign(assign_node)
|
|
# variable.reference_assignments!
|
|
# ```
|
|
def reference_assignments!
|
|
consumed_branches = Set(Branch).new
|
|
|
|
assignments.reverse_each do |assignment|
|
|
next if assignment.branch.in?(consumed_branches)
|
|
assignment.referenced = true
|
|
|
|
break unless branch = assignment.branch
|
|
consumed_branches << branch
|
|
end
|
|
end
|
|
|
|
# Returns `true` if the current var is referenced in
|
|
# in the block. For example this variable is captured
|
|
# by block:
|
|
#
|
|
# ```
|
|
# a = 1
|
|
# 3.times { |i| a = a + i }
|
|
# ```
|
|
#
|
|
# And this variable is not captured by block.
|
|
#
|
|
# ```
|
|
# i = 1
|
|
# 3.times { |i| i + 1 }
|
|
# ```
|
|
def captured_by_block?(scope = @scope)
|
|
scope.inner_scopes.each do |inner_scope|
|
|
return true if inner_scope.block? &&
|
|
inner_scope.references?(self, check_inner_scopes: false)
|
|
return true if captured_by_block?(inner_scope)
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
# Returns `true` if current variable potentially referenced in a macro,
|
|
# `false` if not.
|
|
def used_in_macro?(scope = @scope)
|
|
scope.inner_scopes.each do |inner_scope|
|
|
return true if MacroReferenceFinder.new(inner_scope.node, node.name).references?
|
|
end
|
|
return true if MacroReferenceFinder.new(scope.node, node.name).references?
|
|
return true if (outer_scope = scope.outer_scope) && used_in_macro?(outer_scope)
|
|
|
|
false
|
|
end
|
|
|
|
# Returns `true` if the variable is a target (on the left) of the assignment,
|
|
# `false` otherwise.
|
|
def target_of?(assign)
|
|
case assign
|
|
when Crystal::Assign then eql?(assign.target)
|
|
when Crystal::OpAssign then eql?(assign.target)
|
|
when Crystal::MultiAssign then assign.targets.any? { |target| eql?(target) }
|
|
when Crystal::UninitializedVar then eql?(assign.var)
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
# Returns `true` if the name starts with '_', `false` if not.
|
|
def ignored?
|
|
name.starts_with? '_'
|
|
end
|
|
|
|
# Returns `true` if the `node` represents exactly
|
|
# the same Crystal node as `@node`.
|
|
def eql?(node)
|
|
node.is_a?(Crystal::Var) &&
|
|
node.name == @node.name &&
|
|
node.location == @node.location
|
|
end
|
|
|
|
# Returns `true` if the variable is declared before the `node`.
|
|
def declared_before?(node)
|
|
var_location, node_location = location, node.location
|
|
|
|
return unless var_location && node_location
|
|
|
|
(var_location.line_number < node_location.line_number) ||
|
|
(var_location.line_number == node_location.line_number &&
|
|
var_location.column_number < node_location.column_number)
|
|
end
|
|
|
|
private class MacroReferenceFinder < Crystal::Visitor
|
|
property? references = false
|
|
|
|
def initialize(node, @reference : String)
|
|
node.accept self
|
|
end
|
|
|
|
@[AlwaysInline]
|
|
private def includes_reference?(val)
|
|
val.to_s.includes?(@reference)
|
|
end
|
|
|
|
def visit(node : Crystal::MacroLiteral)
|
|
!(@references ||= includes_reference?(node.value))
|
|
end
|
|
|
|
def visit(node : Crystal::MacroExpression)
|
|
!(@references ||= includes_reference?(node.exp))
|
|
end
|
|
|
|
def visit(node : Crystal::MacroFor)
|
|
!(@references ||= includes_reference?(node.exp) ||
|
|
includes_reference?(node.body))
|
|
end
|
|
|
|
def visit(node : Crystal::MacroIf)
|
|
!(@references ||= includes_reference?(node.cond) ||
|
|
includes_reference?(node.then) ||
|
|
includes_reference?(node.else))
|
|
end
|
|
|
|
def visit(node : Crystal::ASTNode)
|
|
true
|
|
end
|
|
end
|
|
|
|
private def update_assign_reference!
|
|
return if @assign_before_reference
|
|
return if references.size > assignments.size
|
|
return if assignments.any?(&.op_assign?)
|
|
|
|
@assign_before_reference = assignments.find { |ass|
|
|
!ass.in_branch?
|
|
}.try &.node
|
|
end
|
|
end
|
|
end
|