a-b-street/abstreet · error
modify_step broke total_length, it's now
Error message
modify_step broke total_length, it's now {} What it means
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.
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
Example fix
// before path.modify_step(idx, new_step, map); // called in a loop, total_length drifts // after path.modify_step(idx, new_step, map); path.total_length = path.steps.iter().map(|s| s.as_traversable().get_polyline(map).length()).sum(); assert!(path.total_length >= Distance::ZERO);
Defensive patterns
Strategy: validation
Validate before calling
fn total_length(map: &Map, path: &Path) -> Distance {
path.steps.iter().map(|s| s.as_traversable().get_polyline(map).length()).sum()
}
// call after edits; rebuild if it drifts or goes negative Try / catch
// panics are not catchable; keep the invariant yourself:
path.modify_step(idx, new_step, map);
if path.get_length() < Distance::ZERO {
path = rebuild_path(map);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- PathConstraints::from_lt
- Negative dist_ahead?!
- expected turn, but found
- Empty path
- pathfind() returned path that warps
AI-assisted analysis of a-b-street/abstreet@0964f29315 (2026-09-13).
Data as JSON: /api/errors/3176cdf57adff8dc.
Report an issue: GitHub.
Appendix: source
Thrown at map_model/src/pathfind/v1.rs:346
// When replacing a turn, also update any references to it in uber_turns
if let PathStep::Turn(old_turn) = self.steps[idx] {
for uts in &mut self.uber_turns {
if let Some(turn_idx) = uts.path.iter().position(|i| i == &old_turn) {
if let PathStep::Turn(new_turn) = step {
uts.path[turn_idx] = new_turn;
} else {
panic!("expected turn, but found {:?}", step);
}
}
}
}
self.steps[idx] = step;
self.total_length += self.steps[idx].as_traversable().get_polyline(map).length();
if self.total_length < Distance::ZERO {
panic!(
"modify_step broke total_length, it's now {}",
self.total_length
);
}
}
pub fn current_step(&self) -> PathStep {
self.steps[0]
}
pub fn next_step(&self) -> PathStep {
self.steps[1]
}
pub fn maybe_next_step(&self) -> Option<PathStep> {
if self.is_last_step() {
None
} else {
Some(self.next_step())View on GitHub (pinned to 0964f29315)