Skip to main content

lumen_modules/
language.rs

1//! Which runtime module runs which script language.
2//!
3//! A script host is a runtime module like any other. What makes it a host is
4//! a descriptor beside it, `lumen-language.toml`, which names the language it
5//! runs, the file extensions that language owns, and the form it runs a
6//! program in:
7//!
8//! ```toml
9//! [language]
10//! name = "candela"
11//! extensions = ["cdl"]
12//! form = "bytecode"   # or "source"
13//! default = true      # the language an inline `<script>` block is read as
14//! ```
15//!
16//! One language can have two providers: one that runs source, with a compiler
17//! and hot reload, for the edit loop, and one that runs bytecode compiled
18//! ahead of time, for a shipped app. A language with a single source-form
19//! provider runs from source everywhere.
20//!
21//! The descriptors are found where bundled modules are: `modules/<name>/`
22//! under the directories a toolchain ships them in, and the crate under
23//! `std/` of a source checkout. Nothing here names a language.
24
25use 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
33/// The descriptor's file name, in a module's directory.
34pub const DESCRIPTOR: &str = "lumen-language.toml";
35
36/// The directory a toolchain keeps its bundled modules' files in, beside its
37/// binaries.
38pub const BUNDLED_MODULE_DIR: &str = "modules";
39
40/// The form a provider runs a program in.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum Form {
44    /// Bytecode compiled ahead of time; the provider carries no compiler.
45    Bytecode,
46    /// Source text, compiled when the program loads.
47    Source,
48}
49
50/// One module's claim to run a language.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct LanguageProvider {
53    /// The module's declared name, the one the loader opens it by.
54    pub module: String,
55    /// The language it runs.
56    pub language: String,
57    /// The file extensions the language owns, without the dot.
58    pub extensions: Vec<String>,
59    /// The form it runs a program in.
60    pub form: Form,
61    /// Whether the language is the one an inline `<script>` block is read as.
62    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
82/// Read the descriptor `text` of the module `module`.
83///
84/// # Errors
85///
86/// The text is not a descriptor, or names a language with no name.
87pub 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/// Every language provider in reach, and the questions an app assembly asks
110/// of them.
111#[derive(Debug, Clone, Default, PartialEq, Eq)]
112pub struct LanguageTable {
113    providers: Vec<LanguageProvider>,
114}
115
116impl LanguageTable {
117    /// A table over `providers`. A module listed twice keeps its first entry.
118    ///
119    /// # Errors
120    ///
121    /// Two different languages both claim to be the default, or two languages
122    /// claim one extension.
123    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    /// The descriptors in reach of this process: `modules/<name>/` under
163    /// `lib_dir`, beside the running executable, beside the shared engine
164    /// library, and under `LUMEN_LIB_DIR`; then the ones the modules linked
165    /// into this binary registered; then the crates under `std/` of the source
166    /// checkout this crate was built in, when that checkout is still on disk.
167    /// The first descriptor found for a module is the one read.
168    ///
169    /// # Errors
170    ///
171    /// A descriptor does not parse, or the descriptors disagree (see
172    /// [`Self::new`]).
173    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    /// Every provider, in the order they were found.
204    pub fn providers(&self) -> &[LanguageProvider] {
205        &self.providers
206    }
207
208    /// Whether no provider is in reach.
209    pub fn is_empty(&self) -> bool {
210        self.providers.is_empty()
211    }
212
213    /// The language a script file belongs to, read off its extension.
214    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    /// The language an inline `<script>` block is read as.
223    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    /// Every extension a language owns.
231    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    /// The provider that runs `language` from source: the edit loop's, which
243    /// can check, compile, and reload a program.
244    pub fn source_provider(&self, language: &str) -> Option<&LanguageProvider> {
245        self.provider(language, Form::Source)
246    }
247
248    /// The provider a shipped app runs `language` with: the bytecode one when
249    /// the language has one, the source one otherwise.
250    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
262/// An app's scripts split by the language that runs them: one entry per
263/// language, each holding that language's whole program, sorted by language
264/// name. Empty when the app ships no script.
265pub type GroupedScripts = Vec<GroupedScript>;
266
267/// One language's whole program.
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct GroupedScript {
270    /// The language's name.
271    pub language: String,
272    /// Every piece the language owns, concatenated in source order.
273    pub source: String,
274    /// Where each piece of [`Self::source`] was read from.
275    pub map: SourceMap,
276}
277
278/// One external script file an app's markup names.
279#[derive(Debug, Clone)]
280pub struct ScriptFile<'a> {
281    /// The path the `<script src>` wrote, which picks the language.
282    pub rel: &'a str,
283    /// The file as an error names it.
284    pub file: String,
285    /// The file's text.
286    pub body: String,
287}
288
289/// Which language each part of an app's script belongs to.
290///
291/// `engine` is the app's `[script] engine` key, which puts every script, inline
292/// and external, on one language. Otherwise each external file belongs to its
293/// extension's language (an extension no language claims reads as the
294/// default language), and the inline block joins the app's one external
295/// language when there is exactly one, and the default language otherwise.
296#[derive(Debug, Clone, Copy)]
297pub struct ScriptGrouping<'a> {
298    /// The descriptors in reach.
299    pub table: &'a LanguageTable,
300    /// The `[script] engine` override.
301    pub engine: Option<&'a str>,
302}
303
304impl ScriptGrouping<'_> {
305    /// The language an external script file belongs to.
306    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    /// The language the inline block belongs to, given the app's external
318    /// script files.
319    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    /// The languages an app needs, from whether it has an inline block and
339    /// the paths of its external script files: what a run loads hosts for
340    /// before anything is parsed.
341    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    /// Split an app's script by language. `inline` is the inline blocks and
355    /// `inline_map` where each of them was written; `externals` is each
356    /// external file, in source order.
357    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
395/// `deps` with a bundled entry added for each module in `modules` it does not
396/// already declare: the host modules an app needs, loaded the way a declared
397/// module is.
398pub 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
413/// The directories `modules/<name>/` is looked for under, in order.
414fn 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
435/// The descriptors under `std/` of the checkout this crate was compiled in,
436/// keyed by package name. A release build's tree is gone by the time anyone
437/// runs it, so there this finds nothing.
438fn 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
459/// The package name in the `Cargo.toml` at `root`.
460fn 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}