seq-runtime 5.7.1

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
//! 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),
    }
}

/// 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,
    pub expected: Option<String>,
    pub actual: Option<String>,
}

/// 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,
        });
        // Same rationale as `record_pass`: don't let this line bleed into
        // the next assertion's record.
        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().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().to_string(),
            _ => panic!("test.init: expected String (test name) on stack"),
        };

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

/// 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) {
            let detail = match (&failure.expected, &failure.actual) {
                (Some(e), Some(a)) => format!("expected {}, got {}", e, a),
                _ => failure.message.clone(),
            };
            match failure.line {
                Some(line) => println!("  at line {}: {}", line, detail),
                None => println!("  {}", detail),
            }
        }
        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: ( expected actual -- )
///
/// 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, actual_val) = pop(stack);
        let (stack, expected_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: ( expected actual -- )
///
/// 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, actual_val) = pop(stack);
        let (stack, expected_val) = pop(stack);

        let (expected, actual) = match (&expected_val, &actual_val) {
            (Value::String(e), Value::String(a)) => {
                (e.as_str().to_string(), a.as_str().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_failure(
                "assertion failed: strings not equal".to_string(),
                Some(format!("\"{}\"", expected)),
                Some(format!("\"{}\"", 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().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;