From e3585951255dfedcf1771dfc32c58d731c47bbdb Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Fri, 28 Feb 2025 00:09:28 +0800 Subject: [PATCH] Support `Slice.literal` with inferred element type --- spec/compiler/semantic/primitives_spec.cr | 58 +++++++++++ spec/primitives/slice_spec.cr | 9 ++ spec/support/number.cr | 14 +++ src/compiler/crystal/semantic/main_visitor.cr | 96 +++++++++++-------- src/primitives.cr | 11 ++- 5 files changed, 145 insertions(+), 43 deletions(-) diff --git a/spec/compiler/semantic/primitives_spec.cr b/spec/compiler/semantic/primitives_spec.cr index 35bd16938005..34b8b3a29501 100644 --- a/spec/compiler/semantic/primitives_spec.cr +++ b/spec/compiler/semantic/primitives_spec.cr @@ -266,6 +266,64 @@ describe "Semantic: primitives" do end CRYSTAL + context "without element type" do + it "types primitive int literal" do + assert_type(<<-CRYSTAL) { generic_class "Slice", int32 } + #{def_slice_literal} + Slice.literal(0, 1, 4, 9) + CRYSTAL + + assert_type(<<-CRYSTAL) { generic_class "Slice", uint8 } + #{def_slice_literal} + Slice.literal(1_u8, 2_u8) + CRYSTAL + end + + it "types primitive float literal" do + assert_type(<<-CRYSTAL) { generic_class "Slice", float64 } + #{def_slice_literal} + Slice.literal(1.2, 3.4) + CRYSTAL + + assert_type(<<-CRYSTAL) { generic_class "Slice", float32 } + #{def_slice_literal} + Slice.literal(5.6_f32) + CRYSTAL + end + + it "errors if empty" do + assert_error <<-CRYSTAL, "Cannot create empty slice literal without element type" + #{def_slice_literal} + Slice.literal + CRYSTAL + end + + it "errors if multiple element types are found" do + assert_error <<-CRYSTAL, "Too many element types for slice literal without generic argument: Int32, UInt8" + #{def_slice_literal} + Slice.literal(1, 2_u8) + CRYSTAL + + assert_error <<-CRYSTAL, "Too many element types for slice literal without generic argument: Float32, Float64" + #{def_slice_literal} + Slice.literal(3.0f32, 4.0) + CRYSTAL + end + + it "errors if element is not number literal" do + assert_error <<-CRYSTAL, "Expected NumberLiteral, got StringLiteral" + #{def_slice_literal} + Slice.literal("") + CRYSTAL + + assert_error <<-CRYSTAL, "Expected NumberLiteral, got Var" + #{def_slice_literal} + x = 1 + Slice.literal(x) + CRYSTAL + end + end + context "with element type" do it "types primitive int literal" do assert_type(<<-CRYSTAL) { generic_class "Slice", uint8 } diff --git a/spec/primitives/slice_spec.cr b/spec/primitives/slice_spec.cr index 98bea774df8b..a5d21e87a712 100644 --- a/spec/primitives/slice_spec.cr +++ b/spec/primitives/slice_spec.cr @@ -20,5 +20,14 @@ describe "Primitives: Slice" do slice1.should eq(slice2) end {% end %} + + {% for num, suffix in BUILTIN_NUMBER_SUFFIXES %} + pending_interpreted {{ "creates a read-only Slice of #{num}" }} do + slice = Slice.literal(1_{{ suffix.id }}, 2_{{ suffix.id }}, 3_{{ suffix.id }}) + slice.should be_a(Slice({{ num }})) + slice.to_a.should eq([1, 2, 3] of {{ num }}) + slice.read_only?.should be_true + end + {% end %} end end diff --git a/spec/support/number.cr b/spec/support/number.cr index 404d2bd32438..244f2684ae18 100644 --- a/spec/support/number.cr +++ b/spec/support/number.cr @@ -14,6 +14,20 @@ BUILTIN_NUMBER_TYPES_LTE_64 = [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32, Float64] BUILTIN_FLOAT_TYPES = [Float32, Float64] +BUILTIN_NUMBER_SUFFIXES = { + Int8 => "i8", + Int16 => "i16", + Int32 => "i32", + Int64 => "i64", + Int128 => "i128", + UInt8 => "u8", + UInt16 => "u16", + UInt32 => "u32", + UInt64 => "u64", + UInt128 => "u128", + Float32 => "f32", + Float64 => "f64", +} macro it_can_convert_between(a_types, b_types) {% for a_type in a_types %} diff --git a/src/compiler/crystal/semantic/main_visitor.cr b/src/compiler/crystal/semantic/main_visitor.cr index efd76f76f056..8e20c4fcb676 100644 --- a/src/compiler/crystal/semantic/main_visitor.cr +++ b/src/compiler/crystal/semantic/main_visitor.cr @@ -1574,55 +1574,71 @@ module Crystal def check_slice_literal_call(node, obj_type) return false unless obj_type return false unless obj_type.metaclass? + return false unless node.name == "literal" instance_type = obj_type.instance_type.remove_typedef - if node.name == "literal" - case instance_type - when GenericClassType # Slice - return false unless instance_type == @program.slice - node.raise "TODO: implement slice_literal primitive for Slice without generic arguments" - when GenericClassInstanceType # Slice(T) - return false unless instance_type.generic_type == @program.slice - - element_type = instance_type.type_vars["T"].type - kind = case element_type - when IntegerType - element_type.kind - when FloatType - element_type.kind - else - node.raise "Only slice literals of primitive integer or float types can be created" - end + case instance_type + when GenericClassType # Slice + return false unless instance_type == @program.slice - node.args.each do |arg| - arg.raise "Expected NumberLiteral, got #{arg.class_desc}" unless arg.is_a?(NumberLiteral) - arg.accept self - arg.raise "Argument out of range for a Slice(#{element_type})" unless arg.representable_in?(element_type) + element_type = nil + + node.args.each do |arg| + arg.raise "Expected NumberLiteral, got #{arg.class_desc}" unless arg.is_a?(NumberLiteral) + arg.accept self + element_type ||= arg.type + unless element_type == arg.type + arg.raise "Too many element types for slice literal without generic argument: #{element_type}, #{arg.type}" end + 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, node.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, node.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(node.args.size.to_s, :i32).at(node) - read_only_node = NamedArgument.new("read_only", BoolLiteral.new(true).at(node)).at(node) - expanded = Call.new(Path.global("Slice").at(node), "new", [pointer_node, size_node], named_args: [read_only_node]).at(node) - - expanded.accept self - node.bind_to expanded - node.expanded = expanded - return true + unless element_type + node.raise "Cannot create empty slice literal without element type" + end + when GenericClassInstanceType # Slice(T) + return false unless instance_type.generic_type == @program.slice + + element_type = instance_type.type_vars["T"].type + + node.args.each do |arg| + arg.raise "Expected NumberLiteral, got #{arg.class_desc}" unless arg.is_a?(NumberLiteral) + arg.accept self + arg.raise "Argument out of range for a Slice(#{element_type})" unless arg.representable_in?(element_type) end + else + return false end - false + kind = + case element_type + when IntegerType + element_type.kind + when FloatType + element_type.kind + else + node.raise "Only slice literals of primitive integer or float types can be created" + 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, node.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, node.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(node.args.size.to_s, :i32).at(node) + read_only_node = NamedArgument.new("read_only", BoolLiteral.new(true).at(node)).at(node) + expanded = Call.new(Path.global("Slice").at(node), "new", [pointer_node, size_node], named_args: [read_only_node]).at(node) + + expanded.accept self + node.bind_to expanded + node.expanded = expanded + + true end # Rewrite: diff --git a/src/primitives.cr b/src/primitives.cr index e033becdfbd2..4f93fac83af3 100644 --- a/src/primitives.cr +++ b/src/primitives.cr @@ -337,14 +337,19 @@ 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`. + # If `T` is specified, it must be one of the `Number::Primitive` types and + # cannot be a union. The *args* must all be number literals that fit into + # `T`'s range, as if they are autocasted into `T`. + # + # If `T` is not specified, it is inferred from *args*, which must all be + # number literals of the same type, and cannot be empty. # # ``` # x = Slice(UInt8).literal(0, 1, 4, 9, 16, 25) # x # => Slice[0, 1, 4, 9, 16, 25] # x.read_only? # => true + # + # Slice.literal(1_u8, 2_u8, 3_u8) # => Bytes[1, 2, 3] # ``` @[Experimental("Slice literals are still under development. Join the discussion at [#2886](https://github.com/crystal-lang/crystal/issues/2886).")] @[Primitive(:slice_literal)]