Skip to main content

cabin_core/compiler/
capabilities.rs

1//! Capability model and capability derivation from tool identity.
2
3use serde::{Deserialize, Serialize};
4
5use super::identity::{
6    ArchiverIdentity, ArchiverKind, CompilerIdentity, CompilerKind, CompilerVersion,
7};
8use crate::language_standard::{CStandard, CxxStandard};
9
10/// Where one capability decision came from. Recorded so
11/// `cabin metadata` can show whether Cabin trusted the version
12/// alone, ran a probe, or fell back to a conservative default.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum CapabilitySource {
16    /// Inferred from a recognized compiler kind/version.
17    Version,
18    /// Conservative default applied when the compiler kind is
19    /// `Unknown` or detection failed.
20    AssumedDefault,
21    /// The selected tool is recognizably unable to provide this
22    /// capability (e.g. MSVC asked for GCC-style flags).
23    Unsupported,
24}
25
26impl CapabilitySource {
27    pub fn as_key(self) -> &'static str {
28        match self {
29            CapabilitySource::Version => "version",
30            CapabilitySource::AssumedDefault => "assumed-default",
31            CapabilitySource::Unsupported => "unsupported",
32        }
33    }
34}
35
36/// One typed capability decision: whether the tool supports it,
37/// and where the answer came from.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub struct Capability {
40    pub supported: bool,
41    pub source: CapabilitySource,
42}
43
44impl Capability {
45    pub fn supported_from(source: CapabilitySource) -> Self {
46        Self {
47            supported: true,
48            source,
49        }
50    }
51    pub fn unsupported_from(source: CapabilitySource) -> Self {
52        Self {
53            supported: false,
54            source,
55        }
56    }
57}
58
59/// Capability set for a C/C++ compiler. Every field is decided
60/// during detection so the planner can compare its required set
61/// against the resolved set without re-running parsing logic.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct CompilerCapabilities {
64    /// Accepts GCC-style `-O<n>`, `-DNAME`, `-Idir`, `-c`, `-o`.
65    pub gcc_style_flags: Capability,
66    /// Accepts MSVC-style `/O<n>`, `/DNAME`, `/I dir`. Detection-
67    /// only; the current backend never emits these.
68    pub msvc_style_flags: Capability,
69    /// Accepts `-MMD -MF <file>` to write a make-style depfile.
70    pub depfile_mmd_mf: Capability,
71    /// Can mark dependency include directories as *system* search
72    /// paths so diagnostics inside their headers are suppressed.
73    /// GCC/Clang spell this `-isystem` (part of the base command
74    /// line); `cl` spells it `/external:I` (+ `/external:W0`),
75    /// non-experimental from VS2019 16.10 (`cl` 19.29); `clang-cl`
76    /// accepts the `/external:` block since clang 13.
77    pub external_include_dirs: Capability,
78}
79
80/// Capability set for a static-library archiver.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct ArchiverCapabilities {
83    /// Accepts the `crs` mode flags (the planner's archive form).
84    pub ar_crs: Capability,
85    /// Produces a `.a` static library archive.
86    pub static_library_output: Capability,
87}
88
89/// Derive a [`CompilerCapabilities`] set from the detected
90/// identity. Decisions are made from the recognized compiler
91/// kind, with conservative defaults for [`CompilerKind::Unknown`].
92/// No probe commands are run from this function — the caller's
93/// detection layer already gathered everything we need.
94/// Decide a version-gated capability for a recognized compiler whose
95/// minimum supporting version is `(min_major, min_minor)`. A parsed
96/// version at or above the threshold is `supported`; below it,
97/// `unsupported`. An unparsed version (`None`) is `supported` as an
98/// assumed default — a recognized compiler always reports a version,
99/// so a parse miss must not reject an otherwise-modern compiler,
100/// matching the per-standard validation policy.
101fn version_gated_capability(
102    version: Option<&CompilerVersion>,
103    min_major: u32,
104    min_minor: u32,
105) -> Capability {
106    match version.map(|v| (v.major, v.minor.unwrap_or(0))) {
107        Some((major, minor)) if major > min_major || (major == min_major && minor >= min_minor) => {
108            Capability::supported_from(CapabilitySource::Version)
109        }
110        Some(_) => Capability::unsupported_from(CapabilitySource::Version),
111        None => Capability::supported_from(CapabilitySource::AssumedDefault),
112    }
113}
114
115pub fn derive_cxx_capabilities(identity: &CompilerIdentity) -> CompilerCapabilities {
116    let gcc_style = if identity.kind.supports_gcc_style_command_line() {
117        Capability::supported_from(CapabilitySource::Version)
118    } else if identity.kind.speaks_msvc_dialect() {
119        Capability::unsupported_from(CapabilitySource::Unsupported)
120    } else {
121        Capability::unsupported_from(CapabilitySource::AssumedDefault)
122    };
123    let msvc_style = if identity.kind.speaks_msvc_dialect() {
124        Capability::supported_from(CapabilitySource::Version)
125    } else {
126        Capability::unsupported_from(CapabilitySource::AssumedDefault)
127    };
128    let depfile_mmd_mf = if identity.kind.supports_gcc_style_command_line() {
129        Capability::supported_from(CapabilitySource::Version)
130    } else if identity.kind.speaks_msvc_dialect() {
131        // MSVC-dialect compilers discover headers with `/showIncludes`,
132        // not a make-style depfile.
133        Capability::unsupported_from(CapabilitySource::Unsupported)
134    } else {
135        Capability::unsupported_from(CapabilitySource::AssumedDefault)
136    };
137    // `-isystem` is part of the base GCC/Clang command line, so every
138    // recognized GNU-dialect compiler can mark dependency includes as
139    // system search paths. `cl /external:I` left
140    // `/experimental:external` in VS2019 16.10 (`cl` 19.29);
141    // `clang-cl` understands the `/external:` block since clang 13.
142    // Unknown compilers stay conservative so the planner never
143    // assumes `-isystem` semantics for a compiler it cannot identify.
144    let external_include_dirs = match identity.kind {
145        CompilerKind::Clang | CompilerKind::AppleClang | CompilerKind::Gcc => {
146            Capability::supported_from(CapabilitySource::Version)
147        }
148        CompilerKind::ClangCl => version_gated_capability(identity.version.as_ref(), 13, 0),
149        CompilerKind::Msvc => version_gated_capability(identity.version.as_ref(), 19, 29),
150        CompilerKind::Unknown => Capability::unsupported_from(CapabilitySource::AssumedDefault),
151    };
152
153    CompilerCapabilities {
154        gcc_style_flags: gcc_style,
155        msvc_style_flags: msvc_style,
156        depfile_mmd_mf,
157        external_include_dirs,
158    }
159}
160
161/// Whether `identity` accepts the exact flag spelling for a C
162/// `standard` (`-std=<std>` on the GNU dialect, `/std:<std>` on
163/// MSVC). Version thresholds follow the audited table in
164/// `docs/language-standards.md`: unknown versions fail open
165/// (assumed-default), recognized-but-old versions fail closed, and
166/// MSVC-dialect gaps (no stable flag at any version) are
167/// `Unsupported`. Unknown compiler kinds are rejected earlier by
168/// the backend validation, so their entry here is conservative.
169#[must_use]
170pub fn c_standard_capability(identity: &CompilerIdentity, standard: CStandard) -> Capability {
171    use CStandard::{C11, C17, C23, C89, C99};
172    let version = identity.version.as_ref();
173    let always = Capability::supported_from(CapabilitySource::Version);
174    let no_flag = Capability::unsupported_from(CapabilitySource::Unsupported);
175    match identity.kind {
176        CompilerKind::Gcc => match standard {
177            C89 | C99 | C11 => always,
178            // `-std=c17` spelling: GCC 8; `-std=c23`: GCC 14.
179            C17 => version_gated_capability(version, 8, 0),
180            C23 => version_gated_capability(version, 14, 0),
181        },
182        CompilerKind::Clang => match standard {
183            C89 | C99 | C11 => always,
184            // `-std=c17`: Clang 6; `-std=c23`: Clang 18.
185            C17 => version_gated_capability(version, 6, 0),
186            C23 => version_gated_capability(version, 18, 0),
187        },
188        CompilerKind::AppleClang => match standard {
189            C89 | C99 | C11 => always,
190            // Apple clang 10 (Xcode 10) is LLVM-6-based; Apple
191            // clang 17 (Xcode 16.3) is LLVM-19-based (>= 18).
192            C17 => version_gated_capability(version, 10, 0),
193            C23 => version_gated_capability(version, 17, 0),
194        },
195        // `clang-cl` gained `/std:c11` / `/std:c17` in Clang 13
196        // (LLVM D95575); there is no `/std:` spelling for the rest.
197        CompilerKind::ClangCl => match standard {
198            C11 | C17 => version_gated_capability(version, 13, 0),
199            C89 | C99 | C23 => no_flag,
200        },
201        // `cl /std:c11` and `/std:c17` arrived together in VS2019
202        // 16.8 (`cl` 19.28); C89/C99/C23 have no selection flag.
203        CompilerKind::Msvc => match standard {
204            C11 | C17 => version_gated_capability(version, 19, 28),
205            C89 | C99 | C23 => no_flag,
206        },
207        CompilerKind::Unknown => Capability::unsupported_from(CapabilitySource::AssumedDefault),
208    }
209}
210
211/// C++ twin of [`c_standard_capability`].
212#[must_use]
213pub fn cxx_standard_capability(identity: &CompilerIdentity, standard: CxxStandard) -> Capability {
214    use CxxStandard::{Cxx03, Cxx11, Cxx14, Cxx17, Cxx20, Cxx23, Cxx98};
215    let version = identity.version.as_ref();
216    let always = Capability::supported_from(CapabilitySource::Version);
217    let no_flag = Capability::unsupported_from(CapabilitySource::Unsupported);
218    match identity.kind {
219        CompilerKind::Gcc => match standard {
220            Cxx98 | Cxx03 | Cxx11 => always,
221            // GCC >= 5 for the c++14 / c++17 spellings (the
222            // repository's long-standing c++17 gate); `-std=c++20`:
223            // GCC 10; `-std=c++23`: GCC 11.
224            Cxx14 | Cxx17 => version_gated_capability(version, 5, 0),
225            Cxx20 => version_gated_capability(version, 10, 0),
226            Cxx23 => version_gated_capability(version, 11, 0),
227        },
228        CompilerKind::Clang => match standard {
229            Cxx98 | Cxx03 | Cxx11 | Cxx14 | Cxx17 => always,
230            // `-std=c++20` spelling shipped in Clang 10 (the
231            // `release/10.x` LangStandards.def already names
232            // `c++20`, with `c++2a` as the deprecated alias; Clang
233            // 9 only had `c++2a`); `-std=c++23` in Clang 17.
234            Cxx20 => version_gated_capability(version, 10, 0),
235            Cxx23 => version_gated_capability(version, 17, 0),
236        },
237        CompilerKind::AppleClang => match standard {
238            Cxx98 | Cxx03 | Cxx11 | Cxx14 | Cxx17 => always,
239            // Xcode <-> LLVM mapping: every Apple clang 12 is at
240            // least LLVM-10-based, and LLVM 10 already spells
241            // `-std=c++20`. Apple clang 16 (Xcode 16) is
242            // LLVM-17-based for the c++23 spelling.
243            Cxx20 => version_gated_capability(version, 12, 0),
244            Cxx23 => version_gated_capability(version, 16, 0),
245        },
246        CompilerKind::ClangCl => match standard {
247            Cxx14 | Cxx17 => always,
248            // Conservative: `/std:c++20` is present by Clang 13.
249            Cxx20 => version_gated_capability(version, 13, 0),
250            // No stable `/std:c++23` exists as of Clang 22 (only
251            // `c++23preview` / `c++latest`), and no `/std:` spelling
252            // for the pre-C++14 standards.
253            Cxx98 | Cxx03 | Cxx11 | Cxx23 => no_flag,
254        },
255        CompilerKind::Msvc => match standard {
256            // `/std:` selection starts at C++14 (VS2017 / `cl`
257            // 19.10); `/std:c++17` from `cl` 19.11; `/std:c++20`
258            // became stable in VS2019 16.11 (`cl` 19.29 — 16.10
259            // shares the minor, so this slightly over-accepts it).
260            Cxx14 => version_gated_capability(version, 19, 10),
261            Cxx17 => version_gated_capability(version, 19, 11),
262            Cxx20 => version_gated_capability(version, 19, 29),
263            // No stable flag: C++98/03/11 predate `/std:` and
264            // `/std:c++23` only exists as `c++23preview`.
265            Cxx98 | Cxx03 | Cxx11 | Cxx23 => no_flag,
266        },
267        CompilerKind::Unknown => Capability::unsupported_from(CapabilitySource::AssumedDefault),
268    }
269}
270
271/// Human-readable reason for an unsupported standard capability,
272/// used by the validation errors: either the compiler has no stable
273/// flag for the standard at any version, or the detected version
274/// predates the flag.
275#[must_use]
276pub fn standard_support_detail(capability: Capability, kind: CompilerKind) -> String {
277    match capability.source {
278        CapabilitySource::Unsupported => {
279            format!("{kind} has no stable flag selecting this standard")
280        }
281        _ => "the detected compiler version predates support for this standard's flag".to_owned(),
282    }
283}
284
285/// Derive an [`ArchiverCapabilities`] set from the detected
286/// identity.
287pub fn derive_ar_capabilities(identity: &ArchiverIdentity) -> ArchiverCapabilities {
288    let ar_crs = if identity.kind.supports_ar_crs() {
289        Capability::supported_from(CapabilitySource::Version)
290    } else if identity.kind == ArchiverKind::Lib {
291        Capability::unsupported_from(CapabilitySource::Unsupported)
292    } else {
293        Capability::unsupported_from(CapabilitySource::AssumedDefault)
294    };
295    // Honest across both dialects: `ar` / `llvm-ar` archive via
296    // `ar crs`, `lib.exe` via `lib /OUT:`. The `ar_crs` capability
297    // above stays GNU-specific (`lib.exe` does not accept `crs`),
298    // but both shapes do produce a static library.
299    let static_library_output = if identity.kind.produces_static_library() {
300        Capability::supported_from(CapabilitySource::Version)
301    } else {
302        Capability::unsupported_from(CapabilitySource::AssumedDefault)
303    };
304    ArchiverCapabilities {
305        ar_crs,
306        static_library_output,
307    }
308}