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,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaskellMatch
|
|
4
|
+
# Works out how a clause body (or guard) wants its bound variables and
|
|
5
|
+
# produces a callable taking them positionally, in pattern order, as the
|
|
6
|
+
# native matcher delivers them.
|
|
7
|
+
#
|
|
8
|
+
# * No parameters, or a splat: the values are passed positionally.
|
|
9
|
+
# * Positional parameters: each must name a bound variable; values are
|
|
10
|
+
# passed by name, so `on("(x:xs)") { |xs| ... }` receives `xs`.
|
|
11
|
+
# * Keyword parameters: passed by name as keywords.
|
|
12
|
+
module BindingPlan
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Returns a callable that accepts the bound values positionally.
|
|
16
|
+
def adapt(callable, names, clause_index, role, function_name)
|
|
17
|
+
params = callable.parameters
|
|
18
|
+
if params.empty?
|
|
19
|
+
# a lambda taking nothing must not be handed the bound values
|
|
20
|
+
return callable unless callable.lambda? && !names.empty?
|
|
21
|
+
|
|
22
|
+
return ->(*) { callable.call }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
kinds = params.map(&:first)
|
|
26
|
+
if kinds.include?(:keyreq) || kinds.include?(:key) || kinds.include?(:keyrest)
|
|
27
|
+
return adapt_keywords(callable, params, names, clause_index, role, function_name)
|
|
28
|
+
end
|
|
29
|
+
return callable if kinds.include?(:rest)
|
|
30
|
+
if kinds.include?(:block)
|
|
31
|
+
params = params.reject { |k, _| k == :block }
|
|
32
|
+
return callable if params.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
wanted = params.map { |_, n| n }
|
|
36
|
+
if wanted.any?(&:nil?)
|
|
37
|
+
# e.g. `|(a, b)|` destructuring parameters: pass positionally
|
|
38
|
+
return callable
|
|
39
|
+
end
|
|
40
|
+
unknown = wanted.reject { |n| names.include?(n.to_s) }
|
|
41
|
+
unless unknown.empty?
|
|
42
|
+
raise DefinitionError,
|
|
43
|
+
"#{role} of clause #{clause_index + 1} of '#{function_name}' names " \
|
|
44
|
+
"#{unknown.map(&:inspect).join(', ')} but the pattern binds " \
|
|
45
|
+
"#{names.empty? ? 'no variables' : names.join(', ')}"
|
|
46
|
+
end
|
|
47
|
+
indexes = wanted.map { |n| names.index(n.to_s) }
|
|
48
|
+
return callable if indexes == (0...indexes.size).to_a && indexes.size == names.size
|
|
49
|
+
return callable if indexes == (0...indexes.size).to_a && !callable.lambda?
|
|
50
|
+
|
|
51
|
+
lambda do |*values|
|
|
52
|
+
callable.call(*indexes.map { |i| values[i] })
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def adapt_keywords(callable, params, names, clause_index, role, function_name)
|
|
57
|
+
positional = params.select { |k, _| %i[req opt].include?(k) }
|
|
58
|
+
unless positional.empty?
|
|
59
|
+
raise DefinitionError,
|
|
60
|
+
"#{role} of clause #{clause_index + 1} of '#{function_name}' mixes positional and " \
|
|
61
|
+
"keyword parameters; use one style"
|
|
62
|
+
end
|
|
63
|
+
keys = params.select { |k, _| %i[keyreq key].include?(k) }.map { |_, n| n }
|
|
64
|
+
unknown = keys.reject { |n| names.include?(n.to_s) }
|
|
65
|
+
unless unknown.empty?
|
|
66
|
+
raise DefinitionError,
|
|
67
|
+
"#{role} of clause #{clause_index + 1} of '#{function_name}' names keyword(s) " \
|
|
68
|
+
"#{unknown.map(&:inspect).join(', ')} but the pattern binds " \
|
|
69
|
+
"#{names.empty? ? 'no variables' : names.join(', ')}"
|
|
70
|
+
end
|
|
71
|
+
symbols = names.map(&:to_sym)
|
|
72
|
+
if params.any? { |k, _| k == :keyrest }
|
|
73
|
+
lambda do |*values|
|
|
74
|
+
callable.call(**symbols.zip(values).to_h)
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
picks = keys.map { |k| [k, names.index(k.to_s)] }
|
|
78
|
+
lambda do |*values|
|
|
79
|
+
callable.call(**picks.to_h { |k, i| [k, values[i]] })
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module HaskellMatch
|
|
6
|
+
# `case ... of` expressions.
|
|
7
|
+
#
|
|
8
|
+
# HaskellMatch.case_of(value) do
|
|
9
|
+
# on("Just x") { |x| x }
|
|
10
|
+
# on("Nothing") { 0 }
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# The decision tree for a given set of patterns is compiled once and cached,
|
|
14
|
+
# so repeated evaluation only pays for collecting the clause blocks.
|
|
15
|
+
module CaseOf
|
|
16
|
+
CACHE = {}
|
|
17
|
+
LOCK = Monitor.new
|
|
18
|
+
MAX_CACHE = 4096
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def evaluate(values, exhaustive:, overlapping:, scope: Native::GLOBAL_SCOPE, &definition)
|
|
23
|
+
raise ArgumentError, "case_of needs a block with on(...) clauses" unless definition
|
|
24
|
+
raise ArgumentError, "case_of needs at least one value" if values.empty?
|
|
25
|
+
|
|
26
|
+
builder = ClauseBuilder.collect(definition)
|
|
27
|
+
clauses = builder.clauses
|
|
28
|
+
key = [scope, cache_key(clauses, exhaustive, overlapping)]
|
|
29
|
+
entry = LOCK.synchronize { CACHE[key] }
|
|
30
|
+
unless entry
|
|
31
|
+
name = "case expression at #{definition.source_location&.join(':')}"
|
|
32
|
+
matcher, _bodies, _guards = Compiler.compile(name, clauses, exhaustive: exhaustive, overlapping: overlapping,
|
|
33
|
+
kind: "case expression", scope: scope,
|
|
34
|
+
resolver: builder.constant_resolver)
|
|
35
|
+
entry = [matcher, matcher.names]
|
|
36
|
+
LOCK.synchronize do
|
|
37
|
+
CACHE.clear if CACHE.size >= MAX_CACHE
|
|
38
|
+
CACHE[key] = entry
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
matcher, names = entry
|
|
42
|
+
bodies = clauses.each_with_index.map { |c, i| BindingPlan.adapt(c.body, names[i], i, "body", "case") }
|
|
43
|
+
guards = clauses.each_with_index.map do |c, i|
|
|
44
|
+
next nil if c.guard.nil? || c.guard.equal?(ClauseBuilder::OTHERWISE)
|
|
45
|
+
|
|
46
|
+
BindingPlan.adapt(c.guard, names[i], i, "guard", "case")
|
|
47
|
+
end
|
|
48
|
+
guards = nil if guards.all?(&:nil?)
|
|
49
|
+
matcher.run(values, bodies, guards)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def cache_key(clauses, exhaustive, overlapping)
|
|
53
|
+
[
|
|
54
|
+
clauses.map { |c| [c.patterns, !c.guard.nil? && !c.guard.equal?(ClauseBuilder::OTHERWISE)] },
|
|
55
|
+
exhaustive, overlapping
|
|
56
|
+
]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def clear_cache
|
|
60
|
+
LOCK.synchronize { CACHE.clear }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# Evaluate a `case ... of` expression over one or more values.
|
|
66
|
+
def case_of(*values, exhaustive: self.exhaustive, overlapping: self.overlapping, scope: Native::GLOBAL_SCOPE,
|
|
67
|
+
&definition)
|
|
68
|
+
CaseOf.evaluate(values, exhaustive: exhaustive, overlapping: overlapping, scope: scope, &definition)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaskellMatch
|
|
4
|
+
# One `on(...)` clause: argument patterns, optional guard, body.
|
|
5
|
+
Clause = Struct.new(:patterns, :guard, :body, :location) do
|
|
6
|
+
def to_s
|
|
7
|
+
patterns.join(" ")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Collects clauses from a definition block.
|
|
12
|
+
#
|
|
13
|
+
# on("[]") { 0 }
|
|
14
|
+
# on("(_:xs)") { |xs| 1 + length.(xs) }
|
|
15
|
+
# on("n", guard: ->(n) { n > 0 }) { :positive }
|
|
16
|
+
#
|
|
17
|
+
# The definition block runs with `self` set to the builder; method calls the
|
|
18
|
+
# builder does not understand are forwarded to the object that owns the
|
|
19
|
+
# block, so helper methods of the surrounding class or test remain
|
|
20
|
+
# callable. Instance variables of that object are not visible; take the
|
|
21
|
+
# builder as a block parameter (`fn { |m| m.on(...) { @x } }`) when needed.
|
|
22
|
+
class ClauseBuilder
|
|
23
|
+
include PatternAST::BuilderMethods
|
|
24
|
+
|
|
25
|
+
attr_reader :clauses
|
|
26
|
+
|
|
27
|
+
def initialize(owner = nil, options = {})
|
|
28
|
+
@clauses = []
|
|
29
|
+
@owner = owner
|
|
30
|
+
@options = options
|
|
31
|
+
@helpers = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A local helper function, like a Haskell `where` binding: defined with
|
|
35
|
+
# its own clauses and reachable by name from every clause body of the
|
|
36
|
+
# enclosing function (and from the other helpers). It is a full
|
|
37
|
+
# {Function}: checked for exhaustiveness, with `tail` and `defer`, and
|
|
38
|
+
# compiled with the enclosing function's options unless overridden.
|
|
39
|
+
#
|
|
40
|
+
# sum_to = HaskellMatch.fn(:sum_to) do
|
|
41
|
+
# on(n) { |n| go.(n, 0) }
|
|
42
|
+
# where :go do
|
|
43
|
+
# on(0, acc) { |acc| acc }
|
|
44
|
+
# on(k, acc) { |k, acc| go.tail(k - 1, acc + k) }
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
def where(name, **options, &definition)
|
|
48
|
+
raise DefinitionError, "where needs a block of on(...) clauses" unless definition
|
|
49
|
+
|
|
50
|
+
name = name.to_s
|
|
51
|
+
raise DefinitionError, "'#{name}' is not a valid helper name" unless name.match?(NAME_PATTERN)
|
|
52
|
+
raise DefinitionError, "helper '#{name}' is defined twice" if @helpers.key?(name)
|
|
53
|
+
|
|
54
|
+
helper = HaskellMatch.fn(name, **@options.merge(options), &definition)
|
|
55
|
+
@helpers[name] = helper
|
|
56
|
+
define_singleton_method(name) { helper }
|
|
57
|
+
helper
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Helper functions defined with {#where}, by name.
|
|
61
|
+
def helpers
|
|
62
|
+
@helpers.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resolve a constant the way code at the definition block would see it:
|
|
66
|
+
# through the receiver (a module, or an object's class) and so through
|
|
67
|
+
# its ancestors and `Object`.
|
|
68
|
+
def constant_resolver
|
|
69
|
+
owner = @owner
|
|
70
|
+
lambda do |name|
|
|
71
|
+
home = owner.is_a?(Module) ? owner : owner.class
|
|
72
|
+
home.const_defined?(name) ? home.const_get(name) : nil
|
|
73
|
+
rescue NameError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Add a clause: one pattern per argument, each either a Haskell pattern
|
|
79
|
+
# string or a pattern written in place (see {PatternAST}).
|
|
80
|
+
def on(*patterns, guard: nil, where: nil, location: nil, &body)
|
|
81
|
+
guard ||= where
|
|
82
|
+
raise DefinitionError, "a clause needs at least one pattern" if patterns.empty?
|
|
83
|
+
raise DefinitionError, "a clause needs a body block" unless body
|
|
84
|
+
if guard && !guard.respond_to?(:call)
|
|
85
|
+
raise DefinitionError, "guard must be callable (a Proc or lambda)"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
texts = patterns.map { |p| p.is_a?(String) ? p.dup.freeze : PatternAST.render(p).freeze }
|
|
89
|
+
@clauses << Clause.new(texts.freeze, guard, body, location || body.source_location)
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
alias clause on
|
|
93
|
+
|
|
94
|
+
# Haskell's `otherwise`: a guard that always passes.
|
|
95
|
+
def otherwise
|
|
96
|
+
OTHERWISE
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
OTHERWISE = ->(*) { true }.freeze
|
|
100
|
+
|
|
101
|
+
# The function being defined, for recursion from inside its own clause
|
|
102
|
+
# bodies (also reachable under the function's own name).
|
|
103
|
+
def recur
|
|
104
|
+
@function or raise DefinitionError, "the function is not defined yet"
|
|
105
|
+
end
|
|
106
|
+
alias this recur
|
|
107
|
+
|
|
108
|
+
def method_missing(name, *args, **kwargs, &blk)
|
|
109
|
+
if @owner && @owner.respond_to?(name, true)
|
|
110
|
+
@owner.__send__(name, *args, **kwargs, &blk)
|
|
111
|
+
else
|
|
112
|
+
super
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def respond_to_missing?(name, include_private = false)
|
|
117
|
+
(@owner && @owner.respond_to?(name, include_private)) || super
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def pattern_owner_responds?(name)
|
|
121
|
+
!@owner.nil? && @owner.respond_to?(name, true)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Run a definition block: instance_exec'd on the builder when it takes no
|
|
125
|
+
# parameters, otherwise called with the builder as its argument. Returns
|
|
126
|
+
# the builder; call {#define_function} once the function exists so clause
|
|
127
|
+
# bodies can recurse through `recur` or the function's name.
|
|
128
|
+
def self.collect(definition, ractor: false, options: {})
|
|
129
|
+
builder = ractor ? RactorHost.new_builder(options) : new(definition.binding.receiver, options)
|
|
130
|
+
if definition.arity.zero?
|
|
131
|
+
builder.instance_exec(&definition)
|
|
132
|
+
else
|
|
133
|
+
definition.call(builder)
|
|
134
|
+
end
|
|
135
|
+
builder
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
NAME_PATTERN = /\A[a-z_][A-Za-z0-9_]*[?!]?\z/
|
|
139
|
+
|
|
140
|
+
# Make the finished function reachable from its clause bodies as `recur`
|
|
141
|
+
# and, when the name is a valid method name, as `name`.
|
|
142
|
+
def define_function(function, name)
|
|
143
|
+
@function = function
|
|
144
|
+
return unless name.is_a?(String) && name.match?(NAME_PATTERN) && !real_method?(name)
|
|
145
|
+
|
|
146
|
+
define_singleton_method(name) { function }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A method actually defined on the builder (not one `method_missing`
|
|
150
|
+
# would synthesise as a pattern variable).
|
|
151
|
+
def real_method?(name)
|
|
152
|
+
singleton_class.method_defined?(name) || singleton_class.private_method_defined?(name)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Clause-builder behaviour for Ractor-shareable functions. A Module is
|
|
156
|
+
# used as the builder (and so as `self` of the clause bodies) because
|
|
157
|
+
# modules are always shareable while remaining open for a constant that
|
|
158
|
+
# points back at the finished function.
|
|
159
|
+
module RactorHost
|
|
160
|
+
include PatternAST::BuilderMethods
|
|
161
|
+
|
|
162
|
+
def self.new_builder(options = {})
|
|
163
|
+
host = Module.new
|
|
164
|
+
host.extend(RactorHost)
|
|
165
|
+
host.instance_variable_set(:@clauses, [])
|
|
166
|
+
host.instance_variable_set(:@options, options)
|
|
167
|
+
host.instance_variable_set(:@helpers, {})
|
|
168
|
+
host
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def clauses
|
|
172
|
+
@clauses
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A `where` helper (see {ClauseBuilder#where}); in Ractor mode helpers
|
|
176
|
+
# are themselves shareable functions and cannot call back into the
|
|
177
|
+
# enclosing function by name.
|
|
178
|
+
def where(name, **options, &definition)
|
|
179
|
+
raise DefinitionError, "where needs a block of on(...) clauses" unless definition
|
|
180
|
+
|
|
181
|
+
name = name.to_s
|
|
182
|
+
raise DefinitionError, "'#{name}' is not a valid helper name" unless name.match?(NAME_PATTERN)
|
|
183
|
+
raise DefinitionError, "helper '#{name}' is defined twice" if @helpers.key?(name)
|
|
184
|
+
|
|
185
|
+
helper = HaskellMatch.fn(name, **@options.merge(options), ractor: true, &definition)
|
|
186
|
+
@helpers[name] = helper
|
|
187
|
+
define_singleton_method(name, &Ractor.make_shareable(-> { helper }))
|
|
188
|
+
helper
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def helpers
|
|
192
|
+
@helpers.dup
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def on(*patterns, guard: nil, where: nil, location: nil, &body)
|
|
196
|
+
guard ||= where
|
|
197
|
+
raise DefinitionError, "a clause needs at least one pattern" if patterns.empty?
|
|
198
|
+
raise DefinitionError, "a clause needs a body block" unless body
|
|
199
|
+
if guard && !guard.respond_to?(:call)
|
|
200
|
+
raise DefinitionError, "guard must be callable (a Proc or lambda)"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
texts = patterns.map { |p| p.is_a?(String) ? p.dup.freeze : PatternAST.render(p).freeze }
|
|
204
|
+
@clauses << Clause.new(texts.freeze, guard, body, location || body.source_location)
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
alias clause on
|
|
208
|
+
|
|
209
|
+
def otherwise
|
|
210
|
+
OTHERWISE
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Constant lookup is the Ractor-safe way back to the function.
|
|
214
|
+
def recur
|
|
215
|
+
const_get(:FUNCTION)
|
|
216
|
+
end
|
|
217
|
+
alias this recur
|
|
218
|
+
|
|
219
|
+
def define_function(function, name)
|
|
220
|
+
const_set(:FUNCTION, function)
|
|
221
|
+
return if !name.is_a?(String) || !name.match?(NAME_PATTERN)
|
|
222
|
+
return if singleton_class.method_defined?(name) || singleton_class.private_method_defined?(name)
|
|
223
|
+
|
|
224
|
+
host = self
|
|
225
|
+
define_singleton_method(name, &Ractor.make_shareable(-> { host.const_get(:FUNCTION) }))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Compiles clauses into a native matcher and enforces exhaustiveness and
|
|
231
|
+
# redundancy policy. Shared by {Function}, {CaseOf} and {Pattern}.
|
|
232
|
+
module Compiler
|
|
233
|
+
WITNESS_LIMIT = 20
|
|
234
|
+
|
|
235
|
+
POLICIES = %i[error warn ignore].freeze
|
|
236
|
+
|
|
237
|
+
module_function
|
|
238
|
+
|
|
239
|
+
def policy(value, option)
|
|
240
|
+
case value
|
|
241
|
+
when true then :error
|
|
242
|
+
when false, nil then :ignore
|
|
243
|
+
when :error, :warn, :ignore then value
|
|
244
|
+
else
|
|
245
|
+
raise ArgumentError, "#{option}: must be true, false, :error, :warn or :ignore (got #{value.inspect})"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Construct a native matcher, registering top-level Ruby `Data`/`Struct`
|
|
250
|
+
# classes named by unknown constructors on the way and adding a caret to
|
|
251
|
+
# syntax errors.
|
|
252
|
+
def build_matcher(klass, name, patterns, guard_flags, limit, scope, resolver: nil)
|
|
253
|
+
tried = []
|
|
254
|
+
begin
|
|
255
|
+
klass.new(name, patterns, guard_flags, limit, scope)
|
|
256
|
+
rescue UnknownConstructorError => e
|
|
257
|
+
con = e.message[/data constructor '([^']+)'/, 1]
|
|
258
|
+
raise if con.nil? || tried.include?(con) || !autoregister(con, scope, resolver)
|
|
259
|
+
|
|
260
|
+
tried << con
|
|
261
|
+
retry
|
|
262
|
+
rescue PatternSyntaxError => e
|
|
263
|
+
raise e.exception(PatternSyntaxError.with_caret(e.message)), cause: nil
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# A pattern named a constructor nobody declared: if a Ruby `Data` or
|
|
268
|
+
# `Struct` class of that name is visible (through `resolver`, which sees
|
|
269
|
+
# what the definition block sees, or at top level), it becomes a type of
|
|
270
|
+
# its own (`HaskellMatch.sealed`) so plain Ruby value classes match
|
|
271
|
+
# without a declaration. Returns whether a type was registered.
|
|
272
|
+
def autoregister(name, scope, resolver = nil)
|
|
273
|
+
return false if HaskellMatch.constructor(name, scope)
|
|
274
|
+
|
|
275
|
+
klass = resolver&.call(name)
|
|
276
|
+
klass = Object.const_get(name) if klass.nil? && Object.const_defined?(name)
|
|
277
|
+
return false unless klass.is_a?(Class) && (klass < Data || klass < Struct)
|
|
278
|
+
return false if klass.singleton_class.include?(Constructor::SealedClassMethods) || klass.include?(Constructor)
|
|
279
|
+
|
|
280
|
+
HaskellMatch.sealed(name, klass, under: nil, scope: scope)
|
|
281
|
+
true
|
|
282
|
+
rescue NameError, CompileError
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Returns [matcher, bodies, guards] where bodies/guards are arrays of
|
|
287
|
+
# callables taking the bound values positionally.
|
|
288
|
+
def compile(name, clauses, exhaustive:, overlapping:, kind: "equation", klass: Native::Matcher,
|
|
289
|
+
scope: Native::GLOBAL_SCOPE, resolver: nil)
|
|
290
|
+
raise DefinitionError, "'#{name}' has no clauses" if clauses.empty?
|
|
291
|
+
|
|
292
|
+
exhaustive = policy(exhaustive, :exhaustive)
|
|
293
|
+
overlapping = policy(overlapping, :overlapping)
|
|
294
|
+
matcher = build_matcher(
|
|
295
|
+
klass,
|
|
296
|
+
name.to_s,
|
|
297
|
+
clauses.map(&:patterns),
|
|
298
|
+
clauses.map { |c| !c.guard.nil? && !c.guard.equal?(ClauseBuilder::OTHERWISE) },
|
|
299
|
+
WITNESS_LIMIT,
|
|
300
|
+
scope,
|
|
301
|
+
resolver: resolver
|
|
302
|
+
)
|
|
303
|
+
report(name, kind, clauses, matcher, exhaustive, overlapping)
|
|
304
|
+
|
|
305
|
+
names = matcher.names
|
|
306
|
+
bodies = clauses.each_with_index.map do |c, i|
|
|
307
|
+
BindingPlan.adapt(c.body, names[i], i, "body", name)
|
|
308
|
+
end
|
|
309
|
+
guards = clauses.each_with_index.map do |c, i|
|
|
310
|
+
next nil if c.guard.nil? || c.guard.equal?(ClauseBuilder::OTHERWISE)
|
|
311
|
+
|
|
312
|
+
BindingPlan.adapt(c.guard, names[i], i, "guard", name)
|
|
313
|
+
end
|
|
314
|
+
guards = nil if guards.all?(&:nil?)
|
|
315
|
+
[matcher, bodies, guards]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def report(name, kind, clauses, matcher, exhaustive, overlapping)
|
|
319
|
+
redundant = matcher.instance_variable_get(:@redundant)
|
|
320
|
+
if !redundant.empty? && overlapping != :ignore
|
|
321
|
+
lines = redundant.map do |i|
|
|
322
|
+
c = clauses[i]
|
|
323
|
+
loc = c.location ? " (#{c.location.join(':')})" : ""
|
|
324
|
+
" #{name} #{c} = ...#{loc}"
|
|
325
|
+
end
|
|
326
|
+
message = "Pattern match#{redundant.size > 1 ? 'es are' : ' is'} redundant\n" \
|
|
327
|
+
"In #{article(kind)} #{kind} for '#{name}':\n#{lines.join("\n")}"
|
|
328
|
+
if overlapping == :error
|
|
329
|
+
raise RedundantClauseError.new(message, redundant)
|
|
330
|
+
else
|
|
331
|
+
warn "haskell_match: #{message}"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
missing = matcher.instance_variable_get(:@missing)
|
|
336
|
+
if !missing.empty? && exhaustive != :ignore
|
|
337
|
+
shown = missing.map { |m| " #{m.empty? ? '(no clause can match)' : m}" }
|
|
338
|
+
shown << " ..." if matcher.instance_variable_get(:@missing_truncated)
|
|
339
|
+
message = "Pattern match(es) are non-exhaustive\n" \
|
|
340
|
+
"In #{article(kind)} #{kind} for '#{name}':\n" \
|
|
341
|
+
" Patterns not matched:\n#{shown.map { |s| ' ' + s }.join("\n")}"
|
|
342
|
+
if exhaustive == :error
|
|
343
|
+
raise NonExhaustiveError.new(message, missing)
|
|
344
|
+
else
|
|
345
|
+
warn "haskell_match: #{message}"
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def article(word)
|
|
351
|
+
word.match?(/\A[aeiou]/i) ? "an" : "a"
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|