Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.

—John Carmack

import argparse
import os

parser = argparse.ArgumentParser(description='Assembles to .hack')
parser.add_argument('filepath', help='<.asm file>')
args = parser.parse_args()

fin = args.filepath
dir = os.path.dirname(fin)
fou = os.path.splitext(fin)[0] + '.hack'

symbols = {'SP'     : 0,
           'LCL'    : 1,
           'ARG'    : 2,
           'THIS'   : 3,
           'THAT'   : 4,
           'R0'     : 0,
           'R1'     : 1,
           'R2'     : 2,
           'R3'     : 3,
           'R4'     : 4,
           'R5'     : 5,
           'R6'     : 6,
           'R7'     : 7,
           'R8'     : 8,
           'R9'     : 9,
           'R10'    : 10,
           'R11'    : 11,
           'R12'    : 12,
           'R13'    : 13,
           'R14'    : 14,
           'R15'    : 15,
           'SCREEN' : 16384,
           'KBD'    : 24576}

compDict = {'0' : '0101010',
            '1' : '0111111',
           '-1' : '0111010',
            'D' : '0001100',
            'A' : '0110000',
            'M' : '1110000',
           '!D' : '0001101',
           '!A' : '0110001',
           '!M' : '1110001',
           '-D' : '0001111',
           '-A' : '0110011',
           '-M' : '1110011',
          'D+1' : '0011111',
          'A+1' : '0110111',
          'M+1' : '1110111',
          'D-1' : '0001110',
          'A-1' : '0110010',
          'M-1' : '1110010',
          'D+A' : '0000010',
          'D+M' : '1000010',
          'D-A' : '0010011',
          'D-M' : '1010011',
          'A-D' : '0000111',
          'M-D' : '1000111',
          'D&A' : '0000000',
          'D&M' : '1000000',
          'D|A' : '0010101',
          'D|M' : '1010101',}

destDict = {'' : '000',
           'M' : '001',
           'D' : '010',
          'MD' : '011',
           'A' : '100',
          'AM' : '101',
          'AD' : '110',
         'AMD' : '111'}

jumpDict = {'' : '000',
         'JGT' : '001',
         'JEQ' : '010',
         'JGE' : '011',
         'JLT' : '100',
         'JNE' : '101',
         'JLE' : '110',
         'JMP' : '111'}

comment_marker = '//'
a_instr_marker = '@'
label_marker = '('

var_address = symbols['R15'] + 1


def assembleAInstruction(line):
    value = line.rstrip()[1:]
    if not value.isdigit():
        # A-instruction w/o a corresponding (label), a user var
        if value not in symbols:
            global var_address
            symbols[value] = var_address
            var_address += 1
        value = symbols[value]
    return "{0:016b}".format(int(value))


def assembleCInstruction(line):
    if '=' in line and ';' not in line:
        dest = line.split('=')[0]
        comp = line.split('=')[1]
        jump = ''
    elif '=' not in line and ';' in line:
        dest = ''
        comp = line.split(';')[0]
        jump = line.split(';')[1]
    elif '=' in line and ';' in line:
        dest = line.split('=')[0]
        comp = line.split('=')[1].split(';')[0]
        jump = line.split(';')[1]
    comp = comp.rstrip()  # remove CR
    jump = jump.rstrip()
    return '111' + compDict[comp] + destDict[dest] + jumpDict[jump]


# 1st pass - symbol resolution
rom_address = 0
lines = open(fin)
for line in lines:
    line = line.lstrip().split(comment_marker)[0]
    comment = line.startswith(comment_marker)
    if not comment and len(line) > 0:
        if line.startswith(label_marker):
            label = line.rstrip()[1:-1]
            symbols[label] = rom_address
        else:
            rom_address += 1

# 2nd pass - assembly
lines.seek(0)
with open(fou, 'w') as fou_handle:
    lines = [line.lstrip().split(comment_marker)[0] for line in lines]
    for line in lines:
        comment = line.startswith(comment_marker)
        label = line.startswith(label_marker)
        if not comment and not label and len(line) > 0:
            if line.startswith(a_instr_marker):
                line = assembleAInstruction(line)
            else:
                line = assembleCInstruction(line)
            fou_handle.write(line + '\n')