You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Admit string and nested-record payloads in variant cases (SPX-T215) #216
Surfaced while implementing #109 (rich interaction schemas, commit 43a64ed).
The base compiler's Copy Variants v1 rule rejects string and nested-record fields inside a variant case unconditionally (SPX-T215). The rich interaction schema's derivation and decoder treat variant-case fields identically to record fields and are forward-compatible, but no compiling fixture can exercise a nested field inside a variant case, so that path is unproven.
This is a language-admission limit, not a defect in the schema work. It blocks:
proving nested payload round-trips through variant cases in the interaction schema;
any downstream consumer that wants a variant case carrying text.
Related unreachable-by-fixture guard: SPX-T223 forecloses generic-instantiated nested fields first, so the schema's type.field.generic_argument refusal is also unreachable from real source today.
Partial landing, verified on main. Not closing — the issue asks for string and nested-record payloads; only the nested-record half shipped, and only its drop-free slice.
Landed (77879d76): a monomorphic variant case field whose type is a Copy-only (drop-free) nested record is admitted — is_admitted_copy_aggregate_variant_field, variant_layout.rs's VariantFieldValueKind::Copy branch — with success, canonical round-trip and graph-revision coverage in copy_aggregate_variant_payload_is_admitted_and_round_trips_canonically (tests/language/variants_semantics.rs:156).
Still refused, each with its own regression asserting the exact code:
Shape
Diagnostic
Test
direct string case field
SPX-T215
string_variant_payload_is_rejected_distinctly_from_the_empty_variant_code — also asserts the message does not say SPX-T268, so the two refusals stay distinguishable
Why the rest is not a diagnostic relaxation.string has no cleanup-plan lifecycle of its own anywhere in the tree — only core.bytes.drop/vec/box/iter exist — and cleanup/iterator.rs::variant_leaf_lifecycle, with its two synchronized counterparts in cleanup_plan/build/type_shape.rs and cleanup.rs, emits only a flat Leaf shape for a variant case field, never recursing through shape_for_type the way an ordinary record field does. Separately, aggregate_layout.rs refuses ResolvedType::String any aggregate layout at all ("owned string values have no aggregate value layout in v1").
Admitting string, or an owned nested record, inside a variant case therefore requires building recursive cleanup-shape capability across build/replay/execute, variant_layout.rs, native codegen and Wasm codegen simultaneously — the change protocol requires those to move together, and a widening admitted by one backend and refused by another would break the first non-negotiable invariant. That is a new subsystem, not a gap-fill, so no partial version was attempted.
Remaining for closure: the string case field, and owned nested records, behind that recursive cleanup-shape work.
No code change this session. Scoping the remaining work concretely, because the previous comment's "this is a cross-backend subsystem" was a conclusion without a map, and the next attempt needs one.
I re-verified the prior tranche is intact on current main (5953e356, 85 commits since, none touching variants or strings). The shipped slice — direct Copy scalars, in-scope type parameters, and drop-free nested records — and its four tests in tests/language/variants_semantics.rs are present and unregressed.
Why string is not a widening of the SPX-T215 type check. The gate itself is one place (src/source_verify/declaration/declarations.rs:469-518). The problem is everything downstream of admitting it:
Owned Bytes in a variant case is admitted through a narrow special case, not a general mechanism.is_admitted_concrete_owned_byte_variant / is_admitted_owned_byte_variant (src/hir/type_reachability.rs:363, src/interpreter.rs:2194) have ~15–18 call sites across the resolver (resolve_pattern.rs, resolve_expr_reference.rs, generic_variant.rs, resolve_program.rs, declaration_index.rs), HIR validation (validation.rs, validation/type_profiles.rs), the interpreter (retained_call.rs, owned_try.rs), the type-checker (type_table.rs, declared_type.rs, declared_type/generic_variant.rs, oracle/matching.rs, iterative/matching.rs), native codegen and Wasm codegen. Every one hardcodes == ResolvedType::Bytes / Type::Bytes rather than delegating to a shared predicate. So there is no single place to widen.
Runtime cleanup is dispatched by lifecycle ID.BYTES_DROP_LIFECYCLE_ID = "core.bytes.drop" (src/cleanup.rs:177) is synchronized across cleanup/iterator.rs::variant_leaf_lifecycle, cleanup_plan/build/type_shape.rs, cleanup_plan/execute.rs and cleanup_plan/replay/nested_shape.rs. None of them knows a string lifecycle.
Native representation differs, so this is new memory-management codegen, not reuse.src/codegen/native_bytes/owned_leaf.rs has a closed OwnedLeafKind{Bytes,Vec,Box,Iter} with hardcoded spx_bytes_move/spx_bytes_drop against the spx_bytes_v1 struct. An ordinary owned string is a different representation entirely — char * / header-prefixed spx_string_v10, dropped via spx_string_drop (src/codegen/native_emit/mod.rs:1240-1330). Reusing spx_string_drop is plausible, but it has to be threaded as a genuinely new OwnedLeafKind::String through native_bytes.rs, variant_match.rs and nested_owned.rs, with an equivalent new path in src/wasm/aggregate.rs's own lifecycle dispatch (:400, :3437).
A second, independent refusal exists.aggregate_layout.rs:275 refuses ResolvedType::String any layout at all ("owned string values have no aggregate value layout in v1") in the executable-aggregate ABI path used by generic records, UI-schema records and owned-data exports. Anything routing through that path needs this resolved too, separately from (1)–(3).
Why I stopped rather than starting it. AGENTS.md requires parser, formatter, resolver/HIR, verifier, graph, native and Wasm to move together, and states that a partial cross-component change is worse than none. Items 2–3 are new runtime memory management in two backends; verifying them honestly needs cross-backend execution fixtures, not diagnostics. Landing half of that and reporting it as "admitted" is precisely how a backend disagreement gets introduced silently.
Recommended scope for the next attempt, in order: start from the OwnedLeafKind/lifecycle-ID plumbing (items 2–3), since that is the part with no existing shape to copy; collapse the ~15–18 hardcoded == Bytes comparisons behind one shared predicate first, as a separate no-behaviour-change commit, so the widening then happens in one place; and make the acceptance test a cross-backend execution fixture that compares interpreter, native and Wasm results to each other — not one that asserts each independently succeeded, which is the vacuity pattern this repository has repeatedly found.
Nested owned-record payloads remain blocked on the same recursive cleanup-shape work and should ride the same tranche.
The drop-free nested-record admission already present is now exercised through Agent interaction-schema derivation, canonical decoding, provider schema references, and a hostile unknown nested-field rejection. All 18 interaction-schema unit tests passed locally. This is coverage of the admitted Copy-only profile; strings and owned nested variant payloads remain outside it, so #216 stays open.
Direct monomorphic owned String variant payloads now execute through the interpreter, C11 and Core Wasm. The profile admits String plus existing Copy scalars, with constructor/case/field identities, own/borrow calls and matching. It introduces the independently reconstructed core.string.drop lifecycle and moves standalone String ownership into canonical cleanup-plan slots. String result traces use an opaque primitive marker, never pointers or text.
The implementation also handles local String relays, scalar-match guard/arm cleanup, content equality and the existing String intrinsics in aggregate Wasm. The generated Web adapter uses the aggregate byte carrier and explicit private runtime imports; it does not reuse incompatible standalone arena handles or add source authority. Actual repeated execution caught and fixed one borrowed-place clone leak per invocation.
Focused local evidence: all three cleanup_backends::owned_string_variant tests pass. The same program returns 16 in the interpreter, native Clang C11 at -O0/-O2 (three invocations with equal allocation/free counts), and one Node 24.3.0 Core-Wasm instance invoked 32 times under a 16-entry allocation limit. Canonical source/graph checks and stable profile refusals pass. The focused String unit run passed 58 tests; its remaining fixture was corrected to create a value requiring finalization, and that exact unit then passed. No full or hosted gates were run, per the requested execution workflow.
The owning profile and executable gate are documented in docs/OWNED-STRING-VARIANTS-V1.md. Local logs: .agent-logs/0913-string-backends-final.log and 0913-string-native-unit-final.log.
Still open: nested owned-record variant payloads and broader generic String substitutions remain refused. Direct owned variant-match expressions returning String also retain SPX-T216. This completes the direct-text payload tranche, not the remaining recursive owned-record scope of #216.
Established empirically with fixtures and semaprax check what is actually admitted today, rather than trusting the issue's original framing (which predates two shipped features):
Shape
Status before this change
Direct string case field, alone
admitted — Owned String Variant v1, 85014337
Drop-free Copy-closed nested-record case field, alone
So the premise "SPX-T215 rejects string and nested-record fields unconditionally" has not held for some time. The real defect was narrower and is now fixed: two declaration-level gates in the same file disagreed with each other. check_byte_data_declarations's has_direct_string branch consulted only owned_byte_record_copy_field_is_admitted (direct Copy scalars) and never the newer is_admitted_copy_aggregate_variant_field predicate that the SPX-T215 check further down the same file already uses, producing:
SPX-T268: owned-string variant field `Payload.value` must be direct `string` or a direct Copy scalar
That was an inconsistency, not a rule anyone chose. b23972b8 makes SPX-T268 agree with SPX-T215. Nothing is weakened — a sibling field that itself needs a drop, or a class sibling, is still refused, and both have regressions. No golden or KAT pinned the old wording.
Evidence: cargo test --locked -p semaprax --lib -- agent_lifecycle::rich_stage source_verify::declaration::declarations::tests → 9 passed, 0 failed (3 new regressions; 6 pre-existing rich_stage tests confirm that file is comments-only here, verified by diff).
Also corrected the stale "Known limitation" rationale in src/agent_lifecycle/rich_stage.rs, which still claimed SPX-T215 forbids nested records in variant cases.
Why this stays open — and exactly what is left
Admission is not execution. Running either payload through authorize/reduce, or passing one as a Proposal parameter, is still refused by the interpreter's retained-call vocabulary:
Prior comments scoped this at a high level. It is now pinned to three small functions:
is_admitted_owned_variant in src/interpreter/variant_admission.rs — currently only is_admitted_owned_byte_variant OR is_admitted_owned_string_variant; needs a third arm for Copy Aggregate Variant Payload v1, mirroring hir::is_admitted_copy_aggregate_variant_field.
harvest() in src/interpreter/retained_call.rs — no ResolvedType::String match arm.
retained_leaf_is_admitted() in src/interpreter/retained_call.rs — omits String; this is what gates flat-record parameters such as Proposal.
That work is in flight now. Until a fixture actually executes both payload shapes end to end, this issue should stay open.
Activity
feat(variants): admit a Copy-only nested record as a variant case pay…
fix(skill-bundle): regenerate the stale pinned agent skill bundle, an…
wsdt commentedon Sep 12, 2026
Partial landing, verified on
main. Not closing — the issue asks for string and nested-record payloads; only the nested-record half shipped, and only its drop-free slice.Landed (
77879d76): a monomorphic variant case field whose type is a Copy-only (drop-free) nested record is admitted —is_admitted_copy_aggregate_variant_field,variant_layout.rs'sVariantFieldValueKind::Copybranch — with success, canonical round-trip and graph-revision coverage incopy_aggregate_variant_payload_is_admitted_and_round_trips_canonically(tests/language/variants_semantics.rs:156).Still refused, each with its own regression asserting the exact code:
stringcase fieldSPX-T215string_variant_payload_is_rejected_distinctly_from_the_empty_variant_code— also asserts the message does not saySPX-T268, so the two refusals stay distinguishableBytesleafSPX-T215andSPX-T268copy_aggregate_variant_payload_rejects_a_nested_owned_leafSPX-T215copy_aggregate_variant_payload_rejects_a_class_and_a_generic_instanceWhy the rest is not a diagnostic relaxation.
stringhas no cleanup-plan lifecycle of its own anywhere in the tree — onlycore.bytes.drop/vec/box/iterexist — andcleanup/iterator.rs::variant_leaf_lifecycle, with its two synchronized counterparts incleanup_plan/build/type_shape.rsandcleanup.rs, emits only a flatLeafshape for a variant case field, never recursing throughshape_for_typethe way an ordinary record field does. Separately,aggregate_layout.rsrefusesResolvedType::Stringany aggregate layout at all ("owned string values have no aggregate value layout in v1").Admitting
string, or an owned nested record, inside a variant case therefore requires building recursive cleanup-shape capability across build/replay/execute,variant_layout.rs, native codegen and Wasm codegen simultaneously — the change protocol requires those to move together, and a widening admitted by one backend and refused by another would break the first non-negotiable invariant. That is a new subsystem, not a gap-fill, so no partial version was attempted.Remaining for closure: the
stringcase field, and owned nested records, behind that recursive cleanup-shape work.wsdt commentedon Sep 12, 2026
No code change this session. Scoping the remaining work concretely, because the previous comment's "this is a cross-backend subsystem" was a conclusion without a map, and the next attempt needs one.
I re-verified the prior tranche is intact on current
main(5953e356, 85 commits since, none touching variants or strings). The shipped slice — direct Copy scalars, in-scope type parameters, and drop-free nested records — and its four tests intests/language/variants_semantics.rsare present and unregressed.Why
stringis not a widening of the SPX-T215 type check. The gate itself is one place (src/source_verify/declaration/declarations.rs:469-518). The problem is everything downstream of admitting it:Bytesin a variant case is admitted through a narrow special case, not a general mechanism.is_admitted_concrete_owned_byte_variant/is_admitted_owned_byte_variant(src/hir/type_reachability.rs:363,src/interpreter.rs:2194) have ~15–18 call sites across the resolver (resolve_pattern.rs,resolve_expr_reference.rs,generic_variant.rs,resolve_program.rs,declaration_index.rs), HIR validation (validation.rs,validation/type_profiles.rs), the interpreter (retained_call.rs,owned_try.rs), the type-checker (type_table.rs,declared_type.rs,declared_type/generic_variant.rs,oracle/matching.rs,iterative/matching.rs), native codegen and Wasm codegen. Every one hardcodes== ResolvedType::Bytes/Type::Bytesrather than delegating to a shared predicate. So there is no single place to widen.BYTES_DROP_LIFECYCLE_ID = "core.bytes.drop"(src/cleanup.rs:177) is synchronized acrosscleanup/iterator.rs::variant_leaf_lifecycle,cleanup_plan/build/type_shape.rs,cleanup_plan/execute.rsandcleanup_plan/replay/nested_shape.rs. None of them knows a string lifecycle.src/codegen/native_bytes/owned_leaf.rshas a closedOwnedLeafKind{Bytes,Vec,Box,Iter}with hardcodedspx_bytes_move/spx_bytes_dropagainst thespx_bytes_v1struct. An ordinary owned string is a different representation entirely —char */ header-prefixedspx_string_v10, dropped viaspx_string_drop(src/codegen/native_emit/mod.rs:1240-1330). Reusingspx_string_dropis plausible, but it has to be threaded as a genuinely newOwnedLeafKind::Stringthroughnative_bytes.rs,variant_match.rsandnested_owned.rs, with an equivalent new path insrc/wasm/aggregate.rs's own lifecycle dispatch (:400,:3437).aggregate_layout.rs:275refusesResolvedType::Stringany layout at all ("owned string values have no aggregate value layout in v1") in the executable-aggregate ABI path used by generic records, UI-schema records and owned-data exports. Anything routing through that path needs this resolved too, separately from (1)–(3).Why I stopped rather than starting it. AGENTS.md requires parser, formatter, resolver/HIR, verifier, graph, native and Wasm to move together, and states that a partial cross-component change is worse than none. Items 2–3 are new runtime memory management in two backends; verifying them honestly needs cross-backend execution fixtures, not diagnostics. Landing half of that and reporting it as "admitted" is precisely how a backend disagreement gets introduced silently.
Recommended scope for the next attempt, in order: start from the
OwnedLeafKind/lifecycle-ID plumbing (items 2–3), since that is the part with no existing shape to copy; collapse the ~15–18 hardcoded== Bytescomparisons behind one shared predicate first, as a separate no-behaviour-change commit, so the widening then happens in one place; and make the acceptance test a cross-backend execution fixture that compares interpreter, native and Wasm results to each other — not one that asserts each independently succeeded, which is the vacuity pattern this repository has repeatedly found.Nested owned-record payloads remain blocked on the same recursive cleanup-shape work and should ride the same tranche.
Validate nested schemas and correct checked write outcome design
wsdt commentedon Sep 13, 2026
Pushed to main in 6a15535.
The drop-free nested-record admission already present is now exercised through Agent interaction-schema derivation, canonical decoding, provider schema references, and a hostile unknown nested-field rejection. All 18 interaction-schema unit tests passed locally. This is coverage of the admitted Copy-only profile; strings and owned nested variant payloads remain outside it, so #216 stays open.
wsdt commentedon Sep 13, 2026
Pushed to main in 8501433 (starting from 0eb2f3d).
Direct monomorphic owned String variant payloads now execute through the interpreter, C11 and Core Wasm. The profile admits String plus existing Copy scalars, with constructor/case/field identities, own/borrow calls and matching. It introduces the independently reconstructed
core.string.droplifecycle and moves standalone String ownership into canonical cleanup-plan slots. String result traces use an opaque primitive marker, never pointers or text.The implementation also handles local String relays, scalar-match guard/arm cleanup, content equality and the existing String intrinsics in aggregate Wasm. The generated Web adapter uses the aggregate byte carrier and explicit private runtime imports; it does not reuse incompatible standalone arena handles or add source authority. Actual repeated execution caught and fixed one borrowed-place clone leak per invocation.
Focused local evidence: all three
cleanup_backends::owned_string_varianttests pass. The same program returns 16 in the interpreter, native Clang C11 at -O0/-O2 (three invocations with equal allocation/free counts), and one Node 24.3.0 Core-Wasm instance invoked 32 times under a 16-entry allocation limit. Canonical source/graph checks and stable profile refusals pass. The focused String unit run passed 58 tests; its remaining fixture was corrected to create a value requiring finalization, and that exact unit then passed. No full or hosted gates were run, per the requested execution workflow.The owning profile and executable gate are documented in docs/OWNED-STRING-VARIANTS-V1.md. Local logs: .agent-logs/0913-string-backends-final.log and 0913-string-native-unit-final.log.
Still open: nested owned-record variant payloads and broader generic String substitutions remain refused. Direct owned variant-match expressions returning String also retain SPX-T216. This completes the direct-text payload tranche, not the remaining recursive owned-record scope of #216.
fix(verify): let SPX-T268 agree with SPX-T215 on Copy aggregate varia…
wsdt commentedon Sep 17, 2026
Progress —
b23972b8— not closingEstablished empirically with fixtures and
semaprax checkwhat is actually admitted today, rather than trusting the issue's original framing (which predates two shipped features):stringcase field, alone8501433777879d76So the premise "SPX-T215 rejects
stringand nested-record fields unconditionally" has not held for some time. The real defect was narrower and is now fixed: two declaration-level gates in the same file disagreed with each other.check_byte_data_declarations'shas_direct_stringbranch consulted onlyowned_byte_record_copy_field_is_admitted(direct Copy scalars) and never the neweris_admitted_copy_aggregate_variant_fieldpredicate that the SPX-T215 check further down the same file already uses, producing:That was an inconsistency, not a rule anyone chose.
b23972b8makes SPX-T268 agree with SPX-T215. Nothing is weakened — a sibling field that itself needs a drop, or a class sibling, is still refused, and both have regressions. No golden or KAT pinned the old wording.Evidence:
cargo test --locked -p semaprax --lib -- agent_lifecycle::rich_stage source_verify::declaration::declarations::tests→ 9 passed, 0 failed (3 new regressions; 6 pre-existingrich_stagetests confirm that file is comments-only here, verified by diff).Also corrected the stale "Known limitation" rationale in
src/agent_lifecycle/rich_stage.rs, which still claimed SPX-T215 forbids nested records in variant cases.Why this stays open — and exactly what is left
Admission is not execution. Running either payload through
authorize/reduce, or passing one as aProposalparameter, is still refused by the interpreter's retained-call vocabulary:Prior comments scoped this at a high level. It is now pinned to three small functions:
is_admitted_owned_variantinsrc/interpreter/variant_admission.rs— currently onlyis_admitted_owned_byte_variant OR is_admitted_owned_string_variant; needs a third arm for Copy Aggregate Variant Payload v1, mirroringhir::is_admitted_copy_aggregate_variant_field.harvest()insrc/interpreter/retained_call.rs— noResolvedType::Stringmatch arm.retained_leaf_is_admitted()insrc/interpreter/retained_call.rs— omitsString; this is what gates flat-record parameters such asProposal.That work is in flight now. Until a fixture actually executes both payload shapes end to end, this issue should stay open.
feat(interpreter): admit string and Copy aggregate variant payloads i…
refactor(variants): give the variant payload profile one owning predi…