The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl

#------------------------------------------------------------------------------
# $Id: build_zx48_dz80.pl,v 1.1 2010/11/21 22:08:26 Paulo Exp $
# Generate a benchmark diassembly of zx48.rom by dZ80c.exe
# - converts hex dump to upper case
# - use $HHHH for hex constants
# - use decimal base in (ix+DIS)
# - do not decode invalid instructions ED.. and DD..
#------------------------------------------------------------------------------

use strict;
use warnings;

use File::Basename;
use File::Slurp;
use File::Copy;


my @delete_files;
END { @delete_files and unlink @delete_files };

my $DIS_CMD = 'dZ80c.exe ! -q -p="<<--" -y="-->>" -d="defb" ';


@ARGV==2 or die "Usage: ",basename($0)," INPUT_BINARY OUTPUT_DISASSEMBLY\n";
my($input, $output) = @ARGV;
my $output_temp = "$output~"; push @delete_files, $output_temp;

open(my $out, ">", $output_temp) or die "write $output: $!\n";

my $in = dis_stream($input);
while (my $line = $in->()) {
	print $out $line;
}
close $out;
move $output_temp, $output;

print "Created $output\n";


#------------------------------------------------------------------------------
# return a stream to return disassembled lines
sub dis_stream {
	my($file, $addr, $header, $length) = @_;
	
	my $index = qr/\b(?:ix|iy)\b/i;
	my $hex = qr/[0-9a-f]+/i;
	
	my @in;		# list of input handles
	push @in, dis_handle($file, $addr, $header, $length);
	
	return sub {
		for (;;) {
			return undef unless @in;
			my $in = $in[-1]; local $_ = <$in>; 
			unless (defined $_) { 
				close($in) or die;
				pop @in; 
				next; 
			}
			
			# convert dump to upper case, ignore lines without dump
			next unless s/^($hex)(\s+)($hex)/uc($1).$2.uc($3).' '/ige;
			
			# reduce space between opcode and args
			s/^($hex\s+$hex\s+\w+)[ \t]+/$1 /ig;
			s/ +$//;
			
			# convert hex constants to 0xHHHH
			s/<<--($hex)-->>/'0x'.uc($1)/ige;
						
			# convert ix/iy offsets to decimal
			# s/($index)\+0x($hex)/ hex($2) > 0 ? $1.'+'.hex($2) : $1 /ige;
			# s/($index)\-0x($hex)/ hex($2) > 0 ? $1.'-'.hex($2) : $1 /ige;
			s/($index)\+0x00/$1/ig;			# (ix+0) -> (ix)
			
			# convert DD00/FD00/ED00 created by a single byte file to defb
			s/^($hex\s+)(DD|FD)00(\s+)nop.*/$1$2$3  defb 0x$2/;
			s/^($hex\s+ED)00(\s+defb\s+0xED).*; Undocumented 8 T-State NOP.*/$1  $2/;
			
			# DD not followed by ix: dump a defb and continue on next address
			# FD not followed by iy: dump a defb and continue on next address
			if (/^($hex)\s+DD$hex/ && ! /ix/ ||
				/^($hex)\s+FD$hex/ && ! /iy/ ||
				/^($hex)\s+ED.*; Undocumented 8 T-State NOP/) {
				my $addr = hex($1);
				
				# discard current handle and create new ones
				1 while (<$in>); pop @in;
				push @in, dis_handle($file, $addr+1, $addr+1);
				push @in, dis_handle($file, $addr,   $addr,  1);
				next;
			}
			
			return $_;
		}
	};
}

sub dis_handle {
	my($file, $addr, $header, $length) = @_;
	
	$addr		||= 0;
	$header		||= 0;
	$length		||= (-s $file) - $header;
	
	# make temporary file for this segment
	my $temp_file = "$file.$addr.$header.$length.bin~"; 
	push @delete_files, $temp_file;
	
	my $data = read_file($file, binmode => ':raw');
	write_file($temp_file, {binmode => ':raw' },
			   substr($data, $header, $length));	
	
	open(my $in, $DIS_CMD."-m=$addr $temp_file |") 
		or die "dZ80c.exe: $!\n";
	return $in;
}