598 lines
23 KiB
Python
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()
|