Skip to main content

basalt_bedrock/language/
mod.rs

1mod language_set;
2use std::str::FromStr;
3
4pub use language_set::LanguageSet;
5
6use phf::{phf_map, phf_ordered_map};
7use serde::{Deserialize, Serialize};
8use strum::VariantNames;
9
10struct LanguageVersion {
11    build: Option<&'static str>,
12    run: &'static str,
13    install_command: Option<&'static str>,
14    init_command: Option<&'static str>,
15}
16
17struct Builtin {
18    builtin: BuiltInLanguage,
19    source_file: &'static str,
20    syntax: Syntax,
21    versions: phf::OrderedMap<&'static str, LanguageVersion>,
22}
23
24// TODO: enforce minimum version count of 1 at compile time
25static BUILTINS: phf::Map<&'static str, Builtin> = phf_map! {
26    "python3" => Builtin {
27        builtin: BuiltInLanguage::Python3,
28        source_file: "solution.py",
29        syntax: Syntax::Python,
30        versions: phf_ordered_map! {
31            "latest" => LanguageVersion {
32                build: None,
33                run: "python3 ./solution.py",
34                install_command: Some("dnf install python3 -y"),
35                init_command: None,
36            }
37        },
38    },
39    "java" => Builtin {
40        builtin: BuiltInLanguage::Java,
41        source_file: "Solution.java",
42        syntax: Syntax::Java,
43        versions: phf_ordered_map! { // `java[c]` is fine since we only allow one language at a time
44            "8" => LanguageVersion {
45                build: Some("javac Solution.java"),
46                run: "java Solution",
47                install_command: Some("dnf install java-1.8.0-openjdk-devel -y"),
48                init_command: None,
49            },
50            "11" => LanguageVersion {
51                build: Some("javac Solution.java"),
52                run: "java Solution",
53                install_command: Some("dnf install java-11-openjdk-devel -y"),
54                init_command: None,
55            },
56            "21" => LanguageVersion {
57                build: Some("javac Solution.java"),
58                run: "java Solution",
59                install_command: Some("dnf install java-21-openjdk-devel -y"),
60                init_command: None,
61            },
62        },
63    },
64    "javascript" => Builtin {
65        builtin: BuiltInLanguage::JavaScript,
66        source_file: "solution.js",
67        syntax: Syntax::Javascript,
68        versions: phf_ordered_map! {
69            "latest" => LanguageVersion {
70                build: None,
71                run: "nodejs solution.js",
72                install_command: Some("dnf install nodejs20 -y"),
73                init_command: None,
74            }
75        },
76    },
77    "rust" => Builtin {
78        builtin: BuiltInLanguage::Rust,
79        source_file: "solution.rs",
80        syntax: Syntax::Rust,
81        versions: phf_ordered_map! {
82            "latest" => LanguageVersion {
83                build: Some("rustc -o solution solution.rs"),
84                run: "./solution",
85                install_command: Some("dnf install rust -y"),
86                init_command: None,
87            }
88        },
89    },
90};
91
92#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, VariantNames)]
93#[strum(serialize_all = "lowercase")]
94pub enum BuiltInLanguage {
95    Python3,
96    Java,
97    JavaScript,
98    Rust,
99}
100
101impl BuiltInLanguage {
102    pub fn has_version(self, version: &Version) -> Result<(), Vec<&str>> {
103        let bil = &BUILTINS[self.name()];
104        match version {
105            Version::Latest => Ok(()),
106            Version::Specific(v) => {
107                if bil.versions.contains_key(v) {
108                    Ok(())
109                } else {
110                    Err(bil.versions.keys().copied().collect())
111                }
112            }
113        }
114    }
115
116    pub fn joined_variants() -> String {
117        BuiltInLanguage::VARIANTS
118            .iter()
119            .map(|s| format!("'{}'", s))
120            .collect::<Vec<_>>()
121            .join(", ")
122    }
123
124    pub const fn name(self) -> &'static str {
125        match self {
126            Self::Python3 => "python3",
127            Self::Java => "java",
128            Self::JavaScript => "javascript",
129            Self::Rust => "rust",
130        }
131    }
132
133    pub const fn display_name(self) -> &'static str {
134        match self {
135            Self::Python3 => "Python3",
136            Self::Java => "Java",
137            Self::JavaScript => "JavaScript",
138            Self::Rust => "Rust",
139        }
140    }
141
142    pub fn source_file(self) -> &'static str {
143        BUILTINS[self.name()].source_file
144    }
145
146    pub fn build_command(self, version: &Version) -> Option<&str> {
147        let bil = &BUILTINS[self.name()];
148        match version {
149            Version::Latest => bil.versions.values().last()?.build,
150            Version::Specific(v) => bil.versions[v].build,
151        }
152    }
153
154    pub fn run_command(self, version: &Version) -> &str {
155        let bil = &BUILTINS[self.name()];
156        match version {
157            Version::Latest => {
158                bil.versions
159                    .values()
160                    .last()
161                    .expect("all language must have at least one version")
162                    .run
163            }
164            Version::Specific(v) => bil.versions[v].run,
165        }
166    }
167
168    pub fn install_command(self, version: &Version) -> Option<&str> {
169        let bil = &BUILTINS[self.name()];
170        match version {
171            Version::Latest => {
172                bil.versions
173                    .values()
174                    .last()
175                    .expect("all language must have at least one version")
176                    .install_command
177            }
178            Version::Specific(v) => bil.versions[v].install_command,
179        }
180    }
181
182    pub fn init_command(self, version: &Version) -> Option<&str> {
183        let bil = &BUILTINS[self.name()];
184        match version {
185            Version::Latest => {
186                bil.versions
187                    .values()
188                    .last()
189                    .expect("all language must have at least one version")
190                    .init_command
191            }
192            Version::Specific(v) => bil.versions[v].init_command,
193        }
194    }
195
196    pub fn syntax(self) -> Syntax {
197        BUILTINS[self.name()].syntax
198    }
199}
200
201impl From<&str> for BuiltInLanguage {
202    fn from(value: &str) -> Self {
203        BUILTINS[value].builtin
204    }
205}
206
207impl FromStr for BuiltInLanguage {
208    type Err = ();
209
210    fn from_str(s: &str) -> Result<Self, Self::Err> {
211        BUILTINS.get(s).map(|b| b.builtin).ok_or(())
212    }
213}
214
215#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
216pub enum Version {
217    Latest,
218    Specific(String),
219}
220
221// Mostly from <https://github.com/ajaxorg/ace/tree/master/src/mode>
222#[derive(
223    Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Default,
224)]
225#[serde(rename_all = "snake_case")]
226pub enum Syntax {
227    Ada,
228    Basic,
229    Batchfile,
230    #[serde(alias = "c", alias = "cpp")]
231    CCpp,
232    Clojure,
233    Cobol,
234    Csharp,
235    D,
236    Dart,
237    Ejs,
238    Elixir,
239    Elm,
240    Erlang,
241    Forth,
242    Fortran,
243    Fsharp,
244    Golang,
245    Haskell,
246    Java,
247    Javascript,
248    Julia,
249    Kotlin,
250    Lisp,
251    Lua,
252    Mips,
253    Nim,
254    Nix,
255    Ocaml,
256    Odin,
257    Pascal,
258    Perl,
259    Php,
260    #[default]
261    PlainText,
262    Powershell,
263    Prolog,
264    Python,
265    R,
266    Ruby,
267    Rust,
268    Scala,
269    Scheme,
270    Sh,
271    Typescript,
272    Zig,
273}
274
275impl Syntax {
276    pub fn from_string<E: serde::de::Error>(s: impl AsRef<str>) -> Result<Self, E> {
277        Syntax::deserialize(serde::de::value::StrDeserializer::<E>::new(s.as_ref()))
278    }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
282pub enum Language {
283    BuiltIn {
284        language: BuiltInLanguage,
285        version: Version,
286    },
287    Custom {
288        name: String,
289        display_name: String,
290        build: Option<String>,
291        run: String,
292        source_file: String,
293        syntax: Syntax,
294    },
295}
296
297impl Language {
298    pub fn name(&self) -> &str {
299        match self {
300            Language::BuiltIn { language, .. } => language.name(),
301            Language::Custom { name, .. } => name,
302        }
303    }
304
305    pub fn display_name(&self) -> &str {
306        match self {
307            Language::BuiltIn { language, .. } => language.display_name(),
308            Language::Custom { display_name, .. } => display_name,
309        }
310    }
311
312    pub fn source_file(&self) -> &str {
313        match self {
314            Language::BuiltIn { language, .. } => language.source_file(),
315            Language::Custom { source_file, .. } => source_file,
316        }
317    }
318
319    pub fn build_command(&self) -> Option<&str> {
320        match self {
321            Language::BuiltIn { language, version } => language.build_command(version),
322            Language::Custom { build, .. } => build.as_deref(),
323        }
324    }
325
326    pub fn run_command(&self) -> &str {
327        match self {
328            Language::BuiltIn { language, version } => language.run_command(version),
329            Language::Custom { run, .. } => run,
330        }
331    }
332
333    pub fn install_command(&self) -> Option<&str> {
334        match self {
335            Language::BuiltIn { language, version } => language.install_command(version),
336            Language::Custom { .. } => None,
337        }
338    }
339
340    pub fn init_command(&self) -> Option<&str> {
341        match self {
342            Language::BuiltIn { language, version } => language.init_command(version),
343            Language::Custom { .. } => None,
344        }
345    }
346
347    pub fn syntax(&self) -> Syntax {
348        match self {
349            Language::BuiltIn { language, .. } => language.syntax(),
350            Language::Custom { syntax, .. } => *syntax,
351        }
352    }
353}