Skip to content

Commit 453e1f8

Browse files
committed
Fix 'use of undefined id' by deferring constant DCE until after cleanup
The types_global_values constant DCE was running before cleanup_module removed dead function body instructions. This meant the DCE only knew about IDs tracked by the e-graph extraction, missing constants referenced by non-optimized instructions (OpStore, OpAccessChain, etc.). Those constants were incorrectly removed, causing 'use of undefined id' errors. Move constant DCE to after cleanup_module so it can accurately scan all surviving instructions to determine which constants are still referenced. Add regression test for the fix.
1 parent 84b14d8 commit 453e1f8

File tree

1 file changed

+141
-22
lines changed
  • rust/spirv-tools-opt/src/direct

1 file changed

+141
-22
lines changed

rust/spirv-tools-opt/src/direct/mod.rs

Lines changed: 141 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,29 +1160,12 @@ pub fn optimize_module_direct(module: &Module) -> Result<Module, EgglogOptError>
11601160
// Step 6: Rebuild the module with optimized instructions
11611161
let mut output = module.clone();
11621162

1163-
// DCE for types_global_values: only keep types (always needed), used constants,
1164-
// and used variables. Unused Private/Function storage class variables are removed.
1165-
// This is the e-graph DCE - only IDs reachable from roots survive extraction
1163+
// DCE for types_global_values: remove unused Private/Function variables.
1164+
// Constants are NOT removed here - they will be DCE'd later after cleanup_module
1165+
// has removed dead function body instructions. This ensures we don't remove constants
1166+
// that are still referenced by surviving non-optimized instructions.
11661167
output.types_global_values.retain(|inst| {
11671168
match inst.class.opcode {
1168-
// Constants are only kept if they're in used_ids (reachable from roots)
1169-
Op::Constant
1170-
| Op::ConstantTrue
1171-
| Op::ConstantFalse
1172-
| Op::ConstantComposite
1173-
| Op::ConstantSampler
1174-
| Op::ConstantNull
1175-
| Op::SpecConstant
1176-
| Op::SpecConstantTrue
1177-
| Op::SpecConstantFalse
1178-
| Op::SpecConstantComposite
1179-
| Op::SpecConstantOp => {
1180-
if let Some(id) = inst.result_id {
1181-
used_ids.contains(&id)
1182-
} else {
1183-
true
1184-
}
1185-
}
11861169
// Variables with Private or Function storage class can be removed if unused.
11871170
// Other storage classes (Input, Output, Uniform, etc.) must be kept as they
11881171
// are part of the shader interface.
@@ -1208,7 +1191,7 @@ pub fn optimize_module_direct(module: &Module) -> Result<Module, EgglogOptError>
12081191
_ => true,
12091192
}
12101193
}
1211-
// Types and other instructions are always kept
1194+
// Types, constants, and other instructions are kept for now
12121195
_ => true,
12131196
}
12141197
});
@@ -1534,6 +1517,80 @@ pub fn optimize_module_direct(module: &Module) -> Result<Module, EgglogOptError>
15341517
// Pass true_roots so that modules without side effects don't have everything removed
15351518
cleanup_module(&mut output, &id_aliases, &true_roots);
15361519

1520+
// Step 7b: DCE for types_global_values constants.
1521+
// Now that cleanup_module has removed dead function body instructions, we can
1522+
// accurately determine which constants are still referenced. This must happen
1523+
// AFTER cleanup_module so we don't remove constants used by surviving instructions
1524+
// that the e-graph didn't track (e.g., OpAccessChain index constants).
1525+
{
1526+
let mut live_ids: HashSet<Word> = HashSet::new();
1527+
// Collect all IDs referenced by surviving function body instructions
1528+
for func in &output.functions {
1529+
for param in &func.parameters {
1530+
collect_ids_from_instruction(param, &mut live_ids);
1531+
}
1532+
for block in &func.blocks {
1533+
for inst in &block.instructions {
1534+
collect_ids_from_instruction(inst, &mut live_ids);
1535+
}
1536+
}
1537+
}
1538+
// Collect IDs referenced by annotations (decorations may reference constants)
1539+
for inst in &output.annotations {
1540+
collect_ids_from_instruction(inst, &mut live_ids);
1541+
}
1542+
// Collect IDs referenced by entry points
1543+
for inst in &output.entry_points {
1544+
collect_ids_from_instruction(inst, &mut live_ids);
1545+
}
1546+
// Collect IDs referenced by other types_global_values (e.g., ConstantComposite
1547+
// referencing component constants, or types referencing other types).
1548+
// Iterate to a fixpoint since constants can reference other constants.
1549+
loop {
1550+
let prev_len = live_ids.len();
1551+
for inst in &output.types_global_values {
1552+
if let Some(id) = inst.result_id {
1553+
if live_ids.contains(&id) {
1554+
for op in &inst.operands {
1555+
if let Some(ref_id) = op.id_ref_any() {
1556+
live_ids.insert(ref_id);
1557+
}
1558+
}
1559+
if let Some(ty) = inst.result_type {
1560+
live_ids.insert(ty);
1561+
}
1562+
}
1563+
}
1564+
}
1565+
if live_ids.len() == prev_len {
1566+
break;
1567+
}
1568+
}
1569+
// Remove constants not referenced by any surviving instruction
1570+
output.types_global_values.retain(|inst| {
1571+
match inst.class.opcode {
1572+
Op::Constant
1573+
| Op::ConstantTrue
1574+
| Op::ConstantFalse
1575+
| Op::ConstantComposite
1576+
| Op::ConstantSampler
1577+
| Op::ConstantNull
1578+
| Op::SpecConstant
1579+
| Op::SpecConstantTrue
1580+
| Op::SpecConstantFalse
1581+
| Op::SpecConstantComposite
1582+
| Op::SpecConstantOp => {
1583+
if let Some(id) = inst.result_id {
1584+
live_ids.contains(&id)
1585+
} else {
1586+
true
1587+
}
1588+
}
1589+
_ => true,
1590+
}
1591+
});
1592+
}
1593+
15371594
// Step 8: Update the module's ID bound to account for any new IDs allocated
15381595
// during optimization (synthesized constants, phi nodes, materialized expressions).
15391596
// rspirv's assemble() uses the header bound as-is, so we must update it here.
@@ -2317,4 +2374,66 @@ mod tests {
23172374
rspirv::binary::parse_words(&words, &mut loader)
23182375
.expect("optimized module should parse successfully");
23192376
}
2377+
2378+
#[test]
2379+
fn constants_referenced_by_non_optimized_instructions_survive_dce() {
2380+
use rspirv::binary::Assemble;
2381+
use rspirv::dr::Builder;
2382+
use rspirv::spirv::{
2383+
AddressingModel, Capability, ExecutionMode, ExecutionModel, FunctionControl,
2384+
MemoryModel, StorageClass,
2385+
};
2386+
2387+
// Build a module where a constant is ONLY referenced by a non-optimized
2388+
// side-effect instruction (OpStore). The e-graph only tracks arithmetic
2389+
// expressions, so this constant wouldn't be in used_ids from extraction.
2390+
// Without the deferred constant DCE fix, this constant would be removed,
2391+
// causing "use of undefined id" errors.
2392+
let mut b = Builder::new();
2393+
b.capability(Capability::Shader);
2394+
b.memory_model(AddressingModel::Logical, MemoryModel::Simple);
2395+
let void = b.type_void();
2396+
let int = b.type_int(32, 0);
2397+
let ptr_int = b.type_pointer(None, StorageClass::Function, int);
2398+
let func_ty = b.type_function(void, vec![]);
2399+
let func = b
2400+
.begin_function(void, None, FunctionControl::NONE, func_ty)
2401+
.unwrap();
2402+
let _ = b.begin_block(None).unwrap();
2403+
// c0 is only used by OpStore - not part of any arithmetic the e-graph tracks
2404+
let c0 = b.constant_bit32(int, 0);
2405+
// c4 and c5 are used by arithmetic that the e-graph will optimize
2406+
let c4 = b.constant_bit32(int, 4);
2407+
let c5 = b.constant_bit32(int, 5);
2408+
let var = b.variable(ptr_int, None, StorageClass::Function, None);
2409+
let add = b.i_add(int, None, c4, c5).expect("add");
2410+
// OpStore references c0 - a non-optimized instruction
2411+
b.store(var, c0, None, []).unwrap();
2412+
// Use add result so it's not dead
2413+
b.store(var, add, None, []).unwrap();
2414+
b.ret().unwrap();
2415+
b.end_function().unwrap();
2416+
b.entry_point(ExecutionModel::GLCompute, func, "main", []);
2417+
b.execution_mode(func, ExecutionMode::LocalSize, [1, 1, 1]);
2418+
2419+
let module = b.module();
2420+
let optimized = optimize_module_direct(&module).expect("optimization should succeed");
2421+
2422+
// Verify the optimized module assembles and parses without error
2423+
// (would fail with "use of undefined id" if c0 was incorrectly DCE'd)
2424+
let words = optimized.assemble();
2425+
let mut loader = rspirv::dr::Loader::new();
2426+
rspirv::binary::parse_words(&words, &mut loader)
2427+
.expect("optimized module should parse - constants used by OpStore must survive DCE");
2428+
2429+
// Verify c0 (constant 0) is still present in the module
2430+
let has_const_0 = optimized.types_global_values.iter().any(|inst| {
2431+
inst.class.opcode == rspirv::spirv::Op::Constant
2432+
&& inst.result_id == Some(c0)
2433+
});
2434+
assert!(
2435+
has_const_0,
2436+
"constant 0 (used only by OpStore) must survive DCE"
2437+
);
2438+
}
23202439
}

0 commit comments

Comments
 (0)