5.7 KiB
5.7 KiB
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, optionalconst_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.
- Holds:
-
BlockCtx<'ctx>
- Holds:
cur_bid,cur_llbb(frombb_map), optionallysuccs. - Role: The canonical site for “where to insert” decisions; pred-end casts are routed via this box.
- Holds:
-
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.
- Holds:
-
StringOps (lightweight types/helpers)
- Types:
StrHandle(IntValue<'ctx>),StrPtr(PointerValue<'ctx>). - Rule: across blocks keep
StrHandle; convert toStrPtronly at call sites in the same BB; return values of string ops are kept asStrHandle. - Entry:
StringOps::concat(ctx, blk, lhs, rhs) -> LlResult<StrHandle>etc.
- Types:
API Sketch
pub type LlResult<T> = Result<T, String>;
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<ValueId> }
impl<'ctx, 'b> LowerFnCtx<'ctx, 'b> {
pub fn ensure_i64(&mut self, blk: &BlockCtx<'ctx>, v: ValueId) -> LlResult<IntValue<'ctx>> { /* Resolver-only */ }
pub fn ensure_ptr(&mut self, blk: &BlockCtx<'ctx>, v: ValueId) -> LlResult<PointerValue<'ctx>> { /* i64 PHI -> inttoptr here */ }
pub fn with_pred_end<R>(&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<StrHandle> { /* … */ }
}
Invariants Enforced by Design
- Resolver-only:
VMapis not exposed; all value access goes throughLowerFnCtxutilities. - Localization discipline: PHIs at BB head; casts at pred-end via
with_pred_end. - Strings handle rule: Only
StrHandlecrosses block boundaries;StrPtrgenerated 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 -lmust be0.
Migration Plan
- Introduce
LowerFnCtx/BlockCtx/InvokeCtx; migratelower_boxcalland invoke path first. - Move string ops to
StringOpswith handle-only rule; clean call sites. - Migrate BinOp/Compare/ExternCall to
LowerFnCtx + BlockCtxAPI. - 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.getin lowering/instructions). - Dominance: verifier green on representative functions (e.g., dep_tree_min_string).
Context Stack Guide (If/Loop)
Purpose
- Make control-flow merge/loop boundaries explicit and uniform across builders (MIR) and the JSON v0 bridge.
Stacks in MirBuilder
- If merge:
if_merge_stack: Vec<BasicBlockId>- Push the merge target before lowering branches, pop after wiring edge copies or Phi at the merge.
- PHI-off: emit per-predecessor edge copies into the merge pred blocks; merge block itself must not add a self-copy.
- PHI-on: place Phi(s) at the merge block head; inputs must cover all predecessors.
- Loop context:
loop_header_stack/loop_exit_stack- Header = re-check condition; Exit = after-loop block.
continue→ jump to Header;break→ jump to Exit. Both add predecessor metadata from the current block.- Builder captures variable-map snapshots on
continueto contribute latch-like inputs when sealing the header.
JSON v0 Bridge parity
- Bridge
LoopContext { cond_bb, exit_bb }mirrors MIR loop stacks. continuelowers toJump { target: cond_bb };breaklowers toJump { target: exit_bb }.
Verification hints
- Use-before-def: delay copies that would reference later-defined values or route via Phi at block head.
- Pred consistency: for every
Jump/Branch, record the predecessor on the successor block. - PHI-off invariant: all merged values reach the merge via predecessor copies; the merge block contains no extra Copy to the same dst.
Snapshot rules (Loop/If)
- Loop: take the latch snapshot at the actual latch block (end of body, after nested if merges). Use it as the backedge source when sealing header.
- If: capture
pre_if_snapshotbefore entering then/else; restore at merge and only bind merged variables (diff-based). Avoid self-copy at merge.