Reference
API reference
Generated from the source of ulang-lite 61af4281ed: every signature and comment below is copied from the release when the site is built, and links to its line on GitHub.
Typical use
import { checkFacts, compileSource, contractDigest } from './vendor/ulang-lite/src/index.ts';
const result = compileSource(source); // bounds scan, parse, subset, validate, compile
if (!result.ok) throw new Error(`${result.diagnostic.kind}[${result.diagnostic.code}]`);
const [contract] = result.contracts; // CompiledContract: name, entity, actions, decide()
const [valid] = result.valid; // ValidContract: the AST with its resolved scope
const problem = checkFacts(valid, facts); // null when the facts fit the entity
const decision = contract.decide('deploy', facts[contract.entity]);
const digest = contractDigest(valid.contract); // SHA-256 of the canonical text
The example project's loadPolicy wraps exactly this.
Compile src/index.ts
CompileResult type
export type CompileResult =
| { readonly ok: true; readonly contracts: readonly CompiledContract[]; readonly valid: readonly ValidContract[] }
| { readonly ok: false; readonly diagnostic: Diagnostic };compileSource function
One call: bounds scan, parse, subset check, validation and compilation of every contract in source.
export function compileSource(source: string, options: ParseOptions & ValidateOptions = {}): CompileResultDecide src/evaluate.ts
FactValue type
export type FactValue = number | string | boolean;EntityFacts type
The facts of one entity: field name -> value.
export type EntityFacts = Readonly<Record<string, FactValue>>;Verdict type
export type Verdict = 'PERMIT' | 'DENY';Decision interface
A decision carries only the verdict and the clause that decided it.
export interface Decision { readonly verdict: Verdict; readonly clause_id: string }approxEqual function
Numeric equality: |a - b| < EPSILON * max(1, |a|, |b|) (EPSILON = 2^-52). This tolerates the last-bit noise of binary floating point while keeping every integer in the exact domain (|n| < 2^52) distinct. Ordering operators are exact IEEE-754 comparisons.
export function approxEqual(a: number, b: number): booleanCompiledContract interface
export interface CompiledContract {
readonly name: string;
readonly entity: string;
readonly actions: ReadonlySet<string>;
/**
* Decide `action`: DENY with the first FORBID of that action (source order) whose condition holds; otherwise
* DENY with the first ENSURE of that action whose condition fails; otherwise PERMIT `<Contract>.<action>.permit`.
* `facts` must already satisfy the entity's types (see checkFacts).
*/
decide(action: string, facts: EntityFacts): Decision;
}
export function compileContract(valid: ValidContract): CompiledContract {
const forbids = new Map<string, { id: string; test: Predicate }[]>(), ensures = new Map<string, { id: string; test: Predicate }[]>();
const add = (map: typeof forbids, action: string, id: string, test: Predicate) => { const list = map.get(action) ?? []; list.push({ id, test }); map.set(action, list); };
for (const def of valid.contract.guarantee?.defs ?? []) add(ensures, def.action, def.source!.id, compile(def.condition!, valid));
for (const def of valid.contract.prohibition?.defs ?? []) add(forbids, def.action, def.source!.id, compile(def.condition!, valid));
const name = valid.contract.name;
return {
name, entity: valid.entity, actions: valid.actions,
decide(action, facts) {
if (!valid.actions.has(action)) throw new RangeError(`action ${action} is not triggered by ${name}`);
for (const clause of forbids.get(action) ?? []) if (clause.test(facts)) return { verdict: 'DENY', clause_id: `${name}.${clause.id}` };
for (const clause of ensures.get(action) ?? []) if (!clause.test(facts)) return { verdict: 'DENY', clause_id: `${name}.${clause.id}` };
return { verdict: 'PERMIT', clause_id: `${name}.${action}.permit` };
},
};
}
/**
* Language-level facts check for standalone use: `facts` must be `{ <Entity>: { ...every declared field... } }`
* with no extra keys; int = integer in -(2^53-1)..2^53-1, float = finite number, bool = boolean, string = string.
* Returns null when valid, else a message. (Applications may impose stricter domains, as the forge does.)
*/
export function checkFacts(valid: ValidContract, facts: unknown): string | null {
if (!facts || typeof facts !== 'object' || Array.isArray(facts)) return 'facts must be an object';
const keys = Object.keys(facts);
if (keys.length !== 1 || keys[0] !== valid.entity) return `facts must contain only ${valid.entity}`;
const values = (facts as Record<string, unknown>)[valid.entity];
if (!values || typeof values !== 'object' || Array.isArray(values)) return `${valid.entity} must be an object`;
const record = values as Record<string, unknown>;
for (const key of Object.keys(record)) if (!valid.fields.has(key)) return `unknown field ${valid.entity}.${key}`;
const declared = new Map<string, string>();
for (const field of valid.contract.context!.entities[0]!.fields) declared.set(field.name, field.type.primitive!);
for (const [name, kind] of valid.fields) {
if (!Object.hasOwn(record, name)) return `missing ${valid.entity}.${name}`;
if (!fits(record[name], kind, declared.get(name)!)) return `invalid type or domain for ${valid.entity}.${name}`;
}
return null;
}
function fits(value: unknown, kind: Kind, primitive: string): boolean {
if (kind === 'bool') return typeof value === 'boolean';
if (kind === 'string') return typeof value === 'string';
if (typeof value !== 'number' || !Number.isFinite(value)) return false;
return primitive === 'float' || (Number.isInteger(value) && Math.abs(value) <= MAX_SAFE_INTEGER);
}compileContract function
export function compileContract(valid: ValidContract): CompiledContractcheckFacts function
Language-level facts check for standalone use: facts must be { <Entity>: { ...every declared field... } } with no extra keys; int = integer in -(2^53-1)..2^53-1, float = finite number, bool = boolean, string = string. Returns null when valid, else a message. (Applications may impose stricter domains, as the forge does.)
export function checkFacts(valid: ValidContract, facts: unknown): string | nullParse src/parser.ts
ParseOptions interface
export interface ParseOptions {
/** Override resource limits (they can be lowered, never raised past HARD_LIMITS). */
readonly limits?: Partial<Limits>;
}ParseResult type
export type ParseResult = { readonly ok: true; readonly file: SourceFile } | { readonly ok: false; readonly diagnostic: Diagnostic };parseSource function
Parse without throwing: runs the linear bounds scan first, then the recursive-descent parser.
export function parseSource(source: string, options: ParseOptions = {}): ParseResultparse function
Parse or throw UlangError.
export function parse(source: string, options: ParseOptions = {}): SourceFileValidate src/validate.ts
Kind type
Value kinds the evaluator compares: int and float facts and constants are both numbers.
export type Kind = 'number' | 'bool' | 'string';kindOf function
export function kindOf(primitive: Primitive): KindvalidIdentifier function
Clause and constant identifiers: 1..80 ASCII letters, digits or underscores.
export function validIdentifier(id: string): booleancheckStructure function
Structural well-formedness that holds for every contract in a file: a GUARANTEE block must not be empty.
export function checkStructure(file: SourceFile): Diagnostic | nullConstantInfo interface
export interface ConstantInfo { readonly kind: Kind; readonly value: number }checkConstants function
Constants must be hard (==), typed int or float, carry a value of exactly that type within the exact numeric domain (ints 0..2^53-1, floats finite with |v| <= 2^53-1), have a valid unique name and a SOURCE tag.
export function checkConstants(source: string, defs: readonly Constant[]): Map<string, ConstantInfo> | DiagnosticScope interface
export interface Scope {
/** The contract's facts entity: only `Entity.field` paths on this entity resolve. */
readonly entity: string;
readonly fields: ReadonlyMap<string, Kind>;
readonly constants: ReadonlyMap<string, { readonly kind: Kind }>;
readonly maxDepth?: number;
}
/**
* Type-check one clause condition (depth-first, left to right, the first problem wins). Comparisons must be
* typed: numbers compare with any operator; strings and booleans only with `==`/`!=` and only as
* `Entity.field op literal`. A bare name compared with a literal is refused (constants are not fact fields).
*/
export function checkCondition(source: string, cond: Condition, scope: Scope, depth = 1): Diagnostic | null {
const maxDepth = scope.maxDepth ?? DEFAULT_LIMITS.maxConditionDepth;
if (depth > maxDepth) return diagnostic(source, 'limit', 'condition-depth', 'condition depth exceeded', cond.span.start);
switch (cond.kind) {
case 'and': case 'or':
return checkCondition(source, cond.left, scope, depth + 1) ?? checkCondition(source, cond.right, scope, depth + 1);
case 'operand': case 'assign':
return diagnostic(source, 'unsupported', 'condition-form', 'only typed comparisons and AND/OR are supported', cond.span.start);
case 'compare': {
const unsupported = (code: string, message: string) => diagnostic(source, 'unsupported', code, message, cond.span.start);
if (cond.left.kind === 'name' && isLiteral(cond.right)) return unsupported('literal-comparison', 'literal comparisons must name a fact field');
const a = operandKind(source, cond.left, scope);
if (typeof a !== 'string') return a;
const b = operandKind(source, cond.right, scope);
if (typeof b !== 'string') return b;
if (a !== b || (a !== 'number' && cond.op !== '==' && cond.op !== '!=')) return unsupported('kind-mismatch', 'comparison type mismatch or ordering of nonnumbers');
if (!(cond.left.kind === 'path' && isLiteral(cond.right)) && a !== 'number') return unsupported('nonnumeric-comparison', 'nonnumeric comparisons require a field and literal');
return null;
}
}
}
function operandKind(source: string, op: Operand, scope: Scope): Kind | Diagnostic {
switch (op.kind) {
case 'int': case 'float': return 'number';
case 'string': return 'string';
case 'bool': return 'bool';
case 'path':
if (op.entity === scope.entity) return scope.fields.get(op.field) ?? diagnostic(source, 'unsupported', 'unknown-field', `unknown field ${op.entity}.${op.field}`, op.span.start);
return diagnostic(source, 'unsupported', 'foreign-reference', 'arithmetic, functions and foreign fields unsupported', op.span.start);
case 'name': return scope.constants.get(op.text)?.kind ?? diagnostic(source, 'unsupported', 'unknown-name', `unknown name ${op.text}`, op.span.start);
default: return diagnostic(source, 'unsupported', 'foreign-reference', 'arithmetic, functions and foreign fields unsupported', op.span.start);
}
}
export interface ValidateOptions {
/** Require `ENFORCEMENT_MODE: enforced` (the forge requires it). Default true. */
readonly requireEnforced?: boolean;
/** Expected entity name; default: the contract's single ENTITIES name. */
readonly entity?: string;
/** Exact field set the entity must declare (name -> primitive). */
readonly fields?: ReadonlyMap<string, Primitive>;
/** Exact action set TRIGGER must list. Default: whatever TRIGGER lists. */
readonly actions?: ReadonlySet<string>;
readonly maxConditionDepth?: number;
}checkCondition function
Type-check one clause condition (depth-first, left to right, the first problem wins). Comparisons must be typed: numbers compare with any operator; strings and booleans only with ==/!= and only as Entity.field op literal. A bare name compared with a literal is refused (constants are not fact fields).
export function checkCondition(source: string, cond: Condition, scope: Scope, depth = 1): Diagnostic | nullValidateOptions interface
export interface ValidateOptions {
/** Require `ENFORCEMENT_MODE: enforced` (the forge requires it). Default true. */
readonly requireEnforced?: boolean;
/** Expected entity name; default: the contract's single ENTITIES name. */
readonly entity?: string;
/** Exact field set the entity must declare (name -> primitive). */
readonly fields?: ReadonlyMap<string, Primitive>;
/** Exact action set TRIGGER must list. Default: whatever TRIGGER lists. */
readonly actions?: ReadonlySet<string>;
readonly maxConditionDepth?: number;
}
/** A contract that passed validation, with resolved scope; ready to compile for evaluation. */
export interface ValidContract {
readonly contract: Contract;
readonly entity: string;
readonly fields: ReadonlyMap<string, Kind>;
readonly constants: ReadonlyMap<string, ConstantInfo>;
readonly actions: ReadonlySet<string>;
readonly clauseCount: number;
}
/**
* Validate one contract of a parsed file (the file must already have passed checkSupport and checkStructure).
* Order: mode, CONTEXT, entity, fields, constants, TRIGGER, then every clause (SOURCE id, action, condition),
* then every TRIGGER action must have at least one clause.
*/
export function validateContract(source: string, contract: Contract, options: ValidateOptions = {}): ValidContract | Diagnostic {
const semantic = (code: string, message: string, offset = contract.span.start) => diagnostic(source, 'semantic', code, message, offset);
if ((options.requireEnforced ?? true) && contract.mode?.value !== 'enforced')
return semantic('enforcement-mode', 'all contracts must use ENFORCEMENT_MODE: enforced', contract.mode?.span.start ?? contract.span.start);
const context = contract.context;
if (!context) return semantic('context-required', 'CONTEXT required');
const entityName = options.entity ?? context.entities[0]?.name ?? null;
if (context.entities.length !== 1 || entityName === null || context.entities[0]!.name !== entityName)
return semantic('entity', 'wrong facts entity', context.span.start);
const entity = context.entities[0]!;
const declared = new Map<string, Primitive>();
for (const field of entity.fields) if (field.type.primitive && !declared.has(field.name)) declared.set(field.name, field.type.primitive);
const expected = options.fields ?? declared;
if (entity.fields.length !== expected.size || declared.size !== entity.fields.length || entity.fields.some(f => expected.get(f.name) !== f.type.primitive))
return semantic('fields', `CONTEXT must exactly match ${entityName} facts schema`, entity.span.start);
const fields = new Map([...expected].map(([name, primitive]) => [name, kindOf(primitive)] as const));
const constants = checkConstants(source, context.constants?.defs ?? []);
if (!(constants instanceof Map)) return constants;
for (const name of constants.keys()) if (fields.has(name)) return semantic('constant-collides', 'constant collides with field', context.constants!.span.start);
if (!contract.trigger) return semantic('trigger-required', 'explicit TRIGGER action list required');
const triggers = new Set<string>();
for (const event of contract.trigger.events) {
if (event.guard || event.args === null || (options.actions && !options.actions.has(event.name)) || triggers.has(event.name))
return semantic('trigger-entry', 'TRIGGER contains duplicate, guarded, unsupported or unknown action', event.span.start);
triggers.add(event.name);
}
if (options.actions && (triggers.size !== options.actions.size || [...options.actions].some(a => !triggers.has(a))))
return semantic('trigger-incomplete', 'TRIGGER must list all actions for its contract', contract.trigger.span.start);
const ids = new Set<string>(), governed = new Set<string>();
const scope: Scope = { entity: entityName, fields, constants, maxDepth: options.maxConditionDepth };
let clauseCount = 0;
for (const def of [...(contract.guarantee?.defs ?? []), ...(contract.prohibition?.defs ?? [])]) {
if (!def.source) return semantic('clause-source', 'every executable clause requires a named SOURCE ID', def.span.start);
if (!def.source.exact || !validIdentifier(def.source.id) || ids.has(def.source.id) || !triggers.has(def.action))
return semantic('clause-id', 'invalid/duplicate clause ID or unknown clause action', def.span.start);
ids.add(def.source.id);
governed.add(def.action);
if (!def.condition) return semantic('clause-predicate', 'FORBID predicate required', def.span.start);
const problem = checkCondition(source, def.condition, scope);
if (problem) return problem;
clauseCount++;
}
if (governed.size !== triggers.size || [...triggers].some(a => !governed.has(a)))
return semantic('ungoverned-action', 'every trigger action requires an executable clause', contract.trigger.span.start);
return { contract, entity: entityName, fields, constants, actions: triggers, clauseCount };
}
/** Full language-level check of a parsed file: subset, structure, and every contract (see validateContract). */
export function validateFile(file: SourceFile, options: ValidateOptions = {}): ValidContract[] | Diagnostic {
const support = checkSupport(file) ?? checkStructure(file);
if (support) return support;
const out: ValidContract[] = [];
const names = new Set<string>();
for (const contract of file.contracts) {
if (names.has(contract.name)) return diagnostic(file.source, 'semantic', 'duplicate-contract', `duplicate contract ${contract.name}`, contract.span.start);
names.add(contract.name);
const valid = validateContract(file.source, contract, options);
if (!('contract' in valid)) return valid;
out.push(valid);
}
return out;
}ValidContract interface
A contract that passed validation, with resolved scope; ready to compile for evaluation.
export interface ValidContract {
readonly contract: Contract;
readonly entity: string;
readonly fields: ReadonlyMap<string, Kind>;
readonly constants: ReadonlyMap<string, ConstantInfo>;
readonly actions: ReadonlySet<string>;
readonly clauseCount: number;
}validateContract function
Validate one contract of a parsed file (the file must already have passed checkSupport and checkStructure). Order: mode, CONTEXT, entity, fields, constants, TRIGGER, then every clause (SOURCE id, action, condition), then every TRIGGER action must have at least one clause.
export function validateContract(source: string, contract: Contract, options: ValidateOptions = {}): ValidContract | DiagnosticvalidateFile function
Full language-level check of a parsed file: subset, structure, and every contract (see validateContract).
export function validateFile(file: SourceFile, options: ValidateOptions = {}): ValidContract[] | DiagnosticExecutable subset src/support.ts
MAX_SAFE_INTEGER const
Largest integer every JSON implementation represents exactly: 2^53 - 1.
export const MAX_SAFE_INTEGER = 9_007_199_254_740_991;integerInRange function
Integer literal text within the exact JSON domain (|n| <= 2^53 - 1).
export function integerInRange(text: string): booleanfloatInRange function
export function floatInRange(value: number): booleancheckSupport function
Executable-subset check over a parsed file, in source pre-order (a construct is reported before anything nested inside it). Returns the first construct that parses but cannot execute, or a literal outside the exact numeric domain. Name resolution and typing are separate (see validate.ts).
export function checkSupport(file: SourceFile): Diagnostic | nullCanonical forms src/canonical.ts
formatContract function
Canonical forms. formatContract prints a contract in one deterministic layout (two-space indentation, sections in grammar order, one item per line, comments dropped, empty CONSTANTS/PROHIBITION/OBSERVABILITY omitted, numbers normalized, minimal parentheses that preserve the condition tree). Parsing the canonical text yields the same tree, and two contracts that differ only in layout, comments or number spelling print the same. canonicalJson is the same contract as key-sorted JSON; contractDigest is the SHA-256 of the canonical text. Canonical forms are defined for contracts that pass validation.
export function formatContract(c: Contract): stringformatFile function
export function formatFile(file: SourceFile): stringcontractDigest function
SHA-256 (lowercase hex) of the canonical text of a contract.
export function contractDigest(c: Contract): stringformatFloat function
Positional (no exponent) shortest round-trip spelling of a float, always with a fractional part.
export function formatFloat(value: number): stringcanonicalJson function
Key-sorted JSON of the contract (no offsets, no comments).
export function canonicalJson(c: Contract): stringLimits src/limits.ts
Limits interface
Resource bounds. The bounds scan runs BEFORE any recursive parsing, in one linear pass, so hostile input (huge files, deep nesting, operator floods) is refused in O(n) time with O(1) state. The parser's own recursion is bounded by maxNesting, and condition trees by maxConditionDepth.
export interface Limits {
/** Largest accepted source, in UTF-8 bytes. */
readonly maxSourceBytes: number;
/** Deepest `{ ( [` nesting outside strings, comments and `[SOURCE:...]` tags. */
readonly maxNesting: number;
/** Most word tokens (runs of letters/digits starting with a letter). */
readonly maxTokens: number;
/** Most `AND`/`OR` occurrences (matched as substrings, so `FORBID` counts once too). */
readonly maxBooleanOperators: number;
/** Deepest AND/OR tree in one clause (parentheses do not add depth). */
readonly maxConditionDepth: number;
}DEFAULT_LIMITS const
export const DEFAULT_LIMITS: Readonly<Limits> = Object.freeze({
maxSourceBytes: 65_536, maxNesting: 20, maxTokens: 6_000, maxBooleanOperators: 256, maxConditionDepth: 16,
});HARD_LIMITS const
Absolute ceilings: limits may be lowered freely but never raised past these (stack and time safety).
export const HARD_LIMITS: Readonly<Limits> = Object.freeze({
maxSourceBytes: 16 * 1024 * 1024, maxNesting: 256, maxTokens: 1_000_000, maxBooleanOperators: 100_000, maxConditionDepth: 256,
});resolveLimits function
export function resolveLimits(overrides: Partial<Limits> = {}): LimitsscanBounds function
Linear pre-parse scan. Refuses (in source order, first hit wins): empty/oversized input; the NOT keyword; arithmetic operators (+ * / ^, and - unless it starts a negative number literal); more than maxBooleanOperators AND/OR; nesting deeper than maxNesting or more than maxTokens words. Strings, #/// comments and [SOURCE:...] tags are skipped.
export function scanBounds(source: string, limits: Limits = DEFAULT_LIMITS): Diagnostic | nullDiagnostics src/diagnostics.ts
DiagnosticKind type
Diagnostic kinds. Every rejection carries exactly one kind and one stable code: - syntax the text is not ulang-lite v1 contract syntax; - limit a resource bound was exceeded (input size, nesting, tokens, literal range, condition depth); - unsupported the construct parses but is outside the executable v1 subset; - semantic the contract is well formed but inconsistent (types, names, clause ids, actions, mode).
export type DiagnosticKind = 'syntax' | 'limit' | 'unsupported' | 'semantic';Diagnostic interface
export interface Diagnostic {
readonly kind: DiagnosticKind;
/** Stable machine-readable code (see README "Diagnostics"). */
readonly code: string;
readonly message: string;
/** UTF-16 offset into the source the diagnostic points at. */
readonly offset: number;
/** 1-based line. */
readonly line: number;
/** 1-based column, counted in Unicode code points. */
readonly column: number;
}UlangError class
export class UlangError extends Error {
readonly diagnostic: Diagnostic;
constructor(diagnostic: Diagnostic);
}diagnostic function
export function diagnostic(source: string, kind: DiagnosticKind, code: string, message: string, offset: number): DiagnosticformatDiagnostic function
Render file:line:column: kind[code]: message (the CLI format).
export function formatDiagnostic(d: Diagnostic, file = '<source>'): stringSHA-256 src/sha256.ts
Sha256 class
export class Sha256 {
update(data: Uint8Array): this;
digest(): Uint8Array;
}toHex function
export function toHex(bytes: Uint8Array): stringsha256Hex function
Lowercase hex SHA-256 of bytes, or of a string encoded as UTF-8.
export function sha256Hex(data: Uint8Array | string): stringText src/text.ts
trimWhiteSpace function
Trim Unicode White_Space from both ends (String.prototype.trim also strips U+FEFF, which is not white space).
export function trimWhiteSpace(text: string): stringutf8Length function
Number of UTF-8 bytes needed to encode text (lone surrogates count as U+FFFD, 3 bytes, like TextEncoder).
export function utf8Length(text: string): numbercodePointLength function
Number of Unicode scalar values (code points) in text.
export function codePointLength(text: string): numbercompareCodePoints function
Compare two strings by code point (equivalent to comparing their UTF-8 bytes), not by UTF-16 code unit.
export function compareCodePoints(a: string, b: string): numberlineColumn function
1-based line and column (column in code points) of a UTF-16 offset.
export function lineColumn(source: string, offset: number): { line: number; column: number }AST src/ast.ts
The syntax tree returned by parse and carried by ValidContract.
38 AST types
Span interface
Typed AST of ulang-lite v1 contracts. Offsets are UTF-16 indices into the parsed source. Parentheses are not nodes: AND binds tighter than OR, both associate to the left, and a parenthesized condition is its inner tree (so parentheses never add condition depth).
export interface Span { readonly start: number; readonly end: number }Primitive type
export type Primitive = 'int' | 'float' | 'bool' | 'string';EnforcementMode type
export type EnforcementMode = 'enforced' | 'advisory' | 'audit';CompareOp type
export type CompareOp = '==' | '!=' | '<' | '<=' | '>' | '>=';SourceFile interface
export interface SourceFile { readonly source: string; readonly contracts: readonly Contract[] }Contract interface
export interface Contract {
readonly name: string;
readonly span: Span;
readonly mode: { readonly value: EnforcementMode; readonly text: string; readonly span: Span } | null;
readonly context: Context | null;
readonly trigger: Trigger | null;
readonly guarantee: ClauseBlock<Ensure> | null;
readonly prohibition: ClauseBlock<Forbid> | null;
readonly observability: Observability | null;
}Context interface
export interface Context { readonly span: Span; readonly entities: readonly Entity[]; readonly constants: Constants | null }Entity interface
export interface Entity { readonly name: string | null; readonly span: Span; readonly fields: readonly Field[] }Field interface
export interface Field { readonly name: string; readonly type: TypeRef; readonly span: Span }TypeRef interface
A type reference. text is the type's source text, trimmed; a comment written between a type and the next token is part of that text (and therefore makes the type non-primitive). primitive is set only when text is exactly one of the four primitive names.
export interface TypeRef { readonly text: string; readonly primitive: Primitive | null; readonly args: readonly TypeRef[]; readonly span: Span }Constants interface
export interface Constants { readonly name: string | null; readonly span: Span; readonly defs: readonly Constant[] }Constant interface
export interface Constant {
readonly name: string;
readonly type: TypeRef;
/** `==` declares a hard constant; `=` a soft one (soft constants are outside the executable subset). */
readonly hard: boolean;
readonly value: Literal;
readonly source: SourceTag | null;
readonly span: Span;
}SourceTag interface
[SOURCE:id]. exact is false when the tag does not start with exactly [SOURCE: (e.g. [ SOURCE:x]).
export interface SourceTag { readonly id: string; readonly exact: boolean; readonly span: Span }Trigger interface
export interface Trigger { readonly span: Span; readonly events: readonly TriggerEvent[] }TriggerEvent interface
args is null for a bare name (no parentheses).
export interface TriggerEvent { readonly name: string; readonly args: readonly Operand[] | null; readonly guard: Condition | null; readonly span: Span }ClauseBlock interface
export interface ClauseBlock<T> { readonly span: Span; readonly defs: readonly T[] }Clause interface
export interface Clause {
readonly action: string;
/** null = no parentheses after the action name. */
readonly args: readonly Operand[] | null;
readonly condition: Condition | null;
readonly source: SourceTag | null;
readonly span: Span;
}Ensure interface
export interface Ensure extends Clause { readonly kind: 'ensure' }Forbid interface
export interface Forbid extends Clause { readonly kind: 'forbid' }Observability interface
export interface Observability { readonly span: Span; readonly logs: readonly Log[] }Log interface
export interface Log { readonly message: string; readonly event: string; readonly span: Span }Condition type
export type Condition = Logical | Comparison | BareOperand | Assignment;Logical interface
export interface Logical { readonly kind: 'and' | 'or'; readonly left: Condition; readonly right: Condition; readonly span: Span }Comparison interface
export interface Comparison { readonly kind: 'compare'; readonly left: Operand; readonly op: CompareOp; readonly right: Operand; readonly span: Span }BareOperand interface
A condition that is a single operand (outside the executable subset: comparisons are required).
export interface BareOperand { readonly kind: 'operand'; readonly operand: Operand; readonly span: Span }Assignment interface
word = value: a single = after a name, Entity.field path or boolean word (reference grammar). It parses but is not a comparison, so it is outside the executable subset (condition-form); write == to compare.
export interface Assignment { readonly kind: 'assign'; readonly left: NameRef | PathRef | BoolLiteral; readonly right: Operand; readonly span: Span }Operand type
export type Operand = Literal | NameRef | PathRef | Call | Arithmetic | Group;Literal type
export type Literal = IntLiteral | FloatLiteral | StringLiteral | BoolLiteral;IntLiteral interface
export interface IntLiteral { readonly kind: 'int'; readonly text: string; readonly span: Span }FloatLiteral interface
export interface FloatLiteral { readonly kind: 'float'; readonly text: string; readonly value: number; readonly span: Span }StringLiteral interface
export interface StringLiteral { readonly kind: 'string'; readonly value: string; readonly span: Span }BoolLiteral interface
export interface BoolLiteral { readonly kind: 'bool'; readonly value: boolean; readonly text: string; readonly span: Span }NameRef interface
A bare name: a constant reference. text may carry a trailing comment (see TypeRef).
export interface NameRef { readonly kind: 'name'; readonly text: string; readonly span: Span }PathRef interface
Entity.field (split at the first dot of text).
export interface PathRef { readonly kind: 'path'; readonly text: string; readonly entity: string; readonly field: string; readonly span: Span }Call interface
export interface Call { readonly kind: 'call'; readonly name: string; readonly args: readonly Operand[]; readonly span: Span }Arithmetic interface
a -1: binary minus (the only arithmetic form that survives the bounds scan); outside the subset.
export interface Arithmetic { readonly kind: 'arith'; readonly left: Operand; readonly op: '-'; readonly opOffset: number; readonly right: Operand; readonly span: Span }Group interface
A parenthesized value expression on the right of a comparison, e.g. x < (5); outside the subset.
export interface Group { readonly kind: 'group'; readonly inner: Operand; readonly span: Span }isLiteral function
export function isLiteral(operand: Operand): operand is Literal