Module: CrystalRuby::Types

Defined in:
lib/crystalruby/types.rb,
lib/crystalruby/types/type.rb,
lib/crystalruby/types/primitive.rb,
lib/crystalruby/types/fixed_width.rb,
lib/crystalruby/types/variable_width.rb,
lib/crystalruby/types/fixed_width/proc.rb,
lib/crystalruby/types/fixed_width/tuple.rb,
lib/crystalruby/types/concerns/allocator.rb,
lib/crystalruby/types/primitive_types/nil.rb,
lib/crystalruby/types/variable_width/hash.rb,
lib/crystalruby/types/primitive_types/bool.rb,
lib/crystalruby/types/primitive_types/time.rb,
lib/crystalruby/types/variable_width/array.rb,
lib/crystalruby/types/variable_width/string.rb,
lib/crystalruby/types/primitive_types/symbol.rb,
lib/crystalruby/types/fixed_width/named_tuple.rb,
lib/crystalruby/types/primitive_types/numbers.rb,
lib/crystalruby/types/fixed_width/tagged_union.rb

Defined Under Namespace

Modules: Allocator, Root Classes: FixedWidth, Primitive, Type, VariableWidth

Constant Summary collapse

PROC_REGISTERY =
{}
Proc =
FixedWidth.build(error: "Proc declarations should contain a list of 0 or more comma separated argument types,"\
"and a single return type (or Nil if it does not return a value)")
Tuple =
FixedWidth.build(
  :Tuple,
  error: "Tuple type must contain one or more types E.g. Tuple(Int32, String)"
)
Nil =
Primitive.build(:Nil, convert_if: [::NilClass], memsize: 0) do
  def initialize(val = nil)
    super
    @value = 0
  end

  def nil?
    true
  end

  def value(native: false)
    nil
  end
end
Hash =
VariableWidth.build(error: "Hash type must have 2 type parameters. E.g. Hash(Float64, String)")
Bool =
Primitive.build(:Bool, convert_if: [::TrueClass, ::FalseClass], ffi_type: :uint8, memsize: 1) do
  def value(native: false)
    super == 1
  end

  def value=(val)
    !!val && val != 0 ? super(1) : super(0)
  end
end
Time =
Primitive.build(:Time, convert_if: [Root::Time, Root::String, DateTime], ffi_type: :double) do
  def initialize(val = Root::Time.now)
    super
  end

  def value=(val)
    super(
      if val.respond_to?(:to_time)
        val.to_time.to_f
      else
        val.respond_to?(:to_f) ? val.to_f : 0
      end
    )
  end

  def value(native: false)
    ::Time.at(super)
  end
end
Array =
VariableWidth.build(error: "Array type must have a type parameter. E.g. Array(Float64)")
String =
VariableWidth.build(:String, convert_if: [String, Root::String]) do
  def self.cast!(rbval)
    rbval.to_s
  end

  def self.copy_to!(rbval, memory:)
    data_pointer = malloc(rbval.bytesize)
    data_pointer.write_string(rbval)
    memory[size_offset].write_uint32(rbval.size)
    memory[data_offset].write_pointer(data_pointer)
  end

  def value(native: false)
    data_pointer.read_string(size)
  end
end
Symbol =
Primitive.build(
  error: "Symbol CrystalRuby types should indicate a list of possible values shared between Crystal and Ruby. "\
  "E.g. Symbol(:green, :blue, :orange). If this list is not known at compile time, you should use a String instead."
)
NamedTuple =
FixedWidth.build(error: "NamedTuple type must contain one or more symbol -> type pairs. E.g. NamedTuple(hello: Int32, world: String)")
TaggedUnion =
Class.new(Type) { @error = "Union type must be instantiated from one or more concrete types" }

Class Method Summary collapse

Class Method Details

.Array(type) ⇒ Object

An array-like, reference counted manually managed memory type. Shareable between Crystal and Crystal.



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/crystalruby/types/variable_width/array.rb', line 8

def self.Array(type)
  VariableWidth.build(:Array, inner_types: [type], convert_if: [Array, Root::Array], superclass: Array) do
    include Enumerable

    # Implement the Enumerable interface
    # Helps this object to act like an Array
    def each
      size.times { |i| yield self[i] }
    end

    # We only accept Array-like values, from which all elements
    # can successfully be cast to our inner type
    def self.cast!(value)
      unless value.is_a?(Array) || value.is_a?(Root::Array) && value.all?(&inner_type.method(:valid_cast?))
        raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}"
      end

      if inner_type.primitive?
        value.map(&inner_type.method(:to_ffi_repr))
      else
        value
      end
    end

    def self.copy_to!(rbval, memory:)
      data_pointer = malloc(rbval.size * inner_type.refsize)

      memory[size_offset].write_uint32(rbval.size)
      memory[data_offset].write_pointer(data_pointer)

      if inner_type.primitive?
        data_pointer.send("put_array_of_#{inner_type.ffi_type}", 0, rbval)
      else
        rbval.each_with_index do |val, i|
          inner_type.write_single(data_pointer[i * refsize], val)
        end
      end
    end

    def self.each_child_address(pointer)
      size = pointer[size_offset].get_int32(0)
      pointer = pointer[data_offset].read_pointer
      size.times do |i|
        yield inner_type, pointer[i * inner_type.refsize]
      end
    end

    def checked_offset!(index, size)
      raise "Index out of bounds: #{index} >= #{size}" if index >= size

      if index < 0
        raise "Index out of bounds: #{index} < -#{size}" if index < -size

        index += size
      end
      index
    end

    # Return the element at the given index.
    # This will automatically increment
    # the reference count if not a primitive type.
    def [](index)
      inner_type.fetch_single(data_pointer[checked_offset!(index, size) * inner_type.refsize])
    end

    # Overwrite the element at the given index
    # The replaced element will have
    # its reference count decremented.
    def []=(index, value)
      inner_type.write_single(data_pointer[checked_offset!(index, size) * inner_type.refsize], value)
    end

    # Load values stored inside array type.
    # If it's a primitive type, we can quickly bulk load the values.
    # Otherwise we need toinstantiate new ref-checked instances.
    def value(native: false)
      inner_type.fetch_multi(data_pointer, size, native: native)
    end
  end
end

.const_missing(const_name) ⇒ Object



14
15
16
17
# File 'lib/crystalruby/types.rb', line 14

def self.const_missing(const_name)
  return @fallback.const_get(const_name) if @fallback&.const_defined?(const_name)
  super
end

.Hash(key_type, value_type) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/crystalruby/types/variable_width/hash.rb', line 6

def self.Hash(key_type, value_type)
  VariableWidth.build(:Hash, inner_types: [key_type, value_type], convert_if: [Root::Hash], superclass: Hash) do
    include Enumerable

    def_delegators :@class, :value_type, :key_type

    # Implement the Enumerable interface
    # Helps this object to act like a true Hash
    def each
      if block_given?
        size.times { |i| yield key_for_index(i), value_for_index(i) }
      else
        to_enum(:each)
      end
    end

    def keys
      each.map { |k, _| k }
    end

    def values
      each.map { |_, v| v }
    end

    def self.key_type
      inner_types.first
    end

    def self.value_type
      inner_types.last
    end

    # We only accept Hash-like values, from which all elements
    # can successfully be cast to our inner types
    def self.cast!(value)
      unless (value.is_a?(Hash) || value.is_a?(Root::Hash)) && value.keys.all?(&key_type.method(:valid_cast?)) && value.values.all?(&value_type.method(:valid_cast?))
        raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}"
      end

      [[key_type, value.keys], [value_type, value.values]].map do |type, values|
        if type.primitive?
          values.map(&type.method(:to_ffi_repr))
        else
          values
        end
      end
    end

    def self.copy_to!((keys, values), memory:)
      data_pointer = malloc(values.size * (key_type.refsize + value_type.refsize))

      memory[size_offset].write_uint32(values.size)
      memory[data_offset].write_pointer(data_pointer)

      [
        [key_type, data_pointer, keys],
        [value_type, data_pointer[values.length * key_type.refsize], values]
      ].each do |type, pointer, list|
        if type.primitive?
          pointer.send("put_array_of_#{type.ffi_type}", 0, list)
        else
          list.each_with_index do |val, i|
            type.write_single(pointer[i * type.refsize], val)
          end
        end
      end
    end

    def index_for_key(key)
      size.times { |i| return i if key_for_index(i) == key }
      nil
    end

    def key_for_index(index)
      key_type.fetch_single(data_pointer[index * key_type.refsize])
    end

    def value_for_index(index)
      value_type.fetch_single(data_pointer[key_type.refsize * size + index * value_type.refsize])
    end

    def self.each_child_address(pointer)
      size = pointer[size_offset].read_int32
      pointer = pointer[data_offset].read_pointer
      size.times do |i|
        yield key_type, pointer[i * key_type.refsize]
        yield value_type, pointer[size * key_type.refsize + i * value_type.refsize]
      end
    end

    def [](key)
      return nil unless index = index_for_key(key)

      value_for_index(index)
    end

    def []=(key, value)
      if index = index_for_key(key)
        value_type.write_single(data_pointer[key_type.refsize * size + index * value_type.refsize], value)
      else
        method_missing(:[]=, key, value)
      end
    end

    def value(native: false)
      keys = key_type.fetch_multi(data_pointer, size, native: native)
      values = value_type.fetch_multi(data_pointer[key_type.refsize * size], size, native: native)
      keys.zip(values).to_h
    end
  end
end

.method_missing(method_name, *args) ⇒ Object



19
20
21
22
# File 'lib/crystalruby/types.rb', line 19

def self.method_missing(method_name, *args)
  return @fallback.send(method_name, *args) if @fallback&.method_defined?(method_name)
  super
end

.NamedTuple(types_hash) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/crystalruby/types/fixed_width/named_tuple.rb', line 6

def self.NamedTuple(types_hash)
  raise "NamedTuple must be instantiated with a hash" unless types_hash.is_a?(Root::Hash)

  types_hash.keys.each do |key|
    raise "NamedTuple keys must be symbols" unless key.is_a?(Root::Symbol) || key.respond_to?(:to_sym)
  end
  keys = types_hash.keys.map(&:to_sym)
  value_types = types_hash.values

  FixedWidth.build(:NamedTuple, ffi_type: :pointer, inner_types: value_types, inner_keys: keys,
                                convert_if: [Root::Hash]) do
    @data_offset = 4

    # We only accept Hash-like values, which have all of the required keys
    # and values of the correct type
    # can successfully be cast to our inner types
    def self.cast!(value)
      value = value.transform_keys(&:to_sym)
      unless value.is_a?(Hash) || value.is_a?(Root::Hash) && inner_keys.each_with_index.all? do |k, i|
               value.key?(k) && inner_types[i].valid_cast?(value[k])
             end
        raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}"
      end

      inner_keys.map { |k| value[k] }
    end

    def self.copy_to!(values, memory:)
      data_pointer = malloc(memsize)

      memory[data_offset].write_pointer(data_pointer)

      inner_types.each.reduce(0) do |offset, type|
        type.write_single(data_pointer[offset], values.shift)
        offset + type.refsize
      end
    end

    def self.memsize
      inner_types.map(&:refsize).sum
    end

    def self.each_child_address(pointer)
      data_pointer = pointer[data_offset].read_pointer
      inner_types.each do |type|
        yield type, data_pointer
        data_pointer += type.refsize
      end
    end

    def self.offset_for(key)
      inner_types[0...inner_keys.index(key)].map(&:refsize).sum
    end

    def value(native: false)
      ptr = data_pointer
      inner_keys.zip(inner_types.map do |type|
        result = type.fetch_single(ptr, native: native)
        ptr += type.refsize
        result
      end).to_h
    end

    inner_keys.each.with_index do |key, index|
      type = inner_types[index]
      offset = offset_for(key)
      unless method_defined?(key)
        define_method(key) do
          type.fetch_single(data_pointer[offset])
        end
      end

      unless method_defined?("#{key}=")
        define_method("#{key}=") do |value|
          type.write_single(data_pointer[offset], value)
        end
      end
    end
  end
end

.Proc(*types) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/crystalruby/types/fixed_width/proc.rb', line 6

def self.Proc(*types)
  proc_type = FixedWidth.build(:Proc, convert_if: [::Proc], inner_types: types, ffi_type: :pointer) do
    @data_offset = 4

    def self.cast!(rbval)
      raise "Value must be a proc" unless rbval.is_a?(::Proc)

      func = FFI::Function.new(FFI::Type.const_get(inner_types[-1].ffi_type.to_s.upcase), inner_types[0...-1].map do |v|
                                                                                            FFI::Type.const_get(v.ffi_type.to_s.upcase)
                                                                                          end) do |*args|
        args = args.map.with_index do |arg, i|
          arg = inner_types[i].new(arg) unless arg.is_a?(inner_types[i])
          inner_types[i].anonymous? ? arg.native : arg
        end
        return_val = rbval.call(*args)
        return_val = inner_types[-1].new(return_val) unless return_val.is_a?(inner_types[-1])
        return_val.memory
      end
      PROC_REGISTERY[func.address] = func
      func
    end

    def self.copy_to!(rbval, memory:)
      memory[4].write_pointer(rbval)
    end

    def invoke(*args)
      invoker = value
      invoker.call(memory[12].read_pointer, *args)
    end

    def value(native: false)
      FFI::VariadicInvoker.new(
        memory[4].read_pointer,
        [FFI::Type::POINTER, *(inner_types[0...-1].map { |v| FFI::Type.const_get(v.ffi_type.to_s.upcase) })],
        FFI::Type.const_get(inner_types[-1].ffi_type.to_s.upcase),
        { ffi_convention: :stdcall }
      )
    end

    def self.block_converter
      <<~CRYSTAL
        { #{
          inner_types.size > 1 ? "|#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}|" : ""
        }
          #{
            inner_types[0...-1].map.with_index do |type, i|
              <<~CRYS
                v#{i} = #{type.crystal_class_name}.new(v#{i}).return_value

                callback_done_channel = Channel(Nil).new
                result = nil
                if Fiber.current == Thread.current.main_fiber
                  block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}))
                  result = #{inner_types[-1].anonymous? ? "block_value.native" : "block_value"}
                  next #{inner_types.last == CrystalRuby::Types::Nil ? "result" : "result.not_nil!"}
                else
                  CrystalRuby.queue_callback(->{
                    block_value = #{inner_types[-1].crystal_class_name}.new(__yield_to.call(#{inner_types.size.-(1).times.map { |i| "v#{i}" }.join(",")}))
                    result = #{inner_types[-1].anonymous? ? "block_value.native" : "block_value"}
                    callback_done_channel.send(nil)
                  })
                end
                callback_done_channel.receive
                #{inner_types.last == CrystalRuby::Types::Nil ? "result" : "result.not_nil!"}
              CRYS
            end.join("\n")
          }
        }
      CRYSTAL
    end
  end
end

.Symbol(*allowed_values) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/crystalruby/types/primitive_types/symbol.rb', line 7

def self.Symbol(*allowed_values)
  raise "Symbol must have at least one value" if allowed_values.empty?

  allowed_values.flatten!
  raise "Symbol allowed values must all be symbols" unless allowed_values.all? { |v| v.is_a?(::Symbol) }

  Primitive.build(:Symbol, ffi_type: :uint32, convert_if: [Root::String, Root::Symbol], memsize: 4) do
    bind_local_vars!(%i[allowed_values], binding)
    define_method(:value=) do |val|
      val = allowed_values[val] if val.is_a?(::Integer) && val >= 0 && val < allowed_values.size
      raise "Symbol must be one of #{allowed_values}" unless allowed_values.include?(val)

      super(allowed_values.index(val))
    end

    define_singleton_method(:valid_cast?) do |raw|
      super(raw) && allowed_values.include?(raw)
    end

    define_method(:value) do |native: false|
      allowed_values[super()]
    end

    define_singleton_method(:type_digest) do
      Digest::MD5.hexdigest(native_type_expr.to_s + allowed_values.map(&:to_s).join(","))
    end
  end
end

.TaggedUnion(*union_types) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/crystalruby/types/fixed_width/tagged_union.rb', line 6

def self.TaggedUnion(*union_types)
  Class.new(FixedWidth) do
    # We only accept List-like values, which have all of the required keys
    # and values of the correct type
    # can successfully be cast to our inner types
    def self.cast!(value)
      casteable_type_index = union_types.find_index do |type, _index|
        next false unless type.valid_cast?(value)

        type.cast!(value)
        next true
      rescue StandardError
        nil
      end
      unless casteable_type_index
        raise CrystalRuby::InvalidCastError,
              "Cannot cast #{value}:#{value.class} to #{inspect}"
      end

      [casteable_type_index, value]
    end

    def value(native: false)
      type = self.class.union_types[data_pointer.read_uint8]
      type.fetch_single(data_pointer[1], native: native)
    end

    def nil?
      value.nil?
    end

    def ==(other)
      value == other
    end

    def self.copy_to!((type_index, value), memory:)
      memory[data_offset].write_int8(type_index)
      union_types[type_index].write_single(memory[data_offset + 1], value)
    end

    def data_pointer
      memory[data_offset]
    end

    def self.each_child_address(pointer)
      pointer += data_offset
      type = self.union_types[pointer.read_uint8]
      yield type, pointer[1]
    end

    def self.inner_types
      union_types
    end

    define_singleton_method(:memsize) do
      union_types.map(&:refsize).max + 1
    end

    def self.refsize
      8
    end

    def self.typename
      "TaggedUnion"
    end

    define_singleton_method(:union_types) do
      union_types
    end

    define_singleton_method(:valid?) do
      union_types.all?(&:valid?)
    end

    define_singleton_method(:error) do
      union_types.map(&:error).join(", ") if union_types.any?(&:error)
    end

    define_singleton_method(:inspect) do
      if anonymous?
        union_types.map(&:inspect).join(" | ")
      else
        crystal_class_name
      end
    end

    define_singleton_method(:native_type_expr) do
      union_types.map(&:native_type_expr).join(" | ")
    end

    define_singleton_method(:type_expr) do
      anonymous? ? native_type_expr : name
    end

    define_singleton_method(:data_offset) do
      4
    end

    define_singleton_method(:valid_cast?) do |raw|
      union_types.any? { |type| type.valid_cast?(raw) }
    end
  end
end

.Tuple(*types) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/crystalruby/types/fixed_width/tuple.rb', line 9

def self.Tuple(*types)
  FixedWidth.build(:Tuple, inner_types: types, convert_if: [Root::Array], superclass: Tuple) do
    @data_offset = 4

    # We only accept List-like values, which have all of the required keys
    # and values of the correct type
    # can successfully be cast to our inner types
    def self.cast!(value)
      unless (value.is_a?(Array) || value.is_a?(Tuple) || value.is_a?(Root::Array)) && value.zip(inner_types).each do |v, t|
               t && t.valid_cast?(v)
             end && value.length == inner_types.length
        raise CrystalRuby::InvalidCastError, "Cannot cast #{value} to #{inspect}"
      end

      value
    end

    def self.copy_to!(values, memory:)
      data_pointer = malloc(memsize)

      memory[data_offset].write_pointer(data_pointer)

      inner_types.each.reduce(0) do |offset, type|
        type.write_single(data_pointer[offset], values.shift)
        offset + type.refsize
      end
    end

    def self.each_child_address(pointer)
      data_pointer = pointer[data_offset].read_pointer
      inner_types.each do |type|
        yield type, data_pointer
        data_pointer += type.refsize
      end
    end

    def self.memsize
      inner_types.map(&:refsize).sum
    end

    def size
      inner_types.size
    end

    def checked_offset!(index, size)
      raise "Index out of bounds: #{index} >= #{size}" if index >= size

      if index < 0
        raise "Index out of bounds: #{index} < -#{size}" if index < -size

        index += size
      end
      self.class.offset_for(index)
    end

    def self.offset_for(index)
      inner_types[0...index].map(&:refsize).sum
    end

    # Return the element at the given index.
    # This will automatically increment
    # the reference count if not a primitive type.
    def [](index)
      inner_types[index].fetch_single(data_pointer[checked_offset!(index, size)])
    end

    # Overwrite the element at the given index
    # The replaced element will have
    # its reference count decremented.
    def []=(index, value)
      inner_types[index].write_single(data_pointer[checked_offset!(index, size)], value)
    end

    def value(native: false)
      ptr = data_pointer
      inner_types.map do |type|
        result = type.fetch_single(ptr, native: native)
        ptr += type.refsize
        result
      end
    end
  end
end

.with_binding_fallback(fallback) ⇒ Object



24
25
26
27
28
29
30
# File 'lib/crystalruby/types.rb', line 24

def self.with_binding_fallback(fallback)
  @fallback, previous_fallback = fallback, @fallback
  @fallback = @fallback.class unless @fallback.kind_of?(Module)
  yield binding
ensure
  @fallback = previous_fallback
end