1
use nomiscript::{Expr, Fraction, Program, Reader};
2

            
3
144
fn parse(input: &str) -> Program {
4
144
    Reader::parse(input).unwrap()
5
144
}
6

            
7
135
fn parse_one(input: &str) -> Expr {
8
135
    let program = parse(input);
9
135
    assert_eq!(program.exprs.len(), 1, "expected single expression");
10
135
    program.exprs.into_iter().next().unwrap()
11
135
}
12

            
13
54
fn num(n: i64) -> Expr {
14
54
    Expr::Number(Fraction::from_integer(n))
15
54
}
16

            
17
13
fn frac(n: i64, d: i64) -> Expr {
18
13
    Expr::Number(Fraction::new(n, d))
19
13
}
20

            
21
102
fn sym(s: &str) -> Expr {
22
102
    Expr::Symbol(s.into())
23
102
}
24

            
25
60
fn list(items: Vec<Expr>) -> Expr {
26
60
    Expr::List(items)
27
60
}
28

            
29
mod atoms {
30
    use super::*;
31

            
32
    #[test]
33
1
    fn test_nil_variations() {
34
1
        let cases = [("nil", Expr::Nil), ("  nil  ", Expr::Nil)];
35

            
36
2
        for (input, expected) in cases {
37
2
            assert_eq!(parse_one(input), expected, "input: {input:?}");
38
        }
39
1
    }
40

            
41
    #[test]
42
1
    fn test_booleans() {
43
1
        let cases = [
44
1
            ("t", Expr::T),
45
1
            ("nil", Expr::Nil),
46
1
            ("  t  ", Expr::T),
47
1
            ("  nil  ", Expr::Nil),
48
1
        ];
49

            
50
4
        for (input, expected) in cases {
51
4
            assert_eq!(parse_one(input), expected, "input: {input:?}");
52
        }
53
1
    }
54
}
55

            
56
mod numbers {
57
    use super::*;
58

            
59
    #[test]
60
1
    fn test_integers() {
61
1
        let cases = [
62
1
            ("0", num(0)),
63
1
            ("1", num(1)),
64
1
            ("42", num(42)),
65
1
            ("123456789", num(123456789)),
66
1
            ("-1", num(-1)),
67
1
            ("-42", num(-42)),
68
1
            ("+1", num(1)),
69
1
            ("+42", num(42)),
70
1
        ];
71

            
72
8
        for (input, expected) in cases {
73
8
            assert_eq!(parse_one(input), expected, "input: {input:?}");
74
        }
75
1
    }
76

            
77
    #[test]
78
1
    fn test_decimals() {
79
1
        let cases = [
80
1
            ("0.1", frac(1, 10)),
81
1
            ("0.01", frac(1, 100)),
82
1
            ("0.001", frac(1, 1000)),
83
1
            ("1.5", frac(3, 2)),
84
1
            ("3.14", frac(314, 100)),
85
1
            ("10.25", frac(1025, 100)),
86
1
            ("-0.5", frac(-1, 2)),
87
1
            ("-1.25", frac(-5, 4)),
88
1
        ];
89

            
90
8
        for (input, expected) in cases {
91
8
            assert_eq!(parse_one(input), expected, "input: {input:?}");
92
        }
93
1
    }
94

            
95
    #[test]
96
1
    fn test_decimal_normalization() {
97
1
        let half = parse_one("0.5");
98
1
        let also_half = parse_one("0.50");
99
1
        assert_eq!(half, also_half);
100

            
101
1
        let quarter = parse_one("0.25");
102
1
        assert_eq!(quarter, frac(1, 4));
103
1
    }
104

            
105
    #[test]
106
1
    fn test_percent_sugar() {
107
        // `15%` reads as `15/100` (reduced). The `%` rides any numeric body —
108
        // integer, decimal, or rational — and divides it by 100.
109
1
        let cases = [
110
1
            ("15%", frac(15, 100)),
111
1
            ("100%", num(1)),
112
1
            ("0.5%", frac(1, 200)),
113
1
            ("1/2%", frac(1, 200)),
114
1
            ("-50%", frac(-1, 2)),
115
1
        ];
116

            
117
5
        for (input, expected) in cases {
118
5
            assert_eq!(parse_one(input), expected, "input: {input:?}");
119
        }
120
1
    }
121

            
122
    #[test]
123
1
    fn test_percent_equivalent_to_explicit_fraction() {
124
        // `15%` and `15/100` denote the same Scalar — and the same inside a form.
125
1
        assert_eq!(parse_one("15%"), parse_one("15/100"));
126
1
        assert_eq!(parse_one("(* amount 15%)"), parse_one("(* amount 15/100)"));
127
1
    }
128

            
129
    #[test]
130
1
    fn test_decimal_overflow_is_parse_error() {
131
        // Malformed decimals must be structured parse errors, never i64
132
        // overflow panics (debug) / wraps (release).
133
        // 19 fractional digits → 10^19 exceeds i64.
134
1
        assert!(Reader::parse("1.0000000000000000000").is_err());
135
        // 9 × 10^18 + 10^18-scale fraction overflows the scaled numerator.
136
1
        assert!(Reader::parse("9.999999999999999999").is_err());
137
1
    }
138

            
139
    #[test]
140
1
    fn test_percent_denominator_overflow_is_parse_error() {
141
        // `denom * 100` would overflow i64 — must be a structured parse error,
142
        // never an overflow panic (debug) or silent wrap (release).
143
1
        assert!(Reader::parse("1/9223372036854775807%").is_err());
144
1
    }
145

            
146
    #[test]
147
1
    fn test_bare_percent_is_still_a_symbol() {
148
        // `%` is delimiter-only for terminating a numeric token; a lone `%`
149
        // (no numeric body) still reads as a symbol.
150
1
        assert_eq!(parse_one("%"), sym("%"));
151
1
    }
152
}
153

            
154
mod strings {
155
    use super::*;
156

            
157
    #[test]
158
1
    fn test_simple_strings() {
159
1
        let cases = [
160
1
            (r#""""#, Expr::String(String::new())),
161
1
            (r#""hello""#, Expr::String("hello".into())),
162
1
            (r#""hello world""#, Expr::String("hello world".into())),
163
1
            (r#""123""#, Expr::String("123".into())),
164
1
        ];
165

            
166
4
        for (input, expected) in cases {
167
4
            assert_eq!(parse_one(input), expected, "input: {input:?}");
168
        }
169
1
    }
170

            
171
    #[test]
172
1
    fn test_escape_sequences() {
173
1
        let cases = [
174
1
            (r#""hello\nworld""#, Expr::String("hello\nworld".into())),
175
1
            (r#""tab\there""#, Expr::String("tab\there".into())),
176
1
            (r#""return\rhere""#, Expr::String("return\rhere".into())),
177
1
            (r#""slash\\here""#, Expr::String("slash\\here".into())),
178
1
            (r#""quote\"here""#, Expr::String("quote\"here".into())),
179
1
            (r#""multi\n\tline""#, Expr::String("multi\n\tline".into())),
180
1
        ];
181

            
182
6
        for (input, expected) in cases {
183
6
            assert_eq!(parse_one(input), expected, "input: {input:?}");
184
        }
185
1
    }
186

            
187
    #[test]
188
1
    fn test_triple_quoted_strings() {
189
1
        let cases = [
190
1
            (r#""""""""#, Expr::String(String::new())),
191
1
            (r#""""hello""""#, Expr::String("hello".into())),
192
1
            (
193
1
                r#""""line1
194
1
line2""""#,
195
1
                Expr::String("line1\nline2".into()),
196
1
            ),
197
1
            (
198
1
                r#""""contains "quotes" inside""""#,
199
1
                Expr::String("contains \"quotes\" inside".into()),
200
1
            ),
201
1
        ];
202

            
203
4
        for (input, expected) in cases {
204
4
            assert_eq!(parse_one(input), expected, "input: {input:?}");
205
        }
206
1
    }
207
}
208

            
209
mod symbols {
210
    use super::*;
211

            
212
    #[test]
213
1
    fn test_simple_symbols() {
214
1
        let cases = [
215
1
            ("x", sym("X")),
216
1
            ("foo", sym("FOO")),
217
1
            ("hello-world", sym("HELLO-WORLD")),
218
1
            ("snake_case", sym("SNAKE_CASE")),
219
1
            ("CamelCase", sym("CAMELCASE")),
220
1
        ];
221

            
222
5
        for (input, expected) in cases {
223
5
            assert_eq!(parse_one(input), expected, "input: {input:?}");
224
        }
225
1
    }
226

            
227
    #[test]
228
1
    fn test_operator_symbols() {
229
1
        let cases = [
230
1
            ("+", sym("+")),
231
1
            ("-", sym("-")),
232
1
            ("*", sym("*")),
233
1
            ("/", sym("/")),
234
1
            ("=", sym("=")),
235
1
            ("<", sym("<")),
236
1
            (">", sym(">")),
237
1
            ("<=", sym("<=")),
238
1
            (">=", sym(">=")),
239
1
            ("!=", sym("!=")),
240
1
            ("++", sym("++")),
241
1
            ("->", sym("->")),
242
1
            ("=>", sym("=>")),
243
1
        ];
244

            
245
13
        for (input, expected) in cases {
246
13
            assert_eq!(parse_one(input), expected, "input: {input:?}");
247
        }
248
1
    }
249

            
250
    #[test]
251
1
    fn test_special_symbols() {
252
1
        let cases = [
253
1
            ("define", sym("DEFINE")),
254
1
            ("lambda", sym("LAMBDA")),
255
1
            ("if", sym("IF")),
256
1
            ("cond", sym("COND")),
257
1
            ("let", sym("LET")),
258
1
            ("let*", sym("LET*")),
259
1
            ("set!", sym("SET!")),
260
1
            ("begin", sym("BEGIN")),
261
1
            ("car", sym("CAR")),
262
1
            ("cdr", sym("CDR")),
263
1
            ("cons", sym("CONS")),
264
1
            ("list?", sym("LIST?")),
265
1
            ("null?", sym("NULL?")),
266
1
            ("pair?", sym("PAIR?")),
267
1
        ];
268

            
269
14
        for (input, expected) in cases {
270
14
            assert_eq!(parse_one(input), expected, "input: {input:?}");
271
        }
272
1
    }
273

            
274
    #[test]
275
1
    fn test_case_insensitive() {
276
1
        assert_eq!(parse_one("foo"), parse_one("FOO"));
277
1
        assert_eq!(parse_one("foo"), parse_one("Foo"));
278
1
        assert_eq!(parse_one("Sum"), sym("SUM"));
279
1
    }
280

            
281
    #[test]
282
1
    fn test_nil_as_symbol_variant() {
283
1
        assert_eq!(parse_one("nIl"), Expr::Nil);
284
1
        assert_eq!(parse_one("nIL"), Expr::Nil);
285
1
    }
286
}
287

            
288
mod lists {
289
    use super::*;
290

            
291
    #[test]
292
1
    fn test_empty_list() {
293
1
        assert_eq!(parse_one("()"), Expr::Nil);
294
1
        assert_eq!(parse_one("(  )"), Expr::Nil);
295
1
    }
296

            
297
    #[test]
298
1
    fn test_simple_lists() {
299
1
        let cases = [
300
1
            ("(1)", list(vec![num(1)])),
301
1
            ("(1 2)", list(vec![num(1), num(2)])),
302
1
            ("(1 2 3)", list(vec![num(1), num(2), num(3)])),
303
1
            ("(a b c)", list(vec![sym("A"), sym("B"), sym("C")])),
304
1
        ];
305

            
306
4
        for (input, expected) in cases {
307
4
            assert_eq!(parse_one(input), expected, "input: {input:?}");
308
        }
309
1
    }
310

            
311
    #[test]
312
1
    fn test_nested_lists() {
313
1
        let cases = [
314
1
            ("(())", list(vec![Expr::Nil])),
315
1
            ("((1))", list(vec![list(vec![num(1)])])),
316
1
            (
317
1
                "((1 2) (3 4))",
318
1
                list(vec![list(vec![num(1), num(2)]), list(vec![num(3), num(4)])]),
319
1
            ),
320
1
            (
321
1
                "(a (b (c d)))",
322
1
                list(vec![
323
1
                    sym("A"),
324
1
                    list(vec![sym("B"), list(vec![sym("C"), sym("D")])]),
325
1
                ]),
326
1
            ),
327
1
        ];
328

            
329
4
        for (input, expected) in cases {
330
4
            assert_eq!(parse_one(input), expected, "input: {input:?}");
331
        }
332
1
    }
333

            
334
    #[test]
335
1
    fn test_mixed_lists() {
336
1
        let cases = [
337
1
            ("(+ 1 2)", list(vec![sym("+"), num(1), num(2)])),
338
1
            (
339
1
                r#"(print "hello")"#,
340
1
                list(vec![sym("PRINT"), Expr::String("hello".into())]),
341
1
            ),
342
1
            ("(if t 1 0)", list(vec![sym("IF"), Expr::T, num(1), num(0)])),
343
1
        ];
344

            
345
3
        for (input, expected) in cases {
346
3
            assert_eq!(parse_one(input), expected, "input: {input:?}");
347
        }
348
1
    }
349
}
350

            
351
mod quotes {
352
    use super::*;
353

            
354
    #[test]
355
1
    fn test_quoted_atoms() {
356
1
        let cases = [
357
1
            ("'x", Expr::Quote(Box::new(sym("X")))),
358
1
            ("'42", Expr::Quote(Box::new(num(42)))),
359
1
            ("'nil", Expr::Nil),
360
1
            ("'t", Expr::T),
361
1
        ];
362

            
363
4
        for (input, expected) in cases {
364
4
            assert_eq!(parse_one(input), expected, "input: {input:?}");
365
        }
366
1
    }
367

            
368
    #[test]
369
1
    fn test_quoted_lists() {
370
1
        let cases = [
371
1
            ("'()", Expr::Nil),
372
1
            (
373
1
                "'(1 2 3)",
374
1
                Expr::Quote(Box::new(list(vec![num(1), num(2), num(3)]))),
375
1
            ),
376
1
            (
377
1
                "'(a b c)",
378
1
                Expr::Quote(Box::new(list(vec![sym("A"), sym("B"), sym("C")]))),
379
1
            ),
380
1
        ];
381

            
382
3
        for (input, expected) in cases {
383
3
            assert_eq!(parse_one(input), expected, "input: {input:?}");
384
        }
385
1
    }
386

            
387
    #[test]
388
1
    fn test_nested_quotes() {
389
1
        let input = "''x";
390
1
        let expected = Expr::Quote(Box::new(Expr::Quote(Box::new(sym("X")))));
391
1
        assert_eq!(parse_one(input), expected);
392
1
    }
393
}
394

            
395
mod programs {
396
    use super::*;
397

            
398
    #[test]
399
1
    fn test_empty_program() {
400
1
        let program = parse("");
401
1
        assert!(program.exprs.is_empty());
402

            
403
1
        let program = parse("   ");
404
1
        assert!(program.exprs.is_empty());
405

            
406
1
        let program = parse("\n\n");
407
1
        assert!(program.exprs.is_empty());
408
1
    }
409

            
410
    #[test]
411
1
    fn test_multiple_expressions() {
412
1
        let program = parse("1 2 3");
413
1
        assert_eq!(program.exprs.len(), 3);
414
1
        assert_eq!(program.exprs[0], num(1));
415
1
        assert_eq!(program.exprs[1], num(2));
416
1
        assert_eq!(program.exprs[2], num(3));
417
1
    }
418

            
419
    #[test]
420
1
    fn test_scheme_like_programs() {
421
1
        let cases = [
422
1
            (
423
1
                "(define x 10)",
424
1
                vec![list(vec![sym("DEFINE"), sym("X"), num(10)])],
425
1
            ),
426
1
            (
427
1
                "(define (square x) (* x x))",
428
1
                vec![list(vec![
429
1
                    sym("DEFINE"),
430
1
                    list(vec![sym("SQUARE"), sym("X")]),
431
1
                    list(vec![sym("*"), sym("X"), sym("X")]),
432
1
                ])],
433
1
            ),
434
1
            (
435
1
                "(define x 10) (+ x 5)",
436
1
                vec![
437
1
                    list(vec![sym("DEFINE"), sym("X"), num(10)]),
438
1
                    list(vec![sym("+"), sym("X"), num(5)]),
439
1
                ],
440
1
            ),
441
1
            (
442
1
                "(if (> x 0) x (- x))",
443
1
                vec![list(vec![
444
1
                    sym("IF"),
445
1
                    list(vec![sym(">"), sym("X"), num(0)]),
446
1
                    sym("X"),
447
1
                    list(vec![sym("-"), sym("X")]),
448
1
                ])],
449
1
            ),
450
1
        ];
451

            
452
4
        for (input, expected) in cases {
453
4
            let program = parse(input);
454
4
            assert_eq!(program.exprs, expected, "input: {input:?}");
455
        }
456
1
    }
457

            
458
    #[test]
459
1
    fn test_lambda_expressions() {
460
1
        let cases = [
461
1
            (
462
1
                "(lambda (x) x)",
463
1
                list(vec![sym("LAMBDA"), list(vec![sym("X")]), sym("X")]),
464
1
            ),
465
1
            (
466
1
                "(lambda (x y) (+ x y))",
467
1
                list(vec![
468
1
                    sym("LAMBDA"),
469
1
                    list(vec![sym("X"), sym("Y")]),
470
1
                    list(vec![sym("+"), sym("X"), sym("Y")]),
471
1
                ]),
472
1
            ),
473
1
            (
474
1
                "((lambda (x) (* x x)) 5)",
475
1
                list(vec![
476
1
                    list(vec![
477
1
                        sym("LAMBDA"),
478
1
                        list(vec![sym("X")]),
479
1
                        list(vec![sym("*"), sym("X"), sym("X")]),
480
1
                    ]),
481
1
                    num(5),
482
1
                ]),
483
1
            ),
484
1
        ];
485

            
486
3
        for (input, expected) in cases {
487
3
            assert_eq!(parse_one(input), expected, "input: {input:?}");
488
        }
489
1
    }
490

            
491
    #[test]
492
1
    fn test_let_expressions() {
493
1
        let cases = [
494
1
            (
495
1
                "(let ((x 1)) x)",
496
1
                list(vec![
497
1
                    sym("LET"),
498
1
                    list(vec![list(vec![sym("X"), num(1)])]),
499
1
                    sym("X"),
500
1
                ]),
501
1
            ),
502
1
            (
503
1
                "(let ((x 1) (y 2)) (+ x y))",
504
1
                list(vec![
505
1
                    sym("LET"),
506
1
                    list(vec![
507
1
                        list(vec![sym("X"), num(1)]),
508
1
                        list(vec![sym("Y"), num(2)]),
509
1
                    ]),
510
1
                    list(vec![sym("+"), sym("X"), sym("Y")]),
511
1
                ]),
512
1
            ),
513
1
        ];
514

            
515
2
        for (input, expected) in cases {
516
2
            assert_eq!(parse_one(input), expected, "input: {input:?}");
517
        }
518
1
    }
519

            
520
    #[test]
521
1
    fn test_cond_expressions() {
522
1
        let input = "(cond ((< x 0) -1) ((= x 0) 0) (t 1))";
523
1
        let expected = list(vec![
524
1
            sym("COND"),
525
1
            list(vec![list(vec![sym("<"), sym("X"), num(0)]), num(-1)]),
526
1
            list(vec![list(vec![sym("="), sym("X"), num(0)]), num(0)]),
527
1
            list(vec![Expr::T, num(1)]),
528
        ]);
529
1
        assert_eq!(parse_one(input), expected);
530
1
    }
531
}
532

            
533
mod whitespace {
534
    use super::*;
535

            
536
    #[test]
537
1
    fn test_various_whitespace() {
538
1
        let inputs = [
539
1
            "(+ 1 2)",
540
1
            "( + 1 2 )",
541
1
            "(  +  1  2  )",
542
1
            "(\n+\n1\n2\n)",
543
1
            "(\t+\t1\t2\t)",
544
1
            "( \n\t + \n\t 1 \n\t 2 \n\t )",
545
1
        ];
546

            
547
1
        let expected = list(vec![sym("+"), num(1), num(2)]);
548

            
549
6
        for input in inputs {
550
6
            assert_eq!(parse_one(input), expected, "input: {input:?}");
551
        }
552
1
    }
553

            
554
    #[test]
555
1
    fn test_leading_trailing_whitespace() {
556
1
        let inputs = ["42", " 42", "42 ", " 42 ", "\n42\n", "\t42\t"];
557

            
558
6
        for input in inputs {
559
6
            assert_eq!(parse_one(input), num(42), "input: {input:?}");
560
        }
561
1
    }
562
}
563

            
564
mod edge_cases {
565
    use super::*;
566

            
567
    #[test]
568
1
    fn test_deeply_nested() {
569
1
        let input = "((((((1))))))";
570
1
        let mut expr = num(1);
571
6
        for _ in 0..6 {
572
6
            expr = list(vec![expr]);
573
6
        }
574
1
        assert_eq!(parse_one(input), expr);
575
1
    }
576

            
577
    #[test]
578
1
    fn test_long_symbol() {
579
1
        let input = "a".repeat(1000);
580
1
        let expected = "A".repeat(1000);
581
1
        assert_eq!(parse_one(&input), sym(&expected));
582
1
    }
583

            
584
    #[test]
585
1
    fn test_many_list_elements() {
586
1
        let input = format!(
587
            "({})",
588
1
            (1..=100)
589
100
                .map(|n| n.to_string())
590
1
                .collect::<Vec<_>>()
591
1
                .join(" ")
592
        );
593
1
        let program = parse(&input);
594
1
        assert_eq!(program.exprs.len(), 1);
595
1
        if let Expr::List(items) = &program.exprs[0] {
596
1
            assert_eq!(items.len(), 100);
597
        } else {
598
            panic!("expected list");
599
        }
600
1
    }
601
}
602

            
603
/// ADR-0029 colon-namespace grammar, plus the cross-crate regression guard
604
/// that the rpc envelope + transaction-plist shapes (which parse through this
605
/// same `Reader`) are unaffected by the grammar tightening.
606
mod namespaces {
607
    use super::*;
608

            
609
    #[test]
610
1
    fn qualified_symbol_single_and_double_colon() {
611
1
        assert_eq!(parse_one("finance:add-money"), sym("FINANCE:ADD-MONEY"));
612
        // `::` folds to the canonical single-colon key.
613
1
        assert_eq!(parse_one("finance::add-money"), sym("FINANCE:ADD-MONEY"));
614
1
    }
615

            
616
    #[test]
617
1
    fn qualified_symbol_in_call_position() {
618
1
        assert_eq!(
619
1
            parse_one("(split:list-for-transaction tx)"),
620
1
            list(vec![sym("SPLIT:LIST-FOR-TRANSACTION"), sym("TX")])
621
        );
622
1
    }
623

            
624
    #[test]
625
1
    fn leading_colon_remains_keyword() {
626
1
        assert_eq!(parse_one(":foo"), Expr::Keyword("FOO".into()));
627
1
    }
628

            
629
    #[test]
630
1
    fn malformed_qualified_symbols_rejected() {
631
7
        for bad in ["foo:", "foo::", "a:b:c", "a::b::c", "a:b::c", "::foo", ":"] {
632
7
            assert!(
633
7
                Reader::parse(bad).is_err(),
634
                "expected parse error for {bad:?}"
635
            );
636
        }
637
1
    }
638

            
639
    // Cross-crate guard: the rpc envelope (`(:id N :form …)`) and the
640
    // create-transaction plist payload (`(:post-date … :splits (…))`) ride
641
    // this same reader. The grammar change touches only symbol/leading-colon
642
    // parsing, so well-formed `:key value` plists must parse byte-unchanged.
643

            
644
    #[test]
645
1
    fn rpc_envelope_frame_parses_unchanged() {
646
1
        let frame = parse_one("(:id 42 :form (list-accounts))");
647
1
        assert_eq!(
648
            frame,
649
1
            list(vec![
650
1
                Expr::Keyword("ID".into()),
651
1
                num(42),
652
1
                Expr::Keyword("FORM".into()),
653
1
                list(vec![sym("LIST-ACCOUNTS")]),
654
            ])
655
        );
656
1
    }
657

            
658
    #[test]
659
1
    fn transaction_plist_payload_parses_unchanged() {
660
1
        let payload = parse_one(
661
1
            "(:post-date \"2026-02-01T00:00:00Z\" \
662
1
             :splits ((:account-id \"a\" :value -100) (:account-id \"b\" :value 100)))",
663
        );
664
1
        let Expr::List(items) = payload else {
665
            panic!("expected plist");
666
        };
667
1
        assert_eq!(items[0], Expr::Keyword("POST-DATE".into()));
668
1
        assert_eq!(items[1], Expr::String("2026-02-01T00:00:00Z".into()));
669
1
        assert_eq!(items[2], Expr::Keyword("SPLITS".into()));
670
1
    }
671
}