Release 4.0.0#340
Open
rubysolo wants to merge 18 commits into
Open
Conversation
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>
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.