Skip to content

Commit aa6b7ff

Browse files
committed
Add tests for cgen versions. Add not done roadmaps
1 parent 7c5a25b commit aa6b7ff

8 files changed

Lines changed: 446 additions & 1 deletion

File tree

crates/xb-compiler/src/c_emit_expr.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ pub(crate) fn emit_expr(expr: &IrExpr, out: &mut String) {
196196
}
197197
emit_expr(arg, out);
198198
}
199+
out.push(')');
199200
} else if is_type_conversion(name) {
200201
emit_type_conversion(name, &args[0], out, emit_expr);
201202
} else if name == "HEXX$" {
@@ -214,13 +215,16 @@ pub(crate) fn emit_expr(expr: &IrExpr, out: &mut String) {
214215
}
215216
emit_expr(arg, out);
216217
}
218+
out.push(')');
217219
} else if name == "EXTS"
218220
|| name == "EXTU"
219221
|| name == "CLR"
220222
|| name == "SET"
221223
|| name == "MAKE"
222224
{
223225
crate::c_emit_bitops::emit_bit_op_call(name, args, out);
226+
} else if name == "MID$" && args.len() == 2 {
227+
crate::c_emit_str2::emit_mid2(args, out, emit_expr);
224228
} else {
225229
emit_c_function_name(name, out);
226230
out.push('(');

crates/xb-compiler/src/c_emit_str2.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ pub(crate) fn emit_clip(
7373
}
7474

7575
/// Emits C code for 2-arg MID$(s$, start) -> xb_mid2(s, start).
76-
#[allow(dead_code)]
7776
pub(crate) fn emit_mid2(args: &[IrExpr], out: &mut String, emit_fn: impl Fn(&IrExpr, &mut String)) {
7877
out.push_str("xb_mid2(");
7978
emit_fn(&args[0], out);

crates/xb-compiler/src/c_runtime.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ out.push_str("#include <stdint.h>\n");
214214
out.push_str(" return xb_strdup(buf);\n");
215215
out.push_str("}\n");
216216
out.push_str("static char* xb_tab(int cur, int col) { if (col <= cur) return xb_strdup(\"\"); int n = col - cur; char* r = malloc(n + 1); memset(r, ' ', n); r[n] = 0; return r; }\n");
217+
out.push_str("static char* xb_tab_0(int col) { return xb_tab(0, col); }\n");
217218
out.push_str("static int xb_isdata(const char* s) { return (s && s[0]) ? -1 : 0; }\n");
218219
out.push_str("static char* xb_inkey(void) {\n");
219220
out.push_str(" int c = getchar();\n");

crates/xb-compiler/src/c_runtime_math.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub(crate) fn emit_math_functions(out: &mut String) {
88
out.push_str("static double xb_acos(double v) { return acos(v); }\n");
99
out.push_str("static double xb_asin(double v) { return asin(v); }\n");
1010
out.push_str("static double xb_atan(double v) { return atan(v); }\n");
11+
out.push_str("static double xb_atn(double v) { return atan(v); }\n");
1112
out.push_str("static double xb_atan2(double a, double b) { return atan2(a, b); }\n");
1213
out.push_str("static double xb_log10(double v) { return log10(v); }\n");
1314
out.push_str("static double xb_power(double a, double b) { return pow(a, b); }\n");
@@ -39,8 +40,10 @@ pub(crate) fn emit_math_functions(out: &mut String) {
3940
out.push_str("static double xb_round(double v) { return round(v); }\n");
4041
out.push_str("static double xb_timer(void) { time_t t = time(NULL); struct tm *tm = localtime(&t); return tm->tm_hour*3600.0 + tm->tm_min*60.0 + tm->tm_sec; }\n");
4142
out.push_str("static char* xb_time(void) { time_t t = time(NULL); struct tm *tm = localtime(&t); char* r = malloc(9); snprintf(r, 9, \"%02d:%02d:%02d\", tm->tm_hour, tm->tm_min, tm->tm_sec); return r; }\n");
43+
out.push_str("static char* xb_date(void) { time_t t = time(NULL); struct tm *tm = localtime(&t); char* r = malloc(11); snprintf(r, 11, \"%04d-%02d-%02d\", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday); return r; }\n");
4244
out.push_str("static char* xb_hexx(int v, int w) { char* r = malloc(34); r[0]='0'; r[1]='x'; if (w > 0) snprintf(r+2, 32, \"%0*X\", w, v); else snprintf(r+2, 32, \"%X\", v); return r; }\n");
4345
out.push_str("static char* xb_rjust(const char* s, int w) { int len = strlen(s); if (len >= w) return xb_strdup(s); char* r = malloc(w + 1); int pad = w - len; for (int i = 0; i < pad; i++) r[i] = ' '; memcpy(r + pad, s, len); r[w] = 0; return r; }\n");
46+
out.push_str("static char* xb_ljust(const char* s, int w) { int len = strlen(s); if (len >= w) return xb_strdup(s); char* r = malloc(w + 1); memcpy(r, s, len); for (int i = len; i < w; i++) r[i] = ' '; r[w] = 0; return r; }\n");
4447
out.push_str("static char* xb_rclip1(const char* s) { int len = strlen(s); while (len > 0 && isspace(s[len-1])) len--; char* r = malloc(len + 1); memcpy(r, s, len); r[len] = 0; return r; }\n");
4548
out.push_str("static char* xb_rclip2(const char* s, int n) { int len = strlen(s); if (n >= len) return xb_strdup(\"\"); int newlen = len - n; char* r = malloc(newlen + 1); memcpy(r, s, newlen); r[newlen] = 0; return r; }\n");
4649
out.push_str("static char* xb_lclip1(const char* s) { int i = 0; while (s[i] && isspace(s[i])) i++; return xb_strdup(s + i); }\n");
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//! Cross-generator sync between the Rust reference C emitter (`CEmitter`) and
2+
//! the self-hosted C generator (`selfhost/cgen.x`).
3+
//!
4+
//! There are two independent implementations of the same IR -> C contract:
5+
//! * `CEmitter` (Rust, in `xb-compiler`) — the reference.
6+
//! * `cgen.x` (XBasic, in `selfhost/`) — the self-hosted generator that the
7+
//! native bootstrap actually ships.
8+
//!
9+
//! Every other cgen/bootstrap test checks ONE generator against the interpreter
10+
//! or the golden output. Nothing pins the two generators to EACH OTHER, so they
11+
//! can silently drift: a codegen rule fixed in one but not the other only breaks
12+
//! a test if some corpus program happens to exercise it. These tests close that
13+
//! gap by requiring both generators to agree, program-for-program.
14+
//!
15+
//! Sync is asserted on OBSERVABLE BEHAVIOR (native run output), not on the
16+
//! emitted C text: the two generators' fixed runtime *preludes* are not yet
17+
//! byte-identical (helper ordering/formatting and a few semantic helper diffs —
18+
//! tracked in `docs/16-cgen-cemitter-sync-roadmap.md`, item CG-PRELUDE). Output
19+
//! equivalence is the contract that actually governs a correct bootstrap.
20+
21+
mod common;
22+
23+
use std::fs;
24+
use std::io::Write;
25+
use std::path::{Path, PathBuf};
26+
use std::process::{Command, Stdio};
27+
use xb_compiler::{CEmitter, FrontendUnit, TextIrEmitter, TextIrParser};
28+
use xb_runtime::Interpreter;
29+
30+
fn root() -> PathBuf {
31+
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
32+
}
33+
34+
/// Build the native `cgen` executable from `selfhost/cgen.x` using the Rust
35+
/// `CEmitter` (this is exactly how the native bootstrap seeds its C generator).
36+
fn build_native_cgen(tmp: &Path) -> PathBuf {
37+
let cgen_src = fs::read_to_string(root().join("selfhost/cgen.x")).expect("read cgen.x");
38+
let cgen_prog = FrontendUnit::parse(&cgen_src)
39+
.expect("parse cgen.x")
40+
.lower_ir()
41+
.expect("lower cgen.x");
42+
let cgen_c = CEmitter::new().emit_program(&cgen_prog);
43+
let c_path = tmp.join("cgen.c");
44+
let exe = tmp.join("cgen");
45+
fs::write(&c_path, &cgen_c).expect("write cgen.c");
46+
let cc = Command::new(common::cc::cc())
47+
.args(["-o", exe.to_str().unwrap(), c_path.to_str().unwrap()])
48+
.output()
49+
.expect("run cc for cgen");
50+
assert!(
51+
cc.status.success(),
52+
"cc cgen failed: {}",
53+
String::from_utf8_lossy(&cc.stderr)
54+
);
55+
exe
56+
}
57+
58+
/// Feed text IR to the native cgen on stdin; return the emitted C source bytes.
59+
fn cgen_emit(cgen_exe: &Path, ir: &str) -> Vec<u8> {
60+
let mut child = Command::new(common::exe_path(cgen_exe))
61+
.stdin(Stdio::piped())
62+
.stdout(Stdio::piped())
63+
.stderr(Stdio::piped())
64+
.spawn()
65+
.expect("spawn cgen");
66+
child
67+
.stdin
68+
.take()
69+
.expect("cgen stdin")
70+
.write_all(ir.as_bytes())
71+
.expect("write IR to cgen");
72+
let out = child.wait_with_output().expect("wait cgen");
73+
assert!(
74+
out.status.success(),
75+
"cgen failed: {}",
76+
String::from_utf8_lossy(&out.stderr)
77+
);
78+
out.stdout
79+
}
80+
81+
/// Compile C source bytes to a native exe, run it with optional stdin, and
82+
/// return stdout decoded byte-faithfully (matching how the goldens were made).
83+
fn compile_and_exec(tmp: &Path, name: &str, c: &[u8], input: Option<&str>) -> String {
84+
let c_path = tmp.join(format!("{name}.c"));
85+
let exe = tmp.join(name);
86+
fs::write(&c_path, c).expect("write c");
87+
let cc = Command::new(common::cc::cc())
88+
.args(["-o", exe.to_str().unwrap(), c_path.to_str().unwrap()])
89+
.output()
90+
.expect("run cc");
91+
assert!(
92+
cc.status.success(),
93+
"cc {name} failed: {}",
94+
String::from_utf8_lossy(&cc.stderr)
95+
);
96+
let mut cmd = Command::new(common::exe_path(&exe));
97+
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
98+
cmd.stdin(if input.is_some() {
99+
Stdio::piped()
100+
} else {
101+
Stdio::null()
102+
});
103+
let mut child = cmd.spawn().expect("spawn native exe");
104+
if let Some(inp) = input {
105+
child
106+
.stdin
107+
.take()
108+
.expect("native stdin")
109+
.write_all(inp.as_bytes())
110+
.expect("write input");
111+
}
112+
let out = child.wait_with_output().expect("wait native exe");
113+
assert!(
114+
out.status.success(),
115+
"native {name} failed: {}",
116+
String::from_utf8_lossy(&out.stderr)
117+
);
118+
out.stdout.iter().map(|&b| b as char).collect()
119+
}
120+
121+
/// The Rust `CEmitter` and the self-hosted `cgen.x` must produce native
122+
/// executables whose output is byte-identical to each other AND to the golden
123+
/// `.out`, for every program in the positive corpus.
124+
///
125+
/// This simultaneously adds the previously-missing coverage of `CEmitter` over
126+
/// the whole corpus (it was only ever run on one hand-written program) and the
127+
/// direct cross-generator equality that locks the two backends together.
128+
#[test]
129+
fn cemitter_and_cgen_agree_on_positive_corpus() {
130+
let tmp = std::env::temp_dir().join("xb_sync_pos_corpus");
131+
fs::create_dir_all(&tmp).expect("mkdir");
132+
let cgen_exe = build_native_cgen(&tmp);
133+
let corpus = root().join("fixtures/corpus/v0.1/positive");
134+
135+
let mut cases: Vec<String> = Vec::new();
136+
for entry in fs::read_dir(&corpus).expect("read_dir positive corpus") {
137+
let path = entry.expect("dir entry").path();
138+
if path.extension().and_then(|e| e.to_str()) == Some("ir") {
139+
cases.push(
140+
path.file_stem()
141+
.expect("stem")
142+
.to_str()
143+
.expect("utf8 stem")
144+
.to_string(),
145+
);
146+
}
147+
}
148+
cases.sort();
149+
assert!(
150+
cases.len() >= 50,
151+
"expected the full positive corpus (>=50 cases), found {}",
152+
cases.len()
153+
);
154+
155+
for stem in &cases {
156+
let ir = fs::read_to_string(corpus.join(format!("{stem}.ir"))).expect("read .ir");
157+
let golden = fs::read_to_string(corpus.join(format!("{stem}.out"))).expect("read .out");
158+
let in_path = corpus.join(format!("{stem}.in"));
159+
let input = if in_path.exists() {
160+
Some(fs::read_to_string(&in_path).expect("read .in"))
161+
} else {
162+
None
163+
};
164+
let input_ref = input.as_deref();
165+
166+
// Rust CEmitter path: text IR -> IrProgram -> C -> native.
167+
let prog = TextIrParser::parse(&ir)
168+
.unwrap_or_else(|e| panic!("TextIrParser failed for {stem}: {e:?}"));
169+
let rust_c = CEmitter::new().emit_program(&prog);
170+
let rust_out = compile_and_exec(&tmp, &format!("{stem}_rust"), rust_c.as_bytes(), input_ref);
171+
172+
// Self-hosted cgen.x path: text IR -> C (native cgen) -> native.
173+
let self_c = cgen_emit(&cgen_exe, &ir);
174+
let self_out = compile_and_exec(&tmp, &format!("{stem}_self"), &self_c, input_ref);
175+
176+
assert_eq!(
177+
rust_out, golden,
178+
"CEmitter-built {stem} output differs from golden .out"
179+
);
180+
assert_eq!(
181+
self_out, golden,
182+
"cgen.x-built {stem} output differs from golden .out"
183+
);
184+
assert_eq!(
185+
rust_out, self_out,
186+
"SYNC BREAK: CEmitter and cgen.x disagree on {stem}"
187+
);
188+
}
189+
let _ = fs::remove_dir_all(&tmp);
190+
}
191+
192+
/// The two generators must also agree on the self-hosting toolchain itself
193+
/// (compiler, lexer, parser, cgen), and both must match the interpreter (the
194+
/// semantic reference) on each tool's own input. This is the sync that directly
195+
/// underwrites the bootstrap: whichever C backend seeds the native tools, the
196+
/// tools behave identically.
197+
#[test]
198+
fn cemitter_and_cgen_agree_on_selfhost_tools() {
199+
let tmp = std::env::temp_dir().join("xb_sync_selfhost_tools");
200+
fs::create_dir_all(&tmp).expect("mkdir");
201+
let cgen_exe = build_native_cgen(&tmp);
202+
203+
for tool in ["compiler", "lexer", "parser", "cgen"] {
204+
let src = fs::read_to_string(root().join(format!("selfhost/{tool}.x")))
205+
.unwrap_or_else(|e| panic!("read selfhost/{tool}.x: {e}"));
206+
let prog = FrontendUnit::parse(&src)
207+
.unwrap_or_else(|e| panic!("parse {tool}: {e:?}"))
208+
.lower_ir()
209+
.unwrap_or_else(|e| panic!("lower {tool}: {e:?}"));
210+
let ir = TextIrEmitter::new().emit_program(&prog);
211+
212+
// Deterministic stdin: the tool's committed `.in` if present, else the
213+
// tool's own text IR (cgen consumes IR, so this is meaningful work).
214+
let in_path = root().join(format!("selfhost/{tool}.in"));
215+
let input = if in_path.exists() {
216+
fs::read_to_string(&in_path).expect("read .in")
217+
} else {
218+
ir.clone()
219+
};
220+
let input_lines: Vec<String> = input.lines().map(String::from).collect();
221+
222+
let rust_c = CEmitter::new().emit_program(&prog);
223+
let rust_out =
224+
compile_and_exec(&tmp, &format!("{tool}_rust"), rust_c.as_bytes(), Some(&input));
225+
226+
let self_c = cgen_emit(&cgen_exe, &ir);
227+
let self_out = compile_and_exec(&tmp, &format!("{tool}_self"), &self_c, Some(&input));
228+
229+
let mut interp = Vec::new();
230+
Interpreter::new()
231+
.execute_main_with_input(&prog, input_lines, &mut interp)
232+
.unwrap_or_else(|e| panic!("interpret {tool}: {e:?}"));
233+
let interp_out: String = interp.into_iter().map(|l| format!("{l}\n")).collect();
234+
235+
assert_eq!(
236+
rust_out, interp_out,
237+
"CEmitter-built {tool} differs from interpreter"
238+
);
239+
assert_eq!(
240+
self_out, interp_out,
241+
"cgen.x-built {tool} differs from interpreter"
242+
);
243+
assert_eq!(
244+
rust_out, self_out,
245+
"SYNC BREAK: CEmitter and cgen.x disagree on selfhost tool {tool}"
246+
);
247+
}
248+
let _ = fs::remove_dir_all(&tmp);
249+
}

0 commit comments

Comments
 (0)