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