dentaku 3.5.8 → 4.0.1
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/.github/workflows/rspec.yml +2 -4
- data/.github/workflows/rubocop.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +108 -1
- data/Gemfile +1 -1
- data/README.md +123 -0
- data/dentaku.gemspec +15 -8
- data/lib/dentaku/ast/access.rb +18 -1
- data/lib/dentaku/ast/arithmetic.rb +22 -9
- data/lib/dentaku/ast/array.rb +6 -0
- data/lib/dentaku/ast/bitwise.rb +6 -4
- data/lib/dentaku/ast/case/case_conditional.rb +6 -0
- data/lib/dentaku/ast/case/case_else.rb +6 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
- data/lib/dentaku/ast/case/case_then.rb +6 -0
- data/lib/dentaku/ast/case/case_when.rb +7 -0
- data/lib/dentaku/ast/case.rb +30 -3
- data/lib/dentaku/ast/combinators.rb +52 -5
- data/lib/dentaku/ast/comparators.rb +2 -3
- data/lib/dentaku/ast/function.rb +15 -2
- data/lib/dentaku/ast/function_registry.rb +28 -2
- data/lib/dentaku/ast/functions/and.rb +4 -4
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/duration.rb +1 -1
- data/lib/dentaku/ast/functions/if.rb +9 -1
- data/lib/dentaku/ast/functions/intercept.rb +2 -2
- data/lib/dentaku/ast/functions/mul.rb +2 -2
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +1 -1
- data/lib/dentaku/ast/functions/string_functions.rb +1 -1
- data/lib/dentaku/ast/functions/sum.rb +2 -2
- data/lib/dentaku/ast/functions/xor.rb +4 -4
- data/lib/dentaku/ast/grouping.rb +6 -0
- data/lib/dentaku/ast/negation.rb +8 -1
- data/lib/dentaku/ast/node.rb +25 -0
- data/lib/dentaku/ast/operation.rb +7 -0
- data/lib/dentaku/bulk_expression_solver.rb +9 -10
- data/lib/dentaku/calculator.rb +83 -34
- data/lib/dentaku/date_arithmetic.rb +2 -2
- data/lib/dentaku/exceptions.rb +105 -13
- data/lib/dentaku/numeric_parser.rb +1 -1
- data/lib/dentaku/parser.rb +24 -48
- data/lib/dentaku/token_matcher.rb +31 -37
- data/lib/dentaku/token_matchers.rb +3 -3
- data/lib/dentaku/tokenizer.rb +1 -15
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +93 -0
- data/spec/ast/case_spec.rb +7 -0
- data/spec/bulk_expression_solver_spec.rb +13 -3
- data/spec/calculator_spec.rb +146 -0
- data/spec/exceptions_spec.rb +87 -0
- data/spec/external_function_spec.rb +33 -0
- data/spec/gemspec_spec.rb +10 -0
- data/spec/identifiers_spec.rb +66 -0
- data/spec/parser_spec.rb +35 -0
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/volatile_functions_spec.rb +121 -0
- metadata +21 -63
- data/.travis.yml +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c52e5ed43e8cc6d9ac6f3cf81ae522e408860ab24bf59109bdd97301e0d6e12
|
|
4
|
+
data.tar.gz: fa376e35d073d5c0ebce858f96c4c1322ed1bba2a202dc6e41d265c1e5fa24f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 040e836d96fad9833095285a2d3a9b6b7e7a3c4bffcd84cac9603bc16128308acb719103eb96693cc04910c028265231f72f982658dd34523d3685ac4dea8e7d
|
|
7
|
+
data.tar.gz: 66aa9ee21ce77b2d9dd25d44b3a7f1db7212e7776614839d4c92e403ed0d4e5561fee07e28a50eb81c1a886bf889c1ae285c4a89aa78a59910f5636de66159d4
|
data/.github/workflows/rspec.yml
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,113 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [v4.0.1] 2026-08-02
|
|
4
|
+
- parsing no longer executes user functions. The three parse-time operand
|
|
5
|
+
validators (`Arithmetic`, `Negation`, and the logical combinators) asked
|
|
6
|
+
each operand for its dependencies without static mode, which evaluates
|
|
7
|
+
short-circuit guards -- so an `IF` or `CASE` used as an operand ran its
|
|
8
|
+
predicate while the parser was still building the node. `Calculator#ast`
|
|
9
|
+
and `Calculator#identifiers` could therefore raise from inside a
|
|
10
|
+
user-registered function, contrary to the documented contract that
|
|
11
|
+
`identifiers` never evaluates guards or functions (#197). Regression
|
|
12
|
+
introduced in 3.5.6 (ecd46bf); evaluation and short-circuiting during
|
|
13
|
+
`evaluate`/`dependencies` are unchanged.
|
|
14
|
+
- functions registered under a name that another calculator already registered
|
|
15
|
+
no longer render as `#<Class:0x...>` in error messages; every generated
|
|
16
|
+
function class now has a stable `to_s` of
|
|
17
|
+
`Dentaku::AST::Function::<Name>` (#264)
|
|
18
|
+
- arithmetic operations that reject an otherwise well-typed operand (notably
|
|
19
|
+
`^` with an oversized exponent) now raise `Dentaku::ArgumentError` instead of
|
|
20
|
+
leaking a raw Ruby `ArgumentError`, so the non-bang `Calculator#evaluate`
|
|
21
|
+
returns `nil` as documented rather than raising (#332)
|
|
22
|
+
- `Calculator#store` restores only the keys it set instead of snapshotting the
|
|
23
|
+
whole memory hash, so evaluation cost no longer scales with the number of
|
|
24
|
+
stored variables (#336). Measured on 20k evaluations of a two-variable
|
|
25
|
+
formula: 34.5 -> 18.6 us/eval with 20,000 variables in memory, and flat
|
|
26
|
+
across memory sizes. Block-scoping semantics are unchanged; when the
|
|
27
|
+
identifier cache is enabled the previous whole-hash snapshot is still used,
|
|
28
|
+
because evaluation writes into memory in that mode.
|
|
29
|
+
- document what the `add_function` return type argument actually does: it is
|
|
30
|
+
consulted at parse time to decide whether a call is a valid arithmetic
|
|
31
|
+
operand, is never coerced or checked at runtime, and has no observable
|
|
32
|
+
effect outside arithmetic (#303)
|
|
33
|
+
- the test suite no longer depends on example ordering: the module-level
|
|
34
|
+
caching opt-ins are reset before each example, since they have no public
|
|
35
|
+
disable and leaked between examples
|
|
36
|
+
|
|
37
|
+
## [v4.0.0] 2026-08-01
|
|
38
|
+
BREAKING CHANGES
|
|
39
|
+
- require Ruby 3.2 or newer
|
|
40
|
+
- `Calculator.new` takes keyword arguments; the AST cache must now be passed
|
|
41
|
+
explicitly via `ast_cache:` instead of as leftover options, unknown options
|
|
42
|
+
raise `ArgumentError`, and the passed values are no longer mutated
|
|
43
|
+
- `Dentaku::Error` is now a module included by all Dentaku exceptions, so
|
|
44
|
+
`rescue Dentaku::Error` also catches `Dentaku::ArgumentError` and
|
|
45
|
+
`Dentaku::ZeroDivisionError`; exceptions that previously subclassed
|
|
46
|
+
`Dentaku::Error` now subclass `Dentaku::BaseError`
|
|
47
|
+
- unify exception metadata naming: the "required vs. received" pair is now
|
|
48
|
+
`expected:` / `actual:` everywhere (previously a mix of `expect:`/`actual:`,
|
|
49
|
+
`for:`/`value:`, `at_least:`/`given:`, and `exact:`/`given:`); arity
|
|
50
|
+
expectations are an Integer or Range (`expected: 1..` means "at least one")
|
|
51
|
+
and `actual:` always carries the offending value itself, not its class
|
|
52
|
+
- `meta[:operation]` is now always the AST class and `meta[:operator]` the
|
|
53
|
+
method symbol (previously `ParseError` used `:operator` for the class);
|
|
54
|
+
`NodeError#child` is renamed to `#operand` (it holds the failing operand's
|
|
55
|
+
position, `:left` / `:right` / `:node`) and `NodeError#expect` to `#expected`;
|
|
56
|
+
`function_name:` metadata carries bare names (`'AND'`, not `'AND()'`)
|
|
57
|
+
- `Dentaku::Error#recipient_variable` is renamed to `#assigned_to`
|
|
58
|
+
- bitwise operations on non-integer operands raise `:incompatible_type`
|
|
59
|
+
(previously `:invalid_operator`) with a descriptive message instead of a
|
|
60
|
+
bare "Dentaku::ArgumentError"
|
|
61
|
+
- indexing into a non-indexable value (e.g. `a[0]` where `a` is a number,
|
|
62
|
+
`NULL`, or a boolean) now raises `Dentaku::ArgumentError` instead of
|
|
63
|
+
silently returning a bit-reference result or leaking a raw Ruby
|
|
64
|
+
`NoMethodError`; an incompatible index type (e.g. a string index into an
|
|
65
|
+
array) raises `Dentaku::ArgumentError` instead of a raw `TypeError`
|
|
66
|
+
- `AND` / `OR` short-circuit: an unbound variable no longer raises when the
|
|
67
|
+
bound operand already decides the result (#234); formulas are documented
|
|
68
|
+
as pure, and misspelled identifiers on never-taken branches are no longer
|
|
69
|
+
reported at evaluation time (use `dependencies` for static validation)
|
|
70
|
+
- `TokenMatcher` no longer builds function matchers via `method_missing`; use
|
|
71
|
+
`TokenMatcher.function(name)` (the unused `math_neg_pow` / `math_neg_mul`
|
|
72
|
+
matcher symbols were removed)
|
|
73
|
+
|
|
74
|
+
OTHER CHANGES
|
|
75
|
+
- add `volatile:` option to `add_function` / `add_functions` / function
|
|
76
|
+
registration for functions that read external state or return different
|
|
77
|
+
values across calls; volatile functions are never executed during
|
|
78
|
+
dependency analysis, so a conditional guarded by one reports all branches
|
|
79
|
+
as dependencies (and `evaluate!` requires variables from every branch),
|
|
80
|
+
and the guard is evaluated exactly once per `evaluate!` instead of twice
|
|
81
|
+
(#339, thanks @d-krushinsky)
|
|
82
|
+
- add `Calculator#identifiers`, a purely syntactic listing of every
|
|
83
|
+
identifier a formula could reference regardless of branching -- nothing
|
|
84
|
+
is evaluated and stored variables are not subtracted (#197, #339)
|
|
85
|
+
- cache flags can be set per calculator (`cache_ast:`,
|
|
86
|
+
`cache_dependency_order:`), defaulting to the module-level settings
|
|
87
|
+
- module-level `Dentaku.aliases` is resolved lazily, so aliases set after a
|
|
88
|
+
calculator was created (including the implicit top-level calculator) apply
|
|
89
|
+
- `CASE` expressions only report dependencies for the branch that would be
|
|
90
|
+
taken when the switch value is resolvable
|
|
91
|
+
- operand type-mismatch parse errors now read as natural English: the parser
|
|
92
|
+
preserves the node-level message ("Dentaku::AST::Addition requires operands
|
|
93
|
+
that are numeric or compatible types, not string") instead of rebuilding a
|
|
94
|
+
misleading one ("requires incompatible operands, but got string"); the
|
|
95
|
+
metadata-derived fallback message says "compatible" rather than
|
|
96
|
+
"incompatible" (idea from #341, thanks @moskvin)
|
|
97
|
+
- `Dentaku::ArgumentError`, `ParseError`, and `TokenizerError` build their own
|
|
98
|
+
default messages from `reason` and `meta` (existing message text is
|
|
99
|
+
unchanged); `Parser#fail!` and `Tokenizer#fail!` reduce to one-liners
|
|
100
|
+
- fix parsing of `CASE` statements with an unparenthesized operation as the
|
|
101
|
+
switch expression (`CASE a % 5 WHEN ...`), which also makes `PrintVisitor`
|
|
102
|
+
output for such statements re-parseable
|
|
103
|
+
- fix `recipient_variable` being nil inside `solve` blocks, a regression
|
|
104
|
+
introduced in 3.5.4 (#333)
|
|
105
|
+
- fix crashes on two error paths: the parser's unbalanced-parenthesis report
|
|
106
|
+
raised a Ruby `ArgumentError` instead of a `ParseError`, and the tokenizer's
|
|
107
|
+
zero-width-match report raised a `KeyError` instead of a `TokenizerError`
|
|
108
|
+
- document Ruby compatibility policy
|
|
109
|
+
- modernize low-risk Ruby syntax
|
|
110
|
+
- remove Travis CI configuration
|
|
4
111
|
|
|
5
112
|
## [v3.5.8] 2026-08-01
|
|
6
113
|
- unify numeric matching and parsing
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -13,6 +13,16 @@ language that allows run-time binding of values to variables referenced in the
|
|
|
13
13
|
formulas. It is intended to safely evaluate untrusted expressions without
|
|
14
14
|
opening security holes.
|
|
15
15
|
|
|
16
|
+
COMPATIBILITY
|
|
17
|
+
-------------
|
|
18
|
+
|
|
19
|
+
Dentaku supports Ruby 3.2 and newer, tracking Ruby's own maintenance policy:
|
|
20
|
+
when a Ruby version reaches end-of-life, it may be dropped from the supported
|
|
21
|
+
compatibility contract in the next major or minor release. Older Rubies may
|
|
22
|
+
continue to work, but are not tested.
|
|
23
|
+
|
|
24
|
+
New code should target the lowest supported Ruby version, currently Ruby 3.2.
|
|
25
|
+
|
|
16
26
|
EXAMPLE
|
|
17
27
|
-------
|
|
18
28
|
|
|
@@ -124,6 +134,13 @@ phases, so if performance is a concern, you can enable AST caching:
|
|
|
124
134
|
Dentaku.enable_ast_cache!
|
|
125
135
|
```
|
|
126
136
|
|
|
137
|
+
Caching can also be controlled per calculator, overriding the module-level
|
|
138
|
+
setting:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
calculator = Dentaku::Calculator.new(cache_ast: true)
|
|
142
|
+
```
|
|
143
|
+
|
|
127
144
|
After this, Dentaku will cache the AST of each formula that it evaluates, so
|
|
128
145
|
subsequent evaluations (even with different values for variables) will be much
|
|
129
146
|
faster -- closer to 4x native Ruby speed. As usual, these benchmarks should be
|
|
@@ -131,6 +148,57 @@ considered rough estimates, and you should measure with representative formulas
|
|
|
131
148
|
from your application. Also, if new formulas are constantly introduced to your
|
|
132
149
|
application, AST caching will consume more memory with each new formula.
|
|
133
150
|
|
|
151
|
+
DEPENDENCY ANALYSIS AND SHORT-CIRCUITING
|
|
152
|
+
----------------------------------------
|
|
153
|
+
|
|
154
|
+
Dentaku treats formulas as pure: a subexpression may be evaluated zero, one,
|
|
155
|
+
or more than one time (for example, a guard position may be evaluated once
|
|
156
|
+
during dependency analysis and again during evaluation), so custom functions
|
|
157
|
+
should not rely on side effects or call counts. Functions that fall outside
|
|
158
|
+
this contract -- reading external state, performing I/O, or returning
|
|
159
|
+
different values across calls -- must be registered with `volatile: true`
|
|
160
|
+
(see EXTERNAL FUNCTIONS below). Volatile functions are never invoked during
|
|
161
|
+
dependency analysis; in guard positions this means no branch can be pruned,
|
|
162
|
+
so all branches count as dependencies and `evaluate!` requires variables
|
|
163
|
+
from every branch to be bound.
|
|
164
|
+
|
|
165
|
+
Logical constructs short-circuit. `IF` and `CASE` only evaluate the branch
|
|
166
|
+
that is taken, and `AND` / `OR` resolve as soon as one operand decides the
|
|
167
|
+
result. An unbound variable raises `Dentaku::UnboundVariableError` only when
|
|
168
|
+
its value is actually needed:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
calculator.evaluate!('a OR b', a: true) #=> true
|
|
172
|
+
calculator.evaluate!('a OR b', a: false) #=> raises Dentaku::UnboundVariableError
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Two methods answer two different questions about a formula's inputs:
|
|
176
|
+
|
|
177
|
+
`calculator.dependencies(expression, context)` reports the identifiers a
|
|
178
|
+
formula still needs, given what is already known. It is resolution-aware:
|
|
179
|
+
guard positions (`IF` predicates, `CASE` switches, `AND` / `OR` operands)
|
|
180
|
+
whose own dependencies are already satisfied are evaluated -- executing any
|
|
181
|
+
(non-volatile) functions they contain -- in order to prune branches that
|
|
182
|
+
cannot be taken.
|
|
183
|
+
|
|
184
|
+
`calculator.identifiers(expression)` reports every identifier the formula
|
|
185
|
+
could ever reference, regardless of branching. It is purely syntactic:
|
|
186
|
+
nothing is evaluated, no functions run (volatile or not), and stored
|
|
187
|
+
variables are not subtracted. Use it to statically list a formula's
|
|
188
|
+
possible inputs, e.g. to tell users what values they may need to supply.
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
calculator.dependencies('IF(x > 5, y, z)', x: 7) #=> ["y"]
|
|
192
|
+
calculator.identifiers('IF(x > 5, y, z)') #=> ["x", "y", "z"]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Because unused operands are never checked at evaluation time, a misspelled
|
|
196
|
+
identifier on a branch that is not taken will not be reported; use
|
|
197
|
+
`identifiers` for static validation.
|
|
198
|
+
|
|
199
|
+
Context keys prefixed with a double underscore (e.g. `__evaluation_mode`)
|
|
200
|
+
are reserved for internal use.
|
|
201
|
+
|
|
134
202
|
BUILT-IN OPERATORS AND FUNCTIONS
|
|
135
203
|
---------------------------------
|
|
136
204
|
|
|
@@ -279,6 +347,61 @@ function)
|
|
|
279
347
|
Functions can be added individually using Calculator#add_function, or en masse
|
|
280
348
|
using Calculator#add_functions.
|
|
281
349
|
|
|
350
|
+
### The return type argument
|
|
351
|
+
|
|
352
|
+
The second argument is the function's *declared* return type. It is used at
|
|
353
|
+
parse time to decide whether a call to the function is a valid operand of an
|
|
354
|
+
arithmetic operator -- only `:numeric`, `:integer`, `:array`, and `nil` are
|
|
355
|
+
accepted there. Declaring anything else means the function cannot be used in
|
|
356
|
+
arithmetic:
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
> c = Dentaku::Calculator.new
|
|
360
|
+
> c.add_function(:label, :string, ->(x) { "item#{x}" })
|
|
361
|
+
> c.evaluate!('label(1)')
|
|
362
|
+
#=> "item1"
|
|
363
|
+
> c.evaluate!("label(1) = 'item1'") # comparison: type is not consulted
|
|
364
|
+
#=> true
|
|
365
|
+
> c.evaluate!('label(1) + 1') # arithmetic: rejected at parse time
|
|
366
|
+
#=> Dentaku::ParseError: Dentaku::AST::Addition requires operands that are
|
|
367
|
+
# numeric or compatible types, not string
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The type is a declaration, not a guarantee: Dentaku does not coerce the return
|
|
371
|
+
value and does not check it at runtime. Declaring `:numeric` for a function
|
|
372
|
+
that actually returns a String defers the failure to whatever consumes it:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
> c.add_function(:mislabeled, :numeric, ->(x) { "item#{x}" })
|
|
376
|
+
> c.evaluate!('mislabeled(1) + 1')
|
|
377
|
+
#=> Dentaku::ArgumentError: String input 'item1' is not coercible to numeric
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
So the type only has an observable effect when the result feeds an arithmetic
|
|
381
|
+
operator. A function used standalone, in comparisons, or as a function
|
|
382
|
+
argument behaves the same whatever type is declared -- which is why an
|
|
383
|
+
incorrect type can go unnoticed for a long time. Declare the type your lambda
|
|
384
|
+
actually returns.
|
|
385
|
+
|
|
386
|
+
Dentaku assumes registered functions are pure (see DEPENDENCY ANALYSIS AND
|
|
387
|
+
SHORT-CIRCUITING above). A function that reads external state, performs
|
|
388
|
+
I/O, or returns different values across calls must be declared volatile so
|
|
389
|
+
that dependency analysis never executes it:
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
> c = Dentaku::Calculator.new
|
|
393
|
+
> c.add_function(:user_level, :numeric, -> { Current.user.level }, volatile: true)
|
|
394
|
+
> c.identifiers('IF(user_level() > 50, high_rate, low_rate)')
|
|
395
|
+
#=> ["high_rate", "low_rate"]
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
A volatile function in a guard position prevents branch pruning: both
|
|
399
|
+
branches count as dependencies, and `evaluate!` requires variables from
|
|
400
|
+
every branch (Dentaku cannot know in advance which branch will be taken).
|
|
401
|
+
The predicate itself is then evaluated exactly once per `evaluate!` call,
|
|
402
|
+
at evaluation time. In `add_functions`, pass volatility as a fifth tuple
|
|
403
|
+
element: `[:user_level, :numeric, -> { ... }, nil, true]`.
|
|
404
|
+
|
|
282
405
|
FUNCTION ALIASES
|
|
283
406
|
----------------
|
|
284
407
|
|
data/dentaku.gemspec
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
-
require "dentaku/version"
|
|
1
|
+
require_relative "lib/dentaku/version"
|
|
4
2
|
|
|
5
3
|
Gem::Specification.new do |s|
|
|
6
4
|
s.name = "dentaku"
|
|
7
5
|
s.version = Dentaku::VERSION
|
|
8
6
|
s.authors = ["Solomon White"]
|
|
9
7
|
s.email = ["rubysolo@gmail.com"]
|
|
10
|
-
s.homepage = "
|
|
8
|
+
s.homepage = "https://github.com/rubysolo/dentaku"
|
|
11
9
|
s.licenses = %w(MIT)
|
|
12
10
|
s.summary = 'A formula language parser and evaluator'
|
|
13
11
|
s.description = <<-DESC
|
|
14
12
|
Dentaku is a parser and evaluator for mathematical formulas
|
|
15
13
|
DESC
|
|
16
14
|
|
|
17
|
-
s.
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
s.metadata = {
|
|
16
|
+
"homepage_uri" => s.homepage,
|
|
17
|
+
"source_code_uri" => s.homepage,
|
|
18
|
+
"changelog_uri" => "#{s.homepage}/blob/main/CHANGELOG.md",
|
|
19
|
+
"bug_tracker_uri" => "#{s.homepage}/issues",
|
|
20
|
+
"rubygems_mfa_required" => "true",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
s.add_dependency('bigdecimal', '>= 3.1')
|
|
24
|
+
s.add_dependency('concurrent-ruby', '~> 1.1')
|
|
25
|
+
s.add_dependency('tsort', '>= 0.1.1')
|
|
26
|
+
|
|
27
|
+
s.required_ruby_version = ">= 3.2"
|
|
20
28
|
|
|
21
29
|
s.files = `git ls-files`.split("\n")
|
|
22
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
23
30
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
|
24
31
|
s.require_paths = ["lib"]
|
|
25
32
|
end
|
data/lib/dentaku/ast/access.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require_relative "../exceptions"
|
|
1
2
|
require_relative "./node"
|
|
2
3
|
|
|
3
4
|
module Dentaku
|
|
@@ -25,7 +26,17 @@ module Dentaku
|
|
|
25
26
|
def value(context = {})
|
|
26
27
|
structure = @structure.value(context)
|
|
27
28
|
index = @index.value(context)
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
unless structure.respond_to?(:[]) && !structure.is_a?(::Numeric)
|
|
31
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: structure),
|
|
32
|
+
"#{self.class} requires an indexable structure, but got #{structure.class}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
structure[index]
|
|
37
|
+
rescue ::TypeError => e
|
|
38
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: index), e.message
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
def dependencies(context = {})
|
|
@@ -39,6 +50,12 @@ module Dentaku
|
|
|
39
50
|
def accept(visitor)
|
|
40
51
|
visitor.visit_access(self)
|
|
41
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def compute_pure?
|
|
57
|
+
@structure.pure? && @index.pure?
|
|
58
|
+
end
|
|
42
59
|
end
|
|
43
60
|
end
|
|
44
61
|
end
|
|
@@ -38,10 +38,21 @@ module Dentaku
|
|
|
38
38
|
l = cast(left_value)
|
|
39
39
|
r = cast(right_value)
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
begin
|
|
42
|
+
l.public_send(operator, r)
|
|
43
|
+
rescue ::TypeError => e
|
|
44
|
+
# Right cannot be converted to a suitable type for left. e.g. [] + 1
|
|
45
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: r, expected: l.class), e.message
|
|
46
|
+
rescue Dentaku::Error
|
|
47
|
+
# already one of ours (e.g. from arithmetic on a custom class)
|
|
48
|
+
raise
|
|
49
|
+
rescue ::ArgumentError, ::RangeError, ::FloatDomainError => e
|
|
50
|
+
# the operation itself rejected an otherwise well-typed operand,
|
|
51
|
+
# e.g. BigDecimal#** with an oversized exponent. Without this, a raw
|
|
52
|
+
# Ruby exception escapes the non-bang Calculator#evaluate, which
|
|
53
|
+
# promises to return nil rather than raise.
|
|
54
|
+
raise Dentaku::ArgumentError.for(:invalid_value, actual: r), e.message
|
|
55
|
+
end
|
|
45
56
|
end
|
|
46
57
|
|
|
47
58
|
def cast(val)
|
|
@@ -66,8 +77,11 @@ module Dentaku
|
|
|
66
77
|
def valid_node?(node)
|
|
67
78
|
return false unless node
|
|
68
79
|
|
|
69
|
-
# Allow nodes with dependencies (identifiers that will be resolved later)
|
|
70
|
-
|
|
80
|
+
# Allow nodes with dependencies (identifiers that will be resolved later).
|
|
81
|
+
# Ask statically: this runs while the parser is building the node, and
|
|
82
|
+
# a non-static dependency check evaluates short-circuit guards, which
|
|
83
|
+
# would execute user functions at parse time (#197).
|
|
84
|
+
return true if node.dependencies(Node::STATIC_CONTEXT).any?
|
|
71
85
|
|
|
72
86
|
# Allow compatible types
|
|
73
87
|
return true if [:numeric, :integer, :array].include?(node.type)
|
|
@@ -97,14 +111,13 @@ module Dentaku
|
|
|
97
111
|
|
|
98
112
|
def validate_operation(val)
|
|
99
113
|
unless val.respond_to?(operator)
|
|
100
|
-
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator)
|
|
101
|
-
"#{ self.class } requires operands that respond to #{operator}"
|
|
114
|
+
raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator)
|
|
102
115
|
end
|
|
103
116
|
end
|
|
104
117
|
|
|
105
118
|
def validate_format(string)
|
|
106
119
|
unless Dentaku::NumericParser.match(string)
|
|
107
|
-
raise Dentaku::ArgumentError.for(:invalid_value,
|
|
120
|
+
raise Dentaku::ArgumentError.for(:invalid_value, actual: string, expected: BigDecimal),
|
|
108
121
|
"String input '#{string}' is not coercible to numeric"
|
|
109
122
|
end
|
|
110
123
|
end
|
data/lib/dentaku/ast/array.rb
CHANGED
data/lib/dentaku/ast/bitwise.rb
CHANGED
|
@@ -8,10 +8,12 @@ module Dentaku
|
|
|
8
8
|
right_value = right.value(context)
|
|
9
9
|
|
|
10
10
|
left_value.public_send(operator, right_value)
|
|
11
|
-
rescue NoMethodError
|
|
12
|
-
raise Dentaku::ArgumentError.for(:
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
rescue NoMethodError
|
|
12
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: left_value, expected: Integer),
|
|
13
|
+
"#{self.class} requires integer operands, but got #{left_value.class}"
|
|
14
|
+
rescue TypeError
|
|
15
|
+
raise Dentaku::ArgumentError.for(:incompatible_type, actual: right_value, expected: Integer),
|
|
16
|
+
"#{self.class} requires integer operands, but got #{right_value.class}"
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
|
data/lib/dentaku/ast/case.rb
CHANGED
|
@@ -65,10 +65,19 @@ module Dentaku
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def dependencies(context = {})
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
return all_dependencies(context) if static_mode?(context) || !prunable?
|
|
69
|
+
|
|
70
|
+
switch_value = @switch.value(context)
|
|
71
|
+
|
|
72
|
+
@conditions.each do |condition|
|
|
73
|
+
if condition.when.value(context) == switch_value
|
|
74
|
+
return condition.then.dependencies(context)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
71
78
|
else_dependencies(context)
|
|
79
|
+
rescue Dentaku::Error
|
|
80
|
+
all_dependencies(context)
|
|
72
81
|
end
|
|
73
82
|
|
|
74
83
|
def accept(visitor)
|
|
@@ -77,6 +86,20 @@ module Dentaku
|
|
|
77
86
|
|
|
78
87
|
private
|
|
79
88
|
|
|
89
|
+
# pruning evaluates the switch and each when-clause until one matches;
|
|
90
|
+
# all of them must be side-effect free before we may do that
|
|
91
|
+
def prunable?
|
|
92
|
+
return @prunable if defined?(@prunable)
|
|
93
|
+
|
|
94
|
+
@prunable = @switch.pure? && @conditions.all? { |condition| condition.when.pure? }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def all_dependencies(context)
|
|
98
|
+
switch_dependencies(context) +
|
|
99
|
+
condition_dependencies(context) +
|
|
100
|
+
else_dependencies(context)
|
|
101
|
+
end
|
|
102
|
+
|
|
80
103
|
def switch_dependencies(context = {})
|
|
81
104
|
@switch.dependencies(context)
|
|
82
105
|
end
|
|
@@ -88,6 +111,10 @@ module Dentaku
|
|
|
88
111
|
def else_dependencies(context = {})
|
|
89
112
|
@else ? @else.dependencies(context) : []
|
|
90
113
|
end
|
|
114
|
+
|
|
115
|
+
def compute_pure?
|
|
116
|
+
([@switch] + @conditions + [@else]).compact.all?(&:pure?)
|
|
117
|
+
end
|
|
91
118
|
end
|
|
92
119
|
end
|
|
93
120
|
end
|