1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::sync::Arc;
3
4use anyhow::Result;
5use oxdock_func_macro::oxdock_func;
6use oxdock_parser::{
7 KEYWORD_INSPECT, SCRIPT_MODULE_NAME, STD_MODULE_NAME, Step, Value, base_name, qualify,
8 split_qualified,
9};
10use oxdock_process::{CommandStdin, DefaultProcessManager, ProcessManager};
11
12use super::io::StreamHandle;
13use super::state::ExecState;
14use super::steps::StepCtx;
15use super::typing::TypeDescriptor;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FuncKind {
24 Script,
25 HostCtx,
26 HostPure,
27}
28
29impl FuncKind {
30 pub fn label(&self) -> &'static str {
31 match self {
32 FuncKind::Script => "script",
33 FuncKind::HostCtx | FuncKind::HostPure => "host",
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
41pub struct FuncParam {
42 pub name: String,
43 pub param_type: Option<String>,
44}
45
46#[derive(Debug, Clone)]
52pub struct FuncMeta {
53 pub name: String,
54 pub module: String,
58 pub kind: FuncKind,
59 pub params: Option<Vec<FuncParam>>,
60 pub returns: Option<String>,
61 pub rpn: bool,
62 pub summary: &'static str,
63 pub docs: &'static str,
64}
65
66pub type PureFn = Arc<dyn Fn(Vec<Value>) -> Result<Value> + Send + Sync>;
69
70pub type NativeFn<P> = Arc<dyn Fn(&mut StepCtx<P>, Vec<Value>) -> Result<Value> + Send + Sync>;
72
73pub trait OxDockFn<P: ProcessManager> {
77 fn registration() -> HostRegistration<P>;
80}
81
82pub enum HostRegistration<P: ProcessManager> {
88 Stateful {
89 name: String,
90 meta: FuncMeta,
91 func: NativeFn<P>,
92 },
93 Pure {
94 name: String,
95 meta: FuncMeta,
96 func: PureFn,
97 },
98}
99
100impl<P: ProcessManager> Clone for HostRegistration<P> {
103 fn clone(&self) -> Self {
104 match self {
105 HostRegistration::Stateful { name, meta, func } => HostRegistration::Stateful {
106 name: name.clone(),
107 meta: meta.clone(),
108 func: Arc::clone(func),
109 },
110 HostRegistration::Pure { name, meta, func } => HostRegistration::Pure {
111 name: name.clone(),
112 meta: meta.clone(),
113 func: Arc::clone(func),
114 },
115 }
116 }
117}
118
119impl<P: ProcessManager> HostRegistration<P> {
120 pub fn meta(&self) -> &FuncMeta {
123 match self {
124 HostRegistration::Stateful { meta, .. } => meta,
125 HostRegistration::Pure { meta, .. } => meta,
126 }
127 }
128}
129
130#[derive(Debug, Clone)]
132pub(super) struct FuncDefData {
133 pub(super) params: Vec<(String, String)>,
134 pub(super) body: Vec<Step>,
135}
136
137pub(super) enum FuncBody<P: ProcessManager> {
140 Script(FuncDefData),
141 Pure(PureFn),
142 Ctx(NativeFn<P>),
143}
144
145impl<P: ProcessManager> Clone for FuncBody<P> {
147 fn clone(&self) -> Self {
148 match self {
149 FuncBody::Script(def) => FuncBody::Script(def.clone()),
150 FuncBody::Pure(func) => FuncBody::Pure(Arc::clone(func)),
151 FuncBody::Ctx(func) => FuncBody::Ctx(Arc::clone(func)),
152 }
153 }
154}
155
156pub(super) struct FuncEntry<P: ProcessManager> {
160 pub(super) meta: FuncMeta,
161 pub(super) body: FuncBody<P>,
162}
163
164impl<P: ProcessManager> Clone for FuncEntry<P> {
166 fn clone(&self) -> Self {
167 Self {
168 meta: self.meta.clone(),
169 body: self.body.clone(),
170 }
171 }
172}
173
174struct ScopeFrame<P: ProcessManager> {
177 defined: HashSet<String>,
178 shadowed: Vec<(String, FuncEntry<P>)>,
179}
180
181impl<P: ProcessManager> Clone for ScopeFrame<P> {
183 fn clone(&self) -> Self {
184 Self {
185 defined: self.defined.clone(),
186 shadowed: self.shadowed.clone(),
187 }
188 }
189}
190
191pub struct FunctionRegistry<P: ProcessManager> {
198 entries: HashMap<String, FuncEntry<P>>,
199 scopes: Vec<ScopeFrame<P>>,
200}
201
202impl<P: ProcessManager> FunctionRegistry<P> {
203 pub(super) fn with_builtins() -> Self {
204 let mut reg = Self {
205 entries: HashMap::new(),
206 scopes: vec![ScopeFrame {
207 defined: HashSet::new(),
208 shadowed: Vec::new(),
209 }],
210 };
211 for host in Self::builtin_registrations() {
215 match host {
216 HostRegistration::Stateful { name, meta, func } => {
217 reg.insert_qualified(STD_MODULE_NAME, name, meta, FuncBody::Ctx(func));
218 }
219 HostRegistration::Pure { name, meta, func } => {
220 reg.insert_qualified(STD_MODULE_NAME, name, meta, FuncBody::Pure(func));
221 }
222 }
223 }
224 reg
225 }
226
227 pub(super) fn builtin_registrations() -> Vec<HostRegistration<P>> {
232 vec![
233 Int::registration(),
234 Float::registration(),
235 Types::registration(),
236 TypeDescribe::registration(),
237 Glob::registration(),
238 LoadToml::registration(),
239 LoadJson::registration(),
240 PathType::registration(),
241 Functions::registration(),
242 Describe::registration(),
243 IsTerminal::registration(),
244 SemaphoreNew::registration(),
245 SemaphoreTryAcquire::registration(),
246 SemaphoreAvailable::registration(),
247 ]
248 }
249
250 pub(super) fn keys(&self) -> HashSet<String> {
253 self.entries.keys().cloned().collect()
254 }
255
256 pub(super) fn get(&self, name: &str) -> Option<FuncEntry<P>> {
258 self.entries.get(name).cloned()
259 }
260
261 fn insert_native(&mut self, name: String, meta: FuncMeta, body: FuncBody<P>) {
262 self.entries.insert(name, FuncEntry { meta, body });
263 }
264
265 fn insert_qualified(
269 &mut self,
270 module: &str,
271 base: String,
272 mut meta: FuncMeta,
273 body: FuncBody<P>,
274 ) {
275 meta.name = qualify(module, &base);
276 meta.module = module.to_string();
277 if self.entries.contains_key(&meta.name) {
282 panic!("duplicate function registration `{}`", meta.name);
283 }
284 self.insert_native(meta.name.clone(), meta, body);
285 }
286
287 pub(super) fn register_host(
288 &mut self,
289 module: &str,
290 name: String,
291 mut meta: FuncMeta,
292 func: NativeFn<P>,
293 ) {
294 meta.kind = FuncKind::HostCtx;
295 self.insert_qualified(module, name, meta, FuncBody::Ctx(func));
296 }
297
298 pub(super) fn register_pure_host(
299 &mut self,
300 module: &str,
301 name: String,
302 mut meta: FuncMeta,
303 func: PureFn,
304 ) {
305 meta.kind = FuncKind::HostPure;
306 self.insert_qualified(module, name, meta, FuncBody::Pure(func));
307 }
308
309 pub(super) fn define_script(
314 &mut self,
315 name: &str,
316 params: &[(String, String)],
317 body: &[Step],
318 ) -> Result<()> {
319 let qualified = qualify(SCRIPT_MODULE_NAME, name);
320 let shadowable = matches!(
321 self.entries.get(&qualified).map(|entry| &entry.body),
322 Some(FuncBody::Script(_))
323 );
324 let reserved = self
327 .entries
328 .keys()
329 .any(|key| split_qualified(key).is_some_and(|(_, base)| base == name));
330 if reserved && !shadowable {
331 anyhow::bail!("cannot shadow reserved function `{name}`");
332 }
333 if self
334 .scopes
335 .last()
336 .is_some_and(|frame| frame.defined.contains(&qualified))
337 {
338 anyhow::bail!("duplicate function `{name}` in same scope");
339 }
340 let old = self.entries.insert(
341 qualified.clone(),
342 FuncEntry {
343 meta: FuncMeta {
344 name: qualified.clone(),
345 module: SCRIPT_MODULE_NAME.to_string(),
346 kind: FuncKind::Script,
347 params: Some(
348 params
349 .iter()
350 .map(|(name, param_type)| FuncParam {
351 name: name.clone(),
352 param_type: Some(param_type.clone()),
353 })
354 .collect(),
355 ),
356 returns: None,
357 rpn: false,
358 summary: "DSL-defined function.",
359 docs: "Defined via FUNC in script.",
360 },
361 body: FuncBody::Script(FuncDefData {
362 params: params.to_vec(),
363 body: body.to_vec(),
364 }),
365 },
366 );
367 if let Some(frame) = self.scopes.last_mut() {
368 frame.defined.insert(qualified.clone());
369 if let Some(old) = old {
370 frame.shadowed.push((qualified, old));
371 }
372 }
373 Ok(())
374 }
375
376 pub(super) fn push_scope(&mut self) {
379 self.scopes.push(ScopeFrame {
380 defined: HashSet::new(),
381 shadowed: Vec::new(),
382 });
383 }
384
385 pub(super) fn pop_scope(&mut self) {
388 let Some(frame) = self.scopes.pop() else {
389 return;
390 };
391 for name in frame.defined {
392 self.entries.remove(&name);
393 }
394 for (name, old) in frame.shadowed {
395 self.entries.insert(name, old);
396 }
397 }
398
399 pub(super) fn contains_script(&self, name: &str) -> bool {
403 matches!(
404 self.entries.get(name).map(|entry| &entry.body),
405 Some(FuncBody::Script(_))
406 )
407 }
408
409 fn clone_pure_fn(&self, name: &str) -> Option<PureFn> {
412 match self.entries.get(name)?.body {
413 FuncBody::Pure(ref func) => Some(Arc::clone(func)),
414 _ => None,
415 }
416 }
417
418 fn clone_ctx_fn(&self, name: &str) -> Option<NativeFn<P>> {
421 match self.entries.get(name)?.body {
422 FuncBody::Ctx(ref func) => Some(Arc::clone(func)),
423 _ => None,
424 }
425 }
426
427 fn meta(&self, name: &str) -> Option<FuncMeta> {
428 self.entries.get(name).map(|entry| entry.meta.clone())
429 }
430
431 fn native_metas(&self) -> Vec<FuncMeta> {
432 let mut out: Vec<FuncMeta> = Vec::new();
433 for entry in self.entries.values() {
434 if !matches!(entry.body, FuncBody::Script(_)) {
435 out.push(entry.meta.clone());
436 }
437 }
438 out.sort_by(|a, b| a.name.cmp(&b.name));
439 out
440 }
441
442 fn entries_metas(&self) -> Vec<FuncMeta> {
445 let mut out: Vec<FuncMeta> = self
446 .entries
447 .values()
448 .map(|entry| entry.meta.clone())
449 .collect();
450 out.sort_by(|a, b| a.name.cmp(&b.name));
451 out
452 }
453}
454
455impl<P: ProcessManager> Clone for FunctionRegistry<P> {
456 fn clone(&self) -> Self {
457 Self {
458 entries: self.entries.clone(),
459 scopes: self.scopes.clone(),
460 }
461 }
462}
463
464pub fn builtin_function_names() -> HashSet<String> {
471 let mut names = FunctionRegistry::<DefaultProcessManager>::with_builtins().keys();
472 names.insert(KEYWORD_INSPECT.to_string());
473 names
474}
475
476pub fn builtin_function_metas() -> Vec<FuncMeta> {
480 FunctionRegistry::<DefaultProcessManager>::with_builtins().native_metas()
481}
482
483pub fn std_module_table() -> oxdock_parser::ModuleTable {
488 let functions: HashSet<String> = builtin_function_metas()
490 .into_iter()
491 .map(|meta| base_name(&meta.name).to_string())
492 .collect();
493 oxdock_parser::ModuleTable {
494 modules: HashMap::from([(
495 STD_MODULE_NAME.to_string(),
496 Some(oxdock_parser::ModuleFuncs { functions }),
497 )]),
498 }
499}
500
501#[oxdock_func(pure, returns = "INT")]
513fn int(val: Value) -> Result<Value> {
514 super::args::int_from_value(val)
515}
516
517#[oxdock_func(pure, returns = "FLOAT")]
521fn float(val: Value) -> Result<Value> {
522 super::args::float_from_value(val)
523}
524
525#[oxdock_func(rpn, returns = "LIST")]
529fn glob<P: ProcessManager>(cx: &mut StepCtx<P>, pattern: String) -> Result<Value> {
530 super::args::glob_from_value(&[Value::string(pattern)], cx)
531}
532
533#[oxdock_func(rpn, returns = "MAP")]
537fn load_toml<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
538 super::args::load_toml_from_value(&[Value::string(path)], cx)
539}
540
541#[oxdock_func(rpn, returns = "MAP")]
545fn load_json<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
546 super::args::load_json_from_value(&[Value::string(path)], cx)
547}
548
549#[oxdock_func(returns = "STRING")]
554fn path_type<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
555 super::args::path_type_from_value(&[Value::string(path)], cx)
556}
557
558#[oxdock_func(returns = "LIST")]
563fn functions<P: ProcessManager>(cx: &mut StepCtx<P>) -> Result<Value> {
564 let mut names: Vec<String> = cx
565 .state
566 .list_functions()
567 .into_iter()
568 .map(|meta| meta.name)
569 .collect();
570 names.sort();
571 names.dedup();
572 Ok(Value::list(names.into_iter().map(Value::string).collect()))
573}
574
575#[oxdock_func(returns = "MAP")]
582fn describe<P: ProcessManager>(cx: &mut StepCtx<P>, name: String) -> Result<Value> {
583 if split_qualified(&name).is_none() && name != KEYWORD_INSPECT {
584 anyhow::bail!(
585 "unknown function `{name}`: DESCRIBE requires a qualified name (e.g. `STD::{name}`)"
586 );
587 }
588 cx.state
589 .describe_function(&name)
590 .ok_or_else(|| anyhow::anyhow!("unknown function {name}"))
591}
592
593#[oxdock_func(returns = "LIST")]
599fn types<P: ProcessManager>(cx: &mut StepCtx<P>) -> Result<Value> {
600 Ok(Value::list(
601 cx.state
602 .type_names()
603 .into_iter()
604 .map(Value::string)
605 .collect(),
606 ))
607}
608
609#[oxdock_func(returns = "MAP")]
614fn type_describe<P: ProcessManager>(cx: &mut StepCtx<P>, name: String) -> Result<Value> {
615 cx.state
616 .describe_type(&name)
617 .map(|descriptor| {
618 let mut map = BTreeMap::new();
619 map.insert(
620 "name".to_string(),
621 Value::string(descriptor.name.to_string()),
622 );
623 map.insert(
624 "summary".to_string(),
625 Value::string(descriptor.summary.to_string()),
626 );
627 map.insert(
628 "docs".to_string(),
629 Value::string(descriptor.docs.to_string()),
630 );
631 Value::map(map)
632 })
633 .ok_or_else(|| anyhow::anyhow!("unknown type {name}"))
634}
635
636#[oxdock_func(returns = "BOOL")]
649fn is_terminal<P: ProcessManager>(cx: &mut StepCtx<P>, stream: String) -> Result<Value> {
650 use std::io::IsTerminal;
651 let terminal = match stream.as_str() {
652 "stdin" => {
653 if cx.stdin_pipe.is_some() {
658 false
659 } else {
660 match &cx.stdin {
661 CommandStdin::Null => false,
662 #[cfg(not(miri))]
663 CommandStdin::OsPipe(_) => false,
664 CommandStdin::Stream(_) => false,
665 CommandStdin::Inherit => std::io::stdin().is_terminal(),
666 }
667 }
668 }
669 "stdout" => {
670 if cx.out_pipe.is_some() {
671 false
673 } else if cx.state.io.stdout().is_some() {
674 false
677 } else {
678 match &cx.out {
679 None => std::io::stdout().is_terminal(),
681 #[cfg(not(miri))]
683 Some(StreamHandle::Os(_)) => false,
684 Some(StreamHandle::Stream(_)) => std::io::stdout().is_terminal(),
694 }
695 }
696 }
697 "stderr" => {
698 if cx.state.io.stderr().is_some() {
699 false
701 } else {
702 match &cx.err {
703 None => std::io::stderr().is_terminal(),
705 #[cfg(not(miri))]
707 Some(StreamHandle::Os(_)) => false,
708 Some(StreamHandle::Stream(_)) => false,
711 }
712 }
713 }
714 _ => anyhow::bail!(
715 "IS_TERMINAL expects \"stdin\", \"stdout\", or \"stderr\", got {stream:?}"
716 ),
717 };
718 Ok(Value::bool(terminal))
719}
720
721#[oxdock_func(returns = "SEMAPHORE")]
732fn semaphore_new<P: ProcessManager>(cx: &mut StepCtx<P>, max: i64) -> Result<Value> {
733 let _ = cx;
734 if max <= 0 {
735 return Err(anyhow::anyhow!(
736 "SEMAPHORE_NEW() requires a positive max, got {max}"
737 ));
738 }
739 Ok(Value::semaphore(max as usize))
740}
741
742#[oxdock_func(returns = "MAP")]
759fn semaphore_try_acquire<P: ProcessManager>(cx: &mut StepCtx<P>, sem: Value) -> Result<Value> {
760 let _ = cx;
761 let Some(sem) = sem.as_semaphore() else {
762 return Err(anyhow::anyhow!(
763 "SEMAPHORE_TRY_ACQUIRE() argument `$sem` must be a SEMAPHORE, got {}",
764 sem.type_name(),
765 ));
766 };
767 let mut map = BTreeMap::new();
768 if sem.try_acquire() {
769 map.insert("held".to_string(), Value::int(1));
770 map.insert("permit".to_string(), Value::permit(&sem));
771 } else {
772 map.insert("held".to_string(), Value::int(0));
773 }
774 Ok(Value::map(map))
775}
776
777#[oxdock_func(pure, returns = "INT")]
787fn semaphore_available(sem: Value) -> Result<Value> {
788 let Some(sem) = sem.as_semaphore() else {
789 return Err(anyhow::anyhow!(
790 "SEMAPHORE_AVAILABLE() argument `$sem` must be a SEMAPHORE, got {}",
791 sem.type_name(),
792 ));
793 };
794 Ok(Value::int(sem.available() as i64))
795}
796
797fn meta_to_value(meta: &FuncMeta) -> Value {
798 let mut map = BTreeMap::new();
799 map.insert("name".to_string(), Value::string(meta.name.clone()));
800 map.insert("module".to_string(), Value::string(meta.module.clone()));
801 map.insert(
802 "kind".to_string(),
803 Value::string(meta.kind.label().to_string()),
804 );
805 let params = match &meta.params {
806 Some(params) => Value::list(
807 params
808 .iter()
809 .map(|p| {
810 let mut entry = BTreeMap::new();
811 entry.insert("name".to_string(), Value::string(p.name.clone()));
812 entry.insert(
813 "param_type".to_string(),
814 Value::string(p.param_type.clone().unwrap_or_default()),
815 );
816 Value::map(entry)
817 })
818 .collect(),
819 ),
820 None => Value::string(String::new()),
821 };
822 map.insert("params".to_string(), params);
823 map.insert(
824 "returns".to_string(),
825 Value::string(meta.returns.clone().unwrap_or_default()),
826 );
827 map.insert("rpn".to_string(), Value::bool(meta.rpn));
828 map.insert(
829 "summary".to_string(),
830 Value::string(meta.summary.to_string()),
831 );
832 Value::map(map)
833}
834
835#[derive(Clone)]
839pub struct HostModule<P: ProcessManager> {
840 pub name: String,
841 pub funcs: Vec<HostRegistration<P>>,
842 pub types: Vec<&'static TypeDescriptor>,
843}
844
845impl<P: ProcessManager> ExecState<P> {
846 pub fn register_module(&mut self, module: HostModule<P>) {
849 for registration in module.funcs {
850 match registration {
851 HostRegistration::Stateful { name, meta, func } => {
852 self.functions.register_host(&module.name, name, meta, func);
853 }
854 HostRegistration::Pure { name, meta, func } => {
855 self.functions
856 .register_pure_host(&module.name, name, meta, func);
857 }
858 }
859 }
860 for descriptor in module.types {
861 self.register_type(descriptor);
862 }
863 }
864
865 pub fn list_functions(&self) -> Vec<FuncMeta> {
867 let mut out: Vec<FuncMeta> = self.functions.entries_metas().into_iter().collect();
868 if !out.iter().any(|m| m.name == KEYWORD_INSPECT) {
870 out.push(FuncMeta {
871 name: KEYWORD_INSPECT.to_string(),
872 module: STD_MODULE_NAME.to_string(),
873 kind: FuncKind::HostCtx,
874 params: None,
875 returns: Some("MAP".to_string()),
876 rpn: false,
877 summary: "Inspect a variable binding.",
878 docs: "INSPECT($var): dedicated AST node taking a variable, not a value.",
879 });
880 }
881 out.sort_by(|a, b| a.name.cmp(&b.name));
882 out
883 }
884
885 pub fn describe_function(&self, name: &str) -> Option<Value> {
887 if let Some(meta) = self.functions.meta(name) {
888 return Some(meta_to_value(&meta));
889 }
890 if name == KEYWORD_INSPECT {
892 return Some(meta_to_value(&FuncMeta {
893 name: KEYWORD_INSPECT.to_string(),
894 module: STD_MODULE_NAME.to_string(),
895 kind: FuncKind::HostCtx,
896 params: None,
897 returns: Some("MAP".to_string()),
898 rpn: false,
899 summary: "Inspect a variable binding.",
900 docs: "INSPECT($var): dedicated AST node taking a variable, not a value.",
901 }));
902 }
903 None
904 }
905
906 pub(super) fn clone_native_pure(&self, name: &str) -> Option<PureFn> {
907 self.functions.clone_pure_fn(name)
908 }
909
910 pub(super) fn clone_native_ctx(&self, name: &str) -> Option<NativeFn<P>> {
911 self.functions.clone_ctx_fn(name)
912 }
913
914 pub(super) fn native_meta(&self, name: &str) -> Option<FuncMeta> {
915 self.functions.meta(name)
916 }
917}