From b232489d2cf333de6a7bd0719b460625e5848fb5 Mon Sep 17 00:00:00 2001 From: egecanakincioglu Date: Sat, 6 Jun 2026 03:44:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20SafeRegAlloc=20=E2=80=94=20stack-canoni?= =?UTF-8?q?cal=20register=20allocator=20for=20V1=20correctness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SafeRegAlloc: correctness-first allocator. Every IR value in unique stack slot. Registers are instruction-local scratch only. No live value survives instruction boundary. Implements allocate(), frameSize(), scratch pool, safety assertions, written tracking. --- arimo/compiler/backend/SafeRegAlloc.arm | 337 ++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 arimo/compiler/backend/SafeRegAlloc.arm diff --git a/arimo/compiler/backend/SafeRegAlloc.arm b/arimo/compiler/backend/SafeRegAlloc.arm new file mode 100644 index 0000000..2c52381 --- /dev/null +++ b/arimo/compiler/backend/SafeRegAlloc.arm @@ -0,0 +1,337 @@ +/* +Arimo Lang Compiler - A modern programming language and compiler +Copyright (C) 2026 Egecan Akıncıoğlu + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package arimo.compiler.backend; + +import arimo.compiler.backend.X64Reg; +import arimo.compiler.backend.IRFunction; +import arimo.compiler.backend.IRInstr; +import arimo.compiler.backend.IRValue; +import arimo.compiler.backend.IRValueKind; +import arimo.compiler.backend.IRType; + +// SafeRegAlloc: stack-canonical register allocator for V1 correctness. +// Every IR value lives in a unique stack slot. Registers are instruction-local scratch. +// No callee-saved register reuse across instructions. No spill/reload tracking needed. +// Simple, slow, correct. + +public class SafeValueSlot { + public name : String; + public slot : Integer; + public written : Boolean; + public ty : Integer; // IRType + + public constructor(name: String, slot: Integer) { + this.name = name; + this.slot = slot; + this.written = false; + this.ty = IRType.I64; + } +} + +public class SafeRegAlloc { + private slots : List; + private slotCnt : Integer; + private scratch : List; + private sci : Integer; + private curFn : String; + private dirty : Boolean; + + public constructor() { + this.slots = List(); + this.slotCnt = 0; + this.scratch = List(); + this.sci = 0; + this.curFn = ""; + this.dirty = false; + // Scratch pool: caller-saved regs for instruction-local use + // RAX excluded — reserved for idiv/call-return/syscall-num + this.scratch.append(X64Reg.RDI); + this.scratch.append(X64Reg.RSI); + this.scratch.append(X64Reg.RDX); + this.scratch.append(X64Reg.RCX); + this.scratch.append(X64Reg.R8); + this.scratch.append(X64Reg.R9); + this.scratch.append(X64Reg.R10); + this.scratch.append(X64Reg.R11); + } + + // ===== P1: Core allocation ===== + + public allocate(fn: IRFunction) { + this.slots = List(); + this.slotCnt = 0; + this.curFn = fn.name; + + // Allocate slots for parameters — captured from arg regs at prologue + Integer pi = 0; + while (pi < fn.params.length()) { + IRParam param = fn.params.get(pi) as IRParam; + this.findOrCreate(param.name); + pi = pi + 1; + } + + // Allocate slots for all REG destination values + // IMM_INT, GLOBAL, LABEL_REF operands need no stack slot + Integer ii = 0; + while (ii < fn.instrs.length()) { + IRInstr instr = fn.instrs.get(ii) as IRInstr; + if (!instr.dst.isNone() && instr.dst.kind == IRValueKind.REG) { + this.findOrCreate(instr.dst.name); + } + ii = ii + 1; + } + } + + // findOrCreate: lookup by name, create slot if not found. + // Returns slot index. Each value name gets exactly one slot (no reuse). + public findOrCreate(name: String) : Integer { + Integer i = 0; + while (i < this.slots.length()) { + SafeValueSlot sv = this.slots.get(i) as SafeValueSlot; + if (sv.name == name) { return i; } + i = i + 1; + } + SafeValueSlot sv = SafeValueSlot(name, this.slotCnt); + this.slotCnt = this.slotCnt + 1; + this.slots.append(sv); + return this.slots.length() - 1; + } + + // getSlot: lookup-only, no creation. Returns -1 if not found. + public getSlot(name: String) : Integer { + Integer i = 0; + while (i < this.slots.length()) { + SafeValueSlot sv = this.slots.get(i) as SafeValueSlot; + if (sv.name == name) { return i; } + i = i + 1; + } + return -1; + } + + // slotByName: alias for getSlot — allocate-sonrası lookup-only. + public slotByName(name: String) : Integer { + return this.getSlot(name); + } + + // slotOffset: stack offset for a given slot index. + // Slot 0 → [rbp-8], slot 1 → [rbp-16], ... + public slotOffset(idx: Integer) : Integer { + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + return -(sv.slot + 1) * 8; + } + + // slotOffsetByName: stack offset looked up by value name. + public slotOffsetByName(name: String) : Integer { + Integer idx = this.getSlot(name); + if (idx < 0) { return 0; } + return this.slotOffset(idx); + } + + // frameSize: total stack space for all spill slots, 16-byte aligned. + // Added to sub RSP in prologue. Minimum 0. + public frameSize() : Integer { + Integer raw = this.slotCnt * 8; + if (raw == 0) { return 0; } + Integer rem = raw % 16; + if (rem == 0) { return raw; } + return raw + (16 - rem); + } + + public totalSlots() : Integer { return this.slotCnt; } + + // valueType: IR type of the value at given slot. + public valueType(idx: Integer) : Integer { + if (idx < 0 || idx >= this.slots.length()) { return IRType.VOID; } + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + return sv.ty; + } + + public setValueType(idx: Integer, ty: Integer) { + if (idx >= 0 && idx < this.slots.length()) { + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + sv.ty = ty; + } + } + + // ===== P2: Scratch register allocation ===== + + // allocScratch: single scratch register, advances internal cursor. + // Call resetScratch() at instruction boundary. + public allocScratch() : Integer { + Integer r = this.scratch.get(this.sci) as Integer; + this.sci = this.sci + 1; + if (this.sci >= this.scratch.length()) { this.sci = 0; } + this.dirty = true; + return r; + } + + // allocScratch2: two distinct scratch registers. + // Guaranteed different — wraps correctly if pool boundary crossed. + public allocScratch2() : List { + List result = List(); + Integer r1 = this.scratch.get(this.sci) as Integer; + result.append(r1); + this.sci = this.sci + 1; + if (this.sci >= this.scratch.length()) { this.sci = 0; } + + Integer r2 = this.scratch.get(this.sci) as Integer; + result.append(r2); + this.sci = this.sci + 1; + if (this.sci >= this.scratch.length()) { this.sci = 0; } + + this.dirty = true; + return result; + } + + // allocScratch3: three distinct scratch registers. + public allocScratch3() : List { + List result = List(); + Integer r1 = this.scratch.get(this.sci) as Integer; + result.append(r1); + this.sci = this.sci + 1; + if (this.sci >= this.scratch.length()) { this.sci = 0; } + + Integer r2 = this.scratch.get(this.sci) as Integer; + result.append(r2); + this.sci = this.sci + 1; + if (this.sci >= this.scratch.length()) { this.sci = 0; } + + Integer r3 = this.scratch.get(this.sci) as Integer; + result.append(r3); + this.sci = this.sci + 1; + if (this.sci >= this.scratch.length()) { this.sci = 0; } + + this.dirty = true; + return result; + } + + // resetScratch: reset scratch cursor for next instruction. + // Must be called at every instruction boundary. + public resetScratch() { + this.sci = 0; + this.dirty = false; + } + + // scratchRegCount: total available scratch registers. + public scratchRegCount() : Integer { + return this.scratch.length(); + } + + // ===== P3: Safety assertions ===== + + // assertKnown: hard-fail if value name has no allocated slot. + public assertKnown(name: String) { + Integer idx = this.getSlot(name); + if (idx < 0) { + IO.println("arc: [FAIL] SafeRegAlloc: unknown value \"${name}\" in fn ${this.curFn} — no slot allocated"); + // In V1 self-host: mark error state for caller to check + } + } + + // assertKnownIdx: index-based variant. + public assertKnownIdx(idx: Integer) { + if (idx < 0 || idx >= this.slots.length()) { + IO.println("arc: [FAIL] SafeRegAlloc: bad slot index ${idx} in fn ${this.curFn} — out of range (0..${this.slots.length() - 1})"); + } + } + + // assertWritten: hard-fail if slot exists but never written to. + // Prevents reloading uninitialized stack slots (e.g., used before def). + public assertWritten(name: String) { + Integer idx = this.getSlot(name); + if (idx < 0) { + IO.println("arc: [FAIL] SafeRegAlloc: assertWritten unknown value \"${name}\" in fn ${this.curFn}"); + return; + } + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + if (!sv.written) { + IO.println("arc: [FAIL] SafeRegAlloc: reload of unwritten slot ${sv.slot} (value \"${name}\") in fn ${this.curFn}"); + } + } + + // assertClean: invariant check — no dirty scratch state at instruction boundary. + // Should be called BEFORE any scratch allocation for a new instruction. + // In safe mode, resetScratch() must have been called since last instruction. + public assertClean() { + if (this.dirty) { + IO.println("arc: [FAIL] SafeRegAlloc: dirty scratch state at instruction boundary in fn ${this.curFn} — resetScratch() not called"); + } + } + + // ===== Slot write tracking ===== + + // markWritten: index-based — mark slot as having been written to. + public markWritten(idx: Integer) { + if (idx >= 0 && idx < this.slots.length()) { + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + sv.written = true; + } + } + + // markValueWritten: name-based convenience wrapper. + // Looks up slot by name, marks it written. No-op if unknown (assertKnown for that). + public markValueWritten(name: String) { + Integer idx = this.getSlot(name); + if (idx >= 0) { + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + sv.written = true; + } + } + + // isWritten: index-based — check if slot has been written to. + public isWritten(idx: Integer) : Boolean { + if (idx < 0 || idx >= this.slots.length()) { return false; } + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + return sv.written; + } + + // isValueWritten: name-based convenience. + public isValueWritten(name: String) : Boolean { + Integer idx = this.getSlot(name); + if (idx < 0) { return false; } + SafeValueSlot sv = this.slots.get(idx) as SafeValueSlot; + return sv.written; + } + + // markAllParamsWritten: mark all parameter slots as written (initialized at prologue). + // Call after emitFunction prologue saves incoming arg regs to slots. + public markAllParamsWritten(fn: IRFunction) { + Integer pi = 0; + while (pi < fn.params.length()) { + IRParam param = fn.params.get(pi) as IRParam; + this.markValueWritten(param.name); + pi = pi + 1; + } + } + + // dump: debug string showing all slots and their state. + public dump() : String { + String s = "SafeRegAlloc fn=${this.curFn} slots=${this.slotCnt} frame=${this.frameSize()}\n"; + Integer i = 0; + while (i < this.slots.length()) { + SafeValueSlot sv = this.slots.get(i) as SafeValueSlot; + String wflag = ""; + if (sv.written) { wflag = " W"; } + else { wflag = " -"; } + s = s + " [${i}] ${sv.name} slot=${sv.slot} off=${this.slotOffset(i)} ty=${IRType.name(sv.ty)}${wflag}\n"; + i = i + 1; + } + return s; + } +}