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 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()
|
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)
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]
|
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, 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
|