1mod binary;
3mod memory;
4mod native;
5pub use native::{ANY, STD, ZustCallback};
6
7mod fns;
8use anyhow::{Result, anyhow};
9pub use fns::{FnInfo, FnVariant};
10mod context;
11pub use context::BuildContext;
12
13mod rt;
14use cranelift::prelude::types;
15use dynamic::{Dynamic, Type};
16pub use rt::JITRunTime;
17#[cfg(feature = "db")]
18mod db_module;
19mod gpu_layout;
20#[cfg(feature = "gpu")]
21mod gpu_module;
22#[cfg(feature = "http")]
23mod http_module;
24#[cfg(feature = "llm")]
25mod llm_module;
26#[cfg(feature = "llm")]
27mod oss_module;
28mod root_module;
29pub use gpu_layout::{GpuFieldLayout, GpuStructLayout};
30pub use parking_lot::RwLock;
31
32use std::sync::{OnceLock, Weak};
33static PTR_TYPE: OnceLock<types::Type> = OnceLock::new();
34pub fn ptr_type() -> types::Type {
35 PTR_TYPE.get().cloned().unwrap()
36}
37
38pub fn get_type(ty: &Type) -> Result<types::Type> {
39 if ty.is_f64() {
40 Ok(types::F64)
41 } else if ty.is_f32() {
42 Ok(types::F32)
43 } else if ty.is_int() | ty.is_uint() {
44 match ty.width() {
45 1 => Ok(types::I8),
46 2 => Ok(types::I16),
47 4 => Ok(types::I32),
48 8 => Ok(types::I64),
49 _ => Err(anyhow!("非法类型 {:?}", ty)),
50 }
51 } else if let Type::Bool = ty {
52 Ok(types::I8)
53 } else {
54 Ok(ptr_type())
55 }
56}
57
58use compiler::Symbol;
59use cranelift::prelude::*;
60use cranelift_module::Module;
61
62pub fn init_jit(mut jit: JITRunTime) -> Result<JITRunTime> {
63 jit.add_all()?;
64 Ok(jit)
65}
66
67use std::sync::Arc;
68unsafe impl Send for JITRunTime {}
69unsafe impl Sync for JITRunTime {}
70
71pub(crate) fn with_vm_context<T>(context: *const Weak<RwLock<JITRunTime>>, f: impl FnOnce(&Vm) -> Result<T>) -> Result<T> {
72 if context.is_null() {
73 return Err(anyhow!("VM context is null"));
74 }
75 let jit = unsafe { &*context }.upgrade().ok_or_else(|| anyhow!("VM context has expired"))?;
76 let vm = Vm { jit };
77 f(&vm)
78}
79
80fn add_method_field(jit: &mut JITRunTime, def: &str, method: &str, id: u32) -> Result<()> {
81 let def_id = jit.get_id(def)?;
82 if let Some((_, define)) = jit.compiler.symbols.get_symbol_mut(def_id) {
83 if let Symbol::Struct(Type::Struct { params, fields }, _) = define {
84 fields.push((method.into(), Type::Symbol { id, params: params.clone() }));
85 }
86 }
87 Ok(())
88}
89
90fn add_native_module_fns(jit: &mut JITRunTime, module: &str, fns: &[(&str, &[Type], Type, *const u8)]) -> Result<()> {
91 jit.add_module(module);
92 for (name, arg_tys, ret_ty, fn_ptr) in fns {
93 let full_name = format!("{}::{}", module, name);
94 jit.add_native_ptr(&full_name, name, arg_tys, ret_ty.clone(), *fn_ptr)?;
95 }
96 jit.pop_module();
97 Ok(())
98}
99
100impl JITRunTime {
101 fn add_memory_runtime(&mut self) -> Result<()> {
102 self.native_symbols.write().insert("__vm_scope_enter".to_string(), memory::scope_enter as *const () as usize);
103 self.native_symbols.write().insert("__vm_scope_exit_void".to_string(), memory::scope_exit_void as *const () as usize);
104 self.native_symbols.write().insert("__vm_scope_exit_dynamic".to_string(), memory::scope_exit_dynamic as *const () as usize);
105 self.native_symbols.write().insert("__vm_scope_exit_bytes".to_string(), memory::scope_exit_bytes as *const () as usize);
106 self.native_symbols.write().insert("__vm_struct_alloc".to_string(), native::struct_alloc as *const () as usize);
107 self.native_symbols.write().insert("__vm_repeat_fill".to_string(), native::repeat_fill as *const () as usize);
108 self.native_symbols.write().insert("__vm_strcat".to_string(), native::strcat as *const () as usize);
109 self.native_symbols.write().insert("__vm_strcat_i64".to_string(), native::strcat_i64 as *const () as usize);
110 self.native_symbols.write().insert("__vm_strcat_assign".to_string(), native::strcat_assign as *const () as usize);
111 self.native_symbols.write().insert("__vm_callback_new".to_string(), native::callback_new as *const () as usize);
112 self.native_symbols.write().insert("__vm_spawn_ptr".to_string(), native::spawn_ptr as *const () as usize);
113 self.native_symbols.write().insert("__vm_struct_from_ptr".to_string(), native::struct_from_ptr as *const () as usize);
114 self.native_symbols.write().insert("__vm_array_from_ptr".to_string(), native::array_from_ptr as *const () as usize);
115 self.native_symbols.write().insert("__vm_array_to_ptr".to_string(), native::array_to_ptr as *const () as usize);
116 self.native_symbols.write().insert("__vm_arith_fault".to_string(), memory::arith_fault as *const () as usize);
117
118 let void_sig = self.get_sig(&[], Type::Void)?;
119 self.scope_enter_fn = Some(self.module.declare_function("__vm_scope_enter", cranelift_module::Linkage::Import, &void_sig)?);
120 self.scope_exit_void_fn = Some(self.module.declare_function("__vm_scope_exit_void", cranelift_module::Linkage::Import, &void_sig)?);
121
122 let dynamic_sig = self.get_sig(&[Type::Any], Type::Any)?;
123 self.scope_exit_dynamic_fn = Some(self.module.declare_function("__vm_scope_exit_dynamic", cranelift_module::Linkage::Import, &dynamic_sig)?);
124
125 let bytes_sig = self.get_sig(&[Type::Any, Type::I64], Type::Any)?;
126 self.scope_exit_bytes_fn = Some(self.module.declare_function("__vm_scope_exit_bytes", cranelift_module::Linkage::Import, &bytes_sig)?);
127
128 let struct_alloc_sig = self.get_sig(&[Type::I64], Type::Any)?;
129 self.struct_alloc_fn = Some(self.module.declare_function("__vm_struct_alloc", cranelift_module::Linkage::Import, &struct_alloc_sig)?);
130
131 let repeat_fill_sig = self.get_sig(&[Type::Any, Type::I64, Type::I64, Type::I64], Type::Void)?;
132 self.repeat_fill_fn = Some(self.module.declare_function("__vm_repeat_fill", cranelift_module::Linkage::Import, &repeat_fill_sig)?);
133
134 let strcat_sig = self.get_sig(&[Type::Str, Type::Str], Type::Str)?;
135 self.strcat_fn = Some(self.module.declare_function("__vm_strcat", cranelift_module::Linkage::Import, &strcat_sig)?);
136
137 let strcat_i64_sig = self.get_sig(&[Type::Str, Type::I64], Type::Str)?;
138 self.strcat_i64_fn = Some(self.module.declare_function("__vm_strcat_i64", cranelift_module::Linkage::Import, &strcat_i64_sig)?);
139
140 let strcat_assign_sig = self.get_sig(&[Type::Any, Type::Any], Type::Any)?;
141 self.strcat_assign_fn = Some(self.module.declare_function("__vm_strcat_assign", cranelift_module::Linkage::Import, &strcat_assign_sig)?);
142
143 let callback_new_sig = self.get_sig(&[Type::I64, Type::I64, Type::I64, Type::Any], Type::Any)?;
144 self.callback_new_fn = Some(self.module.declare_function("__vm_callback_new", cranelift_module::Linkage::Import, &callback_new_sig)?);
145
146 let spawn_ptr_sig = self.get_sig(&[Type::I64, Type::I64, Type::Any], Type::Bool)?;
147 self.spawn_ptr_fn = Some(self.module.declare_function("__vm_spawn_ptr", cranelift_module::Linkage::Import, &spawn_ptr_sig)?);
148
149 let struct_from_ptr_sig = self.get_sig(&[Type::I64, Type::I64], Type::Any)?;
150 self.struct_from_ptr_fn = Some(self.module.declare_function("__vm_struct_from_ptr", cranelift_module::Linkage::Import, &struct_from_ptr_sig)?);
151 self.array_from_ptr_fn = Some(self.module.declare_function("__vm_array_from_ptr", cranelift_module::Linkage::Import, &struct_from_ptr_sig)?);
152 let array_to_ptr_sig = self.get_sig(&[Type::Any, Type::Any, Type::I64], Type::Void)?;
153 self.array_to_ptr_fn = Some(self.module.declare_function("__vm_array_to_ptr", cranelift_module::Linkage::Import, &array_to_ptr_sig)?);
154
155 self.arith_fault_fn = Some(self.module.declare_function("__vm_arith_fault", cranelift_module::Linkage::Import, &void_sig)?);
156 Ok(())
157 }
158
159 pub fn add_module(&mut self, name: &str) {
160 self.compiler.symbols.add_module(name.into());
161 }
162
163 pub fn pop_module(&mut self) {
164 self.compiler.symbols.pop_module();
165 }
166
167 pub fn add_native_const(&mut self, name: &str, value: impl Into<Dynamic>, ty: Type) -> u32 {
168 self.compiler.add_symbol(name, Symbol::Const { value: value.into(), ty, is_pub: true })
169 }
170
171 pub fn add_type(&mut self, name: &str, ty: Type, is_pub: bool) -> u32 {
172 self.compiler.add_symbol(name, Symbol::Struct(ty, is_pub))
173 }
174
175 pub fn add_empty_type(&mut self, name: &str) -> Result<u32> {
176 match self.get_id(name) {
177 Ok(id) => Ok(id),
178 Err(_) => Ok(self.add_type(name, Type::Struct { params: Vec::new(), fields: Vec::new() }, true)),
179 }
180 }
181
182 pub fn add_native_module_ptr(&mut self, module: &str, name: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
183 self.add_module(module);
184 let full_name = format!("{}::{}", module, name);
185 let result = self.add_native_ptr(&full_name, name, arg_tys, ret_ty, fn_ptr);
186 self.pop_module();
187 result
188 }
189
190 pub(crate) fn add_native_module_context_ptr(&mut self, module: &str, name: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
191 self.add_module(module);
192 let full_name = format!("{}::{}", module, name);
193 let result = self.add_context_native_ptr(&full_name, name, arg_tys, ret_ty, fn_ptr);
194 self.pop_module();
195 result
196 }
197
198 pub fn add_native_method_ptr(&mut self, def: &str, method: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
199 self.add_empty_type(def)?;
200 let full_name = format!("{}::{}", def, method);
201 let id = self.add_native_ptr(&full_name, &full_name, arg_tys, ret_ty, fn_ptr)?;
202 add_method_field(self, def, method, id)?;
203 Ok(id)
204 }
205
206 pub fn add_std(&mut self) -> Result<()> {
207 if self.compiler.symbols.get_id("std::print").is_ok() {
208 return Ok(());
209 }
210 self.add_module("std");
211 for (name, arg_tys, ret_ty, fn_ptr) in STD {
212 self.add_native_ptr(name, name, arg_tys, ret_ty, fn_ptr)?;
213 }
214 self.add_context_native_ptr("import", "import", &[Type::Any, Type::Any], Type::Bool, native::import_with_vm as *const u8)?;
215 self.add_context_native_ptr("spawn", "spawn", &[Type::Any, Type::Any], Type::Bool, native::spawn_with_vm as *const u8)?;
216 Ok(())
217 }
218
219 pub fn add_any(&mut self) -> Result<()> {
220 if self.compiler.symbols.get_id("Any").is_ok() && self.compiler.symbols.get_id("Any::is_map").is_ok() {
221 return Ok(());
222 }
223 for (name, arg_tys, ret_ty, fn_ptr) in ANY {
224 let (_, method) = name.split_once("::").ok_or_else(|| anyhow!("非法 Any 方法名 {}", name))?;
225 self.add_native_method_ptr("Any", method, arg_tys, ret_ty, fn_ptr)?;
226 }
227 Ok(())
228 }
229
230 pub fn add_vec(&mut self) -> Result<()> {
231 if self.compiler.symbols.get_id("Vec::get_idx").is_ok() {
232 return Ok(());
233 }
234 self.add_empty_type("Vec")?;
235 let vec_def = Type::Symbol { id: self.get_id("Vec")?, params: Vec::new() };
236 self.add_inline("Vec::swap", vec![vec_def.clone(), Type::I64, Type::I64], Type::Void, |ctx: Option<&mut BuildContext>, args: Vec<Value>| {
237 if let Some(ctx) = ctx {
238 let width = ctx.builder.ins().iconst(types::I64, 4);
239 let offset_val = ctx.builder.ins().imul(args[1], width); let final_addr = ctx.builder.ins().iadd(args[0], offset_val); let dest = ctx.builder.ins().imul(args[2], width);
242 let dest_addr = ctx.builder.ins().iadd(args[0], dest); let dest_val = ctx.builder.ins().load(types::I32, MemFlags::trusted(), dest_addr, 0);
244 let v = ctx.builder.ins().load(types::I32, MemFlags::trusted(), final_addr, 0);
245 ctx.builder.ins().store(MemFlags::trusted(), v, dest_addr, 0);
246 ctx.builder.ins().store(MemFlags::trusted(), dest_val, final_addr, 0);
247 }
248 Err(anyhow!("无返回值"))
249 })?;
250
251 self.add_inline("Vec::get_idx", vec![vec_def.clone(), Type::I64], Type::I32, |ctx: Option<&mut BuildContext>, args: Vec<Value>| {
252 if let Some(ctx) = ctx {
253 let width = ctx.builder.ins().iconst(types::I64, 4);
254 let offset_val = ctx.builder.ins().imul(args[1], width); let final_addr = ctx.builder.ins().iadd(args[0], offset_val);
256 Ok((Some(ctx.builder.ins().load(types::I32, MemFlags::trusted(), final_addr, 0)), Type::I32))
257 } else {
258 Ok((None, Type::I32))
259 }
260 })?;
261 Ok(())
262 }
263
264 #[cfg(feature = "llm")]
265 pub fn add_llm(&mut self) -> Result<()> {
266 if self.compiler.symbols.get_id("llm::complete").is_ok() {
267 return Ok(());
268 }
269 add_native_module_fns(self, "llm", &llm_module::LLM_NATIVE)?;
270 add_native_module_fns(self, "oss", &oss_module::OSS_NATIVE)
271 }
272
273 pub fn add_root(&mut self) -> Result<()> {
274 if self.compiler.symbols.get_id("root::get").is_ok() {
275 return Ok(());
276 }
277 add_native_module_fns(self, "root", &root_module::ROOT_NATIVE)?;
278 self.add_native_module_context_ptr("root", "add_fn", &[Type::Any, Type::Any], Type::Bool, root_module::root_add_fn_with_vm as *const u8)?;
279 Ok(())
280 }
281
282 #[cfg(feature = "http")]
283 pub fn add_http(&mut self) -> Result<()> {
284 if self.compiler.symbols.get_id("http::request").is_ok() {
285 return Ok(());
286 }
287 add_native_module_fns(self, "http", &http_module::HTTP_NATIVE)?;
288 http_module::add_root_handlers()
289 }
290
291 #[cfg(feature = "db")]
292 pub fn add_db(&mut self) -> Result<()> {
293 if self.compiler.symbols.get_id("db::select").is_ok() {
294 return Ok(());
295 }
296 add_native_module_fns(self, "db", &db_module::DB_NATIVE)
297 }
298
299 #[cfg(feature = "gpu")]
300 pub fn add_gpu(&mut self) -> Result<()> {
301 if self.compiler.symbols.get_id("gpu::spirv_check").is_ok() {
302 return Ok(());
303 }
304 add_native_module_fns(self, "gpu", &gpu_module::GPU_NATIVE)
305 }
306
307 pub fn add_all(&mut self) -> Result<()> {
308 self.add_std()?;
309 self.add_any()?;
310 self.add_vec()?;
311 self.add_root()?;
312 #[cfg(feature = "llm")]
313 self.add_llm()?;
314 #[cfg(feature = "http")]
315 self.add_http()?;
316 #[cfg(feature = "db")]
317 self.add_db()?;
318 #[cfg(feature = "gpu")]
319 self.add_gpu()?;
320 Ok(())
321 }
322}
323
324#[derive(Clone)]
325pub struct Vm {
326 pub jit: Arc<parking_lot::RwLock<JITRunTime>>,
327}
328
329impl Vm {
330 pub fn new() -> Self {
331 dynamic::set_dynamic_return_handler(memory::take_dynamic_return);
332 let jit = Arc::new(RwLock::new(JITRunTime::new(|_| {})));
333 {
334 let mut guard = jit.write();
335 guard.set_owner(Arc::downgrade(&jit));
336 guard.add_memory_runtime().expect("register VM memory runtime");
337 guard.add_std().expect("register VM std runtime");
338 guard.add_any().expect("register VM Any runtime");
339 guard.add_vec().expect("register VM Vec runtime");
340 guard.add_root().expect("register VM root runtime");
341 }
342 Self { jit }
343 }
344
345 pub fn with_all() -> Result<Self> {
346 let vm = Self::new();
347 vm.jit.write().add_all()?;
348 Ok(vm)
349 }
350
351 pub fn import(&self, name: &str, path: &str) -> Result<()> {
352 if let Ok(code) = root::get(path) {
355 if code.is_str() {
356 self.jit.write().import_code(name, code.as_str().as_bytes().to_vec())?;
357 } else {
358 self.jit.write().import_code(name, code.get_dynamic("code").ok_or_else(|| anyhow!("{:?} 没有 code 成员", code))?.as_str().as_bytes().to_vec())?;
359 }
360 Ok(())
361 } else {
362 self.jit.write().compiler.import_file(name, path)?;
363 Ok(())
364 }
365 }
366}
367
368impl Default for Vm {
369 fn default() -> Self {
370 Self::new()
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::{GpuStructLayout, Vm, ZustCallback};
377 use dynamic::{CustomProperty, Dynamic, ToJson, Type};
378 use std::collections::BTreeMap;
379
380 struct TestFn {
382 ptr: *const u8,
383 ret: Type,
384 }
385
386 impl TestFn {
387 fn ptr(&self) -> *const u8 {
388 self.ptr
389 }
390 fn ret_ty(&self) -> &Type {
391 &self.ret
392 }
393 }
394
395 trait VmTestExt {
397 fn import_code(&self, name: &str, code: Vec<u8>) -> anyhow::Result<()>;
398 fn get_fn(&self, name: &str, arg_tys: &[Type]) -> anyhow::Result<TestFn>;
399 fn get_fn_with_params(&self, name: &str, arg_tys: &[Type], generic_args: &[Type]) -> anyhow::Result<TestFn>;
400 fn get_fn_ptr(&self, name: &str, arg_tys: &[Type]) -> anyhow::Result<(*const u8, Type)>;
401 fn infer(&self, name: &str, arg_tys: &[Type]) -> anyhow::Result<Type>;
402 fn add_native_module_ptr(&self, module: &str, name: &str, arg_tys: &[Type], ret_ty: Type, ptr: *const u8) -> anyhow::Result<u32>;
403 fn add_native_method_ptr(&self, def: &str, method: &str, arg_tys: &[Type], ret_ty: Type, ptr: *const u8) -> anyhow::Result<u32>;
404 fn add_empty_type(&self, name: &str) -> anyhow::Result<u32>;
405 fn add_std(&self) -> anyhow::Result<()>;
406 fn add_any(&self) -> anyhow::Result<()>;
407 fn get_symbol(&self, name: &str, params: Vec<Type>) -> anyhow::Result<Type>;
408 fn gpu_struct_layout(&self, name: &str, params: &[Type]) -> anyhow::Result<GpuStructLayout>;
409 fn load(&self, code: Vec<u8>, arg_name: smol_str::SmolStr) -> anyhow::Result<(i64, Type)>;
410 }
411
412 impl VmTestExt for Vm {
413 fn import_code(&self, name: &str, code: Vec<u8>) -> anyhow::Result<()> {
414 self.jit.write().import_code(name, code)
415 }
416 fn get_fn(&self, name: &str, arg_tys: &[Type]) -> anyhow::Result<TestFn> {
417 let (ptr, ret) = self.jit.write().get_fn_ptr(name, arg_tys)?;
418 Ok(TestFn { ptr, ret })
419 }
420 fn get_fn_with_params(&self, name: &str, arg_tys: &[Type], generic_args: &[Type]) -> anyhow::Result<TestFn> {
421 let (ptr, ret) = self.jit.write().get_fn_ptr_with_params(name, arg_tys, generic_args)?;
422 Ok(TestFn { ptr, ret })
423 }
424 fn get_fn_ptr(&self, name: &str, arg_tys: &[Type]) -> anyhow::Result<(*const u8, Type)> {
425 self.jit.write().get_fn_ptr(name, arg_tys)
426 }
427 fn infer(&self, name: &str, arg_tys: &[Type]) -> anyhow::Result<Type> {
428 self.jit.write().get_type(name, arg_tys)
429 }
430 fn add_native_module_ptr(&self, module: &str, name: &str, arg_tys: &[Type], ret_ty: Type, ptr: *const u8) -> anyhow::Result<u32> {
431 self.jit.write().add_native_module_ptr(module, name, arg_tys, ret_ty, ptr)
432 }
433 fn add_native_method_ptr(&self, def: &str, method: &str, arg_tys: &[Type], ret_ty: Type, ptr: *const u8) -> anyhow::Result<u32> {
434 self.jit.write().add_native_method_ptr(def, method, arg_tys, ret_ty, ptr)
435 }
436 fn add_empty_type(&self, name: &str) -> anyhow::Result<u32> {
437 self.jit.write().add_empty_type(name)
438 }
439 fn add_std(&self) -> anyhow::Result<()> {
440 self.jit.write().add_std()
441 }
442 fn add_any(&self) -> anyhow::Result<()> {
443 self.jit.write().add_any()
444 }
445 fn get_symbol(&self, name: &str, params: Vec<Type>) -> anyhow::Result<Type> {
446 Ok(Type::Symbol { id: self.jit.write().get_id(name)?, params })
447 }
448 fn gpu_struct_layout(&self, name: &str, params: &[Type]) -> anyhow::Result<GpuStructLayout> {
449 let jit = self.jit.write();
450 GpuStructLayout::from_symbol_table(&jit.compiler.symbols, name, params)
451 }
452 fn load(&self, code: Vec<u8>, arg_name: smol_str::SmolStr) -> anyhow::Result<(i64, Type)> {
453 self.jit.write().load(code, arg_name)
454 }
455 }
456
457 extern "C" fn math_double(value: i64) -> i64 {
458 value * 2
459 }
460
461 #[test]
462 fn build_context_set_var_fills_sparse_none_slots() -> anyhow::Result<()> {
463 use crate::context::{BuildContext, LocalVar};
464 use cranelift::codegen::ir::{Function, Signature, UserFuncName};
465 use cranelift::codegen::isa::CallConv;
466 use cranelift::prelude::{FunctionBuilder, FunctionBuilderContext};
467
468 let mut function = Function::with_name_signature(UserFuncName::user(0, 0), Signature::new(CallConv::Fast));
469 let mut function_ctx = FunctionBuilderContext::new();
470 let builder = FunctionBuilder::new(&mut function, &mut function_ctx);
471 let mut ctx = BuildContext::new(builder, &[], Type::Void)?;
472
473 ctx.set_var(33, LocalVar::None)?;
474
475 assert!(matches!(ctx.get_var(32)?, LocalVar::None));
476 assert!(matches!(ctx.get_var(33)?, LocalVar::None));
477 assert!(ctx.get_var(34).is_err());
478 Ok(())
479 }
480
481 #[test]
482 fn vm_can_add_native_after_jit_creation() -> anyhow::Result<()> {
483 let vm = Vm::new();
484 vm.add_native_module_ptr("math", "double", &[Type::I64], Type::I64, math_double as *const u8)?;
485 vm.import_code(
486 "vm_dynamic_native",
487 br#"
488 pub fn run(value: i64) {
489 math::double(value)
490 }
491 "#
492 .to_vec(),
493 )?;
494
495 let compiled = vm.get_fn("vm_dynamic_native::run", &[Type::I64])?;
496 assert_eq!(compiled.ret_ty(), &Type::I64);
497 let run: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
498 assert_eq!(run(21), 42);
499 Ok(())
500 }
501
502 #[test]
503 fn vm_new_registers_std_and_any() -> anyhow::Result<()> {
504 let vm = Vm::new();
505 vm.add_std()?;
506 vm.add_any()?;
507 assert_eq!(vm.infer("std::print", &[Type::Any])?, Type::Void);
508 assert_eq!(vm.infer("std::sqrt", &[Type::F64])?, Type::F64);
509
510 vm.import_code(
511 "vm_new_default_any",
512 br#"
513 pub fn has_items(content) {
514 if content.is_map() {
515 if content.contains("items") {
516 return content.items.len() > 0;
517 }
518 }
519 false
520 }
521 "#
522 .to_vec(),
523 )?;
524
525 assert_eq!(vm.infer("vm_new_default_any::has_items", &[Type::Any])?, Type::Bool);
526 let compiled = vm.get_fn("vm_new_default_any::has_items", &[Type::Any])?;
527 assert_eq!(compiled.ret_ty(), &Type::Bool);
528 Ok(())
529 }
530
531 #[test]
532 fn std_sqrt_is_available_as_top_level_function() -> anyhow::Result<()> {
533 let vm = Vm::with_all()?;
534 vm.import_code(
535 "vm_std_sqrt",
536 br#"
537 pub fn run() {
538 sqrt(9.0f64)
539 }
540 "#
541 .to_vec(),
542 )?;
543
544 let compiled = vm.get_fn("vm_std_sqrt::run", &[])?;
545 assert_eq!(compiled.ret_ty(), &Type::F64);
546 let run: extern "C" fn() -> f64 = unsafe { std::mem::transmute(compiled.ptr()) };
547 assert_eq!(run(), 3.0);
548 Ok(())
549 }
550
551 #[test]
552 fn tuple_assignment_uses_simultaneous_scalar_temps() -> anyhow::Result<()> {
553 let vm = Vm::with_all()?;
554 vm.import_code(
555 "vm_tuple_assignment",
556 br#"
557 pub fn swap() {
558 let a = 1i64;
559 let b = 2i64;
560 (a, b) = (b, a);
561 a * 10i64 + b
562 }
563
564 pub fn fib(n: i64) {
565 let a = 0i64;
566 let b = 1i64;
567 for _ in 0..n {
568 (a, b) = (b, (a + b) % 1000000007i64);
569 }
570 a
571 }
572 "#
573 .to_vec(),
574 )?;
575
576 let swap = vm.get_fn("vm_tuple_assignment::swap", &[])?;
577 let swap: extern "C" fn() -> i64 = unsafe { std::mem::transmute(swap.ptr()) };
578 assert_eq!(swap(), 21);
579
580 let fib = vm.get_fn("vm_tuple_assignment::fib", &[Type::I64])?;
581 let fib: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(fib.ptr()) };
582 assert_eq!(fib(10), 55);
583 Ok(())
584 }
585
586 #[test]
587 fn nested_struct_arg_return_struct_field_is_static_field_access() -> anyhow::Result<()> {
588 let vm = Vm::with_all()?;
589 vm.import_code(
590 "vm_nested_struct_return_field",
591 br#"
592 pub struct Inner {
593 value: i64,
594 }
595
596 pub struct RoleMini {
597 inner: Inner,
598 hp: i64,
599 }
600
601 pub struct TeamMini {
602 role: RoleMini,
603 }
604
605 pub struct BigSummary {
606 winner: i64,
607 loser: i64,
608 }
609
610 pub fn make_big_with_team(team: TeamMini) {
611 let score = team.role.inner.value;
612 BigSummary{winner: score, loser: 0}
613 }
614
615 pub fn read_team_winner_direct() {
616 let team = TeamMini{role: RoleMini{inner: Inner{value: 9}, hp: 1}};
617 make_big_with_team(team).winner
618 }
619
620 pub fn read_team_winner_bound() {
621 let team = TeamMini{role: RoleMini{inner: Inner{value: 9}, hp: 1}};
622 let summary = make_big_with_team(team);
623 summary.winner
624 }
625 "#
626 .to_vec(),
627 )?;
628
629 let compiled = vm.get_fn("vm_nested_struct_return_field::read_team_winner_direct", &[])?;
630 assert_eq!(compiled.ret_ty(), &Type::I64);
631 let direct: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
632 assert_eq!(direct(), 9);
633
634 let compiled = vm.get_fn("vm_nested_struct_return_field::read_team_winner_bound", &[])?;
635 assert_eq!(compiled.ret_ty(), &Type::I64);
636 let bound: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
637 assert_eq!(bound(), 9);
638 Ok(())
639 }
640
641 #[test]
642 fn any_push_does_not_consume_reused_value() -> anyhow::Result<()> {
643 let vm = Vm::with_all()?;
644 vm.import_code(
645 "vm_any_push_reused_value",
646 br#"
647 pub fn run() {
648 let role_id = "acct_role_2";
649 let updated = [];
650 updated.push(role_id);
651 {
652 ok: true,
653 user_id: role_id,
654 first: updated.get_idx(0)
655 }
656 }
657 "#
658 .to_vec(),
659 )?;
660
661 let compiled = vm.get_fn("vm_any_push_reused_value::run", &[])?;
662 assert_eq!(compiled.ret_ty(), &Type::Any);
663 let run: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
664 let result = unsafe { &*run() };
665 assert_eq!(result.get_dynamic("ok").and_then(|value| value.as_bool()), Some(true));
666 assert_eq!(result.get_dynamic("user_id").map(|value| value.as_str().to_string()), Some("acct_role_2".to_string()));
667 assert_eq!(result.get_dynamic("first").map(|value| value.as_str().to_string()), Some("acct_role_2".to_string()));
668 Ok(())
669 }
670
671 #[test]
672 fn negate_narrow_integers() -> anyhow::Result<()> {
673 let vm = Vm::with_all()?;
674 vm.import_code(
675 "vm_neg_narrow",
676 br#"
677 pub fn neg_i8(a: i8) { -a }
678 pub fn neg_i16(a: i16) { -a }
679 "#
680 .to_vec(),
681 )?;
682
683 let neg_i8 = vm.get_fn("vm_neg_narrow::neg_i8", &[Type::I8])?;
684 assert_eq!(neg_i8.ret_ty(), &Type::I8);
685 let neg_i8: extern "C" fn(i8) -> i8 = unsafe { std::mem::transmute(neg_i8.ptr()) };
686 assert_eq!(neg_i8(5), -5);
687 assert_eq!(neg_i8(-7), 7);
688
689 let neg_i16 = vm.get_fn("vm_neg_narrow::neg_i16", &[Type::I16])?;
690 assert_eq!(neg_i16.ret_ty(), &Type::I16);
691 let neg_i16: extern "C" fn(i16) -> i16 = unsafe { std::mem::transmute(neg_i16.ptr()) };
692 assert_eq!(neg_i16(5), -5);
693 assert_eq!(neg_i16(-300), 300);
694 Ok(())
695 }
696
697 #[test]
698 fn integer_divide_by_zero_does_not_crash() -> anyhow::Result<()> {
699 let vm = Vm::with_all()?;
700 vm.import_code(
701 "vm_div_by_zero",
702 br#"
703 pub fn divz(a: i64, b: i64) { a / b }
704 pub fn modz(a: i64, b: i64) { a % b }
705 pub fn overflow(a: i64, b: i64) { a / b }
706 "#
707 .to_vec(),
708 )?;
709
710 let divz = vm.get_fn("vm_div_by_zero::divz", &[Type::I64, Type::I64])?;
711 let modz = vm.get_fn("vm_div_by_zero::modz", &[Type::I64, Type::I64])?;
712 let overflow = vm.get_fn("vm_div_by_zero::overflow", &[Type::I64, Type::I64])?;
713 let divz: extern "C" fn(i64, i64) -> i64 = unsafe { std::mem::transmute(divz.ptr()) };
714 let modz: extern "C" fn(i64, i64) -> i64 = unsafe { std::mem::transmute(modz.ptr()) };
715 let overflow: extern "C" fn(i64, i64) -> i64 = unsafe { std::mem::transmute(overflow.ptr()) };
716
717 let _ = dynamic::take_fault();
719 assert_eq!(divz(7, 2), 3);
720 assert_eq!(modz(7, 2), 1);
721 assert!(dynamic::take_fault().is_none());
722
723 assert_eq!(divz(7, 0), 0);
725 assert!(dynamic::take_fault().is_some());
726 assert_eq!(modz(7, 0), 0);
727 assert!(dynamic::take_fault().is_some());
728
729 assert_eq!(overflow(i64::MIN, -1), 0);
731 assert!(dynamic::take_fault().is_some());
732 Ok(())
733 }
734
735 #[test]
736 fn constant_divide_by_zero_does_not_crash() -> anyhow::Result<()> {
737 let vm = Vm::with_all()?;
738 vm.import_code(
739 "vm_const_div_zero",
740 br#"
741 pub fn divz(a: i64) { a / 0 }
742 pub fn modz(a: i64) { a % 0 }
743 pub fn divc(a: i64) { a / 7 }
744 "#
745 .to_vec(),
746 )?;
747 let divz: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(vm.get_fn("vm_const_div_zero::divz", &[Type::I64])?.ptr()) };
748 let modz: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(vm.get_fn("vm_const_div_zero::modz", &[Type::I64])?.ptr()) };
749 let divc: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(vm.get_fn("vm_const_div_zero::divc", &[Type::I64])?.ptr()) };
750
751 let _ = dynamic::take_fault();
752 assert_eq!(divz(42), 0);
754 assert!(dynamic::take_fault().is_some());
755 assert_eq!(modz(42), 0);
756 assert!(dynamic::take_fault().is_some());
757 assert_eq!(divc(42), 6);
759 assert!(dynamic::take_fault().is_none());
760 Ok(())
761 }
762
763 #[test]
764 fn dynamic_divide_by_zero_returns_null() -> anyhow::Result<()> {
765 let vm = Vm::with_all()?;
766 vm.import_code(
767 "vm_any_div_by_zero",
768 br#"
769 pub fn divz(a, b) { a / b }
770 "#
771 .to_vec(),
772 )?;
773
774 let divz = vm.get_fn("vm_any_div_by_zero::divz", &[Type::Any, Type::Any])?;
775 let divz: extern "C" fn(*const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(divz.ptr()) };
776 let a = Dynamic::from(7i64);
777 let zero = Dynamic::from(0i64);
778 let _ = dynamic::take_fault();
779 let result = unsafe { &*divz(&a, &zero) };
780 assert!(result.is_null());
781 assert!(dynamic::take_fault().is_some());
782 Ok(())
783 }
784
785 #[test]
786 fn compares_any_with_string_literal_as_string() -> anyhow::Result<()> {
787 let vm = Vm::with_all()?;
788 vm.import_code(
789 "vm_string_compare_any",
790 br#"
791 pub fn any_ne_empty(chat_path) {
792 chat_path != ""
793 }
794 "#
795 .to_vec(),
796 )?;
797
798 let compiled = vm.get_fn("vm_string_compare_any::any_ne_empty", &[Type::Any])?;
799 assert_eq!(compiled.ret_ty(), &Type::Bool);
800
801 let any_ne_empty: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
802 let empty = Dynamic::from("");
803 let non_empty = Dynamic::from("chat");
804
805 assert!(!any_ne_empty(&empty));
806 assert!(any_ne_empty(&non_empty));
807 Ok(())
808 }
809
810 #[test]
811 fn compares_bool_values_and_bool_literals() -> anyhow::Result<()> {
812 let vm = Vm::with_all()?;
813 vm.import_code(
814 "vm_bool_compare",
815 br#"
816 pub fn eq_true(value: bool) {
817 value == true
818 }
819
820 pub fn ne_false(value: bool) {
821 value != false
822 }
823
824 pub fn literal_left(value: bool) {
825 true == value
826 }
827
828 pub fn eq_pair(left: bool, right: bool) {
829 left == right
830 }
831
832 pub fn logic_pair(left: bool, right: bool) {
833 (left && right) || (left == true && right != false)
834 }
835 "#
836 .to_vec(),
837 )?;
838
839 let compiled = vm.get_fn("vm_bool_compare::eq_true", &[Type::Bool])?;
840 assert_eq!(compiled.ret_ty(), &Type::Bool);
841 let eq_true: extern "C" fn(bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
842 assert!(eq_true(true));
843 assert!(!eq_true(false));
844
845 let compiled = vm.get_fn("vm_bool_compare::ne_false", &[Type::Bool])?;
846 assert_eq!(compiled.ret_ty(), &Type::Bool);
847 let ne_false: extern "C" fn(bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
848 assert!(ne_false(true));
849 assert!(!ne_false(false));
850
851 let compiled = vm.get_fn("vm_bool_compare::literal_left", &[Type::Bool])?;
852 assert_eq!(compiled.ret_ty(), &Type::Bool);
853 let literal_left: extern "C" fn(bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
854 assert!(literal_left(true));
855 assert!(!literal_left(false));
856
857 let compiled = vm.get_fn("vm_bool_compare::eq_pair", &[Type::Bool, Type::Bool])?;
858 assert_eq!(compiled.ret_ty(), &Type::Bool);
859 let eq_pair: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
860 assert!(eq_pair(true, true));
861 assert!(eq_pair(false, false));
862 assert!(!eq_pair(true, false));
863 assert!(!eq_pair(false, true));
864
865 let compiled = vm.get_fn("vm_bool_compare::logic_pair", &[Type::Bool, Type::Bool])?;
866 assert_eq!(compiled.ret_ty(), &Type::Bool);
867 let logic_pair: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
868 assert!(logic_pair(true, true));
869 assert!(!logic_pair(true, false));
870 assert!(!logic_pair(false, true));
871 assert!(!logic_pair(false, false));
872 Ok(())
873 }
874
875 #[test]
876 fn parenthesized_expression_can_call_any_method() -> anyhow::Result<()> {
877 let vm = Vm::with_all()?;
878 vm.import_code(
879 "vm_parenthesized_method_call",
880 br#"
881 pub fn run(value) {
882 (value + 2).to_i64()
883 }
884 "#
885 .to_vec(),
886 )?;
887
888 let compiled = vm.get_fn("vm_parenthesized_method_call::run", &[Type::Any])?;
889 assert_eq!(compiled.ret_ty(), &Type::I64);
890 let run: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
891 let value = Dynamic::from(40i64);
892
893 assert_eq!(run(&value), 42);
894 Ok(())
895 }
896
897 #[test]
898 fn casts_any_float_to_i32_without_zeroing() -> anyhow::Result<()> {
899 let vm = Vm::with_all()?;
900 vm.import_code(
901 "vm_any_float_to_i32",
902 br#"
903 pub fn direct(value) {
904 value as i32
905 }
906
907 pub fn map_field(value) {
908 let field = value.v;
909 field as i32
910 }
911
912 pub fn damage(attacker, def_rate) {
913 let x = attacker.atk * (1.0 - def_rate);
914 x as i32
915 }
916 "#
917 .to_vec(),
918 )?;
919
920 let compiled = vm.get_fn("vm_any_float_to_i32::direct", &[Type::Any])?;
921 assert_eq!(compiled.ret_ty(), &Type::I32);
922 let direct: extern "C" fn(*const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
923 let value = Dynamic::from(9.5f64);
924 assert_eq!(direct(&value), 9);
925
926 let compiled = vm.get_fn("vm_any_float_to_i32::map_field", &[Type::Any])?;
927 assert_eq!(compiled.ret_ty(), &Type::I32);
928 let map_field: extern "C" fn(*const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
929 let value = dynamic::map!("v"=> 9.5f64);
930 assert_eq!(map_field(&value), 9);
931
932 let compiled = vm.get_fn("vm_any_float_to_i32::damage", &[Type::Any, Type::Any])?;
933 assert_eq!(compiled.ret_ty(), &Type::I32);
934 let damage: extern "C" fn(*const Dynamic, *const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
935 let attacker = dynamic::map!("atk"=> 64i64);
936 let def_rate = Dynamic::from(0.17f64);
937 assert_eq!(damage(&attacker, &def_rate), 53);
938 Ok(())
939 }
940
941 #[test]
942 fn binary_imm_promotes_integer_literals_for_float_left_values() -> anyhow::Result<()> {
943 let vm = Vm::with_all()?;
944 vm.import_code(
945 "vm_float_binary_imm",
946 br#"
947 pub fn add_f32(value: f32) {
948 value + 1i32
949 }
950
951 pub fn sub_f32(value: f32) {
952 value - 1i32
953 }
954
955 pub fn mul_f32(value: f32) {
956 value * 2i32
957 }
958
959 pub fn div_f32(value: f32) {
960 value / 2i32
961 }
962
963 pub fn gt_f32(value: f32) {
964 value > 2i32
965 }
966 "#
967 .to_vec(),
968 )?;
969
970 let compiled = vm.get_fn("vm_float_binary_imm::add_f32", &[Type::F32])?;
971 assert_eq!(compiled.ret_ty(), &Type::F32);
972 let add_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
973 assert_eq!(add_f32(2.5), 3.5);
974
975 let compiled = vm.get_fn("vm_float_binary_imm::sub_f32", &[Type::F32])?;
976 assert_eq!(compiled.ret_ty(), &Type::F32);
977 let sub_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
978 assert_eq!(sub_f32(2.5), 1.5);
979
980 let compiled = vm.get_fn("vm_float_binary_imm::mul_f32", &[Type::F32])?;
981 assert_eq!(compiled.ret_ty(), &Type::F32);
982 let mul_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
983 assert_eq!(mul_f32(2.5), 5.0);
984
985 let compiled = vm.get_fn("vm_float_binary_imm::div_f32", &[Type::F32])?;
986 assert_eq!(compiled.ret_ty(), &Type::F32);
987 let div_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
988 assert_eq!(div_f32(5.0), 2.5);
989
990 let compiled = vm.get_fn("vm_float_binary_imm::gt_f32", &[Type::F32])?;
991 assert_eq!(compiled.ret_ty(), &Type::Bool);
992 let gt_f32: extern "C" fn(f32) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
993 assert!(gt_f32(2.5));
994 assert!(!gt_f32(1.5));
995 Ok(())
996 }
997
998 #[test]
999 fn any_keys_returns_map_keys_and_empty_list_for_other_values() -> anyhow::Result<()> {
1000 let vm = Vm::with_all()?;
1001 vm.import_code(
1002 "vm_any_keys",
1003 br#"
1004 pub fn map_keys(value) {
1005 let keys = value.keys();
1006 keys.len() == 2 && keys.contains("alpha") && keys.contains("beta")
1007 }
1008
1009 pub fn non_map_keys(value) {
1010 value.keys().len() == 0
1011 }
1012 "#
1013 .to_vec(),
1014 )?;
1015
1016 let compiled = vm.get_fn("vm_any_keys::map_keys", &[Type::Any])?;
1017 assert_eq!(compiled.ret_ty(), &Type::Bool);
1018 let map_keys: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1019 let value = dynamic::map!("alpha"=> 1i64, "beta"=> 2i64);
1020 assert!(map_keys(&value));
1021
1022 let compiled = vm.get_fn("vm_any_keys::non_map_keys", &[Type::Any])?;
1023 assert_eq!(compiled.ret_ty(), &Type::Bool);
1024 let non_map_keys: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1025 let value = Dynamic::from("alpha");
1026 assert!(non_map_keys(&value));
1027 Ok(())
1028 }
1029
1030 #[test]
1031 fn any_logic_comparisons_use_bool_abi() -> anyhow::Result<()> {
1032 let vm = Vm::with_all()?;
1033 vm.import_code(
1034 "vm_any_logic_abi",
1035 br#"
1036 pub fn ne_empty(value) {
1037 value != ""
1038 }
1039
1040 pub fn eq_empty(value) {
1041 value == ""
1042 }
1043
1044 pub fn less_than_ten(value) {
1045 value < 10
1046 }
1047
1048 pub fn contains_key(value) {
1049 value.contains("alpha") == true
1050 }
1051 "#
1052 .to_vec(),
1053 )?;
1054
1055 let compiled = vm.get_fn("vm_any_logic_abi::ne_empty", &[Type::Any])?;
1056 assert_eq!(compiled.ret_ty(), &Type::Bool);
1057 let ne_empty: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1058 assert!(ne_empty(&Dynamic::from("x")));
1059 assert!(!ne_empty(&Dynamic::from("")));
1060
1061 let compiled = vm.get_fn("vm_any_logic_abi::eq_empty", &[Type::Any])?;
1062 assert_eq!(compiled.ret_ty(), &Type::Bool);
1063 let eq_empty: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1064 assert!(eq_empty(&Dynamic::from("")));
1065 assert!(!eq_empty(&Dynamic::from("x")));
1066
1067 let compiled = vm.get_fn("vm_any_logic_abi::less_than_ten", &[Type::Any])?;
1068 assert_eq!(compiled.ret_ty(), &Type::Bool);
1069 let less_than_ten: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1070 assert!(less_than_ten(&Dynamic::from(4i64)));
1071 assert!(!less_than_ten(&Dynamic::from(14i64)));
1072
1073 let compiled = vm.get_fn("vm_any_logic_abi::contains_key", &[Type::Any])?;
1074 assert_eq!(compiled.ret_ty(), &Type::Bool);
1075 let contains_key: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1076 assert!(contains_key(&dynamic::map!("alpha"=> 1i64)));
1077 assert!(!contains_key(&dynamic::map!("beta"=> 1i64)));
1078 Ok(())
1079 }
1080
1081 #[test]
1082 fn string_methods_work_on_static_string_and_any_string_values() -> anyhow::Result<()> {
1083 let vm = Vm::with_all()?;
1084 vm.import_code(
1085 "vm_string_methods",
1086 br#"
1087 pub fn static_string_methods(text: string) {
1088 let parts = text.split(",");
1089 text.starts_with("alpha")
1090 && text.is_string()
1091 && !text.is_null()
1092 && parts.len() == 2
1093 && parts.get_idx(0) == "alpha"
1094 && parts.get_idx(1) == "beta"
1095 }
1096
1097 pub fn any_string_methods(value) {
1098 let parts = value.split(",");
1099 value.starts_with("alpha")
1100 && value.is_string()
1101 && !value.is_null()
1102 && parts.len() == 2
1103 && parts.get_idx(0) == "alpha"
1104 && parts.get_idx(1) == "beta"
1105 }
1106
1107 pub fn any_null_methods(value) {
1108 value.is_null() && !value.is_string()
1109 }
1110 "#
1111 .to_vec(),
1112 )?;
1113
1114 let compiled = vm.get_fn("vm_string_methods::static_string_methods", &[Type::Str])?;
1115 assert_eq!(compiled.ret_ty(), &Type::Bool);
1116 let static_string_methods: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1117 let text = Dynamic::from("alpha,beta");
1118 assert!(static_string_methods(&text));
1119
1120 let compiled = vm.get_fn("vm_string_methods::any_string_methods", &[Type::Any])?;
1121 assert_eq!(compiled.ret_ty(), &Type::Bool);
1122 let any_string_methods: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1123 assert!(any_string_methods(&text));
1124
1125 let compiled = vm.get_fn("vm_string_methods::any_null_methods", &[Type::Any])?;
1126 assert_eq!(compiled.ret_ty(), &Type::Bool);
1127 let any_null_methods: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1128 let value = Dynamic::Null;
1129 assert!(any_null_methods(&value));
1130 Ok(())
1131 }
1132
1133 #[test]
1134 fn static_string_add_uses_direct_strcat() -> anyhow::Result<()> {
1135 let vm = Vm::with_all()?;
1136 vm.import_code(
1137 "vm_static_strcat",
1138 br#"
1139 pub fn join(left: string, right: string) {
1140 left + right
1141 }
1142
1143 pub fn suffix(left: string) {
1144 left + "-tail"
1145 }
1146
1147 pub fn append_local() {
1148 let text: string = "alpha";
1149 text += "-beta";
1150 text += "-tail";
1151 text
1152 }
1153
1154 pub fn append_local_assign() {
1155 let text: string = "alpha";
1156 text = text + "-beta";
1157 text = text + "-tail";
1158 text
1159 }
1160
1161 pub fn append_arg(text: string) {
1162 text += "-tail";
1163 text
1164 }
1165
1166 pub fn append_arg_assign(text: string) {
1167 text = text + "-tail";
1168 text
1169 }
1170
1171 pub fn append_any(value) {
1172 value += "-tail";
1173 value
1174 }
1175
1176 pub fn add_sub_assign_form() {
1177 let x = 10i64;
1178 x = x + 1i64;
1179 x = x - 2i64;
1180 x
1181 }
1182 "#
1183 .to_vec(),
1184 )?;
1185
1186 let compiled = vm.get_fn("vm_static_strcat::join", &[Type::Str, Type::Str])?;
1187 assert_eq!(compiled.ret_ty(), &Type::Str);
1188 let join: extern "C" fn(*const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1189 let left = Dynamic::from("alpha");
1190 let right = Dynamic::from("-beta");
1191 let result = unsafe { &*join(&left, &right) };
1192 assert!(matches!(result, Dynamic::StringBuf(_)));
1193 assert_eq!(result.as_str(), "alpha-beta");
1194
1195 let compiled = vm.get_fn("vm_static_strcat::suffix", &[Type::Str])?;
1196 assert_eq!(compiled.ret_ty(), &Type::Str);
1197 let suffix: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1198 let result = unsafe { &*suffix(&left) };
1199 assert!(matches!(result, Dynamic::StringBuf(_)));
1200 assert_eq!(result.as_str(), "alpha-tail");
1201
1202 let compiled = vm.get_fn("vm_static_strcat::append_local", &[])?;
1203 assert_eq!(compiled.ret_ty(), &Type::Str);
1204 let append_local: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1205 let result = unsafe { &*append_local() };
1206 assert!(matches!(result, Dynamic::StringBuf(_)));
1207 assert_eq!(result.as_str(), "alpha-beta-tail");
1208
1209 let compiled = vm.get_fn("vm_static_strcat::append_local_assign", &[])?;
1210 assert_eq!(compiled.ret_ty(), &Type::Str);
1211 let append_local_assign: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1212 let result = unsafe { &*append_local_assign() };
1213 assert!(matches!(result, Dynamic::StringBuf(_)));
1214 assert_eq!(result.as_str(), "alpha-beta-tail");
1215
1216 let compiled = vm.get_fn("vm_static_strcat::append_arg", &[Type::Str])?;
1217 assert_eq!(compiled.ret_ty(), &Type::Str);
1218 let append_arg: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1219 let input = Dynamic::from("alpha");
1220 let result = unsafe { &*append_arg(&input) };
1221 assert_eq!(result.as_str(), "alpha-tail");
1222 assert_eq!(input.as_str(), "alpha");
1223
1224 let compiled = vm.get_fn("vm_static_strcat::append_arg_assign", &[Type::Str])?;
1225 assert_eq!(compiled.ret_ty(), &Type::Str);
1226 let append_arg_assign: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1227 let input = Dynamic::from("alpha");
1228 let result = unsafe { &*append_arg_assign(&input) };
1229 assert_eq!(result.as_str(), "alpha-tail");
1230 assert_eq!(input.as_str(), "alpha");
1231
1232 let compiled = vm.get_fn("vm_static_strcat::append_any", &[Type::Any])?;
1233 assert_eq!(compiled.ret_ty(), &Type::Str);
1234 let append_any: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1235 let input = Dynamic::from("alpha");
1236 let result = unsafe { &*append_any(&input) };
1237 assert_eq!(result.as_str(), "alpha-tail");
1238 assert_eq!(input.as_str(), "alpha");
1239
1240 let compiled = vm.get_fn("vm_static_strcat::add_sub_assign_form", &[])?;
1241 assert_eq!(compiled.ret_ty(), &Type::I64);
1242 let add_sub_assign_form: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1243 assert_eq!(add_sub_assign_form(), 9);
1244 Ok(())
1245 }
1246
1247 #[test]
1248 fn primitive_type_check_methods_call_any_runtime() -> anyhow::Result<()> {
1249 let vm = Vm::with_all()?;
1250 vm.import_code(
1251 "vm_primitive_type_check_methods",
1252 br#"
1253 pub fn int_checks() {
1254 !42i64.is_list()
1255 && !42i64.is_map()
1256 && !42i64.is_string()
1257 && !42i64.is_null()
1258 }
1259
1260 pub fn bool_checks() {
1261 !true.is_list() && !true.is_map() && !true.is_null()
1262 }
1263 "#
1264 .to_vec(),
1265 )?;
1266
1267 let compiled = vm.get_fn("vm_primitive_type_check_methods::int_checks", &[])?;
1268 assert_eq!(compiled.ret_ty(), &Type::Bool);
1269 let int_checks: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1270 assert!(int_checks());
1271
1272 let compiled = vm.get_fn("vm_primitive_type_check_methods::bool_checks", &[])?;
1273 assert_eq!(compiled.ret_ty(), &Type::Bool);
1274 let bool_checks: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1275 assert!(bool_checks());
1276 Ok(())
1277 }
1278
1279 #[test]
1280 fn for_loop_iterates_any_list_and_map_values() -> anyhow::Result<()> {
1281 let vm = Vm::with_all()?;
1282 vm.import_code(
1283 "vm_for_any_collections",
1284 br#"
1285 pub fn list_sum(items) {
1286 let total = 0i64;
1287 for item in items {
1288 total += item;
1289 }
1290 total
1291 }
1292
1293 pub fn map_sum(data) {
1294 let total = 0i64;
1295 for (key, value) in data {
1296 total += value;
1297 }
1298 total
1299 }
1300 "#
1301 .to_vec(),
1302 )?;
1303
1304 let compiled = vm.get_fn("vm_for_any_collections::list_sum", &[Type::Any])?;
1305 assert_eq!(compiled.ret_ty(), &Type::I64);
1306 let list_sum: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1307 let items = Dynamic::list(vec![1i64.into(), 2i64.into(), 3i64.into()]);
1308 assert_eq!(list_sum(&items), 6);
1309
1310 let compiled = vm.get_fn("vm_for_any_collections::map_sum", &[Type::Any])?;
1311 assert_eq!(compiled.ret_ty(), &Type::I64);
1312 let map_sum: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1313 let data = dynamic::map!("a"=> 4i64, "b"=> 5i64);
1314 assert_eq!(map_sum(&data), 9);
1315 Ok(())
1316 }
1317
1318 #[test]
1319 fn compares_concrete_value_with_string_literal_as_string() -> anyhow::Result<()> {
1320 let vm = Vm::with_all()?;
1321 vm.import_code(
1322 "vm_string_compare_imm",
1323 br#"
1324 pub fn int_eq_str(value: i64) {
1325 value == "42"
1326 }
1327
1328 pub fn int_to_str(value: i64) {
1329 value + ""
1330 }
1331 "#
1332 .to_vec(),
1333 )?;
1334
1335 let compiled = vm.get_fn("vm_string_compare_imm::int_eq_str", &[Type::I64])?;
1336 assert_eq!(compiled.ret_ty(), &Type::Bool);
1337
1338 let int_eq_str: extern "C" fn(i64) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1339
1340 let compiled = vm.get_fn("vm_string_compare_imm::int_to_str", &[Type::I64])?;
1341 assert_eq!(compiled.ret_ty(), &Type::Str);
1342 let int_to_str: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1343 let text = int_to_str(42);
1344 assert_eq!(unsafe { &*text }.as_str(), "42");
1345
1346 assert!(int_eq_str(42));
1347 assert!(!int_eq_str(7));
1348 Ok(())
1349 }
1350
1351 #[test]
1352 fn concatenates_string_with_integer_values() -> anyhow::Result<()> {
1353 let vm = Vm::with_all()?;
1354 vm.import_code(
1355 "vm_string_concat_integer",
1356 br#"
1357 pub fn idx_key(idx: i64) {
1358 "" + idx
1359 }
1360
1361 pub fn level_text(level: i64) {
1362 "" + level + " level"
1363 }
1364
1365 pub fn gold_text(currency) {
1366 "" + currency.gold
1367 }
1368 "#
1369 .to_vec(),
1370 )?;
1371
1372 let compiled = vm.get_fn("vm_string_concat_integer::idx_key", &[Type::I64])?;
1373 let idx_key: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1374 let result = unsafe { &*idx_key(7) };
1375 assert!(matches!(result, Dynamic::StringBuf(_)));
1376 assert_eq!(result.as_str(), "7");
1377
1378 let compiled = vm.get_fn("vm_string_concat_integer::level_text", &[Type::I64])?;
1379 let level_text: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1380 let result = unsafe { &*level_text(12) };
1381 assert_eq!(result.as_str(), "12 level");
1382
1383 let compiled = vm.get_fn("vm_string_concat_integer::gold_text", &[Type::Any])?;
1384 let gold_text: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1385 let currency = dynamic::map!("gold"=> 345i64);
1386 let result = unsafe { &*gold_text(¤cy) };
1387 assert_eq!(result.as_str(), "345");
1388 Ok(())
1389 }
1390
1391 #[test]
1392 fn coerces_string_concat_to_i64_without_unimplemented_log() -> anyhow::Result<()> {
1393 let vm = Vm::with_all()?;
1394 vm.import_code(
1395 "vm_string_concat_to_i64",
1396 br#"
1397 pub fn run(idx: i64) {
1398 ("" + idx) as i64
1399 }
1400 "#
1401 .to_vec(),
1402 )?;
1403
1404 let compiled = vm.get_fn("vm_string_concat_to_i64::run", &[Type::I64])?;
1405 assert_eq!(compiled.ret_ty(), &Type::I64);
1406 let run: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1407 assert_eq!(run(7), 7);
1408 Ok(())
1409 }
1410
1411 #[test]
1412 fn casts_dynamic_string_numbers_to_ints_and_floats() -> anyhow::Result<()> {
1413 let vm = Vm::with_all()?;
1414 vm.import_code(
1415 "vm_string_number_casts",
1416 br#"
1417 pub fn limit_i64(req) {
1418 req["@query"].limit as i64
1419 }
1420
1421 pub fn limit_i32(req) {
1422 req["@query"].limit as i32
1423 }
1424
1425 pub fn price_f64(req) {
1426 req["@query"].price as f64
1427 }
1428
1429 pub fn price_f32(req) {
1430 req["@query"].price as f32
1431 }
1432
1433 pub fn literal_i64() {
1434 "42" as i64
1435 }
1436
1437 pub fn literal_f64() {
1438 "3.5" as f64
1439 }
1440
1441 pub fn bad_number(req) {
1442 req["@query"].bad as i64
1443 }
1444 "#
1445 .to_vec(),
1446 )?;
1447
1448 let req = dynamic::map!("@query"=> dynamic::map!("limit"=> "50", "price"=> "3.5", "bad"=> "nope"));
1449
1450 let limit_i64 = vm.get_fn("vm_string_number_casts::limit_i64", &[Type::Any])?;
1451 assert_eq!(limit_i64.ret_ty(), &Type::I64);
1452 let limit_i64: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(limit_i64.ptr()) };
1453 assert_eq!(limit_i64(&req), 50);
1454
1455 let limit_i32 = vm.get_fn("vm_string_number_casts::limit_i32", &[Type::Any])?;
1456 assert_eq!(limit_i32.ret_ty(), &Type::I32);
1457 let limit_i32: extern "C" fn(*const Dynamic) -> i32 = unsafe { std::mem::transmute(limit_i32.ptr()) };
1458 assert_eq!(limit_i32(&req), 50);
1459
1460 let price_f64 = vm.get_fn("vm_string_number_casts::price_f64", &[Type::Any])?;
1461 assert_eq!(price_f64.ret_ty(), &Type::F64);
1462 let price_f64: extern "C" fn(*const Dynamic) -> f64 = unsafe { std::mem::transmute(price_f64.ptr()) };
1463 assert_eq!(price_f64(&req), 3.5);
1464
1465 let price_f32 = vm.get_fn("vm_string_number_casts::price_f32", &[Type::Any])?;
1466 assert_eq!(price_f32.ret_ty(), &Type::F32);
1467 let price_f32: extern "C" fn(*const Dynamic) -> f32 = unsafe { std::mem::transmute(price_f32.ptr()) };
1468 assert_eq!(price_f32(&req), 3.5);
1469
1470 let literal_i64 = vm.get_fn("vm_string_number_casts::literal_i64", &[])?;
1471 assert_eq!(literal_i64.ret_ty(), &Type::I64);
1472 let literal_i64: extern "C" fn() -> i64 = unsafe { std::mem::transmute(literal_i64.ptr()) };
1473 assert_eq!(literal_i64(), 42);
1474
1475 let literal_f64 = vm.get_fn("vm_string_number_casts::literal_f64", &[])?;
1476 assert_eq!(literal_f64.ret_ty(), &Type::F64);
1477 let literal_f64: extern "C" fn() -> f64 = unsafe { std::mem::transmute(literal_f64.ptr()) };
1478 assert_eq!(literal_f64(), 3.5);
1479
1480 let bad_number = vm.get_fn("vm_string_number_casts::bad_number", &[Type::Any])?;
1481 assert_eq!(bad_number.ret_ty(), &Type::I64);
1482 let bad_number: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(bad_number.ptr()) };
1483 assert_eq!(bad_number(&req), 0);
1484 Ok(())
1485 }
1486
1487 #[test]
1488 fn unifies_explicit_return_and_tail_integer_widths() -> anyhow::Result<()> {
1489 let vm = Vm::with_all()?;
1490 vm.import_code(
1491 "vm_return_integer_widths",
1492 br#"
1493 pub fn selected(flag, slot) {
1494 if flag {
1495 return slot;
1496 }
1497 0
1498 }
1499 "#
1500 .to_vec(),
1501 )?;
1502
1503 let compiled = vm.get_fn("vm_return_integer_widths::selected", &[Type::Bool, Type::I64])?;
1504 assert_eq!(compiled.ret_ty(), &Type::I64);
1505 let selected: extern "C" fn(bool, i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1506
1507 assert_eq!(selected(true, 7), 7);
1508 assert_eq!(selected(false, 7), 0);
1509 Ok(())
1510 }
1511
1512 #[test]
1513 fn root_contains_string_concat_is_bool_condition() -> anyhow::Result<()> {
1514 let vm = Vm::with_all()?;
1515 vm.import_code(
1516 "vm_root_contains_condition",
1517 br#"
1518 pub fn exists(user_id) {
1519 if root::contains("redis/user/" + user_id) {
1520 return 1;
1521 }
1522 0
1523 }
1524 "#
1525 .to_vec(),
1526 )?;
1527
1528 assert_eq!(vm.infer("root::contains", &[Type::Any])?, Type::Bool);
1529 let compiled = vm.get_fn("vm_root_contains_condition::exists", &[Type::Any])?;
1530 assert_eq!(compiled.ret_ty(), &Type::I64);
1531 Ok(())
1532 }
1533
1534 #[test]
1535 fn root_add_map_can_be_printed() -> anyhow::Result<()> {
1536 let vm = Vm::with_all()?;
1537 assert_eq!(vm.infer("root::add_map", &[Type::Any])?, Type::Bool);
1538 vm.import_code(
1539 "vm_root_add_map_print",
1540 br#"
1541 pub fn run() {
1542 print(root::add_map("local/world_handlers/til_map_novicevillage"));
1543 }
1544 "#
1545 .to_vec(),
1546 )?;
1547
1548 let compiled = vm.get_fn("vm_root_add_map_print::run", &[])?;
1549 assert!(compiled.ret_ty().is_void());
1550 Ok(())
1551 }
1552
1553 #[test]
1554 fn std_log_accepts_any_and_returns_void() -> anyhow::Result<()> {
1555 let vm = Vm::with_all()?;
1556 vm.import_code(
1557 "vm_std_log",
1558 br#"
1559 pub fn run(value) {
1560 log({ ok: true, value: value });
1561 }
1562 "#
1563 .to_vec(),
1564 )?;
1565
1566 let compiled = vm.get_fn("vm_std_log::run", &[Type::Any])?;
1567 assert!(compiled.ret_ty().is_void());
1568 let run: extern "C" fn(*const Dynamic) = unsafe { std::mem::transmute(compiled.ptr()) };
1569 let value = Dynamic::from(7i64);
1570 run(&value);
1571 Ok(())
1572 }
1573
1574 #[test]
1575 fn unary_not_any_loop_var_is_bool_condition() -> anyhow::Result<()> {
1576 let vm = Vm::with_all()?;
1577 vm.import_code(
1578 "vm_unary_not_any_loop_var",
1579 br#"
1580 pub fn count_missing(flags) {
1581 let missing = 0;
1582 for exists in flags {
1583 if !exists {
1584 missing = missing + 1;
1585 }
1586 }
1587 missing
1588 }
1589 "#
1590 .to_vec(),
1591 )?;
1592
1593 let compiled = vm.get_fn("vm_unary_not_any_loop_var::count_missing", &[Type::Any])?;
1594 assert_eq!(compiled.ret_ty(), &Type::I64);
1595 Ok(())
1596 }
1597
1598 #[test]
1599 fn closure_literal_can_be_called_immediately() -> anyhow::Result<()> {
1600 let vm = Vm::with_all()?;
1601 vm.import_code(
1602 "vm_closure_immediate_call",
1603 br#"
1604 pub fn no_args() {
1605 let r = || { 1i32 }();
1606 r
1607 }
1608
1609 pub fn with_arg() {
1610 |value: i32| { value + 1i32 }(2i32)
1611 }
1612 "#
1613 .to_vec(),
1614 )?;
1615
1616 let compiled = vm.get_fn("vm_closure_immediate_call::no_args", &[])?;
1617 assert_eq!(compiled.ret_ty(), &Type::I32);
1618 let no_args: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
1619 assert_eq!(no_args(), 1);
1620
1621 let compiled = vm.get_fn("vm_closure_immediate_call::with_arg", &[])?;
1622 assert_eq!(compiled.ret_ty(), &Type::I32);
1623 let with_arg: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
1624 assert_eq!(with_arg(), 3);
1625 Ok(())
1626 }
1627
1628 #[test]
1629 fn nested_closure_captures_outer_closure_arg() -> anyhow::Result<()> {
1630 let vm = Vm::with_all()?;
1631 vm.import_code(
1632 "vm_nested_closure_capture",
1633 br#"
1634 pub fn run() {
1635 let reference_label = "reference";
1636 |path: string| {
1637 let upload_done = |uploaded: bool| {
1638 if uploaded {
1639 reference_label + ":" + path
1640 } else {
1641 "missing"
1642 }
1643 };
1644 upload_done(true)
1645 }("reference.png")
1646 }
1647 "#
1648 .to_vec(),
1649 )?;
1650
1651 let compiled = vm.get_fn("vm_nested_closure_capture::run", &[])?;
1652 assert_eq!(compiled.ret_ty(), &Type::Any);
1653 let run: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1654 let result = unsafe { &*run() };
1655 assert_eq!(result.as_str(), "reference:reference.png");
1656 Ok(())
1657 }
1658
1659 #[test]
1660 fn semicolon_tail_call_makes_function_void() -> anyhow::Result<()> {
1661 let vm = Vm::with_all()?;
1662 vm.import_code(
1663 "vm_semicolon_tail_void",
1664 br#"
1665 pub fn send_role_select(idx, account_id, selected_slot) {
1666 root::send("local/ui/send_dialog", {
1667 idx: idx,
1668 account_id: account_id,
1669 selected_slot: selected_slot
1670 });
1671 }
1672 "#
1673 .to_vec(),
1674 )?;
1675
1676 let compiled = vm.get_fn("vm_semicolon_tail_void::send_role_select", &[Type::Any, Type::Any, Type::Any])?;
1677 assert_eq!(compiled.ret_ty(), &Type::Void);
1678 Ok(())
1679 }
1680
1681 #[test]
1682 fn bare_return_conflicts_with_non_void_return() -> anyhow::Result<()> {
1683 let vm = Vm::with_all()?;
1684 vm.import_code(
1685 "vm_bare_return_conflict",
1686 br#"
1687 pub fn run(flag) {
1688 if flag {
1689 return;
1690 }
1691 1
1692 }
1693 "#
1694 .to_vec(),
1695 )?;
1696
1697 let err = match vm.get_fn("vm_bare_return_conflict::run", &[Type::Bool]) {
1698 Ok(_) => panic!("expected mismatched return types to fail"),
1699 Err(err) => err,
1700 };
1701 assert!(format!("{err:#}").contains("返回类型不一致"));
1702 Ok(())
1703 }
1704
1705 #[test]
1706 fn root_get_accepts_string_concat_with_dynamic_field() -> anyhow::Result<()> {
1707 let vm = Vm::with_all()?;
1708 vm.import_code(
1709 "vm_root_get_dynamic_concat",
1710 br#"
1711 pub fn get_action(req) {
1712 root::get("local/game/panel_actions/" + req.idx)
1713 }
1714 "#
1715 .to_vec(),
1716 )?;
1717
1718 root::add("local/game/panel_actions/7", dynamic::map!("id"=> "action-7").into())?;
1719 let compiled = vm.get_fn("vm_root_get_dynamic_concat::get_action", &[Type::Any])?;
1720 assert_eq!(compiled.ret_ty(), &Type::Any);
1721 let get_action: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1722 let req = dynamic::map!("idx"=> 7i64);
1723 let result = unsafe { &*get_action(&req) };
1724
1725 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("action-7".to_string()));
1726 Ok(())
1727 }
1728
1729 #[test]
1730 fn root_add_fn_registers_handler_with_dynamic_field_path_concat() -> anyhow::Result<()> {
1731 let vm = Vm::with_all()?;
1732 vm.import_code(
1733 "vm_registered_panel_action",
1734 br#"
1735 pub fn panel_action(req) {
1736 root::get("local/game/panel_actions/" + req.idx)
1737 }
1738
1739 pub fn register() {
1740 root::add_fn("local/ui/panel_action", "vm_registered_panel_action::panel_action")
1741 }
1742 "#
1743 .to_vec(),
1744 )?;
1745
1746 let compiled = vm.get_fn("vm_registered_panel_action::register", &[])?;
1747 assert_eq!(compiled.ret_ty(), &Type::Bool);
1748 let register: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1749 assert!(register());
1750 Ok(())
1751 }
1752
1753 #[test]
1754 fn std_spawn_runs_named_function_with_tuple_args() -> anyhow::Result<()> {
1755 let zero_path = "local/vm_std_spawn/zero";
1756 let sum_path = "local/vm_std_spawn/sum";
1757 let closure_path = "local/vm_std_spawn/closure";
1758 let closure_vars_path = "local/vm_std_spawn/closure_vars";
1759 let _ = root::remove(zero_path);
1760 let _ = root::remove(sum_path);
1761 let _ = root::remove(closure_path);
1762 let _ = root::remove(closure_vars_path);
1763 let vm = Vm::with_all()?;
1764 vm.import_code(
1765 "vm_std_spawn",
1766 br#"
1767 pub fn zero() {
1768 root::add("local/vm_std_spawn/zero", 1);
1769 }
1770
1771 pub fn job(left, right) {
1772 root::add("local/vm_std_spawn/sum", left + right);
1773 }
1774
1775 pub fn start_zero() {
1776 spawn("vm_std_spawn::zero", ())
1777 }
1778
1779 pub fn start_sum() {
1780 spawn("vm_std_spawn::job", (10, 20))
1781 }
1782
1783 pub fn start_closure() {
1784 spawn(|x, y| {
1785 root::add("local/vm_std_spawn/closure", x + y);
1786 }, (3, 4))
1787 }
1788
1789 pub fn start_closure_vars() {
1790 let x = 5;
1791 let y = 6;
1792 spawn(|left, right| {
1793 root::add("local/vm_std_spawn/closure_vars", left + right);
1794 }, (x, y))
1795 }
1796 "#
1797 .to_vec(),
1798 )?;
1799
1800 let compiled = vm.get_fn("vm_std_spawn::start_zero", &[])?;
1801 assert_eq!(compiled.ret_ty(), &Type::Bool);
1802 let start_zero: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1803 assert!(start_zero());
1804
1805 let compiled = vm.get_fn("vm_std_spawn::start_sum", &[])?;
1806 assert_eq!(compiled.ret_ty(), &Type::Bool);
1807 let start_sum: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1808 assert!(start_sum());
1809
1810 let compiled = vm.get_fn("vm_std_spawn::start_closure", &[])?;
1811 assert_eq!(compiled.ret_ty(), &Type::Bool);
1812 let start_closure: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1813 assert!(start_closure());
1814
1815 let compiled = vm.get_fn("vm_std_spawn::start_closure_vars", &[])?;
1816 assert_eq!(compiled.ret_ty(), &Type::Bool);
1817 let start_closure_vars: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1818 assert!(start_closure_vars());
1819
1820 for _ in 0..50 {
1821 let zero_done = root::get(zero_path).ok().and_then(|value| value.as_int()) == Some(1);
1822 let sum_done = root::get(sum_path).ok().and_then(|value| value.as_int()) == Some(30);
1823 let closure_done = root::get(closure_path).ok().and_then(|value| value.as_int()) == Some(7);
1824 let closure_vars_done = root::get(closure_vars_path).ok().and_then(|value| value.as_int()) == Some(11);
1825 if zero_done && sum_done && closure_done && closure_vars_done {
1826 return Ok(());
1827 }
1828 std::thread::sleep(std::time::Duration::from_millis(10));
1829 }
1830
1831 anyhow::bail!("spawned jobs did not write expected results");
1832 }
1833
1834 #[test]
1835 fn native_can_save_and_later_call_closure_callback() -> anyhow::Result<()> {
1836 static SAVED_CALLBACK: parking_lot::Mutex<Option<ZustCallback>> = parking_lot::Mutex::new(None);
1837
1838 extern "C" fn save_callback(callback: *const Dynamic) -> bool {
1839 if callback.is_null() {
1840 return false;
1841 }
1842 let Some(callback) = (unsafe { &*callback }).as_custom::<ZustCallback>().cloned() else {
1843 return false;
1844 };
1845 *SAVED_CALLBACK.lock() = Some(callback);
1846 true
1847 }
1848
1849 let path = "local/vm_callback/result";
1850 let _ = root::remove(path);
1851 *SAVED_CALLBACK.lock() = None;
1852
1853 let vm = Vm::with_all()?;
1854 vm.add_native_module_ptr("callback_test", "save", &[Type::Any], Type::Bool, save_callback as *const u8)?;
1855 vm.import_code(
1856 "vm_callback",
1857 br#"
1858 pub fn register() {
1859 let n = 41;
1860 callback_test::save(|| {
1861 root::add("local/vm_callback/result", n + 1);
1862 true
1863 })
1864 }
1865 "#
1866 .to_vec(),
1867 )?;
1868
1869 let compiled = vm.get_fn("vm_callback::register", &[])?;
1870 assert_eq!(compiled.ret_ty(), &Type::Bool);
1871 let register: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1872 assert!(register());
1873 assert!(root::get(path).is_err());
1874
1875 let callback = SAVED_CALLBACK.lock().clone().expect("callback should be saved");
1876 let result = callback.call0()?;
1877 assert_eq!(result.as_bool(), Some(true));
1878 assert_eq!(root::get(path)?.as_int(), Some(42));
1879 Ok(())
1880 }
1881
1882 #[test]
1883 fn closure_captures_share_state_between_callbacks() -> anyhow::Result<()> {
1884 static SAVED_CALLBACKS: parking_lot::Mutex<Vec<ZustCallback>> = parking_lot::Mutex::new(Vec::new());
1885
1886 extern "C" fn save_callback(callback: *const Dynamic) -> bool {
1887 if callback.is_null() {
1888 return false;
1889 }
1890 let Some(callback) = (unsafe { &*callback }).as_custom::<ZustCallback>().cloned() else {
1891 return false;
1892 };
1893 SAVED_CALLBACKS.lock().push(callback);
1894 true
1895 }
1896
1897 SAVED_CALLBACKS.lock().clear();
1898
1899 let vm = Vm::with_all()?;
1900 vm.add_native_module_ptr("capture_test", "save", &[Type::Any], Type::Bool, save_callback as *const u8)?;
1901 vm.import_code(
1902 "vm_shared_capture",
1903 br#"
1904 pub fn register() {
1905 let state = {};
1906 state.drag_kind = 0;
1907 capture_test::save(|| {
1908 state.drag_kind = 2;
1909 true
1910 });
1911 capture_test::save(|| {
1912 state.drag_kind
1913 })
1914 }
1915 "#
1916 .to_vec(),
1917 )?;
1918
1919 let register = vm.get_fn("vm_shared_capture::register", &[])?;
1920 let register: extern "C" fn() -> bool = unsafe { std::mem::transmute(register.ptr()) };
1921 assert!(register());
1922
1923 let (writer, reader) = {
1924 let saved = SAVED_CALLBACKS.lock();
1925 assert_eq!(saved.len(), 2);
1926 (saved[0].clone(), saved[1].clone())
1927 };
1928 assert_eq!(reader.call0()?.as_int(), Some(0));
1929 assert_eq!(writer.call0()?.as_bool(), Some(true));
1930 assert_eq!(reader.call0()?.as_int(), Some(2));
1931 Ok(())
1932 }
1933
1934 #[test]
1935 fn native_can_save_and_later_call_named_function_callback() -> anyhow::Result<()> {
1936 static SAVED_CALLBACK: parking_lot::Mutex<Option<ZustCallback>> = parking_lot::Mutex::new(None);
1937
1938 extern "C" fn save_callback(callback: *const Dynamic) -> bool {
1939 if callback.is_null() {
1940 return false;
1941 }
1942 let Some(callback) = (unsafe { &*callback }).as_custom::<ZustCallback>().cloned() else {
1943 return false;
1944 };
1945 *SAVED_CALLBACK.lock() = Some(callback);
1946 true
1947 }
1948
1949 let path = "local/vm_named_callback/result";
1950 let _ = root::remove(path);
1951 *SAVED_CALLBACK.lock() = None;
1952
1953 let vm = Vm::with_all()?;
1954 vm.add_native_module_ptr("callback_test", "save", &[Type::Any], Type::Bool, save_callback as *const u8)?;
1955 vm.import_code(
1956 "vm_named_callback",
1957 br#"
1958 pub fn on_result() {
1959 root::add("local/vm_named_callback/result", "done");
1960 true
1961 }
1962
1963 pub fn register() {
1964 callback_test::save(on_result)
1965 }
1966 "#
1967 .to_vec(),
1968 )?;
1969
1970 let register = vm.get_fn("vm_named_callback::register", &[])?;
1971 let register: extern "C" fn() -> bool = unsafe { std::mem::transmute(register.ptr()) };
1972 assert!(register());
1973 assert!(root::get(path).is_err());
1974
1975 let callback = SAVED_CALLBACK.lock().clone().expect("callback should be saved");
1976 assert_eq!(callback.call1(dynamic::map!("text"=> "done"))?.as_bool(), Some(true));
1977 assert_eq!(root::get(path)?.as_str(), "done");
1978 Ok(())
1979 }
1980
1981 #[test]
1982 fn native_callback_can_receive_later_dynamic_args() -> anyhow::Result<()> {
1983 static SAVED_PATH_CALLBACK: parking_lot::Mutex<Option<ZustCallback>> = parking_lot::Mutex::new(None);
1984 static SAVED_SUM_CALLBACK: parking_lot::Mutex<Option<ZustCallback>> = parking_lot::Mutex::new(None);
1985
1986 extern "C" fn save_path_callback(callback: *const Dynamic) -> bool {
1987 if callback.is_null() {
1988 return false;
1989 }
1990 let Some(callback) = (unsafe { &*callback }).as_custom::<ZustCallback>().cloned() else {
1991 return false;
1992 };
1993 *SAVED_PATH_CALLBACK.lock() = Some(callback);
1994 true
1995 }
1996
1997 extern "C" fn save_sum_callback(callback: *const Dynamic) -> bool {
1998 if callback.is_null() {
1999 return false;
2000 }
2001 let Some(callback) = (unsafe { &*callback }).as_custom::<ZustCallback>().cloned() else {
2002 return false;
2003 };
2004 *SAVED_SUM_CALLBACK.lock() = Some(callback);
2005 true
2006 }
2007
2008 let path_result = "local/vm_callback/path";
2009 let sum_result = "local/vm_callback/sum8";
2010 let _ = root::remove(path_result);
2011 let _ = root::remove(sum_result);
2012 *SAVED_PATH_CALLBACK.lock() = None;
2013 *SAVED_SUM_CALLBACK.lock() = None;
2014
2015 let vm = Vm::with_all()?;
2016 vm.add_native_module_ptr("callback_test", "save_path", &[Type::Any], Type::Bool, save_path_callback as *const u8)?;
2017 vm.add_native_module_ptr("callback_test", "save_sum", &[Type::Any], Type::Bool, save_sum_callback as *const u8)?;
2018 vm.import_code(
2019 "vm_callback_args",
2020 br#"
2021 pub fn register_path() {
2022 let key = "local/vm_callback/path";
2023 callback_test::save_path(|path| {
2024 root::add(key, path);
2025 true
2026 })
2027 }
2028
2029 pub fn register_sum() {
2030 callback_test::save_sum(|a, b, c, d, e, f, g, h| {
2031 root::add("local/vm_callback/sum8", a + b + c + d + e + f + g + h);
2032 true
2033 })
2034 }
2035 "#
2036 .to_vec(),
2037 )?;
2038
2039 let register_path = vm.get_fn("vm_callback_args::register_path", &[])?;
2040 let register_path: extern "C" fn() -> bool = unsafe { std::mem::transmute(register_path.ptr()) };
2041 assert!(register_path());
2042
2043 let register_sum = vm.get_fn("vm_callback_args::register_sum", &[])?;
2044 let register_sum: extern "C" fn() -> bool = unsafe { std::mem::transmute(register_sum.ptr()) };
2045 assert!(register_sum());
2046
2047 let path_callback = SAVED_PATH_CALLBACK.lock().clone().expect("path callback should be saved");
2048 assert_eq!(path_callback.call1(Dynamic::from("picked.txt"))?.as_bool(), Some(true));
2049 assert_eq!(root::get(path_result)?.as_str(), "picked.txt");
2050
2051 let sum_callback = SAVED_SUM_CALLBACK.lock().clone().expect("sum callback should be saved");
2052 let sum_args = (1i64..=8).map(Dynamic::from).collect();
2053 assert_eq!(sum_callback.call(sum_args)?.as_bool(), Some(true));
2054 assert_eq!(root::get(sum_result)?.as_int(), Some(36));
2055 Ok(())
2056 }
2057
2058 #[test]
2059 fn callback_with_16_explicit_args_and_captures() -> anyhow::Result<()> {
2060 static SAVED_SUM16: parking_lot::Mutex<Option<ZustCallback>> = parking_lot::Mutex::new(None);
2061
2062 extern "C" fn save_sum16(callback: *const Dynamic) -> bool {
2063 if callback.is_null() {
2064 return false;
2065 }
2066 let Some(callback) = (unsafe { &*callback }).as_custom::<ZustCallback>().cloned() else {
2067 return false;
2068 };
2069 *SAVED_SUM16.lock() = Some(callback);
2070 true
2071 }
2072
2073 let sum16_path = "local/vm_callback/sum16";
2074 let _ = root::remove(sum16_path);
2075 *SAVED_SUM16.lock() = None;
2076
2077 let vm = Vm::with_all()?;
2078 vm.add_native_module_ptr("callback_test", "save_sum16", &[Type::Any], Type::Bool, save_sum16 as *const u8)?;
2079 vm.import_code(
2080 "vm_callback_16_args",
2081 br#"
2082 pub fn register_sum16() {
2083 let prefix = "sum=";
2084 callback_test::save_sum16(|a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p| {
2085 let total = a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p;
2086 root::add("local/vm_callback/sum16", prefix + total);
2087 true
2088 })
2089 }
2090 "#
2091 .to_vec(),
2092 )?;
2093
2094 let register = vm.get_fn("vm_callback_16_args::register_sum16", &[])?;
2095 let register: extern "C" fn() -> bool = unsafe { std::mem::transmute(register.ptr()) };
2096 assert!(register());
2097
2098 let callback = SAVED_SUM16.lock().clone().expect("sum16 callback saved");
2099 let args: Vec<Dynamic> = (1i64..=16).map(Dynamic::from).collect();
2100 assert_eq!(callback.call(args)?.as_bool(), Some(true));
2101 assert_eq!(root::get(sum16_path)?.as_str(), "sum=136");
2102 Ok(())
2103 }
2104
2105 #[test]
2106 fn spawn_closure_with_16_args() -> anyhow::Result<()> {
2107 let spawn16_path = "local/vm_spawn/spawn16";
2108 let _ = root::remove(spawn16_path);
2109
2110 let vm = Vm::with_all()?;
2111 vm.import_code(
2112 "vm_spawn_16_args",
2113 br#"
2114 pub fn start_spawn16() {
2115 spawn(|a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p| {
2116 root::add("local/vm_spawn/spawn16", a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p);
2117 }, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16))
2118 }
2119 "#
2120 .to_vec(),
2121 )?;
2122
2123 let compiled = vm.get_fn("vm_spawn_16_args::start_spawn16", &[])?;
2124 assert_eq!(compiled.ret_ty(), &Type::Bool);
2125 let start: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2126 assert!(start());
2127
2128 for _ in 0..50 {
2129 if root::get(spawn16_path).ok().and_then(|v| v.as_int()) == Some(136) {
2130 return Ok(());
2131 }
2132 std::thread::sleep(std::time::Duration::from_millis(10));
2133 }
2134 anyhow::bail!("spawned job did not write expected result");
2135 }
2136
2137 #[test]
2138 fn spawn_native_closure_avoids_any_boxing() -> anyhow::Result<()> {
2139 let nat_path = "local/vm_spawn_native/result";
2140 let _ = root::remove(nat_path);
2141 let vm = Vm::with_all()?;
2142 vm.import_code(
2143 "vm_spawn_native",
2144 br#"
2145 pub fn start() {
2146 spawn(|x: i64, y: i64| {
2147 root::add("local/vm_spawn_native/result", x + y);
2148 }, (10i64, 20i64))
2149 }
2150 "#
2151 .to_vec(),
2152 )?;
2153 let compiled = vm.get_fn("vm_spawn_native::start", &[])?;
2154 assert_eq!(compiled.ret_ty(), &Type::Bool);
2155 let start: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2156 assert!(start());
2157 for _ in 0..50 {
2158 if root::get(nat_path).ok().and_then(|v| v.as_int()) == Some(30) {
2159 return Ok(());
2160 }
2161 std::thread::sleep(std::time::Duration::from_millis(10));
2162 }
2163 anyhow::bail!("spawned native closure did not write expected result");
2164 }
2165
2166 #[test]
2167 fn multi_level_nested_closure_captures() -> anyhow::Result<()> {
2168 let vm = Vm::with_all()?;
2169 vm.import_code(
2170 "vm_multi_level_captures",
2171 br#"
2172 pub fn run() {
2173 let level1 = "L1";
2174 let level2 = "L2";
2175 |path: string| {
2176 let level3 = "L3";
2177 let inner = |suffix: string| {
2178 let level4 = "L4";
2179 |flag: bool| {
2180 if flag {
2181 level1 + "." + level2 + "." + level3 + "." + level4 + "." + path + suffix
2182 } else {
2183 "off"
2184 }
2185 }(true)
2186 };
2187 inner(".ext")
2188 }("file.txt")
2189 }
2190 "#
2191 .to_vec(),
2192 )?;
2193
2194 let compiled = vm.get_fn("vm_multi_level_captures::run", &[])?;
2195 assert_eq!(compiled.ret_ty(), &Type::Any);
2196 let run: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2197 let result = unsafe { &*run() };
2198 assert_eq!(result.as_str(), "L1.L2.L3.L4.file.txt.ext");
2199 Ok(())
2200 }
2201
2202 #[test]
2203 fn root_add_fn_accepts_string_concat_in_registered_handler() -> anyhow::Result<()> {
2204 let vm = Vm::with_all()?;
2205 vm.import_code(
2206 "vm_registered_string_concat",
2207 br#"
2208 pub fn send_panel(idx: i64) {
2209 let idx_key = "" + idx;
2210 idx_key
2211 }
2212 "#
2213 .to_vec(),
2214 )?;
2215
2216 assert!(vm.get_fn_ptr("vm_registered_string_concat::send_panel", &[Type::Any]).is_ok());
2217 Ok(())
2218 }
2219
2220 #[test]
2221 fn dynamic_method_error_reports_source_location() -> anyhow::Result<()> {
2222 let vm = Vm::with_all()?;
2223 vm.import_code(
2224 "vm_bad_dynamic_method",
2225 br#"
2226 pub fn main(value) {
2227 let out = "";
2228 out = out + value.fetch("name");
2229 }
2230 "#
2231 .to_vec(),
2232 )?;
2233
2234 let err = vm.get_fn_ptr("vm_bad_dynamic_method::main", &[Type::Any]).expect_err("bad dynamic method should fail to compile");
2235 let msg = format!("{err:#}");
2236 assert!(msg.contains("vm_bad_dynamic_method:4:"), "{msg}");
2237 assert!(msg.contains("`Any.fetch` 不是成员函数"), "{msg}");
2238 assert!(msg.contains(r#"out = out + value.fetch("name");"#), "{msg}");
2239 Ok(())
2240 }
2241
2242 #[test]
2243 fn root_send_idx_returns_handler_value() -> anyhow::Result<()> {
2244 fn echo_handler(msg: Dynamic) -> Dynamic {
2245 dynamic::map!("type"=> "echo", "id"=> msg.get_dynamic("id").unwrap_or(Dynamic::Null))
2246 }
2247
2248 let vm = Vm::with_all()?;
2249 vm.import_code(
2250 "vm_root_send_idx_return",
2251 br#"
2252 pub fn call(req) {
2253 root::send_idx("local/send_idx_return_handlers", 0, req)
2254 }
2255 "#
2256 .to_vec(),
2257 )?;
2258
2259 root::add_list("local/send_idx_return_handlers")?;
2260 let (mount, name) = root::get_mount("local/send_idx_return_handlers")?;
2261 mount.push(name, root::Object::Native(echo_handler))?;
2262
2263 assert_eq!(vm.infer("root::send_idx", &[Type::Any, Type::I64, Type::Any])?, Type::Any);
2264 let compiled = vm.get_fn("vm_root_send_idx_return::call", &[Type::Any])?;
2265 assert_eq!(compiled.ret_ty(), &Type::Any);
2266 let call: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2267 let req = dynamic::map!("id"=> 42i64);
2268 let result = unsafe { &*call(&req) };
2269
2270 assert_eq!(result.get_dynamic("type").map(|value| value.as_str().to_string()), Some("echo".to_string()));
2271 assert_eq!(result.get_dynamic("id").and_then(|value| value.as_int()), Some(42));
2272 Ok(())
2273 }
2274
2275 #[test]
2276 fn compiles_public_hotspots_with_string_paths_and_keys() -> anyhow::Result<()> {
2277 let vm = Vm::with_all()?;
2278 vm.import_code(
2279 "vm_public_hotspots",
2280 br#"
2281 pub fn public_hotspot(action_map_path, panel_id, action_id, hotspot) {
2282 {
2283 path: action_map_path,
2284 panel_id: panel_id,
2285 action_id: action_id,
2286 id: hotspot.id
2287 }
2288 }
2289
2290 pub fn public_hotspots(idx, panel_id, hotspots) {
2291 let idx_key = "" + idx;
2292 let action_map_path = "local/game/panel_actions/" + idx_key;
2293
2294 let existing_action_map = root::get(action_map_path);
2295 if !existing_action_map.is_map() {
2296 root::add_map(action_map_path);
2297 }
2298
2299 if hotspots.is_map() {
2300 let public_items = {};
2301 for action_id in hotspots.keys() {
2302 public_items[action_id] = public_hotspot(action_map_path, panel_id, action_id, hotspots[action_id]);
2303 }
2304 return public_items;
2305 }
2306
2307 let public_items = [];
2308 let i = 0;
2309 while i < hotspots.len() {
2310 let hotspot = hotspots.get_idx(i);
2311 let item = public_hotspot(action_map_path, panel_id, hotspot.id, hotspot);
2312 public_items.push(item);
2313 i = i + 1;
2314 }
2315
2316 public_items
2317 }
2318 "#
2319 .to_vec(),
2320 )?;
2321
2322 assert!(vm.get_fn("vm_public_hotspots::public_hotspots", &[Type::I64, Type::Any, Type::Any]).is_ok());
2323 assert!(vm.get_fn("vm_public_hotspots::public_hotspots", &[Type::Any, Type::Any, Type::Any]).is_ok());
2324 Ok(())
2325 }
2326
2327 #[test]
2328 fn send_panel_calls_public_hotspots_with_dynamic_request() -> anyhow::Result<()> {
2329 let vm = Vm::with_all()?;
2330 vm.import_code(
2331 "vm_send_panel_public_hotspots",
2332 br#"
2333 pub fn ok(value) {
2334 value
2335 }
2336
2337 pub fn panel_from_node(req) {
2338 {
2339 panel_id: req.panel_id,
2340 hotspots: req.hotspots
2341 }
2342 }
2343
2344 pub fn public_hotspot(action_map_path, panel_id, action_id, hotspot) {
2345 {
2346 path: action_map_path,
2347 panel_id: panel_id,
2348 action_id: action_id,
2349 id: hotspot.id
2350 }
2351 }
2352
2353 pub fn public_hotspots(idx, panel_id, hotspots) {
2354 let idx_key = "" + idx;
2355 let action_map_path = "local/game/panel_actions/" + idx_key;
2356
2357 let existing_action_map = root::get(action_map_path);
2358 if !existing_action_map.is_map() {
2359 root::add_map(action_map_path);
2360 }
2361
2362 if hotspots.is_map() {
2363 let public_items = {};
2364 for action_id in hotspots.keys() {
2365 public_items[action_id] = public_hotspot(action_map_path, panel_id, action_id, hotspots[action_id]);
2366 }
2367 return public_items;
2368 }
2369
2370 let public_items = [];
2371 let i = 0;
2372 while i < hotspots.len() {
2373 let hotspot = hotspots.get_idx(i);
2374 let item = public_hotspot(action_map_path, panel_id, hotspot.id, hotspot);
2375 public_items.push(item);
2376 i = i + 1;
2377 }
2378
2379 public_items
2380 }
2381
2382 pub fn send_panel(req) {
2383 let panel = req.panel;
2384 if !panel.is_map() {
2385 panel = panel_from_node(req);
2386 }
2387 if !panel.is_map() {
2388 return ok({
2389 id: 4,
2390 type: "panel_rejected",
2391 reason: "invalid panel"
2392 });
2393 }
2394 panel.id = 4;
2395 panel.idx = req.idx;
2396 if !panel.contains("type") {
2397 panel.type = "panel";
2398 }
2399 if panel.contains("hotspots") {
2400 panel.hotspots = public_hotspots(req.idx, panel.panel_id, panel.hotspots);
2401 }
2402 root::send_idx("local/ws", req.idx, panel);
2403 ok({
2404 id: 4,
2405 type: "panel",
2406 panel_id: panel.panel_id
2407 })
2408 }
2409 "#
2410 .to_vec(),
2411 )?;
2412
2413 let compiled = vm.get_fn("vm_send_panel_public_hotspots::send_panel", &[Type::Any])?;
2414 assert_eq!(compiled.ret_ty(), &Type::Any);
2415 let send_panel: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2416 let req = dynamic::map!(
2417 "idx"=> 7i64,
2418 "panel"=> dynamic::map!(
2419 "panel_id"=> "main",
2420 "hotspots"=> dynamic::map!(
2421 "open"=> dynamic::map!("id"=> "open")
2422 )
2423 )
2424 );
2425 let result = unsafe { &*send_panel(&req) };
2426
2427 assert_eq!(result.get_dynamic("type").map(|value| value.as_str().to_string()), Some("panel".to_string()));
2428 assert_eq!(result.get_dynamic("panel_id").map(|value| value.as_str().to_string()), Some("main".to_string()));
2429 Ok(())
2430 }
2431
2432 #[test]
2433 fn map_assignment_accepts_string_concat_key() -> anyhow::Result<()> {
2434 let vm = Vm::with_all()?;
2435 vm.import_code(
2436 "vm_string_concat_map_key",
2437 br##"
2438 pub fn write_action(action_map, panel_id, action_id, action) {
2439 action_map[panel_id + "#" + action_id] = action;
2440 action_map[panel_id + "#" + action_id]
2441 }
2442 "##
2443 .to_vec(),
2444 )?;
2445
2446 let compiled = vm.get_fn("vm_string_concat_map_key::write_action", &[Type::Any, Type::Any, Type::Any, Type::Any])?;
2447 let write_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2448 let action_map = dynamic::map!();
2449 let panel_id: Dynamic = "panel".into();
2450 let action_id: Dynamic = "open".into();
2451 let action = dynamic::map!("id"=> "open");
2452
2453 let result = unsafe { &*write_action(&action_map, &panel_id, &action_id, &action) };
2454
2455 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("open".to_string()));
2456 assert_eq!(action_map.get_dynamic("panel#open").and_then(|value| value.get_dynamic("id")).map(|value| value.as_str().to_string()), Some("open".to_string()));
2457 Ok(())
2458 }
2459
2460 #[test]
2461 fn map_get_key_accepts_string_concat_key_variable() -> anyhow::Result<()> {
2462 let vm = Vm::with_all()?;
2463 vm.import_code(
2464 "vm_get_key_string_concat_key",
2465 br##"
2466 pub fn read_action(action_map, panel_id, action_id) {
2467 let action_key = panel_id + "#" + action_id;
2468 action_map.get_key(action_key)
2469 }
2470 "##
2471 .to_vec(),
2472 )?;
2473
2474 let compiled = vm.get_fn("vm_get_key_string_concat_key::read_action", &[Type::Any, Type::Any, Type::Any])?;
2475 let read_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2476 let action_map = dynamic::map!("panel#open"=> dynamic::map!("id"=> "open"));
2477 let panel_id: Dynamic = "panel".into();
2478 let action_id: Dynamic = "open".into();
2479
2480 let result = unsafe { &*read_action(&action_map, &panel_id, &action_id) };
2481
2482 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("open".to_string()));
2483 Ok(())
2484 }
2485
2486 #[test]
2487 fn map_get_alias_matches_get_key() -> anyhow::Result<()> {
2488 let vm = Vm::with_all()?;
2489 vm.import_code(
2490 "vm_map_get_alias",
2491 br#"
2492 pub fn read_name(data) {
2493 data.get("name")
2494 }
2495
2496 pub fn read_missing(data) {
2497 data.get("missing")
2498 }
2499 "#
2500 .to_vec(),
2501 )?;
2502
2503 let data = dynamic::map!("name"=> "zust");
2504
2505 let compiled = vm.get_fn("vm_map_get_alias::read_name", &[Type::Any])?;
2506 assert_eq!(compiled.ret_ty(), &Type::Any);
2507 let read_name: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2508 let result = unsafe { &*read_name(&data) };
2509 assert_eq!(result.as_str(), "zust");
2510
2511 let compiled = vm.get_fn("vm_map_get_alias::read_missing", &[Type::Any])?;
2512 assert_eq!(compiled.ret_ty(), &Type::Any);
2513 let read_missing: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2514 let result = unsafe { &*read_missing(&data) };
2515 assert!(result.is_null());
2516 Ok(())
2517 }
2518
2519 #[test]
2520 fn map_get_key_accepts_helper_string_key() -> anyhow::Result<()> {
2521 let vm = Vm::with_all()?;
2522 vm.import_code(
2523 "vm_get_key_helper_string_key",
2524 br##"
2525 pub fn make_action_key(panel_id, action_id) {
2526 panel_id + "#" + action_id
2527 }
2528
2529 pub fn read_action(action_map, panel_id, action_id) {
2530 let action_key = make_action_key(panel_id, action_id);
2531 let action = action_map.get_key(action_key);
2532 action
2533 }
2534 "##
2535 .to_vec(),
2536 )?;
2537
2538 let compiled = vm.get_fn("vm_get_key_helper_string_key::read_action", &[Type::Any, Type::Any, Type::Any])?;
2539 let read_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2540 let action_map = dynamic::map!("panel#open"=> dynamic::map!("id"=> "open"));
2541 let panel_id: Dynamic = "panel".into();
2542 let action_id: Dynamic = "open".into();
2543
2544 let result = unsafe { &*read_action(&action_map, &panel_id, &action_id) };
2545
2546 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("open".to_string()));
2547 Ok(())
2548 }
2549
2550 #[test]
2551 fn map_del_key_removes_string_key_and_returns_removed_value() -> anyhow::Result<()> {
2552 let vm = Vm::with_all()?;
2553 vm.import_code(
2554 "vm_del_key_string_key",
2555 br##"
2556 pub fn remove_action(action_map, panel_id, action_id) {
2557 let action_key = panel_id + "#" + action_id;
2558 let removed = action_map.del_key(action_key);
2559 [removed, action_map.get_key(action_key)]
2560 }
2561 "##
2562 .to_vec(),
2563 )?;
2564
2565 let compiled = vm.get_fn("vm_del_key_string_key::remove_action", &[Type::Any, Type::Any, Type::Any])?;
2566 let remove_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2567 let action_map = dynamic::map!("panel#open"=> dynamic::map!("id"=> "open"));
2568 let panel_id: Dynamic = "panel".into();
2569 let action_id: Dynamic = "open".into();
2570
2571 let result = unsafe { &*remove_action(&action_map, &panel_id, &action_id) };
2572
2573 assert_eq!(result.get_idx(0).and_then(|value| value.get_dynamic("id")).map(|value| value.as_str().to_string()), Some("open".to_string()));
2574 assert!(result.get_idx(1).is_some_and(|value| value.is_null()));
2575 assert!(action_map.get_dynamic("panel#open").is_none());
2576 Ok(())
2577 }
2578
2579 #[test]
2580 fn dynamic_field_value_participates_in_or_expression() -> anyhow::Result<()> {
2581 let vm = Vm::with_all()?;
2582 vm.import_code(
2583 "vm_dynamic_field_or",
2584 r#"
2585 pub fn direct_next() {
2586 let choice = {
2587 label: "颜色",
2588 next: "color"
2589 };
2590 choice.next
2591 }
2592
2593 pub fn bracket_next() {
2594 let choice = {
2595 label: "颜色",
2596 next: "color"
2597 };
2598 choice["next"]
2599 }
2600 "#
2601 .as_bytes()
2602 .to_vec(),
2603 )?;
2604
2605 let compiled = vm.get_fn("vm_dynamic_field_or::direct_next", &[])?;
2606 assert_eq!(compiled.ret_ty(), &Type::Any);
2607 let direct_next: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2608 assert_eq!(unsafe { &*direct_next() }.as_str(), "color");
2609
2610 let compiled = vm.get_fn("vm_dynamic_field_or::bracket_next", &[])?;
2611 assert_eq!(compiled.ret_ty(), &Type::Any);
2612 let bracket_next: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2613 assert_eq!(unsafe { &*bracket_next() }.as_str(), "color");
2614 Ok(())
2615 }
2616
2617 #[test]
2618 fn empty_object_literal_in_if_branch_stays_dynamic() -> anyhow::Result<()> {
2619 let vm = Vm::with_all()?;
2620 vm.import_code(
2621 "vm_if_empty_object_branch",
2622 r#"
2623 pub fn first_note(steps) {
2624 let first = if steps.len() > 0 { steps[0] } else { {} };
2625 let first_note = if first.contains("note") { first.note } else { "fallback" };
2626 first_note
2627 }
2628
2629 pub fn first_ja(steps) {
2630 let first = if steps.len() > 0 { steps[0] } else { {} };
2631 if first.contains("ja") { first.ja } else { "すみません" }
2632 }
2633
2634 pub fn assign_first_note(steps) {
2635 let first = {};
2636 first = if steps.len() > 0 { steps[0] } else { {} };
2637 if first.contains("note") { first.note } else { "fallback" }
2638 }
2639 "#
2640 .as_bytes()
2641 .to_vec(),
2642 )?;
2643
2644 let compiled = vm.get_fn("vm_if_empty_object_branch::first_note", &[Type::Any])?;
2645 assert_eq!(compiled.ret_ty(), &Type::Str);
2646 let first_note: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2647
2648 let empty_steps = Dynamic::list(Vec::new());
2649 assert_eq!(unsafe { &*first_note(&empty_steps) }.as_str(), "fallback");
2650
2651 let mut step = std::collections::BTreeMap::new();
2652 step.insert("note".into(), "hello".into());
2653 let steps = Dynamic::list(vec![Dynamic::map(step)]);
2654 assert_eq!(unsafe { &*first_note(&steps) }.as_str(), "hello");
2655
2656 let compiled = vm.get_fn("vm_if_empty_object_branch::first_ja", &[Type::Any])?;
2657 assert_eq!(compiled.ret_ty(), &Type::Any);
2658 let first_ja: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2659 assert_eq!(unsafe { &*first_ja(&empty_steps) }.as_str(), "すみません");
2660
2661 let compiled = vm.get_fn("vm_if_empty_object_branch::assign_first_note", &[Type::Any])?;
2662 assert_eq!(compiled.ret_ty(), &Type::Any);
2663 let assign_first_note: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2664 assert_eq!(unsafe { &*assign_first_note(&empty_steps) }.as_str(), "fallback");
2665 assert_eq!(unsafe { &*assign_first_note(&steps) }.as_str(), "hello");
2666 Ok(())
2667 }
2668
2669 #[test]
2670 fn list_literal_can_be_function_tail_expression() -> anyhow::Result<()> {
2671 let vm = Vm::with_all()?;
2672 vm.import_code(
2673 "vm_tail_list_literal",
2674 r#"
2675 pub fn numbers() {
2676 [1, 2, 3]
2677 }
2678
2679 pub fn maps() {
2680 [
2681 {note: "first"},
2682 {note: "second"}
2683 ]
2684 }
2685
2686 pub fn object_with_maps() {
2687 {
2688 steps: [
2689 {note: "first"},
2690 {note: "second"}
2691 ]
2692 }
2693 }
2694
2695 pub fn return_maps() {
2696 return [
2697 {note: "first"},
2698 {note: "second"}
2699 ];
2700 }
2701
2702 pub fn return_maps_without_semicolon() {
2703 return [
2704 {note: "first"},
2705 {note: "second"}
2706 ]
2707 }
2708
2709 pub fn tail_bare_variable() {
2710 let value = [
2711 {note: "first"},
2712 {note: "second"}
2713 ];
2714 value
2715 }
2716
2717 pub fn return_bare_variable_without_semicolon() {
2718 let value = [
2719 {note: "first"},
2720 {note: "second"}
2721 ];
2722 return value
2723 }
2724
2725 pub fn tail_object_variable() {
2726 let result = {
2727 steps: [
2728 {note: "first"},
2729 {note: "second"}
2730 ]
2731 };
2732 result
2733 }
2734 "#
2735 .as_bytes()
2736 .to_vec(),
2737 )?;
2738
2739 let compiled = vm.get_fn("vm_tail_list_literal::numbers", &[])?;
2740 assert_eq!(compiled.ret_ty(), &Type::Any);
2741 let numbers: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2742 let result = unsafe { &*numbers() };
2743 assert_eq!(result.len(), 3);
2744 assert_eq!(result.get_idx(1).and_then(|value| value.as_int()), Some(2));
2745
2746 let compiled = vm.get_fn("vm_tail_list_literal::maps", &[])?;
2747 assert_eq!(compiled.ret_ty(), &Type::Any);
2748 let maps: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2749 let result = unsafe { &*maps() };
2750 assert_eq!(result.len(), 2);
2751 assert_eq!(result.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
2752
2753 let compiled = vm.get_fn("vm_tail_list_literal::object_with_maps", &[])?;
2754 assert_eq!(compiled.ret_ty(), &Type::Any);
2755 let object_with_maps: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2756 let result = unsafe { &*object_with_maps() };
2757 let steps = result.get_dynamic("steps").expect("steps");
2758 assert_eq!(steps.len(), 2);
2759 assert_eq!(steps.get_idx(0).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("first".to_string()));
2760
2761 let compiled = vm.get_fn("vm_tail_list_literal::return_maps", &[])?;
2762 assert_eq!(compiled.ret_ty(), &Type::Any);
2763 let return_maps: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2764 let result = unsafe { &*return_maps() };
2765 assert_eq!(result.len(), 2);
2766 assert_eq!(result.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
2767
2768 let compiled = vm.get_fn("vm_tail_list_literal::return_maps_without_semicolon", &[])?;
2769 assert_eq!(compiled.ret_ty(), &Type::Any);
2770 let return_maps_without_semicolon: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2771 let result = unsafe { &*return_maps_without_semicolon() };
2772 assert_eq!(result.len(), 2);
2773 assert_eq!(result.get_idx(0).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("first".to_string()));
2774
2775 let compiled = vm.get_fn("vm_tail_list_literal::tail_bare_variable", &[])?;
2776 assert_eq!(compiled.ret_ty(), &Type::Any);
2777 let tail_bare_variable: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2778 let result = unsafe { &*tail_bare_variable() };
2779 assert_eq!(result.len(), 2);
2780 assert_eq!(result.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
2781
2782 let compiled = vm.get_fn("vm_tail_list_literal::return_bare_variable_without_semicolon", &[])?;
2783 assert_eq!(compiled.ret_ty(), &Type::Any);
2784 let return_bare_variable_without_semicolon: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2785 let result = unsafe { &*return_bare_variable_without_semicolon() };
2786 assert_eq!(result.len(), 2);
2787 assert_eq!(result.get_idx(0).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("first".to_string()));
2788
2789 let compiled = vm.get_fn("vm_tail_list_literal::tail_object_variable", &[])?;
2790 assert_eq!(compiled.ret_ty(), &Type::Any);
2791 let tail_object_variable: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2792 let result = unsafe { &*tail_object_variable() };
2793 let steps = result.get_dynamic("steps").expect("steps");
2794 assert_eq!(steps.len(), 2);
2795 assert_eq!(steps.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
2796 Ok(())
2797 }
2798
2799 #[test]
2800 fn list_return_value_supports_get_idx_method_call() -> anyhow::Result<()> {
2801 let vm = Vm::with_all()?;
2802 vm.import_code(
2803 "vm_returned_list_get_idx",
2804 r#"
2805 pub fn ids() {
2806 [
2807 "base",
2808 "2",
2809 "3"
2810 ]
2811 }
2812
2813 pub fn combinations() {
2814 let result = [];
2815 let values = ids();
2816 let idx = 0;
2817 while idx < values.len() {
2818 result.push(values.get_idx(idx));
2819 idx = idx + 1;
2820 }
2821 result
2822 }
2823 "#
2824 .as_bytes()
2825 .to_vec(),
2826 )?;
2827
2828 let compiled = vm.get_fn("vm_returned_list_get_idx::combinations", &[])?;
2829 assert_eq!(compiled.ret_ty(), &Type::Any);
2830 let combinations: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2831 let result = unsafe { &*combinations() };
2832
2833 assert_eq!(result.len(), 3);
2834 assert_eq!(result.get_idx(0).map(|value| value.as_str().to_string()), Some("base".to_string()));
2835 assert_eq!(result.get_idx(2).map(|value| value.as_str().to_string()), Some("3".to_string()));
2836 Ok(())
2837 }
2838
2839 #[test]
2840 fn repeated_deep_step_literals_import_successfully() -> anyhow::Result<()> {
2841 fn extra_page_literal(depth: usize) -> String {
2842 let mut value = "{leaf: \"done\"}".to_string();
2843 for idx in 0..depth {
2844 value = format!("{{kind: \"page\", idx: {idx}, children: [{value}], meta: {{title: \"extra\", visible: true}}}}");
2845 }
2846 value
2847 }
2848
2849 let extra = extra_page_literal(48);
2850 let code = format!(
2851 r#"
2852 pub fn script() {{
2853 return [
2854 {{ja: "一つ目", note: "first", extra: {extra}}},
2855 {{ja: "二つ目", note: "second", extra: {extra}}},
2856 {{ja: "三つ目", note: "third", extra: {extra}}}
2857 ]
2858 }}
2859 "#
2860 );
2861
2862 let vm = Vm::with_all()?;
2863 vm.import_code("vm_repeated_deep_step_literals", code.into_bytes())?;
2864 let compiled = vm.get_fn("vm_repeated_deep_step_literals::script", &[])?;
2865 assert_eq!(compiled.ret_ty(), &Type::Any);
2866 let script: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2867 let result = unsafe { &*script() };
2868 assert_eq!(result.len(), 3);
2869 assert_eq!(result.get_idx(2).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("third".to_string()));
2870 Ok(())
2871 }
2872
2873 #[test]
2874 fn native_import_uses_owning_vm() -> anyhow::Result<()> {
2875 let module_path = std::env::temp_dir().join(format!("zust_vm_import_owner_{}.zs", std::process::id()));
2876 std::fs::write(&module_path, "pub fn value() { 41 }")?;
2877 let module_path = module_path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\"");
2878
2879 let vm1 = Vm::with_all()?;
2880 vm1.import_code(
2881 "vm_import_owner",
2882 format!(
2883 r#"
2884 pub fn run() {{
2885 import("vm_imported_owner", "{module_path}");
2886 }}
2887 "#
2888 )
2889 .into_bytes(),
2890 )?;
2891 let compiled = vm1.get_fn("vm_import_owner::run", &[])?;
2892
2893 let vm2 = Vm::with_all()?;
2894 vm2.import_code("vm_import_other", b"pub fn run() { 0 }".to_vec())?;
2895 let _ = vm2.get_fn("vm_import_other::run", &[])?;
2896
2897 let run: extern "C" fn() = unsafe { std::mem::transmute(compiled.ptr()) };
2898 run();
2899
2900 assert!(vm1.get_fn("vm_imported_owner::value", &[]).is_ok());
2901 assert!(vm2.get_fn("vm_imported_owner::value", &[]).is_err());
2902 Ok(())
2903 }
2904
2905 #[test]
2906 fn object_last_field_call_does_not_need_trailing_comma() -> anyhow::Result<()> {
2907 let vm = Vm::with_all()?;
2908 vm.import_code(
2909 "vm_object_last_call_field",
2910 r#"
2911 pub fn extra_page() {
2912 {
2913 title: "extra",
2914 pages: [
2915 {note: "nested"}
2916 ]
2917 }
2918 }
2919
2920 pub fn data() {
2921 return [
2922 {
2923 note: "first",
2924 choices: ["a", "b"],
2925 extras: extra_page()
2926 },
2927 {
2928 note: "second",
2929 choices: ["c"],
2930 extras: extra_page()
2931 }
2932 ]
2933 }
2934 "#
2935 .as_bytes()
2936 .to_vec(),
2937 )?;
2938
2939 let compiled = vm.get_fn("vm_object_last_call_field::data", &[])?;
2940 assert_eq!(compiled.ret_ty(), &Type::Any);
2941 let data: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2942 let result = unsafe { &*data() };
2943 assert_eq!(result.len(), 2);
2944 let first = result.get_idx(0).expect("first step");
2945 assert_eq!(first.get_dynamic("extras").and_then(|extras| extras.get_dynamic("title")).map(|title| title.as_str().to_string()), Some("extra".to_string()));
2946 Ok(())
2947 }
2948
2949 #[test]
2950 fn string_return_survives_scope_exit() -> anyhow::Result<()> {
2951 let vm = Vm::with_all()?;
2952 vm.import_code(
2953 "vm_string_return_scope",
2954 r#"
2955 pub fn source_root() {
2956 "../assets/character/男主角换装"
2957 }
2958
2959 pub fn binary_root() {
2960 "character_binary/男主角换装"
2961 }
2962
2963 pub fn runtime_binary_url() {
2964 "/" + binary_root()
2965 }
2966
2967 pub fn action_groups() {
2968 let root = source_root();
2969 let binary_url = runtime_binary_url();
2970 let binary_root = binary_root();
2971 [
2972 {
2973 id: "field_bottom",
2974 source_spine: root + "/战斗外/boy_b.spine",
2975 skeleton: binary_url + "/战斗外/boy_b/boy_b.skel.bytes",
2976 export_skeleton: binary_root + "/战斗外/boy_b/boy_b.skel.bytes"
2977 }
2978 ]
2979 }
2980 "#
2981 .as_bytes()
2982 .to_vec(),
2983 )?;
2984
2985 let compiled = vm.get_fn("vm_string_return_scope::source_root", &[])?;
2986 assert_eq!(compiled.ret_ty(), &Type::Str);
2987 let source_root: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2988 let source_root = unsafe { &*source_root() };
2989 assert_eq!(source_root.as_str(), "../assets/character/男主角换装");
2990
2991 let compiled = vm.get_fn("vm_string_return_scope::action_groups", &[])?;
2992 assert_eq!(compiled.ret_ty(), &Type::Any);
2993 let action_groups: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2994 let groups = unsafe { &*action_groups() };
2995 let first = groups.get_idx(0).expect("first action group");
2996 assert_eq!(first.get_dynamic("source_spine").map(|value| value.as_str().to_string()), Some("../assets/character/男主角换装/战斗外/boy_b.spine".to_string()));
2997 assert_eq!(first.get_dynamic("skeleton").map(|value| value.as_str().to_string()), Some("/character_binary/男主角换装/战斗外/boy_b/boy_b.skel.bytes".to_string()));
2998 Ok(())
2999 }
3000
3001 #[test]
3002 fn dynamic_string_add_uses_any_binary_fast_path() -> anyhow::Result<()> {
3003 let vm = Vm::with_all()?;
3004 vm.import_code(
3005 "vm_dynamic_string_add",
3006 br#"
3007 pub fn concat(left, right) {
3008 left + right
3009 }
3010 "#
3011 .to_vec(),
3012 )?;
3013
3014 let compiled = vm.get_fn("vm_dynamic_string_add::concat", &[Type::Any, Type::Any])?;
3015 assert_eq!(compiled.ret_ty(), &Type::Any);
3016 let concat: extern "C" fn(*const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3017 let left = Dynamic::from("hello");
3018 let right = Dynamic::from(" world");
3019 let result = unsafe { &*concat(&left, &right) };
3020 assert_eq!(result.as_str(), "hello world");
3021 Ok(())
3022 }
3023
3024 #[test]
3025 fn large_dynamic_object_accepts_inline_call_fields() -> anyhow::Result<()> {
3026 let vm = Vm::with_all()?;
3027 let model_count = 180;
3028 let combination_count = 90;
3029 let models = (0..model_count)
3030 .map(|idx| {
3031 format!(
3032 r#"{{id: "model_{idx}", name: "模型_{idx}", source: "/美术资源/角色/少年/套装_{idx}/模型_{idx}.model.json", parts: [
3033 {{slot: "hair", path: "/模型/头发/颜色_{idx}/默认.png", z: 10}},
3034 {{slot: "body", path: "/模型/身体/套装_{idx}/默认.png", z: 1}},
3035 {{slot: "face", path: "/模型/表情/表情_{idx}/默认.png", z: 20}}
3036 ]}}"#
3037 )
3038 })
3039 .collect::<Vec<_>>()
3040 .join(",\n");
3041 let combinations = (0..combination_count).map(|idx| format!(r#"{{hair: "color_{idx}", body: "set_{idx}", face: "face_{idx}"}}"#)).collect::<Vec<_>>().join(",\n");
3042 let code = format!(
3043 r#"
3044 pub fn source_root() {{
3045 "/美术资源/角色/少年/默认"
3046 }}
3047
3048 pub fn runtime_boy_url() {{
3049 "/cdn/runtime/角色/少年/少年.model.json"
3050 }}
3051
3052 pub fn parts() {{
3053 [
3054 {{id: "hair", path: "/模型/头发/黑色/默认.png", z: 10}},
3055 {{id: "body", path: "/模型/身体/校服/默认.png", z: 1}},
3056 {{id: "face", path: "/模型/表情/微笑/默认.png", z: 20}}
3057 ]
3058 }}
3059
3060 pub fn action_groups() {{
3061 {{
3062 idle: [
3063 {{id: "stand", name: "站立", frames: ["待机/0001.png", "待机/0002.png"]}},
3064 {{id: "blink", name: "眨眼", frames: ["表情/眨眼/0001.png", "表情/眨眼/0002.png"]}}
3065 ],
3066 move: [
3067 {{id: "walk", name: "行走", frames: ["行走/0001.png", "行走/0002.png"]}},
3068 {{id: "run", name: "奔跑", frames: ["奔跑/0001.png", "奔跑/0002.png"]}}
3069 ]
3070 }}
3071 }}
3072
3073 pub fn default_model() {{
3074 {{
3075 id: "runtime_boy",
3076 name: "运行时少年",
3077 skins: [
3078 {{id: "school", title: "校服", source: "/套装/校服/model.json"}},
3079 {{id: "casual", title: "便服", source: "/套装/便服/model.json"}}
3080 ],
3081 models: [
3082 {models}
3083 ]
3084 }}
3085 }}
3086
3087 pub fn first_nine_combinations() {{
3088 [
3089 {combinations}
3090 ]
3091 }}
3092
3093 pub fn config() {{
3094 {{
3095 source_root: source_root(),
3096 runtime_boy_url: runtime_boy_url(),
3097 parts: parts(),
3098 action_groups: action_groups(),
3099 default_model: default_model(),
3100 first_nine_combinations: first_nine_combinations()
3101 }}
3102 }}
3103
3104 pub fn start() {{
3105 root::add("local/vm_large_inline_call_object/config", {{
3106 source_root: source_root(),
3107 runtime_boy_url: runtime_boy_url(),
3108 parts: parts(),
3109 action_groups: action_groups(),
3110 default_model: default_model(),
3111 first_nine_combinations: first_nine_combinations()
3112 }})
3113 }}
3114 "#
3115 );
3116 vm.import_code("vm_large_inline_call_object", code.into_bytes())?;
3117
3118 let compiled = vm.get_fn("vm_large_inline_call_object::config", &[])?;
3119 assert_eq!(compiled.ret_ty(), &Type::Any);
3120 let config: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3121 let result = unsafe { &*config() };
3122 assert_eq!(result.get_dynamic("source_root").map(|value| value.as_str().to_string()), Some("/美术资源/角色/少年/默认".to_string()));
3123 assert_eq!(result.get_dynamic("first_nine_combinations").map(|value| value.len()), Some(combination_count));
3124
3125 let compiled = vm.get_fn("vm_large_inline_call_object::start", &[])?;
3126 assert_eq!(compiled.ret_ty(), &Type::Bool);
3127 let start: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3128 assert!(start());
3129 let saved = root::get("local/vm_large_inline_call_object/config")?;
3130 assert_eq!(saved.get_dynamic("first_nine_combinations").map(|value| value.len()), Some(combination_count));
3131 Ok(())
3132 }
3133
3134 #[cfg(feature = "http")]
3135 #[test]
3136 fn http_serve_accepts_inline_config_map() -> anyhow::Result<()> {
3137 let vm = Vm::with_all()?;
3138 vm.import_code(
3139 "vm_http_serve_inline_config",
3140 br#"
3141 pub fn start() {
3142 let server = http::serve({host: "127.0.0.1:5192"});
3143 server
3144 }
3145 "#
3146 .to_vec(),
3147 )?;
3148
3149 let compiled = vm.get_fn("vm_http_serve_inline_config::start", &[])?;
3150 assert_eq!(compiled.ret_ty(), &Type::Any);
3151 Ok(())
3152 }
3153
3154 #[cfg(feature = "http")]
3155 #[test]
3156 fn http_serve_accepts_variable_and_quoted_static_key() -> anyhow::Result<()> {
3157 let vm = Vm::with_all()?;
3158 vm.import_code(
3159 "vm_http_serve_quoted_static",
3160 br#"
3161 pub fn start(server_addr) {
3162 let http_server = http::serve({
3163 host: server_addr,
3164 ws: true,
3165 upload: "upload",
3166 "static": {
3167 path: "/",
3168 dir: "public/local"
3169 }
3170 });
3171 http_server
3172 }
3173 "#
3174 .to_vec(),
3175 )?;
3176
3177 let compiled = vm.get_fn("vm_http_serve_quoted_static::start", &[Type::Any])?;
3178 assert_eq!(compiled.ret_ty(), &Type::Any);
3179 Ok(())
3180 }
3181
3182 #[cfg(all(feature = "http", feature = "llm"))]
3183 #[test]
3184 fn oss_helpers_accept_explicit_config() -> anyhow::Result<()> {
3185 let vm = Vm::with_all()?;
3186 vm.import_code(
3187 "vm_oss_explicit_config",
3188 br#"
3189 pub fn upload(oss, bytes) {
3190 oss::upload(oss, "llm/input/audio.wav", bytes)
3191 }
3192
3193 pub fn http_upload(oss, bytes) {
3194 http::upload(oss, "uploads/input.bin", bytes)
3195 }
3196
3197 pub fn link(oss, uploaded) {
3198 oss::signed_url(oss, {oss_url: uploaded, expires: 3600})
3199 }
3200 "#
3201 .to_vec(),
3202 )?;
3203
3204 assert_eq!(vm.get_fn("vm_oss_explicit_config::upload", &[Type::Any, Type::Any])?.ret_ty(), &Type::Any);
3205 assert_eq!(vm.get_fn("vm_oss_explicit_config::http_upload", &[Type::Any, Type::Any])?.ret_ty(), &Type::Any);
3206 assert_eq!(vm.get_fn("vm_oss_explicit_config::link", &[Type::Any, Type::Any])?.ret_ty(), &Type::Any);
3207 Ok(())
3208 }
3209
3210 #[cfg(feature = "http")]
3211 #[test]
3212 fn load_script_accepts_http_serve_inline_config() -> anyhow::Result<()> {
3213 let vm = Vm::with_all()?;
3214 let (_fn_ptr, ty) = vm.load(
3215 br#"
3216 let server_addr = "127.0.0.1:5192";
3217 let http_server = http::serve({
3218 host: server_addr,
3219 ws: true,
3220 upload: "upload",
3221 "static": {
3222 path: "/",
3223 dir: "public/local"
3224 }
3225 });
3226 http_server
3227 "#
3228 .to_vec(),
3229 "arg".into(),
3230 )?;
3231
3232 assert_eq!(ty, Type::Any);
3233 Ok(())
3234 }
3235
3236 #[test]
3237 fn load_script_resolves_import_before_compile() -> anyhow::Result<()> {
3238 let module_path = std::env::temp_dir().join(format!("zust_vm_load_import_{}.zs", std::process::id()));
3239 std::fs::write(&module_path, "pub fn init() { return {ok: true}; }")?;
3240 let module_path = module_path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\"");
3241
3242 let vm = Vm::with_all()?;
3243 let (_fn_ptr, ty) = vm.load(
3244 format!(
3245 r#"
3246 import("create_scene", "{module_path}");
3247 create_scene::init();
3248 "#
3249 )
3250 .into_bytes(),
3251 "req".into(),
3252 )?;
3253
3254 assert_eq!(ty, Type::Void);
3255 Ok(())
3256 }
3257
3258 #[test]
3259 fn gpu_struct_layout_packs_and_unpacks_dynamic_maps() -> anyhow::Result<()> {
3260 let vm = Vm::with_all()?;
3261 vm.import_code(
3262 "vm_gpu_layout",
3263 br#"
3264 pub struct Params {
3265 a: u32,
3266 b: u32,
3267 c: u32,
3268 }
3269 "#
3270 .to_vec(),
3271 )?;
3272
3273 let layout = vm.gpu_struct_layout("vm_gpu_layout::Params", &[])?;
3274 assert_eq!(layout.size, 16);
3275 assert_eq!(layout.fields.iter().map(|field| (field.name.as_str(), field.offset)).collect::<Vec<_>>(), vec![("a", 0), ("b", 4), ("c", 8)]);
3276
3277 let value = dynamic::map!("a"=> 1u32, "b"=> 2u32, "c"=> 3u32);
3278 let bytes = layout.pack_map(&value)?;
3279 assert_eq!(bytes.len(), 16);
3280 assert_eq!(&bytes[0..4], &1u32.to_ne_bytes());
3281 assert_eq!(&bytes[4..8], &2u32.to_ne_bytes());
3282 assert_eq!(&bytes[8..12], &3u32.to_ne_bytes());
3283
3284 let read = layout.unpack_map(&bytes)?;
3285 assert_eq!(read.get_dynamic("a").and_then(|value| value.as_uint()), Some(1));
3286 assert_eq!(read.get_dynamic("b").and_then(|value| value.as_uint()), Some(2));
3287 assert_eq!(read.get_dynamic("c").and_then(|value| value.as_uint()), Some(3));
3288 Ok(())
3289 }
3290
3291 #[test]
3292 fn root_native_calls_do_not_take_ownership_of_dynamic_args() -> anyhow::Result<()> {
3293 let vm = Vm::with_all()?;
3294 vm.import_code(
3295 "vm_root_clone_bridge",
3296 br#"
3297 pub fn add_then_reuse(arg) {
3298 let user = {
3299 address: "test-wallet",
3300 points: 20
3301 };
3302 root::add("local/root-clone-bridge-user", user);
3303 user.points = user.points - 7;
3304 root::add("local/root-clone-bridge-user", user);
3305 {
3306 user: user,
3307 points: user.points
3308 }
3309 }
3310
3311 pub fn clone_then_mutate(arg) {
3312 let user = {
3313 profile: {
3314 points: 20
3315 }
3316 };
3317 let copied = user.clone();
3318 copied.profile.points = 13;
3319 user
3320 }
3321 "#
3322 .to_vec(),
3323 )?;
3324
3325 let compiled = vm.get_fn("vm_root_clone_bridge::add_then_reuse", &[Type::Any])?;
3326 assert_eq!(compiled.ret_ty(), &Type::Any);
3327 let add_then_reuse: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3328 let arg = Dynamic::Null;
3329 let result = add_then_reuse(&arg);
3330 let result = unsafe { &*result };
3331
3332 assert_eq!(result.get_dynamic("points").and_then(|value| value.as_int()), Some(13));
3333 let mut json = String::new();
3334 result.to_json(&mut json);
3335 assert!(json.contains("\"points\": 13"));
3336
3337 let clone_then_mutate = vm.get_fn("vm_root_clone_bridge::clone_then_mutate", &[Type::Any])?;
3338 let clone_then_mutate: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(clone_then_mutate.ptr()) };
3339 let result = clone_then_mutate(&arg);
3340 let result = unsafe { &*result };
3341 assert_eq!(result.get_dynamic("profile").unwrap().get_dynamic("points").and_then(|value| value.as_int()), Some(20));
3342 Ok(())
3343 }
3344
3345 struct CounterForTypedReceiver {
3346 value: i64,
3347 }
3348
3349 extern "C" fn counter_for_typed_receiver_get(value: *const Dynamic) -> i64 {
3350 unsafe { &*value }.as_custom::<CounterForTypedReceiver>().map(|counter| counter.value).unwrap_or(-1)
3351 }
3352
3353 struct NavMapForFunctionArg;
3354
3355 extern "C" fn nav_map_for_function_arg_new() -> *const Dynamic {
3356 Box::into_raw(Box::new(Dynamic::custom(NavMapForFunctionArg)))
3357 }
3358
3359 #[derive(Debug, Default)]
3360 struct PropertyForwardingObject {
3361 values: parking_lot::RwLock<BTreeMap<String, Dynamic>>,
3362 }
3363
3364 impl CustomProperty for PropertyForwardingObject {
3365 fn get_key(&self, key: &str) -> Option<Dynamic> {
3366 self.values.read().get(key).cloned()
3367 }
3368
3369 fn set_key(&self, key: &str, value: Dynamic) -> bool {
3370 self.values.write().insert(key.to_string(), value);
3371 true
3372 }
3373 }
3374
3375 extern "C" fn property_forwarding_object_new() -> *const Dynamic {
3376 Box::into_raw(Box::new(Dynamic::custom_with_properties(PropertyForwardingObject::default())))
3377 }
3378
3379 #[test]
3380 fn typed_receiver_method_call_dispatches_with_type_hint() -> anyhow::Result<()> {
3381 let vm = Vm::with_all()?;
3382 vm.add_empty_type("Counter")?;
3383 let counter_ty = vm.get_symbol("Counter", Vec::new())?;
3384 vm.add_native_method_ptr("Counter", "get", &[counter_ty], Type::I64, counter_for_typed_receiver_get as *const u8)?;
3385 vm.import_code(
3386 "vm_typed_receiver_method",
3387 br#"
3388 pub fn run(value) {
3389 value::<Counter>::get()
3390 }
3391 "#
3392 .to_vec(),
3393 )?;
3394
3395 let compiled = vm.get_fn("vm_typed_receiver_method::run", &[Type::Any])?;
3396 assert_eq!(compiled.ret_ty(), &Type::I64);
3397 let run: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3398 let value = Dynamic::custom(CounterForTypedReceiver { value: 42 });
3399
3400 assert_eq!(run(&value), 42);
3401 Ok(())
3402 }
3403
3404 #[test]
3405 fn native_custom_object_can_be_passed_to_zs_function() -> anyhow::Result<()> {
3406 let vm = Vm::with_all()?;
3407 vm.add_empty_type("NavMap")?;
3408 vm.add_native_method_ptr("NavMap", "new", &[], Type::Any, nav_map_for_function_arg_new as *const u8)?;
3409 vm.import_code(
3410 "vm_native_custom_arg",
3411 br#"
3412 pub fn add_nav_spawns(world, navmap) {
3413 navmap
3414 }
3415
3416 pub fn run(world) {
3417 let navmap = NavMap::new();
3418 let with_spawns = add_nav_spawns(world, navmap);
3419 with_spawns
3420 }
3421 "#
3422 .to_vec(),
3423 )?;
3424
3425 let compiled = vm.get_fn("vm_native_custom_arg::run", &[Type::Any])?;
3426 assert_eq!(compiled.ret_ty(), &Type::Any);
3427 let run: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3428 let world = Dynamic::Null;
3429 let result = run(&world);
3430 let result = unsafe { &*result };
3431
3432 assert!(result.as_custom::<NavMapForFunctionArg>().is_some());
3433 Ok(())
3434 }
3435
3436 #[test]
3437 fn any_field_assignment_forwards_to_custom_properties() -> anyhow::Result<()> {
3438 let vm = Vm::with_all()?;
3439 vm.add_empty_type("Dialog")?;
3440 vm.add_native_method_ptr("Dialog", "new", &[], Type::Any, property_forwarding_object_new as *const u8)?;
3441 vm.import_code(
3442 "vm_custom_property_forwarding",
3443 br#"
3444 pub fn run() {
3445 let dialog = Dialog::new();
3446 dialog.file_mode = 3;
3447 dialog.file_mode
3448 }
3449 "#
3450 .to_vec(),
3451 )?;
3452
3453 let compiled = vm.get_fn("vm_custom_property_forwarding::run", &[])?;
3454 assert_eq!(compiled.ret_ty(), &Type::Any);
3455 let run: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3456 let result = unsafe { &*run() };
3457
3458 assert_eq!(result.as_int(), Some(3));
3459 Ok(())
3460 }
3461
3462 #[test]
3463 fn native_custom_object_typed_local_can_be_passed_to_zs_function() -> anyhow::Result<()> {
3464 let vm = Vm::with_all()?;
3465 vm.add_empty_type("NavMap")?;
3466 let _nav_map_ty = vm.get_symbol("NavMap", Vec::new())?;
3467 vm.add_native_method_ptr("NavMap", "new", &[], Type::Any, nav_map_for_function_arg_new as *const u8)?;
3468 vm.import_code(
3469 "vm_native_custom_typed_arg",
3470 br#"
3471 pub fn add_nav_spawns(world, navmap) {
3472 navmap
3473 }
3474
3475 pub fn run(world) {
3476 let navmap: NavMap = NavMap::new();
3477 let with_spawns = add_nav_spawns(world, navmap);
3478 with_spawns
3479 }
3480 "#
3481 .to_vec(),
3482 )?;
3483
3484 let compiled = vm.get_fn("vm_native_custom_typed_arg::run", &[Type::Any])?;
3485 let run: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3486 let world = Dynamic::Null;
3487 let result = run(&world);
3488 let result = unsafe { &*result };
3489
3490 assert!(result.as_custom::<NavMapForFunctionArg>().is_some());
3491 Ok(())
3492 }
3493
3494 #[test]
3497 fn dynamic_type_checks_on_null_and_primitive_values() -> anyhow::Result<()> {
3498 let vm = Vm::with_all()?;
3499 vm.import_code(
3500 "vm_dynamic_type_checks",
3501 br#"
3502 pub fn is_list_on_int() {
3503 let x = 42i64;
3504 x.is_list()
3505 }
3506
3507 pub fn is_map_on_int() {
3508 let x = 42i64;
3509 x.is_map()
3510 }
3511
3512 pub fn is_null_on_int() {
3513 let x = 42i64;
3514 x.is_null()
3515 }
3516 "#
3517 .to_vec(),
3518 )?;
3519
3520 let compiled = vm.get_fn("vm_dynamic_type_checks::is_list_on_int", &[])?;
3521 assert_eq!(compiled.ret_ty(), &Type::Bool);
3522 let is_list_on_int: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3523 assert!(!is_list_on_int());
3524
3525 let compiled = vm.get_fn("vm_dynamic_type_checks::is_map_on_int", &[])?;
3526 assert_eq!(compiled.ret_ty(), &Type::Bool);
3527 let is_map_on_int: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3528 assert!(!is_map_on_int());
3529
3530 let compiled = vm.get_fn("vm_dynamic_type_checks::is_null_on_int", &[])?;
3531 assert_eq!(compiled.ret_ty(), &Type::Bool);
3532 let is_null_on_int: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3533 assert!(!is_null_on_int());
3534 Ok(())
3535 }
3536
3537 #[test]
3538 fn void_and_null_are_false_in_boolean_context() -> anyhow::Result<()> {
3539 let vm = Vm::with_all()?;
3540 vm.import_code(
3541 "vm_void_bool_context",
3542 br#"
3543 pub fn run() {
3544 let items = [1i32, 2i32];
3545 let ok1 = !(items.push(3i32) && false);
3546 let ok2 = !(true && items.push(4i32));
3547 let ok3 = null || true;
3548 let ok4 = null || items.len() == 4;
3549 ok1 && ok2 && ok3 && ok4
3550 }
3551 "#
3552 .to_vec(),
3553 )?;
3554
3555 let compiled = vm.get_fn("vm_void_bool_context::run", &[])?;
3556 assert_eq!(compiled.ret_ty(), &Type::Bool);
3557 let run: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3558 assert!(run());
3559 Ok(())
3560 }
3561
3562 #[test]
3563 fn empty_for_loop_range_has_zero_iterations() -> anyhow::Result<()> {
3564 let vm = Vm::with_all()?;
3565 vm.import_code(
3566 "vm_empty_for_range",
3567 br#"
3568 pub fn empty_exclusive() {
3569 let count = 0i32;
3570 for i in 0..0 {
3571 count += i;
3572 }
3573 count
3574 }
3575
3576 pub fn single_inclusive_iteration() {
3577 let count = 0i32;
3578 for i in 5..=5 {
3579 count += i;
3580 }
3581 count
3582 }
3583 "#
3584 .to_vec(),
3585 )?;
3586
3587 let compiled = vm.get_fn("vm_empty_for_range::empty_exclusive", &[])?;
3589 assert_eq!(compiled.ret_ty(), &Type::I64);
3590 let empty_exclusive: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3591 assert_eq!(empty_exclusive(), 0);
3592
3593 let compiled = vm.get_fn("vm_empty_for_range::single_inclusive_iteration", &[])?;
3594 assert_eq!(compiled.ret_ty(), &Type::I64);
3595 let single_inclusive: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3596 assert_eq!(single_inclusive(), 5);
3597 Ok(())
3598 }
3599
3600 #[test]
3601 fn for_loop_range_accepts_dynamic_i64_bounds() -> anyhow::Result<()> {
3602 let vm = Vm::with_all()?;
3603 vm.import_code(
3604 "vm_dynamic_for_range",
3605 br#"
3606 pub fn main() {
3607 let view = {};
3608 view.grid_min_x = -2i64;
3609 view.grid_max_x = 2i64;
3610
3611 let end_x = view.grid_max_x + 1i64;
3612 let count = 0i64;
3613
3614 for x in view.grid_min_x..end_x {
3615 count += 1i64;
3616 }
3617
3618 count
3619 }
3620 "#
3621 .to_vec(),
3622 )?;
3623
3624 let compiled = vm.get_fn("vm_dynamic_for_range::main", &[])?;
3625 assert_eq!(compiled.ret_ty(), &Type::I64);
3626 let main: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3627 assert_eq!(main(), 5);
3628 Ok(())
3629 }
3630
3631 #[test]
3632 fn map_contains_key_on_non_existent_and_nested_keys() -> anyhow::Result<()> {
3633 let vm = Vm::with_all()?;
3634 vm.import_code(
3635 "vm_map_contains",
3636 br#"
3637 pub fn contains_existing(data) {
3638 data.contains("name")
3639 }
3640
3641 pub fn contains_missing(data) {
3642 data.contains("nothing")
3643 }
3644 "#
3645 .to_vec(),
3646 )?;
3647
3648 let compiled = vm.get_fn("vm_map_contains::contains_existing", &[Type::Any])?;
3649 assert_eq!(compiled.ret_ty(), &Type::Bool);
3650 let contains_existing: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3651 let data = dynamic::map!("name"=> "test");
3652 assert!(contains_existing(&data));
3653
3654 let compiled = vm.get_fn("vm_map_contains::contains_missing", &[Type::Any])?;
3655 assert_eq!(compiled.ret_ty(), &Type::Bool);
3656 let contains_missing: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
3657 assert!(!contains_missing(&data));
3658 Ok(())
3659 }
3660
3661 #[test]
3662 fn list_pop_on_empty_list_returns_null() -> anyhow::Result<()> {
3663 let vm = Vm::with_all()?;
3664 vm.import_code(
3665 "vm_pop_empty",
3666 br#"
3667 pub fn pop_new_list() {
3668 let items = [];
3669 let value = items.pop();
3670 let still_empty = items.len() == 0;
3671 {value: value, empty: still_empty}
3672 }
3673
3674 pub fn pop_until_empty() {
3675 let items = [1i64, 2i64];
3676 items.pop();
3677 let last = items.pop();
3678 let drained = items.pop();
3679 {last: last, drained: drained}
3680 }
3681 "#
3682 .to_vec(),
3683 )?;
3684
3685 let compiled = vm.get_fn("vm_pop_empty::pop_new_list", &[])?;
3686 assert_eq!(compiled.ret_ty(), &Type::Any);
3687 let pop_new_list: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3688 let result = unsafe { &*pop_new_list() };
3689 assert!(result.get_dynamic("value").is_some_and(|v| v.is_null()));
3690 assert_eq!(result.get_dynamic("empty").and_then(|v| v.as_bool()), Some(true));
3691
3692 let compiled = vm.get_fn("vm_pop_empty::pop_until_empty", &[])?;
3693 assert_eq!(compiled.ret_ty(), &Type::Any);
3694 let pop_until_empty: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3695 let result = unsafe { &*pop_until_empty() };
3696 assert_eq!(result.get_dynamic("last").and_then(|v| v.as_int()), Some(1));
3697 assert!(result.get_dynamic("drained").is_some_and(|v| v.is_null()));
3698 Ok(())
3699 }
3700
3701 #[test]
3702 fn void_function_with_multiple_code_paths() -> anyhow::Result<()> {
3703 let vm = Vm::with_all()?;
3704 vm.import_code(
3705 "vm_void_multi_path",
3706 br#"
3707 pub fn log_if_positive(value: i64) {
3708 if value > 0 {
3709 print(value);
3710 return;
3711 }
3712 if value < 0 {
3713 print(-value);
3714 return;
3715 }
3716 print(0);
3717 }
3718 "#
3719 .to_vec(),
3720 )?;
3721
3722 let compiled = vm.get_fn("vm_void_multi_path::log_if_positive", &[Type::I64])?;
3723 assert!(compiled.ret_ty().is_void());
3724 Ok(())
3725 }
3726
3727 #[test]
3728 fn any_method_call_chain_on_returned_dynamic_value() -> anyhow::Result<()> {
3729 let vm = Vm::with_all()?;
3730 vm.import_code(
3731 "vm_any_method_chain",
3732 br#"
3733 pub fn get_tags(data) {
3734 let tags = data.tags;
3735 if tags.is_list() {
3736 return tags.len();
3737 }
3738 0
3739 }
3740 "#
3741 .to_vec(),
3742 )?;
3743
3744 let compiled = vm.get_fn("vm_any_method_chain::get_tags", &[Type::Any])?;
3745 assert_eq!(compiled.ret_ty(), &Type::I64);
3746 let get_tags: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3747 let data = dynamic::map!("tags"=> Dynamic::list(vec!["a".into(), "b".into(), "c".into()]));
3748 assert_eq!(get_tags(&data), 3);
3749
3750 let empty_data = Dynamic::Null;
3751 assert_eq!(get_tags(&empty_data), 0);
3752 Ok(())
3753 }
3754
3755 #[test]
3756 fn infers_any_arg_function_return_before_body_compile() -> anyhow::Result<()> {
3757 let vm = Vm::with_all()?;
3758 vm.import_code(
3759 "vm_infer_any_arg_return",
3760 br#"
3761 pub fn caller(candidate) {
3762 let center = polygon_center(candidate.visualPolygon);
3763 center[0]
3764 }
3765
3766 pub fn polygon_center(point_list) {
3767 let total_x = 0;
3768 let total_y = 0;
3769 let count = 0;
3770 if point_list.is_list() {
3771 for point in point_list {
3772 if point.is_list() && point.len() >= 2 {
3773 total_x += point[0];
3774 total_y += point[1];
3775 count += 1;
3776 }
3777 }
3778 }
3779 if count == 0 {
3780 return [0, 0];
3781 }
3782 [total_x / count, total_y / count]
3783 }
3784 "#
3785 .to_vec(),
3786 )?;
3787
3788 let compiled = vm.get_fn("vm_infer_any_arg_return::caller", &[Type::Any])?;
3789 assert_eq!(compiled.ret_ty(), &Type::Any);
3790 let caller: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
3791 let candidate = dynamic::map!(
3792 "visualPolygon"=> Dynamic::list(vec![
3793 Dynamic::list(vec![2i64.into(), 4i64.into()]),
3794 Dynamic::list(vec![6i64.into(), 8i64.into()]),
3795 ])
3796 );
3797 let result = unsafe { &*caller(&candidate) };
3798 assert_eq!(result.as_int(), Some(4));
3799 Ok(())
3800 }
3801
3802 #[test]
3803 fn recursive_factorial_keeps_static_return_type() -> anyhow::Result<()> {
3804 let vm = Vm::with_all()?;
3805 vm.import_code(
3806 "vm_recursive_factorial",
3807 br#"
3808 fn factorial(n: i64) {
3809 if n <= 1 {
3810 return 1;
3811 }
3812 n * factorial(n - 1)
3813 }
3814
3815 pub fn run(n: i64) {
3816 factorial(n)
3817 }
3818 "#
3819 .to_vec(),
3820 )?;
3821
3822 let compiled = vm.get_fn("vm_recursive_factorial::run", &[Type::I64])?;
3823 assert_eq!(compiled.ret_ty(), &Type::I64);
3824 let run: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3825 assert_eq!(run(5), 120);
3826 Ok(())
3827 }
3828
3829 #[test]
3830 fn explicit_const_generic_function_calls_generate_distinct_variants() -> anyhow::Result<()> {
3831 let vm = Vm::with_all()?;
3832 vm.import_code(
3833 "vm_generic_const_variants",
3834 br#"
3835 fn value<N>() {
3836 N
3837 }
3838
3839 pub fn two() {
3840 value::<2>()
3841 }
3842
3843 pub fn three() {
3844 value::<3>()
3845 }
3846 "#
3847 .to_vec(),
3848 )?;
3849
3850 let compiled = vm.get_fn("vm_generic_const_variants::two", &[])?;
3851 assert_eq!(compiled.ret_ty(), &Type::I32);
3852 let two: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
3853 assert_eq!(two(), 2);
3854
3855 let compiled = vm.get_fn("vm_generic_const_variants::three", &[])?;
3856 assert_eq!(compiled.ret_ty(), &Type::I32);
3857 let three: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
3858 assert_eq!(three(), 3);
3859 Ok(())
3860 }
3861
3862 #[test]
3863 fn generic_function_body_resolves_private_generic_helper_after_import() -> anyhow::Result<()> {
3864 let vm = Vm::with_all()?;
3865 vm.import_code(
3866 "vm_generic_private_helper",
3867 br#"
3868 fn helper<N>() {
3869 N
3870 }
3871
3872 pub fn bench<N>() {
3873 helper::<N>()
3874 }
3875 "#
3876 .to_vec(),
3877 )?;
3878
3879 let compiled = vm.get_fn_with_params("vm_generic_private_helper::bench", &[], &[Type::ConstInt(7)])?;
3880 assert_eq!(compiled.ret_ty(), &Type::I32);
3881 let run: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
3882 assert_eq!(run(), 7);
3883 Ok(())
3884 }
3885
3886 #[test]
3887 fn const_generic_repeat_array_initializes_all_items() -> anyhow::Result<()> {
3888 let vm = Vm::with_all()?;
3889 vm.import_code(
3890 "vm_generic_repeat_array",
3891 br#"
3892 fn bench<N>() {
3893 let is_prime = [true; N];
3894 is_prime[0] = false;
3895 is_prime[1] = false;
3896 let count = 0i64;
3897 for p in 2i64..N {
3898 if is_prime[p] == true {
3899 count = count + 1;
3900 let step = p;
3901 let j = p * p;
3902 while j < N {
3903 is_prime[j] = false;
3904 j = j + step;
3905 }
3906 }
3907 }
3908 count
3909 }
3910
3911 pub fn run() {
3912 bench::<10>()
3913 }
3914
3915 pub fn run_1000() {
3916 bench::<1000>()
3917 }
3918
3919 pub fn run_100000() {
3920 bench::<100000>()
3921 }
3922 "#
3923 .to_vec(),
3924 )?;
3925
3926 let compiled = vm.get_fn("vm_generic_repeat_array::run", &[])?;
3927 assert_eq!(compiled.ret_ty(), &Type::I64);
3928 let run: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3929 assert_eq!(run(), 4);
3930
3931 let compiled = vm.get_fn("vm_generic_repeat_array::run_1000", &[])?;
3932 assert_eq!(compiled.ret_ty(), &Type::I64);
3933 let run_1000: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3934 assert_eq!(run_1000(), 168);
3935
3936 let compiled = vm.get_fn("vm_generic_repeat_array::run_100000", &[])?;
3937 assert_eq!(compiled.ret_ty(), &Type::I64);
3938 let run_100000: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3939 assert_eq!(run_100000(), 9592);
3940 Ok(())
3941 }
3942
3943 #[test]
3944 fn repeat_array_initializes_scalar_patterns() -> anyhow::Result<()> {
3945 let vm = Vm::with_all()?;
3946 vm.import_code(
3947 "vm_repeat_scalar_patterns",
3948 br#"
3949 pub fn count_true() {
3950 let items = [true; 100000];
3951 let count = 0i64;
3952 for idx in 0i64..100000 {
3953 if items[idx] == true {
3954 count = count + 1;
3955 }
3956 }
3957 count
3958 }
3959
3960 pub fn i32_pair() {
3961 let items = [-7i32; 1000];
3962 items[0i64] + items[999i64]
3963 }
3964
3965 pub fn i64_pair() {
3966 let items = [1234567890123i64; 1000];
3967 items[0i64] + items[999i64]
3968 }
3969
3970 pub fn f64_pair() {
3971 let items = [1.5f64; 1000];
3972 items[0i64] + items[999i64]
3973 }
3974 "#
3975 .to_vec(),
3976 )?;
3977
3978 let compiled = vm.get_fn("vm_repeat_scalar_patterns::count_true", &[])?;
3979 assert_eq!(compiled.ret_ty(), &Type::I64);
3980 let count_true: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3981 assert_eq!(count_true(), 100000);
3982
3983 let compiled = vm.get_fn("vm_repeat_scalar_patterns::i32_pair", &[])?;
3984 assert_eq!(compiled.ret_ty(), &Type::I32);
3985 let i32_pair: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
3986 assert_eq!(i32_pair(), -14);
3987
3988 let compiled = vm.get_fn("vm_repeat_scalar_patterns::i64_pair", &[])?;
3989 assert_eq!(compiled.ret_ty(), &Type::I64);
3990 let i64_pair: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
3991 assert_eq!(i64_pair(), 2469135780246);
3992
3993 let compiled = vm.get_fn("vm_repeat_scalar_patterns::f64_pair", &[])?;
3994 assert_eq!(compiled.ret_ty(), &Type::F64);
3995 let f64_pair: extern "C" fn() -> f64 = unsafe { std::mem::transmute(compiled.ptr()) };
3996 assert_eq!(f64_pair(), 3.0);
3997 Ok(())
3998 }
3999
4000 #[test]
4001 fn bool_array_store_normalizes_condition_values() -> anyhow::Result<()> {
4002 let vm = Vm::with_all()?;
4003 vm.import_code(
4004 "vm_bool_array_store",
4005 br#"
4006 pub fn run() {
4007 let items = [false; 4];
4008 items[1] = 3i64 > 2i64;
4009 items[2] = 3i64 < 2i64;
4010 if items[1] == true && items[2] == false {
4011 1i64
4012 } else {
4013 0i64
4014 }
4015 }
4016 "#
4017 .to_vec(),
4018 )?;
4019
4020 let compiled = vm.get_fn("vm_bool_array_store::run", &[])?;
4021 assert_eq!(compiled.ret_ty(), &Type::I64);
4022 let run: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4023 assert_eq!(run(), 1);
4024 Ok(())
4025 }
4026
4027 #[test]
4028 fn bool_array_large_sequential_writes() -> anyhow::Result<()> {
4029 let vm = Vm::with_all()?;
4030 vm.import_code(
4031 "vm_bool_array_large_writes",
4032 br#"
4033 pub fn run() {
4034 let items = [true; 100000];
4035 for idx in 0i64..100000 {
4036 items[idx] = false;
4037 }
4038 let count = 0i64;
4039 for idx in 0i64..100000 {
4040 if items[idx] == false {
4041 count = count + 1;
4042 }
4043 }
4044 count
4045 }
4046 "#
4047 .to_vec(),
4048 )?;
4049
4050 let compiled = vm.get_fn("vm_bool_array_large_writes::run", &[])?;
4051 assert_eq!(compiled.ret_ty(), &Type::I64);
4052 let run: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4053 assert_eq!(run(), 100000);
4054 Ok(())
4055 }
4056
4057 #[test]
4058 fn bool_array_sieve_style_indices_stay_in_bounds() -> anyhow::Result<()> {
4059 let vm = Vm::with_all()?;
4060 vm.import_code(
4061 "vm_bool_array_sieve_indices",
4062 br#"
4063 pub fn run() {
4064 let items = [true; 100000];
4065 let writes = 0i64;
4066 for p in 2i64..100000 {
4067 let step = p;
4068 let j = p * p;
4069 while j < 100000 {
4070 items[j] = false;
4071 writes = writes + 1;
4072 j = j + step;
4073 }
4074 }
4075 writes
4076 }
4077 "#
4078 .to_vec(),
4079 )?;
4080
4081 let compiled = vm.get_fn("vm_bool_array_sieve_indices::run", &[])?;
4082 assert_eq!(compiled.ret_ty(), &Type::I64);
4083 let run: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4084 assert!(run() > 0);
4085 Ok(())
4086 }
4087
4088 #[test]
4089 fn sieve_style_indices_compute_in_bounds_without_array_write() -> anyhow::Result<()> {
4090 let vm = Vm::with_all()?;
4091 vm.import_code(
4092 "vm_sieve_indices_no_write",
4093 br#"
4094 pub fn run() {
4095 let max_j = 0i64;
4096 for p in 2i64..100000 {
4097 let step = p;
4098 let j = p * p;
4099 while j < 100000 {
4100 if j < 0i64 {
4101 return -1i64;
4102 }
4103 if j > max_j {
4104 max_j = j;
4105 }
4106 j = j + step;
4107 }
4108 }
4109 max_j
4110 }
4111 "#
4112 .to_vec(),
4113 )?;
4114
4115 let compiled = vm.get_fn("vm_sieve_indices_no_write::run", &[])?;
4116 assert_eq!(compiled.ret_ty(), &Type::I64);
4117 let run: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4118 assert_eq!(run(), 99999);
4119 Ok(())
4120 }
4121
4122 #[test]
4123 fn dynamic_list_index_sum_uses_static_accumulator_type() -> anyhow::Result<()> {
4124 let vm = Vm::with_all()?;
4125 vm.import_code(
4126 "vm_dynamic_index_sum",
4127 br#"
4128 pub fn sum_list(n: i64) {
4129 let l = [];
4130 for i in 0..n {
4131 l.push(i);
4132 }
4133 let sum = 0i64;
4134 for j in 0..n {
4135 sum = sum + l[j];
4136 }
4137 sum
4138 }
4139 "#
4140 .to_vec(),
4141 )?;
4142
4143 let compiled = vm.get_fn("vm_dynamic_index_sum::sum_list", &[Type::I64])?;
4144 let sum_list_id = vm.jit.write().compiler.symbols.get_id("vm_dynamic_index_sum::sum_list")?;
4145 let hints = vm.jit.write().compiler.inferred_local_type_hints(sum_list_id, &[], &[Type::I64]);
4146 assert!(hints.iter().any(|ty| matches!(ty, Some(Type::List(elem)) if elem.as_ref() == &Type::I64)), "local type hints: {:?}", hints);
4147 assert_eq!(compiled.ret_ty(), &Type::I64);
4148 let sum_list: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4149 assert_eq!(sum_list(1000), 499500);
4150 Ok(())
4151 }
4152
4153 #[test]
4154 fn loop_pushed_list_is_typed_vector() -> anyhow::Result<()> {
4155 let vm = Vm::with_all()?;
4156 vm.import_code(
4157 "vm_loop_pushed_list",
4158 br#"
4159 pub fn make(n: i64) {
4160 let l = [];
4161 for i in 0..n {
4162 l.push(i);
4163 }
4164 l
4165 }
4166 "#
4167 .to_vec(),
4168 )?;
4169 let compiled = vm.get_fn("vm_loop_pushed_list::make", &[Type::I64])?;
4170 let make: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4171 let result = unsafe { &*make(3) };
4172 assert!(matches!(result, Dynamic::VecI64(v) if v == &vec![0, 1, 2]), "expected flat VecI64, got: {:?}", result);
4173 Ok(())
4174 }
4175
4176 #[test]
4177 fn inferred_empty_list_uses_typed_dynamic_vector() -> anyhow::Result<()> {
4178 let vm = Vm::with_all()?;
4179 vm.import_code(
4180 "vm_inferred_typed_list",
4181 br#"
4182 pub fn make() {
4183 let l = [];
4184 l.push(1i64);
4185 l
4186 }
4187 "#
4188 .to_vec(),
4189 )?;
4190
4191 let compiled = vm.get_fn("vm_inferred_typed_list::make", &[])?;
4192 assert_eq!(compiled.ret_ty(), &Type::Any);
4193 let make: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4194 let result = unsafe { &*make() };
4195 assert!(matches!(result, Dynamic::VecI64(values) if values == &vec![1]), "result: {:?}", result);
4196 Ok(())
4197 }
4198
4199 #[test]
4200 fn for_in_iterates_list_filled_in_same_function() -> anyhow::Result<()> {
4201 let vm = Vm::with_all()?;
4202 vm.import_code(
4203 "vm_for_in_local_pushed_list",
4204 br#"
4205 pub fn sum_i32_items() {
4206 let items = [];
4207 items.push(6000i32);
4208 items.push(4000i32);
4209 let total = 0i32;
4210 for item in items {
4211 total += item;
4212 }
4213 total
4214 }
4215
4216 pub fn sum_split_bps() {
4217 let splits = [];
4218 splits.push({ bps: "6000" });
4219 splits.push({ bps: 4000 });
4220 let total = 0i32;
4221 let count = 0i32;
4222 for split in splits {
4223 total += split.bps as i32;
4224 count += 1i32;
4225 }
4226 total + count
4227 }
4228 "#
4229 .to_vec(),
4230 )?;
4231
4232 let compiled = vm.get_fn("vm_for_in_local_pushed_list::sum_i32_items", &[])?;
4233 assert_eq!(compiled.ret_ty(), &Type::I32);
4234 let sum_i32_items: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
4235 assert_eq!(sum_i32_items(), 10000);
4236
4237 let compiled = vm.get_fn("vm_for_in_local_pushed_list::sum_split_bps", &[])?;
4238 assert_eq!(compiled.ret_ty(), &Type::I32);
4239 let sum_split_bps: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
4240 assert_eq!(sum_split_bps(), 10002);
4241 Ok(())
4242 }
4243
4244 #[test]
4245 fn inferred_list_shortcuts_cover_scalar_types() -> anyhow::Result<()> {
4246 let vm = Vm::with_all()?;
4247 vm.import_code(
4248 "vm_inferred_list_shortcuts",
4249 br#"
4250 pub fn second_bool() {
4251 let l = [];
4252 l.push(true);
4253 l.push(false);
4254 l[1]
4255 }
4256
4257 pub fn first_u8() {
4258 let l = [];
4259 l.push(7u8);
4260 l[0]
4261 }
4262
4263 pub fn sum_i32(n: i64) {
4264 let l = [];
4265 for i in 0..n {
4266 l.push(i as i32);
4267 }
4268 let sum = 0i32;
4269 for j in 0..n {
4270 sum = sum + l[j];
4271 }
4272 sum
4273 }
4274
4275 pub fn sum_f32(n: i64) {
4276 let l = [];
4277 for i in 0..n {
4278 l.push(i as f32);
4279 }
4280 let sum = 0f32;
4281 for j in 0..n {
4282 sum = sum + l[j];
4283 }
4284 sum
4285 }
4286
4287 pub fn second_str() {
4288 let l = [];
4289 l.push("first");
4290 l.push("second");
4291 l[1]
4292 }
4293 "#
4294 .to_vec(),
4295 )?;
4296
4297 let compiled = vm.get_fn("vm_inferred_list_shortcuts::second_bool", &[])?;
4298 let second_bool_id = vm.jit.write().compiler.symbols.get_id("vm_inferred_list_shortcuts::second_bool")?;
4299 let hints = vm.jit.write().compiler.inferred_local_type_hints(second_bool_id, &[], &[]);
4300 assert!(hints.iter().any(|ty| matches!(ty, Some(Type::List(elem)) if elem.as_ref() == &Type::Bool)), "bool local type hints: {:?}", hints);
4301 assert_eq!(compiled.ret_ty(), &Type::Bool);
4302 let second_bool: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
4303 assert!(!second_bool());
4304
4305 let compiled = vm.get_fn("vm_inferred_list_shortcuts::first_u8", &[])?;
4306 let first_u8_id = vm.jit.write().compiler.symbols.get_id("vm_inferred_list_shortcuts::first_u8")?;
4307 let hints = vm.jit.write().compiler.inferred_local_type_hints(first_u8_id, &[], &[]);
4308 assert!(hints.iter().any(|ty| matches!(ty, Some(Type::List(elem)) if elem.as_ref() == &Type::U8)), "u8 local type hints: {:?}", hints);
4309 assert_eq!(compiled.ret_ty(), &Type::U8);
4310 let first_u8: extern "C" fn() -> u8 = unsafe { std::mem::transmute(compiled.ptr()) };
4311 assert_eq!(first_u8(), 7);
4312
4313 let compiled = vm.get_fn("vm_inferred_list_shortcuts::sum_i32", &[Type::I64])?;
4314 let sum_i32_id = vm.jit.write().compiler.symbols.get_id("vm_inferred_list_shortcuts::sum_i32")?;
4315 let hints = vm.jit.write().compiler.inferred_local_type_hints(sum_i32_id, &[], &[Type::I64]);
4316 assert!(hints.iter().any(|ty| matches!(ty, Some(Type::List(elem)) if elem.as_ref() == &Type::I32)), "i32 local type hints: {:?}", hints);
4317 assert_eq!(compiled.ret_ty(), &Type::I32);
4318 let sum_i32: extern "C" fn(i64) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
4319 assert_eq!(sum_i32(100), 4950);
4320
4321 let compiled = vm.get_fn("vm_inferred_list_shortcuts::sum_f32", &[Type::I64])?;
4322 let sum_f32_id = vm.jit.write().compiler.symbols.get_id("vm_inferred_list_shortcuts::sum_f32")?;
4323 let hints = vm.jit.write().compiler.inferred_local_type_hints(sum_f32_id, &[], &[Type::I64]);
4324 assert!(hints.iter().any(|ty| matches!(ty, Some(Type::List(elem)) if elem.as_ref() == &Type::F32)), "f32 local type hints: {:?}", hints);
4325 assert_eq!(compiled.ret_ty(), &Type::F32);
4326 let sum_f32: extern "C" fn(i64) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
4327 assert_eq!(sum_f32(10), 45.0);
4328
4329 let compiled = vm.get_fn("vm_inferred_list_shortcuts::second_str", &[])?;
4330 let second_str_id = vm.jit.write().compiler.symbols.get_id("vm_inferred_list_shortcuts::second_str")?;
4331 let hints = vm.jit.write().compiler.inferred_local_type_hints(second_str_id, &[], &[]);
4332 assert!(hints.iter().any(|ty| matches!(ty, Some(Type::List(elem)) if elem.as_ref() == &Type::Str)), "str local type hints: {:?}", hints);
4333 assert_eq!(compiled.ret_ty(), &Type::Str);
4334 let second_str: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4335 let result = unsafe { &*second_str() };
4336 assert_eq!(result.as_str(), "second");
4337 Ok(())
4338 }
4339
4340 #[test]
4341 fn inferred_list_supports_bracket_set_idx() -> anyhow::Result<()> {
4342 let vm = Vm::with_all()?;
4343 vm.import_code(
4344 "vm_inferred_list_set_idx",
4345 br#"
4346 pub fn swap_first_two() {
4347 let items = [];
4348 items.push(1i64);
4349 items.push(2i64);
4350 let j = 0i64;
4351 let a = items[j];
4352 let b = items[j + 1];
4353 items[j] = b;
4354 items[j + 1] = a;
4355 items[0] * 10i64 + items[1]
4356 }
4357
4358 pub fn replace_string() {
4359 let items = [];
4360 items.push("old");
4361 items[0] = "new";
4362 items[0]
4363 }
4364 "#
4365 .to_vec(),
4366 )?;
4367
4368 let compiled = vm.get_fn("vm_inferred_list_set_idx::swap_first_two", &[])?;
4369 assert_eq!(compiled.ret_ty(), &Type::I64);
4370 let swap_first_two: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4371 assert_eq!(swap_first_two(), 21);
4372
4373 let compiled = vm.get_fn("vm_inferred_list_set_idx::replace_string", &[])?;
4374 assert_eq!(compiled.ret_ty(), &Type::Str);
4375 let replace_string: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4376 let result = unsafe { &*replace_string() };
4377 assert_eq!(result.as_str(), "new");
4378 Ok(())
4379 }
4380
4381 #[test]
4382 fn root_get_returns_null_for_missing_key_which_compares_correctly() -> anyhow::Result<()> {
4383 let vm = Vm::with_all()?;
4384 vm.import_code(
4385 "vm_root_get_missing",
4386 br#"
4387 pub fn check_missing() {
4388 let existing = root::get("local/vm_root_get_missing_test");
4389 if existing.is_map() {
4390 return false;
4391 }
4392 true
4393 }
4394 "#
4395 .to_vec(),
4396 )?;
4397
4398 let compiled = vm.get_fn("vm_root_get_missing::check_missing", &[])?;
4399 assert_eq!(compiled.ret_ty(), &Type::Bool);
4400 let check_missing: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
4401 assert!(check_missing());
4402 Ok(())
4403 }
4404
4405 #[test]
4406 fn map_get_key_on_null_map_returns_null() -> anyhow::Result<()> {
4407 let vm = Vm::with_all()?;
4408 vm.import_code(
4409 "vm_get_key_null_map",
4410 br#"
4411 pub fn get_key_null(data) {
4412 data.get_key("missing")
4413 }
4414 "#
4415 .to_vec(),
4416 )?;
4417
4418 let compiled = vm.get_fn("vm_get_key_null_map::get_key_null", &[Type::Any])?;
4419 assert_eq!(compiled.ret_ty(), &Type::Any);
4420 let get_key_null: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4421
4422 let data_map = dynamic::map!("exists"=> 1i64);
4423 let missing = unsafe { &*get_key_null(&data_map) };
4424 assert!(missing.is_null());
4425
4426 let null = Dynamic::Null;
4427 let result = unsafe { &*get_key_null(&null) };
4428 assert!(result.is_null());
4429 Ok(())
4430 }
4431
4432 #[test]
4433 fn keys_on_empty_map_returns_empty_list() -> anyhow::Result<()> {
4434 let vm = Vm::with_all()?;
4435 vm.import_code(
4436 "vm_keys_empty_map",
4437 br#"
4438 pub fn empty_map_keys() {
4439 let data = {};
4440 data.keys().len()
4441 }
4442 "#
4443 .to_vec(),
4444 )?;
4445
4446 let compiled = vm.get_fn("vm_keys_empty_map::empty_map_keys", &[])?;
4447 assert_eq!(compiled.ret_ty(), &Type::I32);
4448 let empty_map_keys: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
4449 assert_eq!(empty_map_keys(), 0);
4450 Ok(())
4451 }
4452
4453 #[test]
4454 fn cast_between_all_integer_widths() -> anyhow::Result<()> {
4455 let vm = Vm::with_all()?;
4456 vm.import_code(
4457 "vm_cast_integer_widths",
4458 br#"
4459 pub fn i64_to_i32(value: i64) {
4460 value as i32
4461 }
4462
4463 pub fn i32_to_i64(value: i32) {
4464 value as i64
4465 }
4466
4467 pub fn u32_to_i64(value: u32) {
4468 value as i64
4469 }
4470 "#
4471 .to_vec(),
4472 )?;
4473
4474 let compiled = vm.get_fn("vm_cast_integer_widths::i64_to_i32", &[Type::I64])?;
4475 assert_eq!(compiled.ret_ty(), &Type::I32);
4476 let i64_to_i32: extern "C" fn(i64) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
4477 assert_eq!(i64_to_i32(42), 42);
4478
4479 let compiled = vm.get_fn("vm_cast_integer_widths::i32_to_i64", &[Type::I32])?;
4480 assert_eq!(compiled.ret_ty(), &Type::I64);
4481 let i32_to_i64: extern "C" fn(i32) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4482 assert_eq!(i32_to_i64(-1), -1);
4483
4484 let compiled = vm.get_fn("vm_cast_integer_widths::u32_to_i64", &[Type::U32])?;
4485 assert_eq!(compiled.ret_ty(), &Type::I64);
4486 let u32_to_i64: extern "C" fn(u32) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4487 assert_eq!(u32_to_i64(42), 42);
4488 Ok(())
4489 }
4490
4491 #[test]
4492 fn boolean_literals_in_complex_expression_trees() -> anyhow::Result<()> {
4493 let vm = Vm::with_all()?;
4494 vm.import_code(
4495 "vm_complex_boolean",
4496 br#"
4497 pub fn exclusive_or(a: bool, b: bool) {
4498 (a && !b) || (!a && b)
4499 }
4500
4501 pub fn implies(a: bool, b: bool) {
4502 !a || b
4503 }
4504 "#
4505 .to_vec(),
4506 )?;
4507
4508 let compiled = vm.get_fn("vm_complex_boolean::exclusive_or", &[Type::Bool, Type::Bool])?;
4509 assert_eq!(compiled.ret_ty(), &Type::Bool);
4510 let exclusive_or: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
4511 assert!(exclusive_or(true, false));
4512 assert!(exclusive_or(false, true));
4513 assert!(!exclusive_or(true, true));
4514 assert!(!exclusive_or(false, false));
4515
4516 let compiled = vm.get_fn("vm_complex_boolean::implies", &[Type::Bool, Type::Bool])?;
4517 assert_eq!(compiled.ret_ty(), &Type::Bool);
4518 let implies: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
4519 assert!(implies(false, true));
4520 assert!(implies(false, false));
4521 assert!(implies(true, true));
4522 assert!(!implies(true, false));
4523 Ok(())
4524 }
4525
4526 #[test]
4527 fn concrete_struct_method_returning_self_type() -> anyhow::Result<()> {
4528 let vm = Vm::with_all()?;
4529 vm.import_code(
4530 "vm_struct_method_self",
4531 br#"
4532 pub struct Vec3 {
4533 x: f64,
4534 y: f64,
4535 z: f64,
4536 }
4537
4538 impl Vec3 {
4539 pub fn add(self: Vec3, other: Vec3) {
4540 Vec3{x: self.x + other.x, y: self.y + other.y, z: self.z + other.z}
4541 }
4542 }
4543
4544 pub fn run() {
4545 let v1 = Vec3{x: 1.0f64, y: 2.0f64, z: 3.0f64};
4546 let v2 = Vec3{x: 4.0f64, y: 5.0f64, z: 6.0f64};
4547 let sum = v1.add(v2);
4548 sum.x + sum.y + sum.z
4549 }
4550 "#
4551 .to_vec(),
4552 )?;
4553
4554 let compiled = vm.get_fn("vm_struct_method_self::run", &[])?;
4555 assert_eq!(compiled.ret_ty(), &Type::F64);
4556 let run: extern "C" fn() -> f64 = unsafe { std::mem::transmute(compiled.ptr()) };
4557 assert_eq!(run(), 21.0);
4558 Ok(())
4559 }
4560
4561 #[test]
4562 fn deep_nested_struct_access_with_multiple_field_levels() -> anyhow::Result<()> {
4563 let vm = Vm::with_all()?;
4564 vm.import_code(
4565 "vm_deep_nested_struct",
4566 br#"
4567 pub struct A {
4568 value: i64,
4569 }
4570
4571 pub struct B {
4572 a: A,
4573 }
4574
4575 pub struct C {
4576 b: B,
4577 }
4578
4579 pub fn direct_access() {
4580 let c = C{b: B{a: A{value: 99}}};
4581 c.b.a.value
4582 }
4583
4584 pub fn via_variable() {
4585 let c = C{b: B{a: A{value: 77}}};
4586 let b = c.b;
4587 let a = b.a;
4588 a.value
4589 }
4590 "#
4591 .to_vec(),
4592 )?;
4593
4594 let compiled = vm.get_fn("vm_deep_nested_struct::direct_access", &[])?;
4595 assert_eq!(compiled.ret_ty(), &Type::I64);
4596 let direct_access: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4597 assert_eq!(direct_access(), 99);
4598
4599 let compiled = vm.get_fn("vm_deep_nested_struct::via_variable", &[])?;
4600 assert_eq!(compiled.ret_ty(), &Type::I64);
4601 let via_variable: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4602 assert_eq!(via_variable(), 77);
4603 Ok(())
4604 }
4605
4606 #[test]
4607 fn array_index_with_dynamic_value_via_method() -> anyhow::Result<()> {
4608 let vm = Vm::with_all()?;
4609 vm.import_code(
4610 "vm_array_idx_dynamic",
4611 br#"
4612 pub fn get_by_idx(list, idx) {
4613 list.get_idx(idx)
4614 }
4615 "#
4616 .to_vec(),
4617 )?;
4618
4619 let compiled = vm.get_fn("vm_array_idx_dynamic::get_by_idx", &[Type::Any, Type::I64])?;
4620 assert_eq!(compiled.ret_ty(), &Type::Any);
4621 let get_by_idx: extern "C" fn(*const Dynamic, i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4622
4623 let list = Dynamic::list(vec!["a".into(), "b".into()]);
4624 let first = unsafe { &*get_by_idx(&list, 0) };
4625 assert_eq!(first.as_str(), "a");
4626
4627 let out = unsafe { &*get_by_idx(&list, 10) };
4628 assert!(out.is_null());
4629 Ok(())
4630 }
4631
4632 #[test]
4633 fn dynamic_field_access_with_optional_or_fallback() -> anyhow::Result<()> {
4634 let vm = Vm::with_all()?;
4635 vm.import_code(
4636 "vm_dynamic_or_fallback",
4637 br#"
4638 pub fn with_fallback(data) {
4639 if data.contains("name") { data.name } else { "unknown" }
4640 }
4641
4642 pub fn with_fallback_missing(data) {
4643 if data.contains("nickname") { data.nickname } else { "unnamed" }
4644 }
4645 "#
4646 .to_vec(),
4647 )?;
4648
4649 let compiled = vm.get_fn("vm_dynamic_or_fallback::with_fallback", &[Type::Any])?;
4650 assert_eq!(compiled.ret_ty(), &Type::Any);
4651 let with_fallback: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4652 let data = dynamic::map!("name"=> "Alice");
4653 let result = unsafe { &*with_fallback(&data) };
4654 assert_eq!(result.as_str(), "Alice");
4655
4656 let compiled = vm.get_fn("vm_dynamic_or_fallback::with_fallback_missing", &[Type::Any])?;
4657 let with_fallback_missing: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
4658 let result = unsafe { &*with_fallback_missing(&data) };
4659 assert_eq!(result.as_str(), "unnamed");
4660 Ok(())
4661 }
4662
4663 #[test]
4664 fn for_in_loop_iterates_over_list_and_map_directly() -> anyhow::Result<()> {
4665 let vm = Vm::with_all()?;
4666 vm.import_code(
4667 "vm_for_in_collection",
4668 br#"
4669 pub fn sum_list(items) {
4670 let total = 0i64;
4671 for item in items {
4672 total = total + 1;
4673 }
4674 total
4675 }
4676
4677 pub fn count_map_keys(data) {
4678 let count = 0i64;
4679 for key in data.keys() {
4680 count = count + 1;
4681 }
4682 count
4683 }
4684
4685 pub fn for_in_list_works(items) {
4686 let exists = false;
4687 for item in items {
4688 exists = true;
4689 }
4690 exists
4691 }
4692
4693 pub fn for_in_map_values_works(data) {
4694 let exists = false;
4695 for value in data {
4696 exists = true;
4697 }
4698 exists
4699 }
4700 "#
4701 .to_vec(),
4702 )?;
4703
4704 let compiled = vm.get_fn("vm_for_in_collection::sum_list", &[Type::Any])?;
4705 assert_eq!(compiled.ret_ty(), &Type::I64);
4706 let sum_list: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4707 let items = Dynamic::list(vec![Dynamic::from(1i64), Dynamic::from(2i64), Dynamic::from(3i64)]);
4708 assert_eq!(sum_list(&items), 3);
4709
4710 let data = dynamic::map!("x"=> 1i64, "y"=> 2i64);
4711 let compiled = vm.get_fn("vm_for_in_collection::count_map_keys", &[Type::Any])?;
4712 assert_eq!(compiled.ret_ty(), &Type::I64);
4713 let count_map_keys: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
4714 assert_eq!(count_map_keys(&data), 2);
4715
4716 let compiled = vm.get_fn("vm_for_in_collection::for_in_list_works", &[Type::Any])?;
4717 assert_eq!(compiled.ret_ty(), &Type::Bool);
4718 let for_in_list_works: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
4719 let empty = Dynamic::list(Vec::new());
4720 assert!(!for_in_list_works(&empty));
4721 assert!(for_in_list_works(&items));
4722
4723 let compiled = vm.get_fn("vm_for_in_collection::for_in_map_values_works", &[Type::Any])?;
4724 assert_eq!(compiled.ret_ty(), &Type::Bool);
4725 let for_in_map_values_works: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
4726 let empty_map = dynamic::map!();
4727 assert!(!for_in_map_values_works(&empty_map));
4728 assert!(for_in_map_values_works(&data));
4729
4730 Ok(())
4731 }
4732
4733 #[test]
4734 fn concurrent_100_threads_no_memory_leak() -> anyhow::Result<()> {
4735 let vm = Vm::with_all()?;
4736 vm.import_code(
4737 "vm_stress",
4738 br#"
4739 pub fn heavy_alloc(idx: i64) {
4740 let items = [];
4741 let i = 0;
4742 while i < 50 {
4743 items.push({
4744 id: i + idx,
4745 name: "item-" + i,
4746 tags: ["tag-a", "tag-b", "tag-c"],
4747 meta: {
4748 created: 1234567890i64,
4749 score: (i * 3.14f64) as i64,
4750 extra: "prefix/" + i + "/" + idx
4751 }
4752 });
4753 i = i + 1;
4754 }
4755 items
4756 }
4757
4758 pub fn string_concat_stress() {
4759 let i = 0;
4760 let result = "";
4761 while i < 200 {
4762 result = result + "data-" + i + ",";
4763 i = i + 1;
4764 }
4765 result
4766 }
4767 "#
4768 .to_vec(),
4769 )?;
4770
4771 let (heavy_ptr, _) = vm.get_fn_ptr("vm_stress::heavy_alloc", &[Type::I64])?;
4772 let (concat_ptr, _) = vm.get_fn_ptr("vm_stress::string_concat_stress", &[])?;
4773
4774 let threads: usize = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).max(100);
4775 let iters_per_thread = 200;
4776 let total_calls = threads * iters_per_thread * 2;
4777
4778 let before = current_rss_kb();
4779 eprintln!("threads={threads} iters_per_thread={iters_per_thread} total_calls={total_calls} rss_before={before}KB");
4780
4781 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
4783 let r1 = current_rss_kb();
4784 eprintln!("rss_after_round1={r1}KB");
4785
4786 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
4788 let r2 = current_rss_kb();
4789 eprintln!("rss_after_round2={r2}KB");
4790
4791 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
4793 let r3 = current_rss_kb();
4794 eprintln!("rss_after_round3={r3}KB");
4795
4796 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
4798 let r4 = current_rss_kb();
4799 eprintln!("rss_after_round4={r4}KB");
4800
4801 let d12 = r2.saturating_sub(r1);
4803 let d23 = r3.saturating_sub(r2);
4804 let d34 = r4.saturating_sub(r3);
4805 eprintln!("delta_r1→r2={d12}KB delta_r2→r3={d23}KB delta_r3→r4={d34}KB");
4806
4807 let max_growth_kb = 20 * 1024;
4809 assert!(d34 < max_growth_kb, "memory keeps growing after allocator warm-up: round1={r1} round2={r2} round3={r3} round4={r4} delta12={d12}KB delta23={d23}KB delta34={d34}KB (max stable growth={max_growth_kb}KB)");
4810
4811 Ok(())
4812 }
4813
4814 fn run_stress_round(threads: usize, iters: usize, heavy_ptr: usize, concat_ptr: usize) {
4815 std::thread::scope(|scope| {
4816 let mut handles = Vec::with_capacity(threads);
4817 for t in 0..threads {
4818 let heavy_ptr = heavy_ptr;
4819 let concat_ptr = concat_ptr;
4820 handles.push(scope.spawn(move || {
4821 let heavy_fn: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(heavy_ptr as *const u8) };
4822 let concat_fn: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(concat_ptr as *const u8) };
4823 for i in 0..iters {
4824 let r_ptr = heavy_fn((t * iters + i) as i64);
4826 assert!(!r_ptr.is_null());
4827 unsafe {
4828 let r = &*r_ptr;
4829 assert!(r.len() > 0, "heavy_alloc returned empty list");
4830 drop(Box::from_raw(r_ptr as *mut Dynamic));
4831 }
4832
4833 let s_ptr = concat_fn();
4835 assert!(!s_ptr.is_null());
4836 unsafe {
4837 let s = &*s_ptr;
4838 assert!(s.len() > 0, "string_concat_stress returned empty");
4839 drop(Box::from_raw(s_ptr as *mut Dynamic));
4840 }
4841 }
4842 }));
4843 }
4844 for h in handles {
4845 h.join().unwrap();
4846 }
4847 });
4848 }
4849
4850 fn current_rss_kb() -> u64 {
4851 let pid = std::process::id();
4853 if let Ok(output) = std::process::Command::new("ps").args(["-p", &pid.to_string(), "-o", "rss="]).output() {
4854 if let Ok(s) = String::from_utf8(output.stdout) {
4855 if let Some(kb) = s.trim().parse::<u64>().ok() {
4856 return kb;
4857 }
4858 }
4859 }
4860 if let Ok(statm) = std::fs::read_to_string("/proc/self/statm") {
4862 let parts: Vec<&str> = statm.split_whitespace().collect();
4863 if let Some(rss_pages) = parts.get(1).and_then(|s| s.parse::<u64>().ok()) {
4864 return rss_pages * 4; }
4866 }
4867 0
4868 }
4869}