Skip to content

Run more ordinary Fe at compile time - #1582

Open
micahscopes wants to merge 3 commits into
masterfrom
pr/const-eval-capabilities
Open

micahscopes wants to merge 3 commits into
masterfrom
pr/const-eval-capabilities

Conversation

@micahscopes

@micahscopes micahscopes commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

This lets compile-time evaluation run more ordinary Fe, so a helper does not need a separate const-only version.

  • A const fn can update a local through a mut borrow

    const fn set(value: mut u8) { value = 42 }
    
    const fn answer() -> u8 {
        let mut x: u8 = 0
        set(value: mut x)
        x
    }
    

    answer() now evaluates to 42; on master, evaluation rejects the borrow with InvalidProviderUse. Borrows through pointers or of provider-backed locals still fail, since compile-time evaluation has no memory outside its own call frames. Evaluation also fails if a function returns a borrow of its own local.

  • A const fn can take an immutable trait provider with uses, and a caller can supply one with with

    const fn consume<T>() -> T uses (evidence: Compute<Item = T>) {
        evidence.compute()
    }
    
    const fn answer() -> u256 {
        with (Compute<Item = u256> = Number { value: 11 }) { consume<u256>() }
    }
    

    On master, the uses clause and the with are both errors. A test runs the same code at compile time and at runtime and checks that the results agree.

  • Rebuild the compiler when a file is added to core or std. Before, a new library file did not trigger a rebuild, so a cached build kept the old library. The fingerprint only invalidates caches and is never compared, so its per-platform value is harmless.

A few decisions:

  • Providers are passed as values, matched to the callee's effect by binding rather than by argument position
  • Mutable providers, storage effects, and extern functions with effects remain rejected
  • The error for with in a const fn (8-0056) is removed, so that code is no longer emitted. The two remaining errors for effects in a const fn (8-0055, 8-0063) now say which effects are supported

How this fits mainline:

Overlap with open PRs: #1576 conflicts in const_check.rs and machine.rs. It moves the const fn effects check, which this PR changes from "any effects" to const_effects_supported, so whichever lands second carries that change into the moved check. #1455 and #1506 conflict in const_check.rs as well. Other open PRs that touch these files conflict only where they already conflict with master.

Planned later PRs in this series (trait evidence, named implementation selection, generated code) build on the provider support here.

micahscopes added a commit that referenced this pull request Sep 23, 2026
Rename the placeholder newsfragments to the pull request number, following the repository's <PR>.<type>.md convention.
micahscopes added a commit that referenced this pull request Sep 23, 2026
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.
@micahscopes
micahscopes force-pushed the pr/const-eval-capabilities branch from 82f1b5e to ff45c5a Compare September 23, 2026 13:19
micahscopes added a commit that referenced this pull request Sep 23, 2026
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.
Const evaluation rejected every borrow that carries address-space
metadata. Explicit borrows of locals (`mut x`, `ref x`, and `mut`
arguments) carry `Memory` metadata too, so a const fn that updated a
local in place, or passed one to a helper by `mut`, failed to evaluate.

Accept a `Memory` borrow whose place is rooted in a frame local and does
not go through a dereference. Borrows through pointers and of
provider-backed locals still fail with `InvalidProviderUse`, since
constant evaluation has no memory outside its frames. A request cannot
carry a pointer argument (request inputs are verified before
execution), so the pointer case is tested inside the machine tests.

When a frame returns a reference into itself, the value is read out if
the declared result type is not a borrow, and the evaluation fails with
`InvalidBorrow` if it is, so a dangling reference cannot escape. A
reference into a caller's frame is returned unchanged.

This fits the transactional evaluator from #1556 as is: machine
references stay private to one attempt, and a blocked attempt discards
its frames, so a replay after specialization starts from the original
locals. #1556 recorded the borrowing restriction as intentionally kept
(`docs/ctfe-deferral-audit.md`, "Borrowing scope") and pinned it with
`ctfe_typed_read_preserves_provider_borrow_rejection`. That test now
checks that the typed read through `ref value` evaluates to 7, under
the name `ctfe_typed_read_through_frame_local_borrow_evaluates`, and the
audit's borrowing paragraph describes the admitted borrows.
fe-common embeds `ingots/core` and `ingots/std` with rust-embed. The
compiler records only the files that the previous expansion embedded as
dependencies, so adding a new file to either library (for example a new
`core` module) did not rebuild fe-common, and a cached build kept
serving the old library.

Add a build script that reruns when either directory changes and hashes
the paths and contents of both trees into `FE_BUILTIN_FINGERPRINT`.
`stdlib.rs` reads that variable, so rustc and compiler caches such as
sccache treat the library's file set as an input.

The fingerprint hashes native paths and raw file bytes, so its value
differs between platforms and line-ending settings. It is only used to
invalidate caches and is never compared.
Const functions could not use effects at all: a `uses` clause on a
`const fn` was an error, a call to any function with effects was an
error, and every `with` expression in a `const fn` was rejected. A
trait provider is an ordinary value, so an immutable one can be
evaluated at compile time like any other argument.

Allow a `const fn` to declare immutable trait effects and to supply
them with `with`. During evaluation a call passes each provider as a
value into the callee's effect slot, matched by the effect's binding
index rather than by its position among ordinary arguments. A `with`
provider that lowering captured as a place (#1564) is read from that
place. The checker still rejects mutable effects, type-keyed (storage)
effects, and extern functions with effects.

Deferral needs no special case. Since #1556, a call whose input is
still generic blocks the whole attempt, which is discarded and replayed
after specialization; the replay recomputes the providers from the
caller's body. Term extraction already declines calls with effect
arguments, so a provider never enters a retained term. A test checks
that an effectful call blocks while generic and evaluates with its
provider once specialized, like a pure call.

Effect slots follow the same rules as ordinary argument slots: a
request that supplies no providers for an effectful function is
unsupported (`NotConstEvaluable`), and a slot count or index mismatch
is an invalid operation, like the existing argument arity check.

The `with`-in-const-fn diagnostic (8-0056) is removed, and the two
remaining const-effect messages now say what is supported.
@micahscopes
micahscopes force-pushed the pr/const-eval-capabilities branch from ff45c5a to 08e8b62 Compare September 25, 2026 04:15
micahscopes added a commit that referenced this pull request Sep 25, 2026
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.
@micahscopes
micahscopes marked this pull request as ready for review September 25, 2026 04:51

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant