Skip to content

Commit 72b0070

Browse files
weinbe58claude
andauthored
feat(core): implement AtomStateData in Rust with PyO3 bindings (#247) (#271)
* refactor(python): replace PathFinder with ArchSpec in AtomStateData.apply_moves Part 1 of #247. The only reason PathFinder was passed to apply_moves was for get_endpoints(), which ArchSpec already provides directly. This removes the unnecessary dependency and prepares for porting AtomStateData to Rust. Also removes duplicate get_blockaded_location call on line 136 and the path_finder field from AtomInterpreter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(core): implement AtomStateData in Rust with PyO3 bindings Part 2 of #247. Implements AtomStateData as a pure Rust type in bloqade-lanes-bytecode-core with PyO3 bindings exposed as bloqade.lanes.bytecode.AtomStateData. - AtomStateData tracks qubit-to-location mappings, collisions, lane history, and move counts as atoms move through the arch - add_atoms, apply_moves, get_qubit, get_qubit_pairing methods - Immutable value type with deterministic hashing - Adds get_blockaded_location to Rust ArchSpec for CZ pair lookup - Adds Hash derive to address types (LocationAddr, LaneAddr, etc.) - 8 new Rust tests for the atom_state module Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * revert: restore PathFinder in AtomStateData.apply_moves Revert the PathFinder → ArchSpec change from Part 1 since Part 3 (replacing Python AtomStateData with Rust) is deferred. The API change is unnecessary without the migration. Keeps the duplicate get_blockaded_location line removal as a standalone bug fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): address review feedback on AtomStateData - Validate CZ pair targets in get_blockaded_location — return None if the pair points to an out-of-range location - Use LocationAddr and LaneAddr directly as HashMap keys instead of encoded u32/u64 values for cleaner code - Sort qubit iteration in get_qubit_pairing for deterministic output ordering (HashMap iteration is randomized in Rust) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(python): use validate_field for integer inputs in PyAtomStateData Accept i64 from Python and validate with validate_field<u32> for all qubit IDs, move counts, and collision mappings. Gives clear error messages ("qubit_id=-1 must be non-negative") instead of silent overflow or opaque PyO3 conversion errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: improve AtomStateData docstrings in Rust, PyO3, and type stubs Add detailed documentation for all fields, methods, and return values across the Rust core, PyO3 bindings, and Python .pyi stubs. Explains bidirectional map semantics, collision behavior, move_count accumulation, and deterministic ordering guarantees. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(core): add comprehensive tests for AtomStateData and get_blockaded_location Adds 16 new Rust tests to match Python test coverage: atom_state: - apply_moves_verifies_all_fields: checks prev_lanes, move_count, unmoved qubit - apply_moves_collision_verifies_all_fields: checks all collision state fields - apply_moves_skips_empty_source: lane with no qubit at source is a no-op - apply_moves_invalid_lane_returns_none: bad bus id returns None - apply_moves_accumulates_move_count: two sequential moves increment count - get_qubit_empty_location: lookup at unoccupied site returns None - get_qubit_pairing_with_pairs: verifies control/target/unpaired with CZ data - get_qubit_pairing_invalid_zone: bad zone id returns None - get_qubit_pairing_skips_qubits_outside_zone: filters by zone membership - default_is_empty: Default trait produces empty state - clone_produces_equal_state: Clone trait preserves equality query: - get_blockaded_location_valid: site 0 pairs with site 5 - get_blockaded_location_reverse: site 5 pairs back with site 0 - get_blockaded_location_invalid_word: out-of-range word returns None - get_blockaded_location_invalid_site: out-of-range site returns None Also strengthens add_atoms_succeeds to verify all field values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): address review feedback — endpoint validation, hash safety, collision docs - Validate src/dst from lane_endpoints against arch spec before mutating state, returning None for out-of-range locations - Add field discriminant tags and length prefixes to Hash impl to prevent cross-field collisions in HashMap/HashSet usage - Fix collision field docs: collisions accumulate across apply_moves calls, only cleared by constructors/add_atoms (not per-call) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): remove redundant endpoint validation in apply_moves lane_endpoints already resolves through the bus tables, so if it returns Some the endpoints are valid by construction. The extra check_location call was unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(core): validate lane address in lane_endpoints Move lane validation into lane_endpoints itself so all callers get consistent None behavior for invalid lanes. Previously, lane_endpoints could return valid-looking endpoints for lanes with out-of-range word_id or site_id values because those fields were copied directly without checking against the architecture bounds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(python): move HashMap validation helpers to validation.rs Move validate_i64_key_map, validate_i64_value_map, and validate_i64_kv_map from atom_state_python.rs into validation.rs alongside the existing validate_field and validate_vec helpers. Made generic over the target type T (not hardcoded to u32). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(python): add unit tests for HashMap validation helpers Tests for validate_i64_key_map, validate_i64_value_map, and validate_i64_kv_map covering valid inputs, negative keys/values, overflow, and empty maps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9a2b655 commit 72b0070

File tree

11 files changed

+1551
-8
lines changed

11 files changed

+1551
-8
lines changed

crates/bloqade-lanes-bytecode-core/src/arch/addr.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! representations used in the 16-byte instruction format.
55
66
/// Atom movement direction along a transport bus.
7-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88
#[repr(u8)]
99
pub enum Direction {
1010
/// Movement from source to destination (value 0).
@@ -14,7 +14,7 @@ pub enum Direction {
1414
}
1515

1616
/// Type of transport bus used for an atom move operation.
17-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1818
#[repr(u8)]
1919
pub enum MoveType {
2020
/// Moves atoms between sites within a word (value 0).
@@ -28,7 +28,7 @@ pub enum MoveType {
2828
/// Encodes `word_id` (16 bits) and `site_id` (16 bits) into a 32-bit word.
2929
///
3030
/// Layout: `[word_id:16][site_id:16]`
31-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3232
pub struct LocationAddr {
3333
pub word_id: u32,
3434
pub site_id: u32,
@@ -59,7 +59,7 @@ impl LocationAddr {
5959
/// Layout:
6060
/// - data0: `[word_id:16][site_id:16]`
6161
/// - data1: `[dir:1][mt:1][pad:14][bus_id:16]`
62-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6363
pub struct LaneAddr {
6464
pub direction: Direction,
6565
pub move_type: MoveType,
@@ -116,7 +116,7 @@ impl LaneAddr {
116116
/// Encodes a zone identifier (16 bits) into a 32-bit value.
117117
///
118118
/// Layout: `[pad:16][zone_id:16]`
119-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120120
pub struct ZoneAddr {
121121
pub zone_id: u32,
122122
}

crates/bloqade-lanes-bytecode-core/src/arch/query.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ impl ArchSpec {
187187
) -> Option<(LocationAddr, LocationAddr)> {
188188
use crate::arch::addr::{Direction, MoveType};
189189

190+
// Validate the lane address up front so callers always get None
191+
// for invalid lanes (e.g. out-of-range word_id or site_id).
192+
if !self.check_lane_strict(lane).is_empty() {
193+
return None;
194+
}
195+
190196
let src = LocationAddr {
191197
word_id: lane.word_id,
192198
site_id: lane.site_id,
@@ -230,6 +236,26 @@ impl ArchSpec {
230236
Some((src, dst))
231237
}
232238

239+
/// Get the CZ pair (blockaded location) for a given location.
240+
///
241+
/// Returns `Some(LocationAddr)` if the word at `loc.word_id` has CZ data,
242+
/// site `loc.site_id` has a partner, and the partner is a valid location.
243+
/// Returns `None` otherwise.
244+
pub fn get_blockaded_location(&self, loc: &LocationAddr) -> Option<LocationAddr> {
245+
let word = self.word_by_id(loc.word_id)?;
246+
let cz_pairs = word.has_cz.as_ref()?;
247+
let pair = cz_pairs.get(loc.site_id as usize)?;
248+
let result = LocationAddr {
249+
word_id: pair[0],
250+
site_id: pair[1],
251+
};
252+
// Validate the CZ pair target is in range
253+
if self.check_location(&result).is_some() {
254+
return None;
255+
}
256+
Some(result)
257+
}
258+
233259
// -- Address validation --
234260

235261
/// Check whether a location address (word_id, site_id) is valid.
@@ -276,6 +302,55 @@ impl ArchSpec {
276302
errors
277303
}
278304

305+
/// Strict lane validation: checks everything in [`check_lane`] plus
306+
/// verifies that the site/word is a valid source or destination for
307+
/// the bus in the given direction.
308+
///
309+
/// This is used by [`lane_endpoints`](Self::lane_endpoints) to guarantee
310+
/// that returned endpoints are fully valid.
311+
pub fn check_lane_strict(&self, addr: &LaneAddr) -> Vec<String> {
312+
use super::addr::Direction;
313+
314+
// Start with the basic checks
315+
let mut errors = self.check_lane(addr);
316+
if !errors.is_empty() {
317+
return errors;
318+
}
319+
320+
// Additionally verify bus resolution for the given direction
321+
match addr.move_type {
322+
MoveType::SiteBus => {
323+
if let Some(bus) = self.site_bus_by_id(addr.bus_id) {
324+
let resolvable = match addr.direction {
325+
Direction::Forward => bus.resolve_forward(addr.site_id).is_some(),
326+
Direction::Backward => bus.resolve_backward(addr.site_id).is_some(),
327+
};
328+
if !resolvable {
329+
errors.push(format!(
330+
"site_id {} is not a valid {:?} source for site_bus {}",
331+
addr.site_id, addr.direction, addr.bus_id
332+
));
333+
}
334+
}
335+
}
336+
MoveType::WordBus => {
337+
if let Some(bus) = self.word_bus_by_id(addr.bus_id) {
338+
let resolvable = match addr.direction {
339+
Direction::Forward => bus.resolve_forward(addr.word_id).is_some(),
340+
Direction::Backward => bus.resolve_backward(addr.word_id).is_some(),
341+
};
342+
if !resolvable {
343+
errors.push(format!(
344+
"word_id {} is not a valid {:?} source for word_bus {}",
345+
addr.word_id, addr.direction, addr.bus_id
346+
));
347+
}
348+
}
349+
}
350+
}
351+
errors
352+
}
353+
279354
/// Check whether a zone address is valid.
280355
pub fn check_zone(&self, zone: &crate::arch::addr::ZoneAddr) -> Option<String> {
281356
if self.zone_by_id(zone.zone_id).is_none() {
@@ -639,4 +714,50 @@ mod tests {
639714
let result = super::super::ArchSpec::from_json_validated(json);
640715
assert!(result.is_err());
641716
}
717+
718+
#[test]
719+
fn get_blockaded_location_valid() {
720+
let spec = example_arch_spec();
721+
// Site 0 in word 0 pairs with site 5 in word 0
722+
let loc = crate::arch::addr::LocationAddr {
723+
word_id: 0,
724+
site_id: 0,
725+
};
726+
let pair = spec.get_blockaded_location(&loc).unwrap();
727+
assert_eq!(pair.word_id, 0);
728+
assert_eq!(pair.site_id, 5);
729+
}
730+
731+
#[test]
732+
fn get_blockaded_location_reverse() {
733+
let spec = example_arch_spec();
734+
// Site 5 in word 0 pairs back with site 0 in word 0
735+
let loc = crate::arch::addr::LocationAddr {
736+
word_id: 0,
737+
site_id: 5,
738+
};
739+
let pair = spec.get_blockaded_location(&loc).unwrap();
740+
assert_eq!(pair.word_id, 0);
741+
assert_eq!(pair.site_id, 0);
742+
}
743+
744+
#[test]
745+
fn get_blockaded_location_invalid_word() {
746+
let spec = example_arch_spec();
747+
let loc = crate::arch::addr::LocationAddr {
748+
word_id: 99,
749+
site_id: 0,
750+
};
751+
assert!(spec.get_blockaded_location(&loc).is_none());
752+
}
753+
754+
#[test]
755+
fn get_blockaded_location_invalid_site() {
756+
let spec = example_arch_spec();
757+
let loc = crate::arch::addr::LocationAddr {
758+
word_id: 0,
759+
site_id: 99,
760+
};
761+
assert!(spec.get_blockaded_location(&loc).is_none());
762+
}
642763
}

0 commit comments

Comments
 (0)