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::{DefaultProcessManager, ProcessManager};
11
12use super::state::ExecState;
13use super::steps::StepCtx;
14use super::typing::TypeDescriptor;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum FuncKind {
23 Script,
24 HostCtx,
25 HostPure,
26}
27
28impl FuncKind {
29 pub fn label(&self) -> &'static str {
30 match self {
31 FuncKind::Script => "script",
32 FuncKind::HostCtx | FuncKind::HostPure => "host",
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
40pub struct FuncParam {
41 pub name: String,
42 pub param_type: Option<String>,
43}
44
45#[derive(Debug, Clone)]
51pub struct FuncMeta {
52 pub name: String,
53 pub module: String,
57 pub kind: FuncKind,
58 pub params: Option<Vec<FuncParam>>,
59 pub returns: Option<String>,
60 pub rpn: bool,
61 pub summary: &'static str,
62 pub docs: &'static str,
63}
64
65pub type PureFn = Arc<dyn Fn(Vec<Value>) -> Result<Value> + Send + Sync>;
68
69pub type NativeFn<P> = Arc<dyn Fn(&mut StepCtx<P>, Vec<Value>) -> Result<Value> + Send + Sync>;
71
72pub trait OxDockFn<P: ProcessManager> {
76 fn registration() -> HostRegistration<P>;
79}
80
81pub enum HostRegistration<P: ProcessManager> {
87 Stateful {
88 name: String,
89 meta: FuncMeta,
90 func: NativeFn<P>,
91 },
92 Pure {
93 name: String,
94 meta: FuncMeta,
95 func: PureFn,
96 },
97}
98
99impl<P: ProcessManager> Clone for HostRegistration<P> {
102 fn clone(&self) -> Self {
103 match self {
104 HostRegistration::Stateful { name, meta, func } => HostRegistration::Stateful {
105 name: name.clone(),
106 meta: meta.clone(),
107 func: Arc::clone(func),
108 },
109 HostRegistration::Pure { name, meta, func } => HostRegistration::Pure {
110 name: name.clone(),
111 meta: meta.clone(),
112 func: Arc::clone(func),
113 },
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
120pub(super) struct FuncDefData {
121 pub(super) params: Vec<(String, String)>,
122 pub(super) body: Vec<Step>,
123}
124
125pub(super) enum FuncBody<P: ProcessManager> {
128 Script(FuncDefData),
129 Pure(PureFn),
130 Ctx(NativeFn<P>),
131}
132
133impl<P: ProcessManager> Clone for FuncBody<P> {
135 fn clone(&self) -> Self {
136 match self {
137 FuncBody::Script(def) => FuncBody::Script(def.clone()),
138 FuncBody::Pure(func) => FuncBody::Pure(Arc::clone(func)),
139 FuncBody::Ctx(func) => FuncBody::Ctx(Arc::clone(func)),
140 }
141 }
142}
143
144pub(super) struct FuncEntry<P: ProcessManager> {
148 pub(super) meta: FuncMeta,
149 pub(super) body: FuncBody<P>,
150}
151
152impl<P: ProcessManager> Clone for FuncEntry<P> {
154 fn clone(&self) -> Self {
155 Self {
156 meta: self.meta.clone(),
157 body: self.body.clone(),
158 }
159 }
160}
161
162struct ScopeFrame<P: ProcessManager> {
165 defined: HashSet<String>,
166 shadowed: Vec<(String, FuncEntry<P>)>,
167}
168
169impl<P: ProcessManager> Clone for ScopeFrame<P> {
171 fn clone(&self) -> Self {
172 Self {
173 defined: self.defined.clone(),
174 shadowed: self.shadowed.clone(),
175 }
176 }
177}
178
179pub struct FunctionRegistry<P: ProcessManager> {
186 entries: HashMap<String, FuncEntry<P>>,
187 scopes: Vec<ScopeFrame<P>>,
188}
189
190impl<P: ProcessManager> FunctionRegistry<P> {
191 pub(super) fn with_builtins() -> Self {
192 let mut reg = Self {
193 entries: HashMap::new(),
194 scopes: vec![ScopeFrame {
195 defined: HashSet::new(),
196 shadowed: Vec::new(),
197 }],
198 };
199 for host in Self::builtin_registrations() {
203 match host {
204 HostRegistration::Stateful { name, meta, func } => {
205 reg.insert_qualified(STD_MODULE_NAME, name, meta, FuncBody::Ctx(func));
206 }
207 HostRegistration::Pure { name, meta, func } => {
208 reg.insert_qualified(STD_MODULE_NAME, name, meta, FuncBody::Pure(func));
209 }
210 }
211 }
212 reg
213 }
214
215 pub(super) fn builtin_registrations() -> Vec<HostRegistration<P>> {
220 vec![
221 Int::registration(),
222 Float::registration(),
223 Types::registration(),
224 TypeDescribe::registration(),
225 Glob::registration(),
226 LoadToml::registration(),
227 LoadJson::registration(),
228 PathType::registration(),
229 Functions::registration(),
230 Describe::registration(),
231 ]
232 }
233
234 pub(super) fn keys(&self) -> HashSet<String> {
237 self.entries.keys().cloned().collect()
238 }
239
240 pub(super) fn get(&self, name: &str) -> Option<FuncEntry<P>> {
242 self.entries.get(name).cloned()
243 }
244
245 fn insert_native(&mut self, name: String, meta: FuncMeta, body: FuncBody<P>) {
246 self.entries.insert(name, FuncEntry { meta, body });
247 }
248
249 fn insert_qualified(
253 &mut self,
254 module: &str,
255 base: String,
256 mut meta: FuncMeta,
257 body: FuncBody<P>,
258 ) {
259 meta.name = qualify(module, &base);
260 meta.module = module.to_string();
261 if self.entries.contains_key(&meta.name) {
266 panic!("duplicate function registration `{}`", meta.name);
267 }
268 self.insert_native(meta.name.clone(), meta, body);
269 }
270
271 pub(super) fn register_host(
272 &mut self,
273 module: &str,
274 name: String,
275 mut meta: FuncMeta,
276 func: NativeFn<P>,
277 ) {
278 meta.kind = FuncKind::HostCtx;
279 self.insert_qualified(module, name, meta, FuncBody::Ctx(func));
280 }
281
282 pub(super) fn register_pure_host(
283 &mut self,
284 module: &str,
285 name: String,
286 mut meta: FuncMeta,
287 func: PureFn,
288 ) {
289 meta.kind = FuncKind::HostPure;
290 self.insert_qualified(module, name, meta, FuncBody::Pure(func));
291 }
292
293 pub(super) fn define_script(
298 &mut self,
299 name: &str,
300 params: &[(String, String)],
301 body: &[Step],
302 ) -> Result<()> {
303 let qualified = qualify(SCRIPT_MODULE_NAME, name);
304 let shadowable = matches!(
305 self.entries.get(&qualified).map(|entry| &entry.body),
306 Some(FuncBody::Script(_))
307 );
308 let reserved = self
311 .entries
312 .keys()
313 .any(|key| split_qualified(key).is_some_and(|(_, base)| base == name));
314 if reserved && !shadowable {
315 anyhow::bail!("cannot shadow reserved function `{name}`");
316 }
317 if self
318 .scopes
319 .last()
320 .is_some_and(|frame| frame.defined.contains(&qualified))
321 {
322 anyhow::bail!("duplicate function `{name}` in same scope");
323 }
324 let old = self.entries.insert(
325 qualified.clone(),
326 FuncEntry {
327 meta: FuncMeta {
328 name: qualified.clone(),
329 module: SCRIPT_MODULE_NAME.to_string(),
330 kind: FuncKind::Script,
331 params: Some(
332 params
333 .iter()
334 .map(|(name, param_type)| FuncParam {
335 name: name.clone(),
336 param_type: Some(param_type.clone()),
337 })
338 .collect(),
339 ),
340 returns: None,
341 rpn: false,
342 summary: "DSL-defined function.",
343 docs: "Defined via FUNC in script.",
344 },
345 body: FuncBody::Script(FuncDefData {
346 params: params.to_vec(),
347 body: body.to_vec(),
348 }),
349 },
350 );
351 if let Some(frame) = self.scopes.last_mut() {
352 frame.defined.insert(qualified.clone());
353 if let Some(old) = old {
354 frame.shadowed.push((qualified, old));
355 }
356 }
357 Ok(())
358 }
359
360 pub(super) fn push_scope(&mut self) {
363 self.scopes.push(ScopeFrame {
364 defined: HashSet::new(),
365 shadowed: Vec::new(),
366 });
367 }
368
369 pub(super) fn pop_scope(&mut self) {
372 let Some(frame) = self.scopes.pop() else {
373 return;
374 };
375 for name in frame.defined {
376 self.entries.remove(&name);
377 }
378 for (name, old) in frame.shadowed {
379 self.entries.insert(name, old);
380 }
381 }
382
383 pub(super) fn contains_script(&self, name: &str) -> bool {
387 matches!(
388 self.entries.get(name).map(|entry| &entry.body),
389 Some(FuncBody::Script(_))
390 )
391 }
392
393 fn clone_pure_fn(&self, name: &str) -> Option<PureFn> {
396 match self.entries.get(name)?.body {
397 FuncBody::Pure(ref func) => Some(Arc::clone(func)),
398 _ => None,
399 }
400 }
401
402 fn clone_ctx_fn(&self, name: &str) -> Option<NativeFn<P>> {
405 match self.entries.get(name)?.body {
406 FuncBody::Ctx(ref func) => Some(Arc::clone(func)),
407 _ => None,
408 }
409 }
410
411 fn meta(&self, name: &str) -> Option<FuncMeta> {
412 self.entries.get(name).map(|entry| entry.meta.clone())
413 }
414
415 fn native_metas(&self) -> Vec<FuncMeta> {
416 let mut out: Vec<FuncMeta> = Vec::new();
417 for entry in self.entries.values() {
418 if !matches!(entry.body, FuncBody::Script(_)) {
419 out.push(entry.meta.clone());
420 }
421 }
422 out.sort_by(|a, b| a.name.cmp(&b.name));
423 out
424 }
425
426 fn entries_metas(&self) -> Vec<FuncMeta> {
429 let mut out: Vec<FuncMeta> = self
430 .entries
431 .values()
432 .map(|entry| entry.meta.clone())
433 .collect();
434 out.sort_by(|a, b| a.name.cmp(&b.name));
435 out
436 }
437}
438
439impl<P: ProcessManager> Clone for FunctionRegistry<P> {
440 fn clone(&self) -> Self {
441 Self {
442 entries: self.entries.clone(),
443 scopes: self.scopes.clone(),
444 }
445 }
446}
447
448pub fn builtin_function_names() -> HashSet<String> {
455 let mut names = FunctionRegistry::<DefaultProcessManager>::with_builtins().keys();
456 names.insert(KEYWORD_INSPECT.to_string());
457 names
458}
459
460pub fn builtin_function_metas() -> Vec<FuncMeta> {
464 FunctionRegistry::<DefaultProcessManager>::with_builtins().native_metas()
465}
466
467pub fn std_module_table() -> oxdock_parser::ModuleTable {
472 let functions: HashSet<String> = builtin_function_metas()
474 .into_iter()
475 .map(|meta| base_name(&meta.name).to_string())
476 .collect();
477 oxdock_parser::ModuleTable {
478 modules: HashMap::from([(
479 STD_MODULE_NAME.to_string(),
480 Some(oxdock_parser::ModuleFuncs { functions }),
481 )]),
482 }
483}
484
485#[oxdock_func(pure, returns = "INT")]
497fn int(val: Value) -> Result<Value> {
498 super::args::int_from_value(val)
499}
500
501#[oxdock_func(pure, returns = "FLOAT")]
505fn float(val: Value) -> Result<Value> {
506 super::args::float_from_value(val)
507}
508
509#[oxdock_func(rpn, returns = "LIST")]
513fn glob<P: ProcessManager>(cx: &mut StepCtx<P>, pattern: String) -> Result<Value> {
514 super::args::glob_from_value(&[Value::string(pattern)], cx)
515}
516
517#[oxdock_func(rpn, returns = "MAP")]
521fn load_toml<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
522 super::args::load_toml_from_value(&[Value::string(path)], cx)
523}
524
525#[oxdock_func(rpn, returns = "MAP")]
529fn load_json<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
530 super::args::load_json_from_value(&[Value::string(path)], cx)
531}
532
533#[oxdock_func(returns = "STRING")]
538fn path_type<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
539 super::args::path_type_from_value(&[Value::string(path)], cx)
540}
541
542#[oxdock_func(returns = "LIST")]
547fn functions<P: ProcessManager>(cx: &mut StepCtx<P>) -> Result<Value> {
548 let mut names: Vec<String> = cx
549 .state
550 .list_functions()
551 .into_iter()
552 .map(|meta| meta.name)
553 .collect();
554 names.sort();
555 names.dedup();
556 Ok(Value::list(names.into_iter().map(Value::string).collect()))
557}
558
559#[oxdock_func(returns = "MAP")]
566fn describe<P: ProcessManager>(cx: &mut StepCtx<P>, name: String) -> Result<Value> {
567 if split_qualified(&name).is_none() && name != KEYWORD_INSPECT {
568 anyhow::bail!(
569 "unknown function `{name}`: DESCRIBE requires a qualified name (e.g. `STD::{name}`)"
570 );
571 }
572 cx.state
573 .describe_function(&name)
574 .ok_or_else(|| anyhow::anyhow!("unknown function {name}"))
575}
576
577#[oxdock_func(returns = "LIST")]
583fn types<P: ProcessManager>(cx: &mut StepCtx<P>) -> Result<Value> {
584 Ok(Value::list(
585 cx.state
586 .type_names()
587 .into_iter()
588 .map(Value::string)
589 .collect(),
590 ))
591}
592
593#[oxdock_func(returns = "MAP")]
598fn type_describe<P: ProcessManager>(cx: &mut StepCtx<P>, name: String) -> Result<Value> {
599 cx.state
600 .describe_type(&name)
601 .map(|descriptor| {
602 let mut map = BTreeMap::new();
603 map.insert(
604 "name".to_string(),
605 Value::string(descriptor.name.to_string()),
606 );
607 map.insert(
608 "summary".to_string(),
609 Value::string(descriptor.summary.to_string()),
610 );
611 map.insert(
612 "docs".to_string(),
613 Value::string(descriptor.docs.to_string()),
614 );
615 Value::map(map)
616 })
617 .ok_or_else(|| anyhow::anyhow!("unknown type {name}"))
618}
619
620fn meta_to_value(meta: &FuncMeta) -> Value {
621 let mut map = BTreeMap::new();
622 map.insert("name".to_string(), Value::string(meta.name.clone()));
623 map.insert("module".to_string(), Value::string(meta.module.clone()));
624 map.insert(
625 "kind".to_string(),
626 Value::string(meta.kind.label().to_string()),
627 );
628 let params = match &meta.params {
629 Some(params) => Value::list(
630 params
631 .iter()
632 .map(|p| {
633 let mut entry = BTreeMap::new();
634 entry.insert("name".to_string(), Value::string(p.name.clone()));
635 entry.insert(
636 "param_type".to_string(),
637 Value::string(p.param_type.clone().unwrap_or_default()),
638 );
639 Value::map(entry)
640 })
641 .collect(),
642 ),
643 None => Value::string(String::new()),
644 };
645 map.insert("params".to_string(), params);
646 map.insert(
647 "returns".to_string(),
648 Value::string(meta.returns.clone().unwrap_or_default()),
649 );
650 map.insert("rpn".to_string(), Value::bool(meta.rpn));
651 map.insert(
652 "summary".to_string(),
653 Value::string(meta.summary.to_string()),
654 );
655 Value::map(map)
656}
657
658#[derive(Clone)]
662pub struct HostModule<P: ProcessManager> {
663 pub name: String,
664 pub funcs: Vec<HostRegistration<P>>,
665 pub types: Vec<&'static TypeDescriptor>,
666}
667
668impl<P: ProcessManager> ExecState<P> {
669 pub fn register_module(&mut self, module: HostModule<P>) {
672 for registration in module.funcs {
673 match registration {
674 HostRegistration::Stateful { name, meta, func } => {
675 self.functions.register_host(&module.name, name, meta, func);
676 }
677 HostRegistration::Pure { name, meta, func } => {
678 self.functions
679 .register_pure_host(&module.name, name, meta, func);
680 }
681 }
682 }
683 for descriptor in module.types {
684 self.register_type(descriptor);
685 }
686 }
687
688 pub fn list_functions(&self) -> Vec<FuncMeta> {
690 let mut out: Vec<FuncMeta> = self.functions.entries_metas().into_iter().collect();
691 if !out.iter().any(|m| m.name == KEYWORD_INSPECT) {
693 out.push(FuncMeta {
694 name: KEYWORD_INSPECT.to_string(),
695 module: STD_MODULE_NAME.to_string(),
696 kind: FuncKind::HostCtx,
697 params: None,
698 returns: Some("MAP".to_string()),
699 rpn: false,
700 summary: "Inspect a variable binding.",
701 docs: "INSPECT($var): dedicated AST node taking a variable, not a value.",
702 });
703 }
704 out.sort_by(|a, b| a.name.cmp(&b.name));
705 out
706 }
707
708 pub fn describe_function(&self, name: &str) -> Option<Value> {
710 if let Some(meta) = self.functions.meta(name) {
711 return Some(meta_to_value(&meta));
712 }
713 if name == KEYWORD_INSPECT {
715 return Some(meta_to_value(&FuncMeta {
716 name: KEYWORD_INSPECT.to_string(),
717 module: STD_MODULE_NAME.to_string(),
718 kind: FuncKind::HostCtx,
719 params: None,
720 returns: Some("MAP".to_string()),
721 rpn: false,
722 summary: "Inspect a variable binding.",
723 docs: "INSPECT($var): dedicated AST node taking a variable, not a value.",
724 }));
725 }
726 None
727 }
728
729 pub(super) fn clone_native_pure(&self, name: &str) -> Option<PureFn> {
730 self.functions.clone_pure_fn(name)
731 }
732
733 pub(super) fn clone_native_ctx(&self, name: &str) -> Option<NativeFn<P>> {
734 self.functions.clone_ctx_fn(name)
735 }
736
737 pub(super) fn native_meta(&self, name: &str) -> Option<FuncMeta> {
738 self.functions.meta(name)
739 }
740}