RED: io/print_str must print exactly the bytes of its argument — refs #29
Two RED E2E tests that pin the byte-faithful-print property of io/print_str. Both fail today because the codegen at crates/ailang-codegen/src/lib.rs:2754 lowers the op to a C `@puts` call, which writes the string plus a trailing newline. - io_print_str_does_not_append_auto_newline: (seq (do io/print_str "a") (do io/print_str "b")) → stdout "ab" current: "a\nb\n" - io_print_str_does_not_double_embedded_newline: (do io/print_str "x\n") → stdout "x\n" current: "x\n\n" Assertions compare RAW stdout (no .trim()) — the byte-level property IS the assertion target. Empirically caught in the cross-model-authoring naming-A/B run on 2026-05-21 (experiments/2026-05-21-naming-ab/runs/r1/): Qwen3- Coder repeatedly wrote `(do io/print_str "text\n")` with explicit \n, expecting byte-faithful output, and got doubled newlines — failing every t3_main_prints stdout match across all three cohorts. GREEN side (codegen swap @puts → fputs(s, @stdout) + sweep of existing fixtures that implicitly relied on the auto-newline) is the next commit, via implement mini-mode. refs #29
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
//! RED tests for Gitea #29: `io/print_str` must print exactly the
|
||||
//! bytes of its argument with no implicit trailing newline.
|
||||
//!
|
||||
//! Today the codegen at `crates/ailang-codegen/src/lib.rs:2734-2754`
|
||||
//! lowers `(do io/print_str s)` to a C `@puts(ptr)` call. `puts`
|
||||
//! writes the C string PLUS a trailing newline, so `io/print_str` is
|
||||
//! de-facto `println_str`. That is undocumented in the design ledger
|
||||
//! and was empirically caught in the cross-model-authoring naming-A/B
|
||||
//! run on 2026-05-21: Qwen3-Coder repeatedly wrote
|
||||
//! `(do io/print_str "text\n")` with explicit `\n`, expecting the
|
||||
//! bytes to be printed literally — and got doubled newlines, failing
|
||||
//! every t3_main_prints stdout match across all three cohorts.
|
||||
//!
|
||||
//! These tests will turn GREEN once the codegen swaps the `@puts`
|
||||
//! call for `fputs(s, stdout)` (Option 1 from Gitea #29). Until
|
||||
//! then they pin the symptom: two single-character prints emit
|
||||
//! `"ab"`, not `"a\nb\n"`; one print of `"x\n"` emits `"x\n"`, not
|
||||
//! `"x\n\n"`.
|
||||
//!
|
||||
//! NOTE: Unlike `show_print_e2e.rs` these assertions compare RAW
|
||||
//! stdout — no `.trim()`. The whole point of #29 is the byte-level
|
||||
//! property; trimming would mask both bugs.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../examples")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn build_and_run_raw(fixture: &str) -> String {
|
||||
let src = fixture_path(fixture);
|
||||
let out = std::env::temp_dir().join(format!("ail_{}.bin", fixture.replace('.', "_")));
|
||||
|
||||
let build = Command::new(env!("CARGO_BIN_EXE_ail"))
|
||||
.args(["build", src.to_str().unwrap(), "-o", out.to_str().unwrap()])
|
||||
.output()
|
||||
.expect("ail build");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"ail build failed for {fixture}:\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&build.stdout),
|
||||
String::from_utf8_lossy(&build.stderr),
|
||||
);
|
||||
|
||||
let run = Command::new(&out).output().expect("run binary");
|
||||
assert!(
|
||||
run.status.success(),
|
||||
"binary exited non-zero:\nstdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
String::from_utf8_lossy(&run.stderr),
|
||||
);
|
||||
// Raw stdout — no trimming. The bug is at the byte level.
|
||||
String::from_utf8(run.stdout).expect("stdout is valid UTF-8")
|
||||
}
|
||||
|
||||
/// Property: two consecutive `(do io/print_str ...)` calls with
|
||||
/// single-character no-newline arguments produce exactly the
|
||||
/// concatenation of those characters on stdout — no separator, no
|
||||
/// trailing newline.
|
||||
///
|
||||
/// Fixture body: `(seq (do io/print_str "a") (do io/print_str "b"))`.
|
||||
///
|
||||
/// Expected (post-fix): `"ab"`.
|
||||
/// Today (pre-fix): `"a\nb\n"` — `@puts` appends a newline per call.
|
||||
#[test]
|
||||
fn io_print_str_does_not_append_auto_newline() {
|
||||
let stdout = build_and_run_raw("print_str_no_auto_newline.ail");
|
||||
assert_eq!(stdout, "ab", "got: {stdout:?}");
|
||||
}
|
||||
|
||||
/// Property: a single `(do io/print_str "x\n")` emits exactly one
|
||||
/// newline — the one the author embedded. The codegen must not
|
||||
/// double it.
|
||||
///
|
||||
/// Fixture body: `(do io/print_str "x\n")`.
|
||||
///
|
||||
/// Expected (post-fix): `"x\n"`.
|
||||
/// Today (pre-fix): `"x\n\n"` — embedded `\n` plus auto-newline
|
||||
/// from `@puts`.
|
||||
#[test]
|
||||
fn io_print_str_does_not_double_embedded_newline() {
|
||||
let stdout = build_and_run_raw("print_str_embedded_newline_no_doubling.ail");
|
||||
assert_eq!(stdout, "x\n", "got: {stdout:?}");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
(module print_str_embedded_newline_no_doubling
|
||||
(fn main
|
||||
(doc "RED fixture for Gitea #29 (companion to print_str_no_auto_newline.ail): when the author embeds a single `\\n` in the argument, exactly one newline must reach stdout — not two. Today the codegen lowers io/print_str to `@puts(ptr)` which appends its own newline, so this program emits \"x\\n\\n\" (two newlines) instead of the author-intended \"x\\n\". Pins the no-doubling property as a separate axis from the no-auto-newline property: even authors who want a newline must not get a doubled one.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body (do io/print_str "x\n"))))
|
||||
@@ -0,0 +1,6 @@
|
||||
(module print_str_no_auto_newline
|
||||
(fn main
|
||||
(doc "RED fixture for Gitea #29: `io/print_str` must print exactly the bytes of its argument with NO auto-newline. Today the codegen lowers this op to a C `@puts(ptr)` call, which writes the string PLUS a trailing newline — so two consecutive prints of single-character strings emit \"a\\nb\\n\" instead of the expected \"ab\". This is the de-facto-println behaviour LLM authors (Qwen3-Coder, naming-A/B run 2026-05-21) trip over when they write explicit `\\n` and get doubled newlines. Property protected once the codegen swaps `@puts` for `fputs(s, stdout)`: io/print_str is a byte-faithful print, the author chooses whether to embed a newline.")
|
||||
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||
(params)
|
||||
(body (seq (do io/print_str "a") (do io/print_str "b")))))
|
||||
Reference in New Issue
Block a user