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::{Cxx11, Cxx14, Cxx17, Cxx20, Cxx23, Cxx26, 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 | 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; `-std=c++26`: GCC 14.
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            Cxx26 => version_gated_capability(version, 14, 0),
228        },
229        CompilerKind::Clang => match standard {
230            Cxx98 | Cxx11 | Cxx14 | Cxx17 => always,
231            // `-std=c++20` spelling shipped in Clang 10 (the
232            // `release/10.x` LangStandards.def already names
233            // `c++20`, with `c++2a` as the deprecated alias; Clang
234            // 9 only had `c++2a`); `-std=c++23` and `-std=c++26`
235            // both landed in Clang 17.
236            Cxx20 => version_gated_capability(version, 10, 0),
237            Cxx23 | Cxx26 => version_gated_capability(version, 17, 0),
238        },
239        CompilerKind::AppleClang => match standard {
240            Cxx98 | Cxx11 | Cxx14 | Cxx17 => always,
241            // Xcode <-> LLVM mapping: every Apple clang 12 is at
242            // least LLVM-10-based, and LLVM 10 already spells
243            // `-std=c++20`.  Apple clang 16 (Xcode 16) is
244            // LLVM-17-based for the c++23 / c++26 spellings.
245            Cxx20 => version_gated_capability(version, 12, 0),
246            Cxx23 | Cxx26 => version_gated_capability(version, 16, 0),
247        },
248        CompilerKind::ClangCl => match standard {
249            Cxx14 | Cxx17 => always,
250            // Conservative: `/std:c++20` is present by Clang 13.
251            Cxx20 => version_gated_capability(version, 13, 0),
252            // No stable `/std:c++23` / `/std:c++26` exists as of
253            // Clang 22 (only `c++23preview` / `c++latest`), and no
254            // `/std:` spelling for the pre-C++14 standards.
255            Cxx98 | Cxx11 | Cxx23 | Cxx26 => no_flag,
256        },
257        CompilerKind::Msvc => match standard {
258            // `/std:` selection starts at C++14 (VS2017 / `cl`
259            // 19.10); `/std:c++17` from `cl` 19.11; `/std:c++20`
260            // became stable in VS2019 16.11 (`cl` 19.29 - 16.10
261            // shares the minor, so this slightly over-accepts it).
262            Cxx14 => version_gated_capability(version, 19, 10),
263            Cxx17 => version_gated_capability(version, 19, 11),
264            Cxx20 => version_gated_capability(version, 19, 29),
265            // No stable flag: C++98/11 predate `/std:`, and
266            // `/std:c++23` / `/std:c++26` only exist as
267            // `c++23preview` / `c++latest`.
268            Cxx98 | Cxx11 | Cxx23 | Cxx26 => no_flag,
269        },
270        CompilerKind::Unknown => Capability::unsupported_from(CapabilitySource::AssumedDefault),
271    }
272}
273
274/// Human-readable reason for an unsupported standard capability,
275/// used by the validation errors: either the compiler has no stable
276/// flag for the standard at any version, or the detected version
277/// predates the flag.
278#[must_use]
279pub fn standard_support_detail(capability: Capability, kind: CompilerKind) -> String {
280    match capability.source {
281        CapabilitySource::Unsupported => {
282            format!("{kind} has no stable flag selecting this standard")
283        }
284        _ => "the detected compiler version predates support for this standard's flag".to_owned(),
285    }
286}
287
288/// Derive an [`ArchiverCapabilities`] set from the detected
289/// identity.
290pub fn derive_ar_capabilities(identity: &ArchiverIdentity) -> ArchiverCapabilities {
291    let ar_crs = if identity.kind.supports_ar_crs() {
292        Capability::supported_from(CapabilitySource::Version)
293    } else if identity.kind == ArchiverKind::Lib {
294        Capability::unsupported_from(CapabilitySource::Unsupported)
295    } else {
296        Capability::unsupported_from(CapabilitySource::AssumedDefault)
297    };
298    // Honest across both dialects: `ar` / `llvm-ar` archive via
299    // `ar crs`, `lib.exe` via `lib /OUT:`.  The `ar_crs` capability
300    // above stays GNU-specific (`lib.exe` does not accept `crs`),
301    // but both shapes do produce a static library.
302    let static_library_output = if identity.kind.produces_static_library() {
303        Capability::supported_from(CapabilitySource::Version)
304    } else {
305        Capability::unsupported_from(CapabilitySource::AssumedDefault)
306    };
307    ArchiverCapabilities {
308        ar_crs,
309        static_library_output,
310    }
311}