summaryrefslogtreecommitdiffstats
path: root/lib/Eval/Closure.pm
blob: 5094d0a2768faa92f304569ee89bf9310d932f34 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package Eval::Closure;
use strict;
use warnings;
use Sub::Exporter -setup => {
    exports => [qw(eval_closure)],
    groups  => { default => [qw(eval_closure)] },
};

use Carp;
use overload ();
use Scalar::Util qw(reftype);
use Try::Tiny;

sub eval_closure {
    my (%args) = @_;

    $args{source} = _canonicalize_source($args{source});
    _validate_env($args{environment} ||= {});

    $args{source} = _line_directive($args{description}) . $args{source}
        if defined $args{description};

    my ($code, $e) = _clean_eval_closure(@args{qw(source environment name)});

    croak("Failed to compile source: $e\n\nsource:\n$args{source}")
        unless $code;

    return $code;
}

sub _canonicalize_source {
    my ($source) = @_;

    if (defined($source)) {
        if (ref($source)) {
            if (reftype($source) eq 'ARRAY'
             || overload::Method($source, '@{}')) {
                return join "\n", @$source;
            }
            elsif (overload::Method($source, '""')) {
                return "$source";
            }
            else {
                croak("The 'source' parameter to eval_closure must be a "
                    . "string or array reference");
            }
        }
        else {
            return $source;
        }
    }
    else {
        croak("The 'source' parameter to eval_closure is required");
    }
}

sub _validate_env {
    my ($env) = @_;

    croak("The 'environment' parameter must be a hashref")
        unless reftype($env) eq 'HASH';

    for my $var (keys %$env) {
        croak("Environment key '$var' should start with \@, \%, or \$")
            unless $var =~ /^([\@\%\$])/;
        croak("Environment values must be references, not $env->{$var}")
            unless ref($env->{$var});
    }
}

sub _line_directive {
    my ($description) = @_;

    return qq{#line 1 "$description"\n};
}

sub _clean_eval_closure {
    # my ($source, $__captures, $name) = @_
    my $__captures = $_[1];

    local $@;
    local $SIG{__DIE__};

    if ($ENV{EVAL_CLOSURE_PRINT_SOURCE}) {
        _dump_source(_make_source(@_), $_[2]);
    }

    my $code = eval _make_source(@_);
    my $e = $@;

    if (defined($code) && (!ref($code) || ref($code) ne 'CODE')) {
        $e = "The 'source' parameter must return a subroutine reference, "
           . "not $code";
        undef $code;
    }

    return ($code, $e);
}

sub _make_source {
    my ($source, $__captures) = @_;
    return join "\n", (
        (map {
            'my ' . $_ . ' = '
                . substr($_, 0, 1) . '{$__captures->{\'' . $_ . '\'}};'
         } keys %$__captures),
        $source,
    );
}

sub _dump_source {
    my ($source, $name) = @_;

    my $output;
    if (try { require Perl::Tidy }) {
        Perl::Tidy::perltidy(
            source      => \$source,
            destination => \$output,
        );
    }
    else {
        $output = $source;
    }

    $name = defined($name) ? $name : "__ANON__";
    warn $name . ":\n" . $output . "\n";
}

1;