Skip to main content

cabin_core/
language_standard.rs

1//! Typed C/C++ language standards.
2//!
3//! Owns the standard enums (ISO levels only; GNU extensions are the
4//! orthogonal per-target `gnu-extensions` boolean), the manifest
5//! declaration shape shared by `[package]` and `[target.<name>]`,
6//! effective-standard resolution (target ▶ package; there is no
7//! built-in default - a target that compiles a language without a
8//! declared standard is a manifest error), interface-requirement
9//! relevance and fallback, the escape-hatch conflict detector, the
10//! interface/implementation contradiction lint, and the per-package
11//! summary that feeds `BuildConfiguration` fingerprinting and the
12//! metadata view.  Pure data and logic only; no I/O.  See
13//! `docs/language-standards.md` for the user-facing contract.
14
15use std::collections::BTreeMap;
16use std::marker::PhantomData;
17
18use serde::de::{MapAccess, Visitor, value::MapAccessDeserializer};
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use crate::{ResolvedProfileFlags, SourceLanguage, Target, classify_source};
23
24/// C language standards Cabin can request, oldest to newest.  The
25/// `Ord` derive follows declaration order, which is the plain
26/// chronological chain (in particular `c11 < c17`).  Implements the
27/// level chain `Level_C` of spec D2
28/// (`docs/design/standard-compatibility/spec.md`): chronological
29/// enumeration order, not numeric (`c89 < c11`), with no
30/// equivalence special case anywhere in the chain.  The `c90` alias
31/// is normalized away by [`Self::parse`] and is not an element of
32/// the chain (D2 remark).
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
34pub enum CStandard {
35    #[serde(rename = "c89")]
36    C89,
37    #[serde(rename = "c99")]
38    C99,
39    #[serde(rename = "c11")]
40    C11,
41    #[serde(rename = "c17")]
42    C17,
43    #[serde(rename = "c23")]
44    C23,
45}
46
47impl CStandard {
48    pub const ALL: [Self; 5] = [Self::C89, Self::C99, Self::C11, Self::C17, Self::C23];
49
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::C89 => "c89",
53            Self::C99 => "c99",
54            Self::C11 => "c11",
55            Self::C17 => "c17",
56            Self::C23 => "c23",
57        }
58    }
59
60    /// # Errors
61    /// Returns [`LanguageStandardParseError`] when `value` is not a
62    /// recognized C standard: a dedicated variant for range-like
63    /// inputs and for the interface-only `none`, otherwise the
64    /// invalid-value error listing the accepted identifiers.
65    /// `c90` parses as an alias of `c89`, normalized immediately.
66    pub fn parse(value: &str) -> Result<Self, LanguageStandardParseError> {
67        reject_non_identifier(SourceLanguage::C, value)?;
68        let normalized = if value == "c90" { "c89" } else { value };
69        Self::ALL
70            .into_iter()
71            .find(|s| s.as_str() == normalized)
72            .ok_or_else(|| LanguageStandardParseError::Unknown {
73                language: SourceLanguage::C,
74                value: value.to_owned(),
75            })
76    }
77}
78
79impl std::fmt::Display for CStandard {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.write_str(self.as_str())
82    }
83}
84
85impl std::str::FromStr for CStandard {
86    type Err = LanguageStandardParseError;
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        Self::parse(s)
89    }
90}
91
92/// C++ language standards Cabin can request, oldest to newest, in
93/// the plain chronological chain.  Implements the level chain
94/// `Level_C++` of spec D2
95/// (`docs/design/standard-compatibility/spec.md`): chronological
96/// enumeration order, not numeric (`c++98 < c++11`), with no
97/// equivalence special case anywhere in the chain.  The `c++03`
98/// alias is normalized away by [`Self::parse`] and is not an
99/// element of the chain (D2 remark).
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
101pub enum CxxStandard {
102    #[serde(rename = "c++98")]
103    Cxx98,
104    #[serde(rename = "c++11")]
105    Cxx11,
106    #[serde(rename = "c++14")]
107    Cxx14,
108    #[serde(rename = "c++17")]
109    Cxx17,
110    #[serde(rename = "c++20")]
111    Cxx20,
112    #[serde(rename = "c++23")]
113    Cxx23,
114    #[serde(rename = "c++26")]
115    Cxx26,
116}
117
118impl CxxStandard {
119    pub const ALL: [Self; 7] = [
120        Self::Cxx98,
121        Self::Cxx11,
122        Self::Cxx14,
123        Self::Cxx17,
124        Self::Cxx20,
125        Self::Cxx23,
126        Self::Cxx26,
127    ];
128
129    pub const fn as_str(self) -> &'static str {
130        match self {
131            Self::Cxx98 => "c++98",
132            Self::Cxx11 => "c++11",
133            Self::Cxx14 => "c++14",
134            Self::Cxx17 => "c++17",
135            Self::Cxx20 => "c++20",
136            Self::Cxx23 => "c++23",
137            Self::Cxx26 => "c++26",
138        }
139    }
140
141    /// # Errors
142    /// Returns [`LanguageStandardParseError`] when `value` is not a
143    /// recognized C++ standard: a dedicated variant for range-like
144    /// inputs and for the interface-only `none`, otherwise the
145    /// invalid-value error listing the accepted identifiers.
146    /// `c++03` parses as an alias of `c++98`, normalized
147    /// immediately.
148    pub fn parse(value: &str) -> Result<Self, LanguageStandardParseError> {
149        reject_non_identifier(SourceLanguage::Cxx, value)?;
150        let normalized = if value == "c++03" { "c++98" } else { value };
151        Self::ALL
152            .into_iter()
153            .find(|s| s.as_str() == normalized)
154            .ok_or_else(|| LanguageStandardParseError::Unknown {
155                language: SourceLanguage::Cxx,
156                value: value.to_owned(),
157            })
158    }
159}
160
161impl std::fmt::Display for CxxStandard {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        f.write_str(self.as_str())
164    }
165}
166
167impl std::str::FromStr for CxxStandard {
168    type Err = LanguageStandardParseError;
169    fn from_str(s: &str) -> Result<Self, Self::Err> {
170        Self::parse(s)
171    }
172}
173
174/// The shared pre-lookup checks: range-like inputs and the
175/// interface-only `none` get dedicated diagnostics on every field
176/// that parses a standard value.
177fn reject_non_identifier(
178    language: SourceLanguage,
179    value: &str,
180) -> Result<(), LanguageStandardParseError> {
181    // `>=` / `<=` are covered by their first character.
182    if value.contains(['>', '<', ',']) {
183        return Err(LanguageStandardParseError::RangeReserved {
184            language,
185            value: value.to_owned(),
186        });
187    }
188    if value == "none" {
189        return Err(LanguageStandardParseError::NoneOnImplementation { language });
190    }
191    Ok(())
192}
193
194/// An invalid manifest standard value.  Range-like inputs and the
195/// misplaced interface-only `none` get dedicated variants; anything
196/// else is the invalid-value error listing the accepted
197/// identifiers.
198#[derive(Debug, Error, Clone, PartialEq, Eq)]
199pub enum LanguageStandardParseError {
200    #[error(
201        "unknown {} standard `{value}`: expected one of {}",
202        .language.human_label(),
203        valid_standard_values(*.language)
204    )]
205    Unknown {
206        language: SourceLanguage,
207        value: String,
208    },
209    #[error(
210        "range requirement `{value}` is reserved for a future version of Cabin; declare a single {} standard",
211        .language.human_label()
212    )]
213    RangeReserved {
214        language: SourceLanguage,
215        value: String,
216    },
217    #[error(
218        "`none` is only valid on `interface-c-standard` / `interface-cxx-standard`, where it marks the target's headers as not consumable from that language; compiled {} sources need a concrete standard",
219        .language.human_label()
220    )]
221    NoneOnImplementation { language: SourceLanguage },
222}
223
224fn valid_standard_values(language: SourceLanguage) -> String {
225    match language {
226        SourceLanguage::C => CStandard::ALL.map(CStandard::as_str).join(", "),
227        SourceLanguage::Cxx => CxxStandard::ALL.map(CxxStandard::as_str).join(", "),
228    }
229}
230
231/// The implementation-standard field family for `language`
232/// (`c-standard` / `cxx-standard`), for diagnostics.
233const fn implementation_field(language: SourceLanguage) -> &'static str {
234    match language {
235        SourceLanguage::C => "c-standard",
236        SourceLanguage::Cxx => "cxx-standard",
237    }
238}
239
240/// One per-compile standard value, carried by the build IR.  Encodes
241/// the source language, so the dialect lowering derives both the
242/// rule kind and the standard flag from this single field.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
244pub enum LanguageStandard {
245    C(CStandard),
246    Cxx(CxxStandard),
247}
248
249impl LanguageStandard {
250    pub const fn language(self) -> SourceLanguage {
251        match self {
252            Self::C(_) => SourceLanguage::C,
253            Self::Cxx(_) => SourceLanguage::Cxx,
254        }
255    }
256
257    pub const fn as_str(self) -> &'static str {
258        match self {
259            Self::C(s) => s.as_str(),
260            Self::Cxx(s) => s.as_str(),
261        }
262    }
263
264    /// The `/std:` value `cl.exe` accepts for this standard, when a
265    /// stable one exists.  `None` marks the MSVC-dialect gaps
266    /// (C89/C99/C23, C++98/11/23/26); the planner rejects those
267    /// before lowering on the MSVC dialect.
268    pub const fn msvc_spelling(self) -> Option<&'static str> {
269        match self {
270            Self::C(CStandard::C11) => Some("/std:c11"),
271            Self::C(CStandard::C17) => Some("/std:c17"),
272            Self::Cxx(CxxStandard::Cxx14) => Some("/std:c++14"),
273            Self::Cxx(CxxStandard::Cxx17) => Some("/std:c++17"),
274            Self::Cxx(CxxStandard::Cxx20) => Some("/std:c++20"),
275            _ => None,
276        }
277    }
278}
279
280impl std::fmt::Display for LanguageStandard {
281    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282        f.write_str(self.as_str())
283    }
284}
285
286/// One interface standard requirement: the minimum standard the
287/// target's public headers require from consumers.  `max` is
288/// reserved for future range requirements and is never populated
289/// today, but it stays in the type and every serialized form so the
290/// wire shape does not change when ranges land.  This is the
291/// `{ min, max }` pair of spec D4's remark
292/// (`docs/design/standard-compatibility/spec.md`): with `max`
293/// always absent in v1, a requirement denotes the declared minimum
294/// `decl_L(t) = m` of spec D6, which D9 row 2 maps to the
295/// compatibility requirement `[m]`
296/// ([`crate::standard_compatibility::Requirement::Min`]).
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
298#[serde(deny_unknown_fields)]
299pub struct StandardRequirement<S> {
300    pub min: S,
301    #[serde(default = "none")]
302    pub max: Option<S>,
303}
304
305// `#[serde(default)]` needs a fn item; `Option::default` would also
306// work but reads as a value default rather than "absent max".
307fn none<S>() -> Option<S> {
308    None
309}
310
311/// One declared interface-standard value: either a requirement or
312/// the explicit `none`, meaning the target's headers are not
313/// consumable from that language.  Implements the explicit
314/// interface declaration `decl_L(t)` of spec D6
315/// (`docs/design/standard-compatibility/spec.md`); D6's `⊥` (no
316/// declaration) is an absent `Option<InterfaceRequirement>` at the
317/// use sites.
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum InterfaceRequirement<S> {
320    /// The target's headers are not consumable from this language:
321    /// the declared `"none"` of spec D6, which D9 row 1 maps to the
322    /// unsatisfiable requirement
323    /// ([`crate::standard_compatibility::Requirement::Forbidden`]).
324    None,
325    Requirement(StandardRequirement<S>),
326}
327
328impl<S> InterfaceRequirement<S> {
329    /// The minimum standard, when this is a requirement.
330    #[must_use]
331    pub fn min(self) -> Option<S> {
332        match self {
333            Self::None => None,
334            Self::Requirement(requirement) => Some(requirement.min),
335        }
336    }
337}
338
339/// Parse one `interface-c-standard` value: `none` or a single C
340/// standard (ranges are reserved; see [`CStandard::parse`]).
341///
342/// # Errors
343/// Propagates [`CStandard::parse`] errors for anything but `none`.
344pub fn parse_interface_c(
345    value: &str,
346) -> Result<InterfaceRequirement<CStandard>, LanguageStandardParseError> {
347    if value == "none" {
348        return Ok(InterfaceRequirement::None);
349    }
350    CStandard::parse(value)
351        .map(|min| InterfaceRequirement::Requirement(StandardRequirement { min, max: None }))
352}
353
354/// Parse one `interface-cxx-standard` value: `none` or a single C++
355/// standard.
356///
357/// # Errors
358/// Propagates [`CxxStandard::parse`] errors for anything but `none`.
359pub fn parse_interface_cxx(
360    value: &str,
361) -> Result<InterfaceRequirement<CxxStandard>, LanguageStandardParseError> {
362    if value == "none" {
363        return Ok(InterfaceRequirement::None);
364    }
365    CxxStandard::parse(value)
366        .map(|min| InterfaceRequirement::Requirement(StandardRequirement { min, max: None }))
367}
368
369impl<S: std::fmt::Display> std::fmt::Display for InterfaceRequirement<S> {
370    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371        match self {
372            Self::None => f.write_str("none"),
373            Self::Requirement(StandardRequirement { min, max: None }) => min.fmt(f),
374            Self::Requirement(StandardRequirement {
375                min,
376                max: Some(max),
377            }) => write!(f, "{min}..{max}"),
378        }
379    }
380}
381
382// `none` serializes as the bare string; a requirement serializes as
383// its `{ min, max }` table (with `max` present even while reserved)
384// so the canonical-metadata / index wire format is stable when
385// range support lands.
386impl<S: Serialize> Serialize for InterfaceRequirement<S> {
387    fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
388    where
389        Ser: serde::Serializer,
390    {
391        match self {
392            Self::None => serializer.serialize_str("none"),
393            Self::Requirement(requirement) => requirement.serialize(serializer),
394        }
395    }
396}
397
398impl<'de, S: Deserialize<'de>> Deserialize<'de> for InterfaceRequirement<S> {
399    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
400    where
401        D: serde::Deserializer<'de>,
402    {
403        struct InterfaceRequirementVisitor<S>(PhantomData<S>);
404
405        impl<'de, S: Deserialize<'de>> Visitor<'de> for InterfaceRequirementVisitor<S> {
406            type Value = InterfaceRequirement<S>;
407
408            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409                f.write_str("`none` or a `{ min, max }` requirement table")
410            }
411
412            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
413            where
414                E: serde::de::Error,
415            {
416                if v == "none" {
417                    Ok(InterfaceRequirement::None)
418                } else {
419                    Err(E::invalid_value(serde::de::Unexpected::Str(v), &self))
420                }
421            }
422
423            fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
424            where
425                M: MapAccess<'de>,
426            {
427                StandardRequirement::deserialize(MapAccessDeserializer::new(map))
428                    .map(InterfaceRequirement::Requirement)
429            }
430        }
431
432        deserializer.deserialize_any(InterfaceRequirementVisitor(PhantomData))
433    }
434}
435
436/// One declared standard-field value as it travels from the
437/// manifest to the resolved package model.  Mirrors the
438/// `DependencySource::Workspace` contract: `cabin-manifest`
439/// constructs `Declared` (literal) or `Workspace` (the
440/// `{ workspace = true }` opt-in marker), `cabin-workspace`
441/// rewrites every marker into `Inherited(value)` before any
442/// consumer sees the `Package`, and a marker that survives past
443/// the loader is a workspace invariant violation.  Marker
444/// semantics deliberately split by consumer: `.is_some()`-based
445/// relevance checks (`imposes_requirement`,
446/// `find_standard_flag_conflicts`, `is_empty`) count an
447/// unresolved marker as a declaration, while the `*_value()`
448/// accessors treat it as absent - both cases are unreachable
449/// post-loader under the rewrite invariant.
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451pub enum StandardDeclaration<S> {
452    /// Literal value written in this manifest → source `package`
453    /// (or `target` for a target-level field).
454    Declared(S),
455    /// Unresolved `{ workspace = true }` opt-in marker.
456    Workspace,
457    /// Value resolved from the workspace root's `[workspace]`
458    /// declaration → source `workspace`.
459    Inherited(S),
460}
461
462impl<S> StandardDeclaration<S> {
463    /// The resolved standard value.  `None` only for an unresolved
464    /// marker, which must not reach consumers (debug-asserted).
465    #[must_use]
466    pub fn value(self) -> Option<S> {
467        match self {
468            Self::Declared(s) | Self::Inherited(s) => Some(s),
469            Self::Workspace => {
470                debug_assert!(
471                    false,
472                    "unresolved `{{ workspace = true }}` standard marker reached a consumer"
473                );
474                None
475            }
476        }
477    }
478}
479
480// `Declared` and `Inherited` serialize as the bare value so the
481// canonical-metadata / index wire format is identical to a literal
482// declaration (publish bakes inherited values).  An unresolved
483// marker must never reach a serialization boundary.
484impl<S: Serialize> Serialize for StandardDeclaration<S> {
485    fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error>
486    where
487        Ser: serde::Serializer,
488    {
489        match self {
490            Self::Declared(s) | Self::Inherited(s) => s.serialize(serializer),
491            Self::Workspace => Err(serde::ser::Error::custom(
492                "unresolved `{ workspace = true }` standard marker cannot be serialized",
493            )),
494        }
495    }
496}
497
498// A bare value deserializes as `Declared`: a consumer re-parsing
499// published metadata sees a plain declaration.
500impl<'de, S: Deserialize<'de>> Deserialize<'de> for StandardDeclaration<S> {
501    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
502    where
503        D: serde::Deserializer<'de>,
504    {
505        S::deserialize(deserializer).map(Self::Declared)
506    }
507}
508
509/// The language fields shared by `[package]` and `[target.<name>]`:
510/// the four standard fields (`c-standard` / `cxx-standard` /
511/// `interface-c-standard` / `interface-cxx-standard`) plus the
512/// `gnu-extensions` boolean.  At `[package]` level each standard
513/// field may also be the `{ workspace = true }` opt-in marker;
514/// target-level fields are always `Declared` (the parser rejects
515/// markers there).  `gnu-extensions` is a plain boolean (no marker
516/// form): target level overrides package level, defaulting to
517/// `false`.  It selects GNU-extension compiler flag spellings only
518/// and never participates in interface compatibility.
519#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
520pub struct LanguageStandardSettings {
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub c_standard: Option<StandardDeclaration<CStandard>>,
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub cxx_standard: Option<StandardDeclaration<CxxStandard>>,
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    pub interface_c_standard: Option<StandardDeclaration<InterfaceRequirement<CStandard>>>,
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub interface_cxx_standard: Option<StandardDeclaration<InterfaceRequirement<CxxStandard>>>,
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub gnu_extensions: Option<bool>,
531}
532
533impl LanguageStandardSettings {
534    #[must_use]
535    pub fn is_empty(&self) -> bool {
536        self.c_standard.is_none()
537            && self.cxx_standard.is_none()
538            && self.interface_c_standard.is_none()
539            && self.interface_cxx_standard.is_none()
540            && self.gnu_extensions.is_none()
541    }
542
543    /// Resolved C implementation standard, when declared or
544    /// inherited.
545    #[must_use]
546    pub fn c_standard_value(&self) -> Option<CStandard> {
547        self.c_standard.and_then(StandardDeclaration::value)
548    }
549
550    /// Resolved C++ implementation standard, when declared or
551    /// inherited.
552    #[must_use]
553    pub fn cxx_standard_value(&self) -> Option<CxxStandard> {
554        self.cxx_standard.and_then(StandardDeclaration::value)
555    }
556
557    /// Resolved C interface requirement, when declared or inherited.
558    #[must_use]
559    pub fn interface_c_standard_value(&self) -> Option<InterfaceRequirement<CStandard>> {
560        self.interface_c_standard
561            .and_then(StandardDeclaration::value)
562    }
563
564    /// Resolved C++ interface requirement, when declared or
565    /// inherited.
566    #[must_use]
567    pub fn interface_cxx_standard_value(&self) -> Option<InterfaceRequirement<CxxStandard>> {
568        self.interface_cxx_standard
569            .and_then(StandardDeclaration::value)
570    }
571
572    /// First field carrying the unresolved `{ workspace = true }`
573    /// marker, for error reporting.
574    #[must_use]
575    pub fn workspace_marker_field(&self) -> Option<&'static str> {
576        if self.c_standard == Some(StandardDeclaration::Workspace) {
577            return Some("c-standard");
578        }
579        if self.cxx_standard == Some(StandardDeclaration::Workspace) {
580            return Some("cxx-standard");
581        }
582        if self.interface_c_standard == Some(StandardDeclaration::Workspace) {
583            return Some("interface-c-standard");
584        }
585        if self.interface_cxx_standard == Some(StandardDeclaration::Workspace) {
586            return Some("interface-cxx-standard");
587        }
588        None
589    }
590}
591
592/// Effective `gnu-extensions` value for one target: target override
593/// ▶ package ▶ `false`.
594#[must_use]
595pub fn effective_gnu_extensions(package: &LanguageStandardSettings, target: &Target) -> bool {
596    target
597        .language
598        .gnu_extensions
599        .or(package.gnu_extensions)
600        .unwrap_or(false)
601}
602
603/// Literal `[workspace]`-level standard default values that member
604/// packages opt into per field with `<field> = { workspace = true }`
605/// on `[package]`.  Plain values only - the opt-in marker is not
606/// accepted on the `[workspace]` table itself.
607#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
608pub struct WorkspaceStandardDefaults {
609    #[serde(rename = "c-standard", skip_serializing_if = "Option::is_none")]
610    pub c_standard: Option<CStandard>,
611    #[serde(rename = "cxx-standard", skip_serializing_if = "Option::is_none")]
612    pub cxx_standard: Option<CxxStandard>,
613    #[serde(
614        rename = "interface-c-standard",
615        skip_serializing_if = "Option::is_none"
616    )]
617    pub interface_c_standard: Option<InterfaceRequirement<CStandard>>,
618    #[serde(
619        rename = "interface-cxx-standard",
620        skip_serializing_if = "Option::is_none"
621    )]
622    pub interface_cxx_standard: Option<InterfaceRequirement<CxxStandard>>,
623}
624
625impl WorkspaceStandardDefaults {
626    #[must_use]
627    pub fn is_empty(&self) -> bool {
628        self.c_standard.is_none()
629            && self.cxx_standard.is_none()
630            && self.interface_c_standard.is_none()
631            && self.interface_cxx_standard.is_none()
632    }
633}
634
635/// Provenance of an effective implementation standard.
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
637#[serde(rename_all = "kebab-case")]
638pub enum LanguageStandardSource {
639    Package,
640    Target,
641    Workspace,
642}
643
644impl LanguageStandardSource {
645    pub const fn as_key(self) -> &'static str {
646        match self {
647            Self::Package => "package",
648            Self::Target => "target",
649            Self::Workspace => "workspace",
650        }
651    }
652}
653
654/// Provenance of an effective interface standard.
655/// `CompileStandard` marks the documented default: no interface
656/// field was declared, so the requirement equals the target's
657/// effective implementation standard.
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "kebab-case")]
660pub enum InterfaceStandardSource {
661    Target,
662    Package,
663    CompileStandard,
664    Workspace,
665}
666
667impl InterfaceStandardSource {
668    pub const fn as_key(self) -> &'static str {
669        match self {
670            Self::Target => "target",
671            Self::Package => "package",
672            Self::CompileStandard => "compile-standard",
673            Self::Workspace => "workspace",
674        }
675    }
676}
677
678/// A resolved implementation standard plus where it came from.
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
680pub struct ResolvedStandard<S> {
681    pub standard: S,
682    pub source: LanguageStandardSource,
683}
684
685/// A resolved interface requirement plus where it came from.
686#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
687pub struct InterfaceStandard<S> {
688    pub requirement: InterfaceRequirement<S>,
689    pub source: InterfaceStandardSource,
690}
691
692/// Package-level effective implementation standards.  `None` means
693/// no declaration anywhere - there is no built-in default, and a
694/// target that compiles the language without an effective standard
695/// is rejected at manifest load.
696#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
697pub struct ResolvedLanguageStandards {
698    pub c: Option<ResolvedStandard<CStandard>>,
699    pub cxx: Option<ResolvedStandard<CxxStandard>>,
700}
701
702/// Map a package-level declaration to its resolved standard and
703/// provenance: literal → `package`, workspace-inherited →
704/// `workspace`, absent (or an unresolved marker, debug-asserted
705/// here) → `None`.
706fn package_resolution<S: Copy>(
707    declaration: Option<StandardDeclaration<S>>,
708) -> Option<ResolvedStandard<S>> {
709    match declaration {
710        Some(StandardDeclaration::Declared(standard)) => Some(ResolvedStandard {
711            standard,
712            source: LanguageStandardSource::Package,
713        }),
714        Some(StandardDeclaration::Inherited(standard)) => Some(ResolvedStandard {
715            standard,
716            source: LanguageStandardSource::Workspace,
717        }),
718        Some(StandardDeclaration::Workspace) => {
719            debug_assert!(
720                false,
721                "unresolved `{{ workspace = true }}` standard marker reached resolution"
722            );
723            None
724        }
725        None => None,
726    }
727}
728
729/// Resolve the package-level effective standards from the
730/// `[package]` declarations (literal or workspace-inherited).
731#[must_use]
732pub fn resolve_language_standards(package: &LanguageStandardSettings) -> ResolvedLanguageStandards {
733    ResolvedLanguageStandards {
734        c: package_resolution(package.c_standard),
735        cxx: package_resolution(package.cxx_standard),
736    }
737}
738
739/// Effective C implementation standard for one target:
740/// target override ▶ package (literal or workspace-inherited).
741/// `None` when neither tier declares one.
742#[must_use]
743pub fn effective_c(
744    package: &ResolvedLanguageStandards,
745    target: &Target,
746) -> Option<ResolvedStandard<CStandard>> {
747    target
748        .language
749        .c_standard_value()
750        .map_or(package.c, |standard| {
751            Some(ResolvedStandard {
752                standard,
753                source: LanguageStandardSource::Target,
754            })
755        })
756}
757
758/// Effective C++ implementation standard for one target.
759#[must_use]
760pub fn effective_cxx(
761    package: &ResolvedLanguageStandards,
762    target: &Target,
763) -> Option<ResolvedStandard<CxxStandard>> {
764    target
765        .language
766        .cxx_standard_value()
767        .map_or(package.cxx, |standard| {
768            Some(ResolvedStandard {
769                standard,
770                source: LanguageStandardSource::Target,
771            })
772        })
773}
774
775/// Map a package-level *interface* declaration to its provenance:
776/// literal → `package`, workspace-inherited → `workspace`.
777fn interface_resolution<S: Copy>(
778    declaration: Option<StandardDeclaration<InterfaceRequirement<S>>>,
779) -> Option<InterfaceStandard<S>> {
780    match declaration {
781        Some(StandardDeclaration::Declared(requirement)) => Some(InterfaceStandard {
782            requirement,
783            source: InterfaceStandardSource::Package,
784        }),
785        Some(StandardDeclaration::Inherited(requirement)) => Some(InterfaceStandard {
786            requirement,
787            source: InterfaceStandardSource::Workspace,
788        }),
789        Some(StandardDeclaration::Workspace) => {
790            debug_assert!(
791                false,
792                "unresolved `{{ workspace = true }}` standard marker reached resolution"
793            );
794            None
795        }
796        None => None,
797    }
798}
799
800/// Effective C interface requirement for a library-like target:
801/// target interface ▶ package interface (literal or
802/// workspace-inherited) ▶ the target's effective implementation
803/// standard, when one is declared (an interface may still default
804/// from an explicit implementation standard).  `None` when no tier
805/// yields a value.
806#[must_use]
807pub fn interface_c(
808    package: &ResolvedLanguageStandards,
809    package_settings: &LanguageStandardSettings,
810    target: &Target,
811) -> Option<InterfaceStandard<CStandard>> {
812    if let Some(requirement) = target.language.interface_c_standard_value() {
813        return Some(InterfaceStandard {
814            requirement,
815            source: InterfaceStandardSource::Target,
816        });
817    }
818    if let Some(interface) = interface_resolution(package_settings.interface_c_standard) {
819        return Some(interface);
820    }
821    effective_c(package, target).map(|resolved| InterfaceStandard {
822        requirement: InterfaceRequirement::Requirement(StandardRequirement {
823            min: resolved.standard,
824            max: None,
825        }),
826        source: InterfaceStandardSource::CompileStandard,
827    })
828}
829
830/// Effective C++ interface requirement for a library-like target.
831#[must_use]
832pub fn interface_cxx(
833    package: &ResolvedLanguageStandards,
834    package_settings: &LanguageStandardSettings,
835    target: &Target,
836) -> Option<InterfaceStandard<CxxStandard>> {
837    if let Some(requirement) = target.language.interface_cxx_standard_value() {
838        return Some(InterfaceStandard {
839            requirement,
840            source: InterfaceStandardSource::Target,
841        });
842    }
843    if let Some(interface) = interface_resolution(package_settings.interface_cxx_standard) {
844        return Some(interface);
845    }
846    effective_cxx(package, target).map(|resolved| InterfaceStandard {
847        requirement: InterfaceRequirement::Requirement(StandardRequirement {
848            min: resolved.standard,
849            max: None,
850        }),
851        source: InterfaceStandardSource::CompileStandard,
852    })
853}
854
855/// Whether a dependency target imposes an interface requirement for
856/// `language` on its consumers.  A language is relevant when the
857/// target has sources of that language, declares a target-level
858/// field for it (implementation or interface), or is header-only
859/// while the package declares a package-level *interface* standard
860/// for it.  Package-level implementation defaults never create
861/// relevance by themselves.
862#[must_use]
863pub fn imposes_requirement(
864    target: &Target,
865    package_settings: &LanguageStandardSettings,
866    language: SourceLanguage,
867) -> bool {
868    let has_sources = target
869        .sources
870        .iter()
871        .any(|s| classify_source(s) == Some(language));
872    let target_declares = match language {
873        SourceLanguage::C => {
874            target.language.c_standard.is_some() || target.language.interface_c_standard.is_some()
875        }
876        SourceLanguage::Cxx => {
877            target.language.cxx_standard.is_some()
878                || target.language.interface_cxx_standard.is_some()
879        }
880    };
881    let header_only_package_interface = target.kind.is_header_only()
882        && match language {
883            SourceLanguage::C => package_settings.interface_c_standard.is_some(),
884            SourceLanguage::Cxx => package_settings.interface_cxx_standard.is_some(),
885        };
886    has_sources || target_declares || header_only_package_interface
887}
888
889/// Token prefixes that select a language standard inside an
890/// escape-hatch flag list.
891pub const STANDARD_FLAG_PREFIXES: [&str; 3] = ["-std=", "--std=", "/std:"];
892
893/// A first-class standard declaration conflicting with an explicit
894/// standard flag in the same package's manifest-derived flags.
895#[derive(Debug, Error, Clone, PartialEq, Eq)]
896#[error(
897    "package `{package}` declares a first-class {} standard (`{field}`) but its `{flag_list}` also select one via `{flag}`; remove the flag, or drop the `{field}` declaration and keep the raw flag",
898    .language.human_label()
899)]
900pub struct StandardFlagConflict {
901    pub package: String,
902    pub language: SourceLanguage,
903    /// The manifest field family that was declared (`c-standard` or
904    /// `cxx-standard`, at package or target level).
905    pub field: &'static str,
906    /// The flag list carrying the conflicting token (`cflags` or
907    /// `cxxflags`).
908    pub flag_list: &'static str,
909    pub flag: String,
910    /// Scope of the conflicting declaration: `Some(target)` when a
911    /// target-level field created it (the ambiguity exists only on
912    /// that target's compiles), `None` when the package-level field
913    /// did (every compile of the language is ambiguous).  The build
914    /// planner uses the scope to surface a conflict only when a
915    /// matching compile is planned.
916    pub target: Option<String>,
917}
918
919fn first_standard_token(flags: &[String]) -> Option<String> {
920    flags
921        .iter()
922        .find(|f| STANDARD_FLAG_PREFIXES.iter().any(|p| f.starts_with(p)))
923        .cloned()
924}
925
926/// Detect the documented conflict candidates: an explicit
927/// first-class implementation standard declaration (package or
928/// target level) for a language whose manifest-derived flag list
929/// also pins a standard.  Runs on resolved flags *before* env /
930/// pkg-config augmentation so `CFLAGS` / `CXXFLAGS` remain exempt.
931///
932/// These are *candidates*, scoped per declaration: the build
933/// planner surfaces one only when a compile its scope covers is
934/// planned, so an unbuilt sibling target's declaration
935/// never gates a command that does not compile it.
936#[must_use]
937pub fn find_standard_flag_conflicts(
938    package: &str,
939    settings: &LanguageStandardSettings,
940    targets: &[Target],
941    flags: &ResolvedProfileFlags,
942) -> Vec<StandardFlagConflict> {
943    let mut out = Vec::new();
944    // C and C++ follow identical conflict logic; only the language,
945    // field / flag-list names, and standard declarations differ.
946    let mut check = |language: SourceLanguage,
947                     field: &'static str,
948                     flag_list: &'static str,
949                     list: &[String],
950                     package_declares: bool,
951                     target_declares: fn(&Target) -> bool| {
952        let Some(flag) = first_standard_token(list) else {
953            return;
954        };
955        let mut push = |flag: String, target: Option<String>| {
956            out.push(StandardFlagConflict {
957                package: package.to_owned(),
958                language,
959                field,
960                flag_list,
961                flag,
962                target,
963            });
964        };
965        if package_declares {
966            push(flag, None);
967        } else {
968            for target in targets {
969                if target_declares(target) {
970                    push(flag.clone(), Some(target.name.as_str().to_owned()));
971                }
972            }
973        }
974    };
975    check(
976        SourceLanguage::C,
977        "c-standard",
978        "cflags",
979        &flags.cflags,
980        settings.c_standard.is_some(),
981        |target| target.language.c_standard.is_some(),
982    );
983    check(
984        SourceLanguage::Cxx,
985        "cxx-standard",
986        "cxxflags",
987        &flags.cxxflags,
988        settings.cxx_standard.is_some(),
989        |target| target.language.cxx_standard.is_some(),
990    );
991    out
992}
993
994/// A target whose declared interface minimum is newer than the
995/// implementation standard its own sources compile with - a
996/// manifest contradiction, rejected at load.
997#[derive(Debug, Error, Clone, PartialEq, Eq)]
998#[error(
999    "target `{target}` in package `{package}` sets `{field} = \"{interface_min}\"` but compiles its {} sources as `{implementation}`; the target's own translation units could not include its own public headers - raise `{}` or lower the interface minimum",
1000    .language.human_label(),
1001    implementation_field(*.language)
1002)]
1003pub struct InterfaceStandardContradiction {
1004    pub package: String,
1005    pub target: String,
1006    pub language: SourceLanguage,
1007    /// The interface field family that was declared
1008    /// (`interface-c-standard` or `interface-cxx-standard`).
1009    pub field: &'static str,
1010    pub implementation: LanguageStandard,
1011    pub interface_min: LanguageStandard,
1012}
1013
1014/// Detect interface/implementation contradictions: for each
1015/// library-like target and language it compiles, the effective
1016/// interface minimum must not be newer than the effective
1017/// implementation standard (the target's own translation units
1018/// include its own public headers).  Runs on resolved declarations,
1019/// after workspace-marker resolution.  The compile-standard
1020/// interface fallback equals the implementation standard, so it can
1021/// never contradict.
1022#[must_use]
1023pub fn find_interface_standard_contradictions(
1024    package: &crate::Package,
1025) -> Vec<InterfaceStandardContradiction> {
1026    let resolved = resolve_language_standards(&package.language);
1027    let mut out = Vec::new();
1028    for target in &package.targets {
1029        let library_like = target.kind.is_library_like();
1030        if !library_like {
1031            continue;
1032        }
1033        let compiles = |language: SourceLanguage| {
1034            target
1035                .sources
1036                .iter()
1037                .any(|s| classify_source(s) == Some(language))
1038        };
1039        if compiles(SourceLanguage::C)
1040            && let (Some(implementation), Some(interface)) = (
1041                effective_c(&resolved, target),
1042                interface_c(&resolved, &package.language, target),
1043            )
1044            && let Some(min) = interface.requirement.min()
1045            && min > implementation.standard
1046        {
1047            out.push(InterfaceStandardContradiction {
1048                package: package.name.as_str().to_owned(),
1049                target: target.name.as_str().to_owned(),
1050                language: SourceLanguage::C,
1051                field: "interface-c-standard",
1052                implementation: LanguageStandard::C(implementation.standard),
1053                interface_min: LanguageStandard::C(min),
1054            });
1055        }
1056        if compiles(SourceLanguage::Cxx)
1057            && let (Some(implementation), Some(interface)) = (
1058                effective_cxx(&resolved, target),
1059                interface_cxx(&resolved, &package.language, target),
1060            )
1061            && let Some(min) = interface.requirement.min()
1062            && min > implementation.standard
1063        {
1064            out.push(InterfaceStandardContradiction {
1065                package: package.name.as_str().to_owned(),
1066                target: target.name.as_str().to_owned(),
1067                language: SourceLanguage::Cxx,
1068                field: "interface-cxx-standard",
1069                implementation: LanguageStandard::Cxx(implementation.standard),
1070                interface_min: LanguageStandard::Cxx(min),
1071            });
1072        }
1073    }
1074    out
1075}
1076
1077/// Per-package language-standard summary carried by
1078/// `BuildConfiguration`: package-level effective standards plus the
1079/// effective values for every target.  Values (not provenance) feed
1080/// the fingerprint; the whole struct feeds `cabin metadata` /
1081/// `cabin explain build-config`.  Absent entries mean the language
1082/// has no declared standard anywhere for that scope.
1083#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1084pub struct LanguageStandardsSummary {
1085    #[serde(default, skip_serializing_if = "Option::is_none")]
1086    pub c: Option<ResolvedStandard<CStandard>>,
1087    #[serde(default, skip_serializing_if = "Option::is_none")]
1088    pub cxx: Option<ResolvedStandard<CxxStandard>>,
1089    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1090    pub targets: BTreeMap<String, TargetStandardsSummary>,
1091}
1092
1093/// Effective standards for one target.  Interface entries are
1094/// present only for `library` / `header-only` kinds.
1095#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1096pub struct TargetStandardsSummary {
1097    #[serde(default, skip_serializing_if = "Option::is_none")]
1098    pub c: Option<ResolvedStandard<CStandard>>,
1099    #[serde(default, skip_serializing_if = "Option::is_none")]
1100    pub cxx: Option<ResolvedStandard<CxxStandard>>,
1101    #[serde(default, skip_serializing_if = "Option::is_none")]
1102    pub interface_c: Option<InterfaceStandard<CStandard>>,
1103    #[serde(default, skip_serializing_if = "Option::is_none")]
1104    pub interface_cxx: Option<InterfaceStandard<CxxStandard>>,
1105    /// Effective `gnu-extensions` value (target ▶ package ▶
1106    /// `false`).  Omitted from the serialized form when `false`.
1107    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1108    pub gnu_extensions: bool,
1109}
1110
1111impl LanguageStandardsSummary {
1112    /// Compute the summary from a package's declarations.
1113    #[must_use]
1114    pub fn from_package(package: &crate::Package) -> Self {
1115        let resolved = resolve_language_standards(&package.language);
1116        let targets = package
1117            .targets
1118            .iter()
1119            .map(|target| {
1120                let library_like = target.kind.is_library_like();
1121                let summary = TargetStandardsSummary {
1122                    c: effective_c(&resolved, target),
1123                    cxx: effective_cxx(&resolved, target),
1124                    interface_c: library_like
1125                        .then(|| interface_c(&resolved, &package.language, target))
1126                        .flatten(),
1127                    interface_cxx: library_like
1128                        .then(|| interface_cxx(&resolved, &package.language, target))
1129                        .flatten(),
1130                    gnu_extensions: effective_gnu_extensions(&package.language, target),
1131                };
1132                (target.name.as_str().to_owned(), summary)
1133            })
1134            .collect();
1135        Self {
1136            c: resolved.c,
1137            cxx: resolved.cxx,
1138            targets,
1139        }
1140    }
1141
1142    /// Stable line serialization for the build-configuration
1143    /// fingerprint.  Values only - provenance must not move the
1144    /// fingerprint, and absent standards (and the default
1145    /// `gnu-extensions = false`) contribute no line.
1146    #[must_use]
1147    pub fn fingerprint_lines(&self) -> Vec<String> {
1148        let mut lines = Vec::new();
1149        if let Some(resolved) = &self.c {
1150            lines.push(format!("c={}", resolved.standard));
1151        }
1152        if let Some(resolved) = &self.cxx {
1153            lines.push(format!("cxx={}", resolved.standard));
1154        }
1155        for (name, target) in &self.targets {
1156            lines.push(format!("target={name}"));
1157            if let Some(resolved) = &target.c {
1158                lines.push(format!("c={}", resolved.standard));
1159            }
1160            if let Some(resolved) = &target.cxx {
1161                lines.push(format!("cxx={}", resolved.standard));
1162            }
1163            if let Some(interface) = &target.interface_c {
1164                lines.push(format!("interface-c={}", interface.requirement));
1165            }
1166            if let Some(interface) = &target.interface_cxx {
1167                lines.push(format!("interface-cxx={}", interface.requirement));
1168            }
1169            if target.gnu_extensions {
1170                lines.push("gnu-extensions=true".to_owned());
1171            }
1172        }
1173        lines
1174    }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180    use crate::{TargetKind, TargetName};
1181    use camino::Utf8PathBuf;
1182
1183    fn target(kind: TargetKind, sources: &[&str], language: LanguageStandardSettings) -> Target {
1184        Target {
1185            name: TargetName::new("t").unwrap(),
1186            kind,
1187            sources: sources.iter().map(Utf8PathBuf::from).collect(),
1188            include_dirs: Vec::new(),
1189            defines: Vec::new(),
1190            deps: Vec::new(),
1191            required_features: Vec::new(),
1192            language,
1193        }
1194    }
1195
1196    fn requirement<S>(min: S) -> InterfaceRequirement<S> {
1197        InterfaceRequirement::Requirement(StandardRequirement { min, max: None })
1198    }
1199
1200    #[test]
1201    fn every_accepted_identifier_parses_and_round_trips() {
1202        for s in CStandard::ALL {
1203            assert_eq!(CStandard::parse(s.as_str()).unwrap(), s);
1204        }
1205        for s in CxxStandard::ALL {
1206            assert_eq!(CxxStandard::parse(s.as_str()).unwrap(), s);
1207        }
1208    }
1209
1210    #[test]
1211    fn aliases_normalize_immediately() {
1212        assert_eq!(CStandard::parse("c90").unwrap(), CStandard::C89);
1213        assert_eq!(CxxStandard::parse("c++03").unwrap(), CxxStandard::Cxx98);
1214        // The alias never survives as a spelling of its own.
1215        assert_eq!(CStandard::parse("c90").unwrap().as_str(), "c89");
1216        assert_eq!(CxxStandard::parse("c++03").unwrap().as_str(), "c++98");
1217    }
1218
1219    #[test]
1220    fn unknown_values_list_the_accepted_identifiers() {
1221        let err = CStandard::parse("c++17").unwrap_err();
1222        assert_eq!(
1223            err.to_string(),
1224            "unknown C standard `c++17`: expected one of c89, c99, c11, c17, c23"
1225        );
1226        let err = CxxStandard::parse("c++29").unwrap_err();
1227        assert_eq!(
1228            err.to_string(),
1229            "unknown C++ standard `c++29`: expected one of c++98, c++11, c++14, c++17, c++20, c++23, c++26"
1230        );
1231    }
1232
1233    #[test]
1234    fn gnu_spellings_are_ordinary_unknown_values() {
1235        for value in ["gnu89", "gnu99", "gnu11", "gnu17", "gnu23", "gnu90"] {
1236            let err = CStandard::parse(value).unwrap_err();
1237            assert!(
1238                matches!(&err, LanguageStandardParseError::Unknown { value: v, .. } if v == value),
1239                "unexpected error for {value}: {err}"
1240            );
1241            // No special-cased hint: gnu spellings are unknown
1242            // values like any other.
1243            assert!(!err.to_string().contains("gnu-extensions"));
1244        }
1245        for value in [
1246            "gnu++98", "gnu++03", "gnu++11", "gnu++14", "gnu++17", "gnu++20", "gnu++23", "gnu++26",
1247        ] {
1248            let err = CxxStandard::parse(value).unwrap_err();
1249            assert!(
1250                matches!(&err, LanguageStandardParseError::Unknown { value: v, .. } if v == value),
1251                "unexpected error for {value}: {err}"
1252            );
1253            assert!(!err.to_string().contains("gnu-extensions"));
1254        }
1255    }
1256
1257    #[test]
1258    fn range_like_values_get_the_reserved_diagnostic() {
1259        for value in [">=c11", "<=c17", ">c99", "<c23", "c11,c17", "c11, c17"] {
1260            let err = CStandard::parse(value).unwrap_err();
1261            assert!(
1262                matches!(err, LanguageStandardParseError::RangeReserved { .. }),
1263                "expected reserved-range error for {value}, got: {err}"
1264            );
1265            assert!(err.to_string().contains("reserved for a future version"));
1266        }
1267        for value in [">=c++17", "<=c++20", ">c++11", "<c++23", "c++17,c++20"] {
1268            let err = CxxStandard::parse(value).unwrap_err();
1269            assert!(
1270                matches!(err, LanguageStandardParseError::RangeReserved { .. }),
1271                "expected reserved-range error for {value}, got: {err}"
1272            );
1273        }
1274        // The interface parsers share the same rejection.
1275        assert!(matches!(
1276            parse_interface_c(">=c11").unwrap_err(),
1277            LanguageStandardParseError::RangeReserved { .. }
1278        ));
1279        assert!(matches!(
1280            parse_interface_cxx(">=c++17").unwrap_err(),
1281            LanguageStandardParseError::RangeReserved { .. }
1282        ));
1283    }
1284
1285    #[test]
1286    fn none_is_interface_only() {
1287        assert_eq!(
1288            parse_interface_c("none").unwrap(),
1289            InterfaceRequirement::None
1290        );
1291        assert_eq!(
1292            parse_interface_cxx("none").unwrap(),
1293            InterfaceRequirement::None
1294        );
1295        let err = CStandard::parse("none").unwrap_err();
1296        assert!(
1297            matches!(err, LanguageStandardParseError::NoneOnImplementation { .. }),
1298            "expected misplaced-none error, got: {err}"
1299        );
1300        assert!(err.to_string().contains("interface-c-standard"));
1301        let err = CxxStandard::parse("none").unwrap_err();
1302        assert!(matches!(
1303            err,
1304            LanguageStandardParseError::NoneOnImplementation { .. }
1305        ));
1306    }
1307
1308    #[test]
1309    fn interface_parsers_accept_every_identifier_and_alias() {
1310        for s in CStandard::ALL {
1311            assert_eq!(parse_interface_c(s.as_str()).unwrap(), requirement(s));
1312        }
1313        for s in CxxStandard::ALL {
1314            assert_eq!(parse_interface_cxx(s.as_str()).unwrap(), requirement(s));
1315        }
1316        assert_eq!(
1317            parse_interface_c("c90").unwrap(),
1318            requirement(CStandard::C89)
1319        );
1320        assert_eq!(
1321            parse_interface_cxx("c++03").unwrap(),
1322            requirement(CxxStandard::Cxx98)
1323        );
1324    }
1325
1326    #[test]
1327    fn standards_order_chronologically() {
1328        assert!(CStandard::C89 < CStandard::C99);
1329        assert!(CStandard::C99 < CStandard::C11);
1330        assert!(CStandard::C11 < CStandard::C17);
1331        assert!(CStandard::C17 < CStandard::C23);
1332        assert!(CxxStandard::Cxx98 < CxxStandard::Cxx11);
1333        assert!(CxxStandard::Cxx11 < CxxStandard::Cxx14);
1334        assert!(CxxStandard::Cxx14 < CxxStandard::Cxx17);
1335        assert!(CxxStandard::Cxx17 < CxxStandard::Cxx20);
1336        assert!(CxxStandard::Cxx20 < CxxStandard::Cxx23);
1337        assert!(CxxStandard::Cxx23 < CxxStandard::Cxx26);
1338    }
1339
1340    #[test]
1341    fn msvc_spellings_cover_exactly_the_stable_flags() {
1342        assert_eq!(
1343            LanguageStandard::Cxx(CxxStandard::Cxx20).msvc_spelling(),
1344            Some("/std:c++20")
1345        );
1346        assert_eq!(
1347            LanguageStandard::C(CStandard::C17).msvc_spelling(),
1348            Some("/std:c17")
1349        );
1350        assert_eq!(LanguageStandard::C(CStandard::C99).msvc_spelling(), None);
1351        assert_eq!(
1352            LanguageStandard::Cxx(CxxStandard::Cxx23).msvc_spelling(),
1353            None
1354        );
1355        assert_eq!(
1356            LanguageStandard::Cxx(CxxStandard::Cxx26).msvc_spelling(),
1357            None
1358        );
1359        assert_eq!(
1360            LanguageStandard::Cxx(CxxStandard::Cxx11).msvc_spelling(),
1361            None
1362        );
1363    }
1364
1365    #[test]
1366    fn standard_requirement_serde_round_trips_preserving_max() {
1367        let min_only = StandardRequirement {
1368            min: CxxStandard::Cxx17,
1369            max: None,
1370        };
1371        let json = serde_json::to_string(&min_only).unwrap();
1372        // `max` stays in the serialized form even while reserved.
1373        assert_eq!(json, r#"{"min":"c++17","max":null}"#);
1374        let parsed: StandardRequirement<CxxStandard> = serde_json::from_str(&json).unwrap();
1375        assert_eq!(parsed, min_only);
1376
1377        let with_max = StandardRequirement {
1378            min: CStandard::C11,
1379            max: Some(CStandard::C17),
1380        };
1381        let json = serde_json::to_string(&with_max).unwrap();
1382        assert_eq!(json, r#"{"min":"c11","max":"c17"}"#);
1383        let parsed: StandardRequirement<CStandard> = serde_json::from_str(&json).unwrap();
1384        assert_eq!(parsed, with_max);
1385
1386        // A missing `max` still deserializes (as unpopulated).
1387        let parsed: StandardRequirement<CStandard> =
1388            serde_json::from_str(r#"{"min":"c11"}"#).unwrap();
1389        assert_eq!(parsed.max, None);
1390        // Unknown future range syntax falls through
1391        // `deny_unknown_fields`.
1392        assert!(
1393            serde_json::from_str::<StandardRequirement<CStandard>>(
1394                r#"{"min":"c11","exact":"c17"}"#
1395            )
1396            .is_err()
1397        );
1398    }
1399
1400    #[test]
1401    fn interface_requirement_serde_round_trips() {
1402        let none: InterfaceRequirement<CxxStandard> = InterfaceRequirement::None;
1403        let json = serde_json::to_string(&none).unwrap();
1404        assert_eq!(json, "\"none\"");
1405        let parsed: InterfaceRequirement<CxxStandard> = serde_json::from_str(&json).unwrap();
1406        assert_eq!(parsed, none);
1407
1408        let req = requirement(CxxStandard::Cxx20);
1409        let json = serde_json::to_string(&req).unwrap();
1410        assert_eq!(json, r#"{"min":"c++20","max":null}"#);
1411        let parsed: InterfaceRequirement<CxxStandard> = serde_json::from_str(&json).unwrap();
1412        assert_eq!(parsed, req);
1413
1414        // A bare standard string is not a serialized requirement.
1415        assert!(serde_json::from_str::<InterfaceRequirement<CxxStandard>>("\"c++20\"").is_err());
1416    }
1417
1418    #[test]
1419    fn interface_requirement_displays_min_max_and_none() {
1420        assert_eq!(
1421            InterfaceRequirement::<CxxStandard>::None.to_string(),
1422            "none"
1423        );
1424        assert_eq!(requirement(CxxStandard::Cxx17).to_string(), "c++17");
1425        assert_eq!(
1426            InterfaceRequirement::Requirement(StandardRequirement {
1427                min: CStandard::C11,
1428                max: Some(CStandard::C17),
1429            })
1430            .to_string(),
1431            "c11..c17"
1432        );
1433    }
1434
1435    #[test]
1436    fn gnu_extensions_default_false_with_target_over_package() {
1437        let plain = target(
1438            TargetKind::Executable,
1439            &["a.cc"],
1440            LanguageStandardSettings::default(),
1441        );
1442        let none = LanguageStandardSettings::default();
1443        assert!(!effective_gnu_extensions(&none, &plain));
1444
1445        let package_on = LanguageStandardSettings {
1446            gnu_extensions: Some(true),
1447            ..Default::default()
1448        };
1449        assert!(effective_gnu_extensions(&package_on, &plain));
1450
1451        let target_off = target(
1452            TargetKind::Executable,
1453            &["a.cc"],
1454            LanguageStandardSettings {
1455                gnu_extensions: Some(false),
1456                ..Default::default()
1457            },
1458        );
1459        assert!(!effective_gnu_extensions(&package_on, &target_off));
1460
1461        let target_on = target(
1462            TargetKind::Executable,
1463            &["a.cc"],
1464            LanguageStandardSettings {
1465                gnu_extensions: Some(true),
1466                ..Default::default()
1467            },
1468        );
1469        assert!(effective_gnu_extensions(&none, &target_on));
1470    }
1471
1472    #[test]
1473    fn effective_standard_prefers_target_then_package_then_none() {
1474        let undeclared = resolve_language_standards(&LanguageStandardSettings::default());
1475        let plain = target(
1476            TargetKind::Executable,
1477            &["a.cc"],
1478            LanguageStandardSettings::default(),
1479        );
1480        assert_eq!(effective_cxx(&undeclared, &plain), None);
1481        assert_eq!(effective_c(&undeclared, &plain), None);
1482
1483        let package = resolve_language_standards(&LanguageStandardSettings {
1484            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx14)),
1485            ..Default::default()
1486        });
1487        let effective = effective_cxx(&package, &plain).unwrap();
1488        assert_eq!(effective.standard, CxxStandard::Cxx14);
1489        assert_eq!(effective.source, LanguageStandardSource::Package);
1490        // A declared C++ standard yields no effective C standard.
1491        assert_eq!(effective_c(&package, &plain), None);
1492
1493        let overridden = target(
1494            TargetKind::Executable,
1495            &["a.cc"],
1496            LanguageStandardSettings {
1497                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
1498                ..Default::default()
1499            },
1500        );
1501        let effective = effective_cxx(&package, &overridden).unwrap();
1502        assert_eq!(effective.standard, CxxStandard::Cxx20);
1503        assert_eq!(effective.source, LanguageStandardSource::Target);
1504    }
1505
1506    #[test]
1507    fn interface_standard_falls_back_to_explicit_compile_standard_or_none() {
1508        let package_settings = LanguageStandardSettings {
1509            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
1510            ..Default::default()
1511        };
1512        let resolved = resolve_language_standards(&package_settings);
1513        let lib = target(
1514            TargetKind::Library,
1515            &["a.cc"],
1516            LanguageStandardSettings::default(),
1517        );
1518        let interface = interface_cxx(&resolved, &package_settings, &lib).unwrap();
1519        assert_eq!(interface.requirement, requirement(CxxStandard::Cxx20));
1520        assert_eq!(interface.source, InterfaceStandardSource::CompileStandard);
1521        // No implementation or interface standard anywhere: no
1522        // interface value either (there is no built-in default).
1523        let undeclared = LanguageStandardSettings::default();
1524        let resolved_undeclared = resolve_language_standards(&undeclared);
1525        assert_eq!(interface_cxx(&resolved_undeclared, &undeclared, &lib), None);
1526        assert_eq!(interface_c(&resolved_undeclared, &undeclared, &lib), None);
1527
1528        let package_interface = LanguageStandardSettings {
1529            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
1530            interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1531                CxxStandard::Cxx17,
1532            ))),
1533            ..Default::default()
1534        };
1535        let resolved = resolve_language_standards(&package_interface);
1536        let interface = interface_cxx(&resolved, &package_interface, &lib).unwrap();
1537        assert_eq!(interface.requirement, requirement(CxxStandard::Cxx17));
1538        assert_eq!(interface.source, InterfaceStandardSource::Package);
1539
1540        let lib_override = target(
1541            TargetKind::Library,
1542            &["a.cc"],
1543            LanguageStandardSettings {
1544                interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1545                    CxxStandard::Cxx14,
1546                ))),
1547                ..Default::default()
1548            },
1549        );
1550        let interface = interface_cxx(&resolved, &package_interface, &lib_override).unwrap();
1551        assert_eq!(interface.requirement, requirement(CxxStandard::Cxx14));
1552        assert_eq!(interface.source, InterfaceStandardSource::Target);
1553    }
1554
1555    #[test]
1556    fn declared_none_interface_survives_resolution() {
1557        let package_settings = LanguageStandardSettings {
1558            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
1559            ..Default::default()
1560        };
1561        let resolved = resolve_language_standards(&package_settings);
1562        let lib = target(
1563            TargetKind::Library,
1564            &["a.cc"],
1565            LanguageStandardSettings {
1566                interface_cxx_standard: Some(StandardDeclaration::Declared(
1567                    InterfaceRequirement::None,
1568                )),
1569                ..Default::default()
1570            },
1571        );
1572        let interface = interface_cxx(&resolved, &package_settings, &lib).unwrap();
1573        assert_eq!(interface.requirement, InterfaceRequirement::None);
1574        assert_eq!(interface.source, InterfaceStandardSource::Target);
1575        assert_eq!(interface.requirement.min(), None);
1576    }
1577
1578    #[test]
1579    fn imposes_requirement_relevance_rules() {
1580        let none = LanguageStandardSettings::default();
1581        // A pure-C library imposes no C++ requirement.
1582        let c_lib = target(
1583            TargetKind::Library,
1584            &["a.c"],
1585            LanguageStandardSettings::default(),
1586        );
1587        assert!(imposes_requirement(&c_lib, &none, SourceLanguage::C));
1588        assert!(!imposes_requirement(&c_lib, &none, SourceLanguage::Cxx));
1589
1590        // A package-level *implementation* default alone creates no
1591        // relevance for a target without that language.
1592        let package_impl = LanguageStandardSettings {
1593            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
1594            ..Default::default()
1595        };
1596        assert!(!imposes_requirement(
1597            &c_lib,
1598            &package_impl,
1599            SourceLanguage::Cxx
1600        ));
1601
1602        // A target-level field (implementation or interface) does.
1603        let declared = target(
1604            TargetKind::Library,
1605            &["a.c"],
1606            LanguageStandardSettings {
1607                interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1608                    CxxStandard::Cxx17,
1609                ))),
1610                ..Default::default()
1611            },
1612        );
1613        assert!(imposes_requirement(&declared, &none, SourceLanguage::Cxx));
1614
1615        // Header-only + package-level *interface* standard does.
1616        let header_only = target(
1617            TargetKind::HeaderOnly,
1618            &[],
1619            LanguageStandardSettings::default(),
1620        );
1621        assert!(!imposes_requirement(
1622            &header_only,
1623            &none,
1624            SourceLanguage::Cxx
1625        ));
1626        let package_interface = LanguageStandardSettings {
1627            interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1628                CxxStandard::Cxx20,
1629            ))),
1630            ..Default::default()
1631        };
1632        assert!(imposes_requirement(
1633            &header_only,
1634            &package_interface,
1635            SourceLanguage::Cxx
1636        ));
1637        // ... but not via a package-level implementation default.
1638        assert!(!imposes_requirement(
1639            &header_only,
1640            &package_impl,
1641            SourceLanguage::Cxx
1642        ));
1643    }
1644
1645    #[test]
1646    fn conflict_fires_only_for_declared_language_and_matching_bucket() {
1647        let flags = ResolvedProfileFlags {
1648            cxxflags: vec!["-std=c++14".to_owned()],
1649            ..Default::default()
1650        };
1651        let declared_cxx = LanguageStandardSettings {
1652            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1653            ..Default::default()
1654        };
1655
1656        // Nothing declared: never a conflict.
1657        assert!(
1658            find_standard_flag_conflicts("p", &LanguageStandardSettings::default(), &[], &flags)
1659                .is_empty()
1660        );
1661
1662        // Declared C++ + `-std=` in cxxflags: a package-scoped
1663        // conflict candidate.
1664        let conflicts = find_standard_flag_conflicts("p", &declared_cxx, &[], &flags);
1665        let conflict = conflicts.first().unwrap();
1666        assert_eq!(conflicts.len(), 1);
1667        assert_eq!(conflict.language, SourceLanguage::Cxx);
1668        assert_eq!(conflict.flag, "-std=c++14");
1669        assert_eq!(conflict.field, "cxx-standard");
1670        assert_eq!(conflict.target, None);
1671        assert!(conflict.to_string().contains("cxx-standard"));
1672
1673        // Declared C++ + `-std=` in cflags only: no conflict.
1674        let c_only_flags = ResolvedProfileFlags {
1675            cflags: vec!["-std=c99".to_owned()],
1676            ..Default::default()
1677        };
1678        assert!(find_standard_flag_conflicts("p", &declared_cxx, &[], &c_only_flags).is_empty());
1679
1680        // A target-level declaration counts as declared.
1681        let t = target(
1682            TargetKind::Executable,
1683            &["a.c"],
1684            LanguageStandardSettings {
1685                c_standard: Some(StandardDeclaration::Declared(CStandard::C17)),
1686                ..Default::default()
1687            },
1688        );
1689        let conflicts = find_standard_flag_conflicts(
1690            "p",
1691            &LanguageStandardSettings::default(),
1692            std::slice::from_ref(&t),
1693            &c_only_flags,
1694        );
1695        let conflict = conflicts.first().unwrap();
1696        assert_eq!(conflict.language, SourceLanguage::C);
1697        assert_eq!(conflict.flag_list, "cflags");
1698        // A target-level declaration scopes the candidate to that
1699        // target so the planner only surfaces it when the target's
1700        // compile is planned.
1701        assert_eq!(conflict.target.as_deref(), Some("t"));
1702
1703        // `/std:` and `--std=` prefixes are recognized too.
1704        let msvc_flags = ResolvedProfileFlags {
1705            cxxflags: vec!["/std:c++latest".to_owned()],
1706            ..Default::default()
1707        };
1708        assert!(!find_standard_flag_conflicts("p", &declared_cxx, &[], &msvc_flags).is_empty());
1709    }
1710
1711    fn package_with(targets: Vec<Target>, language: LanguageStandardSettings) -> crate::Package {
1712        use crate::{Package, PackageName};
1713        Package::new(
1714            PackageName::new("demo").unwrap(),
1715            semver::Version::parse("0.1.0").unwrap(),
1716            targets,
1717            Vec::new(),
1718        )
1719        .unwrap()
1720        .with_language(language)
1721    }
1722
1723    #[test]
1724    fn contradiction_fires_when_interface_minimum_exceeds_implementation() {
1725        // Target-level interface newer than the package
1726        // implementation standard the target compiles with.
1727        let lib = target(
1728            TargetKind::Library,
1729            &["a.cc"],
1730            LanguageStandardSettings {
1731                interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1732                    CxxStandard::Cxx20,
1733                ))),
1734                ..Default::default()
1735            },
1736        );
1737        let package = package_with(
1738            vec![lib],
1739            LanguageStandardSettings {
1740                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1741                ..Default::default()
1742            },
1743        );
1744        let contradictions = find_interface_standard_contradictions(&package);
1745        assert_eq!(contradictions.len(), 1);
1746        let contradiction = &contradictions[0];
1747        assert_eq!(contradiction.target, "t");
1748        assert_eq!(contradiction.field, "interface-cxx-standard");
1749        assert_eq!(
1750            contradiction.implementation,
1751            LanguageStandard::Cxx(CxxStandard::Cxx17)
1752        );
1753        assert_eq!(
1754            contradiction.interface_min,
1755            LanguageStandard::Cxx(CxxStandard::Cxx20)
1756        );
1757        let message = contradiction.to_string();
1758        assert!(
1759            message.contains("could not include its own public headers"),
1760            "message must state the reason plainly: {message}"
1761        );
1762
1763        // Same shape on the C side, via package-level interface.
1764        let c_lib = target(
1765            TargetKind::Library,
1766            &["a.c"],
1767            LanguageStandardSettings::default(),
1768        );
1769        let package = package_with(
1770            vec![c_lib],
1771            LanguageStandardSettings {
1772                c_standard: Some(StandardDeclaration::Declared(CStandard::C11)),
1773                interface_c_standard: Some(StandardDeclaration::Declared(requirement(
1774                    CStandard::C23,
1775                ))),
1776                ..Default::default()
1777            },
1778        );
1779        let contradictions = find_interface_standard_contradictions(&package);
1780        assert_eq!(contradictions.len(), 1);
1781        assert_eq!(contradictions[0].field, "interface-c-standard");
1782    }
1783
1784    #[test]
1785    fn contradiction_ignores_equal_older_none_and_non_compiling_targets() {
1786        // Interface at or below the implementation standard is fine.
1787        for interface in [CxxStandard::Cxx17, CxxStandard::Cxx14] {
1788            let lib = target(
1789                TargetKind::Library,
1790                &["a.cc"],
1791                LanguageStandardSettings {
1792                    interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1793                        interface,
1794                    ))),
1795                    ..Default::default()
1796                },
1797            );
1798            let package = package_with(
1799                vec![lib],
1800                LanguageStandardSettings {
1801                    cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1802                    ..Default::default()
1803                },
1804            );
1805            assert!(find_interface_standard_contradictions(&package).is_empty());
1806        }
1807
1808        // `none` imposes no minimum, so it cannot contradict.
1809        let lib = target(
1810            TargetKind::Library,
1811            &["a.cc"],
1812            LanguageStandardSettings {
1813                interface_cxx_standard: Some(StandardDeclaration::Declared(
1814                    InterfaceRequirement::None,
1815                )),
1816                ..Default::default()
1817            },
1818        );
1819        let package = package_with(
1820            vec![lib],
1821            LanguageStandardSettings {
1822                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1823                ..Default::default()
1824            },
1825        );
1826        assert!(find_interface_standard_contradictions(&package).is_empty());
1827
1828        // A header-only target has no translation units, so a newer
1829        // interface minimum is not a contradiction.
1830        let header_only = target(
1831            TargetKind::HeaderOnly,
1832            &[],
1833            LanguageStandardSettings::default(),
1834        );
1835        let package = package_with(
1836            vec![header_only],
1837            LanguageStandardSettings {
1838                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1839                interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1840                    CxxStandard::Cxx20,
1841                ))),
1842                ..Default::default()
1843            },
1844        );
1845        assert!(find_interface_standard_contradictions(&package).is_empty());
1846
1847        // A pure-C library with a newer C++ interface minimum has no
1848        // C++ translation units of its own.
1849        let c_lib = target(
1850            TargetKind::Library,
1851            &["a.c"],
1852            LanguageStandardSettings {
1853                interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1854                    CxxStandard::Cxx26,
1855                ))),
1856                ..Default::default()
1857            },
1858        );
1859        let package = package_with(
1860            vec![c_lib],
1861            LanguageStandardSettings {
1862                c_standard: Some(StandardDeclaration::Declared(CStandard::C11)),
1863                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1864                ..Default::default()
1865            },
1866        );
1867        assert!(find_interface_standard_contradictions(&package).is_empty());
1868
1869        // Executables never carry interface requirements.
1870        let exe = target(
1871            TargetKind::Executable,
1872            &["a.cc"],
1873            LanguageStandardSettings::default(),
1874        );
1875        let package = package_with(
1876            vec![exe],
1877            LanguageStandardSettings {
1878                cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1879                interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1880                    CxxStandard::Cxx20,
1881                ))),
1882                ..Default::default()
1883            },
1884        );
1885        assert!(find_interface_standard_contradictions(&package).is_empty());
1886    }
1887
1888    #[test]
1889    fn summary_lists_every_target_with_interface_only_for_library_like() {
1890        use crate::{Package, PackageName};
1891        let package = Package::new(
1892            PackageName::new("demo").unwrap(),
1893            semver::Version::parse("0.1.0").unwrap(),
1894            vec![
1895                target(
1896                    TargetKind::Executable,
1897                    &["main.cc"],
1898                    LanguageStandardSettings::default(),
1899                ),
1900                Target {
1901                    name: TargetName::new("core").unwrap(),
1902                    kind: TargetKind::Library,
1903                    sources: vec![Utf8PathBuf::from("core.cc")],
1904                    include_dirs: Vec::new(),
1905                    defines: Vec::new(),
1906                    deps: Vec::new(),
1907                    required_features: Vec::new(),
1908                    language: LanguageStandardSettings {
1909                        cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
1910                        interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1911                            CxxStandard::Cxx17,
1912                        ))),
1913                        gnu_extensions: Some(true),
1914                        ..Default::default()
1915                    },
1916                },
1917            ],
1918            Vec::new(),
1919        )
1920        .unwrap()
1921        .with_language(LanguageStandardSettings {
1922            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
1923            ..Default::default()
1924        });
1925        let summary = LanguageStandardsSummary::from_package(&package);
1926        assert_eq!(summary.cxx.unwrap().standard, CxxStandard::Cxx17);
1927        assert_eq!(summary.c, None);
1928        assert_eq!(summary.targets.len(), 2);
1929        let exe = &summary.targets["t"];
1930        assert!(exe.interface_c.is_none() && exe.interface_cxx.is_none());
1931        assert!(!exe.gnu_extensions);
1932        let core = &summary.targets["core"];
1933        assert_eq!(core.cxx.unwrap().standard, CxxStandard::Cxx20);
1934        assert_eq!(
1935            core.interface_cxx.unwrap().requirement,
1936            requirement(CxxStandard::Cxx17)
1937        );
1938        assert!(core.gnu_extensions);
1939        // No C standard is declared anywhere, so the library gets
1940        // no C interface entry either.
1941        assert_eq!(core.interface_c, None);
1942    }
1943
1944    #[test]
1945    fn package_level_interface_fields_are_inert_without_library_like_targets() {
1946        use crate::{Package, PackageName};
1947        // docs/language-standards.md: package-level interface fields
1948        // are "allowed, and inert, in packages without any"
1949        // library-like target - the summary must not attach them to
1950        // executables.
1951        let package = Package::new(
1952            PackageName::new("demo").unwrap(),
1953            semver::Version::parse("0.1.0").unwrap(),
1954            vec![target(
1955                TargetKind::Executable,
1956                &["main.cc"],
1957                LanguageStandardSettings::default(),
1958            )],
1959            Vec::new(),
1960        )
1961        .unwrap()
1962        .with_language(LanguageStandardSettings {
1963            interface_c_standard: Some(StandardDeclaration::Declared(requirement(CStandard::C17))),
1964            interface_cxx_standard: Some(StandardDeclaration::Declared(requirement(
1965                CxxStandard::Cxx20,
1966            ))),
1967            ..Default::default()
1968        });
1969        let summary = LanguageStandardsSummary::from_package(&package);
1970        let exe = &summary.targets["t"];
1971        assert!(
1972            exe.interface_c.is_none() && exe.interface_cxx.is_none(),
1973            "package-level interface fields must stay inert on executables"
1974        );
1975    }
1976
1977    #[test]
1978    fn fingerprint_lines_are_values_only_and_deterministic() {
1979        let mut summary = LanguageStandardsSummary::default();
1980        // Nothing declared anywhere: nothing to fingerprint.
1981        assert!(summary.fingerprint_lines().is_empty());
1982
1983        summary.c = Some(ResolvedStandard {
1984            standard: CStandard::C11,
1985            source: LanguageStandardSource::Package,
1986        });
1987        summary.cxx = Some(ResolvedStandard {
1988            standard: CxxStandard::Cxx17,
1989            source: LanguageStandardSource::Package,
1990        });
1991        let lines = summary.fingerprint_lines();
1992        assert_eq!(lines, vec!["c=c11".to_owned(), "cxx=c++17".to_owned()]);
1993
1994        // Provenance must not appear in the lines.
1995        summary.cxx = Some(ResolvedStandard {
1996            standard: CxxStandard::Cxx17,
1997            source: LanguageStandardSource::Workspace,
1998        });
1999        assert_eq!(summary.fingerprint_lines(), lines);
2000
2001        summary.targets.insert(
2002            "core".to_owned(),
2003            TargetStandardsSummary {
2004                c: summary.c,
2005                cxx: Some(ResolvedStandard {
2006                    standard: CxxStandard::Cxx20,
2007                    source: LanguageStandardSource::Target,
2008                }),
2009                interface_c: Some(InterfaceStandard {
2010                    requirement: InterfaceRequirement::None,
2011                    source: InterfaceStandardSource::Target,
2012                }),
2013                interface_cxx: Some(InterfaceStandard {
2014                    requirement: requirement(CxxStandard::Cxx17),
2015                    source: InterfaceStandardSource::Target,
2016                }),
2017                gnu_extensions: true,
2018            },
2019        );
2020        assert_eq!(
2021            summary.fingerprint_lines(),
2022            vec![
2023                "c=c11".to_owned(),
2024                "cxx=c++17".to_owned(),
2025                "target=core".to_owned(),
2026                "c=c11".to_owned(),
2027                "cxx=c++20".to_owned(),
2028                "interface-c=none".to_owned(),
2029                "interface-cxx=c++17".to_owned(),
2030                "gnu-extensions=true".to_owned(),
2031            ]
2032        );
2033    }
2034
2035    #[test]
2036    fn standard_declaration_serde_is_a_bare_value_and_rejects_markers() {
2037        let declared: StandardDeclaration<CxxStandard> =
2038            StandardDeclaration::Declared(CxxStandard::Cxx20);
2039        let inherited: StandardDeclaration<CxxStandard> =
2040            StandardDeclaration::Inherited(CxxStandard::Cxx20);
2041        assert_eq!(serde_json::to_string(&declared).unwrap(), "\"c++20\"");
2042        assert_eq!(serde_json::to_string(&inherited).unwrap(), "\"c++20\"");
2043        let marker: StandardDeclaration<CxxStandard> = StandardDeclaration::Workspace;
2044        assert!(serde_json::to_string(&marker).is_err());
2045        let parsed: StandardDeclaration<CxxStandard> = serde_json::from_str("\"c++20\"").unwrap();
2046        assert_eq!(parsed, StandardDeclaration::Declared(CxxStandard::Cxx20));
2047
2048        // Interface declarations carry the `{ min, max }` shape (or
2049        // `none`) through the same bare-value contract.
2050        let declared_interface: StandardDeclaration<InterfaceRequirement<CxxStandard>> =
2051            StandardDeclaration::Declared(requirement(CxxStandard::Cxx20));
2052        let json = serde_json::to_string(&declared_interface).unwrap();
2053        assert_eq!(json, r#"{"min":"c++20","max":null}"#);
2054        let parsed: StandardDeclaration<InterfaceRequirement<CxxStandard>> =
2055            serde_json::from_str(&json).unwrap();
2056        assert_eq!(
2057            parsed,
2058            StandardDeclaration::Declared(requirement(CxxStandard::Cxx20))
2059        );
2060        let parsed: StandardDeclaration<InterfaceRequirement<CxxStandard>> =
2061            serde_json::from_str("\"none\"").unwrap();
2062        assert_eq!(
2063            parsed,
2064            StandardDeclaration::Declared(InterfaceRequirement::None)
2065        );
2066    }
2067
2068    #[test]
2069    fn inherited_standard_resolves_with_workspace_source() {
2070        let settings = LanguageStandardSettings {
2071            cxx_standard: Some(StandardDeclaration::Inherited(CxxStandard::Cxx20)),
2072            ..Default::default()
2073        };
2074        let resolved = resolve_language_standards(&settings);
2075        let cxx = resolved.cxx.unwrap();
2076        assert_eq!(cxx.standard, CxxStandard::Cxx20);
2077        assert_eq!(cxx.source, LanguageStandardSource::Workspace);
2078        assert_eq!(resolved.c, None);
2079    }
2080
2081    #[test]
2082    fn inherited_interface_standard_resolves_with_workspace_source() {
2083        let settings = LanguageStandardSettings {
2084            cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
2085            interface_cxx_standard: Some(StandardDeclaration::Inherited(requirement(
2086                CxxStandard::Cxx17,
2087            ))),
2088            ..Default::default()
2089        };
2090        let resolved = resolve_language_standards(&settings);
2091        let lib = target(
2092            TargetKind::Library,
2093            &["a.cc"],
2094            LanguageStandardSettings::default(),
2095        );
2096        let interface = interface_cxx(&resolved, &settings, &lib).unwrap();
2097        assert_eq!(interface.requirement, requirement(CxxStandard::Cxx17));
2098        assert_eq!(interface.source, InterfaceStandardSource::Workspace);
2099    }
2100
2101    #[test]
2102    fn inherited_values_behave_like_declarations_for_conflicts_and_relevance() {
2103        let flags = ResolvedProfileFlags {
2104            cxxflags: vec!["-std=c++20".to_owned()],
2105            ..Default::default()
2106        };
2107        let inherited = LanguageStandardSettings {
2108            cxx_standard: Some(StandardDeclaration::Inherited(CxxStandard::Cxx17)),
2109            ..Default::default()
2110        };
2111        assert_eq!(
2112            find_standard_flag_conflicts("p", &inherited, &[], &flags).len(),
2113            1
2114        );
2115
2116        let header_only = target(
2117            TargetKind::HeaderOnly,
2118            &[],
2119            LanguageStandardSettings::default(),
2120        );
2121        let pkg = LanguageStandardSettings {
2122            interface_cxx_standard: Some(StandardDeclaration::Inherited(requirement(
2123                CxxStandard::Cxx20,
2124            ))),
2125            ..Default::default()
2126        };
2127        assert!(imposes_requirement(&header_only, &pkg, SourceLanguage::Cxx));
2128    }
2129
2130    #[test]
2131    fn fingerprint_is_identical_for_declared_and_inherited_values() {
2132        use crate::{Package, PackageName};
2133        let make = |decl: StandardDeclaration<CxxStandard>| {
2134            let package = Package::new(
2135                PackageName::new("demo").unwrap(),
2136                semver::Version::parse("0.1.0").unwrap(),
2137                vec![target(
2138                    TargetKind::Executable,
2139                    &["main.cc"],
2140                    LanguageStandardSettings::default(),
2141                )],
2142                Vec::new(),
2143            )
2144            .unwrap()
2145            .with_language(LanguageStandardSettings {
2146                cxx_standard: Some(decl),
2147                ..Default::default()
2148            });
2149            LanguageStandardsSummary::from_package(&package).fingerprint_lines()
2150        };
2151        assert_eq!(
2152            make(StandardDeclaration::Declared(CxxStandard::Cxx20)),
2153            make(StandardDeclaration::Inherited(CxxStandard::Cxx20))
2154        );
2155    }
2156
2157    #[test]
2158    fn workspace_marker_field_reports_the_first_marker() {
2159        assert_eq!(
2160            LanguageStandardSettings::default().workspace_marker_field(),
2161            None
2162        );
2163        let settings = LanguageStandardSettings {
2164            interface_c_standard: Some(StandardDeclaration::Workspace),
2165            ..Default::default()
2166        };
2167        assert_eq!(
2168            settings.workspace_marker_field(),
2169            Some("interface-c-standard")
2170        );
2171        let settings = LanguageStandardSettings {
2172            c_standard: Some(StandardDeclaration::Workspace),
2173            interface_c_standard: Some(StandardDeclaration::Workspace),
2174            ..Default::default()
2175        };
2176        assert_eq!(settings.workspace_marker_field(), Some("c-standard"));
2177    }
2178}