Skip to content

math_spec.expansion

Named sub-expressions and expression macros — YAML-defined, schema-local.

Both are expanded into core AST before any backend sees the expression, so the eager builder and the relational backend support them identically and the engine contract (core AST is the whole language) is untouched.

Two mechanisms, one substitution engine, zero global state:

  • Named sub-expressions: the YAML expressions: block maps a name to an expression string. Referencing the name splices in the parsed subtree. Substitution is only half of what the block means, though: a named expression has fixed dims and is readable after a solve (the rules for named expressions), which a macro — parameterised, dimensionless until called — never is.

  • Macros: the YAML macros: block declares parameterised expression templates — language, not code::

    macros: weighted_sum: args: [array, weights] kwargs: [over] template: sum(array * weights, over=over)

Usage: weighted_sum(p, cost, over=generator). Formal names shadow model names inside the body; everything else resolves against the model namespace as usual.

Because macros live in the schema, a YAML file is fully self-contained: its meaning never depends on Python-side registration state. This also makes load-time validation complete — every template can be name-checked against this schema (see validation.py), used or not.

There is no Python operator registry: the built-in set is closed, macros cover composition, and math the language cannot say goes in a declared escape: island (#38) — visible in the file and bounded by its where mask, rather than a registered function that reads like a built-in on the page.

expand(node, schema, context='expression') #

expand(
    node: ArithmeticNode, schema: Model, context: str = ...
) -> ArithmeticNode
expand(
    node: ComparisonNode, schema: Model, context: str = ...
) -> ComparisonNode

Expand all named sub-expressions and macro calls under node.

Expansion never changes the shape of the root: a comparison stays a comparison, an arithmetic node stays arithmetic. The overloads say so, so callers holding an ArithmeticNode keep it across the call.

Source code in src/math_spec/expansion.py
def expand(node: ExpressionNode, schema: Model, context: str = 'expression') -> ExpressionNode:
    """Expand all named sub-expressions and macro calls under *node*.

    Expansion never changes the shape of the root: a comparison stays a
    comparison, an arithmetic node stays arithmetic. The overloads say so, so
    callers holding an ``ArithmeticNode`` keep it across the call.
    """
    if isinstance(node, ComparisonNode):
        return ComparisonNode(
            node.op,
            _expand(node.left, schema, context, ()),
            _expand(node.right, schema, context, ()),
        )
    return _expand(node, schema, context, ())

macro_signature(name, macro) #

Human-readable call signature, for error messages.

Source code in src/math_spec/expansion.py
def macro_signature(name: str, macro: MacroBlock) -> str:
    """Human-readable call signature, for error messages."""
    parts = [*macro.args, *(f'{k}=...' for k in macro.kwargs)]
    return f'{name}({", ".join(parts)})'

parse_and_expand(text, schema, context='expression') #

Parse text and expand named sub-expressions and macros to core AST.

Source code in src/math_spec/expansion.py
def parse_and_expand(text: str, schema: Model, context: str = 'expression') -> ExpressionNode:
    """Parse *text* and expand named sub-expressions and macros to core AST."""
    return expand(parse_expression(text), schema, context)

parse_template(name, macro, context) #

Parse a macro template, rejecting comparisons.

Source code in src/math_spec/expansion.py
def parse_template(name: str, macro: MacroBlock, context: str) -> ArithmeticNode:
    """Parse a macro template, rejecting comparisons."""
    body = parse_expression(macro.template)
    if isinstance(body, ComparisonNode):
        msg = f"{context}: macro '{name}' template must not contain a comparison operator. Got: {macro.template!r}"
        raise SchemaError(msg)
    return body