Skip to main content

vyre_reference/execution/hashmap/
mod.rs

1//! HashMap-backed reference interpreter split into execution, state, memory,
2//! synchronization, and optional subgroup semantics.
3//!
4//! This root module owns expression evaluation and the split modules own their
5//! state, memory, execution, synchronization, and subgroup contracts.
6
7pub(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/// Order in which the interpreter steps workgroups and the invocations within
32/// each workgroup.
33///
34/// The GPU makes NO ordering guarantee across invocations for NON-atomic stores:
35/// two lanes that plain-`store` the same slot leave a driver-defined winner. The
36/// single-threaded reference resolves that race DETERMINISTICALLY (last stepped
37/// lane wins), which HIDES the hazard, the output looks stable here but is
38/// nondeterministic on real hardware. Running the identical dispatch once
39/// [`Forward`](LaneOrder::Forward) and once [`Reversed`](LaneOrder::Reversed) and
40/// comparing outputs surfaces it: a race-free program (disjoint output slots, or
41/// commutative atomics for any shared slot) is order-invariant; a program with a
42/// non-atomic cross-lane write-write conflict produces a DIFFERENT result, exactly
43/// the way it would nondeterministically diverge across GPU runs.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum LaneOrder {
46    /// The canonical order: workgroups `0..N`, invocations in `create_invocations`
47    /// (z,y,x-nested linear) order. Byte-for-byte the interpreter's original path.
48    Forward,
49    /// Workgroups and intra-workgroup invocations both stepped in reverse. Only the
50    /// STEPPING order changes; every invocation keeps its true global/local ids.
51    Reversed,
52}
53
54/// A `MemoryOrdering::GridSync` barrier, the grid-wide fence `fuse_programs` inserts
55/// between arms whose later arm reads an earlier arm's cross-workgroup
56/// (launch-geometry) output. On real hardware the driver lowers it into separate
57/// globally-ordered dispatch segments; the reference interpreter mirrors that by
58/// advancing the whole grid through one segment before the next.
59fn is_grid_sync_barrier(node: &Node) -> bool {
60    matches!(
61        node,
62        Node::Barrier {
63            ordering: MemoryOrdering::GridSync
64        }
65    )
66}
67
68/// Whether a GridSync barrier appears anywhere in the SEQUENTIAL scope tree, the
69/// top level or nested inside transparent `Block` / `Region` scopes. It does NOT
70/// descend into data-dependent control flow (`If` / `Loop`), where a grid-wide fence
71/// is ill-defined and fusion never emits one.
72fn 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
80/// Flatten every transparent scope (`Block` / `Region`) that CONTAINS a GridSync so
81/// each GridSync becomes a top-level node ready to split on. A re-fused program (e.g.
82/// the exclusive scan = fuse(inclusive-chain, subtract)) nests the inner chain's
83/// A→B / B→C GridSyncs one arm-scope deeper, so a single-level unwrap misses them.
84/// Scopes WITHOUT a GridSync are kept intact (their locals keep their own scope);
85/// only GridSync-carrying wrappers are dissolved, and post-fusion arm names are
86/// already globally unique, so dropping such a wrapper's scope cannot collide.
87fn 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
101/// Split a flattened body (all GridSyncs top-level) into execution segments at each
102/// GridSync barrier. Running the ENTIRE grid through segment `k` before any
103/// workgroup enters segment `k+1` reproduces the driver's dispatch split and makes
104/// GridSync globally ordered (fixes multi-block prefix-scan Pass-B reading Pass-A's
105/// per-block totals, and the same shape in every fused multi-pass kernel).
106fn 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
119/// True when `reference_eval` RETURNS this buffer among its outputs. This is the SINGLE
120/// source of truth for the interpreter's output ABI: `reference_eval` collects exactly
121/// these decls, in `Program::buffers` order, into its result `Vec`. Test harnesses that
122/// need the position of a named output MUST use [`output_index`] (which filters by this
123/// predicate) rather than re-deriving the selection, a hand-rolled copy silently drifts
124/// (e.g. keying on `is_pipeline_live_out` alone admits `ReadOnly` live-outs the
125/// interpreter never returns, shifting every later index).
126///
127/// The "backend-allocated output" half is `BufferDecl::is_backend_allocated_output`, the
128/// single cross-backend contract in vyre-foundation shared with the CpuRef/device
129/// backends; this adds the interpreter's extra `ReadWrite` inputs-are-also-returned rule.
130pub fn is_reference_output(decl: &vyre::ir::BufferDecl) -> bool {
131    decl.is_backend_allocated_output() || decl.access() == BufferAccess::ReadWrite
132}
133
134/// Position of the buffer `name` within `reference_eval`'s returned outputs, the
135/// buffers matching [`is_reference_output`], in `Program::buffers` order, or `None`
136/// when the program declares no such returned output under that name.
137pub 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        // Caller-supplied grid floor. Buffer-shape inference cannot see the true
285        // per-INVOCATION count of a byte-scan program: the haystack is packed 4
286        // bytes/u32 and the scan length is a runtime VALUE (an input buffer of one
287        // element), so a program that runs one invocation per haystack BYTE would
288        // otherwise be under-dispatched to `haystack_len / 4` (or the largest
289        // table) invocations and SILENTLY skip high positions. A caller that knows
290        // the real grid (e.g. `haystack_len`) passes it here so the reference
291        // interpreter covers exactly what the real dispatch would, no silent
292        // under-coverage (Law 10).
293        .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    // Grid-sync-aware execution: if the body carries `GridSync` barriers (a fused
316    // multi-pass kernel the driver would split into ordered dispatches), run the
317    // WHOLE grid through each inter-barrier segment before the next, so a later pass
318    // never reads a prior pass's not-yet-written cross-workgroup output. GridSyncs
319    // can nest inside transparent Block/Region scopes (a re-fused program buries an
320    // inner chain's barriers an arm-scope deep), so flatten those scopes first. A
321    // body with no GridSync keeps the exact single-segment path (`entry`), preserving
322    // the original single-pass behavior byte-for-byte.
323    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    // Canonical workgroup dispatch order (z,y,x-nested). `LaneOrder::Reversed`
337    // steps this list, and the invocations within each workgroup, back to front
338    // to flip the deterministic last-writer of any non-atomic same-slot store, so a
339    // forward-vs-reversed output comparison surfaces a hidden cross-lane race (see
340    // [`LaneOrder`]). Forward keeps the exact original nested-loop order.
341    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                // Reverse the STEP order only; each invocation retains its true
359                // global/local ids and linear_local_index (fields move with the
360                // element), so semantics are unchanged for a race-free program.
361                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                // Single-lane interpreter: a reduction over one lane is that
599                // lane's value for every operator (Add/Mul/Min/Max/And/Or/Xor).
600                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/// Structural locks for the GridSync segmentation that makes fused multi-pass kernels
665/// globally ordered under `reference_eval` (the fix for multi-block prefix-scan Pass-B
666/// reading Pass-A's not-yet-written per-block totals). These pin the private splitting
667/// helpers IN the crate that owns them, the end-to-end value parity lives downstream in
668/// `vyre-primitives`'s multi_block/line_index tests, but the split MECHANICS belong here.
669#[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        // A workgroup-scoped SeqCst barrier is NOT a grid fence and must not split.
707        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        // A Region wrapping a Block wrapping the barrier (the re-fused exclusive scan).
719        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        // Fusion never emits a grid fence inside an `If`/`Loop`; the splitter must not
725        // treat one there as a top-level segment boundary (it would be ill-defined).
726        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        // The barriers are the split points and appear in NO segment.
737        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        // A Block carrying a GridSync is dissolved so the barrier surfaces to top level.
760        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        // A scope WITHOUT a GridSync is preserved intact (its locals keep their scope).
770        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        // The re-fused exclusive-scan shape nests the barrier one scope deeper; the
782        // recursion must reach it so the subsequent split sees it at top level.
783        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}