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
//! #80 P3 async intrinsic honest-degradation gate (CLI end-to-end).
//!
//! Synth lowers exactly ONE async intrinsic op — `error-context.drop` (a
//! scalar handle op) — and LOUD-DECLINES the rest by name, INCLUDING the other
//! error-context ops (`.new`/`.debug-message`) which carry a linmem message
//! pointer. This test exercises the real `synth compile` binary:
//!
//! - the LOWERED family (`error-context.drop`) compiles to a relocatable ELF
//! whose symtab carries the field-name as an UNDEFINED symbol — the AAPCS
//! `BL` call site the host linker resolves against kiln-builtins (the
//! "executes vs reference" ABI contract, checked via symtab per the
//! read-symtab-not-disasm lesson);
//! - each DECLINED family (`stream`, `future`, `waitable-set`, `task`) makes
//! the compile FAIL with a machine reason naming the family.
use std::path::{Path, PathBuf};
use std::process::Command;
// #977 RQ-59-FRESHNESS: nothing here parses an artifact until the artifact is
// proven to be THIS invocation's output — see `artifact_guard`.
mod artifact_guard;
/// Locate the `synth` binary the way every other CLI test does.
///
/// This used to walk two parents up from `current_exe()` and append `synth`,
/// which happens to be right for the plain `target/debug/deps/<test>` layout and
/// WRONG for any other. Under `cargo llvm-cov` the test binary lives at
/// `target/llvm-cov-target/debug/build/synth-cli/<hash>/out/<test>`, so the walk
/// produced `…/<hash>/synth` — a path that does not exist — and BOTH tests in
/// this file failed for a missing binary rather than for anything they assert.
/// That kept `Code Coverage` red repo-wide while the required `Test` job (plain
/// layout) stayed green: a real gate, failing for a reason unrelated to the code.
///
/// `CARGO_BIN_EXE_<name>` is set by Cargo at compile time to the actual path of
/// the built binary, so it is correct under every target-dir layout. Same
/// mechanism as the other CLI integration tests in this directory.
fn synth_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_synth"))
}
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf()
}
fn fixture(name: &str) -> PathBuf {
workspace_root()
.join("tests")
.join("integration")
.join(name)
}
/// The LOWERED family compiles and emits a field-name BL call site.
#[test]
fn error_context_family_is_lowered_and_emits_field_name_symbol() {
let wat = fixture("async_error_context.wat");
assert!(wat.exists(), "fixture missing: {}", wat.display());
// #977: unique per call + remove-first + status/exists/non-empty guards.
let out = artifact_guard::unique_artifact("synth_async_ec", "o");
let mut cmd = Command::new(synth_binary());
cmd.args([
"compile",
wat.to_str().unwrap(),
"--no-optimize",
"--relocatable",
"-o",
out.to_str().unwrap(),
]);
let data =
artifact_guard::compile_bytes_or_panic(&mut cmd, &out, "error-context should compile");
// The field-name `error-context.drop` must appear as the BL target symbol
// in the ELF (the host-link contract). We scan the raw bytes for the
// symbol string — the relocatable path records it as an UNDEF symbol.
assert_eq!(&data[0..4], b"\x7fELF", "not an ELF");
let sym = b"error-context.drop";
assert!(
data.windows(sym.len()).any(|w| w == sym),
"field-name BL target 'error-context.drop' must be in the ELF symbol \
table (the AAPCS call site the host linker resolves)"
);
}
/// Every DECLINED family fails the compile with a named machine reason.
#[test]
fn declined_families_reject_loudly() {
// (fixture, family substring the diagnostic must name)
let cases = [
("async_stream_declined.wat", "stream"),
("async_future_declined.wat", "future"),
("async_waitable_declined.wat", "waitable"),
("async_task_declined.wat", "task"),
// SOUNDNESS: error-context.new carries a linmem message pointer — it is
// NOT the scalar op error-context.drop, so it is declined (buffer class).
("async_error_context_new_declined.wat", "buffer"),
];
for (fixture_name, family) in cases {
let wat = fixture(fixture_name);
assert!(wat.exists(), "fixture missing: {}", wat.display());
let out = std::env::temp_dir().join(format!("synth_async_decl_{family}.o"));
let result = Command::new(synth_binary())
.args([
"compile",
wat.to_str().unwrap(),
"--no-optimize",
"-o",
out.to_str().unwrap(),
])
.output()
.expect("run synth");
assert!(
!result.status.success(),
"{fixture_name}: declined family must FAIL the compile"
);
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
stderr.contains("#80 async-intrinsic decline"),
"{fixture_name}: decline must carry the #80 machine reason; got: {stderr}"
);
assert!(
stderr.contains(family),
"{fixture_name}: decline must name the '{family}' family; got: {stderr}"
);
}
}