The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!/usr/bin/env perl
# Copyright (c) 2015 Christian Jaeger, copying@christianjaeger.ch
# This is free software. See the file COPYING.md that came bundled
# with this file.
use strict;
use warnings FATAL => 'uninitialized';
# find modules from functional-perl working directory (not installed)
use Cwd 'abs_path';
our ($mydir, $myname);
BEGIN {
my $location = (-l $0) ? abs_path($0) : $0;
$location =~ /(.*?)([^\/]+?)_?\z/s or die "?";
($mydir, $myname) = ($1, $2);
}
use lib "$mydir/../lib";
use Chj::xopen qw(xopen_read xopen_write);
use Chj::TEST ":all";
use Chj::xIOUtil qw(xgetfile_utf8);
#use FP::Repl::Trap; $SIG{INT}=sub{ die "SIGINT" };
sub usage {
print "usage: $myname in out
expand `tail ` syntax in Perl code using `Sub::Call::Tail`, so that it
doesn't depend on that module anymore.
This is just a crude hack (totally imprecise).
";
exit 1;
}
our $verbose = 0;
our $opt_repl;
GetOptions(
"verbose" => \$verbose,
"help" => sub {usage},
"repl" => \$opt_repl,
#"dry-run"=> \$opt_dry,
) or exit 1;
usage unless @ARGV == 2;
our ($inpath, $outpath) = @ARGV;
our $code = xgetfile_utf8($inpath);
our $IDENT = qr/\w+(?:::\w+)*/;
sub translate {
my ($c) = @_;
#warn "translate: <$c>";
$c =~ s/\s+\z//s; # XX killing line numbering
$c =~ s/^\s+//s; # dito?
my @p;
if ($c =~ /\#/) {
undef
} elsif (@p = split /->/, $c and @p == 2) {
my ($before, $after) = @p;
'@_=' . $after . '; goto &{' . $before . '}'
} elsif ($c =~ s/^\&//) {
if (my ($ident, $args) = $c =~ m/^(\$${IDENT})\s*(\(.*)/s) {
'@_=' . $args . '; goto &' . $ident
} else {
die "dunno about '$c'";
}
} elsif (my ($ident, $args) = $c =~ m/^($IDENT)\s*(\(.*)/s) {
'@_=' . $args . '; goto \&' . $ident
} else {
undef
}
}
TEST { translate '&$odd ($n - 1)' . "\n\t " }
'@_=($n - 1); goto &$odd';
TEST { translate 'Weakened($even)->($n)' }
'@_=($n); goto &{Weakened($even)}';
TEST {
translate ' &$then
($$config{downcaps} && is_allcaps ($2) ? $1.lc($2).".xhtml"
: $path0)'
}
'@_=($$config{downcaps} && is_allcaps ($2) ? $1.lc($2).".xhtml"
: $path0); goto &$then';
use FP::Div 'min';
sub min_maybe {
min grep { defined $_ } @_
}
# register positions of the lines, and their indentation
sub get_line_position_and_indents {
my $line_position_and_indents = [];
my $lineno = -1; # 0-based index, *not* what editors expect
while ($code =~ /(?:^|\n)([ \t]*)/g) {
$lineno++;
my $indentstr = $1;
my $pos = pos($code);
# the pos where that line starts:
my $pos0 = $pos - length($indentstr);
my $i = 0;
for (split //, $indentstr) {
if ($_ eq ' ') {
$i++
} elsif ($_ eq "\t") {
# 8-based tabs
$i = (int($i / 8) + 1) * 8
} else {
die "??"
}
}
push @$line_position_and_indents, [$lineno, $pos0, $i];
}
$line_position_and_indents
}
our $line_position_and_indents = get_line_position_and_indents;
sub find_line_by_pos {
my ($pos) = @_;
# XX would need binary search for efficiency.
my $prevline = $$line_position_and_indents[0];
for (@$line_position_and_indents[1 .. $#$line_position_and_indents]) {
my ($lineno, $pos0, $i) = @$_;
return $prevline if $pos < $pos0;
$prevline = $_;
}
return $prevline # (don't have len of that line to check, thus trust)
}
# expand the 'tail' keyword right before pos in $code, set pos to
# afterwards.
sub expand_tail_at_pos {
my $pos = pos($code);
#warn "expand_tail_at_pos $pos";
# Where is the end of the arguments? Either when encountering a
# ";", or a line with indent the same or smaller than the current
# line, whichever comes first.
my $maybe_endpos_semicolon = pos($code) - 1 if $code =~ /;/g;
# -1 so as to leave the ';' in *afterwards*.
my ($tailline_lineno, $tailline_pos0, $tailline_i)
= @{ find_line_by_pos $pos};
my $afterline;
for my $lineno ($tailline_lineno + 1 .. $#$line_position_and_indents) {
$afterline = $$line_position_and_indents[$lineno];
last if $$afterline[2] <= $tailline_i;
}
my $maybe_endpos_indent = $$afterline[1] - 1 if $afterline;
# -1 so as to leave the "\n" in.
my $maybe_endpos = min_maybe($maybe_endpos_semicolon, $maybe_endpos_indent);
if (defined $maybe_endpos) {
my $endpos = $maybe_endpos;
my $args = substr $code, $pos, $endpos - $pos;
if (defined(my $replacement = translate $args)) {
my $startpos = $pos - 4;
substr $code, $startpos, $endpos - $startpos, $replacement;
# re-init index. XX nonscalable of course.
$line_position_and_indents = get_line_position_and_indents;
pos($code) = $startpos + length $replacement;
#warn "right: '$args'";
} else {
#warn "wrong1: '$args'";
pos($code) = $pos + 1;
}
} else {
warn "wrong2"
}
}
if ($opt_repl) {
require Chj::Backtrace;
require FP::Repl;
FP::Repl::repl();
exit;
} else {
# be insensitive to 'tail ' mentioned in comments; so bad. but
# lookbehind are not variable width, and setting pos($code) from
# within a substitution does not work.
$code =~ s=\n[\t ]*#[^\n]*\btail [^\n]*[^\n]=\n\n=sg;
# Instead of writing a various kinds of parens and various kinds
# of quoting parser, look at indentation: after newlines allow
# only more indentation than the line where the tail statement is
# found on has.
while ($code =~ m/(?<!\$)\btail\b/g) {
expand_tail_at_pos
}
$code =~ s/\buse\s*Sub::Call::Tail\b.*?;//s; # XX kills line numbering
my $o = xopen_write $outpath;
$o->xprint($code);
$o->xclose;
chmod 0755, $outpath if -x $inpath;
}