diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b0d31a2..85237be9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ it releases that version ([RELEASING.md](https://github.com/energy-models/mathsp ## Upcoming version +- refactor(program)!: the node a use of a named expression stands as is `NamedExpression`, beside `NamedMask`, and `Named` is gone ([#822](https://github.com/energy-models/mathspec/pull/822)) - docs(pypsa): the pypsa spec names its repeated row conditions as masks, and its topic files read them under `given: masks:` ([#825](https://github.com/energy-models/mathspec/pull/825)) - feat(language): a where predicate is named once under `masks:`, and another file reads it under `given: masks:` ([#821](https://github.com/energy-models/mathspec/pull/821)) - feat(language)!: a construct that reads the order of a dimension needs the dimension declared ordered ([#793](https://github.com/energy-models/mathspec/pull/793)) diff --git a/docs/reference/reading.md b/docs/reference/reading.md index fc6b5036..3be20cac 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -127,7 +127,7 @@ node's operands, and `where_children()` walks a predicate's. `walk()` yields every node under an expression, parents first. `walk_regions()` yields each node with the `cases:` regions it stands inside, outermost first. -A `Named` stands where an `expressions:` entry is used. Its `body` is the +A `NamedExpression` stands where an `expressions:` entry is used. Its `body` is the entry's expression, the same object that `program.expressions[name].expression` holds, and its value is the body's value. `children()` steps into the body, so a walk reads through it. diff --git a/src/mathspec/_expression_resolver.py b/src/mathspec/_expression_resolver.py index 6b7d1f74..c076c803 100644 --- a/src/mathspec/_expression_resolver.py +++ b/src/mathspec/_expression_resolver.py @@ -186,7 +186,7 @@ def _name(self, node: NameNode) -> Expression | None: A named expression arrives as the one node [`Namespace.named`][] built for it; the cast is the one place a - [`Named`][mathspec.program.Named] enters a tree typed as a program's, + [`NamedExpression`][mathspec.program.NamedExpression] enters a tree typed as a program's, which lowering makes true. """ if node.name in self.formals: diff --git a/src/mathspec/boundedness.py b/src/mathspec/boundedness.py index ffaeaee1..f3546b83 100644 --- a/src/mathspec/boundedness.py +++ b/src/mathspec/boundedness.py @@ -25,7 +25,7 @@ Expression, Join, Multiply, - Named, + NamedExpression, Negate, Parameter, Power, @@ -121,7 +121,7 @@ def _coefficient_sign(node: Expression) -> Sign: """ if isinstance(node, Negate): return _flip(_coefficient_sign(node.operand)) - if isinstance(node, Named): + if isinstance(node, NamedExpression): return _coefficient_sign(node.body) if isinstance(node, Constant) and node.value != 0: return '+' if node.value > 0 else '-' @@ -162,7 +162,7 @@ def _record_signs(node: Expression, sign: Sign, signs: dict[str, Sign]) -> None: _record_signs(node.base, None, signs) _record_signs(node.exponent, None, signs) return - if isinstance(node, Sum | Join | Translate | WindowSum | Cases | Named): + if isinstance(node, Sum | Join | Translate | WindowSum | Cases | NamedExpression): for child in children(node): _record_signs(child, sign, signs) return diff --git a/src/mathspec/dimensions.py b/src/mathspec/dimensions.py index bb8b7333..27037897 100644 --- a/src/mathspec/dimensions.py +++ b/src/mathspec/dimensions.py @@ -32,7 +32,7 @@ JoinedPredicate, Mask, Multiply, - Named, + NamedExpression, Negate, Parameter, ParameterComparison, @@ -75,7 +75,7 @@ def dims_of(node: Expression, schema: Spec, context: str) -> frozenset[str]: if isinstance(node, Dual): return frozenset({**schema.constraints, **schema.given.constraints}[node.constraint].dims) - if isinstance(node, Named): + if isinstance(node, NamedExpression): return _named_dims(node, schema, context) if isinstance(node, Cases): @@ -96,7 +96,7 @@ def dims_of(node: Expression, schema: Spec, context: str) -> frozenset[str]: assert_never(node) -def _named_dims(node: Named, schema: Spec, context: str) -> frozenset[str]: +def _named_dims(node: NamedExpression, schema: Spec, context: str) -> frozenset[str]: """An entry's declared frame where it has one — a narrower arm or body broadcasts along the rest — else its body's.""" declared = schema.expressions[node.name].dims if declared is not None: diff --git a/src/mathspec/lowering.py b/src/mathspec/lowering.py index 4ec69f24..173decc5 100644 --- a/src/mathspec/lowering.py +++ b/src/mathspec/lowering.py @@ -33,7 +33,7 @@ Link, Mask, MaskDeclaration, - Named, + NamedExpression, ObjectiveDeclaration, Parameter, ParameterDeclaration, @@ -121,7 +121,7 @@ def lower(schema: Spec) -> Program: ) resolve_expression(body_ast, ns, context, errors, formals=formals) - entries: dict[str, Named] = {} + entries: dict[str, NamedExpression] = {} for ename in schema.expressions: node, refusals = ns.named_entry(ename) errors.extend(refusals) @@ -196,7 +196,7 @@ def lower(schema: Spec) -> Program: roots.append(objective.expression) roots.extend(link for links in curves.values() for link in links) roots.extend(terms) - in_math = frozenset(node.name for node in walk(*roots) if isinstance(node, Named)) + in_math = frozenset(node.name for node in walk(*roots) if isinstance(node, NamedExpression)) piecewise = {} for pname, links in curves.items(): @@ -269,7 +269,7 @@ def lower(schema: Spec) -> Program: return program -def _terms(entries: Iterable[str], schema: Spec, ns: Namespace, errors: list[str]) -> list[Named]: +def _terms(entries: Iterable[str], schema: Spec, ns: Namespace, errors: list[str]) -> list[NamedExpression]: """Every entry with ``adds_to:`` that loads as a term, with a refusal in *errors* for each other. A term that reads its own sum, directly or through the sums the file's @@ -282,7 +282,7 @@ def _terms(entries: Iterable[str], schema: Spec, ns: Namespace, errors: list[str if (target := schema.expressions[name].adds_to) is not None and (entry := _term(name, target, schema, ns, errors)) is not None } - sums: dict[str, list[Named]] = {} + sums: dict[str, list[NamedExpression]] = {} for target, entry in resolved.values(): sums.setdefault(target, []).append(entry) terms = [] @@ -299,7 +299,7 @@ def _terms(entries: Iterable[str], schema: Spec, ns: Namespace, errors: list[str return terms -def _term(name: str, target: str, schema: Spec, ns: Namespace, errors: list[str]) -> Named | None: +def _term(name: str, target: str, schema: Spec, ns: Namespace, errors: list[str]) -> NamedExpression | None: """Named expression *name* as the term it writes into a given expression of its file, or ``None``. The given entry is what makes a misspelt target a refusal in the file that @@ -325,11 +325,11 @@ def _term(name: str, target: str, schema: Spec, ns: Namespace, errors: list[str] entry = resolve_expression_text(name, ns, context, errors, ceiling=2) if entry is None: return None - assert isinstance(entry, Named), 'a term is a name, and a name resolves to the entry it names' + assert isinstance(entry, NamedExpression), 'a term is a name, and a name resolves to the entry it names' return entry -def _loop(target: str, entry: Named, sums: Mapping[str, list[Named]]) -> list[str] | None: +def _loop(target: str, entry: NamedExpression, sums: Mapping[str, list[NamedExpression]]) -> list[str] | None: """The sums *entry* reads *target* through, by the terms in *sums*, or ``None`` where it does not read it. ``[]`` is a term that reads its own sum. @@ -347,7 +347,7 @@ def _loop(target: str, entry: Named, sums: Mapping[str, list[Named]]) -> list[st return None -def _frame_of(name: str, entry: Named, schema: Spec) -> tuple[str, ...]: +def _frame_of(name: str, entry: NamedExpression, schema: Spec) -> tuple[str, ...]: """The dims an entry is read over: the ``dims:`` it declares, as written, else the body's in declaration order.""" declared = schema.expressions[name].dims if declared is not None: diff --git a/src/mathspec/program.py b/src/mathspec/program.py index 6ab7e62e..60d9d7b7 100644 --- a/src/mathspec/program.py +++ b/src/mathspec/program.py @@ -68,7 +68,7 @@ 'Mask', 'MaskDeclaration', 'Multiply', - 'Named', + 'NamedExpression', 'NamedMask', 'Negate', 'Not', @@ -396,7 +396,7 @@ class Cases: @dataclass(frozen=True) -class Named: +class NamedExpression: """A use of an ``expressions:`` entry, standing where its name was written, with the entry's body under it. Its value is its body's: a consumer building rows steps through it, as @@ -435,13 +435,13 @@ class Named: | Translate | WindowSum | Cases - | Named + | NamedExpression ) def children(expression: Expression) -> tuple[Expression, ...]: """The sub-expressions of *expression* — what every walk recurses through.""" - if isinstance(expression, Named): + if isinstance(expression, NamedExpression): return (expression.body,) if isinstance(expression, Negate): return (expression.operand,) @@ -1039,7 +1039,7 @@ class Program: #: each and refuses with [`assumption_message`][]. assumptions: Mapping[str, Assumption] = Sealed({}) #: Declared ``expressions:``, each saying whether the math reads it. None - #: builds a row of its own — one the math reads stands as a [`Named`][] + #: builds a row of its own — one the math reads stands as a [`NamedExpression`][] #: where it is read — but all are lowered with the program, so a file whose #: named expression is outside the language is refused by every verb that #: reads the file rather than only by the one that reads the expression. @@ -1416,7 +1416,7 @@ class NamedMask: Its truth is its body's: a consumer steps through it, as [`where_children`][] does. It is kept as a node rather than written in so the typesetter can print the symbol where the name stood and define it - once, as [`Named`][] does for an expression. + once, as [`NamedExpression`][] does for an expression. """ name: str diff --git a/src/mathspec/resolution.py b/src/mathspec/resolution.py index 9200d75e..affcc977 100644 --- a/src/mathspec/resolution.py +++ b/src/mathspec/resolution.py @@ -40,7 +40,7 @@ Cases, Expression, Mask, - Named, + NamedExpression, NamedMask, Predicate, Region, @@ -128,7 +128,7 @@ def __init__(self, schema: Spec) -> None: } #: named expression -> its resolved node, or ``None``, and its refusals; #: filled the first time anything reads the name. - self._named: dict[str, tuple[Named | None, tuple[str, ...]]] = {} + self._named: dict[str, tuple[NamedExpression | None, tuple[str, ...]]] = {} #: The named expressions and masks waiting to be resolved, the one #: asked for first — each above the entries it reads, so it is a cycle's chain. self._loading: list[str] = [] @@ -136,7 +136,7 @@ def __init__(self, schema: Spec) -> None: #: the first time anything reads the name. self._masks: dict[str, tuple[NamedMask | None, tuple[str, ...]]] = {} - def named(self, name: str, context: str) -> Named: + def named(self, name: str, context: str) -> NamedExpression: """The ``expressions:`` entry *name* as the node that stands where its name is written. Resolved under the entry's own context the first time it is asked @@ -187,7 +187,7 @@ def mask_entry(self, name: str) -> tuple[NamedMask | None, tuple[str, ...]]: self._masks[name] = (node, tuple(errors)) return self._masks[name] - def named_entry(self, name: str) -> tuple[Named | None, tuple[str, ...]]: + def named_entry(self, name: str) -> tuple[NamedExpression | None, tuple[str, ...]]: """The ``expressions:`` entry *name* resolved, or ``None``, with every refusal it earned. The entries it reads are resolved before it, walked from a stack that @@ -475,7 +475,7 @@ def _over_the_ceiling(node: Expression, context: str, errors: list[str], *, ceil return False -def _named(name: str, block: ExpressionBlock, ns: Namespace, errors: list[str]) -> Named | None: +def _named(name: str, block: ExpressionBlock, ns: Namespace, errors: list[str]) -> NamedExpression | None: """One ``expressions:`` entry as the node every use of it holds, or ``None`` once anything in it failed. A cased entry's arms are checked one by one, so every fault is collected @@ -488,7 +488,7 @@ def _named(name: str, block: ExpressionBlock, ns: Namespace, errors: list[str]) if not block.cases: assert block.expression is not None body = resolve_expression_text(block.expression, ns, context, errors, ceiling=None) - return None if body is None else Named(name, body) + return None if body is None else NamedExpression(name, body) found = len(errors) regions: list[Region] = [] @@ -511,7 +511,7 @@ def _named(name: str, block: ExpressionBlock, ns: Namespace, errors: list[str]) if len(errors) > found: return None left_over = Region(remainder(region.when for region in regions), fallback) - return Named(name, Cases((*regions, left_over))) + return NamedExpression(name, Cases((*regions, left_over))) def _mask(name: str, block: MaskBlock, ns: Namespace, errors: list[str]) -> NamedMask | None: diff --git a/src/mathspec/typesetting/walk.py b/src/mathspec/typesetting/walk.py index 1e0aa369..bc0e7879 100644 --- a/src/mathspec/typesetting/walk.py +++ b/src/mathspec/typesetting/walk.py @@ -34,7 +34,7 @@ JoinedPredicate, Mask, Multiply, - Named, + NamedExpression, NamedMask, Negate, Not, @@ -319,7 +319,7 @@ def _expression(self, node: Expression, ctx: _Context, *, need: int = 0) -> str: def _arithmetic(self, node: Expression, ctx: _Context) -> tuple[str, int]: """Render *node*, returning the text and the precedence it binds at.""" - if isinstance(node, Named): + if isinstance(node, NamedExpression): if self.inline_expressions and not isinstance(node.body, Cases): return self._arithmetic(node.body, ctx) return ctx.indexed(self.symbols.name[node.name], self._frame_of(node.name)), _ATOM @@ -409,7 +409,7 @@ def _substituted(self, node: Expression) -> Expression: [`_binary`][] folds the sign of the result, so a substituted term prints as its body written out. """ - while self.inline_expressions and isinstance(node, Named) and not isinstance(node.body, Cases): + while self.inline_expressions and isinstance(node, NamedExpression) and not isinstance(node.body, Cases): node = node.body return node diff --git a/tests/test_boundedness.py b/tests/test_boundedness.py index 4ebcbbe1..be2bcc30 100644 --- a/tests/test_boundedness.py +++ b/tests/test_boundedness.py @@ -73,7 +73,7 @@ def test_a_variable_the_objective_drives_unopposed_is_named_with_its_side(patch, ) def test_a_named_constant_coefficient_carries_its_sign(objective, side): """A coefficient written as an ``expressions:`` entry reaches the pass as a - ``Named`` node over its constant. The sign was read off the node alone, so a + ``NamedExpression`` node over its constant. The sign was read off the node alone, so a named ``2`` claimed nothing and the unbounded variable went unnamed.""" notes = _notes( **{ diff --git a/tests/test_degree.py b/tests/test_degree.py index 20638262..1b55a271 100644 --- a/tests/test_degree.py +++ b/tests/test_degree.py @@ -133,7 +133,7 @@ def test_calls_dual_finds_a_dual_wherever_it_stands(text, found): def test_calls_dual_finds_a_dual_inside_a_cased_arm(): """`calls_dual` recurses through a region of a `Cases`, not only the top node. - The reference resolves to the `Named` node carrying the block, so this also + The reference resolves to the `NamedExpression` node carrying the block, so this also guards that the walk steps through it into the region values, reaching a dual a non-recursive check — one that only inspected the node it was handed — would miss. diff --git a/tests/test_expansion.py b/tests/test_expansion.py index 0f8178f4..4dbdc147 100644 --- a/tests/test_expansion.py +++ b/tests/test_expansion.py @@ -14,7 +14,7 @@ from mathspec import to_spec from mathspec.errors import LanguageError from mathspec.expansion import parse_and_expand -from mathspec.program import Axis, Multiply, Named, Parameter, Sum, Translate, Variable +from mathspec.program import Axis, Multiply, NamedExpression, Parameter, Sum, Translate, Variable from mathspec.resolution import Namespace from tests.fixtures import DISPATCH_MODEL, SMALL_MODEL, comparison_of, expression_of, schema_of @@ -36,7 +36,7 @@ def _resolved(text, ns): def _bodies(resolved): """*resolved* with every named expression's body standing bare where its name was.""" - if isinstance(resolved, Named): + if isinstance(resolved, NamedExpression): return _bodies(resolved.body) if isinstance(resolved, tuple): return tuple(_bodies(part) for part in resolved) @@ -132,7 +132,7 @@ def _bodies(resolved): ) def test_a_call_expands_to_core_ast(expressions, macros, call, want): """The math a call expands to is what `want` spells; a named expression's - body arrives under the `Named` node carrying its name, which `_bodies` + body arrives under the `NamedExpression` node carrying its name, which `_bodies` inlines, as lowering does.""" ns = Namespace(schema(expressions=expressions, macros=macros)) assert _bodies(_resolved(call, ns)) == _resolved(want, ns) @@ -141,9 +141,9 @@ def test_a_call_expands_to_core_ast(expressions, macros, call, want): def test_a_named_expression_arrives_under_the_node_carrying_its_name(): ns = Namespace(schema(expressions={'gen_cost': 'p * cost'})) resolved = expression_of('sum(gen_cost, over=generator)', ns, 'e') - assert resolved == Sum(Named('gen_cost', Multiply(Variable('p'), Parameter('cost'))), (Axis('generator'),)), ( - 'the body is inlined resolved and the name kept, for the typesetter to define it once' - ) + assert resolved == Sum( + NamedExpression('gen_cost', Multiply(Variable('p'), Parameter('cost'))), (Axis('generator'),) + ), 'the body is inlined resolved and the name kept, for the typesetter to define it once' assert isinstance(resolved, Sum) assert resolved.operand is expression_of('gen_cost', ns, 'another use'), 'every use reads the one node' diff --git a/tests/typesetting/test_golden.py b/tests/typesetting/test_golden.py index 3263704e..3cdc9873 100644 --- a/tests/typesetting/test_golden.py +++ b/tests/typesetting/test_golden.py @@ -16,7 +16,7 @@ import pytest from mathspec.operators import BUILTIN_NAMES -from mathspec.program import Dual, Expression, Join, Named, Predicate, Sum, Translate, WindowSum +from mathspec.program import Dual, Expression, Join, NamedExpression, Predicate, Sum, Translate, WindowSum from mathspec.typesetting import FORMATS, legend, to_latex, typeset, walk from mathspec.typesetting.format import OPERATOR_NAMES from mathspec.validation import to_spec @@ -166,7 +166,7 @@ def test_the_golden_model_carries_every_node_kind_the_walk_renders(): `coverage` installed, and its failure names the construct rather than a line. """ kinds = {type(node).__name__ for tree in _rendered_trees() for node in _nodes(tree)} - CARRIERS - declared = {node.__name__ for node in (*get_args(Predicate), *get_args(Expression), Named)} + declared = {node.__name__ for node in (*get_args(Predicate), *get_args(Expression), NamedExpression)} assert kinds == declared, ( f'tests/typesetting/golden/model.yaml reaches {sorted(kinds - declared)} and misses ' f'{sorted(declared - kinds)}. Every node the walk renders needs a case here, ' @@ -191,7 +191,7 @@ def test_the_golden_model_calls_every_operator_in_the_language(): #: What the fixture cannot reach, by module and the source text of the line. -#: A bare ``Cases`` stands under the ``Named`` node resolution builds for its +#: A bare ``Cases`` stands under the ``NamedExpression`` node resolution builds for its #: entry and nowhere else, so the arm that would print one in place is the #: type's closure rather than a case. The absent objective is the arm a #: *different* model takes — a file declares at most one — and diff --git a/tools/gallery.py b/tools/gallery.py index 131755f6..12ee6019 100644 --- a/tools/gallery.py +++ b/tools/gallery.py @@ -22,7 +22,7 @@ import yaml from mathspec import merge, override, to_spec, typeset_declaration -from mathspec.program import Add, Constant, Named +from mathspec.program import Add, Constant, NamedExpression from mathspec.typesetting import to_markdown from tools._page import ROOT, sidecar_for, splice, tab, without_header from tools._page import main as page_main @@ -333,10 +333,10 @@ def _reads_a_sum(model: Spec, name: str) -> bool: row = model.program.constraints[name] lhs, rhs = row.lhs, row.rhs return ( - isinstance(lhs, Named) + isinstance(lhs, NamedExpression) and isinstance(rhs, Constant) and rhs.value == 0 - and all(isinstance(term, Named) for term in _summands(lhs.body)) + and all(isinstance(term, NamedExpression) for term in _summands(lhs.body)) and len(_summands(lhs.body)) > 1 )