fix(#916): reset OCCTSectionBuilder's built flag on failed rebuild - #918
Conversation
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.
|
Self-review pass, per request, before your own review. Fixed the
Self-review findings:
All 5 required static gates clean, full |
…r-built-flag-stale
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.
…r-built-flag-stale
What & why
OCCTSectionBuilder'sbuiltfield (gatingancestorFaceOn1(edge:)/ancestorFaceOn2(edge:)) wasonly ever set
trueon a successfulBuild()and never reset on a failed rebuild — the samestaleness class PR #912 fixed for the sibling
OCCTThruSectionsstruct (#910). ASectionBuilderreused via
init1/init2+build()that had already built successfully once keptbuilt == truethrough a later failed rebuild, soancestorFaceOn1/ancestorFaceOn2skipped their guardand read state from the failed/incomplete rebuild's
BRepAlgoAPI_Sectioninstance.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. ReadBRepAlgoAPI_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_Shapeargument(
BOPAlgo_PaveFiller::Init()'s ownAddError(new BOPAlgo_AlertNullInputShapes)check) — andShapenever 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.mmand drove its actual exportedOCCTSectionBuilder*Cfunctions with a hand-constructed null-wrapping
OCCTShapeas the one input Swift's type systemcan'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 viaAddError(BOPAlgo_AlertNullInputShapes)before setting its ownmyDS, andHasAncestorFaceOn1/HasAncestorFaceOn2— reached only becausebuiltwas wrongly still true —dereferences it. Full writeup, driver, and before/after transcripts:
Scripts/repro/916-sectionbuilder-built-flag-stale/.The fix mirrors #912's
OCCTThruSectionspattern:built = falseon both the!IsDone()andcatch (...)paths inOCCTSectionBuilderBuild. It also mirrors #912's own second-review finding(the
AddWire/AddVertexstaleness gap): all sixInit1Shape/Init1Plane/Init1Surface/Init2Shape/Init2Plane/Init2Surfacefunctions nowreset
built = falseon 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
SectionBuilderno longer returns stale results after a failed rebuild (#916)ancestorFaceOn1(edge:)andancestorFaceOn2(edge:)read post-build OCCT state gated on abridge-side
builtflag that was only ever settrue, never reset on a failed rebuild. ASectionBuilderreused across multiplebuild()calls could silently keep answering from a priorsuccessful 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 internalnull-shape check), could crash instead of returning
nil. Callinginit1/init2again after asuccessful build without a following
build()call had the identical staleness gap. Fixed byresetting 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 abuilder across
build()calls, or callsinit1/init2again without a followingbuild(), nowcorrectly gets
nilinstead of stale geometry from an earlier build. No signature change; the onlyobservable difference is a previously-buggy return value becoming correct (or, in the crash case,
no longer crashing). No migration.
Checklist
verification):
ancestorFaceNilAfterReinitWithoutRebuildinTests/OCCTStressTests/StressBuilderLifecycleTests.swift, covering the half of OCCTSectionBuilder's built flag never resets to false on a failed rebuild (same class as #910) #916 reachablethrough the public Swift API (the
Init*-without-rebuild staleness gap). The other half (agenuinely failed
build()itself, via the null-shape mechanism) is proven at the bridgeboundary instead — see
Scripts/repro/916-sectionbuilder-built-flag-stale/— since it isn'treachable through Swift's type-safe
ShapeAPI. Also fixed a fixture bug while writing thistest:
Shape.box(width:height:depth:)(no origin) centers at origin, whileShape.box(origin:width:height:depth:)treatsoriginas a corner — mixing the two (as anearly draft of this test did, copying
SectionBuilderTests.sectionTwoShapes's own fixtureverbatim) 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.
--self-testcase was run once with its subject broken, and thefailure is reported here: reverted the
built = falsereset inOCCTSectionBuilderInit1Shapeonly, re-ran
ancestorFaceNilAfterReinitWithoutRebuild— it failed with bothancestorFaceOn1(edge:)andancestorFaceOn2(edge:)wrongly returning non-nil (Expectation failed: ... == nilon both). Restored the fix; the test and the rest ofStressSectionBuilderLifecycleTests(7/7) pass again. Separately, at the bridge level: ranocct_916_section_builder_staleagainst the unmodified bridge file first (SIGSEGV, seebefore.txt), then against the fixed one (clean exit, correctnil, seeafter.txt).docs/CHANGELOG.mdis not in this diff.docs/SEMVER.mdis not in this diff.Notes for the reviewer
code-styleis expected red here, for the same reason as #912:OCCTBridge_Modeling.mmisstill on
Scripts/style-manifest-bridge.txt(a fullclang-formatpass is ~24,000 diff lines forthis file alone — disproportionate for a 2-function-signature change, and tracked by #917, a
sub-issue of #876).
code-styleis not a required status check (onlygate-scriptsis, confirmedvia the repo's rulesets), so this doesn't block merge. Unlike #912, this PR did bring
SectionBuilder.swiftinto fullswift-format/swiftlintcompliance and remove it fromScripts/style-manifest-swift.txt— matching #912's own precedent for the analogous small Swiftfile (import order, one blank-comment-line-after-summary fix per doc comment, one line wrap).
Verified:
StressSectionBuilderLifecycleTests(7/7), fullOCCTModelingTests(645/645), fullOCCTStressTests(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), andcheck-style-manifest.py --base origin/main(clean except the expected, precedentedOCCTBridge_Modeling.mmexemption above).