Compare commits
2
Commits
9013cff6b1
...
b8a3647ae5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8a3647ae5 | ||
|
|
adce930a17 |
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
z80fmt - column formatter for Z80 assembly source (z88dk z80asm flavour).
|
||||
|
||||
Splits each line into label | mnemonic | operands | ;comment and pads the
|
||||
first three into fixed-width columns with spaces. String and character
|
||||
literals are parsed properly, so semicolons inside DEFM data are never
|
||||
mistaken for comments, and EX AF,AF' does not open a quote.
|
||||
|
||||
Usage:
|
||||
z80fmt.py file.z80 [more.z80 ...] # write formatted source to stdout
|
||||
z80fmt.py -i file.z80 # rewrite in place
|
||||
z80fmt.py --check *.z80 # exit 1 if any file needs formatting
|
||||
cat file.z80 | z80fmt.py # filter mode
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
MNEMONICS = {
|
||||
"adc", "add", "and", "bit", "call", "ccf", "cp", "cpd", "cpdr", "cpi",
|
||||
"cpir", "cpl", "daa", "dec", "di", "djnz", "ei", "ex", "exx", "halt",
|
||||
"im", "in", "inc", "ind", "indr", "ini", "inir", "jp", "jr", "ld",
|
||||
"ldd", "lddr", "ldi", "ldir", "neg", "nop", "or", "otdr", "otir", "out",
|
||||
"outd", "outi", "pop", "push", "res", "ret", "reti", "retn", "rl",
|
||||
"rla", "rlc", "rlca", "rld", "rr", "rra", "rrc", "rrca", "rrd", "rst",
|
||||
"sbc", "scf", "set", "sla", "sll", "sli", "sra", "srl", "sub", "xor",
|
||||
# Z180 extras
|
||||
"in0", "out0", "mlt", "tst", "tstio", "slp", "otim", "otdm", "otimr",
|
||||
"otdmr",
|
||||
}
|
||||
|
||||
DIRECTIVES = {
|
||||
"org", "defb", "defw", "defm", "defs", "defc", "equ", "include",
|
||||
"binary", "incbin", "public", "extern", "global", "module", "section",
|
||||
"align", "defvars", "if", "ifdef", "ifndef", "else", "elif", "endif",
|
||||
"end", "db", "dw", "ds", "dm", "dc", "byte", "word", "ascii", "asciz",
|
||||
"asciiz", "macro", "endm", "rept", "endr", "local", "title", "line",
|
||||
"defgroup", "defarray",
|
||||
}
|
||||
|
||||
|
||||
def split_comment(line):
|
||||
"""Return (code, comment). Quote-aware so ';' inside literals is kept."""
|
||||
quote = None
|
||||
i, n = 0, len(line)
|
||||
while i < n:
|
||||
c = line[i]
|
||||
if quote:
|
||||
if c == "\\" and quote == '"' and i + 1 < n:
|
||||
i += 2
|
||||
continue
|
||||
if c == quote:
|
||||
quote = None
|
||||
elif c == '"':
|
||||
quote = '"'
|
||||
elif c == "'":
|
||||
# A quote right after an identifier char is a register prime
|
||||
# (EX AF,AF' / HL'), not the start of a character literal.
|
||||
prev = line[i - 1] if i else ""
|
||||
if not (prev.isalnum() or prev == "_"):
|
||||
quote = "'"
|
||||
elif c == ";":
|
||||
return line[:i], line[i:]
|
||||
i += 1
|
||||
return line, ""
|
||||
|
||||
|
||||
def is_opcode(token):
|
||||
base = token.rstrip(":").lstrip(".").lower()
|
||||
return base in MNEMONICS or base in DIRECTIVES
|
||||
|
||||
|
||||
def split_code(code):
|
||||
"""Return (label, mnemonic, operands) from the comment-stripped code."""
|
||||
if not code.strip():
|
||||
return "", "", ""
|
||||
|
||||
label = ""
|
||||
if code[0] not in " \t":
|
||||
first = code.split(None, 1)[0]
|
||||
rest = code.split(None, 1)[1] if len(code.split(None, 1)) > 1 else ""
|
||||
if first.endswith(":") or not is_opcode(first):
|
||||
label, code = first, rest
|
||||
|
||||
rest = code.strip()
|
||||
if not rest:
|
||||
return label, "", ""
|
||||
parts = rest.split(None, 1)
|
||||
mnemonic = parts[0]
|
||||
operands = parts[1].strip() if len(parts) > 1 else ""
|
||||
return label, mnemonic, operands
|
||||
|
||||
|
||||
def normalize_commas(operands, sep):
|
||||
"""Rewrite comma spacing outside of literals."""
|
||||
out, quote, i, n = [], None, 0, len(operands)
|
||||
while i < n:
|
||||
c = operands[i]
|
||||
if quote:
|
||||
out.append(c)
|
||||
if c == "\\" and quote == '"' and i + 1 < n:
|
||||
out.append(operands[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
if c == quote:
|
||||
quote = None
|
||||
elif c == '"':
|
||||
quote = '"'
|
||||
out.append(c)
|
||||
elif c == "'":
|
||||
prev = operands[i - 1] if i else ""
|
||||
if not (prev.isalnum() or prev == "_"):
|
||||
quote = "'"
|
||||
out.append(c)
|
||||
elif c == ",":
|
||||
while out and out[-1] == " ":
|
||||
out.pop()
|
||||
out.append("," + sep)
|
||||
j = i + 1
|
||||
while j < n and operands[j] in " \t":
|
||||
j += 1
|
||||
i = j
|
||||
continue
|
||||
else:
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out).rstrip()
|
||||
|
||||
|
||||
def pad(text, width, gap=1):
|
||||
return text.ljust(width) if len(text) < width else text + " " * gap
|
||||
|
||||
|
||||
def format_line(line, opt):
|
||||
raw = line.rstrip("\n").rstrip()
|
||||
if not raw.strip():
|
||||
return [""]
|
||||
|
||||
stripped = raw.lstrip()
|
||||
|
||||
# Preprocessor lines pass through untouched.
|
||||
if stripped.startswith("#"):
|
||||
return [stripped]
|
||||
|
||||
# Whole-line comments: keep column-0 banners at column 0, indent the
|
||||
# rest to the mnemonic column.
|
||||
if stripped.startswith(";"):
|
||||
if raw[0] in " \t":
|
||||
return [" " * opt.label_width + stripped]
|
||||
return [stripped]
|
||||
|
||||
code, comment = split_comment(raw)
|
||||
label, mnemonic, operands = split_code(code)
|
||||
|
||||
if opt.case == "upper":
|
||||
mnemonic = mnemonic.upper()
|
||||
elif opt.case == "lower":
|
||||
mnemonic = mnemonic.lower()
|
||||
|
||||
if opt.comma_space and operands:
|
||||
operands = normalize_commas(operands, " ")
|
||||
elif operands:
|
||||
operands = normalize_commas(operands, "")
|
||||
|
||||
lines = []
|
||||
has_code = bool(mnemonic or operands or comment)
|
||||
|
||||
if label and has_code and opt.wrap_long_labels and len(label) >= opt.label_width:
|
||||
lines.append(label)
|
||||
label = ""
|
||||
|
||||
if not has_code:
|
||||
lines.append(label)
|
||||
return lines
|
||||
|
||||
out = pad(label, opt.label_width) if label else " " * opt.label_width
|
||||
out += pad(mnemonic, opt.mnemonic_width) if mnemonic else " " * opt.mnemonic_width
|
||||
|
||||
if comment:
|
||||
out += pad(operands, opt.operand_width) if operands else " " * opt.operand_width
|
||||
out += comment
|
||||
else:
|
||||
out += operands
|
||||
|
||||
lines.append(out.rstrip())
|
||||
return lines
|
||||
|
||||
|
||||
def format_text(text, opt):
|
||||
out = []
|
||||
for line in text.splitlines():
|
||||
out.extend(format_line(line, opt))
|
||||
return "\n".join(out) + ("\n" if text.endswith("\n") or text else "")
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Format Z80 assembly into columns.")
|
||||
p.add_argument("files", nargs="*", help="source files (default: stdin)")
|
||||
p.add_argument("-i", "--in-place", action="store_true")
|
||||
p.add_argument("--check", action="store_true",
|
||||
help="exit 1 if any file would change; write nothing")
|
||||
p.add_argument("--label-width", type=int, default=16)
|
||||
p.add_argument("--mnemonic-width", type=int, default=8)
|
||||
p.add_argument("--operand-width", type=int, default=24)
|
||||
p.add_argument("--comma-space", action="store_true",
|
||||
help="put one space after operand commas (default: none)")
|
||||
p.add_argument("--case", choices=["keep", "upper", "lower"], default="keep",
|
||||
help="case of mnemonics only; symbols are left alone")
|
||||
p.add_argument("--wrap-long-labels", action="store_true",
|
||||
help="put over-long labels on their own line")
|
||||
opt = p.parse_args()
|
||||
|
||||
if not opt.files:
|
||||
sys.stdout.write(format_text(sys.stdin.read(), opt))
|
||||
return 0
|
||||
|
||||
changed = False
|
||||
for path in opt.files:
|
||||
with open(path, "r", encoding="utf-8", errors="surrogateescape") as fh:
|
||||
original = fh.read()
|
||||
formatted = format_text(original, opt)
|
||||
if formatted != original:
|
||||
changed = True
|
||||
if opt.check:
|
||||
print(f"would reformat: {path}", file=sys.stderr)
|
||||
if opt.check:
|
||||
continue
|
||||
if opt.in_place:
|
||||
if formatted != original:
|
||||
with open(path, "w", encoding="utf-8",
|
||||
errors="surrogateescape") as fh:
|
||||
fh.write(formatted)
|
||||
else:
|
||||
sys.stdout.write(formatted)
|
||||
|
||||
return 1 if (opt.check and changed) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user