Skip to content

math_spec.expansion

Named sub-expressions and macros, both declared in the YAML and expanded into the AST before anything reads the expression.

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

expand(node, schema, context='expression', *, shadow=frozenset()) #

expand(
    node: ArithmeticNode,
    schema: Spec,
    context: str = ...,
    *,
    shadow: frozenset[str] = ...,
) -> ArithmeticNode
expand(
    node: ComparisonNode,
    schema: Spec,
    context: str = ...,
    *,
    shadow: frozenset[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.

PARAMETER DESCRIPTION
node

The parsed expression.

TYPE: ExpressionNode

schema

Where names and macros are declared.

TYPE: Spec

context

What an error names.

TYPE: str DEFAULT: 'expression'

shadow

Names left as written even where a named expression has that name — a template's formals, checked without a call to bind them.

TYPE: frozenset[str] DEFAULT: frozenset()

Source code in src/math_spec/expansion.py
def expand(
    node: ExpressionNode, schema: Spec, context: str = 'expression', *, shadow: frozenset[str] = frozenset()
) -> 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.

    Args:
        node: The parsed expression.
        schema: Where names and macros are declared.
        context: What an error names.
        shadow: Names left as written even where a named expression has that
            name — a template's formals, checked without a call to bind them.
    """
    if isinstance(node, ComparisonNode):
        return ComparisonNode(
            node.op,
            _expand(node.left, schema, context, (), shadow),
            _expand(node.right, schema, context, (), shadow),
        )
    return _expand(node, schema, context, (), shadow)

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: Spec, 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