mod condition;
pub mod macro_;
mod pptoken;
use crate::diagnostic::{Diagnostic, DiagnosticCode};
use alloc::borrow::ToOwned;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use macro_::{MacroTable, MacroUse};
use zerodds_idl_cst::{Span, Token, Trivia, TriviaKind, lex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct FileId(pub u32);
#[derive(Debug, Clone)]
pub struct SourceFile {
pub path: String,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Origin {
pub file: FileId,
pub span: Span,
pub macro_chain: Vec<MacroUse>,
}
impl Origin {
#[must_use]
pub const fn literal(file: FileId, span: Span) -> Self {
Self {
file,
span,
macro_chain: Vec::new(),
}
}
#[must_use]
pub fn is_expanded(&self) -> bool {
!self.macro_chain.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct SourceMap {
files: Vec<SourceFile>,
origins: Vec<Origin>,
ranges: Vec<(Span, Origin)>,
}
impl SourceMap {
#[must_use]
pub const fn new() -> Self {
Self {
files: Vec::new(),
origins: Vec::new(),
ranges: Vec::new(),
}
}
pub const ROOT: FileId = FileId(0);
pub fn add_file(&mut self, path: &str, text: &str) -> FileId {
let id = u32::try_from(self.files.len()).unwrap_or(u32::MAX);
self.files.push(SourceFile {
path: path.to_owned(),
text: text.to_owned(),
});
FileId(id)
}
#[must_use]
pub fn path(&self, file: FileId) -> Option<&str> {
self.files.get(file.0 as usize).map(|f| f.path.as_str())
}
#[must_use]
pub fn text(&self, file: FileId) -> Option<&str> {
self.files.get(file.0 as usize).map(|f| f.text.as_str())
}
#[must_use]
pub fn origin(&self, index: usize) -> Option<&Origin> {
self.origins.get(index)
}
#[must_use]
pub fn len(&self) -> usize {
self.origins.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.origins.is_empty()
}
fn push(&mut self, origin: Origin) {
self.origins.push(origin);
}
fn push_range(&mut self, expanded: Span, origin: Origin) {
self.ranges.push((expanded, origin));
}
#[must_use]
pub fn original(&self, expanded: Span) -> Option<(FileId, Span)> {
let containing = |at: u32| {
self.ranges
.partition_point(|(range, _)| range.start <= at)
.checked_sub(1)
};
let first = containing(expanded.start)?;
let last = containing(expanded.end.saturating_sub(1).max(expanded.start))
.unwrap_or(first)
.max(first);
let (from, start) = &self.ranges[first];
let (to, end) = &self.ranges[last];
let start_at = Self::offset_in(*from, start, expanded.start, false);
if start.file != end.file {
return Some((start.file, Span::new(start_at, start.span.end)));
}
let end_at = Self::offset_in(*to, end, expanded.end, true).max(start_at);
Some((start.file, Span::new(start_at, end_at)))
}
#[must_use]
pub fn origin_at(&self, at: u32) -> Option<&Origin> {
let index = self
.ranges
.partition_point(|(range, _)| range.start <= at)
.checked_sub(1)?;
self.ranges.get(index).map(|(_, origin)| origin)
}
fn offset_in(range: Span, origin: &Origin, at: u32, end: bool) -> u32 {
let written =
!origin.is_expanded() && range.end - range.start == origin.span.end - origin.span.start;
if written {
let within = at.clamp(range.start, range.end) - range.start;
origin.span.start + within
} else if end {
origin.span.end
} else {
origin.span.start
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Provenance {
pub path: String,
pub span: Span,
pub macro_chain: Vec<ProvenanceMacro>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProvenanceMacro {
pub name: String,
pub path: Option<String>,
pub at: Span,
}
pub trait FileLoader {
fn load(&self, path: &str, from: Option<&str>) -> Option<(String, String)>;
}
#[derive(Debug, Clone, Default)]
pub struct MemoryLoader {
files: Vec<(String, String)>,
}
impl MemoryLoader {
#[must_use]
pub const fn new() -> Self {
Self { files: Vec::new() }
}
#[must_use]
pub fn with(mut self, path: &str, text: &str) -> Self {
self.files.push((path.to_owned(), text.to_owned()));
self
}
}
impl FileLoader for MemoryLoader {
fn load(&self, path: &str, _from: Option<&str>) -> Option<(String, String)> {
self.files
.iter()
.find(|(name, _)| name == path)
.map(|(name, text)| (name.clone(), text.clone()))
}
}
#[derive(Debug, Clone)]
pub struct Preprocessed {
pub tokens: Vec<Token>,
pub text: String,
pub map: SourceMap,
pub diagnostics: Vec<Diagnostic>,
}
impl Preprocessed {
#[must_use]
pub fn provenance(&self, expanded: Span) -> Option<Provenance> {
let (file, span) = self.map.original(expanded)?;
let path = self.map.path(file)?.to_string();
let macro_chain = self
.map
.origin_at(expanded.start)
.map(|origin| {
origin
.macro_chain
.iter()
.map(|used| ProvenanceMacro {
name: used.name.clone(),
path: self.map.path(FileId(used.file)).map(ToString::to_string),
at: used.at,
})
.collect()
})
.unwrap_or_default();
Some(Provenance {
path,
span,
macro_chain,
})
}
pub fn locate(&self, diagnostic: &mut Diagnostic) {
let path = |file: FileId| {
(file != SourceMap::ROOT)
.then(|| self.map.path(file).map(ToString::to_string))
.flatten()
};
let primary_file = match self.map.original(diagnostic.primary) {
Some((file, span)) => {
diagnostic.primary = span;
diagnostic.file = path(file);
Some(file)
}
None => None,
};
for (span, note) in &mut diagnostic.labels {
if let Some((file, mapped)) = self.map.original(*span) {
*span = mapped;
if Some(file) != primary_file {
let name = self.map.path(file).unwrap_or("?");
*note = format!("{name}: {note}");
}
}
}
}
}
const MAX_INCLUDE_DEPTH: usize = 64;
const MAX_EXPANSION_DEPTH: usize = 256;
const MAX_EXPANDED_TOKENS: usize = 1 << 22;
#[must_use]
pub fn preprocess(path: &str, source: &str, loader: &dyn FileLoader) -> Preprocessed {
preprocess_with(path, source, loader, &[])
}
#[must_use]
pub fn preprocess_with(
path: &str,
source: &str,
loader: &dyn FileLoader,
defines: &[(String, String)],
) -> Preprocessed {
let mut state = Preprocessor {
loader,
map: SourceMap::new(),
macros: MacroTable::new(),
diagnostics: Vec::new(),
text: String::new(),
tokens: Vec::new(),
pending_pragmas: Vec::new(),
open: Vec::new(),
once: Vec::new(),
presumed: Vec::new(),
expansion_depth: 0,
produced: 0,
};
for (name, body) in defines {
let line = format!("{name} {body}");
state.define(&line, Span::default());
}
state.include(path, source, 0);
state.flush_trailing_pragmas();
Preprocessed {
tokens: state.tokens,
text: state.text,
map: state.map,
diagnostics: state.diagnostics,
}
}
struct Preprocessor<'a> {
loader: &'a dyn FileLoader,
map: SourceMap,
macros: MacroTable,
diagnostics: Vec<Diagnostic>,
text: String,
tokens: Vec<Token>,
pending_pragmas: Vec<(String, Trivia, FileId)>,
open: Vec<String>,
once: Vec<String>,
presumed: Vec<(FileId, i64, Option<String>)>,
expansion_depth: usize,
produced: usize,
}
#[derive(Debug, Clone)]
struct PpToken {
text: String,
space_before: bool,
origin: Origin,
written: Option<Token>,
hide: Vec<String>,
painted: bool,
}
impl PpToken {
fn made(text: String, space_before: bool, origin: Origin) -> Self {
Self {
text,
space_before,
origin,
written: None,
hide: Vec::new(),
painted: false,
}
}
fn is_identifier(&self) -> bool {
pptoken::is_identifier(&self.text)
}
}
struct FileCursor<'s> {
tokens: Vec<Token>,
at: usize,
source: &'s str,
file: FileId,
conditions: Vec<Condition>,
depth: usize,
}
impl FileCursor<'_> {
fn keeping(&self) -> bool {
self.conditions.iter().all(Condition::keeping)
}
}
enum Sink<'v> {
Emit,
Collect(&'v mut Vec<PpToken>),
}
impl Preprocessor<'_> {
fn include(&mut self, path: &str, source: &str, depth: usize) {
if self.once.iter().any(|seen| seen == path) {
return;
}
if depth > MAX_INCLUDE_DEPTH {
self.report(Diagnostic::error(
DiagnosticCode::UnterminatedDeclaration,
Span::default(),
format!("include nesting past {MAX_INCLUDE_DEPTH} levels at `{path}`"),
"OMG IDL 4.2 §7.3",
));
return;
}
if self.open.iter().any(|open| open == path) {
self.report(Diagnostic::error(
DiagnosticCode::UnexpectedToken,
Span::default(),
format!(
"`{path}` includes itself, directly or through {}",
self.open.join(" -> ")
),
"OMG IDL 4.2 §7.3",
));
return;
}
let file = self.map.add_file(path, source);
self.open.push(path.to_owned());
let mut cursor = FileCursor {
tokens: lex(source),
at: 0,
source,
file,
conditions: Vec::new(),
depth,
};
let mut queue = VecDeque::new();
self.run(&mut queue, Some(&mut cursor), &mut Sink::Emit);
if !cursor.conditions.is_empty() {
self.report(Diagnostic::error(
DiagnosticCode::UnterminatedDeclaration,
Span::default(),
format!(
"{} conditional(s) left open in `{path}`",
cursor.conditions.len()
),
"OMG IDL 4.2 §7.3",
));
}
let _ = self.open.pop();
}
fn pull(
&mut self,
queue: &mut VecDeque<PpToken>,
cursor: Option<&mut FileCursor<'_>>,
) -> Option<PpToken> {
if let Some(token) = queue.pop_front() {
return Some(token);
}
let cursor = cursor?;
while cursor.at < cursor.tokens.len() {
let index = cursor.at;
cursor.at += 1;
let leading = cursor.tokens[index].leading.clone();
for trivia in &leading {
match trivia.kind {
TriviaKind::Directive => {
let line = trivia.span.slice(cursor.source).unwrap_or("");
self.directive(line, trivia.span, cursor);
}
TriviaKind::LineComment
if crate::suffix::is_suffix_comment(
trivia.span.slice(cursor.source).unwrap_or(""),
) && cursor.keeping() =>
{
self.pending_pragmas.push((
trivia.span.slice(cursor.source).unwrap_or("").to_string(),
Trivia {
kind: TriviaKind::LineComment,
span: trivia.span,
},
cursor.file,
));
}
_ => {}
}
}
let token = &cursor.tokens[index];
if token.kind == zerodds_idl_cst::TokenKind::Eof || !cursor.keeping() {
continue;
}
return Some(PpToken {
text: token.span.slice(cursor.source).unwrap_or("").to_string(),
space_before: !token.leading.is_empty(),
origin: Origin::literal(cursor.file, token.span),
written: Some(token.clone()),
hide: Vec::new(),
painted: false,
});
}
None
}
fn run(
&mut self,
queue: &mut VecDeque<PpToken>,
mut cursor: Option<&mut FileCursor<'_>>,
sink: &mut Sink<'_>,
) {
while let Some(mut token) = self.pull(queue, cursor.as_deref_mut()) {
if token.painted || !token.is_identifier() {
self.deliver(token, sink);
continue;
}
if token.hide.contains(&token.text) {
token.painted = true;
self.deliver(token, sink);
continue;
}
if let Some(answer) = self.predefined(&token) {
self.deliver(answer, sink);
continue;
}
let Some(found) = self.macros.get(&token.text).cloned() else {
self.deliver(token, sink);
continue;
};
let replacement = match &found.params {
None => {
let mut hide = token.hide.clone();
hide.push(token.text.clone());
self.substitute(&found, &[], &token, &hide)
}
Some(params) => {
let Some(next) = self.pull(queue, cursor.as_deref_mut()) else {
self.deliver(token, sink);
continue;
};
if next.text != "(" {
queue.push_front(next);
self.deliver(token, sink);
continue;
}
let Some((mut args, closing)) =
self.arguments(queue, cursor.as_deref_mut(), &token)
else {
continue;
};
if params.is_empty() && args.len() == 1 && args[0].is_empty() {
args.clear();
}
if args.len() != params.len() {
self.preprocessing_error(
token.origin.span,
format!(
"`{}` takes {} argument(s) and is given {}",
token.text,
params.len(),
args.len()
),
"ISO/IEC 14882:2003 §16.3/4",
);
continue;
}
let mut hide: Vec<String> = token
.hide
.iter()
.filter(|name| closing.hide.contains(name))
.cloned()
.collect();
hide.push(token.text.clone());
self.substitute(&found, &args, &token, &hide)
}
};
self.produced = self.produced.saturating_add(replacement.len());
if self.produced > MAX_EXPANDED_TOKENS {
self.preprocessing_error(
token.origin.span,
format!(
"macro expansion produced more than {MAX_EXPANDED_TOKENS} tokens; `{}` is \
not expanded further",
token.text
),
"zerodds-idl-frontend-1.0 §3.4",
);
return;
}
for replaced in replacement.into_iter().rev() {
queue.push_front(replaced);
}
}
}
fn arguments(
&mut self,
queue: &mut VecDeque<PpToken>,
mut cursor: Option<&mut FileCursor<'_>>,
name: &PpToken,
) -> Option<(Vec<Vec<PpToken>>, PpToken)> {
let mut args: Vec<Vec<PpToken>> = alloc::vec![Vec::new()];
let mut nesting = 0_usize;
while let Some(token) = self.pull(queue, cursor.as_deref_mut()) {
match token.text.as_str() {
")" if nesting == 0 => return Some((args, token)),
"," if nesting == 0 => args.push(Vec::new()),
"(" | ")" => {
if token.text == "(" {
nesting += 1;
} else {
nesting -= 1;
}
if let Some(last) = args.last_mut() {
last.push(token);
}
}
_ => {
if let Some(last) = args.last_mut() {
last.push(token);
}
}
}
}
self.preprocessing_error(
name.origin.span,
format!(
"the invocation of `{}` is not closed before the end of the input",
name.text
),
"ISO/IEC 14882:2003 §16.3/10",
);
None
}
fn substitute(
&mut self,
found: ¯o_::Macro,
args: &[Vec<PpToken>],
invocation: &PpToken,
hide: &[String],
) -> Vec<PpToken> {
enum Item {
Token(PpToken),
Placemarker,
Paste,
}
let mut chain = invocation.origin.macro_chain.clone();
chain.push(MacroUse {
name: invocation.text.clone(),
at: invocation.origin.span,
file: invocation.origin.file.0,
});
let origin = Origin {
file: invocation.origin.file,
span: invocation.origin.span,
macro_chain: chain.clone(),
};
let params: &[String] = found.params.as_deref().unwrap_or(&[]);
let function_like = found.params.is_some();
let body = &found.body;
let mut expanded: Vec<Option<Vec<PpToken>>> = alloc::vec![None; args.len()];
let mut items: Vec<Item> = Vec::new();
let mut index = 0;
while index < body.len() {
let piece = &body[index];
let parameter = |text: &str| params.iter().position(|param| param == text);
if function_like && piece.text == "#" {
if let Some(slot) = body.get(index + 1).and_then(|next| parameter(&next.text)) {
items.push(Item::Token(PpToken::made(
stringize(&args[slot]),
piece.space_before,
origin.clone(),
)));
index += 2;
continue;
}
}
if piece.text == "##" {
items.push(Item::Paste);
index += 1;
continue;
}
if let Some(slot) = parameter(&piece.text).filter(|_| function_like) {
let pasted = body.get(index + 1).is_some_and(|next| next.text == "##")
|| (index > 0 && body[index - 1].text == "##");
let tokens = if pasted {
args[slot].clone()
} else {
if expanded[slot].is_none() {
expanded[slot] = Some(self.expand_isolated(args[slot].clone()));
}
expanded[slot].clone().unwrap_or_default()
};
if tokens.is_empty() {
items.push(Item::Placemarker);
}
for (position, mut token) in tokens.into_iter().enumerate() {
if position == 0 {
token.space_before = piece.space_before;
}
let mut inner = chain.clone();
inner.extend(token.origin.macro_chain);
token.origin.macro_chain = inner;
items.push(Item::Token(token));
}
index += 1;
continue;
}
items.push(Item::Token(PpToken::made(
piece.text.clone(),
piece.space_before,
origin.clone(),
)));
index += 1;
}
let mut joined: Vec<Item> = Vec::new();
let mut items = items.into_iter();
while let Some(item) = items.next() {
if !matches!(item, Item::Paste) {
joined.push(item);
continue;
}
let left = joined.pop();
let right = items.next();
match (left, right) {
(Some(Item::Placemarker), Some(right)) => joined.push(right),
(Some(left), Some(Item::Placemarker)) => joined.push(left),
(Some(Item::Token(left)), Some(Item::Token(right))) => {
let text = format!("{}{}", left.text, right.text);
match pptoken::tokenize(&text) {
Ok(pieces) if pieces.len() == 1 => {
joined.push(Item::Token(PpToken::made(
text,
left.space_before,
origin.clone(),
)));
}
_ => {
self.preprocessing_error(
invocation.origin.span,
format!(
"pasting `{}` and `{}` gives `{text}`, which is not one \
preprocessing token",
left.text, right.text
),
"ISO/IEC 14882:2003 §16.3.3/3",
);
joined.push(Item::Token(left));
joined.push(Item::Token(right));
}
}
}
(left, right) => {
joined.extend(left);
joined.extend(right);
}
}
}
joined
.into_iter()
.filter_map(|item| match item {
Item::Token(mut token) => {
for name in hide {
if !token.hide.contains(name) {
token.hide.push(name.clone());
}
}
Some(token)
}
Item::Placemarker | Item::Paste => None,
})
.collect()
}
fn expand_isolated(&mut self, tokens: Vec<PpToken>) -> Vec<PpToken> {
if self.expansion_depth >= MAX_EXPANSION_DEPTH {
if let Some(first) = tokens.first() {
self.preprocessing_error(
first.origin.span,
format!(
"macro arguments nest past {MAX_EXPANSION_DEPTH} levels; this one is not \
expanded"
),
"zerodds-idl-frontend-1.0 §3.4",
);
}
return tokens;
}
self.expansion_depth += 1;
let mut queue: VecDeque<PpToken> = tokens.into();
let mut out = Vec::new();
self.run(&mut queue, None, &mut Sink::Collect(&mut out));
self.expansion_depth -= 1;
out
}
fn predefined(&self, token: &PpToken) -> Option<PpToken> {
let file = token.origin.file;
let presumed = self.presumed.iter().rev().find(|(of, _, _)| *of == file);
let text = match token.text.as_str() {
"__LINE__" => {
let physical = self
.map
.text(file)
.map_or(1, |source| line_of(source, token.origin.span.start));
let delta = presumed.map_or(0, |(_, delta, _)| *delta);
format!("{}", i64::from(physical) + delta)
}
"__FILE__" => {
let name = presumed
.and_then(|(_, _, name)| name.clone())
.or_else(|| self.map.path(file).map(ToOwned::to_owned))
.unwrap_or_default();
quote(&name)
}
_ => return None,
};
let mut answer = PpToken::made(text, token.space_before, token.origin.clone());
answer.hide.clone_from(&token.hide);
Some(answer)
}
fn deliver(&mut self, token: PpToken, sink: &mut Sink<'_>) {
match sink {
Sink::Collect(list) => list.push(token),
Sink::Emit => self.emit(token),
}
}
fn emit(&mut self, token: PpToken) {
if let Some(written) = &token.written {
let written = written.clone();
self.push_text(written.kind, &token.text, &written, token.origin);
return;
}
let lexed = lex(&token.text);
let mut significant = lexed
.iter()
.filter(|lexed| lexed.kind != zerodds_idl_cst::TokenKind::Eof);
match (significant.next(), significant.next()) {
(Some(only), None) if only.leading.is_empty() => {
let only = only.clone();
self.push_text(only.kind, &token.text, &only, token.origin);
}
_ => {
self.preprocessing_error(
token.origin.span,
format!(
"macro expansion produced `{}`, which is not an IDL token",
token.text
),
"OMG IDL 4.2 §7.2",
);
let error = Token {
kind: zerodds_idl_cst::TokenKind::Error,
span: Span::default(),
leading: Vec::new(),
value: zerodds_idl_cst::TokenValue::None,
};
self.push_text(error.kind, &token.text, &error, token.origin);
}
}
}
fn report(&mut self, mut diagnostic: Diagnostic) {
if self.open.len() > 1 {
diagnostic.file = self.open.last().cloned();
}
self.diagnostics.push(diagnostic);
}
fn preprocessing_error(&mut self, span: Span, message: String, rule: &'static str) {
self.report(Diagnostic::error(
DiagnosticCode::Preprocessing,
span,
message,
rule,
));
}
fn push_text(
&mut self,
kind: zerodds_idl_cst::TokenKind,
text: &str,
original: &Token,
origin: Origin,
) {
if !self.text.is_empty() {
self.text.push(' ');
}
#[allow(clippy::cast_possible_truncation)]
let start = self.text.len() as u32;
self.text.push_str(text);
#[allow(clippy::cast_possible_truncation)]
let end = self.text.len() as u32;
self.map.push_range(Span::new(start, end), origin.clone());
let leading = core::mem::take(&mut self.pending_pragmas)
.into_iter()
.map(|(text, held, file)| {
self.text.push(' ');
#[allow(clippy::cast_possible_truncation)]
let from = self.text.len() as u32;
self.text.push_str(&text);
#[allow(clippy::cast_possible_truncation)]
let to = self.text.len() as u32;
self.map
.push_range(Span::new(from, to), Origin::literal(file, held.span));
Trivia {
kind: held.kind,
span: Span::new(from, to),
}
})
.collect();
self.tokens.push(Token {
kind,
span: Span::new(start, end),
leading,
value: original.value.clone(),
});
self.map.push(origin);
}
fn flush_trailing_pragmas(&mut self) {
if self.pending_pragmas.is_empty() {
return;
}
let pending = core::mem::take(&mut self.pending_pragmas);
let mut on_last = Vec::new();
let mut on_eof = Vec::new();
let mut eof_file = None;
for (text, held, file) in pending {
self.text.push(' ');
#[allow(clippy::cast_possible_truncation)]
let from = self.text.len() as u32;
self.text.push_str(&text);
#[allow(clippy::cast_possible_truncation)]
let to = self.text.len() as u32;
self.map
.push_range(Span::new(from, to), Origin::literal(file, held.span));
let moved = Trivia {
kind: held.kind,
span: Span::new(from, to),
};
if held.kind == TriviaKind::LineComment {
on_eof.push(moved);
eof_file = eof_file.or(Some(file));
} else {
on_last.push(moved);
}
}
if let Some(last) = self.tokens.last_mut() {
last.leading.extend(on_last);
}
let (Some(file), false) = (eof_file, on_eof.is_empty()) else {
return;
};
#[allow(clippy::cast_possible_truncation)]
let at = self.text.len() as u32;
self.tokens.push(Token {
kind: zerodds_idl_cst::TokenKind::Eof,
span: Span::new(at, at),
leading: on_eof,
value: zerodds_idl_cst::TokenValue::None,
});
self.map.push(Origin {
file,
span: Span::new(at, at),
macro_chain: Vec::new(),
});
}
fn define(&mut self, rest: &str, span: Span) {
let text = rest.trim_start();
let name_end = text
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || !c.is_ascii()))
.unwrap_or(text.len());
let name = &text[..name_end];
if name.is_empty() {
self.report(Diagnostic::error(
DiagnosticCode::MissingToken,
span,
"`#define` without a name".to_string(),
"OMG IDL 4.2 §7.3",
));
return;
}
let after = &text[name_end..];
let defined = if let Some(list) = after.strip_prefix('(') {
let Some(close) = list.find(')') else {
self.preprocessing_error(
span,
format!("the parameter list of `{name}` is not closed"),
"ISO/IEC 14882:2003 §16.3",
);
return;
};
let params: Vec<String> = if list[..close].trim().is_empty() {
Vec::new()
} else {
list[..close]
.split(',')
.map(|param| param.trim().to_string())
.collect()
};
self.macros
.define_function(name, params, &list[close + 1..])
} else {
self.macros.define(name, after)
};
if let Err(why) = defined {
self.preprocessing_error(
span,
format!("`#define {name}`: {why}"),
"ISO/IEC 14882:2003 §16.3",
);
}
}
#[allow(clippy::too_many_lines)]
fn directive(&mut self, raw: &str, span: Span, cursor: &mut FileCursor<'_>) {
let line = splice(raw);
let body = line.trim_start().trim_start_matches('#').trim();
let (word, rest) = split_word(body);
let keeping = cursor.keeping();
let file = cursor.file;
match word {
"define" if keeping => self.define(rest, span),
"error" if keeping => {
let text = rest.trim();
let message = if text.is_empty() {
String::from("`#error`")
} else {
format!("`#error {text}`")
};
self.report(Diagnostic::error(
DiagnosticCode::ErrorDirective,
span,
message,
"OMG IDL 4.2 §7.3, ISO/IEC 14882:2003 §16.5",
));
}
"undef" if keeping => {
let (name, _) = split_word(rest);
if macro_::RESERVED.contains(&name) {
self.preprocessing_error(
span,
format!("`{name}` cannot be undefined"),
"ISO/IEC 14882:2003 §16.8/4",
);
} else {
self.macros.undefine(name);
}
}
"include" if keeping => {
let Some(target) = include_target(rest) else {
self.report(Diagnostic::error(
DiagnosticCode::UnexpectedToken,
span,
"`#include` expects \"path\" or <path>".to_string(),
"OMG IDL 4.2 §7.3",
));
return;
};
let from = self.map.path(file).map(ToOwned::to_owned);
match self.loader.load(&target, from.as_deref()) {
Some((resolved, text)) => self.include(&resolved, &text, cursor.depth + 1),
None => self.report(Diagnostic::error(
DiagnosticCode::UnexpectedToken,
span,
format!("cannot find `{target}`"),
"OMG IDL 4.2 §7.3",
)),
}
}
"line" if keeping => self.line_control(rest, span, cursor),
"ifdef" => {
let (name, _) = split_word(rest);
cursor
.conditions
.push(Condition::new(keeping && self.macros.is_defined(name)));
}
"ifndef" => {
let (name, _) = split_word(rest);
cursor
.conditions
.push(Condition::new(keeping && !self.macros.is_defined(name)));
}
"if" => {
let taken = keeping && self.evaluate_condition(rest, span, file);
cursor.conditions.push(Condition::new(taken));
}
"elif" => {
let outer = cursor.conditions.len().saturating_sub(1);
let enclosing_keeps = cursor.conditions[..outer].iter().all(Condition::keeping);
let open = cursor
.conditions
.last()
.map(|condition| condition.satisfied);
match open {
Some(satisfied) => {
let taken = enclosing_keeps
&& !satisfied
&& self.evaluate_condition(rest, span, file);
if let Some(condition) = cursor.conditions.last_mut() {
condition.elif(taken);
}
}
None => self.unmatched(span, "#elif"),
}
}
"else" => match cursor.conditions.last_mut() {
Some(condition) => condition.otherwise(),
None => self.unmatched(span, "#else"),
},
"endif" => {
if cursor.conditions.pop().is_none() {
self.unmatched(span, "#endif");
}
}
"pragma" if keeping && rest.trim() == "once" => {
if let Some(current) = self.open.last().cloned()
&& !self.once.contains(¤t)
{
self.once.push(current);
}
}
"pragma" => {
if keeping {
self.pending_pragmas.push((
line.trim().to_string(),
Trivia {
kind: TriviaKind::Directive,
span,
},
file,
));
}
}
_ => {}
}
}
fn line_control(&mut self, rest: &str, span: Span, cursor: &FileCursor<'_>) {
let tokens = self.expand_line(rest, span, cursor.file);
let mut texts = tokens.iter().map(|token| token.text.as_str());
let number = texts.next().and_then(|digits| {
if digits.bytes().all(|b| b.is_ascii_digit()) {
digits.parse::<i64>().ok()
} else {
None
}
});
let name = match texts.next() {
None => None,
Some(literal) => match unquote(literal) {
Some(name) => Some(name),
None => {
self.preprocessing_error(
span,
format!("`#line` names its file with a string literal, not `{literal}`"),
"ISO/IEC 14882:2003 §16.4/4",
);
return;
}
},
};
let Some(number) = number.filter(|n| (1..=2_147_483_647).contains(n)) else {
self.preprocessing_error(
span,
"`#line` takes a line number from 1 to 2147483647".to_string(),
"ISO/IEC 14882:2003 §16.4/3",
);
return;
};
let next = line_of(cursor.source, span.end) + 1;
let delta = number - i64::from(next);
let name = name.or_else(|| {
self.presumed
.iter()
.rev()
.find(|(of, _, _)| *of == cursor.file)
.and_then(|(_, _, name)| name.clone())
});
self.presumed.push((cursor.file, delta, name));
}
fn expand_line(&mut self, rest: &str, span: Span, file: FileId) -> Vec<PpToken> {
match pptoken::tokenize(rest) {
Ok(pieces) => self.expand_isolated(
pieces
.into_iter()
.map(|piece| {
PpToken::made(piece.text, piece.space_before, Origin::literal(file, span))
})
.collect(),
),
Err(why) => {
self.preprocessing_error(span, why, "ISO/IEC 14882:2003 §2.4");
Vec::new()
}
}
}
fn evaluate_condition(&mut self, rest: &str, span: Span, file: FileId) -> bool {
let pieces = match pptoken::tokenize(rest) {
Ok(pieces) => pieces,
Err(why) => {
self.preprocessing_error(span, why, "ISO/IEC 14882:2003 §2.4");
return false;
}
};
let mut tokens = Vec::new();
let mut index = 0;
while index < pieces.len() {
let piece = &pieces[index];
if piece.text != "defined" {
tokens.push(PpToken::made(
piece.text.clone(),
piece.space_before,
Origin::literal(file, span),
));
index += 1;
continue;
}
let (name, next) = if pieces.get(index + 1).is_some_and(|p| p.text == "(") {
let closed = pieces.get(index + 3).is_some_and(|p| p.text == ")");
(pieces.get(index + 2).filter(|_| closed), index + 4)
} else {
(pieces.get(index + 1), index + 2)
};
let Some(name) = name.filter(|name| pptoken::is_identifier(&name.text)) else {
self.preprocessing_error(
span,
"`defined` is followed by a macro name, alone or in parentheses".to_string(),
"ISO/IEC 14882:2003 §16.1/1",
);
return false;
};
let answer = if self.macros.is_defined(&name.text)
|| macro_::RESERVED[1..].contains(&name.text.as_str())
{
"1"
} else {
"0"
};
tokens.push(PpToken::made(
answer.to_string(),
piece.space_before,
Origin::literal(file, span),
));
index = next;
}
let expanded = self.expand_isolated(tokens);
if expanded.iter().any(|token| token.text == "defined") {
self.preprocessing_error(
span,
"a macro in this condition expands to `defined`".to_string(),
"ISO/IEC 14882:2003 §16.1/4",
);
return false;
}
let texts: Vec<String> = expanded.into_iter().map(|token| token.text).collect();
match condition::evaluate(&texts) {
Ok(outcome) => {
for name in outcome.zeroed {
self.report(Diagnostic::warning(
DiagnosticCode::Preprocessing,
span,
format!("`{name}` in this condition names no macro and is taken as 0"),
"ISO/IEC 14882:2003 §16.1/4",
));
}
outcome.value
}
Err(why) => {
self.preprocessing_error(
span,
format!("`#if {}`: {why}", rest.trim()),
"ISO/IEC 14882:2003 §16.1",
);
false
}
}
}
fn unmatched(&mut self, span: Span, what: &str) {
self.report(Diagnostic::error(
DiagnosticCode::UnexpectedToken,
span,
format!("`{what}` without a matching `#if`"),
"OMG IDL 4.2 §7.3",
));
}
}
fn splice(line: &str) -> String {
line.replace("\\\r\n", "").replace("\\\n", "")
}
fn line_of(source: &str, at: u32) -> u32 {
let end = usize::try_from(at).unwrap_or(usize::MAX).min(source.len());
let newlines = source.as_bytes()[..end]
.iter()
.filter(|byte| **byte == b'\n')
.count();
u32::try_from(newlines)
.unwrap_or(u32::MAX)
.saturating_add(1)
}
fn quote(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
out.push('"');
for ch in text.chars() {
if ch == '\\' || ch == '"' {
out.push('\\');
}
out.push(ch);
}
out.push('"');
out
}
fn unquote(literal: &str) -> Option<String> {
let inner = literal.strip_prefix('"')?.strip_suffix('"')?;
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
out.push(chars.next()?);
} else {
out.push(ch);
}
}
Some(out)
}
fn stringize(argument: &[PpToken]) -> String {
let mut spelled = String::new();
for (index, token) in argument.iter().enumerate() {
if index > 0 && token.space_before {
spelled.push(' ');
}
let literal = token.text.starts_with(['"', '\''])
|| token.text.starts_with("L\"")
|| token.text.starts_with("L'");
if literal {
for ch in token.text.chars() {
if ch == '\\' || ch == '"' {
spelled.push('\\');
}
spelled.push(ch);
}
} else {
spelled.push_str(&token.text);
}
}
format!("\"{spelled}\"")
}
#[derive(Debug, Clone, Copy)]
struct Condition {
active: bool,
satisfied: bool,
}
impl Condition {
const fn new(taken: bool) -> Self {
Self {
active: taken,
satisfied: taken,
}
}
const fn keeping(&self) -> bool {
self.active
}
const fn elif(&mut self, taken: bool) {
self.active = taken && !self.satisfied;
if self.active {
self.satisfied = true;
}
}
const fn otherwise(&mut self) {
self.active = !self.satisfied;
self.satisfied = true;
}
}
fn split_word(text: &str) -> (&str, &str) {
let trimmed = text.trim_start();
match trimmed.find(char::is_whitespace) {
Some(at) => (&trimmed[..at], &trimmed[at..]),
None => (trimmed, ""),
}
}
fn include_target(rest: &str) -> Option<String> {
let text = rest.trim();
let inner = text
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.or_else(|| text.strip_prefix('<').and_then(|s| s.strip_suffix('>')))?;
Some(inner.to_owned())
}