#!/usr/bin/env python3 # genToolbox.py — generate IIgs toolbox wrappers from ORCA-C headers. # # Reads ORCA's extern declarations of the form: # extern pascal RetType FuncName(ArgType, ArgType) inline(0xNNTT, dispatcher); # and 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). # # Tool number convention: 0xNNTT high byte = function, low byte = tool set # Dispatcher: JSL $E10000 for normal toolbox; JSL $E100A8 for GS/OS # (only the ProDOS-16 / GS/OS calls use _CallBackVector). # # Calling convention conversion: ORCA uses Pascal (args pushed L-to-R), # our C ABI passes arg0 in A and arg1+ on stack RTL. Each generated # wrapper re-pushes args in toolbox order. # # Type widths (matching ORCA): # Word, Boolean, Integer, Char, Byte = 2 bytes (16-bit) # LongWord, Long, Handle, Pointer = 4 bytes (32-bit) # Ptr, Ref, ResType = 4 bytes # (Pointer is 4 bytes in ORCA -- it's a far/24-bit pointer. Our backend # uses 16-bit pointers, but the toolbox expects 32-bit on the stack; # we extend with a zero high word.) # # Output files are written to the runtime tree. import re import sys from pathlib import Path ORCA_DIR = Path("/tmp/orca-headers") 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", } def parseLine(line): """Parse `extern pascal RetType Name(args) inline(0xNNTT, dispatcher);` Returns dict or None if not a toolbox decl. """ m = re.match( r'^\s*extern\s+pascal\s+(\w+)\s+(\w+)\s*\((.*?)\)\s+inline\(0x([0-9A-Fa-f]+)\s*,\s*(\w+)\)\s*;', line, ) if not m: return None retType, name, args, toolHex, dispatcher = m.group(1, 2, 3, 4, 5) toolNum = int(toolHex, 16) # Parse arg types (just the types, no names since ORCA omits them). args = args.strip() argTypes = [] if args and args != "void": for a in args.split(","): a = a.strip() # ORCA may have type-only or "type name"; take the first word. t = a.split()[0] argTypes.append(t) return { "ret": retType, "name": name, "args": argTypes, "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 *") def emit(decls): """Generate C header and .s file from parsed decls.""" cLines = [ "// AUTOGENERATED by scripts/genToolbox.py from ORCA-C ORCACDefs/.", "// DO NOT EDIT by hand — regenerate to update.", "//", "// Complete IIgs toolbox: ~1300 routines across 35 tool sets.", "// 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", "", ] sLines = [ "; AUTOGENERATED by scripts/genToolbox.py from ORCA-C ORCACDefs/.", "; DO NOT EDIT by hand — regenerate to update.", ";", "; 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 from header re-include, etc. seenNames.add(name) retType = d["ret"] argTypes = d["args"] tool = d["tool"] dispatcher = d["dispatcher"] # Check if all types are known. retSize, retC = typeInfo(retType) argInfo = [typeInfo(a) for a in argTypes] if any(ai is None for ai in argInfo): skipped.append((name, "unknown arg type")) continue # 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, '?')})") 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). 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, but each multi-byte value # pushed lo-word first then hi-word per ORCA convention). # 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 ---") sLines.append(f"\tlda 0x{scratchDP:02X}") sLines.append(f"\tpha") pushedBytes += 2 if firstArgIs32: sLines.append(f"\tlda 0x{scratchDP + 2: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: # Load lo, push; load hi, push. sLines.append(f"\tlda {stackArgOffset + pushedBytes}, s") sLines.append(f"\tpha") pushedBytes += 2 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 = [] for h in sorted(ORCA_DIR.glob("*.h")): for line in h.read_text().splitlines(): d = parseLine(line) if d: decls.append(d) print(f"parsed {len(decls)} declarations from {ORCA_DIR}") emit(decls) if __name__ == "__main__": main()