@@ -13,6 +13,12 @@ use super::summary::{TypeDiagnostic, TypeSummary, TypeSummaryMap};
1313/// Maximum number of fixed-point iterations for local type inference.
1414const MAX_TYPE_PASSES : usize = 128 ;
1515
16+ /// Upper bound (exclusive) of the valid Miden VM memory address range.
17+ ///
18+ /// Memory addresses must be in `[0, 2^32)`. Operations like `mem_load`
19+ /// and `mem_store` trap at runtime if the address is `>= 2^32`.
20+ const MAX_MEMORY_ADDRESS : u64 = 1u64 << 32 ;
21+
1622/// Abstract memory address identity for type tracking.
1723///
1824/// Two memory operations target the same logical address when they share
@@ -21,10 +27,22 @@ const MAX_TYPE_PASSES: usize = 128;
2127/// to the same constant or `locaddr` result.
2228#[ derive( Debug , Clone , Copy , PartialEq , Eq , Hash ) ]
2329enum MemAddressKey {
24- /// Immediate constant address (from `mem_store.N` / `mem_load.N`).
25- Constant ( u64 ) ,
30+ /// Constant address known to be in the valid memory range `[0, 2^32)`.
31+ ///
32+ /// Stored as `u32` to enforce the range invariant at the type level.
33+ /// Created from `Constant::Felt(n)` assignments where `n < 2^32`.
34+ Constant ( u32 ) ,
2635 /// Local-mapped address (from `locaddr.N`).
2736 LocalAddr ( u16 ) ,
37+ /// Local-mapped address offset by a known constant (from `locaddr.N + k`).
38+ ///
39+ /// Only created for `Add` operations (not `Sub`), because field `sub`
40+ /// computes `(a - b) mod p` which can wrap to addresses outside the
41+ /// procedure's local frame.
42+ ///
43+ /// The absolute address is not known at analysis time, but two operations
44+ /// sharing the same `(local_index, offset)` target the same location.
45+ LocalAddrOffset ( u16 , u32 ) ,
2846}
2947
3048/// Output of type analysis for a single procedure.
@@ -117,11 +135,29 @@ impl<'a> ProcTypeAnalyzer<'a> {
117135 /// Run fixed-point inference and mismatch checks.
118136 fn analyze ( & mut self , stmts : & [ Stmt ] ) -> ProcTypeAnalysisResult {
119137 for _ in 0 ..MAX_TYPE_PASSES {
120- let mut changed = false ;
121- changed |= self . infer_types_in_block ( stmts) ;
122- changed |= self . seed_requirements_in_block ( stmts) ;
123- changed |= self . propagate_requirements_in_block ( stmts) ;
124- if !changed {
138+ let prev_inferred = self . inferred . clone ( ) ;
139+ let prev_required = self . required . clone ( ) ;
140+ let prev_local_types = self . local_types . clone ( ) ;
141+ let prev_mem_types = self . mem_types . clone ( ) ;
142+ let prev_local_req = self . local_requirements . clone ( ) ;
143+ let prev_mem_req = self . mem_requirements . clone ( ) ;
144+ let prev_addr_keys = self . var_address_keys . clone ( ) ;
145+
146+ // Return values are intentionally discarded: convergence is
147+ // detected by comparing full state snapshots (below), not by
148+ // per-call `changed` flags, which can oscillate within a pass.
149+ let _ = self . infer_types_in_block ( stmts) ;
150+ let _ = self . seed_requirements_in_block ( stmts) ;
151+ let _ = self . propagate_requirements_in_block ( stmts) ;
152+
153+ if self . inferred == prev_inferred
154+ && self . required == prev_required
155+ && self . local_types == prev_local_types
156+ && self . mem_types == prev_mem_types
157+ && self . local_requirements == prev_local_req
158+ && self . mem_requirements == prev_mem_req
159+ && self . var_address_keys == prev_addr_keys
160+ {
125161 break ;
126162 }
127163 }
@@ -194,9 +230,9 @@ impl<'a> ProcTypeAnalyzer<'a> {
194230 let changed = self . set_inferred_type_for_var ( dest, self . infer_expr_type ( expr) ) ;
195231 // Track abstract address keys for memory type tracking.
196232 match expr {
197- Expr :: Constant ( Constant :: Felt ( n) ) => {
233+ Expr :: Constant ( Constant :: Felt ( n) ) if * n < MAX_MEMORY_ADDRESS => {
198234 self . var_address_keys
199- . insert ( VarKey :: from_var ( dest) , MemAddressKey :: Constant ( * n) ) ;
235+ . insert ( VarKey :: from_var ( dest) , MemAddressKey :: Constant ( * n as u32 ) ) ;
200236 }
201237 Expr :: Var ( src) => {
202238 if let Some ( key) =
@@ -205,6 +241,22 @@ impl<'a> ProcTypeAnalyzer<'a> {
205241 self . var_address_keys . insert ( VarKey :: from_var ( dest) , key) ;
206242 }
207243 }
244+ Expr :: Binary ( BinOp :: Add , lhs, rhs) => {
245+ // Propagate MemAddressKey through locaddr + constant offset.
246+ // Try both operand orderings since addition is commutative.
247+ // Sub is excluded: field sub computes (a - b) mod p, which
248+ // wraps to addresses outside the procedure's local frame.
249+ //
250+ // Uses `or` (eager) instead of `or_else` (lazy) because
251+ // a closure capturing `&self` conflicts with the outer
252+ // `&mut self` borrow.
253+ let key = self
254+ . resolve_addr_offset_key ( lhs, rhs)
255+ . or ( self . resolve_addr_offset_key ( rhs, lhs) ) ;
256+ if let Some ( key) = key {
257+ self . var_address_keys . insert ( VarKey :: from_var ( dest) , key) ;
258+ }
259+ }
208260 _ => { }
209261 }
210262 changed
@@ -287,6 +339,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
287339 let else_ty = self . inferred_type_for_var ( & phi. else_var ) ;
288340 changed |=
289341 self . set_inferred_type_for_var ( & phi. dest , then_ty. combine_paths ( else_ty) ) ;
342+ self . propagate_phi_address_key ( & phi. dest , & phi. then_var , & phi. else_var ) ;
290343 }
291344 changed
292345 }
@@ -298,6 +351,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
298351 let step_ty = self . inferred_type_for_var ( & phi. step ) ;
299352 changed |=
300353 self . set_inferred_type_for_var ( & phi. dest , init_ty. combine_paths ( step_ty) ) ;
354+ self . propagate_phi_address_key ( & phi. dest , & phi. init , & phi. step ) ;
301355 }
302356 changed
303357 }
@@ -343,7 +397,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
343397 return false ;
344398 } ;
345399 for ( idx, result) in results. iter ( ) . enumerate ( ) {
346- let ty = if summary. is_unknown ( ) {
400+ let ty = if summary. is_opaque ( ) {
347401 InferredType :: Unknown
348402 } else {
349403 summary
@@ -443,7 +497,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
443497 }
444498
445499 /// Infer type for a binary expression.
446- fn infer_binary_expr_type ( & self , op : BinOp , lhs : & Expr , rhs : & Expr ) -> InferredType {
500+ fn infer_binary_expr_type ( & self , op : BinOp , _lhs : & Expr , _rhs : & Expr ) -> InferredType {
447501 match op {
448502 BinOp :: Eq
449503 | BinOp :: Neq
@@ -467,18 +521,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
467521 | BinOp :: U32WrappingAdd
468522 | BinOp :: U32WrappingSub
469523 | BinOp :: U32WrappingMul => InferredType :: U32 ,
470- BinOp :: Add | BinOp :: Sub => {
471- let lhs_ty = self . infer_expr_type ( lhs) ;
472- let rhs_ty = self . infer_expr_type ( rhs) ;
473- match ( lhs_ty, rhs_ty) {
474- ( InferredType :: Address , InferredType :: Bool | InferredType :: U32 )
475- | ( InferredType :: Bool | InferredType :: U32 , InferredType :: Address ) => {
476- InferredType :: Address
477- }
478- _ => InferredType :: Felt ,
479- }
480- }
481- BinOp :: Mul | BinOp :: Div | BinOp :: U32Exp => InferredType :: Felt ,
524+ BinOp :: Add | BinOp :: Sub | BinOp :: Mul | BinOp :: Div | BinOp :: U32Exp => InferredType :: Felt ,
482525 }
483526 }
484527
@@ -495,6 +538,52 @@ impl<'a> ProcTypeAnalyzer<'a> {
495538 self . var_address_keys . get ( & VarKey :: from_var ( var) ) . copied ( )
496539 }
497540
541+ /// Propagate a `MemAddressKey` through a phi node when both incoming
542+ /// values share the same key. If the keys disagree or either is absent,
543+ /// no key is assigned (conservative).
544+ fn propagate_phi_address_key ( & mut self , dest : & Var , lhs : & Var , rhs : & Var ) {
545+ let lhs_key = self . mem_address_key_for_var ( lhs) ;
546+ let rhs_key = self . mem_address_key_for_var ( rhs) ;
547+ if let ( Some ( lk) , Some ( rk) ) = ( lhs_key, rhs_key)
548+ && lk == rk
549+ {
550+ self . var_address_keys . insert ( VarKey :: from_var ( dest) , lk) ;
551+ }
552+ }
553+
554+ /// Resolve a `MemAddressKey` for an address-plus-offset expression.
555+ ///
556+ /// Returns `Some(LocalAddrOffset(index, offset))` when `base_expr`
557+ /// resolves to a `LocalAddr` or `LocalAddrOffset` key and `offset_expr`
558+ /// is a constant in `[0, 2^32)`. Returns `None` if the accumulated
559+ /// offset overflows `u32` or the base is not a local address.
560+ fn resolve_addr_offset_key (
561+ & self ,
562+ base_expr : & Expr ,
563+ offset_expr : & Expr ,
564+ ) -> Option < MemAddressKey > {
565+ let base_key = match base_expr {
566+ Expr :: Var ( v) => self . mem_address_key_for_var ( v) ?,
567+ _ => return None ,
568+ } ;
569+ let offset: u32 = match offset_expr {
570+ Expr :: Constant ( Constant :: Felt ( n) ) if * n < MAX_MEMORY_ADDRESS => * n as u32 ,
571+ Expr :: Var ( v) => match self . mem_address_key_for_var ( v) ? {
572+ MemAddressKey :: Constant ( n) => n,
573+ _ => return None ,
574+ } ,
575+ _ => return None ,
576+ } ;
577+ match base_key {
578+ MemAddressKey :: LocalAddr ( index) => Some ( MemAddressKey :: LocalAddrOffset ( index, offset) ) ,
579+ MemAddressKey :: LocalAddrOffset ( index, base_offset) => {
580+ let total = base_offset. checked_add ( offset) ?;
581+ Some ( MemAddressKey :: LocalAddrOffset ( index, total) )
582+ }
583+ MemAddressKey :: Constant ( _) => None ,
584+ }
585+ }
586+
498587 /// Record the inferred type for a local variable slot from stored values.
499588 ///
500589 /// Combines the types of all stored values via [`InferredType::refine`] and
@@ -625,7 +714,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
625714 let Some ( summary) = self . summary_for_target ( target) . cloned ( ) else {
626715 return false ;
627716 } ;
628- if summary. is_unknown ( ) {
717+ if summary. is_opaque ( ) {
629718 return false ;
630719 }
631720 let mut changed = false ;
@@ -791,6 +880,16 @@ impl<'a> ProcTypeAnalyzer<'a> {
791880 if req == TypeRequirement :: Unknown {
792881 continue ;
793882 }
883+ // Sticky Unknown: once a slot's requirement collapses to
884+ // Unknown from a conflict (two incompatible non-Unknown
885+ // requirements), it stays Unknown for the rest of this pass.
886+ // This prevents order-dependent results when 3+ loads from
887+ // the same slot have conflicting requirements.
888+ if let Some ( & TypeRequirement :: Unknown ) =
889+ self . local_requirements . get ( & load. index )
890+ {
891+ continue ;
892+ }
794893 let current = self
795894 . local_requirements
796895 . get ( & load. index )
@@ -822,6 +921,12 @@ impl<'a> ProcTypeAnalyzer<'a> {
822921 if req == TypeRequirement :: Unknown {
823922 continue ;
824923 }
924+ // Sticky Unknown: see comment in LocalLoad handler.
925+ if let Some ( & TypeRequirement :: Unknown ) =
926+ self . mem_requirements . get ( & addr_key)
927+ {
928+ continue ;
929+ }
825930 let current = self
826931 . mem_requirements
827932 . get ( & addr_key)
@@ -1127,7 +1232,7 @@ impl<'a> ProcTypeAnalyzer<'a> {
11271232 let Some ( summary) = self . summary_for_target ( target) . cloned ( ) else {
11281233 return ;
11291234 } ;
1130- if summary. is_unknown ( ) {
1235+ if summary. is_opaque ( ) {
11311236 return ;
11321237 }
11331238 let callee = SymbolPath :: new ( target. to_string ( ) ) ;
0 commit comments