1use std::future::Future;
2use std::pin::Pin;
3use std::rc::Rc;
4
5use crate::value::{ErrorCategory, VmClosure, VmError, VmValue};
6use crate::BuiltinId;
7
8use super::async_builtin::CURRENT_ASYNC_BUILTIN_CHILD_VM;
9use super::{ScopeSpan, Vm, VmBuiltinDispatch, VmBuiltinEntry};
10
11impl Vm {
12 fn index_builtin_id(&mut self, name: &str, dispatch: VmBuiltinDispatch) {
13 let id = BuiltinId::from_name(name);
14 if self.builtin_id_collisions.contains(&id) {
15 return;
16 }
17 if let Some(existing) = self.builtins_by_id.get(&id) {
18 if existing.name.as_ref() != name {
19 self.builtins_by_id.remove(&id);
20 self.builtin_id_collisions.insert(id);
21 return;
22 }
23 }
24 self.builtins_by_id.insert(
25 id,
26 VmBuiltinEntry {
27 name: Rc::from(name),
28 dispatch,
29 },
30 );
31 }
32
33 fn refresh_builtin_id(&mut self, name: &str) {
34 if let Some(builtin) = self.builtins.get(name).cloned() {
35 self.index_builtin_id(name, VmBuiltinDispatch::Sync(builtin));
36 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
37 self.index_builtin_id(name, VmBuiltinDispatch::Async(async_builtin));
38 } else {
39 let id = BuiltinId::from_name(name);
40 if self
41 .builtins_by_id
42 .get(&id)
43 .is_some_and(|entry| entry.name.as_ref() == name)
44 {
45 self.builtins_by_id.remove(&id);
46 }
47 }
48 }
49
50 pub fn register_builtin<F>(&mut self, name: &str, f: F)
52 where
53 F: Fn(&[VmValue], &mut String) -> Result<VmValue, VmError> + 'static,
54 {
55 self.builtins.insert(name.to_string(), Rc::new(f));
56 self.refresh_builtin_id(name);
57 }
58
59 pub fn unregister_builtin(&mut self, name: &str) {
61 self.builtins.remove(name);
62 self.refresh_builtin_id(name);
63 }
64
65 pub fn register_async_builtin<F, Fut>(&mut self, name: &str, f: F)
67 where
68 F: Fn(Vec<VmValue>) -> Fut + 'static,
69 Fut: Future<Output = Result<VmValue, VmError>> + 'static,
70 {
71 self.async_builtins
72 .insert(name.to_string(), Rc::new(move |args| Box::pin(f(args))));
73 self.refresh_builtin_id(name);
74 }
75
76 pub(crate) fn registered_builtin_id(&self, name: &str) -> Option<BuiltinId> {
77 let id = BuiltinId::from_name(name);
78 if self
79 .builtins_by_id
80 .get(&id)
81 .is_some_and(|entry| entry.name.as_ref() == name)
82 {
83 Some(id)
84 } else {
85 None
86 }
87 }
88
89 pub(crate) fn call_closure<'a>(
92 &'a mut self,
93 closure: &'a VmClosure,
94 args: &'a [VmValue],
95 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
96 Box::pin(async move {
97 let saved_env = self.env.clone();
98 let mut call_env = self.closure_call_env_for_current_frame(closure);
99 let saved_frames = std::mem::take(&mut self.frames);
100 let saved_handlers = std::mem::take(&mut self.exception_handlers);
101 let saved_iterators = std::mem::take(&mut self.iterators);
102 let saved_deadlines = std::mem::take(&mut self.deadlines);
103
104 call_env.push_scope();
105
106 self.env = call_env;
107 let argc = args.len();
108 let mut local_slots = Self::fresh_local_slots(&closure.func.chunk);
109 Self::bind_param_slots(&mut local_slots, &closure.func, args, false);
110 let saved_source_dir = if let Some(ref dir) = closure.source_dir {
111 let prev = crate::stdlib::process::VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
112 crate::stdlib::set_thread_source_dir(dir);
113 prev
114 } else {
115 None
116 };
117 let result = self
118 .run_chunk_ref(
119 Rc::clone(&closure.func.chunk),
120 argc,
121 saved_source_dir,
122 closure.module_functions.clone(),
123 closure.module_state.clone(),
124 Some(local_slots),
125 )
126 .await;
127
128 self.env = saved_env;
129 self.frames = saved_frames;
130 self.exception_handlers = saved_handlers;
131 self.iterators = saved_iterators;
132 self.deadlines = saved_deadlines;
133
134 result
135 })
136 }
137
138 #[allow(clippy::manual_async_fn)]
143 pub(crate) fn call_callable_value<'a>(
144 &'a mut self,
145 callable: &'a VmValue,
146 args: &'a [VmValue],
147 ) -> Pin<Box<dyn Future<Output = Result<VmValue, VmError>> + 'a>> {
148 Box::pin(async move {
149 match callable {
150 VmValue::Closure(closure) => self.call_closure(closure, args).await,
151 VmValue::BuiltinRef(name) => {
152 if let Some(result) = self.call_sync_builtin_by_ref(name, args) {
153 result
154 } else {
155 self.call_named_builtin(name, args.to_vec()).await
156 }
157 }
158 VmValue::BuiltinRefId { id, name } => {
159 self.call_builtin_id_or_name(*id, name, args.to_vec()).await
160 }
161 other => Err(VmError::TypeError(format!(
162 "expected callable, got {}",
163 other.type_name()
164 ))),
165 }
166 })
167 }
168
169 fn call_sync_builtin_by_ref(
170 &mut self,
171 name: &str,
172 args: &[VmValue],
173 ) -> Option<Result<VmValue, VmError>> {
174 let builtin = self.builtins.get(name).cloned()?;
175
176 let span_kind = match name {
177 "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
178 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
179 _ => None,
180 };
181 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
182
183 if self.denied_builtins.contains(name) {
184 return Some(Err(VmError::CategorizedError {
185 message: format!("Tool '{}' is not permitted.", name),
186 category: ErrorCategory::ToolRejected,
187 }));
188 }
189 if let Err(err) = crate::orchestration::enforce_current_policy_for_builtin(name, args) {
190 return Some(Err(err));
191 }
192
193 Some(builtin(args, &mut self.output))
194 }
195
196 pub(crate) fn is_callable_value(v: &VmValue) -> bool {
198 matches!(
199 v,
200 VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
201 )
202 }
203
204 pub async fn call_closure_pub(
207 &mut self,
208 closure: &VmClosure,
209 args: &[VmValue],
210 ) -> Result<VmValue, VmError> {
211 self.cancel_grace_instructions_remaining = None;
212 self.call_closure(closure, args).await
213 }
214
215 pub(crate) async fn call_named_builtin(
218 &mut self,
219 name: &str,
220 args: Vec<VmValue>,
221 ) -> Result<VmValue, VmError> {
222 self.call_builtin_impl(name, args, None).await
223 }
224
225 pub(crate) async fn call_builtin_id_or_name(
226 &mut self,
227 id: BuiltinId,
228 name: &str,
229 args: Vec<VmValue>,
230 ) -> Result<VmValue, VmError> {
231 self.call_builtin_impl(name, args, Some(id)).await
232 }
233
234 async fn call_builtin_impl(
235 &mut self,
236 name: &str,
237 args: Vec<VmValue>,
238 direct_id: Option<BuiltinId>,
239 ) -> Result<VmValue, VmError> {
240 let span_kind = match name {
242 "llm_call" | "llm_stream" | "agent_loop" => Some(crate::tracing::SpanKind::LlmCall),
243 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
244 _ => None,
245 };
246 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
247
248 if self.denied_builtins.contains(name) {
250 return Err(VmError::CategorizedError {
251 message: format!("Tool '{}' is not permitted.", name),
252 category: ErrorCategory::ToolRejected,
253 });
254 }
255 crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
256
257 if let Some(result) =
258 crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
259 {
260 return result;
261 }
262
263 if let Some(id) = direct_id {
264 if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
265 if entry.name.as_ref() == name {
266 return self.call_builtin_entry(entry.dispatch, args).await;
267 }
268 }
269 }
270
271 if let Some(builtin) = self.builtins.get(name).cloned() {
272 self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
273 .await
274 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
275 self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
276 .await
277 } else if let Some(bridge) = &self.bridge {
278 crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
279 let args_json: Vec<serde_json::Value> =
280 args.iter().map(crate::llm::vm_value_to_json).collect();
281 let result = bridge
282 .call(
283 "builtin_call",
284 serde_json::json!({"name": name, "args": args_json}),
285 )
286 .await?;
287 Ok(crate::bridge::json_result_to_vm_value(&result))
288 } else {
289 let all_builtins = self
290 .builtins
291 .keys()
292 .chain(self.async_builtins.keys())
293 .map(|s| s.as_str());
294 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
295 return Err(VmError::Runtime(format!(
296 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
297 )));
298 }
299 Err(VmError::UndefinedBuiltin(name.to_string()))
300 }
301 }
302
303 async fn call_builtin_entry(
304 &mut self,
305 dispatch: VmBuiltinDispatch,
306 args: Vec<VmValue>,
307 ) -> Result<VmValue, VmError> {
308 match dispatch {
309 VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
310 VmBuiltinDispatch::Async(async_builtin) => {
311 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
312 slot.borrow_mut().push(self.child_vm());
313 });
314 let result = async_builtin(args).await;
315 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
316 slot.borrow_mut().pop();
317 });
318 result
319 }
320 }
321 }
322}