Skip to content

fix(#916): reset OCCTSectionBuilder's built flag on failed rebuild - #918

Merged
gsdali merged 6 commits into
mainfrom
fix/916-sectionbuilder-built-flag-stale
Aug 16, 2026
Merged

fix(#916): reset OCCTSectionBuilder's built flag on failed rebuild#918
gsdali merged 6 commits into
mainfrom
fix/916-sectionbuilder-built-flag-stale

Conversation

@gsdali

@gsdali gsdali commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

What & why

OCCTSectionBuilder's built field (gating ancestorFaceOn1(edge:)/ancestorFaceOn2(edge:)) was
only ever set true on a successful Build() and never reset on a failed rebuild — the same
staleness class PR #912 fixed for the sibling OCCTThruSections struct (#910). A SectionBuilder
reused via init1/init2 + build() that had already built successfully once kept built == true through a later failed rebuild, so ancestorFaceOn1/ancestorFaceOn2 skipped their guard
and read state from the failed/incomplete rebuild's BRepAlgoAPI_Section instance.

No live repro existed when #916 was filed — a quick attempt (two far-apart, non-intersecting
boxes) just produces an empty, still-IsDone() section, not a failure. This PR finds one. Read
BRepAlgoAPI_BooleanOperation::Build()/BOPAlgo_PaveFiller::Init() directly
(Libraries/occt-src/...) and swept 13 candidate triggers against the real pinned kernel:
self-intersecting/bowtie faces, coincident/duplicate solids (including two exactly-coincident
spheres), an empty compound, a degenerate collinear-point face, NaN and zero-coefficient plane
coefficients, an invalid #905-style uncapped loft solid, and the too-few-arguments path. None make
Build() cleanly fail on a builder that already built successfully once. The only clean
(non-throwing) trigger found is a literal null TopoDS_Shape argument
(BOPAlgo_PaveFiller::Init()'s own AddError(new BOPAlgo_AlertNullInputShapes) check) — and
Shape never wraps one, so it is not reachable through OCCTSwift's public Swift API.

Proved the defect live anyway, at the bridge boundary rather than through Swift: compiled the real,
unmodified OCCTBridge_Modeling.mm and drove its actual exported OCCTSectionBuilder* C
functions with a hand-constructed null-wrapping OCCTShape as the one input Swift's type system
can't produce. This found something worse than the issue anticipated — an uncatchable SIGSEGV,
not just a stale answer: the failed rebuild's BOPAlgo_PaveFiller::Init() bails via
AddError(BOPAlgo_AlertNullInputShapes) before setting its own myDS, and
HasAncestorFaceOn1/HasAncestorFaceOn2 — reached only because built was wrongly still true —
dereferences it. Full writeup, driver, and before/after transcripts:
Scripts/repro/916-sectionbuilder-built-flag-stale/.

The fix mirrors #912's OCCTThruSections pattern: built = false on both the !IsDone() and
catch (...) paths in OCCTSectionBuilderBuild. It also mirrors #912's own second-review finding
(the AddWire/AddVertex staleness gap): all six
Init1Shape/Init1Plane/Init1Surface/Init2Shape/Init2Plane/Init2Surface functions now
reset built = false on success too, closing the adjacent "re-init after a successful build,
without rebuilding" gap — which is reachable through pure Swift (unlike the null-shape SIGSEGV
above), and is what the new Swift regression test exercises.

Closes #916

CHANGELOG entry

SectionBuilder no longer returns stale results after a failed rebuild (#916)

ancestorFaceOn1(edge:) and ancestorFaceOn2(edge:) read post-build OCCT state gated on a
bridge-side built flag that was only ever set true, never reset on a failed rebuild. A
SectionBuilder reused across multiple build() calls could silently keep answering from a prior
successful build's geometry after a later build() call on the same instance genuinely failed —
and, in the worst case (an argument that fails BRepAlgoAPI_Section::Build()'s own internal
null-shape check), could crash instead of returning nil. Calling init1/init2 again after a
successful build without a following build() call had the identical staleness gap. Fixed by
resetting the tracked build outcome on every path that invalidates the previous result.

SemVer impact

PATCH. A caller of SectionBuilder.ancestorFaceOn1(edge:)/.ancestorFaceOn2(edge:) who reuses a
builder across build() calls, or calls init1/init2 again without a following build(), now
correctly gets nil instead of stale geometry from an earlier build. No signature change; the only
observable difference is a previously-buggy return value becoming correct (or, in the crash case,
no longer crashing). No migration.

Checklist

  • New or changed behavior is covered by a unit test in the same PR (not just manual
    verification): ancestorFaceNilAfterReinitWithoutRebuild in
    Tests/OCCTStressTests/StressBuilderLifecycleTests.swift, covering the half of OCCTSectionBuilder's built flag never resets to false on a failed rebuild (same class as #910) #916 reachable
    through the public Swift API (the Init*-without-rebuild staleness gap). The other half (a
    genuinely failed build() itself, via the null-shape mechanism) is proven at the bridge
    boundary instead — see Scripts/repro/916-sectionbuilder-built-flag-stale/ — since it isn't
    reachable through Swift's type-safe Shape API. Also fixed a fixture bug while writing this
    test: Shape.box(width:height:depth:) (no origin) centers at origin, while
    Shape.box(origin:width:height:depth:) treats origin as a corner — mixing the two (as an
    early draft of this test did, copying SectionBuilderTests.sectionTwoShapes's own fixture
    verbatim) produces boxes that only touch tangentially along a shared face plane, not a genuine
    3D overlap, so no section edge ever resolved an ancestor face. Verified directly in C++ against
    the real kernel before rewriting the fixture.
  • Every new test and every new --self-test case was run once with its subject broken, and the
    failure is reported here: reverted the built = false reset in OCCTSectionBuilderInit1Shape
    only, re-ran ancestorFaceNilAfterReinitWithoutRebuild — it failed with both
    ancestorFaceOn1(edge:) and ancestorFaceOn2(edge:) wrongly returning non-nil (Expectation failed: ... == nil on both). Restored the fix; the test and the rest of
    StressSectionBuilderLifecycleTests (7/7) pass again. Separately, at the bridge level: ran
    occt_916_section_builder_stale against the unmodified bridge file first (SIGSEGV, see
    before.txt), then against the fixed one (clean exit, correct nil, see after.txt).
  • The CHANGELOG entry above is complete, and docs/CHANGELOG.md is not in this diff.
  • The SemVer impact above is stated, and docs/SEMVER.md is not in this diff.

Notes for the reviewer

code-style is expected red here, for the same reason as #912: OCCTBridge_Modeling.mm is
still on Scripts/style-manifest-bridge.txt (a full clang-format pass is ~24,000 diff lines for
this file alone — disproportionate for a 2-function-signature change, and tracked by #917, a
sub-issue of #876). code-style is not a required status check (only gate-scripts is, confirmed
via the repo's rulesets), so this doesn't block merge. Unlike #912, this PR did bring
SectionBuilder.swift into full swift-format/swiftlint compliance and remove it from
Scripts/style-manifest-swift.txt — matching #912's own precedent for the analogous small Swift
file (import order, one blank-comment-line-after-summary fix per doc comment, one line wrap).

Verified: StressSectionBuilderLifecycleTests (7/7), full OCCTModelingTests (645/645), full
OCCTStressTests (361/361), all six static gate scripts (check-bridge-index,
check-null-handle-guards, check-docs-defaults, check-docs-existence,
derive-bridge-header-split --verify, count-operations), and check-style-manifest.py --base origin/main (clean except the expected, precedented OCCTBridge_Modeling.mm exemption above).

gsdali and others added 3 commits August 15, 2026 23:57
OCCTSectionBuilder's `built` field (gating ancestorFaceOn1/2) was only ever
set true on a successful Build() and never reset on a failed rebuild —
the same staleness class PR #912 fixed for the sibling OCCTThruSections
struct (#910). A SectionBuilder reused via init1/init2 + build() that had
already built successfully once kept `built == true` through a later
failed rebuild.

No live repro existed when #916 was filed (far-apart, non-intersecting
boxes just produce an empty, still-IsDone() section, not a failure).
Read BRepAlgoAPI_BooleanOperation::Build()/BOPAlgo_PaveFiller::Init()
directly and swept 13 candidate triggers against the real pinned kernel
(self-intersecting faces, coincident/duplicate solids, an empty compound,
degenerate geometry, NaN/zero plane coefficients, an invalid #905-style
uncapped loft solid, too-few-arguments) — none make Build() cleanly fail
on a reused builder. The only clean (non-throwing) trigger is a literal
null TopoDS_Shape argument, which Shape never wraps and so is not
reachable through the public Swift API.

Proved the defect live anyway at the bridge boundary: compiled the real,
unmodified OCCTBridge_Modeling.mm and drove its actual exported
OCCTSectionBuilder* C functions with a hand-constructed null-wrapping
OCCTShape as the one input Swift's type system can't produce. This found
something worse than a stale answer: an uncatchable SIGSEGV, because the
failed rebuild's BOPAlgo_PaveFiller::Init() bails via
AddError(BOPAlgo_AlertNullInputShapes) before setting its own myDS, and
HasAncestorFaceOn1 (reached only because `built` was wrongly still true)
dereferences it. See Scripts/repro/916-sectionbuilder-built-flag-stale/.

Fix mirrors #912's OCCTThruSections pattern: built = false on both the
!IsDone() and catch(...) paths in OCCTSectionBuilderBuild. Also mirrors
#912's own second-review finding (the AddWire/AddVertex staleness gap):
all six Init1Shape/Init1Plane/Init1Surface/Init2Shape/Init2Plane/
Init2Surface functions now reset built = false on success too, closing
the adjacent "re-init after a successful build, without rebuilding" gap
— reachable through pure Swift, unlike the null-shape SIGSEGV, and
covered by the new ancestorFaceNilAfterReinitWithoutRebuild test.

Doc comments on ancestorFaceOn1(edge:)/ancestorFaceOn2(edge:) (source +
docs/reference/Document-Completions.md) now state the "nil if the last
build did not succeed" contract, matching #912's fix to
ThruSectionsBuilder.swift's doc comments.

Brought SectionBuilder.swift into full swift-format/swiftlint compliance
(import order, doc-comment blank lines, one line wrap) and removed it
from Scripts/style-manifest-swift.txt, matching #912's own precedent for
the analogous small file. OCCTBridge_Modeling.mm stays on the bridge
manifest: a full clang-format pass is ~24,000 diff lines, the same
disproportionate-reformat call #912 made and deferred to #917.

Closes #916

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r-built-flag-stale

Conflict in Scripts/style-manifest-swift.txt: this branch removed
SectionBuilder.swift from the manifest (brought into compliance by
this PR), origin/main (via an unrelated already-merged PR) removed
Section2D.swift. Resolved by removing both — no substantive overlap.
code-style fix: merged origin/main (brings in PR #912's already-merged
"# deferred, see #917" manifest annotation for OCCTBridge_Modeling.mm);
updated that annotation to note PR #918 hit the identical wall days
later — every PR touching this file does, not just #912's. Resolved a
real conflict in Scripts/style-manifest-swift.txt (this branch removed
SectionBuilder.swift, an unrelated already-merged PR removed
Section2D.swift — kept both removals).

Self-review, independently verified rather than trusting the PR body's
claims at face value:

- Re-ran the repro's before/after comparison myself (not just read the
  transcripts): compiled the unmodified bridge, reverted the full fix
  via `git apply -R`, and reproduced the claimed SIGSEGV directly
  (exit 139, matching before.txt). A first attempt at this reverted
  only OCCTSectionBuilderBuild and got a false negative — the driver's
  own Init1Shape call, still carrying the OTHER half of the fix,
  independently reset `built` before the crash trigger could fire.
  Redid it as a full reverse-patch to get an authentic revert.
- Re-ran ancestorFaceNilAfterReinitWithoutRebuild's "prove the test
  fails" claim by injecting the same defect (removing Init1Shape's
  reset) myself: failed with both assertions, matching the PR body.
- Verified the Shape.box(origin:...) "corner not center" fixture-bug
  claim directly against the doc comments.
- The one question PR #912's own history raised but this PR's body
  didn't address: does OCCTSectionBuilder have #912's finding-1 class
  of gap (a `built` flag insufficient because internal state
  accumulates across builds, needing a membership check too)? Checked
  the actual OCCT source: myDSFiller is delete+new'd fresh on every
  Build() (BRepAlgoAPI_BuilderAlgo::IntersectShapes), architecturally
  the OPPOSITE of ThruSections' additive-only myEdgeFace map — a
  simple built flag is genuinely sufficient here, confirmed empirically
  via a throwaway probe (setApproximation/computePCurveOn1/2 not
  resetting `built` is correct, not a gap: SectionBuilder has no cached
  .shape accessor to go stale, unlike ThruSectionsBuilder — build()
  always re-executes fresh).
- Found and fixed one real gap: no fenced ```swift snippet on the
  changed public API (CLAUDE.md's Documentation Standards, the same
  finding-9 class from PR #912's own round 2) — added to
  ancestorFaceOn1(edge:)/ancestorFaceOn2(edge:), plus - Parameter/
  - Returns structure to match.

All 5 required static gates clean (code-style still red for the
tracked, deferred #917 reason only — same as #912/#915). Full
`swift test` (5611/5611) passes. SectionBuilder-specific suites
(11/11) re-verified after every change.
@gsdali

gsdali commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Self-review pass, per request, before your own review. Fixed the code-style failure and independently verified (not just re-read) every substantive claim in the PR body.

code-style fix: merged current origin/main (brings in #912's already-merged # deferred, see #917 annotation on OCCTBridge_Modeling.mm in style-manifest-bridge.txt, plus resolved a real, small conflict in style-manifest-swift.txt — this branch and an unrelated already-merged PR each removed a different file from the same list). Updated the annotation to note this is now confirmed recurring (#912 and #918 both hit it) rather than a one-off. code-style still fails, for the same tracked/deferred reason as #912/#915 — not a new problem.

Self-review findings:

  • Re-ran the SIGSEGV repro myself rather than trusting before.txt/after.txt: compiled the unmodified bridge, reverted the fix via git apply -R, reproduced exit 139 directly. Caught my own mistake along the way — a first attempt reverted only OCCTSectionBuilderBuild and got a false negative, because the driver's Init1Shape call still carried the other half of the fix and independently reset built before the trigger could fire. Redid it as a full reverse-patch.
  • Re-ran the "prove the test fails" claim for `ancestorFaceNilAfterReinitWithoutRebuild" by injecting the same defect myself — matches the PR body exactly.
  • Verified the Shape.box(origin:...) corner-vs-center fixture claim against the actual doc comments.
  • The one question this PR's own history should have raised but didn't address: does OCCTSectionBuilder have fix(#910): guard ThruSectionsBuilder.generatedFace(from:) on IsDone() #912's finding-1 class of gap — a built flag insufficient on its own because internal state accumulates across builds (needing a membership check too, the way OCCTThruSections ended up needing)? Checked the actual OCCT source (BRepAlgoAPI_BuilderAlgo::IntersectShapes): myDSFiller is delete+new'd fresh on every Build() call — architecturally the opposite of ThruSections' additive-only myEdgeFace map. A simple built flag is genuinely sufficient here; confirmed empirically with a throwaway probe that setApproximation/computePCurveOn1/2 not resetting built is correct, not a gap (SectionBuilder has no cached .shape accessor to go stale — build() always re-executes fresh, unlike ThruSectionsBuilder).
  • Found and fixed one real gap: no fenced ```swift snippet on the changed public API (CLAUDE.md's Documentation Standards — the same class of finding fix(#910): guard ThruSectionsBuilder.generatedFace(from:) on IsDone() #912's own round 2 caught). Added to ancestorFaceOn1(edge:)/`ancestorFaceOn2(edge:)`, plus `- Parameter`/`- Returns` structure.

All 5 required static gates clean, full swift test (5611/5611), SectionBuilder-specific suites (11/11) re-verified after every change. Pushed as fac32a1.

gsdali added 2 commits August 16, 2026 17:38
Pays down the deferred code-style debt that PR #912 and this PR (#918)
both hit and deferred, tracked as #917 — "every PR touching this file
hits this wall." Rather than defer a third time, applied the file's
own OCCT house style (Sources/OCCTBridge/.clang-format: 2-space indent,
Allman braces, 100-col wrap, one-parameter-per-line for long
signatures) for real and removed both files from
Scripts/style-manifest-bridge.txt.

Pure `clang-format -i -style=file`, applied once to each file, no
manual edits. Mechanically proven safe before applying, not assumed:

- A token-equivalence script stripped all comments and whitespace from
  both the original and clang-format's dry-run output and confirmed
  the CODE TOKEN STREAM is byte-identical for both files (256,949 and
  67,392 chars respectively) — proof that this is a pure layout
  transformation with zero token added, removed, or reordered.
- A second script confirmed COMMENT TEXT content (`//`, `///`, `/* */`)
  is word-for-word identical between original and reformatted, only
  re-wrapped across a different number of physical lines where prose
  exceeded the new column width (54,397 and 83,983 comment characters
  respectively, whitespace-stripped, byte-identical).

Independently self-reviewed beyond the mechanical proof: dispatched 7
parallel agents (6 covering ~1,800-2,900-line chunks of the .mm file
at its own MARK-comment section boundaries, 1 covering the whole .h
file), each given both the original and reformatted text for their
chunk with no shared context, instructed to do a human-style read for
things a token-diff can't catch — comment misattachment, awkward
wrapping, macro/lambda/preprocessor edge cases, logical structure
drift. All 7 reported clean; findings were exclusively cosmetic
(alignment quirks, a couple of long-signature wraps flagged as mildly
verbose but not confusing) — see PR discussion for the full per-chunk
reports. One agent caught and correctly flagged a pre-existing
doc/param-name mismatch in the header as NOT introduced by this PR
(verified against the original).

Verified this doesn't regress any bridge-structure-parsing gate script
(all five, plus count-operations, are line/brace-shape sensitive by
construction, not immune to a reformat that changes how guards read
textually):
- check-null-handle-guards.py: confirmed the detector isn't just
  reporting clean because it went blind on the new two-line Allman
  `if (!x)\n  return Y;` guard shape (previously one-line) — injected
  a real defect (stripped an IsNull() check from a genuine two-line
  post-reformat guard site) and confirmed it was caught, then restored.
- derive-bridge-header-split.py --verify: mapped count unchanged
  (4021) before/after.
- check-docs-defaults.py / check-docs-existence.py: all counts
  unchanged (1472 defaults compared, 0 drift; 6411 symbol refs, 0
  stale) before/after.
- count-operations.py: unchanged (4339).
- check-style-manifest.py: clean against origin/main (both files
  removed from the manifest, no other file touched).
- file-size-check.py scope is Sources/OCCTSwift/*.swift only and is
  report-only regardless — the .mm file's raw line count growing from
  11,045 to 16,804 (Allman braces + wrap) doesn't trip anything, but
  noting it here since it's a large number to see in a diff stat with
  no code-content explanation otherwise.

Full `swift test`: 5611/5611, unchanged from before the reformat.
All 5 required static gates clean.
@gsdali
gsdali merged commit b91ef8e into main Aug 16, 2026
5 checks passed
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.

OCCTSectionBuilder's built flag never resets to false on a failed rebuild (same class as #910)

1 participant