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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaskellMatch
|
|
4
|
+
# Mixin providing `fn`, `case_of`, `pattern`, `data` and `hdef`.
|
|
5
|
+
#
|
|
6
|
+
# class Calculator
|
|
7
|
+
# extend HaskellMatch::DSL
|
|
8
|
+
# include Maybe
|
|
9
|
+
#
|
|
10
|
+
# hdef :or_zero do
|
|
11
|
+
# on("Just x") { |x| x }
|
|
12
|
+
# on("Nothing") { 0 }
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
module DSL
|
|
16
|
+
def fn(name = nil, **options, &definition)
|
|
17
|
+
HaskellMatch.fn(name, **options, &definition)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def case_of(*values, **options, &definition)
|
|
21
|
+
HaskellMatch.case_of(*values, **options, &definition)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pattern(source = nil, **options, &block)
|
|
25
|
+
HaskellMatch.pattern(source, **options, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def data(decl, **options)
|
|
29
|
+
HaskellMatch.data(decl, **options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Define an instance method (when extended onto a class or module) or a
|
|
33
|
+
# singleton method (when `self` is any other object) by clauses. Clause
|
|
34
|
+
# bodies and guards run with `self` set to the receiver.
|
|
35
|
+
def hdef(name, exhaustive: HaskellMatch.exhaustive, overlapping: HaskellMatch.overlapping,
|
|
36
|
+
scope: Native::GLOBAL_SCOPE, &definition)
|
|
37
|
+
raise ArgumentError, "hdef needs a block with on(...) clauses" unless definition
|
|
38
|
+
|
|
39
|
+
builder = ClauseBuilder.collect(definition)
|
|
40
|
+
clauses = builder.clauses
|
|
41
|
+
matcher, _bodies, _guards = Compiler.compile(name, clauses, exhaustive: exhaustive, overlapping: overlapping,
|
|
42
|
+
scope: scope, resolver: builder.constant_resolver)
|
|
43
|
+
names = matcher.names
|
|
44
|
+
guards = clauses.map { |c| c.guard.equal?(ClauseBuilder::OTHERWISE) ? nil : c.guard }
|
|
45
|
+
plans = clauses.each_with_index.map do |c, i|
|
|
46
|
+
[BindingPlan.adapt(c.body, names[i], i, "body", name),
|
|
47
|
+
(guards[i] && BindingPlan.adapt(guards[i], names[i], i, "guard", name))]
|
|
48
|
+
end
|
|
49
|
+
has_guards = guards.any?
|
|
50
|
+
|
|
51
|
+
impl = lambda do |receiver, args|
|
|
52
|
+
guard_procs = nil
|
|
53
|
+
if has_guards
|
|
54
|
+
guard_procs = plans.map do |_, g|
|
|
55
|
+
next nil unless g
|
|
56
|
+
|
|
57
|
+
->(*vals) { receiver.instance_exec(*vals, &g) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
index, values = matcher.select_with(args, guard_procs)
|
|
61
|
+
raise MatchError, "Non-exhaustive patterns in #{name}" if index.nil?
|
|
62
|
+
|
|
63
|
+
receiver.instance_exec(*values, &plans[index][0])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
target = is_a?(Module) ? self : singleton_class
|
|
67
|
+
target.define_method(name) { |*args| impl.call(self, args) }
|
|
68
|
+
name
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaskellMatch
|
|
4
|
+
# Base class for every error raised by haskell_match.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised while compiling patterns (definition time).
|
|
8
|
+
class CompileError < Error; end
|
|
9
|
+
|
|
10
|
+
# Malformed pattern text.
|
|
11
|
+
class PatternSyntaxError < CompileError
|
|
12
|
+
# Reformat the native "msg (column N in \"src\")" into a message that
|
|
13
|
+
# shows the pattern with a caret under the column.
|
|
14
|
+
def self.with_caret(message)
|
|
15
|
+
m = message.match(/\A(.*) \(column (\d+) in (".*")\)\z/m)
|
|
16
|
+
return message unless m
|
|
17
|
+
|
|
18
|
+
src = begin
|
|
19
|
+
eval(m[3]) # rubocop:disable Security/Eval -- a Rust-escaped string literal
|
|
20
|
+
rescue SyntaxError, StandardError
|
|
21
|
+
return message
|
|
22
|
+
end
|
|
23
|
+
col = m[2].to_i
|
|
24
|
+
"#{m[1]}\n #{src}\n #{' ' * [col - 1, 0].max}^"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A constructor name that no registered data type declares.
|
|
29
|
+
class UnknownConstructorError < CompileError; end
|
|
30
|
+
|
|
31
|
+
# A constructor applied to the wrong number of arguments.
|
|
32
|
+
class ArityError < CompileError; end
|
|
33
|
+
|
|
34
|
+
# Patterns of different types in the same position.
|
|
35
|
+
class PatternTypeError < CompileError; end
|
|
36
|
+
|
|
37
|
+
# The same variable bound twice in one clause.
|
|
38
|
+
class DuplicateVariableError < CompileError; end
|
|
39
|
+
|
|
40
|
+
# Record pattern problems (unknown field, positional constructor, ...).
|
|
41
|
+
class FieldError < CompileError; end
|
|
42
|
+
|
|
43
|
+
# Malformed `data` declaration.
|
|
44
|
+
class DataDeclarationError < CompileError; end
|
|
45
|
+
|
|
46
|
+
# Clauses of one function with different numbers of arguments.
|
|
47
|
+
class ClauseArityError < CompileError; end
|
|
48
|
+
|
|
49
|
+
# The clauses do not cover every possible value (GHC: -Wincomplete-patterns).
|
|
50
|
+
class NonExhaustiveError < CompileError
|
|
51
|
+
attr_reader :missing
|
|
52
|
+
|
|
53
|
+
def initialize(message, missing = [])
|
|
54
|
+
super(message)
|
|
55
|
+
@missing = missing
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A clause can never be selected (GHC: -Woverlapping-patterns).
|
|
60
|
+
class RedundantClauseError < CompileError
|
|
61
|
+
attr_reader :clauses
|
|
62
|
+
|
|
63
|
+
def initialize(message, clauses = [])
|
|
64
|
+
super(message)
|
|
65
|
+
@clauses = clauses
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Problems with the Ruby side of a definition (bad block parameters, ...).
|
|
70
|
+
class DefinitionError < CompileError; end
|
|
71
|
+
|
|
72
|
+
# Raised at match time.
|
|
73
|
+
class MatchError < Error; end
|
|
74
|
+
|
|
75
|
+
# A value is not of the type the patterns expect (what Haskell's type
|
|
76
|
+
# checker would have rejected statically).
|
|
77
|
+
class TypeMismatchError < MatchError; end
|
|
78
|
+
|
|
79
|
+
# An irrefutable (`~`) pattern failed to destructure after its clause was
|
|
80
|
+
# selected.
|
|
81
|
+
class IrrefutablePatternError < MatchError; end
|
|
82
|
+
|
|
83
|
+
# Recursion through compiled functions exceeded `HaskellMatch.max_depth`.
|
|
84
|
+
class StackOverflowError < Error; end
|
|
85
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaskellMatch
|
|
4
|
+
# A constructor argument of the wrong type (`HaskellMatch.data` with
|
|
5
|
+
# `check_types: true`).
|
|
6
|
+
class FieldTypeError < Error; end
|
|
7
|
+
|
|
8
|
+
# Optional run-time checks of the field types written in a `data`
|
|
9
|
+
# declaration:
|
|
10
|
+
#
|
|
11
|
+
# HaskellMatch.data "Person = Person { name :: String, age :: Int }", check_types: true
|
|
12
|
+
# Person.new("Al", "3") # raises FieldTypeError: field 'age' expects Int, got "3" (String)
|
|
13
|
+
#
|
|
14
|
+
# Checks follow the declared Haskell type: `Int`/`Integer` take Integers,
|
|
15
|
+
# `Double`/`Float` any Numeric, `String` a String (or a list of
|
|
16
|
+
# characters), `Char` a one-character String, `Bool` true/false, `[a]` any
|
|
17
|
+
# list (Array, String, LazyList, Enumerator), `(a, b)` an Array of that
|
|
18
|
+
# size with each element checked, `a -> b` anything callable, a declared
|
|
19
|
+
# data type (`Maybe a`, `Shape`) a value of that type, any other
|
|
20
|
+
# capitalised name a Ruby class or module of that name (`Hash`, `Time`,
|
|
21
|
+
# `MyApp::Money`) when one exists, and a type variable anything.
|
|
22
|
+
module FieldTypes
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Wrap `klass`'s constructor so each field is checked against `types`.
|
|
26
|
+
def install(klass, types, scope)
|
|
27
|
+
checkers = types.map { |t| checker(t, scope) }
|
|
28
|
+
return if checkers.all?(&:nil?)
|
|
29
|
+
|
|
30
|
+
names = klass.members
|
|
31
|
+
cname = klass.constructor_name
|
|
32
|
+
klass.define_method(:initialize) do |**kw|
|
|
33
|
+
names.each_with_index do |n, i|
|
|
34
|
+
next unless kw.key?(n)
|
|
35
|
+
|
|
36
|
+
check = checkers[i]
|
|
37
|
+
next if check.nil? || check.call(kw[n])
|
|
38
|
+
|
|
39
|
+
raise FieldTypeError,
|
|
40
|
+
"#{cname}: field '#{n}' expects #{types[i]}, got #{kw[n].inspect} (#{kw[n].class})"
|
|
41
|
+
end
|
|
42
|
+
super(**kw)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
LIST = ->(v) { v.is_a?(Array) || v.is_a?(String) || v.is_a?(LazyList) || v.is_a?(Enumerator) }
|
|
47
|
+
|
|
48
|
+
# A predicate for Haskell type text, or nil when the type is unchecked.
|
|
49
|
+
def checker(text, scope = Native::GLOBAL_SCOPE)
|
|
50
|
+
t = strip_parens(text.to_s.strip)
|
|
51
|
+
return nil if t.empty?
|
|
52
|
+
return ->(v) { v.respond_to?(:call) } if split_top(t, "->").size > 1
|
|
53
|
+
|
|
54
|
+
return ->(v) { v == [] } if t == "()"
|
|
55
|
+
if t.start_with?("(") && t.end_with?(")")
|
|
56
|
+
parts = split_top(t[1..-2], ",")
|
|
57
|
+
if parts.size > 1
|
|
58
|
+
subs = parts.map { |part| checker(part, scope) }
|
|
59
|
+
return lambda { |v|
|
|
60
|
+
v.is_a?(Array) && v.size == subs.size && subs.each_with_index.all? { |c, i| c.nil? || c.call(v[i]) }
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
return LIST if t.start_with?("[") && t.end_with?("]")
|
|
65
|
+
return nil if t.match?(/\A[a-z]/) # a type variable
|
|
66
|
+
|
|
67
|
+
head = t.split(/\s+/).first.sub(/\A!/, "")
|
|
68
|
+
case head
|
|
69
|
+
when "Int", "Integer", "Word", "Int8", "Int16", "Int32", "Int64", "Word8", "Word16", "Word32", "Word64", "Natural"
|
|
70
|
+
->(v) { v.is_a?(Integer) }
|
|
71
|
+
when "Double", "Float", "Rational", "Num", "Real", "Fractional"
|
|
72
|
+
->(v) { v.is_a?(Numeric) }
|
|
73
|
+
when "String", "Text"
|
|
74
|
+
->(v) { v.is_a?(String) || v.is_a?(Array) || v.is_a?(LazyList) }
|
|
75
|
+
when "Char"
|
|
76
|
+
->(v) { v.is_a?(String) && v.length == 1 }
|
|
77
|
+
when "Bool"
|
|
78
|
+
->(v) { v == true || v == false }
|
|
79
|
+
when "Symbol"
|
|
80
|
+
->(v) { v.is_a?(Symbol) }
|
|
81
|
+
else
|
|
82
|
+
mod = HaskellMatch.type_module(head, scope)
|
|
83
|
+
return ->(v) { mod === v } if mod # rubocop:disable Style/CaseEquality
|
|
84
|
+
|
|
85
|
+
const = ruby_constant(head)
|
|
86
|
+
const.is_a?(Module) ? ->(v) { v.is_a?(const) } : nil
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ruby_constant(name)
|
|
91
|
+
Object.const_get(name)
|
|
92
|
+
rescue NameError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# `(T)` -> `T`, when the parentheses wrap the whole text.
|
|
97
|
+
def strip_parens(t)
|
|
98
|
+
while t.start_with?("(") && t.end_with?(")") && split_top(t[1..-2], ",").size == 1 && balanced?(t[1..-2])
|
|
99
|
+
t = t[1..-2].strip
|
|
100
|
+
end
|
|
101
|
+
t
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def balanced?(t)
|
|
105
|
+
depth = 0
|
|
106
|
+
t.each_char do |c|
|
|
107
|
+
depth += 1 if "([".include?(c)
|
|
108
|
+
depth -= 1 if ")]".include?(c)
|
|
109
|
+
return false if depth.negative?
|
|
110
|
+
end
|
|
111
|
+
depth.zero?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Split on `sep` at bracket depth zero.
|
|
115
|
+
def split_top(t, sep)
|
|
116
|
+
parts = []
|
|
117
|
+
depth = 0
|
|
118
|
+
cur = +""
|
|
119
|
+
i = 0
|
|
120
|
+
while i < t.length
|
|
121
|
+
c = t[i]
|
|
122
|
+
if "([".include?(c)
|
|
123
|
+
depth += 1
|
|
124
|
+
elsif ")]".include?(c)
|
|
125
|
+
depth -= 1
|
|
126
|
+
end
|
|
127
|
+
if depth.zero? && t[i, sep.length] == sep && !(sep == "," && i.zero?)
|
|
128
|
+
parts << cur.strip
|
|
129
|
+
cur = +""
|
|
130
|
+
i += sep.length
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
cur << c
|
|
134
|
+
i += 1
|
|
135
|
+
end
|
|
136
|
+
parts << cur.strip
|
|
137
|
+
parts
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HaskellMatch
|
|
4
|
+
# A function defined by pattern-matching clauses.
|
|
5
|
+
#
|
|
6
|
+
# length = HaskellMatch.fn(:length) do
|
|
7
|
+
# on("[]") { 0 }
|
|
8
|
+
# on("(_:xs)") { |xs| 1 + length.(xs) }
|
|
9
|
+
# end
|
|
10
|
+
# length.([1, 2, 3]) # => 3
|
|
11
|
+
# Returned by {Function#tail} and {Function#defer}: tells the native `call`
|
|
12
|
+
# to continue with `function` applied to `args`, and (for `defer`) to pass
|
|
13
|
+
# the eventual result to `continuation`. The native call keeps pending
|
|
14
|
+
# continuations on its own stack, so recursion written this way is bounded
|
|
15
|
+
# by memory rather than by Ruby's VM stack.
|
|
16
|
+
TailCall = Data.define(:function, :args, :continuation)
|
|
17
|
+
|
|
18
|
+
class Function < Native::Matcher
|
|
19
|
+
attr_reader :clauses
|
|
20
|
+
|
|
21
|
+
# Request a tail call: `recur.tail(n - 1, acc * n)` as the last
|
|
22
|
+
# expression of a clause body re-enters the function (or any other
|
|
23
|
+
# function) in constant stack space, like a tail call in Haskell.
|
|
24
|
+
def tail(*args)
|
|
25
|
+
TailCall.new(self, args, nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Request a non-tail call whose result the block will receive:
|
|
29
|
+
# `length.defer(xs) { |n| 1 + n }` stands for `1 + length xs` and runs
|
|
30
|
+
# with the pending work kept off Ruby's stack, however deep it goes.
|
|
31
|
+
def defer(*args, &continuation)
|
|
32
|
+
raise ArgumentError, "defer needs a block to receive the result" unless continuation
|
|
33
|
+
|
|
34
|
+
TailCall.new(self, args, continuation)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Build a function from clauses (use {HaskellMatch.fn}). The native
|
|
38
|
+
# `new` compiles the patterns; the clause bodies and guards are then
|
|
39
|
+
# attached as instance variables the native `call` reads on every call.
|
|
40
|
+
#
|
|
41
|
+
# With `ractor: true` the clause bodies and guards are made shareable
|
|
42
|
+
# (`Ractor.make_shareable`), as is the function, so it can be sent to and
|
|
43
|
+
# called from other Ractors. In that mode the definition block cannot
|
|
44
|
+
# call methods of the surrounding object, and local variables captured
|
|
45
|
+
# by the bodies must already be shareable; recursion goes through the
|
|
46
|
+
# function's name or `recur`, which work in both modes.
|
|
47
|
+
#
|
|
48
|
+
# With `deep: true` the function's `call` is implemented in Ruby (see
|
|
49
|
+
# {DeepCall}): about 100 ns slower per call, but deep recursion costs a
|
|
50
|
+
# tenth of the memory and the GC scans a fifth as much. The default is
|
|
51
|
+
# {HaskellMatch.deep_by_default}.
|
|
52
|
+
def self.define(name, builder, exhaustive:, overlapping:, ractor: false, deep: HaskellMatch.deep_by_default,
|
|
53
|
+
scope: Native::GLOBAL_SCOPE)
|
|
54
|
+
clauses = builder.clauses
|
|
55
|
+
clauses = make_shareable(clauses) if ractor
|
|
56
|
+
resolver = builder.respond_to?(:constant_resolver) ? builder.constant_resolver : nil
|
|
57
|
+
f, bodies, guards = Compiler.compile(name.to_s, clauses, exhaustive: exhaustive, overlapping: overlapping,
|
|
58
|
+
klass: self, scope: scope, resolver: resolver)
|
|
59
|
+
f.instance_variable_set(:@scope, scope)
|
|
60
|
+
f.instance_variable_set(:@helpers, builder.helpers.freeze)
|
|
61
|
+
f.extend(DeepCall.module_for(f.arity)) if deep
|
|
62
|
+
f.send(:attach, clauses, bodies, guards)
|
|
63
|
+
builder.define_function(f, name.to_s)
|
|
64
|
+
Ractor.make_shareable(f) if ractor
|
|
65
|
+
f
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.make_shareable(clauses)
|
|
69
|
+
raise DefinitionError, "ractor: true requires Ractor support in this Ruby" unless defined?(Ractor)
|
|
70
|
+
|
|
71
|
+
clauses.map do |c|
|
|
72
|
+
body = Ractor.make_shareable(c.body)
|
|
73
|
+
guard = if c.guard.nil? || c.guard.equal?(ClauseBuilder::OTHERWISE)
|
|
74
|
+
c.guard
|
|
75
|
+
else
|
|
76
|
+
Ractor.make_shareable(c.guard)
|
|
77
|
+
end
|
|
78
|
+
Clause.new(c.patterns, guard, body, c.location)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
private_class_method :make_shareable
|
|
82
|
+
|
|
83
|
+
# call(*args) is native (or Ruby, in deep mode); these make a Function
|
|
84
|
+
# behave like a Proc.
|
|
85
|
+
def [](*args)
|
|
86
|
+
call(*args)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ===(*args)
|
|
90
|
+
call(*args)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether this function runs in deep mode (see {DeepCall}).
|
|
94
|
+
def deep?
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A lambda with the function's exact arity. Built on first use (a
|
|
99
|
+
# shareable function cannot hold a reference back to itself).
|
|
100
|
+
def to_proc
|
|
101
|
+
return ProcBuilder.build(self, arity) if frozen?
|
|
102
|
+
|
|
103
|
+
@proc ||= ProcBuilder.build(self, arity)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def curry
|
|
107
|
+
to_proc.curry(arity)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Composition, as for Procs: `(f >> g).(x)` is `g.(f.(x))` and
|
|
111
|
+
# `(f << g).(x)` is `f.(g.(x))` (Haskell's `g . f` and `f . g`).
|
|
112
|
+
def >>(other)
|
|
113
|
+
to_proc >> other
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def <<(other)
|
|
117
|
+
to_proc << other
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# The function as a curried Proc (memoised); what Haskell code receives
|
|
121
|
+
# when it passes a function as a value.
|
|
122
|
+
def curried
|
|
123
|
+
return to_proc if arity == 1
|
|
124
|
+
return @curried if defined?(@curried) && @curried
|
|
125
|
+
|
|
126
|
+
c = to_proc.curry(arity)
|
|
127
|
+
@curried = c unless frozen?
|
|
128
|
+
c
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Variables bound by each clause, in order.
|
|
132
|
+
def bindings
|
|
133
|
+
names
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The `where` helper functions defined inside this function, by name.
|
|
137
|
+
def helpers
|
|
138
|
+
@helpers || {}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The type scope the patterns were compiled in.
|
|
142
|
+
def scope
|
|
143
|
+
@scope || Native::GLOBAL_SCOPE
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Human-readable dump of the compiled decision tree.
|
|
147
|
+
def decision_tree
|
|
148
|
+
tree
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def inspect
|
|
152
|
+
"#<HaskellMatch::Function #{name}/#{arity}>"
|
|
153
|
+
end
|
|
154
|
+
alias to_s inspect
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def attach(clauses, bodies, guards)
|
|
159
|
+
@clauses = clauses.freeze
|
|
160
|
+
@bodies = bodies.freeze
|
|
161
|
+
@guards = guards&.freeze
|
|
162
|
+
@proc = nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Builds the lambda returned by {Function#to_proc}. It is created here,
|
|
166
|
+
# rather than inside the function, so its `self` is this (shareable)
|
|
167
|
+
# module and `Ractor.make_shareable` accepts it.
|
|
168
|
+
module ProcBuilder
|
|
169
|
+
def self.build(function, arity)
|
|
170
|
+
params = (1..arity).map { |i| "a#{i}" }.join(", ")
|
|
171
|
+
# a lambda with the function's exact arity
|
|
172
|
+
eval("->(#{params}) { function.call(#{params}) }", binding, __FILE__, __LINE__) # rubocop:disable Style/EvalWithLocation
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class << self
|
|
178
|
+
attr_writer :exhaustive, :overlapping
|
|
179
|
+
|
|
180
|
+
# Every this many nested calls the next clause body runs in a fresh Fiber
|
|
181
|
+
# (with its own VM and machine stacks), so plain recursion is bounded by
|
|
182
|
+
# memory rather than by Ruby's stack size. 0 disables this.
|
|
183
|
+
def stack_segment
|
|
184
|
+
Native.stack_segment
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def stack_segment=(levels)
|
|
188
|
+
Native.stack_segment = levels
|
|
189
|
+
DeepCall.segment = Native.stack_segment
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Whether functions use deep mode unless told otherwise (default false).
|
|
193
|
+
attr_writer :deep_by_default
|
|
194
|
+
|
|
195
|
+
def deep_by_default
|
|
196
|
+
@deep_by_default ? true : false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Deepest allowed nesting of calls (0 = unlimited); beyond it
|
|
200
|
+
# {StackOverflowError} is raised rather than letting a runaway recursion
|
|
201
|
+
# take all memory.
|
|
202
|
+
def max_depth
|
|
203
|
+
Native.max_depth
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def max_depth=(levels)
|
|
207
|
+
Native.max_depth = levels
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Default policy for non-exhaustive clause sets: :error (Haskell's
|
|
211
|
+
# behaviour, the default), :warn or :ignore.
|
|
212
|
+
def exhaustive
|
|
213
|
+
@exhaustive.nil? ? :error : @exhaustive
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Default policy for redundant clauses: :error (default), :warn or :ignore.
|
|
217
|
+
def overlapping
|
|
218
|
+
@overlapping.nil? ? :error : @overlapping
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Define a function by clauses. See {Function}.
|
|
222
|
+
#
|
|
223
|
+
# Options:
|
|
224
|
+
# * `exhaustive:` true/:error (default), :warn, or false/:ignore
|
|
225
|
+
# * `overlapping:` true/:error (default), :warn, or false/:ignore
|
|
226
|
+
# * `ractor:` true to make the function Ractor-shareable (see {Function.define})
|
|
227
|
+
# * `deep:` true for Ruby-level body invocation (cheap deep recursion)
|
|
228
|
+
# * `scope:` the type scope the patterns resolve constructors in (see {HaskellMatch.new_scope})
|
|
229
|
+
def fn(name = nil, exhaustive: self.exhaustive, overlapping: self.overlapping, ractor: false,
|
|
230
|
+
deep: deep_by_default, scope: Native::GLOBAL_SCOPE, &definition)
|
|
231
|
+
raise ArgumentError, "HaskellMatch.fn needs a block with on(...) clauses" unless definition
|
|
232
|
+
|
|
233
|
+
options = { exhaustive: exhaustive, overlapping: overlapping, deep: deep, scope: scope }
|
|
234
|
+
builder = ClauseBuilder.collect(definition, ractor: ractor, options: options)
|
|
235
|
+
name ||= "anonymous function at #{definition.source_location&.join(':')}"
|
|
236
|
+
Function.define(name, builder, exhaustive: exhaustive, overlapping: overlapping, ractor: ractor, deep: deep,
|
|
237
|
+
scope: scope)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|