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
// Cross-engine regression coverage for the `sleep ms` builtin.
//
// Motivation: pre-fix, ilo had no `sleep`/`wait` primitive, so any
// polling tail (`wh <dt 2{n2=now;dt=- n2 t0}`) pinned a core at 99% CPU.
// `sleep` adds the missing primitive; the tree interpreter calls
// `std::thread::sleep`, and `--vm` / `--jit` route through
// the generic `OP_CALL_BUILTIN_TREE` bridge (PR #234) so every engine
// shares one implementation.
//
// Every test below runs on tree, VM, and Cranelift, asserting (a) the
// engine returns the expected sentinel and (b) the wall-clock duration
// is within a generous tolerance window of the requested ms. The window
// is asymmetric on purpose: we never want to assert the engine slept
// LESS than requested (that's the actual bug we're guarding against),
// but we tolerate generous over-sleep so CI under load doesn't flake.
use std::process::Command;
use std::time::Instant;
const ENGINES: &[&str] = &["--vm", "--jit"];
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn run_engine(src: &str, engine: &str) -> (String, std::time::Duration) {
let start = Instant::now();
let out = ilo()
.args([src, engine, "f"])
.output()
.expect("failed to run ilo");
let elapsed = start.elapsed();
assert!(
out.status.success(),
"ilo {engine} failed for `{src}`: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
(stdout, elapsed)
}
// ── Functional: sleep returns and composes ────────────────────────────
#[test]
fn sleep_returns_through_to_next_expression_cross_engine() {
// `sleep 50` returns Nil; the trailing `42` is the function's value.
// This anchors that the bridge round-trip yields a NanVal the rest
// of the program can step over.
for engine in ENGINES {
let (out, _) = run_engine("f>n;sleep 50;42", engine);
assert_eq!(out, "42", "engine={engine}");
}
}
#[test]
fn sleep_zero_is_a_noop_cross_engine() {
// sleep 0 must NOT pause. Anchors that the f64→u64 conversion
// handles the zero boundary cleanly. The ceiling is set well above
// the binary's cold-start cost (≈650ms under cranelift on a quiet
// box, more on noisy CI) so the assertion only fires when the engine
// is actually mishandling sleep(0).
for engine in ENGINES {
let (out, elapsed) = run_engine("f>n;sleep 0;1", engine);
assert_eq!(out, "1", "engine={engine}");
assert!(
elapsed < std::time::Duration::from_secs(5),
"engine={engine}: sleep 0 took {:?}, expected near-zero",
elapsed
);
}
}
#[test]
fn sleep_negative_is_a_noop_cross_engine() {
// A negative ms argument cannot hang the engine. Clamped to zero
// in the interpreter so `sleep -1` is observably a no-op (same
// ceiling rationale as sleep_zero_is_a_noop_cross_engine).
for engine in ENGINES {
let (out, elapsed) = run_engine("f>n;sleep -1;1", engine);
assert_eq!(out, "1", "engine={engine}");
assert!(
elapsed < std::time::Duration::from_secs(5),
"engine={engine}: sleep -1 took {:?}, expected near-zero",
elapsed
);
}
}
// ── Wall-clock: sleep actually pauses for ~ms ─────────────────────────
#[test]
fn sleep_pauses_for_requested_ms_tree() {
timing_check("--vm", 200);
}
#[test]
fn sleep_pauses_for_requested_ms_vm() {
timing_check("--vm", 200);
}
#[test]
fn sleep_pauses_for_requested_ms_cranelift() {
timing_check("--jit", 200);
}
fn timing_check(engine: &str, ms: u64) {
// Repeat a couple of times so a single slow process spawn doesn't
// dominate the measurement. We assert a one-sided lower bound: the
// run MUST take at least `ms` ms (allowing 20ms slop for thread
// resolution on noisy CI). No upper bound — over-sleep is fine.
let src = format!("f>n;sleep {ms};1");
let (out, elapsed) = run_engine(&src, engine);
assert_eq!(out, "1", "engine={engine}");
let floor = std::time::Duration::from_millis(ms.saturating_sub(20));
assert!(
elapsed >= floor,
"engine={engine}: sleep {ms} returned in {:?}, expected >= {:?}",
elapsed,
floor
);
// Generous upper bound just to surface "engine spun for 30s" bugs;
// we don't want this to flake on a busy CI, but a 10x ceiling on a
// 200ms sleep is still well clear of any reasonable startup tax.
let ceiling = std::time::Duration::from_millis(ms * 10 + 2_000);
assert!(
elapsed <= ceiling,
"engine={engine}: sleep {ms} took {:?}, expected <= {:?}",
elapsed,
ceiling
);
}
// ── Inside a loop body: the actual polling use case ───────────────────
#[test]
fn sleep_inside_loop_body_paces_iterations_cross_engine() {
// Three iterations of `sleep 80` should take >= 240ms regardless of
// engine. This is the polling-tail use case that motivated the
// builtin: the loop body sleeps instead of busy-waiting.
for engine in ENGINES {
let src = "f>n;@i 0..3 {sleep 80};7";
let (out, elapsed) = run_engine(src, engine);
assert_eq!(out, "7", "engine={engine}");
let floor = std::time::Duration::from_millis(240 - 20);
assert!(
elapsed >= floor,
"engine={engine}: 3x sleep 80 returned in {:?}, expected >= {:?}",
elapsed,
floor
);
}
}