seq-runtime 6.6.2

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
//! Test framework support for Seq
//!
//! Provides assertion primitives and test context management for the `seqc test` runner.
//! Assertions collect failures instead of panicking, allowing all tests to run and
//! report comprehensive results.
//!
//! These functions are exported with C ABI for LLVM codegen to call.

use crate::stack::{Stack, pop, push};
use crate::value::Value;
use std::sync::Mutex;

/// Render a stack `Value` for a failure message — prefers the natural
/// form for `Int` / `Bool`, falls back to debug for anything else.
fn display_value(val: &Value) -> String {
    match val {
        Value::Bool(b) => b.to_string(),
        Value::Int(n) => n.to_string(),
        other => format!("{:?}", other),
    }
}

/// Escape a string for inclusion in a single-line failure detail.
///
/// `\n`, `\r`, `\t`, `\\`, `\"` become their two-character source forms;
/// other control bytes (< 0x20 or 0x7F) become `\xNN`. Non-ASCII bytes
/// pass through unchanged so legible UTF-8 stays legible.
///
/// Critical for the `assert-eq-str` failure path: the test runner's
/// `collect_failure_block` only attaches continuation lines that start
/// with whitespace, so an embedded raw newline in a value would break
/// the failure block in two and silently truncate the report.
fn escape_for_display(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.bytes() {
        match b {
            b'\\' => out.push_str("\\\\"),
            b'"' => out.push_str("\\\""),
            b'\n' => out.push_str("\\n"),
            b'\r' => out.push_str("\\r"),
            b'\t' => out.push_str("\\t"),
            0x20..=0x7E => out.push(b as char),
            0x00..=0x1F | 0x7F => {
                use std::fmt::Write;
                let _ = write!(out, "\\x{:02X}", b);
            }
            _ => out.push(b as char),
        }
    }
    out
}

/// Discriminator for how a failure should be rendered. The string-eq
/// path needs a multi-line block (escaped values, byte counts, first
/// differing offset); other failures keep the historical single-line
/// `expected E, got A` shape.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FailureKind {
    #[default]
    Other,
    StringEq,
}

/// Maximum number of per-test assertion failures to print in the run
/// summary. Additional failures are rolled up into a `+N more failure(s)`
/// footer so noisy tests (loop-like assertions over lists) don't drown
/// the overall report. Tune here if feedback suggests a different value.
const MAX_PRINTED_FAILURES_PER_TEST: usize = 5;

/// A single test failure with context
#[derive(Debug, Clone)]
pub struct TestFailure {
    /// Source line of the assertion (1-indexed), if codegen set one.
    pub line: Option<u32>,
    pub message: String,
    /// For `kind == StringEq` these hold the *raw* values; the printer
    /// escapes them. For other kinds they hold pre-formatted strings.
    pub expected: Option<String>,
    pub actual: Option<String>,
    pub kind: FailureKind,
}

/// Test context that tracks assertion results
#[derive(Debug, Default)]
pub struct TestContext {
    /// Current test name being executed
    pub current_test: Option<String>,
    /// Source line of the assertion most recently announced by codegen.
    /// Set by `patch_seq_test_set_line` just before each `test.assert*`
    /// call; captured into a `TestFailure` if the assertion fails.
    pub current_line: Option<u32>,
    /// Number of passed assertions
    pub passes: usize,
    /// Collected failures
    pub failures: Vec<TestFailure>,
}

impl TestContext {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn reset(&mut self, test_name: Option<String>) {
        self.current_test = test_name;
        self.current_line = None;
        self.passes = 0;
        self.failures.clear();
    }

    pub fn record_pass(&mut self) {
        self.passes += 1;
        // Consume the line so a following assertion without a `set_line`
        // hook (defensive — span-less `WordCall`s don't emit one) can't
        // inherit this one's attribution.
        self.current_line = None;
    }

    pub fn record_failure(
        &mut self,
        message: String,
        expected: Option<String>,
        actual: Option<String>,
    ) {
        self.failures.push(TestFailure {
            line: self.current_line,
            message,
            expected,
            actual,
            kind: FailureKind::Other,
        });
        // Same rationale as `record_pass`: don't let this line bleed into
        // the next assertion's record.
        self.current_line = None;
    }

    /// Record a string-equality failure. Stores the raw (unescaped)
    /// values; rendering happens at print time so we keep the byte
    /// counts and first-differing offset accurate.
    pub fn record_string_eq_failure(&mut self, expected: String, actual: String) {
        self.failures.push(TestFailure {
            line: self.current_line,
            message: "assertion failed: strings not equal".to_string(),
            expected: Some(expected),
            actual: Some(actual),
            kind: FailureKind::StringEq,
        });
        self.current_line = None;
    }

    pub fn has_failures(&self) -> bool {
        !self.failures.is_empty()
    }
}

/// Global test context protected by mutex
static TEST_CONTEXT: Mutex<TestContext> = Mutex::new(TestContext {
    current_test: None,
    current_line: None,
    passes: 0,
    failures: Vec::new(),
});

/// Announce the source line of the next `test.assert*` call.
///
/// Called by generated code immediately before each assertion so the
/// runtime can attribute a failure to its source position. `line` is
/// 1-indexed; pass 0 to clear.
///
/// This helper takes a raw `i64` rather than a stack argument because it
/// is a compiler-emitted diagnostic, not a user-callable Seq builtin.
///
/// # Safety
///
/// Safe to call from any thread. Acquires the global test-context
/// mutex; no other preconditions.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_set_line(line: i64) {
    let mut ctx = TEST_CONTEXT.lock().unwrap();
    // Reject 0 (the agreed "clear" sentinel) and any value that can't
    // fit in a u32 (no real source file has 4B lines, but be explicit
    // about truncation intent rather than silently wrapping).
    ctx.current_line = if line > 0 {
        u32::try_from(line).ok()
    } else {
        None
    };
}

/// Set the current test's display name without touching any other state.
///
/// Used by the `seqc test` runner to reassert the word-level test name
/// after the user's test word has run, in case the user called
/// `test.init "friendly name"` internally and overwrote the header.
/// Unlike `test.init`, this does NOT clear `failures`, `passes`, or
/// `current_line`.
///
/// Stack effect: ( ..a String -- ..a )
///
/// # Safety
/// Stack must have a String (test name) on top.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_set_name(stack: Stack) -> Stack {
    unsafe {
        let (stack, name_val) = pop(stack);
        let name = match name_val {
            Value::String(s) => s.as_str_or_empty().to_string(),
            _ => panic!("test.set-name: expected String (test name) on stack"),
        };
        let mut ctx = TEST_CONTEXT.lock().unwrap();
        ctx.current_test = Some(name);
        stack
    }
}

/// Initialize test context for a new test
///
/// Stack effect: ( name -- )
///
/// # Safety
/// Stack must have a String (test name) on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_init(stack: Stack) -> Stack {
    unsafe {
        let (stack, name_val) = pop(stack);
        let name = match name_val {
            Value::String(s) => s.as_str_or_empty().to_string(),
            _ => panic!("test.init: expected String (test name) on stack"),
        };

        let mut ctx = TEST_CONTEXT.lock().unwrap();
        ctx.reset(Some(name));
        stack
    }
}

/// Render a single failure as one or more indented detail lines,
/// terminated by a final newline. The result is always whitespace-prefixed
/// on every line so the runner's `collect_failure_block` attaches it to
/// the preceding `... FAILED` header.
///
/// Pure (no I/O, no globals) so the output is unit-testable.
pub fn format_failure_detail(failure: &TestFailure) -> String {
    use std::fmt::Write;
    let mut out = String::new();

    match (failure.kind, &failure.expected, &failure.actual) {
        (FailureKind::StringEq, Some(e), Some(a)) => {
            match failure.line {
                Some(line) => writeln!(out, "  at line {}: {}", line, failure.message).unwrap(),
                None => writeln!(out, "  {}", failure.message).unwrap(),
            }
            writeln!(
                out,
                "    expected ({} bytes): \"{}\"",
                e.len(),
                escape_for_display(e)
            )
            .unwrap();
            writeln!(
                out,
                "    actual   ({} bytes): \"{}\"",
                a.len(),
                escape_for_display(a)
            )
            .unwrap();
            if e != a {
                let common = e
                    .as_bytes()
                    .iter()
                    .zip(a.as_bytes().iter())
                    .take_while(|(x, y)| x == y)
                    .count();
                writeln!(out, "    first differ at byte {}", common).unwrap();
            }
        }
        (_, Some(e), Some(a)) => {
            let detail = format!("expected {}, got {}", e, a);
            match failure.line {
                Some(line) => writeln!(out, "  at line {}: {}", line, detail).unwrap(),
                None => writeln!(out, "  {}", detail).unwrap(),
            }
        }
        _ => match failure.line {
            Some(line) => writeln!(out, "  at line {}: {}", line, failure.message).unwrap(),
            None => writeln!(out, "  {}", failure.message).unwrap(),
        },
    }

    out
}

/// Finalize test and print results
///
/// Stack effect: ( -- )
///
/// Prints pass/fail summary for the current test in a format parseable by the test runner.
/// Output format: "test-name ... ok" or "test-name ... FAILED"
///
/// # Safety
/// Stack pointer must be valid
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_finish(stack: Stack) -> Stack {
    let ctx = TEST_CONTEXT.lock().unwrap();
    let test_name = ctx.current_test.as_deref().unwrap_or("unknown");

    if ctx.failures.is_empty() {
        // Output pass in parseable format
        println!("{} ... ok", test_name);
    } else {
        // Output failure in parseable format. Detail lines are emitted on
        // STDOUT, indented, so the test runner can associate them with the
        // preceding FAILED header on the same stream.
        // Cap the per-test output so a flood of failures (e.g. a loop-like
        // test walking a list) doesn't drown the summary. The first
        // `MAX_PRINTED_FAILURES_PER_TEST` are printed in full; a footer
        // counts anything suppressed.
        println!("{} ... FAILED", test_name);
        for failure in ctx.failures.iter().take(MAX_PRINTED_FAILURES_PER_TEST) {
            print!("{}", format_failure_detail(failure));
        }
        if ctx.failures.len() > MAX_PRINTED_FAILURES_PER_TEST {
            let remaining = ctx.failures.len() - MAX_PRINTED_FAILURES_PER_TEST;
            let s = if remaining == 1 { "" } else { "s" };
            println!("  +{} more failure{}", remaining, s);
        }
    }

    stack
}

/// Check if any assertions failed
///
/// Stack effect: ( -- Int )
///
/// Returns 1 if there are failures, 0 if all passed.
///
/// # Safety
/// Stack pointer must be valid
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_has_failures(stack: Stack) -> Stack {
    let ctx = TEST_CONTEXT.lock().unwrap();
    let has_failures = ctx.has_failures();
    unsafe { push(stack, Value::Bool(has_failures)) }
}

/// Assert that a value is truthy (non-zero)
///
/// Stack effect: ( Int -- )
///
/// Records failure if value is 0, records pass otherwise.
///
/// # Safety
/// Stack must have an Int on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert(stack: Stack) -> Stack {
    unsafe {
        let (stack, val) = pop(stack);
        let condition = match val {
            Value::Int(n) => n != 0,
            Value::Bool(b) => b,
            _ => panic!("test.assert: expected Int or Bool on stack, got {:?}", val),
        };

        let mut ctx = TEST_CONTEXT.lock().unwrap();
        if condition {
            ctx.record_pass();
        } else {
            ctx.record_failure(
                "assertion failed".to_string(),
                Some("true".to_string()),
                Some(display_value(&val)),
            );
        }

        stack
    }
}

/// Assert that a value is falsy (zero)
///
/// Stack effect: ( Int -- )
///
/// Records failure if value is non-zero, records pass otherwise.
///
/// # Safety
/// Stack must have an Int on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert_not(stack: Stack) -> Stack {
    unsafe {
        let (stack, val) = pop(stack);
        let is_falsy = match val {
            Value::Int(n) => n == 0,
            Value::Bool(b) => !b,
            _ => panic!(
                "test.assert-not: expected Int or Bool on stack, got {:?}",
                val
            ),
        };

        let mut ctx = TEST_CONTEXT.lock().unwrap();
        if is_falsy {
            ctx.record_pass();
        } else {
            ctx.record_failure(
                "assertion failed".to_string(),
                Some("false".to_string()),
                Some(display_value(&val)),
            );
        }

        stack
    }
}

/// Assert that two integers are equal
///
/// Stack effect: ( actual expected -- )
///
/// Records failure if values differ, records pass otherwise.
///
/// # Safety
/// Stack must have two Ints on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert_eq(stack: Stack) -> Stack {
    unsafe {
        let (stack, expected_val) = pop(stack);
        let (stack, actual_val) = pop(stack);

        let (expected, actual) = match (&expected_val, &actual_val) {
            (Value::Int(e), Value::Int(a)) => (*e, *a),
            _ => panic!(
                "test.assert-eq: expected two Ints on stack, got {:?} and {:?}",
                expected_val, actual_val
            ),
        };

        let mut ctx = TEST_CONTEXT.lock().unwrap();
        if expected == actual {
            ctx.record_pass();
        } else {
            ctx.record_failure(
                "assertion failed: values not equal".to_string(),
                Some(expected.to_string()),
                Some(actual.to_string()),
            );
        }

        stack
    }
}

/// Assert that two strings are equal
///
/// Stack effect: ( actual expected -- )
///
/// Records failure if strings differ, records pass otherwise.
///
/// # Safety
/// Stack must have two Strings on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert_eq_str(stack: Stack) -> Stack {
    unsafe {
        let (stack, expected_val) = pop(stack);
        let (stack, actual_val) = pop(stack);

        let (expected, actual) = match (&expected_val, &actual_val) {
            (Value::String(e), Value::String(a)) => (
                e.as_str_or_empty().to_string(),
                a.as_str_or_empty().to_string(),
            ),
            _ => panic!(
                "test.assert-eq-str: expected two Strings on stack, got {:?} and {:?}",
                expected_val, actual_val
            ),
        };

        let mut ctx = TEST_CONTEXT.lock().unwrap();
        if expected == actual {
            ctx.record_pass();
        } else {
            ctx.record_string_eq_failure(expected, actual);
        }

        stack
    }
}

/// Explicitly fail a test with a message
///
/// Stack effect: ( message -- )
///
/// Always records a failure with the given message.
///
/// # Safety
/// Stack must have a String on top
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_fail(stack: Stack) -> Stack {
    unsafe {
        let (stack, msg_val) = pop(stack);
        let message = match msg_val {
            Value::String(s) => s.as_str_or_empty().to_string(),
            _ => panic!("test.fail: expected String (message) on stack"),
        };

        let mut ctx = TEST_CONTEXT.lock().unwrap();
        ctx.record_failure(message, None, None);

        stack
    }
}

/// Get the number of passed assertions
///
/// Stack effect: ( -- Int )
///
/// # Safety
/// Stack pointer must be valid
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_pass_count(stack: Stack) -> Stack {
    let ctx = TEST_CONTEXT.lock().unwrap();
    unsafe { push(stack, Value::Int(ctx.passes as i64)) }
}

/// Get the number of failed assertions
///
/// Stack effect: ( -- Int )
///
/// # Safety
/// Stack pointer must be valid
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_fail_count(stack: Stack) -> Stack {
    let ctx = TEST_CONTEXT.lock().unwrap();
    unsafe { push(stack, Value::Int(ctx.failures.len() as i64)) }
}

#[cfg(test)]
mod tests;