Skip to main content

detcore_model/
happens_before.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Happens-before edges: a sparse, authored partial order over dynamic events.
10//!
11//! Where `--replay-schedule-from` replays a *complete* total order captured from
12//! a prior run, a happens-before specification pins down only the *few* events
13//! that matter for a race and lets the deterministic scheduler fill in the rest.
14//! An agent (or human) that already knows a target race can therefore construct
15//! it deterministically instead of blind seed-search.
16//!
17//! # Model
18//!
19//! An [`Anchor`] names a precise, deterministic per-thread stop point. Following
20//! the owner's refinement of RFC #1146, the *primary* addressing is a
21//! [`Position`] — "after N syscalls" or "after M retired conditional branches
22//! (RCBs)" on a specific thread — optionally decorated with a [`CodeLocation`]
23//! (function and/or source line, resolved from debug info) for readability. The
24//! RFC's richer addressing modes (Nth occurrence of a named syscall, RIP hit, or
25//! function entry) remain expressible but are deliberately *not* the lead
26//! addressing scheme.
27//!
28//! A [`HappensBeforeEdge`] states that one anchor must be observed before another
29//! thread is allowed to proceed past its anchor. A [`HappensBeforeSpec`] is the
30//! whole authored partial order: a table of named threads, a table of named
31//! events (anchors), and the edge list connecting them.
32//!
33//! This module owns the *model* only: parsing (JSON and a terse DSL),
34//! normalization, and static validation (name resolution, exactly-one-position,
35//! and cycle detection). Resolving a [`CodeLocation`] to a concrete address via
36//! debug info, and enforcing edges in the scheduler, live in higher layers.
37
38use std::collections::BTreeMap;
39use std::collections::BTreeSet;
40use std::fmt;
41use std::str::FromStr;
42
43use reverie_syscalls::Sysno;
44use serde::Deserialize;
45use serde::Serialize;
46
47use crate::pid::DetTid;
48use crate::schedule::SyscallPhase;
49
50/// The schema version understood by this build.
51pub const HAPPENS_BEFORE_VERSION: u32 = 1;
52
53// ================================================================================
54// Declarative on-disk / on-wire format (serde)
55// ================================================================================
56
57/// The declarative happens-before specification, as read from a JSON file or
58/// desugared from the terse DSL. This mirrors the reviewed RFC #1146 file format
59/// verbatim so an authored file round-trips.
60#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
61pub struct HappensBeforeSpec {
62    /// Schema version. Must equal [`HAPPENS_BEFORE_VERSION`].
63    pub version: u32,
64
65    /// Symbolic thread labels mapped to a resolution rule, so authors need not
66    /// hard-code raw `DetTid`s.
67    #[serde(default)]
68    pub threads: BTreeMap<String, ThreadSpec>,
69
70    /// Named events (anchors). Naming events separately from edges lets one event
71    /// participate in several edges and keeps the edge list readable.
72    #[serde(default)]
73    pub events: BTreeMap<String, EventSpec>,
74
75    /// The partial order itself.
76    #[serde(default)]
77    pub edges: Vec<EdgeSpec>,
78}
79
80/// How a symbolic thread label resolves to a concrete `DetTid`.
81///
82/// Exactly one resolution rule should be provided; if `dettid` is present it wins.
83#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
84pub struct ThreadSpec {
85    /// A human-facing label (defaults to the map key).
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub label: Option<String>,
88
89    /// An explicit deterministic thread id, when the author knows it.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub dettid: Option<i32>,
92
93    /// Resolve to the thread created by the Nth `clone`/`fork`, 1-based. Thread
94    /// creation is deterministic under sequentialization, so this is stable.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub spawn_ordinal: Option<u32>,
97}
98
99/// A single named event (anchor) in the declarative format.
100///
101/// The addressing fields are flat and optional to match the RFC JSON; exactly one
102/// *position* selector must be set (see [`HappensBeforeSpec::normalize`]). The
103/// two owner-preferred primaries are [`syscalls`](Self::syscalls) ("after N
104/// syscalls") and [`rcbs`](Self::rcbs) ("after M RBCs").
105#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
106pub struct EventSpec {
107    /// The thread this event is on: a key into [`HappensBeforeSpec::threads`], or
108    /// a raw integer `DetTid`.
109    pub thread: String,
110
111    // ---- primary positions (owner's refinement) ----
112    /// After the thread has executed this many syscalls in total (any syscall).
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub syscalls: Option<u64>,
115
116    /// After the thread has retired this many conditional branches (its RCB
117    /// clock reaches this absolute value). Accepted as `rcbs` or `rcb`.
118    #[serde(default, alias = "rcb", skip_serializing_if = "Option::is_none")]
119    pub rcbs: Option<u64>,
120
121    // ---- code location (readability; resolves to a RIP via debug info) ----
122    /// A function name; with `line` this is "function+line", the preferred
123    /// human-legible location.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub func: Option<String>,
126
127    /// A source file name (optional companion to `line`).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub file: Option<String>,
130
131    /// A source line number.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub line: Option<u32>,
134
135    // ---- RFC richer addressing (expressible, not led with) ----
136    /// A specific syscall by name (e.g. `"futex"`), the Nth occurrence of which
137    /// is the anchor. Combine with `phase` and `nth`.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub syscall: Option<String>,
140
141    /// Which phase of the named `syscall` to anchor on.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub phase: Option<PhaseSpec>,
144
145    /// A raw instruction pointer, as a hex string like `"0x401f3c"` or decimal.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub rip: Option<String>,
148
149    /// A cooperative marker name (guest-emitted). Reserved for a future backend.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub mark: Option<String>,
152
153    /// Which occurrence of the addressed point (1-based). Defaults to 1. Only
154    /// meaningful for the occurrence-counted modes (`syscall`, `func`, `rip`,
155    /// `mark`); the absolute-count primaries (`syscalls`, `rcbs`) ignore it.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub nth: Option<u64>,
158}
159
160/// Serializable mirror of [`SyscallPhase`] using lowercase author-friendly names.
161#[derive(PartialEq, Eq, Debug, Clone, Copy, Serialize, Deserialize)]
162#[serde(rename_all = "lowercase")]
163pub enum PhaseSpec {
164    /// Before the syscall instruction executes.
165    #[serde(alias = "pre")]
166    Prehook,
167    /// A nonblocking poll retry.
168    Polling,
169    /// After the syscall logically completes.
170    #[serde(alias = "post")]
171    Posthook,
172}
173
174impl From<PhaseSpec> for SyscallPhase {
175    fn from(p: PhaseSpec) -> Self {
176        match p {
177            PhaseSpec::Prehook => SyscallPhase::Prehook,
178            PhaseSpec::Polling => SyscallPhase::Polling,
179            PhaseSpec::Posthook => SyscallPhase::Posthook,
180        }
181    }
182}
183
184/// One ordering constraint: `before` happens-before `after`.
185#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
186pub struct EdgeSpec {
187    /// The name of the source event (must be observed first).
188    pub before: String,
189    /// The name of the sink event (blocked until the source fires).
190    pub after: String,
191    /// Enforcement strength; defaults to [`Strength::Hard`].
192    #[serde(default)]
193    pub strength: Strength,
194}
195
196/// How strictly an edge is enforced by the scheduler.
197#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Serialize, Deserialize)]
198#[serde(rename_all = "lowercase")]
199pub enum Strength {
200    /// Park the sink thread in a true gate until the source fires. The guarantee
201    /// wanted for constructed repros, and the default.
202    #[default]
203    Hard,
204    /// Merely bias scheduling (priority nudge); the sink may still run if it is
205    /// the only runnable thread.
206    Soft,
207}
208
209// ================================================================================
210// Normalized model
211// ================================================================================
212
213/// A resolved reference to a thread.
214#[derive(PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
215pub struct ThreadRef {
216    /// The symbolic label (map key, or the raw id as text).
217    pub label: String,
218    /// The concrete `DetTid`, when statically known.
219    pub dettid: Option<DetTid>,
220    /// Resolve to the Nth spawned thread, when that is the rule.
221    pub spawn_ordinal: Option<u32>,
222}
223
224/// A source-level location, resolvable to/from an address via debug info.
225#[derive(PartialEq, Eq, Debug, Clone, Default)]
226pub struct CodeLocation {
227    /// Function name.
228    pub function: Option<String>,
229    /// Source file name.
230    pub file: Option<String>,
231    /// Source line number.
232    pub line: Option<u32>,
233}
234
235impl CodeLocation {
236    /// True when this location carries no information.
237    pub fn is_empty(&self) -> bool {
238        self.function.is_none() && self.file.is_none() && self.line.is_none()
239    }
240}
241
242impl fmt::Display for CodeLocation {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        match (&self.function, &self.file, self.line) {
245            (Some(func), _, Some(line)) => write!(f, "{}:{}", func, line),
246            (Some(func), _, None) => write!(f, "{}", func),
247            (None, Some(file), Some(line)) => write!(f, "{}:{}", file, line),
248            (None, Some(file), None) => write!(f, "{}", file),
249            (None, None, Some(line)) => write!(f, "line {}", line),
250            (None, None, None) => write!(f, "<unlocated>"),
251        }
252    }
253}
254
255/// The deterministic per-thread stop point that an anchor addresses.
256///
257/// All variants reduce to "a predicate over this thread's deterministic event
258/// stream plus an occurrence count," but the two leading variants
259/// ([`Position::SyscallCount`] and [`Position::Rcb`]) are absolute counts that
260/// need no per-anchor occurrence tracking.
261#[derive(PartialEq, Eq, Debug, Clone)]
262pub enum Position {
263    /// After the thread has executed exactly this many syscalls (any syscall).
264    SyscallCount(u64),
265
266    /// When the thread's RCB clock reaches this absolute value.
267    Rcb(u64),
268
269    /// The `nth` occurrence of a specific syscall, optionally phase-qualified.
270    Syscall {
271        /// The syscall number.
272        sysno: Sysno,
273        /// Restrict to a phase, or match any phase when `None`.
274        phase: Option<SyscallPhase>,
275        /// 1-based occurrence.
276        nth: u64,
277    },
278
279    /// The `nth` execution of the instruction at an absolute address. The address
280    /// is resolved later when it comes from a [`CodeLocation`].
281    Rip {
282        /// Absolute instruction pointer, or `None` until resolved from a
283        /// [`CodeLocation`].
284        addr: Option<u64>,
285        /// 1-based occurrence.
286        nth: u64,
287    },
288
289    /// A cooperative guest marker. Reserved for a future backend.
290    Marker {
291        /// Marker name.
292        name: String,
293        /// 1-based occurrence.
294        nth: u64,
295    },
296}
297
298impl fmt::Display for Position {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        match self {
301            Position::SyscallCount(n) => write!(f, "after {} syscalls", n),
302            Position::Rcb(m) => write!(f, "at RCB {}", m),
303            Position::Syscall { sysno, phase, nth } => {
304                write!(f, "{}", sysno.name())?;
305                if let Some(p) = phase {
306                    write!(f, "@{:?}", p)?;
307                }
308                write!(f, "#{}", nth)
309            }
310            Position::Rip { addr, nth } => match addr {
311                Some(a) => write!(f, "@{:#x}#{}", a, nth),
312                None => write!(f, "@<unresolved>#{}", nth),
313            },
314            Position::Marker { name, nth } => write!(f, "mark:{}#{}", name, nth),
315        }
316    }
317}
318
319/// A fully normalized anchor: a named, deterministic per-thread stop point.
320#[derive(PartialEq, Eq, Debug, Clone)]
321pub struct Anchor {
322    /// The event name (map key), for diagnostics and edge references.
323    pub name: String,
324    /// The thread this anchor is on.
325    pub thread: ThreadRef,
326    /// The deterministic position selector.
327    pub position: Position,
328    /// Optional human-legible / debug-info-resolved code location.
329    pub location: CodeLocation,
330}
331
332impl fmt::Display for Anchor {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(f, "{}[{}: {}", self.name, self.thread.label, self.position)?;
335        if !self.location.is_empty() {
336            write!(f, " ({})", self.location)?;
337        }
338        write!(f, "]")
339    }
340}
341
342/// A normalized happens-before edge between two anchors.
343#[derive(PartialEq, Eq, Debug, Clone)]
344pub struct HappensBeforeEdge {
345    /// The source anchor name (observed first).
346    pub before: String,
347    /// The sink anchor name (gated until the source fires).
348    pub after: String,
349    /// Enforcement strength.
350    pub strength: Strength,
351}
352
353/// A validated, normalized happens-before program: anchors indexed by name plus
354/// the edge list, guaranteed acyclic with all references resolved.
355#[derive(PartialEq, Eq, Debug, Clone)]
356pub struct HappensBeforeProgram {
357    /// Normalized anchors, keyed by event name.
358    pub anchors: BTreeMap<String, Anchor>,
359    /// The validated, acyclic edge list.
360    pub edges: Vec<HappensBeforeEdge>,
361}
362
363impl HappensBeforeProgram {
364    /// Anchors that still require debug-info resolution (an unresolved RIP from a
365    /// code location, i.e. a `func`/`line` that has not been turned into an
366    /// address yet).
367    pub fn unresolved_locations(&self) -> impl Iterator<Item = &Anchor> {
368        self.anchors.values().filter(|a| {
369            matches!(a.position, Position::Rip { addr: None, .. }) && !a.location.is_empty()
370        })
371    }
372
373    /// True when any anchor addresses a [`Position::SyscallCount`]. The scheduler
374    /// only issues per-syscall happens-before checkpoints when this holds, so a
375    /// program made entirely of not-yet-enforced positions adds no per-syscall
376    /// overhead.
377    pub fn has_syscall_count_anchors(&self) -> bool {
378        self.anchors
379            .values()
380            .any(|a| matches!(a.position, Position::SyscallCount(_)))
381    }
382
383    /// Anchors whose position kind the current scheduler does not yet enforce
384    /// (everything other than [`Position::SyscallCount`]). Reported so a run does
385    /// not silently ignore an authored ordering constraint it cannot honor.
386    pub fn unenforced_positions(&self) -> impl Iterator<Item = &Anchor> {
387        self.anchors
388            .values()
389            .filter(|a| !matches!(a.position, Position::SyscallCount(_)))
390    }
391}
392
393// ================================================================================
394// Errors
395// ================================================================================
396
397/// An error produced while parsing or validating a happens-before specification.
398#[derive(PartialEq, Eq, Debug, Clone)]
399pub enum HappensBeforeError {
400    /// The schema `version` is not understood by this build.
401    UnsupportedVersion(u32),
402    /// An event named more than one position selector, or none.
403    AmbiguousPosition {
404        /// The offending event name.
405        event: String,
406        /// The selectors that were set.
407        found: Vec<String>,
408    },
409    /// A syscall name could not be parsed.
410    UnknownSyscall {
411        /// The offending event name.
412        event: String,
413        /// The unparseable name.
414        name: String,
415    },
416    /// A RIP string could not be parsed as an address.
417    BadRip {
418        /// The offending event name.
419        event: String,
420        /// The unparseable text.
421        text: String,
422    },
423    /// An edge referenced an event that does not exist.
424    UnknownEvent {
425        /// `before` or `after`.
426        which: String,
427        /// The dangling name.
428        name: String,
429    },
430    /// An event referenced a thread label that is not in the `threads` table and
431    /// is not a raw integer id.
432    UnknownThread {
433        /// The offending event name.
434        event: String,
435        /// The dangling thread label.
436        thread: String,
437    },
438    /// The edge graph contains a cycle (listed in discovery order).
439    Cycle(Vec<String>),
440    /// A DSL line could not be parsed.
441    DslSyntax {
442        /// 1-based line number.
443        line: usize,
444        /// What went wrong.
445        message: String,
446    },
447}
448
449impl fmt::Display for HappensBeforeError {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        match self {
452            HappensBeforeError::UnsupportedVersion(v) => write!(
453                f,
454                "unsupported happens-before schema version {} (this build understands {})",
455                v, HAPPENS_BEFORE_VERSION
456            ),
457            HappensBeforeError::AmbiguousPosition { event, found } => {
458                if found.is_empty() {
459                    write!(
460                        f,
461                        "event '{}' must specify a position: a count (syscalls/rcbs), a syscall, a \
462                         rip, a mark, or a code location (func/file/line)",
463                        event
464                    )
465                } else {
466                    write!(
467                        f,
468                        "event '{}' names conflicting positions {:?}; use at most one explicit \
469                         position selector (a code location may accompany it)",
470                        event, found
471                    )
472                }
473            }
474            HappensBeforeError::UnknownSyscall { event, name } => {
475                write!(f, "event '{}' names unknown syscall '{}'", event, name)
476            }
477            HappensBeforeError::BadRip { event, text } => {
478                write!(f, "event '{}' has unparseable rip '{}'", event, text)
479            }
480            HappensBeforeError::UnknownEvent { which, name } => {
481                write!(f, "edge '{}' references unknown event '{}'", which, name)
482            }
483            HappensBeforeError::UnknownThread { event, thread } => write!(
484                f,
485                "event '{}' references unknown thread '{}'",
486                event, thread
487            ),
488            HappensBeforeError::Cycle(names) => {
489                write!(
490                    f,
491                    "happens-before edges contain a cycle: {}",
492                    names.join(" -> ")
493                )
494            }
495            HappensBeforeError::DslSyntax { line, message } => {
496                write!(f, "DSL parse error on line {}: {}", line, message)
497            }
498        }
499    }
500}
501
502impl std::error::Error for HappensBeforeError {}
503
504// ================================================================================
505// Parsing & normalization
506// ================================================================================
507
508impl HappensBeforeSpec {
509    /// Parse a JSON specification.
510    pub fn from_json(s: &str) -> anyhow::Result<HappensBeforeSpec> {
511        Ok(serde_json::from_str(s)?)
512    }
513
514    /// Serialize to pretty JSON.
515    pub fn to_json(&self) -> anyhow::Result<String> {
516        Ok(serde_json::to_string_pretty(self)?)
517    }
518
519    /// Normalize and statically validate: check the version, resolve thread and
520    /// event references, require exactly one position per event, parse syscalls
521    /// and RIPs, and confirm the edge graph is acyclic.
522    pub fn normalize(&self) -> Result<HappensBeforeProgram, HappensBeforeError> {
523        if self.version != HAPPENS_BEFORE_VERSION {
524            return Err(HappensBeforeError::UnsupportedVersion(self.version));
525        }
526
527        let mut anchors = BTreeMap::new();
528        for (name, ev) in &self.events {
529            anchors.insert(name.clone(), self.normalize_event(name, ev)?);
530        }
531
532        // Resolve edges against the anchor table.
533        let mut edges = Vec::with_capacity(self.edges.len());
534        for e in &self.edges {
535            if !anchors.contains_key(&e.before) {
536                return Err(HappensBeforeError::UnknownEvent {
537                    which: "before".to_string(),
538                    name: e.before.clone(),
539                });
540            }
541            if !anchors.contains_key(&e.after) {
542                return Err(HappensBeforeError::UnknownEvent {
543                    which: "after".to_string(),
544                    name: e.after.clone(),
545                });
546            }
547            edges.push(HappensBeforeEdge {
548                before: e.before.clone(),
549                after: e.after.clone(),
550                strength: e.strength,
551            });
552        }
553
554        detect_cycle(&anchors, &edges)?;
555
556        Ok(HappensBeforeProgram { anchors, edges })
557    }
558
559    /// Resolve one event into a normalized [`Anchor`].
560    fn normalize_event(&self, name: &str, ev: &EventSpec) -> Result<Anchor, HappensBeforeError> {
561        let thread = self.resolve_thread(name, &ev.thread)?;
562
563        // A code location can accompany any position; it also *supplies* a RIP
564        // position when no explicit position selector is present.
565        let location = CodeLocation {
566            function: ev.func.clone(),
567            file: ev.file.clone(),
568            line: ev.line,
569        };
570
571        // Determine which *explicit* position selectors are present. A code
572        // location (`func`/`file`/`line`) is descriptive and may accompany any
573        // one of these — the owner's primary anchor is "function foo on thread T
574        // after N syscalls / M RBCs", i.e. a code location *and* a count. The
575        // code location only *becomes* the (deferred RIP) position when no
576        // explicit selector is present at all.
577        let mut found: Vec<&str> = Vec::new();
578        if ev.syscalls.is_some() {
579            found.push("syscalls");
580        }
581        if ev.rcbs.is_some() {
582            found.push("rcbs");
583        }
584        if ev.syscall.is_some() {
585            found.push("syscall");
586        }
587        if ev.rip.is_some() {
588            found.push("rip");
589        }
590        if ev.mark.is_some() {
591            found.push("mark");
592        }
593        let has_code_location = !location.is_empty();
594
595        // Reject multiple explicit selectors outright. A single explicit
596        // selector wins as the position (code location stays descriptive). Zero
597        // explicit selectors is only valid when a code location supplies a RIP.
598        if found.len() > 1 {
599            return Err(HappensBeforeError::AmbiguousPosition {
600                event: name.to_string(),
601                found: found.iter().map(|s| s.to_string()).collect(),
602            });
603        }
604        if found.is_empty() && !has_code_location {
605            return Err(HappensBeforeError::AmbiguousPosition {
606                event: name.to_string(),
607                found: Vec::new(),
608            });
609        }
610
611        let nth = ev.nth.unwrap_or(1);
612        let position = if let Some(n) = ev.syscalls {
613            Position::SyscallCount(n)
614        } else if let Some(m) = ev.rcbs {
615            Position::Rcb(m)
616        } else if let Some(sc) = &ev.syscall {
617            let sysno = Sysno::from_str(sc).map_err(|_| HappensBeforeError::UnknownSyscall {
618                event: name.to_string(),
619                name: sc.clone(),
620            })?;
621            Position::Syscall {
622                sysno,
623                phase: ev.phase.map(Into::into),
624                nth,
625            }
626        } else if let Some(rip) = &ev.rip {
627            let addr = parse_rip(rip).ok_or_else(|| HappensBeforeError::BadRip {
628                event: name.to_string(),
629                text: rip.clone(),
630            })?;
631            Position::Rip {
632                addr: Some(addr),
633                nth,
634            }
635        } else if let Some(mark) = &ev.mark {
636            Position::Marker {
637                name: mark.clone(),
638                nth,
639            }
640        } else {
641            // Code-location-only: a RIP to be resolved later from debug info.
642            debug_assert!(has_code_location);
643            Position::Rip { addr: None, nth }
644        };
645
646        Ok(Anchor {
647            name: name.to_string(),
648            thread,
649            position,
650            location,
651        })
652    }
653
654    /// Resolve an event's `thread` field to a [`ThreadRef`], consulting the
655    /// `threads` table and falling back to a raw integer id.
656    fn resolve_thread(&self, event: &str, thread: &str) -> Result<ThreadRef, HappensBeforeError> {
657        if let Some(spec) = self.threads.get(thread) {
658            Ok(ThreadRef {
659                label: spec.label.clone().unwrap_or_else(|| thread.to_string()),
660                dettid: spec.dettid.map(DetTid::from_raw),
661                spawn_ordinal: spec.spawn_ordinal,
662            })
663        } else if let Ok(raw) = thread.parse::<i32>() {
664            Ok(ThreadRef {
665                label: thread.to_string(),
666                dettid: Some(DetTid::from_raw(raw)),
667                spawn_ordinal: None,
668            })
669        } else {
670            Err(HappensBeforeError::UnknownThread {
671                event: event.to_string(),
672                thread: thread.to_string(),
673            })
674        }
675    }
676}
677
678/// Parse a RIP string: hex (`0x...`) or plain decimal.
679fn parse_rip(s: &str) -> Option<u64> {
680    let s = s.trim();
681    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
682        u64::from_str_radix(hex, 16).ok()
683    } else {
684        s.parse::<u64>().ok()
685    }
686}
687
688/// Detect a cycle in the edge graph via depth-first search, returning the cycle
689/// path if one exists. Anchors are visited in name order for determinism.
690fn detect_cycle(
691    anchors: &BTreeMap<String, Anchor>,
692    edges: &[HappensBeforeEdge],
693) -> Result<(), HappensBeforeError> {
694    // Adjacency: before -> [after...]
695    let mut adj: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
696    for name in anchors.keys() {
697        adj.entry(name.as_str()).or_default();
698    }
699    for e in edges {
700        adj.entry(e.before.as_str())
701            .or_default()
702            .push(e.after.as_str());
703    }
704
705    #[derive(Clone, Copy, PartialEq)]
706    enum Mark {
707        Visiting,
708        Done,
709    }
710    let mut state: BTreeMap<&str, Mark> = BTreeMap::new();
711
712    // Iterative DFS to avoid stack overflow on deep chains, tracking the current
713    // path so we can report a concrete cycle.
714    for root in adj.keys().copied() {
715        if state.contains_key(root) {
716            continue;
717        }
718        // Stack of (node, index of next neighbor to visit).
719        let mut stack: Vec<(&str, usize)> = vec![(root, 0)];
720        let mut path: Vec<&str> = vec![root];
721        state.insert(root, Mark::Visiting);
722
723        while let Some(&mut (node, ref mut idx)) = stack.last_mut() {
724            let neighbors = &adj[node];
725            if *idx < neighbors.len() {
726                let next = neighbors[*idx];
727                *idx += 1;
728                match state.get(next) {
729                    Some(Mark::Visiting) => {
730                        // Found a back-edge: assemble the cycle from `path`.
731                        let start = path.iter().position(|&n| n == next).unwrap_or(0);
732                        let mut cycle: Vec<String> =
733                            path[start..].iter().map(|s| s.to_string()).collect();
734                        cycle.push(next.to_string());
735                        return Err(HappensBeforeError::Cycle(cycle));
736                    }
737                    Some(Mark::Done) => {}
738                    None => {
739                        state.insert(next, Mark::Visiting);
740                        path.push(next);
741                        stack.push((next, 0));
742                    }
743                }
744            } else {
745                state.insert(node, Mark::Done);
746                stack.pop();
747                path.pop();
748            }
749        }
750    }
751    Ok(())
752}
753
754// ================================================================================
755// Terse DSL
756// ================================================================================
757//
758// One edge per non-empty, non-comment line:
759//
760//     writer:free_buffer#342  <  reader:read_buffer#97
761//     writer:futex@post#5     <  reader:@0x401f3c#1
762//     A:rcb=123456            <  B:sc=97
763//
764// Each side is `thread:anchor[#ordinal]`. The anchor token is one of:
765//   * `name`            -> function name (code location)
766//   * `@0xADDR`         -> raw RIP
767//   * `syscall@phase`   -> a named syscall, optional `@pre`/`@post`/`@polling`
768//   * `rcb=M`           -> after M RBCs (owner primary)
769//   * `sc=N`            -> after N syscalls (owner primary)
770// A trailing `#N` sets the occurrence ordinal (ignored by `rcb=`/`sc=`).
771// A `!soft` suffix on the line marks the edge soft; default is hard.
772
773impl HappensBeforeSpec {
774    /// Parse the terse line-oriented DSL into a specification. Symbolic threads
775    /// mentioned by name become entries in the `threads` table.
776    pub fn from_dsl(input: &str) -> Result<HappensBeforeSpec, HappensBeforeError> {
777        let mut spec = HappensBeforeSpec {
778            version: HAPPENS_BEFORE_VERSION,
779            threads: BTreeMap::new(),
780            events: BTreeMap::new(),
781            edges: Vec::new(),
782        };
783        let mut seen_names: BTreeSet<String> = BTreeSet::new();
784
785        for (i, raw_line) in input.lines().enumerate() {
786            let lineno = i + 1;
787            let line = strip_comment(raw_line).trim();
788            if line.is_empty() {
789                continue;
790            }
791
792            // Optional trailing "!soft" / "!hard".
793            let (body, strength) = if let Some(b) = line.strip_suffix("!soft") {
794                (b.trim(), Strength::Soft)
795            } else if let Some(b) = line.strip_suffix("!hard") {
796                (b.trim(), Strength::Hard)
797            } else {
798                (line, Strength::Hard)
799            };
800
801            let (lhs, rhs) = body
802                .split_once('<')
803                .ok_or_else(|| HappensBeforeError::DslSyntax {
804                    line: lineno,
805                    message: "expected '<' separating two events".to_string(),
806                })?;
807
808            let before = parse_dsl_side(lhs.trim(), lineno, &mut spec, &mut seen_names)?;
809            let after = parse_dsl_side(rhs.trim(), lineno, &mut spec, &mut seen_names)?;
810            spec.edges.push(EdgeSpec {
811                before,
812                after,
813                strength,
814            });
815        }
816        Ok(spec)
817    }
818}
819
820/// Strip a `#`-or-`//` comment, but not a `#ordinal` that is part of a token.
821/// We treat `//` as the only comment marker to avoid clashing with `#ordinal`.
822fn strip_comment(line: &str) -> &str {
823    match line.find("//") {
824        Some(idx) => &line[..idx],
825        None => line,
826    }
827}
828
829/// Parse one side of a DSL edge, registering the event and thread in `spec`, and
830/// returning the generated event name.
831fn parse_dsl_side(
832    token: &str,
833    lineno: usize,
834    spec: &mut HappensBeforeSpec,
835    seen_names: &mut BTreeSet<String>,
836) -> Result<String, HappensBeforeError> {
837    let (thread, anchor) = token
838        .split_once(':')
839        .ok_or_else(|| HappensBeforeError::DslSyntax {
840            line: lineno,
841            message: format!("expected 'thread:anchor' in '{}'", token),
842        })?;
843    let thread = thread.trim();
844    let anchor = anchor.trim();
845    if thread.is_empty() || anchor.is_empty() {
846        return Err(HappensBeforeError::DslSyntax {
847            line: lineno,
848            message: format!("empty thread or anchor in '{}'", token),
849        });
850    }
851
852    // Split a trailing "#ordinal".
853    let (anchor_body, nth) = match anchor.split_once('#') {
854        Some((a, n)) => {
855            let parsed = n
856                .parse::<u64>()
857                .map_err(|_| HappensBeforeError::DslSyntax {
858                    line: lineno,
859                    message: format!("bad ordinal '#{}'", n),
860                })?;
861            (a.trim(), Some(parsed))
862        }
863        None => (anchor, None),
864    };
865
866    let mut ev = EventSpec {
867        thread: thread.to_string(),
868        nth,
869        ..Default::default()
870    };
871
872    if let Some(rest) = anchor_body.strip_prefix('@') {
873        // raw rip: @0x...
874        ev.rip = Some(rest.to_string());
875    } else if let Some(m) = anchor_body.strip_prefix("rcb=") {
876        ev.rcbs = Some(
877            m.parse::<u64>()
878                .map_err(|_| HappensBeforeError::DslSyntax {
879                    line: lineno,
880                    message: format!("bad rcb count '{}'", m),
881                })?,
882        );
883        ev.nth = None;
884    } else if let Some(n) = anchor_body.strip_prefix("sc=") {
885        ev.syscalls = Some(
886            n.parse::<u64>()
887                .map_err(|_| HappensBeforeError::DslSyntax {
888                    line: lineno,
889                    message: format!("bad syscall count '{}'", n),
890                })?,
891        );
892        ev.nth = None;
893    } else if let Some((sc, phase)) = anchor_body.split_once('@') {
894        // syscall@phase
895        ev.syscall = Some(sc.to_string());
896        ev.phase = Some(parse_dsl_phase(phase, lineno)?);
897    } else if is_syscall_name(anchor_body) {
898        // bare syscall name
899        ev.syscall = Some(anchor_body.to_string());
900    } else {
901        // function name (code location)
902        ev.func = Some(anchor_body.to_string());
903    }
904
905    // Generate a stable, unique event name from the token.
906    let base = sanitize_name(token);
907    let mut ev_name = base.clone();
908    let mut suffix = 1;
909    while seen_names.contains(&ev_name) && spec.events.get(&ev_name) != Some(&ev) {
910        suffix += 1;
911        ev_name = format!("{}_{}", base, suffix);
912    }
913    seen_names.insert(ev_name.clone());
914    spec.events.entry(ev_name.clone()).or_insert(ev);
915
916    // Register the thread label if not already present and not a raw id.
917    if thread.parse::<i32>().is_err() {
918        spec.threads
919            .entry(thread.to_string())
920            .or_insert(ThreadSpec {
921                label: Some(thread.to_string()),
922                dettid: None,
923                spawn_ordinal: None,
924            });
925    }
926
927    Ok(ev_name)
928}
929
930fn parse_dsl_phase(phase: &str, lineno: usize) -> Result<PhaseSpec, HappensBeforeError> {
931    match phase.trim().to_ascii_lowercase().as_str() {
932        "pre" | "prehook" => Ok(PhaseSpec::Prehook),
933        "post" | "posthook" => Ok(PhaseSpec::Posthook),
934        "poll" | "polling" => Ok(PhaseSpec::Polling),
935        other => Err(HappensBeforeError::DslSyntax {
936            line: lineno,
937            message: format!("unknown syscall phase '{}'", other),
938        }),
939    }
940}
941
942/// True when the token parses as a known syscall name.
943fn is_syscall_name(s: &str) -> bool {
944    Sysno::from_str(s).is_ok()
945}
946
947/// Turn a DSL token into a valid, readable event-name slug.
948fn sanitize_name(token: &str) -> String {
949    let mut out = String::with_capacity(token.len());
950    for ch in token.chars() {
951        if ch.is_ascii_alphanumeric() || ch == '_' {
952            out.push(ch);
953        } else {
954            out.push('_');
955        }
956    }
957    if out.is_empty() {
958        out.push('e');
959    }
960    out
961}
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966
967    fn spec_json() -> &'static str {
968        r#"{
969          "version": 1,
970          "threads": { "writer": {"label": "writer"}, "reader": {"label": "reader"} },
971          "events": {
972            "X_342": {"thread": "writer", "func": "free_buffer", "line": 120, "nth": 342},
973            "Y_97":  {"thread": "reader", "func": "read_buffer", "nth": 97},
974            "lockA":  {"thread": "writer", "syscall": "futex", "phase": "posthook", "nth": 5},
975            "storeB": {"thread": "reader", "rip": "0x401f3c", "nth": 1},
976            "scA":    {"thread": "writer", "syscalls": 10},
977            "rcbB":   {"thread": "reader", "rcbs": 123456}
978          },
979          "edges": [
980            {"before": "X_342", "after": "Y_97", "strength": "hard"},
981            {"before": "lockA", "after": "storeB"},
982            {"before": "scA", "after": "rcbB", "strength": "soft"}
983          ]
984        }"#
985    }
986
987    #[test]
988    fn parse_and_normalize_rfc_example() {
989        let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
990        let prog = spec.normalize().unwrap();
991        assert_eq!(prog.anchors.len(), 6);
992        assert_eq!(prog.edges.len(), 3);
993
994        // Owner-primary positions.
995        assert_eq!(prog.anchors["scA"].position, Position::SyscallCount(10));
996        assert_eq!(prog.anchors["rcbB"].position, Position::Rcb(123456));
997
998        // Function+line becomes an unresolved RIP with a code location attached.
999        match &prog.anchors["X_342"].position {
1000            Position::Rip { addr: None, nth } => assert_eq!(*nth, 342),
1001            other => panic!("expected unresolved RIP, got {:?}", other),
1002        }
1003        assert_eq!(
1004            prog.anchors["X_342"].location.function.as_deref(),
1005            Some("free_buffer")
1006        );
1007        assert_eq!(prog.anchors["X_342"].location.line, Some(120));
1008
1009        // Syscall anchor parses the name and phase.
1010        match &prog.anchors["lockA"].position {
1011            Position::Syscall { sysno, phase, nth } => {
1012                assert_eq!(*sysno, Sysno::futex);
1013                assert_eq!(*phase, Some(SyscallPhase::Posthook));
1014                assert_eq!(*nth, 5);
1015            }
1016            other => panic!("expected syscall, got {:?}", other),
1017        }
1018
1019        // RIP anchor.
1020        assert_eq!(
1021            prog.anchors["storeB"].position,
1022            Position::Rip {
1023                addr: Some(0x401f3c),
1024                nth: 1
1025            }
1026        );
1027
1028        // Soft strength preserved.
1029        assert_eq!(prog.edges[2].strength, Strength::Soft);
1030        // Default strength is hard.
1031        assert_eq!(prog.edges[1].strength, Strength::Hard);
1032
1033        // One anchor needs debug-info resolution (X_342, Y_97).
1034        assert_eq!(prog.unresolved_locations().count(), 2);
1035    }
1036
1037    #[test]
1038    fn round_trip_json() {
1039        let spec = HappensBeforeSpec::from_json(spec_json()).unwrap();
1040        let json = spec.to_json().unwrap();
1041        let spec2 = HappensBeforeSpec::from_json(&json).unwrap();
1042        assert_eq!(spec, spec2);
1043    }
1044
1045    #[test]
1046    fn rejects_wrong_version() {
1047        let spec = HappensBeforeSpec {
1048            version: 999,
1049            ..HappensBeforeSpec::from_json(spec_json()).unwrap()
1050        };
1051        assert_eq!(
1052            spec.normalize().unwrap_err(),
1053            HappensBeforeError::UnsupportedVersion(999)
1054        );
1055    }
1056
1057    #[test]
1058    fn rejects_ambiguous_position() {
1059        let json = r#"{
1060          "version": 1,
1061          "events": { "bad": {"thread": "1", "syscalls": 3, "rcbs": 5} },
1062          "edges": []
1063        }"#;
1064        let spec = HappensBeforeSpec::from_json(json).unwrap();
1065        match spec.normalize().unwrap_err() {
1066            HappensBeforeError::AmbiguousPosition { event, found } => {
1067                assert_eq!(event, "bad");
1068                assert_eq!(found.len(), 2);
1069            }
1070            other => panic!("expected AmbiguousPosition, got {:?}", other),
1071        }
1072    }
1073
1074    #[test]
1075    fn rejects_no_position() {
1076        let json = r#"{
1077          "version": 1,
1078          "events": { "bad": {"thread": "1"} },
1079          "edges": []
1080        }"#;
1081        let spec = HappensBeforeSpec::from_json(json).unwrap();
1082        assert!(matches!(
1083            spec.normalize().unwrap_err(),
1084            HappensBeforeError::AmbiguousPosition { .. }
1085        ));
1086    }
1087
1088    #[test]
1089    fn code_location_accompanies_count() {
1090        // The owner's primary anchor: "function foo (line L) on thread T after
1091        // N syscalls / M RBCs". The code location is descriptive and the count
1092        // is the enforced position; the two must coexist, not conflict.
1093        let json = r#"{
1094          "version": 1,
1095          "events": {
1096            "w": {"thread": "1", "func": "free_buffer", "line": 342, "syscalls": 7},
1097            "r": {"thread": "1", "func": "read_buffer", "rcbs": 900}
1098          },
1099          "edges": [ {"before": "w", "after": "r"} ]
1100        }"#;
1101        let prog = HappensBeforeSpec::from_json(json)
1102            .unwrap()
1103            .normalize()
1104            .unwrap();
1105
1106        // The count wins as the position; the code location is retained.
1107        assert_eq!(prog.anchors["w"].position, Position::SyscallCount(7));
1108        assert_eq!(
1109            prog.anchors["w"].location.function.as_deref(),
1110            Some("free_buffer")
1111        );
1112        assert_eq!(prog.anchors["w"].location.line, Some(342));
1113
1114        assert_eq!(prog.anchors["r"].position, Position::Rcb(900));
1115        assert_eq!(
1116            prog.anchors["r"].location.function.as_deref(),
1117            Some("read_buffer")
1118        );
1119
1120        // A descriptive-only code location is not an unresolved RIP position.
1121        assert_eq!(prog.unresolved_locations().count(), 0);
1122    }
1123
1124    #[test]
1125    fn raw_dettid_thread() {
1126        let json = r#"{
1127          "version": 1,
1128          "events": { "e": {"thread": "42", "rcbs": 7} },
1129          "edges": []
1130        }"#;
1131        let prog = HappensBeforeSpec::from_json(json)
1132            .unwrap()
1133            .normalize()
1134            .unwrap();
1135        assert_eq!(prog.anchors["e"].thread.dettid, Some(DetTid::from_raw(42)));
1136    }
1137
1138    #[test]
1139    fn rejects_unknown_thread() {
1140        let json = r#"{
1141          "version": 1,
1142          "events": { "e": {"thread": "ghost", "rcbs": 7} },
1143          "edges": []
1144        }"#;
1145        let spec = HappensBeforeSpec::from_json(json).unwrap();
1146        assert!(matches!(
1147            spec.normalize().unwrap_err(),
1148            HappensBeforeError::UnknownThread { .. }
1149        ));
1150    }
1151
1152    #[test]
1153    fn rejects_unknown_event_in_edge() {
1154        let json = r#"{
1155          "version": 1,
1156          "events": { "a": {"thread": "1", "rcbs": 7} },
1157          "edges": [ {"before": "a", "after": "missing"} ]
1158        }"#;
1159        let spec = HappensBeforeSpec::from_json(json).unwrap();
1160        assert!(matches!(
1161            spec.normalize().unwrap_err(),
1162            HappensBeforeError::UnknownEvent { .. }
1163        ));
1164    }
1165
1166    #[test]
1167    fn rejects_unknown_syscall() {
1168        let json = r#"{
1169          "version": 1,
1170          "events": { "a": {"thread": "1", "syscall": "not_a_syscall"} },
1171          "edges": []
1172        }"#;
1173        let spec = HappensBeforeSpec::from_json(json).unwrap();
1174        assert!(matches!(
1175            spec.normalize().unwrap_err(),
1176            HappensBeforeError::UnknownSyscall { .. }
1177        ));
1178    }
1179
1180    #[test]
1181    fn detects_cycle() {
1182        let json = r#"{
1183          "version": 1,
1184          "events": {
1185            "a": {"thread": "1", "rcbs": 1},
1186            "b": {"thread": "1", "rcbs": 2},
1187            "c": {"thread": "1", "rcbs": 3}
1188          },
1189          "edges": [
1190            {"before": "a", "after": "b"},
1191            {"before": "b", "after": "c"},
1192            {"before": "c", "after": "a"}
1193          ]
1194        }"#;
1195        let spec = HappensBeforeSpec::from_json(json).unwrap();
1196        match spec.normalize().unwrap_err() {
1197            HappensBeforeError::Cycle(path) => {
1198                // Path forms a closed loop.
1199                assert_eq!(path.first(), path.last());
1200                assert!(path.len() >= 4);
1201            }
1202            other => panic!("expected Cycle, got {:?}", other),
1203        }
1204    }
1205
1206    #[test]
1207    fn accepts_dag() {
1208        let json = r#"{
1209          "version": 1,
1210          "events": {
1211            "a": {"thread": "1", "rcbs": 1},
1212            "b": {"thread": "1", "rcbs": 2},
1213            "c": {"thread": "1", "rcbs": 3}
1214          },
1215          "edges": [
1216            {"before": "a", "after": "c"},
1217            {"before": "b", "after": "c"}
1218          ]
1219        }"#;
1220        let spec = HappensBeforeSpec::from_json(json).unwrap();
1221        assert!(spec.normalize().is_ok());
1222    }
1223
1224    #[test]
1225    fn dsl_desugars() {
1226        let dsl = "\
1227            // btrfs race: erase-by-key must precede the re-insert
1228            writer:free_buffer#342  <  reader:read_buffer#97
1229            writer:futex@post#5     <  reader:@0x401f3c#1
1230            A:rcb=123456            <  B:sc=97   !soft
1231        ";
1232        let spec = HappensBeforeSpec::from_dsl(dsl).unwrap();
1233        let prog = spec.normalize().unwrap();
1234        assert_eq!(prog.edges.len(), 3);
1235        assert_eq!(prog.anchors.len(), 6);
1236
1237        // The rcb/sc line desugars to owner-primary positions and is soft.
1238        let soft = &prog.edges[2];
1239        assert_eq!(soft.strength, Strength::Soft);
1240        assert_eq!(prog.anchors[&soft.before].position, Position::Rcb(123456));
1241        assert_eq!(
1242            prog.anchors[&soft.after].position,
1243            Position::SyscallCount(97)
1244        );
1245
1246        // futex@post#5 desugars to a phase-qualified syscall.
1247        let futex = prog
1248            .anchors
1249            .values()
1250            .find(
1251                |a| matches!(a.position, Position::Syscall { sysno, .. } if sysno == Sysno::futex),
1252            )
1253            .unwrap();
1254        match &futex.position {
1255            Position::Syscall { phase, nth, .. } => {
1256                assert_eq!(*phase, Some(SyscallPhase::Posthook));
1257                assert_eq!(*nth, 5);
1258            }
1259            _ => unreachable!(),
1260        }
1261    }
1262
1263    #[test]
1264    fn dsl_rejects_missing_arrow() {
1265        let err = HappensBeforeSpec::from_dsl("writer:foo reader:bar").unwrap_err();
1266        assert!(matches!(err, HappensBeforeError::DslSyntax { line: 1, .. }));
1267    }
1268
1269    #[test]
1270    fn parse_rip_forms() {
1271        assert_eq!(parse_rip("0x401f3c"), Some(0x401f3c));
1272        assert_eq!(parse_rip("4201276"), Some(4201276));
1273        assert_eq!(parse_rip("nonsense"), None);
1274    }
1275}