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,
which positional forms printed, and which dimensions were compared against
a coordinate that is a number; all three are things the legend has to
explain once the equations print them.
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.positions: set[str] = set()
self.numeric_coordinates: set[str] = set()
|
namespace = namespace
instance-attribute
numeric_coordinates = set()
instance-attribute
policies = set()
instance-attribute
positions = 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)
Source code in src/math_spec/typeset/walk.py
| def conjoined(self, ctx: _Context, *nodes: WhereNode | None) -> str:
parts = [self.where(n, ctx, need=1) for n in nodes if n is not None]
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)
|
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]
|
ordinal(dimension, at, grouping=None)
The position compared against, counted from the end where it is negative.
A position is a place in the order, and the legend runs that order
from 0 to the size less one — so -1 printed as itself asserts a
position the page has just said cannot exist. What it counts back from
is that size, and the group's size where the count is grouped, which
is the set those positions are positions in.
0 prints as 0: the file is 0-based and so is the page, so a
clause can be read off one and written into the other.
Source code in src/math_spec/typeset/walk.py
| def ordinal(self, dimension: str, at: int, grouping: str | None = None) -> str:
"""The position compared against, counted from the end where it is negative.
A position is a place in the order, and the legend runs that order
from ``0`` to the size less one — so ``-1`` printed as itself asserts a
position the page has just said cannot exist. What it counts back from
is that size, and the *group's* size where the count is grouped, which
is the set those positions are positions in.
``0`` prints as ``0``: the file is 0-based and so is the page, so a
clause can be read off one and written into the other.
"""
if at >= 0:
return self.number(at)
self.positions.add('from_end')
size = self.symbols.set[dimension]
if grouping is not None:
size = self.format.subscript(size, [grouping])
return f'{self.format.cardinality(size)} {self.op("minus")} {self.number(-at)}'
|
position(index, grouping=None)
position(dim) as the row's place along the dimension.
Applied to the row rather than to the set, because that is what it
converts: a coordinate to where that coordinate sits. grouping is the
lookup already applied to the row, and rides as a subscript rather
than as a second argument — a modifier saying which order is being
counted, the way an edge fill rides its translation. As an argument it
sat where the first one's integer sits, and read as a second position.
Source code in src/math_spec/typeset/walk.py
| def position(self, index: str, grouping: str | None = None) -> str:
"""``position(dim)`` as the row's place along the dimension.
Applied to the *row* rather than to the set, because that is what it
converts: a coordinate to where that coordinate sits. *grouping* is the
lookup already applied to the row, and rides as a **subscript** rather
than as a second argument — a modifier saying which order is being
counted, the way an edge fill rides its translation. As an argument it
sat where the first one's integer sits, and read as a second position.
"""
self.positions.add('grouped' if grouping is not None else 'plain')
symbol = self.op('position')
if grouping is not None:
symbol = self.format.subscript(symbol, [grouping])
return self.format.apply(symbol, index)
|
position_notes()
A sentence for each positional symbol the model actually printed.
Gated the way the translation notes are, and the first of them is what
the page cannot go without. A reader arrives from papers where the
index is the ordinal — sets are written as {1, …, T} there, so
nothing is marked because nothing needs to be — and this language
indexes by coordinates instead. A page printing both
pos(t) = 0 and t >= 3 therefore has to say once which of the
two is the position, or the reader recovers a different model from the
one the file holds.
Source code in src/math_spec/typeset/walk.py
| def position_notes(self) -> list[str]:
"""A sentence for each positional symbol the model actually printed.
Gated the way the translation notes are, and the first of them is what
the page cannot go without. A reader arrives from papers where the
index *is* the ordinal — sets are written as ``{1, …, T}`` there, so
nothing is marked because nothing needs to be — and this language
indexes by coordinates instead. A page printing both
``pos(t) = 0`` and ``t >= 3`` therefore has to say once which of the
two is the position, or the reader recovers a different model from the
one the file holds.
"""
notes = []
if self.positions:
index = self.format.math('t')
place = self.format.math(self.format.apply(self.op('position'), 't'))
dash = self.format.dash
notes.append(
f"{place} denotes where index {index} sits along its dimension's own order {dash} the order "
f'{self.format.mono("shift")} walks, not the order labels sort in {dash} counted from '
f'{self.format.math("0")}. The index itself stays the coordinate, so {index} compares against '
f'labels and {place} against positions.'
)
if 'grouped' in self.positions:
applied = self.format.apply(self.format.upright('lookup'), 't')
grouped = self.format.math(self.format.apply(self.format.subscript(self.op('position'), [applied]), 't'))
group = self.format.math(self.format.subscript(self.format.script('T'), [applied]))
notes.append(
f'{grouped} counts within the group a lookup puts {self.format.math("t")} in: the subscript names '
f'the map, {group} is the group it lands in, and that group has a first position of its own.'
)
if 'from_end' in self.positions:
size = self.format.cardinality(self.format.script('T'))
last = self.format.math(f'{size} {self.op("minus")} {self.number(1)}')
notes.append(
f'{self.format.math(size)} denotes the size of the set being counted along, and a position '
f'counted from the end prints against it {self.format.dash} {last} is the last position, one '
f'less than the size because the first is {self.format.math("0")}.'
)
return notes
|
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, so a call with both writes one subscript
group: two subscripts on one symbol is a TeX error rather than a
rendering, and the equation carrying it stopped compiling (#1165).
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, so a call with both writes *one* subscript
group: two subscripts on one symbol is a TeX error rather than a
rendering, and the equation carrying it stopped compiling (#1165).
"""
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)
indices = [index for index in (step.fill, group) if index]
return self.format.subscript(operator, indices) if indices else operator
|
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.'
)
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
|