Skip to content

Release 4.0.0#340

Open
rubysolo wants to merge 18 commits into
mainfrom
v4.0.0-pre
Open

Release 4.0.0#340
rubysolo wants to merge 18 commits into
mainfrom
v4.0.0-pre

Conversation

@rubysolo

Copy link
Copy Markdown
Owner

No description provided.

rubysolo and others added 15 commits July 18, 2026 19:02
Breaking changes:
- require Ruby 3.2 or newer (EOL Rubies dropped from support matrix)
- Calculator.new takes keyword arguments; AST cache is an explicit
  ast_cache: option and unknown options raise ArgumentError
- Dentaku::Error is now a module included by all Dentaku exceptions,
  so `rescue Dentaku::Error` catches ArgumentError/ZeroDivisionError;
  internal exceptions subclass the new Dentaku::BaseError
- TokenMatcher function matchers use an explicit .function(name)
  constructor instead of method_missing; vestigial math_neg_pow /
  math_neg_mul matcher symbols removed
- remove dead Travis CI configuration

Other changes:
- cache flags configurable per calculator (cache_ast:,
  cache_dependency_order:), defaulting to module-level settings
- Dentaku.aliases resolved lazily so post-creation assignment applies
- CASE dependencies short-circuit to the matched branch when the
  switch value is resolvable

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first-WHEN branch of Parser#handle_case only finalized the switch
expression when the CASE marker was on top of the operator stack, so
any pending operator (CASE a % 5 WHEN ...) derailed parsing until END
failed with :unprocessed_token. This also meant PrintVisitor output for
such statements (which deliberately drops the parentheses) could not be
re-parsed.

Fold pending operators down to the innermost CASE marker before
finalizing the switch variable, mirroring the consume_until pattern the
THEN/subsequent-WHEN branches already use. Malformed statements (double
WHEN, WHEN after ELSE) keep their existing failure paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- AND/OR short-circuit with unbound operands (issue #234), plus a
  passing guard that genuinely undecidable expressions still raise
- recipient_variable visible inside solve blocks (issue #333) - the
  existing specs pass only because load_results sets the attribute on
  the block's return value after the block has already run
- tsort declared as a runtime dependency for Ruby 4.1 (PR #334)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AND/OR now resolve as soon as one operand decides the result, matching
the short-circuit behavior IF and CASE already have. An unbound variable
raises only when its value is actually needed: `a OR b` with a=true
returns true, and dependency analysis prunes decided combinators the
same way it prunes IF predicates and CASE switches. Ruby-truthiness
return values (`5 OR x` => 5, `nil AND x` => nil) are preserved. The
formula-purity contract and the static-validation escape hatch
(context-free `dependencies`) are documented in the README.

Fixes #234

BulkExpressionSolver's evaluators dropped the per-variable handler
block, so recipient_variable was only assigned after the solve block
had already run (and only when the block returned the exception
itself). Forward the handler so the attribute is set before the block
is invoked, restoring the pre-3.5.4 behavior.

Fixes #333

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ception classes

Indexing a non-indexable value (number, NULL, boolean) with [] now raises
Dentaku::ArgumentError instead of silently bit-referencing integers or
leaking raw NoMethodError/TypeError.

ParseError and TokenizerError now build their own default messages from
reason and meta, collapsing Parser#fail! and Tokenizer#fail! to one-liners.
Message text is unchanged, and call sites can still override via the
two-arg raise form.

Also fixes two crashes on error paths: the parser's unbalanced-parenthesis
report passed the token positionally to fail! (raising Ruby ArgumentError
instead of ParseError), and the tokenizer's zero-width-match message
fetched the wrong meta key (raising KeyError instead of TokenizerError).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dependency analysis and evaluation can each execute a guard position,
so the README's "zero or one times" claim was inaccurate. State that
subexpressions may run more than once so heavy or impure predicates
aren't a contract violation on our side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Track purity through the AST: every node memoizes pure?, and functions
registered with the new volatile: option (add_function/add_functions and
FunctionRegistry#register) are never executed during dependency
resolution. Branch-pruning nodes (IF, CASE, AND/OR) now prune only when
their guards are pure; otherwise they fall back to reporting every
branch, so a conditional guarded by a volatile function requires
variables from all branches and evaluates the guard exactly once per
evaluate! instead of twice.

Also add Calculator#identifiers, a purely syntactic listing of every
identifier a formula could reference regardless of branching, backed by
a reserved __static_dependencies context key that disables pruning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parser's NodeError rescue discarded the precise message built at the
raise site (e.g. "Dentaku::AST::Addition requires operands that are
numeric or compatible types, not string") and rebuilt a misleading one
from expect/actual metadata ("requires incompatible operands, but got
string"). Thread the original message through instead, and reword the
metadata-derived fallback so an :incompatible expectation reads as
"requires compatible operands".

Idea from #341, thanks Nikolay Moskvin.

Co-authored-by: Nikolay Moskvin <moskvin@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "required vs. received" pair was spelled four different ways across
exception metadata: expect:/actual: (NodeError, ParseError, logical
functions), for:/value: (ArgumentError type mismatches), at_least:/given:
(function arity minimums), and exact:/given: (exact arity). Rescue code
pattern-matching one vocabulary silently missed the others.

Standardize on expected:/actual: everywhere. Arity expectations follow the
parser's existing convention of an Integer or Range (at_least: 1 becomes
expected: 1..), and actual: always carries the offending value itself
rather than sometimes its class. NodeError#expect is renamed to #expected
to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…metadata

The :operator meta key held a class in ParseError (Dentaku::AST::Addition)
but a symbol in ArgumentError's :invalid_operator (:+, where the class was
under :operation). Standardize: operation: is the AST class, operator: is
the symbol, in both exception types. The parser's consume-time local is
renamed to match the operations stack it pops from.

Also rename NodeError#child to #operand: it holds the failing operand's
position (:left / :right / :node), not a child node object, and the old
name suggested otherwise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
function_name: sometimes carried presentation parens ('AND()') and
sometimes not (the parser's :undefined_function passes the bare token
value). Metadata is data: store the bare name and let message text add
parens where it wants them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ArgumentError was the last reason/meta exception without default messages,
so any raise site that skipped the explicit-message form surfaced with the
bare class name as its message. Build defaults from reason and metadata
like ParseError and TokenizerError, and drop the per-site messages the
defaults now cover (the logical functions' arity/type complaints, the
shared "requires operands that respond to" text in comparators and
arithmetic).

Bitwise operations were the victims of the missing defaults: they raised
:invalid_operator (an operand-type problem, not an operator problem) with
no message at all, surfacing as "Dentaku::ArgumentError". They now raise
:incompatible_type with a real message naming the operation and offending
type.

The parser's blanket ::ArgumentError re-wrap also dropped reason/meta when
the error was already a Dentaku::ArgumentError, and constructed the
replacement with the class-form raise that bypassed .for entirely;
re-raise Dentaku errors untouched and wrap foreign ones as :invalid_value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Recipient variable" never quite parsed; the attribute holds the name of
the variable the result was being assigned to when the bulk solver caught
the error. assigned_to says that directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rubysolo and others added 3 commits July 18, 2026 19:08
- use https for the rubygems source and the gem homepage
- drop the deprecated s.test_files= assignment (a no-op in modern RubyGems
  and only useful for packaging specs into the gem, which we don't want)
- remove obsolete gemspec boilerplate: the utf-8 magic comment (default
  since Ruby 2.0; we require 3.2) and the $LOAD_PATH dance, loading the
  version with require_relative instead

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RubyGems flags open-ended (>= 0) dependencies. Floor each to the version
that ships with our minimum Ruby (3.2):

- bigdecimal  >= 3.1   (uncapped: a bundled gem whose major tracks Ruby's
                        releases -- already 4.x on Ruby 3.4 -- so an upper
                        cap would eventually collide with a stock Ruby)
- tsort       >= 0.1.1 (uncapped, same bundled-gem reasoning; frozen API)
- concurrent-ruby ~> 1.1 (third-party, so our constraint drives resolution;
                          cap below the next major)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Populate rubygems.org sidebar links (homepage, source, changelog, issues)
and require MFA for gem pushes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant