# Lowering Contexts (Boxing the API) Motivation - Current lowering functions carry too many parameters (10+), scattering responsibilities and making invariants (Resolver-only, localization discipline, dispatch-only PHI) hard to enforce. - Following Nyash’s “Everything is Box” philosophy, we group related data into small, composable context boxes to create clean boundaries and make invariants structural. Core Context Boxes - LowerFnCtx<'ctx, 'b> - Holds: `codegen`, `func`, `cursor`, `resolver`, `vmap`, `bb_map`, `preds`, `block_end_values`, `phis_by_block`, optional `const_strs`, `box_type_ids`. - Utilities: `ensure_i64/ptr/f64` (Resolver-only), `with_block(..)`, `with_pred_end(..)`, `guard_post_terminator`. - Effect: All lowering entrypoints can accept a single `&mut LowerFnCtx`. - BlockCtx<'ctx> - Holds: `cur_bid`, `cur_llbb` (from `bb_map`), optionally `succs`. - Role: The canonical site for “where to insert” decisions; pred-end casts are routed via this box. - InvokeCtx - Holds: `method_id: u16`, `type_id: i64`, `recv_h: IntValue<'ctx>`, `args: &[ValueId]`. - Role: Unifies plugin invoke by-id/by-name into a single API surface. - StringOps (lightweight types/helpers) - Types: `StrHandle(IntValue<'ctx>)`, `StrPtr(PointerValue<'ctx>)`. - Rule: across blocks keep `StrHandle`; convert to `StrPtr` only at call sites in the same BB; return values of string ops are kept as `StrHandle`. - Entry: `StringOps::concat(ctx, blk, lhs, rhs) -> LlResult` etc. API Sketch ``` pub type LlResult = Result; pub struct LowerFnCtx<'ctx, 'b> { /* see above */ } pub struct BlockCtx<'ctx> { pub cur_bid: BasicBlockId, pub cur_llbb: BasicBlock<'ctx> } pub struct InvokeCtx<'ctx> { pub method_id: u16, pub type_id: i64, pub recv_h: IntValue<'ctx>, pub args: Vec } impl<'ctx, 'b> LowerFnCtx<'ctx, 'b> { pub fn ensure_i64(&mut self, blk: &BlockCtx<'ctx>, v: ValueId) -> LlResult> { /* Resolver-only */ } pub fn ensure_ptr(&mut self, blk: &BlockCtx<'ctx>, v: ValueId) -> LlResult> { /* i64 PHI -> inttoptr here */ } pub fn with_pred_end(&mut self, pred: BasicBlockId, f: impl FnOnce(&Builder) -> R) -> R { /* insert before terminator */ } } pub fn lower_boxcall(ctx: &mut LowerFnCtx, blk: &BlockCtx, inst: &BoxCallInst) -> LlResult<()> { /* … */ } pub fn try_handle_tagged_invoke(ctx: &mut LowerFnCtx, blk: &BlockCtx, call: &InvokeCtx) -> LlResult<()> { /* … */ } pub mod string_ops { pub struct StrHandle<'ctx>(pub IntValue<'ctx>); pub struct StrPtr<'ctx>(pub PointerValue<'ctx>); pub fn concat(ctx: &mut LowerFnCtx, blk: &BlockCtx, lhs: ValueId, rhs: ValueId) -> LlResult { /* … */ } } ``` Invariants Enforced by Design - Resolver-only: `VMap` is not exposed; all value access goes through `LowerFnCtx` utilities. - Localization discipline: PHIs at BB head; casts at pred-end via `with_pred_end`. - Strings handle rule: Only `StrHandle` crosses block boundaries; `StrPtr` generated only in the same BB at call sites. - LoopForm rule: preheader mandatory, header condition built via Resolver; dispatch-only PHI. Dev guard checks enforce these. Dev Guards (optional, recommended) - `PhiGuard::assert_dispatch_only(&LowerFnCtx)` to fail fast when non-dispatch PHIs appear. - `LoopGuard::assert_preheader(&LowerFnCtx)` to ensure preheader presence and header i1 formation point. - CI Deny-Direct: `rg -n "vmap\.get\(" src/backend/llvm/compiler/codegen/instructions | wc -l` must be `0`. Migration Plan 1) Introduce `LowerFnCtx`/`BlockCtx`/`InvokeCtx`; migrate `lower_boxcall` and invoke path first. 2) Move string ops to `StringOps` with handle-only rule; clean call sites. 3) Migrate BinOp/Compare/ExternCall to `LowerFnCtx + BlockCtx` API. 4) Turn on dev guards; remove remaining fallback paths; simplify code. Acceptance - Refactored entrypoints accept at most three boxed parameters. - Deny-Direct passes (no direct `vmap.get` in lowering/instructions). - Dominance: verifier green on representative functions (e.g., dep_tree_min_string).