use std::collections::HashMap;
use std::path::{Path, PathBuf};
use rucc_base::{Interner, Symbol};
use rucc_diag::{Diagnostic, FileId, SourceMapFull, Span};
use rucc_gnu::Kind;
use rucc_lex::{Options, PpToken, PpTokenKind, Punct, TokenFlags, tokenize};
use rucc_session::{Found, IncludeForm};
use rucc_target::TargetInfo;
use crate::cond;
use crate::embed;
use crate::expand::Expander;
use crate::include::{
Context, Frame, Header, Reader, directory_of, header_from_token, header_from_tokens, spelling,
};
use crate::macros::{Builtin, MacroTable, parse_define};
use crate::predef::{BUILT_IN, COMMAND_LINE, Predef, built_in, command_line};
use crate::token::Tok;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Guard {
Once,
Macro(Symbol),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Scan {
Start,
Inside(Symbol),
Closed(Symbol),
No,
}
#[derive(Debug)]
struct Cond {
span: Span,
live: bool,
taken: bool,
enclosing_live: bool,
seen_else: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineDirective {
pub span: Span,
pub line: u32,
pub file: Option<Symbol>,
}
#[derive(Debug, Default)]
pub struct Preprocessor {
macros: MacroTable,
expander: Expander,
diagnostics: Vec<Diagnostic>,
conds: Vec<Cond>,
lines: Vec<LineDirective>,
stack: Vec<Frame>,
seen: HashMap<PathBuf, Guard>,
}
impl Preprocessor {
pub fn new() -> Preprocessor {
Preprocessor::default()
}
pub fn macros(&self) -> &MacroTable {
&self.macros
}
pub fn macros_mut(&mut self) -> &mut MacroTable {
&mut self.macros
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
std::mem::take(&mut self.diagnostics)
}
pub fn line_directives(&self) -> &[LineDirective] {
&self.lines
}
pub fn predefine(
&mut self,
target: &TargetInfo,
opts: &Predef,
cx: &mut Context<'_>,
) -> Result<(), SourceMapFull> {
let names = Names::new(cx.interner);
let file = self.synthetic(BUILT_IN, built_in(target, opts), cx, &names)?;
let start = cx.sources.file(file).start;
for (spelling, builtin) in Builtin::ALL {
let name = cx.interner.intern(spelling);
self.macros.define_builtin(name, builtin, Span::new(start, start));
}
let text = command_line(opts);
if !text.is_empty() {
self.synthetic(COMMAND_LINE, text, cx, &names)?;
}
Ok(())
}
fn synthetic(
&mut self,
name: &str,
text: String,
cx: &mut Context<'_>,
names: &Names,
) -> Result<FileId, SourceMapFull> {
let file = cx.sources.add(name, text.into_bytes())?;
let mut out = Vec::new();
self.stack.push(Frame { at: Span::DUMMY, path: PathBuf::from(name), dir: None, next: 0 });
self.process(file, &mut out, cx, names);
self.stack.clear();
debug_assert!(out.is_empty(), "{name} is directives only and produces no tokens");
Ok(file)
}
pub fn run(&mut self, file: FileId, cx: &mut Context<'_>) -> Vec<Tok> {
let names = Names::new(cx.interner);
let mut out = Vec::new();
let name = cx.sources.file(file).name.clone();
let dir = directory_of(&name);
self.stack.push(Frame { at: Span::DUMMY, path: PathBuf::from(name), dir, next: 0 });
self.process(file, &mut out, cx, &names);
self.stack.clear();
out
}
fn process(&mut self, file: FileId, out: &mut Vec<Tok>, cx: &mut Context<'_>, names: &Names) {
let bytes = cx.sources.file(file).shared_bytes();
let start = cx.sources.file(file).start;
let mut reader = Reader::new(bytes.as_slice(), start, cx.lex);
let depth_on_entry = self.conds.len();
let mut text: Vec<Tok> = Vec::new();
let mut body: Vec<PpToken> = Vec::new();
let mut scan = Scan::Start;
loop {
let was_live = self.live();
let first = reader.next(cx.interner);
if first.is_eof() {
break;
}
if is_directive(first) {
self.flush(&mut text, out, cx, names);
body.clear();
let name_tok = reader.next(cx.interner);
if name_tok.is_eof() || name_tok.flags.has(TokenFlags::START_OF_LINE) {
reader.put_back(name_tok);
continue;
}
body.push(name_tok);
if was_live && is_include(ident_of(&name_tok), names) {
if let Some(header) = reader.header_name(cx.interner) {
body.push(header);
}
}
reader.line(cx.interner, &mut body);
let opens =
matches!(scan, Scan::Start).then(|| guard_opener(&body, names)).flatten();
self.directive(&body, first.span, out, cx, names);
scan = match scan {
Scan::Start => match opens {
Some(name) if self.conds.len() == depth_on_entry + 1 => Scan::Inside(name),
_ => Scan::No,
},
Scan::Inside(name) if self.conds.len() == depth_on_entry => Scan::Closed(name),
Scan::Inside(name) => Scan::Inside(name),
Scan::Closed(_) | Scan::No => Scan::No,
};
} else {
body.clear();
reader.line(cx.interner, &mut body);
if self.live() {
text.push(Tok::new(first));
text.extend(body.iter().copied().map(Tok::new));
}
if !matches!(scan, Scan::Inside(_)) {
scan = Scan::No;
}
}
let complaints = reader.take_diagnostics();
if was_live || self.live() {
self.diagnostics.extend(complaints);
}
}
self.flush(&mut text, out, cx, names);
self.diagnostics.extend(reader.take_diagnostics());
if let Scan::Closed(name) = scan {
if self.macros.is_defined(name) {
if let Some(frame) = self.stack.last() {
self.seen.entry(frame.path.clone()).or_insert(Guard::Macro(name));
}
}
}
for cond in self.conds.drain(depth_on_entry..) {
self.diagnostics
.push(Diagnostic::error("unterminated `#if`", cond.span).with_code("E0330"));
}
}
fn live(&self) -> bool {
self.conds.last().is_none_or(|c| c.live)
}
fn flush(
&mut self,
text: &mut Vec<Tok>,
out: &mut Vec<Tok>,
cx: &mut Context<'_>,
names: &Names,
) {
if text.is_empty() {
return;
}
let taken = std::mem::take(text);
let expanded = self.expander.expand_toks(taken, &self.macros, cx.interner, cx.sources);
self.diagnostics.append(&mut self.expander.take_diagnostics());
let expanded = self.resolve_has(expanded, cx, names, Pass::Text);
self.pragma_operator(expanded, out, cx.interner, names);
}
fn directive(
&mut self,
body: &[PpToken],
hash: Span,
out: &mut Vec<Tok>,
cx: &mut Context<'_>,
names: &Names,
) {
let Some(first) = body.first().copied() else {
return;
};
let name = ident_of(&first);
let rest = &body[1..];
if name == Some(names.r#if) {
let value = self.live() && self.eval(rest, hash, cx, names);
self.open(hash, value);
return;
}
if name == Some(names.ifdef) || name == Some(names.ifndef) {
let want = name == Some(names.ifdef);
let value = self.live() && self.defined_check(rest, hash, want, names);
self.open(hash, value);
return;
}
if name == Some(names.elif) || name == Some(names.elifdef) || name == Some(names.elifndef) {
self.elif(name, rest, hash, cx, names);
return;
}
if name == Some(names.r#else) {
self.branch_else(rest, hash);
return;
}
if name == Some(names.endif) {
self.endif(rest, hash);
return;
}
if !self.live() {
return;
}
let interner = &mut *cx.interner;
if name == Some(names.define) {
let (def, diagnostics) = parse_define(rest, interner);
self.diagnostics.extend(diagnostics);
if let Some(def) = def {
if let Some(problem) = self.macros.define(def, interner) {
self.diagnostics.push(problem);
}
}
} else if name == Some(names.undef) {
self.undef(rest, hash, interner);
} else if name == Some(names.error) || name == Some(names.warning) {
self.message(rest, hash, name == Some(names.error), interner);
} else if name == Some(names.line) {
self.line(rest, hash, cx);
} else if name == Some(names.pragma) {
if rest.len() == 1 && ident_of(&rest[0]) == Some(names.once) {
self.pragma_once(hash);
} else {
self.pass_through(body, hash, out);
}
} else if name == Some(names.include) || name == Some(names.include_next) {
self.include(rest, hash, name == Some(names.include_next), out, cx, names);
} else if name == Some(names.embed) {
self.embed(rest, hash, out, cx);
} else {
self.diagnostics.push(
Diagnostic::error("invalid preprocessing directive", first.span).with_code("E0332"),
);
}
}
fn pragma_once(&mut self, hash: Span) {
if self.stack.len() <= 1 {
self.diagnostics.push(
Diagnostic::warning("`#pragma once` in the main file", hash).with_code("W0332"),
);
return;
}
if let Some(frame) = self.stack.last() {
self.seen.insert(frame.path.clone(), Guard::Once);
}
}
fn skip(&self, path: &Path) -> bool {
match self.seen.get(path) {
Some(Guard::Once) => true,
Some(Guard::Macro(name)) => self.macros.is_defined(*name),
None => false,
}
}
fn pass_through(&mut self, body: &[PpToken], hash: Span, out: &mut Vec<Tok>) {
let _ = self;
out.push(Tok::synthetic(
PpTokenKind::Punct(Punct::Hash),
None,
TokenFlags::START_OF_LINE,
hash,
));
out.extend(body.iter().copied().map(Tok::new));
}
fn include(
&mut self,
rest: &[PpToken],
hash: Span,
is_next: bool,
out: &mut Vec<Tok>,
cx: &mut Context<'_>,
names: &Names,
) {
let Some(header) = self.header_of(rest, hash, cx) else {
return;
};
let (form, relative_to, from) = self.where_to_look(&header, is_next, cx);
let found = cx.search.resolve(cx.fs, &header.name, form, relative_to.as_deref(), from);
let Some(found) = found else {
let tried = cx.search.tried(&header.name, form, relative_to.as_deref(), from);
let where_looked = if tried.is_empty() {
"the name is an absolute path, so the search path was not used".to_owned()
} else {
let list: Vec<String> =
tried.iter().map(|d| d.to_string_lossy().into_owned()).collect();
format!("searched: {}", list.join(", "))
};
self.diagnostics.push(
Diagnostic::error(format!("`{}` file not found", header.name), hash)
.with_code("E0341")
.note(where_looked, hash),
);
return;
};
if self.skip(&found.path) {
return;
}
if self.stack.len() >= cx.max_include_depth as usize {
let mut diagnostic =
Diagnostic::error("`#include` nested too deeply", hash).with_code("E0342").note(
"a header that includes itself with no include guard is the usual cause",
hash,
);
if let Some(outer) = self.stack.first().filter(|f| !f.at.is_dummy()) {
diagnostic = diagnostic.note("the outermost include is here", outer.at);
}
self.diagnostics.push(diagnostic);
return;
}
let added = cx.sources.add_shared(found.name.clone(), found.bytes.clone(), Some(hash));
let file = match added {
Ok(file) => file,
Err(full) => {
self.diagnostics.push(Diagnostic::error(full.to_string(), hash).with_code("E0344"));
return;
}
};
self.stack.push(Frame {
at: hash,
dir: found.path.parent().map(Path::to_path_buf),
path: found.path,
next: found.next,
});
self.process(file, out, cx, names);
self.stack.pop();
}
fn embed(&mut self, rest: &[PpToken], hash: Span, out: &mut Vec<Tok>, cx: &mut Context<'_>) {
let Some((header, params)) = self.embed_line(rest, hash, cx) else {
return;
};
let Some(found) = self.find(&header, false, cx) else {
self.diagnostics.push(
Diagnostic::error(format!("`{}` resource not found", header.name), hash)
.with_code("E0341")
.note("an `#embed` resource is looked for on the include path", hash),
);
return;
};
embed::tokens(found.bytes.as_slice(), ¶ms, hash, cx.interner, out);
}
fn embed_line(
&mut self,
rest: &[PpToken],
hash: Span,
cx: &mut Context<'_>,
) -> Option<(Header, embed::Params)> {
if rest.is_empty() {
self.bad_header(hash);
return None;
}
let line: Vec<Tok> = rest.iter().copied().map(Tok::new).collect();
let line = if line[0].kind == PpTokenKind::HeaderName {
line
} else {
let expanded = self.expander.expand_toks(line, &self.macros, cx.interner, cx.sources);
self.diagnostics.append(&mut self.expander.take_diagnostics());
expanded
};
let Some(used) = embed::header_length(&line) else {
self.bad_header(line.first().map_or(hash, |t| t.report_span()));
return None;
};
let header = if line[0].kind == PpTokenKind::HeaderName {
header_from_token(spelling(line[0], cx.interner))
} else {
let spellings: Vec<&str> =
line[..used].iter().map(|t| spelling(*t, cx.interner)).collect();
header_from_tokens(&spellings)
};
let Some(header) = header else {
self.bad_header(line[0].report_span());
return None;
};
let params = self.embed_params(&line[used..], hash, cx)?;
Some((header, params))
}
fn embed_params(
&mut self,
line: &[Tok],
at: Span,
cx: &mut Context<'_>,
) -> Option<embed::Params> {
let Preprocessor { expander, macros, diagnostics, .. } = self;
let sources = &mut *cx.sources;
let mut expand = |toks: Vec<Tok>, interner: &mut Interner| {
expander.expand_toks(toks, macros, interner, sources)
};
let params = embed::parse(line, at, cx.interner, diagnostics, &mut expand);
self.diagnostics.append(&mut self.expander.take_diagnostics());
params
}
fn where_to_look(
&self,
header: &Header,
is_next: bool,
cx: &Context<'_>,
) -> (IncludeForm, Option<PathBuf>, usize) {
let form = if header.angled { IncludeForm::Angled } else { IncludeForm::Quoted };
let frame = self.stack.last();
let from = if is_next {
frame.map_or(0, |f| f.next).max(cx.search.start(form))
} else {
cx.search.start(form)
};
let relative_to = if is_next { None } else { frame.and_then(|f| f.dir.clone()) };
(form, relative_to, from)
}
fn find(&self, header: &Header, is_next: bool, cx: &Context<'_>) -> Option<Found> {
let (form, relative_to, from) = self.where_to_look(header, is_next, cx);
cx.search.resolve(cx.fs, &header.name, form, relative_to.as_deref(), from)
}
fn header_of(&mut self, rest: &[PpToken], hash: Span, cx: &mut Context<'_>) -> Option<Header> {
if let Some(first) = rest.first().copied() {
if first.kind == PpTokenKind::HeaderName {
let text = first.value.map_or("", |v| cx.interner.resolve(v));
let header = header_from_token(text);
if header.is_none() {
self.bad_header(first.span);
}
self.extra_tokens(&rest[1..], "#include");
return header;
}
}
if rest.is_empty() {
self.bad_header(hash);
return None;
}
let line: Vec<Tok> = rest.iter().copied().map(Tok::new).collect();
let expanded = self.expander.expand_toks(line, &self.macros, cx.interner, cx.sources);
self.diagnostics.append(&mut self.expander.take_diagnostics());
let spellings: Vec<&str> = expanded.iter().map(|t| spelling(*t, cx.interner)).collect();
let header = header_from_tokens(&spellings);
if header.is_none() {
let at = expanded.first().map_or(hash, |t| t.report_span());
self.bad_header(at);
}
header
}
fn bad_operand(&mut self, tok: Tok, at: Span, interner: &Interner) {
self.diagnostics.push(
Diagnostic::error(
format!("expected an identifier as the operand of `{}`", spelling(tok, interner)),
at,
)
.with_code("E0345"),
);
}
fn bad_header(&mut self, at: Span) {
self.diagnostics.push(
Diagnostic::error("expected a file name in `<>` or `\"\"`", at).with_code("E0343"),
);
}
fn open(&mut self, span: Span, value: bool) {
let enclosing_live = self.live();
self.conds.push(Cond {
span,
live: enclosing_live && value,
taken: value,
enclosing_live,
seen_else: false,
});
}
fn elif(
&mut self,
name: Option<Symbol>,
rest: &[PpToken],
hash: Span,
cx: &mut Context<'_>,
names: &Names,
) {
let Some(top) = self.conds.last() else {
self.stray("elif", hash);
return;
};
if top.seen_else {
self.diagnostics
.push(Diagnostic::error("`#elif` after `#else`", hash).with_code("E0333"));
return;
}
let (enclosing_live, already_taken) = (top.enclosing_live, top.taken);
let consider = enclosing_live && !already_taken;
let value = if !consider {
false
} else if name == Some(names.elif) {
self.eval(rest, hash, cx, names)
} else {
self.defined_check(rest, hash, name == Some(names.elifdef), names)
};
let top = self.conds.last_mut().expect("checked above and nothing popped");
top.live = consider && value;
top.taken = already_taken || value;
}
fn branch_else(&mut self, rest: &[PpToken], hash: Span) {
let Some(top) = self.conds.last_mut() else {
self.stray("else", hash);
return;
};
if top.seen_else {
self.diagnostics.push(Diagnostic::error("a second `#else`", hash).with_code("E0333"));
return;
}
top.live = top.enclosing_live && !top.taken;
top.taken = true;
top.seen_else = true;
let enclosing_live = top.enclosing_live;
if enclosing_live {
self.extra_tokens(rest, "#else");
}
}
fn endif(&mut self, rest: &[PpToken], hash: Span) {
if self.conds.pop().is_none() {
self.stray("endif", hash);
return;
}
if self.live() {
self.extra_tokens(rest, "#endif");
}
}
fn stray(&mut self, what: &str, hash: Span) {
self.diagnostics
.push(Diagnostic::error(format!("`#{what}` without `#if`"), hash).with_code("E0334"));
}
fn extra_tokens(&mut self, rest: &[PpToken], what: &str) {
if let Some(first) = rest.first() {
self.diagnostics.push(
Diagnostic::warning(format!("extra tokens after `{what}`"), first.span)
.with_code("W0330"),
);
}
}
fn eval(&mut self, rest: &[PpToken], hash: Span, cx: &mut Context<'_>, names: &Names) -> bool {
let line: Vec<Tok> = rest.iter().copied().map(Tok::new).collect();
let line = self.resolve_defined(line, cx.interner, names);
let line = self.resolve_has(line, cx, names, Pass::Headers);
let line = self.expander.expand_toks(line, &self.macros, cx.interner, cx.sources);
self.diagnostics.append(&mut self.expander.take_diagnostics());
let line = self.resolve_defined(line, cx.interner, names);
let line = self.resolve_has(line, cx, names, Pass::Rest);
cond::evaluate(&line, cx.interner, &mut self.diagnostics, hash)
}
fn resolve_has(
&mut self,
line: Vec<Tok>,
cx: &mut Context<'_>,
names: &Names,
pass: Pass,
) -> Vec<Tok> {
if !line.iter().any(|t| t.ident().is_some_and(|n| names.has.op(n).is_some())) {
return line;
}
let mut out = Vec::with_capacity(line.len());
let mut at = 0;
while at < line.len() {
let tok = line[at];
let op = tok.ident().and_then(|n| names.has.op(n));
let Some(op) = op.filter(|op| pass.answers(*op)) else {
if pass == Pass::Text && op.is_some_and(Op::is_header) {
self.outside_a_directive(tok, cx);
}
out.push(tok);
at += 1;
continue;
};
let Some((operand, after)) = arguments(&line, at + 1) else {
if pass != Pass::Headers {
self.diagnostics.push(
Diagnostic::error(
format!("expected `(` after `{}`", spelling(tok, cx.interner)),
tok.report_span(),
)
.with_code("E0345"),
);
}
out.push(tok);
at += 1;
continue;
};
at = after;
let value = self.ask(op, operand, tok, cx);
let sym = cx.interner.intern(&value.to_string());
out.push(Tok::synthetic(PpTokenKind::Number, Some(sym), tok.flags, tok.report_span()));
}
out
}
fn outside_a_directive(&mut self, tok: Tok, cx: &Context<'_>) {
self.diagnostics.push(
Diagnostic::error(
format!(
"`{}` used outside of a preprocessing directive",
spelling(tok, cx.interner)
),
tok.report_span(),
)
.with_code("E0350"),
);
}
fn ask(&mut self, op: Op, operand: &[Tok], tok: Tok, cx: &mut Context<'_>) -> u32 {
let at = operand.first().map_or(tok.report_span(), |t| t.report_span());
match op {
Op::Include | Op::IncludeNext => {
let spellings: Vec<&str> =
operand.iter().map(|t| spelling(*t, cx.interner)).collect();
let Some(header) = header_from_tokens(&spellings) else {
self.bad_header(at);
return 0;
};
u32::from(self.find(&header, op == Op::IncludeNext, cx).is_some())
}
Op::Embed => {
let Some(used) = embed::header_length(operand) else {
self.bad_header(at);
return 0;
};
let header = if operand[0].kind == PpTokenKind::HeaderName {
header_from_token(spelling(operand[0], cx.interner))
} else {
let spellings: Vec<&str> =
operand[..used].iter().map(|t| spelling(*t, cx.interner)).collect();
header_from_tokens(&spellings)
};
let Some(header) = header else {
self.bad_header(at);
return 0;
};
let Some(params) = self.embed_params(&operand[used..], at, cx) else {
return 0;
};
match self.find(&header, false, cx) {
None => 0,
Some(found) => {
let taken = params.taken(found.bytes.as_slice().len() as u64);
if taken == 0 { 2 } else { 1 }
}
}
}
Op::BuildingModule => {
if attribute_name(operand, cx.interner).is_none() {
self.bad_operand(tok, at, cx.interner);
}
0
}
Op::Table(kind) => {
let Some(name) = attribute_name(operand, cx.interner) else {
self.bad_operand(tok, at, cx.interner);
return 0;
};
match kind {
Kind::Attribute => rucc_gnu::has_attribute(name),
Kind::CAttribute => rucc_gnu::has_c_attribute(name),
Kind::Builtin => rucc_gnu::has_builtin(name),
Kind::Feature => rucc_gnu::has_feature(name),
Kind::Extension => rucc_gnu::has_extension(name),
}
}
}
}
fn resolve_defined(
&mut self,
line: Vec<Tok>,
interner: &mut Interner,
names: &Names,
) -> Vec<Tok> {
if !line.iter().any(|t| t.ident() == Some(names.defined)) {
return line;
}
let mut out = Vec::with_capacity(line.len());
let mut at = 0;
while at < line.len() {
let tok = line[at];
if tok.ident() != Some(names.defined) {
out.push(tok);
at += 1;
continue;
}
let parenthesised = line.get(at + 1).is_some_and(|t| t.is(Punct::LParen));
let name_at = if parenthesised { at + 2 } else { at + 1 };
let name = line.get(name_at).and_then(|t| t.ident());
let Some(name) = name else {
self.diagnostics.push(
Diagnostic::error("`defined` without a macro name", tok.report_span())
.with_code("E0335"),
);
out.push(tok);
at += 1;
continue;
};
at = name_at + 1;
if parenthesised {
if line.get(at).is_some_and(|t| t.is(Punct::RParen)) {
at += 1;
} else {
self.diagnostics.push(
Diagnostic::error("expected `)` after `defined`", tok.report_span())
.with_code("E0335"),
);
}
}
let value = self.macros.is_defined(name) || names.has.op(name).is_some();
out.push(number(value, tok.flags, tok.report_span(), interner));
}
out
}
fn defined_check(
&mut self,
rest: &[PpToken],
hash: Span,
want_defined: bool,
names: &Names,
) -> bool {
let Some(name) = rest.first().and_then(ident_of) else {
self.diagnostics.push(
Diagnostic::error("expected a macro name", rest.first().map_or(hash, |t| t.span))
.with_code("E0336"),
);
return false;
};
self.extra_tokens(&rest[1..], if want_defined { "#ifdef" } else { "#ifndef" });
let defined = self.macros.is_defined(name) || names.has.op(name).is_some();
defined == want_defined
}
fn undef(&mut self, rest: &[PpToken], hash: Span, interner: &Interner) {
let Some(name) = rest.first().and_then(ident_of) else {
self.diagnostics.push(
Diagnostic::error("expected a macro name", rest.first().map_or(hash, |t| t.span))
.with_code("E0336"),
);
return;
};
let text = interner.resolve(name);
if text == "defined" || text.starts_with("__STDC_") {
self.diagnostics.push(
Diagnostic::error(format!("`{text}` cannot be undefined"), rest[0].span)
.with_code("E0337"),
);
return;
}
self.macros.undef(name);
self.extra_tokens(&rest[1..], "#undef");
}
fn message(&mut self, rest: &[PpToken], hash: Span, fatal: bool, interner: &Interner) {
let text = spell_line(rest, interner);
let span = rest.first().map_or(hash, |t| t.span.to(last_span(rest)));
let diag = if fatal {
Diagnostic::error(text, span).with_code("E0338")
} else {
Diagnostic::warning(text, span).with_code("W0331")
};
self.diagnostics.push(diag);
}
fn line(&mut self, rest: &[PpToken], hash: Span, cx: &mut Context<'_>) {
let line: Vec<Tok> = rest.iter().copied().map(Tok::new).collect();
let line = self.expander.expand_toks(line, &self.macros, cx.interner, cx.sources);
self.diagnostics.append(&mut self.expander.take_diagnostics());
let interner = &mut *cx.interner;
let number_text = line
.first()
.filter(|t| t.kind == PpTokenKind::Number)
.and_then(|t| t.value)
.map(|v| interner.resolve(v));
let Some(parsed) = number_text.and_then(|t| t.parse::<u64>().ok()) else {
self.diagnostics.push(
Diagnostic::error(
"`#line` needs a decimal line number",
line.first().map_or(hash, |t| t.report_span()),
)
.with_code("E0339"),
);
return;
};
if parsed == 0 || parsed > 2_147_483_647 {
self.diagnostics.push(
Diagnostic::error("`#line` number is out of range", line[0].report_span())
.with_code("E0339"),
);
return;
}
let mut file = None;
if let Some(second) = line.get(1) {
if second.kind == PpTokenKind::StringLit {
file = second.value;
} else {
self.diagnostics.push(
Diagnostic::error(
"`#line` file name must be a string literal",
second.report_span(),
)
.with_code("E0339"),
);
return;
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "the range check above keeps this inside i32, let alone u32"
)]
self.lines.push(LineDirective { span: hash, line: parsed as u32, file });
}
fn pragma_operator(
&mut self,
expanded: Vec<Tok>,
out: &mut Vec<Tok>,
interner: &mut Interner,
names: &Names,
) {
if !expanded.iter().any(|t| t.ident() == Some(names.pragma_op)) {
out.extend(expanded);
return;
}
let mut at = 0;
while at < expanded.len() {
let tok = expanded[at];
if tok.ident() != Some(names.pragma_op) {
out.push(tok);
at += 1;
continue;
}
let open = expanded.get(at + 1).is_some_and(|t| t.is(Punct::LParen));
let text = expanded.get(at + 2).filter(|t| t.kind == PpTokenKind::StringLit);
let close = expanded.get(at + 3).is_some_and(|t| t.is(Punct::RParen));
let (Some(text), true, true) = (text, open, close) else {
self.diagnostics.push(
Diagnostic::error("`_Pragma` takes a single string literal", tok.report_span())
.with_code("E0340"),
);
out.push(tok);
at += 1;
continue;
};
let literal = text.value.map(|v| interner.resolve(v)).unwrap_or_default();
let body = destringize(literal);
self.emit_pragma(&body, tok, out, interner, names);
at += 4;
}
}
fn emit_pragma(
&mut self,
body: &str,
at: Tok,
out: &mut Vec<Tok>,
interner: &mut Interner,
names: &Names,
) {
let span = at.report_span();
let (tokens, diagnostics) = tokenize(body.as_bytes(), 0, Options::new(), interner);
self.diagnostics.extend(
diagnostics
.into_iter()
.map(|d| Diagnostic::new(d.severity, d.message, span).with_code("E0340")),
);
out.push(Tok::synthetic(
PpTokenKind::Punct(Punct::Hash),
None,
TokenFlags::START_OF_LINE,
span,
));
out.push(Tok::synthetic(PpTokenKind::Ident, Some(names.pragma), TokenFlags::EMPTY, span));
for (at, t) in tokens.into_iter().filter(|t| !t.is_eof()).enumerate() {
let spaced = at == 0 || t.flags.has(TokenFlags::LEADING_SPACE);
let flags = if spaced {
TokenFlags::EMPTY.with(TokenFlags::LEADING_SPACE)
} else {
TokenFlags::EMPTY
};
out.push(Tok::synthetic(t.kind, t.value, flags, span));
}
}
}
fn guard_opener(body: &[PpToken], names: &Names) -> Option<Symbol> {
let name = ident_of(body.first()?)?;
let rest = &body[1..];
if name == names.ifndef {
let [only] = rest else {
return None;
};
return ident_of(only);
}
if name != names.r#if {
return None;
}
let [bang, defined, tail @ ..] = rest else {
return None;
};
if bang.punct() != Some(Punct::Bang) || ident_of(defined) != Some(names.defined) {
return None;
}
match tail {
[only] => ident_of(only),
[open, only, close]
if open.punct() == Some(Punct::LParen) && close.punct() == Some(Punct::RParen) =>
{
ident_of(only)
}
_ => None,
}
}
fn is_include(name: Option<Symbol>, names: &Names) -> bool {
name == Some(names.include) || name == Some(names.include_next) || name == Some(names.embed)
}
fn is_directive(tok: PpToken) -> bool {
tok.flags.has(TokenFlags::START_OF_LINE) && tok.punct() == Some(Punct::Hash)
}
fn ident_of(tok: &PpToken) -> Option<Symbol> {
match tok.kind {
PpTokenKind::Ident => tok.value,
_ => None,
}
}
fn last_span(tokens: &[PpToken]) -> Span {
tokens.last().map_or(Span::DUMMY, |t| t.span)
}
fn number(value: bool, flags: TokenFlags, span: Span, interner: &mut Interner) -> Tok {
let sym = interner.intern(if value { "1" } else { "0" });
Tok::synthetic(PpTokenKind::Number, Some(sym), flags, span)
}
fn spell_line(tokens: &[PpToken], interner: &Interner) -> String {
let mut out = String::new();
for (index, tok) in tokens.iter().enumerate() {
if index > 0 && tok.flags.has(TokenFlags::LEADING_SPACE) {
out.push(' ');
}
match tok.value {
Some(sym) => out.push_str(interner.resolve(sym)),
None => {
if let Some(p) = tok.punct() {
out.push_str(p.as_str());
}
}
}
}
out
}
fn destringize(literal: &str) -> String {
let body = literal
.trim_start_matches(['L', 'u', 'U', '8'])
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(literal);
let mut out = String::with_capacity(body.len());
let mut chars = body.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('"') => out.push('"'),
Some('\\') => out.push('\\'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
fn arguments(line: &[Tok], at: usize) -> Option<(&[Tok], usize)> {
if !line.get(at)?.is(Punct::LParen) {
return None;
}
let mut depth = 1u32;
let mut end = at + 1;
while end < line.len() {
if line[end].is(Punct::LParen) {
depth += 1;
} else if line[end].is(Punct::RParen) {
depth -= 1;
if depth == 0 {
return Some((&line[at + 1..end], end + 1));
}
}
end += 1;
}
None
}
fn attribute_name<'i>(operand: &[Tok], interner: &'i Interner) -> Option<&'i str> {
let name = match operand {
[one] => one,
[_, scope, name] if scope.is(Punct::ColonColon) => name,
_ => return None,
};
name.ident().map(|sym| interner.resolve(sym))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Pass {
Headers,
Rest,
Text,
}
impl Pass {
fn answers(self, op: Op) -> bool {
match self {
Pass::Headers => op.is_header(),
Pass::Rest => true,
Pass::Text => !op.is_header(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Include,
IncludeNext,
Embed,
BuildingModule,
Table(Kind),
}
impl Op {
fn is_header(self) -> bool {
matches!(self, Op::Include | Op::IncludeNext | Op::Embed)
}
}
struct HasOps {
ops: [(Symbol, Op); 9],
range: (Symbol, Symbol),
}
impl HasOps {
fn new(interner: &mut Interner) -> HasOps {
let ops = [
(interner.intern("__has_include"), Op::Include),
(interner.intern("__has_include_next"), Op::IncludeNext),
(interner.intern("__has_embed"), Op::Embed),
(interner.intern("__has_attribute"), Op::Table(Kind::Attribute)),
(interner.intern("__has_c_attribute"), Op::Table(Kind::CAttribute)),
(interner.intern("__has_builtin"), Op::Table(Kind::Builtin)),
(interner.intern("__has_feature"), Op::Table(Kind::Feature)),
(interner.intern("__has_extension"), Op::Table(Kind::Extension)),
(interner.intern("__building_module"), Op::BuildingModule),
];
let mut range = (ops[0].0, ops[0].0);
for &(sym, _) in &ops {
range = (range.0.min(sym), range.1.max(sym));
}
HasOps { ops, range }
}
#[inline]
fn op(&self, name: Symbol) -> Option<Op> {
if name < self.range.0 || name > self.range.1 {
return None;
}
self.ops.iter().find(|(sym, _)| *sym == name).map(|(_, op)| *op)
}
}
struct Names {
define: Symbol,
undef: Symbol,
r#if: Symbol,
ifdef: Symbol,
ifndef: Symbol,
elif: Symbol,
elifdef: Symbol,
elifndef: Symbol,
r#else: Symbol,
endif: Symbol,
line: Symbol,
error: Symbol,
warning: Symbol,
pragma: Symbol,
include: Symbol,
include_next: Symbol,
embed: Symbol,
defined: Symbol,
once: Symbol,
pragma_op: Symbol,
has: HasOps,
}
impl Names {
fn new(interner: &mut Interner) -> Names {
Names {
define: interner.intern("define"),
undef: interner.intern("undef"),
r#if: interner.intern("if"),
ifdef: interner.intern("ifdef"),
ifndef: interner.intern("ifndef"),
elif: interner.intern("elif"),
elifdef: interner.intern("elifdef"),
elifndef: interner.intern("elifndef"),
r#else: interner.intern("else"),
endif: interner.intern("endif"),
line: interner.intern("line"),
error: interner.intern("error"),
warning: interner.intern("warning"),
pragma: interner.intern("pragma"),
include: interner.intern("include"),
include_next: interner.intern("include_next"),
embed: interner.intern("embed"),
defined: interner.intern("defined"),
once: interner.intern("once"),
pragma_op: interner.intern("_Pragma"),
has: HasOps::new(interner),
}
}
}
#[cfg(test)]
mod tests {
use rucc_diag::{Severity, SourceMap};
use rucc_session::{MemoryFileSystem, SearchPath};
use super::*;
use rucc_session::Std;
use crate::predef::Timestamp;
struct Run {
interner: Interner,
sources: SourceMap,
fs: MemoryFileSystem,
search: SearchPath,
pp: Preprocessor,
}
impl Run {
fn new() -> Run {
Run {
interner: Interner::new(),
sources: SourceMap::new(),
fs: MemoryFileSystem::new(),
search: SearchPath::new(),
pp: Preprocessor::new(),
}
}
fn file(&mut self, path: &str, contents: &str) {
self.fs.insert(path, contents.as_bytes().to_vec());
}
fn bytes(&mut self, path: &str, contents: &[u8]) {
self.fs.insert(path, contents.to_vec());
}
fn dir(&mut self, path: &str) {
self.search.push_bracket(path);
}
fn predefine(&mut self, triple: &str, opts: &Predef) {
let target = TargetInfo::new(triple.parse().expect("a supported triple"));
let mut cx =
Context::new(&mut self.interner, &mut self.sources, &self.fs, &self.search);
self.pp.predefine(&target, opts, &mut cx).expect("the map has room");
}
fn go(&mut self, src: &str) -> String {
self.go_named("/main.c", src)
}
fn go_named(&mut self, path: &str, src: &str) -> String {
let file = self.sources.add(path, src.as_bytes().to_vec()).expect("the map has room");
let out = {
let mut cx =
Context::new(&mut self.interner, &mut self.sources, &self.fs, &self.search);
self.pp.run(file, &mut cx)
};
let mut text = String::new();
for (at, tok) in out.iter().enumerate() {
let spaced = tok.flags.has(TokenFlags::LEADING_SPACE)
|| tok.flags.has(TokenFlags::START_OF_LINE);
if at > 0 && spaced {
text.push(' ');
}
match tok.kind {
PpTokenKind::Punct(p) => text.push_str(p.as_str()),
_ => text.push_str(
self.interner.resolve(tok.value.expect("every non-punctuator interns")),
),
}
}
text
}
fn files(&self) -> usize {
self.sources.files().len()
}
fn messages(&mut self) -> Vec<String> {
self.pp.take_diagnostics().into_iter().map(|d| d.message).collect()
}
fn severities(&mut self) -> Vec<Severity> {
self.pp.diagnostics().iter().map(|d| d.severity).collect()
}
}
fn clean(src: &str) -> String {
let mut run = Run::new();
let text = run.go(src);
assert!(run.messages().is_empty(), "expected no diagnostics from {src:?}");
text
}
#[test]
fn a_taken_branch_is_kept_and_the_other_is_not() {
assert_eq!(clean("#if 1\nyes\n#else\nno\n#endif\n"), "yes");
assert_eq!(clean("#if 0\nyes\n#else\nno\n#endif\n"), "no");
}
#[test]
fn ifdef_and_ifndef_ask_the_macro_table() {
assert_eq!(clean("#define F 1\n#ifdef F\nyes\n#endif\n"), "yes");
assert_eq!(clean("#ifdef F\nyes\n#endif\n"), "");
assert_eq!(clean("#ifndef F\nyes\n#endif\n"), "yes");
assert_eq!(clean("#define F 1\n#if 0\na\n#elifdef F\nb\n#endif\n"), "b");
assert_eq!(clean("#if 0\na\n#elifndef F\nb\n#endif\n"), "b");
}
#[test]
fn only_the_first_true_branch_of_a_chain_is_taken() {
assert_eq!(clean("#if 0\na\n#elif 1\nb\n#elif 1\nc\n#else\nd\n#endif\n"), "b");
assert_eq!(clean("#if 0\na\n#elif 0\nb\n#else\nc\n#endif\n"), "c");
}
#[test]
fn a_branch_after_one_that_was_taken_is_not_evaluated() {
assert_eq!(clean("#if 1\na\n#elif 1/0\nb\n#endif\n"), "a");
}
#[test]
fn a_skipped_region_is_not_read_for_anything_but_nesting() {
let src = "#if 0\nthis is not C at all\n#frobnicate\n#define\n#if 1\ninner\n#endif\n#endif\nafter\n";
assert_eq!(clean(src), "after");
}
#[test]
fn nesting_inside_a_dead_branch_stays_balanced() {
let src = "#if 0\n#ifdef X\na\n#else\nb\n#endif\n#else\nc\n#endif\n";
assert_eq!(clean(src), "c");
}
#[test]
fn defined_works_in_both_spellings_and_before_expansion() {
assert_eq!(clean("#define F 0\n#if defined F\nyes\n#endif\n"), "yes");
assert_eq!(clean("#define F 0\n#if defined(F)\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if defined(F)\nyes\n#endif\n"), "");
assert_eq!(clean("#define F 0\n#if defined F && !F\nyes\n#endif\n"), "yes");
}
#[test]
fn an_identifier_that_survived_expansion_is_zero() {
assert_eq!(clean("#if NOT_DEFINED_ANYWHERE\nyes\n#else\nno\n#endif\n"), "no");
assert_eq!(clean("#if !NOT_DEFINED_ANYWHERE\nyes\n#endif\n"), "yes");
}
#[test]
fn short_circuiting_keeps_a_guarded_expression_safe() {
assert_eq!(clean("#if defined(F) && 1/F\nyes\n#else\nno\n#endif\n"), "no");
assert_eq!(clean("#if 1 ? 2 : 1/0\nyes\n#endif\n"), "yes");
}
#[test]
fn the_operators_have_the_precedence_they_do_in_c() {
assert_eq!(clean("#if 1 + 2 * 3 == 7\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if (1 + 2) * 3 == 9\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if 1 << 4 == 16\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if -8 / 3 == -2\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if (0xff & 0x0f) == 15\nyes\n#endif\n"), "yes");
}
#[test]
fn an_unsigned_operand_makes_the_whole_comparison_unsigned() {
assert_eq!(clean("#if -1 < 0u\nyes\n#else\nno\n#endif\n"), "no");
assert_eq!(clean("#if -1 < 0\nyes\n#else\nno\n#endif\n"), "yes");
}
#[test]
fn character_constants_evaluate() {
assert_eq!(clean("#if 'A' == 65\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if '\\n' == 10\nyes\n#endif\n"), "yes");
}
#[test]
fn a_macro_is_expanded_before_the_expression_is_evaluated() {
assert_eq!(clean("#define V 3\n#if V > 2\nyes\n#endif\n"), "yes");
assert_eq!(clean("#define M(a) ((a) * 2)\n#if M(3) == 6\nyes\n#endif\n"), "yes");
}
#[test]
fn an_invocation_may_span_lines_within_a_run_of_text() {
assert_eq!(clean("#define M(a, b) a + b\nM(1,\n2)\n"), "1 + 2");
}
#[test]
fn undef_removes_a_definition() {
assert_eq!(clean("#define F 1\n#undef F\n#ifdef F\nyes\n#else\nno\n#endif\n"), "no");
assert_eq!(clean("#undef NEVER_DEFINED\nok\n"), "ok");
}
#[test]
fn some_names_cannot_be_undefined() {
let mut run = Run::new();
run.go("#undef defined\n");
assert_eq!(run.messages(), vec!["`defined` cannot be undefined".to_owned()]);
}
#[test]
fn error_reports_the_rest_of_the_line() {
let mut run = Run::new();
run.go("#if 0\n#error not this one\n#else\n#error unsupported target\n#endif\n");
assert_eq!(run.messages(), vec!["unsupported target".to_owned()]);
}
#[test]
fn warning_is_a_warning() {
let mut run = Run::new();
run.go("#warning this is fine\n");
assert_eq!(run.severities(), vec![Severity::Warning]);
assert_eq!(run.messages(), vec!["this is fine".to_owned()]);
}
#[test]
fn an_unterminated_conditional_is_reported() {
let mut run = Run::new();
assert_eq!(run.go("#if 1\nyes\n"), "yes");
assert_eq!(run.messages(), vec!["unterminated `#if`".to_owned()]);
}
#[test]
fn a_conditional_without_an_if_is_reported() {
let mut run = Run::new();
run.go("#endif\n");
assert_eq!(run.messages(), vec!["`#endif` without `#if`".to_owned()]);
let mut run = Run::new();
run.go("#if 1\n#else\n#else\n#endif\n");
assert_eq!(run.messages(), vec!["a second `#else`".to_owned()]);
let mut run = Run::new();
run.go("#if 1\n#else\n#elif 1\n#endif\n");
assert_eq!(run.messages(), vec!["`#elif` after `#else`".to_owned()]);
}
#[test]
fn tokens_after_endif_are_a_warning_rather_than_an_error() {
let mut run = Run::new();
assert_eq!(run.go("#if 1\nyes\n#endif FOO\n"), "yes");
assert_eq!(run.severities(), vec![Severity::Warning]);
assert_eq!(run.messages(), vec!["extra tokens after `#endif`".to_owned()]);
}
#[test]
fn the_null_directive_does_nothing() {
assert_eq!(clean("#\na\n#\nb\n"), "a b");
}
#[test]
fn an_unknown_directive_is_an_error_when_the_region_is_live() {
let mut run = Run::new();
run.go("#frobnicate\n");
assert_eq!(run.messages(), vec!["invalid preprocessing directive".to_owned()]);
}
#[test]
fn line_is_recorded_for_the_source_map() {
let mut run = Run::new();
run.go("#line 42 \"other.c\"\n");
assert!(run.messages().is_empty());
let recorded = run.pp.line_directives();
assert_eq!(recorded.len(), 1);
assert_eq!(recorded[0].line, 42);
let file = recorded[0].file.expect("a file name was given");
assert_eq!(run.interner.resolve(file), "\"other.c\"");
}
#[test]
fn a_line_number_out_of_range_is_refused() {
let mut run = Run::new();
run.go("#line 0\n");
assert_eq!(run.messages(), vec!["`#line` number is out of range".to_owned()]);
let mut run = Run::new();
run.go("#line notanumber\n");
assert_eq!(run.messages(), vec!["`#line` needs a decimal line number".to_owned()]);
}
#[test]
fn a_pragma_passes_through_unchanged() {
assert_eq!(clean("#pragma pack(1)\nint x;\n"), "#pragma pack(1) int x;");
}
#[test]
fn the_pragma_operator_becomes_a_pragma() {
assert_eq!(
clean("_Pragma(\"GCC visibility push(default)\")\nint x;\n"),
"#pragma GCC visibility push(default) int x;"
);
}
#[test]
fn the_pragma_operator_works_from_inside_a_macro() {
let src = "#define PUSH _Pragma(\"pack(push)\")\nPUSH\nint x;\n";
assert_eq!(clean(src), "#pragma pack(push) int x;");
}
#[test]
fn a_pragma_operator_that_is_not_given_a_string_is_reported() {
let mut run = Run::new();
run.go("_Pragma(x)\n");
assert_eq!(run.messages(), vec!["`_Pragma` takes a single string literal".to_owned()]);
}
#[test]
fn an_include_reads_the_file_it_names() {
let mut run = Run::new();
run.file("/dir/one.h", "int from_the_header;\n");
run.dir("/dir");
assert_eq!(run.go("#include <one.h>\nint after;\n"), "int from_the_header; int after;");
assert!(run.messages().is_empty());
}
#[test]
fn a_quoted_include_looks_next_to_the_including_file_first() {
let mut run = Run::new();
run.file("/local.h", "beside\n");
run.file("/dir/local.h", "on the path\n");
run.dir("/dir");
assert_eq!(run.go("#include \"local.h\"\n"), "beside");
assert!(run.messages().is_empty());
}
#[test]
fn an_angled_include_does_not_look_next_to_the_including_file() {
let mut run = Run::new();
run.file("/local.h", "beside\n");
run.file("/dir/local.h", "on the path\n");
run.dir("/dir");
assert_eq!(run.go("#include <local.h>\n"), "on the path");
}
#[test]
fn a_macro_defined_in_a_header_is_visible_after_the_include() {
let mut run = Run::new();
run.file("/dir/defs.h", "#define N 42\n");
run.dir("/dir");
assert_eq!(run.go("#include <defs.h>\nint a = N;\n"), "int a = 42;");
assert!(run.messages().is_empty());
}
#[test]
fn an_include_guard_keeps_the_second_read_empty() {
let mut run = Run::new();
run.file("/dir/g.h", "#ifndef G\n#define G\nonce\n#endif\n");
run.dir("/dir");
assert_eq!(run.go("#include <g.h>\n#include <g.h>\n"), "once");
assert!(run.messages().is_empty());
assert_eq!(run.files(), 2, "the second include is not opened at all");
}
#[test]
fn the_other_spelling_of_a_guard_is_recognised_too() {
for guard in ["#if !defined(G)", "#if !defined G"] {
let mut run = Run::new();
run.file("/dir/g.h", &format!("{guard}\n#define G\nonce\n#endif\n"));
run.dir("/dir");
assert_eq!(run.go("#include <g.h>\n#include <g.h>\n"), "once");
assert_eq!(run.files(), 2, "{guard} should be a guard");
}
}
#[test]
fn a_conditional_that_is_not_a_guard_does_not_skip_anything() {
let mut run = Run::new();
run.file("/dir/g.h", "#ifndef G\ntwice\n#endif\n");
run.dir("/dir");
assert_eq!(run.go("#include <g.h>\n#include <g.h>\n"), "twice twice");
assert_eq!(run.files(), 3);
}
#[test]
fn a_token_outside_the_guard_stops_it_being_a_guard() {
let mut run = Run::new();
run.file("/dir/g.h", "#ifndef G\n#define G\n#endif\nalways\n");
run.dir("/dir");
assert_eq!(run.go("#include <g.h>\n#include <g.h>\n"), "always always");
assert_eq!(run.files(), 3);
}
#[test]
fn pragma_once_skips_the_second_read_and_does_not_reach_the_output() {
let mut run = Run::new();
run.file("/dir/o.h", "#pragma once\nonce\n");
run.dir("/dir");
assert_eq!(run.go("#include <o.h>\n#include <o.h>\n"), "once");
assert!(run.messages().is_empty());
assert_eq!(run.files(), 2);
}
#[test]
fn pragma_once_in_the_main_file_is_a_warning() {
let mut run = Run::new();
assert_eq!(run.go("#pragma once\nx\n"), "x");
assert_eq!(run.severities(), vec![Severity::Warning]);
assert_eq!(run.messages(), vec!["`#pragma once` in the main file".to_owned()]);
}
#[test]
fn any_other_pragma_still_passes_through() {
assert_eq!(clean("#pragma once_upon_a_time\n"), "#pragma once_upon_a_time");
}
#[test]
fn has_include_answers_from_the_search_path() {
let mut run = Run::new();
run.file("/dir/there.h", "");
run.dir("/dir");
let src = "#if __has_include(<there.h>)\nyes\n#endif\n\
#if __has_include(<gone.h>)\nno\n#endif\n";
assert_eq!(run.go(src), "yes");
assert!(run.messages().is_empty(), "a header that is not there is an answer, not an error");
}
#[test]
fn has_include_asks_the_question_the_include_on_the_same_line_would() {
let mut run = Run::new();
run.file("/beside.h", "");
let src = "#if __has_include(\"beside.h\")\nquoted\n#endif\n\
#if __has_include(<beside.h>)\nangled\n#endif\n";
assert_eq!(run.go(src), "quoted");
}
#[test]
fn has_include_next_starts_where_include_next_would() {
let mut run = Run::new();
run.file("/a/both.h", "#if __has_include_next(<both.h>)\nmore\n#endif\n");
run.file("/b/both.h", "last\n");
run.file("/a/only.h", "#if __has_include_next(<only.h>)\nmore\n#endif\n");
run.dir("/a");
run.dir("/b");
assert_eq!(run.go("#include <both.h>\n"), "more");
assert_eq!(run.go("#include <only.h>\n"), "", "there is nothing after /a to find it in");
}
#[test]
fn the_operand_of_has_include_is_not_macro_expanded() {
let mut run = Run::new();
run.file("/dir/linux/version.h", "");
run.dir("/dir");
let src = "#define linux 1\n#if __has_include(<linux/version.h>)\nyes\n#endif\n";
assert_eq!(run.go(src), "yes");
}
#[test]
fn a_macro_may_expand_to_a_has_include() {
let mut run = Run::new();
run.file("/dir/there.h", "");
run.dir("/dir");
let src = "#define HAVE __has_include(<there.h>)\n#if HAVE\nyes\n#endif\n";
assert_eq!(run.go(src), "yes");
}
#[test]
fn defined_says_the_has_operators_are_there() {
let src = "#if defined(__has_include) && defined __has_builtin\nyes\n#endif\n";
assert_eq!(clean(src), "yes");
assert_eq!(clean("#ifdef __has_attribute\nyes\n#endif\n"), "yes");
}
#[test]
fn has_attribute_answers_out_of_the_matrix() {
assert_eq!(clean("#if __has_attribute(packed)\nyes\n#endif\n"), "");
assert_eq!(clean("#if __has_attribute(no_such_attribute)\nyes\n#endif\n"), "");
assert_eq!(clean("#if !__has_attribute(packed)\nno\n#endif\n"), "no");
}
#[test]
fn the_scoped_spelling_of_an_attribute_is_the_same_question() {
assert_eq!(clean("#if __has_c_attribute(gnu::packed)\nyes\n#endif\n"), "");
assert_eq!(clean("#if __has_c_attribute(deprecated)\nyes\n#endif\n"), "");
}
#[test]
fn has_builtin_answers_no_until_the_builtin_is_real() {
assert_eq!(clean("#if __has_builtin(__builtin_expect)\nyes\n#endif\n"), "");
assert_eq!(clean("#if __has_builtin(__builtin_nonesuch)\nyes\n#endif\n"), "");
}
#[test]
fn has_feature_and_has_extension_read_the_same_table() {
assert_eq!(clean("#if __has_feature(pragma_once)\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if __has_extension(pragma_once)\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if __has_extension(include_next)\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if __has_feature(include_next)\nyes\n#endif\n"), "");
assert_eq!(clean("#if __has_feature(statement_expressions)\nyes\n#endif\n"), "");
}
#[test]
fn building_module_is_always_no_and_is_recognised_so_that_the_line_parses() {
assert_eq!(clean("#if __building_module(m)\nyes\n#endif\n"), "");
assert_eq!(clean("#if !__building_module(m)\nyes\n#endif\n"), "yes");
assert_eq!(
clean(
"#if !defined(offsetof) || (__has_feature(modules) && !__building_module(x))\nyes\n#endif\n"
),
"yes"
);
assert_eq!(clean("#ifdef __building_module\nyes\n#endif\n"), "yes");
assert_eq!(clean("#if defined(__building_module)\nyes\n#endif\n"), "yes");
}
#[test]
fn a_has_operator_without_an_operand_is_reported() {
let mut run = Run::new();
run.go("#if __has_include\nyes\n#endif\n");
assert_eq!(run.messages(), ["expected `(` after `__has_include`"]);
let mut run = Run::new();
run.go("#if __has_include(1)\nyes\n#endif\n");
assert_eq!(run.messages(), ["expected a file name in `<>` or `\"\"`"]);
let mut run = Run::new();
run.go("#if __has_attribute(\"packed\")\nyes\n#endif\n");
assert_eq!(run.messages(), ["expected an identifier as the operand of `__has_attribute`"]);
}
#[test]
fn the_has_operators_answer_in_ordinary_text_too() {
assert_eq!(clean("f __has_feature(pragma_once)\n"), "f 1");
assert_eq!(clean("b __has_builtin(__builtin_expect)\n"), "b 0");
assert_eq!(clean("a __has_attribute(packed)\n"), "a 0");
assert_eq!(clean("c __has_c_attribute(deprecated)\n"), "c 0");
assert_eq!(clean("m __building_module(foo)\n"), "m 0");
}
#[test]
fn a_macro_that_expands_to_a_has_operator_is_answered_where_it_is_used() {
assert_eq!(clean("#define HAVE __has_feature(pragma_once)\nx HAVE\n"), "x 1");
assert_eq!(clean("#define HAVE(x) __has_attribute(x)\ny HAVE(packed)\n"), "y 0");
}
#[test]
fn a_has_operator_in_text_still_needs_its_operand() {
let mut run = Run::new();
run.go("tail __has_attribute;\n");
assert_eq!(run.messages(), ["expected `(` after `__has_attribute`"]);
}
#[test]
fn the_header_operators_are_refused_in_ordinary_text() {
let mut run = Run::new();
run.file("/dir/there.h", "");
run.dir("/dir");
run.go("a __has_include(<there.h>)\n");
assert_eq!(run.messages(), ["`__has_include` used outside of a preprocessing directive"]);
let mut run = Run::new();
run.go("b __has_include_next(\"x.h\")\n");
assert_eq!(
run.messages(),
["`__has_include_next` used outside of a preprocessing directive"]
);
}
#[test]
fn the_predefined_set_is_visible_to_the_source_file() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
let src = "#if defined(__x86_64__) && defined(__linux__) && __SIZEOF_LONG__ == 8\n\
yes\n#endif\n";
assert_eq!(run.go(src), "yes");
assert!(run.messages().is_empty());
}
#[test]
fn the_predefined_set_follows_the_target_and_not_the_host() {
let mut run = Run::new();
run.predefine("aarch64-unknown-linux-gnu", &Predef::new());
assert_eq!(
run.go("#ifdef __x86_64__\nno\n#endif\n#ifdef __aarch64__\nyes\n#endif\n"),
"yes"
);
}
#[test]
fn a_predefined_macro_expands_where_it_is_used() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("__SIZE_TYPE__ n;\n"), "long unsigned int n;");
}
#[test]
fn a_command_line_define_is_a_definition_like_any_other() {
let mut opts = Predef::new();
opts.defines = vec!["FOO".to_owned(), "BAR=3".to_owned()];
opts.undefines = vec!["__linux__".to_owned()];
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &opts);
let src = "#if FOO && BAR == 3 && !defined(__linux__)\nyes\n#endif\n";
assert_eq!(run.go(src), "yes");
assert!(run.messages().is_empty());
}
#[test]
fn the_predefined_set_produces_no_tokens_of_its_own() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("alone\n"), "alone");
}
#[test]
fn the_predefined_files_are_named_the_way_gcc_names_them() {
let mut run = Run::new();
let mut opts = Predef::new();
opts.defines = vec!["FOO=1".to_owned()];
run.predefine("x86_64-unknown-linux-gnu", &opts);
let names: Vec<&str> = run.sources.files().iter().map(|f| f.name.as_str()).collect();
assert_eq!(names, ["<built-in>", "<command-line>"]);
}
#[test]
fn a_dialect_without_the_gnu_extensions_says_so() {
let mut opts = Predef::new();
opts.gnu_extensions = false;
opts.std = Std::C99;
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &opts);
let src = "#if defined(__STRICT_ANSI__) && __STDC_VERSION__ == 199901L && !defined(linux)\n\
yes\n#endif\n";
assert_eq!(run.go(src), "yes");
}
#[test]
fn the_date_and_time_are_the_same_for_the_whole_translation_unit() {
let mut opts = Predef::new();
opts.timestamp = Timestamp::from_unix(0);
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &opts);
assert_eq!(run.go("__DATE__ __TIME__\n"), "\"Jan 1 1970\" \"00:00:00\"");
}
#[test]
fn a_has_operator_in_a_dead_branch_is_not_asked_about() {
assert_eq!(clean("#if 0\n#if __has_include\n#endif\n#endif\nafter\n"), "after");
}
#[test]
fn a_conditional_may_not_span_an_include() {
let mut run = Run::new();
run.file("/dir/open.h", "#if 1\n");
run.dir("/dir");
run.go("#include <open.h>\nkept\n#endif\n");
let messages = run.messages();
assert_eq!(messages.len(), 2);
assert!(messages[0].contains("unterminated"));
assert!(messages[1].contains("without"));
}
#[test]
fn include_next_continues_after_the_directory_the_file_came_from() {
let mut run = Run::new();
run.file("/a/limits.h", "wrapper\n#include_next <limits.h>\n");
run.file("/b/limits.h", "real\n");
run.dir("/a");
run.dir("/b");
assert_eq!(run.go("#include <limits.h>\n"), "wrapper real");
assert!(run.messages().is_empty());
}
#[test]
fn a_computed_include_is_expanded_first() {
let mut run = Run::new();
run.file("/dir/sub/thing.h", "computed\n");
run.dir("/dir");
let src = "#define HEADER <sub/thing.h>\n#include HEADER\n";
assert_eq!(run.go(src), "computed");
assert!(run.messages().is_empty());
let mut run = Run::new();
run.file("/dir/sub/thing.h", "computed\n");
run.dir("/dir");
assert_eq!(run.go("#define H \"sub/thing.h\"\n#include H\n"), "computed");
}
#[test]
fn a_header_that_is_not_there_says_where_it_looked() {
let mut run = Run::new();
run.dir("/dir");
run.go("#include <nope.h>\n");
let diagnostics = run.pp.take_diagnostics();
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code, Some("E0341"));
assert_eq!(diagnostics[0].message, "`nope.h` file not found");
assert!(diagnostics[0].children[0].message.contains("/dir"));
}
#[test]
fn an_include_that_is_not_a_header_name_is_reported() {
let mut run = Run::new();
run.go("#include 3\n");
let diagnostics = run.pp.take_diagnostics();
assert_eq!(diagnostics[0].code, Some("E0343"));
}
#[test]
fn a_header_that_includes_itself_stops() {
let mut run = Run::new();
run.file("/dir/loop.h", "#include <loop.h>\n");
run.dir("/dir");
run.go("#include <loop.h>\n");
let diagnostics = run.pp.take_diagnostics();
assert_eq!(diagnostics.len(), 1, "one complaint, not one per level");
assert_eq!(diagnostics[0].code, Some("E0342"));
}
#[test]
fn an_include_in_a_dead_branch_is_not_read() {
let mut run = Run::new();
assert_eq!(run.go("#if 0\n#include <nothing.h>\n#endif\nafter\n"), "after");
assert!(run.messages().is_empty(), "a skipped include is not resolved");
}
#[test]
fn embed_writes_the_bytes_of_the_resource() {
let mut run = Run::new();
run.bytes("/logo.bin", &[0, 1, 127, 128, 255]);
assert_eq!(run.go("#embed \"logo.bin\"\n"), "0, 1, 127, 128, 255");
assert!(run.messages().is_empty());
}
#[test]
fn an_embed_is_a_valid_initializer_on_both_sides_of_empty() {
let mut run = Run::new();
run.bytes("/some.bin", &[7, 8]);
run.bytes("/none.bin", &[]);
let line = |name: &str| {
format!("{{\n#embed \"{name}\" prefix(0xEF,) suffix(,0xFE) if_empty(0)\n}}\n")
};
assert_eq!(run.go(&line("some.bin")), "{ 0xEF,7, 8 ,0xFE }");
assert_eq!(run.go_named("/other.c", &line("none.bin")), "{ 0 }");
assert!(run.messages().is_empty());
}
#[test]
fn the_limit_and_the_offset_choose_a_window_of_the_resource() {
let mut run = Run::new();
run.bytes("/eight.bin", &[1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(run.go("#embed \"eight.bin\" limit(3)\n"), "1, 2, 3");
assert_eq!(
run.go_named("/b.c", "#embed \"eight.bin\" gnu::offset(4) limit(3)\n"),
"5, 6, 7"
);
assert_eq!(run.go_named("/c.c", "#embed \"eight.bin\" limit(0) if_empty(9)\n"), "9");
assert_eq!(run.go_named("/d.c", "#embed \"eight.bin\" gnu::offset(99)\n"), "");
assert!(run.messages().is_empty());
}
#[test]
fn the_limit_is_a_constant_expression_and_not_just_a_number() {
let mut run = Run::new();
run.bytes("/eight.bin", &[1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(
run.go("#define CHUNK 2\n#embed \"eight.bin\" limit(CHUNK * 2)\n"),
"1, 2, 3, 4"
);
assert!(run.messages().is_empty());
}
#[test]
fn a_misspelled_embed_parameter_is_refused_rather_than_ignored() {
let mut run = Run::new();
run.bytes("/eight.bin", &[1, 2]);
assert_eq!(run.go("#embed \"eight.bin\" limits(1)\n"), "");
assert_eq!(run.messages(), vec!["unknown `#embed` parameter `limits`".to_owned()]);
let mut vendor = Run::new();
vendor.bytes("/eight.bin", &[1, 2]);
assert_eq!(vendor.go("#embed \"eight.bin\" clang::offset(1)\n"), "");
assert_eq!(
vendor.messages(),
vec!["unknown `#embed` parameter `clang::offset`".to_owned()]
);
}
#[test]
fn a_missing_embed_resource_is_reported_as_a_resource() {
let mut run = Run::new();
run.go("#embed <nothing.bin>\n");
assert_eq!(run.messages(), vec!["`nothing.bin` resource not found".to_owned()]);
}
#[test]
fn has_embed_tells_missing_from_present_from_empty() {
let mut run = Run::new();
run.bytes("/some.bin", &[1]);
run.bytes("/none.bin", &[]);
let src = "#if __has_embed(\"none.bin\") == __STDC_EMBED_EMPTY__\nempty\n#endif\n\
#if __has_embed(\"some.bin\") == __STDC_EMBED_FOUND__\nfound\n#endif\n\
#if __has_embed(\"gone.bin\") == __STDC_EMBED_NOT_FOUND__\ngone\n#endif\n";
run.predefine("x86_64-unknown-linux-gnu", &Predef::default());
assert_eq!(run.go(src), "empty found gone");
assert!(run.messages().is_empty());
}
#[test]
fn has_embed_takes_the_limit_into_account() {
let mut run = Run::new();
run.bytes("/some.bin", &[1, 2, 3]);
run.predefine("x86_64-unknown-linux-gnu", &Predef::default());
let src = "#if __has_embed(\"some.bin\" limit(0)) == __STDC_EMBED_EMPTY__\nempty\n#endif\n";
assert_eq!(run.go(src), "empty");
assert!(run.messages().is_empty());
}
#[test]
fn a_directive_may_have_space_before_the_hash_and_after_it() {
assert_eq!(clean(" # define F 1\n#ifdef F\nyes\n#endif\n"), "yes");
}
#[test]
fn a_definition_survives_across_a_conditional() {
assert_eq!(clean("#if 1\n#define F 7\n#endif\nF\n"), "7");
}
#[test]
fn an_empty_if_expression_is_reported() {
let mut run = Run::new();
run.go("#if\n#endif\n");
assert_eq!(run.messages(), vec!["`#if` with no expression".to_owned()]);
}
#[test]
fn the_file_and_the_line_say_where_the_use_is() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("__FILE__ __LINE__\n__LINE__\n"), "\"/main.c\" 1 2");
assert!(run.messages().is_empty());
}
#[test]
fn a_macro_that_mentions_the_line_answers_with_the_call() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
run.file("/where.h", "#define WHERE __FILE__ __LINE__\n");
assert_eq!(run.go("#include \"where.h\"\n\n\nWHERE\n"), "\"/main.c\" 4");
assert!(run.messages().is_empty());
}
#[test]
fn the_file_name_is_the_file_without_the_directories() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go_named("/deep/down/main.c", "__FILE_NAME__\n"), "\"main.c\"");
}
#[test]
fn a_backslash_in_the_name_is_escaped() {
let mut run = Run::new();
run.predefine("x86_64-pc-windows-msvc", &Predef::new());
let text = run.go_named("C:\\src\\main.c", "__FILE__ __FILE_NAME__\n");
assert_eq!(text, "\"C:\\\\src\\\\main.c\" \"main.c\"");
}
#[test]
fn the_base_file_is_the_one_named_on_the_command_line() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
run.file("/deep.h", "__FILE__ __BASE_FILE__\n");
assert_eq!(run.go("#include \"deep.h\"\n"), "\"/deep.h\" \"/main.c\"");
assert!(run.messages().is_empty());
}
#[test]
fn the_include_level_counts_the_headers_above_it() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
run.file("/one.h", "__INCLUDE_LEVEL__\n#include \"two.h\"\n");
run.file("/two.h", "__INCLUDE_LEVEL__\n");
assert_eq!(run.go("__INCLUDE_LEVEL__\n#include \"one.h\"\n"), "0 1 2");
assert!(run.messages().is_empty());
}
#[test]
fn the_counter_is_a_different_number_every_time() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("__COUNTER__ __COUNTER__ __COUNTER__\n"), "0 1 2");
}
#[test]
fn the_counter_advances_once_per_argument_rather_than_once_per_use() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("#define TWICE(x) x x\nTWICE(__COUNTER__) __COUNTER__\n"), "0 0 1");
}
#[test]
fn the_line_is_a_number_an_if_can_use() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("#if __LINE__ == 1 && __INCLUDE_LEVEL__ == 0\nyes\n#endif\n"), "yes");
assert!(run.messages().is_empty());
}
#[test]
fn the_dynamic_macros_are_defined_like_any_others() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
let src = "#ifdef __FILE__\nyes\n#endif\n#undef __LINE__\n#ifndef __LINE__\ngone\n#endif\n";
assert_eq!(run.go(src), "yes gone");
assert!(run.messages().is_empty(), "`#undef` of a builtin is allowed, as it is in GCC");
}
#[test]
fn redefining_a_dynamic_macro_warns_and_points_at_the_built_in_file() {
let mut run = Run::new();
run.predefine("x86_64-unknown-linux-gnu", &Predef::new());
assert_eq!(run.go("#define __FILE__ \"mine.c\"\n__FILE__\n"), "\"mine.c\"");
let complaints = run.pp.take_diagnostics();
assert_eq!(complaints.len(), 1);
assert_eq!(complaints[0].code, Some("W0301"));
let previous = complaints[0].children.first().expect("a note saying where it was");
assert_eq!(run.sources.lookup(previous.span.lo).map(|loc| loc.file), {
let built_in = run.sources.files().iter().find(|f| f.name == BUILT_IN);
built_in.map(|f| f.id)
});
}
#[test]
fn destringizing_undoes_what_stringizing_did() {
assert_eq!(destringize(r#""a \"b\" c""#), r#"a "b" c"#);
assert_eq!(destringize(r#""a \\ b""#), r"a \ b");
assert_eq!(destringize(r#"L"wide""#), "wide");
}
}