predicator 1.2.0 → 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 +5 -5
  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 +54 -145
  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 -47
  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 -29
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The order of two values for the ordering operators of `compare`, as
6
+ # the reference's ISA (docs/isa.md, section 5, `compare`) states it.
7
+ # Pairs are matched and compared for equality by {Compare}.
8
+ #
9
+ # The module holds no state.
10
+ module Order
11
+ # The answer a list walk gives for a member pair that does not decide
12
+ # the order, so the walk steps to the next position.
13
+ LEVEL = :level
14
+
15
+ # Where each kind of member sits in the reference's term order, for a
16
+ # member pair of two different kinds: a number, then false, null, true
17
+ # and undefined, then the map-like values (a map, a duration, a date, a
18
+ # datetime), then a list, then a string.
19
+ MEMBER_RANKS = { number: 0, map: 5, duration: 5, chronological: 5, list: 6, string: 7 }.freeze
20
+
21
+ # The rank of each member that is one value of its own kind.
22
+ SINGLE_RANKS = { false => 1, nil => 2, true => 3 }.freeze
23
+
24
+ module_function
25
+
26
+ # The order of a type-matched pair (-1, 0 or 1), or nil when the pair
27
+ # has none. Strings order by byte, which for UTF-8 is code point order
28
+ # and no collation. Two maps and two durations have no order: the
29
+ # reference leaves map ordering unspecified for a sibling.
30
+ def order(left, right) = Compare.peers?(left, right) ? peer_order(left, right) : nil
31
+
32
+ def peer_order(left, right)
33
+ case Compare.kind(left)
34
+ when :number, :string then left <=> right
35
+ when :boolean then (left ? 1 : 0) <=> (right ? 1 : 0)
36
+ when :chronological then Compare.instant(left) <=> Compare.instant(right)
37
+ when :list then list_order(left, right)
38
+ end
39
+ end
40
+
41
+ # Two lists from the front: the first member pair that differs decides,
42
+ # and a proper prefix orders before the longer list.
43
+ #
44
+ # The walk is iterative: a member pair of two lists is walked as a list
45
+ # of its own on an explicit stack of frames (the two lists and the
46
+ # position reached), so lists nested deeper than the stack are ordered
47
+ # rather than exhausting it. Any answer other than level decides the
48
+ # whole order at once. A nested pair that ends level (every member
49
+ # pair level, the sizes equal) is equal by instant, which is what makes
50
+ # a member pair level, so its parent steps to the next position.
51
+ def list_order(left, right)
52
+ frames = [[left, right, 0]]
53
+ until frames.empty?
54
+ step = frame_step(frames)
55
+ return step unless step == LEVEL
56
+ end
57
+ LEVEL
58
+ end
59
+
60
+ # One step of the walk over the top frame: LEVEL to go on, else the
61
+ # whole walk's answer.
62
+ def frame_step(frames)
63
+ outer, inner, at = frames.last
64
+ return frame_end(frames) if at == [outer.size, inner.size].min
65
+
66
+ frames.last[2] = at + 1
67
+ pair = [outer[at], inner[at]]
68
+ pair.all?(Array) ? descend(frames, pair) : member_order(*pair)
69
+ end
70
+
71
+ # Walks a member pair of two lists as a frame of its own.
72
+ def descend(frames, pair)
73
+ frames.push([*pair, 0])
74
+ LEVEL
75
+ end
76
+
77
+ # The end of the top frame, its lists ordered by size: that order
78
+ # decides the walk when it is not level or when it is the outermost
79
+ # frame.
80
+ def frame_end(frames)
81
+ outer, inner, = frames.pop
82
+ order = outer.size <=> inner.size
83
+ frames.empty? || !order.zero? ? order : LEVEL
84
+ end
85
+
86
+ # One member pair of a list walk: LEVEL when it does not decide, else
87
+ # the order it decides, or nil when it has none. Two members of
88
+ # different kinds are placed by the reference's term order (rank); two
89
+ # nulls, two undefineds, two falses or two trues are level; a date
90
+ # member orders before a datetime member, as term order places them.
91
+ def member_order(left, right)
92
+ ranks = [rank(left), rank(right)]
93
+ return ranks.first <=> ranks.last unless ranks.first == ranks.last
94
+ return LEVEL if (1..4).cover?(ranks.first)
95
+ return left.is_a?(Predicator::Date) ? -1 : 1 if Compare.date_against_datetime?(left, right)
96
+ return LEVEL if Compare.loose_equal?(left, right, by_instant: left.is_a?(Hash) && right.is_a?(Hash))
97
+
98
+ differing_order(left, right)
99
+ end
100
+
101
+ # A member pair of one kind that is not equal: its order, LEVEL when it
102
+ # orders level and is equal by instant, or nil.
103
+ def differing_order(left, right)
104
+ order = order(left, right)
105
+ return order unless order&.zero?
106
+
107
+ Compare.loose_equal?(left, right, by_instant: true) ? LEVEL : nil
108
+ end
109
+
110
+ # A member's rank in term order (MEMBER_RANKS, SINGLE_RANKS); undefined
111
+ # ranks 4. A value outside the domain has no rank.
112
+ def rank(member)
113
+ return 4 if Compare.undefined?(member)
114
+
115
+ SINGLE_RANKS.fetch(member) { MEMBER_RANKS[Compare.kind(member)] }
116
+ end
117
+ end
118
+ private_constant :Order
119
+ end
120
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # A clock that failed: it answered something other than a
6
+ # {Predicator::DateTime}, or raised a StandardError. Raised by the read of
7
+ # the clock wherever an instruction reads the time, and rescued by the
8
+ # run's loop ({Machine}), which stops the run with the EvaluationError it
9
+ # carries (reason clock_failed). It never leaves an evaluation.
10
+ class ClockFault < StandardError
11
+ attr_reader :error
12
+
13
+ def initialize(message)
14
+ @error = EvaluationError.new(reason: "clock_failed", message:)
15
+ super
16
+ end
17
+ end
18
+ private_constant :ClockFault
19
+
20
+ # The rich-type opcodes, as the reference's ISA (docs/isa.md, section 5)
21
+ # states them: the handlers {Machine} runs for `duration`,
22
+ # `relative_date`, `object_new` and `object_set`.
23
+ #
24
+ # `relative_date` is the one opcode here whose answer depends on the
25
+ # time. It reads the clock the caller passed to the evaluation, through
26
+ # `now`, which reads that clock at most once per evaluation.
27
+ #
28
+ # A duration is read as the map the ISA (section 3) makes it: its eight
29
+ # units under their names. So `relative_date` takes a duration or a
30
+ # plain map, whose unit names it reads (an absent unit is zero, any
31
+ # other key is ignored, and a unit that is not an Integer is refused);
32
+ # and `object_set` writes into a plain map or a duration, which answers
33
+ # a plain map of its eight units and the key written. Any other value,
34
+ # a date and a datetime included (section 3 types them apart from a
35
+ # map), is refused with the reason section 5 names for a non-map,
36
+ # invalid_stack_value.
37
+ module RichTypes
38
+ # Every unit spelling `duration` accepts, and the unit each names.
39
+ UNIT_SPELLINGS = {
40
+ years: %w[y year years], months: %w[mo month months], weeks: %w[w week weeks],
41
+ days: %w[d day days], hours: %w[h hour hours], minutes: %w[m min minute minutes],
42
+ seconds: %w[s sec second seconds], milliseconds: %w[ms millisecond milliseconds]
43
+ }.flat_map { |unit, spellings| spellings.map { |spelling| [spelling, unit] } }.to_h.freeze
44
+
45
+ # Which way each direction `relative_date` accepts moves the time.
46
+ DIRECTIONS = { "ago" => -1, "last" => -1, "future" => 1, "next" => 1 }.freeze
47
+
48
+ private
49
+
50
+ # Pushes the duration the unit pairs name, every unit they do not name
51
+ # at zero. A later pair naming a unit overwrites an earlier one. A pair
52
+ # that is not an Integer and a String is refused before its unit is
53
+ # read, and pairs are read in order, so the first bad pair decides.
54
+ def duration(at, pairs)
55
+ units = {}
56
+ pairs.each do |pair|
57
+ return invalid_duration("invalid_duration_format", "is not [integer, unit]", pair) unless unit_pair?(pair)
58
+
59
+ unit = UNIT_SPELLINGS[pair.last]
60
+ return invalid_duration("invalid_duration_unit", "names no unit", pair) unless unit
61
+
62
+ units[unit] = pair.first
63
+ end
64
+ push(at, Duration.new(**units))
65
+ end
66
+
67
+ def unit_pair?(pair) = pair.is_a?(Array) && pair.size == 2 && pair.first.is_a?(Integer) && pair.last.is_a?(String)
68
+
69
+ def invalid_duration(reason, says, pair)
70
+ EvaluationError.new(reason:, message: "duration pair #{pair.inspect} #{says}")
71
+ end
72
+
73
+ # Pops a duration, or a map read as one, and pushes the evaluation's
74
+ # current time moved by it: back for "ago" and "last", forward for
75
+ # "future" and "next". The stack depth is checked first, then the
76
+ # popped value, then the direction.
77
+ def relative_date(at, direction)
78
+ return insufficient("relative_date") if @stack.empty?
79
+
80
+ moved = as_duration(@stack.pop)
81
+ return invalid_stack_value("relative_date", "a duration") unless moved
82
+
83
+ sign = DIRECTIONS[direction]
84
+ unless sign
85
+ return EvaluationError.new(reason: "invalid_direction",
86
+ message: "relative_date does not accept #{direction.inspect}")
87
+ end
88
+
89
+ push(at, Chronology.shift(now, moved, sign))
90
+ end
91
+
92
+ # The current time of this evaluation: the clock's answer the first
93
+ # time it is asked for, and the same instant every time after. A clock
94
+ # that answers anything but a {Predicator::DateTime}, or raises a
95
+ # StandardError, raises ClockFault, which stops the run with a failed
96
+ # result whichever instruction read it; an exception that is not a
97
+ # StandardError propagates.
98
+ def now = @now ||= read_clock
99
+
100
+ def read_clock
101
+ instant = @clock.call
102
+ return instant if instant.is_a?(Predicator::DateTime)
103
+
104
+ raise ClockFault, "the clock answered #{instant.inspect}, not a Predicator::DateTime"
105
+ rescue ClockFault
106
+ raise
107
+ rescue StandardError => e
108
+ raise ClockFault, "the clock raised: #{e.inspect}"
109
+ end
110
+
111
+ # A duration as it is, a plain map as the duration its unit names make
112
+ # (an absent unit zero, any other key ignored), or nil when the value
113
+ # is neither or a unit is not an Integer.
114
+ def as_duration(value)
115
+ return value if value.is_a?(Duration)
116
+ return unless value.is_a?(Hash)
117
+
118
+ units = Access::DURATION_UNITS.to_h { |name| [name.to_sym, value.fetch(name, 0)] }
119
+ Duration.new(**units) if units.each_value.all?(Integer)
120
+ end
121
+
122
+ def object_new(at) = push(at, {})
123
+
124
+ # Pops the value, then the map beneath it, and pushes a copy of that
125
+ # map with key set to the value. The map popped is not changed: it may
126
+ # be one the context holds. A duration beneath the value is the map of
127
+ # its eight units, so the answer is a plain map. The depth is checked
128
+ # before the map.
129
+ def object_set(at, key)
130
+ return insufficient("object_set") if @stack.size < 2
131
+
132
+ value = @stack.pop
133
+ target = @stack.pop
134
+ target = Access.units(target) if target.is_a?(Duration)
135
+ return invalid_stack_value("object_set", "a map beneath the value") unless target.is_a?(Hash)
136
+
137
+ push(at, target.merge(key => value))
138
+ end
139
+
140
+ def invalid_stack_value(opcode, wanted)
141
+ EvaluationError.new(reason: "invalid_stack_value", message: "#{opcode} expects #{wanted}")
142
+ end
143
+ end
144
+ private_constant :RichTypes
145
+ end
146
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # The statement opcodes and the statement-level jumps, as the reference's
6
+ # ISA (docs/isa.md, section 5) states them: the handlers {Machine} runs
7
+ # for `store`, `pop`, `jump`, `pop_jump_if_falsy` and `jump_backward`.
8
+ #
9
+ # `store` is the one opcode that writes the context. It never changes a
10
+ # map or a list it writes through: each write builds a new context that
11
+ # shares every branch the path does not pass through, so the context the
12
+ # caller passed stays as it was, and a refused write leaves no partial
13
+ # write behind ({Location}).
14
+ #
15
+ # `jump_backward` is the one back edge. Each back edge taken spends one
16
+ # of the evaluation's loop budget; one that finds the budget spent stops
17
+ # the run with loop_budget_exceeded, so a budget of n lets n back edges
18
+ # through. A program with no back edge halts within its length in steps,
19
+ # since every other jump goes forward.
20
+ module Statements
21
+ private
22
+
23
+ # Pops the value, then count location segments beneath it, and writes
24
+ # the value into the context at the path those segments name, the
25
+ # deepest-pushed segment first (the root). Too few values is
26
+ # insufficient operands; a segment that is not a String or an Integer
27
+ # is a type mismatch, checked before anything is written; a write the
28
+ # path cannot take is refused as {Location} says. Pushes nothing.
29
+ def store(at, count)
30
+ return insufficient("store") if @stack.size < count + 1
31
+
32
+ value = @stack.pop
33
+ path = @stack.pop(count)
34
+ bad = path.index { |segment| !segment.is_a?(String) && !segment.is_a?(Integer) }
35
+ return mismatch("store", "a string or an integer segment", path[bad]) if bad
36
+
37
+ written = Location.put(@context, path, value)
38
+ return written if written.is_a?(EvaluationError)
39
+
40
+ @context = written
41
+ at + 1
42
+ end
43
+
44
+ # `pop`, the statement boundary: discards the stack top, keeping it as
45
+ # the value statement mode answers. An empty stack is insufficient
46
+ # operands; the empty_stack rule belongs to the halt alone.
47
+ def discard(at)
48
+ return insufficient("pop") if @stack.empty?
49
+
50
+ @last_value = @stack.pop
51
+ at + 1
52
+ end
53
+
54
+ def jump(at, offset) = at + offset
55
+
56
+ # Pops the condition whatever it is: false, null and undefined jump,
57
+ # exactly true falls through, and anything else is a type mismatch.
58
+ def pop_jump_if_falsy(at, offset)
59
+ return insufficient("pop_jump_if_falsy") if @stack.empty?
60
+
61
+ condition = @stack.pop
62
+ truth = truth(condition)
63
+ return mismatch("pop_jump_if_falsy", "a boolean", condition) if truth.nil?
64
+
65
+ truth ? at + 1 : at + offset
66
+ end
67
+
68
+ # A target before the first instruction is a malformed operand, an
69
+ # unknown instruction, whatever the budget holds.
70
+ def jump_backward(at, offset)
71
+ return unknown(at) if offset > at
72
+ return loop_budget_exceeded if @budget.zero?
73
+
74
+ @budget -= 1
75
+ at - offset
76
+ end
77
+
78
+ def loop_budget_exceeded
79
+ EvaluationError.new(reason: "loop_budget_exceeded",
80
+ message: "the loop budget is spent at a backward jump")
81
+ end
82
+ end
83
+ private_constant :Statements
84
+
85
+ # The write `store` makes, as the reference writes a location: each
86
+ # interior slot that is missing, null or undefined becomes a list when
87
+ # the segment after it is an Integer and a map otherwise; a duration is
88
+ # written through as the plain map of its eight units; the leaf is
89
+ # overwritten whatever it holds. A map takes a String or an Integer key.
90
+ # A list takes an index of at least 0, and an index past its end pads the
91
+ # gap with undefined. Every container on the path is copied, never
92
+ # changed.
93
+ #
94
+ # The refusals are EvaluationErrors: not_assignable for an empty path,
95
+ # not_a_container for a slot that holds anything else (a date and an
96
+ # instant included) or a list given a String, and invalid_index for a
97
+ # negative index.
98
+ module Location
99
+ # The context with value written at path, or the EvaluationError that
100
+ # refuses the write.
101
+ def self.put(context, path, value)
102
+ return refusal("not_assignable", "store has no location to write") if path.empty?
103
+
104
+ catch(:refused) { write(context, path, value) }
105
+ end
106
+
107
+ def self.write(container, path, value)
108
+ segment, *rest = path
109
+ return set(container, segment, value) if rest.empty?
110
+
111
+ set(container, segment, write(vivify(fetch(container, segment), rest.first), rest, value))
112
+ end
113
+
114
+ def self.fetch(container, segment)
115
+ return container.fetch(segment, nil) if container.is_a?(Hash)
116
+
117
+ index!(segment)
118
+ container[segment]
119
+ end
120
+
121
+ def self.vivify(slot, following)
122
+ case slot
123
+ when nil, UNDEFINED then following.is_a?(Integer) ? [] : {}
124
+ when Hash, Array then slot
125
+ when Duration then Access.units(slot)
126
+ else throw :refused, refusal("not_a_container", "store cannot write into #{slot.inspect}")
127
+ end
128
+ end
129
+
130
+ def self.set(container, segment, value)
131
+ return container.merge(segment => value) if container.is_a?(Hash)
132
+
133
+ index!(segment)
134
+ list = container.dup
135
+ list.fill(UNDEFINED, list.size...segment)
136
+ list[segment] = value
137
+ list
138
+ end
139
+
140
+ def self.index!(segment)
141
+ throw :refused, refusal("not_a_container", "store cannot write a key into a list") unless segment.is_a?(Integer)
142
+ throw :refused, refusal("invalid_index", "store cannot write at index #{segment}") if segment.negative?
143
+ end
144
+
145
+ def self.refusal(reason, message) = EvaluationError.new(reason:, message:)
146
+
147
+ private_class_method :write, :fetch, :vivify, :set, :index!, :refusal
148
+ end
149
+ private_constant :Location
150
+ end
151
+ end