Skip to main content

cabin_core/
condition.rs

1//! Typed model for `cfg(...)` target-condition expressions.
2//!
3//! Cabin manifests can declare *target-specific* dependency
4//! tables, e.g.
5//!
6//! ```toml
7//! [target.'cfg(os = "linux")'.dependencies]
8//! epoll = "^1"
9//! ```
10//!
11//! The condition string between the parentheses is parsed into a
12//! [`Condition`] AST and evaluated against a [`TargetPlatform`]
13//! describing the current evaluation context (the host build
14//! platform in this step). Parsing and evaluation are pure,
15//! deterministic, and side-effect-free.
16//!
17//! Supported keys are intentionally narrow. The platform keys —
18//! `os`, `arch`, `family`, `env`, `abi`, `target` — are listed by
19//! the [`ConditionKey`] enum. `feature = "..."` evaluates against
20//! the owning package's enabled-feature set, and the compiler
21//! keys — `cc`, `cxx`, `cc_version`, `cxx_version` — evaluate
22//! against the *detected* toolchain; both are accepted on profile
23//! flag tables only (the manifest layer rejects them elsewhere).
24//! Any other key is rejected at parse time so manifests do not
25//! silently rely on a future detection layer.
26//!
27//! Public syntax is preserved as the canonical inner-expression
28//! string when round-tripped (see the `Display` impl on
29//! [`Condition`]); the manifest layer wraps it in `cfg(...)` and
30//! the metadata layer emits the bare inner form so JSON /
31//! on-disk shapes stay compact.
32
33use std::collections::BTreeSet;
34use std::fmt;
35use std::str::FromStr;
36
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38use thiserror::Error;
39
40/// Typed AST for a `cfg(...)` target condition.
41///
42/// The wire format matches the manifest text: a key/value
43/// (`key = "value"`) leaf, or one of the `all` / `any` / `not`
44/// combinators. Equality and ordering are structural, so
45/// identical expressions always compare equal regardless of
46/// whitespace or quote style in the original source.
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum Condition {
49    /// `key = "value"`. The key is restricted to the
50    /// [`ConditionKey`] set; the value is a free-form ASCII
51    /// string interpreted by [`evaluate`](Self::evaluate).
52    KeyValue { key: ConditionKey, value: String },
53    /// `feature = "name"`. Evaluates against the *enabled-feature
54    /// set of the package the condition belongs to*, not the
55    /// platform. Feature conditions are only meaningful — and only
56    /// accepted — in flag tables (`[target.'cfg(...)'.profile]`);
57    /// the manifest layer rejects a feature-referencing `cfg` that
58    /// gates a dependency table, because feature resolution itself
59    /// runs over the dependency graph and a feature→dependency edge
60    /// would be circular.
61    Feature(String),
62    /// `cc = "<family>"` / `cxx = "<family>"`. Matches when the
63    /// detected compiler in that slot is the named family
64    /// ([`crate::compiler::CompilerKind`] ids). When detection has
65    /// not run (fail-soft commands) or the slot is unresolved, the
66    /// detected family counts as `unknown`, which is matchable.
67    /// Compiler conditions are accepted on profile flag tables
68    /// only; the manifest layer rejects them on dependency /
69    /// toolchain / cache tables, where detection is unavailable.
70    CompilerFamily {
71        slot: CompilerSlot,
72        family: crate::compiler::CompilerKind,
73    },
74    /// `cc_version = "<req>"` / `cxx_version = "<req>"`. The value
75    /// is a `SemVer` requirement (the same grammar as dependency
76    /// versions, parsed leniently) matched against the detected
77    /// version zero-padded to `major.minor.patch`. No detected
78    /// version ⇒ `false`. The raw requirement string is preserved
79    /// verbatim so `Display` round-trips byte-identically.
80    CompilerVersionReq { slot: CompilerSlot, req: String },
81    /// `all(<conditions>)`. Empty `all()` is rejected at parse
82    /// time.
83    All(Vec<Condition>),
84    /// `any(<conditions>)`. Empty `any()` is rejected at parse
85    /// time.
86    Any(Vec<Condition>),
87    /// `not(<single condition>)`.
88    Not(Box<Condition>),
89}
90
91impl Condition {
92    /// Parse a full `cfg(...)` expression. The wrapping
93    /// `cfg(...)` is required so the parser is symmetric with
94    /// the manifest text users write.
95    ///
96    /// # Errors
97    /// Returns [`ConditionParseError::ExpectedCfgPrefix`] when the input is not
98    /// wrapped in `cfg(`, [`ConditionParseError::UnbalancedParens`] when the
99    /// trailing `)` is missing, and propagates any [`ConditionParseError`] from
100    /// parsing the inner expression.
101    pub fn parse_cfg(input: &str) -> Result<Self, ConditionParseError> {
102        let trimmed = input.trim();
103        let inner = trimmed
104            .strip_prefix("cfg")
105            .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?
106            .trim_start();
107        let inner = inner
108            .strip_prefix('(')
109            .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?;
110        let inner = inner
111            .strip_suffix(')')
112            .ok_or_else(|| ConditionParseError::UnbalancedParens(trimmed.to_owned()))?;
113        Self::parse_inner(inner)
114    }
115
116    /// Parse the inner expression of a `cfg(...)` form (no
117    /// `cfg(` prefix or trailing `)`). Useful for the metadata
118    /// round-trip path, where we store the inner form.
119    ///
120    /// # Errors
121    /// Returns a [`ConditionParseError`] when the expression is malformed —
122    /// e.g. an unsupported key, a missing `=` or quoted value, an empty
123    /// `all()`/`any()`, a `not()` of wrong arity, unbalanced parentheses, or
124    /// trailing input after the expression.
125    pub fn parse_inner(input: &str) -> Result<Self, ConditionParseError> {
126        let mut parser = Parser::new(input);
127        let cond = parser.parse_condition()?;
128        parser.expect_eof()?;
129        Ok(cond)
130    }
131
132    /// Evaluate this condition against the typed
133    /// [`ConditionContext`] — the host platform, the set of
134    /// features enabled on the owning package, and the detected
135    /// compiler identities. The result is fully determined by
136    /// those inputs and the condition's AST — no global state, no
137    /// environment lookup, no I/O.
138    ///
139    /// Contexts that carry no feature information (every
140    /// dependency-gating call) use
141    /// [`ConditionContext::platform_only`]; this is
142    /// correct-by-construction because a feature-referencing `cfg`
143    /// is rejected on dependency tables at manifest-load time, so a
144    /// `Feature` leaf can only be reached here through a flag table
145    /// that threaded the real enabled-feature set in.
146    pub fn evaluate(&self, ctx: &ConditionContext<'_>) -> bool {
147        match self {
148            Condition::KeyValue { key, value } => key.lookup(ctx.platform) == value,
149            Condition::Feature(name) => ctx.features.contains(name),
150            Condition::CompilerFamily { slot, family } => {
151                let detected = ctx
152                    .identity(*slot)
153                    .map_or(crate::compiler::CompilerKind::Unknown, |id| id.kind);
154                detected == *family
155            }
156            Condition::CompilerVersionReq { slot, req } => ctx
157                .identity(*slot)
158                .and_then(|id| id.version.as_ref())
159                .zip(crate::version_req::parse_lenient(req).ok())
160                .is_some_and(|(v, parsed)| {
161                    parsed.matches(&semver::Version::new(
162                        u64::from(v.major),
163                        u64::from(v.minor.unwrap_or(0)),
164                        u64::from(v.patch.unwrap_or(0)),
165                    ))
166                }),
167            Condition::All(items) => items.iter().all(|c| c.evaluate(ctx)),
168            Condition::Any(items) => items.iter().any(|c| c.evaluate(ctx)),
169            Condition::Not(inner) => !inner.evaluate(ctx),
170        }
171    }
172
173    /// Whether this condition references any `feature = "..."`
174    /// leaf. Used by the manifest layer to reject feature
175    /// conditions on dependency tables (where they would be
176    /// circular) while allowing them on flag tables.
177    pub fn references_feature(&self) -> bool {
178        match self {
179            Condition::Feature(_) => true,
180            Condition::KeyValue { .. }
181            | Condition::CompilerFamily { .. }
182            | Condition::CompilerVersionReq { .. } => false,
183            Condition::All(items) | Condition::Any(items) => {
184                items.iter().any(Condition::references_feature)
185            }
186            Condition::Not(inner) => inner.references_feature(),
187        }
188    }
189
190    /// Whether this condition references any compiler leaf
191    /// (`cc` / `cxx` / `cc_version` / `cxx_version`). Used by the
192    /// manifest layer to reject compiler conditions on tables
193    /// evaluated before toolchain detection runs (dependencies,
194    /// toolchain selection, compiler-cache selection).
195    pub fn references_compiler(&self) -> bool {
196        match self {
197            Condition::CompilerFamily { .. } | Condition::CompilerVersionReq { .. } => true,
198            Condition::KeyValue { .. } | Condition::Feature(_) => false,
199            Condition::All(items) | Condition::Any(items) => {
200                items.iter().any(Condition::references_compiler)
201            }
202            Condition::Not(inner) => inner.references_compiler(),
203        }
204    }
205}
206
207impl fmt::Display for Condition {
208    /// Canonical string form. Round-trips through
209    /// [`Condition::parse_inner`].
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        match self {
212            Condition::KeyValue { key, value } => write!(f, "{} = \"{}\"", key.as_str(), value),
213            Condition::Feature(name) => write!(f, "feature = \"{name}\""),
214            Condition::CompilerFamily { slot, family } => {
215                write!(f, "{} = \"{}\"", slot.family_key(), family.as_key())
216            }
217            Condition::CompilerVersionReq { slot, req } => {
218                write!(f, "{} = \"{}\"", slot.version_key(), req)
219            }
220            Condition::All(items) => {
221                f.write_str("all(")?;
222                write_list(f, items)?;
223                f.write_str(")")
224            }
225            Condition::Any(items) => {
226                f.write_str("any(")?;
227                write_list(f, items)?;
228                f.write_str(")")
229            }
230            Condition::Not(inner) => write!(f, "not({inner})"),
231        }
232    }
233}
234
235fn write_list(f: &mut fmt::Formatter<'_>, items: &[Condition]) -> fmt::Result {
236    for (i, c) in items.iter().enumerate() {
237        if i > 0 {
238            f.write_str(", ")?;
239        }
240        write!(f, "{c}")?;
241    }
242    Ok(())
243}
244
245/// Compiler slot a compiler condition tests: the detected C
246/// compiler (`cc` / `cc_version` keys) or the detected C++
247/// compiler (`cxx` / `cxx_version`). Deliberately not
248/// [`crate::toolchain::ToolKind`], which includes the archiver —
249/// conditions support compiler slots only.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
251pub enum CompilerSlot {
252    Cc,
253    Cxx,
254}
255
256impl CompilerSlot {
257    /// The family-condition cfg key (`cc` / `cxx`).
258    pub const fn family_key(self) -> &'static str {
259        match self {
260            CompilerSlot::Cc => "cc",
261            CompilerSlot::Cxx => "cxx",
262        }
263    }
264
265    /// The version-condition cfg key (`cc_version` / `cxx_version`).
266    pub const fn version_key(self) -> &'static str {
267        match self {
268            CompilerSlot::Cc => "cc_version",
269            CompilerSlot::Cxx => "cxx_version",
270        }
271    }
272}
273
274/// Recognized target-condition keys. Anything else is rejected
275/// at parse time.
276#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
277pub enum ConditionKey {
278    /// Operating system name (`linux`, `macos`, `windows`, …).
279    Os,
280    /// CPU architecture (`x86_64`, `aarch64`, …).
281    Arch,
282    /// OS family (`unix`, `windows`, …).
283    Family,
284    /// Toolchain environment (`gnu`, `musl`, `msvc`, …).
285    Env,
286    /// Application binary interface flavor (`eabi`, …).
287    Abi,
288    /// Full normalized target triple, when available.
289    Target,
290}
291
292impl ConditionKey {
293    pub const fn as_str(self) -> &'static str {
294        match self {
295            ConditionKey::Os => "os",
296            ConditionKey::Arch => "arch",
297            ConditionKey::Family => "family",
298            ConditionKey::Env => "env",
299            ConditionKey::Abi => "abi",
300            ConditionKey::Target => "target",
301        }
302    }
303
304    /// All recognized keys, in canonical declaration order.
305    pub const fn all() -> &'static [ConditionKey] {
306        &[
307            ConditionKey::Os,
308            ConditionKey::Arch,
309            ConditionKey::Family,
310            ConditionKey::Env,
311            ConditionKey::Abi,
312            ConditionKey::Target,
313        ]
314    }
315
316    fn lookup(self, platform: &TargetPlatform) -> &str {
317        match self {
318            ConditionKey::Os => platform.os.as_str(),
319            ConditionKey::Arch => platform.arch.as_str(),
320            ConditionKey::Family => platform.family.as_str(),
321            ConditionKey::Env => platform.env.as_str(),
322            ConditionKey::Abi => platform.abi.as_str(),
323            ConditionKey::Target => platform.target.as_str(),
324        }
325    }
326}
327
328impl FromStr for ConditionKey {
329    type Err = ();
330
331    fn from_str(s: &str) -> Result<Self, Self::Err> {
332        match s {
333            "os" => Ok(ConditionKey::Os),
334            "arch" => Ok(ConditionKey::Arch),
335            "family" => Ok(ConditionKey::Family),
336            "env" => Ok(ConditionKey::Env),
337            "abi" => Ok(ConditionKey::Abi),
338            "target" => Ok(ConditionKey::Target),
339            _ => Err(()),
340        }
341    }
342}
343
344/// Evaluation context for [`Condition::evaluate`]. Each field
345/// is a stable, normalized lowercase string. Unknown values
346/// flow through as the literal `unknown`, which is matchable in
347/// `cfg(...)` expressions.
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349pub struct TargetPlatform {
350    pub os: String,
351    pub arch: String,
352    pub family: String,
353    pub env: String,
354    pub abi: String,
355    pub target: String,
356}
357
358impl TargetPlatform {
359    /// Best-effort detection of the *host* platform — the
360    /// platform commands like `cabin build` execute on. Cabin
361    /// does not yet support cross-compilation; future steps may
362    /// add an explicit target-triple selection layer that wraps
363    /// this constructor.
364    pub fn current() -> Self {
365        let os = normalize_os(std::env::consts::OS);
366        let arch = normalize_arch(std::env::consts::ARCH);
367        let family = normalize_family(std::env::consts::FAMILY, &os);
368        let env = normalize_env(&os);
369        let abi = "unknown".to_owned();
370        let target = format!("{arch}-{family}-{os}");
371        Self {
372            os,
373            arch,
374            family,
375            env,
376            abi,
377            target,
378        }
379    }
380}
381
382fn normalize_os(raw: &str) -> String {
383    match raw {
384        "linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
385        | "android" | "ios" => raw.to_owned(),
386        // Map common aliases.
387        "darwin" => "macos".to_owned(),
388        "" => "unknown".to_owned(),
389        other => other.to_owned(),
390    }
391}
392
393fn normalize_arch(raw: &str) -> String {
394    match raw {
395        "x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
396        "" => "unknown".to_owned(),
397        other => other.to_owned(),
398    }
399}
400
401fn normalize_family(raw: &str, os: &str) -> String {
402    match raw {
403        "unix" | "windows" | "wasm" => raw.to_owned(),
404        _ => match os {
405            "linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
406            | "ios" => "unix".to_owned(),
407            "windows" => "windows".to_owned(),
408            _ => "unknown".to_owned(),
409        },
410    }
411}
412
413fn normalize_env(os: &str) -> String {
414    // The host environment cannot be detected from the Rust
415    // standard library alone. We map the obvious cases so users
416    // can write `cfg(env = "gnu")` etc., and fall back to
417    // `unknown` everywhere else so unsupported queries are
418    // explicit rather than silently false.
419    match os {
420        "linux" => "gnu".to_owned(),
421        "macos" | "ios" => "apple".to_owned(),
422        "windows" => "msvc".to_owned(),
423        _ => "unknown".to_owned(),
424    }
425}
426
427/// Evaluation context for [`Condition::evaluate`]. Bundles the
428/// host platform, the owning package's enabled-feature set, and
429/// the detected compiler identities so each leaf kind reads the
430/// input it is defined over. Platform-only call sites (dependency
431/// gating, toolchain / wrapper selection) use
432/// [`ConditionContext::platform_only`]; compiler identities are
433/// attached only on the flag-resolution path, the only place
434/// compiler-referencing leaves are reachable (the manifest layer
435/// rejects them elsewhere).
436#[derive(Debug, Clone, Copy)]
437pub struct ConditionContext<'a> {
438    pub platform: &'a TargetPlatform,
439    pub features: &'a BTreeSet<String>,
440    /// Detected C compiler identity, when detection has run and
441    /// resolved a C compiler.
442    pub cc: Option<&'a crate::compiler::CompilerIdentity>,
443    /// Detected C++ compiler identity, when detection has run.
444    pub cxx: Option<&'a crate::compiler::CompilerIdentity>,
445}
446
447static EMPTY_FEATURES: BTreeSet<String> = BTreeSet::new();
448
449impl<'a> ConditionContext<'a> {
450    /// Platform-only evaluation: no features, no detected
451    /// compilers. Correct for dependency gating and toolchain /
452    /// wrapper selection, where feature and compiler leaves are
453    /// rejected at manifest-load time.
454    pub fn platform_only(platform: &'a TargetPlatform) -> Self {
455        Self {
456            platform,
457            features: &EMPTY_FEATURES,
458            cc: None,
459            cxx: None,
460        }
461    }
462
463    /// Platform + enabled-feature evaluation (no detected
464    /// compilers attached yet).
465    pub fn with_features(platform: &'a TargetPlatform, features: &'a BTreeSet<String>) -> Self {
466        Self {
467            platform,
468            features,
469            cc: None,
470            cxx: None,
471        }
472    }
473
474    /// Attach detected compiler identities (flag-resolution path).
475    #[must_use]
476    pub fn with_compilers(
477        mut self,
478        cc: Option<&'a crate::compiler::CompilerIdentity>,
479        cxx: Option<&'a crate::compiler::CompilerIdentity>,
480    ) -> Self {
481        self.cc = cc;
482        self.cxx = cxx;
483        self
484    }
485
486    /// Detected identity for `slot`, when available.
487    pub fn identity(&self, slot: CompilerSlot) -> Option<&'a crate::compiler::CompilerIdentity> {
488        match slot {
489            CompilerSlot::Cc => self.cc,
490            CompilerSlot::Cxx => self.cxx,
491        }
492    }
493}
494
495// ---------------------------------------------------------------
496// Parser.
497// ---------------------------------------------------------------
498
499/// Errors produced while parsing a `cfg(...)` expression.
500#[derive(Debug, Clone, PartialEq, Eq, Error)]
501pub enum ConditionParseError {
502    #[error("expected a `cfg(...)` expression but found {0:?}")]
503    ExpectedCfgPrefix(String),
504
505    #[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
506    UnbalancedParens(String),
507
508    #[error(
509        "unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, target, cc, cxx, cc_version, cxx_version, and feature"
510    )]
511    UnsupportedKey { key: String },
512
513    #[error(
514        "unknown compiler family {value:?} for cfg key {key:?}; supported families are clang, apple-clang, clang-cl, gcc, msvc, and unknown"
515    )]
516    UnknownCompilerFamily { key: String, value: String },
517
518    #[error("invalid version requirement {value:?} for cfg key {key:?}: {message}")]
519    InvalidCompilerVersionReq {
520        key: String,
521        value: String,
522        message: String,
523    },
524
525    #[error("expected `=` after key {key:?} in cfg expression")]
526    ExpectedEquals { key: String },
527
528    #[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
529    ExpectedQuotedValue { key: String, found: String },
530
531    #[error("unterminated string literal in cfg expression: {0:?}")]
532    UnterminatedString(String),
533
534    #[error("trailing input after cfg expression: {0:?}")]
535    TrailingInput(String),
536
537    #[error("`all()` requires at least one condition")]
538    EmptyAll,
539
540    #[error("`any()` requires at least one condition")]
541    EmptyAny,
542
543    #[error("`not()` takes exactly one condition; found {0}")]
544    NotArity(usize),
545
546    #[error("expected `(` after {0}")]
547    ExpectedOpenParen(&'static str),
548
549    #[error("expected `)` to close {0}")]
550    ExpectedCloseParen(&'static str),
551
552    #[error("unexpected token in cfg expression: {0:?}")]
553    UnexpectedToken(String),
554
555    #[error("empty cfg expression")]
556    Empty,
557}
558
559struct Parser<'a> {
560    src: &'a str,
561    pos: usize,
562}
563
564impl<'a> Parser<'a> {
565    fn new(src: &'a str) -> Self {
566        Self { src, pos: 0 }
567    }
568
569    fn skip_whitespace(&mut self) {
570        while let Some(c) = self.peek_char() {
571            if c.is_whitespace() {
572                self.pos += c.len_utf8();
573            } else {
574                break;
575            }
576        }
577    }
578
579    fn peek_char(&self) -> Option<char> {
580        self.src[self.pos..].chars().next()
581    }
582
583    fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
584        self.skip_whitespace();
585        if self.pos < self.src.len() {
586            Err(ConditionParseError::TrailingInput(
587                self.src[self.pos..].to_owned(),
588            ))
589        } else {
590            Ok(())
591        }
592    }
593
594    fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
595        self.skip_whitespace();
596        if self.pos >= self.src.len() {
597            return Err(ConditionParseError::Empty);
598        }
599        // Read an identifier. It is either a combinator (`all`,
600        // `any`, `not`) or a key in the recognized set.
601        let ident = self.read_ident()?;
602        self.skip_whitespace();
603        match ident.as_str() {
604            "all" => {
605                self.expect_open_paren("all")?;
606                let items = self.parse_condition_list()?;
607                self.expect_close_paren("all")?;
608                if items.is_empty() {
609                    return Err(ConditionParseError::EmptyAll);
610                }
611                Ok(Condition::All(items))
612            }
613            "any" => {
614                self.expect_open_paren("any")?;
615                let items = self.parse_condition_list()?;
616                self.expect_close_paren("any")?;
617                if items.is_empty() {
618                    return Err(ConditionParseError::EmptyAny);
619                }
620                Ok(Condition::Any(items))
621            }
622            "not" => {
623                self.expect_open_paren("not")?;
624                let items = self.parse_condition_list()?;
625                self.expect_close_paren("not")?;
626                if items.len() != 1 {
627                    return Err(ConditionParseError::NotArity(items.len()));
628                }
629                let inner = items.into_iter().next().expect("len==1 above");
630                Ok(Condition::Not(Box::new(inner)))
631            }
632            other => {
633                // `feature` and the platform keys share the
634                // `ident = "value"` shape; parse the `= "value"`
635                // tail once, then dispatch on the identifier.
636                self.skip_whitespace();
637                if self.peek_char() != Some('=') {
638                    return Err(ConditionParseError::ExpectedEquals {
639                        key: other.to_owned(),
640                    });
641                }
642                self.pos += 1; // consume '='
643                self.skip_whitespace();
644                let value = self.read_quoted_string(other)?;
645                let family_slot = match other {
646                    "cc" => Some(CompilerSlot::Cc),
647                    "cxx" => Some(CompilerSlot::Cxx),
648                    _ => None,
649                };
650                if let Some(slot) = family_slot {
651                    let family =
652                        crate::compiler::CompilerKind::from_key(&value).ok_or_else(|| {
653                            ConditionParseError::UnknownCompilerFamily {
654                                key: other.to_owned(),
655                                value: value.clone(),
656                            }
657                        })?;
658                    return Ok(Condition::CompilerFamily { slot, family });
659                }
660                let version_slot = match other {
661                    "cc_version" => Some(CompilerSlot::Cc),
662                    "cxx_version" => Some(CompilerSlot::Cxx),
663                    _ => None,
664                };
665                if let Some(slot) = version_slot {
666                    if let Err(err) = crate::version_req::parse_lenient(&value) {
667                        return Err(ConditionParseError::InvalidCompilerVersionReq {
668                            key: other.to_owned(),
669                            value,
670                            message: err.to_string(),
671                        });
672                    }
673                    return Ok(Condition::CompilerVersionReq { slot, req: value });
674                }
675                if other == "feature" {
676                    Ok(Condition::Feature(value))
677                } else {
678                    let key = ConditionKey::from_str(other).map_err(|()| {
679                        ConditionParseError::UnsupportedKey {
680                            key: other.to_owned(),
681                        }
682                    })?;
683                    Ok(Condition::KeyValue { key, value })
684                }
685            }
686        }
687    }
688
689    fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
690        let mut items = Vec::new();
691        self.skip_whitespace();
692        if self.peek_char() == Some(')') {
693            return Ok(items);
694        }
695        loop {
696            let cond = self.parse_condition()?;
697            items.push(cond);
698            self.skip_whitespace();
699            match self.peek_char() {
700                Some(',') => {
701                    self.pos += 1;
702                    self.skip_whitespace();
703                }
704                _ => break,
705            }
706        }
707        Ok(items)
708    }
709
710    fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
711        self.skip_whitespace();
712        if self.peek_char() == Some('(') {
713            self.pos += 1;
714            Ok(())
715        } else {
716            Err(ConditionParseError::ExpectedOpenParen(what))
717        }
718    }
719
720    fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
721        self.skip_whitespace();
722        if self.peek_char() == Some(')') {
723            self.pos += 1;
724            Ok(())
725        } else {
726            Err(ConditionParseError::ExpectedCloseParen(what))
727        }
728    }
729
730    fn read_ident(&mut self) -> Result<String, ConditionParseError> {
731        let start = self.pos;
732        while let Some(c) = self.peek_char() {
733            if c.is_ascii_alphanumeric() || c == '_' {
734                self.pos += c.len_utf8();
735            } else {
736                break;
737            }
738        }
739        if start == self.pos {
740            return Err(ConditionParseError::UnexpectedToken(
741                self.src[self.pos..].to_owned(),
742            ));
743        }
744        Ok(self.src[start..self.pos].to_owned())
745    }
746
747    fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
748        if self.peek_char() != Some('"') {
749            // Capture the offending token (rest of input up to a
750            // delimiter) so the error message can show what we
751            // saw.
752            let rest_start = self.pos;
753            while let Some(c) = self.peek_char() {
754                if c == ',' || c == ')' || c.is_whitespace() {
755                    break;
756                }
757                self.pos += c.len_utf8();
758            }
759            return Err(ConditionParseError::ExpectedQuotedValue {
760                key: key.to_owned(),
761                found: self.src[rest_start..self.pos].to_owned(),
762            });
763        }
764        self.pos += 1;
765        let start = self.pos;
766        while let Some(c) = self.peek_char() {
767            if c == '"' {
768                let value = self.src[start..self.pos].to_owned();
769                self.pos += 1;
770                return Ok(value);
771            }
772            self.pos += c.len_utf8();
773        }
774        Err(ConditionParseError::UnterminatedString(
775            self.src[start..].to_owned(),
776        ))
777    }
778}
779
780// ---------------------------------------------------------------
781// Serde — Condition serializes as its canonical inner-expression
782// string form so on-disk metadata stays compact and stable.
783// ---------------------------------------------------------------
784
785impl Serialize for Condition {
786    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
787        s.serialize_str(&self.to_string())
788    }
789}
790
791impl<'de> Deserialize<'de> for Condition {
792    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
793        let raw = String::deserialize(d)?;
794        Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::compiler::{CompilerIdentity, CompilerKind, CompilerVersion};
802
803    fn identity(kind: CompilerKind, version: &str) -> CompilerIdentity {
804        CompilerIdentity {
805            kind,
806            version: CompilerVersion::parse(version),
807            target: None,
808            raw_version_line: format!("{kind} {version}"),
809        }
810    }
811
812    fn ctx_with_cxx<'a>(
813        platform: &'a TargetPlatform,
814        cxx: &'a CompilerIdentity,
815    ) -> ConditionContext<'a> {
816        ConditionContext::platform_only(platform).with_compilers(None, Some(cxx))
817    }
818
819    #[test]
820    fn parses_compiler_family_keys() {
821        let cond = Condition::parse_cfg(r#"cfg(cxx = "clang")"#).unwrap();
822        assert_eq!(
823            cond,
824            Condition::CompilerFamily {
825                slot: CompilerSlot::Cxx,
826                family: CompilerKind::Clang
827            }
828        );
829        let cond = Condition::parse_cfg(r#"cfg(cc = "gcc")"#).unwrap();
830        assert_eq!(
831            cond,
832            Condition::CompilerFamily {
833                slot: CompilerSlot::Cc,
834                family: CompilerKind::Gcc
835            }
836        );
837    }
838
839    #[test]
840    fn parses_compiler_version_keys() {
841        let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=18")"#).unwrap();
842        assert_eq!(
843            cond,
844            Condition::CompilerVersionReq {
845                slot: CompilerSlot::Cxx,
846                req: ">=18".to_owned()
847            }
848        );
849        assert!(Condition::parse_cfg(r#"cfg(cc_version = ">=12, <15")"#).is_ok());
850    }
851
852    #[test]
853    fn rejects_unknown_compiler_family_value() {
854        for bad in ["clang++", "Clang", "g++", "icc", ""] {
855            let raw = format!(r#"cfg(cxx = "{bad}")"#);
856            match Condition::parse_cfg(&raw).unwrap_err() {
857                ConditionParseError::UnknownCompilerFamily { key, value } => {
858                    assert_eq!(key, "cxx");
859                    assert_eq!(value, bad);
860                }
861                other => panic!("{raw}: unexpected {other:?}"),
862            }
863        }
864    }
865
866    #[test]
867    fn rejects_invalid_compiler_version_req() {
868        match Condition::parse_cfg(r#"cfg(cxx_version = "not a req")"#).unwrap_err() {
869            ConditionParseError::InvalidCompilerVersionReq { key, value, .. } => {
870                assert_eq!(key, "cxx_version");
871                assert_eq!(value, "not a req");
872            }
873            other => panic!("unexpected {other:?}"),
874        }
875    }
876
877    #[test]
878    fn evaluates_compiler_family_against_detected_identity() {
879        let platform = linux_x86_64();
880        let clang = identity(CompilerKind::Clang, "18.1.3");
881        let ctx = ctx_with_cxx(&platform, &clang);
882        assert!(
883            Condition::parse_cfg(r#"cfg(cxx = "clang")"#)
884                .unwrap()
885                .evaluate(&ctx)
886        );
887        assert!(
888            !Condition::parse_cfg(r#"cfg(cxx = "gcc")"#)
889                .unwrap()
890                .evaluate(&ctx)
891        );
892        // The cc slot is unresolved in this context: family "unknown"
893        // matches.
894        assert!(
895            Condition::parse_cfg(r#"cfg(cc = "unknown")"#)
896                .unwrap()
897                .evaluate(&ctx)
898        );
899        assert!(
900            !Condition::parse_cfg(r#"cfg(cc = "clang")"#)
901                .unwrap()
902                .evaluate(&ctx)
903        );
904    }
905
906    #[test]
907    fn evaluates_compiler_version_with_semver_partial_semantics() {
908        let platform = linux_x86_64();
909        let clang18 = identity(CompilerKind::Clang, "18.1.3");
910        let ctx = ctx_with_cxx(&platform, &clang18);
911        for (req, expect) in [
912            (">=18", true),
913            (">18", false), // semver partial: >18 means 19.0.0+
914            ("18", true),   // bare = caret: any 18.x
915            ("=18", true),
916            (">=16, <19", true),
917            (">=19", false),
918            ("<18", false),
919        ] {
920            let raw = format!(r#"cfg(cxx_version = "{req}")"#);
921            let cond = Condition::parse_cfg(&raw).unwrap();
922            assert_eq!(cond.evaluate(&ctx), expect, "{req} vs 18.1.3");
923        }
924    }
925
926    #[test]
927    fn compiler_version_zero_pads_missing_components() {
928        let platform = linux_x86_64();
929        let gcc14 = identity(CompilerKind::Gcc, "14.2");
930        let ctx = ctx_with_cxx(&platform, &gcc14);
931        let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=14.2")"#).unwrap();
932        assert!(cond.evaluate(&ctx)); // 14.2 compares as 14.2.0
933    }
934
935    #[test]
936    fn compiler_version_without_detected_version_is_false() {
937        let platform = linux_x86_64();
938        let unparsed = CompilerIdentity::unknown("mystery output");
939        let ctx = ctx_with_cxx(&platform, &unparsed);
940        assert!(
941            !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
942                .unwrap()
943                .evaluate(&ctx)
944        );
945        // ... and with no identity at all.
946        let bare = ConditionContext::platform_only(&platform);
947        assert!(
948            !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
949                .unwrap()
950                .evaluate(&bare)
951        );
952    }
953
954    #[test]
955    fn compiler_conditions_compose_with_platform_and_feature_leaves() {
956        let platform = linux_x86_64();
957        let clang = identity(CompilerKind::Clang, "18.1.3");
958        let features: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
959        let ctx = ConditionContext::with_features(&platform, &features)
960            .with_compilers(None, Some(&clang));
961        let cond = Condition::parse_cfg(
962            r#"cfg(all(os = "linux", feature = "simd", cxx = "clang", cxx_version = ">=18"))"#,
963        )
964        .unwrap();
965        assert!(cond.evaluate(&ctx));
966        let not = Condition::parse_cfg(r#"cfg(not(cxx = "clang"))"#).unwrap();
967        assert!(!not.evaluate(&ctx));
968    }
969
970    #[test]
971    fn references_compiler_walks_combinators() {
972        for (raw, expect) in [
973            (r#"cxx = "clang""#, true),
974            (r#"cc_version = ">=12""#, true),
975            (r#"all(os = "linux", cxx = "clang")"#, true),
976            (r#"not(cc = "msvc")"#, true),
977            (r#"all(os = "linux", feature = "simd")"#, false),
978            (r#"os = "linux""#, false),
979        ] {
980            assert_eq!(
981                Condition::parse_inner(raw).unwrap().references_compiler(),
982                expect,
983                "{raw}"
984            );
985        }
986    }
987
988    fn linux_x86_64() -> TargetPlatform {
989        TargetPlatform {
990            os: "linux".into(),
991            arch: "x86_64".into(),
992            family: "unix".into(),
993            env: "gnu".into(),
994            abi: "unknown".into(),
995            target: "x86_64-unix-linux".into(),
996        }
997    }
998
999    fn macos_aarch64() -> TargetPlatform {
1000        TargetPlatform {
1001            os: "macos".into(),
1002            arch: "aarch64".into(),
1003            family: "unix".into(),
1004            env: "apple".into(),
1005            abi: "unknown".into(),
1006            target: "aarch64-unix-macos".into(),
1007        }
1008    }
1009
1010    #[test]
1011    fn parses_simple_key_value() {
1012        let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1013        assert_eq!(
1014            cond,
1015            Condition::KeyValue {
1016                key: ConditionKey::Os,
1017                value: "linux".into()
1018            }
1019        );
1020    }
1021
1022    #[test]
1023    fn parses_each_supported_key() {
1024        for (raw, key) in [
1025            (r#"cfg(os = "linux")"#, ConditionKey::Os),
1026            (r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
1027            (r#"cfg(family = "unix")"#, ConditionKey::Family),
1028            (r#"cfg(env = "gnu")"#, ConditionKey::Env),
1029            (r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
1030            (
1031                r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
1032                ConditionKey::Target,
1033            ),
1034        ] {
1035            let cond = Condition::parse_cfg(raw).unwrap();
1036            match cond {
1037                Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
1038                other => panic!("{raw}: expected key/value, got {other:?}"),
1039            }
1040        }
1041    }
1042
1043    #[test]
1044    fn parses_all_any_not() {
1045        let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1046        let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1047        let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1048        assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
1049        assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
1050        assert!(matches!(not, Condition::Not(_)));
1051    }
1052
1053    #[test]
1054    fn rejects_unquoted_value() {
1055        let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
1056        match err {
1057            ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
1058            other => panic!("unexpected: {other:?}"),
1059        }
1060    }
1061
1062    #[test]
1063    fn rejects_unsupported_key() {
1064        let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
1065        match err {
1066            ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
1067            other => panic!("unexpected: {other:?}"),
1068        }
1069    }
1070
1071    #[test]
1072    fn rejects_empty_all_and_any() {
1073        assert!(matches!(
1074            Condition::parse_cfg("cfg(all())").unwrap_err(),
1075            ConditionParseError::EmptyAll
1076        ));
1077        assert!(matches!(
1078            Condition::parse_cfg("cfg(any())").unwrap_err(),
1079            ConditionParseError::EmptyAny
1080        ));
1081    }
1082
1083    #[test]
1084    fn rejects_not_with_arity_other_than_one() {
1085        let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
1086        assert!(matches!(err, ConditionParseError::NotArity(2)));
1087    }
1088
1089    #[test]
1090    fn rejects_missing_cfg_prefix() {
1091        assert!(matches!(
1092            Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
1093            ConditionParseError::ExpectedCfgPrefix(_)
1094        ));
1095    }
1096
1097    #[test]
1098    fn rejects_unbalanced_parens() {
1099        assert!(matches!(
1100            Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
1101            ConditionParseError::UnbalancedParens(_)
1102        ));
1103    }
1104
1105    #[test]
1106    fn evaluates_simple_key_value() {
1107        let linux = linux_x86_64();
1108        let macos = macos_aarch64();
1109        let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1110        assert!(cond.evaluate(&ConditionContext::platform_only(&linux)));
1111        assert!(!cond.evaluate(&ConditionContext::platform_only(&macos)));
1112    }
1113
1114    #[test]
1115    fn evaluates_all_any_not() {
1116        let linux = linux_x86_64();
1117        let macos = macos_aarch64();
1118        let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1119        let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1120        let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1121        assert!(all.evaluate(&ConditionContext::platform_only(&linux)));
1122        assert!(!all.evaluate(&ConditionContext::platform_only(&macos)));
1123        assert!(any.evaluate(&ConditionContext::platform_only(&linux)));
1124        assert!(any.evaluate(&ConditionContext::platform_only(&macos)));
1125        assert!(not.evaluate(&ConditionContext::platform_only(&linux)));
1126        assert!(not.evaluate(&ConditionContext::platform_only(&macos)));
1127    }
1128
1129    #[test]
1130    fn parses_and_evaluates_feature_leaf() {
1131        let linux = linux_x86_64();
1132        let cond = Condition::parse_cfg(r#"cfg(feature = "simd")"#).unwrap();
1133        assert_eq!(cond, Condition::Feature("simd".to_owned()));
1134        assert!(cond.references_feature());
1135        let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1136        assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1137        assert!(!cond.evaluate(&ConditionContext::platform_only(&linux)));
1138    }
1139
1140    #[test]
1141    fn evaluates_feature_combined_with_platform() {
1142        let linux = linux_x86_64();
1143        let macos = macos_aarch64();
1144        let cond = Condition::parse_cfg(r#"cfg(all(feature = "simd", arch = "x86_64"))"#).unwrap();
1145        assert!(cond.references_feature());
1146        let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1147        // Both the feature and the platform must hold.
1148        assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1149        assert!(!cond.evaluate(&ConditionContext::with_features(&macos, &enabled))); // wrong arch
1150        assert!(!cond.evaluate(&ConditionContext::platform_only(&linux))); // feature off
1151    }
1152
1153    #[test]
1154    fn references_feature_is_false_for_platform_only_conditions() {
1155        for raw in [
1156            r#"cfg(os = "linux")"#,
1157            r#"cfg(all(os = "linux", arch = "x86_64"))"#,
1158            r#"cfg(not(os = "windows"))"#,
1159        ] {
1160            assert!(!Condition::parse_cfg(raw).unwrap().references_feature());
1161        }
1162    }
1163
1164    #[test]
1165    fn display_round_trips_through_parse_inner() {
1166        for raw in [
1167            r#"os = "linux""#,
1168            r#"feature = "simd""#,
1169            r#"all(feature = "simd", arch = "x86_64")"#,
1170            r#"all(os = "linux", arch = "x86_64")"#,
1171            r#"any(os = "macos", os = "linux")"#,
1172            r#"not(os = "windows")"#,
1173            r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
1174            r#"cxx = "clang""#,
1175            r#"cc = "gcc""#,
1176            r#"cxx_version = ">=18""#,
1177            r#"cc_version = ">=12, <15""#,
1178            r#"all(cxx = "clang", cxx_version = ">=18")"#,
1179            r#"not(cxx = "msvc")"#,
1180            r#"all(cxx = "apple-clang", os = "macos")"#,
1181        ] {
1182            let cond = Condition::parse_inner(raw).unwrap();
1183            let rendered = cond.to_string();
1184            assert_eq!(rendered, raw, "round-trip should be byte-identical");
1185            let again = Condition::parse_inner(&rendered).unwrap();
1186            assert_eq!(cond, again);
1187        }
1188    }
1189
1190    #[test]
1191    fn current_target_platform_is_internally_consistent() {
1192        let p = TargetPlatform::current();
1193        // Each field is non-empty and lowercase ASCII.
1194        for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
1195            assert!(!v.is_empty());
1196            assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
1197        }
1198    }
1199
1200    #[test]
1201    fn deterministic_serialization_for_metadata_round_trip() {
1202        let cond = Condition::parse_cfg(
1203            r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
1204        )
1205        .unwrap();
1206        let json = serde_json::to_string(&cond).unwrap();
1207        let parsed: Condition = serde_json::from_str(&json).unwrap();
1208        assert_eq!(cond, parsed);
1209    }
1210}