Skip to main content

maskforge_core/
diagnostics.rs

1//! The provenance sidecar. A `Diagnostic` records where and why an occurrence was rejected;
2//! it is keyed by occurrence and never enters the semantic hash.
3
4use bincode::{Decode, Encode};
5
6use crate::ir::StrRef;
7use crate::primitives::NodeId;
8
9/// Why a keyword or shape is not accepted by the regular-subset engine. The discriminant is the
10/// stable machine code (folded into the content hash and pinned by a wire test); variants are
11/// append-only. `advisory` is display-only.
12#[non_exhaustive]
13#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
14pub enum UnsupportedReason {
15    /// A keyword with no supported arm in this dialect.
16    UnsupportedKeyword = 0,
17    /// A `type` union or array of types (never narrowed to one).
18    UnionType = 1,
19    /// An object whose additional properties are open (absent or a non-`false` schema).
20    OpenAdditionalProperties = 2,
21    /// `additionalProperties` given as a subschema rather than `false`.
22    AdditionalPropertiesSchema = 3,
23    /// A cycle in the `$ref` graph (needs a pushdown grammar).
24    RecursiveReference = 4,
25    /// A `$ref` to an external document or the network.
26    NonRegularRef = 5,
27    /// A regex construct the anchored byte engine cannot accept.
28    InvalidPattern = 6,
29    /// `type:number`, `multipleOf`, or a numeric bound outside the supported integer subset.
30    NumericRangeUnsupported = 7,
31    /// `uniqueItems` (set distinctness is context-sensitive).
32    SetDistinctness = 8,
33    /// `oneOf` (exactly-one is context-sensitive; never downgraded to a union).
34    ExactlyOne = 9,
35    /// `not` (complement).
36    Complement = 10,
37    /// `if`/`then`/`else` or `dependentSchemas` (conditional applicator).
38    ConditionalApplicator = 11,
39    /// `dependentRequired` (a deferred obligation).
40    DeferredObligation = 12,
41    /// A keyword whose meaning depends on collected annotations.
42    AnnotationDependent = 13,
43    /// `$dynamicRef`/`$dynamicAnchor` (dynamic scope).
44    DynamicScope = 14,
45    /// A non-scalar `const`/`enum` member.
46    NonScalarLiteral = 15,
47    /// A length-bounded string that also carries a pattern or a non-ASCII body.
48    LengthWithPattern = 16,
49    /// A `format` this dialect does not enforce even under `format_assertion`.
50    FormatUnsupported = 17,
51    /// A `format` combined with `pattern`, `minLength`, or `maxLength` on the same string.
52    FormatWithPattern = 18,
53    /// A `const`/`enum` numeric member whose lexical form is not a plain JSON integer token
54    /// (a fraction, exponent, or magnitude outside `i64`), e.g. `1.0`, `1e0`.
55    NonIntegerNumericLiteral = 19,
56    /// `allOf` (language intersection needs a product-automaton construction this dialect does not
57    /// build; never approximated as a union or a regex concatenation).
58    AllOfIntersectionUnsupported = 20,
59    /// A `minimum`/`maximum` bound on `type:number`. The engine does not build a decimal-comparison
60    /// automaton for a bounded real range under the current profile, so only unbounded `type:number`
61    /// is supported. This is a current-implementation limit, not a proof of non-representability.
62    NumberBoundUnsupported = 21,
63    /// Unconstrained array items (bare `prefixItems`, `items:true`, or no item schema): they admit
64    /// arbitrary-depth JSON, which is not regular. A regular-item array or `items:false` is supported.
65    OpenArrayTail = 22,
66    /// An `OpenObject` reached the byte-DFA compiler; it needs the structured backend instead.
67    StructuredBackendRequired = 23,
68    /// A look-around assertion is outside the supported anchored regular subset.
69    RegexAssertionUnsupported = 24,
70    /// A numeric or named regex backreference is not compiled as an automaton.
71    RegexBackreferenceUnsupported = 25,
72}
73
74impl UnsupportedReason {
75    /// A short, display-only description. Not the machine contract (the discriminant is).
76    #[must_use]
77    pub fn advisory(self) -> &'static str {
78        match self {
79            Self::UnsupportedKeyword => "keyword is not supported in this dialect",
80            Self::UnionType => "a type union is never narrowed; declare one type",
81            Self::OpenAdditionalProperties => {
82                "object has open additionalProperties; a closed-object profile is required"
83            }
84            Self::AdditionalPropertiesSchema => {
85                "additionalProperties as a subschema is not supported"
86            }
87            Self::RecursiveReference => "a cyclic reference needs a pushdown grammar",
88            Self::NonRegularRef => "external or network references are not supported",
89            Self::InvalidPattern => "the regex uses a construct the byte engine cannot accept",
90            Self::NumericRangeUnsupported => {
91                "only signed 64-bit integers with integer bounds are supported"
92            }
93            Self::SetDistinctness => "uniqueItems is context-sensitive",
94            Self::ExactlyOne => "oneOf is context-sensitive and is not downgraded to a union",
95            Self::Complement => "not is context-sensitive",
96            Self::ConditionalApplicator => "conditional applicators are not supported",
97            Self::DeferredObligation => "dependentRequired is not supported",
98            Self::AnnotationDependent => "the keyword depends on collected annotations",
99            Self::DynamicScope => "dynamic scope resolution is not supported",
100            Self::NonScalarLiteral => "a const/enum member must be a scalar",
101            Self::LengthWithPattern => {
102                "a length-bounded string with a pattern or non-ASCII body is not supported"
103            }
104            Self::FormatUnsupported => "this format is not enforced",
105            Self::FormatWithPattern => {
106                "format combined with pattern or a length bound is not supported"
107            }
108            Self::NonIntegerNumericLiteral => "not a plain JSON integer token (fraction/exponent)",
109            Self::AllOfIntersectionUnsupported => {
110                "allOf is a language intersection, which needs a product-automaton construction \
111                 this dialect does not build; never approximated as a union or concatenation"
112            }
113            Self::NumberBoundUnsupported => {
114                "a bounded real-number range is not implemented; only unbounded type:number \
115                 is supported"
116            }
117            Self::OpenArrayTail => {
118                "unconstrained trailing items admit arbitrary-depth JSON (not regular); use \
119                 items:false or a schema-valued items to close the array"
120            }
121            Self::StructuredBackendRequired => {
122                "this schema needs the structured backend, not the byte-DFA compiler: check \
123                 SchemaIR::requires_structured_backend() before calling compile_ir, or use a \
124                 caller that already dispatches automatically (Python: ConstraintSession.from_json_schema)"
125            }
126            Self::RegexAssertionUnsupported => {
127                "the regex assertion is outside the supported anchored regular subset"
128            }
129            Self::RegexBackreferenceUnsupported => {
130                "regex backreferences are not supported by the automaton compiler"
131            }
132        }
133    }
134}
135
136/// A byte span into the original source text, when the frontend tracks one.
137#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Encode, Decode)]
138pub struct Span {
139    /// Byte offset of the span start.
140    pub start: u32,
141    /// Byte offset one past the span end.
142    pub end: u32,
143}
144
145/// One rejected occurrence: the node it produced, the keyword, its RFC 6901 pointer, and the
146/// machine reason. Keyed by occurrence; never hashed into the content address.
147#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
148pub struct Diagnostic {
149    /// The `Unsupported` node this occurrence lowered to.
150    pub node: NodeId,
151    /// The offending keyword, as an interned string reference.
152    pub keyword: StrRef,
153    /// The RFC 6901 pointer to the occurrence, as an interned string reference.
154    pub json_pointer: StrRef,
155    /// The source span, when the frontend tracked one.
156    pub source_span: Option<Span>,
157    /// The machine-stable reason.
158    pub reason: UnsupportedReason,
159}