Summary
An exported submodule struct that no item in its defining program references is pruned and never emitted, so an external consumer's reference to it dangles and snarkVM rejects the consumer's bytecode:
Error [ECLI0377047]: Leo generated invalid Aleo bytecode for program `use_s` — this is a compiler bug
Struct 'S__...' in 'lib.aleo' is not defined.
export controls visibility, not liveness: a struct reachable only from another program should still be emitted by the program that defines it.
Version
leo 4.3.2
Minimal reproducer
leo test on a package lib whose submodule m exports a struct used only by another program.
program.json
{ "program": "lib.aleo", "version": "0.0.0", "description": "", "license": "MIT", "dependencies": null }
src/main.leo
program lib.aleo {
@noupgrade
constructor() {}
fn noop() {}
}
src/m.leo
export struct S {
hi: u128,
lo: u128,
}
tests/use_s.leo
import lib.aleo;
program use_s.aleo {
@noupgrade
constructor() {}
fn run(a: lib.aleo::m::S) -> u128 {
return a.hi;
}
}
use_s.aleo references lib.aleo/S__... (in its input type), but lib.aleo emits no struct.
Notes on scope
- Adding any item in
lib that references S (e.g. export fn touch(x: S) -> u128 { return x.hi; } in module m) makes lib emit S and the error disappears. So the trigger is specifically an exported submodule struct with no reference inside its own program.
- This is why it does not affect typical library-style modules, whose functions use their structs.
Root cause (from investigation)
Two coupled points:
- Monomorphization drops it. Submodule structs are not nodes in the composite dependency graph (
iter_structs seeds the graph; post_order was empty for lib). A submodule struct is therefore only reconstructed when a reached function uses it via its signature; otherwise it falls through to the "current-program leftover = dead code" step in crates/passes/src/monomorphization/program.rs and is dropped.
- Code generation emits only graph-ordered structs.
visit_program (crates/passes/src/code_generation/program.rs) builds data_types from composite_graph.post_order(), which does not contain submodule structs. Note leo test code-generates lib in two contexts (standalone and as use_s's dependency); in the dependency context the graph came back empty (order = []), so the lib.aleo the consumer validates against has no struct — while the symbol table does contain the struct in both contexts.
A fix likely needs to (a) keep exported, concrete, current-program composites through monomorphization instead of treating them as dead, and (b) have codegen emit the current program's structs from the symbol table (using the graph for dependency ordering, plus any symbol-table structs the graph omits).
Related
Discovered while fixing #29586 (PR #29587). Distinct root cause; #29586's reporter case avoids this because its wm module's functions use U256.
Summary
An
exported submodule struct that no item in its defining program references is pruned and never emitted, so an external consumer's reference to it dangles and snarkVM rejects the consumer's bytecode:exportcontrols visibility, not liveness: a struct reachable only from another program should still be emitted by the program that defines it.Version
leo 4.3.2Minimal reproducer
leo teston a packagelibwhose submodulemexports a struct used only by another program.program.json{ "program": "lib.aleo", "version": "0.0.0", "description": "", "license": "MIT", "dependencies": null }src/main.leosrc/m.leoexport struct S { hi: u128, lo: u128, }tests/use_s.leouse_s.aleoreferenceslib.aleo/S__...(in its input type), butlib.aleoemits no struct.Notes on scope
libthat referencesS(e.g.export fn touch(x: S) -> u128 { return x.hi; }in modulem) makeslibemitSand the error disappears. So the trigger is specifically an exported submodule struct with no reference inside its own program.Root cause (from investigation)
Two coupled points:
iter_structsseeds the graph;post_orderwas empty forlib). A submodule struct is therefore only reconstructed when a reached function uses it via its signature; otherwise it falls through to the "current-program leftover = dead code" step incrates/passes/src/monomorphization/program.rsand is dropped.visit_program(crates/passes/src/code_generation/program.rs) buildsdata_typesfromcomposite_graph.post_order(), which does not contain submodule structs. Noteleo testcode-generateslibin two contexts (standalone and asuse_s's dependency); in the dependency context the graph came back empty (order = []), so thelib.aleothe consumer validates against has no struct — while the symbol table does contain the struct in both contexts.A fix likely needs to (a) keep exported, concrete, current-program composites through monomorphization instead of treating them as dead, and (b) have codegen emit the current program's structs from the symbol table (using the graph for dependency ordering, plus any symbol-table structs the graph omits).
Related
Discovered while fixing #29586 (PR #29587). Distinct root cause; #29586's reporter case avoids this because its
wmmodule's functions useU256.