Skip to main content

Crate bynk_lower

Crate bynk_lower 

Source
Expand description

P6.1 (#1141): the &CheckedProgram → Ir lowering pass — real construction for the node kinds design/tracks/the-ir.md’s own P6.1 row names (Const, Local, Global, Record, Field, List, Block, If, And, Or, Not, Return, Await, Send, Pure); every other IrExprKind/ IrStmt arm is a todo!() naming the slice that completes it (Decision D).

Correction (Arc D, P7.12): the claim this paragraph made through the crate carve — “nothing in this module is called from anywhere in bynk-emit’s existing emission path… it has no consumer yet” — was false and is corrected here rather than left standing. lower_service_ item_ir unconditionally lowers every handler’s own body, and is reached for real from bynk_check-typed Events-protocol services via lower_event_subscriber_shapes_ir (bynk-emit/src/project.rs), reaching lower_service_handler_irlower_service_handler_body_irlower_block_irlower_expr_ir/lower_stmt_ir and the rest of this module’s own recursive expression-lowering machinery — real, live, production code, verified panic-free across the whole e2e fixture corpus by a catch_unwind safety probe (see lower_service_item_ir’s own doc comment). A handful of top-level item constructors genuinely have no caller outside this crate’s own test suite (lower_agent_item_ir, lower_provider_item_ir, lower_fn_item_ir, and the handler/store-field/ invariant/transition helpers only those three call) — each of those is real test-harness infrastructure for the shared lowering machinery above, not dead code, per this crate carve’s own accepted proposal issue.

Totality discipline (ADR 0334, Q2): every entry point here takes a &CheckedProgram, not a bare &TypedCommons — a certified program only, so LowerIrCtx::expr_ty’s .expect() on a miss is the checker and this pass disagreeing about which expressions a unit contains, a compiler bug, not a recoverable state. This scoping is the same discipline bynk-emit/src/emitter/emit.rs’s lower_workers_cross_context_call already applies to its own bynk.emit.unresolved_cross_context_signature panic.

Structs§

LowerIrCtx
The lowering pass’s own working state: the certified program’s typed output (for .ty/.types lookups), a lexical scope stack this pass tracks itself — TypedCommons has no persisted “what type does this bound name have” table (that lived only in the checker’s own transient Ctx), so the one case that needs it (a record shorthand field, { x }, which has no ExprId of its own to key expr_types by) re-derives it from the same param/let binding sites the checker itself walked — and the enclosing fn/method’s own rigid type variables (fn identity[T](x: T), and a generic type’s own params on one of its methods), needed by resolve_type_ref_in the same way Ctx::type_vars is (bynk-check/src/checker.rs:2816); resolve_type_ref (no vars set) would otherwise resolve a rigid T as an unknown declared type and silently fail.

Functions§

body_writes_state
[DECISION B]/[DECISION C] (#1165): does body reach a mutating Callee::Store write, or an unconditional Statement::Assign (:=), anywhere — including inside a nested if/match/lambda? Drives lower_commit_shape_ir’s own Transactional decision, and, as of #1196 (the #1187 emitter-cutover track’s own R6.5 stake), emit_agent‘s (bynk-emit/src/emitter/emit.rs) own real implicit-commit-wrapper decision too — its previous own name-matching block_writes_state (emitter.rs) is deleted, this function is its sole, direct replacement. The walk’s own shape is that deleted function’s own already-correct skeleton reused structurally, not re-derived: Block/If/Match are hand-matched so crossing a nested block re-enters the statement-aware case (an expr_children descent alone flattens a block straight to its statements’ values, losing the Statement::Assign tag), everywhere else recurses over expr_children’s total child iterator.
capability_op_sig_from_commons
P6.29 (design/tracks/the-ir.md §6a): the TypedCommons-only counterpart to lower_capability_item_ir, for call sites (emitter/lower.rs’s cap_op_param_names) that have a TypedCommons in hand but no CheckedProgramLowerCtx/ModuleCtx never carry one (see lower_op_sig_ir_from_commons, this function’s own single-op sibling, for the identical reason it exists as a separate entry point rather than a thin wrapper over the CheckedProgram-driven lower_op_sig_ir).
is_effectful_return
P6.50 (design/tracks/the-ir.md §6b): a return type’s own syntactic Effect[...] wrapper — TypeRef::Effect(_, _), not the resolved Ty::Effect(_) shape lower_handler_signature_ir reads above via cx.program.ty_intern. Relocated here from emitter/emit.rs (its original home, #[allow(dead_code)]-free and with eight call sites across emit.rs/workers.rs/workers_entry.rs) because lower_service_handler_signature_ir below was already calling up into it (bynk_emit::emitter::is_effectful_return) — the Ast → Ir boundary running backwards, an Ir-side lowering function reaching into the emitter module it should only ever be called from. emit.rs and friends now call bynk_lower::is_effectful_return instead (relocated again at the P7.12 crate carve — emitter/ir::lower are now separate crates, bynk-emit/bynk-lower respectively).
lower_actor_seam_ir
#1187’s slice 3: a handler’s resolved actor-verification seam — the same “narrow, standalone reader of already-resolved data” precedent body_writes_state/lower_service_handler_signature_ir established, applied to bynk-check’s own five actor-seam resolvers (bynk-check/src/actors.rs) instead of a full IrHandler assembly. ActorSeamIr’s own doc comment has the full grounding for the priority order and for the deliberately-missing Signature variant.
lower_agent_item_ir
P6.10 (#1169): assemble an agent declaration into a real bynk_ir::IrItem::Agent — wires every prior slice’s own standalone constructor (lower_store_field_ir since P6.7, lower_invariant_ir/ lower_transition_ir/lower_commit_shape_ir since P6.8, lower_handler_ir since P6.9) rather than re-deriving any of their logic — the first IrItem variant assembled from other already-real IR data rather than lowered fresh from the AST by itself.
lower_attached_fn_sig_ir_from_types
P6.x (#1137): lower_fn_sig_ir_from_types over an entire MethodTable’s own instance + static entries — the attached-method gathering [bynk-emit’s build_emit_unit_ctx] needs for a uses-imported type. Filters to FnName::Method before lowering: ResolverMethodTable only ever collects attached methods in practice (bynk-check/src/resolver.rs’s own doc comment on MethodTable), but the filter stays as a defensive match rather than an assumption, matching the caller’s own pre-existing posture one step earlier — this just moves that posture in front of the lowering call instead of behind it, so the FnName read (and the filter itself) never has to leave this module.
lower_block_ir
Lower a block as a value — no Return wrapping (see lower_fn_body_ir’s doc comment for the distinction). A block’s own type is always its tail’s type.
lower_capability_item_ir
P6.12 (#1173): assemble a capability declaration into a real bynk_ir::IrItem::Capability — structural mirror of lower_service_item_ir/lower_agent_item_ir, but with nothing to compute once and share: CapabilityDecl carries no state/invariants/ transitions/protocol of its own, only ops, so each op lowers independently through lower_op_sig_ir.
lower_commit_shape_ir
P6.8 ([DECISION B]/[DECISION D]/[DECISION F], #1165): decide a handler body’s own one-of-three CommitShape from resolved data — body_writes_state’s own write-detection walk, plus emits ([DECISION D]: the caller’s own bynk_emit::emitter::block_uses_emit(body) call, not re-derived here — no Callee classification exists for Events.emit to consume instead, see that decision’s own grounding). Does not lower body into an IrExpr tree first ([DECISION B]) — deciding the shape only needs the two booleans, and lowering the whole body just to throw the result away would be pure waste; a real IrHandler.body lowering is a separate, not-yet-commissioned step (see bynk_ir::IrItem’s own doc comment).
lower_event_subscriber_shapes_ir
P6.47 (design/tracks/the-ir.md §6b): every from Events(E) service in program’s own unit, captured as an bynk_ir::EventSubscriberShape keyed by service name — see that struct’s own doc comment for why this is captured now rather than re-derived cross-unit at compose time. Absorbs the ServiceProtocol::Events pre-filter this function’s own single call site used to apply externally: lower_service_item_ir unconditionally lowers every handler’s own body (not just its declared shape), so the guard stays first here too — a cheap, structural pre-filter (which services even have a shape to capture), not a resurrected raw-AST read — before paying for a full lowering pass on a matching service. Safe as of #1254: a catch_unwind probe wrapping lower_service_item_ir across the entire e2e fixture corpus found zero panics, down from ~51 when the P6.23 investigation first ran.
lower_expr_ir
lower_fn_body_ir
Lower a function/method body: seeds scope from f’s params (and its own rigid type variables — its own [T, ...] type parameters, plus a generic receiver’s, for a method), lowers the body as an ordinary value block, then wraps the tail via wrap_body_return. Distinct from lower_block_ir, which lowers a nested block as a bare value with no such wrapping.
lower_fn_item_ir
P6.6 (#1161): lower a fn declaration into a real IrItem::Fn — wraps lower_fn_body_ir (#1141, unchanged) rather than re-deriving its own rigid-variable seeding or body lowering; adds only def/receiver/ params/ret/effectful around its existing return value. Covers both free functions and methods alike (FnName::Free/FnName::Method) — which IrItem::Fns a future printer re-attaches under which IrItem::Type’s own namespace (R8.1) is phase 7’s own concern, not decided here.
lower_fn_sig_ir_from_types
P6.18: bynk_ir::FnSig’s own constructor — a fn’s own resolved signature, for a call site holding only that fn’s declaring unit’s own combined types (bynk_check::symbols::combined_types_for’s return shape), never a CheckedProgram. The one real call site (bynk-emit/src/project.rs’s build_emit_unit_ctx) reads a uses- imported foreign unit’s own attached methods, whose own CheckedProgram does not survive past that unit’s own check_unit_files iteration — the same “dropped before any later, project-wide pass runs” shape unit_callees (#1202)/EventSubscriberShape (#1232) both work around, except here no project-wide accumulator is needed at all: unlike a Callee/event-subscriber-shape classification (checker-only facts, never re-derivable from raw declarations alone), a fn signature’s own params/return_type are ordinary type references, resolvable from that unit’s own declared types the same way lower_op_sig_ir_from_commons already resolves a capability op’s — so a bare types map is sufficient, the same non-CheckedProgram scope that function already established.
lower_handler_given_ir
#1187’s slice 6 plumbing (sibling of lower_provider_given_ir): a handler’s own given clause, resolved independent of any full IrHandler/IrItem assembly — the standalone entry point for project.rs’s plan_agent_given_deps, EmitProjectCtx:: agent_method_givens, and emitter/workers.rs’s own given collection. Reuses lower_cap_ref_ir verbatim; a handler’s given is syntactically identical to a provider’s (bynk_syntax::ast::CapRef), so this is the same one-line adapter, not a new design.
lower_handler_ir
P6.9’s real IrHandler constructor ([DECISION C]/[DECISION D]/ [DECISION E], #1167) — lowers an agent on call handler h into a real IrHandler. store_cells/state_ty/invariants/transitions are parameters, not re-derived ([DECISION E], mirroring lower_invariant_ir’s/lower_transition_ir’s own precedent, P6.8): no persisted “this agent’s store cells / state type” table survives check_agent_decls’s own transient scope, and invariants/transitions are themselves already-lowered IrPredicates a caller must have produced via those same two functions — this function only threads them into lower_commit_shape_ir, never lowers or re-derives them itself. A future IrItem::Agent builder (not commissioned by this slice — see bynk_ir::IrItem’s own doc comment) computes all four once per agent and calls this function once per handler, not re-deriving any of them per call.
lower_handler_kind_ir
P6.24a: pure, unconditional HandlerKindIrHandlerKind conversion — every field is already fully resolved at parse time, so unlike almost every other function in this module this one takes no &CheckedProgram/&TypedCommons at all and can never miss.
lower_invariant_ir
P6.8 ([DECISION A]/[DECISION E], #1165): lower an agent invariant into a real IrPredicate — seeds the predicate’s own scope from store_cells exactly as checker::check_invariants does (bynk-check/src/checker.rs: each store Cell field in scope by bare name, reading as its element type), then lowers predicate through the ordinary lower_expr_ir machinery unchanged. Takes store_cells as a parameter rather than re-deriving it from program ([DECISION E]) — a certified CheckedProgram carries no persisted “this agent’s store cells” table; that scope is check_agent_decls’s own transient scratch, never persisted to TypedCommons. Called once per agent’s own invariant list, by whichever future slice builds IrItem::Agent for real — not once per handler, mirroring check_invariants’s own once-per-agent posture.
lower_op_sig_ir_from_commons
#1187’s own closing scoping pass: a TypedCommons-only sibling of lower_op_sig_ir, for the one real call site that never has a &CheckedProgramemitter/lower.rs’s cap_op_param_names, feeding trace(Cap.op)/with-predicate observation lowering (bynk.test’s DSL). That call path’s own TypedCommons is a synthetic, hand-assembled project-wide view (project/tests_emit.rs’s synthetic_typed_commons_for_target, merging every consumed unit’s own capability declarations into one scratch commons for lookup) — never itself the output of certify, so wrapping it as a CheckedProgram here would misrepresent an uncertified value as certified (CheckedProgram’s own doc comment, bynk-check/src/checker.rs, warns against exactly this). Splitting this out is sound precisely because this function never calls LowerIrCtx::expr_ty — the one method whose .expect()-panic needs a genuinely certified program, the reason this module’s own file-level doc comment gives for taking &CheckedProgram everywhere else. resolve_type_ref/unit_ty() (below) both degrade via .unwrap_or_else and read nothing TypedCommons doesn’t already expose directly.
lower_protocol_ir
P6.11 ([DECISION A], #1171): lower a service’s own from <protocol> header into a real ProtocolIr — standalone, takes the sub-node rather than the owning ServiceDecl (mirrors lower_store_field_ir), so a from websocket/from Events fixture can pin the descriptor by itself even where lower_service_handler_ir cannot yet lower every handler on the same service (the WebSocket lifecycle-body deferral — see that function’s own doc comment).
lower_protocol_ir_from_commons
P6.24a: a TypedCommons-only sibling of lower_protocol_ir, the same split lower_op_sig_ir/lower_op_sig_ir_from_commons already established — for a call site holding only a unit’s own TypedCommons, never a &CheckedProgram (emitter.rs’s emit_project_imports, a header-import-collection pass that runs well outside the per-declaration emission loop any CheckedProgram is threaded through). Sound for the identical reason: this function never calls LowerIrCtx::expr_ty, the one method whose .expect()-panic needs a genuinely certified program.
lower_provider_given_ir
A provider’s own given clause, resolved independent of ProviderBody’s external/Bynk dispatch and independent of lower_provider_item_ir’s own full assembly (#1187’s Provider given/deps-wiring slice) — the standalone entry point bynk-emit/src/project.rs’s instantiate_provider_ts_expr actually calls. Building a real IrItem::Provider there would still need care for a Bynk provider specifically: ProviderBody::Bynk::ops unconditionally lowers every op’s body through lower_provider_op_irlower_expr_ir, which does not yet handle every expression shape a real op body can contain. Correction (P6.25, 2026-08-19): ? propagation (ExprKind::Question) and an is-expression (ExprKind::Is) are no longer among those gaps — both landed (P6.15/ADR 0337, P6.16/ADR 0338, following #1225’s own Ok/Err/Some/None construction fix) — but lower_expr_ir still has two production-reachable todo!()s (lower_call_ir’s missing-Callee guard and a bare ident naming a free fn used as a value, P6.2 territory), so a real op body is not yet unconditionally safe to build; the risk has just narrowed. This function never touches ops/bodies, so it carries none of that risk; lower_provider_item_ir itself now calls it too, rather than hand-duplicating the one-line map.
lower_provider_item_ir
P6.14 (#1174): assemble a provider declaration into a real bynk_ir::IrItem::Provider — reads ProviderDecl::external straight into ProviderBody’s own Bynk/External dispatch (bynk_ir::IrItem’s own doc comment has the full grounding for why this, unlike Actor, was buildable this slice). external: true means ops is empty by the field’s own doc comment (bynk-syntax/src/ast.rs:592-595) — nothing to lower. given lowers unconditionally via lower_provider_given_ir (#1187’s Provider given/deps-wiring slice, fixing a real gap the External variant’s own bare-unit shape used to leave: provider.given is populated the same way regardless of external, and ProviderBody::Bynk’s own doc comment names why it, unlike module, is not deferrable — that reasoning always applied to External too, just wasn’t wired through).
lower_route_cache_ir
#1228: a GET handler’s own @cache(maxAge:, scope:) freshness policy — bynk_ir::CacheIr’s own doc comment has the full grounding for why this is a standalone reader rather than a PolicyIr field. Field-for- field the same extraction emitter/workers_entry.rs’s own (now superseded) cache_policy_for did: only a GET yields a policy; project validation (bynk.http.cache_*) has already rejected a @cache anywhere else, and a malformed maxAge there, so a missing or ill-formed annotation here simply yields None — no &CheckedProgram needed, the same posture lower_policy_ir’s own doc comment already argues for: maxAge/scope are already-resolved syntactic literals (ExprKind::DurationLit/Ident), not a type this pass would ever need to resolve.
lower_route_limit_ir
#1228: a route’s own @limit(maxBody:) annotation, if present — the override half of emitter/workers_entry.rs’s own (now superseded) effective_max_body; the service-wide limits { maxBody } fallback stays that function’s own concern (already IR-native via PolicyIr::max_body_bytes, but read from a service, not a per-route Handler, so it does not move here). Project validation (bynk.http.limit_*/limits_*) has already rejected a malformed or misplaced @limit, so an absent/ill-formed annotation here simply yields None — the caller’s own service-default fallback still applies. No &CheckedProgram needed, same reasoning as lower_route_cache_ir: maxBody is an already-resolved ExprKind::IntLit, not a type.
lower_service_handler_ir
P6.11’s real service-handler IrHandler constructor ([DECISION E], #1171) — the sibling to lower_handler_ir, not a widening of it. lower_fn_body_ir’s own doc comment already states the governing rule (“a free fn/method and an agent handler seed genuinely different scopes … that would otherwise have to coexist behind one signature for no shared benefit”); the same reasoning gives the same answer here, more strongly:
lower_service_handler_signature_ir
#1187’s slice 5 (the Service emitter cutover): emit_service’s own standalone entry point for a handler’s resolved signature only — params/ret/effectful, never the body. Deliberately does not build a real IrHandler/bynk_ir::IrItem::Service: both lower_handler_ir/lower_service_handler_ir unconditionally lower the handler’s own body into a real IrExpr (IrHandler::body is not Option), and an ordinary from http handler’s body routinely uses ? propagation (ExprKind::Question) or an is-expression (ExprKind::Is). Correction (P6.25, 2026-08-19): both landedQuestion as P6.15 (ADR 0337, lower_question_ir, decomposing to IrExprKind::Match per #1225’s own Ok/Err/Some/None identity precedent) and Is as P6.16 (ADR 0338, lower_is_ir, a forced-temp Let discharging R5.10). Neither is reached from this call site or any other shipped emitter path yet — emitter/lower.rs’s own P6.2 Call/Lambda cutover hasn’t landed — so the reasoning below (a real IrHandler here would still panic on other, still-unconverted constructs reachable from an ordinary from http body) stands, just not on Question/Is specifically anymore. Building a real IrHandler at emit_service’s own call site would panic on exactly the ordinary Http services this slice needs to keep working. Mirrors body_writes_state’s own precedent (#1196): a narrow, standalone reader of already-resolved data, not the full IrItem/IrHandler assembly — the same posture, applied to signature data instead of a single boolean. Deliberately not lower_handler_signature_ir(h, &cx) (review of #1198) — that helper’s own ADR 0334 .unwrap_or_else(|| panic!(..)) on a resolution miss is correct for an agent handler (the checker guarantees resolution there) but not for a service one: resolver.rs skips CommonsItem::Service in every type-ref-resolution pass, check_handler_body silently skips a param whose type doesn’t resolve and silently returns on an unresolvable return type (no diagnostic either way), and check_http_handler only constrains a param’s name (path segment or body), never validates a body: param’s own declared type. A service handler naming an undeclared type certifies today (and previously just emitted that bad name verbatim, a tsc-only failure) — reusing the strict helper here would turn that pre-existing, real-but-harmless-to-the-compiler gap into an ICE on the production emit path for every service in every project (confirmed live: on POST("/x") (body: Nope) -> Effect[HttpResult[String]] by v: Visitor { ... } panics bynkc before this fix). Mirrors lower_protocol_ir’s own Ty::Unit-on-miss posture, for the identical underlying reason (that function’s own doc comment already documents the checker’s Service-wide resolution gap).
lower_service_item_ir
P6.11 (#1171): assemble a service declaration into a real bynk_ir::IrItem::Service — structural mirror of lower_agent_item_ir, wiring lower_protocol_ir, lower_service_handler_ir and lower_policy_ir rather than re-deriving any of their logic. Unlike the agent case, there is no shared per-declaration context to compute once: a service has no store_cells/state_ty/invariants/transitions (none of those concepts exist for a service at all), so &service.protocol, already on the declaration, is threaded to each handler call directly — the “compute once, reuse” step lower_agent_item_ir’s own doc comment describes degenerates to a borrow here, not because this function skips a step, but because a service’s own handlers have nothing else to share.
lower_store_field_ir
P6.7 (#1163): lower an agent store field declaration into a real StoreFieldIr — dispatches on f.kind.head.name into StoreKindIr’s five real variants; Queue cannot reach a certified program (bynk.store.kind_unsupported gates it before certify, R3.10), so this match is total over what one can actually contain, not a gap needing its own extension later.
lower_store_field_shape_ir
#1187’s Agent state-field slice: lower_store_field_ir’s shape-only sibling — same kind/indexed (via store_field_kind_and_indexed), init always None. This is the entry point emit_agent’s own state section actually needs: a field’s storage shape (its Cell/Map/ Set/Cache/Log kind and @indexed keys), never its Cell zero/ initial value expression. Deliberately never lowers init, unlike lower_store_field_ir — a Cell field’s initializer can be an is-expression (= x is SomeVariant), which still hits ExprKind::Is’s own todo!() a few hundred lines below (1029_agent_static_init_hoist’s store active: Cell[Bool] = if true { 5 is PositiveInt } else { false } hits exactly this on lower_store_field_ir’s own init arm). The sibling = None shape (223_store_cell_agent’s own store paymentRef: Cell[Option[AuthId]] = None) no longer needs this workaround as of #1225’s own ADR — lower_store_field_ir lowers it directly now (store_field_cell_option_init_none_lowers_without_panicking). This function’s callers never need init at all regardless, so neither gap is a risk for them.
lower_transition_ir
P6.8 ([DECISION A]/[DECISION E], #1165): lower a step invariant (Transition) into a real IrPredicate — seeds old/new, both bound to the agent’s own synthetic state-record type, exactly as checker::check_transitions does. state_ty is a parameter, not re-derived, for the same reason lower_invariant_ir’s own store_cells is: no persisted “this agent’s state type” table survives past check_agent_decls’s own transient scope. Called once per agent’s own transition list, by the same future caller lower_invariant_ir names.
lower_type_item_ir
P6.6 (#1161): lower a type declaration into a real IrItem::TypeIrItem’s own doc comment names which of its seven design-sketch variants are real as of this slice (Type/Fn only, Decision D). Takes a certified &CheckedProgram, matching this module’s own categorical discipline (this file’s own header doc: “every entry point here takes a &CheckedProgram”), even though only TypedCommons::types/ty_intern are read — no per-expression expr_types lookup is involved (Q2, design/tracks/the-ir.md §3.2), but which fields are read isn’t the discipline; which failures are allowed to panic! is. Every panic below asserts “the checker already accepted this declaration” — true only once certify has run: a bare TypedCommons is not certified by construction (checker.rs’s own CheckedProgram doc notes the project/batch path holds per-unit TypedCommons values before that unit’s build-wide gate is decided), so accepting one here would make resolve_type_ref_in returning None a reachable, not just a buggy, outcome.

Type Aliases§

HandlerSignatureIr
(params, given, ret, effectful)lower_handler_signature_ir’s own return shape, and lower_service_handler_signature_ir’s (#1187’s slice 5), reused as a named alias rather than a bare tuple at both call sites once one of them (emit_service, bynk-emit/src/emitter/emit.rs) had to spell it out in a function signature.