haskell_match 0.1.0

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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE-APACHE +202 -0
  4. data/LICENSE-MIT +21 -0
  5. data/README.md +1484 -0
  6. data/ext/haskell_match/Cargo.lock +33 -0
  7. data/ext/haskell_match/Cargo.toml +22 -0
  8. data/ext/haskell_match/extconf.rb +41 -0
  9. data/ext/haskell_match/src/core/ast.rs +190 -0
  10. data/ext/haskell_match/src/core/error.rs +52 -0
  11. data/ext/haskell_match/src/core/exhaust.rs +699 -0
  12. data/ext/haskell_match/src/core/hs/ast.rs +256 -0
  13. data/ext/haskell_match/src/core/hs/json.rs +225 -0
  14. data/ext/haskell_match/src/core/hs/layout.rs +346 -0
  15. data/ext/haskell_match/src/core/hs/lexer.rs +688 -0
  16. data/ext/haskell_match/src/core/hs/mod.rs +14 -0
  17. data/ext/haskell_match/src/core/hs/parser.rs +1945 -0
  18. data/ext/haskell_match/src/core/lexer.rs +590 -0
  19. data/ext/haskell_match/src/core/mod.rs +19 -0
  20. data/ext/haskell_match/src/core/parser.rs +1116 -0
  21. data/ext/haskell_match/src/core/pretty.rs +373 -0
  22. data/ext/haskell_match/src/core/resolve.rs +336 -0
  23. data/ext/haskell_match/src/core/tree.rs +921 -0
  24. data/ext/haskell_match/src/core/typecheck.rs +226 -0
  25. data/ext/haskell_match/src/core/types.rs +404 -0
  26. data/ext/haskell_match/src/lib.rs +19 -0
  27. data/ext/haskell_match/src/ruby/mod.rs +1195 -0
  28. data/ext/haskell_match/src/ruby/runtime.rs +1045 -0
  29. data/lib/haskell_match/binding_plan.rb +84 -0
  30. data/lib/haskell_match/case_of.rb +71 -0
  31. data/lib/haskell_match/clauses.rb +354 -0
  32. data/lib/haskell_match/data.rb +417 -0
  33. data/lib/haskell_match/deep_call.rb +98 -0
  34. data/lib/haskell_match/deriving.rb +130 -0
  35. data/lib/haskell_match/dsl.rb +71 -0
  36. data/lib/haskell_match/errors.rb +85 -0
  37. data/lib/haskell_match/field_types.rb +140 -0
  38. data/lib/haskell_match/function.rb +240 -0
  39. data/lib/haskell_match/haskell/compiler.rb +961 -0
  40. data/lib/haskell_match/haskell.rb +326 -0
  41. data/lib/haskell_match/inspect.rb +45 -0
  42. data/lib/haskell_match/lazy_list.rb +210 -0
  43. data/lib/haskell_match/native_loader.rb +64 -0
  44. data/lib/haskell_match/pattern.rb +75 -0
  45. data/lib/haskell_match/pattern_ast.rb +394 -0
  46. data/lib/haskell_match/prelude.rb +448 -0
  47. data/lib/haskell_match/scope.rb +44 -0
  48. data/lib/haskell_match/version.rb +5 -0
  49. data/lib/haskell_match.rb +41 -0
  50. metadata +124 -0
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # Mixin for every constructor class produced by {HaskellMatch.data}.
5
+ # Instances are frozen `Data` values; the class knows its type module,
6
+ # constructor name and field names.
7
+ module Constructor
8
+ module ClassMethods
9
+ attr_reader :data_type, :constructor_name, :field_names, :arity
10
+
11
+ # `Just.(5)` -- Haskell constructors are functions.
12
+ def call(*args, **kwargs)
13
+ new(*args, **kwargs)
14
+ end
15
+
16
+ def to_proc
17
+ method(:new).to_proc
18
+ end
19
+
20
+ def record?
21
+ !field_names.nil?
22
+ end
23
+
24
+ def nullary?
25
+ arity.zero?
26
+ end
27
+
28
+ # The nullary singleton value, or nil for constructors with fields.
29
+ def value
30
+ @value
31
+ end
32
+
33
+ # Declared field types, as written (`["String", "Int"]`), or nil.
34
+ def field_types
35
+ @field_types
36
+ end
37
+ end
38
+
39
+ # Class-level information for Ruby classes made constructors of a type by
40
+ # {HaskellMatch.sealed}; readers only, nothing of the class's behaviour
41
+ # changes.
42
+ module SealedClassMethods
43
+ attr_reader :data_type, :constructor_name, :field_names, :arity
44
+
45
+ def record?
46
+ !field_names.nil?
47
+ end
48
+
49
+ def nullary?
50
+ arity.zero?
51
+ end
52
+
53
+ # Sealed classes have no singleton value; `constructors` lists the class.
54
+ def value
55
+ nil
56
+ end
57
+
58
+ def field_types
59
+ nil
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ # Field values in declaration order.
68
+ def fields
69
+ to_h.values
70
+ end
71
+
72
+ def [](index)
73
+ case index
74
+ when Integer then fields[index]
75
+ else public_send(index)
76
+ end
77
+ end
78
+
79
+ def constructor_name
80
+ self.class.constructor_name
81
+ end
82
+
83
+ def data_type
84
+ self.class.data_type
85
+ end
86
+
87
+ def inspect
88
+ Inspect.render(self)
89
+ end
90
+ alias to_s inspect
91
+ end
92
+
93
+ # The module returned by {HaskellMatch.data}; it holds a constant per
94
+ # constructor so `include Maybe` brings `Just` and `Nothing` into scope.
95
+ module DataType
96
+ attr_reader :type_name, :type_variables, :constructor_classes
97
+
98
+ # Constructors in declaration order: classes for constructors with
99
+ # fields, singleton values for nullary ones (classes for nullary sealed
100
+ # classes, which have no singleton).
101
+ def constructors
102
+ constructor_classes.map { |k| k.value || k }
103
+ end
104
+
105
+ def constructor_names
106
+ constructor_classes.map(&:constructor_name)
107
+ end
108
+
109
+ # `Maybe === Just.new(1)` -> true
110
+ def ===(value)
111
+ k = value.class
112
+ k.respond_to?(:data_type) && k.data_type.equal?(self)
113
+ end
114
+
115
+ # Haskell keeps types and constructors in separate namespaces; for
116
+ # `data Person = Person {...}` the type module answers `new`, `[]` and
117
+ # `call` with the same-named constructor, so `Person.new("Al", 3)` works
118
+ # whether `Person` names the type or the constructor.
119
+ def new(*args, **kwargs)
120
+ same_named_constructor.new(*args, **kwargs)
121
+ end
122
+
123
+ def [](*args)
124
+ same_named_constructor[*args]
125
+ end
126
+
127
+ def call(*args)
128
+ same_named_constructor.call(*args)
129
+ end
130
+
131
+ def same_named_constructor
132
+ k = constructor_classes.find { |c| c.constructor_name == type_name }
133
+ raise NoMethodError, "#{inspect} has no constructor named #{type_name}" if k.nil?
134
+ raise NoMethodError, "#{type_name} is a nullary constructor: use #{type_name}::#{type_name}" if k.nullary?
135
+
136
+ k
137
+ end
138
+ private :same_named_constructor
139
+
140
+ def inspect
141
+ "data #{[type_name, *type_variables].join(' ')} = " +
142
+ constructor_classes.map do |k|
143
+ if k.record?
144
+ "#{k.constructor_name} {#{k.field_names.join(', ')}}"
145
+ elsif k.constructor_name.start_with?(":") && k.arity == 2
146
+ "_ #{k.constructor_name} _"
147
+ else
148
+ [k.constructor_name, *Array.new(k.arity, "_")].join(" ")
149
+ end
150
+ end.join(" | ")
151
+ end
152
+ end
153
+
154
+ class << self
155
+ SYMBOL_WORDS = {
156
+ ":" => "Colon", "+" => "Plus", "-" => "Minus", "*" => "Star", "/" => "Slash", "<" => "Lt", ">" => "Gt",
157
+ "=" => "Eq", "!" => "Bang", "@" => "At", "#" => "Hash", "$" => "Dollar", "%" => "Percent", "&" => "Amp",
158
+ "^" => "Caret", "|" => "Bar", "~" => "Tilde", "?" => "Query", "." => "Dot", "\\" => "Backslash"
159
+ }.freeze
160
+
161
+ # The Ruby constant for a constructor: its own name, or for an infix
162
+ # constructor such as `:+:` a spelled-out one (`ColonPlusColon`).
163
+ def constructor_constant(cname)
164
+ return cname unless cname.start_with?(":")
165
+
166
+ const = cname.chars.map { |c| SYMBOL_WORDS.fetch(c) { "U#{c.ord}" } }.join
167
+ (@symbolic_constants ||= {})[const] = cname
168
+ const
169
+ end
170
+
171
+ # The Haskell name of an infix constructor from its spelled-out constant
172
+ # (`ColonPlusColon` -> `:+:`), or nil.
173
+ def symbolic_constructor(const_name)
174
+ (@symbolic_constants ||= {})[const_name.to_s]
175
+ end
176
+
177
+ # Copy the registrations of `type_names` from scope `src` into `dst`
178
+ # (the Ruby side of `Native.import_scope`).
179
+ def import_constructors(dst, src, type_names)
180
+ @constructors ||= {}
181
+ @type_modules ||= {}
182
+ (@constructors[src] || {}).each do |n, c|
183
+ k = c.is_a?(Class) ? c : c.class
184
+ (@constructors[dst] ||= {})[n] = c if k.respond_to?(:data_type) && type_names.include?(k.data_type.type_name)
185
+ end
186
+ (@type_modules[src] || {}).each do |n, m|
187
+ (@type_modules[dst] ||= {})[n] = m if type_names.include?(n)
188
+ end
189
+ end
190
+
191
+ # Constructor classes and nullary values by name, as currently registered
192
+ # (a redefined type replaces its constructors).
193
+ def constructor(name, scope = Native::GLOBAL_SCOPE)
194
+ @constructors ||= {}
195
+ (@constructors[scope] || {})[name.to_s] || (scope != Native::GLOBAL_SCOPE ? constructor(name) : nil)
196
+ end
197
+
198
+ def register_constructors(classes, scope = Native::GLOBAL_SCOPE)
199
+ @constructors ||= {}
200
+ table = (@constructors[scope] ||= {})
201
+ classes.each { |k| table[k.constructor_name] = k.value || k }
202
+ end
203
+
204
+ # The type module registered under `name` (in `scope`, falling back to
205
+ # the global scope), or nil.
206
+ def type_module(name, scope = Native::GLOBAL_SCOPE)
207
+ @type_modules ||= {}
208
+ (@type_modules[scope] || {})[name.to_s] || (scope != Native::GLOBAL_SCOPE ? type_module(name) : nil)
209
+ end
210
+
211
+ # Whether `HaskellMatch.data` checks field types by default (see
212
+ # {FieldTypes}); off unless set.
213
+ attr_writer :check_field_types
214
+
215
+ def check_field_types
216
+ @check_field_types ? true : false
217
+ end
218
+
219
+ # Declare an algebraic data type.
220
+ #
221
+ # HaskellMatch.data "Maybe a = Nothing | Just a"
222
+ # HaskellMatch.data "Shape = Circle Double | Rect Double Double"
223
+ # HaskellMatch.data "Person = Person { name :: String, age :: Int }"
224
+ # HaskellMatch.data :Maybe, Nothing: 0, Just: 1 # arities
225
+ # HaskellMatch.data :Person, Person: { name: :String, age: :Int }
226
+ #
227
+ # Returns a module with one constant per constructor and defines it as a
228
+ # constant named after the type under `under` (default: `Object`, i.e. a
229
+ # top-level constant, like a Haskell `data` declaration). Pass
230
+ # `under: nil` to skip the constant definition. `scope:` registers the
231
+ # type in a type scope other than the global one (see {Native::GLOBAL_SCOPE}
232
+ # and {Haskell#haskell_scope}). Constructors with fields
233
+ # are `Data` subclasses (`Just.new(1)`, `Just[1]`, `Just.(1)`); nullary
234
+ # constructors are frozen singleton values (`Nothing`).
235
+ def data(decl, under: Object, scope: Native::GLOBAL_SCOPE, check_types: check_field_types, **constructors)
236
+ name, tyvars, specs, deriving = normalize_data(decl, constructors)
237
+ validate_type_name!(name)
238
+ Deriving.validate!(name, specs, deriving)
239
+ mod = new_type_module(name, tyvars, under)
240
+
241
+ classes = specs.map do |cname, arity, fields, types|
242
+ members = fields ? fields.map(&:to_sym) : (1..arity).map { |i| :"_#{i}" }
243
+ klass = Data.define(*members)
244
+ klass.include(Constructor)
245
+ klass.instance_variable_set(:@data_type, mod)
246
+ klass.instance_variable_set(:@constructor_name, cname)
247
+ klass.instance_variable_set(:@field_names, fields&.map(&:to_sym))
248
+ klass.instance_variable_set(:@arity, arity)
249
+ klass.instance_variable_set(:@field_types, types&.map(&:to_s))
250
+ const = constructor_constant(cname)
251
+ mod.const_set(const, klass)
252
+ klass.name # cache the permanent name
253
+ FieldTypes.install(klass, types.map(&:to_s), scope) if check_types && types && types.size == arity && arity.positive?
254
+ if arity.zero?
255
+ value = klass.new.freeze
256
+ klass.instance_variable_set(:@value, value)
257
+ klass.singleton_class.send(:undef_method, :new)
258
+ klass.singleton_class.send(:undef_method, :[])
259
+ klass.singleton_class.send(:undef_method, :call)
260
+ mod.send(:remove_const, const)
261
+ mod.const_set(const, value)
262
+ end
263
+ klass
264
+ end
265
+ finish_type(mod, classes, under, scope)
266
+ Deriving.apply(mod, classes, deriving)
267
+ mod
268
+ end
269
+
270
+ # Make existing Ruby classes the constructors of a closed type, so their
271
+ # instances match constructor patterns with full exhaustiveness checking
272
+ # and no `HaskellMatch.data` declaration:
273
+ #
274
+ # Circle = Data.define(:r)
275
+ # Rect = Data.define(:w, :h)
276
+ # HaskellMatch.sealed :Shape, Circle, Rect
277
+ # area = HaskellMatch.fn(:area) { on(Circle(r)) { |r| 3 * r * r }; on("Rect w h") { |w, h| w * h } }
278
+ #
279
+ # `Data` and `Struct` classes supply their own field names (so record
280
+ # patterns `Circle { r = x }` work too); for any other class pass the
281
+ # reader methods that are its fields:
282
+ #
283
+ # HaskellMatch.sealed :Shape, Circle => [:r], Rect => [:w, :h]
284
+ #
285
+ # The constructor name is the class's own name (`Geometry::Circle` is the
286
+ # constructor `Circle`). Instances are identified by exact class, so list
287
+ # the leaf classes of a hierarchy. Returns the type module, defined as a
288
+ # constant under `under` when given (default: none).
289
+ def sealed(name, *classes, under: nil, scope: Native::GLOBAL_SCOPE, **with_fields)
290
+ name = name.to_s
291
+ validate_type_name!(name)
292
+ with_fields = with_fields.merge(classes.pop) if classes.last.is_a?(Hash)
293
+ entries = classes.map { |k| [k, nil] } + with_fields.map { |k, f| [k, Array(f)] }
294
+ raise DataDeclarationError, "type '#{name}' must have at least one constructor class" if entries.empty?
295
+
296
+ mod = new_type_module(name, [], under)
297
+ cons = entries.map do |klass, fields|
298
+ raise DataDeclarationError, "#{klass.inspect} is not a Class" unless klass.is_a?(Class)
299
+
300
+ cname = klass.name&.split("::")&.last
301
+ if cname.nil? || !cname.match?(/\A[A-Z][A-Za-z0-9_']*\z/)
302
+ raise DataDeclarationError, "#{klass.inspect} needs a constant name starting with an upper-case letter"
303
+ end
304
+ if klass.singleton_class.include?(Constructor::SealedClassMethods) || klass.include?(Constructor)
305
+ raise DataDeclarationError, "#{klass} is already a constructor of type '#{klass.data_type.type_name}'"
306
+ end
307
+
308
+ fields ||= klass.members.map(&:to_s) if klass.respond_to?(:members)
309
+ if fields.nil?
310
+ raise DataDeclarationError,
311
+ "#{klass} is neither a Data nor a Struct class: list its field readers (#{cname} => [:a, :b])"
312
+ end
313
+ fields = fields.map(&:to_s)
314
+ klass.extend(Constructor::SealedClassMethods)
315
+ klass.instance_variable_set(:@data_type, mod)
316
+ klass.instance_variable_set(:@constructor_name, cname)
317
+ klass.instance_variable_set(:@field_names, fields.map(&:to_sym))
318
+ klass.instance_variable_set(:@arity, fields.size)
319
+ mod.const_set(cname, klass)
320
+ klass
321
+ end
322
+ begin
323
+ finish_type(mod, cons, under, scope)
324
+ rescue CompileError
325
+ cons.each do |k|
326
+ %i[@data_type @constructor_name @field_names @arity].each { |iv| k.remove_instance_variable(iv) }
327
+ k.singleton_class.send(:undef_method, *Constructor::SealedClassMethods.instance_methods(false)) rescue nil # rubocop:disable Style/RescueModifier
328
+ end
329
+ raise
330
+ end
331
+ mod
332
+ end
333
+
334
+ private
335
+
336
+ def new_type_module(name, tyvars, under)
337
+ mod = Module.new
338
+ mod.extend(DataType)
339
+ mod.instance_variable_set(:@type_name, name)
340
+ mod.instance_variable_set(:@type_variables, tyvars)
341
+ mod.instance_variable_set(:@constructor_classes, [])
342
+ # Name the module first so nullary constructor classes keep a permanent
343
+ # class path even after their constant is replaced by the singleton.
344
+ home = under || Types
345
+ home.send(:remove_const, name) if home.const_defined?(name, false)
346
+ home.const_set(name, mod)
347
+ mod
348
+ end
349
+
350
+ # Register the type natively and in the Ruby registries; on failure the
351
+ # constant is removed again.
352
+ def finish_type(mod, classes, under, scope)
353
+ mod.instance_variable_get(:@constructor_classes).concat(classes).freeze
354
+ name = mod.type_name
355
+ begin
356
+ Native.register_type(name, classes.map { |k| [k.constructor_name, k.arity, k.field_names&.map(&:to_s), k] }, scope)
357
+ rescue CompileError
358
+ home = under || Types
359
+ home.send(:remove_const, name) if home.const_defined?(name, false) && home.const_get(name, false).equal?(mod)
360
+ raise
361
+ end
362
+ register_constructors(classes, scope)
363
+ @type_modules ||= {}
364
+ (@type_modules[scope] ||= {})[name] = mod
365
+ mod
366
+ end
367
+
368
+ def normalize_data(decl, constructors)
369
+ if decl.is_a?(String)
370
+ unless constructors.empty?
371
+ raise ArgumentError, "pass either a declaration string or constructor keywords, not both"
372
+ end
373
+ name, tyvars, cons, deriving = Native.parse_data(decl)
374
+ [name, tyvars, cons, deriving]
375
+ else
376
+ name = decl.to_s
377
+ deriving = Array(constructors.delete(:deriving)).map(&:to_s)
378
+ if constructors.empty?
379
+ raise DataDeclarationError, "type '#{name}' must have at least one constructor"
380
+ end
381
+ specs = constructors.map do |cname, spec|
382
+ case spec
383
+ when Integer
384
+ raise DataDeclarationError, "arity of '#{cname}' must not be negative" if spec.negative?
385
+
386
+ [cname.to_s, spec, nil, []]
387
+ when Array
388
+ [cname.to_s, spec.size, nil, spec.map(&:to_s)]
389
+ when Hash
390
+ fields = spec.keys.map(&:to_s)
391
+ fields.each do |f|
392
+ unless f.match?(/\A[a-z_][A-Za-z0-9_']*\z/)
393
+ raise DataDeclarationError, "field name '#{f}' must start with a lower-case letter"
394
+ end
395
+ end
396
+ [cname.to_s, fields.size, fields, spec.values.map(&:to_s)]
397
+ when nil
398
+ [cname.to_s, 0, nil, []]
399
+ else
400
+ raise DataDeclarationError,
401
+ "constructor '#{cname}' must be given an arity, an Array of field types or a Hash of fields"
402
+ end
403
+ end
404
+ [name, [], specs, deriving]
405
+ end
406
+ end
407
+
408
+ def validate_type_name!(name)
409
+ return if name.match?(/\A[A-Z][A-Za-z0-9_']*\z/)
410
+
411
+ raise DataDeclarationError, "type name '#{name}' must start with an upper-case letter"
412
+ end
413
+ end
414
+
415
+ # Home for types declared with `under: nil`.
416
+ module Types; end
417
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # Deep mode: `call` implemented in Ruby, so that while a recursion is
5
+ # pending no native frame sits on the machine stack. Each level then costs
6
+ # only its Ruby VM frames (a few hundred bytes) and the GC scans only those,
7
+ # which is what makes very deep recursion cheap. The price is one extra
8
+ # Ruby method frame per call (see the README's expert notes).
9
+ #
10
+ # The native `prepare` does the matching, counts the nesting level and
11
+ # returns `[values..., body, depth]`; the generated `call` invokes the body
12
+ # (on a fresh Fiber at segment boundaries), pops the level, and hands any
13
+ # `tail`/`defer` marker to the trampoline below, which has the same
14
+ # semantics as the native `call`.
15
+ module DeepCall
16
+ CONT_CHUNK = 256
17
+
18
+ class << self
19
+ # Mirrors HaskellMatch.stack_segment (kept here to avoid a native call
20
+ # per invocation).
21
+ attr_accessor :segment
22
+ end
23
+ self.segment = Native.stack_segment
24
+
25
+ # Generate a `call` of exact arity (avoids a splat allocation per call).
26
+ def self.module_for(arity)
27
+ (@modules ||= {})[arity] ||= begin
28
+ params = (1..arity).map { |i| "a#{i}" }.join(", ")
29
+ Module.new.tap do |m|
30
+ m.module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
31
+ def call(#{params})
32
+ vals = prepare(#{params})
33
+ depth = vals.pop
34
+ body = vals.pop
35
+ result = begin
36
+ seg = DeepCall.segment
37
+ if seg > 0 && depth % seg == 0
38
+ Fiber.new { body.call(*vals) }.resume
39
+ else
40
+ body.call(*vals)
41
+ end
42
+ ensure
43
+ Native.depth_pop
44
+ end
45
+ TailCall === result ? DeepCall.trampoline(result) : result
46
+ end
47
+
48
+ def deep?
49
+ true
50
+ end
51
+ RUBY
52
+ end
53
+ end
54
+ end
55
+
56
+ # Run `body` with `vals`, on a fresh Fiber at segment boundaries.
57
+ def self.invoke(body, vals, depth)
58
+ seg = segment
59
+ if seg > 0 && (depth % seg).zero?
60
+ Fiber.new { body.call(*vals) }.resume
61
+ else
62
+ body.call(*vals)
63
+ end
64
+ ensure
65
+ Native.depth_pop
66
+ end
67
+
68
+ # Continue a call whose body returned a `TailCall` marker: apply tail
69
+ # calls on the spot and keep `defer` continuations on a chunked stack.
70
+ def self.trampoline(result)
71
+ conts = nil
72
+ while true
73
+ if TailCall === result
74
+ if (k = result.continuation)
75
+ conts = [conts] if conts.nil? || conts.size > CONT_CHUNK
76
+ conts << k
77
+ end
78
+ vals = result.function.prepare(*result.args)
79
+ depth = vals.pop
80
+ body = vals.pop
81
+ result = invoke(body, vals, depth)
82
+ else
83
+ k = nil
84
+ while conts
85
+ if conts.size > 1
86
+ k = conts.pop
87
+ break
88
+ end
89
+ conts = conts[0]
90
+ end
91
+ return result unless k
92
+
93
+ result = k.call(result)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # `deriving (...)` on a data declaration. `Eq` and `Show` always hold for
5
+ # constructor values (structural equality and Haskell-style `inspect`);
6
+ # `Ord`, `Enum` and `Bounded` add the corresponding behaviour:
7
+ #
8
+ # HaskellMatch.data "Color = Red | Green | Blue deriving (Eq, Ord, Enum, Bounded)"
9
+ # Red < Blue # => true (Ord: by constructor order, then fields)
10
+ # Red.succ # => Green (Enum)
11
+ # (Red..Blue).to_a # => [Red, Green, Blue]
12
+ # Color.enum_from(Green) # => [Green, Blue]
13
+ # Color.min_bound # => Red (Bounded)
14
+ module Deriving
15
+ SUPPORTED = %w[Eq Show Ord Enum Bounded].freeze
16
+
17
+ module_function
18
+
19
+ # Reject unsupported classes and ill-typed derivations up front, before
20
+ # the type is registered (`specs` are [name, arity, fields, types] rows).
21
+ def validate!(type_name, specs, deriving)
22
+ deriving.each do |cls|
23
+ cls = cls.to_s
24
+ unless SUPPORTED.include?(cls)
25
+ raise DataDeclarationError,
26
+ "cannot derive '#{cls}' for type '#{type_name}' (supported: #{SUPPORTED.join(', ')})"
27
+ end
28
+ next unless %w[Enum Bounded].include?(cls) && specs.any? { |(_, arity, *)| arity.positive? }
29
+
30
+ raise DataDeclarationError,
31
+ "cannot derive '#{cls}' for type '#{type_name}': it must be an enumeration type (all constructors nullary)"
32
+ end
33
+ end
34
+
35
+ def apply(mod, classes, deriving)
36
+ deriving.each do |cls|
37
+ case cls.to_s
38
+ when "Eq", "Show" then nil
39
+ when "Ord" then ord(mod, classes)
40
+ when "Enum" then enum(mod, classes)
41
+ when "Bounded" then bounded(mod, classes)
42
+ else
43
+ raise DataDeclarationError,
44
+ "cannot derive '#{cls}' for type '#{mod.type_name}' (supported: #{SUPPORTED.join(', ')})"
45
+ end
46
+ end
47
+ end
48
+
49
+ # Values compare by constructor order first, then field by field.
50
+ def ord(mod, classes)
51
+ tags = classes.each_with_index.to_h
52
+ type = mod
53
+ classes.each do |k|
54
+ k.include(Comparable)
55
+ k.define_method(:<=>) do |other|
56
+ return nil unless other.is_a?(Constructor) && other.class.data_type.equal?(type)
57
+
58
+ c = tags[self.class] <=> tags[other.class]
59
+ return c unless c.zero?
60
+
61
+ fields.zip(other.fields).each do |a, b|
62
+ r = a <=> b
63
+ return r if r.nil? || r != 0
64
+ end
65
+ 0
66
+ end
67
+ end
68
+ end
69
+
70
+ # `succ`, `pred`, ranges and `Type.enum_from*`; only for enumerations
71
+ # (every constructor nullary), as in GHC.
72
+ def enum(mod, classes)
73
+ unless classes.all?(&:nullary?)
74
+ raise DataDeclarationError,
75
+ "cannot derive 'Enum' for type '#{mod.type_name}': it must be an enumeration type (all constructors nullary)"
76
+ end
77
+ values = classes.map(&:value)
78
+ type_name = mod.type_name
79
+ values.each_with_index do |v, i|
80
+ k = v.class
81
+ k.define_method(:succ) do
82
+ values[i + 1] or raise ArgumentError, "#{type_name}.succ: bad argument (#{self} is the last value)"
83
+ end
84
+ k.define_method(:pred) do
85
+ i.positive? ? values[i - 1] : raise(ArgumentError, "#{type_name}.pred: bad argument (#{self} is the first value)")
86
+ end
87
+ k.define_method(:from_enum) { i }
88
+ k.alias_method(:to_i, :from_enum)
89
+ next if k.method_defined?(:<=>, false)
90
+
91
+ k.include(Comparable)
92
+ k.define_method(:<=>) do |other|
93
+ other.is_a?(Constructor) && other.class.data_type.equal?(mod) ? i <=> other.from_enum : nil
94
+ end
95
+ end
96
+ mod.define_singleton_method(:values) { values }
97
+ mod.define_singleton_method(:to_enum) do |i|
98
+ values[i] or raise ArgumentError, "#{type_name}.to_enum: bad argument #{i.inspect} (0..#{values.size - 1})"
99
+ end
100
+ mod.define_singleton_method(:from_enum) { |v| v.from_enum }
101
+ mod.define_singleton_method(:enum_from) { |v| values[v.from_enum..] }
102
+ mod.define_singleton_method(:enum_from_to) { |a, b| a > b ? [] : values[a.from_enum..b.from_enum] }
103
+ mod.define_singleton_method(:enum_from_then) do |a, b|
104
+ step = b.from_enum - a.from_enum
105
+ raise ArgumentError, "#{type_name}.enum_from_then: the two values must differ" if step.zero?
106
+
107
+ (a.from_enum..(step.positive? ? values.size - 1 : 0)).step(step.abs).map { |i| values[i] }
108
+ end
109
+ mod.define_singleton_method(:enum_from_then_to) do |a, b, c|
110
+ step = b.from_enum - a.from_enum
111
+ raise ArgumentError, "#{type_name}.enum_from_then_to: the first two values must differ" if step.zero?
112
+
113
+ a.from_enum.step(c.from_enum, step).map { |i| values[i] }
114
+ end
115
+ end
116
+
117
+ # `Type.min_bound` / `Type.max_bound`: the first and last constructor of
118
+ # an enumeration.
119
+ def bounded(mod, classes)
120
+ unless classes.all?(&:nullary?)
121
+ raise DataDeclarationError,
122
+ "cannot derive 'Bounded' for type '#{mod.type_name}': only enumeration types are supported"
123
+ end
124
+ first = classes.first.value
125
+ last = classes.last.value
126
+ mod.define_singleton_method(:min_bound) { first }
127
+ mod.define_singleton_method(:max_bound) { last }
128
+ end
129
+ end
130
+ end