/** * Program canonicalization. * * Produces a deterministic normal form for parsed programs so that two * programs differing only in cosmetic ways canonicalize identically. Used * primarily to detect whether a viewed program has actually been changed * (fork-or-not on dice.run). * * The pipeline is a fixed sequence of seven passes: * * 1. Strip cosmetic metadata (comments, parameter labels, dice source). * 2. Constant-fold + algebraic identities + dice fold/fusion (to fixpoint). * 3. Sort commutative operands and record fields (name-agnostic key). * 4. Reorder match arms (literal-only, no-guard runs). * 5. Reorder independent statements (rollfree assignments only). * 6. Alpha-rename parameters and assignments. * 7. Re-sort commutative operands using canonical names. * * Public surface: {@link canonicalize}, {@link equivalent}, * {@link canonicalHash}, and {@link CanonicalizeOptions}. * * Design spec: `docs/superpowers/specs/2026-05-03-program-equivalence-design.md`. */ import type { Expression } from './program'; import type { Program } from './program'; import type { DiceExpression } from './dice-expression'; /** * Per-pass enable flags for {@link canonicalize}. All default to `true`, * giving the most aggressive canonicalization (suitable for fork-detection * use cases). Disable individual flags to weaken equivalence. * * See `docs/superpowers/specs/2026-05-03-program-equivalence-design.md` for * the full design rationale. */ export interface CanonicalizeOptions { alphaRename?: boolean; sortCommutative?: boolean; sortRecordFields?: boolean; reorderStatements?: boolean; reorderMatchArms?: boolean; foldConstants?: boolean; applyIdentities?: boolean; normalizeDice?: boolean; normalizeParameters?: boolean; } /** * Produce a deterministic canonical form for a program. * * Two programs canonicalize to the same form iff they are equivalent under * the rules in the design spec: cosmetic differences (comments, whitespace, * variable names, parenthesization) are ignored; commutative operands are * sorted; independent statements are reordered when safe; algebraic * identities and constant folds are applied; certain dice expressions are * fused (e.g. `1d6 + 1d6` ≡ `2d6`). * * The function is conservative: when in doubt, it leaves a difference rather * than collapsing it. False positives (claiming two distinct programs are * equivalent) are considered worse than false negatives. * * @param program - The parsed program AST. * @param options - Per-pass enable flags. Defaults are aggressive. * @returns A new Program AST in canonical form. The input is not mutated. */ export declare function canonicalize(program: Program, options?: CanonicalizeOptions): Program; /** * Returns true iff two programs canonicalize to the same form. * * Equivalent to `canonicalHash(a) === canonicalHash(b)`. Use this for * one-shot comparisons; for repeated comparisons against many candidates * (e.g. caching) prefer {@link canonicalHash} and compare strings. */ export declare function equivalent(a: Program, b: Program, options?: CanonicalizeOptions): boolean; /** * Returns a stable string hash of a program's canonical form. * * Two programs produce the same hash iff they are {@link equivalent}. The * hash is deterministic across runs (stable JSON serialization of the * canonical AST). Useful as a cache key for fork-detection. */ export declare function canonicalHash(program: Program, options?: CanonicalizeOptions): string; /** * Deep clone of an AST with `loc` and `comments` properties stripped at every * level. Used by `canonicalize` and each individual pass to ensure neither * source positions nor comment attachments influence canonical form. * Idempotent. * * Comments are stripped here because `canonicalize` is intentionally * comment-blind: its equivalence-rewriting purpose (constant folding, * commutative sort, alpha-rename, etc.) is unrelated to comment text or * attachment. Two behaviour-equivalent programs that differ only in comments * must hash identically. The pretty-printer's round-trip property uses * {@link equivalentWithComments} instead. */ export declare function stripLocs(value: T): T; /** * Returns true iff two programs have the same AST structure *and* the same * attached comments at every node, ignoring source positions. * * This is the stricter cousin of {@link equivalent}: where `equivalent` * compares canonicalized (behaviour-equivalent) programs and ignores * comments, `equivalentWithComments` compares the *exact* parsed structure * including every comment's text and attachment slot (leading vs trailing). * * Used as the round-trip invariant for the pretty-printer: * `equivalentWithComments(parse(format(p)), p)` must hold for every `p`. * * Not symmetric with `equivalent`: two `equivalent` programs may differ in * comments and so be inequivalent under `equivalentWithComments`; two * `equivalentWithComments` programs are always `equivalent`. */ export declare function equivalentWithComments(a: Program, b: Program): boolean; export declare function stableStringify(value: unknown): string; /** * A "rollfree" expression contains no dice rolling anywhere in its tree — so * it has no observable RNG side effect and can be freely duplicated, dropped, * or reordered by the algebraic passes. * * Expressed as a structural fold over {@link everyChild}: a dice atom is a * rolling boundary (returns `false`); every other node is rollfree exactly * when all of its children are. Because `everyChild` walks the very children * {@link mapChildren} rewrites, this predicate cannot drift out of sync with * the rewrite passes (it now also covers `comprehension.by`, which the former * hand-written walker silently skipped). */ export declare function isRollfree(expr: Expression): boolean; export declare function structuralKey(node: Expression | DiceExpression): string; export declare function serializeDiceCanonical(d: DiceExpression): string; export declare function pass1StripCosmetics(program: Program, options?: CanonicalizeOptions): Program; export declare function pass2FoldConstants(program: Program): Program; export declare function pass2ApplyIdentities(program: Program): Program; /** * Expression-level algebraic simplification: bottom-up constant folding * followed by the rollfree-aware identity rewrites used by canonicalization. * * This is the single source of truth for algebraic simplification. It is the * rollfree-aware folder (`x * 0` collapses only when the dropped operand can * never roll dice), so it never silently discards a subexpression with * observable rolling behaviour. `DE.simplify` in `dice-expression-domain.ts` * delegates here so the two no longer drift. * * `DiceExpression` is a strict subset of `Expression`, so dice-side callers * can pass a `DiceExpression` directly. */ export declare function simplifyExpression(expr: Expression): Expression; export declare function pass2NormalizeDice(program: Program): Program; export declare function pass3SortCommutative(program: Program): Program; export declare function pass4ReorderMatchArms(program: Program): Program; export declare function pass5ReorderStatements(program: Program): Program; export declare function pass6AlphaRename(program: Program, options?: CanonicalizeOptions): Program;