Skip to main content

yog_script_build/
macros.rs

1//! The procedural macros a build has: Yog Script sources whose methods are
2//! marked `[ProcMacro]` (`Name!(…)`) or `[DeriveMacro]` (`[Derive(Name)]`),
3//! and attribute classes marked `[Macro]` (`[Name(…)]`, their `Expand`),
4//! checked first, then run by the interpreter when a source calls them.
5
6use 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
15/// A build's macros, loaded from their sources.
16pub struct MacroLibrary {
17    files: Vec<File>,
18    calls: HashSet<String>,
19    derives: HashSet<String>,
20    /// Attribute classes marked `[Macro]`: their `Expand` runs.
21    attributes: HashSet<String>,
22}
23
24impl MacroLibrary {
25    /// The macros of `sources` (name, text), or what's wrong with them.
26    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    /// `f` run with an interpreter over the library and the System's.
60    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
96/// What an attribute macro's class runs: `static string Expand(string
97/// args, Item item)`.
98const EXPAND: &str = "Expand";
99
100/// Whether `d` carries attribute `name`.
101fn 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
107/// A macro's result as code.
108fn 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
116/// The `System.Meta.Item` a derive macro is given for the declaration
117/// written `text`.
118fn 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
139/// A type's name, its word, its fields (name, type as written).
140type Shape = (String, String, Vec<(String, String)>);
141
142/// A type's name, word and fields (name, type as written).
143fn 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}