bynk_ir/lib.rs
1//! `bynk-ir`: the resolved *declaration-level* vocabulary `bynk-emit` reads
2//! through `bynk-lower`'s AST-analysis helpers, instead of re-deriving the
3//! same facts from `bynk_syntax::ast` a second time.
4//!
5//! Every type here is a value some `bynk-lower` helper returns from a
6//! syntax-tree node plus the checker's own `TypedCommons`, and some
7//! `bynk-emit` site consumes — a service's protocol ([`ProtocolIr`]), a
8//! handler's kind ([`IrHandlerKind`]) and route cache ([`CacheIr`]), a
9//! `type` declaration's structure ([`TypeShape`]), an agent store field's
10//! storage shape ([`StoreFieldIr`]/[`StoreKindIr`]), a
11//! capability op's or attached method's signature ([`OpSig`]/[`FnSig`]), a
12//! `given` capability reference ([`CapRefIr`]), an actor's authentication
13//! seam ([`ActorSeamIr`]), an events subscription's pattern and shape
14//! ([`EventPatternIr`]/[`EventPatternValueIr`]/[`EventSubscriberShape`]),
15//! and the literal values a pattern can carry ([`ConstVal`]). Alongside
16//! them: four AST-walk helpers `bynk-emit` reads ([`block_uses_emit`],
17//! [`walk_block_exprs`], [`walk_exprs`], [`match_needs_if_chain`] — they
18//! live here rather than in the emitter because they were written to be
19//! shared with `bynk-lower`, and `bynk-emit` is the consumer that remains).
20//!
21//! **Every `pub` item here has a reader outside both `bynk-ir` and
22//! `bynk-lower`**, and the gated `unconsumed_ir_items` probe
23//! (`cargo xtask greenfield-status`) fails CI if one ever does not — the
24//! two crates do not get to vouch for each other, since a `bynk-ir` type
25//! constructed only by a `bynk-lower` helper nobody calls is exactly the
26//! shape phase 6 shipped.
27//!
28//! **What this crate is not, and was.** Phase 6 of
29//! `design/bynk-compiler-trajectory.md` (`the-ir.md`, #1137) built here a
30//! full typed expression IR — `IrExpr`/`IrExprKind`/`IrStmt`, patterns and
31//! match compilation, `IrItem` with every declaration variant, `IrHandler`,
32//! `CommitShape` — and `bynk-lower` built the `&CheckedProgram → Ir` pass
33//! that constructed it. The emitter never consumed any of it: its own
34//! string-emitting lowerer kept reading the AST, and the one production
35//! route into the IR constructors lowered every events-service handler body
36//! and discarded the result. The follow-on track (the IR cutover,
37//! #1542) priced finishing that cutover, found it was a second code
38//! generator rather than a retype, and re-settled on deletion: Slices D0–D2
39//! removed the detour, the constructors and those 23 types. The refusal is
40//! recorded with a trigger in `design/bynk-greenfield-compiler.md` Part 15.1
41//! (Slice D3). `design/archive/retired-tracks.md` has both tracks' closing
42//! summaries.
43//!
44//! **Identity fields are adapted, not literal** (Decision B of P6.1,
45//! extending ADR 0333's own precedent): the reference's
46//! `DefId`/`FieldId`/`VariantId` arena does not exist in this codebase, so
47//! every such slot is whatever cheap resolved handle the checker already
48//! has — a [`TyId`] for a type, a `String` for a name with no arena of its
49//! own.
50//!
51//! **Two AST types are embedded on purpose** (ADR 0366, P6.41):
52//! [`TypeShape::Refined`] carries `bynk_syntax::ast::BaseType` and
53//! `Refinement` directly, because the refinement predicate is rendered from
54//! its syntax and an IR mirror would be a field-for-field copy with no
55//! consumer of its own. The `ast_importers` probe's own doc comment
56//! (`xtask/src/greenfield_status.rs`) records that this embedding is
57//! invisible to it by construction.
58
59use std::collections::HashMap;
60
61use bynk_check::checker::TyId;
62use bynk_syntax::ast::{
63 BaseType, Block, Expr, ExprId, MatchArm, Pattern, Refinement, expr_children, statement_exprs,
64};
65
66/// A literal value an event-subscription pattern can match on
67/// ([`EventPatternValueIr::Const`]). Adapted from the
68/// reference's own `ConstVal` (`Int Float Str Bool Unit Bytes`, Part 6.2's
69/// comment): `DurationMillis` replaces `Bytes` because Bynk has a real
70/// `<int>.<unit>` duration *literal* (`ExprKind::DurationLit`) `Const` must
71/// cover, while `Bytes` has no literal AST form at all in this language —
72/// every `Bytes` value comes from a static-constructor *call*
73/// (`Bytes.fromUtf8`/`fromBase64`/`empty()`, `Callee::Intrinsic` territory,
74/// not a literal).
75#[derive(Debug, Clone, PartialEq)]
76pub enum ConstVal {
77 Int(i64),
78 Float(f64),
79 DurationMillis(i64),
80 Str(String),
81 Bool(bool),
82 Unit,
83}
84
85/// P6.14's real `CapRefIr` ([DECISION A], #1174, review of #1186) — one
86/// `bynk_syntax::ast::CapRef` entry of a provider's own `given` clause,
87/// under this module's usual "no arena, bare name" substitution:
88/// `context: Option<QualifiedName>` flattens to `Option<String>` via
89/// `QualifiedName::joined()` (the same `.`-joined form
90/// `resolve_consume_prefix` — `bynk-emit/src/project.rs` — already
91/// resolves against `consumes`/aliases), and `name: Ident` flattens to
92/// `String`, mirroring every other bare-name identity field in this module.
93/// Deliberately **not** resolved further here: which context a `Some`
94/// prefix actually names is whole-project `consumes`/alias data, a phase
95/// boundary this crate sits below — this type only preserves what `CapRef`
96/// itself carries, unresolved.
97#[derive(Debug, Clone)]
98pub struct CapRefIr {
99 pub context: Option<String>,
100 pub name: String,
101}
102
103/// A capability op's resolved signature (P6.12, [DECISION A], #1173) —
104/// `bynk_lower::lower_capability_ops_ir`'s per-op return value, read by
105/// `emit_capability` (`bynk-emit/src/emitter/emit.rs`). The reference's own
106/// sketch named `ops: Vec<OpSig>` (`bynk-greenfield-compiler.md:1134`)
107/// without defining the type; this is that shape, adapted from
108/// `bynk_syntax::ast::CapabilityOp` — a signature only, no body — under
109/// this crate's "no arena" substitutions: `params: Vec<(String, TyId)>`,
110/// and `type_params: Vec<String>` mirroring the checker's own
111/// already-resolved `CapabilityOpInfo::type_params`
112/// (`bynk-check/src/checker.rs`) — a bare rigid-variable name, not a
113/// `TypeParam` AST node, since nothing here re-derives bounds a capability
114/// op's own `[T, …]` list never carries in the first place (#926).
115/// `bynk_lower::lower_op_sig_ir` resolves `params`/`return_ty` in the scope
116/// `type_params` names, mirroring `context_checks::build_capability_op_info`'s
117/// own `vars` treatment (`bynk-check/src/context_checks.rs`) so a generic
118/// op's own `T` survives as `Ty::Var("T")` rather than collapsing to
119/// `Ty::Unit`. On a genuinely unresolvable name a `params`/`return_ty` entry
120/// *is* `Ty::Unit`, deliberately — see `bynk_lower::lower_op_sig_ir`'s own doc
121/// comment for why that mirrors the checker's own fallback rather than
122/// panicking.
123#[derive(Debug, Clone)]
124pub struct OpSig {
125 pub name: String,
126 /// The op's own type parameters (#926) — empty for a non-generic op.
127 /// Scoped to the op itself, not the capability: `CapabilityDecl` carries
128 /// no `type_params` of its own (`bynk-syntax/src/ast.rs:556-562`), so
129 /// this is never merged with anything above it, unlike an attached
130 /// method, whose receiver type's own params are in scope too.
131 pub type_params: Vec<String>,
132 pub params: Vec<(String, TyId)>,
133 pub return_ty: TyId,
134}
135
136/// A `fn`'s own resolved signature, with no `body` and no `receiver` —
137/// the [`OpSig`]-shaped value a *foreign* unit's attached method needs when
138/// only its signature will ever be rendered (`emit_attached_methods`'
139/// delegating forward at `bynk_emit::emitter::emit::emit_forwarded_methods`),
140/// never its body (P6.18). `bynk_lower::lower_attached_fn_sig_ir_from_types`
141/// resolves `params`/`return_ty` in the scope the method's own `[T, …]` list
142/// names (mirroring `OpSig`'s identical `type_params` treatment) — a
143/// genuinely unresolvable name degrades to `Ty::Unit`, deliberately, the
144/// same non-panicking posture `OpSig` already established: nothing
145/// checker-side actually validates an attached method's own
146/// `params`/`return_type` against the *importing* context's own visible
147/// types (only the declaring commons' own checking does), so a resolve miss
148/// here is an expected, not exceptional, state.
149#[derive(Debug, Clone)]
150pub struct FnSig {
151 pub name: String,
152 pub has_self: bool,
153 pub params: Vec<(String, TyId)>,
154 pub return_ty: TyId,
155}
156
157/// P6.11's real `ProtocolIr` ([DECISION A], #1171) — one variant per
158/// `bynk_syntax::ast::ServiceProtocol` variant, `bynk_lower::lower_protocol_ir`'s
159/// own return value. The reference's own sketch specifies only two of the
160/// six: `Events { event, pattern, schema_dispatch }`
161/// (`bynk-greenfield-compiler.md:1881`) and `WebSocket { in_ty, out_ty }`
162/// (`:1959`) — field names taken verbatim from those two rows. `Call`/
163/// `Http`/`Cron` carry no payload, not because one was dropped: the actual
164/// per-trigger binding (a route, a schedule) lives on each *handler*
165/// (`HandlerKind::Http { method, path }`/`Cron { expr }`), already
166/// reachable through [`IrHandlerKind`] — `ServiceProtocol`'s own doc
167/// comment says this in as many words ("the endpoint lives on each
168/// handler"), which is why the reference never spells these three out
169/// either. E2 (`:1737`) constrains the *set*, not the shape: "a closed
170/// nominal set … grows one variant per real trigger" — the AST's own closed
171/// `ServiceProtocol` already satisfies that exactly, so this type is total
172/// over what a certified program's own service can declare, the same claim
173/// [`StoreKindIr`]'s own doc makes about `Queue` being gated pre-`certify`.
174#[derive(Debug, Clone)]
175pub enum ProtocolIr {
176 Call,
177 Http,
178 Cron,
179 /// `from queue("name")`.
180 Queue {
181 name: String,
182 },
183 /// `from websocket(in: …, out: …)` — the two frame types, resolved
184 /// (each through the `Ty::Unit` fallback `bynk_lower::lower_protocol_ir`
185 /// documents, since the checker itself does not reject an unresolvable
186 /// frame type).
187 WebSocket {
188 in_ty: TyId,
189 out_ty: TyId,
190 },
191 /// `from Events(E)` — the subscribed event type, resolved, plus the
192 /// two independent, optional filters a subscription may carry.
193 Events {
194 event: TyId,
195 pattern: Option<EventPatternIr>,
196 /// P6.40 (design/tracks/the-ir.md §6a): flattened to `Option<i64>` —
197 /// `SchemaVersionPattern` has exactly one variant (`Literal(i64)`),
198 /// so once a real consumer needed to match on it
199 /// (`emitter/emit.rs`'s `via schema(N)` guard prologue), mirroring
200 /// the single-payload `i64` directly was simpler than introducing a
201 /// one-variant IR-native enum purely to re-wrap it. A future range
202 /// pattern (`via schema(2..)`) widens this field's own shape when it
203 /// lands, not before. The `SchemaDispatch` wrapper itself stays
204 /// dropped — it carried only this `pattern` plus a parse-only
205 /// `span`.
206 schema_dispatch: Option<i64>,
207 },
208}
209
210/// #1226/#1187 slice 6: the two facts a service's own event-subscriber
211/// *shape* needs, captured at that unit's own check time (its
212/// `CheckedProgram` does not survive past `check_unit_files`'s per-file loop)
213/// so a *different* unit's own composition root can later decide whether its
214/// subscriber to this service wants the event envelope forwarded, without
215/// re-walking this unit's raw `UnitTable`. Pure syntax, zero `TyId`
216/// dependency. Produced by `bynk_lower::lower_event_subscriber_shapes_ir`, sized
217/// like #1187's own `unit_callees` (#1202) accumulator.
218#[derive(Debug, Clone, Copy, Default)]
219pub struct EventSubscriberShape {
220 pub two_param_handler: bool,
221 pub schema_dispatch: bool,
222}
223
224/// The payload of `ProtocolIr::Events`'s own `pattern` — a `from
225/// Events(E { field: value, .. })` structural filter, [DECISION C] (#1171).
226/// **Not** `bynk_syntax::ast::EventPattern` reused verbatim, unlike
227/// `SchemaVersionPattern`: that type carries `rest_span` (a parse artefact
228/// for the grammar-required trailing `..`, giving a later reader nothing to
229/// act on) and its own `EventPatternValue::Variant` is an *unresolved,
230/// optionally-qualified* name pair — exactly the shape this module's whole
231/// posture rejects everywhere else.
232#[derive(Debug, Clone)]
233pub struct EventPatternIr {
234 /// `(field name, matched value)`, in source order — no dedicated
235 /// `EventPatternFieldIr` struct: a two-part fact with no further
236 /// structure, the same plain-tuple shape `TypeShape::Sum::embeds` uses.
237 pub fields: Vec<(String, EventPatternValueIr)>,
238}
239
240/// One [`EventPatternIr`] field's own matched value.
241#[derive(Debug, Clone)]
242pub enum EventPatternValueIr {
243 /// Reuses [`ConstVal`] for the closed `Int`/`Str`/`Bool` literal set —
244 /// `Pattern::Literal`'s own closed set, and the only reason `ConstVal`
245 /// is still a type of its own.
246 Const(ConstVal),
247 /// A nullary sum-variant tag, resolved and unqualified — a bare
248 /// `tag: String`. The AST's own optional qualifying `type_name` is
249 /// dropped, not lost: the sole consumer
250 /// (`bynk_emit::emitter::lower::event_pattern_guard_ir`, #1187's slice 5)
251 /// already destructures down to the bare tag alone — the qualification
252 /// is disambiguation for the *checker*, resolved against the field's
253 /// declared sum type before this point.
254 Variant { tag: String },
255}
256
257/// A GET handler's own `@cache(maxAge:, scope:)` freshness policy (#1228),
258/// interpreted — raw `{name, value: Expr}` annotation pairs mean nothing on
259/// their own, so the seconds and the scope are resolved here once.
260/// Handler-scoped, not service-scoped: `bynk_lower::lower_route_cache_ir` is
261/// a standalone per-route reader, wired directly into
262/// `emitter/workers_entry.rs`'s own route construction — the same live,
263/// standalone-consumer shape `lower_protocol_ir`/`lower_handler_given_ir`/
264/// `lower_actor_seam_ir` already established. (A service-level policy
265/// struct existed beside it until Slice D2 of #1542; it had no consumer.)
266#[derive(Debug, Clone)]
267pub struct CacheIr {
268 /// `maxAge` in whole seconds (the `Cache-Control: max-age`).
269 pub max_age_secs: i64,
270 /// `"public"` or `"private"` — defaults to `"private"` so a *shared*
271 /// cache never stores unless the author opts into `public`. Bare
272 /// `&'static str`, not an enum: mirrors `emitter/workers_entry.rs`'s
273 /// own former `CachePolicy::scope` shape verbatim (the two literal
274 /// values this route's own generated `Cache-Control` header ever
275 /// spells), not a new representation invented here — `CachePolicy`
276 /// itself is gone, superseded by this struct (#1228).
277 pub scope: &'static str,
278}
279
280/// A declared type's own resolved structure (Part 6.6, #1161) —
281/// `bynk_lower::lower_type_shape_ir`'s return value. Covers the AST's
282/// four `TypeBody` variants (`Refined`/`Record`/`Sum`/`Opaque`) with the
283/// reference's own three ([DECISION A]): `Opaque` unifies into `Refined`
284/// via its own `opaque: bool` field, mirroring `emitter/emit.rs`'s own
285/// `RefinedShape { base, refinement, is_opaque }` — the shipped emitter's
286/// own precedent for exactly this unification (`emit_type`,
287/// `emitter/emit.rs:19`).
288#[derive(Debug, Clone)]
289pub enum TypeShape {
290 /// Every field the record declares, in declaration order ([DECISION B]
291 /// extended: a field's own inline `refinement` is dropped — a
292 /// construction-time constraint the checker already enforces, not part
293 /// of the emitted shape. Scoped claim: no reader on the record-*type*
294 /// emission path (`emit_record_type`, `emitter/emit.rs:234-263`, reads
295 /// `type_ref` alone). There *is* one `.refinement` reader in the
296 /// emitter overall — `emitter/emit.rs:2781`, agent-state zero-value
297 /// construction — but that is `StoreFieldIr` territory (P6.7), out of
298 /// this variant's scope; do not read this comment as licence to drop
299 /// `refinement` from a future store field too.
300 Record { fields: Vec<(String, TyId)> },
301 /// Every variant the sum declares, each with its own payload field
302 /// list, plus any `embeds` clauses ([DECISION C], #1161) — each a
303 /// `(source type, target variant tag)` pair, a two-part fact with no
304 /// further structure and so a plain tuple, not a dedicated struct.
305 Sum {
306 variants: Vec<(String, Vec<(String, TyId)>)>,
307 embeds: Vec<(TyId, String)>,
308 },
309 /// `type X = base where refinement` (`Refined`) or `type X = unsafe
310 /// base ...` (`Opaque`, `opaque: true`). `refinement` is `Option`, not
311 /// the reference's own bare `RefinementId` ([DECISION B]) — a bare
312 /// `type X = Int` (no `where` clause) is legal and carries none.
313 Refined {
314 base: BaseType,
315 refinement: Option<Refinement>,
316 opaque: bool,
317 },
318}
319
320/// An agent `store` field's storage shape (`design/bynk-greenfield-compiler.md`
321/// §6.6, R6.14, #1163) — `bynk_lower::lower_store_field_shape_ir`'s return
322/// value, read by `emit_agent`'s state section. Mirrors
323/// `checker::StoreField`'s own five-kind dispatch
324/// (`bynk-check/src/checker.rs`) in shape, but is persistent IR data, not
325/// that checking pass's own ephemeral, per-agent scratch value — the two are
326/// deliberately not unified. Shape only: a `Cell` field's initialiser
327/// expression is rendered by the emitter from the AST (the `init` slot that
328/// once carried it as an `IrExpr` went with the expression IR in Slice D2 of
329/// #1542).
330#[derive(Debug, Clone)]
331pub struct StoreFieldIr {
332 /// The field's own declared name ([DECISION A]: `String`, sourced
333 /// directly from `StoreField.name.name` — this module's own "no arena
334 /// exists in this codebase" substitution, applied to the reference's
335 /// own `FieldId` arena slot).
336 pub field: String,
337 pub kind: StoreKindIr,
338 /// `@indexed(by: …)` sibling-table keys, in the annotation's own
339 /// `by:`-argument order — one entry per *distinct* `by:` argument
340 /// ([DECISION C]), no sort ([DECISION E]). Deduplicated: the checker
341 /// validates each `by:` argument independently with no duplicate check
342 /// (`validate_indexed_keys`), so `@indexed(by: k, by: k)` certifies —
343 /// `bynk_lower::lower_store_field_shape_ir` guards against it, mirroring the
344 /// shipped emitter's own `store_map_indexes` dedup. Empty for every kind
345 /// but `Map`, the only kind `@indexed` attaches to (`ANNOTATIONS`'s own
346 /// registry, `bynk-check/src/context_checks.rs`). Each key is the
347 /// indexed value-field's own name, a bare `String` ([DECISION C],
348 /// #1163): the sibling table's own emitted shape
349 /// (`Record<string, string[]>`) is fixed by the *map's own key type*,
350 /// not the indexed field's, so the indexed field's resolved type is not
351 /// needed downstream — the same "no further structure" plain-value
352 /// shape `TypeShape::Sum::embeds` uses.
353 pub indexed: Vec<String>,
354}
355
356/// P6.7's real `StoreKindIr` (Part 6.6, R6.14, #1163) — five variants, one
357/// per functional storage kind (`Cell`/`Map`/`Set`/`Cache`/`Log`). `Queue`
358/// is not a variant here: `bynk.store.kind_unsupported` gates it before
359/// `certify` (R3.10), so this type is total over what a certified program's
360/// own store fields can actually contain, not a subset some later slice
361/// needs to extend. [DECISION B]: `Duration` substitutes to `i64`
362/// milliseconds throughout — the same substitution [`ConstVal::DurationMillis`]
363/// and `checker::StoreField::Cache`'s own already-resolved TTL already made.
364#[derive(Debug, Clone)]
365pub enum StoreKindIr {
366 /// `Cell[T]` — element type.
367 Cell(TyId),
368 /// `Map[K, V]` — key, value.
369 Map(TyId, TyId),
370 /// `Set[T]` — element type.
371 Set(TyId),
372 /// `Cache[K, V] @ttl(...)` — key, value, TTL in milliseconds.
373 Cache(TyId, TyId, i64),
374 /// `Log[T] [@retain(...)]` — element type, optional retain millis.
375 Log(TyId, Option<i64>),
376}
377
378/// #1187's slice 3: a handler's resolved actor-verification seam, wrapping
379/// `bynk-check`'s own five already-resolved seam structs
380/// (`bynk-check/src/actors.rs`) by value — confirmed none carry any
381/// `bynk_syntax::ast`/`TypeRef`/`Expr`: every field is `String`/`bool`/
382/// `i64`/`Option`/`Vec` (or, for `BearerSeam::authorization`,
383/// `ClaimPredicate`, itself a plain recursive `String`/`Box` enum). Built by
384/// `bynk_lower::lower_actor_seam_ir`, which tries the five resolvers in the one
385/// priority order that actually matters — `sum_members_for` first, since
386/// it's the only resolver whose result can otherwise collide with
387/// `bearer_seam_for`'s (a sum's own first peer can itself be Bearer-schemed;
388/// `bearer_seam_for` has no `by.is_sum()` guard of its own to prevent that).
389/// The other three pairs are mutually exclusive by construction — each
390/// single-actor resolver requires the primary actor's own `auth` scheme to
391/// match one specific `Scheme` variant, a closed set — so their relative
392/// order here is a no-op, not a second load-bearing decision.
393///
394/// No `Signature` variant, deliberately: neither call site this slice
395/// converts (`emit_service`'s `deps`-identity-binder chain, `emit.rs`;
396/// `emit_worker_compose`'s HTTP-dispatch match, `workers.rs`) ever consults
397/// `signature_seam_for` as part of this priority chain — Signature is a
398/// separate, request-verification-only concept there (see
399/// `workers_entry.rs`'s own `HttpRoute.signature` field), not one this
400/// enum's callers need. Adding an unreachable variant this slice's own
401/// `lower_actor_seam_ir` never constructs would be exactly the kind of
402/// premature surface `bynk-design-notes.md`'s own conventions ask this
403/// codebase to avoid.
404#[derive(Debug, Clone)]
405pub enum ActorSeamIr {
406 /// No `by` clause resolves to any of the four seams below (`Visitor`/
407 /// `None`-schemed, or no `by` clause at all).
408 None,
409 /// `by who: A | B` — an ordered sum of peer actors, first-wins.
410 Sum(Vec<bynk_check::actors::SumMember>),
411 Bearer(bynk_check::actors::BearerSeam),
412 Oidc(bynk_check::actors::OidcSeam),
413 /// A cross-context `on call … by c: Caller` handler's own binder name —
414 /// `caller_binder_for`'s return type is already the bare `Option<String>`
415 /// the other four resolvers reduce a whole struct down to one field for.
416 Caller(String),
417}
418
419/// P6.24a: an IR-native mirror of [`bynk_syntax::ast::HandlerKind`] — a
420/// field-for-field copy, not a re-export. Every field (`HttpMethod`, a
421/// route `path: String`, a cron `expr: String`) is already fully resolved
422/// at parse time; nothing here ever needed `TyId`/`CheckedProgram`, so
423/// `bynk_lower::lower_handler_kind_ir` is a pure, unconditional conversion —
424/// unlike almost everything else in this module, it carries no ADR 0334
425/// totality story because it can never fail to resolve.
426///
427/// Exists so `emitter.rs`'s several purely-structural handler-kind scans
428/// ("is this an HTTP handler", "which cron expression") have somewhere
429/// IR-native to route through instead of matching `bynk_syntax::ast`
430/// directly (#1184's review; R6.16, handler-invocation
431/// origin-independence). With 70-odd references across `bynk-emit` it is
432/// this crate's most-consumed type.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub enum IrHandlerKind {
435 Call,
436 Http { method: IrHttpMethod, path: String },
437 Cron { expr: String },
438 Message,
439 Open,
440 Close,
441 Event,
442}
443
444/// [`IrHandlerKind::Http`]'s own method field — a field-for-field mirror of
445/// [`bynk_syntax::ast::HttpMethod`], same reasoning as [`IrHandlerKind`]
446/// itself.
447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
448pub enum IrHttpMethod {
449 Get,
450 Post,
451 Put,
452 Patch,
453 Delete,
454}
455
456impl IrHttpMethod {
457 /// P6.51 (design/tracks/the-ir.md §6b): field-for-field mirror of
458 /// [`bynk_syntax::ast::HttpMethod::as_str`].
459 pub fn as_str(self) -> &'static str {
460 match self {
461 IrHttpMethod::Get => "GET",
462 IrHttpMethod::Post => "POST",
463 IrHttpMethod::Put => "PUT",
464 IrHttpMethod::Patch => "PATCH",
465 IrHttpMethod::Delete => "DELETE",
466 }
467 }
468
469 /// P6.57 (design/tracks/the-ir.md §6b): field-for-field mirror of
470 /// [`bynk_syntax::ast::HttpMethod::from_ident`].
471 pub fn from_ident(s: &str) -> Option<IrHttpMethod> {
472 match s {
473 "GET" => Some(IrHttpMethod::Get),
474 "POST" => Some(IrHttpMethod::Post),
475 "PUT" => Some(IrHttpMethod::Put),
476 "PATCH" => Some(IrHttpMethod::Patch),
477 "DELETE" => Some(IrHttpMethod::Delete),
478 _ => None,
479 }
480 }
481}
482
483/// Events track, slice 0 (spine #936): does this block contain a real
484/// `Events.emit[...]` call anywhere — including nested branches, match arms,
485/// lambdas, and any other expression position (a `Paren`, an `Ok`/`Err`
486/// wrapper, a `Call`/`RecordConstruction` argument, a `BinOp` operand, …)?
487/// Gates release-at-commit buffer threading (`deps.__events`) so a handler
488/// that never emits keeps byte-identical output, mirroring `block_uses_send`'s
489/// gate on `deps.__exec`.
490///
491/// Driven off the exhaustive `walk_block_exprs`/`walk_exprs` visitor rather
492/// than a hand-rolled `ExprKind` match — a bespoke match here previously
493/// covered only `MethodCall`/`Block`/`If`/`Match`/`Lambda` and silently
494/// disagreed with `lower_expr_into` (which recurses into every expression
495/// position), so `do (Events.emit[E](event))` — one added paren — compiled
496/// clean but emitted a body that referenced an undeclared `__events` local
497/// (`tsc`-only failure, no bynk diagnostic). Riding the walker means this
498/// can't drift from the lowering again: a new `ExprKind` variant fails to
499/// compile here until `walk_exprs` itself is taught to visit it.
500///
501/// #1187's slice 6 plumbing (review of #1202): reads the checker's own
502/// already-resolved `Callee::Capability{cap:"Events",op:"emit"}` for each
503/// visited call site instead of a bare-`Ident("Events")`-receiver name
504/// match. Was deliberately syntactic before this — this function's own
505/// prior doc comment named the locally-shadowed-`Events` false positive an
506/// "accepted approximation," matching `block_uses_send`'s own precedent —
507/// but that approximation stopped being harmless once `crate::project::
508/// unit_table_uses_emit` (the project-wide compose-gating twin this
509/// function's own callers must agree with) became precise first: the two
510/// disagreeing on exactly the shadowed case produces a real `tsc` type
511/// error (a `deps.__eventsDispatch` call site with nothing supplying it),
512/// not just an unused interface field. `block_uses_send` needs no matching
513/// fix — a `~>` send is a real `Statement::Send` AST variant, not a method
514/// call that could be shadowed, so it was never approximate to begin with.
515pub fn block_uses_emit(b: &Block, callees: &HashMap<ExprId, bynk_check::checker::Callee>) -> bool {
516 let mut found = false;
517 walk_block_exprs(b, &mut |e| {
518 if !found
519 && matches!(
520 callees.get(&e.id),
521 Some(bynk_check::checker::Callee::Capability { cap, op })
522 if cap == "Events" && op == "emit"
523 )
524 {
525 found = true;
526 }
527 });
528 found
529}
530
531pub fn walk_block_exprs(b: &Block, f: &mut impl FnMut(&Expr)) {
532 let mut exprs = Vec::new();
533 for s in &b.statements {
534 statement_exprs(s, &mut exprs);
535 }
536 exprs.push(&b.tail);
537 for e in exprs {
538 walk_exprs(e, f);
539 }
540}
541
542/// v0.22b: pre-order expression visitor — visits `e`, then every
543/// sub-expression, including statements and tails of nested blocks. Driven by
544/// `ast::expr_children`, the exhaustive total child iterator, rather than a
545/// hand-matched recursion duplicating it — a new `ExprKind` variant fails to
546/// compile in `expr_children` until it is taught to visit it, instead of
547/// silently under-visiting here.
548pub fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
549 f(e);
550 for child in expr_children(e) {
551 walk_exprs(child, f);
552 }
553}
554
555/// A match needs the if/else-if lowering (ADR 0169) when any arm carries a guard
556/// or a refutable nested payload pattern — a JS `switch` on `.tag` can express
557/// neither. Flat, unguarded matches keep the `switch` (zero churn to existing
558/// output).
559///
560/// Consumed only by `bynk-emit`'s string emitter today (`emitter/lower.rs`'s
561/// two match-lowering sites). It became `pub` and moved here at P6.5/P7.12
562/// so the IR lowering pass could reuse the identical predicate to record its
563/// own match form and the two could never silently disagree; that second
564/// consumer went with Slices D1/D2 of #1542. It stays in `bynk-ir` rather
565/// than moving back because it is a pure predicate over the AST with no
566/// emitter state, the same footing as its three sibling walk helpers above,
567/// and moving it would be churn with no reader gained.
568pub fn match_needs_if_chain(arms: &[MatchArm]) -> bool {
569 arms.iter().any(|a| {
570 a.guard.is_some()
571 || pattern_has_nested_test(&a.pattern)
572 || matches!(a.pattern, Pattern::Refined { .. })
573 })
574}
575
576/// True when `pat` carries a payload sub-pattern that is itself refutable (a
577/// nested variant/literal) — i.e. it cannot be tested by a single `.tag` switch.
578fn pattern_has_nested_test(pat: &Pattern) -> bool {
579 match pat {
580 Pattern::Variant { bindings, .. } => bindings.iter().any(|b| {
581 let sp = b.pattern();
582 !sp.is_irrefutable() || pattern_has_nested_test(sp)
583 }),
584 // #472: a refined pattern is never a top-level irrefutable/nested
585 // payload today (the parser admits only `_` as `inner`, and only at a
586 // match arm's top level), but keep this exhaustive and correct for
587 // when nesting is admitted. (An `Or`'s alternatives can never
588 // themselves be `Refined` — #472/#474's merged parser design only
589 // ever wraps a *whole*, already-folded `|`-chain in `Refined`, never
590 // the other way around — so this arm only matters nested under a
591 // payload, e.g. a hypothetical `Some(_ where P)`.)
592 Pattern::Refined { inner, .. } => !inner.is_irrefutable() || pattern_has_nested_test(inner),
593 // #474: a bindingless, non-nested or-pattern (`1 | 2 | 3`,
594 // `Pending | Cancelled(_, _)`) stays on the flat switch — it lowers to
595 // fall-through `case` labels sharing one body. One with bindings or a
596 // nested refutable payload needs the if-chain (a `switch` can't bind
597 // different alternatives' fields to the same name).
598 Pattern::Or(alts, _) => alts
599 .iter()
600 .any(|p| !p.bound_names().is_empty() || pattern_has_nested_test(p)),
601 _ => false,
602 }
603}