Skip to content

feat(spec): the canonical form prints each condition from its parsed tree, so two conditions that differ only in spelling write one text - #927

Merged
FBumann merged 2 commits into
claude/pensive-goldberg-s12y69from
feat/canonical-conditions
Oct 23, 2026
Merged

FBumann merged 2 commits into
claude/pensive-goldberg-s12y69from
feat/canonical-conditions

Conversation

@FBumann

@FBumann FBumann commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: And also fix the canonocal stuff in a separate PR

Note

The following content was generated by AI.

The canonical form now prints every where:, when:, condition: and holds: from its parsed tree. Conditions that differ only in the order of AND or OR, keyword case, brackets or spacing write one text.

Stacked on #924.

Written Canonical
p_max > 0 and cost > 0 cost > 0 AND p_max > 0
(load < 0) or load>1 load < 0 OR load > 1
(b or a) and c (a OR b) AND c
not a > 0 NOT (a > 0)
p_max + cost > 0 cost + p_max > 0
p_max > 0 and 0 < p_max kept apart: the sides of a comparison keep their places, as in an expression
Method
  • printed_condition in _canonical.py prints a parsed condition. A chain of one connective is flattened, and its operands are sorted by the text each prints as, as the form already sorts the terms of a sum. AND, OR and NOT print in upper case. A bracket stays around an OR under an AND, and under NOT around anything but a name, a literal or a call. A side of a comparison goes through normalised, as an expression does. A quoted label keeps its escape.
  • _CONDITION_KEYS names the four fields, next to _EXPRESSION_KEYS. _BARE names what an entry written as a bare string is, by section: an expression, a condition or an assumption. It replaces the bare_is_expression flag.
  • Nothing reads the operands of AND or OR in order. Resolution and the exclusivity proof treat both sides alike, so sorting them changes no program.
  • docs/reference/reading.md lists what the form does to a condition, and no longer says conditions are left as written.
Tests, gates and mutation table

Tests.

  • New: test_a_condition_is_written_one_way has six cases, one per place a condition sits: a variable's and a constraint's where:, the side of a comparison, a condition written as a bare string, an assumption's holds: and where:, and the when: of a case. Each one failed before the fix: two texts, not one.
  • New: test_a_condition_is_printed_one_way has twelve cases for the printer, among them every bracket rule, a count, a column of a relation and an escaped label.
  • New: test_a_condition_in_the_form_holds_where_the_one_it_came_from_holds generates 500 conditions over four flags, with any case and any brackets. It checks each one on all 16 assignments, and checks that the form is a fixed point.
  • Moved: the the-conditions-of-a-where case of test_an_order_the_form_keeps_is_a_difference_in_the_text asserted the old behaviour. It is now the-sides-of-a-comparison-in-a-condition, the one order the form still keeps in a condition.
  • test_the_form_loads_and_is_already_in_the_form already runs on every example. With this change, it also checks that every condition there prints into text that loads, and that a second pass changes nothing.

Gates on 52bb202. abe796c adds only the changelog line.

  • pixi run lint: passed.
  • pixi run test: 3556 passed, 1 skipped.
  • pixi run docs-build: passed.
  • Every generator re-run: no diff.
  • compile-tex: not run, because Tectonic cannot download its bundle in this sandbox. CI runs it.

Mutation table on 52bb202. A script made each change, dropped __pycache__ before and after, ran the full suite, restored with git checkout --, and checked that the tree came back clean each time.

Change Caught by
a condition field kept as written 5 failures, among them test_a_condition_is_written_one_way[the-operands-of-and-and-the-case-of-the-keyword]
a condition or an assumption written as a bare string kept as written 1 failure: test_a_condition_is_written_one_way[a-condition-entry-written-as-a-string]
the operands of AND and OR in the order written 11 failures, among them test_a_condition_is_written_one_way[the-operands-of-or-the-brackets-and-the-spacing]
a chain of one connective not flattened 1 failure: test_a_condition_in_the_form_holds_where_the_one_it_came_from_holds
no bracket around an OR under an AND 8 failures, among them test_the_form_loads_and_is_already_in_the_form[pypsa]
no bracket under NOT 17 failures, among them test_the_form_loads_and_is_already_in_the_form[generator_commitment]
a condition call's keywords in the order written 1 failure: test_a_condition_is_printed_one_way[a-calls-kwargs-are-sorted]
a quoted label printed without its escape 1 failure: test_a_condition_is_printed_one_way[a-quoted-label-keeps-its-escape]
Not done
  • The sides of a comparison are not turned to one direction (0 < p_max to p_max > 0). The form does not do that for an expression's comparison either, and doing it for both is a separate decision.
  • to_yaml() still writes a condition as the file wrote it. Only the canonical form reprints it, as for an expression.
  • The typeset document prints a condition in the order the file wrote it, as it prints the terms of a sum.

🤖 Generated with Claude Code

https://claude.ai/code/session_019j1TmBS3MvzmuarWjhCHHu

…tree, so two conditions that differ only in spelling write one text

A `where:`, `when:`, `condition:` or `holds:` string, and a condition or an
assumption written as a bare string, is parsed and printed rather than kept
as written. `AND`, `OR` and `NOT` print in upper case, the operands of a
chain of `AND` or of `OR` are sorted, a bracket stays only where the grammar
needs it or around a comparison under `NOT`, and each side of a comparison is
normalised as an expression is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j1TmBS3MvzmuarWjhCHHu
@read-the-docs-community

read-the-docs-community Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

FBumann commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Prompt: i think the sorting one should replace the others with the clear goal of making everything sorted and pedictable, saying the others are superseeded and making them drafts. Not closing them

Note

The following content was generated by AI.

Superseded by #924. This branch was fast-forwarded into #924 (abe796c), together with #918 and #923. The canonical form of a condition is unchanged there. This PR stays open as a draft for its review history.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants