predicator 1.2.1 → 2.0.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +322 -46
  5. data/lib/predicator/ast.rb +74 -187
  6. data/lib/predicator/boundary.rb +237 -0
  7. data/lib/predicator/civil.rb +112 -0
  8. data/lib/predicator/duration_text.rb +92 -0
  9. data/lib/predicator/emitter/chains.rb +62 -0
  10. data/lib/predicator/emitter/statements.rb +86 -0
  11. data/lib/predicator/emitter.rb +187 -0
  12. data/lib/predicator/errors.rb +140 -0
  13. data/lib/predicator/evaluator/access.rb +108 -0
  14. data/lib/predicator/evaluator/arithmetic.rb +132 -0
  15. data/lib/predicator/evaluator/calls.rb +129 -0
  16. data/lib/predicator/evaluator/casts.rb +158 -0
  17. data/lib/predicator/evaluator/chronology.rb +88 -0
  18. data/lib/predicator/evaluator/compare.rb +158 -0
  19. data/lib/predicator/evaluator/json_functions.rb +167 -0
  20. data/lib/predicator/evaluator/json_scan.rb +303 -0
  21. data/lib/predicator/evaluator/library.rb +127 -0
  22. data/lib/predicator/evaluator/math_functions.rb +72 -0
  23. data/lib/predicator/evaluator/order.rb +120 -0
  24. data/lib/predicator/evaluator/rich_types.rb +146 -0
  25. data/lib/predicator/evaluator/statements.rb +151 -0
  26. data/lib/predicator/evaluator.rb +391 -117
  27. data/lib/predicator/float_text.rb +94 -0
  28. data/lib/predicator/iso_text.rb +99 -0
  29. data/lib/predicator/lexer/literals.rb +115 -0
  30. data/lib/predicator/lexer/numbers.rb +91 -0
  31. data/lib/predicator/lexer.rb +194 -0
  32. data/lib/predicator/parser/durations.rb +108 -0
  33. data/lib/predicator/parser/operators.rb +86 -0
  34. data/lib/predicator/parser/postfix.rb +57 -0
  35. data/lib/predicator/parser/primaries.rb +122 -0
  36. data/lib/predicator/parser/statements.rb +114 -0
  37. data/lib/predicator/parser/token_text.rb +52 -0
  38. data/lib/predicator/parser.rb +217 -438
  39. data/lib/predicator/result.rb +46 -0
  40. data/lib/predicator/tagged/reader.rb +139 -0
  41. data/lib/predicator/tagged/writer.rb +106 -0
  42. data/lib/predicator/tagged.rb +134 -0
  43. data/lib/predicator/values.rb +147 -0
  44. data/lib/predicator/version.rb +4 -1
  45. data/lib/predicator.rb +384 -14
  46. metadata +53 -147
  47. data/.gitignore +0 -11
  48. data/.rubocop.yml +0 -46
  49. data/.travis.yml +0 -5
  50. data/CODE_OF_CONDUCT.md +0 -49
  51. data/Gemfile +0 -4
  52. data/HISTORY.md +0 -51
  53. data/Rakefile +0 -24
  54. data/lib/predicator/context.rb +0 -18
  55. data/lib/predicator/lexer.rex +0 -69
  56. data/lib/predicator/lexer.rex.rb +0 -187
  57. data/lib/predicator/parser.y +0 -120
  58. data/lib/predicator/visitors/dot.rb +0 -113
  59. data/lib/predicator/visitors/each.rb +0 -16
  60. data/lib/predicator/visitors/instructions.rb +0 -183
  61. data/lib/predicator/visitors/string.rb +0 -76
  62. data/lib/predicator/visitors/visitor.rb +0 -100
  63. data/lib/predicator/visitors.rb +0 -5
  64. data/predicator.gemspec +0 -34
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The five arithmetic opcodes, as the reference's ISA (docs/isa.md,
6
+ # section 5) states them: the handlers {Machine} runs for their rows of
7
+ # {INSTRUCTIONS}, and the rule each applies to its pair of operands.
8
+ #
9
+ # Integer and Float stay apart: two Integers answer an Integer, and a
10
+ # Float on either side answers a Float. A Float answer that is not finite
11
+ # (an overflow) has no member in the value domain, and is refused as an
12
+ # EvaluationError with the reason non_finite_number; the reference has no
13
+ # answer there of its own to follow. Integers have no bound.
14
+ #
15
+ # A pair no rule takes is a TypeMismatchError whose reason is the opcode,
16
+ # or the unbound-variable error when an operand is the undefined an
17
+ # unbound load pushed.
18
+ module Arithmetic
19
+ # What a pair no rule takes answers inside this module.
20
+ NO_RULE = Object.new.freeze
21
+
22
+ # What each opcode's refusal says it wanted.
23
+ WANTED = {
24
+ "add" => "a number or a string", "subtract" => "a number or a date", "multiply" => "a number",
25
+ "divide" => "a number", "modulo" => "an integer"
26
+ }.freeze
27
+
28
+ private
29
+
30
+ def add(at) = arithmetic(at, "add") { |left, right| sum(left, right) }
31
+ def subtract(at) = arithmetic(at, "subtract") { |left, right| difference(left, right) }
32
+ def multiply(at) = arithmetic(at, "multiply") { |left, right| numbers?(left, right) ? left * right : NO_RULE }
33
+
34
+ # The zero check runs before the type check, for an Integer 0 and a
35
+ # Float 0.0 (negative zero included) alike. Two Integers divide by
36
+ # truncating toward zero.
37
+ def divide(at)
38
+ arithmetic(at, "divide", zero: ->(right) { number?(right) && right.zero? }) do |left, right|
39
+ next NO_RULE unless numbers?(left, right)
40
+
41
+ left.is_a?(Integer) && right.is_a?(Integer) ? truncated_quotient(left, right) : left / right
42
+ end
43
+ end
44
+
45
+ # Integers only, a Float operand included in the refusal. The zero
46
+ # check runs before the type check for an Integer 0 alone: a Float 0.0
47
+ # is a type mismatch, not modulo by zero. The remainder takes the sign
48
+ # of the dividend.
49
+ def modulo(at)
50
+ arithmetic(at, "modulo", zero: ->(right) { right.is_a?(Integer) && right.zero? }) do |left, right|
51
+ left.is_a?(Integer) && right.is_a?(Integer) ? left.remainder(right) : NO_RULE
52
+ end
53
+ end
54
+
55
+ # Pops right then left, refuses a zero divisor when zero says so, and
56
+ # pushes what the rule answers.
57
+ def arithmetic(at, opcode, zero: nil)
58
+ return insufficient(opcode) if @stack.size < 2
59
+
60
+ right = @stack.pop
61
+ left = @stack.pop
62
+ return by_zero(opcode) if zero&.call(right)
63
+
64
+ answer = yield(left, right)
65
+ return mismatch_pair(opcode, WANTED.fetch(opcode), left, right) if NO_RULE.equal?(answer)
66
+ return non_finite(opcode) if answer.is_a?(Float) && !answer.finite?
67
+
68
+ push(at, answer)
69
+ end
70
+
71
+ # Numbers add. Strings join, and a number beside a string is written
72
+ # as text first. Lists join. A date or a datetime and a duration, in
73
+ # either order, move the date.
74
+ def sum(left, right)
75
+ return left + right if numbers?(left, right) || both?(left, right, String) || both?(left, right, Array)
76
+ return text(left) + text(right) if text_pair?(left, right)
77
+
78
+ shifted(left, right)
79
+ end
80
+
81
+ def text_pair?(left, right) = (left.is_a?(String) && number?(right)) || (number?(left) && right.is_a?(String))
82
+
83
+ def text(value) = value.is_a?(String) ? value : number_text(value)
84
+
85
+ # A date or a datetime moved forward by a duration on either side.
86
+ def shifted(left, right)
87
+ return Chronology.shift(left, right, 1) if Chronology.chronological?(left) && right.is_a?(Duration)
88
+ return Chronology.shift(right, left, 1) if left.is_a?(Duration) && Chronology.chronological?(right)
89
+
90
+ NO_RULE
91
+ end
92
+
93
+ # Numbers subtract. Two dates or datetimes answer the duration between
94
+ # them, and a date or a datetime minus a duration moves it back. No
95
+ # string or list rule: subtract never joins.
96
+ def difference(left, right)
97
+ return left - right if numbers?(left, right)
98
+ return Chronology.difference(left, right) if Chronology.chronological?(left) && Chronology.chronological?(right)
99
+ return Chronology.shift(left, right, -1) if Chronology.chronological?(left) && right.is_a?(Duration)
100
+
101
+ NO_RULE
102
+ end
103
+
104
+ def number?(value) = value.is_a?(Integer) || value.is_a?(Float)
105
+ def numbers?(left, right) = number?(left) && number?(right)
106
+ def both?(left, right, type) = left.is_a?(type) && right.is_a?(type)
107
+
108
+ def number_text(number) = number.is_a?(Float) ? FloatText.spell(number) : number.to_s
109
+
110
+ def truncated_quotient(left, right)
111
+ whole = left.abs / right.abs
112
+ left.negative? == right.negative? ? whole : -whole
113
+ end
114
+
115
+ # A refused pair: the operand reported is the undefined one, if either
116
+ # is, so that an unbound load is named.
117
+ def mismatch_pair(opcode, wanted, left, right)
118
+ mismatch(opcode, wanted, UNDEFINED.equal?(right) && !UNDEFINED.equal?(left) ? right : left)
119
+ end
120
+
121
+ def by_zero(opcode)
122
+ reason = opcode == "divide" ? "division_by_zero" : "modulo_by_zero"
123
+ EvaluationError.new(reason:, message: "#{opcode} was given a zero divisor")
124
+ end
125
+
126
+ def non_finite(opcode)
127
+ EvaluationError.new(reason: "non_finite_number", message: "#{opcode} answered a float that is not finite")
128
+ end
129
+ end
130
+ private_constant :Arithmetic
131
+ end
132
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The `call` opcode, as the reference's ISA (docs/isa.md, section 5)
6
+ # states it: the handler {Machine} runs for its row of {INSTRUCTIONS}.
7
+ #
8
+ # `call` pops its argument count of values, the deepest first, and pushes
9
+ # what the named function answers. The name is looked up first among the
10
+ # functions the caller passed to this evaluation, then in the package's
11
+ # library ({Library}); nothing else is reachable, and nothing is
12
+ # registered globally.
13
+ #
14
+ # Every refusal is an EvaluationError, and except for too few values on
15
+ # the stack (reason insufficient_arguments) its reason is its message,
16
+ # as the reference writes them:
17
+ #
18
+ # - a name neither holds: `Unknown function: NAME`;
19
+ # - a count of arguments the function does not take:
20
+ # `Function NAME() expects COUNTS arguments, got N`, where COUNTS is a
21
+ # count, the counts it takes joined by " or ", or "at least" a count;
22
+ # - a library function refusing its arguments: the library's sentence,
23
+ # e.g. `len() expects a string argument`;
24
+ # - a function that raises: `Function NAME() raised: ` and the
25
+ # exception's inspect.
26
+ #
27
+ # A host function is called with a copy of each argument
28
+ # ({Boundary.copy}), so that nothing it does to a list, a map or a
29
+ # String it is handed changes the context or a value the evaluation
30
+ # holds. Its answer is checked against the value domain: one outside it
31
+ # is refused with an EvaluationError carrying the boundary's reason
32
+ # (unsupported_host_value, non_finite_number, cyclic_value or
33
+ # depth_limit_exceeded), and nothing is pushed.
34
+ module Calls
35
+ # How a library function refuses its arguments: the message is the
36
+ # refusal's reason. Raised and rescued inside a call; it never leaves
37
+ # one.
38
+ class Refusal < StandardError; end
39
+
40
+ include Library
41
+
42
+ # The counts of arguments a host's callable takes, as a Range: its
43
+ # required positional parameters up to those and its optional ones,
44
+ # with no end when it takes a rest parameter. A Proc is read as a
45
+ # lambda would be, so a block's missing arguments are refused rather
46
+ # than read as null.
47
+ def self.counts(callable)
48
+ parameters = parameters_of(callable)
49
+ required = parameters.count { |kind, _| kind == :req }
50
+ return (required..) if parameters.any? { |kind, _| kind == :rest }
51
+
52
+ required..(required + parameters.count { |kind, _| kind == :opt })
53
+ end
54
+
55
+ def self.parameters_of(callable)
56
+ case callable
57
+ when Proc then callable.parameters(lambda: true)
58
+ when Method then callable.parameters
59
+ else callable.method(:call).parameters
60
+ end
61
+ end
62
+ private_class_method :parameters_of
63
+
64
+ private
65
+
66
+ # Pops arg_count values, calls the named function with them, first
67
+ # argument deepest, and pushes its answer.
68
+ def call_function(at, name, arg_count)
69
+ if @stack.size < arg_count
70
+ return EvaluationError.new(
71
+ reason: "insufficient_arguments",
72
+ message: "Function #{name}() expects #{arg_count} arguments, but only #{@stack.size} values on stack"
73
+ )
74
+ end
75
+
76
+ args = @stack.pop(arg_count)
77
+ answer = dispatch(name, args)
78
+ answer.is_a?(Answered) ? push(at, answer.value) : answer
79
+ end
80
+
81
+ # What a call that succeeded answered, kept apart from the error value
82
+ # a refused call answers, since a function may answer any value.
83
+ Answered = Data.define(:value)
84
+ private_constant :Answered
85
+
86
+ def dispatch(name, args)
87
+ callable, counts = resolve(name)
88
+ return call_error("Unknown function: #{name}") unless callable
89
+ return call_error(wrong_count(name, counts, args.size)) unless counts.include?(args.size)
90
+ return host_call(name, callable, args) if @functions.key?(name)
91
+
92
+ Answered.new(callable.call(*args))
93
+ rescue ClockFault then raise
94
+ rescue Refusal => e
95
+ call_error(e.message)
96
+ rescue StandardError => e
97
+ call_error("Function #{name}() raised: #{e.inspect}")
98
+ end
99
+
100
+ # Calls a host function with a copy of each argument, answering its
101
+ # answer, or the refusal of one outside the value domain ({Boundary}).
102
+ def host_call(name, callable, args)
103
+ answer = callable.call(*args.map { |arg| Boundary.copy(arg) })
104
+ Boundary.refusal(answer, "Function #{name}() answered a value that") || Answered.new(answer)
105
+ end
106
+
107
+ # The callable a name reaches and the counts of arguments it takes: a
108
+ # host function first, then the library's.
109
+ def resolve(name)
110
+ host = @functions[name]
111
+ return [host, Calls.counts(host)] if host
112
+
113
+ entry = Library::FUNCTIONS[name]
114
+ [method(entry.handler), entry.counts] if entry
115
+ end
116
+
117
+ def wrong_count(name, counts, given)
118
+ described =
119
+ if counts.end.nil? then "at least #{counts.begin}"
120
+ else counts.to_a.join(" or ")
121
+ end
122
+ "Function #{name}() expects #{described} arguments, got #{given}"
123
+ end
124
+
125
+ def call_error(message) = EvaluationError.new(reason: message, message:)
126
+ end
127
+ private_constant :Calls
128
+ end
129
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The `cast` opcode, as the reference's ISA (docs/isa.md, section 5)
6
+ # states it: the handler {Machine} runs for `cast`, and the conversion
7
+ # matrix it reads through.
8
+ #
9
+ # The operand is one of the seven scalar type names in {TYPE_NAMES}; any
10
+ # other operand is an unknown instruction. The cast pops one value and
11
+ # pushes what that value is as the named type. It never fails but on an
12
+ # empty stack: undefined converts to undefined, a value already of the
13
+ # type passes through, and a conversion that cannot produce a value of
14
+ # the type answers undefined (null, a list and a map included).
15
+ #
16
+ # The conversions:
17
+ #
18
+ # - integer: a float truncates toward zero; the text of an integer (an
19
+ # optional minus and decimal digits, nothing else) parses.
20
+ # - float: an integer widens; the text of a number (an optional minus,
21
+ # digits and an optional fraction, no exponent) parses.
22
+ # - string: an integer in decimal, a float as {FloatText} writes it, a
23
+ # boolean as `true` or `false`, a date as `YYYY-MM-DD`, a datetime in
24
+ # UTC as {Predicator::DateTime#to_s} writes it, a duration in the
25
+ # literal grammar {DurationText} writes.
26
+ # - boolean: exactly the text `true` or `false`.
27
+ # - date: a datetime's calendar date in UTC; the text of an ISO 8601
28
+ # calendar date that names a real date.
29
+ # - datetime: a date at midnight UTC; the text of an ISO 8601 datetime
30
+ # carrying a UTC offset, as the instant it names.
31
+ # - duration: the text of a duration literal, read as {DurationText}
32
+ # reads it.
33
+ #
34
+ # A map carrying the seven units the reference's duration always holds
35
+ # (years to seconds), each an Integer, casts as a duration: it passes
36
+ # through `::duration` and is written by `::string` in the literal
37
+ # grammar. The map is read by its String unit names, as `relative_date`
38
+ # reads one. `object_set` on a duration answers the map of its units
39
+ # with the key written; when that key is not a unit name, the cast
40
+ # writes the duration's own units, as the reference's cast does after
41
+ # its `object_set`. When the key is a unit name the two part: this reads
42
+ # the unit written (`days` set to 5 on `3d` writes `5d`, `milliseconds`
43
+ # set to 500 writes `3d500ms`, a unit set to a non-Integer is
44
+ # undefined), while the reference keeps its duration's own unit (`3d`),
45
+ # since its durations carry their units under keys a written String key
46
+ # does not replace. A String-keyed map from the context that carries
47
+ # the unit names is a duration here and undefined in the reference. A
48
+ # map whose units are not all Integers casts to undefined.
49
+ #
50
+ # The text forms `::date` and `::datetime` read are the reference's, as
51
+ # {IsoText} states them.
52
+ #
53
+ # Where the reference raises rather than answering, this answers
54
+ # undefined: an integer or a number's text too large for a finite Float
55
+ # (the domain holds no other) and an instant {IsoText} refuses for its
56
+ # year. A number's text too close to zero for any other Float reads as
57
+ # zero, signed as the text is, as the reference reads it.
58
+ module Casts
59
+ # The type names a cast converts to.
60
+ TYPE_NAMES = %w[integer float string boolean date datetime duration].freeze
61
+
62
+ # The units a map carries to be read as a duration.
63
+ DURATION_KEYS = %w[years months weeks days hours minutes seconds].freeze
64
+
65
+ INTEGER_TEXT = /\A-?[0-9]+\z/
66
+ FLOAT_TEXT = /\A-?[0-9]+(?:\.[0-9]+)?\z/
67
+
68
+ # The method that converts to each type.
69
+ CONVERTERS = TYPE_NAMES.to_h { |name| [name, :"cast_to_#{name}"] }.freeze
70
+
71
+ private
72
+
73
+ def cast(at, type_name)
74
+ return insufficient("cast") if @stack.empty?
75
+
76
+ value = @stack.pop
77
+ push(at, UNDEFINED.equal?(value) ? UNDEFINED : send(CONVERTERS.fetch(type_name), value))
78
+ end
79
+
80
+ def cast_to_integer(value)
81
+ case value
82
+ when Integer then value
83
+ when Float then value.truncate
84
+ when String then INTEGER_TEXT.match?(value) ? Integer(value, 10) : UNDEFINED
85
+ else UNDEFINED
86
+ end
87
+ end
88
+
89
+ def cast_to_float(value)
90
+ case value
91
+ when Float then value
92
+ when Integer then value.abs < FloatText::CEILING ? value.to_f : UNDEFINED
93
+ when String then FLOAT_TEXT.match?(value) ? float_of_text(value) : UNDEFINED
94
+ else UNDEFINED
95
+ end
96
+ end
97
+
98
+ # The Float nearest the number the text writes ({FloatText.read}), or
99
+ # undefined when it is too large for one.
100
+ def float_of_text(text) = FloatText.read(text) || UNDEFINED
101
+
102
+ def cast_to_string(value)
103
+ case value
104
+ when String then value
105
+ when Float then FloatText.spell(value)
106
+ when Integer, true, false, Predicator::Date, Predicator::DateTime then value.to_s
107
+ else cast_duration_text(value)
108
+ end
109
+ end
110
+
111
+ def cast_duration_text(value)
112
+ duration = cast_duration(value)
113
+ duration ? DurationText.write(duration) : UNDEFINED
114
+ end
115
+
116
+ def cast_to_boolean(value)
117
+ case value
118
+ when true, false then value
119
+ when "true" then true
120
+ when "false" then false
121
+ else UNDEFINED
122
+ end
123
+ end
124
+
125
+ def cast_to_date(value)
126
+ case value
127
+ when Predicator::Date then value
128
+ when Predicator::DateTime then Civil.date_of(value)
129
+ when String then IsoText.read_date(value) || UNDEFINED
130
+ else UNDEFINED
131
+ end
132
+ end
133
+
134
+ def cast_to_datetime(value)
135
+ case value
136
+ when Predicator::DateTime then value
137
+ when Predicator::Date then Civil.instant_at(value)
138
+ when String then IsoText.read_datetime(value) || UNDEFINED
139
+ else UNDEFINED
140
+ end
141
+ end
142
+
143
+ def cast_to_duration(value)
144
+ return DurationText.read(value) || UNDEFINED if value.is_a?(String)
145
+
146
+ cast_duration(value) ? value : UNDEFINED
147
+ end
148
+
149
+ # A duration, or a map carrying the seven units read as one, or nil.
150
+ def cast_duration(value)
151
+ return value if value.is_a?(Duration)
152
+
153
+ as_duration(value) if value.is_a?(Hash) && DURATION_KEYS.all? { |key| value.key?(key) }
154
+ end
155
+ end
156
+ private_constant :Casts
157
+ end
158
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The date arithmetic `add` and `subtract` reach for, as the reference
6
+ # states it at the tag the corpus names (its ISA, docs/isa.md, section 5,
7
+ # and its duration module where the ISA is silent).
8
+ #
9
+ # Two dates are apart by a duration in whole days and two datetimes by a
10
+ # duration in whole seconds, truncated toward zero; a date beside a
11
+ # datetime is read at midnight UTC. A date moved by a duration stays a
12
+ # date and a datetime stays a datetime. A month counts as 30 days and a
13
+ # year as 365, with no calendar rule and no month-end clamp: the
14
+ # reference's own conversion, which it calls approximate.
15
+ #
16
+ # The module holds no state.
17
+ module Chronology
18
+ MICROS_PER_SECOND = 1_000_000
19
+
20
+ # The day count of each date unit of a duration.
21
+ DAYS = { weeks: 7, months: 30, years: 365 }.freeze
22
+
23
+ # The second count of each time unit of a duration.
24
+ SECONDS = { minutes: 60, hours: 3600 }.freeze
25
+
26
+ module_function
27
+
28
+ # Whether value is a date or a datetime.
29
+ def chronological?(value) = value.is_a?(Predicator::Date) || value.is_a?(Predicator::DateTime)
30
+
31
+ # left minus right, both dates or datetimes, as a duration.
32
+ def difference(left, right)
33
+ if left.is_a?(Predicator::Date) && right.is_a?(Predicator::Date)
34
+ return Duration.new(days: Civil.days_of(left) - Civil.days_of(right))
35
+ end
36
+
37
+ Duration.new(seconds: truncated_seconds(micros(left) - micros(right)))
38
+ end
39
+
40
+ # anchor (a date or a datetime) moved by duration, forward when sign is
41
+ # 1 and back when it is -1.
42
+ def shift(anchor, duration, sign)
43
+ if anchor.is_a?(Predicator::Date)
44
+ return Civil.date_of_days(Civil.days_of(anchor) + (sign * whole_days(duration)))
45
+ end
46
+
47
+ moved = micros(anchor) + (sign * datetime_step(duration))
48
+ Predicator::DateTime.new(epoch_seconds: moved.div(MICROS_PER_SECOND), microsecond: moved % MICROS_PER_SECOND)
49
+ end
50
+
51
+ # The days a date moves by: the date units, then the whole days the
52
+ # time units add up to, truncated toward zero. Milliseconds do not move
53
+ # a date.
54
+ def whole_days(duration)
55
+ time = duration.seconds + SECONDS.sum { |unit, seconds| duration.public_send(unit) * seconds }
56
+ date_days(duration) + truncated(time, Civil::SECONDS_PER_DAY)
57
+ end
58
+
59
+ # The microseconds a datetime moves by. A positive milliseconds moves it
60
+ # by milliseconds; any other duration moves it by whole seconds, its
61
+ # milliseconds left out, as the reference splits the two.
62
+ def datetime_step(duration)
63
+ seconds = duration.seconds + SECONDS.sum { |unit, factor| duration.public_send(unit) * factor } +
64
+ (date_days(duration) * Civil::SECONDS_PER_DAY)
65
+ step = seconds * MICROS_PER_SECOND
66
+ duration.milliseconds.positive? ? step + (duration.milliseconds * 1000) : step
67
+ end
68
+
69
+ def date_days(duration) = duration.days + DAYS.sum { |unit, days| duration.public_send(unit) * days }
70
+
71
+ # A date at midnight UTC, or a datetime, as microseconds since the epoch.
72
+ def micros(value)
73
+ return Civil.days_of(value) * Civil::SECONDS_PER_DAY * MICROS_PER_SECOND if value.is_a?(Predicator::Date)
74
+
75
+ (value.epoch_seconds * MICROS_PER_SECOND) + value.microsecond
76
+ end
77
+
78
+ def truncated_seconds(micros) = truncated(micros, MICROS_PER_SECOND)
79
+
80
+ # amount divided by unit, truncated toward zero.
81
+ def truncated(amount, unit)
82
+ whole = amount.abs / unit
83
+ amount.negative? ? -whole : whole
84
+ end
85
+ end
86
+ private_constant :Chronology
87
+ end
88
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The `compare` opcode's answer for one operator and one pair of values,
6
+ # as the reference's ISA (docs/isa.md, section 5, `compare`) states it.
7
+ #
8
+ # Strict equality is decided first and answers a boolean over every
9
+ # value, undefined and null included. Every other operator propagates:
10
+ # undefined on either side, or a pair whose types do not match, answers
11
+ # undefined, never an error. Null has no type peer, so every non-strict
12
+ # comparison involving it answers undefined too.
13
+ #
14
+ # The module holds no state.
15
+ module Compare
16
+ # The operators `compare` accepts; any other operand is an unknown
17
+ # instruction.
18
+ OPERATORS = %w[GT LT EQ GTE LTE NE STRICT_EQ STRICT_NE].freeze
19
+
20
+ # What each ordering operator makes of an order (-1, 0 or 1).
21
+ ORDERED = {
22
+ "GT" => lambda(&:positive?), "LT" => lambda(&:negative?),
23
+ "GTE" => ->(order) { order >= 0 }, "LTE" => ->(order) { order <= 0 }
24
+ }.freeze
25
+
26
+ # The type of each kind of value a comparison matches a pair on.
27
+ KINDS = {
28
+ Integer => :number, Float => :number, TrueClass => :boolean, FalseClass => :boolean,
29
+ String => :string, Array => :list, Predicator::Date => :chronological,
30
+ Predicator::DateTime => :chronological, Predicator::Duration => :duration, Hash => :map
31
+ }.freeze
32
+
33
+ module_function
34
+
35
+ # The comparison of left and right under operator: true, false or
36
+ # {Predicator::UNDEFINED}.
37
+ def call(operator, left, right)
38
+ case operator
39
+ when "STRICT_EQ" then strict?(left, right)
40
+ when "STRICT_NE" then !strict?(left, right)
41
+ else loose(operator, left, right)
42
+ end
43
+ end
44
+
45
+ def loose(operator, left, right)
46
+ return UNDEFINED unless peers?(left, right)
47
+ return loose_equal?(left, right, by_instant: false) == (operator == "EQ") if %w[EQ NE].include?(operator)
48
+
49
+ order = Order.order(left, right)
50
+ order.nil? ? UNDEFINED : ORDERED.fetch(operator).call(order)
51
+ end
52
+
53
+ # The type a comparison matches a pair on: integer and float are one
54
+ # type here, and a date and a datetime are one type. Null and undefined
55
+ # have none.
56
+ def kind(value) = KINDS.fetch(value.class) { KINDS.find { |type, _| value.is_a?(type) }&.last }
57
+
58
+ def peers?(left, right) = !kind(left).nil? && kind(left) == kind(right)
59
+
60
+ def undefined?(value) = UNDEFINED.equal?(value)
61
+
62
+ # Strict equality: the same member of the domain with the same value.
63
+ # It does not bridge integer and float, and a date is never strictly
64
+ # equal to a datetime. The walk is iterative, so a pair nested deeper
65
+ # than the stack is compared rather than exhausting it; two lists or
66
+ # maps that are one object are equal without a walk.
67
+ def strict?(left, right)
68
+ pairs = [[left, right]]
69
+ until pairs.empty?
70
+ a, b = pairs.pop
71
+ return false unless strict_pair?(a, b) { |member, other| pairs.push([member, other]) }
72
+ end
73
+ true
74
+ end
75
+
76
+ # One pair of a strict walk: false when it differs, else true, having
77
+ # handed each member pair of two lists or two maps to the block.
78
+ def strict_pair?(left, right, &)
79
+ return right.instance_of?(left.class) && left == right unless container?(left)
80
+ return true if left.equal?(right)
81
+
82
+ right.is_a?(left.is_a?(Array) ? Array : Hash) && left.size == right.size && queued(left, right, &)
83
+ end
84
+
85
+ # Hands each member pair of two lists of one size, or of two maps of
86
+ # one size, to the block; false when a key of the left map is missing
87
+ # from the right.
88
+ def queued(left, right)
89
+ if left.is_a?(Array)
90
+ left.each_index { |at| yield(left[at], right[at]) }
91
+ return true
92
+ end
93
+
94
+ left.all? { |key, member| right.key?(key) && (yield(member, right[key]) || true) }
95
+ end
96
+
97
+ def container?(value) = value.is_a?(Array) || value.is_a?(Hash)
98
+
99
+ # Loose equality: numbers by value across integer and float, a date
100
+ # and a datetime by the instant each names. Undefined equals nothing,
101
+ # itself included; null equals only null. Two members of a list or a
102
+ # map follow the member rule (member_pair?). The walk is iterative, as
103
+ # strict? is.
104
+ def loose_equal?(left, right, by_instant:)
105
+ pairs = [[left, right, false]]
106
+ until pairs.empty?
107
+ a, b, member = pairs.pop
108
+ return false unless loose_pair?(a, b, member, by_instant) { |x, y| pairs.push([x, y, true]) }
109
+ end
110
+ true
111
+ end
112
+
113
+ # One pair of a loose walk: false when it differs, else true, having
114
+ # handed each member pair of two lists or two maps to the block.
115
+ def loose_pair?(left, right, member, by_instant, &)
116
+ verdict = member_rule(left, right, by_instant) if member
117
+ return verdict unless verdict.nil?
118
+ return left.nil? && right.nil? if absent?(left) || absent?(right)
119
+
120
+ peers?(left, right) && peers_equal?(left, right, &)
121
+ end
122
+
123
+ # Two values of one type: equal, or their member pairs handed to the
124
+ # block.
125
+ def peers_equal?(left, right, &)
126
+ return instant(left) == instant(right) if kind(left) == :chronological
127
+ return left == right unless container?(left)
128
+
129
+ left.equal?(right) || (left.size == right.size && queued(left, right, &))
130
+ end
131
+
132
+ # The member rule, or nil when it does not decide: two undefined
133
+ # members are the same member, and a date member and a datetime member
134
+ # differ whatever instants they name, unless by_instant asks for the
135
+ # instant alone.
136
+ def member_rule(left, right, by_instant)
137
+ return left.equal?(right) if undefined?(left) || undefined?(right)
138
+
139
+ false if !by_instant && date_against_datetime?(left, right)
140
+ end
141
+
142
+ def absent?(value) = value.nil? || undefined?(value)
143
+
144
+ def date_against_datetime?(left, right)
145
+ (left.is_a?(Predicator::Date) && right.is_a?(Predicator::DateTime)) ||
146
+ (left.is_a?(Predicator::DateTime) && right.is_a?(Predicator::Date))
147
+ end
148
+
149
+ # A date or a datetime as the instant it names, a date at midnight UTC.
150
+ def instant(value)
151
+ return [value.epoch_seconds, value.microsecond] if value.is_a?(Predicator::DateTime)
152
+
153
+ [Civil.days_of(value) * Civil::SECONDS_PER_DAY, 0]
154
+ end
155
+ end
156
+ private_constant :Compare
157
+ end
158
+ end