crystalruby 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46edf2cddbf8fc124cca7cab14a4c3d745fcf88b9a841468cdbc7f2fc517187a
4
- data.tar.gz: c350e0049aff5d88f8326314e0a70fbe57ea3866dcdf247010a0c1c64f656899
3
+ metadata.gz: 26b2e0d15fd3b320f700a1f0571b8bb5d8991a4ed1c60cfcd9f003b48bb9f891
4
+ data.tar.gz: 27243d104172eef50adfc46524a0b9052410636dc0b39e142fc3e53c04ded1f0
5
5
  SHA512:
6
- metadata.gz: 54ca6d542370d360f1f48959e4edf5a4308985d8e952317e2780b980c068674cb56c257c9d8241d0fc929bb152ca9960a8c7397f90c08725acf9bd268f794842
7
- data.tar.gz: 303d6c379501dcf8966dd3941f2e667f35efeec67b24eb68bf2965abee06e5afa4b631c7b23e7493e4a3e088f650bd5a85271ae2bedb18b7a0120ca4c4bc605c
6
+ metadata.gz: a54c1cf883a7dea97d66e9c18903139713aaec0b965026a21fc2e14ced85c13fc676f7d8e76ecc9a2e6ca2fc69034185a9fa4eb6fc66a6986cc9bb2d037738fb
7
+ data.tar.gz: 0f29fe7bad266af6593d0e4533af27a9be925372befe40ed0e4ee5ea8c9293c7ea62aab4b1664eaf3d2202eb3000b3dda1dfbdc44ae9e924e2de017f5516a6bc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.4] - 2024-04-10
4
+
5
+ - Fix bug in type checking on deserialization of union types
6
+
7
+ ## [0.1.3] - 2024-04-10
8
+
9
+ - Support exceptions thrown in Crystal being caught in Ruby
10
+ - Support complex Ruby type passing (while preserving type checking), using `JSON` as serialization format.
11
+
3
12
  ## [0.1.0] - 2024-04-07
4
13
 
5
14
  - Initial release
data/README.md CHANGED
@@ -120,8 +120,10 @@ end
120
120
 
121
121
  ## Types
122
122
 
123
- Currently only primitive types are supported. Structures are a WIP.
124
- To see the list of currently supported type mappings of FFI types to crystal types, you can check: `CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP`
123
+ Currently primitive types are supported.
124
+ Composite types are supported using JSON serialization.
125
+ C-Structures are a WIP.
126
+ To see the list of currently supported primitive type mappings of FFI types to crystal types, you can check: `CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP`
125
127
  E.g.
126
128
 
127
129
  ```ruby
@@ -151,7 +153,70 @@ CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP
151
153
  :string=>"String"}
152
154
  ```
153
155
 
154
- ### Installing shards and writing non-embedded Crystal code
156
+ ## Composite Types (using JSON serialization)
157
+
158
+ The library allows you to pass complex nested structures using JSON as a serialization format.
159
+ The type signatures for composite types can use ordinary Crystal Type syntax.
160
+ Type conversion is applied automatically.
161
+
162
+ E.g.
163
+
164
+ ```ruby
165
+ crystalize [a: json{ Int64 | Float64 | Nil }, b: json{ String | Array(Bool) } ] => :void
166
+ def complex_argument_types
167
+ puts "Got #{a} and #{b}"
168
+ end
169
+
170
+ crystalize [] => json{ Int32 | String | Hash(String, Array(NamedTuple(hello: Int32)) | Time)}
171
+ def complex_return_type
172
+ return {
173
+ "hello" => [
174
+ {
175
+ hello: 1,
176
+ },
177
+ ],
178
+ "world" => Time.utc
179
+ }
180
+ end
181
+ ```
182
+
183
+ Type signatures validations are applied to both arguments and return types.
184
+
185
+ ```ruby
186
+ [1] pry(main)> Foo.complex_argument_types(nil, "test")
187
+ Got and test
188
+ => nil
189
+
190
+ [2] pry(main)> Foo.complex_argument_types(88, [true, false, true])
191
+ Got 88 and [true, false, true]
192
+ => nil
193
+
194
+ [3] pry(main)> Foo.complex_argument_types(88, [true, false, 88])
195
+ ArgumentError: Expected Bool but was Int at line 1, column 15
196
+ from crystalruby.rb:303:in `block in compile!'
197
+ ```
198
+
199
+ ## Named Types
200
+
201
+ You can name your types, for more succinct method signatures.
202
+ The type names will be mirrored in the generated Crystal code.
203
+ E.g.
204
+
205
+ ```ruby
206
+
207
+ IntArrOrBoolArr = crtype{ Array(Bool) | Array(Int32) }
208
+
209
+ crystalize [a: IntArrOrBoolArr] => json{ IntArrOrBoolArr }
210
+ def method_with_named_types(a)
211
+ return a
212
+ end
213
+ ```
214
+
215
+ ## Exceptions
216
+
217
+ Exceptions thrown in Crystal code can be caught in Ruby.
218
+
219
+ ## Installing shards and writing non-embedded Crystal code
155
220
 
156
221
  You can use any Crystal shards and write ordinary, stand-alone Crystal code.
157
222
 
data/exe/crystalruby CHANGED
@@ -13,6 +13,7 @@ def init
13
13
  crystal_lib_dir: "./crystalruby/lib"
14
14
  crystal_main_file: "main.cr"
15
15
  crystal_lib_name: "crlib"
16
+ crystal_codegen_dir: "generated"
16
17
  YAML
17
18
 
18
19
  # Create the file at the root of the current directory
@@ -9,16 +9,20 @@ module CrystalRuby
9
9
  # Define a nested Config class
10
10
  class Config
11
11
  include Singleton
12
- attr_accessor :debug, :crystal_src_dir, :crystal_lib_dir, :crystal_main_file, :crystal_lib_name
12
+ attr_accessor :debug, :crystal_src_dir, :crystal_lib_dir, :crystal_main_file, :crystal_lib_name, :crystal_codegen_dir
13
13
 
14
14
  def initialize
15
- # Set default configuration options
16
15
  @debug = true
17
- return unless File.exist?("crystalruby.yaml")
18
-
19
- @crystal_src_dir, @crystal_lib_dir, @crystal_main_file, @crystal_lib_name =
20
- YAML.safe_load(IO.read("crystalruby.yaml")).values_at("crystal_src_dir", "crystal_lib_dir", "crystal_main_file",
21
- "crystal_lib_name")
16
+ config = if File.exist?("crystalruby.yaml")
17
+ YAML.safe_load(IO.read("crystalruby.yaml")) rescue {}
18
+ else
19
+ {}
20
+ end
21
+ @crystal_src_dir = config.fetch("crystal_src_dir", "./crystalruby/src")
22
+ @crystal_lib_dir = config.fetch("crystal_lib_dir", "./crystalruby/lib")
23
+ @crystal_main_file = config.fetch("crystal_main_file", "main.cr")
24
+ @crystal_lib_name = config.fetch("crystal_lib_name", "crlib")
25
+ @crystal_codegen_dir = config.fetch("crystal_codegen_dir", "generated")
22
26
  end
23
27
  end
24
28
 
@@ -0,0 +1,13 @@
1
+ require 'pry-byebug'
2
+ module CrystalRuby
3
+ module Template
4
+ Dir[File.join(File.dirname(__FILE__), "templates", "*.cr")].each do |file|
5
+ template_name = File.basename(file, File.extname(file)).capitalize
6
+ const_set(template_name, File.read(file))
7
+ end
8
+
9
+ def self.render(template, context)
10
+ template % context
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module %{module_name}
2
+ def self.%{fn_name}(%{fn_args}) : %{fn_ret_type}
3
+ %{fn_body}
4
+ end
5
+ end
6
+
7
+ fun %{lib_fn_name}(%{lib_fn_args}): %{lib_fn_ret_type}
8
+ begin
9
+ %{convert_lib_args}
10
+ begin
11
+ return_value = %{module_name}.%{fn_name}(%{arg_names})
12
+ return %{convert_return_type}
13
+ rescue ex
14
+ CrystalRuby.report_error("RuntimeError", ex.message.to_s)
15
+ end
16
+ rescue ex
17
+ CrystalRuby.report_error("ArgumentError", ex.message.to_s)
18
+ end
19
+ return %{error_value}
20
+ end
@@ -0,0 +1,38 @@
1
+ FAKE_ARG = "crystal"
2
+ alias Callback = (Pointer(UInt8), Pointer(UInt8) -> Void)
3
+
4
+ module CrystalRuby
5
+ @@initialized = false
6
+ def self.init
7
+ if @@initialized
8
+ return
9
+ end
10
+ @@initialized = true
11
+ GC.init
12
+ ptr = FAKE_ARG.to_unsafe
13
+ LibCrystalMain.__crystal_main(1, pointerof(ptr))
14
+ end
15
+
16
+ def self.attach_rb_error_handler(cb : Callback)
17
+ @@rb_error_handler = cb
18
+ end
19
+
20
+ def self.report_error(error_type : String, str : String)
21
+ handler = @@rb_error_handler
22
+ if handler
23
+ handler.call(error_type.to_unsafe, str.to_unsafe)
24
+ end
25
+ end
26
+ end
27
+
28
+
29
+ fun init(): Void
30
+ CrystalRuby.init
31
+ end
32
+
33
+ fun attach_rb_error_handler(cb : Callback) : Void
34
+ CrystalRuby.attach_rb_error_handler(cb)
35
+ end
36
+
37
+ %{type_modules}
38
+ %{requires}
@@ -0,0 +1,179 @@
1
+ require_relative "types"
2
+
3
+ module CrystalRuby
4
+ module TypeBuilder
5
+ module_function
6
+
7
+ def with_injected_type_dsl(context, &block)
8
+ with_constants(context) do
9
+ with_methods(context, &block)
10
+ end
11
+ end
12
+
13
+ def with_methods(context)
14
+ restores = []
15
+ %i[Array Hash NamedTuple Tuple].each do |method_name|
16
+ old_method = begin
17
+ context.instance_method(method_name)
18
+ rescue StandardError
19
+ nil
20
+ end
21
+ restores << [context, method_name, old_method]
22
+ context.define_singleton_method(method_name) do |*args|
23
+ Types.send(method_name, *args)
24
+ end
25
+ end
26
+ yield
27
+ ensure
28
+ restores.each do |context, method_name, old_method|
29
+ context.define_singleton_method(method_name, old_method) if old_method
30
+ end
31
+ end
32
+
33
+ def with_constants(context)
34
+ previous_const_pairs = CrystalRuby::Types.constants.map do |type|
35
+ [type, begin
36
+ context.const_get(type)
37
+ rescue StandardError
38
+ nil
39
+ end]
40
+ end
41
+ CrystalRuby::Types.constants.each do |type|
42
+ begin
43
+ context.send(:remove_const, type)
44
+ rescue StandardError
45
+ nil
46
+ end
47
+ context.const_set(type, CrystalRuby::Types.const_get(type))
48
+ end
49
+ yield
50
+ ensure
51
+ previous_const_pairs.each do |const_name, const_value|
52
+ begin
53
+ context.send(:remove_const, const_name)
54
+ rescue StandardError
55
+ nil
56
+ end
57
+ context.const_set(const_name, const_value)
58
+ end
59
+ end
60
+
61
+ def build
62
+ result = yield
63
+ Types::Type.validate!(result)
64
+ Types::Typedef(result)
65
+ end
66
+ end
67
+ end
68
+
69
+ # class UnionType
70
+ # attr_reader :inner
71
+
72
+ # def initialize(*inner)
73
+ # @inner = inner
74
+ # end
75
+
76
+ # def |(other)
77
+ # UnionType.new(*inner, *other.inner)
78
+ # end
79
+
80
+ # def inspect
81
+ # elements = inner.map(&:inspect).join(" | ")
82
+ # end
83
+ # end
84
+
85
+ # class Type
86
+ # attr_reader :inner, :contains
87
+
88
+ # def initialize(name)
89
+ # @name = name
90
+ # @contains = contains
91
+ # @inner = [self]
92
+ # end
93
+
94
+ # def |(other)
95
+ # UnionType.new(*inner, *other.inner)
96
+ # end
97
+
98
+ # def inspect
99
+ # if @contains
100
+ # "#{@name}(#{@contains.inspect})"
101
+ # else
102
+ # @name
103
+ # end
104
+ # end
105
+ # end
106
+
107
+ # module_function
108
+
109
+ # %w[
110
+ # Bool Uint8 Uint16 Uint32 Uint64 Int8 Int16 Int32 Int64 Float32 Float64 String Time Symbol
111
+ # Null
112
+ # ].map do |t|
113
+ # cls = Class.new(Type)
114
+ # const_set(t, cls)
115
+ # define_method(t.downcase) do
116
+ # cls.new(t)
117
+ # end
118
+ # end
119
+
120
+ # def build(&blk)
121
+ # instance_exec(&blk)
122
+ # end
123
+
124
+ # def hash(key_type, value_type)
125
+ # Hash.new(key_type, value_type)
126
+ # end
127
+
128
+ # def array(type)
129
+ # Array.new(type)
130
+ # end
131
+
132
+ # def tuple(*types)
133
+ # Tuple.new(*types)
134
+ # end
135
+
136
+ # def named_tuple(type_hash)
137
+ # NamedTuple.new(type_hash)
138
+ # end
139
+
140
+ # def NamedTuple(type_hash)
141
+ # NamedTuple.new(type_hash)
142
+ # end
143
+
144
+ # class Hash < Type
145
+ # HASH_KEY_TYPES = %w[String Symbol].freeze
146
+ # def initialize(key_type, value_type)
147
+ # super("Hash")
148
+ # @key_type = key_type
149
+ # @value_type = value_type
150
+ # raise "Invalid key type" unless [Uint8, Uint16, Uint32, Uint64, Int8, Int16, Int32, Int64,
151
+ # String].include?(key_type)
152
+ # raise "Invalid value type" unless value_type.is_a?(Type)
153
+ # end
154
+ # end
155
+
156
+ # class Array < Type
157
+ # def initialize(value_type)
158
+ # super("Array")
159
+ # @value_type = value_type
160
+ # raise "Invalid value type" unless value_type.is_a?(Type)
161
+ # end
162
+ # end
163
+
164
+ # class NamedTuple < Type
165
+ # def initialize(types_hash)
166
+ # raise "keys must be symbols" unless types_hash.keys.all? { |k| k.is_a?(Symbol) }
167
+ # raise "Invalid value type" unless types_hash.values.all? { |v| v.is_a?(Type) }
168
+
169
+ # super("NamedTuple")
170
+ # @types_hash = types_hash
171
+ # end
172
+ # end
173
+
174
+ # class Tuple < Type
175
+ # def initialize(*value_types)
176
+ # super("Tuple")
177
+ # raise "Invalid value type" unless value_types.all? { |v| v.is_a?(Type) }
178
+ # end
179
+ # end
@@ -26,6 +26,32 @@ module CrystalRuby
26
26
  :string => "String" # String type
27
27
  }
28
28
 
29
+ ERROR_VALUE = {
30
+ :char => "0", # In Crystal, :char is typically represented as Int8
31
+ :uchar => "0", # Unsigned char
32
+ :int8 => "0", # Same as :char
33
+ :uint8 => "0", # Same as :uchar
34
+ :short => "0", # Short integer
35
+ :ushort => "0", # Unsigned short integer
36
+ :int16 => "0", # Same as :short
37
+ :uint16 => "0", # Same as :ushort
38
+ :int => "0", # Integer, Crystal defaults to 32 bits
39
+ :uint => "0", # Unsigned integer
40
+ :int32 => "0", # 32-bit integer
41
+ :uint32 => "0", # 32-bit unsigned integer
42
+ :long => "0", # Long integer, size depends on the platform (32 or 64 bits)
43
+ :ulong => "0", # Unsigned long integer, size depends on the platform
44
+ :int64 => "0", # 64-bit integer
45
+ :uint64 => "0", # 64-bit unsigned integer
46
+ :long_long => "0", # Same as :int64
47
+ :ulong_long => "0", # Same as :uint64
48
+ :float => "0.0", # Floating point number (single precision)
49
+ :double => "0.0", # Double precision floating point number
50
+ :bool => "false", # Boolean type
51
+ :void => "Void", # Void type
52
+ :string => '""' # String type
53
+ }
54
+
29
55
  C_TYPE_MAP = CRYSTAL_TYPE_MAP.merge({
30
56
  :string => "UInt8*"
31
57
  })
@@ -0,0 +1,14 @@
1
+ module CrystalRuby::Types
2
+ Array = Type.new(
3
+ :Array,
4
+ error: "Array type must have a type parameter. E.g. Array(Float64)"
5
+ )
6
+
7
+ def self.Array(type)
8
+ Type.validate!(type)
9
+ Type.new("Array", inner_types: [type], accept_if: [::Array]
10
+ ) do |a|
11
+ a.map!{|v| type.interpret!(v) }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ Bool = Type.new(:Bool, accept_if: [::TrueClass, ::FalseClass])
3
+ end
@@ -0,0 +1,15 @@
1
+ module CrystalRuby::Types
2
+ Hash = Type.new(
3
+ :Hash,
4
+ error: "Hash type must have 2 type parameters. E.g. Hash(Float64, String)",
5
+ )
6
+
7
+ def self.Hash(key_type, value_type)
8
+ Type.validate!(key_type)
9
+ Type.validate!(value_type)
10
+ Type.new("Hash", inner_types: [key_type, value_type], accept_if: [::Hash]) do |h|
11
+ h.transform_keys!{|k| key_type.interpret!(k) }
12
+ h.transform_values!{|v| value_type.interpret!(v) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module CrystalRuby::Types
2
+ NamedTuple = Type.new(
3
+ :NamedTuple,
4
+ error: "NamedTuple type must contain one or more symbol -> type pairs. E.g. NamedTuple(hello: Int32, world: String)"
5
+ )
6
+
7
+ def self.NamedTuple(types_hash)
8
+ types_hash.keys.each do |key|
9
+ raise "NamedTuple keys must be symbols" unless key.kind_of?(::Symbol) || key.respond_to?(:to_sym)
10
+ end
11
+ types_hash.values.each do |value_type|
12
+ Type.validate!(value_type)
13
+ end
14
+ keys = types_hash.keys.map(&:to_sym)
15
+ values = types_hash.values
16
+ Type.new("NamedTuple", inner_types: values, inner_keys: keys, accept_if: [::Hash]) do |h|
17
+ h.transform_keys!{|k| k.to_sym }
18
+ raise "Invalid keys for named tuple" unless h.keys.length == keys.length
19
+ raise "Invalid keys for named tuple" unless h.keys.all?{|k| keys.include?(k)}
20
+ h.each do |key, value|
21
+ h[key] = values[keys.index(key)].interpret!(value)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ Nil = Type.new(:Nil, accept_if: [::NilClass])
3
+ end
@@ -0,0 +1,5 @@
1
+ module CrystalRuby::Types
2
+ %i[Uint8 Uint16 Uint32 Uint64 Int8 Int16 Int32 Int64 Float32 Float64].each do |type_name|
3
+ const_set type_name, Type.new(type_name, accept_if: [::Numeric])
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ String = Type.new(:String, accept_if: [::String])
3
+ end
@@ -0,0 +1,3 @@
1
+ module CrystalRuby::Types
2
+ Symbol = Type.new(:Symbol, accept_if: [::String, ::Symbol])
3
+ end
@@ -0,0 +1,6 @@
1
+ module CrystalRuby::Types
2
+ require 'date'
3
+ Time = Type.new(:Time, accept_if: [::Time, ::String]) do |v|
4
+ DateTime.parse(v)
5
+ end
6
+ end
@@ -0,0 +1,15 @@
1
+ module CrystalRuby::Types
2
+ Tuple = Type.new(
3
+ :Tuple,
4
+ error: "Tuple type must contain one or more types E.g. Tuple(Int32, String)"
5
+ )
6
+
7
+ def self.Tuple(*types)
8
+ types.each do |value_type|
9
+ Type.validate!(value_type)
10
+ end
11
+ Type.new("Tuple", inner_types: types, accept_if: [::Array]) do |a|
12
+ a.map!.with_index{|v, i| self.inner_types[i].interpret!(v) }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ module CrystalRuby
2
+ module Types
3
+ class Type
4
+ attr_accessor :name, :error, :inner_types, :inner_keys, :accept_if, :convert
5
+
6
+ def initialize(name, error: nil, inner_types: nil, inner_keys: nil, accept_if: [], &convert)
7
+ self.name = name
8
+ self.error = error
9
+ self.inner_types = inner_types
10
+ self.inner_keys = inner_keys
11
+ self.accept_if = accept_if
12
+ self.convert = convert
13
+ end
14
+
15
+ def union_types
16
+ [self]
17
+ end
18
+
19
+ def valid?
20
+ !error
21
+ end
22
+
23
+ def |(other)
24
+ raise "Cannot union non-crystal type #{other}" unless other.is_a?(Type) || (
25
+ other.is_a?(Class) && other.ancestors.include?(Typedef)
26
+ )
27
+
28
+ UnionType.new(*union_types, *other.union_types)
29
+ end
30
+
31
+ def type_expr
32
+ inspect
33
+ end
34
+
35
+ def inspect
36
+ if !inner_types
37
+ name
38
+ elsif !inner_keys
39
+ "#{name}(#{inner_types.map(&:inspect).join(", ")})"
40
+ else
41
+ "#{name}(#{inner_keys.zip(inner_types).map { |k, v| "#{k}: #{v.inspect}" }.join(", ")})"
42
+ end
43
+ end
44
+
45
+ def interprets?(raw)
46
+ accept_if.any? { |type| raw.is_a?(type) }
47
+ end
48
+
49
+ def interpret!(raw)
50
+ if interprets?(raw)
51
+ convert ? convert.call(raw) : raw
52
+ else
53
+ raise "Invalid deserialized value #{raw} for type #{inspect}"
54
+ end
55
+ end
56
+
57
+ def self.validate!(type)
58
+ unless type.is_a?(Types::Type) || (type.is_a?(Class) && type.ancestors.include?(Types::Typedef))
59
+ raise "Result #{type} is not a valid CrystalRuby type"
60
+ end
61
+
62
+ raise "Invalid type: #{type.error}" unless type.valid?
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ require 'json'
2
+
3
+ module CrystalRuby::Types
4
+ class TypeSerializer
5
+ class JSON < TypeSerializer
6
+
7
+ def lib_type
8
+ "UInt8*"
9
+ end
10
+
11
+ def crystal_type
12
+ type_expr
13
+ end
14
+
15
+ def error_value
16
+ '"{}".to_unsafe'
17
+ end
18
+
19
+ def ffi_type
20
+ :string
21
+ end
22
+
23
+ def prepare_argument(arg)
24
+ arg.to_json
25
+ end
26
+
27
+ def prepare_retval(retval)
28
+ @typedef.interpret!(::JSON.parse(retval))
29
+ end
30
+
31
+ def lib_to_crystal_type_expr(expr)
32
+ "(#{type_expr}).from_json(String.new(%s))" % expr
33
+ end
34
+
35
+ def crystal_to_lib_type_expr(expr)
36
+ "%s.to_json.to_unsafe" % expr
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ module CrystalRuby::Types
2
+ class TypeSerializer
3
+ include FFI::DataConverter
4
+ def self.for(format)
5
+ case format
6
+ when :json then JSON
7
+ else raise "Unknown type format: #{format}"
8
+ end
9
+ end
10
+
11
+ def error_value
12
+ 0
13
+ end
14
+
15
+ def initialize(typedef)
16
+ @typedef = typedef
17
+ end
18
+
19
+ def type_expr
20
+ @typedef.type_expr
21
+ end
22
+
23
+ def type_defn
24
+ @typedef.type_defn
25
+ end
26
+
27
+ def anonymous?
28
+ @typedef.anonymous?
29
+ end
30
+
31
+ def name
32
+ @typedef.name
33
+ end
34
+ end
35
+ end
36
+
37
+ require_relative "type_serializer/json"
@@ -0,0 +1,55 @@
1
+ require_relative "type_serializer"
2
+
3
+ module CrystalRuby
4
+ module Types
5
+ class Typedef; end
6
+
7
+ def self.Typedef(type)
8
+ return type if type.kind_of?(Class) && type < Typedef
9
+
10
+ Class.new(Typedef) do
11
+ define_singleton_method(:union_types) do
12
+ [self]
13
+ end
14
+
15
+ define_singleton_method(:anonymous?) do
16
+ name.nil?
17
+ end
18
+
19
+ define_singleton_method(:valid?) do
20
+ type.valid?
21
+ end
22
+
23
+ define_singleton_method(:type) do
24
+ type
25
+ end
26
+
27
+ define_singleton_method(:type_expr) do
28
+ anonymous? ? type.type_expr : name
29
+ end
30
+
31
+ define_singleton_method(:type_defn) do
32
+ type.type_expr
33
+ end
34
+
35
+ define_singleton_method(:|) do |other|
36
+ raise "Cannot union non-crystal type #{other}" unless other.is_a?(Type) || other.is_a?(Typedef)
37
+
38
+ UnionType.new(*union_types, *other.union_types)
39
+ end
40
+
41
+ define_singleton_method(:inspect) do
42
+ "<#{name || "AnonymousType"} #{type.inspect}>"
43
+ end
44
+
45
+ define_singleton_method(:serialize_as) do |format|
46
+ TypeSerializer.for(format).new(self)
47
+ end
48
+
49
+ define_singleton_method(:interpret!) do |raw|
50
+ type.interpret!(raw)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,41 @@
1
+ module CrystalRuby
2
+ module Types
3
+ class UnionType < Type
4
+ attr_accessor :name, :union_types
5
+
6
+ def initialize(*union_types)
7
+ self.name = "UnionType"
8
+ self.union_types = union_types
9
+ end
10
+
11
+ def valid?
12
+ union_types.all?(&:valid?)
13
+ end
14
+
15
+ def error
16
+ union_types.map(&:error).join(", ")
17
+ end
18
+
19
+ def inspect
20
+ union_types.map(&:inspect).join(" | ")
21
+ end
22
+
23
+ def interprets?(raw)
24
+ union_types.any? { |type| type.interprets?(raw) }
25
+ end
26
+
27
+ def interpret!(raw)
28
+ union_types.each do |type|
29
+ if type.interprets?(raw)
30
+ begin
31
+ return type.interpret!(raw)
32
+ rescue
33
+ # Pass
34
+ end
35
+ end
36
+ end
37
+ raise "Invalid deserialized value #{raw} for type #{inspect}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "types/type"
2
+ require_relative "types/union_type"
3
+ require_relative "types/typedef"
4
+ require_relative "types/string"
5
+ require_relative "types/time"
6
+ require_relative "types/symbol"
7
+ require_relative "types/array"
8
+ require_relative "types/hash"
9
+ require_relative "types/nil"
10
+ require_relative "types/bool"
11
+ require_relative "types/named_tuple"
12
+ require_relative "types/tuple"
13
+ require_relative "types/numbers"
14
+
15
+ module CrystalRuby
16
+ module Types
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crystalruby
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
data/lib/crystalruby.rb CHANGED
@@ -5,6 +5,9 @@ require "method_source"
5
5
  require_relative "crystalruby/config"
6
6
  require_relative "crystalruby/version"
7
7
  require_relative "crystalruby/typemaps"
8
+ require_relative "crystalruby/types"
9
+ require_relative "crystalruby/typebuilder"
10
+ require_relative "crystalruby/template"
8
11
 
9
12
  module CrystalRuby
10
13
  # Define a method to set the @crystalize proc if it doesn't already exist
@@ -16,6 +19,24 @@ module CrystalRuby
16
19
  @crystalize_next = { raw: type.to_sym == :raw, args: args, returns: returns, block: block }
17
20
  end
18
21
 
22
+ def crtype(&block)
23
+ TypeBuilder.with_injected_type_dsl(self) do
24
+ TypeBuilder.build(&block)
25
+ end
26
+ end
27
+
28
+ def json(&block)
29
+ crtype(&block).serialize_as(:json)
30
+ end
31
+
32
+ def with_temporary_constant(constant_name, new_value)
33
+ old_value = const_get(constant_name)
34
+ const_set(constant_name, new_value)
35
+ yield
36
+ ensure
37
+ const_set(constant_name, old_value)
38
+ end
39
+
19
40
  def method_added(method_name)
20
41
  if @crystalize_next
21
42
  attach_crystalized_method(method_name)
@@ -40,11 +61,10 @@ module CrystalRuby
40
61
  args ||= {}
41
62
  @crystalize_next = nil
42
63
  function = build_function(self, method_name, args, returns, function_body)
43
-
44
- CrystalRuby.write_function(self, **function) do
64
+ CrystalRuby.write_function(self, name: function[:name], body: function[:body]) do
45
65
  extend FFI::Library
46
66
  ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
47
- attach_function "#{method_name}", fname, args.map(&:last), returns
67
+ attach_function "#{method_name}", fname, function[:ffi_types], function[:return_ffi_type]
48
68
  attach_function "init!", "init", [], :void
49
69
  if block
50
70
  [singleton_class, self].each do |receiver|
@@ -62,7 +82,15 @@ module CrystalRuby
62
82
  define_method(method_name) do |*args|
63
83
  CrystalRuby.compile! unless CrystalRuby.compiled?
64
84
  CrystalRuby.attach! unless CrystalRuby.attached?
65
- super(*args)
85
+ args.each_with_index do |arg, i|
86
+ args[i] = function[:arg_maps][i][arg] if function[:arg_maps][i]
87
+ end
88
+ result = super(*args)
89
+ if function[:retval_map]
90
+ function[:retval_map][result]
91
+ else
92
+ result
93
+ end
66
94
  end
67
95
  end)
68
96
  end
@@ -71,42 +99,105 @@ module CrystalRuby
71
99
  module_function
72
100
 
73
101
  def build_function(owner, name, args, returns, body)
74
- fnname = "#{owner.name.downcase}_#{name}"
75
- args ||= {}
76
- string_conversions = args.select { |_k, v| v.eql?(:string) }.keys
77
- function_body = <<~CRYSTAL
78
- module #{owner.name}
79
- def self.#{name}(#{args.map { |k, v| "#{k} : #{native_type(v)}" }.join(",")}) : #{native_type(returns)}
80
- #{body}
81
- end
82
- end
102
+ arg_types = args.transform_values(&method(:build_type_map))
103
+ return_type = build_type_map(returns)
104
+ function_body = Template.render(
105
+ Template::Function,
106
+ {
107
+ module_name: owner.name,
108
+ lib_fn_name: "#{owner.name.downcase}_#{name}",
109
+ fn_name: name,
110
+ fn_body: body,
111
+ fn_args: arg_types.map { |k, arg_type| "#{k} : #{arg_type[:crystal_type]}" }.join(","),
112
+ fn_ret_type: return_type[:crystal_type],
113
+ lib_fn_args: arg_types.map { |k, arg_type| "_#{k}: #{arg_type[:lib_type]}" }.join(","),
114
+ lib_fn_ret_type: return_type[:lib_type],
115
+ convert_lib_args: arg_types.map do |k, arg_type|
116
+ "#{k} = #{arg_type[:convert_lib_to_crystal_type]["_#{k}"]}"
117
+ end.join("\n "),
118
+ arg_names: args.keys.join(","),
119
+ convert_return_type: return_type[:convert_crystal_to_lib_type]["return_value"],
120
+ error_value: return_type[:error_value]
121
+ }
122
+ )
123
+ {
124
+ name: name,
125
+ body: function_body,
126
+ retval_map: returns.is_a?(Types::TypeSerializer) ? ->(rv) { returns.prepare_retval(rv) } : nil,
127
+ ffi_types: arg_types.map { |_k, arg_type| arg_type[:ffi_type] },
128
+ arg_maps: arg_types.map { |_k, arg_type| arg_type[:mapper] },
129
+ return_ffi_type: return_type[:return_ffi_type]
130
+ }
131
+ end
83
132
 
84
- fun #{fnname}(#{args.map { |k, v| "_#{k}: #{lib_type(v)}" }.join(",")}): #{lib_type(returns)}
85
- #{args.map { |k, v| "#{k} = #{convert_to_native_type("_#{k}", v)}" }.join("\n\t")}
86
- #{convert_to_return_type("#{owner.name}.#{name}(#{args.keys.map { |k| "#{k}" }.join(",")})", returns)}
87
- end
88
- CRYSTAL
133
+ def build_type_map(crystalruby_type)
134
+ if crystalruby_type.is_a?(Types::TypeSerializer) && !crystalruby_type.anonymous?
135
+ CrystalRuby.register_type!(crystalruby_type)
136
+ end
89
137
 
90
138
  {
91
- name: fnname,
92
- body: function_body
139
+ ffi_type: ffi_type(crystalruby_type),
140
+ return_ffi_type: ffi_type(crystalruby_type),
141
+ crystal_type: crystal_type(crystalruby_type),
142
+ lib_type: lib_type(crystalruby_type),
143
+ error_value: error_value(crystalruby_type),
144
+ mapper: crystalruby_type.is_a?(Types::TypeSerializer) ? ->(arg) { crystalruby_type.prepare_argument(arg) } : nil,
145
+ convert_crystal_to_lib_type: ->(expr) { convert_crystal_to_lib_type(expr, crystalruby_type) },
146
+ convert_lib_to_crystal_type: ->(expr) { convert_lib_to_crystal_type(expr, crystalruby_type) }
93
147
  }
94
148
  end
95
149
 
150
+ def ffi_type(type)
151
+ case type
152
+ when Symbol then type
153
+ when Types::TypeSerializer then type.ffi_type
154
+ end
155
+ end
156
+
96
157
  def lib_type(type)
97
- Typemaps::C_TYPE_MAP[type]
158
+ if type.is_a?(Types::TypeSerializer)
159
+ type.lib_type
160
+ else
161
+ Typemaps::C_TYPE_MAP.fetch(type)
162
+ end
163
+ rescue StandardError => e
164
+ raise "Unsupported type #{type}"
165
+ end
166
+
167
+ def error_value(type)
168
+ if type.is_a?(Types::TypeSerializer)
169
+ type.error_value
170
+ else
171
+ Typemaps::ERROR_VALUE.fetch(type)
172
+ end
173
+ rescue StandardError => e
174
+ raise "Unsupported type #{type}"
98
175
  end
99
176
 
100
- def native_type(type)
101
- Typemaps::CRYSTAL_TYPE_MAP[type]
177
+ def crystal_type(type)
178
+ if type.is_a?(Types::TypeSerializer)
179
+ type.crystal_type
180
+ else
181
+ Typemaps::CRYSTAL_TYPE_MAP.fetch(type)
182
+ end
183
+ rescue StandardError => e
184
+ raise "Unsupported type #{type}"
102
185
  end
103
186
 
104
- def convert_to_native_type(expr, outtype)
105
- Typemaps::C_TYPE_CONVERSIONS[outtype] ? Typemaps::C_TYPE_CONVERSIONS[outtype][:from] % expr : expr
187
+ def convert_lib_to_crystal_type(expr, type)
188
+ if type.is_a?(Types::TypeSerializer)
189
+ type.lib_to_crystal_type_expr(expr)
190
+ else
191
+ Typemaps::C_TYPE_CONVERSIONS[type] ? Typemaps::C_TYPE_CONVERSIONS[type][:from] % expr : expr
192
+ end
106
193
  end
107
194
 
108
- def convert_to_return_type(expr, outtype)
109
- Typemaps::C_TYPE_CONVERSIONS[outtype] ? Typemaps::C_TYPE_CONVERSIONS[outtype][:to] % expr : expr
195
+ def convert_crystal_to_lib_type(expr, type)
196
+ if type.is_a?(Types::TypeSerializer)
197
+ type.crystal_to_lib_type_expr(expr)
198
+ else
199
+ Typemaps::C_TYPE_CONVERSIONS[type] ? Typemaps::C_TYPE_CONVERSIONS[type][:to] % expr : expr
200
+ end
110
201
  end
111
202
 
112
203
  def self.instantiate_crystal_ruby!
@@ -120,10 +211,13 @@ module CrystalRuby
120
211
  raise "Missing config option `#{config_key}`. \nProvide this inside crystalruby.yaml (run `bundle exec crystalruby init` to generate this file with detaults)"
121
212
  end
122
213
  end
123
- FileUtils.mkdir_p "#{config.crystal_src_dir}/generated"
214
+ FileUtils.mkdir_p "#{config.crystal_src_dir}/#{config.crystal_codegen_dir}"
124
215
  FileUtils.mkdir_p "#{config.crystal_lib_dir}"
125
216
  unless File.exist?("#{config.crystal_src_dir}/#{config.crystal_main_file}")
126
- IO.write("#{config.crystal_src_dir}/#{config.crystal_main_file}", "require \"./generated/index\"\n")
217
+ IO.write(
218
+ "#{config.crystal_src_dir}/#{config.crystal_main_file}",
219
+ "require \"./#{config.crystal_codegen_dir}/index\"\n"
220
+ )
127
221
  end
128
222
  return if File.exist?("#{config.crystal_src_dir}/shard.yml")
129
223
 
@@ -145,42 +239,74 @@ module CrystalRuby
145
239
  !!@attached
146
240
  end
147
241
 
148
- def self.compile!
149
- return unless @block_store
242
+ def self.register_type!(type)
243
+ @types_cache ||= {}
244
+ @types_cache[type.name] = type.type_defn
245
+ end
150
246
 
151
- index_content = <<~CRYSTAL
152
- FAKE_ARG = "crystal"
153
- fun init(): Void
154
- GC.init
155
- ptr = FAKE_ARG.to_unsafe
156
- LibCrystalMain.__crystal_main(1, pointerof(ptr))
247
+ def type_modules
248
+ (@types_cache || {}).map do |type_name, expr|
249
+ typedef = ""
250
+ parts = type_name.split("::")
251
+ indent = ""
252
+ parts[0...-1].each do |part|
253
+ typedef << "#{indent} module #{part}\n"
254
+ indent += " "
157
255
  end
158
- CRYSTAL
256
+ typedef << "#{indent}alias #{parts[-1]} = #{expr}\n"
257
+ parts[0...-1].each do |_part|
258
+ indent = indent[0...-2]
259
+ typedef << "#{indent} end\n"
260
+ end
261
+ typedef
262
+ end.join("\n")
263
+ end
159
264
 
160
- index_content += @block_store.map do |function|
265
+ def self.requires
266
+ @block_store.map do |function|
161
267
  function_data = function[:body]
162
268
  file_digest = Digest::MD5.hexdigest function_data
163
269
  fname = function[:name]
164
270
  "require \"./#{function[:owner].name}/#{fname}_#{file_digest}.cr\"\n"
165
271
  end.join("\n")
272
+ end
166
273
 
167
- File.write("#{config.crystal_src_dir}/generated/index.cr", index_content)
168
- begin
169
- lib_target = "#{Dir.pwd}/#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
170
- Dir.chdir(config.crystal_src_dir) do
171
- if config.debug
172
- `crystal build -o #{lib_target} #{config.crystal_main_file}`
173
- else
174
- `crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}`
175
- end
176
- end
274
+ def self.compile!
275
+ return unless @block_store
276
+
277
+ index_content = Template.render(
278
+ Template::Index,
279
+ {
280
+ type_modules: type_modules,
281
+ requires: requires
282
+ }
283
+ )
284
+
285
+ File.write("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/index.cr", index_content)
286
+ lib_target = "#{Dir.pwd}/#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
287
+
288
+ Dir.chdir(config.crystal_src_dir) do
289
+ cmd = if config.debug
290
+ "crystal build -o #{lib_target} #{config.crystal_main_file}"
291
+ else
292
+ "crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}"
293
+ end
294
+
295
+ raise "Error compiling crystal code" unless result = system(cmd)
177
296
 
178
297
  @compiled = true
179
- rescue StandardError => e
180
- puts "Error compiling crystal code"
181
- puts e
182
- File.delete("#{config.crystal_src_dir}/generated/index.cr")
298
+ File.delete("#{config.crystal_codegen_dir}/index.cr") if File.exist?("#{config.crystal_codegen_dir}/index.cr")
183
299
  end
300
+ extend FFI::Library
301
+ ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
302
+ attach_function :attach_rb_error_handler, [:pointer], :int
303
+ const_set(:ErrorCallback, FFI::Function.new(:void, %i[string string]) do |error_type, message|
304
+ error_type = error_type.to_sym
305
+ is_exception_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception)
306
+ error_type = is_exception_type ? Object.const_get(error_type) : RuntimeError
307
+ raise error_type.new(message)
308
+ end)
309
+ attach_rb_error_handler(ErrorCallback)
184
310
  end
185
311
 
186
312
  def self.attach!
@@ -191,25 +317,27 @@ module CrystalRuby
191
317
  end
192
318
 
193
319
  def self.write_function(owner, name:, body:, &compile_callback)
194
- @compiled = File.exist?("#{config.crystal_src_dir}/generated/index.cr") unless defined?(@compiled)
320
+ unless defined?(@compiled)
321
+ @compiled = File.exist?("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/index.cr")
322
+ end
195
323
  @block_store ||= []
196
324
  @block_store << { owner: owner, name: name, body: body, compile_callback: compile_callback }
197
- FileUtils.mkdir_p("#{config.crystal_src_dir}/generated")
198
- existing = Dir.glob("#{config.crystal_src_dir}/generated/**/*.cr")
325
+ FileUtils.mkdir_p("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}")
326
+ existing = Dir.glob("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/**/*.cr")
199
327
  @block_store.each do |function|
200
328
  owner_name = function[:owner].name
201
- FileUtils.mkdir_p("#{config.crystal_src_dir}/generated/#{owner_name}")
329
+ FileUtils.mkdir_p("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/#{owner_name}")
202
330
  function_data = function[:body]
203
331
  fname = function[:name]
204
332
  file_digest = Digest::MD5.hexdigest function_data
205
- filename = "#{config.crystal_src_dir}/generated/#{owner_name}/#{fname}_#{file_digest}.cr"
333
+ filename = "#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/#{owner_name}/#{fname}_#{file_digest}.cr"
206
334
  unless existing.delete(filename)
207
335
  @compiled = false
208
336
  @attached = false
209
337
  File.write(filename, function_data)
210
338
  end
211
339
  existing.select do |f|
212
- f =~ %r{#{config.crystal_src_dir}/generated/#{owner_name}/#{fname}_[a-f0-9]{32}\.cr}
340
+ f =~ %r{#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/#{owner_name}/#{fname}_[a-f0-9]{32}\.cr}
213
341
  end.each do |fl|
214
342
  File.delete(fl) unless fl.eql?(filename)
215
343
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crystalruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-08 00:00:00.000000000 Z
11
+ date: 2024-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: digest
@@ -84,7 +84,27 @@ files:
84
84
  - exe/crystalruby
85
85
  - lib/crystalruby.rb
86
86
  - lib/crystalruby/config.rb
87
+ - lib/crystalruby/template.rb
88
+ - lib/crystalruby/templates/function.cr
89
+ - lib/crystalruby/templates/index.cr
90
+ - lib/crystalruby/typebuilder.rb
87
91
  - lib/crystalruby/typemaps.rb
92
+ - lib/crystalruby/types.rb
93
+ - lib/crystalruby/types/array.rb
94
+ - lib/crystalruby/types/bool.rb
95
+ - lib/crystalruby/types/hash.rb
96
+ - lib/crystalruby/types/named_tuple.rb
97
+ - lib/crystalruby/types/nil.rb
98
+ - lib/crystalruby/types/numbers.rb
99
+ - lib/crystalruby/types/string.rb
100
+ - lib/crystalruby/types/symbol.rb
101
+ - lib/crystalruby/types/time.rb
102
+ - lib/crystalruby/types/tuple.rb
103
+ - lib/crystalruby/types/type.rb
104
+ - lib/crystalruby/types/type_serializer.rb
105
+ - lib/crystalruby/types/type_serializer/json.rb
106
+ - lib/crystalruby/types/typedef.rb
107
+ - lib/crystalruby/types/union_type.rb
88
108
  - lib/crystalruby/version.rb
89
109
  - lib/module.rb
90
110
  - sig/crystalruby.rbs