Skip to main content

linkrs_query/binder/
macro_expansion.rs

1//! User-defined macro expansion at bind time.
2//!
3//! A macro call `name(args...)` in expression position is expanded before
4//! normal function binding: actual argument expressions are substituted for
5//! the macro parameters in the stored body template, and the result is bound
6//! recursively. Expansion is purely syntactic; the planner and executor never
7//! see macros.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use linkrs_core::error::{DBError, QueryError};
13use linkrs_core::metadata::MacroManager;
14use linkrs_core::types::expr::{substitute_variables, Expression, FunctionArg};
15use linkrs_core::{DBResult, DataType};
16
17use super::bind::Binder;
18use super::bound::BoundExpression;
19
20/// Maximum nested macro expansion depth (direct or indirect recursion).
21pub const MAX_MACRO_EXPANSION_DEPTH: usize = 32;
22
23impl Binder {
24    /// Attach the shared macro catalog. When set, function calls that name a
25    /// defined macro expand at bind time instead of resolving as functions.
26    pub fn with_macro_manager(mut self, manager: Arc<MacroManager>) -> Self {
27        self.macro_manager = Some(manager);
28        self
29    }
30
31    /// Try to expand `name(args)` as a user-defined macro.
32    ///
33    /// Returns `Ok(None)` when no macro is defined under `name`, so the
34    /// caller falls through to normal function binding.
35    pub(crate) fn try_expand_macro(
36        &mut self,
37        name: &str,
38        args: &[FunctionArg],
39        type_hint: Option<&DataType>,
40    ) -> DBResult<Option<BoundExpression>> {
41        let Some(ref manager) = self.macro_manager else {
42            return Ok(None);
43        };
44        let Some(def) = manager.get_macro(name) else {
45            return Ok(None);
46        };
47
48        let canonical = name.to_ascii_uppercase();
49        if self.expanding.iter().any(|n| n == &canonical) {
50            return Err(DBError::from(QueryError::invalid_query(format!(
51                "Recursive macro expansion detected for macro '{}'",
52                def.name
53            ))));
54        }
55        if self.expanding.len() >= MAX_MACRO_EXPANSION_DEPTH {
56            return Err(DBError::from(QueryError::invalid_query(format!(
57                "Macro expansion depth exceeds limit ({}) for macro '{}'",
58                MAX_MACRO_EXPANSION_DEPTH, def.name
59            ))));
60        }
61
62        // Map actual arguments onto parameters (positional first, then
63        // named). Missing arguments fall back to parameter defaults.
64        let mut mapping: HashMap<String, Expression> = HashMap::new();
65        let mut assigned = vec![false; def.params.len()];
66        let mut positional = 0usize;
67        for arg in args {
68            match arg {
69                FunctionArg::Positional(expr) => {
70                    while positional < def.params.len() && assigned[positional] {
71                        positional += 1;
72                    }
73                    if positional >= def.params.len() {
74                        return Err(DBError::from(QueryError::invalid_query(format!(
75                            "Macro '{}' takes at most {} arguments ({} given)",
76                            def.name,
77                            def.params.len(),
78                            args.len()
79                        ))));
80                    }
81                    mapping.insert(def.params[positional].name.clone(), expr.clone());
82                    assigned[positional] = true;
83                    positional += 1;
84                }
85                FunctionArg::Named {
86                    name: arg_name,
87                    value,
88                } => {
89                    let Some(index) = def
90                        .params
91                        .iter()
92                        .position(|p| p.name.eq_ignore_ascii_case(arg_name))
93                    else {
94                        return Err(DBError::from(QueryError::invalid_query(format!(
95                            "Macro '{}' has no parameter named '{}'",
96                            def.name, arg_name
97                        ))));
98                    };
99                    if assigned[index] {
100                        return Err(DBError::from(QueryError::invalid_query(format!(
101                            "Duplicate argument for macro '{}' parameter '{}'",
102                            def.name, def.params[index].name
103                        ))));
104                    }
105                    mapping.insert(def.params[index].name.clone(), value.clone());
106                    assigned[index] = true;
107                }
108            }
109        }
110        for (index, param) in def.params.iter().enumerate() {
111            if !assigned[index] {
112                match &param.default {
113                    Some(default) => {
114                        mapping.insert(param.name.clone(), default.clone());
115                    }
116                    None => {
117                        return Err(DBError::from(QueryError::invalid_query(format!(
118                            "Macro '{}' missing required argument '{}'",
119                            def.name, param.name
120                        ))));
121                    }
122                }
123            }
124        }
125
126        let expanded = substitute_variables(&def.body, &mapping);
127        self.expanding.push(canonical);
128        let bound = self.bind_inner_expr(&expanded, type_hint);
129        self.expanding.pop();
130        bound.map(Some)
131    }
132}