425 lines
16 KiB
Python
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()
|