From c77e478b7dcb5238ab251fc7f08f58c7e07b33ae Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sun, 30 Jul 2023 03:45:59 +0800 Subject: [PATCH 1/2] Experimental: Add `Slice.literal` --- src/compiler/crystal/codegen/codegen.cr | 32 +++++++++++++ src/compiler/crystal/program.cr | 8 ++++ src/compiler/crystal/semantic/main_visitor.cr | 47 +++++++++++++++++++ src/primitives.cr | 19 ++++++++ 4 files changed, 106 insertions(+) diff --git a/src/compiler/crystal/codegen/codegen.cr b/src/compiler/crystal/codegen/codegen.cr index 2c357183ffa5..9890454ecfef 100644 --- a/src/compiler/crystal/codegen/codegen.cr +++ b/src/compiler/crystal/codegen/codegen.cr @@ -234,6 +234,10 @@ module Crystal symbol_table.initializer = llvm_type(@program.string).const_array(@symbol_table_values) end + program.const_slices.each do |info| + define_slice_constant(info) + end + @last = llvm_nil @fun_literal_count = 0 @@ -293,6 +297,34 @@ module Crystal llvm_mod.globals.add llvm_typer.llvm_type(@program.string).array(@symbol_table_values.size), SYMBOL_TABLE_NAME end + def define_slice_constant(info : Program::ConstSliceInfo) + args = info.args.to_unsafe + kind = info.element_type + llvm_element_type = llvm_type(@program.type_from_literal_kind(kind)) + llvm_elements = Array.new(info.args.size) do |i| + num = args[i].as(NumberLiteral) + case kind + in .i8? then llvm_element_type.const_int(num.value.to_i8) + in .i16? then llvm_element_type.const_int(num.value.to_i16) + in .i32? then llvm_element_type.const_int(num.value.to_i32) + in .i64? then llvm_element_type.const_int(num.value.to_i64) + in .i128? then llvm_element_type.const_int(num.value.to_i128) + in .u8? then llvm_element_type.const_int(num.value.to_u8) + in .u16? then llvm_element_type.const_int(num.value.to_u16) + in .u32? then llvm_element_type.const_int(num.value.to_u32) + in .u64? then llvm_element_type.const_int(num.value.to_u64) + in .u128? then llvm_element_type.const_int(num.value.to_u128) + in .f32? then llvm_element_type.const_float(num.value) + in .f64? then llvm_element_type.const_double(num.value) + end + end + + global = @llvm_mod.globals.add(llvm_element_type.array(info.args.size), info.name) + global.linkage = LLVM::Linkage::Private + global.global_constant = true + global.initializer = llvm_element_type.const_array(llvm_elements) + end + def data_layout @program.target_machine.data_layout end diff --git a/src/compiler/crystal/program.cr b/src/compiler/crystal/program.cr index 3fa7aa37f45a..d1bf702d70b1 100644 --- a/src/compiler/crystal/program.cr +++ b/src/compiler/crystal/program.cr @@ -84,6 +84,14 @@ module Crystal # This pool is passed to the parser, macro expander, etc. getter string_pool = StringPool.new + record ConstSliceInfo, + name : String, + element_type : NumberKind, + args : Array(ASTNode) + + # All constant slices constructed via the `Slice.literal` primitive. + getter const_slices = [] of ConstSliceInfo + # Here we store constants, in the # order that they are used. They will be initialized as soon # as the program starts, before the main code. diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index 2f84fea5d81c..10dc2cf9597b 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -2298,6 +2298,8 @@ module Crystal visit_pointer_set node when "pointer_new" visit_pointer_new node + when "slice_literal" + visit_slice_literal node when "argc" # Already typed when "argv" @@ -2417,6 +2419,51 @@ module Crystal node.type = scope.instance_type end + def visit_slice_literal(node) + call = self.call.not_nil! + + case slice_type = scope.instance_type + when GenericClassType # Slice + call.raise "TODO: implement slice_literal primitive for Slice without generic arguments" + when GenericClassInstanceType # Slice(T) + element_type = slice_type.type_vars["T"].type + kind = case element_type + when IntegerType + element_type.kind + when FloatType + element_type.kind + else + call.raise "Only slice literals of integer or float types can be created" + end + + call.args.each do |arg| + arg.raise "Expected NumberLiteral, got #{arg.class_desc}" unless arg.is_a?(NumberLiteral) + arg.raise "Argument out of range for a Slice(#{element_type})" unless arg.representable_in?(element_type) + end + + # create the internal constant `$Slice:n` to hold the slice contents + const_name = "$Slice:#{@program.const_slices.size}" + const_value = Nop.new + const_value.type = @program.static_array_of(element_type, call.args.size) + const = Const.new(@program, @program, const_name, const_value) + @program.types[const_name] = const + @program.const_slices << Program::ConstSliceInfo.new(const_name, kind, call.args) + + # ::Slice.new(pointerof($Slice:n.@buffer), {{ args.size }}, read_only: true) + pointer_node = PointerOf.new(ReadInstanceVar.new(Path.new(const_name).at(node), "@buffer").at(node)).at(node) + size_node = NumberLiteral.new(call.args.size.to_s, :i32).at(node) + read_only_node = NamedArgument.new("read_only", BoolLiteral.new(true).at(node)).at(node) + extra = Call.new(Path.global("Slice").at(node), "new", [pointer_node, size_node], named_args: [read_only_node]).at(node) + + extra.accept self + node.extra = extra + node.type = slice_type + call.expanded = extra + else + node.raise "BUG: Unknown scope for slice_literal primitive" + end + end + def visit_struct_or_union_set(node) scope = @scope.as(NonGenericClassType) diff --git a/src/primitives.cr b/src/primitives.cr index bb64121da705..e01be8884dbb 100644 --- a/src/primitives.cr +++ b/src/primitives.cr @@ -254,6 +254,25 @@ struct Pointer(T) end end +struct Slice(T) + # Constructs a read-only `Slice` constant from the given *args*. The slice + # contents are stored in the program's read-only data section. + # + # `T` must be one of the `Number::Primitive` types and cannot be a union. It + # also cannot be inferred. The *args* must all be number literals that fit + # into `T`'s range, as if they are autocasted into `T`. + # + # ``` + # x = Slice(UInt8).literal(0, 1, 4, 9, 16, 25) + # x # => Slice[0, 1, 4, 9, 16, 25] + # x.read_only? # => true + # ``` + @[Experimental("Slice literals are still under development. Join the discussion at [#2886](https://github.com/crystal-lang/crystal/issues/2886).")] + @[Primitive(:slice_literal)] + def self.literal(*args) + end +end + struct Proc # Invokes this `Proc` and returns the result. # From 8c91ae268133d4738ebcbcebe13b07089019c73b Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 20 Sep 2023 21:58:20 +0800 Subject: [PATCH 2/2] add specs --- spec/compiler/semantic/primitives_spec.cr | 78 +++++++++++++++++++ spec/primitives/slice_spec.cr | 15 ++++ src/compiler/crystal/semantic/main_visitor.cr | 2 +- 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 spec/primitives/slice_spec.cr diff --git a/spec/compiler/semantic/primitives_spec.cr b/spec/compiler/semantic/primitives_spec.cr index 485fc80d3a58..b806361dc01d 100644 --- a/spec/compiler/semantic/primitives_spec.cr +++ b/spec/compiler/semantic/primitives_spec.cr @@ -253,4 +253,82 @@ describe "Semantic: primitives" do Bar::A.new.foo CRYSTAL end + + describe "Slice.literal" do + def_slice_literal = <<-CRYSTAL + struct Slice(T) + def initialize(pointer : T*, size : Int32, *, read_only : Bool) + end + + @[Primitive(:slice_literal)] + def self.literal(*args) + end + end + CRYSTAL + + context "with element type" do + it "types primitive int literal" do + assert_type(<<-CRYSTAL) { generic_class "Slice", uint8 } + #{def_slice_literal} + Slice(UInt8).literal(0, 1, 4, 9) + CRYSTAL + end + + it "types primitive float literal" do + assert_type(<<-CRYSTAL) { generic_class "Slice", float64 } + #{def_slice_literal} + Slice(Float64).literal(0, 1, 4, 9) + CRYSTAL + end + + it "types empty literal" do + assert_type(<<-CRYSTAL) { generic_class "Slice", int32 } + #{def_slice_literal} + Slice(Int32).literal + CRYSTAL + end + + it "errors if element type is not primitive int or float" do + assert_error <<-CRYSTAL, "Only slice literals of primitive integer or float types can be created" + #{def_slice_literal} + Slice(String).literal + CRYSTAL + + assert_error <<-CRYSTAL, "Only slice literals of primitive integer or float types can be created" + #{def_slice_literal} + Slice(Bool).literal + CRYSTAL + + assert_error <<-CRYSTAL, "Only slice literals of primitive integer or float types can be created" + #{def_slice_literal} + Slice(Int32 | Int64).literal + CRYSTAL + end + + it "errors if element is not number literal" do + assert_error <<-CRYSTAL, "Expected NumberLiteral, got StringLiteral" + #{def_slice_literal} + Slice(Int32).literal("") + CRYSTAL + + assert_error <<-CRYSTAL, "Expected NumberLiteral, got Var" + #{def_slice_literal} + x = 1 + Slice(Int32).literal(x) + CRYSTAL + end + + it "errors if element is out of range" do + assert_error <<-CRYSTAL, "Argument out of range for a Slice(UInt8)" + #{def_slice_literal} + Slice(UInt8).literal(-1) + CRYSTAL + + assert_error <<-CRYSTAL, "Argument out of range for a Slice(UInt8)" + #{def_slice_literal} + Slice(UInt8).literal(256) + CRYSTAL + end + end + end end diff --git a/spec/primitives/slice_spec.cr b/spec/primitives/slice_spec.cr new file mode 100644 index 000000000000..8e440d2ff905 --- /dev/null +++ b/spec/primitives/slice_spec.cr @@ -0,0 +1,15 @@ +require "spec" +require "../support/number" + +describe "Primitives: Slice" do + describe ".literal" do + {% for num in BUILTIN_NUMBER_TYPES %} + it {{ "creates a read-only Slice(#{num})" }} do + slice = Slice({{ num }}).literal(0, 1, 4, 9, 16, 25) + slice.should be_a(Slice({{ num }})) + slice.to_a.should eq([0, 1, 4, 9, 16, 25] of {{ num }}) + slice.read_only?.should be_true + end + {% end %} + end +end diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index 10dc2cf9597b..712d9e8d72cf 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -2433,7 +2433,7 @@ module Crystal when FloatType element_type.kind else - call.raise "Only slice literals of integer or float types can be created" + call.raise "Only slice literals of primitive integer or float types can be created" end call.args.each do |arg|