Skip to content

Resolve requirements through lookup-backed witnesses - #12752

Open
kaizhangNV wants to merge 1 commit into
shader-slang:masterfrom
kaizhangNV:fix/12751-lookup-witness
Open

Resolve requirements through lookup-backed witnesses#12752
kaizhangNV wants to merge 1 commit into
shader-slang:masterfrom
kaizhangNV:fix/12751-lookup-witness

Conversation

@kaizhangNV

Copy link
Copy Markdown
Contributor

Motivation

Consider this checked source shape:

interface IPrimitive { associatedtype Attributes; }
interface IContext { associatedtype Primitive : IPrimitive; }

After a concrete context selects its primitive, the compiler can obtain the witness for Primitive : IPrimitive. A later call to tryLookUpRequirementWitness should then obtain Attributes, but it returns no witness when that conformance is represented by a LookupDeclRef. This blocks consumers that query an already-checked nested associated-type conformance. Fixes #12751.

Proposed solution

When a declared subtype witness is lookup-backed, resolve the requirement with the same getUnspecializedLookupRec and specializeLookedUpRec path already used by LookupDeclRef::tryResolve. This keeps the witness table as the semantic source of truth and applies the substitutions recorded along the lookup path.

Change summary

  • Teach tryLookUpRequirementWitness to traverse lookup-backed declared subtype witnesses.
  • Reuse the existing canonical lookup and specialization helpers.

Concepts and vocabulary

  • Lookup-backed witness: a conformance represented by a DeclaredSubtypeWitness whose declaration reference reaches an inheritance declaration through a witness-table lookup.
  • Requirement key: the interface declaration, such as IPrimitive.Attributes, used to select a witness-table entry.

Process report

The front end already produces a valid conformance. For a concrete IContext, its witness table stores the selected Primitive type and the constraint witness for Primitive : IPrimitive. That constraint witness is intentionally represented by a LookupDeclRef; ordinary declaration lookup already handles this shape in LookupDeclRef::tryResolve.

The failure was in the consumer. tryLookUpRequirementWitness only handled a declared witness when its declaration reference directly cast to InheritanceDecl, so the second lookup stopped before reading the primitive witness table. The fix detects the valid lookup-backed form and sends it through the existing recursive lookup helpers. It does not scan concrete declarations, rebuild checked syntax, or add a second equivalence rule.

The downstream structural ray-tracing tests provide the observable regression: removing this branch makes 25 procedural-layout directives fail while resolving the primitive attribute type; retaining it restores all 216 directives. On this isolated branch, a clean Debug slangc and slang-test build passed. A focused associated-type/interface run passed all compile, Vulkan, and CUDA cases; 17 LLVM execution cases could not launch the local LLVM runtime and produced no shader output.

@kaizhangNV
kaizhangNV requested a review from a team as a code owner August 25, 2026 16:07
@kaizhangNV
kaizhangNV requested review from bmillsNV and removed request for a team August 25, 2026 16:07
@jhelferty-nv
jhelferty-nv removed the request for review from bmillsNV August 25, 2026 16:07
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f672d140-a863-4e77-b48f-12193c70f4cf

📥 Commits

Reviewing files that changed from the base of the PR and between 5faf399 and e1374a8.

📒 Files selected for processing (1)
  • source/slang/slang-syntax.cpp

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Witness lookup resolution

Layer / File(s) Summary
Resolve and specialize lookup-backed witnesses
source/slang/slang-syntax.cpp
tryLookUpRequirementWitness resolves LookupDeclRef-backed declared subtype requirements through getUnspecializedLookupRec. It returns an empty witness when lookup fails and specializes successful results with specializeLookedUpRec.

Suggested reviewers: bmillsnv, csyonghe, saipraveenb25

Merge Risk: ⚪ Minimal · up to e1374

This localized compiler change resolves lookup-backed associated-type witnesses without any identified merge-blocking risk; it is merge-ready after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: resolving requirements through lookup-backed witnesses.
Description check ✅ Passed The description directly explains the motivation, implementation, scope, and validation for the lookup-backed witness fix.
Linked Issues check ✅ Passed The change satisfies issue #12751 by traversing LookupDeclRef-backed DeclaredSubtypeWitness instances and reusing getUnspecializedLookupRec and specializeLookedUpRec without rediscovering declarations…
Out of Scope Changes check ✅ Passed The reported change is limited to the requested witness-resolution behavior in source/slang/slang-syntax.cpp. No unrelated changes are identified.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Full details: Linked Issues check

Explanation

The change satisfies issue #12751 by traversing LookupDeclRef-backed DeclaredSubtypeWitness instances and reusing getUnspecializedLookupRec and specializeLookedUpRec without rediscovering declarations.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot 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.

Verdict: 🟡 Has issues — 2 gap(s) + 1 clarity note

This PR teaches tryLookUpRequirementWitness to resolve requirements through LookupDeclRef-backed declared subtype witnesses by delegating to the canonical getUnspecializedLookupRec / specializeLookedUpRec helpers (the path already used by LookupDeclRef::tryResolve). The delegation and branch ordering are sound and security/recursion concerns were checked and cleared, but the new early-return on a none result changes control flow for every lookup-backed witness, and the fix ships without a non-GPU regression test.

Changes Overview

Lookup-backed witness resolution (source/slang/slang-syntax.cpp)

  • What changed: adds a branch to tryLookUpRequirementWitness that detects a DeclaredSubtypeWitness whose decl-ref base is a LookupDeclRef and routes it through the shared recursive lookup/specialization helpers, so a nested associated-type conformance selected through a witness-table lookup (IContext.Primitive : IPrimitiveAttributes) resolves instead of returning an empty witness. Fixes #12751.
Findings (3 total)
Severity Location Finding
🟡 Gap source/slang/slang-syntax.cpp:741 No non-GPU regression test for the changed path; existing look-alike CPU/INTERPRET tests apparently don't trigger the branch — verify via revert drill
🟡 Gap source/slang/slang-syntax.cpp:757 Early return RequirementWitness() on a none lookup exits the function, skipping the ThisType/ThisTypeConstraint fallbacks at lines 828/836 for lookup-backed witnesses
🔵 Clarity source/slang/slang-syntax.cpp:754-758 Undocumented flavor-forwarding divergence from the two other callers; local requirementWitness names an unspecialized entry

reviewed: e1374a8 · diff sha256 764b70b0c639


if (auto declaredSubtypeWitness = as<DeclaredSubtypeWitness>(subtypeWitness))
{
if (as<LookupDeclRef>(declaredSubtypeWitness->getDeclRef().declRefBase))

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.

🟡 Gap: No regression test for the changed resolution path

This PR changes how tryLookUpRequirementWitness resolves a requirement through a LookupDeclRef-backed declared witness (the IContext.Primitive : IPrimitiveAttributes chain in the comment), but adds no test under tests/. tmp/pr-files.txt lists only source/slang/slang-syntax.cpp. Per CLAUDE.md ("Include tests: Add regression tests as .slang files under tests/"), and because the PR body states the observable coverage is downstream ray-tracing tests (Vulkan/D3D12/OptiX), the changed path is not exercised by the default non-GPU CI. A future regression in this branch would pass all non-GPU CI and only surface as a broken shader.

This scenario is expressible without a GPU (associated-type resolution happens at semantic-check/lowering time), so a -cpu or INTERPRET regression test is feasible.

Important caveat: the repo already has near-identical non-GPU tests — tests/compute/assoctype-complex.slang (-cpu, nested U : IBase / U.V) and tests/language-feature/interfaces/derived-interface-constraint-multilevel.slang (INTERPRET, multi-level A.B.C) — that apparently do not trigger this branch (they pass without this fix). Please do the revert drill: confirm the new test actually fails without this branch, and identify what distinguishes the failing shape (likely a concrete, non-generic context whose Primitive : IPrimitive witness resolves via a witness-table lookup) from those existing look-alikes, so the test provides real coverage rather than a false sense of it.

Suggestion: add a test modeled on the comment's IPrimitive/IContext example that resolves the nested associated type and verify (revert drill) it regresses without this branch.

auto requirementWitness =
getUnspecializedLookupRec(astBuilder, requirementKey, declaredSubtypeWitness);
if (requirementWitness.getFlavor() == RequirementWitness::Flavor::none)
return RequirementWitness();

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.

🟡 Gap: Early return bypasses the ThisType/ThisTypeConstraint fallbacks for every lookup-backed witness

The new branch's guard (if (as<LookupDeclRef>(declaredSubtypeWitness->getDeclRef().declRefBase))) depends only on the witness's decl-ref shape, not on requirementKey, so it is entered for any requirement key on a lookup-backed witness — including ThisTypeDecl/ThisTypeConstraintDecl. getUnspecializedLookupRec only resolves keys stored in a witness-table requirement dictionary, and the ThisType requirement is deliberately not stored there. For those keys it returns none, and this branch then return RequirementWitness()s — exiting the whole function and skipping the shared tail fallbacks at lines 828 (if (as<ThisTypeDecl>(requirementKey)) → returns subtypeWitness->getSub()) and 836 (ThisTypeConstraintDecl → returns the witness itself).

Before this PR these keys reached the tail: a lookup-backed witness either skipped the InheritanceDecl branch entirely, or entered it, missed the dictionary lookup, and fell through — either way landing on line 828/836. After this PR the same query returns an empty RequirementWitness.

Example: tryLookUpRequirementWitness(lookupBackedWitness, IPrimitive.ThisTypeDecl) for the PR's own Primitive : IPrimitive conformance now returns none instead of getSub(), which can surface downstream as an unresolved this-type. (I have not proven this exact query is issued in practice on a lookup-backed witness, so this is a definite control-flow divergence whose reachability I could not confirm — a This-type resolution test on the lookup-backed shape would settle it.)

Suggested fix: don't consume control flow when the lookup yields nothing — let none fall through to the shared tail:

auto requirementWitness =
    getUnspecializedLookupRec(astBuilder, requirementKey, declaredSubtypeWitness);
if (requirementWitness.getFlavor() != RequirementWitness::Flavor::none)
    return specializeLookedUpRec(astBuilder, declaredSubtypeWitness, requirementWitness);
// else fall through to the ThisType / ThisTypeConstraint special cases below

Note this requires restructuring so the none case continues to lines 827-842 rather than returning inside the declaredSubtypeWitness block.

Comment on lines +754 to +758
auto requirementWitness =
getUnspecializedLookupRec(astBuilder, requirementKey, declaredSubtypeWitness);
if (requirementWitness.getFlavor() == RequirementWitness::Flavor::none)
return RequirementWitness();
return specializeLookedUpRec(astBuilder, declaredSubtypeWitness, requirementWitness);

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.

🔵 Clarity: document the flavor-forwarding divergence and rename the unspecialized local

Two small readability points on this block:

  1. Flavor divergence from the other callers. The two existing callers of this helper pair guard the specializeLookedUpRec call by flavor: LookupDeclRef::tryResolve calls it only for val/declRef, and WitnessLookupIntVal::tryFoldOrNull only for val. This branch forwards any non-none flavor, including witnessTable. That is in fact correct here — tryResolve returns Val* (which a witnessTable cannot be, so it declines that flavor), whereas tryLookUpRequirementWitness returns a RequirementWitness that legitimately can be a witness table, and specializeLookedUpRecRequirementWitness::specialize handles Flavor::witnessTable (slang-syntax.cpp:693). But a reader comparing the three call sites cannot infer that without the trace. A one-line comment stating that a witnessTable result is expected and specialized via WitnessTable::specialize would prevent the divergence from reading as an oversight.

  2. Local name. requirementWitness holds the unspecialized lookup result that is then specialized. The other two callers name the equivalent variable unspecializedEntry / lookedUpVal, reserving "requirement witness" for the fully-resolved result. Consider unspecializedEntry here for consistency with the unspecialized-vs-specialized distinction the surrounding code relies on.

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.

tryLookUpRequirementWitness cannot traverse a LookupDeclRef-backed conformance witness

2 participants