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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2177b079f5600e5c04e1f1f15ba535838c9acfe7244c0041408bd874f6f02e1
|
|
4
|
+
data.tar.gz: 823005978281166065bb392f6cee4665bfd4196c181272392b21feac89fd2974
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0c462b21de4ef04f9bcaf6fb5052a58e913cb4e7a1901acad868e90fb286370874eddfe38112d0aee50bc0a55ae4a09bfa58760e497ffe83f9be4207e5e10d8
|
|
7
|
+
data.tar.gz: cb9fd8cc76825c0741cd5f568340d8649ce3b6465240805bfd620d92ad1a9d1adcddf1369db1465d9e50907b90607e81fbf9f84fc2b7682fd2cf65a83fbab3e5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
### Upcoming
|
|
2
|
+
|
|
3
|
+
* (Nothing yet)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### 2.0.0 / 2026-10-24
|
|
7
|
+
|
|
8
|
+
A rewrite of the gem under its existing name, and a breaking release: the 1.x
|
|
9
|
+
API is gone. The gem now compiles and evaluates the condition language of the
|
|
10
|
+
Elixir reference implementation, [predicator-ex](https://github.com/riddler/predicator-ex),
|
|
11
|
+
with its instruction set version 6, and is checked against that
|
|
12
|
+
implementation's conformance corpus at the tag `v9.5.0`, vendored under
|
|
13
|
+
`conformance/` (`conformance/SOURCE.json` records the tag, its commit and the
|
|
14
|
+
corpus hash). It claims tier 9 of the corpus's evaluator surface and tier 7 of
|
|
15
|
+
its compiler surface (`conformance/registry.json`), with no case skipped. It
|
|
16
|
+
requires Ruby 3.3 or later and has no runtime dependencies. To stay on 1.x,
|
|
17
|
+
pin `gem "predicator", "~> 1.2"`; the README's "Upgrading from 1.x" maps each
|
|
18
|
+
1.x call to its 2.0 counterpart. The 1.x history, and the source of the 1.x
|
|
19
|
+
gem, live in the [riddler/predicator](https://github.com/riddler/predicator)
|
|
20
|
+
repository (`impl/rb`).
|
|
21
|
+
|
|
22
|
+
**Added**
|
|
23
|
+
|
|
24
|
+
* The value domain: `Predicator::UNDEFINED` (an absence, distinct from `nil`, which is null), and the immutable values `Predicator::Date`, `Predicator::DateTime` (an instant in UTC to the microsecond) and `Predicator::Duration` (eight integer units). Integers and floats stay Ruby's `Integer` and `Float`, so `1` and `1.0` stay distinct.
|
|
25
|
+
* The error values `Predicator::ParseError`, `Predicator::EvaluationError`, `Predicator::TypeMismatchError` and `Predicator::UndefinedVariableError`, each with a `type` and a `reason`, a `message`, and a `Predicator::Position` (line and column) where one is known; and `Predicator::Error`, the exception that carries one.
|
|
26
|
+
* `Predicator::Result`, the value a call answers: `ok?`, `value` and `error`.
|
|
27
|
+
* `require "predicator/tagged"` loads `Predicator::Tagged`, a codec for the conformance corpus's tagged JSON encoding: `decode("[1.0]")` and `encode([1.0])` answer a `Predicator::Result`, and `decode!` and `encode!` raise `Predicator::Error` instead.
|
|
28
|
+
* `Predicator.compile(source)` compiles an expression to the instruction list the evaluator runs, and `Predicator.compile_program(source)` compiles one or more statements (assignments, `if`, `while`, expressions) separated by `;`. Each answers a `Predicator::Result`: the list, its operands predicator values (`Predicator.compile("x > 1").value` is `[["load", "x"], ["lit", 1], ["compare", "GT"]]`), or a `Predicator::ParseError` with its reason, message and position. A source that is not a String, or not valid text in its encoding, raises `ArgumentError`.
|
|
29
|
+
* A decimal literal too large for a finite Float is refused with the reason `number_out_of_range`, and compiling any source prints no warning.
|
|
30
|
+
* `Predicator.evaluate(program, context, **options)` runs an expression, given as an instruction list or as source text, and answers its value; `Predicator.execute` runs a statement program and answers the context at halt; `Predicator.execute_value` answers the pair of the last expression statement's value and that context. A run that stops answers its error value and no context.
|
|
31
|
+
* `Predicator.parse(source)` answers an expression's syntax tree (its node classes are private and its shape is not a compatibility promise), `Predicator.parse_duration("1h30m")` answers a `Predicator::Duration` or an `EvaluationError` with the reason `invalid_duration_format`, and `Predicator.isa_version` answers 6.
|
|
32
|
+
* Every entry point that answers a `Predicator::Result` has a bang variant (`compile!`, `compile_program!`, `evaluate!`, `execute!`, `execute_value!`, `parse!`, `parse_duration!`) that answers the value or raises `Predicator::Error` carrying the error value.
|
|
33
|
+
* A run's options are keywords of the call: `functions:` (a Hash of String name to a callable, called with the arguments only, each a copy, refusing by raising), `clock:` (a callable answering a `Predicator::DateTime`; one that answers anything else, or raises, fails the run with an `EvaluationError` whose reason is `clock_failed`), `on_unbound:` (`:undefined` or `:error`) and `loop_budget:` (10000 back edges by default). Nothing is configured globally.
|
|
34
|
+
* A context, a literal operand or a host function's answer outside the value domain (a key that is not a String at the context's root, a Symbol or other object, a Float that is not finite, a list or map that contains itself or nests past 100 levels) fails the run with an `EvaluationError` (`unsupported_host_value`, `non_finite_number`, `cyclic_value`, `depth_limit_exceeded`) instead of raising.
|
|
35
|
+
|
|
36
|
+
**Changed**
|
|
37
|
+
|
|
38
|
+
* Breaking: Ruby 3.3 or later is required, where 1.x declared no Ruby version; stay on 1.x on an older Ruby.
|
|
39
|
+
|
|
40
|
+
**Removed**
|
|
41
|
+
|
|
42
|
+
* Breaking: the 1.x API is removed, and 1.x code does not run against 2.0. Pin `gem "predicator", "~> 1.2"` to stay on 1.x, or move each call as the README's "Upgrading from 1.x" says; store a condition's source text and compile it with 2.0 rather than reusing a 1.x instruction list.
|
data/{LICENSE.txt → LICENSE}
RENAMED
data/README.md
CHANGED
|
@@ -1,78 +1,354 @@
|
|
|
1
|
-
[](http://badge.fury.io/rb/predicator)
|
|
2
|
-
[](https://travis-ci.org/predicator/predicator)
|
|
3
|
-
[](https://coveralls.io/github/predicator/predicator?branch=master)
|
|
4
|
-
|
|
5
1
|
# Predicator
|
|
6
2
|
|
|
7
|
-
|
|
3
|
+
[![MIT License][badge_license_url]](LICENSE)
|
|
4
|
+
[![CI][badge_ci_url]](https://github.com/riddler/predicator-rb/actions/workflows/ci.yml)
|
|
5
|
+
[![Gem Version][badge_version_url]](https://rubygems.org/gems/predicator)
|
|
6
|
+
|
|
7
|
+
A safe, non-evaluating condition language for end users, for Ruby. A condition
|
|
8
|
+
is written as text, such as `score > 85 and 2 in items`, compiled to a flat
|
|
9
|
+
list of stack-machine instructions and run against a context of variables. No
|
|
10
|
+
Ruby is ever evaluated: the text is read by a lexer and a parser of this
|
|
11
|
+
package, and the instructions are run by an evaluator that can do only what
|
|
12
|
+
the instruction set says. The language, the instruction set and the answers
|
|
13
|
+
are wire-compatible with the Elixir reference implementation,
|
|
14
|
+
[predicator-ex](https://github.com/riddler/predicator-ex), and the gem is
|
|
15
|
+
checked against that implementation's conformance corpus.
|
|
16
|
+
|
|
17
|
+
## Why Predicator
|
|
18
|
+
|
|
19
|
+
A rule an end user writes has to be decided by code the user did not write,
|
|
20
|
+
and often in more than one place. Passing the text to `eval` hands the user
|
|
21
|
+
the whole interpreter; a hand-rolled parser per place lets the copies drift
|
|
22
|
+
apart. Predicator gives one small language with a defined set of operations,
|
|
23
|
+
a defined answer for every operand a rule can meet (an error value, never a
|
|
24
|
+
crash), and a shared corpus that every implementation of it must pass. A
|
|
25
|
+
condition compiled in one implementation can be run by another, because the
|
|
26
|
+
instruction list is plain data.
|
|
8
27
|
|
|
9
|
-
##
|
|
28
|
+
## Install
|
|
10
29
|
|
|
11
|
-
|
|
30
|
+
Add `predicator` to your Gemfile:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
gem "predicator", "~> 2.0"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
And then execute:
|
|
37
|
+
|
|
38
|
+
$ bundle install
|
|
39
|
+
|
|
40
|
+
Or install it yourself as:
|
|
41
|
+
|
|
42
|
+
$ gem install predicator
|
|
43
|
+
|
|
44
|
+
## Basic usage
|
|
12
45
|
|
|
13
46
|
```ruby
|
|
14
47
|
require "predicator"
|
|
15
48
|
|
|
16
|
-
|
|
49
|
+
# Compile once: the instruction list is plain data you can store or send
|
|
50
|
+
compiled = Predicator.compile("score > 85 and 2 in items")
|
|
51
|
+
compiled.value
|
|
52
|
+
# => [["load", "score"], ["lit", 85], ["compare", "GT"], ["jump_if_falsy_or_pop", 4],
|
|
53
|
+
# ["lit", 2], ["load", "items"], ["in"]]
|
|
54
|
+
|
|
55
|
+
# Run it against a context: a Hash keyed by Strings
|
|
56
|
+
context = { "score" => 90, "items" => [1, 2, 3] }
|
|
57
|
+
Predicator.evaluate(compiled.value, context).value # => true
|
|
58
|
+
|
|
59
|
+
# Or hand evaluate the source and let it compile
|
|
60
|
+
Predicator.evaluate("a.b + 1", { "a" => { "b" => 2 } }).value # => 3
|
|
61
|
+
|
|
62
|
+
# A statement program answers the context it leaves
|
|
63
|
+
Predicator.execute("x = 1; y = x + 2").value # => {"x" => 1, "y" => 3}
|
|
17
64
|
|
|
18
|
-
|
|
65
|
+
# A host function is passed per call, by name
|
|
66
|
+
double = ->(n) { n * 2 }
|
|
67
|
+
Predicator.evaluate("double(x)", { "x" => 4 }, functions: { "double" => double }).value # => 8
|
|
68
|
+
|
|
69
|
+
# The bang variants answer the value itself, or raise Predicator::Error
|
|
70
|
+
Predicator.evaluate!("score > 85", { "score" => 90 }) # => true
|
|
19
71
|
```
|
|
20
72
|
|
|
21
|
-
|
|
73
|
+
## The result and error values
|
|
74
|
+
|
|
75
|
+
Every entry point answers a `Predicator::Result`: `ok?`, `value` and `error`.
|
|
76
|
+
A successful result has `error == nil` and a failed one has `value == nil`, so
|
|
77
|
+
read `ok?` and not the value: `nil` is predicator's null, and a successful
|
|
78
|
+
result may hold it. Nothing about the source or the program raises: a source
|
|
79
|
+
that does not compile, a run that stops and a value outside the language's
|
|
80
|
+
domain all answer a failed result.
|
|
22
81
|
|
|
23
82
|
```ruby
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
score: score,
|
|
30
|
-
income: income,
|
|
31
|
-
...
|
|
32
|
-
}
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
class Offer
|
|
37
|
-
attr_accessor :available_predicate
|
|
38
|
-
|
|
39
|
-
...
|
|
40
|
-
|
|
41
|
-
def available_to? customer
|
|
42
|
-
Predicator.evaluate available_predicate, customer.to_hash
|
|
43
|
-
end
|
|
44
|
-
end
|
|
83
|
+
result = Predicator.evaluate("x / 0", { "x" => 1 })
|
|
84
|
+
result.ok? # => false
|
|
85
|
+
result.error.type # => "EvaluationError"
|
|
86
|
+
result.error.reason # => "division_by_zero"
|
|
87
|
+
result.error.message # => "divide was given a zero divisor"
|
|
45
88
|
```
|
|
46
89
|
|
|
47
|
-
|
|
90
|
+
A call that compiles or runs answers one of four error values, each with a
|
|
91
|
+
`type` (its class's name), a `reason`, a `message` and a `position` (a
|
|
92
|
+
`Predicator::Position` with `line` and `column`, or `nil` where none is
|
|
93
|
+
known):
|
|
94
|
+
|
|
95
|
+
- `Predicator::ParseError`: a source that does not compile. It always has a
|
|
96
|
+
position.
|
|
97
|
+
- `Predicator::EvaluationError`: a run that stopped, such as a division by
|
|
98
|
+
zero, an unknown instruction or a host function that raised.
|
|
99
|
+
- `Predicator::TypeMismatchError`: an operand an operation refused on its
|
|
100
|
+
type. Its reason is the operation.
|
|
101
|
+
- `Predicator::UndefinedVariableError`: a variable the context does not bind,
|
|
102
|
+
when the run's answer depends on it.
|
|
103
|
+
|
|
104
|
+
The `type` and the `reason` are what the conformance corpus matches; the
|
|
105
|
+
`message` is for a person and may change. Each entry point has a bang variant
|
|
106
|
+
(`compile!`, `compile_program!`, `evaluate!`, `execute!`, `execute_value!`,
|
|
107
|
+
`parse!`, `parse_duration!`) that answers the value or raises
|
|
108
|
+
`Predicator::Error`, whose `error` is the same error value.
|
|
109
|
+
|
|
110
|
+
Two things still raise. A bug in the call itself (an argument of the wrong
|
|
111
|
+
kind, an unknown option, a source that is not valid text) raises
|
|
112
|
+
`ArgumentError`. And an exception that is not a `StandardError` (an
|
|
113
|
+
`Interrupt`, a `SystemExit`, a `NoMemoryError`) raised by host code a run
|
|
114
|
+
calls propagates unchanged.
|
|
115
|
+
|
|
116
|
+
### The entry points
|
|
117
|
+
|
|
118
|
+
| Call | Answers, in the result |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `Predicator.compile(source)` | the instruction list of an expression, or a `ParseError` |
|
|
121
|
+
| `Predicator.compile_program(source)` | the instruction list of one or more statements separated by `;`, or a `ParseError` |
|
|
122
|
+
| `Predicator.evaluate(program, context = {}, **options)` | an expression's value; `program` is an instruction list or source |
|
|
123
|
+
| `Predicator.execute(program, context = {}, **options)` | the context at halt, a Hash holding every write; the context passed is never changed |
|
|
124
|
+
| `Predicator.execute_value(program, context = {}, **options)` | the pair `[value, context]` of the last expression statement's value and the context at halt |
|
|
125
|
+
| `Predicator.parse(source)` | the root of the syntax tree |
|
|
126
|
+
| `Predicator.parse_duration(text)` | a `Predicator::Duration` read from text such as `"1h30m"` |
|
|
127
|
+
| `Predicator.isa_version` | the instruction-set version the build emits and runs, 6 |
|
|
128
|
+
|
|
129
|
+
`parse` answers the syntax tree's root. The node classes and the tree's shape
|
|
130
|
+
are not part of the public surface and may change in a minor version.
|
|
131
|
+
|
|
132
|
+
### Options
|
|
48
133
|
|
|
49
|
-
|
|
134
|
+
The options of a run are keywords of the call that runs it. Nothing is
|
|
135
|
+
configured globally.
|
|
136
|
+
|
|
137
|
+
- `functions:` a Hash of String name to a callable, the host's functions for
|
|
138
|
+
this run. A function is called with the call's arguments only, first
|
|
139
|
+
argument first, and nothing else (no context argument). Each argument is a
|
|
140
|
+
copy, so nothing the function does to one changes the context. The counts of
|
|
141
|
+
arguments a function takes are read from its parameters. It refuses by
|
|
142
|
+
raising a `StandardError`, which fails the run with an `EvaluationError`. Its
|
|
143
|
+
answer is checked against the value domain, and an answer outside it fails
|
|
144
|
+
the run; the answer is not copied. A name here shadows the library function
|
|
145
|
+
of the same name, for this run only. `{}` by default.
|
|
146
|
+
- `clock:` a callable answering the current time as a `Predicator::DateTime`,
|
|
147
|
+
read at most once per run and only when the program reads the time. The
|
|
148
|
+
system's real-time clock by default. A clock that answers anything else, or
|
|
149
|
+
raises a `StandardError`, fails the run with the reason `clock_failed`.
|
|
150
|
+
- `on_unbound:` what a load of a variable the context does not bind does:
|
|
151
|
+
`:undefined` (the default) pushes `Predicator::UNDEFINED` and fails the run
|
|
152
|
+
with an `UndefinedVariableError` only if undefined is the answer; `:error`
|
|
153
|
+
fails the run at the load.
|
|
154
|
+
- `loop_budget:` how many back edges one run may take, an Integer of at least
|
|
155
|
+
0, 10_000 by default. A run that finds it spent fails with the reason
|
|
156
|
+
`loop_budget_exceeded`.
|
|
157
|
+
|
|
158
|
+
A run that stops answers its error value and no partial context: `execute` and
|
|
159
|
+
`execute_value` answer neither a context nor a value for a run that stopped.
|
|
160
|
+
|
|
161
|
+
### The value domain
|
|
162
|
+
|
|
163
|
+
A context, and any literal operand of an instruction list you built, is
|
|
164
|
+
checked against the value domain before anything runs. The domain is Ruby's
|
|
165
|
+
own values where they fit: `nil` (null), `true` and `false`, `Integer` and
|
|
166
|
+
`Float` (kept apart, so `1` and `1.0` differ), `String`, `Array` (a list) and
|
|
167
|
+
`Hash` (a map, keyed by a String, an Integer, `true`, `false`, `nil` or
|
|
168
|
+
`Predicator::UNDEFINED`), plus `Predicator::UNDEFINED` (an absence, distinct
|
|
169
|
+
from `nil`), `Predicator::Date`, `Predicator::DateTime` (an instant in UTC to
|
|
170
|
+
the microsecond) and `Predicator::Duration` (eight integer units).
|
|
171
|
+
|
|
172
|
+
The root keys of a context are Strings: a Symbol key at the root fails the run
|
|
173
|
+
with an `EvaluationError` (reason `unsupported_host_value`) and is not
|
|
174
|
+
converted. The same reason refuses a value the domain has no member for (a
|
|
175
|
+
Symbol, a `Time`, any other object). A Float that is not finite
|
|
176
|
+
(`non_finite_number`), a list or map that contains itself (`cyclic_value`) and
|
|
177
|
+
lists and maps nested past 100 levels (`depth_limit_exceeded`) are refused the
|
|
178
|
+
same way. Equality, ordering and membership walk nested lists and maps without
|
|
179
|
+
recursing, so a value nested to any depth inside those limits is compared
|
|
180
|
+
without overflowing the stack.
|
|
181
|
+
|
|
182
|
+
`require "predicator/tagged"` loads `Predicator::Tagged`, a codec for the
|
|
183
|
+
conformance corpus's tagged JSON encoding, which keeps what a JSON round trip
|
|
184
|
+
loses (dates, instants, durations, undefined): `decode` and `encode` answer a
|
|
185
|
+
`Predicator::Result`, and `decode!` and `encode!` raise `Predicator::Error`.
|
|
186
|
+
A failed codec result carries a `Predicator::Tagged::CodecError`, which has a
|
|
187
|
+
`type`, a `reason` and a `message` and no position.
|
|
188
|
+
|
|
189
|
+
## Compatibility
|
|
190
|
+
|
|
191
|
+
- **Ruby 3.3 or later** (`required_ruby_version` in the gemspec). CI runs the
|
|
192
|
+
test suite on every Ruby minor from 3.3 up to the version `mise.toml` pins,
|
|
193
|
+
and the full quality gate on that pinned version.
|
|
194
|
+
- **No runtime dependencies.** The gemspec declares none; `strscan` and `json`
|
|
195
|
+
are default gems that ship with Ruby.
|
|
196
|
+
- **No shared mutable state.** There is no global configuration, no monkey
|
|
197
|
+
patch and no class-level or module-level mutable state. Host functions and
|
|
198
|
+
clocks are passed per call, and a call reads only its arguments, so nothing
|
|
199
|
+
is shared between calls.
|
|
200
|
+
- **Instruction set:** version 6, answered by `Predicator.isa_version`.
|
|
201
|
+
|
|
202
|
+
## Conformance
|
|
203
|
+
|
|
204
|
+
The language is defined by the reference implementation's conformance corpus,
|
|
205
|
+
vendored under `conformance/` at a release tag of
|
|
206
|
+
[predicator-ex](https://github.com/riddler/predicator-ex). `conformance/SOURCE.json`
|
|
207
|
+
names the tag, the commit and the corpus hash; at this head they are the tag
|
|
208
|
+
`v9.5.0`, the commit `fd7fb0157e5a314086197187b4c5192bae31720e`, the
|
|
209
|
+
instruction-set version 6 and the hash
|
|
210
|
+
`sha256:ddc5cf824b06b4899e1bd4166263a1ee07016ff239216e029b9276ac17243975`.
|
|
211
|
+
|
|
212
|
+
The corpus has two surfaces: the **evaluator** runs an instruction list against
|
|
213
|
+
a context, and the **compiler** compiles a source to the instruction list the
|
|
214
|
+
corpus expects. Cases are grouped in nine cumulative tiers, and a claim of
|
|
215
|
+
tier N says every case of that surface in tiers 1 through N passes.
|
|
216
|
+
`conformance/registry.json` records the claims and one entry per passing case.
|
|
217
|
+
At this head:
|
|
218
|
+
|
|
219
|
+
- The evaluator claims tier 9, with 262 entries. The corpus holds 267 evaluator
|
|
220
|
+
cases; the other 5 carry the `retired` feature tag (they use an opcode the
|
|
221
|
+
instruction-set version 6 retired), and a run that claims version 6 does not
|
|
222
|
+
attempt them.
|
|
223
|
+
- The compiler claims tier 7, with 220 entries: every compiler case in the
|
|
224
|
+
corpus, all of them in tiers 1 through 5 and 7. A case with no source text
|
|
225
|
+
is not on the compiler surface.
|
|
226
|
+
|
|
227
|
+
No case is skipped. A case this package cannot pass is a defect to bring to
|
|
228
|
+
the reference, never an entry on a skip list. The counts are read from the
|
|
229
|
+
registry and the corpus, and you can re-derive them: the registry's `claims`
|
|
230
|
+
array and its `entries` (each `{case_id, surface, tier}`) hold the claims and
|
|
231
|
+
the passes, `conformance/manifest.json` holds the case count of each tier, and
|
|
232
|
+
`mise exec -- bundle exec rake registry` re-runs both surfaces and prints, per
|
|
233
|
+
surface, how many cases of the corpus have no entry (0 of 220 for the compiler,
|
|
234
|
+
5 of 267 for the evaluator at this head). `mise exec -- bundle exec rake` runs
|
|
235
|
+
the whole gate, which includes that check and fails if the registry and a run
|
|
236
|
+
made now disagree.
|
|
237
|
+
|
|
238
|
+
### Differences from the reference
|
|
239
|
+
|
|
240
|
+
Where the corpus is silent, the reference at the tag answers. These are the
|
|
241
|
+
places where this package's answer follows the reference's normative text
|
|
242
|
+
rather than its code, follows its code where its text is silent, or has no
|
|
243
|
+
counterpart in the reference:
|
|
244
|
+
|
|
245
|
+
- **A duration is the map of its eight units, from any origin.** The
|
|
246
|
+
reference's instruction-set document (`docs/isa.md`, section 3) makes a
|
|
247
|
+
duration a map of its eight units, `years` through `milliseconds`. A
|
|
248
|
+
duration here reads and moves as that map whether it came from a literal, an
|
|
249
|
+
opcode or the host: its unit names are its keys and any other key misses. A
|
|
250
|
+
plain map handed where a duration is read (a relative date's operand) is
|
|
251
|
+
read the same way, with an absent unit as 0.
|
|
252
|
+
- **Bracket access on a date or an instant** follows the reference's code,
|
|
253
|
+
which its instruction-set prose does not state: a `Predicator::Date` or
|
|
254
|
+
`Predicator::DateTime` takes a bracket access as a map would. A key that can
|
|
255
|
+
be a map key (a String, an Integer, a boolean, null or undefined) misses and
|
|
256
|
+
answers undefined; any other key is a `TypeMismatchError`.
|
|
257
|
+
- **A Float result that is not finite** (an overflow) is refused as an
|
|
258
|
+
`EvaluationError` with the reason `non_finite_number`. The reference has no
|
|
259
|
+
answer of its own there to follow.
|
|
260
|
+
- **A store through a date or an instant** is refused with the reason
|
|
261
|
+
`not_a_container`. **A store through a duration** writes through the plain map
|
|
262
|
+
of its eight units, the same reading the `object_set` instruction uses.
|
|
263
|
+
- **The parser bounds nesting.** The reference's parser has no bound. This
|
|
264
|
+
parser refuses a source that nests 400 levels or more (the whole expression
|
|
265
|
+
counts as the first level), with the reason `nesting_depth_exceeded`. On a
|
|
266
|
+
stack too small to reach that limit, as a Fiber's default stack is (each
|
|
267
|
+
request of an asynchronous server runs in one), the parser refuses at a
|
|
268
|
+
lower depth under the same reason rather than raising `SystemStackError`.
|
|
269
|
+
The emitter has the same refusal for a tree whose walk runs out of stack.
|
|
270
|
+
- **The host function contract is arguments only.** The reference passes a
|
|
271
|
+
function its arguments and the context, and takes an ok or error tuple. Here
|
|
272
|
+
a function takes the arguments, answers its value and refuses by raising.
|
|
273
|
+
- **The options differ.** The reference's `protected_roots` option (roots a
|
|
274
|
+
statement program may not store into) is not offered, nor are its
|
|
275
|
+
`providers`, `builtins`, `host`, `positions` and `segment_positions`
|
|
276
|
+
options; an unknown keyword raises
|
|
277
|
+
`ArgumentError`. `functions:`, `on_unbound:` and `loop_budget:` are in both.
|
|
278
|
+
`clock:` is this package's own: the reference reads the system clock, and
|
|
279
|
+
here the host passes one per call.
|
|
280
|
+
|
|
281
|
+
## Upgrading from 1.x
|
|
282
|
+
|
|
283
|
+
Version 2.0.0 is a different implementation under the same gem name. It is a
|
|
284
|
+
breaking release, not an upgrade path for 1.x code, and the 1.x API is gone.
|
|
285
|
+
To stay on 1.x, pin it (1.x releases stay installable from rubygems.org; the
|
|
286
|
+
last is 1.2.1):
|
|
50
287
|
|
|
51
288
|
```ruby
|
|
52
|
-
gem "predicator"
|
|
289
|
+
gem "predicator", "~> 1.2"
|
|
53
290
|
```
|
|
54
291
|
|
|
55
|
-
|
|
292
|
+
The 1.x source lives in the
|
|
293
|
+
[riddler/predicator](https://github.com/riddler/predicator) repository under
|
|
294
|
+
`impl/rb`. To move to 2.0:
|
|
56
295
|
|
|
57
|
-
|
|
296
|
+
| 1.x | 2.0 |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `Predicator.evaluate(source, context = {})` answered the value | `Predicator.evaluate(source, context)` answers a `Predicator::Result`; `evaluate!` answers the value and raises `Predicator::Error` |
|
|
299
|
+
| `Predicator.compile(source)` answered an instruction list | `Predicator.compile(source)` answers a `Result` holding an instruction list; `compile!` answers the list |
|
|
300
|
+
| `Predicator.evaluate_instructions(instructions, context)` | gone: `Predicator.evaluate(instructions, context)` takes an instruction list or source |
|
|
301
|
+
| `Predicator.parse(source)` answered a `Predicator::AST` node with `to_instructions`, `to_s`, `to_dot` and `Enumerable` | `Predicator.parse(source)` answers a `Result` holding the tree's root; its node classes and shape are not public, and the node methods and the visitors are gone |
|
|
302
|
+
| `Predicator::Parser`, `Predicator::Evaluator` (with `stack` and `result`), `Predicator::Context` and `Predicator::AST` | gone: they are private to the package, and a context is a plain Hash |
|
|
303
|
+
| a source that did not parse raised from the parser | a source that does not compile answers a failed `Result` with a `ParseError` |
|
|
304
|
+
| a context given as a Hash with String or Symbol keys, or as keywords (`evaluate "score > 1", score: 2`) | a Hash passed as the second argument, keyed by Strings; a Symbol key at the root fails the run with `unsupported_host_value`, and keywords are the run's options |
|
|
305
|
+
| a variable the context did not bind read as `nil`, so a comparison with it answered `false` | the run's answer is undefined, and fails with an `UndefinedVariableError` when undefined is the answer; `on_unbound: :error` fails at the load |
|
|
306
|
+
| 1.x instruction lists | a list that uses an opcode 2.0 does not have (such as `jfalse`, `to_int` or `array`) fails with `unknown_instruction`; a list of only `lit`, `load`, `not` and `compare` runs, under 2.0's rules for the comparison and the unbound variable above; recompile the source with 2.0 either way |
|
|
307
|
+
| a custom function, a clock or a limit: none | `functions:`, `clock:`, `on_unbound:` and `loop_budget:` keywords |
|
|
308
|
+
| statement programs: none | new in 2.0: `compile_program`, `execute` and `execute_value` |
|
|
58
309
|
|
|
59
|
-
|
|
310
|
+
Store the condition's source text, not its 1.x instruction list, and compile
|
|
311
|
+
it with 2.0. The instruction list 2.0 emits is the reference's instruction set
|
|
312
|
+
version 6, which is the same plain data in every implementation of it.
|
|
60
313
|
|
|
61
|
-
|
|
314
|
+
## Documentation
|
|
62
315
|
|
|
63
|
-
|
|
316
|
+
- Look up
|
|
317
|
+
- Every public method and class carries a YARD doc comment in `lib/`:
|
|
318
|
+
the `Predicator` module's entry points, the values, the error values and
|
|
319
|
+
`Predicator::Result`, and `Predicator::Tagged`.
|
|
320
|
+
- [The instruction-set reference](https://github.com/riddler/predicator-ex/blob/v9.5.0/docs/isa.md):
|
|
321
|
+
the instructions, the value space and the refusals, which this package
|
|
322
|
+
implements, read at the corpus tag.
|
|
323
|
+
- [The changelog](CHANGELOG.md): what changed in each version.
|
|
324
|
+
- [The conformance corpus](conformance/README.md): what is vendored, from
|
|
325
|
+
where, and what is this repository's own.
|
|
326
|
+
- Understand
|
|
327
|
+
- [Why a predicate language of its own](https://github.com/riddler/predicator-ex/blob/v9.5.0/docs/explanation/why-a-predicate-language-of-its-own.md):
|
|
328
|
+
why not `eval`, and why not a general-purpose language.
|
|
329
|
+
- [Embedding Predicator](https://github.com/riddler/predicator-ex/blob/v9.5.0/docs/guides/embedding.md)
|
|
330
|
+
and [custom functions](https://github.com/riddler/predicator-ex/blob/v9.5.0/docs/guides/custom-functions.md),
|
|
331
|
+
in the reference's terms (read the function contract above for this
|
|
332
|
+
package's).
|
|
64
333
|
|
|
65
|
-
|
|
66
|
-
Then, run `rake test` (or just `rake`) to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
334
|
+
## Development
|
|
67
335
|
|
|
68
|
-
|
|
336
|
+
```bash
|
|
337
|
+
mise install # the Ruby that mise.toml pins
|
|
338
|
+
mise exec -- bundle install # the dev tools
|
|
339
|
+
mise exec -- bundle exec rake # the gate: rubocop, the corpus and registry checks, the tests
|
|
340
|
+
```
|
|
69
341
|
|
|
70
342
|
## Contributing
|
|
71
343
|
|
|
72
|
-
Bug reports and pull requests are welcome on GitHub at
|
|
73
|
-
|
|
74
|
-
|
|
344
|
+
Bug reports and pull requests are welcome on GitHub at
|
|
345
|
+
https://github.com/riddler/predicator-rb.
|
|
75
346
|
|
|
76
347
|
## License
|
|
77
348
|
|
|
78
|
-
|
|
349
|
+
MIT, see `LICENSE`.
|
|
350
|
+
|
|
351
|
+
<!-- BADGES -->
|
|
352
|
+
[badge_license_url]: https://img.shields.io/badge/license-MIT-brightgreen.svg?cacheSeconds=3600?style=flat-square
|
|
353
|
+
[badge_ci_url]: https://github.com/riddler/predicator-rb/actions/workflows/ci.yml/badge.svg
|
|
354
|
+
[badge_version_url]: https://badge.fury.io/rb/predicator.svg
|