crystalruby 0.1.2 → 0.1.3

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: 157d03e574c1ef5e06073e71c29e259741c26373113f2ae51c692b4aaef429f9
4
+ data.tar.gz: 94c1f670b52a39b78c961a3050c2587c540a1cb1593c30e87d6bd05d90a4d736
5
5
  SHA512:
6
- metadata.gz: 54ca6d542370d360f1f48959e4edf5a4308985d8e952317e2780b980c068674cb56c257c9d8241d0fc929bb152ca9960a8c7397f90c08725acf9bd268f794842
7
- data.tar.gz: 303d6c379501dcf8966dd3941f2e667f35efeec67b24eb68bf2965abee06e5afa4b631c7b23e7493e4a3e088f650bd5a85271ae2bedb18b7a0120ca4c4bc605c
6
+ metadata.gz: 1b7972639977a290fd9a42b737edd1f3eec28a240a6e0e773619190bf646ec0707520d317ddf6e6152f507753b16fa66db0d2903c18fd23e58ebce4422e02555
7
+ data.tar.gz: bc3d03de05c21a14a6d6d9b6de76ff04efcd660999c0dd2c166cc306707d0f93be01053e34ba8a531c9e0a9154e887bb1c6796b141564e290e25d537eeab88b4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.3] - 2024-04-10
4
+
5
+ - Support exceptions thrown in Crystal being caught in Ruby
6
+ - Support complex Ruby type passing (while preserving type checking), using `JSON` as serialization format.
7
+
3
8
  ## [0.1.0] - 2024-04-07
4
9
 
5
10
  - 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,54 @@ 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
+ ## Exceptions
200
+
201
+ Exceptions thrown in Crystal code can be caught in Ruby.
202
+
203
+ ## Installing shards and writing non-embedded Crystal code
155
204
 
156
205
  You can use any Crystal shards and write ordinary, stand-alone Crystal code.
157
206
 
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,37 @@
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
+ return type.interpret!(raw)
31
+ end
32
+ end
33
+ raise "Invalid deserialized value #{raw} for type #{inspect}"
34
+ end
35
+ end
36
+ end
37
+ 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.3"
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,103 @@ 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{|k, arg_type| "#{k} = #{arg_type[:convert_lib_to_crystal_type]["_#{k}"]}" }.join("\n "),
116
+ arg_names: args.keys.join(","),
117
+ convert_return_type: return_type[:convert_crystal_to_lib_type]["return_value"],
118
+ error_value: return_type[:error_value]
119
+ }
120
+ )
121
+ {
122
+ name: name,
123
+ body: function_body,
124
+ retval_map: returns.is_a?(Types::TypeSerializer) ? ->(rv) { returns.prepare_retval(rv) } : nil,
125
+ ffi_types: arg_types.map { |_k, arg_type| arg_type[:ffi_type] },
126
+ arg_maps: arg_types.map { |_k, arg_type| arg_type[:mapper] },
127
+ return_ffi_type: return_type[:return_ffi_type]
128
+ }
129
+ end
83
130
 
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
131
+ def build_type_map(crystalruby_type)
132
+ if crystalruby_type.is_a?(Types::TypeSerializer) && !crystalruby_type.anonymous?
133
+ CrystalRuby.register_type!(crystalruby_type)
134
+ end
89
135
 
90
136
  {
91
- name: fnname,
92
- body: function_body
137
+ ffi_type: ffi_type(crystalruby_type),
138
+ return_ffi_type: ffi_type(crystalruby_type),
139
+ crystal_type: crystal_type(crystalruby_type),
140
+ lib_type: lib_type(crystalruby_type),
141
+ error_value: error_value(crystalruby_type),
142
+ mapper: crystalruby_type.is_a?(Types::TypeSerializer) ? ->(arg) { crystalruby_type.prepare_argument(arg) } : nil,
143
+ convert_crystal_to_lib_type: ->(expr) { convert_crystal_to_lib_type(expr, crystalruby_type) },
144
+ convert_lib_to_crystal_type: ->(expr) { convert_lib_to_crystal_type(expr, crystalruby_type) }
93
145
  }
94
146
  end
95
147
 
148
+ def ffi_type(type)
149
+ case type
150
+ when Symbol then type
151
+ when Types::TypeSerializer then type.ffi_type
152
+ end
153
+ end
154
+
96
155
  def lib_type(type)
97
- Typemaps::C_TYPE_MAP[type]
156
+ if type.is_a?(Types::TypeSerializer)
157
+ type.lib_type
158
+ else
159
+ Typemaps::C_TYPE_MAP.fetch(type)
160
+ end
161
+ rescue StandardError => e
162
+ raise "Unsupported type #{type}"
98
163
  end
99
164
 
100
- def native_type(type)
101
- Typemaps::CRYSTAL_TYPE_MAP[type]
165
+ def error_value(type)
166
+ if type.is_a?(Types::TypeSerializer)
167
+ type.error_value
168
+ else
169
+ Typemaps::ERROR_VALUE.fetch(type)
170
+ end
171
+ rescue StandardError => e
172
+ raise "Unsupported type #{type}"
102
173
  end
103
174
 
104
- def convert_to_native_type(expr, outtype)
105
- Typemaps::C_TYPE_CONVERSIONS[outtype] ? Typemaps::C_TYPE_CONVERSIONS[outtype][:from] % expr : expr
175
+ def crystal_type(type)
176
+ if type.is_a?(Types::TypeSerializer)
177
+ type.crystal_type
178
+ else
179
+ Typemaps::CRYSTAL_TYPE_MAP.fetch(type)
180
+ end
181
+ rescue StandardError => e
182
+ raise "Unsupported type #{type}"
183
+ end
184
+
185
+ def convert_lib_to_crystal_type(expr, type)
186
+ if type.is_a?(Types::TypeSerializer)
187
+ type.lib_to_crystal_type_expr(expr)
188
+ else
189
+ Typemaps::C_TYPE_CONVERSIONS[type] ? Typemaps::C_TYPE_CONVERSIONS[type][:from] % expr : expr
190
+ end
106
191
  end
107
192
 
108
- def convert_to_return_type(expr, outtype)
109
- Typemaps::C_TYPE_CONVERSIONS[outtype] ? Typemaps::C_TYPE_CONVERSIONS[outtype][:to] % expr : expr
193
+ def convert_crystal_to_lib_type(expr, type)
194
+ if type.is_a?(Types::TypeSerializer)
195
+ type.crystal_to_lib_type_expr(expr)
196
+ else
197
+ Typemaps::C_TYPE_CONVERSIONS[type] ? Typemaps::C_TYPE_CONVERSIONS[type][:to] % expr : expr
198
+ end
110
199
  end
111
200
 
112
201
  def self.instantiate_crystal_ruby!
@@ -120,10 +209,10 @@ module CrystalRuby
120
209
  raise "Missing config option `#{config_key}`. \nProvide this inside crystalruby.yaml (run `bundle exec crystalruby init` to generate this file with detaults)"
121
210
  end
122
211
  end
123
- FileUtils.mkdir_p "#{config.crystal_src_dir}/generated"
212
+ FileUtils.mkdir_p "#{config.crystal_src_dir}/#{config.crystal_codegen_dir}"
124
213
  FileUtils.mkdir_p "#{config.crystal_lib_dir}"
125
214
  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")
215
+ IO.write("#{config.crystal_src_dir}/#{config.crystal_main_file}", "require \"./#{config.crystal_codegen_dir}/index\"\n")
127
216
  end
128
217
  return if File.exist?("#{config.crystal_src_dir}/shard.yml")
129
218
 
@@ -145,42 +234,75 @@ module CrystalRuby
145
234
  !!@attached
146
235
  end
147
236
 
148
- def self.compile!
149
- return unless @block_store
237
+ def self.register_type!(type)
238
+ @types_cache ||= {}
239
+ @types_cache[type.name] = type.type_defn
240
+ end
150
241
 
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))
242
+ def type_modules
243
+ (@types_cache || {}).map do |type_name, expr|
244
+ typedef = ""
245
+ parts = type_name.split("::")
246
+ indent = ""
247
+ parts[0...-1].each do |part|
248
+ typedef << "#{indent} module #{part}\n"
249
+ indent += " "
157
250
  end
158
- CRYSTAL
251
+ typedef << "#{indent}alias #{parts[-1]} = #{expr}\n"
252
+ parts[0...-1].each do |_part|
253
+ indent = indent[0...-2]
254
+ typedef << "#{indent} end\n"
255
+ end
256
+ typedef
257
+ end.join("\n")
258
+ end
159
259
 
160
- index_content += @block_store.map do |function|
260
+ def self.requires
261
+ @block_store.map do |function|
161
262
  function_data = function[:body]
162
263
  file_digest = Digest::MD5.hexdigest function_data
163
264
  fname = function[:name]
164
265
  "require \"./#{function[:owner].name}/#{fname}_#{file_digest}.cr\"\n"
165
266
  end.join("\n")
267
+ end
166
268
 
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
269
+ def self.compile!
270
+ return unless @block_store
271
+
272
+ index_content = Template.render(
273
+ Template::Index,
274
+ {
275
+ type_modules: type_modules,
276
+ requires: requires
277
+ }
278
+ )
279
+
280
+ File.write("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/index.cr", index_content)
281
+ lib_target = "#{Dir.pwd}/#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
282
+
283
+ Dir.chdir(config.crystal_src_dir) do
284
+ cmd = if config.debug
285
+ "crystal build -o #{lib_target} #{config.crystal_main_file}"
286
+ else
287
+ "crystal build --release --no-debug -o #{lib_target} #{config.crystal_main_file}"
288
+ end
289
+
290
+ raise "Error compiling crystal code" unless result = system(cmd)
177
291
 
178
292
  @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")
293
+ File.delete("#{config.crystal_codegen_dir}/index.cr") if File.exist?("#{config.crystal_codegen_dir}/index.cr")
183
294
  end
295
+ extend FFI::Library
296
+ ffi_lib "#{config.crystal_lib_dir}/#{config.crystal_lib_name}"
297
+ attach_function :attach_rb_error_handler, [:pointer], :int
298
+ const_set(:ErrorCallback, FFI::Function.new(:void, [:string, :string]) do |error_type, message|
299
+ error_type = error_type.to_sym
300
+ error_type = Object.const_defined?(error_type) && Object.const_get(error_type).ancestors.include?(Exception) ?
301
+ Object.const_get(error_type) :
302
+ RuntimeError
303
+ raise error_type.new(message)
304
+ end)
305
+ attach_rb_error_handler(ErrorCallback)
184
306
  end
185
307
 
186
308
  def self.attach!
@@ -191,25 +313,25 @@ module CrystalRuby
191
313
  end
192
314
 
193
315
  def self.write_function(owner, name:, body:, &compile_callback)
194
- @compiled = File.exist?("#{config.crystal_src_dir}/generated/index.cr") unless defined?(@compiled)
316
+ @compiled = File.exist?("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/index.cr") unless defined?(@compiled)
195
317
  @block_store ||= []
196
318
  @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")
319
+ FileUtils.mkdir_p("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}")
320
+ existing = Dir.glob("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/**/*.cr")
199
321
  @block_store.each do |function|
200
322
  owner_name = function[:owner].name
201
- FileUtils.mkdir_p("#{config.crystal_src_dir}/generated/#{owner_name}")
323
+ FileUtils.mkdir_p("#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/#{owner_name}")
202
324
  function_data = function[:body]
203
325
  fname = function[:name]
204
326
  file_digest = Digest::MD5.hexdigest function_data
205
- filename = "#{config.crystal_src_dir}/generated/#{owner_name}/#{fname}_#{file_digest}.cr"
327
+ filename = "#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/#{owner_name}/#{fname}_#{file_digest}.cr"
206
328
  unless existing.delete(filename)
207
329
  @compiled = false
208
330
  @attached = false
209
331
  File.write(filename, function_data)
210
332
  end
211
333
  existing.select do |f|
212
- f =~ %r{#{config.crystal_src_dir}/generated/#{owner_name}/#{fname}_[a-f0-9]{32}\.cr}
334
+ f =~ %r{#{config.crystal_src_dir}/#{config.crystal_codegen_dir}/#{owner_name}/#{fname}_[a-f0-9]{32}\.cr}
213
335
  end.each do |fl|
214
336
  File.delete(fl) unless fl.eql?(filename)
215
337
  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.3
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