Commit Graph

8 Commits

Author SHA1 Message Date
5d38f0c82f
perf(schema): graph-structured-stack content matcher, killing the last 2^K content-model hang (#364)
* perf(schema): graph-structured-stack content matcher, sharing each xs:group (alongside)

The per-instance content-model build inlines every xs:group reference, so a
multiply-referenced nested group (g0=(g1,g1), g1=(g2,g2), ...) explodes to 2^K
states and reset actions: a depth-16 doubling chain already takes seconds, depth
20+ hangs. This compiles the same particle to a counted Recursive Transition
Network instead, where each definitionID group is compiled once and reached by a
call, and simulates it with a graph-structured stack.

ContentRTNBuilder mirrors ContentNFABuilder exactly except that a group with a
definitionID becomes a call into its shared, compiled-once fragment (memoized by
id; the fragment's accept is a return). The callee's counters are deliberately not
propagated to the caller: every counter is reset on its own particle's entry edge,
which is on the path from the callee's start, so a shared fragment resets itself on
every re-entry without the caller re-listing it. Re-listing was the residual
blow-up: it nested the reset-action lists to 2^K even when the states stayed O(K).

ContentRTNMatcher runs configurations (state, counters, gssNode): a call saves the
caller's counters on a GSS node interned by (returnState, savedCounters, position)
and a return restores them, so the shared callee never mixes counter state across
its callers; parents are a set and a popped node replays its pop to a parent added
later (the GLL completeness detail). The build drops to O(groups): a depth-24
doubling chain (2^24 inlined states) compiles to under 480 states in under a
millisecond.

It is gated by a differential (ContentRTNDifferentialTests) against the trusted
inlining matcher ContentNFA as the oracle: the two agree on the accept verdict and
the matched particle over the hand model cases, the named-over-wildcard tie-break,
the shared-group doubling chains that exercise call and return, and a 4000-case
fuzz of nested models with occurrences, wildcards, and shared groups, plus a bound
assertion. Built alongside the inlining matcher and not yet wired into validation,
so no behaviour change and the XSTS buckets are unchanged; the switch-over is gated
on this differential.

* perf(schema): add the streaming/located surface to the GSS matcher, lock it differentially

The content-model validator reads two things off the inlining matcher: matchedParticles
for per-child attribution, and the streaming startStates/step/admissible/isComplete for
located diagnostics (which child breaks the model, and the "expected a, b" hint). The GSS
matcher only had matchesWhole/matchedParticles, so it could not yet replace the inlining
matcher. This adds the streaming surface: admissible works because the closure already
expands call states into the callee's label states, so the follow set has the same shape
as ContentNFA's.

The differential gate now locks the whole surface, not just the accept verdict: driving
both matchers child by child, the first rejected child (step is nil) agrees, the
admissible expectation set agrees at the start and after every accepted child, and
isComplete agrees once the sequence is consumed. The fuzz now stamps group references the
way the parser does, a unique definitionID per distinct group (so a singly-referenced
named group also takes the call path, not only doubled groups), a shared id only for
genuinely shared structure, and some anonymous inline groups, over occurrences and
wildcards. Still alongside, not wired; no behaviour change, XSTS buckets unchanged.

* perf(schema): complete the GSS matcher surface with follow(after:), verified

follow(after:) is the last ContentNFA method the GSS matcher lacked: the completion
path (XSDCompletions) reads it for the acceptable-next labels and whether the content
may end. It composes the streaming surface, and the differential now locks its allowed
set and complete flag to ContentNFA too. The RTN matcher is now a complete drop-in for
the ContentNFA surface the validator and completions read, so the switch-over is a
mechanical builder swap gated on XSTS neutrality. Still alongside; no behaviour change.

* docs(design): record GSS Steps 1-3 done, the switch as a corpus-gated mechanical swap

Updates the counted-content-automaton Step 4 to the current state: the GSS matcher and
its full differential gate are built and green in-repo, and the production switch is now
a three-site builder swap blocked only on the XSTS neutrality measurement, which must run
on a machine that has the corpus. Records the exact swap sites and the gate so the corpus
machine can execute it mechanically.

* perf(schema): validate and complete on the GSS matcher, killing the last 2^K content-model hang

The three call sites that built the inlining content automaton once per validated
element, ComplexValidatorContentModel.sequenceStructureErrors (located structure
errors), ComplexValidator.validateParticle (per-child particle attribution), and
XSDCompletions (acceptable-next labels), now build ContentRTNBuilder and run
ContentRTNMatcher. A schema whose nested <xs:group> references inline to 2^K positions
(the g{i}=(g{i+1},g{i+1}) doubling shape) no longer makes each keystroke-time validation
of an element exponential in the nesting depth: the automaton is built in O(groups).

Validating a short instance against a depth-40 doubling schema (2^40 inlined positions),
the shape an editor sees mid-typing, now completes in milliseconds with a located error
instead of hanging; a new end-to-end test pins it.

Verified conformance-neutral on the full W3C XSTS corpus in release: valid-schemas-rejected
and valid-instances-rejected stay 0, invalid-schemas-accepted and invalid-instances-accepted
unchanged at 8 and 14. The located diagnostics, per-child attribution, and completions are
behaviour-identical by the surface differential (ContentRTNDifferentialTests), which locks
the verdict, matched particle, rejection index, admissible set, isComplete, and follow to
the inlining ContentNFA. ContentNFABuilder/ContentNFA are retained as that oracle.

* docs(design): mark GSS content-matcher Step 4 done, XSTS-neutral in production

Records that the three production sites are switched to the graph-structured-stack
matcher and the switch was measured conformance-neutral on the full W3C XSTS corpus in
release (all four buckets held). Notes the local corpus path and that ContentNFABuilder
is kept as the differential oracle rather than deleted.

* test(schema): compare full matched-particle attribution; bound a cyclic group id

From the adversarial review of the GSS matcher:

- The differential compared only the matched-particle kind (element/wildcard), so it
  could not observe the type/value metadata matchedParticles carries. Every fuzz and
  hand element now carries a per-name fixed value constraint and the comparison projects
  it, so the gate verifies the metadata is carried identically through call/return, not
  only that the kind agrees. A name maps to one constraint, so the comparison does not
  depend on the lowest-state-index tie-break (which only distinguishes same-name
  particles, a UPA violation the compiler rejects before instance validation); the
  matchedParticles doc now records that caveat.

- ContentRTNBuilder.groupFragment memoizes after compiling, so a (malformed) cyclic
  definitionID would recur without bound, the interactive-safety hazard this matcher
  removes. definitionID groups are acyclic by construction (the parser breaks cycles at
  element boundaries, the inlining builder relies on the same), but bound it anyway:
  a cycle degrades to a terminating empty fragment instead of recurring.

No production behaviour change; the differential, full suite, and the full XSTS corpus
in release (16/16, all four buckets held) stay green.
2026-06-28 05:07:37 +02:00
116b260149
docs(design): add the code-level implementation blueprint for the GSS matcher (#362)
The previous section settled the algorithm and the bound proof; this makes the
build turnkey rather than exploratory. It is the full scope of the reverted RTN
attempt (whose group sharing was correct) with the explicit return stack replaced
by the graph-structured stack:

- Step 1: a group-definition identity (`Group.definitionID`, stamped from the
  existing `GroupParticleMemoKey` in `XSDParserParticles`), FP-neutral on its own.
- Step 2: a `ContentRTNBuilder` alongside the trusted inlining `ContentNFABuilder`,
  compiling each definition once via `call`/`return` epsilon edges, counters still
  per-configuration and reset on entry so a shared fragment does not alias.
- Step 3: the GSS simulation: a node `(returnState, parents-set)` interned per
  `(returnState, inputPosition)`, configuration `(state, counters, gssNode)`, with
  call interning and pop fan-out, deduplicated on the configuration key.
- Step 4: the differential gate against the inlining oracle (XSTS + a
  nullable-nested-group fuzz, verdict AND matched-particle equal), then the switch
  and a perf assertion on GSS/configuration COUNTS (not a wall-clock timeout, the
  reverted attempt's lesson).

Each step is its own PR; steps 1-2 keep the oracle authoritative, step 4 is the
only behaviour change and is gated to equivalence before it lands. Design only.
2026-06-28 01:40:47 +02:00
1057fc19d0
docs(design): derive the graph-structured-stack bound for the content matcher (#361)
Closes the proof gap that sank the reverted RTN matcher. The counted automaton
removed the occurrence-magnitude blow-up and the compositional determinism check
removed the determinism group-sharing blow-up, but the per-instance matcher build
(ContentNFABuilder.group) still expands a multiply-referenced group once per
reference, so a doubling chain g0=(g1,g1)... builds 2^K states and hangs on the
first validate. That is the last place stopper 4 is a hang, not a proven bound.

The reverted pushdown rewrite compiled the group DAG to call/return edges
(O(groups) program) but kept EXPLICIT return stacks, so a nullable doubling chain
made the start epsilon-closure reach ~3^depth distinct stacks: the exponential
moved from the build to the per-keystroke closure. It was verdict-correct yet
bounded nothing, and was reverted.

This section derives the fix: a graph-structured stack (the GLL/Earley device).
A GSS node (returnState, parents-set) shares stack suffixes, so configurations are
keyed on (state, counters, gssNode) and the live set is bounded by O(states), not
by the number of distinct stacks. The nullable doubling chain that produced
3^depth explicit stacks produces O(K) GSS nodes. The section states the size and
termination bounds (counters clamped to N+1, non-recursive group DAG), the
deterministic-model O(N*S*D) match, and the same alongside-then-differential-gated
rollout that made the compositional-determinism change FP-safe, with a perf
ASSERTION on configuration/GSS counts rather than a wall-clock limit (the reverted
attempt's lesson). Design only; no code change. Status: DESIGNED, not implemented.
2026-06-28 01:15:51 +02:00
330e827262
docs(design): derive the exact Particle Valid (Restriction) procedure (#252)
* docs(design): derive the exact Particle Valid (Restriction) procedure

The false-negative tail (particles*/elemZ) routes through one approximate
oracle, ParticleRestriction.violation. Rather than patch cases, derive the
exact XSD 1.0 3.9.6 decision procedure once, clean-room from the spec and the
Xerces reference algorithm, with the soundness (no-false-positive) argument for
each clause: normalization, substitution-group expansion, effective total
range, the compositor dispatch table, and the rcases.

Identifies the closeable gaps (subst-group expansion for elemZ026/V020,
MapAndSum effective-total-range, NameAndTypeOK union-member derivation) and
names the genuine spec limitations (Ha161/Z001 contradiction, mgO013 contested,
union-member by-name derivation) as bounded, not bugs. Sets the implementation
order, each clause gated by the proof plus the XSTS valid-rejected=0 gate and an
adversarial over-rejection critic. No behaviour change.

* docs(design): record the empirical finding -- particle-restriction tail is contested

Probing the actual cases overturned the first hypothesis. elemZ026 and
particlesV020 are the same pattern (a derived ref to a substitution-group
MEMBER restricting a base ref to the HEAD), and PureXML already accepts them
because its content model expands a head ref to admit members (for instance
validation), which the restriction oracle inherits -- the Xerces-lenient
reading. They are CONTESTED (Xerces/PureXML accept, XSTS rejects under strict
NameAndTypeOK), so the keystone (adding expansion) would entrench the
acceptance, not close them; closing them risks a false positive. Every
remaining particle case is contested or FP-delicate, so the exact-oracle
keystone yields no clean conformance win. The remaining lever toward the full
bar is the proven resource bound (pushdown matcher), not this oracle. Proof
first earned its keep: it prevented shipping against contested targets.
2026-06-23 10:50:38 +02:00
6f8222a840 feat(schema): compositional UPA determinism, delete positionCap
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.
2026-06-22 22:19:44 +02:00
ba69d716fa docs(design): specify the compositional UPA algorithm to delete positionCap
Turns the 'blocked design frontier' for ContentModelDeterminism.positionCap into
a ready-to-implement, FP-safe plan derived from the current engine. Documents the
root cause (group-ref inlining gives 2^K positions), why naive position
dedup/sharing is unsound (cross-context followpos merge = false positive), the
sound compositional algorithm (per-group memoized nullable/firstParticles/
lastParticles/followlast over particle identities, cross-boundary conflicts
computed per reference-site by the parent, the star condition for repetition,
bounded O(nodes x particles^2)), and the differential-gated rollout (build
alongside the inlining oracle, prove verdict-equality on XSTS + a group-ref fuzz,
only then switch and delete the cap) with acceptance criteria.
2026-06-22 21:08:12 +02:00
b73384bc2e fix(schema): count content-model occurrences 2026-06-19 23:32:04 +02:00
47c3a230e4 docs(schema): design counted content automaton 2026-06-19 09:23:05 +02:00