1use std::path::PathBuf;
17
18use bevy_ecs::component::Mutable;
19use bevy_ecs::prelude::{Resource, World};
20use lumen_core::prelude::App;
21use lumen_core::warn_line;
22use lumen_ir::fragment::FragmentComponent;
23use lumen_ir::source_map::SourceMap;
24
25use crate::{ScriptError, ScriptFn, ScriptHost, ScriptPrelude, ScriptValue};
26
27#[derive(Debug, Clone, Default)]
29pub struct ScriptProgram {
30 pub source: String,
33 pub bytecode: Option<Vec<u8>>,
37 pub uri: String,
39 pub source_map: SourceMap,
42 pub lib_dir: Option<PathBuf>,
45 pub import_roots: Vec<(String, PathBuf)>,
48 pub cfg_flags: Vec<String>,
50}
51
52#[derive(Debug, Clone, Copy)]
56pub struct ScriptCompile<'a> {
57 pub uri: &'a str,
59 pub source_map: &'a SourceMap,
62 pub lib_dir: Option<&'a std::path::Path>,
64 pub import_roots: &'a [(String, PathBuf)],
66 pub cfg_flags: &'a [&'a str],
68 pub fns: &'a [ScriptFn],
70 pub preludes: &'a [ScriptPrelude],
73}
74
75#[derive(Debug, Clone, PartialEq)]
78pub struct MarkupBlock {
79 pub offset: usize,
82 pub markup: String,
84 pub key: String,
86 pub args: Vec<String>,
88 pub component: Option<FragmentComponent>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct MarkupBlockError {
96 pub offset: usize,
98 pub message: String,
100}
101
102#[derive(Clone, Copy)]
107pub struct ScriptHostAccess {
108 pub signal: fn(&World, &str) -> Option<ScriptValue>,
110 pub call: fn(&mut World, &str) -> Result<Option<ScriptValue>, ScriptError>,
113 pub exports: fn(&World) -> Vec<String>,
115}
116
117impl ScriptHostAccess {
118 pub fn absent() -> Self {
121 Self {
122 signal: |_, _| None,
123 call: |_, _| Err(ScriptError::Runtime("no script is loaded".to_owned())),
124 exports: |_| Vec::new(),
125 }
126 }
127
128 pub fn of<H>(exports: fn(&World) -> Vec<String>) -> Self
132 where
133 H: ScriptHost + Resource<Mutability = Mutable>,
134 {
135 Self {
136 signal: signal_of::<H>,
137 call: call_of::<H>,
138 exports,
139 }
140 }
141}
142
143fn signal_of<H>(world: &World, name: &str) -> Option<ScriptValue>
145where
146 H: ScriptHost + Resource<Mutability = Mutable>,
147{
148 world.get_resource::<H>().and_then(|h| h.mirror_get(name))
149}
150
151fn call_of<H>(world: &mut World, name: &str) -> Result<Option<ScriptValue>, ScriptError>
154where
155 H: ScriptHost + Resource<Mutability = Mutable>,
156{
157 let Some(mut host) = world.get_resource_mut::<H>() else {
158 return Err(ScriptError::Runtime("no script is loaded".to_owned()));
159 };
160 match host.call(name, &[]) {
161 Ok(outcome) => {
162 host.push_commands(outcome.commands);
163 Ok(outcome.ret.filter(|_| outcome.found))
164 }
165 Err(failure) => {
166 host.push_commands(failure.commands);
167 Err(failure.error)
168 }
169 }
170}
171
172pub type InstallFn = fn(&mut App, ScriptProgram, bool);
176pub type ReloadFn = fn(&mut World, &str, &str, &SourceMap) -> Option<Result<(), ScriptError>>;
180pub type CheckFn = fn(&str, &ScriptCompile<'_>) -> Result<(), String>;
182pub type CompileFn = fn(&str, &ScriptCompile<'_>) -> Result<(Vec<u8>, Vec<String>), String>;
185pub type ImageExportsFn = fn(&[u8], &[ScriptFn]) -> Result<Vec<String>, String>;
188pub type MarkupBlocksFn = fn(&str) -> Result<Vec<MarkupBlock>, MarkupBlockError>;
190
191#[derive(Clone, Copy)]
197pub struct ScriptLanguage {
198 pub name: &'static str,
201 pub install: InstallFn,
203 pub reload: Option<ReloadFn>,
205 pub access: fn() -> ScriptHostAccess,
207 pub check: Option<CheckFn>,
209 pub compile: Option<CompileFn>,
211 pub image_exports: Option<ImageExportsFn>,
213 pub markup_blocks: Option<MarkupBlocksFn>,
215}
216
217impl ScriptLanguage {
218 pub fn new(name: &'static str, install: InstallFn, access: fn() -> ScriptHostAccess) -> Self {
221 Self {
222 name,
223 install,
224 reload: None,
225 access,
226 check: None,
227 compile: None,
228 image_exports: None,
229 markup_blocks: None,
230 }
231 }
232}
233
234impl std::fmt::Debug for ScriptLanguage {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 f.debug_struct("ScriptLanguage")
237 .field("name", &self.name)
238 .field("reload", &self.reload.is_some())
239 .field("check", &self.check.is_some())
240 .field("compile", &self.compile.is_some())
241 .field("image_exports", &self.image_exports.is_some())
242 .field("markup_blocks", &self.markup_blocks.is_some())
243 .finish()
244 }
245}
246
247#[derive(Resource, Default, Debug, Clone)]
249pub struct ScriptLanguages(Vec<ScriptLanguage>);
250
251impl ScriptLanguages {
252 pub fn get(&self, name: &str) -> Option<&ScriptLanguage> {
254 self.0.iter().find(|l| l.name == name)
255 }
256
257 pub fn iter(&self) -> impl Iterator<Item = &ScriptLanguage> {
259 self.0.iter()
260 }
261
262 pub fn is_empty(&self) -> bool {
264 self.0.is_empty()
265 }
266
267 pub fn add(&mut self, language: ScriptLanguage) {
271 if self.get(language.name).is_some() {
272 warn_line!(
273 "lumen-runtime: script language `{}` is registered twice; the first \
274 registration runs it",
275 language.name
276 );
277 return;
278 }
279 self.0.push(language);
280 }
281}
282
283pub trait ScriptLanguageAppExt {
285 fn add_script_language(&mut self, language: ScriptLanguage) -> &mut Self;
287}
288
289impl ScriptLanguageAppExt for App {
290 fn add_script_language(&mut self, language: ScriptLanguage) -> &mut Self {
291 self.world
292 .get_resource_or_insert_with(ScriptLanguages::default)
293 .add(language);
294 self
295 }
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct UnknownLanguage {
301 pub language: String,
303 pub registered: Vec<String>,
306}
307
308impl std::fmt::Display for UnknownLanguage {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 write!(f, "no script host for language \"{}\"", self.language)?;
311 if self.registered.is_empty() {
312 write!(f, "; no script host is installed")
313 } else {
314 write!(f, "; this build runs {}", self.registered.join(", "))
315 }
316 }
317}
318
319impl std::error::Error for UnknownLanguage {}
320
321pub fn install_program(
328 app: &mut App,
329 language: &str,
330 program: ScriptProgram,
331 multi_host: bool,
332) -> Result<ScriptHostAccess, UnknownLanguage> {
333 let languages = app.world.get_resource::<ScriptLanguages>();
334 let Some(entry) = languages.and_then(|l| l.get(language)).copied() else {
335 return Err(UnknownLanguage {
336 language: language.to_owned(),
337 registered: languages
338 .map(|l| l.iter().map(|entry| entry.name.to_owned()).collect())
339 .unwrap_or_default(),
340 });
341 };
342 (entry.install)(app, program, multi_host);
343 Ok((entry.access)())
344}
345
346pub fn line_col(src: &str, offset: usize) -> (usize, usize) {
348 let before = &src[..offset.min(src.len())];
349 let line = before.matches('\n').count() + 1;
350 let col = before.rfind('\n').map_or(before.chars().count(), |nl| {
351 before[nl + 1..].chars().count()
352 }) + 1;
353 (line, col)
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 fn install(_app: &mut App, _program: ScriptProgram, _multi: bool) {}
361
362 #[test]
363 fn the_first_registration_of_a_name_is_the_one_kept() {
364 let mut app = App::new();
365 let mut first = ScriptLanguage::new("toy", install, ScriptHostAccess::absent);
366 first.check = Some(|_, _| Ok(()));
367 app.add_script_language(first);
368 app.add_script_language(ScriptLanguage::new(
369 "toy",
370 install,
371 ScriptHostAccess::absent,
372 ));
373 let languages = app.world.resource::<ScriptLanguages>();
374 assert_eq!(languages.iter().count(), 1);
375 assert!(languages.get("toy").and_then(|l| l.check).is_some());
376 }
377
378 #[test]
379 fn a_program_for_an_unregistered_language_is_refused_by_name() {
380 let mut app = App::new();
381 let refusal = install_program(&mut app, "toy", ScriptProgram::default(), false)
382 .err()
383 .expect("nothing registered toy");
384 assert_eq!(
385 refusal.to_string(),
386 "no script host for language \"toy\"; no script host is installed"
387 );
388
389 app.add_script_language(ScriptLanguage::new(
390 "other",
391 install,
392 ScriptHostAccess::absent,
393 ));
394 let refusal = install_program(&mut app, "toy", ScriptProgram::default(), false)
395 .err()
396 .expect("still nothing registered toy");
397 assert_eq!(
398 refusal.to_string(),
399 "no script host for language \"toy\"; this build runs other"
400 );
401 }
402
403 #[test]
404 fn line_and_column_count_from_one() {
405 let src = "ab\ncd\nef";
406 assert_eq!(line_col(src, 0), (1, 1));
407 assert_eq!(line_col(src, 4), (2, 2));
408 assert_eq!(line_col(src, 99), (3, 3));
409 }
410}