Skip to content

math_spec.typeset.walk

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

format = fmt 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) #

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