# This module exports a single subroutine named "SIL" (for Spesh Inline Log).
# When this sub is called, returns a MoarVM::SIL object that can be queried
# for the result of the Spesh Inline Log for the executed program.  Or it
# returns Nil when it is being run inside the program producing the Spesh
# Inline Log.
#
# When introspection of the SIL object is done, the "exit" method should be
# called on the object to prevent the code of the program actually running
# twice.
# 
# A typical use case would be:
#
#  use MoarVM::SIL;
#
#  if SIL() -> $SIL {
#      LEAVE $SIL.exit;
#
#      # do testing, or just show the report:
#      say $SIL.report;
#  }

class BB {
    has Str() $.name is built(:bind);
    has Int() $.id   is built(:bind);
    has Int() $.size is built(:bind);
    has Bool  $.specialized is built(False);

    my constant $prefix = 'unspecialized ';
    my constant $offset = $prefix.chars;

    method TWEAK() {
        if $!name.starts-with($prefix) {
            $!name        := $!name.substr($offset);
            $!specialized := False;
        }
        else {
            $!specialized := True;
        }
    }

    method description() {
        $!name ?? "$!name BB($!id)" !! "BB($!id)"
    }

    method gist() { self.Str }
    method Str() {
        $!size ?? "$.description.chop(), $!size bytes)" !! $.description
    }
    method WHICH(--> ValueObjAt:D) {
        ValueObjAt.new("BB|$!id")
    }
}

class Inlined {
    has BB $.inlinee is built(:bind);
    has BB $.into    is built(:bind);

    method gist() { self.Str }
    method Str() { "$!inlinee -> $!into" }
    method WHICH(--> ValueObjAt:D) {
        ValueObjAt.new("BB|$!inlinee.WHICH()|$!into.WHICH()")
    }
}

class Not-Inlined {
    has BB    $.frame  is built(:bind);
    has BB    $.target is built(:bind);
    has Str() $.reason is built(:bind);

    method gist() { self.Str }
    method Str() { "$!frame -> $!target:\n      $!reason" }
    method WHICH(--> ValueObjAt:D) {
        ValueObjAt.new("BB|$!frame.WHICH()|$!target.WHICH()")
    }
}

class MoarVM::SIL {
    has Bag $.inlineds     is built(:bind);
    has Bag $.not-inlineds is built(:bind);
    has Int $!status       is built(:bind);

    method sink() { say self.report }
    method report() {
        my str @lines;

        @lines.push: "Spesh Inline Log Report of Process #$*PID ({
            now.DateTime.truncated-to('second')
        })";
        @lines.push: "Executing: " ~ Rakudo::Internals.PROGRAM;
        @lines.push: "";

        @lines.push: "Successful inlines";
        @lines.push: "-" x 80;
        @lines.push("{
            $_ == 1 ?? '   ' !! .fmt('%2dx') given $!inlineds{$_}
        } $_.gist()") for $!inlineds.keys.sort: +*.inlinee.id;
        @lines.push: "-" x 80;

        @lines.push: "";
        @lines.push: "Unsuccessful inlines:";
        @lines.push: "-" x 80;
        @lines.push("{
            $_ == 1 ?? '   ' !! .fmt('%2dx') given $!not-inlineds{$_}
        } $_.gist()") for $!not-inlineds.keys.sort: +*.frame.id;
        @lines.push: "-" x 80;

        @lines.join("\n");
    }

    method inlined-by-name($name) {
        $!inlineds.keys.grep: *.inlinee.name eq $name
    }

    method not-inlined-by-name($name) {
        $!not-inlineds.keys.grep: *.frame.name eq $name
    }

    method gist() { self.report }
    method Str()  { self.report }

    method exit() { exit $!status }
}

sub SIL is export {

    my $proc := Rakudo::Internals.RERUN-WITH(MVM_SPESH_INLINE_LOG => 1);
    return Nil unless $proc;

    my $out := $*OUT;
    my $err := $*ERR;
    my @inlineds;
    my @not-inlineds;
    my $status;

    react {
        whenever $proc.stdout.lines {
            $out.say($_);
        }
        whenever $proc.stderr.lines {
            if m/^ 'Can inline ' (<-[(]>+)
                   '(' (\d+)
                   ') with bytecode size ' (\d+)
                   ' into ' (<-[(]>+)
                   '(' (\d+) /
            {
                @inlineds.push: Inlined.new:
                  inlinee => BB.new(name => $0.chop, id => $1, size => $2),
                  into    => BB.new(name => $3.chop, id => $4);

            }
            elsif m/^ 'Can NOT inline ' (<-[(]>+)
                   '(' (\d+)
                   ') with bytecode size ' (\d+)
                   ' into ' (<-[(]>+)
                   '(' (\d+) 
                   '): ' (.*) /
            {
                @not-inlineds.push: Not-Inlined.new:
                  frame  => BB.new(name => $0.chop, id => $1, size => $2),
                  target => BB.new(name => $3.chop, id => $4),
                  reason => $5;
            }
            else {
                $err.say($_);
            }
        }
        whenever $proc.start(:%*ENV) {
            $status = .exitcode;
        }
    }

    MoarVM::SIL.new(
      inlineds     => @inlineds.Bag,
      not-inlineds => @not-inlineds.Bag,
      status       => $status,
    )
}

# vim: expandtab shiftwidth=4
