Skip to content

First-class obligations, derive providers, and trait-resolution unification#1506

Draft
micahscopes wants to merge 36 commits into
argotorg:masterfrom
micahscopes:fco-sgk
Draft

First-class obligations, derive providers, and trait-resolution unification#1506
micahscopes wants to merge 36 commits into
argotorg:masterfrom
micahscopes:fco-sgk

Conversation

@micahscopes

@micahscopes micahscopes commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

First-class obligations, derive providers, and trait-resolution unification

Fe had grown several overlapping mechanisms for one job: deciding which trait impl satisfies a requirement, and where the compile-time policy about that decision lives. This branch coordinates them behind a single selection discriminator and moves the policy out of hardcoded Rust into Fe itself. It is one architectural change, presented end-to-end. Please review it as a design.

Because it is large, the honest framing up front: the derive/provider/ABI/identity spine is the finished, strongly-tested core, and the coexistence/cascade half (scoped selection, default tier, single-impl authority) is newer, carries the open design questions, and is still being hardened. If you would rather review in two passes, the recommended split at the bottom follows exactly that seam.

What it does

  • Coordinated trait resolution. The solver, the effect/capability environment, and coherence now share one selection discriminator and one verification primitive, and the old separate global coherence checker is deleted. This is coordination through a shared discriminator, not a single literal walk: the effect environment is deliberately kept distinct (folding it in would be a facade), and the coexistence rules add candidate-selection paths that are still being hardened (see Known limitations).

  • Derive providers, recognized by what they are. A provider is compile-time code that generates a trait impl (it is how derive(Eq) produces an impl Eq). Providers and the compile-time capabilities they use are identified by resolved path (core::derive::*), not by matching string names, so a rename or alias cannot fool the recognizer, and the authority to do privileged things rides the normal effect system rather than a side channel. (Two documented string-name fallbacks remain in goal selection, marked for removal.)

  • Stable identity for generated impls. A generated impl is identified by what it is (its goal trait and self type), not by the order it was created in. The old positional id was reset every lowering pass, so reordering derives renumbered unrelated ids downstream; content-keying removes that hazard and makes "which derive site and provider produced this impl" reconstructable.

  • ABI layout computed in Fe. The hand-written Rust generators that compute ABI sizes (including message and error variants) are replaced by a Fe provider, StableAbiSize; desugar snapshots show byte-equivalent output. The layout rules now live in the language's own library.

  • Compile-time predicates in where clauses. You can write boolean compile-time conditions such as where N > 0 or where T::SIZE >= 50 (plus a brace form where { ... }), discharged by compile-time evaluation against concrete types. The minimal concrete-only form is also carved out as a small standalone PR for independent review. (An earlier draft additionally added trait-application constraints, where Eq<T>; that surface was removed in the final commit, since it was notationally incoherent and unlocked nothing concrete. Only the predicate form above ships.)

  • EVM intrinsics gated by capability. Low-level EVM ops carry a real obligation resolved against the compilation target, keyed by trait identity. The intended rule is that a target which does not grant the capability rejects the op, which is what would make the gate meaningful across backends. Today that root capability is hardcoded to the EVM target and the gate covers sload/sstore (the remaining externs are a mechanical follow-up), so the cross-backend rejection is design intent, not yet a realized mechanism, and is untestable until a second backend exists.

  • Single-implementation traits. Some traits must have exactly one impl program-wide (consensus, ABI, and storage-layout witnesses, where a second impl silently breaks agreement). Marking such a trait #[fixed] enforces at-most-one by a count, checked pairwise per ingot environment and reported as a diagnostic. An accompanying authority token is threaded but currently inert (computed, not yet enforcing); the count is what enforces today.

  • Scoped impl selection (provisional surface). impl T for Y as Name together with with (Name) lets an inner scope choose among several real impls without naming an impl directly. Global coherence is no longer the only law: selection can be scoped, on purpose. This surface is labeled provisional in its own fixtures and will change; it also carries the coexistence soundness work noted below.

The part deliberately left as-is: provider bodies are staged generation, not evaluation

derive provider bodies do not run as ordinary compile-time evaluation. They run through a small dedicated generator that builds a fragment of code, which is then fed back through normal checking. The distinction is the point: ordinary compile-time evaluation produces a value; a provider body produces a piece of the program.

This boundary is deliberate and rests on a real limitation. There is a known result (Pédrot and Tabareau, "The Fire Triangle", POPL 2020) that you cannot freely combine observable effects, substitution, and dependent elimination without breaking the type theory. A provider body is effectful: it reads the target's shape and emits code. We avoid the unsafe combination by staging: the effects run to completion during generation, the result is sealed into a stable, content-identified impl, and only that sealed impl crosses into the solver, never the running computation. The generator is never run inside a solver query (enforced by a compile-time source gate); it runs before, and its output re-enters checking like any hand-written impl.

So what is finished here is everything around the body: selection, authority, goal-checking, identity, and provenance. The body's generation machinery lands its first two principled steps: a provider can emit a whole method from one quote with its structural signature (name, receiver, parameter names) validated against the trait, and a provider's pure numeric decisions reuse the compiler's shared compare_nats primitive rather than a private copy, with a guard blocking a second copy. Deleting the generation backend is not a goal (emitting code is an effect, and that effect is the kept layer). What is deferred is further potentiation of that layer, not its removal. This is post-merge direction, and we would value reviewers' view on where the boundary should ultimately sit.

Known limitations and residuals

Surfaced rather than left in code comments:

  • Coexistence resolution soundness (cascade half). With more than one coexisting impl for a concrete goal, the default-tier / dedup selection path can currently mis-resolve. This branch converts the previously-observed backend abort into a clean named diagnostic (unresolved trait selection) that points the user at a with (...) disambiguation; the underlying fix (deciding the default tier over the solver's confirmed solution set) is deferred to the cascade follow-up. This is the primary reason the split below is recommended.
  • Non-fragment where predicates. A predicate that is not a simple comparison (an if/match/block form) over a still-symbolic parameter is silently treated as satisfied rather than turned into a caller-side proof obligation; the planned undroppability mechanism is not yet built. Concrete-site predicates are checked.
  • Tree-sitter grammar is stale. grammar.js does not model quote/derive/provider syntax or the compile-time where predicates, and still accepts the removed where Eq<T> form. Editor/playground tooling will diverge from the compiler until it is regenerated (the compiler's own hand-written parser is correct).
  • EIP-712 provider is correct only within a documented envelope (no signed ints or dynamic Bytes, single nesting level, field-order type descriptors); outside it the hashes are silently nonconformant rather than a compile error. Tightening to a hard error is a follow-up.
  • with (Name) alias namespace is global across all traits, and single-impl coverage does not yet extend to every consensus-bearing trait (MsgVariant, EventAbiEncode, AbiSpan); until it does, their dispatch metadata is selected first-match by environment/file order rather than pinned to a unique impl.
  • Cosmetic: a redundant dynamic_payload_size note still rides an already-rejected #[error] field; the error is sound, only the extra note is noise.

Tests and CI

The full workspace suite passes locally on the author's machine with the exact CI command (cargo nextest run --release --workspace --all-features --no-fail-fast --locked, language-server and bench excluded): 2451 tests, 0 failures, including the end-to-end suites that compile to EVM bytecode and execute it, so intrinsic and effect-capability codegen is checked at runtime, not only as IR text. Upstream Actions on this PR are pending fork-run approval, so these results are author-side until then. Lint, formatting, and the invariant source-gates are clean.

Recommended split

The pieces share one dependency spine, but acceptance strength and open questions sit on a clean seam, so rather than one 35k-line review we suggest two:

  1. Spine (land first): derive/provider recognition by identity, staged generation, content-keyed generated-impl identity, StableAbiSize and retiring the Rust ABI generators, and the EVM capability gate. Acceptance is strongest here (byte-identical codegen locks, identity tripwires, end-to-end EVM execution), and the one open question that touches it is narrow: Q1 below, where post-lowering generated code should live.
  2. Coexistence/cascade (follow-up): scoped selection (as/with), the default tier, N-way coexistence, and the #[fixed] authority rail. This half is self-labeled provisional, carries the resolution-soundness limitation above, and implicitly answers scoping questions that are still open with the maintainers.

Two questions from the June review remain open and this branch's design leans on their answers, so reviewer input is especially welcome: (Q1) does post-lowering provider expansion match where generated code should live, and (Q3) should traits and effects be one mechanism with the orphan rule retired in favor of scoped provisions.

Graduate the concrete predicate form: a boundless where-predicate
`where Eq<T>` is now collected as the trait obligation `T: Eq` and
enforced at use sites exactly like `where T: Eq`. Built over the
existing `TraitInstId`/`PredicateListId` substrate, with NO new
`TyData` variant and NO `ConstraintTerm`.

Convention: a constraint application uses the proposition shape
`Trait<Self, Args...>`, so the FIRST written arg is the subject.
`where Eq<T>` binds `Self = T` (filling `Eq<T = Self>`), i.e. "T is Eq".

Layers:
- Parser (param.rs): a boundless predicate whose final path segment has
  generic args and is not followed by an expression continuation routes
  to a `WherePredicate` with the colon made optional. The discriminator
  (`is_constraint_application_predicate`) excludes the look-alikes that
  must keep requiring a colon. Pinned by a syntax_node fixture, and the
  tree-sitter grammar's `where_predicate` rule is updated to match.
- Lowering (trait_lower.rs): `lower_hir_constraint_application` resolves
  the head to a trait and builds the `TraitInstId` with `Self = first
  arg`, mirroring `lower_trait_ref_impl_inner`'s minter-based
  arg-completion/kind/const checks. Returns `None` for a non-trait head.
- Collection (constraint.rs): `Deferred` becomes an enum (`Bound` /
  `Application`); a boundless predicate feeds the SAME fixed-point as
  `Type: Trait` bounds (one collection routine, no parallel pipeline).
  Both variants carry the upstream `PredicateSource` provenance so
  use-site diagnostics can point back at the originating bound.
- Diagnostics (diagnosable.rs + semantic accessors): a non-trait head is
  rejected by name in the analysis pass (after collection, no salsa
  cycle), reporting `expected trait here, but found type` instead of
  silently dropping.

A ty_check fixture proves use-site enforcement (`No` doesn't implement
`Equal<No>`).
Proves the concrete constraint-application form works through the full
pipeline (collect obligation, resolve `a.eq(b)`/`a.ne(b)` against it,
codegen, RUN), not just type-check. Generic fns `same`/`differ` bounded
by `where Eq<T>` over a struct with a hand-written `impl Eq` and over
typed primitives; 4 `#[test]`s execute green. Complements the unit-level
ty_check fixture (which pins the unsatisfied-bound diagnostic).
…urn-down

Collapse the solver, the effect env, and the global coherence check into one
first-match ProvisionEnv walk at the establish gate. Derive and providers are
recognized by resolved core::derive identity, not string keys. Capability
authority is effect-carried. Generated impls get content-keyed stable identity.
ABI size is providerized (a Fe StableAbiSize provider replaces the Rust
generators, including the msg/error variant generators). The single-impl money
floor moves to Fe via the #[fixed] trait attribute (count + PermitAuthority).
N-way cascade selection via impl..as Name + with(Name), no impl naming. EVM
intrinsics are gated as per-family capabilities (StorageIntrinsic, ...) with
ambient EVM-root provisioning: the storage ops carry a real obligation resolved
against the target root, so a non-EVM target rejects them while existing EVM
code needs no per-function ceremony.

Reconstructed onto argot/master. Deferred with reason: live abstract-head
(variable-headed) solving, Generic<T>, type-to-type CTFE, per-contract authority
delegation, broad multi-backend.
…ings already pinned

Q2 (assoc-const read bound via FCO obligation path): already covered by
trait_const_requires_impl.fe (concrete no-impl, read-site 6-0003) and
expr_trait_const_missing_bound.fe (generic). PathRes::TraitConst routes
through register_trait_obligation/snapshot_evidence_provisions, not
is_goal_satisfiable.

Q4 (Fe AbiSize provider covers restructured msg / recv-dynamic-Bytes):
already covered. msg variants + #[error] structs schedule StableAbiSize
(expansion.rs schedule_msg_variant_abi_size / schedule_error_abi_size;
Rust generators deleted). Provider produces HEAD_SIZE/IS_DYNAMIC for a
dynamic field in derived_abi_size.fe (WithDynamic); end-to-end recv-
dynamic-Bytes in msg_multiple_dynamic_bytes.fe + abi_dynamic_payload.fe;
missing-field-bound diagnoses (no ICE) in abi_size_concrete_missing_field.fe
and derived_abi_size_missing_field_bound.fe.

Q5 (ordered const-ref fallback): already covered. body.rs lower_const_ref
implements the one ordered policy (concrete -> type_level_fallback symbolic
-> typed placeholder + recorded 6-0003); abi_size_concrete_missing_field.fe
(concrete/placeholder) and derived_abi_size_missing_field_bound.fe
(generic/symbolic) pin both branches, no ICE.

CallConstraintBoundOwner: orphan. Upstream 79ed24d moved bound-origin
recording into trait_resolution/constraint.rs and deleted the enum + its
two consumers; the FCO merge re-added the bare enum with no producer or
consumer. Removed; fe-hir builds with no unused-enum warning.
…-2 + SGK deferral

The concrete-require gate comment said removing it was an open owner decision.
Architect ratified framing-2 (2026-06-24): a fully concrete require must not
become a param-env assumption. That observable contract is already realized
here (concrete requires are not where-predicates, so no phantom method-resolution
candidate / no 8-0026; a missing concrete impl fails 6-0003 at the body use
site, pinned by derived_abi_size_missing_field_bound / abi_size_concrete_missing_field).
Proactively verifying a require the body never uses needs analysis-side access to
the provider Require effects, which crosses the expansion/analysis boundary and is
deferred to the Staged Generation Kernel follow-up. The general assumption-vs-impl
dedup (framing-1) stays deferred, witness-keyed on ImplementorId. Comment-only.
The grammar changed (cascade `as`/`with`, `where Eq<T>`, `Constraint` kind) but the
vendored wasm + input stamp were stale, so build.rs would try to rebuild and panic on a
machine without the pinned tree-sitter CLI. Regenerated from the committed src/parser.c
with tree-sitter 0.24.5 (matches the pinned tree-sitter-cli ^0.24.5) + emscripten; stamp
updated to the current grammar-input hash. CI's --all-features build of fe-web no longer
hits the stale-wasm panic.
A batch of trivial, correct style lints (local clippy is ahead of CI stable, so some
of these CI may not yet enforce, but all are valid):
- needless_borrow: is_constraint_ctor(ty.kind(db)) (provider_goal.rs, diagnosable.rs)
- doc_lazy_continuation: a line-leading `+` read as a markdown list bullet (provider_goal.rs)
  and `>1` read as a blockquote (ty_check/expr.rs); reworded to plain prose / code span
- unneeded_wildcard_pattern: `effect_args: _,` already covered by `..`
  (borrowck/noesc.rs, mir runtime lower/body.rs)
- useless_format / useless_borrows_in_formatting / question_mark in inherited crates
  (diagnostics.rs, borrowck/verify.rs, common/file/workspace.rs, fe-web/model.rs, fe/abi.rs)

`make clippy` (cargo clippy --workspace --all-targets --all-features -- -D warnings) is green.
Lock the invariant the staged-generation kernel relies on: a generated
derive-provider impl's interning identity (its TrackedItemId, rendered via
ImplTrait::interning_identity_repr) is (i) stable across two independent
analyses of the same input and (ii) invariant under reordering sibling
derive targets. Both hold because the keystone keys on content
(GeneratedImplTrait{goal,self_ty}), not a positional ordinal.
…uery

SGK rung 1b. Introduce expand_provider_impl, a LOWERING-phase
#[salsa::tracked] query keyed on an interned ProviderExpansionKey (the
resolved derive inputs: provider, goal, self_ty, reflection, generics,
parent scope, derive-site origin). It wraps the EXISTING generation: it
still runs ProviderExecutor::run and synthesize_provider_impl internally
(no body ported, executor unchanged) and returns the synthesized impl as a
self-contained scope-graph fragment, or the execution failure as data.

expanded_items_impl now collects per-impl fragments in base-DFS/request
order and merges them into the expansion graph, reproducing the previous
single-context build byte-for-byte: per-impl scopes append in order and
sibling shims union their parent-to-child edges (the same union
merged_scope_graph_impl performs). Provider selection and all diagnostics
stay in execute_requests, so diagnostic order/spans are unchanged.

The query is content-keyed, so it is query-order-independent, and runs
strictly upstream of analysis. It is pub(super) and unreachable from the
solver; sgk_solver_guard pins that by source-scan. Generated-impl identity
stays byte-stable because it is content-keyed (GeneratedImplTrait under
Expansion), independent of which query mints it.

Interned-key fields require Hash + Eq + Clone (not Update): add Hash to
ValidatedProvider/Capability and the reflection types, and
Debug/Clone/Eq/Hash to DeriveGenerics.
The earlier clippy edit (dropping effect_args) left the Call pattern in
expanded multi-line form; rustfmt collapses it now that it fits one line.
FCO's trait-solving rework made <Self as ThisTrait> and <T::Name as Bound>
projection bases classify as UnSat when the header assumptions omit the
synthetic Self: Trait self-predicate (per the upstream 'avoid self-assumptions
in trait headers' fix). That broke valid trait headers that project their own
associated types, e.g. trait Direct: A<Self::Item> and
trait RecursiveSuper: A<Self::Item::Assoc> { type Item: RecursiveSuper }.

Recognize two structurally-valid projection bases in check_projected_trait_use_wf:
  1. <Self as ThisTrait> where Self is the trait's own self-parameter (Self
     implements its trait by definition);
  2. <T::Name as Tr> where 'type Name: Tr' is a declared associated-type bound
     (every projection of Name satisfies its declared bounds for any T).

Both are sound invariants, so a header projecting an undeclared bound still
errors. Fixes constraints::trait_header_assoc_projection_recursive_bound; full
fe-hir sweep confirms no regressions.
…const

select_assoc_const_candidate took the matched implementor's trait instance via
skip_binder(), leaving the impl's (const-)generic parameters unsubstituted (e.g.
GenericForward<N> instead of GenericForward<7>). FCO's stricter downstream
impl-satisfiability check then failed to match the const-generic blanket impl,
so accessing an associated const on a concrete const-generic instance
(GenericForwardStruct<7>::B) reported 6-0003 'doesn't implement'.

Recover the concrete arguments by unifying the candidate impl's self type with
the receiver and folding the trait instance through that table (mirroring
assoc_const_body_and_impl_args_for_trait_inst). Fixes
trait_resolution_conformance::trait_impl_assoc_const_selected_instance.
…e mismatch

The const-predicate conformance rejection fires correctly as
ImplDiag::MethodConstPredicateMismatch ('method has different where const
predicates than trait'), but its positional local_code is now 22, not 16:
ImplDiag grew new variants (ConstTyMismatchWithTrait owns 16, etc.) ahead of it.
The m5 _is_rejected tests pinned the stale 6-0016, so they panicked despite the
rejection being emitted. Update the 6 expectations + the doc comment to 6-0022
(16 belongs to ConstTyMismatchWithTrait and is left untouched).
…ecessary)

Extend the keystone tripwire from impl-level to method/body level. A generated
provider method's TrackedItemId is content-rooted at
..::GeneratedImplTrait{goal,self_ty}::Func(name) (its body one constant
::FuncBody join below), with no positional ordinal at any level, because the
synthesis builds them via with_item_scope/joined_id under the content-keyed
GeneratedImplTrait scope. So relocating body construction to a downstream salsa
query (x-3d) cannot change the identity: the current content-keyed
expand_provider_impl query already provides the stable, provenance-rooted body
identity x-3d was meant to add. Two new tests assert generated method/body ids
are content-keyed (GeneratedImplTrait::Func, no ord:) and invariant under
sibling derive reordering.
Quote bodies gain a third structural shape alongside the block (v1) and
match-arm forms: a method definition `quote { fn name(self, ..) -> Ret { .. } }`.
This is the front-end for the deep-quasiquote quoted-method artifact: providers
will emit a whole method through one quote rather than the inferred
`emit_method(name, body)` form.

- New `SyntaxKind::QuoteMethod` and `QuoteExpr::method()` / `QuoteMethod::func()`
  AST accessors returning the real `Func` node, so lowering reads name, params,
  return type and body block off existing machinery.
- `QuoteExprScope::parse` detects a method body by `fn` as the first meaningful
  token at brace depth zero (dry-run lookahead), ordered method -> arms -> block.
  A `fn` nested inside a block/match/if arm sits behind its own brace and so is
  never the first token, so it does not misfire.
- `QuoteMethodScope` delegates the header and body to the real fn-item parser
  (`FuncScope`, `Impl` mode permits the `self` receiver and requires a body),
  owning only the outer quote-body braces; params, return type and the `${..}`
  splice holes all go through existing paths.
- Fixture `exprs/quote.fe` proves all five cases: method, arms, block, nested
  match (stays block), and a non-first nested `fn` (stays block, no misfire).
- `quote { .. }` is absent from the editor tree-sitter grammar in every form, so
  the new fixture is excluded from `tree_sitter_parse_strict` (sanctioned list).
Two pre-existing CI-lint deviations carried in by earlier commits on this
branch, fixed with no behavior change:

- `trait_resolution/mod.rs`: collapse the nested `if let`/`if let`/`if` in the
  `<T::Name as Tr>` projection-base guard into one let-chain (clippy
  `collapsible_if`, denied under CI's `-D clippy::all`).
- `item.rs`: reflow the keystone-tripwire assert's `.iter().all(..)` chain to
  rustfmt's canonical form (CI runs `cargo fmt --check`).
Add the HIR representation for a `quote { fn name(self, ..) -> Ret { body } }`
method template, the front-half of the deep-quasiquote quoted-method artifact.
Additive: no provider emits a method quote yet (the StableEq port and the
`emit_method` 1-arg dispatch land in S3/S4), so behavior is unchanged.

- `QuoteBody` becomes generic over `'db` and gains a `Method(QuoteMethodSig, ExprId)`
  variant. `QuoteMethodSig` captures the spelled signature STRUCTURALLY (name +
  the interned `FuncParamListId` an ordinary `fn` uses + return `TypeId`), not as
  stringy builder state; the `ExprId` is the method's body block, lowered into the
  provider body's arena so its `${...}` holes are captured like any expression
  template. The signature is inert (never type-checked as written); it is
  validated against the goal trait's declaration at emit time (S3).
- Lowering recognizes the method body first (matching the parser's
  method -> arms -> block order), reusing the real `fn`-item param/type lowering.
- The provider executor captures the method body's holes (same path as
  `QuoteBody::Expr`); a method quote reaching expression or arm position is a
  named error (method quotes are emitted only through `emit_method`).
- Visitor walks the method body; the pretty-printer renders the method form.

Verified: cargo check + clippy clean; full fe-hir suite green except the 4
pre-existing WIP #[error]-providerization desugar snapshots, which fail
identically with this change stashed (zero regressions).
…B S3)

Wire the deep-quasiquote quoted-method artifact end to end: a provider can now
emit a whole method from one quote,
`builder.emit_method(quote { fn name(self, ..) -> Ret { body } })`, alongside
the existing `emit_method(name, body)` form (kept as a compat path).

- Extract `elab_block_template`, the single-expression-block elaborator shared
  by `elaborate_quote` (expression form) and the new `elaborate_quote_method`,
  so the method form produces byte-identical output to the expression form.
- `elaborate_quote_method`: the spelled `fn` header is the structural signature
  SOURCE, but codegen uses the canonical signature inferred from the goal
  trait's declaration (`infer_method_sig`) so a conforming provider is
  byte-identical to `emit_method(name, body)`. The method's parameters are its
  body's binding scope (effective open names = canonical parameter names), so
  `self`/parameter references resolve with no open-name dance.
- `validate_spelled_method_sig`: the spelled header must conform to the
  declaration (same `self` receiver, parameter count, parameter names in order);
  a mismatch is a named provider diagnostic instead of the silent inference the
  `emit_method(name, body)` form did. Parameter/return TYPE conformance is left
  to ordinary checking when the sealed impl re-enters (the spelled types are
  inert here).
- 1-arg `emit_method` dispatch: evaluate the argument to a method quote, reject
  non-method quotes with a named error, then elaborate and push the same
  `ProviderEffect::EmitMethod { sig, body }` the 2-arg form produces. The freeze
  guard (TD5.0) dedups arm keys, so the second arity adds no op to the surface
  and the count stays 39 (no caller in the ingots yet; StableEq ports in S4).

Verified: cargo check + clippy clean; corelib derive snapshots byte-identical
(19/0), confirming the shared-elaborator refactor preserves all existing output.
…SGK-B S4)

Port the canonical `StableEq` provider's `eq` from `emit_method("eq", body)` to
the deep-quasiquote quoted-method form,
`emit_method(quote { fn eq(self, other: Self) -> bool { ${body} } })`, for both
the struct and enum branches. The accumulated comparison body splices in as
`${body}`; the spelled `fn` header is validated against `Eq`'s declaration and
the canonical (declaration-derived) signature drives codegen.

Proven byte-identical by a stash differential: the generated `eq` body for a
struct and an enum is character-for-character what the `emit_method("eq", body)`
form produced (the parameter even renders as the canonical `_ other: <Target>`,
not the spelled `other: Self`, because codegen uses the declared signature). The
new `derived_eq_method_body_is_byte_identical` keystone test locks both bodies
as a permanent regression tripwire.

Verified: corelib 19/0, keystone tripwire 4/0, the new body-lock test green;
clippy + rustfmt clean.
…B island 1/3)

Lift the inline natural-number comparison from `normalize_cmp` (const-predicate
folding) into a `pub(crate) compare_nats(op, a, b)` function: the single place
that decides a `TermCmpOp` over two concrete naturals. `normalize_cmp` now calls
it; behavior is unchanged (term-folding tests 49/0).

This is the shared value-CTFE primitive the next steps route provider steering's
integer comparison through, so steering grows no second integer comparator: a
pure numeric decision in a provider body will be decided by the same code that
decides a `where N > 0` const predicate.
…GK-B island 2/3 + 3/3)

Grow ONE real pure-steering decision and route it through the shared value-CTFE
comparison primitive, proving steering can grow a numeric decision only via the
shared layer (deep-B deliverables 4 + 6).

Steering integer support (provider executor):
- `Value::Int(IntegerId)` — a compile-time natural (integer literal or reflected
  count); `eval_expr` evaluates integer literals and integer comparisons.
- An integer comparison `a <op> b` is decided by the SHARED `compare_nats`
  primitive (term.rs) — the same code that discharges a `where N > 0` const
  predicate — so steering grows no local comparator. Pure decision, no emission.
- `reflect.variants().len()` answers a sequence's element count as a `Value::Int`
  (the integer source). Not a `builder.*`/reflection-read op, so the frozen op
  surface stays 39/0.

Real use: StableEq's enum branch now computes `multi = reflect.variants().len() > 1`
(the architect's variant-count-comparison example) instead of bool flags. Proven
byte-identical by the existing generated-eq body-lock test (the `count > 1`
decision emits the same match as the bool-flag version).

Proof:
- `term::steering_int_comparison_agrees_with_ctfe_folding` — the differential
  test: const-predicate folding (`normalize_cmp`) and steering's `compare_nats`
  agree for every relation over every sample (both route through `compare_nats`).
- `freeze_guard::steering_numeric_decisions_use_the_shared_primitive_only` — the
  no-second-evaluator structural guard: `eval_expr` routes comparison through
  `compare_nats` and evaluates no integer arithmetic locally (an `ArithBinOp`
  falls through to unsupported).

Verified: corelib 19/0, body-lock byte-identical, freeze_guard 6/0 (surface
still 39/0), clippy + rustfmt clean.
…rization

The `#[error]`/`#[msg]` desugar snapshots were stale relative to the committed
ABI-size providerization: error/msg `AbiSize` impls are now generated by the Fe
`StableAbiSize` provider (`HEAD_SIZE = <() as AbiSize>::HEAD_SIZE` instead of a
literal, `payload_size` returning `Self::HEAD_SIZE`, impl ordering shifted). The
generated output is well-formed and validated by the otherwise-green suite;
regenerate the four snapshots to match so the desugar suite is green.
…e providerization

The six contract/recv ty_check snapshots were stale relative to the committed
msg/ABI-size providerization: the inferred-type dumps no longer list the deleted
Rust-injected `dynamic_payload_size` and now annotate the msg variant
declarations. The output is error-free (`assert_no_diags` passes; the diffs are
inferred-type notes only, no new diagnostics), so regenerate the snapshots to
match. Completes the regeneration of the known WIP error/msg-providerization
snapshot set (4 desugar + these 6 ty_check).
…n threading

The effect-pointer/capability work threads an ambient, zero-sized effect-domain
token through intrinsic calls: each effect call now takes a leading phantom
`undef.objref<@layout_N>` operand (the EVM root / RawMem / RawStorage capability),
where `@layout_N` is a zero-sized aggregate. This shifted 34 sonatina_ir golden
files and 1 name_resolution fixture (a pure line-number move, the cited source and
message are byte-identical).

These snapshots were left stale because the prior reconstruction pass did not run
the fe-codegen suite. Regenerating, not changing behavior.

Verified semantically, not by eyeballing text:
- The domain operand is never dereferenced. Across all 34 fixtures no native EVM
  instruction (mstore/mload/sstore/sload/evm_return/evm_calldata_load/
  evm_code_copy/evm_log*/evm_caller) takes the phantom as an operand; it appears
  only as a call argument that is ignored or forwarded, and real addresses,
  values, slots, and log topics are unchanged (operands just shift one register).
- Execution tests pass on real bytecode: differential_storage_packed_array and
  differential_deposit deploy the compiled contracts to revm and assert
  byte-for-byte return parity against a Solidity reference; storage_packed_array
  is one of the regenerated fixtures. runtime_handle_preservation (35 cases) is
  green. 37/37 execution tests pass.
- Re-running the regenerated suite is green: 129/129 (sonatina_ir + the
  name_resolution fixture).
…tion surface

`where Eq<T>` was a strict alias for `where T: Eq` with incoherent argument
slots (arg-0 is the Rhs in `impl Eq<Foo> for Bar` but the subject in
`where Eq<T>`) and unlocked nothing over an ordinary trait bound; its only
motivation, the abstract head `where P<T>`, is shelved. Remove the parser
recognition (`is_constraint_application_predicate` plus the now-unused
`continues_const_expr`) and the three fixtures that exercised it.

`where P<T>` now falls through to the const-expression path and is rejected with
a named `8-0030 value is expected` diagnostic (no silent accept, no ICE); the
abstract-head guard fixture is updated to pin that. The internal reifier
`lower_hir_constraint_application` stays: it is still used by provider synthesis
and capability-goal lowering.
…g (B-1)

An unscoped trait-method call over coexisting impls (two `as Name`
aliases, or a constraint-violating derived default alongside an
override) type-checks clean but has no unique impl selection. MIR
runtime resolution (`resolve_runtime_call_key`) already returned a
`LowerError` for it, but both call sites (`classify.rs` return-class
inference and `lower_call`) `unwrap_or_else` that into a `panic!`,
turning a legal program into a Sonatina backend ICE at
`classify.rs:1177`.

Add a pre-flight `check_runtime_trait_calls_resolvable` in
`lower_to_rmir`, mirroring the existing `check_runtime_body_supported`
guard: it resolves every trait-method call before the panicking
static-facts pre-pass and body lowerer run, and returns the first
resolution failure through the ordinary `Result<_, LowerError>` path so
it surfaces as a clean compile error with a nonzero exit instead.

Give the resolver's two "no unique concrete impl" arms a named
`LowerError::UnresolvedTraitSelection` variant with a deterministic,
user-facing message (trait method plus goal only, no internal keys, so
snapshots are stable) that points at `with (...)` disambiguation.

This is a de-panic guard only. It does NOT change how selection is
decided; the deeper dedup-collapse / default-tier fix is a separate
cascade follow-up.

Pinned by new fixtures under
crates/fe/tests/fixtures/fe_test_unresolved_selection/ plus a
test_fe_test_unresolved_selection harness that asserts a nonzero exit,
the absence of any panic, and snapshots the exact diagnostic.
…g (B-2)

`check_with` runs `provisional_scoped_selection` before the ordinary
value path, so a keyless `with (Name)` head is first interpreted as an
impl-alias selection. On the ALIAS arm the success path called
`alias_scoped_selection` unconditionally: if `Name` matched an
`as Name` impl it was selected even when `Name` also resolves to a real
value binding. A `const Casual` value provider then lost to an
`as Casual` impl alias of the same spelling: `with (Casual) { u.greet() }`
selected the aliased impl and silently swallowed the const value.

The failure path (`classify_with_alias_failure`) already carries a
"do NOT hijack a real value binding" rule; mirror it on the success
path. Consult the impl-alias namespace only when the bare single-segment
ident does NOT resolve as an ordinary value/type binding
(`resolve_ident_expr` yields `NewBinding`). A resolvable `with (value)`
now falls through to the ordinary value path, so the value provision
wins and the unscoped call selects the sole real impl.

Pinned by crates/fe/tests/fixtures/fe_test/cascade_with_alias_value_precision.fe
(a `const` whose name collides with an `as` alias): the const value is
used and the sole anonymous impl is selected (returns 1, not the
alias's 2). Genuine alias/scoped selection with no colliding value is
unchanged (existing cascade fixtures stay green).
Several doc comments in the cascade machinery describe behavior that no
longer matches the code:

- trait_resolution/mod.rs: the default-tier doc claimed its own Ambiguous
  return is a guaranteed clean diagnostic and never a panic. In fact this
  function never diagnoses or panics itself; some callers (e.g.
  resolve_trait_method_instance) just return None on Ambiguous. What
  actually prevents a backend panic today is the separate B-1 runtime
  pre-flight guard in crates/mir, not this mechanism, and the ty_check-level
  AmbiguousTraitInst leg can go unreached for concrete-goal coexistence
  (dedup limitation, deferred cascade follow-up).
- trait_def.rs: is_single_impl's doc cited a test function name that does
  not exist in the file; fixed to the real test name
  (is_single_impl_reads_fixed_attribute). The rest of the doc (attribute-
  driven, not a hardcoded allowlist) was already accurate.
- term.rs: the module doc claimed nothing depends on it yet; it now has
  five live consumers (ty_check, method_cmp, diagnostics, provider_executor,
  test_db).
- ty_check/env.rs: snapshot_evidence_provisions's doc claimed it always
  returns empty; with (<T as Trait>) scoped selection now stamps a real
  Evidence-typed provision, so it can be non-empty.
- core/hir_def/item.rs: ImplTrait's alias field/accessor docs claimed the
  alias is stored but not used for selection; it is now read live by
  selection_discriminator and alias_scoped_selection for N-way coexistence
  and with (Name) selection. (The neighboring impl_permit docs were checked
  and are still accurate: that half really is recognition-only today.)
- core/lower/provider_executor.rs: the frozen-command-surface comment
  claimed the catch-all dispatch arms debug_assert against the canonical
  list; no debug_assert exists anywhere in the file. The real guard is the
  compile-time source-scan freeze tests, corrected the comment to say so.
recognized_reflect_ops_match_dispatch's start marker only matched inside
the test module itself (as the string literal argument to its own
slice_between call), so it scanned the empty gap between that call's two
string-literal arguments and always asserted [] == []. A new bespoke
reflect-op arm added to eval_method_call would land completely unpinned.

Point the marker at the real freeze comment in eval_method_call's body
instead, so the scan covers the actual dispatch region it is meant to
freeze. Verified non-vacuous by temporarily injecting a fake
("name", ..)-shaped line into the scanned region: the test now fails
loudly on drift instead of passing regardless (reverted before this
commit). Still green today because the reflection-read surface is
genuinely empty post-TD5c.
DeriveErrorKind::ProviderAmbiguous and ::CanonicalProviderAmbiguous both
rendered as derive-lower code 9. Renumber CanonicalProviderAmbiguous to 15
(the next free code in that enum's numbering; 1-14 were all already
claimed).

TraitConstraintDiag::ConstPredicateNotSat built its CompleteDiagnostic with
a hardcoded GlobalErrorCode::new(DiagnosticPass::TyCheck, 85) instead of
the error_code binding every other arm in the same to_complete uses, so its
rendered code silently squatted on BodyDiag's TyCheck code space (shared
with WhereConstPredicateFailed) rather than being tracked by this enum's
own local_code() table. Use the shared error_code binding instead; it now
renders as 6-0007 (TraitSatisfaction, this enum's own local_code) rather
than 8-0085.

Regenerated the one affected snapshot (where_const_predicate_adt_wf.snap,
the concrete-ADT-application trigger for ConstPredicateNotSat); confirmed
the diff is exactly the code renumbering (8-0085 -> 6-0007) with identical
messages, spans, and notes. The six sibling where_const_predicate_*.snap
fixtures exercise the call-site WhereConstPredicateFailed check instead
(same message text by design) and are unaffected, as is
contract_field_mut_binding.snap (a different BodyDiag variant that
happens to share local_code 85).
git diff 4f38ba2..25dc157 -- newsfragments was empty: this headline
feature set shipped with zero changelog entries. Add fragments for the
shipped headline pieces: derive providers, content-keyed generated-impl
identity, StableAbiSize/ABI-in-Fe, EVM capability gating for storage
intrinsics, compile-time where const-predicates, #[fixed] single-impl
traits, and scoped impl selection (noted as provisional).
…ing (S7)

derived_pairs keyed conflict detection on (target item, last-segment ident),
so two derive requests for the same trait under different spellings (a bare
name and a `use .. as Alias` import of the same trait) were treated as
unrelated and both scheduled. Both then expanded into real impls, with the
conflict only caught downstream (if at all) by the ordinary ImplTrait
coherence gate once both landed in the trait env.

Key derived_pairs on the trait's resolved identity instead, via the same
resolve_trait_def/canonical_trait_path machinery goal_matches_provider
already uses for provider selection, falling back to the raw ident when
the path does not resolve to a trait def here (an unrelated diagnostic
already covers that case). This is keystone-protective: GeneratedImplTrait's
content-keyed identity assumes at most one impl per (goal, self_ty), and an
undetected aliased-derive conflict is the remaining way to violate that
premise.

Added a regression fixture (derive_aliased_conflict.fe/.snap) exercising
`derive Eq for Point` + `derive EqAlias for Point` where EqAlias aliases the
same core::ops::Eq trait. Verified against the pre-fix code (temporarily
stashed) that this shape previously fell through to a generic
5-0001 conflicting-trait-implementations diagnostic from the coherence gate
(incidentally caught only because both expansions happened to share the
same Default selection discriminator); with the fix it is now caught
precisely and early, at expansion time, as ConflictingDerive.
Reconcile item S10 with the maintainer's decision: the earlier tightening
switched ConstPredicateNotSat to this enum's own shared error_code binding
(rendering 6-0007), but the deliberate design is for a const-predicate
violation to report the SAME code (8-0085) at every position, whether it is
the call-site BodyDiag::WhereConstPredicateFailed or this WF-level check on
a concrete ADT application. Restore the intentional cross-pass code override
to 8-0085, with comments marking it as deliberate so it is not "fixed"
again, and flip the where_const_predicate_adt_wf snapshot back to 8-0085 to
match (identical message, spans, and notes; only the code reverts).

The other, uncontroversial half of S10 stays: the duplicate diagnostic
code 9 is still resolved (CanonicalProviderAmbiguous renumbered to 15).
…rait selection

The de-panic guard ran per-instance in lower_to_rmir, but a callee body's static facts were walked earlier during return-class inference (runtime_return_class -> runtime_return_summary -> BodyStaticFacts::new), so an ambiguous call inside a helper with an inferred (dynamic/erased) return still hit the classify.rs panic. RuntimeGraphBuilder::build now pre-checks each instance's reachable trait calls before lowering, raising the clean UnresolvedTraitSelection diagnostic on that route too. Verified green: nextest 2451 passed / 0 failed. Pinned by fe_test_unresolved_selection/helper_zst_ret.
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