Skip to content

Don't export internal procedures in the MAST/.masp#1203

Open
greenhat wants to merge 15 commits into
nextfrom
i1197-export-internal-codex
Open

Don't export internal procedures in the MAST/.masp#1203
greenhat wants to merge 15 commits into
nextfrom
i1197-export-internal-codex

Conversation

@greenhat

@greenhat greenhat commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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)

  • Core Wasm exports inside a component are translated as Private; the specific core export wrapped by a lifted function is promoted to Internal so the wrapper can still resolve it. Standalone (non-component) module exports stay Public.

Codegen (codegen/masm)

  • At assembly, non-root support modules are statically linked into the assembler instead of being surfaced as package source, so internal procedures never reach Library.exports() and now-unreachable ones are pruned by DCE.
  • 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.
  • Executable main/test-harness generation moved from assembly-time into lowering.

Session (midenc-session)

  • Centralized executable-target detection in Session::is_executable_target().

greenhat added 15 commits June 19, 2026 16:02
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.
@greenhat greenhat marked this pull request as ready for review June 23, 2026 12:20
@greenhat greenhat requested a review from bitwalker June 23, 2026 12:20

@bitwalker bitwalker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@greenhat

Copy link
Copy Markdown
Contributor Author

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.

Got it.

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.

You're right. I forgot that the end goal for the init procedure to be called by the VM before the first invocation of any component method only once (per component instance).

Given all of the above, I think we can safely close this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Don't export internal procedures in the compiled package

2 participants