crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.41k stars 1.62k forks source link

Upcast bug in compiler #1346

Open russolsen opened 9 years ago

russolsen commented 9 years ago

The following code crashes the compiler:

class Session;end

alias CmdHandler = Proc(Session, String, Nil)

class Session
  def initialize(&h: CmdHandler)
    @h = h
  end

  def handle(cmd: String)
    @h.call(self, cmd)
  end
end

class SmtpSession < Session
end

session = SmtpSession.new do |session, cmd|
  puts "handling cmd for #{session}: #{cmd}"
end

session.handle("hello")

Details:

~/projects/crystal/crystal: ./bin/crystal --version
Using compiled compiler at .build/crystal
Crystal 0.7.6 [fd79767] (Tue Sep  1 12:17:23 UTC 2015)

~/projects/crystal/crystal: ./bin/crystal ../fake_smtp/src/bug1.cr 
Using compiled compiler at .build/crystal
Bug: trying to upcast Session <- SmtpSession
*Exception#initialize<Exception, String, Nil>:Array(String) +14 [4492669598]
*Exception::new<String>:Exception +89 [4492669545]
*raise<String>:NoReturn +6 [4492476454]
*Crystal::CodeGenVisitor#upcast_distinct<Crystal::CodeGenVisitor, LLVM::Value, Crystal::Type+, Crystal::Type+>:NoReturn +97 [4503369809]
*Crystal::CodeGenVisitor#upcast<Crystal::CodeGenVisitor, LLVM::Value, Crystal::Type+, Crystal::Type+>:LLVM::Value +3230 [4503367470]
*Crystal::CodeGenVisitor#codegen_primitive_fun_call<Crystal::CodeGenVisitor, Crystal::Primitive+, Crystal::Def+, Array(LLVM::Value)>:LLVM::Value +516 [4503515012]
*Crystal::CodeGenVisitor#codegen_primitive<Crystal::CodeGenVisitor, Crystal::Primitive+, Crystal::Def+, Array(LLVM::Value)>:LLVM::Value +3040 [4503477280]
*Crystal::CodeGenVisitor#codegen_call<Crystal::CodeGenVisitor, Crystal::Call, Crystal::Def+, Crystal::Type+, Array(LLVM::Value)>:(Nil | LLVM::Value) +156 [4503473996]
*Crystal::CodeGenVisitor#visit<Crystal::CodeGenVisitor, Crystal::Call>:Bool +1365 [4503429989]
*Crystal::ASTNode+@Crystal::ASTNode#accept<Crystal::ASTNode+, Crystal::CodeGenVisitor>:Nil +4986 [4493097530]
*Crystal::CodeGenVisitor#accept<Crystal::CodeGenVisitor, Crystal::ASTNode+>:Nil +17 [4503340209]
*Crystal::CodeGenVisitor#codegen_fun<Crystal::CodeGenVisitor, String, Crystal::Def+, Crystal::Type+, Bool, LLVM::Module, Bool, Bool>:LLVM::Function +1874 [4503375650]
*Crystal::CodeGenVisitor#codegen_fun<Crystal::CodeGenVisitor, String, Crystal::Def+, Crystal::Type+>:LLVM::Function +120 [4503373752]
*Crystal::CodeGenVisitor#target_def_fun<Crystal::CodeGenVisitor, Crystal::Def+, Crystal::Type+>:LLVM::Function +375 [4503372487]
*Crystal::CodeGenVisitor#codegen_call<Crystal::CodeGenVisitor, Crystal::Call, Crystal::Def+, Crystal::Type+, Array(LLVM::Value)>:(Nil | LLVM::Value) +260 [4503474100]
*Crystal::CodeGenVisitor#visit<Crystal::CodeGenVisitor, Crystal::Call>:Bool +1365 [4503429989]
*Crystal::ASTNode+@Crystal::ASTNode#accept<Crystal::ASTNode+, Crystal::CodeGenVisitor>:Nil +4986 [4493097530]
*Crystal::CodeGenVisitor#accept<Crystal::CodeGenVisitor, Crystal::ASTNode+>:Nil +17 [4503340209]
*Crystal::CodeGenVisitor#visit<Crystal::CodeGenVisitor, Crystal::Expressions>:Bool +288 [4503385696]
*Crystal::Expressions@Crystal::ASTNode#accept<Crystal::Expressions, Crystal::CodeGenVisitor>:Nil +51 [4496417251]
*Crystal::Program#codegen<Crystal::Program, Crystal::Expressions, (Nil | String | Bool | Array(String)), Bool, LLVM::Module, Bool>:Hash(String, LLVM::Module) +135 [4493444119]
*Crystal::Program#codegen:debug:single_module:expose_crystal_main<Crystal::Program, Crystal::Expressions, Bool, (Nil | String | Bool | Array(String)), Bool>:Hash(String, LLVM::Module) +124 [4493443964]
*Crystal::Compiler#codegen<Crystal::Compiler, Crystal::Program, Crystal::Expressions, Array(Crystal::Compiler::Source), String>:(Nil | Int32) +1286 [4502827974]
*Crystal::Compiler#compile<Crystal::Compiler, Array(Crystal::Compiler::Source), String>:Crystal::Compiler::Result +558 [4502822622]
*Crystal::Command::CompilerConfig#compile<Crystal::Command::CompilerConfig, String>:Crystal::Compiler::Result +100 [4503960132]
*Crystal::Command::run_command<Array(String)>:Nil +373 [4503933589]
*Crystal::Command::run<Array(String)>:(Nil | Bool | FileDescriptorIO+ | Crystal::Compiler::Result | Array(Crystal::Init::View+:Class)) +172 [4503931068]
*Crystal::Command::run:(Nil | Bool | FileDescriptorIO+ | Crystal::Compiler::Result | Array(Crystal::Init::View+:Class)) +44 [4503930812]
__crystal_main +34868 [4492475620]
main +40 [4492503320]

Error: you've found a bug in the Crystal compiler. Please open an issue, including source code that will allow us to reproduce the bug: https://github.com/manastech/crystal/issues
asterite commented 9 years ago

As a workaround for now you can move the alias below the SmtpSession class.

russolsen commented 9 years ago

Thanks! That's very helpful.

jhass commented 9 years ago
class A
end

class B < A
end

module Foo
  alias Callback = A ->
  @@callbacks = Hash(String, Callback).new
  def self.add(name, &block : Callback)
    @@callbacks[name] = block
  end

  def self.call
    @@callbacks.each_value(&.call(B.new))
  end
end

Foo.add("foo") do |a|
  puts "hi"
end

Foo.call

I don't think that's helping in all cases.

asterite commented 9 years ago

Hmmm... yes. We can workaround that too, but it's better if we solve this issue "the right away". We'll take a look at it with @waj.

asterite commented 9 years ago

I fixed #1347. I'll still leave this open because order of declaration shouldn't matter, it should always work. This will need a bit of rethinking, but at least we have more specs and cases to test against.

rmosolgo commented 9 years ago

I have a really similar case: https://gist.github.com/rmosolgo/1f96e96d73a60b37bfcb

In my case the problem class was abstract, and adding abstract class InputObject "fixed" the issue.

Should I open a new issue?

asterite commented 6 years ago

This still happens if the alias is used in the top-level. This makes the compiler solve it for possible macro resolution (which also causes #6344 to behave differently depending on whether it's used or not at the top-level). Reproducible code:

class Session; end

alias CmdHandler = Proc(Session, String, Nil)

CmdHandler.class

class Session
  def initialize(&h : CmdHandler)
    @h = h
  end

  def handle(cmd : String)
    @h.call(self, cmd)
  end
end

class SmtpSession < Session
end

session = SmtpSession.new do |session, cmd|
  puts "handling cmd for #{session}: #{cmd}"
end

session.handle("hello")
vlazar commented 5 years ago

Seem like this can be closed?

The original code on top seem to work https://github.com/crystal-lang/crystal/issues/1346#issue-104253186 in Crystal 0.31.1

with these modifications:

class Session;end

alias CmdHandler = Proc(Session, String, Nil)

class Session
-  def initialize(&h: CmdHandler)
+  def initialize(&h : CmdHandler)
    @h = h
  end

-  def handle(cmd: String)
+  def handle(cmd : String)
    @h.call(self, cmd)
  end

class SmtpSession < Session
end

session = SmtpSession.new do |session, cmd|
  puts "handling cmd for #{session}: #{cmd}"
end

session.handle("hello")
end

Produces

$ crystal e.cr
handling cmd for #<SmtpSession:0x104abce80>: hello

Env

$ crystal --version
Crystal 0.31.1 (2019-10-02)

LLVM: 8.0.1
Default target: x86_64-apple-macosx
vlazar commented 5 years ago

However Ary's code in https://github.com/crystal-lang/crystal/issues/1346#issuecomment-406468683 gives this:

$ crystal e.cr
Showing last frame. Use --error-trace for full trace.

In e.cr:8:3

 8 | def initialize(&h : CmdHandler)
     ^---------
Error: expected block argument's argument #1 to be Session, not Session+
konovod commented 4 years ago

Error is actually true, it works if you make it Session+ Actually, Proc(Session+, String, Nil) fails to parse, but Proc((Session | Session1), String, Nil) works https://carc.in/#/r/7wpe

asterite commented 2 years ago

Regarding https://github.com/crystal-lang/crystal/issues/1346#issuecomment-406468683

Maybe one way to fix this is by not caching the resolved alias inside the alias type when trying to do macro expansion the first time. The way it works now is like this:

Because Session doesn't have subclasses, it's not a virtual type. We store this resolved alias inside the alias type itself. Next time the alias' type is checked, regardless of whether Session had a sublcass later on, the alias' resolved type won't be virtual (it won't be Proc(Session+, ...))

So instead of remembering that the alias resolves to that, in this first pass, we could try not caching that information. Later on when the alias is checked again, after Session had a subclass, it will resolve to the correct value.

An alternative is to stop having this distinction between virtual and non virtual types and just consider all types as potentially virtual.

Either of those changes are pretty tricky to implement.

beta-ziliani commented 2 years ago

Following the Crystal approach of making virtual types if necessary, I would expect the same here: if you only use (by some definition of use) the parent class, then there's no need to virtualize it. I guess this is compatible to the the two-passes you're referring to, right? The alternative you're suggesting is to do the opposite, always consider a type as virtual, only to tighten it to not-virtual later as an optimization?

asterite commented 2 years ago

if you only use (by some definition of use) the parent class, then there's no need to virtualize it. I guess this is compatible to the the two-passes you're referring to, right?

The problem is that macro expansion reaches Proc(Session, ...) before it knows Session has subclasses. That's why it's wrong for Session to not be virtual there. And why I also suggest not eagerly making that distinction.

The alternative you're suggesting is to do the opposite, always consider a type as virtual, only to tighten it to not-virtual later as an optimization?

Exactly. Though I think it's probably a breaking change. For example:

class Foo
  def foo
    1
  end
end

class Bar < Foo
  def foo
    'a'
  end
end

foo = Foo.new
foo.foo + 1 

The above works fine because the type of Foo.new returns Foo, not "Foo or any of its subclasses". If we change that, the code will break, because a call to foo.foo will do a dispatch over Foo | Bar, even though in practice it's never the case that foo will be Bar.