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
CONTEXTis required (semantic[context-required]).- It holds exactly one
ENTITIESblock, and the block is named (semantic[entity]). That name is the contract's facts entity. - Every field has a primitive type:
int,float,boolorstring(unsupported[type-unsupported]). Field names are unique (semantic[fields]). - There is at most one
CONSTANTSblock, and it is anonymous (unsupported[constants-named]).
2.1 Constants
Every constant is:
| Requirement | Diagnostic |
|---|---|
tagged with [SOURCE:id] | unsupported[constant-form] |
| of a primitive type | unsupported[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 unique | unsupported[constant-invalid] |
| not named like a field | semantic[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.
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
TRIGGERis required (semantic[trigger-required]). It lists each governed action once, asaction(): parentheses, no parameters, noWHENguard (unsupported[trigger-form],unsupported[trigger-params],semantic[trigger-entry]).- A guarantee clause is exactly
ENSURE action() WHEN condition [SOURCE:id](unsupported[ensure-form]). - A prohibition clause is
FORBID action WHEN condition [SOURCE:id].FORBID action()means the same (unsupported[forbid-form]). - A
GUARANTEEblock is not empty (semantic[empty-guarantee]). An empty or missingPROHIBITIONis allowed. - A clause's action must be listed in
TRIGGER, and its source id must be[A-Za-z0-9_]{1,80}, written as exactly[SOURCE:(no space after[) and unique within the contract, acrossENSUREandFORBID(semantic[clause-id]). - Every action in
TRIGGERhas at least one clause (semantic[ungoverned-action]). An action with no rule cannot be listed.
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:
| Operand | Diagnostic if wrong |
|---|---|
Entity.field on the facts entity, declared field | unsupported[unknown-field] |
Other.field (another entity) | unsupported[foreign-reference] |
| a declared constant | unsupported[unknown-name] |
a parenthesised value (x < (5)) | unsupported[foreign-reference] |
| a call or a subtraction | unsupported[call-or-arithmetic] |
Comparisons must type-check:
| Operand kinds | Allowed operators | Allowed shapes |
|---|---|---|
number and number (int and float mix freely) | all six | any |
| 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.
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
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:
| Literal | Domain | Diagnostic |
|---|---|---|
| integer | |n| ≤ 2^53-1 (9,007,199,254,740,991) | limit[integer-range] |
| float | finite, |v| ≤ 2^53-1 | limit[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.