{"record":{"id":"3176cdf57adff8dc","repo":"a-b-street/abstreet","slug":"modify-step-broke-total-length-it-s-now","errorCode":null,"errorMessage":"modify_step broke total_length, it's now {}","messagePattern":"modify_step broke total_length, it's now (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"map_model/src/pathfind/v1.rs","lineNumber":346,"sourceCode":"\n        // When replacing a turn, also update any references to it in uber_turns\n        if let PathStep::Turn(old_turn) = self.steps[idx] {\n            for uts in &mut self.uber_turns {\n                if let Some(turn_idx) = uts.path.iter().position(|i| i == &old_turn) {\n                    if let PathStep::Turn(new_turn) = step {\n                        uts.path[turn_idx] = new_turn;\n                    } else {\n                        panic!(\"expected turn, but found {:?}\", step);\n                    }\n                }\n            }\n        }\n\n        self.steps[idx] = step;\n        self.total_length += self.steps[idx].as_traversable().get_polyline(map).length();\n\n        if self.total_length < Distance::ZERO {\n            panic!(\n                \"modify_step broke total_length, it's now {}\",\n                self.total_length\n            );\n        }\n    }\n\n    pub fn current_step(&self) -> PathStep {\n        self.steps[0]\n    }\n\n    pub fn next_step(&self) -> PathStep {\n        self.steps[1]\n    }\n    pub fn maybe_next_step(&self) -> Option<PathStep> {\n        if self.is_last_step() {\n            None\n        } else {\n            Some(self.next_step())","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/a-b-street/abstreet/blob/0964f29315820c91b171b585eb51e300164e9197/map_model/src/pathfind/v1.rs#L328-L364","documentation":"After replacing a step, modify_step recomputes total_length by adding the new step's polyline length; because the accumulation effectively assumes the old step's contribution was already netted out by the caller's edits, an inconsistent sequence of edits can drive total_length negative. The method panics when that internal invariant (total_length >= 0) is violated, indicating modify_step broke the path's bookkeeping.","triggerScenarios":"Repeated modify_step calls replacing long steps with short ones (or negative-length arithmetic on edited steps) until cumulative total_length drops below Distance::ZERO.","commonSituations":"Iterative path editing/rerouting loops that shrink path segments progressively; simulation tools that patch many steps per tick; callers replacing steps with polylines whose lengths are inconsistent with the original path.","solutions":["Rebuild the path from scratch instead of repeatedly patching steps when many edits accumulate","After each modify_step, recompute total_length from the full steps list to prevent drift","Check that replacement steps have physically plausible polyline lengths before applying","Call get_length()/recalculate total_length periodically and rebuild when it approaches zero"],"exampleFix":"// before\npath.modify_step(idx, new_step, map); // called in a loop, total_length drifts\n// after\npath.modify_step(idx, new_step, map);\npath.total_length = path.steps.iter().map(|s| s.as_traversable().get_polyline(map).length()).sum();\nassert!(path.total_length >= Distance::ZERO);","handlingStrategy":"validation","validationCode":"fn total_length(map: &Map, path: &Path) -> Distance {\n    path.steps.iter().map(|s| s.as_traversable().get_polyline(map).length()).sum()\n}\n// call after edits; rebuild if it drifts or goes negative","typeGuard":null,"tryCatchPattern":"// panics are not catchable; keep the invariant yourself:\npath.modify_step(idx, new_step, map);\nif path.get_length() < Distance::ZERO {\n    path = rebuild_path(map);\n}","preventionTips":["Recompute total_length from steps after batches of edits","Limit the number of modify_step calls per path; rebuild instead","Verify replacement steps have sensible polyline lengths","Assert total_length >= 0 in tests after every edit operation"],"tags":["rust","panic","invariant-violation","pathfinding"],"backgroundTag":"internal-invariant-violation","analyzedSha":"0964f29315820c91b171b585eb51e300164e9197","analyzedAt":"2026-09-13T18:02:03.421Z","contentChangedAt":"2026-09-13T18:02:03.421Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}