#!/usr/bin/perl
#
# Read in files of PDP-7 assembly code in Ken Thompon's as format
# and convert them into PDP-7 machine code
#
# (c) 2016 Warren Toomey, GPL3
# Tweaked by Phil Budne (line, expression parsing, "list" format)
#
use strict;
use warnings;
use Data::Dumper;
use Getopt::Long qw(GetOptions);

Getopt::Long::Configure qw(gnu_getopt);

### Global variables ###
my %Var;       # Variables such as ., .., t
my %Label;     # Labels that are defined once
my %Rlabel;    # Relative labels, e.g. 1:, 2:
               # with an array of locations for each label

my @Mem;       # Actual PDP-7 memory locations
my @Mline;     # Source lines associated with mem locations
my $origline;  # The original current input line of code
my $line;      # line being parsed
my $stage = 1; # Pass one or pass two
my $errors = 0; # set to non-zero on error
my $line_error = ' ';
my $file;	# current file name
my $lineno;	# current line number

## command line options
my $debug = 0;		# Run in debug mode
my $symbols = 0;	# dump symbols
my $format = 'a7out';	# output format

# keep this near the GetOptions call to make it easy to add documentation!
sub usage {
    die("Usage: $0 [--debug] [--format=a7out|list ] file1.s [file2.s ...]\n")
}

### Main program ###

GetOptions(
    'debug|d' => \$debug,
    'format|f=s' => \$format,
) or usage();

usage() if ( @ARGV < 1 );

# predefine syscall and opcodes as variables
# start with the location counter at zero
%Var = (
    '.'    => 0,
    save   => 1,		# saves core dump & user area!
    getuid => 2,
    open   => 3,
    read   => 4,
    write  => 5,
    creat  => 6,
    seek   => 7,
    tell   => 8,
    close  => 9,
    link   => 10,
    unlink => 11,
    setuid => 12,
    rename => 13,
    exit   => 14,
    time   => 15,
    intrp  => 16,
    chdir  => 17,
    chmod  => 18,
    chown  => 19,
    # 20 removed
    sysloc => 21,		# return system addresses
    # 22 removed
    capt   => 23,		# capture display?
    rele   => 24,		# release display?
    status => 25,		# "stat"
    smes   => 27,
    rmes   => 28,
    fork   => 29,

    # List of instruction names and machine code values
    # These come from https://raw.githubusercontent.com/simh/
    #   simtools/master/crossassemblers/macro7/macro7.c
    # PLB: replace with symbols in "sop.s"?
    sys    => 0000000,		# cal instruction (trap thru 020)
    i      => 0020000,		# indirect bit

    # memory reference instructions
    dac    => 0040000,		# deposit AC
    jms    => 0100000,		# jump to subroutine
    dzm    => 0140000,		# deposit zero in memory
    lac    => 0200000,		# load AC
    xor    => 0240000,		# exclusive or
    add    => 0300000,		# one's complement add
    tad    => 0340000,		# two's complement add
    xct    => 0400000,		# execute
    isz    => 0440000,		# increment and skip if zero
    and    => 0500000,		# AND with contents of Y
    sad    => 0540000,		# skip if AC different from content of Y
    jmp    => 0600000,		# jump to Y

    # Type 177 Extended Arithmetic Element (EAE)
    eae    => 0640000,		# base instruction (nop)
    osc    => 0640001,		# OR SC into AC
    omq    => 0640002,		# OR MQ into AC
    cmq    => 0640004,		# Complement MQ
    div    => 0640323,		# divide
    norm   => 0640444,		# normalize, unsigned
    lls    => 0640600,		# long left shift
    als    => 0640700,		# AC shift
    lrs    => 0640500,		# long right shift
    lacs   => 0641001,		# load AC with SC
    lacq   => 0641002,		# load AC with MQ
    abs    => 0644000,		# absolute value
    divs   => 0644323,		# divide, signed

    clq    => 0650000,		# clear MQ
    frdiv  => 0650323,		# fractional divide
    lmq    => 0652000,		# load MQ from AC
    mul    => 0653122,		# multiply
    idiv   => 0653323,		# integer divide
    idivs  => 0657323,		# integer divide, signed
    frdivs => 0654323,		# fractional divide, signed
    muls   => 0657122,		# multiply, signed

    norms  => 0660444,		# normalize, signed
    gsm    => 0664000,		# get sign and magnitude
    lrss   => 0660500,		# long right shift, signed
    llss   => 0660600,		# long left shift, signed
    alss   => 0660700,		# AC left shift, signed

    # I/OT instructions
    iot    => 0700000,		# base i/o transfer instruction
    clsf   => 0700001,		# skip if RT clock overflow
    iof    => 0700002,		# interrupts off (disable PIC)
    ion    => 0700042,		# interrupts on (enable PIC)
    iton   => 0700062,		# interrupt and trap on
    clof   => 0700004,		# clear clock flag, disable clock
    clon   => 0700044,		# clear clock flag, enable clock

    rsf    => 0700101,		# skip if PTR flag set
    rrb    => 0700112,		# clear PTR flag, OR buffer with AC
    rcf    => 0700102,		# clear PTR flag
    rsa    => 0700104,		# select PTR in alphanumeric mode
    rsb    => 0700144,		# select PTR in binary mode

    psf    => 0700201,		# skip if PTP flag set
    pcf    => 0700202,		# clear PTP flag
    psa    => 0700204,		# punch PTP in alphanumeric mode
    psb    => 0700244,		# punch PTP in binary mode

    ksf    => 0700301,		# skip if KBD flag set
    krb    => 0700312,		# read KBD buffer
    iors   => 0700314,		# input/output read status

    tsf    => 0700401,		# skip if if TTY output flag set
    tcf    => 0700402,		# clear TTY output flag
    tls    => 0700406,		# load TTY output buffer and select

    tts    => 0703301,		# Test Teleprinter and Skip
    skp7   => 0703341,		# skip if processor is PDP-7!
    caf    => 0703302,		# Clear All Flags

    crsf   => 0706701,		# skip if CDR is ready
    crrb   => 0706712,		# read CDR buffer
    crsa   => 0706704,		# select CDR alphanumeric mode
    crsb   => 0706744,		# select CDR binary mode

    # Operate Instructions

    # Group 1 (OPR 1) instructions
    opr    => 0740000,		# base operate instruction (nop)
    nop    => 0740000,
    cma    => 0740001,		# complement accumulator
    cml    => 0740002,		# complement link
    oas    => 0740004,		# inclusive or accumulator switches
    ral    => 0740010,		# rotate (ac, link) left
    rar    => 0740020,		# rotate (ac, link) right
    hlt    => 0740040,		# HALT
    xx     => 0740040,
    sma    => 0740100,		# skip on minus accumulator
    sza    => 0740200,		# skip on zero accumulator
    snl    => 0740400,		# skip on non-zero link

    skp    => 0741000,		# unconditional skip
    spa    => 0741100,		# skip on positive accumulator
    sna    => 0741200,		# skip on negative accumulator
    szl    => 0741400,		# skip on zero link

    rtl    => 0742010,		# rotate two left (ral*2)
    rtr    => 0742020,		# rotate two right (rar*2)

    cll    => 0744000,		# clear link
    stl    => 0744002,		# set link
    rcl    => 0744010,		# clear link, rotate left
    rcr    => 0744020,		# clear link, rotate right

    cla    => 0750000,		# clear accumulator
    clc    => 0750001,		# clear and complement acc
    las    => 0750004,		# load acc from switches
    glk    => 0750010,		# get link

    # Group 2 operate
    law    => 0760000,		# load accumulator with (instruction)
    lam    => 0777777,		# (load accumulator minus)
);

# Parse all the files
foreach my $file (@ARGV) {
    parse_file($file);
}

# Now do it all again, pass two
$Var{'.'} = 0;
$stage = 2;
print("PASS 2\n") if ($debug);
foreach my $file (@ARGV) {
    parse_file($file);
}

if ($format eq 'a7out') {
    # print out the contents of memory
    for my $i ( 0 .. $#Mem ) {
	if ( defined( $Mem[$i] ) ) {
	    printf( "%06o: %06o\t%s\n", $i, $Mem[$i], $Mline[$i] || "" );
	}
    }
}
elsif ($format eq 'list') {
    print "\n";
    print "Labels:\n";
    foreach my $key (sort keys %Label) {
	printf("%-8.8s %#06o\n", $key, $Label{$key});
    }
}
else {
    die("unknown format $format");
}
exit($errors);

# report an assmebly error:
# sets error flag
# reports filename:lineno for emacs m-x compile
sub err {
    $line_error = shift;
    my $msg = shift;

    $errors = 1;		# exit status
    if ($stage == 2) {
	print STDERR "$file:$lineno: $msg\n";
	print "$file:$lineno: $msg\n" if (! -t STDOUT);
    }
    return 0;			# expression value
}

# Open and parse the given file
sub parse_file {
    $file = shift;
    open( my $IN, "<", $file ) || die("Cannot read $file: $!\n");
    $lineno = 0;
    while ( $line = <$IN> ) {
	$lineno++;
        chomp($line);		# Lose the end of line
        $origline = $line;
	print "\t\t$line\n" if ($stage == 2 && $line ne '' && $format eq 'list');
	parse_line();
    }
    close($IN);
}

# process a label and set its value to the location counter
# only called on pass 1;
# if called on pass 2, should check if values are identical
sub process_label {
    my $label = shift;

    # It's a relative label, save its current value in a list
    if ( $label =~ m{^\d+$} ) {
        push( @{ $Rlabel{$label} }, $Var{'.'} );
        printf( "Pushing %#o for relative label %s\n", $Var{'.'}, $label ) if ($debug);
        return;
    }

    # It's a textual label, check if it's been defined before
    if ( defined( $Label{$label} ) ) {
	err('M', "Label $label defined multiple times\n");
    }
    # Otherwise, save its value
    $Label{$label} = $Var{'.'};
    printf( "Set label %s to %#o\n", $label, $Label{$label} ) if ($debug);
}

# Blame Phil for this....
# parses global $line based on prefixes, nibbling of a bit at a time
# (: and ; can appear in char literals)
# handles multiple ';' separated words per line
sub parse_line {
    # Lose any leading whitespace
    $line =~ s{^\s*}{};

    while (1) {
	$line_error = ' ';	# clear listing error indicator

	return if ($line eq '' || $line =~ m{^"}); # empty or comment: quit

	print "parse_line: '$line'\n" if ($debug);

	while ($line =~ s{^([a-z0-9\.]+):\s*}{}) { # labels
	    my $label = $1;

            # First pass: parse the labels
	    # (could check for same value on pass 2)
            if ( $stage == 1 ) {
		process_label($1);
	    }
	}

	if ( $line =~ s{^(\S+)\s*=}{}) { # assignment
	    my $lhs = $1;
	    my $word = parse_expression();
	    printf( "Setting variable %s to 0%o\n", $lhs, $word ) if ($debug);
	    $Var{$lhs} = $word;
	    printf("\t%06o %s\n", $word, $line_error) if ($stage == 2 && $format eq 'list');
	}
	else {	# bare expression
	    # Get its value on pass two and save to memory
	    # Also save the input line that altered memory
	    my $word = parse_expression();
	    if ( $stage == 2 ) {
		my $location  = $Var{'.'};
		$Mem[$location]   = $word;
		$Mline[$location] = $origline;
		$origline = '';
		if ($format eq 'list') {
		    printf( "%06o: %06o %s\n", $location, $word, $line_error);
		}
	    }
	    # Move up to the next location in both passes
	    $Var{'.'}++;
	} # expr

	# eat trailing whitespace and ";", if any
	$line =~ s{^\s*;?}{};
    } # while
}

# Blame Phil for this bit too...
# Parse an expression off $line and return a PDP-7 word
# as a series of whitespace separated "syllables"
# ORed, added, or subtracted
sub parse_expression {
    my $word = 0;

    print "expression: '$line'\n" if ($debug);

    while (1) {
	my $syllable = 0;
	my $op = '|';

	$line =~ s{^\s+}{};

	if ($line eq '' || $line =~ m{^[";]}) { # EOL ; and " terminate expr
	    printf("\tparse_expression => %#o\n", $word) if ($debug);
	    return $word;
	}

	print "    '$line'\n" if ($debug);

	if ($line =~ s{^-}{}) {
	    $op = '-';
	}
        elsif ($line =~ s{^\+}{}) {
	    $op = '+';
	}

	if ($line =~ s{^<(.)}{}) { # <char
	    $syllable = ord($1) << 9;
	}
	elsif ($line =~ s{^(.)>}{}) { # char>
	    $syllable = ord($1)
	}
	elsif ($line =~ s{^>(.)}{}) { # >char !!
	    $syllable = ord($1)
	}
	elsif ($line =~ s{^([a-z\.][a-z0-9\.]*)}{}) {
	    my $sym = $1;
	    print "\tsym: $sym\n" if ($debug);
	    if (defined($Var{$sym})) {
		$syllable = $Var{$sym};
		printf("\tvar: %s: %#o\n", $sym, $syllable) if ($debug);
	    }
	    elsif (defined($Label{$sym})) {
		$syllable = $Label{$sym};
		printf("\tlbl: %s: %#o\n", $sym, $syllable) if ($debug);
	    }
	    elsif ($stage == 2) {
		err('U', "$sym not defined")
	    } # pass 2
	} # symbol
	elsif ( $line =~ s{^(\d+)([fb])}{} ) { # relative label
	    $syllable = find_relative_label( $1, $2 ) if ($stage == 2);
	}
	elsif ( $line =~ s{^(\d+)}{} )  {
	    my $value = $1;
	    if ( $value =~ m{^0} ) {
		$syllable = oct($value);
	    }
	    else {
		$syllable = $value + 0;
	    }
	    $syllable &= 0777777;
	}
	else {
	    # From the BSD fortune file:
	    # Ken Thompson has an automobile which he helped design.
	    # Unlike most automobiles, it has neither speedometer,
	    # nor gas gauge, nor any of the numerous idiot lights
	    # which plague the modern driver. Rather, if the driver
	    # makes any mistake, a giant "?" lights up in the center
	    # of the dashboard. "The experienced driver",
	    # he says, "will usually know what's wrong.
	    err('?', "huh? '$line'");
	    $line = '';		# abort processing
	    return $word;
	}
	if ($op eq '+') {
	    $word += $syllable;
	}
	elsif ($op eq '-') {
	    $word -= $syllable;
	}
	else {
	    $word |= $syllable;
	}
	$word &= 0777777;
	printf("\tsyllable: %#o word: %#o\n", $syllable, $word) if ($debug);
    }
}

# Given a relative label number and a direction,
# return the location of this relative label or
# die if we don't have one
sub find_relative_label {
    my ( $label, $direction ) = @_;
    my $curlocation = $Var{'.'};

    # Error check: no labels at all
    if ( !defined( $Rlabel{$label} ) ) {
	return err('U', "relative label $label not defined\n");
    }

    # Get the list of possible locations for this label
    my $locarray = $Rlabel{$label};

    # Error check: no locations
    return err('U', "No relative labels") if ( @{$locarray} == 0 );

    if ( $direction eq 'f' ) {
	# Search forward for first location larger then the current one
        foreach my $reflocation ( @{$locarray} ) {
	    printf("forward %#o %#o\n", $reflocation, $curlocation) if ($debug);
            return ($reflocation) if ( $reflocation > $curlocation );
        }
    }
    else {
        # Search backwards for first location smaller than the current one
        foreach my $reflocation ( sort( { $b <=> $b } @{$locarray} ) ) {
	    printf("backward %#o %#o\n", $reflocation, $curlocation) if ($debug);
            return ($reflocation) if ( $reflocation < $curlocation );
        }
    }
    return err('U', "No relative label $label");
}
