Skip to content
This repository has been archived by the owner on Jun 1, 2023. It is now read-only.

Type system

Reini Urban edited this page Jan 7, 2016 · 27 revisions

See the perltypes documentation from master, and the latest version is usually in the feature/gh7-signatures or featurex/gh16-multi branch.

In the featurex/gh7-sigs+libs branch several libraries are already "modernized", i.e. converted to typed signatures.

Modernization of libraries has the benefit of using the faster signature calling convention. The variables are passed from the stack directly to the pad, the place for lexical variables without being copied to the global @_. And you can avoid many costly run-time type checks. Most type violations are already caught at compile-time, with much better error messages.

See for example the modernization of Test-Simple in commit d9e5269.

The translation is pretty straightforward:

-sub is_eq {
-    my( $self, $got, $expect, $name ) = @_;
+sub is_eq ( $self, $got?, $expect?, $name?) {

The ? suffix from perl6 denotes an optional argument. If left out in the call, the value will be undef. There's no arity inspection as in @_.

$self is still in the signature. When is_eq is declared as method and not as sub, $self is not needed anymore and should be omitted. This is currently in work in the feature/gh16-multi branch.

To type or not?

-sub skip {
-    my( $self, $why, $name ) = @_;
-    $why ||= '';
-    $name = '' unless defined $name;
+sub skip ( $self, str $why='', $name? ) {
     $self->_unoverload_str( \$why );

$why is declared with the str coretype. str is the unboxed variant of the perl5-level boxed Str type, same as in Perl6. That means the compiler accepts only strings, not other scalars, and only strings which are known to the compiler at compile-time. Those strings will then be optimized to fast unboxed variants if possible or not, as the compiler sees fit. Note that there's no Uni type yet as in perl6 for utf8 strings. Str accepts currently all strings, and str needs to be a simple ASCIIZ string without embedded \0, when being upgraded to a native type.

So a call to skip needs to stringify the $why argument, otherwise you get a compile-time error.

skip($test->{why}, "not working yet"); will be invalid.

skip("$test->{why}", "not working yet"); is invalid.

$name? should have also been declared as str $name=undef instead. This is currently valid code I think, but inconsistent with the current type system. undef is no str type. It really should be str? $name?, str? denoting str or the Undef type.

int vs Int vs Numeric

The type int cannot overflow to a float, a number (type Num). This is just as under use integer. Arithmetic ops with int are much faster than with generic numbers of type Numeric.

E.g. the generic add op is 50 lines of c code, which calculates the sum twice and checks both args for UV, IV or NV, and then for possible overflow. The optimized Int variant for add where both arguments are int is one line, and 10x faster. The optimized int variant where both arguments are unboxed, true native ints, is 20x faster, as the value needs not be extracted from the SV head, the result needs not be changed to an SvIV, it is just a single word.

But with the declared int type you cannot just pass normal expressions to this method anymore. E.g. with the skip similar to above, just as Test::More function and declared as

cpan/Test-Simple/lib/Test/More.pm
-sub skip {
-    my( $why, $how_many ) = @_;
+sub skip ( str $why, Int $how_many = 0) {
     my $tb = Test::More->builder;

-    unless( defined $how_many ) {
+    unless ($how_many) {

We need the numeric argument $how_many, which is often mixed up with str $why. We can declare $how_many as int, Int or Numeric, but overflow to NV (the Num type as in perl6) makes no sense as $how_many is later used as loop counter.

So we have to fix several wrong type usages of this skip, as seen below.

-- cpan/Test-Harness/t/source_handler.t
+++ cpan/Test-Harness/t/source_handler.t
@@ -355,9 +355,9 @@ sub test_handler {

     SKIP:
     {
-            my $planned = 1;
+            my int $planned = 1;
             $planned += 1 + scalar @{ $test->{output} } if $test->{output};
-            skip $test->{skip_reason}, $planned if $test->{skip};
+            skip "$test->{skip_reason}", $planned if $test->{skip};

We need to stringify the $why argument, and type the $planned number to int to bypass the type checker. The old $planned was of type Scalar, with a better type inferencer $planned could have been promoted to Numeric, because the += op returns Numeric.

Or another needed typing for this call:

--- cpan/Test-Harness/t/prove.t
+++ cpan/Test-Harness/t/prove.t
@@ -1483,7 +1483,7 @@ for my $test (@SCHEDULE) {

     SKIP:
     {
-        skip $test->{skip_reason}, $test->{_planned} if $test->{skip};
+        skip "$test->{skip_reason}", int($test->{_planned}) if $test->{skip};

int() promotes the Scalar type of $test->{_planned} to Int, which is acceptable for skip(str, int). If you want to pass the type checker for expressions you also need to use int(), as for example in skip "bla", int(2*keys %hash);

You could also weaken the type of skip(.., int) to skip(..., Numeric) so that you can avoid the new int() cast. But look the the whole function:

sub skip ( str $why, Int $how_many = 0) {
    my $tb = Test::More->builder;

    unless ($how_many) {
        # $how_many can only be avoided when no_plan is in use.
        _carp "skip() needs to know \$how_many tests are in the block"
          unless $tb->has_plan eq 'no_plan';
        $how_many = 1;
    }

    #if ($how_many and $how_many =~ /\D/ ) {
    #    _carp
    #      "skip() was passed a non-numeric number of tests.  Did you get the arguments backwards?";
    #    $how_many = 1;
    #}

    for( 1 .. $how_many ) {
        $tb->skip($why);
    }

    no warnings 'exiting';
    last SKIP;
}

The run-time type check for $how_many =~ /\D/ was removed because this is already done at compile-time. for( 1 .. $how_many ) only accepts int, so using the int() cast before is perfectly fine. With overflow to NV the loop will be wrong, and int can even be typed as uint as it needs to be > 1.

I left the type Int here because I'm thinking to allow those Int argument types to carry magic and with int not. Int being the soft, int the hard type, besides the possibility to being boxed or unboxed, native or not. But variables typed as Int are forbidden to carry magic, so it's not really consistent neither.

New compile time errors:

my Int $i = 1;
bless \$i, "MyInt";     => Invalid type Int for bless $i
tie $i, "Tie::Scalar";  => Invalid type Int for tie $i

Another typical new type errors are e.g.:

./perl -Ilib -T ext/POSIX/t/sysconf.t
Type of arg $how_many to Test::More::skip must be Int (not Numeric) at 
ext/POSIX/t/sysconf.t line 86, near "@path_consts;"
Type of arg $how_many to Test::More::skip must be Int (not Numeric) at 
ext/POSIX/t/sysconf.t line 138, near "@path_consts_fifo)"
Type of arg $how_many to Test::More::skip must be Int (not Numeric) at 
ext/POSIX/t/sysconf.t line 142, near "@path_consts_fifo)"

The fix is to add types (better) or int() casts.

--- ext/POSIX/t/sysconf.t
+++ ext/POSIX/t/sysconf.t
@@ -83,7 +83,7 @@ sub _check_and_report {
 SKIP: {
     my $fd = POSIX::open($testdir, O_RDONLY)
         or skip "could not open test directory '$testdir' ($!)",
-         2 * @path_consts;
+         int(2 * @path_consts);

     for my $constant (@path_consts) {
         SKIP: {
@@ -106,7 +106,7 @@ for my $constant (@path_consts) {
 }

 SKIP: {
-    my $n = 2 * 2 * @path_consts_terminal;
+    my int $n = 2 * 2 * @path_consts_terminal;

     -c $TTY
        or skip("$TTY not a character file", $n);
@@ -135,11 +135,11 @@ my $fifo = "fifo$$";

 SKIP: {
     eval { mkfifo($fifo, 0666) }
-       or skip("could not create fifo $fifo ($!)", 2 * 2 * @path_consts_fifo);
+       or skip("could not create fifo $fifo ($!)", int(2 * 2 * @path_consts_fifo));

   SKIP: {
       my $fd = POSIX::open($fifo, O_RDONLY | O_NONBLOCK)
-         or skip("could not open $fifo ($!)", 2 * @path_consts_fifo);
+         or skip("could not open $fifo ($!)", int(2 * @path_consts_fifo));

You will argue correctly that 2 * @path_consts_fifo is of type int already. The const 2 is of type int, and the arylen also, so the type inferer in one of the ck_ check functions should have already upgraded mul(int, int):Numeric to i_mul(Int, Int):Int or even int_mul(int, int):int. But this is not implemented yet.

I planned to release this earlier, late 2015, but I will spend more time fixing goto sigs (tailcalls), and those simple type inference and checks to be useful. The run-time int() has some costs, which will go away with the better type inferencer.

Clone this wiki locally