#!/usr/bin/env python3
# genGnoKernel.py — generate wrappers for the GNO kernel toolset.
#
# The GNO kernel is implemented as Apple IIgs user toolset $03.  Each
# kernel function is identified by a 16-bit number $XX03 where XX is
# the function index and 03 is the toolset.  Kernel calls dispatch through
# GNO's own vector $E10008 (the "udispatch" JSL target GNO installs), NOT the
# standard IIgs tool dispatcher $E10000 (toolset $03 there is the Misc Tool Set).
#
# Function list is hard-coded from include/gno/kerntool.h in the GNO
# Consortium source (https://github.com/GnoConsortium/gno).  The
# canonical ORCA-style signature is preserved in a comment.
#
# Output: runtime/src/gnoKernel.s — asm wrappers callable from our C
# ABI (arg0 in A, arg0 i32 in A:X, rest pushed RTL on stack).  Each
# wrapper re-pushes args in toolbox/Pascal order (high-word first for
# 32-bit values), allocates result space, JSLs $E10008, pops the
# result back into A:X if non-void.
#
# Run after editing the function table:
#   python3 scripts/genGnoKernel.py
#
# The libc layer in runtime/src/libcGno.c wraps these K* primitives
# into POSIX names (fork, exec, wait, etc.).

from pathlib import Path

OUT_ASM = Path("/home/scott/claude/llvm816/runtime/src/gnoKernel.s")
OUT_HEADER = Path("/home/scott/claude/llvm816/runtime/include/gno/kernel.h")

# Each entry: (funcId, name, retSize, [argSize, ...])
# Sizes in bytes (2 = i16, 4 = i32 / ptr32).  Names follow ORCA's K* prefix.
#
# Pulled from include/gno/kerntool.h in the GNO Consortium repo.
SYSCALLS = [
    # ---- Process control ----
    (0x0903, "Kgetpid",       2, []),                  # () -> int
    (0x0A03, "Kkill",         2, [2, 2, 4]),           # (pid, sig, *errno) -> int
    (0x0B03, "Kfork",         2, [4, 4]),              # (*subr, *errno) -> int
    (0x0903, "Kgetpid_dup",   2, []),                  # alias kept for sanity check
    (0x4003, "Kgetppid",      2, [4]),                 # (*errno) -> int
    (0x1703, "Kwait",         2, [4, 4]),              # (*status, *errno) -> int
    (0x1D03, "K_execve",      2, [4, 4, 4]),           # (*file, *cmdline, *err) -> int
    (0x1603, "Ksignal",       4, [2, 4, 4]),           # (sig, func_ptr, *err) -> sig_t
    (0x1E03, "Kalarm",        4, [4, 4]),              # (seconds, *errno) -> longword
    (0x4203, "Kalarm10",      4, [4, 4]),              # (seconds-tenths, *errno) -> longword
    (0x2103, "Ksigpause",     2, [4, 4]),              # (mask, *errno) -> int
    (0x1B03, "Ksigsetmask",   4, [4, 4]),              # (mask, *errno) -> longword
    (0x1C03, "Ksigblock",     4, [4, 4]),              # (mask, *errno) -> longword

    # ---- semaphores (binary-mutex + handshake for thread-safe malloc/fork) ----
    (0x0F03, "Kscreate",      2, [2, 4]),              # (count, *errno) -> sem id
    (0x0D03, "Kswait",        2, [2, 4]),              # (sem, *errno) -> int  (P / lock)
    (0x0E03, "Kssignal",      2, [2, 4]),              # (sem, *errno) -> int  (V / unlock)

    # ---- File descriptors ----
    (0x2203, "Kdup",          2, [2, 4]),              # (oldfd, *errno) -> int
    (0x2303, "Kdup2",         2, [2, 2, 4]),           # (oldfd, newfd, *errno) -> int
    (0x2403, "Kpipe",         2, [4, 4]),              # (*fildes[2], *errno) -> int
    (0x2603, "Kioctl",        2, [2, 4, 4, 4]),        # (fd, req:ulong, *ptr, *errno) -> int

    # ---- stat ----
    (0x2703, "Kstat",         2, [4, 4, 4]),           # (*path, *sbuf, *errno) -> int
    (0x2803, "Kfstat",        2, [2, 4, 4]),           # (fd, *sbuf, *errno) -> int
    (0x2903, "Klstat",        2, [4, 4, 4]),           # (*path, *sbuf, *errno) -> int

    # ---- IDs ----
    (0x2A03, "Kgetuid",       2, [4]),                 # (*errno) -> int
    (0x2B03, "Kgetgid",       2, [4]),                 # (*errno) -> int
    (0x2C03, "Kgeteuid",      2, [4]),                 # (*errno) -> int
    (0x2D03, "Kgetegid",      2, [4]),                 # (*errno) -> int
    (0x2E03, "Ksetuid",       2, [2, 4]),              # (uid, *errno) -> int
    (0x2F03, "Ksetgid",       2, [2, 4]),              # (gid, *errno) -> int

    # ---- Process groups / TTY ----
    (0x1803, "Ktcnewpgrp",    2, [2, 4]),              # (fdtty, *errno) -> int
    (0x1903, "Ksettpgrp",     2, [2, 4]),              # (fdtty, *errno) -> int
    (0x1A03, "Ktctpgrp",      2, [2, 2, 4]),           # (fdtty, pid, *err) -> int
    (0x2503, "K_getpgrp",     2, [2, 4]),              # (pid, *errno) -> pid_t
    (0x3403, "Ksetpgrp",      2, [2, 2, 4]),           # (pid, pgrp, *errno) -> int

    # ---- Kernel VM (process inspection) ----
    (0x1103, "Kkvm_open",     2, [4]),                 # (*errno) -> int
    (0x1203, "Kkvm_close",    2, [4, 4]),              # (kvmt, *errno) -> int

    # ---- Misc ----
    (0x3503, "Ktimes",        4, [4, 4]),              # (*tms, *errno) -> clock_t

    # ---- Quit setup ----
    (0x4103, "KSetGNOQuitRec", 0, [2, 4, 2, 4]),       # (pCount, GSStringPtr, flags, *err) -> void
]

# GNO kernel calls dispatch through GNO's OWN vector $E10008 (the "udispatch"
# JSL target GNO installs -- lib/libc/sys/trap.asm: `udispatch gequ $E10008`),
# NOT the standard IIgs Tool Locator at $E10000 (where toolset $03 is the Misc
# Tool Set, so calling it there hangs/misbehaves).  Same LDX #funcId convention.
DISPATCHER = 0xE10008


def emitWrapper(funcId, name, retSize, argSizes):
    """Emit one .s function body for the given GNO kernel call."""
    lines = []
    argTypes = [f"i{size*8}" for size in argSizes]
    lines.append(f"; {name}({', '.join(argTypes) or 'void'}) -> "
                 f"{'void' if retSize == 0 else f'i{retSize*8}'}")
    lines.append(f"; toolset 3, func 0x{funcId:04X}")
    lines.append(f"\t.section .text.{name},\"ax\"")
    lines.append(f"\t.globl {name}")
    lines.append(f"{name}:")

    scratchDP = 0xE0
    firstArgIs32 = bool(argSizes) and argSizes[0] == 4

    # Stash arg0 if any args.  arg0 in A (i16) or A:X (i32).
    if argSizes:
        lines.append(f"\t; --- stash arg0 (in A{'/X' if firstArgIs32 else ''}) ---")
        lines.append(f"\tsta 0x{scratchDP:02X}")
        if firstArgIs32:
            lines.append(f"\tstx 0x{scratchDP + 2:02X}")

    # Result space (rounded up to whole words).
    resultWords = (retSize + 1) // 2
    if resultWords > 0:
        lines.append(f"\t; --- result space ({retSize} bytes) ---")
        for _ in range(resultWords):
            lines.append("\tpea 0")

    # Push args in Pascal order (high-word first for 32-bit).
    pushedBytes = resultWords * 2

    # arg0.
    if argSizes:
        lines.append("\t; --- arg0 ---")
        if firstArgIs32:
            lines.append(f"\tlda 0x{scratchDP + 2:02X}")
            lines.append("\tpha")
            pushedBytes += 2
        lines.append(f"\tlda 0x{scratchDP:02X}")
        lines.append("\tpha")
        pushedBytes += 2

    # arg1+.
    stackArgOffset = 4   # caller's first stack arg (after 3-byte JSL ret addr)
    for i, size in enumerate(argSizes[1:], start=1):
        lines.append(f"\t; --- arg{i} ({size}B) ---")
        if size <= 2:
            lines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
            lines.append("\tpha")
            pushedBytes += 2
            stackArgOffset += 2
        elif size == 4:
            # High word first.  Caller pushed low-word-first, so HI is at
            # caller offset +2.  After PHA, the LO becomes accessible at
            # the SAME stack-rel offset (PHA shifted SP by 2).
            lines.append(f"\tlda {stackArgOffset + pushedBytes + 2}, s")
            lines.append("\tpha")
            pushedBytes += 2
            lines.append(f"\tlda {stackArgOffset + pushedBytes}, s")
            lines.append("\tpha")
            pushedBytes += 2
            stackArgOffset += 4
        else:
            raise RuntimeError(f"unhandled arg size {size} in {name}")

    # Dispatch.
    lines.append(f"\tldx #0x{funcId:04X}")
    lines.append(f"\tjsl 0x{DISPATCHER:x}")

    # Pop result.
    if resultWords > 0:
        lines.append("\tpla              ; result lo -> A")
        if resultWords == 2:
            lines.append("\tplx              ; result hi -> X")
    lines.append("\trtl")
    lines.append("")
    return lines


def emitHeader():
    """Generate a C header with extern decls for use by libc."""
    hLines = [
        "// AUTOGENERATED by scripts/genGnoKernel.py — DO NOT EDIT.",
        "// GNO kernel toolset $03 wrappers, callable from C.",
        "// Convention: each K* returns the kernel result (or -1 on error);",
        "// the last argument is `int *errno` and gets the kernel's errno.",
        "//",
        "// These are LOW-LEVEL primitives — libc routines in libcGno.c",
        "// wrap them into POSIX-named fork/exec/wait/etc.",
        "#ifndef GNO_KERNEL_H",
        "#define GNO_KERNEL_H",
        "",
        "#include <stdint.h>",
        "",
    ]
    for funcId, name, retSize, argSizes in SYSCALLS:
        # Skip the sanity-check duplicate.
        if name.endswith("_dup"):
            continue
        retC = "void" if retSize == 0 else (
            "int" if retSize == 2 else "unsigned long")
        argC = []
        for i, size in enumerate(argSizes):
            t = "int" if size == 2 else "void *"
            argC.append(f"{t} a{i}")
        hLines.append(f"extern {retC} {name}({', '.join(argC) or 'void'});"
                      f"  // 0x{funcId:04X}")
    hLines.append("")
    hLines.append("#endif")
    hLines.append("")
    return hLines


def main():
    asmLines = [
        "; AUTOGENERATED by scripts/genGnoKernel.py — DO NOT EDIT by hand.",
        ";",
        "; GNO/ME kernel toolset $03 wrappers.",
        "; Dispatcher: JSL $E10008 (GNO's udispatch vector) with LDX #funcId.",
        ";",
        "; C ABI: arg0 (i16) in A, arg0 (i32) in A:X, arg1+ on stack",
        "; (caller-pushed, lo-word first for 32-bit values).",
        "; Each wrapper re-pushes args in Pascal order (high-word first",
        "; for 32-bit), allocates result space, dispatches, pops result.",
        "",
        "\t.text",
        "",
    ]
    seen = set()
    for funcId, name, retSize, argSizes in SYSCALLS:
        if name in seen:
            continue
        seen.add(name)
        asmLines.extend(emitWrapper(funcId, name, retSize, argSizes))

    OUT_ASM.parent.mkdir(parents=True, exist_ok=True)
    OUT_ASM.write_text("\n".join(asmLines))
    print(f"wrote {OUT_ASM} ({len(asmLines)} lines, "
          f"{len(seen) - 1} wrappers)")

    OUT_HEADER.parent.mkdir(parents=True, exist_ok=True)
    OUT_HEADER.write_text("\n".join(emitHeader()))
    print(f"wrote {OUT_HEADER}")


if __name__ == "__main__":
    main()
