Skip to content

Commit 6bc22ae

Browse files
Fix #6703: fold associated constant in signature-type positions (#11706)
## Motivation An associated `static const` interface requirement (an *associated constant*) is folded to a different compile-time value depending on which path reaches it, so otherwise-valid code that uses the constant in an array size is rejected. Concretely, given: ```slang interface IAssocConst { static const uint COUNT; } struct Data<let N : uint> : IAssocConst { static const uint COUNT = N; float values[N]; } struct Wrapper<T : IAssocConst> { static const uint COUNT = T::COUNT; uint sum(float[T::COUNT] input) { /* ... */ } // signature-type position } static const let VALUE = Wrapper<Data<3>>(); float[VALUE::COUNT] makeInput() { /* ... */ } // signature-type position ``` When `T::COUNT` / `VALUE::COUNT` is reached from a **signature-type** position (an array size in a parameter or return type), the constant is left as a *symbolic* `WitnessLookupIntVal` (`int(Data<3>.COUNT)`). When the same constant is reached from the **generic-substitution** path it folds to the literal `3`. The two `IntVal`s intern to different syntax classes (`WitnessLookupIntVal` vs the literal), so they never compare equal, and the two array sizes are reported as mismatched — `E30019` (type mismatch, ×2) plus a cascading `E30523` ("too many initializers"). This is issue #6703. ## Proposed solution Fold the associated constant to the same concrete value on both paths by ensuring the conforming type's conformances exist *before* the early signature-type fold, then re-folding. The producer is the interface-requirement branch of `SemanticsVisitor::tryConstantFoldDeclRef` (`source/slang/slang-check-expr.cpp`). The first `WitnessLookupIntVal::tryFold` can run before the conforming type's conformance witness tables have been built; in that window the requirement lookup reaches an `InheritanceDecl::witnessTable` that is still null, so `tryFold` leaves the symbolic `WitnessLookupIntVal`. The fix: when the first fold yields a symbolic result and the witness sub-type is a `DeclRefType`, call `ensureDecl(sub, DeclCheckState::ReadyForConformances)` and re-fold. After conformance state exists the requirement lookup resolves to the concrete `RequirementWitness::Flavor::val`, so the first value built for the requirement is the literal constant — identical to the substitution path. This is the principled layer: the bug is a *phase-ordering* defect at the producer, not a mismatch to paper over at the consumer. It does **not** introduce a new witness/`Val` representation or alter lowering / witness-table construction; it makes this producer use the already-existing concrete constant `Val` that the substitution path already produces, instead of leaving the symbolic lookup (so it respects the one-canonical-representation rule and stays lowering-neutral). It only forces the conformance state the fold depends on to be present, then re-folds. Patching `IntVal`/`ArrayExpressionType` equality was rejected: `getOrCreate` interns on syntax class, so a symbolic `WitnessLookupIntVal` can never be made equal to a literal — the fold must be fixed, not the comparison. ## Change summary | File | Change | | --- | --- | | `source/slang/slang-check-expr.cpp` | In `tryConstantFoldDeclRef`'s interface-requirement branch: if `tryFold` yields a symbolic `WitnessLookupIntVal` and the witness sub is a `DeclRefType`, `ensureDecl(sub, ReadyForConformances)` and re-fold. | | `tests/language-feature/constants/associated-const-in-signature-type.slang` | New CPU `COMPARE_COMPUTE` regression test exercising the associated constant from signature-type, value-access, and generic-argument positions. | ## Concepts and vocabulary - **Associated constant** — a `static const` requirement declared in an `interface`, satisfied by each conforming type (here `Data<N>.COUNT`). - **`WitnessLookupIntVal`** — the symbolic `IntVal` for "the value of requirement `R` via witness `W`". `tryFold`/`tryFoldOrNull` collapse it to the concrete value **only** when the requirement lookup yields `RequirementWitness::Flavor::val`; otherwise the symbolic form survives. - **`InheritanceDecl::witnessTable`** — the per-conformance requirement→witness dictionary, populated during conformance checking. It can still be null before the conforming type reaches `DeclCheckState::ReadyForConformances`; the requirement lookup the fold relies on reads it. - **Signature-type position** — an array size appearing in a function parameter/return type or a global declaration type, which the checker folds early (before the conforming type's conformance tables are built) — the window in which the bug occurs. ## Process report **The one change — re-fold after ensuring conformance state.** Local instrumentation at the producer (since removed) established that the witness reaching the failing fold is already a `DeclaredSubtypeWitness` (the gate in `getUnspecializedLookupRec` only handles that class), that `witness->resolve()` is a no-op on it, and that re-folding the resolved witness still returns null. So canonicalizing/resolving the witness does **not** address the defect — ruling out the "normalize the witness" direction. The true discriminator is downstream: on the failing (signature-type) folds the requirement lookup reaches a null `InheritanceDecl::witnessTable`, while on the succeeding generic-substitution folds the table is populated. The producer simply runs too early for signature-type positions. Forcing `DeclCheckState::ReadyForConformances` on the conforming type's decl and re-folding makes the returned value the literal constant. *Input-shape check (per methodology):* the input here — a symbolic `WitnessLookupIntVal` emerging from a fold that ran before conformance checking — is **not** a correct/principled shape to consume; it is an accident of checking order. The fix corrects the producer (folds to the concrete constant once state exists) rather than teaching a downstream consumer to tolerate the symbolic form. The retry path is limited to cases where the initial fold remains symbolic (`as<WitnessLookupIntVal>`) and the witness sub-type is a `DeclRefType`. Folds that already produce a concrete value do not enter the retry path. **Verification** (run locally with a debug `slang-test`; CI re-runs the full suite): - New test `associated-const-in-signature-type.slang` passes on `-cpu` (`1/1`). - The original issue repro no longer emits `E30019`/`E30523`. - RED check: stashing the fix and rebuilding makes the new test fail and the repro emit `E30019`/`E30523` again — confirming the test pins the bug. - Regression sweeps clean: `constants` (28/28), `generics` (169/169); and the conformance-order sensitive areas `autodiff`, `dynamic-dispatch`, `inheritance`, `interface` (no regressions). Closes #6703. --- <sub>🤖 Generated by an automated Slang coworker — may be inaccurate. A human maintainer should verify.</sub> --------- Co-authored-by: nv-slang-bot[bot] <274397474+nv-slang-bot[bot]@users.noreply.github.com>
1 parent efd7952 commit 6bc22ae

2 files changed

Lines changed: 81 additions & 5 deletions

File tree

source/slang/slang-check-expr.cpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2759,11 +2759,24 @@ IntVal* SemanticsVisitor::tryConstantFoldDeclRef(
27592759
auto witness =
27602760
findThisTypeWitness(SubstitutionSet(declRef), as<InterfaceDecl>(decl->parentDecl));
27612761

2762-
auto val = WitnessLookupIntVal::tryFold(
2763-
m_astBuilder,
2764-
witness,
2765-
decl,
2766-
declRef.substitute(m_astBuilder, decl->type.type));
2762+
auto foldType = declRef.substitute(m_astBuilder, decl->type.type);
2763+
auto val = WitnessLookupIntVal::tryFold(m_astBuilder, witness, decl, foldType);
2764+
2765+
// A signature-type-position fold (e.g. `float[VALUE::COUNT]`) can run before the
2766+
// conforming type's witness table is built, leaving a symbolic result; ensure its
2767+
// conformances and re-fold so the value matches the concrete constant the in-body
2768+
// path produces.
2769+
if (as<WitnessLookupIntVal>(val))
2770+
{
2771+
SLANG_ASSERT(witness);
2772+
if (auto subDeclRefType = as<DeclRefType>(witness->getSub()))
2773+
{
2774+
ensureDecl(
2775+
subDeclRefType->getDeclRef().getDecl(),
2776+
DeclCheckState::ReadyForConformances);
2777+
val = WitnessLookupIntVal::tryFold(m_astBuilder, witness, decl, foldType);
2778+
}
2779+
}
27672780
return as<IntVal>(val);
27682781
}
27692782

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK):-cpu -output-using-type
2+
3+
// Regression test for https://github.com/shader-slang/slang/issues/6703
4+
//
5+
// An associated `static const` requirement (associated constant) used in a
6+
// signature-type position (e.g. `float[T::COUNT]`) must fold to the same
7+
// compile-time constant as the value produced on the generic-substitution
8+
// path. Before the fix, reaching the requirement from a signature-type
9+
// position folded it before the conforming type's witness table was built,
10+
// leaving a symbolic `WitnessLookupIntVal` that never compared equal to the
11+
// literal produced elsewhere, so otherwise-valid array sizes were rejected
12+
// (E30019 / E30523).
13+
14+
interface IAssocConst
15+
{
16+
static const uint COUNT;
17+
}
18+
19+
struct Data<let N : uint> : IAssocConst
20+
{
21+
static const uint COUNT = N;
22+
float values[N];
23+
}
24+
25+
struct Wrapper<T : IAssocConst>
26+
{
27+
static const uint COUNT = T::COUNT;
28+
29+
// `T::COUNT` here is reached from a signature-type position.
30+
uint sum(float[T::COUNT] input)
31+
{
32+
uint total = 0;
33+
for (uint i = 0; i < T::COUNT; ++i)
34+
total += (uint)input[i];
35+
return total;
36+
}
37+
}
38+
39+
// `VALUE::COUNT` is reached from a global `static const let` initializer, a
40+
// signature-type position (`makeInput`'s return type), and a generic-argument
41+
// position (`Data<VALUE::COUNT>`) — the exact combination that failed in #6703.
42+
static const let VALUE = Wrapper<Data<3>>();
43+
44+
float[VALUE::COUNT] makeInput()
45+
{
46+
float[VALUE::COUNT] result;
47+
for (uint i = 0; i < VALUE::COUNT; ++i)
48+
result[i] = float(i + 1) * 100.0;
49+
return result;
50+
}
51+
52+
//TEST_INPUT:ubuffer(data=[0 0 0 0], stride=4):out,name=outputBuffer
53+
RWStructuredBuffer<uint> outputBuffer;
54+
55+
[numthreads(1, 1, 1)]
56+
void computeMain(int3 dispatchThreadID : SV_DispatchThreadID)
57+
{
58+
float[VALUE::COUNT] input = makeInput(); // signature-type + value-access paths
59+
Data<VALUE::COUNT> sample; // generic-argument position
60+
sample.values = input;
61+
// CHECK: 600
62+
outputBuffer[0] = VALUE.sum(sample.values);
63+
}

0 commit comments

Comments
 (0)