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 / SRD
14    /// reference. 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 SRD-67 §"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 the design doc's §7 error contract:
79/// [`Self::UnboundImport`], [`Self::FinalShadow`],
80/// [`Self::DuplicateChild`], [`Self::Compile`], and
81/// [`Self::StrictNonePropagation`]. An import's type and modifier are
82/// checked by the compiler and the kernel's slot types, not here
83/// (design doc §2.2).
84#[derive(Debug, Clone)]
85pub enum ContractViolation {
86    /// Rule 1 — Import resolution: an artifact import has no
87    /// matching parent export.
88    UnboundImport {
89        /// The import's name.
90        import: String,
91        /// Where the import is declared.
92        site: SourceContext,
93    },
94    /// Rule 2 — Final-shadow on export: a child can't redefine
95    /// an immutable parent export.
96    FinalShadow {
97        /// The export shadowed.
98        export: String,
99        /// Where the child redefines it.
100        site: SourceContext,
101    },
102    /// Rule 2 — Shared write-through rewrite was required but
103    /// could not be performed.
104    ///
105    /// Named-child registry: a duplicate spawn under the same
106    /// name (SRD-67 §"Named-child registry"). Reports both spawn
107    /// sites.
108    DuplicateChild {
109        /// The child's name.
110        name: ChildName,
111        /// Boxed: this is the only variant carrying two
112        /// `SourceContext`s — boxing one keeps the whole enum (and
113        /// every `Result<_, ContractViolation>`) small.
114        prior_site: Box<SourceContext>,
115        /// The second spawn site.
116        this_site: SourceContext,
117    },
118    /// Polydat compile-time error — the body failed to compile (most
119    /// commonly: unbound identifier; corresponds to Rule 1's
120    /// closure-binding economy detecting a free identifier with
121    /// no matching import).
122    Compile(String),
123    /// L2.f strict-mode hardening: an intermediate-layer
124    /// `const` binding's Plan B materialisation yielded
125    /// `Value::None`, and the build was running with strict
126    /// mode enabled. Per composition_substrate.md L2.f's
127    /// strict-mode hardening clause, silent fall-through to
128    /// the outer scope's binding is rejected in strict mode —
129    /// the author must either ensure the const yields a
130    /// defined value or remove the binding and declare an
131    /// explicit `extern <name>` if fall-through to outer was
132    /// intended. The `bindings` field carries every const
133    /// output that materialised to None.
134    StrictNonePropagation {
135        /// Every const output that materialised to `None`.
136        bindings: Vec<String>,
137        /// Where the bindings are declared.
138        site: SourceContext,
139    },
140}
141
142impl std::fmt::Display for ContractViolation {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        match self {
145            Self::UnboundImport { import, site } => write!(
146                f,
147                "unbound import `{import}` (parent does not export it) at {}",
148                site.display()
149            ),
150            Self::FinalShadow { export, site } => write!(
151                f,
152                "child export `{export}` shadows parent's `final` export at {}",
153                site.display()
154            ),
155            Self::DuplicateChild {
156                name,
157                prior_site,
158                this_site,
159            } => write!(
160                f,
161                "duplicate spawn of child `{name}`: prior at {}, this at {}",
162                prior_site.display(),
163                this_site.display()
164            ),
165            Self::Compile(msg) => write!(f, "compile error: {msg}"),
166            Self::StrictNonePropagation { bindings, site } => {
167                let names = bindings.join(", ");
168                write!(
169                    f,
170                    "L2.f strict-mode violation: intermediate-layer const \
171                    binding(s) [{names}] yielded `Value::None` at scope-init \
172                    at {}; strict mode rejects silent fall-through to the \
173                    outer scope. Either ensure the binding yields a defined \
174                    value, or remove the binding and declare \
175                    `extern <name>` explicitly if fall-through to outer was \
176                    intended.",
177                    site.display()
178                )
179            }
180        }
181    }
182}
183
184impl std::error::Error for ContractViolation {}