pub mod builtin;
mod calls;
mod fragment;
mod matcher;
mod procs;
mod rules;
mod transcribe;
mod tree;
use std::collections::HashMap;
use yog_ids::script_problem;
use crate::source::token::{Punct, Tok, Token};
use crate::words::r#macro;
use rules::Macro;
use tree::{Tree, closer, tree_at};
use yog_analysis::Report;
pub use procs::ProcMacros;
pub(crate) use transcribe::HYGIENE_MARK;
const MAX_EXPANSIONS: usize = 4096;
pub(crate) fn expand(
mut tokens: Vec<Token>,
text: &str,
procs: Option<&dyn ProcMacros>,
report: &mut Report<'_>,
) -> Vec<Token> {
let eof = tokens.pop();
let macros = take_macros(&mut tokens, report);
if !macros.is_empty() {
tokens = expand_calls(tokens, ¯os, report);
}
if let Some(procs) = procs {
tokens = calls::expand(tokens, text, procs, report);
}
tokens.extend(eof);
tokens
}
fn take_macros(tokens: &mut Vec<Token>, report: &mut Report<'_>) -> HashMap<String, Macro> {
let mut macros = HashMap::new();
let mut i = 0;
while i < tokens.len() {
let is_decl = matches!(&tokens[i].tok, Tok::Keyword(w) if **w == r#macro::WORD)
&& matches!(tokens.get(i + 1).map(|t| &t.tok), Some(Tok::Ident(_)))
&& tokens
.get(i + 2)
.is_some_and(|t| t.tok == Tok::Punct(Punct::LBrace));
if !is_decl {
i += 1;
continue;
}
let Tok::Ident(name) = tokens[i + 1].tok.clone() else {
continue;
};
let (body, end) = tree_at(tokens, i + 2);
if let Tree::Group { inner, .. } = &body {
macros.insert(name, rules::read(inner, tokens[i].span, report));
}
tokens.drain(i..end);
}
macros
}
fn expand_calls(
mut tokens: Vec<Token>,
macros: &HashMap<String, Macro>,
report: &mut Report<'_>,
) -> Vec<Token> {
let mut made = 0;
let mut i = 0;
while i < tokens.len() {
let Some(m) = call_at(&tokens, i, macros) else {
i += 1;
continue;
};
let at = tokens[i].span;
let (args, end) = tree_at(&tokens, i + 2);
let Tree::Group { inner, .. } = args else {
break;
};
made += 1;
if made > MAX_EXPANSIONS {
report.error(
script_problem::MACRO,
at,
"macros expand without end here (a macro calling itself?)",
);
break;
}
let Some(written) = expansion(m, &inner, at, made, report) else {
i = end;
continue;
};
tokens.splice(i..end, written);
}
tokens
}
fn call_at<'m>(
tokens: &[Token],
i: usize,
macros: &'m HashMap<String, Macro>,
) -> Option<&'m Macro> {
let Tok::Ident(name) = &tokens[i].tok else {
return None;
};
let bang = tokens.get(i + 1)?;
let open = tokens.get(i + 2)?;
let joined = bang.tok == Tok::Punct(Punct::Bang)
&& bang.span.start == tokens[i].span.end
&& open.span.start == bang.span.end
&& closer(&open.tok).is_some();
if joined { macros.get(name) } else { None }
}
fn expansion(
m: &Macro,
args: &[Tree],
at: crate::source::token::Span,
n: usize,
report: &mut Report<'_>,
) -> Option<Vec<Token>> {
for rule in &m.rules {
if let Some(b) = matcher::matched(&rule.matcher, args, report) {
let mut w = transcribe::Writer::new(at, report);
w.write(&rule.body, &b);
return Some(w.finish(n));
}
}
report.error(
script_problem::MACRO,
at,
"no rule of this macro matches these arguments",
);
None
}