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 or compiler-wrapper 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/// Normalized description of a host/target platform (os, arch,
345/// family, env, abi, target) - one input to
346/// [`Condition::evaluate`], not the evaluation context itself
347/// (see [`ConditionContext`]).  Each field is a stable,
348/// normalized lowercase string; unknown values flow through as
349/// the literal `unknown`, which is matchable in `cfg(...)`
350/// expressions.
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct TargetPlatform {
353    pub os: String,
354    pub arch: String,
355    pub family: String,
356    pub env: String,
357    pub abi: String,
358    pub target: String,
359}
360
361impl TargetPlatform {
362    /// Best-effort detection of the *host* platform - the
363    /// platform commands like `cabin build` execute on.  Cabin
364    /// does not yet support cross-compilation; future steps may
365    /// add an explicit target-triple selection layer that wraps
366    /// this constructor.
367    pub fn current() -> Self {
368        let os = normalize_os(std::env::consts::OS);
369        let arch = normalize_arch(std::env::consts::ARCH);
370        let family = normalize_family(std::env::consts::FAMILY, &os);
371        let env = normalize_env(&os);
372        let abi = "unknown".to_owned();
373        let target = format!("{arch}-{family}-{os}");
374        Self {
375            os,
376            arch,
377            family,
378            env,
379            abi,
380            target,
381        }
382    }
383}
384
385fn normalize_os(raw: &str) -> String {
386    match raw {
387        "linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
388        | "android" | "ios" => raw.to_owned(),
389        // Map common aliases.
390        "darwin" => "macos".to_owned(),
391        "" => "unknown".to_owned(),
392        other => other.to_owned(),
393    }
394}
395
396fn normalize_arch(raw: &str) -> String {
397    match raw {
398        "x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
399        "" => "unknown".to_owned(),
400        other => other.to_owned(),
401    }
402}
403
404fn normalize_family(raw: &str, os: &str) -> String {
405    match raw {
406        "unix" | "windows" | "wasm" => raw.to_owned(),
407        _ => match os {
408            "linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
409            | "ios" => "unix".to_owned(),
410            "windows" => "windows".to_owned(),
411            _ => "unknown".to_owned(),
412        },
413    }
414}
415
416fn normalize_env(os: &str) -> String {
417    // The host environment cannot be detected from the Rust
418    // standard library alone.  We map the obvious cases so users
419    // can write `cfg(env = "gnu")` etc., and fall back to
420    // `unknown` everywhere else so unsupported queries are
421    // explicit rather than silently false.
422    match os {
423        "linux" => "gnu".to_owned(),
424        "macos" | "ios" => "apple".to_owned(),
425        "windows" => "msvc".to_owned(),
426        _ => "unknown".to_owned(),
427    }
428}
429
430/// Evaluation context for [`Condition::evaluate`].  Bundles the
431/// host platform, the owning package's enabled-feature set, and
432/// the detected compiler identities so each leaf kind reads the
433/// input it is defined over.  Platform-only call sites (dependency
434/// gating, toolchain / wrapper selection) use
435/// [`ConditionContext::platform_only`]; compiler identities are
436/// attached only on the flag-resolution path, the only place
437/// compiler-referencing leaves are reachable (the manifest layer
438/// rejects them elsewhere).
439#[derive(Debug, Clone, Copy)]
440pub struct ConditionContext<'a> {
441    pub platform: &'a TargetPlatform,
442    pub features: &'a BTreeSet<String>,
443    /// Detected C compiler identity, when detection has run and
444    /// resolved a C compiler.
445    pub cc: Option<&'a crate::compiler::CompilerIdentity>,
446    /// Detected C++ compiler identity, when detection has run.
447    pub cxx: Option<&'a crate::compiler::CompilerIdentity>,
448}
449
450static EMPTY_FEATURES: BTreeSet<String> = BTreeSet::new();
451
452impl<'a> ConditionContext<'a> {
453    /// Platform-only evaluation: no features, no detected
454    /// compilers.  Correct for dependency gating and toolchain /
455    /// wrapper selection, where feature and compiler leaves are
456    /// rejected at manifest-load time.
457    pub fn platform_only(platform: &'a TargetPlatform) -> Self {
458        Self {
459            platform,
460            features: &EMPTY_FEATURES,
461            cc: None,
462            cxx: None,
463        }
464    }
465
466    /// Platform + enabled-feature evaluation (no detected
467    /// compilers attached yet).
468    pub fn with_features(platform: &'a TargetPlatform, features: &'a BTreeSet<String>) -> Self {
469        Self {
470            platform,
471            features,
472            cc: None,
473            cxx: None,
474        }
475    }
476
477    /// Attach detected compiler identities (flag-resolution path).
478    #[must_use]
479    pub fn with_compilers(
480        mut self,
481        cc: Option<&'a crate::compiler::CompilerIdentity>,
482        cxx: Option<&'a crate::compiler::CompilerIdentity>,
483    ) -> Self {
484        self.cc = cc;
485        self.cxx = cxx;
486        self
487    }
488
489    /// Detected identity for `slot`, when available.
490    pub fn identity(&self, slot: CompilerSlot) -> Option<&'a crate::compiler::CompilerIdentity> {
491        match slot {
492            CompilerSlot::Cc => self.cc,
493            CompilerSlot::Cxx => self.cxx,
494        }
495    }
496}
497
498// ---------------------------------------------------------------
499// Parser.
500// ---------------------------------------------------------------
501
502/// Errors produced while parsing a `cfg(...)` expression.
503#[derive(Debug, Clone, PartialEq, Eq, Error)]
504pub enum ConditionParseError {
505    #[error("expected a `cfg(...)` expression but found {0:?}")]
506    ExpectedCfgPrefix(String),
507
508    #[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
509    UnbalancedParens(String),
510
511    #[error(
512        "unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, target, cc, cxx, cc_version, cxx_version, and feature"
513    )]
514    UnsupportedKey { key: String },
515
516    #[error(
517        "unknown compiler family {value:?} for cfg key {key:?}; supported families are clang, apple-clang, clang-cl, gcc, msvc, and unknown"
518    )]
519    UnknownCompilerFamily { key: String, value: String },
520
521    #[error("invalid version requirement {value:?} for cfg key {key:?}: {message}")]
522    InvalidCompilerVersionReq {
523        key: String,
524        value: String,
525        message: String,
526    },
527
528    #[error("expected `=` after key {key:?} in cfg expression")]
529    ExpectedEquals { key: String },
530
531    #[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
532    ExpectedQuotedValue { key: String, found: String },
533
534    #[error("unterminated string literal in cfg expression: {0:?}")]
535    UnterminatedString(String),
536
537    #[error("trailing input after cfg expression: {0:?}")]
538    TrailingInput(String),
539
540    #[error("`all()` requires at least one condition")]
541    EmptyAll,
542
543    #[error("`any()` requires at least one condition")]
544    EmptyAny,
545
546    #[error("`not()` takes exactly one condition; found {0}")]
547    NotArity(usize),
548
549    #[error("expected `(` after {0}")]
550    ExpectedOpenParen(&'static str),
551
552    #[error("expected `)` to close {0}")]
553    ExpectedCloseParen(&'static str),
554
555    #[error("unexpected token in cfg expression: {0:?}")]
556    UnexpectedToken(String),
557
558    #[error("empty cfg expression")]
559    Empty,
560}
561
562struct Parser<'a> {
563    src: &'a str,
564    pos: usize,
565}
566
567impl<'a> Parser<'a> {
568    fn new(src: &'a str) -> Self {
569        Self { src, pos: 0 }
570    }
571
572    fn skip_whitespace(&mut self) {
573        while let Some(c) = self.peek_char() {
574            if c.is_whitespace() {
575                self.pos += c.len_utf8();
576            } else {
577                break;
578            }
579        }
580    }
581
582    fn peek_char(&self) -> Option<char> {
583        self.src[self.pos..].chars().next()
584    }
585
586    fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
587        self.skip_whitespace();
588        if self.pos < self.src.len() {
589            Err(ConditionParseError::TrailingInput(
590                self.src[self.pos..].to_owned(),
591            ))
592        } else {
593            Ok(())
594        }
595    }
596
597    fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
598        self.skip_whitespace();
599        if self.pos >= self.src.len() {
600            return Err(ConditionParseError::Empty);
601        }
602        // Read an identifier.  It is either a combinator (`all`,
603        // `any`, `not`) or a key in the recognized set.
604        let ident = self.read_ident()?;
605        self.skip_whitespace();
606        match ident.as_str() {
607            "all" => {
608                self.expect_open_paren("all")?;
609                let items = self.parse_condition_list()?;
610                self.expect_close_paren("all")?;
611                if items.is_empty() {
612                    return Err(ConditionParseError::EmptyAll);
613                }
614                Ok(Condition::All(items))
615            }
616            "any" => {
617                self.expect_open_paren("any")?;
618                let items = self.parse_condition_list()?;
619                self.expect_close_paren("any")?;
620                if items.is_empty() {
621                    return Err(ConditionParseError::EmptyAny);
622                }
623                Ok(Condition::Any(items))
624            }
625            "not" => {
626                self.expect_open_paren("not")?;
627                let items = self.parse_condition_list()?;
628                self.expect_close_paren("not")?;
629                if items.len() != 1 {
630                    return Err(ConditionParseError::NotArity(items.len()));
631                }
632                let inner = items.into_iter().next().expect("len==1 above");
633                Ok(Condition::Not(Box::new(inner)))
634            }
635            other => self.parse_leaf(other),
636        }
637    }
638
639    /// Parse a leaf condition of the `key = "value"` shape.  This
640    /// covers `feature`, the compiler-family (`cc`/`cxx`) and
641    /// compiler-version (`cc_version`/`cxx_version`) keys, and the
642    /// platform keys - everything that is not an `all`/`any`/`not`
643    /// combinator.
644    fn parse_leaf(&mut self, key: &str) -> Result<Condition, ConditionParseError> {
645        // `feature` and the platform keys share the `ident =
646        // "value"` shape; parse the `= "value"` tail once, then
647        // dispatch on the identifier.
648        self.skip_whitespace();
649        if self.peek_char() != Some('=') {
650            return Err(ConditionParseError::ExpectedEquals {
651                key: key.to_owned(),
652            });
653        }
654        self.pos += 1; // consume '='
655        self.skip_whitespace();
656        let value = self.read_quoted_string(key)?;
657        let family_slot = match key {
658            "cc" => Some(CompilerSlot::Cc),
659            "cxx" => Some(CompilerSlot::Cxx),
660            _ => None,
661        };
662        if let Some(slot) = family_slot {
663            let family = crate::compiler::CompilerKind::from_key(&value).ok_or_else(|| {
664                ConditionParseError::UnknownCompilerFamily {
665                    key: key.to_owned(),
666                    value: value.clone(),
667                }
668            })?;
669            return Ok(Condition::CompilerFamily { slot, family });
670        }
671        let version_slot = match key {
672            "cc_version" => Some(CompilerSlot::Cc),
673            "cxx_version" => Some(CompilerSlot::Cxx),
674            _ => None,
675        };
676        if let Some(slot) = version_slot {
677            if let Err(err) = crate::version_req::parse_lenient(&value) {
678                return Err(ConditionParseError::InvalidCompilerVersionReq {
679                    key: key.to_owned(),
680                    value,
681                    message: err.to_string(),
682                });
683            }
684            return Ok(Condition::CompilerVersionReq { slot, req: value });
685        }
686        if key == "feature" {
687            Ok(Condition::Feature(value))
688        } else {
689            let key =
690                ConditionKey::from_str(key).map_err(|()| ConditionParseError::UnsupportedKey {
691                    key: key.to_owned(),
692                })?;
693            Ok(Condition::KeyValue { key, value })
694        }
695    }
696
697    fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
698        let mut items = Vec::new();
699        self.skip_whitespace();
700        if self.peek_char() == Some(')') {
701            return Ok(items);
702        }
703        loop {
704            let cond = self.parse_condition()?;
705            items.push(cond);
706            self.skip_whitespace();
707            match self.peek_char() {
708                Some(',') => {
709                    self.pos += 1;
710                    self.skip_whitespace();
711                }
712                _ => break,
713            }
714        }
715        Ok(items)
716    }
717
718    fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
719        self.skip_whitespace();
720        if self.peek_char() == Some('(') {
721            self.pos += 1;
722            Ok(())
723        } else {
724            Err(ConditionParseError::ExpectedOpenParen(what))
725        }
726    }
727
728    fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
729        self.skip_whitespace();
730        if self.peek_char() == Some(')') {
731            self.pos += 1;
732            Ok(())
733        } else {
734            Err(ConditionParseError::ExpectedCloseParen(what))
735        }
736    }
737
738    fn read_ident(&mut self) -> Result<String, ConditionParseError> {
739        let start = self.pos;
740        while let Some(c) = self.peek_char() {
741            if c.is_ascii_alphanumeric() || c == '_' {
742                self.pos += c.len_utf8();
743            } else {
744                break;
745            }
746        }
747        if start == self.pos {
748            return Err(ConditionParseError::UnexpectedToken(
749                self.src[self.pos..].to_owned(),
750            ));
751        }
752        Ok(self.src[start..self.pos].to_owned())
753    }
754
755    fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
756        if self.peek_char() != Some('"') {
757            // Capture the offending token (rest of input up to a
758            // delimiter) so the error message can show what we
759            // saw.
760            let rest_start = self.pos;
761            while let Some(c) = self.peek_char() {
762                if c == ',' || c == ')' || c.is_whitespace() {
763                    break;
764                }
765                self.pos += c.len_utf8();
766            }
767            return Err(ConditionParseError::ExpectedQuotedValue {
768                key: key.to_owned(),
769                found: self.src[rest_start..self.pos].to_owned(),
770            });
771        }
772        self.pos += 1;
773        let start = self.pos;
774        while let Some(c) = self.peek_char() {
775            if c == '"' {
776                let value = self.src[start..self.pos].to_owned();
777                self.pos += 1;
778                return Ok(value);
779            }
780            self.pos += c.len_utf8();
781        }
782        Err(ConditionParseError::UnterminatedString(
783            self.src[start..].to_owned(),
784        ))
785    }
786}
787
788// ---------------------------------------------------------------
789// Serde - Condition serializes as its canonical inner-expression
790// string form so on-disk metadata stays compact and stable.
791// ---------------------------------------------------------------
792
793impl Serialize for Condition {
794    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
795        s.serialize_str(&self.to_string())
796    }
797}
798
799impl<'de> Deserialize<'de> for Condition {
800    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
801        let raw = String::deserialize(d)?;
802        Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809    use crate::compiler::{CompilerIdentity, CompilerKind, CompilerVersion};
810
811    fn identity(kind: CompilerKind, version: &str) -> CompilerIdentity {
812        CompilerIdentity {
813            kind,
814            version: CompilerVersion::parse(version),
815            target: None,
816            raw_version_line: format!("{kind} {version}"),
817        }
818    }
819
820    fn ctx_with_cxx<'a>(
821        platform: &'a TargetPlatform,
822        cxx: &'a CompilerIdentity,
823    ) -> ConditionContext<'a> {
824        ConditionContext::platform_only(platform).with_compilers(None, Some(cxx))
825    }
826
827    #[test]
828    fn parses_compiler_family_keys() {
829        let cond = Condition::parse_cfg(r#"cfg(cxx = "clang")"#).unwrap();
830        assert_eq!(
831            cond,
832            Condition::CompilerFamily {
833                slot: CompilerSlot::Cxx,
834                family: CompilerKind::Clang
835            }
836        );
837        let cond = Condition::parse_cfg(r#"cfg(cc = "gcc")"#).unwrap();
838        assert_eq!(
839            cond,
840            Condition::CompilerFamily {
841                slot: CompilerSlot::Cc,
842                family: CompilerKind::Gcc
843            }
844        );
845    }
846
847    #[test]
848    fn parses_compiler_version_keys() {
849        let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=18")"#).unwrap();
850        assert_eq!(
851            cond,
852            Condition::CompilerVersionReq {
853                slot: CompilerSlot::Cxx,
854                req: ">=18".to_owned()
855            }
856        );
857        assert!(Condition::parse_cfg(r#"cfg(cc_version = ">=12, <15")"#).is_ok());
858    }
859
860    #[test]
861    fn rejects_unknown_compiler_family_value() {
862        for bad in ["clang++", "Clang", "g++", "icc", ""] {
863            let raw = format!(r#"cfg(cxx = "{bad}")"#);
864            match Condition::parse_cfg(&raw).unwrap_err() {
865                ConditionParseError::UnknownCompilerFamily { key, value } => {
866                    assert_eq!(key, "cxx");
867                    assert_eq!(value, bad);
868                }
869                other => panic!("{raw}: unexpected {other:?}"),
870            }
871        }
872    }
873
874    #[test]
875    fn rejects_invalid_compiler_version_req() {
876        match Condition::parse_cfg(r#"cfg(cxx_version = "not a req")"#).unwrap_err() {
877            ConditionParseError::InvalidCompilerVersionReq { key, value, .. } => {
878                assert_eq!(key, "cxx_version");
879                assert_eq!(value, "not a req");
880            }
881            other => panic!("unexpected {other:?}"),
882        }
883    }
884
885    #[test]
886    fn evaluates_compiler_family_against_detected_identity() {
887        let platform = linux_x86_64();
888        let clang = identity(CompilerKind::Clang, "18.1.3");
889        let ctx = ctx_with_cxx(&platform, &clang);
890        assert!(
891            Condition::parse_cfg(r#"cfg(cxx = "clang")"#)
892                .unwrap()
893                .evaluate(&ctx)
894        );
895        assert!(
896            !Condition::parse_cfg(r#"cfg(cxx = "gcc")"#)
897                .unwrap()
898                .evaluate(&ctx)
899        );
900        // The cc slot is unresolved in this context: family "unknown"
901        // matches.
902        assert!(
903            Condition::parse_cfg(r#"cfg(cc = "unknown")"#)
904                .unwrap()
905                .evaluate(&ctx)
906        );
907        assert!(
908            !Condition::parse_cfg(r#"cfg(cc = "clang")"#)
909                .unwrap()
910                .evaluate(&ctx)
911        );
912    }
913
914    #[test]
915    fn evaluates_compiler_version_with_semver_partial_semantics() {
916        let platform = linux_x86_64();
917        let clang18 = identity(CompilerKind::Clang, "18.1.3");
918        let ctx = ctx_with_cxx(&platform, &clang18);
919        for (req, expect) in [
920            (">=18", true),
921            (">18", false), // semver partial: >18 means 19.0.0+
922            ("18", true),   // bare = caret: any 18.x
923            ("=18", true),
924            (">=16, <19", true),
925            (">=19", false),
926            ("<18", false),
927        ] {
928            let raw = format!(r#"cfg(cxx_version = "{req}")"#);
929            let cond = Condition::parse_cfg(&raw).unwrap();
930            assert_eq!(cond.evaluate(&ctx), expect, "{req} vs 18.1.3");
931        }
932    }
933
934    #[test]
935    fn compiler_version_zero_pads_missing_components() {
936        let platform = linux_x86_64();
937        let gcc14 = identity(CompilerKind::Gcc, "14.2");
938        let ctx = ctx_with_cxx(&platform, &gcc14);
939        let cond = Condition::parse_cfg(r#"cfg(cxx_version = ">=14.2")"#).unwrap();
940        assert!(cond.evaluate(&ctx)); // 14.2 compares as 14.2.0
941    }
942
943    #[test]
944    fn compiler_version_without_detected_version_is_false() {
945        let platform = linux_x86_64();
946        let unparsed = CompilerIdentity::unknown("mystery output");
947        let ctx = ctx_with_cxx(&platform, &unparsed);
948        assert!(
949            !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
950                .unwrap()
951                .evaluate(&ctx)
952        );
953        // ... and with no identity at all.
954        let bare = ConditionContext::platform_only(&platform);
955        assert!(
956            !Condition::parse_cfg(r#"cfg(cxx_version = ">=0")"#)
957                .unwrap()
958                .evaluate(&bare)
959        );
960    }
961
962    #[test]
963    fn compiler_conditions_compose_with_platform_and_feature_leaves() {
964        let platform = linux_x86_64();
965        let clang = identity(CompilerKind::Clang, "18.1.3");
966        let features: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
967        let ctx = ConditionContext::with_features(&platform, &features)
968            .with_compilers(None, Some(&clang));
969        let cond = Condition::parse_cfg(
970            r#"cfg(all(os = "linux", feature = "simd", cxx = "clang", cxx_version = ">=18"))"#,
971        )
972        .unwrap();
973        assert!(cond.evaluate(&ctx));
974        let not = Condition::parse_cfg(r#"cfg(not(cxx = "clang"))"#).unwrap();
975        assert!(!not.evaluate(&ctx));
976    }
977
978    #[test]
979    fn references_compiler_walks_combinators() {
980        for (raw, expect) in [
981            (r#"cxx = "clang""#, true),
982            (r#"cc_version = ">=12""#, true),
983            (r#"all(os = "linux", cxx = "clang")"#, true),
984            (r#"not(cc = "msvc")"#, true),
985            (r#"all(os = "linux", feature = "simd")"#, false),
986            (r#"os = "linux""#, false),
987        ] {
988            assert_eq!(
989                Condition::parse_inner(raw).unwrap().references_compiler(),
990                expect,
991                "{raw}"
992            );
993        }
994    }
995
996    fn linux_x86_64() -> TargetPlatform {
997        TargetPlatform {
998            os: "linux".into(),
999            arch: "x86_64".into(),
1000            family: "unix".into(),
1001            env: "gnu".into(),
1002            abi: "unknown".into(),
1003            target: "x86_64-unix-linux".into(),
1004        }
1005    }
1006
1007    fn macos_aarch64() -> TargetPlatform {
1008        TargetPlatform {
1009            os: "macos".into(),
1010            arch: "aarch64".into(),
1011            family: "unix".into(),
1012            env: "apple".into(),
1013            abi: "unknown".into(),
1014            target: "aarch64-unix-macos".into(),
1015        }
1016    }
1017
1018    #[test]
1019    fn parses_simple_key_value() {
1020        let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1021        assert_eq!(
1022            cond,
1023            Condition::KeyValue {
1024                key: ConditionKey::Os,
1025                value: "linux".into()
1026            }
1027        );
1028    }
1029
1030    #[test]
1031    fn parses_each_supported_key() {
1032        for (raw, key) in [
1033            (r#"cfg(os = "linux")"#, ConditionKey::Os),
1034            (r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
1035            (r#"cfg(family = "unix")"#, ConditionKey::Family),
1036            (r#"cfg(env = "gnu")"#, ConditionKey::Env),
1037            (r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
1038            (
1039                r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
1040                ConditionKey::Target,
1041            ),
1042        ] {
1043            let cond = Condition::parse_cfg(raw).unwrap();
1044            match cond {
1045                Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
1046                other => panic!("{raw}: expected key/value, got {other:?}"),
1047            }
1048        }
1049    }
1050
1051    #[test]
1052    fn parses_all_any_not() {
1053        let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1054        let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1055        let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1056        assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
1057        assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
1058        assert!(matches!(not, Condition::Not(_)));
1059    }
1060
1061    #[test]
1062    fn rejects_unquoted_value() {
1063        let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
1064        match err {
1065            ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
1066            other => panic!("unexpected: {other:?}"),
1067        }
1068    }
1069
1070    #[test]
1071    fn rejects_unsupported_key() {
1072        let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
1073        match err {
1074            ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
1075            other => panic!("unexpected: {other:?}"),
1076        }
1077    }
1078
1079    #[test]
1080    fn rejects_empty_all_and_any() {
1081        assert!(matches!(
1082            Condition::parse_cfg("cfg(all())").unwrap_err(),
1083            ConditionParseError::EmptyAll
1084        ));
1085        assert!(matches!(
1086            Condition::parse_cfg("cfg(any())").unwrap_err(),
1087            ConditionParseError::EmptyAny
1088        ));
1089    }
1090
1091    #[test]
1092    fn rejects_not_with_arity_other_than_one() {
1093        let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
1094        assert!(matches!(err, ConditionParseError::NotArity(2)));
1095    }
1096
1097    #[test]
1098    fn rejects_missing_cfg_prefix() {
1099        assert!(matches!(
1100            Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
1101            ConditionParseError::ExpectedCfgPrefix(_)
1102        ));
1103    }
1104
1105    #[test]
1106    fn rejects_unbalanced_parens() {
1107        assert!(matches!(
1108            Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
1109            ConditionParseError::UnbalancedParens(_)
1110        ));
1111    }
1112
1113    #[test]
1114    fn evaluates_simple_key_value() {
1115        let linux = linux_x86_64();
1116        let macos = macos_aarch64();
1117        let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
1118        assert!(cond.evaluate(&ConditionContext::platform_only(&linux)));
1119        assert!(!cond.evaluate(&ConditionContext::platform_only(&macos)));
1120    }
1121
1122    #[test]
1123    fn evaluates_all_any_not() {
1124        let linux = linux_x86_64();
1125        let macos = macos_aarch64();
1126        let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
1127        let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
1128        let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
1129        assert!(all.evaluate(&ConditionContext::platform_only(&linux)));
1130        assert!(!all.evaluate(&ConditionContext::platform_only(&macos)));
1131        assert!(any.evaluate(&ConditionContext::platform_only(&linux)));
1132        assert!(any.evaluate(&ConditionContext::platform_only(&macos)));
1133        assert!(not.evaluate(&ConditionContext::platform_only(&linux)));
1134        assert!(not.evaluate(&ConditionContext::platform_only(&macos)));
1135    }
1136
1137    #[test]
1138    fn parses_and_evaluates_feature_leaf() {
1139        let linux = linux_x86_64();
1140        let cond = Condition::parse_cfg(r#"cfg(feature = "simd")"#).unwrap();
1141        assert_eq!(cond, Condition::Feature("simd".to_owned()));
1142        assert!(cond.references_feature());
1143        let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1144        assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1145        assert!(!cond.evaluate(&ConditionContext::platform_only(&linux)));
1146    }
1147
1148    #[test]
1149    fn evaluates_feature_combined_with_platform() {
1150        let linux = linux_x86_64();
1151        let macos = macos_aarch64();
1152        let cond = Condition::parse_cfg(r#"cfg(all(feature = "simd", arch = "x86_64"))"#).unwrap();
1153        assert!(cond.references_feature());
1154        let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
1155        // Both the feature and the platform must hold.
1156        assert!(cond.evaluate(&ConditionContext::with_features(&linux, &enabled)));
1157        assert!(!cond.evaluate(&ConditionContext::with_features(&macos, &enabled))); // wrong arch
1158        assert!(!cond.evaluate(&ConditionContext::platform_only(&linux))); // feature off
1159    }
1160
1161    #[test]
1162    fn references_feature_is_false_for_platform_only_conditions() {
1163        for raw in [
1164            r#"cfg(os = "linux")"#,
1165            r#"cfg(all(os = "linux", arch = "x86_64"))"#,
1166            r#"cfg(not(os = "windows"))"#,
1167        ] {
1168            assert!(!Condition::parse_cfg(raw).unwrap().references_feature());
1169        }
1170    }
1171
1172    #[test]
1173    fn display_round_trips_through_parse_inner() {
1174        for raw in [
1175            r#"os = "linux""#,
1176            r#"feature = "simd""#,
1177            r#"all(feature = "simd", arch = "x86_64")"#,
1178            r#"all(os = "linux", arch = "x86_64")"#,
1179            r#"any(os = "macos", os = "linux")"#,
1180            r#"not(os = "windows")"#,
1181            r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
1182            r#"cxx = "clang""#,
1183            r#"cc = "gcc""#,
1184            r#"cxx_version = ">=18""#,
1185            r#"cc_version = ">=12, <15""#,
1186            r#"all(cxx = "clang", cxx_version = ">=18")"#,
1187            r#"not(cxx = "msvc")"#,
1188            r#"all(cxx = "apple-clang", os = "macos")"#,
1189        ] {
1190            let cond = Condition::parse_inner(raw).unwrap();
1191            let rendered = cond.to_string();
1192            assert_eq!(rendered, raw, "round-trip should be byte-identical");
1193            let again = Condition::parse_inner(&rendered).unwrap();
1194            assert_eq!(cond, again);
1195        }
1196    }
1197
1198    #[test]
1199    fn current_target_platform_is_internally_consistent() {
1200        let p = TargetPlatform::current();
1201        // Each field is non-empty and lowercase ASCII.
1202        for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
1203            assert!(!v.is_empty());
1204            assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
1205        }
1206    }
1207
1208    #[test]
1209    fn deterministic_serialization_for_metadata_round_trip() {
1210        let cond = Condition::parse_cfg(
1211            r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
1212        )
1213        .unwrap();
1214        let json = serde_json::to_string(&cond).unwrap();
1215        let parsed: Condition = serde_json::from_str(&json).unwrap();
1216        assert_eq!(cond, parsed);
1217    }
1218}