#!/usr/bin/python3
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details

# The purpose of this script is to analyze disassembly generated by objdump or
# dumpbin to print (or to compare) the stack usage of functions/methods.
# This is a quickly written script, so it is quite possible it may not handle
# all code properly.
#
# The script expects the user to create a text assembly dump to be passed to
# the script.
#
# objdump Example
#   objdump --demangle --disassemble objfile.o > objfile.s
#
# dumpbin Example
#   dumpbin /disasm objfile.obj > objfile.s
#
# If the script is passed a single file, then all stack size information that
# is found it printed.  If two files are passed, then the script compares the
# stack usage of the two files (useful for A/B comparisons).
# Currently more than two input files are not supported. (But adding support shouldn't
# be very difficult.)
#
# Note: The script only handles x64 disassembly.  Supporting x86 is likely
# trivial, but ARM support could be difficult.
# Thus far the script has been tested with MSVC on Win64 and clang on OSX.

import argparse
import re

blank_re = re.compile('\s*')

class LineReader:
    def __init__(self, lines):
        self.lines = list(reversed(lines))
    def get_line(self):
        return self.lines.pop(-1)
    def peek_line(self):
        return self.lines[-1]
    def consume_blank_lines(self):
        while blank_re.fullmatch(self.peek_line()):
            self.get_line()
    def is_empty(self):
        return len(self.lines) == 0

def parse_objdump_assembly(in_file):
    results = {}
    text_section_re = re.compile('Disassembly of section __TEXT,__text:\s*')
    symbol_re = re.compile('[^<]*<(.*)>:\s*')
    stack_alloc = re.compile('.*subq\s*\$(\d*), %rsp\s*')

    lr = LineReader(in_file.readlines())

    def find_stack_alloc_size():
        while True:
            if lr.is_empty():
                return None
            if blank_re.fullmatch(lr.peek_line()):
                return None

            line = lr.get_line()
            mo = stack_alloc.fullmatch(line)
            if mo:
                lr.consume_blank_lines()
                return int(mo.group(1))

    # Find beginning of disassembly
    while not text_section_re.fullmatch(lr.get_line()):
        pass

    # Scan for symbols
    while not lr.is_empty():
        lr.consume_blank_lines()
        if lr.is_empty():
            break
        line = lr.get_line()
        mo = symbol_re.fullmatch(line)
        # Found a symbol
        if mo:
            symbol = mo.group(1)
            stack_size = find_stack_alloc_size()
            if stack_size != None:
                results[symbol] = stack_size

    return results

def parse_dumpbin_assembly(in_file):
    results = {}

    file_type_re = re.compile('File Type: COFF OBJECT\s*')
    symbol_re = re.compile('[^(]*\((.*)\):\s*')
    summary_re = re.compile('\s*Summary\s*')
    stack_alloc = re.compile('.*sub\s*rsp,([A-Z0-9]*)h\s*')

    lr = LineReader(in_file.readlines())

    def find_stack_alloc_size():
        while True:
            if lr.is_empty():
                return None
            if blank_re.fullmatch(lr.peek_line()):
                return None

            line = lr.get_line()
            mo = stack_alloc.fullmatch(line)
            if mo:
                lr.consume_blank_lines()
                return int(mo.group(1), 16) # return value in decimal

    # Find beginning of disassembly
    while not file_type_re.fullmatch(lr.get_line()):
        pass

    # Scan for symbols
    while not lr.is_empty():
        lr.consume_blank_lines()
        if lr.is_empty():
            break
        line = lr.get_line()
        if summary_re.fullmatch(line):
            break
        mo = symbol_re.fullmatch(line)
        # Found a symbol
        if mo:
            symbol = mo.group(1)
            stack_size = find_stack_alloc_size()
            if stack_size != None:
                results[symbol] = stack_size
    return results

def main():
    parser = argparse.ArgumentParser(description='Tool used for reporting or comparing the stack usage of functions/methods')
    parser.add_argument('--format', choices=['dumpbin', 'objdump'], required=True, help='Specifies the program used to generate the input files')
    parser.add_argument('--input', action='append', required=True, help='Input assembly file.  This option may be specified multiple times.')
    parser.add_argument('--md-output', action='store_true', help='Show table output in markdown format')
    parser.add_argument('--only-diffs', action='store_true', help='Only show stack info when it differs between the input files')
    args = parser.parse_args()

    parsers = {'dumpbin': parse_dumpbin_assembly, 'objdump' : parse_objdump_assembly}
    parse_func = parsers[args.format]

    input_results = []
    for input_name in args.input:
        with open(input_name) as in_file:
            results = parse_func(in_file)
            input_results.append(results)

    if len(input_results) == 1:
        # Print out the results sorted by size
        size_sorted = sorted([(size, symbol) for symbol, size in results.items()], reverse=True)
        print(input_name)
        for size, symbol in size_sorted:
            print(f'{size:10}\t{symbol}')
        print()
    elif len(input_results) == 2:
        common_symbols = set(input_results[0].keys()).intersection(set(input_results[1].keys()))
        print(f'Found {len(common_symbols)} common symbols')
        stack_sizes = sorted([(input_results[0][sym], input_results[1][sym], sym) for sym in common_symbols], reverse=True)
        if args.md_output:
            print('Before | After | Symbol')
            print('-- | -- | --')
        for size0, size1, symbol in stack_sizes:
            if args.only_diffs and size0 == size1:
                continue
            if args.md_output:
                print(f'{size0} | {size1} | {symbol}')
            else:
                print(f'{size0:10}\t{size1:10}\t{symbol}')
    else:
        print("TODO support more than 2 inputs")

if __name__ == '__main__':
    main()
