local M = {} local symbols,symbolsorg={},{} M.symbols,M.symbolsorg=symbols,symbolsorg local locations={} M.locations=locations local sections={} M.sections=sections local relations={} M.relations=relations local stats={} M.stats=stats setmetatable(stats, stats) local before_link={} M.before_link=before_link M.strip = true -- set to false to disable dead stripping of relocatable sections M.strip_empty = false -- set to true to strip empty sections: their label will then not resolve M.pcall = pcall -- set to empty function returning false to disable eval during compute_size() -- set to pcall directly if you want to keep ldazab/x/y eval during compute_size() even if -- disabled for other parts (required to distinguish automatically between zp/abs addressing) M.pcall_za = function(...) return M.pcall(...) end M.__index = M M.__newindex = function(t,k,v) local kk = k if type(k) == 'string' and k:sub(1,1) == '_' and M.label_current then kk = M.label_current .. k end if symbols[kk] then error("attempt to modify symbol " .. k) end rawset(t,k,v) end symbols.__index = symbols setmetatable(M, symbols) local id_ = 0 local id = function() id_=id_+1 return id_ end M.id=id M.link = function() if stats.unused then return end for _,v in ipairs(before_link) do v() end if M.strip then symbols.__index = function(tab,key) local val = rawget(symbols, key) if type(val) == 'table' and val.type == 'label' then val.section.refcount = val.section.refcount + 1 end return val end end for _,section in ipairs(sections) do section:compute_size() end symbols.__index = symbols local chunk_reserve = function(section, chunk_ix) local chunks = section.location.chunks local chunk = chunks[chunk_ix] local holes = section.holes local new_chunks,ins = {},table.insert local chunk1 = { id=id(), start=chunk.start, size=section.org-chunk.start } local hole_ix = 1 local hole1 = holes[1] if hole1 and hole1.start==0 then chunk1.size = chunk1.size + hole1.size hole_ix = 2 end if chunk1.size > 0 then ins(new_chunks, chunk1) end while hole_ix <= #holes do local hole = holes[hole_ix] local chunki = { id=id(), start=section.org+hole.start, size=hole.size } ins(new_chunks, chunki) hole_ix = hole_ix + 1 end local chunkl = { id=id(), start=section.org+section.size, size=chunk.start+chunk.size-(section.org+section.size) } local chunkn = new_chunks[#new_chunks] if chunkn and chunkn.start+chunkn.size==chunkl.start then chunkn.size = chunkn.size + chunkl.size elseif chunkl.size > 0 then ins(new_chunks, chunkl) end table.remove(chunks, chunk_ix) for i=chunk_ix,chunk_ix+#new_chunks-1 do ins(chunks, i, new_chunks[i-chunk_ix+1]) end end local chunk_from_address = function(section, address) local chunks,rorg = section.location.chunks,section.location.rorg for i,chunk in ipairs(chunks) do if address >= chunk.start and address+section.size <= chunk.start+chunk.size then return chunk,i end end end local check_section_position = function(section, address, chunk) local chunk = chunk_from_address(section, address) if not chunk then return end local rorg = section.location.rorg if section.align then local raddress = rorg(address) if section.offset then raddress = raddress - section.offset end if raddress % section.align ~= 0 then return end end for _,constraint in ipairs(section.constraints) do local cstart, cfinish = address+constraint.start, address+constraint.finish local s, f = rorg(cstart) // 0x100, rorg(cfinish) // 0x100 if s==f or math.ceil(s)==math.floor(s) and s+1==f and cfinish-cstart==0x100 then if constraint.type == 'crosspage' then return end else if constraint.type == 'samepage' then return end end end local address_end = address+section.size local waste = math.min(address - chunk.start, chunk.size - (address_end - chunk.start)) local raddress,raddress_end = rorg(address),rorg(address_end) local align,cross=0x100,0 repeat local cross_count = (raddress_end+align-1)//align - (raddress+align-1)//align if raddress&(align-1) == 0 then cross_count=cross_count+1 end cross = cross + align * align * cross_count align = align>>1 until align==1 local lsb=0 for i=0,15 do if raddress&(1<= section.size then local waste,cross,lsb,position = math.maxinteger,math.maxinteger,math.maxinteger local usage_lowest = function(start, finish) local inc=1 if section.align then local rstart = rorg(start) local arstart = (rstart + section.align - 1) // section.align * section.align if section.offset then arstart = arstart + section.offset end start = start + arstart-rstart inc = section.align end for address=start,finish,inc do local nwaste, ncross, nlsb = check_section_position(section, address, chunk) if nwaste then if constrain then nwaste, ncross, nlsb = constrain(address, nwaste, ncross, nlsb) if not nwaste then goto skip end end if nwaste > waste then goto skip end if nwaste == waste then -- if waste is the same, keep the one that uses the least amount of aligned addresses if ncross > cross then goto skip end if ncross == cross then -- if cross count is same, take the one with the most set LSB count (eg. select 11 over 10) if nlsb > lsb then goto skip end end end position,waste,cross,lsb = address,nwaste,ncross,nlsb ::skip:: end end end local finish = math.min(chunk.start + 0xff, chunk.start + chunk.size - section.size) usage_lowest(chunk.start, finish) if chunk.size ~= math.huge then local start = math.max(chunk.start + chunk.size - section.size - 0xff, chunk.start) usage_lowest(start, chunk.start + chunk.size - section.size) end if position then section.org = position chunk_reserve(section, chunk_ix) --print(section.label, string.format("%04X\t%d", position, section.size)) --for k,v in ipairs(location.chunks) do print(string.format(" %04X %04X %d", v.start, v.size+v.start-1, v.size)) end return position end end end end stats.used = 0 stats.unused = 0 stats.cycles = 0 local related_sections = {} for _,location in ipairs(locations) do local sections,rorg = location.sections,location.rorg -- filter sections list local position_independent_sections = {} local symbols_to_remove = {} local section_count = #sections location.cycles=0 location.used=0 for ix,section in ipairs(sections) do location.cycles = location.cycles + section.cycles location.used = location.used + section.size if section.size == 0 then if M.strip_empty or section.weak then sections[ix]=nil if not section.org then table.insert(symbols_to_remove, section.label) end else section.org = location.start end elseif not section.org then if M.strip and not section.refcount and not section.strong then sections[ix]=nil table.insert(symbols_to_remove, section.label) elseif section.related then table.insert(related_sections, section) else table.insert(position_independent_sections, section) end end end do local j=0 for i=1,section_count do if sections[i] ~= nil then j=j+1 sections[j],sections[i] = sections[i],sections[j] end end end for _,v in ipairs(symbols_to_remove) do symbols[v] = nil end location.position_independent_sections = position_independent_sections stats.cycles = stats.cycles + location.cycles stats.used = stats.used + location.used -- fixed position sections for section_ix,section in ipairs(sections) do if section.org then if section.org < location.start or section.org > (location.finish or math.huge) then error("ORG section " .. section.label .. " starts outside container location") end for chunk_ix,chunk in ipairs(location.chunks) do if chunk.start <= section.org and chunk.size - (section.org - chunk.start) >= section.size then chunk_reserve(section, chunk_ix) goto chunk_located end end error("ORG section " .. section.label .. " overflows its location") ::chunk_located:: end end end table.sort(related_sections, function(a,b) if a.size==b.size then return a.idb.size end) for _,section in ipairs(related_sections) do if not section.org then local related,ins = {},table.insert local function collect(section_parent, offset) local relatives = relations[section_parent] if relatives then for relative,relative_offset in pairs(relatives) do if not related[relative] and relative ~= section then relative_offset = relative_offset + offset related[relative] = relative_offset collect(relative, relative_offset) end end end end collect(section, 0) local location_start = section.location.start local position = position_section(section, function(address, waste, cross, lsb) local waste, cross, lsb = 0, 0, 0 for section,offset in pairs(related) do local section_address = address + (section.location.start - location_start) + offset local nwaste, ncross, nlsb = check_section_position(section, section_address) if not nwaste then return end waste, cross, lsb = waste+nwaste, cross+ncross, lsb+nlsb end return waste, cross, lsb end) if not position then error("unable to find space for related section '" .. section.label .. "' of size " .. section.size) end for section,offset in pairs(related) do section.org = position + (section.location.start - location_start) + offset local chunk,chunk_ix = chunk_from_address(section, section.org) chunk_reserve(section, chunk_ix) end end end for _,location in ipairs(locations) do local position_independent_sections = location.position_independent_sections table.sort(position_independent_sections, function(a,b) if a.size==b.size then return a.label>b.label end return a.size>b.size end) for _,section in ipairs(position_independent_sections) do if not position_section(section) then error("unable to find space for section '" .. section.label .. "' of size " .. section.size) end end -- unused space stats local unused = 0 for _,chunk in ipairs(location.chunks) do if chunk.size ~= math.huge then unused = unused + chunk.size else location.stops_at = chunk.start-1 end end location.unused = unused stats.unused = stats.unused + unused end end M.resolve = function() if stats.resolved_count then return end M.link() stats.resolved_count = 0 repeat local count = 0 for k,v in pairs(symbols) do if k ~= '__index' then local t = type(v) if t == 'function' then v=v() t=type(v) symbols[k]=v count=count+1 end if t == 'table' and type(v.resolve) == 'function' then symbols[k],symbolsorg[k]=v.resolve() count=count+1 end if t == 'string' and symbols[v] then symbols[k]=symbols[v] count=count+1 end stats.resolved_count = stats.resolved_count + count end end until count == 0 -- set local label references resolver local llresolver = { __index = function(tab,key) if type(key) ~= 'string' or key:sub(1,1) ~= '_' or not M.label_current then return nil end return symbols[M.label_current .. key] end } setmetatable(symbols, llresolver) end M.genbin = function(filler) if #locations == 0 then return end if not filler then filler = 0 end -- brk opcode M.resolve() local bin = {} local ins,mov = table.insert,table.move table.sort(locations, function(a,b) return a.start < b.start end) local of0 = locations[1].start local fill for _,location in ipairs(locations) do if location.start < #bin then error(string.format("location [%04x,%04x] overlaps another", location.start, location.finish or location.stops_at)) end if fill then for i=#bin+of0,location.start-1 do ins(bin, filler) end end M.size=0 M.cycles=0 local sections = location.sections table.sort(sections, function(a,b) return a.org < b.org end) for _,section in ipairs(sections) do for i=#bin+of0,section.org-1 do ins(bin, filler) end local bin_offset = math.min(#bin, section.org-of0)+1 for _,instruction in ipairs(section.instructions) do local b,o = instruction.bin if type(b) == 'function' then b,o = b(filler) end if type(b) == 'table' then mov(b,1,#b,bin_offset,bin) bin_offset=bin_offset+#b elseif b then bin[bin_offset]=b bin_offset=bin_offset+1 end if o then bin_offset=bin_offset+o for i=#bin,bin_offset-1 do ins(bin, filler) end end M.size=#bin M.cycles=M.cycles+(instruction.cycles or 0) end end fill = not location.nofill if location.finish and fill then for i=#bin+of0,location.finish do ins(bin, filler) end end end stats.bin_size = #bin return bin end M.writebin = function(filename, bin) if not filename then filename = 'main.bin' end if not bin then bin = M.genbin() end local f = assert(io.open(filename, "wb"), "failed to open " .. filename .. " for writing") f:write(string.char(table.unpack(bin))) f:close() end -- return a table of entry(address, label) M.getsym = function(entry) local ins = table.insert local s,sym_rev = {},{} for k,v in pairs(symbols) do if type(v) == 'number' then ins(sym_rev,k) end end table.sort(sym_rev, function(a,b) local x,y=symbols[a],symbols[b] if x==y then return a 1 then ins(s, string.format(" --- Total --- %5d %5d %5d", stats.unused, stats.used, stats.bin_size)) end return table.concat(s, '\n') end M.location = function(start, finish) local location if type(start) ~= 'table' then location = { start=start, finish=finish } else if start.type == 'location' then for _,v in ipairs(locations) do if v == start then M.location_current = start return start end end error("unable to find reference to location [" .. (start.start or '?') .. ", " .. (start.finish or '?') .. "]") end location = start location.start = start[1] location.finish = start[2] if type(location.rorg) == 'number' then local offset = location.rorg - location.start location.rorg = function(x) return x+offset end end end location.type = 'location' location.sections = {} if not location.rorg then location.rorg = function(x) return x end end local size = (location.finish or math.huge) - location.start + 1 location.chunks={ { id=id(), start=location.start, size=size } } locations[#locations+1] = location M.location_current = location return location end M.section = function(t) local section = {} local name = t or 'S'..id() if type(name) ~= 'string' then assert(type(t) == 'table', "invalid arguments for section") if t.type == 'section' then for _,v in ipairs(sections) do if v == t then M.location_current = t.location M.section_current = t return t end end error("unable to find reference to section " .. (t.label or '?')) end section=t name=t[1] or 'S'..id() section[1]=nil if section.offset and not section.align then error("section " .. name .. " has offset, but no align") end end table.insert(M.location_current.sections, section) table.insert(M.sections, section) section.location = M.location_current M.section_current = section section.type = 'section' section.id = id() section.constraints = {} section.instructions = {} assert(name:sub(1,1) ~= '_', "sections can't be named with a local label") section.label = M.label(name) section.holes = {} section.refcount = 0 function section:compute_size() local instructions = self.instructions self.size=0 self.cycles=0 for _,instruction in ipairs(instructions) do instruction.offset = self.size local ins_sz = instruction.size or 0 if type(ins_sz) == 'function' then -- evaluation is needed to get the size (distinguish zpg/abs) -- labels and sections are not resolved at this point, so -- evaluation will fail if the size is not explicitly stated (.b/.w); -- in that case, assume max size ins_sz = ins_sz() end self.size = self.size + ins_sz self.cycles = self.cycles + (instruction.cycles or 0) end for _,constraint in ipairs(self.constraints) do constraint.start = instructions[constraint.from].offset constraint.finish = constraint.to==#instructions and self.size or instructions[constraint.to+1].offset end end return section end -- relate(section1, section2 [, [offset1,] offset2]) -- Add a position relationship between 'section1' and 'section2', with 'offset1' -- bytes from selected position for 'section2', and 'offset2' bytes from selec- -- -ted positon for 'section1'. -- If offset1 is omitted, -offset2 is used. M.relate = function(section1, section2, offset, offset2) assert(section1.type == 'section', "section1 is not a section") assert(section2.type == 'section', "section2 is not a section") local rel1 = relations[section1] or {} rel1[section2] = (offset2 or offset) or 0 relations[section1] = rel1 local rel2 = relations[section2] or {} rel2[section1] = (offset2 and offset) or -rel1[section2] relations[section2] = rel2 section1.related = true section2.related = true end M.label = function(name) local label,offset local section,rorg = M.section_current,M.location_current.rorg label = { type='label', section=section } if not name then name='_L'..id() end if name:sub(1,1) == '_' then -- local label name = M.label_current .. name else M.label_current = name label.bin = function() M.label_current = name end end if symbols[name] then error("duplicate symbol: " .. name) end symbols[name] = label label.label = name label.size = function() offset = section.size label.size = 0 return 0 end label.resolve = function() local o = section.org + offset return rorg(o),o end table.insert(section.instructions, label) return name,label end M.samepage = function() local section = M.section_current table.insert(section.constraints, { type='samepage', from=#section.instructions+1 }) end M.crosspage = function() local section = M.section_current table.insert(section.constraints, { type='crosspage', from=#section.instructions+1 }) end M.endpage = function() local section = M.section_current local constraint = section.constraints[#section.constraints] assert(constraint and not constraint.to, "closing constraint, but no constraint is open") constraint.to = #section.instructions end -- skip(bytes) -- Insert a hole in the section of 'bytes' bytes, which can be used by other -- relocatable sections. M.skip = function(bytes) local l65dbg = { info=debug.getinfo(2, 'Sl'), trace=debug.traceback(nil, 1) } local ins,section = {},M.section_current ins.size = function() table.insert(section.holes, { start=ins.offset, size=bytes }) return bytes end ins.bin = function(filler) return nil,bytes end table.insert(section.instructions, ins) end -- sleep(cycles [, noillegal]) -- Waste 'cycles' cycles. If 'noillegal' is true, trashes NZ flags. M.sleep = function(cycles, noillegal) assert(cycles > 1, "can't sleep for less than 2 cycles") if cycles & 1 ~= 0 then if noillegal then bitzpg(0) else nopzpg(0) end cycles = cycles - 3 end for i=1,cycles/2 do nopimp() end end local op_resolve = function(v) if type(v) == 'function' then v=v() end if type(v) == 'table' and v.label then v = symbols[v.label] end if type(v) == 'string' then v = symbols[v] end if type(v) ~= 'number' then error("unresolved symbol: " .. tostring(v)) end return v end M.op_resolve = op_resolve local size_ref = function(v) if type(v) == 'string' then v=symbols[v] end if type(v) == 'table' and v.type == 'label' then v.section.refcount = 1 + (v.section.refcount or 0) end end M.size_ref = size_ref local size_dc = function(v) if type(v) == 'function' then local r,x = M.pcall(v) if not r or not x then return v end end size_ref(v) return v end M.size_dc = size_dc local size_op = function(late, early) if type(late) == 'function' then local r,x = M.pcall(late, early or 0, op_resolve) if not r or not x then return late,early end late=x early=nil end size_ref(late) size_ref(early) return late,early end M.size_op = size_op local byte_normalize = function(v) if v < -0x80 or v > 0xFF then error("value out of byte range: " .. v) end if v < 0 then v = v + 0x100 end return v & 0xff end M.byte_normalize = byte_normalize local word_normalize = function(v) if v < -0x8000 or v > 0xFFFF then error("value out of word range: " .. v) end if v < 0 then v = v + 0x10000 end return v & 0xffff end M.word_normalize = word_normalize local long_normalize = function(v) if v < -0x80000000 or v > 0xFFFFFFFF then error("value out of word range: " .. v) end if v < 0 then v = v + 0x100000000 end return v & 0xffffffff end M.long_normalize = long_normalize -- charset([s] [, f]) -- Set a new charset to be used for next string data in byte(). -- Without argument, revert to Lua charset. -- s: string of all letters of charset -- f: letter index offset or function to transform the letter index M.charset = function(s, f) local st = type(s) if st == 'nil' then M.cs = nil return s end if st == 'table' then M.cs = s return s end if not f then f = function(v) return v end elseif type(f) == 'number' then f = function(v) return v + f end end local t,i={},0 for c in s:gmatch'.' do local v=i t[c]=function() return f(v) end i=i+1 end M.cs=t return t end M.byte_impl = function(args, nrm) local l65dbg = { info=debug.getinfo(2, 'Sl'), trace=debug.traceback(nil, 1) } local data,cs = {},M.cs for k,v in ipairs(args) do local t = type(v) if t == 'number' or t == 'function' then data[#data+1] = v elseif t == 'table' then table.move(v,1,#v,#data+1,data) elseif t == 'string' then if cs then for c in v:gmatch'.' do local i=cs[c] if not i then error("character " .. c .. " is not part of current charset") end data[#data+1]=i end else local s = {v:byte(1,#v)} table.move(s, 1, #s, #data+1, data) end else error("unsupported type for byte() argument: " .. t .. ", value: " .. v) end end local size = function() for i,v in ipairs(data) do data[i] = size_dc(v) end return #data end local bin = function() local l65dbg=l65dbg local b={} for k,v in ipairs(data) do if type(v) == 'function' then v = v() end local vt = type(v) if vt == 'table' and v.label then v = symbols[v.label] elseif vt == 'string' then v = symbols[v] end if type(v) ~= 'number' then error("unresolved symbol for dc.b, index " .. k) end b[#b+1] = nrm(v) end return b end table.insert(M.section_current.instructions, { data=data, size=size, bin=bin }) end -- byte(...) -- Declare bytes to go into the binary stream. -- Each argument can be either: -- * a number resolving to a valid range byte -- * a string, converted to bytes using the charset previously defined, -- or Lua's charset if none was defined -- * a table, with each entry resolving to a valid range byte -- * a function, resolving to exactly one valid range byte, evaluated -- after symbols have been resolved M.byte = function(...) return M.byte_impl({...}, byte_normalize) end local byte_encapsulate = function(args) for k,v in ipairs(args) do local vt = type(v) if vt == 'string' or vt == 'table' and (v.type == 'section' or v.type == 'label') then args[k] = function() return v end end end return args end M.byte_hi = function(...) return M.byte_impl(byte_encapsulate{...}, function(v) return (v>>8)&0xff end) end M.byte_lo = function(...) return M.byte_impl(byte_encapsulate{...}, function(v) return v&0xff end) end -- word(...) -- Declare words to go into the binary stream. -- Each argument can be either: -- * a section or a label -- * a number resolving to a valid range word -- * a table, with each entry resolving to a valid range word -- * a function, resolving to exactly one valid range word, evaluated -- after symbols have been resolved M.word = function(...) local l65dbg = { info=debug.getinfo(2, 'Sl'), trace=debug.traceback(nil, 1) } local args = {...} local data = {} for k,v in ipairs(args) do local t = type(v) if t == 'number' or t == 'function' or t == 'string' then data[#data+1] = v elseif t == 'table' then if v.type == 'section' or v.type == 'label' then data[#data+1] = function() return v end else table.move(v,1,#v,#data+1,data) end else error("unsupported type for word() argument: " .. t .. ", value: " .. v) end end local size = function() for i,v in ipairs(data) do data[i] = size_dc(v) end return #data*2 end local bin = function() local l65dbg=l65dbg local b={} for k,v in ipairs(data) do if type(v) == 'function' then v = v() end local vt = type(v) if vt == 'table' and v.label then v = symbols[v.label] elseif vt == 'string' then v = symbols[v] end if type(v) ~= 'number' then error("unresolved symbol for dc.w, index " .. k) end v = word_normalize(v) b[#b+1] = v&0xff b[#b+1] = v>>8 end return b end table.insert(M.section_current.instructions, { data=data, size=size, bin=bin }) end M.long = function(...) local l65dbg = { info=debug.getinfo(2, 'Sl'), trace=debug.traceback(nil, 1) } local args = {...} local data = {} for k,v in ipairs(args) do local t = type(v) if t == 'number' or t == 'function' or t == 'string' then data[#data+1] = v elseif t == 'table' then if v.type == 'section' or v.type == 'label' then data[#data+1] = function() return v end else table.move(v,1,#v,#data+1,data) end else error("unsupported type for long() argument: " .. t .. ", value: " .. v) end end local size = function() for i,v in ipairs(data) do data[i] = size_dc(v) end return #data*4 end local bin = function() local l65dbg=l65dbg local b={} for k,v in ipairs(data) do if type(v) == 'function' then v = v() end local vt = type(v) if vt == 'table' and v.label then v = symbols[v.label] elseif vt == 'string' then v = symbols[v] end if type(v) ~= 'number' then error("unresolved symbol for dc.l, index " .. k) end v = long_normalize(v) b[#b+1] = v&0xff b[#b+1] = (v>>8)&0xff b[#b+1] = (v>>16)&0xff b[#b+1] = v>>24 end return b end table.insert(M.section_current.instructions, { data=data, size=size, bin=bin }) end local op = function(code, cycles, extra_on_crosspage) return { opc=code, cycles=cycles or cycles_def, xcross=extra_on_crosspage or xcross_def } end M.op = op local op_eval = function(late, early) local x = early or 0 return type(late) == 'function' and late(x,op_resolve) or x+op_resolve(late) end M.op_eval = op_eval return M