//------------------------------------------------
//--- 010 Editor v16.0 Script File
//
//      File: OMF51_Outline.1sc
//   Authors: Galen Tackett, with assistance from Claude (Anthropic)
//   Version: 1.0
//   Purpose: Outline an OMF-51 object, linked executable, or library in the
//            Output pane: the module/scope nesting as an indented tree, a
//            one-line summary of every record (debug items, register usage,
//            banks), the source-browser cross-reference (0x60-0x64), and
//            consistency checks on nesting and browse counts.
//  Category: Object File
//   History:
//   1.0  2026-09-29 Galen Tackett: First repository version
//
// Self-contained: walks the raw records (type, length, data, checksum) itself,
// so it does not depend on the OMF51 template having been run.
//
// Pairs tracked:
//   Module HDR (0x02)              ... Module END (0x04)
//   Scope 0x00 BeginModule         ... Scope 0x03 EndModule
//   Scope 0x02 / 0x10-0x13 BeginProc ... Scope 0x05 EndProcedure
//   Scope 0x01 BeginDo             ... Scope 0x04 EndDo
// All other records are leaves at the current depth.
//
// Source Browser groups (0x60-0x64) are checked by counts, not pairs
// (verified 2026-09-23 against TTRD2 sources):
//   0x60 starts a group; the group ends at the first record outside 0x60-0x64
//   0x61 file entries must precede the first 0x62; 1-based file index
//   0x62 data: u32 count of 0x63 records that follow, then counted name
//   0x63 data: u16 kind, u16 space (0x60 list index), u16 type,
//              u16 linkage (0 static, 1 public, 2 extern, 3 sfr),
//              u32 count of 0x64 records that follow
//   0x64 data: u32 entry count, then 16-byte entries:
//              u16 refKind, u16 fileIndex, u32 line,
//              u32 access (1 read, 2 write, 3 modify, 4 addr),
//              u32 tagId (0-based index of a related 0x63 in the group)
// Failures are reported as CHECK FAILED and counted separately from
// nesting errors.
//
// 010 note: no string arrays. The stack holds only ints (file offset,
// kind, record index); names are re-read from the file when needed.
//
// Script variables are implicitly local so that qualifier isn't used here.
//------------------------------------------------

RequiresVersion(16,0,0);

//=============================================================================
// Options
//=============================================================================

int collapseLeaves = false;   // true = summarize runs of leaf records
int collapseBrowse = false;   // true = one summary line per browse group
int listRefEntries = true;    // true = print each 0x64 entry (file:line)

//=============================================================================
// Constants and state
//=============================================================================

const uint MAX_DEPTH = 64;

const int K_MODULE = 1;
const int K_SMOD   = 2;
const int K_PROC   = 3;
const int K_DO     = 4;

const string SPACES = "                                                                                                                                ";

int64 stackOff[MAX_DEPTH];
int   stackKind[MAX_DEPTH];
int   stackRec[MAX_DEPTH];
int   depth = 0;
int   maxDepth = 0;

int errors = 0;
int scopeCount = 0;
int moduleCount = 0;
int recCount = 0;

// Leaf-run collapsing
int   runLen = 0;
int   runFirst = 0;
int64 runOff = 0;
int   typeCount[256];

// Source Browser group state
int   inBrowse = false;
int   brFirst = 0;
int64 brOff = 0;
int   brSeen62 = false;
int   brMetasPending = 0;
int   brRefsPending = 0;
const uint MAX_FILES = 128;
int64 brFileOff[MAX_FILES];
const uint MAX_METAS = 4096;
int64 brMetaSymOff[MAX_METAS];
uint   brMetaCount = 0;
uint   brMetaIdx = 0;
uint   brFiles = 0;
uint   brSyms = 0;
uint   brMetas = 0;
uint   brRefs = 0;
uint   browseGroups = 0;
uint   checkFails = 0;

//=============================================================================
// Helpers
//=============================================================================

string Indent(uint d)
{
    uint n = d * 2;
    if (n > Strlen(SPACES)) n = Strlen(SPACES);
    if (n <= 0) return "";
    return SubStr(SPACES, 0, n);
}

string CountedStr(int64 p)
{
    ubyte n = ReadUByte(p);
    if (n == 0) return "";
    return ReadString(p + 1, n);
}

string RecTypeName(ubyte t)
{
    switch (t) {
        case 0x02: return "Module HDR";
        case 0x04: return "Module END";
        case 0x06: return "Content";
        case 0x07: return "Keil Content";
        case 0x08: return "Fixup";
        case 0x09: return "Keil Fixup";
        case 0x0E: return "Segment DEF";
        case 0x0F: return "Keil Segment DEF";
        case 0x10: return "Scope";
        case 0x12: return "Debug Item";
        case 0x13: return "Keil Debug Item";
        case 0x16: return "Public DEF";
        case 0x17: return "Keil Public DEF";
        case 0x18: return "External DEF";
        case 0x19: return "Keil External DEF";
        case 0x20: return "KeilTypeDefs";
        case 0x22: return "KeilDebugLinked";
        case 0x23: return "KeilDebugCompiled";
        case 0x24: return "KeilSourceFile";
        case 0x26: return "LIB ModLocs";
        case 0x28: return "LIB ModNames";
        case 0x2A: return "LIB Dictionary";
        case 0x2C: return "LIB Header";
        case 0x2E: return "BankConfig";
        case 0x60: return "SrcBrowse Spaces";
        case 0x61: return "SrcBrowse File";
        case 0x62: return "SrcBrowse Symbol";
        case 0x63: return "SrcBrowse SymMeta";
        case 0x64: return "SrcBrowse Refs";
        case 0x70: return "AMAKE";
        case 0x72: return "KeilRegUsage";
    }
    return Str("Type %02Xh", t);
}

string ScopeName(ubyte st)
{
    switch (st) {
        case 0x00: return "BeginModule";
        case 0x01: return "BeginDo";
        case 0x02: return "BeginProcedure";
        case 0x03: return "EndModule";
        case 0x04: return "EndDo";
        case 0x05: return "EndProcedure";
        case 0x10: return "BeginProcedure (bank 0)";     // code banks, linked files
        case 0x11: return "BeginProcedure (bank 1)";
        case 0x12: return "BeginProcedure (bank 2)";
        case 0x13: return "BeginProcedure (bank 3)";
    }
    return Str("Scope type %02Xh", st);
}

string KindName(ubyte k)
{
    switch (k) {
        case K_MODULE: return "Module HDR";
        case K_SMOD:   return "BeginModule";
        case K_PROC:   return "BeginProcedure";
        case K_DO:     return "BeginDo";
    }
    return "?";
}

// Name carried by an opening record, re-read from the file
string OpenerName(int64 off)
{
    int t = ReadUByte(off);
    if (t == 0x02) return CountedStr(off + 3);
    if (t == 0x10) return CountedStr(off + 4);
    return "";
}

// Intel DEBUG ITEMS DEF TYPE (first data byte of 0x12/0x13/0x22/0x23)
string DefTypeName(ubyte d)
{
    switch (d) {
        case 0: return "Locals";
        case 1: return "Publics";
        case 2: return "Segments";
        case 3: return "Lines";
    }
    return Str("DefType %02Xh", d);
}

// 0x72: count defined (flag 0) and called (flag 1) functions
string RegUsageSummary(int64 off, uint len)
{
    int64 p = off + 3;
    int64 dataEnd = off + 3 + len - 1;
    uint nDef = 0;
    uint nCall = 0;
    while (p + 4 <= dataEnd) {
        if (ReadUByte(p) == 0) nDef++; else nCall++;
        p += 4 + ReadUByte(p + 3);
    }
    return Str("%u defined, %u called", nDef, nCall);
}

// Content segment: in linked files, 0x10-0x1F is a code bank (0x10 | bank)
string ContentSeg(uint seg)
{
    if (seg >= 0x10 && seg <= 0x1F) return Str("bank %u", seg & 0x0F);
    if (seg == 0) return "abs";
    return Str("seg %02X", seg);
}

// Short detail for a few leaf types
string LeafDetail(int64 off, uint t, uint len)
{
    switch (t) {
        case 0x06: return Str("%s @ %04X", ContentSeg(ReadUByte(off + 3)), ReadUShort(off + 4));
        case 0x07: return Str("seg %04X @ %04X", ReadUShort(off + 3), ReadUShort(off + 5));
        case 0x12: case 0x13:
        case 0x22: case 0x23:
            return DefTypeName(ReadUByte(off + 3));
        case 0x24: return CountedStr(off + 6);
        case 0x61: return CountedStr(off + 9);
        case 0x62: return CountedStr(off + 7);
        case 0x72: return RegUsageSummary(off, len);
    }
    return "";
}

void PrintLine(int idx, int64 off, int d, string text)
{
    Printf("[%4d] %06X  %s%s\n", idx, off, Indent(d), text);
}

void FlushRun()
{
    int t;
    string s = "";
    string sep = "";
    if (runLen == 0) return;
    for (t = 0; t < 256; t++) {
        if (typeCount[t] > 0) {
            s += Str("%s%u %s", sep, typeCount[t], RecTypeName(t));
            sep = ", ";
            typeCount[t] = 0;
        }
    }
    PrintLine(runFirst, runOff, depth,
        Str("... %u record%s (", runLen, runLen == 1 ? "" : "s") + s + ")");
    runLen = 0;
}

void Push(int kind, int64 off, int idx)
{
    if (depth >= MAX_DEPTH) {
        Printf("        ERROR: nesting deeper than %u at record %u\n", MAX_DEPTH, idx);
        errors++;
        return;
    }
    stackOff[depth]  = off;
    stackKind[depth] = kind;
    stackRec[depth]  = idx;
    depth++;
    if (depth > maxDepth) maxDepth = depth;
}

void ReportUnclosedAbove(int level, int idx)
{
    int i;
    for (i = depth - 1; i >= level; i--) {
        Printf("        ERROR: %s '%s' (record %u) not closed before record %u\n",
            KindName(stackKind[i]), OpenerName(stackOff[i]), stackRec[i], idx);
        errors++;
    }
}

// Close the innermost open construct of the given kind.
void Close(int kind, int idx, int64 off, string text, string closerName, int checkName)
{
    int i;
    int found = -1;
    string openName;

    for (i = depth - 1; i >= 0; i--) {
        if (stackKind[i] == kind) { found = i; break; }
    }
    if (found < 0) {
        PrintLine(idx, off, depth, text + "   <-- ERROR: no open " + KindName(kind));
        errors++;
        return;
    }
    ReportUnclosedAbove(found + 1, idx);
    depth = found;
    PrintLine(idx, off, depth, text);
    if (checkName) {
        openName = OpenerName(stackOff[found]);
        if (openName != closerName) {
            Printf("        ERROR: name mismatch: '%s' opened at record %u, closed as '%s'\n",
                openName, stackRec[found], closerName);
            errors++;
        }
    }
}

//=============================================================================
// Source Browser groups
//=============================================================================

void BrowseFail(int idx, string msg)
{
    Printf("        CHECK FAILED at record %u: %s\n", idx, msg);
    checkFails++;
}

string BrKindName(int k)
{
    switch (k) {
        case 0x01: return "macro";
        case 0x02: return "local";         // sfr when linkage = 3
        case 0x04: return "variable";
        case 0x08: return "function";
        case 0x10: return "member";
        case 0x20: return "tag";
        case 0x40: return "typedef";
        case 0x80: return "enumconst";
    }
    return Str("kind %02Xh", k);
}

// Source-browser type codes (not the same code space as the 0x20 base types)
string BrTypeName(int c)
{
    switch (c) {
        case 0x00: return "";
        case 0x01: return "bit";
        case 0x02: return "char";
        case 0x03: return "uchar";
        case 0x04: return "int";
        case 0x05: return "uint";
        case 0x06: return "long";
        case 0x07: return "ulong";
        case 0x08: return "float";
        case 0x0A: return "ptr1";          // data/idata/pdata pointer
        case 0x0B: return "ptr2";          // xdata/code pointer
        case 0x0C: return "ptr3";          // generic pointer
        case 0x0E: return "array";
        case 0x0F: return "struct";
        case 0x10: return "union";
        case 0x11: return "function";
        case 0x12: return "label";
        case 0x13: return "enum";
    }
    return Str("type %02Xh", c);
}

string BrAccessName(int a)
{
    switch (a) {
        case 0: return "";
        case 1: return "read";
        case 2: return "write";
        case 3: return "modify";
        case 4: return "addr";
    }
    return Str("access=%u", a);
}

string BrRefKindName(int k)
{
    switch (k) {
        case 1: return "use";
        case 2: return "def";
        case 3: return "calls";
        case 4: return "calledby";
        case 5: return "member";
    }
    return Str("ref%u", k);
}

// Pre-scan a browse group so a 0x64 tagId (0-based index of a 0x63 in the
// group) can be resolved to its symbol name, even when the target comes later.
void BrPrescan(int64 start)
{
    int64 p = start;
    int64 sym = -1;
    int t;
    brMetaCount = 0;
    while (p + 3 <= FileSize()) {
        t = ReadUByte(p);
        if (t < 0x60 || t > 0x64) break;
        if (t == 0x62) sym = p;
        else if (t == 0x63) {
            if (brMetaCount < MAX_METAS) brMetaSymOff[brMetaCount] = sym;
            brMetaCount++;
        }
        p += 3 + ReadUShort(p + 1);
    }
}

string BrMetaName(uint k)
{
    if (k >= brMetaCount || k >= MAX_METAS || brMetaSymOff[k] < 0)
        return Str("[%u]", k);
    return CountedStr(brMetaSymOff[k] + 7) + Str("[%u]", k);
}

// Memory space name from the current group's 0x60 list.
// 0x60 data: 8 header bytes, u8 count, u8 ?, then counted strings
// (first one empty), i.e. strings start at record + 3 + 10.
string BrSpaceName(int s)
{
    int64 p;
    int64 recEnd;
    int i;
    if (s == 0) return "";
    p = brOff + 13;
    recEnd = brOff + 3 + ReadUShort(brOff + 1) - 1;
    for (i = 0; i < s; i++) {
        if (p >= recEnd) return Str("space %u", s);
        p += 1 + ReadUByte(p);
    }
    if (p >= recEnd) return Str("space %u", s);
    return CountedStr(p);
}

// File name (no path) for a 1-based index into the group's 0x61 list
string BrFileName(int i)
{
    string nm;
    int j;
    if (i < 1 || i > brFiles || i > MAX_FILES) return Str("file %u", i);
    nm = CountedStr(brFileOff[i - 1] + 9);
    for (j = Strlen(nm) - 1; j >= 0; j--) {
        if (nm[j] == '\\' || nm[j] == '/')
            return SubStr(nm, j + 1);
    }
    return nm;
}

void CloseBrowse(int idx)
{
    if (brMetasPending > 0)
        BrowseFail(idx, Str("browse group ended with %u 0x63 record(s) still expected", brMetasPending));
    if (brRefsPending > 0)
        BrowseFail(idx, Str("browse group ended with %u 0x64 record(s) still expected", brRefsPending));
    if (collapseBrowse)
        PrintLine(brFirst, brOff, depth,
            Str("SrcBrowse group: %u files, %u symbols, %u SymMeta, %u Refs",
                brFiles, brSyms, brMetas, brRefs));
    inBrowse = false;
    brMetasPending = 0;
    brRefsPending = 0;
}

void HandleBrowse(int t, int len, int64 pos, int idx)
{
    int d = depth;
    int n;
    int e;
    int kind, space, tcode, f4;
    int rk, fi, line, acc;
    uint tag;
    int64 ep;
    string s;

    if (t == 0x60) {
        if (inBrowse) CloseBrowse(idx);
        inBrowse = true;
        brFirst = idx;
        brOff = pos;
        brSeen62 = false;
        brMetasPending = 0;
        brRefsPending = 0;
        brFiles = 0;
        brSyms = 0;
        brMetas = 0;
        brRefs = 0;
        brMetaIdx = 0;
        BrPrescan(pos);
        browseGroups++;
        if (!collapseBrowse) PrintLine(idx, pos, d, "SrcBrowse Spaces  (group start)");
        return;
    }

    switch (t) {
        case 0x61:
            if (brFiles < MAX_FILES) brFileOff[brFiles] = pos;
            brFiles++;
            if (!collapseBrowse)
                PrintLine(idx, pos, d + 1,
                    Str("SrcBrowse File %u  ", brFiles) + CountedStr(pos + 9));
            if (brSeen62)
                BrowseFail(idx, "0x61 file entry after symbols began");
            break;

        case 0x62:
            brSyms++;
            n = (int)ReadUInt(pos + 3);
            if (!collapseBrowse)
                PrintLine(idx, pos, d + 1,
                    "SrcBrowse Symbol  " + CountedStr(pos + 7) + Str("  (%u SymMeta)", n));
            if (brMetasPending > 0)
                BrowseFail(idx, Str("new symbol while %u 0x63 record(s) still expected", brMetasPending));
            if (brRefsPending > 0)
                BrowseFail(idx, Str("new symbol while %u 0x64 record(s) still expected", brRefsPending));
            if (n == 0)
                BrowseFail(idx, "0x62 with zero 0x63 count");
            brMetasPending = n;
            brRefsPending = 0;
            brSeen62 = true;
            break;

        case 0x63:
            brMetas++;
            kind  = ReadUShort(pos + 3);
            space = ReadUShort(pos + 5);
            tcode = ReadUShort(pos + 7);
            f4    = ReadUShort(pos + 9);
            n     = (int)ReadUInt(pos + 11);
            if (!collapseBrowse) {
                s = "";
                if (kind == 0x04 || kind == 0x08) {
                    if (f4 == 0) s = "static ";
                    else if (f4 == 2) s = "extern ";
                }
                s = Str("SrcBrowse SymMeta [%u]  ", brMetaIdx) + s +
                    ((kind == 0x02 && tcode == 0x12) ? "label" :
                     (kind == 0x02 && f4 == 3) ? "sfr" : BrKindName(kind));
                if (tcode != 0 && tcode != 0x12 && !(kind == 0x08 && tcode == 0x11))
                    s += " " + BrTypeName(tcode);
                if (space != 0) s += " in " + BrSpaceName(space);
                s += Str(", %u Refs", n);
                PrintLine(idx, pos, d + 2, s);
            }
            if (brRefsPending > 0)
                BrowseFail(idx, Str("0x63 while %u 0x64 record(s) still expected", brRefsPending));
            if (brMetasPending <= 0)
                BrowseFail(idx, "0x63 not expected by a preceding 0x62");
            else
                brMetasPending--;
            brRefsPending = n;
            brMetaIdx++;
            break;

        case 0x64:
            brRefs++;
            n = (int)ReadUInt(pos + 3);
            e = (len - 5) / 16;
            if (!collapseBrowse)
                PrintLine(idx, pos, d + 2,
                    Str("SrcBrowse Refs  %u entr%s", n, n == 1 ? "y" : "ies"));
            if ((len - 5) % 16 != 0)
                BrowseFail(idx, Str("0x64 data length %u is not 4 + 16*n", len - 1));
            else if (n != e)
                BrowseFail(idx, Str("0x64 entry count %u but length holds %u", n, e));
            if (brRefsPending <= 0)
                BrowseFail(idx, "0x64 not expected by a preceding 0x63");
            else
                brRefsPending--;
            if (listRefEntries && !collapseBrowse) {
                for (ep = pos + 7; ep + 16 <= pos + 3 + len - 1; ep += 16) {
                    rk   = ReadUShort(ep);
                    fi   = ReadUShort(ep + 2);
                    line = (int)ReadUInt(ep + 4);
                    acc  = (int)ReadUInt(ep + 8);
                    tag  = ReadUInt(ep + 12);
                    s = Str("%-8s ", BrRefKindName(rk)) + BrFileName(fi) + Str(":%u", line);
                    if (acc != 0) s += "  " + BrAccessName(acc);
                    if (tag != 0xFFFFFFFF && tag != 0x00502682) s += "  -> " + BrMetaName(tag);
                    PrintLine(idx, ep, d + 3, s);
                }
            }
            break;
    }

    if (!inBrowse)
        BrowseFail(idx, RecTypeName(t) + " outside a browse group");
}

//=============================================================================
// Main
//=============================================================================

LittleEndian();

int64 fsize = FileSize();
int64 pos = 0;
ubyte t;
uint len;
ubyte st;
string nm;
string text;
uint i;

for (i = 0; i < 256; i++) typeCount[i] = 0;

Printf("\n=== OMF-51 outline: %s ===\n", GetFileName());

if (fsize == 0) {
    Printf("Empty file.\n");
    Exit(0);
}

while (pos < fsize) {
    if (pos + 3 > fsize) {
        Printf("        ERROR: truncated record header at %06X\n", pos);
        errors++;
        break;
    }
    t   = ReadUByte(pos);
    len = ReadUShort(pos + 1);
    if (pos + 3 + len > fsize) {
        Printf("        ERROR: record %u at %06X (type %02Xh, len %u) runs past EOF\n",
            recCount, pos, t, len);
        errors++;
        break;
    }

    if (inBrowse && (t < 0x60 || t > 0x64))
        CloseBrowse(recCount);

    if (t == 0x02 || t == 0x04 || t == 0x10 || (t >= 0x60 && t <= 0x64))
        FlushRun();

    if (t == 0x02) {
        nm = CountedStr(pos + 3);
        if (depth > 0) {
            // A new module while something is still open
            ReportUnclosedAbove(0, recCount);
            depth = 0;
        }
        PrintLine(recCount, pos, depth, "Module HDR  " + nm);
        Push(K_MODULE, pos, recCount);
        moduleCount++;
    }
    else if (t == 0x04) {
        nm = CountedStr(pos + 3);
        Close(K_MODULE, recCount, pos, "Module END  " + nm, nm, true);
    }
    else if (t == 0x10) {
        st = ReadUByte(pos + 3);
        nm = CountedStr(pos + 4);
        text = "Scope " + ScopeName(st);
        if (nm != "") text += "  " + nm;
        switch (st) {
            case 0x00:
                PrintLine(recCount, pos, depth, text);
                Push(K_SMOD, pos, recCount);
                scopeCount++;
                break;
            case 0x01:
                PrintLine(recCount, pos, depth, text);
                Push(K_DO, pos, recCount);
                scopeCount++;
                break;
            case 0x02:
            case 0x10: case 0x11: case 0x12: case 0x13:
                PrintLine(recCount, pos, depth, text);
                Push(K_PROC, pos, recCount);
                scopeCount++;
                break;
            case 0x03:
                Close(K_SMOD, recCount, pos, text, nm, true);
                break;
            case 0x04:
                Close(K_DO, recCount, pos, text, nm, false);
                break;
            case 0x05:
                Close(K_PROC, recCount, pos, text, nm, true);
                break;
            default:
                PrintLine(recCount, pos, depth, text + "   <-- WARNING: unknown scope type");
                break;
        }
    }
    else if (t >= 0x60 && t <= 0x64) {
        HandleBrowse(t, len, pos, recCount);
    }
    else {
        if (collapseLeaves) {
            if (runLen == 0) { runFirst = recCount; runOff = pos; }
            typeCount[t]++;
            runLen++;
        } else {
            text = RecTypeName(t);
            nm = LeafDetail(pos, t, len);
            if (nm != "") text += "  " + nm;
            PrintLine(recCount, pos, depth, text);
        }
    }

    pos += 3 + len;
    recCount++;
}

FlushRun();

if (inBrowse)
    CloseBrowse(recCount);

if (depth > 0) {
    ReportUnclosedAbove(0, recCount);
    depth = 0;
}

Printf("--- %u records, %u module%s, %u scope%s, max depth %u: %s (%u error%s)\n",
    recCount,
    moduleCount, moduleCount == 1 ? "" : "s",
    scopeCount,  scopeCount  == 1 ? "" : "s",
    maxDepth,
    errors == 0 ? "nesting OK" : "NESTING ERRORS",
    errors, errors == 1 ? "" : "s");

if (browseGroups > 0)
    Printf("--- %u browse group%s: %s (%u check failure%s)\n",
        browseGroups, browseGroups == 1 ? "" : "s",
        checkFails == 0 ? "counts and indices consistent" : "CHECK FAILURES",
        checkFails, checkFails == 1 ? "" : "s");
