Skip to main content

vyre_libs/parsing/c/preprocess/
effects.rs

1//! Side-effect metadata for preprocessor directives.
2
3#[cfg(any(test, feature = "cpu-parity"))]
4use crate::parsing::c::lex::tokens::TOK_PREPROC;
5use crate::parsing::c::lex::tokens::{
6    TOK_PP_EFFECT_ERROR_DIAGNOSTIC, TOK_PP_EFFECT_IDENT, TOK_PP_EFFECT_INCLUDE,
7    TOK_PP_EFFECT_INCLUDE_NEXT, TOK_PP_EFFECT_LINE, TOK_PP_EFFECT_PRAGMA,
8    TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_ERROR, TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_IGNORED,
9    TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_POP, TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_PUSH,
10    TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_WARNING, TOK_PP_EFFECT_PRAGMA_ONCE, TOK_PP_EFFECT_SCCS,
11    TOK_PP_EFFECT_WARNING_DIAGNOSTIC,
12};
13#[cfg(any(test, feature = "cpu-parity"))]
14use crate::parsing::c::preprocess::c_logical_directive_len;
15use crate::parsing::c::preprocess::{
16    c_directive_payload, c_translation_phase_line_splice, try_classify_preprocessor_directive,
17    CPreprocessorDirectiveKind, CPreprocessorError,
18};
19
20/// Stable side-effect kind emitted for directives with frontend-visible state.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum CPreprocessorSideEffectKind {
23    /// `#include`.
24    Include,
25    /// GNU `#include_next`.
26    IncludeNext,
27    /// Unclassified pragma with implementation-defined payload.
28    Pragma,
29    /// `#pragma once`.
30    PragmaOnce,
31    /// `#pragma GCC diagnostic push` or `#pragma clang diagnostic push`.
32    PragmaDiagnosticPush,
33    /// `#pragma GCC diagnostic pop` or `#pragma clang diagnostic pop`.
34    PragmaDiagnosticPop,
35    /// `#pragma GCC diagnostic ignored`.
36    PragmaDiagnosticIgnored,
37    /// `#pragma GCC diagnostic warning`.
38    PragmaDiagnosticWarning,
39    /// `#pragma GCC diagnostic error`.
40    PragmaDiagnosticError,
41    /// `#error`.
42    ErrorDiagnostic,
43    /// GNU `#warning`.
44    WarningDiagnostic,
45    /// `#ident`.
46    Ident,
47    /// `#sccs`.
48    Sccs,
49    /// `#line`.
50    Line,
51}
52
53impl CPreprocessorSideEffectKind {
54    /// Return the stable side-effect metadata token ID.
55    #[must_use]
56    pub const fn token_id(self) -> u32 {
57        match self {
58            Self::Include => TOK_PP_EFFECT_INCLUDE,
59            Self::IncludeNext => TOK_PP_EFFECT_INCLUDE_NEXT,
60            Self::Pragma => TOK_PP_EFFECT_PRAGMA,
61            Self::PragmaOnce => TOK_PP_EFFECT_PRAGMA_ONCE,
62            Self::PragmaDiagnosticPush => TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_PUSH,
63            Self::PragmaDiagnosticPop => TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_POP,
64            Self::PragmaDiagnosticIgnored => TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_IGNORED,
65            Self::PragmaDiagnosticWarning => TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_WARNING,
66            Self::PragmaDiagnosticError => TOK_PP_EFFECT_PRAGMA_DIAGNOSTIC_ERROR,
67            Self::ErrorDiagnostic => TOK_PP_EFFECT_ERROR_DIAGNOSTIC,
68            Self::WarningDiagnostic => TOK_PP_EFFECT_WARNING_DIAGNOSTIC,
69            Self::Ident => TOK_PP_EFFECT_IDENT,
70            Self::Sccs => TOK_PP_EFFECT_SCCS,
71            Self::Line => TOK_PP_EFFECT_LINE,
72        }
73    }
74}
75
76/// Source-positioned side effect decoded from one directive row.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct CPreprocessorSideEffect {
79    /// Classified side-effect kind.
80    pub kind: CPreprocessorSideEffectKind,
81    /// Original source offset of the directive payload.
82    pub payload_start: usize,
83    /// Original byte length of the directive payload.
84    pub payload_len: usize,
85    /// Original source offset of the actionable detail payload.
86    pub detail_start: usize,
87    /// Original byte length of the actionable detail payload.
88    pub detail_len: usize,
89}
90
91/// Parallel metadata streams for preprocessor side effects.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct CPreprocessorSideEffectMetadata {
94    /// Side-effect token ID per input token, or zero for no side effect.
95    pub kinds: Vec<u32>,
96    /// Original source offset of the directive payload per input token.
97    pub payload_starts: Vec<u32>,
98    /// Original byte length of the directive payload per input token.
99    pub payload_lens: Vec<u32>,
100    /// Original source offset of the detail payload per input token.
101    pub detail_starts: Vec<u32>,
102    /// Original byte length of the detail payload per input token.
103    pub detail_lens: Vec<u32>,
104}
105
106/// Decode pragma, include, and diagnostic side effects from one directive row.
107///
108/// # Errors
109///
110/// Returns a diagnostic when the row is not a directive row or uses a malformed
111/// recognized side-effect spelling.
112pub fn classify_c_preprocessor_side_effect(
113    row: &[u8],
114    directive_offset: usize,
115) -> Result<Option<CPreprocessorSideEffect>, CPreprocessorError> {
116    let spliced = c_translation_phase_line_splice(row);
117    let directive = try_classify_preprocessor_directive(&spliced.bytes).map_err(|mut err| {
118        err.offset = directive_offset + spliced.original_offset(err.offset);
119        err
120    })?;
121    let payload = c_directive_payload(&spliced.bytes, directive).map_err(|mut err| {
122        err.offset = directive_offset + spliced.original_offset(err.offset);
123        err
124    })?;
125    let payload_start = directive_offset + spliced.original_offset(directive.payload_start);
126    let payload_end = directive_offset + spliced.original_offset(directive.logical_end);
127    let payload_len = payload_end.saturating_sub(payload_start);
128
129    let (kind, detail_rel, detail_len) = match directive.kind {
130        CPreprocessorDirectiveKind::Include => (
131            CPreprocessorSideEffectKind::Include,
132            first_payload_byte(payload),
133            payload_trimmed_len(payload),
134        ),
135        CPreprocessorDirectiveKind::IncludeNext => (
136            CPreprocessorSideEffectKind::IncludeNext,
137            first_payload_byte(payload),
138            payload_trimmed_len(payload),
139        ),
140        CPreprocessorDirectiveKind::Pragma => {
141            classify_pragma_payload(payload).map_err(|mut err| {
142                err.offset = directive_offset
143                    + spliced.original_offset(directive.payload_start + err.offset);
144                err
145            })?
146        }
147        CPreprocessorDirectiveKind::Error => (
148            CPreprocessorSideEffectKind::ErrorDiagnostic,
149            first_payload_byte(payload),
150            payload_trimmed_len(payload),
151        ),
152        CPreprocessorDirectiveKind::Warning => (
153            CPreprocessorSideEffectKind::WarningDiagnostic,
154            first_payload_byte(payload),
155            payload_trimmed_len(payload),
156        ),
157        CPreprocessorDirectiveKind::Ident => (
158            CPreprocessorSideEffectKind::Ident,
159            first_payload_byte(payload),
160            payload_trimmed_len(payload),
161        ),
162        CPreprocessorDirectiveKind::Sccs => (
163            CPreprocessorSideEffectKind::Sccs,
164            first_payload_byte(payload),
165            payload_trimmed_len(payload),
166        ),
167        CPreprocessorDirectiveKind::Line => (
168            CPreprocessorSideEffectKind::Line,
169            first_payload_byte(payload),
170            payload_trimmed_len(payload),
171        ),
172        _ => return Ok(None),
173    };
174
175    Ok(Some(CPreprocessorSideEffect {
176        kind,
177        payload_start,
178        payload_len,
179        detail_start: directive_offset
180            + spliced.original_offset(directive.payload_start + detail_rel),
181        detail_len,
182    }))
183}
184
185/// Build side-effect metadata for compact C tokens.
186///
187/// # Errors
188///
189/// Returns a diagnostic when token streams are inconsistent or a directive span
190/// is invalid.
191#[deprecated(
192    note = "CPU reference oracle only; production side-effect metadata must use GPU preprocessor classification"
193)]
194#[cfg(any(test, feature = "cpu-parity"))]
195pub fn reference_c_preprocessor_side_effect_metadata(
196    tok_types: &[u32],
197    tok_starts: &[u32],
198    tok_lens: &[u32],
199    source: &[u8],
200) -> Result<CPreprocessorSideEffectMetadata, CPreprocessorError> {
201    if tok_types.len() != tok_starts.len() || tok_types.len() != tok_lens.len() {
202        return Err(CPreprocessorError {
203            offset: tok_types.len().min(tok_starts.len()).min(tok_lens.len()),
204            message: "Fix: token type/start/length streams must have identical lengths",
205        });
206    }
207
208    let mut metadata = CPreprocessorSideEffectMetadata {
209        kinds: vec![0; tok_types.len()],
210        payload_starts: vec![0; tok_types.len()],
211        payload_lens: vec![0; tok_types.len()],
212        detail_starts: vec![0; tok_types.len()],
213        detail_lens: vec![0; tok_types.len()],
214    };
215    for (idx, ((tok_type, start), len)) in
216        tok_types.iter().zip(tok_starts).zip(tok_lens).enumerate()
217    {
218        if *tok_type != TOK_PREPROC {
219            continue;
220        }
221        let start = usize::try_from(*start).map_err(|_| CPreprocessorError {
222            offset: idx,
223            message: "Fix: token start does not fit host usize",
224        })?;
225        let len = usize::try_from(*len).map_err(|_| CPreprocessorError {
226            offset: idx,
227            message: "Fix: token length does not fit host usize",
228        })?;
229        let token_end = start.checked_add(len).ok_or(CPreprocessorError {
230            offset: start,
231            message: "Fix: token span overflows source address space",
232        })?;
233        let logical_len = c_logical_directive_len(source, start);
234        if logical_len > len {
235            return Err(CPreprocessorError {
236                offset: start + len,
237                message:
238                    "Fix: TOK_PREPROC span must include the full phase-2 spliced directive row",
239            });
240        }
241        if token_end > source.len() {
242            return Err(CPreprocessorError {
243                offset: start,
244                message: "Fix: preprocessor token span must be inside the source buffer",
245            });
246        }
247        let logical_end = start.checked_add(logical_len).ok_or(CPreprocessorError {
248            offset: start,
249            message: "Fix: directive logical span overflows source address space",
250        })?;
251        let row = source.get(start..logical_end).ok_or(CPreprocessorError {
252            offset: start,
253            message: "Fix: preprocessor token span must be inside the source buffer",
254        })?;
255        if let Some(effect) = classify_c_preprocessor_side_effect(row, start)? {
256            metadata.kinds[idx] = effect.kind.token_id();
257            metadata.payload_starts[idx] = checked_u32(
258                effect.payload_start,
259                "Fix: payload offset exceeds u32 metadata",
260            )?;
261            metadata.payload_lens[idx] = checked_u32(
262                effect.payload_len,
263                "Fix: payload length exceeds u32 metadata",
264            )?;
265            metadata.detail_starts[idx] = checked_u32(
266                effect.detail_start,
267                "Fix: detail offset exceeds u32 metadata",
268            )?;
269            metadata.detail_lens[idx] =
270                checked_u32(effect.detail_len, "Fix: detail length exceeds u32 metadata")?;
271        }
272    }
273    Ok(metadata)
274}
275
276fn classify_pragma_payload(
277    payload: &[u8],
278) -> Result<(CPreprocessorSideEffectKind, usize, usize), CPreprocessorError> {
279    let Some((first, first_start, first_end)) = next_ident(payload, 0) else {
280        return Err(CPreprocessorError {
281            offset: 0,
282            message: "Fix: #pragma needs a pragma namespace or command",
283        });
284    };
285    if first == b"once" {
286        return Ok((
287            CPreprocessorSideEffectKind::PragmaOnce,
288            first_start,
289            first_end - first_start,
290        ));
291    }
292    if first == b"GCC" || first == b"clang" {
293        let Some((second, _, second_end)) = next_ident(payload, first_end) else {
294            return Ok((
295                CPreprocessorSideEffectKind::Pragma,
296                first_start,
297                payload_trimmed_len(payload),
298            ));
299        };
300        if second == b"diagnostic" {
301            let Some((action, action_start, action_end)) = next_ident(payload, second_end) else {
302                return Err(CPreprocessorError {
303                    offset: second_end,
304                    message: "Fix: #pragma diagnostic needs push, pop, ignored, warning, or error",
305                });
306            };
307            let kind = match action {
308                b"push" => CPreprocessorSideEffectKind::PragmaDiagnosticPush,
309                b"pop" => CPreprocessorSideEffectKind::PragmaDiagnosticPop,
310                b"ignored" => CPreprocessorSideEffectKind::PragmaDiagnosticIgnored,
311                b"warning" => CPreprocessorSideEffectKind::PragmaDiagnosticWarning,
312                b"error" => CPreprocessorSideEffectKind::PragmaDiagnosticError,
313                _ => {
314                    return Err(CPreprocessorError {
315                        offset: action_start,
316                        message:
317                            "Fix: #pragma diagnostic action must be push, pop, ignored, warning, or error",
318                    });
319                }
320            };
321            let detail_start = if matches!(
322                kind,
323                CPreprocessorSideEffectKind::PragmaDiagnosticIgnored
324                    | CPreprocessorSideEffectKind::PragmaDiagnosticWarning
325                    | CPreprocessorSideEffectKind::PragmaDiagnosticError
326            ) {
327                skip_horizontal_ws(payload, action_end)
328            } else {
329                action_start
330            };
331            return Ok((
332                kind,
333                detail_start,
334                payload_trimmed_len(&payload[detail_start..]),
335            ));
336        }
337    }
338    Ok((
339        CPreprocessorSideEffectKind::Pragma,
340        first_start,
341        payload_trimmed_len(payload),
342    ))
343}
344
345fn next_ident(payload: &[u8], index: usize) -> Option<(&[u8], usize, usize)> {
346    let mut start = skip_horizontal_ws(payload, index);
347    if !matches!(payload.get(start), Some(b'_' | b'a'..=b'z' | b'A'..=b'Z')) {
348        return None;
349    }
350    let ident_start = start;
351    start += 1;
352    while matches!(
353        payload.get(start),
354        Some(b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9')
355    ) {
356        start += 1;
357    }
358    Some((&payload[ident_start..start], ident_start, start))
359}
360
361fn first_payload_byte(payload: &[u8]) -> usize {
362    skip_horizontal_ws(payload, 0)
363}
364
365fn payload_trimmed_len(payload: &[u8]) -> usize {
366    let start = skip_horizontal_ws(payload, 0);
367    let mut end = payload.len();
368    while end > start && matches!(payload[end - 1], b' ' | b'\t' | b'\x0b' | b'\x0c') {
369        end -= 1;
370    }
371    end.saturating_sub(start)
372}
373
374fn skip_horizontal_ws(bytes: &[u8], mut index: usize) -> usize {
375    while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\x0b' | b'\x0c')) {
376        index += 1;
377    }
378    index
379}
380
381fn checked_u32(value: usize, message: &'static str) -> Result<u32, CPreprocessorError> {
382    u32::try_from(value).map_err(|_| CPreprocessorError {
383        offset: value,
384        message,
385    })
386}