tenseleyflow/bencch / cd9eeae

Browse files

Expand compiler discovery

Authored by espadonne
SHA
cd9eeae213ecb51099459ebdf4cc560cad826a50
Parents
5571124
Tree
5b3f95d

2 changed files

StatusFile+-
M bench-core/src/lib.rs 62 0
M bench/src/lib.rs 603 110
bench-core/src/lib.rsmodified
@@ -258,14 +258,32 @@ pub enum NamedCompiler {
258258
     Armfortas,
259259
     Gfortran,
260260
     FlangNew,
261
+    LFortran,
262
+    Ifort,
263
+    Ifx,
264
+    Nvfortran,
261265
 }
262266
 
263267
 impl NamedCompiler {
268
+    pub const ALL: [Self; 7] = [
269
+        Self::Armfortas,
270
+        Self::Gfortran,
271
+        Self::FlangNew,
272
+        Self::LFortran,
273
+        Self::Ifort,
274
+        Self::Ifx,
275
+        Self::Nvfortran,
276
+    ];
277
+
264278
     pub fn parse(name: &str) -> Option<Self> {
265279
         match name.trim().to_ascii_lowercase().as_str() {
266280
             "armfortas" | "afs" => Some(Self::Armfortas),
267281
             "gfortran" => Some(Self::Gfortran),
268282
             "flang-new" | "flang_new" | "flang" => Some(Self::FlangNew),
283
+            "lfortran" => Some(Self::LFortran),
284
+            "ifort" => Some(Self::Ifort),
285
+            "ifx" => Some(Self::Ifx),
286
+            "nvfortran" | "pgfortran" => Some(Self::Nvfortran),
269287
             _ => None,
270288
         }
271289
     }
@@ -275,6 +293,34 @@ impl NamedCompiler {
275293
             Self::Armfortas => "armfortas",
276294
             Self::Gfortran => "gfortran",
277295
             Self::FlangNew => "flang-new",
296
+            Self::LFortran => "lfortran",
297
+            Self::Ifort => "ifort",
298
+            Self::Ifx => "ifx",
299
+            Self::Nvfortran => "nvfortran",
300
+        }
301
+    }
302
+
303
+    pub fn accepted_names(&self) -> &'static [&'static str] {
304
+        match self {
305
+            Self::Armfortas => &["armfortas", "afs"],
306
+            Self::Gfortran => &["gfortran"],
307
+            Self::FlangNew => &["flang-new", "flang_new", "flang"],
308
+            Self::LFortran => &["lfortran"],
309
+            Self::Ifort => &["ifort"],
310
+            Self::Ifx => &["ifx"],
311
+            Self::Nvfortran => &["nvfortran", "pgfortran"],
312
+        }
313
+    }
314
+
315
+    pub fn candidate_binaries(&self) -> &'static [&'static str] {
316
+        match self {
317
+            Self::Armfortas => &["armfortas", "afs"],
318
+            Self::Gfortran => &["gfortran"],
319
+            Self::FlangNew => &["flang-new", "flang"],
320
+            Self::LFortran => &["lfortran"],
321
+            Self::Ifort => &["ifort"],
322
+            Self::Ifx => &["ifx"],
323
+            Self::Nvfortran => &["nvfortran", "pgfortran"],
278324
         }
279325
     }
280326
 }
@@ -509,10 +555,26 @@ mod tests {
509555
             CompilerSpec::parse("armfortas"),
510556
             CompilerSpec::Named(NamedCompiler::Armfortas)
511557
         );
558
+        assert_eq!(
559
+            CompilerSpec::parse("afs"),
560
+            CompilerSpec::Named(NamedCompiler::Armfortas)
561
+        );
512562
         assert_eq!(
513563
             CompilerSpec::parse("flang-new"),
514564
             CompilerSpec::Named(NamedCompiler::FlangNew)
515565
         );
566
+        assert_eq!(
567
+            CompilerSpec::parse("lfortran"),
568
+            CompilerSpec::Named(NamedCompiler::LFortran)
569
+        );
570
+        assert_eq!(
571
+            CompilerSpec::parse("ifx"),
572
+            CompilerSpec::Named(NamedCompiler::Ifx)
573
+        );
574
+        assert_eq!(
575
+            CompilerSpec::parse("pgfortran"),
576
+            CompilerSpec::Named(NamedCompiler::Nvfortran)
577
+        );
516578
         assert_eq!(
517579
             CompilerSpec::parse("/tmp/compiler"),
518580
             CompilerSpec::Binary(PathBuf::from("/tmp/compiler"))
bench/src/lib.rsmodified
1164 lines changed — click to load
@@ -41,6 +41,7 @@ struct CaseSpec {
4141
     consistency_checks: Vec<ConsistencyCheck>,
4242
     expectations: Vec<Expectation>,
4343
     status_rules: Vec<StatusRule>,
44
+    capability_policy: Option<CapabilityPolicy>,
4445
 }
4546
 
4647
 impl CaseSpec {
@@ -96,6 +97,12 @@ struct StatusRule {
9697
     reason: String,
9798
 }
9899
 
100
+#[derive(Debug, Clone)]
101
+struct CapabilityPolicy {
102
+    kind: StatusKind,
103
+    reason: String,
104
+}
105
+
99106
 #[derive(Debug, Clone)]
100107
 enum PendingStatusRule {
101108
     Explicit(StatusRule),
@@ -273,6 +280,10 @@ struct ToolchainConfig {
273280
     armfortas: ArmfortasCliAdapter,
274281
     gfortran: String,
275282
     flang_new: String,
283
+    lfortran: String,
284
+    ifort: String,
285
+    ifx: String,
286
+    nvfortran: String,
276287
     system_as: String,
277288
     otool: String,
278289
     nm: String,
@@ -288,6 +299,10 @@ impl ToolchainConfig {
288299
             },
289300
             gfortran: tool_override("BENCCH_GFORTRAN_BIN", "gfortran"),
290301
             flang_new: tool_override("BENCCH_FLANG_BIN", "flang-new"),
302
+            lfortran: tool_override("BENCCH_LFORTRAN_BIN", "lfortran"),
303
+            ifort: tool_override("BENCCH_IFORT_BIN", "ifort"),
304
+            ifx: tool_override("BENCCH_IFX_BIN", "ifx"),
305
+            nvfortran: tool_override("BENCCH_NVFORTRAN_BIN", "nvfortran"),
291306
             system_as: tool_override("BENCCH_AS_BIN", "as"),
292307
             otool: tool_override("BENCCH_OTOOL_BIN", "otool"),
293308
             nm: tool_override("BENCCH_NM_BIN", "nm"),
@@ -334,6 +349,10 @@ impl ToolchainConfig {
334349
             },
335350
             NamedCompiler::Gfortran => Some(self.gfortran.clone()),
336351
             NamedCompiler::FlangNew => Some(self.flang_new.clone()),
352
+            NamedCompiler::LFortran => Some(self.lfortran.clone()),
353
+            NamedCompiler::Ifort => Some(self.ifort.clone()),
354
+            NamedCompiler::Ifx => Some(self.ifx.clone()),
355
+            NamedCompiler::Nvfortran => Some(self.nvfortran.clone()),
337356
         }
338357
     }
339358
 }
@@ -419,6 +438,75 @@ fn capability_unavailable_summary(capabilities: &CompilerCapabilities) -> String
419438
         .join(", ")
420439
 }
421440
 
441
+fn named_compiler_status_value(
442
+    named: NamedCompiler,
443
+    tools: &ToolchainConfig,
444
+    capture_root: Option<&PathBuf>,
445
+) -> String {
446
+    match named {
447
+        NamedCompiler::Armfortas => match &tools.armfortas {
448
+            ArmfortasCliAdapter::Linked => capture_root
449
+                .map(|root| format!("linked via Cargo to {}", display_path(root)))
450
+                .unwrap_or_else(|| {
451
+                    "linked adapter requested but unavailable in this build".to_string()
452
+                }),
453
+            ArmfortasCliAdapter::External(binary) => tool_probe_status(binary, false),
454
+        },
455
+        _ => tool_probe_status(
456
+            &tools
457
+                .named_compiler_binary(named)
458
+                .unwrap_or_else(|| named.as_str().to_string()),
459
+            false,
460
+        ),
461
+    }
462
+}
463
+
464
+fn append_named_compiler_fields(
465
+    fields: &mut Vec<(String, String)>,
466
+    named: NamedCompiler,
467
+    tools: &ToolchainConfig,
468
+    capture_root: Option<&PathBuf>,
469
+) {
470
+    let prefix = format!("named_compiler.{}", named.as_str());
471
+    let capabilities = compiler_capabilities(&CompilerSpec::Named(named), tools);
472
+    if named == NamedCompiler::Armfortas {
473
+        let armfortas = tools.armfortas_adapters();
474
+        fields.push((
475
+            prefix.clone(),
476
+            format!(
477
+                "cli={} capture={}",
478
+                armfortas.cli_mode_name(),
479
+                armfortas.capture_mode_name()
480
+            ),
481
+        ));
482
+    } else {
483
+        fields.push((
484
+            prefix.clone(),
485
+            named_compiler_status_value(named, tools, capture_root),
486
+        ));
487
+    }
488
+    fields.push((
489
+        format!("{}.accepted_names", prefix),
490
+        named.accepted_names().join(", "),
491
+    ));
492
+    fields.push((
493
+        format!("{}.candidate_binaries", prefix),
494
+        named.candidate_binaries().join(", "),
495
+    ));
496
+    fields.push((
497
+        format!("{}.generic_artifacts", prefix),
498
+        format_artifact_name_list(&capabilities.generic_artifacts()),
499
+    ));
500
+    fields.push((
501
+        format!("{}.adapter_extras", prefix),
502
+        capability_extra_summary(&capabilities.adapter_extras()),
503
+    ));
504
+    fields.push((
505
+        format!("{}.unavailable_artifacts", prefix),
506
+        capability_unavailable_summary(&capabilities),
507
+    ));
508
+}
509
+
422510
 fn compiler_capability_backend(spec: &CompilerSpec, tools: &ToolchainConfig) -> (String, String) {
423511
     match spec {
424512
         CompilerSpec::Named(NamedCompiler::Armfortas) => {
@@ -919,6 +1007,28 @@ fn parse_tool_override_arg(
9191007
             tools.flang_new = value.clone();
9201008
             Ok(true)
9211009
         }
1010
+        "--lfortran-bin" => {
1011
+            let value = queue.pop_front().ok_or("--lfortran-bin requires a value")?;
1012
+            tools.lfortran = value.clone();
1013
+            Ok(true)
1014
+        }
1015
+        "--ifort-bin" => {
1016
+            let value = queue.pop_front().ok_or("--ifort-bin requires a value")?;
1017
+            tools.ifort = value.clone();
1018
+            Ok(true)
1019
+        }
1020
+        "--ifx-bin" => {
1021
+            let value = queue.pop_front().ok_or("--ifx-bin requires a value")?;
1022
+            tools.ifx = value.clone();
1023
+            Ok(true)
1024
+        }
1025
+        "--nvfortran-bin" => {
1026
+            let value = queue
1027
+                .pop_front()
1028
+                .ok_or("--nvfortran-bin requires a value")?;
1029
+            tools.nvfortran = value.clone();
1030
+            Ok(true)
1031
+        }
9221032
         "--as-bin" => {
9231033
             let value = queue.pop_front().ok_or("--as-bin requires a value")?;
9241034
             tools.system_as = value.clone();
@@ -1207,12 +1317,13 @@ fn print_usage(program_name: &str) {
12071317
         program_name
12081318
     );
12091319
     eprintln!(
1210
-        "  {} doctor [--json-report <path>] [--markdown-report <path>] [--armfortas-bin <path>] [--gfortran-bin <path>] [--flang-bin <path>] [--as-bin <path>] [--otool-bin <path>] [--nm-bin <path>]",
1320
+        "  {} doctor [--json-report <path>] [--markdown-report <path>] [--armfortas-bin <path>] [--gfortran-bin <path>] [--flang-bin <path>] [--lfortran-bin <path>] [--ifort-bin <path>] [--ifx-bin <path>] [--nvfortran-bin <path>] [--as-bin <path>] [--otool-bin <path>] [--nm-bin <path>]",
12111321
         program_name
12121322
     );
12131323
     eprintln!();
12141324
     eprintln!("env overrides:");
1215
-    eprintln!("  BENCCH_ARMFORTAS_BIN, BENCCH_GFORTRAN_BIN, BENCCH_FLANG_BIN");
1325
+    eprintln!("  BENCCH_ARMFORTAS_BIN, BENCCH_GFORTRAN_BIN, BENCCH_FLANG_BIN, BENCCH_LFORTRAN_BIN");
1326
+    eprintln!("  BENCCH_IFORT_BIN, BENCCH_IFX_BIN, BENCCH_NVFORTRAN_BIN");
12161327
     eprintln!("  BENCCH_AS_BIN, BENCCH_OTOOL_BIN, BENCCH_NM_BIN");
12171328
     eprintln!();
12181329
     if linked_capture_available() {
@@ -1298,41 +1409,62 @@ fn capability_request_issue(
12981409
         return None;
12991410
     }
13001411
 
1301
-    let mut lines = vec![format!("{}:", spec.display_name())];
1302
-    for (artifact, reason) in unavailable {
1303
-        lines.push(format!("  unavailable {}: {}", artifact, reason));
1412
+    let mut sections = Vec::new();
1413
+    if !unavailable.is_empty() {
1414
+        let detail = unavailable
1415
+            .into_iter()
1416
+            .map(|(artifact, reason)| format!("requested {}: {}", artifact, reason))
1417
+            .collect::<Vec<_>>()
1418
+            .join("\n");
1419
+        sections.push(format!(
1420
+            "{} unavailable for requested artifacts in this build\n{}",
1421
+            spec.display_name(),
1422
+            detail
1423
+        ));
13041424
     }
13051425
     if !unsupported.is_empty() {
1306
-        lines.push(format!(
1307
-            "  unsupported in this adapter: {}",
1426
+        sections.push(format!(
1427
+            "{} does not support requested artifacts in this adapter: {}",
1428
+            spec.display_name(),
13081429
             unsupported.join(", ")
13091430
         ));
13101431
     }
1311
-    Some(lines.join("\n"))
1432
+    Some(sections.join("\n"))
13121433
 }
13131434
 
1314
-fn preflight_compare_request(
1315
-    config: &CompareConfig,
1435
+fn compare_capability_issue(
1436
+    left: &CompilerSpec,
1437
+    right: &CompilerSpec,
13161438
     requested: &BTreeSet<ArtifactKey>,
1317
-) -> Result<(), String> {
1439
+    tools: &ToolchainConfig,
1440
+) -> Option<String> {
13181441
     let mut issues = Vec::new();
1319
-    if let Some(issue) = capability_request_issue(&config.left, requested, &config.tools) {
1320
-        issues.push(format!("left {}\n{}", config.left.display_name(), issue));
1442
+    if let Some(issue) = capability_request_issue(left, requested, tools) {
1443
+        issues.push(format!("left:\n{}", issue));
13211444
     }
1322
-    if let Some(issue) = capability_request_issue(&config.right, requested, &config.tools) {
1323
-        issues.push(format!("right {}\n{}", config.right.display_name(), issue));
1445
+    if let Some(issue) = capability_request_issue(right, requested, tools) {
1446
+        issues.push(format!("right:\n{}", issue));
13241447
     }
1325
-
13261448
     if issues.is_empty() {
1327
-        Ok(())
1449
+        None
13281450
     } else {
1329
-        Err(format!(
1451
+        Some(format!(
13301452
             "compare request is not supported for the selected compiler surfaces\n{}",
13311453
             issues.join("\n")
13321454
         ))
13331455
     }
13341456
 }
13351457
 
1458
+fn preflight_compare_request(
1459
+    config: &CompareConfig,
1460
+    requested: &BTreeSet<ArtifactKey>,
1461
+) -> Result<(), String> {
1462
+    match compare_capability_issue(&config.left, &config.right, requested, &config.tools) {
1463
+        Some(issue) => Err(issue),
1464
+        None => Ok(()),
1465
+    }
1466
+}
1467
+
13361468
 fn run_introspect(config: &IntrospectConfig) -> Result<ObservedProgram, String> {
13371469
     let requested = if config.artifacts.is_empty() {
13381470
         default_introspection_artifacts(&config.compiler, config.all_artifacts)
@@ -3191,58 +3323,9 @@ fn doctor_report_fields(config: &DoctorConfig) -> Vec<(String, String)> {
31913323
         "primary_backend_selection".to_string(),
31923324
         "observable backend is selected for asm/obj/run-only cells when the armfortas CLI is external and the case does not require expect-fail or capture-consistency semantics; otherwise full backend".to_string(),
31933325
     ));
3194
-    fields.push((
3195
-        "named_compiler.armfortas".to_string(),
3196
-        format!(
3197
-            "cli={} capture={}",
3198
-            armfortas.cli_mode_name(),
3199
-            armfortas.capture_mode_name()
3200
-        ),
3201
-    ));
3202
-    let armfortas_capabilities = compiler_capabilities(
3203
-        &CompilerSpec::Named(NamedCompiler::Armfortas),
3204
-        &config.tools,
3205
-    );
3206
-    fields.push((
3207
-        "named_compiler.armfortas.generic_artifacts".to_string(),
3208
-        format_artifact_name_list(&armfortas_capabilities.generic_artifacts()),
3209
-    ));
3210
-    fields.push((
3211
-        "named_compiler.armfortas.adapter_extras".to_string(),
3212
-        capability_extra_summary(&armfortas_capabilities.adapter_extras()),
3213
-    ));
3214
-    fields.push((
3215
-        "named_compiler.armfortas.unavailable_artifacts".to_string(),
3216
-        capability_unavailable_summary(&armfortas_capabilities),
3217
-    ));
3218
-    fields.push((
3219
-        "named_compiler.gfortran".to_string(),
3220
-        tool_probe_status(&config.tools.gfortran, false),
3221
-    ));
3222
-    let gfortran_capabilities =
3223
-        compiler_capabilities(&CompilerSpec::Named(NamedCompiler::Gfortran), &config.tools);
3224
-    fields.push((
3225
-        "named_compiler.gfortran.generic_artifacts".to_string(),
3226
-        format_artifact_name_list(&gfortran_capabilities.generic_artifacts()),
3227
-    ));
3228
-    fields.push((
3229
-        "named_compiler.gfortran.adapter_extras".to_string(),
3230
-        capability_extra_summary(&gfortran_capabilities.adapter_extras()),
3231
-    ));
3232
-    fields.push((
3233
-        "named_compiler.flang-new".to_string(),
3234
-        tool_probe_status(&config.tools.flang_new, false),
3235
-    ));
3236
-    let flang_capabilities =
3237
-        compiler_capabilities(&CompilerSpec::Named(NamedCompiler::FlangNew), &config.tools);
3238
-    fields.push((
3239
-        "named_compiler.flang-new.generic_artifacts".to_string(),
3240
-        format_artifact_name_list(&flang_capabilities.generic_artifacts()),
3241
-    ));
3242
-    fields.push((
3243
-        "named_compiler.flang-new.adapter_extras".to_string(),
3244
-        capability_extra_summary(&flang_capabilities.adapter_extras()),
3245
-    ));
3326
+    for named in NamedCompiler::ALL {
3327
+        append_named_compiler_fields(&mut fields, named, &config.tools, capture_root.as_ref());
3328
+    }
32463329
     fields.push((
32473330
         "explicit_compiler_path".to_string(),
32483331
         "any filesystem path passed to compare/introspect uses the generic external-driver adapter"
@@ -3268,6 +3351,22 @@ fn doctor_report_fields(config: &DoctorConfig) -> Vec<(String, String)> {
32683351
         "flang-new".to_string(),
32693352
         tool_probe_status(&config.tools.flang_new, false),
32703353
     ));
3354
+    fields.push((
3355
+        "lfortran".to_string(),
3356
+        tool_probe_status(&config.tools.lfortran, false),
3357
+    ));
3358
+    fields.push((
3359
+        "ifort".to_string(),
3360
+        tool_probe_status(&config.tools.ifort, false),
3361
+    ));
3362
+    fields.push((
3363
+        "ifx".to_string(),
3364
+        tool_probe_status(&config.tools.ifx, false),
3365
+    ));
3366
+    fields.push((
3367
+        "nvfortran".to_string(),
3368
+        tool_probe_status(&config.tools.nvfortran, false),
3369
+    ));
32713370
     fields.push((
32723371
         "as".to_string(),
32733372
         tool_probe_status(&config.tools.system_as, false),
@@ -3362,6 +3461,51 @@ fn json_string_vec_map(map: &BTreeMap<String, Vec<String>>) -> String {
33623461
     rendered
33633462
 }
33643463
 
3464
+fn render_named_compiler_entry_json(
3465
+    named: NamedCompiler,
3466
+    tools: &ToolchainConfig,
3467
+    capture_root: Option<&PathBuf>,
3468
+) -> String {
3469
+    let capabilities = compiler_capabilities(&CompilerSpec::Named(named), tools);
3470
+    let mut fields = vec![
3471
+        format!(
3472
+            "\"accepted_names\": {}",
3473
+            json_string_iter(named.accepted_names().iter().copied())
3474
+        ),
3475
+        format!(
3476
+            "\"candidate_binaries\": {}",
3477
+            json_string_iter(named.candidate_binaries().iter().copied())
3478
+        ),
3479
+        format!(
3480
+            "\"capabilities\": {}",
3481
+            render_doctor_capabilities_json(&capabilities)
3482
+        ),
3483
+    ];
3484
+    if named == NamedCompiler::Armfortas {
3485
+        let armfortas = tools.armfortas_adapters();
3486
+        fields.insert(
3487
+            0,
3488
+            format!(
3489
+                "\"surface\": \"{}\"",
3490
+                json_escape(&format!(
3491
+                    "cli={} capture={}",
3492
+                    armfortas.cli_mode_name(),
3493
+                    armfortas.capture_mode_name()
3494
+                ))
3495
+            ),
3496
+        );
3497
+    } else {
3498
+        fields.insert(
3499
+            0,
3500
+            format!(
3501
+                "\"status\": \"{}\"",
3502
+                json_escape(&named_compiler_status_value(named, tools, capture_root))
3503
+            ),
3504
+        );
3505
+    }
3506
+    format!("{{{}}}", fields.join(", "))
3507
+}
3508
+
33653509
 fn render_doctor_json(config: &DoctorConfig) -> String {
33663510
     let fields = doctor_report_fields(config);
33673511
     let workspace_root = workspace_root();
@@ -3373,18 +3517,26 @@ fn render_doctor_json(config: &DoctorConfig) -> String {
33733517
         .cli_observable_capture_backend(report_root.join(".tmp").join("doctor"));
33743518
     let capture_root = armfortas.capture_root();
33753519
     let capture_manifest = capture_root.as_ref().map(|root| root.join("Cargo.toml"));
3376
-    let armfortas_capabilities = compiler_capabilities(
3377
-        &CompilerSpec::Named(NamedCompiler::Armfortas),
3378
-        &config.tools,
3379
-    );
3380
-    let gfortran_capabilities =
3381
-        compiler_capabilities(&CompilerSpec::Named(NamedCompiler::Gfortran), &config.tools);
3382
-    let flang_capabilities =
3383
-        compiler_capabilities(&CompilerSpec::Named(NamedCompiler::FlangNew), &config.tools);
33843520
     let explicit_capabilities = compiler_capabilities(
33853521
         &CompilerSpec::Binary(PathBuf::from("/path/to/compiler")),
33863522
         &config.tools,
33873523
     );
3524
+    let named_entries = NamedCompiler::ALL
3525
+        .iter()
3526
+        .enumerate()
3527
+        .map(|(index, named)| {
3528
+            format!(
3529
+                "    \"{}\": {}{}",
3530
+                named.as_str(),
3531
+                render_named_compiler_entry_json(*named, &config.tools, capture_root.as_ref()),
3532
+                if index + 1 == NamedCompiler::ALL.len() {
3533
+                    ""
3534
+                } else {
3535
+                    ","
3536
+                }
3537
+            )
3538
+        })
3539
+        .collect::<Vec<_>>();
33883540
     let mut lines = vec![
33893541
         "{".to_string(),
33903542
         "  \"command\": \"doctor\",".to_string(),
@@ -3483,25 +3635,9 @@ fn render_doctor_json(config: &DoctorConfig) -> String {
34833635
         ),
34843636
         "  },".to_string(),
34853637
         "  \"named_compilers\": {".to_string(),
3486
-        format!(
3487
-            "    \"armfortas\": {{\"surface\":\"{}\",\"capabilities\":{}}},",
3488
-            json_escape(&format!(
3489
-                "cli={} capture={}",
3490
-                armfortas.cli_mode_name(),
3491
-                armfortas.capture_mode_name()
3492
-            )),
3493
-            render_doctor_capabilities_json(&armfortas_capabilities)
3494
-        ),
3495
-        format!(
3496
-            "    \"gfortran\": {{\"status\":\"{}\",\"capabilities\":{}}},",
3497
-            json_escape(&tool_probe_status(&config.tools.gfortran, false)),
3498
-            render_doctor_capabilities_json(&gfortran_capabilities)
3499
-        ),
3500
-        format!(
3501
-            "    \"flang-new\": {{\"status\":\"{}\",\"capabilities\":{}}}",
3502
-            json_escape(&tool_probe_status(&config.tools.flang_new, false)),
3503
-            render_doctor_capabilities_json(&flang_capabilities)
3504
-        ),
3638
+    ];
3639
+    lines.extend(named_entries);
3640
+    lines.extend([
35053641
         "  },".to_string(),
35063642
         format!(
35073643
             "  \"explicit_compiler_path\": {{\"description\":\"{}\",\"capabilities\":{}}},",
@@ -3519,6 +3655,22 @@ fn render_doctor_json(config: &DoctorConfig) -> String {
35193655
             "    \"flang-new\": \"{}\",",
35203656
             json_escape(&tool_probe_status(&config.tools.flang_new, false))
35213657
         ),
3658
+        format!(
3659
+            "    \"lfortran\": \"{}\",",
3660
+            json_escape(&tool_probe_status(&config.tools.lfortran, false))
3661
+        ),
3662
+        format!(
3663
+            "    \"ifort\": \"{}\",",
3664
+            json_escape(&tool_probe_status(&config.tools.ifort, false))
3665
+        ),
3666
+        format!(
3667
+            "    \"ifx\": \"{}\",",
3668
+            json_escape(&tool_probe_status(&config.tools.ifx, false))
3669
+        ),
3670
+        format!(
3671
+            "    \"nvfortran\": \"{}\",",
3672
+            json_escape(&tool_probe_status(&config.tools.nvfortran, false))
3673
+        ),
35223674
         format!(
35233675
             "    \"as\": \"{}\",",
35243676
             json_escape(&tool_probe_status(&config.tools.system_as, false))
@@ -3581,7 +3733,7 @@ fn render_doctor_json(config: &DoctorConfig) -> String {
35813733
         ),
35823734
         "  },".to_string(),
35833735
         "  \"fields\": {".to_string(),
3584
-    ];
3736
+    ]);
35853737
     for (index, (field, value)) in fields.iter().enumerate() {
35863738
         lines.push(format!(
35873739
             "    \"{}\": \"{}\"{}",
@@ -3789,6 +3941,34 @@ fn parse_suite_file(path: &Path) -> Result<SuiteSpec, String> {
37893941
             builder
37903942
                 .expectations
37913943
                 .push(parse_expectation(rest, path, line_no)?);
3944
+        } else if let Some(rest) = line.strip_prefix("xfail capability ") {
3945
+            if builder.capability_policy.is_some() {
3946
+                return Err(format!(
3947
+                    "{}:{}: duplicate capability policy",
3948
+                    path.display(),
3949
+                    line_no
3950
+                ));
3951
+            }
3952
+            builder.capability_policy = Some(parse_capability_policy(
3953
+                StatusKind::Xfail,
3954
+                rest,
3955
+                path,
3956
+                line_no,
3957
+            )?);
3958
+        } else if let Some(rest) = line.strip_prefix("future capability ") {
3959
+            if builder.capability_policy.is_some() {
3960
+                return Err(format!(
3961
+                    "{}:{}: duplicate capability policy",
3962
+                    path.display(),
3963
+                    line_no
3964
+                ));
3965
+            }
3966
+            builder.capability_policy = Some(parse_capability_policy(
3967
+                StatusKind::Future,
3968
+                rest,
3969
+                path,
3970
+                line_no,
3971
+            )?);
37923972
         } else if let Some(rest) = line.strip_prefix("xfail ") {
37933973
             builder
37943974
                 .status_rules
@@ -3840,6 +4020,7 @@ struct CaseBuilder {
38404020
     consistency_checks: Vec<ConsistencyCheck>,
38414021
     expectations: Vec<Expectation>,
38424022
     status_rules: Vec<PendingStatusRule>,
4023
+    capability_policy: Option<CapabilityPolicy>,
38434024
 }
38444025
 
38454026
 impl CaseBuilder {
@@ -3860,6 +4041,7 @@ impl CaseBuilder {
38604041
             consistency_checks: Vec::new(),
38614042
             expectations: Vec::new(),
38624043
             status_rules: Vec::new(),
4044
+            capability_policy: None,
38634045
         }
38644046
     }
38654047
 
@@ -4052,6 +4234,7 @@ impl CaseBuilder {
40524234
             consistency_checks: self.consistency_checks,
40534235
             expectations,
40544236
             status_rules,
4237
+            capability_policy: self.capability_policy,
40554238
         })
40564239
     }
40574240
 }
@@ -4435,6 +4618,18 @@ fn parse_status_rule(
44354618
     }))
44364619
 }
44374620
 
4621
+fn parse_capability_policy(
4622
+    kind: StatusKind,
4623
+    rest: &str,
4624
+    path: &Path,
4625
+    line_no: usize,
4626
+) -> Result<CapabilityPolicy, String> {
4627
+    Ok(CapabilityPolicy {
4628
+        kind,
4629
+        reason: parse_quoted(rest.trim(), path, line_no)?,
4630
+    })
4631
+}
4632
+
44384633
 fn resolve_source_comment_expectations(
44394634
     expectations: Vec<Expectation>,
44404635
     source_text: Option<&str>,
@@ -4678,6 +4873,16 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String>
46784873
         format!("source: {}", case.source_label()),
46794874
         format!("opts: {}", format_opt_level_list(&case.opt_levels)),
46804875
     ];
4876
+    if let Some(policy) = &case.capability_policy {
4877
+        lines.push(format!(
4878
+            "capability_policy: {} when blocked ({})",
4879
+            match policy.kind {
4880
+                StatusKind::Xfail => "xfail",
4881
+                StatusKind::Future => "future",
4882
+            },
4883
+            policy.reason
4884
+        ));
4885
+    }
46814886
 
46824887
     if let Some(generic) = &case.generic_introspect {
46834888
         lines.push(format!("compiler: {}", generic.compiler.display_name()));
@@ -4693,7 +4898,11 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String>
46934898
         ));
46944899
         match capability_request_issue(&generic.compiler, &generic.artifacts, tools) {
46954900
             Some(issue) => {
4696
-                lines.push("capability_status: blocked".to_string());
4901
+                lines.push(if case.capability_policy.is_some() {
4902
+                    "capability_status: deferred".to_string()
4903
+                } else {
4904
+                    "capability_status: blocked".to_string()
4905
+                });
46974906
                 lines.extend(
46984907
                     issue
46994908
                         .lines()
@@ -4723,15 +4932,19 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String>
47234932
         ));
47244933
         let mut issues = Vec::new();
47254934
         if let Some(issue) = capability_request_issue(&generic.left, &generic.artifacts, tools) {
4726
-            issues.push(format!("left {}", issue));
4935
+            issues.push(format!("left:\n{}", issue));
47274936
         }
47284937
         if let Some(issue) = capability_request_issue(&generic.right, &generic.artifacts, tools) {
4729
-            issues.push(format!("right {}", issue));
4938
+            issues.push(format!("right:\n{}", issue));
47304939
         }
47314940
         if issues.is_empty() {
47324941
             lines.push("capability_status: ready".to_string());
47334942
         } else {
4734
-            lines.push("capability_status: blocked".to_string());
4943
+            lines.push(if case.capability_policy.is_some() {
4944
+                "capability_status: deferred".to_string()
4945
+            } else {
4946
+                "capability_status: blocked".to_string()
4947
+            });
47354948
             lines.extend(issues.into_iter().flat_map(|issue| {
47364949
                 issue
47374950
                     .lines()
@@ -4780,7 +4993,11 @@ fn case_discovery_lines(case: &CaseSpec, tools: &ToolchainConfig) -> Vec<String>
47804993
         if linked_capture_available() {
47814994
             lines.push("capability_status: ready".to_string());
47824995
         } else {
4783
-            lines.push("capability_status: blocked".to_string());
4996
+            lines.push(if case.capability_policy.is_some() {
4997
+                "capability_status: deferred".to_string()
4998
+            } else {
4999
+                "capability_status: blocked".to_string()
5000
+            });
47845001
             lines.push(
47855002
                 "capability_detail: linked armfortas capture is unavailable in this build"
47865003
                     .to_string(),
@@ -4907,7 +5124,7 @@ fn execute_case_cell(
49075124
             suite,
49085125
             case,
49095126
             opt_level,
4910
-            effective_status,
5127
+            capability_effective_status(&effective_status, case),
49115128
             Err(detail),
49125129
             Some(PrimaryBackendReport::from_selected(&selected_backend)),
49135130
             Vec::new(),
@@ -5168,6 +5385,26 @@ fn execute_generic_compare_case_cell(
51685385
         .ok_or_else(|| "missing generic compare case configuration".to_string())?;
51695386
     let prepared = prepare_case_input(case, suite, opt_level)?;
51705387
 
5388
+    if let Some(detail) = compare_capability_issue(
5389
+        &generic.left,
5390
+        &generic.right,
5391
+        &generic.artifacts,
5392
+        &config.tools,
5393
+    ) {
5394
+        let mut outcome = outcome_from_status_and_execution(
5395
+            suite,
5396
+            case,
5397
+            opt_level,
5398
+            capability_effective_status(&effective_status, case),
5399
+            Err(detail),
5400
+            None,
5401
+            Vec::new(),
5402
+        );
5403
+        outcome.detail = outcome.detail.trim().to_string();
5404
+        cleanup_prepared_input(&prepared);
5405
+        return Ok(outcome);
5406
+    }
5407
+
51715408
     if config.verbose {
51725409
         let artifacts = generic
51735410
             .artifacts
@@ -5274,6 +5511,7 @@ fn execute_generic_compare_case_cell(
52745511
     };
52755512
 
52765513
     outcome.detail = outcome.detail.trim().to_string();
5514
+    cleanup_prepared_input(&prepared);
52775515
     Ok(outcome)
52785516
 }
52795517
 
@@ -5305,6 +5543,23 @@ fn execute_generic_introspect_case_cell(
53055543
         .ok_or_else(|| "missing generic introspection case configuration".to_string())?;
53065544
     let prepared = prepare_case_input(case, suite, opt_level)?;
53075545
 
5546
+    if let Some(detail) =
5547
+        capability_request_issue(&generic.compiler, &generic.artifacts, &config.tools)
5548
+    {
5549
+        let mut outcome = outcome_from_status_and_execution(
5550
+            suite,
5551
+            case,
5552
+            opt_level,
5553
+            capability_effective_status(&effective_status, case),
5554
+            Err(detail),
5555
+            None,
5556
+            Vec::new(),
5557
+        );
5558
+        outcome.detail = outcome.detail.trim().to_string();
5559
+        cleanup_prepared_input(&prepared);
5560
+        return Ok(outcome);
5561
+    }
5562
+
53085563
     if config.verbose {
53095564
         let artifacts = generic
53105565
             .artifacts
@@ -5467,6 +5722,7 @@ fn execute_generic_introspect_case_cell(
54675722
     };
54685723
 
54695724
     outcome.detail = outcome.detail.trim().to_string();
5725
+    cleanup_prepared_input(&prepared);
54705726
     cleanup_consistency_issues(&consistency_issues);
54715727
     Ok(outcome)
54725728
 }
@@ -5616,6 +5872,19 @@ fn status_for_opt(case: &CaseSpec, opt_level: OptLevel) -> EffectiveStatus {
56165872
     status
56175873
 }
56185874
 
5875
+fn capability_effective_status(base: &EffectiveStatus, case: &CaseSpec) -> EffectiveStatus {
5876
+    match base {
5877
+        EffectiveStatus::Normal => match &case.capability_policy {
5878
+            Some(policy) => match policy.kind {
5879
+                StatusKind::Xfail => EffectiveStatus::Xfail(policy.reason.clone()),
5880
+                StatusKind::Future => EffectiveStatus::Future(policy.reason.clone()),
5881
+            },
5882
+            None => EffectiveStatus::Normal,
5883
+        },
5884
+        other => other.clone(),
5885
+    }
5886
+}
5887
+
56195888
 fn ensure_target_stage(expectation: &Expectation, requested: &mut BTreeSet<Stage>) {
56205889
     match expectation {
56215890
         Expectation::CheckComments(target)
@@ -6030,7 +6299,7 @@ fn outcome_from_status_and_execution(
60306299
             suite: suite.name.clone(),
60316300
             case: case.name.clone(),
60326301
             opt_level,
6033
-            kind: OutcomeKind::Pass,
6302
+            kind: OutcomeKind::Xpass,
60346303
             detail: reason,
60356304
             bundle: None,
60366305
             primary_backend,
@@ -6040,7 +6309,7 @@ fn outcome_from_status_and_execution(
60406309
             suite: suite.name.clone(),
60416310
             case: case.name.clone(),
60426311
             opt_level,
6043
-            kind: OutcomeKind::Fail,
6312
+            kind: OutcomeKind::Future,
60446313
             detail: format!("{}\n{}", reason, detail),
60456314
             bundle: None,
60466315
             primary_backend,
@@ -10810,12 +11079,17 @@ mod tests {
1081011079
                 needle: "42".into(),
1081111080
             }],
1081211081
             status_rules: Vec::new(),
11082
+            capability_policy: None,
1081311083
         };
1081411084
         let requested = BTreeSet::from([Stage::Run]);
1081511085
         let external_tools = ToolchainConfig {
1081611086
             armfortas: ArmfortasCliAdapter::External("/tmp/armfortas".into()),
1081711087
             gfortran: "gfortran".into(),
1081811088
             flang_new: "flang-new".into(),
11089
+            lfortran: "lfortran".into(),
11090
+            ifort: "ifort".into(),
11091
+            ifx: "ifx".into(),
11092
+            nvfortran: "nvfortran".into(),
1081911093
             system_as: "as".into(),
1082011094
             otool: "otool".into(),
1082111095
             nm: "nm".into(),
@@ -10886,6 +11160,7 @@ mod tests {
1088611160
                 needle: "program".into(),
1088711161
             }],
1088811162
             status_rules: Vec::new(),
11163
+            capability_policy: None,
1088911164
         };
1089011165
         let backend = SelectedPrimaryBackend {
1089111166
             kind: PrimaryCaptureBackendKind::Full,
@@ -10921,6 +11196,7 @@ mod tests {
1092111196
                 needle: "42".into(),
1092211197
             }],
1092311198
             status_rules: Vec::new(),
11199
+            capability_policy: None,
1092411200
         };
1092511201
         assert!(legacy_case_uses_generic_consistency_checks(&cli_only_case));
1092611202
 
@@ -10952,6 +11228,7 @@ mod tests {
1095211228
                 needle: "42".into(),
1095311229
             }],
1095411230
             status_rules: Vec::new(),
11231
+            capability_policy: None,
1095511232
         };
1095611233
         assert!(legacy_case_uses_generic_observation_execution(
1095711234
             &observable_case,
@@ -11022,6 +11299,7 @@ mod tests {
1102211299
                 },
1102311300
             ],
1102411301
             status_rules: Vec::new(),
11302
+            capability_policy: None,
1102511303
         };
1102611304
         let prepared = PreparedInput {
1102711305
             compiler_source: source.clone(),
@@ -11032,6 +11310,10 @@ mod tests {
1103211310
             armfortas: ArmfortasCliAdapter::External(compiler.display().to_string()),
1103311311
             gfortran: "gfortran".into(),
1103411312
             flang_new: "flang-new".into(),
11313
+            lfortran: "lfortran".into(),
11314
+            ifort: "ifort".into(),
11315
+            ifx: "ifx".into(),
11316
+            nvfortran: "nvfortran".into(),
1103511317
             system_as: "as".into(),
1103611318
             otool: "otool".into(),
1103711319
             nm: "nm".into(),
@@ -11143,7 +11425,8 @@ mod tests {
1114311425
 
1114411426
         let err = run_compare(&config).unwrap_err();
1114511427
         assert!(err.contains("compare request is not supported"));
11146
-        assert!(err.contains("right gfortran"));
11428
+        assert!(err.contains("right:"));
11429
+        assert!(err.contains("gfortran does not support requested artifacts"));
1114711430
         assert!(err.contains("armfortas.ir"));
1114811431
     }
1114911432
 
@@ -11728,6 +12011,7 @@ mod tests {
1172812011
                 needle: "func".into(),
1172912012
             }],
1173012013
             status_rules: Vec::new(),
12014
+            capability_policy: None,
1173112015
         };
1173212016
         let config = RunConfig {
1173312017
             suite_filter: None,
@@ -11750,6 +12034,55 @@ mod tests {
1175012034
         assert!(!outcome.detail.contains("gfortran failed"));
1175112035
     }
1175212036
 
12037
+    #[test]
12038
+    fn execute_generic_introspect_case_applies_future_capability_policy() {
12039
+        let suite = SuiteSpec {
12040
+            name: "v2/capability-policy".into(),
12041
+            path: PathBuf::from("suite.afs"),
12042
+            cases: Vec::new(),
12043
+        };
12044
+        let case = CaseSpec {
12045
+            name: "gfortran-armfortas-ir".into(),
12046
+            source: runtime_fixture("if_else.f90"),
12047
+            graph_files: Vec::new(),
12048
+            requested: BTreeSet::new(),
12049
+            generic_introspect: Some(GenericIntrospectCase {
12050
+                compiler: CompilerSpec::Named(NamedCompiler::Gfortran),
12051
+                artifacts: BTreeSet::from([ArtifactKey::Extra("armfortas.ir".into())]),
12052
+            }),
12053
+            generic_compare: None,
12054
+            opt_levels: vec![OptLevel::O0],
12055
+            repeat_count: 2,
12056
+            reference_compilers: Vec::new(),
12057
+            consistency_checks: Vec::new(),
12058
+            expectations: Vec::new(),
12059
+            status_rules: Vec::new(),
12060
+            capability_policy: Some(CapabilityPolicy {
12061
+                kind: StatusKind::Future,
12062
+                reason: "generic gfortran surface has no armfortas extras".into(),
12063
+            }),
12064
+        };
12065
+        let config = RunConfig {
12066
+            suite_filter: None,
12067
+            case_filter: None,
12068
+            opt_filter: None,
12069
+            verbose: false,
12070
+            fail_fast: false,
12071
+            include_future: false,
12072
+            all_stages: false,
12073
+            json_report: None,
12074
+            markdown_report: None,
12075
+            tools: ToolchainConfig::from_env(),
12076
+        };
12077
+
12078
+        let outcome = execute_case_cell(&suite, &case, OptLevel::O0, &config).unwrap();
12079
+        assert_eq!(outcome.kind, OutcomeKind::Future);
12080
+        assert!(outcome
12081
+            .detail
12082
+            .contains("generic gfortran surface has no armfortas extras"));
12083
+        assert!(outcome.detail.contains("armfortas.ir"));
12084
+    }
12085
+
1175312086
     #[cfg(unix)]
1175412087
     #[test]
1175512088
     fn execute_generic_suite_case_uses_introspect_engine() {
@@ -11790,6 +12123,7 @@ mod tests {
1179012123
                 },
1179112124
             ],
1179212125
             status_rules: Vec::new(),
12126
+            capability_policy: None,
1179312127
         };
1179412128
         let config = RunConfig {
1179512129
             suite_filter: None,
@@ -11849,6 +12183,7 @@ mod tests {
1184912183
                 },
1185012184
             ],
1185112185
             status_rules: Vec::new(),
12186
+            capability_policy: None,
1185212187
         };
1185312188
         let config = RunConfig {
1185412189
             suite_filter: None,
@@ -11906,6 +12241,7 @@ mod tests {
1190612241
                 },
1190712242
             ],
1190812243
             status_rules: Vec::new(),
12244
+            capability_policy: None,
1190912245
         };
1191012246
         let config = RunConfig {
1191112247
             suite_filter: None,
@@ -11980,6 +12316,7 @@ mod tests {
1198012316
                 },
1198112317
             ],
1198212318
             status_rules: Vec::new(),
12319
+            capability_policy: None,
1198312320
         };
1198412321
         let config = RunConfig {
1198512322
             suite_filter: None,
@@ -12029,6 +12366,7 @@ mod tests {
1202912366
                 value: "match".into(),
1203012367
             }],
1203112368
             status_rules: Vec::new(),
12369
+            capability_policy: None,
1203212370
         };
1203312371
         let config = RunConfig {
1203412372
             suite_filter: None,
@@ -12049,6 +12387,56 @@ mod tests {
1204912387
         assert!(outcome.detail.contains("armfortas.ir"));
1205012388
     }
1205112389
 
12390
+    #[test]
12391
+    fn execute_generic_compare_suite_case_applies_xfail_capability_policy() {
12392
+        let suite = SuiteSpec {
12393
+            name: "v2/capability-policy".into(),
12394
+            path: PathBuf::from("suite.afs"),
12395
+            cases: Vec::new(),
12396
+        };
12397
+        let case = CaseSpec {
12398
+            name: "armfortas-ir-vs-gfortran".into(),
12399
+            source: runtime_fixture("if_else.f90"),
12400
+            graph_files: Vec::new(),
12401
+            requested: BTreeSet::new(),
12402
+            generic_introspect: None,
12403
+            generic_compare: Some(GenericCompareCase {
12404
+                left: CompilerSpec::Named(NamedCompiler::Armfortas),
12405
+                right: CompilerSpec::Named(NamedCompiler::Gfortran),
12406
+                artifacts: BTreeSet::from([ArtifactKey::Extra("armfortas.ir".into())]),
12407
+            }),
12408
+            opt_levels: vec![OptLevel::O0],
12409
+            repeat_count: 2,
12410
+            reference_compilers: Vec::new(),
12411
+            consistency_checks: Vec::new(),
12412
+            expectations: Vec::new(),
12413
+            status_rules: Vec::new(),
12414
+            capability_policy: Some(CapabilityPolicy {
12415
+                kind: StatusKind::Xfail,
12416
+                reason: "mixed-surface namespaced compare stays soft for now".into(),
12417
+            }),
12418
+        };
12419
+        let config = RunConfig {
12420
+            suite_filter: None,
12421
+            case_filter: None,
12422
+            opt_filter: None,
12423
+            verbose: false,
12424
+            fail_fast: false,
12425
+            include_future: false,
12426
+            all_stages: false,
12427
+            json_report: None,
12428
+            markdown_report: None,
12429
+            tools: ToolchainConfig::from_env(),
12430
+        };
12431
+
12432
+        let outcome = execute_case_cell(&suite, &case, OptLevel::O0, &config).unwrap();
12433
+        assert_eq!(outcome.kind, OutcomeKind::Xfail);
12434
+        assert!(outcome
12435
+            .detail
12436
+            .contains("mixed-surface namespaced compare stays soft for now"));
12437
+        assert!(outcome.detail.contains("armfortas.ir"));
12438
+    }
12439
+
1205212440
     #[test]
1205312441
     fn parses_suite_and_case() {
1205412442
         let root = std::env::temp_dir().join("afs_tests_parser_spec.afs");
@@ -12242,6 +12630,33 @@ end
1224212630
         let _ = fs::remove_file(&root);
1224312631
     }
1224412632
 
12633
+    #[test]
12634
+    fn parses_capability_policy_for_generic_case() {
12635
+        let root = std::env::temp_dir().join("bencch_generic_capability_policy_spec.afs");
12636
+        fs::write(
12637
+            &root,
12638
+            r#"suite "v2/capability-policy"
12639
+
12640
+case "gfortran_armfortas_ir"
12641
+source "../../fixtures/runtime/if_else.f90"
12642
+compiler gfortran => armfortas.ir
12643
+future capability "generic gfortran surface has no armfortas extras"
12644
+end
12645
+"#,
12646
+        )
12647
+        .unwrap();
12648
+
12649
+        let suite = parse_suite_file(&root).unwrap();
12650
+        let case = &suite.cases[0];
12651
+        let policy = case.capability_policy.as_ref().unwrap();
12652
+        assert!(matches!(policy.kind, StatusKind::Future));
12653
+        assert_eq!(
12654
+            policy.reason,
12655
+            "generic gfortran surface has no armfortas extras"
12656
+        );
12657
+        let _ = fs::remove_file(&root);
12658
+    }
12659
+
1224512660
     #[test]
1224612661
     fn parses_matrix_status_and_differential() {
1224712662
         let root = std::env::temp_dir().join("afs_tests_matrix_spec.afs");
@@ -12374,6 +12789,14 @@ end
1237412789
             "/tmp/gfortran".to_string(),
1237512790
             "--flang-bin".to_string(),
1237612791
             "/tmp/flang-new".to_string(),
12792
+            "--lfortran-bin".to_string(),
12793
+            "/tmp/lfortran".to_string(),
12794
+            "--ifort-bin".to_string(),
12795
+            "/tmp/ifort".to_string(),
12796
+            "--ifx-bin".to_string(),
12797
+            "/tmp/ifx".to_string(),
12798
+            "--nvfortran-bin".to_string(),
12799
+            "/tmp/nvfortran".to_string(),
1237712800
             "--as-bin".to_string(),
1237812801
             "/tmp/as".to_string(),
1237912802
             "--otool-bin".to_string(),
@@ -12406,6 +12829,10 @@ end
1240612829
         );
1240712830
         assert_eq!(config.tools.gfortran, "/tmp/gfortran");
1240812831
         assert_eq!(config.tools.flang_new, "/tmp/flang-new");
12832
+        assert_eq!(config.tools.lfortran, "/tmp/lfortran");
12833
+        assert_eq!(config.tools.ifort, "/tmp/ifort");
12834
+        assert_eq!(config.tools.ifx, "/tmp/ifx");
12835
+        assert_eq!(config.tools.nvfortran, "/tmp/nvfortran");
1240912836
         assert_eq!(config.tools.system_as, "/tmp/as");
1241012837
         assert_eq!(config.tools.otool, "/tmp/otool");
1241112838
         assert_eq!(config.tools.nm, "/tmp/nm");
@@ -12453,6 +12880,14 @@ end
1245312880
             "/tmp/gfortran".to_string(),
1245412881
             "--flang-bin".to_string(),
1245512882
             "/tmp/flang-new".to_string(),
12883
+            "--lfortran-bin".to_string(),
12884
+            "/tmp/lfortran".to_string(),
12885
+            "--ifort-bin".to_string(),
12886
+            "/tmp/ifort".to_string(),
12887
+            "--ifx-bin".to_string(),
12888
+            "/tmp/ifx".to_string(),
12889
+            "--nvfortran-bin".to_string(),
12890
+            "/tmp/nvfortran".to_string(),
1245612891
             "--as-bin".to_string(),
1245712892
             "/tmp/as".to_string(),
1245812893
             "--otool-bin".to_string(),
@@ -12476,6 +12911,10 @@ end
1247612911
         );
1247712912
         assert_eq!(config.tools.gfortran, "/tmp/gfortran");
1247812913
         assert_eq!(config.tools.flang_new, "/tmp/flang-new");
12914
+        assert_eq!(config.tools.lfortran, "/tmp/lfortran");
12915
+        assert_eq!(config.tools.ifort, "/tmp/ifort");
12916
+        assert_eq!(config.tools.ifx, "/tmp/ifx");
12917
+        assert_eq!(config.tools.nvfortran, "/tmp/nvfortran");
1247912918
         assert_eq!(config.tools.system_as, "/tmp/as");
1248012919
         assert_eq!(config.tools.otool, "/tmp/otool");
1248112920
         assert_eq!(config.tools.nm, "/tmp/nm");
@@ -12810,6 +13249,7 @@ end
1281013249
                 needle: "x18".into(),
1281113250
             }],
1281213251
             status_rules: Vec::new(),
13252
+            capability_policy: None,
1281313253
         };
1281413254
         let result = CaptureResult {
1281513255
             input: PathBuf::from("demo.f90"),
@@ -12870,6 +13310,7 @@ end
1287013310
             consistency_checks: vec![ConsistencyCheck::CliObjVsSystemAs],
1287113311
             expectations: Vec::new(),
1287213312
             status_rules: Vec::new(),
13313
+            capability_policy: None,
1287313314
         };
1287413315
         let mut stages = std::collections::BTreeMap::new();
1287513316
         stages.insert(Stage::Ir, CapturedStage::Text("module main".into()));
@@ -13163,6 +13604,7 @@ end
1316313604
             consistency_checks: Vec::new(),
1316413605
             expectations: Vec::new(),
1316513606
             status_rules: Vec::new(),
13607
+            capability_policy: None,
1316613608
         };
1316713609
 
1316813610
         let prepared = prepare_case_input(&case, &suite, OptLevel::O0).unwrap();
@@ -13215,6 +13657,7 @@ end
1321513657
             consistency_checks: Vec::new(),
1321613658
             expectations: Vec::new(),
1321713659
             status_rules: Vec::new(),
13660
+            capability_policy: None,
1321813661
         };
1321913662
         let outcome = Outcome {
1322013663
             suite: suite.name.clone(),
@@ -13698,6 +14141,10 @@ end
1369814141
                 armfortas: ArmfortasCliAdapter::External(armfortas_bin.display().to_string()),
1369914142
                 gfortran: gfortran_bin.display().to_string(),
1370014143
                 flang_new: "/tmp/does-not-exist-flang".into(),
14144
+                lfortran: "/tmp/does-not-exist-lfortran".into(),
14145
+                ifort: "/tmp/does-not-exist-ifort".into(),
14146
+                ifx: "/tmp/does-not-exist-ifx".into(),
14147
+                nvfortran: "/tmp/does-not-exist-nvfortran".into(),
1370114148
                 system_as: "/tmp/does-not-exist-as".into(),
1370214149
                 otool: "/tmp/does-not-exist-otool".into(),
1370314150
                 nm: "/tmp/does-not-exist-nm".into(),
@@ -13737,6 +14184,11 @@ end
1373714184
             "named_compiler.gfortran.generic_artifacts: diagnostics, exit-code, stdout, stderr, asm, obj, executable, runtime"
1373814185
         ));
1373914186
         assert!(rendered.contains("named_compiler.gfortran.adapter_extras: none"));
14187
+        assert!(rendered.contains("named_compiler.lfortran:"));
14188
+        assert!(rendered.contains("named_compiler.lfortran.accepted_names: lfortran"));
14189
+        assert!(rendered.contains("named_compiler.lfortran.candidate_binaries: lfortran"));
14190
+        assert!(rendered.contains("named_compiler.ifx.accepted_names: ifx"));
14191
+        assert!(rendered.contains("named_compiler.nvfortran.accepted_names: nvfortran, pgfortran"));
1374014192
         assert!(rendered.contains(
1374114193
             "explicit_compiler_path: any filesystem path passed to compare/introspect uses the generic external-driver adapter"
1374214194
         ));
@@ -13760,6 +14212,7 @@ end
1376014212
         assert!(rendered_json.contains("\"workspace\": {"));
1376114213
         assert!(rendered_json.contains("\"named_compilers\": {"));
1376214214
         assert!(rendered_json.contains("\"tools\": {"));
14215
+        assert!(rendered_json.contains("\"lfortran\": {"));
1376314216
         assert!(rendered_json.contains("\"named_compiler.armfortas.adapter_extras\""));
1376414217
         assert!(rendered_markdown.contains("# bencch doctor report"));
1376514218
         assert!(rendered_markdown.contains("| `named_compiler.armfortas` |"));
@@ -13785,13 +14238,46 @@ end
1378514238
             consistency_checks: Vec::new(),
1378614239
             expectations: Vec::new(),
1378714240
             status_rules: Vec::new(),
14241
+            capability_policy: None,
1378814242
         };
1378914243
 
1379014244
         let lines = case_discovery_lines(&case, &ToolchainConfig::from_env());
1379114245
         assert!(lines.contains(&"capability_status: blocked".to_string()));
13792
-        assert!(lines
13793
-            .iter()
13794
-            .any(|line| line.contains("unsupported in this adapter: armfortas.ir")));
14246
+        assert!(lines.iter().any(|line| line.contains(
14247
+            "gfortran does not support requested artifacts in this adapter: armfortas.ir"
14248
+        )));
14249
+    }
14250
+
14251
+    #[test]
14252
+    fn case_discovery_lines_report_capability_policy_as_deferred() {
14253
+        let case = CaseSpec {
14254
+            name: "unsupported_extra".into(),
14255
+            source: PathBuf::from("demo.f90"),
14256
+            graph_files: Vec::new(),
14257
+            requested: BTreeSet::new(),
14258
+            generic_introspect: Some(GenericIntrospectCase {
14259
+                compiler: CompilerSpec::Named(NamedCompiler::Gfortran),
14260
+                artifacts: BTreeSet::from([ArtifactKey::Extra("armfortas.ir".into())]),
14261
+            }),
14262
+            generic_compare: None,
14263
+            opt_levels: vec![OptLevel::O0],
14264
+            repeat_count: 2,
14265
+            reference_compilers: Vec::new(),
14266
+            consistency_checks: Vec::new(),
14267
+            expectations: Vec::new(),
14268
+            status_rules: Vec::new(),
14269
+            capability_policy: Some(CapabilityPolicy {
14270
+                kind: StatusKind::Future,
14271
+                reason: "generic gfortran surface has no armfortas extras".into(),
14272
+            }),
14273
+        };
14274
+
14275
+        let lines = case_discovery_lines(&case, &ToolchainConfig::from_env());
14276
+        assert!(lines.contains(&"capability_status: deferred".to_string()));
14277
+        assert!(lines.contains(
14278
+            &"capability_policy: future when blocked (generic gfortran surface has no armfortas extras)"
14279
+                .to_string()
14280
+        ));
1379514281
     }
1379614282
 
1379714283
     #[test]
@@ -13809,6 +14295,7 @@ end
1380914295
             consistency_checks: Vec::new(),
1381014296
             expectations: Vec::new(),
1381114297
             status_rules: Vec::new(),
14298
+            capability_policy: None,
1381214299
         };
1381314300
         let linked_case = CaseSpec {
1381414301
             name: "linked".into(),
@@ -13823,6 +14310,7 @@ end
1382314310
             consistency_checks: vec![ConsistencyCheck::CaptureAsmReproducible],
1382414311
             expectations: Vec::new(),
1382514312
             status_rules: Vec::new(),
14313
+            capability_policy: None,
1382614314
         };
1382714315
 
1382814316
         let tools = ToolchainConfig {
@@ -14140,6 +14628,7 @@ end
1414014628
                 },
1414114629
             ],
1414214630
             status_rules: Vec::new(),
14631
+            capability_policy: None,
1414314632
         };
1414414633
         let artifacts = ExecutionArtifacts {
1414514634
             requested: BTreeSet::from([Stage::Tokens, Stage::Run]),
@@ -14176,6 +14665,7 @@ end
1417614665
             consistency_checks: Vec::new(),
1417714666
             expectations: vec![Expectation::FailCommentPatterns(vec!["hidden".into()])],
1417814667
             status_rules: Vec::new(),
14668
+            capability_policy: None,
1417914669
         };
1418014670
         let artifacts = ExecutionArtifacts {
1418114671
             requested: BTreeSet::from([Stage::Run]),
@@ -14215,6 +14705,7 @@ end
1421514705
                 needle: "expected 'then'".into(),
1421614706
             }],
1421714707
             status_rules: Vec::new(),
14708
+            capability_policy: None,
1421814709
         };
1421914710
         let failure = CaptureFailure {
1422014711
             input: PathBuf::from("generated.f90"),
@@ -14259,6 +14750,7 @@ end
1425914750
                 needle: "42".into(),
1426014751
             }],
1426114752
             status_rules: Vec::new(),
14753
+            capability_policy: None,
1426214754
         };
1426314755
         let failure = CaptureFailure {
1426414756
             input: PathBuf::from("graph.f90"),
@@ -14301,6 +14793,7 @@ end
1430114793
                 needle: ".globl _add_one".into(),
1430214794
             }],
1430314795
             status_rules: Vec::new(),
14796
+            capability_policy: None,
1430414797
         };
1430514798
         let failure = CaptureFailure {
1430614799
             input: PathBuf::from("graph.f90"),