From 1b638d6cbee4045d76899d83983660d2d86941b5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:44:02 +0200 Subject: [PATCH] test: a program node nothing produces is caught, rather than waiting for a consumer to meet it Co-Authored-By: Claude Opus 5 (1M context) --- tests/fixtures/every_program_node.yaml | 42 +++++++++++++++++ tests/test_program_nodes.py | 62 ++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 tests/fixtures/every_program_node.yaml create mode 100644 tests/test_program_nodes.py diff --git a/tests/fixtures/every_program_node.yaml b/tests/fixtures/every_program_node.yaml new file mode 100644 index 00000000..63b133f5 --- /dev/null +++ b/tests/fixtures/every_program_node.yaml @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +description: every node a program can carry, in one file that lowers +dimensions: + t: { dtype: int } + g: { dtype: str } + zone: { dtype: str } +lookups: + zone_of: { over: g, into: zone } +parameters: + cost: { dims: [g] } + load: { dims: [zone] } + rate: { dims: [] } + lead: { dims: [g], dtype: int } + width: { dims: [g], dtype: int } +variables: + p: { foreach: [t, g], bounds: { lower: 0, upper: 100 } } + q: { foreach: [zone], bounds: { lower: 0, upper: 100 } } +constraints: + arithmetic: + foreach: [t, g] + expression: "-p + p * cost - p / cost + rate ** 2 <= 1" + reduced: + foreach: [] + expression: "sum(p) >= 0" + grouped: + foreach: [t, zone] + expression: "sum(p, by=zone_of) - q <= load" + pulled_back: + foreach: [t, g] + expression: "p - at(q, by=zone_of) <= 0" + translated: + foreach: [t, g] + expression: "p - shift(p, over=t, offset=lead, edge='wrap') <= 0" + windowed: + foreach: [t, g] + expression: "sum_back(p, over=t, within=width) <= 10" +objective: + sense: minimize + expression: "sum(p * cost)" diff --git a/tests/test_program_nodes.py b/tests/test_program_nodes.py new file mode 100644 index 00000000..da38e93e --- /dev/null +++ b/tests/test_program_nodes.py @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: math-spec Contributors +# +# SPDX-License-Identifier: MIT + +"""Every node a `Program` can carry is one some file actually lowers to. + +The sibling of `test_the_golden_model_carries_every_node_kind_the_walk_renders`, +one state along. That one holds the *renderer* to the AST; this holds the +*lowering* to the program, and the two cannot share a fixture: rendering +accepts more than lowering does, so the golden model carries a shift over a +variable-free expression that lowering refuses outright. + +Without this, a node can join `ExpressionNode` with nothing producing it and +the suite stays green — `assert_never` fires only where some test happens to +lower a file that uses the construct. That is how `cases:` reached a release +candidate unlowerable. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import get_args + +import math_spec as ms +from math_spec.program import ExpressionNode, Program, walk + +FIXTURE = Path(__file__).resolve().parent / 'fixtures' / 'every_program_node.yaml' + + +def _expressions(program: Program) -> list[ExpressionNode]: + """Every tree a program hangs on to, wherever it hangs it. + + Bounds and named expressions among them: a node reachable only from a + bound is still a node a consumer has to build. + """ + trees = [side for c in program.constraints for side in (c.lhs, c.rhs)] + trees += [bound for v in program.variables for bound in (v.lower, v.upper)] + trees += list(program.expressions.values()) + if program.objective is not None: + trees.append(program.objective.expression) + return trees + + +def test_every_program_node_is_one_some_file_lowers_to(): + """A node nothing produces is a node no consumer has been asked to build.""" + program = ms.to_program(FIXTURE) + reached = {type(node).__name__ for node in walk(*_expressions(program))} + declared = {node.__name__ for node in get_args(ExpressionNode)} + + assert declared <= reached, ( + f'{FIXTURE.name} lowers to none of {sorted(declared - reached)}. A node no file reaches is ' + f'one whose lowering nobody has run — add a declaration using the construct it stands for.' + ) + + +def test_the_fixture_carries_nothing_the_program_has_no_node_for(): + """The other direction, so the fixture cannot drift into asserting nothing.""" + program = ms.to_program(FIXTURE) + reached = {type(node).__name__ for node in walk(*_expressions(program))} + declared = {node.__name__ for node in get_args(ExpressionNode)} + + assert reached <= declared, f'{FIXTURE.name} lowers to {sorted(reached - declared)}, which is not a program node'