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
259 changes: 259 additions & 0 deletions spec/compiler/semantic/abstract_def_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1152,4 +1152,263 @@ describe "Semantic: abstract def" do
CRYSTAL
end
end

it "reports undefined constant error for undefined type in abstract method" do
assert_error <<-CRYSTAL, "undefined constant Unknown"
module Foo
abstract def foo(x : Unknown)
end

class Bar
include Foo

def foo(x : Int32)
end
end
CRYSTAL
end

it "reports undefined constant error for undefined type in implementing method" do
assert_error <<-CRYSTAL, "undefined constant Unknown"
module Foo
abstract def foo(x : Int32)
end

class Bar
include Foo

def foo(x : Unknown)
end
end
CRYSTAL
end

it "allows forall type parameters in abstract methods" do
assert_no_errors <<-CRYSTAL
module Foo
abstract def foo(x : U) : U forall U
end

class Bar
include Foo

def foo(x : U) : U forall U
x
end
end
CRYSTAL
end

it "allows generic type parameters from enclosing class" do
assert_no_errors <<-CRYSTAL
module Comparable(T)
abstract def <=>(other : T)
end

struct Slice(T)
include Comparable(Slice(T))

def <=>(other : Slice(T))
0
end
end
CRYSTAL
end

it "allows multiple generic type parameters" do
assert_no_errors <<-CRYSTAL
abstract class Container(K, V)
abstract def get(key : K) : V?
abstract def set(key : K, value : V) : Nil
end

class HashMap(K, V) < Container(K, V)
def get(key : K) : V?
nil
end

def set(key : K, value : V) : Nil
end
end
CRYSTAL
end

it "distinguishes between undefined types and valid type parameters" do
assert_error <<-CRYSTAL, "undefined constant UnknownType"
abstract class Container(T)
abstract def process(x : T, y : UnknownType)
end

class MyContainer(T) < Container(T)
def process(x : T, y : String)
end
end
CRYSTAL
end

it "reports undefined multi-segment paths" do
assert_error <<-CRYSTAL, "undefined constant Some::Path::Unknown"
module Foo
abstract def foo(x : Some::Path::Unknown)
end

class Bar
include Foo

def foo(x : Int32)
end
end
CRYSTAL
end

it "accepts untyped parameter implementing forall abstract method with return type" do
assert_no_errors <<-CRYSTAL
module Interface
abstract def transform(value : T) : T forall T
end

class Implementation
include Interface

def transform(value)
value
end
end
CRYSTAL
end

it "accepts untyped parameter implementing forall abstract method without return type" do
assert_no_errors <<-CRYSTAL
module Interface
abstract def transform(value : T) forall T
end

class Implementation
include Interface

def transform(value)
value
end
end
CRYSTAL
end

it "accepts specific type implementing forall abstract method" do
assert_no_errors <<-CRYSTAL
module Interface
abstract def transform(type : T.class) : T forall T
end

class Implementation
include Interface

def transform(type : Int32.class) : Int32
123
end
end
CRYSTAL
end

it "accepts more specific type implementing forall generic parameter" do
assert_no_errors <<-CRYSTAL
module Interface
abstract def transform(x : Array(T)) forall T
end

class Implementation
include Interface

def transform(x : Indexable) : Int32
123
end
end
CRYSTAL
end

it "accepts generic method implementing generic abstract method with forall" do
assert_no_errors <<-CRYSTAL
module Interface
abstract def transform(type : T.class) : T forall T
end

class Implementation
include Interface

def transform(type : T.class) : T forall T
end
end
CRYSTAL
end

it "accepts generic method when implementation has at least as many forall values than abstract" do
assert_no_errors <<-CRYSTAL
abstract class Foo
abstract def foo(x : T, y : Int32) forall T
end

class Bar < Foo
def foo(x : T, y : U) forall T, U; end
end
CRYSTAL
end

it "accepts matching forall parameters even with different names" do
assert_no_errors <<-CRYSTAL
module Interface
abstract def transform(value : T) : T forall T
end

class Implementation
include Interface

def transform(value : U) : U forall U
value
end
end
CRYSTAL
end

it "accepts specific types implementing forall abstract method" do
assert_no_errors <<-CRYSTAL
abstract class Container
abstract def get(key : K) : V forall K, V
end

class MyContainer < Container
def get(key : String) : Int32
42
end
end
CRYSTAL
end

it "accepts non-generic implementation when abstract def has no forall" do
assert_no_errors <<-CRYSTAL
abstract class Base
abstract def process(x : Int32) : String
end

class Derived < Base
def process(x : Int32) : String
x.to_s
end
end
CRYSTAL
end

pending "errors when implementation uses incompatible type with forall abstract method" do
assert_error <<-CRYSTAL, "abstract `def Interface#transform(x : Array(T)) forall T` must be implemented"
module Interface
abstract def transform(x : Array(T)) forall T
end

class Implementation
include Interface

def transform(x : Int32) : Int32
123
end
end
CRYSTAL
end
end
2 changes: 1 addition & 1 deletion src/compiler/crystal/codegen/abi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ abstract class Crystal::ABI
end
end

abstract def abi_info(atys : Array(LLVM::Type), rty : LLVM::Type, ret_def : Bool, context : Context)
abstract def abi_info(atys : Array(LLVM::Type), rty : LLVM::Type, ret_def : Bool, context : LLVM::Context)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're validating these, it errored that it didn't know what Context is.

abstract def size(type : LLVM::Type)
abstract def align(type : LLVM::Type)

Expand Down
66 changes: 58 additions & 8 deletions src/compiler/crystal/semantic/abstract_def_checker.cr
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class Crystal::AbstractDefChecker

if implements?(target_type, ancestor_type, a_def, def_free_vars, base, method, method_free_vars)
unless implemented
check_return_type(target_type, ancestor_type, a_def, base, method)
check_return_type(target_type, ancestor_type, a_def, def_free_vars, base, method, method_free_vars)
implemented = true
end

Expand Down Expand Up @@ -264,19 +264,66 @@ class Crystal::AbstractDefChecker
return false if r1 && !r2
if r2 && r1 && r1 != r2
# Check if a1.restriction is contravariant with a2.restriction
rt1 = nil
rt2 = nil

begin
rt1 = t1.lookup_type(r1, free_vars: free_vars1)
rescue Crystal::TypeException
end

begin
rt2 = t2.lookup_type(r2, free_vars: free_vars2)
return false unless rt2.implements?(rt1)
rescue Crystal::TypeException
# Ignore if we can't find a type (assume the method is implemented)
return true
end

if rt1 && rt2
# Both types resolved - check compatibility
return false unless rt2.implements?(rt1)
elsif !rt2
# Only implementation side couldn't be resolved
if !can_lookup?(r2, free_vars2, t2)
report_undefined_type(r2)
end
elsif !rt1
# Only abstract side couldn't be resolved
if !can_lookup?(r1, free_vars1, t1)
report_undefined_type(r1)
end
end
end

true
end

# Check if a restriction can potentially be looked up (is a free var or might be resolved later)
private def can_lookup?(restriction : ASTNode, free_vars, type : Type? = nil) : Bool
return true unless restriction.is_a?(Path)

# For single-name paths, check if it's a free variable
if name = restriction.single_name?
# Check if it's in free_vars (forall parameters)
return true if free_vars && free_vars.has_key?(name)

# Check if it's a type parameter of the enclosing generic type
if type && type.is_a?(GenericType)
return true if type.type_vars.includes?(name)
end

return false
end

return true if restriction.global?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this time all the types have been defined, so I don't really know what this is supposed to do. I would expect to see an error in either abstract def here:

abstract class Foo
  abstract def foo(x : Int32)
  abstract def bar(x : ::A)
end

class Bar < Foo
  def foo(x : ::A); end
  def bar(x : Int32); end
end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a way to retain current behavior, as without you get a lot of spec failures:

  9) Codegen: is_a? evaluates method on filtered union type 3

       In src/crystal/event_loop/polling.cr:255:21

        255 | def read(socket : ::Socket, slice : Bytes) : Int32
                                ^-------
       Error: undefined constant ::Socket  from src/compiler/crystal/semantic/ast.cr:6:7 in 'raise'
         from src/compiler/crystal/semantic/ast.cr:5:5 in 'raise'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:461:5 in 'report_error'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:335:5 in 'report_undefined_type'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:272:27 in 'check_arg'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:203:29 in 'implements?'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:92:20 in 'check_implemented_in_subtypes'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:79:9 in 'check_single'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:38:7 in 'check_single'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:38:7 in 'check_single'
         from src/compiler/crystal/semantic/abstract_def_checker.cr:38:7 in 'run'
         from src/compiler/crystal/progress_tracker.cr:23:20 in 'top_level_semantic'
         from src/compiler/crystal/semantic.cr:22:23 in 'semantic:cleanup'
         from src/compiler/crystal/compiler.cr:229:16 in 'compile'
         from src/process/shell.cr:14:5 in 'run'
         from spec/compiler/codegen/is_a_spec.cr:159:5 in '->'
         from src/spec/example.cr:50:13 in 'internal_run'
         from src/spec/example.cr:38:16 in 'run'
         from src/spec/context.cr:20:23 in 'run'
         from src/spec/context.cr:20:23 in '->'
         from src/crystal/at_exit_handlers.cr:18:17 in 'main'
         from src/crystal/system/unix/main.cr:10:3 in 'main'
         from /usr/lib/libc.so.6 in '??'
         from /usr/lib/libc.so.6 in '__libc_start_main'
         from .build/compiler_spec in '_start'
         from ???

But taking a second look, I think it's just another case similar to the LLVM Context. Maybe a question for @ysbaddaden given he's more familiar with this part of the code.

Copy link
Contributor

@ysbaddaden ysbaddaden Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstract EventLoop interface expects that the ::Socket type may not be defined, because we must always require "crystal/event_loop" but ::Socket is only defined when we explicitly require "socket".


# Other multi-segment paths can't be free variables
# Return false to indicate they should be reported as undefined if lookup failed
false
end

private def report_undefined_type(restriction : ASTNode)
report_error(restriction, "undefined constant #{restriction}")
end

def same_parameters?(m1 : Def, m2 : Def)
return false unless m1.args.size == m2.args.size

Expand Down Expand Up @@ -318,12 +365,13 @@ class Crystal::AbstractDefChecker

# Checks that the return type of `type#method` matches that of `base_type#base_method`
# when computing that information for `target_type` (`type` is an ancestor of `target_type`).
def check_return_type(target_type : Type, type : Type, method : Def, base_type : Type, base_method : Def)
def check_return_type(target_type : Type, type : Type, method : Def, method_free_vars, base_type : Type, base_method : Def, base_method_free_vars)
base_return_type_node = base_method.return_type
return unless base_return_type_node

original_base_return_type = base_type.lookup_type?(base_return_type_node)
original_base_return_type = base_type.lookup_type?(base_return_type_node, free_vars: base_method_free_vars)
unless original_base_return_type
return if can_lookup?(base_return_type_node, base_method_free_vars, base_type)
report_error(base_return_type_node, "can't resolve return type #{base_return_type_node}")
return
end
Expand All @@ -340,8 +388,9 @@ class Crystal::AbstractDefChecker
base_return_type_node.accept(replacer)
end

base_return_type = base_type.lookup_type?(base_return_type_node)
base_return_type = base_type.lookup_type?(base_return_type_node, free_vars: base_method_free_vars)
unless base_return_type
return if can_lookup?(base_return_type_node, base_method_free_vars, base_type)
report_error(base_return_type_node, "can't resolve return type #{base_return_type_node}")
return
end
Expand All @@ -352,8 +401,9 @@ class Crystal::AbstractDefChecker
return
end

return_type = type.lookup_type?(return_type_node)
return_type = type.lookup_type?(return_type_node, free_vars: method_free_vars)
unless return_type
return if can_lookup?(return_type_node, method_free_vars, type)
report_error(return_type_node, "can't resolve return type #{return_type_node}")
return
end
Expand Down
Loading