Skip to main content

weir/
cross_language.rs

1//! `cross_language`  -  forward reachability that requires the flow to cross
2//! at least one cross-language FFI edge anywhere on the source-to-sink path.
3//!
4//! The merged polyglot `ProgramGraph` (built upstream by
5//! `surge_frontend::pipeline::merge_polyglot`) carries language-specific
6//! `CALL_ARG` edges plus a small set of "FFI" edges that span
7//! languages  -  Python `ctypes` → C symbol, JNI Java→C, N-API JS→C,
8//! Rust `bindgen` Rust→C. The vanilla `flows_to` primitive treats
9//! all edges identically and therefore fires on intra-language flows
10//! that never cross a language boundary; this primitive constrains
11//! the path to traverse at least one FFI edge so cross-language
12//! detection is precise.
13//!
14//! Lowering shape (composes existing Tier-2.5 primitives):
15//!
16//! 1. **Source-side reach.** Run `bitset_fixpoint`-driven BFS from the source
17//!    over the full graph. This models intra-language setup before a boundary
18//!    crossing.
19//! 2. **Mandatory FFI bridge.** Run `csr_forward_traverse` from the source-side
20//!    reach set restricted to the `EDGE_KIND_FFI` mask. The output
21//!    (`post_cross`) is exactly the callee-side nodes reached through at least
22//!    one cross-language edge.
23//! 3. **Free-form continuation.** Run `bitset_fixpoint`-driven BFS from
24//!    `post_cross` over the full edge mask. The fixpoint output is the set of
25//!    nodes the source can reach after crossing the language boundary.
26//! 4. **Sink intersection.** AND the reach with the sink bitset; the
27//!    output is non-empty iff some source reaches some sink across a
28//!    language boundary.
29//!
30//! Soundness: [`MayOver`](super::soundness::Soundness::MayOver). The
31//! BFS over-approximates calls (we model every FFI edge as
32//! reachable, even when feature flags / arch gates would prune the
33//! call site). Rules that need precision must compose with a
34//! sanitizer-dominator filter.
35
36#![allow(clippy::too_many_arguments)]
37use vyre::ir::Program;
38use vyre_foundation::execution_plan::fusion::fuse_programs;
39use vyre_primitives::bitset::and::bitset_and;
40use vyre_primitives::bitset::copy::bitset_copy;
41use vyre_primitives::fixpoint::bitset_fixpoint::bitset_fixpoint_warm_start;
42use vyre_primitives::graph::csr_forward_traverse::{bitset_words, csr_forward_traverse};
43use vyre_primitives::graph::program_graph::ProgramGraphShape;
44
45pub(crate) const OP_ID: &str = "weir::cross_language";
46
47/// Edge-kind mask reserved for FFI / cross-language CALL_ARG edges.
48/// Aligns with the `vyre_primitives::predicate::edge_kind` namespace
49///  -  `0x10000` is the bit `surge_frontend::pipeline::merge_polyglot`
50///     stamps onto every edge produced by the FFI binding tables.
51pub const EDGE_KIND_FFI: u32 = 0x0001_0000;
52
53/// Edge-kind mask covering "everything" (post-crossing flow uses any
54/// edge). The exact value mirrors `csr_forward_traverse`'s "all"
55/// dispatch convention.
56pub const EDGE_KIND_ALL: u32 = 0xFFFF_FFFF;
57
58/// Build the cross-language reach Program.
59///
60/// Buffer contract:
61/// - `source`: per-node source bitset (input).
62/// - `sink`: per-node sink bitset (input).
63/// - `post_cross`: scratch bitset for stage-1 output.
64/// - `current` / `next` / `changed` / `seed`: fixpoint scratch
65///   buffers for stage-2 (provided by caller so the same buffers
66///   can be reused across rules in one fused dispatch).
67/// - `out`: per-node bitset, set iff a source reaches a sink across
68///   at least one FFI edge.
69///
70/// # Errors
71///
72/// Returns an actionable fusion error if the composed CSR, fixpoint,
73/// and bitset stages cannot be legally fused into one `vyre::Program`.
74pub fn cross_language(
75    node_count: u32,
76    source: &str,
77    sink: &str,
78    post_cross: &str,
79    current: &str,
80    next: &str,
81    changed: &str,
82    seed: &str,
83    out: &str,
84) -> Result<Program, String> {
85    let words = bitset_words(node_count);
86    let edge_capacity = node_count
87        .checked_mul(8)
88        .map(|capacity| capacity.max(1))
89        .ok_or_else(|| {
90            format!(
91                "Fix: cross-language reachability shape for node_count={node_count} exceeds u32 edge-capacity metadata. Partition the interprocedural graph before constructing the GPU program."
92            )
93        })?;
94    let shape = ProgramGraphShape::new(node_count, edge_capacity);
95
96    // Stage 1: reach every source-side setup node before the cross-language
97    // boundary. Reusing the caller-owned fixpoint buffers keeps the ABI stable.
98    let stage_source_seed = bitset_copy(seed, source, words);
99    let stage_source_reach = bitset_fixpoint_warm_start(current, next, changed, seed, words);
100
101    // Stage 2: take one mandatory cross-language bridge from any source-reached
102    // node. This permits realistic paths such as source -> parser -> FFI call
103    // -> C callee -> sink, while still rejecting all-single-language flows.
104    let stage_bridge = csr_forward_traverse(shape, current, post_cross, EDGE_KIND_FFI);
105
106    // Stage 3: copy the mandatory FFI-hop result into the caller-owned
107    // fixpoint seed, then run free-form BFS-to-fixpoint from that seed.
108    let stage_seed = bitset_copy(seed, post_cross, words);
109    let stage_post_cross_reach = bitset_fixpoint_warm_start(current, next, changed, seed, words);
110
111    // Stage 4: intersect `current` (the converged reach) with the
112    // sink bitset to produce the final answer.
113    let stage_sink = bitset_and(current, sink, out, words);
114
115    let fused = fuse_programs(&[
116        stage_source_seed,
117        stage_source_reach,
118        stage_bridge,
119        stage_seed,
120        stage_post_cross_reach,
121        stage_sink,
122    ])
123    .map_err(|error| {
124        format!(
125            "Fix: cross-language reachability fusion failed for node_count={node_count}, words={words}: {error:?}"
126        )
127    })?;
128    Ok(vyre_harness::region::tag_program(OP_ID, fused))
129}
130
131/// reference oracle: forward reach that requires at least one FFI edge in
132/// the path. Inputs use the same conventions as
133/// `csr_forward_traverse::cpu_ref` plus a parallel `edge_is_ffi`
134/// bitset (per-edge: 1 if the edge is a cross-language FFI edge).
135#[must_use]
136#[cfg(any(test, feature = "cpu-parity"))]
137#[deprecated(
138    note = "reference oracle only; production code must dispatch the Weir Program on a concrete GPU backend or use weir::oracle for parity evidence"
139)]
140pub(crate) fn cpu_ref(
141    node_count: u32,
142    edge_offsets: &[u32],
143    edge_targets: &[u32],
144    edge_kind_mask: &[u32],
145    edge_is_ffi: &[u32],
146    source: &[u32],
147    sink: &[u32],
148) -> Vec<u32> {
149    let nodes =
150        usize::try_from(node_count).expect("cross_language_cpu_ref node_count must fit host usize");
151    let words = nodes.checked_add(31).unwrap_or_else(|| {
152        panic!(
153            "cross_language_cpu_ref node_count={node_count} overflows host bitset word calculation. Fix: shard the oracle graph."
154        )
155    }) / 32;
156    crate::dispatch_decode::require_csr_shape(
157        "cross_language_cpu_ref",
158        node_count,
159        edge_offsets,
160        edge_targets,
161        edge_kind_mask,
162    )
163    .expect("cross_language cpu_ref oracle received malformed CSR graph");
164    if edge_is_ffi.len() != edge_targets.len() {
165        panic!(
166            "cross_language cpu_ref oracle received malformed FFI edge mask: expected {} words, got {}",
167            edge_targets.len(),
168            edge_is_ffi.len()
169        );
170    }
171    crate::dispatch_decode::require_bitset_words("cross_language_cpu_ref source", source, words)
172        .expect("cross_language cpu_ref oracle received malformed source bitset");
173    crate::dispatch_decode::require_bitset_words("cross_language_cpu_ref sink", sink, words)
174        .expect("cross_language cpu_ref oracle received malformed sink bitset");
175    let test = |bs: &[u32], n: u32| -> bool {
176        let n = usize::try_from(n).expect("cross_language_cpu_ref bit index must fit usize");
177        let w = n / 32;
178        let b = n % 32;
179        bs[w] & (1u32 << b) != 0
180    };
181    let mark = |bs: &mut [u32], n: u32| -> bool {
182        let n = usize::try_from(n).expect("cross_language_cpu_ref bit index must fit usize");
183        let w = n / 32;
184        let b = n % 32;
185        let mask = 1u32 << b;
186        let was_set = bs[w] & mask != 0;
187        if was_set {
188            return false;
189        }
190        bs[w] |= 1u32 << b;
191        true
192    };
193
194    // Stage 1: reach source-side setup nodes over all edges.
195    let mut source_side_reach = vec![0u32; words];
196    let mut queue = Vec::new();
197    for n in 0..node_count {
198        if test(source, n) && mark(&mut source_side_reach, n) {
199            queue.push(n);
200        }
201    }
202    let mut cursor = 0usize;
203    while cursor < queue.len() {
204        let n = queue[cursor];
205        cursor += 1;
206        let node = usize::try_from(n).expect("cross_language_cpu_ref node index must fit usize");
207        let start = usize::try_from(edge_offsets[node])
208            .expect("cross_language_cpu_ref CSR row start must fit usize");
209        let end = usize::try_from(edge_offsets[node + 1])
210            .expect("cross_language_cpu_ref CSR row end must fit usize");
211        for i in start..end {
212            let t = edge_targets[i];
213            if t < node_count && mark(&mut source_side_reach, t) {
214                queue.push(t);
215            }
216        }
217    }
218
219    // Stage 2: bridge from any source-reached node across one FFI edge.
220    let mut post_cross = vec![0u32; words];
221    queue.clear();
222    for n in 0..node_count {
223        if !test(&source_side_reach, n) {
224            continue;
225        }
226        let node = usize::try_from(n).expect("cross_language_cpu_ref node index must fit usize");
227        let start = usize::try_from(edge_offsets[node])
228            .expect("cross_language_cpu_ref CSR row start must fit usize");
229        let end = usize::try_from(edge_offsets[node + 1])
230            .expect("cross_language_cpu_ref CSR row end must fit usize");
231        for i in start..end {
232            let kind = edge_kind_mask[i];
233            let is_ffi = edge_is_ffi[i];
234            if (kind & EDGE_KIND_FFI) != 0 || is_ffi != 0 {
235                let t = edge_targets[i];
236                if t < node_count && mark(&mut post_cross, t) {
237                    queue.push(t);
238                }
239            }
240        }
241    }
242
243    // Stage 3: BFS to fixpoint from post_cross via any edge. This is
244    // queue-driven rather than graph-wide rescanning, so sparse
245    // cross-language paths scale with reached edges instead of
246    // `node_count * edge_count * diameter`.
247    let mut reach = post_cross;
248    let mut cursor = 0usize;
249    while cursor < queue.len() {
250        let n = queue[cursor];
251        cursor += 1;
252        let node = usize::try_from(n).expect("cross_language_cpu_ref node index must fit usize");
253        let start = usize::try_from(edge_offsets[node])
254            .expect("cross_language_cpu_ref CSR row start must fit usize");
255        let end = usize::try_from(edge_offsets[node + 1])
256            .expect("cross_language_cpu_ref CSR row end must fit usize");
257        for i in start..end {
258            let t = edge_targets[i];
259            if t < node_count && mark(&mut reach, t) {
260                queue.push(t);
261            }
262        }
263    }
264
265    // Stage 4: reach ∩ sink.
266    let mut out = vec![0u32; words];
267    for w in 0..words {
268        out[w] = reach[w] & sink[w];
269    }
270    out
271}
272
273/// Soundness marker for [`cross_language`].
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub struct CrossLanguage;
276impl super::soundness::SoundnessTagged for CrossLanguage {
277    fn soundness(&self) -> super::soundness::Soundness {
278        super::soundness::Soundness::MayOver
279    }
280}
281
282fn release_witness_program() -> Program {
283    match cross_language(
284        4,
285        "source",
286        "sink",
287        "post_cross",
288        "current",
289        "next",
290        "changed",
291        "seed",
292        "out",
293    ) {
294        Ok(program) => program,
295        Err(error) => {
296            unreachable!("weir::cross_language fixed release witness must fuse; error={error}")
297        }
298    }
299}
300
301fn release_witness_inputs() -> Vec<Vec<Vec<u8>>> {
302    let u32s = crate::dispatch_decode::pack_u32;
303    vec![vec![
304        u32s(&[0]),                           // seed
305        u32s(&[0b0001]),                      // source
306        u32s(&[0]),                           // current
307        u32s(&[0]),                           // next
308        u32s(&[0]),                           // changed
309        u32s(&[0, 0, 0, 0]),                  // pg_nodes
310        u32s(&[0, 1, 1, 1, 1]),               // pg_edge_offsets
311        u32s(&[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), // pg_edge_targets
312        u32s(&[EDGE_KIND_FFI, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), // pg_edge_kind_mask
313        u32s(&[0, 0, 0, 0]),                  // pg_node_tags
314        u32s(&[0]),                           // post_cross scratch
315        u32s(&[0b0010]),                      // sink
316        u32s(&[0]),                           // out
317    ]]
318}
319
320fn release_witness_expected() -> Vec<Vec<Vec<u8>>> {
321    let u32s = crate::dispatch_decode::pack_u32;
322    vec![vec![
323        u32s(&[0b0010]), // seed
324        u32s(&[0b0011]), // current
325        u32s(&[1]),      // changed
326        u32s(&[0b0010]), // post_cross
327        u32s(&[0b0010]), // out
328    ]]
329}
330
331inventory::submit! {
332    vyre_harness::OpEntry::new(
333        OP_ID,
334        release_witness_program,
335        Some(release_witness_inputs),
336        Some(release_witness_expected),
337    )
338}
339
340#[cfg(test)]
341#[allow(deprecated)]
342mod tests {
343    use super::*;
344
345    fn one(n: u32) -> Vec<u32> {
346        let w = n as usize / 32;
347        let b = n as usize % 32;
348        let mut v = vec![0u32; w + 1];
349        v[w] = 1u32 << b;
350        v
351    }
352
353    #[test]
354    fn flow_through_ffi_edge_reaches_sink() {
355        // Graph: 0 → (FFI) → 1 → (CALL_ARG) → 2.
356        // Source = {0}, Sink = {2}. Expected: out has bit 2 set.
357        let edge_offsets = vec![0, 1, 2, 2];
358        let edge_targets = vec![1u32, 2];
359        let edge_kind_mask = vec![EDGE_KIND_FFI, 0x1];
360        let edge_is_ffi = vec![1u32, 0];
361        let source = one(0);
362        let sink = one(2);
363        let out = cpu_ref(
364            3,
365            &edge_offsets,
366            &edge_targets,
367            &edge_kind_mask,
368            &edge_is_ffi,
369            &source,
370            &sink,
371        );
372        assert!(out[0] & (1 << 2) != 0, "sink should be reached: {out:?}");
373    }
374
375    #[test]
376    fn intra_language_flow_does_not_reach_sink() {
377        // Graph: 0 → (CALL_ARG, NOT FFI) → 1 → (CALL_ARG) → 2.
378        // No FFI hop anywhere  -  must not fire.
379        let edge_offsets = vec![0, 1, 2, 2];
380        let edge_targets = vec![1u32, 2];
381        let edge_kind_mask = vec![0x1, 0x1];
382        let edge_is_ffi = vec![0u32, 0];
383        let source = one(0);
384        let sink = one(2);
385        let out = cpu_ref(
386            3,
387            &edge_offsets,
388            &edge_targets,
389            &edge_kind_mask,
390            &edge_is_ffi,
391            &source,
392            &sink,
393        );
394        assert_eq!(out, vec![0u32], "no FFI hop → no cross-lang reach: {out:?}");
395    }
396
397    #[test]
398    fn ffi_edge_after_intra_language_setup_reaches_sink() {
399        // Graph: 0 → (CALL_ARG) → 1 → (FFI) → 2 → (CALL_ARG) → 3.
400        // First hop is intra-language; FFI is at hop 2. Cross-language
401        // reachability requires an FFI bridge anywhere on the path.
402        let edge_offsets = vec![0, 1, 2, 3, 3];
403        let edge_targets = vec![1u32, 2, 3];
404        let edge_kind_mask = vec![0x1, EDGE_KIND_FFI, 0x1];
405        let edge_is_ffi = vec![0u32, 1, 0];
406        let source = one(0);
407        let sink = one(3);
408        let out = cpu_ref(
409            4,
410            &edge_offsets,
411            &edge_targets,
412            &edge_kind_mask,
413            &edge_is_ffi,
414            &source,
415            &sink,
416        );
417        assert!(
418            out[0] & (1 << 3) != 0,
419            "FFI bridge after intra-language setup should reach sink: {out:?}"
420        );
421    }
422
423    #[test]
424    fn empty_source_yields_empty_output() {
425        let edge_offsets = vec![0, 1, 1];
426        let edge_targets = vec![1u32];
427        let edge_kind_mask = vec![EDGE_KIND_FFI];
428        let edge_is_ffi = vec![1u32];
429        let source = vec![0u32];
430        let sink = one(1);
431        let out = cpu_ref(
432            2,
433            &edge_offsets,
434            &edge_targets,
435            &edge_kind_mask,
436            &edge_is_ffi,
437            &source,
438            &sink,
439        );
440        assert_eq!(out, vec![0u32]);
441    }
442
443    #[test]
444    fn multi_step_post_cross_continuation_reaches_distant_sink() {
445        // Graph: 0 → (FFI) → 1 → (CALL_ARG) → 2 → (CALL_ARG) → 3.
446        // FFI happens at hop 1; further hops are free-form.
447        let edge_offsets = vec![0, 1, 2, 3, 3];
448        let edge_targets = vec![1u32, 2, 3];
449        let edge_kind_mask = vec![EDGE_KIND_FFI, 0x1, 0x1];
450        let edge_is_ffi = vec![1u32, 0, 0];
451        let source = one(0);
452        let sink = one(3);
453        let out = cpu_ref(
454            4,
455            &edge_offsets,
456            &edge_targets,
457            &edge_kind_mask,
458            &edge_is_ffi,
459            &source,
460            &sink,
461        );
462        assert!(out[0] & (1 << 3) != 0, "distant sink reachable: {out:?}");
463    }
464
465    #[test]
466    fn op_id_is_canonical() {
467        assert_eq!(OP_ID, "weir::cross_language");
468    }
469
470    #[test]
471    fn release_witness_matches_fused_program_buffer_contract() {
472        let program = release_witness_program();
473        let buffer_names = program
474            .buffers()
475            .iter()
476            .map(|buffer| buffer.name())
477            .collect::<Vec<_>>();
478        assert_eq!(
479            buffer_names,
480            vec![
481                "seed",
482                "source",
483                "current",
484                "next",
485                "changed",
486                "pg_nodes",
487                "pg_edge_offsets",
488                "pg_edge_targets",
489                "pg_edge_kind_mask",
490                "pg_node_tags",
491                "post_cross",
492                "sink",
493                "out",
494            ],
495            "release witness inputs must follow fused Program::buffers order"
496        );
497
498        let inputs = release_witness_inputs();
499        let case = &inputs[0];
500        assert_eq!(case.len(), program.buffers().len());
501        for (buffer, bytes) in program.buffers().iter().zip(case.iter()) {
502            let expected = buffer
503                .static_byte_len()
504                .expect("release witness buffers must have valid static lengths")
505                .expect("release witness buffers must be statically sized");
506            assert_eq!(
507                bytes.len(),
508                expected,
509                "release witness buffer `{}` has wrong byte length",
510                buffer.name()
511            );
512        }
513
514        let writable_names = program
515            .buffers()
516            .iter()
517            .filter(|buffer| {
518                matches!(
519                    buffer.access(),
520                    vyre::ir::BufferAccess::ReadWrite | vyre::ir::BufferAccess::WriteOnly
521                )
522            })
523            .map(|buffer| buffer.name())
524            .collect::<Vec<_>>();
525        assert_eq!(
526            writable_names,
527            vec!["seed", "current", "changed", "post_cross", "out"]
528        );
529        assert_eq!(
530            release_witness_expected()[0].len(),
531            writable_names.len(),
532            "expected outputs must match writable output order"
533        );
534    }
535
536    #[test]
537    fn cpu_ref_rejects_malformed_csr() {
538        let panic = std::panic::catch_unwind(|| {
539            let _ = cpu_ref(2, &[1, 1, 1], &[0], &[0], &[0], &[0b1], &[0b1]);
540        })
541        .expect_err("malformed CSR must panic");
542        let msg = panic_message(&panic);
543        assert!(msg.contains("malformed CSR graph"), "got {msg}");
544    }
545
546    #[test]
547    fn cpu_ref_rejects_mismatched_ffi_mask() {
548        let panic = std::panic::catch_unwind(|| {
549            let _ = cpu_ref(2, &[0, 1, 1], &[1], &[0], &[0, 0], &[0b1], &[0b1]);
550        })
551        .expect_err("mismatched FFI mask must panic");
552        let msg = panic_message(&panic);
553        assert!(msg.contains("malformed FFI edge mask"), "got {msg}");
554    }
555
556    #[test]
557    fn cpu_ref_rejects_malformed_source_bitset() {
558        let panic = std::panic::catch_unwind(|| {
559            let _ = cpu_ref(2, &[0, 1, 1], &[1], &[0], &[0], &[0b1, 0b0], &[0b1]);
560        })
561        .expect_err("malformed source must panic");
562        let msg = panic_message(&panic);
563        assert!(msg.contains("malformed source bitset"), "got {msg}");
564    }
565
566    #[test]
567    fn cpu_ref_rejects_malformed_sink_bitset() {
568        let panic = std::panic::catch_unwind(|| {
569            let _ = cpu_ref(2, &[0, 1, 1], &[1], &[0], &[0], &[0b1], &[0b1, 0b0]);
570        })
571        .expect_err("malformed sink must panic");
572        let msg = panic_message(&panic);
573        assert!(msg.contains("malformed sink bitset"), "got {msg}");
574    }
575
576    fn panic_message(panic: &Box<dyn std::any::Any + Send>) -> String {
577        if let Some(s) = (**panic).downcast_ref::<String>() {
578            s.clone()
579        } else if let Some(s) = (**panic).downcast_ref::<&'static str>() {
580            (*s).to_string()
581        } else {
582            "<non-string>".to_string()
583        }
584    }
585}