Switch to dark mode

learn

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.

Qualifying units

Optionally qualify units. A unit name has one declaring type in scope.

Form Meaning
kilogram Bare: the type that declares the unit
my_weight.kilogram Extension (or owning) type
units.mass.kilogram Import alias + type

Bare 5 kilogram binds the declarer. Qualified forms are Type.unit or alias.Type.unit (not alias.unit). A second independent type that declares the same unit name is a planning error. Extensions inherit: bare stays on the declarer; use qualification or cast for the extension.

Custom units on ratio types follow the same rule: declare basis_points (or any other custom name) on only one type in the spec. Builtin percent and permille are shared — every named ratio type may use them.

spec wages

data money: measure -> unit eur: 1
data price: money -> decimals 2

rule bare: 5 eur
rule priced: 5 price.eur

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. When a local type reuses a stdlib unit name, qualify (units.mass.kilogram): see Qualifying units.

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.

Litmus test: Can the question be answered? If yes, even when the answer is negative, use true/false. If the question itself is unanswerable for this input, use veto. "Is the customer eligible?" is always answerable (true or false). "What is the price of this coffee?" when the product is not on the menu is unanswerable (veto).

A vetoed Rule is not false. x is false does not match a vetoed x. To test whether a Rule vetoed, use x is veto.

Situation Use
Out-of-range input (negative score, age above 120) -> minimum / -> maximum on data
Closed choice list -> option on data
Normal business "no" false or no
Lookup / no mapped result default veto + unless arm per known case
Test veto without propagating x is veto (returns boolean)

Out-of-range values and failed constraints on data bind as a veto on that slot at runtime; express bounds on the data declaration instead of vetoing in a Rule.

Lookup (default veto + unless arms)

When a value is on a closed list but has no mapped outcome, default to veto and map each known case with unless:

spec coffee_prices

data money: measure
  -> unit eur: 1.00
  -> decimals 2

data product: text
  -> option "latte"
  -> option "cappuccino"

rule base_price:
  veto "Unknown product"
  unless product is "latte"      then 3.50 eur
  unless product is "cappuccino" 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 message. The total Rule also vetoes because its dependency has no value.

Bounds on data, not veto in rules

spec age_gate

data customer_age: number
  -> minimum 0
  -> maximum 120
  -> help "How old is the customer?"

rule is_adult:
  customer_age >= 18

Veto does not apply when Unless provides a fallback

spec shipping_estimate

uses lemma units

data weight: units.mass
  -> minimum 0 kilogram

data use_estimated: boolean

rule shipping_weight: weight
  unless use_estimated then 5 kilogram

If weight is missing or vetoed on constraints but use_estimated is true, shipping_weight = 5 kilogram because the Unless arm does not need weight.

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. See the lookup example above when product is absent.

After a MissingData or definitive veto, evaluation may still walk later siblings so nested control can record for explain and prune. Intake keeps unbound keys on missing_data only when some completion can still produce a value. For and, an unbound left stays MissingData even if a later conjunct definitively vetoes (false and … can still answer). Product and other operators that need both values settle on a definitive factor and clear keys that cannot un-veto. is veto stays a boolean probe and does not change this intake rule.

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
  -> minimum 0

data quantity: number

rule line_total: price * quantity
  unless price is veto then 0

When price vetoes (for example a failed constraint override), price is veto is true and line_total can take the fallback 0. The test never returns Veto; only the Rule's final arm can.

Equivalent forms: veto is price, price is not veto, not veto is 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.

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 run data 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 run data 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.