Skip to main content

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}