ysbaddaden / nanolib.cr

18 stars 0 forks source link

Custom primitives to disable binary ops across int/float sizes #1

Open ysbaddaden opened 6 months ago

ysbaddaden commented 6 months ago

The current primitives.cr in crystal always define cross integer and float sizes binary operations. I'd like to disable that in nano, and only have binary operations across same type & sizes, only keeping the checked & unchecked conversions. We could then bring them back on demand.

Note: I wonder why cross type operations are primitives, they could be handled in stdlib only:

struct Int32
  @[AlwaysInline]
  def +(other : Int8) : Int32
    self + other.to_i32!
  end
end
straight-shoota commented 6 months ago

I wonder why cross type operations are primitives, they could be handled in stdlib only.

LLVM exposes cross-size operations directly, so it's quite easily available. Maybe there's some efficiency benefit? Although I would expect the optimizer to handle this as well. But maybe primitives are more performant without optimizations?

ysbaddaden commented 6 months ago

The LLVM IR looks identical to the above crystal code (cast then add), optimized or not (using unchecked math for brevity):

@[NoInline]
def add(a : Int32, other : Int8) : Int32
  a &+ other
end
define i32 @"*add<Int32, Int8>:Int32"(i32 %a, i8 %other) #3 {
entry:
  %0 = sext i8 %other to i32
  %1 = add i32 %a, %0
  ret i32 %1
}

Maybe it's more efficient compile-time wise to always generate the LLVM code in place (inlined), than creating lots of methods to be typed, then inlined :thinking:

HertzDevil commented 2 months ago

LLVM exposes cross-size operations directly

I don't think so: https://llvm.org/docs/LangRef.html#add-instruction

Also there are no pure Crystal equivalents for certain mixed operators, e.g. Int32 + UInt32:

fun __crystal_raise_overflow
  while true
  end
end

x = 1
y = 2_u32
z = x + y
  store i32 1, ptr %x, align 4
  store i32 2, ptr %y, align 4
  %0 = load i32, ptr %x, align 4
  %1 = load i32, ptr %y, align 4
  %2 = sext i32 %0 to i33
  %3 = zext i32 %1 to i33
  %4 = call { i33, i1 } @llvm.sadd.with.overflow.i33(i33 %2, i33 %3)
  %5 = extractvalue { i33, i1 } %4, 0
  %6 = extractvalue { i33, i1 } %4, 1
  %7 = trunc i33 %5 to i32
  %8 = sext i32 %7 to i33
  %9 = icmp ne i33 %5, %8
  %10 = or i1 %6, %9
  %11 = call i1 @llvm.expect.i1(i1 %10, i1 false)
  br i1 %11, label %overflow, label %normal

overflow:                                         ; preds = %entry
  call void @__crystal_raise_overflow()
  unreachable

normal:                                           ; preds = %entry
  %12 = trunc i33 %5 to i32
  store i32 %12, ptr %z, align 4

There are simply no 33-bit integers in Crystal.

ysbaddaden commented 2 months ago

What the heck is that i33 😲

I assume it's to push the sign to bit 33 so we can properly add? I wonder about the generated machine code now.