Skip to content

Bug: typeof returns virtual type with interpreter #16377

@cyangle

Description

@cyangle

Extracted from #14967

class Foo
end

class Bar < Foo
end

foo = Bar.new.as(Foo)
puts typeof(foo) == Foo                   # interpreted: false, compiled: true

OK, It seems like AI found the root cause and fix for the typeof(foo) == Foo # false discrepancy in interpreter.

AI Summary:

Root Cause

The issue stemmed from how the Crystal interpreter handled the typeof() operator. When typeof() was used on a variable that was upcasted (e.g., Bar.new.as(Foo)), the compiler's semantic analysis assigned a virtual metaclass (e.g., Foo+.class) as the type of the TypeOf AST node. This virtual type represents not just the class itself, but the class and all of its descendants.

The interpreter was using this virtual type directly. When typeof(foo) == Foo was evaluated, it was comparing the virtual metaclass Foo+.class with the concrete metaclass Foo.class. These types are not equivalent, so the comparison correctly returned false, leading to the bug. The compiled version of the code does not have this issue because the typeof operator is correctly resolved to the static, concrete type at compile time.


The Fix

The fix was to ensure the interpreter uses a concrete type for typeof expressions, mirroring the behavior of compiled code. This was achieved by modifying the visit(node : TypeOf) method in /home/chao/git/personal/crystal/src/compiler/crystal/interpreter/compiler.cr.

By calling node.type.devirtualize, the virtual metaclass provided by the TypeOf node is converted into its concrete counterpart. This ensures that typeof(foo) resolves to Foo.class, allowing the comparison typeof(foo) == Foo to correctly evaluate to true.


The git patch

--- a/src/compiler/crystal/interpreter/compiler.cr
+++ b/src/compiler/crystal/interpreter/compiler.cr
@@ -1421,7 +1421,10 @@ class Crystal::Repl::Compiler < Crystal::Visitor
   def visit(node : TypeOf)
     return false unless @wants_value
 
-    put_type node.type, node: node
+    # The type of a typeof node can be a virtual metaclass, but typeof
+    # should return a concrete type, so we devirtualize it.
+    type = node.type.devirtualize
+    put_type type, node: node
     false
   end

The compiler also devirtualizes the raw type when visiting the TypeOf node:

def visit(node : TypeOf)
# convert virtual metaclasses to non-virtual ones, because only the
# non-virtual type IDs are needed
set_current_debug_location(node) if @debug.line_numbers?
@last = type_id(node.type.devirtualize)
false
end

Originally posted by @cyangle in #14967

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions