1use std::collections::HashSet;
7
8use yog_data::Diagnostic;
9use yog_script_analyze::ast::{Decl, DeclKind, File, TypeKind};
10use yog_script_analyze::macros::ProcMacros;
11use yog_script_analyze::system;
12use yog_script_ids::system::{DERIVE_MACRO, FIELD, ITEM, MACRO, PROC_MACRO};
13use yog_script_run::{Interp, Panic, Program, Value};
14
15pub struct MacroLibrary {
17 files: Vec<File>,
18 calls: HashSet<String>,
19 derives: HashSet<String>,
20 attributes: HashSet<String>,
22}
23
24impl MacroLibrary {
25 pub fn load(sources: &[(String, String)]) -> Result<Self, Vec<Diagnostic>> {
27 let mut lib = MacroLibrary {
28 files: Vec::new(),
29 calls: HashSet::new(),
30 derives: HashSet::new(),
31 attributes: HashSet::new(),
32 };
33 let mut problems = Vec::new();
34 for (name, text) in sources {
35 let checked = yog_script_analyze::check(name, text);
36 problems.extend(checked.problems.into_iter().filter(Diagnostic::is_error));
37 lib.files.push(checked.file);
38 }
39 if !problems.is_empty() {
40 return Err(problems);
41 }
42 for d in lib.files.iter().flat_map(|f| &f.decls) {
43 match &d.kind {
44 DeclKind::Method(m) if marked(d, PROC_MACRO) => {
45 lib.calls.insert(m.name.text.clone());
46 }
47 DeclKind::Method(m) if marked(d, DERIVE_MACRO) => {
48 lib.derives.insert(m.name.text.clone());
49 }
50 DeclKind::Type { name, .. } if marked(d, MACRO) => {
51 lib.attributes.insert(name.text.clone());
52 }
53 _ => {}
54 }
55 }
56 Ok(lib)
57 }
58
59 fn run<R>(&self, f: impl FnOnce(&mut Interp<'_>) -> R) -> R {
61 let mut sources: Vec<&[Decl]> = vec![&system::DECLS];
62 sources.extend(self.files.iter().map(|f| f.decls.as_slice()));
63 let program = Program::of(&sources);
64 f(&mut Interp::new(&program))
65 }
66}
67
68impl ProcMacros for MacroLibrary {
69 fn call(&self, name: &str, input: &str) -> Option<Result<String, String>> {
70 self.calls
71 .contains(name)
72 .then(|| self.run(|i| code(i.call(name, vec![Value::str(input)]))))
73 }
74
75 fn derive(&self, name: &str, item: &str) -> Option<Result<String, String>> {
76 if !self.derives.contains(name) {
77 return None;
78 }
79 Some(self.run(|i| {
80 let item = item_value(i, item)?;
81 code(i.call(name, vec![item]))
82 }))
83 }
84
85 fn attribute(&self, name: &str, args: &str, item: &str) -> Option<Result<String, String>> {
86 if !self.attributes.contains(name) {
87 return None;
88 }
89 Some(self.run(|i| {
90 let item = item_value(i, item)?;
91 code(i.call_static(name, EXPAND, vec![Value::str(args), item]))
92 }))
93 }
94}
95
96const EXPAND: &str = "Expand";
99
100fn marked(d: &Decl, name: &str) -> bool {
102 d.attributes
103 .iter()
104 .any(|a| a.path.last().is_some_and(|n| n.text == name))
105}
106
107fn code(out: Result<Value, Panic>) -> Result<String, String> {
109 match out {
110 Ok(Value::Str(s)) => Ok(s.to_string()),
111 Ok(other) => Err(format!("a macro gives its code as a string, not `{other}`")),
112 Err(p) => Err(p.message),
113 }
114}
115
116fn item_value(i: &mut Interp<'_>, text: &str) -> Result<Value, String> {
119 let parsed = yog_script_analyze::parse("Item.ys", text);
120 let Some((name, kind, fields)) = parsed.file.decls.iter().find_map(|d| shape(d, text)) else {
121 return Err("a derive macro is for a type".to_string());
122 };
123 let mut made = Vec::new();
124 for (n, t) in fields {
125 made.push(
126 i.make_object(FIELD, vec![Value::str(&n), Value::str(&t)])
127 .map_err(|p| p.message)?,
128 );
129 }
130 let args = vec![
131 Value::str(&name),
132 Value::str(&kind),
133 Value::list(made),
134 Value::str(text),
135 ];
136 i.make_object(ITEM, args).map_err(|p| p.message)
137}
138
139type Shape = (String, String, Vec<(String, String)>);
141
142fn shape(d: &Decl, text: &str) -> Option<Shape> {
144 let written = |t: &yog_script_analyze::ast::Type| {
145 text.get(t.span.start as usize..t.span.end as usize)
146 .unwrap_or_default()
147 .to_string()
148 };
149 match &d.kind {
150 DeclKind::Type {
151 word,
152 name,
153 header,
154 body,
155 ..
156 } => {
157 let mut fields: Vec<(String, String)> = header
158 .ctor
159 .iter()
160 .flatten()
161 .map(|p| (p.name.text.clone(), written(&p.ty)))
162 .collect();
163 for m in body {
164 match &m.kind {
165 DeclKind::Field(f) => fields.push((f.name.text.clone(), written(&f.ty))),
166 DeclKind::Property { ty, name, .. }
167 if !matches!(ty.kind, TypeKind::Lifetime(_)) =>
168 {
169 fields.push((name.text.clone(), written(ty)));
170 }
171 _ => {}
172 }
173 }
174 Some((name.text.clone(), word.text.clone(), fields))
175 }
176 DeclKind::Enum { name, .. } => Some((
177 name.text.clone(),
178 yog_script_analyze::words::r#enum::WORD.text.to_string(),
179 Vec::new(),
180 )),
181 _ => None,
182 }
183}