Repository: ProvableHQ/leo (2026-04-01T20:02:30Z)

Issue:
[Bug] Panic when attempting to use array as key in mapping
# 🐛 Bug Report
I think the title is the issue, but not 100% certain from the panic message

## Code

```leo
// The 'arc20' program.
program arc20.aleo: ARC20 {
    mapping balances: address => u128;
    mapping allowances: [address;2] => u128;  // This seems to be the issue
    ...
```

## Error:
```
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'arc20.aleo'
thread `main` panicked at crates/passes/src/ssa_const_propagation/ast.rs:70:37:
Expected type information to be available
stack backtrace: 
   0: __mh_execute_header
   1: __mh_execute_header
   2: __mh_execute_header
   3: __mh_execute_header
   4: __mh_execute_header
   5: __mh_execute_header
   6: __mh_execute_header
   7: __mh_execute_header
   8: __mh_execute_header
   9: __mh_execute_header
  10: __mh_execute_header
  11: __mh_execute_header
  12: __mh_execute_header
  13: __mh_execute_header
  14: __mh_execute_header
  15: __mh_execute_header
  16: __mh_execute_header
  17: __mh_execute_header
  18: __mh_execute_header
  19: __mh_execute_header
  20: __mh_execute_header
  21: __mh_execute_header
  22: __mh_execute_header
  23: __mh_execute_header
  24: __mh_execute_header
  25: __mh_execute_header
  26: __mh_execute_header
  27: __mh_execute_header
  28: __mh_execute_header
  29: __mh_execute_header
  30: __mh_execute_header
  31: __mh_execute_header
  32: __mh_execute_header
  33: __mh_execute_header
  34: __mh_execute_header
  35: __mh_execute_header

error: internal compiler error: unexpected panic

note: the compiler unexpectedly panicked. this is a bug.

note: we would appreciate a bug report: https://github.com/ProvableHQ/leo/issues/new?labels=bug,panic&template=bug.md&title=[Bug]

note: leo 4.0.0 (d9207de49e HEAD) features=[noconfig] running on Darwin 25.4.0

note: compiler args: leo build

note: compiler flags: CLI { debug: false, quiet: false, json_output: None, disable_update_check: false, command: Build { command: LeoBuild { options: BuildOptions { offline: false, enable_ast_spans: false, enable_dce: true, conditional_block_max_depth: 10, disable_conditional_branch_type_checking: false, enable_initial_ast_snapshot: false, enable_all_ast_snapshots: false, ast_snapshots: [], build_tests: false, no_cache: false, no_local: false }, env_override: EnvOptions { private_key: None, network: None, endpoint: None, devnet: false, consensus_heights: None } } }, path: None, home: None }
```



Discussion:

Patch:
diff --git a/crates/ast/src/passes/visitor.rs b/crates/ast/src/passes/visitor.rs
index 13c10c1af8..0df601d562 100644
--- a/crates/ast/src/passes/visitor.rs
+++ b/crates/ast/src/passes/visitor.rs
@@ -363,6 +363,7 @@ pub trait ProgramVisitor: AstVisitor {
     fn visit_program_scope(&mut self, input: &ProgramScope) {
         input.consts.iter().for_each(|(_, c)| self.visit_const(c));
         input.composites.iter().for_each(|(_, c)| self.visit_composite(c));
+        input.interfaces.iter().for_each(|(_, c)| self.visit_interface(c));
         input.mappings.iter().for_each(|(_, c)| self.visit_mapping(c));
         input.storage_variables.iter().for_each(|(_, c)| self.visit_storage_variable(c));
         input.functions.iter().for_each(|(_, c)| self.visit_function(c));
@@ -374,6 +375,7 @@ pub trait ProgramVisitor: AstVisitor {
     fn visit_module(&mut self, input: &Module) {
         input.consts.iter().for_each(|(_, c)| self.visit_const(c));
         input.composites.iter().for_each(|(_, c)| self.visit_composite(c));
+        input.interfaces.iter().for_each(|(_, c)| self.visit_interface(c));
         input.functions.iter().for_each(|(_, c)| self.visit_function(c));
     }
 
@@ -402,6 +404,8 @@ pub trait ProgramVisitor: AstVisitor {
     fn visit_interface(&mut self, input: &Interface) {
         input.functions.iter().for_each(|(_, f)| self.visit_function_prototype(f));
         input.records.iter().for_each(|(_, r)| self.visit_record_prototype(r));
+        input.mappings.iter().for_each(|m| self.visit_mapping(m));
+        input.storages.iter().for_each(|s| self.visit_storage_variable(s));
     }
 
     fn visit_function_prototype(&mut self, input: &FunctionPrototype) {
diff --git a/crates/passes/src/const_propagation/program.rs b/crates/passes/src/const_propagation/program.rs
index 5999e1767f..74a91fa312 100644
--- a/crates/passes/src/const_propagation/program.rs
+++ b/crates/passes/src/const_propagation/program.rs
@@ -51,6 +51,7 @@ impl ProgramReconstructor for ConstPropagationVisitor<'_> {
         }
 
         input.composites = input.composites.into_iter().map(|(i, c)| (i, self.reconstruct_composite(c))).collect();
+        input.interfaces = input.interfaces.into_iter().map(|(i, int)| (i, self.reconstruct_interface(int))).collect();
         input.mappings =
             input.mappings.into_iter().map(|(id, mapping)| (id, self.reconstruct_mapping(mapping))).collect();
         input.storage_variables =
diff --git a/crates/passes/src/name_validation/program.rs b/crates/passes/src/name_validation/program.rs
index 095081ef53..6486a34570 100644
--- a/crates/passes/src/name_validation/program.rs
+++ b/crates/passes/src/name_validation/program.rs
@@ -31,11 +31,13 @@ impl ProgramVisitor for NameValidationVisitor<'_> {
         self.is_not_keyword(program_name, "program", &[]);
 
         input.composites.iter().for_each(|(_, function)| self.visit_composite(function));
+        input.interfaces.iter().for_each(|(_, interface)| self.visit_interface(interface));
         input.functions.iter().for_each(|(_, function)| self.visit_function(function));
     }
 
     fn visit_module(&mut self, input: &Module) {
         input.composites.iter().for_each(|(_, function)| self.visit_composite(function));
+        input.interfaces.iter().for_each(|(_, interface)| self.visit_interface(interface));
         input.functions.iter().for_each(|(_, function)| self.visit_function(function));
     }
 
diff --git a/crates/passes/src/static_analysis/program.rs b/crates/passes/src/static_analysis/program.rs
index a33d592186..face1a84ab 100644
--- a/crates/passes/src/static_analysis/program.rs
+++ b/crates/passes/src/static_analysis/program.rs
@@ -26,6 +26,7 @@ impl ProgramVisitor for StaticAnalyzingVisitor<'_> {
         // Do the default implementation for visiting the program scope.
         input.consts.iter().for_each(|(_, c)| self.visit_const(c));
         input.composites.iter().for_each(|(_, c)| self.visit_composite(c));
+        input.interfaces.iter().for_each(|(_, c)| self.visit_interface(c));
         input.mappings.iter().for_each(|(_, c)| self.visit_mapping(c));
         input.storage_variables.iter().for_each(|(_, c)| self.visit_storage_variable(c));
         input.functions.iter().for_each(|(_, c)| self.visit_function(c));
diff --git a/crates/passes/src/type_checking/program.rs b/crates/passes/src/type_checking/program.rs
index e92081b6d0..6cc6303c8e 100644
--- a/crates/passes/src/type_checking/program.rs
+++ b/crates/passes/src/type_checking/program.rs
@@ -88,6 +88,11 @@ impl ProgramVisitor for TypeCheckingVisitor<'_> {
             ));
         }
 
+        // Typecheck each interface definition.
+        for (_, interface) in input.interfaces.iter() {
+            self.visit_interface(interface);
+        }
+
         // Typecheck each mapping definition.
         let mut mapping_count = 0;
         for (_, mapping) in input.mappings.iter() {
@@ -158,6 +163,9 @@ impl ProgramVisitor for TypeCheckingVisitor<'_> {
         // Typecheck each composite definition.
         input.composites.iter().for_each(|(_, function)| self.visit_composite(function));
 
+        // Typecheck each interface definition.
+        input.interfaces.iter().for_each(|(_, interface)| self.visit_interface(interface));
+
         for (_, function) in input.functions.iter() {
             self.visit_function(function);
         }


Test patch:
diff --git a/tests/expectations/compiler/mappings/array_key_mapping.out b/tests/expectations/compiler/mappings/array_key_mapping.out
new file mode 100644
index 0000000000..2fa6f16a94
--- /dev/null
+++ b/tests/expectations/compiler/mappings/array_key_mapping.out
@@ -0,0 +1,16 @@
+program test.aleo;
+
+mapping allowances:
+    key as [address; 2u32].public;
+    value as u128.public;
+
+function foo:
+    input r0 as address.private;
+    input r1 as address.private;
+    cast r0 r1 into r2 as [address; 2u32];
+    async foo r2 into r3;
+    output r3 as test.aleo/foo.future;
+
+finalize foo:
+    input r0 as [address; 2u32].public;
+    get.or_use allowances[r0] 0u128 into r1;
diff --git a/tests/expectations/compiler/mappings/interface_array_key_mapping.out b/tests/expectations/compiler/mappings/interface_array_key_mapping.out
new file mode 100644
index 0000000000..2fa6f16a94
--- /dev/null
+++ b/tests/expectations/compiler/mappings/interface_array_key_mapping.out
@@ -0,0 +1,16 @@
+program test.aleo;
+
+mapping allowances:
+    key as [address; 2u32].public;
+    value as u128.public;
+
+function foo:
+    input r0 as address.private;
+    input r1 as address.private;
+    cast r0 r1 into r2 as [address; 2u32];
+    async foo r2 into r3;
+    output r3 as test.aleo/foo.future;
+
+finalize foo:
+    input r0 as [address; 2u32].public;
+    get.or_use allowances[r0] 0u128 into r1;
diff --git a/tests/expectations/passes/const_prop_unroll_and_morphing/interface_array_key_mapping.out b/tests/expectations/passes/const_prop_unroll_and_morphing/interface_array_key_mapping.out
new file mode 100644
index 0000000000..0eda7ea79a
--- /dev/null
+++ b/tests/expectations/passes/const_prop_unroll_and_morphing/interface_array_key_mapping.out
@@ -0,0 +1,7 @@
+program test.aleo {
+    const N: u32 = 2u32;
+    mapping allowances: [address; 2] => u128;
+    fn foo() {
+    }
+}
+
diff --git a/tests/expectations/passes/ssa_const_propagation/array_key_in_mapping.out b/tests/expectations/passes/ssa_const_propagation/array_key_in_mapping.out
new file mode 100644
index 0000000000..5bf316cc64
--- /dev/null
+++ b/tests/expectations/passes/ssa_const_propagation/array_key_in_mapping.out
@@ -0,0 +1,6 @@
+program test.aleo {
+    mapping allowances: [address; 2] => u128;
+    fn foo() {
+    }
+}
+
diff --git a/tests/expectations/passes/ssa_const_propagation/interface_array_key_mapping.out b/tests/expectations/passes/ssa_const_propagation/interface_array_key_mapping.out
new file mode 100644
index 0000000000..5bf316cc64
--- /dev/null
+++ b/tests/expectations/passes/ssa_const_propagation/interface_array_key_mapping.out
@@ -0,0 +1,6 @@
+program test.aleo {
+    mapping allowances: [address; 2] => u128;
+    fn foo() {
+    }
+}
+
diff --git a/tests/expectations/passes/ssa_const_propagation/library_module_interface_array_key.out b/tests/expectations/passes/ssa_const_propagation/library_module_interface_array_key.out
new file mode 100644
index 0000000000..cbbc23ebbf
--- /dev/null
+++ b/tests/expectations/passes/ssa_const_propagation/library_module_interface_array_key.out
@@ -0,0 +1,9 @@
+Error [ETYC0372186]: Interface definitions are not allowed inside library modules.
+    --> token_types.leo:2:1
+     |
+   2 | interface TokenStorage {
+     | ^^^^^^^^^^^^^^^^^^^^^^^^
+   3 |     mapping allowances: [address; 2] => u128;
+     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   4 | }
+     | ^
diff --git a/tests/tests/compiler/mappings/array_key_mapping.leo b/tests/tests/compiler/mappings/array_key_mapping.leo
new file mode 100644
index 0000000000..5722e81141
--- /dev/null
+++ b/tests/tests/compiler/mappings/array_key_mapping.leo
@@ -0,0 +1,12 @@
+program test.aleo {
+    mapping allowances: [address; 2] => u128;
+
+    fn foo(key0: address, key1: address) -> Final {
+        let key: [address; 2] = [key0, key1];
+        return final { fin_foo(key); };
+    }
+}
+
+final fn fin_foo(key: [address; 2]) {
+    let val: u128 = allowances.get_or_use(key, 0u128);
+}
diff --git a/tests/tests/compiler/mappings/interface_array_key_mapping.leo b/tests/tests/compiler/mappings/interface_array_key_mapping.leo
new file mode 100644
index 0000000000..f948424c7e
--- /dev/null
+++ b/tests/tests/compiler/mappings/interface_array_key_mapping.leo
@@ -0,0 +1,19 @@
+// Test: interface with array mapping key compiles end-to-end.
+// Exercises name_validation and static_analysis passes for interface mappings.
+
+interface TokenStorage {
+    mapping allowances: [address; 2] => u128;
+}
+
+program test.aleo: TokenStorage {
+    mapping allowances: [address; 2] => u128;
+
+    fn foo(key0: address, key1: address) -> Final {
+        let key: [address; 2] = [key0, key1];
+        return final { fin_foo(key); };
+    }
+}
+
+final fn fin_foo(key: [address; 2]) {
+    let val: u128 = allowances.get_or_use(key, 0u128);
+}
diff --git a/tests/tests/passes/const_prop_unroll_and_morphing/interface_array_key_mapping.leo b/tests/tests/passes/const_prop_unroll_and_morphing/interface_array_key_mapping.leo
new file mode 100644
index 0000000000..f3521ca70c
--- /dev/null
+++ b/tests/tests/passes/const_prop_unroll_and_morphing/interface_array_key_mapping.leo
@@ -0,0 +1,16 @@
+// Test: const propagation reconstructs interfaces in program scopes, including
+// array lengths that reference const variables. Without the fix to
+// reconstruct_program_scope, the interface mapping key would retain the unfolded
+// const variable instead of the concrete value.
+
+const N: u32 = 2u32;
+
+interface TokenStorage {
+    mapping allowances: [address; N] => u128;
+}
+
+program test.aleo: TokenStorage {
+    mapping allowances: [address; N] => u128;
+
+    fn foo() {}
+}
diff --git a/tests/tests/passes/ssa_const_propagation/array_key_in_mapping.leo b/tests/tests/passes/ssa_const_propagation/array_key_in_mapping.leo
new file mode 100644
index 0000000000..aff50a6f0e
--- /dev/null
+++ b/tests/tests/passes/ssa_const_propagation/array_key_in_mapping.leo
@@ -0,0 +1,5 @@
+program test.aleo {
+    mapping allowances: [address; 2] => u128;
+
+    fn foo() {}
+}
diff --git a/tests/tests/passes/ssa_const_propagation/interface_array_key_mapping.leo b/tests/tests/passes/ssa_const_propagation/interface_array_key_mapping.leo
new file mode 100644
index 0000000000..71c904c622
--- /dev/null
+++ b/tests/tests/passes/ssa_const_propagation/interface_array_key_mapping.leo
@@ -0,0 +1,13 @@
+// Test: An interface declaring a mapping with an array key type does not panic in
+// const propagation passes. The interface's array length literal must be visited
+// by the type checker so its NodeID is in the type table.
+
+interface TokenStorage {
+    mapping allowances: [address; 2] => u128;
+}
+
+program test.aleo: TokenStorage {
+    mapping allowances: [address; 2] => u128;
+
+    fn foo() {}
+}
diff --git a/tests/tests/passes/ssa_const_propagation/library_module_interface_array_key.leo b/tests/tests/passes/ssa_const_propagation/library_module_interface_array_key.leo
new file mode 100644
index 0000000000..d97731d07a
--- /dev/null
+++ b/tests/tests/passes/ssa_const_propagation/library_module_interface_array_key.leo
@@ -0,0 +1,21 @@
+// Test: interface with array mapping key in a library submodule does not panic.
+// The type checker must visit interface mappings inside library submodules so that
+// the type table is populated for the array length literal. Without the fix to
+// visit_module, SSA const propagation panics when reconstructing the interface.
+
+// --- library: token_lib --- //
+
+// --- Next Module: token_types.leo --- //
+
+interface TokenStorage {
+    mapping allowances: [address; 2] => u128;
+}
+
+// --- Next Program --- //
+
+program test.aleo {
+    fn foo() {}
+
+    @noupgrade
+    constructor() {}
+}