1use 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
36pub enum ScriptTy {
37 Any,
40 Unit,
42 Bool,
44 Int,
46 Float,
48 Str,
50 Array(Box<ScriptTy>),
52 Map(Box<ScriptTy>),
54 Dynamic,
59 Struct(ScriptStruct),
66}
67
68#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88pub struct ScriptStruct {
89 pub name: String,
92 pub fields: Vec<ScriptField>,
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
98pub struct ScriptField {
99 pub name: String,
101 pub ty: ScriptTy,
103 pub default: Option<ScriptValue>,
106}
107
108impl ScriptStruct {
109 pub fn new(name: impl Into<String>) -> Self {
111 Self {
112 name: name.into(),
113 fields: Vec::new(),
114 }
115 }
116
117 #[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 #[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 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 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 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 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 fn start_value(&self) -> ScriptValue {
223 self.default
224 .clone()
225 .unwrap_or_else(|| self.ty.empty_value())
226 }
227}
228
229impl ScriptTy {
230 pub fn is_dynamic(&self) -> bool {
235 matches!(self, Self::Any | Self::Dynamic)
236 }
237
238 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 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 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 Self::Struct(_) => "map",
298 }
299 }
300}
301
302impl 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#[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
376fn 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#[derive(Clone, Debug, Serialize, Deserialize)]
391pub struct ScriptParam {
392 pub name: String,
394 pub ty: ScriptTy,
396}
397
398#[derive(Clone, Debug, Serialize, Deserialize)]
400pub struct ScriptSig {
401 pub params: Vec<ScriptParam>,
403 pub ret: ScriptTy,
405 pub variadic: bool,
407 pub min_arity: usize,
410 pub doc: String,
412}
413
414pub const MAX_VARIADIC_ARITY: usize = 8;
418
419impl ScriptSig {
420 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 pub fn is_typed(&self) -> bool {
438 self.params.iter().any(|p| !p.ty.is_dynamic())
439 }
440
441 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 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) = ¶m.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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
526pub enum ScriptNs {
527 Builtin,
530 Extension,
533 Named(String),
536}
537
538pub struct ScriptFnCx<'a> {
545 args: &'a [ScriptValue],
546 out: &'a mut Vec<ScriptCommand>,
547}
548
549impl<'a> ScriptFnCx<'a> {
550 pub fn new(args: &'a [ScriptValue], out: &'a mut Vec<ScriptCommand>) -> Self {
552 Self { args, out }
553 }
554
555 pub fn args(&self) -> &[ScriptValue] {
557 self.args
558 }
559
560 pub fn arg(&self, i: usize) -> ScriptValue {
562 self.arg_ref(i).clone()
563 }
564
565 pub fn arg_ref(&self, i: usize) -> &ScriptValue {
567 self.args.get(i).unwrap_or(&ScriptValue::Unit)
568 }
569
570 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 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 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 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 pub fn emit(&mut self, cmd: ScriptCommand) {
618 self.out.push(cmd);
619 }
620}
621
622pub type ScriptFnBody = Arc<dyn Fn(&mut ScriptFnCx<'_>) -> ScriptResult + Send + Sync>;
631
632pub type ScriptResult = Result<ScriptValue, String>;
635
636#[derive(Clone)]
638pub struct ScriptFn {
639 pub name: String,
641 pub ns: ScriptNs,
643 pub sig: ScriptSig,
645 pub body: ScriptFnBody,
647}
648
649impl ScriptFn {
650 #[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 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 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 #[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 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 #[must_use]
781 pub fn with_ns(mut self, ns: ScriptNs) -> Self {
782 self.ns = ns;
783 self
784 }
785
786 #[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 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 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#[derive(Default)]
842pub struct CallScratch {
843 pub args: Vec<ScriptValue>,
845 pub commands: Vec<ScriptCommand>,
847}
848
849impl CallScratch {
850 const fn new() -> Self {
852 Self {
853 args: Vec::new(),
854 commands: Vec::new(),
855 }
856 }
857}
858
859thread_local! {
860 static SCRATCH: std::cell::RefCell<CallScratch> =
863 const { std::cell::RefCell::new(CallScratch::new()) };
864}
865
866pub 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
883fn 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
907pub struct ScriptFnBuilder {
909 name: String,
910 ns: ScriptNs,
911 sig: ScriptSig,
912}
913
914impl ScriptFnBuilder {
915 #[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 #[must_use]
930 pub fn ret(mut self, ty: ScriptTy) -> Self {
931 self.sig.ret = ty;
932 self
933 }
934
935 #[must_use]
937 pub fn doc(mut self, doc: impl Into<String>) -> Self {
938 self.sig.doc = doc.into();
939 self
940 }
941
942 #[must_use]
944 pub fn ns(mut self, ns: ScriptNs) -> Self {
945 self.ns = ns;
946 self
947 }
948
949 #[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 #[must_use]
960 pub fn variadic(mut self) -> Self {
961 self.sig.variadic = true;
962 self
963 }
964
965 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
982pub trait ScriptType: Sized {
990 fn script_ty() -> ScriptTy;
992
993 fn from_script_value(value: &ScriptValue) -> Self;
995
996 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
1126pub trait ScriptRet {
1132 fn script_ty() -> ScriptTy;
1134
1135 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
1159pub trait IntoScriptFn<Marker> {
1166 fn into_script_fn_parts(self) -> (Vec<ScriptTy>, ScriptTy, ScriptFnBody);
1170}
1171
1172pub 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
1189macro_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#[derive(Clone, Default)]
1230pub struct ScriptFnStore(Vec<ScriptFn>);
1231
1232impl ScriptFnStore {
1233 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 pub fn iter(&self) -> std::slice::Iter<'_, ScriptFn> {
1244 self.0.iter()
1245 }
1246
1247 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#[derive(Clone, Debug, Serialize, Deserialize)]
1267pub struct ScriptPrelude {
1268 pub lang: String,
1270 pub ns: String,
1272 pub source: String,
1274}
1275
1276#[derive(Resource, Default, Debug)]
1282pub struct ScriptFnRegistry {
1283 fns: Vec<ScriptFn>,
1284 preludes: Vec<ScriptPrelude>,
1285 sealed: bool,
1286}
1287
1288impl ScriptFnRegistry {
1289 pub fn push(&mut self, f: ScriptFn) {
1291 self.fns.push(f);
1292 }
1293
1294 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 pub fn fns(&self) -> &[ScriptFn] {
1323 &self.fns
1324 }
1325
1326 pub fn preludes(&self) -> &[ScriptPrelude] {
1328 &self.preludes
1329 }
1330
1331 pub fn preludes_for_lang(&self, lang: &str) -> Vec<&ScriptPrelude> {
1334 self.preludes.iter().filter(|p| p.lang == lang).collect()
1335 }
1336
1337 pub fn seal(&mut self) {
1339 self.sealed = true;
1340 }
1341
1342 pub fn is_sealed(&self) -> bool {
1344 self.sealed
1345 }
1346}
1347
1348pub trait ScriptFnAppExt {
1357 fn add_script_fn(&mut self, f: ScriptFn) -> &mut Self;
1359
1360 fn add_script_fns(&mut self, fns: impl IntoIterator<Item = ScriptFn>) -> &mut Self;
1362
1363 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
1411fn 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 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 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 #[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 #[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 #[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 #[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 #[test]
1774 fn a_struct_spells_as_its_name() {
1775 assert_eq!(ScriptTy::Struct(options()).to_string(), "Options");
1776 }
1777}