Skip to content

Allow __getAddress on a mutable structured-buffer element (#12581) - #12592

Open
nv-slang-bot[bot] wants to merge 11 commits into
masterfrom
fix/issue-12581
Open

Allow __getAddress on a mutable structured-buffer element (#12581)#12592
nv-slang-bot[bot] wants to merge 11 commits into
masterfrom
fix/issue-12581

Conversation

@nv-slang-bot

@nv-slang-bot nv-slang-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Motivation

Taking the address of a mutable structured-buffer element with __getAddress was rejected on every target:

RWStructuredBuffer<int> dataBuffer;
RWStructuredBuffer<int> outputBuffer;

[numthreads(1,1,1)]
void computeMain()
{
    int* ptr = __getAddress(dataBuffer[0]);   // error[E31160]: invalid __getAddress usage:
                                               // cannot take the address of a function-local
                                               // variable on this target.
    outputBuffer[0] = *ptr;
}

The buffer element is a genuine device-memory l-value, not a function-local. The evidence that this is a gap rather than an intended prohibition: &dataBuffer[0] already compiles today on the same buffer, and #10280 established that __getAddress(x) should be equivalent to &x. So the two spellings that are meant to be identical diverged for structured-buffer elements.

Lifting that front-end rejection then exposed a latent SPIR-V lowering bug that CI caught on the -vk targets: the resulting int* slot kept a physical (Device/PhysicalStorageBuffer) pointee even though the value written into it is a descriptor-backed logical StorageBuffer pointer, and a surviving slot (a -O0 local, or a -g debug backing variable) produced a store whose pointer and object storage classes disagreed:

error: OpStore Pointer <id> type does not match Object <id> type

This PR therefore has two parts: (1) the front-end change that lifts E31160, and (2) the address-space fix that keeps the pointer logically StorageBuffer end-to-end so validation passes.

Proposed solution

Part 1 — front-end (getValidTypeForAddressOf). __getAddress and & route through different front-end paths. & binds through the normal __ref machinery; __getAddress parses to an AddressOfExpr and is validated by getValidTypeForAddressOf (source/slang/slang-check-expr.cpp). A subscript buf[i] desugars to an InvokeExpr of the buffer's ref __subscript, whose intrinsic op is kIROp_RWStructuredBufferGetElementPtr (hlsl.meta.slang). The InvokeExpr branch of getValidTypeForAddressOf only whitelisted a ref accessor with kIROp_GetOffsetPtr, so it returned nullptr and the checker raised E31160.

The fix extends that branch to also accept kIROp_RWStructuredBufferGetElementPtr, returning the same pointer type &buf[i] produces: Ptr<T, ReadWrite, Device, DefaultDataLayout>. That intrinsic op is attached to a subscript ref accessor by exactly one place in the core module — the kMutableStructuredBufferCases template in hlsl.meta.slang, which emits it only for RWStructuredBuffer and RasterizerOrderedStructuredBuffer — so reaching this branch already implies one of those two types. The code SLANG_RELEASE_ASSERTs that invariant (after unwrapModifiedType, so globallycoherent etc. are handled) rather than silently rejecting, per the codebase's "assert impossible shapes" rule.

Part 2 — address-space specialization (slang-ir-specialize-address-space.cpp). The surface type int* defaults to AddressSpace.Device (== internal UserPointer == SPIR-V PhysicalStorageBuffer), so int* p = &buf[i] lowers to a Ptr<Ptr<T, Device>, Function> slot. But the value written is RWStructuredBufferGetElementPtr, a logical StorageBuffer pointer, and there is no valid logical↔physical pointer conversion. When the slot is mem2reg'd to SSA the disagreement is invisible (only the value survives); a slot that must materialize — most commonly a DebugVar under -g, or any -O0 local — emits the mismatched store.

The principled fix reconciles the slot with what is actually written into it: a local pointer slot (Var/DebugVar) adopts the address space of the pointer values stored into it, rewriting only the slot's contained pointer value-type (the slot's own Function storage class is unchanged). This runs as a pre-pass in the address-space specialization pass, and the specialized type is then propagated through the slot's loads and their derived pointer users (GetOffsetPtr/GetElementPtr/FieldAddress, phi/block parameters) by the pass's existing propagateAddressSpaceFromInsts machinery — so a value that flows through control-flow re-assignment (if (c) p = q;) or a derived op (p[2]) stays logical StorageBuffer too. The address spaces of all writes into a slot are joined; a genuine conflict between two concrete address spaces (a slot fed both a StorageBuffer and a physical Device pointer) is diagnosed as inconsistent-pointer-address-space rather than silently coerced — the same diagnostic the pre-existing phi-argument TODO in processFunction was always meant to raise, which this change also now wires up. No logical↔physical bitcast is introduced anywhere.

This is the correct layer: the descriptor-backed StorageBuffer pointer produced by RWStructuredBufferGetElementPtr is the canonical, intended representation; the stale Device pointee on the slot is the accidental artifact of the surface Ptr<T> default, and the address-space specialization pass already exists to resolve exactly these Generic/under-specified pointer types before emission. Fixing it there (rather than guarding in the SPIR-V emitter) keeps emission simple, per the codebase philosophy.

Change summary

File Change
source/slang/slang-check-expr.cpp Extend the InvokeExpr branch of getValidTypeForAddressOf to accept a mutable structured-buffer subscript (kIROp_RWStructuredBufferGetElementPtr), returning Ptr<T, ReadWrite, Device, DefaultDataLayout>, and SLANG_RELEASE_ASSERT the base is RWStructuredBuffer/RasterizerOrderedStructuredBuffer (the intrinsic op guarantees it).
source/slang/slang-ir-specialize-address-space.cpp Add an order-independent fixpoint pre-pass (reconcilePointerSlots / reconcilePointerSlotWithStoredValue), gated to the SPIR-V / sink-carrying path, that specializes a local pointer slot's contained value-type to the joined address space of the pointer values written into it — resolving a stored load-of-a-slot or an address-computation op (GetOffsetPtr/GetElementPtr/FieldAddress) through to its base via getStoredValueAddrSpace (a not-yet-resolved parameter — phi or function parameter — resolves to Generic, since its address space is fixed only later by propagation or call-site specialization) — seeds propagateAddressSpaceFromInsts with the retyped loads, and diagnoses a genuine slot conflict once. resolvePhiAddrSpace joins phi arguments across the fixpoint (phi-conflict diagnosis is owned by processParam in spirv-legalize, before phis are eliminated).
source/slang/slang-ir-specialize-address-space.h Thread an optional DiagnosticSink* through specializeAddressSpace (forward-declared).
source/slang/slang-ir-spirv-legalize.cpp Pass m_sink into specializeAddressSpace so the pass can diagnose.
tests/spirv/get-address-buffer-element-debug.slang New SPIR-V regression (-O0 and -g2 -O0) for both __getAddress(buf[i]) and &buf[i], with control-flow re-assignment and a derived p[2] op; the -g2 run is the exact shape CI originally caught.
tests/diagnostics/pointer-address-space-conflict.slang New diagnostic test: a slot fed a StorageBuffer element pointer and a physical Device pointer across control flow is rejected with inconsistent-pointer-address-space.
tests/spirv/get-address-buffer-element-param-merge.slang New regression guard (default and -O0, non-debug): a buffer-element pointer passed through a [noinline] helper's int* parameter and merged with another element pointer must not be falsely flagged as an address-space conflict — pins that the pre-pass treats an unresolved parameter as Generic rather than reading its provisional physical type.
tests/language-feature/pointer/get-address-buffer-element.slang New regression test (-cpu and -vk): read, pointer arithmetic, and write through __getAddress(buf[i]).
tests/language-feature/pointer/get-address-buffer-element-accepted.slang New positive SPIR-V compile test pinning which forms are accepted (RW, ROV, member chain, custom-layout buffers).
tests/language-feature/pointer/get-address-buffer-element-runtime.slang New -cpu/-vk runtime test: member-of-element address read + write-back, and cross-element pointer arithmetic on a struct-element buffer (exercises element stride, not field size).
tests/language-feature/pointer/get-address-buffer-element-ordered.slang New -vk runtime test for a RasterizerOrderedStructuredBuffer element address (ROV is not accepted by nvrtc/CPU; it lowers to a storage buffer on Vulkan).
tests/spirv/get-address-buffer-element-layout.slang New SPIR-V test proving the element pointer for a Std140 struct-element buffer carries ArrayStride 16 — i.e. __getAddress preserves the buffer's stride, matching &buf[i].
tests/language-feature/types/pointer-{basic-operations,arithmetic,generic-syntax,immutable,subscript}.slang Add five pointer coverage tests exercising __getAddress(buf[i]) (deref forms, arithmetic, Ptr<T>/T* equivalence, ImmutablePtr, subscript) on -cpu and -vk (the ImmutablePtr one is -cpu-only, see the process report). These correspond to tests previously carried but disabled with an inaccurate "requires GPU runtime" tag — the real blocker was this front-end rejection.
tests/language-feature/pointer/get-address-validation.slang Flip the RWStructuredBuffer case from a pinned rejection to an accepted use.
tests/diagnostics/invalid-constant-pointer-taking.slang Drop the RWStructuredBuffer E31160 expectation; keep the read-only StructuredBuffer negative.
tests/metal/pointer-in-buffer-getaddress.slang Remove the now-stale comment (and its incorrect link to #10841, a distinct closed Metal runtime bug).

Concepts and vocabulary

  • getValidTypeForAddressOf — the AST-level check that decides whether an __getAddress/AddressOfExpr target is an addressable memory location and, if so, what pointer type it yields.
  • Ref accessor / kIROp_RWStructuredBufferGetElementPtr — a ref subscript accessor lowers to an intrinsic op; a mutable structured buffer's subscript uses this op to form an element pointer. Read-only StructuredBuffer has only a get accessor (no ref), so it never enters this branch.
  • AddressSpace.Device / UserPointer vs StorageBuffer — the Slang-surface Device address space maps to the internal AddressSpace::UserPointer, emitted as SPIR-V PhysicalStorageBuffer (a physical pointer: a raw device address). A descriptor-backed structured-buffer element pointer is instead a logical StorageBuffer pointer (an access chain into a bound resource). The two are not inter-convertible; a value cannot be both.
  • Pointer slot vs pointer value — a Var/DebugVar of type Ptr<Ptr<T, AS>, Function> is the slot (a Function-storage local holding a pointer); the Ptr<T, AS> it contains is the value. Part 2 rewrites only the contained value-type, never the outer Function slot.
  • address-space specialization passslang-ir-specialize-address-space.cpp, run during SPIR-V legalization, resolves under-specified (Generic) pointer address spaces by dataflow before emission.
  • DebugVar — the IR node that carries a source variable for -g debug info; it mirrors a real Var and, on SPIR-V, can emit a backing OpVariable whose stored value must have a matching storage class.

Process report

Part 1 — the whitelist extension (slang-check-expr.cpp)

The input shape handled here — an InvokeExpr of a ref __subscript whose intrinsic op is kIROp_RWStructuredBufferGetElementPtr — is the canonical, intended representation of buf[i] for a mutable structured buffer (produced by the subscript→invoke rewrite against hlsl.meta.slang's ref accessor), not an accidental spelling. So this is the correct layer: it is the same node the existing kIROp_GetOffsetPtr case already handles, and the producer is correct. That intrinsic op is attached to a subscript ref accessor by exactly one place — the kMutableStructuredBufferCases template — which generates it for RWStructuredBuffer and RasterizerOrderedStructuredBuffer and nothing else (read-only StructuredBuffer has a get-only subscript, so it never reaches this branch). Reaching the branch therefore already implies one of the two mutable types; rather than silently rejecting a shape that cannot occur, the code SLANG_RELEASE_ASSERTs that invariant (after unwrapModifiedType, so a modified form like globallycoherent RWStructuredBuffer is handled), per the codebase's "assert impossible shapes rather than silently return a default" rule — a future accessor reusing this op then surfaces instead of producing a spurious E31160. Both mutable types are enabled deliberately (see below).

Why the returned layout is DefaultDataLayout, not the buffer's L. The binding contract (issue + #10280) is that __getAddress(buf[i]) must be identical to &buf[i]. Measured against the current compiler, &buf430[0] on a RWStructuredBuffer<int, Std430DataLayout> produces Ptr<int, ReadWrite, Device, DefaultDataLayout>& does not preserve the buffer's L; it defaults it. Producing the same type is required by the "one canonical representation per value" rule: if this fix instead preserved L, then int* p = __getAddress(buf430[0]) would be a type mismatch (Ptr<…, Std430> vs int* = Ptr<…, Default>) and would fail to compile while &buf430[0] compiles — a regression the fix would introduce, and a divergence from the very equivalence it is meant to restore. (Preserving a generic L would additionally hit a SLANG_RELEASE_ASSERT in ASTBuilder::getPtrType, which reconstructs the IBufferDataLayout witness by walking the layout decl's constraints and cannot handle a generic type parameter.) Element stride is unaffected: the offset is computed from the buffer type's own L at IR generation via the RWStructuredBufferGetElementPtr op, independent of the AST pointer's layout annotation.

RasterizerOrderedStructuredBuffer decision. Enabled deliberately, not by accident: &raster[i] already compiles today, and the equivalence contract makes __getAddress(raster[i]) follow. It is compile-checked in get-address-buffer-element-accepted.slang and runtime-checked on Vulkan in get-address-buffer-element-ordered.slang (ROV is not accepted by nvrtc/CPU, so that runtime test is Vulkan-only).

Part 2 — address-space specialization (slang-ir-specialize-address-space.cpp, .h, slang-ir-spirv-legalize.cpp)

The bug, concretely. Consider:

RWStructuredBuffer<int> dataBuffer;
RWStructuredBuffer<int> outputBuffer;
[numthreads(1,1,1)]
void computeMain()
{
    int* p = __getAddress(dataBuffer[0]);   // value: Ptr<int, StorageBuffer>
    outputBuffer[0] = *p;
}

After Part 1, the checker types p as int* = Ptr<int, Device>, so lowering builds a slot Var : Ptr<Ptr<int, Device>, Function> and a Store(slot, RWStructuredBufferGetElementPtr(...)). The stored value's real type is Ptr<int, StorageBuffer>. With mem2reg the slot vanishes and only the value survives, so -O0-off builds validated by luck. But under -O0 the slot survives, and under -g2 an IRDebugVar backing variable is materialized; the SPIR-V emitter then emits OpStore %slot %value where %slot's pointee is PhysicalStorageBuffer-classed and %value is StorageBuffer-classed → OpStore Pointer type does not match Object type.

The fix and its code trace. reconcilePointerSlots() (called as a pre-pass at the top of processModule, before the existing fixpoint loop) iterates to a fixpoint over every Var/DebugVar, calling reconcilePointerSlotWithStoredValue(slot, reconciledLoads):

  1. It walks the slot's uses, and for each Store whose getPtr() is the slot (and each DebugValue whose getDebugVar() is the slot) it reads the stored pointer value's concrete address space via getStoredValueAddrSpace, and joins them: the first concrete space wins; a second, different concrete space sets a conflict flag.
  2. getStoredValueAddrSpace is the key to order-independence. For a load of another local pointer slot it returns that source slot's reconciled space (from a reconciledSlotAddrSpace map, Generic until the source slot is reconciled) — never the load's stale surface-default pointee. For an address-computation op derived from another pointer (GetOffsetPtr/GetElementPtr/FieldAddress) it resolves through to the base operand, because the derived inst's own type is not retyped until propagation runs after this pre-pass. A pointer-to-pointer BitCast is deliberately not resolved through: it reinterprets its operand, so its declared result type is authoritative. An integer-to-pointer cast is a different op (kIROp_CastIntToPtr, e.g. the conflict test's (int*)0x1000ull, which emits OpConvertUToPtr) — it is not in the resolve-through set either, so it uses its own declared physical Device type and is still detected as a conflict against a StorageBuffer write. Any other value uses its own resolved pointer-type space.
  3. On conflict, it emits Diagnostics::InconsistentPointerAddressSpace (gated to the real Var, not the mirrored DebugVar, and de-duplicated via a diagnosedAddrSpaceConflicts set so the fixpoint reports each once) and returns without rewriting — the program is genuinely ill-formed and a debug slot must never be the thing that rejects it.
  4. Otherwise, if the joined space is concrete, it records the slot's space in reconciledSlotAddrSpace (unblocking any slot fed from this one), and if that space differs from the slot's declared contained space it rebuilds the contained pointer type with the new space and re-types the slot via getPtrTypeWithAddressSpaceonly the contained value-type changes; the outer Function storage class is preserved. It then re-types every Load of the slot to the new contained type and records those loads in reconciledLoads.

Because a slot fed from another slot only resolves after that source slot has, the slot scan is an inner while (changed) fixpoint; the collected loads then seed propagateAddressSpaceFromInsts — the pass's existing propagation worklist — so the specialized StorageBuffer space also flows to the loads' derived users (GetOffsetPtr/GetElementPtr/FieldAddress such as the p[2] in the test, and phi/block parameters), which were lowered with the stale Device type and are otherwise pinned to it by the addrSpaceFromType != Generic early-out in processFunction. The whole thing is an outer fixpoint: propagation may retype a derived op or block parameter that feeds yet another slot (e.g. a pointer flowing through a block parameter and stored back into a slot), so reconciliation re-runs until neither step makes progress. Both steps only move address spaces toward a concrete value — each slot refines a bounded number of times, each inst is retyped once — so the loop terminates.

Concrete cascade the inner fixpoint handles. Consider int* s = &buf[0]; int* alt = &buf[9]; if (c) s = alt; int* d = s + 1; *d; compiled with -g2. Here the conditional reassignment keeps s as a surviving slot, and d's debug value is GetOffsetPtr(Load(s_slot), 1). Within the inner slot fixpoint, once s reconciles to StorageBuffer (its writes are descriptor element pointers) and its loads are retyped, then when d's slot is scanned — in the same pass if s was visited first, otherwise on the next inner pass — getStoredValueAddrSpace resolves the stored GetOffsetPtr through its base Load(s_slot) to s's recorded StorageBuffer (via reconciledSlotAddrSpace) — no propagation round is needed, because the resolution reads the source slot's recorded space, not the derived inst's type. So d (and its DebugVar) specialize in the same fixpoint. Reading the GetOffsetPtr's own type instead — as a single ordered scan would — leaves d's DebugVar physical while the value is StorageBuffer, and -g2 fails validation with OpStore ... type does not match Object. This exact shape is the derived-pointer regression in get-address-buffer-element-debug.slang. The outer reconcile↔propagate fixpoint is instead what covers a value whose type must first be changed by propagation before it can feed a slot — e.g. a live block parameter retyped by propagateAddressSpaceFromInsts and then stored into a further slot, on the pass's pre-phi-elimination callers.

Why a pre-pass, and why this layer. The address-space specialization pass is precisely the phase that resolves under-specified pointer address spaces before SPIR-V emission; the descriptor-backed StorageBuffer pointer is the canonical value and the slot's Device pointee is the accidental artifact of Ptr<T>'s surface default. So the producer of the "wrong shape" is the surface-type default, and the right place to reconcile it is this pass — not a guard in the emitter (which the codebase philosophy explicitly rejects: keep emission simple, do transforms in IR).

The conflict diagnostic (input-shape check). A single pointer slot holding two different concrete address spaces is genuinely out-of-contract input — a SPIR-V pointer has exactly one storage class and there is no conversion between logical StorageBuffer and physical PhysicalStorageBuffer — so the correct response is to fail loudly, not to silently pick one arm. The slot pre-pass (reconcilePointerSlotWithStoredValue) reports inconsistent-pointer-address-space when a Var is fed two different concrete spaces; tests/diagnostics/pointer-address-space-conflict.slang covers it at both optimization levels (non-exhaustive mode, because the diagnostic is raised during SPIR-V legalization after source locations have been dropped, so it carries no line to annotate — precedent: custom-differential-type-error.slang).

The pre-existing phi-argument conflict TODO in processFunction is addressed by explaining why it is unreachable here rather than adding a second diagnosis: SPIRVLegalizationContext::processParam (slang-ir-spirv-legalize.cpp) already diagnoses a pointer phi that merges two concrete address spaces, and it runs while phis still exist; the direct-SPIR-V pipeline then runs eliminatePhis before this pass, so a conflicting phi never reaches specializeAddressSpace on the only path that carries a sink. resolvePhiAddrSpace therefore only joins phi arguments (to map the phi's address space across the fixpoint) and deliberately does not diagnose — doing so would be dead on every path and would double-report with processParam. Relatedly, getStoredValueAddrSpace treats an unresolved pointer kIROp_Param — whether a phi (non-entry block parameter) or a function parameter (entry-block parameter) — as Generic. Neither has a settled address space at this pre-pass: a phi is resolved later by the propagation fixpoint, and a function parameter is rewritten by specializeFunc from its actual argument's address space when callers are specialized, which runs after this pre-pass. Its declared surface pointee (int*Device) is therefore only provisional. Reading it would wrongly reject valid code: a [noinline] helper whose int* parameter is passed a descriptor-backed element pointer is specialized to StorageBuffer, so merging it with another element pointer is consistent — get-address-buffer-element-param-merge.slang pins that this compiles. So the pre-pass lets a slot's concrete writes drive reconciliation and leaves parameters to their own resolving mechanisms.

The whole pre-pass (and its conflict diagnostic) is gated on the presence of a DiagnosticSink, which only the SPIR-V legalizer supplies. The bug is SPIR-V-specific (logical StorageBuffer vs physical PhysicalStorageBuffer); running the slot retyping on the Metal/WGSL/GLSL callers — which pass no sink — would silently change their slot types with no diagnostic on a genuine conflict and no test coverage, so those backends are intentionally left unchanged by this PR.

Scope boundaries — pre-existing limitations, deliberately out of scope. Three related shapes are not addressed here; each is pre-existing, affects &buf[i] identically, and is distinct from the address-space repair:

What is covered and tested here is the default direct-SPIR-V StorageBuffer path for scalar/consistent-layout pointees — direct, derived (s + 1), control-flow re-assignment, surviving slot, -O0/-g. One thing is handled by the code but not directly exercised by these tests: the pre-SPIR-V-1.4 Uniform structured-buffer storage class is not tested here simply because no Uniform-specific test was added; the reconciliation itself is driven by the stored value's actual address space, so it is not hardcoded to StorageBuffer and should handle Uniform identically. (The resolvePhiAddrSpace join runs for the pass's pre-phi-elimination callers but is not the conflict-diagnosis path — see above.)

Tests. get-address-buffer-element-debug.slang runs the CI-failing shape at -O0 (slot survives) and -g2 -O0 (debug backing var materializes): the __getAddress and & spellings with a control-flow re-assignment and a derived p[2], plus the derived-off-a-surviving-slot cascade (s/d, using &) and a control-flow-merged pointer stored into a further slot. Both runs assert CHECK-NOT: PhysicalStorageBuffer (with CHECKDBG-NOT: on the -g2 run). Asserting the logical StorageBuffer pointer is merely present is not discriminating — the element value is always a StorageBuffer pointer, so it appears in the buggy output too; the bug is that the slot's contained pointee stays physical (_ptr_Function__ptr_PhysicalStorageBuffer_int). This file takes no integer-to-pointer cast, so a correct compile emits no physical pointer at all, and the buggy slot type trips the CHECK-NOT. (Verified: with the reconcile pre-pass disabled, this test fails on exactly that slot type. slang-test removes -g embedded source for SPIR-V targets before FileCheck, so the CHECK-NOT does not self-match the source comment.) The conflict path is covered by pointer-address-space-conflict.slang at both optimization levels (a physical pointer from an integer cast, merged with a StorageBuffer element). get-address-buffer-element-param-merge.slang guards the opposite failure: a buffer-element pointer flowing through a [noinline] helper's int* parameter and merged with another element pointer must not be falsely flagged as a conflict. It runs in non-debug builds (default and -O0), where the parameter is specialized to StorageBuffer and the merge is consistent; it deliberately omits -g, which would hit the pre-existing specialized-parameter debug-var gap noted above (deferred to #13039).

Part 1 tests (unchanged from the earlier revision)

The primary regression (get-address-buffer-element.slang) exercises read/arithmetic/write on both -cpu and -vk. get-address-buffer-element-runtime.slang adds -cpu/-vk runtime coverage for a member-of-element address (read + write-back) and cross-element pointer arithmetic on a struct-element buffer, so a wrong field offset or element stride is executed, not just compiled. The non-default-layout stride question is answered statically by tests/spirv/get-address-buffer-element-layout.slang, which pins ArrayStride 16 on a Std140 struct-element pointer (identical to &buf[i]); a runtime Std140 test is not possible because an explicit-layout structured buffer is not runtime-executable in the harness even for a plain read (SPIR-V returns an empty readback; CPU/CUDA reject it with E36107) — a limitation of the buffer type, independent of __getAddress. The five added pointer tests cover deref forms, arithmetic, Ptr<T>/T* equivalence, ImmutablePtr, and subscript on -cpu and -vk, except pointer-immutable.slang, which is -cpu-only because converting to ImmutablePtr and passing it to a function currently mis-lowers to SPIR-V (invalid OpBitcast on a logical pointer) for both this and &buf[i] — a pre-existing codegen limitation unrelated to this change. The two negative tests are flipped for the mutable case while keeping the read-only StructuredBuffer rejection pinned.

Known limitation from Part 1, deliberately out of scope. A custom-layout element address that escapes through a plain T* (default-layout) function parameter can lose its non-default stride. This affects &buf[i] identically today (its declared return type is default-layout Ptr<T>) — it is a pre-existing property of both spellings, not introduced here. Fixing it would require changing operator&'s declared return type in core.meta.slang, a semantics change for every existing &buf[i] user, which is well beyond lifting an E31160 rejection.

Swizzle of a buffer element (investigated — no new risk, no silent miscompile). Lifting E31160 does not widen the addressable surface beyond &. A single-component swizzle — __getAddress(buf[i].x) on a RWStructuredBuffer<float4> — is accepted and lowers to the byte-identical logical StorageBuffer OpAccessChain that &buf[i].x produces (verified for .x and .y: same access chain, same storage class). A single component is a genuine contiguous addressable sub-location, so this is correct. A multi-component or reordered swizzle (.xy, .yx, .xz) is rejected for both spellings__getAddress with E31160 and & with E40008 — so a non-contiguous or reordered swizzle can never silently become a pointer. The accepted swizzle surface is thus exactly &'s, consistent with the __getAddress(x) ≡ &x contract.

Closes #12581.

🤖 Generated by an automated Slang coworker — may be inaccurate. A human maintainer should verify.

@jhelferty-nv

Copy link
Copy Markdown
Contributor

Automated notice (PR board sync) — do not reply to this comment.

Auto-assigned @jvepsalainen-nv as shepherd for this Bot PR.

FYI for maintainers: committer signal on the changed files is higher for jkwak-work than for the auto-requested reviewer (jvepsalainen-nv). They were not auto-requested; a human may optionally add them as a reviewer.

@jhelferty-nv

Copy link
Copy Markdown
Contributor

Automated notice (PR board sync) — do not reply to this comment.

Auto-assigned @jvepsalainen-nv as shepherd for this Bot PR.

FYI for maintainers: committer signal on the changed files is higher for jkwak-work than for the auto-requested reviewer (jvepsalainen-nv). They were not auto-requested; a human may optionally add them as a reviewer.

A subscript into an RWStructuredBuffer/RasterizerOrderedStructuredBuffer is a
device-memory lvalue, and &buf[i] already compiles, but __getAddress(buf[i]) was
rejected with E31160. The AST address-of whitelist in getValidTypeForAddressOf
only accepted a ref accessor with kIROp_GetOffsetPtr; extend it to also accept
kIROp_RWStructuredBufferGetElementPtr when the base is a mutable structured
buffer, returning Ptr<T, ReadWrite, Device, DefaultDataLayout> exactly as &buf[i]
does. Read-only StructuredBuffer has no ref accessor and stays rejected.
@jvepsalainen-nv
jvepsalainen-nv marked this pull request as ready for review August 18, 2026 13:22
@jvepsalainen-nv
jvepsalainen-nv requested a review from a team as a code owner August 18, 2026 13:22
@jvepsalainen-nv
jvepsalainen-nv requested review from bmillsNV and removed request for a team August 18, 2026 13:22
@jvepsalainen-nv

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f400bc78-abfd-4239-8140-6558d8af3cc0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The address-of checker now accepts mutable structured-buffer and rasterizer-ordered buffer elements. New tests cover compilation, SPIR-V layout, runtime behavior, pointer arithmetic, dereference, subscripting, conversions, and immutable pointers.

Changes

Structured-buffer element address support

Layer / File(s) Summary
Address validation and diagnostics
source/slang/slang-check-expr.cpp, tests/diagnostics/invalid-constant-pointer-taking.slang, tests/language-feature/pointer/get-address-validation.slang, tests/metal/pointer-in-buffer-getaddress.slang
Address validation recognizes mutable structured-buffer element pointers and preserves rejection for unsupported accessors. Related diagnostics and comments are updated.
Address formation and layout checks
tests/language-feature/pointer/get-address-buffer-element-accepted.slang, tests/spirv/get-address-buffer-element-layout.slang
Compilation tests cover mutable and rasterizer-ordered buffers, struct members, and Std140/Std430 layouts. SPIR-V checks validate emission and storage-buffer stride.
Buffer pointer runtime behavior
tests/language-feature/pointer/get-address-buffer-element.slang, tests/language-feature/pointer/get-address-buffer-element-runtime.slang, tests/language-feature/pointer/get-address-buffer-element-ordered.slang
Runtime tests cover pointer reads, writes, member access, element-stride arithmetic, and rasterizer-ordered buffer access.
Pointer operation coverage
tests/language-feature/types/pointer-arithmetic.slang, tests/language-feature/types/pointer-subscript.slang, tests/language-feature/types/pointer-basic-operations.slang, tests/language-feature/types/pointer-generic-syntax.slang, tests/language-feature/types/pointer-immutable.slang
Tests cover pointer arithmetic, subscripting, dereference and member access, native and generic pointer conversions, and ImmutablePtr conversion.

Suggested reviewers: bmillsnv, jvepsalainen-nv

Merge Risk: 🔵 Low · up to 7bb0d

The change is localized and adds coverage for mutable structured-buffer element addresses. It is mergeable with owner awareness that the accessor invariant should use a release assertion; otherwise, a future unsupported accessor reuse could produce an incorrect pointer type in release builds.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: allowing __getAddress on mutable structured-buffer elements.
Description check ✅ Passed The description directly explains the motivation, implementation, scope, limitations, and tests for the structured-buffer address change.
Linked Issues check ✅ Passed The changes satisfy issue #12581 by enabling address formation for mutable structured-buffer elements while preserving read-only rejection.
Out of Scope Changes check ✅ Passed The implementation, diagnostics, tests, and stale-comment cleanup are all related to the linked issue objectives.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
✨ 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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b606d0ce-cc15-44f8-b42f-9f6939ba1b75

📥 Commits

Reviewing files that changed from the base of the PR and between 0519d9b and 7bb0d5c.

📒 Files selected for processing (14)
  • source/slang/slang-check-expr.cpp
  • tests/diagnostics/invalid-constant-pointer-taking.slang
  • tests/language-feature/pointer/get-address-buffer-element-accepted.slang
  • tests/language-feature/pointer/get-address-buffer-element-ordered.slang
  • tests/language-feature/pointer/get-address-buffer-element-runtime.slang
  • tests/language-feature/pointer/get-address-buffer-element.slang
  • tests/language-feature/pointer/get-address-validation.slang
  • tests/language-feature/types/pointer-arithmetic.slang
  • tests/language-feature/types/pointer-basic-operations.slang
  • tests/language-feature/types/pointer-generic-syntax.slang
  • tests/language-feature/types/pointer-immutable.slang
  • tests/language-feature/types/pointer-subscript.slang
  • tests/metal/pointer-in-buffer-getaddress.slang
  • tests/spirv/get-address-buffer-element-layout.slang
💤 Files with no reviewable changes (1)
  • tests/metal/pointer-in-buffer-getaddress.slang

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

Comment on lines +7414 to +7417
auto baseType = unwrapModifiedType(functionMemberExpr->baseExpression->type.type);
SLANG_ASSERT(
as<HLSLRWStructuredBufferType>(baseType) ||
as<HLSLRasterizerOrderedStructuredBufferType>(baseType));

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a release assertion for the accessor-type invariant.

Lines 7415-7417 describe an out-of-contract accessor reuse. SLANG_ASSERT is disabled in release builds. A future reuse of kIROp_RWStructuredBufferGetElementPtr could then return a read-write user pointer for an unsupported buffer type. Replace it with SLANG_RELEASE_ASSERT.

As per coding guidelines, use SLANG_RELEASE_ASSERT for out-of-contract input.

Source: Coding guidelines

Comment on lines +1 to +6
// Regression test for shader-slang/slang#12581: `__getAddress(buf[i])` on a mutable
// structured buffer must be accepted (the element is a device-memory lvalue, and
// `&buf[i]` already compiled). Exercises reading, arithmetic, and writing through it.

//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK):-cpu -shaderobj -output-using-type
//TEST(compute):COMPARE_COMPUTE(filecheck-buffer=CHECK):-vk -emit-spirv-directly -output-using-type

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Put the //TEST directive first in each test file.

The coding guidelines require leading test directives. Move each //TEST directive above the narrative comments.

  • tests/language-feature/pointer/get-address-buffer-element.slang#L1-L6: Move the directives on lines 5-6 to the start of the file.
  • tests/language-feature/pointer/get-address-buffer-element-runtime.slang#L1-L9: Move the directives on lines 8-9 to the start of the file.
  • tests/language-feature/pointer/get-address-buffer-element-ordered.slang#L1-L7: Move the directive on line 7 to the start of the file.
  • tests/language-feature/types/pointer-arithmetic.slang#L1-L5: Move the directives on lines 4-5 to the start of the file.
  • tests/language-feature/types/pointer-subscript.slang#L1-L5: Move the directives on lines 4-5 to the start of the file.
  • tests/language-feature/types/pointer-basic-operations.slang#L1-L5: Move the directives on lines 4-5 to the start of the file.
  • tests/language-feature/types/pointer-generic-syntax.slang#L1-L5: Move the directives on lines 4-5 to the start of the file.
  • tests/language-feature/types/pointer-immutable.slang#L1-L6: Move the directive on line 6 to the start of the file.
📍 Affects 8 files
  • tests/language-feature/pointer/get-address-buffer-element.slang#L1-L6 (this comment)
  • tests/language-feature/pointer/get-address-buffer-element-runtime.slang#L1-L9
  • tests/language-feature/pointer/get-address-buffer-element-ordered.slang#L1-L7
  • tests/language-feature/types/pointer-arithmetic.slang#L1-L5
  • tests/language-feature/types/pointer-subscript.slang#L1-L5
  • tests/language-feature/types/pointer-basic-operations.slang#L1-L5
  • tests/language-feature/types/pointer-generic-syntax.slang#L1-L5
  • tests/language-feature/types/pointer-immutable.slang#L1-L6

Source: Coding guidelines

@jvepsalainen-nv jvepsalainen-nv 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.

LGTM

@nv-slang-bot

nv-slang-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

[Supervisor] This chain's session hit its cost ceiling and is paused pending a human cost decision (dashboard → Continue/Stop). No action needed here — it resumes automatically once decided.

@jvepsalainen-nv
jvepsalainen-nv dismissed their stale review September 1, 2026 06:38

Need to fix the CI failures

@jvepsalainen-nv

Copy link
Copy Markdown
Contributor

[agent] CI is exposing a real SPIR-V lowering/address-space bug here, not a flaky GPU failure.

The checker now gives __getAddress(buf[i]) the surface type Ptr<T, ReadWrite, Device>. Internally, Device is UserPointer, which maps to a SPIR-V PhysicalStorageBuffer pointer. However, kIROp_RWStructuredBufferGetElementPtr produces a descriptor-backed pointer that SPIR-V legalization assigns to the logical StorageBuffer storage class. validateAndRemoveAssumeAddress removes the AssumeAddress wrapper without reconciling these types.

As a result, code such as:

int* ptr = __getAddress(dataBuffer[0]);

tries to store a StorageBuffer pointer into a pointer variable whose value type is PhysicalStorageBuffer. SPIR-V validation reports:

OpStore Pointer ... type does not match Object ... type

The seven new Vulkan pointer tests reproduce this consistently on Linux debug/release and Windows. The later “malformed HTTP or JSON response” messages are secondary: the invalid shader is rejected and the test server fails to return a valid response; the harness reproduces it on a fresh server and attributes it to the input. The CPU-only job passes because this address-space distinction does not affect the CPU lowering path.

This means the checker-only approach is insufficient, and the claim that no lowering/IR work is needed does not hold. The fix needs to make the AST pointer type, RWStructuredBufferGetElementPtr, first-class pointer storage/operations, and SPIR-V legalization agree on the address space—likely by preserving/specializing a logical StorageBuffer pointer through these uses. A simple bitcast between logical StorageBuffer and physical Device pointers would not be valid SPIR-V.

Separately, the Falcor check is unrelated infrastructure noise: its external-CI trigger receives HTTP 403. check-ci is just aggregating that failure with the substantive slang-test failures.

@nv-slang-bot

nv-slang-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed — thanks for the precise diagnosis; it holds on every point. I reproduced the exact OpStore … %_dbgvar_ptr … type does not match Object … failure locally with -emit-spirv-directly -g2 and SLANG_RUN_SPIRV_VALIDATION=1 (no GPU needed). The -g is the key: without debug info mem2reg folds the local away and only the self-consistent StorageBuffer value survives, which is why the earlier "-cpu passes / -vk passed locally" signal was misleading.

To pin down where the disagreement actually lives:

  • The checker's Ptr<T, Device> (= UserPointerPhysicalStorageBuffer) type is in fact correct for the surface — it must match the user's int* and the existing &buf[i] (operator& returns the same). I verified that forcing the checker to Generic instead just moves the break to type-checking (int* ptr = … → "Generic not convertible to Device"). So the AST type is not the bug.
  • The value produced by RWStructuredBufferGetElementPtr is (correctly) a logical StorageBuffer pointer, and SPIR-V legalization is authoritative there. validateAndRemoveAssumeAddress is a pure pass-through and reconciles nothing.
  • The only thing that stays physical is the slot that stores the pointer: the int* ptr local (and, under -g, its debug backing variable) has pointee Ptr<int, Device>, so the init-store is StorageBuffer value → PhysicalStorageBuffer slot.

Approach (no logical↔physical bitcast): reconcile in the address-space specialization pass — a local pointer slot / debug variable adopts the address space of the pointer value stored into it (specialize the pointer through its uses), rewriting the slot's contained pointer value-type Device → StorageBuffer while leaving the outer Function storage class unchanged, joined across stores, with InconsistentPointerAddressSpace on a genuine conflict. RWStructuredBufferGetElementPtr stays the single authority for the class. For the SPIR-V ≥1.4 case the tests exercise, no emitter change is required: once the debug var's pointee is StorageBuffer, isAllowedDebugVarType already declines to emit a backing OpVariable, so the spurious OpStore disappears while the DebugValue association is preserved.

I'll push the update and re-run CI. (Agreed the Falcor 403 is unrelated infra noise.)

🤖 Generated by an automated Slang coworker — may be inaccurate. A human maintainer should verify.

@jvepsalainen-nv

Copy link
Copy Markdown
Contributor

Thanks — the proposed address-space-specialization direction is the right one.

I checked the SPIR-V debug emitter as well. Once the pointer value held by the DebugVar is specialized from UserPointer to the resolved logical StorageBuffer address space, isAllowedDebugVarType will decline to create the incompatible backing OpVariable; the NonSemantic DebugValue can remain. That removes the invalid OpStore without inventing a logical-to-physical pointer conversion.

Please make the change a general pointer-value dataflow fix rather than a DebugVar/RWStructuredBufferGetElementPtr special case:

  • Infer the contained pointer value's address space for pointer-holding Var/DebugVar nodes from their store/DebugValue inputs, while leaving the outer slot in AddressSpace::Function.
  • Propagate the specialized contained type through loads, phi/block parameters, calls, returns, and derived pointer operations. The resolved producer (RWStructuredBufferGetElementPtr here) should remain the source of truth.
  • Join all assignments to a slot. If concrete address spaces disagree, diagnose InconsistentPointerAddressSpace; do not silently choose one. In particular, please cover the existing phi-conflict TODO in the specialization pass or explain why it cannot be reached by this path.
  • Do not add a logical/physical bitcast or handle only the debug backing store: the same representation mismatch can surface when a pointer value survives in an ordinary local slot or crosses control flow.

Suggested regression coverage:

  1. Validate direct SPIR-V with full debug info and -O0 for both __getAddress(buf[i]) and the pre-existing &buf[i] form.
  2. Exercise reassignment/control flow, plus passing and returning the pointer, so the fix is proven beyond the current SSA-shaped examples.
  3. Cover multiple consistent StorageBuffer inputs and one genuinely conflicting-address-space case.
  4. If the pre-SPIR-V-1.4 Uniform structured-buffer path is supported by the direct backend, cover that specialization too.

After the update, please run the seven currently failing Vulkan variants locally with SPIR-V validation before restarting full CI. The PR is also behind master, so rebase/update it before the final run. If the shared specialization work becomes substantial, I suggest landing it as a prerequisite bug-fix PR and then rebasing this feature PR on top.

…e in local slots

`__getAddress(buf[i])` / `&buf[i]` on a mutable structured buffer has the surface type
`Ptr<T, Device>` (matching the user's `int*`), but `RWStructuredBufferGetElementPtr` produces a
descriptor-backed, logical `StorageBuffer` pointer. A local pointer slot that survives (a `-O0`
local, or a `-g` debug backing variable) kept the physical `Device` pointee even though the value
stored into it is a `StorageBuffer` pointer, so the store's pointer and object storage classes
disagreed and SPIR-V validation failed with "OpStore Pointer type does not match Object type".

Reconcile in the address-space specialization pass: a local pointer slot (`Var`/`DebugVar`) adopts
the address space of the pointer values written into it, rewriting only the slot's contained
pointer value-type (the outer `Function` storage class is unchanged) and propagating the
specialized type through the slot's loads and their derived pointer ops. Writes are joined; a
genuine conflict between two concrete address spaces is diagnosed as
`InconsistentPointerAddressSpace` (also covering the pre-existing phi-argument TODO) rather than
silently coerced. No logical<->physical bitcast is introduced.

Also promotes the structured-buffer address-of type-gate assert in the checker to
`SLANG_RELEASE_ASSERT`.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ jvepsalainen-nv
❌ nv-slang-bot
You have signed the CLA already but the status is still pending? Let us recheck it.

Reconcile local pointer slots to a fixpoint instead of a single ordered scan, so a slot
fed from another slot resolves to that source slot's reconciled address space rather than
its stale surface-default pointee, independent of the order slots are visited. A new
`getStoredValueAddrSpace` resolves a stored value that is a load of a local pointer slot to
the slot's reconciled space (`Generic` until reconciled), and resolves an address-
computation op (`GetOffsetPtr`/`GetElementPtr`/`FieldAddress`) through to its base -- the
derived inst's own type is not retyped until `propagateAddressSpaceFromInsts` runs after this
pre-pass. Without this, e.g. `s + 1` with `s` a surviving slot is classified from a stale
`Device` type and leaves the destination slot -- most visibly a `-g` `DebugVar` -- with a
physical pointee while the value stored is a StorageBuffer pointer, failing SPIR-V validation
with `OpStore ... type does not match Object`. A pointer bitcast is deliberately not resolved
through: it reinterprets its operand and its declared result type is authoritative (an
int-to-pointer physical pointer must stay physical).

Interleave slot reconciliation and `propagateAddressSpaceFromInsts` in an outer fixpoint:
reconciling slots retypes their loads; propagating those loads may retype the derived ops and
block parameters that feed *other* slots, which are then reconciled (e.g. a pointer flowing
through a block parameter and stored back into a slot). Both steps only move address spaces
toward concrete -- each slot refines a bounded number of times, each inst is retyped once --
so the combined loop terminates.

Fold the phi address-space join and conflict check into one `resolvePhiAddrSpace` helper
called when a block parameter is first resolved and when it is revisited; on revisit, adopt a
newly-consistent concrete join so a stale mapping is corrected, and diagnose each conflicting
slot or block parameter exactly once. (The direct-SPIR-V pipeline eliminates phis before this
pass; the phi path serves the pass's other, pre-phi-elimination callers.)

Extend the tests to cover a pointer derived from a surviving slot, a control-flow-merged
pointer stored into a further slot, and a `-O0` run of the conflict case alongside the
default-optimization run.
… the reconcile pre-pass, tighten comments

Review round 2 on #12592:

- get-address-buffer-element-debug.slang: the positive `OpTypePointer StorageBuffer %int`
  checks passed with or without the fix (the element value is always a StorageBuffer pointer;
  the bug is the *slot's* contained type staying physical). Assert the physical storage class
  is absent instead (`CHECK-NOT: PhysicalStorageBuffer` / `CHECKDBG-NOT:`); this file takes no
  integer-to-pointer cast, so a correct compile emits no physical pointer, and the buggy
  `_ptr_Function__ptr_PhysicalStorageBuffer_int` slot type trips it. Fix the inaccurate comment
  claiming `-g` embedded source would self-match: slang-test removes embedded source for SPIR-V
  targets before FileCheck.

- Scope the reconcile pre-pass to the SPIR-V (sink-carrying) path. It ran unconditionally for
  all four `specializeAddressSpace` callers, silently changing Metal/WGSL/GLSL slot types with
  no conflict diagnostic (no sink) and no test coverage. Gate it on `sink`, leaving those
  backends unchanged, and document the new `sink` parameter in the header.

- getStoredValueAddrSpace: treat a not-yet-resolved `kIROp_Param` like a load of an
  unreconciled slot (return `Generic`) so a slot fed both a reconciled slot-load and a pointer
  phi does not compute a spurious conflict; convergence no longer depends on phi-elimination
  order.

- resolvePhiAddrSpace: drop the sink-gated phi-conflict diagnose. It was unreachable (phis are
  eliminated before this pass on the only sink-carrying path) and duplicated
  `SPIRVLegalizationContext::processParam`, which owns phi-conflict diagnosis while phis exist.
  Keep the join.

- getValidTypeForAddressOf: assert the two ref-accessor intrinsic-op flags are mutually
  exclusive so the ordered checks read as exhaustive rather than priority-dependent; correct
  the two comments (`__getAddress` is not unconditionally equivalent to `&`; the element is a
  logical StorageBuffer pointer, only typed `Device` at the AST level to match `&buf[i]`).

- Consistency: uint3 SV_DispatchThreadID in the accept test; trim in-source termination proofs
  (they live in the PR process report).
A function parameter carries an authoritative declared address space, so
when reconciling a local pointer slot's contained address space,
getStoredValueAddrSpace must honor a stored function parameter rather than
treat every kIROp_Param as an unresolved phi. Dropping a physical int*
function parameter to Generic let a slot also fed a StorageBuffer element
pointer merge silently, emitting an invalid OpStore. Only a phi (non-entry
block parameter) is treated as unknown until the fixpoint resolves it.

Add a regression test for the function-parameter conflict path, extend the
CHECK-NOT coverage in the debug test, correct the header doc for the slot
diagnostic, and rewrite change-history comments as durable rationale.
A function parameter's contained address space is not settled when the
reconcile pre-pass runs: specializeFunc rewrites pointer parameters from
their actual argument address spaces when callers are specialized, which
runs after this pre-pass, so the declared int*->Device surface pointee is
only provisional. Reading it wrongly rejects valid code -- a [noinline]
helper whose int* parameter is passed a descriptor-backed StorageBuffer
element pointer is specialized to StorageBuffer, so merging it with another
element pointer is consistent. Treat any unresolved parameter (phi or
function parameter) as Generic and let the slot's concrete writes drive
reconciliation; a genuine conflict through a physical parameter surfaces
downstream (post-specialization reconciliation is deferred, #13039).

Replace the function-parameter conflict test with the acceptance test that
pins the valid merge compiles, and correct the reconciled-conflict member
comment (block-parameter conflicts are diagnosed by processParam).
Comment- and test-doc-only. The pre-pass does not reason about a pointer
parameter's address space (it is settled later by call specialization), so
cross-function parameter cases -- a physical-parameter conflict, and a
specialized parameter's -g debug backing variable -- are out of scope and
deferred to #13039. Because SPIR-V validation is opt-in, those cases can
currently emit ill-typed SPIR-V caught only under validation, so drop the
inaccurate 'always a compile error' framing. Note the acceptance test runs
non-debug only and why.
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.

__getAddress on a structured-buffer element (buf[i]) is rejected as a function-local address on all targets

4 participants