use std::collections::HashMap;
use rucc_base::{Interner, Symbol};
use rucc_diag::{Diagnostic, Span};
use rucc_lex::{PpToken, PpTokenKind, Punct, TokenFlags};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Builtin {
File,
FileName,
BaseFile,
Line,
IncludeLevel,
Counter,
}
impl Builtin {
pub const ALL: [(&'static str, Builtin); 6] = [
("__FILE__", Builtin::File),
("__FILE_NAME__", Builtin::FileName),
("__BASE_FILE__", Builtin::BaseFile),
("__LINE__", Builtin::Line),
("__INCLUDE_LEVEL__", Builtin::IncludeLevel),
("__COUNTER__", Builtin::Counter),
];
}
#[derive(Debug, Clone)]
pub struct MacroDef {
pub name: Symbol,
pub function_like: bool,
pub params: Vec<Symbol>,
pub variadic: Option<Symbol>,
pub body: Vec<PpToken>,
pub span: Span,
pub builtin: Option<Builtin>,
}
impl MacroDef {
#[inline]
pub fn is_variadic(&self) -> bool {
self.variadic.is_some()
}
#[inline]
pub fn arity(&self) -> usize {
self.params.len()
}
pub fn param_index(&self, name: Symbol) -> Option<usize> {
if let Some(at) = self.params.iter().position(|&p| p == name) {
return Some(at);
}
if self.variadic == Some(name) { Some(self.params.len()) } else { None }
}
#[inline]
pub fn is_variadic_param(&self, name: Symbol) -> bool {
self.variadic == Some(name)
}
pub fn same_definition_as(&self, other: &MacroDef) -> bool {
if self.function_like != other.function_like
|| self.params != other.params
|| self.variadic != other.variadic
|| self.builtin != other.builtin
|| self.body.len() != other.body.len()
{
return false;
}
self.body.iter().zip(&other.body).enumerate().all(|(at, (a, b))| {
a.kind == b.kind
&& a.value == b.value
&& (at == 0
|| a.flags.has(TokenFlags::LEADING_SPACE)
== b.flags.has(TokenFlags::LEADING_SPACE))
})
}
}
#[derive(Debug, Default)]
pub struct MacroTable {
by_name: HashMap<Symbol, MacroDef>,
}
impl MacroTable {
pub fn new() -> MacroTable {
MacroTable::default()
}
#[inline]
pub fn lookup(&self, name: Symbol) -> Option<&MacroDef> {
self.by_name.get(&name)
}
#[inline]
pub fn is_defined(&self, name: Symbol) -> bool {
self.by_name.contains_key(&name)
}
pub fn len(&self) -> usize {
self.by_name.len()
}
pub fn is_empty(&self) -> bool {
self.by_name.is_empty()
}
pub fn define(&mut self, def: MacroDef, interner: &Interner) -> Option<Diagnostic> {
let complaint =
self.by_name.get(&def.name).filter(|old| !old.same_definition_as(&def)).map(|old| {
Diagnostic::warning(format!("`{}` redefined", interner.resolve(def.name)), def.span)
.with_code("W0301")
.note("previous definition was here", old.span)
});
self.by_name.insert(def.name, def);
complaint
}
pub fn define_builtin(&mut self, name: Symbol, builtin: Builtin, span: Span) {
let def = MacroDef {
name,
function_like: false,
params: Vec::new(),
variadic: None,
body: Vec::new(),
span,
builtin: Some(builtin),
};
self.by_name.insert(name, def);
}
pub fn undef(&mut self, name: Symbol) -> Option<MacroDef> {
self.by_name.remove(&name)
}
pub fn sorted(&self) -> Vec<&MacroDef> {
let mut all: Vec<&MacroDef> = self.by_name.values().collect();
all.sort_by_key(|m| m.name);
all
}
}
pub fn parse_define(
tokens: &[PpToken],
interner: &mut Interner,
) -> (Option<MacroDef>, Vec<Diagnostic>) {
let mut diagnostics = Vec::new();
let Some(&first) = tokens.first() else {
return (None, vec![Diagnostic::error("no macro name given in `#define`", Span::DUMMY)]);
};
if first.kind != PpTokenKind::Ident {
diagnostics.push(
Diagnostic::error("macro name must be an identifier", first.span).with_code("E0300"),
);
return (None, diagnostics);
}
let name = first.value.expect("the lexer interns every identifier");
let span = first.span;
let rest = &tokens[1..];
let opens_params = rest.first().is_some_and(|t| {
t.punct() == Some(Punct::LParen) && !t.flags.has(TokenFlags::LEADING_SPACE)
});
let (function_like, params, variadic, body) = if opens_params {
match parse_params(&rest[1..], interner, &mut diagnostics) {
Some((params, variadic, consumed)) => (true, params, variadic, &rest[1 + consumed..]),
None => return (None, diagnostics),
}
} else {
(false, Vec::new(), None, rest)
};
let def = MacroDef {
name,
function_like,
params,
variadic,
body: body.to_vec(),
span,
builtin: None,
};
check_body(&def, interner, &mut diagnostics);
(Some(def), diagnostics)
}
fn parse_params(
tokens: &[PpToken],
interner: &mut Interner,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(Vec<Symbol>, Option<Symbol>, usize)> {
let va_args = interner.intern("__VA_ARGS__");
let mut params: Vec<Symbol> = Vec::new();
let mut variadic = None;
let mut at = 0;
if tokens.first().is_some_and(|t| t.punct() == Some(Punct::RParen)) {
return Some((params, None, 1));
}
loop {
let Some(&tok) = tokens.get(at) else {
diagnostics.push(
Diagnostic::error("missing `)` in macro parameter list", last_span(tokens))
.with_code("E0301"),
);
return None;
};
at += 1;
if tok.punct() == Some(Punct::Ellipsis) {
variadic = Some(va_args);
} else if tok.kind == PpTokenKind::Ident {
let sym = tok.value.expect("the lexer interns every identifier");
if tokens.get(at).is_some_and(|t| t.punct() == Some(Punct::Ellipsis)) {
at += 1;
variadic = Some(sym);
} else if sym == va_args {
diagnostics.push(
Diagnostic::error("`__VA_ARGS__` cannot be used as a parameter name", tok.span)
.with_code("E0302"),
);
return None;
} else if params.contains(&sym) {
diagnostics.push(
Diagnostic::error(
format!("duplicate macro parameter `{}`", interner.resolve(sym)),
tok.span,
)
.with_code("E0303"),
);
return None;
} else {
params.push(sym);
}
} else {
diagnostics.push(
Diagnostic::error("macro parameter must be an identifier", tok.span)
.with_code("E0301"),
);
return None;
}
match tokens.get(at).and_then(|t| t.punct()) {
Some(Punct::RParen) => return Some((params, variadic, at + 1)),
Some(Punct::Comma) if variadic.is_none() => at += 1,
Some(Punct::Comma) => {
diagnostics.push(
Diagnostic::error("`...` must be the last macro parameter", tokens[at].span)
.with_code("E0301"),
);
return None;
}
_ => {
diagnostics.push(
Diagnostic::error("missing `)` in macro parameter list", last_span(tokens))
.with_code("E0301"),
);
return None;
}
}
}
}
fn check_body(def: &MacroDef, interner: &mut Interner, diagnostics: &mut Vec<Diagnostic>) {
let va_opt = interner.intern("__VA_OPT__");
let va_args = interner.intern("__VA_ARGS__");
if let Some(first) = def.body.first().filter(|t| t.punct() == Some(Punct::HashHash)) {
diagnostics.push(
Diagnostic::error("`##` cannot appear at the start of a replacement list", first.span)
.with_code("E0304"),
);
}
let trailing =
def.body.last().filter(|t| def.body.len() > 1 && t.punct() == Some(Punct::HashHash));
if let Some(last) = trailing {
diagnostics.push(
Diagnostic::error("`##` cannot appear at the end of a replacement list", last.span)
.with_code("E0304"),
);
}
for (at, tok) in def.body.iter().enumerate() {
if def.function_like && tok.punct() == Some(Punct::Hash) {
let operand = def.body.get(at + 1);
let names_param = operand.is_some_and(|t| {
t.value.is_some_and(|v| def.param_index(v).is_some())
|| (def.is_variadic() && t.value == Some(va_opt))
});
if !names_param {
diagnostics.push(
Diagnostic::error("`#` must be followed by a macro parameter", tok.span)
.with_code("E0305"),
);
}
}
if tok.kind != PpTokenKind::Ident {
continue;
}
if tok.value == Some(va_args) && !def.is_variadic() {
diagnostics.push(
Diagnostic::error("`__VA_ARGS__` can only appear in a variadic macro", tok.span)
.with_code("E0306"),
);
}
if tok.value == Some(va_opt) {
if !def.is_variadic() {
diagnostics.push(
Diagnostic::error("`__VA_OPT__` can only appear in a variadic macro", tok.span)
.with_code("E0306"),
);
} else if !def.body.get(at + 1).is_some_and(|t| t.punct() == Some(Punct::LParen)) {
diagnostics.push(
Diagnostic::error("`__VA_OPT__` must be followed by `(`", tok.span)
.with_code("E0307"),
);
}
}
}
}
fn last_span(tokens: &[PpToken]) -> Span {
tokens.last().map_or(Span::DUMMY, |t| t.span)
}
#[cfg(test)]
mod tests {
use rucc_diag::Severity;
use rucc_lex::{Options, tokenize};
use super::*;
fn define(src: &str, interner: &mut Interner) -> (Option<MacroDef>, Vec<Diagnostic>) {
let (tokens, lex_errors) = tokenize(src.as_bytes(), 0, Options::new(), interner);
assert!(lex_errors.is_empty(), "the test input should lex cleanly");
let body: Vec<PpToken> =
tokens.into_iter().filter(|t| t.kind != PpTokenKind::Eof).collect();
parse_define(&body, interner)
}
#[test]
fn an_object_like_macro_has_no_parameter_list() {
let mut i = Interner::new();
let (def, errors) = define("PI 3.14", &mut i);
let def = def.expect("should parse");
assert!(errors.is_empty());
assert!(!def.function_like);
assert_eq!(def.body.len(), 1);
}
#[test]
fn a_space_before_the_parenthesis_makes_it_object_like() {
let mut i = Interner::new();
let (def, _) = define("A (x)", &mut i);
let def = def.expect("should parse");
assert!(!def.function_like, "`#define A (x)` defines A as the token sequence `(x)`");
assert_eq!(def.body.len(), 3);
}
#[test]
fn a_function_like_macro_with_no_parameters_is_not_object_like() {
let mut i = Interner::new();
let (def, _) = define("A() 1", &mut i);
let def = def.expect("should parse");
assert!(def.function_like);
assert_eq!(def.arity(), 0);
}
#[test]
fn the_standard_ellipsis_names_the_variadic_va_args() {
let mut i = Interner::new();
let (def, errors) = define("F(a, ...) a", &mut i);
let def = def.expect("should parse");
assert!(errors.is_empty());
assert_eq!(def.arity(), 1);
assert_eq!(def.variadic, Some(i.intern("__VA_ARGS__")));
}
#[test]
fn the_gnu_form_names_the_variadic_itself() {
let mut i = Interner::new();
let (def, errors) = define("F(a, rest...) a", &mut i);
let def = def.expect("should parse");
assert!(errors.is_empty());
assert_eq!(def.variadic, Some(i.intern("rest")));
assert_eq!(def.param_index(i.intern("rest")), Some(1));
}
#[test]
fn a_duplicate_parameter_is_rejected() {
let mut i = Interner::new();
let (def, errors) = define("F(a, a) a", &mut i);
assert!(def.is_none());
assert_eq!(errors[0].code, Some("E0303"));
}
#[test]
fn paste_cannot_start_or_end_a_replacement_list() {
let mut i = Interner::new();
let (_, start) = define("A ## b", &mut i);
assert_eq!(start[0].code, Some("E0304"));
let (_, end) = define("A b ##", &mut i);
assert_eq!(end[0].code, Some("E0304"));
}
#[test]
fn stringify_must_name_a_parameter_but_only_in_a_function_like_macro() {
let mut i = Interner::new();
let (_, bad) = define("F(a) # b", &mut i);
assert_eq!(bad[0].code, Some("E0305"));
let (_, fine) = define("HASH #", &mut i);
assert!(fine.is_empty(), "a bare `#` in an object-like macro is just a token");
}
#[test]
fn va_args_outside_a_variadic_macro_is_rejected() {
let mut i = Interner::new();
let (_, errors) = define("F(a) __VA_ARGS__", &mut i);
assert_eq!(errors[0].code, Some("E0306"));
}
#[test]
fn va_opt_must_be_called() {
let mut i = Interner::new();
let (_, errors) = define("F(...) __VA_OPT__", &mut i);
assert_eq!(errors[0].code, Some("E0307"));
}
#[test]
fn redefining_a_macro_to_the_same_thing_is_silent() {
let mut i = Interner::new();
let mut table = MacroTable::new();
let (first, _) = define("A 1 + 2", &mut i);
let (again, _) = define("A 1 + 2", &mut i);
assert!(table.define(first.expect("should parse"), &i).is_none());
assert!(table.define(again.expect("should parse"), &i).is_none());
assert_eq!(table.len(), 1);
}
#[test]
fn redefining_a_macro_differently_warns_and_takes_the_new_one() {
let mut i = Interner::new();
let mut table = MacroTable::new();
let (first, _) = define("A 1", &mut i);
let (again, _) = define("A 2", &mut i);
table.define(first.expect("should parse"), &i);
let warning = table.define(again.expect("should parse"), &i).expect("should warn");
assert_eq!(warning.severity, Severity::Warning);
assert_eq!(warning.code, Some("W0301"));
assert_eq!(table.lookup(i.intern("A")).expect("still defined").body.len(), 1);
}
#[test]
fn whitespace_inside_the_replacement_list_is_part_of_the_definition() {
let mut i = Interner::new();
let (a, _) = define("A x+y", &mut i);
let (b, _) = define("A x + y", &mut i);
assert!(
!a.expect("should parse").same_definition_as(&b.expect("should parse")),
"the standard compares spelling including whitespace separation"
);
}
#[test]
fn a_builtin_macro_is_defined_like_any_other() {
let mut i = Interner::new();
let mut table = MacroTable::new();
let name = i.intern("__LINE__");
table.define_builtin(name, Builtin::Line, Span::new(0, 0));
assert!(table.is_defined(name), "`#ifdef __LINE__` is true");
assert_eq!(table.lookup(name).and_then(|d| d.builtin), Some(Builtin::Line));
assert!(table.undef(name).is_some(), "`#undef __LINE__` is allowed, as it is in GCC");
}
#[test]
fn redefining_a_builtin_is_a_redefinition() {
let mut i = Interner::new();
let mut table = MacroTable::new();
let name = i.intern("__FILE__");
table.define_builtin(name, Builtin::File, Span::new(0, 0));
let (def, _) = define("__FILE__", &mut i);
let warning = table.define(def.expect("should parse"), &i).expect("should warn");
assert_eq!(warning.code, Some("W0301"));
assert!(table.lookup(name).expect("still defined").builtin.is_none());
}
#[test]
fn undefining_something_that_was_never_defined_is_fine() {
let mut i = Interner::new();
let mut table = MacroTable::new();
assert!(table.undef(i.intern("nothing")).is_none());
}
}