august_build/
lib.rs

1/// When using August as a library,
2/// disable default features so Cargo doesn't pull CLI dependencies
3///
4/// ```toml
5/// [dependencies]
6/// august-build = { version = "*", default-features = false }
7/// ```
8use indexmap::{IndexMap, IndexSet};
9use rustc_hash::FxBuildHasher;
10use thiserror::Error;
11
12use parser::{Spanned, AST};
13
14pub mod lexer;
15pub mod parser;
16pub mod runtime;
17
18pub(crate) type HashMap<K, V> = IndexMap<K, V, FxBuildHasher>;
19pub(crate) type HashSet<K> = IndexSet<K, FxBuildHasher>;
20
21#[derive(Debug, Clone)]
22pub struct Module {
23    expose: HashMap<Pragma, Spanned<String>>,
24    units: HashMap<Spanned<String>, Unit>,
25}
26
27impl Module {
28    pub fn lower(ast: Vec<AST>) -> Result<Self, Vec<LowerError>> {
29        let mut errors = Vec::new();
30
31        let mut unit_iter = ast.iter().filter(|a| matches!(a, AST::Unit(_, _)));
32        let mut units: HashMap<Spanned<String>, Unit> =
33            HashMap::with_capacity_and_hasher(unit_iter.size_hint().0, FxBuildHasher);
34
35        while let Some(AST::Unit(name, cmds)) = unit_iter.next() {
36            let res = Unit::lower(cmds);
37            let unit = match res {
38                Ok(u) => u,
39                Err(e) => {
40                    errors.extend(e);
41                    // Still need the unit to exist for name checks
42                    // Lower will still error
43                    Unit::default()
44                }
45            };
46
47            if let Some((other, _)) = units.get_key_value(name) {
48                errors.push(LowerError::DuplicateUnit(other.clone(), name.clone()));
49            } else {
50                units.insert(name.clone(), unit);
51            }
52        }
53
54        let mut expose_iter = ast.into_iter().filter(|a| matches!(a, AST::Expose(_, _)));
55        let mut expose =
56            HashMap::with_capacity_and_hasher(expose_iter.size_hint().0, FxBuildHasher);
57
58        while let Some(AST::Expose(prag, unit)) = expose_iter.next() {
59            let mut err = false;
60            if expose.contains_key(&prag) {
61                err = true;
62                errors.push(LowerError::DuplicateExpose(prag, unit.clone()));
63            }
64            if !units.contains_key(&unit) {
65                err = true;
66                errors.push(LowerError::NameError(unit.clone()));
67            }
68            if err {
69                continue;
70            }
71
72            expose.insert(prag, unit.clone());
73        }
74
75        for unit in units.values() {
76            for u in &unit.depends_on {
77                if !units.contains_key(u) {
78                    errors.push(LowerError::NameError(u.clone()));
79                }
80            }
81
82            let mut dos_iter = unit.commands.iter().filter(|c| matches!(c, Command::Do(_)));
83            while let Some(Command::Do(dos)) = dos_iter.next() {
84                for d in dos {
85                    if !units.contains_key(d) {
86                        errors.push(LowerError::NameError(d.clone()));
87                    }
88                }
89            }
90        }
91
92        if !errors.is_empty() {
93            return Err(errors);
94        }
95        Ok(Self { expose, units })
96    }
97
98    pub fn units(&self) -> &HashMap<Spanned<String>, Unit> {
99        &self.units
100    }
101
102    pub fn unit_exists(&self, name: impl Into<String>) -> bool {
103        self.units.contains_key(&Spanned::new(name.into()))
104    }
105
106    pub fn unit_by_pragma(&self, pragma: Pragma) -> Option<String> {
107        self.expose.get(&pragma).map(Spanned::inner_owned)
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Error)]
112pub enum LowerError {
113    #[error("Attempted to define another binding for pragma {0:?}")]
114    DuplicateExpose(Pragma, Spanned<String>),
115    #[error("Attempted to define multiple units with the name {1}")]
116    DuplicateUnit(Spanned<String>, Spanned<String>),
117    #[error("Dependency {1} defined multiple times in the same unit")]
118    DuplicateDependency(Spanned<String>, Spanned<String>),
119    #[error("Meta item {1} defined multiple times in the same unit")]
120    DuplicateMetaItem(Spanned<String>, Spanned<String>),
121    #[error("Refers to a unit {0} that doesn't exist")]
122    NameError(Spanned<String>),
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Default)]
126pub struct Unit {
127    depends_on: HashSet<Spanned<String>>,
128    pub meta: HashMap<Spanned<String>, String>,
129    commands: Vec<Command>,
130}
131
132impl Unit {
133    pub fn lower(cmds: &[Command]) -> Result<Self, Vec<LowerError>> {
134        let mut errors = Vec::new();
135
136        let depends_iter = cmds
137            .iter()
138            .filter_map(|c| {
139                if let Command::DependsOn(deps) = c {
140                    Some(deps)
141                } else {
142                    None
143                }
144            })
145            .flatten();
146        let mut depends_on: HashSet<Spanned<String>> =
147            HashSet::with_capacity_and_hasher(depends_iter.size_hint().0, FxBuildHasher);
148
149        for dep in depends_iter {
150            if let Some(other) = depends_on.get(dep) {
151                errors.push(LowerError::DuplicateDependency(other.clone(), dep.clone()));
152            } else {
153                depends_on.insert(dep.clone());
154            }
155        }
156
157        let meta_iter = cmds.iter().filter_map(|c| {
158            if let Command::Meta(meta) = c {
159                Some(meta)
160            } else {
161                None
162            }
163        });
164        let mut meta: HashMap<Spanned<String>, String> =
165            HashMap::with_capacity_and_hasher(meta_iter.size_hint().0, FxBuildHasher);
166
167        for meta_items in meta_iter {
168            for (var, val) in meta_items {
169                if let Some((other, _)) = meta.get_key_value(var) {
170                    errors.push(LowerError::DuplicateMetaItem(other.clone(), var.clone()));
171                } else {
172                    meta.insert(var.clone(), val.clone());
173                }
174            }
175        }
176
177        if !errors.is_empty() {
178            return Err(errors);
179        }
180        Ok(Self {
181            depends_on,
182            meta,
183            commands: cmds
184                .iter()
185                .filter(|c| !matches!(c, Command::Meta(_) | Command::DependsOn(_)))
186                .cloned()
187                .collect(),
188        })
189    }
190
191    pub fn deps(&self) -> &HashSet<Spanned<String>> {
192        &self.depends_on
193    }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
197pub enum Pragma {
198    Test,
199    Build,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum Command {
204    DependsOn(Vec<Spanned<String>>),
205    Meta(Vec<(Spanned<String>, String)>),
206    Do(Vec<Spanned<String>>),
207    Exec(Vec<Spanned<String>>),
208    Concurrent(Vec<Box<Command>>),
209
210    Fs(FsCommand),
211    Io(IoCommand),
212    Env(EnvCommand),
213}
214
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub enum FsCommand {
217    Create(Spanned<String>),
218    CreateDir(Spanned<String>),
219    Remove(Spanned<String>),
220    Move(Spanned<String>, Spanned<String>),
221    MoveTo(
222        Spanned<String>,
223        Vec<(Spanned<String>, Option<Spanned<String>>)>,
224    ),
225    Copy(Spanned<String>, Spanned<String>),
226    CopyTo(
227        Spanned<String>,
228        Vec<(Spanned<String>, Option<Spanned<String>>)>,
229    ),
230    PrintFile(Spanned<String>),
231    EPrintFile(Spanned<String>),
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum IoCommand {
236    PrintLn(Spanned<String>),
237    Print(Spanned<String>),
238    EPrintLn(Spanned<String>),
239    EPrint(Spanned<String>),
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum EnvCommand {
244    SetVar(Spanned<String>, Spanned<String>),
245    RemoveVar(Spanned<String>),
246    PathPush(Spanned<String>),
247    PathRemove(Spanned<String>),
248}