@@ -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`.
130194fn vars_of ( e : & Expr , out : & mut HashSet < String > ) {
131195 match e {
@@ -369,6 +433,25 @@ fn real_valued(f: &FuncDef) -> HashSet<String> {
369433struct 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