tsrun 0.1.23

A TypeScript interpreter designed for embedding in applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
//! Tests for the bytecode compiler
//!
//! These tests verify that the compiler correctly generates bytecode
//! from AST nodes.

use tsrun::compiler::{BytecodeChunk, Compiler, Op};
use tsrun::parser::Parser;
use tsrun::string_dict::StringDict;

/// Parse source and compile to bytecode
#[allow(clippy::expect_used)]
fn compile(source: &str) -> BytecodeChunk {
    let mut dict = StringDict::new();
    let mut parser = Parser::new(source, &mut dict);
    let program = parser.parse_program().expect("parse failed");
    let chunk = Compiler::compile_program(&program).expect("compile failed");
    (*chunk).clone()
}

/// Helper to check if bytecode contains a specific opcode type
fn contains_op<F: Fn(&Op) -> bool>(chunk: &BytecodeChunk, predicate: F) -> bool {
    chunk.code.iter().any(predicate)
}

#[test]
fn test_compile_number_literal() {
    let chunk = compile("42");

    // Should have LoadInt or LoadConst for 42
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::LoadInt { value: 42, .. })),
        "Expected LoadInt for 42, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_string_literal() {
    let chunk = compile("'hello'");

    // Should have LoadConst for the string
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::LoadConst { .. })),
        "Expected LoadConst for string, got {:?}",
        chunk.code
    );

    // Should have the string in constants
    assert!(
        chunk.constants.iter().any(|c| {
            matches!(c, tsrun::compiler::Constant::String(s) if s.as_ref() == "hello")
        }),
        "Expected 'hello' in constants"
    );
}

#[test]
fn test_compile_boolean_literal() {
    let chunk = compile("true");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::LoadBool { value: true, .. })),
        "Expected LoadBool true, got {:?}",
        chunk.code
    );

    let chunk = compile("false");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::LoadBool { value: false, .. })),
        "Expected LoadBool false, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_null_undefined() {
    let chunk = compile("null");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::LoadNull { .. })),
        "Expected LoadNull, got {:?}",
        chunk.code
    );

    // Note: `undefined` is parsed as an identifier, not a literal
    // so it generates GetVar instead of LoadUndefined
    // The LoadUndefined opcode is used for void expressions and uninitialized variables
    let chunk = compile("void 0");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Void { .. })),
        "Expected Void for void 0, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_binary_add() {
    let chunk = compile("1 + 2");

    // Should have Add opcode
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Add { .. })),
        "Expected Add, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_binary_operators() {
    // Test various binary operators
    let ops = [
        ("1 - 2", "Sub"),
        ("1 * 2", "Mul"),
        ("1 / 2", "Div"),
        ("1 % 2", "Mod"),
        ("1 ** 2", "Exp"),
        ("1 === 2", "StrictEq"),
        ("1 !== 2", "StrictNotEq"),
        ("1 == 2", "Eq"),
        ("1 != 2", "NotEq"),
        ("1 < 2", "Lt"),
        ("1 <= 2", "LtEq"),
        ("1 > 2", "Gt"),
        ("1 >= 2", "GtEq"),
        ("1 & 2", "BitAnd"),
        ("1 | 2", "BitOr"),
        ("1 ^ 2", "BitXor"),
        ("1 << 2", "LShift"),
        ("1 >> 2", "RShift"),
        ("1 >>> 2", "URShift"),
    ];

    for (source, expected_op) in ops {
        let chunk = compile(source);
        let has_op = chunk.code.iter().any(|op| {
            let op_name = format!("{:?}", op);
            op_name.starts_with(expected_op)
        });
        assert!(
            has_op,
            "Expected {} for '{}', got {:?}",
            expected_op, source, chunk.code
        );
    }
}

#[test]
fn test_compile_unary_operators() {
    let chunk = compile("-x");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Neg { .. })),
        "Expected Neg, got {:?}",
        chunk.code
    );

    let chunk = compile("!x");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Not { .. })),
        "Expected Not, got {:?}",
        chunk.code
    );

    let chunk = compile("~x");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::BitNot { .. })),
        "Expected BitNot, got {:?}",
        chunk.code
    );

    let chunk = compile("typeof x");
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Typeof { .. })),
        "Expected Typeof, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_variable_declaration() {
    let chunk = compile("let x = 42;");

    // Should have DeclareVar
    assert!(
        contains_op(&chunk, |op| matches!(
            op,
            Op::DeclareVar { mutable: true, .. }
        )),
        "Expected DeclareVar with mutable=true, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_const_declaration() {
    let chunk = compile("const x = 42;");

    // Should have DeclareVar with mutable=false
    assert!(
        contains_op(&chunk, |op| matches!(
            op,
            Op::DeclareVar { mutable: false, .. }
        )),
        "Expected DeclareVar with mutable=false, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_var_declaration() {
    let chunk = compile("var x = 42;");

    // Should have DeclareVarHoisted
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::DeclareVarHoisted { .. })),
        "Expected DeclareVarHoisted, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_variable_read() {
    let chunk = compile("x");

    // Should have GetVar
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::GetVar { .. })),
        "Expected GetVar, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_variable_assignment() {
    let chunk = compile("x = 42");

    // Should have SetVar
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::SetVar { .. })),
        "Expected SetVar, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_if_statement() {
    let chunk = compile("if (true) { x }");

    // Should have JumpIfFalse for the condition
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfFalse { .. })),
        "Expected JumpIfFalse, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_if_else() {
    let chunk = compile("if (true) { x } else { y }");

    // Should have JumpIfFalse and unconditional Jump
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfFalse { .. })),
        "Expected JumpIfFalse, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Jump { .. })),
        "Expected Jump, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_while_loop() {
    let chunk = compile("while (true) { x }");

    // Should have JumpIfFalse and Jump back
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfFalse { .. })),
        "Expected JumpIfFalse, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Jump { .. })),
        "Expected Jump, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_object_literal() {
    let chunk = compile("({ a: 1 })");

    // Should have CreateObject and SetPropertyConst
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::CreateObject { .. })),
        "Expected CreateObject, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::SetPropertyConst { .. })),
        "Expected SetPropertyConst, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_array_literal() {
    let chunk = compile("[1, 2, 3]");

    // Should have CreateArray
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::CreateArray { .. })),
        "Expected CreateArray, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_member_access() {
    let chunk = compile("obj.prop");

    // Should have GetPropertyConst for static property access
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::GetPropertyConst { .. })),
        "Expected GetPropertyConst, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_computed_member_access() {
    let chunk = compile("obj[key]");

    // Should have GetProperty for computed access
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::GetProperty { .. })),
        "Expected GetProperty, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_function_call() {
    let chunk = compile("foo()");

    // Should have Call
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Call { .. })),
        "Expected Call, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_method_call() {
    let chunk = compile("obj.method()");

    // Method calls now use GetPropertyConst + Call pattern for correct evaluation order
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::GetPropertyConst { .. })),
        "Expected GetPropertyConst, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Call { .. })),
        "Expected Call, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_new_expression() {
    let chunk = compile("new Foo()");

    // Should have Construct
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Construct { .. })),
        "Expected Construct, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_return() {
    // Can't have bare return, need to wrap in function context
    // For now just test that Return opcode exists in our enum
    let chunk = compile("42");
    // This is a placeholder - actual return compilation needs function context
    assert!(!chunk.code.is_empty());
}

#[test]
fn test_compile_throw() {
    let chunk = compile("throw new Error()");

    // Should have Throw
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Throw { .. })),
        "Expected Throw, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_try_catch() {
    let chunk = compile("try { x } catch (e) { y }");

    // Should have PushTry and PopTry
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::PushTry { .. })),
        "Expected PushTry, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::PopTry)),
        "Expected PopTry, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_logical_and() {
    let chunk = compile("a && b");

    // Should have JumpIfFalse for short-circuit
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfFalse { .. })),
        "Expected JumpIfFalse for &&, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_logical_or() {
    let chunk = compile("a || b");

    // Should have JumpIfTrue for short-circuit
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfTrue { .. })),
        "Expected JumpIfTrue for ||, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_conditional_expression() {
    let chunk = compile("a ? b : c");

    // Should have JumpIfFalse and Jump
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfFalse { .. })),
        "Expected JumpIfFalse for ternary, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Jump { .. })),
        "Expected Jump for ternary, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_update_increment() {
    let chunk = compile("x++");

    // Should have Add for increment
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Add { .. })),
        "Expected Add for ++, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_update_decrement() {
    let chunk = compile("x--");

    // Should have Sub for decrement
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::Sub { .. })),
        "Expected Sub for --, got {:?}",
        chunk.code
    );
}

#[test]
fn test_compile_halt_at_end() {
    let chunk = compile("42");

    // Every program should end with Halt
    assert!(
        matches!(chunk.code.last(), Some(Op::Halt)),
        "Expected Halt at end, got {:?}",
        chunk.code.last()
    );
}

#[test]
fn test_compile_scope_push_pop() {
    let chunk = compile("{ let x = 1 }");

    // Block should have PushScope and PopScope
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::PushScope)),
        "Expected PushScope, got {:?}",
        chunk.code
    );
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::PopScope)),
        "Expected PopScope, got {:?}",
        chunk.code
    );
}

#[test]
fn test_source_map() {
    let chunk = compile("1 + 2");

    // Should have source map entries
    assert!(!chunk.source_map.is_empty(), "Expected source map entries");
}

#[test]
fn test_register_count() {
    let chunk = compile("1 + 2 + 3 + 4 + 5");

    // Should have a reasonable register count (not 0)
    assert!(
        chunk.register_count > 0,
        "Expected positive register count, got {}",
        chunk.register_count
    );
}

#[test]
fn test_compile_nullish_coalescing() {
    let chunk = compile("null ?? 'default'");

    // Print bytecode for debugging
    println!("Bytecode for: null ?? 'default'");
    println!("Register count: {}", chunk.register_count);
    for (i, op) in chunk.code.iter().enumerate() {
        println!("  {}: {:?}", i, op);
    }

    // Should have JumpIfNotNullish for nullish coalescing
    assert!(
        contains_op(&chunk, |op| matches!(op, Op::JumpIfNotNullish { .. })),
        "Expected JumpIfNotNullish for ??, got {:?}",
        chunk.code
    );

    // Verify the jump target is correct - it should jump past the 'default' load
    let has_valid_jump = chunk.code.iter().any(|op| {
        if let Op::JumpIfNotNullish { target, .. } = op {
            // The target should be a valid instruction index
            (*target as usize) < chunk.code.len()
        } else {
            false
        }
    });
    assert!(has_valid_jump, "JumpIfNotNullish has invalid target");
}

#[test]
fn test_register_usage_array_of_objects() {
    // Array literal with multiple object elements - each object needs temp registers
    let chunk = compile(
        r#"
        const arr = [
            { id: 1, name: "a" },
            { id: 2, name: "b" },
            { id: 3, name: "c" },
            { id: 4, name: "d" },
            { id: 5, name: "e" }
        ];
    "#,
    );

    // Register count should be reasonable, not exploding
    println!(
        "Array of 5 objects register count: {}",
        chunk.register_count
    );
    assert!(
        chunk.register_count < 50,
        "Register count {} is unexpectedly high for array of 5 objects",
        chunk.register_count
    );
}

#[test]
fn test_register_usage_large_array_of_objects() {
    // Larger array to check if registers accumulate
    let chunk = compile(
        r#"
        const arr = [
            { id: 1, name: "a" }, { id: 2, name: "b" }, { id: 3, name: "c" },
            { id: 4, name: "d" }, { id: 5, name: "e" }, { id: 6, name: "f" },
            { id: 7, name: "g" }, { id: 8, name: "h" }, { id: 9, name: "i" },
            { id: 10, name: "j" }, { id: 11, name: "k" }, { id: 12, name: "l" },
            { id: 13, name: "m" }, { id: 14, name: "n" }, { id: 15, name: "o" },
            { id: 16, name: "p" }, { id: 17, name: "q" }, { id: 18, name: "r" },
            { id: 19, name: "s" }, { id: 20, name: "t" }
        ];
    "#,
    );

    println!(
        "Array of 20 objects register count: {}",
        chunk.register_count
    );
    // If registers are freed properly, count should still be low
    // If there's a leak, this would approach 20*3 = 60+ registers
    assert!(
        chunk.register_count < 50,
        "Register count {} suggests register leak in array compilation",
        chunk.register_count
    );
}

#[test]
fn test_register_reuse_in_array_elements() {
    // Registers for each array element should be reused after pushing to array
    // With proper reuse, a 100-element array should NOT need 100+ registers
    let mut elements = String::new();
    for i in 0..100 {
        if i > 0 {
            elements.push_str(", ");
        }
        elements.push_str(&format!("{{ id: {}, name: \"item{}\" }}", i, i));
    }
    let code = format!("const arr = [{}];", elements);

    let result = std::panic::catch_unwind(|| compile(&code));
    match result {
        Ok(chunk) => {
            println!(
                "Array of 100 objects register count: {}",
                chunk.register_count
            );
            // With proper register reuse, we should need far fewer than 100 registers
            assert!(
                chunk.register_count < 50,
                "Register count {} for 100-element array suggests registers aren't being reused",
                chunk.register_count
            );
        }
        Err(_) => {
            panic!("Compilation failed - likely hit 255 register limit, confirming register leak");
        }
    }
}

#[test]
fn test_register_usage_many_statements() {
    // Test many statements at module level (similar to collections/main.ts pattern)
    let chunk = compile(
        r#"
        const map = new Map();
        map.set("one", 1);
        map.set("two", 2);
        map.set("three", 3);
        console.log("Map size:", map.size);
        console.log("get('two'):", map.get("two"));
        console.log("has('three'):", map.has("three"));
        console.log("has('four'):", map.has("four"));
        const fruitPrices = new Map([
            ["apple", 1.5],
            ["banana", 0.75],
            ["orange", 2.0],
            ["grape", 3.25]
        ]);
        console.log("Fruit prices map:");
        fruitPrices.forEach((price, fruit) => {
            console.log("  " + fruit + ": $" + price);
        });
    "#,
    );

    println!("Many statements register count: {}", chunk.register_count);
    assert!(
        chunk.register_count < 100,
        "Register count {} too high for module-level statements",
        chunk.register_count
    );
}

#[test]
fn test_register_usage_many_variables() {
    // Each module-level variable might consume a register if not freed
    let mut code = String::new();
    for i in 0..100 {
        code.push_str(&format!("const v{} = {};\n", i, i));
    }
    code.push_str("v99");

    let result = std::panic::catch_unwind(|| compile(&code));
    match result {
        Ok(chunk) => {
            println!(
                "100 module-level variables register count: {}",
                chunk.register_count
            );
            // Variables should NOT each consume a permanent register
            // They're stored in the environment, not registers
            assert!(
                chunk.register_count < 50,
                "Register count {} suggests variables are using permanent registers",
                chunk.register_count
            );
        }
        Err(_) => {
            panic!("Compilation failed - hit register limit with just 100 variables");
        }
    }
}

#[test]
fn test_register_usage_many_functions() {
    // Many function declarations at module level
    let mut code = String::new();
    for i in 0..50 {
        code.push_str(&format!(
            "function func{}(x: number): number {{ return x + {}; }}\n",
            i, i
        ));
    }
    code.push_str("func49(1)");

    let result = std::panic::catch_unwind(|| compile(&code));
    match result {
        Ok(chunk) => {
            println!("50 functions register count: {}", chunk.register_count);
            assert!(
                chunk.register_count < 100,
                "Register count {} too high for 50 function declarations",
                chunk.register_count
            );
        }
        Err(e) => {
            panic!("Compilation failed with 50 functions: {:?}", e);
        }
    }
}

#[test]
fn test_register_usage_graph_like_module() {
    // Simulate the graph.ts pattern - many exported functions
    let chunk = compile(
        r#"
        function createGraph() { return { nodes: new Map() }; }
        function addNode(graph, node) { if (!graph.nodes.has(node)) { graph.nodes.set(node, new Set()); } }
        function addEdge(graph, from, to) { addNode(graph, from); addNode(graph, to); graph.nodes.get(from).add(to); }
        function hasEdge(graph, from, to) { const n = graph.nodes.get(from); return n !== undefined && n.has(to); }
        function getNeighbors(graph, node) { return graph.nodes.get(node) || new Set(); }
        function removeEdge(graph, from, to) { const n = graph.nodes.get(from); if (n) { n.delete(to); } }
        function bfs(graph, start) {
            const visited = new Set();
            const result = [];
            const queue = [start];
            while (queue.length > 0) {
                const current = queue.shift();
                if (visited.has(current)) continue;
                visited.add(current);
                result.push(current);
                const neighbors = getNeighbors(graph, current);
                for (const neighbor of neighbors) {
                    if (!visited.has(neighbor)) queue.push(neighbor);
                }
            }
            return result;
        }
        function dfs(graph, start) {
            const visited = new Set();
            const result = [];
            function visit(node) {
                if (visited.has(node)) return;
                visited.add(node);
                result.push(node);
                for (const neighbor of getNeighbors(graph, node)) visit(neighbor);
            }
            visit(start);
            return result;
        }
        const g = createGraph();
        addEdge(g, "A", "B");
        bfs(g, "A");
    "#,
    );

    println!("Graph-like module register count: {}", chunk.register_count);
    assert!(
        chunk.register_count < 150,
        "Register count {} too high for graph-like module",
        chunk.register_count
    );
}