Skip to content

Commit

Permalink
Showing 9 changed files with 192 additions and 196 deletions.
12 changes: 12 additions & 0 deletions spec/compiler/semantic/did_you_mean_spec.cr
Original file line number Diff line number Diff line change
@@ -261,4 +261,16 @@ describe "Semantic: did you mean" do
end
), message
end

it "says did you mean in instance var declaration" do
assert_error %(
class FooBar
end
class Foo
@x : FooBaz
end
),
"did you mean 'FooBar'"
end
end
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/enum.cr
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ class Crystal::SemanticVisitor
end

def interpret_enum_value(node : Path, target_type = nil)
type = resolve_ident(node)
type = lookup_type(node)
case type
when Const
interpret_enum_value(type.value, target_type)
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
@@ -128,7 +128,7 @@ module Crystal
end

def visit(node : Path)
type = resolve_ident(node)
type = (@path_lookup || @scope || @current_type).lookup_type_var(node, free_vars: @free_vars)
case type
when Const
if !type.value.type? && !type.visited?
113 changes: 56 additions & 57 deletions src/compiler/crystal/semantic/path_lookup.cr
Original file line number Diff line number Diff line change
@@ -21,95 +21,92 @@ module Crystal
#
# Returns `nil` if the path can't be found.
#
# The result can be an ASTNode in the case the path denotes a type variable
# whose variable is an ASTNode. One such example is the `N` of `StaticArray(T, N)`
# The result can be an `ASTNode` in the case the path denotes a type variable
# whose variable is an `ASTNode`. One such example is the `N` of `StaticArray(T, N)`
# for some instantiated `StaticArray`.
#
# If the path is global (for example ::Foo::Bar), the search starts at
# the top level.
def lookup_path(path : Path, lookup_in_namespace = true) : Type | ASTNode | Nil
(path.global? ? program : self).lookup_path(path.names, lookup_in_namespace: lookup_in_namespace)
rescue ex : Crystal::Exception
raise ex
rescue ex
path.raise ex.message
end

# ditto
def lookup_path(path : Array(String), lookup_in_namespace = true) : Type | ASTNode | Nil
raise "Bug: #{self} doesn't implement lookup_path"
end
end

class NamedType
def lookup_path(names : Array, lookup_in_namespace = true)
def lookup_path(names : Array(String), lookup_in_namespace = true) : Type | ASTNode | Nil
type = self
names.each_with_index do |name, i|
next_type = type.types?.try &.[name]?
if !next_type && i != 0
# Once we find a first type we search in it and don't backtrack
return type.lookup_path_in_parents(names[i..-1])
end
type = next_type
break unless type
# The search must continue in the namespace only for the first path
# item: for subsequent path items only the parents must be looked up
type = type.lookup_path_item(name, lookup_in_namespace: lookup_in_namespace && i == 0)
return unless type

# Stop if this is the last name
break if i == names.size - 1

# An intermediate match could be an ASTNode, for example
# when searching T::N::X, and T denotes a static array:
# in this case we can't continue searching past `N`
return unless type.is_a?(Type)
end
type
end

# Looks up a single path item relative to *self`.
#
# If *lookup_in_namespace* is `true`, if the type is not found
# in `self` or `self`'s parents, the path item is searched in this
# type's namespace. This parameter is useful because when writing
# `Foo::Bar::Baz`, `Foo` should be searched in enclosing namespaces,
# but `Bar` and `Baz` not.
def lookup_path_item(name : String, lookup_in_namespace) : Type | ASTNode | Nil
# First search in our types
type = types?.try &.[name]?
return type if type

parent_match = lookup_path_in_parents(names)
return parent_match if parent_match

lookup_in_namespace && self != program ? namespace.lookup_path(names) : nil
end

protected def lookup_path_in_parents(names : Array, lookup_in_namespace = false)
# Then try out parents, but don't search in our parents namespace
parents.try &.each do |parent|
match = parent.lookup_path(names, lookup_in_namespace)
return match if match.is_a?(Type)
match = parent.lookup_path_item(name, lookup_in_namespace: false)
return match if match
end

# Try our namespace, unless we are the top-level
if lookup_in_namespace && self != program
return namespace.lookup_path_item(name, lookup_in_namespace)
end

nil
end
end

module GenericType
def lookup_path(names : Array, lookup_in_namespace = true)
def lookup_path_item(name : String, lookup_in_namespace)
# If we are Foo(T) and somebody looks up the type T, we return `nil` because we don't
# know what type T is, and we don't want to continue search in the namespace
if !names.empty? && type_vars.includes?(names[0])
if type_vars.includes?(name)
return nil
end
super
end
end

class GenericClassInstanceType
def lookup_path(names : Array, lookup_in_namespace = true)
if !names.empty? && (type_var = type_vars[names[0]]?)
case type_var
when Var
type_var_type = type_var.type
else
type_var_type = type_var
end

if names.size > 1
if type_var_type.is_a?(Type)
type_var_type.lookup_path(names[1..-1], lookup_in_namespace)
else
raise "#{names[0]} is not a type, it's #{type_var_type}"
end
def lookup_path_item(name : String, lookup_in_namespace)
# Check if *name* is a type variable
if type_var = type_vars[name]?
if type_var.is_a?(Var)
type_var.type
else
type_var_type
type_var
end
else
generic_class.lookup_path(names, lookup_in_namespace)
generic_class.lookup_path_item(name, lookup_in_namespace)
end
end
end

class IncludedGenericModule
def lookup_path(names : Array, lookup_in_namespace = true)
if (names.size == 1) && (m = @mapping[names[0]]?)
def lookup_path_item(name : String, lookup_in_namespace)
if m = @mapping[name]?
# Case of a variadic tuple
if m.is_a?(TupleLiteral)
types = m.elements.map do |element|
@@ -126,13 +123,13 @@ module Crystal
end
end

@module.lookup_path(names, lookup_in_namespace)
@module.lookup_path_item(name, lookup_in_namespace)
end
end

class InheritedGenericClass
def lookup_path(names : Array, lookup_in_namespace = true)
if (names.size == 1) && (m = @mapping[names[0]]?)
def lookup_path_item(name : String, lookup_in_namespace)
if m = @mapping[name]?
extending_class = self.extending_class
case extending_class
when GenericClassType
@@ -147,16 +144,18 @@ module Crystal
end
end

@extended_class.lookup_path(names, lookup_in_namespace)
@extended_class.lookup_path_item(name, lookup_in_namespace)
end
end

class UnionType
def lookup_path(names : Array, lookup_in_namespace = true)
if names.size == 1 && names[0] == "T"
def lookup_path_item(name : String, lookup_in_namespace)
# Union type does not currently inherit GenericClassInstanceType,
# so we check if *name* is the only type variable of Union(*T)
if name == "T"
return program.tuple_of(union_types)
end
program.lookup_path(names, lookup_in_namespace)
program.lookup_path_item(name, lookup_in_namespace)
end
end

115 changes: 5 additions & 110 deletions src/compiler/crystal/semantic/semantic_visitor.cr
Original file line number Diff line number Diff line change
@@ -201,111 +201,10 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor
end
end

def lookup_path_type(node : Self, create_modules_if_missing = false)
current_type
end

def lookup_path_type(node : Path, create_modules_if_missing = false)
target_type = resolve_ident(node, create_modules_if_missing)
if target_type.is_a?(Type)
target_type.remove_alias_if_simple
else
node.raise "#{node} must be a type here, not #{target_type}"
end
end

def lookup_path_type(node : Generic, create_modules_if_missing = false)
lookup_path_type node.name, create_modules_if_missing
end

def lookup_path_type(node, create_modules_if_missing = false)
raise "lookup_path_type not implemented for #{node}"
end

def resolve_ident(node : Path, create_modules_if_missing = false)
target_type, similar_name = resolve_ident?(node, create_modules_if_missing)

unless target_type
Crystal.check_cant_infer_generic_type_parameter(@scope, node)

error_msg = String.build do |msg|
msg << "undefined constant #{node}"
msg << @program.colorize(" (did you mean '#{similar_name}'?)").yellow.bold if similar_name
end
node.raise error_msg
end

target_type
end

def resolve_ident?(node : Path, create_modules_if_missing = false)
free_vars = @free_vars
if free_vars && !node.global? && (type_var = free_vars[node.names.first]?)
if type_var.is_a?(Type)
target_type = type_var
if node.names.size > 1
target_type = lookup_path target_type, node.names[1..-1], node
end
else
target_type = type_var
end
else
base_lookup = node.global? ? program : (@path_lookup || @scope || @current_type)
target_type = lookup_path base_lookup, node, node

unless target_type
if create_modules_if_missing
next_type = base_lookup
node.names.each do |name|
next_type = lookup_path base_lookup, [name], node, lookup_in_namespace: false
if next_type
if next_type.is_a?(ASTNode)
node.raise "execpted #{name} to be a type"
end
else
next_type = NonGenericModuleType.new(@program, base_lookup, name)

if (location = node.location)
next_type.locations << location
end

base_lookup.types[name] = next_type
end
base_lookup = next_type
end
target_type = next_type
else
similar_name = base_lookup.lookup_similar_path(node)
end
end
end

{target_type, similar_name}
end

def lookup_type(node : ASTNode)
current_type.lookup_type(node, allow_typeof: false)
end

def lookup_path(base_type, names, node, lookup_in_namespace = true)
base_type.lookup_path names, lookup_in_namespace: lookup_in_namespace
rescue ex : Crystal::Exception
raise ex
rescue ex
node.raise ex.message
end

def process_type_name(node_name)
if node_name.names.size == 1 && !node_name.global?
scope = current_type
name = node_name.names.first
else
name = node_name.names.pop
scope = lookup_path_type node_name, create_modules_if_missing: true
end
{scope, name}
end

def check_outside_exp(node, op)
node.raise "can't #{op} dynamically" if inside_exp?
end
@@ -325,11 +224,8 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor
obj = node.obj
case obj
when Path
if raise_on_missing_const
macro_scope = resolve_ident(obj)
else
macro_scope, similar_name = resolve_ident?(obj)
end
base_type = @path_lookup || @scope || @current_type
macro_scope = base_type.lookup_type_var?(obj, free_vars: @free_vars, raise: raise_on_missing_const)
return false unless macro_scope.is_a?(Type)

macro_scope = macro_scope.remove_alias
@@ -518,11 +414,10 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor

def class_var_owner(node)
scope = (@scope || current_type).class_var_owner
if scope.is_a?(Program)
case scope
when Program
node.raise "can't use class variables at the top level"
end

if scope.is_a?(GenericClassType) || scope.is_a?(GenericModuleType)
when GenericClassType, GenericModuleType
node.raise "can't use class variables in generic types"
end

56 changes: 50 additions & 6 deletions src/compiler/crystal/semantic/top_level_visitor.cr
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
node_superclass = node.superclass

if node_superclass
superclass = lookup_path_type(node_superclass)
superclass = lookup_type_name(node_superclass)
else
superclass = node.struct? ? program.struct : program.reference
end
@@ -61,7 +61,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
end
end

scope, name = process_type_name(node.name)
scope, name = lookup_type_def_name(node.name)

type = scope.types[name]?

@@ -162,7 +162,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
def visit(node : ModuleDef)
check_outside_exp node, "declare module"

scope, name = process_type_name(node.name)
scope, name = lookup_type_def_name(node.name)

type = scope.types[name]?
if type
@@ -245,7 +245,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
end
current_type.metaclass
else
type = lookup_path_type(receiver).metaclass
type = lookup_type(receiver).metaclass
node.raise "can't define 'def' for lib" if type.is_a?(LibType)
type
end
@@ -393,7 +393,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
attributes = check_valid_attributes node, ValidEnumDefAttributes, "enum"
attributes_doc = attributes_doc()

scope, name = process_type_name(node.name)
scope, name = lookup_type_def_name(node.name)

enum_type = scope.types[name]?
if enum_type
@@ -683,7 +683,7 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor

def include_in(current_type, node, kind)
node_name = node.name
type = lookup_path_type(node_name)
type = lookup_type_name(node_name)

unless type.module?
node_name.raise "#{type} is not a module, it's a #{type.type_desc}"
@@ -830,4 +830,48 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
end
end
end

def lookup_type_def_name(path)
if path.names.size == 1 && !path.global?
scope = current_type
name = path.names.first
else
path = path.clone
name = path.names.pop
scope = lookup_type_def_name_creating_modules path
end
{scope, name}
end

def lookup_type_name(node)
node = node.name if node.is_a?(Generic)
lookup_type(node)
end

def lookup_type_def_name_creating_modules(path : Path)
base_type = path.global? ? program : current_type
target_type = base_type.lookup_path(path).as?(Type).try &.remove_alias_if_simple

unless target_type
next_type = base_type
path.names.each do |name|
next_type = base_type.lookup_path_item(name, lookup_in_namespace: false)
if next_type
if next_type.is_a?(ASTNode)
path.raise "execpted #{name} to be a type"
end
else
next_type = NonGenericModuleType.new(@program, base_type, name)
if (location = path.location)
next_type.locations << location
end
base_type.types[name] = next_type
end
base_type = next_type
end
target_type = next_type
end

target_type
end
end
75 changes: 63 additions & 12 deletions src/compiler/crystal/semantic/type_lookup.cr
Original file line number Diff line number Diff line change
@@ -47,6 +47,17 @@ class Crystal::Type
TypeLookup.new(self, self_type, false, allow_typeof, free_vars).lookup(node)
end

# Similar to `lookup_type`, but the result might also be an ASTNode, for example when
# looking `N` relative to a StaticArray.
def lookup_type_var(node : Path, free_vars : Hash(String, TypeVar)? = nil) : Type | ASTNode
TypeLookup.new(self, self.instance_type, true, false, free_vars).lookup_type_var(node).not_nil!
end

# Similar to `lookup_type_var`, but might return `nil`.
def lookup_type_var?(node : Path, free_vars : Hash(String, TypeVar)? = nil, raise = false) : Type | ASTNode | Nil
TypeLookup.new(self, self.instance_type, raise, false, free_vars).lookup_type_var?(node)
end

# :nodoc:
struct TypeLookup
def initialize(@root : Type, @self_type : Type, @raise : Bool, @allow_typeof : Bool, @free_vars : Hash(String, TypeVar)? = nil)
@@ -56,13 +67,39 @@ class Crystal::Type
delegate program, to: @root

def lookup(node : Path)
if (free_vars = @free_vars) && node.names.size == 1
if (type = free_vars[node.names.first]?).is_a?(Type)
return type
type_var = lookup_type_var?(node)
return type_var if type_var.is_a?(Type)

if @raise
raise_undefined_constant(node)
else
nil
end
end

def lookup_type_var(node : Path)
type_var = lookup_type_var?(node)
return type_var if type_var

if @raise
raise_undefined_constant(node)
else
nil
end
end

def lookup_type_var?(node : Path)
# Check if the Path begins with a free variable
if !node.global? && (free_var = @free_vars.try &.[node.names.first]?)
if node.names.size == 1
return free_var
elsif free_var.is_a?(Type)
type = free_var.lookup_path(node.names[1..-1])
end
else
type = @root.lookup_path(node)
end

type = @root.lookup_path(node)
if type.is_a?(Type)
if @in_generic_args == 0 && type.is_a?(AliasType) && !type.aliased_type?
if type.value_processed?
@@ -71,15 +108,10 @@ class Crystal::Type
type.process_value
end
end
type.remove_alias_if_simple
else
if @raise
Crystal.check_cant_infer_generic_type_parameter(@root, node)
node.raise("undefined constant #{node}")
else
nil
end
type = type.remove_alias_if_simple
end

type
end

def lookup(node : Union)
@@ -285,6 +317,25 @@ class Crystal::Type
raise "Bug: unknown node in TypeLookup: #{node} #{node.class_desc}"
end

def raise_undefined_constant(node)
check_cant_infer_generic_type_parameter(@root, node)
similar_name = @root.lookup_similar_path(node)
if similar_name
node.raise("undefined constant #{node} #{@root.program.colorize("(did you mean '#{similar_name}')").yellow.bold}")
else
node.raise("undefined constant #{node}")
end
end

def check_cant_infer_generic_type_parameter(scope, node)
if scope.is_a?(MetaclassType) && (instance_type = scope.instance_type).is_a?(GenericClassType)
first_name = node.names.first
if instance_type.type_vars.includes?(first_name)
node.raise "can't infer the type parameter #{first_name} for the #{instance_type.type_desc} #{instance_type}. Please provide it explicitly"
end
end
end

def in_generic_args
@in_generic_args += 1
value = yield
4 changes: 4 additions & 0 deletions src/compiler/crystal/types.cr
Original file line number Diff line number Diff line change
@@ -30,6 +30,10 @@ module Crystal
self.is_a?(NilType) ? 0_u64 : object_id
end

def namespace : Type
program
end

def abstract?
false
end
9 changes: 0 additions & 9 deletions src/compiler/crystal/util.cr
Original file line number Diff line number Diff line change
@@ -45,13 +45,4 @@ module Crystal
def self.with_line_numbers(source : String)
source.lines.map_with_index { |line, i| "#{"%3d" % (i + 1)}. #{line.to_s.chomp}" }.join "\n"
end

def self.check_cant_infer_generic_type_parameter(scope, node : Path)
if scope.is_a?(MetaclassType) && (instance_type = scope.instance_type).is_a?(GenericClassType)
first_name = node.names.first
if instance_type.type_vars.includes?(first_name)
node.raise "can't infer the type parameter #{first_name} for the #{instance_type.type_desc} #{instance_type}. Please provide it explicitly"
end
end
end
end

0 comments on commit f7fff74

Please sign in to comment.