#!/usr/bin/env perl

use strict;
use warnings;
use Getopt::Long;

#-----------------------------------------------------------------------
# Compile a small C program containing a sequence of assembler
# instructions into an executable that does not need a dynamic linker.
# Very handy for debugging new insns or small snippets without going
# through the multi-pass process of figuring out which SB contains the
# code we're interested in.
#
# Step #1: s390-runone -t bla.c
#
# Creates a template which looks like so:
#
#    int main(void)
#    {
#      asm volatile ("csch");          // begin mark: do not remove
#      //FIXME: insert test code here:
#      asm volatile ("lghi %r1,1");    // __NR_exit
#      asm volatile ("lghi %r2,0");    // return code = 0
#      asm volatile ("svc 0");         // terminate process
#      asm volatile ("csch");          // end mark: do not remove
#      return 0;
#    }
#
# Step #2: Replace the FIXME line with one or more asm statements.
#          Skip this step if you used --insn=... in step #1.
#
# Step #3: s390-runone --build bla.c
#
# Compiles and links "bla.c" into an executable "bla" which does not
# require the dynamic loader and which contains only those insns between
# the two marker insns (csch).
#
# objdump -d:
#
# 00000000010000b0 <_start>:
# 10000b0:	a7 19 00 01       	lghi	%r1,1
# 10000b4:	a7 29 00 00       	lghi	%r2,0
# 10000b8:	0a 00             	svc	0
#
#-----------------------------------------------------------------------
#

&main;

sub usage {
    my $text =<<END

Usage:  s390-runone [options] FILE

   Options:
     --template   Write template
     --build      Build executable from C file
     --insn INSN  Add INSN to template
     --arch ARCH  Passed as -march=ARCH to the compiler
     --cc COMP    Use COMP as compiler
     --help       Write this text

   FILE  is mandatory with --build

END
    ;
    print $text;
    exit 0;
}

sub main {
    my $template = 0;
    my $build    = 0;
    my $help     = 0;
    my $insn     = "";
    my $arch     = "arch14";
    my $cc       = "gcc";

    GetOptions("template|t" => sub { $template = 1; $build = 0; },
               "build|b"    => sub { $template = 0; $build = 1; },
               "help|h"     => \$help,
               "insn|i=s"   => \$insn,
               "arch|a=s"   => \$arch,
               "cc=s"       => \$cc
        ) or usage();

    usage() if ($help);

    my $num_arg = $#ARGV + 1;
    my $file = "";

    if ($num_arg != 1) {
        fatal("Missing file name") if ($build);
    } else {
        $file = $ARGV[0];
    }

    my $rc = 0;
    if ($template) {
        write_template($file, $insn);
    } elsif ($build) {
        $rc = build_exe($file, $arch, $cc);
    } else {
        print "Nothing happens\n";
    }
    exit $rc;
}

sub write_template
{
    my ($file, $insn) = @_;
    my $asm;

    if ($insn eq "") {
        $asm = "//FIXME: insert test code here:";
    } else {
        $asm = "asm volatile (\"$insn\");";
    }

    my $template = <<END2
int main(void)
{
  asm volatile ("csch");          // begin mark: do not remove
  $asm
  asm volatile ("lghi %r1,1");    // __NR_exit
  asm volatile ("lghi %r2,0");    // return code = 0
  asm volatile ("svc 0");         // terminate process
  asm volatile ("csch");          // end mark: do not remove
  return 0;
}
END2
    ;
    if ($file ne "") {
        open(OUT, ">$file") || fatal("Cannot open '$file': $!\n");
        print OUT $template;
        close(OUT);
    } else {
        print $template;
    }
}

sub build_exe
{
    my ($file, $arch, $cc) = @_;

    my $base = `basename "$file" .c`;
    chomp($base);
    my $asm  = "$base.s";
    my $exe  = "$base";

# Compile the testprogram to assembler
    my $stderr = `$cc -S -fno-ident -march=$arch  $file 2>&1`;
    if ($? != 0) {
        error("$cc: Compilation failed\n    $stderr");
        return 1;
    }
    `mv "$asm" "$asm.orig"`;     # save before massaging

# Massage assembler file:
# - rename main ---> _start
# - remove cfi stuff
# - remove comment lines
# - remove insns preceeding 1st mark (csch)
# - remove insns succeeding 2nd mark (csch)

    my $in_main = 0;
    my $mark_seen = 0;
    my $output = "";
    open(IN, "$asm.orig") || fatal("Cannot open '$file': $!\n");
    while (my $line = <IN>) {
        chomp($line);
        next if ($line =~ /^\s*#/);  # comment
        next if ($line =~ /\.cfi_/); # cfi stuff
        if ($in_main == 0) {
            $in_main = 1 if ($line =~ /^main:/);
        } else {
            if ($mark_seen == 0) {
                if ($line =~ /csch/) {
                    $mark_seen = 1;
                    next;
                }
                next if ($line =~ /^\t[a-z]/);  # skip insn
            } else {
                if ($line =~ /csch/) {
                    $mark_seen = 0;
                    next;
                }
            }
        }
        $line =~ s/main/_start/g;
        $output .= "$line\n";
    }
    open(OUT, ">$asm") || fatal("Cannot open '$asm': $!\n");
    print OUT $output;
    close(OUT);

    # Assemble file and link to executable
    my $ccc = "$cc -static -Wa,--fatal-warnings -Wl,--build-id=none"
            . " -nodefaultlibs -nostartfiles";
    $stderr = `$ccc "$asm" -o "$exe" 2>&1`;

    if ($? != 0) {
        error("$cc: Linking executable failed");
        for my $line (split /\n/,$stderr) {
            print STDERR "$line\n" if ($line !~ /treating warnings as errors/);
        }
        return 1;
    }
}

sub error
{
    print STDERR "*** $_[0]\n";
}

sub fatal
{
    error($_[0]);
    exit 1;
}
