Cookbook
Recipes that combine turo’s features into real calculations. Each one starts
with a problem, shows the turo source, and gives the output the turo CLI
actually produces with the standard prelude. Read REFERENCE.md
first for the individual features; this document is about putting them together.
Two display conventions below:
- Inline turo examples are live: the editor shows each line’s computed answer beside it, so there’s no separate echo comment.
- A results block shows the per-line output you get from
turo --json(the notepad surface), one row per line.
A mortgage you can run backwards
Section titled “A mortgage you can run backwards”A formula is a system of equations, and turo solves it in whichever direction you leave a hole. Write the monthly-payment relation once, and you can compute the payment from the principal — or the principal from a payment you can afford.
formula Mortgage(principal, monthly rate, n, payment):
payment = principal * monthly rate / (1 - (1 + monthly rate) ^ (0 - n))
// Forward: what's the payment on a $250k loan at 6% over 30 years?
Mortgage(principal = 250000, monthly rate = 6% / 12, n = 360)::payment as 2 dp
// Backward: $1,500/month supports how much principal? Same formula.
Mortgage(payment = 1500, monthly rate = 6% / 12, n = 360)::principal as 2 dp
This pulls together four features: a formula with named properties, percent
arithmetic for the rate, exponentiation in the body, and the solver
rearranging the equation to isolate whichever property you ask for with ::.
The as 2 dp rounds the result; the thousands separator comes from the prelude’s
default.
A budgeting notepad
Section titled “A budgeting notepad”In --json mode turo evaluates one line at a time and returns a result for each
— a calculating notepad. Bindings can reference earlier ones in any order.
income = 4200
rent = 1350
groceries = 600
savings rate = 15%
saved = income * savings rate
left over = income - rent - groceries - saved
results:
income = 4200rent = 1350groceries = 600savings rate = 15%saved = 630left over = 1620The 15% flows through income * savings rate as ordinary multiplication, and
left over composes three earlier bindings. Change income and every dependent
row recomputes.
Pace and projection for a race
Section titled “Pace and projection for a race”Compound time units (3 hour 45 minute) and compound unit division (minutes/km)
turn a finish time into a pace, then project it onto a different distance.
marathon = 42.195 km
finish = 3 hour 45 minute
pace = finish / marathon to minutes/km
ten k time = pace * 10 km to minutes
results:
marathon = 42.195 kmfinish = 3.75 hourpace = 5.33238535371 minutes/kmten k time = 53.3238535371 minutesfinish / marathon is a Time / Length, which turo recognizes as the inverse
of a speed; converting it to minutes/km gives a readable pace. Multiplying that
pace by a new distance cancels the length dimension back out to a time.
Stacked discounts, tips, and splitting the bill
Section titled “Stacked discounts, tips, and splitting the bill”Percentages chain the way a shopper expects, and combine with a unit so a bill keeps its currency.
// 20% off, then a further 10% off — not 30% off.
let list price = 120
list price - 20% - 10%
Splitting a tab needs a currency. The standard prelude has no money dimension — that is deliberate, since money is not physics — so declare your own:
dimension Money
unit USD of Money, also: dollars
let bill = 84 USD
let total = bill + 18% // add an 18% tip
total / 4 as 2 dp
bill + 18% keeps the USD unit because percentage arithmetic is
dimension-transparent, and dividing by 4 (a plain number) leaves the unit intact.
A currency mini-system
Section titled “A currency mini-system”Extend the money idea into several currencies anchored to one base, and turo will convert between them like any other units.
dimension Money
unit USD of Money, also: dollars
unit EUR = 1.09 USD, also: euros
unit GBP = 1.27 USD, also: pounds
hotel = 220 EUR
flight = 180 GBP
trip = (hotel to USD) + (flight to USD)
results:
hotel = 220 EURflight = 180 GBPtrip = 468.4 USDOne gotcha worth knowing: to binds looser than +, so
hotel to USD + flight to USD parses as hotel to (USD + flight) to USD and
fails. Parenthesize each conversion. The parser’s precedence is the prelude’s
choice — see prelude/format.turo.
Geometry by composition and back-solving
Section titled “Geometry by composition and back-solving”A formula body can bind a sub-instance — a property that is itself another formula. Its inner names join the outer equation system under a path prefix, so turo solves the merged system as a unit. That means a back-solve reaches straight through the composition: supply the outer result, leave an inner property free, and turo finds it.
formula Rect(w, h):
area = w * h
formula Box(height, volume):
base = Rect(h = 3 m) // a sub-instance — Rect lives inside Box
volume = base::area * height
// Know the box's volume and height, and the base's height — solve the base's width.
Box(height = 4 m, volume = 24 m^3)::base::w
turo flattens this to base::area = base::w * 3 m, volume = base::area * 4 m,
and volume = 24 m^3, then isolates base::w two levels down. Run it forward
instead — Box(height = 4 m) with base = Rect(w = 2 m, h = 3 m) — and the
result reads back as a nested record:
Box(height = 4 m, base = Rect(w = 2 m, h = 3 m, area = 6 m^2), volume = 24 m^3).
A single formula back-solves just as readily. Here we know a cylinder’s volume
and height and ask for the radius — turo isolates r through the square:
formula Cylinder(r, h, volume):
volume = 3.14159265 * r ^ 2 * h
Cylinder(volume = 1 l, h = 10 cm)::r to mm
The volume 1 l and height 10 cm are different units of different dimensions;
turo normalizes both to base units before solving, so the radius comes out
correctly dimensioned and converts cleanly to millimetres.
One physics formula, any unknown
Section titled “One physics formula, any unknown”suvat is the classic motion system: five quantities, two equations. Declare it
once and solve for whichever one you leave out.
formula Suvat(s, u, v, a, t):
v = u + a * t
s = u * t + a * t ^ 2 / 2
// A car covers 100 m from rest in 5 s — what was its acceleration?
Suvat(s = 100 m, u = 0 m/s, t = 5 s)::a
This is a genuine two-equation, multi-unknown system. turo substitutes the knowns,
recognizes that a can be isolated from the distance equation once u and t
are fixed, and solves it — carrying m, s, and the derived m/s^2 through the
whole rearrangement.
Calendar math, across time zones
Section titled “Calendar math, across time zones”@-literals are real instants, so date arithmetic respects the calendar, and
zone conversion respects offsets and daylight saving — both via jiff, not
hand-rolled.
let christmas = @2026-12-25
christmas - today to days
Subtracting two instants gives a duration in seconds; converting it to days
makes it readable. (today comes from the host clock; the output above is for
2026-06-26.)
Planning a call between offices is a zone conversion:
@2026-06-26 at @17:00 in Europe/London to America/New_York
in Europe/London fixes the wall-clock time to a zone; to America/New_York
converts the same instant into the other zone, doing the offset arithmetic for
you.
Embedding: a units-aware evaluator in Rust
Section titled “Embedding: a units-aware evaluator in Rust”To put turo behind your own interface, wire the standard prelude into a
ScopeResolver once and vend a fresh document per evaluation. This is the same
wiring the turo CLI uses.
use std::collections::BTreeMap;use turo_lang::{ImportResolver, InMemoryResolver, ScopeResolver};
/// Build an evaluator with the full standard prelude loaded.fn standard_resolver() -> ScopeResolver { let mut chunks = BTreeMap::new(); for entry in std::fs::read_dir("prelude").unwrap().flatten() { let path = entry.path(); if path.extension().and_then(|e| e.to_str()) == Some("turo") { let stem = path.file_stem().unwrap().to_str().unwrap().to_string(); chunks.insert(stem, std::fs::read_to_string(&path).unwrap()); } } let prelude: Box<dyn ImportResolver> = Box::new(InMemoryResolver::from_map(chunks)); let user: Box<dyn ImportResolver> = Box::new(InMemoryResolver::empty()); ScopeResolver::new(prelude, user, Some("standard"))}
/// Evaluate one line and render its result the way the CLI does.fn eval_one(resolver: &ScopeResolver, src: &str) -> String { let mut doc = resolver.new_document(); let result = doc.evaluate_document(src); if let Some(row) = result.results.first() { if let Some((numeric, unit)) = row.display_strings() { return if unit.is_empty() { numeric.to_string() } else { format!("{numeric} {unit}") }; } } result.errors.first() .map(|e| format!("error: {}", e.variant_name())) .unwrap_or_default()}
fn main() { let resolver = standard_resolver(); for line in ["60 mph to kph", "solve for x: x ^ 2 = 144", "5 m + 3 kg"] { println!("{line:<24} => {}", eval_one(&resolver, line)); }}Output:
60 mph to kph => 96.56064 kphsolve for x: x ^ 2 = 144 => 125 m + 3 kg => error: DimensionMismatchdisplay_strings() gives the rendered numeric and unit parts separately, so your
UI can lay them out however it likes. For the browser, the turo-wasm crate wraps
this same engine — it embeds the prelude with include_str! and exposes
evaluate/apply_change returning the JSON envelope, which the Svelte notepad in
web/ renders live as you type.
Where to go next
Section titled “Where to go next”These recipes only scratch what the combination of dimensions, formulas, and the solver makes possible. Two directions to explore:
- Declare your own surface in a prelude. A host prelude can add operators, native functions, inverse rules, and unit groups — the machinery the standard prelude is built from. See DESIGN.md for how dispatch and the solver consume those declarations.
- Push the solver. Multi-equation formulas with units, guards, and
composition are where turo earns its keep. When a shape stalls, the error tells
you whether you under-specified it (
InsufficientBindings) or hit a current engine limit (EngineCapabilityGap).