---
generated: true
model: claude-fable-5
generated_at: 2026-07-08T15:25:00Z
source_commit: 6b490bc8cb3cf11b356ddf70168c5609efa46341
watched_paths_digest: 40c4cf00e9666a39b09588bbd8d3f43205747a31f3e28345a73faeebd523b538
source_doc: docs/generated/design/name-resolution/scopes.md
source_doc_digest: c9901843c1eb994344b12c42c0822075e416d164b0ce0439a8556b971985a664
warning: "Auto-generated. May drift from source. Do not edit by hand."
---

# Tests for name-resolution/scopes

## Intent

Tests verify the lexical-scoping behaviors described in
[`docs/generated/design/name-resolution/scopes.md`](../../../design/name-resolution/scopes.md):
which AST nodes introduce a fresh `Scope`, what is reachable from
which point in the source, how lookup walks the parent chain plus the
sibling chain, and the documented edge cases (block-local
`hiddenFromLookup`, extension-member exclusion, generic-parameter
isolation, namespace reopening, `using`-decl sibling wiring, interface
constraint placement, and the `if (let ...)` synthetic scope).

The bundle pairs each scope-kind claim with a **positive** test that
resolves a name inside the scope and a **negative** test that proves
the same name is not visible just outside it. Diagnostic claims use
`//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK):` to pin the diagnostic code at
the use site (with caret columns matching the runner's "Suggested
annotations" output). Positive resolution claims use
`//TEST:INTERPRET(filecheck=CHECK):` because scoping is
target-independent (the scope chain is fixed at semantic check, before
any backend lowering).

Multi-backend rule: every claim in this doc is target-independent, so
each test uses a single directive.

## Functional coverage

| Claim                                                                                                                                                                                                                 | Intent     | Anchor                                                                                                            | Tests                                                                                                                      |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| A sibling of the outer decl that mentions a generic type parameter cannot reach it; its scope chain does not pass through the GenericDecl.                                                                            | negative   | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`generic-param-not-visible-in-sibling-decl.slang`](generic-param-not-visible-in-sibling-decl.slang)                       |
| A `__constraint` whose subject is an associated type reached through `This` (e.g. `This.A : IBar`) is permitted, unlike a bare-This subject.                                                                          | functional | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`constraint-subject-assoctype-allowed.slang`](constraint-subject-assoctype-allowed.slang)                                 |
| A `__constraint` whose subject resolves to the bare `This` type is rejected with constraint-subject-cannot-be-this-type (E30427); giving an interface a base is the inheritance clause's role.                        | negative   | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`constraint-subject-this-rejected.slang`](constraint-subject-this-rejected.slang)                                         |
| ExtensionDecl members are reachable only via member lookup on the extended type, never as unqualified file-scope names.                                                                                               | negative   | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`extension-method-not-in-enclosing-scope.slang`](extension-method-not-in-enclosing-scope.slang)                           |
| Inside a block the `Decl::hiddenFromLookup` flag hides a local until its DeclStmt; a forward reference inside the same block is rejected as undefined.                                                                | negative   | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`block-scope-forward-ref-rejected.slang`](block-scope-forward-ref-rejected.slang)                                         |
| The block-local `hiddenFromLookup` rule does not apply at file scope; a function may refer to another function declared later in the same file.                                                                       | functional | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`file-scope-forward-reference-allowed.slang`](file-scope-forward-reference-allowed.slang)                                 |
| When a `using` argument does not resolve to a namespace-like entity, no sibling scope is added and the checker diagnoses expected-a-namespace (E30061).                                                               | negative   | [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes)           | [`using-non-namespace-rejected.slang`](using-non-namespace-rejected.slang)                                                 |
| A generic type parameter lives in the GenericDecl's scope; the inner decl's scope chain passes through it, so `T` resolves inside the function body.                                                                  | functional | [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes)                                     | [`generic-param-visible-in-body.slang`](generic-param-visible-in-body.slang)                                               |
| The `associatedtype A : IBar` inheritance-clause form places its GenericTypeConstraintDecl in the enclosing interface's scope as a requirement member, so generic code constrained to the interface can call IBar's requirement on T.A. | functional | [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes)                                     | [`assoctype-constraint-inheritance-clause-form.slang`](assoctype-constraint-inheritance-clause-form.slang)                 |
| The `associatedtype A where A : IBar` where-clause form places its GenericTypeConstraintDecl in the enclosing interface's scope like the inheritance-clause form, so generic code can call IBar's requirement on T.A. | functional | [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes)                                     | [`assoctype-constraint-where-clause-form.slang`](assoctype-constraint-where-clause-form.slang)                             |
| The dedicated `__constraint` keyword inserts a GenericTypeConstraintDecl directly into the interface scope, equivalent to the two associatedtype clause forms, so generic code can call IBar's requirement on T.A.    | functional | [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes)                                     | [`interface-constraint-keyword-form.slang`](interface-constraint-keyword-form.slang)                                       |
| The `if (let x = ...)` desugaring creates a ScopeDecl for the user-visible unwrapped variable inside the positive branch, so the unwrapped name resolves there.                                                       | functional | [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes)                                     | [`if-let-unwrapped-visible-in-positive-branch.slang`](if-let-unwrapped-visible-in-positive-branch.slang)                   |
| The ScopeDecl that `if (let x = ...)` creates for the unwrapped variable covers only the positive branch; the name is undefined after the if statement.                                                               | negative   | [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes)                                     | [`if-let-unwrapped-not-visible-after-if.slang`](if-let-unwrapped-not-visible-after-if.slang)                               |
| A BlockStmt introduces a fresh scope; a local declared inside is not visible outside that block.                                                                                                                      | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`block-scope-name-not-visible-outside.slang`](block-scope-name-not-visible-outside.slang)                                 |
| A CallableDecl's scope contains its parameter decls; the parameter is reachable from the function body.                                                                                                               | functional | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`function-scope-parameter-visible-in-body.slang`](function-scope-parameter-visible-in-body.slang)                         |
| A FuncDecl owns its own scope; a local declared inside helper() is not visible from another function's scope.                                                                                                         | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`function-scope-local-not-visible-from-caller.slang`](function-scope-local-not-visible-from-caller.slang)                 |
| A NamespaceDecl owns its own scope; members declared inside it are not visible from the enclosing module scope without qualification.                                                                                 | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`namespace-member-needs-qualification.slang`](namespace-member-needs-qualification.slang)                                 |
| A switch body parses as a scoped BlockStmt (the parser does not assign SwitchStmt::scopeDecl); a local declared inside one of its cases is not visible after the switch.                                              | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`switch-scope-local-not-visible-outside.slang`](switch-scope-local-not-visible-outside.slang)                             |
| A while loop's block body owns its own scope (the parser does not push a fresh ScopeDecl on the WhileStmt itself); a local declared in the body is not visible after the loop.                                        | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`while-body-local-not-visible-outside.slang`](while-body-local-not-visible-outside.slang)                                 |
| A do-while loop's block body owns its own scope (the parser does not push a fresh ScopeDecl on the DoWhileStmt itself); a local declared in the body is not visible after the loop.                                   | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`do-while-body-local-not-visible-outside.slang`](do-while-body-local-not-visible-outside.slang)                           |
| A `let`-bound local declared inside a block lives in the BlockStmt's scope only; it cannot be referenced from a later statement at the enclosing scope.                                                               | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`let-in-block-not-visible-outside.slang`](let-in-block-not-visible-outside.slang)                                         |
| A function parameter lives in its owning CallableDecl's scope; a sibling function cannot see it.                                                                                                                      | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`function-parameter-not-visible-from-sibling-function.slang`](function-parameter-not-visible-from-sibling-function.slang) |
| A name inside a NamespaceDecl scope is reachable via explicit `Namespace::name` qualification.                                                                                                                        | functional | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`namespace-qualified-name-resolves.slang`](namespace-qualified-name-resolves.slang)                                       |
| A typedef declared inside a block lives in the BlockStmt's scope only; it cannot be referenced from a later statement at the enclosing scope.                                                                         | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`typedef-in-block-not-visible-outside.slang`](typedef-in-block-not-visible-outside.slang)                                 |
| AggTypeDecl/StructDecl introduces a scope; a method body declared inside the struct can refer to the struct's fields by unqualified name.                                                                             | functional | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`member-scope-fields-visible-from-method.slang`](member-scope-fields-visible-from-method.slang)                           |
| ExtensionDecl owns a scope; the extended type's member-lookup finds the extension method when called via the receiver.                                                                                                | functional | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`extension-method-callable-via-receiver.slang`](extension-method-callable-via-receiver.slang)                             |
| ForStmt owns its own scope; the init-statement's variable is not visible after the loop.                                                                                                                              | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`for-loop-init-not-visible-outside.slang`](for-loop-init-not-visible-outside.slang)                                       |
| IfStmt itself does not own a scope, but its body parses as a BlockStmt that does; a local declared inside the then-block is not visible after the `if`.                                                               | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`if-block-local-not-visible-outside.slang`](if-block-local-not-visible-outside.slang)                                     |
| ModuleDecl/FileDecl members are reachable from any function in the file via the parent-scope walk.                                                                                                                    | functional | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`file-scope-const-visible-in-function.slang`](file-scope-const-visible-in-function.slang)                                 |
| Struct fields live in the AggTypeDecl's scope only; they are not visible from a free function at file scope.                                                                                                          | negative   | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`member-field-not-visible-outside-struct.slang`](member-field-not-visible-outside-struct.slang)                           |
| The ForStmt's scope holds the init variable; the body sees it for the duration of the loop.                                                                                                                           | functional | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)                     | [`for-loop-var-visible-in-body.slang`](for-loop-var-visible-in-body.slang)                                                 |
| Lookup walks to the parent scope when a name is not found in the current scope; an inner block can see an enclosing block's local.                                                                                    | functional | [#scope-walking-order-during-lookup](../../../design/name-resolution/scopes.md#scope-walking-order-during-lookup) | [`block-scope-resolves-outer-from-inner.slang`](block-scope-resolves-outer-from-inner.slang)                               |
| Visiting `currentScope` first means an inner declaration with the same name as an outer one shadows it inside the inner block.                                                                                        | functional | [#scope-walking-order-during-lookup](../../../design/name-resolution/scopes.md#scope-walking-order-during-lookup) | [`block-scope-shadowing.slang`](block-scope-shadowing.slang)                                                               |
| When a VarExpr fails to resolve, the checker reuses the scope-chain traversal to find a close spelling and attaches a "did you mean" suggestion to the undefined-identifier diagnostic (E30015).                      | negative   | [#scope-walking-order-during-lookup](../../../design/name-resolution/scopes.md#scope-walking-order-during-lookup) | [`undefined-identifier-suggests-close-spelling.slang`](undefined-identifier-suggests-close-spelling.slang)                 |
| The close-spelling walk skips the core module, so a typo whose only near match is a builtin gets an undefined-identifier diagnostic with no "did you mean" suggestion.                                                | negative   | [#scope-walking-order-during-lookup](../../../design/name-resolution/scopes.md#scope-walking-order-during-lookup) | [`undefined-identifier-no-suggestion-from-core-module.slang`](undefined-identifier-no-suggestion-from-core-module.slang)   |
| A `using` declaration splices the named namespace's scope in as a sibling, making the namespace's members reachable without qualification.                                                                            | functional | [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                                       | [`using-namespace-members-unqualified.slang`](using-namespace-members-unqualified.slang)                                   |
| A `using` of a namespace declared in multiple blocks makes members from every block reachable unqualified, because the splice covers the namespace's owned and sibling scopes.                                        | functional | [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                                       | [`using-reopened-namespace-members-unqualified.slang`](using-reopened-namespace-members-unqualified.slang)                 |
| Re-opening the same namespace name reuses the existing NamespaceDecl and links siblings; members from both declarations are reachable via the same qualified name.                                                    | functional | [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                                       | [`namespace-reopened-merges-members.slang`](namespace-reopened-merges-members.slang)                                       |
| When a module is imported, the checker adds its scope as a sibling so names from it are reachable without explicit qualification; the core module's builtins (e.g. `sin`) are reachable through this mechanism.       | functional | [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                                       | [`builtin-from-implicit-core-module.slang`](builtin-from-implicit-core-module.slang)                                       |

## Untested claims

| Claim                                                                                                                                                                                                                            | Reason                | Anchor                                                                                                  | Why untested                                                                                                                                                                                                                                     |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SemanticDecl` and `AttributeDecl` are direct `ContainerDecl` subclasses that own a scope via `ContainerDecl::ownedScope`.                                                                                                       | internal-source-fact  | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)           | These decls back `[attribute]`/semantic machinery with no surface syntax whose name-resolution behavior the doc states; the ownedScope field has no user-observable consequence described.                                                       |
| `SynthesizedStructDecl`, `GLSLInterfaceBlockDecl`, `AssocTypeDecl`, and `GlobalGenericParamDecl` are `AggTypeDecl`-family subclasses that own scopes via `ContainerDecl::ownedScope`.                                            | internal-source-fact  | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)           | The table row states class-hierarchy facts; the doc gives no name-visibility consequence distinguishing these from the already-tested `StructDecl` case (and `SynthesizedStructDecl` has no surface syntax at all).                              |
| `parseTargetSwitchStmtImpl` creates a per-case `ScopeDecl` rather than one on the `TargetSwitchStmt`/`StageSwitchStmt` statement.                                                                                                | internal-source-fact  | [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)           | The doc states parser mechanics only; it does not commit to a user-visible visibility boundary (e.g. whether a local in one `__target_switch` case is visible in another), so a test cannot anchor an expectation. See the matching doc-gap row. |
| A module split across multiple `__include`d files gets one `FileDecl` per file attached as a sibling of the module's scope, so lookup sees the union of all files' members.                                                      | needs-multi-file-test | [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                             | Sibling-scope use case 1 is observable only across two or more source files (`__include`); this bundle's single-file `slang-test` directives cannot express it.                                                                                  |
| When module B explicitly imports module A, A's scope is added as a sibling of B's scope.                                                                                                                                         | needs-multi-file-test | [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                             | The explicit `import` form of use case 2 needs a second module file. The implicit core-module import half of the claim is covered by [`builtin-from-implicit-core-module.slang`](builtin-from-implicit-core-module.slang).                       |

## Doc gaps observed

| Anchor                                                                                                  | Kind                  | Gap                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Suggested addition                                                                                                                                                  |
| ------------------------------------------------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes)                             | ambiguous-claim       | The `## Sibling scopes` section enumerates four concrete uses of the sibling chain (multi-file modules, imports, re-opened namespaces, `using` declarations) but does not promise a user-visible diagnostic for the "two siblings define the same name" ambiguity case. The closely related ambiguous-reference diagnostic is anchored from `name-resolution/lookup.md` in the pipeline/03-semantic-check bundle; if scopes.md were extended with a one-line note about the ambiguity surface, the test could be re-anchored here. |                                                                                                                                                                     |
| [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)           | ambiguous-claim       | The `WhileStmt`/`DoWhileStmt` row says "the loop body owns its own scope only when it is a `BlockStmt`" but does not state the user-visible consequence when the body is a single non-block statement (e.g. a bare `DeclStmt`): whether the declared name leaks into the enclosing scope, or such a body is rejected outright. A test for the non-block body cannot anchor an expectation.                                                                                                                              | State the observable behavior for a non-block loop body in one sentence (leaks / rejected / unreachable-by-grammar).                                                |
| [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)           | missing-surface       | The `SwitchStmt`/`TargetSwitchStmt`/`StageSwitchStmt` row says `parseTargetSwitchStmtImpl` creates a per-case `ScopeDecl`, but does not name the user-visible consequence — e.g. whether a local declared in one `__target_switch` case is visible from another case of the same statement.                                                                                                                                                                                                                            | Add one sentence stating the cross-case visibility consequence of the per-case `ScopeDecl` so a test can pin it.                                                    |
| [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes) | undocumented-behavior | The `## Edge cases and failure modes` section mentions `UnscopedForStmt` but only describes its existence ("`for` loop's initialization variable leaks into the surrounding scope as HLSL semantics demand"). There is no documented surface for invoking HLSL mode from a `.slang` file at `slang-test` time, so the test cannot anchor to a single-source-file run that flips the language between Slang and HLSL. The doc could note which invocation switch enables UnscopedForStmt behavior to unblock that test. |                                                                                                                                                                     |
| [#edge-cases-and-failure-modes](../../../design/name-resolution/scopes.md#edge-cases-and-failure-modes) | undocumented-behavior | The `## Edge cases and failure modes` section mentions `UnparsedStmt` capturing `currentScope` and `outerScope` at parse time, but its observable consequence (which decls a deferred-body function sees once it is finally parsed) overlaps with pipeline/03-semantic-check's `function-body-checked-after-parser-handoff` test. No additional scopes-specific surface is documented; if the doc enumerated a scope-visibility consequence distinct from the parse-handoff one, a separate test could anchor here.    |                                                                                                                                                                     |
| [#concepts](../../../design/name-resolution/scopes.md#concepts)                                         | undocumented-behavior | The `## Concepts` section gives a precise description of `Scope`'s three fields and the `containerDecl` ownership convention. None of those properties are observable through `slang-test`; they show up only in IR/AST dumps or in internal asserts. Tests for those would have to fish through `-dump-ir` (which lowers past scopes already) and are not anchorable here.                                                                                                                                            |                                                                                                                                                                     |
| [#scope-bearing-ast-nodes](../../../design/name-resolution/scopes.md#scope-bearing-ast-nodes)           | undocumented-behavior | The table's `LambdaExpr (parameter scope)` row states that the parser pushes a dedicated `ScopeDecl` for the lambda parameter list, owned by the expression rather than by `LambdaDecl`, but no boundary claim is stated (e.g. "a lambda parameter is not visible outside the lambda body"). A test would anchor to an implicit consequence rather than an explicit one; deferred until the doc enumerates that boundary.                                                                                              | Add a sentence naming the visibility boundary of the lambda parameter scope so a positive/negative pair can anchor to it.                                           |
