22
33use super :: region:: { BitPartition , NodeKey , PackedSpan , dst_writes, var_reads} ;
44use super :: ssa:: { BranchState , SsaStore , VersionId } ;
5- use crate :: HashSet ;
65use crate :: conv:: Context ;
76use crate :: ir:: VarId ;
87use crate :: ir:: {
@@ -11,6 +10,57 @@ use crate::ir::{
1110 VarSelect ,
1211} ;
1312use crate :: value:: Value ;
13+ use crate :: { HashMap , HashSet } ;
14+
15+ #[ cfg( test) ]
16+ use std:: cell:: Cell ;
17+
18+ #[ derive( Clone ) ]
19+ struct CallResult {
20+ region_groups : Vec < Vec < ( PackedSpan , VersionId ) > > ,
21+ opaque_sources : Vec < VersionId > ,
22+ }
23+
24+ // Region-split writes query one RHS several times, but a function call in that
25+ // RHS is one procedural evaluation. `None` is an invocation barrier: temporary
26+ // call nodes in a cloned callee body must never enter the caller's cache.
27+ type CallCache = Option < HashMap < * const FunctionCall , CallResult > > ;
28+
29+ #[ cfg( test) ]
30+ thread_local ! {
31+ static FUNCTION_EVALUATIONS : Cell <usize > = const { Cell :: new( 0 ) } ;
32+ static FUNCTION_RESULT_VERSIONS : Cell <usize > = const { Cell :: new( 0 ) } ;
33+ static FUNCTION_RESULT_REGION_PROBES : Cell <usize > = const { Cell :: new( 0 ) } ;
34+ static FUNCTION_BARRIER_EVALUATIONS : Cell <usize > = const { Cell :: new( 0 ) } ;
35+ }
36+
37+ #[ cfg( test) ]
38+ pub ( crate ) fn reset_function_evaluation_count ( ) {
39+ FUNCTION_EVALUATIONS . set ( 0 ) ;
40+ FUNCTION_RESULT_VERSIONS . set ( 0 ) ;
41+ FUNCTION_RESULT_REGION_PROBES . set ( 0 ) ;
42+ FUNCTION_BARRIER_EVALUATIONS . set ( 0 ) ;
43+ }
44+
45+ #[ cfg( test) ]
46+ pub ( crate ) fn function_evaluation_count ( ) -> usize {
47+ FUNCTION_EVALUATIONS . get ( )
48+ }
49+
50+ #[ cfg( test) ]
51+ pub ( crate ) fn function_result_version_count ( ) -> usize {
52+ FUNCTION_RESULT_VERSIONS . get ( )
53+ }
54+
55+ #[ cfg( test) ]
56+ pub ( crate ) fn function_result_region_probe_count ( ) -> usize {
57+ FUNCTION_RESULT_REGION_PROBES . get ( )
58+ }
59+
60+ #[ cfg( test) ]
61+ pub ( crate ) fn function_barrier_evaluation_count ( ) -> usize {
62+ FUNCTION_BARRIER_EVALUATIONS . get ( )
63+ }
1464
1565pub ( super ) fn analyze (
1666 module : & Module ,
@@ -25,6 +75,7 @@ struct ProcedureAnalysis<'a> {
2575 ctx : Context ,
2676 ssa : SsaStore < NodeKey > ,
2777 written : HashSet < NodeKey > ,
78+ call_caches : Vec < CallCache > ,
2879}
2980
3081impl < ' a > ProcedureAnalysis < ' a > {
@@ -41,19 +92,39 @@ impl<'a> ProcedureAnalysis<'a> {
4192 ctx,
4293 ssa : SsaStore :: default ( ) ,
4394 written : HashSet :: default ( ) ,
95+ call_caches : Vec :: new ( ) ,
4496 } ;
4597 this. eval_block ( statements, & [ ] ) ;
4698
4799 let mut dependencies = Vec :: new ( ) ;
48- let destinations: Vec < _ > = this. written . iter ( ) . copied ( ) . collect ( ) ;
100+ let destinations: Vec < _ > = this
101+ . written
102+ . iter ( )
103+ . copied ( )
104+ . filter ( |key| this. is_module_scope_key ( * key) )
105+ . collect ( ) ;
49106 for destination in destinations {
50107 let version = this. ssa . read ( destination) ;
51108 let sources = this. ssa . root_sources ( version) ;
52- dependencies. extend ( sources. into_iter ( ) . map ( |source| ( source, destination) ) ) ;
109+ dependencies. extend (
110+ sources
111+ . into_iter ( )
112+ . filter ( |source| this. is_module_scope_key ( * source) )
113+ . map ( |source| ( source, destination) ) ,
114+ ) ;
53115 }
54116 dependencies
55117 }
56118
119+ fn is_module_scope_key ( & self , key : NodeKey ) -> bool {
120+ self . ctx . variables . get ( & key. 0 ) . is_none_or ( |variable| {
121+ matches ! (
122+ variable. affiliation,
123+ crate :: symbol:: Affiliation :: Module | crate :: symbol:: Affiliation :: Interface
124+ )
125+ } )
126+ }
127+
57128 fn read_keys ( & mut self , id : VarId , index : & VarIndex , select : & VarSelect ) -> Vec < NodeKey > {
58129 let mut keys = Vec :: new ( ) ;
59130 for ( idx, span) in var_reads ( id, index, select, & mut self . ctx ) {
@@ -182,6 +253,7 @@ impl<'a> ProcedureAnalysis<'a> {
182253 fn eval_statement ( & mut self , statement : & Statement , controls : & [ VersionId ] ) -> bool {
183254 match statement {
184255 Statement :: Assign ( assign) => {
256+ self . call_caches . push ( Some ( HashMap :: default ( ) ) ) ;
185257 let widths: Vec < _ > = assign
186258 . dst
187259 . iter ( )
@@ -207,6 +279,7 @@ impl<'a> ProcedureAnalysis<'a> {
207279 self . write_destination ( destination, & sources, controls) ;
208280 }
209281 }
282+ self . call_caches . pop ( ) ;
210283 false
211284 }
212285 Statement :: If ( statement) => {
@@ -390,7 +463,7 @@ impl<'a> ProcedureAnalysis<'a> {
390463 }
391464 _ => self . eval_system_call ( call, & [ ] , true ) ,
392465 } ,
393- Factor :: FunctionCall ( call) => self . eval_call ( call, & [ ] ) ,
466+ Factor :: FunctionCall ( call) => self . eval_call_requested ( call, & [ ] , Some ( requested ) ) ,
394467 Factor :: HierVariable ( _)
395468 | Factor :: Value ( _)
396469 | Factor :: Anonymous ( _)
@@ -558,6 +631,75 @@ impl<'a> ProcedureAnalysis<'a> {
558631 }
559632
560633 fn eval_call ( & mut self , call : & FunctionCall , controls : & [ VersionId ] ) -> Vec < VersionId > {
634+ self . eval_call_requested ( call, controls, None )
635+ }
636+
637+ fn eval_call_requested (
638+ & mut self ,
639+ call : & FunctionCall ,
640+ controls : & [ VersionId ] ,
641+ requested : Option < PackedSpan > ,
642+ ) -> Vec < VersionId > {
643+ #[ cfg( test) ]
644+ if matches ! ( self . call_caches. last( ) , Some ( None ) ) {
645+ FUNCTION_BARRIER_EVALUATIONS . set ( FUNCTION_BARRIER_EVALUATIONS . get ( ) + 1 ) ;
646+ }
647+ let cache_key = std:: ptr:: from_ref ( call) ;
648+ if let Some ( cached) = self
649+ . call_caches
650+ . last ( )
651+ . and_then ( Option :: as_ref)
652+ . and_then ( |cache| cache. get ( & cache_key) )
653+ {
654+ let result = self . select_call_result ( cached, requested) ;
655+ #[ cfg( test) ]
656+ FUNCTION_RESULT_VERSIONS . set ( FUNCTION_RESULT_VERSIONS . get ( ) + result. len ( ) ) ;
657+ return result;
658+ }
659+
660+ let evaluated = self . eval_call_uncached ( call, controls) ;
661+ let result = self . select_call_result ( & evaluated, requested) ;
662+ if let Some ( Some ( cache) ) = self . call_caches . last_mut ( ) {
663+ cache. insert ( cache_key, evaluated) ;
664+ }
665+ #[ cfg( test) ]
666+ FUNCTION_RESULT_VERSIONS . set ( FUNCTION_RESULT_VERSIONS . get ( ) + result. len ( ) ) ;
667+ result
668+ }
669+
670+ fn select_call_result (
671+ & self ,
672+ evaluated : & CallResult ,
673+ requested : Option < PackedSpan > ,
674+ ) -> Vec < VersionId > {
675+ let mut result = Vec :: new ( ) ;
676+ for regions in & evaluated. region_groups {
677+ let first = requested. map_or ( 0 , |requested| {
678+ regions. partition_point ( |( span, _) | {
679+ #[ cfg( test) ]
680+ FUNCTION_RESULT_REGION_PROBES . set ( FUNCTION_RESULT_REGION_PROBES . get ( ) + 1 ) ;
681+ span. end ( ) <= requested. start
682+ } )
683+ } ) ;
684+ for ( span, version) in & regions[ first..] {
685+ #[ cfg( test) ]
686+ FUNCTION_RESULT_REGION_PROBES . set ( FUNCTION_RESULT_REGION_PROBES . get ( ) + 1 ) ;
687+ if requested. is_some_and ( |requested| span. start >= requested. end ( ) ) {
688+ break ;
689+ }
690+ result. push ( * version) ;
691+ }
692+ }
693+ result. extend_from_slice ( & evaluated. opaque_sources ) ;
694+ result. sort_unstable ( ) ;
695+ result. dedup ( ) ;
696+ result
697+ }
698+
699+ fn eval_call_uncached ( & mut self , call : & FunctionCall , controls : & [ VersionId ] ) -> CallResult {
700+ #[ cfg( test) ]
701+ FUNCTION_EVALUATIONS . set ( FUNCTION_EVALUATIONS . get ( ) + 1 ) ;
702+
561703 let body = self . ctx . functions . get ( & call. id ) . and_then ( |function| {
562704 if let Some ( index) = & call. index {
563705 function. get_function ( index)
@@ -575,7 +717,10 @@ impl<'a> ProcedureAnalysis<'a> {
575717 self . write_destination ( destination, & sources, controls) ;
576718 }
577719 }
578- return sources;
720+ return CallResult {
721+ region_groups : Vec :: new ( ) ,
722+ opaque_sources : sources,
723+ } ;
579724 } ;
580725 let mut actual_sources = Vec :: new ( ) ;
581726
@@ -591,7 +736,9 @@ impl<'a> ProcedureAnalysis<'a> {
591736 }
592737 }
593738
739+ self . call_caches . push ( None ) ;
594740 self . eval_block ( & body. statements , controls) ;
741+ self . call_caches . pop ( ) ;
595742
596743 for ( path, destinations) in & call. outputs {
597744 let Some ( & formal) = body. arg_map . get ( path) else {
@@ -617,16 +764,19 @@ impl<'a> ProcedureAnalysis<'a> {
617764 }
618765 }
619766
620- let mut result = body
767+ let region_groups = body
621768 . ret
622- . map ( |ret| self . current_versions_for_id ( ret) )
769+ . map ( |ret| self . current_region_groups_for_id ( ret) )
623770 . unwrap_or_default ( ) ;
624- if statements_have_unknown ( & body. statements ) {
625- result. extend ( actual_sources) ;
771+ let opaque_sources = if statements_have_unknown ( & body. statements ) {
772+ actual_sources
773+ } else {
774+ Vec :: new ( )
775+ } ;
776+ CallResult {
777+ region_groups,
778+ opaque_sources,
626779 }
627- result. sort_unstable ( ) ;
628- result. dedup ( ) ;
629- result
630780 }
631781
632782 fn keys_for_id ( & self , id : VarId ) -> Vec < NodeKey > {
@@ -650,6 +800,26 @@ impl<'a> ProcedureAnalysis<'a> {
650800 . collect ( )
651801 }
652802
803+ fn current_region_groups_for_id ( & mut self , id : VarId ) -> Vec < Vec < ( PackedSpan , VersionId ) > > {
804+ let mut groups = Vec :: < Vec < ( PackedSpan , VersionId ) > > :: new ( ) ;
805+ let mut previous_array_span = None ;
806+ for key in self . keys_for_id ( id) {
807+ let Some ( span) = self . key_span ( key) else {
808+ continue ;
809+ } ;
810+ if previous_array_span != Some ( key. 1 ) {
811+ groups. push ( Vec :: new ( ) ) ;
812+ previous_array_span = Some ( key. 1 ) ;
813+ }
814+ let group = groups. last_mut ( ) . expect ( "pushed above" ) ;
815+ debug_assert ! ( group. last( ) . is_none_or( |( previous, _) | {
816+ previous. start <= span. start && previous. end( ) <= span. start
817+ } ) ) ;
818+ group. push ( ( span, self . ssa . read ( key) ) ) ;
819+ }
820+ groups
821+ }
822+
653823 fn eval_actual_for_formal_key (
654824 & mut self ,
655825 actual : & Expression ,
0 commit comments