tenseleyflow/bencch / b9364ba

Browse files

Add graph case support

Authored by espadonne
SHA
b9364ba2e5bec472a6ed22cd024c7649a12064d2
Parents
7619096
Tree
3d05b4e

1 changed file

StatusFile+-
M bench/src/lib.rs 554 195
bench/src/lib.rsmodified
1027 lines changed — click to load
@@ -26,6 +26,7 @@ struct SuiteSpec {
2626
 struct CaseSpec {
2727
     name: String,
2828
     source: PathBuf,
29
+    graph_files: Vec<PathBuf>,
2930
     requested: BTreeSet<Stage>,
3031
     opt_levels: Vec<OptLevel>,
3132
     repeat_count: usize,
@@ -35,6 +36,31 @@ struct CaseSpec {
3536
     status_rules: Vec<StatusRule>,
3637
 }
3738
 
39
+impl CaseSpec {
40
+    fn is_graph(&self) -> bool {
41
+        !self.graph_files.is_empty()
42
+    }
43
+
44
+    fn source_label(&self) -> String {
45
+        if self.is_graph() {
46
+            format!(
47
+                "graph entry {} ({} files)",
48
+                self.source.display(),
49
+                self.graph_files.len()
50
+            )
51
+        } else {
52
+            self.source.display().to_string()
53
+        }
54
+    }
55
+}
56
+
57
+#[derive(Debug, Clone)]
58
+struct PreparedInput {
59
+    compiler_source: PathBuf,
60
+    generated_source: Option<PathBuf>,
61
+    temp_root: Option<PathBuf>,
62
+}
63
+
3864
 #[derive(Debug, Clone)]
3965
 struct StatusRule {
4066
     kind: StatusKind,
@@ -520,9 +546,10 @@ fn parse_cli(args: &[String]) -> Result<CommandKind, String> {
520546
                     "--include-future" => config.include_future = true,
521547
                     "--all" => config.all_stages = true,
522548
                     "--armfortas-bin" => {
523
-                        let value = queue.pop_front().ok_or("--armfortas-bin requires a value")?;
524
-                        config.tools.armfortas =
525
-                            ArmfortasCliAdapter::External(value.clone());
549
+                        let value = queue
550
+                            .pop_front()
551
+                            .ok_or("--armfortas-bin requires a value")?;
552
+                        config.tools.armfortas = ArmfortasCliAdapter::External(value.clone());
526553
                     }
527554
                     "--gfortran-bin" => {
528555
                         let value = queue.pop_front().ok_or("--gfortran-bin requires a value")?;
@@ -666,12 +693,13 @@ fn parse_suite_file(path: &Path) -> Result<SuiteSpec, String> {
666693
         })?;
667694
 
668695
         if let Some(rest) = line.strip_prefix("source ") {
669
-            let relative = parse_quoted(rest, path, line_no)?;
670
-            let source = path
671
-                .parent()
672
-                .unwrap_or_else(|| Path::new("."))
673
-                .join(relative);
674
-            builder.source = Some(source);
696
+            builder.source = Some(resolve_suite_relative_path(rest, path, line_no)?);
697
+        } else if let Some(rest) = line.strip_prefix("entry ") {
698
+            builder.graph_entry = Some(resolve_suite_relative_path(rest, path, line_no)?);
699
+        } else if let Some(rest) = line.strip_prefix("file ") {
700
+            builder
701
+                .graph_files
702
+                .push(resolve_suite_relative_path(rest, path, line_no)?);
675703
         } else if let Some(rest) = line.strip_prefix("armfortas =>") {
676704
             builder.requested = parse_stage_list(rest, path, line_no)?;
677705
         } else if let Some(rest) = line.strip_prefix("repeat =>") {
@@ -728,6 +756,8 @@ fn parse_suite_file(path: &Path) -> Result<SuiteSpec, String> {
728756
 struct CaseBuilder {
729757
     name: String,
730758
     source: Option<PathBuf>,
759
+    graph_entry: Option<PathBuf>,
760
+    graph_files: Vec<PathBuf>,
731761
     requested: BTreeSet<Stage>,
732762
     opt_levels: Vec<OptLevel>,
733763
     repeat_count: usize,
@@ -742,6 +772,8 @@ impl CaseBuilder {
742772
         Self {
743773
             name,
744774
             source: None,
775
+            graph_entry: None,
776
+            graph_files: Vec::new(),
745777
             requested: BTreeSet::new(),
746778
             opt_levels: Vec::new(),
747779
             repeat_count: 2,
@@ -753,13 +785,49 @@ impl CaseBuilder {
753785
     }
754786
 
755787
     fn build(self, suite_path: &Path) -> Result<CaseSpec, String> {
756
-        let source = self.source.ok_or_else(|| {
757
-            format!(
758
-                "{}: case '{}' is missing a source path",
788
+        if self.source.is_some() && (self.graph_entry.is_some() || !self.graph_files.is_empty()) {
789
+            return Err(format!(
790
+                "{}: case '{}' mixes source with graph entry/file declarations",
759791
                 suite_path.display(),
760792
                 self.name
761
-            )
762
-        })?;
793
+            ));
794
+        }
795
+
796
+        if self.graph_entry.is_some() && self.graph_files.is_empty() {
797
+            return Err(format!(
798
+                "{}: case '{}' declares an entry without any file members",
799
+                suite_path.display(),
800
+                self.name
801
+            ));
802
+        }
803
+
804
+        if self.graph_entry.is_none() && !self.graph_files.is_empty() {
805
+            return Err(format!(
806
+                "{}: case '{}' declares file members without an entry",
807
+                suite_path.display(),
808
+                self.name
809
+            ));
810
+        }
811
+
812
+        let (source, graph_files) = if let Some(source) = self.source {
813
+            (source, Vec::new())
814
+        } else if let Some(entry) = self.graph_entry {
815
+            if !self.graph_files.iter().any(|file| file == &entry) {
816
+                return Err(format!(
817
+                    "{}: case '{}' entry '{}' is not listed in file declarations",
818
+                    suite_path.display(),
819
+                    self.name,
820
+                    entry.display()
821
+                ));
822
+            }
823
+            (entry, self.graph_files)
824
+        } else {
825
+            return Err(format!(
826
+                "{}: case '{}' is missing a source path or graph entry",
827
+                suite_path.display(),
828
+                self.name
829
+            ));
830
+        };
763831
 
764832
         let mut requested = self.requested;
765833
         if requested.is_empty() {
@@ -775,6 +843,7 @@ impl CaseBuilder {
775843
         Ok(CaseSpec {
776844
             name: self.name,
777845
             source,
846
+            graph_files,
778847
             requested,
779848
             opt_levels,
780849
             repeat_count: self.repeat_count,
@@ -786,6 +855,14 @@ impl CaseBuilder {
786855
     }
787856
 }
788857
 
858
+fn resolve_suite_relative_path(rest: &str, path: &Path, line_no: usize) -> Result<PathBuf, String> {
859
+    let relative = parse_quoted(rest, path, line_no)?;
860
+    Ok(path
861
+        .parent()
862
+        .unwrap_or_else(|| Path::new("."))
863
+        .join(relative))
864
+}
865
+
789866
 fn parse_stage_list(rest: &str, path: &Path, line_no: usize) -> Result<BTreeSet<Stage>, String> {
790867
     let mut stages = BTreeSet::new();
791868
     for raw in rest.split(',') {
@@ -1251,6 +1328,8 @@ fn execute_case_cell(
12511328
         ensure_consistency_stage(*check, &mut requested);
12521329
     }
12531330
 
1331
+    let prepared = prepare_case_input(case, suite, opt_level)?;
1332
+
12541333
     if config.verbose {
12551334
         let stage_list = requested
12561335
             .iter()
@@ -1266,7 +1345,13 @@ fn execute_case_cell(
12661345
                 .collect::<Vec<_>>()
12671346
                 .join(", ")
12681347
         };
1269
-        println!("  source: {}", case.source.display());
1348
+        println!("  source: {}", case.source_label());
1349
+        if case.is_graph() {
1350
+            for file in &case.graph_files {
1351
+                println!("  file: {}", file.display());
1352
+            }
1353
+            println!("  compiled_as: {}", prepared.compiler_source.display());
1354
+        }
12701355
         println!("  opt: {}", opt_level.as_str());
12711356
         println!("  stages: {}", stage_list);
12721357
         println!("  refs: {}", refs);
@@ -1276,12 +1361,12 @@ fn execute_case_cell(
12761361
     }
12771362
 
12781363
     let request = CaptureRequest {
1279
-        input: case.source.clone(),
1364
+        input: prepared.compiler_source.clone(),
12801365
         requested: requested.clone(),
12811366
         opt_level,
12821367
     };
12831368
 
1284
-    let references = run_reference_compilers(case, opt_level, &config.tools);
1369
+    let references = run_reference_compilers(&prepared, case, opt_level, &config.tools);
12851370
     let mut artifacts = ExecutionArtifacts {
12861371
         requested,
12871372
         armfortas: None,
@@ -1309,7 +1394,7 @@ fn execute_case_cell(
13091394
                 }
13101395
                 if execution.is_ok() && !case.consistency_checks.is_empty() {
13111396
                     artifacts.consistency_issues =
1312
-                        run_consistency_checks(case, opt_level, result, &config.tools);
1397
+                        run_consistency_checks(case, &prepared, opt_level, result, &config.tools);
13131398
                     if !artifacts.consistency_issues.is_empty() {
13141399
                         execution = Err(format_consistency_issues(&artifacts.consistency_issues));
13151400
                     }
@@ -1404,7 +1489,7 @@ fn execute_case_cell(
14041489
         || (matches!(outcome.kind, OutcomeKind::Xfail) && !artifacts.consistency_issues.is_empty());
14051490
 
14061491
     if should_bundle {
1407
-        match write_failure_bundle(suite, case, &outcome, &artifacts) {
1492
+        match write_failure_bundle(suite, case, &prepared, &outcome, &artifacts) {
14081493
             Ok(bundle) => outcome.bundle = Some(bundle),
14091494
             Err(err) => {
14101495
                 if outcome.detail.is_empty() {
@@ -1419,11 +1504,85 @@ fn execute_case_cell(
14191504
         }
14201505
     }
14211506
 
1507
+    cleanup_prepared_input(&prepared);
14221508
     cleanup_consistency_issues(&artifacts.consistency_issues);
14231509
 
14241510
     Ok(outcome)
14251511
 }
14261512
 
1513
+fn prepare_case_input(
1514
+    case: &CaseSpec,
1515
+    suite: &SuiteSpec,
1516
+    opt_level: OptLevel,
1517
+) -> Result<PreparedInput, String> {
1518
+    if case.graph_files.is_empty() {
1519
+        return Ok(PreparedInput {
1520
+            compiler_source: case.source.clone(),
1521
+            generated_source: None,
1522
+            temp_root: None,
1523
+        });
1524
+    }
1525
+
1526
+    let temp_root = default_report_root().join(".tmp").join(format!(
1527
+        "graph_{}_{}_{}",
1528
+        sanitize_component(&suite.name),
1529
+        sanitize_component(&case.name),
1530
+        next_report_suffix(opt_level)
1531
+    ));
1532
+    fs::create_dir_all(&temp_root).map_err(|e| {
1533
+        format!(
1534
+            "cannot create graph temp dir '{}': {}",
1535
+            temp_root.display(),
1536
+            e
1537
+        )
1538
+    })?;
1539
+
1540
+    let extension = case
1541
+        .source
1542
+        .extension()
1543
+        .and_then(|ext| ext.to_str())
1544
+        .filter(|ext| !ext.is_empty())
1545
+        .unwrap_or("f90");
1546
+    let generated_source = temp_root.join(format!(
1547
+        "{}_graph.{}",
1548
+        sanitize_component(&case.name),
1549
+        extension
1550
+    ));
1551
+
1552
+    let mut combined = String::new();
1553
+    for (index, file) in case.graph_files.iter().enumerate() {
1554
+        let text = fs::read_to_string(file)
1555
+            .map_err(|e| format!("cannot read graph file '{}': {}", file.display(), e))?;
1556
+        if index > 0 {
1557
+            combined.push('\n');
1558
+        }
1559
+        combined.push_str(&text);
1560
+        if !text.ends_with('\n') {
1561
+            combined.push('\n');
1562
+        }
1563
+    }
1564
+
1565
+    fs::write(&generated_source, combined).map_err(|e| {
1566
+        format!(
1567
+            "cannot write generated graph input '{}': {}",
1568
+            generated_source.display(),
1569
+            e
1570
+        )
1571
+    })?;
1572
+
1573
+    Ok(PreparedInput {
1574
+        compiler_source: generated_source.clone(),
1575
+        generated_source: Some(generated_source),
1576
+        temp_root: Some(temp_root),
1577
+    })
1578
+}
1579
+
1580
+fn cleanup_prepared_input(prepared: &PreparedInput) {
1581
+    if let Some(temp_root) = &prepared.temp_root {
1582
+        let _ = fs::remove_dir_all(temp_root);
1583
+    }
1584
+}
1585
+
14271586
 fn status_for_opt(case: &CaseSpec, opt_level: OptLevel) -> EffectiveStatus {
14281587
     let mut status = EffectiveStatus::Normal;
14291588
     for rule in &case.status_rules {
@@ -1752,6 +1911,7 @@ fn compose_armfortas_failure_detail(artifacts: &ExecutionArtifacts) -> String {
17521911
 
17531912
 fn run_consistency_checks(
17541913
     case: &CaseSpec,
1914
+    prepared: &PreparedInput,
17551915
     opt_level: OptLevel,
17561916
     capture_result: &CaptureResult,
17571917
     tools: &ToolchainConfig,
@@ -1760,54 +1920,63 @@ fn run_consistency_checks(
17601920
     for check in &case.consistency_checks {
17611921
         let issue = match check {
17621922
             ConsistencyCheck::CliObjVsSystemAs => {
1763
-                run_cli_obj_vs_system_as(&case.source, opt_level, tools)
1764
-            }
1765
-            ConsistencyCheck::CliAsmReproducible => {
1766
-                run_cli_asm_reproducible(&case.source, opt_level, case.repeat_count, tools)
1767
-            }
1768
-            ConsistencyCheck::CliObjReproducible => {
1769
-                run_cli_obj_reproducible(&case.source, opt_level, case.repeat_count, tools)
1770
-            }
1771
-            ConsistencyCheck::CliRunReproducible => {
1772
-                run_cli_run_reproducible(&case.source, opt_level, case.repeat_count, tools)
1923
+                run_cli_obj_vs_system_as(&prepared.compiler_source, opt_level, tools)
17731924
             }
1925
+            ConsistencyCheck::CliAsmReproducible => run_cli_asm_reproducible(
1926
+                &prepared.compiler_source,
1927
+                opt_level,
1928
+                case.repeat_count,
1929
+                tools,
1930
+            ),
1931
+            ConsistencyCheck::CliObjReproducible => run_cli_obj_reproducible(
1932
+                &prepared.compiler_source,
1933
+                opt_level,
1934
+                case.repeat_count,
1935
+                tools,
1936
+            ),
1937
+            ConsistencyCheck::CliRunReproducible => run_cli_run_reproducible(
1938
+                &prepared.compiler_source,
1939
+                opt_level,
1940
+                case.repeat_count,
1941
+                tools,
1942
+            ),
17741943
             ConsistencyCheck::CaptureAsmVsCliAsm => run_capture_asm_vs_cli_asm(
1775
-                &case.source,
1944
+                &prepared.compiler_source,
17761945
                 opt_level,
17771946
                 case.repeat_count,
17781947
                 capture_result,
17791948
                 tools,
17801949
             ),
17811950
             ConsistencyCheck::CaptureObjVsCliObj => run_capture_obj_vs_cli_obj(
1782
-                &case.source,
1951
+                &prepared.compiler_source,
17831952
                 opt_level,
17841953
                 case.repeat_count,
17851954
                 capture_result,
17861955
                 tools,
17871956
             ),
17881957
             ConsistencyCheck::CaptureRunVsCliRun => run_capture_run_vs_cli_run(
1789
-                &case.source,
1958
+                &prepared.compiler_source,
17901959
                 opt_level,
17911960
                 case.repeat_count,
17921961
                 capture_result,
17931962
                 tools,
17941963
             ),
17951964
             ConsistencyCheck::CaptureAsmReproducible => run_capture_asm_reproducible(
1796
-                &case.source,
1965
+                &prepared.compiler_source,
17971966
                 opt_level,
17981967
                 case.repeat_count,
17991968
                 capture_result,
18001969
                 tools,
18011970
             ),
18021971
             ConsistencyCheck::CaptureObjReproducible => run_capture_obj_reproducible(
1803
-                &case.source,
1972
+                &prepared.compiler_source,
18041973
                 opt_level,
18051974
                 case.repeat_count,
18061975
                 capture_result,
18071976
                 tools,
18081977
             ),
18091978
             ConsistencyCheck::CaptureRunReproducible => run_capture_run_reproducible(
1810
-                &case.source,
1979
+                &prepared.compiler_source,
18111980
                 opt_level,
18121981
                 case.repeat_count,
18131982
                 capture_result,
@@ -1870,20 +2039,20 @@ fn run_cli_obj_vs_system_as(
18702039
 
18712040
     let asm_command =
18722041
         match compile_with_driver(source, opt_level, DriverEmitMode::Asm, &asm_path, tools) {
1873
-        Ok(command) => command,
1874
-        Err(detail) => {
1875
-            return Some(ConsistencyIssue {
1876
-                check: ConsistencyCheck::CliObjVsSystemAs,
1877
-                summary: "armfortas -S failed during consistency check".into(),
1878
-                repeat_count: None,
1879
-                unique_variant_count: None,
1880
-                varying_components: Vec::new(),
1881
-                stable_components: Vec::new(),
1882
-                detail,
1883
-                temp_root,
1884
-            })
1885
-        }
1886
-    };
2042
+            Ok(command) => command,
2043
+            Err(detail) => {
2044
+                return Some(ConsistencyIssue {
2045
+                    check: ConsistencyCheck::CliObjVsSystemAs,
2046
+                    summary: "armfortas -S failed during consistency check".into(),
2047
+                    repeat_count: None,
2048
+                    unique_variant_count: None,
2049
+                    varying_components: Vec::new(),
2050
+                    stable_components: Vec::new(),
2051
+                    detail,
2052
+                    temp_root,
2053
+                })
2054
+            }
2055
+        };
18872056
 
18882057
     let as_args = vec![
18892058
         "-o".to_string(),
@@ -1929,20 +2098,20 @@ fn run_cli_obj_vs_system_as(
19292098
 
19302099
     let obj_command =
19312100
         match compile_with_driver(source, opt_level, DriverEmitMode::Obj, &obj_path, tools) {
1932
-        Ok(command) => command,
1933
-        Err(detail) => {
1934
-            return Some(ConsistencyIssue {
1935
-                check: ConsistencyCheck::CliObjVsSystemAs,
1936
-                summary: "armfortas -c failed during consistency check".into(),
1937
-                repeat_count: None,
1938
-                unique_variant_count: None,
1939
-                varying_components: Vec::new(),
1940
-                stable_components: Vec::new(),
1941
-                detail,
1942
-                temp_root,
1943
-            })
1944
-        }
1945
-    };
2101
+            Ok(command) => command,
2102
+            Err(detail) => {
2103
+                return Some(ConsistencyIssue {
2104
+                    check: ConsistencyCheck::CliObjVsSystemAs,
2105
+                    summary: "armfortas -c failed during consistency check".into(),
2106
+                    repeat_count: None,
2107
+                    unique_variant_count: None,
2108
+                    varying_components: Vec::new(),
2109
+                    stable_components: Vec::new(),
2110
+                    detail,
2111
+                    temp_root,
2112
+                })
2113
+            }
2114
+        };
19462115
 
19472116
     let asm_snapshot = match object_snapshot(&asm_obj_path, tools) {
19482117
         Ok(snapshot) => snapshot,
@@ -2038,27 +2207,22 @@ fn run_cli_asm_reproducible(
20382207
     let mut runs = Vec::new();
20392208
     for index in 0..repeat_count {
20402209
         let asm_path = temp_root.join(format!("run_{:02}.s", index));
2041
-        let command = match compile_with_driver(
2042
-            source,
2043
-            opt_level,
2044
-            DriverEmitMode::Asm,
2045
-            &asm_path,
2046
-            tools,
2047
-        ) {
2048
-            Ok(command) => command,
2049
-            Err(detail) => {
2050
-                return Some(ConsistencyIssue {
2051
-                    check: ConsistencyCheck::CliAsmReproducible,
2052
-                    summary: "armfortas -S failed during reproducibility check".into(),
2053
-                    repeat_count: None,
2054
-                    unique_variant_count: None,
2055
-                    varying_components: Vec::new(),
2056
-                    stable_components: Vec::new(),
2057
-                    detail,
2058
-                    temp_root,
2059
-                })
2060
-            }
2061
-        };
2210
+        let command =
2211
+            match compile_with_driver(source, opt_level, DriverEmitMode::Asm, &asm_path, tools) {
2212
+                Ok(command) => command,
2213
+                Err(detail) => {
2214
+                    return Some(ConsistencyIssue {
2215
+                        check: ConsistencyCheck::CliAsmReproducible,
2216
+                        summary: "armfortas -S failed during reproducibility check".into(),
2217
+                        repeat_count: None,
2218
+                        unique_variant_count: None,
2219
+                        varying_components: Vec::new(),
2220
+                        stable_components: Vec::new(),
2221
+                        detail,
2222
+                        temp_root,
2223
+                    })
2224
+                }
2225
+            };
20622226
         let text = match read_text_artifact(&asm_path) {
20632227
             Ok(text) => text,
20642228
             Err(detail) => {
@@ -2135,27 +2299,22 @@ fn run_cli_obj_reproducible(
21352299
     let mut runs = Vec::new();
21362300
     for index in 0..repeat_count {
21372301
         let obj_path = temp_root.join(format!("run_{:02}.o", index));
2138
-        let command = match compile_with_driver(
2139
-            source,
2140
-            opt_level,
2141
-            DriverEmitMode::Obj,
2142
-            &obj_path,
2143
-            tools,
2144
-        ) {
2145
-            Ok(command) => command,
2146
-            Err(detail) => {
2147
-                return Some(ConsistencyIssue {
2148
-                    check: ConsistencyCheck::CliObjReproducible,
2149
-                    summary: "armfortas -c failed during reproducibility check".into(),
2150
-                    repeat_count: None,
2151
-                    unique_variant_count: None,
2152
-                    varying_components: Vec::new(),
2153
-                    stable_components: Vec::new(),
2154
-                    detail,
2155
-                    temp_root,
2156
-                })
2157
-            }
2158
-        };
2302
+        let command =
2303
+            match compile_with_driver(source, opt_level, DriverEmitMode::Obj, &obj_path, tools) {
2304
+                Ok(command) => command,
2305
+                Err(detail) => {
2306
+                    return Some(ConsistencyIssue {
2307
+                        check: ConsistencyCheck::CliObjReproducible,
2308
+                        summary: "armfortas -c failed during reproducibility check".into(),
2309
+                        repeat_count: None,
2310
+                        unique_variant_count: None,
2311
+                        varying_components: Vec::new(),
2312
+                        stable_components: Vec::new(),
2313
+                        detail,
2314
+                        temp_root,
2315
+                    })
2316
+                }
2317
+            };
21592318
         let snapshot = match object_snapshot(&obj_path, tools) {
21602319
             Ok(snapshot) => snapshot,
21612320
             Err(detail) => {
@@ -2259,22 +2418,21 @@ fn run_cli_run_reproducible(
22592418
             &binary_path,
22602419
             tools,
22612420
         ) {
2262
-                Ok(command) => command,
2263
-                Err(detail) => {
2264
-                    return Some(ConsistencyIssue {
2265
-                        check: ConsistencyCheck::CliRunReproducible,
2266
-                        summary:
2267
-                            "armfortas binary build failed during runtime reproducibility check"
2268
-                                .into(),
2269
-                        repeat_count: None,
2270
-                        unique_variant_count: None,
2271
-                        varying_components: Vec::new(),
2272
-                        stable_components: Vec::new(),
2273
-                        detail,
2274
-                        temp_root,
2275
-                    })
2276
-                }
2277
-            };
2421
+            Ok(command) => command,
2422
+            Err(detail) => {
2423
+                return Some(ConsistencyIssue {
2424
+                    check: ConsistencyCheck::CliRunReproducible,
2425
+                    summary: "armfortas binary build failed during runtime reproducibility check"
2426
+                        .into(),
2427
+                    repeat_count: None,
2428
+                    unique_variant_count: None,
2429
+                    varying_components: Vec::new(),
2430
+                    stable_components: Vec::new(),
2431
+                    detail,
2432
+                    temp_root,
2433
+                })
2434
+            }
2435
+        };
22782436
         let run_command = render_binary_run_command(&binary_path);
22792437
         let run = match run_binary_capture(&binary_path, &temp_root, &run_command) {
22802438
             Ok(run) => run,
@@ -2415,27 +2573,23 @@ fn run_capture_asm_vs_cli_asm(
24152573
     let mut mismatch_indices = Vec::new();
24162574
     for index in 0..repeat_count {
24172575
         let asm_path = temp_root.join(format!("cli_run_{:02}.s", index));
2418
-        let command = match compile_with_driver(
2419
-            source,
2420
-            opt_level,
2421
-            DriverEmitMode::Asm,
2422
-            &asm_path,
2423
-            tools,
2424
-        ) {
2425
-            Ok(command) => command,
2426
-            Err(detail) => {
2427
-                return Some(ConsistencyIssue {
2428
-                    check: ConsistencyCheck::CaptureAsmVsCliAsm,
2429
-                    summary: "armfortas -S failed during capture-vs-cli consistency check".into(),
2430
-                    repeat_count: None,
2431
-                    unique_variant_count: None,
2432
-                    varying_components: Vec::new(),
2433
-                    stable_components: Vec::new(),
2434
-                    detail,
2435
-                    temp_root,
2436
-                })
2437
-            }
2438
-        };
2576
+        let command =
2577
+            match compile_with_driver(source, opt_level, DriverEmitMode::Asm, &asm_path, tools) {
2578
+                Ok(command) => command,
2579
+                Err(detail) => {
2580
+                    return Some(ConsistencyIssue {
2581
+                        check: ConsistencyCheck::CaptureAsmVsCliAsm,
2582
+                        summary: "armfortas -S failed during capture-vs-cli consistency check"
2583
+                            .into(),
2584
+                        repeat_count: None,
2585
+                        unique_variant_count: None,
2586
+                        varying_components: Vec::new(),
2587
+                        stable_components: Vec::new(),
2588
+                        detail,
2589
+                        temp_root,
2590
+                    })
2591
+                }
2592
+            };
24392593
         let text = match read_text_artifact(&asm_path) {
24402594
             Ok(text) => text,
24412595
             Err(detail) => {
@@ -2576,27 +2730,23 @@ fn run_capture_obj_vs_cli_obj(
25762730
     let mut mismatch_indices = Vec::new();
25772731
     for index in 0..repeat_count {
25782732
         let obj_path = temp_root.join(format!("cli_run_{:02}.o", index));
2579
-        let command = match compile_with_driver(
2580
-            source,
2581
-            opt_level,
2582
-            DriverEmitMode::Obj,
2583
-            &obj_path,
2584
-            tools,
2585
-        ) {
2586
-            Ok(command) => command,
2587
-            Err(detail) => {
2588
-                return Some(ConsistencyIssue {
2589
-                    check: ConsistencyCheck::CaptureObjVsCliObj,
2590
-                    summary: "armfortas -c failed during capture-vs-cli consistency check".into(),
2591
-                    repeat_count: None,
2592
-                    unique_variant_count: None,
2593
-                    varying_components: Vec::new(),
2594
-                    stable_components: Vec::new(),
2595
-                    detail,
2596
-                    temp_root,
2597
-                })
2598
-            }
2599
-        };
2733
+        let command =
2734
+            match compile_with_driver(source, opt_level, DriverEmitMode::Obj, &obj_path, tools) {
2735
+                Ok(command) => command,
2736
+                Err(detail) => {
2737
+                    return Some(ConsistencyIssue {
2738
+                        check: ConsistencyCheck::CaptureObjVsCliObj,
2739
+                        summary: "armfortas -c failed during capture-vs-cli consistency check"
2740
+                            .into(),
2741
+                        repeat_count: None,
2742
+                        unique_variant_count: None,
2743
+                        varying_components: Vec::new(),
2744
+                        stable_components: Vec::new(),
2745
+                        detail,
2746
+                        temp_root,
2747
+                    })
2748
+                }
2749
+            };
26002750
         let snapshot = match object_snapshot(&obj_path, tools) {
26012751
             Ok(snapshot) => snapshot,
26022752
             Err(detail) => {
@@ -2765,22 +2915,21 @@ fn run_capture_run_vs_cli_run(
27652915
             &binary_path,
27662916
             tools,
27672917
         ) {
2768
-                Ok(command) => command,
2769
-                Err(detail) => {
2770
-                    return Some(ConsistencyIssue {
2771
-                        check: ConsistencyCheck::CaptureRunVsCliRun,
2772
-                        summary:
2773
-                            "armfortas binary build failed during capture-vs-cli runtime check"
2774
-                                .into(),
2775
-                        repeat_count: None,
2776
-                        unique_variant_count: None,
2777
-                        varying_components: Vec::new(),
2778
-                        stable_components: Vec::new(),
2779
-                        detail,
2780
-                        temp_root,
2781
-                    })
2782
-                }
2783
-            };
2918
+            Ok(command) => command,
2919
+            Err(detail) => {
2920
+                return Some(ConsistencyIssue {
2921
+                    check: ConsistencyCheck::CaptureRunVsCliRun,
2922
+                    summary: "armfortas binary build failed during capture-vs-cli runtime check"
2923
+                        .into(),
2924
+                    repeat_count: None,
2925
+                    unique_variant_count: None,
2926
+                    varying_components: Vec::new(),
2927
+                    stable_components: Vec::new(),
2928
+                    detail,
2929
+                    temp_root,
2930
+                })
2931
+            }
2932
+        };
27842933
         let run_command = render_binary_run_command(&binary_path);
27852934
         let run = match run_binary_capture(&binary_path, &temp_root, &run_command) {
27862935
             Ok(run) => run,
@@ -3322,6 +3471,7 @@ fn run_capture_run_reproducible(
33223471
 }
33233472
 
33243473
 fn run_reference_compilers(
3474
+    prepared: &PreparedInput,
33253475
     case: &CaseSpec,
33263476
     opt_level: OptLevel,
33273477
     tools: &ToolchainConfig,
@@ -3329,7 +3479,7 @@ fn run_reference_compilers(
33293479
     case.reference_compilers
33303480
         .iter()
33313481
         .copied()
3332
-        .map(|compiler| run_reference_case(&case.source, opt_level, compiler, tools))
3482
+        .map(|compiler| run_reference_case(&prepared.compiler_source, opt_level, compiler, tools))
33333483
         .collect()
33343484
 }
33353485
 
@@ -3715,12 +3865,22 @@ struct ObjectRun {
37153865
 }
37163866
 
37173867
 fn object_snapshot(path: &Path, tools: &ToolchainConfig) -> Result<ObjectSnapshot, String> {
3718
-    let text = normalize_tool_output(&tool_output(tools.otool_bin(), &["-t", path.to_str().unwrap()])?);
3719
-    let load_commands =
3720
-        normalize_tool_output(&tool_output(tools.otool_bin(), &["-l", path.to_str().unwrap()])?);
3721
-    let relocations =
3722
-        normalize_tool_output(&tool_output(tools.otool_bin(), &["-rv", path.to_str().unwrap()])?);
3723
-    let symbols = normalize_tool_output(&tool_output(tools.nm_bin(), &["-m", path.to_str().unwrap()])?);
3868
+    let text = normalize_tool_output(&tool_output(
3869
+        tools.otool_bin(),
3870
+        &["-t", path.to_str().unwrap()],
3871
+    )?);
3872
+    let load_commands = normalize_tool_output(&tool_output(
3873
+        tools.otool_bin(),
3874
+        &["-l", path.to_str().unwrap()],
3875
+    )?);
3876
+    let relocations = normalize_tool_output(&tool_output(
3877
+        tools.otool_bin(),
3878
+        &["-rv", path.to_str().unwrap()],
3879
+    )?);
3880
+    let symbols = normalize_tool_output(&tool_output(
3881
+        tools.nm_bin(),
3882
+        &["-m", path.to_str().unwrap()],
3883
+    )?);
37243884
 
37253885
     Ok(ObjectSnapshot {
37263886
         text,
@@ -4188,6 +4348,7 @@ fn render_summary(summary: &Summary) -> String {
41884348
 fn write_failure_bundle(
41894349
     suite: &SuiteSpec,
41904350
     case: &CaseSpec,
4351
+    prepared: &PreparedInput,
41914352
     outcome: &Outcome,
41924353
     artifacts: &ExecutionArtifacts,
41934354
 ) -> Result<PathBuf, String> {
@@ -4233,7 +4394,7 @@ fn write_failure_bundle(
42334394
         case.name,
42344395
         outcome.kind,
42354396
         outcome.opt_level.as_str(),
4236
-        case.source.display(),
4397
+        case.source_label(),
42374398
         stage_list,
42384399
         case.repeat_count,
42394400
         refs,
@@ -4244,10 +4405,7 @@ fn write_failure_bundle(
42444405
     fs::write(bundle_root.join("detail.txt"), &outcome.detail)
42454406
         .map_err(|e| format!("cannot write bundle detail: {}", e))?;
42464407
 
4247
-    let source_text = fs::read_to_string(&case.source)
4248
-        .map_err(|e| format!("cannot read case source '{}': {}", case.source.display(), e))?;
4249
-    fs::write(bundle_root.join("source.f90"), source_text)
4250
-        .map_err(|e| format!("cannot write bundle source copy: {}", e))?;
4408
+    write_case_sources_bundle(&bundle_root, case, prepared)?;
42514409
 
42524410
     let armfortas_root = bundle_root.join("armfortas");
42534411
     fs::create_dir_all(&armfortas_root)
@@ -4280,6 +4438,52 @@ fn write_failure_bundle(
42804438
     Ok(bundle_root)
42814439
 }
42824440
 
4441
+fn write_case_sources_bundle(
4442
+    bundle_root: &Path,
4443
+    case: &CaseSpec,
4444
+    prepared: &PreparedInput,
4445
+) -> Result<(), String> {
4446
+    if case.graph_files.is_empty() {
4447
+        let source_text = fs::read_to_string(&case.source)
4448
+            .map_err(|e| format!("cannot read case source '{}': {}", case.source.display(), e))?;
4449
+        fs::write(bundle_root.join("source.f90"), source_text)
4450
+            .map_err(|e| format!("cannot write bundle source copy: {}", e))?;
4451
+        return Ok(());
4452
+    }
4453
+
4454
+    let generated_source = prepared.generated_source.as_ref().ok_or_else(|| {
4455
+        format!(
4456
+            "graph case '{}' was missing a generated compiler source",
4457
+            case.name
4458
+        )
4459
+    })?;
4460
+    let generated_text = fs::read_to_string(generated_source).map_err(|e| {
4461
+        format!(
4462
+            "cannot read generated graph source '{}': {}",
4463
+            generated_source.display(),
4464
+            e
4465
+        )
4466
+    })?;
4467
+    fs::write(bundle_root.join("source.f90"), generated_text)
4468
+        .map_err(|e| format!("cannot write generated bundle source copy: {}", e))?;
4469
+
4470
+    let sources_root = bundle_root.join("sources");
4471
+    fs::create_dir_all(&sources_root)
4472
+        .map_err(|e| format!("cannot create bundle sources dir: {}", e))?;
4473
+    for (index, file) in case.graph_files.iter().enumerate() {
4474
+        let text = fs::read_to_string(file)
4475
+            .map_err(|e| format!("cannot read graph source '{}': {}", file.display(), e))?;
4476
+        let file_name = file
4477
+            .file_name()
4478
+            .and_then(|name| name.to_str())
4479
+            .unwrap_or("source.f90");
4480
+        let target = sources_root.join(format!("{:02}_{}", index, file_name));
4481
+        fs::write(target, text).map_err(|e| format!("cannot write bundle graph source: {}", e))?;
4482
+    }
4483
+
4484
+    Ok(())
4485
+}
4486
+
42834487
 fn write_capture_result(root: &Path, result: &CaptureResult) -> Result<(), String> {
42844488
     for (stage, captured) in &result.stages {
42854489
         match captured {
@@ -4656,8 +4860,8 @@ fn match_checks(checks: &[Check], output: &str, case_name: &str) -> Result<(), S
46564860
 mod tests {
46574861
     use super::*;
46584862
     use crate::compiler::test_support::{
4659
-        verify_module, BlockParam, FloatWidth, Function, Inst, InstKind, IntWidth, IrType,
4660
-        Module, Position, Span, Terminator, ValueId,
4863
+        verify_module, BlockParam, FloatWidth, Function, Inst, InstKind, IntWidth, IrType, Module,
4864
+        Position, Span, Terminator, ValueId,
46614865
     };
46624866
 
46634867
     fn dummy_span() -> Span {
@@ -4781,6 +4985,43 @@ end
47814985
         let _ = fs::remove_file(&root);
47824986
     }
47834987
 
4988
+    #[test]
4989
+    fn parses_graph_case() {
4990
+        let root = std::env::temp_dir().join("afs_tests_graph_spec");
4991
+        let _ = fs::remove_dir_all(&root);
4992
+        fs::create_dir_all(&root).unwrap();
4993
+        fs::write(
4994
+            root.join("math_values.f90"),
4995
+            "module math_values\nend module\n",
4996
+        )
4997
+        .unwrap();
4998
+        fs::write(root.join("main.f90"), "program main\nend program\n").unwrap();
4999
+        fs::write(
5000
+            root.join("graph.afs"),
5001
+            r#"suite "modules/graph"
5002
+
5003
+case "basic_use"
5004
+entry "main.f90"
5005
+file "math_values.f90"
5006
+file "main.f90"
5007
+armfortas => run
5008
+expect run.exit_code equals 0
5009
+end
5010
+"#,
5011
+        )
5012
+        .unwrap();
5013
+
5014
+        let suite = parse_suite_file(&root.join("graph.afs")).unwrap();
5015
+        let case = &suite.cases[0];
5016
+        assert_eq!(case.source, root.join("main.f90"));
5017
+        assert_eq!(
5018
+            case.graph_files,
5019
+            vec![root.join("math_values.f90"), root.join("main.f90")]
5020
+        );
5021
+
5022
+        let _ = fs::remove_dir_all(&root);
5023
+    }
5024
+
47845025
     #[test]
47855026
     fn parse_cli_collects_tool_overrides() {
47865027
         let args = vec![
@@ -4804,7 +5045,10 @@ end
48045045
         let command = parse_cli(&args).unwrap();
48055046
         let config = match command {
48065047
             CommandKind::Run(config) => config,
4807
-            other => panic!("expected run command, got {:?}", std::mem::discriminant(&other)),
5048
+            other => panic!(
5049
+                "expected run command, got {:?}",
5050
+                std::mem::discriminant(&other)
5051
+            ),
48085052
         };
48095053
 
48105054
         assert_eq!(config.suite_filter.as_deref(), Some("consistency/runtime"));
@@ -4899,6 +5143,7 @@ end
48995143
         let case = CaseSpec {
49005144
             name: "no_reserved_register".into(),
49015145
             source: PathBuf::from("demo.f90"),
5146
+            graph_files: Vec::new(),
49025147
             requested: BTreeSet::from([Stage::Asm]),
49035148
             opt_levels: vec![OptLevel::O0],
49045149
             repeat_count: 2,
@@ -4945,6 +5190,7 @@ end
49455190
         let case = CaseSpec {
49465191
             name: "hello_bundle".into(),
49475192
             source: source.clone(),
5193
+            graph_files: Vec::new(),
49485194
             requested: BTreeSet::from([Stage::Ir, Stage::Run]),
49495195
             opt_levels: vec![OptLevel::O0],
49505196
             repeat_count: 3,
@@ -5034,8 +5280,13 @@ end
50345280
             bundle: None,
50355281
             consistency_observations: Vec::new(),
50365282
         };
5283
+        let prepared = PreparedInput {
5284
+            compiler_source: source.clone(),
5285
+            generated_source: None,
5286
+            temp_root: None,
5287
+        };
50375288
 
5038
-        let bundle = write_failure_bundle(&suite, &case, &outcome, &artifacts).unwrap();
5289
+        let bundle = write_failure_bundle(&suite, &case, &prepared, &outcome, &artifacts).unwrap();
50395290
         assert!(bundle.join("metadata.txt").exists());
50405291
         assert!(bundle.join("detail.txt").exists());
50415292
         assert!(bundle.join("source.f90").exists());
@@ -5089,6 +5340,114 @@ end
50895340
         let _ = fs::remove_file(source);
50905341
     }
50915342
 
5343
+    #[test]
5344
+    fn materializes_graph_input_in_declared_file_order() {
5345
+        let root = std::env::temp_dir().join("afs_tests_graph_materialize");
5346
+        let _ = fs::remove_dir_all(&root);
5347
+        fs::create_dir_all(&root).unwrap();
5348
+        let module = root.join("math_values.f90");
5349
+        let main = root.join("main.f90");
5350
+        fs::write(&module, "module math_values\ncontains\nend module\n").unwrap();
5351
+        fs::write(&main, "program main\nuse math_values\nend program\n").unwrap();
5352
+
5353
+        let suite = SuiteSpec {
5354
+            name: "modules/graph".into(),
5355
+            path: root.join("graph.afs"),
5356
+            cases: Vec::new(),
5357
+        };
5358
+        let case = CaseSpec {
5359
+            name: "basic_use".into(),
5360
+            source: main.clone(),
5361
+            graph_files: vec![module.clone(), main.clone()],
5362
+            requested: BTreeSet::from([Stage::Run]),
5363
+            opt_levels: vec![OptLevel::O0],
5364
+            repeat_count: 2,
5365
+            reference_compilers: Vec::new(),
5366
+            consistency_checks: Vec::new(),
5367
+            expectations: Vec::new(),
5368
+            status_rules: Vec::new(),
5369
+        };
5370
+
5371
+        let prepared = prepare_case_input(&case, &suite, OptLevel::O0).unwrap();
5372
+        let generated = fs::read_to_string(&prepared.compiler_source).unwrap();
5373
+        assert!(generated.contains("module math_values"));
5374
+        assert!(generated.contains("program main"));
5375
+        assert!(
5376
+            generated.find("module math_values").unwrap() < generated.find("program main").unwrap()
5377
+        );
5378
+
5379
+        cleanup_prepared_input(&prepared);
5380
+        let _ = fs::remove_dir_all(&root);
5381
+    }
5382
+
5383
+    #[test]
5384
+    fn graph_failure_bundle_writes_authored_sources() {
5385
+        let root = std::env::temp_dir().join("afs_tests_graph_bundle");
5386
+        let _ = fs::remove_dir_all(&root);
5387
+        fs::create_dir_all(&root).unwrap();
5388
+        let module = root.join("math_values.f90");
5389
+        let main = root.join("main.f90");
5390
+        let generated = root.join("generated.f90");
5391
+        fs::write(
5392
+            &module,
5393
+            "module math_values\n integer :: answer = 42\nend module\n",
5394
+        )
5395
+        .unwrap();
5396
+        fs::write(
5397
+            &main,
5398
+            "program main\n use math_values\n print *, answer\nend program\n",
5399
+        )
5400
+        .unwrap();
5401
+        fs::write(&generated, "module math_values\n integer :: answer = 42\nend module\n\nprogram main\n use math_values\n print *, answer\nend program\n").unwrap();
5402
+
5403
+        let suite = SuiteSpec {
5404
+            name: "modules/bundles".into(),
5405
+            path: root.join("bundle.afs"),
5406
+            cases: Vec::new(),
5407
+        };
5408
+        let case = CaseSpec {
5409
+            name: "graph_bundle".into(),
5410
+            source: main.clone(),
5411
+            graph_files: vec![module.clone(), main.clone()],
5412
+            requested: BTreeSet::from([Stage::Run]),
5413
+            opt_levels: vec![OptLevel::O0],
5414
+            repeat_count: 2,
5415
+            reference_compilers: Vec::new(),
5416
+            consistency_checks: Vec::new(),
5417
+            expectations: Vec::new(),
5418
+            status_rules: Vec::new(),
5419
+        };
5420
+        let outcome = Outcome {
5421
+            suite: suite.name.clone(),
5422
+            case: case.name.clone(),
5423
+            opt_level: OptLevel::O0,
5424
+            kind: OutcomeKind::Fail,
5425
+            detail: "boom".into(),
5426
+            bundle: None,
5427
+            consistency_observations: Vec::new(),
5428
+        };
5429
+        let artifacts = ExecutionArtifacts {
5430
+            requested: BTreeSet::from([Stage::Run]),
5431
+            armfortas: Some(run_only_result("42\n", "", 0)),
5432
+            armfortas_failure: None,
5433
+            references: Vec::new(),
5434
+            consistency_issues: Vec::new(),
5435
+        };
5436
+        let prepared = PreparedInput {
5437
+            compiler_source: generated.clone(),
5438
+            generated_source: Some(generated.clone()),
5439
+            temp_root: None,
5440
+        };
5441
+
5442
+        let bundle = write_failure_bundle(&suite, &case, &prepared, &outcome, &artifacts).unwrap();
5443
+        assert!(bundle.join("source.f90").exists());
5444
+        assert!(bundle.join("sources").join("00_math_values.f90").exists());
5445
+        assert!(bundle.join("sources").join("01_main.f90").exists());
5446
+
5447
+        let _ = fs::remove_dir_all(bundle);
5448
+        let _ = fs::remove_dir_all(&root);
5449
+    }
5450
+
50925451
     #[test]
50935452
     fn render_summary_includes_consistency_rollups() {
50945453
         let mut summary = Summary::default();