From 0cef028b6aaf4d67722320036f7ed8da754c6f55 Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 9 Nov 2026 11:51:00 +0100 Subject: [PATCH 1/6] Bind values to vars. --- src/backend/ir/debug_info.cpp | 48 ++++++++++++++++++++++++ src/backend/ir/debug_info.h | 24 ++++++++++++ src/backend/ir/function.cpp | 31 ++++++++++++++- src/backend/ir/function.h | 5 +++ src/backend/ir/node.cpp | 2 + src/backend/pass/opt/inline.cpp | 22 +++++++++++ src/backend/pass/opt/inline.h | 1 + src/backend/pass/opt/simplify_cfg.cpp | 4 ++ src/backend/pass/opt/strength_reduce.cpp | 4 ++ src/compiler/emit/emit.h | 2 +- src/compiler/emit/emit_debug.cpp | 14 +++++-- 11 files changed, 151 insertions(+), 6 deletions(-) diff --git a/src/backend/ir/debug_info.cpp b/src/backend/ir/debug_info.cpp index 5c706f01..f40ae14a 100644 --- a/src/backend/ir/debug_info.cpp +++ b/src/backend/ir/debug_info.cpp @@ -1,5 +1,7 @@ #include "ir/debug_info.h" +#include "ir/node.h" + namespace rat { DebugType* DebugInfo::makeType(DebugType::Kind kind, String name, U32 size) { types.push_back(std::make_unique()); @@ -19,4 +21,50 @@ namespace rat { locs.push_back({src, scope}); return (U32)locs.size() - 1; } + // a constant keeps its value wherever it sits, so it needs no link + void DebugBody::link(Node* n, U32 binding) { + if(!n || isa(n)) + return; + U32 id = n->getId(); + if(id >= refHead.size()) + refHead.resize(std::max(id + 1, (U32)refHead.size() * 2), 0); + refs.push_back({binding, refHead[id]}); + refHead[id] = (U32)refs.size(); + } + + void DebugBody::bind(U32 inst, Node* value, Node* anchor, Node* after) { + U32 id = (U32)bindings.size(); + bindings.push_back({inst, value, anchor, after}); + link(value, id); + link(anchor, id); + link(after, id); + } + + void DebugBody::mergeArm(Node* arm, Node* to) { + U32 id = arm->getId(); + for(U32 r = id < refHead.size() ? refHead[id] : 0; r; r = refs[r - 1].next) + if(bindings[refs[r - 1].binding].anchor == arm) + bindings[refs[r - 1].binding].value = nullptr; + retarget(arm, to); + } + + void DebugBody::retarget(Node* from, Node* to) { + U32 id = from->getId(); + if(id >= refHead.size()) + return; + U32 r = refHead[id]; + refHead[id] = 0; + for(; r; r = refs[r - 1].next) { + U32 binding = refs[r - 1].binding; + DebugBinding& b = bindings[binding]; + B32 moved = false; + for(Node** slot : {&b.value, &b.anchor, &b.after}) + if(*slot == from) { + *slot = to; + moved = true; + } + if(moved) + link(to, binding); + } + } } // namespace rat diff --git a/src/backend/ir/debug_info.h b/src/backend/ir/debug_info.h index 298502d4..b1146071 100644 --- a/src/backend/ir/debug_info.h +++ b/src/backend/ir/debug_info.h @@ -6,6 +6,7 @@ namespace rat { struct AllocNode; + struct Node; struct SrcLoc { U32 file = 0; // DebugInfo::files @@ -91,14 +92,37 @@ namespace rat { AllocNode* slot = nullptr; // frame storage }; + // var inst holds value from where control reaches anchor on, past the store or call after, a null + // value is optimized out + struct DebugBinding { + U32 inst; + Node* value; + Node* anchor; + Node* after; // the last effect made before it, null = none + }; + // debug state of one IR function, inlining copies it into the caller struct DebugBody { DebugFunc* func; List locs = List(1); // 0 = no location List scopes = List(1); // 0 = the body List vars; + List bindings; + // bindings naming a node, linked per node id, constants are never relinked + struct Ref { + U32 binding; + U32 next; // + 1, 0 ends + }; + List refHead; // node id -> first ref + 1 + List refs; U32 addLoc(SrcLoc src, U32 scope); + void bind(U32 inst, Node* value, Node* anchor, Node* after); + // moves the bindings of from onto to, null drops them + void retarget(Node* from, Node* to); + // bindings anchored at a branch arm merged into to hold on one path only, they lose their value + void mergeArm(Node* arm, Node* to); + void link(Node* n, U32 binding); }; struct DebugInfo { diff --git a/src/backend/ir/function.cpp b/src/backend/ir/function.cpp index 87fe3125..7d0f5f9d 100644 --- a/src/backend/ir/function.cpp +++ b/src/backend/ir/function.cpp @@ -121,6 +121,7 @@ namespace rat { StoreNode* s = create(memTy(), control(), mem(), pointer, value); s->setVolatile(isVolatile); set(memVar, s); + lastEffect = s; } Node* Function::global(const String& name) { return create(ptrTy(), name); } @@ -138,6 +139,7 @@ namespace rat { } void Function::bindEffects(Node* n) { + lastEffect = n; Node* ctrlProj = proj(n, CallNode::controlProjIndex(), ctrlTy(), "ctrl"); if(cur->ctrl) cur->ctrl = ctrlProj; @@ -288,7 +290,28 @@ namespace rat { } Node* Function::get(Var var) { return read(var, cur); } - void Function::set(Var var, Node* value) { cacheDef(cur, var, value); } + void Function::set(Var var, Node* value) { + cacheDef(cur, var, value); + noteBinding(var, value, cur->ctrl, lastEffect); + } + + void Function::bindVar(Var var, U32 inst) { + if(varInst.size() <= var) + varInst.resize(var + 1, 0); + varInst[var] = inst + 1; + } + + void Function::bindDebug(U32 inst, Node* value) { + if(cur->ctrl) + debug->bind(inst, value, cur->ctrl, lastEffect); + } + + // for a debug bound var, a phi binds after no effect as it holds from its block start + void Function::noteBinding(Var var, Node* value, Node* anchor, Node* after) { + if(var < varInst.size() && varInst[var] && anchor) + debug->bind(varInst[var] - 1, value, anchor, after); + } + Node* Function::mem() { return get(memVar); } Node** Function::findDef(Block* block, Var var) { @@ -320,12 +343,14 @@ namespace rat { // unsealed (loop header): an incomplete phi, completed at seal() PhiNode* p = phi(varTypes[var], block->region, {}); block->incompletePhis.push_back({var, p}); + noteBinding(var, p, block->ctrl, nullptr); val = p; } else if(block->preds.size() == 1) { val = read(var, block->predBlocks[0]); } else { PhiNode* p = phi(varTypes[var], block->region, {}); cacheDef(block, var, p); // break cycles before reading predecessors + noteBinding(var, p, block->ctrl, nullptr); val = addPhiOperands(var, p, block); } cacheDef(block, var, val); @@ -423,6 +448,8 @@ namespace rat { for(Node* n : nodes) if(!mark[n->getId()]) nodes[kept++] = n; + else if(debug) + debug->retarget(n, nullptr); U32 removed = (U32)nodes.size() - kept; nodes.resize(kept); return removed; @@ -487,6 +514,8 @@ namespace rat { void Function::removeNode(Node* n) { touch(); + if(debug) + debug->retarget(n, nullptr); n->clearInputs(); auto it = std::find(nodes.begin(), nodes.end(), n); if(it != nodes.end()) diff --git a/src/backend/ir/function.h b/src/backend/ir/function.h index 07e93c99..e9eeb47b 100644 --- a/src/backend/ir/function.h +++ b/src/backend/ir/function.h @@ -171,6 +171,8 @@ namespace rat { Var declareLocal(String name, Node* init); Node* get(Var var); void set(Var var, Node* value); + void bindVar(Var var, U32 inst); + void bindDebug(U32 inst, Node* value); // terminators void ret(Node* value); @@ -219,6 +221,7 @@ namespace rat { // them without scanning every block void cacheDef(Block* block, Var var, Node* val); static Node** findDef(Block* block, Var var); + void noteBinding(Var var, Node* value, Node* anchor, Node* after); Node* addPhiOperands(Var var, PhiNode* phi, Block* block); Node* tryRemoveTrivialPhi(PhiNode* phi); void replacePhiEverywhere(PhiNode* phi, Node* with); @@ -259,6 +262,8 @@ namespace rat { List varTypes; Var memVar = 0; // reserved variable carrying the memory token Map>> phiDefSites; + List varInst; // var -> its debug var inst + 1 + Node* lastEffect = nullptr; // store or call last made, for debug bindings }; } // namespace rat diff --git a/src/backend/ir/node.cpp b/src/backend/ir/node.cpp index d5424cca..0aaaf818 100644 --- a/src/backend/ir/node.cpp +++ b/src/backend/ir/node.cpp @@ -116,6 +116,8 @@ namespace rat { void Node::replaceAllUsesWith(Node* value) { if(value == this) return; + if(DebugBody* dbg = fn->getDebug()) + dbg->retarget(this, value); if(userCount) fn->touch(); for(U32 u = 0; u < userCount; ++u) { diff --git a/src/backend/pass/opt/inline.cpp b/src/backend/pass/opt/inline.cpp index f040734f..fc65ba63 100644 --- a/src/backend/pass/opt/inline.cpp +++ b/src/backend/pass/opt/inline.cpp @@ -155,12 +155,31 @@ namespace rat { void InlinePass::inlineVars(Function& caller, const Function& callee) { DebugBody& to = *caller.getDebug(); U32 scopeBase = (U32)(to.scopes.size() - callee.getDebug()->scopes.size()); + U32 instBase = (U32)to.vars.size(); for(const DebugVarInst& v : callee.getDebug()->vars) { DebugVarInst copy = v; copy.scope = scopeBase + v.scope; copy.slot = dyn_cast(mapped(v.slot)); to.vars.push_back(copy); } + // clones carry the callee bindings, the params bind the call arguments. a constant may be erased + // or from another callee, it is read for its value only + for(const DebugBinding& b : callee.getDebug()->bindings) { + Node* value = isa(b.value) ? b.value : mapped(b.value); + if(Node* anchor = mapped(b.anchor)) + to.bind(instBase + b.inst, value, anchor, mapped(b.after)); + } + } + + // the store, call or asm that made mem, null when none did + Node* InlinePass::lastEffect(Node* mem) { + if(isa(mem)) + return mem; + ProjNode* p = dyn_cast(mem); + Node* producer = p ? p->getProducer() : nullptr; + if(isa(producer) || isa(producer)) + return producer; + return nullptr; } B32 InlinePass::inlineCallSite(Function& caller, CallNode* call, Function& callee) { @@ -174,6 +193,9 @@ namespace rat { Merged m = mergeReturns(caller, callee); if(!m.ctrl) return false; // callee never returns + // what came after the call comes after the callee effects + if(DebugBody* dbg = caller.getDebug()) + dbg->retarget(call, lastEffect(m.mem)); replaceCall(caller, call, m); return true; } diff --git a/src/backend/pass/opt/inline.h b/src/backend/pass/opt/inline.h index 78400003..fc9c5fb2 100644 --- a/src/backend/pass/opt/inline.h +++ b/src/backend/pass/opt/inline.h @@ -48,6 +48,7 @@ namespace rat { static void replaceCall(Function& caller, CallNode* call, const Merged& m); void inlineScopes(Function& caller, CallNode* call, const Function& callee); void inlineVars(Function& caller, const Function& callee); + static Node* lastEffect(Node* mem); B32 inlineCallSite(Function& caller, CallNode* call, Function& callee); private: const CallGraph* graph = nullptr; // of the current module run diff --git a/src/backend/pass/opt/simplify_cfg.cpp b/src/backend/pass/opt/simplify_cfg.cpp index 450acd63..9c4550be 100644 --- a/src/backend/pass/opt/simplify_cfg.cpp +++ b/src/backend/pass/opt/simplify_cfg.cpp @@ -201,6 +201,10 @@ namespace rat { } // splice the merge out of the control chain, then drop the diamond r->replaceAllUsesWith(iff->getControl()); + if(DebugBody* dbg = fn.getDebug()) { + dbg->mergeArm(a, iff->getControl()); + dbg->mergeArm(b, iff->getControl()); + } for(PhiNode* phi : phis) if(!phi->hasUsers()) fn.removeNode(phi); diff --git a/src/backend/pass/opt/strength_reduce.cpp b/src/backend/pass/opt/strength_reduce.cpp index 21a7af02..3cce8af4 100644 --- a/src/backend/pass/opt/strength_reduce.cpp +++ b/src/backend/pass/opt/strength_reduce.cpp @@ -149,6 +149,10 @@ namespace rat { List users(p->getUsers().begin(), p->getUsers().end()); for(Node* u : users) detail::widenUse(fn, u, p, q, cmp, bound); + if(DebugBody* dbg = fn.getDebug()) { + dbg->retarget(p->getValue(recIdx), q->getValue(recIdx)); + dbg->retarget(p, q); + } p->setInput(1 + recIdx, init); // p and its step are dead, cut their cycle return 1; } diff --git a/src/compiler/emit/emit.h b/src/compiler/emit/emit.h index 17b9a017..8872a307 100644 --- a/src/compiler/emit/emit.h +++ b/src/compiler/emit/emit.h @@ -351,7 +351,7 @@ namespace rat::cc { void debugFunction(Function& fn, const FuncDef* def); void debugSetLoc(Function& fn, U32 tok); U32 debugEnterScope(Function& fn, const Stmt* s); - void debugVar(Function& fn, DebugVar v, const Local& l); + void debugVar(Function& fn, DebugVar v, const Local& l, B32 hasValue); void debugLocal(Function& fn, const Declarator& d); void debugGlobal(const Declarator& d); diff --git a/src/compiler/emit/emit_debug.cpp b/src/compiler/emit/emit_debug.cpp index c679d8b5..bd6788d8 100644 --- a/src/compiler/emit/emit_debug.cpp +++ b/src/compiler/emit/emit_debug.cpp @@ -160,7 +160,7 @@ namespace rat::cc { Local l; if(!dbg.describeVars || !p.name || !func.scopes.lookup(*p.name, l)) continue; - debugVar(fn, {*p.name, debugType(p.type), debugLoc(p.offset), "", true}, l); + debugVar(fn, {*p.name, debugType(p.type), debugLoc(p.offset), "", true}, l, true); } } @@ -186,12 +186,18 @@ namespace rat::cc { return outer; } - // records v in the current scope with the frame slot l lives in, if any - void Emitter::debugVar(Function& fn, DebugVar v, const Local& l) { + // records v in the current scope and binds where l keeps its value + void Emitter::debugVar(Function& fn, DebugVar v, const Local& l, B32 hasValue) { DebugBody& body = *fn.getDebug(); body.func->vars.push_back(std::make_unique(std::move(v))); AllocNode* slot = !l.staticSymbol && l.inMem() ? dyn_cast(l.addr) : nullptr; body.vars.push_back({body.func->vars.back().get(), dbg.scope, slot}); + U32 inst = (U32)body.vars.size() - 1; + if(l.staticSymbol || l.inMem()) + return; + fn.bindVar(l.var, inst); + if(hasValue) + fn.bindDebug(inst, fn.get(l.var)); } void Emitter::debugLocal(Function& fn, const Declarator& d) { @@ -200,7 +206,7 @@ namespace rat::cc { return; String symbol = l.staticSymbol ? *l.staticSymbol : ""; DebugVar v{*d.name, debugObjectType(d.type, d.isArray, l.count), debugLoc(d.offset), symbol}; - debugVar(fn, std::move(v), l); + debugVar(fn, std::move(v), l, d.init != nullptr); } void Emitter::debugGlobal(const Declarator& d) { From e19fd6a4c37aad5dc61b6352299ef031c71b3332 Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 9 Nov 2026 14:07:00 +0100 Subject: [PATCH 2/6] Track var records. --- src/backend/codegen/machine_function.cpp | 30 ++++++++ src/backend/codegen/machine_function.h | 17 +++++ src/backend/codegen/reg_alloc.cpp | 71 +++++++++++++++--- src/backend/codegen/reg_alloc.h | 5 ++ src/backend/codegen/schedule.cpp | 8 ++ src/backend/codegen/schedule.h | 1 + src/backend/pass/emit/x86/x86_layout.cpp | 11 ++- src/backend/pass/emit/x86/x86_lower.cpp | 12 ++- src/backend/pass/emit/x86/x86_lower.h | 15 ++++ src/backend/pass/emit/x86/x86_lower_debug.cpp | 73 +++++++++++++++++++ 10 files changed, 232 insertions(+), 11 deletions(-) create mode 100644 src/backend/pass/emit/x86/x86_lower_debug.cpp diff --git a/src/backend/codegen/machine_function.cpp b/src/backend/codegen/machine_function.cpp index 334583b0..a2adc636 100644 --- a/src/backend/codegen/machine_function.cpp +++ b/src/backend/codegen/machine_function.cpp @@ -23,12 +23,16 @@ namespace rat { return o; } + MachineOperand MachineOperand::none(U32 w) { return make(Kind::None, w, 0); } + MachineOperand MachineOperand::symbol(const String& s) { MachineOperand o = make(Kind::Sym, 8, 0); o.name = &s; return o; } + U32& MachineBlock::dbgBefore(U32 k) { return k < insts.size() ? insts[k].dbg : dbgEnd; } + void MachineFunc::reset() { for(MachineBlock& b : blocks) { b.id = -1; @@ -37,7 +41,9 @@ namespace rat { b.preds.clear(); b.succs.clear(); b.insts.clear(); + b.dbgEnd = 0; } + dbg.clear(); nextVReg = 1; vregClass.clear(); frameBytes = 0; @@ -52,4 +58,28 @@ namespace rat { vregClass[v] = cls; return v; } + + void MachineFunc::addDbg(U32& chain, U32 inst, MachineOperand loc) { + if(dbg.empty()) + dbg.emplace_back(); + dbg.push_back({inst, loc, 0}); + chain = joinDbg(chain, (U32)dbg.size() - 1); + } + + U32 MachineFunc::joinDbg(U32 first, U32 second) { + if(!first || !second) + return first ? first : second; + U32 tail = first; + while(dbg[tail].next) + tail = dbg[tail].next; + dbg[tail].next = second; + return first; + } + + U32 MachineFunc::copyDbg(U32 chain) { + U32 head = 0; + for(U32 r = chain; r; r = dbg[r].next) + addDbg(head, dbg[r].inst, dbg[r].loc); + return head; + } } // namespace rat diff --git a/src/backend/codegen/machine_function.h b/src/backend/codegen/machine_function.h index 2a4ae300..8326e10a 100644 --- a/src/backend/codegen/machine_function.h +++ b/src/backend/codegen/machine_function.h @@ -46,12 +46,20 @@ namespace rat { static MachineOperand frameSlot(I32 s, U32 w = 8); static MachineOperand symbol(const String& s); static MachineOperand blockRef(I32 b); + static MachineOperand none(U32 w = 8); static MachineOperand make(Kind k, U32 w, U32 v); B32 isVReg() const { return kind == Kind::VReg; } B32 isPhys() const { return kind == Kind::Phys; } }; + // a debug var inst lives in loc from here on, a None loc is optimized out + struct DbgRec { + U32 inst; + MachineOperand loc; + U32 next; // in MachineFunc::dbg, 0 ends the chain + }; + using MachineDefs = SmallList; using MachineOperands = SmallList; @@ -65,6 +73,7 @@ namespace rat { I64 imm = 0; // backend-defined small immediate I64 imm2 = 0; // second backend-defined immediate U32 loc = 0; // DebugBody loc id + U32 dbg = 0; // DbgRec chain applied before this instr }; struct MachineBlock { @@ -74,6 +83,9 @@ namespace rat { SmallList preds; SmallList succs; List insts; + U32 dbgEnd = 0; // DbgRec chain applied past the last instr + + U32& dbgBefore(U32 k); // the chain applied before instr k, dbgEnd past the last }; struct MachineFuncAux { @@ -87,8 +99,13 @@ namespace rat { U32 frameBytes = 0; List usedCalleeSaved; UniquePtr aux; + List dbg; // chain storage, 0 unused VReg newVReg(U32 cls); + // a chain is its head index in dbg, 0 empty. addDbg appends in place, joinDbg returns first then second + void addDbg(U32& chain, U32 inst, MachineOperand loc); + U32 joinDbg(U32 first, U32 second); + U32 copyDbg(U32 chain); void reset(); // keeps the block storage }; } // namespace rat diff --git a/src/backend/codegen/reg_alloc.cpp b/src/backend/codegen/reg_alloc.cpp index 5409cb83..88a0f9e9 100644 --- a/src/backend/codegen/reg_alloc.cpp +++ b/src/backend/codegen/reg_alloc.cpp @@ -45,8 +45,13 @@ namespace rat { number(); liveness(); buildIntervals(); + B32 dbg = f.dbg.size() > 1; + if(dbg) + checkDebugLive(); coalesce(); assignRegs(); + if(dbg) + rewriteDebug(); rewrite(); fn->usedCalleeSaved.clear(); for(U64 m = usedCallee; m; m &= m - 1) @@ -251,6 +256,46 @@ namespace rat { chunk[s >> 6] |= busy[s]; } + B32 RegAllocPass::covers(VReg v, I32 pos) const { + const List& segs = iv[v].segs; + auto it = std::upper_bound(segs.begin(), segs.end(), Seg{pos, INT32_MAX}); + return it != segs.begin() && (it - 1)->second >= pos; + } + + // a record holds when its vreg is live right before the instr, or was just written by the one ahead, + // before bundles merge. stack resident vregs keep their slot + B32 RegAllocPass::liveAt(const MachineOperand& o, U32 b, U32 k) const { + I32 u = 2 * (I32)(blockFirst[b] + k); + B32 last = k == fn->blocks[b].insts.size(); + return cross[o.vreg] || (!last && covers(o.vreg, u)) || (k > 0 && covers(o.vreg, u - 1)); + } + + void RegAllocPass::checkDebugLive() { + for(U32 b = 0; b < fn->blocks.size(); ++b) { + MachineBlock& blk = fn->blocks[b]; + for(U32 k = 0; k <= blk.insts.size(); ++k) { + for(U32 r = blk.dbgBefore(k); r; r = fn->dbg[r].next) { + MachineOperand& o = fn->dbg[r].loc; + if(o.isVReg() && !liveAt(o, b, k)) + o = MachineOperand::none(o.width); + } + } + } + } + + // spilled bundles are found in their slot + void RegAllocPass::rewriteDebug() { + for(DbgRec& r : fn->dbg) { + if(!r.loc.isVReg()) + continue; + const Interval& t = bundle(r.loc.vreg); + if(t.reg != kNoReg) + r.loc = MachineOperand::fixed(t.reg, r.loc.width); + else + r.loc = MachineOperand::frameSlot(t.slot, r.loc.width); + } + } + // path halving VReg RegAllocPass::find(VReg v) { while(iv[v].root != v) @@ -485,21 +530,29 @@ namespace rat { return bundle(o.vreg).reg; } - // copies inside a bundle vanish + // copies inside a bundle vanish, debug records move to the first instr standing for theirs void RegAllocPass::rewrite() { List out; for(U32 b = 0; b < fn->blocks.size(); ++b) { - List& insts = fn->blocks[b].insts; + MachineBlock& blk = fn->blocks[b]; out.clear(); - out.reserve(insts.size()); - for(U32 k = 0; k < insts.size(); ++k) { - MachineInstr& in = insts[k]; + out.reserve(blk.insts.size()); + U32 dbg = 0; + for(U32 k = 0; k < blk.insts.size(); ++k) { + MachineInstr& in = blk.insts[k]; U32 i = blockFirst[b] + k; - if(copyAt[i] && (sameBundle(in.defs[0], in.uses[0]) || rewriteCopy(out, in, i))) - continue; - rewriteInstr(out, in, i); + U32 first = (U32)out.size(); + dbg = fn->joinDbg(dbg, in.dbg); + in.dbg = 0; + if(!copyAt[i] || !(sameBundle(in.defs[0], in.uses[0]) || rewriteCopy(out, in, i))) + rewriteInstr(out, in, i); + if(out.size() > first) { + out[first].dbg = dbg; + dbg = 0; + } } - insts.swap(out); + blk.dbgEnd = fn->joinDbg(dbg, blk.dbgEnd); + blk.insts.swap(out); } } diff --git a/src/backend/codegen/reg_alloc.h b/src/backend/codegen/reg_alloc.h index d2394b21..01942ea2 100644 --- a/src/backend/codegen/reg_alloc.h +++ b/src/backend/codegen/reg_alloc.h @@ -50,6 +50,11 @@ namespace rat { void assignSlots(List>& spilled); I32 newSlot(U32 cls); PhysReg pick(VReg v) const; + // debug records + B32 covers(VReg v, I32 pos) const; + B32 liveAt(const MachineOperand& o, U32 b, U32 k) const; + void checkDebugLive(); + void rewriteDebug(); // rewrite void rewrite(); B32 rewriteCopy(List& out, const MachineInstr& in, U32 i); diff --git a/src/backend/codegen/schedule.cpp b/src/backend/codegen/schedule.cpp index 9ded3305..24f683ee 100644 --- a/src/backend/codegen/schedule.cpp +++ b/src/backend/codegen/schedule.cpp @@ -602,6 +602,14 @@ namespace rat { I32 Schedule::blockOf(const Node* n) const { return n ? nodeBlock[n->getId()] : -1; } + // a head starts its block, a call or asm control proj follows its producer + I32 Schedule::anchorBlock(const Node* ctrl) const { + if(I32 b = blockOfHead(ctrl); b >= 0) + return b; + const ProjNode* p = dyn_cast(ctrl); + return p ? blockOf(p->getProducer()) : -1; + } + NodeSpan Schedule::phis(I32 b) const { return NodeSpan{phiFlat.data() + phiStart[b], (U32)(phiStart[b + 1] - phiStart[b])}; } diff --git a/src/backend/codegen/schedule.h b/src/backend/codegen/schedule.h index e72fc2ec..ed5b1bc3 100644 --- a/src/backend/codegen/schedule.h +++ b/src/backend/codegen/schedule.h @@ -58,6 +58,7 @@ namespace rat { const List& allocNodes() const; I32 blockOf(const Node* n) const; + I32 anchorBlock(const Node* ctrl) const; U32 succCount(I32 b) const; I32 succAt(I32 b, U32 i) const; diff --git a/src/backend/pass/emit/x86/x86_layout.cpp b/src/backend/pass/emit/x86/x86_layout.cpp index 858dc705..8e49b08d 100644 --- a/src/backend/pass/emit/x86/x86_layout.cpp +++ b/src/backend/pass/emit/x86/x86_layout.cpp @@ -34,9 +34,15 @@ namespace rat { if(b.id < 0 || !isPureTestBlock(b)) continue; - // replace the jmp with a copy of the test block's instructions + // replace the jmp with a copy of the test block's instructions, debug records copied along + U32 dbg = mf.joinDbg(term.dbg, p.dbgEnd); p.insts.pop_back(); + U32 first = (U32)p.insts.size(); p.insts.insert(p.insts.end(), b.insts.begin(), b.insts.end()); + for(U32 i = first; i < (U32)p.insts.size(); ++i) + p.insts[i].dbg = mf.copyDbg(p.insts[i].dbg); + p.insts[first].dbg = mf.joinDbg(dbg, p.insts[first].dbg); + p.dbgEnd = mf.copyDbg(b.dbgEnd); // p's successor edge moves from b to b's successors p.succs.erase(std::remove(p.succs.begin(), p.succs.end(), t), p.succs.end()); @@ -174,6 +180,9 @@ namespace rat { I32 id = mf.blocks[i].id; if(id >= 0 && !placed[(U32)id]) { MachineBlock stub = std::move(mf.blocks[i]); + // debug records stay, the successor lists still route through a skipped jump + for(U32 k = (U32)stub.insts.size(); k-- > 0;) // backward, each join walks one instr chain + stub.dbgEnd = mf.joinDbg(stub.insts[k].dbg, stub.dbgEnd); stub.insts.clear(); arranged.push_back(std::move(stub)); } diff --git a/src/backend/pass/emit/x86/x86_lower.cpp b/src/backend/pass/emit/x86/x86_lower.cpp index 40657769..7689353a 100644 --- a/src/backend/pass/emit/x86/x86_lower.cpp +++ b/src/backend/pass/emit/x86/x86_lower.cpp @@ -498,6 +498,10 @@ namespace rat { layout(); const List& order = sched->rpo(); out->blocks.resize(sched->numBlocks()); + BindingPlan plan; + B32 bind = fn->getDebug() && !fn->getDebug()->bindings.empty(); + if(bind) + planBindings(plan); for(U32 i = 0; i < order.size(); ++i) { I32 b = order[i]; MachineBlock& block = out->blocks[b]; @@ -507,15 +511,21 @@ namespace rat { block.succs.reserve(sched->succCount(b)); block.preds.reserve(sched->predCount(b)); mb = █ - if(i == 0) + if(i == 0) { emitPrologue(); + plan.prologueEnd = (U32)block.insts.size(); + } for(Node* n : sched->nodes(b)) { curLoc = n->getLoc(); emitNode(n); + if(bind) + plan.nodeEnd[n->getId()] = (U32)block.insts.size(); } Node* term = sched->block(b).termNode; curLoc = term ? term->getLoc() : 0; emitTerminator(b); + if(bind) + placeBindings(b, plan); } for(I32 b : order) for(U32 i = 0, e = sched->succCount(b); i < e; ++i) { diff --git a/src/backend/pass/emit/x86/x86_lower.h b/src/backend/pass/emit/x86/x86_lower.h index 8ff93e6f..a3201de7 100644 --- a/src/backend/pass/emit/x86/x86_lower.h +++ b/src/backend/pass/emit/x86/x86_lower.h @@ -4,6 +4,7 @@ #include "core.h" #include "codegen/schedule.h" +#include "ir/debug_info.h" #include "ir/opcode.h" #include "pass/emit/x86/x86_op.h" #include "pass/pass.h" @@ -299,6 +300,20 @@ namespace rat { void moveValue(VReg dst, VReg src, U32 cls, U32 w); void emitPhiCopies(I32 targetBlock, I32 predIdx); void emitTerminator(I32 b); + // debug bindings of the function, grouped by the block of their anchor + struct BindingPlan { + List first; // block -> first of its bindings in order + List order; // binding indexes + List nodeEnd; // node id -> insts index past its code + List cursor; // var inst -> least position left in the block + U32 prologueEnd = 0; + }; + void planBindings(BindingPlan& plan) const; + U32 endIn(const Node* n, I32 b, const BindingPlan& plan) const; + U32 sourcePos(const DebugBinding& bd, I32 b, const BindingPlan& plan) const; + U32 valuePos(const DebugBinding& bd, I32 b, const BindingPlan& plan) const; + MachineOperand bindingLoc(const Node* value) const; + void placeBindings(I32 b, BindingPlan& plan); private: // target const X86CallConv* conv = &abi::kSysV; diff --git a/src/backend/pass/emit/x86/x86_lower_debug.cpp b/src/backend/pass/emit/x86/x86_lower_debug.cpp new file mode 100644 index 00000000..3b9d17d4 --- /dev/null +++ b/src/backend/pass/emit/x86/x86_lower_debug.cpp @@ -0,0 +1,73 @@ +#include "pass/emit/x86/x86_lower.h" + +#include "codegen/reg_alloc.h" +#include "ir/function.h" +#include "ir/node.h" + +namespace rat { + void X86LowerPass::planBindings(BindingPlan& plan) const { + const DebugBody& body = *fn->getDebug(); + List> byBlock; // (anchor block, binding) + for(U32 i = 0; i < body.bindings.size(); ++i) { + const Node* anchor = body.bindings[i].anchor; + I32 b = anchor ? sched->anchorBlock(anchor) : -1; + if(b >= 0) + byBlock.push_back({(U32)b, i}); + } + detail::groupBy(byBlock, (U32)sched->numBlocks(), plan.first, plan.order); + plan.nodeEnd.assign(fn->idBound(), 0); + plan.cursor.assign(body.vars.size(), 0); + } + + // past the code of n when this block has it + U32 X86LowerPass::endIn(const Node* n, I32 b, const BindingPlan& plan) const { + return n && sched->blockOf(n) == b ? plan.nodeEnd[n->getId()] : 0; + } + + // past a call anchor and the effect before it in the source, after the var's last record + U32 X86LowerPass::sourcePos(const DebugBinding& bd, I32 b, const BindingPlan& plan) const { + U32 pos = std::max(plan.cursor[bd.inst], endIn(bd.after, b, plan)); + if(const ProjNode* p = dyn_cast(bd.anchor)) + pos = std::max(pos, endIn(p->getProducer(), b, plan)); + return pos; + } + + // past the code of the value when this block has it + U32 X86LowerPass::valuePos(const DebugBinding& bd, I32 b, const BindingPlan& plan) const { + const Node* v = bd.value; + if(!v || isa(v) || isa(v)) // constants may sit in another function + return 0; + const ProjNode* vp = dyn_cast(v); + if(vp && isa(vp->getProducer())) // params are copied in by the prologue + return b == sched->rpo()[0] ? plan.prologueEnd : 0; + return std::max(endIn(v, b, plan), vp ? endIn(vp->getProducer(), b, plan) : 0); + } + + // a vreg, an integer constant or nothing + MachineOperand X86LowerPass::bindingLoc(const Node* value) const { + MachineOperand none = MachineOperand::none(); + if(!value) + return none; + const Type* t = value->getType(); + if(const ConstantNode* c = dyn_cast(value)) + return t->isInt() || t->isPtr() ? MachineOperand::immVal(c->getValue(), opWidth(t)) : none; + VReg v = vregOf[value->getId()]; + return v == kNoVReg ? none : MachineOperand::vr(v, opWidth(t)); + } + + // records hang off the instr they precede, outside the instr lists + void X86LowerPass::placeBindings(I32 b, BindingPlan& plan) { + const DebugBody& body = *fn->getDebug(); + for(U32 k = plan.first[b]; k < plan.first[b + 1]; ++k) { + const DebugBinding& bd = body.bindings[plan.order[k]]; + U32 src = sourcePos(bd, b, plan); + U32 pos = std::max(src, valuePos(bd, b, plan)); + plan.cursor[bd.inst] = pos; + if(pos > src) // the old value is gone, the new one not there yet + out->addDbg(mb->dbgBefore(src), bd.inst, bindingLoc(nullptr)); + out->addDbg(mb->dbgBefore(pos), bd.inst, bindingLoc(bd.value)); + } + for(U32 k = plan.first[b]; k < plan.first[b + 1]; ++k) + plan.cursor[body.bindings[plan.order[k]].inst] = 0; + } +} // namespace rat From c33b21022d14bce51422fc35bcae50ac7b0a4a62 Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 9 Nov 2026 15:38:00 +0100 Subject: [PATCH 3/6] Fix var records in peephole. --- src/backend/pass/emit/x86/x86_peephole.cpp | 41 +++-- src/backend/pass/emit/x86/x86_peephole.h | 34 ++++- .../pass/emit/x86/x86_peephole_debug.cpp | 140 ++++++++++++++++++ 3 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 src/backend/pass/emit/x86/x86_peephole_debug.cpp diff --git a/src/backend/pass/emit/x86/x86_peephole.cpp b/src/backend/pass/emit/x86/x86_peephole.cpp index 174d0349..c96c3687 100644 --- a/src/backend/pass/emit/x86/x86_peephole.cpp +++ b/src/backend/pass/emit/x86/x86_peephole.cpp @@ -127,13 +127,14 @@ namespace rat { return (X86Op)in.op == X86Op::Load && in.regClass == detail::kGp && isAnySlotLoad(in); } - // replaces at, keeping its debug location + // replaces at, keeping its debug location and records MachineInstr X86PeepholePass::makeCopy(PhysReg dst, PhysReg src, const MachineInstr& at, U32 width) { MachineInstr m; m.op = (MachineOpcode)X86Op::Copy; m.regClass = at.regClass; m.loc = at.loc; + m.dbg = at.dbg; m.defs = {MachineOperand::fixed(dst, width)}; m.uses = {MachineOperand::fixed(src, width)}; return m; @@ -210,9 +211,10 @@ namespace rat { st.killReg((PhysReg)countTrailingZeros64(m)); } - U32 X86PeepholePass::runOnBlock(MachineBlock& b) { + U32 X86PeepholePass::runOnBlock(MachineFunc& mf, MachineBlock& b) { U32 changed = 0; U32 kept = 0; // compacts in place, a fold never adds instructions + U32 dbg = 0; // records of dropped instrs, for the next kept one st.reset(); for(U32 i = 0; i < (U32)b.insts.size(); ++i) { MachineInstr& in = b.insts[i]; @@ -225,13 +227,18 @@ namespace rat { changed += foldSlotLoad(in, keep); else stepOther(in); - if(!keep) + if(!keep) { + dbg = mf.joinDbg(dbg, in.dbg); continue; + } + in.dbg = mf.joinDbg(dbg, in.dbg); + dbg = 0; slots.note(in, (U32)b.id); if(kept != i) b.insts[kept] = std::move(in); ++kept; } + b.dbgEnd = mf.joinDbg(dbg, b.dbgEnd); b.insts.erase(b.insts.begin() + kept, b.insts.end()); return changed; } @@ -451,21 +458,33 @@ namespace rat { } dropped += here; if(here) - eraseMarked(b, drop); + eraseMarked(mf, b); } return dropped; } - // drop the flagged instructions - void X86PeepholePass::eraseMarked(MachineBlock& b, const List& drop) { + // drop the flagged instructions, their debug records go to the next kept one. with records, each + // drop seeds the debug taint at its place + void X86PeepholePass::eraseMarked(MachineFunc& mf, MachineBlock& b) { U32 kept = 0; + U32 dbg = 0; for(U32 i = 0; i < (U32)b.insts.size(); ++i) { - if(drop[i]) + MachineInstr& in = b.insts[i]; + if(drop[i]) { + dbg = mf.joinDbg(dbg, in.dbg); + if(mf.dbg.size() > 1 && in.defs.empty()) // a store leaves its slot stale + dbgSeeds.push_back({(U32)b.id, kept, in.uses[0], 0}); + else if(mf.dbg.size() > 1) // a normalize leaves its imm low bits right + dbgSeeds.push_back({(U32)b.id, kept, in.defs[0], (U32)in.imm}); continue; + } + in.dbg = mf.joinDbg(dbg, in.dbg); + dbg = 0; if(kept != i) - b.insts[kept] = std::move(b.insts[i]); + b.insts[kept] = std::move(in); ++kept; } + b.dbgEnd = mf.joinDbg(dbg, b.dbgEnd); b.insts.erase(b.insts.begin() + kept, b.insts.end()); } @@ -671,7 +690,7 @@ namespace rat { } removed += here; if(here) - eraseMarked(b, drop); + eraseMarked(mf, b); } return removed; } @@ -679,13 +698,15 @@ namespace rat { B32 X86PeepholePass::run(Module&, const Function&, MachineFunc& mf, const TargetInfo&) { seedQueued.clear(); U32 changed = elimRedundantExt(mf); + killTainted(mf); st.begin(mf.frameBytes / 8 + 1); slots.begin(mf); for(MachineBlock& b : mf.blocks) if(b.id >= 0) - changed += runOnBlock(b); + changed += runOnBlock(mf, b); slots.finish(); changed += elimDeadSlotStores(mf); + killTainted(mf); return changed != 0; } } // namespace rat diff --git a/src/backend/pass/emit/x86/x86_peephole.h b/src/backend/pass/emit/x86/x86_peephole.h index 4ffa85f8..bf899813 100644 --- a/src/backend/pass/emit/x86/x86_peephole.h +++ b/src/backend/pass/emit/x86/x86_peephole.h @@ -67,7 +67,7 @@ namespace rat { U32 foldSlotStore(MachineInstr& in, B32& keep); U32 foldSlotLoad(MachineInstr& in, B32& keep); void stepOther(const MachineInstr& in); - U32 runOnBlock(MachineBlock& b); + U32 runOnBlock(MachineFunc& mf, MachineBlock& b); // demanded-bits phase static B32 tracked(const MachineOperand& o) { return o.isPhys() && o.phys < kDemRegs; } @@ -81,7 +81,7 @@ namespace rat { static B32 writesFlags(X86Op op); static B32 flagSafeToDrop(const MachineBlock& b, U32 at); static void transfer(const MachineInstr& in, U64* dem); - static void eraseMarked(MachineBlock& b, const List& drop); + void eraseMarked(MachineFunc& mf, MachineBlock& b); static B32 orInto(U64* into, const List& from); void seedWork(const MachineFunc& mf, List& work, List& queued); void postorder(const MachineFunc& mf); @@ -113,11 +113,35 @@ namespace rat { static void slotStep(const MachineInstr& in, const TrackedSlots& slots, List& cur); U32 elimDeadSlotStores(MachineFunc& mf); + struct DbgSeed { + U32 block; + U32 at; // index of the instr after the drop + MachineOperand loc; + U32 bits; // low bits still right, 0 none + }; + struct DbgTaint { + // low bits right half then wrong half, each regs by phys then slots by slotKey + List marks; + U32 keys = 0; + U32 rightBits = 64; // low bits every dropped normalize leaves right + + I32 bitOf(const MachineOperand& o) const; + U32 level(const MachineOperand& o) const; + void set(const MachineOperand& o, U32 lv); + }; + + static B32 keepsLowBits(X86Op op); + static void taintStep(const MachineInstr& in, DbgTaint& t); + static void killRecords(MachineFunc& mf, U32 chain, const DbgTaint& t); + B32 taintPass(MachineFunc& mf, List>& entry, DbgTaint& t, B32 kill) const; + void killTainted(MachineFunc& mf); + ValueState st; TrackedSlots slots; - List seed; // block postorder - List seedQueued; // block -> in seed, empty until computed - List drop; // per instruction of the block being swept + List seed; // block postorder + List seedQueued; // block -> in seed, empty until computed + List drop; // per instruction of the block being swept + List dbgSeeds; // of the phase just run, by block }; } // namespace rat diff --git a/src/backend/pass/emit/x86/x86_peephole_debug.cpp b/src/backend/pass/emit/x86/x86_peephole_debug.cpp new file mode 100644 index 00000000..22f38ea0 --- /dev/null +++ b/src/backend/pass/emit/x86/x86_peephole_debug.cpp @@ -0,0 +1,140 @@ +#include "pass/emit/x86/x86_peephole.h" + +#include "codegen/machine_function.h" + +namespace rat { + // in the low bits right half, -1 when untracked + I32 X86PeepholePass::DbgTaint::bitOf(const MachineOperand& o) const { + static_assert(kMaxPhys <= 64, "regs fit word 0"); + if(o.isPhys()) + return o.phys < kMaxPhys ? (I32)o.phys : -1; + if(o.kind != MachineOperand::Kind::FrameSlot) + return -1; + U32 k = slotKey(o.slot, keys); + return k < keys ? 64 + (I32)k : -1; + } + + // 0 clean, 1 low bits right, 2 wrong + U32 X86PeepholePass::DbgTaint::level(const MachineOperand& o) const { + I32 bit = bitOf(o); + U32 lv = 0; + for(U32 p = 0; bit >= 0 && p < 2; ++p, bit += (I32)marks.size() * 32) + if((marks[bit >> 6] >> (bit & 63)) & 1) + lv = p + 1; + return lv; + } + + void X86PeepholePass::DbgTaint::set(const MachineOperand& o, U32 lv) { + I32 bit = bitOf(o); + for(U32 p = 0; bit >= 0 && p < 2; ++p, bit += (I32)marks.size() * 32) { + U64 mask = (U64)1 << (bit & 63); + if(lv == p + 1) + marks[bit >> 6] |= mask; + else + marks[bit >> 6] &= ~mask; + } + } + + // ops whose low result bits come from the low bits of their inputs only + B32 X86PeepholePass::keepsLowBits(X86Op op) { + switch(op) { + case X86Op::Copy: + case X86Op::Load: + case X86Op::Lea: + case X86Op::Add: + case X86Op::Sub: + case X86Op::Mul: + case X86Op::And: + case X86Op::Or: + case X86Op::Xor: + case X86Op::Not: + case X86Op::Neg: + case X86Op::Shl: + case X86Op::CMov: + case X86Op::MaskBits: + case X86Op::SignExtBits: + return true; + default: + return false; + } + } + + // low taint survives ops that keep low bits and ends at a normalization within the right bits, + // anything else turns it wrong + void X86PeepholePass::taintStep(const MachineInstr& in, DbgTaint& t) { + if(isAnySlotStore(in)) { + const MachineOperand& slot = in.uses[0]; + U32 kept = in.uses[1].width < 8 ? t.level(slot) : 0; + t.set(slot, std::max(kept, t.level(in.uses[1]))); + return; + } + U32 lv = 0; + for(const MachineOperand& u : in.uses) + lv = std::max(lv, t.level(u)); + X86Op op = (X86Op)in.op; + if(lv == 1 && !keepsLowBits(op)) + lv = 2; + if(lv == 1 && isNormalize(op) && (U64)in.imm <= t.rightBits) + lv = 0; + t.marks[0] &= ~in.clobbers; + t.marks[t.marks.size() / 2] &= ~in.clobbers; + for(const MachineOperand& d : in.defs) { + U32 kept = d.width < 4 ? t.level(d) : 0; + t.set(d, std::max(lv, kept)); + } + } + + void X86PeepholePass::killRecords(MachineFunc& mf, U32 chain, const DbgTaint& t) { + for(U32 r = chain; r; r = mf.dbg[r].next) { + MachineOperand& loc = mf.dbg[r].loc; + U32 lv = t.level(loc); + if(lv == 2 || (lv == 1 && loc.width * 8u > t.rightBits)) + loc = MachineOperand::none(loc.width); + } + } + + // one sweep in block order, true when a block entry taint grew + B32 X86PeepholePass::taintPass(MachineFunc& mf, + List>& entry, + DbgTaint& t, + B32 kill) const { + B32 grew = false; + const DbgSeed* seed = dbgSeeds.data(); + for(U32 bi = 0; bi < (U32)mf.blocks.size(); ++bi) { + B32 clean = std::count(entry[bi].begin(), entry[bi].end(), 0) == (I64)entry[bi].size(); + if(clean && seed->block != bi) + continue; // no taint enters or starts here + MachineBlock& b = mf.blocks[bi]; + t.marks = entry[bi]; + for(U32 i = 0; i <= (U32)b.insts.size(); ++i) { + for(; seed->block == bi && seed->at == i; ++seed) + t.set(seed->loc, seed->bits ? 1u : 2u); // a dead store leaves its slot wrong + if(kill) + killRecords(mf, b.dbgBefore(i), t); + if(i < b.insts.size()) + taintStep(b.insts[i], t); + } + for(I32 s : b.succs) + grew |= orInto(entry[(U32)s].data(), t.marks); + } + return grew; + } + + // forward may-dataflow from the seeds of the phase just run + void X86PeepholePass::killTainted(MachineFunc& mf) { + if(dbgSeeds.empty()) + return; + DbgTaint t; + t.keys = mf.frameBytes / 8 + 1; + for(const DbgSeed& s : dbgSeeds) + if(s.bits) + t.rightBits = std::min(t.rightBits, s.bits); + dbgSeeds.push_back({~0u, 0, {}, 0}); // ends the seeds + List> entry(mf.blocks.size(), List(2 * (1 + (t.keys + 63) / 64), 0)); + B32 grew = true; + while(grew) + grew = taintPass(mf, entry, t, false); + taintPass(mf, entry, t, true); + dbgSeeds.clear(); + } +} // namespace rat From a2081c89d019462bc74e23b2c8ebb0a41ea33dad Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 9 Nov 2026 17:55:00 +0100 Subject: [PATCH 4/6] Emit location lists. --- src/backend/pass/emit/x86/x86_encode.cpp | 15 ++ src/backend/pass/emit/x86/x86_encode.h | 22 +++ src/backend/pass/emit/x86/x86_encode_vars.cpp | 170 ++++++++++++++++++ src/backend/target/dwarf.cpp | 50 +++++- src/backend/target/dwarf.h | 25 +++ src/compiler/test/debug/debug.O1.expect | 21 ++- src/compiler/test/debug/scopes.O1.expect | 4 +- src/compiler/test/debug/text_check.sh | 29 +++ 8 files changed, 327 insertions(+), 9 deletions(-) create mode 100644 src/backend/pass/emit/x86/x86_encode_vars.cpp create mode 100755 src/compiler/test/debug/text_check.sh diff --git a/src/backend/pass/emit/x86/x86_encode.cpp b/src/backend/pass/emit/x86/x86_encode.cpp index 76e7c2e2..fd550907 100644 --- a/src/backend/pass/emit/x86/x86_encode.cpp +++ b/src/backend/pass/emit/x86/x86_encode.cpp @@ -648,6 +648,11 @@ namespace rat { a->code.reserve((U64)instCount * 16u + 64u); // cheap upper estimate blockOffset.assign(blockIdBound(f), 0); + varsIn.clear(); + const DebugBody* body = encodedFunc ? encodedFunc->fn->getDebug() : nullptr; + if(body && f.dbg.size() > 1) + solveVars(f); + B32 trackVars = !varsIn.empty(); prologue(); for(U32 bi = 0; bi < f.blocks.size(); ++bi) { const MachineBlock& blk = f.blocks[bi]; @@ -657,12 +662,21 @@ namespace rat { a->alignNop(blk.align); blockOffset[blk.id] = a->here(); I32 fallthrough = (bi + 1 < f.blocks.size()) ? (I32)f.blocks[bi + 1].id : -1; + if(trackVars) + beginVars(bi); for(const MachineInstr& in : blk.insts) { U32 at = a->here(); markLine(in.loc); + if(trackVars) + applyRecords(liveVars, in.dbg, at); emitInst(in, fallthrough); markScope(in.loc, at, a->here()); + // a write ends its vars one byte in, so the instr start still shows them but no later pc does + if(trackVars) + clobber(liveVars, in, std::min(at + 1, a->here())); } + if(trackVars) + endVars(blk); } // jump tables: 4-byte offsets from the table base, emitted after the code for(const PendingTable& t : tables) { @@ -717,6 +731,7 @@ namespace rat { if(f.getDebug()) { encodedFunc->lines.push_back({0, f.getDebug()->func->decl}); encodedFunc->scopes.resize(f.getDebug()->scopes.size()); + encodedFunc->varRanges.resize(f.getDebug()->vars.size()); } scope = 0; } diff --git a/src/backend/pass/emit/x86/x86_encode.h b/src/backend/pass/emit/x86/x86_encode.h index ad18fac1..b9bc7abf 100644 --- a/src/backend/pass/emit/x86/x86_encode.h +++ b/src/backend/pass/emit/x86/x86_encode.h @@ -38,6 +38,14 @@ namespace rat { U32 relocEnd; // one past its last reloc }; + // a debug var location not yet closed into a VarRange, lists of them are sorted by inst + struct OpenVarRange { + U32 inst; + VarLoc loc; // reg is a PhysReg + U32 start; + }; + static constexpr U32 kNoPc = ~0u; // solving, no ranges + static void emitGlobal(ObjectFile& obj, const Global* g, U32 ptrBytes); void defineFunctions(); @@ -104,6 +112,18 @@ namespace rat { void markLine(U32 loc); void markScope(U32 loc, U32 start, U32 end); void recordVars(const Function& f); + static VarLoc toVarLoc(const MachineOperand& o); + static B32 writesSlot(const MachineInstr& in, I64 cfaOff); + static B32 instBefore(const OpenVarRange& open, U32 inst); + void closeRange(const OpenVarRange& open, U32 end); + void setLoc(List& live, U32 inst, VarLoc loc, U32 pc); + void applyRecords(List& live, U32 chain, U32 pc); + void clobber(List& live, const MachineInstr& in, U32 pc); + static void meet(List& into, const List& from); + void blockVars(const MachineBlock& blk, List& live); + void solveVars(const MachineFunc& f); + void beginVars(U32 bi); + void endVars(const MachineBlock& blk); private: std::ostream* os; UniquePtr obj; @@ -113,6 +133,8 @@ namespace rat { List encoded; // elf only, one per placed function EncodedFunc* encodedFunc = nullptr; // the function being encoded, null on coff U32 scope = 0; // of the last located instr + List> varsIn; // block index -> var locations at its start, empty untracked + List liveVars; // in the block being encoded const X86CallConv* conv = &abi::kSysV; const MachineFunc* fn = nullptr; const X86FrameLayout* fl = nullptr; diff --git a/src/backend/pass/emit/x86/x86_encode_vars.cpp b/src/backend/pass/emit/x86/x86_encode_vars.cpp new file mode 100644 index 00000000..f9c2ca43 --- /dev/null +++ b/src/backend/pass/emit/x86/x86_encode_vars.cpp @@ -0,0 +1,170 @@ +#include "pass/emit/x86/x86_encode.h" + +#include "codegen/machine_function.h" + +// debug var locations in the style of LLVM's LiveDebugValues, a block starts with the locations all +// its predecessors agree on, a record moves its var, a write to a location ends the vars living there +namespace rat { + VarLoc X86EncodePass::toVarLoc(const MachineOperand& o) { + switch(o.kind) { + case MachineOperand::Kind::Phys: + return {VarLoc::Kind::Reg, o.phys, 0}; + case MachineOperand::Kind::FrameSlot: + return {VarLoc::Kind::Frame, 0, (I64)o.slot - detail::kCfaFromRbp}; + case MachineOperand::Kind::Imm: + return {VarLoc::Kind::Const, 0, o.imm}; + default: + return {}; + } + } + + // loads and call stack arguments only read their slot, anything else may write it + B32 X86EncodePass::writesSlot(const MachineInstr& in, I64 cfaOff) { + I32 slot = (I32)(cfaOff + detail::kCfaFromRbp); + for(const MachineOperand& d : in.defs) + if(d.kind == MachineOperand::Kind::FrameSlot && d.slot == slot) + return true; + X86Op op = (X86Op)in.op; + if(in.isCall || op == X86Op::Load || op == X86Op::FLoad || op == X86Op::Copy) + return false; + for(const MachineOperand& u : in.uses) + if(u.kind == MachineOperand::Kind::FrameSlot && u.slot == slot) + return true; + return false; + } + + B32 X86EncodePass::instBefore(const OpenVarRange& open, U32 inst) { return open.inst < inst; } + + void X86EncodePass::closeRange(const OpenVarRange& open, U32 end) { + if(end == kNoPc || end <= open.start) + return; + VarLoc loc = open.loc; + if(loc.kind == VarLoc::Kind::Reg) + loc.reg = detail::kDwarfRegs[loc.reg]; + List& r = encodedFunc->varRanges[open.inst]; + if(!r.empty() && r.back().end == open.start && r.back().loc == loc) + r.back().end = end; + else + r.push_back({open.start, end, loc}); + } + + void X86EncodePass::setLoc(List& live, U32 inst, VarLoc loc, U32 pc) { + auto it = std::lower_bound(live.begin(), live.end(), inst, instBefore); + B32 found = it != live.end() && it->inst == inst; + if(found && it->loc == loc) + return; + if(found) + closeRange(*it, pc); + if(found && loc.kind == VarLoc::Kind::None) + live.erase(it); + else if(found) + *it = {inst, loc, pc}; + else if(loc.kind != VarLoc::Kind::None) + live.insert(it, {inst, loc, pc}); + } + + void X86EncodePass::applyRecords(List& live, U32 chain, U32 pc) { + for(U32 r = chain; r; r = fn->dbg[r].next) + setLoc(live, fn->dbg[r].inst, toVarLoc(fn->dbg[r].loc), pc); + } + + // the epilogue ends all + void X86EncodePass::clobber(List& live, const MachineInstr& in, U32 pc) { + if(live.empty()) + return; + B32 ret = (X86Op)in.op == X86Op::Ret; + U64 regs = in.clobbers; + for(const MachineOperand& d : in.defs) + if(d.isPhys()) + regs |= detail::physBit(d.phys); + U32 kept = 0; + for(const OpenVarRange& open : live) { + B32 reg = open.loc.kind == VarLoc::Kind::Reg; + B32 frame = open.loc.kind == VarLoc::Kind::Frame; + B32 written = (reg && (regs >> open.loc.reg) & 1) || (frame && writesSlot(in, open.loc.value)); + if(ret || written) + closeRange(open, pc); + else + live[kept++] = open; + } + live.resize(kept); + } + + // what both sides agree on + void X86EncodePass::meet(List& into, const List& from) { + U32 kept = 0; + U32 j = 0; + for(const OpenVarRange& open : into) { + while(j < from.size() && from[j].inst < open.inst) + ++j; + if(j < from.size() && from[j].inst == open.inst && from[j].loc == open.loc) + into[kept++] = open; + } + into.resize(kept); + } + + void X86EncodePass::blockVars(const MachineBlock& blk, List& live) { + for(const MachineInstr& in : blk.insts) { + applyRecords(live, in.dbg, kNoPc); + clobber(live, in, kNoPc); + } + applyRecords(live, blk.dbgEnd, kNoPc); + } + + // in-states only shrink, a block is redone when its in-state changed. the first predecessor to + // reach a block seeds it. the successor lists still name the blocks forwarded jumps skip, so their + // records count + void X86EncodePass::solveVars(const MachineFunc& f) { + U32 nb = (U32)f.blocks.size(); + varsIn.assign(nb, {}); + List at(blockIdBound(f), -1); + for(U32 bi = 0; bi < nb; ++bi) + if(f.blocks[bi].id >= 0) + at[(U32)f.blocks[bi].id] = (I32)bi; + U32 entry = 0; + while(entry < nb && f.blocks[entry].id < 0) + ++entry; + if(entry == nb) + return; + List reached(nb, 0); + List dirty(nb, 0); + reached[entry] = dirty[entry] = 1; + List out; + for(B32 again = true; again;) { + again = false; + for(U32 bi = 0; bi < nb; ++bi) { + if(!dirty[bi]) + continue; + dirty[bi] = 0; + out = varsIn[bi]; + blockVars(f.blocks[bi], out); + for(I32 sid : f.blocks[bi].succs) { + I32 s = at[(U32)sid]; + if(s < 0) + continue; + U32 before = (U32)varsIn[(U32)s].size(); + if(!reached[(U32)s]) + varsIn[(U32)s] = out; + else + meet(varsIn[(U32)s], out); + B32 changed = !reached[(U32)s] || varsIn[(U32)s].size() != before; + reached[(U32)s] = 1; + dirty[(U32)s] |= changed; + again |= changed; + } + } + } + } + + void X86EncodePass::beginVars(U32 bi) { + liveVars = varsIn[bi]; + for(OpenVarRange& open : liveVars) + open.start = a->here(); + } + + void X86EncodePass::endVars(const MachineBlock& blk) { + applyRecords(liveVars, blk.dbgEnd, a->here()); + for(const OpenVarRange& open : liveVars) + closeRange(open, a->here()); + } +} // namespace rat diff --git a/src/backend/target/dwarf.cpp b/src/backend/target/dwarf.cpp index 85a8d631..20320fc3 100644 --- a/src/backend/target/dwarf.cpp +++ b/src/backend/target/dwarf.cpp @@ -62,8 +62,11 @@ namespace rat { constexpr U32 DW_FORM_flag_present = 0x19; constexpr U8 DW_OP_addr = 0x03; + constexpr U8 DW_OP_consts = 0x11; + constexpr U8 DW_OP_regx = 0x90; constexpr U8 DW_OP_fbreg = 0x91; constexpr U8 DW_OP_call_frame_cfa = 0x9c; + constexpr U8 DW_OP_stack_value = 0x9f; constexpr U8 DW_LE_end_of_list = 0; constexpr U8 DW_LE_offset_pair = 4; // same in rnglists and loclists @@ -136,7 +139,13 @@ namespace rat { constexpr U8 kPtrBytes = 8; constexpr U32 kDwarfSp = 7; constexpr U32 kDwarfRa = 16; + } // namespace detail + + B32 VarLoc::operator==(const VarLoc& o) const { + return kind == o.kind && reg == o.reg && value == o.value; + } + namespace detail { void Die::attr(U32 name, U32 form) { le::uleb(decl, name); le::uleb(decl, form); @@ -202,6 +211,19 @@ namespace rat { d.attr(DW_AT_external, DW_FORM_flag_present); } + void locExpr(List& out, const VarLoc& loc) { + if(loc.kind == VarLoc::Kind::Const) { + out.push_back(DW_OP_consts); + le::sleb(out, loc.value); + out.push_back(DW_OP_stack_value); + } else if(loc.kind == VarLoc::Kind::Frame) { + out.push_back(DW_OP_fbreg); + le::sleb(out, loc.value); + } else { + out.insert(out.end(), {DW_OP_regx, (U8)loc.reg}); + } + } + // appends d to .debug_info under its abbreviation code, declaring it once. a die that may have // children always declares them, its list may end right away U32 DieWriter::emit(U32 tag, B32 children, const Die& d) { @@ -317,6 +339,22 @@ namespace rat { info.push_back(0); } + // returns the list's offset in .debug_loclists, list offsets are from the unit base, .text + U32 DieWriter::locList(const List& r, U32 textOff) { + U32 at = (U32)loclists.size(); + for(const VarRange& v : r) { + loclists.push_back(DW_LE_offset_pair); + le::uleb(loclists, textOff + v.start); + le::uleb(loclists, textOff + v.end); + List expr; + locExpr(expr, v.loc); + le::uleb(loclists, expr.size()); + loclists.insert(loclists.end(), expr.begin(), expr.end()); + } + loclists.push_back(DW_LE_end_of_list); + return at; + } + // a static whose storage was never emitted gets no die B32 DieWriter::hasStorage(const DebugVar& v) { return v.symbol.empty() || mod.getGlobal(v.symbol); @@ -347,12 +385,17 @@ namespace rat { else describeVar(d, v); I32 frameOff = tree.func.varFrameOff[inst]; + const List& r = tree.func.varRanges[inst]; if(!v.symbol.empty()) { d.reloc(DW_AT_location, DW_FORM_exprloc, v.symbol, 0); } else if(frameOff != kNoFrameSlot) { - List expr = {DW_OP_fbreg}; - le::sleb(expr, frameOff); + List expr; + locExpr(expr, {VarLoc::Kind::Frame, 0, frameOff}); d.exprloc(DW_AT_location, expr); + } else if(!r.empty()) { + U32 at = locList(r, tree.func.textOff); + d.reloc( + DW_AT_location, DW_FORM_sec_offset, obj.sectionSymbol(ObjectFile::DebugLoclists), at); } emit(v.isParam ? DW_TAG_formal_parameter : DW_TAG_variable, false, d); } @@ -463,6 +506,7 @@ namespace rat { emit(DW_TAG_compile_unit, true, cu); unitHeader(rnglists); + unitHeader(loclists); for(const DebugVar& g : di.globals) globalDie(g); for(const EncodedFunc& func : funcs) @@ -480,6 +524,7 @@ namespace rat { le::patch32(info, at, dieOff[target]); le::patch32(info, 0, (U32)info.size() - 4); le::patch32(rnglists, 0, (U32)rnglists.size() - 4); + le::patch32(loclists, 0, (U32)loclists.size() - 4); } // unit_length, version 5, address_size, segment_selector_size, then the line table's @@ -603,6 +648,7 @@ namespace rat { obj.append(ObjectFile::DebugInfo, w.info.data(), (U32)w.info.size()); obj.append(ObjectFile::DebugAbbrev, w.abbrev.data(), (U32)w.abbrev.size()); obj.append(ObjectFile::DebugRnglists, w.rnglists.data(), (U32)w.rnglists.size()); + obj.append(ObjectFile::DebugLoclists, w.loclists.data(), (U32)w.loclists.size()); detail::writeLineTable(*di, funcs, obj); } } // namespace rat diff --git a/src/backend/target/dwarf.h b/src/backend/target/dwarf.h index 9def5485..dc5d6362 100644 --- a/src/backend/target/dwarf.h +++ b/src/backend/target/dwarf.h @@ -33,6 +33,27 @@ namespace rat { U32 end; }; + // where a variable lives over a pc range + struct VarLoc { + enum class Kind : U8 { + None, + Reg, // in register reg + Frame, // at the cfa + value + Const // is value + }; + Kind kind = Kind::None; + U32 reg = 0; + I64 value = 0; + + B32 operator==(const VarLoc& o) const; + }; + + struct VarRange { + U32 start; + U32 end; + VarLoc loc; // reg is a dwarf number + }; + constexpr I32 kNoFrameSlot = INT32_MIN; struct EncodedFunc { @@ -43,6 +64,7 @@ namespace rat { List lines; List> scopes; // per DebugBody scope, 0 unused List varFrameOff; // per DebugBody var, its frame slot from the cfa or kNoFrameSlot + List> varRanges; // per DebugBody var without a frame slot }; // .eh_frame always, the .debug_* sections when the module carries debug info @@ -69,6 +91,7 @@ namespace rat { void describeVar(Die& d, const DebugVar& v); void describeFunc(Die& d, const String& name, const DebugFunc& f, B32 external); + void locExpr(List& out, const VarLoc& loc); struct ScopeTree { const EncodedFunc& func; @@ -93,12 +116,14 @@ namespace rat { List pendingTypes; List pendingFuncs; // abstract instances of inlined funcs List rnglists; + List loclists; U32 emit(U32 tag, B32 children, const Die& d); void pcRanges(Die& d, const List& r, U32 textOff); void typeDie(const DebugType* t); void typeChildren(const DebugType* t); void endChildren(B32 variadic, B32 children); + U32 locList(const List& r, U32 textOff); B32 hasStorage(const DebugVar& v); void globalDie(const DebugVar& v); void instDie(const ScopeTree& tree, U32 inst); diff --git a/src/compiler/test/debug/debug.O1.expect b/src/compiler/test/debug/debug.O1.expect index 3ec9d728..8e9d2d75 100644 --- a/src/compiler/test/debug/debug.O1.expect +++ b/src/compiler/test/debug/debug.O1.expect @@ -1,4 +1,4 @@ -Breakpoint 1, leaf (a= +Breakpoint 1, leaf (a=53) at #1 0x in deep ( debug.c:16 @@ -7,14 +7,25 @@ debug.c:16 #5 0x in main () at debug.c:38 +$1 = 53 Value returned is $2 = 160 -Breakpoint 2, scale (p= +Breakpoint 2, scale (p=0x +k=5) at debug.c:24 +$3 = {x = 1, y = 2.5, flag = 5, name = "hi\000\000\000\000\000"} +$4 = 5 +$5 = "hi\000\000\000\000\000" +$6 = 5 +$7 = 7 in main () at debug.c:39 -= {4, 5, 6} -Breakpoint 3, span (q=..., bias= -Value returned is $10 = 8 +$8 = {4, 5, 6} +$9 = 97 'a' +Line 2 of +debug.h" starts at address +Breakpoint 3, span (q=..., bias=2) at +$12 = 2 +Value returned is $13 = 8 type = Tagged type = struct { Color c; diff --git a/src/compiler/test/debug/scopes.O1.expect b/src/compiler/test/debug/scopes.O1.expect index 12feb90c..dd71b05f 100644 --- a/src/compiler/test/debug/scopes.O1.expect +++ b/src/compiler/test/debug/scopes.O1.expect @@ -1,5 +1,5 @@ -Breakpoint 1, square (x= -#1 work (n= +Breakpoint 1, square (x=3) at +#1 work (n=3) at scopes.c:17 #2 0x in main () at diff --git a/src/compiler/test/debug/text_check.sh b/src/compiler/test/debug/text_check.sh new file mode 100755 index 00000000..c53e33ea --- /dev/null +++ b/src/compiler/test/debug/text_check.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# -g must not change -O1 code. used as RATCC it compiles as asked, then again with -g, and logs every +# object whose .text bytes differ +# RATCC=src/compiler/test/debug/text_check.sh ./src/compiler/test/run.py -j3 +# then build/test/text_check.log lists the differing objects, one per line +root="$(cd "$(dirname "$0")/../../../.." && pwd)" +cc="$root/bin/cc" +log="$root/build/test/text_check.log" +"$cc" "$@" || exit +obj="" +opt="" +args=() +prev="" +for arg in "$@"; do + [ "$arg" = "-g" ] && exit 0 + [ "$arg" = "-O1" ] && opt=1 + if [ "$prev" = "-o" ]; then + obj="$arg" + arg="$arg.g" + fi + args+=("$arg") + prev="$arg" +done +[ -n "$opt" ] && [ -n "$obj" ] || exit 0 +"$cc" -g "${args[@]}" || { echo "$obj: -g build failed" >>"$log"; exit 0; } +objcopy -O binary --only-section=.text "$obj" "$obj.text" +objcopy -O binary --only-section=.text "$obj.g" "$obj.g.text" +cmp -s "$obj.text" "$obj.g.text" || echo "$obj" >>"$log" +rm -f "$obj.g" "$obj.text" "$obj.g.text" From a9efdb55e94fcac93c76f5e8ee8ece3b699c1804 Mon Sep 17 00:00:00 2001 From: nella Date: Tue, 10 Nov 2026 11:12:00 +0100 Subject: [PATCH 5/6] Locate struct params and VLAs. --- src/backend/ir/debug_info.h | 1 + src/backend/target/dwarf.cpp | 21 ++++++++++++++------- src/backend/target/dwarf.h | 4 ++-- src/compiler/emit/emit_debug.cpp | 7 ++++++- src/compiler/test/debug/debug.O0.expect | 2 ++ src/compiler/test/debug/debug.O1.expect | 2 ++ 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/backend/ir/debug_info.h b/src/backend/ir/debug_info.h index b1146071..d0b32212 100644 --- a/src/backend/ir/debug_info.h +++ b/src/backend/ir/debug_info.h @@ -90,6 +90,7 @@ namespace rat { const DebugVar* var; U32 scope = 0; AllocNode* slot = nullptr; // frame storage + B32 indirect = false; // bound values are its address }; // var inst holds value from where control reaches anchor on, past the store or call after, a null diff --git a/src/backend/target/dwarf.cpp b/src/backend/target/dwarf.cpp index 20320fc3..b8bc2e73 100644 --- a/src/backend/target/dwarf.cpp +++ b/src/backend/target/dwarf.cpp @@ -62,9 +62,11 @@ namespace rat { constexpr U32 DW_FORM_flag_present = 0x19; constexpr U8 DW_OP_addr = 0x03; + constexpr U8 DW_OP_deref = 0x06; constexpr U8 DW_OP_consts = 0x11; constexpr U8 DW_OP_regx = 0x90; constexpr U8 DW_OP_fbreg = 0x91; + constexpr U8 DW_OP_bregx = 0x92; constexpr U8 DW_OP_call_frame_cfa = 0x9c; constexpr U8 DW_OP_stack_value = 0x9f; @@ -211,14 +213,19 @@ namespace rat { d.attr(DW_AT_external, DW_FORM_flag_present); } - void locExpr(List& out, const VarLoc& loc) { - if(loc.kind == VarLoc::Kind::Const) { + void locExpr(List& out, const VarLoc& loc, B32 indirect) { + if(loc.kind == VarLoc::Kind::Const) { // a constant address names memory out.push_back(DW_OP_consts); le::sleb(out, loc.value); - out.push_back(DW_OP_stack_value); + if(!indirect) + out.push_back(DW_OP_stack_value); } else if(loc.kind == VarLoc::Kind::Frame) { out.push_back(DW_OP_fbreg); le::sleb(out, loc.value); + if(indirect) + out.push_back(DW_OP_deref); + } else if(indirect) { // a register holding the address names the memory it points at + out.insert(out.end(), {DW_OP_bregx, (U8)loc.reg, 0}); } else { out.insert(out.end(), {DW_OP_regx, (U8)loc.reg}); } @@ -340,14 +347,14 @@ namespace rat { } // returns the list's offset in .debug_loclists, list offsets are from the unit base, .text - U32 DieWriter::locList(const List& r, U32 textOff) { + U32 DieWriter::locList(const List& r, B32 indirect, U32 textOff) { U32 at = (U32)loclists.size(); for(const VarRange& v : r) { loclists.push_back(DW_LE_offset_pair); le::uleb(loclists, textOff + v.start); le::uleb(loclists, textOff + v.end); List expr; - locExpr(expr, v.loc); + locExpr(expr, v.loc, indirect); le::uleb(loclists, expr.size()); loclists.insert(loclists.end(), expr.begin(), expr.end()); } @@ -390,10 +397,10 @@ namespace rat { d.reloc(DW_AT_location, DW_FORM_exprloc, v.symbol, 0); } else if(frameOff != kNoFrameSlot) { List expr; - locExpr(expr, {VarLoc::Kind::Frame, 0, frameOff}); + locExpr(expr, {VarLoc::Kind::Frame, 0, frameOff}, false); d.exprloc(DW_AT_location, expr); } else if(!r.empty()) { - U32 at = locList(r, tree.func.textOff); + U32 at = locList(r, vi.indirect, tree.func.textOff); d.reloc( DW_AT_location, DW_FORM_sec_offset, obj.sectionSymbol(ObjectFile::DebugLoclists), at); } diff --git a/src/backend/target/dwarf.h b/src/backend/target/dwarf.h index dc5d6362..2c250adb 100644 --- a/src/backend/target/dwarf.h +++ b/src/backend/target/dwarf.h @@ -91,7 +91,7 @@ namespace rat { void describeVar(Die& d, const DebugVar& v); void describeFunc(Die& d, const String& name, const DebugFunc& f, B32 external); - void locExpr(List& out, const VarLoc& loc); + void locExpr(List& out, const VarLoc& loc, B32 indirect); struct ScopeTree { const EncodedFunc& func; @@ -123,7 +123,7 @@ namespace rat { void typeDie(const DebugType* t); void typeChildren(const DebugType* t); void endChildren(B32 variadic, B32 children); - U32 locList(const List& r, U32 textOff); + U32 locList(const List& r, B32 indirect, U32 textOff); B32 hasStorage(const DebugVar& v); void globalDie(const DebugVar& v); void instDie(const ScopeTree& tree, U32 inst); diff --git a/src/compiler/emit/emit_debug.cpp b/src/compiler/emit/emit_debug.cpp index bd6788d8..5875bd47 100644 --- a/src/compiler/emit/emit_debug.cpp +++ b/src/compiler/emit/emit_debug.cpp @@ -193,8 +193,13 @@ namespace rat::cc { AllocNode* slot = !l.staticSymbol && l.inMem() ? dyn_cast(l.addr) : nullptr; body.vars.push_back({body.func->vars.back().get(), dbg.scope, slot}); U32 inst = (U32)body.vars.size() - 1; - if(l.staticSymbol || l.inMem()) + if(l.staticSymbol || slot) return; + if(l.inMem()) { // a vla, over-aligned local or by-value aggregate, only its address is known + body.vars[inst].indirect = true; + fn.bindDebug(inst, l.addr); + return; + } fn.bindVar(l.var, inst); if(hasValue) fn.bindDebug(inst, fn.get(l.var)); diff --git a/src/compiler/test/debug/debug.O0.expect b/src/compiler/test/debug/debug.O0.expect index 969b9e4b..a095734b 100644 --- a/src/compiler/test/debug/debug.O0.expect +++ b/src/compiler/test/debug/debug.O0.expect @@ -24,6 +24,8 @@ $9 = 97 'a' Line 2 of debug.h" starts at address Breakpoint 3, span (q=..., bias=2) at +$10 = {x = 1, y = 2.5, flag = 5, name = "hi\000\000\000\000\000"} +$11 = "hi\000\000\000\000\000" $12 = 2 Value returned is $13 = 8 type = Tagged diff --git a/src/compiler/test/debug/debug.O1.expect b/src/compiler/test/debug/debug.O1.expect index 8e9d2d75..38e30986 100644 --- a/src/compiler/test/debug/debug.O1.expect +++ b/src/compiler/test/debug/debug.O1.expect @@ -24,6 +24,8 @@ $9 = 97 'a' Line 2 of debug.h" starts at address Breakpoint 3, span (q=..., bias=2) at +$10 = {x = 1, y = 2.5, flag = 5, name = "hi\000\000\000\000\000"} +$11 = "hi\000\000\000\000\000" $12 = 2 Value returned is $13 = 8 type = Tagged From b9cc6739bd1a2fc2fc617a2ff63fd1e15c15803f Mon Sep 17 00:00:00 2001 From: nella Date: Tue, 10 Nov 2026 13:37:00 +0100 Subject: [PATCH 6/6] Cap var tracking. --- src/backend/pass/emit/x86/x86_encode.cpp | 4 +++- src/backend/pass/emit/x86/x86_encode.h | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/pass/emit/x86/x86_encode.cpp b/src/backend/pass/emit/x86/x86_encode.cpp index fd550907..314de4ed 100644 --- a/src/backend/pass/emit/x86/x86_encode.cpp +++ b/src/backend/pass/emit/x86/x86_encode.cpp @@ -649,8 +649,10 @@ namespace rat { blockOffset.assign(blockIdBound(f), 0); varsIn.clear(); + // like gcc's max-vartrack-size, huge functions keep their vars without locations const DebugBody* body = encodedFunc ? encodedFunc->fn->getDebug() : nullptr; - if(body && f.dbg.size() > 1) + U64 trackCost = body ? (U64)instCount * body->vars.size() : 0; + if(body && f.dbg.size() > 1 && trackCost <= kMaxVarTrackCost) solveVars(f); B32 trackVars = !varsIn.empty(); prologue(); diff --git a/src/backend/pass/emit/x86/x86_encode.h b/src/backend/pass/emit/x86/x86_encode.h index b9bc7abf..3a412238 100644 --- a/src/backend/pass/emit/x86/x86_encode.h +++ b/src/backend/pass/emit/x86/x86_encode.h @@ -44,7 +44,8 @@ namespace rat { VarLoc loc; // reg is a PhysReg U32 start; }; - static constexpr U32 kNoPc = ~0u; // solving, no ranges + static constexpr U32 kNoPc = ~0u; // solving, no ranges + static constexpr U64 kMaxVarTrackCost = 20000000; // instrs times tracked vars static void emitGlobal(ObjectFile& obj, const Global* g, U32 ptrBytes); void defineFunctions();