Skip to content

Offer a page set's pages again when it starts again, and place the last 1 887 - #15

Merged
tannevaled merged 3 commits into
mainfrom
slice8-page-set-exhaustion
Sep 4, 2026
Merged

Offer a page set's pages again when it starts again, and place the last 1 887#15
tannevaled merged 3 commits into
mainfrom
slice8-page-set-exhaustion

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

[2026-09-04 15:02:00 CEST]

81 738 of the 81 750 body fields placed, on 3 088 sheets. Slice 7 placed
79 851 on 3 040. The whole of the 1 887 that had not moved since slice 5.

The disagreement first: the bucket was its label, and the label was pdf.js's crash

1 887 is what "the page set gives no page after the last one" reported, and
classified before any code was written, that is exactly what it was — with a
twist that changes what the slice is.

All ten forms carry page areas whose <occur> writes a max, under a page
set with no <occur> at all.
That is precisely the shape pdf.js exhausts its
stack on. Nine of the ten are among the 77 forms pdf.js cannot lay out:

fields forms
pdf.js recurses to stack exhaustion 1 134 the seven named in slice 3
pdf.js dies of its font defect first, same page set 626 ca-cra__t2121-fill-24e, -25e
pdf.js lays it out — on nine sheets 127 us-ssa__ssa-3371-bk

So slice 3's guard was the right shape and the wrong result. It terminated
by saying the form had run out of pages. It had not.

A page area's <occur max> bounds ONE RUN of the set, not the form

Restarting a page set offers its page areas again. Both references say so and
neither quite carries it out.

pdfium says it in a line. FindPageAreaFromPageSet_Ordered walks a set from
its first child and sets cur_page_count_ = 1 on the page area it settles on
(cxfa_viewlayoutprocessor.cpp:1240-1244) — and cur_page_count_ is the
counter GetNextAvailPageArea tests the max against (:1414-1425). What bounds
the whole run is the set's own max, read against page_set_map_
(:1196-1215), which nothing resets.

pdf.js means the same and cannot reach it. $cleanPage
(template.js:4160-4167) is that reset, but it sits in the LAST branch of
PageSet[$getNextPage] (:4230-4231), below the branch that restarts a usable
set (:4222-4227) — and a set with no <occur> is usable for ever
($isUsable, :4169-4174, first clause !this.occur). 559 of the 560 corpus
forms write a page set with no <occur>.
So the restart fires, is handed back
a page area whose max is spent, and recurses until the stack is gone.

Cleaning the page areas on the restart is the change. Termination is then not a
counter but an answer: the restart has a page to give.

Demonstrated on the seven by name, not asserted.
TestTheSevenFormsPdfjsRecursesForeverOn transcribes each form's page set
attribute for attribute — no corpus document enters the repository — and asks
each for more sheets than it has page areas.

An absent max bounds nothing, whatever the min says

Occur[$clean] reads as though a written min with no max pins the max to
the min (template.js:3925-3932). That branch cannot fire on an attribute
nobody wrote: the constructor tests attributes.max !== "" (:3896-3903), and
a missing attribute is undefined rather than "" (_mkAttributes,
parser.js:79-111) — so getInteger's default of -1 is taken and $clean's
this.max === "" is already false. Only max="" written out reaches it, and
nothing in the corpus writes one.

That is pdf.js arriving somewhere by accident, so it is not read from pdf.js.
pdfium asks the same question with the default suppressed
TryInteger(XFA_Attribute::Max, /*bUseDefault=*/false) — and takes -1. The
two agree on the number by different routes.

us-ssa__ssa-3371-bk writes <occur min="1"/> on its only page area. Read as a
max of one it gives one sheet and 127 fields fall off; pdf.js's dump says nine.

The reconciliation, field by field

81 750 dispositions from main and from the branch, paired on
(form, path, occurrence), same multiset of keys:

past the last sheet the page set gives → placed 1 887
placed → unplaced 0
placed both times, on a different sheet 0

79 851 + 1 887 = 81 738. Third slice running with no loss anywhere.

slice 7 slice 8
placed 79 851 81 738
past the last sheet the page set gives 1 887 0
anchored by a corner, with no size of its own 9 9
no room inside a container that moves in one piece 3 3
sheets 3 040 3 088

The judges, and the pairing re-audited rather than the rate

The one external check this change has is us-ssa__ssa-3371-bk, and it is a
good one: 9 sheets against pdf.js's 9, 0 unplaced, 190 body boxes, 190 paired,
190 on the sheet pdf.js put them on.

slice 7 slice 8
container heights this package computes 7 794 7 794
agree to within 1/100 pt 100.00% 100.00%
heights it cannot measure 0 0
boxes where pdf.js emits a real place 176 176
agree exactly 100.00% 100.00%
flexbox boxes above or left of their container 0 0
forms where it placed every element 476 477
of those, agreeing on the number of sheets 472 473
boxes paired on those forms 150 792 150 982
on the same sheet as pdf.js 100.00% 100.00%
forms placing fewer elements and using MORE sheets 2 2

The pairing, audited because more sheets means more boxes and different
keys.
Of 162 353 body boxes on the 473 agreeing forms: 150 982 paired,
6 781 unnamed and so never keyed, 4 590 keyed but absent from pdf.js's dump
(1 900 presence="hidden"), and 0 dropped because the two sides counted a key
differently
. The same audit against main gives 6 781, 4 590 and 1 900 —
identical — so the one newly comparable form contributed 190 boxes and every
one of them paired.

One correction that came with it, and it is measured by one test

pdf.js starts a form with pageSetIndex: 0 (template.js:5487) on the page set
the first sheet came from, which says a nested page set has been offered when
none has. It changed no answer while the restart did not clean, because the
restart offered the nested set on its second pass. It does now, so it is -1
here — which is what pdfium does, looking at the siblings after the spent page
area and descending into a nested set as it meets one (:1444-1447,
:1249-1258). No corpus form nests a page set — 560 forms, 560 page sets —
so this is measured by TestTheSequenceDescendsIntoANestedPageSet and by
nothing else, and it is stated rather than buried.

A fourth branch also changed: a page set whose own <occur> is spent, with no
page set above it, is the end of the form. pdf.js's answer there is its second
infinite loop
(:4230-4231 cleans the page areas below and calls itself,
leaving its own pageIndex at the end of the list); pdfium returns nullptr
(:1450-1470). Without it a page set's <occur> would bound nothing at all,
and it is now the one branch that still says the form ran out of pages.

The two open items are not this, and I checked before assuming so

  • The four breakBefore/positioned sheet-count disagreements
    us-opm__sf813, us-opm__sf39a, us-uscis__i-600a, us-uscis__i-821 — are
    the same four, unchanged. None of them writes an <occur> on any page
    area
    , so neither half of this change can reach them.
  • The two forms using more sheets than the reference, us-uscis__i-956 and
    i-956g, are likewise unchanged and likewise write no <occur>.

Both stay open.

What is left

anchored by a corner, with no size of their own 9
no room inside a container that moves in one piece 3

Twelve fields of 81 750. What is left is not a bucket any more: it is the
positioned fit check (layout.js:355-364), the breakBefore drift, and the
i-956 overshoot — three sheet-count questions, all of them judged.

🤖 Generated with Claude Code

tannevaled and others added 3 commits September 4, 2026 14:58
A page area's <occur max> bounds how many sheets it makes in ONE RUN of the
page set holding it, not how many the form gets. Restarting the set offers them
afresh. Both references say so and neither quite carries it out, and reading
the max as a bound on the form left 1 887 fields on 10 forms off the paper.

pdfium says it in a line. FindPageAreaFromPageSet_Ordered walks a set from its
first child and sets cur_page_count_ = 1 on the page area it settles on
(cxfa_viewlayoutprocessor.cpp:1240-1244), and cur_page_count_ is the counter
GetNextAvailPageArea tests the max against (:1414-1425). What bounds the whole
run is the SET's own max, read against page_set_map_ (:1196-1215), which
nothing resets.

pdf.js means the same and cannot reach it. Its $cleanPage (template.js:
4160-4167) is exactly that reset, but it sits in the LAST branch of
PageSet[$getNextPage] (:4230-4231), below the branch that restarts a usable set
(:4222-4227) — and a set with no <occur> is usable for ever ($isUsable,
:4169-4174, whose first clause is !this.occur). 559 of the 560 corpus forms
write a page set with no <occur> at all. So the restart fires, is handed back a
page area whose own max is spent, and recurses until the stack is gone. That is
the seven-form crash slice 3 guarded rather than answered, and guarding a
recursion is not the same as answering it: the sequence now terminates because
it has a page to give.

Second, an absent max bounds nothing whatever the min says. Occur[$clean] reads
as though a written min with no max pins the max to the min (template.js:
3925-3932), but that branch cannot fire on an attribute nobody wrote: the
constructor tests attributes.max !== "" (:3896-3903) and a missing attribute is
undefined rather than "" (_mkAttributes, parser.js:79-111), so getInteger's
default of -1 is taken and $clean's this.max === "" is already false. That is
pdf.js arriving somewhere by accident, so it is not read from pdf.js — pdfium
asks with the default suppressed, TryInteger(Max, /*bUseDefault=*/false), and
takes -1 too. The two agree by different routes. It decides one form outright:
us-ssa__ssa-3371-bk writes <occur min="1"/> on its only page area, and pdf.js's
own dump for it is nine sheets.

Third, a page set whose own <occur> is spent with no page set above it is the
end of the form. pdf.js's answer there is its second infinite loop; pdfium
returns nullptr (:1450-1470). Without it a page set's <occur> would bound
nothing at all, and it is the one branch that still says the form ran out.

Last, a form starts with the page set's pageSetIndex at -1 rather than at
pdf.js's 0 (template.js:5487), which said a nested page set had been offered
when none had. It changed no answer while the restart did not clean; it does
now. pdfium looks at the siblings after the spent page area, descending into a
nested set as it meets one (:1444-1447, :1249-1258).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…orms

TestTheSevenFormsPdfjsRecursesForeverOn builds the page set of each of the
seven corpus forms pdf.js exhausts its stack on — ca-cra__rc1-fill-11-25e,
ca-cra__t2042-fill-24e and -25e, us-ssa__ha-4631, us-ssa__ssa-372,
us-ssa__ssa-5062, us-ssa__ssa-766 — transcribed attribute for attribute, since
no corpus document enters this repository, and asks each for more sheets than
it has page areas. Every one of them is one shape: a page set with no <occur>
over page areas that all write a max.

TestTheEighthFormIsNotTheSeven is us-ssa__ssa-3371-bk, which pdf.js DOES lay
out, on nine sheets, from an <occur min="1"/> and nothing else.

TestAPageSetIsStartedAgainAtMostOncePerSheetAsked keeps the guard honest by
naming the one shape that still needs it once the restart cleans: a set holding
no page area of its own and one nested set that may run once, so the two hand
back and forth with no page between them.

The tests that were about a form running out of pages now cap the page SET
rather than the page area, because a capped page area in an uncapped set no
longer ends anything — which is the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reconciled field by field against main, 81 750 dispositions paired on
(form, path, occurrence) with the same multiset of keys: 1 887 placed, 0 lost,
0 fields placed both times on a different sheet. Third slice running with no
loss anywhere.

1 760 of the 1 887 are on forms pdf.js cannot lay out at all — the seven it
recurses to death on, plus ca-cra__t2121-fill-24e and -25e, which have the same
page set and die of its font defect first. The remaining 127 are
us-ssa__ssa-3371-bk, the only external check this change has: the form goes
from one sheet to nine, its 190 body boxes all pair, and every one lands on the
sheet pdf.js put it on.

The pairing re-audited rather than the rate, because more sheets means more
boxes: of 162 353 body boxes on the 473 agreeing forms, 150 982 paired, 6 781
unnamed and so never keyed, 4 590 keyed but absent from pdf.js's dump (1 900
presence="hidden"), and 0 dropped because the two sides counted a key
differently. The same audit on main gives 6 781, 4 590 and 1 900 — identical —
so the newly comparable form contributed 190 boxes and every one paired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit be7dd90 into main Sep 4, 2026
1 check passed
@tannevaled
tannevaled deleted the slice8-page-set-exhaustion branch September 4, 2026 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant