Don't export internal procedures in the MAST/.masp#1203
Conversation
Component packages were exporting lowered core Wasm procedures alongside the lifted Component Model wrappers, which exposed internal implementation details such as raw core exports, init, and cabi helpers. Mark core Wasm exports as private when translating component modules, promote only wrapper targets to internal visibility for component-level calls, and assemble component support modules as private linked inputs instead of package export modules. This keeps lifted wrappers at the component level while limiting MAST and manifest exports to the Component Model surface.
Executable builds previously generated main as a separate MASM module, which forced the component initializer to be public so the generated entry block could call it. Move generated executable main emission into component lowering so it lives in the root component module alongside init. This lets init remain private while preserving the existing test-harness setup and stack truncation behavior, and removes the artifact-level synthetic main path.
Component export wrappers live in the root component module with the generated initializer, but they still rebuilt an absolute init path. That made the private init call depend on the root module keeping the component namespace after project assembly. Pass the root-local init invocation target from component-level lowering into function lowering, and make nested module lowering pass no initializer target. Canonical ABI wrappers that require initialization now emit a same-module call instead of reconstructing a qualified path.
Codegen lowering and project assembly both need the same answer for whether the active project target is executable. Keeping that predicate duplicated made main emission and project target selection vulnerable to drifting independently. Move the predicate onto Session and call it from both codegen sites. This keeps the executable-main emission decision and the project assembler selector tied to one helper while preserving the existing target-selection behavior.
Package assembly should not infer component export policy by scanning root procedures for canonical ABI signatures. That tied package visibility to an implicit component shape and made future frontend layouts easy to misclassify. Carry the support-module privacy policy on MasmComponent instead. Component lowering marks non-root support modules as private implementation details, while world lowering leaves them visible, and project assembly consumes that explicit flag for library packaging.
Several comments still described the previous assembly-time main generation and implied that frontend internal visibility directly hid MASM package exports. Update the comments to describe the current lowering-time main and test-harness emission, clarify that MASM package assembly performs support-module export pruning, and document the different visibility choices for standalone modules and component core modules.
The package-size regression test caught export pruning indirectly, but it did not verify which procedures remained public. That made leaks of core helpers or allocator shims harder to diagnose. Move the lifted Component Model export assertion into a shared helper and apply it to the counter contract, basic wallet, tx script, and note package examples. The assertions now pin the exact public procedure set, require Component Model calling conventions, and check that manifest procedure exports match the MAST exports.
The generated component initializer is always private now, and the remaining qualified init target construction no longer needs to be duplicated at each component construction site. Hardcode private visibility in the init helper, factor qualified init target creation, and expand the compiler changelog entries to describe the narrowed package export surface and preserved initializer call metadata.
Core Wasm modules are lowered through a synthetic wrapper component, but their package surface still comes from the nested core module exports. Keep support modules public for that synthetic wrapper while continuing to link support modules privately for real Component Model components. This preserves ordinary core-module library exports without reopening lifted component internals.
Address pre-submit review feedback on the component export-pruning change. - Replace `MasmComponent.init: Option<InvocationTarget>` with `requires_init: bool`. The stored target was never used as a call target; every call site rebuilds a root-local `init` symbol. This also drops the now-dead `init_invocation_target` helper. - Extract `qualified_procedure_target` to replace five copies of the qualified "stdlib/intrinsics procedure -> InvocationTarget::Path" idiom, including a byte-identical `truncate_stack` duplicate. - Rename `prepare_sources`'s `generate_executable_main` parameter to `is_executable_target`. Main generation moved to lowering, so the flag now only gates whether support modules are statically linked. - Reject a component export named `main` when generating the executable entrypoint, with an actionable error instead of an opaque assembler symbol conflict. - Document that Component Model export wrappers are only lowered in the component root module, and turn the unreachable missing-init branch into an explicit internal error.
The cross-context SDK tests and the auth-component examples only checked that a single expected export existed, so a lowered core Wasm procedure leaking back into the package export surface would not have failed them. Add `assert_all_exports_are_lifted_wrappers`, which fails if any exported procedure does not use the Component Model calling convention, and call it from the three cross-context account tests and both auth-component examples. The strict `assert_lifted_component_exports` now reuses the same check.
Follow-ups from the export-pruning review. - Gate the export-wrapper `init` prologue in `MasmFunctionBuilder::build` on the presence of the threaded root-local `init` target instead of re-deriving `has_globals() || has_data_segments()`. `define_function` already supplies the target exactly when initialization is required, so this drops the duplicated predicate and the unreachable internal-error branch. Add `LinkInfo::requires_init` as the single source of that predicate. - Extract `MasmComponent::root_module_mut`, which asserts the `modules[0]` == root invariant, and use it at the three root-module access sites. - Use `ProcedureName::is_main` in the generated-entrypoint name-collision guard. - Document that static linking (not procedure visibility) is what prunes support modules from the package export surface, and why the World path keeps them public. - Fix the `prepare_sources` comment that described the support-module linkage backwards.
`Session::new` has a local `is_executable_target` that gates the virtual-bin target fixup from `target_type` alone, which is strictly narrower than the `Session::is_executable_target()` method. Rename the local to `is_executable_target_type` so the two are not mistaken for one another.
The generated executable entrypoint is the reserved procedure `$main` (`ProcedureName::main()`), while lifted Component Model wrappers are named after their WIT exports. A WIT identifier can never be `$main`, so a wrapper can never collide with the generated entrypoint; the guard could only fire on a pre-existing `$main`, which lowering never produces. Remove the guard and its misleading "named `main`" diagnostic — `Module::define_procedure` still reports a conflict for the impossible duplicate. Also fix two stale doc comments: `MasmComponent.entrypoint` is an invocation target, not a symbol name, and `prepare_sources` partitions modules rather than synthesizing `main` (which moved to lowering).
- `assert_all_exports_are_lifted_wrappers` now fails on non-procedure (constant/ type) exports instead of skipping them, so it lives up to its name, and documents that it relies on only lifted wrappers carrying the Component Model calling convention. - Drop the manual "no intrinsics in the exports" assertion in the cross-context SDK test, now subsumed by the helper. - Assert `is_library()` on the counter-contract package before checking its exports, matching the sibling tests.
bitwalker
left a comment
There was a problem hiding this comment.
The way the module hierarchy is defined (and thus the public surface of the package that gets assembled) has changed in the upcoming VM release. In particular, the public surface of the package is determined based on the public symbols of the root module, and any of its public submodules; rather than any public symbol in either the root or its support modules. I think we have to keep the current way we assemble the target namespace (which will expose more symbols than will be exposed after we migrate to the new VM) - but we should ensure that intrinsics (and other library code that isn't part of the target namespace) aren't linked as support modules, and are instead statically-linked separately.
At assembly, non-root support modules are statically linked into the assembler instead of being surfaced as package source
This is fine for things that don't belong to the target namespace (e.g. the compiler intrinsics, statically-linked libraries, etc.). For modules that belong to the target namespace, we must continue to provide all of them as the public surface of the package - when we migrate to the next VM release, the public surface will automatically be pruned based on what is actually visible according to the module hierarchy.
The component init procedure is now a private, root-local procedure, invoked via a same-module symbol from the lifted wrappers and the generated entrypoint.
I think the component init procedure probably needs to remain public, since it must be called by more than just the component-model exports themselves, but before any exported procedure from the Miden component can be used (since those exports will assume that linear memory, etc., has been initialized). Codegen can't assume that the Miden component is a Wasm component (and therefore only has component-model exports).
In practice, currently, the component level init procedure is unlikely to be used outside the component-model exports, or the generated main procedure - but it is technically a requirement for using a component, so it should be exported IMO.
Got it.
You're right. I forgot that the end goal for the Given all of the above, I think we can safely close this PR. |
Close #1197
Compiled Miden packages now export only the lifted Component Model (CM) wrapper procedures. Internal procedures — lowered core Wasm functions, the component
init,cabi_*, and intrinsics — are kept off the package export surface. Previously they leaked in as public exports.Frontend (
frontend/wasm)Private; the specific core export wrapped by a lifted function is promoted toInternalso the wrapper can still resolve it. Standalone (non-component) module exports stayPublic.Codegen (
codegen/masm)Library.exports()and now-unreachable ones are pruned by DCE.initprocedure is now a private, root-local procedure, invoked via a same-module symbol from the lifted wrappers and the generated entrypoint.main/test-harness generation moved from assembly-time into lowering.Session (
midenc-session)Session::is_executable_target().