Skip to content

math_spec.expression_parser

The core AST every pass reads, and the pyparsing grammar that builds it.

Arithmetic nests anywhere; a comparison appears only at the top of a parsed expression.

ArithmeticNode = NumberNode | NameNode | NameListNode | VariableNode | ParameterNode | DimensionNode | LookupNode | EdgeNode | KeywordNode | UnaryOperatorNode | BinaryOperatorNode | FunctionCallNode | CasesNode module-attribute #

BinaryOperator = Literal['+', '-', '*', '/', '**'] module-attribute #

BranchNode = UnaryOperatorNode | BinaryOperatorNode | ComparisonNode | FunctionCallNode | CasesNode module-attribute #

ComparisonOperator = Literal['<=', '>=', '=='] module-attribute #

ExpressionNode = ArithmeticNode | ComparisonNode module-attribute #

KwargNode = DimensionNode | LookupNode | EdgeNode module-attribute #

LeafNode = NumberNode | VariableNode | ParameterNode | KwargNode | UnresolvedNode module-attribute #

NAME = '[a-zA-Z_][a-zA-Z0-9_]*' module-attribute #

REAL = '\\d+\\.\\d*([eE][+-]?\\d+)?|\\d+[eE][+-]?\\d+' module-attribute #

UnaryOperator = Literal['+', '-'] module-attribute #

UnresolvedNode = NameNode | NameListNode | KeywordNode module-attribute #

BinaryOperatorNode(op, left, right) dataclass #

left instance-attribute #

op instance-attribute #

right instance-attribute #

CaseArm(label, when, value) dataclass #

One region of a :class:CasesNode: where it applies, and the value there.

when is None on the last arm and only there — the block's otherwise:, which is what makes the quantity total without anything having to prove it. Every other arm's when is proved apart from every other arm's.

label instance-attribute #

value instance-attribute #

when instance-attribute #

CasesNode(name, arms) dataclass #

A value defined by region — a named expression's cases:, inlined where its name stood.

Exactly one arm applies at every coordinate, which :mod:math_spec.exclusivity proves at load; the last arm is the block's otherwise: and carries no when. The arms are in file order. The frame is not carried here: it is on the declaration.

arms instance-attribute #

name instance-attribute #

ComparisonNode(op, left, right) dataclass #

left instance-attribute #

op instance-attribute #

right instance-attribute #

DimensionNode(name) dataclass #

A resolved reference to a declared dimension.

Only legal in operator kwarg values (sum(x, over=generator)), never as a value in arithmetic — a dimension is a coordinate space, not data.

name instance-attribute #

EdgeNode() dataclass #

The resolved edge='wrap'; a number in the same position stays a :class:NumberNode.

FunctionCallNode(name, args=(), kwargs=dict()) dataclass #

An operator or macro call.

kwargs is held behind a read-only view and excluded from the hash; equal nodes still hash equal on name and args.

args = () class-attribute instance-attribute #

kwargs = field(default_factory=dict, hash=False) class-attribute instance-attribute #

name instance-attribute #

KeywordNode(value) dataclass #

A quoted closed keyword in a kwarg value — shift(..., edge='wrap').

Unresolved: which keywords the kwarg accepts is the operator's business.

value instance-attribute #

LookupNode(names, dimension, into) dataclass #

A resolved reference to one or more declared lookups, legal only in a kwarg value.

dimension is the one every lookup is over — what sum consumes and at produces — and into the targets, one per name in the order written; sum(x, by=[gen_bus, gen_tech]) is one grouping, not two.

dimension instance-attribute #

into instance-attribute #

names instance-attribute #

shown property #

The kwarg value as the author wrote it, for an error message.

NameListNode(names) dataclass #

A bracketed list of names in a kwarg value — sum(x, by=[a, b]).

Unresolved: which kind of name the kwarg admits is the operator's business.

names instance-attribute #

shown property #

The kwarg value as the author wrote it, for an error message.

NameNode(name) dataclass #

A bare name whose kind only the schema knows; resolution rewrites every one into a typed node.

name instance-attribute #

NumberNode(value) dataclass #

value instance-attribute #

ParameterNode(name) dataclass #

A resolved reference to a declared parameter.

name instance-attribute #

UnaryOperatorNode(op, operand) dataclass #

op instance-attribute #

operand instance-attribute #

VariableNode(name) dataclass #

A resolved reference to a declared decision variable.

name instance-attribute #

case_context(name, label) #

The context an error inside one arm of a cased expression is reported under.

PARAMETER DESCRIPTION
name

The named expression the arm belongs to.

TYPE: str

label

The case's name, or None for the block's otherwise:.

TYPE: str | None

RETURNS DESCRIPTION
str

The context prefix an error message carries.

Source code in src/math_spec/expression_parser.py
def case_context(name: str, label: str | None) -> str:
    """The context an error inside one arm of a cased expression is reported under.

    Args:
        name: The named expression the arm belongs to.
        label: The case's name, or ``None`` for the block's ``otherwise:``.

    Returns:
        The context prefix an error message carries.
    """
    where = 'otherwise' if label is None else f"case '{label}'"
    return f"Named expression '{name}', {where}"

children(node) #

The sub-expressions of node — the structural half of any walk.

Every pass that recurses the whole tree and acts only at certain leaves goes through here, so a node added later reaches all of them. An operator's kwargs are children too — a dimension or coordinate is an ordinary node in a kwarg value, which is what lets a macro bind a formal. A case arm's when is not: it is a mask over the frame, not a value in it.

Source code in src/math_spec/expression_parser.py
def children(node: ExpressionNode) -> tuple[ArithmeticNode, ...]:
    """The sub-expressions of *node* — the structural half of any walk.

    Every pass that recurses the whole tree and acts only at certain leaves
    goes through here, so a node added later reaches all of them. An
    operator's kwargs are children too — a dimension or coordinate is an
    ordinary node in a kwarg value, which is what lets a macro bind a formal.
    A case arm's ``when`` is not: it is a mask over the frame, not a value in it.
    """
    if isinstance(node, UnaryOperatorNode):
        return (node.operand,)
    if isinstance(node, (BinaryOperatorNode, ComparisonNode)):
        return (node.left, node.right)
    if isinstance(node, FunctionCallNode):
        return (*node.args, *node.kwargs.values())
    if isinstance(node, CasesNode):
        return tuple(arm.value for arm in node.arms)
    return ()

parse_expression(text) cached #

Parse a math expression string into an AST.

RAISES DESCRIPTION
SchemaError

If text is not an expression of the language. A predictable mistake — a strict or chained comparison, !=, a lone =, ^ for power — is named with its rewrite before the grammar's own complaint.

Source code in src/math_spec/expression_parser.py
@lru_cache(maxsize=4096)
def parse_expression(text: str) -> ExpressionNode:
    """Parse a math expression string into an AST.

    Raises:
        SchemaError: If *text* is not an expression of the language. A
            predictable mistake — a strict or chained comparison, ``!=``, a
            lone ``=``, ``^`` for power — is named with its rewrite before the
            grammar's own complaint.
    """
    return cast('ExpressionNode', parse_text(_GRAMMAR, text, 'expression', _named_rewrite))

parse_text(grammar, text, what, rewrite) #

Parse the whole of text with grammar, or raise :class:SchemaError naming what failed to parse.

rewrite is asked for the predictable mistake at the failure position; its sentence, if any, precedes the grammar's own complaint.

Source code in src/math_spec/expression_parser.py
def parse_text(grammar: pp.ParserElement, text: str, what: str, rewrite: Callable[[str, int], str | None]) -> Any:
    """Parse the whole of *text* with *grammar*, or raise :class:`SchemaError` naming *what* failed to parse.

    *rewrite* is asked for the predictable mistake at the failure position; its
    sentence, if any, precedes the grammar's own complaint.
    """
    try:
        result = grammar.parse_string(text, parse_all=True)
    except pp.ParseException as e:
        hint = rewrite(text, e.loc)
        msg = f'Failed to parse {what}: {text!r}\n{f"{hint}\n" if hint is not None else ""}{e}'
        raise SchemaError(msg) from e
    return result[0]

shown(names) #

Names as a kwarg value is written: bare when one, bracketed when several.

Source code in src/math_spec/expression_parser.py
def shown(names: tuple[str, ...]) -> str:
    """Names as a kwarg value is written: bare when one, bracketed when several."""
    return names[0] if len(names) == 1 else f'[{", ".join(names)}]'

with_children(node, recurse) #

node rebuilt with recurse applied to each of its :func:children; a leaf comes back as is.

A case arm's when is a mask over the frame, not a value in it, and is carried across unchanged.

Source code in src/math_spec/expression_parser.py
def with_children(node: ArithmeticNode, recurse: Callable[[ArithmeticNode], ArithmeticNode]) -> ArithmeticNode:
    """*node* rebuilt with *recurse* applied to each of its :func:`children`; a leaf comes back as is.

    A case arm's ``when`` is a mask over the frame, not a value in it, and is
    carried across unchanged.
    """
    if isinstance(node, LeafNode):
        return node
    if isinstance(node, UnaryOperatorNode):
        return UnaryOperatorNode(node.op, recurse(node.operand))
    if isinstance(node, BinaryOperatorNode):
        return BinaryOperatorNode(node.op, recurse(node.left), recurse(node.right))
    if isinstance(node, FunctionCallNode):
        return FunctionCallNode(
            node.name,
            tuple(recurse(a) for a in node.args),
            {k: recurse(v) for k, v in node.kwargs.items()},
        )
    if isinstance(node, CasesNode):
        return CasesNode(node.name, tuple(CaseArm(a.label, a.when, recurse(a.value)) for a in node.arms))
    assert_never(node)