sovereign-trinity-kernel / lua /assembler.lua
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/sovereign-trinity-kernel
50dd446 verified
Raw
History Blame Contribute Delete
7.08 kB
-- assembler.lua
-- Mini 6502 assembler in Lua — compiles mnemonic source to hex strings
-- at LuaLaTeX load time. Two-pass: labels resolved on second pass.
--
-- Authors: Ahmad Ali Parr, Jessica L. Williams (SNAPKITTYWEST)
local asm = {}
asm.opcodes = {
-- LDA
LDA_IMM=0xA9, LDA_ZP=0xA5, LDA_ZPX=0xB5, LDA_ABS=0xAD,
LDA_ABSX=0xBD, LDA_ABSY=0xB9, LDA_INDX=0xA1, LDA_INDY=0xB1,
-- STA
STA_ZP=0x85, STA_ZPX=0x95, STA_ABS=0x8D,
STA_ABSX=0x9D, STA_ABSY=0x99, STA_INDX=0x81, STA_INDY=0x91,
-- LDX / LDY
LDX_IMM=0xA2, LDX_ZP=0xA6, LDX_ABS=0xAE,
LDY_IMM=0xA0, LDY_ZP=0xA4, LDY_ABS=0xAC,
STX_ZP=0x86, STX_ABS=0x8E,
STY_ZP=0x84, STY_ABS=0x8C,
-- ALU
ADC_IMM=0x69, ADC_ZP=0x65, ADC_ABS=0x6D,
SBC_IMM=0xE9, SBC_ZP=0xE5, SBC_ABS=0xED,
AND_IMM=0x29, AND_ZP=0x25, AND_ABS=0x2D,
ORA_IMM=0x09, ORA_ZP=0x05, ORA_ABS=0x0D,
EOR_IMM=0x49, EOR_ZP=0x45, EOR_ABS=0x4D,
CMP_IMM=0xC9, CMP_ZP=0xC5, CMP_ABS=0xCD,
BIT_ZP=0x24, BIT_ABS=0x2C,
-- Shifts
ASL_A=0x0A, ASL_ZP=0x06, ASL_ABS=0x0E,
LSR_A=0x4A, LSR_ZP=0x46, LSR_ABS=0x4E,
ROL_A=0x2A, ROL_ZP=0x26, ROL_ABS=0x2E,
ROR_A=0x6A, ROR_ZP=0x66, ROR_ABS=0x6E,
-- Inc/Dec
INC_ZP=0xE6, INC_ABS=0xEE, INC_ZPX=0xF6, INC_ABSX=0xFE,
DEC_ZP=0xC6, DEC_ABS=0xCE, DEC_ZPX=0xD6, DEC_ABSX=0xDE,
INX=0xE8, DEX=0xCA, INY=0xC8, DEY=0x88,
-- Transfer
TAX=0xAA, TXA=0x8A, TAY=0xA8, TYA=0x98, TXS=0x9A, TSX=0xBA,
-- Stack
PHA=0x48, PLA=0x68, PHP=0x08, PLP=0x28,
-- Flags
CLC=0x18, SEC=0x38, CLI=0x58, SEI=0x78,
CLV=0xB8, CLD=0xD8, SED=0xF8,
-- Branch
BCC=0x90, BCS=0xB0, BEQ=0xF0, BNE=0xD0,
BMI=0x30, BPL=0x10, BVC=0x50, BVS=0x70,
-- Jump/Call
JMP_ABS=0x4C, JMP_IND=0x6C, JSR_ABS=0x20, RTS=0x60,
-- Misc
BRK=0x00, NOP=0xEA,
}
function asm:new()
local o = {
labels = {},
output = {},
addr = 0x0600,
patches = {}, -- {idx, label, is_branch} for second pass
}
setmetatable(o, { __index = self })
return o
end
function asm:emit(byte)
table.insert(self.output, byte & 0xFF)
self.addr = self.addr + 1
end
function asm:emit16(val)
self:emit(val & 0xFF)
self:emit((val >> 8) & 0xFF)
end
function asm:parse_val(s)
s = s:gsub("%s", "")
if s:match("^%$%x+$") then return tonumber(s:sub(2), 16) end
if s:match("^%d+$") then return tonumber(s) end
if s:match("^#") then return self:parse_val(s:sub(2)) end
return nil, s -- might be a label
end
function asm:parse_line(line)
-- Strip comment
line = line:gsub(";.*$", ""):match("^%s*(.-)%s*$")
if line == "" then return end
-- Label definition
local lbl, rest = line:match("^(%w+):%s*(.*)")
if lbl then
self.labels[lbl] = self.addr
line = rest
if line == "" then return end
end
-- Mnemonic and operand
local mn, op = line:match("^(%u+)%s*(.*)")
if not mn then return end
op = op:match("^%s*(.-)%s*$")
-- Implied / Accumulator
if op == "" or op == "A" then
local code = self.opcodes[mn] or self.opcodes[mn.."_A"]
if code then self:emit(code) end
return
end
-- Immediate: #$xx or #nn
if op:match("^#") then
local val = self:parse_val(op:sub(2))
local code = self.opcodes[mn.."_IMM"]
if code and val then self:emit(code); self:emit(val) end
return
end
-- Indirect X: ($xx,X)
if op:match("^%(.*,X%)$") then
local inner = op:match("^%((.+),X%)$")
local val = self:parse_val(inner)
if val and val < 256 then
self:emit(self.opcodes[mn.."_INDX"]); self:emit(val)
end
return
end
-- Indirect Y: ($xx),Y
if op:match("^%(.*%),Y$") then
local inner = op:match("^%((.+)%),Y$")
local val = self:parse_val(inner)
if val and val < 256 then
self:emit(self.opcodes[mn.."_INDY"]); self:emit(val)
end
return
end
-- Absolute,X / ZP,X
if op:match(",X$") then
local base = op:gsub(",X$","")
local val = self:parse_val(base)
if val then
if val < 256 then
self:emit(self.opcodes[mn.."_ZPX"]); self:emit(val)
else
self:emit(self.opcodes[mn.."_ABSX"]); self:emit16(val)
end
end
return
end
-- Absolute,Y
if op:match(",Y$") then
local base = op:gsub(",Y$","")
local val = self:parse_val(base)
if val then
self:emit(self.opcodes[mn.."_ABSY"]); self:emit16(val)
end
return
end
-- Branch (relative label or offset)
local branches = {BCC=true,BCS=true,BEQ=true,BNE=true,BMI=true,BPL=true,BVC=true,BVS=true}
if branches[mn] then
local code = self.opcodes[mn]
self:emit(code)
local val = self:parse_val(op)
if val then
self:emit(val & 0xFF)
else
-- label reference — patch in second pass
table.insert(self.patches, {idx=#self.output+1, label=op, branch_from=self.addr+1})
self:emit(0x00) -- placeholder
end
return
end
-- ZP or ABS
local val = self:parse_val(op)
if val then
if val < 256 then
local code = self.opcodes[mn.."_ZP"]
if code then self:emit(code); self:emit(val) return end
end
local code = self.opcodes[mn.."_ABS"] or self.opcodes[mn.."_ABS"]
if not code then code = self.opcodes[mn] end
if code then self:emit(code); self:emit16(val) end
return
end
-- JSR / JMP with label
if mn == "JSR" or mn == "JMP" then
local code = (mn == "JSR") and 0x20 or 0x4C
self:emit(code)
local target = self.labels[op]
if target then
self:emit16(target)
else
table.insert(self.patches, {idx=#self.output, label=op, is16=true})
self:emit(0x00); self:emit(0x00)
end
end
end
function asm:second_pass()
for _, p in ipairs(self.patches) do
local target = self.labels[p.label]
if target then
if p.is16 then
self.output[p.idx] = target & 0xFF
self.output[p.idx+1] = (target >> 8) & 0xFF
else
-- branch: signed 8-bit relative offset
local offset = target - p.branch_from
if offset < -128 or offset > 127 then
if tex then tex.print("\\PackageWarning{assembler}{Branch out of range: "..p.label.."}") end
offset = 0
end
self.output[p.idx] = offset & 0xFF
end
end
end
end
function asm:assemble(source)
local inst = self:new()
for line in source:gmatch("[^\r\n]+") do
inst:parse_line(line)
end
inst:second_pass()
local parts = {}
for _, b in ipairs(inst.output) do
table.insert(parts, string.format("%02X", b))
end
return table.concat(parts, " ")
end
return asm