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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! R10-6 — an aCalc array stack value carries C's ACTIVE WINDOW (`stackElement`,
//! `aCalcPerform.c:74-80`), and every reduction folds over the window, not over the
//! whole `arraySize` buffer.
//!
//! ```c
//! void calcFirstLast(stackElement *ps, int *firstEl, int *lastEl, int arraySize) {
//! if (ps->numEl != -1) { *firstEl = ps->firstEl; *lastEl = ps->firstEl + ps->numEl - 1; }
//! else { *firstEl = 0; *lastEl = arraySize-1; }
//! }
//! ```
//!
//! SUBRANGE, SUBRANGE_IP and CAT set the window; AMAX/AMIN/IXMAX/IXMIN/IXZ/IXNZ/
//! AVERAGE/STD_DEV/FWHM/ARRSUM/SMOOTH/DERIV/NDERIV/FIT* read it. The port kept only
//! the vector, so every reduction after a subrange folded over the zero fill:
//! `AMIN(AA[1,3])` answered 0 (a tail zero) where C answers 20.
//!
//! Every expectation below is the output of a driver compiled from
//! `/home/stevek/work/epics-modules/calc/calcApp/src/{aCalcPerform,aCalcPostfix,calcUtil}.c`.
use epics_base_rs::calc::{ArrayInputs, ArrayStackValue, acalc};
fn inputs6() -> ArrayInputs {
// Compiled-C: arraySize 6, AA=[10,20,30,40,50,60].
let mut i = ArrayInputs::new(6);
i.arrays[0] = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
i
}
fn d(expr: &str, inputs: &mut ArrayInputs) -> f64 {
match acalc(expr, inputs).expect("status 0") {
ArrayStackValue::Double(v) => v,
other => panic!("expected a Double result, got {other:?}"),
}
}
fn a(expr: &str, inputs: &mut ArrayInputs) -> Vec<f64> {
match acalc(expr, inputs).expect("status 0") {
ArrayStackValue::Array(cell) => cell.buf().to_vec(),
other => panic!("expected an Array result, got {other:?}"),
}
}
/// `[i,j]` selects an inclusive range, shifts it down to index 0, zero-fills the
/// tail — and sets `numEl = 1+j-i`. The reductions then see three elements, not
/// six. Compiled C: AMIN 20, AMAX 40, AVG 30, SUM 90, IXMAX 2, IXMIN 0.
#[test]
fn r10_6_subrange_window_bounds_every_reduction() {
let mut i = inputs6();
assert_eq!(a("AA[1,3]", &mut i), vec![20.0, 30.0, 40.0, 0.0, 0.0, 0.0]);
assert_eq!(
d("AMIN(AA[1,3])", &mut i),
20.0,
"AMIN over the window, not the zero fill"
);
assert_eq!(d("AMAX(AA[1,3])", &mut i), 40.0);
assert_eq!(
d("AVG(AA[1,3])", &mut i),
30.0,
"divisor is 1+lastEl-firstEl = 3"
);
assert_eq!(d("SUM(AA[1,3])", &mut i), 90.0);
assert_eq!(d("IXMAX(AA[1,3])", &mut i), 2.0);
assert_eq!(d("IXMIN(AA[1,3])", &mut i), 0.0);
}
/// Negative control: with no subrange there is no window (C's `numEl == -1`
/// sentinel), so the same reductions cover the whole buffer. A fix that simply
/// truncated arrays would fail here.
#[test]
fn r10_6_no_window_reduces_over_the_whole_buffer() {
let mut i = inputs6();
assert_eq!(d("AMIN(AA)", &mut i), 10.0);
assert_eq!(d("AMAX(AA)", &mut i), 60.0);
assert_eq!(d("AVG(AA)", &mut i), 35.0);
assert_eq!(d("SUM(AA)", &mut i), 210.0);
assert_eq!(d("IXMAX(AA)", &mut i), 5.0);
}
/// `{i,j}` (SUBRANGE_IP) keeps the elements where they are, zeroes everything
/// outside — and sets `numEl = j+1`, so the window STARTS AT 0 and includes the
/// zeroed head. Compiled C: `AVG(AA{1,3})` is 22.5 = (0+20+30+40)/4, not 30.
#[test]
fn r10_6_subrange_in_place_window_includes_the_zeroed_head() {
let mut i = inputs6();
assert_eq!(a("AA{1,3}", &mut i), vec![0.0, 20.0, 30.0, 40.0, 0.0, 0.0]);
assert_eq!(d("AVG(AA{1,3})", &mut i), 22.5);
assert_eq!(d("SUM(AA{1,3})", &mut i), 90.0);
assert_eq!(
d("AMIN(AA{1,3})", &mut i),
0.0,
"the zeroed head IS in the window"
);
}
/// C clamps the upper bound to `arraySize` (not `arraySize-1`), so `AA[1,99]` has
/// `numEl = 6` — a six-element window over a buffer whose last element is the zero
/// left by the shift. Compiled C: AVG = 200/6 = 33.333..., the tail zero included.
#[test]
fn r10_6_subrange_upper_bound_clamps_to_array_size() {
let mut i = inputs6();
assert_eq!(
a("AA[1,99]", &mut i),
vec![20.0, 30.0, 40.0, 50.0, 60.0, 0.0]
);
assert!((d("AVG(AA[1,99])", &mut i) - 200.0 / 6.0).abs() < 1e-12);
}
/// `numEl = 1+j-i` is a COUNT, and C stores it in the same field as the -1 "no
/// window" sentinel. So the two degenerate ranges are not the same degenerate:
///
/// * `AA[2,1]` — count 0: an empty window, and C's AVERAGE divides by it. The
/// seed `a[firstEl]` is still read (in bounds), and SUBRANGE has just zeroed the
/// whole buffer, so the answer is 0/0 = NaN.
/// * `AA[3,1]` — count -1: ALIASES the sentinel, so the window reads back as the
/// FULL buffer, whose elements SUBRANGE has also just zeroed. AVG is 0.
#[test]
fn r10_6_degenerate_windows_split_on_the_sentinel() {
let mut i = inputs6();
assert!(
d("AVG(AA[2,1])", &mut i).is_nan(),
"numEl == 0: C divides the seed by an empty window"
);
let mut i = inputs6();
assert_eq!(
d("AVG(AA[3,1])", &mut i),
0.0,
"numEl == -1 aliases the sentinel: the whole (zeroed) buffer averages to 0"
);
}
/// CAT is a window writer, not a vector append. Array+scalar writes the scalar at
/// `lastEl+1` and grows the window — but only if the `arraySize` buffer has room,
/// so a full left operand makes CAT a no-op. Compiled C, arraySize 6,
/// AA=[1,2,3,4,5,6]: `CAT(AA[0,1],5)` -> [1,2,5,0,0,0] with numEl 3 (AVG 2.667),
/// and `CAT(AA,9)` -> AA unchanged.
#[test]
fn r10_6_cat_appends_inside_the_buffer_and_grows_the_window() {
let mut i = ArrayInputs::new(6);
i.arrays[0] = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
i.arrays[1] = vec![7.0, 8.0, 9.0, 0.0, 0.0, 0.0];
assert_eq!(
a("CAT(AA[0,1],5)", &mut i),
vec![1.0, 2.0, 5.0, 0.0, 0.0, 0.0]
);
assert!((d("AVG(CAT(AA[0,1],5))", &mut i) - 8.0 / 3.0).abs() < 1e-12);
assert_eq!(
a("CAT(AA[0,1],BB[0,1])", &mut i),
vec![1.0, 2.0, 7.0, 8.0, 0.0, 0.0]
);
assert_eq!(
a("CAT(AA,9)", &mut i),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
"no window means lastEl+1 == arraySize: C's `if (lastEl+1 < arraySize)` fails"
);
assert_eq!(
a("CAT(9,AA)", &mut i),
vec![9.0, 9.0, 9.0, 9.0, 9.0, 9.0],
"the LEFT operand is promoted (toArray), filling the buffer; nothing fits after it"
);
}
/// R11-7 — and CAT of two SCALARS is a no-op: `case CAT: break;` (`:1411`). With
/// neither operand an array the two-arg dispatch takes the scalar branch, and the left
/// operand's cell IS the result cell, so the answer is the LEFT scalar — still a
/// scalar. Compiled C, A=5, B=7: `CAT(A,B)` is 5, and `AVG(CAT(A,B))` is 5.
///
/// The port built a two-element array [5,7]. C has nowhere to put one: CAT
/// concatenates INTO the left operand's buffer, and a scalar has no buffer.
#[test]
fn r11_7_cat_of_two_scalars_is_the_left_scalar() {
let mut i = ArrayInputs::new(6);
i.num_vars[0] = 5.0;
i.num_vars[1] = 7.0;
assert_eq!(
acalc("CAT(A,B)", &mut i).unwrap(),
ArrayStackValue::Double(5.0),
"the result must stay a SCALAR, not become [5,7]"
);
assert_eq!(
d("AVG(CAT(A,B))", &mut i),
5.0,
"a two-element array would have averaged to 6"
);
assert_eq!(
d("CAT(CAT(A,B),C)", &mut i),
5.0,
"chaining does not accumulate: each CAT is still the left scalar"
);
}
/// The negative control for that: as soon as EITHER operand is an array, CAT is the
/// window writer again — the scalar no-op is the scalar branch's rule alone.
#[test]
fn r11_7_cat_still_concatenates_when_an_operand_is_an_array() {
let mut i = ArrayInputs::new(6);
i.num_vars[1] = 7.0;
i.arrays[0] = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
assert_eq!(
a("CAT(AA[0,1],B)", &mut i),
vec![1.0, 2.0, 7.0, 0.0, 0.0, 0.0]
);
}