Skip to main content

vyre_libs/parsing/c/preprocess/
source.rs

1//! Source-manager ABI for C `#include` and GNU `#include_next`.
2
3#[cfg(any(test, feature = "cpu-parity"))]
4use crate::parsing::c::lex::tokens::TOK_PREPROC;
5#[cfg(any(test, feature = "cpu-parity"))]
6use crate::parsing::c::preprocess::c_logical_directive_len;
7use crate::parsing::c::preprocess::{
8    c_directive_payload, c_translation_phase_line_splice, try_classify_preprocessor_directive,
9    CPreprocessorDirectiveKind, CPreprocessorError,
10};
11
12/// Header spelling class from a C include directive.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum CIncludeStyle {
15    /// `"header.h"` lookup.
16    Quote,
17    /// `<header.h>` lookup.
18    Angle,
19}
20
21/// Fully parsed include request passed to the embedding source manager.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct CIncludeRequest {
24    /// `#include` or GNU `#include_next`.
25    pub directive: CPreprocessorDirectiveKind,
26    /// Header-name delimiter style.
27    pub style: CIncludeStyle,
28    /// Header spelling without delimiters.
29    pub spelling: Vec<u8>,
30    /// Original source offset of the directive row.
31    pub directive_offset: usize,
32    /// Original source offset of the header payload.
33    pub payload_offset: usize,
34}
35
36impl CIncludeRequest {
37    /// Return true when this request came from GNU `#include_next`.
38    #[must_use]
39    pub const fn is_include_next(&self) -> bool {
40        matches!(self.directive, CPreprocessorDirectiveKind::IncludeNext)
41    }
42}
43
44/// Source bytes returned by a source manager include load.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct CSourceFile {
47    /// Stable source ID assigned by the embedding source manager.
48    pub source_id: u32,
49    /// Human-readable resolved name or path for diagnostics.
50    pub display_name: String,
51    /// Loaded source bytes.
52    pub bytes: Vec<u8>,
53}
54
55/// Host source manager contract for include loading.
56///
57/// The preprocessor frontend owns directive parsing and include spelling
58/// validation. The embedder owns search paths, `#include_next` continuation,
59/// virtual filesystems, and filesystem policy.
60pub trait CPreprocessorSourceManager {
61    /// Resolve and load one parsed include request.
62    ///
63    /// # Errors
64    ///
65    /// Returns an actionable preprocessor diagnostic when the include cannot
66    /// be resolved or loaded.
67    fn load_include(&self, request: &CIncludeRequest) -> Result<CSourceFile, CPreprocessorError>;
68}
69
70/// Include source loaded for one directive token.
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct CResolvedInclude {
73    /// Index of the `TOK_PREPROC` token that requested this include.
74    pub token_index: usize,
75    /// Parsed include request.
76    pub request: CIncludeRequest,
77    /// Loaded source returned by the source manager.
78    pub source: CSourceFile,
79}
80
81/// Parse an include request from one physical directive row.
82///
83/// `row` may contain phase-2 line splices; the returned offsets are mapped back
84/// to the original row byte space and then shifted by `directive_offset`.
85///
86/// # Errors
87///
88/// Returns a diagnostic when the row is an include directive but its header
89/// payload is malformed.
90pub fn parse_c_include_request(
91    row: &[u8],
92    directive_offset: usize,
93) -> Result<Option<CIncludeRequest>, CPreprocessorError> {
94    let spliced = c_translation_phase_line_splice(row);
95    let directive = try_classify_preprocessor_directive(&spliced.bytes).map_err(|mut err| {
96        err.offset = directive_offset + spliced.original_offset(err.offset);
97        err
98    })?;
99    if !matches!(
100        directive.kind,
101        CPreprocessorDirectiveKind::Include | CPreprocessorDirectiveKind::IncludeNext
102    ) {
103        return Ok(None);
104    }
105
106    let payload = c_directive_payload(&spliced.bytes, directive).map_err(|mut err| {
107        err.offset = directive_offset + spliced.original_offset(err.offset);
108        err
109    })?;
110    let (style, spelling, payload_rel) =
111        parse_header_name_payload(payload).map_err(|mut err| {
112            err.offset =
113                directive_offset + spliced.original_offset(directive.payload_start + err.offset);
114            err
115        })?;
116    Ok(Some(CIncludeRequest {
117        directive: directive.kind,
118        style,
119        spelling,
120        directive_offset,
121        payload_offset: directive_offset
122            + spliced.original_offset(directive.payload_start + payload_rel),
123    }))
124}
125
126/// Load all include directives from a compact token stream through `manager`.
127///
128/// # Errors
129///
130/// Returns a diagnostic when token streams are inconsistent, a directive span
131/// is invalid, an include payload is malformed, or the source manager rejects
132/// a load.
133#[deprecated(
134    note = "CPU reference oracle only; production include loading must use the GPU preprocessor pipeline source-manager path"
135)]
136#[cfg(any(test, feature = "cpu-parity"))]
137pub fn reference_c_preprocessor_load_includes<M: CPreprocessorSourceManager>(
138    tok_types: &[u32],
139    tok_starts: &[u32],
140    tok_lens: &[u32],
141    source: &[u8],
142    manager: &M,
143) -> Result<Vec<CResolvedInclude>, CPreprocessorError> {
144    if tok_types.len() != tok_starts.len() || tok_types.len() != tok_lens.len() {
145        return Err(CPreprocessorError {
146            offset: tok_types.len().min(tok_starts.len()).min(tok_lens.len()),
147            message: "Fix: token type/start/length streams must have identical lengths",
148        });
149    }
150
151    let mut resolved = Vec::new();
152    for (idx, ((tok_type, start), len)) in
153        tok_types.iter().zip(tok_starts).zip(tok_lens).enumerate()
154    {
155        if *tok_type != TOK_PREPROC {
156            continue;
157        }
158        let start = usize::try_from(*start).map_err(|_| CPreprocessorError {
159            offset: idx,
160            message: "Fix: token start does not fit host usize",
161        })?;
162        let len = usize::try_from(*len).map_err(|_| CPreprocessorError {
163            offset: idx,
164            message: "Fix: token length does not fit host usize",
165        })?;
166        let token_end = start.checked_add(len).ok_or(CPreprocessorError {
167            offset: start,
168            message: "Fix: token span overflows source address space",
169        })?;
170        let logical_len = c_logical_directive_len(source, start);
171        if logical_len > len {
172            return Err(CPreprocessorError {
173                offset: start + len,
174                message:
175                    "Fix: TOK_PREPROC span must include the full phase-2 spliced directive row",
176            });
177        }
178        if token_end > source.len() {
179            return Err(CPreprocessorError {
180                offset: start,
181                message: "Fix: preprocessor token span must be inside the source buffer",
182            });
183        }
184        let logical_end = start.checked_add(logical_len).ok_or(CPreprocessorError {
185            offset: start,
186            message: "Fix: directive logical span overflows source address space",
187        })?;
188        let row = source.get(start..logical_end).ok_or(CPreprocessorError {
189            offset: start,
190            message: "Fix: preprocessor token span must be inside the source buffer",
191        })?;
192        if let Some(request) = parse_c_include_request(row, start)? {
193            let source = manager.load_include(&request)?;
194            resolved.push(CResolvedInclude {
195                token_index: idx,
196                request,
197                source,
198            });
199        }
200    }
201    Ok(resolved)
202}
203
204fn parse_header_name_payload(
205    payload: &[u8],
206) -> Result<(CIncludeStyle, Vec<u8>, usize), CPreprocessorError> {
207    let start = skip_horizontal_ws(payload, 0);
208    let Some(open) = payload.get(start).copied() else {
209        return Err(CPreprocessorError {
210            offset: start,
211            message: "Fix: #include needs a header name payload",
212        });
213    };
214    match open {
215        b'"' => parse_delimited_header(payload, start, b'"', CIncludeStyle::Quote),
216        b'<' => parse_delimited_header(payload, start, b'>', CIncludeStyle::Angle),
217        _ => Err(CPreprocessorError {
218            offset: start,
219            message:
220                "Fix: #include payload must be a quoted or angle-bracket header name after macro expansion",
221        }),
222    }
223}
224
225fn parse_delimited_header(
226    payload: &[u8],
227    start: usize,
228    close: u8,
229    style: CIncludeStyle,
230) -> Result<(CIncludeStyle, Vec<u8>, usize), CPreprocessorError> {
231    let mut index = start + 1;
232    while let Some(byte) = payload.get(index).copied() {
233        if matches!(byte, b'\n' | b'\r') {
234            return Err(CPreprocessorError {
235                offset: index,
236                message: "Fix: #include header name must close before newline",
237            });
238        }
239        if byte == close {
240            let trailing = skip_horizontal_ws(payload, index + 1);
241            if trailing != payload.len() {
242                return Err(CPreprocessorError {
243                    offset: trailing,
244                    message: "Fix: unexpected tokens after #include header name",
245                });
246            }
247            return Ok((style, payload[start + 1..index].to_vec(), start + 1));
248        }
249        index += 1;
250    }
251    Err(CPreprocessorError {
252        offset: start,
253        message: "Fix: terminate #include header name",
254    })
255}
256
257fn skip_horizontal_ws(bytes: &[u8], mut index: usize) -> usize {
258    while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\x0b' | b'\x0c')) {
259        index += 1;
260    }
261    index
262}