---
generated: true
model: claude-opus-4-7
generated_at: 2026-05-20T18:00:00+00:00
source_commit: 3250005059a2746ebc504a9d3f71ed112f1f2b94
watched_paths_digest: 0e24c839aae758e71e82ec905ea23d5fa0a613909e9d55418f42457a1e2bd89a
source_doc: docs/generated/design/name-resolution/scopes.md
source_doc_digest: b163eb95da44a7485f2a712a76e0f23eac29f3fa131adb146f13413b1dc7f67d
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).

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
`undefined identifier` error 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) |
| 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) |
| 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) |
| 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 SwitchStmt owns a fresh scope (BreakableStmt subclass); 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 `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) |
| 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
NA


## Doc gaps observed
| Anchor | Kind | Gap | Suggested addition |
| --- | --- | --- | --- |
| [#sibling-scopes](../../../design/name-resolution/scopes.md#sibling-scopes) | ambiguous-claim | The `## Sibling scopes` section enumerates three concrete uses of the sibling chain (multi-file modules, imports, re-opened namespaces) 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. |  |
| [#implicit-scopes](../../../design/name-resolution/scopes.md#implicit-scopes) | undocumented-behavior | The `## Implicit scopes` section names `parseIfLetStatement`'s twin-`ScopeDecl` desugaring but does not state a user-visible consequence (e.g. "the unwrapped variable is visible only in the positive branch"). A test for `if (let x = opt)` would have to anchor to an unstated claim, so it is deferred. | Adding a sentence along the lines of "the unwrapped variable is visible in the positive branch but not in the else branch or after the if" would 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 `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. |  |
| [#concepts](../../../design/name-resolution/scopes.md#concepts) | undocumented-behavior | The `## Concepts` section lists `LambdaDecl (parameter scope)` and `parseIfLetStatement (synthetic)` in the scope-bearing table. Lambdas have a complete surface in `tests/language-feature/lambda/` but no scope-specific claim is restated in scopes.md beyond "a dedicated ScopeDecl for the parameter list". A test for "lambda parameter is not visible outside the lambda body" would anchor to an implicit consequence rather than an explicit one; deferred until the doc enumerates that boundary. |  |
