Skip to main content

lumen_script/
script_fn.rs

1//! One description of a native function every script host can bind.
2//!
3//! A [`ScriptFn`] says what a function is called, what it takes, what it
4//! returns, and which namespace it lives in. Each
5//! host implements [`ScriptHost::register_script_fn`](crate::ScriptHost::register_script_fn)
6//! once and gets every function an app, a plugin, the C ABI, or the Rust SDK
7//! describes.
8//!
9//! The registration channel is [`ScriptFnRegistry`], a resource on the main
10//! world. A [`Plugin`](lumen_core::app::Plugin) pushes into it through
11//! [`ScriptFnAppExt::add_script_fn`]; the generic script plugin drains it into
12//! the host it is about to load, then seals it. This is Lumen's counterpart of
13//! QML's `QQmlContext::setContextProperty`: one embedder-facing surface, seen
14//! by every scripting context the app runs.
15
16use std::fmt;
17use std::sync::Arc;
18
19use bevy_ecs::prelude::Resource;
20use lumen_core::app::App;
21use lumen_core::warn_line;
22use serde::{Deserialize, Serialize};
23
24use crate::{ScriptCommand, ScriptValue};
25
26/// The shape of one parameter or return value.
27///
28/// A host binds a typed parameter to its own type where it can (Rhai resolves a
29/// call by argument type) and checks the argument where it cannot (Lua binds
30/// variadically and raises on a mismatch). [`ScriptTy::Any`] accepts whatever
31/// the script passes, and [`ScriptTy::Dynamic`] says the same of a return that
32/// was meant to be open rather than left undeclared.
33///
34/// Variants are append-only; see [`SCRIPT_WIRE_VERSION`](crate::SCRIPT_WIRE_VERSION).
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
36pub enum ScriptTy {
37    /// Any value; no check. This is what a signature carries where nothing was
38    /// declared, so an untyped return is read as a forgotten declaration.
39    Any,
40    /// No value (`()` in Rhai, `nil` in Lua, `null` in candela).
41    Unit,
42    /// Boolean.
43    Bool,
44    /// Signed 64-bit integer.
45    Int,
46    /// 64-bit float.
47    Float,
48    /// String.
49    Str,
50    /// List whose elements carry the inner type.
51    Array(Box<ScriptTy>),
52    /// String-keyed map whose values carry the inner type.
53    Map(Box<ScriptTy>),
54    /// Any value, declared on purpose: the result has no shape narrower than
55    /// this one, as with a parsed JSON document. Every host treats it exactly
56    /// as [`Any`](ScriptTy::Any); the difference is that the author said so,
57    /// which is what tells a deliberate open return from a missing one.
58    Dynamic,
59    /// A named set of fields with declared defaults, taken as one parameter:
60    /// the shape a function's options have. See [`ScriptStruct`].
61    ///
62    /// A parameter type only. Each host renders it in its own terms (a struct
63    /// candela declares beside the function, a map on Rhai and Lua), and the
64    /// body always receives a [`ScriptValue::Map`] holding every field.
65    Struct(ScriptStruct),
66}
67
68/// A struct parameter: a name, and fields in declaration order, each with the
69/// value it starts with.
70///
71/// A script sets the fields it cares about and the rest take their defaults.
72/// candela writes it as a struct literal (`ns::Name { field: v,
73/// ..Default::default() }`); Rhai and Lua pass a map and leave out the keys
74/// they do not set. Either way the body reads one map with every field
75/// present, so it never handles a missing key.
76///
77/// ```
78/// use lumen_script::{ScriptStruct, ScriptTy, ScriptValue};
79///
80/// let opts = ScriptStruct::new("Options")
81///     .field_default("dir", ScriptTy::Str, "out")
82///     .field("verbose", ScriptTy::Bool);
83/// let ScriptValue::Map(full) = opts.default_value() else { unreachable!() };
84/// assert_eq!(full["dir"], ScriptValue::Str("out".into()));
85/// assert_eq!(full["verbose"], ScriptValue::Bool(false));
86/// ```
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88pub struct ScriptStruct {
89    /// The type name. candela declares it in the function's namespace, so a
90    /// script names it `ns::Name`.
91    pub name: String,
92    /// The fields, in the order they are declared.
93    pub fields: Vec<ScriptField>,
94}
95
96/// One field of a [`ScriptStruct`].
97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
98pub struct ScriptField {
99    /// The field name, the same in every host.
100    pub name: String,
101    /// The field's type.
102    pub ty: ScriptTy,
103    /// The value it starts with, or `None` for its type's empty value (see
104    /// [`ScriptTy::empty_value`]).
105    pub default: Option<ScriptValue>,
106}
107
108impl ScriptStruct {
109    /// A struct with no fields yet.
110    pub fn new(name: impl Into<String>) -> Self {
111        Self {
112            name: name.into(),
113            fields: Vec::new(),
114        }
115    }
116
117    /// Add a field that starts as its type's empty value.
118    #[must_use]
119    pub fn field(mut self, name: impl Into<String>, ty: ScriptTy) -> Self {
120        self.fields.push(ScriptField {
121            name: name.into(),
122            ty,
123            default: None,
124        });
125        self
126    }
127
128    /// Add a field that starts as `default`.
129    #[must_use]
130    pub fn field_default(
131        mut self,
132        name: impl Into<String>,
133        ty: ScriptTy,
134        default: impl Into<ScriptValue>,
135    ) -> Self {
136        self.fields.push(ScriptField {
137            name: name.into(),
138            ty,
139            default: Some(default.into()),
140        });
141        self
142    }
143
144    /// The value with every field at its default.
145    pub fn default_value(&self) -> ScriptValue {
146        ScriptValue::Map(
147            self.fields
148                .iter()
149                .map(|f| (f.name.clone(), f.start_value()))
150                .collect(),
151        )
152    }
153
154    /// `value` with every field it leaves out set to its default, nested
155    /// structs included. A value that is not a map comes back unchanged; an
156    /// empty list is read as an empty map, since an empty Lua table cannot say
157    /// which of the two it is.
158    pub fn complete(&self, value: &ScriptValue) -> ScriptValue {
159        let given = match value {
160            ScriptValue::Map(map) => map,
161            ScriptValue::Array(items) if items.is_empty() => return self.default_value(),
162            other => return other.clone(),
163        };
164        let mut full = given.clone();
165        for field in &self.fields {
166            match (full.get_mut(&field.name), &field.ty) {
167                (Some(inner), ScriptTy::Struct(nested)) => *inner = nested.complete(inner),
168                (Some(_), _) => {}
169                (None, _) => {
170                    full.insert(field.name.clone(), field.start_value());
171                }
172            }
173        }
174        ScriptValue::Map(full)
175    }
176
177    /// Why `value` is not this struct, naming the first field at fault, or
178    /// `None` when it is one. A field left out is fine; it takes its default.
179    fn mismatch(&self, value: &ScriptValue) -> Option<String> {
180        let given = match value {
181            ScriptValue::Map(map) => map,
182            ScriptValue::Array(items) if items.is_empty() => return None,
183            other => {
184                return Some(format!(
185                    "expects {}, got {}",
186                    self.name,
187                    value_ty_name(other)
188                ));
189            }
190        };
191        // Sorted, so a value with two faults always reports the same one.
192        let mut keys: Vec<&String> = given.keys().collect();
193        keys.sort();
194        for key in keys {
195            let Some(field) = self.fields.iter().find(|f| &f.name == key) else {
196                let names: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
197                return Some(format!(
198                    "has no field `{key}`; {} has {}",
199                    self.name,
200                    names.join(", ")
201                ));
202            };
203            let inner = &given[key];
204            if let ScriptTy::Struct(nested) = &field.ty {
205                if let Some(why) = nested.mismatch(inner) {
206                    return Some(format!("field `{key}` {why}"));
207                }
208            } else if !field.ty.accepts(inner) {
209                return Some(format!(
210                    "field `{key}` expects {}, got {}",
211                    field.ty,
212                    value_ty_name(inner)
213                ));
214            }
215        }
216        None
217    }
218}
219
220impl ScriptField {
221    /// The value this field starts with.
222    fn start_value(&self) -> ScriptValue {
223        self.default
224            .clone()
225            .unwrap_or_else(|| self.ty.empty_value())
226    }
227}
228
229impl ScriptTy {
230    /// Whether this type names no shape a host can bind: [`Any`](ScriptTy::Any)
231    /// because nothing was declared, [`Dynamic`](ScriptTy::Dynamic) because
232    /// what was declared has no fixed shape. Every host treats the two alike;
233    /// the builtin table's own check is what tells them apart.
234    pub fn is_dynamic(&self) -> bool {
235        matches!(self, Self::Any | Self::Dynamic)
236    }
237
238    /// Whether `value` satisfies this type. A [dynamic](Self::is_dynamic) type
239    /// accepts anything; an [`Array`](ScriptTy::Array) or
240    /// [`Map`](ScriptTy::Map) also checks its elements.
241    ///
242    /// An integer satisfies a declared float: every scripting language Lumen
243    /// hosts spells `1` and `1.0` as the same literal kind, so `seek(30)`
244    /// is the call an author writes. The reverse does not hold; a float where
245    /// an integer is declared would silently drop its fraction.
246    pub fn accepts(&self, value: &ScriptValue) -> bool {
247        if self.is_dynamic() {
248            return true;
249        }
250        match (self, value) {
251            (Self::Unit, ScriptValue::Unit) => true,
252            (Self::Bool, ScriptValue::Bool(_)) => true,
253            (Self::Int, ScriptValue::I64(_)) => true,
254            (Self::Float, ScriptValue::F64(_) | ScriptValue::I64(_)) => true,
255            (Self::Str, ScriptValue::Str(_)) => true,
256            (Self::Array(inner), ScriptValue::Array(items)) => {
257                items.iter().all(|v| inner.accepts(v))
258            }
259            (Self::Map(inner), ScriptValue::Map(entries)) => {
260                entries.values().all(|v| inner.accepts(v))
261            }
262            (Self::Struct(shape), value) => shape.mismatch(value).is_none(),
263            _ => false,
264        }
265    }
266
267    /// The value a field of this type starts with when its struct declares
268    /// none: `0`, `0.0`, `false`, `""`, an empty list or map, a nested
269    /// struct's own defaults, and unit for the open types. These are the
270    /// values candela gives an undeclared field, so every host agrees.
271    pub fn empty_value(&self) -> ScriptValue {
272        match self {
273            Self::Any | Self::Dynamic | Self::Unit => ScriptValue::Unit,
274            Self::Bool => ScriptValue::Bool(false),
275            Self::Int => ScriptValue::I64(0),
276            Self::Float => ScriptValue::F64(0.0),
277            Self::Str => ScriptValue::Str(String::new()),
278            Self::Array(_) => ScriptValue::Array(Vec::new()),
279            Self::Map(_) => ScriptValue::Map(std::collections::HashMap::new()),
280            Self::Struct(shape) => shape.default_value(),
281        }
282    }
283
284    /// The name a host puts in a type-mismatch message.
285    pub fn name(&self) -> &'static str {
286        match self {
287            Self::Any | Self::Dynamic => "any",
288            Self::Unit => "unit",
289            Self::Bool => "bool",
290            Self::Int => "int",
291            Self::Float => "float",
292            Self::Str => "string",
293            Self::Array(_) => "array",
294            Self::Map(_) => "map",
295            // Rhai and Lua pass a struct as a map, so that is what a mismatch
296            // message asks them for.
297            Self::Struct(_) => "map",
298        }
299    }
300}
301
302/// A type written the way candela writes it: `int`, `float`, `bool`,
303/// `string`, `null`, `any`, `T[]` for an array, `{string: T}` for a map, and
304/// a struct by its name. A struct name does not parse back: its fields are not
305/// in the spelling.
306///
307/// This is the spelling a declaration uses wherever a signature is written as
308/// text rather than built in Rust, so a function described in a data file
309/// reads the same as one a candela script declares. [`ScriptTy::Any`] and
310/// [`ScriptTy::Dynamic`] both write as `any`, and `any` reads back as
311/// [`ScriptTy::Dynamic`]: a type someone wrote down is a declared one.
312impl fmt::Display for ScriptTy {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            Self::Any | Self::Dynamic => f.write_str("any"),
316            Self::Unit => f.write_str("null"),
317            Self::Bool => f.write_str("bool"),
318            Self::Int => f.write_str("int"),
319            Self::Float => f.write_str("float"),
320            Self::Str => f.write_str("string"),
321            Self::Array(inner) => write!(f, "{inner}[]"),
322            Self::Map(value) => write!(f, "{{string: {value}}}"),
323            Self::Struct(shape) => f.write_str(&shape.name),
324        }
325    }
326}
327
328/// Why a type spelling is not one [`ScriptTy`] reads.
329#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct ScriptTyError(String);
331
332impl fmt::Display for ScriptTyError {
333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334        write!(
335            f,
336            "`{}` is not a type; write int, float, bool, string, null, any, T[] or \
337             {{string: T}}",
338            self.0
339        )
340    }
341}
342
343impl std::error::Error for ScriptTyError {}
344
345impl std::str::FromStr for ScriptTy {
346    type Err = ScriptTyError;
347
348    fn from_str(text: &str) -> Result<Self, Self::Err> {
349        let spelling = text.trim();
350        let refuse = || ScriptTyError(text.to_owned());
351        if let Some(inner) = spelling.strip_suffix("[]") {
352            return Ok(Self::Array(Box::new(inner.parse().map_err(|_| refuse())?)));
353        }
354        if let Some(body) = spelling
355            .strip_prefix('{')
356            .and_then(|rest| rest.strip_suffix('}'))
357        {
358            let (key, value) = body.split_once(':').ok_or_else(refuse)?;
359            if key.trim() != "string" {
360                return Err(refuse());
361            }
362            return Ok(Self::Map(Box::new(value.parse().map_err(|_| refuse())?)));
363        }
364        Ok(match spelling {
365            "int" => Self::Int,
366            "float" => Self::Float,
367            "bool" => Self::Bool,
368            "string" => Self::Str,
369            "null" => Self::Unit,
370            "any" => Self::Dynamic,
371            _ => return Err(refuse()),
372        })
373    }
374}
375
376/// The type name of a value, for the other half of a mismatch message.
377fn value_ty_name(value: &ScriptValue) -> &'static str {
378    match value {
379        ScriptValue::Unit => "unit",
380        ScriptValue::Bool(_) => "bool",
381        ScriptValue::I64(_) => "int",
382        ScriptValue::F64(_) => "float",
383        ScriptValue::Str(_) => "string",
384        ScriptValue::Array(_) => "array",
385        ScriptValue::Map(_) => "map",
386    }
387}
388
389/// One declared parameter: the name a doc or an editor shows, and its type.
390#[derive(Clone, Debug, Serialize, Deserialize)]
391pub struct ScriptParam {
392    /// Parameter name, for docs and diagnostics.
393    pub name: String,
394    /// Declared type.
395    pub ty: ScriptTy,
396}
397
398/// The declared signature of a [`ScriptFn`].
399#[derive(Clone, Debug, Serialize, Deserialize)]
400pub struct ScriptSig {
401    /// Positional parameters, in order.
402    pub params: Vec<ScriptParam>,
403    /// Return type.
404    pub ret: ScriptTy,
405    /// Accepts more arguments than [`Self::params`] declares.
406    pub variadic: bool,
407    /// Smallest accepted argument count. Parameters past it are optional and
408    /// arrive as [`ScriptValue::Unit`] when the call omits them.
409    pub min_arity: usize,
410    /// One-line description, surfaced by editor tooling.
411    pub doc: String,
412}
413
414/// How many arguments a variadic signature is bound for on a host that
415/// dispatches by arity. Rhai has no native variadics, so it takes one
416/// registration per count up to this bound.
417pub const MAX_VARIADIC_ARITY: usize = 8;
418
419impl ScriptSig {
420    /// The inclusive range of argument counts a host binds this signature for.
421    pub fn arity_range(&self) -> std::ops::RangeInclusive<usize> {
422        let max = if self.variadic {
423            self.params.len().max(MAX_VARIADIC_ARITY)
424        } else {
425            self.params.len()
426        };
427        self.min_arity.min(max)..=max
428    }
429
430    /// Whether any parameter declares a type.
431    ///
432    /// An all-[`Any`](ScriptTy::Any) signature says nothing to check, so a host
433    /// that binds variadically passes whatever the script sent straight
434    /// through; that is what `lumen_app_expose` and the SDK's `native_fn`
435    /// describe, and a call with the wrong count reaches the body rather than
436    /// failing to resolve. [`Dynamic`](ScriptTy::Dynamic) says as little.
437    pub fn is_typed(&self) -> bool {
438        self.params.iter().any(|p| !p.ty.is_dynamic())
439    }
440
441    /// `args` with each [struct](ScriptTy::Struct) argument's missing fields
442    /// set to their defaults, or `None` when the signature takes no struct and
443    /// `args` stands as it is.
444    pub fn complete_args(&self, args: &[ScriptValue]) -> Option<Vec<ScriptValue>> {
445        if !self
446            .params
447            .iter()
448            .any(|p| matches!(p.ty, ScriptTy::Struct(_)))
449        {
450            return None;
451        }
452        Some(
453            args.iter()
454                .enumerate()
455                .map(|(i, arg)| match self.params.get(i).map(|p| &p.ty) {
456                    Some(ScriptTy::Struct(shape)) => shape.complete(arg),
457                    _ => arg.clone(),
458                })
459                .collect(),
460        )
461    }
462
463    /// Check `args` against the declared parameters. Returns the message a host
464    /// raises to the script on a mismatch.
465    ///
466    /// Too few arguments is a mismatch; extra arguments are one only when the
467    /// signature is not variadic. A parameter past [`Self::min_arity`] accepts
468    /// [`ScriptValue::Unit`] whatever its declared type, so a host that pads a
469    /// short call with unit placeholders still passes.
470    pub fn check_args(&self, args: &[ScriptValue]) -> Result<(), String> {
471        if args.len() < self.min_arity {
472            return Err(format!(
473                "expected at least {} argument(s), got {}",
474                self.min_arity,
475                args.len()
476            ));
477        }
478        if !self.variadic && args.len() > self.params.len() {
479            return Err(format!(
480                "expected at most {} argument(s), got {}",
481                self.params.len(),
482                args.len()
483            ));
484        }
485        for (i, arg) in args.iter().enumerate() {
486            let Some(param) = self.params.get(i) else {
487                break;
488            };
489            let optional = i >= self.min_arity && matches!(arg, ScriptValue::Unit);
490            if !optional
491                && let ScriptTy::Struct(shape) = &param.ty
492                && let Some(why) = shape.mismatch(arg)
493            {
494                return Err(format!("argument {} (`{}`) {why}", i + 1, param.name));
495            }
496            if !optional && !param.ty.accepts(arg) {
497                return Err(format!(
498                    "argument {} (`{}`) expects {}, got {}",
499                    i + 1,
500                    param.name,
501                    param.ty.name(),
502                    value_ty_name(arg)
503                ));
504            }
505        }
506        Ok(())
507    }
508}
509
510impl Default for ScriptSig {
511    fn default() -> Self {
512        Self {
513            params: Vec::new(),
514            ret: ScriptTy::Any,
515            variadic: false,
516            min_arity: 0,
517            doc: String::new(),
518        }
519    }
520}
521
522/// Where a function lives in the script's name space.
523///
524/// Variants are append-only; see [`SCRIPT_WIRE_VERSION`](crate::SCRIPT_WIRE_VERSION).
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
526pub enum ScriptNs {
527    /// The runtime's own surface. Global on Rhai and Lua; the `lumen` host
528    /// namespace on candela.
529    Builtin,
530    /// An embedder's surface (the C ABI's `lumen_app_expose`, the Rust SDK, a
531    /// plugin). Global on Rhai and Lua; the `native` host namespace on candela.
532    Extension,
533    /// A namespace of the embedder's choosing: a static module on Rhai, a
534    /// global table on Lua, a host namespace on candela.
535    Named(String),
536}
537
538/// The call a [`ScriptFn`] body receives: its arguments, and the sink it emits
539/// commands into.
540///
541/// The command scratch is a plain `Vec`. A host moves what the body emitted
542/// into its own sink after the body returns, so a body that calls back into the
543/// host cannot deadlock against a lock the call is already holding.
544pub struct ScriptFnCx<'a> {
545    args: &'a [ScriptValue],
546    out: &'a mut Vec<ScriptCommand>,
547}
548
549impl<'a> ScriptFnCx<'a> {
550    /// Wrap the arguments of one call and the scratch its commands land in.
551    pub fn new(args: &'a [ScriptValue], out: &'a mut Vec<ScriptCommand>) -> Self {
552        Self { args, out }
553    }
554
555    /// Every argument the script passed.
556    pub fn args(&self) -> &[ScriptValue] {
557        self.args
558    }
559
560    /// Argument `i`, or [`ScriptValue::Unit`] when the call passed fewer.
561    pub fn arg(&self, i: usize) -> ScriptValue {
562        self.arg_ref(i).clone()
563    }
564
565    /// Argument `i` by reference, for a body that only reads it.
566    pub fn arg_ref(&self, i: usize) -> &ScriptValue {
567        self.args.get(i).unwrap_or(&ScriptValue::Unit)
568    }
569
570    /// Argument `i` as a string. Non-strings take their canonical rendering; a
571    /// missing argument is the empty string.
572    pub fn str_arg(&self, i: usize) -> String {
573        self.args
574            .get(i)
575            .map(ScriptValue::stringify)
576            .unwrap_or_default()
577    }
578
579    /// Argument `i` as an integer. Floats truncate, numeric strings parse, and
580    /// anything else (including a missing argument) is `0`.
581    pub fn int_arg(&self, i: usize) -> i64 {
582        match self.args.get(i) {
583            Some(ScriptValue::I64(v)) => *v,
584            Some(ScriptValue::F64(v)) => *v as i64,
585            Some(ScriptValue::Bool(b)) => i64::from(*b),
586            Some(ScriptValue::Str(s)) => s.trim().parse().unwrap_or(0),
587            _ => 0,
588        }
589    }
590
591    /// Argument `i` as a float. Integers widen, numeric strings parse, and
592    /// anything else (including a missing argument) is `0.0`.
593    pub fn float_arg(&self, i: usize) -> f64 {
594        match self.args.get(i) {
595            Some(ScriptValue::F64(v)) => *v,
596            Some(ScriptValue::I64(v)) => *v as f64,
597            Some(ScriptValue::Str(s)) => s.trim().parse().unwrap_or(0.0),
598            _ => 0.0,
599        }
600    }
601
602    /// Argument `i` as a boolean. `0` and `""` are false, other numbers and
603    /// non-empty strings other than `"false"` are true, and a missing argument
604    /// is false.
605    pub fn bool_arg(&self, i: usize) -> bool {
606        match self.args.get(i) {
607            Some(ScriptValue::Bool(b)) => *b,
608            Some(ScriptValue::I64(v)) => *v != 0,
609            Some(ScriptValue::F64(v)) => *v != 0.0,
610            Some(ScriptValue::Str(s)) => !s.is_empty() && s != "false" && s != "0",
611            _ => false,
612        }
613    }
614
615    /// Queue a command. The host forwards it to the ECS message bus on the tick
616    /// the call happened.
617    pub fn emit(&mut self, cmd: ScriptCommand) {
618        self.out.push(cmd);
619    }
620}
621
622/// The callable half of a [`ScriptFn`]. Shared so one description binds into
623/// several hosts, `Send + Sync` so it runs on any of their threads.
624///
625/// A body that fails hands back the message the script sees. Each host raises
626/// it the way its language raises: a Rhai runtime error, a Lua
627/// `error(...)`, a candela `host_fn_error` the script can catch. The
628/// message names the function, so a failure is attributable without a
629/// stack trace.
630pub type ScriptFnBody = Arc<dyn Fn(&mut ScriptFnCx<'_>) -> ScriptResult + Send + Sync>;
631
632/// What a [`ScriptFn`] body hands back: a value, or the message to raise in
633/// the script that called it.
634pub type ScriptResult = Result<ScriptValue, String>;
635
636/// One native function, in terms every script host understands.
637#[derive(Clone)]
638pub struct ScriptFn {
639    /// The name the script calls it by.
640    pub name: String,
641    /// The namespace it lives in.
642    pub ns: ScriptNs,
643    /// Its declared signature.
644    pub sig: ScriptSig,
645    /// The function body.
646    pub body: ScriptFnBody,
647}
648
649impl ScriptFn {
650    /// Start a typed description.
651    ///
652    /// ```
653    /// use lumen_script::{ScriptFn, ScriptTy, ScriptValue};
654    ///
655    /// let f = ScriptFn::new("set_pin")
656    ///     .param("pin", ScriptTy::Int)
657    ///     .ret(ScriptTy::Bool)
658    ///     .doc("Drive a GPIO pin high.")
659    ///     .build(|cx| Ok(ScriptValue::Bool(cx.int_arg(0) > 0)));
660    /// assert_eq!(f.sig.params.len(), 1);
661    /// ```
662    ///
663    /// [`ScriptFn::from_fn`] takes a plain Rust closure instead and reads the
664    /// signature off its types; the builder is for a function that also wants
665    /// a doc line, optional arguments, or the command sink.
666    // A `ScriptFn` is not complete until it has a body, so the entry point
667    // hands back the builder that collects one.
668    #[allow(clippy::new_ret_no_self)]
669    pub fn new(name: impl Into<String>) -> ScriptFnBuilder {
670        ScriptFnBuilder {
671            name: name.into(),
672            ns: ScriptNs::Extension,
673            sig: ScriptSig::default(),
674        }
675    }
676
677    /// Describe an untyped, value-returning function of `arity` arguments: the
678    /// shape the C ABI's `lumen_app_expose` and the Rust SDK's `native_fn`
679    /// produce. Lands in [`ScriptNs::Extension`].
680    pub fn value<F>(name: impl Into<String>, arity: usize, f: F) -> Self
681    where
682        F: Fn(&[ScriptValue]) -> ScriptValue + Send + Sync + 'static,
683    {
684        Self {
685            name: name.into(),
686            ns: ScriptNs::Extension,
687            sig: any_sig(arity),
688            body: Arc::new(move |cx: &mut ScriptFnCx<'_>| Ok(f(cx.args()))),
689        }
690    }
691
692    /// Describe a function from a plain Rust closure, reading its signature off
693    /// the argument and return types.
694    ///
695    /// This is the short form: no builder, no [`ScriptTy`] spelled out. Each
696    /// parameter type maps onto the declared type a host binds it as, so
697    /// `|pin: i64, high: bool|` declares `(int, bool)` and a call passing
698    /// anything else fails at the call site. A closure returning
699    /// `Result<T, String>` may fail, and the message reaches the script.
700    ///
701    /// Parameters are named `arg0`, `arg1`, ... unless
702    /// [`param_names`](Self::param_names) renames them.
703    ///
704    /// ```
705    /// use lumen_script::{ScriptFn, ScriptTy};
706    ///
707    /// let f = ScriptFn::from_fn("gpio_read", |pin: i64| -> Result<bool, String> {
708    ///     if pin < 0 {
709    ///         return Err(format!("pin {pin} is out of range"));
710    ///     }
711    ///     Ok(pin % 2 == 0)
712    /// })
713    /// .param_names(["pin"]);
714    ///
715    /// assert_eq!(f.sig.params[0].ty, ScriptTy::Int);
716    /// assert_eq!(f.sig.ret, ScriptTy::Bool);
717    /// ```
718    pub fn from_fn<Marker, F>(name: impl Into<String>, f: F) -> Self
719    where
720        F: IntoScriptFn<Marker>,
721    {
722        let (params, ret, body) = f.into_script_fn_parts();
723        let arity = params.len();
724        Self {
725            name: name.into(),
726            ns: ScriptNs::Extension,
727            sig: ScriptSig {
728                params: params
729                    .into_iter()
730                    .enumerate()
731                    .map(|(i, ty)| ScriptParam {
732                        name: format!("arg{i}"),
733                        ty,
734                    })
735                    .collect(),
736                ret,
737                variadic: false,
738                min_arity: arity,
739                doc: String::new(),
740            },
741            body,
742        }
743    }
744
745    /// Rename the parameters, in order. Names past the declared arity are
746    /// ignored, and a parameter the list does not reach keeps `argN`.
747    ///
748    /// Only docs and diagnostics read these, so a function built with
749    /// [`ScriptFn::from_fn`] works without them; they are what makes an
750    /// editor's signature hint and a type-mismatch message readable.
751    #[must_use]
752    pub fn param_names<N: Into<String>>(mut self, names: impl IntoIterator<Item = N>) -> Self {
753        for (param, name) in self.sig.params.iter_mut().zip(names) {
754            param.name = name.into();
755        }
756        self
757    }
758
759    /// Describe an untyped function of `arity` arguments whose whole effect is
760    /// the commands it emits. Returns [`ScriptValue::Unit`] to the script.
761    pub fn commands<F>(name: impl Into<String>, arity: usize, f: F) -> Self
762    where
763        F: Fn(&mut ScriptFnCx<'_>) + Send + Sync + 'static,
764    {
765        Self {
766            name: name.into(),
767            ns: ScriptNs::Extension,
768            sig: ScriptSig {
769                ret: ScriptTy::Unit,
770                ..any_sig(arity)
771            },
772            body: Arc::new(move |cx: &mut ScriptFnCx<'_>| {
773                f(cx);
774                Ok(ScriptValue::Unit)
775            }),
776        }
777    }
778
779    /// Move it into another namespace.
780    #[must_use]
781    pub fn with_ns(mut self, ns: ScriptNs) -> Self {
782        self.ns = ns;
783        self
784    }
785
786    /// Accept fewer arguments than the signature declares; the trailing
787    /// parameters become optional.
788    #[must_use]
789    pub fn with_min_arity(mut self, min_arity: usize) -> Self {
790        self.sig.min_arity = min_arity;
791        self
792    }
793
794    /// Run the body over `args`, appending whatever it emitted to `out`.
795    ///
796    /// Every host adapter goes through here, so a body observes the same
797    /// argument slice and the same command scratch whichever language called
798    /// it. `out` is the caller's buffer rather than a fresh one, so a host that
799    /// reuses [`CallScratch`] pays no allocation per call.
800    ///
801    /// A [struct](ScriptTy::Struct) argument reaches the body with every field
802    /// present: the ones the script left out take their defaults here, so a
803    /// Rhai or Lua map that sets one key reads the same as a full candela
804    /// struct.
805    ///
806    /// A body that fails still leaves what it emitted before failing: a host
807    /// forwards those commands and then raises, which is what the runtime has
808    /// always done with a partially-completed call.
809    ///
810    /// A call to a builtin whose subsystem the engine was linked without
811    /// warns once (see [`crate::unlinked`]) and then runs as it always does.
812    ///
813    /// # Errors
814    ///
815    /// Whatever the body reported, verbatim.
816    pub fn invoke_into(&self, args: &[ScriptValue], out: &mut Vec<ScriptCommand>) -> ScriptResult {
817        crate::unlinked::warn_if_unlinked(&self.name);
818        let completed = self.sig.complete_args(args);
819        let args = completed.as_deref().unwrap_or(args);
820        let mut cx = ScriptFnCx::new(args, out);
821        (self.body)(&mut cx)
822    }
823
824    /// Run the body over `args`, returning its result and whatever it emitted.
825    ///
826    /// The allocating convenience over [`Self::invoke_into`], for a caller with
827    /// no scratch of its own.
828    pub fn invoke(&self, args: &[ScriptValue]) -> (ScriptResult, Vec<ScriptCommand>) {
829        let mut out = Vec::new();
830        let ret = self.invoke_into(args, &mut out);
831        (ret, out)
832    }
833}
834
835/// The two buffers one call needs: the arguments going in, and the commands
836/// coming out.
837///
838/// A host builds the argument list on every call, and most bodies queue a
839/// command, so both were an allocation per script call. Borrowing them from
840/// [`with_call_scratch`] instead keeps the capacity between calls.
841#[derive(Default)]
842pub struct CallScratch {
843    /// The arguments, converted out of the engine's own value type.
844    pub args: Vec<ScriptValue>,
845    /// What the body emitted, to be moved into the host's sink.
846    pub commands: Vec<ScriptCommand>,
847}
848
849impl CallScratch {
850    /// Empty buffers, allocating nothing until something is pushed.
851    const fn new() -> Self {
852        Self {
853            args: Vec::new(),
854            commands: Vec::new(),
855        }
856    }
857}
858
859thread_local! {
860    /// This thread's scratch buffers, kept between calls so their capacity is
861    /// paid for once.
862    static SCRATCH: std::cell::RefCell<CallScratch> =
863        const { std::cell::RefCell::new(CallScratch::new()) };
864}
865
866/// Borrow this thread's cleared [`CallScratch`] for the length of one call.
867///
868/// The buffers keep their capacity between calls, so a host adapter that wraps
869/// its call in this stops allocating once the app is warm. A body that calls
870/// back into another function finds the shared buffers taken and gets fresh
871/// ones, so a nested call cannot disturb the one it is inside.
872pub fn with_call_scratch<R>(f: impl FnOnce(&mut CallScratch) -> R) -> R {
873    SCRATCH.with(|cell| match cell.try_borrow_mut() {
874        Ok(mut scratch) => {
875            scratch.args.clear();
876            scratch.commands.clear();
877            f(&mut scratch)
878        }
879        Err(_) => f(&mut CallScratch::new()),
880    })
881}
882
883/// A signature of `arity` untyped parameters, all required.
884fn any_sig(arity: usize) -> ScriptSig {
885    ScriptSig {
886        params: (0..arity)
887            .map(|i| ScriptParam {
888                name: format!("arg{i}"),
889                ty: ScriptTy::Any,
890            })
891            .collect(),
892        min_arity: arity,
893        ..ScriptSig::default()
894    }
895}
896
897impl fmt::Debug for ScriptFn {
898    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
899        f.debug_struct("ScriptFn")
900            .field("name", &self.name)
901            .field("ns", &self.ns)
902            .field("sig", &self.sig)
903            .finish_non_exhaustive()
904    }
905}
906
907/// Builder for a typed [`ScriptFn`]. Start it with [`ScriptFn::new`].
908pub struct ScriptFnBuilder {
909    name: String,
910    ns: ScriptNs,
911    sig: ScriptSig,
912}
913
914impl ScriptFnBuilder {
915    /// Append a required parameter. Call in argument order.
916    #[must_use]
917    pub fn param(mut self, name: impl Into<String>, ty: ScriptTy) -> Self {
918        self.sig.params.push(ScriptParam {
919            name: name.into(),
920            ty,
921        });
922        self.sig.min_arity = self.sig.params.len();
923        self
924    }
925
926    /// Set the return type. Defaults to [`ScriptTy::Any`], which reads as a
927    /// return nobody declared; a function whose result really is open declares
928    /// [`ScriptTy::Dynamic`].
929    #[must_use]
930    pub fn ret(mut self, ty: ScriptTy) -> Self {
931        self.sig.ret = ty;
932        self
933    }
934
935    /// Set the one-line description editor tooling shows.
936    #[must_use]
937    pub fn doc(mut self, doc: impl Into<String>) -> Self {
938        self.sig.doc = doc.into();
939        self
940    }
941
942    /// Choose the namespace. Defaults to [`ScriptNs::Extension`].
943    #[must_use]
944    pub fn ns(mut self, ns: ScriptNs) -> Self {
945        self.ns = ns;
946        self
947    }
948
949    /// Make the trailing parameters optional: a call may pass as few as
950    /// `min_arity` arguments, and the body reads the rest as
951    /// [`ScriptValue::Unit`].
952    #[must_use]
953    pub fn min_arity(mut self, min_arity: usize) -> Self {
954        self.sig.min_arity = min_arity;
955        self
956    }
957
958    /// Accept arguments past the declared parameters.
959    #[must_use]
960    pub fn variadic(mut self) -> Self {
961        self.sig.variadic = true;
962        self
963    }
964
965    /// Finish with the body.
966    ///
967    /// The body returns `Ok(value)`, or `Err(message)` to raise `message` in
968    /// the script that called it.
969    pub fn build<F>(self, body: F) -> ScriptFn
970    where
971        F: Fn(&mut ScriptFnCx<'_>) -> ScriptResult + Send + Sync + 'static,
972    {
973        ScriptFn {
974            name: self.name,
975            ns: self.ns,
976            sig: self.sig,
977            body: Arc::new(body),
978        }
979    }
980}
981
982/// A Rust type [`ScriptFn::from_fn`] can carry across the script boundary.
983///
984/// Implemented for the scalars every host shares (`i64`, `f64`, `bool`,
985/// `String`), for `()`, and recursively for `Vec<T>` and
986/// `HashMap<String, T>`. The conversion into Rust is the same coercion the
987/// [`ScriptFnCx`] accessors apply, so an integer arrives where a float is
988/// declared and a missing argument reads as the type's empty value.
989pub trait ScriptType: Sized {
990    /// The declared type a host binds this parameter or return slot as.
991    fn script_ty() -> ScriptTy;
992
993    /// Read one argument.
994    fn from_script_value(value: &ScriptValue) -> Self;
995
996    /// Hand one value back to the script.
997    fn into_script_value(self) -> ScriptValue;
998}
999
1000impl ScriptType for i64 {
1001    fn script_ty() -> ScriptTy {
1002        ScriptTy::Int
1003    }
1004
1005    fn from_script_value(value: &ScriptValue) -> Self {
1006        match value {
1007            ScriptValue::I64(v) => *v,
1008            ScriptValue::F64(v) => *v as Self,
1009            ScriptValue::Bool(b) => Self::from(*b),
1010            ScriptValue::Str(s) => s.trim().parse().unwrap_or(0),
1011            _ => 0,
1012        }
1013    }
1014
1015    fn into_script_value(self) -> ScriptValue {
1016        ScriptValue::I64(self)
1017    }
1018}
1019
1020impl ScriptType for f64 {
1021    fn script_ty() -> ScriptTy {
1022        ScriptTy::Float
1023    }
1024
1025    fn from_script_value(value: &ScriptValue) -> Self {
1026        match value {
1027            ScriptValue::F64(v) => *v,
1028            ScriptValue::I64(v) => *v as Self,
1029            ScriptValue::Str(s) => s.trim().parse().unwrap_or(0.0),
1030            _ => 0.0,
1031        }
1032    }
1033
1034    fn into_script_value(self) -> ScriptValue {
1035        ScriptValue::F64(self)
1036    }
1037}
1038
1039impl ScriptType for bool {
1040    fn script_ty() -> ScriptTy {
1041        ScriptTy::Bool
1042    }
1043
1044    fn from_script_value(value: &ScriptValue) -> Self {
1045        match value {
1046            ScriptValue::Bool(b) => *b,
1047            ScriptValue::I64(v) => *v != 0,
1048            ScriptValue::F64(v) => *v != 0.0,
1049            ScriptValue::Str(s) => !s.is_empty() && s != "false" && s != "0",
1050            _ => false,
1051        }
1052    }
1053
1054    fn into_script_value(self) -> ScriptValue {
1055        ScriptValue::Bool(self)
1056    }
1057}
1058
1059impl ScriptType for String {
1060    fn script_ty() -> ScriptTy {
1061        ScriptTy::Str
1062    }
1063
1064    fn from_script_value(value: &ScriptValue) -> Self {
1065        value.stringify()
1066    }
1067
1068    fn into_script_value(self) -> ScriptValue {
1069        ScriptValue::Str(self)
1070    }
1071}
1072
1073impl ScriptType for () {
1074    fn script_ty() -> ScriptTy {
1075        ScriptTy::Unit
1076    }
1077
1078    fn from_script_value(_value: &ScriptValue) -> Self {}
1079
1080    fn into_script_value(self) -> ScriptValue {
1081        ScriptValue::Unit
1082    }
1083}
1084
1085impl<T: ScriptType> ScriptType for Vec<T> {
1086    fn script_ty() -> ScriptTy {
1087        ScriptTy::Array(Box::new(T::script_ty()))
1088    }
1089
1090    fn from_script_value(value: &ScriptValue) -> Self {
1091        match value {
1092            ScriptValue::Array(items) => items.iter().map(T::from_script_value).collect(),
1093            _ => Self::new(),
1094        }
1095    }
1096
1097    fn into_script_value(self) -> ScriptValue {
1098        ScriptValue::Array(self.into_iter().map(T::into_script_value).collect())
1099    }
1100}
1101
1102impl<T: ScriptType> ScriptType for std::collections::HashMap<String, T> {
1103    fn script_ty() -> ScriptTy {
1104        ScriptTy::Map(Box::new(T::script_ty()))
1105    }
1106
1107    fn from_script_value(value: &ScriptValue) -> Self {
1108        match value {
1109            ScriptValue::Map(entries) => entries
1110                .iter()
1111                .map(|(k, v)| (k.clone(), T::from_script_value(v)))
1112                .collect(),
1113            _ => Self::new(),
1114        }
1115    }
1116
1117    fn into_script_value(self) -> ScriptValue {
1118        ScriptValue::Map(
1119            self.into_iter()
1120                .map(|(k, v)| (k, T::into_script_value(v)))
1121                .collect(),
1122        )
1123    }
1124}
1125
1126/// What a [`ScriptFn::from_fn`] closure may return: a value, or a
1127/// `Result` whose error message reaches the script.
1128///
1129/// The declared return type is `T` either way, so the same declaration binds a
1130/// closure that can fail and one that cannot.
1131pub trait ScriptRet {
1132    /// The declared return type.
1133    fn script_ty() -> ScriptTy;
1134
1135    /// The result the body hands the host.
1136    fn into_script_result(self) -> ScriptResult;
1137}
1138
1139impl<T: ScriptType> ScriptRet for T {
1140    fn script_ty() -> ScriptTy {
1141        <T as ScriptType>::script_ty()
1142    }
1143
1144    fn into_script_result(self) -> ScriptResult {
1145        Ok(self.into_script_value())
1146    }
1147}
1148
1149impl<T: ScriptType> ScriptRet for Result<T, String> {
1150    fn script_ty() -> ScriptTy {
1151        <T as ScriptType>::script_ty()
1152    }
1153
1154    fn into_script_result(self) -> ScriptResult {
1155        self.map(ScriptType::into_script_value)
1156    }
1157}
1158
1159/// Adapts a plain Rust closure into the body and signature of a [`ScriptFn`].
1160///
1161/// The `Marker` parameter disambiguates the per-arity impls, the same trick
1162/// Rhai and Bevy use so one entry point accepts closures of many shapes without
1163/// a type annotation. Arity runs to [`MAX_VARIADIC_ARITY`]; past that, describe
1164/// the function with [`ScriptFn::new`].
1165pub trait IntoScriptFn<Marker> {
1166    /// The declared parameter types, the declared return type, and the body.
1167    ///
1168    /// Driven by [`ScriptFn::from_fn`]; not meant to be called directly.
1169    fn into_script_fn_parts(self) -> (Vec<ScriptTy>, ScriptTy, ScriptFnBody);
1170}
1171
1172/// Marker for a closure that takes no arguments.
1173pub struct Arity0;
1174
1175impl<F, R> IntoScriptFn<Arity0> for F
1176where
1177    F: Fn() -> R + Send + Sync + 'static,
1178    R: ScriptRet,
1179{
1180    fn into_script_fn_parts(self) -> (Vec<ScriptTy>, ScriptTy, ScriptFnBody) {
1181        (
1182            Vec::new(),
1183            <R as ScriptRet>::script_ty(),
1184            Arc::new(move |_cx: &mut ScriptFnCx<'_>| self().into_script_result()),
1185        )
1186    }
1187}
1188
1189/// One `IntoScriptFn` impl per arity. The marker is the tuple of parameter
1190/// types, which keeps the impls disjoint from each other and from [`Arity0`].
1191macro_rules! impl_into_script_fn {
1192    ($($ty:ident $idx:tt),+) => {
1193        impl<F, R, $($ty,)+> IntoScriptFn<($($ty,)+)> for F
1194        where
1195            F: Fn($($ty,)+) -> R + Send + Sync + 'static,
1196            R: ScriptRet,
1197            $( $ty: ScriptType + 'static, )+
1198        {
1199            fn into_script_fn_parts(self) -> (Vec<ScriptTy>, ScriptTy, ScriptFnBody) {
1200                (
1201                    vec![ $( <$ty as ScriptType>::script_ty(), )+ ],
1202                    <R as ScriptRet>::script_ty(),
1203                    Arc::new(move |cx: &mut ScriptFnCx<'_>| {
1204                        self( $( <$ty as ScriptType>::from_script_value(cx.arg_ref($idx)), )+ )
1205                            .into_script_result()
1206                    }),
1207                )
1208            }
1209        }
1210    };
1211}
1212
1213impl_into_script_fn!(A0 0);
1214impl_into_script_fn!(A0 0, A1 1);
1215impl_into_script_fn!(A0 0, A1 1, A2 2);
1216impl_into_script_fn!(A0 0, A1 1, A2 2, A3 3);
1217impl_into_script_fn!(A0 0, A1 1, A2 2, A3 3, A4 4);
1218impl_into_script_fn!(A0 0, A1 1, A2 2, A3 3, A4 4, A5 5);
1219impl_into_script_fn!(A0 0, A1 1, A2 2, A3 3, A4 4, A5 5, A6 6);
1220impl_into_script_fn!(A0 0, A1 1, A2 2, A3 3, A4 4, A5 5, A6 6, A7 7);
1221
1222/// The functions a host registered, kept so it can put them back.
1223///
1224/// A host that rebuilds its engine on [`ScriptHost::reset`](crate::ScriptHost::reset)
1225/// (Lua does) would otherwise lose every embedder registration, and the
1226/// candela compiler host replays the store into the scratch engine
1227/// `compile_check` builds so a source declaring `host "native" { .. }` still
1228/// checks.
1229#[derive(Clone, Default)]
1230pub struct ScriptFnStore(Vec<ScriptFn>);
1231
1232impl ScriptFnStore {
1233    /// Record `f`, replacing any earlier function of the same namespace and
1234    /// name so a replay reproduces what the script last saw.
1235    pub fn record(&mut self, f: &ScriptFn) {
1236        match self.0.iter_mut().find(|e| e.name == f.name && e.ns == f.ns) {
1237            Some(existing) => *existing = f.clone(),
1238            None => self.0.push(f.clone()),
1239        }
1240    }
1241
1242    /// Every recorded function, in registration order.
1243    pub fn iter(&self) -> std::slice::Iter<'_, ScriptFn> {
1244        self.0.iter()
1245    }
1246
1247    /// Whether nothing has been recorded.
1248    pub fn is_empty(&self) -> bool {
1249        self.0.is_empty()
1250    }
1251}
1252
1253impl fmt::Debug for ScriptFnStore {
1254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1255        f.debug_list().entries(self.0.iter()).finish()
1256    }
1257}
1258
1259/// Source a plugin ships in a script language, compiled ahead of the app's own
1260/// program.
1261///
1262/// This is how a plugin offers sugar over the functions it registered: a struct
1263/// and an `impl` block in that language, so a script calls `Gpio::read(pin)`
1264/// rather than the free function. Only the host whose language it names sees
1265/// it.
1266#[derive(Clone, Debug, Serialize, Deserialize)]
1267pub struct ScriptPrelude {
1268    /// The language tag the source is written in, such as `"candela"`.
1269    pub lang: String,
1270    /// The namespace the functions it wraps are registered under.
1271    pub ns: String,
1272    /// The source itself.
1273    pub source: String,
1274}
1275
1276/// The app-wide registration channel every script host drains.
1277///
1278/// Order is preserved and meaningful: a later function of the same namespace
1279/// and name shadows an earlier one, because that is what each engine does when
1280/// the same name is registered twice.
1281#[derive(Resource, Default, Debug)]
1282pub struct ScriptFnRegistry {
1283    fns: Vec<ScriptFn>,
1284    preludes: Vec<ScriptPrelude>,
1285    sealed: bool,
1286}
1287
1288impl ScriptFnRegistry {
1289    /// Append a function.
1290    pub fn push(&mut self, f: ScriptFn) {
1291        self.fns.push(f);
1292    }
1293
1294    /// Append a plugin's language source.
1295    ///
1296    /// Two plugins may ship source for the same namespace, and both are kept:
1297    /// one plugin registers the functions and another wraps them, which is a
1298    /// composition the registry has no business breaking. It is worth saying
1299    /// out loud, because the two sources are compiled into the same program and
1300    /// a name written in both is a compile error the app author did not write.
1301    pub fn push_prelude(&mut self, prelude: ScriptPrelude) {
1302        if let Some(prior) = self
1303            .preludes
1304            .iter()
1305            .find(|p| p.lang == prelude.lang && p.ns == prelude.ns)
1306        {
1307            warn_line!(
1308                "add_script_prelude(`{lang}`, `{ns}`): `{ns}` already carries {prior_len} bytes of \
1309                 {lang} source from an earlier plugin, and this adds {next_len} more; both \
1310                 compile, in registration order, so a name written in both is a compile error \
1311                 pointing into the wrapper",
1312                lang = prelude.lang,
1313                ns = prelude.ns,
1314                prior_len = prior.source.len(),
1315                next_len = prelude.source.len(),
1316            );
1317        }
1318        self.preludes.push(prelude);
1319    }
1320
1321    /// Every registered function, in registration order.
1322    pub fn fns(&self) -> &[ScriptFn] {
1323        &self.fns
1324    }
1325
1326    /// Every registered language source, in registration order.
1327    pub fn preludes(&self) -> &[ScriptPrelude] {
1328        &self.preludes
1329    }
1330
1331    /// The sources `lang` is to compile ahead of the app's program, in
1332    /// registration order.
1333    pub fn preludes_for_lang(&self, lang: &str) -> Vec<&ScriptPrelude> {
1334        self.preludes.iter().filter(|p| p.lang == lang).collect()
1335    }
1336
1337    /// Close the channel. Every host has bound what it is going to bind.
1338    pub fn seal(&mut self) {
1339        self.sealed = true;
1340    }
1341
1342    /// Whether the channel is closed.
1343    pub fn is_sealed(&self) -> bool {
1344        self.sealed
1345    }
1346}
1347
1348/// Register script functions on an [`App`].
1349///
1350/// Install the plugin that calls these through `RunOptions::with_plugin`, or
1351/// through the Rust SDK's `lumenui::App::add_plugin`; both run before the
1352/// script hosts load. A registration made after that is refused: Rhai and Lua
1353/// would bind a function no already-compiled program resolves, and candela
1354/// binds its host declarations when the program compiles or the artifact loads,
1355/// so there is nothing left to bind to.
1356pub trait ScriptFnAppExt {
1357    /// Register one function.
1358    fn add_script_fn(&mut self, f: ScriptFn) -> &mut Self;
1359
1360    /// Register several functions, in order.
1361    fn add_script_fns(&mut self, fns: impl IntoIterator<Item = ScriptFn>) -> &mut Self;
1362
1363    /// Register source in `lang` that the host of that language compiles ahead
1364    /// of the app's own program, wrapping the functions registered under `ns`.
1365    fn add_script_prelude(&mut self, lang: &str, ns: &str, source: &str) -> &mut Self;
1366}
1367
1368impl ScriptFnAppExt for App {
1369    fn add_script_fn(&mut self, f: ScriptFn) -> &mut Self {
1370        let mut registry = registry_mut(self);
1371        if registry.is_sealed() {
1372            warn_line!(
1373                "add_script_fn(`{}`): the script hosts have already bound their functions; \
1374                 register it from a plugin installed through `RunOptions::with_plugin` or \
1375                 `lumenui::App::add_plugin`, which run before they load",
1376                f.name
1377            );
1378            return self;
1379        }
1380        registry.push(f);
1381        self
1382    }
1383
1384    fn add_script_fns(&mut self, fns: impl IntoIterator<Item = ScriptFn>) -> &mut Self {
1385        for f in fns {
1386            self.add_script_fn(f);
1387        }
1388        self
1389    }
1390
1391    fn add_script_prelude(&mut self, lang: &str, ns: &str, source: &str) -> &mut Self {
1392        let mut registry = registry_mut(self);
1393        if registry.is_sealed() {
1394            warn_line!(
1395                "add_script_prelude(`{lang}`, `{ns}`): the script hosts have already bound their \
1396                 functions; register it from a plugin installed through \
1397                 `RunOptions::with_plugin` or `lumenui::App::add_plugin`, which run before they \
1398                 load"
1399            );
1400            return self;
1401        }
1402        registry.push_prelude(ScriptPrelude {
1403            lang: lang.to_string(),
1404            ns: ns.to_string(),
1405            source: source.to_string(),
1406        });
1407        self
1408    }
1409}
1410
1411/// The app's registry, created on first use.
1412fn registry_mut(app: &mut App) -> bevy_ecs::change_detection::Mut<'_, ScriptFnRegistry> {
1413    if !app.world.contains_resource::<ScriptFnRegistry>() {
1414        app.world.insert_resource(ScriptFnRegistry::default());
1415    }
1416    app.world.resource_mut::<ScriptFnRegistry>()
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421    use super::*;
1422
1423    #[test]
1424    fn a_type_spelling_reads_back_as_the_type_it_writes() {
1425        let types = [
1426            ScriptTy::Int,
1427            ScriptTy::Float,
1428            ScriptTy::Bool,
1429            ScriptTy::Str,
1430            ScriptTy::Unit,
1431            ScriptTy::Dynamic,
1432            ScriptTy::Array(Box::new(ScriptTy::Str)),
1433            ScriptTy::Map(Box::new(ScriptTy::Array(Box::new(ScriptTy::Int)))),
1434        ];
1435        for ty in types {
1436            let text = ty.to_string();
1437            assert_eq!(text.parse::<ScriptTy>(), Ok(ty), "{text}");
1438        }
1439        assert_eq!(
1440            " int[] ".parse::<ScriptTy>(),
1441            Ok(ScriptTy::Array(Box::new(ScriptTy::Int)))
1442        );
1443        assert_eq!(ScriptTy::Any.to_string(), "any");
1444    }
1445
1446    #[test]
1447    fn a_spelling_that_names_no_type_is_refused() {
1448        for bad in ["", "integer", "{int: string}", "{string int}", "[]", "str"] {
1449            let err = bad.parse::<ScriptTy>().expect_err(bad);
1450            assert!(err.to_string().contains("is not a type"), "{err}");
1451        }
1452    }
1453
1454    #[test]
1455    fn a_typed_signature_rejects_the_wrong_shape_and_accepts_the_right_one() {
1456        let f = ScriptFn::new("set_pin")
1457            .param("pin", ScriptTy::Int)
1458            .param("high", ScriptTy::Bool)
1459            .ret(ScriptTy::Bool)
1460            .build(|cx| Ok(ScriptValue::Bool(cx.bool_arg(1))));
1461
1462        assert!(
1463            f.sig
1464                .check_args(&[ScriptValue::I64(3), ScriptValue::Bool(true)])
1465                .is_ok()
1466        );
1467        let err = f
1468            .sig
1469            .check_args(&[ScriptValue::Str("three".into()), ScriptValue::Bool(true)])
1470            .expect_err("a string where an int is declared is a mismatch");
1471        assert!(err.contains("pin") && err.contains("int"), "{err}");
1472        assert!(
1473            f.sig.check_args(&[ScriptValue::I64(3)]).is_err(),
1474            "both parameters are required"
1475        );
1476    }
1477
1478    #[test]
1479    fn optional_trailing_parameters_widen_the_arity_range() {
1480        let f = ScriptFn::new("page")
1481            .param("path", ScriptTy::Str)
1482            .min_arity(0)
1483            .build(|cx| Ok(ScriptValue::Str(cx.str_arg(0))));
1484
1485        assert_eq!(f.sig.arity_range(), 0..=1);
1486        assert!(f.sig.check_args(&[]).is_ok());
1487        assert!(
1488            f.sig.check_args(&[ScriptValue::Unit]).is_ok(),
1489            "a unit placeholder stands in for an omitted optional argument"
1490        );
1491    }
1492
1493    #[test]
1494    fn a_variadic_signature_binds_up_to_the_arity_bound() {
1495        let f = ScriptFn::new("log")
1496            .min_arity(0)
1497            .variadic()
1498            .build(|_cx| Ok(ScriptValue::Unit));
1499        assert_eq!(f.sig.arity_range(), 0..=MAX_VARIADIC_ARITY);
1500        let args = vec![ScriptValue::I64(1); 5];
1501        assert!(f.sig.check_args(&args).is_ok());
1502    }
1503
1504    #[test]
1505    fn a_command_body_emits_into_the_call_scratch() {
1506        let f = ScriptFn::commands("shout", 1, |cx| {
1507            let text = cx.str_arg(0).to_uppercase();
1508            cx.emit(ScriptCommand::Print(text));
1509        });
1510        let (ret, cmds) = f.invoke(&[ScriptValue::Str("hi".into())]);
1511        assert_eq!(ret, Ok(ScriptValue::Unit));
1512        assert!(matches!(&cmds[..], [ScriptCommand::Print(s)] if s == "HI"));
1513    }
1514
1515    #[test]
1516    fn the_store_replays_the_last_registration_of_a_name() {
1517        let mut store = ScriptFnStore::default();
1518        store.record(&ScriptFn::value("f", 0, |_| ScriptValue::I64(1)));
1519        store.record(&ScriptFn::value("g", 0, |_| ScriptValue::I64(2)));
1520        store.record(&ScriptFn::value("f", 0, |_| ScriptValue::I64(3)));
1521
1522        let names: Vec<&str> = store.iter().map(|f| f.name.as_str()).collect();
1523        assert_eq!(names, vec!["f", "g"], "one entry per name, in first order");
1524        let (ret, _) = store.iter().next().unwrap().invoke(&[]);
1525        assert_eq!(ret, Ok(ScriptValue::I64(3)), "the last body wins");
1526    }
1527
1528    #[test]
1529    fn a_body_that_fails_hands_back_its_message_and_what_it_emitted() {
1530        let f = ScriptFn::new("gpio_write")
1531            .param("pin", ScriptTy::Int)
1532            .ret(ScriptTy::Unit)
1533            .build(|cx| {
1534                cx.emit(ScriptCommand::Print("about to write".into()));
1535                Err(format!("pin {} is not exported", cx.int_arg(0)))
1536            });
1537        let (ret, cmds) = f.invoke(&[ScriptValue::I64(21)]);
1538        assert_eq!(ret, Err("pin 21 is not exported".to_string()));
1539        assert_eq!(cmds.len(), 1, "what ran before the failure still counts");
1540    }
1541
1542    #[test]
1543    fn a_plain_closure_declares_the_types_of_its_arguments() {
1544        let f = ScriptFn::from_fn("local_id", |source: String, suffix: String| {
1545            format!("{source}:{suffix}")
1546        })
1547        .param_names(["source", "suffix"]);
1548
1549        assert_eq!(f.sig.params.len(), 2);
1550        assert_eq!(f.sig.params[0].name, "source");
1551        assert_eq!(f.sig.params[1].ty, ScriptTy::Str);
1552        assert_eq!(f.sig.ret, ScriptTy::Str);
1553        assert_eq!(f.sig.min_arity, 2);
1554        let (ret, _) = f.invoke(&[
1555            ScriptValue::Str("card".into()),
1556            ScriptValue::Str("label".into()),
1557        ]);
1558        assert_eq!(ret, Ok(ScriptValue::Str("card:label".into())));
1559    }
1560
1561    #[test]
1562    fn a_plain_closure_may_fail_and_still_declares_the_value_type() {
1563        let f = ScriptFn::from_fn("half", |n: i64| -> Result<i64, String> {
1564            if n % 2 == 0 {
1565                Ok(n / 2)
1566            } else {
1567                Err(format!("{n} is odd"))
1568            }
1569        });
1570        assert_eq!(
1571            f.sig.ret,
1572            ScriptTy::Int,
1573            "the declared type is the `Ok` one"
1574        );
1575        assert_eq!(f.invoke(&[ScriptValue::I64(8)]).0, Ok(ScriptValue::I64(4)));
1576        assert_eq!(
1577            f.invoke(&[ScriptValue::I64(7)]).0,
1578            Err("7 is odd".to_string())
1579        );
1580    }
1581
1582    #[test]
1583    fn a_plain_closure_carries_lists_maps_and_no_arguments_at_all() {
1584        let nullary = ScriptFn::from_fn("answer", || 42i64);
1585        assert!(nullary.sig.params.is_empty());
1586        assert_eq!(nullary.invoke(&[]).0, Ok(ScriptValue::I64(42)));
1587
1588        let listy = ScriptFn::from_fn("join", |parts: Vec<String>| parts.join("-"));
1589        assert_eq!(
1590            listy.sig.params[0].ty,
1591            ScriptTy::Array(Box::new(ScriptTy::Str))
1592        );
1593        let (ret, _) = listy.invoke(&[ScriptValue::Array(vec![
1594            ScriptValue::Str("a".into()),
1595            ScriptValue::Str("b".into()),
1596        ])]);
1597        assert_eq!(ret, Ok(ScriptValue::Str("a-b".into())));
1598
1599        let mappy = ScriptFn::from_fn("sizes", || {
1600            std::collections::HashMap::from([("w".to_string(), 3i64)])
1601        });
1602        assert_eq!(mappy.sig.ret, ScriptTy::Map(Box::new(ScriptTy::Int)));
1603    }
1604
1605    #[test]
1606    fn the_widest_closure_binds_and_the_names_stop_where_the_list_does() {
1607        let f = ScriptFn::from_fn(
1608            "wide",
1609            |a: i64, b: i64, c: i64, d: i64, e: i64, g: i64, h: i64, i: i64| {
1610                a + b + c + d + e + g + h + i
1611            },
1612        )
1613        .param_names(["a", "b"]);
1614        assert_eq!(f.sig.params.len(), MAX_VARIADIC_ARITY);
1615        assert_eq!(f.sig.params[1].name, "b");
1616        assert_eq!(f.sig.params[2].name, "arg2", "unnamed keeps its position");
1617        let args: Vec<ScriptValue> = (1..=8).map(ScriptValue::I64).collect();
1618        assert_eq!(f.invoke(&args).0, Ok(ScriptValue::I64(36)));
1619    }
1620
1621    #[test]
1622    fn the_call_scratch_comes_back_cleared_and_nests() {
1623        let f = ScriptFn::commands("shout", 1, |cx| {
1624            cx.emit(ScriptCommand::Print(cx.str_arg(0)));
1625        });
1626        for _ in 0..3 {
1627            with_call_scratch(|scratch| {
1628                scratch.args.push(ScriptValue::Str("hi".into()));
1629                assert!(scratch.commands.is_empty(), "borrowed cleared");
1630                let ret = f.invoke_into(&scratch.args, &mut scratch.commands);
1631                assert_eq!(ret, Ok(ScriptValue::Unit));
1632                assert_eq!(scratch.commands.len(), 1);
1633                // A body that calls back into another function gets its own
1634                // pair, so the outer one is untouched.
1635                with_call_scratch(|inner| {
1636                    assert!(inner.args.is_empty() && inner.commands.is_empty());
1637                });
1638                assert_eq!(scratch.commands.len(), 1);
1639            });
1640        }
1641    }
1642
1643    #[test]
1644    fn registration_after_the_seal_is_refused() {
1645        let mut app = App::new();
1646        app.add_script_fn(ScriptFn::value("early", 0, |_| ScriptValue::Unit));
1647        app.world.resource_mut::<ScriptFnRegistry>().seal();
1648        app.add_script_fn(ScriptFn::value("late", 0, |_| ScriptValue::Unit));
1649
1650        let registry = app.world.resource::<ScriptFnRegistry>();
1651        let names: Vec<&str> = registry.fns().iter().map(|f| f.name.as_str()).collect();
1652        assert_eq!(names, vec!["early"]);
1653    }
1654
1655    /// A small options struct with one nested struct, for the struct tests.
1656    fn options() -> ScriptStruct {
1657        ScriptStruct::new("Options")
1658            .field_default("dir", ScriptTy::Str, "out")
1659            .field("env", ScriptTy::Map(Box::new(ScriptTy::Str)))
1660            .field(
1661                "retry",
1662                ScriptTy::Struct(ScriptStruct::new("Retry").field_default(
1663                    "times",
1664                    ScriptTy::Int,
1665                    3_i64,
1666                )),
1667            )
1668    }
1669
1670    fn map(entries: &[(&str, ScriptValue)]) -> ScriptValue {
1671        ScriptValue::Map(
1672            entries
1673                .iter()
1674                .map(|(k, v)| ((*k).to_string(), v.clone()))
1675                .collect(),
1676        )
1677    }
1678
1679    /// A field with no declared default starts empty; a nested struct starts
1680    /// at its own defaults.
1681    #[test]
1682    fn a_struct_default_fills_every_field() {
1683        assert_eq!(
1684            options().default_value(),
1685            map(&[
1686                ("dir", "out".into()),
1687                ("env", map(&[])),
1688                ("retry", map(&[("times", ScriptValue::I64(3))])),
1689            ])
1690        );
1691    }
1692
1693    /// Completing keeps what the script set and fills the rest, nested
1694    /// structs included; an empty list is read as an empty map.
1695    #[test]
1696    fn completing_a_struct_keeps_what_was_set() {
1697        let given = map(&[("dir", "build".into()), ("retry", map(&[]))]);
1698        assert_eq!(
1699            options().complete(&given),
1700            map(&[
1701                ("dir", "build".into()),
1702                ("env", map(&[])),
1703                ("retry", map(&[("times", ScriptValue::I64(3))])),
1704            ])
1705        );
1706        assert_eq!(
1707            options().complete(&ScriptValue::Array(Vec::new())),
1708            options().default_value()
1709        );
1710    }
1711
1712    /// A struct accepts a map with any subset of its fields, and refuses an
1713    /// unknown field or a wrong type by name.
1714    #[test]
1715    fn a_struct_checks_the_fields_it_is_given() {
1716        let sig = ScriptSig {
1717            params: vec![ScriptParam {
1718                name: "opts".into(),
1719                ty: ScriptTy::Struct(options()),
1720            }],
1721            min_arity: 1,
1722            ..ScriptSig::default()
1723        };
1724        assert!(sig.check_args(&[map(&[])]).is_ok());
1725        assert!(sig.check_args(&[map(&[("dir", "x".into())])]).is_ok());
1726        assert!(sig.check_args(&[ScriptValue::Array(Vec::new())]).is_ok());
1727
1728        let why = sig
1729            .check_args(&[map(&[("dri", "x".into())])])
1730            .expect_err("unknown field");
1731        assert!(why.contains("no field `dri`"), "{why}");
1732        assert!(why.contains("Options has dir, env, retry"), "{why}");
1733
1734        let why = sig
1735            .check_args(&[map(&[("dir", ScriptValue::I64(1))])])
1736            .expect_err("wrong type");
1737        assert!(why.contains("field `dir` expects string, got int"), "{why}");
1738
1739        let why = sig
1740            .check_args(&[map(&[("retry", map(&[("times", "x".into())]))])])
1741            .expect_err("nested wrong type");
1742        assert!(why.contains("field `retry` field `times`"), "{why}");
1743
1744        let why = sig.check_args(&["x".into()]).expect_err("not a map");
1745        assert!(why.contains("expects Options, got string"), "{why}");
1746        assert!(
1747            sig.check_args(&[]).is_err(),
1748            "a struct parameter is required"
1749        );
1750    }
1751
1752    /// The body receives every field, whatever the script left out.
1753    #[test]
1754    fn a_body_reads_a_complete_struct() {
1755        let f = ScriptFn::new("run")
1756            .param("opts", ScriptTy::Struct(options()))
1757            .ret(ScriptTy::Str)
1758            .build(|cx| {
1759                let ScriptValue::Map(opts) = cx.arg_ref(0) else {
1760                    return Err("not a map".into());
1761                };
1762                Ok(ScriptValue::Str(format!(
1763                    "{}:{}",
1764                    opts["dir"].stringify(),
1765                    opts["retry"].stringify()
1766                )))
1767            });
1768        let (ret, _) = f.invoke(&[map(&[])]);
1769        assert_eq!(ret, Ok(ScriptValue::Str("out:{times: 3}".into())));
1770    }
1771
1772    /// A struct writes as its name, the way a declaration names it.
1773    #[test]
1774    fn a_struct_spells_as_its_name() {
1775        assert_eq!(ScriptTy::Struct(options()).to_string(), "Options");
1776    }
1777}