use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
use anyhow::Result;
use oxdock_func_macro::oxdock_func;
use oxdock_parser::{
KEYWORD_INSPECT, SCRIPT_MODULE_NAME, STD_MODULE_NAME, Step, Value, base_name, qualify,
split_qualified,
};
use oxdock_process::{DefaultProcessManager, ProcessManager};
use super::state::ExecState;
use super::steps::StepCtx;
use super::typing::TypeDescriptor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuncKind {
Script,
HostCtx,
HostPure,
}
impl FuncKind {
pub fn label(&self) -> &'static str {
match self {
FuncKind::Script => "script",
FuncKind::HostCtx | FuncKind::HostPure => "host",
}
}
}
#[derive(Debug, Clone)]
pub struct FuncParam {
pub name: String,
pub param_type: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FuncMeta {
pub name: String,
pub module: String,
pub kind: FuncKind,
pub params: Option<Vec<FuncParam>>,
pub returns: Option<String>,
pub rpn: bool,
pub summary: &'static str,
pub docs: &'static str,
}
pub type PureFn = Arc<dyn Fn(Vec<Value>) -> Result<Value> + Send + Sync>;
pub type NativeFn<P> = Arc<dyn Fn(&mut StepCtx<P>, Vec<Value>) -> Result<Value> + Send + Sync>;
pub trait OxDockFn<P: ProcessManager> {
fn registration() -> HostRegistration<P>;
}
pub enum HostRegistration<P: ProcessManager> {
Stateful {
name: String,
meta: FuncMeta,
func: NativeFn<P>,
},
Pure {
name: String,
meta: FuncMeta,
func: PureFn,
},
}
impl<P: ProcessManager> Clone for HostRegistration<P> {
fn clone(&self) -> Self {
match self {
HostRegistration::Stateful { name, meta, func } => HostRegistration::Stateful {
name: name.clone(),
meta: meta.clone(),
func: Arc::clone(func),
},
HostRegistration::Pure { name, meta, func } => HostRegistration::Pure {
name: name.clone(),
meta: meta.clone(),
func: Arc::clone(func),
},
}
}
}
#[derive(Debug, Clone)]
pub(super) struct FuncDefData {
pub(super) params: Vec<(String, String)>,
pub(super) body: Vec<Step>,
}
pub(super) enum FuncBody<P: ProcessManager> {
Script(FuncDefData),
Pure(PureFn),
Ctx(NativeFn<P>),
}
impl<P: ProcessManager> Clone for FuncBody<P> {
fn clone(&self) -> Self {
match self {
FuncBody::Script(def) => FuncBody::Script(def.clone()),
FuncBody::Pure(func) => FuncBody::Pure(Arc::clone(func)),
FuncBody::Ctx(func) => FuncBody::Ctx(Arc::clone(func)),
}
}
}
pub(super) struct FuncEntry<P: ProcessManager> {
pub(super) meta: FuncMeta,
pub(super) body: FuncBody<P>,
}
impl<P: ProcessManager> Clone for FuncEntry<P> {
fn clone(&self) -> Self {
Self {
meta: self.meta.clone(),
body: self.body.clone(),
}
}
}
struct ScopeFrame<P: ProcessManager> {
defined: HashSet<String>,
shadowed: Vec<(String, FuncEntry<P>)>,
}
impl<P: ProcessManager> Clone for ScopeFrame<P> {
fn clone(&self) -> Self {
Self {
defined: self.defined.clone(),
shadowed: self.shadowed.clone(),
}
}
}
pub struct FunctionRegistry<P: ProcessManager> {
entries: HashMap<String, FuncEntry<P>>,
scopes: Vec<ScopeFrame<P>>,
}
impl<P: ProcessManager> FunctionRegistry<P> {
pub(super) fn with_builtins() -> Self {
let mut reg = Self {
entries: HashMap::new(),
scopes: vec![ScopeFrame {
defined: HashSet::new(),
shadowed: Vec::new(),
}],
};
for host in Self::builtin_registrations() {
match host {
HostRegistration::Stateful { name, meta, func } => {
reg.insert_qualified(STD_MODULE_NAME, name, meta, FuncBody::Ctx(func));
}
HostRegistration::Pure { name, meta, func } => {
reg.insert_qualified(STD_MODULE_NAME, name, meta, FuncBody::Pure(func));
}
}
}
reg
}
pub(super) fn builtin_registrations() -> Vec<HostRegistration<P>> {
vec![
Int::registration(),
Float::registration(),
Types::registration(),
TypeDescribe::registration(),
Glob::registration(),
LoadToml::registration(),
LoadJson::registration(),
PathType::registration(),
Functions::registration(),
Describe::registration(),
]
}
pub(super) fn keys(&self) -> HashSet<String> {
self.entries.keys().cloned().collect()
}
pub(super) fn get(&self, name: &str) -> Option<FuncEntry<P>> {
self.entries.get(name).cloned()
}
fn insert_native(&mut self, name: String, meta: FuncMeta, body: FuncBody<P>) {
self.entries.insert(name, FuncEntry { meta, body });
}
fn insert_qualified(
&mut self,
module: &str,
base: String,
mut meta: FuncMeta,
body: FuncBody<P>,
) {
meta.name = qualify(module, &base);
meta.module = module.to_string();
if self.entries.contains_key(&meta.name) {
panic!("duplicate function registration `{}`", meta.name);
}
self.insert_native(meta.name.clone(), meta, body);
}
pub(super) fn register_host(
&mut self,
module: &str,
name: String,
mut meta: FuncMeta,
func: NativeFn<P>,
) {
meta.kind = FuncKind::HostCtx;
self.insert_qualified(module, name, meta, FuncBody::Ctx(func));
}
pub(super) fn register_pure_host(
&mut self,
module: &str,
name: String,
mut meta: FuncMeta,
func: PureFn,
) {
meta.kind = FuncKind::HostPure;
self.insert_qualified(module, name, meta, FuncBody::Pure(func));
}
pub(super) fn define_script(
&mut self,
name: &str,
params: &[(String, String)],
body: &[Step],
) -> Result<()> {
let qualified = qualify(SCRIPT_MODULE_NAME, name);
let shadowable = matches!(
self.entries.get(&qualified).map(|entry| &entry.body),
Some(FuncBody::Script(_))
);
let reserved = self
.entries
.keys()
.any(|key| split_qualified(key).is_some_and(|(_, base)| base == name));
if reserved && !shadowable {
anyhow::bail!("cannot shadow reserved function `{name}`");
}
if self
.scopes
.last()
.is_some_and(|frame| frame.defined.contains(&qualified))
{
anyhow::bail!("duplicate function `{name}` in same scope");
}
let old = self.entries.insert(
qualified.clone(),
FuncEntry {
meta: FuncMeta {
name: qualified.clone(),
module: SCRIPT_MODULE_NAME.to_string(),
kind: FuncKind::Script,
params: Some(
params
.iter()
.map(|(name, param_type)| FuncParam {
name: name.clone(),
param_type: Some(param_type.clone()),
})
.collect(),
),
returns: None,
rpn: false,
summary: "DSL-defined function.",
docs: "Defined via FUNC in script.",
},
body: FuncBody::Script(FuncDefData {
params: params.to_vec(),
body: body.to_vec(),
}),
},
);
if let Some(frame) = self.scopes.last_mut() {
frame.defined.insert(qualified.clone());
if let Some(old) = old {
frame.shadowed.push((qualified, old));
}
}
Ok(())
}
pub(super) fn push_scope(&mut self) {
self.scopes.push(ScopeFrame {
defined: HashSet::new(),
shadowed: Vec::new(),
});
}
pub(super) fn pop_scope(&mut self) {
let Some(frame) = self.scopes.pop() else {
return;
};
for name in frame.defined {
self.entries.remove(&name);
}
for (name, old) in frame.shadowed {
self.entries.insert(name, old);
}
}
pub(super) fn contains_script(&self, name: &str) -> bool {
matches!(
self.entries.get(name).map(|entry| &entry.body),
Some(FuncBody::Script(_))
)
}
fn clone_pure_fn(&self, name: &str) -> Option<PureFn> {
match self.entries.get(name)?.body {
FuncBody::Pure(ref func) => Some(Arc::clone(func)),
_ => None,
}
}
fn clone_ctx_fn(&self, name: &str) -> Option<NativeFn<P>> {
match self.entries.get(name)?.body {
FuncBody::Ctx(ref func) => Some(Arc::clone(func)),
_ => None,
}
}
fn meta(&self, name: &str) -> Option<FuncMeta> {
self.entries.get(name).map(|entry| entry.meta.clone())
}
fn native_metas(&self) -> Vec<FuncMeta> {
let mut out: Vec<FuncMeta> = Vec::new();
for entry in self.entries.values() {
if !matches!(entry.body, FuncBody::Script(_)) {
out.push(entry.meta.clone());
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
fn entries_metas(&self) -> Vec<FuncMeta> {
let mut out: Vec<FuncMeta> = self
.entries
.values()
.map(|entry| entry.meta.clone())
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
}
impl<P: ProcessManager> Clone for FunctionRegistry<P> {
fn clone(&self) -> Self {
Self {
entries: self.entries.clone(),
scopes: self.scopes.clone(),
}
}
}
pub fn builtin_function_names() -> HashSet<String> {
let mut names = FunctionRegistry::<DefaultProcessManager>::with_builtins().keys();
names.insert(KEYWORD_INSPECT.to_string());
names
}
pub fn builtin_function_metas() -> Vec<FuncMeta> {
FunctionRegistry::<DefaultProcessManager>::with_builtins().native_metas()
}
pub fn std_module_table() -> oxdock_parser::ModuleTable {
let functions: HashSet<String> = builtin_function_metas()
.into_iter()
.map(|meta| base_name(&meta.name).to_string())
.collect();
oxdock_parser::ModuleTable {
modules: HashMap::from([(
STD_MODULE_NAME.to_string(),
Some(oxdock_parser::ModuleFuncs { functions }),
)]),
}
}
#[oxdock_func(pure, returns = "INT")]
fn int(val: Value) -> Result<Value> {
super::args::int_from_value(val)
}
#[oxdock_func(pure, returns = "FLOAT")]
fn float(val: Value) -> Result<Value> {
super::args::float_from_value(val)
}
#[oxdock_func(rpn, returns = "LIST")]
fn glob<P: ProcessManager>(cx: &mut StepCtx<P>, pattern: String) -> Result<Value> {
super::args::glob_from_value(&[Value::string(pattern)], cx)
}
#[oxdock_func(rpn, returns = "MAP")]
fn load_toml<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
super::args::load_toml_from_value(&[Value::string(path)], cx)
}
#[oxdock_func(rpn, returns = "MAP")]
fn load_json<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
super::args::load_json_from_value(&[Value::string(path)], cx)
}
#[oxdock_func(returns = "STRING")]
fn path_type<P: ProcessManager>(cx: &mut StepCtx<P>, path: String) -> Result<Value> {
super::args::path_type_from_value(&[Value::string(path)], cx)
}
#[oxdock_func(returns = "LIST")]
fn functions<P: ProcessManager>(cx: &mut StepCtx<P>) -> Result<Value> {
let mut names: Vec<String> = cx
.state
.list_functions()
.into_iter()
.map(|meta| meta.name)
.collect();
names.sort();
names.dedup();
Ok(Value::list(names.into_iter().map(Value::string).collect()))
}
#[oxdock_func(returns = "MAP")]
fn describe<P: ProcessManager>(cx: &mut StepCtx<P>, name: String) -> Result<Value> {
if split_qualified(&name).is_none() && name != KEYWORD_INSPECT {
anyhow::bail!(
"unknown function `{name}`: DESCRIBE requires a qualified name (e.g. `STD::{name}`)"
);
}
cx.state
.describe_function(&name)
.ok_or_else(|| anyhow::anyhow!("unknown function {name}"))
}
#[oxdock_func(returns = "LIST")]
fn types<P: ProcessManager>(cx: &mut StepCtx<P>) -> Result<Value> {
Ok(Value::list(
cx.state
.type_names()
.into_iter()
.map(Value::string)
.collect(),
))
}
#[oxdock_func(returns = "MAP")]
fn type_describe<P: ProcessManager>(cx: &mut StepCtx<P>, name: String) -> Result<Value> {
cx.state
.describe_type(&name)
.map(|descriptor| {
let mut map = BTreeMap::new();
map.insert(
"name".to_string(),
Value::string(descriptor.name.to_string()),
);
map.insert(
"summary".to_string(),
Value::string(descriptor.summary.to_string()),
);
map.insert(
"docs".to_string(),
Value::string(descriptor.docs.to_string()),
);
Value::map(map)
})
.ok_or_else(|| anyhow::anyhow!("unknown type {name}"))
}
fn meta_to_value(meta: &FuncMeta) -> Value {
let mut map = BTreeMap::new();
map.insert("name".to_string(), Value::string(meta.name.clone()));
map.insert("module".to_string(), Value::string(meta.module.clone()));
map.insert(
"kind".to_string(),
Value::string(meta.kind.label().to_string()),
);
let params = match &meta.params {
Some(params) => Value::list(
params
.iter()
.map(|p| {
let mut entry = BTreeMap::new();
entry.insert("name".to_string(), Value::string(p.name.clone()));
entry.insert(
"param_type".to_string(),
Value::string(p.param_type.clone().unwrap_or_default()),
);
Value::map(entry)
})
.collect(),
),
None => Value::string(String::new()),
};
map.insert("params".to_string(), params);
map.insert(
"returns".to_string(),
Value::string(meta.returns.clone().unwrap_or_default()),
);
map.insert("rpn".to_string(), Value::bool(meta.rpn));
map.insert(
"summary".to_string(),
Value::string(meta.summary.to_string()),
);
Value::map(map)
}
#[derive(Clone)]
pub struct HostModule<P: ProcessManager> {
pub name: String,
pub funcs: Vec<HostRegistration<P>>,
pub types: Vec<&'static TypeDescriptor>,
}
impl<P: ProcessManager> ExecState<P> {
pub fn register_module(&mut self, module: HostModule<P>) {
for registration in module.funcs {
match registration {
HostRegistration::Stateful { name, meta, func } => {
self.functions.register_host(&module.name, name, meta, func);
}
HostRegistration::Pure { name, meta, func } => {
self.functions
.register_pure_host(&module.name, name, meta, func);
}
}
}
for descriptor in module.types {
self.register_type(descriptor);
}
}
pub fn list_functions(&self) -> Vec<FuncMeta> {
let mut out: Vec<FuncMeta> = self.functions.entries_metas().into_iter().collect();
if !out.iter().any(|m| m.name == KEYWORD_INSPECT) {
out.push(FuncMeta {
name: KEYWORD_INSPECT.to_string(),
module: STD_MODULE_NAME.to_string(),
kind: FuncKind::HostCtx,
params: None,
returns: Some("MAP".to_string()),
rpn: false,
summary: "Inspect a variable binding.",
docs: "INSPECT($var): dedicated AST node taking a variable, not a value.",
});
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub fn describe_function(&self, name: &str) -> Option<Value> {
if let Some(meta) = self.functions.meta(name) {
return Some(meta_to_value(&meta));
}
if name == KEYWORD_INSPECT {
return Some(meta_to_value(&FuncMeta {
name: KEYWORD_INSPECT.to_string(),
module: STD_MODULE_NAME.to_string(),
kind: FuncKind::HostCtx,
params: None,
returns: Some("MAP".to_string()),
rpn: false,
summary: "Inspect a variable binding.",
docs: "INSPECT($var): dedicated AST node taking a variable, not a value.",
}));
}
None
}
pub(super) fn clone_native_pure(&self, name: &str) -> Option<PureFn> {
self.functions.clone_pure_fn(name)
}
pub(super) fn clone_native_ctx(&self, name: &str) -> Option<NativeFn<P>> {
self.functions.clone_ctx_fn(name)
}
pub(super) fn native_meta(&self, name: &str) -> Option<FuncMeta> {
self.functions.meta(name)
}
}