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::preprocess::directive_scan::for_each_directive_row;
5use crate::parsing::c::preprocess::directive_scan::{skip_horizontal_ws, ScannedDirective};
6use crate::parsing::c::preprocess::{CPreprocessorDirectiveKind, CPreprocessorError};
7
8/// Header spelling class from a C include directive.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CIncludeStyle {
11    /// `"header.h"` lookup.
12    Quote,
13    /// `<header.h>` lookup.
14    Angle,
15}
16
17/// Fully parsed include request passed to the embedding source manager.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct CIncludeRequest {
20    /// `#include` or GNU `#include_next`.
21    pub directive: CPreprocessorDirectiveKind,
22    /// Header-name delimiter style.
23    pub style: CIncludeStyle,
24    /// Header spelling without delimiters.
25    pub spelling: Vec<u8>,
26    /// Original source offset of the directive row.
27    pub directive_offset: usize,
28    /// Original source offset of the header payload.
29    pub payload_offset: usize,
30}
31
32impl CIncludeRequest {
33    /// Return true when this request came from GNU `#include_next`.
34    #[must_use]
35    pub const fn is_include_next(&self) -> bool {
36        matches!(self.directive, CPreprocessorDirectiveKind::IncludeNext)
37    }
38}
39
40/// Source bytes returned by a source manager include load.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct CSourceFile {
43    /// Stable source ID assigned by the embedding source manager.
44    pub source_id: u32,
45    /// Human-readable resolved name or path for diagnostics.
46    pub display_name: String,
47    /// Loaded source bytes.
48    pub bytes: Vec<u8>,
49}
50
51/// Host source manager contract for include loading.
52///
53/// The preprocessor frontend owns directive parsing and include spelling
54/// validation. The embedder owns search paths, `#include_next` continuation,
55/// virtual filesystems, and filesystem policy.
56pub trait CPreprocessorSourceManager {
57    /// Resolve and load one parsed include request.
58    ///
59    /// # Errors
60    ///
61    /// Returns an actionable preprocessor diagnostic when the include cannot
62    /// be resolved or loaded.
63    fn load_include(&self, request: &CIncludeRequest) -> Result<CSourceFile, CPreprocessorError>;
64}
65
66/// Include source loaded for one directive token.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct CResolvedInclude {
69    /// Index of the `TOK_PREPROC` token that requested this include.
70    pub token_index: usize,
71    /// Parsed include request.
72    pub request: CIncludeRequest,
73    /// Loaded source returned by the source manager.
74    pub source: CSourceFile,
75}
76
77/// Parse an include request from one physical directive row.
78///
79/// `row` may contain phase-2 line splices; the returned offsets are mapped back
80/// to the original row byte space and then shifted by `directive_offset`.
81///
82/// # Errors
83///
84/// Returns a diagnostic when the row is an include directive but its header
85/// payload is malformed.
86pub fn parse_c_include_request(
87    row: &[u8],
88    directive_offset: usize,
89) -> Result<Option<CIncludeRequest>, CPreprocessorError> {
90    let scan = ScannedDirective::classify(row, directive_offset)?;
91    let directive = scan.directive;
92    if !matches!(
93        directive.kind,
94        CPreprocessorDirectiveKind::Include | CPreprocessorDirectiveKind::IncludeNext
95    ) {
96        return Ok(None);
97    }
98
99    let payload = scan.payload()?;
100    let (style, spelling, payload_rel) =
101        parse_header_name_payload(payload).map_err(|mut err| {
102            err.offset += directive.payload_start;
103            scan.remap(err)
104        })?;
105    Ok(Some(CIncludeRequest {
106        directive: directive.kind,
107        style,
108        spelling,
109        directive_offset,
110        payload_offset: scan.source_offset(directive.payload_start + payload_rel),
111    }))
112}
113
114/// Load all include directives from a compact token stream through `manager`.
115///
116/// # Errors
117///
118/// Returns a diagnostic when token streams are inconsistent, a directive span
119/// is invalid, an include payload is malformed, or the source manager rejects
120/// a load.
121#[cfg(any(test, feature = "cpu-parity"))]
122pub fn reference_c_preprocessor_load_includes<M: CPreprocessorSourceManager>(
123    tok_types: &[u32],
124    tok_starts: &[u32],
125    tok_lens: &[u32],
126    source: &[u8],
127    manager: &M,
128) -> Result<Vec<CResolvedInclude>, CPreprocessorError> {
129    let mut resolved = Vec::new();
130    for_each_directive_row(tok_types, tok_starts, tok_lens, source, |row| {
131        if let Some(request) = parse_c_include_request(row.bytes, row.start)? {
132            let loaded = manager.load_include(&request)?;
133            resolved.push(CResolvedInclude {
134                token_index: row.index,
135                request,
136                source: loaded,
137            });
138        }
139        Ok(())
140    })?;
141    Ok(resolved)
142}
143
144fn parse_header_name_payload(
145    payload: &[u8],
146) -> Result<(CIncludeStyle, Vec<u8>, usize), CPreprocessorError> {
147    let start = skip_horizontal_ws(payload, 0);
148    let Some(open) = payload.get(start).copied() else {
149        return Err(CPreprocessorError {
150            offset: start,
151            message: "Fix: #include needs a header name payload",
152        });
153    };
154    match open {
155        b'"' => parse_delimited_header(payload, start, b'"', CIncludeStyle::Quote),
156        b'<' => parse_delimited_header(payload, start, b'>', CIncludeStyle::Angle),
157        _ => Err(CPreprocessorError {
158            offset: start,
159            message:
160                "Fix: #include payload must be a quoted or angle-bracket header name after macro expansion",
161        }),
162    }
163}
164
165fn parse_delimited_header(
166    payload: &[u8],
167    start: usize,
168    close: u8,
169    style: CIncludeStyle,
170) -> Result<(CIncludeStyle, Vec<u8>, usize), CPreprocessorError> {
171    let mut index = start + 1;
172    while let Some(byte) = payload.get(index).copied() {
173        if matches!(byte, b'\n' | b'\r') {
174            return Err(CPreprocessorError {
175                offset: index,
176                message: "Fix: #include header name must close before newline",
177            });
178        }
179        if byte == close {
180            let trailing = skip_horizontal_ws(payload, index + 1);
181            if trailing != payload.len() {
182                return Err(CPreprocessorError {
183                    offset: trailing,
184                    message: "Fix: unexpected tokens after #include header name",
185                });
186            }
187            return Ok((style, payload[start + 1..index].to_vec(), start + 1));
188        }
189        index += 1;
190    }
191    Err(CPreprocessorError {
192        offset: start,
193        message: "Fix: terminate #include header name",
194    })
195}