crystalruby 0.1.2 → 0.1.3

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: 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