From f8dc48c2cecc4e9a7c8ca4e1fc6f7f5e248ac937 Mon Sep 17 00:00:00 2001 From: adrianpbrown Date: Sun, 20 Jul 2025 20:37:51 +0100 Subject: [PATCH 1/4] Update pyz80.py Added the ability to use STRUCT/RS/ENDS --- pyz80.py | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/pyz80.py b/pyz80.py index da795a3..f455151 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)") @@ -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,6 +1622,76 @@ 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 assemble_instruction(p, line): match = re.match(r'^(\w+)(.*)', line) if not match: @@ -1628,7 +1700,15 @@ def assemble_instruction(p, line): inst = match.group(1).upper() args = match.group(2).strip() - if (ifstate < 2) or inst in ('IF', 'ELSE', 'ENDIF'): + # 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") + + 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') : functioncall = 'op_'+inst+'(p,args)' if PYTHONERRORS: return eval(functioncall) @@ -1638,7 +1718,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 @@ -1896,6 +1976,8 @@ def writelisting(line): forstack=[] ifstack = [] ifstate = 0 + structstack = [] + structstate = 0 for value in predefsymbols: sym=value.split('=',1) @@ -1960,6 +2042,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 From ec100945b8ca8672135547c48410e9d3c700a170 Mon Sep 17 00:00:00 2001 From: adrianpbrown Date: Tue, 22 Jul 2025 19:42:59 +0100 Subject: [PATCH 2/4] Added MACRO Support Added MACRO support and update readme file. --- README.md | 85 ++++++++++++++++++++++++++++++ pyz80.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 231 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 22d3c4f..ccda208 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,14 @@ INFORMATION PROVIDED BY THE PROGRAM AND THE DOCUMENTATION. **Release History** +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 f455151..ee68157 100755 --- a/pyz80.py +++ b/pyz80.py @@ -355,10 +355,10 @@ 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) - + print("SYMBOL:" + sym + " : " +str(value) + " = " + str(explicit_currentfile)) symboltable[sym] = value if sym != symorig: symbolcase[sym] = symorig @@ -1692,14 +1692,139 @@ def op_ENDS(p,opargs): 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 + + print("MACROLINE:" + processed_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") + + print("MACROS: " + str(macrostate)) + + 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 = "" - inst = match.group(1).upper() - args = match.group(2).strip() + # 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") @@ -1710,6 +1835,7 @@ def assemble_instruction(p, line): if (ifstate < 2) or inst in ('IF', 'ELSE', 'ENDIF') : functioncall = 'op_'+inst+'(p,args)' + if PYTHONERRORS: return eval(functioncall) else: @@ -1748,7 +1874,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): @@ -1809,7 +1943,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: @@ -1846,7 +1980,7 @@ def assembler_pass(p, inputfile): outputfile = '' objectfile = '' -PYTHONERRORS = False +PYTHONERRORS = True ZIP = True CASE = False NOBODMAS = False @@ -1978,7 +2112,10 @@ def writelisting(line): ifstate = 0 structstack = [] structstate = 0 - + macros = [] + macrostate = 0 + currentmacro = None + for value in predefsymbols: sym=value.split('=',1) if len(sym)==1: @@ -2026,7 +2163,7 @@ def writelisting(line): dumpused = False autoexecpage = 0 autoexecorigin = 0 - + macroindex = 0 assembler_pass(p, inputfile) check_lastpage() From eefbce5661d23fd521a540a90359b841a5831ec6 Mon Sep 17 00:00:00 2001 From: adrianpbrown Date: Tue, 22 Jul 2025 19:49:06 +0100 Subject: [PATCH 3/4] Update README.md --- README.md | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ccda208..44e8a35 100644 --- a/README.md +++ b/README.md @@ -455,36 +455,36 @@ 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 +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** From 5fbb4188bc1e7b09382d0c1355ffa4c5b6ebe018 Mon Sep 17 00:00:00 2001 From: adrianpbrown Date: Tue, 22 Jul 2025 20:19:20 +0100 Subject: [PATCH 4/4] Removed Debug printing --- README.md | 4 ++++ pyz80.py | 8 ++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44e8a35..e0e6300 100644 --- a/README.md +++ b/README.md @@ -566,6 +566,10 @@ 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) diff --git a/pyz80.py b/pyz80.py index ee68157..18994ec 100755 --- a/pyz80.py +++ b/pyz80.py @@ -358,7 +358,7 @@ def set_symbol(sym, value, explicit_currentfile=None, is_label=False): if sym[0]=='@': sym = sym + '@' + file_and_stack(explicit_currentfile=explicit_currentfile) - print("SYMBOL:" + sym + " : " +str(value) + " = " + str(explicit_currentfile)) + symboltable[sym] = value if sym != symorig: symbolcase[sym] = symorig @@ -1733,8 +1733,6 @@ def op_HandleMacro(p,opargs) : else : processed_line = line - print("MACROLINE:" + processed_line) - lines.append(processed_line) # Track how many times for local label reasons @@ -1764,8 +1762,6 @@ def op_MACRO(p,opargs): else: warning("MACRO without symbol name") - print("MACROS: " + str(macrostate)) - return 0 def op_ENDM(p,opargs) : @@ -1980,7 +1976,7 @@ def do_pass(p, wholefile, this_currentfilename) : outputfile = '' objectfile = '' -PYTHONERRORS = True +PYTHONERRORS = False ZIP = True CASE = False NOBODMAS = False