# Glslang

> glslang SPIR-V Builder API for types, instructions, control flow, and decorations.

- Skill: `luisagroup/glslang` (Agent Skill)
- Install (CLI): `npx skillmds@latest add luisagroup/glslang`
- Raw SKILL.md: https://api.skillmd.com/api/skills/luisagroup/glslang/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: luisagroup (https://skillmd.com/u/luisagroup)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/luisagroup/glslang

---


# glslang SPIR-V Usage

Located in `src/ext/glslang/SPIRV`. Headers:

```cpp
#include "SPIRV/SpvBuilder.h"
#include "SPIRV/spvIR.h"
#include "SPIRV/GlslangToSpv.h"
#include "SPIRV/disassemble.h"
```

> Code snippets follow glslang's own conventions (e.g. `camelCase` builder methods). LuisaCompute project style rules apply to project code, while `src/ext/glslang` is third-party code.

## SpvBuilder Lifecycle

`spv::Builder` owns one SPIR-V module. Thread-safe internal IR.

```cpp
spv::SpvBuildLogger logger;
spv::Builder builder(spv::Spv_1_5, 0, &logger);
builder.setSource(spv::SourceLanguage::GLSL, 450);
builder.setMemoryModel(spv::AddressingModel::Logical, spv::MemoryModel::GLSL450);
builder.addCapability(spv::Capability::Shader);
// ... build ...
std::vector<unsigned int> spirv;
builder.dump(spirv);
```

## Module Setup

```cpp
builder.setSource(spv::SourceLanguage::GLSL, 450);
builder.setEmitSpirvDebugInfo();                    // required before setting debug locations
builder.setDebugMainSourceFile("shader.frag");
builder.setDebugSourceLocation(10, "shader.frag");
builder.addCapability(spv::Capability::Shader);
builder.addExtension("SPV_KHR_ray_tracing");
builder.setMemoryModel(spv::AddressingModel::Logical, spv::MemoryModel::GLSL450);
spv::Id glsl450 = builder.import("GLSL.std.450");
```

## Types (canonicalized)

```cpp
spv::Id voidTy   = builder.makeVoidType();
spv::Id boolTy   = builder.makeBoolType();
spv::Id int32Ty  = builder.makeIntType(32);
spv::Id uint32Ty = builder.makeUintType(32);
spv::Id uint64Ty = builder.makeUintType(64);
spv::Id floatTy  = builder.makeFloatType(32);
spv::Id doubleTy = builder.makeFloatType(64);
spv::Id halfTy   = builder.makeFloatType(16);
spv::Id bfloat16 = builder.makeBFloat16Type();
spv::Id float8e5 = builder.makeFloatE5M2Type();
spv::Id float8e4 = builder.makeFloatE4M3Type();

spv::Id vec4Ty   = builder.makeVectorType(floatTy, 4);
spv::Id mat4x4Ty = builder.makeMatrixType(floatTy, 4, 4);
spv::Id arrTy    = builder.makeArrayType(floatTy, builder.makeUintConstant(16), 0);
spv::Id runArrTy = builder.makeRuntimeArray(floatTy);

std::vector<spv::Id> members = {floatTy, int32Ty};
// Second argument is member debug info; use {} when no per-member debug data is needed.
spv::Id structTy = builder.makeStructType(members, {}, "MyStruct", false);

spv::Id ptrTy     = builder.makePointer(spv::StorageClass::Function, floatTy);
spv::Id fwdPtrTy  = builder.makeForwardPointer(spv::StorageClass::PhysicalStorageBuffer);
// Resolve a forward pointer to its pointee type once the pointee is known.
spv::Id resolvedPtrTy = builder.makePointerFromForwardPointer(spv::StorageClass::PhysicalStorageBuffer, fwdPtrTy, floatTy);
spv::Id untypedPtr= builder.makeUntypedPointer(spv::StorageClass::StorageBuffer);
spv::Id fnTy      = builder.makeFunctionType(voidTy, {floatTy, int32Ty});

spv::Id imgTy       = builder.makeImageType(floatTy, spv::Dim::Dim2D, false, false, false, 1, spv::ImageFormat::Rgba32f, "texture2D");
spv::Id sampledImgTy= builder.makeSampledImageType(imgTy, "sampler2D");
spv::Id samplerTy   = builder.makeSamplerType("sampler");

spv::Id asTy = builder.makeAccelerationStructureType();
spv::Id rqTy = builder.makeRayQueryType();
spv::Id hoTy = builder.makeHitObjectEXTType();

spv::Id coopMatTy = builder.makeCooperativeMatrixTypeKHR(floatTy, scopeId, rowsId, colsId, useId);
spv::Id coopVecTy = builder.makeCooperativeVectorTypeNV(floatTy, componentsId);
spv::Id tensorTy  = builder.makeTensorTypeARM(floatTy, rankId);

// Generic
std::vector<spv::IdImmediate> ops = {{true, someId}};
spv::Id genericTy = builder.makeGenericType(spv::Op::OpType..., ops);
```

## Type Queries

```cpp
spv::Id typeId = builder.getTypeId(resultId);
spv::Op opCode = builder.getOpCode(id);
spv::Op cls    = builder.getTypeClass(typeId);
bool isPtr     = builder.isPointer(id);
bool isScalar  = builder.isScalar(id);
bool isVec     = builder.isVector(id);
bool isMat     = builder.isMatrix(id);
bool isArray   = builder.isArrayType(typeId);
bool isStruct  = builder.isStructType(typeId);
bool isImage   = builder.isImageType(typeId);
bool isSampler = builder.isSamplerType(typeId);
int  width     = builder.getScalarTypeWidth(typeId);
spv::Id scalar = builder.getScalarTypeId(typeId);
spv::Id contained = builder.getContainedTypeId(typeId);     // single
spv::Id contained = builder.getContainedTypeId(typeId, n);  // nth
unsigned cols = builder.getNumColumns(id);
unsigned rows = builder.getNumRows(id);
unsigned comps= builder.getNumComponents(id);
```

## Constants (deduplicated; spec constants not)

```cpp
spv::Id t = builder.makeBoolConstant(true), f = builder.makeBoolConstant(false);
spv::Id i32 = builder.makeIntConstant(5), u32 = builder.makeUintConstant(7);
spv::Id i64 = builder.makeInt64Constant(9), u64 = builder.makeUint64Constant(11);
spv::Id i8  = builder.makeInt8Constant(1),  u8  = builder.makeUint8Constant(2);
spv::Id i16 = builder.makeInt16Constant(3), u16 = builder.makeUint16Constant(4);
spv::Id f32 = builder.makeFloatConstant(1.0f), f64 = builder.makeDoubleConstant(2.0);
spv::Id f16 = builder.makeFloat16Constant(3.0f), bf16 = builder.makeBFloat16Constant(4.0f);
spv::Id fp  = builder.makeFpConstant(floatTy, 1.5, false);
spv::Id null= builder.makeNullConstant(structTy);

// Composite
spv::Id vec4 = builder.makeCompositeConstant(vec4Ty, {f32, f32, f32, f32});

// Spec constants
spv::Id specI32 = builder.makeIntConstant(builder.makeIntType(32), 10, true);
spv::Id specVec = builder.makeCompositeConstant(vec4Ty, {f32, f32, f32, f32}, true);
```

## Variables

```cpp
spv::Id global = builder.createVariable(spv::Decoration::NoPrecision, spv::StorageClass::Private, floatTy, "g", builder.makeFloatConstant(0.0f));
spv::Id local  = builder.createVariable(spv::Decoration::NoPrecision, spv::StorageClass::Function, floatTy, "l");
spv::Id untyped= builder.createUntypedVariable(spv::Decoration::NoPrecision, spv::StorageClass::StorageBuffer, "u", dataTypeId, initId);
spv::Id undef  = builder.createUndefined(floatTy);
```

## Functions

```cpp
// Entry point
spv::Function* entry = builder.makeEntryPoint("main");
builder.addEntryPoint(spv::ExecutionModel::Fragment, entry, "main");
builder.addExecutionMode(entry, spv::ExecutionMode::OriginUpperLeft);

// Regular function
spv::Block* entryBlock = nullptr;
spv::Function* func = builder.makeFunctionEntry(
    spv::Decoration::NoPrecision, floatTy, "myFunc", spv::LinkageType::Max,
    {floatTy, int32Ty},
    {{spv::Decoration::NoPrecision}, {spv::Decoration::NoPrecision}},
    &entryBlock);

builder.enterFunction(func);
builder.setBuildPoint(entryBlock);
spv::Id p0 = func->getParamId(0);
spv::Id p1 = func->getParamId(1);
builder.makeReturn(false, resultId);  // or makeReturn(false) for void
builder.leaveFunction();
```

## Control Flow

### If-Then-Else
```cpp
spv::Builder::If ifBuilder(cond, spv::SelectionControlMask::MaskNone, builder);
// then block
ifBuilder.makeBeginElse();
// else block
ifBuilder.makeEndIf();
// merge block
```

### Switch
```cpp
std::vector<int> caseValues = {0, 1}, valueToSegment = {0, 1};
int defaultSegment = 2, numSegments = 3;
std::vector<Block*> segmentBB;
builder.makeSwitch(selectorId, spv::SelectionControlMask::MaskNone, numSegments, caseValues, valueToSegment, defaultSegment, segmentBB);
builder.nextSwitchSegment(segmentBB, 0); /* ... */ builder.addSwitchBreak(false);
builder.nextSwitchSegment(segmentBB, 1); /* ... */ builder.addSwitchBreak(false);
builder.nextSwitchSegment(segmentBB, 2); /* ... */ builder.addSwitchBreak(false);
builder.endSwitch(segmentBB);
```

### Loops
```cpp
spv::Builder::LoopBlocks& loop = builder.makeNewLoop();
builder.setBuildPoint(&loop.head);
builder.createLoopMerge(&loop.merge, &loop.continue_target, spv::LoopControlMask::MaskNone, {});
builder.createConditionalBranch(cond, &loop.body, &loop.merge);
builder.setBuildPoint(&loop.body);
// loop body
builder.createLoopContinue();
builder.setBuildPoint(&loop.continue_target);
// loop increment (optional)
builder.createBranch(false, &loop.head);
builder.setBuildPoint(&loop.merge);
builder.closeLoop();
// break: builder.createLoopExit();  continue: builder.createLoopContinue();
```

## Arithmetic & Logic

```cpp
spv::Id neg  = builder.createUnaryOp(spv::Op::OpSNegate, int32Ty, val);
spv::Id notb = builder.createUnaryOp(spv::Op::OpLogicalNot, boolTy, bval);
spv::Id add  = builder.createBinOp(spv::Op::OpFAdd, floatTy, a, b);
spv::Id sub  = builder.createBinOp(spv::Op::OpISub, int32Ty, a, b);
spv::Id mul  = builder.createBinOp(spv::Op::OpIMul, int32Ty, a, b);
spv::Id div  = builder.createBinOp(spv::Op::OpFDiv, floatTy, a, b);
spv::Id and_ = builder.createBinOp(spv::Op::OpBitwiseAnd, uint32Ty, a, b);

// ExtInst (ternary)
spv::Id fma = builder.createOp(spv::Op::OpExtInst, floatTy, {glsl450, GLSLstd450Fma, a, b, c});

// Generic n-ary
spv::Id r = builder.createOp(spv::Op::OpVectorTimesMatrix, vec4Ty, {a, b, c});

// Mixed ID/immediates
std::vector<spv::IdImmediate> mixed = {{true, idOp}, {false, (unsigned)spv::MemoryAccessMask::Aligned}};
spv::Id r = builder.createOp(spv::Op::Op..., typeId, mixed);

// SpecConstantOp
spv::Id specAdd = builder.createSpecConstantOp(spv::Op::OpIAdd, int32Ty, {specA, specB}, {});
```

## Memory Instructions

```cpp
spv::Id loaded = builder.createLoad(ptrId, spv::Decoration::NoPrecision);
builder.createStore(valueId, ptrId);
builder.createStore(valueId, ptrId, spv::MemoryAccessMask::NonUniformPointerEXT, spv::Scope::Device, 4);

// Access chain
std::vector<spv::Id> indexes = {builder.makeUintConstant(0), builder.makeUintConstant(2)};
spv::Id chain = builder.createAccessChain(spv::StorageClass::Function, basePtr, indexes);

// Composite
spv::Id elem  = builder.createCompositeExtract(composite, elemType, 2);
spv::Id elem  = builder.createCompositeExtract(composite, elemType, std::vector<unsigned>{0, 1});
spv::Id ins   = builder.createCompositeInsert(newVal, composite, compositeType, 0);
spv::Id dynEl = builder.createVectorExtractDynamic(vec, elemType, indexId);
spv::Id dynVec= builder.createVectorInsertDynamic(vec, vecType, newElem, indexId);
spv::Id comp  = builder.createCompositeConstruct(vec4Ty, {a, b, c, d});

spv::Id vec4 = builder.createConstructor(spv::Decoration::NoPrecision, {scalarId}, vec4Ty);
spv::Id mat  = builder.createMatrixConstructor(spv::Decoration::NoPrecision, srcs, mat4x4Ty);

// Swizzle
spv::Id swz = builder.createRvalueSwizzle(spv::Decoration::NoPrecision, vec4Ty, vec, {2, 1, 0, 3});
spv::Id lswz= builder.createLvalueSwizzle(vec4Ty, target, source, {2, 1, 0, 3});

// Scalar promotion (in-place)
builder.promoteScalar(spv::Decoration::NoPrecision, left, right);
spv::Id smeared = builder.smearScalar(spv::Decoration::NoPrecision, scalarId, vec4Ty);
```

## Access Chain Helper

Builder maintains one active access chain for l-value/r-value tracking:

```cpp
builder.clearAccessChain();
builder.setAccessChainLValue(ptrId);    // base is pointer
builder.setAccessChainRValue(valueId);  // base is r-value
builder.accessChainPush(indexId, coherentFlags, alignment);
builder.accessChainPushSwizzle(channels, preSwizzleBaseType, coherentFlags, alignment);
builder.accessChainPushComponent(componentId, preSwizzleBaseType, coherentFlags, alignment);

spv::Id result = builder.accessChainLoad(precision, lvalNonUniform, rvalNonUniform, resultType, memAccess, scope, n);
builder.accessChainStore(valueId, spv::Decoration::NonUniform,
                         spv::MemoryAccessMask::MaskNone, spv::Scope::Max, 0);
spv::Id lval = builder.accessChainGetLValue();
spv::Id inferred = builder.accessChainGetInferredType();
bool canBeLvalue = builder.isSpvLvalue();  // false for multi-component swizzles like .yx

// Save/restore
spv::Builder::AccessChain saved = builder.getAccessChain();
builder.setAccessChain(saved);
```

## Texture Operations

```cpp
spv::Builder::TextureParameters params = {};
params.sampler = sampledImageId;
params.coords = coordsId;
params.lod = lodId;  // etc: bias, Dref, offset, gradX, gradY, component, sample, lodClamp, ...
// nonprivate, volatil, nontemporal = false

spv::Id tex = builder.createTextureCall(precision, resultType,
    false/*sparse*/, false/*fetch*/, false/*proj*/, false/*gather*/, false/*noImplicit*/,
    params, spv::ImageOperandsMask::MaskNone);
```

## Decorations & Names

```cpp
builder.addName(id, "myVar");
builder.addMemberName(structTy, 0, "field0");
builder.addDecoration(id, spv::Decoration::Location, 0);
builder.addDecoration(id, spv::Decoration::Binding, 2);
builder.addDecoration(id, spv::Decoration::DescriptorSet, 0);
builder.addDecoration(id, spv::Decoration::NoContraction);
builder.addDecoration(id, spv::Decoration::RelaxedPrecision);
builder.addDecoration(id, spv::Decoration::BuiltIn, (int)spv::BuiltIn::Position);
builder.addMemberDecoration(structTy, 0, spv::Decoration::Offset, 0);
builder.addMemberDecoration(structTy, 1, spv::Decoration::Offset, 16);
builder.addDecoration(id, spv::Decoration::WorkgroupSize, std::vector<unsigned>{64, 1, 1});
builder.addDecorationId(id, spv::Decoration::ArrayStrideIdEXT, strideId);
builder.addLinkageDecoration(id, "myFunc", spv::LinkageType::Export);
```

## Barriers

```cpp
builder.createControlBarrier(spv::Scope::Workgroup, spv::Scope::Device,
    spv::MemorySemanticsMask::UniformMemory | spv::MemorySemanticsMask::WorkgroupMemory);
builder.createMemoryBarrier(spv::Scope::Device, spv::MemorySemanticsMask::ImageMemory);
```

## Debug Info

### SPIR-V Standard (OpLine/OpSource)
```cpp
builder.setEmitSpirvDebugInfo();  // enables OpLine/OpSource tracking
builder.setDebugMainSourceFile("shader.glsl");
builder.setDebugSourceLocation(42, "shader.glsl");
builder.setSourceText(sourceText);
```

### NonSemantic Shader Debug Info
```cpp
builder.setEmitNonSemanticShaderDebugInfo(true);  // also enables OpLine-style tracking
spv::Id debugType = builder.getDebugType(spirvTypeId);
builder.enterLexicalBlock(line, column);
builder.leaveLexicalBlock();
builder.setupFunctionDebugInfo(func, "myFunc", paramTypes, paramNames);
spv::Id dbgGlobal = builder.createDebugGlobalVariable(debugType, "globalVar", varId);
spv::Id dbgLocal  = builder.createDebugLocalVariable(debugType, "localVar", argNumber);
spv::Id dbgDecl   = builder.makeDebugDeclare(dbgLocal, ptrId);
spv::Id dbgVal    = builder.makeDebugValue(dbgLocal, valueId);
```

## Function Calls & Builtins

```cpp
spv::Id result = builder.createFunctionCall(calleeFunc, {arg0, arg1, arg2});
spv::Id sqrtVal = builder.createBuiltinCall(floatTy, glsl450, GLSLstd450Sqrt, {val});
```

## Post-Processing & Serialization

```cpp
builder.postProcess(false);       // prune + caps/extensions
builder.postProcessCFG();         // prune unreachable
builder.postProcessFeatures();    // add caps/extensions from instructions
builder.postProcessSamplers();    // move OpSampledImage near users

std::vector<unsigned int> spirv;
builder.dump(spirv);
spv::Disassemble(std::cout, spirv);
glslang::OutputSpvBin(spirv, "out.spv");
glslang::OutputSpvHex(spirv, "out.h", "g_spv");
```

Both `postProcessCFG()` and `Function::dump()` traverse physical blocks with
`inReadableOrder()`, which assumes structured merge roles already nest. If an
outer selection merge is also an inner arm and then branches to the inner
merge, the physical graph exits the inner construct and re-enters it. The
traversal can initially mask that invalid topology by classifying the inner
merge as dead, replacing live code with `OpUnreachable`, and serializing it
before its dominator. Fix the producer's physical control-flow plan: preserve
the payload blocks but rotate the adjacent merge declarations so the inner
merge physically precedes the outer merge. Do not patch serialization order or
disable post-processing/validation around an invalid graph.

`OpSwitch` case literals are sized by the selector's `OpTypeInt`, not by the
generated operand-table class alone. A selector up to 32 bits uses one literal
word; a 64-bit selector uses two low-word-first literal words followed by one
target label ID. Disassemblers and binary walkers must resolve the selector
type and consume `ceil(bit_width / 32)` words per case before reading the label.
Never infer case boundaries by alternating one literal word and one ID.

Treat disassembly input as untrusted. Validate each instruction-local word
count before reading operands: reject zero, undersized, or module-truncated
instructions. When resolving an `OpSwitch` selector, also validate the mapped
defining instruction bounds and result ID; accept `OpTypeInt` only with its
exact four-word layout and a width of 8, 16, 32, or 64. Validate a directly
visited `OpTypeInt` before reading its width operand. The disassembler's fatal
path exits the process, so malformed-input regressions must run it in a child
process and assert the deterministic nonzero exit.

## IR Classes (`spvIR.h`)

```cpp
spv::Instruction* inst = new spv::Instruction(resultId, typeId, spv::Op::OpIAdd);
inst->addIdOperand(opA);
inst->addIdOperand(opB);

spv::Block* block = new spv::Block(blockId, *function);
block->addInstruction(std::unique_ptr<spv::Instruction>(inst));
block->addLocalVariable(std::unique_ptr<spv::Instruction>(varInst));
bool terminated = block->isTerminated();

spv::Function* func = new spv::Function(funcId, retType, funcType, firstParamId, linkage, name, module);
func->addBlock(block);
func->setReturnPrecision(spv::Decoration::RelaxedPrecision);
func->addParamPrecision(0, spv::Decoration::RelaxedPrecision);

spv::Module module;
module.addFunction(func);
module.mapInstruction(inst);
spv::Instruction* found = module.getInstruction(id);
spv::Id typeId = module.getTypeId(resultId);
```

## Key Types

| Type | Purpose |
|---|---|
| `spv::Builder` | SPIR-V module construction |
| `spv::Instruction` | Single SPIR-V instruction |
| `spv::Block` | Basic block |
| `spv::Function` | SPIR-V function |
| `spv::Module` | Module root, ID→instruction map |
| `spv::Builder::If` | Structured if-then-else helper |
| `spv::Builder::LoopBlocks` | Structured loop blocks |
| `spv::Builder::AccessChain` | L-value/R-value access chain |
| `spv::Builder::TextureParameters` | Texture op parameters |
| `spv::IdImmediate` | Operand: ID or immediate |
| `glslang::SpvOptions` | GlslangToSpv options |

## GlslangToSpv Patterns

From `TGlslangToSpvTraverser` (`src/ext/glslang/SPIRV/GlslangToSpv.cpp`). Common pattern: clear access chain → traverse → load/store → set R-value.

### visitSymbol
```cpp
builder.clearAccessChain();
// Treat spec constants, r-value parameters, and non-pointer/untyped values as r-values.
if (isRValue || rValueParameters.count(symbolId) ||
    (!builder.isPointerType(builder.getTypeId(id)) && !builder.isUntypedPointer(id)))
    builder.setAccessChainRValue(id);
else
    builder.setAccessChainLValue(id);

spv::StorageClass sc = builder.getStorageClass(id);
if (builder.isGlobalVariable(id))
    iOSet.insert(id);

builder.addExtension("SPV_GOOGLE_hlsl_functionality1");
builder.addDecorationId(id, spv::Decoration::HlslCounterBufferGOOGLE, counterId);
```

### visitBinary (Assignment)
```cpp
builder.clearAccessChain(); node->getLeft()->traverse(this);
auto lValue = builder.getAccessChain();
builder.clearAccessChain(); node->getRight()->traverse(this);
spv::Id rValue = accessChainLoad(node->getRight()->getType());
builder.setAccessChain(lValue);
multiTypeStore(node->getLeft()->getType(), rValue);
builder.clearAccessChain(); builder.setAccessChainRValue(rValue);
```

### visitBinary (Array/Vector Index)
```cpp
// zero-extend narrow uint indexes to 32-bit
if (builder.isUintType(indexType) && builder.getScalarTypeWidth(indexType) < 32)
    index = builder.createUnaryOp(spv::Op::OpUConvert, builder.makeUintType(32), index);
builder.accessChainPush(index, coherentFlags, alignment);
```

### visitBinary (Swizzle)
```cpp
builder.accessChainPushSwizzle(swizzle, convertGlslangToSpvType(node->getLeft()->getType()),
                               coherentFlags, alignment);
```

### visitUnary (Inc/Dec)
```cpp
spv::Id operand = builder.accessChainGetLValue();
spv::Id one = builder.makeIntConstant(1);
spv::Id result = builder.createBinOp(op, type, operand, one);
builder.accessChainStore(result, ...);
builder.clearAccessChain(); builder.setAccessChainRValue(result);
```

### visitUnary (Builtin / NoResult / ArrayLength)
```cpp
// Builtin
spv::Id result = builder.createBuiltinCall(resultType(), glsl450, opcode, {operand});
// No-result
builder.createNoResultOp(spv::Op::OpKill);
builder.createNoResultOp(spv::Op::OpTerminateInvocation);
builder.createNoResultOp(spv::Op::OpDemoteToHelperInvocationEXT);
builder.createNoResultOp(spv::Op::OpAssumeTrueKHR, operand);
// Array length
spv::Id len = builder.createArrayLength(builder.accessChainGetLValue(), member, bits);
len = builder.createUnaryOp(spv::Op::OpBitcast, builder.makeIntType(bits), len);
// Cooperative matrix/vector
spv::Id lenKHR = builder.createCooperativeMatrixLengthKHR(typeId);
spv::Id lenNV  = builder.createCooperativeMatrixLengthNV(typeId);
spv::Id lenVec = builder.getCooperativeVectorNumComponents(typeId);
// Tensor
spv::Id layout = builder.createOp(spv::Op::OpCreateTensorLayoutNV, resultType(), {});
spv::Id view   = builder.createOp(spv::Op::OpCreateTensorViewNV, resultType(), {});
```

### visitAggregate
```cpp
// Function entry/leave
builder.setBuildPoint(shaderEntry->getLastBlock());
builder.enterFunction(shaderEntry); /* body */ builder.leaveFunction();
// Function call
spv::Id result = builder.createFunctionCall(callee, arguments);
// Constructors
spv::Id c = builder.createConstructor(precision, arguments, resultType());
spv::Id m = builder.createMatrixConstructor(precision, arguments, resultType());
// Builtin
spv::Id r = builder.createBuiltinCall(resultType(), extInst, opcode, arguments);
// Texture
spv::Builder::TextureParameters params = {sampledImageId, coordsId, /*...*/};
spv::Id tex = builder.createTextureCall(precision, resultType(), sparse, fetch, proj, gather, noImplicit, params, mask);
// Sampled image
spv::Id sampled = builder.createOp(spv::Op::OpSampledImage, resultType(), {imageId, samplerId});
// Cooperative matrix conversion
spv::Id coop = builder.createCooperativeMatrixConversion(resultType(), arguments[0]);
// Variable
spv::Id var = builder.createVariable(precision, spv::StorageClass::Function, type, name, init);
// Load/store
spv::Id loaded = builder.createLoad(ptrId, precision);
builder.createStore(valueId, ptrId);
// Debug scopes
builder.enterLexicalBlock(loc.line, loc.column); /* body */ builder.leaveLexicalBlock();
```

### visitSelection
```cpp
// Scalar ternary
spv::Id result = builder.createTriOp(spv::Op::OpSelect, resultType, cond, trueVal, falseVal);
// Vector selection: for SPIR-V < 1.4 smear the scalar condition to the vector width;
// for SPIR-V >= 1.4 OpSelect accepts a scalar condition directly.
if (builder.getSpvVersion() < spv::Spv_1_4 && builder.isVector(trueVal)) {
    cond = builder.smearScalar(precision, cond,
                               builder.makeVectorType(builder.makeBoolType(),
                                                      builder.getNumComponents(trueVal)));
}
// If aggregate decorations cause type mismatches, normalize with OpCopyLogical.
if (builder.getTypeId(trueVal) != resultType)
    trueVal = builder.createUnaryOp(spv::Op::OpCopyLogical, resultType, trueVal);
if (builder.getTypeId(falseVal) != resultType)
    falseVal = builder.createUnaryOp(spv::Op::OpCopyLogical, resultType, falseVal);
spv::Id result = builder.createTriOp(spv::Op::OpSelect, resultType, cond, trueVal, falseVal);
```

### visitSwitch
```cpp
std::vector<int> caseValues = {0,1,2}, valueToSegment = {0,1,2};
builder.makeSwitch(selectorId, spv::SelectionControlMask::MaskNone, 4, caseValues, valueToSegment, 3, segmentBB);
builder.nextSwitchSegment(segmentBB, 0); /* case 0 */ builder.addSwitchBreak(false);
// ...
builder.endSwitch(segmentBB);
```

### visitLoop
```cpp
spv::Builder::LoopBlocks& loop = builder.makeNewLoop();
builder.setBuildPoint(&loop.head);
builder.createLoopMerge(&loop.merge, &loop.continue_target, spv::LoopControlMask::MaskNone, {});
builder.createConditionalBranch(cond, &loop.body, &loop.merge);
builder.setBuildPoint(&loop.body); /* body */ builder.createLoopContinue();
builder.setBuildPoint(&loop.continue_target); /* increment */ builder.createBranch(false, &loop.head);
builder.setBuildPoint(&loop.merge);
builder.closeLoop();
```

### visitBranch
```cpp
builder.makeReturn(false, returnValue);   // with value
builder.makeReturn(false);                // void
builder.createLoopExit();                 // break
builder.createLoopContinue();             // continue
builder.makeStatementTerminator(spv::Op::OpKill, "post-discard");
builder.makeStatementTerminator(spv::Op::OpTerminateInvocation, "post-terminate");
builder.createNoResultOp(spv::Op::OpDemoteToHelperInvocationEXT);
builder.makeStatementTerminator(spv::Op::OpTerminateRayKHR, "post-terminate-ray");
builder.makeStatementTerminator(spv::Op::OpIgnoreIntersectionKHR, "post-ignore");
```

### visitConstantUnion
```cpp
spv::Id constantId = createSpvConstant(node);
builder.clearAccessChain();
builder.setAccessChainRValue(constantId);
```

### visitVariableDecl
```cpp
builder.setDebugSourceLocation(node->getLoc().line, node->getLoc().getFilename());
```

### visitFunctions
```cpp
// No direct builder usage; drives traversal of the translation unit.
```

