Skip to main content

bynk_ir/
lib.rs

1//! P6.1 (design/tracks/the-ir.md §6, #1141): the IR's core node types —
2//! `IrExpr`/`IrExprKind`/`IrStmt`, Part 6.2 of `design/bynk-greenfield-compiler.md`.
3//!
4//! R6.1 — "Every `IrExpr` carries its type. The constructor requires it;
5//! there is no side table and no fallible lookup from the emitter to the
6//! checker." This module is the type only; `bynk_lower` is the
7//! `&CheckedProgram → Ir` pass that constructs values of it.
8//!
9//! **Identity fields are adapted, not literal** (Decision B, extending P6.0's
10//! own precedent, ADR 0333 `the-ir-callee-in-bynk-check`): the reference's
11//! `DefId`/`FieldId`/`LocalId`/`VariantId` arena does not
12//! exist in this codebase (no `Resolve` phase mints them —
13//! `project-model.md` §3.4 deferred that to phase 8), so every such slot
14//! becomes whatever cheap resolved handle the checker already has —
15//! `Arc<TypeDecl>`/`Arc<FnDecl>` for a declaration, `String` for a name with
16//! no arena of its own. `Call`'s payload is [`Callee`] verbatim — P6.0
17//! already did this exact substitution for call-dispatch identity, so `Call`
18//! needs no adaptation of its own here.
19//!
20//! **The whole Part 6.2 shape lands in one piece** (Decision D): every
21//! variant below exists, including `Match`/`Variant`/`Call`/`Lambda`, so a
22//! later slice (P6.2, P6.4, P6.5) widens only `bynk_lower`'s match, never this
23//! type. `Match`'s own payload is four types: [`IrPat`]/[`IrArm`]/
24//! [`Exhaustive`] are P6.4's own commission (#1157, Part 5.1/5.2 of the
25//! reference); [`MatchForm`] is P6.5's own (#1159, R5.2/R5.3, scoped to
26//! shape only — Decision A). All four are real, constructible types as of
27//! P6.5, wired into a real [`IrExprKind::Match`] by `bynk_lower`'s
28//! `ExprKind::Match` arm, which calls P6.4's own standalone constructors
29//! (`bynk_lower::lower_pattern_ir`/`bynk_lower::lower_arm_ir`/
30//! `bynk_lower::lower_exhaustive_ir`) verbatim. `Question`/`Is` stay
31//! desugars-to-`Match` in name only — neither gets real construction this
32//! slice, each for a reason specific to it (see `bynk_lower`'s own `todo!()`
33//! text for each).
34//!
35//! **Decision D's own "never widens beyond the reference's Part 6.2 shape"
36//! is not absolute — it held only as long as the reference's own node set
37//! was complete** ([`IrExprKind::BinOp`]/[`IrExprKind::Neg`]/
38//! [`IrExprKind::InterpStr`], #1189).
39//! `design/bynk-greenfield-compiler.md` §6.2's own listing (`Const, Local,
40//! Global, Record, Variant, Field, List, Block, If, Match, And, Or, Not,
41//! Return, Call, Lambda, Await, Send, Pure`) never names comparison,
42//! arithmetic, unary negation, or string interpolation at all — confirmed a
43//! true omission, not a deliberately-out-of-scope row this track chose to
44//! defer (unlike, say, `Question`/`Is` above, each of which the reference
45//! *does* name and this slice explicitly declines). P6.2 (#1143) and P6.3
46//! (#1145) each independently confirmed the gap and left it a `todo!()`
47//! rather than force-fitting it under `Decision D`'s closed-shape framing;
48//! #1189 settled it as buildable, three new variants, added here rather than
49//! deferred indefinitely — real programs use `+`/`-`/`<`/`==` routinely (a
50//! certified `.bynk` invariant like `balance >= 0` is ordinary, not an edge
51//! case), and the checker already resolves every one of these expressions'
52//! types (`bynk-check/src/checker/expressions.rs`'s `check_binop`/
53//! `check_unary`, and `checker.rs`'s own `InterpStr` hole-checking) — nothing
54//! about them was actually blocked on new checker work, only on this type
55//! gaining a place to land them.
56//!
57//! **P6.6 (#1161) adds [`IrItem`]/[`TypeShape`] (Part 6.6)** — a *separate*
58//! top-level type from [`IrExpr`], not a new [`IrExprKind`] variant:
59//! declarations and expressions are different node families in the
60//! reference sketch, and unlike `Match`'s own payload, `IrItem` carries no
61//! constraint that its whole seven-variant design-sketch shape must exist as
62//! of this slice — see [`IrItem`]'s own doc comment for exactly which are
63//! real.
64//!
65//! **P6.7 (#1163) adds [`StoreFieldIr`]/[`StoreKindIr`]/[`IndexIr`]** (Part
66//! 6.6's own trailing two structs, R6.14) — an agent `store` field's own
67//! state shape and index-table keys, derived once here rather than
68//! re-derived independently by both the checker's own ephemeral
69//! `checker::StoreField` dispatch and the shipped emitter's own
70//! `store_map_fields`/`store_cache_fields`/`store_log_fields`/…
71//! (`bynk-emit/src/emitter/emit.rs`). Same posture as every prior P6.x
72//! slice: no `IrItem` variant references these yet — `IrItem::Agent`/
73//! `Service` remain unconstructed (see [`IrItem`]'s own doc comment for
74//! exactly what still blocks them).
75//!
76//! **P6.8 (#1165) adds [`CommitShape`]/[`IrPredicate`]** (Part 6.7's own
77//! trailing two types, R6.15) — a handler body's own resolved one-of-three
78//! commit shape, decided once here from a mutating `Callee::Store` write or
79//! a bare `:=`, rather than re-derived at emission time by the shipped
80//! emitter's own name-matching `block_writes_state` (R6.5). Same posture
81//! again: no `IrItem` variant references either yet — `IrHandler` itself
82//! still does not exist, and no rule in `design/tracks/the-ir.md`'s own
83//! slice table commissions it (see [`IrItem`]'s own doc comment).
84//!
85//! **P6.9 (#1167) adds [`IrStmt::Assign`]/[`ActorBinder`]/[`IrHandler`]**
86//! (Part 6.7's own trailing construct, R6.16) — a real, standalone,
87//! constructible agent `on call` handler, plus the `Statement::Assign`
88//! prerequisite gap (`todo!()` since P6.1, twice deferred by P6.7/P6.8's own
89//! Risks) it needed closed first: `bynk_lower::lower_handler_ir` cannot lower a
90//! store-writing handler body without a real `IrStmt` target for `:=`.
91//! `binder: Option<ActorBinder>` is always `None` from
92//! `bynk_lower::lower_handler_ir` this slice — an agent handler structurally
93//! cannot carry one (`bynk.actor.by_on_agent`); a real service handler's own
94//! non-`None` `binder` needs a `bynk-check` change this slice does not make
95//! (see [`IrHandler`]'s own doc comment). Same posture again: no `IrItem`
96//! variant references `IrHandler` yet — `IrItem::Agent`/`Service` remain
97//! unconstructed (see [`IrItem`]'s own doc comment).
98//!
99//! **P6.10 (#1169) adds [`IrItem::Agent`]** — the assembly P6.9's own Risks
100//! section named without numbering it: every ingredient (`StoreFieldIr`
101//! since P6.7, `CommitShape`/`IrPredicate` since P6.8, `IrHandler` since
102//! P6.9) was already real, but nothing combined them into one `IrItem`
103//! value, and `IrItem` had no variant to carry the result. This is the
104//! first `IrItem` variant with a real consumer shape below the top level —
105//! `bynk_lower::lower_agent_item_ir` calls every prior slice's own standalone
106//! constructor rather than re-deriving any of their logic, the same
107//! "wire, don't re-derive" posture this whole module has held since P6.0.
108//! `IrItem::Service`/`Actor`/`Capability`/`Provider` remain deferred, each
109//! for its own reason (see [`IrItem`]'s own doc comment).
110//!
111//! **P6.11 (#1171) adds [`IrItem::Service`]** — the sibling assembly to
112//! P6.10's `Agent`, closing both blockers `IrItem`'s own doc comment named:
113//! #1170 made a service handler's `binder` readable post-`certify` for the
114//! first time, and this slice specifies the two types the reference names
115//! but never defines for a service ([`ProtocolIr`], [`PolicyIr`]).
116//! `bynk_lower::lower_service_handler_ir` is a new sibling of
117//! `bynk_lower::lower_handler_ir`, not a widening of it — the two seed
118//! disjoint scopes, and widening would have deleted the agent-only `by`
119//! assertion that today catches a service handler reaching the wrong entry
120//! point. A `from websocket` lifecycle handler's own body
121//! (`on open`/`on message`/`on close`) is the one named exception: the
122//! checker-injected synthetic `connection` param has no IR target yet, so
123//! lowering one hits an explicit `todo!()` rather than a silently wrong
124//! tree — see `bynk_lower::lower_service_handler_ir`'s own doc comment.
125//! `IrItem::Actor`/`Capability`/`Provider` remain deferred, each with its
126//! own already-tracked, genuinely unsettled blocker (see [`IrItem`]'s own
127//! doc comment).
128//!
129//! **P6.12 (#1173) adds [`IrItem::Capability`]** — the reference's own
130//! `ops: Vec<OpSig>` sketch (`bynk-greenfield-compiler.md:1134`) named a
131//! type it never defined anywhere in the document; [`OpSig`] is that
132//! missing shape, adapted from `bynk_syntax::ast::CapabilityOp` under this
133//! module's own "no arena" substitution (`Vec<(String, TyId)>` for
134//! `params`, mirroring [`IrItem::Fn::params`] exactly).
135//! `bynk_lower::lower_capability_item_ir` resolves each op's own `params`/
136//! `return_type` in that op's own rigid-variable scope (`op.type_params`)
137//! — a capability op's type parameters are scoped to the op itself, not
138//! the capability, unlike a method's generic receiver — the same per-op
139//! treatment the checker's own `CapabilityOpInfo` already gives a generic
140//! op (`context_checks.rs`'s `build_capability_op_info`). `IrItem::Actor`/
141//! `Provider` remain deferred, each with its own already-tracked,
142//! genuinely unsettled blocker (see [`IrItem`]'s own doc comment).
143//!
144//! **P6.14 (#1174) adds [`IrItem::Provider`]** — #1174's own grounding pass
145//! found this buildable, unlike its sibling `Actor`: the reference's own
146//! `body: ProviderBody // Bynk ops | External(module)` comment names a type
147//! it never defines, the same gap [`OpSig`] closed for `Capability`, and
148//! `ProviderDecl::external` already carries the exact dispatch it needs.
149//! `bynk_lower::lower_provider_op_ir` is a new sibling of
150//! `bynk_lower::lower_fn_body_ir`, not a widening of it, for the same "each
151//! body-lowering entry point seeds its own scope" reason
152//! `bynk_lower::lower_handler_body_ir`'s own doc comment already gives: no
153//! `self`, no store cells, just a provider op's own params — `given`
154//! capability calls need no scope entry of their own, the same
155//! already-generic `Callee`-wrapping `bynk_lower::lower_handler_body_ir`'s own
156//! doc comment credits for handler bodies. `IrItem::Actor` remains
157//! deferred, its own already-tracked blocker unchanged by this slice (see
158//! [`IrItem`]'s own doc comment).
159
160use std::collections::HashMap;
161
162use bynk_check::checker::{Callee, TyId};
163use bynk_syntax::ast::{
164    BaseType, Block, Expr, ExprId, MatchArm, Pattern, Refinement, expr_children, statement_exprs,
165};
166use bynk_syntax::span::Span;
167
168/// A lowered expression: its shape, its checked type, and the source span it
169/// came from. `ty` is required at construction (R6.1) — never `Option`,
170/// never looked up lazily by a reader.
171#[derive(Debug, Clone)]
172pub struct IrExpr {
173    pub kind: IrExprKind,
174    pub ty: TyId,
175    pub span: Span,
176}
177
178/// A constant value — the payload of [`IrExprKind::Const`]. Adapted from the
179/// reference's own `ConstVal` (`Int Float Str Bool Unit Bytes`, Part 6.2's
180/// comment): `DurationMillis` replaces `Bytes` because Bynk has a real
181/// `<int>.<unit>` duration *literal* (`ExprKind::DurationLit`) `Const` must
182/// cover, while `Bytes` has no literal AST form at all in this language —
183/// every `Bytes` value comes from a static-constructor *call*
184/// (`Bytes.fromUtf8`/`fromBase64`/`empty()`, `Callee::Intrinsic` territory,
185/// not a literal).
186#[derive(Debug, Clone, PartialEq)]
187pub enum ConstVal {
188    Int(i64),
189    Float(f64),
190    DurationMillis(i64),
191    Str(String),
192    Bool(bool),
193    Unit,
194}
195
196/// The payload of [`IrExprKind::Global`] — adapted per Decision C's narrow
197/// scope, refined during implementation: a bare nullary sum-variant
198/// constructor reference (`Miss`, `PaymentDeclined`), identified structurally
199/// (exactly one sum type in `TypedCommons::types` owns a variant of this
200/// name with an empty payload — the same "unique owner" test
201/// `check_ident`'s own unconditional fallback arm uses, `bynk-check/src/checker/expressions.rs:103-130`).
202///
203/// The proposal's own Decision C also named a bare `HttpResult`/
204/// `QueueResult` nullary built-in variant reference (`NotFound`, `Ack`) as
205/// in scope — dropped during implementation: the checker's own detection for
206/// that case (`checker.rs`'s `type_of`, `ExprKind::Ident` arm) is gated on
207/// `expected`/`ctx.return_ty` ("resolve to `HttpResult` only when the
208/// surrounding type implies it, or no user sum-type variant of the same name
209/// exists") — contextual, position-dependent disambiguation this pass has no
210/// sink to read back, the exact re-derivation this track's whole `Callee`
211/// precedent (P6.0) exists to avoid. Left for a slice that either adds that
212/// sink or accepts re-deriving it, not built worse here to hit a self-set
213/// scope target.
214/// P6.39 (design/tracks/the-ir.md §6a): the owning sum's own `Arc<TypeDecl>`
215/// (`sum`) was dropped — the "unique owner" test above still runs to
216/// disambiguate, but had no reader for the `TypeDecl` itself (verified: zero
217/// production sites, only a test assertion). `tag` alone is this variant's
218/// whole payload now.
219#[derive(Debug, Clone)]
220pub struct GlobalRef {
221    pub tag: String,
222}
223
224/// P6.4's real Pattern IR (`design/bynk-greenfield-compiler.md` §5.1, #1157):
225/// a pattern's own recursive shape, six variants mapping one-to-one onto
226/// `bynk_syntax::ast::Pattern`'s own six (`Wildcard`, `Binding`, `Literal`,
227/// `Variant`, `Refined`, `Or`). `bynk_lower::lower_pattern_ir` is the
228/// `&Pattern -> IrPat` constructor, tested standalone — not yet wired into
229/// [`IrExprKind::Match`]/`Question`/`Is` construction (P6.5's own
230/// commission). No `PatId` arena — a pattern owns its children directly
231/// (`Box<IrPat>`), the same "no arena exists in this codebase" substitution
232/// this module's own doc comment already applies throughout.
233#[derive(Debug, Clone)]
234pub enum IrPat {
235    /// `_` — matches anything, binds nothing.
236    Wild,
237    /// A name binding — matches anything, binds `local` to the whole value
238    /// at this position (Decision B substitution: `String`, no `LocalId`
239    /// arena).
240    Bind { local: String },
241    /// A literal pattern — `Int`/`Str`/`Bool` only (ADR 0001's closed
242    /// literal-pattern set), reusing [`ConstVal`] rather than inventing a
243    /// narrower value type for the variants (`Float`/`DurationMillis`/
244    /// `Unit`) no `Pattern::Literal` ever produces.
245    Const { value: ConstVal },
246    /// A sum-variant pattern, `Variant` or `Variant(bindings)` — not just a
247    /// user-declared sum (Decision A): `scrutinee_ty` is resolved through
248    /// the checker's own `variants_of`, the same function that already
249    /// flattens a user sum, `Result`, `Option`, `ActorSum` and `HttpResult`
250    /// into one uniform shape, rather than `Callee::Ctor`'s
251    /// `Arc<TypeDecl>`-keyed identity scheme, which never fires for
252    /// `Ok`/`Err`/`Some`/`None` at all (`#1145`'s own Decision B) —
253    /// [`IrExprKind::Variant`] later resolved the identical problem on the
254    /// *construction* side the same way, #1225's own ADR.
255    Variant {
256        /// The value this pattern matches against — resolved via
257        /// `variants_of(scrutinee_ty, ..)` to find `tag`'s own payload
258        /// shape. Not the sum's own declaration identity: no `Arc<TypeDecl>`
259        /// exists for a built-in sum like `Option`/`Result`.
260        scrutinee_ty: TyId,
261        tag: String,
262        /// Exactly the payload bindings the source pattern names — a named
263        /// form may bind a strict subset of the variant's payload fields
264        /// (`bynk-check`'s own `check_pattern` allows this); empty for a
265        /// nullary pattern, even over a non-nullary variant (`Miss` without
266        /// `(..)` binds nothing and only tests the tag).
267        fields: Vec<(String, Box<IrPat>)>,
268    },
269    /// `p 'where' predicate` — R5.4: a refinement is a *test*, ordered after
270    /// structural matching and before the guard; never a binding site of its
271    /// own.
272    Refined {
273        inner: Box<IrPat>,
274        refinement: Refinement,
275    },
276    /// `p1 | p2 | … | pn` — matches if any alternative matches. R5.5's own
277    /// binding-mode consequence lives on [`IrArm::binding_mode`], not here —
278    /// an `Or` node is a pure structural fact about the pattern's shape.
279    Or { alts: Vec<IrPat> },
280}
281
282/// P6.4's real `IrArm` (Part 5.1, #1157) — the reference's own bare sketch
283/// (`struct IrArm { pat, guard, body, binds }`) adapted per this module's
284/// "no arena" substitution (`local: LocalId -> String`, `pat: PatId ->
285/// IrPat` owned directly) plus one field the sketch doesn't carry:
286/// `binding_mode` (Decision C, R5.5, computed once during this arm's own
287/// construction rather than re-walked by any later reader —
288/// `design/bynk-greenfield-compiler.md:749-751`). See [`BindingMode`]'s own
289/// doc comment for exactly what this one arm-level flag does and doesn't
290/// tell a future reader.
291#[derive(Debug, Clone)]
292pub struct IrArm {
293    pub pat: IrPat,
294    pub guard: Option<IrExpr>,
295    pub body: IrExpr,
296    /// Every name this arm's pattern binds (Decision B substitution:
297    /// `String`, no `LocalId`) — `Pattern::bound_names`'s own "first
298    /// alternative" defensive default for an `Or` (the checker separately
299    /// verifies every alternative binds the same set at the same types).
300    pub binds: Vec<String>,
301    pub binding_mode: BindingMode,
302}
303
304/// R5.5, Decision C — `OrDispatch` iff `IrPat::Or` occurs anywhere in the
305/// arm's own pattern tree, computed once by `bynk_lower::lower_arm_ir`.
306///
307/// **Arm-level granularity, not node-level** — worth being precise about,
308/// since `emit_pattern_bindings` (`emitter/lower.rs:5401-5503`) is not the
309/// single top-level check this doc comment used to claim: it is itself a
310/// recursive walk, and its `Pattern::Or` arm fires wherever an `Or` node
311/// occurs, at whatever depth — a nested `Or` inside a `Variant` payload
312/// really parses (`Hit(a, Sub(b) | Other(b))`) and really reaches it. So
313/// this flag answers "does *any* node in this arm's pattern need `let` +
314/// per-alternative dispatch", not "which node" — a consumer that needs the
315/// latter still walks `IrPat`'s own recursive shape to find it (P6.5's own
316/// job, whichever future consumer needs it). What this flag removes is
317/// having to do that walk at all just to answer the yes/no question.
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum BindingMode {
320    /// No `Or` anywhere in the pattern's tree — every bound name can emit
321    /// as a plain `const`.
322    Direct,
323    /// An `Or` occurs somewhere in the pattern's tree — at least one shared
324    /// name can bind at a different structural path across alternatives, so
325    /// emission needs `let` plus a per-alternative dispatch somewhere in
326    /// this arm (see this type's own doc comment for why "somewhere" is as
327    /// precise as this flag alone gets).
328    OrDispatch,
329}
330
331/// P6.4's real `Exhaustive` (Part 5.2, R5.6/R5.7, #1157) — `Partial`'s own
332/// witness payload reuses the checker's already-shipped `missing_patterns`
333/// shape (`Vec<String>`, human-readable witness descriptions) rather than
334/// inventing the reference's own unspecified `PatternWitness` struct
335/// (Decision B). Both variants are real and matchable, but this slice's own
336/// `bynk_lower::lower_exhaustive_ir` only ever constructs `Total` — see that
337/// function's own doc comment for why `Partial` is real, inhabited code,
338/// yet unreached here.
339#[derive(Debug, Clone)]
340pub enum Exhaustive {
341    Total,
342    Partial(Vec<String>),
343}
344
345/// P6.5's own real `MatchForm` (#1159, R5.2/R5.3) — scoped to shape only
346/// (Decision A). The reference's own table crosses tail-vs-value position
347/// with flat-vs-if-chain shape into four printed forms, but position is
348/// decided by *where in the AST the caller already is* — the same mechanism
349/// that already decides tail-vs-value for every other `IrExprKind`,
350/// including `If`, which P6.1 already committed to modelling
351/// position-agnostically (this module's own `IrExprKind::If` doc comment).
352/// `Match`'s own `scrutinee`/`arms`/`exhaustive` are identical regardless of
353/// position, so only the shape bit is recorded here — a future printer
354/// derives the tail-vs-value physical shape itself, the same way it would
355/// for `If`.
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum MatchForm {
358    /// A flat `switch` on the scrutinee's own tag/value — no arm carries a
359    /// guard or a refutable nested payload pattern.
360    Flat,
361    /// An if/else-if chain (ADR 0169) — at least one arm carries a guard or
362    /// a refutable nested payload pattern a single-level `switch` can't
363    /// express.
364    IfChain,
365}
366
367/// A lowered expression's shape. Every node kind from Part 6.2 exists here
368/// (Decision D); `bynk_lower` implements real construction only for the
369/// subset named in `design/tracks/the-ir.md`'s own P6.1 row — every other
370/// arm is a named `todo!()` in the lowering pass, not a missing variant
371/// here.
372#[derive(Debug, Clone)]
373pub enum IrExprKind {
374    /// A literal value.
375    Const(ConstVal),
376    /// Reading a function-scoped local or parameter, by name (Decision B —
377    /// no `LocalId` arena exists).
378    Local(String),
379    /// A bare reference to something with no scope of its own — narrowly
380    /// scoped per Decision C; see [`GlobalRef`].
381    Global(GlobalRef),
382    /// A bare `store Map[K, V]` field name used as a **value**, not a
383    /// method-call receiver (`Callee::Store`/`Callee::Query` already lower
384    /// those separately) and not a `Cell` (those are bound into scope as an
385    /// ordinary [`IrExprKind::Local`], v0.81's "implicit deref" rule — see
386    /// `bynk_lower::lower_handler_body_ir`'s own doc comment). The
387    /// checker types this expression `Ty::Query(V)` (ADR 0120) *without*
388    /// ever binding the name into its own value scope
389    /// (`bynk-check/src/checker.rs`'s `ExprKind::Ident` dispatch,
390    /// `ctx.lookup(...).is_none() && ctx.store_fields.get(...)`, checked
391    /// *before* falling through to `check_ident`) — a lazy query over the
392    /// field's current values, not a snapshot. Reached two ways: a bare
393    /// argument position (`lines.joinOn(orders, ...)`) and, unrecognised
394    /// until this variant existed, the receiver of a
395    /// `.entries`/`.keys`/`.values` [`IrExprKind::Field`] (ADR 0184) —
396    /// `ExprKind::FieldAccess`'s own lowering always lowers its `receiver`
397    /// unconditionally, so a bare store-map field reached there the same
398    /// unbound-ident path an argument-position reference does. `Log`
399    /// deliberately not covered here (review of #1240): unlike `Map`,
400    /// `bynk-check` never special-cases `StoreField::Log` in a bare-value or
401    /// `FieldAccess` dispatch, so a bare `Log` value is not checker-legal
402    /// today — the same bucket `Set`/`Cache` are already in. Carries only
403    /// the field's own name — every consumer already has the enclosing
404    /// `IrItem::Agent`'s `state: Vec<StoreFieldIr>` to look the kind back up
405    /// in, the same "identity, not a copy" posture
406    /// `Callee::Store::field`/`GlobalRef::tag` already established.
407    StoreQuery(String),
408    /// Record construction. `fields` is always complete — every field the
409    /// record declares is present, exactly once, a shorthand field (`{ x }`)
410    /// resolved to its full `(name, value)` pair during lowering same as
411    /// every other field — and *ordered by evaluation order*, left to
412    /// right: a reader walking `fields` in order reproduces the same
413    /// left-to-right effect sequencing a value expression's own evaluation
414    /// has, so this is never re-sorted to, say, the record's own declared
415    /// field order once evaluation order and declaration order diverge
416    /// (`RecordSpread`'s own lowering, `ir/lower.rs`'s
417    /// `lower_record_spread_ir`, is the one producer where they can).
418    /// P6.39: `def: Arc<TypeDecl>` dropped — zero production readers
419    /// (verified), only test assertions. `fields` alone is this variant's
420    /// whole payload now.
421    Record { fields: Vec<(String, IrExpr)> },
422    /// Sum-variant construction — a user-declared sum's own constructor
423    /// (`Circle(n)`/`Shape.Circle(n)`, driven by `Callee::Ctor`) *and*, as
424    /// of the #1225 ADR, the four built-in constructors `Ok`/`Err`/`Some`/
425    /// `None` too, which `Callee::Ctor` can never classify (no
426    /// `Arc<TypeDecl>` exists for `Option`/`Result` — `Callee::Ctor`'s own
427    /// two minting sites, `bynk-check/src/checker/calls.rs`, can only ever
428    /// resolve a real `TypeBody::Sum` declaration by name, and `Ok`/`Err`/
429    /// `Some`/`None` are dedicated `ExprKind` variants entirely, checked
430    /// through `check_ok`/`check_err`/`check_some`/`check_none` with no
431    /// `Callee` ever recorded).
432    ///
433    /// **No `sum` identity field, deliberately** — an earlier draft carried
434    /// `sum: Arc<TypeDecl>` (mirroring [`GlobalRef`]/`Record::def`'s own
435    /// declaration-identity convention), which is exactly what made
436    /// `Ok`/`Err`/`Some`/`None` unrepresentable: no `TypeDecl` exists for a
437    /// built-in sum, so a real fix would either need a synthetic decl (no
438    /// established path for one, and a new kind of "fake but not fake"
439    /// value this module would need to invent) or would have to widen `sum`
440    /// into an `Arc<TypeDecl>`-or-built-in enum for the sake of a case that
441    /// doesn't need declaration identity at all. #1225's own resolution: the
442    /// wrapping [`IrExpr::ty`] — already present on every node (R6.1) —
443    /// already carries this exact identity as a `TyId`, uniformly, for both
444    /// a user sum (`Ty::Named { kind: Sum, .. }`) and a built-in one
445    /// (`Ty::Option`/`Ty::Result`), since a constructor call's own checked
446    /// type *is* the sum it constructs. Mirrors [`IrPat::Variant`]'s own
447    /// `scrutinee_ty: TyId` precedent exactly — the one field this module
448    /// already had to solve the identical problem for, on the
449    /// pattern-matching side — rather than introducing a second, redundant
450    /// copy of the same `TyId` one level in. A consumer needing the sum's
451    /// own tag/payload shape calls `bynk_check::checker::variants_of`
452    /// (already `pub` for precisely this, `checker.rs:4323`) against the
453    /// enclosing `IrExpr::ty` — the same function [`IrPat::Variant`]'s own
454    /// lowering already calls, proven to resolve a user sum, `Result`,
455    /// `Option`, `ActorSum`, and `HttpResult` alike with no special-casing
456    /// gap for the built-in cases.
457    Variant { tag: String, payload: Vec<IrExpr> },
458    /// Field access on a record value.
459    Field { base: Box<IrExpr>, field: String },
460    /// A list literal.
461    List { elems: Vec<IrExpr> },
462    /// A `{ ... }` block: statements, then a tail value.
463    Block {
464        stmts: Vec<IrStmt>,
465        tail: Box<IrExpr>,
466    },
467    /// `if cond { then } else { else }` — both branches always present (an
468    /// else-less `if` already carries a synthesised unit `else` at the AST
469    /// level, `Block::is_synth_unit`).
470    If {
471        cond: Box<IrExpr>,
472        then_: Box<IrExpr>,
473        else_: Box<IrExpr>,
474    },
475    /// Pattern matching. `scrutinee`/`arms`/`exhaustive`/`form` are all
476    /// real, constructible values as of P6.5 (#1159) — `arms`/`exhaustive`
477    /// built by calling P6.4's own `lower_pattern_ir`/`lower_arm_ir`/
478    /// `lower_exhaustive_ir` verbatim (#1157), `form` by reusing the
479    /// shipped string emitter's own `match_needs_if_chain`/
480    /// `pattern_has_nested_test` (Decision B) rather than re-deriving an
481    /// equivalent predicate over `IrPat`'s own (slightly different)
482    /// recursive shape.
483    Match {
484        scrutinee: Box<IrExpr>,
485        arms: Vec<IrArm>,
486        exhaustive: Exhaustive,
487        form: MatchForm,
488    },
489    /// `lhs && rhs` — short-circuit, structurally (R6.3, already true of the
490    /// existing string-based emitter; this tree shape makes it true of the
491    /// IR too, not just the emission-time machinery threaded to preserve it).
492    And { lhs: Box<IrExpr>, rhs: Box<IrExpr> },
493    /// `lhs || rhs`.
494    Or { lhs: Box<IrExpr>, rhs: Box<IrExpr> },
495    /// `!operand`.
496    Not { operand: Box<IrExpr> },
497    /// Comparison/arithmetic (#1189): `Eq`/`NotEq`/`Lt`/`LtEq`/`Gt`/`GtEq`/
498    /// `Add`/`Sub`/`Mul`/`Div` — every [`bynk_syntax::ast::BinOp`] member
499    /// *except* `And`/`Or`/`Implies`, which stay their own dedicated
500    /// variants above (Decision A, #1189: `And`/`Or` exist as their own
501    /// nodes specifically to make short-circuit evaluation a structural
502    /// property of the tree — R6.3 — and `Implies` desugars away entirely;
503    /// none of that applies to a strict, both-operands-always-evaluated
504    /// arithmetic/comparison operator, so one shared, `op`-tagged variant
505    /// covers all ten without ten near-duplicate variants/lowering arms).
506    /// `lhs`/`rhs` are lowered independently — no `is`-binding propagation
507    /// exists for these operators, unlike `And`.
508    BinOp {
509        op: IrBinOp,
510        lhs: Box<IrExpr>,
511        rhs: Box<IrExpr>,
512    },
513    /// `-operand` (#1189) — `bynk_syntax::ast::UnaryOp::Neg`'s own
514    /// counterpart to `Not` above; the checker requires an `Int` operand and
515    /// returns `Int` (`check_unary`, `bynk-check/src/checker/expressions.rs`).
516    Neg { operand: Box<IrExpr> },
517    /// An interpolated string (#1189) — `bynk_syntax::ast::ExprKind::InterpStr`'s
518    /// alternating chunk/hole run, each hole an ordinary lowered expression
519    /// (the checker's own hole rule restricts a hole to a scalar or
520    /// scalar-refinement type; this module does not re-derive that
521    /// restriction, only carries the already-checked result). Always typed
522    /// `String`.
523    InterpStr { parts: Vec<IrInterpPart> },
524    /// A function/handler body's own tail value, in return position. Built,
525    /// not parsed — Bynk has no `return` keyword; this node is constructed
526    /// only by `lower::lower_fn_body_ir` wrapping a body block's tail (the
527    /// `?` operator's early-return desugar is P6.3's row, a second future
528    /// producer of this same node).
529    Return { value: Box<IrExpr> },
530    /// The `HttpResult.NotFound` sentinel `Option[T]?`'s own desugar
531    /// early-returns on `None` (ADR 0177) — `bynk_lower::lower_question_ir`'s
532    /// own construction, never sourced from user syntax (no bynk source
533    /// text spells `HttpResult.NotFound`; the shipped string emitter
534    /// hand-writes this exact text as boilerplate, `emitter/lower.rs`'s own
535    /// `ExprKind::Question` arm). Deliberately its own zero-payload variant,
536    /// not routed through [`GlobalRef`]: that type resolves a *source*
537    /// identifier against `TypedCommons::types`, and `HttpResult` is a
538    /// checker built-in with no `TypeDecl` there to resolve against at all
539    /// ([`GlobalRef`]'s own doc comment already names this exact case as
540    /// out of its scope, "dropped during implementation").
541    HttpResultNotFound,
542    /// A refined-type/inline-predicate boolean check — `value is Quantity`
543    /// (`Quantity` a declared refined type) or `_ where predicate`'s own
544    /// predicate half. `refinement`/`base` are `bynk_syntax::ast` values
545    /// reused verbatim, not decomposed into `IrExprKind` boolean primitives
546    /// — the same "reused, not adapted" posture [`IrPat::Refined`] (P6.4,
547    /// #1157) already committed to for the identical payload, one variant
548    /// case at a time (`PredKind`'s own closed set — `InRange`/`MinLength`/
549    /// `Matches`/…) rather than an open-ended expression tree, so nothing
550    /// about this construction contradicts R6.7's own "desugar once"
551    /// mandate the way an *arbitrary* un-desugared sub-expression would;
552    /// decomposing `PredKind` itself into `BinOp`/`Call` primitives is real,
553    /// separate, deferred work (mirrors the base-type check the shipped
554    /// string emitter's own `refined_check_as_bool` always prepends when
555    /// `base` is `Int`/`Float` — folded into this one node rather than a
556    /// second sibling, since the two are never meaningfully separable at a
557    /// call site: every real reader wants "is this refined value valid," not
558    /// the base-type and predicate halves independently).
559    RefinedCheck {
560        value: Box<IrExpr>,
561        base: BaseType,
562        refinement: Option<Refinement>,
563    },
564    /// A call, classified by P6.0's `Callee` — no adaptation needed here,
565    /// `Callee` already resolves identity the way this module's other
566    /// `DefId`-shaped slots do. Lowering deferred to P6.2.
567    Call {
568        callee: Callee,
569        targs: Vec<TyId>,
570        args: Vec<IrExpr>,
571    },
572    /// A lambda. Lowering deferred to P6.2, alongside `Call` (a lambda's
573    /// only use today is as a kernel-method argument, `Callee::Kernel`
574    /// territory).
575    Lambda {
576        params: Vec<String>,
577        body: Box<IrExpr>,
578        captures: Vec<String>,
579    },
580    /// `<- effect` — await an `Effect[T]`'s value.
581    Await { effect: Box<IrExpr> },
582    /// `~> effect` — fire-and-forget; typed `Unit`.
583    Send { effect: Box<IrExpr> },
584    /// `Effect.pure(value)` — introduce a synchronous value as `Effect[T]`.
585    Pure { value: Box<IrExpr> },
586}
587
588/// A lowered statement — Bynk's real statement surface
589/// (`Let`/`EffectLet`/`Expect`/`Send`/`Do`/`Assign`) folds down onto the
590/// reference's own two-variant `IrStmt` (Part 6.2), extended by P6.9
591/// (#1167) with a third: `Send`/`Do` become `Expr` wrapping
592/// [`IrExprKind::Send`]/[`IrExprKind::Await`]; `EffectLet` becomes `Let`
593/// wrapping an `Await`. `Assign` (a `Cell` `:=` write) is real as of P6.9
594/// ([DECISION B], #1167) — this comment used to (twice) forward-reference
595/// `Callee::Store` territory for it, on a premise P6.9's own grounding pass
596/// found false: `checker.rs`'s own `Statement::Assign` arm resolves
597/// `a.target.name` directly against `ctx.store_fields` by bare name and
598/// never keys a `Callee` at all (only `a.value`, an ordinary sub-expression,
599/// ever gets one), so no `ExprId`-keyed sink was ever actually needed —
600/// [`IrStmt::Assign`] is the ordinary two-field `Let`-shaped fix that was
601/// available the whole time. `Expect` (test-only) has no target here — not
602/// named by any rule this track commissions — and stays `todo!()` in the
603/// lowering pass, not silently dropped.
604#[derive(Debug, Clone)]
605pub enum IrStmt {
606    Let {
607        local: String,
608        value: IrExpr,
609    },
610    Expr {
611        value: IrExpr,
612    },
613    /// `cell := value` — [DECISION B] (#1167). `field` is the target
614    /// `Cell` field's own bare name (`AssignStmt.target.name`, this
615    /// module's usual "no arena" substitution); `value` is the ordinary
616    /// lowered RHS.
617    Assign {
618        field: String,
619        value: IrExpr,
620    },
621}
622
623/// [`IrExprKind::BinOp`]'s own operator tag (#1189) — every
624/// [`bynk_syntax::ast::BinOp`] member that lowers into `BinOp` rather than
625/// its own dedicated `IrExprKind` variant or a desugar (see `BinOp`'s own
626/// doc comment for which and why). Deliberately a plain tag with no payload
627/// of its own — `lhs`/`rhs` already carry their own resolved `ty`, and
628/// unlike, say, [`StoreKindIr`], no variant here needs anything beyond its
629/// own identity.
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631pub enum IrBinOp {
632    Eq,
633    NotEq,
634    Lt,
635    LtEq,
636    Gt,
637    GtEq,
638    Add,
639    Sub,
640    Mul,
641    Div,
642}
643
644/// [`IrExprKind::InterpStr`]'s own per-part payload (#1189) — the IR-side
645/// mirror of [`bynk_syntax::ast::InterpPart`], substituting `Hole`'s raw
646/// `Box<Expr>` for an already-lowered `Box<IrExpr>` under this module's
647/// usual "carry the lowered form, not the source form" discipline (the same
648/// substitution every other `IrExprKind` payload already makes).
649#[derive(Debug, Clone)]
650pub enum IrInterpPart {
651    /// Literal text between holes, escapes already resolved (as in
652    /// [`bynk_syntax::ast::InterpPart::Chunk`]) — an emitter must escape it
653    /// for its own target string syntax, as `escape_ts_template` does today.
654    Chunk(String),
655    /// An interpolated expression, already lowered.
656    Hole(Box<IrExpr>),
657}
658
659/// P6.6's real declaration IR (`design/bynk-greenfield-compiler.md` §6.6,
660/// #1161) — a top-level declaration's own shape, the payload
661/// `bynk_lower::lower_type_item_ir`/`bynk_lower::lower_fn_item_ir` construct.
662/// Identity is adapted per this module's own "no arena" substitution
663/// (`DefId -> Arc<TypeDecl>`/`Arc<FnDecl>`, the same substitution
664/// `Record`/`GlobalRef` already made).
665///
666/// **`Type`, `Fn`, `Agent` (P6.10, #1169), `Service` (P6.11, #1171),
667/// `Capability` (P6.12, #1173) and `Provider` (P6.14, #1174) exist as
668/// variants.** `Actor` is still deferred (Decision D, #1161, matching the
669/// issue's own title: "Agent/Service/Actor/Capability/Provider deferred").
670/// This is a different posture from
671/// `Match`'s own payload (`IrPat`/`IrArm`/`Exhaustive`/`MatchForm`), which
672/// had to exist — even genuinely uninhabited — the moment
673/// `IrExprKind::Match`'s own field list was written, because `IrExprKind`
674/// is one enum whose whole Part 6.2 shape landed in a single slice (P6.1)
675/// and never grows a new variant after. `IrItem` carries no such
676/// constraint: nothing outside this module matches on it exhaustively yet
677/// (no consumer at all, same posture as every prior P6.x slice), so a
678/// later slice can add a wholly new variant rather than needing a
679/// placeholder reserved here in advance. Each deferred variant has its own
680/// real, distinct blocker, not a shared "later" (full grounding in #1161's
681/// own Decision D — `Service`'s own two blockers, closed by #1170/#1171,
682/// are recorded on `IrItem::Service`'s own doc comment rather than kept
683/// here as a stale bullet):
684/// - `Actor` — #1172's own investigation settled this one as a **non-build
685///   decision**, not an unbuilt gap: the reference's own sketch
686///   (`Actor { def, scheme: AuthScheme, identity: Option<TyId>, claims:
687///   Option<IrExpr> }`, `bynk-greenfield-compiler.md:1133`) is the wrong
688///   shape for this codebase, on three independent findings. (1) `claims:
689///   Option<IrExpr>` is unbuildable, not merely unbuilt: an actor
690///   refinement's own predicate (`hasClaim`/`claimEquals`,
691///   `ActorRefinement::predicate`) is validated only *structurally*
692///   (`bynk-check/src/actors.rs`'s `parse_claim_predicate`) — no `Callee`,
693///   no `expr_types` entry, no typing at all, because claims are
694///   deliberately untyped JSON (`context_checks.rs`'s own
695///   `refinement_predicate_unsupported` note) and lowered straight to a JS string by
696///   `claim_predicate_to_js`. An `IrExpr` here would mean inventing a
697///   typing for a surface the checker deliberately never gave one. (2)
698///   `scheme: AuthScheme` names a type the reference defines nowhere else
699///   (`:1133`/`:1847` only) — the same "referenced, not specified" gap
700///   `Capability`'s own `OpSig` carried before [`OpSig`] settled it (P6.12,
701///   #1173), except here a real 5-variant candidate already ships
702///   (`bynk-check::actors::Scheme`), so this piece alone is specifiable.
703///   (3) The decisive finding: every real consumer of actor data — the
704///   five `bynk-check` seam resolvers the shipped emitter
705///   consumes (`bearer_seam_for`/`oidc_seam_for`/`signature_seam_for`/
706///   `sum_members_for`/`caller_binder_for`, `bynk-check/src/actors.rs`) and
707///   the reference's own R8.11 (`deps` derivation)/R8.13 (boundary-wrapper
708///   verification) — is **handler-keyed, not declaration-keyed**: binder
709///   presence, sum-member ordering, and which scheme's config applies are
710///   all facts about a handler's own `by` clause, not the `actor`
711///   declaration in isolation. This is exactly why the two actor-shaped IR
712///   additions that *did* have a real consumer ([`ActorBinder`], and
713///   `IrHandler`'s own `actors: Vec<String>`, review of #1180) both landed
714///   on [`IrHandler`], not a new `IrItem` variant — a `{def, scheme,
715///   identity}` record beside `IrItem::Service`'s handlers would have zero
716///   consumers and would not discharge R6.13 for actors at all, the "carve
717///   when a dependency arrives, not on appetite" anti-pattern R10.3 already
718///   argues against elsewhere in this track. Revisit only if a real
719///   phase-7 printer needs declaration-level actor data beyond what
720///   `IrHandler::binder`/`IrHandler::actors` already carry — not before.
721/// - `Provider` — #1174's own investigation found this one buildable this
722///   slice, unlike `Actor`: the real `ProviderDecl`
723///   (`bynk-syntax/src/ast.rs:582-600`) already carries the exact
724///   `Bynk`/`External` dispatch the reference's own `// Bynk ops |
725///   External(module)` comment names (never a real Rust enum in the
726///   document itself — the same "referenced, not specified" gap
727///   `Capability`'s own `OpSig` carried before #1173 settled it), via
728///   `external: bool` plus `ops: Vec<ProviderOp>` (empty exactly when
729///   `external` is `true`, per the field's own doc comment,
730///   `bynk-syntax/src/ast.rs:592-595`). And unlike `CapabilityOp`, a
731///   `ProviderOp`'s own body is a **real, fully type-checked** handler
732///   body — `check_provider_decls`
733///   (`bynk-check/src/context_checks.rs:626`) pushes every op through
734///   `checker::check_handler_body` (`:667`) with the provider's own
735///   `given` as its capability scope and no `self`/state/store binding at
736///   all (`HandlerBodyCheck::new`'s own "everything optional empty"
737///   default, `bynk-check/src/checker.rs:1055`) — so
738///   `bynk_lower::lower_provider_op_ir` mirrors `bynk_lower::lower_fn_body_ir`
739///   (ADR 0334 panic-on-miss, not `bynk_lower::lower_op_sig_ir`'s lenient
740///   `Ty::Unit` fallback: this body's types are checker-guaranteed to
741///   resolve, same as a fn's) with no receiver/self and no rigid type
742///   variables (`ProviderOp` carries none of its own,
743///   `bynk-syntax/src/ast.rs:604-611`). [`ProviderBody::Bynk`]'s own `ops:
744///   Vec<ProviderOpIr>` is real per-op signature-plus-body, one
745///   `ProviderOpIr` per `bynk_lower::lower_provider_op_ir` call, in
746///   declaration order.
747///
748///   **`given` is carried, `module` is not — the two omissions are not
749///   symmetric** (review of #1186). R8.1's own `Provider{Bynk}` row is
750///   `export class` with a **`deps` constructor**
751///   (`bynk-greenfield-compiler.md:1309`), built today straight off
752///   `ProviderDecl::given` (`bynk-emit/src/project.rs:2608-2620`) — unlike
753///   `module`, `given` is not one-phase-up data, it lives on the very
754///   `ProviderDecl` this function already holds, and it is not
755///   reconstructible from the lowered op bodies alone: an unused `given`
756///   capability, a cross-context `given B.Cap` prefix (`CapRef::context`,
757///   v0.15), and declaration order would all silently vanish if this
758///   variant carried only `ops`. So [`ProviderBody::Bynk`] also carries
759///   `given: Vec<CapRefIr>` — [`CapRefIr`] adapts `CapRef` under this
760///   module's usual "no arena, bare name" substitution (`context`'s own
761///   `QualifiedName` flattened via `.joined()`), leaving *resolving* a
762///   prefix against `consumes`/aliases to whichever future project-level
763///   pass also resolves `module` — `bynk_lower::lower_provider_item_ir` does
764///   no more than `bynk_lower::lower_capability_item_ir` already declines to
765///   do for a capability op's own type refs. [`ProviderBody::External`]
766///   deliberately carries **no** `module` field, despite the reference's
767///   own parenthetical —
768///   that name lives one phase up from this per-context `CheckedProgram`,
769///   on the adapter's own `binding "<module>"` clause
770///   (`BindingDecl::module`, `bynk-syntax/src/ast.rs:186`), resolved only
771///   by the whole-project pass into `adapter_bindings: HashMap<String,
772///   AdapterBinding>` (`bynk-check/src/project_model.rs:729`) — data no
773///   other `lower_X_item_ir` function in this module reaches either.
774///   Nothing needs it yet: R8.1's own emission table already gives
775///   `Provider{External}` "nothing — the binding module supplies it"
776///   (`bynk-greenfield-compiler.md:1310`), i.e. even phase 7's own per-item
777///   printer does not consume a module name — only the still-AST-driven
778///   `project.rs`'s existing import wiring does
779///   (`bynk-emit/src/project.rs:2675-2678`), untouched by this slice.
780///   Revisit only once a real project-level IR/linking phase gives this
781///   module something to thread the module name through.
782#[derive(Debug, Clone)]
783pub enum IrItem {
784    /// A `type` declaration. `shape` covers all three real [`TypeShape`]
785    /// forms. P6.39: `def: Arc<TypeDecl>` dropped — its one production
786    /// consumer (`emitter.rs`'s `type_shape_for` call site) already ignored
787    /// it via `..`, and no other reader existed.
788    Type { shape: TypeShape },
789    /// A `fn` declaration — free function or method alike (`FnName::Free`/
790    /// `FnName::Method`); which `IrItem::Fn`s a future printer re-attaches
791    /// under which `IrItem::Type`'s own namespace (R8.1) is phase 7's own
792    /// concern, not decided here. P6.39: `def: Arc<FnDecl>` dropped —
793    /// `lower_fn_item_ir` (the one constructor) has no production call site
794    /// at all today; every other field remains, so the constructor and this
795    /// variant both survive, just without the raw declaration.
796    Fn {
797        /// The method receiver's own type, generic in the owning type's own
798        /// rigid variables (e.g. `Box[A]`'s `self` is `Ty::Named { name:
799        /// "Box", args: [Ty::Var("A")], .. }`) — `None` for a free function.
800        /// **Not** in `params`: `self` is never in `f.params` either
801        /// (`FnDecl::has_self` gates it, mirrored by
802        /// `bynk_lower::lower_fn_body_ir`'s own binding), but `body` still
803        /// references it as `Local { name: "self" }` when `has_self` is
804        /// true — a consumer that walks `body` needs this field to know
805        /// what that bare name resolves to, rather than re-deriving the
806        /// generic-receiver type itself from `def`.
807        receiver: Option<TyId>,
808        /// Adapts the reference's own bare `Vec<LocalId>` to `Vec<(String,
809        /// TyId)>` (Decision E, #1161) — no arena exists to look a param's
810        /// type back up from its name alone once this `IrItem` outlives the
811        /// `LowerIrCtx` that resolved it, unlike every other Decision-B
812        /// "name only" substitution in this module, where the type is
813        /// either implied by context or carried alongside on the same node
814        /// (e.g. `IrExpr::ty`).
815        params: Vec<(String, TyId)>,
816        ret: TyId,
817        /// `bynk_lower::lower_fn_body_ir`'s own return value, unchanged
818        /// (#1141) — this constructor adds no further transformation.
819        body: IrExpr,
820        /// Derived once from `ret`'s structural shape (`Ty::Fn`'s own doc
821        /// comment, `bynk-check/src/checker.rs`: effectful iff `ret` is
822        /// `Effect[_]`), never threaded from anywhere else — the same
823        /// single-source-of-truth discipline this module already follows
824        /// for [`BindingMode`]/[`Exhaustive`].
825        effectful: bool,
826    },
827    /// P6.10's real `IrItem::Agent` (Part 6.6, R6.13, #1169) —
828    /// `bynk_lower::lower_agent_item_ir`'s own return value, assembling every
829    /// P6.7–P6.9 ingredient (`StoreFieldIr`, `CommitShape`/`IrPredicate`,
830    /// `IrHandler`) that had no `IrItem` variant to land in until now.
831    /// Matches the reference's own sketch
832    /// (`bynk-greenfield-compiler.md:1129-1131`) field-for-field, under this
833    /// module's already-established substitutions.
834    Agent {
835        /// The agent's own declared name — this module's usual "no arena"
836        /// substitution for the reference's own `DefId`, but *not* the same
837        /// substitution [`IrItem::Type`]/[`IrItem::Fn`] made
838        /// (`Arc<TypeDecl>`/`Arc<FnDecl>`): unlike `types`/`fns`,
839        /// `TypedCommons` carries no `Arc`-wrapped agents table to source a
840        /// cheap pointer from (`UnitTable::agents`/`CommonsItem::Agent` are
841        /// both a plain, owned `AgentDecl`), and `Callee::Agent { agent:
842        /// String, handler: String }` already establishes bare-name
843        /// identity as sufficient for this checked output — a full
844        /// `AgentDecl` clone (its own handler bodies included) would be
845        /// needless weight this module's other `String`-identity fields
846        /// (`GlobalRef::tag`, `Callee::Store::field`, …) don't carry either.
847        def: String,
848        /// `key id: Type` adapted from the reference's own bare `(LocalId,
849        /// TyId)` (Decision B extended): the bound name plus its resolved
850        /// type, the same `Vec<(String, TyId)>`-per-entry substitution
851        /// [`IrItem::Fn::params`] already made for a `Vec<LocalId>`.
852        key: (String, TyId),
853        /// Every `store` field, `bynk_lower::lower_store_field_ir`'s own
854        /// return value, in declaration order.
855        state: Vec<StoreFieldIr>,
856        /// Every `on call` handler, `bynk_lower::lower_handler_ir`'s own
857        /// return value, in declaration order.
858        handlers: Vec<IrHandler>,
859        /// Every invariant, `bynk_lower::lower_invariant_ir`'s own return
860        /// value, in declaration order — the same already-lowered list each
861        /// `handlers` entry's own `commit: CommitShape::Transactional`
862        /// (when it is one) carries a copy of, not a fresh lowering.
863        invariants: Vec<IrPredicate>,
864        /// Every transition, `bynk_lower::lower_transition_ir`'s own return
865        /// value, in declaration order — same relationship to `handlers` as
866        /// `invariants`.
867        transitions: Vec<IrPredicate>,
868    },
869    /// P6.11's real `IrItem::Service` (Part 6.6, R6.13, #1171) —
870    /// `bynk_lower::lower_service_item_ir`'s own return value, the sibling
871    /// assembly to [`IrItem::Agent`]. Matches the reference's own sketch
872    /// (`bynk-greenfield-compiler.md:1132`) field-for-field. Closes the two
873    /// blockers this variant carried while deferred: a real service
874    /// `IrHandler` needed the `binder`-persistence `bynk-check` change
875    /// #1170 made, and the CORS/security-headers/request-body-size policy
876    /// surface the reference sketch never shows at all now has a real,
877    /// specified shape ([`ProtocolIr`]/[`PolicyIr`]).
878    Service {
879        /// The service's own declared name — same reasoning as
880        /// `IrItem::Agent`'s own `def`: `UnitTable::services` is a plain
881        /// owned `HashMap<String, ServiceDecl>`, no `Arc` to borrow
882        /// cheaply, and bare-name identity is already sufficient for this
883        /// checked output.
884        def: String,
885        /// `bynk_lower::lower_protocol_ir`'s own return value.
886        protocol: ProtocolIr,
887        /// Every handler, `bynk_lower::lower_service_handler_ir`'s own return
888        /// value, in declaration order.
889        handlers: Vec<IrHandler>,
890        /// `bynk_lower::lower_policy_ir`'s own return value — `None` whenever
891        /// `protocol` is not [`ProtocolIr::Http`], not just when the source
892        /// declares none of `cors`/`security`/`limits`. See [`PolicyIr`]'s
893        /// own doc comment for why this is `Option`, not the reference's
894        /// own unconditional `PolicyIr` field.
895        policy: Option<PolicyIr>,
896    },
897    /// P6.12's real `IrItem::Capability` (Part 6.6, R6.13, #1173) —
898    /// `bynk_lower::lower_capability_item_ir`'s own return value. Matches the
899    /// reference's own sketch (`bynk-greenfield-compiler.md:1134`)
900    /// field-for-field, once [`OpSig`] fills in the type the sketch names
901    /// but never defines.
902    Capability {
903        /// The capability's own declared name — same reasoning as
904        /// `IrItem::Agent`/`IrItem::Service`'s own `def`:
905        /// `UnitTable::capabilities` is a plain owned `HashMap<String,
906        /// CapabilityDecl>`, no `Arc` to borrow cheaply, and bare-name
907        /// identity is already sufficient — `Callee::Capability { cap:
908        /// String, op: String }` (`bynk-check/src/checker.rs`) already
909        /// resolves a same-context capability call by name alone.
910        def: String,
911        /// Every operation, `bynk_lower::lower_op_sig_ir`'s own return value,
912        /// in declaration order.
913        ops: Vec<OpSig>,
914    },
915    /// P6.14's real `IrItem::Provider` (Part 6.6, R6.13, #1174) —
916    /// `bynk_lower::lower_provider_item_ir`'s own return value. Matches the
917    /// reference's own sketch (`bynk-greenfield-compiler.md:1135`)
918    /// field-for-field, once [`ProviderBody`] fills in the type the
919    /// sketch's own comment names but never defines as a real Rust enum.
920    Provider {
921        /// The provider's own declared name (`ProviderDecl::provider_name`,
922        /// "used in tests/config to select impls",
923        /// `bynk-syntax/src/ast.rs:586`) — distinct from `cap` below even
924        /// though `UnitTable::providers` happens to key by capability name
925        /// today ("one provider per capability in v0.5",
926        /// `bynk-check/src/symbols.rs`): the reference's own sketch keeps
927        /// `def`/`cap` as two separate `DefId`s, and a provider's own
928        /// identity is never actually the capability it implements. Same
929        /// "no arena, bare name is enough" substitution as
930        /// `IrItem::Agent`/`Service`/`Capability`'s own `def`.
931        def: String,
932        /// The capability this provider implements
933        /// (`ProviderDecl::capability`) — same bare-name substitution as
934        /// `def`; resolves against `Callee::Capability`'s own `cap: String`
935        /// the identical way `IrItem::Capability::def` does.
936        cap: String,
937        /// `bynk_lower::lower_provider_item_ir`'s own `Bynk`/`External`
938        /// dispatch, read straight off `ProviderDecl::external`.
939        body: ProviderBody,
940    },
941}
942
943/// P6.14's real `ProviderBody` ([DECISION A], #1174) — referenced by the
944/// reference's own `IrItem::Provider` sketch
945/// (`bynk-greenfield-compiler.md:1135`, `body: ProviderBody // Bynk ops |
946/// External(module)`) but never defined anywhere in the document as a real
947/// Rust type, the same "referenced, not specified" gap `Capability`'s own
948/// `OpSig` carried before #1173 settled it. Mirrors `ProviderDecl::external`
949/// (`bynk-syntax/src/ast.rs:592-595`) exactly: `true` (no brace block, an
950/// adapter-supplied binding) becomes `External`, `false` (a real Bynk-
951/// authored implementation) becomes `Bynk`. See [`IrItem`]'s own doc
952/// comment for why `External` carries no `module` field despite the
953/// reference's own parenthetical, and why it carries `given` instead of
954/// omitting that too.
955#[derive(Debug, Clone)]
956pub enum ProviderBody {
957    /// A real Bynk-authored implementation.
958    Bynk {
959        /// `ProviderDecl::given`, adapted to [`CapRefIr`] — needed to build
960        /// R8.1's own `Provider{Bynk}` `deps` constructor, and not
961        /// reconstructible from `ops` alone ([`IrItem`]'s own doc comment
962        /// has the full grounding, review of #1186). In declaration order.
963        given: Vec<CapRefIr>,
964        /// Every operation, `bynk_lower::lower_provider_op_ir`'s own return
965        /// value, in declaration order. **Not** guaranteed non-empty
966        /// (review of #1186): `external` is set purely by brace-block
967        /// *absence*, not emptiness (`bynk-syntax/src/parser/declarations.rs:1795-1811`),
968        /// so `provides Cap = P {}` also lowers here, with `ops: vec![]` —
969        /// the `bynk.provider.missing_operation` check that would reject
970        /// it is a whole-project pass (`bynk-check/src/project_model.rs:2468`),
971        /// not part of `check_provider_decls`, so a bare `CheckedProgram`
972        /// (this function's only input) is certified without it.
973        ops: Vec<ProviderOpIr>,
974    },
975    /// `provides Cap = Name` with no brace block — the adapter's own
976    /// binding supplies the implementation; the emitter produces no class
977    /// (`bynk-greenfield-compiler.md:1310`, `Provider{External} | nothing`).
978    /// Carries `given` too (added #1187's Provider `given`/deps-wiring
979    /// slice, correcting a real gap this bare-unit shape left): an external
980    /// provider's own `given` clause is populated the same way `Bynk`'s is
981    /// (`ProviderDecl::given` is not gated on `external` anywhere in the
982    /// grammar or checker) and `instantiate_provider_ts_expr`
983    /// (`bynk-emit/src/project.rs`) needs it to build an external
984    /// provider's own `deps` constructor argument — this variant's own doc
985    /// comment already claimed `given` "lowers unconditionally" (P6.14's
986    /// own review of #1186) before this fix actually made that true.
987    External { given: Vec<CapRefIr> },
988}
989
990/// P6.14's real `CapRefIr` ([DECISION A], #1174, review of #1186) — one
991/// `bynk_syntax::ast::CapRef` entry of a provider's own `given` clause,
992/// under this module's usual "no arena, bare name" substitution:
993/// `context: Option<QualifiedName>` flattens to `Option<String>` via
994/// `QualifiedName::joined()` (the same `.`-joined form
995/// `resolve_consume_prefix` — `bynk-emit/src/project.rs` — already
996/// resolves against `consumes`/aliases), and `name: Ident` flattens to
997/// `String`, mirroring every other bare-name identity field in this module.
998/// Deliberately **not** resolved further here: which context a `Some`
999/// prefix actually names is whole-project `consumes`/alias data, the same
1000/// phase boundary [`IrItem`]'s own doc comment already names for
1001/// `ProviderBody::External`'s missing `module` field — this type only
1002/// preserves what `CapRef` itself carries, unresolved.
1003#[derive(Debug, Clone)]
1004pub struct CapRefIr {
1005    pub context: Option<String>,
1006    pub name: String,
1007}
1008
1009/// P6.14's real `ProviderOpIr` ([DECISION A], #1174) — one `ProviderOp`
1010/// (`bynk-syntax/src/ast.rs:604-611`), signature *and* body, unlike
1011/// [`OpSig`]'s signature-only shape: a `CapabilityOp` never carries a body
1012/// (a capability is a contract), but a `ProviderOp` always does. No
1013/// `type_params`, unlike `OpSig`: `ProviderOp` carries none of its own
1014/// (#1173's own `OpSig::type_params` doc comment names the same absence for
1015/// `CapabilityDecl`, but a capability op's `[T, …]` (#926) has no
1016/// provider-op equivalent — nothing in the grammar or the checker's own
1017/// `check_provider_decls` gives a provider op a `[T, …]` list to parse).
1018#[derive(Debug, Clone)]
1019pub struct ProviderOpIr {
1020    pub name: String,
1021    pub params: Vec<(String, TyId)>,
1022    pub return_ty: TyId,
1023    /// `bynk_lower::lower_provider_op_ir`'s own return value — a real,
1024    /// checker-guaranteed-to-resolve body (see [`IrItem`]'s own doc
1025    /// comment for why this differs from [`OpSig`]'s lenient treatment).
1026    pub body: IrExpr,
1027}
1028
1029/// P6.12's real `OpSig` ([DECISION A], #1173) — referenced by the
1030/// reference's own `IrItem::Capability` sketch
1031/// (`bynk-greenfield-compiler.md:1134`, `ops: Vec<OpSig>`) but never
1032/// defined anywhere in the document — the same "referenced, not specified"
1033/// gap #1161's own Decision C named for [`EmbedIr`], #1163's own Decision C
1034/// named for [`IndexIr`], #1165's own Decision A named for [`IrPredicate`],
1035/// and #1167's own Decision A named for [`ActorBinder`]. Adapted from
1036/// `bynk_syntax::ast::CapabilityOp` — a signature only, no body — under
1037/// this module's already-established "no arena" substitutions: `params:
1038/// Vec<(String, TyId)>` mirrors [`IrItem::Fn::params`] exactly (Decision E,
1039/// #1161), and `type_params: Vec<String>` mirrors the checker's own
1040/// already-resolved `CapabilityOpInfo::type_params`
1041/// (`bynk-check/src/checker.rs`) — a bare rigid-variable name, not a
1042/// `TypeParam` AST node, since nothing here re-derives bounds a capability
1043/// op's own `[T, …]` list never carries in the first place (#926).
1044/// `bynk_lower::lower_op_sig_ir` resolves `params`/`return_ty` in the scope
1045/// `type_params` names, mirroring `context_checks::build_capability_op_info`'s
1046/// own `vars` treatment (`bynk-check/src/context_checks.rs`) so a generic
1047/// op's own `T` survives as `Ty::Var("T")` rather than collapsing to
1048/// `Ty::Unit`. On a genuinely unresolvable name a `params`/`return_ty` entry
1049/// *is* `Ty::Unit`, deliberately — see `bynk_lower::lower_op_sig_ir`'s own doc
1050/// comment for why that mirrors the checker's own fallback rather than
1051/// panicking.
1052#[derive(Debug, Clone)]
1053pub struct OpSig {
1054    pub name: String,
1055    /// The op's own type parameters (#926) — empty for a non-generic op.
1056    /// Scoped to the op itself, not the capability: `CapabilityDecl` carries
1057    /// no `type_params` of its own (`bynk-syntax/src/ast.rs:556-562`), so
1058    /// this is never merged with anything above it, unlike
1059    /// [`IrItem::Fn::receiver`]'s generic-receiver treatment for a method.
1060    pub type_params: Vec<String>,
1061    pub params: Vec<(String, TyId)>,
1062    pub return_ty: TyId,
1063}
1064
1065/// P6.18: a `fn`'s own resolved signature, with no `body` and no `receiver` —
1066/// the narrow, [`OpSig`]-shaped sibling a *foreign* unit's attached method
1067/// needs when only its signature will ever be rendered (`emit_attached_
1068/// methods`' delegating forward at `bynk_emit::emitter::emit::emit_forwarded_methods`),
1069/// never its body. Deliberately not [`IrItem::Fn`]: that variant
1070/// mandates lowering `body: IrExpr` through `bynk_lower::lower_fn_body_ir`,
1071/// which still `todo!()`s on `ExprKind::Question`/`Is` (design/tracks/
1072/// the-ir.md §6's own P6.3 correction) — forcing every `uses`-imported
1073/// method through that gate for a signature nothing here ever reads would
1074/// make this reader strictly less total than the raw-`TypeRef` code it
1075/// replaces, for zero benefit. `bynk_lower::lower_fn_sig_ir_from_types` resolves
1076/// `params`/`return_ty` in the scope the method's own `[T, …]` list names
1077/// (mirroring `bynk_lower::lower_op_sig_ir_from_commons`'s identical `type_params`
1078/// treatment) — a genuinely unresolvable name degrades to `Ty::Unit`,
1079/// deliberately, the same non-panicking posture `OpSig` already established:
1080/// nothing checker-side actually validates an attached method's own
1081/// `params`/`return_type` against the *importing* context's own visible
1082/// types (only the declaring commons' own checking does), so a resolve miss
1083/// here is an expected, not exceptional, state.
1084#[derive(Debug, Clone)]
1085pub struct FnSig {
1086    pub name: String,
1087    pub has_self: bool,
1088    pub params: Vec<(String, TyId)>,
1089    pub return_ty: TyId,
1090}
1091
1092/// P6.11's real `ProtocolIr` ([DECISION A], #1171) — one variant per
1093/// `bynk_syntax::ast::ServiceProtocol` variant, `bynk_lower::lower_protocol_ir`'s
1094/// own return value. The reference's own sketch specifies only two of the
1095/// six: `Events { event, pattern, schema_dispatch }`
1096/// (`bynk-greenfield-compiler.md:1881`) and `WebSocket { in_ty, out_ty }`
1097/// (`:1959`) — field names taken verbatim from those two rows. `Call`/
1098/// `Http`/`Cron` carry no payload, not because one was dropped: the actual
1099/// per-trigger binding (a route, a schedule) lives on each *handler*
1100/// (`HandlerKind::Http { method, path }`/`Cron { expr }`), already
1101/// reachable through `IrHandler::kind` — `ServiceProtocol`'s own doc
1102/// comment says this in as many words ("the endpoint lives on each
1103/// handler"), which is why the reference never spells these three out
1104/// either. E2 (`:1737`) constrains the *set*, not the shape: "a closed
1105/// nominal set … grows one variant per real trigger" — the AST's own closed
1106/// `ServiceProtocol` already satisfies that exactly, so this type is total
1107/// over what a certified program's own service can declare, the same claim
1108/// [`StoreKindIr`]'s own doc makes about `Queue` being gated pre-`certify`.
1109#[derive(Debug, Clone)]
1110pub enum ProtocolIr {
1111    Call,
1112    Http,
1113    Cron,
1114    /// `from queue("name")`.
1115    Queue {
1116        name: String,
1117    },
1118    /// `from websocket(in: …, out: …)` — the two frame types, resolved.
1119    /// As of P6.13 (#1179), `bynk_lower::lower_service_handler_ir` lowers an
1120    /// `on open`/`on message`/`on close` handler's own *body* for a
1121    /// service carrying this protocol too — see [`ConnectionBinder`]'s own
1122    /// doc comment for how the synthetic `connection` binding reaches that
1123    /// body's scope.
1124    WebSocket {
1125        in_ty: TyId,
1126        out_ty: TyId,
1127    },
1128    /// `from Events(E)` — the subscribed event type, resolved, plus the
1129    /// two independent, optional filters a subscription may carry.
1130    Events {
1131        event: TyId,
1132        pattern: Option<EventPatternIr>,
1133        /// P6.40 (design/tracks/the-ir.md §6a): flattened to `Option<i64>` —
1134        /// `SchemaVersionPattern` has exactly one variant (`Literal(i64)`),
1135        /// so once a real consumer needed to match on it
1136        /// (`emitter/emit.rs`'s `via schema(N)` guard prologue), mirroring
1137        /// the single-payload `i64` directly was simpler than introducing a
1138        /// one-variant IR-native enum purely to re-wrap it. A future range
1139        /// pattern (`via schema(2..)`) widens this field's own shape when it
1140        /// lands, not before. The `SchemaDispatch` wrapper itself stays
1141        /// dropped — it carried only this `pattern` plus a parse-only
1142        /// `span`.
1143        schema_dispatch: Option<i64>,
1144    },
1145}
1146
1147/// #1226/#1187 slice 6: the two facts a service's own event-subscriber
1148/// *shape* needs, captured at that unit's own check time (its
1149/// `CheckedProgram` does not survive past `check_unit_files`'s per-file loop)
1150/// so a *different* unit's own composition root can later decide whether its
1151/// subscriber to this service wants the event envelope forwarded, without
1152/// re-walking this unit's raw `UnitTable`. Pure syntax, zero `TyId`
1153/// dependency. Produced by `bynk_lower::lower_event_subscriber_shapes_ir`, sized
1154/// like #1187's own `unit_callees` (#1202) accumulator.
1155#[derive(Debug, Clone, Copy, Default)]
1156pub struct EventSubscriberShape {
1157    pub two_param_handler: bool,
1158    pub schema_dispatch: bool,
1159}
1160
1161/// The payload of `ProtocolIr::Events`'s own `pattern` — a `from
1162/// Events(E { field: value, .. })` structural filter, [DECISION C] (#1171).
1163/// **Not** `bynk_syntax::ast::EventPattern` reused verbatim, unlike
1164/// `SchemaVersionPattern`: that type carries `rest_span` (a parse artefact
1165/// for the grammar-required trailing `..`, giving a later reader nothing to
1166/// act on) and its own `EventPatternValue::Variant` is an *unresolved,
1167/// optionally-qualified* name pair — exactly the shape this module's whole
1168/// posture rejects everywhere else.
1169#[derive(Debug, Clone)]
1170pub struct EventPatternIr {
1171    /// `(field name, matched value)`, in source order — no dedicated
1172    /// `EventPatternFieldIr` struct, mirroring `IrPat::Variant`'s own
1173    /// `fields: Vec<(String, Box<IrPat>)>` precedent for a two-part fact
1174    /// with no further structure.
1175    pub fields: Vec<(String, EventPatternValueIr)>,
1176}
1177
1178/// One [`EventPatternIr`] field's own matched value.
1179#[derive(Debug, Clone)]
1180pub enum EventPatternValueIr {
1181    /// Reuses [`ConstVal`] for the closed `Int`/`Str`/`Bool` literal set —
1182    /// verbatim the same reuse `IrPat::Const` already made for
1183    /// `Pattern::Literal`'s identical closed set.
1184    Const(ConstVal),
1185    /// A nullary sum-variant tag, resolved and unqualified — bare
1186    /// `tag: String` mirrors `IrPat::Variant`'s own `tag`/[`GlobalRef`]'s
1187    /// own `tag`. The AST's own optional qualifying `type_name` is
1188    /// dropped, not lost: the sole consumer
1189    /// (`bynk_emit::emitter::lower::event_pattern_guard_ir`, #1187's slice 5)
1190    /// already destructures down to the bare tag alone — the qualification
1191    /// is disambiguation for the *checker*, resolved against the field's
1192    /// declared sum type before this point.
1193    Variant { tag: String },
1194}
1195
1196/// P6.11's real `PolicyIr`/`CorsIr`/`SecurityIr` ([DECISION D], #1171) —
1197/// the interpreted (not passed-through) form of a `from http` service's
1198/// `cors`/`security`/`limits` blocks, `bynk_lower::lower_policy_ir`'s own
1199/// return value. The reference names `PolicyIr` once, in the
1200/// `IrItem::Service` sketch itself (`:1132`), and never elsewhere — the
1201/// same "referenced, not specified" gap every trailing struct in this
1202/// module has carried before its own slice specified it.
1203///
1204/// **Interpreted, not passed through** — the one discipline this struct
1205/// exists to enforce. Every AST policy type
1206/// (`bynk_syntax::ast::CorsPolicy`/`SecurityPolicy`/`LimitsPolicy`) stores
1207/// raw, unvalidated `{name, value: Expr}` pairs; meaning only exists
1208/// through each type's own already-shipped typed accessor
1209/// (`CorsPolicy::origins()`/`credentials()`/`allow_headers()`/
1210/// `max_age_secs()`, `SecurityPolicy::nosniff()`/`hsts_max_age_secs()`,
1211/// `LimitsPolicy::max_body()`) — carrying the raw AST struct here instead
1212/// would embed exactly the kind of unresolved value this whole track
1213/// (R6.13) exists to remove from the IR. The shipped emitter already never
1214/// reads a policy's own `.fields` directly (`emitter/workers_entry.rs`),
1215/// so this is that emitter's own already-established reading of the AST,
1216/// moved upstream, not a new interpretation invented here.
1217#[derive(Debug, Clone)]
1218pub struct PolicyIr {
1219    pub cors: Option<CorsIr>,
1220    /// **Not `Option`** — unlike `cors`/`limits`, `security: None` on the
1221    /// AST means *defaults* (`nosniff` on), not *no headers* (ADR 0164
1222    /// Decision A) — keeping this `Option` here would re-export exactly
1223    /// the ambiguity this struct exists to remove. The `None`-source arm
1224    /// of `bynk_lower::lower_policy_ir` materialises `SecurityIr { nosniff:
1225    /// true, hsts_max_age_secs: None }` — the emitter's own already-shipped
1226    /// default, verbatim, not invented here.
1227    pub security: SecurityIr,
1228    /// `LimitsPolicy::max_body()`'s own return value, flattened directly —
1229    /// no dedicated `LimitsIr` struct. `LimitsPolicy` has exactly one
1230    /// accessor; a one-field wrapper would carry no more information than
1231    /// this `Option<i64>` itself, the same "no further structure"
1232    /// precedent [`EmbedIr`]/[`IndexIr`] already set.
1233    pub max_body_bytes: Option<i64>,
1234}
1235
1236/// The payload of `PolicyIr::cors` — present only when the source declares
1237/// a `cors { }` block at all (ADR 0159's own opt-in posture); the
1238/// asymmetry with `PolicyIr::security` is this struct's load-bearing
1239/// content, not an inconsistency.
1240#[derive(Debug, Clone)]
1241pub struct CorsIr {
1242    pub origins: Vec<String>,
1243    pub credentials: bool,
1244    /// The author's own explicit `headers:` override, kept `Option` rather
1245    /// than materialising the emitter's own smart default (`content-type`
1246    /// and `authorization` when a `Bearer`-authed route exists,
1247    /// `emitter/workers_entry.rs`) — that default reads *route* facts
1248    /// (whether any handler's actor is `Bearer`/OIDC-scheme), not policy
1249    /// facts, the same reason `StoreFieldIr::init` stays `Option` rather
1250    /// than a materialised zero value.
1251    pub allow_headers: Option<Vec<String>>,
1252    pub max_age_secs: Option<i64>,
1253}
1254
1255/// The payload of `PolicyIr::security` — always present for an HTTP
1256/// service (see `PolicyIr::security`'s own doc comment).
1257#[derive(Debug, Clone)]
1258pub struct SecurityIr {
1259    pub nosniff: bool,
1260    pub hsts_max_age_secs: Option<i64>,
1261}
1262
1263/// #1228's own `CacheIr` — a GET handler's own `@cache(maxAge:, scope:)`
1264/// freshness policy, interpreted the same "raw `{name, value: Expr}` pairs
1265/// mean nothing on their own" discipline [`PolicyIr`]'s own doc comment
1266/// argues for, but **deliberately not a `PolicyIr` field**. `PolicyIr`
1267/// itself is built only by `bynk_lower::lower_policy_ir`, which only
1268/// `bynk_lower::lower_service_item_ir` calls — and nothing in the shipped
1269/// emitter constructs a real `IrItem::Service` yet (every call site is this
1270/// module's own test suite). Nesting `CacheIr` under `PolicyIr` would land
1271/// it inert: real in `bynk_lower`'s own tests, with zero effect on any
1272/// emitted route. `@cache` is also handler-scoped, not service-scoped
1273/// (`PolicyIr`'s whole shape), so it was never a natural fit regardless.
1274/// `bynk_lower::lower_route_cache_ir` is a standalone reader instead, wired
1275/// directly into `emitter/workers_entry.rs`'s own route construction — the
1276/// same live, standalone-consumer shape `lower_protocol_ir`/
1277/// `lower_handler_given_ir`/`lower_actor_seam_ir` already established.
1278#[derive(Debug, Clone)]
1279pub struct CacheIr {
1280    /// `maxAge` in whole seconds (the `Cache-Control: max-age`).
1281    pub max_age_secs: i64,
1282    /// `"public"` or `"private"` — defaults to `"private"` so a *shared*
1283    /// cache never stores unless the author opts into `public`. Bare
1284    /// `&'static str`, not an enum: mirrors `emitter/workers_entry.rs`'s
1285    /// own former `CachePolicy::scope` shape verbatim (the two literal
1286    /// values this route's own generated `Cache-Control` header ever
1287    /// spells), not a new representation invented here — `CachePolicy`
1288    /// itself is gone, superseded by this struct (#1228).
1289    pub scope: &'static str,
1290}
1291
1292/// P6.6's real `TypeShape` (Part 6.6, #1161) — a declared type's own
1293/// resolved structure, the payload of [`IrItem::Type`]. Covers the AST's
1294/// four `TypeBody` variants (`Refined`/`Record`/`Sum`/`Opaque`) with the
1295/// reference's own three ([DECISION A]): `Opaque` unifies into `Refined`
1296/// via its own `opaque: bool` field, mirroring `emitter/emit.rs`'s own
1297/// `RefinedShape { base, refinement, is_opaque }` — the shipped emitter's
1298/// own precedent for exactly this unification (`emit_type`,
1299/// `emitter/emit.rs:19`).
1300#[derive(Debug, Clone)]
1301pub enum TypeShape {
1302    /// Every field the record declares, in declaration order ([DECISION B]
1303    /// extended: a field's own inline `refinement` is dropped — a
1304    /// construction-time constraint the checker already enforces, not part
1305    /// of the emitted shape. Scoped claim: no reader on the record-*type*
1306    /// emission path (`emit_record_type`, `emitter/emit.rs:234-263`, reads
1307    /// `type_ref` alone). There *is* one `.refinement` reader in the
1308    /// emitter overall — `emitter/emit.rs:2781`, agent-state zero-value
1309    /// construction — but that is `StoreFieldIr` territory (P6.7), out of
1310    /// this variant's scope; do not read this comment as licence to drop
1311    /// `refinement` from a future store field too.
1312    Record { fields: Vec<(String, TyId)> },
1313    /// Every variant the sum declares, each with its own payload field
1314    /// list, plus any `embeds` clauses ([DECISION C]: [`EmbedIr`]).
1315    Sum {
1316        variants: Vec<(String, Vec<(String, TyId)>)>,
1317        embeds: Vec<EmbedIr>,
1318    },
1319    /// `type X = base where refinement` (`Refined`) or `type X = unsafe
1320    /// base ...` (`Opaque`, `opaque: true`). `refinement` is `Option`, not
1321    /// the reference's own bare `RefinementId` ([DECISION B]) — a bare
1322    /// `type X = Int` (no `where` clause) is legal and carries none.
1323    Refined {
1324        base: BaseType,
1325        refinement: Option<Refinement>,
1326        opaque: bool,
1327    },
1328}
1329
1330/// The payload of [`TypeShape::Sum`]'s own `embeds` — a resolved `embeds`
1331/// clause ([DECISION C], #1161): the source type paired with the target
1332/// variant's own tag name. A plain tuple, not a dedicated struct, mirroring
1333/// [`IrPat::Variant`]'s own `fields: Vec<(String, Box<IrPat>)>` precedent
1334/// for a two-part fact with no further structure.
1335pub type EmbedIr = (TyId, String);
1336
1337/// P6.7's real store-field state shape (`design/bynk-greenfield-compiler.md`
1338/// §6.6, R6.14, #1163) — the payload of an agent `store` field declaration,
1339/// `bynk_lower::lower_store_field_ir`'s own return value. Mirrors
1340/// `checker::StoreField`'s own five-kind dispatch
1341/// (`bynk-check/src/checker.rs`) in shape, but is persistent IR data with no
1342/// consumer yet, not that checking pass's own ephemeral, per-agent scratch
1343/// value — the two are deliberately not unified (see
1344/// `bynk_lower::lower_store_field_ir`'s own doc comment). No `IrItem` variant
1345/// references this yet — `IrItem::Agent`/`Service` remain unconstructed
1346/// (`IrItem`'s own doc comment names exactly what still blocks them).
1347#[derive(Debug, Clone)]
1348pub struct StoreFieldIr {
1349    /// The field's own declared name ([DECISION A]: `String`, sourced
1350    /// directly from `StoreField.name.name` — this module's own "no arena
1351    /// exists in this codebase" substitution, applied to the reference's
1352    /// own `FieldId` arena slot).
1353    pub field: String,
1354    pub kind: StoreKindIr,
1355    /// The fresh-key initialiser, constructed only for a `Cell` field
1356    /// ([DECISION D]) — `None` for every other kind, regardless of whether
1357    /// the AST grammatically parsed one there. A non-`Cell` field's `init`
1358    /// expression is parsed but never type-checked (a real, pre-existing
1359    /// checker gap; see `bynk_lower::lower_store_field_ir`'s own doc comment),
1360    /// so on a certified program it has no `expr_types` entry to lower.
1361    pub init: Option<IrExpr>,
1362    /// `@indexed(by: …)` sibling-table keys, in the annotation's own
1363    /// `by:`-argument order — one entry per *distinct* `by:` argument
1364    /// ([DECISION C]), no sort ([DECISION E]). Deduplicated: the checker
1365    /// validates each `by:` argument independently with no duplicate check
1366    /// (`validate_indexed_keys`), so `@indexed(by: k, by: k)` certifies —
1367    /// `bynk_lower::lower_store_field_ir` guards against it, mirroring the
1368    /// shipped emitter's own `store_map_indexes` dedup. Empty for every kind
1369    /// but `Map`, the only kind `@indexed` attaches to (`ANNOTATIONS`'s own
1370    /// registry, `bynk-check/src/context_checks.rs`).
1371    pub indexed: Vec<IndexIr>,
1372}
1373
1374/// P6.7's real `StoreKindIr` (Part 6.6, R6.14, #1163) — five variants, one
1375/// per functional storage kind (`Cell`/`Map`/`Set`/`Cache`/`Log`). `Queue`
1376/// is not a variant here: `bynk.store.kind_unsupported` gates it before
1377/// `certify` (R3.10), so this type is total over what a certified program's
1378/// own store fields can actually contain, not a subset some later slice
1379/// needs to extend. [DECISION B]: `Duration` substitutes to `i64`
1380/// milliseconds throughout — the same substitution [`ConstVal::DurationMillis`]
1381/// and `checker::StoreField::Cache`'s own already-resolved TTL already made.
1382#[derive(Debug, Clone)]
1383pub enum StoreKindIr {
1384    /// `Cell[T]` — element type.
1385    Cell(TyId),
1386    /// `Map[K, V]` — key, value.
1387    Map(TyId, TyId),
1388    /// `Set[T]` — element type.
1389    Set(TyId),
1390    /// `Cache[K, V] @ttl(...)` — key, value, TTL in milliseconds.
1391    Cache(TyId, TyId, i64),
1392    /// `Log[T] [@retain(...)]` — element type, optional retain millis.
1393    Log(TyId, Option<i64>),
1394}
1395
1396/// The payload of [`StoreFieldIr::indexed`] — one `@indexed(by: …)` key,
1397/// identified by the indexed value-field's own name ([DECISION C], #1163):
1398/// referenced by the reference's own `StoreFieldIr.indexed: Vec<IndexIr>`
1399/// but never defined anywhere in the document, the same "referenced, not
1400/// specified" gap #1161's own Decision C named for [`EmbedIr`]. No
1401/// dedicated struct: the sibling table's own emitted shape
1402/// (`Record<string, string[]>`) is fixed by the *map's own key type*, not
1403/// the indexed field's, so the indexed field's resolved type is not needed
1404/// downstream — mirrors `EmbedIr`'s own "no further structure" precedent.
1405pub type IndexIr = String;
1406
1407/// P6.8's real `IrPredicate` ([DECISION A], #1165) — referenced by the
1408/// reference's own `CommitShape::Transactional { invariants: Vec<IrPredicate>,
1409/// transitions: Vec<IrPredicate> }` and `IrItem::Agent`'s own sketch
1410/// (`bynk-greenfield-compiler.md:1130-1131`/`1182`), but never defined
1411/// anywhere in the document — the same "referenced, not specified" gap
1412/// #1161's own Decision C named for [`EmbedIr`] and #1163's own Decision C
1413/// named for [`IndexIr`]. One type serves both an agent's own `invariants`
1414/// and `transitions` fields, rather than two near-identical structs:
1415/// `Invariant`/`Transition` (`bynk_syntax::ast`) already share this exact
1416/// shape — a name plus a `Bool`-typed predicate expression — and
1417/// `bynk_lower::lower_invariant_ir`/`bynk_lower::lower_transition_ir` differ only
1418/// in how they seed the predicate's own scope (an invariant over the
1419/// agent's `store` `Cell` fields, a transition over `old`/`new`), not in
1420/// what they produce. `name: String` is this module's own "no arena"
1421/// substitution ([DECISION B] extended) — a predicate has no `DefId` of its
1422/// own in the reference either, referenced only by position within its
1423/// owning `Vec`.
1424#[derive(Debug, Clone)]
1425pub struct IrPredicate {
1426    pub name: String,
1427    pub predicate: IrExpr,
1428}
1429
1430/// P6.8's real `CommitShape` (Part 6.7, R6.15, #1165) — a handler body's own
1431/// resolved one-of-three commit shape, `bynk_lower::lower_commit_shape_ir`'s own
1432/// return value. Matches the reference's own three-variant shape verbatim
1433/// (`bynk-greenfield-compiler.md:1179-1183`) — no substitution needed,
1434/// `Transactional`'s own payload already reuses [`IrPredicate`] rather than
1435/// carrying `Invariant`/`Transition` AST nodes directly. Shape-agnostic
1436/// between an agent and a service handler ([DECISION F]): as of P6.11
1437/// (#1171), `bynk_lower::lower_service_handler_ir` is the real service call
1438/// site this decision predicted, passing empty `invariants`/`transitions`
1439/// slices, and the identical write-detection walk
1440/// (`bynk_lower::lower_commit_shape_ir`'s own doc comment) naturally finds
1441/// neither a mutating `Callee::Store` nor a bare `:=` in a service body (a
1442/// service declares no `store` fields to write), so `Transactional` is
1443/// never constructed for one — the shipped emitter's own `emit_service`
1444/// already only ever produces the other two shapes, for the same reason.
1445#[derive(Debug, Clone)]
1446pub enum CommitShape {
1447    /// No store write and no `Events.emit` — the body splices flat, no
1448    /// commit or flush of any kind.
1449    ReadOnly,
1450    /// No store write, but the body emits at least one event — `__events`
1451    /// is flushed at the end of the handler, state is not copied.
1452    FlushEvents,
1453    /// A mutating `Callee::Store` write or a bare `:=` reaches this body —
1454    /// state is snapshotted, the body runs in an IIFE, then the snapshot is
1455    /// committed (and `__events`, if any, flushed alongside it). Carries
1456    /// the agent's own already-lowered invariants/transitions
1457    /// (`bynk_lower::lower_invariant_ir`/`bynk_lower::lower_transition_ir`), not
1458    /// the raw AST lists — a future consumer checking them at commit time
1459    /// reads real `IrPredicate`s, not `Invariant`/`Transition` nodes it
1460    /// would have to lower itself.
1461    ///
1462    /// **Does not carry its own `emits` bit** — matches the reference's own
1463    /// verbatim shape (`bynk-greenfield-compiler.md:1182`), which gives
1464    /// `Transactional` no such field even though a writing handler that also
1465    /// emits is real (the shipped emitter's own `writes_state`/
1466    /// `body_emits_directly`, `emitter/emit.rs:3124`/`3290`, are independent
1467    /// booleans — a store write does not preclude an emit). A future
1468    /// `IrHandler` consumer that needs both facts at once — to decide
1469    /// whether *this* `Transactional` handler also flushes `__events` —
1470    /// re-derives it the same way `lower_commit_shape_ir`'s own caller
1471    /// already must ([DECISION D]: `bynk_ir::block_uses_emit(body)`),
1472    /// not from this variant. Named here rather than silently assumed lost:
1473    /// the fact is recoverable from `body`, which every real consumer holds
1474    /// alongside a `CommitShape` in the first place — `IrHandler` itself
1475    /// (real as of P6.9, #1167) is exactly that consumer, carrying both
1476    /// `commit`/`body` side by side.
1477    Transactional {
1478        invariants: Vec<IrPredicate>,
1479        transitions: Vec<IrPredicate>,
1480    },
1481}
1482
1483/// P6.9's real `ActorBinder` ([DECISION A], #1167) — referenced by the
1484/// reference's own `IrHandler` sketch (`bynk-greenfield-compiler.md:1169-1177`,
1485/// `binder: Option<ActorBinder>`) but never defined anywhere in the
1486/// document — the same "referenced, not specified" gap #1161's own
1487/// Decision C named for [`EmbedIr`], #1163's own Decision C named for
1488/// [`IndexIr`], and #1165's own Decision A named for [`IrPredicate`].
1489/// Mirrors the checker's own already-resolved
1490/// `actor_binding: Option<(String, TyId)>` shape
1491/// (`bynk-check/src/checker.rs`'s `HandlerBodyCheck::actor_binding`):
1492/// `binder` is the bound name, `ty` the sealed `Ty::Actor(identity)` or
1493/// `Ty::ActorSum(members)` — both ordinary, already-real `TyId`s, no
1494/// synthetic type needed the way `<Agent>State` was for P6.8's `state_ty`.
1495/// No dedicated `lower_actor_binder_ir` constructor: the pair has no
1496/// further structure to derive, mirroring [`EmbedIr`]'s/[`IndexIr`]'s own
1497/// "no further structure, plain tuple/alias" precedent — as of P6.11
1498/// (#1171), `bynk_lower::lower_service_handler_ir` is the real caller that
1499/// reads `TypedCommons::actor_bindings` (#1170) and writes
1500/// `ActorBinder { binder, ty }` directly; `bynk_lower::lower_handler_ir`
1501/// (agent-only) still never does — see [`IrHandler`]'s own doc comment.
1502#[derive(Debug, Clone)]
1503pub struct ActorBinder {
1504    pub binder: String,
1505    pub ty: TyId,
1506}
1507
1508/// #1187's slice 3: a handler's resolved actor-verification seam, wrapping
1509/// `bynk-check`'s own five already-resolved seam structs
1510/// (`bynk-check/src/actors.rs`) by value — confirmed none carry any
1511/// `bynk_syntax::ast`/`TypeRef`/`Expr`: every field is `String`/`bool`/
1512/// `i64`/`Option`/`Vec` (or, for `BearerSeam::authorization`,
1513/// `ClaimPredicate`, itself a plain recursive `String`/`Box` enum). Built by
1514/// `bynk_lower::lower_actor_seam_ir`, which tries the five resolvers in the one
1515/// priority order that actually matters — `sum_members_for` first, since
1516/// it's the only resolver whose result can otherwise collide with
1517/// `bearer_seam_for`'s (a sum's own first peer can itself be Bearer-schemed;
1518/// `bearer_seam_for` has no `by.is_sum()` guard of its own to prevent that).
1519/// The other three pairs are mutually exclusive by construction — each
1520/// single-actor resolver requires the primary actor's own `auth` scheme to
1521/// match one specific `Scheme` variant, a closed set — so their relative
1522/// order here is a no-op, not a second load-bearing decision.
1523///
1524/// No `Signature` variant, deliberately: neither call site this slice
1525/// converts (`emit_service`'s `deps`-identity-binder chain, `emit.rs`;
1526/// `emit_worker_compose`'s HTTP-dispatch match, `workers.rs`) ever consults
1527/// `signature_seam_for` as part of this priority chain — Signature is a
1528/// separate, request-verification-only concept there (see
1529/// `workers_entry.rs`'s own `HttpRoute.signature` field), not one this
1530/// enum's callers need. Adding an unreachable variant this slice's own
1531/// `lower_actor_seam_ir` never constructs would be exactly the kind of
1532/// premature surface `bynk-design-notes.md`'s own conventions ask this
1533/// codebase to avoid.
1534#[derive(Debug, Clone)]
1535pub enum ActorSeamIr {
1536    /// No `by` clause resolves to any of the four seams below (`Visitor`/
1537    /// `None`-schemed, or no `by` clause at all).
1538    None,
1539    /// `by who: A | B` — an ordered sum of peer actors, first-wins.
1540    Sum(Vec<bynk_check::actors::SumMember>),
1541    Bearer(bynk_check::actors::BearerSeam),
1542    Oidc(bynk_check::actors::OidcSeam),
1543    /// A cross-context `on call … by c: Caller` handler's own binder name —
1544    /// `caller_binder_for`'s return type is already the bare `Option<String>`
1545    /// the other four resolvers reduce a whole struct down to one field for.
1546    Caller(String),
1547}
1548
1549/// P6.13's real `ConnectionBinder` ([DECISION G], #1179) — the synthetic
1550/// leading `connection: Connection[out]` binding a `from websocket`
1551/// lifecycle handler's body receives, `bynk_lower::lower_service_handler_ir`'s
1552/// own return value for exactly the `on open`/`on message`/`on close`
1553/// handlers of a `ServiceProtocol::WebSocket` service. Mirrors the
1554/// checker's own `open_connection_param`
1555/// (`bynk-check/src/context_checks.rs:2020-2032`): `ty` is the resolved
1556/// `Ty::Connection(out_ty)`, always present regardless of handler kind —
1557/// unlike `ActorBinder`, there is no `None` case here, since a websocket
1558/// lifecycle handler is checker-guaranteed to receive this binding
1559/// (`bynk.service.websocket_open_arity`, `context_checks.rs:742-763`, plus
1560/// the unconditional injection at `context_checks.rs:1944-1954`).
1561///
1562/// Deliberately **not** folded into `IrHandler::params`: the checker keeps
1563/// this binding out of `handler.params` itself (only `params_for_check`,
1564/// a check-local, carries it) — `IrHandler::params` mirrors `h.params`
1565/// exactly for every handler kind, and widening it here would be a real
1566/// behavior change beyond what this slice asks for, not a faithful mirror
1567/// of the checker's own asymmetry. Consumers that need "does this handler
1568/// have a connection" read `IrHandler::connection` instead.
1569///
1570/// `borrowed` is this slice's own IR target for the checker's own
1571/// owned-vs-borrowed linearity distinction (`borrowed_held`,
1572/// `context_checks.rs:1955-1963`): `false` for `on open` (a fresh owned
1573/// socket the handler must dispose/transfer), `true` for `on message`/
1574/// `on close` (the borrowed firing socket, no disposal obligation). A
1575/// bare `bool`, not a richer type: `borrowed_held` itself is only ever a
1576/// `HashSet<String>` keyed by binding name on the checker side, so there
1577/// is no further structure to represent for a type that names exactly one
1578/// binding (`"connection"`, never persisted as a field here — every
1579/// reader already knows the name statically, the same reasoning
1580/// `ActorBinder`'s own doc comment gives for omitting a redundant
1581/// discriminant). No dedicated `lower_connection_binder_ir` constructor,
1582/// following `ActorBinder`'s own precedent: the pair has no further
1583/// structure to derive.
1584#[derive(Debug, Clone)]
1585pub struct ConnectionBinder {
1586    pub ty: TyId,
1587    pub borrowed: bool,
1588}
1589
1590/// P6.9's real `IrHandler` ([DECISION C], #1167) — an agent `on call`
1591/// handler's own resolved shape, `bynk_lower::lower_handler_ir`'s own return
1592/// value. Six of the reference's own eight sketched fields are its
1593/// verbatim shape (`bynk-greenfield-compiler.md:1169-1177`) under this module's
1594/// already-established substitutions: `kind: IrHandlerKind` — originally
1595/// `HandlerKind` reused verbatim from `bynk_syntax::ast` (the same "reused,
1596/// not adapted" treatment `IrExprKind::Call`'s own `Callee` payload got),
1597/// converted to a real IR-native mirror by P6.24a once a purely-structural
1598/// emitter reader (no body, no `IrItem::Service`) needed to match on it
1599/// without spelling `bynk_syntax::ast` — see [`IrHandlerKind`]'s own doc
1600/// comment; `params`/
1601/// `given` are this module's standard "no arena" substitution (`params:
1602/// Vec<(String, TyId)>` mirrors [`IrItem::Fn::params`] exactly; `given:
1603/// Vec<String>` reads each `CapRef::key()`, the same identity
1604/// `Callee::Capability` already uses); `binder: Option<ActorBinder>` per
1605/// [`ActorBinder`]'s own doc comment; `body: IrExpr` is
1606/// `bynk_lower::lower_handler_ir`'s own new handler-body lowering entry point
1607/// (parallel to, but distinct from, `bynk_lower::lower_fn_body_ir` — that
1608/// entry point's own doc comment names exactly why a handler body cannot
1609/// reuse it); `commit: CommitShape` calls `bynk_lower::lower_commit_shape_ir`
1610/// (P6.8, unchanged); `effectful: bool` reuses [`IrItem::Fn::effectful`]'s
1611/// own derivation (`Ty::Fn`'s doc: effectful iff `ret` is `Effect[_]`)
1612/// unchanged.
1613///
1614/// **`method_name: Option<String>` is added beyond the reference's own
1615/// sketch** — the same class of addition #1162's own review made for
1616/// [`IrItem::Fn::receiver`] (#1161): the reference sketch has no slot for a
1617/// handler's own declared name at all, but a future printer (or R8.10's own
1618/// handler-key mangling) structurally needs it to know *which* of an
1619/// agent's several `on call <name>` handlers this is. `None` for the shapes
1620/// that have none today (a service's bare `on call`).
1621///
1622/// **`actors: Vec<String>` is a second addition beyond the reference's own
1623/// sketch** (review of #1171/#1180) — the actor name(s) a `by` clause
1624/// itself names, read directly from `h.by_clause.as_ref().map(|by|
1625/// &by.actors)` rather than from `binder`. Needed because `binder` alone
1626/// cannot represent the gate: a binder-less `by <Actor>` (verify-and-
1627/// discard, `ByClause::binder: Option<Ident>`) resolves to `binder: None`
1628/// with nothing else distinguishing it from a handler with no `by` clause
1629/// at all, and the same erasure hits a binder that shadowed a param and
1630/// was suppressed (`context_checks.rs:2050-2055`) — in both cases the
1631/// authorization gate is real even though no identity got bound to a
1632/// local. Even the happy path loses information a single-actor `binder`
1633/// alone can't recover: `ActorBinder::ty` is `Ty::Actor(identity_ty)`,
1634/// which carries the *sealed identity type*, not the actor's own
1635/// *declared name* — `by u: Buyer` and `by u: Seller` sharing one identity
1636/// type would otherwise lower to byte-identical `ActorBinder`s. The sum
1637/// path already avoids this (`Ty::ActorSum(Vec<(String, TyId)>)` retains
1638/// member names); `actors` gives the single-actor and no-binder cases the
1639/// same guarantee, uniformly, mirroring [`IrHandler::given`]'s own "read
1640/// straight off the AST, not through any checker-persisted resolution"
1641/// shape. Empty for an agent handler unconditionally — the same
1642/// `bynk.actor.by_on_agent` guarantee [DECISION D] already grounds for
1643/// `binder`.
1644///
1645/// `bynk_lower::lower_handler_ir` is agent-only **by design, still** ([DECISION
1646/// D]) — a real service handler's `IrHandler` (specifically, a non-`None`
1647/// `binder`) is never constructed by *this* function, and that stays true
1648/// even now that #1170 persisted `handler_actor_binding`'s own resolved
1649/// `(String, TyId)` into `TypedCommons::actor_bindings`/`CheckedProgram`.
1650/// P6.11 (#1171) built the real service-handler path as a **sibling**,
1651/// `bynk_lower::lower_service_handler_ir`, not a widening of this one —
1652/// `bynk_lower::lower_handler_ir`'s own doc comment names exactly why
1653/// (disjoint scopes, and widening would delete the `by_clause.is_none()`
1654/// assertion that today catches a service handler reaching the wrong
1655/// entry point). Not a functional gap for R6.16's own claim (invocation
1656/// origin-independence is specifically about an *agent* handler): an agent
1657/// handler's own `binder` is `None` unconditionally and by construction —
1658/// `bynk.actor.by_on_agent` (`context_checks.rs:2986-2996`) rejects any
1659/// `by` clause on an agent handler outright, so `bynk_lower::lower_handler_ir`
1660/// never has one to lower in the first place.
1661/// P6.24a: an IR-native mirror of [`bynk_syntax::ast::HandlerKind`] — a
1662/// field-for-field copy, not a re-export. Every field (`HttpMethod`, a
1663/// route `path: String`, a cron `expr: String`) is already fully resolved
1664/// at parse time; nothing here ever needed `TyId`/`CheckedProgram`, so
1665/// `bynk_lower::lower_handler_kind_ir` is a pure, unconditional conversion —
1666/// unlike almost everything else in this module, it carries no ADR 0334
1667/// totality story because it can never fail to resolve.
1668///
1669/// Exists because [`IrHandler::kind`] was still typed as the raw AST enum
1670/// (confirmed live, #1184's own review) — the one field on an otherwise
1671/// fully IR-native struct that still forced any reader down to
1672/// `bynk_syntax::ast` for a check as simple as "is this an HTTP handler."
1673/// R6.16 (handler-invocation origin-independence, P6.9) is this type's own
1674/// real destination; giving `emitter.rs`'s several purely-structural
1675/// `HandlerKind`/`ServiceProtocol` scans (no body needed, no `IrItem::
1676/// Service` required) somewhere IR-native to route through first is what
1677/// this slice actually lands.
1678#[derive(Debug, Clone, PartialEq, Eq)]
1679pub enum IrHandlerKind {
1680    Call,
1681    Http { method: IrHttpMethod, path: String },
1682    Cron { expr: String },
1683    Message,
1684    Open,
1685    Close,
1686    Event,
1687}
1688
1689/// [`IrHandlerKind::Http`]'s own method field — a field-for-field mirror of
1690/// [`bynk_syntax::ast::HttpMethod`], same reasoning as [`IrHandlerKind`]
1691/// itself.
1692#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1693pub enum IrHttpMethod {
1694    Get,
1695    Post,
1696    Put,
1697    Patch,
1698    Delete,
1699}
1700
1701impl IrHttpMethod {
1702    /// P6.51 (design/tracks/the-ir.md §6b): field-for-field mirror of
1703    /// [`bynk_syntax::ast::HttpMethod::as_str`].
1704    pub fn as_str(self) -> &'static str {
1705        match self {
1706            IrHttpMethod::Get => "GET",
1707            IrHttpMethod::Post => "POST",
1708            IrHttpMethod::Put => "PUT",
1709            IrHttpMethod::Patch => "PATCH",
1710            IrHttpMethod::Delete => "DELETE",
1711        }
1712    }
1713
1714    /// P6.57 (design/tracks/the-ir.md §6b): field-for-field mirror of
1715    /// [`bynk_syntax::ast::HttpMethod::from_ident`].
1716    pub fn from_ident(s: &str) -> Option<IrHttpMethod> {
1717        match s {
1718            "GET" => Some(IrHttpMethod::Get),
1719            "POST" => Some(IrHttpMethod::Post),
1720            "PUT" => Some(IrHttpMethod::Put),
1721            "PATCH" => Some(IrHttpMethod::Patch),
1722            "DELETE" => Some(IrHttpMethod::Delete),
1723            _ => None,
1724        }
1725    }
1726}
1727
1728#[derive(Debug, Clone)]
1729pub struct IrHandler {
1730    pub kind: IrHandlerKind,
1731    pub params: Vec<(String, TyId)>,
1732    pub given: Vec<String>,
1733    /// The actor name(s) a `by` clause names — see this struct's own doc
1734    /// comment for why `binder` alone cannot represent the gate. Empty iff
1735    /// `h.by_clause.is_none()`.
1736    pub actors: Vec<String>,
1737    pub binder: Option<ActorBinder>,
1738    /// P6.13 ([DECISION G], #1179): `Some` iff this handler is a `from
1739    /// websocket` service's `on open`/`on message`/`on close` — see
1740    /// [`ConnectionBinder`]'s own doc comment. `None` unconditionally for
1741    /// every other handler kind/protocol, including an agent handler
1742    /// (`binder`'s own sibling gate: `bynk_lower::lower_handler_ir` never
1743    /// sets this either).
1744    pub connection: Option<ConnectionBinder>,
1745    pub body: IrExpr,
1746    pub commit: CommitShape,
1747    /// The handler's own declared return type, resolved (#1187's slice 5,
1748    /// the `Service` emitter cutover) — mirrors [`IrItem::Fn::ret`]'s
1749    /// identical field, added here
1750    /// for the identical reason: `bynk_lower::lower_handler_signature_ir`
1751    /// already resolved this value to compute `effectful` below and
1752    /// discarded it, leaving a service emitter with no IR-native way to
1753    /// render a handler's own return-type annotation without re-walking
1754    /// `Handler::return_type` (`bynk_syntax::ast::TypeRef`) itself.
1755    pub ret: TyId,
1756    pub effectful: bool,
1757    pub method_name: Option<String>,
1758}
1759
1760/// Events track, slice 0 (spine #936): does this block contain a real
1761/// `Events.emit[...]` call anywhere — including nested branches, match arms,
1762/// lambdas, and any other expression position (a `Paren`, an `Ok`/`Err`
1763/// wrapper, a `Call`/`RecordConstruction` argument, a `BinOp` operand, …)?
1764/// Gates release-at-commit buffer threading (`deps.__events`) so a handler
1765/// that never emits keeps byte-identical output, mirroring `block_uses_send`'s
1766/// gate on `deps.__exec`.
1767///
1768/// Driven off the exhaustive `walk_block_exprs`/`walk_exprs` visitor rather
1769/// than a hand-rolled `ExprKind` match — a bespoke match here previously
1770/// covered only `MethodCall`/`Block`/`If`/`Match`/`Lambda` and silently
1771/// disagreed with `lower_expr_into` (which recurses into every expression
1772/// position), so `do (Events.emit[E](event))` — one added paren — compiled
1773/// clean but emitted a body that referenced an undeclared `__events` local
1774/// (`tsc`-only failure, no bynk diagnostic). Riding the walker means this
1775/// can't drift from the lowering again: a new `ExprKind` variant fails to
1776/// compile here until `walk_exprs` itself is taught to visit it.
1777///
1778/// #1187's slice 6 plumbing (review of #1202): reads the checker's own
1779/// already-resolved `Callee::Capability{cap:"Events",op:"emit"}` for each
1780/// visited call site instead of a bare-`Ident("Events")`-receiver name
1781/// match. Was deliberately syntactic before this — this function's own
1782/// prior doc comment named the locally-shadowed-`Events` false positive an
1783/// "accepted approximation," matching `block_uses_send`'s own precedent —
1784/// but that approximation stopped being harmless once `crate::project::
1785/// unit_table_uses_emit` (the project-wide compose-gating twin this
1786/// function's own callers must agree with) became precise first: the two
1787/// disagreeing on exactly the shadowed case produces a real `tsc` type
1788/// error (a `deps.__eventsDispatch` call site with nothing supplying it),
1789/// not just an unused interface field. `block_uses_send` needs no matching
1790/// fix — a `~>` send is a real `Statement::Send` AST variant, not a method
1791/// call that could be shadowed, so it was never approximate to begin with.
1792pub fn block_uses_emit(b: &Block, callees: &HashMap<ExprId, bynk_check::checker::Callee>) -> bool {
1793    let mut found = false;
1794    walk_block_exprs(b, &mut |e| {
1795        if !found
1796            && matches!(
1797                callees.get(&e.id),
1798                Some(bynk_check::checker::Callee::Capability { cap, op })
1799                    if cap == "Events" && op == "emit"
1800            )
1801        {
1802            found = true;
1803        }
1804    });
1805    found
1806}
1807
1808/// Decision C (#1165): the closed sets of mutating storage-op names, one
1809/// `pub` constant per kind group — read by `bynk_lower::body_writes_state`'s
1810/// own `Callee::Store`-keyed write-detection walk (P6.8, Decision B), which
1811/// needs no receiver-name gate at all: a `Callee::Store` already carries the
1812/// field's own resolved identity, not a name that could be shadowed. Until
1813/// #1196, this module also had its own bare-`Ident`-receiver-name-matching
1814/// reader (`block_writes_state`'s own `mutating_op`, deleted) — a single
1815/// shared source avoided the class of drift #1164's own review caught twice
1816/// for a different pair of independently hand-maintained copies
1817/// (`cache_ttl_millis`'s `DurationLit` extraction, `store_map_indexes`'s
1818/// dedup); now there is only the one reader. Live in `bynk-ir` (not
1819/// `bynk-emit` or `bynk-lower` specifically, moved here from
1820/// `bynk-emit::emitter` at the P7.12 crate carve, no behaviour change)
1821/// since a future `bynk-emit`-side reader (a `Service` handler's own write
1822/// detection, say) may need them again, the same reasoning that kept them
1823/// `pub(crate)` rather than `bynk-lower`-private before the carve.
1824/// `Map`/`Cache` share one list — both support the same four entry ops —
1825/// rather than two identical ones.
1826pub const MUTATING_MAP_CACHE_OPS: &[&str] = &["put", "remove", "update", "upsert"];
1827/// v0.83: `<set>.add`/`<set>.remove` mutate a `store Set[T]` field.
1828pub const MUTATING_SET_OPS: &[&str] = &["add", "remove"];
1829/// v0.95: `<log>.append` mutates the durable array (ADR 0121) — every other
1830/// `Log` method is a query-lifting read.
1831pub const MUTATING_LOG_OPS: &[&str] = &["append"];
1832/// v0.98 (ADR 0125): `<cell>.update(f)` is a read-modify-write of the
1833/// working state — the bare `:=` write form is `Statement::Assign`, checked
1834/// separately and unconditionally, no method name involved.
1835pub const MUTATING_CELL_OPS: &[&str] = &["update"];
1836
1837pub fn walk_block_exprs(b: &Block, f: &mut impl FnMut(&Expr)) {
1838    let mut exprs = Vec::new();
1839    for s in &b.statements {
1840        statement_exprs(s, &mut exprs);
1841    }
1842    exprs.push(&b.tail);
1843    for e in exprs {
1844        walk_exprs(e, f);
1845    }
1846}
1847
1848/// v0.22b: pre-order expression visitor — visits `e`, then every
1849/// sub-expression, including statements and tails of nested blocks. Driven by
1850/// `ast::expr_children`, the exhaustive total child iterator, rather than a
1851/// hand-matched recursion duplicating it — a new `ExprKind` variant fails to
1852/// compile in `expr_children` until it is taught to visit it, instead of
1853/// silently under-visiting here.
1854pub fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
1855    f(e);
1856    for child in expr_children(e) {
1857        walk_exprs(child, f);
1858    }
1859}
1860
1861/// A match needs the if/else-if lowering (ADR 0169) when any arm carries a guard
1862/// or a refutable nested payload pattern — a JS `switch` on `.tag` can express
1863/// neither. Flat, unguarded matches keep the `switch` (zero churn to existing
1864/// output).
1865///
1866/// `pub` since P6.5 (#1159, Decision B) — `bynk-lower`'s own lowering pass
1867/// reuses this pure predicate verbatim to decide `MatchForm`, rather than
1868/// re-deriving an equivalent one over `IrPat`'s own shape, so the string
1869/// emitter's own if-chain-vs-switch choice and the IR's own recorded `form`
1870/// can never silently disagree. Lives in `bynk-ir` (not `bynk-emit` or
1871/// `bynk-lower` specifically) since both `bynk-emit`'s own string emitter and
1872/// `bynk-lower`'s IR lowering need it — moved out of `bynk-emit::emitter::lower`
1873/// at the P7.12 crate carve, no behaviour change.
1874pub fn match_needs_if_chain(arms: &[MatchArm]) -> bool {
1875    arms.iter().any(|a| {
1876        a.guard.is_some()
1877            || pattern_has_nested_test(&a.pattern)
1878            || matches!(a.pattern, Pattern::Refined { .. })
1879    })
1880}
1881
1882/// True when `pat` carries a payload sub-pattern that is itself refutable (a
1883/// nested variant/literal) — i.e. it cannot be tested by a single `.tag` switch.
1884fn pattern_has_nested_test(pat: &Pattern) -> bool {
1885    match pat {
1886        Pattern::Variant { bindings, .. } => bindings.iter().any(|b| {
1887            let sp = b.pattern();
1888            !sp.is_irrefutable() || pattern_has_nested_test(sp)
1889        }),
1890        // #472: a refined pattern is never a top-level irrefutable/nested
1891        // payload today (the parser admits only `_` as `inner`, and only at a
1892        // match arm's top level), but keep this exhaustive and correct for
1893        // when nesting is admitted. (An `Or`'s alternatives can never
1894        // themselves be `Refined` — #472/#474's merged parser design only
1895        // ever wraps a *whole*, already-folded `|`-chain in `Refined`, never
1896        // the other way around — so this arm only matters nested under a
1897        // payload, e.g. a hypothetical `Some(_ where P)`.)
1898        Pattern::Refined { inner, .. } => !inner.is_irrefutable() || pattern_has_nested_test(inner),
1899        // #474: a bindingless, non-nested or-pattern (`1 | 2 | 3`,
1900        // `Pending | Cancelled(_, _)`) stays on the flat switch — it lowers to
1901        // fall-through `case` labels sharing one body. One with bindings or a
1902        // nested refutable payload needs the if-chain (a `switch` can't bind
1903        // different alternatives' fields to the same name).
1904        Pattern::Or(alts, _) => alts
1905            .iter()
1906            .any(|p| !p.bound_names().is_empty() || pattern_has_nested_test(p)),
1907        _ => false,
1908    }
1909}