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