Repository navigation
fix(language): a divisor that a where, a case or a masked variable guards loads under missing: 0, and so does a variable number outside its bounds - #942
Merged
FBumann merged 3 commits intoOct 22, 2026
Conversation
…ards loads under missing: 0, and so does a variable number outside its bounds The divisor refusal looked at the divisor alone, so `where: c > 0` over `y <= d / c` was refused although no row divides where `c` is missing. A pass after lowering now reads the guards in force: the conjuncts of the constraint's where, of a case's when, and of the mask of a variable that multiplies the quotient. A sum, a window, a shift or a lookup drops the guards from outside it. A quotient in a condition keeps the unguarded check. A variable's missing: number no longer has to lie inside its literal bounds, which hold only inside the mask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ68heVBrHxKUKsXxqJrXX
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ68heVBrHxKUKsXxqJrXX
Documentation build overview
32 files changed ·
|
A masked variable spared a divisor because the product is absent where the
variable is absent. A variable with a missing: number is not absent outside
its mask: it reads that number, so `sum(y / c, over=g)` with
`y: {where: c > 0, missing: 1}` divided 1 by 0 at every missing row of c,
and loaded. Only a missing: absent variable spares a divisor now.
Found by a differential fuzzer that evaluates random specs on random data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ68heVBrHxKUKsXxqJrXX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
Bug fixes only, stacked on #894. Two load checks that #894 adds refuse valid files.
missing: 0on a divisor now loads where a guard leaves the missing row out. A variable'smissing:number may now lie outside its bounds.A later ask to fuzz the check found one bug in this PR's own guard: a variable with a
missing:number spared a divisor.0aa8a62fixes it.Case 2: a guarded divisor
#894 refused a divisor by looking at the divisor alone. Each file below was refused, although no row divides where
cis missing:cases:region withwhen: c > 0;sum(y / c, over=g)whereyhaswhere: c > 0;p / p_nomin a reported expression.missing: absentis not a fix for these. It deletes thecoefrow wherecis missing, soxis unbounded there: specsolve solvedx[b] = 100instead of0.The check now runs after lowering, in
_divisors.py. A guard spares a divisor when the guard is false at every missing row:c > 0,c != 0, the bare namec, or a named condition that holds one of these. The guard counts when it is a conjunct of one of three:where;whenof the case that holds the quotient;whereof amissing: absentvariable that multiplies the quotient. A variable with amissing:number reads that number outside its mask, so its mask guards nothing.A
sum, a window, ashiftor anatreads other coordinates. So a guard from outside one of them is dropped inside it, and a guard under anORor aNOTspares nothing. A named expression is still checked on its own frame, so its guard goes in its owncases:. A quotient inside a condition keeps the unguarded check.The refusal now also suggests the guard:
…or guard the quotient with a where such as "c > 0". A refusal inside a case now names the entry, without the case.Case 3: a variable number outside its bounds
#894 refused this with
missing: 0 is below bounds.lower 1.0. The bounds hold only inside the mask, so a unit of one to four modules where it is a candidate has 0 modules elsewhere. A parameter bound already loaded the same file. The domain check stays: abinaryvariable still takes0or1.Verification
Reproduced first. Each new loading case was a strict
xfailonda86544(the head of #894) and failed with the refusal it reports. So was[a-mask-of-a-variable-that-reads-a-number-outside-it]on41bed1d. Each marker came off with its fix, and its XPASS is the mutation row for that guard.Gates on
0aa8a62:pixi run lintpasses.pixi run testpasses: 3097 passed, 1 skipped.pixi run docs-buildpassed on07cdf5a.0aa8a62changes one bullet in absence.md, and prettier and the lint pass on it.compile-texwas not run, because the container cannot download the TeX bundle. CI runs it.Mutation table on
07cdf5a. A script deleted each guard in turn and ran the full suite. It restored the file withgit checkout --and dropped__pycache__before and after each run. The tree came back clean.[a-masked-numerator],[a-masked-factor]whenspares a quotient in its region[a-case][a-guard-outside-a-shift][a-guard-at-another-coordinate]whenspares a divisor that is a case[a-case-in-the-divisor][has-a-row][a-guard-that-admits-zero]test_a_divisor_guarded_against_a_missing_row_loads[a-named-condition]ANDspine spares[a-guard-under-an-or],[a-negated-guard][a-quotient-in-a-where]missing: absentvariable spares (0aa8a62)[a-mask-of-a-variable-that-reads-a-number-outside-it]Coverage moved. The
below-lowerandabove-upperrefusal cases are gone.test_a_variable_number_outside_its_bounds_loadsnow asserts that the same shapes load.Fuzzing
A differential fuzzer drew 12,000 random specs, together with a Python model of what each one means. A spec holds one dimension,
missing:values, guards, cases, shifts, sums and masked variables. The model ran each spec on random data with missing rows.On
41bed1d, 225 seeds were soundness failures of one kind: a masked variable withmissing: 1spared a divisor.0aa8a62fixes that, and on it none of that kind remain.The other soundness failures exist on #894's head too, so this PR leaves them:
missing:value other than 0 that cancels, such asmissing: 1withx / (1 - c)(about 250 seeds);missing: 0parameter, such asx / ((c + 1) - 1);when: d > 0, expression: 1 - 1.The 70 precision seeds are mostly rows that are never built, because their masks contradict each other. Two kinds are real and conservative: a guard written
NOT (c != 1), and a masked variable in another term of the same row.#893's fuzzer ran 6,000 mutants of the examples on
0aa8a62, withneutralremoved from its choices. It found only the known #890 canonical-form order.Not done
*_nom_modparameters staymissing: absent. Their divisors are guarded by*_p_nom_mod > 0, somissing: 0would load now. Whether to change them back is a call for docs(pypsa): a PyPSA schedule, weight or limit that the data leaves out is absent rather than zero, and a committable extendable unit is assumed to have its big-M #912's scope.neutralgave is not restored: a missing row on a right-hand side or in a bound reads as0without an error. That is a decision for feat(language)!: a missing row reads as the value the file names, andmissing: neutralis gone #894, not a bug fix.missing: neutralis gone #894 are not fixed here.🤖 Generated with Claude Code
https://claude.ai/code/session_01YZ68heVBrHxKUKsXxqJrXX