Skip to main content

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}