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
// Regression tests for the `-0` literal hijacking start-of-decl parse.
//
// Originating bug: scientific-researcher rerun9. `- -0 a bo` tripped
// ILO-P020 because the lexer kept `-0` as a glued `Number(-0.0)` token
// (the neg-literal split predicate didn't include `Minus` as a
// fresh-expression context). The outer `-` then parsed as binary subtract
// with operands `-0` and `a`, leaving `bo` orphaned at statement level
// where the multi-fn parser mis-classified it as a new decl header.
// Workaround was binding `na=- 0 a` first - a token tax across every
// persona that reaches for unary negation in hand-rolled numerics
// (Taylor series, RK4, log-fit, distance kernels).
//
// Fix: lexer's neg-literal split predicate now treats `Minus` as a
// fresh-expression context, so `- -0 a bo` re-lexes as
// `Minus, Minus, Number(0), Ident(a), Ident(bo)` and parses as
// `Subtract(Subtract(0, a), bo)` = `-a - bo`. Same for the wider
// `*-0 k s` / `t=-0 /t 6` family.
//
// Collateral semantics change for `- -N M` (N != 0): from
// `Subtract(-N, M)` (pre-fix) to `Negate(Subtract(N, M))` (post-fix).
// Zero in-repo usages of this shape before the fix; pinned explicitly in
// the new reading by the `minus_minus_three_five_*` tests below.
//
// Variants exercised across all three engines (tree, VM, cranelift):
// - `- -0 a bo` : the originating shape, returns `-a - bo`
// - `- -0 a` : two-operand form, returns `-a`
// - `- -3 5` : non-zero collateral, returns `2` (new reading)
// - workaround `na=- 0 a` still works (no regression)
use std::process::Command;
fn ilo() -> Command {
Command::new(env!("CARGO_BIN_EXE_ilo"))
}
fn run(engine: &str, src: &str, args: &[&str]) -> String {
let mut cmd = ilo();
cmd.arg(src).arg(engine);
for a in args {
cmd.arg(a);
}
let out = cmd.output().expect("failed to run ilo");
assert!(
out.status.success(),
"ilo {engine} failed for `{src}` args={args:?}: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
// Originating repro shape.
const DECL: &str = "f a:n bo:n>n;- -0 a bo";
// Two-operand form: `- -0 a` = `Subtract(Negate(0), a)` or equivalently
// `Negate(Subtract(0, a))` = `Subtract(0, Subtract(0, a))` - all reduce to
// `-(-a) = a`? No: `-, -, 0, a` parses outer-Minus consuming inner subtract
// `Subtract(0, a) = -a` then `can_start_operand` is false at EOF, so falls
// into Negate arm: `Negate(Subtract(0, a))` = `Negate(-a)` = `a`.
const NEGATE_ONLY: &str = "f a:n>n;- -0 a";
// Non-zero collateral: pinned to the post-fix reading
// `Negate(Subtract(3, 5))` = `Negate(-2)` = `2`. Pre-fix this was
// `Subtract(-3, 5) = -8`. Zero in-repo usages; new reading is the
// natural left-associative parse.
const NONZERO: &str = "f>n;- -3 5";
// Documented workaround: `na=- 0 a` must continue to work.
const WORKAROUND: &str = "f a:n>n;na=- 0 a;na";
fn check_all(engine: &str) {
// `- -0 a bo` with a=5, bo=7 -> `(-5) - 7` = -12.
assert_eq!(
run(engine, DECL, &["f", "5", "7"]),
"-12",
"decl: a=5 bo=7 engine={engine}"
);
// `- -0 a bo` with a=3, bo=2 -> `(-3) - 2` = -5.
assert_eq!(
run(engine, DECL, &["f", "3", "2"]),
"-5",
"decl: a=3 bo=2 engine={engine}"
);
// `- -0 a bo` with a=0, bo=0 -> 0.
assert_eq!(
run(engine, DECL, &["f", "0", "0"]),
"0",
"decl: a=0 bo=0 engine={engine}"
);
// `- -0 a bo` with negative a -> double negation.
// a=-4, bo=1 -> (-(-4)) - 1 = 4 - 1 = 3.
assert_eq!(
run(engine, DECL, &["f", "-4", "1"]),
"3",
"decl: a=-4 bo=1 engine={engine}"
);
// `- -0 a` with a=5 -> Negate(Subtract(0, 5)) = Negate(-5) = 5.
assert_eq!(
run(engine, NEGATE_ONLY, &["f", "5"]),
"5",
"negate-only: a=5 engine={engine}"
);
// `- -0 a` with a=-3 -> Negate(Subtract(0, -3)) = Negate(3) = -3
// wait: Subtract(0, -3) = 0 - (-3) = 3, Negate(3) = -3. Yes, -3.
assert_eq!(
run(engine, NEGATE_ONLY, &["f", "-3"]),
"-3",
"negate-only: a=-3 engine={engine}"
);
// `- -3 5` -> Negate(Subtract(3, 5)) = Negate(-2) = 2 (post-fix reading).
assert_eq!(
run(engine, NONZERO, &["f"]),
"2",
"nonzero: - -3 5 engine={engine}"
);
// Workaround `na=- 0 a` still produces unary-negate semantics.
assert_eq!(
run(engine, WORKAROUND, &["f", "7"]),
"-7",
"workaround: na=- 0 a with a=7 engine={engine}"
);
assert_eq!(
run(engine, WORKAROUND, &["f", "-2"]),
"2",
"workaround: na=- 0 a with a=-2 engine={engine}"
);
}
#[test]
fn minus_zero_decl_tree() {
check_all("--run-tree");
}
#[test]
fn minus_zero_decl_vm() {
check_all("--run-vm");
}
#[test]
#[cfg(feature = "cranelift")]
fn minus_zero_decl_cranelift() {
check_all("--jit");
}