Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows Nightly Regression #14285

Closed
Blacksmoke16 opened this issue Feb 5, 2024 · 3 comments · Fixed by #14289
Closed

Windows Nightly Regression #14285

Blacksmoke16 opened this issue Feb 5, 2024 · 3 comments · Fixed by #14289
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. platform:x86 topic:compiler:codegen

Comments

@Blacksmoke16
Copy link
Member

Will try to reduce more, but this is what I was able to come up with so far:

class Constraints::Range
  getter min : Number::Primitive | Time | Nil
  getter max : Number::Primitive | Time | Nil

  def self.new(
    range : ::Range
  )
    new range.begin, range.end
  end

  private def initialize(
    @min : Number::Primitive | Time | Nil,
    @max : Number::Primitive | Time | Nil
  )
  end

  class Validator
    def validate(value : Number | Time | Nil, constraint : Constraints::Range) : Nil
      return if value.nil?

      min = constraint.min
      max = constraint.max

      case {value, min, max}
      when {Time, Time?, Time?}
        pp! value
        pp! value.to_s

        return self.add_not_in_range_violation constraint, value, min, max if min && max && (value < min || value > max)
      end
    end

    private def add_not_in_range_violation(constraint, value, min, max) : Nil
      pp! value.to_s
    end
  end
end

Constraints::Range::Validator.new.validate Time.utc(2020, 4, 15), Constraints::Range.new(Time.utc(2020, 4, 10)..Time.utc(2020, 4, 20))

Previous nightly version:

$ ~/Downloads/crystal-prev/crystal --version
Crystal 1.12.0-dev [592051e] (2024-02-02)

The compiler was not built in release mode.

LLVM: 17.0.2
Default target: x86_64-pc-windows-msvc

$ ~/Downloads/crystal-prev/crystal test.cr
value # => 2020-04-15 00:00:00.0 UTC
value.to_s # => "2020-04-15 00:00:00 UTC"

Latest nightly version:

$ ~/Downloads/crystal/crystal --version
Crystal 1.12.0-dev [c67883c] (2024-02-04)

The compiler was not built in release mode.

LLVM: 18.1.0
Default target: x86_64-pc-windows-msvc

$ ~/Downloads/crystal/crystal test.cr
value # => 2020-04-15 00:00:00.0 UTC
value.to_s # => "2020-04-15 00:00:00 UTC"
value.to_s # => "0001-01-01 00:00:00 UTC

Seemingly is related to #14277 🤷.

@Blacksmoke16 Blacksmoke16 added kind:bug A bug in the code. Does not apply to documentation, specs, etc. platform:windows Windows support based on the MSVC toolchain / Win32 API labels Feb 5, 2024
@HertzDevil HertzDevil added topic:compiler:codegen and removed platform:windows Windows support based on the MSVC toolchain / Win32 API labels Feb 5, 2024
@HertzDevil
Copy link
Contributor

HertzDevil commented Feb 5, 2024

Reduced:

struct Time2
  def initialize(@seconds : Int64)
    @nanoseconds = uninitialized UInt32[3]
  end

  def <(other : Time2) : Bool
    @seconds < other.@seconds
  end
end

class Constraints::Range
  def initialize(@min : Int128 | Time2 | Nil)
  end
end

def validate(value : Time2, constraint) : Nil
  min = constraint.@min

  if min.is_a?(Time2?)
    if min
      if value < min
        LibC.printf("foo\n")
      end
    end
  end
end

value = Time2.new(123)
constraint = Constraints::Range.new(Time2.new(45))
validate(value, constraint)

This generates the following fragment for the #< call:

then2:                                            ; preds = %then
  %25 = getelementptr inbounds %"(Int128 | Time2 | Nil)", ptr %min, i32 0, i32 0
  %26 = load i32, ptr %25, align 4
  %27 = getelementptr inbounds %"(Int128 | Time2 | Nil)", ptr %min, i32 0, i32 1
  %28 = load %Time2, ptr %27, align 8 ; underaligned load occurs here
  %29 = call i1 @"*Time2#<<Time2>:Bool"(ptr %value1, %Time2 %28)
  br i1 %29, label %then4, label %else5

It looks like all loads that go through union_type_and_value_pointer must be aligned.

@HertzDevil
Copy link
Contributor

HertzDevil commented Feb 5, 2024

The problem is that the getelementptr and the load instructions are not necessarily emitted in the same place. For example, the following part of the compiler handles the snippet above:

request_value(arg)
if arg.type.void?
call_arg = int8(0)
else
call_arg = @last
call_arg = llvm_nil if arg.type.nil_type?
call_arg = downcast(call_arg, def_arg.type, arg.type, true)
end
# - C calling convention passing needs a separate handling of pass-by-value
# - Primitives might need a separate handling (for example invoking a Proc)
if arg.type.passed_by_value? && !c_calling_convention && !is_primitive
call_arg = load(llvm_type(arg.type), call_arg)
end

%27 is emitted by the request_value call on line 85 (which calls CodeGenVisitor#visit(Var) etc.), but %28 is emitted by the load on line 98, so we have to somehow carry this alignment information across those lines, otherwise %28 will pick up the alignment from alignof(Time2) automatically. Yet only the relevant LLVM instructions have alignment, not LLVM types, for reasons similar to the recent transition to opaque pointers.

It was mostly coincidence that underaligned operations for smaller sizes were working before on x86-64. With actual 16-byte-aligned types this is not the case anymore.

@HertzDevil
Copy link
Contributor

Changing the alignments in the emitted LLVM IR manually did not help. Instead it looks like the problem is with how the new memcpy in #14279 always picks the size of the larger union, regardless of whether an upcast or downcast is performed:

if min.is_a?(Time2?)
  if min # this downcast
    # ...
%"(Time2 | Nil)" = type { i32, [3 x i64] }

%11 = getelementptr inbounds %"(Int128 | Time2 | Nil)", ptr %min, i32 0, i32 0
%12 = load i32, ptr %11, align 4
%14 = getelementptr inbounds %"(Time2 | Nil)", ptr %0, i32 0, i32 0
store i32 %12, ptr %14, align 4
%15 = getelementptr inbounds %"(Time2 | Nil)", ptr %0, i32 0, i32 1
%16 = getelementptr inbounds %"(Int128 | Time2 | Nil)", ptr %min, i32 0, i32 1
call void @llvm.memcpy.p0.p0.i64(ptr align 8 %15, ptr align 16 %16, i64 32, i1 false) ; this 32

So we are corrupting the stack by copying 32 bytes to the data field of a Time2 | Nil, which only measures 24 bytes. Using the less size among the two unions seems to fix the issue.

We probably don't need to touch the load / store alignments for now. Maybe LLVM cannot prove that some align 16 values are always satisfied...?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. platform:x86 topic:compiler:codegen
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants