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,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "haskell/compiler"
4
+
5
+ module HaskellMatch
6
+ # Raised when Haskell source fails to parse or compile; wraps the original
7
+ # error with the source location.
8
+ class HaskellSyntaxError < CompileError; end
9
+
10
+ # Haskell source compiled into a Ruby module.
11
+ #
12
+ # module Geometry
13
+ # extend HaskellMatch::Haskell
14
+ # haskell <<~HS
15
+ # data Shape = Circle Double | Rect Double Double
16
+ # area (Circle r) = pi * r * r
17
+ # area (Rect w h) = w * h
18
+ # HS
19
+ # end
20
+ # Geometry.area(Geometry::Circle.new(1.0))
21
+ #
22
+ # Or from a file: `HaskellMatch.load("geometry.hs")` returns a module;
23
+ # `HaskellMatch.require "geometry"` finds `geometry.hs` on `$LOAD_PATH` and
24
+ # defines a constant named after the module header (or the file name).
25
+ module Haskell
26
+ # Compile Haskell source into the extending module. Errors are reported
27
+ # against `file`/`line` (default: the caller, so a heredoc's lines map to
28
+ # the Ruby file).
29
+ def haskell(source, file: nil, line: nil, exhaustive: HaskellMatch.exhaustive)
30
+ file, line = HaskellMatch::Haskell.caller_site(caller_locations(1, 1)) if file.nil?
31
+ HaskellMatch::Haskell.compile(source, self, file: file, line: line, exhaustive: exhaustive)
32
+ self
33
+ end
34
+
35
+ # The type scope this module's `data` declarations live in and its
36
+ # patterns compile against: a snapshot of the global registry taken when
37
+ # the module was first compiled, plus the module's own types.
38
+ def haskell_scope
39
+ @__haskell_scope__ ||= HaskellMatch.new_scope
40
+ end
41
+
42
+ # `HaskellMatch.fn`, `.case_of`, `.pattern` and `.data` in this module's
43
+ # type scope, so Ruby code can match on the module's Haskell types:
44
+ #
45
+ # Shapes.fn(:name) { on("Circle _") { "circle" }; on("Rect _ _") { "rect" } }
46
+ def fn(name = nil, **options, &definition)
47
+ HaskellMatch.fn(name, scope: haskell_scope, **options, &definition)
48
+ end
49
+
50
+ def case_of(*values, **options, &definition)
51
+ HaskellMatch.case_of(*values, scope: haskell_scope, **options, &definition)
52
+ end
53
+
54
+ def pattern(source = nil, &block)
55
+ HaskellMatch.pattern(source, scope: haskell_scope, &block)
56
+ end
57
+
58
+ def data(decl, under: self, **constructors)
59
+ HaskellMatch.data(decl, under: under, scope: haskell_scope, **constructors)
60
+ end
61
+
62
+ # The {Function} objects behind the module's Haskell functions, by
63
+ # Haskell name (`mod.haskell_functions["insert"].tail(...)`).
64
+ def haskell_functions
65
+ (@__haskell_functions__ ||= {}).dup
66
+ end
67
+
68
+ # Top-level values by Haskell name (to the Ruby method that memoises them).
69
+ def haskell_values
70
+ (@__haskell_values__ ||= {}).dup
71
+ end
72
+
73
+ # Names of the data types the module declares.
74
+ def haskell_types
75
+ (@__haskell_types__ ||= []).dup
76
+ end
77
+
78
+ # The module's export list, or nil when everything is exported.
79
+ def haskell_exports
80
+ @__haskell_exports__
81
+ end
82
+
83
+ # Names brought in by imports, to :function or :value.
84
+ def haskell_imports
85
+ (@__haskell_imports__ ||= {}).dup
86
+ end
87
+
88
+ # One compiled function, as a {Function}.
89
+ def haskell_function(name)
90
+ (@__haskell_functions__ ||= {}).fetch(name.to_s) do
91
+ raise NameError, "no Haskell function #{name} in #{self}"
92
+ end
93
+ end
94
+
95
+ class << self
96
+ # `file`/`line` locate the source for error messages; `line` is the line
97
+ # before the Haskell text (a heredoc opener), so Haskell line 1 is
98
+ # reported as `line + 1`.
99
+ def compile(source, host, file: "(haskell)", line: nil, exhaustive: HaskellMatch.exhaustive, ast: nil)
100
+ offset = line || 0
101
+ ast ||= parse(source, file, offset)
102
+ host.extend(Haskell) unless host.singleton_class.include?(Haskell)
103
+ # imports first: the generator resolves imported constructors
104
+ ast["decls"].each do |d|
105
+ next unless d["kind"] == "import"
106
+
107
+ import_into(host, d["module"], qualified: d["qualified"], as: d["as"], hiding: d["hiding"],
108
+ items: d["items"], file: file, line: d["line"] + offset)
109
+ end
110
+ compiler = Compiler.new(ast, host, source_name: file, exhaustive: exhaustive, line_offset: offset,
111
+ scope: host.haskell_scope)
112
+ ruby = compiler.generate
113
+ begin
114
+ host.module_eval(ruby, "#{file} (compiled)", 1)
115
+ rescue CompileError => e
116
+ name = e.message[/In an equation for '([^']+)'/, 1]
117
+ line = name && (compiler.lines[name] || compiler.lifted_lines[name])
118
+ raise e.exception("#{file}#{line ? ":#{line}" : ''}: #{e.message}"), cause: nil
119
+ end
120
+ host.instance_variable_set(:@__haskell_source__, (host.instance_variable_get(:@__haskell_source__) || []) << source)
121
+ host.instance_variable_set(:@__haskell_ruby__, (host.instance_variable_get(:@__haskell_ruby__) || []) << ruby)
122
+ ast["name"]
123
+ end
124
+
125
+ # Bring another module's functions, values and types into `host`, as
126
+ # `import M [qualified] [as A] [hiding] [(items)]` does: functions and
127
+ # values become forwarding methods, types join the host's type scope
128
+ # and their constructors become constants. `source` is a module or a
129
+ # Haskell module name, resolved to a constant (`Data.Tree` is
130
+ # `Data::Tree`) or loaded from `Data/Tree.hs` on `$LOAD_PATH`.
131
+ # Qualification is accepted and ignored: compiled code refers to names
132
+ # unqualified.
133
+ # Standard library modules whose functions the Prelude provides; importing
134
+ # them only checks the named items exist.
135
+ STANDARD_MODULES = %w[
136
+ Prelude Data.Char Data.List Data.Maybe Data.Either Data.Function Data.Ord Data.Tuple Data.Bool
137
+ Data.Foldable Data.Traversable Control.Monad Numeric Data.String Data.Ratio Data.Int Data.Word
138
+ ].freeze
139
+
140
+ def import_into(host, source, qualified: false, as: nil, hiding: false, items: nil, file: nil, line: nil)
141
+ _ = [qualified, as]
142
+ if source.is_a?(String) && STANDARD_MODULES.include?(source)
143
+ unknown = (items || []) - Prelude::ARITY.keys.map(&:to_s)
144
+ unless unknown.empty?
145
+ where = file ? "#{file}:#{line}: " : ""
146
+ raise HaskellSyntaxError, "#{where}module #{source} does not export #{unknown.join(', ')} (not in this Prelude)"
147
+ end
148
+ return Prelude
149
+ end
150
+ mod = source.is_a?(Module) ? source : resolve_module(source.to_s, file, line)
151
+ host.extend(Haskell) unless host.singleton_class.include?(Haskell)
152
+ functions = mod.respond_to?(:haskell_functions) ? mod.haskell_functions : {}
153
+ values = mod.respond_to?(:haskell_values) ? mod.haskell_values : {}
154
+ types = mod.respond_to?(:haskell_types) ? mod.haskell_types : []
155
+ names = functions.keys + values.keys + types
156
+ names = mod.singleton_methods(false).map(&:to_s) if names.empty? # a plain Ruby module
157
+ if mod.respond_to?(:haskell_exports) && mod.haskell_exports
158
+ names &= mod.haskell_exports
159
+ end
160
+ if items
161
+ unknown = items - names
162
+ unless unknown.empty? || hiding
163
+ where = file ? "#{file}:#{line}: " : ""
164
+ raise HaskellSyntaxError, "#{where}module #{mod} does not export #{unknown.join(', ')}"
165
+ end
166
+ names = hiding ? names - items : names & items
167
+ end
168
+ table = host.instance_variable_get(:@__haskell_functions__) || host.instance_variable_set(:@__haskell_functions__, {})
169
+ imports = host.instance_variable_get(:@__haskell_imports__) || host.instance_variable_set(:@__haskell_imports__, {})
170
+ names.each do |n|
171
+ next if types.include?(n)
172
+
173
+ host.define_singleton_method(n) { |*a| mod.public_send(n, *a) }
174
+ table[n] = functions[n] if functions.key?(n)
175
+ imports[n] = values.key?(n) ? :value : :function
176
+ end
177
+ imported_types = names & types
178
+ unless imported_types.empty?
179
+ Native.import_scope(host.haskell_scope, mod.haskell_scope, imported_types)
180
+ HaskellMatch.import_constructors(host.haskell_scope, mod.haskell_scope, imported_types)
181
+ imported_types.each do |t|
182
+ next unless mod.const_defined?(t, false)
183
+
184
+ tm = mod.const_get(t, false)
185
+ host.const_set(t, tm) unless host.const_defined?(t, false)
186
+ host.include(tm) if tm.instance_of?(Module)
187
+ end
188
+ end
189
+ mod
190
+ end
191
+
192
+ # A module by Haskell name: an existing constant, or a `.hs` file.
193
+ def resolve_module(name, file = nil, line = nil)
194
+ const = name.split(".").inject(Object) do |ns, part|
195
+ break nil unless ns.const_defined?(part, false)
196
+
197
+ ns.const_get(part, false)
198
+ end
199
+ return const if const.is_a?(Module)
200
+
201
+ begin
202
+ HaskellMatch.require(name)
203
+ rescue LoadError
204
+ where = file ? "#{file}:#{line}: " : ""
205
+ raise HaskellSyntaxError,
206
+ "#{where}cannot find module #{name}: no constant #{name.gsub('.', '::')} and no #{name.gsub('.', '/')}.hs on $LOAD_PATH"
207
+ end
208
+ end
209
+
210
+ # Call a compiled function from Ruby the way Haskell application
211
+ # works: exactly `arity` arguments call it; fewer return a partial
212
+ # application (a curried Proc); more apply the result to the rest.
213
+ def apply(function, arity, args)
214
+ if args.size == arity
215
+ function.(*args)
216
+ elsif args.size < arity
217
+ args.inject(function.curried) { |f, x| f.(x) }
218
+ else
219
+ args.drop(arity).inject(function.(*args.first(arity))) { |f, x| f.(x) }
220
+ end
221
+ end
222
+
223
+ # Parse Haskell source into its JSON AST (a Hash).
224
+ def parse(source, file = "(haskell)", line_offset = 0)
225
+ JSON.parse(Native.parse_haskell(source))
226
+ rescue CompileError => e
227
+ raise HaskellSyntaxError, located(e.message, source, file, line_offset)
228
+ end
229
+
230
+ # "line:col: message" -> "file:line:col: message" plus the source line
231
+ # and a caret under the column.
232
+ def located(message, source, file, line_offset)
233
+ m = message.match(/\A(\d+):(\d+): (.*)\z/m)
234
+ return "#{file}:#{message}" unless m
235
+
236
+ line, col, text = m[1].to_i, m[2].to_i, m[3]
237
+ src_line = source.lines[line - 1]&.chomp
238
+ shown = "#{file}:#{line + line_offset}:#{col}: #{text}"
239
+ return shown if src_line.nil?
240
+
241
+ "#{shown}\n #{src_line}\n #{' ' * [col - 1, 0].max}^"
242
+ end
243
+
244
+ # [path, line] of the Ruby call site, for default error locations.
245
+ def caller_site(locations)
246
+ loc = locations&.first
247
+ loc ? [loc.path, loc.lineno] : ["(haskell)", 0]
248
+ end
249
+
250
+ # The Ruby generated for a module's Haskell (for debugging).
251
+ def generated_ruby(host)
252
+ (host.instance_variable_get(:@__haskell_ruby__) || []).join("\n")
253
+ end
254
+
255
+ # Constant path for a loaded file: its `module` header (`Data.Tree` ->
256
+ # `Data::Tree`) or its camelised file name.
257
+ def module_name_for(ast_name, path)
258
+ return ast_name.split(".") if ast_name
259
+
260
+ [File.basename(path, ".*").split(/[^A-Za-z0-9]+/).map(&:capitalize).join]
261
+ end
262
+
263
+ # Define `mod` as the (possibly nested) constant `names` under `under`.
264
+ def define_constant(under, names, mod)
265
+ *parents, last = names
266
+ home = parents.inject(under) do |ns, n|
267
+ ns.const_defined?(n, false) ? ns.const_get(n, false) : ns.const_set(n, Module.new)
268
+ end
269
+ home.send(:remove_const, last) if home.const_defined?(last, false)
270
+ home.const_set(last, mod)
271
+ end
272
+ end
273
+ end
274
+
275
+ class << self
276
+ # Compile Haskell source into a fresh (or given) module.
277
+ def haskell(source, into: Module.new, file: nil, line: nil, exhaustive: self.exhaustive)
278
+ file, line = Haskell.caller_site(caller_locations(1, 1)) if file.nil?
279
+ into.extend(Haskell) unless into.singleton_class.include?(Haskell)
280
+ Haskell.compile(source, into, file: file, line: line, exhaustive: exhaustive)
281
+ into
282
+ end
283
+
284
+ # Load a `.hs` file into a module (a new anonymous one by default).
285
+ def load(path, into: Module.new, exhaustive: self.exhaustive)
286
+ source = File.read(path)
287
+ into.extend(Haskell) unless into.singleton_class.include?(Haskell)
288
+ Haskell.compile(source, into, file: path, exhaustive: exhaustive)
289
+ into
290
+ end
291
+
292
+ # Find `name.hs` (or a path) on `$LOAD_PATH`, compile it once, and define
293
+ # a constant for it named after its `module` header or its file name.
294
+ # Returns the module.
295
+ def require(name, under: Object)
296
+ path = resolve_hs(name)
297
+ @required_hs ||= {}
298
+ return @required_hs[path] if @required_hs.key?(path)
299
+
300
+ source = File.read(path)
301
+ ast = Haskell.parse(source, path)
302
+ mod = Module.new
303
+ mod.extend(Haskell)
304
+ Haskell.compile(source, mod, file: path, ast: ast)
305
+ Haskell.define_constant(under, Haskell.module_name_for(ast["name"], path), mod)
306
+ @required_hs[path] = mod
307
+ end
308
+
309
+ private
310
+
311
+ def resolve_hs(name)
312
+ name = name.to_s
313
+ slashed = name.gsub(".", "/")
314
+ snake = slashed.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
315
+ candidates = [name, "#{name}.hs", "#{slashed}.hs", "#{snake}.hs"].uniq
316
+ candidates.each { |c| return File.expand_path(c) if File.file?(c) }
317
+ $LOAD_PATH.each do |dir|
318
+ candidates.each do |c|
319
+ full = File.join(dir, c)
320
+ return full if File.file?(full)
321
+ end
322
+ end
323
+ raise LoadError, "cannot load Haskell file -- #{name} (looked for #{name}.hs in the current directory and $LOAD_PATH)"
324
+ end
325
+ end
326
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # Haskell-style rendering of values, used by constructor `#inspect`.
5
+ module Inspect
6
+ module_function
7
+
8
+ # Render `value` as a Haskell expression. `atomic` requests parentheses
9
+ # around anything that is an application (so it can appear as an
10
+ # argument).
11
+ def render(value, atomic: false)
12
+ case value
13
+ when Constructor
14
+ render_constructor(value, atomic)
15
+ when Array
16
+ "[#{value.map { |v| render(v) }.join(', ')}]"
17
+ when Integer, Float, Rational
18
+ s = value.inspect
19
+ value.negative? && atomic ? "(#{s})" : s
20
+ when true then "True"
21
+ when false then "False"
22
+ else
23
+ value.inspect
24
+ end
25
+ end
26
+
27
+ def render_constructor(value, atomic)
28
+ name = value.class.constructor_name
29
+ fields = value.class.field_names
30
+ values = value.fields
31
+ return name if values.empty?
32
+
33
+ if fields
34
+ inner = fields.zip(values).map { |f, v| "#{f} = #{render(v)}" }.join(", ")
35
+ "#{name} {#{inner}}"
36
+ elsif name.to_s.start_with?(":") && values.size == 2
37
+ s = "#{render(values[0], atomic: true)} #{name} #{render(values[1], atomic: true)}"
38
+ atomic ? "(#{s})" : s
39
+ else
40
+ s = "#{name} #{values.map { |v| render(v, atomic: true) }.join(' ')}"
41
+ atomic ? "(#{s})" : s
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HaskellMatch
4
+ # A lazy, memoised cons list: Haskell's list, including infinite ones.
5
+ #
6
+ # naturals = HaskellMatch.lazy(1..) # or any Enumerable / Enumerator
7
+ # take = HaskellMatch.fn(:take) do
8
+ # on("0", "_") { [] }
9
+ # on("_", "[]") { [] }
10
+ # on("n", "(x:xs)") { |n, x, xs| [x] + take.(n - 1, xs) }
11
+ # end
12
+ # take.(5, naturals) # => [1, 2, 3, 4, 5]
13
+ #
14
+ # List patterns (`[]`, `(x:xs)`, `[a, b]`) match a LazyList directly; `xs`
15
+ # is bound to the rest of the list, still lazy. An Enumerator (including
16
+ # `Enumerator::Lazy`) used as a value is wrapped automatically; each wrap
17
+ # iterates it from the start, so the same stream matches the same way every
18
+ # time. Elements are computed once and shared, as in Haskell.
19
+ class LazyList
20
+ include Enumerable
21
+
22
+ # Build a lazy list over any Enumerable (an Enumerator is iterated from
23
+ # its start; the caller's enumerator is left untouched).
24
+ def self.from(source)
25
+ return source if source.is_a?(LazyList)
26
+ raise TypeError, "#{source.inspect} is not Enumerable" unless source.respond_to?(:each)
27
+
28
+ new(source.to_enum(:each))
29
+ end
30
+
31
+ # The empty lazy list.
32
+ def self.empty
33
+ from([])
34
+ end
35
+
36
+ # An infinite list `x, f(x), f(f(x)), ...` (Haskell's `iterate`).
37
+ def self.iterate(seed, &step)
38
+ from(Enumerator.produce(seed, &step))
39
+ end
40
+
41
+ # A lazy list from a block that yields elements (`yielder << x`).
42
+ def self.generate(&block)
43
+ from(Enumerator.new(&block))
44
+ end
45
+
46
+ # An infinite repetition of `value` (Haskell's `repeat`).
47
+ def self.repeat(value)
48
+ from(Enumerator.produce(value) { value })
49
+ end
50
+
51
+ # A cons cell with an already-known head in front of a lazy tail.
52
+ def self.cons(head, tail)
53
+ tail = from(tail) unless tail.is_a?(LazyList)
54
+ cell = allocate
55
+ cell.instance_variable_set(:@enum, nil)
56
+ cell.instance_variable_set(:@state, :cons)
57
+ cell.instance_variable_set(:@head, head)
58
+ cell.instance_variable_set(:@tail, tail)
59
+ cell
60
+ end
61
+
62
+ # A cons cell whose tail is computed on first use (Haskell's `x : e`
63
+ # when `e` is an unevaluated expression). The block may return any list:
64
+ # an Array, a String, an Enumerator or another LazyList.
65
+ def self.lazy_cons(head, &tail)
66
+ cons(head, deferred(&tail))
67
+ end
68
+
69
+ # A list that is computed by `thunk` on first use.
70
+ def self.deferred(&thunk)
71
+ cell = allocate
72
+ cell.instance_variable_set(:@enum, nil)
73
+ cell.instance_variable_set(:@state, :thunk)
74
+ cell.instance_variable_set(:@tail, thunk)
75
+ cell
76
+ end
77
+
78
+ # A finite or infinite list from `first` upwards (`[n..]`, `[n..m]`).
79
+ def self.range(first, last = nil)
80
+ from(last ? (first..last) : (first..))
81
+ end
82
+
83
+ # @api private: `enum` is an external enumerator shared along the spine.
84
+ def initialize(enum)
85
+ @enum = enum
86
+ @state = :unforced
87
+ end
88
+
89
+ # Evaluate this cell: nil for the empty list, `[head, tail]` for a cons.
90
+ # Called by the native matcher; memoised, so each element is produced
91
+ # once.
92
+ def force
93
+ if @state == :thunk
94
+ thunk = @tail
95
+ @tail = nil
96
+ @state = :forcing
97
+ list = thunk.call
98
+ list = LazyList.from(list.is_a?(String) ? list.each_char : list) unless list.is_a?(LazyList)
99
+ if (pair = list.force)
100
+ @head, @tail = pair
101
+ @state = :cons
102
+ else
103
+ @state = :nil
104
+ end
105
+ elsif @state == :forcing
106
+ raise MatchError, "a lazy list refers to itself before its first element is known"
107
+ end
108
+ if @state == :unforced
109
+ begin
110
+ @head = @enum.next
111
+ @tail = LazyList.new(@enum)
112
+ @state = :cons
113
+ rescue StopIteration
114
+ @state = :nil
115
+ end
116
+ end
117
+ @state == :cons ? [@head, @tail] : nil
118
+ end
119
+
120
+ def empty?
121
+ force.nil?
122
+ end
123
+
124
+ def head
125
+ pair = force or raise MatchError, "head of an empty list"
126
+ pair[0]
127
+ end
128
+
129
+ def tail
130
+ pair = force or raise MatchError, "tail of an empty list"
131
+ pair[1]
132
+ end
133
+
134
+ # Iterate the elements (does not terminate for an infinite list).
135
+ def each
136
+ return to_enum(:each) unless block_given?
137
+
138
+ cell = self
139
+ while (pair = cell.force)
140
+ yield pair[0]
141
+ cell = pair[1]
142
+ end
143
+ self
144
+ end
145
+
146
+ # The first `n` elements, as a (strict) Array.
147
+ def take(n)
148
+ out = []
149
+ cell = self
150
+ while out.size < n && (pair = cell.force)
151
+ out << pair[0]
152
+ cell = pair[1]
153
+ end
154
+ out
155
+ end
156
+
157
+ # Elements already evaluated, without forcing more.
158
+ def forced_prefix
159
+ out = []
160
+ cell = self
161
+ while cell.instance_variable_get(:@state) == :cons
162
+ out << cell.instance_variable_get(:@head)
163
+ cell = cell.instance_variable_get(:@tail)
164
+ end
165
+ [out, cell.instance_variable_get(:@state) == :nil]
166
+ end
167
+
168
+ # Element-wise equality with any list (does not terminate when both
169
+ # lists are infinite and equal, as in Haskell).
170
+ def ==(other)
171
+ return false unless other.respond_to?(:each)
172
+
173
+ a = each
174
+ b = other.each
175
+ loop do
176
+ x = begin
177
+ a.next
178
+ rescue StopIteration
179
+ return (b.next; false) rescue StopIteration; return true
180
+ end
181
+ y = begin
182
+ b.next
183
+ rescue StopIteration
184
+ return false
185
+ end
186
+ return false unless x == y
187
+ end
188
+ end
189
+ alias eql? ==
190
+
191
+ def hash
192
+ to_a.hash
193
+ end
194
+
195
+ def inspect
196
+ prefix, complete = forced_prefix
197
+ items = prefix.map(&:inspect)
198
+ items << "..." unless complete
199
+ "LazyList[#{items.join(', ')}]"
200
+ end
201
+ alias to_s inspect
202
+ end
203
+
204
+ class << self
205
+ # Build a {LazyList} from any Enumerable.
206
+ def lazy(source)
207
+ LazyList.from(source)
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiddle"
4
+ require "rbconfig"
5
+
6
+ module HaskellMatch
7
+ # Locates and loads the compiled Rust extension, then runs its `Init`
8
+ # function. The search order is:
9
+ #
10
+ # 1. `ENV["HASKELL_MATCH_NATIVE"]` (an explicit path to the library)
11
+ # 2. `lib/haskell_match/native/` (where `rake compile` and `gem install` put it)
12
+ # 3. `ext/haskell_match/target/{release,debug}/` (a fresh `cargo build`)
13
+ module NativeLoader
14
+ LIB_NAME = "haskell_match"
15
+
16
+ def self.library_file_names
17
+ base = "lib#{LIB_NAME}"
18
+ exts = case RbConfig::CONFIG["host_os"]
19
+ when /darwin/ then %w[dylib bundle so]
20
+ when /mswin|mingw|cygwin/ then %w[dll so]
21
+ else %w[so dylib]
22
+ end
23
+ exts.map { |e| "#{base}.#{e}" } + exts.map { |e| "#{LIB_NAME}.#{e}" }
24
+ end
25
+
26
+ def self.candidates
27
+ root = File.expand_path("../..", __dir__)
28
+ dirs = [
29
+ File.join(root, "lib", "haskell_match", "native"),
30
+ File.join(root, "ext", "haskell_match", "target", "release"),
31
+ File.join(root, "ext", "haskell_match", "target", "debug")
32
+ ]
33
+ paths = dirs.product(library_file_names).map { |d, f| File.join(d, f) }
34
+ explicit = ENV["HASKELL_MATCH_NATIVE"]
35
+ paths.unshift(explicit) if explicit && !explicit.empty?
36
+ paths
37
+ end
38
+
39
+ def self.find_library
40
+ candidates.find { |p| File.file?(p) }
41
+ end
42
+
43
+ def self.load!
44
+ return if @loaded
45
+
46
+ path = find_library
47
+ unless path
48
+ raise LoadError,
49
+ "haskell_match: compiled native library not found. Run `rake compile` " \
50
+ "(or `cargo build --release` in ext/haskell_match). Looked in:\n " +
51
+ candidates.join("\n ")
52
+ end
53
+ lib = Fiddle.dlopen(path)
54
+ init = Fiddle::Function.new(lib["Init_#{LIB_NAME}"], [], Fiddle::TYPE_VOID)
55
+ init.call
56
+ @loaded = true
57
+ @path = path
58
+ end
59
+
60
+ def self.path
61
+ @path
62
+ end
63
+ end
64
+ end