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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +98 -0
- data/LICENSE-APACHE +202 -0
- data/LICENSE-MIT +21 -0
- data/README.md +1484 -0
- data/ext/haskell_match/Cargo.lock +33 -0
- data/ext/haskell_match/Cargo.toml +22 -0
- data/ext/haskell_match/extconf.rb +41 -0
- data/ext/haskell_match/src/core/ast.rs +190 -0
- data/ext/haskell_match/src/core/error.rs +52 -0
- data/ext/haskell_match/src/core/exhaust.rs +699 -0
- data/ext/haskell_match/src/core/hs/ast.rs +256 -0
- data/ext/haskell_match/src/core/hs/json.rs +225 -0
- data/ext/haskell_match/src/core/hs/layout.rs +346 -0
- data/ext/haskell_match/src/core/hs/lexer.rs +688 -0
- data/ext/haskell_match/src/core/hs/mod.rs +14 -0
- data/ext/haskell_match/src/core/hs/parser.rs +1945 -0
- data/ext/haskell_match/src/core/lexer.rs +590 -0
- data/ext/haskell_match/src/core/mod.rs +19 -0
- data/ext/haskell_match/src/core/parser.rs +1116 -0
- data/ext/haskell_match/src/core/pretty.rs +373 -0
- data/ext/haskell_match/src/core/resolve.rs +336 -0
- data/ext/haskell_match/src/core/tree.rs +921 -0
- data/ext/haskell_match/src/core/typecheck.rs +226 -0
- data/ext/haskell_match/src/core/types.rs +404 -0
- data/ext/haskell_match/src/lib.rs +19 -0
- data/ext/haskell_match/src/ruby/mod.rs +1195 -0
- data/ext/haskell_match/src/ruby/runtime.rs +1045 -0
- data/lib/haskell_match/binding_plan.rb +84 -0
- data/lib/haskell_match/case_of.rb +71 -0
- data/lib/haskell_match/clauses.rb +354 -0
- data/lib/haskell_match/data.rb +417 -0
- data/lib/haskell_match/deep_call.rb +98 -0
- data/lib/haskell_match/deriving.rb +130 -0
- data/lib/haskell_match/dsl.rb +71 -0
- data/lib/haskell_match/errors.rb +85 -0
- data/lib/haskell_match/field_types.rb +140 -0
- data/lib/haskell_match/function.rb +240 -0
- data/lib/haskell_match/haskell/compiler.rb +961 -0
- data/lib/haskell_match/haskell.rb +326 -0
- data/lib/haskell_match/inspect.rb +45 -0
- data/lib/haskell_match/lazy_list.rb +210 -0
- data/lib/haskell_match/native_loader.rb +64 -0
- data/lib/haskell_match/pattern.rb +75 -0
- data/lib/haskell_match/pattern_ast.rb +394 -0
- data/lib/haskell_match/prelude.rb +448 -0
- data/lib/haskell_match/scope.rb +44 -0
- data/lib/haskell_match/version.rb +5 -0
- data/lib/haskell_match.rb +41 -0
- 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
|