The walk: resolved AST → typeset lines. Written once, for every format.
Everything here is a decision about the math — where a bracket changes the
reading, which dimension a reduction binds, that a mask belongs on the ∀ rather
than in the equation, that a translation shows at the leaf it re-indexes. None
of it is about syntax, so none is duplicated per format.
The walk holds no opinion the lanes do not: names come from resolution, dim
sets from dimensions, operator shapes from the closed BUILTINS set, and a
operator it forgot is an assert_never rather than a blank.
PRIME = "'"
module-attribute
Walk(schema, namespace, symbols, fmt)
Walks a validated schema, emitting :class:Lines in one format.
Stateful only in what it has noticed — which edge policies appeared and
whether a translation was counted inside a group, which the legend needs
to explain the symbols they print.
Source code in src/math_spec/typeset/walk.py
| def __init__(self, schema: Buildable, namespace: Namespace, symbols: Symbols, fmt: Format) -> None:
self.schema = schema
self.namespace = namespace
self.symbols = symbols
self.format = fmt
self.policies: set[str] = set()
self.grouped = False
|
grouped = False
instance-attribute
namespace = namespace
instance-attribute
policies = set()
instance-attribute
schema = schema
instance-attribute
symbols = symbols
instance-attribute
arithmetic(node, ctx, *, need=0)
Source code in src/math_spec/typeset/walk.py
| def arithmetic(self, node: ArithmeticNode, ctx: _Context, *, need: int = 0) -> str:
text, precedence = self._arithmetic(node, ctx)
return self.format.parenthesise(text) if precedence < need else text
|
conjoined(ctx, *nodes)
The mask on a quantifier, as one condition.
A mask that is only True prints nothing: the language says it is
the same as no where at all, so a quantifier reading : \top
would put a condition on the page that reads as one and is not. Nested
it still prints — \top \wedge x is what the file says, and
simplifying a mask is resolution's job rather than the typesetter's.
Source code in src/math_spec/typeset/walk.py
| def conjoined(self, ctx: _Context, *nodes: WhereNode | None) -> str:
r"""The mask on a quantifier, as one condition.
A mask that is *only* ``True`` prints nothing: the language says it is
the same as no ``where`` at all, so a quantifier reading ``: \top``
would put a condition on the page that reads as one and is not. Nested
it still prints — ``\top \wedge x`` is what the file says, and
simplifying a mask is resolution's job rather than the typesetter's.
"""
kept = [n for n in nodes if n is not None and not (isinstance(n, BooleanLiteralNode) and n.value)]
parts = [self.where(n, ctx, need=1) for n in kept]
return self.format.joined(parts, self.op('and')) if parts else ''
|
constraints()
Source code in src/math_spec/typeset/walk.py
| def constraints(self) -> list[Line]:
lines = []
for name, block in self.schema.constraints.items():
context = f"constraint '{name}'"
node = expression_of(block.expression, self.schema, self.namespace, context)
if not isinstance(node, ComparisonNode):
msg = f'{context}: expected a comparison, got {type(node).__name__}'
raise AssertionError(msg)
ctx = self.context(ceiling=2)
condition = self.conjoined(ctx, where_of(block.where, self.namespace, context))
lines.append(
Line(
label=name,
left=self.arithmetic(node.left, ctx),
right=f'{self.op(_RELATIONS[node.op])} {self.arithmetic(node.right, ctx)}',
condition=self.quantifier(list(block.foreach), condition),
)
)
return lines
|
context(ceiling=1)
Source code in src/math_spec/typeset/walk.py
| def context(self, ceiling: int = 1) -> _Context:
return _Context(self, ceiling=ceiling)
|
convention_notes()
What the two faces mean, said once, with the model's own symbols.
A nomenclature table already splits the legend into parameters and
variables, and that is the convention a paper in this field uses. It is
a lookup rather than a reading, though: quote one equation on a slide
and the distinction is gone. So the symbols carry it, and this is where
the page says which face is which.
Only where the model has both, and only quoting symbols the
derivation produced: a table is printed verbatim and is the author's
to write, so a symbol it supplies is not one this note governs.
Source code in src/math_spec/typeset/walk.py
| def convention_notes(self) -> list[str]:
"""What the two faces mean, said once, with the model's own symbols.
A nomenclature table already splits the legend into parameters and
variables, and that is the convention a paper in this field uses. It is
a *lookup* rather than a reading, though: quote one equation on a slide
and the distinction is gone. So the symbols carry it, and this is where
the page says which face is which.
Only where the model has both, and only quoting symbols the
*derivation* produced: a table is printed verbatim and is the author's
to write, so a symbol it supplies is not one this note governs.
"""
derived = [
next((n for n in names if n not in self.symbols.overridden), None)
for names in (self.schema.parameters, self.schema.variables)
]
if not all(derived):
return []
given, chosen = (self.format.math(self.symbols.name[n]) for n in derived if n is not None)
return [
f'Upright is what the model is given {self.format.dash} a parameter such as {given}, a coordinate '
f'map, a label {self.format.dash} and italic is what the solver chooses, such as {chosen}. '
f'An index is italic too, being what a quantifier chooses, and a set is script.'
]
|
glossaries()
Source code in src/math_spec/typeset/walk.py
| def glossaries(self) -> list[Glossary]:
fmt = self.format
sets = [
Entry(
symbol=self.symbols.set[d],
name=f'index {fmt.math(self.symbols.index[d])} {fmt.dash} {fmt.mono(d)}',
detail=self._coords(d),
description=fmt.escape(block.description or ''),
)
for d, block in self.schema.dimensions.items()
]
parameters = [
Entry(
symbol=self.symbols.name[p],
name=fmt.mono(p),
detail=self._over(list(block.dims)),
description=fmt.escape(block.description or ''),
)
for p, block in self.schema.parameters.items()
]
variables = [
Entry(
symbol=self.symbols.name[v],
name=fmt.mono(v),
detail=self._over(list(block.foreach)),
description=fmt.escape(block.description or ''),
)
for v, block in self.schema.variables.items()
]
groups = (Glossary('Sets', sets), Glossary('Parameters', parameters), Glossary('Variables', variables))
return [group for group in groups if group.entries]
|
literal(value)
Source code in src/math_spec/typeset/walk.py
| def literal(self, value: float | str | datetime.date) -> str:
return self.number(value) if isinstance(value, (int, float)) else self.format.prose(str(value))
|
membership(dim)
Source code in src/math_spec/typeset/walk.py
| def membership(self, dim: str) -> str:
return f'{self.symbols.index[dim]} {self.op("in")} {self.symbols.set[dim]}'
|
number(value)
Source code in src/math_spec/typeset/walk.py
| def number(self, value: float) -> str:
if value == float('inf'):
return self.op('infinity')
if value == float('-inf'):
return self.op('minus_infinity')
return str(int(value)) if value == int(value) else repr(value)
|
objective()
The objective's line.
The expression is scalar — every reduction in it is one the file wrote
— so it renders like any other, and the line carries no label: the
block has no name, and the section heading already says what it is.
Source code in src/math_spec/typeset/walk.py
| def objective(self) -> list[Line]:
"""The objective's line.
The expression is scalar — every reduction in it is one the file wrote
— so it renders like any other, and the line carries no label: the
block has no name, and the section heading already says what it is.
"""
block = self.schema.objective
if block is None:
return []
sense = self.op('minimize' if block.sense == 'minimize' else 'maximize')
node = expression_of(block.expression, self.schema, self.namespace, 'the objective')
assert not isinstance(node, ComparisonNode)
return [Line(label='', left=sense, right=self.arithmetic(node, self.context(ceiling=2)))]
|
op(name)
Source code in src/math_spec/typeset/walk.py
| def op(self, name: str) -> str:
return self.format.operators[name]
|
position(dimension, at, grouping=None)
index(dim, i) as the coordinate it names.
An upright application of the operator to the set, the same shape a
lookup gets — rather than min/max, which would read the two
ends and leave every other position without a notation. grouping is
the lookup already applied to the row, and prints as a third argument
so the row a position is counted for is visible where the position is.
Source code in src/math_spec/typeset/walk.py
| def position(self, dimension: str, at: int, grouping: str | None = None) -> str:
"""``index(dim, i)`` as the coordinate it names.
An upright application of the operator to the set, the same shape a
lookup gets — rather than ``min``/``max``, which would read the two
ends and leave every other position without a notation. *grouping* is
the lookup already applied to the row, and prints as a third argument
so the row a position is counted for is visible where the position is.
"""
parts = [self.symbols.set[dimension], self.number(at)]
if grouping is not None:
parts.append(grouping)
return self.format.apply(self.format.upright('index'), ', '.join(parts))
|
quantifier(dims, condition)
Source code in src/math_spec/typeset/walk.py
| def quantifier(self, dims: list[str], condition: str) -> str:
if not dims and not condition:
return ''
over = self.format.joined([self.membership(d) for d in dims], '')
if not condition:
return f'{self.op("forall")} {over}'
if not over:
return f'{self.format.prose("where ")} {condition}'
return f'{self.op("forall")} {over} {self.op("such_that")} {condition}'
|
reduction_body(node, ctx)
What sits to the right of a sum, bracketed only where it must be.
A sum binds everything up to the next + or - at its own level,
so an additive body needs the bracket and nothing else does — including
a nested reduction, which is unambiguous. The precedence rule would
bracket that too, and a renderer that brackets everything is one nobody
trusts to bracket the thing that matters.
Source code in src/math_spec/typeset/walk.py
| def reduction_body(self, node: ArithmeticNode, ctx: _Context) -> str:
"""What sits to the right of a sum, bracketed only where it must be.
A sum binds everything up to the next ``+`` or ``-`` at its own level,
so an additive body needs the bracket and nothing else does — including
a nested reduction, which is unambiguous. The precedence rule would
bracket that too, and a renderer that brackets everything is one nobody
trusts to bracket the thing that matters.
"""
additive = isinstance(node, UnaryOperatorNode) or (
isinstance(node, BinaryOperatorNode) and node.op in ('+', '-')
)
return self.arithmetic(node, ctx, need=2 if additive else 0)
|
translation(step, group='')
The operator for one translation, carrying its fill and its group.
Both ride the operator, and they take different slots: the fill
below, the partition above. Sharing one subscript is what a single
symbol allows — two subscripts is a TeX error rather than a rendering
(#1165) — but comma-joined there, 0,season_of(t) said nothing about
which of the two was the value standing at the boundary and which was
the group the translation stays inside.
Source code in src/math_spec/typeset/walk.py
| def translation(self, step: _Step, group: str = '') -> str:
"""The operator for one translation, carrying its fill and its group.
Both ride the operator, and they take **different slots**: the fill
below, the partition above. Sharing one subscript is what a single
symbol allows — two subscripts is a TeX error rather than a rendering
(#1165) — but comma-joined there, ``0,season_of(t)`` said nothing about
which of the two was the value standing at the boundary and which was
the group the translation stays inside.
"""
backward, forward = _TRANSLATIONS[step.policy]
# a named offset is always backward: `by=-p` is refused, so the sign is
# in the data and the operator cannot read it off the call
operator = self.op(backward if isinstance(step.by, str) or step.by > 0 else forward)
if step.fill:
operator = self.format.subscript(operator, [step.fill])
if not group:
return operator
self.grouped = True
return self.format.superscript(operator, group)
|
translation_notes()
A sentence for each translation symbol the model actually printed.
Only those: a legend explaining a symbol that is nowhere on the page is
a dead end, and plain t-k needs no note until something else stands
beside it.
Source code in src/math_spec/typeset/walk.py
| def translation_notes(self) -> list[str]:
"""A sentence for each translation symbol the model actually printed.
Only those: a legend explaining a symbol that is nowhere on the page is
a dead end, and plain ``t-k`` needs no note until something else stands
beside it.
"""
notes = []
if 'wrap' in self.policies:
cyclic = self.format.math(f't {self.op("cyclic_minus")} k')
notes.append(
f'{cyclic} denotes cyclic translation: index {self.format.math("t-k")} taken modulo the size of '
f'the dimension ({self.format.mono("roll")}). Plain {self.format.math("t-k")} '
f'({self.format.mono("shift")}) has no wraparound {self.format.dash} terms translated past '
f'the edge are simply absent.'
)
if 'edge' in self.policies:
filled = self.format.math(f't {self.format.subscript(self.op("edge_minus"), ["v"])} k')
notes.append(
f'{filled} denotes translation with {self.format.math("v")} standing where index '
f'{self.format.math("t-k")} leaves the dimension ({self.format.mono("shift(edge=v)")}), so the row '
f'at that boundary is built and carries {self.format.math("v")} rather than being dropped.'
)
if self.grouped:
applied = self.format.apply(self.format.upright('lookup'), 't')
counted = self.format.math(f't {self.format.superscript(self.op("cyclic_minus"), applied)} k')
note = (
f'{counted} denotes a translation counted inside the group a lookup puts {self.format.math("t")} '
f'in ({self.format.mono("shift(by=lookup)")}), so a term never crosses out of its own group.'
)
if 'edge' in self.policies:
both = self.format.superscript(self.format.subscript(self.op('edge_minus'), ['v']), applied)
note += (
f' The two modifiers take different slots {self.format.dash} the group above, the fill '
f'below {self.format.dash} so {self.format.math(f"t {both} k")} is both at once.'
)
notes.append(note)
return notes
|
variables()
One line per variable, and one more for a set the variable carries.
A sos: block restricts the domain — which members of a family may
be nonzero at once — so it prints under this heading, beside the
variable it is a property of, rather than among the constraints, where
it would read as a row a solver holds.
Source code in src/math_spec/typeset/walk.py
| def variables(self) -> list[Line]:
"""One line per variable, and one more for a set the variable carries.
A ``sos:`` block restricts the *domain* — which members of a family may
be nonzero at once — so it prints under this heading, beside the
variable it is a property of, rather than among the constraints, where
it would read as a row a solver holds.
"""
sets = {block.variable: block for block in self.schema.sos.values()}
lines = []
for name, block in self.schema.variables.items():
ctx = self.context()
symbol = ctx.indexed(self.symbols.name[name], list(block.foreach))
where = where_of(block.where, self.namespace, f"variable '{name}'", self_variable=name)
condition = self.quantifier(list(block.foreach), self.conjoined(ctx, where))
lower, upper = block.bounds.lower, block.bounds.upper
if block.domain == 'binary':
left, right = symbol, f'{self.op("in")} {self.op("binary_set")}'
else:
below, above = lower == float('-inf'), upper == float('inf')
if below and above:
domain = self.op('integers' if block.domain == 'integer' else 'reals')
left, right = symbol, f'{self.op("in")} {domain}'
elif below:
left, right = symbol, f'{self.op("le")} {self._bound(ctx, upper)}'
elif above:
left, right = symbol, f'{self.op("ge")} {self._bound(ctx, lower)}'
else:
left = f'{self._bound(ctx, lower)} {self.op("le")} {symbol}'
right = f'{self.op("le")} {self._bound(ctx, upper)}'
if block.domain == 'integer' and not (below and above):
right = f'{right}, {symbol} {self.op("in")} {self.op("integers")}'
lines.append(Line(label=name, left=left, right=right, condition=condition))
if name in sets:
lines.append(self._sos(name, sets[name], ctx))
return lines
|
where(node, ctx, *, need=0)
Source code in src/math_spec/typeset/walk.py
| def where(self, node: WhereNode, ctx: _Context, *, need: int = 0) -> str:
text, precedence = self._where(node, ctx)
return self.format.parenthesise(text) if precedence < need else text
|