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,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Emitter
5
+ # Chains, emitted into the walk that includes this module. A chain is a
6
+ # run of nodes each leaning on the one child the source writes beside it
7
+ # rather than inside it: a postfix step's object, a binary operator's
8
+ # left operand. The parser reads a chain in a loop, and so does this
9
+ # walk: it collects the spine, emits the innermost operand, then each
10
+ # link from the inside out, which is the post-order a recursive walk
11
+ # would give. Only the spine is flattened; a right operand or a key goes
12
+ # through the walk a level deeper.
13
+ module Chains
14
+ # The nodes a chain is made of.
15
+ KINDS = [Ast::PropertyAccess, Ast::BracketAccess, Ast::Cast, Ast::Comparison, Ast::Arithmetic,
16
+ Ast::Membership, Ast::LogicalAnd, Ast::LogicalOr].freeze
17
+
18
+ private
19
+
20
+ def chain(root)
21
+ spine = [root]
22
+ spine << leaning(spine.last) while KINDS.include?(leaning(spine.last).class)
23
+ spine.reverse.each_with_object(visit(leaning(spine.last))) { |node, out| link(node, out) }
24
+ end
25
+
26
+ def leaning(node)
27
+ case node
28
+ when Ast::PropertyAccess, Ast::BracketAccess then node.object
29
+ when Ast::Cast then node.expression
30
+ else node.left
31
+ end
32
+ end
33
+
34
+ # Appends one link's own instructions to the chain emitted so far.
35
+ def link(node, out)
36
+ case node
37
+ when Ast::LogicalAnd, Ast::LogicalOr then jump_over(node, out)
38
+ when Ast::PropertyAccess, Ast::Cast then out << own(node, step(node))
39
+ else out.concat(visit(node.is_a?(Ast::BracketAccess) ? node.key : node.right)) << own(node, step(node))
40
+ end
41
+ end
42
+
43
+ # The instruction a link emits after its operands.
44
+ def step(node)
45
+ case node
46
+ when Ast::PropertyAccess then ["access", node.property]
47
+ when Ast::Cast then ["cast", node.type_name]
48
+ when Ast::BracketAccess then ["bracket_access"]
49
+ else OPCODES.fetch(node.operator).dup
50
+ end
51
+ end
52
+
53
+ # `and` and `or`: a jump between the operands, past the right one.
54
+ def jump_over(node, out)
55
+ right = visit(node.right)
56
+ opcode = node.is_a?(Ast::LogicalAnd) ? "jump_if_falsy_or_pop" : "jump_if_true_or_pop"
57
+ out.push(own(node, [opcode, right.size + 1])).concat(right)
58
+ end
59
+ end
60
+ private_constant :Chains
61
+ end
62
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Emitter
5
+ # A program's statements, emitted into the walk that includes this
6
+ # module. Every statement leaves the stack as it found it: an assignment
7
+ # ends in the `store` that consumes its value, an expression in a `pop`,
8
+ # and an `if` or a `while` consumes its condition in the jump that tests
9
+ # it. The jumps are relative, counted from the jump itself, and carry the
10
+ # statement's own position and span.
11
+ module Statements
12
+ # The instructions of a statement sequence, one statement after
13
+ # another.
14
+ def statements(list) = list.each_with_object([]) { |statement, out| out.concat(statement(statement)) }
15
+
16
+ private
17
+
18
+ def statement(node)
19
+ case node
20
+ when Ast::Assignment then assignment(node)
21
+ when Ast::If then if_statement(node)
22
+ when Ast::While then while_statement(node)
23
+ else visit(node) << own(node, ["pop"])
24
+ end
25
+ end
26
+
27
+ # The condition, a falsy jump past the then block (and past the jump
28
+ # that ends it, when there is an else block), the block, then that
29
+ # jump past the else block.
30
+ def if_statement(node)
31
+ then_block = block(node.then_block)
32
+ else_block = node.else_block && block(node.else_block)
33
+ out = visit(node.condition) << own(node, ["pop_jump_if_falsy", then_block.size + (else_block ? 2 : 1)])
34
+ out.concat(then_block)
35
+ else_block ? jump_past(node, else_block, out) : out
36
+ end
37
+
38
+ # The jump that ends a then block, past the else block, then that block.
39
+ def jump_past(node, else_block, out) = out.push(own(node, ["jump", else_block.size + 1])).concat(else_block)
40
+
41
+ def block(node) = statements(node.statements)
42
+
43
+ # The condition, a falsy jump past the body and its back edge, the
44
+ # body, then the back edge to the condition's first instruction.
45
+ def while_statement(node)
46
+ out = visit(node.condition)
47
+ back = out.size
48
+ body = statements(node.body.statements)
49
+ out.push(own(node, ["pop_jump_if_falsy", body.size + 2])).concat(body)
50
+ out << own(node, ["jump_backward", back + body.size + 1])
51
+ end
52
+
53
+ # The location's segments root first, the value, and the store.
54
+ def assignment(node)
55
+ root, links = location(node.target)
56
+ out = [own(root, ["lit", root.name])]
57
+ segments = [root] + links.map { |link| segment(link, out) }
58
+ out.concat(visit(node.value))
59
+ out << Entry.new(instruction: ["store", segments.size], position: root.position, span: node.span, segments:)
60
+ end
61
+
62
+ # A location's root identifier and its accesses, root first.
63
+ def location(target)
64
+ links = []
65
+ while target.is_a?(Ast::PropertyAccess) || target.is_a?(Ast::BracketAccess)
66
+ links.unshift(target)
67
+ target = target.object
68
+ end
69
+ [target, links]
70
+ end
71
+
72
+ # Appends one access's segment, answering the node behind it: a
73
+ # property's name, or a bracket's key.
74
+ def segment(link, out)
75
+ if link.is_a?(Ast::BracketAccess)
76
+ out.concat(visit(link.key))
77
+ return link.key
78
+ end
79
+
80
+ out << own(link, ["lit", link.property])
81
+ link
82
+ end
83
+ end
84
+ private_constant :Statements
85
+ end
86
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "values"
4
+ require_relative "errors"
5
+ require_relative "lexer"
6
+ require_relative "ast"
7
+ require_relative "parser"
8
+ require_relative "emitter/chains"
9
+ require_relative "emitter/statements"
10
+
11
+ module Predicator
12
+ # The compiler's last stage: the syntax tree the {Parser} builds in, the
13
+ # instruction list the evaluator runs out, or the one refusal that stopped
14
+ # it. It is a post-order walk, the reference's: an operand's instructions
15
+ # come before the instruction that consumes it, and the short-circuiting
16
+ # pair puts its jump between its two operands, since what the jump skips
17
+ # is the right one.
18
+ #
19
+ # The operands are predicator values, the ones the evaluator takes: a date
20
+ # literal is a {Predicator::Date}, a decimal a Float, `undefined`
21
+ # {Predicator::UNDEFINED}. Written in the conformance corpus's tagged
22
+ # encoding they are the corpus's instructions.
23
+ #
24
+ # The shapes the walk emits:
25
+ #
26
+ # - a literal is `lit`; a list whose every element is a literal or a
27
+ # string is one `lit` carrying the list, and any other list is its
28
+ # elements then `make_list`;
29
+ # - an object is `object_new`, then each entry's value and an
30
+ # `object_set` of its key, in the order written;
31
+ # - `a and b` is `a`, a `jump_if_falsy_or_pop` past `b`, then `b`; `or`
32
+ # the same with `jump_if_true_or_pop`;
33
+ # - in a program, an assignment is its location's segments (the root's
34
+ # name, each property's name, each bracket's key), its value and a
35
+ # `store` of the segment count; any other expression statement is the
36
+ # expression and a `pop`; an `if` is its condition, a
37
+ # `pop_jump_if_falsy` past the then block (and past the `jump` that ends
38
+ # it when there is an else block), the block, and that `jump` past the
39
+ # else block; a `while` is its condition, a `pop_jump_if_falsy` past the
40
+ # body and its back edge, the body, then a `jump_backward` to the
41
+ # condition's first instruction.
42
+ #
43
+ # Each instruction is annotated with the position and the span of the node
44
+ # that emitted it ({Entry}); an `object_set` carries its key's, a `store`
45
+ # its location root's position and its statement's span, and a `pop` the
46
+ # expression's own. Every store also carries the node behind each segment
47
+ # of the location it writes: the root, each property access, each bracket
48
+ # key.
49
+ #
50
+ # One refusal is not the reference's, whose compiler raises there: a
51
+ # decimal literal too large for a finite Float is refused with the reason
52
+ # `number_out_of_range`, over the literal, as a value.
53
+ #
54
+ # A chain the parser read in a loop (nodes leaning on their left operand
55
+ # or their object, as a run of binary operators or of postfix steps
56
+ # builds) is walked in a loop here too, so a long chain takes no deeper a
57
+ # stack than a short one; anything nested inside a node is walked a level
58
+ # deeper, which the parser's depth limit bounds. Where the stack the walk
59
+ # runs on runs out all the same, the source is refused under the parser's
60
+ # reason, `nesting_depth_exceeded`, rather than raising.
61
+ #
62
+ # The module holds no state; each call walks with its own {Walk}. It is
63
+ # private to the package.
64
+ module Emitter
65
+ # One instruction and the node behind it: that node's position and span,
66
+ # and on a `store` the node behind each segment, root first (nil on any
67
+ # other instruction).
68
+ Entry = Data.define(:instruction, :position, :span, :segments)
69
+
70
+ # The message a decimal literal too large for a finite Float is refused
71
+ # with. The reference has none: its compiler raises there.
72
+ OUT_OF_RANGE = "Number literal is outside the range this implementation can represent"
73
+
74
+ # The opcode each operator node emits, frozen: each instruction emitted
75
+ # is a copy, so no caller holds the table's own arrays.
76
+ OPCODES = {
77
+ gt: %w[compare GT], lt: %w[compare LT], gte: %w[compare GTE], lte: %w[compare LTE],
78
+ equal_equal: %w[compare EQ], ne: %w[compare NE], strict_eq: %w[compare STRICT_EQ],
79
+ strict_ne: %w[compare STRICT_NE], add: ["add"], subtract: ["subtract"], multiply: ["multiply"],
80
+ divide: ["divide"], modulo: ["modulo"], in: ["in"], contains: ["contains"], minus: ["unary_minus"],
81
+ bang: ["unary_bang"]
82
+ }.transform_values(&:freeze).freeze
83
+
84
+ # The tag a refusal is thrown with, to the catch around a walk.
85
+ REFUSED = :refused
86
+
87
+ # Emits one expression.
88
+ #
89
+ # @param node [Object] an {Ast} node
90
+ # @return [Array<Entry>, Lexer::Refusal] the annotated instructions, or
91
+ # the refusal
92
+ def self.emit(node) = Walk.new.run(node) { |walk| walk.visit(node) }
93
+
94
+ # Emits a program, one statement after another.
95
+ #
96
+ # @param program [Ast::Program]
97
+ # @return [Array<Entry>, Lexer::Refusal] the annotated instructions, or
98
+ # the refusal
99
+ def self.emit_program(program) = Walk.new.run(program) { |walk| walk.statements(program.statements) }
100
+
101
+ # One walk of one tree. A refusal is thrown to the catch around the whole
102
+ # walk and is never seen outside this class.
103
+ class Walk
104
+ include Chains
105
+ include Statements
106
+
107
+ # The method that emits each kind of expression node.
108
+ VISITS = {
109
+ Ast::Literal => :literal, Ast::StringLiteral => :literal, Ast::Identifier => :identifier,
110
+ Ast::Unary => :prefixed, Ast::LogicalNot => :prefixed, Ast::ListLiteral => :list,
111
+ Ast::ObjectLiteral => :object, Ast::FunctionCall => :call, Ast::Duration => :duration,
112
+ Ast::RelativeDate => :relative_date
113
+ }.merge(Chains::KINDS.to_h { |kind| [kind, :chain] }).freeze
114
+
115
+ # The walk in the block, or the refusal it threw; a stack exhausted
116
+ # before the walk finished is refused over the whole tree.
117
+ def run(root)
118
+ catch(REFUSED) { yield self }
119
+ rescue SystemStackError
120
+ refuse(root, "nesting_depth_exceeded", "Expression nests deeper than the stack this compilation runs " \
121
+ "on can follow, short of the depth limit of " \
122
+ "#{Parser::DEPTH_LIMIT} levels", thrown: false)
123
+ end
124
+
125
+ # The instructions of one expression node.
126
+ def visit(node) = send(VISITS.fetch(node.class), node)
127
+
128
+ private
129
+
130
+ def literal(node) = [own(node, ["lit", value_of(node)])]
131
+
132
+ def identifier(node) = [own(node, ["load", node.name])]
133
+
134
+ # A unary operator or `not`: the operand, then the operator.
135
+ def prefixed(node)
136
+ visit(node.operand) << own(node, node.is_a?(Ast::LogicalNot) ? ["not"] : OPCODES.fetch(node.operator).dup)
137
+ end
138
+
139
+ # A list whose every element is a literal or a string is one `lit`
140
+ # carrying the list; any other list is its elements and `make_list`.
141
+ def list(node)
142
+ if node.elements.all? { |element| [Ast::Literal, Ast::StringLiteral].include?(element.class) }
143
+ return [own(node, ["lit", node.elements.map { |element| value_of(element) }])]
144
+ end
145
+
146
+ each_visited(node.elements) << own(node, ["make_list", node.elements.size])
147
+ end
148
+
149
+ # `object_new`, then each entry's value and an `object_set` carrying
150
+ # its key's position, in the order written.
151
+ def object(node)
152
+ node.entries.each_with_object([own(node, ["object_new"])]) do |(key, value), out|
153
+ out.concat(visit(value)) << own(key, ["object_set", key.name])
154
+ end
155
+ end
156
+
157
+ def call(node) = each_visited(node.arguments) << own(node, ["call", node.name, node.arguments.size])
158
+
159
+ # One instruction carrying the units in the order written.
160
+ def duration(node) = [own(node, ["duration", node.units.map { |amount, unit| [amount, unit] }])]
161
+
162
+ def relative_date(node) = visit(node.duration) << own(node, ["relative_date", node.direction.to_s])
163
+
164
+ def each_visited(nodes) = nodes.each_with_object([]) { |node, out| out.concat(visit(node)) }
165
+
166
+ # A literal's value; a Float the domain has no finite member for is
167
+ # refused over the literal.
168
+ def value_of(node)
169
+ value = node.value
170
+ refuse(node, "number_out_of_range", OUT_OF_RANGE) if value.is_a?(Float) && !value.finite?
171
+ value
172
+ end
173
+
174
+ def own(node, instruction) = Entry.new(instruction:, position: node.position, span: node.span, segments: nil)
175
+
176
+ # The refusal over a node, thrown to the catch around the walk or,
177
+ # outside it, answered.
178
+ def refuse(node, reason, message, thrown: true)
179
+ refusal = Lexer::Refusal.new(error: ParseError.new(reason:, message:, position: node.position),
180
+ span: node.span)
181
+ thrown ? throw(REFUSED, refusal) : refusal
182
+ end
183
+ end
184
+ private_constant :Walk
185
+ end
186
+ private_constant :Emitter
187
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ # A place in source text: a line and a column, both counted from 1. The
5
+ # reference reports positions in this shape.
6
+ #
7
+ # @!attribute [r] line
8
+ # @return [Integer] the line, from 1
9
+ # @!attribute [r] column
10
+ # @return [Integer] the column, from 1
11
+ Position = Data.define(:line, :column) do
12
+ # @raise [ArgumentError] unless both are Integers of at least 1
13
+ def initialize(line:, column:)
14
+ unless [line, column].all? { |part| part.is_a?(Integer) && part >= 1 }
15
+ raise ArgumentError, "a position is a line and a column from 1, not #{[line, column].inspect}"
16
+ end
17
+
18
+ super
19
+ end
20
+ end
21
+
22
+ # What every error value has, built into each of the four classes below.
23
+ # The type names the class, and with the reason it is normative: the
24
+ # conformance corpus matches an expected error on exactly those two. The
25
+ # message is for a person and is not normative. The position is where in
26
+ # the source the failure is, where the reference gives one.
27
+ module ErrorValue
28
+ # The error's type as the corpus spells it, which is its class's name.
29
+ #
30
+ # @return [String]
31
+ def type = self.class.name.delete_prefix("Predicator::")
32
+
33
+ private
34
+
35
+ def check(reason, message, position, position_required: false)
36
+ raise ArgumentError, "reason must be a String, not #{reason.inspect}" unless reason.is_a?(String)
37
+ raise ArgumentError, "message must be a String, not #{message.inspect}" unless message.is_a?(String)
38
+ return if position.is_a?(Position) || (position.nil? && !position_required)
39
+
40
+ raise ArgumentError, "position must be a Predicator::Position#{" or nil" unless position_required}, " \
41
+ "not #{position.inspect}"
42
+ end
43
+ end
44
+ private_constant :ErrorValue
45
+
46
+ # A source text that does not parse. It is not an evaluation outcome, so
47
+ # the corpus never expects one; its reason is this package's own token for
48
+ # the refusal, and it always knows its position.
49
+ #
50
+ # @!attribute [r] reason
51
+ # @return [String] a structured token naming the refusal
52
+ # @!attribute [r] message
53
+ # @return [String] a sentence for a person; not normative
54
+ # @!attribute [r] position
55
+ # @return [Position] where in the source the refusal is
56
+ ParseError = Data.define(:reason, :message, :position) do
57
+ include ErrorValue
58
+
59
+ # @raise [ArgumentError] when a field has the wrong type
60
+ def initialize(reason:, message:, position:)
61
+ check(reason, message, position, position_required: true)
62
+ super
63
+ end
64
+ end
65
+
66
+ # An evaluation failure that is neither a type mismatch nor an unbound
67
+ # variable: an empty stack at halt, too few operands, an unknown
68
+ # instruction, a division by zero, a failed call.
69
+ #
70
+ # @!attribute [r] reason
71
+ # @return [String] the normative reason token, e.g. "division_by_zero"
72
+ # @!attribute [r] message
73
+ # @return [String] a sentence for a person; not normative
74
+ # @!attribute [r] position
75
+ # @return [Position, nil] where in the source the failing instruction
76
+ # came from, when that is known
77
+ EvaluationError = Data.define(:reason, :message, :position) do
78
+ include ErrorValue
79
+
80
+ # @raise [ArgumentError] when a field has the wrong type
81
+ def initialize(reason:, message:, position: nil)
82
+ check(reason, message, position)
83
+ super
84
+ end
85
+ end
86
+
87
+ # An operand an opcode refused on its type. The reason is the operation
88
+ # that refused it, e.g. "add".
89
+ #
90
+ # @!attribute [r] reason
91
+ # @return [String] the normative reason token
92
+ # @!attribute [r] message
93
+ # @return [String] a sentence for a person; not normative
94
+ # @!attribute [r] position
95
+ # @return [Position, nil] where in the source, when that is known
96
+ TypeMismatchError = Data.define(:reason, :message, :position) do
97
+ include ErrorValue
98
+
99
+ # @raise [ArgumentError] when a field has the wrong type
100
+ def initialize(reason:, message:, position: nil)
101
+ check(reason, message, position)
102
+ super
103
+ end
104
+ end
105
+
106
+ # A variable the context does not bind, reported as an error rather than
107
+ # as undefined.
108
+ #
109
+ # @!attribute [r] reason
110
+ # @return [String] the normative reason token, e.g. "unbound_variable"
111
+ # @!attribute [r] message
112
+ # @return [String] a sentence for a person; not normative
113
+ # @!attribute [r] position
114
+ # @return [Position, nil] where in the source, when that is known
115
+ UndefinedVariableError = Data.define(:reason, :message, :position) do
116
+ include ErrorValue
117
+
118
+ # @raise [ArgumentError] when a field has the wrong type
119
+ def initialize(reason:, message:, position: nil)
120
+ check(reason, message, position)
121
+ super
122
+ end
123
+ end
124
+
125
+ # The exception a raising variant raises. It carries the error value a
126
+ # result would have answered, so a caller that rescues it reads the same
127
+ # type, reason and position.
128
+ class Error < StandardError
129
+ # @return [Object] the error value: a {ParseError}, an
130
+ # {EvaluationError}, a {TypeMismatchError}, an
131
+ # {UndefinedVariableError}, or a {Predicator::Tagged::CodecError}
132
+ attr_reader :error
133
+
134
+ # @param error [Object] an error value; its message is the exception's
135
+ def initialize(error)
136
+ @error = error
137
+ super(error.message)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Evaluator
5
+ # Member and index access, membership and list construction, as the
6
+ # reference's ISA (docs/isa.md, section 5) states them: the handlers
7
+ # {Machine} runs for `access`, `bracket_access`, `in`, `contains` and
8
+ # `make_list`, and the rules they read through.
9
+ #
10
+ # A duration reads as the map its shape is, as the reference's ISA
11
+ # (section 3) makes it normative: each of its eight unit names answers
12
+ # that unit, and any other key misses. A duration is one value wherever
13
+ # it came from, a literal, an opcode or the host.
14
+ module Access
15
+ # The keys a duration answers.
16
+ DURATION_UNITS = %w[years months weeks days hours minutes seconds milliseconds].freeze
17
+
18
+ # The one-of-a-kind values a map key may be besides a String and an
19
+ # Integer: the reference admits its atoms, which are these.
20
+ ATOM_KEYS = [true, false, nil, UNDEFINED].freeze
21
+
22
+ # The plain map a duration is: each of its eight unit names to that
23
+ # unit.
24
+ def self.units(duration) = DURATION_UNITS.to_h { |name| [name, duration.public_send(name)] }
25
+
26
+ private
27
+
28
+ # Reads the named member off the popped target. A miss, and a target
29
+ # that is not a map or a duration, the null value included, answer
30
+ # undefined: access never fails but on an empty stack.
31
+ def access(at, property)
32
+ return insufficient("access") if @stack.empty?
33
+
34
+ push(at, member(@stack.pop, property))
35
+ end
36
+
37
+ # Pops the key, then the target. A list takes a non-negative Integer and
38
+ # misses past either end; any other key is a type mismatch. A map, a
39
+ # duration, a date or a datetime (each a map in the reference) takes a
40
+ # String, an Integer, a boolean, null or undefined and misses on a key it
41
+ # does not hold; any other key is a type mismatch. Any other target
42
+ # answers undefined whatever the key.
43
+ def bracket_access(at)
44
+ return insufficient("bracket_access") if @stack.size < 2
45
+
46
+ key = @stack.pop
47
+ target = @stack.pop
48
+ case target
49
+ when Array then list_index(at, target, key)
50
+ when Hash, Duration, Predicator::Date, Predicator::DateTime then map_key(at, target, key)
51
+ else push(at, UNDEFINED)
52
+ end
53
+ end
54
+
55
+ def list_index(at, list, key)
56
+ return mismatch("bracket_access", "an integer index", key) unless key.is_a?(Integer)
57
+
58
+ push(at, key.negative? || key >= list.size ? UNDEFINED : list[key])
59
+ end
60
+
61
+ def map_key(at, target, key)
62
+ unless key.is_a?(String) || key.is_a?(Integer) || ATOM_KEYS.any? { |atom| atom.equal?(key) }
63
+ return mismatch("bracket_access", "a string key", key)
64
+ end
65
+
66
+ push(at, member(target, key))
67
+ end
68
+
69
+ def member(target, key)
70
+ case target
71
+ when Hash then target.fetch(key, UNDEFINED)
72
+ when Duration then DURATION_UNITS.include?(key) ? target.public_send(key) : UNDEFINED
73
+ else UNDEFINED
74
+ end
75
+ end
76
+
77
+ def membership_in(at) = membership(at, "in")
78
+ def membership_contains(at) = membership(at, "contains")
79
+
80
+ # `left in right` and `left contains right`: undefined on either side
81
+ # answers undefined; otherwise the list side (right for in, left for
82
+ # contains) must be a list. A member matches by the equality `EQ` uses,
83
+ # with one difference: it answers false where `EQ` answers undefined,
84
+ # so null is found in a list holding null and undefined never is.
85
+ def membership(at, operation)
86
+ return insufficient(operation) if @stack.size < 2
87
+
88
+ right = @stack.pop
89
+ left = @stack.pop
90
+ return push(at, UNDEFINED) if UNDEFINED.equal?(left) || UNDEFINED.equal?(right)
91
+
92
+ list, sought = operation == "in" ? [right, left] : [left, right]
93
+ return mismatch(operation, "a list", list) unless list.is_a?(Array)
94
+
95
+ push(at, list.any? { |item| Compare.loose_equal?(sought, item, by_instant: false) })
96
+ end
97
+
98
+ # Pops count values and pushes them as one list, deepest first, which
99
+ # is source order.
100
+ def make_list(at, count)
101
+ return insufficient("make_list") if @stack.size < count
102
+
103
+ push(at, @stack.pop(count))
104
+ end
105
+ end
106
+ private_constant :Access
107
+ end
108
+ end