predicator 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +322 -46
  5. data/lib/predicator/ast.rb +74 -187
  6. data/lib/predicator/boundary.rb +237 -0
  7. data/lib/predicator/civil.rb +112 -0
  8. data/lib/predicator/duration_text.rb +92 -0
  9. data/lib/predicator/emitter/chains.rb +62 -0
  10. data/lib/predicator/emitter/statements.rb +86 -0
  11. data/lib/predicator/emitter.rb +187 -0
  12. data/lib/predicator/errors.rb +140 -0
  13. data/lib/predicator/evaluator/access.rb +108 -0
  14. data/lib/predicator/evaluator/arithmetic.rb +132 -0
  15. data/lib/predicator/evaluator/calls.rb +129 -0
  16. data/lib/predicator/evaluator/casts.rb +158 -0
  17. data/lib/predicator/evaluator/chronology.rb +88 -0
  18. data/lib/predicator/evaluator/compare.rb +158 -0
  19. data/lib/predicator/evaluator/json_functions.rb +167 -0
  20. data/lib/predicator/evaluator/json_scan.rb +303 -0
  21. data/lib/predicator/evaluator/library.rb +127 -0
  22. data/lib/predicator/evaluator/math_functions.rb +72 -0
  23. data/lib/predicator/evaluator/order.rb +120 -0
  24. data/lib/predicator/evaluator/rich_types.rb +146 -0
  25. data/lib/predicator/evaluator/statements.rb +151 -0
  26. data/lib/predicator/evaluator.rb +391 -117
  27. data/lib/predicator/float_text.rb +94 -0
  28. data/lib/predicator/iso_text.rb +99 -0
  29. data/lib/predicator/lexer/literals.rb +115 -0
  30. data/lib/predicator/lexer/numbers.rb +91 -0
  31. data/lib/predicator/lexer.rb +194 -0
  32. data/lib/predicator/parser/durations.rb +108 -0
  33. data/lib/predicator/parser/operators.rb +86 -0
  34. data/lib/predicator/parser/postfix.rb +57 -0
  35. data/lib/predicator/parser/primaries.rb +122 -0
  36. data/lib/predicator/parser/statements.rb +114 -0
  37. data/lib/predicator/parser/token_text.rb +52 -0
  38. data/lib/predicator/parser.rb +217 -438
  39. data/lib/predicator/result.rb +46 -0
  40. data/lib/predicator/tagged/reader.rb +139 -0
  41. data/lib/predicator/tagged/writer.rb +106 -0
  42. data/lib/predicator/tagged.rb +134 -0
  43. data/lib/predicator/values.rb +147 -0
  44. data/lib/predicator/version.rb +4 -1
  45. data/lib/predicator.rb +384 -14
  46. metadata +53 -147
  47. data/.gitignore +0 -11
  48. data/.rubocop.yml +0 -46
  49. data/.travis.yml +0 -5
  50. data/CODE_OF_CONDUCT.md +0 -49
  51. data/Gemfile +0 -4
  52. data/HISTORY.md +0 -51
  53. data/Rakefile +0 -24
  54. data/lib/predicator/context.rb +0 -18
  55. data/lib/predicator/lexer.rex +0 -69
  56. data/lib/predicator/lexer.rex.rb +0 -187
  57. data/lib/predicator/parser.y +0 -120
  58. data/lib/predicator/visitors/dot.rb +0 -113
  59. data/lib/predicator/visitors/each.rb +0 -16
  60. data/lib/predicator/visitors/instructions.rb +0 -183
  61. data/lib/predicator/visitors/string.rb +0 -76
  62. data/lib/predicator/visitors/visitor.rb +0 -100
  63. data/lib/predicator/visitors.rb +0 -5
  64. data/predicator.gemspec +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3d6163b7499f9afc436a5b7f11ac3b38e128f6e2f8eb6a37fc6fe6521080e1d
4
- data.tar.gz: b2647db6156463d1d06317a3cd82305cd2cf29a464484017e920e653d9db7dcc
3
+ metadata.gz: b2177b079f5600e5c04e1f1f15ba535838c9acfe7244c0041408bd874f6f02e1
4
+ data.tar.gz: 823005978281166065bb392f6cee4665bfd4196c181272392b21feac89fd2974
5
5
  SHA512:
6
- metadata.gz: e4233d02d68f77beec56049b8ab6bb13c93b114b003d57a903b945f29f9caace9dd9e835b0e9bf7baf9a60dd581b8aceadc2b3f3151e49539d1fbce6ebc8c7d9
7
- data.tar.gz: a93c002f76da05a95d1d426a8a94db8107b59627217ccf2a4d559a381c941b6d7ea7c1a1ede8e5063aa3c8d2d52b4ce3d8397e77e014dc4b7aeb4626a8f6905f
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.
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2016 JohnnyT
3
+ Copyright (c) 2016-2026 JohnnyT
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,78 +1,354 @@
1
- [![Gem Version](https://badge.fury.io/rb/predicator.svg)](http://badge.fury.io/rb/predicator)
2
- [![Build Status](https://travis-ci.org/predicator/predicator.svg?branch=master)](https://travis-ci.org/predicator/predicator)
3
- [![Coverage Status](https://coveralls.io/repos/github/predicator/predicator/badge.svg?branch=master)](https://coveralls.io/github/predicator/predicator?branch=master)
4
-
5
1
  # Predicator
6
2
 
7
- Predicator is a safe (does not eval code), admin or business user facing predicate engine. It turns a string predicate like `"score > 600 or (score > 580 and monthly_income > 9000)"` along with a supplied context into a `true` or `false`. This predicate can be stored as an attribute of a model (ex: an Offer model could store a predicate indicating if it is available to a customer).
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
- ## Usage
28
+ ## Install
10
29
 
11
- Simple usage:
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
- Predicator.evaluate "score > 600 or (score > 580 and income > 9000)", score: 590 # false
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
- Predicator.evaluate "score > 600 or (score > 580 and income > 9000)", score: 590, income: 9500 # true
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
- Example usage with a model:
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
- class Customer
25
- ...
26
- def to_hash
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
- ## Installation
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
- Add this line to your application's Gemfile:
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
- And then execute:
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
- $ bundle
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
- Or install it yourself as:
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
- $ gem install predicator
314
+ ## Documentation
62
315
 
63
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies.
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
- To install this gem onto your local machine, run `bundle exec rake install`.
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 https://github.com/predicator/predicator.
73
- This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
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
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
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