Project Declaration Move v1

September 10, 2026 ยท View on GitHub

Audience: compiler contributors and agents relocating checked declarations.

Status: implemented bounded profile; HOSTED GREEN under the v0.4.0 release baseline. Historical local, authoring-time, ignored, or separately provisioned observations below retain their narrower scope; public promotion and broader product completion remain separately gated.

The additive move_declaration intention relocates one existing top-level function between already authenticated Project modules. It preserves the function's explicit stable ID and display name, reconstructs imports and calls by stable identity, and passes through full candidate source and Project admission. Human .spx files remain canonical; this operation constructs an immutable private candidate and has no filesystem publication authority.

{
  "kind": "move_declaration",
  "target": "application.helper",
  "destination": "application.destination-anchor"
}

These are the exact required keys. Both selectors are existing function stable IDs of at most 4,096 UTF-8 bytes, without NUL. The destination selects a module through an explicit monomorphic top-level anchor, including an existing main. It supplies neither a source path nor an insertion position. The function is appended after the destination module's existing functions.

Admitted boundary

The target must be explicit, monomorphic, non-main, and absent from the fixed manifest's selected Web exports. All selected exports are conservatively excluded because relocation would change their authenticated source origin. The destination must be a different module, permit every existing target effect, and have no conflicting declaration or import alias with the moved function's name. Importing this exact target is permitted: those imports become the local declaration. Neither module permits nor function effects are widened.

The planner admits value-mode i64, i32, u8, usize, and bool, ordinary String ownership, direct owned Bytes, and authenticated record/variant values whose exact checked TypeFacts establish Sized resource-free storage. Nominal values must be either Copy without drop obligations or non-Copy with drop obligations. The same checks apply to parameters, results, body values and local/pattern bindings. Bare source string parameters retain their checked owning mode; explicit own remains required for Bytes and admitted owned nominals. A source type's spelling or field shape does not establish eligibility.

Planning reuses the Project's bounded cross-module admission. An explicit nongeneric resource-free record or variant containing owned Bytes may cross the boundary as a stable-ID type import, so an owning nominal function can move when no surviving caller needs a forbidden callable import. Imports of functions exposing owned nominal arguments retain SPX-G172; borrowed storage, generic types, resources and dependency cycles remain closed. Direct-Bytes and String helpers retain their existing behavior.

Bodies and contracts can contain admitted scalar expressions, local bindings, whole-binding assignments, loops, explicit monomorphic calls, Copy record and variant construction, field reads, record updates, plain Copy matching and admitted match own forms. String and byte-array literals, owned locals and internal byte/string views are also planned. Direct source callees must themselves have explicit top-level monomorphic resource-free signatures. At most 64 combined distinct direct callable and nominal type dependencies are admitted, including dependencies from contracts. Matching locals participate in alias hygiene.

Nominal type planning and nominal source/pattern rewriting each have a 4,096-node budget; the type-planning budget also charges each retained builtin occurrence. Checked HIR traversal admits at most 1,048,576 visited items and depth 256. These bounds do not charge ordinary scalar annotations as new nominal syntax.

Borrowed/shared parameters, borrowed results, explicit borrowed matching, resources and resource-containing values, field mutation, propagation, methods, host calls and audited unsafe boundaries remain excluded. Internal str and Slice<u8> views use ordinary checked loan provenance and cannot escape through the relocated signature. Generic function calls and generic source-type imports also remain closed. Fixed compiler-owned Option/Result instances keep their direct i64/bool argument rules. No type declaration moves with the function and no type argument is inferred or converted by relocation. Existing cross-module function-signature admission also remains unchanged: moving a prelude-typed signature can still reject if surviving callers would require a currently unsupported generic-signature import. A body-local prelude value does not itself require a synthetic type import.

Compiler byte and string operations are recognized from retained checked HIR operation identities at the exact original source occurrence. Their source spellings stay unchanged and never become authored function imports. Source identity/binding collisions and destination shadowing reject; dependency aliases reserve those spellings. The operation inventory does not widen nominal admission: byte_get still fails this planner's direct-i64/bool nominal argument gate because its result is Option<u8>. The same restriction preserves the current scalar type vocabulary: a string_from_char call still encounters the planner's unsupported char type, even though typed candidate expression construction can select that operation. Other String operations can relocate only when every existing signature, body, ownership, source namespace and import check succeeds.

These restrictions do not restrict unrelated functions in the Project. Caller migration uses the existing bounded exhaustive AST walker, including contracts, generic declarations, methods, guards, and loops in those callers. The full Project verifier retains responsibility for all source/profile rules and module dependency cycles; structural discovery does not pre-approve a move.

Binding migration and replay

The compiler resolves the original target's call names through its admitted local and imported function bindings. Dependencies already present in the destination reuse a stable-ID binding; otherwise an import is constructed from the existing provider's identity/module. The original alias is retained when available. On collision, the compiler chooses the first available _spx_move_N identifier, in ascending order bounded to 65,536 attempts, avoiding destination declaration/import names and moved local names. A dependency whose existing destination bindings all conflict with moved local names is rejected. No submitted alias or source fragment gains authority.

Existing destination calls through imports of the moved function are rebound to its unchanged local display name, and those target imports are removed. Other consumers keep their aliases while their target module is updated. Any remaining caller in the source module receives a function import for the moved identity. Source function-import aliases used by the moved body/contracts are removed only when no surviving source call uses that alias. Unrelated and originally unused imports remain unchanged. Dependency imports are added in stable-ID order; pre-existing retained imports keep their relative order. Self-calls retain the moved stable identity. Real module cycles still reject through ordinary complete Project admission.

Nominal dependencies bind their authenticated type owner and provider module. The destination reuses an unambiguous existing local/imported type binding or receives a deterministic type import from the actual provider. Newly selected aliases avoid destination declarations/imports and moved lexical bindings. Ambiguous or conflicting existing bindings reject rather than being silently retargeted. Source type imports remain unchanged, including ones that become unused; type-import pruning is not part of this operation.

An inferred nominal result or projected value need not have a source type alias: its retained checked owner and complete declaration shape authenticate the provider. Any actual type spelling in the moved source still requires an exact authenticated source binding before it can be rewritten.

The moved signature, explicit local annotations, constructors and type-qualified patterns use the destination binding for each stable type identity. Field and case labels, local names and type arguments remain unchanged. This includes type syntax inside contracts. A source-local type dependency can create a real module cycle after relocation; full Project admission rejects that candidate.

The compiler-owned DeclarationMove fact contains only the moved ID and its original/new path and module. Parent candidate checks transfer exactly the existing function effect/contract inventory between those modules and permit only that explicit identity's path/module fields to change. No new declaration identity is allowed. The manifest, source inventory, other identities, effect budgets, and existing contracts remain subject to their normal invariants.

After full candidate construction, movement::validate independently parses the original retained sources, reconstructs the move, formats them under the existing aggregate source limit, and compares every canonical source exactly. It additionally compares the retained per-module HIR call inventory by caller stable ID, phase (requires, body, ensures), callee stable ID, and call count. That comparison includes local and cross-file calls from declared functions and templates, with a 65,536-call bound. It ignores source paths and revision-scoped expression IDs that relocation necessarily changes; it does not infer runtime coverage or dynamic calls. Exact source reconstruction plus admitted HIR bindings prevents an accidental alias change from silently changing a callee. The moved function's checked nominal identities are also independently compared after rebuilding; a same-spelled destination type cannot replace the original owner. This is semantic identity preservation, not a runtime-equivalence claim. Place roots and projections, view-operation identities and byte-range operation identities are compared after rebuilding. Unlike signature reordering, a move introduces no argument staging or additional local copies. Ordinary source verification still reconstructs loans and cleanup plans; the planner never sorts or repairs cleanup vectors, performs a finalizer, or treats proof data as runtime permission. Source locations and generated artifacts can change.

destinations(revision, target) supplies sorted structural anchor choices for constructor discovery. Unsupported targets produce no choices. Namespace and permit checks apply, but graph cycles and full semantic admission are still decided by applying the exact request. Existing candidate replay reconstructs the full history; rebase and merge retain their conservative location/conflict checks and revalidate moved dependencies against the new admitted source base.

Malformed/unsupported constructors, selectors, namespace conflicts, and prohibited relocation shapes report SPX-G225; local movement bounds report SPX-G226. Source verification, profile admission, and cycle failures retain their owning diagnostics. Stale candidate handles remain SPX-G224, and competing semantic move histories remain SPX-G235. Failure leaves the original candidate and canonical source files unchanged.

Authored evidence and remaining work

src/project/candidate/movement.rs owns construction, discovery, and exact post-validation. Parent candidate dispatch/invariants, catalogue, schemas, and semantic rebase own integration. Five authored tests in tests/project_candidate/movement.rs cover destination alias removal, cross-file callers, body dependencies, source callers, obsolete import pruning, hygienic alias collisions, contracts, exact replay without source writes, fixed-export/main/path rejection, cycles, unrelated rename/body merges, competing locations, and stale handles. None has been run in this change.

Additional release-tested nominal cases are in tests/project_candidate/nominal_movement.rs. They cover destination type bindings, aggregate syntax, replay, and rejected relocation shapes. Discovery advertises checked nominal identity and type-binding migration constraints; an advertised destination still requires full candidate admission.

tests/project_candidate/owned_movement.rs and tests/project_candidate/nominal_movement.rs cover String call/import migration, scalar-signature internal byte work, unused owning Bytes relocation, one exact owning-nominal type import, replay and unchanged-source assertions. Owned callable-import and cycle failures remain negative cases. These are not physical execution or cross-module owning ABI evidence.

General declaration kinds, owned callable-import admission, public-export origin migration, audited boundary relocation, broader expression syntax, runtime equivalence evidence, and full graph-operational programme completion remain outside this bounded slice.