The Auxiliary Reference Graph
The ARG is AuxFlow’s intermediate representation. Nodes are individual auxiliary fact instances; edges are dependencies carrying a pass lag.
Why this is an intermediate representation
It satisfies the three properties that distinguish an IR from a data structure that happens to be a graph:
- It is lowered to. A syntax-directed translation maps AST constructs to facts and edges, one rule per modelled construct.
- It is analysed and transformed in its own terms. Taint propagation, SCC detection, stable-pass numbering and the classifier all operate on the ARG and never consult the AST. This is enforced architecturally:
analysis/may not importast/. - It is lowered from. The build planner consumes the ARG and the analysis results and emits an executable plan.
It also abstracts: prose, formatting, macro bodies and everything else irrelevant to convergence are discarded, while source spans are retained so that results can be explained.
Lowering assigns a separate ID to each modelled construct instance. It adds incoming dependencies once when it completes the graph. Unsupported control sequences and opaque macro bodies mark the document’s facts opaque.
A design failure to avoid. An earlier version of this design used fact kinds as nodes — one node for “labels”, one for “table of contents entries”. That graph is a fixed eight-node lattice, identical for every document, and computing its height is a lookup table, not an analysis. Nodes are instances. That single decision is what makes the analysis a genuine dataflow problem.
Fact
class FactKind(Enum):
LABEL_NUMBER = auto() # \label -> counter value component
LABEL_PAGE = auto() # \label -> page component
REF_USE = auto() # \ref consumer
PAGEREF_USE = auto() # \pageref consumer
TOC_ENTRY = auto()
LOF_ENTRY = auto()
LOT_ENTRY = auto()
LIST_EXTENT = auto() # page footprint of a generated list
CITE_USE = auto()
BBL = auto() # bibliography body, produced by an external tool
INDEX_ENTRY = auto() # optional
IND = auto() # optional
COUNTER_READ = auto() # \value{c} appearing in an expression
CONTENT_VOLUME = auto() # abstract: the quantity of generated content (pagination)
OPAQUE = auto()
Component = Literal["exists", "value"]
@dataclass(frozen=True)
class Fact:
fact_id: int
kind: FactKind
key: str | None # label or cite key; None for singletons such as CONTENT_VOLUME
component: Component
producer: str # "engine" | "bibtex" | "makeindex"
consumer: str | None # tool that reads it, if any
source_location: Span # (file_id, start_offset, end_offset) -- mandatory
stable_pass: int | None # filled by the analysis; None means not bounded
dependencies: tuple[int, ...]
opaque: bool
metadata: dict # counter kind, list kind, originating package
source_location is mandatory on every fact, including synthetic ones such as CONTENT_VOLUME (which points at the construct that established pagination sensitivity). The diagnostics in NON-CONVERGENCE.md are the project’s main user-visible output and they are worthless without spans.
Fact rejects a missing or null source_location. Fact records and dependency edges are frozen. Analysis stages keep mutable graph state separate from these records.
The fact kind set is open decision D4. It will be revised once the corpus is authored, on the principle that a fact kind must be justified by a document that needs it.
Dependency edge
class DepType(Enum):
AUX_ROUNDTRIP = auto() # written to an auxiliary file, read on a later run
TOOL = auto() # mediated by an external program
LAYOUT = auto() # mediated by pagination
STRUCTURAL = auto() # the value influences the QUANTITY of generated content
@dataclass(frozen=True)
class DepEdge:
source: int
destination: int
pass_lag: int # runs that must elapse before the value is observable
dependency_type: DepType
reason: str # kept in the ARG and plan JSON, described in plain words by reports
reason is not decoration. It is kept in the serialized graph and plan. Cycle reports and auxflow plan --explain describe it in plain words. Writing it at edge-construction time is the only point where the justification is actually known.
STRUCTURAL is the load-bearing edge type. It is never produced by the lowering rules directly; it is added by the taint analysis (see STATIC-ANALYSIS.md).
What pass_lag means
Let an execution be a sequence of engine runs r₁, r₂, …. Let A_k be the auxiliary state after run k, with A₀ empty. Run k reads A_{k−1} and writes A_k; it cannot read what it is in the middle of writing.
For a fact f, write valₖ(f) for its value in A_k. Then
val_k(f) = F_f(A_{k-1})
for a function F_f determined by the document. An edge g → f with pass_lag = 1 records that F_f reads val(g).
Define f to be stable at pass p when valₖ(f) = val_p(f) for every k ≥ p. Then
stable_pass(f) = 1 if f has no dependencies
stable_pass(f) = 1 + max over g in deps(f) of
( stable_pass(g) + pass_lag(g -> f) - 1 ) otherwise
which for the ordinary case pass_lag = 1 reduces to 1 + max(stable_pass(g)).
Let P be the maximum of stable_pass over all facts, and 0 when there are none. Then A_P is the fixpoint, and since a run produces correct output exactly when it reads the fixpoint:
required runs = P + 1
The exists / value split
Every list-producing fact is split into two nodes:
exists— whether the fact is present, and its contribution to content volume. Determined by document structure, not by any fact value. A\sectionproduces exactly one table-of- contents entry whatever the page numbers are.value— the page number or counter recorded.
CONTENT_VOLUME depends only on exists components. LABEL_PAGE.value depends on CONTENT_VOLUME. PAGEREF_USE depends on LABEL_PAGE.value.
Lowering creates one CONTENT_VOLUME fact for the source document and links each list entry’s exists node to its list extent. The extent links to CONTENT_VOLUME. CONTENT_VOLUME links to each entry’s value node. A bibliography adds a BBL.exists fact and a contents entry.
Without this split the analysis is vacuous. TOC_ENTRY.value → CONTENT_VOLUME → TOC_ENTRY.value is a cycle, so every document containing a table of contents would be classified cyclic and no bound could ever be computed. With the split, ordinary documents are acyclic, while genuine amplifying feedback still forms a cycle because it passes through a STRUCTURAL edge.
This is an abstraction, and it is not faithful — see limitation L2 in PROJECT.md. It is stated explicitly rather than buried because it is the assumption most likely to be attacked.
Examples
Backward reference, no generated lists
\chapter{One}\label{c:one}
...
\chapter{Two} Refer to Chapter~\ref{c:one}.
| fact | deps | stable_pass |
|---|---|---|
LABEL_NUMBER(c:one).value | none | 1 |
REF_USE(c:one) | LABEL_NUMBER(c:one) via AUX_ROUNDTRIP lag 1 | 2 |
REF_USE is a consumer rendered into output, not a fact stored in the auxiliary file, so P = 1 and the document needs 2 runs.
Table of contents
\tableofcontents
\section{...} x 40
| fact | deps | stable_pass |
|---|---|---|
TOC_ENTRY(i).exists | none (structural) | 1 |
LIST_EXTENT(toc) | all TOC_ENTRY(i).exists, LAYOUT lag 0 | 1 |
CONTENT_VOLUME | LIST_EXTENT(toc), LAYOUT lag 1 | 2 |
LABEL_PAGE(*).value, TOC_ENTRY(i).value | CONTENT_VOLUME, LAYOUT lag 0 | 2 |
P = 2, so 3 runs. Note the shape of the argument: the list is typeset in run 2 from the file written in run 1, which shifts pagination, and the corrected pages are recorded in the same run they are determined.
Bibliography cascade
\tableofcontents
... \cite{k} ...
\bibliography{refs}
| fact | deps | stable_pass |
|---|---|---|
CITE_USE(k) | none | 1 |
BBL.exists | CITE_USE(*), TOOL lag 1 | 1 (external tool runs after run 1) |
TOC_ENTRY(bib).exists | BBL.exists, LAYOUT lag 1 | 2 |
LIST_EXTENT(toc) | TOC_ENTRY(*).exists | 2 |
CONTENT_VOLUME | LIST_EXTENT(toc), lag 1 | 3 |
LABEL_PAGE(*).value | CONTENT_VOLUME | 3 |
PAGEREF_USE(*) | LABEL_PAGE, AUX_ROUNDTRIP lag 1 | 4 |
P = 3, so 4 runs — one more than the conventional fixed schedule performs. The chain is three links deep and every link is document-dependent, which is the evidence that instance-level facts carry information a fixed lattice could not.
The exact lag semantics for the TOOL edge is open decision D5.
Amplifying cycle
\newcounter{n}
\forloop{n}{0}{\value{n} < \pageref{l}}{~\newpage}
Last page label here\label{l}.
| edge | type |
|---|---|
LABEL_PAGE(l) → PAGEREF_USE(l) | AUX_ROUNDTRIP lag 1 |
PAGEREF_USE(l) → CONTENT_VOLUME | STRUCTURAL (added by taint: the value bounds a loop that emits pages) |
CONTENT_VOLUME → LABEL_PAGE(l) | LAYOUT |
A cycle containing a STRUCTURAL edge. stable_pass is undefined for all three facts and the document is classified amplifying. Contrast the table-of-contents example, which is also feedback but contains no STRUCTURAL edge and receives a bound.
Serialisation
The ARG serialises to JSON for tests and to Graphviz DOT for inspection (auxflow graph). DOT output colours nodes by component and edges by dependency type, so that a benign cycle and an amplifying one are visually distinct — this is the single most useful debugging artifact in the project and is worth building early.