Skip to content
This repository was archived by the owner on May 4, 2026. It is now read-only.

Commit 3275b11

Browse files
mohammadfawazclaude
andcommitted
Document cross-package submodule access and library frontend passes
- Layout: explain program.aleo::submodule::item for structs, consts, helper fns, and interface references in program headers; helpers reached through this path are inlined into the caller's bytecode. - Interfaces: document Interface@(target)::mapping.{get,contains, get_or_use}(...) and Interface@(target)::storage[.get(i)|.len()] dynamic-read forms, valid only in final context. - Libraries: note that `leo build` on a library package now runs the full frontend pipeline, so errors surface at the library itself. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 40673ba commit 3275b11

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

documentation/language/01_layout.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,85 @@ fn increment(x: field) -> field {
165165
}
166166
```
167167

168+
### Accessing Submodules of Imported Programs
169+
170+
When an imported program organizes its source across submodules, you can reach any `struct`, `const`, or helper `fn` from those submodules using an extended locator path:
171+
172+
```
173+
program.aleo::submodule::item
174+
```
175+
176+
For example, suppose `provider.aleo` has a submodule `colors` that defines a `Color` struct, a `MAX_CH` constant, and a `blend` helper:
177+
178+
```leo title="provider/src/colors.leo"
179+
const MAX_CH: u32 = 255u32;
180+
181+
struct Color {
182+
r: u32,
183+
g: u32,
184+
b: u32,
185+
}
186+
187+
fn blend(a: Color, b: Color) -> Color {
188+
return Color {
189+
r: (a.r + b.r) / 2u32,
190+
g: (a.g + b.g) / 2u32,
191+
b: (a.b + b.b) / 2u32,
192+
};
193+
}
194+
```
195+
196+
```leo title="provider/src/main.leo"
197+
program provider.aleo {
198+
fn sum_channels(c: colors::Color) -> u32 {
199+
return c.r + c.g + c.b;
200+
}
201+
202+
fn mix_colors(a: colors::Color, b: colors::Color) -> colors::Color {
203+
return colors::blend(a, b);
204+
}
205+
206+
@noupgrade
207+
constructor() {}
208+
}
209+
```
210+
211+
A program that imports `provider.aleo` can reach the submodule struct, constant, and helper through the extended path, and can also call `provider.aleo`'s top-level entry functions:
212+
213+
```leo title="consumer/src/main.leo"
214+
import provider.aleo;
215+
216+
program consumer.aleo {
217+
// Struct and const from the submodule.
218+
fn make_white() -> provider.aleo::colors::Color {
219+
return provider.aleo::colors::Color {
220+
r: provider.aleo::colors::MAX_CH,
221+
g: provider.aleo::colors::MAX_CH,
222+
b: provider.aleo::colors::MAX_CH,
223+
};
224+
}
225+
226+
// Top-level entry function from the provider.
227+
fn mix(a: provider.aleo::colors::Color, b: provider.aleo::colors::Color) -> provider.aleo::colors::Color {
228+
return provider.aleo::mix_colors(a, b);
229+
}
230+
231+
// Submodule helper called directly — inlined into consumer's bytecode.
232+
fn average(a: provider.aleo::colors::Color, b: provider.aleo::colors::Color) -> provider.aleo::colors::Color {
233+
return provider.aleo::colors::blend(a, b);
234+
}
235+
236+
@noupgrade
237+
constructor() {}
238+
}
239+
```
240+
241+
Helper `fn`s reached through `program.aleo::submodule::name(...)` are inlined directly into the caller's bytecode; they are not separate on-chain calls and do not appear in the provider's ABI. Only top-level entry functions declared inside `program provider.aleo { ... }` remain part of its on-chain interface.
242+
243+
Submodule paths can be arbitrarily deep — `program.aleo::a::b::item` is valid if `program.aleo` has a nested submodule `a/b.leo`. The same extended path syntax applies to library submodules (see [Leo Libraries](./06_libraries.md#submodules)).
244+
245+
`interface` definitions may also be referenced through the same path syntax — both library submodules (`program my_app.aleo: my_lib::interfaces::Adder { ... }`) and imported program submodules (`program my_app.aleo: other_prog.aleo::interfaces::Adder { ... }`) work in a program header.
246+
168247
<!--
169248
170249
## The Tests

documentation/language/06_libraries.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,26 @@ When a library dependency and a local submodule share the same name, paths begin
181181
Explicit disambiguation using absolute paths (similar to Rust's `crate::foo::…` for local modules) is planned for a future release.
182182
:::
183183

184+
## Building a Library
185+
186+
Running `leo build` inside a library package parses the library sources and runs the full frontend pipeline — name validation, global-item collection, path resolution, interface checks, type checking, and static analysis — on the library itself. Type errors, unknown identifiers, interface-cycle errors, and the like are reported at the library package, instead of surfacing only when a downstream program consumes it.
187+
188+
```bash
189+
cd math_utils
190+
leo build
191+
```
192+
193+
```
194+
Leo 🔨 Building library 'math_utils'
195+
Leo ✅ Validated 'math_utils'.
196+
```
197+
198+
No bytecode is produced — libraries are inlined at the point of use and have no on-chain footprint — but any frontend errors are reported with spans pointing into the library's own source files.
199+
200+
:::note
201+
When a program that depends on a library is built, library sources are compiled holistically with the program — any errors in the library still surface, but as part of the consuming program's build. Running `leo build` inside the library package itself validates it in isolation, so problems are caught at the source before any consumer tries to use it.
202+
:::
203+
184204
## Testing
185205

186206
`leo test` works on library packages directly — no wrapper program is required. Place test files in the `tests/` directory and call library functions using the `library_name::item` path syntax.

documentation/language/programs_in_practice/interfaces.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,93 @@ return TokenStandard@(target, network)::transfer_public(to, amount);
188188
The only valid network identifier currently is `aleo`.
189189
:::
190190

191+
### Dynamic Mapping Reads
192+
193+
An interface that declares a `mapping` can also be used to read that mapping on a runtime-determined program. The syntax mirrors dynamic calls, but with a mapping name in place of a method name and a trailing read operation:
194+
195+
```
196+
Interface@(target[, network])::mapping.get(key)
197+
Interface@(target[, network])::mapping.contains(key)
198+
Interface@(target[, network])::mapping.get_or_use(key, default)
199+
```
200+
201+
- `.get(key)` returns the mapped value; the transition fails at runtime if `key` is not present.
202+
- `.contains(key)` returns a `bool`.
203+
- `.get_or_use(key, default)` returns the mapped value, or `default` if `key` is absent.
204+
205+
These reads are only valid inside a `final fn` or a `final {}` block — they lower to the AVM `get.dynamic`, `contains.dynamic`, and `get.or_use.dynamic` instructions. Dynamic *writes* are not supported.
206+
207+
```leo
208+
interface Bank {
209+
mapping balances: address => u64;
210+
}
211+
212+
import bank.aleo;
213+
214+
program checker.aleo {
215+
mapping snapshot: address => u64;
216+
217+
fn read_balance(target: field, user: address) -> Final {
218+
return final { do_read(target, user); };
219+
}
220+
221+
@noupgrade
222+
constructor() {}
223+
}
224+
225+
final fn do_read(target: field, user: address) {
226+
let present: bool = bank.aleo::Bank@(target)::balances.contains(user);
227+
let val: u64 = bank.aleo::Bank@(target)::balances.get_or_use(user, 0u64);
228+
Mapping::set(snapshot, user, present ? val : 0u64);
229+
}
230+
```
231+
232+
When the interface is in scope without a program qualifier (for example, self-reading from the program that declares it), the leading `program.aleo::` segment can be omitted: `Bank@(target)::balances.get(key)`.
233+
234+
### Dynamic Storage Reads
235+
236+
Interfaces that declare [`storage`](../02_structure.md#storage) variables support dynamic reads with the same pattern. Storage reads always return an `Option<T>`:
237+
238+
```
239+
Interface@(target[, network])::singleton // Option<T>
240+
Interface@(target[, network])::vector.get(index) // Option<T>
241+
Interface@(target[, network])::vector.len() // u32
242+
```
243+
244+
Singleton storage is read by naming the variable directly (no trailing `.op(...)`). Vector storage supports `.get(index)` (out-of-bounds reads return `none`) and `.len()` (no arguments). Storage writes through the dynamic interface are not supported — use a dynamic call to an entry function that performs the write.
245+
246+
```leo
247+
interface Logger {
248+
storage counter: u64;
249+
storage entries: [u64];
250+
}
251+
252+
import logger.aleo;
253+
254+
program reader.aleo {
255+
mapping latest: u32 => u64;
256+
257+
fn snapshot(target: field, i: u32) -> Final {
258+
return final { do_snapshot(target, i); };
259+
}
260+
261+
@noupgrade
262+
constructor() {}
263+
}
264+
265+
final fn do_snapshot(target: field, i: u32) {
266+
let n: u32 = logger.aleo::Logger@(target)::entries.len();
267+
let entry: u64? = logger.aleo::Logger@(target)::entries.get(i);
268+
let current: u64? = logger.aleo::Logger@(target)::counter;
269+
let stored: u64 = i < n ? entry.unwrap() : current.unwrap_or(0u64);
270+
Mapping::set(latest, i, stored);
271+
}
272+
```
273+
274+
:::note
275+
Dynamic mapping reads are a type-checked alternative to the [`_dynamic_get`, `_dynamic_contains`, and `_dynamic_get_or_use`](./intrinsics.md) intrinsics. The interface form checks that the named mapping exists on the interface and that keys, values, and defaults have matching types; the intrinsics accept arbitrary runtime identifiers and leave that responsibility to the caller. Prefer the interface form whenever an interface is available.
276+
:::
277+
191278
## Dynamic Records
192279

193280
A `dyn record` is a record whose field structure is not known at compile time. It retains all the ownership and privacy properties of a regular record:

0 commit comments

Comments
 (0)