Skip to content

v3: implement substrate reflection prereqs 1 and 2 - #458

Merged
briansrls merged 6 commits into
mainfrom
keen-ram-105
Apr 15, 2026
Merged

briansrls merged 6 commits into
mainfrom
keen-ram-105

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Summary

Implements the two locked compiler-side prerequisites from the substrate reflection design work:

  1. Prereq 1: field-access lowering

    • adds TransformTarget::FieldProject { parent_type, field_label }
    • lowers local dotted paths like value.field into one projection Transform per segment
    • teaches inference and emit_rust to handle the new target directly
    • adds structural tests for success, non-Conj rejection, and missing-field diagnostics
  2. Prereq 2: match-with-payload-binding

    • adds SurfacePattern::VariantWith { name, binding, span }
    • adds PayloadBinding plus Path.binding: Option<PayloadBinding>
    • keeps BranchPattern at its existing two variants
    • lowers single-positional payload captures into typed arm-local ports and inserts the binding into arm scope
    • adds tests for basic payload capture, mixed bare+payload arms, payload + field access integration, and fail-closed non-Disj behavior

The branch is intentionally split into two commits, one per prereq, because both changes touch lower.rs and the shared PR avoids merge churn there.

Why

PR #455 locked the design answers for these two prerequisites after PR #453 showed the failure modes:

  • no synthesized accessor declarations for field access
  • no flat 4-way BranchPattern growth for payload binding

This implementation follows those decisions directly:

  • field access is represented as a typed FieldProject transform target
  • payload extraction is represented on Path.binding, separate from BranchPattern discrimination

Non-goals

Validation

  • cargo test -p v3-compiler
  • cargo clippy -p v3-compiler --all-targets -- -D warnings

Commit split

  • v3: lower local field access to FieldProject
  • v3: add match payload binding on Path

Replace expression-position local dotted paths with structural `TransformTarget::FieldProject { parent_type, field_label }` nodes. Lowering now emits one projection Transform per segment, inference resolves the projected field type directly from the parent Conj, and emit_rust renders the projection as `.field` without synthesizing accessor declarations.

§9 receipt: extend the Transform target coproduct, not Behavior and not the declaration table. Field access is a dispatch target carried by Transform itself; this commit does not add synthesized accessor Declarations, parallel metadata, or surface-IR carry-through.

§8.10 4-pattern check: Pattern 1 fails because the projection fact has no other natural home; Pattern 2 fails because Callable, FieldProject, and Operator carry different payloads; Pattern 3 fails because projection is intrinsic structural elimination, not an algebra field; Pattern 4 fails because the dispatch targets are categorically distinct. Verdict: FieldProject is terminal at current scope.

Validation: cargo test -p v3-compiler; cargo clippy -p v3-compiler --all-targets -- -D warnings
Add `SurfacePattern::VariantWith { name, binding, span }` to the parser, add `PayloadBinding { binding_name, payload_port }` plus `Path.binding: Option<PayloadBinding>` to the substrate, and lower single-positional variant payload captures into typed arm-local ports.

BranchPattern stays at its existing two variants. Discrimination remains on `path.pattern`; extraction lives on `path.binding`. Lowering resolves the matched variant against the scrutinee Disj, allocates the payload port, inserts the binding name into the arm-local scope, and leaves inference decoupled from the binding record.

`binding_name` is carry-forward surface information for lowering scope insertion and readable emission; inference consumes `payload_port` and does not consult the name.

Validation: cargo test -p v3-compiler; cargo clippy -p v3-compiler --all-targets -- -D warnings
@briansrls

briansrls commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review

Principle audit.

FAIL-CLOSED. Mostly satisfies, but behaviorally. New user-path failures become diagnostics, not silent drops: lower_field_path_expr diagnoses empty/nonlocal/unresolved/non-Conj/missing-field paths (lower.rs:1818-1896), lower_payload_binding marks the payload port unresolved with specific ResolveErrors (lower.rs:1924-2026), and decide_field_project returns ArityMismatch/TypeMismatch/ResolveError instead of panicking (infer.rs:714-806). The caveat is that valid projections can still die early if the receiver is merely uninferred during lowering (lower.rs:1853-1862, 1925-1937); that is fail-closed behavior, not fail-closed structure.

ILLEGAL STATES UNREPRESENTABLE. Does not satisfy; this is structural. Path { pattern, binding: Option<PayloadBinding> } admits states that should never exist: binding: Some(_) on a bool if/else path, binding: None on a VariantWith arm, and the same None meaning both “bare-variant arm” and “not a match payload at all” (dag.rs:724-727; lower.rs:2138-2147, 2169-2201). TransformTarget::FieldProject { parent_type, field_label } also admits impossible pairs that only inference later rejects (dag.rs:624-632; infer.rs:740-794). The type system is not stopping illegal combinations here.

FACTS FLOW FORWARD. Fails, and this is the deepest concrete bug. Payload binding is the good half: lowering turns the matched payload into a typed port and the arm body consumes that port (lower.rs:2018-2026, 2178-2193). Field projection does the opposite. Lowering already resolves each dotted segment to a concrete child declaration field_decl_id (lower.rs:1883-1886), then drops that fact and stores only field_label on the transform target (lower.rs:1902-1905). Inference re-derives the child by string lookup (infer.rs:775-795). The instantiation facts are dropped too: the thesis says substitution must happen lazily via a substitution stack, but decide_field_project resolves the field type with walk_to_type_shape(..., &SubstStack::new(), 0), so generic projections have no way to preserve T := ... from the receiver type. The fact that should flow is resolved child identity plus substitution context, not a label that must be looked up again.

COPROD DISSOLUTION. Fails. SurfacePattern at least declares itself 🟡 scaffold, though it still lacks a named dissolution trigger; “future extensions go through audit” is not a trigger (parse.rs:465-500). The blocking problem is TransformTarget::FieldProject: the diff marks it terminal (dag.rs:603-615), but the thesis uses FieldAccess = Product elimination as the canonical algebraic-form example that should dissolve into std/, not survive as a terminal enum arm. On the project’s own rule set, FieldProject has not passed pattern 3; it has been accepted in defiance of it. chatgpt-review-e33c25ee-f210-45…

SINGLE AUTHORITY. Fails structurally. Field projection now has multiple competing authorities: the input port’s resolved type, the duplicated parent_type stored on FieldProject, and the field label string that is looked up again later (dag.rs:624-632; infer.rs:740-795). Lowering even computes the canonical child declaration once, then throws it away (lower.rs:1883-1905). The post-lowering rule in this repo is explicit: downstream stages should read facts from Dag.declarations by DeclarationId, not carry a String beside it and recover structure later. This PR does the opposite.

API-LEVEL ENFORCEMENT. Fails. Nothing in the substrate types prevents constructing an inconsistent Path/PayloadBinding pair or a FieldProject whose label is not a child of its parent. Nothing in the API preserves instantiated field types either; that obligation lives in a handwritten check inside decide_field_project. The bad states are rejected late, behaviorally, by validation code. The discipline here is to move those guarantees into the boundary type so the invalid state is impossible to construct; this diff does not do that.

Design question.

Is field access in v3 a terminal computation-target (parent_type, field_label), or is it product elimination that must carry resolved child identity and instantiation substitution from the type substrate? That answer is load-bearing: if it is treated as a terminal target, every future consumer—emit, interpreter, lenses, complexity—learns a second, name-based authority, and generic field access stays wrong by construction; if it is product elimination, this PR needs to move the resolved child fact into the substrate now.

Verdict.

REQUEST_CHANGES. The direction is right—this round adds real user-facing capability and real substrate tests instead of more inert scaffolding—but the FieldProject design bakes in exactly the post-lowering string recovery and duplication of authority the thesis is trying to eliminate. Fixing that now is much cheaper than migrating every future consumer off (DeclarationId, String).

LOOP HEALTH: 📉 ACCUMULATING_DEBT — this round does enable real consumer tests, but it also adds a new scaffold (VariantWith) and reintroduces the already-known post-lowering name-recovery pattern via field_label, so capability is growing faster than substrate debt is being dissolved.


View conversation

@briansrls
briansrls marked this pull request as ready for review April 15, 2026 22:19

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb6e4207cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/v3/compiler/src/lower.rs Outdated
Comment on lines +1925 to +1929
let Some(scrutinee_decl_id) = (match dag.port(scrutinee_port).state() {
PortState::Resolved(ty) => Some(ty.declaration),
PortState::Unresolved | PortState::Uninferred => None,
}) else {
dag.mark_unresolved(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid lowering-time rejection of uninferred match scrutinees

lower_payload_binding immediately fails when the scrutinee port is Uninferred, but lowering runs before inference and many valid scrutinees (for example SurfaceExpr::Call and operator expressions) are created with untyped output ports and only resolved later. This makes valid payload matches like match id(x) { Some(v) => ... } fail closed purely due to phase ordering, even though the infer pass can resolve the scrutinee to a Disj afterward.

Useful? React with 👍 / 👎.

Comment thread src/v3/compiler/src/infer.rs Outdated
},
);
};
let Some(output_ty) = walk_to_type_shape(dag, field_decl_id, &SubstStack::new(), 0) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Substitute template args when inferring field projection output

decide_field_project computes the projected field type with walk_to_type_shape(..., &SubstStack::new(), ...), which drops any instantiation context from parent_type. For generic records (e.g., Box<Int> with field type T), this either fails (TypeParam lookup has no substitution) or yields an unsubstituted type, so valid projections like b.value cannot type-resolve correctly.

Useful? React with 👍 / 👎.

Comment thread src/v3/compiler/src/lower.rs Outdated
Comment on lines +2018 to +2019
match declaration_to_port_shape(payload_type_decl, dag, span) {
Ok(ty) => dag.set_port_type(payload_port, ty),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Instantiate payload binding types from scrutinee arguments

Payload bindings take payload_type_decl directly from the variant template and pass it to declaration_to_port_shape without substituting the scrutinee’s instantiated arguments. On instantiated sums like Maybe<Int>, the bound variable gets the template TypeParam id instead of Int, which then causes downstream branch-arm type mismatches in otherwise valid matches.

Useful? React with 👍 / 👎.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · bb6e4207

⚠️ Review (blocking: 2, non-blocking: 2+/0-)

BLOCKING (2)

Root Cause

  • src/v3/compiler/src/lower.rs Both prerequisites resolve user-visible structure from PortState during lowering instead of carrying unresolved projection/payload facts into infer, which duplicates type authority via FieldProject.parent_type and makes valid programs fail before the fixpoint can type them -> move both resolutions behind infer and derive them from the input port or resolved branch variant.

Non-blocking — Strengths

  • src/v3/compiler/src/dag.rs TransformTarget::FieldProject lands with an explicit lifecycle receipt, so the new enum arm respects the coproduct-dissolution ratchet.
  • src/v3/compiler/tests/m1_substrate_test.rs The tests cover both success cases and fail-closed rejection paths for each prerequisite, which is the right regression surface for substrate work.

⚠️ The new field-access and payload-binding paths still depend on lowering-time typing, so they reject valid inferred-local programs and would calcify the wrong phase boundary if merged.

Comment thread src/v3/compiler/src/lower.rs Outdated
},
)
}
PortState::Uninferred => {

This comment was marked as resolved.

Comment thread src/v3/compiler/src/lower.rs Outdated
) -> PayloadBinding {
let payload_port = dag.alloc_port(None);
let Some(scrutinee_decl_id) = (match dag.port(scrutinee_port).state() {
PortState::Resolved(ty) => Some(ty.declaration),

This comment was marked as resolved.

@briansrls

Copy link
Copy Markdown
Contributor Author

codex · gpt-5.4 · bb6e4207

⚠️ Review (blocking: 2, non-blocking: 2+/0-)

BLOCKING (2)

Root Cause

  • src/v3/compiler/src/lower.rs Both prerequisites resolve user-visible structure from PortState during lowering instead of carrying unresolved projection/payload facts into infer, which duplicates type authority via FieldProject.parent_type and makes valid programs fail before the fixpoint can type them -> move both resolutions behind infer and derive them from the input port or resolved branch variant.

Non-blocking — Strengths

  • src/v3/compiler/src/dag.rs TransformTarget::FieldProject lands with an explicit lifecycle receipt, so the new enum arm respects the coproduct-dissolution ratchet.
  • src/v3/compiler/tests/m1_substrate_test.rs The tests cover both success cases and fail-closed rejection paths for each prerequisite, which is the right regression surface for substrate work.

⚠️ The new field-access and payload-binding paths still depend on lowering-time typing, so they reject valid inferred-local programs and would calcify the wrong phase boundary if merged.

briansrls added a commit that referenced this pull request Apr 15, 2026
Three coherence edits now that prereq implementations are in flight:

1. §11 Prereq 1 checklist — removed stale "delete substrate.dag
   FieldAccessor marker" item (§3.6 1a never adds FieldAccessor,
   so there's nothing to delete). Added infer.rs decide_field_project
   and multi-hop test items to match #458's actual implementation.

2. §11 Prereq 3 checklist — added call-site rewrite item, unannotated
   fail-closed test, and a scope note documenting the contextual-first
   landing (#457's intentional scope: lambdas lower only when an
   expected function type is available). Callback-rule lens test
   moved to follow-up scope.

3. inference-as-data-experiments.md §6 Q6 — replaced placeholder
   with I0 result summary and cross-reference to
   docs/inference-as-data-i0-result.md (ships with #457). Updated
   §7 "When this doc updates" to mark I0 as complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Apr 15, 2026
Three coherence edits now that prereq implementations are in flight:

1. §11 Prereq 1 checklist — removed stale "delete substrate.dag
   FieldAccessor marker" item (§3.6 1a never adds FieldAccessor,
   so there's nothing to delete). Added infer.rs decide_field_project
   and multi-hop test items to match #458's actual implementation.

2. §11 Prereq 3 checklist — added call-site rewrite item, unannotated
   fail-closed test, and a scope note documenting the contextual-first
   landing (#457's intentional scope: lambdas lower only when an
   expected function type is available). Callback-rule lens test
   moved to follow-up scope.

3. inference-as-data-experiments.md §6 Q6 — replaced placeholder
   with I0 result summary and cross-reference to
   docs/inference-as-data-i0-result.md (ships with #457). Updated
   §7 "When this doc updates" to mark I0 as complete.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@briansrls

briansrls commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review

Principle audit.

FAIL-CLOSED. Mostly satisfies, behaviorally. The new explicit failure paths all go through diagnostics: dotted-path lowering fails closed on empty/nonlocal/uninferred/non-Conj/missing-field cases in lower.rs:1812-1915, payload-capture lowering diagnoses every rejected shape in lower.rs:1917-2033, and decide_field_project fails through ArityMismatch / ResolveError / TypeMismatch rather than panicking in infer.rs:708-809. I do not see a new silent None-on-error path here. The caveat is that this is still behavioral, not structural, because the guarantees live in lowering/infer code rather than in an unrepresentable boundary type. NON-BLOCKING by itself.

ILLEGAL STATES UNREPRESENTABLE. Violates, structurally, and this is BLOCKING. PayloadBinding can hold any PortId, and Path can carry binding: Some(_) with no type-level guarantee that the port is arm-local, sourced from the enclosing branch input, or even semantically related to that path at all (dag.rs:703-727). The lowerer currently constructs the binding with dag.alloc_port(None) and then blesses it by convention (lower.rs:1924-2033). A pattern-bound payload is not “just any port”; if the reviewer can imagine attaching an arbitrary port there, the type is too weak.

FACTS FLOW FORWARD. Violates, structurally, and BLOCKING. For Variant(binding), the upstream facts are: the scrutinee, the selected variant, the payload type, and the binder spelling. This diff uses those facts to typecheck the arm body, but after lowering the new substrate fact stops being authoritative: the code comment on PayloadBinding explicitly says inference does not consult it and it remains only as carry-forward for downstream rendering (dag.rs:704-708), and the tests are the only actual readers of the new field (m1_substrate_test.rs:1490-1580). That is speculative boundary data, not end-to-end fact flow. The project’s own boundary rule is that new semantic boundary data should not land without a real downstream consumer using it as authority. chatgpt-review-d0068318-2595-40…

COPRODUCT DISSOLUTION. On the changed terminal/substrate choice, this round is better than the prior one. I would not block TransformTarget::FieldProject: the current thesis explicitly says structural projection on Conj and case discrimination on Disj are substrate primitives expressed through Behavior::Transform and Behavior::Branch, not std/ algebra-inhabitance dispatch. chatgpt-review-64c9ee6f-8d62-48…

The remaining caveat is SurfacePattern: VariantWith widens an already-🟡 parse scaffold (parse.rs:465-500, 1476-1516) before the branch substrate has an end-to-end authoritative consumer for the new payload-binding fact. That caveat is behavioral. I would not make coproduct classification my primary block this round.

SINGLE AUTHORITY. Violates, structurally, and BLOCKING. FieldProject is introduced as a structural substrate target, but emit_rust immediately gives it a second authority by hardcoding its realization as parent_expr.field_label and ignoring parent_type entirely (emit_rust.rs:553-577). That is not “translation from annotated graph + language spec”; it is a fresh Rust-specific semantics path in compiler code. The invariants are explicit that emission should read graph facts and LanguageSpec, not decide. chatgpt-review-fee87877-916f-49…

API-LEVEL ENFORCEMENT. Violates, structurally, and BLOCKING. Nothing in the API prevents constructing Path { binding: Some(PayloadBinding { payload_port: arbitrary_port, .. }), .. }, and nothing in the type system forces any downstream consumer to honor Path.binding rather than ignore it. The same pattern shows up in emit: nothing at the API boundary forces FieldProject to be realized through spec data rather than an inline Rust code path, and this diff already takes the inline path (emit_rust.rs:565-577). Those are convention-level rules, exactly the class the discipline says should be pushed into the data model now rather than after more consumers copy it. chatgpt-review-cd55bc36-f553-40…

Design question. Is Path.binding real substrate, or is it just carried metadata for a future renderer/interpreter? The code currently says the latter: lowering uses it, inference does not, and it remains on the DAG for “readable downstream rendering” (dag.rs:704-708). If it is real substrate, it needs a real consumer and a canonical contract now; if it is future metadata, it should not be landing in the substrate yet, because speculative boundary facts calcify fast and turn into parallel representations.

Verdict. REQUEST_CHANGES. The direction is better than the earlier attempts: this diff avoids synthesized accessor declarations, and I would not re-raise the old algebraic-form objection to FieldProject under the current thesis. But the previously-raised lowering-time typing blocker still remains, and new in this round the PR adds Path.binding as substrate data without an end-to-end authoritative consumer while also teaching emit_rust a new hardcoded realization path for projection. That is the wrong substrate pattern to normalize.

LOOP HEALTH: 📉 ACCUMULATING_DEBT — this round fixes some earlier shape mistakes, but it still lands new substrate semantics as tests-only metadata and grows a fresh emitter-side special case before the semantic boundary is settled.


View conversation

@briansrls

briansrls commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Meta-Review (Loop Health)

Loop summary

From the attached artifacts, I can verify 3 recorded review rounds against 1 visible diff snapshot: 1 codex-cli review and 2 chatgpt-browser entries, spanning about 33 minutes from 2026-04-15 22:17Z to 22:50Z. The supplied files do not expose the full PR commit count, so I cannot verify total commits beyond the single current diff state in pr-458.diff and the 3 recorded review events in ALL_REVIEWS.txt (ALL_REVIEWS.txt:1-46).

Forward progress evidence

This loop is not pure churn.

The current diff does enable real consumers:

  • emit_rust now has a concrete FieldProject render path instead of leaving the new target inert (pr-458.diff:79-114).
  • m1_substrate_test.rs adds real substrate consumers for prereq 1 and 2: field access lowering, multi-hop chaining, payload binding, payload-binding-plus-field-access, and fail-closed rejection cases (pr-458.diff:934-1195).

That matters. The roadmap explicitly says a type or substrate shape with zero consumers is a paper exercise, not structural proof (ROADMAP.md:1051-1080). This PR does clear that bar: it creates real tests and at least one real emit path.

Debt accumulation evidence

The loop is still shifting debt faster than it is paying it down.

First, the same root cause is being rediscovered instead of dissolved. The browser review says lowering resolves the concrete field_decl_id and then drops it, storing only field_label; inference re-derives the child by string and also loses generic substitution context. Codex independently flags the same issue: both prerequisites are resolving user-visible structure too early in lowering, duplicating type authority and calcifying the wrong phase boundary (ALL_REVIEWS.txt:8-18, ALL_REVIEWS.txt:34-41).

Second, the diff adds fresh scaffold / invalid-state surface area:

  • TransformTarget::FieldProject { parent_type, field_label }
  • Path.binding: Option<PayloadBinding>
  • SurfacePattern::VariantWith

Those shapes admit ambiguous or illegal combinations instead of making them unrepresentable (pr-458.diff:34-75, pr-458.diff:787-829). The browser review calls that out directly: Option<PayloadBinding> overloads None, and FieldProject admits inconsistent pairs that only inference rejects later (ALL_REVIEWS.txt:6-14).

Third, this recurring class has already been graduated in the project foundations, and the PR is still violating it. INVARIANTS.md now says that after lowering, DeclarationId is the only semantic authority: no name recovery, no parallel authority, no post-lowering string→identity bridge. That invariant was added specifically because an earlier meta-review kept finding the same disease across rounds and concluded the right response was PAUSE_AND_REGROUP (INVARIANTS.md:1722-1778). This PR reintroduces the class anyway.

Fourth, the scaffold accounting is incomplete. SurfacePattern is labeled as scaffold, but the browser review notes the new VariantWith addition still lacks a real dissolution trigger, and the invariant says scaffold variants need an explicit boundary/gate, not just a comment (ALL_REVIEWS.txt:10,24, INVARIANTS.md:1142-1188).

Cheating signal

Yes, but documented.

The implementer is not hiding compromises. FieldProject has a lifecycle receipt, SurfacePattern carries scaffold language, and the tests are honest about current behavior. That is better than quiet debt.

But the most recent fixes are still small-blast-radius fixes: add tests, add fail-closed diagnostics, add a render path, keep the wrong lower→infer authority boundary. That is rational triage under review pressure, not laziness. It is still good-enough-for-now fixing with accounting, not structural payoff.

Meta-verdict

🔁 PAUSE_AND_REGROUP

The direction is right, so revert is too strong. Real consumers were enabled, so this is not empty substrate growth. But the loop is now re-finding a known substrate bug class that the repo has already named structurally: post-lowering secondary authority / name recovery. Another round of local patching will add more tests and more surface area without settling the boundary.

Stop iterating on instances. Write the next move as a structural rule and representation change: field projection must carry resolved child identity plus substitution context, and payload binding must become a boundary shape that cannot encode illegal states. Until that happens, the review loop is not paying debt down; it is relocating it.


View conversation

@briansrls

Copy link
Copy Markdown
Contributor Author

Director review — #458 (Prereqs 1+2)

LGTM. Both prereqs implement the committed design answers from #455 correctly.

Prereq 1 (§3.6 1a): TransformTarget::FieldProject { field_label } — clean. Deliberately omits parent_type since the input port already carries it. 4-pattern check inline. lower_field_path_expr produces one Transform per segment, no synthesized accessors. Infer resolves through SubstStack with fail-closed diagnostics. Emit renders .field_label.

Prereq 2 (§3.7 2e): PayloadBinding on Path.binding, BranchPattern stays at 2 variants. Infer resolves payload type separately from pattern resolution — decoupled as designed. binding_name documented as carry-forward.

Anti-patterns: none. No synthesized Declarations, no BranchPattern growth, no substrate.dag, no new semantic string lookups.

Tests: 10 new + 1 emit unit test. Success, fail-closed (non-Conj, missing field, non-Disj, record variant rejection), generic SubstStack, and payload+field-access integration all covered.

Design doc: Two follow-up items tracked (FieldProject phase carrier, generic call dispatch collapse). Correctly scoped.

One minor gap: No test for empty path expression at the top of lower_field_path_expr. Likely parser-unreachable but could add a defensive test later.

No blocking issues.

@briansrls

Copy link
Copy Markdown
Contributor Author

ChatGPT review in progress... (view conversation)

Check back in ~30 minutes for the full review.

@briansrls
briansrls merged commit b672694 into main Apr 15, 2026
2 checks passed

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

codex · gpt-5.4 · 773849bb

⚠️ Review (blocking: 1, non-blocking: 2+/1- · loop: converging)

BLOCKING (1)

Root Cause

  • src/v3/compiler/src/infer.rs decide_field_project computes field_decl_id as an ephemeral local and only writes the output TypeShape -> make FieldProject a phase carrier for resolved child identity and have infer fill it once so emit and upcoming reflection lenses consume the same fact.

Non-blocking — Strengths

  • src/v3/compiler/tests/m1_substrate_test.rs The new tests cover both previously flagged inferred-scrutinee regressions, generic substitution, and fail-closed negative cases, which is the right consumer surface for substrate prerequisites.
  • docs/substrate-reflection-design.md The design doc now records the post-458 follow-ups explicitly, which keeps the reflection loop honest instead of pretending the substrate is already done.

Non-blocking — Improvements (fix in-PR if easy, else defer to roadmap)

  • src/v3/compiler/src/infer.rs Payload-binding diagnostics fall back to the whole match span once BranchPattern resolves, so either carry the arm span on the resolved form or run payload binding before pattern rewrite; roadmap lane if deferred: prereq2 polish before reflection.

⚠️ The prior defer-to-infer blockers are fixed, but FieldProject still needs to carry the resolved child fact forward before this substrate is safe to build reflection consumers on.

Loop health: 📈 CONVERGING — This round removes the two earlier lowering-time authority breaches and adds real consumer tests, so the loop is moving forward even though one narrower carry-forward issue remains.

/// ResolvedIdentifier edges, looks up `field_label` on the
/// reached Conj, and resolves the output through the same
/// substitution context. No synthesized accessor declaration.
FieldProject { field_label: String },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Invariant violation: BLOCKING: TransformTarget::FieldProject carries only field_label, so infer resolves the child declaration and then drops it, forcing downstream consumers to redo the projection walk instead of reading the resolved child fact structurally (Facts Flow Forward).

@briansrls

Copy link
Copy Markdown
Contributor Author

codex · gpt-5.4 · 773849bb

⚠️ Review (blocking: 1, non-blocking: 2+/1- · loop: converging)

BLOCKING (1)

Root Cause

  • src/v3/compiler/src/infer.rs decide_field_project computes field_decl_id as an ephemeral local and only writes the output TypeShape -> make FieldProject a phase carrier for resolved child identity and have infer fill it once so emit and upcoming reflection lenses consume the same fact.

Non-blocking — Strengths

  • src/v3/compiler/tests/m1_substrate_test.rs The new tests cover both previously flagged inferred-scrutinee regressions, generic substitution, and fail-closed negative cases, which is the right consumer surface for substrate prerequisites.
  • docs/substrate-reflection-design.md The design doc now records the post-458 follow-ups explicitly, which keeps the reflection loop honest instead of pretending the substrate is already done.

Non-blocking — Improvements (fix in-PR if easy, else defer to roadmap)

  • src/v3/compiler/src/infer.rs Payload-binding diagnostics fall back to the whole match span once BranchPattern resolves, so either carry the arm span on the resolved form or run payload binding before pattern rewrite; roadmap lane if deferred: prereq2 polish before reflection.

⚠️ The prior defer-to-infer blockers are fixed, but FieldProject still needs to carry the resolved child fact forward before this substrate is safe to build reflection consumers on.

Loop health: 📈 CONVERGING — This round removes the two earlier lowering-time authority breaches and adds real consumer tests, so the loop is moving forward even though one narrower carry-forward issue remains.

briansrls added a commit that referenced this pull request Apr 16, 2026
…tus + research framing

Three updates to v3's self-hosting design note:

- §2 L1 status: prereqs shipping (Prereqs 0-3 merged via #458/#460,
  Prereqs 4-5 in flight), reflection PR is next. Replaces stale
  "in design" label.
- §2.1 (new): the pipeline is a .dag composition, not a file
  convention. Stages are typed functions composed via let bindings
  with explicit dependencies. Stage contracts are first-class (not
  decorative), per-stage fixed-point is structural, independent
  stages parallelize, and the compiler is analyzable by its own
  lenses. Pipeline declaration ships FIRST (model before implement);
  each stage starts as ExternalRealization during migration.
- §5 (infer): explicitly framed as research, not engineering.
  I0 result cross-referenced (no decidability blocker). The open
  question narrows from "is this possible?" to "which write-surface
  option?"

Also updates src/v3/ROADMAP.md M3 section to cross-reference
SELF_HOSTING.md with key points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Apr 16, 2026
…rence research framing (#464)

* docs: update SELF_HOSTING.md with compositional pipeline + prereq status + research framing

Three updates to v3's self-hosting design note:

- §2 L1 status: prereqs shipping (Prereqs 0-3 merged via #458/#460,
  Prereqs 4-5 in flight), reflection PR is next. Replaces stale
  "in design" label.
- §2.1 (new): the pipeline is a .dag composition, not a file
  convention. Stages are typed functions composed via let bindings
  with explicit dependencies. Stage contracts are first-class (not
  decorative), per-stage fixed-point is structural, independent
  stages parallelize, and the compiler is analyzable by its own
  lenses. Pipeline declaration ships FIRST (model before implement);
  each stage starts as ExternalRealization during migration.
- §5 (infer): explicitly framed as research, not engineering.
  I0 result cross-referenced (no decidability blocker). The open
  question narrows from "is this possible?" to "which write-surface
  option?"

Also updates src/v3/ROADMAP.md M3 section to cross-reference
SELF_HOSTING.md with key points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: §2.2 model every stage's domain to spec before implementation

New section in SELF_HOSTING.md capturing the design principle:
every pipeline stage models its inputs and outputs to spec BEFORE
the stage body is implemented. Stage implementation is filling in
function bodies over already-declared types — the same model-
before-implement discipline the thesis demands of user programs,
applied to the compiler itself.

Per-stage domain modeling covers: parse (files, encodings, tokens,
grammar as data), lower (surface AST as proper .dag Disj
declarations with structural decompression), infer (rules as data
operations, substitution stacks and scope as structural types),
emit (full target language spec including rendering conventions).

Between-stage contracts are the pipeline's function signatures —
first-class, not decorative. A lens verifies each contract holds.

The v2 lesson this prevents: types accreted reactively, downstream
stages reconstructed facts instead of receiving them as typed
edges. The annotate_descent_evidence pattern (33 heuristics in
v2's complexity pass) is the canonical failure mode.

Practical sequencing: model review → pipeline slot → implementation
→ parity test. Step 1 is where the hard thinking happens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add L2.5 per-stage domain modeling to dependency order

Updates SELF_HOSTING.md §2 dependency diagram and gating rules:

- L2.5 (per-stage domain modeling) runs in parallel with L2 (lens
  migrations). Both start after L1 (reflection). L2 writes lenses
  in dsl/lenses/; L2.5 declares pipeline-domain types in std/ and
  extdeps/. Independent work streams, no file overlap.
- L2.5 stages proceed in L3's implementation order: emit domain
  first (partially exists), lower domain second, parse domain
  third, infer domain last (blocked on I3 write-surface experiment).
  Each domain model runs ahead of its stage's implementation.
- New gating rule: L3 stage N cannot start until L2.5's model for
  stage N is reviewed. Implementation fills in function bodies
  over already-declared types.
- Four-step sequencing table: model review → pipeline slot →
  implementation → parity test.
- v3 ROADMAP.md M3 section updated with L2.5 in the dependency
  chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: address 3 review blockers on SELF_HOSTING.md §2.1/§2.2

Three fixes from codex review on #464:

1. §2.1 v2 contrast reframed. v2's compile.dag IS a .dag
   composition — the gap is decorative metadata with no consumers
   + no structural self-analysis + stage boundaries as convention
   not enforcement. Removed the claim that v2 "lacks a declared
   pipeline graph."

2. §2.1 typed boundary restated as lens-checked property. The
   infer output guarantee (all ports Resolved) is lens-enforced,
   not type-level — there is no InferredDag wrapper type. The doc
   now says "lens-checked property" explicitly and notes a future
   wrapper type as a stronger possible guarantee, not a current
   promise.

3. §2.2 substrate-vs-implementation split added. Walker-local
   state (SubstStack, Scope, UnificationState, fold locals) stays
   in stage .dag bodies, NOT in std/. Only cross-stage and
   downstream-consumed types go in std/. Rule: "if it crosses a
   boundary or a lens reads it, std/. If it's internal to one
   stage's walk, implementation." Table enumerates both categories.
   Infer domain bullet updated to match. Principle paragraph
   updated to say "at boundaries" not "universally."

Also fixes stale Prereq 5 status (merged, not in flight).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: §2.3 language-agnostic compiler core + emission layering

New section in SELF_HOSTING.md capturing three architectural
commitments for omni-emission/ingestion:

§2.3 — The compiler knows only substrate primitives (six
connectives + five behaviors). It does NOT know what language it
parses or emits. .dag is a GrammarSpec. Rust is a LanguageSpec.
Both are data. The compiler is parametric on both input and output
language.

§2.3.1 — Emission/ingestion cost model. Any medium that can
represent the substrate's primitives can be a target. Cost is
structural: O(|substrate| × k^depth) where k is the expansion
factor per nesting level. Well-matched targets (k ≈ 1) are cheap;
poorly-matched targets (k > 1) grow exponentially with depth.
Ingestion adds an information-deficit dimension — how much
structural information the source language doesn't carry.

§2.3.2 — Emission layering: spec vs preferences. Three layers
with compositional opacity: Layer 1 (language specification —
facts from the target's own spec), Layer 2 (representation
mapping — substrate to target constructs), Layer 3 (style/lint/
idiom preferences — composable ON TOP of the spec). Quality of
emitted code is a typed, composable property. The same layering
applies to ingestion (grammar facts vs parsing preferences).

§2.3.3 — Four design challenges: GrammarSpec formalism,
LanguageSpec completeness, bootstrap seed size, per-language
spec authoring cost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: resolve single-authority blocker — pipeline supersedes bootstrap.dag

Two fixes addressing the remaining review blockers on #464:

1. ROADMAP Track 17b + Phase 4: bootstrap.dag's stage vocabulary
   (CompilerStage, TransformContract, ChangeClassification,
   BootstrapStrategy) is explicitly superseded by the pipeline
   composition from SELF_HOSTING.md §2.1. regen.dag reads the
   pipeline's typed stage signatures, not a separate bootstrap.dag
   type set. Phase 4 Level 1-2 updated to reference pipeline
   signatures and lens-checked contracts instead of bootstrap.dag's
   TransformContract vocabulary. Resolves the "two authorities for
   stage metadata" finding both reviewers flagged.

2. SELF_HOSTING.md §2 gating rule 2: explicit distinction between
   L2.5 and the decorative-modeling pattern Track 17 warns about.
   Each L2.5 type has a named consumer: the L3 stage implementation
   that reads it. The test is: "which function body in L3 reads
   this type?" If you can't name one, don't declare it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix §2.3 Shape A/B boundary + add ElaborationSpec + reframe StyleSpec

Three fixes addressing ChatGPT review findings on #464:

1. Shape A / Shape B boundary respected. §2.3 now explicitly
   scopes to Shape A targets only. English, YAML, SPICE, and
   Terraform are never EmissionTarget values — they are Shape B
   outputs of .dag user programs. The thesis's category-error
   warning (§"Omni-emission") is referenced directly. The cost
   model in §2.3.1 keeps the expansion-factor analysis but
   separates Shape A cost (in LanguageSpec, compiler-owned) from
   Shape B cost (in user program type specs, user-owned).

2. GrammarSpec + ElaborationSpec split. The pipeline signature
   grows an elaboration parameter: grammar owns syntax (tokens →
   surface tree), elaboration owns semantics (surface → substrate).
   This matches MODELING.md's four-layer stack (surface sugar /
   composition / semantic kernel / foundation). Collapsing both
   into GrammarSpec would leave elaboration as implicit compiler
   behavior. New design challenge §2.3.3 #2 names the
   ElaborationSpec design question.

3. StyleSpec reframed as RenderingSpec — declared rendering
   conventions with structural authority. NOT "preferences" or
   "annotations." When present, they are hard constraints on the
   emitter's output formatting. "Optional" means the emitter CAN
   run without them, not that they are soft suggestions. Aligns
   with MODELING.md ("models are shared facts, not preferences")
   and INVARIANTS.md ("no annotation mechanisms through M3").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Prereq 0.5 (implicit generic instantiation) + §4.4 (lens invocation)

Two additions to substrate-reflection-design.md surfaced by the
swift-ram-158 reflection implementer:

Prereq 0.5 — Implicit generic-call instantiation. Prereq 0 lands
explicit template binding (apply(3, negate)). Prereq 0.5 extends
to implicit binding: when fold(singleton(1), 0, |acc, x| acc + x)
is called, inference synthesizes T := Int, U := Int from argument
types without explicit template arguments. This is the gap the
*_int monomorphized wrappers in list.dag exist to bridge. Standard
HM-style inference scoped to call sites, no new substrate variants.
Blocks: lens migration (Unit 3), list.dag wrapper dissolution,
compositional generic list operations.

§4.4 — Lens invocation mechanism. Two paths:
- Path A (compiled Rust integration) for parity tests — emitted
  Rust function linked into test binary, called with a Dag value,
  output compared byte-identical to Rust lens. Blocked on
  Prereq 0.5.
- Path C (interpreter) for development and self-analysis — dag run
  evaluates the lens on a Dag value. No Rust compilation needed.
  Available earlier.
- Path B (subprocess) rejected — awkward, slow, loses type safety.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: narrow RenderingSpec to visual-only + fix bootstrap.dag dissolution scope

Two fixes from review feedback on #464:

1. RenderingSpec narrowed to strictly lexical/visual concerns.
   Removed Result<T,E> vs panic! (semantic — belongs in
   LanguageSpec), removed "idioms" field (semantic), removed
   ambiguity resolution from ingestion conventions (semantic —
   belongs in GrammarSpec or ElaborationSpec). Added explicit
   "MUST NOT change observable semantics" rule with test: if two
   RenderingSpec values produce programs with different runtime
   behavior, one contains a semantic choice that should be in
   LanguageSpec.

2. ROADMAP Track 17b authority note narrowed. Stage-contract
   vocabulary (CompilerStage, TransformContract, StageInput,
   StageOutput, FieldPropagation) is superseded by the pipeline.
   Schema-migration vocabulary (ChangeClassification, ChangeKind,
   BootstrapStrategy, FixedPointCheck) is NOT superseded — these
   remain as load-bearing types for SELF_HOSTING.md §10's patch
   pipeline until the schema-migration PR lands their first real
   consumer. Resolves the single-authority contradiction between
   §2.1 (pipeline supersedes) and §10 (schema-migration types
   are load-bearing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Apr 16, 2026
…igration (#466)

* docs: update SELF_HOSTING.md with compositional pipeline + prereq status + research framing

Three updates to v3's self-hosting design note:

- §2 L1 status: prereqs shipping (Prereqs 0-3 merged via #458/#460,
  Prereqs 4-5 in flight), reflection PR is next. Replaces stale
  "in design" label.
- §2.1 (new): the pipeline is a .dag composition, not a file
  convention. Stages are typed functions composed via let bindings
  with explicit dependencies. Stage contracts are first-class (not
  decorative), per-stage fixed-point is structural, independent
  stages parallelize, and the compiler is analyzable by its own
  lenses. Pipeline declaration ships FIRST (model before implement);
  each stage starts as ExternalRealization during migration.
- §5 (infer): explicitly framed as research, not engineering.
  I0 result cross-referenced (no decidability blocker). The open
  question narrows from "is this possible?" to "which write-surface
  option?"

Also updates src/v3/ROADMAP.md M3 section to cross-reference
SELF_HOSTING.md with key points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: §2.2 model every stage's domain to spec before implementation

New section in SELF_HOSTING.md capturing the design principle:
every pipeline stage models its inputs and outputs to spec BEFORE
the stage body is implemented. Stage implementation is filling in
function bodies over already-declared types — the same model-
before-implement discipline the thesis demands of user programs,
applied to the compiler itself.

Per-stage domain modeling covers: parse (files, encodings, tokens,
grammar as data), lower (surface AST as proper .dag Disj
declarations with structural decompression), infer (rules as data
operations, substitution stacks and scope as structural types),
emit (full target language spec including rendering conventions).

Between-stage contracts are the pipeline's function signatures —
first-class, not decorative. A lens verifies each contract holds.

The v2 lesson this prevents: types accreted reactively, downstream
stages reconstructed facts instead of receiving them as typed
edges. The annotate_descent_evidence pattern (33 heuristics in
v2's complexity pass) is the canonical failure mode.

Practical sequencing: model review → pipeline slot → implementation
→ parity test. Step 1 is where the hard thinking happens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add L2.5 per-stage domain modeling to dependency order

Updates SELF_HOSTING.md §2 dependency diagram and gating rules:

- L2.5 (per-stage domain modeling) runs in parallel with L2 (lens
  migrations). Both start after L1 (reflection). L2 writes lenses
  in dsl/lenses/; L2.5 declares pipeline-domain types in std/ and
  extdeps/. Independent work streams, no file overlap.
- L2.5 stages proceed in L3's implementation order: emit domain
  first (partially exists), lower domain second, parse domain
  third, infer domain last (blocked on I3 write-surface experiment).
  Each domain model runs ahead of its stage's implementation.
- New gating rule: L3 stage N cannot start until L2.5's model for
  stage N is reviewed. Implementation fills in function bodies
  over already-declared types.
- Four-step sequencing table: model review → pipeline slot →
  implementation → parity test.
- v3 ROADMAP.md M3 section updated with L2.5 in the dependency
  chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: address 3 review blockers on SELF_HOSTING.md §2.1/§2.2

Three fixes from codex review on #464:

1. §2.1 v2 contrast reframed. v2's compile.dag IS a .dag
   composition — the gap is decorative metadata with no consumers
   + no structural self-analysis + stage boundaries as convention
   not enforcement. Removed the claim that v2 "lacks a declared
   pipeline graph."

2. §2.1 typed boundary restated as lens-checked property. The
   infer output guarantee (all ports Resolved) is lens-enforced,
   not type-level — there is no InferredDag wrapper type. The doc
   now says "lens-checked property" explicitly and notes a future
   wrapper type as a stronger possible guarantee, not a current
   promise.

3. §2.2 substrate-vs-implementation split added. Walker-local
   state (SubstStack, Scope, UnificationState, fold locals) stays
   in stage .dag bodies, NOT in std/. Only cross-stage and
   downstream-consumed types go in std/. Rule: "if it crosses a
   boundary or a lens reads it, std/. If it's internal to one
   stage's walk, implementation." Table enumerates both categories.
   Infer domain bullet updated to match. Principle paragraph
   updated to say "at boundaries" not "universally."

Also fixes stale Prereq 5 status (merged, not in flight).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: §2.3 language-agnostic compiler core + emission layering

New section in SELF_HOSTING.md capturing three architectural
commitments for omni-emission/ingestion:

§2.3 — The compiler knows only substrate primitives (six
connectives + five behaviors). It does NOT know what language it
parses or emits. .dag is a GrammarSpec. Rust is a LanguageSpec.
Both are data. The compiler is parametric on both input and output
language.

§2.3.1 — Emission/ingestion cost model. Any medium that can
represent the substrate's primitives can be a target. Cost is
structural: O(|substrate| × k^depth) where k is the expansion
factor per nesting level. Well-matched targets (k ≈ 1) are cheap;
poorly-matched targets (k > 1) grow exponentially with depth.
Ingestion adds an information-deficit dimension — how much
structural information the source language doesn't carry.

§2.3.2 — Emission layering: spec vs preferences. Three layers
with compositional opacity: Layer 1 (language specification —
facts from the target's own spec), Layer 2 (representation
mapping — substrate to target constructs), Layer 3 (style/lint/
idiom preferences — composable ON TOP of the spec). Quality of
emitted code is a typed, composable property. The same layering
applies to ingestion (grammar facts vs parsing preferences).

§2.3.3 — Four design challenges: GrammarSpec formalism,
LanguageSpec completeness, bootstrap seed size, per-language
spec authoring cost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: resolve single-authority blocker — pipeline supersedes bootstrap.dag

Two fixes addressing the remaining review blockers on #464:

1. ROADMAP Track 17b + Phase 4: bootstrap.dag's stage vocabulary
   (CompilerStage, TransformContract, ChangeClassification,
   BootstrapStrategy) is explicitly superseded by the pipeline
   composition from SELF_HOSTING.md §2.1. regen.dag reads the
   pipeline's typed stage signatures, not a separate bootstrap.dag
   type set. Phase 4 Level 1-2 updated to reference pipeline
   signatures and lens-checked contracts instead of bootstrap.dag's
   TransformContract vocabulary. Resolves the "two authorities for
   stage metadata" finding both reviewers flagged.

2. SELF_HOSTING.md §2 gating rule 2: explicit distinction between
   L2.5 and the decorative-modeling pattern Track 17 warns about.
   Each L2.5 type has a named consumer: the L3 stage implementation
   that reads it. The test is: "which function body in L3 reads
   this type?" If you can't name one, don't declare it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: fix §2.3 Shape A/B boundary + add ElaborationSpec + reframe StyleSpec

Three fixes addressing ChatGPT review findings on #464:

1. Shape A / Shape B boundary respected. §2.3 now explicitly
   scopes to Shape A targets only. English, YAML, SPICE, and
   Terraform are never EmissionTarget values — they are Shape B
   outputs of .dag user programs. The thesis's category-error
   warning (§"Omni-emission") is referenced directly. The cost
   model in §2.3.1 keeps the expansion-factor analysis but
   separates Shape A cost (in LanguageSpec, compiler-owned) from
   Shape B cost (in user program type specs, user-owned).

2. GrammarSpec + ElaborationSpec split. The pipeline signature
   grows an elaboration parameter: grammar owns syntax (tokens →
   surface tree), elaboration owns semantics (surface → substrate).
   This matches MODELING.md's four-layer stack (surface sugar /
   composition / semantic kernel / foundation). Collapsing both
   into GrammarSpec would leave elaboration as implicit compiler
   behavior. New design challenge §2.3.3 #2 names the
   ElaborationSpec design question.

3. StyleSpec reframed as RenderingSpec — declared rendering
   conventions with structural authority. NOT "preferences" or
   "annotations." When present, they are hard constraints on the
   emitter's output formatting. "Optional" means the emitter CAN
   run without them, not that they are soft suggestions. Aligns
   with MODELING.md ("models are shared facts, not preferences")
   and INVARIANTS.md ("no annotation mechanisms through M3").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Prereq 0.5 (implicit generic instantiation) + §4.4 (lens invocation)

Two additions to substrate-reflection-design.md surfaced by the
swift-ram-158 reflection implementer:

Prereq 0.5 — Implicit generic-call instantiation. Prereq 0 lands
explicit template binding (apply(3, negate)). Prereq 0.5 extends
to implicit binding: when fold(singleton(1), 0, |acc, x| acc + x)
is called, inference synthesizes T := Int, U := Int from argument
types without explicit template arguments. This is the gap the
*_int monomorphized wrappers in list.dag exist to bridge. Standard
HM-style inference scoped to call sites, no new substrate variants.
Blocks: lens migration (Unit 3), list.dag wrapper dissolution,
compositional generic list operations.

§4.4 — Lens invocation mechanism. Two paths:
- Path A (compiled Rust integration) for parity tests — emitted
  Rust function linked into test binary, called with a Dag value,
  output compared byte-identical to Rust lens. Blocked on
  Prereq 0.5.
- Path C (interpreter) for development and self-analysis — dag run
  evaluates the lens on a Dag value. No Rust compilation needed.
  Available earlier.
- Path B (subprocess) rejected — awkward, slow, loses type safety.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Advance reflected substrate and runtime emission

* Advance reflected runtime carrier and address review feedback

* Extend payload binding and structural recursion checks

* Make v3 List structural during bootstrap

* Canonicalize specialized generic signature shapes

* Advance reflected lens execution path

* Migrate unused-parameters lens to emitted shim

* Add freshness gate for emitted lens snapshot

* Tighten reflected realization validation

* Drop dead syntax binding fields

* Fix v3 clippy regressions

* Fix remaining v3 clippy targets

* Finish remaining lens migration review fixes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@briansrls
briansrls deleted the keen-ram-105 branch June 1, 2026 18:41
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.

1 participant