1pub(crate) mod memory;
8pub(crate) mod state;
9pub(crate) mod step;
10pub(crate) mod subgroup;
11pub(crate) mod sync;
12
13use memory::{atomic_buffer_mut, output_value, resolve_buffer, HashmapMemory};
14#[cfg(feature = "subgroup-ops")]
15use state::HashmapInvocationSnapshot;
16use state::{create_invocations, run_invocations, HashmapInvocation};
17use step::{axis_value, eval_call, eval_to_index};
18#[cfg(feature = "subgroup-ops")]
19use subgroup::{eval_subgroup_ballot, eval_subgroup_reduce, eval_subgroup_shuffle};
20use sync::element_count;
21
22use crate::{
23 atomics,
24 oob::{self, Buffer},
25 value::Value,
26};
27use rustc_hash::FxHashMap;
28use vyre::ir::{AtomicOp, BufferAccess, Expr, MemoryOrdering, Node, Program};
29use vyre::Error;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum LaneOrder {
46 Forward,
49 Reversed,
52}
53
54fn is_grid_sync_barrier(node: &Node) -> bool {
60 matches!(
61 node,
62 Node::Barrier {
63 ordering: MemoryOrdering::GridSync
64 }
65 )
66}
67
68fn contains_grid_sync(nodes: &[Node]) -> bool {
73 nodes.iter().any(|node| match node {
74 Node::Block(inner) => contains_grid_sync(inner),
75 Node::Region { body, .. } => contains_grid_sync(body),
76 other => is_grid_sync_barrier(other),
77 })
78}
79
80fn flatten_grid_sync_scopes(nodes: &[Node], out: &mut Vec<Node>) {
88 for node in nodes {
89 match node {
90 Node::Block(inner) if contains_grid_sync(inner) => {
91 flatten_grid_sync_scopes(inner, out);
92 }
93 Node::Region { body, .. } if contains_grid_sync(body) => {
94 flatten_grid_sync_scopes(body, out);
95 }
96 other => out.push(other.clone()),
97 }
98 }
99}
100
101fn split_top_level_grid_sync(nodes: &[Node]) -> Vec<&[Node]> {
107 let mut segments = Vec::new();
108 let mut start = 0;
109 for (index, node) in nodes.iter().enumerate() {
110 if is_grid_sync_barrier(node) {
111 segments.push(&nodes[start..index]);
112 start = index + 1;
113 }
114 }
115 segments.push(&nodes[start..]);
116 segments
117}
118
119pub fn is_reference_output(decl: &vyre::ir::BufferDecl) -> bool {
131 decl.is_backend_allocated_output() || decl.access() == BufferAccess::ReadWrite
132}
133
134pub fn output_index(program: &Program, name: &str) -> Option<usize> {
138 program
139 .buffers()
140 .iter()
141 .filter(|decl| is_reference_output(decl))
142 .position(|decl| decl.name() == name)
143}
144
145#[doc = " Execute a vyre IR program using hashmap-backed locals."]
146pub(crate) fn run_hashmap_reference(
147 program: &Program,
148 inputs: &[Value],
149 min_dispatch_elements: u32,
150 lane_order: LaneOrder,
151) -> Result<Vec<Value>, Error> {
152 #[cfg(feature = "subgroup-ops")]
153 let validation_report = vyre::validate::validate::validate_with_options(
154 program,
155 vyre::validate::ValidationOptions::default().with_backend_capabilities(
156 vyre::validate::BackendCapabilities {
157 supports_subgroup_ops: true,
158 ..Default::default()
159 },
160 ),
161 );
162 #[cfg(not(feature = "subgroup-ops"))]
163 let validation_report = vyre::validate::validate::validate_with_options(
164 program,
165 vyre::validate::ValidationOptions::default(),
166 );
167 let validation_errors = validation_report.errors;
168 if !validation_errors.is_empty() {
169 let message_len = validation_errors
170 .iter()
171 .map(|error| error.message().len())
172 .sum::<usize>()
173 + validation_errors.len().saturating_sub(1) * 2;
174 let mut messages = String::with_capacity(message_len);
175 for (index, error) in validation_errors.iter().enumerate() {
176 if index != 0 {
177 messages.push_str("; ");
178 }
179 messages.push_str(error.message());
180 }
181 return Err(Error::interp(format!(
182 "program failed IR validation: {messages}. Fix: repair the Program before invoking the reference interpreter."
183 )));
184 }
185 let mut storage = FxHashMap::default();
186 let logical_input_count = program
187 .buffers()
188 .iter()
189 .filter(|decl| {
190 decl.access() != BufferAccess::Workgroup && !decl.is_backend_allocated_output()
191 })
192 .count();
193 let legacy_input_count = program
194 .buffers()
195 .iter()
196 .filter(|decl| decl.access() != BufferAccess::Workgroup)
197 .count();
198 let legacy_input_mode =
199 inputs.len() == legacy_input_count && inputs.len() != logical_input_count;
200 let mut input_index = 0usize;
201 let mut output_decls = Vec::new();
202 let mut max_output_elements = 0u32;
203 let mut max_input_elements = 1u32;
204 let mut program_graph_node_count = None;
205 let mut has_workgroup_buffer = false;
206 for decl in program.buffers() {
207 if decl.access() == BufferAccess::Workgroup {
208 has_workgroup_buffer = true;
209 continue;
210 }
211 if decl.binding() == 0 && decl.name() == "pg_nodes" {
212 program_graph_node_count = Some(decl.count());
213 }
214 let required_bytes = declared_min_byte_len(decl)?;
215 let backend_allocated = decl.is_backend_allocated_output();
216 let bytes = if backend_allocated {
217 if legacy_input_mode {
218 let _legacy_output_initializer = inputs.get(input_index).ok_or_else(|| {
219 Error::interp(format!(
220 "missing legacy output initializer for buffer `{}`. Fix: pass one Value for each non-workgroup buffer or migrate to logical inputs only.",
221 decl.name()
222 ))
223 })?;
224 input_index += 1;
225 }
226 vec![0u8; required_bytes]
227 } else {
228 let value = inputs.get(input_index).ok_or_else(|| {
229 Error::interp(format!(
230 "missing input for buffer `{}`. Fix: pass one Value for each non-output, non-workgroup buffer in Program::buffers order.",
231 decl.name()
232 ))
233 })?;
234 input_index += 1;
235 value.to_bytes()
236 };
237 if bytes.len() < required_bytes {
238 return Err(Error::interp(format!(
239 "buffer `{}` has {} bytes but requires at least {} bytes ({} elements of {}). Fix: provide a larger input buffer.",
240 decl.name(),
241 bytes.len(),
242 required_bytes,
243 decl.count(),
244 decl.element()
245 )));
246 }
247 let elements = element_count(decl, bytes.len())?;
248 if is_reference_output(decl) {
249 max_output_elements = max_output_elements.max(elements);
250 output_decls.push(decl.clone());
251 } else {
252 max_input_elements = max_input_elements.max(elements);
253 }
254 storage.insert(
255 decl.name().to_string(),
256 Buffer::new(bytes, decl.element().clone()),
257 );
258 }
259 if input_index != inputs.len() {
260 return Err(Error::interp(
261 "unused input values supplied. Fix: pass exactly one Value per non-workgroup buffer declaration.",
262 ));
263 }
264 if program.workgroup_size().contains(&0) {
265 return Err(Error::interp(
266 "workgroup size contains zero. Fix: all dimensions must be >= 1.",
267 ));
268 }
269 let [sx, sy, sz] = program.workgroup_size();
270 let invocations_per_workgroup = [sx, sy, sz]
271 .iter()
272 .copied()
273 .fold(1u32, u32::saturating_mul)
274 .max(1);
275 let force_full_span = has_workgroup_buffer || program.stats().atomic_op_count > 0;
276 let dispatch_elements = max_output_elements
277 .max(program_graph_node_count.unwrap_or(0))
278 .max(1)
279 .max(if output_decls.is_empty() || force_full_span {
280 max_input_elements
281 } else {
282 1
283 })
284 .max(min_dispatch_elements);
294 let total_wg = dispatch_elements.div_ceil(invocations_per_workgroup).max(1);
295 let active: Vec<usize> = [sx, sy, sz]
296 .iter()
297 .enumerate()
298 .filter(|(_, size)| **size > 1)
299 .map(|(i, _)| i)
300 .collect();
301 let n = active.len().max(1);
302 let mut counts = [1u32, 1, 1];
303 if active.is_empty() {
304 counts[0] = total_wg;
305 } else {
306 let base = (total_wg as f64).powf(1.0 / n as f64).ceil() as u32;
307 for &axis in &active {
308 counts[axis] = base.max(1);
309 }
310 }
311 let [workgroup_count_x, workgroup_count_y, workgroup_count_z] = counts;
312 let entry = program.entry();
313 #[cfg(feature = "subgroup-ops")]
314 let uses_subgroup_ops = vyre::program_caps::scan(program).subgroup_ops;
315 let has_grid_sync = contains_grid_sync(entry);
324 let flattened: Vec<Node> = if has_grid_sync {
325 let mut nodes = Vec::new();
326 flatten_grid_sync_scopes(entry, &mut nodes);
327 nodes
328 } else {
329 Vec::new()
330 };
331 let segments: Vec<&[Node]> = if has_grid_sync {
332 split_top_level_grid_sync(&flattened)
333 } else {
334 vec![entry]
335 };
336 let mut wg_coords: Vec<[u32; 3]> = Vec::new();
342 for wg_z in 0..workgroup_count_z {
343 for wg_y in 0..workgroup_count_y {
344 for wg_x in 0..workgroup_count_x {
345 wg_coords.push([wg_x, wg_y, wg_z]);
346 }
347 }
348 }
349 if lane_order == LaneOrder::Reversed {
350 wg_coords.reverse();
351 }
352 let mut memory = HashmapMemory::new(storage);
353 for &segment in &segments {
354 for &wg in &wg_coords {
355 memory.reset_workgroup(program)?;
356 let mut invocations = create_invocations(program, wg, segment)?;
357 if lane_order == LaneOrder::Reversed {
358 invocations.reverse();
362 }
363 run_invocations(
364 &mut memory,
365 &mut invocations,
366 #[cfg(feature = "subgroup-ops")]
367 uses_subgroup_ops,
368 )?;
369 }
370 }
371 let mut storage = memory.storage;
372 output_decls . into_iter () . map (| decl | { storage . remove (decl . name ()) . map (| buffer | output_value (buffer , & decl)) . ok_or_else (| | { let name = decl . name () ; Error :: interp (format ! ("missing output buffer `{name}` after dispatch. Fix: keep buffer declarations unique.")) }) }) . collect ()
373}
374
375fn declared_min_byte_len(decl: &vyre::ir::BufferDecl) -> Result<usize, Error> {
376 match decl.static_byte_len() {
377 Ok(Some(byte_len)) => Ok(byte_len),
378 Ok(None) if decl.count() == 0 => Ok(0),
379 Ok(None) => Err(Error::interp(format!(
380 "buffer `{}` has unsized element type {}. Fix: provide a fixed-width buffer element type before invoking the reference interpreter.",
381 decl.name(),
382 decl.element()
383 ))),
384 Err(error) => Err(Error::interp(error)),
385 }
386}
387
388fn eval_expr(
389 expr: &Expr,
390 invocation: &mut HashmapInvocation<'_>,
391 memory: &mut HashmapMemory,
392 #[cfg(feature = "subgroup-ops")] snapshots: &[HashmapInvocationSnapshot],
393) -> Result<Value, Error> {
394 match expr {
395 Expr::LitU32(value) => Ok(Value::U32(*value)),
396 Expr::LitI32(value) => Ok(Value::I32(*value)),
397 Expr::LitF32(value) => Ok(Value::Float(f64::from(crate::execution::typed_ops::canonical_f32(
398 *value,
399 )))),
400 Expr::LitBool(value) => Ok(Value::Bool(*value)),
401 Expr::Var(name) => invocation.locals.local(name).ok_or_else(|| {
402 Error::interp(format!(
403 "reference to undeclared variable `{name}`. Fix: add a Let before this use."
404 ))
405 }),
406 Expr::Load { buffer, index } => {
407 let idx = eval_to_index(
408 index,
409 "load index",
410 invocation,
411 memory,
412 #[cfg(feature = "subgroup-ops")]
413 snapshots,
414 )?;
415 Ok(oob::load(resolve_buffer(memory, buffer)?, idx))
416 }
417 Expr::BufLen { buffer } => Ok(Value::U32(resolve_buffer(memory, buffer)?.len())),
418 Expr::InvocationId { axis } => axis_value(invocation.ids.global, *axis),
419 Expr::WorkgroupId { axis } => axis_value(invocation.ids.workgroup, *axis),
420 Expr::LocalId { axis } => axis_value(invocation.ids.local, *axis),
421 Expr::BinOp { op, left, right } => {
422 let left = eval_expr(
423 left,
424 invocation,
425 memory,
426 #[cfg(feature = "subgroup-ops")]
427 snapshots,
428 )?;
429 let right = eval_expr(
430 right,
431 invocation,
432 memory,
433 #[cfg(feature = "subgroup-ops")]
434 snapshots,
435 )?;
436 crate::execution::op_count::record_op();
437 crate::execution::typed_ops::eval_binop(*op, left, right)
438 }
439 Expr::UnOp { op, operand } => {
440 let operand = eval_expr(
441 operand,
442 invocation,
443 memory,
444 #[cfg(feature = "subgroup-ops")]
445 snapshots,
446 )?;
447 crate::execution::op_count::record_op();
448 crate::execution::typed_ops::eval_unop(op, operand)
449 }
450 Expr::Call { op_id, args } => eval_call(
451 expr as *const Expr,
452 op_id,
453 args,
454 invocation,
455 memory,
456 #[cfg(feature = "subgroup-ops")]
457 snapshots,
458 ),
459 Expr::Select {
460 cond,
461 true_val,
462 false_val,
463 } => {
464 let cond = eval_expr(
465 cond,
466 invocation,
467 memory,
468 #[cfg(feature = "subgroup-ops")]
469 snapshots,
470 )?
471 .truthy();
472 let true_val = eval_expr(
473 true_val,
474 invocation,
475 memory,
476 #[cfg(feature = "subgroup-ops")]
477 snapshots,
478 )?;
479 let false_val = eval_expr(
480 false_val,
481 invocation,
482 memory,
483 #[cfg(feature = "subgroup-ops")]
484 snapshots,
485 )?;
486 Ok(if cond { true_val } else { false_val })
487 }
488 Expr::Cast { target, value } => {
489 let value = eval_expr(
490 value,
491 invocation,
492 memory,
493 #[cfg(feature = "subgroup-ops")]
494 snapshots,
495 )?;
496 crate::execution::expr_cast::cast_value(target, &value)
497 }
498 Expr::Fma { a, b, c } => {
499 let a = eval_expr(
500 a,
501 invocation,
502 memory,
503 #[cfg(feature = "subgroup-ops")]
504 snapshots,
505 )?
506 .try_as_f32()
507 .ok_or_else(|| {
508 Error::interp("fma operand `a` is not a float. Fix: cast to f32 before fma.")
509 })?;
510 let b = eval_expr(
511 b,
512 invocation,
513 memory,
514 #[cfg(feature = "subgroup-ops")]
515 snapshots,
516 )?
517 .try_as_f32()
518 .ok_or_else(|| {
519 Error::interp("fma operand `b` is not a float. Fix: cast to f32 before fma.")
520 })?;
521 let c = eval_expr(
522 c,
523 invocation,
524 memory,
525 #[cfg(feature = "subgroup-ops")]
526 snapshots,
527 )?
528 .try_as_f32()
529 .ok_or_else(|| {
530 Error::interp("fma operand `c` is not a float. Fix: cast to f32 before fma.")
531 })?;
532 let a = crate::execution::typed_ops::canonical_f32(a);
533 let b = crate::execution::typed_ops::canonical_f32(b);
534 let c = crate::execution::typed_ops::canonical_f32(c);
535 crate::execution::op_count::record_op();
536 Ok(Value::Float(f64::from(crate::execution::typed_ops::canonical_f32(
537 a.mul_add(b, c),
538 ))))
539 }
540 Expr::Atomic {
541 op,
542 buffer,
543 index,
544 expected,
545 value,
546 ordering: _,
547 } => eval_atomic(
548 *op,
549 buffer,
550 index,
551 expected.as_deref(),
552 value,
553 invocation,
554 memory,
555 #[cfg(feature = "subgroup-ops")]
556 snapshots,
557 ),
558 Expr::Opaque(extension) => Err(Error::interp(format!(
559 "hashmap reference interpreter does not support opaque expression extension `{}`/`{}`. Fix: provide a reference evaluator for this ExprNode or lower it to core Expr variants before evaluation.",
560 extension.extension_kind(),
561 extension.debug_identity()
562 ))),
563 Expr::SubgroupBallot { cond } => {
564 #[cfg(feature = "subgroup-ops")]
565 {
566 eval_subgroup_ballot(cond, invocation, snapshots, memory)
567 }
568 #[cfg(not(feature = "subgroup-ops"))]
569 {
570 let cond = eval_expr(cond, invocation, memory)?.truthy();
571 Ok(Value::U32(u32::from(cond)))
572 }
573 }
574 Expr::SubgroupShuffle { value, lane } => {
575 #[cfg(feature = "subgroup-ops")]
576 {
577 eval_subgroup_shuffle(value, lane, invocation, snapshots, memory)
578 }
579 #[cfg(not(feature = "subgroup-ops"))]
580 {
581 let value_val = eval_expr(value, invocation, memory)?;
582 let lane_val = eval_expr(lane, invocation, memory)?;
583 let lane_u32 = lane_val . try_as_u32 () . ok_or_else (| | { Error :: interp ("subgroup_shuffle lane index is not a u32. Fix: use a scalar u32 lane argument." ,) }) ? ;
584 Ok(if lane_u32 == 0 {
585 value_val
586 } else {
587 Value::U32(0)
588 })
589 }
590 }
591 Expr::SubgroupReduce { op, value } => {
592 #[cfg(feature = "subgroup-ops")]
593 {
594 eval_subgroup_reduce(*op, value, invocation, snapshots, memory)
595 }
596 #[cfg(not(feature = "subgroup-ops"))]
597 {
598 let _ = op;
601 eval_expr(value, invocation, memory)
602 }
603 }
604 _ => Err(Error::interp(
605 "hashmap reference interpreter encountered an unknown expression variant. Fix: add explicit reference semantics for the new ExprNode before dispatch.",
606 )),
607 }
608}
609#[allow(clippy::too_many_arguments)]
610fn eval_atomic(
611 op: AtomicOp,
612 buffer: &str,
613 index: &Expr,
614 expected: Option<&Expr>,
615 value: &Expr,
616 invocation: &mut HashmapInvocation<'_>,
617 memory: &mut HashmapMemory,
618 #[cfg(feature = "subgroup-ops")] snapshots: &[HashmapInvocationSnapshot],
619) -> Result<Value, Error> {
620 match (op, expected) {
621 (AtomicOp::CompareExchange, None) => {
622 return Err(Error::interp(
623 "compare-exchange atomic is missing expected value. Fix: set Expr::Atomic.expected for AtomicOp::CompareExchange.",
624 ));
625 }
626 (AtomicOp::CompareExchange, Some(_)) => {}
627 (_, Some(_)) => {
628 return Err(Error::interp(
629 "non-compare-exchange atomic includes an expected value. Fix: use Expr::Atomic.expected only with AtomicOp::CompareExchange.",
630 ));
631 }
632 (_, None) => {}
633 }
634 let idx = eval_to_index(
635 index,
636 "atomic index",
637 invocation,
638 memory,
639 #[cfg(feature = "subgroup-ops")]
640 snapshots,
641 )?;
642 let expected = expected . map (| expr | { eval_expr (expr , invocation , memory , #[cfg (feature = "subgroup-ops")] snapshots ,) ? . try_as_u32 () . ok_or_else (| | { Error :: interp (format ! ("atomic expected value {expr:?} cannot be represented as u32. Fix: use a scalar u32-compatible argument.")) }) }) . transpose () ? ;
643 let value = eval_expr(
644 value,
645 invocation,
646 memory,
647 #[cfg(feature = "subgroup-ops")]
648 snapshots,
649 )?;
650 let value = value.try_as_u32().ok_or_else(|| {
651 Error::interp(
652 "atomic value cannot be represented as u32. Fix: use a scalar u32-compatible argument.",
653 )
654 })?;
655 let target = atomic_buffer_mut(memory, buffer)?;
656 let Some(old) = oob::atomic_load(target, idx) else {
657 return Ok(Value::U32(0));
658 };
659 let (old, new) = atomics::apply(op, old, expected, value)?;
660 oob::atomic_store(target, idx, new);
661 Ok(Value::U32(old))
662}
663
664#[cfg(test)]
670mod grid_sync_segmentation {
671 use super::*;
672 use std::sync::Arc;
673 use vyre::ir::{Expr, Ident, MemoryOrdering};
674
675 fn gs() -> Node {
676 Node::barrier_with_ordering(MemoryOrdering::GridSync)
677 }
678 fn seqcst() -> Node {
679 Node::barrier_with_ordering(MemoryOrdering::SeqCst)
680 }
681 fn other() -> Node {
682 Node::return_()
683 }
684 fn region(body: Vec<Node>) -> Node {
685 Node::Region {
686 generator: Ident::from("g"),
687 source_region: None,
688 body: Arc::new(body),
689 }
690 }
691 fn gs_count(nodes: &[Node]) -> usize {
692 nodes
693 .iter()
694 .filter(|node| is_grid_sync_barrier(node))
695 .count()
696 }
697 fn has_scope(nodes: &[Node]) -> bool {
698 nodes
699 .iter()
700 .any(|node| matches!(node, Node::Block(_) | Node::Region { .. }))
701 }
702
703 #[test]
704 fn is_grid_sync_barrier_matches_only_gridsync() {
705 assert!(is_grid_sync_barrier(&gs()));
706 assert!(!is_grid_sync_barrier(&seqcst()));
708 assert!(!is_grid_sync_barrier(&other()));
709 }
710
711 #[test]
712 fn contains_grid_sync_finds_top_level_and_nested_scopes() {
713 assert!(contains_grid_sync(&[other(), gs(), other()]));
714 assert!(!contains_grid_sync(&[other(), other()]));
715 assert!(!contains_grid_sync(&[seqcst()]));
716 assert!(contains_grid_sync(&[Node::block(vec![gs()])]));
717 assert!(contains_grid_sync(&[region(vec![other(), gs()])]));
718 assert!(contains_grid_sync(&[region(vec![Node::block(vec![gs()])])]));
720 }
721
722 #[test]
723 fn contains_grid_sync_does_not_descend_into_data_dependent_control_flow() {
724 let inside_if = Node::if_then(Expr::bool(true), vec![gs()]);
727 assert!(!contains_grid_sync(&[inside_if]));
728 }
729
730 #[test]
731 fn split_partitions_at_each_top_level_barrier() {
732 let body = vec![other(), gs(), other(), gs(), other()];
733 let segments = split_top_level_grid_sync(&body);
734 assert_eq!(segments.len(), 3, "two barriers => three segments");
735 assert!(segments.iter().all(|segment| segment.len() == 1));
736 assert!(segments.iter().all(|segment| gs_count(segment) == 0));
738 }
739
740 #[test]
741 fn split_yields_one_segment_without_a_barrier() {
742 let body = vec![other(), other()];
743 let segments = split_top_level_grid_sync(&body);
744 assert_eq!(segments.len(), 1);
745 assert_eq!(segments[0].len(), 2);
746 }
747
748 #[test]
749 fn split_emits_empty_trailing_segment_for_trailing_barrier() {
750 let body = vec![other(), gs()];
751 let segments = split_top_level_grid_sync(&body);
752 assert_eq!(segments.len(), 2);
753 assert_eq!(segments[0].len(), 1);
754 assert_eq!(segments[1].len(), 0);
755 }
756
757 #[test]
758 fn flatten_dissolves_gridsync_scopes_and_keeps_the_rest() {
759 let mut dissolved = Vec::new();
761 flatten_grid_sync_scopes(&[Node::block(vec![other(), gs(), other()])], &mut dissolved);
762 assert_eq!(dissolved.len(), 3);
763 assert!(
764 !has_scope(&dissolved),
765 "GridSync-carrying Block must be dissolved"
766 );
767 assert_eq!(gs_count(&dissolved), 1);
768
769 let mut preserved = Vec::new();
771 flatten_grid_sync_scopes(&[Node::block(vec![other(), other()])], &mut preserved);
772 assert_eq!(preserved.len(), 1);
773 assert!(
774 has_scope(&preserved),
775 "a scope with no GridSync must be preserved"
776 );
777 }
778
779 #[test]
780 fn flatten_recurses_through_nested_gridsync_scopes_then_splits() {
781 let nested = region(vec![Node::block(vec![other(), gs(), other()])]);
784 let mut flattened = Vec::new();
785 flatten_grid_sync_scopes(&[nested], &mut flattened);
786 assert!(
787 !has_scope(&flattened),
788 "all GridSync-carrying scopes must dissolve"
789 );
790 assert_eq!(gs_count(&flattened), 1);
791 assert_eq!(
792 split_top_level_grid_sync(&flattened).len(),
793 2,
794 "the surfaced barrier must partition into two segments"
795 );
796 }
797}