1mod binary;
3mod memory;
4mod native;
5pub use native::{ANY, STD};
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::Type;
16pub use rt::JITRunTime;
17use smol_str::SmolStr;
18mod db_module;
19mod gpu_layout;
20mod gpu_module;
21mod http_module;
22mod llm_module;
23mod root_module;
24pub use gpu_layout::{GpuFieldLayout, GpuStructLayout};
25
26use std::sync::{Mutex, OnceLock, Weak};
27static PTR_TYPE: OnceLock<types::Type> = OnceLock::new();
28pub fn ptr_type() -> types::Type {
29 PTR_TYPE.get().cloned().unwrap()
30}
31
32pub fn get_type(ty: &Type) -> Result<types::Type> {
33 if ty.is_f64() {
34 Ok(types::F64)
35 } else if ty.is_f32() {
36 Ok(types::F32)
37 } else if ty.is_int() | ty.is_uint() {
38 match ty.width() {
39 1 => Ok(types::I8),
40 2 => Ok(types::I16),
41 4 => Ok(types::I32),
42 8 => Ok(types::I64),
43 _ => Err(anyhow!("非法类型 {:?}", ty)),
44 }
45 } else if let Type::Bool = ty {
46 Ok(types::I8)
47 } else {
48 Ok(ptr_type())
49 }
50}
51
52use compiler::Symbol;
53use cranelift::prelude::*;
54use cranelift_module::Module;
55
56pub fn init_jit(mut jit: JITRunTime) -> Result<JITRunTime> {
57 jit.add_all()?;
58 Ok(jit)
59}
60
61use std::sync::Arc;
62unsafe impl Send for JITRunTime {}
63unsafe impl Sync for JITRunTime {}
64
65pub(crate) fn with_vm_context<T>(context: *const Weak<Mutex<JITRunTime>>, f: impl FnOnce(&Vm) -> Result<T>) -> Result<T> {
66 if context.is_null() {
67 return Err(anyhow!("VM context is null"));
68 }
69 let jit = unsafe { &*context }.upgrade().ok_or_else(|| anyhow!("VM context has expired"))?;
70 let vm = Vm { jit };
71 f(&vm)
72}
73
74fn add_method_field(jit: &mut JITRunTime, def: &str, method: &str, id: u32) -> Result<()> {
75 let def_id = jit.get_id(def)?;
76 if let Some((_, define)) = jit.compiler.symbols.get_symbol_mut(def_id) {
77 if let Symbol::Struct(Type::Struct { params, fields }, _) = define {
78 fields.push((method.into(), Type::Symbol { id, params: params.clone() }));
79 }
80 }
81 Ok(())
82}
83
84fn add_native_module_fns(jit: &mut JITRunTime, module: &str, fns: &[(&str, &[Type], Type, *const u8)]) -> Result<()> {
85 jit.add_module(module);
86 for (name, arg_tys, ret_ty, fn_ptr) in fns {
87 let full_name = format!("{}::{}", module, name);
88 jit.add_native_ptr(&full_name, name, arg_tys, ret_ty.clone(), *fn_ptr)?;
89 }
90 jit.pop_module();
91 Ok(())
92}
93
94impl JITRunTime {
95 fn add_memory_runtime(&mut self) -> Result<()> {
96 self.native_symbols.write().unwrap().insert("__vm_scope_enter".to_string(), memory::scope_enter as *const () as usize);
97 self.native_symbols.write().unwrap().insert("__vm_scope_exit_void".to_string(), memory::scope_exit_void as *const () as usize);
98 self.native_symbols.write().unwrap().insert("__vm_scope_exit_dynamic".to_string(), memory::scope_exit_dynamic as *const () as usize);
99 self.native_symbols.write().unwrap().insert("__vm_scope_exit_bytes".to_string(), memory::scope_exit_bytes as *const () as usize);
100 self.native_symbols.write().unwrap().insert("__vm_struct_alloc".to_string(), native::struct_alloc as *const () as usize);
101 self.native_symbols.write().unwrap().insert("__vm_struct_from_ptr".to_string(), native::struct_from_ptr as *const () as usize);
102
103 let void_sig = self.get_sig(&[], Type::Void)?;
104 self.scope_enter_fn = Some(self.module.declare_function("__vm_scope_enter", cranelift_module::Linkage::Import, &void_sig)?);
105 self.scope_exit_void_fn = Some(self.module.declare_function("__vm_scope_exit_void", cranelift_module::Linkage::Import, &void_sig)?);
106
107 let dynamic_sig = self.get_sig(&[Type::Any], Type::Any)?;
108 self.scope_exit_dynamic_fn = Some(self.module.declare_function("__vm_scope_exit_dynamic", cranelift_module::Linkage::Import, &dynamic_sig)?);
109
110 let bytes_sig = self.get_sig(&[Type::Any, Type::I64], Type::Any)?;
111 self.scope_exit_bytes_fn = Some(self.module.declare_function("__vm_scope_exit_bytes", cranelift_module::Linkage::Import, &bytes_sig)?);
112
113 let struct_alloc_sig = self.get_sig(&[Type::I64], Type::Any)?;
114 self.struct_alloc_fn = Some(self.module.declare_function("__vm_struct_alloc", cranelift_module::Linkage::Import, &struct_alloc_sig)?);
115
116 let struct_from_ptr_sig = self.get_sig(&[Type::I64, Type::I64], Type::Any)?;
117 self.struct_from_ptr_fn = Some(self.module.declare_function("__vm_struct_from_ptr", cranelift_module::Linkage::Import, &struct_from_ptr_sig)?);
118 Ok(())
119 }
120
121 pub fn add_module(&mut self, name: &str) {
122 self.compiler.symbols.add_module(name.into());
123 }
124
125 pub fn pop_module(&mut self) {
126 self.compiler.symbols.pop_module();
127 }
128
129 pub fn add_type(&mut self, name: &str, ty: Type, is_pub: bool) -> u32 {
130 self.compiler.add_symbol(name, Symbol::Struct(ty, is_pub))
131 }
132
133 pub fn add_empty_type(&mut self, name: &str) -> Result<u32> {
134 match self.get_id(name) {
135 Ok(id) => Ok(id),
136 Err(_) => Ok(self.add_type(name, Type::Struct { params: Vec::new(), fields: Vec::new() }, true)),
137 }
138 }
139
140 pub fn add_native_module_ptr(&mut self, module: &str, name: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
141 self.add_module(module);
142 let full_name = format!("{}::{}", module, name);
143 let result = self.add_native_ptr(&full_name, name, arg_tys, ret_ty, fn_ptr);
144 self.pop_module();
145 result
146 }
147
148 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> {
149 self.add_module(module);
150 let full_name = format!("{}::{}", module, name);
151 let result = self.add_context_native_ptr(&full_name, name, arg_tys, ret_ty, fn_ptr);
152 self.pop_module();
153 result
154 }
155
156 pub fn add_native_method_ptr(&mut self, def: &str, method: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
157 self.add_empty_type(def)?;
158 let full_name = format!("{}::{}", def, method);
159 let id = self.add_native_ptr(&full_name, &full_name, arg_tys, ret_ty, fn_ptr)?;
160 add_method_field(self, def, method, id)?;
161 Ok(id)
162 }
163
164 pub fn add_std(&mut self) -> Result<()> {
165 if self.compiler.symbols.get_id("std::print").is_ok() {
166 return Ok(());
167 }
168 self.add_module("std");
169 for (name, arg_tys, ret_ty, fn_ptr) in STD {
170 self.add_native_ptr(name, name, arg_tys, ret_ty, fn_ptr)?;
171 }
172 self.add_context_native_ptr("import", "import", &[Type::Any, Type::Any], Type::Bool, native::import_with_vm as *const u8)?;
173 Ok(())
174 }
175
176 pub fn add_any(&mut self) -> Result<()> {
177 if self.compiler.symbols.get_id("Any").is_ok() && self.compiler.symbols.get_id("Any::is_map").is_ok() {
178 return Ok(());
179 }
180 for (name, arg_tys, ret_ty, fn_ptr) in ANY {
181 let (_, method) = name.split_once("::").ok_or_else(|| anyhow!("非法 Any 方法名 {}", name))?;
182 self.add_native_method_ptr("Any", method, arg_tys, ret_ty, fn_ptr)?;
183 }
184 Ok(())
185 }
186
187 pub fn add_vec(&mut self) -> Result<()> {
188 self.add_empty_type("Vec")?;
189 let vec_def = Type::Symbol { id: self.get_id("Vec")?, params: Vec::new() };
190 self.add_inline("Vec::swap", vec![vec_def.clone(), Type::I64, Type::I64], Type::Void, |ctx: Option<&mut BuildContext>, args: Vec<Value>| {
191 if let Some(ctx) = ctx {
192 let width = ctx.builder.ins().iconst(types::I64, 4);
193 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);
196 let dest_addr = ctx.builder.ins().iadd(args[0], dest); let dest_val = ctx.builder.ins().load(types::I32, MemFlags::trusted(), dest_addr, 0);
198 let v = ctx.builder.ins().load(types::I32, MemFlags::trusted(), final_addr, 0);
199 ctx.builder.ins().store(MemFlags::trusted(), v, dest_addr, 0);
200 ctx.builder.ins().store(MemFlags::trusted(), dest_val, final_addr, 0);
201 }
202 Err(anyhow!("无返回值"))
203 })?;
204
205 self.add_inline("Vec::get_idx", vec![vec_def.clone(), Type::I64], Type::I32, |ctx: Option<&mut BuildContext>, args: Vec<Value>| {
206 if let Some(ctx) = ctx {
207 let width = ctx.builder.ins().iconst(types::I64, 4);
208 let offset_val = ctx.builder.ins().imul(args[1], width); let final_addr = ctx.builder.ins().iadd(args[0], offset_val);
210 Ok((Some(ctx.builder.ins().load(types::I32, MemFlags::trusted(), final_addr, 0)), Type::I32))
211 } else {
212 Ok((None, Type::I32))
213 }
214 })?;
215 Ok(())
216 }
217
218 pub fn add_llm(&mut self) -> Result<()> {
219 add_native_module_fns(self, "llm", &llm_module::LLM_NATIVE)
220 }
221
222 pub fn add_root(&mut self) -> Result<()> {
223 add_native_module_fns(self, "root", &root_module::ROOT_NATIVE)?;
224 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)?;
225 Ok(())
226 }
227
228 pub fn add_http(&mut self) -> Result<()> {
229 add_native_module_fns(self, "http", &http_module::HTTP_NATIVE)
230 }
231
232 pub fn add_db(&mut self) -> Result<()> {
233 add_native_module_fns(self, "db", &db_module::DB_NATIVE)
234 }
235
236 pub fn add_gpu(&mut self) -> Result<()> {
237 add_native_module_fns(self, "gpu", &gpu_module::GPU_NATIVE)
238 }
239
240 pub fn add_all(&mut self) -> Result<()> {
241 self.add_std()?;
242 self.add_any()?;
243 self.add_vec()?;
244 self.add_llm()?;
245 self.add_root()?;
246 self.add_http()?;
247 self.add_db()?;
248 self.add_gpu()?;
249 Ok(())
250 }
251}
252
253#[derive(Clone)]
254pub struct Vm {
255 jit: Arc<Mutex<JITRunTime>>,
256}
257
258#[derive(Clone)]
259pub struct CompiledFn {
260 ptr: usize,
261 ret: Type,
262 owner: Vm,
263}
264
265impl CompiledFn {
266 pub fn ptr(&self) -> *const u8 {
267 self.ptr as *const u8
268 }
269
270 pub fn ret_ty(&self) -> &Type {
271 &self.ret
272 }
273
274 pub fn owner(&self) -> &Vm {
275 &self.owner
276 }
277}
278
279impl Vm {
280 pub fn new() -> Self {
281 dynamic::set_dynamic_return_handler(memory::take_dynamic_return);
282 let jit = Arc::new(Mutex::new(JITRunTime::new(|_| {})));
283 {
284 let mut guard = jit.lock().unwrap();
285 guard.set_owner(Arc::downgrade(&jit));
286 guard.add_memory_runtime().expect("register VM memory runtime");
287 guard.add_std().expect("register VM std runtime");
288 guard.add_any().expect("register VM Any runtime");
289 }
290 Self { jit }
291 }
292
293 pub fn with_all() -> Result<Self> {
294 let vm = Self::new();
295 vm.add_all()?;
296 Ok(vm)
297 }
298
299 pub fn add_module(&self, name: &str) {
300 self.jit.lock().unwrap().add_module(name)
301 }
302
303 pub fn pop_module(&self) {
304 self.jit.lock().unwrap().pop_module()
305 }
306
307 pub fn add_type(&self, name: &str, ty: Type, is_pub: bool) -> u32 {
308 self.jit.lock().unwrap().add_type(name, ty, is_pub)
309 }
310
311 pub fn add_empty_type(&self, name: &str) -> Result<u32> {
312 self.jit.lock().unwrap().add_empty_type(name)
313 }
314
315 pub fn add_std(&self) -> Result<()> {
316 self.jit.lock().unwrap().add_std()
317 }
318
319 pub fn add_any(&self) -> Result<()> {
320 self.jit.lock().unwrap().add_any()
321 }
322
323 pub fn add_vec(&self) -> Result<()> {
324 self.jit.lock().unwrap().add_vec()
325 }
326
327 pub fn add_llm(&self) -> Result<()> {
328 self.jit.lock().unwrap().add_llm()
329 }
330
331 pub fn add_root(&self) -> Result<()> {
332 self.jit.lock().unwrap().add_root()
333 }
334
335 pub fn add_http(&self) -> Result<()> {
336 self.jit.lock().unwrap().add_http()
337 }
338
339 pub fn add_db(&self) -> Result<()> {
340 self.jit.lock().unwrap().add_db()
341 }
342
343 pub fn add_gpu(&self) -> Result<()> {
344 self.jit.lock().unwrap().add_gpu()
345 }
346
347 pub fn add_all(&self) -> Result<()> {
348 self.jit.lock().unwrap().add_all()
349 }
350
351 pub fn add_native_ptr(&self, full_name: &str, name: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
352 self.jit.lock().unwrap().add_native_ptr(full_name, name, arg_tys, ret_ty, fn_ptr)
353 }
354
355 pub fn add_native_module_ptr(&self, module: &str, name: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
356 self.jit.lock().unwrap().add_native_module_ptr(module, name, arg_tys, ret_ty, fn_ptr)
357 }
358
359 pub fn add_native_method_ptr(&self, def: &str, method: &str, arg_tys: &[Type], ret_ty: Type, fn_ptr: *const u8) -> Result<u32> {
360 self.jit.lock().unwrap().add_native_method_ptr(def, method, arg_tys, ret_ty, fn_ptr)
361 }
362
363 pub fn add_inline(&self, name: &str, args: Vec<Type>, ret: Type, f: fn(Option<&mut BuildContext>, Vec<Value>) -> Result<(Option<Value>, Type)>) -> Result<u32> {
364 self.jit.lock().unwrap().add_inline(name, args, ret, f)
365 }
366
367 pub fn import_code(&self, name: &str, code: Vec<u8>) -> Result<()> {
368 self.jit.lock().unwrap().import_code(name, code)
369 }
370
371 pub fn import_file(&self, name: &str, path: &str) -> Result<()> {
372 self.jit.lock().unwrap().compiler.import_file(name, path)?;
373 Ok(())
374 }
375
376 pub fn import(&self, name: &str, path: &str) -> Result<()> {
377 if root::contains(path) {
378 let code = root::get(path).unwrap();
379 if code.is_str() {
380 self.import_code(name, code.as_str().as_bytes().to_vec())
381 } else {
382 self.import_code(name, code.get_dynamic("code").ok_or(anyhow!("{:?} 没有 code 成员", code))?.as_str().as_bytes().to_vec())
383 }
384 } else {
385 self.import_file(name, path)
386 }
387 }
388
389 pub fn infer(&self, name: &str, arg_tys: &[Type]) -> Result<Type> {
390 self.jit.lock().unwrap().get_type(name, arg_tys)
391 }
392
393 pub fn get_fn_ptr(&self, name: &str, arg_tys: &[Type]) -> Result<(*const u8, Type)> {
394 self.jit.lock().unwrap().get_fn_ptr(name, arg_tys)
395 }
396
397 pub fn get_fn(&self, name: &str, arg_tys: &[Type]) -> Result<CompiledFn> {
398 let (ptr, ret) = self.get_fn_ptr(name, arg_tys)?;
399 Ok(CompiledFn { ptr: ptr as usize, ret, owner: self.clone() })
400 }
401
402 pub fn load(&self, code: Vec<u8>, arg_name: SmolStr) -> Result<(i64, Type)> {
403 self.jit.lock().unwrap().load(code, arg_name)
404 }
405
406 pub fn get_symbol(&self, name: &str, params: Vec<Type>) -> Result<Type> {
407 Ok(Type::Symbol { id: self.jit.lock().unwrap().get_id(name)?, params })
408 }
409
410 pub fn gpu_struct_layout(&self, name: &str, params: &[Type]) -> Result<GpuStructLayout> {
411 let jit = self.jit.lock().unwrap();
412 GpuStructLayout::from_symbol_table(&jit.compiler.symbols, name, params)
413 }
414
415 pub fn disassemble(&self, name: &str) -> Result<String> {
416 self.jit.lock().unwrap().compiler.symbols.disassemble(name)
417 }
418
419 #[cfg(feature = "ir-disassembly")]
420 pub fn disassemble_ir(&self, name: &str) -> Result<String> {
421 self.jit.lock().unwrap().disassemble_ir(name)
422 }
423}
424
425impl Default for Vm {
426 fn default() -> Self {
427 Self::new()
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::Vm;
434 use dynamic::{Dynamic, ToJson, Type};
435
436 extern "C" fn math_double(value: i64) -> i64 {
437 value * 2
438 }
439
440 #[test]
441 fn vm_can_add_native_after_jit_creation() -> anyhow::Result<()> {
442 let vm = Vm::new();
443 vm.add_native_module_ptr("math", "double", &[Type::I64], Type::I64, math_double as *const u8)?;
444 vm.import_code(
445 "vm_dynamic_native",
446 br#"
447 pub fn run(value: i64) {
448 math::double(value)
449 }
450 "#
451 .to_vec(),
452 )?;
453
454 let compiled = vm.get_fn("vm_dynamic_native::run", &[Type::I64])?;
455 assert_eq!(compiled.ret_ty(), &Type::I64);
456 let run: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
457 assert_eq!(run(21), 42);
458 Ok(())
459 }
460
461 #[test]
462 fn vm_new_registers_std_and_any() -> anyhow::Result<()> {
463 let vm = Vm::new();
464 vm.add_std()?;
465 vm.add_any()?;
466 assert_eq!(vm.infer("std::print", &[Type::Any])?, Type::Void);
467
468 vm.import_code(
469 "vm_new_default_any",
470 br#"
471 pub fn has_items(content) {
472 if content.is_map() {
473 if content.contains("items") {
474 return content.items.len() > 0;
475 }
476 }
477 false
478 }
479 "#
480 .to_vec(),
481 )?;
482
483 assert_eq!(vm.infer("vm_new_default_any::has_items", &[Type::Any])?, Type::Bool);
484 let compiled = vm.get_fn("vm_new_default_any::has_items", &[Type::Any])?;
485 assert_eq!(compiled.ret_ty(), &Type::Bool);
486 Ok(())
487 }
488
489 #[test]
490 fn nested_struct_arg_return_struct_field_is_static_field_access() -> anyhow::Result<()> {
491 let vm = Vm::with_all()?;
492 vm.import_code(
493 "vm_nested_struct_return_field",
494 br#"
495 pub struct Inner {
496 value: i64,
497 }
498
499 pub struct RoleMini {
500 inner: Inner,
501 hp: i64,
502 }
503
504 pub struct TeamMini {
505 role: RoleMini,
506 }
507
508 pub struct BigSummary {
509 winner: i64,
510 loser: i64,
511 }
512
513 pub fn make_big_with_team(team: TeamMini) {
514 let score = team.role.inner.value;
515 BigSummary{winner: score, loser: 0}
516 }
517
518 pub fn read_team_winner_direct() {
519 let team = TeamMini{role: RoleMini{inner: Inner{value: 9}, hp: 1}};
520 make_big_with_team(team).winner
521 }
522
523 pub fn read_team_winner_bound() {
524 let team = TeamMini{role: RoleMini{inner: Inner{value: 9}, hp: 1}};
525 let summary = make_big_with_team(team);
526 summary.winner
527 }
528 "#
529 .to_vec(),
530 )?;
531
532 let compiled = vm.get_fn("vm_nested_struct_return_field::read_team_winner_direct", &[])?;
533 assert_eq!(compiled.ret_ty(), &Type::I64);
534 let direct: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
535 assert_eq!(direct(), 9);
536
537 let compiled = vm.get_fn("vm_nested_struct_return_field::read_team_winner_bound", &[])?;
538 assert_eq!(compiled.ret_ty(), &Type::I64);
539 let bound: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
540 assert_eq!(bound(), 9);
541 Ok(())
542 }
543
544 #[test]
545 fn any_push_does_not_consume_reused_value() -> anyhow::Result<()> {
546 let vm = Vm::with_all()?;
547 vm.import_code(
548 "vm_any_push_reused_value",
549 br#"
550 pub fn run() {
551 let role_id = "acct_role_2";
552 let updated = [];
553 updated.push(role_id);
554 {
555 ok: true,
556 user_id: role_id,
557 first: updated.get_idx(0)
558 }
559 }
560 "#
561 .to_vec(),
562 )?;
563
564 let compiled = vm.get_fn("vm_any_push_reused_value::run", &[])?;
565 assert_eq!(compiled.ret_ty(), &Type::Any);
566 let run: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
567 let result = unsafe { &*run() };
568 assert_eq!(result.get_dynamic("ok").and_then(|value| value.as_bool()), Some(true));
569 assert_eq!(result.get_dynamic("user_id").map(|value| value.as_str().to_string()), Some("acct_role_2".to_string()));
570 assert_eq!(result.get_dynamic("first").map(|value| value.as_str().to_string()), Some("acct_role_2".to_string()));
571 Ok(())
572 }
573
574 #[test]
575 fn compares_any_with_string_literal_as_string() -> anyhow::Result<()> {
576 let vm = Vm::with_all()?;
577 vm.import_code(
578 "vm_string_compare_any",
579 br#"
580 pub fn any_ne_empty(chat_path) {
581 chat_path != ""
582 }
583 "#
584 .to_vec(),
585 )?;
586
587 let compiled = vm.get_fn("vm_string_compare_any::any_ne_empty", &[Type::Any])?;
588 assert_eq!(compiled.ret_ty(), &Type::Bool);
589
590 let any_ne_empty: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
591 let empty = Dynamic::from("");
592 let non_empty = Dynamic::from("chat");
593
594 assert!(!any_ne_empty(&empty));
595 assert!(any_ne_empty(&non_empty));
596 Ok(())
597 }
598
599 #[test]
600 fn compares_bool_values_and_bool_literals() -> anyhow::Result<()> {
601 let vm = Vm::with_all()?;
602 vm.import_code(
603 "vm_bool_compare",
604 br#"
605 pub fn eq_true(value: bool) {
606 value == true
607 }
608
609 pub fn ne_false(value: bool) {
610 value != false
611 }
612
613 pub fn literal_left(value: bool) {
614 true == value
615 }
616
617 pub fn eq_pair(left: bool, right: bool) {
618 left == right
619 }
620
621 pub fn logic_pair(left: bool, right: bool) {
622 (left && right) || (left == true && right != false)
623 }
624 "#
625 .to_vec(),
626 )?;
627
628 let compiled = vm.get_fn("vm_bool_compare::eq_true", &[Type::Bool])?;
629 assert_eq!(compiled.ret_ty(), &Type::Bool);
630 let eq_true: extern "C" fn(bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
631 assert!(eq_true(true));
632 assert!(!eq_true(false));
633
634 let compiled = vm.get_fn("vm_bool_compare::ne_false", &[Type::Bool])?;
635 assert_eq!(compiled.ret_ty(), &Type::Bool);
636 let ne_false: extern "C" fn(bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
637 assert!(ne_false(true));
638 assert!(!ne_false(false));
639
640 let compiled = vm.get_fn("vm_bool_compare::literal_left", &[Type::Bool])?;
641 assert_eq!(compiled.ret_ty(), &Type::Bool);
642 let literal_left: extern "C" fn(bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
643 assert!(literal_left(true));
644 assert!(!literal_left(false));
645
646 let compiled = vm.get_fn("vm_bool_compare::eq_pair", &[Type::Bool, Type::Bool])?;
647 assert_eq!(compiled.ret_ty(), &Type::Bool);
648 let eq_pair: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
649 assert!(eq_pair(true, true));
650 assert!(eq_pair(false, false));
651 assert!(!eq_pair(true, false));
652 assert!(!eq_pair(false, true));
653
654 let compiled = vm.get_fn("vm_bool_compare::logic_pair", &[Type::Bool, Type::Bool])?;
655 assert_eq!(compiled.ret_ty(), &Type::Bool);
656 let logic_pair: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
657 assert!(logic_pair(true, true));
658 assert!(!logic_pair(true, false));
659 assert!(!logic_pair(false, true));
660 assert!(!logic_pair(false, false));
661 Ok(())
662 }
663
664 #[test]
665 fn parenthesized_expression_can_call_any_method() -> anyhow::Result<()> {
666 let vm = Vm::with_all()?;
667 vm.import_code(
668 "vm_parenthesized_method_call",
669 br#"
670 pub fn run(value) {
671 (value + 2).to_i64()
672 }
673 "#
674 .to_vec(),
675 )?;
676
677 let compiled = vm.get_fn("vm_parenthesized_method_call::run", &[Type::Any])?;
678 assert_eq!(compiled.ret_ty(), &Type::I64);
679 let run: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
680 let value = Dynamic::from(40i64);
681
682 assert_eq!(run(&value), 42);
683 Ok(())
684 }
685
686 #[test]
687 fn casts_any_float_to_i32_without_zeroing() -> anyhow::Result<()> {
688 let vm = Vm::with_all()?;
689 vm.import_code(
690 "vm_any_float_to_i32",
691 br#"
692 pub fn direct(value) {
693 value as i32
694 }
695
696 pub fn map_field(value) {
697 let field = value.v;
698 field as i32
699 }
700
701 pub fn damage(attacker, def_rate) {
702 let x = attacker.atk * (1.0 - def_rate);
703 x as i32
704 }
705 "#
706 .to_vec(),
707 )?;
708
709 let compiled = vm.get_fn("vm_any_float_to_i32::direct", &[Type::Any])?;
710 assert_eq!(compiled.ret_ty(), &Type::I32);
711 let direct: extern "C" fn(*const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
712 let value = Dynamic::from(9.5f64);
713 assert_eq!(direct(&value), 9);
714
715 let compiled = vm.get_fn("vm_any_float_to_i32::map_field", &[Type::Any])?;
716 assert_eq!(compiled.ret_ty(), &Type::I32);
717 let map_field: extern "C" fn(*const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
718 let value = dynamic::map!("v"=> 9.5f64);
719 assert_eq!(map_field(&value), 9);
720
721 let compiled = vm.get_fn("vm_any_float_to_i32::damage", &[Type::Any, Type::Any])?;
722 assert_eq!(compiled.ret_ty(), &Type::I32);
723 let damage: extern "C" fn(*const Dynamic, *const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
724 let attacker = dynamic::map!("atk"=> 64i64);
725 let def_rate = Dynamic::from(0.17f64);
726 assert_eq!(damage(&attacker, &def_rate), 53);
727 Ok(())
728 }
729
730 #[test]
731 fn binary_imm_promotes_integer_literals_for_float_left_values() -> anyhow::Result<()> {
732 let vm = Vm::with_all()?;
733 vm.import_code(
734 "vm_float_binary_imm",
735 br#"
736 pub fn add_f32(value: f32) {
737 value + 1i32
738 }
739
740 pub fn sub_f32(value: f32) {
741 value - 1i32
742 }
743
744 pub fn mul_f32(value: f32) {
745 value * 2i32
746 }
747
748 pub fn div_f32(value: f32) {
749 value / 2i32
750 }
751
752 pub fn gt_f32(value: f32) {
753 value > 2i32
754 }
755 "#
756 .to_vec(),
757 )?;
758
759 let compiled = vm.get_fn("vm_float_binary_imm::add_f32", &[Type::F32])?;
760 assert_eq!(compiled.ret_ty(), &Type::F32);
761 let add_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
762 assert_eq!(add_f32(2.5), 3.5);
763
764 let compiled = vm.get_fn("vm_float_binary_imm::sub_f32", &[Type::F32])?;
765 assert_eq!(compiled.ret_ty(), &Type::F32);
766 let sub_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
767 assert_eq!(sub_f32(2.5), 1.5);
768
769 let compiled = vm.get_fn("vm_float_binary_imm::mul_f32", &[Type::F32])?;
770 assert_eq!(compiled.ret_ty(), &Type::F32);
771 let mul_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
772 assert_eq!(mul_f32(2.5), 5.0);
773
774 let compiled = vm.get_fn("vm_float_binary_imm::div_f32", &[Type::F32])?;
775 assert_eq!(compiled.ret_ty(), &Type::F32);
776 let div_f32: extern "C" fn(f32) -> f32 = unsafe { std::mem::transmute(compiled.ptr()) };
777 assert_eq!(div_f32(5.0), 2.5);
778
779 let compiled = vm.get_fn("vm_float_binary_imm::gt_f32", &[Type::F32])?;
780 assert_eq!(compiled.ret_ty(), &Type::Bool);
781 let gt_f32: extern "C" fn(f32) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
782 assert!(gt_f32(2.5));
783 assert!(!gt_f32(1.5));
784 Ok(())
785 }
786
787 #[test]
788 fn any_keys_returns_map_keys_and_empty_list_for_other_values() -> anyhow::Result<()> {
789 let vm = Vm::with_all()?;
790 vm.import_code(
791 "vm_any_keys",
792 br#"
793 pub fn map_keys(value) {
794 let keys = value.keys();
795 keys.len() == 2 && keys.contains("alpha") && keys.contains("beta")
796 }
797
798 pub fn non_map_keys(value) {
799 value.keys().len() == 0
800 }
801 "#
802 .to_vec(),
803 )?;
804
805 let compiled = vm.get_fn("vm_any_keys::map_keys", &[Type::Any])?;
806 assert_eq!(compiled.ret_ty(), &Type::Bool);
807 let map_keys: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
808 let value = dynamic::map!("alpha"=> 1i64, "beta"=> 2i64);
809 assert!(map_keys(&value));
810
811 let compiled = vm.get_fn("vm_any_keys::non_map_keys", &[Type::Any])?;
812 assert_eq!(compiled.ret_ty(), &Type::Bool);
813 let non_map_keys: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
814 let value = Dynamic::from("alpha");
815 assert!(non_map_keys(&value));
816 Ok(())
817 }
818
819 #[test]
820 fn string_methods_work_on_static_string_and_any_string_values() -> anyhow::Result<()> {
821 let vm = Vm::with_all()?;
822 vm.import_code(
823 "vm_string_methods",
824 br#"
825 pub fn static_string_methods(text: string) {
826 let parts = text.split(",");
827 text.starts_with("alpha")
828 && text.is_string()
829 && !text.is_null()
830 && parts.len() == 2
831 && parts.get_idx(0) == "alpha"
832 && parts.get_idx(1) == "beta"
833 }
834
835 pub fn any_string_methods(value) {
836 let parts = value.split(",");
837 value.starts_with("alpha")
838 && value.is_string()
839 && !value.is_null()
840 && parts.len() == 2
841 && parts.get_idx(0) == "alpha"
842 && parts.get_idx(1) == "beta"
843 }
844
845 pub fn any_null_methods(value) {
846 value.is_null() && !value.is_string()
847 }
848 "#
849 .to_vec(),
850 )?;
851
852 let compiled = vm.get_fn("vm_string_methods::static_string_methods", &[Type::Str])?;
853 assert_eq!(compiled.ret_ty(), &Type::Bool);
854 let static_string_methods: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
855 let text = Dynamic::from("alpha,beta");
856 assert!(static_string_methods(&text));
857
858 let compiled = vm.get_fn("vm_string_methods::any_string_methods", &[Type::Any])?;
859 assert_eq!(compiled.ret_ty(), &Type::Bool);
860 let any_string_methods: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
861 assert!(any_string_methods(&text));
862
863 let compiled = vm.get_fn("vm_string_methods::any_null_methods", &[Type::Any])?;
864 assert_eq!(compiled.ret_ty(), &Type::Bool);
865 let any_null_methods: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
866 let value = Dynamic::Null;
867 assert!(any_null_methods(&value));
868 Ok(())
869 }
870
871 #[test]
872 fn primitive_type_check_methods_call_any_runtime() -> anyhow::Result<()> {
873 let vm = Vm::with_all()?;
874 vm.import_code(
875 "vm_primitive_type_check_methods",
876 br#"
877 pub fn int_checks() {
878 !42i64.is_list()
879 && !42i64.is_map()
880 && !42i64.is_string()
881 && !42i64.is_null()
882 }
883
884 pub fn bool_checks() {
885 !true.is_list() && !true.is_map() && !true.is_null()
886 }
887 "#
888 .to_vec(),
889 )?;
890
891 let compiled = vm.get_fn("vm_primitive_type_check_methods::int_checks", &[])?;
892 assert_eq!(compiled.ret_ty(), &Type::Bool);
893 let int_checks: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
894 assert!(int_checks());
895
896 let compiled = vm.get_fn("vm_primitive_type_check_methods::bool_checks", &[])?;
897 assert_eq!(compiled.ret_ty(), &Type::Bool);
898 let bool_checks: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
899 assert!(bool_checks());
900 Ok(())
901 }
902
903 #[test]
904 fn for_loop_iterates_any_list_and_map_values() -> anyhow::Result<()> {
905 let vm = Vm::with_all()?;
906 vm.import_code(
907 "vm_for_any_collections",
908 br#"
909 pub fn list_sum(items) {
910 let total = 0i64;
911 for item in items {
912 total += item;
913 }
914 total
915 }
916
917 pub fn map_sum(data) {
918 let total = 0i64;
919 for (key, value) in data {
920 total += value;
921 }
922 total
923 }
924 "#
925 .to_vec(),
926 )?;
927
928 let compiled = vm.get_fn("vm_for_any_collections::list_sum", &[Type::Any])?;
929 assert_eq!(compiled.ret_ty(), &Type::Any);
930 let list_sum: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
931 let items = Dynamic::list(vec![1i64.into(), 2i64.into(), 3i64.into()]);
932 let result = unsafe { &*list_sum(&items) };
933 assert_eq!(result.as_int(), Some(6));
934
935 let compiled = vm.get_fn("vm_for_any_collections::map_sum", &[Type::Any])?;
936 assert_eq!(compiled.ret_ty(), &Type::Any);
937 let map_sum: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
938 let data = dynamic::map!("a"=> 4i64, "b"=> 5i64);
939 let result = unsafe { &*map_sum(&data) };
940 assert_eq!(result.as_int(), Some(9));
941 Ok(())
942 }
943
944 #[test]
945 fn compares_concrete_value_with_string_literal_as_string() -> anyhow::Result<()> {
946 let vm = Vm::with_all()?;
947 vm.import_code(
948 "vm_string_compare_imm",
949 br#"
950 pub fn int_eq_str(value: i64) {
951 value == "42"
952 }
953
954 pub fn int_to_str(value: i64) {
955 value + ""
956 }
957 "#
958 .to_vec(),
959 )?;
960
961 let compiled = vm.get_fn("vm_string_compare_imm::int_eq_str", &[Type::I64])?;
962 assert_eq!(compiled.ret_ty(), &Type::Bool);
963
964 let int_eq_str: extern "C" fn(i64) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
965
966 let compiled = vm.get_fn("vm_string_compare_imm::int_to_str", &[Type::I64])?;
967 assert_eq!(compiled.ret_ty(), &Type::Any);
968 let int_to_str: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
969 let text = int_to_str(42);
970 assert_eq!(unsafe { &*text }.as_str(), "42");
971
972 assert!(int_eq_str(42));
973 assert!(!int_eq_str(7));
974 Ok(())
975 }
976
977 #[test]
978 fn concatenates_string_with_integer_values() -> anyhow::Result<()> {
979 let vm = Vm::with_all()?;
980 vm.import_code(
981 "vm_string_concat_integer",
982 br#"
983 pub fn idx_key(idx: i64) {
984 "" + idx
985 }
986
987 pub fn level_text(level: i64) {
988 "" + level + " level"
989 }
990
991 pub fn gold_text(currency) {
992 "" + currency.gold
993 }
994 "#
995 .to_vec(),
996 )?;
997
998 let compiled = vm.get_fn("vm_string_concat_integer::idx_key", &[Type::I64])?;
999 let idx_key: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1000 let result = unsafe { &*idx_key(7) };
1001 assert_eq!(result.as_str(), "7");
1002
1003 let compiled = vm.get_fn("vm_string_concat_integer::level_text", &[Type::I64])?;
1004 let level_text: extern "C" fn(i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1005 let result = unsafe { &*level_text(12) };
1006 assert_eq!(result.as_str(), "12 level");
1007
1008 let compiled = vm.get_fn("vm_string_concat_integer::gold_text", &[Type::Any])?;
1009 let gold_text: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1010 let currency = dynamic::map!("gold"=> 345i64);
1011 let result = unsafe { &*gold_text(¤cy) };
1012 assert_eq!(result.as_str(), "345");
1013 Ok(())
1014 }
1015
1016 #[test]
1017 fn coerces_string_concat_to_i64_without_unimplemented_log() -> anyhow::Result<()> {
1018 let vm = Vm::with_all()?;
1019 vm.import_code(
1020 "vm_string_concat_to_i64",
1021 br#"
1022 pub fn run(idx: i64) {
1023 ("" + idx) as i64
1024 }
1025 "#
1026 .to_vec(),
1027 )?;
1028
1029 let compiled = vm.get_fn("vm_string_concat_to_i64::run", &[Type::I64])?;
1030 assert_eq!(compiled.ret_ty(), &Type::I64);
1031 let run: extern "C" fn(i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1032 assert_eq!(run(7), 0);
1033 Ok(())
1034 }
1035
1036 #[test]
1037 fn unifies_explicit_return_and_tail_integer_widths() -> anyhow::Result<()> {
1038 let vm = Vm::with_all()?;
1039 vm.import_code(
1040 "vm_return_integer_widths",
1041 br#"
1042 pub fn selected(flag, slot) {
1043 if flag {
1044 return slot;
1045 }
1046 0
1047 }
1048 "#
1049 .to_vec(),
1050 )?;
1051
1052 let compiled = vm.get_fn("vm_return_integer_widths::selected", &[Type::Bool, Type::I64])?;
1053 assert_eq!(compiled.ret_ty(), &Type::I64);
1054 let selected: extern "C" fn(bool, i64) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
1055
1056 assert_eq!(selected(true, 7), 7);
1057 assert_eq!(selected(false, 7), 0);
1058 Ok(())
1059 }
1060
1061 #[test]
1062 fn root_contains_string_concat_is_bool_condition() -> anyhow::Result<()> {
1063 let vm = Vm::with_all()?;
1064 vm.import_code(
1065 "vm_root_contains_condition",
1066 br#"
1067 pub fn exists(user_id) {
1068 if root::contains("redis/user/" + user_id) {
1069 return 1;
1070 }
1071 0
1072 }
1073 "#
1074 .to_vec(),
1075 )?;
1076
1077 assert_eq!(vm.infer("root::contains", &[Type::Any])?, Type::Bool);
1078 let compiled = vm.get_fn("vm_root_contains_condition::exists", &[Type::Any])?;
1079 assert_eq!(compiled.ret_ty(), &Type::I32);
1080 Ok(())
1081 }
1082
1083 #[test]
1084 fn root_add_map_can_be_printed() -> anyhow::Result<()> {
1085 let vm = Vm::with_all()?;
1086 assert_eq!(vm.infer("root::add_map", &[Type::Any])?, Type::Bool);
1087 vm.import_code(
1088 "vm_root_add_map_print",
1089 br#"
1090 pub fn run() {
1091 print(root::add_map("local/world_handlers/til_map_novicevillage"));
1092 }
1093 "#
1094 .to_vec(),
1095 )?;
1096
1097 let compiled = vm.get_fn("vm_root_add_map_print::run", &[])?;
1098 assert!(compiled.ret_ty().is_void());
1099 Ok(())
1100 }
1101
1102 #[test]
1103 fn std_log_accepts_any_and_returns_void() -> anyhow::Result<()> {
1104 let vm = Vm::with_all()?;
1105 vm.import_code(
1106 "vm_std_log",
1107 br#"
1108 pub fn run(value) {
1109 log({ ok: true, value: value });
1110 }
1111 "#
1112 .to_vec(),
1113 )?;
1114
1115 let compiled = vm.get_fn("vm_std_log::run", &[Type::Any])?;
1116 assert!(compiled.ret_ty().is_void());
1117 let run: extern "C" fn(*const Dynamic) = unsafe { std::mem::transmute(compiled.ptr()) };
1118 let value = Dynamic::from(7i64);
1119 run(&value);
1120 Ok(())
1121 }
1122
1123 #[test]
1124 fn unary_not_any_loop_var_is_bool_condition() -> anyhow::Result<()> {
1125 let vm = Vm::with_all()?;
1126 vm.import_code(
1127 "vm_unary_not_any_loop_var",
1128 br#"
1129 pub fn count_missing(flags) {
1130 let missing = 0;
1131 for exists in flags {
1132 if !exists {
1133 missing = missing + 1;
1134 }
1135 }
1136 missing
1137 }
1138 "#
1139 .to_vec(),
1140 )?;
1141
1142 let compiled = vm.get_fn("vm_unary_not_any_loop_var::count_missing", &[Type::Any])?;
1143 assert_eq!(compiled.ret_ty(), &Type::I32);
1144 Ok(())
1145 }
1146
1147 #[test]
1148 fn closure_literal_can_be_called_immediately() -> anyhow::Result<()> {
1149 let vm = Vm::with_all()?;
1150 vm.import_code(
1151 "vm_closure_immediate_call",
1152 br#"
1153 pub fn no_args() {
1154 let r = || { 1i32 }();
1155 r
1156 }
1157
1158 pub fn with_arg() {
1159 |value: i32| { value + 1i32 }(2i32)
1160 }
1161 "#
1162 .to_vec(),
1163 )?;
1164
1165 let compiled = vm.get_fn("vm_closure_immediate_call::no_args", &[])?;
1166 assert_eq!(compiled.ret_ty(), &Type::I32);
1167 let no_args: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
1168 assert_eq!(no_args(), 1);
1169
1170 let compiled = vm.get_fn("vm_closure_immediate_call::with_arg", &[])?;
1171 assert_eq!(compiled.ret_ty(), &Type::I32);
1172 let with_arg: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
1173 assert_eq!(with_arg(), 3);
1174 Ok(())
1175 }
1176
1177 #[test]
1178 fn semicolon_tail_call_makes_function_void() -> anyhow::Result<()> {
1179 let vm = Vm::with_all()?;
1180 vm.import_code(
1181 "vm_semicolon_tail_void",
1182 br#"
1183 pub fn send_role_select(idx, account_id, selected_slot) {
1184 root::send("local/ui/send_dialog", {
1185 idx: idx,
1186 account_id: account_id,
1187 selected_slot: selected_slot
1188 });
1189 }
1190 "#
1191 .to_vec(),
1192 )?;
1193
1194 let compiled = vm.get_fn("vm_semicolon_tail_void::send_role_select", &[Type::Any, Type::Any, Type::Any])?;
1195 assert_eq!(compiled.ret_ty(), &Type::Void);
1196 Ok(())
1197 }
1198
1199 #[test]
1200 fn bare_return_conflicts_with_non_void_return() -> anyhow::Result<()> {
1201 let vm = Vm::with_all()?;
1202 vm.import_code(
1203 "vm_bare_return_conflict",
1204 br#"
1205 pub fn run(flag) {
1206 if flag {
1207 return;
1208 }
1209 1
1210 }
1211 "#
1212 .to_vec(),
1213 )?;
1214
1215 let err = match vm.get_fn("vm_bare_return_conflict::run", &[Type::Bool]) {
1216 Ok(_) => panic!("expected mismatched return types to fail"),
1217 Err(err) => err,
1218 };
1219 assert!(format!("{err:#}").contains("返回类型不一致"));
1220 Ok(())
1221 }
1222
1223 #[test]
1224 fn root_get_accepts_string_concat_with_dynamic_field() -> anyhow::Result<()> {
1225 let vm = Vm::with_all()?;
1226 vm.import_code(
1227 "vm_root_get_dynamic_concat",
1228 br#"
1229 pub fn get_action(req) {
1230 root::get("local/game/panel_actions/" + req.idx)
1231 }
1232 "#
1233 .to_vec(),
1234 )?;
1235
1236 root::add("local/game/panel_actions/7", dynamic::map!("id"=> "action-7").into())?;
1237 let compiled = vm.get_fn("vm_root_get_dynamic_concat::get_action", &[Type::Any])?;
1238 assert_eq!(compiled.ret_ty(), &Type::Any);
1239 let get_action: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1240 let req = dynamic::map!("idx"=> 7i64);
1241 let result = unsafe { &*get_action(&req) };
1242
1243 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("action-7".to_string()));
1244 Ok(())
1245 }
1246
1247 #[test]
1248 fn root_add_fn_registers_handler_with_dynamic_field_path_concat() -> anyhow::Result<()> {
1249 let vm = Vm::with_all()?;
1250 vm.import_code(
1251 "vm_registered_panel_action",
1252 br#"
1253 pub fn panel_action(req) {
1254 root::get("local/game/panel_actions/" + req.idx)
1255 }
1256
1257 pub fn register() {
1258 root::add_fn("local/ui/panel_action", "vm_registered_panel_action::panel_action")
1259 }
1260 "#
1261 .to_vec(),
1262 )?;
1263
1264 let compiled = vm.get_fn("vm_registered_panel_action::register", &[])?;
1265 assert_eq!(compiled.ret_ty(), &Type::Bool);
1266 let register: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
1267 assert!(register());
1268 Ok(())
1269 }
1270
1271 #[test]
1272 fn root_add_fn_accepts_string_concat_in_registered_handler() -> anyhow::Result<()> {
1273 let vm = Vm::with_all()?;
1274 vm.import_code(
1275 "vm_registered_string_concat",
1276 br#"
1277 pub fn send_panel(idx: i64) {
1278 let idx_key = "" + idx;
1279 idx_key
1280 }
1281 "#
1282 .to_vec(),
1283 )?;
1284
1285 assert!(vm.get_fn_ptr("vm_registered_string_concat::send_panel", &[Type::Any]).is_ok());
1286 Ok(())
1287 }
1288
1289 #[test]
1290 fn root_send_idx_returns_handler_value() -> anyhow::Result<()> {
1291 fn echo_handler(msg: Dynamic) -> Dynamic {
1292 dynamic::map!("type"=> "echo", "id"=> msg.get_dynamic("id").unwrap_or(Dynamic::Null))
1293 }
1294
1295 let vm = Vm::with_all()?;
1296 vm.import_code(
1297 "vm_root_send_idx_return",
1298 br#"
1299 pub fn call(req) {
1300 root::send_idx("local/send_idx_return_handlers", 0, req)
1301 }
1302 "#
1303 .to_vec(),
1304 )?;
1305
1306 root::add_list("local/send_idx_return_handlers")?;
1307 let (mount, name) = root::get_mount("local/send_idx_return_handlers")?;
1308 mount.push(name, root::Object::Native(echo_handler))?;
1309
1310 assert_eq!(vm.infer("root::send_idx", &[Type::Any, Type::I64, Type::Any])?, Type::Any);
1311 let compiled = vm.get_fn("vm_root_send_idx_return::call", &[Type::Any])?;
1312 assert_eq!(compiled.ret_ty(), &Type::Any);
1313 let call: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1314 let req = dynamic::map!("id"=> 42i64);
1315 let result = unsafe { &*call(&req) };
1316
1317 assert_eq!(result.get_dynamic("type").map(|value| value.as_str().to_string()), Some("echo".to_string()));
1318 assert_eq!(result.get_dynamic("id").and_then(|value| value.as_int()), Some(42));
1319 Ok(())
1320 }
1321
1322 #[test]
1323 fn compiles_public_hotspots_with_string_paths_and_keys() -> anyhow::Result<()> {
1324 let vm = Vm::with_all()?;
1325 vm.import_code(
1326 "vm_public_hotspots",
1327 br#"
1328 pub fn public_hotspot(action_map_path, panel_id, action_id, hotspot) {
1329 {
1330 path: action_map_path,
1331 panel_id: panel_id,
1332 action_id: action_id,
1333 id: hotspot.id
1334 }
1335 }
1336
1337 pub fn public_hotspots(idx, panel_id, hotspots) {
1338 let idx_key = "" + idx;
1339 let action_map_path = "local/game/panel_actions/" + idx_key;
1340
1341 let existing_action_map = root::get(action_map_path);
1342 if !existing_action_map.is_map() {
1343 root::add_map(action_map_path);
1344 }
1345
1346 if hotspots.is_map() {
1347 let public_items = {};
1348 for action_id in hotspots.keys() {
1349 public_items[action_id] = public_hotspot(action_map_path, panel_id, action_id, hotspots[action_id]);
1350 }
1351 return public_items;
1352 }
1353
1354 let public_items = [];
1355 let i = 0;
1356 while i < hotspots.len() {
1357 let hotspot = hotspots.get_idx(i);
1358 let item = public_hotspot(action_map_path, panel_id, hotspot.id, hotspot);
1359 public_items.push(item);
1360 i = i + 1;
1361 }
1362
1363 public_items
1364 }
1365 "#
1366 .to_vec(),
1367 )?;
1368
1369 assert!(vm.get_fn("vm_public_hotspots::public_hotspots", &[Type::I64, Type::Any, Type::Any]).is_ok());
1370 assert!(vm.get_fn("vm_public_hotspots::public_hotspots", &[Type::Any, Type::Any, Type::Any]).is_ok());
1371 Ok(())
1372 }
1373
1374 #[test]
1375 fn send_panel_calls_public_hotspots_with_dynamic_request() -> anyhow::Result<()> {
1376 let vm = Vm::with_all()?;
1377 vm.import_code(
1378 "vm_send_panel_public_hotspots",
1379 br#"
1380 pub fn ok(value) {
1381 value
1382 }
1383
1384 pub fn panel_from_node(req) {
1385 {
1386 panel_id: req.panel_id,
1387 hotspots: req.hotspots
1388 }
1389 }
1390
1391 pub fn public_hotspot(action_map_path, panel_id, action_id, hotspot) {
1392 {
1393 path: action_map_path,
1394 panel_id: panel_id,
1395 action_id: action_id,
1396 id: hotspot.id
1397 }
1398 }
1399
1400 pub fn public_hotspots(idx, panel_id, hotspots) {
1401 let idx_key = "" + idx;
1402 let action_map_path = "local/game/panel_actions/" + idx_key;
1403
1404 let existing_action_map = root::get(action_map_path);
1405 if !existing_action_map.is_map() {
1406 root::add_map(action_map_path);
1407 }
1408
1409 if hotspots.is_map() {
1410 let public_items = {};
1411 for action_id in hotspots.keys() {
1412 public_items[action_id] = public_hotspot(action_map_path, panel_id, action_id, hotspots[action_id]);
1413 }
1414 return public_items;
1415 }
1416
1417 let public_items = [];
1418 let i = 0;
1419 while i < hotspots.len() {
1420 let hotspot = hotspots.get_idx(i);
1421 let item = public_hotspot(action_map_path, panel_id, hotspot.id, hotspot);
1422 public_items.push(item);
1423 i = i + 1;
1424 }
1425
1426 public_items
1427 }
1428
1429 pub fn send_panel(req) {
1430 let panel = req.panel;
1431 if !panel.is_map() {
1432 panel = panel_from_node(req);
1433 }
1434 if !panel.is_map() {
1435 return ok({
1436 id: 4,
1437 type: "panel_rejected",
1438 reason: "invalid panel"
1439 });
1440 }
1441 panel.id = 4;
1442 panel.idx = req.idx;
1443 if !panel.contains("type") {
1444 panel.type = "panel";
1445 }
1446 if panel.contains("hotspots") {
1447 panel.hotspots = public_hotspots(req.idx, panel.panel_id, panel.hotspots);
1448 }
1449 root::send_idx("local/ws", req.idx, panel);
1450 ok({
1451 id: 4,
1452 type: "panel",
1453 panel_id: panel.panel_id
1454 })
1455 }
1456 "#
1457 .to_vec(),
1458 )?;
1459
1460 let compiled = vm.get_fn("vm_send_panel_public_hotspots::send_panel", &[Type::Any])?;
1461 assert_eq!(compiled.ret_ty(), &Type::Any);
1462 let send_panel: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1463 let req = dynamic::map!(
1464 "idx"=> 7i64,
1465 "panel"=> dynamic::map!(
1466 "panel_id"=> "main",
1467 "hotspots"=> dynamic::map!(
1468 "open"=> dynamic::map!("id"=> "open")
1469 )
1470 )
1471 );
1472 let result = unsafe { &*send_panel(&req) };
1473
1474 assert_eq!(result.get_dynamic("type").map(|value| value.as_str().to_string()), Some("panel".to_string()));
1475 assert_eq!(result.get_dynamic("panel_id").map(|value| value.as_str().to_string()), Some("main".to_string()));
1476 Ok(())
1477 }
1478
1479 #[test]
1480 fn map_assignment_accepts_string_concat_key() -> anyhow::Result<()> {
1481 let vm = Vm::with_all()?;
1482 vm.import_code(
1483 "vm_string_concat_map_key",
1484 br##"
1485 pub fn write_action(action_map, panel_id, action_id, action) {
1486 action_map[panel_id + "#" + action_id] = action;
1487 action_map[panel_id + "#" + action_id]
1488 }
1489 "##
1490 .to_vec(),
1491 )?;
1492
1493 let compiled = vm.get_fn("vm_string_concat_map_key::write_action", &[Type::Any, Type::Any, Type::Any, Type::Any])?;
1494 let write_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1495 let action_map = dynamic::map!();
1496 let panel_id: Dynamic = "panel".into();
1497 let action_id: Dynamic = "open".into();
1498 let action = dynamic::map!("id"=> "open");
1499
1500 let result = unsafe { &*write_action(&action_map, &panel_id, &action_id, &action) };
1501
1502 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("open".to_string()));
1503 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()));
1504 Ok(())
1505 }
1506
1507 #[test]
1508 fn map_get_key_accepts_string_concat_key_variable() -> anyhow::Result<()> {
1509 let vm = Vm::with_all()?;
1510 vm.import_code(
1511 "vm_get_key_string_concat_key",
1512 br##"
1513 pub fn read_action(action_map, panel_id, action_id) {
1514 let action_key = panel_id + "#" + action_id;
1515 action_map.get_key(action_key)
1516 }
1517 "##
1518 .to_vec(),
1519 )?;
1520
1521 let compiled = vm.get_fn("vm_get_key_string_concat_key::read_action", &[Type::Any, Type::Any, Type::Any])?;
1522 let read_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1523 let action_map = dynamic::map!("panel#open"=> dynamic::map!("id"=> "open"));
1524 let panel_id: Dynamic = "panel".into();
1525 let action_id: Dynamic = "open".into();
1526
1527 let result = unsafe { &*read_action(&action_map, &panel_id, &action_id) };
1528
1529 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("open".to_string()));
1530 Ok(())
1531 }
1532
1533 #[test]
1534 fn map_get_key_accepts_helper_string_key() -> anyhow::Result<()> {
1535 let vm = Vm::with_all()?;
1536 vm.import_code(
1537 "vm_get_key_helper_string_key",
1538 br##"
1539 pub fn make_action_key(panel_id, action_id) {
1540 panel_id + "#" + action_id
1541 }
1542
1543 pub fn read_action(action_map, panel_id, action_id) {
1544 let action_key = make_action_key(panel_id, action_id);
1545 let action = action_map.get_key(action_key);
1546 action
1547 }
1548 "##
1549 .to_vec(),
1550 )?;
1551
1552 let compiled = vm.get_fn("vm_get_key_helper_string_key::read_action", &[Type::Any, Type::Any, Type::Any])?;
1553 let read_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1554 let action_map = dynamic::map!("panel#open"=> dynamic::map!("id"=> "open"));
1555 let panel_id: Dynamic = "panel".into();
1556 let action_id: Dynamic = "open".into();
1557
1558 let result = unsafe { &*read_action(&action_map, &panel_id, &action_id) };
1559
1560 assert_eq!(result.get_dynamic("id").map(|value| value.as_str().to_string()), Some("open".to_string()));
1561 Ok(())
1562 }
1563
1564 #[test]
1565 fn map_del_key_removes_string_key_and_returns_removed_value() -> anyhow::Result<()> {
1566 let vm = Vm::with_all()?;
1567 vm.import_code(
1568 "vm_del_key_string_key",
1569 br##"
1570 pub fn remove_action(action_map, panel_id, action_id) {
1571 let action_key = panel_id + "#" + action_id;
1572 let removed = action_map.del_key(action_key);
1573 [removed, action_map.get_key(action_key)]
1574 }
1575 "##
1576 .to_vec(),
1577 )?;
1578
1579 let compiled = vm.get_fn("vm_del_key_string_key::remove_action", &[Type::Any, Type::Any, Type::Any])?;
1580 let remove_action: extern "C" fn(*const Dynamic, *const Dynamic, *const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1581 let action_map = dynamic::map!("panel#open"=> dynamic::map!("id"=> "open"));
1582 let panel_id: Dynamic = "panel".into();
1583 let action_id: Dynamic = "open".into();
1584
1585 let result = unsafe { &*remove_action(&action_map, &panel_id, &action_id) };
1586
1587 assert_eq!(result.get_idx(0).and_then(|value| value.get_dynamic("id")).map(|value| value.as_str().to_string()), Some("open".to_string()));
1588 assert!(result.get_idx(1).is_some_and(|value| value.is_null()));
1589 assert!(action_map.get_dynamic("panel#open").is_none());
1590 Ok(())
1591 }
1592
1593 #[test]
1594 fn dynamic_field_value_participates_in_or_expression() -> anyhow::Result<()> {
1595 let vm = Vm::with_all()?;
1596 vm.import_code(
1597 "vm_dynamic_field_or",
1598 r#"
1599 pub fn direct_next() {
1600 let choice = {
1601 label: "颜色",
1602 next: "color"
1603 };
1604 choice.next
1605 }
1606
1607 pub fn bracket_next() {
1608 let choice = {
1609 label: "颜色",
1610 next: "color"
1611 };
1612 choice["next"]
1613 }
1614 "#
1615 .as_bytes()
1616 .to_vec(),
1617 )?;
1618
1619 let compiled = vm.get_fn("vm_dynamic_field_or::direct_next", &[])?;
1620 assert_eq!(compiled.ret_ty(), &Type::Any);
1621 let direct_next: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1622 assert_eq!(unsafe { &*direct_next() }.as_str(), "color");
1623
1624 let compiled = vm.get_fn("vm_dynamic_field_or::bracket_next", &[])?;
1625 assert_eq!(compiled.ret_ty(), &Type::Any);
1626 let bracket_next: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1627 assert_eq!(unsafe { &*bracket_next() }.as_str(), "color");
1628 Ok(())
1629 }
1630
1631 #[test]
1632 fn empty_object_literal_in_if_branch_stays_dynamic() -> anyhow::Result<()> {
1633 let vm = Vm::with_all()?;
1634 vm.import_code(
1635 "vm_if_empty_object_branch",
1636 r#"
1637 pub fn first_note(steps) {
1638 let first = if steps.len() > 0 { steps[0] } else { {} };
1639 let first_note = if first.contains("note") { first.note } else { "fallback" };
1640 first_note
1641 }
1642
1643 pub fn first_ja(steps) {
1644 let first = if steps.len() > 0 { steps[0] } else { {} };
1645 if first.contains("ja") { first.ja } else { "すみません" }
1646 }
1647
1648 pub fn assign_first_note(steps) {
1649 let first = {};
1650 first = if steps.len() > 0 { steps[0] } else { {} };
1651 if first.contains("note") { first.note } else { "fallback" }
1652 }
1653 "#
1654 .as_bytes()
1655 .to_vec(),
1656 )?;
1657
1658 let compiled = vm.get_fn("vm_if_empty_object_branch::first_note", &[Type::Any])?;
1659 assert_eq!(compiled.ret_ty(), &Type::Any);
1660 let first_note: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1661
1662 let empty_steps = Dynamic::list(Vec::new());
1663 assert_eq!(unsafe { &*first_note(&empty_steps) }.as_str(), "fallback");
1664
1665 let mut step = std::collections::BTreeMap::new();
1666 step.insert("note".into(), "hello".into());
1667 let steps = Dynamic::list(vec![Dynamic::map(step)]);
1668 assert_eq!(unsafe { &*first_note(&steps) }.as_str(), "hello");
1669
1670 let compiled = vm.get_fn("vm_if_empty_object_branch::first_ja", &[Type::Any])?;
1671 assert_eq!(compiled.ret_ty(), &Type::Any);
1672 let first_ja: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1673 assert_eq!(unsafe { &*first_ja(&empty_steps) }.as_str(), "すみません");
1674
1675 let compiled = vm.get_fn("vm_if_empty_object_branch::assign_first_note", &[Type::Any])?;
1676 assert_eq!(compiled.ret_ty(), &Type::Any);
1677 let assign_first_note: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1678 assert_eq!(unsafe { &*assign_first_note(&empty_steps) }.as_str(), "fallback");
1679 assert_eq!(unsafe { &*assign_first_note(&steps) }.as_str(), "hello");
1680 Ok(())
1681 }
1682
1683 #[test]
1684 fn list_literal_can_be_function_tail_expression() -> anyhow::Result<()> {
1685 let vm = Vm::with_all()?;
1686 vm.import_code(
1687 "vm_tail_list_literal",
1688 r#"
1689 pub fn numbers() {
1690 [1, 2, 3]
1691 }
1692
1693 pub fn maps() {
1694 [
1695 {note: "first"},
1696 {note: "second"}
1697 ]
1698 }
1699
1700 pub fn object_with_maps() {
1701 {
1702 steps: [
1703 {note: "first"},
1704 {note: "second"}
1705 ]
1706 }
1707 }
1708
1709 pub fn return_maps() {
1710 return [
1711 {note: "first"},
1712 {note: "second"}
1713 ];
1714 }
1715
1716 pub fn return_maps_without_semicolon() {
1717 return [
1718 {note: "first"},
1719 {note: "second"}
1720 ]
1721 }
1722
1723 pub fn tail_bare_variable() {
1724 let value = [
1725 {note: "first"},
1726 {note: "second"}
1727 ];
1728 value
1729 }
1730
1731 pub fn return_bare_variable_without_semicolon() {
1732 let value = [
1733 {note: "first"},
1734 {note: "second"}
1735 ];
1736 return value
1737 }
1738
1739 pub fn tail_object_variable() {
1740 let result = {
1741 steps: [
1742 {note: "first"},
1743 {note: "second"}
1744 ]
1745 };
1746 result
1747 }
1748 "#
1749 .as_bytes()
1750 .to_vec(),
1751 )?;
1752
1753 let compiled = vm.get_fn("vm_tail_list_literal::numbers", &[])?;
1754 assert_eq!(compiled.ret_ty(), &Type::Any);
1755 let numbers: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1756 let result = unsafe { &*numbers() };
1757 assert_eq!(result.len(), 3);
1758 assert_eq!(result.get_idx(1).and_then(|value| value.as_int()), Some(2));
1759
1760 let compiled = vm.get_fn("vm_tail_list_literal::maps", &[])?;
1761 assert_eq!(compiled.ret_ty(), &Type::Any);
1762 let maps: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1763 let result = unsafe { &*maps() };
1764 assert_eq!(result.len(), 2);
1765 assert_eq!(result.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
1766
1767 let compiled = vm.get_fn("vm_tail_list_literal::object_with_maps", &[])?;
1768 assert_eq!(compiled.ret_ty(), &Type::Any);
1769 let object_with_maps: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1770 let result = unsafe { &*object_with_maps() };
1771 let steps = result.get_dynamic("steps").expect("steps");
1772 assert_eq!(steps.len(), 2);
1773 assert_eq!(steps.get_idx(0).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("first".to_string()));
1774
1775 let compiled = vm.get_fn("vm_tail_list_literal::return_maps", &[])?;
1776 assert_eq!(compiled.ret_ty(), &Type::Any);
1777 let return_maps: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1778 let result = unsafe { &*return_maps() };
1779 assert_eq!(result.len(), 2);
1780 assert_eq!(result.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
1781
1782 let compiled = vm.get_fn("vm_tail_list_literal::return_maps_without_semicolon", &[])?;
1783 assert_eq!(compiled.ret_ty(), &Type::Any);
1784 let return_maps_without_semicolon: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1785 let result = unsafe { &*return_maps_without_semicolon() };
1786 assert_eq!(result.len(), 2);
1787 assert_eq!(result.get_idx(0).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("first".to_string()));
1788
1789 let compiled = vm.get_fn("vm_tail_list_literal::tail_bare_variable", &[])?;
1790 assert_eq!(compiled.ret_ty(), &Type::Any);
1791 let tail_bare_variable: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1792 let result = unsafe { &*tail_bare_variable() };
1793 assert_eq!(result.len(), 2);
1794 assert_eq!(result.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
1795
1796 let compiled = vm.get_fn("vm_tail_list_literal::return_bare_variable_without_semicolon", &[])?;
1797 assert_eq!(compiled.ret_ty(), &Type::Any);
1798 let return_bare_variable_without_semicolon: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1799 let result = unsafe { &*return_bare_variable_without_semicolon() };
1800 assert_eq!(result.len(), 2);
1801 assert_eq!(result.get_idx(0).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("first".to_string()));
1802
1803 let compiled = vm.get_fn("vm_tail_list_literal::tail_object_variable", &[])?;
1804 assert_eq!(compiled.ret_ty(), &Type::Any);
1805 let tail_object_variable: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1806 let result = unsafe { &*tail_object_variable() };
1807 let steps = result.get_dynamic("steps").expect("steps");
1808 assert_eq!(steps.len(), 2);
1809 assert_eq!(steps.get_idx(1).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("second".to_string()));
1810 Ok(())
1811 }
1812
1813 #[test]
1814 fn list_return_value_supports_get_idx_method_call() -> anyhow::Result<()> {
1815 let vm = Vm::with_all()?;
1816 vm.import_code(
1817 "vm_returned_list_get_idx",
1818 r#"
1819 pub fn ids() {
1820 [
1821 "base",
1822 "2",
1823 "3"
1824 ]
1825 }
1826
1827 pub fn combinations() {
1828 let result = [];
1829 let values = ids();
1830 let idx = 0;
1831 while idx < values.len() {
1832 result.push(values.get_idx(idx));
1833 idx = idx + 1;
1834 }
1835 result
1836 }
1837 "#
1838 .as_bytes()
1839 .to_vec(),
1840 )?;
1841
1842 let compiled = vm.get_fn("vm_returned_list_get_idx::combinations", &[])?;
1843 assert_eq!(compiled.ret_ty(), &Type::Any);
1844 let combinations: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1845 let result = unsafe { &*combinations() };
1846
1847 assert_eq!(result.len(), 3);
1848 assert_eq!(result.get_idx(0).map(|value| value.as_str().to_string()), Some("base".to_string()));
1849 assert_eq!(result.get_idx(2).map(|value| value.as_str().to_string()), Some("3".to_string()));
1850 Ok(())
1851 }
1852
1853 #[test]
1854 fn repeated_deep_step_literals_import_successfully() -> anyhow::Result<()> {
1855 fn extra_page_literal(depth: usize) -> String {
1856 let mut value = "{leaf: \"done\"}".to_string();
1857 for idx in 0..depth {
1858 value = format!("{{kind: \"page\", idx: {idx}, children: [{value}], meta: {{title: \"extra\", visible: true}}}}");
1859 }
1860 value
1861 }
1862
1863 let extra = extra_page_literal(48);
1864 let code = format!(
1865 r#"
1866 pub fn script() {{
1867 return [
1868 {{ja: "一つ目", note: "first", extra: {extra}}},
1869 {{ja: "二つ目", note: "second", extra: {extra}}},
1870 {{ja: "三つ目", note: "third", extra: {extra}}}
1871 ]
1872 }}
1873 "#
1874 );
1875
1876 let vm = Vm::with_all()?;
1877 vm.import_code("vm_repeated_deep_step_literals", code.into_bytes())?;
1878 let compiled = vm.get_fn("vm_repeated_deep_step_literals::script", &[])?;
1879 assert_eq!(compiled.ret_ty(), &Type::Any);
1880 let script: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1881 let result = unsafe { &*script() };
1882 assert_eq!(result.len(), 3);
1883 assert_eq!(result.get_idx(2).and_then(|value| value.get_dynamic("note")).map(|value| value.as_str().to_string()), Some("third".to_string()));
1884 Ok(())
1885 }
1886
1887 #[test]
1888 fn native_import_uses_owning_vm() -> anyhow::Result<()> {
1889 let module_path = std::env::temp_dir().join(format!("zust_vm_import_owner_{}.zs", std::process::id()));
1890 std::fs::write(&module_path, "pub fn value() { 41 }")?;
1891 let module_path = module_path.to_string_lossy().replace('\\', "\\\\").replace('"', "\\\"");
1892
1893 let vm1 = Vm::with_all()?;
1894 vm1.import_code(
1895 "vm_import_owner",
1896 format!(
1897 r#"
1898 pub fn run() {{
1899 import("vm_imported_owner", "{module_path}");
1900 }}
1901 "#
1902 )
1903 .into_bytes(),
1904 )?;
1905 let compiled = vm1.get_fn("vm_import_owner::run", &[])?;
1906
1907 let vm2 = Vm::with_all()?;
1908 vm2.import_code("vm_import_other", b"pub fn run() { 0 }".to_vec())?;
1909 let _ = vm2.get_fn("vm_import_other::run", &[])?;
1910
1911 let run: extern "C" fn() = unsafe { std::mem::transmute(compiled.ptr()) };
1912 run();
1913
1914 assert!(vm1.get_fn("vm_imported_owner::value", &[]).is_ok());
1915 assert!(vm2.get_fn("vm_imported_owner::value", &[]).is_err());
1916 Ok(())
1917 }
1918
1919 #[test]
1920 fn object_last_field_call_does_not_need_trailing_comma() -> anyhow::Result<()> {
1921 let vm = Vm::with_all()?;
1922 vm.import_code(
1923 "vm_object_last_call_field",
1924 r#"
1925 pub fn extra_page() {
1926 {
1927 title: "extra",
1928 pages: [
1929 {note: "nested"}
1930 ]
1931 }
1932 }
1933
1934 pub fn data() {
1935 return [
1936 {
1937 note: "first",
1938 choices: ["a", "b"],
1939 extras: extra_page()
1940 },
1941 {
1942 note: "second",
1943 choices: ["c"],
1944 extras: extra_page()
1945 }
1946 ]
1947 }
1948 "#
1949 .as_bytes()
1950 .to_vec(),
1951 )?;
1952
1953 let compiled = vm.get_fn("vm_object_last_call_field::data", &[])?;
1954 assert_eq!(compiled.ret_ty(), &Type::Any);
1955 let data: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
1956 let result = unsafe { &*data() };
1957 assert_eq!(result.len(), 2);
1958 let first = result.get_idx(0).expect("first step");
1959 assert_eq!(first.get_dynamic("extras").and_then(|extras| extras.get_dynamic("title")).map(|title| title.as_str().to_string()), Some("extra".to_string()));
1960 Ok(())
1961 }
1962
1963 #[test]
1964 fn string_return_survives_scope_exit() -> anyhow::Result<()> {
1965 let vm = Vm::with_all()?;
1966 vm.import_code(
1967 "vm_string_return_scope",
1968 r#"
1969 pub fn source_root() {
1970 "../assets/character/男主角换装"
1971 }
1972
1973 pub fn binary_root() {
1974 "character_binary/男主角换装"
1975 }
1976
1977 pub fn runtime_binary_url() {
1978 "/" + binary_root()
1979 }
1980
1981 pub fn action_groups() {
1982 let root = source_root();
1983 let binary_url = runtime_binary_url();
1984 let binary_root = binary_root();
1985 [
1986 {
1987 id: "field_bottom",
1988 source_spine: root + "/战斗外/boy_b.spine",
1989 skeleton: binary_url + "/战斗外/boy_b/boy_b.skel.bytes",
1990 export_skeleton: binary_root + "/战斗外/boy_b/boy_b.skel.bytes"
1991 }
1992 ]
1993 }
1994 "#
1995 .as_bytes()
1996 .to_vec(),
1997 )?;
1998
1999 let compiled = vm.get_fn("vm_string_return_scope::source_root", &[])?;
2000 assert_eq!(compiled.ret_ty(), &Type::Str);
2001 let source_root: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2002 let source_root = unsafe { &*source_root() };
2003 assert_eq!(source_root.as_str(), "../assets/character/男主角换装");
2004
2005 let compiled = vm.get_fn("vm_string_return_scope::action_groups", &[])?;
2006 assert_eq!(compiled.ret_ty(), &Type::Any);
2007 let action_groups: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2008 let groups = unsafe { &*action_groups() };
2009 let first = groups.get_idx(0).expect("first action group");
2010 assert_eq!(first.get_dynamic("source_spine").map(|value| value.as_str().to_string()), Some("../assets/character/男主角换装/战斗外/boy_b.spine".to_string()));
2011 assert_eq!(first.get_dynamic("skeleton").map(|value| value.as_str().to_string()), Some("/character_binary/男主角换装/战斗外/boy_b/boy_b.skel.bytes".to_string()));
2012 Ok(())
2013 }
2014
2015 #[test]
2016 fn large_dynamic_object_accepts_inline_call_fields() -> anyhow::Result<()> {
2017 let vm = Vm::with_all()?;
2018 let model_count = 180;
2019 let combination_count = 90;
2020 let models = (0..model_count)
2021 .map(|idx| {
2022 format!(
2023 r#"{{id: "model_{idx}", name: "模型_{idx}", source: "/美术资源/角色/少年/套装_{idx}/模型_{idx}.model.json", parts: [
2024 {{slot: "hair", path: "/模型/头发/颜色_{idx}/默认.png", z: 10}},
2025 {{slot: "body", path: "/模型/身体/套装_{idx}/默认.png", z: 1}},
2026 {{slot: "face", path: "/模型/表情/表情_{idx}/默认.png", z: 20}}
2027 ]}}"#
2028 )
2029 })
2030 .collect::<Vec<_>>()
2031 .join(",\n");
2032 let combinations = (0..combination_count).map(|idx| format!(r#"{{hair: "color_{idx}", body: "set_{idx}", face: "face_{idx}"}}"#)).collect::<Vec<_>>().join(",\n");
2033 let code = format!(
2034 r#"
2035 pub fn source_root() {{
2036 "/美术资源/角色/少年/默认"
2037 }}
2038
2039 pub fn runtime_boy_url() {{
2040 "/cdn/runtime/角色/少年/少年.model.json"
2041 }}
2042
2043 pub fn parts() {{
2044 [
2045 {{id: "hair", path: "/模型/头发/黑色/默认.png", z: 10}},
2046 {{id: "body", path: "/模型/身体/校服/默认.png", z: 1}},
2047 {{id: "face", path: "/模型/表情/微笑/默认.png", z: 20}}
2048 ]
2049 }}
2050
2051 pub fn action_groups() {{
2052 {{
2053 idle: [
2054 {{id: "stand", name: "站立", frames: ["待机/0001.png", "待机/0002.png"]}},
2055 {{id: "blink", name: "眨眼", frames: ["表情/眨眼/0001.png", "表情/眨眼/0002.png"]}}
2056 ],
2057 move: [
2058 {{id: "walk", name: "行走", frames: ["行走/0001.png", "行走/0002.png"]}},
2059 {{id: "run", name: "奔跑", frames: ["奔跑/0001.png", "奔跑/0002.png"]}}
2060 ]
2061 }}
2062 }}
2063
2064 pub fn default_model() {{
2065 {{
2066 id: "runtime_boy",
2067 name: "运行时少年",
2068 skins: [
2069 {{id: "school", title: "校服", source: "/套装/校服/model.json"}},
2070 {{id: "casual", title: "便服", source: "/套装/便服/model.json"}}
2071 ],
2072 models: [
2073 {models}
2074 ]
2075 }}
2076 }}
2077
2078 pub fn first_nine_combinations() {{
2079 [
2080 {combinations}
2081 ]
2082 }}
2083
2084 pub fn config() {{
2085 {{
2086 source_root: source_root(),
2087 runtime_boy_url: runtime_boy_url(),
2088 parts: parts(),
2089 action_groups: action_groups(),
2090 default_model: default_model(),
2091 first_nine_combinations: first_nine_combinations()
2092 }}
2093 }}
2094
2095 pub fn start() {{
2096 root::add("local/vm_large_inline_call_object/config", {{
2097 source_root: source_root(),
2098 runtime_boy_url: runtime_boy_url(),
2099 parts: parts(),
2100 action_groups: action_groups(),
2101 default_model: default_model(),
2102 first_nine_combinations: first_nine_combinations()
2103 }})
2104 }}
2105 "#
2106 );
2107 vm.import_code("vm_large_inline_call_object", code.into_bytes())?;
2108
2109 let compiled = vm.get_fn("vm_large_inline_call_object::config", &[])?;
2110 assert_eq!(compiled.ret_ty(), &Type::Any);
2111 let config: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2112 let result = unsafe { &*config() };
2113 assert_eq!(result.get_dynamic("source_root").map(|value| value.as_str().to_string()), Some("/美术资源/角色/少年/默认".to_string()));
2114 assert_eq!(result.get_dynamic("first_nine_combinations").map(|value| value.len()), Some(combination_count));
2115
2116 let compiled = vm.get_fn("vm_large_inline_call_object::start", &[])?;
2117 assert_eq!(compiled.ret_ty(), &Type::Bool);
2118 let start: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2119 assert!(start());
2120 let saved = root::get("local/vm_large_inline_call_object/config")?;
2121 assert_eq!(saved.get_dynamic("first_nine_combinations").map(|value| value.len()), Some(combination_count));
2122 Ok(())
2123 }
2124
2125 #[test]
2126 fn gpu_struct_layout_packs_and_unpacks_dynamic_maps() -> anyhow::Result<()> {
2127 let vm = Vm::with_all()?;
2128 vm.import_code(
2129 "vm_gpu_layout",
2130 br#"
2131 pub struct Params {
2132 a: u32,
2133 b: u32,
2134 c: u32,
2135 }
2136 "#
2137 .to_vec(),
2138 )?;
2139
2140 let layout = vm.gpu_struct_layout("vm_gpu_layout::Params", &[])?;
2141 assert_eq!(layout.size, 16);
2142 assert_eq!(layout.fields.iter().map(|field| (field.name.as_str(), field.offset)).collect::<Vec<_>>(), vec![("a", 0), ("b", 4), ("c", 8)]);
2143
2144 let value = dynamic::map!("a"=> 1u32, "b"=> 2u32, "c"=> 3u32);
2145 let bytes = layout.pack_map(&value)?;
2146 assert_eq!(bytes.len(), 16);
2147 assert_eq!(&bytes[0..4], &1u32.to_ne_bytes());
2148 assert_eq!(&bytes[4..8], &2u32.to_ne_bytes());
2149 assert_eq!(&bytes[8..12], &3u32.to_ne_bytes());
2150
2151 let read = layout.unpack_map(&bytes)?;
2152 assert_eq!(read.get_dynamic("a").and_then(|value| value.as_uint()), Some(1));
2153 assert_eq!(read.get_dynamic("b").and_then(|value| value.as_uint()), Some(2));
2154 assert_eq!(read.get_dynamic("c").and_then(|value| value.as_uint()), Some(3));
2155 Ok(())
2156 }
2157
2158 #[test]
2159 fn root_native_calls_do_not_take_ownership_of_dynamic_args() -> anyhow::Result<()> {
2160 let vm = Vm::with_all()?;
2161 vm.import_code(
2162 "vm_root_clone_bridge",
2163 br#"
2164 pub fn add_then_reuse(arg) {
2165 let user = {
2166 address: "test-wallet",
2167 points: 20
2168 };
2169 root::add("local/root-clone-bridge-user", user);
2170 user.points = user.points - 7;
2171 root::add("local/root-clone-bridge-user", user);
2172 {
2173 user: user,
2174 points: user.points
2175 }
2176 }
2177
2178 pub fn clone_then_mutate(arg) {
2179 let user = {
2180 profile: {
2181 points: 20
2182 }
2183 };
2184 let copied = user.clone();
2185 copied.profile.points = 13;
2186 user
2187 }
2188 "#
2189 .to_vec(),
2190 )?;
2191
2192 let compiled = vm.get_fn("vm_root_clone_bridge::add_then_reuse", &[Type::Any])?;
2193 assert_eq!(compiled.ret_ty(), &Type::Any);
2194 let add_then_reuse: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2195 let arg = Dynamic::Null;
2196 let result = add_then_reuse(&arg);
2197 let result = unsafe { &*result };
2198
2199 assert_eq!(result.get_dynamic("points").and_then(|value| value.as_int()), Some(13));
2200 let mut json = String::new();
2201 result.to_json(&mut json);
2202 assert!(json.contains("\"points\": 13"));
2203
2204 let clone_then_mutate = vm.get_fn("vm_root_clone_bridge::clone_then_mutate", &[Type::Any])?;
2205 let clone_then_mutate: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(clone_then_mutate.ptr()) };
2206 let result = clone_then_mutate(&arg);
2207 let result = unsafe { &*result };
2208 assert_eq!(result.get_dynamic("profile").unwrap().get_dynamic("points").and_then(|value| value.as_int()), Some(20));
2209 Ok(())
2210 }
2211
2212 struct CounterForTypedReceiver {
2213 value: i64,
2214 }
2215
2216 extern "C" fn counter_for_typed_receiver_get(value: *const Dynamic) -> i64 {
2217 unsafe { &*value }.as_custom::<CounterForTypedReceiver>().map(|counter| counter.value).unwrap_or(-1)
2218 }
2219
2220 struct NavMapForFunctionArg;
2221
2222 extern "C" fn nav_map_for_function_arg_new() -> *const Dynamic {
2223 Box::into_raw(Box::new(Dynamic::custom(NavMapForFunctionArg)))
2224 }
2225
2226 #[test]
2227 fn typed_receiver_method_call_dispatches_with_type_hint() -> anyhow::Result<()> {
2228 let vm = Vm::with_all()?;
2229 vm.add_empty_type("Counter")?;
2230 let counter_ty = vm.get_symbol("Counter", Vec::new())?;
2231 vm.add_native_method_ptr("Counter", "get", &[counter_ty], Type::I64, counter_for_typed_receiver_get as *const u8)?;
2232 vm.import_code(
2233 "vm_typed_receiver_method",
2234 br#"
2235 pub fn run(value) {
2236 value::<Counter>::get()
2237 }
2238 "#
2239 .to_vec(),
2240 )?;
2241
2242 let compiled = vm.get_fn("vm_typed_receiver_method::run", &[Type::Any])?;
2243 assert_eq!(compiled.ret_ty(), &Type::I64);
2244 let run: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2245 let value = Dynamic::custom(CounterForTypedReceiver { value: 42 });
2246
2247 assert_eq!(run(&value), 42);
2248 Ok(())
2249 }
2250
2251 #[test]
2252 fn native_custom_object_can_be_passed_to_zs_function() -> anyhow::Result<()> {
2253 let vm = Vm::with_all()?;
2254 vm.add_empty_type("NavMap")?;
2255 vm.add_native_method_ptr("NavMap", "new", &[], Type::Any, nav_map_for_function_arg_new as *const u8)?;
2256 vm.import_code(
2257 "vm_native_custom_arg",
2258 br#"
2259 pub fn add_nav_spawns(world, navmap) {
2260 navmap
2261 }
2262
2263 pub fn run(world) {
2264 let navmap = NavMap::new();
2265 let with_spawns = add_nav_spawns(world, navmap);
2266 with_spawns
2267 }
2268 "#
2269 .to_vec(),
2270 )?;
2271
2272 let compiled = vm.get_fn("vm_native_custom_arg::run", &[Type::Any])?;
2273 assert_eq!(compiled.ret_ty(), &Type::Any);
2274 let run: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2275 let world = Dynamic::Null;
2276 let result = run(&world);
2277 let result = unsafe { &*result };
2278
2279 assert!(result.as_custom::<NavMapForFunctionArg>().is_some());
2280 Ok(())
2281 }
2282
2283 #[test]
2284 fn native_custom_object_typed_local_can_be_passed_to_zs_function() -> anyhow::Result<()> {
2285 let vm = Vm::with_all()?;
2286 vm.add_empty_type("NavMap")?;
2287 let _nav_map_ty = vm.get_symbol("NavMap", Vec::new())?;
2288 vm.add_native_method_ptr("NavMap", "new", &[], Type::Any, nav_map_for_function_arg_new as *const u8)?;
2289 vm.import_code(
2290 "vm_native_custom_typed_arg",
2291 br#"
2292 pub fn add_nav_spawns(world, navmap) {
2293 navmap
2294 }
2295
2296 pub fn run(world) {
2297 let navmap: NavMap = NavMap::new();
2298 let with_spawns = add_nav_spawns(world, navmap);
2299 with_spawns
2300 }
2301 "#
2302 .to_vec(),
2303 )?;
2304
2305 let compiled = vm.get_fn("vm_native_custom_typed_arg::run", &[Type::Any])?;
2306 let run: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2307 let world = Dynamic::Null;
2308 let result = run(&world);
2309 let result = unsafe { &*result };
2310
2311 assert!(result.as_custom::<NavMapForFunctionArg>().is_some());
2312 Ok(())
2313 }
2314
2315 #[test]
2318 fn dynamic_type_checks_on_null_and_primitive_values() -> anyhow::Result<()> {
2319 let vm = Vm::with_all()?;
2320 vm.import_code(
2321 "vm_dynamic_type_checks",
2322 br#"
2323 pub fn is_list_on_int() {
2324 let x = 42i64;
2325 x.is_list()
2326 }
2327
2328 pub fn is_map_on_int() {
2329 let x = 42i64;
2330 x.is_map()
2331 }
2332
2333 pub fn is_null_on_int() {
2334 let x = 42i64;
2335 x.is_null()
2336 }
2337 "#
2338 .to_vec(),
2339 )?;
2340
2341 let compiled = vm.get_fn("vm_dynamic_type_checks::is_list_on_int", &[])?;
2342 assert_eq!(compiled.ret_ty(), &Type::Bool);
2343 let is_list_on_int: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2344 assert!(!is_list_on_int());
2345
2346 let compiled = vm.get_fn("vm_dynamic_type_checks::is_map_on_int", &[])?;
2347 assert_eq!(compiled.ret_ty(), &Type::Bool);
2348 let is_map_on_int: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2349 assert!(!is_map_on_int());
2350
2351 let compiled = vm.get_fn("vm_dynamic_type_checks::is_null_on_int", &[])?;
2352 assert_eq!(compiled.ret_ty(), &Type::Bool);
2353 let is_null_on_int: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2354 assert!(!is_null_on_int());
2355 Ok(())
2356 }
2357
2358 #[test]
2359 fn empty_for_loop_range_has_zero_iterations() -> anyhow::Result<()> {
2360 let vm = Vm::with_all()?;
2361 vm.import_code(
2362 "vm_empty_for_range",
2363 br#"
2364 pub fn empty_exclusive() {
2365 let count = 0i32;
2366 for i in 0..0 {
2367 count += i;
2368 }
2369 count
2370 }
2371
2372 pub fn single_inclusive_iteration() {
2373 let count = 0i32;
2374 for i in 5..=5 {
2375 count += i;
2376 }
2377 count
2378 }
2379 "#
2380 .to_vec(),
2381 )?;
2382
2383 let compiled = vm.get_fn("vm_empty_for_range::empty_exclusive", &[])?;
2384 assert_eq!(compiled.ret_ty(), &Type::I32);
2385 let empty_exclusive: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
2386 assert_eq!(empty_exclusive(), 0);
2387
2388 let compiled = vm.get_fn("vm_empty_for_range::single_inclusive_iteration", &[])?;
2389 assert_eq!(compiled.ret_ty(), &Type::I32);
2390 let single_inclusive: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
2391 assert_eq!(single_inclusive(), 5);
2392 Ok(())
2393 }
2394
2395 #[test]
2396 fn map_contains_key_on_non_existent_and_nested_keys() -> anyhow::Result<()> {
2397 let vm = Vm::with_all()?;
2398 vm.import_code(
2399 "vm_map_contains",
2400 br#"
2401 pub fn contains_existing(data) {
2402 data.contains("name")
2403 }
2404
2405 pub fn contains_missing(data) {
2406 data.contains("nothing")
2407 }
2408 "#
2409 .to_vec(),
2410 )?;
2411
2412 let compiled = vm.get_fn("vm_map_contains::contains_existing", &[Type::Any])?;
2413 assert_eq!(compiled.ret_ty(), &Type::Bool);
2414 let contains_existing: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2415 let data = dynamic::map!("name"=> "test");
2416 assert!(contains_existing(&data));
2417
2418 let compiled = vm.get_fn("vm_map_contains::contains_missing", &[Type::Any])?;
2419 assert_eq!(compiled.ret_ty(), &Type::Bool);
2420 let contains_missing: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2421 assert!(!contains_missing(&data));
2422 Ok(())
2423 }
2424
2425 #[test]
2426 fn list_pop_on_empty_list_returns_null() -> anyhow::Result<()> {
2427 let vm = Vm::with_all()?;
2428 vm.import_code(
2429 "vm_pop_empty",
2430 br#"
2431 pub fn pop_new_list() {
2432 let items = [];
2433 let value = items.pop();
2434 let still_empty = items.len() == 0;
2435 {value: value, empty: still_empty}
2436 }
2437
2438 pub fn pop_until_empty() {
2439 let items = [1i64, 2i64];
2440 items.pop();
2441 let last = items.pop();
2442 let drained = items.pop();
2443 {last: last, drained: drained}
2444 }
2445 "#
2446 .to_vec(),
2447 )?;
2448
2449 let compiled = vm.get_fn("vm_pop_empty::pop_new_list", &[])?;
2450 assert_eq!(compiled.ret_ty(), &Type::Any);
2451 let pop_new_list: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2452 let result = unsafe { &*pop_new_list() };
2453 assert!(result.get_dynamic("value").is_some_and(|v| v.is_null()));
2454 assert_eq!(result.get_dynamic("empty").and_then(|v| v.as_bool()), Some(true));
2455
2456 let compiled = vm.get_fn("vm_pop_empty::pop_until_empty", &[])?;
2457 assert_eq!(compiled.ret_ty(), &Type::Any);
2458 let pop_until_empty: extern "C" fn() -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2459 let result = unsafe { &*pop_until_empty() };
2460 assert_eq!(result.get_dynamic("last").and_then(|v| v.as_int()), Some(1));
2461 assert!(result.get_dynamic("drained").is_some_and(|v| v.is_null()));
2462 Ok(())
2463 }
2464
2465 #[test]
2466 fn void_function_with_multiple_code_paths() -> anyhow::Result<()> {
2467 let vm = Vm::with_all()?;
2468 vm.import_code(
2469 "vm_void_multi_path",
2470 br#"
2471 pub fn log_if_positive(value: i64) {
2472 if value > 0 {
2473 print(value);
2474 return;
2475 }
2476 if value < 0 {
2477 print(-value);
2478 return;
2479 }
2480 print(0);
2481 }
2482 "#
2483 .to_vec(),
2484 )?;
2485
2486 let compiled = vm.get_fn("vm_void_multi_path::log_if_positive", &[Type::I64])?;
2487 assert!(compiled.ret_ty().is_void());
2488 Ok(())
2489 }
2490
2491 #[test]
2492 fn any_method_call_chain_on_returned_dynamic_value() -> anyhow::Result<()> {
2493 let vm = Vm::with_all()?;
2494 vm.import_code(
2495 "vm_any_method_chain",
2496 br#"
2497 pub fn get_tags(data) {
2498 let tags = data.tags;
2499 if tags.is_list() {
2500 return tags.len();
2501 }
2502 0
2503 }
2504 "#
2505 .to_vec(),
2506 )?;
2507
2508 let compiled = vm.get_fn("vm_any_method_chain::get_tags", &[Type::Any])?;
2509 assert_eq!(compiled.ret_ty(), &Type::I32);
2510 let get_tags: extern "C" fn(*const Dynamic) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
2511 let data = dynamic::map!("tags"=> Dynamic::list(vec!["a".into(), "b".into(), "c".into()]));
2512 assert_eq!(get_tags(&data), 3);
2513
2514 let empty_data = Dynamic::Null;
2515 assert_eq!(get_tags(&empty_data), 0);
2516 Ok(())
2517 }
2518
2519 #[test]
2520 fn root_get_returns_null_for_missing_key_which_compares_correctly() -> anyhow::Result<()> {
2521 let vm = Vm::with_all()?;
2522 vm.import_code(
2523 "vm_root_get_missing",
2524 br#"
2525 pub fn check_missing() {
2526 let existing = root::get("local/vm_root_get_missing_test");
2527 if existing.is_map() {
2528 return false;
2529 }
2530 true
2531 }
2532 "#
2533 .to_vec(),
2534 )?;
2535
2536 let compiled = vm.get_fn("vm_root_get_missing::check_missing", &[])?;
2537 assert_eq!(compiled.ret_ty(), &Type::Bool);
2538 let check_missing: extern "C" fn() -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2539 assert!(check_missing());
2540 Ok(())
2541 }
2542
2543 #[test]
2544 fn map_get_key_on_null_map_returns_null() -> anyhow::Result<()> {
2545 let vm = Vm::with_all()?;
2546 vm.import_code(
2547 "vm_get_key_null_map",
2548 br#"
2549 pub fn get_key_null(data) {
2550 data.get_key("missing")
2551 }
2552 "#
2553 .to_vec(),
2554 )?;
2555
2556 let compiled = vm.get_fn("vm_get_key_null_map::get_key_null", &[Type::Any])?;
2557 assert_eq!(compiled.ret_ty(), &Type::Any);
2558 let get_key_null: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2559
2560 let data_map = dynamic::map!("exists"=> 1i64);
2561 let missing = unsafe { &*get_key_null(&data_map) };
2562 assert!(missing.is_null());
2563
2564 let null = Dynamic::Null;
2565 let result = unsafe { &*get_key_null(&null) };
2566 assert!(result.is_null());
2567 Ok(())
2568 }
2569
2570 #[test]
2571 fn keys_on_empty_map_returns_empty_list() -> anyhow::Result<()> {
2572 let vm = Vm::with_all()?;
2573 vm.import_code(
2574 "vm_keys_empty_map",
2575 br#"
2576 pub fn empty_map_keys() {
2577 let data = {};
2578 data.keys().len()
2579 }
2580 "#
2581 .to_vec(),
2582 )?;
2583
2584 let compiled = vm.get_fn("vm_keys_empty_map::empty_map_keys", &[])?;
2585 assert_eq!(compiled.ret_ty(), &Type::I32);
2586 let empty_map_keys: extern "C" fn() -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
2587 assert_eq!(empty_map_keys(), 0);
2588 Ok(())
2589 }
2590
2591 #[test]
2592 fn cast_between_all_integer_widths() -> anyhow::Result<()> {
2593 let vm = Vm::with_all()?;
2594 vm.import_code(
2595 "vm_cast_integer_widths",
2596 br#"
2597 pub fn i64_to_i32(value: i64) {
2598 value as i32
2599 }
2600
2601 pub fn i32_to_i64(value: i32) {
2602 value as i64
2603 }
2604
2605 pub fn u32_to_i64(value: u32) {
2606 value as i64
2607 }
2608 "#
2609 .to_vec(),
2610 )?;
2611
2612 let compiled = vm.get_fn("vm_cast_integer_widths::i64_to_i32", &[Type::I64])?;
2613 assert_eq!(compiled.ret_ty(), &Type::I32);
2614 let i64_to_i32: extern "C" fn(i64) -> i32 = unsafe { std::mem::transmute(compiled.ptr()) };
2615 assert_eq!(i64_to_i32(42), 42);
2616
2617 let compiled = vm.get_fn("vm_cast_integer_widths::i32_to_i64", &[Type::I32])?;
2618 assert_eq!(compiled.ret_ty(), &Type::I64);
2619 let i32_to_i64: extern "C" fn(i32) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2620 assert_eq!(i32_to_i64(-1), -1);
2621
2622 let compiled = vm.get_fn("vm_cast_integer_widths::u32_to_i64", &[Type::U32])?;
2623 assert_eq!(compiled.ret_ty(), &Type::I64);
2624 let u32_to_i64: extern "C" fn(u32) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2625 assert_eq!(u32_to_i64(42), 42);
2626 Ok(())
2627 }
2628
2629 #[test]
2630 fn boolean_literals_in_complex_expression_trees() -> anyhow::Result<()> {
2631 let vm = Vm::with_all()?;
2632 vm.import_code(
2633 "vm_complex_boolean",
2634 br#"
2635 pub fn exclusive_or(a: bool, b: bool) {
2636 (a && !b) || (!a && b)
2637 }
2638
2639 pub fn implies(a: bool, b: bool) {
2640 !a || b
2641 }
2642 "#
2643 .to_vec(),
2644 )?;
2645
2646 let compiled = vm.get_fn("vm_complex_boolean::exclusive_or", &[Type::Bool, Type::Bool])?;
2647 assert_eq!(compiled.ret_ty(), &Type::Bool);
2648 let exclusive_or: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2649 assert!(exclusive_or(true, false));
2650 assert!(exclusive_or(false, true));
2651 assert!(!exclusive_or(true, true));
2652 assert!(!exclusive_or(false, false));
2653
2654 let compiled = vm.get_fn("vm_complex_boolean::implies", &[Type::Bool, Type::Bool])?;
2655 assert_eq!(compiled.ret_ty(), &Type::Bool);
2656 let implies: extern "C" fn(bool, bool) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2657 assert!(implies(false, true));
2658 assert!(implies(false, false));
2659 assert!(implies(true, true));
2660 assert!(!implies(true, false));
2661 Ok(())
2662 }
2663
2664 #[test]
2665 fn concrete_struct_method_returning_self_type() -> anyhow::Result<()> {
2666 let vm = Vm::with_all()?;
2667 vm.import_code(
2668 "vm_struct_method_self",
2669 br#"
2670 pub struct Vec3 {
2671 x: f64,
2672 y: f64,
2673 z: f64,
2674 }
2675
2676 impl Vec3 {
2677 pub fn add(self: Vec3, other: Vec3) {
2678 Vec3{x: self.x + other.x, y: self.y + other.y, z: self.z + other.z}
2679 }
2680 }
2681
2682 pub fn run() {
2683 let v1 = Vec3{x: 1.0f64, y: 2.0f64, z: 3.0f64};
2684 let v2 = Vec3{x: 4.0f64, y: 5.0f64, z: 6.0f64};
2685 let sum = v1.add(v2);
2686 sum.x + sum.y + sum.z
2687 }
2688 "#
2689 .to_vec(),
2690 )?;
2691
2692 let compiled = vm.get_fn("vm_struct_method_self::run", &[])?;
2693 assert_eq!(compiled.ret_ty(), &Type::F64);
2694 let run: extern "C" fn() -> f64 = unsafe { std::mem::transmute(compiled.ptr()) };
2695 assert_eq!(run(), 21.0);
2696 Ok(())
2697 }
2698
2699 #[test]
2700 fn deep_nested_struct_access_with_multiple_field_levels() -> anyhow::Result<()> {
2701 let vm = Vm::with_all()?;
2702 vm.import_code(
2703 "vm_deep_nested_struct",
2704 br#"
2705 pub struct A {
2706 value: i64,
2707 }
2708
2709 pub struct B {
2710 a: A,
2711 }
2712
2713 pub struct C {
2714 b: B,
2715 }
2716
2717 pub fn direct_access() {
2718 let c = C{b: B{a: A{value: 99}}};
2719 c.b.a.value
2720 }
2721
2722 pub fn via_variable() {
2723 let c = C{b: B{a: A{value: 77}}};
2724 let b = c.b;
2725 let a = b.a;
2726 a.value
2727 }
2728 "#
2729 .to_vec(),
2730 )?;
2731
2732 let compiled = vm.get_fn("vm_deep_nested_struct::direct_access", &[])?;
2733 assert_eq!(compiled.ret_ty(), &Type::I64);
2734 let direct_access: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2735 assert_eq!(direct_access(), 99);
2736
2737 let compiled = vm.get_fn("vm_deep_nested_struct::via_variable", &[])?;
2738 assert_eq!(compiled.ret_ty(), &Type::I64);
2739 let via_variable: extern "C" fn() -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2740 assert_eq!(via_variable(), 77);
2741 Ok(())
2742 }
2743
2744 #[test]
2745 fn array_index_with_dynamic_value_via_method() -> anyhow::Result<()> {
2746 let vm = Vm::with_all()?;
2747 vm.import_code(
2748 "vm_array_idx_dynamic",
2749 br#"
2750 pub fn get_by_idx(list, idx) {
2751 list.get_idx(idx)
2752 }
2753 "#
2754 .to_vec(),
2755 )?;
2756
2757 let compiled = vm.get_fn("vm_array_idx_dynamic::get_by_idx", &[Type::Any, Type::I64])?;
2758 assert_eq!(compiled.ret_ty(), &Type::Any);
2759 let get_by_idx: extern "C" fn(*const Dynamic, i64) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2760
2761 let list = Dynamic::list(vec!["a".into(), "b".into()]);
2762 let first = unsafe { &*get_by_idx(&list, 0) };
2763 assert_eq!(first.as_str(), "a");
2764
2765 let out = unsafe { &*get_by_idx(&list, 10) };
2766 assert!(out.is_null());
2767 Ok(())
2768 }
2769
2770 #[test]
2771 fn dynamic_field_access_with_optional_or_fallback() -> anyhow::Result<()> {
2772 let vm = Vm::with_all()?;
2773 vm.import_code(
2774 "vm_dynamic_or_fallback",
2775 br#"
2776 pub fn with_fallback(data) {
2777 if data.contains("name") { data.name } else { "unknown" }
2778 }
2779
2780 pub fn with_fallback_missing(data) {
2781 if data.contains("nickname") { data.nickname } else { "unnamed" }
2782 }
2783 "#
2784 .to_vec(),
2785 )?;
2786
2787 let compiled = vm.get_fn("vm_dynamic_or_fallback::with_fallback", &[Type::Any])?;
2788 assert_eq!(compiled.ret_ty(), &Type::Any);
2789 let with_fallback: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2790 let data = dynamic::map!("name"=> "Alice");
2791 let result = unsafe { &*with_fallback(&data) };
2792 assert_eq!(result.as_str(), "Alice");
2793
2794 let compiled = vm.get_fn("vm_dynamic_or_fallback::with_fallback_missing", &[Type::Any])?;
2795 let with_fallback_missing: extern "C" fn(*const Dynamic) -> *const Dynamic = unsafe { std::mem::transmute(compiled.ptr()) };
2796 let result = unsafe { &*with_fallback_missing(&data) };
2797 assert_eq!(result.as_str(), "unnamed");
2798 Ok(())
2799 }
2800
2801 #[test]
2802 fn for_in_loop_iterates_over_list_and_map_directly() -> anyhow::Result<()> {
2803 let vm = Vm::with_all()?;
2804 vm.import_code(
2805 "vm_for_in_collection",
2806 br#"
2807 pub fn sum_list(items) {
2808 let total = 0i64;
2809 for item in items {
2810 total = total + 1;
2811 }
2812 total
2813 }
2814
2815 pub fn count_map_keys(data) {
2816 let count = 0i64;
2817 for key in data.keys() {
2818 count = count + 1;
2819 }
2820 count
2821 }
2822
2823 pub fn for_in_list_works(items) {
2824 let exists = false;
2825 for item in items {
2826 exists = true;
2827 }
2828 exists
2829 }
2830
2831 pub fn for_in_map_values_works(data) {
2832 let exists = false;
2833 for value in data {
2834 exists = true;
2835 }
2836 exists
2837 }
2838 "#
2839 .to_vec(),
2840 )?;
2841
2842 let compiled = vm.get_fn("vm_for_in_collection::sum_list", &[Type::Any])?;
2843 assert_eq!(compiled.ret_ty(), &Type::I64);
2844 let sum_list: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2845 let items = Dynamic::list(vec![Dynamic::from(1i64), Dynamic::from(2i64), Dynamic::from(3i64)]);
2846 assert_eq!(sum_list(&items), 3);
2847
2848 let data = dynamic::map!("x"=> 1i64, "y"=> 2i64);
2849 let compiled = vm.get_fn("vm_for_in_collection::count_map_keys", &[Type::Any])?;
2850 assert_eq!(compiled.ret_ty(), &Type::I64);
2851 let count_map_keys: extern "C" fn(*const Dynamic) -> i64 = unsafe { std::mem::transmute(compiled.ptr()) };
2852 assert_eq!(count_map_keys(&data), 2);
2853
2854 let compiled = vm.get_fn("vm_for_in_collection::for_in_list_works", &[Type::Any])?;
2855 assert_eq!(compiled.ret_ty(), &Type::Bool);
2856 let for_in_list_works: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2857 let empty = Dynamic::list(Vec::new());
2858 assert!(!for_in_list_works(&empty));
2859 assert!(for_in_list_works(&items));
2860
2861 let compiled = vm.get_fn("vm_for_in_collection::for_in_map_values_works", &[Type::Any])?;
2862 assert_eq!(compiled.ret_ty(), &Type::Bool);
2863 let for_in_map_values_works: extern "C" fn(*const Dynamic) -> bool = unsafe { std::mem::transmute(compiled.ptr()) };
2864 let empty_map = dynamic::map!();
2865 assert!(!for_in_map_values_works(&empty_map));
2866 assert!(for_in_map_values_works(&data));
2867
2868 Ok(())
2869 }
2870
2871 #[test]
2872 fn concurrent_100_threads_no_memory_leak() -> anyhow::Result<()> {
2873 let vm = Vm::with_all()?;
2874 vm.import_code(
2875 "vm_stress",
2876 br#"
2877 pub fn heavy_alloc(idx: i64) {
2878 let items = [];
2879 let i = 0;
2880 while i < 50 {
2881 items.push({
2882 id: i + idx,
2883 name: "item-" + i,
2884 tags: ["tag-a", "tag-b", "tag-c"],
2885 meta: {
2886 created: 1234567890i64,
2887 score: (i * 3.14f64) as i64,
2888 extra: "prefix/" + i + "/" + idx
2889 }
2890 });
2891 i = i + 1;
2892 }
2893 items
2894 }
2895
2896 pub fn string_concat_stress() {
2897 let i = 0;
2898 let result = "";
2899 while i < 200 {
2900 result = result + "data-" + i + ",";
2901 i = i + 1;
2902 }
2903 result
2904 }
2905 "#
2906 .to_vec(),
2907 )?;
2908
2909 let (heavy_ptr, _) = vm.get_fn_ptr("vm_stress::heavy_alloc", &[Type::I64])?;
2910 let (concat_ptr, _) = vm.get_fn_ptr("vm_stress::string_concat_stress", &[])?;
2911
2912 let threads: usize = std::thread::available_parallelism()
2913 .map(|n| n.get())
2914 .unwrap_or(4)
2915 .max(100);
2916 let iters_per_thread = 200;
2917 let total_calls = threads * iters_per_thread * 2;
2918
2919 let before = current_rss_kb();
2920 eprintln!("threads={threads} iters_per_thread={iters_per_thread} total_calls={total_calls} rss_before={before}KB");
2921
2922 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
2924 let r1 = current_rss_kb();
2925 eprintln!("rss_after_round1={r1}KB");
2926
2927 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
2929 let r2 = current_rss_kb();
2930 eprintln!("rss_after_round2={r2}KB");
2931
2932 run_stress_round(threads, iters_per_thread, heavy_ptr as usize, concat_ptr as usize);
2934 let r3 = current_rss_kb();
2935 eprintln!("rss_after_round3={r3}KB");
2936
2937 let d12 = r2.saturating_sub(r1);
2939 let d23 = r3.saturating_sub(r2);
2940 eprintln!("delta_r1→r2={d12}KB delta_r2→r3={d23}KB");
2941
2942 let max_growth_kb = 20 * 1024;
2945 assert!(
2946 d12 < max_growth_kb && d23 < max_growth_kb,
2947 "memory keeps growing between rounds: round1={r1} round2={r2} round3={r3} delta12={d12}KB delta23={d23}KB (max allowed={max_growth_kb}KB)"
2948 );
2949
2950 Ok(())
2951 }
2952
2953 fn run_stress_round(threads: usize, iters: usize, heavy_ptr: usize, concat_ptr: usize) {
2954 std::thread::scope(|scope| {
2955 let mut handles = Vec::with_capacity(threads);
2956 for t in 0..threads {
2957 let heavy_ptr = heavy_ptr;
2958 let concat_ptr = concat_ptr;
2959 handles.push(scope.spawn(move || {
2960 let heavy_fn: extern "C" fn(i64) -> *const Dynamic =
2961 unsafe { std::mem::transmute(heavy_ptr as *const u8) };
2962 let concat_fn: extern "C" fn() -> *const Dynamic =
2963 unsafe { std::mem::transmute(concat_ptr as *const u8) };
2964 for i in 0..iters {
2965 let r_ptr = heavy_fn((t * iters + i) as i64);
2967 assert!(!r_ptr.is_null());
2968 unsafe {
2969 let r = &*r_ptr;
2970 assert!(r.len() > 0, "heavy_alloc returned empty list");
2971 drop(Box::from_raw(r_ptr as *mut Dynamic));
2972 }
2973
2974 let s_ptr = concat_fn();
2976 assert!(!s_ptr.is_null());
2977 unsafe {
2978 let s = &*s_ptr;
2979 assert!(s.len() > 0, "string_concat_stress returned empty");
2980 drop(Box::from_raw(s_ptr as *mut Dynamic));
2981 }
2982 }
2983 }));
2984 }
2985 for h in handles {
2986 h.join().unwrap();
2987 }
2988 });
2989 }
2990
2991 fn current_rss_kb() -> u64 {
2992 let pid = std::process::id();
2994 if let Ok(output) = std::process::Command::new("ps")
2995 .args(["-p", &pid.to_string(), "-o", "rss="])
2996 .output()
2997 {
2998 if let Ok(s) = String::from_utf8(output.stdout) {
2999 if let Some(kb) = s.trim().parse::<u64>().ok() {
3000 return kb;
3001 }
3002 }
3003 }
3004 if let Ok(statm) = std::fs::read_to_string("/proc/self/statm") {
3006 let parts: Vec<&str> = statm.split_whitespace().collect();
3007 if let Some(rss_pages) = parts.get(1).and_then(|s| s.parse::<u64>().ok()) {
3008 return rss_pages * 4; }
3010 }
3011 0
3012}
3013
3014}