Specification · v1

Executable subset

A contract that parses is not yet a contract that runs. To be valid it must stay inside the executable subset below. Each rule names the diagnostic kind[code] that enforces it. The order in which they are checked is in Validation.

1. Mode

ENFORCEMENT_MODE: enforced is required (semantic[enforcement-mode]). A contract without a mode is refused the same way.

advisory and audit, in lower or upper case, parse. ulang-lite refuses them unless the caller turns the requirement off (requireEnforced: false). The mode never changes a decision: evaluation is the same in every mode.

2. CONTEXT: one entity, and its constants

2.1 Constants

Every constant is:

RequirementDiagnostic
tagged with [SOURCE:id]unsupported[constant-form]
of a primitive typeunsupported[type-unsupported]
inside the exact numeric domain (§5)limit[integer-range], limit[float-range]
hard (==, not =)unsupported[constant-invalid]
typed int with an integer literal 0 … 2^53-1, or typed float with a float literal, finite, |v| ≤ 2^53-1 (negative allowed)unsupported[constant-invalid]
named [A-Za-z0-9_]{1,80} and uniqueunsupported[constant-invalid]
not named like a fieldsemantic[constant-collides]

bool and string constants are not executable. Write those values as literals in the condition.

Constants are not facts. They are named numbers that conditions compare facts against.

Constants: hard, typed, sourced; floats may be negative or use an exponentconstants
CONTRACT Thermostat {
  ENFORCEMENT_MODE: enforced
  CONTEXT {
    ENTITIES Reading {
      celsius: float
      sensor_ok: bool
    }
    CONSTANTS {
      floor_c: float == -40.0 [SOURCE:SPEC-7]
      ceiling_c: float == 1.2e2 [SOURCE:SPEC-7]
    }
  }
  TRIGGER {
    accept_reading()
  }
  GUARANTEE {
    ENSURE accept_reading() WHEN Reading.sensor_ok == true [SOURCE:sensor_healthy]
  }
  PROHIBITION {
    FORBID accept_reading WHEN Reading.celsius < floor_c OR Reading.celsius > ceiling_c [SOURCE:out_of_range]
  }
}

valid Thermostat 2 clauses · sha256 46b2bd81659c2d927d909f163fe452baf44c8a0057eaf2ce646c24a80b8ba53a

3. TRIGGER and clauses

The source id is the clause id. Decisions name clauses as <Contract>.<id>, so ids are stable public names: renaming one changes what every decision that cites it says.

4. Conditions

A condition is an AND/OR tree of typed comparisons.

Operands are literals, constant names, and Entity.field paths on the contract's own entity:

OperandDiagnostic if wrong
Entity.field on the facts entity, declared fieldunsupported[unknown-field]
Other.field (another entity)unsupported[foreign-reference]
a declared constantunsupported[unknown-name]
a parenthesised value (x < (5))unsupported[foreign-reference]
a call or a subtractionunsupported[call-or-arithmetic]

Comparisons must type-check:

Operand kindsAllowed operatorsAllowed shapes
number and number (int and float mix freely)all sixany
string and string==, !=Entity.field op "literal" only
bool and bool==, !=Entity.field op true/false only

Anything else is unsupported[kind-mismatch], or unsupported[nonnumeric-comparison] for strings and booleans in another shape. constant op literal (zero == 0) is unsupported[literal-comparison]: a constant is not a fact, and comparing it with a literal decides nothing.

A condition must be a comparison. A bare operand (WHEN Request.ready) or an assignment (WHEN Request.ready = true) is unsupported[condition-form]. Write Request.ready == true.

NOT, +, *, / and ^ never reach validation: the bounds scan refuses them (see Syntax §7).

The condition tree may be at most 16 levels deep by default, counting each comparison as a level (limit[condition-depth]). Parentheses add no depth. Because AND and OR associate to the left, a flat chain c₁ AND c₂ AND … AND cₙ is n levels deep: 16 comparisons with 15 operators is the longest flat chain allowed.

16 comparisons in a flat AND chain: depth 16, accepteddepth-16
CONTRACT Claim {
  ENFORCEMENT_MODE: enforced
  CONTEXT {
    ENTITIES Request {
      count: int
      region: string
      ready: bool
      ratio: float
    }
    CONSTANTS {
      zero: int == 0 [SOURCE:POLICY]
      cap: int == 25 [SOURCE:POLICY]
    }
  }
  TRIGGER {
    grant()
  }
  GUARANTEE {
    ENSURE grant() WHEN Request.ready == true [SOURCE:must_be_ready]
  }
  PROHIBITION {
    FORBID grant WHEN Request.count != 0 AND Request.count != 1 AND Request.count != 2 AND Request.count != 3 AND Request.count != 4 AND Request.count != 5 AND Request.count != 6 AND Request.count != 7 AND Request.count != 8 AND Request.count != 9 AND Request.count != 10 AND Request.count != 11 AND Request.count != 12 AND Request.count != 13 AND Request.count != 14 AND Request.count != 15 [SOURCE:long_chain]
  }
}

valid Claim 2 clauses · sha256 cd6fcc39d39a158e1a07629a7421a2fd2d6ff8db20478bb73bb6d72a0baa2be8

17 comparisons: depth 17, refuseddepth-17
CONTRACT Claim {
  ENFORCEMENT_MODE: enforced
  CONTEXT {
    ENTITIES Request {
      count: int
      region: string
      ready: bool
      ratio: float
    }
    CONSTANTS {
      zero: int == 0 [SOURCE:POLICY]
      cap: int == 25 [SOURCE:POLICY]
    }
  }
  TRIGGER {
    grant()
  }
  GUARANTEE {
    ENSURE grant() WHEN Request.ready == true [SOURCE:must_be_ready]
  }
  PROHIBITION {
    FORBID grant WHEN Request.count != 0 AND Request.count != 1 AND Request.count != 2 AND Request.count != 3 AND Request.count != 4 AND Request.count != 5 AND Request.count != 6 AND Request.count != 7 AND Request.count != 8 AND Request.count != 9 AND Request.count != 10 AND Request.count != 11 AND Request.count != 12 AND Request.count != 13 AND Request.count != 14 AND Request.count != 15 AND Request.count != 16 [SOURCE:long_chain]
  }
}

rejected limit[condition-depth] at 22:23 condition depth exceeded

5. The exact numeric domain

Every literal stays inside the range where every JSON implementation represents numbers exactly:

LiteralDomainDiagnostic
integer|n| ≤ 2^53-1 (9,007,199,254,740,991)limit[integer-range]
floatfinite, |v| ≤ 2^53-1limit[float-range]

Integer constants are narrower still: 0 … 2^53-1. Integer literals in conditions may be negative.

6. What the subset buys

A contract inside the subset is a finite list of comparisons over one record of typed facts. It cannot loop, call out, allocate or depend on anything except the facts, so a decision always terminates, in time linear in the contract's size. That is the point of the subset: it is the part of µLang that can sit in an execution path where every effect needs a permit.