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

Experimental: Add Slice.literal for numeric slice constants #13716

Merged
merged 3 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions spec/compiler/semantic/primitives_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions spec/primitives/slice_spec.cr
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/crystal/program.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
47 changes: 47 additions & 0 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 primitive 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)

Expand Down
19 changes: 19 additions & 0 deletions src/primitives.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down