Skip to content

Commit 4ad52e9

Browse files
feggeclaude
andcommitted
Rename TypeSummary.unknown to TypeSummary.opaque
Decouples opacity tracking from the Unknown type variant in preparation for removing Unknown from the semantic type domain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 781b1e4 commit 4ad52e9

File tree

3 files changed

+144
-39
lines changed

3 files changed

+144
-39
lines changed

src/types/inter.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,23 @@ fn infer_summary_for_node(
4848
) -> TypeSummary {
4949
let proc_path = crate::symbol::path::SymbolPath::new(fq_name.to_string());
5050
let Some(signature) = signatures.get(&proc_path) else {
51-
return TypeSummary::unknown();
51+
return TypeSummary::opaque();
5252
};
5353

5454
let (inputs, outputs) = match signature {
5555
ProcSignature::Known {
5656
inputs, outputs, ..
5757
} => (*inputs, *outputs),
58-
ProcSignature::Unknown => return TypeSummary::unknown(),
58+
ProcSignature::Unknown => return TypeSummary::opaque(),
5959
};
6060

6161
let Some((program, proc)) = workspace.lookup_proc_entry(&proc_path) else {
62-
return TypeSummary::unknown_with_arity(inputs, outputs);
62+
return TypeSummary::opaque_with_arity(inputs, outputs);
6363
};
6464
let resolver = create_resolver(program.module(), workspace.source_manager());
6565
let stmts = match lift::lift_proc(proc, &proc_path, &resolver, signatures) {
6666
Ok(stmts) => stmts,
67-
Err(_err) => return TypeSummary::unknown_with_arity(inputs, outputs),
67+
Err(_err) => return TypeSummary::opaque_with_arity(inputs, outputs),
6868
};
6969

7070
let analysis = analyze_proc_types(&proc_path, inputs, outputs, &stmts, callee_summaries);

src/types/intra.rs

Lines changed: 130 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ use super::summary::{TypeDiagnostic, TypeSummary, TypeSummaryMap};
1313
/// Maximum number of fixed-point iterations for local type inference.
1414
const 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)]
2329
enum 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());

src/types/summary.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct TypeSummary {
2121
/// new return values on the stack).
2222
pub outputs: Vec<InferredType>,
2323
/// Indicates the summary is opaque and should not be used for mismatch checks.
24-
pub unknown: bool,
24+
pub opaque: bool,
2525
}
2626

2727
impl TypeSummary {
@@ -30,33 +30,33 @@ impl TypeSummary {
3030
Self {
3131
inputs,
3232
outputs,
33-
unknown: false,
33+
opaque: false,
3434
}
3535
}
3636

3737
/// Create an opaque summary with explicit input/output arity.
38-
pub fn unknown_with_arity(inputs: usize, outputs: usize) -> Self {
38+
pub fn opaque_with_arity(inputs: usize, outputs: usize) -> Self {
3939
Self {
4040
inputs: vec![TypeRequirement::Unknown; inputs],
4141
outputs: vec![InferredType::Unknown; outputs],
42-
unknown: true,
42+
opaque: true,
4343
}
4444
}
4545

46-
/// Create a fully unknown summary without arity information.
47-
pub fn unknown() -> Self {
48-
Self::unknown_with_arity(0, 0)
46+
/// Create a fully opaque summary without arity information.
47+
pub fn opaque() -> Self {
48+
Self::opaque_with_arity(0, 0)
4949
}
5050

5151
/// Returns true if this summary is opaque.
52-
pub const fn is_unknown(&self) -> bool {
53-
self.unknown
52+
pub const fn is_opaque(&self) -> bool {
53+
self.opaque
5454
}
5555
}
5656

5757
impl Default for TypeSummary {
5858
fn default() -> Self {
59-
Self::unknown()
59+
Self::opaque()
6060
}
6161
}
6262

0 commit comments

Comments
 (0)