diff --git a/Lua/inject.lua b/Lua/inject.lua index 90ef787..639ec5f 100644 --- a/Lua/inject.lua +++ b/Lua/inject.lua @@ -108,6 +108,7 @@ local function inject(fn) dprint(("%08X"):format(pos), line) pos = pos % 0x80000000 size = size + 1 + -- FIXME: doesn't detect .skip/.space directives if pos > cons_pos and (pos < inject_end or cons_pos == pos - 1) then cons_pos = pos end diff --git a/Lua/inject/dpad control.asm b/Lua/inject/dpad control.asm index 4b8070a..45a3f7c 100644 --- a/Lua/inject/dpad control.asm +++ b/Lua/inject/dpad control.asm @@ -13,7 +13,7 @@ dpad_control: dpad_values: // use table of values for branchless operation .byte 0, 1, -1, 0 - .byte -16, -8, -32, -16 - .byte +16, +32, +8, +16 + .byte -16, -4, -64, -16 + .byte +16, +64, +4, +16 .byte 0, 1, -1, 0 .align diff --git a/Lua/inject/print.asm b/Lua/inject/print.asm index 956df91..3314b51 100644 --- a/Lua/inject/print.asm +++ b/Lua/inject/print.asm @@ -34,10 +34,10 @@ jpop 4, 1, ra fmt: - .byte 0x25,0x73,0x00 // %s + .asciiz "%s" .align str: - .byte 0x68,0x65,0x79,0x00 // hey + .asciiz "hey" .align .include "simple text.asm" @@ -72,10 +72,9 @@ spawned: buffer_pos: .word 0 -// we'll just let this overflow -.align 8 +.align 4 buffer: - .word 0 + .skip 0x3000 // overwrite (not hook) the debug printing function .org 0x800021B0 diff --git a/Lua/inject/spawn.asm b/Lua/inject/spawn.asm index 394082d..f59e580 100644 --- a/Lua/inject/spawn.asm +++ b/Lua/inject/spawn.asm @@ -2,6 +2,8 @@ [button_R]: 0x0010 [button_any]: 0x0F20 +[min_actor_no]: 0 + [hold_delay_amount]: 3 push 4, s0, s1, s2, s3, s4, ra, @@ -40,7 +42,7 @@ mov a0, s3 andi s3, v0, 0xFFFF +: // set min/max on actor number - subi t4, s0, 1 + subi t4, s0, @min_actor_no bgez t4, + nop li s0, @max_actor_no @@ -48,7 +50,7 @@ subi t4, s0, @max_actor_no blez t4, + nop - li s0, 1 + li s0, @min_actor_no +: // spawn andi t3, s2, @button_L beqz t3, return @@ -87,7 +89,7 @@ selected: .word 0 fmt: - .byte 0x25,0x30,0x34,0x58,0x00 // %04X + .asciiz "%04X" .align .include "dpad control.asm" @@ -97,7 +99,24 @@ fmt: hold_delay: .word 0 +object_spawn_wrap: + // a0: object table + // a1: object number + beqz a0, + + nop + beqi a0, 1, + + nop + beqi a0, 2, + + nop + jal @object_spawn + nop ++: + jr + nop + .org @object_index + // a0: object table + // a1: object number // we have space for 22 instructions (on debug, 23 on 1.0?) push 4, ra, 1 mov t0, a0 @@ -116,8 +135,6 @@ hold_delay: addi t0, t0, 68 bnez t1, - nop - // NOTE: this allows object 0002 to load in places it's not meant to. - // this can mess up door graphics (among other things?) jal @object_spawn nop //subiu v0, r0, -1 // original code diff --git a/Lua/lib/lips/Dumper.lua b/Lua/lib/lips/Dumper.lua index faacf02..fb54c93 100644 --- a/Lua/lib/lips/Dumper.lua +++ b/Lua/lib/lips/Dumper.lua @@ -1,13 +1,13 @@ -local insert = table.insert local floor = math.floor +local format = string.format +local insert = table.insert local data = require "lips.data" +local util = require "lips.util" -local function bitrange(x, lower, upper) - return floor(x/2^lower) % 2^(upper - lower + 1) -end +local bitrange = util.bitrange -local Dumper = require("lips.Class")() +local Dumper = util.Class() function Dumper:init(writer, fn, options) self.writer = writer self.fn = fn or '(string)' @@ -32,16 +32,16 @@ function Dumper:push_instruction(t) self:advance(4) end -function Dumper:add_instruction_j(line, o, T) - self:push_instruction{line=line, o, T} +function Dumper:add_instruction_j(fn, line, o, T) + self:push_instruction{fn=fn, line=line, o, T} end -function Dumper:add_instruction_i(line, o, s, t, i) - self:push_instruction{line=line, o, s, t, i} +function Dumper:add_instruction_i(fn, line, o, s, t, i) + self:push_instruction{fn=fn, line=line, o, s, t, i} end -function Dumper:add_instruction_r(line, o, s, t, d, f, c) - self:push_instruction{line=line, o, s, t, d, f, c} +function Dumper:add_instruction_r(fn, line, o, s, t, d, f, c) + self:push_instruction{fn=fn, line=line, o, s, t, d, f, c} end function Dumper:add_label(name) @@ -57,6 +57,8 @@ function Dumper:add_bytes(line, ...) t = {} t.kind = 'bytes' t.size = 0 + t.fn = self.fn + t.line = self.line end t.line = line for _, b in ipairs{...} do @@ -69,9 +71,12 @@ function Dumper:add_bytes(line, ...) self:advance(t.size) end -function Dumper:add_directive(line, name, a, b) +function Dumper:add_directive(fn, line, name, a, b) + self.fn = fn + self.line = line local t = {} - t.line = line + t.fn = self.fn + t.line = self.line if name == 'BYTE' then self:add_bytes(line, a % 0x100) elseif name == 'HALFWORD' then @@ -79,11 +84,17 @@ function Dumper:add_directive(line, name, a, b) local b1 = bitrange(a, 8, 15) self:add_bytes(line, b1, b0) elseif name == 'WORD' then - local b0 = bitrange(a, 0, 7) - local b1 = bitrange(a, 8, 15) - local b2 = bitrange(a, 16, 23) - local b3 = bitrange(a, 24, 31) - self:add_bytes(line, b3, b2, b1, b0) + if type(a) == 'string' then + local t = {line=line, kind='label', name=a} + insert(self.commands, t) + self:advance(4) + else + local b0 = bitrange(a, 0, 7) + local b1 = bitrange(a, 8, 15) + local b2 = bitrange(a, 16, 23) + local b3 = bitrange(a, 24, 31) + self:add_bytes(line, b3, b2, b1, b0) + end elseif name == 'ORG' then t.kind = 'goto' t.addr = a @@ -92,11 +103,13 @@ function Dumper:add_directive(line, name, a, b) self:advance(0) elseif name == 'ALIGN' then t.kind = 'ahead' - local align = a*2 - if align == 0 then + local align + if a == 0 then align = 4 - elseif align < 0 then + elseif a < 0 then self:error('negative alignment') + else + align = 2^a end local temp = self.pos + align - 1 t.skip = temp - (temp % align) - self.pos @@ -114,18 +127,20 @@ function Dumper:add_directive(line, name, a, b) end end -function Dumper:desym(tok) - -- FIXME: errors can give wrong filename, also off by one - if type(tok[2]) == 'number' then - return tok[2] - elseif tok[1] == 'LABELSYM' then - local label = self.labels[tok[2]] +function Dumper:desym(t) + if type(t.tok) == 'number' then + return t.tok + elseif t.tt == 'REG' then + assert(data.all_registers[t.tok], 'Internal Error: unknown register') + return data.registers[t.tok] or data.fpu_registers[t.tok] or data.sys_registers[t.tok] + elseif t.tt == 'LABELSYM' then + local label = self.labels[t.tok] if label == nil then self:error('undefined label') end return label - elseif tok[1] == 'LABELREL' then - local label = self.labels[tok[2]] + elseif t.tt == 'LABELREL' then + local label = self.labels[t.tok] if label == nil then self:error('undefined label') end @@ -137,57 +152,43 @@ function Dumper:desym(tok) end return rel % 0x10000 end - self:error('failed to desym') -- internal error? + error('Internal Error: failed to desym') end -function Dumper:toval(tok) - if tok == nil then - self:error('nil value') - elseif type(tok) == 'number' then - return tok - elseif data.all_registers[tok] then - return data.registers[tok] or data.fpu_registers[tok] or data.sys_registers[tok] +function Dumper:toval(t) + assert(type(t) == 'table', 'Internal Error: invalid value') + + local val = self:desym(t) + + if t.index then + val = val % 0x80000000 + val = floor(val/4) end - if type(tok) == 'table' then - if #tok ~= 2 then - self:error('invalid token') - end - if tok[1] == 'UPPER' then - local val = self:desym(tok[2]) - return bitrange(val, 16, 31) - elseif tok[1] == 'LOWER' then - local val = self:desym(tok[2]) - return bitrange(val, 0, 15) - elseif tok[1] == 'UPPEROFF' then - local val = self:desym(tok[2]) - local upper = bitrange(val, 16, 31) - local lower = bitrange(val, 0, 15) - if lower >= 0x8000 then - -- accommodate for offsets being signed - upper = (upper + 1) % 0x10000 - end - return upper - elseif tok[1] == 'SIGNED' then - local val = self:desym(tok[2]) - if val >= 0x10000 or val < -0x8000 then - self:error('value out of range') - end - return val % 0x10000 - elseif tok[1] == 'NEGATE' then - local val = -self:desym(tok[2]) - if val >= 0x10000 or val < -0x8000 then - self:error('value out of range') - end - return val % 0x10000 - elseif tok[1] == 'INDEX' then - local val = self:desym(tok[2]) % 0x80000000 - val = floor(val/4) - return val - else - return self:desym(tok) - end + if t.negate then + val = -val end - self:error('invalid value') -- internal error? + if t.negate or t.signed then + if val >= 0x10000 or val < -0x8000 then + self:error('value out of range') + end + val = val % 0x10000 + end + + if t.portion == 'upper' then + val = bitrange(val, 16, 31) + elseif t.portion == 'lower' then + val = bitrange(val, 0, 15) + elseif t.portion == 'upperoff' then + local upper = bitrange(val, 16, 31) + local lower = bitrange(val, 0, 15) + if lower >= 0x8000 then + -- accommodate for offsets being signed + upper = (upper + 1) % 0x10000 + end + val = upper + end + + return val end function Dumper:validate(n, bits) @@ -200,8 +201,8 @@ function Dumper:validate(n, bits) end end -function Dumper:valvar(tok, bits) - local val = self:toval(tok) +function Dumper:valvar(t, bits) + local val = self:toval(t) self:validate(val, bits) return val end @@ -236,7 +237,7 @@ function Dumper:dump_instruction(t) lw = lw + self:valvar(t[5], 5)*0x40 lw = lw + self:valvar(t[6], 6) else - error('Internal Error: unknown n-size', 1) + error('Internal Error: unknown n-size') end return uw, lw @@ -245,9 +246,9 @@ end function Dumper:dump() self.pos = self.options.offset or 0 for i, t in ipairs(self.commands) do - if t.line == nil then - error('Internal Error: no line number available') - end + assert(t.fn, 'Internal Error: no file name available') + assert(t.line, 'Internal Error: no line number available') + self.fn = t.fn self.line = t.line if t.kind == 'instruction' then local uw, lw = self:dump_instruction(t) @@ -268,8 +269,16 @@ function Dumper:dump() else self.pos = self.pos + t.skip end + elseif t.kind == 'label' then + local val = self:desym{'LABELSYM', t.name} + val = (val % 0x80000000) + 0x80000000 + local b0 = bitrange(val, 0, 7) + local b1 = bitrange(val, 8, 15) + local b2 = bitrange(val, 16, 23) + local b3 = bitrange(val, 24, 31) + self:write{b3, b2, b1, b0} else - error('Internal Error: unknown command', 1) + error('Internal Error: unknown command') end end end diff --git a/Lua/lib/lips/Lexer.lua b/Lua/lib/lips/Lexer.lua index 922f454..33f6ebd 100644 --- a/Lua/lib/lips/Lexer.lua +++ b/Lua/lib/lips/Lexer.lua @@ -1,21 +1,26 @@ local byte = string.byte local char = string.char local find = string.find -local open = io.open +local format = string.format +local insert = table.insert local data = require "lips.data" +local util = require "lips.util" -local function readfile(fn) - local f = open(fn, 'r') - if not f then - error('could not open assembly file for reading: '..tostring(fn), 2) - end - local asm = f:read('*a') - f:close() - return asm -end +local simple_escapes = { + ['0'] = 0x00, + ['\\'] = 0x5C, + ['"'] = 0x22, + ['a'] = 0x07, + ['b'] = 0x08, + ['f'] = 0x0C, + ['n'] = 0x0A, + ['r'] = 0x0D, + ['t'] = 0x09, + ['v'] = 0x0B, +} -local Lexer = require("lips.Class")() +local Lexer = util.Class() function Lexer:init(asm, fn, options) self.asm = asm self.fn = fn or '(string)' @@ -139,8 +144,8 @@ function Lexer:lex_hex(yield) local entered = false while true do if self.chr == '\n' then - self:nextc() yield('EOL', '\n') + self:nextc() elseif self.ord == self.EOF then self:error('unexpected EOF; incomplete hex directive') elseif self.chr == ';' then @@ -191,8 +196,8 @@ end function Lexer:lex_block_comment(yield) while true do if self.chr == '\n' then - self:nextc() yield('EOL', '\n') + self:nextc() elseif self.ord == self.EOF then self:error('unexpected EOF; incomplete block comment') elseif self.chrchr == '*/' then @@ -206,14 +211,49 @@ function Lexer:lex_block_comment(yield) end function Lexer:lex_string(yield) - -- TODO: support escaping if self.chr ~= '"' then - self:error("expected opening double quote") + self:error('expected opening double quote') + end + self:nextc() + + local bytes = {} + while true do + if self.chr == '\n' then + self:error('unimplemented') + yield('EOL', '\n') + self:nextc() + elseif self.ord == self.EOF then + self:nextc() + self:error('unexpected EOF; incomplete string') + elseif self.chr == '"' then + self:nextc() + break + elseif self.chr == '\\' then + self:nextc() + local simple = simple_escapes[self.chr] + if simple then + insert(bytes, simple) + else + self:error('unknown escape sequence') + end + self:nextc() + else + insert(bytes, byte(self.chr)) + self:nextc() + end + end + + yield('STRING', bytes) +end + +function Lexer:lex_string_naive(yield) -- no escape sequences + if self.chr ~= '"' then + self:error('expected opening double quote') end self:nextc() local buff = self:read_chars('[^"\n]') if self.chr ~= '"' then - self:error("expected closing double quote") + self:error('expected closing double quote') end self:nextc() yield('STRING', buff) @@ -222,13 +262,13 @@ end function Lexer:lex_include(_yield) self:read_chars('%s') local fn - self:lex_string(function(tt, tok) + self:lex_string_naive(function(tt, tok) fn = tok end) if self.options.path then fn = self.options.path..fn end - local sublexer = Lexer(readfile(fn), fn, self.options) + local sublexer = Lexer(util.readfile(fn), fn, self.options) sublexer:lex(_yield) end @@ -238,8 +278,8 @@ function Lexer:lex(_yield) end while true do if self.chr == '\n' then - self:nextc() yield('EOL', '\n') + self:nextc() elseif self.ord == self.EOF then yield('EOF', self.EOF) break @@ -268,24 +308,23 @@ function Lexer:lex(_yield) end self:nextc() yield('DEF', buff) + elseif self.chr == ']' then + self:error('unmatched closing bracket') elseif self.chr == '(' then self:nextc() - local buff = self:read_chars('[%w_]') - if self.chr ~= ')' then - self:error('invalid register name') - end + yield('OPEN', '(') + elseif self.chr == ')' then self:nextc() - local up = buff:upper() - if not data.all_registers[up] then - self:error('not a register') - end - yield('DEREF', up) + yield('CLOSE', ')') elseif self.chr == '.' then self:nextc() local buff = self:read_chars('[%w]') local up = buff:upper() + if data.directive_aliases[up] then + up = data.directive_aliases[up] + end if not data.all_directives[up] then - self:error('not a directive') + self:error('unknown directive') end if up == 'INC' or up == 'INCASM' or up == 'INCLUDE' then yield('DIR', 'INC') @@ -293,10 +332,16 @@ function Lexer:lex(_yield) else yield('DIR', up) end + elseif self.chr == '"' then + self:lex_string(yield) elseif self.chr == '@' then self:nextc() local buff = self:read_chars('[%w_]') yield('DEFSYM', buff) + elseif self.chr == '%' then + self:nextc() + local call = self:read_chars('[%w_]') + yield('SPECIAL', call) elseif self.chr:find('[%a_]') then local buff = self:read_chars('[%w_.]') local up = buff:upper() @@ -318,10 +363,6 @@ function Lexer:lex(_yield) end yield('LABELSYM', buff) end - elseif self.chr == ']' then - self:error('unmatched closing bracket') - elseif self.chr == ')' then - self:error('unmatched closing parenthesis') elseif self.chr == '+' or self.chr == '-' then local sign_chr = self.chr local sign = sign_chr == '+' and 1 or -1 diff --git a/Lua/lib/lips/Muncher.lua b/Lua/lib/lips/Muncher.lua new file mode 100644 index 0000000..db54263 --- /dev/null +++ b/Lua/lib/lips/Muncher.lua @@ -0,0 +1,157 @@ +local format = string.format +local insert = table.insert + +local data = require "lips.data" +local util = require "lips.util" +local Token = require "lips.Token" + +local arg_types = { + NUM = true, + REG = true, + DEFSYM = true, + LABELSYM = true, + RELLABELSYM = true, +} + +local Muncher = util.Class() +-- no base init method + +function Muncher:error(msg) + error(format('%s:%d: Error: %s', self.fn, self.line, msg), 2) +end + +function Muncher:token(t, val) + -- note: call Token directly if you want to specify fn and line manually + if type(t) == 'table' then + t.fn = self.fn + t.line = self.line + return Token(t) + else + return Token(self.fn, self.line, t, val) + end +end + +function Muncher:advance() + self.i = self.i + 1 + self.t = self.tokens[self.i] + self.tt = self.t.tt + self.tok = self.t.tok + self.fn = self.t.fn + self.line = self.t.line + return self.t +end + +function Muncher:is_EOL() + return self.tt == 'EOL' or self.tt == 'EOF' +end + +function Muncher:expect_EOL() + if self:is_EOL() then + self:advance() + return + end + self:error('expected end of line') +end + +function Muncher:optional_comma() + if self.tt == 'SEP' and self.tok == ',' then + self:advance() + return true + end +end + +function Muncher:number() + if self.tt ~= 'NUM' then + self:error('expected number') + end + local t = self.t + self:advance() + return self:token(t) +end + +function Muncher:string() + if self.tt ~= 'STRING' then + self:error('expected string') + end + local t = self.t + self:advance() + return self:token(t) +end + +function Muncher:register(registers) + registers = registers or data.registers + if self.tt ~= 'REG' then + self:error('expected register') + end + local t = self.t + if not registers[t.tok] then + self:error('wrong type of register') + end + self:advance() + return self:token(t) +end + +function Muncher:deref() + if self.tt ~= 'OPEN' then + self:error('expected opening parenthesis for dereferencing') + end + self:advance() + if self.tt ~= 'REG' then + self:error('expected register to dereference') + end + local t = self.t + self:advance() + if self.tt ~= 'CLOSE' then + self:error('expected closing parenthesis for dereferencing') + end + self:advance() + return self:token(t) +end + +function Muncher:const(relative, no_label) + if self.tt ~= 'NUM' and self.tt ~= 'LABELSYM' then + self:error('expected constant') + end + if no_label and self.tt == 'LABELSYM' then + self:error('labels are not allowed here') + end + local t = self:token(self.t) + if relative and self.tt == 'LABELSYM' then + t.tt = 'LABELREL' + end + self:advance() + return t +end + +function Muncher:special() + if self.tt ~= 'SPECIAL' then + self:error('expected special name to call') + end + local name = self.tok + self:advance() + if self.tt ~= 'OPEN' then + self:error('expected opening parenthesis for special call') + end + + local args = {} + while true do + local arg = self:advance() + if not arg_types[arg.tt] then + self:error('invalid argument type') + else + self:advance() + end + if self.tt == 'SEP' then + insert(args, arg) + elseif self.tt == 'CLOSE' then + insert(args, arg) + break + else + self:error('unexpected token in argument list') + end + end + + return name, args +end + +return Muncher diff --git a/Lua/lib/lips/Parser.lua b/Lua/lib/lips/Parser.lua index f3e9bc3..e16e933 100644 --- a/Lua/lib/lips/Parser.lua +++ b/Lua/lib/lips/Parser.lua @@ -1,139 +1,79 @@ local insert = table.insert -local format = string.format local data = require "lips.data" +local util = require "lips.util" local overrides = require "lips.overrides" -local Dumper = require "lips.Dumper" +local Token = require "lips.Token" local Lexer = require "lips.Lexer" +local Dumper = require "lips.Dumper" +local Muncher = require "lips.Muncher" +local Preproc = require "lips.Preproc" -local Parser = require("lips.Class")() +local Parser = util.Class(Muncher) function Parser:init(writer, fn, options) self.fn = fn or '(string)' self.main_fn = self.fn self.options = options or {} self.dumper = Dumper(writer, fn, options) - self.defines = {} -end - -function Parser:error(msg) - error(format('%s:%d: Error: %s', self.fn, self.line, msg), 2) -end - -function Parser:advance() - self.i = self.i + 1 - local t = self.tokens[self.i] - self.tt = t.tt - self.tok = t.tok - self.fn = t.fn - self.line = t.line - return t.tt, t.tok -end - -function Parser:is_EOL() - return self.tt == 'EOL' or self.tt == 'EOF' -end - -function Parser:expect_EOL() - if self:is_EOL() then - self:advance() - return - end - self:error('expected end of line') -end - -function Parser:optional_comma() - if self.tt == 'SEP' and self.tok == ',' then - self:advance() - return true - end -end - -function Parser:number() - if self.tt ~= 'NUM' then - self:error('expected number') - end - local value = self.tok - self:advance() - return value end function Parser:directive() local name = self.tok self:advance() - local line = self.line + local function add(...) + self.dumper:add_directive(self.fn, self.line, ...) + end if name == 'ORG' then - self.dumper:add_directive(line, name, self:number()) + add(name, self:number().tok) elseif name == 'ALIGN' or name == 'SKIP' then if self:is_EOL() and name == 'ALIGN' then - self.dumper:add_directive(line, name, 0) + add(name, 0) else local size = self:number() if self:is_EOL() then - self.dumper:add_directive(line, name, size) + add(name, size) else self:optional_comma() - self.dumper:add_directive(line, name, size, self:number()) + add(name, size, self:number().tok) end self:expect_EOL() end - elseif name == 'BYTE' or name == 'HALFWORD' or name == 'WORD' then - self.dumper:add_directive(line, name, self:number()) + elseif name == 'BYTE' or name == 'HALFWORD' then + add(name, self:number().tok) while not self:is_EOL() do self:advance() self:optional_comma() - self.dumper:add_directive(line, name, self:number()) + add(name, self:number().tok) + end + self:expect_EOL() + elseif name == 'WORD' then -- allow labels in word directives + add(name, self:const().tok) + while not self:is_EOL() do + self:advance() + self:optional_comma() + add(name, self:const().tok) end self:expect_EOL() - elseif name == 'HEX' then - self:error('unimplemented') elseif name == 'INC' then - -- noop + -- noop, handled by lexer + elseif name == 'ASCII' or name == 'ASCIIZ' then + local bytes = self:string() + for i, number in ipairs(bytes.tok) do + add('BYTE', number) + end + if name == 'ASCIIZ' then + add('BYTE', 0) + end + self:expect_EOL() elseif name == 'INCBIN' then self:error('unimplemented') - elseif name == 'FLOAT' or name == 'ASCII' or name == 'ASCIIZ' then + elseif name == 'FLOAT' then self:error('unimplemented') else self:error('unknown directive') end end -function Parser:register(t) - t = t or data.registers - if self.tt ~= 'REG' then - self:error('expected register') - end - local reg = self.tok - if not t[reg] then - self:error('wrong type of register') - end - self:advance() - return reg -end - -function Parser:deref() - if self.tt ~= 'DEREF' then - self:error('expected register to dereference') - end - local reg = self.tok - self:advance() - return reg -end - -function Parser:const(relative, no_label) - if self.tt ~= 'NUM' and self.tt ~= 'LABELSYM' then - self:error('expected constant') - end - if no_label and self.tt == 'LABELSYM' then - self:error('labels are not allowed here') - end - if relative and self.tt == 'LABELSYM' then - self.tt = 'LABELREL' - end - local t = {self.tt, self.tok} - self:advance() - return t -end - function Parser:format_in(informat) local args = {} for i=1,#informat do @@ -146,33 +86,33 @@ function Parser:format_in(informat) elseif c == 't' and not args.rt then args.rt = self:register() elseif c == 'D' and not args.fd then - args.fd = self:register(fpu_registers) + args.fd = self:register(data.fpu_registers) elseif c == 'S' and not args.fs then - args.fs = self:register(fpu_registers) + args.fs = self:register(data.fpu_registers) elseif c == 'T' and not args.ft then - args.ft = self:register(fpu_registers) + args.ft = self:register(data.fpu_registers) elseif c == 'X' and not args.rd then - args.rd = self:register(sys_registers) + args.rd = self:register(data.sys_registers) elseif c == 'Y' and not args.rs then - args.rs = self:register(sys_registers) + args.rs = self:register(data.sys_registers) elseif c == 'Z' and not args.rt then - args.rt = self:register(sys_registers) + args.rt = self:register(data.sys_registers) elseif c == 'o' and not args.offset then - args.offset = {'SIGNED', self:const()} + args.offset = Token(self:const()):set('signed') elseif c == 'r' and not args.offset then - args.offset = {'SIGNED', self:const('relative')} + args.offset = Token(self:const('relative')):set('signed') elseif c == 'i' and not args.immediate then args.immediate = self:const(nil, 'no label') elseif c == 'I' and not args.index then - args.index = {'INDEX', self:const()} + args.index = Token(self:const()):set('index') elseif c == 'k' and not args.immediate then - args.immediate = {'NEGATE', self:const(nil, 'no label')} + args.immediate = Token(self:const(nil, 'no label')):set('negate') elseif c == 'K' and not args.immediate then - args.immediate = {'SIGNED', self:const(nil, 'no label')} + args.immediate = Token(self:const(nil, 'no label')):set('signed') elseif c == 'b' and not args.base then args.base = self:deref() else - error('Internal Error: invalid input formatting string', 1) + error('Internal Error: invalid input formatting string') end if c2:find('[dstDSTorIikKXYZ]') then self:optional_comma() @@ -191,38 +131,38 @@ function Parser:format_out_raw(outformat, first, args, const, formatconst) for i=1,#outformat do local c = outformat:sub(i, i) if c == 'd' then - out[#out+1] = args.rd + out[#out+1] = self:token(args.rd) elseif c == 's' then - out[#out+1] = args.rs + out[#out+1] = self:token(args.rs) elseif c == 't' then - out[#out+1] = args.rt + out[#out+1] = self:token(args.rt) elseif c == 'D' then - out[#out+1] = args.fd + out[#out+1] = self:token(args.fd) elseif c == 'S' then - out[#out+1] = args.fs + out[#out+1] = self:token(args.fs) elseif c == 'T' then - out[#out+1] = args.ft + out[#out+1] = self:token(args.ft) elseif c == 'o' then - out[#out+1] = args.offset + out[#out+1] = self:token(args.offset) elseif c == 'i' then - out[#out+1] = args.immediate + out[#out+1] = self:token(args.immediate) elseif c == 'I' then - out[#out+1] = args.index + out[#out+1] = self:token(args.index) elseif c == 'b' then - out[#out+1] = args.base + out[#out+1] = self:token(args.base) elseif c == '0' then - out[#out+1] = 0 + out[#out+1] = self:token(0) elseif c == 'C' then - out[#out+1] = const + out[#out+1] = self:token(const) elseif c == 'F' then - out[#out+1] = formatconst + out[#out+1] = self:token(formatconst) end end local f = lookup[#outformat] if f == nil then - error('Internal Error: invalid output formatting string', 1) + error('Internal Error: invalid output formatting string') end - f(self.dumper, self.line, first, out[1], out[2], out[3], out[4], out[5]) + f(self.dumper, self.fn, self.line, first, out[1], out[2], out[3], out[4], out[5]) end function Parser:format_out(t, args) @@ -234,33 +174,41 @@ function Parser:instruction() local h = data.instructions[name] self:advance() - -- FIXME: errors thrown here probably have the wrong line number (+1) - if h == nil then - self:error('undefined instruction') + error('Internal Error: undefined instruction') elseif overrides[name] then overrides[name](self, name) - elseif h[2] == 'tob' then -- or h[2] == 'Tob' then + elseif h[2] == 'tob' then -- TODO: or h[2] == 'Tob' then local lui = data.instructions['LUI'] + local addu = data.instructions['ADDU'] local args = {} args.rt = self:register() self:optional_comma() - local o = self:const() - local is_label = o[1] == 'LABELSYM' - if self:is_EOL() then - local lui_args = {} - lui_args.immediate = {'UPPEROFF', o} - lui_args.rt = 'AT' - self:format_out(lui, lui_args) - args.offset = {'LOWER', o} - args.base = 'AT' - else - if is_label then - self:error('labels cannot be used as offsets') - end - args.offset = {'SIGNED', o} - self:optional_comma() + if self.tt == 'OPEN' then + args.offset = 0 args.base = self:deref() + else -- NUM or LABELSYM + local lui_args = {} + local addu_args = {} + local o = self:const() + args.offset = self:token(o) + if not o.portion then + args.offset:set('portion', 'lower') + end + if not o.portion and (o.tt == 'LABELSYM' or o.tok >= 0x80000000) then + lui_args.immediate = Token(o):set('portion', 'upperoff') + lui_args.rt = 'AT' + self:format_out(lui, lui_args) + if not self:is_EOL() then + addu_args.rd = 'AT' + addu_args.rs = 'AT' + addu_args.rt = self:deref() + self:format_out(addu, addu_args) + end + args.base = 'AT' + else + args.base = self:deref() + end end self:format_out(h, args) elseif h[2] ~= nil then @@ -273,7 +221,6 @@ function Parser:instruction() end function Parser:tokenize(asm) - self.tokens = {} self.i = 0 local routine = coroutine.create(function() @@ -281,103 +228,27 @@ function Parser:tokenize(asm) lexer:lex(coroutine.yield) end) - local function lex() - local t = {} + local tokens = {} + while true do local ok, a, b, c, d = coroutine.resume(routine) if not ok then a = a or 'Internal Error: lexer coroutine has stopped' error(a) end - t.tt = a - t.tok = b - t.fn = c - t.line = d - insert(self.tokens, t) - return t.tt, t.tok, t.fn, t.line - end + assert(a, 'Internal Error: missing token') - -- first pass: collect tokens, constants, and relative labels. - -- can't do more because instruction size can depend on a constant's size - -- and labels depend on instruction size. - -- note however, instruction size does not depend on label size. - -- this would cause a recursive problem to solve, - -- which is too much for our simple assembler. - local plus_labels = {} -- constructed forwards - local minus_labels = {} -- constructed backwards - while true do - local tt, tok, fn, line = lex() - self.fn = fn - self.line = line - if tt == 'DEF' then - local tt2, tok2 = lex() - if tt2 ~= 'NUM' then - self:error('expected number for define') - end - self.defines[tok] = tok2 - elseif tt == 'RELLABEL' then - if tok == '+' then - insert(plus_labels, #self.tokens) - elseif tok == '-' then - insert(minus_labels, 1, #self.tokens) - else - error('Internal Error: unexpected token for relative label', 1) - end - elseif tt == 'EOL' then - -- noop - elseif tt == 'EOF' then - if fn == self.main_fn then - break - end - elseif tt == nil then - error('Internal Error: missing token', 1) + local t = Token(c, d, a, b) + insert(tokens, t) + + if t.tt == 'EOF' and t.fn == self.main_fn then + break end end - -- resolve defines and relative labels - for i, t in ipairs(self.tokens) do - self.fn = t.fn - self.line = t.line - if t.tt == 'DEFSYM' then - t.tt = 'NUM' - t.tok = self.defines[t.tok] - if t.tok == nil then - self:error('undefined define') -- uhhh nice wording - end - elseif t.tt == 'RELLABEL' then - t.tt = 'LABEL' - -- exploits the fact that user labels can't begin with a number - t.tok = tostring(i) - elseif t.tt == 'RELLABELSYM' then - t.tt = 'LABELSYM' - local rel = t.tok - local seen = 0 - -- TODO: don't iterate over *every* label, just the ones nearby - if rel > 0 then - for _, label_i in ipairs(plus_labels) do - if label_i > i then - seen = seen + 1 - if seen == rel then - t.tok = tostring(label_i) - break - end - end - end - else - for _, label_i in ipairs(minus_labels) do - if label_i < i then - seen = seen - 1 - if seen == rel then - t.tok = tostring(label_i) - break - end - end - end - end - if seen ~= rel then - self:error('could not find appropriate relative label') - end - end - end + local preproc = Preproc(self.options) + self.tokens = preproc:process(tokens) + + assert(#self.tokens > 0, 'Internal Error: no tokens after preprocessing') end function Parser:parse(asm) @@ -392,9 +263,6 @@ function Parser:parse(asm) elseif self.tt == 'EOL' then -- empty line self:advance() - elseif self.tt == 'DEF' then - self:advance() - self:advance() elseif self.tt == 'DIR' then self:directive() elseif self.tt == 'LABEL' then diff --git a/Lua/lib/lips/Preproc.lua b/Lua/lib/lips/Preproc.lua new file mode 100644 index 0000000..07f0e72 --- /dev/null +++ b/Lua/lib/lips/Preproc.lua @@ -0,0 +1,137 @@ +local insert = table.insert + +local data = require "lips.data" +local util = require "lips.util" +local Muncher = require "lips.Muncher" +local Token = require "lips.Token" + +local Preproc = util.Class(Muncher) +function Preproc:init(options) + self.options = options or {} +end + +function Preproc:process(tokens) + self.tokens = tokens + + local defines = {} + local plus_labels = {} -- constructed forwards + local minus_labels = {} -- constructed backwards + + -- first pass: resolve defines, collect relative labels + local new_tokens = {} + self.i = 0 + while self.i < #self.tokens do + local t = self:advance() + if t.tt == nil then + error('Internal Error: missing token') + elseif t.tt == 'DEF' then + local t2 = self:advance() + if t2.tt ~= 'NUM' then + self:error('expected number for define') + end + defines[t.tok] = t2.tok + elseif t.tt == 'DEFSYM' then + local tt = 'NUM' + local tok = defines[t.tok] + if tok == nil then + self:error('undefined define') -- uhhh nice wording + end + insert(new_tokens, self:token(tt, tok)) + elseif t.tt == 'RELLABEL' then + if t.tok == '+' then + insert(plus_labels, #new_tokens + 1) + elseif t.tok == '-' then + insert(minus_labels, 1, #new_tokens + 1) + else + error('Internal Error: unexpected token for relative label') + end + insert(new_tokens, t) + else + insert(new_tokens, t) + end + end + + -- second pass: resolve relative labels + for i, t in ipairs(new_tokens) do + self.fn = t.fn + self.line = t.line + if t.tt == 'RELLABEL' then + t.tt = 'LABEL' + -- exploits the fact that user labels can't begin with a number + t.tok = tostring(i) + elseif t.tt == 'RELLABELSYM' then + t.tt = 'LABELSYM' + + local rel = t.tok + local seen = 0 + -- TODO: don't iterate over *every* label, just the ones nearby + if rel > 0 then + for _, label_i in ipairs(plus_labels) do + if label_i > i then + seen = seen + 1 + if seen == rel then + t.tok = tostring(label_i) + break + end + end + end + else + for _, label_i in ipairs(minus_labels) do + if label_i < i then + seen = seen - 1 + if seen == rel then + t.tok = tostring(label_i) + break + end + end + end + end + + if seen ~= rel then + self:error('could not find appropriate relative label') + end + end + end + + self.tokens = new_tokens + new_tokens = {} + + -- third pass: resolve specials + self.i = 0 + while self.i < #self.tokens do + local t = self:advance() + if t.tt == 'SPECIAL' then + local name, args = self:special() + -- TODO: split to its own file, not unlike overrides.lua + if name == 'hi' then + if #args ~= 1 then + self:error('%hi expected exactly one argument') + end + local tnew = self:token(args[1]):set('portion', 'upperoff') + insert(new_tokens, tnew) + elseif name == 'up' then + if #args ~= 1 then + self:error('%up expected exactly one argument') + end + local tnew = self:token(args[1]):set('portion', 'upper') + insert(new_tokens, tnew) + elseif name == 'lo' then + if #args ~= 1 then + self:error('%lo expected exactly one argument') + end + local tnew = self:token(args[1]):set('portion', 'lower') + insert(new_tokens, tnew) + else + self:error('unknown special') + end + else + insert(new_tokens, t) + end + end + + self.tokens = new_tokens + + return self.tokens +end + +return Preproc diff --git a/Lua/lib/lips/Token.lua b/Lua/lib/lips/Token.lua new file mode 100644 index 0000000..7978fec --- /dev/null +++ b/Lua/lib/lips/Token.lua @@ -0,0 +1,60 @@ +local util = require "lips.util" + +local Token = util.Class() +function Token:init(...) + local args = {...} + if #args == 1 then + local t = args[1] + if type(t) == 'table' then + for k, v in pairs(t) do + self[k] = v + end + end + elseif #args == 3 then + self.fn = args[1] + self.line = args[2] + local t = args[3] + if type(t) == 'table' then + self.tt = t[1] + self.tok = t[2] + elseif type(t) == 'string' then + self.tt = 'REG' + self.tok = t + elseif type(t) == 'number' then + self.tt = 'NUM' + self.tok = t + else + error('Internal Error: unknown type to construct', 3) + end + elseif #args == 4 then + self.fn = args[1] + self.line = args[2] + self.tt = args[3] + self.tok = args[4] + else + error('Internal Error: init takes 1, 3 or 4 arguments', 3) + end + if not self.fn then + error('Internal Error: tokens require a filename', 3) + end + if not self.line then + error('Internal Error: tokens require a line number', 3) + end + if not self.tt then + error('Internal Error: token is missing a type', 3) + end + if not self.tok then + error('Internal Error: token is missing a value', 3) + end + return self +end + +function Token:set(key, value) + if value == nil then + value = true + end + self[key] = value + return self +end + +return Token diff --git a/Lua/lib/lips/data.lua b/Lua/lib/lips/data.lua index 5129589..41656a0 100644 --- a/Lua/lib/lips/data.lua +++ b/Lua/lib/lips/data.lua @@ -29,13 +29,19 @@ data.fpu_registers = { } data.all_directives = { - 'ALIGN', 'SKIP', + 'ORG', 'ALIGN', 'SKIP', 'ASCII', 'ASCIIZ', - 'BYTE', 'HALFWORD', 'WORD', 'FLOAT', + 'BYTE', 'HALFWORD', 'WORD', --'HEX', -- excluded here due to different syntax 'INC', 'INCASM', 'INCLUDE', 'INCBIN', - 'ORG', + -- these are unlikely to be implemented + 'FLOAT', 'DOUBLE', +} + +data.directive_aliases = { + SPACE = 'SKIP', + HALF = 'HALFWORD', } data.all_registers = {} diff --git a/Lua/lib/lips/init.lua b/Lua/lib/lips/init.lua index d3dfac7..76768bb 100644 --- a/Lua/lib/lips/init.lua +++ b/Lua/lib/lips/init.lua @@ -1,6 +1,4 @@ --- split lips.lua - -local assembler = { +local lips = { _DESCRIPTION = 'Assembles MIPS assembly files for the R4300i CPU.', _URL = 'https://github.com/notwa/lips/', _LICENSE = [[ @@ -12,21 +10,10 @@ local assembler = { ]], } -local open = io.open - +local util = require "lips.util" local Parser = require "lips.Parser" -local function readfile(fn) - local f = open(fn, 'r') - if not f then - error('could not open assembly file for reading: '..tostring(fn), 2) - end - local asm = f:read('*a') - f:close() - return asm -end - -function assembler.word_writer() +function lips.word_writer() local buff = {} local max = -1 return function(pos, b) @@ -47,13 +34,13 @@ function assembler.word_writer() end end -function assembler.assemble(fn_or_asm, writer, options) +function lips.assemble(fn_or_asm, writer, options) -- assemble MIPS R4300i assembly code. -- if fn_or_asm contains a newline; treat as assembly, otherwise load file. -- returns error message on error, or nil on success. fn_or_asm = tostring(fn_or_asm) local default_writer = not writer - writer = writer or assembler.word_writer() + writer = writer or lips.word_writer() options = options or {} local function main() @@ -63,7 +50,7 @@ function assembler.assemble(fn_or_asm, writer, options) asm = fn_or_asm else fn = fn_or_asm - asm = readfile(fn) + asm = util.readfile(fn) options.path = fn:match(".*/") end @@ -83,7 +70,7 @@ function assembler.assemble(fn_or_asm, writer, options) end end -return setmetatable(assembler, { +return setmetatable(lips, { __call = function(self, ...) return self.assemble(...) end, diff --git a/Lua/lib/lips/overrides.lua b/Lua/lib/lips/overrides.lua index dfbce6e..6893414 100644 --- a/Lua/lib/lips/overrides.lua +++ b/Lua/lib/lips/overrides.lua @@ -1,6 +1,8 @@ local insert = table.insert local data = require "lips.data" +local util = require "lips.util" + local instructions = data.instructions local overrides = {} @@ -17,26 +19,33 @@ function overrides.LI(self, name) -- for us, this is just semantics. for a "real" assembler, -- LA could add appropriate RELO LUI/ADDIU directives. - if im[1] == 'LABELSYM' then + if im.tt == 'LABELSYM' then self:error('use LA for labels') end - im[2] = im[2] % 0x100000000 - if im[2] >= 0x10000 and im[2] <= 0xFFFF8000 then + if im.portion then + args.rs = 'R0' + args.immediate = im + self:format_out(addiu, args) + return + end + + im.tok = im.tok % 0x100000000 + if im.tok >= 0x10000 and im.tok <= 0xFFFF8000 then args.rs = args.rt - args.immediate = {'UPPER', im} + args.immediate = self:token(im):set('portion', 'upper') self:format_out(lui, args) - if im[2] % 0x10000 ~= 0 then - args.immediate = {'LOWER', im} + if im.tok % 0x10000 ~= 0 then + args.immediate = self:token(im):set('portion', 'lower') self:format_out(ori, args) end - elseif im[2] >= 0x8000 and im[2] < 0x10000 then + elseif im.tok >= 0x8000 and im.tok < 0x10000 then args.rs = 'R0' - args.immediate = {'LOWER', im} + args.immediate = self:token(im):set('portion', 'lower') self:format_out(ori, args) else args.rs = 'R0' - args.immediate = {'LOWER', im} + args.immediate = self:token(im):set('portion', 'lower') self:format_out(addiu, args) end end @@ -50,9 +59,9 @@ function overrides.LA(self, name) local im = self:const() args.rs = args.rt - args.immediate = {'UPPEROFF', im} + args.immediate = self:token(im):set('portion', 'upperoff') self:format_out(lui, args) - args.immediate = {'LOWER', im} + args.immediate = self:token(im):set('portion', 'lower') self:format_out(addiu, args) end @@ -84,14 +93,14 @@ function overrides.PUSH(self, name) if name == 'PUSH' then args.rt = 'SP' args.rs = 'SP' - args.immediate = {'NEGATE', {'NUM', #stack*4}} + args.immediate = self:token(#stack*4):set('negate') self:format_out(addi, args) end args.base = 'SP' for i, r in ipairs(stack) do args.rt = r if r ~= '' then - args.offset = {'NUM', (i - 1)*4} + args.offset = (i - 1)*4 self:format_out(w, args) end end @@ -102,7 +111,7 @@ function overrides.PUSH(self, name) if name == 'POP' or name == 'JPOP' then args.rt = 'SP' args.rs = 'SP' - args.immediate = {'NUM', #stack*4} + args.immediate = #stack*4 self:format_out(addi, args) end end @@ -175,7 +184,7 @@ function overrides.ROL(self, name) end self:format_out(sll, args) args.rd = 'AT' - args.immediate = {'NUM', 32 - args.immediate[2]} + args.immediate = 32 - args.immediate[2] self:format_out(srl, args) args.rd = left args.rs = left @@ -202,7 +211,7 @@ function overrides.ROR(self, name) end self:format_out(srl, args) args.rd = 'AT' - args.immediate = {'NUM', 32 - args.immediate[2]} + args.immediate = 32 - args.immediate[2] self:format_out(sll, args) args.rd = right args.rs = right @@ -238,7 +247,7 @@ function overrides.BEQI(self, name) self:optional_comma() args.immediate = self:const() self:optional_comma() - args.offset = {'SIGNED', self:const('relative')} + args.offset = self:token(self:const('relative')):set('signed') if reg == 'AT' then self:error('register cannot be AT in this pseudo-instruction') @@ -261,7 +270,7 @@ function overrides.BLTI(self, name) self:optional_comma() args.immediate = self:const() self:optional_comma() - args.offset = {'SIGNED', self:const('relative')} + args.offset = self:token(self:const('relative')):set('signed') if args.rs == 'AT' then self:error('register cannot be AT in this pseudo-instruction') @@ -287,7 +296,7 @@ function overrides.BLEI(self, name) self:optional_comma() args.immediate = self:const() self:optional_comma() - local offset = {'SIGNED', self:const('relative')} + local offset = self:token(self:const('relative')):set('signed') if reg == 'AT' then self:error('register cannot be AT in this pseudo-instruction') diff --git a/Lua/lib/lips/util.lua b/Lua/lib/lips/util.lua new file mode 100644 index 0000000..e545561 --- /dev/null +++ b/Lua/lib/lips/util.lua @@ -0,0 +1,37 @@ +local floor = math.floor +local open = io.open + +local function Class(inherit) + local class = {} + local mt_obj = {__index = class} + local mt_class = { + __call = function(self, ...) + local obj = setmetatable({}, mt_obj) + obj:init(...) + return obj + end, + __index = inherit, + } + + return setmetatable(class, mt_class) +end + +local function readfile(fn) + local f = open(fn, 'r') + if not f then + error('could not open assembly file for reading: '..tostring(fn), 2) + end + local asm = f:read('*a') + f:close() + return asm +end + +local function bitrange(x, lower, upper) + return floor(x/2^lower) % 2^(upper - lower + 1) +end + +return { + Class = Class, + readfile = readfile, + bitrange = bitrange, +}