Let’s take this code:
def foo(x)
if x.is_a?(Int32)
1
else
2
end
end
foo(1 || 'a')
foo('a' || 1)
We can compile it like this:
crystal build --prelude=empty --emit llvm-ir foo.cr
That will give us the LLVM IR for that file, and specifically for what the generated foo
function is.
How does the generated code check if x.is_a?(Int32)
? The answer is that Crystal gives each type a unique ID: an integer.
Here’s the LLVM IR code for the generated foo
function:
define internal i32 @"*foo<(Char | Int32)>:Int32"(%"(Char | Int32)" %x) #0 !dbg !21 {
alloca:
%x1 = alloca %"(Char | Int32)", align 8, !dbg !22
br label %entry
entry: ; preds = %alloca
store %"(Char | Int32)" %x, %"(Char | Int32)"* %x1, align 8, !dbg !22
%0 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 0, !dbg !23
%1 = load i32, i32* %0, align 4, !dbg !23
%2 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 1, !dbg !23
%3 = icmp eq i32 12, %1, !dbg !23
br i1 %3, label %then, label %else, !dbg !23
then: ; preds = %entry
br label %exit, !dbg !23
else: ; preds = %entry
br label %exit, !dbg !23
exit: ; preds = %else, %then
%4 = phi i32 [ 1, %then ], [ 2, %else ], !dbg !23
ret i32 %4, !dbg !23
}
It’s hard to grok, but there are these lines:
%3 = icmp eq i32 11, %1, !dbg !23
br i1 %3, label %then, label %else, !dbg !23
If “something” is 11, then jump to the “then” label, otherwise jump to the “else” label. That 11 is the ID Crystal assigned to the Int32
type.
Now let’s slightly change the original program:
# This type wasn't there before!
class Foo
end
def foo(x)
if x.is_a?(Int32)
1
else
2
end
end
foo(1 || 'a')
foo('a' || 1)
Recompiling and checking the generated LLVM IR, we get this now for foo
:
define internal i32 @"*foo<(Char | Int32)>:Int32"(%"(Char | Int32)" %x) #0 !dbg !21 {
alloca:
%x1 = alloca %"(Char | Int32)", align 8, !dbg !22
br label %entry
entry: ; preds = %alloca
store %"(Char | Int32)" %x, %"(Char | Int32)"* %x1, align 8, !dbg !22
%0 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 0, !dbg !23
%1 = load i32, i32* %0, align 4, !dbg !23
%2 = getelementptr inbounds %"(Char | Int32)", %"(Char | Int32)"* %x1, i32 0, i32 1, !dbg !23
%3 = icmp eq i32 12, %1, !dbg !23
br i1 %3, label %then, label %else, !dbg !23
then: ; preds = %entry
br label %exit, !dbg !23
else: ; preds = %entry
br label %exit, !dbg !23
exit: ; preds = %else, %then
%4 = phi i32 [ 1, %then ], [ 2, %else ], !dbg !23
ret i32 %4, !dbg !23
}
Do you see what the comparison looks now?
%3 = icmp eq i32 12, %1, !dbg !23
br i1 %3, label %then, label %else, !dbg !23
Now the compiler assigned 12 to the type ID of Int32, presumably because types were ordered alphabetically and Foo
comes before Int32
. Well, I’m not sure if that’s the reason, but what’s important to know is that the type ID of a given type isn’t guaranteed to be the same across different compilations.
If we compile two Crystal programs into shared libraries and load them both at the same time, one of the foo
will win and override the other. When doing that, the logic for one of the programs will break, because if we pass an Int32
in that program the check x.is_a?(Int32)
will be false, likely leading to a segfault.
This is the main reason creating shared libraries in Crystal isn’t possible right now. Well, it’s possible if you only use one shared library, and that’s it… which in my opinion is not very useful.
So if you run into segfaults or strange behavior when playing with this… you know why! You didn’t do anything wrong: it’s just not supposed to work at all.
If we want this to work, we first have to solve the problem of changing type IDs.