Skip to content

Commit

Permalink
Experimental: Add Slice.literal for numeric slice constants (crysta…
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored and Blacksmoke16 committed Dec 11, 2023
1 parent f8d841c commit beff32b
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 0 deletions.
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

0 comments on commit beff32b

Please sign in to comment.