Skip to content

Commit

Permalink
Improve function return type inference
Browse files Browse the repository at this point in the history
Rename `type()` to `typeof()`
No longer perform arithmetic in Validator
  • Loading branch information
pragma- committed Jan 19, 2022
1 parent 2b87fc5 commit d526e07
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 106 deletions.
22 changes: 11 additions & 11 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ This README describes what is implemented so far.
* [Input/Output](#inputoutput)
* [print](#print)
* [Introspection](#introspection)
* [type](#type)
* [typeof](#typeof)
* [whatis](#whatis)
* [Data and structures](#data-and-structures)
* [length](#length)
Expand Down Expand Up @@ -468,23 +468,23 @@ Optionally, you can use [named arguments](#named-arguments) for clarity:
hello! good-bye!

#### Introspection
##### type
The `type` function returns the type of an expression, as a string. For functions,
##### typeof
The `typeof` function returns the type of an expression, as a string. For functions,
it returns strictly the type signature. If you're interested in function parameter
identifiers and default arguments, see the [`whatis`](#whatis) builtin function.

Its signature is: `Builtin (expr: Any) -> String`

> type(3.14)
> typeof(3.14)
"Real"

> var a = "hello"; type(a)
> var a = "hello"; typeof(a)
"String"

> type(print)
> typeof(print)
"Builtin (Any, String) -> Null"

> type(filter)
> typeof(filter)
"Builtin (Function (Any) -> Boolean, Array) -> Array"

##### whatis
Expand Down Expand Up @@ -674,7 +674,7 @@ Plang will attempt to infer the types from the values. If Plang cannot infer the
Let's consider a simple `add` function. With no explicit type annotations and no inferrable values,
the function's return type and the types of its parameters will default to the `Any` type:

> fn add(a, b) a + b; print(type(add));
> fn add(a, b) a + b; print(typeof(add));
Function (Any, Any) -> Any

This tells Plang to accept any types of values for the function call.
Expand All @@ -698,7 +698,7 @@ to `Real`:
This will still produce a run-time error if something that cannot be converted to `Real` is passed. If
explicit compile-time type-checking is desired, a type annotation may be provided:

> fn add(a: Real, b: Real) a + b; print(type(add));
> fn add(a: Real, b: Real) a + b; print(typeof(add));
Function (Real, Real) -> Real

Now Plang will throw a compile-time error if the types of the arguments do not match the
Expand All @@ -717,7 +717,7 @@ a value that is not a `Real`:

Consider the built-in `filter` function:

> print(type(filter))
> print(typeof(filter))
Builtin (Function (Any) -> Boolean, Array) -> Array

It has two parameters and returns an `Array`. The first parameter is a `Function` that takes
Expand Down Expand Up @@ -746,7 +746,7 @@ variables declared as `Any` will be narrowed to the type of the value initially
For example, a variable of type `Any` initialized to `true` will have its type narrowed
to `Boolean`:

> var a = true; type(a)
> var a = true; typeof(a)
"Boolean"

It will then be a compile-time type error to assign a value of any other type to it.
Expand Down
10 changes: 6 additions & 4 deletions lib/Plang/AstInterpreter.pm
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,19 @@ sub declare_variable {
my ($self, $context, $type, $name, $value) = @_;
$context->{guards}->{$name} = $type;
$context->{locals}->{$name} = $value;
$self->{dprint}->('VARS', "declare_variable $name\n" . Dumper($context->{locals}) . "\n") if $self->{debug};
$self->{dprint}->('VARS', "declare_variable $name with value " . Dumper($value) ."\n") if $self->{debug};
}

sub set_variable {
my ($self, $context, $name, $value) = @_;
$context->{locals}->{$name} = $value;
$self->{dprint}->('VARS', "set_variable $name\n" . Dumper($context->{locals}) . "\n") if $self->{debug};
$self->{dprint}->('VARS', "set_variable $name to value " . Dumper($value) . "\n") if $self->{debug};
}

sub get_variable {
my ($self, $context, $name, %opt) = @_;

$self->{dprint}->('VARS', "get_variable: $name\n" . Dumper($context->{locals}) . "\n") if $self->{debug};
$self->{dprint}->('VARS', "get_variable: $name has value " . Dumper($context->{locals}->{$name}) . "\n") if $self->{debug} and $name ne 'fib';

# look for variables in current scope
if (exists $context->{locals}->{$name}) {
Expand Down Expand Up @@ -418,6 +418,8 @@ sub keyword_try {
my $catch;

if (not ref $exception) {
chomp $exception;
$exception =~ s/ at.*// if $exception =~ /\.pm line \d+/; # strip Perl info
$exception = [['TYPE', 'String'], $exception];
}

Expand Down Expand Up @@ -845,7 +847,7 @@ my %function_builtins = (
subref => \&function_builtin_print,
vsubref => \&validate_builtin_print,
},
'type' => {
'typeof' => {
params => [[['TYPE', 'Any'], 'expr', undef]],
ret => ['TYPE', 'String'],
subref => \&function_builtin_type,
Expand Down
2 changes: 2 additions & 0 deletions lib/Plang/Interpreter.pm
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ sub interpret {
my $result = eval { $self->{interpreter}->run($self->{ast}, %opt) };

if (my $exception = $@) {
chomp $exception;
$exception =~ s/ at.*// if $exception =~ /\.pm line \d+/; # strip Perl info
die "Run-time error: unhandled exception: ", $self->{interpreter}->output_value($exception), "\n";
}

Expand Down
43 changes: 26 additions & 17 deletions lib/Plang/Types.pm
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,18 @@ sub is_subtype {
# return true if a type name is arithmetic
sub is_arithmetic {
my ($self, $type) = @_;
return 1 if $self->has_subtype('Number', $type->[1]);
return 1 if $self->has_subtype('Boolean', $type->[1]);

if ($type->[0] eq 'TYPE') {
return 1 if $self->has_subtype('Number', $type->[1]);
return 1 if $self->has_subtype('Boolean', $type->[1]);
}

if ($type->[0] eq 'TYPEUNION') {
foreach my $t (@{$type->[1]}) {
return $self->is_arithmetic($t);
}
}

return 0;
}

Expand Down Expand Up @@ -319,25 +329,24 @@ sub is_equal {
die "unknown type\n";
}


sub contains {
my ($self, $types, $type) = @_;

foreach my $t (@$types) {
return 1 if $self->is_equal($t, $type);
}

return 0;
}

sub unite {
my ($self, $types) = @_;

my @union;
my %uniq;

foreach my $type (@$types) {
next if $self->is_equal($type, ['TYPE', 'Any']);
next if $self->contains(\@union, $type);
push @union, $type;

if ($type->[0] eq 'TYPEUNION') {
foreach my $t (@{$type->[1]}) {
push @union, $t unless exists $uniq{$t->[1]};
$uniq{$t->[1]} = 1;
}
} else {
push @union, $type unless exists $uniq{$type->[1]};
$uniq{$type->[1]} = 1;
}
}

return ['TYPE', 'Any'] if @union == 0;
Expand All @@ -351,8 +360,8 @@ sub unite {

sub make_typeunion {
my ($self, $types) = @_;
my @sorted = sort { $a->[1] cmp $b->[1] } @$types;
return ['TYPEUNION', \@sorted];
my @union = sort { $a->[1] cmp $b->[1] } @$types;
return ['TYPEUNION', \@union];
}

1;
101 changes: 37 additions & 64 deletions lib/Plang/Validator.pm
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,15 @@ sub unary_op {

my $value = $self->evaluate($context, $data->[1]);

if ($self->{types}->is_equal(['TYPE', 'Any'], $value->[0])) {
return $value;
}

if ($self->{types}->is_arithmetic($value->[0])) {
if ($self->{types}->is_equal(['TYPE', 'Any'], $value->[0]) || $self->{types}->is_arithmetic($value->[0])) {
my $result;

if ($instr == INSTR_NOT) {
$result = [['TYPE', 'Boolean'], int ! $value->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
} elsif ($instr == INSTR_NEG) {
$result = [['TYPE', 'Number'], - $value->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_POS) {
$result = [['TYPE', 'Number'], + $value->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} else {
$self->error($context, "Unknown unary operator $pretty_instr[$instr]", $pos);
}
Expand All @@ -148,83 +144,64 @@ sub binary_op {
my $left = $self->evaluate($context, $data->[1]);
my $right = $self->evaluate($context, $data->[2]);

if ($self->{types}->is_equal(['TYPE', 'Any'], $left->[0])) {
return $left;
}

if ($self->{types}->is_equal(['TYPE', 'Any'], $right->[0])) {
return $right;
}

# String operations

if ($self->{types}->check(['TYPE', 'String'], $left->[0])
or $self->{types}->check(['TYPE', 'String'], $right->[0])) {

if ($self->{types}->check(['TYPE', 'Number'], $left->[0])) {
$left->[1] = chr $left->[1];
}

if ($self->{types}->check(['TYPE', 'Number'], $right->[0])) {
$right->[1] = chr $right->[1];
}

return [['TYPE', 'Boolean'], $left->[1] eq $right->[1], $pos] if $instr == INSTR_EQ;
return [['TYPE', 'Boolean'], $left->[1] ne $right->[1], $pos] if $instr == INSTR_NEQ;
return [['TYPE', 'Boolean'], ($left->[1] cmp $right->[1]) == -1, $pos] if $instr == INSTR_LT;
return [['TYPE', 'Boolean'], ($left->[1] cmp $right->[1]) == 1, $pos] if $instr == INSTR_GT;
return [['TYPE', 'Boolean'], ($left->[1] cmp $right->[1]) <= 0, $pos] if $instr == INSTR_LTE;
return [['TYPE', 'Boolean'], ($left->[1] cmp $right->[1]) >= 0, $pos] if $instr == INSTR_GTE;
return [['TYPE', 'String'], $left->[1] . $right->[1], $pos] if $instr == INSTR_STRCAT;
return [['TYPE', 'Integer'], index $left->[1], $right->[1], $pos] if $instr == INSTR_STRIDX;
if ( $self->{types}->check(['TYPE', 'String'], $left->[0])
or $self->{types}->check(['TYPE', 'String'], $right->[0])
or $self->{types}->is_equal(['TYPE', 'Any'], $left->[0])
or $self->{types}->is_equal(['TYPE', 'Any'], $right->[0]))
{
return [['TYPE', 'Boolean'], 0, $pos] if $instr == INSTR_EQ;
return [['TYPE', 'Boolean'], 0, $pos] if $instr == INSTR_NEQ;
return [['TYPE', 'Boolean'], 0, $pos] if $instr == INSTR_LT;
return [['TYPE', 'Boolean'], 0, $pos] if $instr == INSTR_GT;
return [['TYPE', 'Boolean'], 0, $pos] if $instr == INSTR_LTE;
return [['TYPE', 'Boolean'], 0, $pos] if $instr == INSTR_GTE;
return [['TYPE', 'String'], 0, $pos] if $instr == INSTR_STRCAT;
return [['TYPE', 'Integer'], 0, $pos] if $instr == INSTR_STRIDX;
}

# Number operations

if (not $self->{types}->is_arithmetic($left->[0])) {
if (!$self->{types}->is_equal(['TYPE', 'Any'], $left->[0]) && !$self->{types}->is_arithmetic($left->[0])) {
$self->error($context, "cannot apply operator $pretty_instr[$instr] to non-arithmetic type " . $self->{types}->to_string($left->[0]), $pos);
}

if (not $self->{types}->is_arithmetic($right->[0])) {
if (!$self->{types}->is_equal(['TYPE', 'Any'], $right->[0]) && !$self->{types}->is_arithmetic($right->[0])) {
$self->error($context, "cannot apply operator $pretty_instr[$instr] to non-arithmetic type " . $self->{types}->to_string($right->[0]), $pos);
}

if ($self->{types}->check($left->[0], $right->[0]) or $self->{types}->check($right->[0], $left->[0])) {
my $result;

if ($instr == INSTR_EQ) {
$result = [['TYPE', 'Boolean'], $left->[1] == $right->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
} elsif ($instr == INSTR_NEQ) {
$result = [['TYPE', 'Boolean'], $left->[1] != $right->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
} elsif ($instr == INSTR_ADD) {
$result = [['TYPE', 'Number'], $left->[1] + $right->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_SUB) {
$result = [['TYPE', 'Number'], $left->[1] - $right->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_MUL) {
$result = [['TYPE', 'Number'], $left->[1] * $right->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_DIV) {
if ($right->[1] == 0) {
$self->error($context, "division by zero", $self->position($right));
}

$result = [['TYPE', 'Number'], $left->[1] / $right->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_REM) {
$result = [['TYPE', 'Number'], $left->[1] % $right->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_POW) {
$result = [['TYPE', 'Number'], $left->[1] ** $right->[1]];
$result = [['TYPE', 'Number'], 0, $pos];
} elsif ($instr == INSTR_LT) {
$result = [['TYPE', 'Boolean'], $left->[1] < $right->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
} elsif ($instr == INSTR_LTE) {
$result = [['TYPE', 'Boolean'], $left->[1] <= $right->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
} elsif ($instr == INSTR_GT) {
$result = [['TYPE', 'Boolean'], $left->[1] > $right->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
} elsif ($instr == INSTR_GTE) {
$result = [['TYPE', 'Boolean'], $left->[1] >= $right->[1]];
$result = [['TYPE', 'Boolean'], 0, $pos];
}

if (defined $result) {
push @$result, $pos;

my $promotion = $self->{types}->get_promoted_type($left->[0], $right->[0]);

if ($self->{types}->is_subtype($promotion, $result->[0])) {
Expand Down Expand Up @@ -343,10 +320,6 @@ sub type_check_op_assign {
$self->error($context, "cannot apply operator $op to non-arithmetic type " . $self->{types}->to_string($right->[0]), $pos_right);
}

if ($op eq 'DIV' && $right->[1] == 0) {
$self->error($context, "division by zero", $self->position($right));
}

if ($self->{types}->check($left->[0], $right->[0])) {
return $left;
}
Expand Down Expand Up @@ -431,7 +404,7 @@ sub variable_declaration {
sub set_variable {
my ($self, $context, $name, $value) = @_;

$self->{dprint}->('VARS', "set_variable $name\n" . Dumper($context) . "\n") if $self->{debug};
$self->{dprint}->('VARS', "set_variable $name to " . Dumper($value) . "\n") if $self->{debug};

my $guard = $context->{guards}->{$name};

Expand Down Expand Up @@ -693,7 +666,7 @@ sub function_definition {
$self->declare_variable($new_context, $type, $ident, $value);
}

# infer return type
# collect returned values to infer return type
my @return_values;
my $result;
my $expr_pos;
Expand Down Expand Up @@ -890,7 +863,7 @@ sub function_call {
$name = "anonymous-2";

if (not $self->{types}->name_is($func->[0], 'TYPEFUNC')) {
$self->error($context, "cannot invoke `" . $self->output_value($func) . "` as a function (have type " . $self->{types}->to_string($func->[0]) . ")", $self->position($target));
$self->error($context, "cannot invoke value of type " . $self->{types}->to_string($func->[0]) . " as a function", $self->position($target));
}
}

Expand Down Expand Up @@ -1030,11 +1003,11 @@ sub keyword_while {
$context->{while_loop} = 1;

# validate expressions
$self->evaluate($context, $data->[2]);
my $result = $self->evaluate($context, $data->[2]);

delete $context->{while_loop};

return [['TYPE', 'Null'], undef];
return $result;
}

sub keyword_next {
Expand All @@ -1044,7 +1017,7 @@ sub keyword_next {
$self->error($context, "cannot use `next` outside of loop", $self->position($data));
}

return [['TYPE', 'Null'], undef];
return [['TYPE', 'Null'], undef, $self->position($data)];
}

sub keyword_last {
Expand Down
2 changes: 1 addition & 1 deletion test/curry_err.pt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ fn curriedAdd(x) fn add(y) x + y

curriedAdd(3)(4)(5)

# Validator error: cannot invoke `7` as a function (have type Integer) at line 3, col 14
# Validator error: cannot invoke value of type Integer as a function at line 3, col 14
Loading

0 comments on commit d526e07

Please sign in to comment.