Skip to content
Open
136 changes: 60 additions & 76 deletions crates/abi/src/interfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::{
convert_record_field,
convert_storage_type,
convert_struct,
find_composite_at_location,
interface_ref_from_type,
resolve_io_mode,
};
Expand Down Expand Up @@ -145,7 +146,7 @@ pub fn generate_program_interfaces(ast: &ast::Program) -> Vec<CompiledInterface>
let Some(stub) = ast.stubs.get(&ext_program) else { continue };
let Some(iface) = find_interface_in_stub(stub, &iface_path) else { continue };

let ext_cs = composite_source_for_stub(stub);
let ext_cs = composite_source_for_stub(stub, &ast.stubs);
let module_path: Vec<Symbol> = iface_path[..iface_path.len().saturating_sub(1)].to_vec();
let abi = build_interface(iface, ext_program, &module_path, &ext_cs);
let key = (Some(owner_str.clone()), abi.path.clone());
Expand Down Expand Up @@ -204,44 +205,16 @@ enum CompositeSource<'a> {
impl<'a> CompositeSource<'a> {
/// Checks if a composite type refers to a record.
fn is_record(&self, comp_ty: &ast::CompositeType) -> bool {
let name = comp_ty.path.identifier().name;

// Check local composites.
match self {
CompositeSource::Program { scope, modules, .. } => {
if let Some((_, c)) = scope.composites.iter().find(|(sym, _)| *sym == name) {
return c.is_record;
}
for module in modules.values() {
if let Some((_, c)) = module.composites.iter().find(|(sym, _)| *sym == name) {
return c.is_record;
}
}
let Some(location) = comp_ty.path.try_global_location() else { return false };
let composite = match self {
CompositeSource::Program { scope, modules, stubs } => {
find_composite_at_location(location, scope.program_id.as_symbol(), &scope.composites, modules, stubs)
}
CompositeSource::Library { library, .. } => {
if let Some((_, c)) = library.structs.iter().find(|(sym, _)| *sym == name) {
return c.is_record;
}
for module in library.modules.values() {
if let Some((_, c)) = module.composites.iter().find(|(sym, _)| *sym == name) {
return c.is_record;
}
}
CompositeSource::Library { library, stubs } => {
find_composite_at_location(location, library.name, &library.structs, &library.modules, stubs)
}
}

// Check stubs.
let stubs = match self {
CompositeSource::Program { stubs, .. } | CompositeSource::Library { stubs, .. } => stubs,
};
if let Some(program) = comp_ty.path.program()
&& let Some(stub) = stubs.get(&program)
&& let Some(is_rec) = find_is_record_in_stub(stub, name)
{
return is_rec;
}

false
composite.is_some_and(|composite| composite.is_record)
}

/// Collects all struct (non-record) composites as ABI structs.
Expand Down Expand Up @@ -293,32 +266,20 @@ impl<'a> CompositeSource<'a> {
}
}

/// Checks if a name is a record in a stub.
fn find_is_record_in_stub(stub: &ast::Stub, name: Symbol) -> Option<bool> {
match stub {
ast::Stub::FromAleo { program, .. } => {
program.composites.iter().find(|(sym, _)| *sym == name).map(|(_, c)| c.is_record)
}
ast::Stub::FromLeo { program, .. } => program
.program_scopes
.values()
.flat_map(|scope| scope.composites.iter())
.find(|(sym, _)| *sym == name)
.map(|(_, c)| c.is_record),
ast::Stub::FromLibrary { library, .. } => {
library.structs.iter().find(|(sym, _)| *sym == name).map(|(_, c)| c.is_record)
}
}
}

/// Builds a `CompositeSource` for a stub (for looking up composites in an external dependency).
fn composite_source_for_stub(stub: &ast::Stub) -> CompositeSource<'_> {
///
/// Use the compilation unit's reachable stubs because a dependency's nested `Program.stubs`
/// map does not contain its sibling dependencies.
fn composite_source_for_stub<'a>(
stub: &'a ast::Stub,
reachable_stubs: &'a IndexMap<Symbol, ast::Stub>,
) -> CompositeSource<'a> {
match stub {
ast::Stub::FromLeo { program, .. } => {
let scope = program.program_scopes.values().next().unwrap();
CompositeSource::Program { scope, modules: &program.modules, stubs: &program.stubs }
CompositeSource::Program { scope, modules: &program.modules, stubs: reachable_stubs }
}
ast::Stub::FromLibrary { library, .. } => CompositeSource::Library { library, stubs: &library.stubs },
ast::Stub::FromLibrary { library, .. } => CompositeSource::Library { library, stubs: reachable_stubs },
ast::Stub::FromAleo { .. } => {
// Aleo stubs can't define interfaces, so this shouldn't be reached.
// Use an empty library as a placeholder.
Expand Down Expand Up @@ -382,12 +343,22 @@ fn build_interface(

let parents: Vec<abi::InterfaceRef> =
iface.parents.iter().filter_map(|(_, ty)| interface_ref_from_type(ty, &program)).collect();
let prototype_record_locations: HashSet<ast::Location> = iface
.records
.iter()
.map(|(record_name, _)| {
let mut path = Vec::with_capacity(module_path.len() + 1);
path.extend_from_slice(module_path);
path.push(*record_name);
ast::Location::new(owning_program, path)
})
.collect();

// Split prototypes by variant so view fns appear in their own ABI bucket,
// parallel to how `Program.functions` and `Program.views` are split.
let (functions, views): (Vec<abi::Function>, Vec<abi::Function>) =
iface.functions.iter().partition_map(|(_, proto)| {
let converted = convert_function_prototype(proto, iface, cs);
let converted = convert_function_prototype(proto, &prototype_record_locations, cs);
if proto.variant.is_view() { Either::Right(converted) } else { Either::Left(converted) }
});

Expand Down Expand Up @@ -419,13 +390,21 @@ fn build_interface(

fn convert_function_prototype(
proto: &ast::FunctionPrototype,
iface: &ast::Interface,
prototype_record_locations: &HashSet<ast::Location>,
cs: &CompositeSource<'_>,
) -> abi::Function {
abi::Function {
name: proto.identifier.name.to_string(),
inputs: proto.input.iter().map(|i| convert_input(i, iface, cs, proto.variant.is_view())).collect(),
outputs: proto.output.iter().map(|o| convert_output(o, iface, cs, proto.variant.is_view())).collect(),
inputs: proto
.input
.iter()
.map(|i| convert_input(i, prototype_record_locations, cs, proto.variant.is_view()))
.collect(),
outputs: proto
.output
.iter()
.map(|o| convert_output(o, prototype_record_locations, cs, proto.variant.is_view()))
.collect(),
}
}

Expand All @@ -450,46 +429,51 @@ fn convert_storage_variable_prototype(proto: &ast::StorageVariablePrototype) ->

fn convert_input(
input: &ast::Input,
iface: &ast::Interface,
prototype_record_locations: &HashSet<ast::Location>,
cs: &CompositeSource<'_>,
is_view: bool,
) -> abi::FunctionInput {
convert_function_input(input.type_.kind(), iface, cs, resolve_io_mode(input.mode, is_view))
convert_function_input(input.type_.kind(), prototype_record_locations, cs, resolve_io_mode(input.mode, is_view))
}

fn convert_output(
output: &ast::Output,
iface: &ast::Interface,
prototype_record_locations: &HashSet<ast::Location>,
cs: &CompositeSource<'_>,
is_view: bool,
) -> abi::FunctionOutput {
convert_function_output(output.type_.kind(), iface, cs, resolve_io_mode(output.mode, is_view))
convert_function_output(output.type_.kind(), prototype_record_locations, cs, resolve_io_mode(output.mode, is_view))
}

/// Checks if a composite type is a record in the context of an interface.
/// Checks if a composite type is a record in the context of an interface ABI.
///
/// Checks the interface's own record prototypes first, then falls back to the
/// composite source for records from the surrounding scope.
fn is_record_for_interface(comp_ty: &ast::CompositeType, iface: &ast::Interface, cs: &CompositeSource<'_>) -> bool {
// Check the interface's own records.
let name = comp_ty.path.identifier().name;
if iface.records.iter().any(|(n, _)| *n == name) {
/// Direct record prototype locations are checked first. Those locations use the interface's
/// owning program and containing module path; inherited parent prototypes are not added here.
/// Concrete composites are then resolved from the surrounding source by their complete location.
fn is_record_for_interface(
comp_ty: &ast::CompositeType,
prototype_record_locations: &HashSet<ast::Location>,
cs: &CompositeSource<'_>,
) -> bool {
if let Some(location) = comp_ty.path.try_global_location()
&& prototype_record_locations.contains(location)
{
return true;
}
cs.is_record(comp_ty)
}

fn convert_function_input(
ty: &ast::TypeKind,
iface: &ast::Interface,
prototype_record_locations: &HashSet<ast::Location>,
cs: &CompositeSource<'_>,
mode: abi::Mode,
) -> abi::FunctionInput {
if let ast::TypeKind::DynRecord = ty {
return abi::FunctionInput::DynamicRecord;
}
if let ast::TypeKind::Composite(comp_ty) = ty
&& is_record_for_interface(comp_ty, iface, cs)
&& is_record_for_interface(comp_ty, prototype_record_locations, cs)
{
return abi::FunctionInput::Record(abi::RecordRef {
path: comp_ty.path.segments_iter().map(|s| s.to_string()).collect(),
Expand All @@ -501,14 +485,14 @@ fn convert_function_input(

fn convert_function_output(
ty: &ast::TypeKind,
iface: &ast::Interface,
prototype_record_locations: &HashSet<ast::Location>,
cs: &CompositeSource<'_>,
mode: abi::Mode,
) -> abi::FunctionOutput {
match ty {
ast::TypeKind::Future(_) => abi::FunctionOutput::Final,
ast::TypeKind::DynRecord => abi::FunctionOutput::DynamicRecord,
ast::TypeKind::Composite(comp_ty) if is_record_for_interface(comp_ty, iface, cs) => {
ast::TypeKind::Composite(comp_ty) if is_record_for_interface(comp_ty, prototype_record_locations, cs) => {
abi::FunctionOutput::Record(abi::RecordRef {
path: comp_ty.path.segments_iter().map(|s| s.to_string()).collect(),
program: comp_ty.path.program().map(|s| s.to_string()),
Expand Down
99 changes: 65 additions & 34 deletions crates/abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ struct Ctx<'a> {
/// The returned ABI is pruned to only include types that appear in the public
/// interface (functions, mappings, storage variables).
pub fn generate(ast: &ast::Program) -> abi::Program {
generate_with_stubs(ast, &ast.stubs)
}

/// Generates the ABI for a Leo program using the compilation unit's reachable stubs.
///
/// A program imported from another compilation unit may carry only its nested stubs, while
/// ABI conversion needs the outer unit's reachable map to resolve sibling dependencies.
pub fn generate_with_stubs(ast: &ast::Program, stubs: &IndexMap<Symbol, ast::Stub>) -> abi::Program {
let scope = ast.program_scopes.values().next().unwrap();
let ctx = Ctx { scope, stubs: &ast.stubs, modules: &ast.modules };
let ctx = Ctx { scope, stubs, modules: &ast.modules };

let program = scope.program_id.to_string();

Expand Down Expand Up @@ -251,46 +259,69 @@ fn convert_function_output(ty: &ast::TypeKind, ctx: &Ctx, mode: abi::Mode) -> ab
}
}

/// Checks if a composite type refers to a record.
fn is_record(comp_ty: &ast::CompositeType, ctx: &Ctx) -> bool {
let name = comp_ty.path.identifier().name;

// Check if it's defined in the current program scope
if let Some((_, composite)) = ctx.scope.composites.iter().find(|(sym, _)| *sym == name) {
return composite.is_record;
}
/// Finds a composite by its exact module path and terminal name within one compilation unit.
/// A non-empty module path must match a module entry; the root path searches only top-level
/// composites.
fn find_composite_at_path<'a>(
path: &[Symbol],
composites: &'a [(Symbol, ast::Composite)],
modules: &'a IndexMap<Vec<Symbol>, ast::Module>,
) -> Option<&'a ast::Composite> {
let (&name, module_path) = path.split_last()?;
let composites = if module_path.is_empty() {
composites
} else {
let module = modules.iter().find(|(path, _)| path.as_slice() == module_path)?.1;
&module.composites
};

composites.iter().find(|(symbol, _)| *symbol == name).map(|(_, composite)| composite)
}

// Check if it's defined in a module
for module in ctx.modules.values() {
if let Some((_, composite)) = module.composites.iter().find(|(sym, _)| *sym == name) {
return composite.is_record;
}
/// Finds a composite by its canonical `(program, path)` identity.
///
/// Leo and library sources resolve the path inside the exact module selected by the location.
/// Aleo stubs expose root composites only, so a module-qualified Aleo location returns `None`.
fn find_composite_at_location<'a>(
location: &ast::Location,
current_program: Symbol,
composites: &'a [(Symbol, ast::Composite)],
modules: &'a IndexMap<Vec<Symbol>, ast::Module>,
stubs: &'a IndexMap<Symbol, ast::Stub>,
) -> Option<&'a ast::Composite> {
if location.program == current_program {
return find_composite_at_path(&location.path, composites, modules);
}

// Check if it's defined in an imported stub
if let Some(program) = comp_ty.path.program()
&& let Some(stub) = ctx.stubs.get(&program)
{
let found = match stub {
ast::Stub::FromAleo { program, .. } => {
program.composites.iter().find(|(sym, _)| *sym == name).map(|(_, c)| c.is_record)
match stubs.get(&location.program)? {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When ABI generation processes a FromLeo dependency, this map is the dependency nested Program.stubs, so sibling dependencies are absent and valid external records can be emitted as Plaintext::Struct. Please thread the outer reachable stub map into dependency and external-interface ABI conversion, and add a three-program regression test that asserts Record in both generated ABIs.

ast::Stub::FromAleo { program, .. } => {
let (&name, module_path) = location.path.split_last()?;
if !module_path.is_empty() {
return None;
}
ast::Stub::FromLeo { program, .. } => program
.program_scopes
.values()
.flat_map(|scope| scope.composites.iter())
.find(|(sym, _)| *sym == name)
.map(|(_, c)| c.is_record),

ast::Stub::FromLibrary { .. } => None,
};
if let Some(is_record) = found {
return is_record;
program.composites.iter().find(|(symbol, _)| *symbol == name).map(|(_, composite)| composite)
}
ast::Stub::FromLeo { program, .. } => {
let scope = program.program_scopes.get(&location.program)?;
find_composite_at_path(&location.path, &scope.composites, &program.modules)
}
ast::Stub::FromLibrary { library, .. } => {
find_composite_at_path(&location.path, &library.structs, &library.modules)
}
}
}

// Default to struct if not found (shouldn't happen after type checking)
false
/// Checks if a composite type refers to a record.
fn is_record(comp_ty: &ast::CompositeType, ctx: &Ctx) -> bool {
let Some(location) = comp_ty.path.try_global_location() else { return false };
find_composite_at_location(
location,
ctx.scope.program_id.as_symbol(),
&ctx.scope.composites,
ctx.modules,
ctx.stubs,
)
.is_some_and(|composite| composite.is_record)
}

fn extract_array_length(expr: &Expression) -> u32 {
Expand Down
Loading