diff options
Diffstat (limited to 'pokemontools/preprocessor.py')
-rw-r--r-- | pokemontools/preprocessor.py | 414 |
1 files changed, 212 insertions, 202 deletions
diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index d9373ac..026da41 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -5,13 +5,8 @@ Basic preprocessor for both pokecrystal and pokered. import sys -from crystal import ( - DataByteWordMacro, -) - -default_macros = [ - DataByteWordMacro, -] +import exceptions +import crystal chars = { "ガ": 0x05, @@ -278,16 +273,6 @@ chars = { "9": 0xFF } -class PreprocessorException(Exception): - """ - There was a problem in the preprocessor. - """ - -class MacroException(PreprocessorException): - """ - There was a problem with a macro. - """ - def separate_comment(l): """ Separates asm and comments on a single line. @@ -299,7 +284,10 @@ def separate_comment(l): break if l[i] == "\"": in_quotes = not in_quotes - return l[:i], l[i:] or None + return (l[:i], l[i:]) or None + +def make_macro_table(macros): + return dict(((macro.macro_name, macro) for macro in macros)) def quote_translator(asm): """ @@ -387,38 +375,7 @@ def quote_translator(asm): return output -def extract_token(asm): - return asm.split(" ")[0].strip() - -def make_macro_table(macros): - return dict(((macro.macro_name, macro) for macro in macros)) - -def macro_test(asm, macro_table): - """ - Returns a matching macro, or None/False. - """ - # macros are determined by the first symbol on the line - token = extract_token(asm) - - # skip db and dw since rgbasm handles those and they aren't macros - if token is not None and token not in ["db", "dw"] and token in macro_table: - return (macro_table[token], token) - else: - return (None, None) - -def is_based_on(something, base): - """ - Checks whether or not 'something' is a class that is a subclass of a class - by name. This is a terrible hack but it removes a direct dependency on - existing macros. - - Used by macro_translator. - """ - options = [str(klass.__name__) for klass in something.__bases__] - options += [something.__name__] - return (base in options) - -def check_macro_sanity(params, macro, original_line): +def check_macro_sanity(self, params, macro, original_line): """ Checks whether or not the correct number of arguments are being passed to a certain macro. There are a number of possibilities based on the types of @@ -441,12 +398,12 @@ def check_macro_sanity(params, macro, original_line): elif param_klass.size == 3: allowed_length += 2 # bank and label else: - raise MacroException( + raise exceptions.MacroException( "dunno what to do with a macro param with a size > 3 (size={size})" .format(size=param_klass.size) ) else: - raise MacroException( + raise exceptions.MacroException( "dunno what to do with this non db/dw macro param: {klass} in line {line}" .format(klass=param_klass, line=original_line) ) @@ -461,7 +418,7 @@ def check_macro_sanity(params, macro, original_line): params_len = len(params) if params_len not in allowed_lengths: - raise PreprocessorException( + raise exceptions.PreprocessorException( "mismatched number of parameters ({count}, instead of any of {allowed}) on this line: {line}" .format( count=params_len, @@ -472,170 +429,223 @@ def check_macro_sanity(params, macro, original_line): return True -def macro_translator(macro, token, line, show_original_lines=False, do_macro_sanity_check=False): - """ - Converts a line with a macro into a rgbasm-compatible line. +def extract_token(asm): + return asm.split(" ")[0].strip() - @param show_original_lines: show lines before preprocessing in stdout - @param do_macro_sanity_check: helpful for debugging macros +def is_based_on(something, base): """ - if macro.macro_name != token: - raise MacroException("macro/token mismatch") - - original_line = line - - # remove trailing newline - if line[-1] == "\n": - line = line[:-1] - else: - original_line += "\n" - - # remove first tab - has_tab = False - if line[0] == "\t": - has_tab = True - line = line[1:] - - # remove duplicate whitespace (also trailing) - line = " ".join(line.split()) - - params = [] - - # check if the line has params - if " " in line: - # split the line into separate parameters - params = line.replace(token, "").split(",") - - # check if there are no params (redundant) - if len(params) == 1 and params[0] == "": - raise MacroException("macro has no params?") - - # write out a comment showing the original line - if show_original_lines: - sys.stdout.write("; original_line: " + original_line) - - # rgbasm can handle "db" so no preprocessing is required, plus this wont be - # reached because of earlier checks in macro_test. - if macro.macro_name in ["db", "dw"]: - sys.stdout.write(original_line) - return - - # certain macros don't need an initial byte written - # do: all scripting macros - # don't: signpost, warp_def, person_event, xy_trigger - if not macro.override_byte_check: - sys.stdout.write("db ${0:02X}\n".format(macro.id)) - - # Does the number of parameters on this line match any allowed number of - # parameters that the macro expects? - if do_macro_sanity_check: - check_macro_sanity(params, macro, original_line) - - # used for storetext - correction = 0 - - output = "" - - index = 0 - while index < len(params): - param_type = macro.param_types[index - correction] - description = param_type["name"] - param_klass = param_type["class"] - byte_type = param_klass.byte_type # db or dw - size = param_klass.size - param = params[index].strip() - - # param_klass.to_asm() won't work here because it doesn't - # include db/dw. - - # some parameters are really multiple types of bytes - if (byte_type == "dw" and size != 2) or \ - (byte_type == "db" and size != 1): - - output += ("; " + description + "\n") - - if size == 3 and is_based_on(param_klass, "PointerLabelBeforeBank"): - # write the bank first - output += ("db " + param + "\n") - # write the pointer second - output += ("dw " + params[index+1].strip() + "\n") - index += 2 - correction += 1 - elif size == 3 and is_based_on(param_klass, "PointerLabelAfterBank"): - # write the pointer first - output += ("dw " + param + "\n") - # write the bank second - output += ("db " + params[index+1].strip() + "\n") - index += 2 - correction += 1 - elif size == 3 and "from_asm" in dir(param_klass): - output += ("db " + param_klass.from_asm(param) + "\n") - index += 1 - else: - raise MacroException( - "dunno what to do with this macro param ({klass}) in line: {line}" - .format( - klass=param_klass, - line=original_line, - ) - ) + Checks whether or not 'something' is a class that is a subclass of a class + by name. This is a terrible hack but it removes a direct dependency on + existing macros. - # or just print out the byte - else: - output += (byte_type + " " + param + " ; " + description + "\n") + Used by macro_translator. + """ + options = [str(klass.__name__) for klass in something.__bases__] + options += [something.__name__] + return (base in options) - index += 1 +class Preprocessor(object): + """ + A wrapper around the actual preprocessing step. Because rgbasm can't handle + many of these macros. + """ - sys.stdout.write(output) + default_macros = [ + crystal.DataByteWordMacro, + ] + + def __init__(self, config, macros=None): + """ + Setup the preprocessor. + """ + self.config = config + + if macros == None: + macros = Preprocessor.default_macros + + self.macros = macros + self.macro_table = make_macro_table(self.macros) + + def preprocess(self, lines=None): + """ + Run the preprocessor against stdin. + """ + if not lines: + # read each line from stdin + lines = (sys.stdin.readlines()) + elif not isinstance(lines, list): + # split up the input into individual lines + lines = lines.split("\n") + + for l in lines: + self.read_line(l) + + def read_line(self, l): + """ + Preprocesses a given line of asm. + """ + + if l in ["\n", ""] or l[0] == ";": + sys.stdout.write(l) + return # jump out early + + # strip comments from asm + asm, comment = separate_comment(l) + + # export all labels + if ':' in asm[:asm.find('"')] and "macro" not in asm.lower(): + sys.stdout.write('GLOBAL ' + asm.split(':')[0] + '\n') + + # expect preprocessed .asm files + if "INCLUDE" in asm: + asm = asm.replace('.asm','.tx') + sys.stdout.write(asm) -def read_line(l, macro_table): - """Preprocesses a given line of asm.""" + # ascii string macro preserves the bytes as ascii (skip the translator) + elif len(asm) > 6 and ("ascii " == asm[:6] or "\tascii " == asm[:7]): + asm = asm.replace("ascii", "db", 1) + sys.stdout.write(asm) - if l in ["\n", ""] or l[0] == ";": - sys.stdout.write(l) - return # jump out early + # convert text to bytes when a quote appears (not in a comment) + elif "\"" in asm: + sys.stdout.write(quote_translator(asm)) - # strip comments from asm - asm, comment = separate_comment(l) + # check against other preprocessor features + else: + macro, token = self.macro_test(asm) + if macro: + self.macro_translator(macro, token, asm) + else: + sys.stdout.write(asm) - # export all labels - if ':' in asm[:asm.find('"')] and "macro" not in asm.lower(): - sys.stdout.write('GLOBAL ' + asm.split(':')[0] + '\n') + if comment: + sys.stdout.write(comment) - # expect preprocessed .asm files - if "INCLUDE" in asm: - asm = asm.replace('.asm','.tx') - sys.stdout.write(asm) + def macro_translator(self, macro, token, line, show_original_lines=False, do_macro_sanity_check=False): + """ + Converts a line with a macro into a rgbasm-compatible line. - # ascii string macro preserves the bytes as ascii (skip the translator) - elif len(asm) > 6 and ("ascii " == asm[:6] or "\tascii " == asm[:7]): - asm = asm.replace("ascii", "db", 1) - sys.stdout.write(asm) + @param show_original_lines: show lines before preprocessing in stdout + @param do_macro_sanity_check: helpful for debugging macros + """ + if macro.macro_name != token: + raise exceptions.MacroException("macro/token mismatch") - # convert text to bytes when a quote appears (not in a comment) - elif "\"" in asm: - sys.stdout.write(quote_translator(asm)) + original_line = line - # check against other preprocessor features - else: - macro, token = macro_test(asm, macro_table) - if macro: - macro_translator(macro, token, asm) + # remove trailing newline + if line[-1] == "\n": + line = line[:-1] else: - sys.stdout.write(asm) + original_line += "\n" + + # remove first tab + has_tab = False + if line[0] == "\t": + has_tab = True + line = line[1:] + + # remove duplicate whitespace (also trailing) + line = " ".join(line.split()) + + params = [] + + # check if the line has params + if " " in line: + # split the line into separate parameters + params = line.replace(token, "").split(",") + + # check if there are no params (redundant) + if len(params) == 1 and params[0] == "": + raise exceptions.MacroException("macro has no params?") + + # write out a comment showing the original line + if show_original_lines: + sys.stdout.write("; original_line: " + original_line) + + # rgbasm can handle "db" so no preprocessing is required, plus this wont be + # reached because of earlier checks in macro_test. + if macro.macro_name in ["db", "dw"]: + sys.stdout.write(original_line) + return + + # certain macros don't need an initial byte written + # do: all scripting macros + # don't: signpost, warp_def, person_event, xy_trigger + if not macro.override_byte_check: + sys.stdout.write("db ${0:02X}\n".format(macro.id)) + + # Does the number of parameters on this line match any allowed number of + # parameters that the macro expects? + if do_macro_sanity_check: + self.check_macro_sanity(params, macro, original_line) + + # used for storetext + correction = 0 + + output = "" + + index = 0 + while index < len(params): + param_type = macro.param_types[index - correction] + description = param_type["name"] + param_klass = param_type["class"] + byte_type = param_klass.byte_type # db or dw + size = param_klass.size + param = params[index].strip() + + # param_klass.to_asm() won't work here because it doesn't + # include db/dw. + + # some parameters are really multiple types of bytes + if (byte_type == "dw" and size != 2) or \ + (byte_type == "db" and size != 1): + + output += ("; " + description + "\n") + + if size == 3 and is_based_on(param_klass, "PointerLabelBeforeBank"): + # write the bank first + output += ("db " + param + "\n") + # write the pointer second + output += ("dw " + params[index+1].strip() + "\n") + index += 2 + correction += 1 + elif size == 3 and is_based_on(param_klass, "PointerLabelAfterBank"): + # write the pointer first + output += ("dw " + param + "\n") + # write the bank second + output += ("db " + params[index+1].strip() + "\n") + index += 2 + correction += 1 + elif size == 3 and "from_asm" in dir(param_klass): + output += ("db " + param_klass.from_asm(param) + "\n") + index += 1 + else: + raise exceptions.MacroException( + "dunno what to do with this macro param ({klass}) in line: {line}" + .format( + klass=param_klass, + line=original_line, + ) + ) - if comment: - sys.stdout.write(comment) + # or just print out the byte + else: + output += (byte_type + " " + param + " ; " + description + "\n") + + index += 1 -def preprocess(macro_table, lines=None): - """Main entry point for the preprocessor.""" + sys.stdout.write(output) - if not lines: - # read each line from stdin - lines = (sys.stdin.readlines()) - elif not isinstance(lines, list): - # split up the input into individual lines - lines = lines.split("\n") + def macro_test(self, asm): + """ + Returns a matching macro, or None/False. + """ + # macros are determined by the first symbol on the line + token = extract_token(asm) - for l in lines: - read_line(l, macro_table) + # skip db and dw since rgbasm handles those and they aren't macros + if token is not None and token not in ["db", "dw"] and token in self.macro_table: + return (self.macro_table[token], token) + else: + return (None, None) |