use std::collections::HashSet;
use yog_data::Diagnostic;
use yog_script_analyze::ast::{Decl, DeclKind, File, TypeKind};
use yog_script_analyze::macros::ProcMacros;
use yog_script_analyze::system::{self, DERIVE_MACRO, FIELD, ITEM, MACRO, PROC_MACRO};
use yog_script_run::{Interp, Panic, Program, Value};
pub struct MacroLibrary {
files: Vec<File>,
calls: HashSet<String>,
derives: HashSet<String>,
attributes: HashSet<String>,
}
impl MacroLibrary {
pub fn load(sources: &[(String, String)]) -> Result<Self, Vec<Diagnostic>> {
let mut lib = MacroLibrary {
files: Vec::new(),
calls: HashSet::new(),
derives: HashSet::new(),
attributes: HashSet::new(),
};
let mut problems = Vec::new();
for (name, text) in sources {
let checked = yog_script_analyze::check(name, text);
problems.extend(checked.problems.into_iter().filter(Diagnostic::is_error));
lib.files.push(checked.file);
}
if !problems.is_empty() {
return Err(problems);
}
for d in lib.files.iter().flat_map(|f| &f.decls) {
match &d.kind {
DeclKind::Method(m) if marked(d, PROC_MACRO) => {
lib.calls.insert(m.name.text.clone());
}
DeclKind::Method(m) if marked(d, DERIVE_MACRO) => {
lib.derives.insert(m.name.text.clone());
}
DeclKind::Type { name, .. } if marked(d, MACRO) => {
lib.attributes.insert(name.text.clone());
}
_ => {}
}
}
Ok(lib)
}
fn run<R>(&self, f: impl FnOnce(&mut Interp<'_>) -> R) -> R {
let mut sources: Vec<&[Decl]> = vec![&system::DECLS];
sources.extend(self.files.iter().map(|f| f.decls.as_slice()));
let program = Program::of(&sources);
f(&mut Interp::new(&program))
}
}
impl ProcMacros for MacroLibrary {
fn call(&self, name: &str, input: &str) -> Option<Result<String, String>> {
self.calls
.contains(name)
.then(|| self.run(|i| code(i.call(name, vec![Value::str(input)]))))
}
fn derive(&self, name: &str, item: &str) -> Option<Result<String, String>> {
if !self.derives.contains(name) {
return None;
}
Some(self.run(|i| {
let item = item_value(i, item)?;
code(i.call(name, vec![item]))
}))
}
fn attribute(&self, name: &str, args: &str, item: &str) -> Option<Result<String, String>> {
if !self.attributes.contains(name) {
return None;
}
Some(self.run(|i| {
let item = item_value(i, item)?;
code(i.call_static(name, EXPAND, vec![Value::str(args), item]))
}))
}
}
const EXPAND: &str = "Expand";
fn marked(d: &Decl, name: &str) -> bool {
d.attributes
.iter()
.any(|a| a.path.last().is_some_and(|n| n.text == name))
}
fn code(out: Result<Value, Panic>) -> Result<String, String> {
match out {
Ok(Value::Str(s)) => Ok(s.to_string()),
Ok(other) => Err(format!("a macro gives its code as a string, not `{other}`")),
Err(p) => Err(p.message),
}
}
fn item_value(i: &mut Interp<'_>, text: &str) -> Result<Value, String> {
let parsed = yog_script_analyze::parse("item.ys", text);
let Some((name, kind, fields)) = parsed.file.decls.iter().find_map(|d| shape(d, text)) else {
return Err("a derive macro is for a type".to_string());
};
let mut made = Vec::new();
for (n, t) in fields {
made.push(
i.make_object(FIELD, vec![Value::str(&n), Value::str(&t)])
.map_err(|p| p.message)?,
);
}
let args = vec![
Value::str(&name),
Value::str(&kind),
Value::list(made),
Value::str(text),
];
i.make_object(ITEM, args).map_err(|p| p.message)
}
type Shape = (String, String, Vec<(String, String)>);
fn shape(d: &Decl, text: &str) -> Option<Shape> {
let written = |t: &yog_script_analyze::ast::Type| {
text.get(t.span.start as usize..t.span.end as usize)
.unwrap_or_default()
.to_string()
};
match &d.kind {
DeclKind::Type {
word,
name,
header,
body,
..
} => {
let mut fields: Vec<(String, String)> = header
.ctor
.iter()
.flatten()
.map(|p| (p.name.text.clone(), written(&p.ty)))
.collect();
for m in body {
match &m.kind {
DeclKind::Field(f) => fields.push((f.name.text.clone(), written(&f.ty))),
DeclKind::Property { ty, name, .. }
if !matches!(ty.kind, TypeKind::Lifetime(_)) =>
{
fields.push((name.text.clone(), written(ty)));
}
_ => {}
}
}
Some((name.text.clone(), word.text.clone(), fields))
}
DeclKind::Enum { name, .. } => Some((
name.text.clone(),
yog_script_analyze::words::r#enum::WORD.text.to_string(),
Vec::new(),
)),
_ => None,
}
}