Skip to main content

logicaffeine_compile/
aot_unsupported.rs

1//! The ops no *single-device* AOT backend can run — concurrency, virtual-time
2//! scheduling, and networking — classified once so the device tiers (Tenstorrent
3//! baby core, one CUDA kernel launch) cannot drift apart on their refusal set.
4//!
5//! A Tensix baby core and a GPU are both SPMD single-program engines with no
6//! cooperative scheduler and no host IO on the compute path. Whatever else a
7//! given tier additionally cannot lower (a GPU thread cannot run the glibc-heavy
8//! dynamic runtime, for instance) it layers on top of THIS shared base; the base
9//! itself is the one thing every offload target agrees on.
10
11use crate::vm::{CompiledProgram, Op};
12
13/// The category of op that no single-device AOT backend can lower. Each tier maps
14/// a category to its own worded reason (a Tensix core vs. a CUDA kernel phrase it
15/// differently) but the SET is shared, so a concurrency/networking op can never
16/// be silently accepted by one tier and refused by another.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Offload {
19    /// Shared-memory channel ops — no channel exists on a single compute engine.
20    Channel,
21    /// Task spawn / await / abort — the engine is SPMD, not a thread pool.
22    Task,
23    /// Sleep / select — needs a cooperative scheduler (virtual time).
24    VirtualTime,
25    /// Peer networking — host-only IO, off the compute datapath.
26    Net,
27    /// The DOM, the reactive-signal store, WebRTC — a BROWSER surface. A device has no document
28    /// to build and no page to render into, so a program that touches one is a program aimed at
29    /// the wrong tier. Without this category those tiers ACCEPTED a page and returned an
30    /// artifact, and the mistake only showed up as a blank screen on the device.
31    Dom,
32}
33
34/// Classify a single op. `None` for every op that is not inherently off a
35/// single-device datapath (arithmetic, control flow, collections, …).
36pub fn classify(op: &Op) -> Option<Offload> {
37    match op {
38        Op::ChanNew { .. }
39        | Op::ChanSend { .. }
40        | Op::ChanRecv { .. }
41        | Op::ChanTrySend { .. }
42        | Op::ChanTryRecv { .. }
43        | Op::ChanClose { .. } => Some(Offload::Channel),
44        Op::Spawn { .. } | Op::SpawnHandle { .. } | Op::TaskAwait { .. } | Op::TaskAbort { .. } => {
45            Some(Offload::Task)
46        }
47        Op::Sleep { .. }
48        | Op::SelectArmRecv { .. }
49        | Op::SelectArmTimeout { .. }
50        | Op::SelectWait { .. } => Some(Offload::VirtualTime),
51        Op::NetConnect { .. }
52        | Op::NetListen { .. }
53        | Op::NetSend { .. }
54        | Op::NetStream { .. }
55        | Op::NetAwait { .. }
56        | Op::NetMakePeer { .. }
57        | Op::NetSync { .. }
58        // The real-socket TCP client (`tcpConnect`/…) and the HTTP server (`Serve on port …`) are host-only IO
59        // — no single-device AOT datapath (analog/Tenstorrent/CUDA) can lower them, so they offload like the
60        // peer-`Net*` ops. Without this, those tiers silently accept a socket program instead of fail-closed.
61        | Op::Tcp { .. }
62        | Op::Serve { .. } => Some(Offload::Net),
63        // The DOM arrives as ordinary BUILTIN calls rather than opcodes of its own — which is why
64        // this classifier, matching only on `Op` variants, could not see it and every device tier
65        // silently accepted a web page.
66        Op::CallBuiltin { builtin, .. } if builtin.is_browser_only() => Some(Offload::Dom),
67        _ => None,
68    }
69}
70
71/// The first offload-unsupported op in `program`, classified — `None` if the whole
72/// program is on the single-device datapath.
73pub fn first(program: &CompiledProgram) -> Option<Offload> {
74    program.code.iter().find_map(classify)
75}
76
77/// The first unsupported op REACHABLE from the program's entry — the honest question for a
78/// backend deciding whether it can build this program.
79///
80/// ★ [`first`] scans the WHOLE code array, which includes every standard-library function the
81/// prelude loaded and nobody calls. `web.lg` holds the browser fetch wrappers AND the pure-text
82/// helpers `htmlEscape`, `isIntText` and `matchRoute`, so scanning everything told any program
83/// that escaped a string or matched a route that it "touches the DOM" — which is every real
84/// server. A backend must not refuse a program for code it will never emit.
85///
86/// Reachability walks `Op::Call` from Main, plus every closure body (a closure is called through
87/// a handle, so its target is not a static call edge and treating it as reachable is the SAFE
88/// direction: an unreachable refusal is a nuisance, a missed one ships a program that cannot run).
89pub fn first_reachable(program: &CompiledProgram) -> Option<Offload> {
90    // Bodies share one code array; a function owns `[entry_pc, next entry)`. Main is whatever
91    // precedes the first function body.
92    let mut entries: Vec<(usize, usize)> = program
93        .functions
94        .iter()
95        .enumerate()
96        .map(|(i, f)| (f.entry_pc, i))
97        .collect();
98    entries.sort_unstable();
99    let range = |i: usize| -> (usize, usize) {
100        let start = program.functions[i].entry_pc;
101        let end = entries
102            .iter()
103            .map(|(pc, _)| *pc)
104            .find(|pc| *pc > start)
105            .unwrap_or(program.code.len());
106        (start, end)
107    };
108    let main_end = entries
109        .first()
110        .map(|(pc, _)| *pc)
111        .unwrap_or(program.code.len());
112
113    let mut seen = vec![false; program.functions.len()];
114    // A closure body is entered through a handle rather than a `Call`, so it has no static edge
115    // to follow. Seed them all: refusing something unreachable is a nuisance; missing something
116    // reachable ships a program that cannot run.
117    for (i, f) in program.functions.iter().enumerate() {
118        if !f.captures.is_empty() {
119            seen[i] = true;
120        }
121    }
122    let mut queue: Vec<usize> = (0..program.functions.len()).filter(|i| seen[*i]).collect();
123    let scan = |from: usize, to: usize, queue: &mut Vec<usize>, seen: &mut Vec<bool>| {
124        let mut found = None;
125        for op in &program.code[from..to] {
126            if found.is_none() {
127                if let Some(off) = classify(op) {
128                    found = Some(off);
129                }
130            }
131            if let crate::vm::Op::Call { func, .. } = op {
132                let idx = *func as usize;
133                if idx < seen.len() && !seen[idx] {
134                    seen[idx] = true;
135                    queue.push(idx);
136                }
137            }
138        }
139        found
140    };
141    if let Some(off) = scan(0, main_end, &mut queue, &mut seen) {
142        return Some(off);
143    }
144    while let Some(i) = queue.pop() {
145        let (from, to) = range(i);
146        if let Some(off) = scan(from, to, &mut queue, &mut seen) {
147            return Some(off);
148        }
149    }
150    None
151}