diff --git a/CHANGELOG.md b/CHANGELOG.md index 19cb57f..d83a2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Unique Particle Attribution now detects overlapping `xs:any` wildcards involving `##other`. `wildcardsOverlap` returned false for any pair involving `##other`, so two competing wildcards whose namespaces actually intersect, two `##other` constraints (which always share infinitely many namespaces) or `##other` against an enumerated set naming a non-target, non-absent namespace, were not flagged as non-deterministic, and the schema was wrongly accepted. The overlap is now computed correctly: `##other`∩`##other` always overlaps, and `##other`∩enumerated overlaps iff the set names a namespace that is neither absent (`##local`) nor the target. Disjoint cases (`##other` against `##local` or against the target namespace) remain deterministic, so no valid schema is newly rejected. XSTS invalid-schemas-accepted 35 → 31 (wildI009/wildI013/wildI014 and one more), valid-schemas-rejected held at 0. - The content-model matcher no longer blows up on a nested model group with a huge but finite `maxOccurs` (#192, partial). The counted-automaton active set is bounded by the product of the per-counter saturation caps; a finite `maximum` the input cannot reach diff --git a/Sources/Schema/ContentModelDeterminism.swift b/Sources/Schema/ContentModelDeterminism.swift index a6df60b..61c64c2 100644 --- a/Sources/Schema/ContentModelDeterminism.swift +++ b/Sources/Schema/ContentModelDeterminism.swift @@ -290,14 +290,30 @@ public extension PureXML.Schema { private static func wildcardsOverlap(_ lhs: Wildcard, _ rhs: Wildcard) -> Bool { switch (lhs.namespace, rhs.namespace) { case (.any, _), (_, .any): + // `##any` admits every name, so it shares names with any other constraint. + true + case (.other, .other): + // Two `##other` constraints each admit every namespace but their own + // target (and the absent namespace); those infinite sets always + // intersect, so they overlap (XSTS wildI* non-deterministic choices). true case let (.enumerated(lhsSet), .enumerated(rhsSet)): !lhsSet.isDisjoint(with: rhsSet) - default: - false + case let (.other, .enumerated(set)): + otherOverlapsEnumerated(set, target: lhs.targetNamespace) + case let (.enumerated(set), .other): + otherOverlapsEnumerated(set, target: rhs.targetNamespace) } } + /// Whether `##other` (any namespace but the target and the absent namespace) + /// shares a name with an enumerated set: it does iff the set names some + /// namespace that is neither absent (the empty string, `##local`) nor the + /// target namespace `##other` excludes. + private static func otherOverlapsEnumerated(_ set: Set, target: String?) -> Bool { + set.contains { !$0.isEmpty && $0 != (target ?? "") } + } + private static func describe(_ label: TermLabel) -> String { switch label { case let .name(name): name.localName diff --git a/Tests/ContentModelDeterminismTests.swift b/Tests/ContentModelDeterminismTests.swift index f7c2aa3..f3884c2 100644 --- a/Tests/ContentModelDeterminismTests.swift +++ b/Tests/ContentModelDeterminismTests.swift @@ -46,6 +46,25 @@ struct ContentModelDeterminismTests { #expect(rejects(complexType(#""#))) } + @Test("a choice of ##other and an overlapping namespace wildcard is ambiguous") + func test_choiceOfOtherAndOverlappingWildcard() { + // `##other` admits namespace A (A is neither absent nor the target), and so + // does `namespace="A"`, so an A-namespaced element matches both (XSTS wildI009). + #expect(rejects(complexType(""))) + } + + @Test("a choice of two ##other wildcards is ambiguous") + func test_choiceOfTwoOtherWildcards() { + #expect(rejects(complexType(""))) + } + + @Test("a choice of ##other and ##local is deterministic and compiles") + func test_choiceOfOtherAndLocalCompiles() { + // `##other` never admits the absent namespace and `##local` admits only it, + // so they are disjoint: the model is deterministic and must not be rejected. + #expect(!rejects(complexType(""))) + } + @Test("two same-name members of an all group are ambiguous") func test_allWithSameName() { #expect(rejects(complexType(#""#))) diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index a3a5677..f349861 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -143,12 +143,12 @@ struct XSTSSuiteTests { /// full effective-total-range Particle-Valid-Restriction (tracked). /// Further rules, each with valid-schemas-rejected held at 0 (see CHANGELOG and /// docs/xsts-deviations.md), drove invalid-schemas-accepted 130 -> 45 across - /// model-group occurrence, identity-constraint content, derivation base kind, - /// attribute/simple-type rules, composition, simpleContent inline-type checks, - /// complexContent mixedness, and substitution-group anySimpleType checks. + /// model-group occurrence, identity-constraint content, derivation base kind, attribute/simple-type, + /// composition, simpleContent inline-type, complexContent mixedness, and substitution-group anySimpleType checks. /// src-resolve (#185) 43->39; xsi:Not-Allowed (#183) 39->37; NameAndTypeOK restriction-only + cos-all-limited group-ref (#158) 37->35. + /// UPA wildcard-overlap (`##other` was treated as overlapping nothing) 35->31 (wildI009/I013/I014 + one more), no other bucket moved. private let knownSchemaValidRejected = 0 - private let knownSchemaInvalidAccepted = 35 + private let knownSchemaInvalidAccepted = 31 private let knownInstanceValidRejected = 0 /// Instance bucket (133 -> 26), per-step deltas in CHANGELOG.md: xsi:type must derive from the /// declared type (complex/atomic/list/union via the element-ref chain); anyType cannot stand in