1#[cfg(test)]
2mod test;
3
4pub(crate) mod dce;
5mod destructure_composites;
6mod duplicates;
7mod entry_interface;
8mod import_export_link;
9mod inline;
10mod ipo;
11mod mem2reg;
12mod param_weakening;
13mod peephole_opts;
14mod simple_passes;
15mod specializer;
16mod spirt_passes;
17mod zombies;
18
19use std::borrow::Cow;
20
21use crate::codegen_cx::{ModuleOutputType, SpirvMetadata};
22use crate::custom_decorations::{CustomDecoration, SrcLocDecoration, ZombieDecoration};
23use crate::custom_insts;
24use either::Either;
25use rspirv::binary::Assemble;
26use rspirv::dr::{Block, Module, ModuleHeader, Operand};
27use rspirv::spirv::{Op, StorageClass, Word};
28use rustc_data_structures::fx::FxHashMap;
29use rustc_errors::ErrorGuaranteed;
30use rustc_session::Session;
31use rustc_session::config::OutputFilenames;
32use std::cell::Cell;
33use std::collections::BTreeMap;
34use std::ffi::{OsStr, OsString};
35use std::path::PathBuf;
36
37pub type Result<T> = std::result::Result<T, ErrorGuaranteed>;
38
39#[derive(Default)]
40pub struct Options {
41 pub compact_ids: bool,
42 pub early_report_zombies: bool,
43 pub infer_storage_classes: bool,
44 pub structurize: bool,
45 pub preserve_bindings: bool,
46 pub spirt_passes: Vec<String>,
47
48 pub abort_strategy: Option<String>,
49 pub module_output_type: ModuleOutputType,
50
51 pub spirv_metadata: SpirvMetadata,
52
53 pub keep_link_exports: bool,
58
59 pub dump_post_merge: Option<PathBuf>,
62 pub dump_pre_inline: Option<PathBuf>,
63 pub dump_post_inline: Option<PathBuf>,
64 pub dump_post_split: Option<PathBuf>,
65 pub dump_spirt_passes: Option<PathBuf>,
66 pub spirt_strip_custom_debuginfo_from_dumps: bool,
67 pub spirt_keep_debug_sources_in_dumps: bool,
68 pub spirt_keep_unstructured_cfg_in_dumps: bool,
69 pub specializer_dump_instances: Option<PathBuf>,
70}
71
72pub enum LinkResult {
73 SingleModule(Box<Module>),
74 MultipleModules {
75 file_stem_to_entry_name_and_module: BTreeMap<OsString, (String, Module)>,
80 },
81}
82
83fn id(header: &mut ModuleHeader) -> Word {
84 let result = header.bound;
85 header.bound += 1;
86 result
87}
88
89fn apply_rewrite_rules<'a>(
90 rewrite_rules: &FxHashMap<Word, Word>,
91 blocks: impl IntoIterator<Item = &'a mut Block>,
92) {
93 let all_ids_mut = blocks
94 .into_iter()
95 .flat_map(|b| b.label.iter_mut().chain(b.instructions.iter_mut()))
96 .flat_map(|inst| {
97 inst.result_id
98 .iter_mut()
99 .chain(inst.result_type.iter_mut())
100 .chain(
101 inst.operands
102 .iter_mut()
103 .filter_map(|op| op.id_ref_any_mut()),
104 )
105 });
106 for id in all_ids_mut {
107 if let Some(&rewrite) = rewrite_rules.get(id) {
108 *id = rewrite;
109 }
110 }
111}
112
113fn get_names(module: &Module) -> FxHashMap<Word, &str> {
114 let entry_names = module
115 .entry_points
116 .iter()
117 .filter(|i| i.class.opcode == Op::EntryPoint)
118 .map(|i| {
119 (
120 i.operands[1].unwrap_id_ref(),
121 i.operands[2].unwrap_literal_string(),
122 )
123 });
124 let debug_names = module
125 .debug_names
126 .iter()
127 .filter(|i| i.class.opcode == Op::Name)
128 .map(|i| {
129 (
130 i.operands[0].unwrap_id_ref(),
131 i.operands[1].unwrap_literal_string(),
132 )
133 });
134 entry_names.chain(debug_names).collect()
136}
137
138fn get_name<'a>(names: &FxHashMap<Word, &'a str>, id: Word) -> Cow<'a, str> {
139 names.get(&id).map_or_else(
140 || Cow::Owned(format!("Unnamed function ID %{id}")),
141 |&s| Cow::Borrowed(s),
142 )
143}
144
145impl Options {
146 fn spirt_cleanup_for_dumping(&self, module: &mut spirt::Module) {
148 if self.spirt_strip_custom_debuginfo_from_dumps {
149 spirt_passes::debuginfo::convert_custom_debuginfo_to_spv(module);
150 }
151 if !self.spirt_keep_debug_sources_in_dumps {
152 const DOTS: &str = "⋯";
153 let dots_interned_str = module.cx().intern(DOTS);
154 let spirt::ModuleDebugInfo::Spv(debuginfo) = &mut module.debug_info;
155 for sources in debuginfo.source_languages.values_mut() {
156 for file in sources.file_contents.values_mut() {
157 *file = DOTS.into();
158 }
159 sources.file_contents.insert(
160 dots_interned_str,
161 "sources hidden, to show them use \
162 `RUSTGPU_CODEGEN_ARGS=--spirt-keep-debug-sources-in-dumps`"
163 .into(),
164 );
165 }
166 }
167 }
168}
169
170pub fn link(
171 sess: &Session,
172 mut inputs: Vec<Module>,
173 opts: &Options,
174 outputs: &OutputFilenames,
175 disambiguated_crate_name_for_dumps: &OsStr,
176) -> Result<LinkResult> {
177 let spv_module_to_spv_words_and_spirt_module = |spv_module: &Module| {
182 let spv_words;
183 let spv_bytes = {
184 let _timer = sess.timer("assemble-to-spv_bytes-for-spirt");
185 spv_words = spv_module.assemble();
186 spirv_tools::binary::from_binary(&spv_words).to_vec()
189 };
190
191 let lower_from_spv_timer = sess.timer("spirt::Module::lower_from_spv_file");
193 let cx = std::rc::Rc::new(spirt::Context::new());
194 crate::custom_insts::register_to_spirt_context(&cx);
195 (
196 spv_words,
197 spirt::Module::lower_from_spv_bytes(cx, spv_bytes),
198 lower_from_spv_timer,
200 )
201 };
202
203 let dump_spv_and_spirt = |spv_module: &Module, dump_file_path_stem: PathBuf| {
205 let (spv_words, spirt_module_or_err, _) =
206 spv_module_to_spv_words_and_spirt_module(spv_module);
207 std::fs::write(
208 dump_file_path_stem.with_extension("spv"),
209 spirv_tools::binary::from_binary(&spv_words),
210 )
211 .unwrap();
212
213 if let Ok(mut module) = spirt_module_or_err {
215 spirt::passes::link::minimize_exports(&mut module, |export_key| {
217 matches!(export_key, spirt::ExportKey::SpvEntryPoint { .. })
218 });
219
220 opts.spirt_cleanup_for_dumping(&mut module);
221
222 let pretty = spirt::print::Plan::for_module(&module).pretty_print();
223
224 std::fs::write(
226 dump_file_path_stem.with_extension("spirt"),
227 pretty.to_string(),
228 )
229 .unwrap();
230 std::fs::write(
231 dump_file_path_stem.with_extension("spirt.html"),
232 pretty
233 .render_to_html()
234 .with_dark_mode_support()
235 .to_html_doc(),
236 )
237 .unwrap();
238 }
239 };
240
241 let mut output = {
242 let _timer = sess.timer("link_merge");
243 let mut bound = inputs[0].header.as_ref().unwrap().bound - 1;
245 let version = inputs[0].header.as_ref().unwrap().version();
246
247 for module in inputs.iter_mut().skip(1) {
248 simple_passes::shift_ids(module, bound);
249 bound += module.header.as_ref().unwrap().bound - 1;
250 let this_version = module.header.as_ref().unwrap().version();
251 if version != this_version {
252 return Err(sess.dcx().err(format!(
253 "cannot link two modules with different SPIR-V versions: v{}.{} and v{}.{}",
254 version.0, version.1, this_version.0, this_version.1
255 )));
256 }
257 }
258
259 let mut output = crate::link::with_rspirv_loader(|loader| {
261 for module in inputs {
262 for inst in module.all_inst_iter() {
263 use rspirv::binary::ParseAction;
264 match loader.consume_instruction(inst.clone()) {
265 ParseAction::Continue => {}
266 ParseAction::Stop => unreachable!(),
267 ParseAction::Error(err) => return Err(err),
268 }
269 }
270 }
271 Ok(())
272 })
273 .unwrap();
274
275 let mut header = ModuleHeader::new(bound + 1);
276 header.set_version(version.0, version.1);
277 header.generator = 0x001B_0000;
278 output.header = Some(header);
279 output
280 };
281
282 if let Some(dir) = &opts.dump_post_merge {
283 dump_spv_and_spirt(&output, dir.join(disambiguated_crate_name_for_dumps));
284 }
285
286 {
288 let _timer = sess.timer("link_remove_duplicates");
289 duplicates::remove_duplicate_extensions(&mut output);
290 duplicates::remove_duplicate_capabilities(&mut output);
291 duplicates::remove_duplicate_ext_inst_imports(&mut output);
292 duplicates::remove_duplicate_types(&mut output);
293 }
295
296 {
298 let _timer = sess.timer("link_find_pairs");
299 import_export_link::run(opts, sess, &mut output)?;
300 }
301
302 {
303 let _timer = sess.timer("link_dce-post-link");
304 dce::dce(&mut output);
305 }
306
307 {
308 let _timer = sess.timer("link_fragment_inst_check");
309 simple_passes::check_fragment_insts(sess, &output)?;
310 }
311
312 {
316 let _timer = sess.timer("link_remove_unused_params");
317 output = param_weakening::remove_unused_params(output);
318 }
319
320 if opts.early_report_zombies {
321 let _timer = sess.timer("link_report_zombies");
322 zombies::report_zombies(sess, &output)?;
323 }
324
325 if opts.infer_storage_classes {
326 let _timer = sess.timer("specialize_generic_storage_class");
327 for func in &mut output.functions {
331 simple_passes::block_ordering_pass(func);
332 }
333 output = specializer::specialize(
334 opts,
335 output,
336 specializer::SimpleSpecialization {
337 specialize_operand: |operand| {
338 matches!(operand, Operand::StorageClass(StorageClass::Generic))
339 },
340
341 concrete_fallback: Operand::StorageClass(StorageClass::Function),
352 },
353 );
354 }
355
356 {
358 {
359 let _timer = sess.timer("link_dce-before-inlining");
360 dce::dce(&mut output);
361 }
362
363 let _timer = sess.timer("link_block_ordering_pass_and_mem2reg-before-inlining");
364 let mut pointer_to_pointee = FxHashMap::default();
365 let mut constants = FxHashMap::default();
366 let mut u32 = None;
367 for inst in &output.types_global_values {
368 match inst.class.opcode {
369 Op::TypePointer => {
370 pointer_to_pointee
371 .insert(inst.result_id.unwrap(), inst.operands[1].unwrap_id_ref());
372 }
373 Op::TypeInt
374 if inst.operands[0].unwrap_literal_bit32() == 32
375 && inst.operands[1].unwrap_literal_bit32() == 0 =>
376 {
377 assert!(u32.is_none());
378 u32 = Some(inst.result_id.unwrap());
379 }
380 Op::Constant if u32.is_some() && inst.result_type == u32 => {
381 let value = inst.operands[0].unwrap_literal_bit32();
382 constants.insert(inst.result_id.unwrap(), value);
383 }
384 _ => {}
385 }
386 }
387 for func in &mut output.functions {
388 simple_passes::block_ordering_pass(func);
389 mem2reg::mem2reg(
391 output.header.as_mut().unwrap(),
392 &mut output.types_global_values,
393 &pointer_to_pointee,
394 &constants,
395 func,
396 );
397 destructure_composites::destructure_composites(func);
398 }
399 }
400
401 {
402 let _timer =
403 sess.timer("link_dce-and-remove_duplicate_debuginfo-after-mem2reg-before-inlining");
404 dce::dce(&mut output);
405 duplicates::remove_duplicate_debuginfo(&mut output);
406 }
407
408 if let Some(dir) = &opts.dump_pre_inline {
410 dump_spv_and_spirt(&output, dir.join(disambiguated_crate_name_for_dumps));
411 }
412
413 {
414 let _timer = sess.timer("link_inline");
415 inline::inline(sess, &mut output)?;
416 }
417
418 {
424 let _timer = sess.timer("link_fold_load_from_constant_variable");
425 peephole_opts::fold_load_from_constant_variable(&mut output);
426 }
427
428 {
429 let _timer = sess.timer("link_dce-after-inlining");
430 dce::dce(&mut output);
431 }
432
433 if let Some(dir) = &opts.dump_post_inline {
435 dump_spv_and_spirt(&output, dir.join(disambiguated_crate_name_for_dumps));
436 }
437
438 {
439 let _timer = sess.timer("link_block_ordering_pass_and_mem2reg-after-inlining");
440 let mut pointer_to_pointee = FxHashMap::default();
441 let mut constants = FxHashMap::default();
442 let mut u32 = None;
443 for inst in &output.types_global_values {
444 match inst.class.opcode {
445 Op::TypePointer => {
446 pointer_to_pointee
447 .insert(inst.result_id.unwrap(), inst.operands[1].unwrap_id_ref());
448 }
449 Op::TypeInt
450 if inst.operands[0].unwrap_literal_bit32() == 32
451 && inst.operands[1].unwrap_literal_bit32() == 0 =>
452 {
453 assert!(u32.is_none());
454 u32 = Some(inst.result_id.unwrap());
455 }
456 Op::Constant if u32.is_some() && inst.result_type == u32 => {
457 let value = inst.operands[0].unwrap_literal_bit32();
458 constants.insert(inst.result_id.unwrap(), value);
459 }
460 _ => {}
461 }
462 }
463 for func in &mut output.functions {
464 simple_passes::block_ordering_pass(func);
465 mem2reg::mem2reg(
467 output.header.as_mut().unwrap(),
468 &mut output.types_global_values,
469 &pointer_to_pointee,
470 &constants,
471 func,
472 );
473 destructure_composites::destructure_composites(func);
474 }
475 }
476
477 {
478 let _timer =
479 sess.timer("link_dce-and-remove_duplicate_debuginfo-after-mem2reg-after-inlining");
480 dce::dce(&mut output);
481 duplicates::remove_duplicate_debuginfo(&mut output);
482 }
483
484 {
485 let _timer = sess.timer("link_remove_non_uniform");
486 simple_passes::remove_non_uniform_decorations(sess, &mut output)?;
487 }
488
489 {
491 let (spv_words, module_or_err, lower_from_spv_timer) =
492 spv_module_to_spv_words_and_spirt_module(&output);
493 let module = &mut module_or_err.map_err(|e| {
494 let spv_path = outputs.temp_path_for_diagnostic("spirt-lower-from-spv-input.spv");
495
496 let was_saved_msg =
497 match std::fs::write(&spv_path, spirv_tools::binary::from_binary(&spv_words)) {
498 Ok(()) => format!("was saved to {}", spv_path.display()),
499 Err(e) => format!("could not be saved: {e}"),
500 };
501
502 sess.dcx()
503 .struct_err(format!("{e}"))
504 .with_note("while lowering SPIR-V module to SPIR-T (spirt::spv::lower)")
505 .with_note(format!("input SPIR-V module {was_saved_msg}"))
506 .emit()
507 })?;
508
509 let mut dump_guard = SpirtDumpGuard {
510 sess,
511 linker_options: opts,
512 outputs,
513 disambiguated_crate_name_for_dumps,
514
515 module,
516 per_pass_module_for_dumping: vec![],
517 in_progress_pass_name: Cell::new(Some("lower_from_spv")),
518 any_spirt_bugs: false,
519 };
520 let module = &mut *dump_guard.module;
521 let before_pass = |pass_name| {
523 let outer_pass_name = dump_guard.in_progress_pass_name.replace(Some(pass_name));
524
525 assert_eq!(outer_pass_name, None);
527
528 sess.timer(pass_name)
529 };
530 let mut after_pass = |module: Option<&spirt::Module>, timer| {
531 drop(timer);
532 let pass_name = dump_guard.in_progress_pass_name.take().unwrap();
533 if let Some(module) = module
534 && opts.dump_spirt_passes.is_some()
535 {
536 dump_guard
537 .per_pass_module_for_dumping
538 .push((pass_name.into(), module.clone()));
539 }
540 };
541 after_pass(
544 (opts.spirt_keep_unstructured_cfg_in_dumps || !opts.structurize).then_some(&*module),
545 lower_from_spv_timer,
546 );
547
548 {
553 let timer = before_pass(
554 "spirt_passes::controlflow::convert_custom_aborts_to_unstructured_returns_in_entry_points",
555 );
556 spirt_passes::controlflow::convert_custom_aborts_to_unstructured_returns_in_entry_points(opts, module);
557 after_pass(None, timer);
558 }
559
560 if opts.structurize {
561 let timer = before_pass("spirt::legalize::structurize_func_cfgs");
562 spirt::passes::legalize::structurize_func_cfgs(module);
563 after_pass(Some(module), timer);
564 }
565
566 if !opts.spirt_passes.is_empty() {
567 spirt_passes::run_func_passes(
569 module,
570 &opts.spirt_passes,
571 |name, _module| before_pass(name),
572 &mut after_pass,
573 );
574 }
575
576 {
577 let timer = before_pass("spirt_passes::explicit_layout::erase_when_invalid");
578 spirt_passes::explicit_layout::erase_when_invalid(module);
579 after_pass(Some(module), timer);
580 }
581
582 {
583 let timer = before_pass("spirt_passes::validate");
584 spirt_passes::validate::validate(module);
585 after_pass(Some(module), timer);
586 }
587
588 {
589 let timer = before_pass("spirt_passes::diagnostics::report_diagnostics");
590 spirt_passes::diagnostics::report_diagnostics(sess, opts, module).map_err(
591 |spirt_passes::diagnostics::ReportedDiagnostics {
592 rustc_errors_guarantee,
593 any_errors_were_spirt_bugs,
594 }| {
595 dump_guard.any_spirt_bugs |= any_errors_were_spirt_bugs;
596 rustc_errors_guarantee
597 },
598 )?;
599 after_pass(None, timer);
600 }
601
602 {
604 let timer = before_pass("spirt_passes::debuginfo::convert_custom_debuginfo_to_spv");
605 spirt_passes::debuginfo::convert_custom_debuginfo_to_spv(module);
606 after_pass(None, timer);
607 }
608
609 let spv_words = {
610 let timer = before_pass("spirt::Module::lift_to_spv_module_emitter");
611 let spv_words = module.lift_to_spv_module_emitter().unwrap().words;
612 after_pass(None, timer);
613 spv_words
614 };
615 output = {
617 let _timer = sess.timer("parse-spv_words-from-spirt");
618 crate::link::with_rspirv_loader(|loader| {
619 rspirv::binary::parse_words(&spv_words, loader)
620 })
621 .unwrap()
622 };
623 }
624
625 for inst in &output.ext_inst_imports {
627 assert_eq!(inst.class.opcode, Op::ExtInstImport);
628 let ext_inst_set = inst.operands[0].unwrap_literal_string();
629 if ext_inst_set.starts_with(custom_insts::CUSTOM_EXT_INST_SET_PREFIX) {
630 let expected = &custom_insts::CUSTOM_EXT_INST_SET[..];
631 if ext_inst_set == expected {
632 return Err(sess.dcx().err(format!(
633 "`OpExtInstImport {ext_inst_set:?}` should not have been \
634 left around after SPIR-T passes"
635 )));
636 } else {
637 return Err(sess.dcx().err(format!(
638 "unsupported `OpExtInstImport {ext_inst_set:?}`
639 (expected {expected:?} name - version mismatch?)"
640 )));
641 }
642 }
643 }
644
645 {
651 let _timer = sess.timer("peephole_opts");
652 let types = peephole_opts::collect_types(&output);
653 for func in &mut output.functions {
654 peephole_opts::composite_construct(&types, func);
655 peephole_opts::vector_ops(output.header.as_mut().unwrap(), &types, func);
656 peephole_opts::bool_fusion(output.header.as_mut().unwrap(), &types, func);
657 }
658 }
659
660 {
661 let _timer = sess.timer("link_remove_unused_type_capabilities");
662 simple_passes::remove_unused_type_capabilities(&mut output);
663 }
664
665 {
666 let _timer = sess.timer("link_gather_all_interface_vars_from_uses");
667 entry_interface::gather_all_interface_vars_from_uses(&mut output, opts.preserve_bindings);
668 }
669
670 if opts.spirv_metadata == SpirvMetadata::NameVariables {
671 let _timer = sess.timer("link_name_variables");
672 simple_passes::name_variables_pass(&mut output);
673 }
674
675 {
676 let _timer = sess.timer("link_sort_globals");
677 simple_passes::sort_globals(&mut output);
678 }
679
680 let mut output = if opts.module_output_type == ModuleOutputType::Multiple {
681 let mut file_stem_to_entry_name_and_module = BTreeMap::new();
682 for (i, entry) in output.entry_points.iter().enumerate() {
683 let mut module = output.clone();
684 module.entry_points.clear();
685 module.entry_points.push(entry.clone());
686 let entry_name = entry.operands[2].unwrap_literal_string().to_string();
687 let mut file_stem = OsString::from(
688 sanitize_filename::sanitize_with_options(
689 &entry_name,
690 sanitize_filename::Options {
691 replacement: "-",
692 ..Default::default()
693 },
694 )
695 .replace("--", "-"),
696 );
697 let mut disambiguator = Some(i);
700 loop {
701 use std::collections::btree_map::Entry;
702 match file_stem_to_entry_name_and_module.entry(file_stem) {
703 Entry::Vacant(entry) => {
704 entry.insert((entry_name, module));
705 break;
706 }
707 #[allow(clippy::assigning_clones)]
710 Entry::Occupied(entry) => {
711 file_stem = entry.key().clone();
714 file_stem.push(".");
715 match disambiguator.take() {
716 Some(d) => file_stem.push(d.to_string()),
717 None => file_stem.push("next"),
718 }
719 }
720 }
721 }
722 }
723 LinkResult::MultipleModules {
724 file_stem_to_entry_name_and_module,
725 }
726 } else {
727 LinkResult::SingleModule(Box::new(output))
728 };
729
730 let output_module_iter = match &mut output {
731 LinkResult::SingleModule(m) => Either::Left(std::iter::once((None, &mut **m))),
732 LinkResult::MultipleModules {
733 file_stem_to_entry_name_and_module,
734 } => Either::Right(
735 file_stem_to_entry_name_and_module
736 .iter_mut()
737 .map(|(file_stem, (_, m))| (Some(file_stem), m)),
738 ),
739 };
740 for (file_stem, output) in output_module_iter {
741 {
745 let _timer = sess.timer("link_dce-post-split");
746 dce::dce(output);
747 }
748
749 if let Some(dir) = &opts.dump_post_split {
751 let mut file_name = disambiguated_crate_name_for_dumps.to_os_string();
752 if let Some(file_stem) = file_stem {
753 file_name.push(".");
754 file_name.push(file_stem);
755 }
756
757 dump_spv_and_spirt(output, dir.join(file_name));
758 }
759
760 {
761 let _timer = sess.timer("link_remove_duplicate_debuginfo");
762 duplicates::remove_duplicate_debuginfo(output);
763 }
764
765 if opts.compact_ids {
766 let _timer = sess.timer("link_compact_ids");
767 output.header.as_mut().unwrap().bound = simple_passes::compact_ids(output);
769 };
770
771 SrcLocDecoration::remove_all(output);
775
776 ZombieDecoration::remove_all(output);
778 }
779
780 Ok(output)
781}
782
783struct SpirtDumpGuard<'a> {
786 sess: &'a Session,
787 linker_options: &'a Options,
788 outputs: &'a OutputFilenames,
789 disambiguated_crate_name_for_dumps: &'a OsStr,
790
791 module: &'a mut spirt::Module,
792 per_pass_module_for_dumping: Vec<(Cow<'static, str>, spirt::Module)>,
793 in_progress_pass_name: Cell<Option<&'static str>>,
794 any_spirt_bugs: bool,
795}
796
797impl Drop for SpirtDumpGuard<'_> {
798 fn drop(&mut self) {
799 if std::thread::panicking() {
800 self.any_spirt_bugs = true;
801
802 if let Some(pass_name) = self.in_progress_pass_name.get() {
806 self.per_pass_module_for_dumping.push((
807 format!("{pass_name} [PANICKED]").into(),
808 self.module.clone(),
809 ));
810 }
811 }
812
813 let mut dump_spirt_file_path =
814 self.linker_options
815 .dump_spirt_passes
816 .as_ref()
817 .map(|dump_dir| {
818 dump_dir
819 .join(self.disambiguated_crate_name_for_dumps)
820 .with_extension("spirt")
821 });
822
823 if self.any_spirt_bugs && dump_spirt_file_path.is_none() {
829 if self.per_pass_module_for_dumping.is_empty() {
830 self.per_pass_module_for_dumping
831 .push(("".into(), self.module.clone()));
832 }
833 dump_spirt_file_path = Some(self.outputs.temp_path_for_diagnostic("spirt"));
834 }
835
836 let Some(dump_spirt_file_path) = &dump_spirt_file_path else {
837 return;
838 };
839
840 for (_, module) in &mut self.per_pass_module_for_dumping {
841 self.linker_options.spirt_cleanup_for_dumping(module);
843 }
844
845 let cx = self.module.cx();
846 let versions = self
847 .per_pass_module_for_dumping
848 .iter()
849 .map(|(pass_name, module)| (format!("after {pass_name}"), module));
850
851 let mut panicked_printing_after_passes = None;
852 for truncate_version_count in (1..=versions.len()).rev() {
853 let printed_or_panicked =
861 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
862 let pretty = spirt::print::Plan::for_versions(
863 &cx,
864 versions.clone().take(truncate_version_count),
865 )
866 .pretty_print();
867
868 std::fs::write(dump_spirt_file_path, pretty.to_string()).unwrap();
870 std::fs::write(
871 dump_spirt_file_path.with_extension("spirt.html"),
872 pretty
873 .render_to_html()
874 .with_dark_mode_support()
875 .to_html_doc(),
876 )
877 .unwrap();
878 }));
879 match printed_or_panicked {
880 Ok(()) => {
881 if truncate_version_count != versions.len() {
882 panicked_printing_after_passes = Some(
883 self.per_pass_module_for_dumping[truncate_version_count..]
884 .iter()
885 .map(|(pass_name, _)| format!("`{pass_name}`"))
886 .collect::<Vec<_>>()
887 .join(", "),
888 );
889 }
890 break;
891 }
892 Err(panic) => {
893 if truncate_version_count == 1 {
894 std::panic::resume_unwind(panic);
895 }
896 }
897 }
898 }
899 if self.any_spirt_bugs || panicked_printing_after_passes.is_some() {
900 let mut note = self.sess.dcx().struct_note("SPIR-T bugs were encountered");
901 if let Some(pass_names) = panicked_printing_after_passes {
902 note.warn(format!(
903 "SPIR-T pretty-printing panicked after: {pass_names}"
904 ));
905 }
906 note.help(format!(
907 "pretty-printed SPIR-T was saved to {}.html",
908 dump_spirt_file_path.display()
909 ));
910 if self.linker_options.dump_spirt_passes.is_none() {
911 note.help("re-run with `RUSTGPU_CODEGEN_ARGS=\"--dump-spirt-passes=$PWD\"` for more details");
912 }
913 note.note("pretty-printed SPIR-T is preferred when reporting Rust-GPU issues");
914 note.emit();
915 }
916 }
917}