Skip to content
50 changes: 50 additions & 0 deletions spec/compiler/codegen/target_annotation_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require "../spec_helper"

describe "Code gen: TargetFeature annotation" do
it "can target optional CPU features" do
{% if compare_versions(Crystal::LLVM_VERSION, "13.0.0") < 0 %} pending! "requires LLVM 13+" {% end %}

compile(<<-CRYSTAL, target: "aarch64-darwin")
@[TargetFeature("+sve,+sve2")]
def sve2_smoke_test : Nil
asm("ext z0.b, { z1.b, z2.b }, #0" :::: "volatile")
nil
end

sve2_smoke_test
CRYSTAL
end

it "can optimize code for a specific CPU" do
{% if compare_versions(Crystal::LLVM_VERSION, "13.0.0") < 0 %} pending! "requires LLVM 13+" {% end %}

# unlike the ARM backend, the X86 backend doesn't validate assembly
# instructions, but the LLVM intrinsics are validated so we use one
compile(<<-CRYSTAL, target: "x86_64-linux-gnu")
lib LibIntrinsics
fun x86_avx_vzeroall = "llvm.x86.avx.vzeroall"
end

@[TargetFeature(cpu: "x86-64-v3")]
def x86_vzeroall : Nil
LibIntrinsics.x86_avx_vzeroall
end

x86_vzeroall
CRYSTAL
end

it "can target optional CPU features and optimize code for a specific CPU" do
{% if compare_versions(Crystal::LLVM_VERSION, "13.0.0") < 0 %} pending! "requires LLVM 13+" {% end %}

compile(<<-CRYSTAL, target: "aarch64-darwin")
@[TargetFeature("+sve,+sve2", cpu: "apple-m1")]
def sve2_smoke_test : Nil
asm("ext z0.b, { z1.b, z2.b }, #0" :::: "volatile")
nil
end

sve2_smoke_test
CRYSTAL
end
end
63 changes: 63 additions & 0 deletions spec/compiler/semantic/target_annotation_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require "../spec_helper"

describe "Semantic: TargetFeature annotation" do
it "errors if invalid argument provided" do
assert_error <<-CRYSTAL, "no argument named 'invalid', expected 'cpu'"
@[TargetFeature(invalid: "lorem ipsum")]
def foo
end
CRYSTAL
end

it "errors if invalid cpu argument type provided" do
assert_error <<-CRYSTAL, "expected argument 'cpu' to be String"
@[TargetFeature(cpu: 3)]
def foo
end
CRYSTAL
end

it "errors if invalid cpu argument type provided and feature provided" do
assert_error <<-CRYSTAL, "expected argument 'cpu' to be String"
@[TargetFeature("+sve", cpu: 4)]
def foo
end
CRYSTAL
end

it "errors if invalid feature argument type provided" do
assert_error <<-CRYSTAL, "expected argument #1 to 'TargetFeature' to be String"
@[TargetFeature(3)]
def foo
end
CRYSTAL
end

it "errors if invalid feature argument type provided and cpu provided" do
assert_error <<-CRYSTAL, "expected argument #1 to 'TargetFeature' to be String"
@[TargetFeature(3, cpu: "apple-m1")]
def foo
end
CRYSTAL
end

it "errors if wrong number of arguments provided" do
assert_error <<-CRYSTAL, "wrong number of arguments for TargetFeature (given 2, expected 0..1)"
@[TargetFeature("+sve", "+sve2")]
def foo
end
CRYSTAL
end

it "can target a specific LLVM supported feature" do
assert_type(<<-CRYSTAL) { int32 }
# This feature is available on all platforms
@[TargetFeature("+strict-align")]
def strict_align(input : Int32) : Int32
input
end

strict_align(1)
CRYSTAL
end
end
15 changes: 12 additions & 3 deletions src/compiler/crystal/codegen/fun.cr
Original file line number Diff line number Diff line change
Expand Up @@ -451,16 +451,25 @@ class Crystal::CodeGenVisitor
context.fun = typed_fun.func
context.fun_type = typed_fun.type

if @debug.variables?
optimize_none = @debug.variables?
features = target_def.target_features
cpu = target_def.target_cpu

if optimize_none || features || cpu
# Make it a hard boundary, disabling inlining
context.fun.add_attribute LLVM::Attribute::NoInline
context.fun.add_attribute LLVM::Attribute::OptimizeNone

context.fun.add_attribute LLVM::Attribute::OptimizeNone if optimize_none
context.fun.add_target_dependent_attribute("target-features", features) if features
context.fun.add_target_dependent_attribute("target-cpu", cpu) if cpu
else
context.fun.add_attribute LLVM::Attribute::AlwaysInline if target_def.always_inline?
context.fun.add_attribute LLVM::Attribute::NoInline if target_def.no_inline?
end

context.fun.add_attribute LLVM::Attribute::ReturnsTwice if target_def.returns_twice?
context.fun.add_attribute LLVM::Attribute::Naked if target_def.naked?
context.fun.add_attribute LLVM::Attribute::NoReturn if target_def.no_returns?
context.fun.add_attribute LLVM::Attribute::NoInline if target_def.no_inline?
end

def setup_closure_vars(target_def, closure_vars, context, closure_type, closure_ptr)
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/crystal/program.cr
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ module Crystal
types["ThreadLocal"] = @thread_local_annotation = AnnotationType.new self, self, "ThreadLocal"
types["Deprecated"] = @deprecated_annotation = AnnotationType.new self, self, "Deprecated"
types["Experimental"] = @experimental_annotation = AnnotationType.new self, self, "Experimental"
types["TargetFeature"] = @target_feature_annotation = AnnotationType.new self, self, "TargetFeature"

define_crystal_constants

Expand Down Expand Up @@ -562,7 +563,7 @@ module Crystal
{% for name in %w(object no_return value number reference void nil bool char int int8 int16 int32 int64 int128
uint8 uint16 uint32 uint64 uint128 float float32 float64 string symbol pointer enumerable indexable
array static_array exception tuple named_tuple proc union enum range slice regex crystal
packed_annotation thread_local_annotation no_inline_annotation
packed_annotation thread_local_annotation no_inline_annotation target_feature_annotation
always_inline_annotation naked_annotation returns_twice_annotation
raises_annotation primitive_annotation call_convention_annotation
flags_annotation link_annotation extern_annotation deprecated_annotation experimental_annotation) %}
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/crystal/semantic/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ module Crystal
# `true` if this def has the `@[ReturnsTwice]` annotation
property? returns_twice = false

# Set to the value of the `@[TargetFeature("+sve,+sve2")]` annotation
property target_features : String? = nil

# Set to the value of the `@[TargetFeature(cpu: "apple-m4")]` annotation
property target_cpu : String? = nil

# `true` if this def has the `@[Naked]` annotation
property? naked = false

Expand Down Expand Up @@ -222,6 +228,8 @@ module Crystal
a_def.no_inline = no_inline?
a_def.always_inline = always_inline?
a_def.returns_twice = returns_twice?
a_def.target_features = target_features
a_def.target_cpu = target_cpu
a_def.naked = naked?
a_def.annotations = annotations
a_def.new = new?
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/crystal/semantic/top_level_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,24 @@ class Crystal::TopLevelVisitor < Crystal::SemanticVisitor
node.returns_twice = true
when @program.raises_annotation
node.raises = true
when @program.target_feature_annotation
ann.named_args.try &.each do |named_arg|
case named_arg.name
when "cpu"
cpu_value = named_arg.value
named_arg.raise "expected argument 'cpu' to be String" unless cpu_value.is_a?(StringLiteral)
node.target_cpu = cpu_value.value
else
named_arg.raise "no argument named '#{named_arg.name}', expected 'cpu'"
end
end

if ann.args.size > 0
ann.raise "wrong number of arguments for TargetFeature (given #{ann.args.size}, expected 0..1)" if ann.args.size > 1
features_value = ann.args[0]
ann.raise "expected argument #1 to 'TargetFeature' to be String" unless features_value.is_a?(StringLiteral)
node.target_features = features_value.value
end
else
yield annotation_type, ann
end
Expand Down
Loading