seq-runtime 5.4.0

Runtime library for the Seq programming language
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
//! I/O Operations for Seq
//!
//! These functions are exported with C ABI for LLVM codegen to call.
//!
//! # Safety Contract
//!
//! **IMPORTANT:** These functions are designed to be called ONLY by compiler-generated code,
//! not by end users or arbitrary C code. The compiler is responsible for:
//!
//! - Ensuring stack has correct types (verified by type checker)
//! - Passing valid, null-terminated C strings to `push_string`
//! - Never calling these functions directly from user code
//!
//! # String Handling
//!
//! String literals from the compiler must be valid UTF-8 C strings (null-terminated).
//! Currently, each string literal is allocated as an owned `String`. See
//! `docs/STRING_INTERNING_DESIGN.md` for discussion of future optimizations
//! (interning, static references, etc.).

use crate::stack::{Stack, pop, push};
use crate::value::Value;
use std::ffi::CStr;
use std::io;
use std::sync::LazyLock;

/// Coroutine-aware stdout mutex.
/// Uses may::sync::Mutex which yields the coroutine when contended instead of blocking the OS thread.
/// By serializing access to stdout, we prevent RefCell borrow panics that occur when multiple
/// coroutines on the same thread try to access stdout's internal RefCell concurrently.
static STDOUT_MUTEX: LazyLock<may::sync::Mutex<()>> = LazyLock::new(|| may::sync::Mutex::new(()));

/// Valid exit code range for Unix compatibility
const EXIT_CODE_MIN: i64 = 0;
const EXIT_CODE_MAX: i64 = 255;

/// Write a string to stdout followed by a newline
///
/// Stack effect: ( str -- )
///
/// # Safety
/// Stack must have a String value on top
///
/// # Concurrency
/// Uses may::sync::Mutex to serialize stdout writes from multiple strands.
/// When the mutex is contended, the strand yields to the scheduler (doesn't block the OS thread).
/// This prevents RefCell borrow panics when multiple strands write concurrently.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_write_line(stack: Stack) -> Stack {
    assert!(!stack.is_null(), "write_line: stack is empty");

    let (rest, value) = unsafe { pop(stack) };

    match value {
        Value::String(s) => {
            // Acquire coroutine-aware mutex (yields if contended, doesn't block)
            // This serializes access to stdout
            let _guard = STDOUT_MUTEX.lock().unwrap();

            // Write directly to fd 1 using libc to avoid Rust's std::io::stdout() RefCell.
            // Rust's standard I/O uses RefCell which panics on concurrent access from
            // multiple coroutines on the same thread.
            let str_slice = s.as_str();
            let newline = b"\n";
            unsafe {
                libc::write(
                    1,
                    str_slice.as_ptr() as *const libc::c_void,
                    str_slice.len(),
                );
                libc::write(1, newline.as_ptr() as *const libc::c_void, newline.len());
            }

            rest
        }
        _ => panic!("write_line: expected String on stack, got {:?}", value),
    }
}

/// Write a string to stdout without a trailing newline
///
/// Stack effect: ( str -- )
///
/// This is useful for protocols like LSP that require exact byte output
/// without trailing newlines.
///
/// # Safety
/// Stack must have a String value on top
///
/// # Concurrency
/// Uses may::sync::Mutex to serialize stdout writes from multiple strands.
/// When the mutex is contended, the strand yields to the scheduler (doesn't block the OS thread).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_write(stack: Stack) -> Stack {
    assert!(!stack.is_null(), "write: stack is empty");

    let (rest, value) = unsafe { pop(stack) };

    match value {
        Value::String(s) => {
            let _guard = STDOUT_MUTEX.lock().unwrap();

            let str_slice = s.as_str();
            unsafe {
                libc::write(
                    1,
                    str_slice.as_ptr() as *const libc::c_void,
                    str_slice.len(),
                );
            }

            rest
        }
        _ => panic!("write: expected String on stack, got {:?}", value),
    }
}

/// Read a line from stdin
///
/// Returns the line and a success flag:
/// - ( line true ) on success (line includes trailing newline)
/// - ( "" false ) on I/O error or EOF
///
/// Use `string.chomp` to remove trailing newlines if needed.
///
/// # Line Ending Normalization
///
/// Line endings are normalized to `\n` regardless of platform. Windows-style
/// `\r\n` endings are converted to `\n`. This ensures consistent behavior
/// across different operating systems.
///
/// Stack effect: ( -- String Bool )
///
/// Errors are values, not crashes.
///
/// # Safety
/// Always safe to call
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_read_line(stack: Stack) -> Stack {
    use std::io::BufRead;

    let stdin = io::stdin();
    let mut line = String::new();

    match stdin.lock().read_line(&mut line) {
        Ok(0) => {
            // EOF - return empty string and false
            let stack = unsafe { push(stack, Value::String("".to_string().into())) };
            unsafe { push(stack, Value::Bool(false)) }
        }
        Ok(_) => {
            // Normalize line endings: \r\n -> \n
            if line.ends_with("\r\n") {
                line.pop(); // remove \n
                line.pop(); // remove \r
                line.push('\n'); // add back \n
            }
            let stack = unsafe { push(stack, Value::String(line.into())) };
            unsafe { push(stack, Value::Bool(true)) }
        }
        Err(_) => {
            // I/O error - return empty string and false
            let stack = unsafe { push(stack, Value::String("".to_string().into())) };
            unsafe { push(stack, Value::Bool(false)) }
        }
    }
}

/// Read a line from stdin with explicit EOF detection
///
/// Returns the line and a status flag:
/// - ( line 1 ) on success (line includes trailing newline)
/// - ( "" 0 ) at EOF or I/O error
///
/// Stack effect: ( -- String Int )
///
/// The `+` suffix indicates this returns a result pattern (value + status).
/// Errors are values, not crashes.
///
/// # Line Ending Normalization
///
/// Line endings are normalized to `\n` regardless of platform. Windows-style
/// `\r\n` endings are converted to `\n`. This ensures consistent behavior
/// across different operating systems.
///
/// # Safety
/// Always safe to call
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_read_line_plus(stack: Stack) -> Stack {
    use std::io::BufRead;

    let stdin = io::stdin();
    let mut line = String::new();

    match stdin.lock().read_line(&mut line) {
        Ok(0) => {
            // EOF
            let stack = unsafe { push(stack, Value::String("".to_string().into())) };
            unsafe { push(stack, Value::Int(0)) }
        }
        Ok(_) => {
            // Normalize line endings: \r\n -> \n
            if line.ends_with("\r\n") {
                line.pop(); // remove \n
                line.pop(); // remove \r
                line.push('\n'); // add back \n
            }
            let stack = unsafe { push(stack, Value::String(line.into())) };
            unsafe { push(stack, Value::Int(1)) }
        }
        Err(_) => {
            // I/O error - treat like EOF
            let stack = unsafe { push(stack, Value::String("".to_string().into())) };
            unsafe { push(stack, Value::Int(0)) }
        }
    }
}

/// Maximum bytes allowed for a single read_n call (10MB)
/// This prevents accidental or malicious massive memory allocations.
/// LSP messages are typically < 1MB, so 10MB provides generous headroom.
const READ_N_MAX_BYTES: i64 = 10 * 1024 * 1024;

/// Validates and extracts the byte count from a Value for read_n.
/// Returns Ok(usize) on success, Err(message) on validation failure.
fn validate_read_n_count(value: &Value) -> Result<usize, String> {
    match value {
        Value::Int(n) if *n < 0 => Err(format!(
            "read_n: byte count must be non-negative, got {}",
            n
        )),
        Value::Int(n) if *n > READ_N_MAX_BYTES => Err(format!(
            "read_n: byte count {} exceeds maximum allowed ({})",
            n, READ_N_MAX_BYTES
        )),
        Value::Int(n) => Ok(*n as usize),
        _ => Err(format!("read_n: expected Int on stack, got {:?}", value)),
    }
}

/// Read exactly N bytes from stdin
///
/// Returns the bytes read and a status flag:
/// - ( string 1 ) on success (read all N bytes)
/// - ( string 0 ) at EOF, partial read, or error (string may be shorter than N)
///
/// Stack effect: ( Int -- String Int )
///
/// Like `io.read-line+`, this returns a result pattern (value + status) to allow
/// explicit EOF detection. The function name omits the `+` suffix for brevity
/// since byte-count reads are inherently status-oriented.
///
/// Errors are values, not crashes.
///
/// This is used for protocols like LSP where message bodies are byte-counted
/// and don't have trailing newlines.
///
/// # UTF-8 Handling
/// The bytes are interpreted as UTF-8. Invalid UTF-8 sequences are replaced
/// with the Unicode replacement character (U+FFFD). This is appropriate for
/// text-based protocols like LSP but may not be suitable for binary data.
///
/// # Safety
/// Stack must have an Int on top. The integer must be non-negative and
/// not exceed READ_N_MAX_BYTES (10MB).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_read_n(stack: Stack) -> Stack {
    use std::io::Read;

    assert!(!stack.is_null(), "read_n: stack is empty");

    let (stack, value) = unsafe { pop(stack) };

    // Validate input - return error status for invalid input
    let n = match validate_read_n_count(&value) {
        Ok(n) => n,
        Err(_) => {
            // Invalid input - return empty string and error status
            let stack = unsafe { push(stack, Value::String("".to_string().into())) };
            return unsafe { push(stack, Value::Int(0)) };
        }
    };

    let stdin = io::stdin();
    let mut buffer = vec![0u8; n];
    let mut total_read = 0;

    {
        let mut handle = stdin.lock();
        while total_read < n {
            match handle.read(&mut buffer[total_read..]) {
                Ok(0) => break, // EOF
                Ok(bytes_read) => total_read += bytes_read,
                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
                Err(_) => break, // I/O error - stop reading, return what we have
            }
        }
    }

    // Truncate to actual bytes read
    buffer.truncate(total_read);

    // Convert to String (assuming UTF-8)
    let s = String::from_utf8_lossy(&buffer).into_owned();

    // Status: 1 if we read all N bytes, 0 otherwise
    let status = if total_read == n { 1i64 } else { 0i64 };

    let stack = unsafe { push(stack, Value::String(s.into())) };
    unsafe { push(stack, Value::Int(status)) }
}

/// Convert an integer to a string
///
/// Stack effect: ( Int -- String )
///
/// # Safety
/// Stack must have an Int value on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_int_to_string(stack: Stack) -> Stack {
    assert!(!stack.is_null(), "int_to_string: stack is empty");

    let (rest, value) = unsafe { pop(stack) };

    match value {
        Value::Int(n) => unsafe { push(rest, Value::String(n.to_string().into())) },
        _ => panic!("int_to_string: expected Int on stack, got {:?}", value),
    }
}

/// Push a C string literal onto the stack (for compiler-generated code)
///
/// Stack effect: ( -- str )
///
/// # Safety
/// The c_str pointer must be valid and null-terminated
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_push_string(stack: Stack, c_str: *const i8) -> Stack {
    assert!(!c_str.is_null(), "push_string: null string pointer");

    let s = unsafe {
        CStr::from_ptr(c_str)
            .to_str()
            .expect("push_string: invalid UTF-8 in string literal")
            .to_owned()
    };

    unsafe { push(stack, Value::String(s.into())) }
}

/// Push a C string literal onto the stack as a Symbol (for compiler-generated code)
///
/// Stack effect: ( -- symbol )
///
/// # Safety
/// The c_str pointer must be valid and null-terminated
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_push_symbol(stack: Stack, c_str: *const i8) -> Stack {
    assert!(!c_str.is_null(), "push_symbol: null string pointer");

    let s = unsafe {
        CStr::from_ptr(c_str)
            .to_str()
            .expect("push_symbol: invalid UTF-8 in symbol literal")
            .to_owned()
    };

    unsafe { push(stack, Value::Symbol(s.into())) }
}

/// Layout of static interned symbol data from LLVM IR
///
/// Matches the LLVM IR structure:
/// `{ ptr, i64 len, i64 capacity, i8 global }`
///
/// # Safety Contract
///
/// This struct must ONLY be constructed by the compiler in static globals.
/// Invariants that MUST hold:
/// - `ptr` points to valid static UTF-8 string data with lifetime `'static`
/// - `len` matches the actual byte length of the string
/// - `capacity` MUST be 0 (marks symbol as interned/static)
/// - `global` MUST be 1 (marks symbol as static allocation)
///
/// Violating these invariants causes undefined behavior (memory corruption,
/// double-free, or null pointer dereference).
#[repr(C)]
pub struct InternedSymbolData {
    ptr: *const u8,
    len: i64,
    capacity: i64, // MUST be 0 for interned symbols
    global: i8,    // MUST be 1 for interned symbols
}

/// Push an interned symbol onto the stack (Issue #166)
///
/// This pushes a compile-time symbol literal that shares static memory.
/// The SeqString has capacity=0 to mark it as interned (never freed).
///
/// Stack effect: ( -- Symbol )
///
/// # Safety
/// The symbol_data pointer must point to a valid static InternedSymbolData structure.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_push_interned_symbol(
    stack: Stack,
    symbol_data: *const InternedSymbolData,
) -> Stack {
    assert!(
        !symbol_data.is_null(),
        "push_interned_symbol: null symbol data pointer"
    );

    let data = unsafe { &*symbol_data };

    // Validate interned symbol invariants - these are safety-critical
    // and must run in release builds to prevent memory corruption
    assert!(!data.ptr.is_null(), "Interned symbol data pointer is null");
    assert_eq!(data.capacity, 0, "Interned symbols must have capacity=0");
    assert_ne!(data.global, 0, "Interned symbols must have global=1");

    // Create SeqString that points to static data
    // capacity=0 marks it as interned (Drop will skip deallocation)
    // Safety: from_raw_parts requires valid ptr/len/capacity, which we trust
    // from the LLVM-generated static data
    let seq_str = unsafe {
        crate::seqstring::SeqString::from_raw_parts(
            data.ptr,
            data.len as usize,
            data.capacity as usize, // 0 for interned
            data.global != 0,       // true for interned
        )
    };

    unsafe { push(stack, Value::Symbol(seq_str)) }
}

/// Push a SeqString value onto the stack
///
/// This is used when we already have a SeqString (e.g., from closures).
/// Unlike push_string which takes a C string, this takes a SeqString by value.
///
/// Stack effect: ( -- String )
///
/// # Safety
/// The SeqString must be valid. This is only called from LLVM-generated code, not actual C code.
#[allow(improper_ctypes_definitions)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_push_seqstring(
    stack: Stack,
    seq_str: crate::seqstring::SeqString,
) -> Stack {
    unsafe { push(stack, Value::String(seq_str)) }
}

/// Convert a Symbol to a String
///
/// Stack effect: ( Symbol -- String )
///
/// # Safety
/// Stack must have a Symbol on top.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_symbol_to_string(stack: Stack) -> Stack {
    assert!(!stack.is_null(), "symbol_to_string: stack is empty");

    let (rest, value) = unsafe { pop(stack) };

    match value {
        Value::Symbol(s) => unsafe { push(rest, Value::String(s)) },
        _ => panic!(
            "symbol_to_string: expected Symbol on stack, got {:?}",
            value
        ),
    }
}

/// Convert a String to a Symbol
///
/// Stack effect: ( String -- Symbol )
///
/// # Safety
/// Stack must have a String on top.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_string_to_symbol(stack: Stack) -> Stack {
    assert!(!stack.is_null(), "string_to_symbol: stack is empty");

    let (rest, value) = unsafe { pop(stack) };

    match value {
        Value::String(s) => unsafe { push(rest, Value::Symbol(s)) },
        _ => panic!(
            "string_to_symbol: expected String on stack, got {:?}",
            value
        ),
    }
}

/// Exit the program with a status code
///
/// Stack effect: ( exit_code -- )
///
/// # Safety
/// Stack must have an Int on top. Never returns.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_exit_op(stack: Stack) -> ! {
    assert!(!stack.is_null(), "exit_op: stack is empty");

    let (_rest, value) = unsafe { pop(stack) };

    match value {
        Value::Int(code) => {
            // Explicitly validate exit code is in Unix-compatible range
            if !(EXIT_CODE_MIN..=EXIT_CODE_MAX).contains(&code) {
                panic!(
                    "exit_op: exit code must be in range {}-{}, got {}",
                    EXIT_CODE_MIN, EXIT_CODE_MAX, code
                );
            }
            std::process::exit(code as i32);
        }
        _ => panic!("exit_op: expected Int on stack, got {:?}", value),
    }
}

// Public re-exports with short names for internal use
pub use patch_seq_exit_op as exit_op;
pub use patch_seq_int_to_string as int_to_string;
pub use patch_seq_push_interned_symbol as push_interned_symbol;
pub use patch_seq_push_seqstring as push_seqstring;
pub use patch_seq_push_string as push_string;
pub use patch_seq_push_symbol as push_symbol;
pub use patch_seq_read_line as read_line;
pub use patch_seq_read_line_plus as read_line_plus;
pub use patch_seq_read_n as read_n;
pub use patch_seq_string_to_symbol as string_to_symbol;
pub use patch_seq_symbol_to_string as symbol_to_string;
pub use patch_seq_write as write;
pub use patch_seq_write_line as write_line;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value::Value;
    use std::ffi::CString;

    #[test]
    fn test_write_line() {
        unsafe {
            let stack = crate::stack::alloc_test_stack();
            let stack = push(stack, Value::String("Hello, World!".into()));
            let _stack = write_line(stack);
        }
    }

    #[test]
    fn test_write() {
        unsafe {
            let stack = crate::stack::alloc_test_stack();
            let stack = push(stack, Value::String("no newline".into()));
            let _stack = write(stack);
        }
    }

    #[test]
    fn test_push_string() {
        unsafe {
            let stack = crate::stack::alloc_test_stack();
            let test_str = CString::new("Test").unwrap();
            let stack = push_string(stack, test_str.as_ptr());

            let (_stack, value) = pop(stack);
            assert_eq!(value, Value::String("Test".into()));
        }
    }

    #[test]
    fn test_empty_string() {
        unsafe {
            // Empty string should be handled correctly
            let stack = crate::stack::alloc_test_stack();
            let empty_str = CString::new("").unwrap();
            let stack = push_string(stack, empty_str.as_ptr());

            let (_stack, value) = pop(stack);
            assert_eq!(value, Value::String("".into()));

            // Write empty string should work without panic
            let stack = push(stack, Value::String("".into()));
            let _stack = write_line(stack);
        }
    }

    #[test]
    fn test_unicode_strings() {
        unsafe {
            // Test that Unicode strings are handled correctly
            let stack = crate::stack::alloc_test_stack();
            let unicode_str = CString::new("Hello, δΈ–η•Œ! 🌍").unwrap();
            let stack = push_string(stack, unicode_str.as_ptr());

            let (_stack, value) = pop(stack);
            assert_eq!(value, Value::String("Hello, δΈ–η•Œ! 🌍".into()));
        }
    }

    // =========================================================================
    // read_n validation tests
    // =========================================================================

    #[test]
    fn test_read_n_valid_input() {
        assert_eq!(super::validate_read_n_count(&Value::Int(0)), Ok(0));
        assert_eq!(super::validate_read_n_count(&Value::Int(100)), Ok(100));
        assert_eq!(
            super::validate_read_n_count(&Value::Int(1024 * 1024)), // 1MB
            Ok(1024 * 1024)
        );
    }

    #[test]
    fn test_read_n_negative_input() {
        let result = super::validate_read_n_count(&Value::Int(-1));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("must be non-negative"));
    }

    #[test]
    fn test_read_n_large_negative_input() {
        let result = super::validate_read_n_count(&Value::Int(i64::MIN));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("must be non-negative"));
    }

    #[test]
    fn test_read_n_exceeds_max_bytes() {
        let result = super::validate_read_n_count(&Value::Int(super::READ_N_MAX_BYTES + 1));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("exceeds maximum allowed"));
    }

    #[test]
    fn test_read_n_at_max_bytes_ok() {
        // Exactly at the limit should be OK
        let result = super::validate_read_n_count(&Value::Int(super::READ_N_MAX_BYTES));
        assert_eq!(result, Ok(super::READ_N_MAX_BYTES as usize));
    }

    #[test]
    fn test_read_n_wrong_type_string() {
        let result = super::validate_read_n_count(&Value::String("not an int".into()));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("expected Int"));
    }

    #[test]
    fn test_read_n_wrong_type_bool() {
        let result = super::validate_read_n_count(&Value::Bool(true));
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("expected Int"));
    }
}