Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benches/allocation_hot_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ mod allocation_contracts {
|b| {
b.iter(|| {
let (vertex_count, info) = measure_with_result(|| {
tds.simplex_vertices(simplex_key).map(|keys| keys.len())
tds.simplex_vertices(simplex_key).map(<[VertexKey]>::len)
});
assert_eq!(vertex_count.or_abort(), D + 1);
assert_zero_allocations(&info, "Tds::simplex_vertices");
Expand Down
51 changes: 49 additions & 2 deletions docs/api_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ for topology guarantee and validation policy details.
Delaunay repair or orientation canonicalization fails, the triangulation and
internal caches are restored to their pre-removal state.
- **Auxiliary data**: Vertices and simplices carry optional user data (`U` / `V`). Read via `vertex.data()` /
`simplex.data()`, write via `dt.set_vertex_data(key, data)` / `dt.set_simplex_data(key, data)` (O(1),
invariant-preserving). See [`workflows.md`](workflows.md) for examples.
`simplex.data()`, write via checked `dt.set_vertex_data(key, data)?` /
`dt.set_simplex_data(key, data)?` calls (O(1), invariant-preserving, typed failure for stale keys).
See [`workflows.md`](workflows.md) for examples.
- **Error handling**: Operations fail gracefully if they would violate invariants (see
[`invariants.md`](invariants.md)). Mutating operations that invoke repair use
typed repair diagnostics where available, for example
Expand Down Expand Up @@ -383,6 +384,52 @@ let report = dt.validation_report();
- **Edit API**: Implemented in `delaunay::flips` (public trait) and `core::algorithms::flips` (internal implementation)
- **Low-level primitives**: Context builders and flip application functions are `pub(crate)` in `core::algorithms::flips`

### Borrowed Views, Handles, Snapshots, And Rollback State

Topology APIs use names to make ownership visible:

- `*View` values borrow the canonical owner or are lifetime-bound to it, so they
cannot outlive the storage they observe. Examples include `FacetView<'tds>`,
`IncidenceView<'tds>`, `EdgeIndex<'tds>`, `SimplexNeighborIndex<'tds>`, and
`TriangulationAdjacency<'tds>`.
- Borrowed slices over canonical storage follow the same rule. For example,
`Tds::simplex_vertices(simplex_key)` validates the key relation, then returns
the simplex's stored `&[VertexKey]` instead of copying detached keys into a
buffer.
- `*Handle` and `*Key` values are detached, copyable runtime references. They
may be queued, stored, or returned from snapshots, but callers must validate
them against a live owner before reading through them. Examples include
`VertexKey`, `SimplexKey`, `FacetHandle`, `RidgeHandle`, `EdgeKey`, and
`TriangleHandle`.
- Owned snapshots are allowed only when the data must cross a persistence,
detached-analysis, or cache boundary. `TdsSnapshot`/`RawTdsSnapshot` are the
durable UUID persistence boundary. `ConvexHull` is a logically immutable hull
snapshot that stores `FacetHandle`s, while `ConvexHull::facets(triangulation)`
returns borrowed `FacetView` values and `ConvexHull::facet_handles()` exposes
the detached handles explicitly.
- Transactional rollback state may own cloned topology or exact mutation
records while an operation is in flight. `Tds::clone_for_rollback`,
`Tds::clone_from_for_rollback`, `SimplexIncidenceRemoval`, and flip trial
workspaces are rollback state, not long-lived public views. Replacing
full-TDS clone rollback with a journaled or localized design remains tracked
by #364.

Runtime generation or identity checks remain appropriate for detached handles,
owned snapshots, serialization boundaries, persistent performance caches, and
tests that intentionally construct inconsistent topology. They should not be
used as a substitute for lifetimes when a value is truly a view over live
canonical storage.

Algorithms follow the same phase split. Read-only traversal, classification,
and validation should work through borrowed views or lifetime-bound indexes
where practical. Mutating topology APIs should take `&mut Tds`/`&mut
Triangulation` directly, or execute behind a transaction guard that holds that
mutable borrow for the mutation or rollback window. Handles and keys may appear
inside that guard as short-lived, validated commit identifiers; they are not
proof that topology still exists by themselves. Keep views in lexical scopes
that end before the mutation so Rust enforces both existence and mutable versus
immutable access.

### Design Rationale

The separation serves several purposes:
Expand Down
24 changes: 24 additions & 0 deletions docs/dev/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,30 @@ and either borrow canonical relations for `'tds` or carry a lifetime tie to the
source snapshot for derived maps, so mutation through the same owner is
impossible while the view is alive.

Names should match ownership. A `*View` type or a method described as returning
views must borrow the canonical owner, or return values lifetime-bound to that
owner, so the view cannot outlive the data it observes. Detached, copyable
runtime references should be named `*Handle` or `*Key` instead, and APIs that
turn handles back into views must revalidate the handle against a live owner at
the conversion boundary. For example, `ConvexHull::facets(triangulation)`
returns borrowed `FacetView<'_>` values, while `ConvexHull::facet_handles()`
exposes the stored `FacetHandle`s explicitly.

Borrowed slices over canonical topology storage follow the same convention:
return `&[Key]` when the slice lives in the owner and the caller should not keep
it across mutation. For example, `Tds::simplex_vertices(simplex_key)` validates
the relation and lends the simplex's stored `&[VertexKey]`.

Algorithm implementations should use borrowed views for read-only observation,
classification, and validation phases. Mutation APIs that change canonical
topology should take `&mut Tds`/`&mut Triangulation` directly, or expose a guard
that holds that mutable borrow for the whole mutation or rollback window. This
ties existence and aliasing to the real owner: missing topology fails at view or
guard construction, and Rust prevents mutation while immutable views remain
live. Inside the mutable scope, collapse short-lived views into validated
`*Handle`/`*Key` commit identifiers before mutating; a live view must not span a
topology mutation.

Keep runtime identity or generation checks for detached handles, separately
supplied indexes, serialization boundaries, and tests that intentionally corrupt
metadata. Those checks complement lifetimes at API boundaries where Rust cannot
Expand Down
10 changes: 6 additions & 4 deletions docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,21 +327,23 @@ fn main() -> DelaunayResult<()> {
let Some((key, _)) = dt.vertices().next() else {
return Ok(());
};
let prev = dt.set_vertex_data(key, Some(99));
let prev = dt.set_vertex_data(key, Some(99))?;
assert!(prev.is_some()); // returns the old Option<U>

// Simplex data works the same way
let Some((simplex_key, _)) = dt.simplices().next() else {
return Ok(());
};
dt.set_simplex_data(simplex_key, Some(42));
dt.set_simplex_data(simplex_key, Some(42))?;
assert_eq!(dt.tds().simplex(simplex_key).map(|s| s.data()), Some(Some(&42)));
Ok(())
}
```

`set_vertex_data` and `set_simplex_data` are safe O(1) operations — they modify only the
user-data field and do not invalidate geometry, topology, or Delaunay invariants.
`set_vertex_data` and `set_simplex_data` are checked O(1) operations — they modify only the
user-data field, return the previous payload on success, and fail with a typed mutation error
if the supplied key no longer exists. Successful calls do not invalidate geometry, topology, or
Delaunay invariants.

For algorithm-local state keyed by existing vertices or simplices, prefer the
caller-owned secondary-map aliases instead of mutating stored user data:
Expand Down
8 changes: 4 additions & 4 deletions src/core/algorithms/pl_manifold_repair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,8 +676,8 @@ mod tests {

// Duplicate the first simplex → its facets go from degree 2 to degree 3.
let simplex_key = tds.simplex_keys().next().unwrap();
let vkeys = tds.simplex_vertices(simplex_key).unwrap();
let dup_simplex = Simplex::try_new_with_data(vkeys.to_vec(), None).unwrap();
let vkeys = tds.simplex_vertices(simplex_key).unwrap().to_vec();
let dup_simplex = Simplex::try_new_with_data(vkeys, None).unwrap();
tds.insert_simplex_bypassing_topology_checks_for_test(dup_simplex)
.unwrap();

Expand All @@ -695,10 +695,10 @@ mod tests {
fn make_multi_duplicate_overshared_tds() -> Tds<(), (), 3> {
let mut tds = make_overshared_tds();
let simplex_key = tds.simplex_keys().next().unwrap();
let vkeys = tds.simplex_vertices(simplex_key).unwrap();
let vkeys = tds.simplex_vertices(simplex_key).unwrap().to_vec();

for _ in 0..5 {
let dup_simplex = Simplex::try_new_with_data(vkeys.to_vec(), None).unwrap();
let dup_simplex = Simplex::try_new_with_data(vkeys.clone(), None).unwrap();
tds.insert_simplex_bypassing_topology_checks_for_test(dup_simplex)
.unwrap();
}
Expand Down
24 changes: 16 additions & 8 deletions src/core/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,15 @@ impl<K, U, V, const D: usize> Triangulation<K, U, V, D> {

/// Returns a slice view of a simplex's vertex keys.
///
/// This is a zero-allocation accessor. If `c` is not present, returns `None`.
#[must_use]
pub fn simplex_vertices(&self, c: SimplexKey) -> Option<&[VertexKey]> {
self.tds.simplex(c).map(Simplex::vertices)
/// This is a zero-allocation accessor that validates the simplex key and
/// referenced vertex keys before lending the canonical slice.
///
/// # Errors
///
/// Returns [`TdsError`] if `c` does not identify a simplex in this
/// triangulation, or if the simplex references a missing vertex key.
pub fn simplex_vertices(&self, c: SimplexKey) -> Result<&[VertexKey], TdsError> {
self.tds.simplex_vertices(c)
}

/// Returns a slice view of a vertex's coordinates.
Expand Down Expand Up @@ -1082,7 +1087,10 @@ mod tests {
neighbor_index.number_of_simplex_neighbors(missing_simplex_key),
0
);
assert!(tri.simplex_vertices(missing_simplex_key).is_none());
assert_matches!(
tri.simplex_vertices(missing_simplex_key),
Err(TdsError::SimplexNotFound { .. })
);
}

#[test]
Expand Down Expand Up @@ -1464,9 +1472,9 @@ mod tests {
assert_eq!(coords.len(), 3);
}

assert!(
tri.simplex_vertices(SimplexKey::from(KeyData::from_ffi(0xDEAD)))
.is_none()
assert_matches!(
tri.simplex_vertices(SimplexKey::from(KeyData::from_ffi(0xDEAD))),
Err(TdsError::SimplexNotFound { .. })
);
assert!(
tri.vertex_coords(VertexKey::from(KeyData::from_ffi(0xBEEF)))
Expand Down
2 changes: 1 addition & 1 deletion src/core/simplex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4398,7 +4398,7 @@ mod tests {
assert_eq!(dt.tds().simplex(key).unwrap().data(), None);

// Set data and verify via accessor
dt.set_simplex_data(key, Some(99));
dt.set_simplex_data(key, Some(99)).unwrap();
assert_eq!(dt.tds().simplex(key).unwrap().data(), Some(&99));
}
}
Loading
Loading