Skip to main content

logicaffeine_compile/codegen_c/
cshake.rs

1//! Structural reachability over an emitted C translation unit — split the runtime into top-level
2//! definition UNITS, then keep only those reachable (transitively) from a set of root symbols.
3//!
4//! This is the shared tree-shaking pass for the C-derived tiers. The CUDA device-lift uses it so
5//! `nvcc` only compiles the slice a program actually references (a float one-liner pulls in ~3
6//! functions, not the whole ~1900-line runtime); the same pass can later shrink the C / Tenstorrent
7//! output. It is an OVER-APPROXIMATION (a body's every identifier is treated as a dependency), so it
8//! never drops a needed unit; and it is verified downstream — a genuinely missed edge surfaces as a
9//! compiler "undefined symbol", never a silent miscompile.
10//!
11//! The parser is a small byte scanner that understands C string/char/line/block-comment lexemes (so a
12//! brace inside `"{"` does not throw off nesting) and the three top-level shapes the runtime uses:
13//! `typedef … ;`, `static … (…) { … }` / `… (…) ;` functions, and file-scope globals.
14
15use std::collections::{BTreeSet, VecDeque};
16
17/// One top-level C construct: a typedef, a `static` function definition or forward declaration, or a
18/// file-scope global. `defined` is the symbol(s) it introduces; `references` is every identifier it
19/// mentions (an over-approximation of its dependencies); `text` is the exact source span (with any
20/// leading comment/blank lines attached, so units travel with their doc comment).
21#[derive(Debug, Clone)]
22pub struct CUnit {
23    pub defined: Vec<String>,
24    pub references: BTreeSet<String>,
25    pub text: String,
26    pub kind: UnitKind,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum UnitKind {
31    Typedef,
32    /// A `static … (…) { … }` definition or a `… (…);` forward declaration.
33    Function,
34    /// A file-scope `static`/`const` global (data, not a function).
35    Global,
36    /// A `#…` preprocessor line — carried through the splitter but never a dependency target.
37    Preproc,
38}
39
40/// Collect every C identifier in `s`, skipping string/char literals and comments.
41pub fn c_identifiers(s: &str) -> BTreeSet<String> {
42    let b = s.as_bytes();
43    let mut out = BTreeSet::new();
44    let mut i = 0;
45    while i < b.len() {
46        let c = b[i];
47        match c {
48            b'"' => {
49                i += 1;
50                while i < b.len() && b[i] != b'"' {
51                    if b[i] == b'\\' {
52                        i += 1;
53                    }
54                    i += 1;
55                }
56                i += 1;
57            }
58            b'\'' => {
59                i += 1;
60                while i < b.len() && b[i] != b'\'' {
61                    if b[i] == b'\\' {
62                        i += 1;
63                    }
64                    i += 1;
65                }
66                i += 1;
67            }
68            b'/' if i + 1 < b.len() && b[i + 1] == b'/' => {
69                while i < b.len() && b[i] != b'\n' {
70                    i += 1;
71                }
72            }
73            b'/' if i + 1 < b.len() && b[i + 1] == b'*' => {
74                i += 2;
75                while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
76                    i += 1;
77                }
78                i += 2;
79            }
80            _ if c == b'_' || c.is_ascii_alphabetic() => {
81                let start = i;
82                while i < b.len() && (b[i] == b'_' || b[i].is_ascii_alphanumeric()) {
83                    i += 1;
84                }
85                out.insert(s[start..i].to_string());
86            }
87            _ => i += 1,
88        }
89    }
90    out
91}
92
93/// A byte cursor that steps one lexeme at a time, reporting the significant structural bytes (`{`,
94/// `}`, `;`, `(`, `)`, `=`, `#`) at brace/paren depth while skipping over string/char/comment content.
95struct Scan<'a> {
96    b: &'a [u8],
97    i: usize,
98}
99
100impl<'a> Scan<'a> {
101    /// Advance past one lexeme starting at `self.i`. Returns the significant byte if this lexeme is a
102    /// bare structural char (else `None` for identifiers/whitespace/strings/comments). Always makes
103    /// progress.
104    fn step(&mut self) -> Option<u8> {
105        let b = self.b;
106        let c = b[self.i];
107        match c {
108            b'"' => {
109                self.i += 1;
110                while self.i < b.len() && b[self.i] != b'"' {
111                    if b[self.i] == b'\\' {
112                        self.i += 1;
113                    }
114                    self.i += 1;
115                }
116                self.i += 1;
117                None
118            }
119            b'\'' => {
120                self.i += 1;
121                while self.i < b.len() && b[self.i] != b'\'' {
122                    if b[self.i] == b'\\' {
123                        self.i += 1;
124                    }
125                    self.i += 1;
126                }
127                self.i += 1;
128                None
129            }
130            b'/' if self.i + 1 < b.len() && b[self.i + 1] == b'/' => {
131                while self.i < b.len() && b[self.i] != b'\n' {
132                    self.i += 1;
133                }
134                None
135            }
136            b'/' if self.i + 1 < b.len() && b[self.i + 1] == b'*' => {
137                self.i += 2;
138                while self.i + 1 < b.len() && !(b[self.i] == b'*' && b[self.i + 1] == b'/') {
139                    self.i += 1;
140                }
141                self.i += 2;
142                None
143            }
144            _ if c == b'_' || c.is_ascii_alphabetic() => {
145                while self.i < b.len() && (b[self.i] == b'_' || b[self.i].is_ascii_alphanumeric()) {
146                    self.i += 1;
147                }
148                None
149            }
150            _ => {
151                self.i += 1;
152                Some(c)
153            }
154        }
155    }
156}
157
158/// Split an emitted C translation unit into top-level [`CUnit`]s. Leading comments/blank lines attach
159/// to the unit that follows. `#…` preprocessor directives, backslash-continued lines included, are their own
160/// [`UnitKind::Preproc`] units.
161pub fn parse_c_units(src: &str) -> Vec<CUnit> {
162    let b = src.as_bytes();
163    let mut units = Vec::new();
164    let mut unit_start = 0usize; // includes leading ws/comments
165    let mut i = 0usize;
166    // Find where the current unit's *code* begins (first significant, non-ws/comment/`#`).
167    while i < b.len() {
168        // Skip whitespace.
169        while i < b.len() && (b[i] as char).is_whitespace() {
170            i += 1;
171        }
172        if i >= b.len() {
173            break;
174        }
175        // A comment before a definition — skip it (stays attached via `unit_start`).
176        if b[i] == b'/' && i + 1 < b.len() && (b[i + 1] == b'/' || b[i + 1] == b'*') {
177            let mut sc = Scan { b, i };
178            sc.step();
179            i = sc.i;
180            continue;
181        }
182        // A preprocessor directive is its own unit (dropped downstream: no defined symbol). It ends at the
183        // first newline no backslash escapes, so a macro continued over lines stays whole.
184        if b[i] == b'#' {
185            let ls = unit_start;
186            let continued = |nl: usize| {
187                nl > 0
188                    && (b[nl - 1] == b'\\' || (nl > 1 && b[nl - 1] == b'\r' && b[nl - 2] == b'\\'))
189            };
190            while i < b.len() && (b[i] != b'\n' || continued(i)) {
191                i += 1;
192            }
193            if i < b.len() {
194                i += 1;
195            }
196            let text = src[ls..i].to_string();
197            units.push(CUnit {
198                defined: vec![],
199                references: BTreeSet::new(),
200                text,
201                kind: UnitKind::Preproc,
202            });
203            unit_start = i;
204            continue;
205        }
206        // The code of a real definition begins here. Consume to its terminator.
207        let code_start = i;
208        let starts_typedef = src[code_start..].trim_start().starts_with("typedef");
209        let mut sc = Scan { b, i };
210        let mut depth = 0i32;
211        // For a function DEF the terminating `}` returns depth to 0 and its matching `{` was preceded
212        // by `)`. Track the byte just before each depth 0→1 `{` to tell a body from an initializer/struct.
213        let mut last_sig_before_brace: u8 = 0;
214        let mut last_sig: u8 = 0;
215        let mut end = b.len();
216        while sc.i < b.len() {
217            let at = sc.i;
218            if let Some(ch) = sc.step() {
219                match ch {
220                    b'{' => {
221                        if depth == 0 {
222                            last_sig_before_brace = last_sig;
223                        }
224                        depth += 1;
225                    }
226                    b'}' => {
227                        depth -= 1;
228                        if depth == 0 && !starts_typedef && last_sig_before_brace == b')' {
229                            // Function body just closed — unit ends here (no trailing `;`).
230                            end = sc.i;
231                            break;
232                        }
233                    }
234                    b';' if depth == 0 => {
235                        end = sc.i;
236                        break;
237                    }
238                    _ => {}
239                }
240                if !(ch as char).is_whitespace() {
241                    last_sig = ch;
242                }
243                let _ = at;
244            }
245        }
246        let text = src[unit_start..end].to_string();
247        let code = &src[code_start..end];
248        let (defined, kind) = classify_unit(code, starts_typedef);
249        units.push(CUnit {
250            defined,
251            references: c_identifiers(code),
252            text,
253            kind,
254        });
255        i = end;
256        unit_start = end;
257    }
258    units
259}
260
261/// Extract the defined symbol name(s) and the kind of a unit from its code span (no leading comments).
262fn classify_unit(code: &str, starts_typedef: bool) -> (Vec<String>, UnitKind) {
263    if starts_typedef {
264        // `typedef … Name;` — the LAST identifier in the span. Handles `typedef struct {…} Name;`
265        // (whose inner `;`s sit inside the braces) and plain `typedef T Name;`. The runtime uses no
266        // function-pointer typedefs (no `typedef R (*Name)(…);`), so last-identifier is exact.
267        let name = last_ident(code);
268        return (name.into_iter().collect(), UnitKind::Typedef);
269    }
270    // `struct Name { … };` — a struct defined apart from its typedef defines its tag, the name a
271    // `typedef struct Name Name;` elsewhere gives it.
272    if let Some(tag) = struct_definition_tag(code) {
273        return (vec![tag], UnitKind::Typedef);
274    }
275    // A function: the identifier immediately before the first top-level `(`.
276    if let Some(name) = ident_before_first_paren(code) {
277        return (vec![name], UnitKind::Function);
278    }
279    // Otherwise a global: the last identifier before the first `;`/`=`/`[`.
280    let name = global_name(code);
281    (name.into_iter().collect(), UnitKind::Global)
282}
283
284/// The tag of a `struct Tag { … };` definition: the code begins `struct Tag` and its body opens before any `(`.
285fn struct_definition_tag(code: &str) -> Option<String> {
286    let rest = code.trim_start().strip_prefix("struct")?;
287    if !rest.starts_with(|c: char| c.is_whitespace()) {
288        return None;
289    }
290    let rest = rest.trim_start();
291    let end = rest
292        .find(|c: char| !(c == '_' || c.is_ascii_alphanumeric()))
293        .unwrap_or(rest.len());
294    let (tag, after) = rest.split_at(end);
295    let brace = after.find('{')?;
296    if tag.is_empty() || after[..brace].trim() != "" || after[..brace].contains('(') {
297        return None;
298    }
299    Some(tag.to_string())
300}
301
302fn ident_before_first_paren(code: &str) -> Option<String> {
303    let b = code.as_bytes();
304    let mut sc = Scan { b, i: 0 };
305    let mut last_ident: Option<(usize, usize)> = None;
306    while sc.i < b.len() {
307        let start = sc.i;
308        let c = b[sc.i];
309        if c == b'_' || c.is_ascii_alphabetic() {
310            sc.step();
311            last_ident = Some((start, sc.i));
312        } else if c == b'(' {
313            // A GCC attribute specifier `__attribute__((…))` opens a `(` that is NOT the parameter
314            // list — skip its balanced parens so the real function name (which follows) is found.
315            let is_attr = last_ident.map(|(s, e)| &code[s..e]) == Some("__attribute__");
316            sc.step(); // consume the '('
317            if is_attr {
318                let mut depth = 1i32;
319                while sc.i < b.len() && depth > 0 {
320                    match sc.step() {
321                        Some(b'(') => depth += 1,
322                        Some(b')') => depth -= 1,
323                        _ => {}
324                    }
325                }
326                last_ident = None;
327                continue;
328            }
329            return last_ident.map(|(s, e)| code[s..e].to_string());
330        } else {
331            sc.step();
332        }
333    }
334    None
335}
336
337fn last_ident(code: &str) -> Option<String> {
338    let b = code.as_bytes();
339    let mut sc = Scan { b, i: 0 };
340    let mut last: Option<(usize, usize)> = None;
341    while sc.i < b.len() {
342        let start = sc.i;
343        let c = b[sc.i];
344        if c == b'_' || c.is_ascii_alphabetic() {
345            sc.step();
346            last = Some((start, sc.i));
347        } else {
348            sc.step();
349        }
350    }
351    last.map(|(s, e)| code[s..e].to_string())
352}
353
354fn global_name(code: &str) -> Option<String> {
355    let b = code.as_bytes();
356    let mut sc = Scan { b, i: 0 };
357    let mut last_ident: Option<(usize, usize)> = None;
358    while sc.i < b.len() {
359        let start = sc.i;
360        let c = b[sc.i];
361        if c == b'_' || c.is_ascii_alphabetic() {
362            sc.step();
363            last_ident = Some((start, sc.i));
364        } else if c == b';' || c == b'=' || c == b'[' {
365            break;
366        } else {
367            sc.step();
368        }
369    }
370    last_ident.map(|(s, e)| code[s..e].to_string())
371}
372
373/// A `#define`'s macro name and the names its replacement mentions, its own parameters left out: a macro is
374/// expanded where it is named, so what it names is reached through it. `None` for any other directive. A
375/// preprocessor unit is its one directive behind whatever comments [`parse_c_units`] attached to it.
376pub fn macro_references(unit: &CUnit) -> Option<(String, BTreeSet<String>)> {
377    if unit.kind != UnitKind::Preproc {
378        return None;
379    }
380    let mut text = unit.text.as_str();
381    loop {
382        text = text.trim_start();
383        if let Some(line) = text.strip_prefix("//") {
384            text = line.split_once('\n').map_or("", |(_, rest)| rest);
385        } else if let Some(block) = text.strip_prefix("/*") {
386            text = block.split_once("*/").map_or("", |(_, rest)| rest);
387        } else {
388            break;
389        }
390    }
391    let directive = text.strip_prefix('#')?.trim_start();
392    let rest = directive.strip_prefix("define")?;
393    if !rest.starts_with(|c: char| c.is_whitespace()) {
394        return None;
395    }
396    let rest = rest.trim_start();
397    let end = rest
398        .find(|c: char| c != '_' && !c.is_ascii_alphanumeric())
399        .unwrap_or(rest.len());
400    let name = &rest[..end];
401    if name.is_empty() {
402        return None;
403    }
404    let after = &rest[end..];
405    let (params, replacement): (BTreeSet<&str>, &str) = match after.strip_prefix('(') {
406        Some(list) => {
407            let close = list.find(')').unwrap_or(list.len());
408            (
409                list[..close].split(',').map(str::trim).collect(),
410                &list[(close + 1).min(list.len())..],
411            )
412        }
413        None => (BTreeSet::new(), after),
414    };
415    let references = c_identifiers(replacement)
416        .into_iter()
417        .filter(|r| r != name && !params.contains(r.as_str()))
418        .collect();
419    Some((name.to_string(), references))
420}
421
422/// Indices (in original order) of the units transitively reachable from `roots`. A unit is reached
423/// when any symbol it defines is in the frontier; reaching it adds every identifier it references.
424pub fn reachable_units(units: &[CUnit], roots: &BTreeSet<String>) -> Vec<usize> {
425    // symbol → indices of units that define it (a symbol may have a forward decl + a definition).
426    let mut by_symbol: std::collections::HashMap<&str, Vec<usize>> =
427        std::collections::HashMap::new();
428    for (idx, u) in units.iter().enumerate() {
429        for d in &u.defined {
430            by_symbol.entry(d.as_str()).or_default().push(idx);
431        }
432    }
433    let mut wanted: BTreeSet<usize> = BTreeSet::new();
434    let mut queue: VecDeque<String> = roots.iter().cloned().collect();
435    let mut seen_sym: BTreeSet<String> = roots.iter().cloned().collect();
436    while let Some(sym) = queue.pop_front() {
437        if let Some(idxs) = by_symbol.get(sym.as_str()) {
438            for &idx in idxs {
439                if wanted.insert(idx) {
440                    for r in &units[idx].references {
441                        if seen_sym.insert(r.clone()) {
442                            queue.push_back(r.clone());
443                        }
444                    }
445                }
446            }
447        }
448    }
449    wanted.into_iter().collect()
450}
451
452#[cfg(test)]
453#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_c/cshake/tests.rs"]
454mod tests;