seqc/codegen/specialization/eligibility.rs
1//! Eligibility analysis for register-based specialization.
2//!
3//! Decides whether a word is a candidate for the fast-path codegen: its
4//! declared effect must use only primitive types, its body must stay within
5//! a fixed allowlist of operations (`SPECIALIZABLE_OPS`) or call other
6//! already-specialized words, and it must not take/return heap-allocated
7//! values. No IR is emitted here — this is read-only analysis.
8
9use super::CodeGen;
10use super::types::{RegisterType, SpecSignature};
11use crate::ast::{Statement, WordDef};
12use crate::types::StackType;
13
14/// Operations that can be emitted in specialized (register-based) mode.
15///
16/// These operations either:
17/// - Map directly to LLVM instructions (arithmetic, comparisons, bitwise)
18/// - Use LLVM intrinsics (popcount, clz, ctz)
19/// - Are pure context manipulations (stack shuffles like dup, swap, rot)
20///
21/// Operations NOT in this list (like `print`, `read`, string ops) require
22/// the full stack-based calling convention.
23pub(super) const SPECIALIZABLE_OPS: &[&str] = &[
24 // Integer arithmetic (i./ and i.% emit safe division with zero checks)
25 "i.+",
26 "i.add",
27 "i.-",
28 "i.subtract",
29 "i.*",
30 "i.multiply",
31 "i./",
32 "i.divide",
33 "i.%",
34 "i.mod",
35 // Bitwise operations
36 "band",
37 "bor",
38 "bxor",
39 "bnot",
40 "shl",
41 "shr",
42 // Bit counting operations
43 "popcount",
44 "clz",
45 "ctz",
46 // Type conversions
47 "int->float",
48 "float->int",
49 // Boolean logical operations
50 "and",
51 "or",
52 "not",
53 // Integer comparisons
54 "i.<",
55 "i.lt",
56 "i.>",
57 "i.gt",
58 "i.<=",
59 "i.lte",
60 "i.>=",
61 "i.gte",
62 "i.=",
63 "i.eq",
64 "i.<>",
65 "i.neq",
66 // Float arithmetic
67 "f.+",
68 "f.add",
69 "f.-",
70 "f.subtract",
71 "f.*",
72 "f.multiply",
73 "f./",
74 "f.divide",
75 // Float comparisons
76 "f.<",
77 "f.lt",
78 "f.>",
79 "f.gt",
80 "f.<=",
81 "f.lte",
82 "f.>=",
83 "f.gte",
84 "f.=",
85 "f.eq",
86 "f.<>",
87 "f.neq",
88 // Stack operations (handled as context shuffles)
89 "dup",
90 "drop",
91 "swap",
92 "over",
93 "rot",
94 "nip",
95 "tuck",
96 "pick",
97 "roll",
98];
99
100impl CodeGen {
101 /// Check if a word can be specialized and return its signature if so
102 pub fn can_specialize(&self, word: &WordDef) -> Option<SpecSignature> {
103 // Must have an effect declaration
104 let effect = word.effect.as_ref()?;
105
106 // Must not have side effects (like Yield)
107 if !effect.is_pure() {
108 return None;
109 }
110
111 // Extract input/output types from the effect
112 let inputs = Self::extract_register_types(&effect.inputs)?;
113 let outputs = Self::extract_register_types(&effect.outputs)?;
114
115 // Must have at least one input or output to optimize
116 if inputs.is_empty() && outputs.is_empty() {
117 return None;
118 }
119
120 // Must have at least one output (zero outputs means side-effect only)
121 if outputs.is_empty() {
122 return None;
123 }
124
125 // Check that the body is specializable
126 if !self.is_body_specializable(&word.body, &word.name) {
127 return None;
128 }
129
130 Some(SpecSignature { inputs, outputs })
131 }
132
133 /// Extract register types from a stack type.
134 ///
135 /// The parser always adds a row variable for composability, so we accept
136 /// stack types with a row variable at the base and extract the concrete
137 /// types on top of it.
138 fn extract_register_types(stack: &StackType) -> Option<Vec<RegisterType>> {
139 let mut types = Vec::new();
140 let mut current = stack;
141
142 loop {
143 match current {
144 StackType::Empty => break,
145 StackType::RowVar(_) => {
146 // Row variable at the base is OK - we can specialize the
147 // concrete types on top of it. The row variable just means
148 // "whatever else is on the stack stays there".
149 break;
150 }
151 StackType::Cons { rest, top } => {
152 let reg_ty = RegisterType::from_type(top)?;
153 types.push(reg_ty);
154 current = rest;
155 }
156 }
157 }
158
159 // Reverse to get bottom-to-top order
160 types.reverse();
161 Some(types)
162 }
163
164 /// Check if a word body can be specialized.
165 ///
166 /// Tracks whether the previous statement was an integer literal, which is
167 /// required for `pick` and `roll` operations in specialized mode.
168 pub(super) fn is_body_specializable(&self, body: &[Statement], word_name: &str) -> bool {
169 let mut prev_was_int_literal = false;
170 for stmt in body {
171 if !self.is_statement_specializable(stmt, word_name, prev_was_int_literal) {
172 return false;
173 }
174 prev_was_int_literal = matches!(stmt, Statement::IntLiteral(_));
175 }
176 true
177 }
178
179 /// Check if a single statement can be specialized.
180 ///
181 /// `prev_was_int_literal` indicates whether the preceding statement was an
182 /// IntLiteral, which is required for `pick` and `roll` to be specializable.
183 fn is_statement_specializable(
184 &self,
185 stmt: &Statement,
186 word_name: &str,
187 prev_was_int_literal: bool,
188 ) -> bool {
189 match stmt {
190 Statement::IntLiteral(_) => true,
191 Statement::FloatLiteral(_) => true,
192 Statement::BoolLiteral(_) => true,
193 // Heap-backed literals cannot flow through registers.
194 Statement::StringLiteral(_) => false,
195 Statement::Symbol(_) => false,
196 Statement::Quotation { .. } => false,
197 Statement::Match { .. } => false,
198
199 Statement::WordCall { name, .. } => {
200 // Recursive calls to self are OK (we'll call specialized version)
201 if name == word_name {
202 return true;
203 }
204
205 // pick and roll require a compile-time constant N (previous int literal)
206 if (name == "pick" || name == "roll") && !prev_was_int_literal {
207 return false;
208 }
209
210 if SPECIALIZABLE_OPS.contains(&name.as_str()) {
211 return true;
212 }
213
214 if self.specialized_words.contains_key(name) {
215 return true;
216 }
217
218 false
219 }
220
221 Statement::If {
222 then_branch,
223 else_branch,
224 span: _,
225 } => {
226 if !self.is_body_specializable(then_branch, word_name) {
227 return false;
228 }
229 if let Some(else_stmts) = else_branch
230 && !self.is_body_specializable(else_stmts, word_name)
231 {
232 return false;
233 }
234 true
235 }
236 }
237 }
238}