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 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 ¤t_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(), ¤t_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 ¤t_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(), ¤t_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![],
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 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 assert_eq!(res.errors, expected_errors);
432 }
433}