Skip to content

Exported submodule struct unused in its own program is pruned, dangling external references #29589

Description

@mohammadfawaz

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

Labels

🐛 bugSomething isn't working🧱 Core CompilerAnything related to the core compiler including parsing, analysis, transforms, codegen, etc.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions