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

Add compiler support for AVR architecture (Arduino) #14393

Merged
198 changes: 198 additions & 0 deletions spec/std/llvm/avr_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
require "spec"

{% if flag?(:interpreted) && !flag?(:win32) %}
# TODO: figure out how to link against libstdc++ in interpreted code (#14398)
pending LLVM::ABI::AVR
{% skip_file %}
{% end %}

require "llvm"

{% if LibLLVM::BUILT_TARGETS.includes?(:avr) %}
LLVM.init_avr
{% end %}
ysbaddaden marked this conversation as resolved.
Show resolved Hide resolved

private def abi
triple = "avr-unknown-unknown-atmega328p"
target = LLVM::Target.from_triple(triple)
machine = target.create_target_machine(triple)
machine.enable_global_isel = false
LLVM::ABI::AVR.new(machine)
end

private def test(msg, &block : LLVM::ABI, LLVM::Context ->)
it msg do
abi = abi()
ctx = LLVM::Context.new
block.call(abi, ctx)
end
end

class LLVM::ABI
describe AVR do
{% if LibLLVM::BUILT_TARGETS.includes?(:avr) %}
describe "align" do
test "for integer" do |abi, ctx|
abi.align(ctx.int1).should be_a(::Int32)
abi.align(ctx.int1).should eq(1)
abi.align(ctx.int8).should eq(1)
abi.align(ctx.int16).should eq(1)
abi.align(ctx.int32).should eq(1)
abi.align(ctx.int64).should eq(1)
end

test "for pointer" do |abi, ctx|
abi.align(ctx.int8.pointer).should eq(1)
end

test "for float" do |abi, ctx|
abi.align(ctx.float).should eq(1)
end

test "for double" do |abi, ctx|
abi.align(ctx.double).should eq(1)
end

test "for struct" do |abi, ctx|
abi.align(ctx.struct([ctx.int32, ctx.int64])).should eq(1)
abi.align(ctx.struct([ctx.int8, ctx.int16])).should eq(1)
end

test "for packed struct" do |abi, ctx|
abi.align(ctx.struct([ctx.int32, ctx.int64], packed: true)).should eq(1)
end

test "for array" do |abi, ctx|
abi.align(ctx.int16.array(10)).should eq(1)
end
end

describe "size" do
test "for integer" do |abi, ctx|
abi.size(ctx.int1).should be_a(::Int32)
abi.size(ctx.int1).should eq(1)
abi.size(ctx.int8).should eq(1)
abi.size(ctx.int16).should eq(2)
abi.size(ctx.int32).should eq(4)
abi.size(ctx.int64).should eq(8)
end

test "for pointer" do |abi, ctx|
abi.size(ctx.int8.pointer).should eq(2)
end

test "for float" do |abi, ctx|
abi.size(ctx.float).should eq(4)
end

test "for double" do |abi, ctx|
abi.size(ctx.double).should eq(8)
end

test "for struct" do |abi, ctx|
abi.size(ctx.struct([ctx.int32, ctx.int64])).should eq(12)
abi.size(ctx.struct([ctx.int16, ctx.int8])).should eq(3)
abi.size(ctx.struct([ctx.int32, ctx.int8, ctx.int8])).should eq(6)
end

test "for packed struct" do |abi, ctx|
abi.size(ctx.struct([ctx.int32, ctx.int8], packed: true)).should eq(5)
end

test "for array" do |abi, ctx|
abi.size(ctx.int16.array(10)).should eq(20)
end
end

describe "abi_info" do
{% for bits in [1, 8, 16, 32, 64] %}
test "int{{bits}}" do |abi, ctx|
arg_type = ArgType.direct(ctx.int{{bits}})
info = abi.abi_info([ctx.int{{bits}}], ctx.int{{bits}}, true, ctx)
info.arg_types.size.should eq(1)
info.arg_types[0].should eq(arg_type)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.return_type.should eq(arg_type)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end
{% end %}

test "float" do |abi, ctx|
arg_type = ArgType.direct(ctx.float)
info = abi.abi_info([ctx.float], ctx.float, true, ctx)
info.arg_types.size.should eq(1)
info.arg_types[0].should eq(arg_type)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.return_type.should eq(arg_type)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "double" do |abi, ctx|
arg_type = ArgType.direct(ctx.double)
info = abi.abi_info([ctx.double], ctx.double, true, ctx)
info.arg_types.size.should eq(1)
info.arg_types[0].should eq(arg_type)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.return_type.should eq(arg_type)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "multiple arguments" do |abi, ctx|
args = 9.times.map { ctx.int16 }.to_a
info = abi.abi_info(args, ctx.int8, false, ctx)
info.arg_types.size.should eq(9)
info.arg_types.each(&.kind.should eq(LLVM::ABI::ArgKind::Direct))
end

test "multiple arguments above registers" do |abi, ctx|
args = 5.times.map { ctx.int32 }.to_a
info = abi.abi_info(args, ctx.int8, false, ctx)
info.arg_types.size.should eq(5)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[1].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[2].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[3].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[4].kind.should eq(LLVM::ABI::ArgKind::Indirect)
end

test "struct args within 18 bytes" do |abi, ctx|
args = [
ctx.int8, # rounded to 2 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
]
info = abi.abi_info(args, ctx.void, false, ctx)
info.arg_types.size.should eq(3)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[1].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[2].kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "struct args over 18 bytes" do |abi, ctx|
args = [
ctx.int32, # 4 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
ctx.struct([ctx.int32, ctx.int32]), # 8 bytes
]
info = abi.abi_info(args, ctx.void, false, ctx)
info.arg_types.size.should eq(3)
info.arg_types[0].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[1].kind.should eq(LLVM::ABI::ArgKind::Direct)
info.arg_types[2].kind.should eq(LLVM::ABI::ArgKind::Indirect)
end

test "returns struct within 8 bytes" do |abi, ctx|
rty = ctx.struct([ctx.int32, ctx.int32])
info = abi.abi_info([] of Type, rty, true, ctx)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Direct)
end

test "returns struct over 8 bytes" do |abi, ctx|
rty = ctx.struct([ctx.int32, ctx.int32, ctx.int8])
info = abi.abi_info([] of Type, rty, true, ctx)
info.return_type.kind.should eq(LLVM::ABI::ArgKind::Indirect)
end
end
{% end %}
end
end
22 changes: 20 additions & 2 deletions src/compiler/crystal/codegen/target.cr
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ class Crystal::Codegen::Target

def pointer_bit_width
case @architecture
when "x86_64", "aarch64"
when "aarch64", "x86_64"
64
else
when "arm", "i386", "wasm32"
32
when "avr"
16
else
raise "BUG: unknown Target#pointer_bit_width for #{@architecture} target architecture"
end
end

Expand All @@ -64,6 +68,8 @@ class Crystal::Codegen::Target
64
when "arm", "i386", "wasm32"
32
when "avr"
16
else
raise "BUG: unknown Target#size_bit_width for #{@architecture} target architecture"
end
Expand Down Expand Up @@ -93,6 +99,7 @@ class Crystal::Codegen::Target
def executable_extension
case
when windows? then ".exe"
when avr? then ".elf"
else ""
end
end
Expand Down Expand Up @@ -181,6 +188,10 @@ class Crystal::Codegen::Target
environment_parts.any? &.in?("gnueabihf", "musleabihf")
end

def avr?
@architecture == "avr"
end

def to_target_machine(cpu = "", features = "", optimization_mode = Compiler::OptimizationMode::O0,
code_model = LLVM::CodeModel::Default) : LLVM::TargetMachine
case @architecture
Expand All @@ -197,6 +208,13 @@ class Crystal::Codegen::Target
if cpu.empty? && !features.includes?("fp") && armhf?
features += "+vfp2"
end
when "avr"
LLVM.init_avr

if cpu.blank?
# the ABI call convention, codegen and the linker need to known the CPU model
raise Target::Error.new("AVR targets must declare a CPU model, for example --mcpu=atmega328p")
end
when "wasm32"
LLVM.init_webassembly
else
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,13 @@ module Crystal
elsif program.has_flag? "wasm32"
link_flags = @link_flags || ""
{"wasm-ld", %(wasm-ld "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} -lc #{program.lib_flags}), object_names}
elsif program.has_flag? "avr"
link_flags = @link_flags || ""
link_flags += " --target=avr-unknown-unknown -mmcu=#{@mcpu} -Wl,--gc-sections"
{DEFAULT_LINKER, %(#{DEFAULT_LINKER} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names}
else
link_flags = @link_flags || ""
link_flags += " -rdynamic"

{DEFAULT_LINKER, %(#{DEFAULT_LINKER} "${@}" -o #{Process.quote_posix(output_filename)} #{link_flags} #{program.lib_flags}), object_names}
end
end
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/crystal/semantic/flags.cr
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class Crystal::Program

flags.add "bsd" if target.bsd?

if target.avr? && (cpu = target_machine.cpu.presence)
straight-shoota marked this conversation as resolved.
Show resolved Hide resolved
flags.add cpu
end

flags
end
end
74 changes: 43 additions & 31 deletions src/intrinsics.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,57 @@ lib LibIntrinsics
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_debugtrap)] {% end %}
fun debugtrap = "llvm.debugtrap"

{% if flag?(:bits64) %}
{% if flag?(:avr) %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
fun memcpy = "llvm.memcpy.p0i8.p0i8.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memmove = "llvm.memmove.p0i8.p0i8.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memset = "llvm.memset.p0i8.i16"(dest : Void*, val : UInt8, len : UInt16, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
fun memcpy = "llvm.memcpy.p0.p0.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memmove = "llvm.memmove.p0.p0.i16"(dest : Void*, src : Void*, len : UInt16, is_volatile : Bool)
fun memset = "llvm.memset.p0.i16"(dest : Void*, val : UInt8, len : UInt16, is_volatile : Bool)
{% end %}
{% else %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:bits64) %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i64"(dest : Void*, src : Void*, len : UInt64, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i64"(dest : Void*, val : UInt8, len : UInt64, is_volatile : Bool)
{% end %}
{% else %}
{% if compare_versions(Crystal::LLVM_VERSION, "15.0.0") < 0 %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0i8.p0i8.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0i8.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% else %}
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memcpy)] {% end %}
fun memcpy = "llvm.memcpy.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memmove)] {% end %}
fun memmove = "llvm.memmove.p0.p0.i32"(dest : Void*, src : Void*, len : UInt32, is_volatile : Bool)

{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% if flag?(:interpreted) %} @[Primitive(:interpreter_intrinsics_memset)] {% end %}
fun memset = "llvm.memset.p0.i32"(dest : Void*, val : UInt8, len : UInt32, is_volatile : Bool)
{% end %}
{% end %}
{% end %}

Expand Down
16 changes: 16 additions & 0 deletions src/llvm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ module LLVM
{% end %}
end

def self.init_avr : Nil
return if @@initialized_avr
@@initialized_avr = true

{% if LibLLVM::BUILT_TARGETS.includes?(:avr) %}
LibLLVM.initialize_avr_target_info
LibLLVM.initialize_avr_target
LibLLVM.initialize_avr_target_mc
LibLLVM.initialize_avr_asm_printer
LibLLVM.initialize_avr_asm_parser
LibLLVM.link_in_mc_jit
{% else %}
raise "ERROR: LLVM was built without AVR target"
{% end %}
end

def self.init_webassembly : Nil
return if @@initialized_webassembly
@@initialized_webassembly = true
Expand Down
Loading
Loading