PureXML/Sources/Schema/ParticleRestrictionChoice.swift
Mihaela Mihaljevic 6a7bf78903 fix(schema): literal RecurseAsIfGroup for a non-repeating base choice
An element restricting a base choice was always checked by the
effective-total-range approximation, which exists for one reason: the
literal section 3.9.6 pairing has no allowance for occurrence counts
accumulated across the choice's own repetitions and over-rejects
W3C-read-valid schemas (particlesZ001, whose manifest calls the rule
ambiguous). That ambiguity is impossible when the choice cannot repeat:
at {max occurs} = 1 every accepted child comes from one traversal of one
branch, so the literal rule governs. elementRestrictsChoice now applies
the literal pairing (the derived element, with its own occurrence range,
must validly restrict some non-pointless branch) when base maxOccurs is
1, and keeps the effective-range reading only for repeating choices.

Closes XSTS particlesHa161 (a derived a{0,1} is not a pointwise
NameAndTypeOK restriction of a required branch a{1,1}) and particlesM034
(a derived d1+ cannot map into the branch (d1*, d2+): the required d2 is
unmapped and not emptiable). The settled-valid neighbors that pin the
boundary from the other side all still pass: Ha160, M002, M003, M035,
and the Z001 shape, each now a regression test in
XSDParticleChoiceRestrictionTests. invalid-schemas-accepted 6 -> 4.

particlesIg004 is reclassified as spec-divergent (4 -> 3): section 3.3.2
maps block="#all" and the explicit substitution/extension/restriction
list to the SAME {disallowed substitutions} set, so NameAndTypeOK clause
5's superset requirement holds and the invalid expectation has no spec
path; it is jointly unsatisfiable with the settled Ig001/002/003 (equal
sets, valid) and Ig005 (#all covers the triple, valid).

The remaining three schema deviations are documented divergences, not
open bugs: Ik025/Ik027 (cos-st-derived-ok 2.2.4 reading) and schN12,
newly adjudicated in docs/xsts-deviations.md: its chameleon redefine
fails src-redefine 6.2.2 on NameAndTypeOK clause 1 (no-namespace locals
vs qualified locals), but 6.2.2 cannot be enforced uniformly because
mgO013's settled-VALID schema also violates it; any uniform enforcement
trades a false negative for a valid-schema-rejected false positive.

Also corrects the stale deviations-doc row that listed the already-closed
particlesIk011 and omitted schN12; the failures file is the authority.

Gates: full XSTS release suite green at 0/3/0/4 (valid buckets unchanged
at zero), libxml2 differential green at 244 pinned entries (Ha161 and
M034 newly pinned, both compile under libxml2), full local gate green.
2026-07-05 08:58:35 +02:00

73 lines
4.5 KiB
Swift

extension PureXML.Schema.ParticleRestriction {
/// RecurseAsIfGroup against a CHOICE, split by whether the choice can repeat.
/// A non-repeating choice ({max occurs} = 1) gets the literal §3.9.6 pairing
/// (no occurrence ambiguity exists, so the spec text governs: particlesHa161,
/// particlesM034). A repeating choice is read by effective total range, because
/// the literal rule has no allowance for counts accumulated across the choice's
/// repetitions and over-rejects W3C-read-valid schemas (particlesZ001); the
/// rationale for both regimes is inline in `elementRestrictsChoice`.
static func elementRestrictsGroup(
_ restricted: PureXML.Schema.Particle,
_ base: PureXML.Schema.Particle,
_ baseGroup: PureXML.Schema.Group,
_ types: [String: PureXML.Schema.ElementType],
_ derivation: [String: PureXML.Schema.TypeDerivation],
) -> Bool {
if baseGroup.compositor == .choice {
return elementRestrictsChoice(restricted, base, baseGroup, types, derivation)
}
// RecurseAsIfGroup for a sequence/all base (§3.9.6): the element as a
// {1,1} synthetic group must be within the base group's range and map to
// a base member inside `groupValid`.
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
&& groupValid(PureXML.Schema.Group(compositor: .sequence, particles: [restricted]), baseGroup, types, derivation)
}
private static func elementRestrictsChoice(
_ restricted: PureXML.Schema.Particle,
_ base: PureXML.Schema.Particle,
_ baseGroup: PureXML.Schema.Group,
_ types: [String: PureXML.Schema.ElementType],
_ derivation: [String: PureXML.Schema.TypeDerivation],
) -> Bool {
// A base choice that cannot repeat ({max occurs} = 1) has no occurrence
// ambiguity: every child the type accepts comes from ONE traversal of ONE
// branch, so the literal RecurseAsIfGroup applies as written. The derived
// element (with its own occurrence range) must be a valid restriction of
// some non-pointless branch: for an element branch that is pointwise
// NameAndTypeOK (a derived `a{0,1}` does not restrict a branch `a{1,1}`,
// particlesHa161); for a group branch the element maps into the branch
// with the branch's other members emptiable (a derived `d1+` does not
// restrict `(d1*, d2+)` because every traversal emits a `d2`,
// particlesM034; its twin `d2+` over the same branch is valid,
// particlesM035). The effective-range reading below would accept both.
if base.maxOccurs == 1 {
return rangeWithinWildcard(derivedMin: 1, derivedMax: 1, wildcardMin: base.minOccurs, wildcardMax: base.maxOccurs)
&& baseGroup.particles.filter { $0.maxOccurs != 0 }.contains { valid(restricted, $0, types, derivation) }
}
// A REPEATING base choice is the case the spec's literal rule handles
// badly: §3.9.6 pairs the derived element against one branch with no
// allowance for counts accumulated across the choice's own repetitions,
// which over-rejects e.g. `element{0,∞}` restricting `choice{0,∞}(...)`
// (particlesZ001, whose test doc calls the rule "ambiguous"; W3C reads it
// valid). First principles: the element validly restricts the repeating
// choice iff its leaf is a NameAndTypeOK restriction of some element the
// choice admits AND its effective occurrence range is within the choice's.
// A must-repeat choice (effective min ≥ 2) still rejects a single element
// (particlesL). A documented, bounded under-rejection (exact in-branch
// sequencing is not modelled across repetitions); over-rejection of a
// W3C-read-valid schema is the non-starter.
let neutral = PureXML.Schema.Particle(minOccurs: 1, maxOccurs: 1, term: restricted.term)
let admitted = elementLeaves(of: baseGroup).contains {
valid(neutral, PureXML.Schema.Particle(minOccurs: 1, maxOccurs: 1, term: $0), types, derivation)
}
return admitted
&& rangeWithinWildcard(
derivedMin: restricted.effectiveOccurrenceMin(),
derivedMax: restricted.effectiveOccurrenceMax(),
wildcardMin: base.effectiveOccurrenceMin(),
wildcardMax: base.effectiveOccurrenceMax(),
)
}
}