Skip to content

feat(language): a where selects several labels at once with 'in' - #283

Closed
FabianHofmann wants to merge 4 commits into
mainfrom
feat/where-membership
Closed

FabianHofmann wants to merge 4 commits into
mainfrom
feat/where-membership

Conversation

@FabianHofmann

@FabianHofmann FabianHofmann commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: implement dev/plan-254-where-membership.md — set membership in a where (#254).

Note

The following content was generated by AI.

Closes #254. A where can now select several labels at once: where: "carrier in ['ccgt', 'ocgt']" parses to a node of its own, resolves per left-hand kind, and prints as carrier ∈ {ccgt, ocgt} in all three formats.

What this changes

  • A new membership atom name in [l1, l2, …]. The left-hand side is the same three kinds a comparison takes (parameter, dimension, lookup); a variable is refused with the existing message.
  • It is a node of its own, not a parse-time desugar, so a consumer gets one relational isin filter rather than an OR chain.
  • The right-hand side is literals only. Each element is dtype-checked by the same _typed_literal a scalar comparison runs — its core is refactored to (name, value, quoted, dtype, …) so the scalar and per-element paths share one home.
  • Load refusals that name the rewrite: an empty list points to where: "False"; a duplicate says drop it; a declared name among the elements points to #258 or a bool parameter.
  • Typesetting: three walk cases at comparison precedence, and an in operator plus set-braces in the format protocol (\in \{…\} / in {…}). NOT stays generic — no ∉ case.
  • Negation stays the existing NOT; there is no not in spelling. Float left-hand sides are allowed (the OR chain already permits exact float equality).

Syntax

Now accepted — the left-hand side is a parameter, a dimension, or a lookup:

where: "carrier in ['ccgt', 'ocgt']"   # labels against a str dimension
where: "c in [1.5, 2.5]"               # floats against a float parameter (exact equality)
where: "flag in [1]"                   # a number against a bool parameter
where: "snapshot in ['2030-01-01', '2030-06-01T06:00']"  # ISO dates against a datetime dimension
where: "carrier IN ['ccgt']"           # `in` is case-insensitive, like NOT/AND/OR
where: "NOT (carrier in ['ccgt', 'ocgt'])"       # negate with the existing NOT
where: "carrier in ['ccgt'] AND p_max > 0"       # combines like any atom

Both quote characters, escapes, and negative numbers carry over per element unchanged, and a single-element list is fine.

Out of scope — refused at load, each message naming the rewrite:

where: "carrier not in ['ccgt']"       # no `not in` spelling — write NOT ( … in [ … ] )
where: "carrier in allowed_carriers"   # a declared name on the right is data-driven membership (#258)
where: "carrier in []"                 # an empty list — write where: "False" for an always-false mask
where: "carrier in ['ccgt', 'ccgt']"   # a duplicate selects nothing extra — drop it
where: "p in ['ccgt']"                 # a variable on the left — a mask is built before variables exist

Also not in this PR: membership over an expression or a lookup pair (only a bare name on the left).

Why

The design was settled in the issue thread. It sits inside the existing first admissibility row in ceiling.md (relational ∩ local, an isin filter), so this is not a ceiling move — flagging that reading here rather than editing the page.

Breaking change (free, alpha stream): in is now reserved between a name and the rest of an atom. A model that used in as a bare token there no longer parses.

Verification, decisions, and departures

Gate: pixi run ci in full — lint clean, 865 passed, docs built --strict, 27 TeX documents compiled.

Built and reviewed by agents: one implementer followed the plan end to end; three review lenses (correctness/exhaustiveness, typesetting/generated artifacts, tests/messages) then checked the diff and each returned "sound as it stands". Four minor findings were addressed:

  • The declared-name refusal reported a 0-indexed list position; the number is dropped, since the quoted value already identifies the element.
  • Duplicate detection now runs on the typed values (so two spellings of one instant, T00:00 and T00:00:00, collide) and renders numbers via :g (0, not 0.0), matching the rest of the where path. A reviewer's illustration using '2030-01-01' vs '2030-01-01T00:00' was set aside: in Python a date does not equal a datetime at midnight, so those are genuinely different values, not a duplicate. Two new tests pin the real cases.

Departure from the plan (Part 2 asks this be named): the docs atoms table folds the three per-kind rows into one row spanning parameter/dimension/lookup, with the refusals in following prose. The plan said "a row per kind"; the fold is the DRY choice and the content (EBNF production, #258 pointer, extended float-equality note) is complete.

Generated files: schema is untouched (a where is a string — confirmed by the drift test). The golden fixture gained an in mask exercising all three kinds; the .out files were regenerated, not hand-edited, and their diff read as the review.

Delete-the-guard: each of the three refusals is load-bearing and has a test that fails without it; the removal-and-watch was reasoned, not mechanically performed for every guard.

@read-the-docs-community

read-the-docs-community Bot commented Aug 31, 2026 •

Copy link
Copy Markdown

@FBumann FBumann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See stacked PR. Add some tests

…e-program

test(lowering): a where atom reaches the program as the file wrote it
@FBumann

FBumann commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Same here. SOrry for the churn. But I made the where parser fully internal.

This should also make this easier or at least more obvious to implement!

@FBumann FBumann added the area: where What a where predicate may say label Sep 15, 2026 — with Claude
@FBumann

FBumann commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

@FabianHofmann I would tend to close this unmerged. This feature doesnt fit the agreement about #121

Whats your take?

@brynpickering

Copy link
Copy Markdown
Contributor

@FBumann I tend to agree that we close it unmerged. I can see it coming back to hurt us later as users get annoyed by not being able to define values directly in math. What I would like is to have some dimensions/parameters defining an enum and then allowing references to those members later, including in cases such as this. In that case, a feature like this would be fine as the spec can validate it as it's internally consistent.

FBumann commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Note

The following content was generated by AI.

Closed unmerged, on the decision in this thread: a literal list writes label
values into the math file, which #121 settled against.

The follow-up named here — an enum on a dimension or a parameter, with
membership referencing declared members — is #518. #254 stays open and is
blocked on it. The branch feat/where-membership is not deleted, so the
implementation is there if the enum lands.


Generated by Claude Code

@FBumann FBumann closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: where What a where predicate may say

Projects

None yet

Development

Successfully merging this pull request may close these issues.

set membership in a where: carrier in ["ccgt", "ocgt"]

3 participants