Types and units
Lemma has a rich type system built from primitives, quantities with units, ratios, and ranges. This chapter covers literals, operators, the standard library, conversions, ranges, dates, and Veto (the domain outcome when a Rule cannot produce a value).
Literal types
| Type | Example | Notes |
|---|---|---|
| Number | 42, 3.14, 1.23e10 |
Exact rational arithmetic |
| Text | "hello" |
String literals |
| Boolean | true, false, yes, no |
Aliases |
| Date | 2024-01-15, 2024-01-15T14:30:00Z |
ISO 8601 |
| Time | 14:30:00 |
Time of day |
| Measure | 100 eur, 40 hour, 12 kilogram |
Unit must be declared by a measure type in scope |
| Ratio | 15 percent, 15%, 5 permille, 5%% |
Proportional values |
| Range | 0...100, 2024-01-01...2024-06-15, 18 year...67 year |
Half-open intervals |
See Primitive types in the language reference.
Arithmetic
spec arithmetic_examples
data price: 100
data tax: 21
data quantity: 3
data principal: 1_000
data rate: 0.05
data year: 10
rule total: (price + tax) * quantity
rule compound: principal * (1 + rate) ^ year
Operators: +, -, *, /, %, ^
Comparison
spec comparison_examples
data status: text
data age: number
data income: number
rule status_ok: status is "approved"
rule not_cancelled: status is not "cancelled"
rule is_eligible: age >= 18 and income > 30_000
Operators: >, <, >=, <=, is, is not, is veto, is not veto
Logical
spec loan_approval
data credit_score: number
data income_verified: boolean
data has_bankruptcy: boolean
rule can_approve_loan:
credit_score >= 650 and income_verified and not has_bankruptcy
Operators: and, not (there is no or: Unless chains accommodate such logic)
Mathematical
spec math_examples
data a: 3
data b: 4
data angle: 0.5
rule hypotenuse: sqrt (a ^ 2 + b ^ 2)
rule sine_value: sin angle
rule log_value: log 10
Prefix operators (parentheses optional): sqrt, sin, cos, tan, log, exp, abs, floor, ceil, round
Standard library: uses lemma units
Lemma embeds SI bases, derived compounds, imperial, and information units in the standard library (Repo lemma, Spec units). Import with uses lemma units, then use units directly in literals or reference types as units.mass, units.duration, units.length, units.calendar, units.force, and others.
Names are singular only (8 hour, not 8 hours). Length uses American meter (not metre).
spec logistics
uses lemma units
data package_weight: 12 kilogram
data shift_length: 8 hour
data route_distance: 45 kilometer
rule weight_grams: package_weight as gram
rule is_heavy: package_weight > 20 kilogram
rule is_long_shift: shift_length >= 8 hour
Duration units (hour, day, week, ...) come from units.duration; calendar periods (year, month) from units.calendar. Prefer the stdlib types over redefining kilogram or hour in every Spec.
Unit conversions
Convert within a unit family with as:
spec conversion_examples
data money: measure
-> unit eur 1.00
-> unit usd 0.91
data price: 100 eur
rule price_usd: price as usd
rule as_percent: 0.25 as percent
Durations convert the same way:
spec schedule
uses lemma units
data workweek: 40 hour
rule workweek_days: workweek as day
Strip to a bare number with a chained cast: amount as eur as number. See Type cast in the language reference.
Ranges
Intervals use lo...hi (lower inclusive, upper exclusive). Test membership with in; project width with (lo...hi) as <unit>. Range slots use number range, date range, time range, measure range, ratio range, or a named <type> range. Constrain endpoints with -> lower / -> upper and span width with -> minimum / -> maximum:
spec eligibility
uses lemma units
data age: 25 year
data score: 50
data window: date range
-> lower 2020-01-01
-> upper 2030-12-31
-> minimum 1 day
-> maximum 90 day
rule in_working_age: age in 18 year...67 year
rule in_band: score in 0...100
rule days_in_q2: (2024-04-01...2024-07-01) as day
See Ranges in the language reference and Data commands.
Date and time
Dates compare directly; spans between dates are ranges projected to a unit; durations add to dates:
spec deadlines
uses lemma units
data today: 2024-09-30
data deadline: 2024-12-31
rule days_until_deadline: (today...deadline) as day
rule is_overdue: today > deadline
rule follow_up_date: deadline + 14 day
Calendar vs duration arithmetic
Calendar units (year, month) use calendar-aware arithmetic; duration units (day, hour, second, ...) use fixed-length arithmetic. Adding one month to March 1 gives April 1 regardless of month length, while adding 30 day always adds exactly 30 × 86400 second.
Month-end clamping: adding month or year clamps to the last valid day of the target month. January 31 + 1 month → February 28 (or 29 in a leap year). March 31 − 1 month → February 28/29.
Relative to now
now is the evaluation instant. Compare dates to it, or to a sliding window / calendar period (duration units from uses lemma units):
spec recency
uses lemma units
data event_date: date
rule was_before_now: (event_date in past)
rule recent: event_date in past 7 day
rule this_year: event_date in calendar year
rule last_month: event_date in past calendar month
Also: in future, future N day, in past|future calendar year|month|week, not in calendar …, and bare windows past 7 day. Full table: Date predicates in the language reference.
Veto
Use Veto when a Rule cannot produce a meaningful value: the domain says "no answer here." Use Veto for impossible situations, not for negative business results. A Rule that evaluates to false or 0 is still a valid result.
Common cases:
- Data validation: input is out of acceptable range.
- Missing or unrecognized input: a required Data field has no match.
- Constraint violations at runtime: a Data override breaks a bound or type constraint.
spec age_validation
data age: number
rule validated_age: age
unless age < 0 then veto "Age must be a positive number"
unless age > 120 then veto "Invalid age value"
A vetoed Rule produces no result. If a Rule references a vetoed Rule and needs its value, the Veto propagates. If an Unless clause provides an alternative, the Veto does not propagate:
spec scoring
data score: number
data use_default: false
rule validated_score: score
unless score < 0 then veto "Invalid score"
rule result: validated_score
unless use_default then 50
If validated_score is vetoed but use_default is true, result = 50.
Veto applies to dependent Rules
spec order_total
data price: number
data quantity: number
rule validated_price: price
unless price < 0 then veto "Price cannot be negative"
rule total: validated_price * quantity
If validated_price is vetoed, total is also vetoed because we need the price value.
Veto does not apply when Unless provides a fallback
spec shipping_estimate
data weight: number
data use_estimated: boolean
rule validated_weight: weight
unless weight < 0 then veto "Weight cannot be negative"
rule shipping_weight: validated_weight
unless use_estimated then 5
If validated_weight is vetoed but use_estimated is true, then shipping_weight = 5. The Veto does not apply because validated_weight is never evaluated (the Unless clause provides the value).
Missing Data propagates as Veto
When a Data field has no value (not provided), Rules that depend on it Veto with a "Missing data" reason:
spec coffee_prices
data money: measure
-> unit eur 1.00
-> decimals 2
data product: text
-> option "latte"
rule base_price: veto "Unknown product"
unless product is "latte" then 3.50 eur
rule total: base_price * 2
If product is not provided, base_price vetoes with "Missing data: product", not the default arm's "Unknown product" message. The total Rule also vetoes because its dependency has no value.
is veto (boolean test)
Test whether an expression produced no value and branch on a boolean, without propagating the operand's Veto through the test:
spec fallback_total
data price: number
data quantity: number
rule validated_price: price
unless price < 0 then veto "Price cannot be negative"
rule total: validated_price * quantity
unless validated_price is veto then 0
When validated_price vetoes, validated_price is veto is true and total can take the fallback 0. The test never returns Veto; only the Rule's final arm can.
Equivalent forms: veto is validated_price, validated_price is not veto, not veto is validated_price.
When the operand is a Rule reference (validated_price is veto), the test reads that Rule's already-computed result. When the operand is a compound expression (price * quantity is veto), that expression is evaluated and the test is true when the result is a Veto. To test a single failing operand inside a sum or product, apply is veto to that operand (b is veto) rather than to the whole expression (a + b is veto).
You can Veto again based on a test: unless x is veto then veto "outer". The Rule's result message is then "outer"; explanations may still show the inner Veto beneath the is veto operand.
veto and veto "message" are only valid as a Rule or Unless result. See Special expressions in the language reference.
Test for a Veto without propagating it:
spec veto_checks
data score: number
rule validated_score: score
unless score < 0 then veto "Invalid score"
rule has_valid_score: validated_score is not veto
Veto vs Error vs Panic
Lemma distinguishes three outcomes:
| Outcome | When | Example |
|---|---|---|
| Planning Error | Invalid Spec (wrong types, unsupported operations) | 5 and "text" (logical AND requires boolean operands); 1 / 0 (literal division by zero) |
| Request Error | Malformed run request (before evaluation) | Duplicate overlay keys that canonicalize to the same name (Age and age) |
| Veto | Domain "no value" at runtime | Division by zero from Data, missing Data, invalid Data override, user veto "...", date overflow |
| Panic | Bug (invariant violated; should never happen after planning) | Internal consistency failure |
After planning succeeds, a well-formed run completes with Rule results (values or Vetoes). Data overrides that violate type constraints, minimum/maximum bounds, or allowed options bind as a Veto on that Data (dependent Rules veto); they are not a planning error. Unknown overlay keys and import aliases are ignored; a MissingData veto may suggest a near match from ignored keys. Duplicate canonical keys in the same request are a request Error and abort before evaluation.
Veto is only for domain-level "no value", not for type errors or invalid operations in the Spec itself. Those are caught at planning time.
Next up
Extending Data: parent types, Data commands, and reuse across Specs.