1use std::collections::BTreeMap;
26use std::path::{Path, PathBuf};
27
28use lumen_ir::source_map::SourceMap;
29use serde::Deserialize;
30
31use crate::{DepCfg, DependenciesCfg, ModuleSource};
32
33pub const DESCRIPTOR: &str = "lumen-language.toml";
35
36pub const BUNDLED_MODULE_DIR: &str = "modules";
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum Form {
44 Bytecode,
46 Source,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct LanguageProvider {
53 pub module: String,
55 pub language: String,
57 pub extensions: Vec<String>,
59 pub form: Form,
61 pub default: bool,
63}
64
65#[derive(Deserialize)]
66#[serde(deny_unknown_fields)]
67struct Descriptor {
68 language: LanguageSection,
69}
70
71#[derive(Deserialize)]
72#[serde(deny_unknown_fields)]
73struct LanguageSection {
74 name: String,
75 #[serde(default)]
76 extensions: Vec<String>,
77 form: Form,
78 #[serde(default)]
79 default: bool,
80}
81
82pub fn parse_descriptor(module: &str, text: &str) -> Result<LanguageProvider, String> {
88 let descriptor: Descriptor =
89 toml::from_str(text).map_err(|e| format!("module '{module}': {DESCRIPTOR}: {e}"))?;
90 let section = descriptor.language;
91 if section.name.trim().is_empty() {
92 return Err(format!(
93 "module '{module}': {DESCRIPTOR} names a language with an empty name"
94 ));
95 }
96 Ok(LanguageProvider {
97 module: module.to_string(),
98 language: section.name,
99 extensions: section
100 .extensions
101 .into_iter()
102 .map(|e| e.trim_start_matches('.').to_string())
103 .collect(),
104 form: section.form,
105 default: section.default,
106 })
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Eq)]
112pub struct LanguageTable {
113 providers: Vec<LanguageProvider>,
114}
115
116impl LanguageTable {
117 pub fn new(providers: Vec<LanguageProvider>) -> Result<Self, String> {
124 let mut kept: Vec<LanguageProvider> = Vec::new();
125 for provider in providers {
126 if kept.iter().any(|p| p.module == provider.module) {
127 continue;
128 }
129 kept.push(provider);
130 }
131 let mut defaults: Vec<&str> = kept
132 .iter()
133 .filter(|p| p.default)
134 .map(|p| p.language.as_str())
135 .collect();
136 defaults.sort_unstable();
137 defaults.dedup();
138 if defaults.len() > 1 {
139 return Err(format!(
140 "script languages {} each claim to be the default; one language reads inline \
141 `<script>` blocks, so at most one module descriptor may say `default = true`",
142 defaults.join(" and ")
143 ));
144 }
145 let mut owners: BTreeMap<&str, &str> = BTreeMap::new();
146 for provider in &kept {
147 for ext in &provider.extensions {
148 match owners.insert(ext, &provider.language) {
149 Some(other) if other != provider.language => {
150 return Err(format!(
151 "script languages {other} and {} both claim the `.{ext}` extension",
152 provider.language
153 ));
154 }
155 _ => {}
156 }
157 }
158 }
159 Ok(Self { providers: kept })
160 }
161
162 pub fn discover(lib_dir: Option<&Path>) -> Result<Self, String> {
174 let mut providers = Vec::new();
175 for root in search_roots(lib_dir) {
176 let Ok(entries) = std::fs::read_dir(root.join(BUNDLED_MODULE_DIR)) else {
177 continue;
178 };
179 let mut dirs: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
180 dirs.sort();
181 for dir in dirs {
182 let file = dir.join(DESCRIPTOR);
183 let Ok(text) = std::fs::read_to_string(&file) else {
184 continue;
185 };
186 let module = dir
187 .file_name()
188 .map(|n| n.to_string_lossy().into_owned())
189 .unwrap_or_default();
190 providers.push(parse_descriptor(&module, &text)?);
191 }
192 }
193 #[cfg(feature = "loader")]
194 for (module, text) in lumen_module_registry::languages() {
195 providers.push(parse_descriptor(module, text)?);
196 }
197 for (module, text) in checkout_descriptors() {
198 providers.push(parse_descriptor(&module, &text)?);
199 }
200 Self::new(providers)
201 }
202
203 pub fn providers(&self) -> &[LanguageProvider] {
205 &self.providers
206 }
207
208 pub fn is_empty(&self) -> bool {
210 self.providers.is_empty()
211 }
212
213 pub fn language_of(&self, path: &Path) -> Option<&str> {
215 let ext = path.extension()?.to_str()?;
216 self.providers
217 .iter()
218 .find(|p| p.extensions.iter().any(|e| e == ext))
219 .map(|p| p.language.as_str())
220 }
221
222 pub fn default_language(&self) -> Option<&str> {
224 self.providers
225 .iter()
226 .find(|p| p.default)
227 .map(|p| p.language.as_str())
228 }
229
230 pub fn extensions(&self) -> Vec<&str> {
232 let mut all: Vec<&str> = self
233 .providers
234 .iter()
235 .flat_map(|p| p.extensions.iter().map(String::as_str))
236 .collect();
237 all.sort_unstable();
238 all.dedup();
239 all
240 }
241
242 pub fn source_provider(&self, language: &str) -> Option<&LanguageProvider> {
245 self.provider(language, Form::Source)
246 }
247
248 pub fn shipped_provider(&self, language: &str) -> Option<&LanguageProvider> {
251 self.provider(language, Form::Bytecode)
252 .or_else(|| self.provider(language, Form::Source))
253 }
254
255 fn provider(&self, language: &str, form: Form) -> Option<&LanguageProvider> {
256 self.providers
257 .iter()
258 .find(|p| p.language == language && p.form == form)
259 }
260}
261
262pub type GroupedScripts = Vec<GroupedScript>;
266
267#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct GroupedScript {
270 pub language: String,
272 pub source: String,
274 pub map: SourceMap,
276}
277
278#[derive(Debug, Clone)]
280pub struct ScriptFile<'a> {
281 pub rel: &'a str,
283 pub file: String,
285 pub body: String,
287}
288
289#[derive(Debug, Clone, Copy)]
297pub struct ScriptGrouping<'a> {
298 pub table: &'a LanguageTable,
300 pub engine: Option<&'a str>,
302}
303
304impl ScriptGrouping<'_> {
305 pub fn external_language(&self, rel: &str) -> String {
307 if let Some(engine) = self.engine {
308 return engine.to_string();
309 }
310 self.table
311 .language_of(Path::new(rel))
312 .or_else(|| self.table.default_language())
313 .unwrap_or_default()
314 .to_string()
315 }
316
317 pub fn inline_language(&self, externals: &[&str]) -> String {
320 if let Some(engine) = self.engine {
321 return engine.to_string();
322 }
323 let mut languages: Vec<String> = externals
324 .iter()
325 .map(|rel| self.external_language(rel))
326 .collect();
327 languages.dedup();
328 match languages.as_slice() {
329 [only] => only.clone(),
330 _ => self
331 .table
332 .default_language()
333 .unwrap_or_default()
334 .to_string(),
335 }
336 }
337
338 pub fn languages(&self, has_inline: bool, externals: &[&str]) -> Vec<String> {
342 let mut needed: Vec<String> = externals
343 .iter()
344 .map(|rel| self.external_language(rel))
345 .collect();
346 if has_inline {
347 needed.push(self.inline_language(externals));
348 }
349 needed.sort();
350 needed.dedup();
351 needed
352 }
353
354 pub fn group(
358 &self,
359 inline: &str,
360 inline_map: &SourceMap,
361 externals: &[ScriptFile<'_>],
362 ) -> GroupedScripts {
363 let rels: Vec<&str> = externals.iter().map(|f| f.rel).collect();
364 let mut sources: GroupedScripts = Vec::new();
365 fn slot(sources: &mut GroupedScripts, language: String) -> &mut GroupedScript {
366 let i = match sources.iter().position(|g| g.language == language) {
367 Some(i) => i,
368 None => {
369 sources.push(GroupedScript {
370 language,
371 source: String::new(),
372 map: SourceMap::default(),
373 });
374 sources.len() - 1
375 }
376 };
377 &mut sources[i]
378 }
379 if !inline.trim().is_empty() {
380 let g = slot(&mut sources, self.inline_language(&rels));
381 g.map.append_mapped(&mut g.source, inline, inline_map);
382 }
383 for f in externals {
384 if f.body.trim().is_empty() {
385 continue;
386 }
387 let g = slot(&mut sources, self.external_language(f.rel));
388 g.map.append(&mut g.source, &f.body, &f.file, 1);
389 }
390 sources.sort_by(|a, b| a.language.cmp(&b.language));
391 sources
392 }
393}
394
395pub fn with_implied(deps: &DependenciesCfg, modules: &[String]) -> DependenciesCfg {
399 let mut all = deps.clone();
400 for module in modules {
401 if !all.0.iter().any(|d| d.name == *module) {
402 all.0.push(DepCfg {
403 name: module.clone(),
404 source: ModuleSource::Bundled,
405 config: toml::Table::new(),
406 tags: Vec::new(),
407 });
408 }
409 }
410 all
411}
412
413fn search_roots(lib_dir: Option<&Path>) -> Vec<PathBuf> {
415 let mut roots: Vec<PathBuf> = Vec::new();
416 if let Some(dir) = lib_dir {
417 roots.push(dir.to_path_buf());
418 }
419 if let Ok(exe) = std::env::current_exe()
420 && let Some(dir) = exe.parent()
421 {
422 roots.push(dir.to_path_buf());
423 }
424 #[cfg(feature = "loader")]
425 if let Some(dir) = crate::engine_dir() {
426 roots.push(dir);
427 }
428 if let Some(dir) = std::env::var_os("LUMEN_LIB_DIR") {
429 roots.push(PathBuf::from(dir));
430 }
431 roots.dedup();
432 roots
433}
434
435fn checkout_descriptors() -> Vec<(String, String)> {
439 let Some(std_dir) = Path::new(env!("CARGO_MANIFEST_DIR"))
440 .ancestors()
441 .map(|dir| dir.join("std"))
442 .find(|dir| dir.is_dir())
443 else {
444 return Vec::new();
445 };
446 let Ok(entries) = std::fs::read_dir(&std_dir) else {
447 return Vec::new();
448 };
449 let mut dirs: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
450 dirs.sort();
451 dirs.into_iter()
452 .filter_map(|dir| {
453 let text = std::fs::read_to_string(dir.join(DESCRIPTOR)).ok()?;
454 Some((package_name(&dir)?, text))
455 })
456 .collect()
457}
458
459fn package_name(root: &Path) -> Option<String> {
461 let text = std::fs::read_to_string(root.join("Cargo.toml")).ok()?;
462 let manifest: toml::Table = toml::from_str(&text).ok()?;
463 manifest
464 .get("package")?
465 .get("name")?
466 .as_str()
467 .map(str::to_string)
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 fn provider(
475 module: &str,
476 language: &str,
477 ext: &str,
478 form: Form,
479 default: bool,
480 ) -> LanguageProvider {
481 LanguageProvider {
482 module: module.into(),
483 language: language.into(),
484 extensions: vec![ext.into()],
485 form,
486 default,
487 }
488 }
489
490 fn table() -> LanguageTable {
491 LanguageTable::new(vec![
492 provider("toy-vm", "toy", "toy", Form::Bytecode, true),
493 provider("toy-dev", "toy", "toy", Form::Source, true),
494 provider("old", "old", "old", Form::Source, false),
495 ])
496 .expect("one default language")
497 }
498
499 #[test]
500 fn a_descriptor_reads_into_a_provider() {
501 let p = parse_descriptor(
502 "toy-dev",
503 "[language]\nname = \"toy\"\nextensions = [\".toy\"]\nform = \"source\"\ndefault = true\n",
504 )
505 .expect("parses");
506 assert_eq!(p, provider("toy-dev", "toy", "toy", Form::Source, true));
507 assert!(parse_descriptor("x", "[language]\nname = \"\"\nform = \"source\"\n").is_err());
508 assert!(parse_descriptor("x", "[language]\nname = \"a\"\nform = \"jit\"\n").is_err());
509 }
510
511 #[test]
512 fn two_default_languages_are_refused() {
513 let refusal = LanguageTable::new(vec![
514 provider("a", "a", "a", Form::Source, true),
515 provider("b", "b", "b", Form::Source, true),
516 ])
517 .expect_err("two defaults");
518 assert!(refusal.contains("a and b"), "{refusal}");
519 }
520
521 #[test]
522 fn two_languages_claiming_one_extension_are_refused() {
523 assert!(
524 LanguageTable::new(vec![
525 provider("a", "a", "x", Form::Source, false),
526 provider("b", "b", "x", Form::Source, false),
527 ])
528 .is_err()
529 );
530 }
531
532 #[test]
533 fn a_shipped_app_takes_the_bytecode_provider_when_there_is_one() {
534 let t = table();
535 assert_eq!(
536 t.shipped_provider("toy").map(|p| p.module.as_str()),
537 Some("toy-vm")
538 );
539 assert_eq!(
540 t.source_provider("toy").map(|p| p.module.as_str()),
541 Some("toy-dev")
542 );
543 assert_eq!(
544 t.shipped_provider("old").map(|p| p.module.as_str()),
545 Some("old")
546 );
547 assert!(t.shipped_provider("nope").is_none());
548 }
549
550 #[test]
551 fn scripts_group_by_extension_and_inline_follows_the_one_external_language() {
552 let t = table();
553 let g = ScriptGrouping {
554 table: &t,
555 engine: None,
556 };
557 let file = |rel: &'static str, body: &str| ScriptFile {
558 rel,
559 file: format!("src/{rel}"),
560 body: body.into(),
561 };
562 let mut inline_map = SourceMap::default();
563 inline_map.append(&mut String::new(), "inline();", "main.lmn", 4);
564 let sources = |grouped: &GroupedScripts| -> Vec<(String, String)> {
565 grouped
566 .iter()
567 .map(|g| (g.language.clone(), g.source.clone()))
568 .collect()
569 };
570 let grouped = g.group(
571 "inline();",
572 &inline_map,
573 &[file("a.old", "a();"), file("b.old", "b();")],
574 );
575 assert_eq!(
576 sources(&grouped),
577 vec![("old".into(), "inline();\na();\nb();".into())]
578 );
579 assert_eq!(grouped[0].map.locate(1), Some(("main.lmn", 4)));
580 assert_eq!(grouped[0].map.locate(2), Some(("src/a.old", 1)));
581 assert_eq!(grouped[0].map.locate(3), Some(("src/b.old", 1)));
582
583 let mixed = g.group(
584 "inline();",
585 &inline_map,
586 &[file("a.old", "a();"), file("m.toy", "m();")],
587 );
588 assert_eq!(
589 sources(&mixed),
590 vec![
591 ("old".into(), "a();".into()),
592 ("toy".into(), "inline();\nm();".into())
593 ]
594 );
595 assert_eq!(mixed[0].map.locate(1), Some(("src/a.old", 1)));
596 assert_eq!(mixed[1].map.locate(2), Some(("src/m.toy", 1)));
597 assert_eq!(g.languages(true, &[]), vec!["toy".to_string()]);
598 assert!(g.languages(false, &[]).is_empty());
599 }
600
601 #[test]
602 fn the_engine_key_puts_every_script_on_one_language() {
603 let t = table();
604 let g = ScriptGrouping {
605 table: &t,
606 engine: Some("old"),
607 };
608 let grouped = g.group(
609 "i();",
610 &SourceMap::default(),
611 &[ScriptFile {
612 rel: "m.toy",
613 file: "m.toy".into(),
614 body: "m();".into(),
615 }],
616 );
617 assert_eq!(grouped.len(), 1);
618 assert_eq!(grouped[0].language, "old");
619 assert_eq!(grouped[0].source, "i();\nm();");
620 }
621
622 #[test]
623 fn implied_modules_join_the_declared_ones_once() {
624 let declared = DependenciesCfg(vec![DepCfg {
625 name: "toy-dev".into(),
626 source: ModuleSource::Path("x".into()),
627 config: toml::Table::new(),
628 tags: Vec::new(),
629 }]);
630 let all = with_implied(&declared, &["toy-dev".into(), "old".into()]);
631 assert_eq!(all.0.len(), 2);
632 assert_eq!(all.0[0].source, ModuleSource::Path("x".into()));
633 assert_eq!(all.0[1].source, ModuleSource::Bundled);
634 }
635}