chronlang_engine/
compiler.rs

1use crate::resolver::Resolve;
2use crate::project::*;
3use chronlang_parser::{ast, parse};
4
5#[derive(Debug, PartialEq, Clone)]
6pub enum CompilationError {
7    ParseErrors(Vec<(ast::Span, String)>),
8    NoLanguage(ast::Span),
9    BadImport(ast::Span, String),
10    ImportedNameNotFound(ast::Span, String),
11    ImportDependencyNotFound(String),
12    NameCollision(ast::Span, Symbol),
13}
14
15#[derive(Debug, PartialEq, Clone)]
16pub enum CompilationWarning {
17    Unimplemented(ast::Span, String),
18}
19
20#[derive(Debug, PartialEq)]
21pub struct CompilationResult {
22    pub ok: bool,
23    pub project: Project,
24    pub errors: Vec<CompilationError>,
25    pub warnings: Vec<CompilationWarning>,
26}
27
28fn compile_ast(ast: Vec<(ast::Span, ast::Stmt)>, source_name: &str, resolver: &impl Resolve) -> CompilationResult {
29    let mut project = Project::new();
30    let mut errors = Vec::new();
31    let mut warnings = Vec::new();
32
33    let mut current_language: Option<ast::Spanned<String>> = None;
34    let mut current_time: ast::Spanned<ast::Time> = (0..0, ast::Time::Instant(0));
35
36    for (span, stmt) in ast {
37        // println!("{:?}", stmt);
38
39        match stmt {
40            ast::Stmt::Milestone { time, language } => {
41                if let Some(time) = time {
42                    current_time = time.clone();
43                    project.milestones.push(match time.1 {
44                        ast::Time::Instant(t) => t,
45                        ast::Time::Range(_, t) => t,
46                    })
47                }
48                if let Some(language) = language {
49                    current_language = Some(language.clone());
50                }
51            }
52            ast::Stmt::SoundChange {
53                source,
54                target,
55                environment,
56                description,
57            } => match &current_language {
58                Some(lang) => {
59                    project.sound_changes.push(SoundChange {
60                        source_name: source_name.into(),
61                        source,
62                        target,
63                        environment,
64                        description,
65                        tag: Tag::new(&lang.clone(), &current_time),
66                    })
67                },
68                _ => errors.push(CompilationError::NoLanguage(span)),
69            }
70            ast::Stmt::Language { id, parent, name } => {
71                let maybe_clash = project.add_symbol(Symbol {
72                    name: id.1.clone(),
73                    loc: Location { source_name: source_name.into(), span },
74                    value: Entity::Language { id: id.clone(), name, parent: parent.clone() },
75                    dependencies: parent.map(|(_, p)| vec![p]).unwrap_or(vec![]),
76                });
77                
78                if let Err(clash) = maybe_clash {
79                    errors.push(CompilationError::NameCollision(id.0, clash));
80                }
81            }
82            ast::Stmt::Word {
83                gloss,
84                pronunciation,
85                definitions,
86            } => match &current_language {
87                Some(lang) => {
88                    let maybe_clash = project.add_symbol(Symbol {
89                        name: gloss.1.clone(),
90                        loc: Location { source_name: source_name.into(), span },
91                        value: Entity::Word {
92                            gloss: gloss.clone(),
93                            pronunciation,
94                            definitions,
95                            tag: Tag::new(&lang.clone(), &current_time)
96                        },
97                        dependencies: vec![],
98                    });
99                
100                    if let Err(clash) = maybe_clash {
101                        errors.push(CompilationError::NameCollision(gloss.0, clash));
102                    }
103                },
104                _ => errors.push(CompilationError::NoLanguage(span)),
105            },
106            ast::Stmt::Class {
107                label,
108                encodes,
109                annotates,
110                phonemes,
111            } => {
112                let phoneme_names = phonemes.iter().map(|p| p.label.1.clone()).collect::<Vec<_>>();
113                let encodes_names = encodes.iter().map(|e| e.1.clone()).collect::<Vec<_>>();
114                let annotates_names = annotates.iter().map(|e| e.1.clone()).collect::<Vec<_>>();
115
116                let maybe_clash = project.add_symbol(Symbol {
117                    name: label.1.clone(),
118                    loc: Location { source_name: source_name.into(), span: span.clone() },
119                    value: Entity::Class {
120                        label: label.clone(),
121                        encodes,
122                        annotates,
123                        phonemes: phoneme_names.clone(),
124                    },
125                    dependencies: [phoneme_names, encodes_names, annotates_names].concat(),
126                });
127                
128                if let Err(clash) = maybe_clash {
129                    errors.push(CompilationError::NameCollision(label.0.clone(), clash));
130                }
131
132                let mut clashes = phonemes.iter()
133                    .flat_map(|p| {
134                        let symbol = Symbol {
135                            name: p.label.1.clone(),
136                            loc: Location { source_name: source_name.into(), span: span.clone() },
137                            value: Entity::Phoneme { class: label.clone(), label: p.label.clone(), traits: p.traits.clone() },
138                            dependencies: vec![label.1.clone()],
139                        };
140
141                        match project.add_symbol(symbol) {
142                            Ok(_) => vec![],
143                            Err(clash) => vec![CompilationError::NameCollision(p.label.0.clone(), clash.clone())],
144                        }
145                    })
146                    .collect::<Vec<_>>();
147
148                errors.append(&mut clashes);
149            },
150            ast::Stmt::Series {
151                label,
152                series
153            } => {
154                let maybe_clash = project.add_symbol(Symbol {
155                    name: label.1.clone(),
156                    loc: Location { source_name: source_name.into(), span: span.clone() },
157                    value: Entity::Series {
158                        label: label.clone(),
159                        series: series.clone(),
160                    },
161                    dependencies: match series.1 {
162                        ast::Series::Category(_) => vec![/* TODO: resolve category to get list of phonemes */],
163                        ast::Series::List(ps) => ps.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
164                    },
165                });
166                
167                if let Err(clash) = maybe_clash {
168                    errors.push(CompilationError::NameCollision(label.0, clash));
169                }
170            },
171            ast::Stmt::Trait {
172                label,
173                members
174            } => {
175                let maybe_clash = project.add_symbol(Symbol {
176                    name: label.1.clone(),
177                    loc: Location { source_name: source_name.into(), span: span.clone() },
178                    value: Entity::Trait {
179                        label: label.clone(),
180                        members: members.clone(),
181                    },
182                    dependencies: members.iter().map(|member| member.labels[0].1.clone()).collect::<Vec<_>>(),
183                });
184                
185                if let Err(clash) = maybe_clash {
186                    errors.push(CompilationError::NameCollision(label.0, clash));
187                }
188
189                let mut clashes = members.iter()
190                    .flat_map(|m| {
191                        let symbol = Symbol {
192                            name: m.labels[0].1.clone(),
193                            loc: Location { source_name: source_name.into(), span: span.clone()},
194                            value: Entity::TraitMember {
195                                label: m.labels[0].clone(),
196                                aliases: m.labels[1..].to_vec(),
197                                default: m.default,
198                                notation: m.notation.clone(),
199                            },
200                            dependencies: vec![label.1.clone()],
201                        };
202
203                        match project.add_symbol(symbol) {
204                            Ok(_) => vec![],
205                            Err(clash) => vec![CompilationError::NameCollision(m.labels[0].0.clone(), clash.clone())],
206                        }
207                    })
208                    .collect::<Vec<_>>();
209
210                errors.append(&mut clashes);
211            },
212            ast::Stmt::Import { path, names, .. } => {
213                let seg_vec = path
214                    .iter()
215                    .map(|(_, seg)| seg.as_str())
216                    .collect::<Vec<_>>();
217                let path = &seg_vec[..];
218                let import_source = resolver.resolve(path);
219                match import_source {
220                    Ok((source, import_source_name)) => {
221                        let res = compile(source.as_str(), &import_source_name, resolver);
222                        errors.append(&mut res.errors.clone());
223                        warnings.append(&mut res.warnings.clone());
224
225                        // println!("\n\n{:#?}\n\n", res.project.symbols.iter().sorted_by_key(|(key, _)| key.to_owned()).map(|(key, value)| (key, value.dependencies.clone())).collect_vec());
226
227                        names
228                            .into_iter()
229                            .for_each(|(span, name)| {
230                                if name == "*" {
231                                    let res = project.import_all_from(&res.project);
232                                    
233                                    if let Err(errs) = res {
234                                        errors.append(&mut errs.iter().map(|e| match e {
235                                            ImportError::NoSuchSymbol(_) => CompilationError::ImportedNameNotFound(span.clone(), name.clone()),
236                                            ImportError::FailedDependency(dep) => CompilationError::ImportDependencyNotFound(dep.clone()),
237                                            ImportError::NameCollision(clash) => CompilationError::NameCollision(span.clone(), clash.clone()),
238                                        }).collect());
239                                    }
240                                }
241                                else {
242                                    let res = project.import(&[name.as_str()], &res.project);
243
244                                    if let Err(errs) = res {
245                                        errors.append(&mut errs.iter().map(|e| match e {
246                                            ImportError::NoSuchSymbol(_) => CompilationError::ImportedNameNotFound(span.clone(), name.clone()),
247                                            ImportError::FailedDependency(dep) => CompilationError::ImportDependencyNotFound(dep.clone()),
248                                            ImportError::NameCollision(clash) => CompilationError::NameCollision(span.clone(), clash.clone()),
249                                        }).collect());
250                                    }
251                                }
252                            })
253                    },
254                    Err(e) => errors.push(CompilationError::BadImport(span, e.to_string()))
255                }
256            },
257        }
258    }
259
260    CompilationResult {
261        ok: errors.len() == 0,
262        project,
263        errors,
264        warnings,
265    }
266}
267
268pub fn compile(source: &str, source_name: &str, resolver: &impl Resolve) -> CompilationResult {
269    match parse(source) {
270        Ok(ast) => compile_ast(ast, source_name, resolver),
271        Err(errs) => CompilationResult {
272            ok: false,
273            project: Project::new(),
274            warnings: Vec::new(),
275            errors: vec![
276                CompilationError::ParseErrors(
277                    errs.iter().map(|e| (e.span(), e.to_string())).collect(),
278                ),
279            ],
280        }
281    }
282}
283
284#[cfg(test)]
285mod tests {
286
287    use std::collections::HashMap;
288
289    use crate::resolver::MockResolver;
290
291    use super::*;
292
293    #[test]
294    fn it_works() {
295        let resolver = MockResolver::new(HashMap::from([
296            ("@core/ipa".into(), "
297            trait Manner { stop, flap | tap, fricative, approximant }
298            trait Place { bilabial, alveolar, palatal, velar }
299            trait Voice { voiced, unvoiced }
300
301            class C encodes (Voice Place Manner) {
302                p = voiceless bilabial stop,
303                b = voiced bilabial stop,
304            }".into()),
305        ]));
306
307        let res = compile(
308            "
309            import * from @core/ipa
310            
311            series F = { i, e, ε, æ }
312
313            class X encodes (Place Manner) {
314                ℂ = velar trill,
315                ℤ = labiodental lateral_fricative,
316            }
317
318            lang OEng : Old English
319            lang AmEng < OEng : American English
320            lang RP < OEng : Received Pronunciation
321            
322            @ 1000, OEng
323            
324            - water /ˈwæ.ter/ {
325                noun. liquid that forms the seas, lakes, rivers, and rain
326                verb. pour or sprinkle water over a plant or area
327            }
328            
329            @ 1940, AmEng
330            
331            $ [C+alveolar+stop] > [+flap] / V_V : Alveolar stops lenite to flaps intervocallically
332            ",
333            "demo",
334            &resolver,
335        );
336
337        assert!(res.ok);
338        assert_eq!(res.errors, vec![]);
339        assert_eq!(res.warnings, vec![]);
340    }
341
342    #[test]
343    fn it_raises_name_collision_errors() {
344        let resolver = MockResolver::new(HashMap::from([
345            ("consonants".into(), "
346            trait Manner { stop, flap | tap, fricative, approximant }
347            trait Place { bilabial, alveolar, palatal, velar }
348            trait Voice { voiced, unvoiced }
349
350            class C encodes (Voice Place Manner) {
351                p = voiceless bilabial stop,
352                b = voiced bilabial stop,
353            }".into()),
354        ]));
355
356        let res = compile("
357            import (C) from consonants
358
359            series C = { p, b }
360
361            class B encodes (Voice Place Manner) {
362                b = voiceless bilabial stop
363            }
364
365            lang B: b-lang
366        ", "demo", &resolver);
367        
368        let expected_errors = vec![
369            CompilationError::NameCollision(
370                60..61,
371                Symbol {
372                    name: "C".into(),
373                    loc: Location { source_name: "consonants".into(), span: 192..331 },
374                    value: Entity::Class {
375                        label: (198..199, "C".into()),
376                        encodes: vec![
377                            (209..214, "Voice".into()),
378                            (215..220, "Place".into()),
379                            (221..227, "Manner".into()),
380                        ],
381                        annotates: vec![],
382                        phonemes: vec!["p".into(), "b".into()],
383                    },
384                    dependencies: vec!["p".into(), "b".into(), "Voice".into(), "Place".into(), "Manner".into()],
385                }
386            ),
387            CompilationError::NameCollision(
388                141..142,
389                Symbol {
390                    name: "b".into(),
391                    loc: Location { source_name: "consonants".into(), span: 192..331 },
392                    value: Entity::Phoneme {
393                        class:  (198..199, "C".into()),
394                        label: (292..293, "b".into()),
395                        traits: vec![
396                            (296..302, "voiced".into()),
397                            (303..311, "bilabial".into()),
398                            (312..316, "stop".into()),
399                        ],
400                    },
401                    dependencies: vec!["C".into()],
402                }
403            ),
404            CompilationError::NameCollision(
405                201..202,
406                Symbol {
407                    name: "B".into(),
408                    loc: Location { source_name: "demo".into(), span: 86..182 },
409                    value: Entity::Class {
410                        label: (92..93, "B".into()),
411                        encodes: vec![
412                            (103..108, "Voice".into()),
413                            (109..114, "Place".into()),
414                            (115..121, "Manner".into()),
415                        ],
416                        annotates: vec![],
417                        phonemes: vec!["b".into()],
418                    },
419                    dependencies: vec!["b".into(), "Voice".into(), "Place".into(), "Manner".into()],
420                }
421            ),
422        ];
423
424        // println!("\n\n{:#?}\n\n", res.project.symbols.keys().collect::<Vec<_>>());
425
426        // println!("\n\n{:#?}\n\n", res);
427
428        // println!("Expected:\n{:#?}\n\n", expected_errors);
429        // println!("Actual:\n{:#?}\n\n", res.errors);
430        
431        assert_eq!(res.errors, expected_errors);
432    }
433}