From 9239634fdec204c6f7542a71a120bf2c7fa111ec Mon Sep 17 00:00:00 2001 From: holistis Date: Sat, 11 Jul 2026 23:55:07 +0200 Subject: [PATCH 1/5] fix: handle empty or undefined connections arrays without crashing Solvers crashed with TypeError when chips, directConnections, or netConnections were empty or undefined. - Normalize all three arrays to [] in cloneAndCorrectInputProblem (pipeline entry point) - Add ?? [] fallbacks in getConnectivityMapsFromInputProblem (defense-in-depth) - Guard ChipObstacleSpatialIndex against chips.length === 0 (Flatbush requires gt 0 items) - Type spatialIndex as Flatbush | null; guard getChipsInBounds against null index Fixes #657 --- .../ChipObstacleSpatialIndex.ts | 10 ++++- .../getConnectivityMapFromInputProblem.ts | 4 +- .../SchematicTracePipelineSolver.ts | 5 +++ tests/repros/repro-empty-connections.test.ts | 44 +++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/repros/repro-empty-connections.test.ts diff --git a/lib/data-structures/ChipObstacleSpatialIndex.ts b/lib/data-structures/ChipObstacleSpatialIndex.ts index c9132ac3c..2ca35b73d 100644 --- a/lib/data-structures/ChipObstacleSpatialIndex.ts +++ b/lib/data-structures/ChipObstacleSpatialIndex.ts @@ -10,7 +10,7 @@ export interface SpatiallyIndexedChip extends InputChip { export class ChipObstacleSpatialIndex { chips: Array - spatialIndex: Flatbush + spatialIndex: Flatbush | null spatialIndexIdToChip: Map constructor(chips: InputChip[]) { @@ -21,6 +21,13 @@ export class ChipObstacleSpatialIndex { })) this.spatialIndexIdToChip = new Map() + + if (chips.length === 0) { + // Flatbush requires at least 1 item; skip index construction for empty input + this.spatialIndex = null + return + } + this.spatialIndex = new Flatbush(chips.length) for (const chip of this.chips) { @@ -37,6 +44,7 @@ export class ChipObstacleSpatialIndex { } getChipsInBounds(bounds: Bounds): Array { + if (!this.spatialIndex) return [] const chipSpatialIndexIds = this.spatialIndex.search( bounds.minX, bounds.minY, diff --git a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts index d3a098c33..449fb858c 100644 --- a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts +++ b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts @@ -6,7 +6,7 @@ export const getConnectivityMapsFromInputProblem = ( ): { directConnMap: ConnectivityMap; netConnMap: ConnectivityMap } => { const directConnMap = new ConnectivityMap({}) - for (const directConn of inputProblem.directConnections) { + for (const directConn of inputProblem.directConnections ?? []) { directConnMap.addConnections([ directConn.netId ? [directConn.netId, ...directConn.pinIds] @@ -16,7 +16,7 @@ export const getConnectivityMapsFromInputProblem = ( const netConnMap = new ConnectivityMap(directConnMap.netMap) - for (const netConn of inputProblem.netConnections) { + for (const netConn of inputProblem.netConnections ?? []) { netConnMap.addConnections([[netConn.netId, ...netConn.pinIds]]) } diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 766928eb8..0e729dc9c 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -344,6 +344,11 @@ export class SchematicTracePipelineSolver extends BaseSolver { _hideRatsNet: this.hideRatsNet, }) + // Ensure required array fields are present even when caller omits them + cloned.chips ??= [] + cloned.directConnections ??= [] + cloned.netConnections ??= [] + // First, expand chips so existing pin coordinates sit on or within their edges without shrinking. expandChipsToFitPins(cloned) // Then, for any remaining pins that are still inside due to mixed extremes, snap them to the nearest edge. diff --git a/tests/repros/repro-empty-connections.test.ts b/tests/repros/repro-empty-connections.test.ts new file mode 100644 index 000000000..9539021a1 --- /dev/null +++ b/tests/repros/repro-empty-connections.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" + +/** + * Regression test for issue #657: + * Solver crashed with "TypeError: inputProblem.directConnections is not iterable" + * when connections arrays were empty or undefined. + */ + +test("solver handles empty connections without crashing", () => { + const solver = new SchematicTracePipelineSolver({ + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) + +test("solver handles undefined connections without crashing", () => { + const solver = new SchematicTracePipelineSolver({ + chips: [], + directConnections: undefined as any, + netConnections: undefined as any, + availableNetLabelOrientations: {}, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) + +test("solver handles all fields undefined without crashing", () => { + const solver = new SchematicTracePipelineSolver({ + chips: undefined as any, + directConnections: undefined as any, + netConnections: undefined as any, + availableNetLabelOrientations: {}, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) From 2032b48576e3a9fee10f5916ba93e3dcc1cc5109 Mon Sep 17 00:00:00 2001 From: holistis Date: Sun, 12 Jul 2026 00:11:33 +0200 Subject: [PATCH 2/5] fix: guard MspConnectionPairSolver loops and availableNetLabelOrientations Add ?? [] to all four for-loops in MspConnectionPairSolver constructor so the solver is safe when called directly outside the pipeline. Add availableNetLabelOrientations ??= {} in cloneAndCorrectInputProblem to prevent TypeError in label solvers when field is omitted. Extend regression tests: direct MspConnectionPairSolver test and missing availableNetLabelOrientations test (5 tests total, all pass). --- .../MspConnectionPairSolver.ts | 8 +++--- .../SchematicTracePipelineSolver.ts | 3 ++- tests/repros/repro-empty-connections.test.ts | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts index 5aae38063..26d947d35 100644 --- a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +++ b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts @@ -49,27 +49,27 @@ export class MspConnectionPairSolver extends BaseSolver { this.globalConnMap = netConnMap this.pinMap = {} - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { for (const pin of chip.pins) { this.pinMap[pin.pinId] = { ...pin, chipId: chip.chipId } } } this.chipMap = {} - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { this.chipMap[chip.chipId] = chip } // Build a mapping from PinId to user-provided netId (if any) this.userNetIdByPinId = {} - for (const dc of inputProblem.directConnections) { + for (const dc of inputProblem.directConnections ?? []) { if (dc.netId) { const [a, b] = dc.pinIds this.userNetIdByPinId[a] = dc.netId this.userNetIdByPinId[b] = dc.netId } } - for (const nc of inputProblem.netConnections) { + for (const nc of inputProblem.netConnections ?? []) { for (const pid of nc.pinIds) { this.userNetIdByPinId[pid] = nc.netId } diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 0e729dc9c..47848c307 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -344,10 +344,11 @@ export class SchematicTracePipelineSolver extends BaseSolver { _hideRatsNet: this.hideRatsNet, }) - // Ensure required array fields are present even when caller omits them + // Ensure required array/object fields are present even when caller omits them cloned.chips ??= [] cloned.directConnections ??= [] cloned.netConnections ??= [] + cloned.availableNetLabelOrientations ??= {} // First, expand chips so existing pin coordinates sit on or within their edges without shrinking. expandChipsToFitPins(cloned) diff --git a/tests/repros/repro-empty-connections.test.ts b/tests/repros/repro-empty-connections.test.ts index 9539021a1..270cfacb0 100644 --- a/tests/repros/repro-empty-connections.test.ts +++ b/tests/repros/repro-empty-connections.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test" +import { MspConnectionPairSolver } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" /** @@ -42,3 +43,29 @@ test("solver handles all fields undefined without crashing", () => { expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) }) + +test("solver handles missing availableNetLabelOrientations without crashing", () => { + const solver = new SchematicTracePipelineSolver({ + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: undefined as any, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) + +test("MspConnectionPairSolver handles undefined connections directly without crashing", () => { + const solver = new MspConnectionPairSolver({ + inputProblem: { + chips: undefined as any, + directConnections: undefined as any, + netConnections: undefined as any, + availableNetLabelOrientations: {}, + }, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) From 4aeec0901c5def9bbac61b5d778f374416676ef0 Mon Sep 17 00:00:00 2001 From: holistis Date: Sun, 12 Jul 2026 00:32:13 +0200 Subject: [PATCH 3/5] fix: make chips/directConnections/netConnections optional in InputProblem Make chips, directConnections, netConnections, and availableNetLabelOrientations optional in the InputProblem interface so callers that omit them get a compile- time safe type, not a runtime crash. Add ?? [] / ?? {} fallback guards in every sub-solver that iterates or indexes these fields directly (AvailableNetOrientationSolver, NetLabelPlacementSolver, SchematicTraceSingleLineSolver, SchematicTraceSingleLineSolver2, visualizeInputProblem, TraceAnchoredNetLabelOverlapSolver, candidates, GuidelinesSolver, correctPinsInsideChip, expandChipsToFitPins, and more). Add tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts covering empty arrays and undefined (cast as any) for both connections fields. All 109 tests pass; bunx tsc --noEmit reports 0 errors. Co-Authored-By: Claude Sonnet 4.6 --- .../AvailableNetOrientationSolver.ts | 18 +++++------ .../AvailableNetOrientationSolver/geometry.ts | 2 +- .../AvailableNetOrientationSolver/traces.ts | 2 +- .../Example28Solver/Example28Solver.ts | 2 +- .../GuidelinesSolver/GuidelinesSolver.ts | 2 +- .../GuidelinesSolver/getInputProblemBounds.ts | 2 +- .../LongDistancePairSolver.ts | 2 +- .../NetLabelNetLabelCollisionSolver.ts | 2 +- .../NetLabelPlacementSolver.ts | 24 +++++++------- .../SingleNetLabelPlacementSolver.ts | 2 +- .../SingleNetLabelPlacementSolver/host.ts | 2 +- .../solvePortOnlyPin.ts | 2 +- .../RailNetLabelCornerPlacementSolver.ts | 2 +- .../SchematicTraceSingleLineSolver.ts | 4 +-- .../SchematicTraceSingleLineSolver2.ts | 16 +++++----- .../SchematicTraceSingleLineSolver2/rect.ts | 2 +- .../colorAvailableNetOrientationLabels.ts | 1 + .../correctPinsInsideChip.ts | 2 +- .../expandChipsToFitPins.ts | 2 +- .../visualizeInputProblem.ts | 8 ++--- .../TraceAnchoredNetLabelOverlapSolver.ts | 2 +- .../candidates.ts | 14 ++++---- .../sub-solver/UntangleTraceSubsolver.ts | 2 +- .../LabelMergingSolver/LabelMergingSolver.ts | 2 +- lib/types/InputProblem.ts | 8 ++--- .../arePinsInDifferentSchematicSections.ts | 2 +- lib/utils/getOrientationConstraintKeys.ts | 2 +- ...-variant-resistor-facing-direction.test.ts | 2 +- .../empty-connections.test.ts | 32 +++++++++++++++++++ 29 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts diff --git a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts index 3017d184d..30ae7acc5 100644 --- a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +++ b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts @@ -68,7 +68,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { this.pinMap = getPinMap(params.inputProblem) this.chipObstacleSpatialIndex = params.inputProblem._chipObstacleSpatialIndex ?? - new ChipObstacleSpatialIndex(params.inputProblem.chips) + new ChipObstacleSpatialIndex(params.inputProblem.chips ?? []) this.maxSearchDistance = getMaxSearchDistance(params.inputProblem) this.queuedLabelIndices = this.getProcessableLabelIndices() this.setCurrentLabel(this.queuedLabelIndices[0] ?? null) @@ -290,7 +290,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { private getAvailableOrientations(label: NetLabelPlacement) { const effectiveNetId = label.netId ?? label.globalConnNetId - return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? [] + return (this.inputProblem.availableNetLabelOrientations ?? {})[effectiveNetId] ?? [] } private findValidShiftedCandidate( @@ -627,36 +627,36 @@ export class AvailableNetOrientationSolver extends BaseSolver { private getNetLabelWidth(label: NetLabelPlacement) { if (label.netId) { - const ncWidth = this.inputProblem.netConnections.find( + const ncWidth = (this.inputProblem.netConnections ?? []).find( (connection) => connection.netId === label.netId, )?.netLabelWidth if (ncWidth !== undefined) return ncWidth - const dcWidthByNetId = this.inputProblem.directConnections.find( + const dcWidthByNetId = (this.inputProblem.directConnections ?? []).find( (dc) => dc.netId === label.netId, )?.netLabelWidth if (dcWidthByNetId !== undefined) return dcWidthByNetId } - const dcWidthByPinId = this.inputProblem.directConnections.find((dc) => - dc.pinIds.some((pid) => label.pinIds.includes(pid)), + const dcWidthByPinId = (this.inputProblem.directConnections ?? []).find( + (dc) => dc.pinIds.some((pid) => label.pinIds.includes(pid)), )?.netLabelWidth if (dcWidthByPinId !== undefined) return dcWidthByPinId - return this.inputProblem.netConnections.find((nc) => + return (this.inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid)), )?.netLabelWidth } private getNetLabelHeight(label: NetLabelPlacement) { if (label.netId) { - const ncHeight = this.inputProblem.netConnections.find( + const ncHeight = (this.inputProblem.netConnections ?? []).find( (connection) => connection.netId === label.netId, )?.netLabelHeight if (ncHeight !== undefined) return ncHeight } - return this.inputProblem.netConnections.find((nc) => + return (this.inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid)), )?.netLabelHeight } diff --git a/lib/solvers/AvailableNetOrientationSolver/geometry.ts b/lib/solvers/AvailableNetOrientationSolver/geometry.ts index 3b6370d7a..d5f2b422b 100644 --- a/lib/solvers/AvailableNetOrientationSolver/geometry.ts +++ b/lib/solvers/AvailableNetOrientationSolver/geometry.ts @@ -212,7 +212,7 @@ const sameY = (a: Point, b: Point) => Math.abs(a.y - b.y) <= EPS export const getMaxSearchDistance = (inputProblem: InputProblem) => { const maxChipWidth = Math.max( - ...inputProblem.chips.map((chip) => chip.width), + ...(inputProblem.chips ?? []).map((chip) => chip.width), 1, ) return maxChipWidth * 3 diff --git a/lib/solvers/AvailableNetOrientationSolver/traces.ts b/lib/solvers/AvailableNetOrientationSolver/traces.ts index 0a45de36b..9f3babd5b 100644 --- a/lib/solvers/AvailableNetOrientationSolver/traces.ts +++ b/lib/solvers/AvailableNetOrientationSolver/traces.ts @@ -5,7 +5,7 @@ import type { CandidateLabel } from "./types" export const getPinMap = (inputProblem: InputProblem) => { const pinMap: Record = {} - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { for (const pin of chip.pins) { pinMap[pin.pinId] = { ...pin, chipId: chip.chipId } } diff --git a/lib/solvers/Example28Solver/Example28Solver.ts b/lib/solvers/Example28Solver/Example28Solver.ts index 532a97997..08455cb01 100644 --- a/lib/solvers/Example28Solver/Example28Solver.ts +++ b/lib/solvers/Example28Solver/Example28Solver.ts @@ -139,7 +139,7 @@ export class Example28Solver extends BaseSolver { private hasExplicitOrientationConstraint(label: NetLabelPlacement) { const effectiveNetId = label.netId ?? label.globalConnNetId return Object.hasOwn( - this.inputProblem.availableNetLabelOrientations, + this.inputProblem.availableNetLabelOrientations ?? {}, effectiveNetId, ) } diff --git a/lib/solvers/GuidelinesSolver/GuidelinesSolver.ts b/lib/solvers/GuidelinesSolver/GuidelinesSolver.ts index 37177f1f2..1846849ec 100644 --- a/lib/solvers/GuidelinesSolver/GuidelinesSolver.ts +++ b/lib/solvers/GuidelinesSolver/GuidelinesSolver.ts @@ -60,7 +60,7 @@ export class GuidelinesSolver extends BaseSolver { }, ] this.chipPairsGenerator = getGeneratorForAllChipPairs( - this.inputProblem.chips, + this.inputProblem.chips ?? [], ) this.usedXGuidelines = new Set() diff --git a/lib/solvers/GuidelinesSolver/getInputProblemBounds.ts b/lib/solvers/GuidelinesSolver/getInputProblemBounds.ts index 97bf3e7f3..6ba71a3ef 100644 --- a/lib/solvers/GuidelinesSolver/getInputProblemBounds.ts +++ b/lib/solvers/GuidelinesSolver/getInputProblemBounds.ts @@ -8,7 +8,7 @@ export const getInputProblemBounds = (inputProblem: InputProblem) => { minY: Infinity, maxY: -Infinity, } - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { const chipBounds = getInputChipBounds(chip) bounds.minX = Math.min(bounds.minX, chipBounds.minX) bounds.maxX = Math.max(bounds.maxX, chipBounds.maxX) diff --git a/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts b/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts index 9911d3949..a1f6acdb4 100644 --- a/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +++ b/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts @@ -60,7 +60,7 @@ export class LongDistancePairSolver extends BaseSolver { const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem) this.netConnMap = netConnMap const pinMap = new Map() - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { this.chipMap[chip.chipId] = chip for (const pin of chip.pins) { pinMap.set(pin.pinId, { ...pin, chipId: chip.chipId }) diff --git a/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts b/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts index e0a3daf36..a174c3aa8 100644 --- a/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +++ b/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts @@ -123,7 +123,7 @@ export class NetLabelNetLabelCollisionSolver extends BaseSolver { this.outputNetLabelPlacements = [...params.netLabelPlacements] this.chipIndex = params.inputProblem._chipObstacleSpatialIndex ?? - new ChipObstacleSpatialIndex(params.inputProblem.chips) + new ChipObstacleSpatialIndex(params.inputProblem.chips ?? []) this.traceMap = Object.fromEntries( params.traces.map((t) => [t.mspPairId, t]), ) as Record diff --git a/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts b/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts index 8a76c4606..5c0d98c9a 100644 --- a/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +++ b/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts @@ -109,7 +109,7 @@ export class NetLabelPlacementSolver extends BaseSolver { ) const pinIdToPinMap = new Map() - for (const chip of this.inputProblem.chips) { + for (const chip of this.inputProblem.chips ?? []) { for (const pin of chip.pins) { pinIdToPinMap.set(pin.pinId, pin) } @@ -117,14 +117,14 @@ export class NetLabelPlacementSolver extends BaseSolver { // Map pins to user-provided netIds (if any) const userNetIdByPinId: Record = {} - for (const dc of this.inputProblem.directConnections) { + for (const dc of this.inputProblem.directConnections ?? []) { if (dc.netId) { const [a, b] = dc.pinIds userNetIdByPinId[a] = dc.netId userNetIdByPinId[b] = dc.netId } } - for (const nc of this.inputProblem.netConnections) { + for (const nc of this.inputProblem.netConnections ?? []) { for (const pid of nc.pinIds) { userNetIdByPinId[pid] = nc.netId } @@ -132,7 +132,7 @@ export class NetLabelPlacementSolver extends BaseSolver { const groups: Array = [] - const allPinIds = this.inputProblem.chips.flatMap((c) => + const allPinIds = (this.inputProblem.chips ?? []).flatMap((c) => c.pins.map((p) => p.pinId), ) @@ -255,12 +255,12 @@ export class NetLabelPlacementSolver extends BaseSolver { group: OverlappingSameNetTraceGroup, ): number | undefined { if (group.netId) { - const ncWidth = this.inputProblem.netConnections.find( + const ncWidth = (this.inputProblem.netConnections ?? []).find( (nc) => nc.netId === group.netId, )?.netLabelWidth if (ncWidth !== undefined) return ncWidth - const dcWidthByNetId = this.inputProblem.directConnections.find( + const dcWidthByNetId = (this.inputProblem.directConnections ?? []).find( (dc) => dc.netId === group.netId, )?.netLabelWidth if (dcWidthByNetId !== undefined) return dcWidthByNetId @@ -271,12 +271,12 @@ export class NetLabelPlacementSolver extends BaseSolver { pinIds.push(group.portOnlyPinId) } - const dcWidthByPinId = this.inputProblem.directConnections.find((dc) => - dc.pinIds.some((pid) => pinIds.includes(pid)), + const dcWidthByPinId = (this.inputProblem.directConnections ?? []).find( + (dc) => dc.pinIds.some((pid) => pinIds.includes(pid)), )?.netLabelWidth if (dcWidthByPinId !== undefined) return dcWidthByPinId - return this.inputProblem.netConnections.find((nc) => + return (this.inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => pinIds.includes(pid)), )?.netLabelWidth } @@ -285,7 +285,7 @@ export class NetLabelPlacementSolver extends BaseSolver { group: OverlappingSameNetTraceGroup, ): number | undefined { if (group.netId) { - const ncHeight = this.inputProblem.netConnections.find( + const ncHeight = (this.inputProblem.netConnections ?? []).find( (nc) => nc.netId === group.netId, )?.netLabelHeight if (ncHeight !== undefined) return ncHeight @@ -296,7 +296,7 @@ export class NetLabelPlacementSolver extends BaseSolver { pinIds.push(group.portOnlyPinId) } - return this.inputProblem.netConnections.find((nc) => + return (this.inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => pinIds.includes(pid)), )?.netLabelHeight } @@ -374,7 +374,7 @@ export class NetLabelPlacementSolver extends BaseSolver { inputProblem: this.inputProblem, inputTraceMap: this.inputTraceMap, overlappingSameNetTraceGroup: nextOverlappingSameNetTraceGroup, - availableOrientations: this.inputProblem.availableNetLabelOrientations[ + availableOrientations: (this.inputProblem.availableNetLabelOrientations ?? {})[ netId ] ?? ["x+", "x-", "y+", "y-"], netLabelWidth, diff --git a/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts b/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts index 74c5bba17..0e9edb906 100644 --- a/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +++ b/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts @@ -98,7 +98,7 @@ export class SingleNetLabelPlacementSolver extends BaseSolver { this.chipObstacleSpatialIndex = params.inputProblem._chipObstacleSpatialIndex ?? - new ChipObstacleSpatialIndex(params.inputProblem.chips) + new ChipObstacleSpatialIndex(params.inputProblem.chips ?? []) } override getConstructorParams(): ConstructorParameters< diff --git a/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts b/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts index eb980fd20..a0a8ed3ee 100644 --- a/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts +++ b/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/host.ts @@ -27,7 +27,7 @@ export function chooseHostTraceForGroup(params: { mspConnectionPairIds, } = params const chipsById: Record = Object.fromEntries( - inputProblem.chips.map((c) => [c.chipId, c]), + (inputProblem.chips ?? []).map((c) => [c.chipId, c]), ) let groupTraces = Object.values(inputTraceMap).filter( diff --git a/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts b/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts index 4e40be9e3..e19778462 100644 --- a/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +++ b/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts @@ -65,7 +65,7 @@ export function solveNetLabelPlacementForPortOnlyPin(params: { // Find pin coordinates and facing direction let pin: { x: number; y: number } | null = null let pinFacingDirection: FacingDirection | null = null - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { const p = chip.pins.find((pp) => pp.pinId === pinId) if (p) { pin = { x: p.x, y: p.y } diff --git a/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts b/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts index 28b9339c3..fcfaabe26 100644 --- a/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +++ b/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts @@ -209,7 +209,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver { } private intersectsAnyChip(bounds: Bounds) { - return this.inputProblem.chips.some((chip) => + return (this.inputProblem.chips ?? []).some((chip) => rectsOverlap(bounds, getRectBounds(chip.center, chip.width, chip.height)), ) } diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts index 305e4e060..f44c11eb0 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/SchematicTraceSingleLineSolver.ts @@ -56,7 +56,7 @@ export class SchematicTraceSingleLineSolver extends BaseSolver { this.chipMap = params.chipMap this.chipObstacleSpatialIndex = this.inputProblem._chipObstacleSpatialIndex || - new ChipObstacleSpatialIndex(this.inputProblem.chips) + new ChipObstacleSpatialIndex(this.inputProblem.chips ?? []) if (!this.inputProblem._chipObstacleSpatialIndex) { this.inputProblem._chipObstacleSpatialIndex = @@ -64,7 +64,7 @@ export class SchematicTraceSingleLineSolver extends BaseSolver { } // Build a lookup of all pins by id and attach chipId to each pin entry - for (const chip of this.inputProblem.chips) { + for (const chip of this.inputProblem.chips ?? []) { for (const pin of chip.pins) { this.pinIdMap.set(pin.pinId, { ...pin, chipId: chip.chipId }) } diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts index e5cad258c..e4b5c444d 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts @@ -131,7 +131,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver { if (!netId) return {} const orientations = - this.inputProblem.availableNetLabelOrientations[netId] ?? + (this.inputProblem.availableNetLabelOrientations ?? {})[netId] ?? (["x+", "x-", "y+", "y-"] as FacingDirection[]) const netLabelWidth = this.getNetLabelWidthForConnectionPair(netId) const netLabelHeight = this.getNetLabelHeightForConnectionPair(netId) @@ -172,35 +172,35 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver { } private getNetLabelWidthForConnectionPair(netId: string) { - const ncWidth = this.inputProblem.netConnections.find( + const ncWidth = (this.inputProblem.netConnections ?? []).find( (nc) => nc.netId === netId, )?.netLabelWidth if (ncWidth !== undefined) return ncWidth - const dcWidthByNetId = this.inputProblem.directConnections.find( + const dcWidthByNetId = (this.inputProblem.directConnections ?? []).find( (dc) => dc.netId === netId, )?.netLabelWidth if (dcWidthByNetId !== undefined) return dcWidthByNetId const pinIds = this.pins.map((p) => p.pinId) - const dcWidthByPinId = this.inputProblem.directConnections.find((dc) => - dc.pinIds.some((pid) => pinIds.includes(pid)), + const dcWidthByPinId = (this.inputProblem.directConnections ?? []).find( + (dc) => dc.pinIds.some((pid) => pinIds.includes(pid)), )?.netLabelWidth if (dcWidthByPinId !== undefined) return dcWidthByPinId - return this.inputProblem.netConnections.find((nc) => + return (this.inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => pinIds.includes(pid)), )?.netLabelWidth } private getNetLabelHeightForConnectionPair(netId: string) { - const ncHeight = this.inputProblem.netConnections.find( + const ncHeight = (this.inputProblem.netConnections ?? []).find( (nc) => nc.netId === netId, )?.netLabelHeight if (ncHeight !== undefined) return ncHeight const pinIds = this.pins.map((p) => p.pinId) - return this.inputProblem.netConnections.find((nc) => + return (this.inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => pinIds.includes(pid)), )?.netLabelHeight } diff --git a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts index d21297753..9cc94ff87 100644 --- a/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +++ b/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts @@ -30,7 +30,7 @@ export const getObstacleRects = ( problem: InputProblem, opts: { textBoxPadding?: RectPadding } = {}, ): ObstacleRect[] => { - const chipRects = problem.chips.map(chipToRect) + const chipRects = (problem.chips ?? []).map(chipToRect) const textBoxRects = (problem.textBoxes ?? []).map((textBox) => { const b = getTextBoxBounds(textBox, opts.textBoxPadding) return { diff --git a/lib/solvers/SchematicTracePipelineSolver/colorAvailableNetOrientationLabels.ts b/lib/solvers/SchematicTracePipelineSolver/colorAvailableNetOrientationLabels.ts index 177bc3a59..7d189f556 100644 --- a/lib/solvers/SchematicTracePipelineSolver/colorAvailableNetOrientationLabels.ts +++ b/lib/solvers/SchematicTracePipelineSolver/colorAvailableNetOrientationLabels.ts @@ -35,6 +35,7 @@ const getAvailableOrientationsForRect = ( label: string | undefined, availableOrientations: InputProblem["availableNetLabelOrientations"], ) => { + if (!availableOrientations) return undefined for (const netId of getNetIdsFromRectLabel(label)) { if (Object.hasOwn(availableOrientations, netId)) { return availableOrientations[netId] diff --git a/lib/solvers/SchematicTracePipelineSolver/correctPinsInsideChip.ts b/lib/solvers/SchematicTracePipelineSolver/correctPinsInsideChip.ts index fd736adbe..4c9a18c99 100644 --- a/lib/solvers/SchematicTracePipelineSolver/correctPinsInsideChip.ts +++ b/lib/solvers/SchematicTracePipelineSolver/correctPinsInsideChip.ts @@ -2,7 +2,7 @@ import type { InputProblem } from "lib/types/InputProblem" import { getInputChipBounds } from "../GuidelinesSolver/getInputChipBounds" export const correctPinsInsideChips = (problem: InputProblem) => { - for (const chip of problem.chips) { + for (const chip of problem.chips ?? []) { const bounds = getInputChipBounds(chip) for (const pin of chip.pins) { const isInside = diff --git a/lib/solvers/SchematicTracePipelineSolver/expandChipsToFitPins.ts b/lib/solvers/SchematicTracePipelineSolver/expandChipsToFitPins.ts index 6d5a7ba26..d2f50ab4d 100644 --- a/lib/solvers/SchematicTracePipelineSolver/expandChipsToFitPins.ts +++ b/lib/solvers/SchematicTracePipelineSolver/expandChipsToFitPins.ts @@ -11,7 +11,7 @@ import type { InputProblem } from "lib/types/InputProblem" * - Clears cached _facingDirection on pins since geometry changed. */ export const expandChipsToFitPins = (problem: InputProblem) => { - for (const chip of problem.chips) { + for (const chip of problem.chips ?? []) { const halfWidth = chip.width / 2 const halfHeight = chip.height / 2 diff --git a/lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts b/lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts index 99cd3f655..8dcbb5d59 100644 --- a/lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts +++ b/lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem.ts @@ -27,13 +27,13 @@ export const visualizeInputProblem = ( } const pinIdMap = new Map() - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { for (const pin of chip.pins) { pinIdMap.set(pin.pinId, pin) } } - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { graphics.rects.push({ label: chip.chipId, center: chip.center, @@ -64,7 +64,7 @@ export const visualizeInputProblem = ( } if (!hideRatsNet) { - for (const directConn of inputProblem.directConnections) { + for (const directConn of inputProblem.directConnections ?? []) { const [pinId1, pinId2] = directConn.pinIds const pin1 = pinIdMap.get(pinId1)! const pin2 = pinIdMap.get(pinId2)! @@ -89,7 +89,7 @@ export const visualizeInputProblem = ( }) } - for (const netConn of inputProblem.netConnections) { + for (const netConn of inputProblem.netConnections ?? []) { const pins = netConn.pinIds.map((pinId) => pinIdMap.get(pinId)!) for (let i = 0; i < pins.length - 1; i++) { for (let j = i + 1; j < pins.length; j++) { diff --git a/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts b/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts index 9f126dbcf..450793abe 100644 --- a/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +++ b/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts @@ -255,7 +255,7 @@ export class TraceAnchoredNetLabelOverlapSolver extends BaseSolver { } private intersectsAnyChip(bounds: Bounds) { - return this.inputProblem.chips.some((chip) => + return (this.inputProblem.chips ?? []).some((chip) => rectsTouchOrOverlap( bounds, getRectBounds(chip.center, chip.width, chip.height), diff --git a/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts b/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts index 0e40a7f69..3c98d5405 100644 --- a/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts +++ b/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts @@ -273,7 +273,7 @@ type ChipBounds = Bounds & { } const getChipBounds = (inputProblem: InputProblem): ChipBounds => { - if (inputProblem.chips.length === 0) { + if ((inputProblem.chips ?? []).length === 0) { return { minX: 0, minY: 0, @@ -284,7 +284,7 @@ const getChipBounds = (inputProblem: InputProblem): ChipBounds => { } } - const bounds = inputProblem.chips.reduce( + const bounds = (inputProblem.chips ?? []).reduce( (acc, chip) => ({ minX: Math.min(acc.minX, chip.center.x - chip.width / 2), minY: Math.min(acc.minY, chip.center.y - chip.height / 2), @@ -310,17 +310,17 @@ const getNetLabelWidth = ( inputProblem: InputProblem, label: NetLabelPlacement, ) => { - const ncWidthByNetId = inputProblem.netConnections.find( + const ncWidthByNetId = (inputProblem.netConnections ?? []).find( (connection) => connection.netId === label.netId, )?.netLabelWidth if (ncWidthByNetId !== undefined) return ncWidthByNetId - const dcWidth = inputProblem.directConnections.find((dc) => + const dcWidth = (inputProblem.directConnections ?? []).find((dc) => dc.pinIds.some((pid) => label.pinIds.includes(pid)), )?.netLabelWidth if (dcWidth !== undefined) return dcWidth - const ncWidthByPinId = inputProblem.netConnections.find((nc) => + const ncWidthByPinId = (inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid)), )?.netLabelWidth if (ncWidthByPinId !== undefined) return ncWidthByPinId @@ -336,12 +336,12 @@ const getNetLabelHeight = ( inputProblem: InputProblem, label: NetLabelPlacement, ): number | undefined => { - const ncHeightByNetId = inputProblem.netConnections.find( + const ncHeightByNetId = (inputProblem.netConnections ?? []).find( (connection) => connection.netId === label.netId, )?.netLabelHeight if (ncHeightByNetId !== undefined) return ncHeightByNetId - return inputProblem.netConnections.find((nc) => + return (inputProblem.netConnections ?? []).find((nc) => nc.pinIds.some((pid) => label.pinIds.includes(pid)), )?.netLabelHeight } diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts index ee541e67b..f2813a217 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts @@ -90,7 +90,7 @@ export class UntangleTraceSubsolver extends BaseSolver { this.chipObstacleSpatialIndex = this.input.inputProblem._chipObstacleSpatialIndex ?? - new ChipObstacleSpatialIndex(this.input.inputProblem.chips) + new ChipObstacleSpatialIndex(this.input.inputProblem.chips ?? []) if (!this.input.inputProblem._chipObstacleSpatialIndex) { this.input.inputProblem._chipObstacleSpatialIndex = this.chipObstacleSpatialIndex diff --git a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts index ff777b421..259072d9e 100644 --- a/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +++ b/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts @@ -71,7 +71,7 @@ export class MergedNetLabelObstacleSolver extends BaseSolver { case "grouping_labels": this.labelGroups = groupLabelsByChipAndOrientation({ labels: this.filteredLabels, - chips: this.inputProblem.chips, + chips: this.inputProblem.chips ?? [], }) this.groupKeysToProcess = Object.keys(this.labelGroups) this.pipelineStep = "merging_groups" diff --git a/lib/types/InputProblem.ts b/lib/types/InputProblem.ts index fd9b2e90a..d419d3c20 100644 --- a/lib/types/InputProblem.ts +++ b/lib/types/InputProblem.ts @@ -44,12 +44,12 @@ export interface InputNetConnection { } export interface InputProblem { - chips: Array - directConnections: Array - netConnections: Array + chips?: Array + directConnections?: Array + netConnections?: Array textBoxes?: Array - availableNetLabelOrientations: Record + availableNetLabelOrientations?: Record maxMspPairDistance?: number _chipObstacleSpatialIndex?: ChipObstacleSpatialIndex diff --git a/lib/utils/arePinsInDifferentSchematicSections.ts b/lib/utils/arePinsInDifferentSchematicSections.ts index 16db4eca8..f530af3e4 100644 --- a/lib/utils/arePinsInDifferentSchematicSections.ts +++ b/lib/utils/arePinsInDifferentSchematicSections.ts @@ -21,7 +21,7 @@ export const arePinsInDifferentSchematicSections = ( const sectionByChipId = new Map() const sectionByPinId = new Map() - for (const chip of inputProblem.chips) { + for (const chip of inputProblem.chips ?? []) { if (!chip.sectionId) continue sectionByChipId.set(chip.chipId, chip.sectionId) diff --git a/lib/utils/getOrientationConstraintKeys.ts b/lib/utils/getOrientationConstraintKeys.ts index 36352d4cd..390692e8a 100644 --- a/lib/utils/getOrientationConstraintKeys.ts +++ b/lib/utils/getOrientationConstraintKeys.ts @@ -9,7 +9,7 @@ export const getOrientationConstraintKeys = ( dedupeStrings([ label.netId, label.globalConnNetId, - ...inputProblem.netConnections + ...(inputProblem.netConnections ?? []) .filter((connection) => label.pinIds.some((pinId) => connection.pinIds.includes(pinId)), ) diff --git a/tests/repros/small-variant-resistor-facing-direction.test.ts b/tests/repros/small-variant-resistor-facing-direction.test.ts index afb974e8d..6d5b6e9b0 100644 --- a/tests/repros/small-variant-resistor-facing-direction.test.ts +++ b/tests/repros/small-variant-resistor-facing-direction.test.ts @@ -17,7 +17,7 @@ test("small-variant resistor with facing directions routes from the sides", () = solver.solve() - const resistor = solver.inputProblem.chips.find( + const resistor = (solver.inputProblem.chips ?? []).find( (c) => c.chipId === "schematic_component_1", )! // Terminals keep their symbol-provided facing instead of being re-detected as diff --git a/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts b/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts new file mode 100644 index 000000000..a4b780a93 --- /dev/null +++ b/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" + +/** + * Regression tests for issue #657: + * Solver crashed with "TypeError: inputProblem.directConnections is not iterable" + * when directConnections or netConnections were undefined or empty. + */ + +test("pipeline solver handles empty directConnections and netConnections", () => { + const solver = new SchematicTracePipelineSolver({ + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) + +test("pipeline solver handles undefined directConnections and netConnections", () => { + const solver = new SchematicTracePipelineSolver({ + chips: [], + directConnections: undefined as any, + netConnections: undefined as any, + availableNetLabelOrientations: {}, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) +}) From adb7f5d19f035c9adf5d2ac0bd398b2d486c28e3 Mon Sep 17 00:00:00 2001 From: holistis Date: Sun, 12 Jul 2026 01:27:16 +0200 Subject: [PATCH 4/5] fix: more robust Array.isArray guards + filter(Boolean) for empty connections --- .../MspConnectionPairSolver.ts | 2 + .../getConnectivityMapFromInputProblem.ts | 2 + .../SchematicTracePipelineSolver.ts | 22 ++++++++--- tests/repros/repro-empty-connections.test.ts | 38 ++++++++++++++----- .../empty-connections.test.ts | 6 ++- 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts index 26d947d35..a9bb811e6 100644 --- a/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +++ b/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts @@ -63,6 +63,7 @@ export class MspConnectionPairSolver extends BaseSolver { // Build a mapping from PinId to user-provided netId (if any) this.userNetIdByPinId = {} for (const dc of inputProblem.directConnections ?? []) { + if (!dc) continue if (dc.netId) { const [a, b] = dc.pinIds this.userNetIdByPinId[a] = dc.netId @@ -70,6 +71,7 @@ export class MspConnectionPairSolver extends BaseSolver { } } for (const nc of inputProblem.netConnections ?? []) { + if (!nc) continue for (const pid of nc.pinIds) { this.userNetIdByPinId[pid] = nc.netId } diff --git a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts index 449fb858c..9610c321f 100644 --- a/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts +++ b/lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem.ts @@ -7,6 +7,7 @@ export const getConnectivityMapsFromInputProblem = ( const directConnMap = new ConnectivityMap({}) for (const directConn of inputProblem.directConnections ?? []) { + if (!directConn) continue directConnMap.addConnections([ directConn.netId ? [directConn.netId, ...directConn.pinIds] @@ -17,6 +18,7 @@ export const getConnectivityMapsFromInputProblem = ( const netConnMap = new ConnectivityMap(directConnMap.netMap) for (const netConn of inputProblem.netConnections ?? []) { + if (!netConn) continue netConnMap.addConnections([[netConn.netId, ...netConn.pinIds]]) } diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 47848c307..fd99d891d 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -344,11 +344,23 @@ export class SchematicTracePipelineSolver extends BaseSolver { _hideRatsNet: this.hideRatsNet, }) - // Ensure required array/object fields are present even when caller omits them - cloned.chips ??= [] - cloned.directConnections ??= [] - cloned.netConnections ??= [] - cloned.availableNetLabelOrientations ??= {} + // Ensure required array/object fields are present even when caller passes + // a non-array (e.g. null, a number) instead of an array/object. + // Also filter out any null/undefined elements within the arrays so + // downstream solvers never encounter sparse/corrupted entries. + cloned.chips = Array.isArray(cloned.chips) ? cloned.chips.filter(Boolean) : [] + cloned.directConnections = Array.isArray(cloned.directConnections) + ? cloned.directConnections.filter(Boolean) + : [] + cloned.netConnections = Array.isArray(cloned.netConnections) + ? cloned.netConnections.filter(Boolean) + : [] + cloned.availableNetLabelOrientations = + cloned.availableNetLabelOrientations != null && + typeof cloned.availableNetLabelOrientations === "object" && + !Array.isArray(cloned.availableNetLabelOrientations) + ? cloned.availableNetLabelOrientations + : {} // First, expand chips so existing pin coordinates sit on or within their edges without shrinking. expandChipsToFitPins(cloned) diff --git a/tests/repros/repro-empty-connections.test.ts b/tests/repros/repro-empty-connections.test.ts index 270cfacb0..8799d834f 100644 --- a/tests/repros/repro-empty-connections.test.ts +++ b/tests/repros/repro-empty-connections.test.ts @@ -18,30 +18,33 @@ test("solver handles empty connections without crashing", () => { solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) test("solver handles undefined connections without crashing", () => { const solver = new SchematicTracePipelineSolver({ chips: [], - directConnections: undefined as any, - netConnections: undefined as any, + directConnections: undefined, + netConnections: undefined, availableNetLabelOrientations: {}, }) solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) test("solver handles all fields undefined without crashing", () => { const solver = new SchematicTracePipelineSolver({ - chips: undefined as any, - directConnections: undefined as any, - netConnections: undefined as any, + chips: undefined, + directConnections: undefined, + netConnections: undefined, availableNetLabelOrientations: {}, }) solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) test("solver handles missing availableNetLabelOrientations without crashing", () => { @@ -49,23 +52,40 @@ test("solver handles missing availableNetLabelOrientations without crashing", () chips: [], directConnections: [], netConnections: [], - availableNetLabelOrientations: undefined as any, + availableNetLabelOrientations: undefined, }) solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) test("MspConnectionPairSolver handles undefined connections directly without crashing", () => { const solver = new MspConnectionPairSolver({ inputProblem: { - chips: undefined as any, - directConnections: undefined as any, - netConnections: undefined as any, + chips: undefined, + directConnections: undefined, + netConnections: undefined, availableNetLabelOrientations: {}, }, }) solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.mspConnectionPairs)).toBe(true) +}) + +test("solver handles directConnections array with undefined elements without crashing", () => { + // Intentional as any: we deliberately pass a sparse/corrupted array to prove + // the element-level null guards (Fix E) protect against individual null entries. + const solver = new SchematicTracePipelineSolver({ + chips: [], + directConnections: [undefined] as any, + netConnections: [], + availableNetLabelOrientations: {}, + }) + solver.solve() + expect(solver.solved).toBe(true) + expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) diff --git a/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts b/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts index a4b780a93..69e3930c4 100644 --- a/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts +++ b/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts @@ -17,16 +17,18 @@ test("pipeline solver handles empty directConnections and netConnections", () => solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) test("pipeline solver handles undefined directConnections and netConnections", () => { const solver = new SchematicTracePipelineSolver({ chips: [], - directConnections: undefined as any, - netConnections: undefined as any, + directConnections: undefined, + netConnections: undefined, availableNetLabelOrientations: {}, }) solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) + expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) }) From ac338b46a33b483e23d5ff0194da23271d3d42cb Mon Sep 17 00:00:00 2001 From: holistis Date: Sun, 12 Jul 2026 01:46:39 +0200 Subject: [PATCH 5/5] style: biome format fix --- .../AvailableNetOrientationSolver.ts | 5 ++++- .../NetLabelPlacementSolver.ts | 5 ++--- .../SchematicTracePipelineSolver.ts | 4 +++- tests/repros/repro-empty-connections.test.ts | 20 ++++++++++++++----- .../empty-connections.test.ts | 8 ++++++-- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts index 30ae7acc5..8b6d18263 100644 --- a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +++ b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts @@ -290,7 +290,10 @@ export class AvailableNetOrientationSolver extends BaseSolver { private getAvailableOrientations(label: NetLabelPlacement) { const effectiveNetId = label.netId ?? label.globalConnNetId - return (this.inputProblem.availableNetLabelOrientations ?? {})[effectiveNetId] ?? [] + return ( + (this.inputProblem.availableNetLabelOrientations ?? {})[effectiveNetId] ?? + [] + ) } private findValidShiftedCandidate( diff --git a/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts b/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts index 5c0d98c9a..533e0e254 100644 --- a/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +++ b/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts @@ -374,9 +374,8 @@ export class NetLabelPlacementSolver extends BaseSolver { inputProblem: this.inputProblem, inputTraceMap: this.inputTraceMap, overlappingSameNetTraceGroup: nextOverlappingSameNetTraceGroup, - availableOrientations: (this.inputProblem.availableNetLabelOrientations ?? {})[ - netId - ] ?? ["x+", "x-", "y+", "y-"], + availableOrientations: (this.inputProblem.availableNetLabelOrientations ?? + {})[netId] ?? ["x+", "x-", "y+", "y-"], netLabelWidth, netLabelHeight, }) diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index fd99d891d..c31518805 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -348,7 +348,9 @@ export class SchematicTracePipelineSolver extends BaseSolver { // a non-array (e.g. null, a number) instead of an array/object. // Also filter out any null/undefined elements within the arrays so // downstream solvers never encounter sparse/corrupted entries. - cloned.chips = Array.isArray(cloned.chips) ? cloned.chips.filter(Boolean) : [] + cloned.chips = Array.isArray(cloned.chips) + ? cloned.chips.filter(Boolean) + : [] cloned.directConnections = Array.isArray(cloned.directConnections) ? cloned.directConnections.filter(Boolean) : [] diff --git a/tests/repros/repro-empty-connections.test.ts b/tests/repros/repro-empty-connections.test.ts index 8799d834f..964519230 100644 --- a/tests/repros/repro-empty-connections.test.ts +++ b/tests/repros/repro-empty-connections.test.ts @@ -18,7 +18,9 @@ test("solver handles empty connections without crashing", () => { solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) }) test("solver handles undefined connections without crashing", () => { @@ -31,7 +33,9 @@ test("solver handles undefined connections without crashing", () => { solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) }) test("solver handles all fields undefined without crashing", () => { @@ -44,7 +48,9 @@ test("solver handles all fields undefined without crashing", () => { solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) }) test("solver handles missing availableNetLabelOrientations without crashing", () => { @@ -57,7 +63,9 @@ test("solver handles missing availableNetLabelOrientations without crashing", () solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) }) test("MspConnectionPairSolver handles undefined connections directly without crashing", () => { @@ -87,5 +95,7 @@ test("solver handles directConnections array with undefined elements without cra solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) }) diff --git a/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts b/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts index 69e3930c4..abafaac8e 100644 --- a/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts +++ b/tests/solvers/SchematicTracePipelineSolver/empty-connections.test.ts @@ -17,7 +17,9 @@ test("pipeline solver handles empty directConnections and netConnections", () => solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) }) test("pipeline solver handles undefined directConnections and netConnections", () => { @@ -30,5 +32,7 @@ test("pipeline solver handles undefined directConnections and netConnections", ( solver.solve() expect(solver.solved).toBe(true) expect(solver.failed).toBe(false) - expect(Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths)).toBe(true) + expect( + Array.isArray(solver.schematicTraceLinesSolver?.solvedTracePaths), + ).toBe(true) })