#!/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()