Specification · v1

Evaluation

1. decide

A valid contract is compiled once and then decides actions:

decide(action, facts) → { verdict: "PERMIT" | "DENY", clause_id }

action must be listed in the contract's TRIGGER. Asking about any other action is a usage error (ulang-lite throws a RangeError), not a verdict. Only the clauses for that action take part, in source order:

  1. The first FORBID whose condition holds decides: DENY <Contract>.<id>.
  2. Otherwise, the first ENSURE whose condition fails decides: DENY <Contract>.<id>.
  3. Otherwise: PERMIT <Contract>.<action>.permit.

Prohibitions are checked before guarantees, whatever their order in the source. Within each group, source order decides which clause a DENY names when more than one applies. A contract that lists its prohibitions from most to least specific therefore produces the most useful DENY.

Expense approvalexpense
// Expense approvals: who may approve, and up to how much.
CONTRACT ExpenseApproval {
  ENFORCEMENT_MODE: enforced
  CONTEXT {
    ENTITIES Expense {
      amount: float
      approver_is_submitter: bool
      approver_limit: float
      receipt_attached: bool
      category: string
    }
    CONSTANTS {
      receipt_threshold: float == 75.0 [SOURCE:FIN-POLICY-4.2]
      hard_cap: float == 10000.0 [SOURCE:FIN-POLICY-4.1]
    }
  }
  TRIGGER {
    approve_expense()
  }
  GUARANTEE {
    ENSURE approve_expense() WHEN Expense.receipt_attached == true OR Expense.amount < receipt_threshold [SOURCE:receipt_required]
  }
  PROHIBITION {
    FORBID approve_expense WHEN Expense.approver_is_submitter == true [SOURCE:no_self_approval]
    FORBID approve_expense WHEN Expense.amount > hard_cap [SOURCE:over_hard_cap]
    FORBID approve_expense WHEN Expense.amount > Expense.approver_limit [SOURCE:over_approver_limit]
    FORBID approve_expense WHEN Expense.category == "gift_card" [SOURCE:no_gift_cards]
  }
  OBSERVABILITY {
    LOG "expense approval decided" ON decision
  }
}

valid ExpenseApproval 5 clauses · sha256 5a6326d24470e02c54a4f175994a1b0c2c4a6efa08baec2c025b0af1fc78e7fd

Facts of the first request; each other request changes only the fields shown.

{
  "Expense": {
    "amount": 120,
    "approver_is_submitter": false,
    "approver_limit": 500,
    "receipt_attached": true,
    "category": "travel"
  }
}
RequestActionChangedVerdictClause
within limits, receipt attachedapprove_expenseas abovePERMITExpenseApproval.approve_expense.permit
approver submitted itapprove_expenseapprover_is_submitter: trueDENYExpenseApproval.no_self_approval
above the approver’s limitapprove_expenseamount: 800DENYExpenseApproval.over_approver_limit
no receipt above the thresholdapprove_expensereceipt_attached: falseDENYExpenseApproval.receipt_required
no receipt, under the thresholdapprove_expenseamount: 40 receipt_attached: falsePERMITExpenseApproval.approve_expense.permit
self-approval and no receipt: prohibitions are checked firstapprove_expenseapprover_is_submitter: true receipt_attached: falseDENYExpenseApproval.no_self_approval

2. Facts

Facts are the values of the contract's one entity: { "<Entity>": { "<field>": value, … } }. Before deciding, facts are checked (checkFacts):

decide assumes checked facts. Applications may impose narrower domains; the µLang Git forge checks its facts against its own schema first.

3. Comparisons

ComparisonSemantics
Entity.field == "text" / !=exact string equality, by code points. No case folding, no Unicode normalisation.
Entity.field == true / !=boolean equality
number < <= > >= numberexact IEEE-754 comparison
number == / != numberscaled tolerance, below

Operands evaluate as follows: an integer literal is its exact value, a float literal is its IEEE-754 double, a path is the fact's value, and a constant is its declared value.

3.1 Numeric equality

a == b   ⇔   |a − b| < 2^-52 · max(1, |a|, |b|)
a != b   ⇔   not (a == b)

This tolerates last-bit noise in binary floating point. 0.1 + 0.2 arriving as a fact equals the literal 0.3. Distinct integers below 2^52 stay distinct. Ordering operators are exact, so it is possible for a == b and a < b to hold together when a and b differ in the last bit. Write thresholds with <=/>= when the boundary itself matters.

int and float operands mix freely. Both are numbers.

4. Logic

AND and OR short-circuit, left to right. Evaluation has no side effects, so short-circuiting never changes a result, only its cost.

5. What does not affect a decision

6. Determinism

The same contract, action and facts give the same decision, everywhere. There is no clock, randomness, I/O, locale or hidden state in the evaluation path. The canonical digest identifies the contract that produced a decision, so a decision recorded with the digest can be reproduced later by anyone who holds the same source.

7. Many contracts, one action

An application that holds several contracts routes each action to the contract whose TRIGGER lists it. The command line refuses an action listed by more than one loaded contract, or by none. The µLang Git forge pins every contract to its schema: each TRIGGER must list exactly that contract's actions, and each entity must match the schema's fields and types.