1
use nms::interpreter::Interpreter;
2
use scripting::nomiscript::{Fraction, Value};
3
use scripting::parser::EntityData;
4
use scripting::{MemorySerializer, ScriptExecutor};
5
use scripting_format::{ContextType, EntityType, Operation};
6
use wasmtime::{Config, Engine, Module};
7

            
8
18
fn num(n: i64) -> Value {
9
18
    Value::Number(Fraction::new(n, 1))
10
18
}
11

            
12
15
fn gc_engine() -> Engine {
13
15
    let mut config = Config::new();
14
15
    config.wasm_gc(true);
15
15
    Engine::new(&config).unwrap()
16
15
}
17

            
18
2
fn wasm_export_names(wasm: &[u8], engine: &Engine) -> Vec<String> {
19
2
    let module = Module::new(engine, wasm).unwrap();
20
8
    module.exports().map(|e| e.name().to_string()).collect()
21
2
}
22

            
23
4
fn build_minimal_input(output_size: u32) -> Vec<u8> {
24
4
    let mut ser = MemorySerializer::new();
25
4
    ser.set_context(ContextType::BatchProcess, EntityType::Transaction);
26
4
    ser.finalize(output_size)
27
4
}
28

            
29
#[test]
30
1
fn test_compiled_wasm_has_standard_exports() {
31
1
    let mut interp = Interpreter::new(false).unwrap();
32
1
    let wasm = interp.compile_to_wasm("42").unwrap();
33
1
    let engine = gc_engine();
34
1
    let exports = wasm_export_names(&wasm, &engine);
35
1
    assert!(exports.contains(&"should_apply".to_string()));
36
1
    assert!(exports.contains(&"process".to_string()));
37
1
    assert!(exports.contains(&"memory".to_string()));
38
1
}
39

            
40
#[test]
41
1
fn test_groceries_wasm_has_standard_exports() {
42
1
    let wasm = include_bytes!("../../../web/static/wasm/groceries_markup.wasm");
43
1
    let exports = wasm_export_names(wasm, &Engine::default());
44
1
    assert!(exports.contains(&"should_apply".to_string()));
45
1
    assert!(exports.contains(&"process".to_string()));
46
1
    assert!(exports.contains(&"memory".to_string()));
47
1
}
48

            
49
#[test]
50
1
fn test_compile_and_load_roundtrip() {
51
1
    let mut interp = Interpreter::new(false).unwrap();
52
1
    let wasm = interp.compile_to_wasm("(+ 1 2 3)").unwrap();
53
1
    let result = interp.run_wasm(&wasm).unwrap();
54
1
    assert_eq!(result, Value::Number(Fraction::from_integer(6)));
55
1
}
56

            
57
#[test]
58
1
fn test_compile_and_load_roundtrip_string() {
59
1
    let mut interp = Interpreter::new(false).unwrap();
60
1
    let wasm = interp.compile_to_wasm(r#""hello""#).unwrap();
61
1
    let result = interp.run_wasm(&wasm).unwrap();
62
1
    assert_eq!(result, Value::String("hello".to_string()));
63
1
}
64

            
65
#[test]
66
1
fn test_compile_and_load_roundtrip_defun() {
67
1
    let mut interp = Interpreter::new(false).unwrap();
68
1
    let wasm = interp
69
1
        .compile_to_wasm("(defun add (a b) (+ a b)) (add 10 20)")
70
1
        .unwrap();
71
1
    let result = interp.run_wasm(&wasm).unwrap();
72
1
    assert_eq!(result, Value::Number(Fraction::from_integer(30)));
73
1
}
74

            
75
#[test]
76
1
fn test_nms_wasm_through_executor() {
77
1
    let mut interp = Interpreter::new(false).unwrap();
78
1
    let wasm = interp.compile_to_wasm("(+ 10 20)").unwrap();
79

            
80
1
    let input = build_minimal_input(4096);
81
1
    let executor = ScriptExecutor::with_engine(gc_engine());
82
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
83
1
    assert_eq!(
84
1
        entities.len(),
85
        1,
86
        "NMS WASM should produce one DebugValue entity"
87
    );
88
1
    assert_eq!(entities[0].entity_type, EntityType::DebugValue);
89
1
}
90

            
91
#[test]
92
1
fn test_groceries_wasm_through_executor_empty_input() {
93
1
    let wasm = include_bytes!("../../../web/static/wasm/groceries_markup.wasm");
94
1
    let input = build_minimal_input(4096);
95
1
    let executor = ScriptExecutor::new();
96
1
    let entities = executor
97
1
        .execute(wasm.as_slice(), &input, Some(4096))
98
1
        .unwrap();
99
1
    assert!(
100
1
        entities.is_empty(),
101
        "groceries WASM with empty input should produce no entities"
102
    );
103
1
}
104

            
105
#[test]
106
1
fn test_groceries_wasm_tags_splits() {
107
1
    let wasm = include_bytes!("../../../web/static/wasm/groceries_markup.wasm");
108

            
109
1
    let mut ser = MemorySerializer::new();
110
1
    ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
111

            
112
1
    let tx_id = [1u8; 16];
113
1
    let account1 = [2u8; 16];
114
1
    let account2 = [3u8; 16];
115
1
    let commodity = [4u8; 16];
116
1
    let split1_id = [5u8; 16];
117
1
    let split2_id = [6u8; 16];
118
1
    let tag_id = [7u8; 16];
119

            
120
1
    let tx_idx = ser.add_transaction(tx_id, -1, true, false, 0, 0, 2, 1, false);
121
1
    ser.set_primary(tx_idx);
122
1
    let split1_idx = ser.add_split(
123
1
        split1_id,
124
1
        tx_idx as i32,
125
        false,
126
        false,
127
1
        account1,
128
1
        commodity,
129
        -5000,
130
        100,
131
        0,
132
        0,
133
    );
134
1
    let split2_idx = ser.add_split(
135
1
        split2_id,
136
1
        tx_idx as i32,
137
        false,
138
        false,
139
1
        account2,
140
1
        commodity,
141
        5000,
142
        100,
143
        0,
144
        0,
145
    );
146
1
    ser.add_tag(tag_id, tx_idx as i32, false, false, "note", "groceries");
147

            
148
1
    let input = ser.finalize(4096);
149
1
    let executor = ScriptExecutor::new();
150
1
    let entities = executor
151
1
        .execute(wasm.as_slice(), &input, Some(4096))
152
1
        .unwrap();
153

            
154
1
    assert_eq!(
155
1
        entities.len(),
156
        2,
157
        "expected 2 category tags (one per split)"
158
    );
159
2
    for entity in &entities {
160
2
        assert_eq!(entity.entity_type, EntityType::Tag);
161
2
        if let EntityData::Tag { name, value } = &entity.data {
162
2
            assert_eq!(name, "category");
163
2
            assert_eq!(value, "groceries");
164
        } else {
165
            panic!("expected Tag data, got {:?}", entity.data);
166
        }
167
    }
168

            
169
1
    let parents: Vec<i32> = entities.iter().map(|e| e.parent_idx).collect();
170
1
    assert!(parents.contains(&(split1_idx as i32)));
171
1
    assert!(parents.contains(&(split2_idx as i32)));
172
1
}
173

            
174
#[test]
175
1
fn test_entity_count_through_executor() {
176
1
    let mut interp = Interpreter::new(false).unwrap();
177
1
    let wasm = interp.compile_to_wasm("(entity-count)").unwrap();
178

            
179
1
    let mut ser = MemorySerializer::new();
180
1
    ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
181
1
    let tx_id = [1u8; 16];
182
1
    let tx_idx = ser.add_transaction(tx_id, -1, true, false, 0, 0, 0, 0, false);
183
1
    ser.set_primary(tx_idx);
184
1
    ser.add_split(
185
1
        [2u8; 16],
186
1
        tx_idx as i32,
187
        false,
188
        false,
189
1
        [3u8; 16],
190
1
        [4u8; 16],
191
        100,
192
        1,
193
        0,
194
        0,
195
    );
196

            
197
1
    let input = ser.finalize(4096);
198
1
    let executor = ScriptExecutor::with_engine(gc_engine());
199
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
200

            
201
1
    assert_eq!(entities.len(), 1, "should produce one DebugValue entity");
202
1
    assert_eq!(entities[0].entity_type, EntityType::DebugValue);
203
1
}
204

            
205
#[test]
206
1
fn test_entity_count_conditional_through_executor() {
207
1
    let mut interp = Interpreter::new(false).unwrap();
208
1
    let script = r#"
209
1
        (if (= (entity-count) 4)
210
1
            "four-entities"
211
1
            "not-four")
212
1
    "#;
213
1
    let wasm = interp.compile_to_wasm(script).unwrap();
214

            
215
1
    let mut ser = MemorySerializer::new();
216
1
    ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
217
1
    let tx_id = [1u8; 16];
218
1
    let tx_idx = ser.add_transaction(tx_id, -1, true, false, 0, 0, 2, 1, false);
219
1
    ser.set_primary(tx_idx);
220
1
    ser.add_split(
221
1
        [5u8; 16],
222
1
        tx_idx as i32,
223
        false,
224
        false,
225
1
        [2u8; 16],
226
1
        [4u8; 16],
227
        -5000,
228
        100,
229
        0,
230
        0,
231
    );
232
1
    ser.add_split(
233
1
        [6u8; 16],
234
1
        tx_idx as i32,
235
        false,
236
        false,
237
1
        [3u8; 16],
238
1
        [4u8; 16],
239
        5000,
240
        100,
241
        0,
242
        0,
243
    );
244
1
    ser.add_tag([7u8; 16], tx_idx as i32, false, false, "note", "test");
245

            
246
1
    let input = ser.finalize(4096);
247
1
    let executor = ScriptExecutor::with_engine(gc_engine());
248
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
249

            
250
1
    assert_eq!(entities.len(), 1);
251
1
    assert_eq!(entities[0].entity_type, EntityType::DebugValue);
252
1
}
253

            
254
#[test]
255
1
fn test_runtime_do_loop_through_executor() {
256
1
    let mut interp = Interpreter::new(false).unwrap();
257
1
    let script = r"
258
1
        (let* ((n (entity-count))
259
1
               (sum 0))
260
1
            (do ((i 0 (+ i 1)))
261
1
                ((>= i n) sum)
262
1
                (setf sum (+ sum 1))))
263
1
    ";
264
1
    let wasm = interp.compile_to_wasm(script).unwrap();
265

            
266
1
    let mut ser = MemorySerializer::new();
267
1
    ser.set_context(ContextType::BatchProcess, EntityType::Transaction);
268
1
    let tx_id = [1u8; 16];
269
1
    let tx_idx = ser.add_transaction(tx_id, -1, true, false, 0, 0, 0, 0, false);
270
1
    ser.set_primary(tx_idx);
271
1
    ser.add_split(
272
1
        [2u8; 16],
273
1
        tx_idx as i32,
274
        false,
275
        false,
276
1
        [3u8; 16],
277
1
        [4u8; 16],
278
        100,
279
        1,
280
        0,
281
        0,
282
    );
283
1
    ser.add_split(
284
1
        [5u8; 16],
285
1
        tx_idx as i32,
286
        false,
287
        false,
288
1
        [6u8; 16],
289
1
        [4u8; 16],
290
        -100,
291
        1,
292
        0,
293
        0,
294
    );
295

            
296
1
    let input = ser.finalize(4096);
297
1
    let executor = ScriptExecutor::with_engine(gc_engine());
298
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
299

            
300
1
    assert_eq!(entities.len(), 1);
301
1
    assert_eq!(entities[0].entity_type, EntityType::DebugValue);
302
1
}
303

            
304
#[test]
305
1
fn test_primary_entity_idx_through_executor() {
306
1
    let mut interp = Interpreter::new(false).unwrap();
307
1
    let wasm = interp.compile_to_wasm("(primary-entity-idx)").unwrap();
308

            
309
1
    let mut ser = MemorySerializer::new();
310
1
    ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
311
1
    let tx_id = [1u8; 16];
312
1
    let tx_idx = ser.add_transaction(tx_id, -1, true, false, 0, 0, 0, 0, false);
313
1
    ser.set_primary(tx_idx);
314

            
315
1
    let input = ser.finalize(4096);
316
1
    let executor = ScriptExecutor::with_engine(gc_engine());
317
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
318

            
319
1
    assert_eq!(entities.len(), 1);
320
1
    assert_eq!(entities[0].entity_type, EntityType::DebugValue);
321
1
}
322

            
323
25
fn build_tx_with_splits() -> (MemorySerializer, u32, u32, u32) {
324
25
    let mut ser = MemorySerializer::new();
325
25
    ser.set_context(ContextType::EntityCreate, EntityType::Transaction);
326
25
    let tx_idx = ser.add_transaction([1u8; 16], -1, true, false, 1000, 2000, 2, 1, false);
327
25
    ser.set_primary(tx_idx);
328
25
    let s0 = ser.add_split(
329
25
        [2u8; 16],
330
25
        tx_idx as i32,
331
        false,
332
        false,
333
25
        [10u8; 16],
334
25
        [20u8; 16],
335
        -5000,
336
        100,
337
        0,
338
        0,
339
    );
340
25
    let s1 = ser.add_split(
341
25
        [3u8; 16],
342
25
        tx_idx as i32,
343
        false,
344
        false,
345
25
        [11u8; 16],
346
25
        [20u8; 16],
347
        5000,
348
        100,
349
        0,
350
        0,
351
    );
352
25
    ser.add_tag([4u8; 16], tx_idx as i32, false, false, "note", "test");
353
25
    (ser, tx_idx, s0, s1)
354
25
}
355

            
356
#[test]
357
1
fn test_transaction_split_count_accessor() {
358
1
    let mut interp = Interpreter::new(false).unwrap();
359
1
    let wasm = interp
360
1
        .compile_to_wasm(r"(= (transaction-split-count 0) 2)")
361
1
        .unwrap();
362

            
363
1
    let (ser, ..) = build_tx_with_splits();
364
1
    let input = ser.finalize(4096);
365
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
366
1
    assert_eq!(result, num(1));
367
1
}
368

            
369
#[test]
370
1
fn test_transaction_tag_count_accessor() {
371
1
    let mut interp = Interpreter::new(false).unwrap();
372
1
    let wasm = interp
373
1
        .compile_to_wasm(r"(= (transaction-tag-count 0) 1)")
374
1
        .unwrap();
375

            
376
1
    let (ser, ..) = build_tx_with_splits();
377
1
    let input = ser.finalize(4096);
378
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
379
1
    assert_eq!(result, num(1));
380
1
}
381

            
382
#[test]
383
1
fn test_transaction_is_multi_currency_accessor() {
384
1
    let mut interp = Interpreter::new(false).unwrap();
385
1
    let wasm = interp
386
1
        .compile_to_wasm(r"(= (transaction-is-multi-currency 0) 0)")
387
1
        .unwrap();
388

            
389
1
    let (ser, ..) = build_tx_with_splits();
390
1
    let input = ser.finalize(4096);
391
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
392
1
    assert_eq!(result, num(1));
393
1
}
394

            
395
#[test]
396
1
fn test_transaction_post_date_accessor() {
397
1
    let mut interp = Interpreter::new(false).unwrap();
398
1
    let wasm = interp
399
1
        .compile_to_wasm(r"(= (transaction-post-date 0) 1000)")
400
1
        .unwrap();
401

            
402
1
    let (ser, ..) = build_tx_with_splits();
403
1
    let input = ser.finalize(4096);
404
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
405
1
    assert_eq!(result, num(1));
406
1
}
407

            
408
#[test]
409
1
fn test_split_value_accessor() {
410
1
    let mut interp = Interpreter::new(false).unwrap();
411
    // Split at index 1 has value_num=-5000, value_denom=100 → ratio -5000/100 = -50/1
412
1
    let wasm = interp.compile_to_wasm(r"(= (split-value 1) -50)").unwrap();
413

            
414
1
    let (ser, ..) = build_tx_with_splits();
415
1
    let input = ser.finalize(4096);
416
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
417
1
    assert_eq!(result, num(1));
418
1
}
419

            
420
#[test]
421
1
fn test_split_value_num_accessor() {
422
1
    let mut interp = Interpreter::new(false).unwrap();
423
1
    let wasm = interp
424
1
        .compile_to_wasm(r"(= (split-value-num 1) -5000)")
425
1
        .unwrap();
426

            
427
1
    let (ser, ..) = build_tx_with_splits();
428
1
    let input = ser.finalize(4096);
429
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
430
1
    assert_eq!(result, num(1));
431
1
}
432

            
433
#[test]
434
1
fn test_split_value_denom_accessor() {
435
1
    let mut interp = Interpreter::new(false).unwrap();
436
1
    let wasm = interp
437
1
        .compile_to_wasm(r"(= (split-value-denom 1) 100)")
438
1
        .unwrap();
439

            
440
1
    let (ser, ..) = build_tx_with_splits();
441
1
    let input = ser.finalize(4096);
442
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
443
1
    assert_eq!(result, num(1));
444
1
}
445

            
446
#[test]
447
1
fn test_entity_type_with_constants() {
448
1
    let mut interp = Interpreter::new(false).unwrap();
449
1
    let wasm = interp
450
1
        .compile_to_wasm(
451
1
            r"(and (= (entity-type 0) +entity-transaction+)
452
1
                    (= (entity-type 1) +entity-split+))",
453
        )
454
1
        .unwrap();
455

            
456
1
    let (ser, ..) = build_tx_with_splits();
457
1
    let input = ser.finalize(4096);
458
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
459
1
    assert_eq!(result, num(1));
460
1
}
461

            
462
#[test]
463
1
fn test_entity_parent_idx_accessor() {
464
1
    let mut interp = Interpreter::new(false).unwrap();
465
    // Split at index 1 should have parent_idx = 0 (the transaction)
466
1
    let wasm = interp
467
1
        .compile_to_wasm(r"(= (entity-parent-idx 1) 0)")
468
1
        .unwrap();
469

            
470
1
    let (ser, ..) = build_tx_with_splits();
471
1
    let input = ser.finalize(4096);
472
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
473
1
    assert_eq!(result, num(1));
474
1
}
475

            
476
#[test]
477
1
fn test_transaction_enter_date_accessor() {
478
1
    let mut interp = Interpreter::new(false).unwrap();
479
1
    let wasm = interp
480
1
        .compile_to_wasm(r"(= (transaction-enter-date 0) 2000)")
481
1
        .unwrap();
482

            
483
1
    let (ser, ..) = build_tx_with_splits();
484
1
    let input = ser.finalize(4096);
485
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
486
1
    assert_eq!(result, num(1));
487
1
}
488

            
489
#[test]
490
1
fn test_split_reconcile_state_accessor() {
491
1
    let mut interp = Interpreter::new(false).unwrap();
492
1
    let wasm = interp
493
1
        .compile_to_wasm(r"(= (split-reconcile-state 1) 0)")
494
1
        .unwrap();
495

            
496
1
    let (ser, ..) = build_tx_with_splits();
497
1
    let input = ser.finalize(4096);
498
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
499
1
    assert_eq!(result, num(1));
500
1
}
501

            
502
#[test]
503
1
fn test_split_reconcile_date_accessor() {
504
1
    let mut interp = Interpreter::new(false).unwrap();
505
1
    let wasm = interp
506
1
        .compile_to_wasm(r"(= (split-reconcile-date 1) 0)")
507
1
        .unwrap();
508

            
509
1
    let (ser, ..) = build_tx_with_splits();
510
1
    let input = ser.finalize(4096);
511
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
512
1
    assert_eq!(result, num(1));
513
1
}
514

            
515
#[test]
516
1
fn test_runtime_cons_car() {
517
1
    let mut interp = Interpreter::new(false).unwrap();
518
    // Build a runtime cons list from entity indices and get the first element
519
1
    let wasm = interp
520
1
        .compile_to_wasm(
521
1
            r"(let ((idx (primary-entity-idx)))
522
1
                   (= (car (cons idx nil)) 0))",
523
        )
524
1
        .unwrap();
525

            
526
1
    let (ser, ..) = build_tx_with_splits();
527
1
    let input = ser.finalize(4096);
528
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
529
1
    assert_eq!(result, num(1));
530
1
}
531

            
532
#[test]
533
1
fn test_runtime_cons_null_cdr() {
534
1
    let mut interp = Interpreter::new(false).unwrap();
535
    // CDR of a single-element cons list should be null
536
1
    let wasm = interp
537
1
        .compile_to_wasm(
538
1
            r"(let ((idx (primary-entity-idx)))
539
1
                   (null? (cdr (cons idx nil))))",
540
        )
541
1
        .unwrap();
542

            
543
1
    let (ser, ..) = build_tx_with_splits();
544
1
    let input = ser.finalize(4096);
545
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
546
1
    assert_eq!(result, num(1));
547
1
}
548

            
549
#[test]
550
1
fn test_runtime_do_loop_cons_list() {
551
1
    let mut interp = Interpreter::new(false).unwrap();
552
    // Build a list of split entity indices by filtering entity-type = split(1)
553
    // Entities: 0=tx, 1=split, 2=split, 3=tag → splits at indices 1, 2
554
1
    let wasm = interp
555
1
        .compile_to_wasm(
556
1
            r"(do ((i 0 (+ i 1))
557
1
                     (result nil (if (= (entity-type i) +entity-split+)
558
1
                                     (cons i result)
559
1
                                     result)))
560
1
                    ((>= i (entity-count)) (car result)))",
561
        )
562
1
        .unwrap();
563

            
564
1
    let (ser, ..) = build_tx_with_splits();
565
1
    let input = ser.finalize(4096);
566
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
567
    // Last split added = index 2, so car(result) = 2
568
1
    assert_eq!(result, Value::Number(Fraction::new(2, 1)));
569
1
}
570

            
571
#[test]
572
1
fn test_create_tag_output() {
573
1
    let mut interp = Interpreter::new(false).unwrap();
574
1
    let wasm = interp
575
1
        .compile_to_wasm(r#"(create-tag 0 "note" "test-value")"#)
576
1
        .unwrap();
577

            
578
1
    let (ser, tx_idx, ..) = build_tx_with_splits();
579
1
    let input = ser.finalize(4096);
580
1
    let executor = ScriptExecutor::with_engine(gc_engine());
581
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
582

            
583
1
    assert_eq!(entities.len(), 1, "expected one tag entity");
584
1
    assert_eq!(entities[0].entity_type, EntityType::Tag);
585
1
    assert_eq!(entities[0].parent_idx, tx_idx as i32);
586
1
    if let EntityData::Tag { name, value } = &entities[0].data {
587
1
        assert_eq!(name, "note");
588
1
        assert_eq!(value, "test-value");
589
    } else {
590
        panic!("expected Tag data, got {:?}", entities[0].data);
591
    }
592
1
}
593

            
594
#[test]
595
1
fn test_create_tag_with_runtime_parent() {
596
1
    let mut interp = Interpreter::new(false).unwrap();
597
1
    let wasm = interp
598
1
        .compile_to_wasm(r#"(create-tag (primary-entity-idx) "category" "food")"#)
599
1
        .unwrap();
600

            
601
1
    let (ser, tx_idx, ..) = build_tx_with_splits();
602
1
    let input = ser.finalize(4096);
603
1
    let executor = ScriptExecutor::with_engine(gc_engine());
604
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
605

            
606
1
    assert_eq!(entities.len(), 1, "expected one tag entity");
607
1
    assert_eq!(entities[0].entity_type, EntityType::Tag);
608
1
    assert_eq!(entities[0].parent_idx, tx_idx as i32);
609
1
    if let EntityData::Tag { name, value } = &entities[0].data {
610
1
        assert_eq!(name, "category");
611
1
        assert_eq!(value, "food");
612
    } else {
613
        panic!("expected Tag data, got {:?}", entities[0].data);
614
    }
615
1
}
616

            
617
#[test]
618
1
fn test_create_tag_as_side_effect() {
619
1
    let mut interp = Interpreter::new(false).unwrap();
620
    // create-tag used as intermediate expression, not last
621
1
    let wasm = interp
622
1
        .compile_to_wasm(
623
1
            r#"(let ((idx (primary-entity-idx)))
624
1
                   (create-tag idx "side" "effect")
625
1
                   42)"#,
626
        )
627
1
        .unwrap();
628

            
629
1
    let (ser, tx_idx, ..) = build_tx_with_splits();
630
1
    let input = ser.finalize(4096);
631
1
    let executor = ScriptExecutor::with_engine(gc_engine());
632
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
633

            
634
    // Should have both the tag and the debug value (42)
635
1
    assert_eq!(entities.len(), 2, "expected tag + debug value");
636
1
    let tag = entities.iter().find(|e| e.entity_type == EntityType::Tag);
637
1
    assert!(tag.is_some(), "expected a tag entity in output");
638
1
    let tag = tag.unwrap();
639
1
    assert_eq!(tag.parent_idx, tx_idx as i32);
640
1
    if let EntityData::Tag { name, value } = &tag.data {
641
1
        assert_eq!(name, "side");
642
1
        assert_eq!(value, "effect");
643
    } else {
644
        panic!("expected Tag data");
645
    }
646
1
}
647

            
648
#[test]
649
1
fn test_runtime_dolist_over_cons_list() {
650
1
    let mut interp = Interpreter::new(false).unwrap();
651
    // Build a cons list of split indices via DO, then iterate with DOLIST counting elements
652
1
    let wasm = interp
653
1
        .compile_to_wasm(
654
1
            r"(let* ((splits (do ((i 0 (+ i 1))
655
1
                                  (result nil (if (= (entity-type i) +entity-split+)
656
1
                                                  (cons i result)
657
1
                                                  result)))
658
1
                                 ((>= i (entity-count)) result)))
659
1
                    (count 0))
660
1
                   (dolist (s splits)
661
1
                       (setf count (+ count 1)))
662
1
                   count)",
663
        )
664
1
        .unwrap();
665

            
666
1
    let (ser, ..) = build_tx_with_splits();
667
1
    let input = ser.finalize(4096);
668
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
669
    // 2 splits in the input (indices 1, 2)
670
1
    assert_eq!(result, num(2));
671
1
}
672

            
673
#[test]
674
1
fn test_delete_entity_output() {
675
1
    let mut interp = Interpreter::new(false).unwrap();
676
1
    let wasm = interp.compile_to_wasm(r"(delete-entity 0)").unwrap();
677

            
678
1
    let (ser, _tx_idx, ..) = build_tx_with_splits();
679
1
    let input = ser.finalize(4096);
680
1
    let executor = ScriptExecutor::with_engine(gc_engine());
681
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
682

            
683
1
    assert_eq!(entities.len(), 1, "expected one delete entity");
684
1
    assert_eq!(entities[0].entity_type, EntityType::Transaction);
685
1
    assert_eq!(entities[0].operation, Operation::Delete);
686
1
    assert_eq!(entities[0].id, [1u8; 16]);
687
1
    assert_eq!(entities[0].parent_idx, -1);
688
1
}
689

            
690
#[test]
691
1
fn test_delete_entity_with_runtime_idx() {
692
1
    let mut interp = Interpreter::new(false).unwrap();
693
1
    let wasm = interp
694
1
        .compile_to_wasm(r"(delete-entity (primary-entity-idx))")
695
1
        .unwrap();
696

            
697
1
    let (ser, ..) = build_tx_with_splits();
698
1
    let input = ser.finalize(4096);
699
1
    let executor = ScriptExecutor::with_engine(gc_engine());
700
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
701

            
702
1
    assert_eq!(entities.len(), 1);
703
1
    assert_eq!(entities[0].entity_type, EntityType::Transaction);
704
1
    assert_eq!(entities[0].operation, Operation::Delete);
705
1
    assert_eq!(entities[0].id, [1u8; 16]);
706
1
}
707

            
708
#[test]
709
1
fn test_delete_entity_split() {
710
1
    let mut interp = Interpreter::new(false).unwrap();
711
1
    let wasm = interp.compile_to_wasm(r"(delete-entity 1)").unwrap();
712

            
713
1
    let (ser, ..) = build_tx_with_splits();
714
1
    let input = ser.finalize(4096);
715
1
    let executor = ScriptExecutor::with_engine(gc_engine());
716
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
717

            
718
1
    assert_eq!(entities.len(), 1);
719
1
    assert_eq!(entities[0].entity_type, EntityType::Split);
720
1
    assert_eq!(entities[0].operation, Operation::Delete);
721
1
    assert_eq!(entities[0].id, [2u8; 16]);
722
1
}
723

            
724
#[test]
725
1
fn test_should_apply_default() {
726
1
    let mut interp = Interpreter::new(false).unwrap();
727
1
    let wasm = interp.compile_to_wasm(r"42").unwrap();
728
1
    let module = Module::new(&gc_engine(), &wasm).unwrap();
729

            
730
3
    let exports: Vec<_> = module.exports().map(|e| e.name().to_string()).collect();
731
1
    assert!(exports.contains(&"should_apply".to_string()));
732
1
    assert!(exports.contains(&"process".to_string()));
733
1
}
734

            
735
#[test]
736
1
fn test_should_apply_custom() {
737
1
    let mut interp = Interpreter::new(false).unwrap();
738
1
    let wasm = interp
739
1
        .compile_to_wasm(
740
1
            r#"(defun should-apply ()
741
1
                   (= (primary-entity-type) +entity-transaction+))
742
1
               (create-tag 0 "test" "value")"#,
743
        )
744
1
        .unwrap();
745

            
746
1
    let (ser, ..) = build_tx_with_splits();
747
1
    let input = ser.finalize(4096);
748
1
    let executor = ScriptExecutor::with_engine(gc_engine());
749
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
750
1
    assert!(
751
1
        !entities.is_empty(),
752
        "should_apply should return true for transactions"
753
    );
754
1
}
755

            
756
#[test]
757
1
fn test_should_apply_rejects() {
758
1
    let mut interp = Interpreter::new(false).unwrap();
759
1
    let wasm = interp
760
1
        .compile_to_wasm(
761
1
            r"(defun should-apply ()
762
1
                   (= (primary-entity-type) +entity-account+))
763
1
               42",
764
        )
765
1
        .unwrap();
766

            
767
1
    let (ser, ..) = build_tx_with_splits();
768
1
    let input = ser.finalize(4096);
769
1
    let executor = ScriptExecutor::with_engine(gc_engine());
770
1
    let entities = executor.execute(&wasm, &input, Some(4096)).unwrap();
771
1
    assert!(
772
1
        entities.is_empty(),
773
        "should_apply should return false for non-accounts"
774
    );
775
1
}
776

            
777
#[test]
778
1
fn test_get_input_entities() {
779
1
    let mut interp = Interpreter::new(false).unwrap();
780
    // Count entities by iterating the cons list from get-input-entities
781
1
    let wasm = interp
782
1
        .compile_to_wasm(
783
1
            r"(let ((count 0))
784
1
                   (dolist (e (get-input-entities))
785
1
                       (setf count (+ count 1)))
786
1
                   count)",
787
        )
788
1
        .unwrap();
789

            
790
1
    let (ser, ..) = build_tx_with_splits();
791
1
    let input = ser.finalize(4096);
792
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
793
    // 4 entities: 1 transaction + 2 splits + 1 tag
794
1
    assert_eq!(result, num(4));
795
1
}
796

            
797
// Large constant-init DO loop (> MAX_STATIC_LOOP_ITERS=64): must fall back to runtime WASM
798
// path rather than attempting compile-time unrolling that would hang the compiler.
799

            
800
#[test]
801
1
fn test_do_large_constant_loop_compiles_and_runs() {
802
1
    let mut interp = Interpreter::new(false).unwrap();
803
1
    let wasm = interp
804
1
        .compile_to_wasm("(do ((i 0 (+ i 1)) (sum 0 (+ sum i))) ((= i 100) sum))")
805
1
        .unwrap();
806
1
    let input = build_minimal_input(4096);
807
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
808
1
    assert_eq!(result, num(4950));
809
1
}
810

            
811
#[test]
812
1
fn test_do_star_large_constant_loop_compiles_and_runs() {
813
1
    let mut interp = Interpreter::new(false).unwrap();
814
    // DO* steps sequentially: acc's step sees the already-incremented i, so sum = 1+2+...+100
815
1
    let wasm = interp
816
1
        .compile_to_wasm("(do* ((i 0 (+ i 1)) (acc 0 (+ acc i))) ((= i 100) acc))")
817
1
        .unwrap();
818
1
    let input = build_minimal_input(4096);
819
1
    let result = interp.run_wasm_with_input(&wasm, &input).unwrap();
820
1
    assert_eq!(result, num(5050));
821
1
}