The Unique Particle Attribution check no longer inlines group references or caps. A new CompositionalDeterminism engine computes first/followlast over particle identities and summarizes each xs:group once, so a multiply-referenced nested group whose inlined Glushkov automaton would be 2^K positions is checked in O(nodes * particles^2) time. ContentModelDeterminism now delegates to it and the inlining position automaton plus positionCap are deleted, closing the last named interactive-safety resource-bound blocker (production-readiness stopper #4) and the silent under-rejection the cap caused (stopper #3). The compositional engine was proven verdict-equivalent to the prior inlining automaton by a differential over the entire XSTS corpus (zero divergences) before replacing it. The followlast detail was essential: a first attempt that emitted a repeating end-item's loop-back and the suffix-first as separate decision sets under-detected 13 XSTS content models; the differential caught every one. Verification: XSTS gate unchanged (invalid-schemas-accepted 15, valid-schemas- rejected 0, instance buckets held); SchemaDeterminismGroupBlowupTests compiles a 2^20-position model without hanging and rejects an ambiguous one the old cap would have skipped (WASI uses a smaller depth, native time limits gated off WASI); full suite, style, namespacing, and WASM all pass. The per-particle model and QName-only overlap are preserved, so the false-positive direction stays at zero.
74 lines
4.1 KiB
Swift
74 lines
4.1 KiB
Swift
public extension PureXML.Schema {
|
|
/// Unique Particle Attribution (cos-nonambig, XSD 1.0 3.8.6): a content model
|
|
/// must be deterministic, so each element in an instance is attributed to one
|
|
/// particle without lookahead. The model is ambiguous iff some decision set (the
|
|
/// content's `first`, or any item's `followpos`) holds two distinct particles
|
|
/// whose labels can match the same element.
|
|
///
|
|
/// The check delegates to ``PureXML/Schema/CompositionalDeterminism``, which
|
|
/// computes `first`/`followlast` over PARTICLE identities WITHOUT inlining
|
|
/// `<xs:group ref>`, so a multiply-referenced nested group cannot blow up to
|
|
/// `2^K` positions. That gives a proven `O(nodes * particles^2)` bound with no
|
|
/// position cap and no silent skip (production-readiness stopper #4). The
|
|
/// compositional engine was proven verdict-equivalent to the prior inlining
|
|
/// Glushkov automaton by a differential over the whole XSTS corpus (zero
|
|
/// divergences) before it became authoritative; see
|
|
/// docs/design/counted-content-automaton.md.
|
|
///
|
|
/// It works on a LITERAL view of the raw schema tree, distinct from the compiled
|
|
/// (instance) content model: a `ref` is one particle carrying its resolved QName
|
|
/// (prefix bindings, not the blanket target namespace), and substitution groups
|
|
/// are NOT expanded. The overlap test is QName-only, so it can only under-report
|
|
/// substitution-group ambiguities, never over-report.
|
|
enum ContentModelDeterminism {
|
|
/// Every UPA-violation finding across the schema's complex-type content
|
|
/// models, deduplicated by message in first-seen order. Each finding is
|
|
/// located on the content-model node (`sequence`/`choice`/`all`/`group`)
|
|
/// whose particles are ambiguous; a message shared by two models locates on
|
|
/// the first, preserving the deduplicated count.
|
|
static func violationFindings(in schema: XSDTree, context: XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
let bindings = namespaceBindings(schema)
|
|
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
|
var seen: Set<String> = []
|
|
forEachComplexType(schema) { complexType in
|
|
let models = effectiveModelNodes(complexType, bindings, context, [])
|
|
guard !models.isEmpty,
|
|
let conflict = PureXML.Schema.CompositionalDeterminism.violation(models, bindings, context)
|
|
else { return }
|
|
// The conflicting particle the author can act on is in this type's own
|
|
// content, so locate on its own content-model node.
|
|
let node = contentModelNode(complexType) ?? complexType
|
|
if seen.insert(conflict).inserted {
|
|
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: conflict, node: node))
|
|
}
|
|
}
|
|
return findings
|
|
}
|
|
|
|
/// The prefix-to-namespace bindings declared on the schema element (its
|
|
/// `xmlns:prefix` attributes; the default namespace under the empty key).
|
|
private static func namespaceBindings(_ schema: XSDTree) -> [String: String] {
|
|
var bindings: [String: String] = [:]
|
|
for attribute in schema.attributes {
|
|
if attribute.name.prefix == "xmlns" {
|
|
bindings[attribute.name.localName] = attribute.value
|
|
} else if attribute.name.prefix == nil, attribute.name.localName == "xmlns" {
|
|
bindings[""] = attribute.value
|
|
}
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
/// Visits every `complexType` element in the schema document, skipping
|
|
/// foreign annotation content.
|
|
private static func forEachComplexType(_ node: XSDTree, _ visit: (XSDTree) -> Void) {
|
|
let local = PureXML.Schema.XSDNode.localName(node)
|
|
if local == "appinfo" || local == "documentation" { return }
|
|
if local == "complexType" { visit(node) }
|
|
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
|
forEachComplexType(child, visit)
|
|
}
|
|
}
|
|
}
|
|
}
|