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
data/lib/predicator.rb
CHANGED
|
@@ -1,24 +1,394 @@
|
|
|
1
|
-
|
|
2
|
-
require "predicator/evaluator"
|
|
3
|
-
require "predicator/parser"
|
|
1
|
+
# frozen_string_literal: true
|
|
4
2
|
|
|
3
|
+
require_relative "predicator/version"
|
|
4
|
+
require_relative "predicator/values"
|
|
5
|
+
require_relative "predicator/errors"
|
|
6
|
+
require_relative "predicator/result"
|
|
7
|
+
require_relative "predicator/evaluator"
|
|
8
|
+
require_relative "predicator/lexer"
|
|
9
|
+
require_relative "predicator/parser"
|
|
10
|
+
require_relative "predicator/emitter"
|
|
11
|
+
|
|
12
|
+
# A safe, non-evaluating condition language for end users.
|
|
13
|
+
#
|
|
14
|
+
# Every entry point answers a {Predicator::Result} (`ok?`, `value`, `error`)
|
|
15
|
+
# and never raises for an outcome of the source or the program: a source
|
|
16
|
+
# that does not compile, a run that stops, a value outside the domain all
|
|
17
|
+
# answer a failed result carrying an error value. Each has a bang variant
|
|
18
|
+
# (`compile!`, `evaluate!`, ...) that answers the value itself or raises
|
|
19
|
+
# {Predicator::Error} carrying the same error value. Equality, ordering and
|
|
20
|
+
# membership walk nested lists and maps without recursing, so a value a
|
|
21
|
+
# program nests to any depth is compared, never overflowing the stack.
|
|
22
|
+
#
|
|
23
|
+
# Two things still raise. A caller's own bug in the call itself (an argument
|
|
24
|
+
# of the wrong kind, an unknown or malformed option, a clock that does not
|
|
25
|
+
# respond to `call`) raises ArgumentError at the entry point. And an
|
|
26
|
+
# exception that is not a StandardError (an Interrupt, a SystemExit, a
|
|
27
|
+
# NoMemoryError), raised by host code a run calls (a host function or the
|
|
28
|
+
# clock), propagates unchanged: it is the host failing, not an outcome of
|
|
29
|
+
# the program.
|
|
30
|
+
#
|
|
31
|
+
# The value domain is Ruby's own values where they fit - nil (null), true and
|
|
32
|
+
# false, Integer and Float (kept apart), String, Array (a list) and Hash (a
|
|
33
|
+
# map, keyed by a String, an Integer, true, false, nil or
|
|
34
|
+
# {Predicator::UNDEFINED}) - plus {Predicator::UNDEFINED},
|
|
35
|
+
# {Predicator::Date}, {Predicator::DateTime} (built with
|
|
36
|
+
# `Predicator::DateTime.new(epoch_seconds:, microsecond:)`) and
|
|
37
|
+
# {Predicator::Duration}. The codec for the
|
|
38
|
+
# conformance corpus's tagged encoding is loaded on its own, with
|
|
39
|
+
# `require "predicator/tagged"`.
|
|
40
|
+
#
|
|
41
|
+
# The options of an evaluation are keywords of the call that runs it, and
|
|
42
|
+
# nothing is configured globally:
|
|
43
|
+
#
|
|
44
|
+
# - `functions:` a Hash of String name to a callable (anything that responds
|
|
45
|
+
# to `call`), the host's functions for this run. A function is called with
|
|
46
|
+
# the call's arguments only, first argument first, each a copy, so that
|
|
47
|
+
# nothing it does to one changes the context; the counts of arguments it
|
|
48
|
+
# takes are read from its parameters; it refuses by raising a
|
|
49
|
+
# StandardError, which fails the run with an EvaluationError whose reason
|
|
50
|
+
# and message are `Function NAME() raised: ` and the exception's inspect;
|
|
51
|
+
# an answer
|
|
52
|
+
# outside the value domain fails the run too. A name here shadows the
|
|
53
|
+
# library function of the same name, for this run only. `{}` by default.
|
|
54
|
+
# - `clock:` a callable answering the current time as a
|
|
55
|
+
# {Predicator::DateTime}, read at most once per run and only when the
|
|
56
|
+
# program reads the time; the system's real-time clock by default. A
|
|
57
|
+
# clock that answers anything else, or raises a StandardError, fails the
|
|
58
|
+
# run with an EvaluationError whose reason is `clock_failed`, whether a
|
|
59
|
+
# relative date or `Date.now()` read it.
|
|
60
|
+
# - `on_unbound:` what a load of a variable the context does not bind does:
|
|
61
|
+
# `:undefined` (the default) pushes {Predicator::UNDEFINED}, and the run
|
|
62
|
+
# fails with an UndefinedVariableError only if undefined is its answer;
|
|
63
|
+
# `:error` fails the run at the load.
|
|
64
|
+
# - `loop_budget:` how many back edges (`jump_backward`) one run may take,
|
|
65
|
+
# an Integer of at least 0; 10_000 by default. A run that finds it spent
|
|
66
|
+
# fails with the reason `loop_budget_exceeded`.
|
|
67
|
+
#
|
|
68
|
+
# A context, and any `lit` operand of an instruction list the host built, is
|
|
69
|
+
# checked against the value domain before anything runs: a root key of the
|
|
70
|
+
# context that is not a String, a map key or a value the domain has no member
|
|
71
|
+
# for (a Symbol, a Time, any other object), a Float that is not finite, a
|
|
72
|
+
# list or map that contains itself, and lists and maps nested past 100
|
|
73
|
+
# levels along any path each fail the run with an EvaluationError (reasons
|
|
74
|
+
# `unsupported_host_value`, `non_finite_number`, `cyclic_value`,
|
|
75
|
+
# `depth_limit_exceeded`).
|
|
5
76
|
module Predicator
|
|
6
|
-
|
|
7
|
-
|
|
77
|
+
# The instruction-set version this build emits and runs.
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# Predicator.isa_version # => 6
|
|
81
|
+
#
|
|
82
|
+
# @return [Integer]
|
|
83
|
+
def self.isa_version = Evaluator::ISA_VERSION
|
|
84
|
+
|
|
85
|
+
# Compiles an expression's source to the instruction list the evaluator
|
|
86
|
+
# runs. The operands are predicator values: a date literal is a
|
|
87
|
+
# {Predicator::Date}, a decimal a Float, `undefined`
|
|
88
|
+
# {Predicator::UNDEFINED}.
|
|
89
|
+
#
|
|
90
|
+
# A source that does not compile answers a failed result carrying a
|
|
91
|
+
# {Predicator::ParseError}: the first refusal the scan, the parse or the
|
|
92
|
+
# emit met, with its reason, the reference's message where the reference
|
|
93
|
+
# has one, and its position. A statement (an assignment, an `if`, a
|
|
94
|
+
# `while`) is not an expression and is refused; {compile_program} compiles
|
|
95
|
+
# one.
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# Predicator.compile("score > 85").value
|
|
99
|
+
# # => [["load", "score"], ["lit", 85], ["compare", "GT"]]
|
|
100
|
+
#
|
|
101
|
+
# @param source [String] the text, in UTF-8 or an encoding that converts
|
|
102
|
+
# to it
|
|
103
|
+
# @return [Result] the instruction list, or the {ParseError}
|
|
104
|
+
# @raise [ArgumentError] when source is not a String, or is not text in
|
|
105
|
+
# its encoding
|
|
106
|
+
def self.compile(source) = compiled(source) { |tokens| Parser.parse(tokens) }
|
|
107
|
+
|
|
108
|
+
# As {compile}, answering the instruction list itself.
|
|
109
|
+
#
|
|
110
|
+
# @param source [String]
|
|
111
|
+
# @return [Array] the instruction list
|
|
112
|
+
# @raise [Predicator::Error] carrying the {ParseError}
|
|
113
|
+
# @raise [ArgumentError] as {compile} raises it
|
|
114
|
+
def self.compile!(source) = unwrap(compile(source))
|
|
115
|
+
|
|
116
|
+
# Compiles a program's source, one or more statements separated by `;`, to
|
|
117
|
+
# the instruction list the evaluator runs in statement mode. A statement is
|
|
118
|
+
# an assignment to a location (an identifier, a property access or a
|
|
119
|
+
# bracket access), an `if` with an optional `else` or `else if`, a
|
|
120
|
+
# `while`, or an expression whose value is discarded. Every statement
|
|
121
|
+
# leaves the stack as it found it: an assignment ends in a `store`, an
|
|
122
|
+
# expression in a `pop`.
|
|
123
|
+
#
|
|
124
|
+
# A source that does not compile answers a failed result carrying a
|
|
125
|
+
# {Predicator::ParseError}, as {compile} does.
|
|
126
|
+
#
|
|
127
|
+
# @example
|
|
128
|
+
# Predicator.compile_program("x = 1; x + 1").value
|
|
129
|
+
# # => [["lit", "x"], ["lit", 1], ["store", 1], ["load", "x"], ["lit", 1], ["add"], ["pop"]]
|
|
130
|
+
#
|
|
131
|
+
# @param source [String] the text, in UTF-8 or an encoding that converts
|
|
132
|
+
# to it
|
|
133
|
+
# @return [Result] the instruction list, or the {ParseError}
|
|
134
|
+
# @raise [ArgumentError] when source is not a String, or is not text in
|
|
135
|
+
# its encoding
|
|
136
|
+
def self.compile_program(source) = compiled(source, program: true) { |tokens| Parser.parse_program(tokens) }
|
|
137
|
+
|
|
138
|
+
# As {compile_program}, answering the instruction list itself.
|
|
139
|
+
#
|
|
140
|
+
# @param source [String]
|
|
141
|
+
# @return [Array] the instruction list
|
|
142
|
+
# @raise [Predicator::Error] carrying the {ParseError}
|
|
143
|
+
# @raise [ArgumentError] as {compile_program} raises it
|
|
144
|
+
def self.compile_program!(source) = unwrap(compile_program(source))
|
|
145
|
+
|
|
146
|
+
# Runs an expression against a context and answers its value: the top of
|
|
147
|
+
# the stack at halt. The program is an instruction list, or an
|
|
148
|
+
# expression's source, which is compiled as {compile} compiles it (a
|
|
149
|
+
# source that does not compile answers its {ParseError}).
|
|
150
|
+
#
|
|
151
|
+
# A run that stops answers the error value that stopped it: an
|
|
152
|
+
# {EvaluationError}, a {TypeMismatchError} or an
|
|
153
|
+
# {UndefinedVariableError}. An empty stack at halt is an error, and so is
|
|
154
|
+
# undefined at halt when a load did not find its variable.
|
|
155
|
+
#
|
|
156
|
+
# @example
|
|
157
|
+
# Predicator.evaluate("score > 85", { "score" => 90 }).value # => true
|
|
158
|
+
# Predicator.evaluate([["load", "x"], ["lit", 1], ["add"]], { "x" => 2 }).value # => 3
|
|
159
|
+
#
|
|
160
|
+
# @param program [Array, String] an instruction list, or an expression's
|
|
161
|
+
# source
|
|
162
|
+
# @param context [Hash{String => Object}] the variables a load reads
|
|
163
|
+
# @param options [Hash] `functions:`, `clock:`, `on_unbound:` and
|
|
164
|
+
# `loop_budget:`, as {Predicator} describes them
|
|
165
|
+
# @return [Result] the value, or the error value
|
|
166
|
+
# @raise [ArgumentError] when the program or the context is not of its
|
|
167
|
+
# kind, or an option is unknown or malformed (a clock that does not
|
|
168
|
+
# respond to `call` among them)
|
|
169
|
+
def self.evaluate(program, context = {}, **)
|
|
170
|
+
instructions = instructions_of(program) { |source| compile(source) }
|
|
171
|
+
return instructions if instructions.is_a?(Result)
|
|
172
|
+
|
|
173
|
+
refused = refusal_of(instructions, context)
|
|
174
|
+
refused ? Results.failure(refused) : Evaluator.evaluate(instructions, context, **)
|
|
8
175
|
end
|
|
9
176
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
177
|
+
# As {evaluate}, answering the value itself.
|
|
178
|
+
#
|
|
179
|
+
# @param program [Array, String]
|
|
180
|
+
# @param context [Hash{String => Object}]
|
|
181
|
+
# @param options [Hash] as {evaluate} takes them
|
|
182
|
+
# @return [Object] the value
|
|
183
|
+
# @raise [Predicator::Error] carrying the error value
|
|
184
|
+
# @raise [ArgumentError] as {evaluate} raises it
|
|
185
|
+
def self.evaluate!(program, context = {}, **) = unwrap(evaluate(program, context, **))
|
|
186
|
+
|
|
187
|
+
# Runs a statement program against a context and answers the context at
|
|
188
|
+
# halt, a Hash holding every write the program made. The program is an
|
|
189
|
+
# instruction list, or a program's source, which is compiled as
|
|
190
|
+
# {compile_program} compiles it. The context passed is never changed: a
|
|
191
|
+
# write builds a new one, and when nothing is written the context at halt
|
|
192
|
+
# is the one passed. An empty stack at halt is a normal halt.
|
|
193
|
+
#
|
|
194
|
+
# A run that stops answers the error value that stopped it, and no
|
|
195
|
+
# context.
|
|
196
|
+
#
|
|
197
|
+
# @example
|
|
198
|
+
# Predicator.execute("x = 1; y = x + 2").value # => {"x" => 1, "y" => 3}
|
|
199
|
+
#
|
|
200
|
+
# @param program [Array, String] an instruction list, or a program's
|
|
201
|
+
# source
|
|
202
|
+
# @param context [Hash{String => Object}] the variables a load reads
|
|
203
|
+
# @param options [Hash] as {evaluate} takes them
|
|
204
|
+
# @return [Result] the context at halt, or the error value
|
|
205
|
+
# @raise [ArgumentError] as {evaluate} raises it
|
|
206
|
+
def self.execute(program, context = {}, **)
|
|
207
|
+
ran = statements(program, context, **)
|
|
208
|
+
return ran if ran.is_a?(Result)
|
|
209
|
+
|
|
210
|
+
ran.ok? ? Results.ok(ran.context) : Results.failure(ran.error)
|
|
13
211
|
end
|
|
14
212
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
213
|
+
# As {execute}, answering the context itself.
|
|
214
|
+
#
|
|
215
|
+
# @param program [Array, String]
|
|
216
|
+
# @param context [Hash{String => Object}]
|
|
217
|
+
# @param options [Hash] as {evaluate} takes them
|
|
218
|
+
# @return [Hash] the context at halt
|
|
219
|
+
# @raise [Predicator::Error] carrying the error value
|
|
220
|
+
# @raise [ArgumentError] as {evaluate} raises it
|
|
221
|
+
def self.execute!(program, context = {}, **) = unwrap(execute(program, context, **))
|
|
222
|
+
|
|
223
|
+
# Runs a statement program as {execute} does and answers a pair: the value
|
|
224
|
+
# of its last expression statement (the value the last `pop` discarded,
|
|
225
|
+
# {Predicator::UNDEFINED} when no expression statement ran) and the context
|
|
226
|
+
# at halt.
|
|
227
|
+
#
|
|
228
|
+
# A run that stops answers the error value that stopped it, and neither a
|
|
229
|
+
# value nor a context.
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# value, context = Predicator.execute_value!("x = 2; x * 10")
|
|
233
|
+
# value # => 20
|
|
234
|
+
# context # => {"x" => 2}
|
|
235
|
+
#
|
|
236
|
+
# @param program [Array, String] an instruction list, or a program's
|
|
237
|
+
# source
|
|
238
|
+
# @param context [Hash{String => Object}] the variables a load reads
|
|
239
|
+
# @param options [Hash] as {evaluate} takes them
|
|
240
|
+
# @return [Result] the pair `[value, context]`, or the error value
|
|
241
|
+
# @raise [ArgumentError] as {evaluate} raises it
|
|
242
|
+
def self.execute_value(program, context = {}, **)
|
|
243
|
+
ran = statements(program, context, **)
|
|
244
|
+
return ran if ran.is_a?(Result)
|
|
245
|
+
|
|
246
|
+
ran.ok? ? Results.ok([ran.value, ran.context]) : Results.failure(ran.error)
|
|
18
247
|
end
|
|
19
248
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
249
|
+
# As {execute_value}, answering the pair itself.
|
|
250
|
+
#
|
|
251
|
+
# @param program [Array, String]
|
|
252
|
+
# @param context [Hash{String => Object}]
|
|
253
|
+
# @param options [Hash] as {evaluate} takes them
|
|
254
|
+
# @return [Array] the pair `[value, context]`
|
|
255
|
+
# @raise [Predicator::Error] carrying the error value
|
|
256
|
+
# @raise [ArgumentError] as {evaluate} raises it
|
|
257
|
+
def self.execute_value!(program, context = {}, **) = unwrap(execute_value(program, context, **))
|
|
258
|
+
|
|
259
|
+
# Parses an expression's source to its syntax tree, without compiling it:
|
|
260
|
+
# the first two of the three stages {compile} runs, so a source this
|
|
261
|
+
# refuses, {compile} refuses with the same {ParseError}. The converse does
|
|
262
|
+
# not hold: the third stage, the emitter, refuses a decimal literal too
|
|
263
|
+
# large for a finite Float (`number_out_of_range`), and a tree whose walk
|
|
264
|
+
# runs out of stack (`nesting_depth_exceeded`), and such a source parses
|
|
265
|
+
# here.
|
|
266
|
+
#
|
|
267
|
+
# The tree is the reference's: each node an immutable value of the node's
|
|
268
|
+
# kind, carrying its fields, its position and its span. The node classes
|
|
269
|
+
# are private to the package, and the tree's shape is not a compatibility
|
|
270
|
+
# promise: it may change in a minor version.
|
|
271
|
+
#
|
|
272
|
+
# @param source [String] the text, in UTF-8 or an encoding that converts
|
|
273
|
+
# to it
|
|
274
|
+
# @return [Result] the root node of the tree, or the {ParseError}
|
|
275
|
+
# @raise [ArgumentError] as {compile} raises it
|
|
276
|
+
def self.parse(source)
|
|
277
|
+
tokens = Lexer.tokenize(text_of(source))
|
|
278
|
+
tree = refused?(tokens) ? tokens : Parser.parse(tokens)
|
|
279
|
+
refused?(tree) ? failed(tree) : Results.ok(tree)
|
|
23
280
|
end
|
|
281
|
+
|
|
282
|
+
# As {parse}, answering the tree itself.
|
|
283
|
+
#
|
|
284
|
+
# @param source [String]
|
|
285
|
+
# @return [Object] the root node of the tree
|
|
286
|
+
# @raise [Predicator::Error] carrying the {ParseError}
|
|
287
|
+
# @raise [ArgumentError] as {compile} raises it
|
|
288
|
+
def self.parse!(source) = unwrap(parse(source))
|
|
289
|
+
|
|
290
|
+
# Reads a duration from its literal spelling, such as `"3d8h30m"` or
|
|
291
|
+
# `"1.5s"`: the spelling the `::duration` cast reads and the `::string`
|
|
292
|
+
# cast writes. It is one or more components, each a whole number with an
|
|
293
|
+
# optional decimal fraction followed by one of the units `y`, `mo`, `w`,
|
|
294
|
+
# `d`, `h`, `m`, `s` and `ms`, with no whitespace, no sign and no empty
|
|
295
|
+
# text. A repeated unit adds to what came before; a fraction expands into
|
|
296
|
+
# whole smaller units (a month 30 days, a year 365), and one that is not a
|
|
297
|
+
# whole number of milliseconds is refused.
|
|
298
|
+
#
|
|
299
|
+
# A text that does not spell a duration answers a failed result carrying
|
|
300
|
+
# an {EvaluationError} with the reason `invalid_duration_format`.
|
|
301
|
+
#
|
|
302
|
+
# @example
|
|
303
|
+
# Predicator.parse_duration("1h30m").value.minutes # => 30
|
|
304
|
+
#
|
|
305
|
+
# @param text [String]
|
|
306
|
+
# @return [Result] the {Predicator::Duration}, or the error value
|
|
307
|
+
# @raise [ArgumentError] when text is not a String, or is not text in its
|
|
308
|
+
# encoding
|
|
309
|
+
def self.parse_duration(text)
|
|
310
|
+
duration = DurationText.read(text_of(text))
|
|
311
|
+
return Results.ok(duration) if duration
|
|
312
|
+
|
|
313
|
+
Results.failure(EvaluationError.new(reason: "invalid_duration_format", message: "Invalid duration format: #{text}"))
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# As {parse_duration}, answering the duration itself.
|
|
317
|
+
#
|
|
318
|
+
# @param text [String]
|
|
319
|
+
# @return [Duration]
|
|
320
|
+
# @raise [Predicator::Error] carrying the error value
|
|
321
|
+
# @raise [ArgumentError] as {parse_duration} raises it
|
|
322
|
+
def self.parse_duration!(text) = unwrap(parse_duration(text))
|
|
323
|
+
|
|
324
|
+
# Scans the source, parses its tokens with the block and emits the tree,
|
|
325
|
+
# answering the first refusal of the three.
|
|
326
|
+
def self.compiled(source, program: false)
|
|
327
|
+
tokens = Lexer.tokenize(text_of(source))
|
|
328
|
+
tree = refused?(tokens) ? tokens : yield(tokens)
|
|
329
|
+
entries = if refused?(tree) then tree
|
|
330
|
+
elsif program then Emitter.emit_program(tree)
|
|
331
|
+
else Emitter.emit(tree)
|
|
332
|
+
end
|
|
333
|
+
return failed(entries) if refused?(entries)
|
|
334
|
+
|
|
335
|
+
Results.ok(entries.map(&:instruction))
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def self.refused?(stage) = stage.is_a?(Lexer::Refusal)
|
|
339
|
+
def self.failed(refusal) = Results.failure(refusal.error)
|
|
340
|
+
|
|
341
|
+
# The source as valid UTF-8: bytes with no encoding of their own (binary
|
|
342
|
+
# or US-ASCII) are read as UTF-8, and text in any other encoding is
|
|
343
|
+
# converted to it.
|
|
344
|
+
def self.text_of(source)
|
|
345
|
+
raise ArgumentError, "a source is a String, not #{source.class}" unless source.is_a?(String)
|
|
346
|
+
|
|
347
|
+
text = if [Encoding::BINARY, Encoding::US_ASCII].include?(source.encoding)
|
|
348
|
+
source.dup.force_encoding(Encoding::UTF_8)
|
|
349
|
+
else
|
|
350
|
+
source.encode(Encoding::UTF_8)
|
|
351
|
+
end
|
|
352
|
+
return text if text.valid_encoding?
|
|
353
|
+
|
|
354
|
+
raise ArgumentError, "a source is text, and this one is not valid #{text.encoding}"
|
|
355
|
+
rescue EncodingError => e
|
|
356
|
+
raise ArgumentError, "a source is text that converts to UTF-8: #{e.message}"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# The instruction list a program names: the list itself, or the source
|
|
360
|
+
# compiled by the block, or the failed result of a source that does not
|
|
361
|
+
# compile.
|
|
362
|
+
def self.instructions_of(program)
|
|
363
|
+
return program unless program.is_a?(String)
|
|
364
|
+
|
|
365
|
+
compiled = yield(program)
|
|
366
|
+
compiled.ok? ? compiled.value : compiled
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# A statement-mode run of a program, or the failed result of a source
|
|
370
|
+
# that does not compile.
|
|
371
|
+
def self.statements(program, context, **)
|
|
372
|
+
instructions = instructions_of(program) { |source| compile_program(source) }
|
|
373
|
+
return instructions if instructions.is_a?(Result)
|
|
374
|
+
|
|
375
|
+
refused = refusal_of(instructions, context)
|
|
376
|
+
refused ? Results.failure(refused) : Evaluator.execute(instructions, context, **)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# The refusal of a context or a literal operand outside the value domain,
|
|
380
|
+
# or nil. Arguments of the wrong kind are left for the evaluator to raise
|
|
381
|
+
# on.
|
|
382
|
+
def self.refusal_of(instructions, context)
|
|
383
|
+
Boundary.input_refusal(instructions, context) if instructions.is_a?(Array) && context.is_a?(Hash)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# The value of a result, or the raise of its error.
|
|
387
|
+
def self.unwrap(result)
|
|
388
|
+
raise Error, result.error unless result.ok?
|
|
389
|
+
|
|
390
|
+
result.value
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
private_class_method :compiled, :refused?, :failed, :text_of, :instructions_of, :statements, :refusal_of, :unwrap
|
|
24
394
|
end
|
metadata
CHANGED
|
@@ -1,164 +1,75 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: predicator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JohnnyT
|
|
8
|
-
|
|
9
|
-
bindir: exe
|
|
8
|
+
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: coveralls
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: minitest
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - '='
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: 5.4.2
|
|
48
|
-
type: :development
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - '='
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: 5.4.2
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: minitest-focus
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - ">="
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - ">="
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: oedipus_lex
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - ">="
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
76
|
-
type: :development
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ">="
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: pry-nav
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - ">="
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '0'
|
|
90
|
-
type: :development
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ">="
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '0'
|
|
97
|
-
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: racc
|
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
|
100
|
-
requirements:
|
|
101
|
-
- - ">="
|
|
102
|
-
- !ruby/object:Gem::Version
|
|
103
|
-
version: '0'
|
|
104
|
-
type: :development
|
|
105
|
-
prerelease: false
|
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
-
requirements:
|
|
108
|
-
- - ">="
|
|
109
|
-
- !ruby/object:Gem::Version
|
|
110
|
-
version: '0'
|
|
111
|
-
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: rake
|
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
|
114
|
-
requirements:
|
|
115
|
-
- - ">="
|
|
116
|
-
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0'
|
|
118
|
-
type: :development
|
|
119
|
-
prerelease: false
|
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
-
requirements:
|
|
122
|
-
- - ">="
|
|
123
|
-
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0'
|
|
125
|
-
description:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Compiles a condition written as text into stack-machine instructions
|
|
13
|
+
and evaluates it against a context, without evaluating code. Wire-compatible with
|
|
14
|
+
the Elixir reference implementation. No runtime dependencies.
|
|
126
15
|
email:
|
|
127
16
|
- ubergeek3141@gmail.com
|
|
128
17
|
executables: []
|
|
129
18
|
extensions: []
|
|
130
19
|
extra_rdoc_files: []
|
|
131
20
|
files:
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
- ".travis.yml"
|
|
135
|
-
- CODE_OF_CONDUCT.md
|
|
136
|
-
- Gemfile
|
|
137
|
-
- HISTORY.md
|
|
138
|
-
- LICENSE.txt
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
139
23
|
- README.md
|
|
140
|
-
- Rakefile
|
|
141
24
|
- lib/predicator.rb
|
|
142
25
|
- lib/predicator/ast.rb
|
|
143
|
-
- lib/predicator/
|
|
26
|
+
- lib/predicator/boundary.rb
|
|
27
|
+
- lib/predicator/civil.rb
|
|
28
|
+
- lib/predicator/duration_text.rb
|
|
29
|
+
- lib/predicator/emitter.rb
|
|
30
|
+
- lib/predicator/emitter/chains.rb
|
|
31
|
+
- lib/predicator/emitter/statements.rb
|
|
32
|
+
- lib/predicator/errors.rb
|
|
144
33
|
- lib/predicator/evaluator.rb
|
|
145
|
-
- lib/predicator/
|
|
146
|
-
- lib/predicator/
|
|
34
|
+
- lib/predicator/evaluator/access.rb
|
|
35
|
+
- lib/predicator/evaluator/arithmetic.rb
|
|
36
|
+
- lib/predicator/evaluator/calls.rb
|
|
37
|
+
- lib/predicator/evaluator/casts.rb
|
|
38
|
+
- lib/predicator/evaluator/chronology.rb
|
|
39
|
+
- lib/predicator/evaluator/compare.rb
|
|
40
|
+
- lib/predicator/evaluator/json_functions.rb
|
|
41
|
+
- lib/predicator/evaluator/json_scan.rb
|
|
42
|
+
- lib/predicator/evaluator/library.rb
|
|
43
|
+
- lib/predicator/evaluator/math_functions.rb
|
|
44
|
+
- lib/predicator/evaluator/order.rb
|
|
45
|
+
- lib/predicator/evaluator/rich_types.rb
|
|
46
|
+
- lib/predicator/evaluator/statements.rb
|
|
47
|
+
- lib/predicator/float_text.rb
|
|
48
|
+
- lib/predicator/iso_text.rb
|
|
49
|
+
- lib/predicator/lexer.rb
|
|
50
|
+
- lib/predicator/lexer/literals.rb
|
|
51
|
+
- lib/predicator/lexer/numbers.rb
|
|
147
52
|
- lib/predicator/parser.rb
|
|
148
|
-
- lib/predicator/parser.
|
|
53
|
+
- lib/predicator/parser/durations.rb
|
|
54
|
+
- lib/predicator/parser/operators.rb
|
|
55
|
+
- lib/predicator/parser/postfix.rb
|
|
56
|
+
- lib/predicator/parser/primaries.rb
|
|
57
|
+
- lib/predicator/parser/statements.rb
|
|
58
|
+
- lib/predicator/parser/token_text.rb
|
|
59
|
+
- lib/predicator/result.rb
|
|
60
|
+
- lib/predicator/tagged.rb
|
|
61
|
+
- lib/predicator/tagged/reader.rb
|
|
62
|
+
- lib/predicator/tagged/writer.rb
|
|
63
|
+
- lib/predicator/values.rb
|
|
149
64
|
- lib/predicator/version.rb
|
|
150
|
-
|
|
151
|
-
- lib/predicator/visitors/dot.rb
|
|
152
|
-
- lib/predicator/visitors/each.rb
|
|
153
|
-
- lib/predicator/visitors/instructions.rb
|
|
154
|
-
- lib/predicator/visitors/string.rb
|
|
155
|
-
- lib/predicator/visitors/visitor.rb
|
|
156
|
-
- predicator.gemspec
|
|
157
|
-
homepage: https://github.com/predicator/predicator
|
|
65
|
+
homepage: https://github.com/riddler/predicator-rb
|
|
158
66
|
licenses:
|
|
159
67
|
- MIT
|
|
160
|
-
metadata:
|
|
161
|
-
|
|
68
|
+
metadata:
|
|
69
|
+
homepage_uri: https://github.com/riddler/predicator-rb
|
|
70
|
+
source_code_uri: https://github.com/riddler/predicator-rb
|
|
71
|
+
changelog_uri: https://github.com/riddler/predicator-rb/blob/main/CHANGELOG.md
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
162
73
|
rdoc_options: []
|
|
163
74
|
require_paths:
|
|
164
75
|
- lib
|
|
@@ -166,16 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
166
77
|
requirements:
|
|
167
78
|
- - ">="
|
|
168
79
|
- !ruby/object:Gem::Version
|
|
169
|
-
version: '
|
|
80
|
+
version: '3.3'
|
|
170
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
171
82
|
requirements:
|
|
172
83
|
- - ">="
|
|
173
84
|
- !ruby/object:Gem::Version
|
|
174
85
|
version: '0'
|
|
175
86
|
requirements: []
|
|
176
|
-
|
|
177
|
-
rubygems_version: 2.6.8
|
|
178
|
-
signing_key:
|
|
87
|
+
rubygems_version: 4.0.20
|
|
179
88
|
specification_version: 4
|
|
180
|
-
summary:
|
|
89
|
+
summary: A safe, non-evaluating condition language for end users.
|
|
181
90
|
test_files: []
|