diff --git a/spec/compiler/codegen/target_annotation_spec.cr b/spec/compiler/codegen/target_annotation_spec.cr new file mode 100644 index 000000000000..6376b412aca1 --- /dev/null +++ b/spec/compiler/codegen/target_annotation_spec.cr @@ -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 diff --git a/spec/compiler/semantic/target_annotation_spec.cr b/spec/compiler/semantic/target_annotation_spec.cr new file mode 100644 index 000000000000..7581a356bf22 --- /dev/null +++ b/spec/compiler/semantic/target_annotation_spec.cr @@ -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 diff --git a/src/compiler/crystal/codegen/fun.cr b/src/compiler/crystal/codegen/fun.cr index b3d122bf8f92..4e0943047fad 100644 --- a/src/compiler/crystal/codegen/fun.cr +++ b/src/compiler/crystal/codegen/fun.cr @@ -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) diff --git a/src/compiler/crystal/program.cr b/src/compiler/crystal/program.cr index 3e4e2ab6fb09..f1afb2fc6ebf 100644 --- a/src/compiler/crystal/program.cr +++ b/src/compiler/crystal/program.cr @@ -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 @@ -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) %} diff --git a/src/compiler/crystal/semantic/ast.cr b/src/compiler/crystal/semantic/ast.cr index ca5905ef223e..fedeae5f3fd5 100644 --- a/src/compiler/crystal/semantic/ast.cr +++ b/src/compiler/crystal/semantic/ast.cr @@ -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 @@ -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? diff --git a/src/compiler/crystal/semantic/top_level_visitor.cr b/src/compiler/crystal/semantic/top_level_visitor.cr index 17a29ee2ba93..814242ac8b70 100644 --- a/src/compiler/crystal/semantic/top_level_visitor.cr +++ b/src/compiler/crystal/semantic/top_level_visitor.cr @@ -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