@@ -429,6 +429,74 @@ fn write_factor_sig(s: &mut String, factor: &Factor) {
429429 }
430430}
431431
432+ /// If `expr` is a masked read-modify-write of RAM `vid` at `wr_index` —
433+ /// `(vid[wr_index] & ~m) | (d & m)`, each `&`/`|` commuting — return `(d, m)`.
434+ /// The retention read `vid[wr_index]` folds into the mask, so it costs no read
435+ /// port and a lookup-plus-RMW array stays 1R1W. Port counting (`read_pattern_ok`)
436+ /// and building (`conv::statement`) both call this, so they agree on which reads
437+ /// are retention reads.
438+ pub ( crate ) fn match_masked_write < ' a > (
439+ vid : air:: VarId ,
440+ wr_index : & air:: VarIndex ,
441+ expr : & ' a Expression ,
442+ ) -> Option < ( & ' a Expression , & ' a Expression ) > {
443+ let Expression :: Binary ( a, air:: Op :: BitOr , b, _) = expr else {
444+ return None ;
445+ } ;
446+ match_masked_arms ( vid, wr_index, a, b) . or_else ( || match_masked_arms ( vid, wr_index, b, a) )
447+ }
448+
449+ /// `retain` = `vid[wr_index] & ~m`, `write` = `d & m`. The two masks must be
450+ /// structurally identical, else it isn't a clean masked write — some bits would
451+ /// be both kept and written, or neither.
452+ fn match_masked_arms < ' a > (
453+ vid : air:: VarId ,
454+ wr_index : & air:: VarIndex ,
455+ retain : & ' a Expression ,
456+ write : & ' a Expression ,
457+ ) -> Option < ( & ' a Expression , & ' a Expression ) > {
458+ let Expression :: Binary ( ra, air:: Op :: BitAnd , rb, _) = retain else {
459+ return None ;
460+ } ;
461+ let notm: & Expression = if is_self_read ( vid, wr_index, ra) {
462+ rb
463+ } else if is_self_read ( vid, wr_index, rb) {
464+ ra
465+ } else {
466+ return None ;
467+ } ;
468+ let Expression :: Unary ( air:: Op :: BitNot , m_retain, _) = notm else {
469+ return None ;
470+ } ;
471+ let Expression :: Binary ( wa, air:: Op :: BitAnd , wb, _) = write else {
472+ return None ;
473+ } ;
474+ let m_sig = addr_signature ( m_retain) ;
475+ if addr_signature ( wb) == m_sig {
476+ Some ( ( wa, wb) )
477+ } else if addr_signature ( wa) == m_sig {
478+ Some ( ( wb, wa) )
479+ } else {
480+ None
481+ }
482+ }
483+
484+ /// `expr` is exactly `vid[wr_index]`: a whole-word self-read at the write's own
485+ /// index, no bit/part select.
486+ fn is_self_read ( vid : air:: VarId , wr_index : & air:: VarIndex , expr : & Expression ) -> bool {
487+ let Expression :: Term ( factor) = expr else {
488+ return false ;
489+ } ;
490+ let Factor :: Variable ( id, index, select, _) = & * * factor else {
491+ return false ;
492+ } ;
493+ * id == vid
494+ && select. is_empty ( )
495+ && index. 0 . len ( ) == 1
496+ && wr_index. 0 . len ( ) == 1
497+ && addr_signature ( & index. 0 [ 0 ] ) == addr_signature ( & wr_index. 0 [ 0 ] )
498+ }
499+
432500/// `mem[addr]` with a single dynamic index dimension and no bit/part select.
433501fn is_dynamic_whole_word ( dst : & AssignDestination ) -> bool {
434502 dst. index . 0 . len ( ) == 1
@@ -540,8 +608,21 @@ fn for_each_read_in_dsts(dsts: &[AssignDestination], vid: air::VarId, f: &mut Re
540608fn for_each_read_in_stmt ( stmt : & Statement , vid : air:: VarId , f : & mut ReadVisitor ) {
541609 match stmt {
542610 Statement :: Assign ( a) => {
543- for_each_read_in_expr ( & a. expr , vid, f) ;
544- for_each_read_in_dsts ( & a. dst , vid, f) ;
611+ // A masked write folds its retention read into the mask (see
612+ // `match_masked_write`), so count only `d`/`m`/dst, not that read. A
613+ // genuine read at the same index elsewhere is still a distinct
614+ // factor and counted.
615+ if a. dst . len ( ) == 1
616+ && a. dst [ 0 ] . id == vid
617+ && let Some ( ( d, m) ) = match_masked_write ( vid, & a. dst [ 0 ] . index , & a. expr )
618+ {
619+ for_each_read_in_expr ( d, vid, f) ;
620+ for_each_read_in_expr ( m, vid, f) ;
621+ for_each_read_in_dsts ( & a. dst , vid, f) ;
622+ } else {
623+ for_each_read_in_expr ( & a. expr , vid, f) ;
624+ for_each_read_in_dsts ( & a. dst , vid, f) ;
625+ }
545626 }
546627 Statement :: If ( i) => {
547628 for_each_read_in_expr ( & i. cond , vid, f) ;
0 commit comments