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,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # A single compiled pattern, usable on its own:
5
+ #
6
+ # p = HaskellMatch.pattern("Just (x:_)")
7
+ # p.match(Just.new([1, 2])) # => { x: 1 }
8
+ # p.match(Nothing) # => nil
9
+ # p === Just.new([1]) # => true (so it works in case/when)
10
+ class Pattern
11
+ attr_reader :source, :names
12
+
13
+ def initialize(source = nil, scope: Native::GLOBAL_SCOPE, &block)
14
+ if block
15
+ raise ArgumentError, "pass either a pattern string or a block, not both" if source
16
+
17
+ source = PatternAST.render(ClauseBuilder.new(block.binding.receiver).instance_exec(&block))
18
+ end
19
+ raise ArgumentError, "pattern must be a String" unless source.is_a?(String)
20
+
21
+ @source = source.dup.freeze
22
+ @scope = scope
23
+ resolver = block && ClauseBuilder.new(block.binding.receiver).constant_resolver
24
+ @matcher = Compiler.build_matcher(Native::Matcher, "pattern #{source.inspect}", [[source]], [false], 1, scope,
25
+ resolver: resolver)
26
+ @names = @matcher.names.first.map(&:to_sym).freeze
27
+ @exhaustive = @matcher.instance_variable_get(:@missing).empty?
28
+ freeze
29
+ end
30
+
31
+ # Bindings as a Hash, or nil when the value does not match. Raises
32
+ # {TypeMismatchError} when the value is of the wrong type altogether.
33
+ def match(value)
34
+ result = @matcher.select(value)
35
+ return nil unless result
36
+
37
+ @names.zip(result[1]).to_h
38
+ end
39
+
40
+ # Like {#match} but raises {MatchError} on failure.
41
+ def match!(value)
42
+ match(value) or raise MatchError, "#{value.inspect} does not match pattern #{@source}"
43
+ end
44
+
45
+ # Does the value match? Unlike {#match}, a value of the wrong type is
46
+ # simply a non-match, so the pattern can be used in `case`/`when`.
47
+ def ===(value)
48
+ !@matcher.select(value).nil?
49
+ rescue TypeMismatchError
50
+ false
51
+ end
52
+
53
+ # Whether the pattern matches every value of its type (irrefutable).
54
+ def irrefutable?
55
+ @exhaustive
56
+ end
57
+
58
+ def to_proc
59
+ method(:match).to_proc
60
+ end
61
+
62
+ def inspect
63
+ "#<HaskellMatch::Pattern #{@source}>"
64
+ end
65
+ alias to_s inspect
66
+ end
67
+
68
+ class << self
69
+ # `HaskellMatch.pattern("Just (x:_)")` or `HaskellMatch.pattern { Just([x, *_]) }`
70
+ def pattern(source = nil, scope: Native::GLOBAL_SCOPE, &block)
71
+ Pattern.new(source, scope: scope, &block)
72
+ end
73
+ alias [] pattern
74
+ end
75
+ end
@@ -0,0 +1,394 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # Patterns written in place inside a definition block, without quotes.
5
+ # Each expression builds the pattern tree directly in Ruby and is rendered
6
+ # to Haskell pattern syntax, then compiled exactly like a quoted pattern, so
7
+ # matching, exhaustiveness checking and error messages are identical.
8
+ #
9
+ # on([]) { 0 }
10
+ # on([x, *xs]) { |x, xs| ... } # (x:xs); also cons(x, xs)
11
+ # on(Just(Just(_))) { ... } # or Just[Just[_]]
12
+ # on(Person(name: n)) { |n| n } # Person { name = n }
13
+ # on(Person(name: n, **_)) { |n| n } # Person { name = n, .. }
14
+ # on(tuple(a, b)) { |a, b| a + b } # (a, b)
15
+ # on(as(all, [x, *_])) { |all, x| ... } # all@(x:_)
16
+ # on(lazy(tuple(a, b))) { ... } # ~(a, b)
17
+ # on(0) ... on(1.5) ... on(str("abc")) ... on(char("c")) ... on(:sym) ... on(true)
18
+ #
19
+ # A String given directly to `on` is quoted Haskell pattern syntax; inside a
20
+ # pattern (`Just("abc")`) a Ruby String is a string literal, and `str("abc")`
21
+ # makes one at the top level.
22
+ # Bare lower-case names are variables (use `var(:name)` when the name is a
23
+ # method of the enclosing object or a local variable), `_` is the wildcard.
24
+ # Quoted and in-place patterns may be mixed freely.
25
+ module PatternAST
26
+ class Node
27
+ # `*node` inside an array literal marks the rest of a list.
28
+ def to_a
29
+ [Rest.new(self)]
30
+ end
31
+
32
+ # `**_` inside a record pattern stands for `..`.
33
+ def to_hash
34
+ { REST_KEY => true }
35
+ end
36
+
37
+ def inspect
38
+ "#<HaskellMatch pattern #{to_haskell}>"
39
+ end
40
+ alias to_s inspect
41
+ end
42
+
43
+ REST_KEY = :"haskell_match.rest"
44
+
45
+ class Var < Node
46
+ attr_reader :name
47
+
48
+ def initialize(name)
49
+ @name = name.to_s
50
+ unless @name.match?(/\A[a-z_][A-Za-z0-9_']*\z/)
51
+ raise DefinitionError, "'#{@name}' is not a valid pattern variable name (lower-case identifier)"
52
+ end
53
+ freeze
54
+ end
55
+
56
+ def to_haskell(_atomic = false)
57
+ name
58
+ end
59
+ end
60
+
61
+ class Wild < Node
62
+ def to_haskell(_atomic = false)
63
+ "_"
64
+ end
65
+ end
66
+
67
+ WILD = Wild.new.freeze
68
+
69
+ # `*xs` inside an array literal: the rest of the list.
70
+ class Rest < Node
71
+ attr_reader :pattern
72
+
73
+ def initialize(pattern)
74
+ @pattern = pattern
75
+ freeze
76
+ end
77
+
78
+ def to_haskell(_atomic = false)
79
+ raise DefinitionError, "a splat (*rest) may only appear last in a list pattern"
80
+ end
81
+ end
82
+
83
+ class ConApp < Node
84
+ attr_reader :name, :args
85
+
86
+ def initialize(name, args)
87
+ @name = name.to_s
88
+ @args = args.freeze
89
+ freeze
90
+ end
91
+
92
+ def to_haskell(atomic = false)
93
+ hs = HaskellMatch.symbolic_constructor(name) || name
94
+ return hs if args.empty?
95
+
96
+ s = if hs.start_with?(":") && args.size == 2
97
+ "#{PatternAST.render(args[0], true)} #{hs} #{PatternAST.render(args[1], true)}"
98
+ else
99
+ "#{hs.start_with?(':') ? "(#{hs})" : hs} #{args.map { |a| PatternAST.render(a, true) }.join(' ')}"
100
+ end
101
+ atomic ? "(#{s})" : s
102
+ end
103
+ end
104
+
105
+ class Record < Node
106
+ attr_reader :name, :fields, :rest
107
+
108
+ def initialize(name, fields, rest)
109
+ @name = name.to_s
110
+ @fields = fields.freeze
111
+ @rest = rest
112
+ freeze
113
+ end
114
+
115
+ def to_haskell(_atomic = false)
116
+ parts = fields.map { |f, p| "#{f} = #{PatternAST.render(p)}" }
117
+ parts << ".." if rest
118
+ "#{name} {#{parts.join(', ')}}"
119
+ end
120
+ end
121
+
122
+ class Tuple < Node
123
+ attr_reader :items
124
+
125
+ def initialize(items)
126
+ @items = items.freeze
127
+ freeze
128
+ end
129
+
130
+ def to_haskell(_atomic = false)
131
+ "(#{items.map { |i| PatternAST.render(i) }.join(', ')})"
132
+ end
133
+ end
134
+
135
+ class Cons < Node
136
+ attr_reader :head, :tail
137
+
138
+ def initialize(head, tail)
139
+ @head = head
140
+ @tail = tail
141
+ freeze
142
+ end
143
+
144
+ def to_haskell(_atomic = false)
145
+ "(#{PatternAST.render(head, true)}:#{PatternAST.render(tail, true)})"
146
+ end
147
+ end
148
+
149
+ class Prefixed < Node
150
+ attr_reader :prefix, :pattern
151
+
152
+ def initialize(prefix, pattern)
153
+ @prefix = prefix
154
+ @pattern = pattern
155
+ freeze
156
+ end
157
+
158
+ def to_haskell(_atomic = false)
159
+ "#{prefix}#{PatternAST.render(pattern, true)}"
160
+ end
161
+ end
162
+
163
+ class As < Node
164
+ attr_reader :name, :pattern
165
+
166
+ def initialize(name, pattern)
167
+ @name = name.is_a?(Var) ? name : Var.new(name)
168
+ @pattern = pattern
169
+ freeze
170
+ end
171
+
172
+ def to_haskell(_atomic = false)
173
+ "#{name.to_haskell}@#{PatternAST.render(pattern, true)}"
174
+ end
175
+ end
176
+
177
+ class Str < Node
178
+ attr_reader :string
179
+
180
+ def initialize(string)
181
+ @string = string.to_s
182
+ freeze
183
+ end
184
+
185
+ def to_haskell(_atomic = false)
186
+ "\"#{PatternAST.escape(string, '"')}\""
187
+ end
188
+ end
189
+
190
+ class Char < Node
191
+ attr_reader :char
192
+
193
+ def initialize(str)
194
+ s = str.to_s
195
+ raise DefinitionError, "char(#{str.inspect}) needs exactly one character" unless s.length == 1
196
+
197
+ @char = s
198
+ freeze
199
+ end
200
+
201
+ def to_haskell(_atomic = false)
202
+ "'#{PatternAST.escape(char, "'")}'"
203
+ end
204
+ end
205
+
206
+ module_function
207
+
208
+ # Render any supported Ruby object as Haskell pattern text.
209
+ def render(obj, atomic = false)
210
+ case obj
211
+ when Node then obj.to_haskell(atomic)
212
+ when String then "\"#{escape(obj, '"')}\""
213
+ when Symbol then symbol(obj)
214
+ when Integer then obj.negative? && atomic ? "(#{obj})" : obj.to_s
215
+ when Float then float(obj, atomic)
216
+ when true then "True"
217
+ when false then "False"
218
+ when Array then list(obj)
219
+ when Hash then hash_pattern(obj)
220
+ when Constructor then constructor(obj, atomic)
221
+ when Class
222
+ if obj < Data && obj.include?(Constructor)
223
+ obj.constructor_name # the compiler reports the arity error
224
+ else
225
+ unsupported(obj)
226
+ end
227
+ when nil
228
+ raise DefinitionError, "nil is not a Haskell pattern (use _ for a wildcard)"
229
+ else
230
+ unsupported(obj)
231
+ end
232
+ end
233
+
234
+ def unsupported(obj)
235
+ raise DefinitionError, "#{obj.inspect} (#{obj.class}) cannot be used as a pattern"
236
+ end
237
+
238
+ def list(items)
239
+ return "[]" if items.empty?
240
+
241
+ if items.last.is_a?(Rest)
242
+ *init, rest = items
243
+ return render(rest.pattern) if init.empty?
244
+
245
+ inner = init.map { |i| render(i, true) } + [render(rest.pattern, true)]
246
+ return "(#{inner.join(':')})"
247
+ end
248
+ if items.any? { |i| i.is_a?(Rest) }
249
+ raise DefinitionError, "a splat (*rest) may only appear last in a list pattern"
250
+ end
251
+
252
+ "[#{items.map { |i| render(i) }.join(', ')}]"
253
+ end
254
+
255
+ # `{name: n, "k" => v}` -> `{name = n, "k" = v}`: a Hash having those keys
256
+ # (Symbol or String), whose values match the sub-patterns; other keys are
257
+ # ignored. `**_` (the record-pattern rest marker) is accepted and means
258
+ # nothing extra, since Hash patterns are always open.
259
+ def hash_pattern(hash)
260
+ parts = hash.filter_map do |k, v|
261
+ next if k == REST_KEY
262
+
263
+ key = case k
264
+ when Symbol then k.to_s.match?(/\A[a-z_][A-Za-z0-9_']*\z/) ? k.to_s : ":#{k.to_s.inspect}"
265
+ when String then k.inspect
266
+ else raise DefinitionError, "Hash pattern keys must be Symbols or Strings (got #{k.inspect})"
267
+ end
268
+ "#{key} = #{render(v)}"
269
+ end
270
+ "{#{parts.join(', ')}}"
271
+ end
272
+
273
+ def constructor(value, atomic)
274
+ name = value.class.constructor_name
275
+ fields = value.fields
276
+ return name if fields.empty?
277
+
278
+ s = if name.to_s.start_with?(":") && fields.size == 2
279
+ "#{render(fields[0], true)} #{name} #{render(fields[1], true)}"
280
+ else
281
+ "#{name} #{fields.map { |f| render(f, true) }.join(' ')}"
282
+ end
283
+ atomic ? "(#{s})" : s
284
+ end
285
+
286
+ def symbol(sym)
287
+ s = sym.to_s
288
+ if s.match?(/\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/)
289
+ ":#{s}"
290
+ else
291
+ ":\"#{escape(s, '"')}\""
292
+ end
293
+ end
294
+
295
+ def float(f, atomic)
296
+ raise DefinitionError, "#{f} cannot be used as a pattern" unless f.finite?
297
+
298
+ s = f.to_s
299
+ f.negative? && atomic ? "(#{s})" : s
300
+ end
301
+
302
+ # Escape for the pattern lexer's string/char literals.
303
+ def escape(str, quote)
304
+ str.each_char.map do |c|
305
+ case c
306
+ when "\\" then "\\\\"
307
+ when quote then "\\#{quote}"
308
+ when "\n" then "\\n"
309
+ when "\t" then "\\t"
310
+ when "\r" then "\\r"
311
+ when "\0" then "\\0"
312
+ else
313
+ c.ord < 0x20 ? format("\\x%02x", c.ord) : c
314
+ end
315
+ end.join
316
+ end
317
+
318
+ # Methods mixed into clause builders so patterns can be written in place.
319
+ module BuilderMethods
320
+ def _
321
+ WILD
322
+ end
323
+
324
+ def var(name)
325
+ Var.new(name)
326
+ end
327
+
328
+ def cons(head, tail)
329
+ Cons.new(head, tail)
330
+ end
331
+
332
+ def tuple(*items)
333
+ Tuple.new(items)
334
+ end
335
+
336
+ def unit
337
+ Tuple.new([])
338
+ end
339
+
340
+ def as(name, pattern)
341
+ As.new(name, pattern)
342
+ end
343
+
344
+ def lazy(pattern)
345
+ Prefixed.new("~", pattern)
346
+ end
347
+
348
+ def bang(pattern)
349
+ Prefixed.new("!", pattern)
350
+ end
351
+
352
+ def char(str)
353
+ Char.new(str)
354
+ end
355
+
356
+ # A string literal pattern. Needed only as a whole argument of `on`,
357
+ # where a bare String is read as quoted Haskell pattern syntax; inside
358
+ # a pattern (`Just("abc")`, `[str, *rest]`) Ruby Strings are literals.
359
+ def str(string)
360
+ Str.new(string)
361
+ end
362
+
363
+ # `Just(x)`, `Person(name: n, age: _)`, `Node(l, v, r)`; a bare
364
+ # lower-case name is a pattern variable.
365
+ def method_missing(name, *args, **kwargs, &blk)
366
+ s = name.to_s
367
+ if s.match?(/\A[A-Z]/) && blk.nil?
368
+ if kwargs.empty?
369
+ ConApp.new(s, args)
370
+ elsif args.empty?
371
+ rest = kwargs.delete(REST_KEY) ? true : false
372
+ Record.new(s, kwargs.map { |k, v| [k.to_s, v] }, rest)
373
+ else
374
+ raise DefinitionError, "#{s}(...) takes either positional or named fields, not both"
375
+ end
376
+ elsif args.empty? && kwargs.empty? && blk.nil? && s.match?(/\A[a-z_][A-Za-z0-9_]*\z/) &&
377
+ !pattern_owner_responds?(name)
378
+ Var.new(s)
379
+ else
380
+ super
381
+ end
382
+ end
383
+
384
+ def respond_to_missing?(name, include_private = false)
385
+ name.to_s.match?(/\A[A-Za-z_]/) || super
386
+ end
387
+
388
+ # Hook: builders with an owner delegate names the owner knows.
389
+ def pattern_owner_responds?(_name)
390
+ false
391
+ end
392
+ end
393
+ end
394
+ end