Add const where predicates - #1576
Open
micahscopes wants to merge 14 commits into
Open
micahscopes wants to merge 14 commits into
micahscopes wants to merge 14 commits into
Conversation
micahscopes
force-pushed
the
pr/checked-generation-const-predicates
branch
4 times, most recently
from
September 23, 2026 22:10
96b4bba to
2cda47f
Compare
This was referenced Sep 24, 2026
fe-mir and fe-hir each had two #[test] functions with their own trybuild::TestCases. Under nextest every test runs in its own process, and both processes build their single case as `trybuild000` in the same scratch project under target/tests/trybuild/<crate>. trybuild's cross-process lock is a best-effort lockfile that another process takes over after 1.5 s without a refresh, so on a slow build one process can rewrite the shared manifest while the other is compiling. The other then checks its sibling's file, finds no error for its own case, and reports "Expected test case to fail to compile, but it succeeded". This was seen on Windows CI, where the two fe-mir tests overlapped and finished 0.2 s apart. Keep one `ui` test per crate that globs tests/ui/*.rs, the pattern trace-facts already uses. Both cases now run in one process as distinct bins, and new cases cannot reintroduce the race.
`DiagnosticPass::ErrorLower` and `DiagnosticPass::SemanticBorrowck` both used pass code 16, so `error[16-0003]` could name either a generic `#[error]` struct or a borrow-checking error. Give error lowering the unused code 13. Only the four `#[error]` diagnostics change their rendered code.
An explicit const argument such as the `3` in `Window<1> {}.take<3>()`
or `Window<1>::take<3>()` was lowered without an expected type and
stayed unevaluated. A free function's arguments are evaluated when its
type is applied, but a method's and an associated function's are not,
so constant evaluation of such a function that reads its const
parameter panicked with "instantiated constant template retains its
value description".
Evaluate an explicit const argument against the parameter's type when
it is unified, as type application does. An argument whose expected
type still holds an inference variable, or that fails to evaluate, is
left as before.
The new `fe test` fixture failed with that panic before this change.
Parse boolean conditions in `where` clauses next to type bounds, and check each condition that mentions no generic parameter where it is declared, even if the item is unused. A condition passes only when it evaluates to `true`; type errors, non-const operations and failed evaluation are errors. Const conditions in generic scopes are rejected until use-site checking exists. `where T` without `:` now parses as a const condition, so a lone path that names a type reports the missing trait bound instead of a type used as a value. The tree-sitter grammar, its vendored wasm build and the formatter learn the new predicate form. A predicate block goes through semantic borrow checking like any const body. Its test needs frame-local borrows in const evaluation (#1582) to evaluate the valid case, so it is added with that change.
Allow const `where` conditions on top-level generic functions. The declaration checks each condition's type and const-ness, and every use of the function discharges its conditions after inference: a concrete condition is evaluated, and a condition that still mentions a caller's parameter must match one of the caller's own conditions exactly after substitution. Function values and calls in anonymous constant bodies use the same check. Inference and discharge are separate queries, so evaluation can use a finished inference while a condition is checked. A failed requirement is reported once, at the call site, with the reason.
An anonymous constant in a generic function's signature or body, such as an array length, could not use the function's conditions, so a call to a constrained helper there always failed. Let such constants forward the enclosing function's conditions by the same exact-match rule as calls. A predicate still cannot use the function's conditions to justify itself, including through constants nested inside it.
Allow const `where` conditions on generic structs. Every concrete use of the type must satisfy them: construction, type annotations, defaults, aliases, fields and trait arguments, whether declared or inferred. A generic use forwards a condition only by stating the same condition, as with generic functions. Mentioning `Bounded<N>` does not establish `N > 0`. Each unmet condition is reported once, where the type enters: at the innermost written type, or at the expression that instantiates it. An enclosing type does not repeat a failure a nested written type reported. A call's generic arguments are either written in its path, and checked there, or inferred from expressions that are checked themselves, so a function-typed expression is not checked for them. Patterns, binding uses, blocks and branches only carry a type that entered elsewhere. A constrained record must be fully applied where it is used as a type, since nothing would carry its conditions through a higher-kinded parameter.
A tuple-variant constructor such as `Choice::Value` has direct-call lowering but no value representation, so using one without calling it, for example `let f = Choice::Value`, panicked during lowering. Report an error at the path instead and mark the expression invalid so later lowering skips it.
Allow const `where` conditions on generic enums, with the same rules as records. The conditions apply to the whole type, so constructing a unit variant of `Choice<0>` fails too, and every variant's payload types must be valid even when that variant is never built. Payloads may forward the enum's conditions into nested constrained types, and a predicate can still rely on ordinary trait bounds such as `T::ALLOWED`.
Allow const `where` conditions on inherent methods and associated functions, over both the impl's parameters and the method's own. The conditions of the method that ordinary resolution picked are discharged at each receiver or qualified call; they never take part in choosing a method. The checker maps the impl's parameters onto the method's leading generic slots before applying the call's arguments. The unsupported-context error now names where const predicates are and are not supported.
A block condition had to be parenthesized, as in `where ({ ... })`,
because a `{` after `where` could also open the item's body. Accept a
bare block as a predicate right after `where` or after a `,`, when what
follows it continues the header: a `,`, another predicate, or the
item's own `{`. A `{` after a completed predicate is always the body,
so `where T: Copy { body }` and `where T: Copy, { body }` keep their
meaning, including when the body starts on the next line. Trait and
extern function declarations, whose bodies are optional, read a block
right after `where` as a predicate.
The tree-sitter grammar accepts a block predicate as well, and the
vendored wasm build is regenerated. The tooling grammar cannot look
past the block, so it reads the `where T: Copy, { body }` form as a
predicate with no body.
The driver tests check substrings of the errors, so duplicate or misplaced labels pass unnoticed. Snapshot the rendered errors for declaration failures, calls to generic functions and inherent methods, record and enum uses, recursive requirements, and the contexts that are rejected.
A type whose arguments still hold an inference variable is not checked at that use, and a layout hole leaves such a variable without a "type annotation needed" error. Check that the concrete arguments are still checked where the type is declared, next to a valid control case. Also cover a record requirement that reaches its own record through a function's requirement. The cycle must be rejected with the same diagnostics in every query order.
check_body cloned the whole TypedBody out of the tracked infer_body query, and the tracked check_* queries then kept that copy, so every function, const and anonymous const body was stored twice. Share the tables behind an Arc, with copy-on-write for the few places that mutate a TypedBody. The write-only has_diagnostics flag stays outside the Arc, so setting it never copies. The shared tables type is private to the crate, and query results are unchanged.
micahscopes
force-pushed
the
pr/checked-generation-const-predicates
branch
from
September 25, 2026 04:34
2cda47f to
3499bc4
Compare
micahscopes
marked this pull request as ready for review
September 25, 2026 05:11
This branch has not been deployed
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.
This lets an item state a boolean condition in its
whereclause, such asN > 0, and have the compiler check it wherever the item is used.A
whereclause can hold boolean conditions next to trait boundsbounded<1>()is accepted andbounded<0>()is rejected, even though the body never readsN. On master, awhereclause takes only trait bounds, sowhere N > 0is a parse error.A generic caller satisfies a condition by stating the same one
The same holds in anonymous constants of the caller's signature and body, such as an array length.
Structs, enums and inherent methods take conditions too
Every use of
Bounded<0>is rejected, including unused type annotations, defaults and aliases. For a constrained enum, unit variants such asChoice<0>::Emptyare rejected too. Method conditions are checked on the method that ordinary resolution picks.A condition without generic parameters is checked where it is declared, even if nothing uses the item
A condition that needs several lines can be written as a block
where Twithout:now parses as a condition, and type checking reports the missing trait bound. On master it is a parse error.A tuple-variant constructor used as a value, such as
let f = Choice::Value, is an error. On master it panics during lowering.#[error]lowering errors get code prefix 13. On master they share prefix 16 with borrow-checking errors, soerror[16-0003]can name two different errors.A few decisions:
N > 1does not establishN > 0,, another predicate or the item's own{follows it, sowhere T: Copy, { body }keeps its meaning. In trait and extern function declarations, a block right afterwhereis always a conditionLimits:
implblocks themselves and on items nested in generic items are rejected with an error that says sowhere T: Copy, { body }as a condition without a body, since it cannot look past the block; the compiler's parser is unaffectedAlso included:
Window<1> {}.take<3>(), are evaluated against the parameter's type, as a free function's are. On master, compile-time evaluation of such a function panics when it reads that parameter. It is its own commit, with a test.fe fmtand the tree-sitter grammar understand the new syntax.Recent mainline changes:
static_assertdoes. An evaluation that fails or still waits on an unknown value is an error.error[3-0057], since Fix CTFE evaluation of symbolic array repeats #1556 took codes 50 to 56.Overlap with open PRs (
git merge-treeagainst their current heads, compared with master): this PR adds conflicts with #1535 inty_check/callable.rs, in the loop that evaluates explicit const arguments, and inty_check/env.rs; with #1582 inconst_check.rs, where it changes the effects condition that this PR moves; and with draft #1536 ininstance/template.rs. #1535 already conflicts with master incapability/index.rs,capability/shape.rsandconst_ty.rs, and #1582 inctfe/machine.rs. Other open PRs, including #1589, get no new conflicts.Const predicates on trait impls would build on this requirement checking.