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