Skip to main content

oxdock_core/exec/
native.rs

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/// Origin of a callable in the unified function registry. `Script` is an
17/// interpreted `FUNC` body; `HostCtx` and `HostPure` are compiled Rust
18/// functions (the `#[oxdock_func]` host export macro). Builtins and
19/// runtime-registered hosts share the host kinds; only `DESCRIBE` output
20/// shows the label, and both host kinds keep rendering as `host`.
21#[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/// One declared parameter of a registered function. `param_type` is `None`
38/// for unconstrained `Value` parameters (the macro accepts any value).
39#[derive(Debug, Clone)]
40pub struct FuncParam {
41    pub name: String,
42    pub param_type: Option<String>,
43}
44
45/// Introspectable metadata for one function. Single source for
46/// `DESCRIBE(name)` output and the static function reference. `rpn` names
47/// whether the function also runs on the compiled math path (`true` for
48/// pure functions and opted-in stateful ones); everything runs on the AST
49/// path.
50#[derive(Debug, Clone)]
51pub struct FuncMeta {
52    pub name: String,
53    /// Owning module (`STD` for builtins, `SCRIPT` for DSL definitions,
54    /// the host module name otherwise). `name` is always the qualified
55    /// `MODULE::BASE` form, so listings and `DESCRIBE` never lose origin.
56    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
65/// Pure scalar function: no filesystem, no scope, no process access.
66/// Usable from both AST evaluation and compiled RPN math.
67pub type PureFn = Arc<dyn Fn(Vec<Value>) -> Result<Value> + Send + Sync>;
68
69/// Stateful/IO function with full step context (fs, cwd, envs, vars, pipes).
70pub type NativeFn<P> = Arc<dyn Fn(&mut StepCtx<P>, Vec<Value>) -> Result<Value> + Send + Sync>;
71
72/// Export hook for a DSL function, implemented by `#[oxdock_func]` on a
73/// registration marker. The engine calls `registration()` and never names
74/// a generated symbol.
75pub trait OxDockFn<P: ProcessManager> {
76    /// The registry entry deriving from the Rust signature plus doc
77    /// comments: name, metadata, and entry point, composed.
78    fn registration() -> HostRegistration<P>;
79}
80
81/// One host-registered function, grouped into a [`HostModule`] and passed
82/// to `Engine::register_module`. Build the entry with the `#[oxdock_func]`-
83/// generated registration marker, or by hand. `Pure` entries run
84/// on both the AST and the compiled RPN math paths; `Stateful` entries run
85/// on the AST path with full step context.
86pub 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
99// Manual `Clone` (a derive would demand `P: Clone`): entries share their
100// function pointers through the `Arc`s and deep-copy the metadata.
101impl<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/// One user-defined function body (`FUNC NAME($p: TYPE, ...) { ... }`).
119#[derive(Debug, Clone)]
120pub(super) struct FuncDefData {
121    pub(super) params: Vec<(String, String)>,
122    pub(super) body: Vec<Step>,
123}
124
125/// Executable body behind one registry entry: an interpreted script, a
126/// pure scalar function, or a stateful function with step context.
127pub(super) enum FuncBody<P: ProcessManager> {
128    Script(FuncDefData),
129    Pure(PureFn),
130    Ctx(NativeFn<P>),
131}
132
133// Manual `Clone` (a derive would demand `P: Clone`).
134impl<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
144/// One entry in the unified function registry: introspectable metadata
145/// (mandatory for every entry, backing the pre-evaluation arity gate)
146/// plus the executable body.
147pub(super) struct FuncEntry<P: ProcessManager> {
148    pub(super) meta: FuncMeta,
149    pub(super) body: FuncBody<P>,
150}
151
152// Manual `Clone` (a derive would demand `P: Clone`).
153impl<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
162/// Names defined in one lexical scope frame: `defined` rejects duplicates
163/// in the same scope, `shadowed` restores outer definitions on exit.
164struct ScopeFrame<P: ProcessManager> {
165    defined: HashSet<String>,
166    shadowed: Vec<(String, FuncEntry<P>)>,
167}
168
169// Manual `Clone` (a derive would demand `P: Clone`).
170impl<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
179/// The single function registry: DSL `FUNC` definitions, builtins, and
180/// host extensions share one lookup table, one metadata path, and one
181/// dispatch order. Script entries scope lexically (defined names revert on
182/// scope exit, shadowing an outer definition restores it); native entries
183/// persist for the run. Shared across `fork()` via clone; the entry maps
184/// clone while scope frames stay per state.
185pub 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        // Every builtin registers through the same `HostRegistration`
200        // entries hosts use: no separate authoring path for engine natives.
201        // Builtins land in the `STD` module, exactly like a host module.
202        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    /// All builtins as host-style registrations, built from the same
216    /// `#[oxdock_func]`-generated markers hosts use. `with_builtins`
217    /// consumes this list, so builtins and hosts share one registration
218    /// pathway instead of two authoring models.
219    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    /// Every name the registry answers to. Backs parse-time shadow
235    /// validation through `builtin_function_names`.
236    pub(super) fn keys(&self) -> HashSet<String> {
237        self.entries.keys().cloned().collect()
238    }
239
240    /// Single lookup for every callable: script, pure, or stateful.
241    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    /// Insert under `MODULE::BASE`, stamping provenance on the metadata.
250    /// The single choke point for every registry entry: builtins pass
251    /// `STD`, hosts pass their module, scripts pass `SCRIPT`.
252    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        // Last-write-wins would silently reroute calls, so a repeated
262        // qualified name is a programmer error, never a shadow: panic like
263        // conflicting type registrations do. `SCRIPT` definitions bypass
264        // this path (`define_script` owns their scoped shadowing).
265        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    /// Define a DSL `FUNC`: names colliding with any known base name cannot
294    /// shadow, same-scope duplicates cannot redefine, and nested shadowing
295    /// of an outer script definition restores on scope exit. Stored as
296    /// `SCRIPT::NAME`, exactly like every other qualified entry.
297    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        // Reserved spans every module: compare base names so the message
309        // keeps naming the bare script identifier.
310        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    /// Open a lexical scope frame for script definitions. Native entries
361    /// persist; only script names track here.
362    pub(super) fn push_scope(&mut self) {
363        self.scopes.push(ScopeFrame {
364            defined: HashSet::new(),
365            shadowed: Vec::new(),
366        });
367    }
368
369    /// Close a lexical scope frame: names defined inside revert, and any
370    /// outer definition they shadowed is restored.
371    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    /// True when `name` is an interpreted script definition (needing call
384    /// scoping and depth budgeting through `call_func_value` rather than
385    /// inline evaluation).
386    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    /// Clone the pure fn for `name` (ending the registry borrow) so callers
394    /// can invoke it without holding `&self` across a `&mut StepCtx` use.
395    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    /// Clone the ctx fn for `name` (ending the registry borrow) so callers
403    /// can invoke it with `&mut StepCtx` without double-borrowing state.
404    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    /// Every entry's metadata, scripts included, sorted by name. Backs
427    /// runtime `FUNCTIONS()` listings.
428    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
448/// Names of all compiled-in builtins plus `INSPECT` (a dedicated AST/RPN
449/// node, not a registry entry). Read straight off a stock registry, so the
450/// `#[oxdock_func]` annotations stay the single source of truth: adding a
451/// builtin extends this set with no parallel list to update. Seeds
452/// parse-time shadow validation, so the parser crate keeps zero
453/// compile-time knowledge of builtin names.
454pub 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
460/// Metadata of every builtin function, sorted by name, for static
461/// rendering (docs-gen). Same single source as `builtin_function_names`:
462/// the `#[oxdock_func]` annotations, never a parallel list.
463pub fn builtin_function_metas() -> Vec<FuncMeta> {
464    FunctionRegistry::<DefaultProcessManager>::with_builtins().native_metas()
465}
466
467/// Stock `STD` module table derived from the `#[oxdock_func]` builtins:
468/// the single source of truth for builtin membership and RPN eligibility.
469/// Seeds parse-time module resolution, so the parser crate keeps zero
470/// compile-time knowledge of builtin names.
471pub fn std_module_table() -> oxdock_parser::ModuleTable {
472    // Registry names are qualified (`STD::GLOB`); the table holds bases.
473    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// Builtins below use the `#[oxdock_func]` host export macro, the exact same authoring
486// model as host-registered functions: metadata, arity checks, and argument
487// unpacking derive from the signature plus doc comments, so adding a native
488// means writing one small typed function plus one line each in
489// `builtin_registrations` (consumed by `with_builtins`) and the
490// `FUNCTIONS`/`DESCRIBE`/`TYPES` surface, which all read the same symbols.
491
492/// Convert a value to INT.
493///
494/// Trims ASCII whitespace and parses i64. Passes Int through; Float only
495/// when integral and finite.
496#[oxdock_func(pure, returns = "INT")]
497fn int(val: Value) -> Result<Value> {
498    super::args::int_from_value(val)
499}
500
501/// Convert a value to FLOAT.
502///
503/// Parses f64 (accepts int strings), bails on non-finite or non-numeric.
504#[oxdock_func(pure, returns = "FLOAT")]
505fn float(val: Value) -> Result<Value> {
506    super::args::float_from_value(val)
507}
508
509/// List workspace paths matching a glob pattern.
510///
511/// Sorted, root-relative LIST; empty on no match or `..` escape.
512#[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/// Load and parse a TOML file.
518///
519/// Reads a workspace file and parses TOML into a DSL value.
520#[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/// Load and parse a JSON file.
526///
527/// Reads a workspace file and parses JSON into a DSL value.
528#[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/// Describe a filesystem entry.
534///
535/// Reports file, dir, symlink (no-follow), or absent. AST-only by design;
536/// there is no RPN arm for filesystem IO.
537#[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/// List all visible function names.
543///
544/// Sorted LIST of qualified `MODULE::NAME` entries: DSL-defined plus native
545/// plus host-registered names.
546#[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/// Describe one function by qualified name.
560///
561/// Returns a MAP with name, module, kind, params, returns, and summary.
562/// Bare names fail closed: `DESCRIBE` requires the qualified form (except
563/// `INSPECT`, which is syntax rather than a registry entry). Errors on
564/// unknown function.
565#[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/// List all known type names.
578///
579/// Sorted LIST of startup plus host-registered type descriptors. Reads the
580/// run's name directory, so it runs on the AST path like the other
581/// introspection functions.
582#[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/// Describe one type by name.
594///
595/// Returns a MAP with name, summary, and docs. Errors on unknown type.
596/// Reads the run's name directory, so it runs on the AST path.
597#[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/// One host library: functions and types registered under a single module
659/// name. `Engine::register_module` stages these; runs expose them as
660/// `MODULE::NAME` calls with `MODULE` provenance on every entry.
661#[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    /// Register one [`HostModule`]: every function becomes callable as
670    /// `MODULE::NAME`, every type joins the run's name directory.
671    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    /// All visible functions: natives plus hosts plus current DSL definitions.
689    pub fn list_functions(&self) -> Vec<FuncMeta> {
690        let mut out: Vec<FuncMeta> = self.functions.entries_metas().into_iter().collect();
691        // TODO: Make a "virtual function" and don't hardcode
692        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    /// Describe one function by name, or `None` when unknown.
709    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        // TODO: Make a "virtual function" and don't hardcode
714        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}