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 = {}): CompileResult

Decide 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): boolean

CompiledContract 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): CompiledContract

checkFacts 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 | null

Parse 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 = {}): ParseResult

parse function

Parse or throw UlangError.

export function parse(source: string, options: ParseOptions = {}): SourceFile

Validate 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): Kind

validIdentifier function

Clause and constant identifiers: 1..80 ASCII letters, digits or underscores.

export function validIdentifier(id: string): boolean

checkStructure 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 | null

ConstantInfo 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> | Diagnostic

Scope 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 | null

ValidateOptions 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 | Diagnostic

validateFile function

Full language-level check of a parsed file: subset, structure, and every contract (see validateContract).

export function validateFile(file: SourceFile, options: ValidateOptions = {}): ValidContract[] | Diagnostic

Executable 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): boolean

floatInRange function

export function floatInRange(value: number): boolean

checkSupport 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 | null

Canonical 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): string

formatFile function

export function formatFile(file: SourceFile): string

contractDigest function

SHA-256 (lowercase hex) of the canonical text of a contract.

export function contractDigest(c: Contract): string

formatFloat function

Positional (no exponent) shortest round-trip spelling of a float, always with a fractional part.

export function formatFloat(value: number): string

canonicalJson function

Key-sorted JSON of the contract (no offsets, no comments).

export function canonicalJson(c: Contract): string

Limits 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> = {}): Limits

scanBounds 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 | null

Diagnostics 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): Diagnostic

formatDiagnostic function

Render file:line:column: kind[code]: message (the CLI format).

export function formatDiagnostic(d: Diagnostic, file = '<source>'): string

SHA-256 src/sha256.ts

Sha256 class

export class Sha256 {
  update(data: Uint8Array): this;
  digest(): Uint8Array;
}

toHex function

export function toHex(bytes: Uint8Array): string

sha256Hex function

Lowercase hex SHA-256 of bytes, or of a string encoded as UTF-8.

export function sha256Hex(data: Uint8Array | string): string

Text 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): string

utf8Length 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): number

codePointLength function

Number of Unicode scalar values (code points) in text.

export function codePointLength(text: string): number

compareCodePoints 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): number

lineColumn 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