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 {}