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.
- checksums.yaml +4 -4
- 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 +53 -147
- 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 -51
- 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 -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
|