diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f30c2..be7471e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- A model `Group` now carries an internal `definitionID`, a stable identity stamped on each `` expansion (from the existing per-compile memo key) and shared by every reference to it. It is metadata only (`Group` is not `Equatable`/`Hashable`, so it perturbs no comparison) and has no consumer yet: it is the first step of replacing the per-instance content-model build's per-reference group inlining (the `2^K` doubling-chain hang) with the graph-structured-stack matcher designed in `docs/design/counted-content-automaton.md`. No behaviour change; the XSTS buckets are unchanged. + - The schema-validity differential harness (`XSTSDifferentialTests`, the opt-in libxml2 characterisation for production-readiness stopper 3) now reports each disagreeing schema by its path relative to the corpus root, not its bare basename. The same basename recurs across subdirectories (`Facets/Schemas/gMonth_maxInclusive001.xsd`, which carries `value=""` and is correctly rejected, versus `Facets/gMonth/gMonth_maxInclusive001.xsd`, which carries the contested 2001-REC `--MM--` form), so a basename-only entry hid which file actually disagrees. The header now also records the triage of the "libxml2 stricter" queue: it is dominated by libxml2 over-strictness (a `maxOccurs` like `999999999999999` is a valid unbounded-precision integer libxml2 rejects on its machine-int limit; an unresolvable `include`/`import` it fails to load) and contested datatype-version readings (`gMonth --MM--`, accepted to keep valid XSTS instances; `anyURI` strictness), not clean false negatives. Characterisation only; no shipped behaviour changes. ### Fixed diff --git a/Sources/Schema/ComplexType.swift b/Sources/Schema/ComplexType.swift index c2319be..84b2099 100644 --- a/Sources/Schema/ComplexType.swift +++ b/Sources/Schema/ComplexType.swift @@ -250,10 +250,20 @@ public extension PureXML.Schema { struct Group: Sendable { public var compositor: Compositor public var particles: [Particle] + /// A stable identity shared by every reference that expands to this same + /// `` definition (in the same scope), assigned when the expansion + /// is first compiled. It lets a content-model compiler emit a multiply + /// referenced group once instead of inlining it per reference (the `2^K` + /// build the design doc closes with a graph-structured-stack matcher). It + /// is metadata only: `Group` is not `Equatable`/`Hashable`, so carrying it + /// changes no comparison, and it is `nil` for an inline (non-referenced) + /// group. + public var definitionID: Int? - public init(compositor: Compositor, particles: [Particle]) { + public init(compositor: Compositor, particles: [Particle], definitionID: Int? = nil) { self.compositor = compositor self.particles = particles + self.definitionID = definitionID } } diff --git a/Sources/Schema/GroupParticleMemo.swift b/Sources/Schema/GroupParticleMemo.swift index a0b0e3d..edd15e6 100644 --- a/Sources/Schema/GroupParticleMemo.swift +++ b/Sources/Schema/GroupParticleMemo.swift @@ -18,5 +18,14 @@ extension PureXML.Schema { /// instance per compile. final class GroupParticleMemo { var store: [GroupParticleMemoKey: PureXML.Schema.Particle] = [:] + /// Source of the stable `Group.definitionID` stamped on each distinct + /// expansion as it is first compiled, so every reference sharing the + /// expansion carries the same id and distinct definitions differ. + private var nextID = 0 + + func allocateDefinitionID() -> Int { + defer { nextID += 1 } + return nextID + } } } diff --git a/Sources/Schema/XSDParserParticles.swift b/Sources/Schema/XSDParserParticles.swift index 7418f7b..84e3145 100644 --- a/Sources/Schema/XSDParserParticles.swift +++ b/Sources/Schema/XSDParserParticles.swift @@ -110,8 +110,22 @@ extension PureXML.Schema.XSDParser { guard let inner = modelGroup(in: definition, scoped.visiting(name)) else { return nil } - context.groupMemo.store[key] = inner - return Particle(occurrenceRange: occurrence, term: inner.term) + // Stamp the expansion with a stable identity so a content-model compiler + // can emit this group once rather than inline it per reference; every + // reference sharing this memo key reuses the stamped term. + let stamped = withDefinitionID(context.groupMemo.allocateDefinitionID(), on: inner) + context.groupMemo.store[key] = stamped + return Particle(occurrenceRange: occurrence, term: stamped.term) + } + + /// `particle` with its model-group term carrying `id`, or unchanged when the + /// term is not a group (a group reference always expands to a model group, so + /// this only no-ops defensively). + private static func withDefinitionID(_ id: Int, on particle: Particle) -> Particle { + guard case let .group(group) = particle.term else { return particle } + var stamped = group + stamped.definitionID = id + return Particle(occurrenceRange: particle.occurrenceRange, term: .group(stamped)) } /// The particle for an ``. An abstract head may not appear