Skip to main content

vyre_libs/parsing/c/preprocess/
gpu_define_parse.rs

1//! GPU `#define` row parser.
2//!
3//! Per `TOK_PREPROC` token classified as `TOK_PP_DEFINE`, extract the
4//! macro name byte span, optional arg-list byte span, and replacement
5//! body byte span. Per-thread, no inter-token state.
6//!
7//! ## Output columns (one row per token)
8//!
9//! - `name_start`, `name_len`         -  macro name byte span in `source`.
10//! - `args_start`, `args_len`         -  arg-list span (between the `(`
11//!                                     immediately after the name and
12//!                                     the matching `)`). `args_len = 0`
13//!                                     for object-like macros.
14//! - `body_start`, `body_len`         -  replacement body span (with
15//!                                     trailing horizontal whitespace
16//!                                     trimmed).
17//! - `is_function_like`               -  `1` if there was a `(`
18//!                                     immediately after the name, else 0.
19//!
20//! Non-DEFINE rows get all-zero output.
21//!
22//! ## Real-GPU lowering note
23//!
24//! Same conventions as the rest of the directive-classify family  -
25//! `source` is declared as packed U32 so reference-eval and naga-
26//! emitted real GPU agree on word-indexed access; byte extraction is
27//! inline. Fixed-width whitespace probes keep directive alignment cheap, while
28//! macro names and function-like argument lists are scanned with per-row GPU
29//! loops bounded by the directive token length. That keeps the compiled program
30//! shape independent of translation-unit size without truncating long
31//! clang-valid macro identifiers or parameter lists.
32
33use super::gpu_directive_parse_shared::{
34    directive_program_from_parse_with_source_layout, push_bounded_byte_scan_until,
35    push_c_identifier_span, push_directive_row_bounds, push_hash_and_keyword_start,
36    push_keyword_end, push_ws_skip_from_expr, safe_source_byte_expr,
37    trailing_ws_flag as is_trailing_ws, DirectiveOutputColumn, DirectiveSourceLayout,
38    DirectiveThreadLayout, MAX_DIRECTIVE_WS_PREFIX as MAX_WS_PREFIX,
39};
40use crate::parsing::c::lex::tokens::TOK_PP_DEFINE;
41use vyre_foundation::ir::{Expr, Node, Program};
42
43/// Canonical op id.
44pub const OP_ID: &str = "vyre-libs::parsing::c::preprocess::gpu_define_parse";
45
46/// Canonical binding for the input per-token start-offset buffer.
47pub const BINDING_TOK_STARTS: u32 = 0;
48/// Canonical binding for the input per-token length buffer.
49pub const BINDING_TOK_LENS: u32 = 1;
50/// Canonical binding for the input directive-kinds buffer.
51pub const BINDING_DIRECTIVE_KINDS: u32 = 2;
52/// Canonical binding for the input source bytes (packed U32).
53pub const BINDING_SOURCE: u32 = 3;
54/// Canonical binding for the output `name_start` column.
55pub const BINDING_NAME_START_OUT: u32 = 4;
56/// Canonical binding for the output `name_len` column.
57pub const BINDING_NAME_LEN_OUT: u32 = 5;
58/// Canonical binding for the output `args_start` column.
59pub const BINDING_ARGS_START_OUT: u32 = 6;
60/// Canonical binding for the output `args_len` column.
61pub const BINDING_ARGS_LEN_OUT: u32 = 7;
62/// Canonical binding for the output `body_start` column.
63pub const BINDING_BODY_START_OUT: u32 = 8;
64/// Canonical binding for the output `body_len` column.
65pub const BINDING_BODY_LEN_OUT: u32 = 9;
66/// Canonical binding for the output `is_function_like` column.
67pub const BINDING_IS_FUNCTION_LIKE_OUT: u32 = 10;
68
69const OUTPUT_COLUMNS: [DirectiveOutputColumn; 7] = [
70    DirectiveOutputColumn {
71        name: "name_start_out",
72        binding: BINDING_NAME_START_OUT,
73    },
74    DirectiveOutputColumn {
75        name: "name_len_out",
76        binding: BINDING_NAME_LEN_OUT,
77    },
78    DirectiveOutputColumn {
79        name: "args_start_out",
80        binding: BINDING_ARGS_START_OUT,
81    },
82    DirectiveOutputColumn {
83        name: "args_len_out",
84        binding: BINDING_ARGS_LEN_OUT,
85    },
86    DirectiveOutputColumn {
87        name: "body_start_out",
88        binding: BINDING_BODY_START_OUT,
89    },
90    DirectiveOutputColumn {
91        name: "body_len_out",
92        binding: BINDING_BODY_LEN_OUT,
93    },
94    DirectiveOutputColumn {
95        name: "is_function_like_out",
96        binding: BINDING_IS_FUNCTION_LIKE_OUT,
97    },
98];
99
100/// Length of the `define` keyword (6 bytes), used to step past it.
101const DEFINE_KW_LEN: u32 = 6;
102
103/// Build the `#define` row parser `Program`.
104///
105/// `num_tokens` is kept ONLY to size the host-allocated output buffers
106/// (the CUDA backend rejects readback when output buffers don't have a
107/// static byte length). The kernel BODY itself uses `Expr::buf_len()` for
108/// every per-thread bound  -  so the program AST is independent of the
109/// host's input/source size and the dispatcher's pipeline cache hits
110#[must_use]
111pub fn gpu_define_parse(num_tokens: u32, source_len: u32) -> Program {
112    gpu_define_parse_with_source_layout(num_tokens, source_len, DirectiveSourceLayout::PackedU32)
113}
114
115/// Build the `#define` row parser over raw `DataType::U8` source bytes.
116#[must_use]
117pub fn gpu_define_parse_u8(num_tokens: u32, source_len: u32) -> Program {
118    gpu_define_parse_with_source_layout(num_tokens, source_len, DirectiveSourceLayout::RawU8)
119}
120
121fn gpu_define_parse_with_source_layout(
122    num_tokens: u32,
123    source_len: u32,
124    source_layout: DirectiveSourceLayout,
125) -> Program {
126    let t = Expr::var("t");
127    let safe_load = |addr: Expr| safe_source_byte_expr(source_layout, addr);
128
129    let mut parse: Vec<Node> = Vec::new();
130    push_directive_row_bounds(&mut parse);
131    push_hash_and_keyword_start(&mut parse, source_layout);
132    push_keyword_end(&mut parse, Expr::u32(DEFINE_KW_LEN));
133    push_ws_skip_from_expr(
134        &mut parse,
135        source_layout,
136        "np",
137        Expr::var("post_kw"),
138        "name_skip",
139        "name_start_val",
140    );
141    push_c_identifier_span(
142        &mut parse,
143        source_layout,
144        "name_start_val",
145        "name_len_val",
146        "name_done",
147    );
148
149    // ---- Step 5: function-like check (next byte after name is `(`?) ----
150    parse.push(Node::let_bind(
151        "after_name_idx",
152        Expr::add(Expr::var("name_start_val"), Expr::var("name_len_val")),
153    ));
154    parse.push(Node::let_bind(
155        "after_name_byte",
156        safe_load(Expr::var("after_name_idx")),
157    ));
158    parse.push(Node::let_bind(
159        "is_func_val",
160        Expr::select(
161            Expr::eq(Expr::var("after_name_byte"), Expr::u32(b'(' as u32)),
162            Expr::u32(1),
163            Expr::u32(0),
164        ),
165    ));
166
167    // ---- Step 6: scan args bytes for first `)` (function-like only) ----
168    // args_start_val_raw = after_name_idx + 1 (past the `(`). For
169    // object-like macros this position is meaningless; we mask the
170    // output stores below behind `is_func_val == 1`.
171    parse.push(Node::let_bind(
172        "args_start_val_raw",
173        Expr::add(Expr::var("after_name_idx"), Expr::u32(1)),
174    ));
175    push_bounded_byte_scan_until(
176        &mut parse,
177        source_layout,
178        "args_i",
179        "args_start_val_raw",
180        "args_scan_limit",
181        "args_byte",
182        "args_len_val_raw",
183        "args_done",
184        Expr::u32(b')' as u32),
185        Expr::eq(Expr::var("is_func_val"), Expr::u32(1)),
186    );
187
188    // ---- Step 7: body span ----
189    // body_pre_start = position right after the closing `)` for
190    // function-like macros; right after the name otherwise.
191    parse.push(Node::let_bind(
192        "body_pre_start",
193        Expr::select(
194            Expr::eq(Expr::var("is_func_val"), Expr::u32(1)),
195            Expr::select(
196                Expr::eq(Expr::var("args_done"), Expr::u32(1)),
197                Expr::add(
198                    Expr::add(
199                        Expr::var("args_start_val_raw"),
200                        Expr::var("args_len_val_raw"),
201                    ),
202                    Expr::u32(1),
203                ),
204                Expr::var("tok_end"),
205            ),
206            Expr::var("after_name_idx"),
207        ),
208    ));
209    // Skip horizontal WS between `)` (or name) and the start of the body.
210    push_ws_skip_from_expr(
211        &mut parse,
212        source_layout,
213        "bp",
214        Expr::var("body_pre_start"),
215        "body_skip",
216        "body_start_val",
217    );
218
219    // ---- Step 8: trim trailing whitespace (incl. \n/\r) from the body ----
220    // We probe the LAST MAX_WS_PREFIX bytes of the row and count a
221    // trailing-WS run. The body length is `tok_end - body_start_val -
222    // trailing_ws_count` clamped to >= 0.
223    for q in 0..MAX_WS_PREFIX {
224        // tb_q = byte at tok_end - 1 - q (last byte first when q=0).
225        parse.push(Node::let_bind(
226            format!("tb_{q}"),
227            Expr::select(
228                Expr::lt(
229                    Expr::add(Expr::var("body_start_val"), Expr::u32(q + 1)),
230                    Expr::add(Expr::var("tok_end"), Expr::u32(1)),
231                ),
232                safe_load(Expr::sub(Expr::var("tok_end"), Expr::u32(q + 1))),
233                Expr::u32(0),
234            ),
235        ));
236    }
237    for q in 0..MAX_WS_PREFIX {
238        parse.push(Node::let_bind(
239            format!("tb_ws_{q}"),
240            is_trailing_ws(Expr::var(format!("tb_{q}"))),
241        ));
242    }
243    // trailing_ws_count = first q in [0, MAX_WS_PREFIX) where tb_ws_q
244    // == 0 (the run of trailing WS bytes). Same chained-Select shape
245    // as `ws_skip_expr` but reading the `tb_ws_*` bindings.
246    let trailing_ws_expr = {
247        let mut acc = Expr::u32(MAX_WS_PREFIX);
248        for q in (0..MAX_WS_PREFIX).rev() {
249            let mut prefix_ws = Expr::u32(1);
250            for r in 0..q {
251                prefix_ws = Expr::bitand(prefix_ws, Expr::var(format!("tb_ws_{r}")));
252            }
253            let tb_q_not_ws = Expr::select(
254                Expr::eq(Expr::var(format!("tb_ws_{q}")), Expr::u32(0)),
255                Expr::u32(1),
256                Expr::u32(0),
257            );
258            let cond_u32 = Expr::bitand(tb_q_not_ws, prefix_ws);
259            acc = Expr::select(Expr::eq(cond_u32, Expr::u32(1)), Expr::u32(q), acc);
260        }
261        acc
262    };
263    parse.push(Node::let_bind("trailing_ws_count", trailing_ws_expr));
264    // body_len_val = max(0, (tok_end - trailing_ws_count) - body_start_val).
265    parse.push(Node::let_bind(
266        "body_end_trimmed",
267        Expr::sub(Expr::var("tok_end"), Expr::var("trailing_ws_count")),
268    ));
269    parse.push(Node::let_bind(
270        "body_len_val",
271        Expr::select(
272            Expr::lt(Expr::var("body_start_val"), Expr::var("body_end_trimmed")),
273            Expr::sub(Expr::var("body_end_trimmed"), Expr::var("body_start_val")),
274            Expr::u32(0),
275        ),
276    ));
277
278    // ---- Step 9: commit ----
279    // Stores fire only when found_hash == 1. The `is_func` masking
280    // for args fields is handled by storing 0 unconditionally for
281    // non-function-like rows.
282    parse.push(Node::if_then(
283        Expr::and(
284            Expr::eq(Expr::var("found_hash"), Expr::u32(1)),
285            Expr::gt(Expr::var("name_len_val"), Expr::u32(0)),
286        ),
287        vec![
288            Node::store("name_start_out", t.clone(), Expr::var("name_start_val")),
289            Node::store("name_len_out", t.clone(), Expr::var("name_len_val")),
290            Node::store("body_start_out", t.clone(), Expr::var("body_start_val")),
291            Node::store("body_len_out", t.clone(), Expr::var("body_len_val")),
292            Node::store("is_function_like_out", t.clone(), Expr::var("is_func_val")),
293            Node::if_then(
294                Expr::and(
295                    Expr::eq(Expr::var("is_func_val"), Expr::u32(1)),
296                    Expr::eq(Expr::var("args_done"), Expr::u32(1)),
297                ),
298                vec![
299                    Node::store("args_start_out", t.clone(), Expr::var("args_start_val_raw")),
300                    Node::store("args_len_out", t.clone(), Expr::var("args_len_val_raw")),
301                ],
302            ),
303        ],
304    ));
305
306    directive_program_from_parse_with_source_layout(
307        OP_ID,
308        num_tokens,
309        source_len,
310        source_layout,
311        &OUTPUT_COLUMNS,
312        DirectiveThreadLayout::InvocationId,
313        Expr::eq(Expr::var("kind"), Expr::u32(TOK_PP_DEFINE)),
314        parse,
315    )
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use vyre_foundation::ir::DataType;
322
323    #[test]
324    fn op_id_is_canonical_and_stable() {
325        assert_eq!(OP_ID, "vyre-libs::parsing::c::preprocess::gpu_define_parse");
326    }
327
328    #[test]
329    fn build_program_returns_well_formed_program() {
330        let p = gpu_define_parse(8, 64);
331        assert_eq!(p.buffers().len(), 11);
332        assert_eq!(p.workgroup_size(), [256, 1, 1]);
333    }
334
335    #[test]
336    fn source_buffer_layouts_preserve_packed_abi_and_raw_u8_variant() {
337        let packed = gpu_define_parse(8, 64);
338        let raw_u8 = gpu_define_parse_u8(8, 64);
339        let packed_source = packed
340            .buffers()
341            .iter()
342            .find(|buffer| buffer.name() == "source")
343            .expect("Fix: packed define parser source buffer must exist");
344        let raw_u8_source = raw_u8
345            .buffers()
346            .iter()
347            .find(|buffer| buffer.name() == "source")
348            .expect("Fix: raw-U8 define parser source buffer must exist");
349
350        assert_eq!(packed_source.element(), DataType::U32);
351        assert_ne!(packed_source.count(), 0);
352        assert_eq!(raw_u8_source.element(), DataType::U8);
353        assert_eq!(raw_u8_source.count(), 0);
354    }
355}