1use crate::primitives::TokenId;
4
5#[non_exhaustive]
7#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
8pub enum ErrorCode {
9 Unsupported,
11 Malformed,
13 RecursionDetected,
15 NonMonotoneRecursion,
17 InvalidUri,
19 ReferenceResolution,
21 DuplicateResource,
23 DuplicateAnchor,
25 InternalLimitExceeded,
27 EmptyToken,
29 MalformedTokenizer,
31 ArtifactOutOfBounds,
33 ProvenanceIncomplete,
35 IllegalToken,
37 ArtifactMismatch,
39 TooManyDiagnostics,
41}
42
43#[non_exhaustive]
45#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
46pub enum Stage {
47 L1,
49 L2,
51 L3,
53 L4Bind,
55 VocabBuild,
57 ArtifactValidate,
59}
60
61#[non_exhaustive]
64#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
65pub enum LimitKind {
66 EnumSize,
68 ArrayLength,
70 NodeCount,
72 RecursionDepth,
74 StateCount,
76 TransitionCount,
78 ShuffleFields,
80 ShuffleStates,
82 ShuffleEdges,
84 ProductBranches,
86 ProductStates,
88 ProductEdges,
90 VocabSize,
92 TokenByteLen,
94 ArtifactBytes,
96 PropertyCount,
98 DocumentBytes,
100 KeyBytes,
102 StringBytes,
104 NumberBytes,
106 ActiveValidators,
108 CanonicalBytes,
110 UndoEntries,
112 MaskWork,
114 MaskScratchBytes,
116 PlanBytes,
118 SessionBytes,
120 AllocationBytes,
122 DocumentCount,
124 AstNodeCount,
126 AstBytes,
128 ResourceCount,
130 InputBytes,
132 PatternInputBytes,
134 PatternHirNodes,
136 PatternNfaBytes,
138 PatternDfaBytes,
140 PatternCompileWork,
142 UriBytes,
144 AnchorCount,
146 ReferenceCount,
148 SchemaLocationCount,
150 ResourceDepth,
152 DynamicScopeDepth,
154 ResolutionWork,
156 ResourceGraphBytes,
158 ResourceBuildBytes,
160}
161
162#[derive(Clone, PartialEq, Eq, Debug)]
164pub struct CompileError {
165 pub code: ErrorCode,
167 pub stage: Stage,
169 pub json_pointer_path: Option<String>,
171 pub keyword: Option<String>,
173 pub observed: Option<String>,
175 pub limit: Option<(LimitKind, usize, usize)>,
177 pub recoverable: bool,
179 pub message: &'static str,
181}
182
183impl CompileError {
184 #[must_use]
186 pub fn new(code: ErrorCode, stage: Stage, message: &'static str) -> Self {
187 Self {
188 code,
189 stage,
190 json_pointer_path: None,
191 keyword: None,
192 observed: None,
193 limit: None,
194 recoverable: false,
195 message,
196 }
197 }
198
199 #[must_use]
201 pub fn with_pointer(mut self, path: impl Into<String>) -> Self {
202 self.json_pointer_path = Some(path.into());
203 self
204 }
205
206 #[must_use]
208 pub fn with_keyword(mut self, keyword: impl Into<String>) -> Self {
209 self.keyword = Some(keyword.into());
210 self
211 }
212
213 #[must_use]
215 pub fn with_observed(mut self, observed: impl Into<String>) -> Self {
216 self.observed = Some(observed.into());
217 self
218 }
219}
220
221impl std::fmt::Display for CompileError {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 write!(f, "[{:?}/{:?}] {}", self.code, self.stage, self.message)?;
224 if let Some(path) = &self.json_pointer_path {
225 write!(f, " (at {path})")?;
226 }
227 Ok(())
228 }
229}
230
231impl std::error::Error for CompileError {}
232
233#[derive(Clone, PartialEq, Eq, Debug)]
235pub struct VocabError {
236 pub code: ErrorCode,
238 pub token: Option<TokenId>,
240 pub detail: &'static str,
242}
243
244impl VocabError {
245 #[must_use]
247 pub fn empty_token() -> Self {
248 Self {
249 code: ErrorCode::EmptyToken,
250 token: None,
251 detail: "vocabulary token has zero bytes",
252 }
253 }
254
255 #[must_use]
257 pub fn malformed_tokenizer(detail: &'static str) -> Self {
258 Self {
259 code: ErrorCode::MalformedTokenizer,
260 token: None,
261 detail,
262 }
263 }
264
265 #[must_use]
267 pub fn malformed_tokenizer_for(token: TokenId, detail: &'static str) -> Self {
268 Self {
269 code: ErrorCode::MalformedTokenizer,
270 token: Some(token),
271 detail,
272 }
273 }
274
275 #[must_use]
277 pub fn limit_exceeded(detail: &'static str) -> Self {
278 Self {
279 code: ErrorCode::InternalLimitExceeded,
280 token: None,
281 detail,
282 }
283 }
284}
285
286impl std::fmt::Display for VocabError {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 write!(f, "[{:?}/VocabBuild] {}", self.code, self.detail)
289 }
290}
291
292impl std::error::Error for VocabError {}
293
294#[non_exhaustive]
296#[derive(Clone, PartialEq, Eq, Debug)]
297pub enum MatcherError {
298 IllegalToken {
300 token: TokenId,
302 },
303 ArtifactMismatch,
305}
306
307impl std::fmt::Display for MatcherError {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 match self {
310 Self::IllegalToken { token } => write!(f, "[IllegalToken] token {}", token.get()),
311 Self::ArtifactMismatch => write!(f, "[ArtifactMismatch]"),
312 }
313 }
314}
315
316impl std::error::Error for MatcherError {}
317
318#[derive(Copy, Clone, PartialEq, Eq, Debug)]
320pub enum HashState {
321 Unhashed,
323 Hash([u8; 32]),
325}
326
327impl HashState {
328 #[must_use]
330 pub fn is_valid_fingerprint(&self) -> bool {
331 matches!(self, Self::Hash(h) if *h != [0u8; 32])
332 }
333}
334
335#[non_exhaustive]
337#[derive(Copy, Clone, PartialEq, Eq, Debug)]
338pub enum ProvenanceProfile {
339 Reference,
341 Ir,
343 CachedArtifact,
345}
346
347#[derive(Copy, Clone, PartialEq, Eq, Debug)]
349pub struct Provenance {
350 pub profile: ProvenanceProfile,
352 pub source_input_hash: HashState,
354 pub ir_hash: HashState,
356 pub cache_valid: bool,
358}
359
360impl Provenance {
361 #[must_use]
363 pub fn reference(source_input_hash: HashState) -> Self {
364 Self {
365 profile: ProvenanceProfile::Reference,
366 source_input_hash,
367 ir_hash: HashState::Unhashed,
368 cache_valid: false,
369 }
370 }
371
372 pub fn validate(&self) -> Result<(), CompileError> {
376 let err = |msg| {
377 Err(CompileError::new(
378 ErrorCode::ProvenanceIncomplete,
379 Stage::ArtifactValidate,
380 msg,
381 ))
382 };
383 if matches!(self.source_input_hash, HashState::Hash(h) if h == [0u8; 32])
384 || matches!(self.ir_hash, HashState::Hash(h) if h == [0u8; 32])
385 {
386 return err("an all-zero hash is never a valid fingerprint");
387 }
388 if !self.source_input_hash.is_valid_fingerprint() {
389 return err("every provenance record requires a source_input_hash");
390 }
391 match self.profile {
392 ProvenanceProfile::Reference => {
393 if self.cache_valid {
394 return err("a reference-build artifact is never cache-valid");
395 }
396 Ok(())
397 }
398 ProvenanceProfile::Ir => {
399 if !self.ir_hash.is_valid_fingerprint() {
400 return err("an IR-profile artifact requires an ir_hash");
401 }
402 if self.cache_valid {
403 return err("an IR-profile artifact is never cache-valid");
404 }
405 Ok(())
406 }
407 ProvenanceProfile::CachedArtifact => {
408 if !self.ir_hash.is_valid_fingerprint() {
409 return err("a cached artifact requires an ir_hash");
410 }
411 if !self.cache_valid {
412 return err("a cached artifact must be cache-valid");
413 }
414 Ok(())
415 }
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 fn assert_send_sync<T: Send + Sync>() {}
425
426 #[test]
427 fn error_types_are_send_sync() {
428 assert_send_sync::<CompileError>();
429 assert_send_sync::<VocabError>();
430 assert_send_sync::<MatcherError>();
431 assert_send_sync::<ErrorCode>();
432 }
433
434 #[test]
435 fn machine_fields_survive_without_message() {
436 let mut e = CompileError::new(ErrorCode::InternalLimitExceeded, Stage::L2, "cap")
437 .with_pointer("/properties/name")
438 .with_keyword("maxItems")
439 .with_observed("5");
440 e.limit = Some((LimitKind::ArrayLength, 5, 4));
441 assert_eq!(e.code, ErrorCode::InternalLimitExceeded);
442 assert_eq!(e.stage, Stage::L2);
443 assert_eq!(e.json_pointer_path.as_deref(), Some("/properties/name"));
444 assert_eq!(e.keyword.as_deref(), Some("maxItems"));
445 assert_eq!(e.observed.as_deref(), Some("5"));
446 assert_eq!(e.limit, Some((LimitKind::ArrayLength, 5, 4)));
447 assert_eq!(VocabError::empty_token().code, ErrorCode::EmptyToken);
448 match (MatcherError::IllegalToken { token: TokenId(9) }) {
449 MatcherError::IllegalToken { token } => assert_eq!(token, TokenId(9)),
450 MatcherError::ArtifactMismatch => unreachable!(),
451 }
452 }
453
454 #[test]
455 fn provenance_validated_by_profile() {
456 assert!(Provenance::reference(HashState::Hash([2; 32]))
457 .validate()
458 .is_ok());
459 assert_eq!(
460 Provenance::reference(HashState::Unhashed)
461 .validate()
462 .unwrap_err()
463 .code,
464 ErrorCode::ProvenanceIncomplete
465 );
466 let mut cache_bad = Provenance::reference(HashState::Hash([2; 32]));
467 cache_bad.cache_valid = true;
468 assert_eq!(
469 cache_bad.validate().unwrap_err().code,
470 ErrorCode::ProvenanceIncomplete
471 );
472 let ir_profile = Provenance {
473 profile: ProvenanceProfile::Ir,
474 source_input_hash: HashState::Hash([2; 32]),
475 ir_hash: HashState::Unhashed,
476 cache_valid: false,
477 };
478 assert_eq!(
479 ir_profile.validate().unwrap_err().code,
480 ErrorCode::ProvenanceIncomplete
481 );
482 let cached = Provenance {
483 profile: ProvenanceProfile::CachedArtifact,
484 source_input_hash: HashState::Hash([2; 32]),
485 ir_hash: HashState::Hash([1; 32]),
486 cache_valid: true,
487 };
488 assert!(cached.validate().is_ok());
489 }
490
491 #[test]
492 fn only_cached_artifact_may_be_cache_valid() {
493 let ir_cache_valid = Provenance {
494 profile: ProvenanceProfile::Ir,
495 source_input_hash: HashState::Hash([2; 32]),
496 ir_hash: HashState::Hash([3; 32]),
497 cache_valid: true,
498 };
499 assert_eq!(
500 ir_cache_valid.validate().unwrap_err().code,
501 ErrorCode::ProvenanceIncomplete
502 );
503 let cached_not_valid = Provenance {
504 profile: ProvenanceProfile::CachedArtifact,
505 source_input_hash: HashState::Hash([2; 32]),
506 ir_hash: HashState::Hash([3; 32]),
507 cache_valid: false,
508 };
509 assert_eq!(
510 cached_not_valid.validate().unwrap_err().code,
511 ErrorCode::ProvenanceIncomplete
512 );
513 }
514
515 #[test]
516 fn every_profile_requires_a_source_input_hash() {
517 for profile in [
518 ProvenanceProfile::Reference,
519 ProvenanceProfile::Ir,
520 ProvenanceProfile::CachedArtifact,
521 ] {
522 let p = Provenance {
523 profile,
524 source_input_hash: HashState::Unhashed,
525 ir_hash: HashState::Hash([1; 32]),
526 cache_valid: matches!(profile, ProvenanceProfile::CachedArtifact),
527 };
528 assert_eq!(
529 p.validate().unwrap_err().code,
530 ErrorCode::ProvenanceIncomplete
531 );
532 }
533 }
534
535 #[test]
536 fn zero_hash_is_never_a_valid_fingerprint() {
537 assert!(!HashState::Hash([0; 32]).is_valid_fingerprint());
538 assert!(!HashState::Unhashed.is_valid_fingerprint());
539 assert!(HashState::Hash([1; 32]).is_valid_fingerprint());
540 let zero_source = Provenance {
541 profile: ProvenanceProfile::CachedArtifact,
542 source_input_hash: HashState::Hash([0; 32]),
543 ir_hash: HashState::Hash([1; 32]),
544 cache_valid: true,
545 };
546 assert_eq!(
547 zero_source.validate().unwrap_err().code,
548 ErrorCode::ProvenanceIncomplete
549 );
550 }
551}