load is an epoch command not an evaluator symbol, so an .sx file can't wrap the load list. Extract the dependency-ordered file list to lib/maude/load-order.txt; conformance.conf reads it via mapfile. 274/274 green. lib/artdag can source the same file to drop its duplicated load lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
25 KiB
Maude-on-SX: rewriting as primitive
Equational logic + term rewriting as the only computational primitive. Every other guest in the set reduces ultimately to lambda terms or stack frames; Maude (Clavel et al.) reduces to rewrite rules over equational classes modulo theories (associativity, commutativity, identity). Implementing it forces the substrate to articulate its reduction semantics — currently implicit in the CEK machine and the JIT.
The chisel: reduction step. Different from Idris's evidence chisel and from Probabilistic's trace chisel. Maude asks: "what is one step of computation?" Maude's answer (apply a rewrite rule, modulo equational theories) is more general than CEK's transition. Making both consistent is informative — either the substrate has room for them to coexist, or one is a special case of the other.
What this exposes about the substrate:
- Whether SX's pattern matching (lib/guest/match.sx) extends to equational matching — matching modulo associativity, commutativity, identity.
- Whether the substrate has a notion of "normal form" or just "result of evaluation."
- Whether term-graph sharing matters at the value-level.
- Whether confluence (different rewrite orders → same result) can be checked or just hoped for.
- Whether order-sorted signatures (subsorts, polymorphism via inheritance) fit SX's value taxonomy.
End-state goal: Maude 3 functional + system modules — sorts, subsorts, equations, conditional equations, rewrite rules, equational matching modulo assoc/comm/id, simple strategy language. Not the full LTL model checker; a faithful core that runs idiomatic Maude programs and proves equational identities.
Ground rules
- Scope:
lib/maude/**andplans/maude-on-sx.mdonly. Substrate gaps →sx-improvements.md. - Consumes from
lib/guest/:core/lex,core/pratt,core/ast,core/match(extended). - Will propose a new sub-layer
lib/guest/rewriting/— equational matching beyond syntactic match, normal-form computation, confluence checking, term-graph rewriting. Second consumer: a Pure-on-SX plan, a CafeOBJ port, or a research term-rewriting playground. - Branch:
loops/maude. Standard worktree pattern.
Architecture sketch
Maude source text (functional / system / object modules)
│
▼
lib/maude/parser.sx — fmod ... endfm syntax, sort declarations,
│ equations, rewrite rules
▼
lib/maude/signatures.sx — sort hierarchy, operator declarations with arities,
│ subsort relationships, kind inference
▼
lib/maude/matching.sx — pattern matching MODULO equational theories
│ (assoc, comm, id) — generalises core/match.sx
▼
lib/maude/reduce.sx — apply equations until normal form (confluent set)
│
▼
lib/maude/rewrite.sx — apply rewrite rules under a strategy (system modules)
│
▼
lib/maude/runtime.sx — module loading, reflection (META-LEVEL)
Semantic mappings
| Maude construct | SX mapping |
|---|---|
sort Nat . |
declare sort: (declare-sort Nat) |
subsort Nat < Int . |
subsort relation: (declare-subsort Nat Int) |
op _+_ : Nat Nat -> Nat [assoc comm id: 0] . |
operator with equational attributes |
eq X + 0 = X . |
equation in the equational theory |
ceq X + Y = Y if foo(X, Y) . |
conditional equation |
rl [step] : foo(X) => bar(X) . |
rewrite rule (asymmetric, in system modules) |
red TERM . |
reduce term to normal form by equations |
rew TERM . |
apply rewrite rules under default strategy |
META-LEVEL |
reflection: terms representing terms |
The novel substrate stress: equational matching. Pattern X + Y against 1 + 2 + 3 (where + is assoc comm) succeeds with multiple binding sets: (X=1, Y=2+3), (X=2, Y=1+3), (X=3, Y=1+2), etc. The matcher must enumerate solutions, not return the first.
Roadmap
Phase 1 — Parser + signatures
- Parser for
fmod/endfmsyntax, sort declarations, op declarations, equations. - Sort hierarchy with subsort relations.
- Operator overloading by arity + sort.
- Tests: parse classic examples (peano nat, list of naturals).
Phase 2 — Syntactic equational reduction
- Apply equations left-to-right until no equation matches.
- Standard pattern matching (no equational theories yet — strict syntactic match).
- Tests: peano arithmetic, list manipulation, propositional logic simplifier.
Phase 3 — Equational matching (assoc / comm / id)
- Extend matching to handle
assocoperators (flatten then match across permutations of subterm groups). - Handle
comm(try both argument orderings). - Handle
id: e(X * e ≡ X). - Combinations:
assoc comm idtogether. - Returns all matches, not just first — caller drives.
- Tests: classic AC-matching examples (multiset rewriting, set theory, group equations).
Phase 4 — Conditional equations
ceq L = R if Cond— apply only whenCondreduces to true.- Recursion via the same reduce engine (terminating because Cond is shorter).
- Tests: gcd, sorting, conditional simplifications.
Phase 5 — System modules + rewrite rules
mod ... endmsyntax withrlrules.- Rules apply asymmetrically (
=>not=); fairness across rules. - Default strategy: top-down, leftmost-outermost, first applicable rule.
- Tests: state-transition systems (puzzle solvers, protocol simulators).
Phase 6 — Strategy language
- Compose strategies: sequential
;, alternative|, iteration*, fixed-point. - User-named strategies; strategy expressions as values.
- Tests: programs whose meaning depends on strategy choice.
Phase 7 — Reflection (META-LEVEL)
- Terms-as-data:
META-LEVELlets you encode/decode terms as Maude terms. - Build proofs / programs that manipulate Maude programs.
- Tests: meta-circular interpretation, generic theorem helpers.
Extensions (post-roadmap, toward the end-state goal)
- Mixfix surface-syntax printer (
lib/maude/pretty.sx) —mau/term->mauderenders the internal prefix form back as Maude mixfix (((s X) + 0)), driven by op forms;mau/red->maude/mau/rew->maude. 11 tests. - Program runner (
lib/maude/run.sx) —mau/run-program/mau/runparse a module plus trailingreduce/red/rewrite/rew TERM .commands (... in MOD : TERMqualifier accepted) and execute them, rendering results in surface syntax. Runs an idiomatic.maudefile end-to-end. Now also:search START =>* GOAL .command (reports the path), least-sort annotated output viamau/run-pretty→result SORT: TERM(Maude-style). 10 tests. - Witness-path search (
lib/maude/searchpath.sx) —mau/search-path/mau/search-lengthreturn the shortest sequence of states start..goal (the solution moves), not just yes/no. 8 tests. - Order-sorted least-sort inference (
lib/maude/sorts.sx) —mau/term-sortcomputes the least sort of a term: the smallest result sort among the op declarations whose argument sorts the actual args satisfy (modulo subsorting), so an overloadedf(1)isNzNatbutf(s 0)isNat.mau/has-sort?for membership-style checks. Answers the plan's substrate question — order- sorted signatures fit cleanly. 14 tests. gather/ parse-time associativity — infix ops parse left (default,(E e)) or right ((e E)) per the gather attr, so cons_:_ [gather (e E)]readsa : b : cas right-nested. Full insertion sort now runs over BARE cons lists (no parens). 7 tests.owiseequations — parser now reads trailing eq attributes (eq L = R [owise] .),mau/split-attrs;mau/crewrite-topis two-pass (ordinary equations first, owise last), so an owise catch-all fires only when nothing else applies, regardless of declaration order. Parser also readslabel/prec/owise/id:eq+op attrs. 8 tests.
Phase 8 — lib/guest/rewriting/ extraction — DESIGN PROPOSAL (gate satisfied; thin-kernel cut REJECTED on evidence)
- Wait for second consumer before extracting. SATISFIED —
lib/artdagis a live, saturated consumer (Phase-7 maude optimiser:optimize-rules.sxparses afmod, reduces, and gates onmau/confluent?). It is the only external consumer; zero Blockers were filed againstlib/maude, so the API held under real use. - [~] Extract equational matching engine — DO NOT, as specified. See below.
- [~] Extract normal-form-by-equations infrastructure — folds into the facade.
- [~] Extract strategy combinators — DO NOT. Zero external consumers.
Status: gate met, mechanical extraction NOT recommended. Before cutting, the consumer was measured. The result overturns the pre-registered plan.
What the plan assumed: the reusable core is matching.sx (AC matching +
canon) + fire.sx (short-circuit firing), with strategy.sx third.
What the second consumer actually imports (measured across lib/artdag/*.sx):
| Layer | Symbols artdag uses | In planned kernel? |
|---|---|---|
term.sx |
mau/app mau/app? mau/const mau/op mau/args |
yes |
parser.sx |
mau/parse-module (artdag writes rules as fmod … endfm text) |
no — left in surface |
conditional.sx |
mau/creduce mau/creduce-term mau/creduce->str mau/ccanon |
partial |
confluence.sx |
mau/confluent? mau/non-joinable-pairs mau/cp->str |
no — added post-plan |
matching.sx / fire.sx / strategy.sx |
none directly | the whole planned kernel |
So the consumer reuses a vertical (term → parser → conditional → confluence),
not the horizontal AC kernel the plan named. It touches none of
matching/fire/strategy directly, and strategy.sx has no consumer at all.
Extracting matching+fire+strategy would produce a "reusable kernel" with nobody
reusing it — extraction-for-its-own-sake, exactly what the gate existed to prevent.
Why the vertical can't be cleanly thinned into a kernel: the kernel
candidates' only coupling to the front-end is 7 parser symbols —
list utils mau/take mau/drop mau/append2 (relocatable, theory-agnostic) and
module accessors mau/module-eqs mau/module-ops mau/module-rules
mau/parse-term-in (the eqs/ops/rules feed + in-signature term parsing). But the
real consumer wants the fmod parser — it authors rules as Maude text. A thin
term-rewriting kernel that excludes the parser does not serve it; a "kernel" that
includes the parser is just lib/maude renamed.
Recommended move (cheap, low-risk, real) — a driver-level manifest, not a
relocation and not an .sx re-export (the .sx facade was tried and proven
infeasible; see FINDING below). The consumed surface is mau/parse-module,
mau/creduce*/mau/ccanon, mau/confluent?/mau/non-joinable-pairs/mau/cp->str,
term ctors/accessors.
FINDING (2026-07, smoke-tested): an .sx-file facade is architecturally
impossible. load is an epoch-protocol command, not an evaluator symbol —
(load "…") forms inside a loaded .sx file error with "Undefined symbol:
load" (verified: a would-be api.sx of (load …) lines fails on every line).
The nested-load seen in common-lisp/tests/runtime.sx works only because that
harness feeds each form as a command, not because load is callable from
evaluated code. So the load list is inherently a driver-level concern
(shell / conformance.conf), and no .sx file can wrap "nine loads into one".
BUILT INSTEAD — lib/maude/load-order.txt (driver-level manifest, the
working form of the facade): the dependency-ordered file list as the single
source of truth. lib/maude/conformance.conf now reads it
(mapfile -t PRELOADS < <(grep -vE '^\s*(#|$)' lib/maude/load-order.txt)) —
proven by the 274 suite staying green. A file rename is now a one-line change,
and lib/artdag's driver can mapfile the same file to drop its hardcoded copy
of these load lines (artdag-side change, its scope — a documented follow-up, not
done here). This delivers the load-list DRY + file-rename freedom that the
.sx facade could not.
Symbol-contract (optional, not built): a pure-manifest mau/api-symbols
list (the ~25 public symbols, no load forms so it loads fine) + an api test
exercising each end-to-end would pin the symbol surface as a tripwire. Modest;
skipped as gilding — the file-level manifest is the higher-value half.
Defer the physical lib/guest/rewriting/ split until a third consumer
diverges from artdag — the boundary should be drawn by two real shapes, not one
plus a guess. The dependency DAG and 7-symbol coupling set above make that cut a
1-session job when justified.
SATURATION (post-roadmap)
All 7 roadmap phases + 7 extensions (pretty / run / search-path / owise /
gather / order-sorted least-sort / search-command + result-sort) DONE, 254/254
across 13 suites. The end-state goal — a faithful Maude 3 functional+system
core that runs idiomatic programs and proves equational identities — is met:
sorts/subsorts/overloading, equational reduction modulo assoc/comm/id,
conditional eqs + owise, system rules (rew + BFS search with witness paths),
a strategy language, and META-LEVEL reflection, with a mixfix surface printer
and an end-to-end .maude runner (reduce/rewrite/search commands, sort-annotated
output). artdag-on-sx fit prototype (lib/maude/tests/effects.sx, 8 tests): artdag's
optimise passes — adjacent-op fusion, no-op/dead-op elim, identity elim,
CSE/idempotent dedup — expressed as eqs; the optimised pipeline IS the normal
form, and confluence ⇒ a stable content id. This is the "second consumer"
spike: it justifies a maude-driven optimiser in lib/artdag and the eventual
lib/guest/rewriting/ extraction. Faithfulness note surfaced: id: only
affects matching/canon, NOT auto-reduction — write explicit identity eqs (or
read off the canonical form) if you need 0 + N to reduce in the term itself.
Confluence / critical-pair checking (lib/maude/confluence.sx, 12 tests):
mau/confluent? answers the plan's substrate question "can confluence be
checked." Two-sided syntactic unification (mau/u-unify, with occurs check) →
critical pairs from LHS overlaps (mau/critical-pairs) → joinability via
mau/ac-equal? of the normal forms (mau/non-joinable-pairs gives the
diagnostics, mau/cp->str renders left <?> right). Caught f(a)=b, a=c as
non-confluent (b <?> f(c)); confirmed peano/idempotent/AC examples confluent.
SCOPE: unification is SYNTACTIC — exact for free/constructor ops, an
under-approximation for AC overlaps (full AC-unification is NP/infinitary, out
of scope), but joinability uses the AC-canonical form so AC laws still join
correctly. This is the CID-stability oracle for the artdag optimiser: an
optimisation rule set is content-id-stable iff mau/confluent? holds.
Pacing down to hardening. Possible niche future work: membership
axioms (mb/cmb), critical-pair / confluence checking, meta-search, full
mixfix (multi-_ ops, juxtaposition __).
lib/guest feedback loop
Consumes: core/lex, core/pratt, core/ast, core/match (with proposed extension for equational matching).
Stresses substrate: matching backtracking and enumeration (Maude's all-matches semantics is very different from Haskell-style first-match); whether SX values can carry sort metadata efficiently; term-graph sharing.
May propose: lib/guest/rewriting/ sub-layer — equational matching (extending core/match), normal-form-by-equations machinery, strategy combinators, confluence checking.
What it teaches: whether the substrate's reduction model has implicit assumptions (deterministic, leftmost-outermost, etc.) that a rewriting language exposes. If core/match.sx cleanly extends to equational matching via a configuration toggle, that's substrate-deep validation. If extending it requires fundamental rework, the substrate's matching abstraction was leaking.
References
- Clavel et al., "All About Maude — A High-Performance Logical Framework" (Springer, 2007).
- Maude Manual: https://maude.lcc.uma.es/
- "Term Rewriting and All That" (Baader & Nipkow, 1998) — theoretical foundation.
- Eker, "Associative-Commutative Rewriting on Large Terms" (RTA 2003) — for the matching algorithm.
- Pure language (Albrecht Gräf): https://agraef.github.io/pure-lang/ — practical functional rewriting.
Progress log
-
Phase 1 (parser + signatures) — DONE, 65/65.
lib/maude/term.sx(term repr: var/app dicts, equality, vars,term->str) +lib/maude/parser.sx(whitespace+bracket tokenizer with---/***comments; mixfix classification by splitting op names on_; precedence-climbing term parser over a pratt table built from op decls;fmod/modmodules with sorts/subsorts/ops/vars/eqs/rules). Consumeslib/guest/lex.sx(ws classes) andlib/guest/pratt.sx(op-table lookup). Verified on Peano (s X + Yparses_+_(s_(X), Y)— prefix binds tighter than infix) and NatList (transitive subsorts NzNat<Nat<List;_;_overloaded;id: nil/precattrs). ceq/rl/crl parsed structurally (cond split onif, label in[..]). Suite + conformance driver wired (lib/maude/conformance.{conf,sh}, MODE=dict).- Notes for next phases: terms are
{:t :app :op N :args (...)}/{:t :var :name N :sort S}; module carries a:grammarsomau/parse-term-incan parse term strings against its op table. Overloading is recorded but NOT resolved at parse time (resolve at reduce time).
- Notes for next phases: terms are
-
Phase 2 (syntactic reduction) — DONE, 91/91 total.
lib/maude/reduce.sx: one-sided syntactic matching (mau/match— pattern vars only, non-linear patterns checked by bound-var equality), immutable substitutions viaassoc,mau/subst-apply, top rewritemau/rewrite-top(first unconditional eq whose LHS matches; conditional eqs skipped until Phase 4), innermost normalisation to a fixpointmau/normalize(args normalised before the operator; fuel- guarded). API:mau/reduce/mau/reduce-term/mau/reduce->str. Tested on Peano (+,*), list ops (append/length/rev), a propositional simplifier, and non-linearsame(X,X). Innermost is fine for confluent terminating eq sets; Phase 3 will replace the matcher with AC-aware matching (multi-valued). -
Phase 3 (matching modulo assoc/comm/id) — DONE, 119/119 total. THE CHISEL.
lib/maude/matching.sx.mau/mmis the multi-valued matcher (returns the full list of substitutions): free=positional, comm=both orderings, assoc=flatten f-spine + ordered sequence match (vars grab contiguous blocks), assoc+comm=multiset match (vars grab sub-multisets viamau/all-splits= 2^n subset/complement pairs).id: elets a var grab the empty block (contributing e);mau/var-kmingives kmin 0 under id.mau/canonis the AC-canonical printout (flatten, drop identities, sort comm args) and powersmau/ac-equal?(used for bound-var checks too). AC rewriting extends each f-AC equation l=r with rest vars — comm:f(l,$R); assoc:f($L,l,$R)— so a rule fires on any sub-multiset/subword ($-prefixed rest vars allowed empty).mau/first-changewalks candidate matches and only commits a rewrite that changes the canonical form — this is what makes idempotency (X U X = X) and identity-absorbing matches terminate. API:mau/ac-reduce/mau/ac-reduce->str/mau/ac-canon/mau/match-all. Verified: AC match counts (X+Y vs a+b+c = 6), bag collapse, set dedup with empty, group cancellation (assoc non-comm + inverse).- Notes for next phases: AC matching is multi-valued — Phase 5 rule
application should iterate ALL of
mau/mm's results, not just first. Themau/ac-rewrite-eqextension trick (rest vars) is the reusable core for a futurelib/guest/rewriting/(Phase 8). Keepmau/canonas the equality oracle.$EMPTYis a transient marker for empty rest blocks w/o id; never leaks pastmau/restv.
- Notes for next phases: AC matching is multi-valued — Phase 5 rule
application should iterate ALL of
-
Phase 4 (conditional equations) — DONE, 138/138 total.
lib/maude/conditional.sxis a condition-aware superset of the Phase 3 reducer.mau/eq-candidatesenumerates (subst, result) pairs for an equation (AC via rest-var extensionmau/ac-candidates, elsemau/mm);mau/try-candidatescommits the first candidate that both makes progress (canonical form changes) AND whose guard holds.mau/cond-holds?evaluates{:kind :eq}guards (reduce both sides,ac-equal?) and{:kind :bool}guards (reduce,=AC= true), recursing throughmau/cnormalize— same reducer, so guards can mention other (conditional) equations. Public:mau/creduce/mau/creduce->str/mau/ccanon. Verified on gcd (subtractive, recursive guard), insertion sort (true/false branches), max, and even (bool-kindif predguard).- Notes for next phases:
mau/creduceis the canonical reducer now; Phase 5 rules reduce to normal form via creduce between rewrite steps._:_cons parses LEFT-assoc (nogathersupport yet) — write list literals right-parenthesized, or add agather/parse-assoc attr later if a test needs barea : b : c.
- Notes for next phases:
-
Phase 5 (system modules + rewrite rules) — DONE, 159/159 total.
lib/maude/rewrite.sx+lib/maude/fire.sx. Rules (rl/crl) reuse the equation firing machinery (a rule dict is shaped like an eq).mau/rewriteis the default strategy: normalise with eqs (creduce), fire ONE rule top-down/leftmost-outermost/first-applicable, renormalise, repeat (bounded by fuel).mau/rew m src n= boundedrew [n].mau/searchis BFS over ALL one-step successors (mau/all-successors) for reachability — solves the branchinggoalreachable only off the pathrewtakes. Verified: AC multiset coin-change (rule on a sub-multiset), cyclic traffic light (bounded), branching nondeterminism (rew vs search), conditionalcrlclock, eq/rule interleaving.- PERF (important):
lib/maude/fire.sxis the short-circuiting matcher —mau/fire-eqfinds the FIRST productive match via predicate-threadedmau/ms-find/mau/seq-findinstead of materialising the whole solution set. Without it, AC rewriting on N identical elements is exponential (q;q;q;q;q;q;q;qwent 60s+ → <1s). The eagermau/match-multiset/mau/eq-candidatesare kept ONLY formau/match-allandsearch(which truly need every solution). Phase 4creduceand Phase 5 rules both fire viamau/fire-eq. Keep this split: never route single-step rewriting through the eager enumerator. - Notes: juxtaposition
__(empty-token mixfix) andgatherare NOT parsed — use an explicit infix op for multisets and right-parenthesise list literals..can't be an op token (statement terminator).mau/searchis the prime Phase 7 reflection / Phase 8 extraction target alongside the matcher.
- PERF (important):
-
Phase 6 (strategy language) — DONE, 178/178 total.
lib/maude/strategy.sx. Strategies are first-class tagged-dict VALUES and set-valued:mau/sapply ctx strat term→ deduped (by canon) list of results. Combinators:idle/fail/all/rule LABEL/seq/alt/star/plus/bang/name.seq= flatmap B over A's results;alt= union;star= reflexive- transitive closure (BFS, canon-deduped);plus= A then star;bang= normal forms (reachable terms where A yields nothing). Named strategies via a NAME->strategy env dict passed tomau/srun/mau/srun-canon. Verified that the same rule set computes different things under different strategies (single rule vs all vs seq order vs alt vs star vs bang). Built on Phase 5mau/all-successors(rule label filter =mau/rules-with-label).- Note:
dict-set!returns the value, not the dict — build a named-strategy env by binding(define env {})then(dict-set! env ...), passenv.srun-canonsorts results so expected lists must be sorted.
- Note:
-
Phase 7 (reflection / META-LEVEL) — DONE, 196/196 total.
lib/maude/meta.sx.mau/up-termre-encodes an object term as a term built from meta-constructorsmt-var(name,sort) /mt-app(op, args...) — a represented term is itself a first-class object term you can build, inspect, transform.mau/down-termreverses (round-trips). Reflective ops:mau/meta-reduce/mau/meta-rewrite/mau/meta-apply LABELtake and return represented terms.mau/meta-circular?verifies the lawdown(metaReduce(up t)) =AC= reduce t(reflection agrees with the object level).mau/meta-prove-equal?is a generic equational theorem helper (prove an identity by joint reduction). Verified: up/down round-trip, meta-reduce returns a represented normal form, meta-circular law on Peano, meta-apply of a single rule, commutativity/associativity instance proofs, and building a program at the meta level then running it.
Blockers
(none)