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                                        environment: "rust-ctfe".into(),
476                                    });
477                                }
478                            }
479                            runtime_events.push(RuntimeEvent {
480                                event_type: "decision".into(),
481                                id: decision.id.clone(),
482                                vector: Some(McdcVector {
483                                    values: active.values,
484                                    outcome,
485                                }),
486                                timestamp_ms,
487                                phase_id: None,
488                                environment: "rust-ctfe".into(),
489                            });
490                        }
491                        _ => unreachable!("validated decision event"),
492                    }
493                }
494            }
495            "exit" => {
496                let Some(invocation) = stack.pop() else {
497                    return Err(RustCompilerCtfeError::Invalid(format!(
498                        "CTFE marker {marker} closed an absent invocation on {}",
499                        event.thread
500                    )));
501                };
502                if invocation.definition != event.definition || !invocation.decisions.is_empty() {
503                    return Err(RustCompilerCtfeError::Invalid(format!(
504                        "CTFE marker {marker} closed the wrong or incomplete invocation on {}",
505                        event.thread
506                    )));
507                }
508            }
509            _ => unreachable!("validated observation kind"),
510        }
511        for ordinal in &mapping.hit_ordinals {
512            let ordinal = parse_u64(ordinal, "CTFE hit ordinal")?;
513            for id in &normalized.hit_obligations_by_ordinal[&ordinal] {
514                if ignored_hits.contains(id) {
515                    continue;
516                }
517                hits.insert(id.clone());
518                runtime_events.push(RuntimeEvent {
519                    event_type: "hit".into(),
520                    id: id.clone(),
521                    vector: None,
522                    timestamp_ms,
523                    phase_id: None,
524                    environment: "rust-ctfe".into(),
525                });
526            }
527        }
528    }
529    if let Some((thread, stack)) = stacks.iter().find(|(_, stack)| !stack.is_empty()) {
530        return Err(RustCompilerCtfeError::Invalid(format!(
531            "successful compiler unit {identity} left {} CTFE frame(s) open on {thread}",
532            stack.len()
533        )));
534    }
535    Ok(RustCompilerCtfeUnit {
536        identity,
537        crate_name: bundle.crate_name,
538        snapshot: RuntimeSnapshot {
539            decisions: decision_vectors
540                .into_iter()
541                .map(|(id, vectors)| DecisionSnapshot {
542                    meta: decisions[id.as_str()].clone(),
543                    vectors: vectors
544                        .into_iter()
545                        .map(|(values, outcome)| McdcVector { values, outcome })
546                        .collect(),
547                })
548                .collect(),
549            hits: hits.into_iter().collect(),
550            events: runtime_events,
551        },
552        observations: events.len(),
553    })
554}
555
556pub fn read_rust_compiler_ctfe(
557    directory: &Path,
558    normalized: &NormalizedRustCompilerManifest,
559    timestamp_ms: i64,
560) -> Result<Vec<RustCompilerCtfeUnit>, RustCompilerCtfeError> {
561    ctfe_files(directory)?
562        .into_iter()
563        .map(|(identity, bundle)| reconstruct_unit(identity, &bundle, normalized, timestamp_ms))
564        .collect()
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use std::{
571        sync::atomic::{AtomicU64, Ordering},
572        time::{SystemTime, UNIX_EPOCH},
573    };
574
575    use serde_json::{Value, json};
576
577    use crate::{
578        coverage_analysis::PointKind,
579        coverage_report::{
580            BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
581        },
582    };
583
584    static SCRATCH_NONCE: AtomicU64 = AtomicU64::new(0);
585
586    struct Scratch(PathBuf);
587
588    impl Scratch {
589        fn new() -> Self {
590            let epoch = SystemTime::now()
591                .duration_since(UNIX_EPOCH)
592                .expect("clock before epoch")
593                .as_nanos();
594            let path = std::env::temp_dir().join(format!(
595                "supercov-rust-ctfe-{}-{epoch}-{}",
596                std::process::id(),
597                SCRATCH_NONCE.fetch_add(1, Ordering::Relaxed)
598            ));
599            fs::create_dir_all(&path).expect("create CTFE scratch directory");
600            Self(path)
601        }
602
603        fn write(&self, map: Value, events: &[Value]) {
604            let mut bundle = map;
605            bundle["events"] = Value::Array(events.to_vec());
606            fs::write(
607                self.0.join("ctfe-unit-unit.json"),
608                serde_json::to_vec(&bundle).expect("serialize CTFE bundle"),
609            )
610            .expect("write CTFE bundle");
611        }
612
613        fn write_raw(&self, name: &str, bytes: &[u8]) {
614            fs::write(self.0.join(name), bytes).expect("write raw CTFE artifact");
615        }
616    }
617
618    impl Drop for Scratch {
619        fn drop(&mut self) {
620            let _ = fs::remove_dir_all(&self.0);
621        }
622    }
623
624    fn normalized_manifest() -> NormalizedRustCompilerManifest {
625        let decision = DecisionMeta {
626            id: "decision".into(),
627            file: "src/lib.rs".into(),
628            line: 1,
629            column: 1,
630            source: "value".into(),
631            conditions: vec!["value".into()],
632            kind: "control".into(),
633        };
634        let branch = BranchMeta {
635            id: "outcome".into(),
636            kind: "decision-outcome".into(),
637            file: "src/lib.rs".into(),
638            line: 1,
639            column: 1,
640            source: "value".into(),
641            alternatives: vec![
642                BranchAlternativeMeta {
643                    id: "false-alternative".into(),
644                    label: "condition false".into(),
645                },
646                BranchAlternativeMeta {
647                    id: "true-alternative".into(),
648                    label: "condition true".into(),
649                },
650            ],
651        };
652        NormalizedRustCompilerManifest {
653            manifest: CoverageManifest {
654                unmeasured: Vec::new(),
655                decisions: vec![decision],
656                points: vec![PointMeta {
657                    id: "function".into(),
658                    kind: PointKind::Function,
659                    file: "src/lib.rs".into(),
660                    line: 1,
661                    column: 1,
662                    source: "const fn evaluated(value: bool) -> bool".into(),
663                    label: None,
664                }],
665                branches: vec![branch],
666                limitations: Vec::new(),
667                scope: None,
668            },
669            hit_obligations_by_ordinal: BTreeMap::from([
670                (101, vec!["function".into()]),
671                (201, vec!["false-alternative".into()]),
672                (202, vec!["true-alternative".into()]),
673            ]),
674            internal_ordinals: BTreeSet::new(),
675            decision_outcome_obligations: BTreeMap::from([(
676                "decision".into(),
677                ("false-alternative".into(), "true-alternative".into()),
678            )]),
679            decision_loop_obligations: BTreeMap::new(),
680            decision_logical_selection_obligations: BTreeMap::new(),
681        }
682    }
683
684    fn mapping(
685        marker: &str,
686        observation_kind: &str,
687        hit_ordinals: &[&str],
688        decision: Option<Value>,
689    ) -> Value {
690        json!({
691            "marker": marker,
692            "definition": "fixture::evaluated",
693            "observationKind": observation_kind,
694            "ordinal": 0,
695            "hitOrdinals": hit_ordinals,
696            "decision": decision,
697        })
698    }
699
700    fn event(marker: &str, observation_kind: &str) -> Value {
701        json!({
702            "crate": "fixture",
703            "kind": "ctfe-marker",
704            "marker": marker,
705            "definition": "fixture::evaluated",
706            "observationKind": observation_kind,
707            "ordinal": 0,
708            "thread": "compiler-thread-1",
709        })
710    }
711
712    fn decision_event(
713        id: &str,
714        event: &str,
715        condition_index: Option<u64>,
716        value: Option<bool>,
717        outcome: Option<bool>,
718    ) -> Value {
719        json!({
720            "id": id,
721            "event": event,
722            "conditionIndex": condition_index,
723            "value": value,
724            "outcome": outcome,
725        })
726    }
727
728    fn valid_map() -> Value {
729        json!({
730            "schema": BUNDLE_SCHEMA,
731            "crate": "fixture",
732            "mappings": [
733                mapping("1", "entry", &["101"], None),
734                mapping("2", "decision-start", &[], Some(decision_event(
735                    "decision", "start", None, None, None,
736                ))),
737                mapping("3", "decision-condition", &[], Some(decision_event(
738                    "decision", "condition", Some(0), Some(false), None,
739                ))),
740                mapping("4", "decision-finish", &["201"], Some(decision_event(
741                    "decision", "finish", None, None, Some(false),
742                ))),
743                mapping("5", "exit", &[], None),
744                mapping("6", "decision-condition", &[], Some(decision_event(
745                    "decision", "condition", Some(0), Some(true), None,
746                ))),
747                mapping("7", "decision-finish", &["202"], Some(decision_event(
748                    "decision", "finish", None, None, Some(true),
749                ))),
750            ],
751        })
752    }
753
754    fn valid_events() -> Vec<Value> {
755        [
756            ("1", "entry"),
757            ("2", "decision-start"),
758            ("3", "decision-condition"),
759            ("4", "decision-finish"),
760            ("5", "exit"),
761            ("1", "entry"),
762            ("2", "decision-start"),
763            ("6", "decision-condition"),
764            ("7", "decision-finish"),
765            ("5", "exit"),
766        ]
767        .into_iter()
768        .map(|(marker, kind)| event(marker, kind))
769        .collect()
770    }
771
772    fn loop_manifest() -> NormalizedRustCompilerManifest {
773        let mut normalized = normalized_manifest();
774        normalized.manifest.decisions[0].kind = "while".into();
775        normalized.manifest.branches.push(BranchMeta {
776            id: "loop-entry".into(),
777            kind: "loop-entry".into(),
778            file: "src/lib.rs".into(),
779            line: 1,
780            column: 1,
781            source: "while value".into(),
782            alternatives: vec![
783                BranchAlternativeMeta {
784                    id: "zero-iterations".into(),
785                    label: "zero iterations".into(),
786                },
787                BranchAlternativeMeta {
788                    id: "entered".into(),
789                    label: "entered".into(),
790                },
791            ],
792        });
793        normalized
794            .hit_obligations_by_ordinal
795            .insert(301, vec!["zero-iterations".into()]);
796        normalized
797            .hit_obligations_by_ordinal
798            .insert(302, vec!["entered".into()]);
799        normalized.decision_loop_obligations.insert(
800            "decision".into(),
801            ("zero-iterations".into(), "entered".into()),
802        );
803        normalized
804    }
805
806    fn loop_map() -> Value {
807        let mut map = valid_map();
808        map["mappings"][3]["hitOrdinals"] = json!(["201", "301"]);
809        map["mappings"][6]["hitOrdinals"] = json!(["202", "302"]);
810        map
811    }
812
813    #[test]
814    fn canonical_unsigned_decimal_rejects_aliases() {
815        assert_eq!(parse_u64("12", "marker").unwrap(), 12);
816        assert!(parse_u64("012", "marker").is_err());
817        assert!(parse_u64("-1", "marker").is_err());
818        assert!(parse_u64("", "marker").is_err());
819    }
820
821    #[test]
822    fn reconstructs_exact_independent_ctfe_vectors_and_outcome_hits() {
823        let scratch = Scratch::new();
824        scratch.write(valid_map(), &valid_events());
825
826        let units = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42).unwrap();
827        assert_eq!(units.len(), 1);
828        assert_eq!(units[0].observations, 10);
829        assert_eq!(
830            units[0].snapshot.hits,
831            ["false-alternative", "function", "true-alternative"]
832        );
833        assert_eq!(units[0].snapshot.decisions.len(), 1);
834        assert_eq!(
835            units[0].snapshot.decisions[0].vectors,
836            [
837                McdcVector {
838                    values: vec![Some(false)],
839                    outcome: false,
840                },
841                McdcVector {
842                    values: vec![Some(true)],
843                    outcome: true,
844                },
845            ]
846        );
847    }
848
849    #[test]
850    fn reconstructs_logical_selection_hits_from_ctfe_ternary_vectors() {
851        let scratch = Scratch::new();
852        let mut map = valid_map();
853        map["mappings"].as_array_mut().unwrap().push(mapping(
854            "8",
855            "decision-condition",
856            &[],
857            Some(decision_event(
858                "decision",
859                "condition",
860                Some(1),
861                Some(true),
862                None,
863            )),
864        ));
865        let events = [
866            ("1", "entry"),
867            ("2", "decision-start"),
868            ("3", "decision-condition"),
869            ("4", "decision-finish"),
870            ("2", "decision-start"),
871            ("6", "decision-condition"),
872            ("8", "decision-condition"),
873            ("7", "decision-finish"),
874            ("5", "exit"),
875        ]
876        .into_iter()
877        .map(|(marker, kind)| event(marker, kind))
878        .collect::<Vec<_>>();
879        scratch.write(map, &events);
880
881        let mut normalized = normalized_manifest();
882        normalized.manifest.decisions[0].conditions = vec!["left".into(), "right".into()];
883        normalized.manifest.branches.push(BranchMeta {
884            id: "logical".into(),
885            kind: "logical-selection".into(),
886            file: "src/lib.rs".into(),
887            line: 1,
888            column: 1,
889            source: "left && right".into(),
890            alternatives: vec![
891                BranchAlternativeMeta {
892                    id: "short".into(),
893                    label: "short-circuited".into(),
894                },
895                BranchAlternativeMeta {
896                    id: "evaluated".into(),
897                    label: "right operand evaluated".into(),
898                },
899            ],
900        });
901        normalized.decision_logical_selection_obligations.insert(
902            "decision".into(),
903            vec![
904                crate::rust_compiler_manifest::NormalizedRustLogicalSelection {
905                    short_circuited_id: "short".into(),
906                    right_evaluated_id: "evaluated".into(),
907                    right_condition_index: 1,
908                },
909            ],
910        );
911
912        let units = read_rust_compiler_ctfe(&scratch.0, &normalized, 42).unwrap();
913        assert_eq!(
914            units[0].snapshot.hits,
915            [
916                "evaluated",
917                "false-alternative",
918                "function",
919                "short",
920                "true-alternative"
921            ]
922        );
923        assert_eq!(
924            units[0].snapshot.decisions[0].vectors,
925            [
926                McdcVector {
927                    values: vec![Some(false), None],
928                    outcome: false,
929                },
930                McdcVector {
931                    values: vec![Some(true), Some(true)],
932                    outcome: true,
933                },
934            ]
935        );
936    }
937
938    #[test]
939    fn commits_only_the_first_loop_entry_outcome_per_ctfe_invocation() {
940        let scratch = Scratch::new();
941        let events = [
942            ("1", "entry"),
943            ("2", "decision-start"),
944            ("6", "decision-condition"),
945            ("7", "decision-finish"),
946            ("2", "decision-start"),
947            ("3", "decision-condition"),
948            ("4", "decision-finish"),
949            ("5", "exit"),
950        ]
951        .into_iter()
952        .map(|(marker, kind)| event(marker, kind))
953        .collect::<Vec<_>>();
954        scratch.write(loop_map(), &events);
955
956        let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
957        assert_eq!(
958            units[0].snapshot.hits,
959            [
960                "entered",
961                "false-alternative",
962                "function",
963                "true-alternative"
964            ]
965        );
966        assert_eq!(
967            units[0].snapshot.decisions[0].vectors,
968            [
969                McdcVector {
970                    values: vec![Some(false)],
971                    outcome: false,
972                },
973                McdcVector {
974                    values: vec![Some(true)],
975                    outcome: true,
976                },
977            ]
978        );
979    }
980
981    #[test]
982    fn preserves_a_zero_iteration_loop_outcome() {
983        let scratch = Scratch::new();
984        let events = [
985            ("1", "entry"),
986            ("2", "decision-start"),
987            ("3", "decision-condition"),
988            ("4", "decision-finish"),
989            ("5", "exit"),
990        ]
991        .into_iter()
992        .map(|(marker, kind)| event(marker, kind))
993        .collect::<Vec<_>>();
994        scratch.write(loop_map(), &events);
995
996        let units = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42).unwrap();
997        assert_eq!(
998            units[0].snapshot.hits,
999            ["false-alternative", "function", "zero-iterations"]
1000        );
1001    }
1002
1003    #[test]
1004    fn rejects_semantic_marker_with_unrelated_hit() {
1005        let scratch = Scratch::new();
1006        let mut map = valid_map();
1007        map["mappings"][1]["hitOrdinals"] = json!(["201"]);
1008        scratch.write(map, &valid_events());
1009
1010        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1011            .expect_err("semantic start marker must not carry a coverage hit");
1012        assert!(error.to_string().contains("wrong coverage obligations"));
1013    }
1014
1015    #[test]
1016    fn rejects_finish_mapped_to_the_wrong_outcome_alternative() {
1017        let scratch = Scratch::new();
1018        let mut map = valid_map();
1019        map["mappings"][3]["hitOrdinals"] = json!(["202"]);
1020        scratch.write(map, &valid_events());
1021
1022        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1023            .expect_err("false finish must not map to the true alternative");
1024        assert!(error.to_string().contains("wrong coverage obligations"));
1025    }
1026
1027    #[test]
1028    fn rejects_finish_mapped_to_the_wrong_loop_alternative() {
1029        let scratch = Scratch::new();
1030        let mut map = loop_map();
1031        map["mappings"][3]["hitOrdinals"] = json!(["201", "302"]);
1032        scratch.write(map, &valid_events());
1033
1034        let error = read_rust_compiler_ctfe(&scratch.0, &loop_manifest(), 42)
1035            .expect_err("zero-iteration finish must not map to entered");
1036        assert!(error.to_string().contains("wrong coverage obligations"));
1037    }
1038
1039    #[test]
1040    fn rejects_condition_without_an_active_decision() {
1041        let scratch = Scratch::new();
1042        let mut events = valid_events();
1043        events.remove(1);
1044        scratch.write(valid_map(), &events);
1045
1046        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1047            .expect_err("condition without start must fail closed");
1048        assert!(error.to_string().contains("has no active frame"));
1049    }
1050
1051    #[test]
1052    fn rejects_exit_with_an_incomplete_decision() {
1053        let scratch = Scratch::new();
1054        let events = valid_events()
1055            .into_iter()
1056            .enumerate()
1057            .filter_map(|(index, event)| (index != 3).then_some(event))
1058            .collect::<Vec<_>>();
1059        scratch.write(valid_map(), &events);
1060
1061        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1062            .expect_err("invocation exit with an open decision must fail closed");
1063        assert!(error.to_string().contains("wrong or incomplete invocation"));
1064    }
1065
1066    #[test]
1067    fn rejects_legacy_partial_nonregular_and_truncated_units() {
1068        for (name, bytes, expected) in [
1069            ("ctfe-map-unit.json", b"{}".as_slice(), "unrecognized"),
1070            (".ctfe-unit-unit.partial", b"{}".as_slice(), "unrecognized"),
1071            ("ctfe-unit-unit.json", b"{", "EOF"),
1072        ] {
1073            let scratch = Scratch::new();
1074            scratch.write_raw(name, bytes);
1075            let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1076                .expect_err("recognized invalid CTFE artifact must fail closed");
1077            assert!(
1078                error.to_string().contains(expected),
1079                "unexpected {name} error: {error}"
1080            );
1081        }
1082
1083        let scratch = Scratch::new();
1084        fs::create_dir(scratch.0.join("ctfe-unit-unit.json"))
1085            .expect("create nonregular CTFE artifact");
1086        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1087            .expect_err("nonregular CTFE artifact must fail closed");
1088        assert!(error.to_string().contains("not a regular file"));
1089    }
1090
1091    #[test]
1092    fn rejects_unknown_bundle_and_event_fields() {
1093        let scratch = Scratch::new();
1094        let mut map = valid_map();
1095        map["unknown"] = json!(true);
1096        scratch.write(map, &valid_events());
1097        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1098            .expect_err("unknown bundle field must fail closed");
1099        assert!(error.to_string().contains("unknown field"));
1100
1101        let scratch = Scratch::new();
1102        let mut events = valid_events();
1103        events[0]["unknown"] = json!(true);
1104        scratch.write(valid_map(), &events);
1105        let error = read_rust_compiler_ctfe(&scratch.0, &normalized_manifest(), 42)
1106            .expect_err("unknown event field must fail closed");
1107        assert!(error.to_string().contains("unknown field"));
1108    }
1109}