Dentaku: evaluating untrusted formulas safely in Ruby
math and logic formula parser and evaluator
At a glance
- What is it?
- Dentaku is a Ruby parser and evaluator for a mathematical and logical formula language that binds variables at run time, built so untrusted expressions can be evaluated without opening security holes. The cost of that flexibility is roughly two orders of magnitude slower than plain Ruby arithmetic, unless you turn on AST caching.
- Who is it for?
- Dentaku fits Ruby applications that let users, tenants, or administrators write formulas that are evaluated against their own data, such as pricing rules, conditional logic, and report filters. It is the wrong tool for plain arithmetic on a hot path, since the documentation states that tokenizing, parsing, and evaluating are about two orders of magnitude slower than the same math in pure Ruby.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 19 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- rubysolo/dentaku973 ★ · +2
| Repository | 2026-10-21 | 2026-11-07 |
|---|---|---|
| rubysolo/dentaku | 971 | 973 |
2 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The problem: letting users write rules that your code then executes
Plenty of products need a formula feature. A pricing engine wants a rule language. A report builder wants users to type conditions. A rules engine wants thresholds that change without a deploy. All of them hit the same wall: once the expression comes from outside your codebase, evaluating it with eval is a security hole, and evaluating it with a hand-rolled parser is a week of work that ends with bugs. Dentaku is a parser and evaluator for a mathematical and logical formula language that allows run-time binding of values to variables referenced in the formulas, and it states plainly that it is intended to safely evaluate untrusted expressions without opening security holes. The audience is Ruby developers who need to run user-supplied expressions safely, and the language is deliberately small, closer to a spreadsheet function set than to a general programming language.
Run-time binding, stored variables, and case sensitivity
The core API is a calculator object with an evaluate method. A constant expression is the trivial case:
calculator = Dentaku::Calculator.new
calculator.evaluate('10 * 2')
#=> 20What makes it useful is the binding. Variables can be passed per call, which is how a formula gets evaluated against a specific row or request:
calculator.evaluate('kiwi + 5', kiwi: 2)
#=> 7Variable names are case insensitive by default, so Kiwi and kiwi resolve to the same value, and the calculator can be put into a case sensitive mode through a constructor option, in which case the two keys become distinct:
calculator = Dentaku::Calculator.new(case_sensitive: true)
calculator.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2)
#=> 3Values can also be held in the calculator's own memory with store, and evaluated against later, which is how you build a session of context without passing a hash on every call. The alias bind exists for people who prefer that word. Comparisons work on stored values too, so a stored number can be tested with the usual operators. Context keys prefixed with a double underscore are reserved for internal use, so a formula author cannot collide with them.
Built-in functions that behave like their spreadsheet namesakes
Dentaku ships with built-in functions including if, not, min, max, sum, and round, and the documentation notes that functions generally work like their counterparts in Excel. That is a useful mental model, because most people who need a formula language already know it. The condition function takes a predicate and two branches:
calculator.evaluate('if (pears < 10, 10, 20)', pears: 5)
#=> 10
calculator.evaluate('if (pears < 10, 10, 20)', pears: 15)
#=> 20Summation is variadic, so SUM(1, 1, 2, 3, 5, 8) returns 20. Rounding follows the ordinary rounding rules and can be called with or without a decimal place count, giving 8 for round(8.2) and 8.28 for round(8.2759, 2), while roundup and rounddown are the ceil and floor counterparts. There is a shortcut for people who do not want to build a calculator object: calling Dentaku directly with an expression and a variable hash, so Dentaku('plums * 1.5', plums: 2) returns 3.0. The upstream repository also has a gemspec, a Rakefile, a spec directory, a mise.toml for toolchain versions, and a rubocop configuration, so it is a conventional gem with a conventional test suite.
nil for errors, or an exception when you want one
Error handling is a deliberate choice rather than an accident. The evaluate method returns nil if there is an error in the formula, and evaluate! raises instead. The distinction matters when a formula is user supplied and a typo in a label should not take down a request:
calculator.evaluate('10 * x')
#=> nil
calculator.evaluate!('10 * x')
Dentaku::UnboundVariableError: Dentaku::UnboundVariableErrorThe unbound variable case gets its own error class, Dentaku::UnboundVariableError, and that error is raised only when the value is actually needed, because logical constructs short-circuit. IF and CASE evaluate only the branch that is taken, and AND and OR resolve as soon as one operand decides the result. So an unbound variable on a branch that never runs is not an error at evaluation time. The trade-off is that a misspelled identifier on an untaken branch goes unreported, which is why the project provides a separate static method for listing possible inputs. If you want strictness in one place and leniency in another, the two methods let you pick per call site.
dependencies versus identifiers, and why the difference matters
Two methods answer two different questions about a formula's inputs, and confusing them is the easiest way to build a subtly wrong form. calculator.dependencies(expression, context) reports the identifiers a formula still needs, given what is already known. It is resolution aware. Guard positions such as IF predicates, CASE switches, and AND or OR operands whose own dependencies are already satisfied are evaluated, executing any non-volatile functions they contain, so that branches which cannot be taken are pruned. calculator.identifiers(expression) reports every identifier the formula could ever reference, regardless of branching. It is purely syntactic: nothing is evaluated, no functions run whether volatile or not, and stored variables are not subtracted.
calculator.dependencies('IF(x > 5, y, z)', x: 7) #=> ["y"]
calculator.identifiers('IF(x > 5, y, z)') #=> ["x", "y", "z"]Use dependencies when you want to know what to prompt a user for right now, and identifiers when you want to validate a formula statically, for instance to list every value a user might be asked to supply. Because unused operands are never checked at evaluation time, static validation is the only way to catch the typo.
Volatile functions and the purity contract
Dentaku treats formulas as pure, and this has consequences for anyone writing a custom function. A subexpression may be evaluated zero, one, or more than one time, for example because a guard position is evaluated once during dependency analysis and again during evaluation, so custom functions should not rely on side effects or call counts. Functions that fall outside the contract, meaning they read external state, perform I/O, or return different values across calls, must be registered with volatile: true. Volatile functions are never invoked during dependency analysis, which means that in guard positions no branch can be pruned, so all branches count as dependencies, and evaluate! requires variables from every branch to be bound. This is a deliberate safety valve. A function that reads the current time or a random value has no business inside a formula engine that assumes determinism, and marking it volatile keeps the engine honest about that.
AST caching as the answer to the performance cost
The performance section is blunt about the price. Tokenizing a string, parsing to an AST, and evaluating that AST are about two orders of magnitude slower than doing the same math in pure Ruby. Most of that time goes into tokenizing and parsing rather than evaluation, which is what makes caching the right lever:
Dentaku.enable_ast_cache!The cache can also be controlled per calculator, overriding the module level setting:
calculator = Dentaku::Calculator.new(cache_ast: true)With caching on, Dentaku keeps the AST of each formula it evaluates, so later evaluations, even with different values for the variables, are much faster and closer to four times native Ruby speed. The project is careful to call those figures rough estimates and to tell you to measure with representative formulas from your own application. The cost is memory, and it grows with each new formula, so an application that constantly introduces new formulas will trade CPU for RAM. There is also a subtlety about scope: the cache is keyed by formula, so a formula that changes shape on every request, such as one built by string interpolation, will miss every time.
Ruby compatibility, releases, and the licence
Dentaku supports Ruby 3.2 and newer and tracks Ruby's own maintenance policy, meaning that when a Ruby version reaches end of life it may be dropped from the supported compatibility contract in the next major or minor release. Older Rubies may keep working, but they are not tested. New code should target the lowest supported Ruby version, currently Ruby 3.2. That policy is worth reading as an upgrade cost: adopting Dentaku means your supported Ruby range is tied to the gem's, and you will get a version bump when a Ruby version ages out. The project is not archived and the last push was on 2026-10-18, with version 4.1.0 released on 2026-10-15, so releases are moving. Licensing is MIT, with the LICENSE file at the repository root, so embedding the evaluator in a proprietary application carries no copyleft obligation. The gem has a CHANGELOG.md, which is where you would look to understand what changed between major versions before pinning.
Editorial conclusion
Dentaku fits Ruby applications that let users, tenants, or administrators write formulas that are evaluated against their own data, such as pricing rules, conditional logic, and report filters. It is the wrong tool for plain arithmetic on a hot path, since the documentation states that tokenizing, parsing, and evaluating are about two orders of magnitude slower than the same math in pure Ruby. Before adopting it, decide whether to enable AST caching with Dentaku.enable_ast_cache! or per calculator with cache_ast: true, measure with formulas representative of your own application rather than trusting the rough estimates, and check whether any custom function you register reads external state, because those must be marked volatile: true.
Frequently asked questions
How do I evaluate a formula with variables using Dentaku?
Pass the variables as a hash to evaluate, so calculator.evaluate('kiwi + 5', kiwi: 2) returns 7. You can also store values in the calculator with store, or bind as its alias, and evaluate against them later.
Should I use evaluate or evaluate! in Dentaku?
evaluate returns nil when the formula has an error, while evaluate! raises an exception such as Dentaku::UnboundVariableError. Use evaluate! where a broken formula should be surfaced loudly and evaluate where a bad user formula should not break the request.
How do I make Dentaku faster?
Enable AST caching with Dentaku.enable_ast_cache! or per calculator with Dentaku::Calculator.new(cache_ast: true). The documentation notes that tokenizing, parsing and evaluating are about two orders of magnitude slower than pure Ruby, and that caching brings later evaluations closer to four times native Ruby speed.
What is the difference between dependencies and identifiers in Dentaku?
dependencies(expression, context) is resolution aware and reports what a formula still needs, pruning branches that cannot be taken. identifiers(expression) is purely syntactic and lists every identifier the formula could reference, with nothing evaluated.
Can a custom Dentaku function read external state?
Yes, but it must be registered with volatile: true. Dentaku treats formulas as pure, so functions that read external state, perform I/O, or return different values across calls are never invoked during dependency analysis and must be marked volatile.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rubysolo-dentaku)Star history chart for your README
[](https://hysenlabs.com/projects/rubysolo-dentaku)Was this useful?