Skip to main content

leo_abi/
interfaces.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! Interface ABI generation.
18//!
19//! Generates per-interface JSON ABIs from the Leo AST. Supports both programs
20//! (emitting ABIs for locally defined + directly implemented interfaces) and
21//! libraries (emitting ABIs for locally defined interfaces).
22
23use crate::{
24    collect_from_abi_storage_type,
25    collect_from_function_input,
26    collect_from_function_output,
27    collect_from_plaintext,
28    collect_transitive_from_records,
29    collect_transitive_from_structs,
30    convert_mode,
31    convert_plaintext,
32    convert_record,
33    convert_record_field,
34    convert_storage_type,
35    convert_struct,
36    interface_ref_from_type,
37};
38
39use leo_abi_types as abi;
40use leo_ast::{self as ast};
41use leo_span::Symbol;
42
43use indexmap::IndexMap;
44use itertools::{Either, Itertools as _};
45use std::collections::HashSet;
46
47// ------------------------------------------------------------------------- //
48// Public types
49// ------------------------------------------------------------------------- //
50
51/// A generated interface ABI together with its ownership information.
52pub struct CompiledInterface {
53    pub owner: InterfaceOwner,
54    pub abi: abi::Interface,
55}
56
57/// Where the interface is defined relative to the unit being built.
58pub enum InterfaceOwner {
59    /// Defined in the primary program/library being built.
60    /// Written to `build/interfaces/<module_path>/<Name>.json`.
61    Local,
62    /// Defined in an imported program or library.
63    /// Written to `build/interfaces/<owner_program>/<module_path>/<Name>.json`.
64    External { owner_program: String },
65}
66
67// ------------------------------------------------------------------------- //
68// Entry points
69// ------------------------------------------------------------------------- //
70
71/// Emits interface ABIs relevant to the given program:
72///
73/// - All interfaces locally defined in the program's scope and modules.
74/// - All interfaces directly implemented (via `scope.parents`), looked up in stubs.
75/// - All external parent interfaces referenced transitively by any of the above.
76pub fn generate_program_interfaces(ast: &ast::Program) -> Vec<CompiledInterface> {
77    let scope = ast.program_scopes.values().next().unwrap();
78    let program_str = scope.program_id.to_string();
79    let program_sym = scope.program_id.as_symbol();
80    let cs = CompositeSource::Program { scope, modules: &ast.modules, stubs: &ast.stubs };
81
82    let mut result = Vec::new();
83    let mut seen: HashSet<(Option<String>, Vec<String>)> = HashSet::new();
84
85    // 1. Locally defined interfaces (top-level).
86    for (_, iface) in &scope.interfaces {
87        let abi = build_interface(iface, program_sym, &[], &cs);
88        let key = (None, abi.path.clone());
89        if seen.insert(key) {
90            result.push(CompiledInterface { owner: InterfaceOwner::Local, abi });
91        }
92    }
93
94    // 2. Locally defined interfaces (in modules).
95    for (module_path, module) in &ast.modules {
96        for (_, iface) in &module.interfaces {
97            let abi = build_interface(iface, program_sym, module_path, &cs);
98            let key = (None, abi.path.clone());
99            if seen.insert(key) {
100                result.push(CompiledInterface { owner: InterfaceOwner::Local, abi });
101            }
102        }
103    }
104
105    // 3. External interfaces: directly implemented + transitive parents.
106    //
107    // Use a worklist so that every external interface referenced in a `parents`
108    // field (whether on the program itself or on a local/external interface) is
109    // emitted together with its own transitive parents.
110    let mut worklist: Vec<(Symbol, Vec<Symbol>)> = Vec::new();
111
112    // Seed from program-level parent implementations (scope.parents).
113    for (_, ty) in &scope.parents {
114        if let ast::Type::Composite(ct) = ty
115            && let Some(loc) = ct.path.try_global_location()
116        {
117            worklist.push((loc.program, loc.path.clone()));
118        }
119    }
120
121    // Seed from parents of locally-defined interfaces.
122    let local_ifaces = scope
123        .interfaces
124        .iter()
125        .map(|(_, i)| i)
126        .chain(ast.modules.values().flat_map(|m| m.interfaces.iter().map(|(_, i)| i)));
127    for iface in local_ifaces {
128        for (_, parent_ty) in &iface.parents {
129            if let ast::Type::Composite(ct) = parent_ty
130                && let Some(loc) = ct.path.try_global_location()
131            {
132                worklist.push((loc.program, loc.path.clone()));
133            }
134        }
135    }
136
137    while let Some((ext_program, iface_path)) = worklist.pop() {
138        let owner_str = ext_program.to_string();
139
140        // Skip if local (already covered in steps 1 and 2).
141        if owner_str == program_str {
142            continue;
143        }
144
145        let Some(stub) = ast.stubs.get(&ext_program) else { continue };
146        let Some(iface) = find_interface_in_stub(stub, &iface_path) else { continue };
147
148        let ext_cs = composite_source_for_stub(stub);
149        let module_path: Vec<Symbol> = iface_path[..iface_path.len().saturating_sub(1)].to_vec();
150        let abi = build_interface(iface, ext_program, &module_path, &ext_cs);
151        let key = (Some(owner_str.clone()), abi.path.clone());
152        if seen.insert(key) {
153            result.push(CompiledInterface { owner: InterfaceOwner::External { owner_program: owner_str }, abi });
154            // Enqueue this interface's parents for processing.
155            for (_, parent_ty) in &iface.parents {
156                let ast::Type::Composite(ct) = parent_ty else { continue };
157                let Some(parent_loc) = ct.path.try_global_location() else { continue };
158                worklist.push((parent_loc.program, parent_loc.path.clone()));
159            }
160        }
161    }
162
163    result
164}
165
166/// Emits interface ABIs for every interface locally defined in the library
167/// (top-level + modules).
168pub fn generate_library_interfaces(library: &ast::Library) -> Vec<CompiledInterface> {
169    let cs = CompositeSource::Library { library, stubs: &library.stubs };
170    let mut result = Vec::new();
171
172    for (_, iface) in &library.interfaces {
173        let abi = build_interface(iface, library.name, &[], &cs);
174        result.push(CompiledInterface { owner: InterfaceOwner::Local, abi });
175    }
176
177    for (module_path, module) in &library.modules {
178        for (_, iface) in &module.interfaces {
179            let abi = build_interface(iface, library.name, module_path, &cs);
180            result.push(CompiledInterface { owner: InterfaceOwner::Local, abi });
181        }
182    }
183
184    result
185}
186
187// ------------------------------------------------------------------------- //
188// CompositeSource - unified record/struct lookup
189// ------------------------------------------------------------------------- //
190
191/// Abstracts where to look up composite definitions (structs and records).
192enum CompositeSource<'a> {
193    Program {
194        scope: &'a ast::ProgramScope,
195        modules: &'a IndexMap<Vec<Symbol>, ast::Module>,
196        stubs: &'a IndexMap<Symbol, ast::Stub>,
197    },
198    Library {
199        library: &'a ast::Library,
200        stubs: &'a IndexMap<Symbol, ast::Stub>,
201    },
202}
203
204impl<'a> CompositeSource<'a> {
205    /// Checks if a composite type refers to a record.
206    fn is_record(&self, comp_ty: &ast::CompositeType) -> bool {
207        let name = comp_ty.path.identifier().name;
208
209        // Check local composites.
210        match self {
211            CompositeSource::Program { scope, modules, .. } => {
212                if let Some((_, c)) = scope.composites.iter().find(|(sym, _)| *sym == name) {
213                    return c.is_record;
214                }
215                for module in modules.values() {
216                    if let Some((_, c)) = module.composites.iter().find(|(sym, _)| *sym == name) {
217                        return c.is_record;
218                    }
219                }
220            }
221            CompositeSource::Library { library, .. } => {
222                if let Some((_, c)) = library.structs.iter().find(|(sym, _)| *sym == name) {
223                    return c.is_record;
224                }
225                for module in library.modules.values() {
226                    if let Some((_, c)) = module.composites.iter().find(|(sym, _)| *sym == name) {
227                        return c.is_record;
228                    }
229                }
230            }
231        }
232
233        // Check stubs.
234        let stubs = match self {
235            CompositeSource::Program { stubs, .. } | CompositeSource::Library { stubs, .. } => stubs,
236        };
237        if let Some(program) = comp_ty.path.program()
238            && let Some(stub) = stubs.get(&program)
239            && let Some(is_rec) = find_is_record_in_stub(stub, name)
240        {
241            return is_rec;
242        }
243
244        false
245    }
246
247    /// Collects all struct (non-record) composites as ABI structs.
248    fn all_structs(&self) -> Vec<abi::Struct> {
249        let mut out = Vec::new();
250        match self {
251            CompositeSource::Program { scope, modules, .. } => {
252                out.extend(scope.composites.iter().filter(|(_, c)| !c.is_record).map(|(_, c)| convert_struct(c, &[])));
253                for (mp, module) in *modules {
254                    out.extend(
255                        module.composites.iter().filter(|(_, c)| !c.is_record).map(|(_, c)| convert_struct(c, mp)),
256                    );
257                }
258            }
259            CompositeSource::Library { library, .. } => {
260                out.extend(library.structs.iter().filter(|(_, c)| !c.is_record).map(|(_, c)| convert_struct(c, &[])));
261                for (mp, module) in &library.modules {
262                    out.extend(
263                        module.composites.iter().filter(|(_, c)| !c.is_record).map(|(_, c)| convert_struct(c, mp)),
264                    );
265                }
266            }
267        }
268        out
269    }
270
271    /// Collects all record composites as ABI records.
272    fn all_records(&self) -> Vec<abi::Record> {
273        let mut out = Vec::new();
274        match self {
275            CompositeSource::Program { scope, modules, .. } => {
276                out.extend(scope.composites.iter().filter(|(_, c)| c.is_record).map(|(_, c)| convert_record(c, &[])));
277                for (mp, module) in *modules {
278                    out.extend(
279                        module.composites.iter().filter(|(_, c)| c.is_record).map(|(_, c)| convert_record(c, mp)),
280                    );
281                }
282            }
283            CompositeSource::Library { library, .. } => {
284                out.extend(library.structs.iter().filter(|(_, c)| c.is_record).map(|(_, c)| convert_record(c, &[])));
285                for (mp, module) in &library.modules {
286                    out.extend(
287                        module.composites.iter().filter(|(_, c)| c.is_record).map(|(_, c)| convert_record(c, mp)),
288                    );
289                }
290            }
291        }
292        out
293    }
294}
295
296/// Checks if a name is a record in a stub.
297fn find_is_record_in_stub(stub: &ast::Stub, name: Symbol) -> Option<bool> {
298    match stub {
299        ast::Stub::FromAleo { program, .. } => {
300            program.composites.iter().find(|(sym, _)| *sym == name).map(|(_, c)| c.is_record)
301        }
302        ast::Stub::FromLeo { program, .. } => program
303            .program_scopes
304            .values()
305            .flat_map(|scope| scope.composites.iter())
306            .find(|(sym, _)| *sym == name)
307            .map(|(_, c)| c.is_record),
308        ast::Stub::FromLibrary { library, .. } => {
309            library.structs.iter().find(|(sym, _)| *sym == name).map(|(_, c)| c.is_record)
310        }
311    }
312}
313
314/// Builds a `CompositeSource` for a stub (for looking up composites in an external dependency).
315fn composite_source_for_stub(stub: &ast::Stub) -> CompositeSource<'_> {
316    match stub {
317        ast::Stub::FromLeo { program, .. } => {
318            let scope = program.program_scopes.values().next().unwrap();
319            CompositeSource::Program { scope, modules: &program.modules, stubs: &program.stubs }
320        }
321        ast::Stub::FromLibrary { library, .. } => CompositeSource::Library { library, stubs: &library.stubs },
322        ast::Stub::FromAleo { .. } => {
323            // Aleo stubs can't define interfaces, so this shouldn't be reached.
324            // Use an empty library as a placeholder.
325            unreachable!("Aleo stubs do not contain interfaces")
326        }
327    }
328}
329
330// ------------------------------------------------------------------------- //
331// Interface lookup in stubs
332// ------------------------------------------------------------------------- //
333
334/// Finds an interface definition in a stub by its path segments.
335fn find_interface_in_stub<'a>(stub: &'a ast::Stub, path: &[Symbol]) -> Option<&'a ast::Interface> {
336    let (&iface_name, module_path) = path.split_last()?;
337    match stub {
338        ast::Stub::FromLeo { program, .. } => {
339            if module_path.is_empty() {
340                program
341                    .program_scopes
342                    .values()
343                    .flat_map(|scope| scope.interfaces.iter())
344                    .find(|(name, _)| *name == iface_name)
345                    .map(|(_, iface)| iface)
346            } else {
347                program.modules.iter().find(|(mp, _)| mp.as_slice() == module_path).and_then(|(_, module)| {
348                    module.interfaces.iter().find(|(name, _)| *name == iface_name).map(|(_, iface)| iface)
349                })
350            }
351        }
352        ast::Stub::FromLibrary { library, .. } => {
353            if module_path.is_empty() {
354                library.interfaces.iter().find(|(name, _)| *name == iface_name).map(|(_, iface)| iface)
355            } else {
356                library.modules.iter().find(|(mp, _)| mp.as_slice() == module_path).and_then(|(_, module)| {
357                    module.interfaces.iter().find(|(name, _)| *name == iface_name).map(|(_, iface)| iface)
358                })
359            }
360        }
361        ast::Stub::FromAleo { .. } => None,
362    }
363}
364
365// ------------------------------------------------------------------------- //
366// Building a single interface ABI
367// ------------------------------------------------------------------------- //
368
369/// Converts an AST interface to an ABI interface, collecting transitively
370/// referenced struct definitions from the composite source.
371fn build_interface(
372    iface: &ast::Interface,
373    owning_program: Symbol,
374    module_path: &[Symbol],
375    cs: &CompositeSource<'_>,
376) -> abi::Interface {
377    let name = iface.identifier.name.to_string();
378    let program = owning_program.to_string();
379
380    let mut path: Vec<String> = module_path.iter().map(|s| s.to_string()).collect();
381    path.push(name.clone());
382
383    let parents: Vec<abi::InterfaceRef> =
384        iface.parents.iter().filter_map(|(_, ty)| interface_ref_from_type(ty, &program)).collect();
385
386    // Split prototypes by variant so view fns appear in their own ABI bucket,
387    // parallel to how `Program.functions` and `Program.views` are split.
388    let (functions, views): (Vec<abi::Function>, Vec<abi::Function>) =
389        iface.functions.iter().partition_map(|(_, proto)| {
390            let converted = convert_function_prototype(proto, iface, cs);
391            if proto.variant.is_view() { Either::Right(converted) } else { Either::Left(converted) }
392        });
393
394    let records: Vec<abi::Record> = iface.records.iter().map(|(_, proto)| convert_record_prototype(proto)).collect();
395
396    let mappings: Vec<abi::Mapping> = iface.mappings.iter().map(convert_mapping_prototype).collect();
397
398    let storage_variables: Vec<abi::StorageVariable> =
399        iface.storages.iter().map(convert_storage_variable_prototype).collect();
400
401    // Collect transitively referenced structs from the composite source.
402    // Pass both `functions` and `views` so that types referenced only by view
403    // fn signatures are still pulled into the interface's struct set.
404    let structs = collect_interface_structs(
405        &program,
406        functions.iter().chain(views.iter()),
407        &records,
408        &mappings,
409        &storage_variables,
410        cs,
411    );
412
413    abi::Interface { name, program, path, parents, functions, views, records, mappings, storage_variables, structs }
414}
415
416// ------------------------------------------------------------------------- //
417// Prototype -> ABI converters
418// ------------------------------------------------------------------------- //
419
420fn convert_function_prototype(
421    proto: &ast::FunctionPrototype,
422    iface: &ast::Interface,
423    cs: &CompositeSource<'_>,
424) -> abi::Function {
425    abi::Function {
426        name: proto.identifier.name.to_string(),
427        is_final: proto.output.iter().any(|o| matches!(o.type_, ast::Type::Future(_))),
428        const_parameters: proto.const_parameters.iter().map(convert_const_parameter).collect(),
429        inputs: proto.input.iter().map(|i| convert_input(i, iface, cs)).collect(),
430        outputs: proto.output.iter().map(|o| convert_output(o, iface, cs)).collect(),
431    }
432}
433
434fn convert_record_prototype(proto: &ast::RecordPrototype) -> abi::Record {
435    abi::Record {
436        path: vec![proto.identifier.name.to_string()],
437        fields: proto.members.iter().map(convert_record_field).collect(),
438    }
439}
440
441fn convert_mapping_prototype(proto: &ast::MappingPrototype) -> abi::Mapping {
442    abi::Mapping {
443        name: proto.identifier.name.to_string(),
444        key: convert_plaintext(&proto.key_type),
445        value: convert_plaintext(&proto.value_type),
446    }
447}
448
449fn convert_storage_variable_prototype(proto: &ast::StorageVariablePrototype) -> abi::StorageVariable {
450    abi::StorageVariable { name: proto.identifier.name.to_string(), ty: convert_storage_type(&proto.type_) }
451}
452
453fn convert_const_parameter(cp: &ast::ConstParameter) -> abi::ConstParameter {
454    abi::ConstParameter { name: cp.identifier.name.to_string(), ty: convert_plaintext(&cp.type_) }
455}
456
457fn convert_input(input: &ast::Input, iface: &ast::Interface, cs: &CompositeSource<'_>) -> abi::Input {
458    abi::Input {
459        name: input.identifier.name.to_string(),
460        ty: convert_function_input(&input.type_, iface, cs),
461        mode: convert_mode(input.mode),
462    }
463}
464
465fn convert_output(output: &ast::Output, iface: &ast::Interface, cs: &CompositeSource<'_>) -> abi::Output {
466    abi::Output { ty: convert_function_output(&output.type_, iface, cs), mode: convert_mode(output.mode) }
467}
468
469/// Checks if a composite type is a record in the context of an interface.
470///
471/// Checks the interface's own record prototypes first, then falls back to the
472/// composite source for records from the surrounding scope.
473fn is_record_for_interface(comp_ty: &ast::CompositeType, iface: &ast::Interface, cs: &CompositeSource<'_>) -> bool {
474    // Check the interface's own records.
475    let name = comp_ty.path.identifier().name;
476    if iface.records.iter().any(|(n, _)| *n == name) {
477        return true;
478    }
479    cs.is_record(comp_ty)
480}
481
482fn convert_function_input(ty: &ast::Type, iface: &ast::Interface, cs: &CompositeSource<'_>) -> abi::FunctionInput {
483    if let ast::Type::DynRecord = ty {
484        return abi::FunctionInput::DynamicRecord;
485    }
486    if let ast::Type::Composite(comp_ty) = ty
487        && is_record_for_interface(comp_ty, iface, cs)
488    {
489        return abi::FunctionInput::Record(abi::RecordRef {
490            path: comp_ty.path.segments_iter().map(|s| s.to_string()).collect(),
491            program: comp_ty.path.program().map(|s| s.to_string()),
492        });
493    }
494    abi::FunctionInput::Plaintext(convert_plaintext(ty))
495}
496
497fn convert_function_output(ty: &ast::Type, iface: &ast::Interface, cs: &CompositeSource<'_>) -> abi::FunctionOutput {
498    match ty {
499        ast::Type::Future(_) => abi::FunctionOutput::Final,
500        ast::Type::DynRecord => abi::FunctionOutput::DynamicRecord,
501        ast::Type::Composite(comp_ty) if is_record_for_interface(comp_ty, iface, cs) => {
502            abi::FunctionOutput::Record(abi::RecordRef {
503                path: comp_ty.path.segments_iter().map(|s| s.to_string()).collect(),
504                program: comp_ty.path.program().map(|s| s.to_string()),
505            })
506        }
507        _ => abi::FunctionOutput::Plaintext(convert_plaintext(ty)),
508    }
509}
510
511// ------------------------------------------------------------------------- //
512// Transitive struct collection
513// ------------------------------------------------------------------------- //
514
515/// Collects struct definitions transitively referenced by an interface's items.
516///
517/// Only includes structs defined in the same program as the interface; external
518/// struct references remain as `StructRef` with a program field.
519fn collect_interface_structs<'a>(
520    program_name: &str,
521    functions: impl IntoIterator<Item = &'a abi::Function>,
522    records: &[abi::Record],
523    mappings: &[abi::Mapping],
524    storage_variables: &[abi::StorageVariable],
525    cs: &CompositeSource<'_>,
526) -> Vec<abi::Struct> {
527    let mut used_types: HashSet<abi::Path> = HashSet::new();
528
529    // Seed from functions (and views — caller chains them in).
530    for function in functions {
531        for cp in &function.const_parameters {
532            collect_from_plaintext(&cp.ty, program_name, &mut used_types);
533        }
534        for input in &function.inputs {
535            collect_from_function_input(&input.ty, program_name, &mut used_types);
536        }
537        for output in &function.outputs {
538            collect_from_function_output(&output.ty, program_name, &mut used_types);
539        }
540    }
541
542    // Seed from records.
543    for record in records {
544        for field in &record.fields {
545            collect_from_plaintext(&field.ty, program_name, &mut used_types);
546        }
547    }
548
549    // Seed from mappings.
550    for mapping in mappings {
551        collect_from_plaintext(&mapping.key, program_name, &mut used_types);
552        collect_from_plaintext(&mapping.value, program_name, &mut used_types);
553    }
554
555    // Seed from storage variables.
556    for sv in storage_variables {
557        collect_from_abi_storage_type(&sv.ty, program_name, &mut used_types);
558    }
559
560    // Collect all composites from the source, then close transitively.
561    let all_structs = cs.all_structs();
562    let all_records = cs.all_records();
563    collect_transitive_from_structs(&all_structs, program_name, &mut used_types);
564    collect_transitive_from_records(&all_records, program_name, &mut used_types);
565
566    all_structs.into_iter().filter(|s| used_types.contains(&s.path)).collect()
567}