Skip to content

Commit f1dfd41

Browse files
authored
Merge pull request #392 from kevinlincg/issue-390-scaled-divisor
test(codegen): the sweep sees a divisor scaled from the guarded value (#390)
2 parents 393ae24 + bd17e2b commit f1dfd41

1 file changed

Lines changed: 205 additions & 34 deletions

File tree

ta_codegen/generator/tests/divisor_guard_suite.rs

Lines changed: 205 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,72 @@ const ANNOTATED: &[(&str, &str, &str, &str)] = &[
124124
// it. KAMA below is the contrast -- it carries `sumROC1`, so the same reasoning
125125
// does not reach it.
126126
("VWMA", "tempV", "unguarded by decision: stateless per bar", "nan_inf_output"),
127+
128+
// OPEN, and reported by the scaled-derivation arm rather than the accumulator one.
129+
// `stoch.c:230` guards `highest - lowest` and `:231` divides by `diff`, which
130+
// `:189` sets to `(highest - lowest)/100.0`. The scaling is what breaks the
131+
// inference: a denormal range leaves `highest - lowest` non-zero while `diff`
132+
// underflows to exactly 0.0, so the guard says "not flat" and the division is by
133+
// zero. Measured on the released library in #390 -- TA_SUCCESS with inf/nan
134+
// written for well-formed OHLC. Moving the guard onto `diff` changes STOCH's
135+
// output on those windows, so it is a decision about the function, not a patch to
136+
// make a sweep green; `the_scaled_arm_clears_when_the_guard_moves_to_the_divisor`
137+
// pins that this row disappears once that decision is made.
138+
("STOCH", "diff", "OPEN: guard tests the pre-scaled range; see #390", ""),
139+
("STOCHF", "diff", "OPEN: guard tests the pre-scaled range; see #390", ""),
127140
];
128141

142+
/// Variables assigned, inside a loop, from an expression that MULTIPLIES OR DIVIDES
143+
/// something — mapped to the variables that expression reads.
144+
///
145+
/// This is the second defect shape, and it is not the accumulator one. STOCH guards
146+
/// `highest - lowest` and then divides by `diff`, where `diff = (highest-lowest)/100.0`
147+
/// (`stoch.c:189,230-231`). Scaling can send a non-zero quantity to exactly 0.0 by
148+
/// underflow, so a guard on the pre-scaled value does not establish what the division
149+
/// needs — the divisor is a different number. Addition and subtraction are excluded:
150+
/// they cannot turn a guarded-non-zero into a zero divisor the way a scaling can.
151+
fn scaled_derivations(f: &FuncDef) -> Vec<(String, HashSet<String>)> {
152+
fn scaling_reads(e: &Expr) -> Option<HashSet<String>> {
153+
match e {
154+
Expr::BinOp(l, BinOp::Mul | BinOp::Div, r) => {
155+
let mut s = names(l);
156+
s.extend(names(r));
157+
Some(s)
158+
}
159+
Expr::BinOp(l, _, r) => scaling_reads(l).or_else(|| scaling_reads(r)),
160+
Expr::Cast(_, i) => scaling_reads(i),
161+
_ => None,
162+
}
163+
}
164+
fn walk(body: &[Statement], in_loop: bool, out: &mut Vec<(String, HashSet<String>)>) {
165+
for st in body {
166+
match st {
167+
Statement::Assign { target: Expr::Var(t), value, .. } if in_loop => {
168+
if let Some(reads) = scaling_reads(value) {
169+
if !reads.contains(t) {
170+
out.push((t.clone(), reads));
171+
}
172+
}
173+
}
174+
Statement::While { body, .. }
175+
| Statement::DoWhile { body, .. }
176+
| Statement::For { body, .. }
177+
| Statement::ForC { body, .. } => walk(body, true, out),
178+
Statement::Block { body } => walk(body, in_loop, out),
179+
Statement::If { then_body, else_body, .. } => {
180+
walk(then_body, in_loop, out);
181+
walk(else_body, in_loop, out);
182+
}
183+
_ => {}
184+
}
185+
}
186+
}
187+
let mut out = Vec::new();
188+
walk(&f.body, false, &mut out);
189+
walk(&f.private_body, false, &mut out);
190+
out
191+
}
192+
129193
/// Every variable name mentioned anywhere in `e`.
130194
fn vars_of(e: &Expr, out: &mut HashSet<String>) {
131195
match e {
@@ -369,6 +433,25 @@ fn real_valued(f: &FuncDef) -> HashSet<String> {
369433
struct Finding {
370434
func: String,
371435
divisor: String,
436+
kind: FindingKind,
437+
}
438+
439+
#[derive(Debug, Clone, Copy, PartialEq)]
440+
enum FindingKind {
441+
/// Divisor is accumulated across loop iterations and no guard tests it.
442+
Accumulated,
443+
/// Divisor is a SCALED derivation of a quantity a dominating guard does test —
444+
/// the guard proves the pre-scaled value non-zero, which the divisor is not.
445+
ScaledFromGuarded,
446+
}
447+
448+
impl FindingKind {
449+
fn label(self) -> &'static str {
450+
match self {
451+
FindingKind::Accumulated => "accumulated, untested",
452+
FindingKind::ScaledFromGuarded => "scaled from a guarded value",
453+
}
454+
}
372455
}
373456

374457
/// Walk an expression for divisions whose denominator is an accumulated variable that
@@ -378,6 +461,7 @@ fn scan_expr(
378461
accum: &HashSet<String>,
379462
guards: &[Expr],
380463
aliases: &[(String, String)],
464+
derived: &[(String, HashSet<String>)],
381465
func: &str,
382466
out: &mut Vec<Finding>,
383467
) {
@@ -390,31 +474,53 @@ fn scan_expr(
390474
.any(|(alias, of)| *of == v && tests_var_against_zero(g, alias))
391475
});
392476
if accum.contains(&v) && !guarded {
393-
out.push(Finding { func: func.to_string(), divisor: v });
477+
out.push(Finding {
478+
func: func.to_string(),
479+
divisor: v.clone(),
480+
kind: FindingKind::Accumulated,
481+
});
482+
}
483+
// Second shape: the divisor is untested, but a guard DOES test something
484+
// the divisor was scaled from. That reads as guarded and is not -- scaling
485+
// can underflow a non-zero value to exactly 0.0.
486+
if !guarded {
487+
let scaled_from_a_guarded_value = derived
488+
.iter()
489+
.filter(|(d, _)| *d == v)
490+
.any(|(_, reads)| {
491+
reads.iter().any(|r| guards.iter().any(|g| tests_var_against_zero(g, r)))
492+
});
493+
if scaled_from_a_guarded_value {
494+
out.push(Finding {
495+
func: func.to_string(),
496+
divisor: v,
497+
kind: FindingKind::ScaledFromGuarded,
498+
});
499+
}
394500
}
395501
}
396-
scan_expr(num, accum, guards, aliases, func, out);
397-
scan_expr(den, accum, guards, aliases, func, out);
502+
scan_expr(num, accum, guards, aliases, derived, func, out);
503+
scan_expr(den, accum, guards, aliases, derived, func, out);
398504
return;
399505
}
400506
match e {
401507
Expr::BinOp(l, _, r) => {
402-
scan_expr(l, accum, guards, aliases, func, out);
403-
scan_expr(r, accum, guards, aliases, func, out);
508+
scan_expr(l, accum, guards, aliases, derived, func, out);
509+
scan_expr(r, accum, guards, aliases, derived, func, out);
404510
}
405511
Expr::Cast(_, i) | Expr::Not(i) | Expr::BitwiseNot(i) | Expr::AddressOf(i) => {
406-
scan_expr(i, accum, guards, aliases, func, out)
512+
scan_expr(i, accum, guards, aliases, derived, func, out)
407513
}
408514
Expr::FuncCall(_, args) => {
409-
args.iter().for_each(|a| scan_expr(a, accum, guards, aliases, func, out))
515+
args.iter().for_each(|a| scan_expr(a, accum, guards, aliases, derived, func, out))
410516
}
411517
// A ternary's own condition guards both arms.
412518
Expr::Ternary(c, t, f) => {
413-
scan_expr(c, accum, guards, aliases, func, out);
519+
scan_expr(c, accum, guards, aliases, derived, func, out);
414520
let mut inner = guards.to_vec();
415521
inner.push((**c).clone());
416-
scan_expr(t, accum, &inner, aliases, func, out);
417-
scan_expr(f, accum, &inner, aliases, func, out);
522+
scan_expr(t, accum, &inner, aliases, derived, func, out);
523+
scan_expr(f, accum, &inner, aliases, derived, func, out);
418524
}
419525
_ => {}
420526
}
@@ -430,51 +536,52 @@ fn scan_stmts(
430536
accum: &HashSet<String>,
431537
guards: &[Expr],
432538
aliases: &[(String, String)],
539+
derived: &[(String, HashSet<String>)],
433540
in_loop: bool,
434541
func: &str,
435542
out: &mut Vec<Finding>,
436543
) {
437544
for st in body {
438545
match st {
439546
Statement::Assign { value, .. } if in_loop => {
440-
scan_expr(value, accum, guards, aliases, func, out)
547+
scan_expr(value, accum, guards, aliases, derived, func, out)
441548
}
442549
Statement::VarDecl { init: Some(v), .. } if in_loop => {
443-
scan_expr(v, accum, guards, aliases, func, out)
550+
scan_expr(v, accum, guards, aliases, derived, func, out)
444551
}
445552
Statement::Expr(e) | Statement::Return { value: Some(e) } if in_loop => {
446-
scan_expr(e, accum, guards, aliases, func, out)
553+
scan_expr(e, accum, guards, aliases, derived, func, out)
447554
}
448555
Statement::If { condition, then_body, else_body, .. } => {
449556
if in_loop {
450-
scan_expr(condition, accum, guards, aliases, func, out);
557+
scan_expr(condition, accum, guards, aliases, derived, func, out);
451558
}
452559
let mut inner = guards.to_vec();
453560
inner.push(condition.clone());
454-
scan_stmts(then_body, accum, &inner, aliases, in_loop, func, out);
561+
scan_stmts(then_body, accum, &inner, aliases, derived, in_loop, func, out);
455562
// The else arm is guarded by the negation, which `tests_var` treats
456563
// the same way: it names the variable either way.
457-
scan_stmts(else_body, accum, &inner, aliases, in_loop, func, out);
564+
scan_stmts(else_body, accum, &inner, aliases, derived, in_loop, func, out);
458565
}
459566
Statement::While { condition, body } | Statement::DoWhile { condition, body } => {
460-
scan_expr(condition, accum, guards, aliases, func, out);
461-
scan_stmts(body, accum, guards, aliases, true, func, out);
567+
scan_expr(condition, accum, guards, aliases, derived, func, out);
568+
scan_stmts(body, accum, guards, aliases, derived, true, func, out);
462569
}
463570
Statement::For { body, .. } => {
464-
scan_stmts(body, accum, guards, aliases, true, func, out)
571+
scan_stmts(body, accum, guards, aliases, derived, true, func, out)
465572
}
466573
Statement::Block { body } => {
467-
scan_stmts(body, accum, guards, aliases, in_loop, func, out)
574+
scan_stmts(body, accum, guards, aliases, derived, in_loop, func, out)
468575
}
469576
Statement::ForC { condition, body, .. } => {
470-
scan_expr(condition, accum, guards, aliases, func, out);
471-
scan_stmts(body, accum, guards, aliases, true, func, out);
577+
scan_expr(condition, accum, guards, aliases, derived, func, out);
578+
scan_stmts(body, accum, guards, aliases, derived, true, func, out);
472579
}
473580
Statement::Switch { cases, default, .. } => {
474581
for (_, b) in cases {
475-
scan_stmts(b, accum, guards, aliases, in_loop, func, out);
582+
scan_stmts(b, accum, guards, aliases, derived, in_loop, func, out);
476583
}
477-
scan_stmts(default, accum, guards, aliases, in_loop, func, out);
584+
scan_stmts(default, accum, guards, aliases, derived, in_loop, func, out);
478585
}
479586
_ => {}
480587
}
@@ -488,18 +595,17 @@ fn findings_for(f: &FuncDef) -> Vec<Finding> {
488595
propagate_copies(f, &mut accum);
489596
let reals = real_valued(f);
490597
accum.retain(|v| reals.contains(v));
491-
if accum.is_empty() {
492-
return Vec::new();
493-
}
494598
let aliases = magnitude_aliases(f);
599+
let derived = scaled_derivations(f);
495600
let mut out = Vec::new();
496601
// Both bodies: `private_body` is where the arithmetic lives for every function
497602
// that declares a `_private` variant, and scanning only `body` skipped ER's
498603
// divisions entirely.
499-
scan_stmts(&f.body, &accum, &[], &aliases, false, &f.name, &mut out);
500-
scan_stmts(&f.private_body, &accum, &[], &aliases, false, &f.name, &mut out);
604+
scan_stmts(&f.body, &accum, &[], &aliases, &derived, false, &f.name, &mut out);
605+
scan_stmts(&f.private_body, &accum, &[], &aliases, &derived, false, &f.name, &mut out);
501606
out.sort_by(|a, b| a.divisor.cmp(&b.divisor));
502-
out.dedup_by(|a, b| a.func == b.func && a.divisor == b.divisor);
607+
out.sort_by(|a, b| (a.divisor.clone(), a.kind.label()).cmp(&(b.divisor.clone(), b.kind.label())));
608+
out.dedup_by(|a, b| a.func == b.func && a.divisor == b.divisor && a.kind == b.kind);
503609
out
504610
}
505611

@@ -512,16 +618,17 @@ fn loop_accumulated_divisors_are_guarded_on_themselves() {
512618
if ANNOTATED.iter().any(|(fn_, v, _, _)| *fn_ == fd.func && *v == fd.divisor) {
513619
continue;
514620
}
515-
flagged.push(format!("{}: divides by `{}`", fd.func, fd.divisor));
621+
flagged.push(format!("{}: divides by `{}` ({})", fd.func, fd.divisor, fd.kind.label()));
516622
}
517623
}
518624
flagged.sort();
519625
flagged.dedup();
520626
assert!(
521627
flagged.is_empty(),
522-
"divisor(s) accumulated across loop iterations with no guard testing that same \
523-
variable — each is either a missing zero guard or a known-safe case that \
524-
belongs in ANNOTATED with its reason:\n {}",
628+
"divisor(s) no dominating guard establishes non-zero for — either accumulated \
629+
across loop iterations with nothing testing them, or SCALED from a value the \
630+
guard does test, which scaling can underflow to 0.0 independently. Each is a \
631+
missing guard or a case that belongs in ANNOTATED with its reason:\n {}",
525632
flagged.join("\n ")
526633
);
527634
}
@@ -682,3 +789,67 @@ fn annotation_reasons_still_hold() {
682789
);
683790
}
684791
}
792+
793+
/// The scaled-derivation arm must go quiet when the guard moves to the divisor.
794+
///
795+
/// Same requirement as the ER and VORTEX self-tests, in the other direction: those
796+
/// prove the sweep goes loud on a reintroduced defect, this one proves it goes QUIET
797+
/// on the fix. Without it, a check that flags every scaled divisor unconditionally
798+
/// would look identical to one that reasons about the guard.
799+
#[test]
800+
fn the_scaled_arm_clears_when_the_guard_moves_to_the_divisor() {
801+
let funcs = load();
802+
let stoch = funcs.iter().find(|f| f.name == "STOCH").expect("STOCH is in the tree");
803+
804+
assert!(
805+
findings_for(stoch).iter().any(|f| f.divisor == "diff"
806+
&& f.kind == FindingKind::ScaledFromGuarded),
807+
"STOCH ships guarding `highest - lowest` while dividing by `diff`; the sweep \
808+
should say so"
809+
);
810+
811+
// Rewrite the guard's subject from `highest - lowest` to `diff` — the fix #390
812+
// suggests — and the finding must disappear.
813+
let mut fixed = stoch.clone();
814+
guard_on_diff(&mut fixed.body);
815+
guard_on_diff(&mut fixed.private_body);
816+
assert!(
817+
!findings_for(&fixed).iter().any(|f| f.divisor == "diff"),
818+
"the sweep still flags `diff` after the guard was moved onto it — the check is \
819+
not reading the guard, it is flagging every scaled divisor"
820+
);
821+
}
822+
823+
/// Replace `TA_IS_ZERO_SCALED(highest-lowest, ...)` with a plain zero test on `diff`.
824+
fn guard_on_diff(body: &mut [Statement]) {
825+
fn fix(e: &Expr) -> Expr {
826+
if let Expr::FuncCall(name, args) = e {
827+
if name.contains("IS_ZERO") && args.iter().any(|a| names(a).contains("highest")) {
828+
return Expr::BinOp(
829+
Box::new(Expr::Var("diff".to_string())),
830+
BinOp::Eq,
831+
Box::new(Expr::Literal(0.0)),
832+
);
833+
}
834+
}
835+
if let Expr::Not(inner) = e {
836+
return Expr::Not(Box::new(fix(inner)));
837+
}
838+
e.clone()
839+
}
840+
for st in body.iter_mut() {
841+
match st {
842+
Statement::If { condition, then_body, else_body, .. } => {
843+
*condition = fix(condition);
844+
guard_on_diff(then_body);
845+
guard_on_diff(else_body);
846+
}
847+
Statement::While { body, .. }
848+
| Statement::DoWhile { body, .. }
849+
| Statement::For { body, .. }
850+
| Statement::ForC { body, .. }
851+
| Statement::Block { body } => guard_on_diff(body),
852+
_ => {}
853+
}
854+
}
855+
}

0 commit comments

Comments
 (0)