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
// Regression tests pinning the contract that EVERY builtin alias (`head`,
// `length`, `filter`, ...) is rejected as a binding LHS or user-function
// name with `ILO-P011`, mirroring the existing short-form alias guards for
// `rng` and `rand`.
//
// Originating: rerun-prompt-generator and changelog-validator (rerun12) both
// bound `head=...`, and the alias resolver silently rewrote later call
// positions to `hd ...`, emitting empty output with no diagnostic. The
// pre-existing P011 binding-LHS guard used `Builtin::is_builtin(name)` —
// which only matches canonical names — and the alias guards were special-
// cased to `rng` / `rand` only. Every long-form alias (`head`/`length`/
// `tail`/`filter`/`concat`/...) leaked through.
//
// Contracts locked in here:
// 1. `head=...` at top level → ILO-P011 (parse_decl site).
// 2. `head=...` inside a function body → ILO-P011 (parse_stmt site).
// 3. `head>n;42` as a user fn-decl → ILO-P011 (parse_fn_decl site).
// 4. Every entry in `BUILTIN_ALIASES` is rejected in all three positions
// (drift guard — if a new alias is added without the guard catching it,
// this test goes red).
use ilo::ast::all_builtin_aliases;
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn parse_fails_with_p011(src: &str) -> String {
let out = ilo().args([src]).output().expect("failed to run ilo");
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(!out.status.success(), "expected parse failure for {src:?}");
assert!(
stderr.contains("ILO-P011"),
"expected ILO-P011 for {src:?}, got: {stderr}"
);
stderr
}
#[test]
fn head_rejected_as_top_level_binding() {
let stderr = parse_fails_with_p011("head=5");
assert!(
stderr.contains("`head`") && stderr.contains("`hd`"),
"diagnostic must name alias and canonical, got: {stderr}"
);
}
#[test]
fn head_rejected_as_local_binding_inside_fn_body() {
// Same shape as the rerun-prompt-generator / changelog-validator papercut.
let stderr = parse_fails_with_p011("main>n;head=5;1");
assert!(
stderr.contains("`head`") && stderr.contains("`hd`"),
"diagnostic must name alias and canonical, got: {stderr}"
);
}
#[test]
fn head_rejected_as_user_function_name() {
let stderr = parse_fails_with_p011("head x:n>n;+x 1");
assert!(
stderr.contains("`head`") && stderr.contains("`hd`"),
"diagnostic must name alias and canonical, got: {stderr}"
);
}
#[test]
fn length_rejected_in_all_three_positions() {
parse_fails_with_p011("length=5");
parse_fails_with_p011("main>n;length=5;1");
parse_fails_with_p011("length x:n>n;+x 1");
}
#[test]
fn filter_rejected_in_all_three_positions() {
parse_fails_with_p011("filter=5");
parse_fails_with_p011("main>n;filter=5;1");
parse_fails_with_p011("filter x:n>n;+x 1");
}
#[test]
fn every_alias_rejected_as_binding_and_fn_decl() {
// Drift guard: walk the entire `BUILTIN_ALIASES` table and confirm each
// entry is rejected at all three guard sites. New aliases land with the
// protection automatically; missing protection fails this test loud.
for (alias, canonical) in all_builtin_aliases() {
// Skip aliases that contain underscores: ilo identifiers use hyphens,
// so `regex_all` / `regex_sub` are lexically invalid as ilo source and
// would produce ILO-L002 rather than ILO-P011. The resolve_alias guard
// in the parser is still exercised for valid-identifier aliases.
if alias.contains('_') {
continue;
}
// Top-level binding
let top = format!("{alias}=5");
let stderr = parse_fails_with_p011(&top);
assert!(
stderr.contains(alias) && stderr.contains(canonical),
"top-level binding diagnostic for `{alias}` must name canonical `{canonical}`, got: {stderr}"
);
// Local binding inside fn body
let local = format!("main>n;{alias}=5;1");
let stderr = parse_fails_with_p011(&local);
assert!(
stderr.contains(alias) && stderr.contains(canonical),
"local binding diagnostic for `{alias}` must name canonical `{canonical}`, got: {stderr}"
);
// User fn-decl
let fn_decl = format!("{alias} x:n>n;+x 1");
let stderr = parse_fails_with_p011(&fn_decl);
assert!(
stderr.contains(alias) && stderr.contains(canonical),
"fn-decl diagnostic for `{alias}` must name canonical `{canonical}`, got: {stderr}"
);
}
}