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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +42 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +322 -46
- data/lib/predicator/ast.rb +74 -187
- data/lib/predicator/boundary.rb +237 -0
- data/lib/predicator/civil.rb +112 -0
- data/lib/predicator/duration_text.rb +92 -0
- data/lib/predicator/emitter/chains.rb +62 -0
- data/lib/predicator/emitter/statements.rb +86 -0
- data/lib/predicator/emitter.rb +187 -0
- data/lib/predicator/errors.rb +140 -0
- data/lib/predicator/evaluator/access.rb +108 -0
- data/lib/predicator/evaluator/arithmetic.rb +132 -0
- data/lib/predicator/evaluator/calls.rb +129 -0
- data/lib/predicator/evaluator/casts.rb +158 -0
- data/lib/predicator/evaluator/chronology.rb +88 -0
- data/lib/predicator/evaluator/compare.rb +158 -0
- data/lib/predicator/evaluator/json_functions.rb +167 -0
- data/lib/predicator/evaluator/json_scan.rb +303 -0
- data/lib/predicator/evaluator/library.rb +127 -0
- data/lib/predicator/evaluator/math_functions.rb +72 -0
- data/lib/predicator/evaluator/order.rb +120 -0
- data/lib/predicator/evaluator/rich_types.rb +146 -0
- data/lib/predicator/evaluator/statements.rb +151 -0
- data/lib/predicator/evaluator.rb +391 -117
- data/lib/predicator/float_text.rb +94 -0
- data/lib/predicator/iso_text.rb +99 -0
- data/lib/predicator/lexer/literals.rb +115 -0
- data/lib/predicator/lexer/numbers.rb +91 -0
- data/lib/predicator/lexer.rb +194 -0
- data/lib/predicator/parser/durations.rb +108 -0
- data/lib/predicator/parser/operators.rb +86 -0
- data/lib/predicator/parser/postfix.rb +57 -0
- data/lib/predicator/parser/primaries.rb +122 -0
- data/lib/predicator/parser/statements.rb +114 -0
- data/lib/predicator/parser/token_text.rb +52 -0
- data/lib/predicator/parser.rb +217 -438
- data/lib/predicator/result.rb +46 -0
- data/lib/predicator/tagged/reader.rb +139 -0
- data/lib/predicator/tagged/writer.rb +106 -0
- data/lib/predicator/tagged.rb +134 -0
- data/lib/predicator/values.rb +147 -0
- data/lib/predicator/version.rb +4 -1
- data/lib/predicator.rb +384 -14
- metadata +54 -145
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -46
- data/.travis.yml +0 -5
- data/CODE_OF_CONDUCT.md +0 -49
- data/Gemfile +0 -4
- data/HISTORY.md +0 -47
- data/Rakefile +0 -24
- data/lib/predicator/context.rb +0 -18
- data/lib/predicator/lexer.rex +0 -69
- data/lib/predicator/lexer.rex.rb +0 -187
- data/lib/predicator/parser.y +0 -120
- data/lib/predicator/visitors/dot.rb +0 -113
- data/lib/predicator/visitors/each.rb +0 -16
- data/lib/predicator/visitors/instructions.rb +0 -183
- data/lib/predicator/visitors/string.rb +0 -76
- data/lib/predicator/visitors/visitor.rb +0 -100
- data/lib/predicator/visitors.rb +0 -5
- 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
|