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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//! Which cipher forms the **grammar** understands, and which are transpiler-only.
//!
//! §4.3's goal is that the agent surface be parsed by the real lexer/parser, with
//! the `.aeg` transpiler reduced to at most a thin legacy shim. Phase 5 retired
//! the 10-pass pipeline to a single tokenizing `scan`, and several forms
//! (`|.field` projection, SI suffixes, `~x: body`, `?match`, `if`-expressions)
//! moved into the grammar for real.
//!
//! What was left was recorded in prose — "`\x:`/`~.field`/`>`-pipe and the
//! `!try`/`^cond` token overloads stay transpiler-only" — and prose is exactly
//! what goes stale. It did: the roadmap kept describing the blocker in terms of
//! `expand_lambdas`/`expand_pipelines`, functions deleted when Phase 5 landed.
//!
//! So this file makes the division mechanical instead. Each form is fed to the
//! **parser** directly. A form that stays transpiler-only must fail to parse (or
//! parse to something other than what the cipher means); a form the grammar has
//! adopted must parse. Either way, moving one across the line makes a test here
//! fail and say so, rather than leaving a paragraph quietly wrong.
use aethershell::env::Env;
use aethershell::transpile::agentic::transpile_agentic_to_ae;
use aethershell::value::Value;
/// Does the real grammar accept this source on its own?
fn grammar_accepts(src: &str) -> bool {
aethershell::parser::parse_program(src).is_ok()
}
/// Evaluate `src` through the real grammar with **no** transpiler in front of it.
///
/// `Err` covers both "the parser has no production for this" and "it parsed but
/// meant something the evaluator rejected".
fn direct_eval(src: &str) -> Result<Value, String> {
let stmts = aethershell::parser::parse_program(src).map_err(|e| e.to_string())?;
let mut env = Env::new();
aethershell::eval::eval_program(&stmts, &mut env).map_err(|e| e.to_string())
}
/// Evaluate through the transpiler, the way the `.aeg` surface actually runs.
fn eval_aeg(src: &str) -> Value {
let ae = transpile_agentic_to_ae(src).expect("transpile");
let stmts = aethershell::parser::parse_program(&ae).expect("parse transpiled output");
let mut env = Env::new();
aethershell::eval::eval_program(&stmts, &mut env).expect("eval")
}
#[test]
fn the_forms_the_grammar_has_adopted_parse_without_the_transpiler() {
// Each of these was moved into the real grammar during Phase 5. If one stops
// parsing, the grammar has regressed and the transpiler is silently carrying
// it again.
for src in [
"[{a: 1}, {a: 2}] | .a", // |.field projection
"[1, 2, 3] | map(~x: x * 2)", // ~x: body lambda
"let n = 1k;", // SI suffix in the lexer
"let v = if true { 1 } else { 2 };", // if-expression
] {
assert!(
grammar_accepts(src),
"the grammar used to accept `{src}` directly — Phase 5 moved it in, \
and something has taken it back out"
);
}
}
#[test]
fn the_transpiler_only_ciphers_are_still_transpiler_only() {
// The honest statement of what blocks §4.3: the parser does not give these
// forms the meaning the cipher does, so the transpiler cannot become a pure
// shim over the grammar until it does.
//
// The test is *meaning*, not parseability, and `>` is why. `[1,2,3] > len()`
// parses perfectly well — as a greater-than comparison. It is a valid program
// that means something else entirely, which is a worse failure than a syntax
// error and would have been invisible to a `parse_program(..).is_err()`
// check. So: run it both ways and require them to disagree.
let cases: [(&str, &str); 2] = [
(r"[1, 2, 3] | map(\x: x * 2)", "backslash lambda"),
("[1, 2, 3] > len()", "`>`-as-pipe"),
];
for (src, what) in cases {
let through_transpiler = eval_aeg(src);
let direct = direct_eval(src);
assert_ne!(
direct.as_ref().ok(),
Some(&through_transpiler),
"the grammar now gives {what} (`{src}`) the same meaning the cipher \
has. That is progress, not a failure — move it to the adopted list \
above, and update §4.3 and the phase-5 row, which call it \
transpiler-only"
);
}
}
#[test]
fn a_cipher_the_grammar_reads_differently_is_worse_than_one_it_rejects() {
// Worth stating on its own, because it is the reason §4.3 cannot be closed by
// simply deleting the transpiler. `\x:` fails to parse — loud, safe. `>`
// parses as a comparison — silent, and the same text means two different
// programs depending on which surface reads it.
assert!(
!grammar_accepts(r"[1, 2, 3] | map(\x: x * 2)"),
"the backslash lambda should have no production at all"
);
assert!(
grammar_accepts("[1, 2, 3] > len()"),
"`>` is a comparison operator in the grammar, so this parses"
);
assert_ne!(
direct_eval("[1, 2, 3] > len()").ok(),
Some(Value::Int(3)),
"read by the grammar this is a comparison, not a pipe — if it ever \
evaluates to 3 the two surfaces have converged and the roadmap should \
say so"
);
assert_eq!(
eval_aeg("[1, 2, 3] > len()"),
Value::Int(3),
"read by the transpiler it is a pipe"
);
}
#[test]
fn the_transpiler_still_gives_those_ciphers_their_meaning() {
// The other half: "transpiler-only" has to mean the transpiler *does* handle
// them, not that they are broken everywhere.
assert_eq!(
eval_aeg(r"[1, 2, 3] | map(\x: x * 2)"),
Value::Array(vec![Value::Int(2), Value::Int(4), Value::Int(6)]),
"the backslash lambda is the transpiler's job and must still work"
);
assert_eq!(
eval_aeg("[1, 2, 3] > len()"),
Value::Int(3),
"`>`-as-pipe is the transpiler's job and must still work"
);
}
#[test]
fn the_ten_pass_pipeline_really_is_gone() {
// The claim that went stale. Phase 5 deleted all 14 `expand_*`/
// `preprocess_ultra` functions, but the roadmap kept naming
// `expand_lambdas`/`expand_pipelines` as the blocker for another two
// sessions. Assert against the source so the next reader gets a fact.
let src = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/transpile/agentic.rs"
))
.expect("src/transpile/agentic.rs is readable");
for gone in [
"fn expand_lambdas",
"fn expand_pipelines",
"fn expand_si_suffixes",
"fn expand_match",
"fn preprocess_ultra",
] {
assert!(
!src.contains(gone),
"`{gone}` is back. The retired 10-pass pipeline was replaced by a \
single left-to-right `scan`; re-introducing a pass reopens the \
ordering hazard that retirement removed"
);
}
}