diff --git a/README.md b/README.md index 22d3c4f..e0e6300 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,83 @@ Prints the result of the expressions as soon as they are evaluated during assembly. Can be useful for logging and debugging, and showing the value of symbols (as an alternative to the -s command line option) +`STRUCT` + +This allows structures to be defined, entries within the struct are offsets from +the beginning of the structure. Each entry in the structure defines how many bytes +are used via the RS command. ENDS closes the structure, these can be nested. Each STRUCT +automatically defines a .SizeOf value that is set to the size of the struct. +Useful for IX/IY indexing + +e.g. + +``` +Sprite: STRUCT +XPos: RS 1 +YPos: RS 1 +Width: RS 1 +Height: RS 1 +GfxPtr: RS 2 + ENDS + + ld ix, Sprites + ld b, 100 +@Loop: + ld d, (IX + Sprite.XPos) + ld e, (IX + Sprite.YPos) + ld l, (IX + Sprite.GfxPtr + 0) + ld h, (IX + Sprite.GfxPtr + 1) + call DrawSprite + ld de, Sprite.SizeOf + add ix, de + djnz @Loop + +Sprites: ds Sprite.SizeOf * 100 +``` + +`MACRO` + +Macros are small sections of code that can be repeated, passing in various parameters to +alter the contents of the macro. Each parameter is used via \0, \1 etc within the macro. +Macros can be given the same name as long as the number of parameters are different. The number +of parameters required is automatically calculated from the highest \n value within the macro. Local +labels are local to the macro itself + +e.g. + +``` +AND16BIT: MACRO + LD A, \0 + AND \2 + LD \0, A + + LD A, \1 + AND \3 + LD \1, A + ENDM + + ; The following lines in the code + AND16BIT H, L, &ff, &01 + AND16BIT D, E, &0f, &01 + + ; Would expand to the following when assembled + LD A, H + AND &ff + LD H, A + + LD A, L + AND &01 + LD L, A + + LD A, D + AND &0f + LD D, A + + LD A, E + AND &01 + LD E, A +``` + **Expressions and special characters** Wherever an instruction or directive requires a number, a mathematical @@ -489,6 +566,18 @@ INFORMATION PROVIDED BY THE PROGRAM AND THE DOCUMENTATION. **Release History** +Version 1.24, 22 July 2025 + + - Removed Debug (Adrian Brown) + +Version 1.23, 22 July 2025 + + - Added MACRO support (Adrian Brown) + +Version 1.22, 20 July 2025 + + - Added STRUCT, RS, ENDS option (Adrian Brown) + Version 1.21, 11 July 2013 - A symbol name could include tab characters, which should be treated as a source error (reported by Chris Pile) diff --git a/pyz80.py b/pyz80.py index da795a3..18994ec 100755 --- a/pyz80.py +++ b/pyz80.py @@ -7,7 +7,7 @@ # defs doesn't cause bytes to be written to output unless real data follows def printusage(): - print("pyz80 by Andrew Collier, modified by Simon Owen") + print("pyz80 by Andrew Collier, modified by Simon Owen and Adrian Brown") print(" https://github.com/simonowen/pyz80/") print("Usage:") print(" pyz80 (options) inputfile(s)") @@ -355,7 +355,7 @@ def file_and_stack(explicit_currentfile=None): def set_symbol(sym, value, explicit_currentfile=None, is_label=False): symorig = expand_symbol(sym) sym = symorig if CASE else symorig.upper() - + if sym[0]=='@': sym = sym + '@' + file_and_stack(explicit_currentfile=explicit_currentfile) @@ -366,6 +366,7 @@ def set_symbol(sym, value, explicit_currentfile=None, is_label=False): if is_label: labeltable[sym] = value + def get_symbol(sym): symorig = expand_symbol(sym) sym = symorig if CASE else symorig.upper() @@ -785,7 +786,8 @@ def op_EQU(p,opargs): else: if p==1: - set_symbol(symbol, parse_expression(opargs, signed=1, silenterror=1)) + expr_result = parse_expression(opargs, signed=1, silenterror=1) + set_symbol(symbol, expr_result) else: expr_result = parse_expression(opargs, signed=1) @@ -1620,16 +1622,216 @@ def op_ENDIF(p,opargs): return 0 +def op_STRUCT(p,opargs): + global symboltable, structstack, structstate + check_args(opargs,0) + if (symbol): + sizeof_symbol = symbol + ".SizeOf" + structstack.append([symbol, sizeof_symbol]) + set_symbol(sizeof_symbol, 0) + + # We are in a struct + structstate = 1 + else: + warning("STRUCT without symbol name") + + return 0 + +def op_RS(p,opargs) : + global symboltable, structstack, structstate + + check_args(opargs,1) + + if ( len(structstack) == 0 ) : + warning("RS used outside of STRUCT") + return 0 + + # Work out the size + cur_size = 0 + str_sym = symbol + + for s in structstack[::-1] : + # Get the current sizeof + cur_size = cur_size + get_symbol(s[1]) + + # Set the current symbol size + str_sym = s[0] + "." + str_sym + set_symbol(str_sym, cur_size) + + # Work out the size + expr_result = parse_expression(opargs, signed=1, silenterror=0) + + # Move the size on + set_symbol(structstack[-1][1], get_symbol(structstack[-1][1]) + expr_result) + + return 0 + + +def op_ENDS(p,opargs): + global symboltable, structstack, structstate + + check_args(opargs,0) + + if ( structstate != 1 ) : + warning("ENDS without matching STRUCT") + return 0 + + # Pull off the last stuct + ends_symbol, ends_sizeof_symbol = structstack.pop() + ends_sizeof = get_symbol(ends_sizeof_symbol) + + # If this was the last struct on the stack we have done, else we now need to step this size past the outer struct + if ( len(structstack) > 0 ) : + outer_struct = structstack.pop() + outer_sizeof = get_symbol(outer_struct[1]) + set_symbol(outer_struct[1], outer_sizeof + ends_sizeof) + + structstack.append(outer_struct) + else : + structstate = 0 + + return 0 + +def make_replacer(replacements): + def replace_match(match): + index = int(match.group(1)) + return replacements[index] if 0 <= index < len(replacements) else match.group(0) + return replace_match + +def op_HandleMacro(p,opargs) : + global opcode, inst, macros, macrostate, macroindex + + # See how many arguments we have + if opargs=='': + numArgs = 0 + else: + macroArgs = [item.strip() for item in opargs.split(',')] + numArgs = len(macroArgs) + + # See if we can find an entry for this + results = [entry for entry in macros if len(entry) >= 2 and entry[0] == inst and entry[1] == numArgs ] + + if len(results) > 1 : + fatal("Multiple macros with the same name and number of arguments") + elif len(results) == 0 : + fatal("Unable to find macro: " + str(inst) + " (NumArgs = " + str(numArgs) + ")") + else : + # Build up the lines to insert + lines = [] + + for l in results[0][2] : + # Build the line + line = "" + + if len(l[0]) > 0 : + line += l[0] + ":" + + line += "\t" + l[1] + + if numArgs > 0 : + processed_line = re.sub(r'\\(\d+)', make_replacer(macroArgs), line) + else : + processed_line = line + + lines.append(processed_line) + + # Track how many times for local label reasons + macroindex = macroindex + 1 + + do_pass(p, lines, inst + str(macroindex)) + + return 0 + +def op_MACRO(p,opargs): + global macros, macrostate, currentmacro + + check_args(opargs,0) + + if (symbol): + # Handle case + sym = symbol if CASE else symbol.upper() + + # Now we can add this macro, symbol, number of params + currentmacro = [sym, 0, []] + + # We are in a macro + macrostate = 1 + + # Setup the function call + globals()['op_' + sym] = op_HandleMacro + else: + warning("MACRO without symbol name") + + return 0 + +def op_ENDM(p,opargs) : + global macros, macrostate, currentmacro + + check_args(opargs,0) + + if macrostate == 1 : + # Do we already have this macro + results = [entry for entry in macros if len(entry) >= 2 and entry[0] == currentmacro[0] and entry[1] == currentmacro[1] ] + + # If we have this but its different + if len(results) > 0 : + if results[0][2] != currentmacro[2] : + fatal("Macro redefinition: " + str(currentmacro[0])) + else : + macros.append(currentmacro) + + currentmacro = None + + macrostate = 0 + else : + fatal("ENDM without opening MACRO") + + return 0 + def assemble_instruction(p, line): + global macrostate, macros, inst, currentmacro + match = re.match(r'^(\w+)(.*)', line) - if not match: + if not match and macrostate == 0: fatal("Expected opcode or directive") + + if match != None : + inst = match.group(1).upper() + args = match.group(2).strip() + else : + inst = "" + args = "" + + # Handle macros + if macrostate == 1 and (inst == "" or inst not in ('ENDM')): + if currentmacro == None : + fatal("In macro state without a macro") + + params = re.findall(r'\\(\d+)', line) + + if params != None : + numbers = list(map(int, params)) + + if numbers : + highest = max(numbers) + 1 + + # Is this greater than the previous + currentmacro[1] = max(highest, currentmacro[1]) + print("Highest Macro Params: " + str(currentmacro[1])) + currentmacro[2].append([symbol, line]) + return 0 + + # Check the struct state + if (structstate == 1) and inst not in ('RS', 'ENDS', 'STRUCT') : + fatal("Only STRUCT, ENDS and RS are valid within a struct") - inst = match.group(1).upper() - args = match.group(2).strip() + if (structstate == 0) and inst in ('RS', 'ENDS') : + print(str(structstate) + " : " + inst + " : " + args) + fatal("ENDS and RS are not valid outside of a struct") - if (ifstate < 2) or inst in ('IF', 'ELSE', 'ENDIF'): + if (ifstate < 2) or inst in ('IF', 'ELSE', 'ENDIF') : functioncall = 'op_'+inst+'(p,args)' + if PYTHONERRORS: return eval(functioncall) else: @@ -1638,7 +1840,7 @@ def assemble_instruction(p, line): except SystemExit as e: sys.exit(e) except: - fatal("Opcode not recognised") + fatal("Opcode not recognised: " + str(functioncall)) else: return 0 @@ -1668,7 +1870,15 @@ def assembler_pass(p, inputfile): except: fatal("Couldn't open file "+this_currentfilename+" for reading") + do_pass(p, wholefile, this_currentfilename) +def do_pass(p, wholefile, this_currentfilename) : + global memory, symboltable, symusetable, labeltable, origin, dumppage, dumporigin, symbol + global global_currentfile, global_currentline, lstcode, listingfile, macrostate +# file references are local, so assembler_pass can be called recursively (for op_INC) +# but copied to a global identifier for warning printouts + global global_path + consider_linenumber=0 while consider_linenumber < len(wholefile): @@ -1729,7 +1939,7 @@ def assembler_pass(p, inputfile): if len( symbol.split()) > 1: fatal("Whitespace not allowed in symbol name") - if (symbol and (opcode[0:3].upper() !="EQU") and (ifstate < 2)): + if (symbol and (opcode[0:3].upper() !="EQU") and (ifstate < 2) and (macrostate == 0) ): if p==1: set_symbol(symbol, origin, is_label=True) elif get_symbol(symbol) != origin: @@ -1896,7 +2106,12 @@ def writelisting(line): forstack=[] ifstack = [] ifstate = 0 - + structstack = [] + structstate = 0 + macros = [] + macrostate = 0 + currentmacro = None + for value in predefsymbols: sym=value.split('=',1) if len(sym)==1: @@ -1944,7 +2159,7 @@ def writelisting(line): dumpused = False autoexecpage = 0 autoexecorigin = 0 - + macroindex = 0 assembler_pass(p, inputfile) check_lastpage() @@ -1960,6 +2175,10 @@ def writelisting(line): print(item[1]) sys.exit(1) + if len(structstack) > 0: + print("Error: Mismatched STRUCT and ENDS statements, too many STRUCT") + sys.exit(1) + printsymbols = {} for symreg in listsymbols: # add to printsymbols any pair from symboltable whose key matches symreg