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;