Language
Language tour
µLang v1 is small enough to see whole. This page walks through it using the deploy contract from the tutorial. The specification has the precise rules.
Contracts
A source file holds one or more contracts. A contract has up to six sections, always in this order, each at most once:
| Section | Holds | Required |
|---|---|---|
ENFORCEMENT_MODE | enforced | yes |
CONTEXT | the facts entity, then constants | yes |
TRIGGER | the actions the contract governs | yes |
GUARANTEE | ENSURE clauses: what must hold | no |
PROHIBITION | FORBID clauses: what must not hold | no |
OBSERVABILITY | LOG lines for callers | no |
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforcedNames of contracts, fields, actions and constants are [A-Za-z_][A-Za-z0-9_]*. Comments start with // or # and run to the end of the line. ENFORCEMENT_MODE: enforced is required. advisory and audit parse, but ulang-lite refuses them by default, and the mode never changes a decision.
Facts
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}A contract has exactly one entity, and its fields are typed:
| Type | In the facts JSON | Compared |
|---|---|---|
int | an integer, at most 2^53−1 in magnitude | exactly |
float | a finite number | as IEEE-754 doubles |
bool | true or false | by value |
string | a string | exactly, by code point |
The application sends facts as {"DeployRequest": {…}}, with every field present and no others. ulang-lite checks that before deciding.
Actions
TRIGGER {
deploy()
rollback()
}Each governed action is listed once, as name(), and must have at least one rule. Asking a contract about an action it does not list is a mistake in the caller, not a DENY.
Rules
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
} PROHIBITION {
FORBID deploy WHEN DeployRequest.change_freeze == true AND DeployRequest.environment == "production" [SOURCE:change_freeze]
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
FORBID rollback WHEN DeployRequest.environment != "production" AND DeployRequest.environment != "staging" [SOURCE:known_environments]
}FORBID action WHEN condition [SOURCE:id]denies the action when the condition holds.ENSURE action() WHEN condition [SOURCE:id]denies the action when the condition fails.
A decision checks the action's FORBID clauses first, then its ENSURE clauses, each in source order. It names the first clause that denies. If none does, the action is permitted as ProductionDeploy.deploy.permit. See Evaluation.
Conditions
A condition is comparisons joined by AND and OR. AND binds tighter than OR; parentheses group.
| Comparing | Operators | Example |
|---|---|---|
numbers: int, float, or one of each | == != < <= > >= | DeployRequest.error_rate_pct > max_error_rate_pct |
| a string field with a string | == != | DeployRequest.environment == "production" |
a bool field with true or false | == != | DeployRequest.tests_passed == true |
Operands are facts (Entity.field), constants and literals. Two numeric facts can be compared with each other; strings and booleans are always written field op literal.
<, <=, > and >= are exact. == and != tolerate last-bit floating-point noise, so a fact computed as 0.1 + 0.2 equals the literal 0.3. Use <= and >= for thresholds.
Not in the language, on purpose: NOT, arithmetic, function calls, lists and string escapes. Patterns shows how to express the usual needs without them.
Constants
CONSTANTS {
min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
}Constants are typed int (0 to 2^53−1) or float, written with ==, and carry a source. They come after the entity in CONTEXT. Compare them with facts: a constant compared with a literal is refused, because it decides nothing.
Sources and clause ids
Every clause and every constant carries [SOURCE:id]. On a clause, the id is the clause's public name: a decision reads ProductionDeploy.change_freeze. Clause ids are [A-Za-z0-9_]{1,80} and unique within a contract. On a constant, the source is free text that says where the value comes from.
Observability
OBSERVABILITY {
LOG "deploy decision" ON decision
}LOG lines tell callers what to record. They are part of the contract's digest and play no part in decisions.
Canonical form and digest
Every valid contract has one canonical text: a fixed layout, no comments, normalised numbers. Its SHA-256 is the contract's digest. Layout and comments never change the digest; anything that could change a decision does. Record the digest with each decision and you can later prove which contract made it (see Audit trail).
$ ./ulang-lite format policy/deploy.ul
CONTRACT ProductionDeploy {
ENFORCEMENT_MODE: enforced
CONTEXT {
ENTITIES DeployRequest {
environment: string
tests_passed: bool
approvals: int
author_approved_own_change: bool
error_rate_pct: float
change_freeze: bool
}
CONSTANTS {
min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
}
}
TRIGGER {
deploy()
rollback()
}
GUARANTEE {
ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
}
PROHIBITION {
FORBID deploy WHEN DeployRequest.change_freeze == true AND DeployRequest.environment == "production" [SOURCE:change_freeze]
FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
FORBID rollback WHEN DeployRequest.environment != "production" AND DeployRequest.environment != "staging" [SOURCE:known_environments]
}
OBSERVABILITY {
LOG "deploy decision" ON decision
}
}