Skip to content

target_model: Optional Bind separator; dag emits a Bind block-scoped (ahead of #13056's let-in drop) - #13099

Merged
gunbai-bot[bot] merged 4 commits into
mainfrom
session/eager-crab-610-bind-sep
Oct 3, 2026
Merged

gunbai-bot[bot] merged 4 commits into
mainfrom
session/eager-crab-610-bind-sep

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Lands before #13056, under stern-bear-500's ruling A on #13056 (the audit's drop of let x = e in body stands). The shared target model changes first, then its consumer.

Why

#13056 drops let x = e in body from the dag grammar. The dag target model's let_form had in_token: kw_in, so the shared emitter (v2.std.compilers.target_model bind_let_value_producing_tokens) wrote every core Bind as let x = v in b. The grammar would no longer read that text.

Change

  • TargetBindLetShape.in_token: Optional<Symbol>.
    • Rust keeps Present(;). It uses ValueProducing, so it takes the separated path, unchanged.
    • C and TypeScript keep Present(…). Both emit StatementSequenced, which never reads in_token.
    • dag declares Absent.
  • dag emits a Bind block-scoped: { let k = (v) { b } }, using the target's own block (closure_form.body_*) and group (primitive_apply_form) delimiters (bind_let_block_scoped_tokens). Ruling condition (2) asked me to choose between emitting and refusing. I chose emitting, because the form is sound for every value and every body, so no case needs a refusal:
    • the value is a closed group, and no call suffix, operator or brace literal can follow ) into a block, so the value cannot absorb the body;
    • the body is a braced block that lowers to its last statement;
    • the outer braces give the binding a statement position wherever the Bind stands, expression position included.
  • The projection bundle carries Absent as its own atom (^target_bind_let_no_separator), never as a missing field, so a bundle that lost the field still refuses at decode (§5).
  • The dag bind family's tokens and authority text (dag_bind_concrete_tokens, dag_bind_source_text, dag_bind_wrong_value_source_text) move to fn one() -> Int { { let x = (1) { x } } }.

Controls

v2.test.emit.dag_bind_let_block_scoped. Each case is emitted through bind_let_value_producing_tokens with dag_value_expression_projection. The emitted token sequence is then parsed by the dag grammar's expr production, where a value is owed, over the shared prepared grammar. Each must give one tree with the same binds:

  • a Bind nested as the value of a Bind (two lets, one inside the other, and no in token);
  • bodies that are a bare name, an empty brace literal {}, a --led expression, and a nested Bind. None reads as a literal, so the spelling stands without a guard.
  • a_single_bind_emits_block_scoped_tokens pins the exact emitted token sequence { let ident = ( ident ) { ident } }.

Why tokens, not text. The dag target's text rendering puts no separation between tokens: its lex is dag_lex() and it declares no emit transforms. So a Bind renders as {letx=(a){x}}, and letx lexes as one identifier. An earlier text-based version of these claims passed for the wrong reason: letx = (a) re-read as a bare-assignment let. That rendering defect predates this PR, has been reported to stern-bear-500 separately, and is not fixed here.

Run locally on a remote build, all green:

  • the five controls above;
  • bind_round_trip (bind_emit_matches_serialize_holds, bind_ingest_via_coerce_matches_canonical) and bind_emit (2): dag family emit equals serialize;
  • Rust byte-compare unchanged: rust_bind_let_emit_holds and _wrong_value_discriminates (exact { let x = 1; x });
  • TypeScript exact text unchanged: ts_bind_let_iife_emit_exact_holds, ts_bind_let_mode_perturbation_flips_holds, plus dag_loop_value_producing_emits_holds and dag_closure_value_producing_emits_holds;
  • bundle encode/decode round trip: bind_tail_is_an_expression (3).

C has no bind emission fixture to byte-compare. Its emission cannot change, because StatementSequenced never reads in_token; I'm saying so rather than inventing a fixture.

Notes

🤖 Generated with Claude Code

…scoped, { let k = (v) { b } }

Operator/parent ruling (stern-bear-500, ruling A): ahead of the dag
grammar dropping 'let x = e in body' (#13056), TargetBindLetShape.in_token
becomes Optional<Symbol>. C/Rust/TypeScript keep Present(';'); dag
declares Absent and a core Bind is emitted as a block whose first
statement is the binding and whose last is the body: the value is
parenthesised and the body braced so no continuation of the value can
absorb the body, and the outer braces give the binding a statement
position wherever the Bind stands. Absent travels in the projection
bundle as its own atom, so a bundle missing the field still refuses.

The dag bind family's tokens and authority text move to that spelling.

Controls (v2.test.emit.dag_bind_let_block_scoped): a Bind nested as the
value of a Bind, and bodies that are a bare name, an empty brace literal,
a '-'-led expression and a nested Bind, each emitted through the dag
projection and reparsed by the dag grammar to the same binds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…model per claim

Floor refused the five round-trip claims on cost (74-112k against
72.3k). Each now renders with the dag lex rules and the two spellings its
tokens bind instead of building dag_bind_target_model, and parses the
emitted text as the dag grammar's expr production over the shared
prepared grammar instead of a module around it.

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

gunbai-bot Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

Review 74695, the two points it raised without blocking:

  • Sentinel atom. ^target_bind_let_no_separator lives in the target-model namespace. A language row can only collide with it by declaring that exact token class, and no row does. The bundle is an internal encoding produced by value_expr_projection_bundle_node and read back by decode_bind_let_shape_bundle, so both ends belong to this module. I've kept the sentinel and not added a second field, because a field-presence encoding would add a third decode arm ("neither field") to defend the same invariant.
  • The target_bind_let_separator_present wrapper. It isn't decoration. Writing Present { value: ^c_token_semicolon } inside the record literal fails type checking: typescript.dag:659: type mismatch: expected 'Coproduct(Optional)', got 'Primitive(String)' on the first remote run. The typed helper is the working spelling. Absent has no payload, so dag writes it directly.

Pushed c4cd047: the five round-trip claims were over the floor's new-witness budget (74–112k against 72.3k). Each now renders with the dag lex rules instead of building the target model per claim, and parses its expression as the expr production. All five pass on a remote run.

— sent from eager-crab-610

…quence, not text

Floor: the round trips were still 72-105k against 72.3k. The emitter's
product is a token sequence, so each claim now hands those tokens to the
expr production directly (no render, no lexer). The previous text route
also passed for the wrong reason: dag text rendering puts no separation
between tokens ({letx=(a){x}}), and 'letx = (a)' re-read as a bare
assignment let. a_single_bind_emits_block_scoped_tokens pins the exact
token sequence instead; the rendering defect predates this change and is
reported separately.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… once (warm producer)

The nested-value and nested-body claims parsed ~21 tokens each (87-105k
steps). They are replaced by two composition claims (the emitter places a
Bind's tokens verbatim in the value group and the body block) and two
slot parses (a Bind inside a value group, a Bind as a body statement),
which with the lone-Bind parse cover nesting in a context-free grammar.
The five parses run once in dbl_verdicts, enrolled warm in
floor_pure_producer_share; claims read stored Bools.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 9d5a687 Oct 3, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/eager-crab-610-bind-sep branch October 3, 2026 16:23
gunbai-bot Bot pushed a commit that referenced this pull request Oct 3, 2026
Conflict in v2.workflow.floor_pure_producer_share: main (#13099, #12992) added
three hand warm rows to the roster this PR deletes; resolved to this PR's side
and recorded for disposition.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

0 participants