Skip to main content

polydat_core/kernel/subcontext/
error.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Diagnostic types: [`SourceContext`] and [`ContractViolation`].
5
6use super::name::ChildName;
7
8/// Diagnostic context attached to a [`super::ScopeModule`] —
9/// where the module's source came from. Used in error messages
10/// when a contract violation surfaces at spawn or finalize.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct SourceContext {
13    /// Logical label — workload phase / op-template name.
14    /// Free-form; appears verbatim in diagnostics.
15    pub label: String,
16    /// Source file path, if applicable.
17    pub file: Option<String>,
18    /// Line range `(start, end)` if known.
19    pub line_range: Option<(usize, usize)>,
20}
21
22impl SourceContext {
23    /// A context with a label and no file or lines.
24    pub fn new(label: impl Into<String>) -> Self {
25        Self {
26            label: label.into(),
27            file: None,
28            line_range: None,
29        }
30    }
31
32    /// The context of a phase, labelled `phase:<name>`.
33    pub fn for_phase(name: &str) -> Self {
34        Self::new(format!("phase:{name}"))
35    }
36
37    /// The context of an op, labelled `op:<name>`.
38    pub fn for_op(name: &str) -> Self {
39        Self::new(format!("op:{name}"))
40    }
41
42    /// The same context with its source file.
43    pub fn with_file(mut self, file: impl Into<String>) -> Self {
44        self.file = Some(file.into());
45        self
46    }
47
48    /// The same context with its line range.
49    pub fn with_lines(mut self, start: usize, end: usize) -> Self {
50        self.line_range = Some((start, end));
51        self
52    }
53
54    /// Render as a single line for error messages.
55    pub fn display(&self) -> String {
56        let mut s = self.label.clone();
57        if let Some(f) = &self.file {
58            s.push_str(&format!(" ({f}"));
59            if let Some((a, b)) = self.line_range {
60                s.push_str(&format!(":{a}-{b}"));
61            }
62            s.push(')');
63        } else if let Some((a, b)) = self.line_range {
64            s.push_str(&format!(" ({a}-{b})"));
65        }
66        s
67    }
68}
69
70/// Contract violation surfaced at finalize or spawn.
71///
72/// Variants per the cross-binding rules plus the umbrella
73/// [`Self::Compile`] for errors raised by the Polydat compiler when
74/// the body fragment is converted into a program (typically an
75/// unbound identifier in the body, which the compiler catches
76/// after `finalize`'s name-closure check on declared imports).
77///
78/// The set is subcontext_construction.md §7's error contract:
79/// [`Self::UnboundImport`], [`Self::FinalShadow`],
80/// [`Self::DuplicateChild`], [`Self::Compile`],
81/// [`Self::StrictNonePropagation`], and [`Self::Bind`] for a child the
82/// binder could not build under its parent. An import's type and modifier are
83/// checked by the compiler and the kernel's slot types, not here
84/// (subcontext_construction.md §2.2).
85#[derive(Debug, Clone)]
86pub enum ContractViolation {
87    /// Rule 1 — Import resolution: an artifact import has no
88    /// matching parent export.
89    UnboundImport {
90        /// The import's name.
91        import: String,
92        /// Where the import is declared.
93        site: SourceContext,
94    },
95    /// Rule 2 — Final-shadow on export: a child can't redefine
96    /// a parent `const` output.
97    FinalShadow {
98        /// The export shadowed.
99        export: String,
100        /// Where the child redefines it.
101        site: SourceContext,
102    },
103    /// Named-child registry: a duplicate spawn under the same
104    /// name (subcontext_construction.md §4.1). Reports both spawn
105    /// sites.
106    DuplicateChild {
107        /// The child's name.
108        name: ChildName,
109        /// Boxed: this is the only variant carrying two
110        /// `SourceContext`s — boxing one keeps the whole enum (and
111        /// every `Result<_, ContractViolation>`) small.
112        prior_site: Box<SourceContext>,
113        /// The second spawn site.
114        this_site: SourceContext,
115    },
116    /// Polydat compile-time error — the body failed to compile (most
117    /// commonly an unbound identifier: a free identifier in the body
118    /// that no import, parent name, or local binding supplies).
119    Compile(String),
120    /// L2.f strict-mode hardening: an intermediate-layer
121    /// `const` binding's initialization yielded
122    /// `Value::None`, and the build was running with strict
123    /// mode enabled. Per composition_substrate.md L2.f's
124    /// strict-mode hardening clause, silent fall-through to
125    /// the outer scope's binding is rejected in strict mode —
126    /// the author must either ensure the const yields a
127    /// defined value or remove the binding and declare an
128    /// explicit `extern <name>` if fall-through to outer was
129    /// intended. The `bindings` field carries every const
130    /// output that materialised to None.
131    StrictNonePropagation {
132        /// Every const output that materialised to `None`.
133        bindings: Vec<String>,
134        /// Where the bindings are declared.
135        site: SourceContext,
136    },
137    /// Binding the compiled child under its parent failed: a value
138    /// copied from the parent was refused, a const failed when the child
139    /// was initialized, or the parent's engine refused the child's
140    /// program.
141    Bind(std::sync::Arc<crate::KernelError>),
142}
143
144impl std::fmt::Display for ContractViolation {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::UnboundImport { import, site } => write!(
148                f,
149                "unbound import `{import}` (parent does not export it) at {}",
150                site.display()
151            ),
152            Self::FinalShadow { export, site } => write!(
153                f,
154                "child export `{export}` shadows parent's `final` export at {}",
155                site.display()
156            ),
157            Self::DuplicateChild {
158                name,
159                prior_site,
160                this_site,
161            } => write!(
162                f,
163                "duplicate spawn of child `{name}`: prior at {}, this at {}",
164                prior_site.display(),
165                this_site.display()
166            ),
167            Self::Compile(msg) => write!(f, "compile error: {msg}"),
168            Self::StrictNonePropagation { bindings, site } => {
169                let names = bindings.join(", ");
170                write!(
171                    f,
172                    "L2.f strict-mode violation: intermediate-layer const \
173                    binding(s) [{names}] yielded `Value::None` at scope-init \
174                    at {}; strict mode rejects silent fall-through to the \
175                    outer scope. Either ensure the binding yields a defined \
176                    value, or remove the binding and declare \
177                    `extern <name>` explicitly if fall-through to outer was \
178                    intended.",
179                    site.display()
180                )
181            }
182            Self::Bind(e) => write!(f, "binding the child under its parent failed: {e}"),
183        }
184    }
185}
186
187impl From<crate::KernelError> for ContractViolation {
188    fn from(e: crate::KernelError) -> Self {
189        Self::Bind(std::sync::Arc::new(e))
190    }
191}
192
193impl std::error::Error for ContractViolation {}