65816-llvm-mos/scripts/genToolbox.py
Scott Duensing f338d93bae Checkpoint
2026-05-02 18:30:15 -05:00

425 lines
16 KiB
Python

#!/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. i16: 'sta scratch'. i32: 'sta scratch; stx scratch+2'.
scratchDP = 0xE0 # libcall scratch zone
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.
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()