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" | "llm_stream_call" | "agent_loop" => {
178 Some(crate::tracing::SpanKind::LlmCall)
179 }
180 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
181 _ => None,
182 };
183 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
184
185 if self.denied_builtins.contains(name) {
186 return Some(Err(VmError::CategorizedError {
187 message: format!("Tool '{}' is not permitted.", name),
188 category: ErrorCategory::ToolRejected,
189 }));
190 }
191 if let Err(err) = crate::orchestration::enforce_current_policy_for_builtin(name, args) {
192 return Some(Err(err));
193 }
194
195 Some(builtin(args, &mut self.output))
196 }
197
198 pub(crate) fn is_callable_value(v: &VmValue) -> bool {
200 matches!(
201 v,
202 VmValue::Closure(_) | VmValue::BuiltinRef(_) | VmValue::BuiltinRefId { .. }
203 )
204 }
205
206 pub async fn call_closure_pub(
209 &mut self,
210 closure: &VmClosure,
211 args: &[VmValue],
212 ) -> Result<VmValue, VmError> {
213 self.cancel_grace_instructions_remaining = None;
214 self.call_closure(closure, args).await
215 }
216
217 pub(crate) async fn call_named_builtin(
220 &mut self,
221 name: &str,
222 args: Vec<VmValue>,
223 ) -> Result<VmValue, VmError> {
224 self.call_builtin_impl(name, args, None).await
225 }
226
227 pub(crate) async fn call_builtin_id_or_name(
228 &mut self,
229 id: BuiltinId,
230 name: &str,
231 args: Vec<VmValue>,
232 ) -> Result<VmValue, VmError> {
233 self.call_builtin_impl(name, args, Some(id)).await
234 }
235
236 async fn call_builtin_impl(
237 &mut self,
238 name: &str,
239 args: Vec<VmValue>,
240 direct_id: Option<BuiltinId>,
241 ) -> Result<VmValue, VmError> {
242 let span_kind = match name {
244 "llm_call" | "llm_stream" | "llm_stream_call" | "agent_loop" => {
245 Some(crate::tracing::SpanKind::LlmCall)
246 }
247 "mcp_call" => Some(crate::tracing::SpanKind::ToolCall),
248 _ => None,
249 };
250 let _span = span_kind.map(|kind| ScopeSpan::new(kind, name.to_string()));
251
252 if self.denied_builtins.contains(name) {
254 return Err(VmError::CategorizedError {
255 message: format!("Tool '{}' is not permitted.", name),
256 category: ErrorCategory::ToolRejected,
257 });
258 }
259 crate::orchestration::enforce_current_policy_for_builtin(name, &args)?;
260
261 if let Some(result) =
262 crate::runtime_context::dispatch_runtime_context_builtin(self, name, &args)
263 {
264 return result;
265 }
266
267 if let Some(id) = direct_id {
268 if let Some(entry) = self.builtins_by_id.get(&id).cloned() {
269 if entry.name.as_ref() == name {
270 return self.call_builtin_entry(entry.dispatch, args).await;
271 }
272 }
273 }
274
275 if let Some(builtin) = self.builtins.get(name).cloned() {
276 self.call_builtin_entry(VmBuiltinDispatch::Sync(builtin), args)
277 .await
278 } else if let Some(async_builtin) = self.async_builtins.get(name).cloned() {
279 self.call_builtin_entry(VmBuiltinDispatch::Async(async_builtin), args)
280 .await
281 } else if let Some(bridge) = &self.bridge {
282 crate::orchestration::enforce_current_policy_for_bridge_builtin(name)?;
283 let args_json: Vec<serde_json::Value> =
284 args.iter().map(crate::llm::vm_value_to_json).collect();
285 let result = bridge
286 .call(
287 "builtin_call",
288 serde_json::json!({"name": name, "args": args_json}),
289 )
290 .await?;
291 Ok(crate::bridge::json_result_to_vm_value(&result))
292 } else {
293 let all_builtins = self
294 .builtins
295 .keys()
296 .chain(self.async_builtins.keys())
297 .map(|s| s.as_str());
298 if let Some(suggestion) = crate::value::closest_match(name, all_builtins) {
299 return Err(VmError::Runtime(format!(
300 "Undefined builtin: {name} (did you mean `{suggestion}`?)"
301 )));
302 }
303 Err(VmError::UndefinedBuiltin(name.to_string()))
304 }
305 }
306
307 async fn call_builtin_entry(
308 &mut self,
309 dispatch: VmBuiltinDispatch,
310 args: Vec<VmValue>,
311 ) -> Result<VmValue, VmError> {
312 match dispatch {
313 VmBuiltinDispatch::Sync(builtin) => builtin(&args, &mut self.output),
314 VmBuiltinDispatch::Async(async_builtin) => {
315 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
316 slot.borrow_mut().push(self.child_vm());
317 });
318 let result = async_builtin(args).await;
319 CURRENT_ASYNC_BUILTIN_CHILD_VM.with(|slot| {
320 slot.borrow_mut().pop();
321 });
322 result
323 }
324 }
325 }
326}