Skip to content

Fix fragile List::binarySearch() usage#10047

Merged
jhelferty-nv merged 3 commits intoshader-slang:masterfrom
cmarcelo:fix-fragile-binary-search-usage
Mar 23, 2026
Merged

Fix fragile List::binarySearch() usage#10047
jhelferty-nv merged 3 commits intoshader-slang:masterfrom
cmarcelo:fix-fragile-binary-search-usage

Conversation

@cmarcelo
Copy link
Copy Markdown
Contributor

@cmarcelo cmarcelo commented Feb 16, 2026

The binarySearch() was comparing pointer addresses on the member list
that is ordered by the declaration position. That may not necessarily
work unless is guaranteed that the allocator produces pointers in
declaration order and they are not reordered. Use indexOf() instead.

See bfae49d ("Mediate access to ContainerDecl members (#7242)") for context.

@cmarcelo cmarcelo requested a review from a team as a code owner February 16, 2026 22:12
Copilot AI review requested due to automatic review settings February 16, 2026 22:12
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Feb 16, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaced binarySearch with indexOf to locate subAncestor and supAncestor in their common ancestor's member list when computing declaration ordering; added SLANG_ASSERT(subIndex >= 0) and SLANG_ASSERT(supIndex >= 0); retained the supIndex - subIndex difference and unchanged control flow.

Changes

Cohort / File(s) Summary
Declaration ordering change
source/slang/slang-check-decl.cpp
Replaced binarySearch with indexOf to find subAncestor and supAncestor indices; added assertions that both indices are >= 0; preserved supIndex - subIndex calculation and behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

I’m a rabbit in the codewood, hopping light and spry 🐇
I swapped a search for an index beneath the sky ✨
Two asserts stand guard where the numbers lie,
The order stayed steady as I twitched my eye,
A tiny hop, a tidy fix—hooray, code goes by!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing fragile binarySearch() usage with indexOf() for safer type comparison operations.
Description check ✅ Passed The description clearly explains why binarySearch() is fragile, what the fix does, and provides a reference commit for context.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@cmarcelo
Copy link
Copy Markdown
Contributor Author

This patch was motivated by #10048.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes fragile usage of List::binarySearch() in the semantic checking of generic type equality constraints. The old code incorrectly used binarySearch() with pointer comparison on a member list ordered by declaration position, which only worked by chance because the allocator happened to produce pointers in declaration order. This is replaced with the existing helper function _compareDeclsInCommonParentByOrderOfDeclaration() that properly compares declarations by their position in the parent container, following the guidance from PR #7242 to use mediated access to ContainerDecl members.

Changes:

  • Replaced fragile binarySearch() usage with proper declaration order comparison helper function
  • Added forward declaration for _compareDeclsInCommonParentByOrderOfDeclaration()

@cmarcelo cmarcelo force-pushed the fix-fragile-binary-search-usage branch from ecc4e7a to a9dd060 Compare February 17, 2026 01:02
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@source/slang/slang-check-decl.cpp`:
- Around line 3581-3582: The subtraction using auto subIndex =
ancestor->getMembers().indexOf(subAncestor) and auto supIndex =
ancestor->getMembers().indexOf(supAncestor) must guard against indexOf returning
-1; if either subIndex or supIndex is -1, avoid using int(supIndex - subIndex)
and instead fall back to the existing compareDecls(...) logic (or assert/handle
error) to determine ordering. Update the code around where subIndex/supIndex are
computed so you check (subIndex < 0 || supIndex < 0) and call compareDecls(…)
when invalid, otherwise keep the current subtraction path.

Comment on lines +3581 to +3582
auto subIndex = ancestor->getMembers().indexOf(subAncestor);
auto supIndex = ancestor->getMembers().indexOf(supAncestor);
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the List class definition and indexOf method
rg -B2 -A10 'indexOf\(' --type cpp --type hpp | head -100

Repository: shader-slang/slang

Length of output: 89


🏁 Script executed:

# Check Index type definition
rg 'typedef.*Index|using.*Index' --type hpp | head -30

Repository: shader-slang/slang

Length of output: 89


🏁 Script executed:

# Examine the context at lines 3581-3582
sed -n '3570,3600p' source/slang/slang-check-decl.cpp

Repository: shader-slang/slang

Length of output: 1031


🏁 Script executed:

# Search for GenericDecl definition and its member structure
rg -B2 -A15 'class GenericDecl' --type hpp | head -80

Repository: shader-slang/slang

Length of output: 89


🏁 Script executed:

# Check ContainerDecl::getMembers implementation
rg -B2 -A10 'getMembers\(\)' --type hpp --type cpp | head -100

Repository: shader-slang/slang

Length of output: 89


🏁 Script executed:

# Search for List class and indexOf method definition
rg 'indexOf' -A5 -B2 --type c --type h | head -150

Repository: shader-slang/slang

Length of output: 7585


🏁 Script executed:

# Check Index type definition
rg 'typedef.*Index|using.*Index' --type h | head -50

Repository: shader-slang/slang

Length of output: 3685


🏁 Script executed:

# Search for GenericDecl class definition
rg -A20 'class GenericDecl' --type h | head -100

Repository: shader-slang/slang

Length of output: 5103


🏁 Script executed:

# Check ContainerDecl implementation
rg -A10 'class ContainerDecl' --type h | head -150

Repository: shader-slang/slang

Length of output: 2891


🏁 Script executed:

# Search for getMembers in headers
rg -B2 -A8 'getMembers\(' --type h --type c | head -100

Repository: shader-slang/slang

Length of output: 1361


🏁 Script executed:

# Get complete indexOf implementation from slang-list.h
sed -n '1,/template.*indexOf/p' source/core/slang-list.h | tail -40

Repository: shader-slang/slang

Length of output: 891


🏁 Script executed:

# Find the full indexOf method with return statement
rg -B2 -A15 'template.*indexOf\(const T2' source/core/slang-list.h

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Search for getDirectMemberDecls implementation
rg -B2 -A15 'getDirectMemberDecls' --type c --type h | head -150

Repository: shader-slang/slang

Length of output: 9271


🏁 Script executed:

# Check if FIDDLE() marked fields are included in member lists
rg -B5 -A10 'FIDDLE.*inner' source/slang/slang-ast-decl.h

Repository: shader-slang/slang

Length of output: 532


🏁 Script executed:

# Look for how GenericDecl members are enumerated
rg -B10 -A10 'class GenericDecl' source/slang/slang-ast-decl.h

Repository: shader-slang/slang

Length of output: 602


🏁 Script executed:

# Find getDirectMemberDecls implementation in cpp files
rg -B2 -A20 'getDirectMemberDecls\(\)' --type c | head -100

Repository: shader-slang/slang

Length of output: 6349


🏁 Script executed:

# Search for how GenericDecl members are handled in the codebase
rg 'GenericDecl.*inner' --type c --type h | head -50

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Find the complete indexOf implementation with return statement
rg -B2 -A8 'Index indexOf' source/core/slang-list.h

Repository: shader-slang/slang

Length of output: 284


🏁 Script executed:

# Search for getDirectMemberDecls implementation in cpp
rg -B3 -A15 'List<Decl\*> const& ContainerDecl::getDirectMemberDecls' --type c

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Also check slang-ast-decl.cpp for implementation
grep -n 'getDirectMemberDecls' source/slang/slang-ast-decl.cpp | head -20

Repository: shader-slang/slang

Length of output: 196


🏁 Script executed:

# Look at what fields are included for GenericDecl member enumeration
rg -B5 -A20 'class GenericDecl' source/slang/slang-ast-decl.cpp

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Check how FIDDLE macro works - does it include fields in member lists?
rg -B2 -A5 'FIDDLE\(\)' source/slang/slang-ast-decl.h | grep -A5 'GenericDecl'

Repository: shader-slang/slang

Length of output: 385


🏁 Script executed:

# Read the implementation at line 304
sed -n '300,330p' source/slang/slang-ast-decl.cpp

Repository: shader-slang/slang

Length of output: 687


🏁 Script executed:

# Look for how _directMemberDecls is populated for GenericDecl
rg '_directMemberDecls' --type c --type h | grep -i generic

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Search for how GenericDecl handles its inner decl in member operations
rg -B5 -A10 'GenericDecl.*addMember\|addDirectMemberDecl.*GenericDecl' --type c

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Check if there's special handling for GenericDecl's inner in the context of checkGenericTypeEqualityConstraintSubType
sed -n '3550,3600p' source/slang/slang-check-decl.cpp

Repository: shader-slang/slang

Length of output: 1938


🏁 Script executed:

# Look for tests or usages that show expected behavior when ancestor is GenericDecl
rg -B10 -A10 'findDeclsLowestCommonAncestor.*GenericDecl\|GenericDecl.*indexOf' --type c

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Check the actual field structure - is inner a direct member or a special field?
rg -B10 -A30 'class GenericDecl' source/slang/slang-ast-decl.h | head -60

Repository: shader-slang/slang

Length of output: 1027


🏁 Script executed:

# Search for how GenericTypeConstraintDecl relates to GenericDecl
rg -B5 -A15 'class GenericTypeConstraintDecl' source/slang/slang-ast-decl.h

Repository: shader-slang/slang

Length of output: 819


🏁 Script executed:

# Look for where generic parameters are added as members
rg -B5 -A10 'addMember.*GenericTypeParam\|GenericTypeParam.*addMember' --type c | head -50

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Check if there's defensive code elsewhere that handles indexOf returning -1
rg -B3 -A3 'indexOf.*!= -1\|indexOf.*>= 0\|indexOf.*< 0' --type c --type h | head -80

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Let me look at the broader context of checkGenericTypeEqualityConstraintSubType
sed -n '3550,3620p' source/slang/slang-check-decl.cpp

Repository: shader-slang/slang

Length of output: 2569


🏁 Script executed:

# Check findDeclsLowestCommonAncestor to understand what it returns
rg -B5 -A20 'findDeclsLowestCommonAncestor' --type c --type h | head -100

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Look for safeguards elsewhere in the codebase when using indexOf result
rg -B2 -A5 'indexOf.*-1' source/core/slang-list.h

Repository: shader-slang/slang

Length of output: 237


🏁 Script executed:

# Search for where GenericTypeParamDecl instances are added as members
rg -B10 -A10 'GenericTypeParamDecl.*addMember\|addDirectMemberDecl.*Param' --type c | head -50

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Look at the broader generic decl creation flow
rg -B5 -A15 'new GenericTypeParamDecl\|GenericTypeParamDecl\(' --type c | head -80

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# See if there's any documentation about what getMembers returns for GenericDecl
rg -B3 -A3 'GenericDecl.*getMembers\|getMembers.*GenericDecl' --type c --type h

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Check if there are any assertions or checks before using indexOf result elsewhere
rg -B2 -A2 'subIndex.*supIndex\|supIndex.*subIndex' --type c

Repository: shader-slang/slang

Length of output: 44


🏁 Script executed:

# Final check: confirm Index(-1) is the standard sentinel by looking at contains patterns
rg 'indexOf.*!= -1\|indexOf.*== -1\|!= Index\(-1\)' source/core/slang-list.h source/core/slang-array.h

Repository: shader-slang/slang

Length of output: 44


Add guards against indexOf returning -1 to prevent incorrect index subtraction.

At lines 3581-3582, indexOf can return -1 if subAncestor or supAncestor is not present in ancestor->getMembers(). The direct subtraction int(supIndex - subIndex) then produces incorrect ordering: (-1 - 0 = -1), (0 - (-1) = 1), or (-1 - (-1) = 0), with the zero case silently triggering the diagnostic at line 3593. Check both indices and either assert they are valid or fall back to compareDecls.

Suggested guard
        auto subIndex = ancestor->getMembers().indexOf(subAncestor);
        auto supIndex = ancestor->getMembers().indexOf(supAncestor);
+       if (subIndex == Index(-1) || supIndex == Index(-1))
+           return compareDecls(subAncestor, supAncestor);
        return int(supIndex - subIndex);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto subIndex = ancestor->getMembers().indexOf(subAncestor);
auto supIndex = ancestor->getMembers().indexOf(supAncestor);
auto subIndex = ancestor->getMembers().indexOf(subAncestor);
auto supIndex = ancestor->getMembers().indexOf(supAncestor);
if (subIndex == Index(-1) || supIndex == Index(-1))
return compareDecls(subAncestor, supAncestor);
return int(supIndex - subIndex);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@source/slang/slang-check-decl.cpp` around lines 3581 - 3582, The subtraction
using auto subIndex = ancestor->getMembers().indexOf(subAncestor) and auto
supIndex = ancestor->getMembers().indexOf(supAncestor) must guard against
indexOf returning -1; if either subIndex or supIndex is -1, avoid using
int(supIndex - subIndex) and instead fall back to the existing compareDecls(...)
logic (or assert/handle error) to determine ordering. Update the code around
where subIndex/supIndex are computed so you check (subIndex < 0 || supIndex < 0)
and call compareDecls(…) when invalid, otherwise keep the current subtraction
path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would someone help verify if it is applicable, is it a valid concern? My own understanding here is that we expect both to be found.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it wouldn't hurt to have SLANG_ASSERT.
The assumption of the data being sorted holds now but it may change later intentionally or unintentionally.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

        SLANG_ASSERT(subIndex >= 0);
        SLANG_ASSERT(supIndex >= 0);

and appended to the commit message

    Since we are here, document with ASSERTs the assumption that both
    subIndex and supIndex will be present.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've found a case where the asserts added here don't hold, will update the PR and add the test that can trigger it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the assert would've failed on cases where one of the sides is the ancestor. I've posted a test in #10447 that would trigger such assertion. Without the assert this case gets a correct diagnostic by slang. So I've removed the asserts from here now.

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.

@cmarcelo Where is the test? This is #10047, I don't see it in the diff.

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.

Ahh, #10447. I'm half-blind.

@cmarcelo
Copy link
Copy Markdown
Contributor Author

@coderabbitai summary

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Summary regeneration triggered.

@szihs szihs added the pr: non-breaking PRs without breaking changes label Feb 19, 2026
@szihs szihs requested a review from jkwak-work February 19, 2026 10:53
@cmarcelo cmarcelo force-pushed the fix-fragile-binary-search-usage branch 2 times, most recently from c707bf4 to e1c927e Compare February 23, 2026 18:49
The binarySearch() was comparing pointer addresses on the member list
that is ordered by the declaration position.  That may not necessarily
work unless is guaranteed that the allocator produces pointers in
declaration order and they are not reordered.  Use indexOf() instead.

See bfae49d ("Mediate access to ContainerDecl members (shader-slang#7242)") for
context.
@cmarcelo cmarcelo force-pushed the fix-fragile-binary-search-usage branch from e1c927e to 89d2220 Compare March 9, 2026 00:42
github-actions[bot]

This comment was marked as outdated.

github-actions[bot]

This comment was marked as outdated.

@github-actions

This comment has been minimized.

@jhelferty-nv
Copy link
Copy Markdown
Contributor

@cmarcelo it looks like #10048 does the same thing and a bit more. Should this one be closed?

@cmarcelo
Copy link
Copy Markdown
Contributor Author

@cmarcelo it looks like #10048 does the same thing and a bit more. Should this one be closed?

@jhelferty-nv the PR mentioned is on top of this one, so all things being equal I rather we land (or reject) this change independently if possible.

@jhelferty-nv
Copy link
Copy Markdown
Contributor

Ah. Let's get it in then.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ✅ Clean — no significant issues found. 1 gap noted.

This PR replaces two binarySearch() calls with indexOf() in checkGenericTypeEqualityConstraintSubType. The old code used binarySearch() which compares pointer addresses via operator<, but the member list is ordered by declaration position — not by memory address. This made the binary search both incorrect (searching a list not sorted by the search predicate) and non-deterministic (dependent on allocator behavior). The indexOf() replacement uses identity comparison (==) with linear scan, which is correct and consistent with the existing pattern in _compareDeclsInCommonParentByOrderOfDeclaration (line 12866).

Changes Overview

Semantic checker — sibling parameter ordering (source/slang/slang-check-decl.cpp)

  • What changed: Replaced ancestor->getMembers().binarySearch(...) with ancestor->getMembers().indexOf(...) for two lookups that determine declaration order of sibling generic parameters sharing a common ancestor. The O(n) cost is acceptable given typical generic parameter counts and compile-time-only execution.
Findings (1 total)
Severity Location Finding
🟡 Gap slang-check-decl.cpp:3718 No existing test exercises sibling-parameter equality constraint ordering (the exact path this fix targets)


auto subIndex = ancestor->getMembers().binarySearch(subAncestor);
auto supIndex = ancestor->getMembers().binarySearch(supAncestor);
auto subIndex = ancestor->getMembers().indexOf(subAncestor);
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 sibling-parameter equality constraint ordering

The code comments at lines 3706-3710 describe the exact scenario this fixes: __generic<A : IA, B : IB> where A::T == B::T — two sibling parameters in the same GenericDecl with an associated-type equality constraint. However, no existing test exercises this specific path. The closest tests (type-equality-cononical.slang, type-equality-ancestor-ordering.slang) use interface self-types or nesting scenarios, not sibling parameters within the same generic.

Since the old binarySearch bug could silently produce wrong ordering depending on allocator behavior (non-deterministic), a targeted test would guard against future regressions in this path.

Suggestion: Consider adding a test like:

//TEST:SIMPLE(filecheck=CHECK):

interface IHasAssoc { associatedtype T; }

struct Impl : IHasAssoc { typealias T = int; }

__generic<A : IHasAssoc, B : IHasAssoc>
void testSiblings(A a, B b)
    where A::T == B::T
{}

// CHECK-NOT: error

This exercises the findDeclsLowestCommonAncestorindexOf path with both forward and reverse orderings of sibling generic parameters.

Comment on lines +3581 to +3582
auto subIndex = ancestor->getMembers().indexOf(subAncestor);
auto supIndex = ancestor->getMembers().indexOf(supAncestor);
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.

Ahh, #10447. I'm half-blind.

@jhelferty-nv jhelferty-nv added this pull request to the merge queue Mar 23, 2026
Merged via the queue into shader-slang:master with commit 3fa9c71 Mar 23, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: non-breaking PRs without breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants