65816-llvm-mos/scripts/genToolbox.py
2026-06-25 02:41:37 -05:00

598 lines
23 KiB
Python

#!/usr/bin/env python3
# genToolbox.py — generate IIgs toolbox wrappers from NList.Data.
#
# Reads Dave Lyons' NiftyList database (`tools/nlist/NList.Data.txt`),
# which catalogues every IIgs toolbox call and GS/OS call with tool
# number, parameter sizes, and return-value size. Emits two outputs:
# - C header with `static inline` wrappers using clang inline-asm
# - .s file with extern wrapper bodies for multi-arg routines that
# can't fit in inline asm (our backend's constraints don't take
# memory operands).
#
# NList line format (see tools/nlist/README.md):
# TTTT [NS:]FuncName([N:]arg[/sz], ...)[:ret[/sz]]
#
# Tool number convention: TTTT loaded into X; low byte = tool-set,
# high byte = function code. Dispatcher: JSL $E10000 for normal
# toolbox; JSL $E100A8 for GS/OS (entries prefixed `GS/OS:`).
#
# Calling convention: C ABI passes arg0 (i16) in A, arg0 (i32) in A:X,
# arg1+ on stack RTL. Each generated wrapper re-pushes args in toolbox
# (Pascal-style L-to-R) order, with result space first (if any), then
# JSL the dispatcher, then pops result.
#
# Output files are written to the runtime tree.
import re
import sys
from pathlib import Path
NLIST_PATH = Path("/home/scott/claude/llvm816/tools/nlist/NList.Data.txt")
OUT_HEADER = Path("/home/scott/claude/llvm816/runtime/include/iigs/toolbox.h")
OUT_ASM = Path("/home/scott/claude/llvm816/runtime/src/iigsToolbox.s")
# Type table: (size in bytes, c-type)
TYPE_MAP = {
"void": (0, "void"),
"Word": (2, "unsigned short"),
"Boolean": (2, "unsigned short"),
"Integer": (2, "short"),
"Char": (2, "char"), # widened on stack
"Byte": (2, "unsigned char"),
"LongWord": (4, "unsigned long"),
"Long": (4, "long"),
"Handle": (4, "void *"), # 4-byte handle
"Pointer": (4, "void *"), # 4-byte pointer (toolbox semantics)
"Ref": (4, "void *"),
"Ptr": (4, "void *"),
"ResType": (4, "unsigned long"),
"Real": (4, "float"),
"Double": (8, "double"),
"Comp": (8, "long long"),
"Extended": (10, "long double"),
"GrafPortPtr":(4, "void *"),
"WindowPtr": (4, "void *"),
"MenuHandle": (4, "void *"),
"CtlRecHndl": (4, "void *"),
"DialogPtr": (4, "void *"),
"RgnHandle": (4, "void *"),
"PrPort": (4, "void *"),
"PrRecHndl": (4, "void *"),
"PicHandle": (4, "void *"),
"WindRecHndl":(4, "void *"),
}
# Tool number → tool-set name mapping (low byte of toolNumber)
TOOLSET_NAME = {
0x01: "ToolLocator",
0x02: "MemoryManager",
0x03: "MiscTools",
0x04: "QuickDraw",
0x05: "DeskManager",
0x06: "EventManager",
0x07: "Scheduler",
0x08: "SoundManager",
0x09: "AppleDeskBus",
0x0A: "SANE",
0x0B: "IntegerMath",
0x0C: "TextTools",
0x0E: "WindowManager",
0x0F: "MenuManager",
0x10: "ControlManager",
0x11: "Loader",
0x12: "QDAuxiliary",
0x13: "PrintManager",
0x14: "LineEdit",
0x15: "DialogManager",
0x16: "ScrapManager",
0x17: "StandardFile",
0x18: "DiskUtil",
0x19: "NoteSynth",
0x1A: "NoteSequencer",
0x1B: "FontManager",
0x1C: "ListManager",
0x1D: "ACETools",
0x1E: "ResourceManager",
0x1F: "MIDITools",
0x20: "VideoOverlay",
0x21: "Teletext",
0x22: "TextEdit",
0x23: "MediaControl",
0x32: "MediaControl2",
}
# NList default sizes. Pointer (`@name`) is 4 bytes; an explicit `/N`
# suffix overrides. Also: a handle abbreviation (bare `H`, or any
# trailing capital `H` on a short name like `StateH`, `MsgH`) is 4
# bytes — that's NList's convention for `:H`-style return values
# (NewHandle, GetMsgHandle, SaveTextState all use it). Everything
# else defaults to 2 bytes (Word).
_HANDLE_SHORTHAND_RE = re.compile(r"^[A-Z][a-zA-Z]*H$")
def parseSize(rawName):
"""Strip a trailing /N size suffix and return (bareName, size, isPtr).
isPtr is True when the source spelling implied a pointer/handle (the
`@` prefix or an `H`-shorthand for handle); the generator then picks
`void *` rather than `unsigned long` as the C type.
"""
rawName = rawName.strip()
if not rawName:
return ("", 0, False)
sz = None
m = re.search(r"/(\d+)$", rawName)
if m:
sz = int(m.group(1))
rawName = rawName[: m.start()].strip()
isPtr = rawName.startswith("@")
if isPtr:
rawName = rawName[1:]
isHandle = rawName == "H" or bool(_HANDLE_SHORTHAND_RE.match(rawName))
if sz is None:
if isPtr:
sz = 4
elif isHandle:
sz = 4
else:
sz = 2
return (rawName, sz, isPtr or (sz == 4 and isHandle))
_ENTRY_RE = re.compile(
r"^([0-9A-Fa-f]{4})\s+"
r"(?:(GS/OS|P8|P16|Shell|\w+:[\w/]+):)?" # optional namespace prefix(es)
r"(\w+)\s*" # function name
r"\(([^)]*)\)" # arg list
r"(?::([^,\s]+(?:,[^,\s]+)*))?\s*$" # optional :ret(,ret2,...)
)
def parseLine(line):
"""Parse an NList.Data line into a toolbox decl dict.
Returns None for comments, section dividers, unfilled stubs, or
non-toolbox namespaces (P8 / P16 / Shell).
"""
line = line.rstrip()
if not line:
return None
# Comment forms: `* ...`, `fff9 | ...`, `TTTT === name ===`.
head4 = line[:4]
rest = line[4:].lstrip() if len(line) > 4 else ""
if not re.fullmatch(r"[0-9A-Fa-f]{4}", head4):
return None
if rest.startswith("|") or rest.startswith("==="):
return None
if rest.startswith("(...)") or "(...)" in rest:
return None
# Filter by namespace. Standard toolbox calls have no prefix.
# GS/OS calls have `GS/OS:` and dispatch via $E100A8. Skip P8/P16/
# Shell — they use other dispatchers we don't currently expose.
dispatcher = "dispatcher" # → JSL $E10000
nsMatch = re.match(r"^([A-Za-z][\w/]*):", rest)
if nsMatch:
ns = nsMatch.group(1)
if ns == "GS/OS":
dispatcher = "_CallBackVector" # → JSL $E100A8
rest = rest[nsMatch.end():]
elif ns in ("P8", "P16", "Shell"):
return None
else:
# Sub-prefix like `P8:ATLK:...` — already filtered above by
# the outer P8; anything else unknown gets skipped.
return None
m = re.match(r"^(\w+)\s*\(([^)]*)\)(.*)$", rest)
if not m:
return None
name, argsRaw, tail = m.group(1, 2, 3)
toolNum = int(head4, 16)
# Return type lives after `:`. Strip optional leading whitespace.
# Multi-value returns (e.g. `:Created,Type`) sum sizes; if any
# element looked pointer-ish, the overall return type is `void *`
# (handle-style returns are the dominant 4-byte case).
retSize = 0
retIsPtr = False
tail = tail.strip()
if tail.startswith(":"):
retSpec = tail[1:].strip()
for r in retSpec.split(","):
_, sz, isPtr = parseSize(r)
retSize += sz
retIsPtr = retIsPtr or isPtr
# Arg list: strip optional leading arg-count `N:` (e.g. `1-7:` or
# `4or84:`) and split on commas.
argsRaw = argsRaw.strip()
if ":" in argsRaw:
head, sep, body = argsRaw.partition(":")
if re.fullmatch(r"[\d\-or ]+", head.strip()):
argsRaw = body.strip()
argSpec = []
if argsRaw and argsRaw != "void":
for a in argsRaw.split(","):
_, sz, isPtr = parseSize(a)
if sz == 0:
return None
argSpec.append((sz, isPtr))
return {
"name": name,
"argSpec": argSpec, # list of (size, isPtr)
"retSize": retSize,
"retIsPtr": retIsPtr,
"tool": toolNum,
"dispatcher": dispatcher,
}
def typeInfo(t):
"""Return (size_bytes, c_type) for ORCA type, or None if unsupported."""
if t in TYPE_MAP:
return TYPE_MAP[t]
# Default: assume 4 bytes / void* (pointer-like)
return (4, "void *")
# Map an (NList byte size, isPtr) tuple to a (stack-size, c-type) tuple.
# NList only carries sizes — no semantic type names — so the generator
# picks a generic C type. isPtr=True (NList `@name` prefix or `H`
# shorthand) selects `void *` for 4-byte slots; otherwise 4-byte
# non-pointer args become `unsigned long` so the common
# `NewHandle(0x900UL, ...)` pattern compiles without a pointer cast.
def sizeInfo(sz, isPtr=False):
"""Return (effective-size, c-type) for an NList byte size."""
if sz == 0:
return (0, "void")
if sz == 1:
return (2, "unsigned char") # widened on stack
if sz == 2:
return (2, "unsigned short")
if sz == 3:
return (4, "unsigned long") # rare; widened to 4
if sz == 4:
return (4, "void *" if isPtr else "unsigned long")
if sz == 8:
return (8, "long long")
if sz == 10:
return (10, "long double")
return (4, "void *")
def emit(decls):
"""Generate C header and .s file from parsed decls."""
cLines = [
"// AUTOGENERATED by scripts/genToolbox.py from NList.Data.",
"// DO NOT EDIT by hand — regenerate to update.",
"// Source: tools/nlist/NList.Data.txt (Dave Lyons' NiftyList database).",
"//",
"// Complete IIgs toolbox + GS/OS surface (~2000 routines).",
"// Names match Apple's IIgs Toolbox Reference (TLStartUp,",
"// MMStartUp, NewWindow, SysBeep, etc.). Multi-arg wrappers",
"// (those whose stub body uses memory operands) live in",
"// runtime/src/iigsToolbox.s; zero-arg / single-arg simple",
"// ones are inlined here.",
"",
"#ifndef IIGS_TOOLBOX_H",
"#define IIGS_TOOLBOX_H",
"",
"#ifdef __cplusplus",
'extern "C" {',
"#endif",
"",
"// IigsCursorT - opaque handle for the QD CursorRecord layout.",
"// Apple/ORCA `Cursor` is variable-length (cursorData[] and",
"// cursorMask[] sized by cursorHeight/cursorWidth), so we expose",
"// it as an opaque blob. Use iigs/cursor.h helpers to push/pop",
"// stock ROM shapes (arrow, busy) without poking the fields by",
"// hand. Pointer-sized; pass to SetCursor() / GetCursorAdr().",
"typedef struct IigsCursorT IigsCursorT;",
"",
]
sLines = [
"; AUTOGENERATED by scripts/genToolbox.py from NList.Data.",
"; DO NOT EDIT by hand — regenerate to update.",
"; Source: tools/nlist/NList.Data.txt (Dave Lyons' NiftyList database).",
";",
"; IIgs toolbox multi-arg wrappers.",
";",
"; C ABI: arg0 (i16) in A, arg0 (i32) in A:X, arg1+ on stack (4,S etc.).",
"; Each wrapper re-pushes args in toolbox (Pascal-style L-to-R) order,",
"; preceded by result space if non-void return, then JSL $E10000",
"; (or $E100A8 for GS/OS). Pops result if non-void.",
";",
"; Tool number: high byte = function, low byte = tool set.",
"",
"\t.text",
"",
]
seenNames = set()
inlineCount = 0
asmCount = 0
skipped = []
for d in decls:
name = d["name"]
if name in seenNames:
continue # duplicate (e.g. P16 / GS/OS overload of same name).
seenNames.add(name)
tool = d["tool"]
dispatcher = d["dispatcher"]
# Translate NList byte sizes into (effective-size, c-type) tuples
# the emit logic expects. argTypes is rebuilt for the diagnostic
# comment only. Pointer detection follows NList's signal: an
# `@name` prefix or `H`/`*H`-shorthand → `void *`; an explicit
# `/4` without those markers → `unsigned long`. That matches
# how the NList catalog distinguishes integer Long/LongWord
# values (TickCount → :Ticks/4) from pointers/handles
# (NewHandle → :H, GetCursorAdr → :@Curs).
retSize, retC = sizeInfo(d["retSize"], d["retIsPtr"])
argInfo = [sizeInfo(sz, isPtr) for (sz, isPtr) in d["argSpec"]]
argTypes = [
"Pointer" if ai[1] == "void *" else
"LongWord" if ai[0] == 4 else
"Word" if ai[0] == 2 else
f"{ai[0]}B"
for ai in argInfo
]
retType = (
"Pointer" if retC == "void *" else
"LongWord" if retSize == 4 else
"Word" if retSize == 2 else
"void" if retSize == 0 else
f"{retSize}B"
)
# Build C-style arg list.
cArgs = ", ".join(f"{ai[1]} a{i}" for i, ai in enumerate(argInfo))
if not cArgs:
cArgs = "void"
cDecl = f"{retC} {name}({cArgs});"
# Decide inline vs asm.
# Simple cases that can be inlined: no args (with or without 16-bit
# return), or single 16-bit arg with void return / 16-bit return.
canInline = False
if not argInfo and retSize in (0, 2):
canInline = True
elif (
len(argInfo) == 1
and argInfo[0][0] == 2
and retSize in (0, 2)
):
canInline = True
dispAddr = "0xe10000" if dispatcher == "dispatcher" else "0xe100a8"
if canInline:
# Generate inline asm body.
if not argInfo:
if retSize == 0:
body = (
f' __asm__ volatile (\n'
f' "ldx #0x{tool:04X}\\n"\n'
f' "jsl {dispAddr}\\n"\n'
f' :\n'
f' :\n'
f' : "a", "x", "y", "memory"\n'
f' );\n'
)
else: # 16-bit return
body = (
f' {retC} _r;\n'
f' __asm__ volatile (\n'
f' "pha\\n" // result space\n'
f' "ldx #0x{tool:04X}\\n"\n'
f' "jsl {dispAddr}\\n"\n'
f' "pla\\n"\n'
f' : "=a"(_r)\n'
f' :\n'
f' : "x", "y", "memory"\n'
f' );\n'
f' return _r;\n'
)
else: # 1-arg
if retSize == 0:
body = (
f' __asm__ volatile (\n'
f' "pha\\n" // arg0\n'
f' "ldx #0x{tool:04X}\\n"\n'
f' "jsl {dispAddr}\\n"\n'
f' :\n'
f' : "a"(a0)\n'
f' : "x", "y", "memory"\n'
f' );\n'
)
else:
body = (
f' {retC} _r;\n'
f' __asm__ volatile (\n'
f' "pha\\n" // result space\n'
f' "pha\\n" // arg0\n'
f' "ldx #0x{tool:04X}\\n"\n'
f' "jsl {dispAddr}\\n"\n'
f' "pla\\n"\n'
f' : "=a"(_r)\n'
f' : "a"(a0)\n'
f' : "x", "y", "memory"\n'
f' );\n'
f' return _r;\n'
)
cLines.append(f"// tool 0x{tool:04X} set 0x{tool & 0xFF:02X} ({TOOLSET_NAME.get(tool & 0xFF, '?')})")
cLines.append(f"static inline {retC} {name}({cArgs}) {{")
cLines.append(body.rstrip())
cLines.append("}")
cLines.append("")
inlineCount += 1
else:
# Extern decl in header, asm body in .s file.
cLines.append(f"extern {retC} {name}({cArgs}); // 0x{tool:04X}")
# Generate asm body.
sLines.append(f"; {name}({', '.join(argTypes) or 'void'}) -> {retType}")
sLines.append(f"; tool 0x{tool:04X}, set 0x{tool & 0xFF:02X} ({TOOLSET_NAME.get(tool & 0xFF, '?')})")
# One section per wrapper so link816's --gc-sections can
# drop the ~880 wrappers the user's demo doesn't reference.
# Without this the entire 45 KB toolbox.s gets linked into
# every binary.
sLines.append(f"\t.section .text.{name},\"ax\"")
sLines.append(f"\t.globl {name}")
sLines.append(f"{name}:")
# Compute total stack arg bytes (excluding arg0 which is in regs).
# Determine where each arg starts on the caller's stack.
# arg0 is in A (or A:X for i32-first-arg).
firstArgIs32 = argInfo and argInfo[0][0] == 4
stackArgStart = 4 # offset to first stack-passed arg after JSL retaddr
# Stash arg0 if any args exist. i16: 'sta scratch'. i32: 'sta
# scratch; stx scratch+2'. void-arg functions skip this entirely
# — emitting a phantom `pha` for arg0 corrupts the dispatcher's
# stack frame (caught when GetTick crashed under Loader).
#
# No PBR-bank workaround here anymore: the backend now emits a
# proper R_W65816_BANK16 relocation on the high half of &symbol
# so the OMF Loader patches it with our actual placement bank
# at load time. See feedback_pointer_constant_bank_zero.md.
scratchDP = 0xE0 # libcall scratch zone
if argInfo:
sLines.append(f"\t; --- stash arg0 (in A{'/X' if firstArgIs32 else ''}) ---")
sLines.append(f"\tsta 0x{scratchDP:02X}")
if firstArgIs32:
sLines.append(f"\tstx 0x{scratchDP + 2:02X}")
# Push result space (toolbox order: result is highest on stack).
if retSize > 0:
sLines.append(f"\t; --- result space ({retSize} bytes) ---")
for _ in range((retSize + 1) // 2):
sLines.append(f"\tpea 0")
# Push args in Pascal order (L-to-R). For multi-byte values
# the toolbox expects HIGH word first then LOW word (matches
# ORCA-C's PushLong macro: `pea ^addr ; pea addr`). After
# the two pushes, memory at (S+1..S+4) is in little-endian
# long-value order (byte 0 low at lowest address) so a
# `pull long` / 32-bit indirect read sees the value
# correctly. Earlier "lo first then hi" emission left the
# high word at the wrong stack offset and the toolbox saw
# garbage (NewHandle returned NULL for a $900-byte request).
# Tracker: how many bytes have we pushed beyond the original
# caller-stack so all stack-arg loads need to add (pushed) to
# their original offset.
pushedBytes = (retSize + 1) // 2 * 2 # result space rounded up to word
# arg0 first (if any args).
if argInfo:
sLines.append(f"\t; --- arg0 ---")
if firstArgIs32:
# HIGH word first (Long convention).
sLines.append(f"\tlda 0x{scratchDP + 2:02X}")
sLines.append(f"\tpha")
pushedBytes += 2
sLines.append(f"\tlda 0x{scratchDP:02X}")
sLines.append(f"\tpha")
pushedBytes += 2
# arg1, arg2, ... — each loaded from caller stack at original
# offset + pushedBytes.
stackArgOffset = stackArgStart # original offset of next arg
for i, ai in enumerate(argInfo[1:], start=1):
size = ai[0]
sLines.append(f"\t; --- arg{i} ({argTypes[i]}, {size}B) ---")
# i16 / 16-bit-on-stack args: 1 word, push lo
# i32 / 32-bit-on-stack: 2 words, push lo then hi
# We're loading from caller's pre-push stack. Original
# offsets: arg1 at 4, arg2 at 4+size(arg1), ...
# But each load from `(orig+pushed),s` accounts for pushes.
if size <= 2:
sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
sLines.append(f"\tpha")
pushedBytes += 2
stackArgOffset += 2
elif size == 4:
# Long: push HIGH word first, then LOW word (ORCA
# PushLong convention; same fix as the arg0 Long path).
# HI is at caller offset (stackArgOffset + 2); LO at
# stackArgOffset. After the HI PHA, both halves'
# current stack-rel offsets shift +2.
sLines.append(f"\tlda {stackArgOffset + pushedBytes + 2}, s")
sLines.append(f"\tpha")
pushedBytes += 2
# LO at original offset stackArgOffset; current = +pushedBytes.
sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
sLines.append(f"\tpha")
pushedBytes += 2
stackArgOffset += 4
else:
# Bigger types (8-byte Comp, 10-byte Extended) — push word by word.
nWords = (size + 1) // 2
for _ in range(nWords):
sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
sLines.append(f"\tpha")
pushedBytes += 2
stackArgOffset += size
# Dispatch.
sLines.append(f"\tldx #0x{tool:04X}")
sLines.append(f"\tjsl {dispAddr}")
# Pop result.
if retSize == 2:
sLines.append(f"\tpla ; result -> A")
elif retSize == 4:
sLines.append(f"\tpla ; result lo -> A")
sLines.append(f"\tplx ; result hi -> X")
elif retSize > 4:
# Larger results: pop into scratch then load A/X for return.
# Treat as "best effort" — caller should not expect a real
# return value beyond what fits in A:X.
nWords = (retSize + 1) // 2
for _ in range(nWords):
sLines.append(f"\tpla")
sLines.append(f"\trtl")
sLines.append("")
asmCount += 1
cLines.append("")
cLines.append("#ifdef __cplusplus")
cLines.append("}")
cLines.append("#endif")
cLines.append("")
cLines.append("#endif // IIGS_TOOLBOX_H")
OUT_HEADER.write_text("\n".join(cLines))
OUT_ASM.write_text("\n".join(sLines))
print(f"wrote {OUT_HEADER}: {inlineCount} inline + {asmCount} extern decls")
print(f"wrote {OUT_ASM}: {asmCount} bodies")
if skipped:
print(f"skipped {len(skipped)} routines (unhandled types):")
for n, why in skipped[:5]:
print(f" {n}: {why}")
def main():
decls = []
if not NLIST_PATH.exists():
print(f"error: {NLIST_PATH} not found", file=sys.stderr)
sys.exit(1)
for line in NLIST_PATH.read_text().splitlines():
d = parseLine(line)
if d:
decls.append(d)
print(f"parsed {len(decls)} declarations from {NLIST_PATH}")
emit(decls)
if __name__ == "__main__":
main()