Peter Scott wrote:
>
> Tony Olekshy wrote:
> >
> > In fact, not only would I be pleased and honoured to author the
> > Perl 6 core Try.pm module, I'm already working on a Perl 5 standard
> > reference implementation.
>
> > Peter, I think we should make this approach more clear in RFC 88.
>
> I'm not convinced that this can totally be implemented in a
> module.
#- File Try.pm -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
#
# Subject: Structured Exception Handling Mechanism for Perl 6
#
# Purpose: Perl 5 Reference Implementation of RFC 88 functions.
#
# This is a self-documenting self-testing implementation, in
# Perl 5, of the functionality referred to in Perl 6 RFC 88,
# with syntax modified as required by Perl 5. Save in Try.pm.
#
# Author: Tony Olekshy
# Principal Software Architect
# Avra Software Lab Inc.
#
# Copyright: Avra Software Lab Inc, 1999-2000.
#
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
use strict; $Try::VERSION = "1.0.1.2"; # 2000-08-26
=head1 ABSTRACT
This document assumes a certain familiarity with [RFC-88].
26 regression tests can be found herein at /#- Regress/.
To run the regression tests, use one of:
perl -we "use Try regress => 1"
perl -we "use Try regress => 2"
perl -we "use Try regress => 1, test => 'syntax-1'"
perl -we "use Try regress => 2, test => 'syntax-1'"
If regress => !0 is specified, regression test are run at import
time. If regress => 2, detailed output is generated, otherwise
successes report as a single output line. If test => "syntax-1"
is specified, only test "syntax-1" is run (see /!syntax-1/ for
this example).
To run manual tests, perl -w test.pl, where test.pl looks like:
use strict;
use Try;
try sub { throw Exception "Foo" },
catch sub { print "$@[0]\n" };
=head1 DESCRIPTION
use Try;
exception 'Alarm';
try sub {};
try sub {}, catch sub {};
try sub {}, catch "Foo" => sub {};
try sub {}, catch "Foo", "Bar" => sub {};
try sub {}, catch sub {} => sub {};
try sub {}, finally sub {};
throw Alarm "Can't foo.";
=head1 IMPLEMENTATION
This reference implementation is written for readability,
testability, and distributability. A production version
would be optimized for performance. The syntax has been
chosen to make the reference implementation simple, not to
suggest a preferred syntax for the constructs of RFC 88.
The exception, try, catch, and finally "keywords" are exported
to the invoker's package. Try and Exception packages are
defined, the latter of which contains the base class for
exception objects.
The current unwind stack is kept in @@; $@[0] is current
exception, due to Perl 5 constraints on $@ inside eval.
While unwinding, $@ is join("\n",@@)."\n".
This implementation uses explicit subs in the syntax for try,
catch, and finally clauses, due to Perl 5 parser constraints.
Catch clauses that list exception class names take a list of
quoted-string class names, due to Perl 5 parser constraints.
Lexical scope is not shared between clauses, as per Perl 5.
Clauses are closures, not blocks, so @_ == () therein.
Clauses are eval'd, therefore $@ eq "" therein. Use $@[0].
Local gotos across unwind-semanitics blocks have not been tested.
Dead-code catch clauses are not currently detected by engine.
Note that: try {}; finally {}; will ignore the finally! It should
be written try {}, finally {}; This would, presumably, be solved
by parser extensions in Perl 6.
Full exception statement functionality is not yet available.
Mechanism hooks are not yet implemented.
=head1 ISSUES
Get rid of the sub on the sub {} terms.
$@ should equal $@[0] inside the clause blocks, not "".
=head1 REFERENCE
[RFC-88] http://tmtowtdi.perl.org/rfc/88.pod
[RFC-80] http://tmtowtdi.perl.org/rfc/80.pod
=cut
# Interface -------------------------------------------------------------
package Try;
sub exception { &_exception; return undef; }
sub try { return &_try; }
sub catch { return (_catch => @_); }
sub finally { return (_finally => @_); }
sub Hook { return &_Hook; }
package Exception;
sub new { $_[0]->_construct(@_[1..$#_]); }
sub throw { $_[0]->_throw( @_[1..$#_]); }
package main;
@@ = (); # Used for unwind-time exception stack.
# Implementation [ Try ] ------------------------------------------------
package Try;
$Try::Debug = 0; # Set this to include Try subs in traceback.
$Try::Trace = 0; # Set this to trace flow control logic.
$Try::Force = 0; # Set this to force regression temp file writes.
$Try::Temp = "=regress";# Temp file name for regression test capture.
my $_depth = 0; # Keeps track of nested trys for inits.
my $_stack = ""; # Formatted version of current @@.
sub import
{
# Invoked by "use Try" to install the Try mechanism into a package.
#
my ($pkg, %opt) = @_;
# Install new "keywords" into caller's package.
#
my $stash = do { no strict 'refs'; \%{caller(0)."::"} };
map { ${$stash}{$_} = $::{"Try::"}{$_} }
qw( exception try catch finally );
# Process options given to "use Try".
#
defined $opt{debug} and $Try::Debug ||= $opt{debug};
# Run regression tests if requested.
#
$opt{regress} and _regress($opt{regress}, $opt{test});
}
sub _exception
{
# Declares a class that interits from Exception.
#
$Try::Trace and print "exception: ", join(", ", @_), "\n";
#--> Complex exceptions are not yet implemented.
do { no strict 'refs'; @{"${_[0]}::ISA"} = ('Exception'); };
}
sub _try
{
# Structured exception handling engine: process try statement.
#
my (@args) = @_;
$Try::Trace and print "try: ", join(", ", @args), "\n";
$_depth++ or @@ = (); # Danger???
# Evaluate the try clause trapping exceptions.
#
my (@Result, $result);
wantarray ? (@Result = _handle(shift @args))
: ($result = _handle(shift @args));
# Process the try's catch and finally clauses:
#
my $skipping = 0; # Skipping catches until next finally.
Clause: while (@args) {
my $arg = shift @args;
# Look after catch clauses...
#
if ($arg eq "_catch") {
my @cNames = (); my ($test, $closure);
# Pick off class name args up to next closure.
#
while (@args && ref $args[0] ne "CODE") {
$arg = shift @args;
if (ref $arg) {
&_error("Expecting string, found \"$arg\".");
last Clause;
}
push @cNames, $arg;
}
# Pick off the catch closure.
#
if (ref $args[0] eq "CODE") {
$closure = shift @args;
# If two closures and no @cNames: test closure.
#
if (!@cNames and ref $args[0] eq "CODE") {
$test = $closure; $closure = shift @args;
}
}
else {
&_error("Expecting sub but found \"" .
(@args ? $args[0] : "<nothing>") . "\".");
last Clause;
}
# We have a catch clause!
#
# Do nothing if not unwinding or skipping catches.
#
next Clause if @@ == 0 || $skipping;
# Otherwise, determine whether or not to invoke...
#
if (@cNames) { # Check isa relationships.
next Clause unless grep { $@[0]->isa($_) } @cNames;
}
if ($test) { # Check the test closure.
my $before = @@; # To see if $closure dies.
my $t = _handle($test);
@@ > $before and $skipping = 1;
next Clause if $skipping || ! $t;
}
# If we got this far, invoke the catch closure.
#
my $before = @@; # To see if $closure dies.
_handle($closure);
@@ == $before and @@ = (); # Cleanly Caught!
$skipping = 1; # Skip clauses 'till finally.
}
# Look after finally clauses...
#
elsif ($arg eq "_finally") {
$skipping = 0; # Stop catch clause skipping.
my $closure = shift @args;
unless (ref $closure eq "CODE") {
&_error("Expecting sub but found \"$closure\".");
last Clause;
}
# Invoke the finally closure, exceptions simply stacked.
#
_handle($closure);
}
# Right, anything else and we're out'a here.
#
else {
&_error("Expecting catch or finally but found \"$arg\".");
last Clause;
}
}
# The try statement is complete. Handle unwinding stuff...
#
$_depth -= 1;
@@ or $_stack = "";
# Unless the unwind stack is empty, we are to unwind.
#
if (@@) {
local $::SIG{__DIE__} = sub {};
die ($_stack = join("\n", @@) . "\n");
}
# Else we haven't unwound; return result of the try clause.
#
return wantarray ? @Result : $result;
}
sub _handle
{
# Invoke a closure and trap any unwinding thereunder.
#
my ($closure, @rest) = @_; my (@Result, $result);
$Try::Trace > 1 and print "_handle: ", join(", ", @_), "\n";
unless (ref $closure eq "CODE") {
&_error("Expecting closure but found \"$closure\".");
return;
}
# Evaluate closure trapping unwinding.
#
local $::SIG{__DIE__} = \&_diewrap;
wantarray ? (@Result = eval { &{$closure}(@rest) })
: ($result = eval { &{$closure}(@rest) });
# Wrap exceptions raised by "die" (but not by "throw").
#
$@ && "$@" ne $_stack
and
&_push(Exception::Try::Wrap::Die->new("$@", data => $@));
return wantarray ? @Result : $result;
}
sub _diewrap
{
# A $::SIG{__DIE__} that handles die seperate from throw.
#
my ($e) = @_; my @args = ();
$Try::Trace and print "_diewrap: ", join(", ", @_), "\n";
!ref $e && $e =~ s/ at (.*? line \d+)//s
and
push(@args, debug => $1);
throw Exception::Try::Wrap::Die "$e", @args;
}
sub _throwhook
{
# Used by Exception class's throw to die in a detectable way.
#
my ($e) = @_;
# Make a note of the exception being thrown (on the unwind stack).
#
&_push($e);
# And unwind.
#
local $::SIG{__DIE__} = sub {};
die ($_stack = join("\n", @@) . "\n");
}
sub _error
{
# Convenience routine used by try to wrap syntax errors.
#
$Try::Trace and print "_error: ", join(", ", @_), "\n";
&_push(Exception::Try::Syntax->new($_[0]));
};
sub _push
{
# Invoked by try whenever it wants to stack an exception.
#
my ($e) = @_;
$Try::Trace > 1 and print "_push: ", ref $e, " [$e]\n";
ref $e && $e->isa("Exception")
or
$e = Exception::Try::Wrap->new("$e", data => $e);
unshift @@, $e;
$e->{trace} or $e->snapshot;
}
sub _Hook
{
# Perl 5 interface to on_catch_enter et al hooks.
#
# --> Not yet implemented.
#
$Try::Trace and print "Hook: ", join(", ", @_), "\n";
}
# Implementation [ Exception ] ------------------------------------------
package Exception;
@Exception::Try::ISA = ('Exception');
@Exception::Try::Syntax::ISA = ('Exception::Try');
@Exception::Try::Wrap::ISA = ('Exception::Try');
@Exception::Try::Wrap::Die::ISA = ('Exception::Try::Wrap');
sub _construct
{
bless {message => @_[1..$#_]}, ref $_[0] || $_[0];
}
sub _throw
{
$Try::Trace and print "throw: ", join(", ", @_), "\n";
my $I = shift; my $e = ref $I ? $I : $I->new(@_);
&Try::_throwhook($e); # Die in a detectable way.
}
use overload '""' => sub
{
return "[" . (ref $_[0]) . "] " . $_[0]->{message};
};
sub snapshot
{
my ($I) = @_; $I->{trace} = "";
for (my $level = 0; ; $level += 1) {
my @F = caller($level); @F or last;
next if !$Try::Debug && $F[1] =~ /Try.pm$/;
$I->{trace} .= "$F[3] called from $F[1]\[$F[2]].\n";
}
}
# Implementation [ Regression Tests ] -----------------------------------
package Try;
sub _regress
{
my ($level, $which) = @_; my $count = 0;
print "Try.pm $Try::VERSION Regression Tests...\n\n";
!$Try::Force && -f $Try::Temp
and
die "Temp file \"$Try::Temp\" already exists,";
select STDERR; $| = 1; print STDERR "";
select STDOUT; $| = 1; print STDOUT "";
$Try::_Tests =~ s/#.*//mg;
foreach my $test ( split(/^!/m, $Try::_Tests) ) {
# Parse out the test input and expected result.
#
next unless $test =~/\S/; $count += 1;
$test =~ s/(.*)\n\s+=expect\s*\n(.*)/$1/s
or
die "=expect section missing in test:\n$test";
my ($input, $expect) = ($1, $2);
$input =~ s/^(.*?)\n//s or die; my $title = $1;
$title =~ s/^(\S+)\s+(.*)/$2/s or die; my $tag = $1;
next if $which && $which ne $tag;
# Wrap the test code into its own little package.
#
(my $package = $tag) =~ s/\W/_/g;
$input = "package Try::Test::$package; "
. "use strict; use Try; \$Try::Trace = 0;\n"
. $input;
# Run the test code collecting print output into temp file.
#
open(SAVEOUT, ">&STDOUT") or die;
open(SAVEERR, ">&STDERR") or die;
0 and *SAVEOUT = *SAVEERR; # Avoid used only once warnings.
open(STDOUT, ">$Try::Temp")
or
die "Can't write to \"$Try::Temp\", $!";
eval $input; $@ and die "Test $tag died.\n$@";
close(STDOUT); open(STDOUT, ">&SAVEOUT");
close(STDERR); open(STDERR, ">&SAVEERR");
# Read back the test's print output.
#
local *RESULT;
open(RESULT, $Try::Temp)
or
die "Can't read \"$Try::Temp\", $!";
my $result = do { local $/; <RESULT> }; close RESULT;
unlink $Try::Temp or die;
# Fixups so that tests match normally variable stuff.
#
foreach ($input, $expect, $result) {
s/^\s+|\s+$//sg; s/^\s+//mg; # White space.
s/\b0x[0-9a-f]+/0xcafebabe/g; # Addresses.
s/\[\d+\]/[?]/g; # Line numbers.
s/\(eval \d+\)/(eval ?)/g; # Eval depth.
s/^(Try::import called) from.*/$1./m; # perl -e.
}
# Format up the results of this test...
#
my $match = $result eq $expect;
printf "=== %-4s %-12s %s\n",
$match ? "Ok:" : "Bad:", $tag, $title;
$level >= 2
and
print "=== Test:\n$input\n";
$match
or
print "=== Expected:\n$expect\n=== Received:\n$result\n\n";
$match && $level >= 2
and
print "=== Result:\n$result\n\n";
}
print "\nTry.pm Regression Tests Done ($count tests).\n";
}
#- Regression Tests --#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
$Try::_Tests = <<'_END_TESTS_';
!syntax-1 Basic syntax.
$Try::Trace = 1; # Show top-level statement "parsing".
exception 'Alarm';
try sub {};
try sub {}, catch sub {};
try sub {}, catch "Foo" => sub {};
try sub {}, catch "Foo", "Bar" => sub {};
try sub {}, catch sub {} => sub {};
try sub {}, finally sub {};
eval { throw Alarm "Can't foo."; }; print "$@\n";
=expect
exception: Alarm
try: CODE(0xcafebabe)
try: CODE(0xcafebabe), _catch, CODE(0xcafebabe)
try: CODE(0xcafebabe), _catch, Foo, CODE(0xcafebabe)
try: CODE(0xcafebabe), _catch, Foo, Bar, CODE(0xcafebabe)
try: CODE(0xcafebabe), _catch, CODE(0xcafebabe), CODE(0xcafebabe)
try: CODE(0xcafebabe), _finally, CODE(0xcafebabe)
throw: Alarm, Can't foo.
[Alarm] Can't foo.
!syntax-2 Basic syntax error.
eval { try "Foo"; }; print "$@\n";
=expect
[Exception::Try::Syntax] Expecting closure but found "Foo".
!syntax-3 Bad clause arguments.
eval { try sub {}, catch Foo => "Bar"; }; print "$@\n";
eval { try sub {}, catch [] => sub {}; }; print "$@\n";
eval { try sub {}, finally "Foo"; }; print "$@\n";
=expect
[Exception::Try::Syntax] Expecting sub but found "<nothing>".
[Exception::Try::Syntax] Expecting string, found "ARRAY(0xcafebabe)".
[Exception::Try::Syntax] Expecting sub but found "Foo".
!catch-1 Catch clause succeeds.
try sub {
try sub { throw Exception "1" },
catch sub { print "$@[0]\n"; };
},
catch sub {
print "BAD TEST: $@[0]\n";
};
=expect
[Exception] 1
!catch-2 Catch clause throws.
try sub {
try sub { throw Exception "1" },
catch sub { throw Exception "2" },
},
catch sub {
print join("\n", @@), "\n";
};
=expect
[Exception] 2
[Exception] 1
!die-1 Try clause dies.
try sub {
try sub { die "Exception 1" },
catch sub {
print "$@[0]", "\tContext: $@[0]->{debug}\n";
};
},
catch sub {
print "BAD TEST: $@[0]\n";
};
=expect
[Exception::Try::Wrap::Die] Exception 1.
Context: (eval ?) line 4
!die-2 Catch clause dies.
try sub {
try sub { die "Exception 1" },
catch sub { die "Exception 2" },
},
catch sub {
print join("\n", @@), "\n";
};
=expect
[Exception::Try::Wrap::Die] Exception 2.
[Exception::Try::Wrap::Die] Exception 1.
!cascade-1 Multiple catches, first is clean.
try sub { throw Exception "1"; },
catch sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
A: [Exception] 1
!cascade-2 Multiple catches, first throws.
try sub {
try sub { throw Exception "1"; },
catch sub { throw Exception "2"; },
catch sub { print "B: $@[0]\n"; };
},
catch sub {
print join("\n", @@), "\n";
};
=expect
[Exception] 2
[Exception] 1
!test-1 Conditional catches, first matches.
try sub {
throw Exception "1";
},
catch sub { 1 } => sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
A: [Exception] 1
!test-2 Conditional catches, first doesn't match.
try sub {
throw Exception "1";
},
catch sub { 0 } => sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
B: [Exception] 1
!test-3 Conditional catch, test throws.
try sub {
try sub {
throw Exception "1";
},
catch sub { throw Exception "2"; }
=> sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
},
catch sub {
print join("\n", @@), "\n";
};
=expect
[Exception] 2
[Exception] 1
!test-4 Conditional catch based on $@[0].
try sub {
my $x = 0; my $y = 1 / $x;
},
catch sub { $@[0] =~ /root of negative/ }
=> sub { print "A: $@[0]\n"; },
catch sub { $@[0] =~ /division by zero/ }
=> sub { print "B: $@[0]\n"; },
catch sub { print "C: $@[0]\n"; };
=expect
B: [Exception::Try::Wrap::Die] Illegal division by zero.
!isa-1 Catch by ISA matches.
try sub {
throw Exception "1";
},
catch "Exception" => sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
A: [Exception] 1
!isa-2 Catch by ISA doesn't match.
try sub {
throw Exception "1";
},
catch "Foo" => sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
B: [Exception] 1
!isa-3 Catch by ISA list matches.
try sub {
throw Exception "1";
},
catch "Foo", "Exception", "Bar" => sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
A: [Exception] 1
!isa-4 Catch by ISA list doesn't match.
try sub {
throw Exception "1";
},
catch "Foo", "Bar", "Baz" => sub { print "A: $@[0]\n"; },
catch sub { print "B: $@[0]\n"; };
=expect
B: [Exception] 1
!isa-5 Catch by ISA, middle catch matches.
try sub {
throw Exception "1";
},
catch "Foo" => sub { print "A: $@[0]\n"; },
catch "Exception" => sub { print "B: $@[0]\n"; },
catch "Bar" => sub { print "C: $@[0]\n"; },
catch sub { print "D: $@[0]\n"; };
=expect
B: [Exception] 1
!finally-1 Basic finally clause.
try sub { print "Foo" }, finally sub { print ", Bar.\n" };
=expect
Foo, Bar.
!finally-2 Finally clause, try throws.
try sub {
try sub { throw Exception "1" },
finally sub { print "Finally\n"};
},
catch sub { print join("\n", @@), "\n"; };
=expect
Finally
[Exception] 1
!finally-3 Finally clause, finally throws.
try sub {
try sub { throw Exception "1" },
finally sub { throw Exception "2" };
},
catch sub { print join("\n", @@), "\n"; };
=expect
[Exception] 2
[Exception] 1
!finally-4 Multiple finally clauses.
try sub {
try sub { throw Exception "1" },
finally sub { throw Exception "2" },
finally sub { throw Exception "3" },
finally sub { throw Exception "4" };
},
catch sub { print join("\n", @@), "\n"; };
=expect
[Exception] 4
[Exception] 3
[Exception] 2
[Exception] 1
!finally-5 Finally resets catch skipping.
try sub {
try sub { throw Exception "1" },
catch sub { throw Exception "2" },
catch sub { throw Exception "BAD" },
finally sub { throw Exception "3" },
catch sub { throw Exception "4" };
},
catch sub { print join("\n", @@), "\n"; };
=expect
[Exception] 4
[Exception] 3
[Exception] 2
[Exception] 1
!result-1 Basic return value.
my $result = try sub { "Foo" };
print "result: $result\n";
=expect
result: Foo
!trace-1 Basic Perl stack traceback.
try sub { throw Exception "1" },
finally sub { throw Exception "2" },
catch sub {
print join("\n", @@), "\n--\n", $@[0]->{trace} , "\n";
};
=expect
[Exception] 2
[Exception] 1
--
Exception::throw called from (eval ?)[?].
Try::try called from (eval ?)[?].
Try::import called.
main::BEGIN called from (eval ?)[?].
(eval) called from (eval ?)[?].
!trace-2 Stack traceback with Try.pm internals.
$Try::Debug = 1;
try sub { throw Exception "1" },
finally sub { throw Exception "2" },
catch sub {
print join("\n", @@), "\n--\n", $@[0]->{trace} , "\n";
};
=expect
[Exception] 2
[Exception] 1
--
Exception::snapshot called from Try.pm[?].
Try::_push called from Try.pm[?].
Try::_throwhook called from Try.pm[?].
Exception::_throw called from Try.pm[?].
Exception::throw called from (eval ?)[?].
Try::Test::trace_2::__ANON__ called from Try.pm[?].
(eval) called from Try.pm[?].
Try::_handle called from Try.pm[?].
Try::_try called from Try.pm[?].
Try::try called from (eval ?)[?].
(eval) called from Try.pm[?].
Try::_regress called from Try.pm[?].
Try::import called.
main::BEGIN called from (eval ?)[?].
(eval) called from (eval ?)[?].
_END_TESTS_
1;#-#-#-#-#-#-#-#-#-#-#-#-#- End of Try.pm -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#