Skip to main content

supercov_engine/
rust_compiler_ctfe.rs

1//! Strict reconstruction of compiler-owned constant-evaluation evidence.
2//!
3//! rustc's interpreter step events expose no stable parent span. The exact
4//! companion therefore inserts entry/exit markers and records the compiler
5//! thread. This module reconstructs a nested invocation stack per compiler
6//! process and thread before any observation is allowed to cover an
7//! obligation.
8
9use std::{
10    collections::{BTreeMap, BTreeSet},
11    fs,
12    path::{Path, PathBuf},
13};
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    coverage_analysis::McdcVector,
19    coverage_report::{DecisionSnapshot, RuntimeEvent, RuntimeSnapshot},
20    rust_compiler_manifest::NormalizedRustCompilerManifest,
21};
22
23const BUNDLE_SCHEMA: &str = "supercov-rust-ctfe-unit-v1";
24
25#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27struct CtfeBundleFile {
28    schema: String,
29    #[serde(rename = "crate")]
30    crate_name: String,
31    mappings: Vec<CtfeMapping>,
32    events: Vec<CtfeEvent>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37struct CtfeMapping {
38    marker: String,
39    definition: String,
40    observation_kind: String,
41    ordinal: u32,
42    hit_ordinals: Vec<String>,
43    decision: Option<CtfeDecisionMapping>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48struct CtfeDecisionMapping {
49    id: String,
50    event: String,
51    condition_index: Option<u64>,
52    value: Option<bool>,
53    outcome: Option<bool>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58struct CtfeEvent {
59    #[serde(rename = "crate")]
60    crate_name: String,
61    kind: String,
62    marker: String,
63    definition: String,
64    observation_kind: String,
65    ordinal: u32,
66    thread: String,
67}
68
69#[derive(Debug)]
70struct ActiveDecision {
71    id: String,
72    values: Vec<Option<bool>>,
73}
74
75#[derive(Debug)]
76struct ActiveInvocation {
77    definition: String,
78    decisions: Vec<ActiveDecision>,
79    committed_loops: BTreeSet<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct RustCompilerCtfeUnit {
85    pub identity: String,
86    pub crate_name: String,
87    pub snapshot: RuntimeSnapshot,
88    pub observations: usize,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum RustCompilerCtfeError {
93    Io { path: PathBuf, reason: String },
94    Invalid(String),
95}
96
97impl std::fmt::Display for RustCompilerCtfeError {
98    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        match self {
100            Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
101            Self::Invalid(reason) => write!(formatter, "invalid Rust CTFE evidence: {reason}"),
102        }
103    }
104}
105
106impl std::error::Error for RustCompilerCtfeError {}
107
108fn io_error(path: &Path, error: impl std::fmt::Display) -> RustCompilerCtfeError {
109    RustCompilerCtfeError::Io {
110        path: path.to_path_buf(),
111        reason: error.to_string(),
112    }
113}
114
115fn parse_u64(value: &str, context: &str) -> Result<u64, RustCompilerCtfeError> {
116    if value.is_empty() || (value.len() > 1 && value.starts_with('0')) {
117        return Err(RustCompilerCtfeError::Invalid(format!(
118            "{context} is not canonical unsigned decimal"
119        )));
120    }
121    value.parse::<u64>().map_err(|_| {
122        RustCompilerCtfeError::Invalid(format!("{context} is not canonical unsigned decimal"))
123    })
124}
125
126fn parse_json<T: for<'de> Deserialize<'de>>(
127    path: &Path,
128    bytes: &[u8],
129) -> Result<T, RustCompilerCtfeError> {
130    serde_json::from_slice(bytes)
131        .map_err(|error| RustCompilerCtfeError::Invalid(format!("{}: {error}", path.display())))
132}
133
134fn ctfe_files(directory: &Path) -> Result<BTreeMap<String, PathBuf>, RustCompilerCtfeError> {
135    let mut units = BTreeMap::new();
136    for entry in fs::read_dir(directory)
137        .map_err(|error| io_error(directory, error))?
138        .collect::<Result<Vec<_>, _>>()
139        .map_err(|error| io_error(directory, error))?
140    {
141        let path = entry.path();
142        let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
143            return Err(RustCompilerCtfeError::Invalid(
144                "compiler output contains a non-UTF-8 name".into(),
145            ));
146        };
147        let file_type = entry.file_type().map_err(|error| io_error(&path, error))?;
148        if !file_type.is_file() {
149            if name.starts_with("ctfe-") || name.starts_with(".ctfe-") {
150                return Err(RustCompilerCtfeError::Invalid(format!(
151                    "CTFE compiler artifact is not a regular file: {name}"
152                )));
153            }
154            continue;
155        }
156        let identity = name
157            .strip_prefix("ctfe-unit-")
158            .and_then(|name| name.strip_suffix(".json"))
159            .filter(|identity| !identity.is_empty());
160        if let Some(identity) = identity {
161            if units.insert(identity.to_owned(), path).is_some() {
162                return Err(RustCompilerCtfeError::Invalid(format!(
163                    "duplicate CTFE compiler unit {identity}"
164                )));
165            }
166        } else if name.starts_with("ctfe-") || name.starts_with(".ctfe-") {
167            return Err(RustCompilerCtfeError::Invalid(format!(
168                "unrecognized or incomplete CTFE compiler artifact {name}"
169            )));
170        }
171    }
172    Ok(units)
173}
174
175fn reconstruct_unit(
176    identity: String,
177    bundle_path: &Path,
178    normalized: &NormalizedRustCompilerManifest,
179    timestamp_ms: i64,
180) -> Result<RustCompilerCtfeUnit, RustCompilerCtfeError> {
181    let bundle: CtfeBundleFile = parse_json(
182        bundle_path,
183        &fs::read(bundle_path).map_err(|error| io_error(bundle_path, error))?,
184    )?;
185    if bundle.schema != BUNDLE_SCHEMA || bundle.crate_name.trim().is_empty() {
186        return Err(RustCompilerCtfeError::Invalid(format!(
187            "{} has an unsupported schema or empty crate",
188            bundle_path.display()
189        )));
190    }
191    let decisions = normalized
192        .manifest
193        .decisions
194        .iter()
195        .map(|decision| (decision.id.as_str(), decision))
196        .collect::<BTreeMap<_, _>>();
197    let mut mappings = BTreeMap::<u64, CtfeMapping>::new();
198    for mapping in bundle.mappings {
199        let marker = parse_u64(&mapping.marker, "CTFE marker")?;
200        if marker == 0
201            || mapping.definition.trim().is_empty()
202            || !matches!(
203                mapping.observation_kind.as_str(),
204                "entry"
205                    | "block"
206                    | "edge"
207                    | "selection"
208                    | "exit"
209                    | "decision-start"
210                    | "decision-condition"
211                    | "decision-finish"
212            )
213        {
214            return Err(RustCompilerCtfeError::Invalid(format!(
215                "malformed mapping for marker {marker}"
216            )));
217        }
218        let mut previous = None;
219        for hit in &mapping.hit_ordinals {
220            let hit = parse_u64(hit, "CTFE hit ordinal")?;
221            if hit == 0 || previous.is_some_and(|previous| previous >= hit) {
222                return Err(RustCompilerCtfeError::Invalid(format!(
223                    "marker {marker} has non-canonical hit ordinals"
224                )));
225            }
226            if normalized.internal_ordinals.contains(&hit)
227                || !normalized.hit_obligations_by_ordinal.contains_key(&hit)
228            {
229                return Err(RustCompilerCtfeError::Invalid(format!(
230                    "marker {marker} references unknown/non-evidence ordinal {hit}"
231                )));
232            }
233            previous = Some(hit);
234        }
235        match &mapping.decision {
236            None if mapping.observation_kind.starts_with("decision-") => {
237                return Err(RustCompilerCtfeError::Invalid(format!(
238                    "semantic marker {marker} has no decision mapping"
239                )));
240            }
241            Some(_) if !mapping.observation_kind.starts_with("decision-") => {
242                return Err(RustCompilerCtfeError::Invalid(format!(
243                    "non-decision marker {marker} carries a decision mapping"
244                )));
245            }
246            Some(decision) => {
247                let meta = decisions.get(decision.id.as_str()).ok_or_else(|| {
248                    RustCompilerCtfeError::Invalid(format!(
249                        "marker {marker} references unknown decision {}",
250                        decision.id
251                    ))
252                })?;
253                let valid_shape = match decision.event.as_str() {
254                    "start" => {
255                        mapping.observation_kind == "decision-start"
256                            && decision.condition_index.is_none()
257                            && decision.value.is_none()
258                            && decision.outcome.is_none()
259                    }
260                    "condition" => {
261                        mapping.observation_kind == "decision-condition"
262                            && decision
263                                .condition_index
264                                .is_some_and(|index| index < meta.conditions.len() as u64)
265                            && decision.value.is_some()
266                            && decision.outcome.is_none()
267                    }
268                    "finish" => {
269                        mapping.observation_kind == "decision-finish"
270                            && decision.condition_index.is_none()
271                            && decision.value.is_none()
272                            && decision.outcome.is_some()
273                    }
274                    _ => false,
275                };
276                if !valid_shape {
277                    return Err(RustCompilerCtfeError::Invalid(format!(
278                        "marker {marker} has a malformed decision event"
279                    )));
280                }
281                let expected_hits = match decision.event.as_str() {
282                    "start" | "condition" => BTreeSet::new(),
283                    "finish" => {
284                        let outcome = decision.outcome.expect("validated decision outcome");
285                        let alternatives = normalized
286                            .decision_outcome_obligations
287                            .get(&decision.id)
288                            .expect("validated decision outcome mapping");
289                        let mut expected = BTreeSet::from([if outcome {
290                            alternatives.1.as_str()
291                        } else {
292                            alternatives.0.as_str()
293                        }]);
294                        if let Some(loop_alternatives) =
295                            normalized.decision_loop_obligations.get(&decision.id)
296                        {
297                            expected.insert(if outcome {
298                                loop_alternatives.1.as_str()
299                            } else {
300                                loop_alternatives.0.as_str()
301                            });
302                        }
303                        expected
304                    }
305                    _ => unreachable!("validated decision event"),
306                };
307                let mapped_hits = mapping
308                    .hit_ordinals
309                    .iter()
310                    .map(|ordinal| parse_u64(ordinal, "CTFE hit ordinal"))
311                    .collect::<Result<Vec<_>, _>>()?
312                    .into_iter()
313                    .flat_map(|ordinal| normalized.hit_obligations_by_ordinal[&ordinal].iter())
314                    .map(String::as_str)
315                    .collect::<BTreeSet<_>>();
316                if mapped_hits != expected_hits {
317                    return Err(RustCompilerCtfeError::Invalid(format!(
318                        "decision {} {} marker maps to the wrong coverage obligations",
319                        decision.id, decision.event
320                    )));
321                }
322            }
323            None => {}
324        }
325        if mappings.insert(marker, mapping).is_some() {
326            return Err(RustCompilerCtfeError::Invalid(format!(
327                "duplicate CTFE marker {marker}"
328            )));
329        }
330    }
331    if mappings.is_empty() {
332        return Err(RustCompilerCtfeError::Invalid(format!(
333            "{} contains no mappings",
334            bundle_path.display()
335        )));
336    }
337
338    let events = bundle.events;
339    let mut stacks = BTreeMap::<String, Vec<ActiveInvocation>>::new();
340    let mut hits = BTreeSet::new();
341    let mut decision_vectors = BTreeMap::<String, BTreeSet<(Vec<Option<bool>>, bool)>>::new();
342    let mut runtime_events = Vec::new();
343    for event in &events {
344        let mut ignored_hits = BTreeSet::new();
345        if event.kind != "ctfe-marker"
346            || event.crate_name != bundle.crate_name
347            || event.thread.trim().is_empty()
348        {
349            return Err(RustCompilerCtfeError::Invalid(format!(
350                "{} contains malformed event identity",
351                bundle_path.display()
352            )));
353        }
354        let marker = parse_u64(&event.marker, "observed CTFE marker")?;
355        let mapping = mappings.get(&marker).ok_or_else(|| {
356            RustCompilerCtfeError::Invalid(format!("observed CTFE marker {marker} is unmapped"))
357        })?;
358        if mapping.definition != event.definition
359            || mapping.observation_kind != event.observation_kind
360            || mapping.ordinal != event.ordinal
361        {
362            return Err(RustCompilerCtfeError::Invalid(format!(
363                "observed CTFE marker {marker} changed identity"
364            )));
365        }
366        let stack = stacks.entry(event.thread.clone()).or_default();
367        match event.observation_kind.as_str() {
368            "entry" => stack.push(ActiveInvocation {
369                definition: event.definition.clone(),
370                decisions: Vec::new(),
371                committed_loops: BTreeSet::new(),
372            }),
373            "block" | "edge" | "selection" | "decision-start" | "decision-condition"
374            | "decision-finish" => {
375                let Some(invocation) = stack.last_mut() else {
376                    return Err(RustCompilerCtfeError::Invalid(format!(
377                        "CTFE marker {marker} was observed outside an invocation on {}",
378                        event.thread
379                    )));
380                };
381                if invocation.definition != event.definition {
382                    return Err(RustCompilerCtfeError::Invalid(format!(
383                        "CTFE marker {marker} crossed invocation identity on {}",
384                        event.thread
385                    )));
386                }
387                if let Some(decision) = &mapping.decision {
388                    match decision.event.as_str() {
389                        "start" => {
390                            let meta = decisions[decision.id.as_str()];
391                            invocation.decisions.push(ActiveDecision {
392                                id: decision.id.clone(),
393                                values: vec![None; meta.conditions.len()],
394                            });
395                        }
396                        "condition" => {
397                            let active = invocation.decisions.last_mut().ok_or_else(|| {
398                                RustCompilerCtfeError::Invalid(format!(
399                                    "decision condition {} has no active frame",
400                                    decision.id
401                                ))
402                            })?;
403                            if active.id != decision.id {
404                                return Err(RustCompilerCtfeError::Invalid(format!(
405                                    "decision condition {} crossed active decision {}",
406                                    decision.id, active.id
407                                )));
408                            }
409                            let index = usize::try_from(
410                                decision.condition_index.expect("validated condition index"),
411                            )
412                            .map_err(|_| {
413                                RustCompilerCtfeError::Invalid(format!(
414                                    "decision {} condition index exceeds usize",
415                                    decision.id
416                                ))
417                            })?;
418                            if active.values[index]
419                                .replace(decision.value.expect("validated condition value"))
420                                .is_some()
421                            {
422                                return Err(RustCompilerCtfeError::Invalid(format!(
423                                    "decision {} condition {index} was observed twice",
424                                    decision.id
425                                )));
426                            }
427                        }
428                        "finish" => {
429                            let active = invocation.decisions.pop().ok_or_else(|| {
430                                RustCompilerCtfeError::Invalid(format!(
431                                    "decision finish {} has no active frame",
432                                    decision.id
433                                ))
434                            })?;
435                            if active.id != decision.id {
436                                return Err(RustCompilerCtfeError::Invalid(format!(
437                                    "decision finish {} closed active decision {}",
438                                    decision.id, active.id
439                                )));
440                            }
441                            let outcome = decision.outcome.expect("validated decision outcome");
442                            if let Some(loop_alternatives) =
443                                normalized.decision_loop_obligations.get(&decision.id)
444                                && !invocation.committed_loops.insert(decision.id.clone())
445                            {
446                                ignored_hits.insert(if outcome {
447                                    loop_alternatives.1.clone()
448                                } else {
449                                    loop_alternatives.0.clone()
450                                });
451                            }
452                            decision_vectors
453                                .entry(decision.id.clone())
454                                .or_default()
455                                .insert((active.values.clone(), outcome));
456                            if let Some(selections) = normalized
457                                .decision_logical_selection_obligations
458                                .get(&decision.id)
459                            {
460                                for selection in selections {
461                                    let alternative_id =
462                                        if active.values[selection.right_condition_index].is_some()
463                                        {
464                                            &selection.right_evaluated_id
465                                        } else {
466                                            &selection.short_circuited_id
467                                        };
468                                    hits.insert(alternative_id.clone());
469                                    runtime_events.push(RuntimeEvent {
470                                        event_type: "hit".into(),
471                                        id: alternative_id.clone(),
472                                        vector: None,
473                                        timestamp_ms,
474                                        phase_id: None,
475                                        statement_id: None,
476                                        environment: "rust-ctfe".into(),
477                                    });
478                                }
479                            }
480                            runtime_events.push(RuntimeEvent {
481                                event_type: "decision".into(),
482                                id: decision.id.clone(),
483                                vector: Some(McdcVector {
484                                    values: active.values,
485                                    outcome,
486                                }),
487                                timestamp_ms,
488                                phase_id: None,
489                                statement_id: None,
490                                environment: "rust-ctfe".into(),
491                            });
492                        }
493                        _ => unreachable!("validated decision event"),
494                    }
495                }
496            }
497            "exit" => {
498                let Some(invocation) = stack.pop() else {
499                    return Err(RustCompilerCtfeError::Invalid(format!(
500                        "CTFE marker {marker} closed an absent invocation on {}",
501                        event.thread
502                    )));
503                };
504                if invocation.definition != event.definition || !invocation.decisions.is_empty() {
505                    return Err(RustCompilerCtfeError::Invalid(format!(
506                        "CTFE marker {marker} closed the wrong or incomplete invocation on {}",
507                        event.thread
508                    )));
509                }
510            }
511            _ => unreachable!("validated observation kind"),
512        }
513        for ordinal in &mapping.hit_ordinals {
514            let ordinal = parse_u64(ordinal, "CTFE hit ordinal")?;
515            for id in &normalized.hit_obligations_by_ordinal[&ordinal] {
516                if ignored_hits.contains(id) {
517                    continue;
518                }
519                hits.insert(id.clone());
520                runtime_events.push(RuntimeEvent {
521                    event_type: "hit".into(),
522                    id: id.clone(),
523                    vector: None,
524                    timestamp_ms,
525                    phase_id: None,
526                    statement_id: None,
527                    environment: "rust-ctfe".into(),
528                });
529            }
530        }
531    }
532    if let Some((thread, stack)) = stacks.iter().find(|(_, stack)| !stack.is_empty()) {
533        return Err(RustCompilerCtfeError::Invalid(format!(
534            "successful compiler unit {identity} left {} CTFE frame(s) open on {thread}",
535            stack.len()
536        )));
537    }
538    Ok(RustCompilerCtfeUnit {
539        identity,
540        crate_name: bundle.crate_name,
541        snapshot: RuntimeSnapshot {
542            decisions: decision_vectors
543                .into_iter()
544                .map(|(id, vectors)| DecisionSnapshot {
545                    meta: decisions[id.as_str()].clone(),
546                    vectors: vectors
547                        .into_iter()
548                        .map(|(values, outcome)| McdcVector { values, outcome })
549                        .collect(),
550                })
551                .collect(),
552            hits: hits.into_iter().collect(),
553            events: runtime_events,
554            logicals: Vec::new(),
555        },
556        observations: events.len(),
557    })
558}
559
560pub fn read_rust_compiler_ctfe(
561    directory: &Path,
562    normalized: &NormalizedRustCompilerManifest,
563    timestamp_ms: i64,
564) -> Result<Vec<RustCompilerCtfeUnit>, RustCompilerCtfeError> {
565    ctfe_files(directory)?
566        .into_iter()
567        .map(|(identity, bundle)| reconstruct_unit(identity, &bundle, normalized, timestamp_ms))
568        .collect()
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use std::{
575        sync::atomic::{AtomicU64, Ordering},
576        time::{SystemTime, UNIX_EPOCH},
577    };
578
579    use serde_json::{Value, json};
580
581    use crate::{
582        coverage_analysis::PointKind,
583        coverage_report::{
584            BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
585        },
586    };
587
588    static SCRATCH_NONCE: AtomicU64 = AtomicU64::new(0);
589
590    struct Scratch(PathBuf);
591
592    impl Scratch {
593        fn new() -> Self {
594            let epoch = SystemTime::now()
595                .duration_since(UNIX_EPOCH)
596                .expect("clock before epoch")
597                .as_nanos();
598            let path = std::env::temp_dir().join(format!(
599                "supercov-rust-ctfe-{}-{epoch}-{}",
600                std::process::id(),
601                SCRATCH_NONCE.fetch_add(1, Ordering::Relaxed)
602            ));
603            fs::create_dir_all(&path).expect("create CTFE scratch directory");
604            Self(path)
605        }
606
607        fn write(&self, map: Value, events: &[Value]) {
608            let mut bundle = map;
609            bundle["events"] = Value::Array(events.to_vec());
610            fs::write(
611                self.0.join("ctfe-unit-unit.json"),
612                serde_json::to_vec(&bundle).expect("serialize CTFE bundle"),
613            )
614            .expect("write CTFE bundle");
615        }
616
617        fn write_raw(&self, name: &str, bytes: &[u8]) {
618            fs::write(self.0.join(name), bytes).expect("write raw CTFE artifact");
619        }
620    }
621
622    impl Drop for Scratch {
623        fn drop(&mut self) {
624            let _ = fs::remove_dir_all(&self.0);
625        }
626    }
627
628    fn normalized_manifest() -> NormalizedRustCompilerManifest {
629        let decision = DecisionMeta {
630            id: "decision".into(),
631            file: "src/lib.rs".into(),
632            line: 1,
633            column: 1,
634            source: "value".into(),
635            conditions: vec!["value".into()],
636            kind: "control".into(),
637        };
638        let branch = BranchMeta {
639            id: "outcome".into(),
640            kind: "decision-outcome".into(),
641            file: "src/lib.rs".into(),
642            line: 1,
643            column: 1,
644            source: "value".into(),
645            alternatives: vec![
646                BranchAlternativeMeta {
647                    id: "false-alternative".into(),
648                    label: "condition false".into(),
649                },
650                BranchAlternativeMeta {
651                    id: "true-alternative".into(),
652                    label: "condition true".into(),
653                },
654            ],
655        };
656        NormalizedRustCompilerManifest {
657            manifest: CoverageManifest {
658                unmeasured: Vec::new(),
659                decisions: vec![decision],
660                points: vec![PointMeta {
661                    id: "function".into(),
662                    kind: PointKind::Function,
663                    file: "src/lib.rs".into(),
664                    line: 1,
665                    column: 1,
666                    source: "const fn evaluated(value: bool) -> bool".into(),
667                    label: None,
668                }],
669                branches: vec![branch],
670                limitations: Vec::new(),
671                scope: None,
672            },
673            hit_obligations_by_ordinal: BTreeMap::from([
674                (101, vec!["function".into()]),
675                (201, vec!["false-alternative".into()]),
676                (202, vec!["true-alternative".into()]),
677            ]),
678            internal_ordinals: BTreeSet::new(),
679            decision_outcome_obligations: BTreeMap::from([(
680                "decision".into(),
681                ("false-alternative".into(), "true-alternative".into()),
682            )]),
683            decision_loop_obligations: BTreeMap::new(),
684            decision_logical_selection_obligations: BTreeMap::new(),
685        }
686    }
687
688    fn mapping(
689        marker: &str,
690        observation_kind: &str,
691        hit_ordinals: &[&str],
692        decision: Option<Value>,
693    ) -> Value {
694        json!({
695            "marker": marker,
696            "definition": "fixture::evaluated",
697            "observationKind": observation_kind,
698            "ordinal": 0,
699            "hitOrdinals": hit_ordinals,
700            "decision": decision,
701        })
702    }
703
704    fn event(marker: &str, observation_kind: &str) -> Value {
705        json!({
706            "crate": "fixture",
707            "kind": "ctfe-marker",
708            "marker": marker,
709            "definition": "fixture::evaluated",
710            "observationKind": observation_kind,
711            "ordinal": 0,
712            "thread": "compiler-thread-1",
713        })
714    }
715
716    fn decision_event(
717        id: &str,
718        event: &str,
719        condition_index: Option<u64>,
720        value: Option<bool>,
721        outcome: Option<bool>,
722    ) -> Value {
723        json!({
724            "id": id,
725            "event": event,
726            "conditionIndex": condition_index,
727            "value": value,
728            "outcome": outcome,
729        })
730    }
731
732    fn valid_map() -> Value {
733        json!({
734            "schema": BUNDLE_SCHEMA,
735            "crate": "fixture",
736            "mappings": [
737                mapping("1", "entry", &["101"], None),
738                mapping("2", "decision-start", &[], Some(decision_event(
739                    "decision", "start", None, None, None,
740                ))),
741                mapping("3", "decision-condition", &[], Some(decision_event(
742                    "decision", "condition", Some(0), Some(false), None,
743                ))),
744                mapping("4", "decision-finish", &["201"], Some(decision_event(
745                    "decision", "finish", None, None, Some(false),
746                ))),
747                mapping("5", "exit", &[], None),
748                mapping("6", "decision-condition", &[], Some(decision_event(
749                    "decision", "condition", Some(0), Some(true), None,
750                ))),
751                mapping("7", "decision-finish", &["202"], Some(decision_event(
752                    "decision", "finish", None, None, Some(true),
753                ))),
754            ],
755        })
756    }
757
758    fn valid_events() -> Vec<Value> {
759        [
760            ("1", "entry"),
761            ("2", "decision-start"),
762            ("3", "decision-condition"),
763            ("4", "decision-finish"),
764            ("5", "exit"),
765            ("1", "entry"),
766            ("2", "decision-start"),
767            ("6", "decision-condition"),
768            ("7", "decision-finish"),
769            ("5", "exit"),
770        ]
771        .into_iter()
772        .map(|(marker, kind)| event(marker, kind))
773        .collect()
774    }
775
776    fn loop_manifest() -> NormalizedRustCompilerManifest {
777        let mut normalized = normalized_manifest();
778        normalized.manifest.decisions[0].kind = "while".into();
779        normalized.manifest.branches.push(BranchMeta {
780            id: "loop-entry".into(),
781            kind: "loop-entry".into(),
782            file: "src/lib.rs".into(),
783            line: 1,
784            column: 1,
785            source: "while value".into(),
786            alternatives: vec![
787                BranchAlternativeMeta {
788                    id: "zero-iterations".into(),
789                    label: "zero iterations".into(),
790                },
791                BranchAlternativeMeta {
792                    id: "entered".into(),
793                    label: "entered".into(),
794                },
795            ],
796        });
797        normalized
798            .hit_obligations_by_ordinal
799            .insert(301, vec!["zero-iterations".into()]);
800        normalized
801            .hit_obligations_by_ordinal
802            .insert(302, vec!["entered".into()]);
803        normalized.decision_loop_obligations.insert(
804            "decision".into(),
805            ("zero-iterations".into(), "entered".into()),
806        );
807        normalized
808    }
809
810    fn loop_map() -> Value {
811        let mut map = valid_map();
812        map["mappings"][3]["hitOrdinals"] = json!(["201", "301"]);
813        map["mappings"][6]["hitOrdinals"] = json!(["202", "302"]);
814        map
815    }
816
817    #[test]
818    fn canonical_unsigned_decimal_rejects_aliases() {
819        assert_eq!(parse_u64("12", "marker").unwrap(), 12);
820        assert!(parse_u64("012", "marker").is_err());
821        assert!(parse_u64("-1", "marker").is_err());
822        assert!(parse_u64("", "marker").is_err());
823    }
824
825    #[test]
826    fn reconstructs_exact_independent_ctfe_vectors_and_outcome_hits() {
827        let scratch = Scratch::new();
828        scratch.write(valid_map(), &valid_events());
829
830        let units = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42).unwrap();
831        assert_eq!(units.len(), 1);
832        assert_eq!(units[0].observations, 10);
833        assert_eq!(
834            units[0].snapshot.hits,
835            ["false-alternative", "function", "true-alternative"]
836        );
837        assert_eq!(units[0].snapshot.decisions.len(), 1);
838        assert_eq!(
839            units[0].snapshot.decisions[0].vectors,
840            [
841                McdcVector {
842                    values: vec![Some(false)],
843                    outcome: false,
844                },
845                McdcVector {
846                    values: vec![Some(true)],
847                    outcome: true,
848                },
849            ]
850        );
851    }
852
853    #[test]
854    fn reconstructs_logical_selection_hits_from_ctfe_ternary_vectors() {
855        let scratch = Scratch::new();
856        let mut map = valid_map();
857        map["mappings"].as_array_mut().unwrap().push(mapping(
858            "8",
859            "decision-condition",
860            &[],
861            Some(decision_event(
862                "decision",
863                "condition",
864                Some(1),
865                Some(true),
866                None,
867            )),
868        ));
869        let events = [
870            ("1", "entry"),
871            ("2", "decision-start"),
872            ("3", "decision-condition"),
873            ("4", "decision-finish"),
874            ("2", "decision-start"),
875            ("6", "decision-condition"),
876            ("8", "decision-condition"),
877            ("7", "decision-finish"),
878            ("5", "exit"),
879        ]
880        .into_iter()
881        .map(|(marker, kind)| event(marker, kind))
882        .collect::<Vec<_>>();
883        scratch.write(map, &events);
884
885        let mut normalized = normalized_manifest();
886        normalized.manifest.decisions[0].conditions = vec!["left".into(), "right".into()];
887        normalized.manifest.branches.push(BranchMeta {
888            id: "logical".into(),
889            kind: "logical-selection".into(),
890            file: "src/lib.rs".into(),
891            line: 1,
892            column: 1,
893            source: "left && right".into(),
894            alternatives: vec![
895                BranchAlternativeMeta {
896                    id: "short".into(),
897                    label: "short-circuited".into(),
898                },
899                BranchAlternativeMeta {
900                    id: "evaluated".into(),
901                    label: "right operand evaluated".into(),
902                },
903            ],
904        });
905        normalized.decision_logical_selection_obligations.insert(
906            "decision".into(),
907            vec![
908                crate::rust_compiler_manifest::NormalizedRustLogicalSelection {
909                    short_circuited_id: "short".into(),
910                    right_evaluated_id: "evaluated".into(),
911                    right_condition_index: 1,
912                },
913            ],
914        );
915
916        let units = read_rust_compiler_ctfe(&scratch.0, &normalized, 42).unwrap();
917        assert_eq!(
918            units[0].snapshot.hits,
919            [
920                "evaluated",
921                "false-alternative",
922                "function",
923                "short",
924                "true-alternative"
925            ]
926        );
927        assert_eq!(
928            units[0].snapshot.decisions[0].vectors,
929            [
930                McdcVector {
931                    values: vec![Some(false), None],
932                    outcome: false,
933                },
934                McdcVector {
935                    values: vec![Some(true), Some(true)],
936                    outcome: true,
937                },
938            ]
939        );
940    }
941
942    #[test]
943    fn commits_only_the_first_loop_entry_outcome_per_ctfe_invocation() {
944        let scratch = Scratch::new();
945        let events = [
946            ("1", "entry"),
947            ("2", "decision-start"),
948            ("6", "decision-condition"),
949            ("7", "decision-finish"),
950            ("2", "decision-start"),
951            ("3", "decision-condition"),
952            ("4", "decision-finish"),
953            ("5", "exit"),
954        ]
955        .into_iter()
956        .map(|(marker, kind)| event(marker, kind))
957        .collect::<Vec<_>>();
958        scratch.write(loop_map(), &events);
959
960        let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
961        assert_eq!(
962            units[0].snapshot.hits,
963            [
964                "entered",
965                "false-alternative",
966                "function",
967                "true-alternative"
968            ]
969        );
970        assert_eq!(
971            units[0].snapshot.decisions[0].vectors,
972            [
973                McdcVector {
974                    values: vec![Some(false)],
975                    outcome: false,
976                },
977                McdcVector {
978                    values: vec![Some(true)],
979                    outcome: true,
980                },
981            ]
982        );
983    }
984
985    #[test]
986    fn preserves_a_zero_iteration_loop_outcome() {
987        let scratch = Scratch::new();
988        let events = [
989            ("1", "entry"),
990            ("2", "decision-start"),
991            ("3", "decision-condition"),
992            ("4", "decision-finish"),
993            ("5", "exit"),
994        ]
995        .into_iter()
996        .map(|(marker, kind)| event(marker, kind))
997        .collect::<Vec<_>>();
998        scratch.write(loop_map(), &events);
999
1000        let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
1001        assert_eq!(
1002            units[0].snapshot.hits,
1003            ["false-alternative", "function", "zero-iterations"]
1004        );
1005    }
1006
1007    #[test]
1008    fn rejects_semantic_marker_with_unrelated_hit() {
1009        let scratch = Scratch::new();
1010        let mut map = valid_map();
1011        map["mappings"][1]["hitOrdinals"] = json!(["201"]);
1012        scratch.write(map, &valid_events());
1013
1014        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1015            .expect_err("semantic start marker must not carry a coverage hit");
1016        assert!(error.to_string().contains("wrong coverage obligations"));
1017    }
1018
1019    #[test]
1020    fn rejects_finish_mapped_to_the_wrong_outcome_alternative() {
1021        let scratch = Scratch::new();
1022        let mut map = valid_map();
1023        map["mappings"][3]["hitOrdinals"] = json!(["202"]);
1024        scratch.write(map, &valid_events());
1025
1026        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1027            .expect_err("false finish must not map to the true alternative");
1028        assert!(error.to_string().contains("wrong coverage obligations"));
1029    }
1030
1031    #[test]
1032    fn rejects_finish_mapped_to_the_wrong_loop_alternative() {
1033        let scratch = Scratch::new();
1034        let mut map = loop_map();
1035        map["mappings"][3]["hitOrdinals"] = json!(["201", "302"]);
1036        scratch.write(map, &valid_events());
1037
1038        let error = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42)
1039            .expect_err("zero-iteration finish must not map to entered");
1040        assert!(error.to_string().contains("wrong coverage obligations"));
1041    }
1042
1043    #[test]
1044    fn rejects_condition_without_an_active_decision() {
1045        let scratch = Scratch::new();
1046        let mut events = valid_events();
1047        events.remove(1);
1048        scratch.write(valid_map(), &events);
1049
1050        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1051            .expect_err("condition without start must fail closed");
1052        assert!(error.to_string().contains("has no active frame"));
1053    }
1054
1055    #[test]
1056    fn rejects_exit_with_an_incomplete_decision() {
1057        let scratch = Scratch::new();
1058        let events = valid_events()
1059            .into_iter()
1060            .enumerate()
1061            .filter_map(|(index, event)| (index != 3).then_some(event))
1062            .collect::<Vec<_>>();
1063        scratch.write(valid_map(), &events);
1064
1065        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1066            .expect_err("invocation exit with an open decision must fail closed");
1067        assert!(error.to_string().contains("wrong or incomplete invocation"));
1068    }
1069
1070    #[test]
1071    fn rejects_legacy_partial_nonregular_and_truncated_units() {
1072        for (name, bytes, expected) in [
1073            ("ctfe-map-unit.json", b"{}".as_slice(), "unrecognized"),
1074            (".ctfe-unit-unit.partial", b"{}".as_slice(), "unrecognized"),
1075            ("ctfe-unit-unit.json", b"{", "EOF"),
1076        ] {
1077            let scratch = Scratch::new();
1078            scratch.write_raw(name, bytes);
1079            let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1080                .expect_err("recognized invalid CTFE artifact must fail closed");
1081            assert!(
1082                error.to_string().contains(expected),
1083                "unexpected {name} error: {error}"
1084            );
1085        }
1086
1087        let scratch = Scratch::new();
1088        fs::create_dir(scratch.0.join("ctfe-unit-unit.json"))
1089            .expect("create nonregular CTFE artifact");
1090        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1091            .expect_err("nonregular CTFE artifact must fail closed");
1092        assert!(error.to_string().contains("not a regular file"));
1093    }
1094
1095    #[test]
1096    fn rejects_unknown_bundle_and_event_fields() {
1097        let scratch = Scratch::new();
1098        let mut map = valid_map();
1099        map["unknown"] = json!(true);
1100        scratch.write(map, &valid_events());
1101        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1102            .expect_err("unknown bundle field must fail closed");
1103        assert!(error.to_string().contains("unknown field"));
1104
1105        let scratch = Scratch::new();
1106        let mut events = valid_events();
1107        events[0]["unknown"] = json!(true);
1108        scratch.write(valid_map(), &events);
1109        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1110            .expect_err("unknown event field must fail closed");
1111        assert!(error.to_string().contains("unknown field"));
1112    }
1113}