Skip to content

P6.x: resolve Ok/Err/Some/None's IrExprKind identity gap (#1225) - #1227

Merged
accuser merged 2 commits into
mainfrom
p6-option-result-variant-adr
Aug 17, 2026
Merged

P6.x: resolve Ok/Err/Some/None's IrExprKind identity gap (#1225)#1227
accuser merged 2 commits into
mainfrom
p6-option-result-variant-adr

Conversation

@accuser

@accuser accuser commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #1225 — the Ok/Err/Some/None IrExprKind identity gap, open since P6.2/P6.3 (#1143/#1145).

The gap. IrExprKind::Variant { sum: Arc<TypeDecl>, tag: String, payload: Vec<IrExpr> } carried a declaration-identity field mirroring Record::def/GlobalRef::sum. A real user-declared sum's constructor (Circle(n), driven by Callee::Ctor { sum: Arc<TypeDecl>, tag }) has one to give it. Ok/Err/Some/None never do — Option/Result are dedicated Ty variants (Ty::Option/Ty::Result), never Ty::Named, never backed by a TypeDecl. Worse: they aren't even calls — dedicated ExprKind::Ok/Err/Some/None AST variants, checked through check_ok/check_err/check_some/check_none with no Callee ever recorded at all.

The ADR (design/pending/p6-option-result-variant-identity.md): drop sum rather than widen it. IrExpr::ty — already present on every node (R6.1) — already carries the identical identity as a TyId: a constructor call's own checked type is the sum it constructs, and lower_call_ir already computes ty as its first line, before ever branching into the Callee::Ctor arm. This mirrors IrPat::Variant's own scrutinee_ty: TyId precedent exactly — the one place this module already solved the identical problem, resolved via bynk_check::checker::variants_of (already pub for this), proven to cover a user sum, Result, Option, ActorSum, and HttpResult uniformly with no built-in special-casing gap.

Implementation. Variant becomes { tag: String, payload: Vec<IrExpr> }. Ok/Err/Some/None now lower to it directly (tag the constructor name, payload the lowered inner expression, empty for None), same as a real Callee::Ctor construction. Closes the todo!() ir/lower.rs has carried since P6.2/P6.3.

Verified against the real regression this closes: 223_store_cell_agent's own store paymentRef: Cell[Option[AuthId]] = None no longer panics through the full, init-lowering lower_store_field_ir — previously worked around, not fixed, by lower_store_field_shape_ir's own never-lower-init posture (#1187's Agent state-field slice 2a, #1206).

No shipped emitter consumer of this construction path exists yet — ir::lower's whole IrItem/IrExpr output remains dormant, exercised only by its own test suite — so this lands with zero behavioural change to any emitted TypeScript, confirmed by a zero-diff bless run.

Question's own separate todo!() (its three-way ?-desugar fork) is unaffected and named again as still open — a distinct design question this ADR does not settle, per #1225's own scope note.

Test plan

  • cargo build --workspace
  • cargo clippy --workspace --all-targets
  • cargo fmt --all -- --check
  • cargo test -p bynk-emit --lib — all 220 tests pass, including new ok_err_some_none_all_lower_to_variant_by_tag and store_field_cell_option_init_none_lowers_without_panicking, and the updated bare_and_qualified_variant_construction_both_lower_to_variant
  • BYNK_BLESS=1 cargo test -p bynkc --test e2e bless_positive_fixtures — zero fixture diff (no shipped consumer of this path exists yet)
  • cargo test -p bynkc --test tsc_verify — real tsc --strict, all 6 pass
  • cargo test --workspace — full suite

🤖 Generated with Claude Code

@github-actions
github-actions Bot marked this pull request as ready for review August 17, 2026 06:59
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Review

The core reasoning holds up under checking. variants_of (bynk-check/src/checker.rs:4326) resolves Ty::Result to tags Ok/Err and Ty::Option to Some/None — byte-identical to the tags this lowering now emits — so dropping sum in favour of the wrapping IrExpr::ty genuinely loses no information, and the IrPat::Variant / scrutinee_ty precedent is a fair one. ty is computed in lower_expr_ir (bynk-emit/src/ir/lower.rs:1912) before the match, ahead of every new arm, and check_ok/check_err/check_some/check_none each return a concrete Option/Result/HttpResult or push a diagnostic, so in a certified unit the identity is always there to read. The Callee::Ctor destructure with .. is safe — that arm returns immediately, nothing else reads callee.

Release discipline: exactly one design/pending/ increment, with level and changelog. patch is right for a pub(crate) enum with no shipped consumer.

Three findings, none blocking. (The inline-comment API is not writable from this environment, so they are collected here with file:line.)


1. bynk-emit/src/ir/lower.rs:3691-3704 — the new test does not exercise the claim the ADR rests on.

The whole resolution is that Variant needs no sum field because the wrapping IrExpr::ty already carries the constructed sum identity. ok_err_some_none_all_lower_to_variant_by_tag asserts tag and payload.len() only — it would pass unchanged if tail.ty came back as Ty::Unit, or as the inner type rather than the Option/Result it must be. The sibling bare_and_qualified_variant_construction_both_lower_to_variant was correctly updated to assert tail.ty resolves to Shape; this one should match it:

matches!(&*program.program().ty_intern.get(tail.ty), Ty::Result(..) | Ty::Option(_))

ideally per case (Result for ok_case/err_case, Option for some_case/none_case). As written, the half that was never in doubt is pinned and the half the ADR depends on is not.

Related, same test: Ok is overloaded and the overload is uncovered. check_ok (bynk-check/src/checker/expressions.rs:1654-1690) resolves Ok to either Result.Ok or HttpResult.Ok, returning Ty::HttpResult(t) when the surrounding return peels to one — so ExprKind::Ok in an http handler reaches the new arm with ty = Ty::HttpResult(T), a third sum the identity claim has to carry, named neither in the tests nor in the new doc comments. It does hold: variants_of has a Ty::HttpResult arm whose HTTP_VARIANTS list (bynk-syntax/src/ast.rs:1319) carries its own Ok variant, so tag plus ty still resolve consistently. That is the argument for pinning it rather than leaving it implicit — a fifth case, a handler returning HttpResult[Int] with an Ok(1) tail, asserting tag Ok and ty is Ty::HttpResult, plus a line in the IrExprKind::Variant doc comment noting the overload.

2. bynk-emit/src/ir/lower.rs:1062-1076 — doc comment left stale by this change.

lower_store_field_shape_ir still documents itself as never lowering init because a Cell initialiser can be ExprKind::None or an is-expression, both of which hit the built-in-constructor gap, naming 223_store_cell_agent and 1029_agent_static_init_hoist as the two fixtures that do. The None half is now false — this PR adds a test proving lower_store_field_ir lowers exactly the 223_store_cell_agent shape without panicking. Only the is half survives (ExprKind::Is, still todo!() at lower.rs:2255; 1029 is store active: Cell[Bool] = if true ... 5 is PositiveInt). This matters more than ordinary doc drift because the ADR Consequences section points readers at this very comment as the record of the workaround.

3. bynk-emit/src/ir/lower.rs:480-497 — second stale rationale.

The service-handler signature-only helper justifies not building a real IrHandler on the grounds that an ordinary from http body routinely constructs Ok/Err/Some/None, still todo!() in lower_expr_ir. That grounding is now gone. The conclusion very likely still stands via the remaining gaps — ExprKind::Question (lower.rs:2195) is ubiquitous in http handlers, and ExprKind::Is (lower.rs:2255) — but as written a future reader will conclude the helper is now removable and be wrong. Worth re-pointing at whichever todo!() still blocks it.


Otherwise this reads clean: no remaining IrExprKind::Variant consumer carries sum, the None payload is empty exactly as variants_of reports it, and the zero-diff bless run is consistent with the path being dormant.

accuser added a commit that referenced this pull request Aug 17, 2026
- The new test asserted tag/payload only, which would pass unchanged even
  if tail.ty carried the wrong identity (or none) -- the actual claim the
  ADR rests on. Renamed and extended
  ok_err_some_none_all_lower_to_variant_by_tag_and_carry_their_sum_identity_on_ty
  to assert tail.ty resolves to the constructed sum in every case, and
  added a fifth case (ok_http_case) pinning the Ok/Result-vs-HttpResult
  overload check_ok resolves by peeling the surrounding return type --
  previously named in neither the tests nor the new doc comments.

- Swept the file for every other doc comment citing the now-closed gap,
  not just the two the review named:
  lower_store_field_shape_ir's own comment (only the is-expression half of
  its rationale survives; the None half is resolved and now pinned by a
  real fixture-shaped test), lower_service_handler_signature_ir's comment
  (re-pointed at the still-open Question/Is gaps instead), and three more
  the review didn't flag: lower_provider_given_ir's comment (same
  re-pointing, phrased conservatively since Provider op bodies specifically
  aren't verified to hit Question/Is), and two test doc comments
  (http_service_fixture, sum_actor_binder_lowers_an_actor_sum) whose "works
  around a pre-existing gap" framing was stale.

- Found by direct verification, not just doc review: the
  service_handler_signature test's own claim that "a real IrHandler would
  panic on this exact body" is no longer true -- that specific body
  (Effect.pure(Ok("pong"))) no longer panics lower_service_handler_ir now
  that Ok/Err/Some/None resolve. Corrected the doc comment to say so
  rather than chase a new body shape that still panics; the test's own
  broader claim (a real IrHandler is still unsafe to build unconditionally
  at this call site) still holds via Question/Is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
accuser added a commit that referenced this pull request Aug 17, 2026
- The new test asserted tag/payload only, which would pass unchanged even
  if tail.ty carried the wrong identity (or none) -- the actual claim the
  ADR rests on. Renamed and extended
  ok_err_some_none_all_lower_to_variant_by_tag_and_carry_their_sum_identity_on_ty
  to assert tail.ty resolves to the constructed sum in every case, and
  added a fifth case (ok_http_case) pinning the Ok/Result-vs-HttpResult
  overload check_ok resolves by peeling the surrounding return type --
  previously named in neither the tests nor the new doc comments.

- Swept the file for every other doc comment citing the now-closed gap,
  not just the two the review named:
  lower_store_field_shape_ir's own comment (only the is-expression half of
  its rationale survives; the None half is resolved and now pinned by a
  real fixture-shaped test), lower_service_handler_signature_ir's comment
  (re-pointed at the still-open Question/Is gaps instead), and three more
  the review didn't flag: lower_provider_given_ir's comment (same
  re-pointing, phrased conservatively since Provider op bodies specifically
  aren't verified to hit Question/Is), and two test doc comments
  (http_service_fixture, sum_actor_binder_lowers_an_actor_sum) whose "works
  around a pre-existing gap" framing was stale.

- Found by direct verification, not just doc review: the
  service_handler_signature test's own claim that "a real IrHandler would
  panic on this exact body" is no longer true -- that specific body
  (Effect.pure(Ok("pong"))) no longer panics lower_service_handler_ir now
  that Ok/Err/Some/None resolve. Corrected the doc comment to say so
  rather than chase a new body shape that still panics; the test's own
  broader claim (a real IrHandler is still unsafe to build unconditionally
  at this call site) still holds via Question/Is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@accuser
accuser force-pushed the p6-option-result-variant-adr branch from 919e3e2 to 8c006d2 Compare August 17, 2026 07:55
accuser added a commit that referenced this pull request Aug 17, 2026
- The new test asserted tag/payload only, which would pass unchanged even
  if tail.ty carried the wrong identity (or none) -- the actual claim the
  ADR rests on. Renamed and extended
  ok_err_some_none_all_lower_to_variant_by_tag_and_carry_their_sum_identity_on_ty
  to assert tail.ty resolves to the constructed sum in every case, and
  added a fifth case (ok_http_case) pinning the Ok/Result-vs-HttpResult
  overload check_ok resolves by peeling the surrounding return type --
  previously named in neither the tests nor the new doc comments.

- Swept the file for every other doc comment citing the now-closed gap,
  not just the two the review named:
  lower_store_field_shape_ir's own comment (only the is-expression half of
  its rationale survives; the None half is resolved and now pinned by a
  real fixture-shaped test), lower_service_handler_signature_ir's comment
  (re-pointed at the still-open Question/Is gaps instead), and three more
  the review didn't flag: lower_provider_given_ir's comment (same
  re-pointing, phrased conservatively since Provider op bodies specifically
  aren't verified to hit Question/Is), and two test doc comments
  (http_service_fixture, sum_actor_binder_lowers_an_actor_sum) whose "works
  around a pre-existing gap" framing was stale.

- Found by direct verification, not just doc review: the
  service_handler_signature test's own claim that "a real IrHandler would
  panic on this exact body" is no longer true -- that specific body
  (Effect.pure(Ok("pong"))) no longer panics lower_service_handler_ir now
  that Ok/Err/Some/None resolve. Corrected the doc comment to say so
  rather than chase a new body shape that still panics; the test's own
  broader claim (a real IrHandler is still unsafe to build unconditionally
  at this call site) still holds via Question/Is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@accuser
accuser force-pushed the p6-option-result-variant-adr branch from 8c006d2 to 99441db Compare August 17, 2026 08:06
@accuser
accuser enabled auto-merge (squash) August 17, 2026 08:06
accuser and others added 2 commits August 17, 2026 08:11
Adds an ADR (design/pending/p6-option-result-variant-identity.md)
resolving the standing blocker: IrExprKind::Variant carried
sum: Arc<TypeDecl> since P6.2, unreachable for a built-in Option/Result
construction since neither is ever backed by a TypeDecl (Ok/Err/Some/None
aren't even calls -- dedicated ExprKind variants, checked through
check_ok/check_err/check_some/check_none with no Callee ever recorded).

Decision: drop `sum` rather than widen it. The wrapping IrExpr::ty
(already present on every node, R6.1) already carries the identical
identity as a TyId -- a constructor call's own checked type is the sum it
constructs, and lower_call_ir already computes it as its first line,
before ever branching into the Callee::Ctor arm. Mirrors IrPat::Variant's
own scrutinee_ty: TyId precedent, the one place this module already had
to solve the identical problem, resolved via bynk_check::checker's own
already-pub variants_of -- proven to cover a user sum, Result, Option,
ActorSum, and HttpResult uniformly, no special-casing gap for the
built-in cases.

Ok/Err/Some/None now lower to IrExprKind::Variant directly, closing the
todo!() ir/lower.rs has carried since P6.2/P6.3. Verified against the
real regression this closes: 223_store_cell_agent's own
`Cell[Option[AuthId]] = None` no longer panics through the full,
init-lowering lower_store_field_ir (previously worked around, not fixed,
by lower_store_field_shape_ir's own never-lower-init posture, #1187's
Agent state-field slice 2a).

No shipped emitter consumer of this IR construction path exists yet
(zero-diff bless run confirms no behavioural change to any emitted
TypeScript). Question's own separate todo!() (its three-way ?-desugar
fork) is unaffected and named again as still open -- a distinct design
question this ADR does not settle.

Closes #1225.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- The new test asserted tag/payload only, which would pass unchanged even
  if tail.ty carried the wrong identity (or none) -- the actual claim the
  ADR rests on. Renamed and extended
  ok_err_some_none_all_lower_to_variant_by_tag_and_carry_their_sum_identity_on_ty
  to assert tail.ty resolves to the constructed sum in every case, and
  added a fifth case (ok_http_case) pinning the Ok/Result-vs-HttpResult
  overload check_ok resolves by peeling the surrounding return type --
  previously named in neither the tests nor the new doc comments.

- Swept the file for every other doc comment citing the now-closed gap,
  not just the two the review named:
  lower_store_field_shape_ir's own comment (only the is-expression half of
  its rationale survives; the None half is resolved and now pinned by a
  real fixture-shaped test), lower_service_handler_signature_ir's comment
  (re-pointed at the still-open Question/Is gaps instead), and three more
  the review didn't flag: lower_provider_given_ir's comment (same
  re-pointing, phrased conservatively since Provider op bodies specifically
  aren't verified to hit Question/Is), and two test doc comments
  (http_service_fixture, sum_actor_binder_lowers_an_actor_sum) whose "works
  around a pre-existing gap" framing was stale.

- Found by direct verification, not just doc review: the
  service_handler_signature test's own claim that "a real IrHandler would
  panic on this exact body" is no longer true -- that specific body
  (Effect.pure(Ok("pong"))) no longer panics lower_service_handler_ir now
  that Ok/Err/Some/None resolve. Corrected the doc comment to say so
  rather than chase a new body shape that still panics; the test's own
  broader claim (a real IrHandler is still unsafe to build unconditionally
  at this call site) still holds via Question/Is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@accuser
accuser force-pushed the p6-option-result-variant-adr branch from 99441db to 8a3e8b2 Compare August 17, 2026 08:11
@accuser
accuser merged commit 20b5ea8 into main Aug 17, 2026
25 checks passed
@accuser
accuser deleted the p6-option-result-variant-adr branch August 17, 2026 08:18
accuser added a commit that referenced this pull request Aug 17, 2026
Track doc had zero mentions of #1225 (Ok/Err/Some/None IrExprKind gap,
resolved via PR #1227), #1226 (Slice 5 rescoped, split), or #1228 (this
PR) despite §6's own text explicitly naming Slice 5 as "left for a
future pass to scope fresh" — that pass happened and the doc never
recorded it. Also fills a pre-existing dangling forward reference: 2a's
own prose (§6) pointed to "§7 below" for the Ok/Err/Some/None gap, but
§7's table never actually got that row. Now that #1225 has resolved the
construction-side identity question, the correction is recorded inline
in §6 and §7 gets the row that actually belongs there — Question's own
still-open three-way desugar fork.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
accuser added a commit that referenced this pull request Aug 17, 2026
…dler::annotations (#1229)

* P6.x: @cache/@limit route annotations read bynk-emit::ir (#1228)

lower_route_cache_ir/lower_route_limit_ir replace workers_entry.rs's
own hand-rolled ExprKind::DurationLit/Ident/IntLit matching (cache_policy_for,
the annotation half of effective_max_body) with standalone ir::lower readers,
following lower_policy_ir's own no-CheckedProgram precedent — deliberately
bypassing the dormant PolicyIr/IrItem::Service pipeline rather than nesting
under it. CachePolicy is gone; HttpRoute::cache now carries ir::CacheIr
directly (identical field shape). Byte-identical output, confirmed by a
zero-diff bless run (298_http_caching/300_http_limits). ast_importers stays
at 7 (workers_entry.rs already counted for other reasons).

Closes #1228

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* design/tracks/the-ir.md: record #1225/#1226/#1228 in §6/§7

Track doc had zero mentions of #1225 (Ok/Err/Some/None IrExprKind gap,
resolved via PR #1227), #1226 (Slice 5 rescoped, split), or #1228 (this
PR) despite §6's own text explicitly naming Slice 5 as "left for a
future pass to scope fresh" — that pass happened and the doc never
recorded it. Also fills a pre-existing dangling forward reference: 2a's
own prose (§6) pointed to "§7 below" for the Ok/Err/Some/None gap, but
§7's table never actually got that row. Now that #1225 has resolved the
construction-side identity question, the correction is recorded inline
in §6 and §7 gets the row that actually belongs there — Question's own
still-open three-way desugar fork.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Address review: direct-call tests for lower_route_cache_ir/lower_route_limit_ir

Review of #1229 found no unit tests for the two new lowerers, unlike every
other standalone lowerer in this file — and named the specific gap: the
untested branches (non-GET guard, maxAge-less @cache discarding an
otherwise-parsed scope, non-positive maxBody) are exactly what the checker
already rejects, so bless-run coverage alone never reaches them. Five tests
added, parsed-but-not-checked (same posture the Events pattern test already
established for the identical reason: no &CheckedProgram, no resolution, no
panic surface).

Non-blocking finding (maxAge millis truncating to 0s below 1 second) filed
separately as #1230 rather than fixed here — pre-existing, faithfully
preserved, out of this PR's own scope per the reviewer's own framing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <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.

P6.x: decide Option/Result's IrExprKind identity — blocks Ok/Err/Some/None construction, Question's desugar, and Provider's op bodies

1 participant