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}