wyrd/authoring/error.rs
1//! Build-time and validate-time errors for author graphs.
2//!
3//! [`ValidationError`] means a definition cannot become a [`crate::Weave`].
4//! [`BuildError`] covers handle misuse and authoring mistakes before that final
5//! validation step (including wrapped validation failures from `build`).
6
7use core::fmt;
8
9use std::string::String;
10use std::vec::Vec;
11
12use crate::foundation::{NumericPath, PortDir, SignalDomain};
13
14/// A graph definition is structurally invalid and cannot become a [`crate::Weave`].
15#[derive(Clone, Debug, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum ValidationError {
18 /// `validate_def` rejects an empty weave id.
19 InvalidWeaveId {
20 /// Author weave id from the definition.
21 weave_id: String,
22 /// Which naming rule the id violated.
23 reason: &'static str,
24 },
25 /// `validate_def` or `Pattern::try_from` rejects a knot id that breaks naming rules.
26 InvalidKnotId {
27 /// Author knot id from the definition.
28 knot_id: String,
29 /// Which naming rule the id violated.
30 reason: &'static str,
31 },
32 /// `validate_def` finds no knots in the weave definition.
33 EmptyWeave {
34 /// Author weave id of the empty definition.
35 weave_id: String,
36 },
37 /// `validate_def` finds two knots with the same id.
38 DuplicateKnotId {
39 /// Knot id that appears more than once.
40 knot_id: String,
41 },
42 /// A thread or pattern export references a knot id not present in the definition.
43 UnknownKnot {
44 /// Knot id that could not be resolved.
45 knot_id: String,
46 },
47 /// A thread or pattern export names a port that the knot kind does not declare.
48 UnknownPort {
49 /// Knot that owns the referenced port.
50 knot_id: String,
51 /// Port name that is not in the knot catalog.
52 port: String,
53 /// Catalog port names valid for this knot kind.
54 expected: Vec<String>,
55 },
56 /// A referenced port exists but is used as the wrong direction (in vs out).
57 WrongPortDirection {
58 /// Knot that owns the referenced port.
59 knot_id: String,
60 /// Port name used with the wrong direction.
61 port: String,
62 /// Direction required by the caller (thread endpoint or export check).
63 expected: PortDir,
64 /// Direction declared by the knot catalog for this port.
65 actual: PortDir,
66 },
67 /// Two threads target the same input port on one knot.
68 FanIn {
69 /// Knot receiving multiple sources on one input.
70 knot_id: String,
71 /// Input port with more than one incoming thread.
72 port: String,
73 },
74 /// The directed thread graph contains a cycle.
75 Cycle {
76 /// One knot on the cycle, when topological sort can identify it.
77 at_knot: Option<String>,
78 },
79 /// A required input port has no thread and is not satisfied by a pattern export.
80 UnconnectedRequired {
81 /// Knot with the dangling required input.
82 knot_id: String,
83 /// Required input port left unwired.
84 port: String,
85 },
86 /// `validate` or `validate_report` exceeds a hard resource budget.
87 BudgetExceeded {
88 /// Budget dimension that was exceeded (knots, threads, fan-out, etc.).
89 metric: &'static str,
90 /// Observed value that crossed the limit.
91 actual: u32,
92 /// Hard budget ceiling for this metric.
93 limit: u32,
94 /// Knot where the overrun was detected, when applicable.
95 at_knot: Option<String>,
96 },
97 /// The weave's numeric path does not match the compiled crate feature.
98 NumericMismatch {
99 /// Numeric path required by the compiled build.
100 expected: NumericPath,
101 /// Numeric path stored on the weave definition.
102 actual: NumericPath,
103 },
104 /// Connected ports or tied variable ports resolve to incompatible signal domains.
105 SignalDomainMismatch {
106 /// Source knot of the conflicting connection or port pair.
107 from_knot: String,
108 /// Source port involved in the mismatch.
109 from_port: String,
110 /// Domain inferred or fixed on the source side.
111 from_domain: SignalDomain,
112 /// Sink knot of the conflicting connection or port pair.
113 to_knot: String,
114 /// Sink port involved in the mismatch.
115 to_port: String,
116 /// Domain required or inferred on the sink side.
117 to_domain: SignalDomain,
118 },
119 /// Active port domain inference could not fix a domain before validation finished.
120 UnresolvedSignalDomain {
121 /// Knot whose port domain stayed unknown.
122 knot_id: String,
123 /// Port that could not be assigned a signal domain.
124 port: String,
125 },
126 /// A knot kind parameter or port constraint fails catalog rules.
127 InvalidParameter {
128 /// Knot carrying the invalid parameter value.
129 knot_id: String,
130 /// Parameter or port attribute that failed validation.
131 parameter: &'static str,
132 /// Why the parameter value or constraint is rejected.
133 reason: &'static str,
134 },
135 /// Knot or thread count exceeds the dense `u16` representation limit.
136 RepresentationOverflow {
137 /// Representation resource kind (`knot` or `thread`).
138 what: &'static str,
139 /// Count present in the definition.
140 actual: usize,
141 /// Maximum storable in the dense weave representation.
142 limit: usize,
143 },
144 /// `Pattern::try_from` rejects the pattern catalog id.
145 InvalidPatternId {
146 /// Pattern catalog id from the definition.
147 pattern_id: String,
148 /// Which naming rule the id violated.
149 reason: &'static str,
150 },
151 /// `Pattern::try_from` finds two exports with the same name.
152 DuplicateExport {
153 /// Export name declared more than once.
154 export: String,
155 },
156 /// Two input exports map to the same inner physical port.
157 DuplicatePatternInput {
158 /// Inner knot owning the doubly exported input.
159 knot_id: String,
160 /// Inner port exported under two names.
161 port: String,
162 /// First export name bound to this port.
163 first_export: String,
164 /// Second export name bound to the same port.
165 duplicate_export: String,
166 },
167 /// An input export targets a port already wired inside the pattern.
168 PatternInputAlreadyConnected {
169 /// Export name that aliases an internally connected input.
170 export: String,
171 /// Inner knot already receiving an internal thread on this port.
172 knot_id: String,
173 /// Inner input port that is not available for external wiring.
174 port: String,
175 },
176}
177
178impl fmt::Display for ValidationError {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 match self {
181 Self::InvalidWeaveId { weave_id, reason } => {
182 write!(f, "invalid weave id '{weave_id}': {reason}")
183 }
184 Self::InvalidKnotId { knot_id, reason } => {
185 write!(f, "invalid knot id '{knot_id}': {reason}")
186 }
187 Self::EmptyWeave { weave_id } => write!(f, "weave '{weave_id}' has no knots"),
188 Self::DuplicateKnotId { knot_id } => write!(f, "duplicate knot id '{knot_id}'"),
189 Self::UnknownKnot { knot_id } => write!(f, "unknown knot '{knot_id}'"),
190 Self::UnknownPort {
191 knot_id,
192 port,
193 expected,
194 } => write!(
195 f,
196 "unknown port '{knot_id}.{port}'; expected one of {}",
197 Join(expected)
198 ),
199 Self::WrongPortDirection {
200 knot_id,
201 port,
202 expected,
203 actual,
204 } => write!(
205 f,
206 "port '{knot_id}.{port}' is {actual:?}, expected {expected:?}"
207 ),
208 Self::FanIn { knot_id, port } => {
209 write!(f, "input '{knot_id}.{port}' has more than one source")
210 }
211 Self::Cycle {
212 at_knot: Some(knot),
213 } => write!(f, "cycle in weave at knot '{knot}'"),
214 Self::Cycle { at_knot: None } => f.write_str("cycle in weave"),
215 Self::UnconnectedRequired { knot_id, port } => {
216 write!(f, "required input '{knot_id}.{port}' is unconnected")
217 }
218 Self::BudgetExceeded {
219 metric,
220 actual,
221 limit,
222 at_knot,
223 } => {
224 write!(f, "{metric} budget exceeded: {actual} > {limit}")?;
225 if let Some(knot) = at_knot {
226 write!(f, " at knot '{knot}'")?;
227 }
228 Ok(())
229 }
230 Self::NumericMismatch { expected, actual } => write!(
231 f,
232 "numeric path mismatch: expected {expected:?}, got {actual:?}"
233 ),
234 Self::SignalDomainMismatch {
235 from_knot,
236 from_port,
237 from_domain,
238 to_knot,
239 to_port,
240 to_domain,
241 } => write!(
242 f,
243 "signal domain mismatch: '{from_knot}.{from_port}' is {from_domain:?}, but '{to_knot}.{to_port}' requires {to_domain:?}"
244 ),
245 Self::UnresolvedSignalDomain { knot_id, port } => write!(
246 f,
247 "signal domain for '{knot_id}.{port}' could not be resolved"
248 ),
249 Self::InvalidParameter {
250 knot_id,
251 parameter,
252 reason,
253 } => write!(
254 f,
255 "invalid parameter '{parameter}' on knot '{knot_id}': {reason}"
256 ),
257 Self::RepresentationOverflow {
258 what,
259 actual,
260 limit,
261 } => write!(
262 f,
263 "{what} count {actual} exceeds representation limit {limit}"
264 ),
265 Self::InvalidPatternId { pattern_id, reason } => {
266 write!(f, "invalid pattern id '{pattern_id}': {reason}")
267 }
268 Self::DuplicateExport { export } => write!(f, "duplicate pattern export '{export}'"),
269 Self::DuplicatePatternInput {
270 knot_id,
271 port,
272 first_export,
273 duplicate_export,
274 } => write!(
275 f,
276 "pattern input '{knot_id}.{port}' is exported twice as '{first_export}' and '{duplicate_export}'"
277 ),
278 Self::PatternInputAlreadyConnected {
279 export,
280 knot_id,
281 port,
282 } => write!(
283 f,
284 "pattern input export '{export}' targets internally connected input '{knot_id}.{port}'"
285 ),
286 }
287 }
288}
289
290/// An authoring operation failed before final graph validation.
291#[derive(Clone, Debug, PartialEq, Eq)]
292#[non_exhaustive]
293pub enum BuildError {
294 /// A weave, knot, or pattern instance id fails non-empty / character rules during authoring.
295 InvalidId {
296 /// Author id that failed naming rules.
297 id: String,
298 /// Which naming rule the id violated.
299 reason: &'static str,
300 },
301 /// `WeaveBuilder::knot` or `include` would introduce a duplicate knot id.
302 DuplicateKnotId {
303 /// Knot id already present in the builder.
304 knot_id: String,
305 },
306 /// A knot handle or port belongs to a different [`crate::WeaveBuilder`] than the current one.
307 ForeignHandle,
308 /// `input`, `output`, or `slot_of` names a port not declared for that knot kind.
309 UnknownPort {
310 /// Knot whose catalog was consulted.
311 knot_id: String,
312 /// Port name that is not declared for this kind.
313 port: String,
314 /// Catalog port names valid for this knot kind.
315 expected: Vec<String>,
316 },
317 /// The named port exists but was requested with the wrong direction.
318 WrongPortDirection {
319 /// Knot that owns the referenced port.
320 knot_id: String,
321 /// Port name used with the wrong direction.
322 port: String,
323 /// Direction requested by the authoring call.
324 expected: PortDir,
325 /// Direction declared by the knot catalog for this port.
326 actual: PortDir,
327 },
328 /// `PatternInstance::input` or `output` names an export not defined by the pattern.
329 UnknownExport {
330 /// Pattern instance prefix from `include`.
331 instance_id: String,
332 /// Export name missing from the pattern definition.
333 export: String,
334 /// Whether an input or output export was requested.
335 direction: PortDir,
336 },
337 /// `include` finds the pattern's numeric path differs from the parent builder.
338 NumericMismatch {
339 /// Numeric path configured on the parent builder.
340 expected: NumericPath,
341 /// Numeric path stored on the included pattern.
342 actual: NumericPath,
343 },
344 /// `connect` would join ports with incompatible fixed signal domains.
345 SignalDomainMismatch {
346 /// Source knot of the rejected connection.
347 from_knot: String,
348 /// Source port of the rejected connection.
349 from_port: String,
350 /// Fixed domain on the output side.
351 from_domain: SignalDomain,
352 /// Sink knot of the rejected connection.
353 to_knot: String,
354 /// Sink port of the rejected connection.
355 to_port: String,
356 /// Fixed domain required on the input side.
357 to_domain: SignalDomain,
358 },
359 /// Authoring would exceed knot-index or owner-token representation limits.
360 RepresentationOverflow {
361 /// Representation resource kind (`knot` or builder owner token).
362 what: &'static str,
363 /// Count or token value that crossed the limit.
364 actual: usize,
365 /// Maximum storable in the authoring representation.
366 limit: usize,
367 },
368 /// A fallible authoring step failed graph validation (via [`From<ValidationError>`]).
369 Validation(ValidationError),
370}
371
372impl From<ValidationError> for BuildError {
373 fn from(value: ValidationError) -> Self {
374 Self::Validation(value)
375 }
376}
377
378impl fmt::Display for BuildError {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 match self {
381 Self::InvalidId { id, reason } => write!(f, "invalid id '{id}': {reason}"),
382 Self::DuplicateKnotId { knot_id } => write!(f, "duplicate knot id '{knot_id}'"),
383 Self::ForeignHandle => f.write_str("handle belongs to a different weave builder"),
384 Self::UnknownPort {
385 knot_id,
386 port,
387 expected,
388 } => write!(
389 f,
390 "unknown port '{knot_id}.{port}'; expected one of {}",
391 Join(expected)
392 ),
393 Self::WrongPortDirection {
394 knot_id,
395 port,
396 expected,
397 actual,
398 } => write!(
399 f,
400 "port '{knot_id}.{port}' is {actual:?}, expected {expected:?}"
401 ),
402 Self::UnknownExport {
403 instance_id,
404 export,
405 direction,
406 } => write!(
407 f,
408 "unknown {direction:?} export '{export}' on pattern instance '{instance_id}'"
409 ),
410 Self::NumericMismatch { expected, actual } => write!(
411 f,
412 "numeric path mismatch: expected {expected:?}, got {actual:?}"
413 ),
414 Self::SignalDomainMismatch {
415 from_knot,
416 from_port,
417 from_domain,
418 to_knot,
419 to_port,
420 to_domain,
421 } => write!(
422 f,
423 "signal domain mismatch: '{from_knot}.{from_port}' is {from_domain:?}, but '{to_knot}.{to_port}' requires {to_domain:?}"
424 ),
425 Self::RepresentationOverflow {
426 what,
427 actual,
428 limit,
429 } => write!(
430 f,
431 "{what} count {actual} exceeds representation limit {limit}"
432 ),
433 Self::Validation(error) => error.fmt(f),
434 }
435 }
436}
437
438struct Join<'a>(&'a [String]);
439impl fmt::Display for Join<'_> {
440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 for (index, item) in self.0.iter().enumerate() {
442 if index != 0 {
443 f.write_str(", ")?;
444 }
445 f.write_str(item)?;
446 }
447 Ok(())
448 }
449}
450
451#[cfg(feature = "std")]
452impl std::error::Error for ValidationError {}
453
454#[cfg(feature = "std")]
455impl std::error::Error for BuildError {
456 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
457 match self {
458 Self::Validation(error) => Some(error),
459 _ => None,
460 }
461 }
462}