From 071375b8449d9cda8121677ef1a000cc6768b397 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 15:18:02 +0000 Subject: [PATCH 1/3] refactor(program): the node a use of a named expression stands as is NamedExpression, beside NamedMask, and Named is gone mathspec.program.Named is renamed NamedExpression, so the two pass-through nodes read as a pair: NamedExpression over an expressions: entry's arithmetic, NamedMask over a masks: entry's predicate. No alias is kept; a consumer that imports Named imports NamedExpression instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK --- CHANGELOG.md | 1 + docs/reference/reading.md | 2 +- src/mathspec/_expression_resolver.py | 2 +- src/mathspec/boundedness.py | 6 +++--- src/mathspec/dimensions.py | 6 +++--- src/mathspec/lowering.py | 18 +++++++++--------- src/mathspec/program.py | 12 ++++++------ src/mathspec/resolution.py | 14 +++++++------- src/mathspec/typesetting/walk.py | 6 +++--- tests/test_boundedness.py | 2 +- tests/test_degree.py | 2 +- tests/test_expansion.py | 8 ++++---- tests/typesetting/test_golden.py | 6 +++--- tools/gallery.py | 6 +++--- 14 files changed, 46 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b6eb6ee..4613747e 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 ([#PR](https://github.com/energy-models/mathspec/pull/PR)) - 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)) ## 0.2.1 (2026-10-01) diff --git a/docs/reference/reading.md b/docs/reference/reading.md index dcfa17df..fae75dd7 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 9932764e..c93e1d9a 100644 --- a/src/mathspec/_expression_resolver.py +++ b/src/mathspec/_expression_resolver.py @@ -177,7 +177,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 320353b3..e6390912 100644 --- a/src/mathspec/boundedness.py +++ b/src/mathspec/boundedness.py @@ -25,7 +25,7 @@ Expression, GroupSum, Multiply, - Named, + NamedExpression, Negate, Parameter, Power, @@ -122,7 +122,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 '-' @@ -163,7 +163,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 | GroupSum | Pullback | Translate | WindowSum | Cases | Named): + if isinstance(node, Sum | GroupSum | Pullback | 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 0a053793..54cbccd2 100644 --- a/src/mathspec/dimensions.py +++ b/src/mathspec/dimensions.py @@ -31,7 +31,7 @@ GroupSum, Mask, Multiply, - Named, + NamedExpression, Negate, Parameter, ParameterComparison, @@ -76,7 +76,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): @@ -98,7 +98,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 f2605ff9..b53136c1 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(): @@ -268,7 +268,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 @@ -281,7 +281,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 = [] @@ -298,7 +298,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 @@ -324,11 +324,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. @@ -346,7 +346,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 241f6ddd..77c7e1b6 100644 --- a/src/mathspec/program.py +++ b/src/mathspec/program.py @@ -65,7 +65,7 @@ 'Mask', 'MaskDeclaration', 'Multiply', - 'Named', + 'NamedExpression', 'NamedMask', 'Negate', 'Not', @@ -363,7 +363,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 @@ -403,13 +403,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,) @@ -961,7 +961,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. @@ -1337,7 +1337,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 738e5124..976bf4d4 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 @@ -471,7 +471,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 @@ -484,7 +484,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] = [] @@ -507,7 +507,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 2a161022..3e772bde 100644 --- a/src/mathspec/typesetting/walk.py +++ b/src/mathspec/typesetting/walk.py @@ -33,7 +33,7 @@ GroupSum, Mask, Multiply, - Named, + NamedExpression, NamedMask, Negate, Not, @@ -320,7 +320,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 @@ -413,7 +413,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 ed064c51..1c1487e4 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 0aa3a1f0..719407a8 100644 --- a/tests/test_degree.py +++ b/tests/test_degree.py @@ -131,7 +131,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 32dae7da..3a909ae1 100644 --- a/tests/test_expansion.py +++ b/tests/test_expansion.py @@ -13,7 +13,7 @@ from mathspec.errors import LanguageError from mathspec.expansion import parse_and_expand -from mathspec.program import Multiply, Named, Parameter, Sum, Translate, Variable +from mathspec.program import 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 @@ -35,7 +35,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) @@ -131,7 +131,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) @@ -140,7 +140,7 @@ 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'))), ('generator',)), ( + assert resolved == Sum(NamedExpression('gen_cost', Multiply(Variable('p'), Parameter('cost'))), ('generator',)), ( 'the body is inlined resolved and the name kept, for the typesetter to define it once' ) assert isinstance(resolved, Sum) diff --git a/tests/typesetting/test_golden.py b/tests/typesetting/test_golden.py index 06bdaac0..c835a7d3 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, GroupSum, Named, Predicate, Pullback, Sum, Translate, WindowSum +from mathspec.program import Dual, Expression, GroupSum, NamedExpression, Predicate, Pullback, 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, ' @@ -190,7 +190,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 a6252f75..96163472 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 @@ -299,10 +299,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 ) From c32749f12c6dac389716984bca9a5d87af4081f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 15:18:28 +0000 Subject: [PATCH 2/3] docs: the changelog line links #822 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4613747e..2a420972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +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 ([#PR](https://github.com/energy-models/mathspec/pull/PR)) +- 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)) - 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)) ## 0.2.1 (2026-10-01) From 5f7f631732e47ca7ab15bf79862fdfa1a4785e30 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 15:20:02 +0000 Subject: [PATCH 3/3] docs: the changelog line marks the rename as breaking Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a420972..7a52d644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +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)) +- 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)) - 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)) ## 0.2.1 (2026-10-01)