use std::path::PathBuf;
use std::process::Command;
mod common;
use common::bin_path;
fn fixture(name: &str) -> PathBuf {
common::fixture(&format!("run/{name}"))
}
fn scratch_dir() -> PathBuf {
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
.join(format!("run-tests-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create this process's scratch directory");
dir
}
fn run_fixture(name: &str) -> (i32, String, String) {
let output = Command::new(bin_path())
.arg("run")
.arg(fixture(name))
.output()
.expect("failed to run praxis");
let code = output.status.code().unwrap_or(-1);
(
code,
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn assert_passes(name: &str, expected: &str) {
let (code, stdout, stderr) = run_fixture(name);
assert_eq!(
code, 0,
"`{name}` should exit 0\nstdout: {stdout}\nstderr: {stderr}"
);
assert_eq!(
stdout.trim(),
expected,
"`{name}` should print {expected:?}, got {stdout:?}"
);
}
fn assert_faults(name: &str, fault_msg: &str) {
let (code, _stdout, stderr) = run_fixture(name);
assert_eq!(
code, 1,
"`{name}` should exit 1 (fault), got code {code}\nstderr: {stderr}"
);
assert!(
stderr.contains(fault_msg),
"`{name}` should report `{fault_msg}`, got stderr: {stderr}"
);
}
#[test]
fn missing_explicit_input_file_is_a_usage_error() {
let missing = fixture("definitely-missing-input.txt");
let output = Command::new(bin_path())
.args(["run", "--input"])
.arg(&missing)
.arg(fixture("constant.px"))
.output()
.expect("failed to run praxis");
let code = output.status.code().unwrap_or(-1);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(code, 2, "unreadable explicit input is a usage error");
assert!(
stderr.contains("failed to read") && stderr.contains("input"),
"the input I/O error must be reported, got: {stderr}"
);
}
#[test]
fn run_pass_constant() {
assert_passes("constant.px", "42");
}
fn run_with_open_stdin(
name: &str,
stdin: &str,
deadline: std::time::Duration,
) -> Option<(i32, String)> {
use std::io::Write;
let mut child = Command::new(bin_path())
.arg("run")
.arg(fixture(name))
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn praxis");
let mut pipe = child.stdin.take().expect("piped stdin");
if !stdin.is_empty() {
pipe.write_all(stdin.as_bytes()).expect("write to child");
pipe.flush().expect("flush to child");
}
let start = std::time::Instant::now();
let status = loop {
match child.try_wait().expect("try_wait") {
Some(status) => break Some(status),
None if start.elapsed() >= deadline => break None,
None => std::thread::sleep(std::time::Duration::from_millis(20)),
}
};
match status {
None => {
let _ = child.kill();
let _ = child.wait();
None
}
Some(status) => {
drop(pipe);
let out = child.wait_with_output().expect("wait_with_output");
Some((
status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
))
}
}
}
#[test]
fn a_program_that_never_reads_does_not_wait_for_standard_input() {
let deadline = std::time::Duration::from_secs(10);
let outcome = run_with_open_stdin("constant.px", "", deadline);
let (code, stdout) = outcome.expect(
"`praxis run` blocked on standard input for a program with no `read` \
(§7.10: the *first* `read` reads it)",
);
assert_eq!(code, 0, "stdout: {stdout}");
assert_eq!(stdout.trim(), "42");
}
#[test]
fn a_program_that_reads_still_waits_for_its_input() {
let outcome = run_with_open_stdin(
"reads_lines_of_int.px",
"1\n2\n3\n",
std::time::Duration::from_secs(2),
);
assert!(
outcome.is_none(),
"a `read` reads to EOF; this pipe has sent none, so the program \
cannot have finished — laziness must not mean the input is skipped, \
got {outcome:?}"
);
}
fn assert_passes_with_stdin(name: &str, stdin: &str, expected: &str) {
use std::io::Write;
let mut child = Command::new(bin_path())
.arg("run")
.arg(fixture(name))
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn praxis");
{
let mut pipe = child.stdin.take().expect("piped stdin");
pipe.write_all(stdin.as_bytes()).expect("write to child");
}
let out = child.wait_with_output().expect("wait_with_output");
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(out.status.code(), Some(0), "stderr: {stderr}");
assert_eq!(stdout.trim(), expected);
}
#[test]
fn a_second_read_sees_the_same_buffer() {
assert_passes_with_stdin("reads_lines_of_int.px", "1\n2\n3\n", "3\n3");
}
#[test]
fn a_parse_that_outgrows_the_native_root_reservation_still_answers() {
let lines: String = (0..4096).map(|n| format!("{n}\n")).collect();
assert_passes_with_stdin("reads_lines_of_int.px", &lines, "4096\n4096");
}
#[test]
fn a_zero_byte_input_file_is_empty_input_and_not_a_contentless_fault() {
let empty = scratch_dir().join("praxis-rep60-empty.in");
std::fs::write(&empty, b"").expect("write the empty input file");
let output = Command::new(bin_path())
.args(["run", "--debug=never", "--input"])
.arg(&empty)
.arg(fixture("reads_lines_of_int.px"))
.output()
.expect("failed to run praxis");
let code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
let _ = std::fs::remove_file(&empty);
assert_eq!(
code, 0,
"`read lines(int)` over an empty file is the empty list, not a fault\n\
stdout: {stdout}\nstderr: {stderr}"
);
assert_eq!(stdout.trim(), "0\n0", "stderr: {stderr}");
}
fn run_with_closed_stdin(name: &str, stdin: &str) -> (i32, String, String) {
use std::io::Write;
let mut child = Command::new(bin_path())
.args(["run", "--debug=never"])
.arg(fixture(name))
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn praxis");
{
let mut pipe = child.stdin.take().expect("piped stdin");
pipe.write_all(stdin.as_bytes()).expect("write to child");
}
let out = child.wait_with_output().expect("wait_with_output");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn run_with_input_file(name: &str, contents: &str, tag: &str) -> (i32, String, String) {
let path = scratch_dir().join(format!("praxis-{tag}.in"));
std::fs::write(&path, contents).expect("write the input file");
let output = Command::new(bin_path())
.args(["run", "--debug=never", "--input"])
.arg(&path)
.arg(fixture(name))
.output()
.expect("failed to run praxis");
let _ = std::fs::remove_file(&path);
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[test]
fn empty_standard_input_is_empty_input_and_not_a_contentless_fault() {
assert_passes_with_stdin("reads_lines_of_int.px", "", "0\n0");
}
#[test]
fn empty_standard_input_faults_with_an_offset_and_an_expectation() {
let (code, stdout, stderr) = run_with_closed_stdin("reads_an_int.px", "");
assert_eq!(
code, 1,
"`read int` over an empty buffer is a mismatch\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stderr.contains("at input offset 0..0"),
"the mismatch must name where it happened (§7.11 input span): {stderr}"
);
assert!(
stderr.contains("expected int"),
"the mismatch must name what it wanted (§7.11 expected description): {stderr}"
);
}
#[test]
fn a_failing_read_names_what_it_saw() {
let (code, stdout, stderr) = run_with_closed_stdin("reads_an_int.px", "x\n");
assert_eq!(code, 1, "stdout: {stdout}\nstderr: {stderr}");
assert!(
stderr.contains("at input offset 0..0") && stderr.contains("expected int"),
"{stderr}"
);
assert!(
stderr.contains("actual: x⏎"),
"a non-empty failing input previews what was there (§7.11 actual preview): {stderr}"
);
}
#[test]
fn empty_stdin_and_a_zero_byte_input_file_answer_the_same() {
let piped = run_with_closed_stdin("reads_an_int.px", "");
let filed = run_with_input_file("reads_an_int.px", "", "rep60-same-answer");
assert_eq!(
piped, filed,
"empty standard input and a zero-byte `--input` file are the same \
question and must get the same answer (REP-60, ADR-087)"
);
}
#[test]
fn run_pass_arithmetic() {
assert_passes("arithmetic.px", "7");
}
#[test]
fn run_pass_float_literal() {
assert_passes("float_literal.px", "2.5");
}
#[test]
fn run_pass_float_arith() {
assert_passes("float_arith.px", "6.5");
}
#[test]
fn run_pass_float_methods() {
assert_passes("float_methods.px", "9.0");
}
#[test]
fn run_pass_float_div_by_zero() {
assert_passes("float_div_by_zero.px", "inf");
}
#[test]
fn run_pass_float_negative_zero() {
assert_passes(
"float_negative_zero.px",
"-0.0\n-inf\n-0.0\n-inf\n-0.0\n-inf\ninf\ninf\n-2.5\n-2.5",
);
}
#[test]
fn run_pass_float_compound_assign() {
let expected = [
"3.0", "3.0", "2.5", "-0.0", "-inf",
"1.0", "1.0", "3.0", "0.0", "inf",
"0.0", "3.0", "3.0", "2.5", "-inf", "1.0", "3", "3", "ab", "3.0",
]
.join("\n");
assert_passes("float_compound_assign.px", &expected);
}
#[test]
fn run_pass_place_assignment() {
let expected = [
"5",
"2",
"1",
"abcd",
"8",
"9",
"[100, 2, 13]",
"3", "1",
"42",
"2", "{ inner: { v: 7 }, xs: [10, 30] }",
"[{ v: 100 }, { v: 2 }]",
"8",
"55", "1",
"9", "3",
]
.join("\n");
assert_passes("place_assignment.px", &expected);
}
#[test]
fn run_fault_float_to_int_nan() {
assert_faults(
"float_to_int_nan.px",
"float-to-int conversion out of range",
);
}
#[test]
fn run_pass_branch() {
assert_passes("branch.px", "100");
}
#[test]
fn run_pass_loop_sum() {
assert_passes("loop_sum.px", "15");
}
#[test]
fn run_pass_recursive_factorial() {
assert_passes("factorial.px", "120");
}
#[test]
fn run_pass_recursive_fibonacci() {
assert_passes("fibonacci.px", "55");
}
#[test]
fn out_prints_its_argument_once() {
assert_passes("out_prints_once.px", "kurac");
}
#[test]
fn a_program_with_no_out_prints_nothing() {
let (code, stdout, stderr) = run_fixture("prints_nothing.px");
assert_eq!(code, 0, "should exit 0\nstdout: {stdout}\nstderr: {stderr}");
assert_eq!(
stdout, "",
"a program with no `out` prints nothing, got {stdout:?}"
);
}
#[test]
fn run_fault_overflow() {
assert_faults("overflow.px", "integer overflow");
}
#[test]
fn run_fault_division_by_zero() {
assert_faults("div_by_zero.px", "division by zero");
}
#[test]
fn run_fault_does_not_abort() {
let (code, _, _) = run_fixture("overflow.px");
assert_ne!(
code, -1,
"process was killed by a signal (abort/panic leaked across the ABI)"
);
}
fn run_fixture_debug(name: &str, debug: &str) -> (i32, String, String) {
let output = Command::new(bin_path())
.args(["run", "--debug", debug])
.arg(fixture(name))
.output()
.expect("failed to run praxis");
let code = output.status.code().unwrap_or(-1);
(
code,
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[test]
fn m10ws4_noninteractive_renders_backtrace_and_locals() {
let (code, _stdout, stderr) = run_fixture_debug("debug_backtrace.px", "never");
assert_eq!(code, 1, "fault exits 1");
assert!(stderr.contains("program faulted: index out of bounds"));
assert!(stderr.contains("Backtrace:"), "backtrace header present");
assert!(stderr.contains("#0"), "backtrace numbers frames");
assert!(stderr.contains("<entry>"), "frame name shown");
assert!(
stderr.contains("xs:") && stderr.contains("[11, 22]"),
"named local renders with value: {stderr}"
);
assert!(stderr.contains("temps:"), "temps section present: {stderr}");
assert!(
stderr.contains("xs.get(99)"),
"faulting temp shows its materializing expression: {stderr}"
);
}
#[test]
fn m10ws4_debug_never_exits_one_without_repl() {
let (code, _stdout, stderr) = run_fixture_debug("overflow.px", "never");
assert_eq!(code, 1);
assert!(!stderr.contains("Praxis crash>"));
assert!(stderr.contains("integer overflow"));
}
#[test]
fn m10ws4_default_auto_non_tty_is_noninteractive() {
let (code, _stdout, stderr) = run_fixture("overflow.px");
assert_eq!(code, 1);
assert!(
stderr.contains("Backtrace:"),
"auto/non-TTY still renders backtrace"
);
}
fn run_repl_with_cmds(name: &str, repl_cmds: &str) -> (i32, String) {
use std::process::Stdio;
let mut child = Command::new(bin_path())
.args(["run", "--debug=always", "--input", "/dev/null"])
.arg(fixture(name))
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("failed to spawn praxis");
{
use std::io::Write;
let mut stdin = child.stdin.take().expect("stdin");
stdin
.write_all(repl_cmds.as_bytes())
.expect("write repl cmds");
}
let output = child.wait_with_output().expect("wait");
let code = output.status.code().unwrap_or(-1);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
(code, combined)
}
#[test]
fn m10ws5_repl_bt_and_locals_and_quit() {
let (code, out) = run_repl_with_cmds("debug_backtrace.px", "bt\nlocals\nquit\n");
assert_eq!(code, 1, "faulted run exits 1 after REPL quits");
assert!(out.contains("Praxis crash>"), "REPL prompt shown: {out}");
assert!(out.contains("#0"), "bt ran: {out}");
assert!(out.contains("<entry>"), "frame name shown: {out}");
assert!(
out.contains("xs:") && out.contains("[11, 22]"),
"locals ran: {out}"
);
assert!(out.contains("locals:"), "locals section header: {out}");
assert!(out.contains("temps:"), "temps section header: {out}");
}
#[test]
fn m11_locals_split_users_and_temps_with_types() {
let (code, out) = run_repl_with_cmds("debug_temps.px", "locals\nquit\n");
assert_eq!(code, 1, "overflow faults and exits 1 after REPL quits");
assert!(out.contains("a: Int = 10"), "user local a with type: {out}");
assert!(out.contains("b: Int = 20"), "user local b with type: {out}");
assert!(out.contains("c: Int = 30"), "user local c with type: {out}");
assert!(out.contains("temps:"), "temps section header: {out}");
assert!(
out.contains("<tmp#") && out.contains(": Int>"),
"temps tagged with id and type: {out}"
);
assert!(
out.contains("@ \"10\""),
"literal temp shows its source: {out}"
);
assert!(
out.contains("@ \"a + b\""),
"binop temp shows its source: {out}"
);
assert!(
out.contains("@ \"a + b + c + 9223372036854775807\""),
"faulting binop temp shows its source: {out}"
);
}
#[test]
fn a_forwarded_binop_temp_still_renders_the_value_it_materialized() {
let (code, out) = run_repl_with_cmds("debug_temps.px", "locals\nquit\n");
assert_eq!(code, 1, "overflow faults and exits 1 after REPL quits");
assert!(
out.contains("@ \"a + b\" = 30"),
"the `a + b` temp renders its value, not `<uninit>`: {out}"
);
}
#[test]
fn every_temp_the_forwarding_elided_renders_its_value_again() {
let (code, out) = run_repl_with_cmds("debug_temps.px", "locals\nquit\n");
assert_eq!(code, 1, "overflow faults and exits 1 after REPL quits");
for expected in [
"@ \"a + b\" = 30",
"@ \"a + b + c\" = 60",
"@ \"9223372036854775807\" = 9223372036854775807",
"@ \"10\" = 10",
"@ \"20\" = 20",
"@ \"30\" = 30",
] {
assert!(out.contains(expected), "missing `{expected}`: {out}");
}
assert!(
out.contains("@ \"a + b + c + 9223372036854775807\" = <uninit>"),
"the expression that faulted produced no value: {out}"
);
}
#[test]
fn m11_temp_provenance_shows_materializing_expression() {
let (code, out) = run_repl_with_cmds("debug_backtrace.px", "locals\nquit\n");
assert_eq!(code, 1);
assert!(
out.contains("@ \"xs.get(99)\""),
"faulting temp shows its materializing expression: {out}"
);
assert!(
out.contains("@ \"xs.push(11)\""),
"push temp shows its expression too: {out}"
);
}
#[test]
fn every_pattern_binding_prints_its_name_and_type() {
let (code, out) = run_repl_with_cmds("debug_pattern_bindings.px", "locals\nquit\n");
assert_eq!(code, 1, "the subscript faults and the REPL exits 1");
for expected in [
"total: Int = ",
"limit: Int = 100",
"item: Int = 3",
"a: Int = 6",
"b: Int = 7",
"payload: Int = 8",
"bumped: Int = 10",
] {
assert!(out.contains(expected), "missing `{expected}`: {out}");
}
assert!(
!out.contains(" ? = "),
"no binding reaches the frame without a name: {out}"
);
}
#[test]
fn a_destructuring_fors_scrutinee_is_a_temp_not_a_binding() {
let (_code, out) = run_repl_with_cmds("debug_pattern_bindings.px", "locals\nquit\n");
let (_, dump) = out
.rsplit_once(" locals:")
.expect("the REPL printed a locals section");
let (bindings, temps) = dump
.split_once(" temps:")
.expect("and a temps section under it");
assert!(
temps.contains("(Int, Int)> @ \"pairs\" = (6, 7)"),
"the item slot is a temp that says what it holds and what it was read \
out of: {out}"
);
assert!(
!bindings.contains("= (6, 7)"),
"and the pair the loop walks is not listed as a binding: {out}"
);
}
#[test]
fn p_binds_a_pattern_introduced_binding() {
let (_code, out) = run_repl_with_cmds(
"debug_pattern_bindings.px",
"p item + payload\np a + b\nquit\n",
);
for absent in ["`item` is not defined", "`payload` is not defined"] {
assert!(
!out.contains(absent),
"{absent} — but `locals` prints it: {out}"
);
}
assert!(out.contains("11"), "`p item + payload` answers 11: {out}");
assert!(out.contains("13"), "`p a + b` answers 13: {out}");
}
#[test]
fn m10ws5_repl_frame_navigation() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "frame 0\nup\nquit\n");
assert!(out.contains("frame 0:"), "frame select ran: {out}");
assert!(out.contains("outermost"), "up-at-top boundary: {out}");
}
#[test]
fn m10ws5_repl_eof_exits() {
let (code, _out) = run_repl_with_cmds("debug_backtrace.px", "");
assert_eq!(code, 1, "EOF exits the REPL with the fault exit code");
}
#[test]
fn m10ws5_repl_help_lists_commands() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "help\nquit\n");
for cmd in ["bt", "frame", "up", "down", "locals", "quit"] {
assert!(out.contains(cmd), "help lists `{cmd}`: {out}");
}
}
#[test]
fn m10b_ws3_source_renders_faulting_function_text() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "source\nquit\n");
assert!(
out.contains("<entry>:"),
"source shows the frame header: {out}"
);
assert!(
out.contains("xs.get(99)"),
"source shows the faulting line: {out}"
);
assert!(out.contains('^'), "source shows a caret: {out}");
}
#[test]
fn m10b_ws3_source_help_lists_command() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "help\nquit\n");
assert!(out.contains("source"), "help lists `source`: {out}");
assert!(out.contains("input"), "help lists `input`: {out}");
assert!(out.contains("parser"), "help lists `parser`: {out}");
}
#[test]
fn m10b_ws4_p_literal_arithmetic() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "p 1 + 2\nquit\n");
assert!(out.contains("3"), "p 1 + 2 should print 3: {out}");
}
#[test]
fn m10b_ws4_p_evaluates_pure_method_on_snapshot_local() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "p xs.len()\nquit\n");
assert!(out.contains("2"), "p xs.len() should print 2: {out}");
}
#[test]
fn m10b_ws4_p_index_into_snapshot_vec() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "p xs.get(0)\nquit\n");
assert!(out.contains("11"), "p xs.get(0) should print 11: {out}");
}
#[test]
fn m10b_ws4_p_rejects_mutation() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "p xs.push(99)\nquit\n");
assert!(
out.contains("error") && out.contains("impure"),
"p xs.push(99) should be rejected as impure: {out}"
);
}
#[test]
fn m10b_ws4_type_reports_collection_type() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "type xs\nquit\n");
assert!(
out.contains("Vec[Int]"),
"type xs should be Vec[Int]: {out}"
);
}
#[test]
fn m10b_ws4_type_reports_inferred_method_type() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "type xs.len()\nquit\n");
assert!(out.contains("Int"), "type xs.len() should be Int: {out}");
}
#[test]
fn m10b_ws5_heap_shows_value_with_type() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "heap xs\nquit\n");
assert!(
out.contains("Vec[Int]") && out.contains("[11, 22]"),
"heap xs should show type + value: {out}"
);
}
#[test]
fn m10b_ws5_heap_literal() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "heap 1 + 2\nquit\n");
assert!(
out.contains("Int"),
"heap 1 + 2 should show Int type: {out}"
);
assert!(out.contains("3"), "heap 1 + 2 should show value 3: {out}");
}
#[test]
fn dbg06_p_evaluates_a_struct_local_and_its_fields() {
let (_code, out) =
run_repl_with_cmds("debug_user_types.px", "p foo\np foo.y\np foo.x.z\nquit\n");
assert!(
!out.contains("unknown type"),
"the struct's declaration reaches the synthetic module: {out}"
);
assert!(
out.contains(r#"{ x: { z: "qweqwe" }, y: 100 }"#),
"`p foo` prints the record, its `Text` field quoted: {out}"
);
assert!(
out.contains("100"),
"`p foo.y` reads the second field: {out}"
);
assert!(
out.contains("qweqwe"),
"`p foo.x.z` reads through the nested record: {out}"
);
}
#[test]
fn dbg06_type_and_heap_report_user_declared_types() {
let (_code, out) = run_repl_with_cmds(
"debug_user_types.px",
"type foo\nheap foo\ntype move\np move\nquit\n",
);
assert!(out.contains("Foo"), "`type foo` is `Foo`: {out}");
assert!(
out.contains(r#"Foo: { x: { z: "qweqwe" }, y: 100 }"#),
"`heap foo` prefixes the value with its type: {out}"
);
assert!(out.contains("Move"), "`type move` is `Move`: {out}");
assert!(
out.contains("Step(3, 4)"),
"an enum local evaluates to its variant: {out}"
);
}
#[test]
fn dbg06_a_literal_expression_ignores_the_frames_locals() {
for fixture in [
"debug_user_types.px",
"debug_many_locals.px",
"debug_template_record.px",
] {
let (_code, out) = run_repl_with_cmds(fixture, "p 1 + 2\nquit\n");
assert!(
out.contains("Praxis crash> 3"),
"`p 1 + 2` is 3 on {fixture}: {out}"
);
}
}
#[test]
fn dbg06_arity_ceiling_counts_the_expressions_names() {
let (_code, out) = run_repl_with_cmds("debug_many_locals.px", "p a + b + c\nquit\n");
assert!(
!out.contains("supports up to"),
"eight locals in the frame do not refuse a three-name expression: {out}"
);
assert!(out.contains('6'), "a + b + c is 6: {out}");
}
#[test]
fn dbg06_p_evaluates_a_parser_templates_anonymous_record() {
let (_code, out) = run_repl_with_cmds(
"debug_template_record.px",
"p points\ntype points\np points[0].x\nheap points[0]\nquit\n",
);
assert!(
!out.contains("expected a type"),
"the anonymous record is declared under a minted name: {out}"
);
assert!(
out.contains("[{ x: 1, y: 2 }, { x: 3, y: 4 }]"),
"`p points` prints the parsed records: {out}"
);
assert!(
out.contains("Vec[{ x: Int, y: Int }]"),
"`type points` reports the structural type: {out}"
);
assert!(
!out.contains("__p_rec"),
"no minted name reaches the user: {out}"
);
assert!(
out.contains("{ x: Int, y: Int }: { x: 1, y: 2 }"),
"`heap points[0]` reports the element's type and value: {out}"
);
}
#[test]
fn dbg06_an_expression_can_write_a_type_the_program_declares() {
let (_code, out) = run_repl_with_cmds(
"debug_user_types.px",
"p Foo{x: Poo{z: \"hi\"}, y: 1}\np Stay\np match Step(6, 7) { Step(a, b) => a + b, Stay => 0 }\nquit\n",
);
assert!(
out.contains(r#"{ x: { z: "hi" }, y: 1 }"#),
"a record literal builds a value of the program's type: {out}"
);
assert!(
out.contains("Praxis crash> Stay"),
"a payload-less variant evaluates: {out}"
);
assert!(
out.contains("13"),
"a match over a constructed variant evaluates: {out}"
);
}
#[test]
fn dbg06_an_unspellable_local_says_why_it_is_missing() {
let (_code, out) = run_repl_with_cmds("debug_template_record.px", "p empty\nquit\n");
assert!(
out.contains("local `empty` was not bound"),
"the drop is explained: {out}"
);
assert!(
out.contains("Vec[?T]"),
"…with the type that stopped it: {out}"
);
}
#[test]
fn m10b_ws6_restart_refaults_deterministically() {
let (_code, out) = run_repl_with_cmds("debug_backtrace.px", "restart\nbt\nquit\n");
assert!(
out.contains("program faulted"),
"restart should re-fault: {out}"
);
assert!(
out.matches("#0").count() >= 2,
"bt after restart runs against the new snapshot: {out}"
);
}
#[test]
fn a_restart_with_empty_input_sees_the_same_empty_input() {
let (_code, out) = run_repl_with_cmds("reads_an_int.px", "restart\ninput\nquit\n");
assert!(
out.contains("input at offset 0..0:"),
"after `restart`, the REPL's `input` must describe the same zero-length \
buffer the first run parsed against (§9.7): {out}"
);
assert!(
!out.contains("no input context"),
"the restarted run *did* fail to parse, so `input` has a context to \
report: {out}"
);
}
#[test]
fn m10b_ws6_reload_after_edit_changes_result() {
use std::io::{Read, Write};
let dir = scratch_dir();
let src_path = dir.join("m10b_ws6_reload.px");
{
let mut f = std::fs::File::create(&src_path).unwrap();
f.write_all(b"out(1 / 0)").unwrap();
}
use std::process::Stdio;
let mut child = Command::new(bin_path())
.args(["run", "--debug=always", "--input", "/dev/null"])
.arg(&src_path)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn");
let mut stdin = child.stdin.take().expect("stdin");
let stderr = child.stderr.as_mut().expect("stderr");
let mut seen = Vec::new();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
let mut buf = [0u8; 256];
match stderr.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
seen.extend_from_slice(&buf[..n]);
if String::from_utf8_lossy(&seen).contains("Praxis crash>") {
break;
}
}
}
}
assert!(
String::from_utf8_lossy(&seen).contains("Praxis crash>"),
"REPL should start before reload: {}",
String::from_utf8_lossy(&seen)
);
{
let mut f = std::fs::File::create(&src_path).unwrap();
f.write_all(b"out(42)").unwrap();
}
stdin.write_all(b"reload\nquit\n").unwrap();
drop(stdin);
let output = child.wait_with_output().expect("wait");
let combined = format!(
"{}{}{}",
String::from_utf8_lossy(&seen),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let _ = std::fs::remove_file(&src_path);
assert!(
combined.contains("program completed"),
"reload after edit should run cleanly: {combined}"
);
assert!(
combined.contains("42"),
"reload should reflect the edited source: {combined}"
);
}
#[test]
fn m10b_ws6_reload_on_malformed_source_keeps_session() {
use std::io::{Read, Write};
let dir = scratch_dir();
let src_path = dir.join("m10b_ws6_reload_bad.px");
{
let mut f = std::fs::File::create(&src_path).unwrap();
f.write_all(b"out(1 / 0)").unwrap();
}
use std::process::Stdio;
let mut child = Command::new(bin_path())
.args(["run", "--debug=always", "--input", "/dev/null"])
.arg(&src_path)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("spawn");
let mut stdin = child.stdin.take().expect("stdin");
let stderr = child.stderr.as_mut().expect("stderr");
let mut seen = Vec::new();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while std::time::Instant::now() < deadline {
let mut buf = [0u8; 256];
match stderr.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
seen.extend_from_slice(&buf[..n]);
if String::from_utf8_lossy(&seen).contains("Praxis crash>") {
break;
}
}
}
}
assert!(
String::from_utf8_lossy(&seen).contains("Praxis crash>"),
"REPL should start: {}",
String::from_utf8_lossy(&seen)
);
{
let mut f = std::fs::File::create(&src_path).unwrap();
f.write_all(b"out(1 / 0").unwrap();
}
stdin.write_all(b"reload\nbt\nquit\n").unwrap();
drop(stdin);
let output = child.wait_with_output().expect("wait");
let combined = format!(
"{}{}{}",
String::from_utf8_lossy(&seen),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let _ = std::fs::remove_file(&src_path);
assert!(
combined.contains("error") && combined.contains("unchanged"),
"reload on malformed source should error and keep the session: {combined}"
);
assert!(
combined.contains("#0"),
"bt runs against the old snapshot after a failed reload: {combined}"
);
}
#[test]
fn a_top_level_statement_runs_in_the_order_it_is_written() {
assert_passes("top_level_statements.px", "1\n2\n3");
assert_passes("top_level_calls_a_declared_fn.px", "1\n4\n6");
}
#[test]
fn a_declared_main_is_an_ordinary_function() {
assert_passes("top_level_beside_fn_main.px", "1\n2\n3");
let (code, stdout, stderr) = run_fixture("only_fn_main.px");
assert_eq!(code, 1, "stdout: {stdout}\nstderr: {stderr}");
assert!(
stderr.contains("no statements to run")
&& stderr.contains("call it with `main()`")
&& stderr.contains("move its body to the top level"),
"the error names both ways out of it: {stderr}"
);
let (code, stdout, stderr) = run_fixture("no_statements_and_no_main.px");
assert_eq!(code, 1, "stdout: {stdout}\nstderr: {stderr}");
assert!(stderr.contains("no statements to run"), "{stderr}");
assert!(
!stderr.contains("`fn main`"),
"a file with no `main` is not told about one: {stderr}"
);
}
#[test]
fn the_entry_points_name_is_not_one_a_program_can_spell() {
let dir = scratch_dir().join("praxis_rep19_entry_name");
std::fs::create_dir_all(&dir).unwrap();
let src_path = dir.join("entry.px");
std::fs::write(&src_path, "var v = Vec()\nout(v.get(0))\n").unwrap();
let output = Command::new(bin_path())
.arg("run")
.arg(&src_path)
.output()
.expect("failed to run praxis");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
combined.contains("index out of bounds"),
"a top-level statement's fault reaches the host: {combined}"
);
assert!(
combined.contains("<entry>"),
"the generated frame is named, and named unspellably: {combined}"
);
std::fs::write(&src_path, "fn <entry>() { out(1) }\n").unwrap();
let output = Command::new(bin_path())
.arg("check")
.arg(&src_path)
.output()
.expect("failed to run praxis");
assert_ne!(
output.status.code().unwrap_or(-1),
0,
"`fn <entry>()` must not be a declaration"
);
let _ = std::fs::remove_file(&src_path);
}
#[test]
fn run_pass_enum_renders_its_variant_name() {
assert_passes(
"enum_variant_names.px",
"Empty\nWall\nNumber(7)\nSome(3)\nNone\nSome(x)",
);
}
#[test]
fn the_destination_of_a_faulting_instruction_is_uninit() {
let (code, out) = run_repl_with_cmds("faulting_subscript.px", "locals\nquit\n");
assert_eq!(code, 1, "the index fault exits 1 after the REPL quits");
assert!(
out.contains("@ \"values[start + 2]\" = <uninit>"),
"the subscript that faulted produced no value: {out}"
);
for expected in [
"@ \"values[start]\" = 7",
"@ \"start + 1\" = 2",
"@ \"values[start + 1]\" = 41",
"@ \"values[start] + values[start + 1]\" = 48",
"@ \"start + 2\" = 3",
"values: Vec[Int] = [12, 7, 41]",
"start: Int = 1",
] {
assert!(out.contains(expected), "missing `{expected}`: {out}");
}
}
#[test]
fn a_curried_closure_prints_the_same_thing_with_and_without_braces() {
let dir = scratch_dir();
let bare = dir.join("curried-bare.px");
let braced = dir.join("curried-braced.px");
std::fs::write(
&bare,
"var base = 10\nvar mk = |a| |b| b + base\nout(mk)\nout(mk(5)(1))\n",
)
.expect("write the bare spelling");
std::fs::write(
&braced,
"var base = 10\nvar mk = |a| { |b| b + base }\nout(mk)\nout(mk(5)(1))\n",
)
.expect("write the braced spelling");
let run = |path: &PathBuf| {
let output = Command::new(bin_path())
.args(["run", "--debug=never"])
.arg(path)
.output()
.expect("failed to run praxis");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
};
let (bare_code, bare_out, bare_err) = run(&bare);
let (braced_code, braced_out, braced_err) = run(&braced);
let _ = std::fs::remove_file(&bare);
let _ = std::fs::remove_file(&braced);
assert_eq!(bare_code, 0, "the bare spelling should exit 0: {bare_err}");
assert_eq!(
braced_code, 0,
"the braced spelling should exit 0: {braced_err}"
);
assert_eq!(
bare_out, braced_out,
"one pair of braces cannot change a closure's environment"
);
assert_eq!(bare_out, "<closure:1>\n11\n");
}
#[test]
fn a_char_match_dispatches_on_the_character() {
let dir = scratch_dir();
let src = dir.join("char-match.px");
std::fs::write(
&src,
"fn cell(c: Char) -> Text {\n\
\x20 match c {\n\
\x20 '#' => \"wall\"\n\
\x20 '.' => \"open\"\n\
\x20 _ => \"other\"\n\
\x20 }\n\
}\n\
for c in \"#.x\" {\n\
\x20 out(cell(c))\n\
}\n\
out('#' == \"#\"[0])\n\
out('#'.to_int())\n\
out('é'.to_int())\n",
)
.expect("write the source");
let output = Command::new(bin_path())
.args(["run", "--debug=never", "--input", "/dev/null"])
.arg(&src)
.output()
.expect("failed to run praxis");
let _ = std::fs::remove_file(&src);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(0), "{stderr}");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"wall\nopen\nother\ntrue\n35\n233\n",
"{stderr}"
);
}
#[test]
fn a_hole_renders_the_value_end_to_end() {
let dir = scratch_dir();
let src = dir.join("interp.px");
std::fs::write(
&src,
"var part2 = 42\n\
out(\"Part 2: {part2}\")\n\
var a = 3\n\
var b = 4\n\
out(\"{a} + {b} = {a + b}\")\n\
var v = [1, 2, 3]\n\
out(\"v = {v}\")\n\
out(v)\n\
var m = Map[Text, Int]()\n\
m[\"k\"] = 9\n\
out(\"m = {m[\"k\"]}, len = {v.len()}\")\n\
out(\"literal braces: \\{ and \\}\")\n\
var f = |n: Int| \"a is {a}, n is {n}\"\n\
out(f(7))\n",
)
.expect("write the source");
let output = Command::new(bin_path())
.args(["run", "--debug=never", "--input", "/dev/null"])
.arg(&src)
.output()
.expect("failed to run praxis");
let _ = std::fs::remove_file(&src);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(0), "{stderr}");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"Part 2: 42\n\
3 + 4 = 7\n\
v = [1, 2, 3]\n\
[1, 2, 3]\n\
m = 9, len = 3\n\
literal braces: { and }\n\
a is 3, n is 7\n",
"{stderr}"
);
}
#[test]
fn a_hole_renders_an_int_and_plus_still_refuses_one() {
let dir = scratch_dir();
let src = dir.join("interp-plus.px");
std::fs::write(
&src,
"var n = 3\n\
out(\"n = {n}\")\n\
out(\"n = \" + n)\n",
)
.expect("write the source");
let output = Command::new(bin_path())
.arg("check")
.arg(&src)
.output()
.expect("failed to run praxis");
let _ = std::fs::remove_file(&src);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "{stderr}");
let rendered = format!("{}{}", String::from_utf8_lossy(&output.stdout), stderr);
assert!(
rendered.contains("Y001") && rendered.contains("expected Text, found Int"),
"`+` must still refuse an Int operand: {rendered}"
);
assert_eq!(
rendered.matches("error[").count(),
1,
"only the `+` is an error; the hole above it is not: {rendered}"
);
}
#[test]
fn a_nan_key_deduplicates_or_not_depending_on_whether_its_slot_was_promoted() {
let dir = scratch_dir();
let src = dir.join("nan-keys.px");
std::fs::write(
&src,
"var zero = 0.0\n\
var nan = zero / zero\n\
var s = Set()\n\
s.insert(nan)\n\
s.insert(nan)\n\
out(s.len())\n\
var t = Set()\n\
t.insert(0.0 / 0.0)\n\
t.insert(0.0 / 0.0)\n\
out(t.len())\n\
var x = zero / zero\n\
var i = 0\n\
while i < 3 {\n\
\x20 x = x + 0.0\n\
\x20 i = i + 1\n\
}\n\
var u = Set()\n\
u.insert(x)\n\
u.insert(x)\n\
out(u.len())\n",
)
.expect("write the source");
let output = Command::new(bin_path())
.args(["run", "--debug=never", "--input", "/dev/null"])
.arg(&src)
.output()
.expect("failed to run praxis");
let _ = std::fs::remove_file(&src);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(0), "{stderr}");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"1\n2\n2\n",
"`s` is unpromoted and unchanged, `t` never deduplicated, and `u` is \
the promoted case that moved from 1 to 2: {stderr}"
);
}
fn run_stop_with_cmds(name: &str, cmds: &str) -> (i32, String, String) {
use std::process::Stdio;
let mut child = Command::new(bin_path())
.args(["run", "--debug=always", "--input", "/dev/null"])
.arg(fixture(name))
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("failed to spawn praxis");
{
use std::io::Write;
let mut stdin = child.stdin.take().expect("stdin");
stdin.write_all(cmds.as_bytes()).expect("write stop cmds");
}
let output = child.wait_with_output().expect("wait");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[test]
fn a_breakpoint_off_a_terminal_prints_and_keeps_running() {
let (code, stdout, stderr) = run_fixture("breakpoint_once.px");
assert_eq!(code, 0, "a stop is not a failure\nstderr: {stderr}");
assert_eq!(stdout.trim(), "41", "the program ran to its own answer");
assert!(stderr.contains("stop: breakpoint"), "{stderr}");
assert!(
stderr.contains(":bp"),
"the marked line is quoted: {stderr}"
);
assert!(
stderr.contains("#0 grow") && stderr.contains("#1 <entry>"),
"the live chain is shown: {stderr}"
);
assert!(
stderr.contains("doubled: Int = 40"),
"the marked statement had run: {stderr}"
);
assert!(!stderr.contains("Praxis"), "no prompt was opened: {stderr}");
}
#[test]
fn debug_never_makes_a_breakpoint_inert() {
let (code, stdout, stderr) = run_fixture_debug("breakpoint_once.px", "never");
assert_eq!(code, 0);
assert_eq!(stdout.trim(), "41");
assert_eq!(stderr, "", "nothing was printed at all: {stderr:?}");
}
#[test]
fn the_stop_prompt_continues_from_one_pass_to_the_next() {
let (code, stdout, stderr) =
run_stop_with_cmds("breakpoint_loop.px", "locals\ncontinue\nlocals\ncontinue\n");
assert_eq!(code, 0, "the program finished\nstderr: {stderr}");
assert_eq!(stdout.trim(), "11", "and answered what it always answers");
assert!(stderr.contains("Stopped at a breakpoint."), "{stderr}");
assert!(
stderr.contains("Praxis stop> "),
"the stop prompt, not the crash one: {stderr}"
);
assert!(
stderr.contains("(stop #2)"),
"the second pass is numbered: {stderr}"
);
assert!(
stderr.contains("total: Int = 4") && stderr.contains("total: Int = 11"),
"each stop is its own pass's state: {stderr}"
);
}
#[test]
fn quitting_a_stop_detaches_instead_of_killing_the_program() {
let (code, stdout, stderr) = run_stop_with_cmds("breakpoint_loop.px", "quit\n");
assert_eq!(code, 0, "the program was not killed\nstderr: {stderr}");
assert_eq!(stdout.trim(), "11", "it ran to its own answer");
assert!(
stderr.contains("will not stop again"),
"the detach is stated: {stderr}"
);
assert!(
!stderr.contains("(stop #2)"),
"the second pass did not stop: {stderr}"
);
}
#[test]
fn a_stop_refuses_p_and_restart_with_a_reason() {
let (code, _stdout, stderr) =
run_stop_with_cmds("breakpoint_once.px", "p doubled\nrestart\ncontinue\n");
assert_eq!(code, 0);
assert!(
stderr.contains("stopped, not faulted"),
"`p` explains itself: {stderr}"
);
assert!(
stderr.contains("live frames to return to"),
"`restart` explains itself: {stderr}"
);
assert!(
!stderr.contains("unknown command"),
"neither is pretended away: {stderr}"
);
}