Skip to main content

relon_eval_api/
native_fn.rs

1//! Native function interface exposed to host code.
2//!
3//! Hosts implement [`RelonFunction`] and register the resulting function under
4//! a path name via `Context::register_fn`. The evaluator passes a
5//! [`NativeArgs`] bundle that pre-splits positional and named arguments so
6//! implementations can validate either view without re-parsing.
7//!
8//! [`NativeFn`] is a re-export alias of `dyn RelonFunction` so call sites
9//! that want a shorter spelling can write `NativeFn` instead of the longer
10//! trait-object form. The two names refer to the same trait.
11
12use crate::error::RuntimeError;
13use crate::value::Value;
14use relon_parser::TokenRange;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18/// A single evaluated argument from a Relon call site, preserving its name if
19/// it was passed as `name=value`.
20#[derive(Debug, Clone)]
21pub struct EvaluatedArg {
22    pub name: Option<String>,
23    pub value: Value,
24}
25
26impl EvaluatedArg {
27    /// Construct an unnamed (positional) argument. Saves the
28    /// `EvaluatedArg { name: None, value: ... }` boilerplate at call sites
29    /// that synthesize implicit-self / single-arg invocations.
30    pub fn positional(value: Value) -> Self {
31        Self { name: None, value }
32    }
33}
34
35/// A handle to the evaluator's internal execution capabilities, allowing native
36/// functions to call back into Relon logic (closures).
37///
38/// Lives in `relon-eval-api` so that any backend implementing
39/// [`crate::Evaluator`] can mint a `NativeFnCaps` of its own for the native
40/// fns it dispatches. The default impls keep host-supplied `RelonFunction`s
41/// usable in lightweight test contexts where no backend is attached.
42pub trait NativeFnCaps: Send + Sync {
43    fn call_relon(
44        &self,
45        func: &Value,
46        args: Vec<Value>,
47        range: TokenRange,
48    ) -> Result<Value, RuntimeError>;
49
50    /// Expose `Capabilities::max_value_elements` to native functions
51    /// so collection-building intrinsics (`range`, future bulk
52    /// constructors) can pre-flight oversized requests before
53    /// allocating. Returning `None` means the host imposes no cap on
54    /// `List` / `Tuple` / `Dict` element counts.
55    ///
56    /// The evaluator still runs a post-call `check_value_size` on
57    /// every `List` / `Tuple` / `Dict` produced by a native fn (catch-all in
58    /// `call_function` / `try_call_native_method`), so an intrinsic
59    /// that ignores this hint is still bounded — but allocating
60    /// `Vec::with_capacity(end - start)` first would OOM the host
61    /// before the post-call check fires. Intrinsics that build a
62    /// collection whose size is known up-front from their arguments
63    /// should consult this and reject early.
64    fn max_value_elements(&self) -> Option<usize> {
65        None
66    }
67
68    /// Mint a fresh `Iter` cursor id under the originating Context.
69    /// Used by `List.iter()` / `String.iter()` / `Dict.iter()` (and
70    /// any future user-side `Iterable` constructor that wants to
71    /// participate in `Iter.next()` cursor tracking) to stamp the
72    /// `_id` field of the resulting `Iter`-branded dict. Returns
73    /// `0` from the default impl so a non-Context-backed `caps`
74    /// (e.g. in unit tests) still produces a sane id; production
75    /// evaluation overrides this on the per-Context impl.
76    fn next_iter_id(&self) -> u64 {
77        0
78    }
79
80    /// Atomic read-check-increment of the cursor associated with
81    /// `iter_id`. Returns `Some(old_cursor)` when the cursor was
82    /// strictly less than `len` (and the cursor is post-incremented
83    /// in the same critical section), or `None` when the cursor has
84    /// reached `len` **or** the id is unknown to this Context.
85    ///
86    /// The "unknown id ⇒ `None`" branch is the cross-Context
87    /// isolation policy: an `Iter` value built in Context A and then
88    /// handed to Context B looks exhausted from B's perspective.
89    /// Implementations should preserve this — silently auto-inserting
90    /// a fresh cursor for an unknown id would re-introduce ambient
91    /// state across Context boundaries.
92    fn iter_cursor_fetch_and_inc(&self, _iter_id: u64, _len: usize) -> Option<usize> {
93        None
94    }
95
96    /// Advance the step counter by `n` and bail with
97    /// [`RuntimeError::StepLimitExceeded`] if the new count would exceed
98    /// `max_steps`. Native fns with internal loops (`range`,
99    /// `list.map / filter / reduce`, `string.split / replace`,
100    /// `dict.merge`, ...) call this once per inner iteration so a
101    /// million-element pipeline can't hide behind a single AST-node
102    /// step.
103    ///
104    /// Behaviour:
105    /// * `max_steps == None` → no-op (`Ok(())`), no allocation, no
106    ///   lock — the default impl below mirrors that for hosts that
107    ///   build a custom [`NativeFnCaps`].
108    /// * `max_steps == Some(limit)` → `fetch_add(n)` on the same
109    ///   atomic counter the evaluator increments; if the new value
110    ///   crosses the limit, return `StepLimitExceeded { limit, range }`.
111    ///
112    /// `range` should pin the call-site span of the intrinsic so the
113    /// resulting diagnostic points at the same node the AST-level step
114    /// check would have flagged.
115    fn tick(&self, _n: u64, _range: TokenRange) -> Result<(), RuntimeError> {
116        Ok(())
117    }
118}
119
120/// Argument bundle handed to a [`RelonFunction`]. Positional and named
121/// arguments are split apart up front so each host function only inspects
122/// what it cares about.
123#[derive(Clone)]
124pub struct NativeArgs {
125    pub positional: Vec<Value>,
126    pub named: HashMap<String, Value>,
127    caps: Arc<dyn NativeFnCaps>,
128}
129
130impl NativeArgs {
131    pub fn new(caps: Arc<dyn NativeFnCaps>) -> Self {
132        Self {
133            positional: Vec::new(),
134            named: HashMap::new(),
135            caps,
136        }
137    }
138
139    /// Split a list of evaluated args into positional + named buckets.
140    pub fn from_evaluated(args: Vec<EvaluatedArg>, caps: Arc<dyn NativeFnCaps>) -> Self {
141        let mut out = Self::new(caps);
142        for arg in args {
143            match arg.name {
144                Some(name) => {
145                    out.named.insert(name, arg.value);
146                }
147                None => out.positional.push(arg.value),
148            }
149        }
150        out
151    }
152
153    pub fn from_positional(positional: Vec<Value>, caps: Arc<dyn NativeFnCaps>) -> Self {
154        Self {
155            positional,
156            named: HashMap::new(),
157            caps,
158        }
159    }
160
161    pub fn caps(&self) -> &dyn NativeFnCaps {
162        self.caps.as_ref()
163    }
164
165    /// Drop the named-argument map and yield the positional `Vec<Value>` —
166    /// convenient for stdlib functions that only accept positional args.
167    pub fn into_positional(self) -> Vec<Value> {
168        self.positional
169    }
170
171    pub fn len(&self) -> usize {
172        self.positional.len() + self.named.len()
173    }
174
175    pub fn is_empty(&self) -> bool {
176        self.positional.is_empty() && self.named.is_empty()
177    }
178
179    pub fn get(&self, index: usize) -> Option<&Value> {
180        self.positional.get(index)
181    }
182
183    pub fn get_named(&self, name: &str) -> Option<&Value> {
184        self.named.get(name)
185    }
186}
187
188/// Host-implemented native function. Registered into a [`crate::Context`]
189/// via `Context::register_fn` / `register_pure_fn`.
190pub trait RelonFunction: Send + Sync {
191    fn call(&self, args: NativeArgs, range: TokenRange) -> Result<Value, RuntimeError>;
192}
193
194/// Convenience alias for the `dyn RelonFunction` trait object. Hosts that
195/// store native fns by trait object can write `Arc<dyn NativeFn>` instead
196/// of `Arc<dyn RelonFunction>` — the two are interchangeable.
197pub type NativeFn = dyn RelonFunction;