Project Record Field Change v1

September 10, 2026 ยท View on GitHub

Status: implemented bounded profile; HOSTED GREEN under the v0.4.0 release baseline. Historical local, authoring-time, ignored, device/simulator, or separately provisioned evidence below retains its narrower scope; public promotion, registry publication and broader product completion remain separately gated.

Audience: compiler contributors and agents editing immutable Project candidates.

add_record_field appends one explicitly identified scalar or bounded owning string/Bytes field to an existing record and derives the admitted authored constructor and exact-pattern changes. Canonical .spx remains authoritative. This operation introduces no syntax, backend exception, arbitrary source edit, graph mutation, or publication power.

Closed request

The existing Semantic Change envelope binds the current Project revision and fixed candidate requirements. Its intention is exactly:

{
  "kind": "add_record_field",
  "target": "geometry.point",
  "field": {
    "id": "geometry.point.visible",
    "name": "visible",
    "type": "bool",
    "default": { "kind": "bool", "value": false }
  }
}

Supported type/default pairs are bool, i64, i32, u8, usize, string, and Bytes. A string default is {"kind":"string","value":"..."} and is bounded to 4,096 Unicode scalar values and 16,384 UTF-8 bytes. A Bytes default is {"kind":"Bytes","values":[0,1,255]} and contains at most 4,093 exact byte integers. Every migrated constructor materializes a fresh Bytes owner through the compiler-owned bytes_copy/array_as_slice operations; ambiguous authored identities or bindings fail closed. The ordinary indexed byte data contract forbids a view of an array temporary, so the default is a block that binds its bounded array to a reserved spx_field_bytes_<n> name and then derives the view from that named place: { let spx_field_bytes_0 = [0u8, 1u8]; bytes_copy(array_as_slice(spx_field_bytes_0)) }. The index is the first whose spelling occurs in no retained canonical source, so the binder shadows no authored binding, alias, type or field; an exhausted inventory fails closed with SPX-G226. This borrows no temporary and relaxes no loan lifetime. Integer literals must be exact JSON integers with a source-representable magnitude: -i64::MAX..=i64::MAX, -i32::MAX..=i32::MAX, unsigned 8-bit, or unsigned 64-bit usize, respectively. The frozen lexer parses the positive magnitude before unary minus, so i64::MIN and i32::MIN are not admitted literal defaults; the intention rejects them with SPX-G225 before migration. This does not narrow runtime integer values or widen source syntax. The literal kind must equal the field type. Arbitrary calls, places, expressions, unknown keys, and implicit conversions are not accepted defaults. The new stable ID must be globally unused and use one to 128 lowercase ASCII ID characters; the name must be a bounded ordinary field identifier and must not already occur on the target record.

Eligibility and identity

The target is an explicit, monomorphic, authored record from retained validated HIR. Eligibility uses the compiler's checked type facts for sized, resource-free records, including both Copy records and records with owned cleanup. Empty records are allowed. Already-admitted String, Bytes, array and nested record/variant storage need not match the flat owned-byte pattern profile merely to receive an inert scalar field. The selected record's exact dependency closure must still have compiler-derived type facts; display names never establish Copy or ownership facts.

For an unused record, eligibility reconstructs only its selected nominal dependency closure from the retained checked declarations and compiler prelude. The ordinary HIR TypeFacts owner computes Copy, drop, resource, and layout facts; the operation does not infer them from function use or duplicate those rules. This temporary index is neither retained nor a new source of graph authority.

Generic target records, classes, variant targets, resources, borrowed storage, and types without admitted bounded compiler facts remain excluded. Scalar fields retain the earlier sized resource-free lane. An owning string or Bytes field uses a narrower lane: the original record must be Copy, sized, resource-free and drop-free, at least one exact constructor must exist, and no exact pattern of the target may exist. This avoids silently changing value, owning or borrowed pattern semantics. The checked transition must be exactly Copy to non-Copy and drop-free to needs-drop while remaining sized and resource-free.

Module source revision/digest facts and the target declaration's source origin must match the retained Project. Local type bindings and imported aliases map to persistent identities; imported aliases must name the authenticated owning module. Thus an imported Point renamed locally to Metric still selects the same record, while another record with a similar display name is untouched. Alias migration applies only where Project already admits the import: Copy record aliases are supported; owned-record type aliases and owned-argument function imports remain rejected by SPX-G172. Owned-record migration evidence uses local declarations without widening that cross-module boundary. The addition records the exact field ID, name, record owner ID, source path, and module. All old identities and fields retain their existing order.

Derived migration

The compiler traverses all authored function and class-method bodies, including uninstantiated generic bodies, preconditions, postconditions, guards, loops, unsafe blocks, nested constructors, and update expressions. An affected record constructor must exactly match the old field-name inventory. Its old initializer sequence remains unchanged and the default is appended last. This preserves left-to-right evaluation of the old values and their checked failure order; an initializer that was lazy remains at its original position. Scalar and string source literals are direct expressions. Bytes construction uses only the two authenticated compiler-owned byte operations, a bounded array literal, and the reserved block-local binding that names it. The block is the default expression itself: it occupies exactly the appended initializer position and inserts no statement into the enclosing body. Negative scalar defaults retain the ordinary checked unary-negation representation.

For scalar additions, exact record patterns are traversed recursively, including nested field patterns. Each affected exact pattern must match the old field inventory and receives the new field with _. Existing names, binding positions, stable field references, and nested patterns remain unchanged. Whole-record binding and wildcard patterns need no new binding. Record updates are not expanded: the ordinary compiler retains a scalar field from the base. In the owning lane, ordinary rebuilt HIR instead consumes the owned base and transfers its inherited field exactly once; the intention neither duplicates nor reconstructs it.

Owning additions reject every exact occurrence of the target in a record pattern before mutation. Whole-record bindings and wildcards are not target record patterns and remain subject to ordinary full ownership admission.

For an owned record, explicit match own and match borrow modes stay unchanged. Every old droppable field retains its required binding; only the new non-droppable scalar receives _. The migration never discards an owned field or manufactures a second owner. Existing direct field loans retain their root and persistent field identities. Full source admission still checks loan overlap and last use after migration.

Target eligibility does not extend the language's pattern or borrowing profiles. In particular, admitting an existing nested owned record for scalar field addition does not admit a new nested owning match, projected loan or import. A source that requires an unsupported operation still fails ordinary Project verification before a candidate is returned.

The broader target evidence includes migration of checked String-bearing constructor bodies outside the selected executable closure. Current aggregate String target layout remains unsupported; retaining and changing those source bodies does not establish native or Wasm execution of them. Likewise, nested or mixed owned-Bytes storage remains subject to the existing SPX-T268 source gate, even if a hypothetical type-fact calculation would be resource-free.

The operation is append-only. It neither reorders old fields nor renames old members. It intentionally changes record layout and adds a new field to values; it does not claim identical target bytes, physical layout, ABI, memory use, execution fuel, or performance. Public aggregate compatibility is not inferred from unchanged export IDs.

Independent admission and bounds

The candidate pipeline canonically formats, reparses, and independently rebuilds the complete Project under its existing profile. Ordinary ownership, cleanup, record-layout, and admitted target checks remain mandatory; the operation never relaxes them. Native C and structurally validated Core Wasm target facts are rederived where the base lane is admitted, without claiming target execution.

Appending a scalar field adds no owned cleanup leaf. Appending string or Bytes creates a fresh owner at every affected constructor and changes the record to a checked droppable value. Cleanup inventories and plans are rebuilt from the complete candidate, retaining the compiler's canonical order; they are not copied, sorted, repaired, or declared byte-equal merely because the old owned field identities remain unchanged. Only a Bytes owner is a cleanup-plan resource leaf: an owned String settles through the separate native String settlement lane, so a string addition changes the record's checked type facts while its callers' resource cleanup inventories stay empty. That is a reported fact, not a claim that aggregate String targets execute.

After admission, the operation independently reconstructs the field declaration and every migration from the prior immutable revision. All canonical candidate sources must match this reconstruction exactly. It also compares the old ordered checked field identities, names and types against the retained prefix and confirms that the single added field has its requested identity and exact checked type. Scalar additions preserve the selected record's Copy, drop, resource and sized flags. Owning additions require the exact checked transition described above. Layout is intentionally allowed to change. These checks do not copy or reinterpret cleanup vectors. Existing candidate replay binds the full change history and canonical evidence. Rebase checks record-shape conflicts and new-ID collisions before replaying the migration in the new revision; unrelated function display renames can coexist, while concurrent changes to the same record shape fail closed.

A Bytes intention retains both compiler-owned builtin identities as history dependencies. Every later candidate application rechecks their authored IDs and source spellings, and rebase compares a conservative whole-Project dependency fingerprint. A declaration or import that collides with bytes_copy or array_as_slice therefore conflicts instead of silently rebinding generated source.

The final target record has at most 64 fields. Selected type reconstruction has at most 4,096 source declarations, 1,048,576 visits, depth 256, and a 16 MiB charged-input budget; TypeFacts rendering has a separate 16 MiB output bound. Existing global nominal guards remain unchanged. Expression and pattern traversal depth is at most 256. The owning constructor/pattern preflight and the subsequent migration are two independently bounded whole-Project walks of at most 1,048,576 expression visits each. Expression migration and inserted items, including every byte in every materialized default, share a 1,048,576 item ceiling; pattern traversal is separately bounded to that many items. Existing Project source and candidate byte limits still apply. Invalid/unsupported operations use SPX-G225; capacity failures use SPX-G226. Existing candidate stale/replay and rebase diagnostics remain unchanged.

Successful and failed operations affect only private in-memory candidates. They do not rewrite source files, persist images, publish ACTIVE, or commit Git changes.

Authored evidence and remaining scope

tests/project_candidate/record_field.rs contains locally executed cases for cross-module aliases, constructor ordering, contract constructors, nested exact patterns, unchanged updates, lazy failure placement, exact replay, recovery, and tampering, stale requests, global ID/name collisions, default/type rejection, boolean fields, generic rejection and unused owned-record admission, unchanged source bytes, unrelated rename merges, competing record-shape rejection, fresh private String/Bytes owners, exact replay, target-pattern refusal, and owning-default bounds. Rebase evidence additionally changes only the retained nominal identity beneath an unchanged source alias spelling and requires a conflict.

tests/project/owned_record_field_addition.rs adds release-tested cases for flat owned-byte records, initializer order, owning match bindings, live field loans and cleanup order, imported aliases, broader checked Copy fields, and scalar default ranges.

tests/project/resource_free_record_evolution.rs authors broader owned target cases and their ordinary source-admission boundaries. These cases have hosted-green release evidence; they do not broaden runtime, matching, borrowing or ABI support.

The project_candidate and project harnesses above were run locally on a developer machine, together with cargo fmt, cargo clippy -D warnings and the library unit tests. No long or platform gate profile was executed, so this is local development evidence rather than completion evidence. General record evolution, field removal/reordering, generic record migration, owning pattern migration, arbitrary defaults, public ABI compatibility, and the full graph-operational roadmap remain open.