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}