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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! R8-5 — every calc dialect's integer operators cast their `double` operands
//! the way THAT dialect's C does, and the three do not agree.
//!
//! | dialect | bit / shift | MODULO | NINT |
//! |---|---|---|---|
//! | base `calcPerform.c` | `d2i`/`d2ui` (:324-325) | `d2i` (:176-190) since `669a25697`/PR #925 — NaN on zero | `d2i` (:313-317) |
//! | sCalc `sCalcPerform.c` | plain `(long)` (:578-631) | `(long)` (:1121) — error on zero | `(long)` (:730) |
//! | aCalc `aCalcPerform.c` | plain `(int)` (:1355-1357) | `(int)` (:661,:685,:711) — myMAXFLOAT on zero | `(long)` (:839,:1096) |
//!
//! **CBUG-A2 — each engine narrows MODULO and NINT with its OWN dialect's cast**,
//! mirroring that engine's pristine/fixed C. They genuinely disagree because the
//! C widths do: `3e9` is `d2i(3e9) = -1294967296` in base, `(long)3e9 = 3e9` in
//! sCalc, and (for MODULO) `(int)3e9` out-of-range in aCalc. So `3e9 % 7` is `0`
//! (base), `4` (sCalc), `1` (aCalc — its `(int)` saturates to i32::MAX under
//! CBUG-E2, then `% 7`). The structure is a single `cast::nint`/`cast::imod`
//! parameterised by the narrowing fn — one logic, per-engine narrowing.
//!
//! Note aCalc is internally inconsistent: NINT is `(long)` (64-bit) but MODULO
//! is `(int)` (32-bit) — verified in aCalcPerform.c. base is the only engine on
//! `d2i`, because PR #925 is a base-only commit; sCalc/aCalc keep their pristine
//! synApps `(long)`/`(int)`, which the differential oracle also runs.
//!
//! d2i and the plain casts alike stay UNDEFINED in C outside their range
//! (negatives, `>= 2^32`, NaN); on those the port mirrors its own cast owner
//! (base d2i's modular wrap, sCalc/aCalc's saturating `c_long`/`c_int`), never
//! reproducing x86 `cvttsd2si`. Those values carry no claim of C-correctness.
//!
//! This test used to pin the earlier clean no-narrow deviation (`3e9 % 7 == 4`,
//! `NINT(3e9) == 3e9` in every dialect).
use epics_base_rs::calc::{ArrayInputs, NumericInputs, StringInputs, acalc, calc, scalc};
fn n(expr: &str, a: f64) -> f64 {
let mut inp = NumericInputs::new();
inp.vars[0] = a;
calc(expr, &mut inp).expect("compiles and evaluates")
}
/// A constant-only expression (no `A`).
fn nc(expr: &str) -> f64 {
n(expr, 0.0)
}
fn s(expr: &str, a: f64) -> f64 {
let mut inp = StringInputs::new();
inp.num_vars[0] = a;
match scalc(expr, &mut inp).expect("compiles and evaluates") {
epics_base_rs::calc::StackValue::Double(d) => d,
other => panic!("expected a double, got {other:?}"),
}
}
fn a(expr: &str, a0: f64) -> f64 {
let mut inp = ArrayInputs::new(1);
inp.num_vars[0] = a0;
match acalc(expr, &mut inp).expect("compiles and evaluates") {
epics_base_rs::calc::ArrayStackValue::Double(d) => d,
other => panic!("expected a scalar, got {other:?}"),
}
}
/// CBUG-A2 — `A % 7` with `A = 3e9` narrows through EACH dialect's own cast, and
/// the three genuinely disagree because their C widths do:
/// * base — `d2i(3e9) = -1294967296`, `% 7 == 0` (fixed `calcPerform.c`, PR #925)
/// * sCalc — `(long)3e9 = 3000000000`, `% 7 == 4` (64-bit, no loss)
/// * aCalc — `(int)3e9` is out of int32 range, so the port's saturating cast
/// (CBUG-E2) gives `i32::MAX = 2147483647`, `% 7 == 1`
///
/// Each equals its own pristine/fixed C. This used to pin the clean no-narrow
/// deviation (`4` everywhere).
#[test]
fn modulo_above_2_31_mirrors_each_dialect_width() {
assert_eq!(n("A%7", 3e9), 0.0); // base d2i
assert_eq!(s("A%7", 3e9), 4.0); // sCalc (long)
assert_eq!(a("A%7", 3e9), 1.0); // aCalc (int), saturating (CBUG-E2)
}
/// sCalc's C picks its evaluator from the `USES_STRING` marker
/// (`sCalcPostfix.c:447-475`) and its two evaluators cast MODULO at different
/// widths — `(int)` no-string (`sCalcPerform.c:574`) vs `(long)` string
/// (`:1121`). The port models the wider `(long)` path uniformly, so the marker
/// no longer perturbs the arithmetic: both are `4`. This test used to pin the
/// split.
#[test]
fn scalc_modulo_no_longer_depends_on_the_uses_string_marker() {
assert_eq!(s("A%7", 3e9), 4.0, "(long)3e9 % 7, no-string program");
// LEN("") is in C's USES_STRING opcode list, so C's string evaluator would
// run; the port's single `(long)` path is width-independent.
assert_eq!(
s("A%7+LEN('')", 3e9),
4.0,
"same (long) narrowing, string program"
);
}
/// A zero divisor is a THREE-way split, and the port had base's answer in all
/// three engines.
#[test]
fn zero_divisor_disposition_is_per_dialect() {
// base: epicsNAN (calcPerform.c:166).
assert!(n("A%0", 5.0).is_nan());
// sCalc: `return(-1)` — the evaluation FAILS (sCalcPerform.c:560-561).
let mut inp = StringInputs::new();
inp.num_vars[0] = 5.0;
assert!(
scalc("A%0", &mut inp).is_err(),
"sCalcPerform returns -1 for a zero MODULO divisor"
);
// aCalc: myMAXFLOAT == (float)1e35 (aCalcPerform.c:49,701).
assert_eq!(a("A%0", 5.0), 1e35f32 as f64);
}
/// In the BASE (numeric) engine a divisor of exactly 2^32 is the UB boundary:
/// `d2i` is defined only for `[2^31, 2^32)`, and `2^32` is one past it. The
/// port's `d2i` wraps modulo 2^32, so `d2i(2^32) = 0` — a zero divisor — and
/// base's rule makes it NaN. This is NOT a C-defined value; it is the port
/// mirroring its own d2i on a still-UB input. sCalc's `(long)` and aCalc's
/// `(int)` do NOT wrap here — this boundary is base-specific. Under the earlier
/// clean no-narrow deviation this answered `5`.
#[test]
fn base_divisor_at_the_2_32_boundary_narrows_to_zero() {
assert!(nc("5 % 4294967296").is_nan());
assert!(n("A % 4294967296", 5.0).is_nan());
}
/// Base's bitwise ops REALLY do use `d2i` — this must not change. It is the
/// one place the uint32 reinterpretation is C.
#[test]
fn base_bitwise_still_uses_d2i() {
assert_eq!(nc("3000000000 & 4294967295"), -1_294_967_296.0);
assert_eq!(n("A | 0", 3e9), -1_294_967_296.0);
}
/// sCalc's bitwise ops are `(long)` — 64-bit, so 3e9 survives intact instead
/// of being reinterpreted as a negative int32.
#[test]
fn scalc_bitwise_is_long_wide() {
assert_eq!(s("A|0", 3e9), 3e9);
assert_eq!(s("A&0xFFFFFFFF", 3e9), 3e9);
}
/// aCalc's bitwise ops are a plain `(int)` cast, NOT `d2i`'s uint32
/// reinterpretation — so an out-of-range operand does not come back as
/// -1294967296. Where it DOES come back is CBUG-E2's call: the `(int)` cast is
/// UB on an out-of-range double, so compiled C answers INT32_MIN on x86-64 and
/// INT32_MAX on aarch64. The port saturates.
#[test]
fn acalc_bitwise_is_a_plain_int_cast() {
assert_ne!(a("A|0", 3e9), -1_294_967_296.0);
assert_eq!(a("A|0", 3e9), i32::MAX as f64);
}
/// CBUG-A2 — NINT rounds, then narrows with EACH dialect's own cast:
/// * base — `d2i` (fixed `calcPerform.c:316`, PR #925): `NINT(3e9) = -1294967296`
/// * sCalc — `(long)` (`sCalcPerform.c:730`): `NINT(3e9) = 3e9` (64-bit, no loss)
/// * aCalc — `(long)` (`aCalcPerform.c:839,1096`): `NINT(3e9) = 3e9` — aCalc
/// narrows NINT at 64-bit even though its MODULO is 32-bit `(int)` (an internal
/// synApps inconsistency)
///
/// This used to pin the clean no-narrow deviation (`3e9` in every dialect); note
/// sCalc/aCalc happen to still be `3e9` because `(long)` does not lose 3e9.
#[test]
fn nint_mirrors_each_dialect_width() {
assert_eq!(n("NINT(A)", 3e9), -1_294_967_296.0); // base d2i
assert_eq!(s("NINT(A)", 3e9), 3e9); // sCalc (long)
assert_eq!(a("NINT(A)", 3e9), 3e9); // aCalc (long)
// Past each width's range: base d2i wraps 1e300 (a multiple of 2^32) to 0;
// sCalc/aCalc `(long)` saturate to i64::MAX (CBUG-E2). Not C-defined values.
assert_eq!(n("NINT(A)", 1e300), 0.0);
assert_eq!(s("NINT(A)", 1e300), i64::MAX as f64);
assert_eq!(a("NINT(A)", 1e300), i64::MAX as f64);
// In range, all three agree and round half away from zero — same as C.
for expr_val in [(2.5, 3.0), (-2.5, -3.0), (2.4, 2.0)] {
let (input, want) = expr_val;
assert_eq!(n("NINT(A)", input), want);
assert_eq!(s("NINT(A)", input), want);
assert_eq!(a("NINT(A)", input), want);
}
}