Skip to content

Commit af59efe

Browse files
mohammadfawazMohammad Fawaz
andauthored
Fix compiler error with dyn record (#29296)
Co-authored-by: Mohammad Fawaz <mohammadfawaz@gmail.com>
1 parent 10d4c4c commit af59efe

File tree

13 files changed

+139
-124
lines changed

13 files changed

+139
-124
lines changed

crates/ast/src/interface/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ impl Interface {
4848
pub fn name(&self) -> Symbol {
4949
self.identifier.name
5050
}
51+
52+
/// Returns `true` if `ty` is a composite type whose name matches a record declared in this interface.
53+
pub fn is_record_type(&self, ty: &Type) -> bool {
54+
if let Type::Composite(ct) = ty
55+
&& let Some(loc) = ct.path.try_global_location()
56+
&& let Some(&name) = loc.path.first()
57+
{
58+
return self.records.iter().any(|(n, _)| *n == name);
59+
}
60+
false
61+
}
5162
}
5263

5364
impl PartialEq for Interface {

crates/passes/src/code_generation/expression.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ impl CodeGeneratingVisitor<'_> {
625625
.iter()
626626
.map(|inp| {
627627
let viz = AleoVisibility::maybe_from(inp.mode()).or(Some(AleoVisibility::Private));
628-
self.dynamic_call_input_type(&inp.type_, viz)
628+
self.dynamic_call_input_type(&inp.type_, viz, Some(&interface))
629629
})
630630
.collect();
631631

@@ -648,8 +648,11 @@ impl CodeGeneratingVisitor<'_> {
648648

649649
// Determine output types from the function prototype.
650650
// call.dynamic requires explicit visibility on every type; default Mode::None to Private.
651-
let output_types: Vec<(AleoType, Option<AleoVisibility>)> =
652-
func_proto.output.iter().map(|out| self.dynamic_call_output_type(&out.type_, out.mode)).collect();
651+
let output_types: Vec<(AleoType, Option<AleoVisibility>)> = func_proto
652+
.output
653+
.iter()
654+
.map(|out| self.dynamic_call_output_type(&out.type_, out.mode, Some(&interface)))
655+
.collect();
653656

654657
// Emit CallDynamic instruction.
655658
instructions.push(AleoStmt::CallDynamic(
@@ -695,7 +698,7 @@ impl CodeGeneratingVisitor<'_> {
695698
.iter()
696699
.map(|(mode, type_, _)| {
697700
let viz = AleoVisibility::maybe_from(*mode).or(Some(AleoVisibility::Private));
698-
self.dynamic_call_input_type(type_, viz)
701+
self.dynamic_call_input_type(type_, viz, None)
699702
})
700703
.collect()
701704
} else {
@@ -711,7 +714,7 @@ impl CodeGeneratingVisitor<'_> {
711714
.get(&arg.id())
712715
.expect("Type checking guarantees argument types are in the type table");
713716
let viz = Some(AleoVisibility::Private);
714-
self.dynamic_call_input_type(&ty, viz)
717+
self.dynamic_call_input_type(&ty, viz, None)
715718
})
716719
.collect()
717720
};
@@ -724,8 +727,11 @@ impl CodeGeneratingVisitor<'_> {
724727
}
725728

726729
// Determine output types with visibility annotations.
727-
let output_types: Vec<(AleoType, Option<AleoVisibility>)> =
728-
input.return_types.iter().map(|(mode, type_, _)| self.dynamic_call_output_type(type_, *mode)).collect();
730+
let output_types: Vec<(AleoType, Option<AleoVisibility>)> = input
731+
.return_types
732+
.iter()
733+
.map(|(mode, type_, _)| self.dynamic_call_output_type(type_, *mode, None))
734+
.collect();
729735

730736
instructions.push(AleoStmt::CallDynamic(
731737
prog,

crates/passes/src/code_generation/type_.rs

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
use super::*;
1818

19-
use leo_ast::{IntegerType, Mode, Type};
19+
use leo_ast::{IntegerType, Interface, Mode, Type};
2020

2121
impl CodeGeneratingVisitor<'_> {
2222
pub fn visit_type(&self, input: &Type) -> AleoType {
@@ -117,46 +117,34 @@ impl CodeGeneratingVisitor<'_> {
117117
(self.visit_type(type_), visibility)
118118
}
119119

120-
/// Maps a dynamic call input type and mode to an AVM type with visibility.
121-
/// Record types (both concrete and `dyn record`) become `DynamicRecord` (no visibility).
122-
/// All others use the provided visibility (defaulting to Private).
120+
/// Maps a dynamic call input type to an AVM type. `dyn record` and interface record types
121+
/// become `DynamicRecord`; all others use the provided visibility.
123122
pub fn dynamic_call_input_type(
124123
&self,
125124
type_: &Type,
126125
visibility: Option<AleoVisibility>,
126+
interface: Option<&Interface>,
127127
) -> (AleoType, Option<AleoVisibility>) {
128-
if matches!(type_, Type::DynRecord) {
128+
if matches!(type_, Type::DynRecord) || interface.is_some_and(|i| i.is_record_type(type_)) {
129129
return (AleoType::DynamicRecord, None);
130130
}
131-
if let Type::Composite(composite) = type_ {
132-
let composite_location = composite.path.expect_global_location();
133-
let this_program_name = self.program_id.unwrap().as_symbol();
134-
if self.state.symbol_table.lookup_record(this_program_name, composite_location).is_some() {
135-
return (AleoType::DynamicRecord, None);
136-
}
137-
}
138131
(self.visit_type(type_), visibility)
139132
}
140133

141-
/// Maps a dynamic call output type and mode to an AVM type with visibility.
142-
/// Futures become `DynamicFuture` (no visibility), record types (both concrete
143-
/// and `dyn record`) become `DynamicRecord` (no visibility), all others use the
144-
/// provided mode (defaulting to Private).
145-
pub fn dynamic_call_output_type(&self, type_: &Type, mode: Mode) -> (AleoType, Option<AleoVisibility>) {
134+
/// Maps a dynamic call output type to an AVM type. Futures become `DynamicFuture`, `dyn record`
135+
/// and interface record types become `DynamicRecord`; all others use the provided mode.
136+
pub fn dynamic_call_output_type(
137+
&self,
138+
type_: &Type,
139+
mode: Mode,
140+
interface: Option<&Interface>,
141+
) -> (AleoType, Option<AleoVisibility>) {
146142
if matches!(type_, Type::Future(..)) {
147143
(AleoType::DynamicFuture, None)
148-
} else if matches!(type_, Type::DynRecord) {
144+
} else if matches!(type_, Type::DynRecord) || interface.is_some_and(|i| i.is_record_type(type_)) {
149145
(AleoType::DynamicRecord, None)
150146
} else {
151147
let viz = AleoVisibility::maybe_from(mode).or(Some(AleoVisibility::Private));
152-
// Check if this is a concrete record type — those also become DynamicRecord.
153-
if let Type::Composite(composite) = type_ {
154-
let composite_location = composite.path.expect_global_location();
155-
let this_program_name = self.program_id.unwrap().as_symbol();
156-
if self.state.symbol_table.lookup_record(this_program_name, composite_location).is_some() {
157-
return (AleoType::DynamicRecord, None);
158-
}
159-
}
160148
self.visit_type_with_visibility(type_, viz)
161149
}
162150
}

crates/passes/src/type_checking/ast.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,8 +1087,9 @@ impl AstVisitor for TypeCheckingVisitor<'_> {
10871087
// Check argument types. Record-typed parameters require `dyn record` at the call site.
10881088
for (expected_input, argument) in func_proto.input.iter().zip(input.arguments.iter()) {
10891089
let proto_type = expected_input.type_().clone();
1090-
if self.is_record_type(&proto_type) {
1091-
let actual_type = self.visit_expression(argument, &Some(Type::DynRecord));
1090+
if interface.is_record_type(&proto_type) {
1091+
// Visit without an expected type so only the explicit error below fires.
1092+
let actual_type = self.visit_expression(argument, &None);
10921093
if !matches!(actual_type, Type::DynRecord | Type::Err) {
10931094
self.emit_err(TypeCheckerError::dynamic_call_record_arg_requires_dyn_record(
10941095
&proto_type,
@@ -1100,9 +1101,8 @@ impl AstVisitor for TypeCheckingVisitor<'_> {
11001101
}
11011102
}
11021103

1103-
// If the output type contains a Future, mark it as coming from a dynamic call.
1104-
// Replace any record types in the output with `dyn record`.
1105-
let output_type = self.replace_records_with_dyn_record(&func_proto.output_type);
1104+
// Replace interface record types in the output with `dyn record`, then check for futures.
1105+
let output_type = self.replace_records_with_dyn_record(&func_proto.output_type, &interface);
11061106
let contains_future = match &output_type {
11071107
Type::Future(..) => true,
11081108
Type::Tuple(tuple) => tuple.elements().iter().any(|t| matches!(t, Type::Future(..))),

crates/passes/src/type_checking/visitor.rs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2256,26 +2256,15 @@ impl TypeCheckingVisitor<'_> {
22562256
comp.cloned()
22572257
}
22582258

2259-
/// Returns `true` if `ty` is a composite type whose underlying definition is a record.
2260-
pub fn is_record_type(&mut self, ty: &Type) -> bool {
2261-
if let Type::Composite(ct) = ty
2262-
&& let Some(comp) = self.lookup_composite(ct.path.expect_global_location())
2263-
{
2264-
return comp.is_record;
2265-
}
2266-
false
2267-
}
2268-
2269-
/// Replaces any record-typed composites with `Type::DynRecord`.
2270-
/// Only recurses into tuples — records cannot be nested inside structs or arrays.
2271-
pub fn replace_records_with_dyn_record(&mut self, ty: &Type) -> Type {
2259+
/// Replaces interface record types with `Type::DynRecord`. Only recurses into tuples — records cannot be nested inside structs or arrays.
2260+
pub fn replace_records_with_dyn_record(&mut self, ty: &Type, interface: &Interface) -> Type {
22722261
match ty {
22732262
Type::DynRecord => Type::DynRecord,
22742263
Type::Tuple(tuple) => Type::Tuple(TupleType::new(
2275-
tuple.elements().iter().map(|t| self.replace_records_with_dyn_record(t)).collect(),
2264+
tuple.elements().iter().map(|t| self.replace_records_with_dyn_record(t, interface)).collect(),
22762265
)),
22772266
other => {
2278-
if self.is_record_type(other) {
2267+
if interface.is_record_type(other) {
22792268
Type::DynRecord
22802269
} else {
22812270
other.clone()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
program foo.aleo;
2+
3+
function main:
4+
input r0 as identifier.private;
5+
input r1 as dynamic.record;
6+
input r2 as address.private;
7+
call.dynamic r0 'aleo' 'transfer_private' with r1 r2 (as dynamic.record address.private) into r3 (as dynamic.record);
8+
output r3 as dynamic.record;
Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
Error [ETYC0372117]: Expected type `dyn record` but type `test.aleo::Token` was found.
2-
--> compiler-test:17:55
1+
Error [ETYC0372187]: Dynamic call argument has record type `vault.aleo::Token`, but dynamic calls require `dyn record`.
2+
--> compiler-test:7:56
33
|
4-
17 | return test.aleo::TokenOps@(target)::transfer(t);
5-
| ^
6-
Error [ETYC0372187]: Dynamic call argument has record type `test.aleo::Token`, but dynamic calls require `dyn record`.
7-
--> compiler-test:17:55
8-
|
9-
17 | return test.aleo::TokenOps@(target)::transfer(t);
10-
| ^
4+
7 | return vault.aleo::TokenOps@(target)::transfer(t);
5+
| ^
116
|
127
= Cast your record value with `my_arg as dyn record`.
8+
9+
10+
---
11+
Note: Treating dependencies as Aleo produces different results:
12+
13+
Error [ETYC0372005]: Unknown interface `vault.aleo::TokenOps`
14+
--> compiler-test:7:16
15+
|
16+
7 | return vault.aleo::TokenOps@(target)::transfer(t);
17+
| ^^^^^^^^^^^^^^^^^^^^
Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
1-
program test.aleo;
1+
program router.aleo;
22

3-
record Token:
4-
owner as address.private;
5-
amount as u64.private;
6-
7-
function transfer:
8-
input r0 as Token.record;
9-
cast r0.owner r0.amount into r1 as Token.record;
10-
output r1 as Token.record;
11-
12-
function main:
3+
function dispatch:
134
input r0 as field.private;
14-
input r1 as Token.record;
15-
cast r1 into r2 as dynamic.record;
16-
call.dynamic r0 'aleo' 'transfer' with r2 (as dynamic.record) into r3 (as dynamic.record);
17-
output r3 as dynamic.record;
18-
19-
constructor:
20-
assert.eq edition 0u16;
5+
input r1 as dynamic.record;
6+
call.dynamic r0 'aleo' 'transfer' with r1 (as dynamic.record) into r2 (as dynamic.record);
7+
output r2 as dynamic.record;

tests/expectations/execution/dynamic_call_record_interface.out

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
program token_impl.aleo;
1+
program vault.aleo;
22

33
record Token:
44
owner as address.private;
@@ -10,25 +10,24 @@ function double_balance:
1010
cast r0.owner r1 into r2 as Token.record;
1111
output r2 as Token.record;
1212

13-
function get_initial:
13+
function mint:
1414
input r0 as u64.private;
1515
cast self.signer r0 into r1 as Token.record;
1616
output r1 as Token.record;
1717

1818
constructor:
1919
assert.eq edition 0u16;
2020
// --- Next Program --- //
21-
import token_impl.aleo;
22-
program caller.aleo;
21+
import vault.aleo;
22+
program router.aleo;
2323

24-
function main:
25-
input r0 as address.private;
26-
input r1 as u64.private;
27-
call token_impl.aleo/get_initial r1 into r2;
28-
cast r2 into r3 as dynamic.record;
29-
call.dynamic 'token_impl' 'aleo' 'double_balance' with r3 (as dynamic.record) into r4 (as dynamic.record);
30-
get.record.dynamic r4.balance into r5 as u64;
31-
output r5 as u64.private;
24+
function dispatch:
25+
input r0 as u64.private;
26+
call vault.aleo/mint r0 into r1;
27+
cast r1 into r2 as dynamic.record;
28+
call.dynamic 'vault' 'aleo' 'double_balance' with r2 (as dynamic.record) into r3 (as dynamic.record);
29+
get.record.dynamic r3.balance into r4 as u64;
30+
output r4 as u64.private;
3231

3332
constructor:
3433
assert.eq edition 0u16;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// OK: Dynamic call passing `dyn record` where the interface function expects a record type.
2+
// The calling program does not implement the interface, so the record type is not in the
3+
// program's symbol table. A `dyn record` argument should be accepted for any record-typed
4+
// interface parameter.
5+
interface ARC20 {
6+
record Token;
7+
fn transfer_private(token: Token, to: address) -> Token;
8+
}
9+
10+
program foo.aleo {
11+
fn main(target: identifier, token: dyn record, to: address) -> dyn record {
12+
return ARC20@(target)::transfer_private(token, to);
13+
}
14+
}

0 commit comments

Comments
 (0)