crystalruby 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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