#!/usr/bin/perl -CDS

# A typed lambda calculus in perl6-ish perl 5
# (makes use of attribute grammars and multimethods)

# first part is a type pretty printer and the type object model
# it emits greek letters and unicode symbols, or type literals

# second part is the object model for a lambda calculus, and a type inferrencer

# the algorithm implemented is Hindley-Milner type inferrencing, as explained in
# this article: http://research.microsoft.com/Users/luca/Papers/BasicTypechecking.pdf

# please note that this is nothingmuch's first article that he read from cover
# to cover with no hand holding, and actually understood. Everybody cheer!
# (i did not understand (or try very hard to understand) the modula 2 code, so
# the impl is probably different)


use utf8;
use strict;
use warnings;

use Set::Object ();
use Data::Dumper ();
use Storable ();

use Devel::STDERR::Indent;

package type;
use base qw/Clone/;

### the types of types we have are:
# type::operator
#	::binary - takes 2 params (e.g. ->, x)
#	::nullary - takes 0 params (e.g. int)
# type::variable - a placeholder to make a DAG out of a tree (to gain strong typing)
#	::free - an unbound type variable
#	::instantiated - points to another variable or operator

use Language::AttributeGrammar;

# this attribute grammer pretty prints bare types (no foralll)
my $pretty = Language::AttributeGrammar->new(<<'#\'EOG');
#\
type::variable::instantiated: layout($$) = { $.instance }
type::operator::nullary: layout($$) = { $.name }

type::operator::nary: layout($$) = { join(" ", $.name, layout($.param)) }
type::operator::nary: level($.param) = { level($$) }

ROOT: level($$) = { 0 }
type::operator::binary: level($.left) = { level($$) + 1 }
type::operator::binary: level($.right) = { level($$) }

type::operator::binary: inner_layout($$) = { join(" ", layout($.left), $.symbol, layout($.right)) }
type::operator::binary: layout($$) = { return parenthesize($$) ? "($_)" : $_ for inner_layout($$) }

type::operator::binary: parenthesize($$) = { $$->isa("type::operator::pair") || level($$) }
#'EOG

sub stringify {
	my $dag = shift;

	my @free;

	my $tree = $dag->splat(\@free); # Attribute grammars can't eat DAGs, so we have to recursively make shallow copies of the nodes
	# \@free will contain all the free type variables in the type at the end of the pass, and the original object they had
	# this lets us assign letters to them

	my @letters = split //, "αβγδεζηθικλμνξοπρστυφχψω"; # some identifiers for types. Let'shope we never need to print more than these ;-)
	
	my %map;
	my @forall;

	# allocate letters for type variables
	# it's ok to destroy the objects since they're copies
	foreach my $entry (@free) {
		my $assigned = $map{$entry->{orig}};
		unless ($assigned) {
			$assigned = shift @letters;
			$map{$entry->{orig}} = $assigned;
			push @forall, $assigned;
		}
		$entry->{new}->instantiate($assigned);
	}

	eval {
	   join(" ",
		   (map { "∀${_}." } @forall),
		   $pretty->apply($tree)->layout,
	   );
	} || die "$@\n" . Data::Dumper::Dumper($dag);
}

sub splat {
	my $self = shift;
	my $accum = shift;
	
	$self->shallow_clone;
}

sub shallow_clone {
	my $self = shift;
	bless {%$self}, (ref $self);
}

sub is_arrow { 0 }; # or arrows
sub free_vars { () }; # typicall there are no free vars inside a thingy

package type::operator;
use base qw/type/;

sub new {
	my $class = shift;
	bless {}, $class;
}

package type::operator::nullary;
use base qw/type::operator/;

sub new {
	my $class = shift;
	my $self = $class->SUPER::new;
	$self->{name} = shift;
	$self;
}

package type::operator::binary;
use base qw/type::operator/;

use Scalar::Util ();

sub new {
	my $class_that_should_be = shift;
	my $class = __PACKAGE__;
	my $self = $class->SUPER::new;

	$self->{left} = shift;
	$self->{right} = shift;
	
	# FIXME - this is a hack for L::AG, since it doesn't know about @ISA
	$self->{symbol} = $class_that_should_be->symbol;
	$self->{class_that_should_be} = $class_that_should_be;

	$self;
}

sub left { $_[0]{left} }
sub right { $_[0]{right} }


sub isa { # FIXME this is the same hack for L::AG
	my $self = shift;
	my $class = shift;

	if (Scalar::Util::blessed($self)){
		return 1 if $self->{class_that_should_be}->isa($class);
	}
	 
	$self->SUPER::isa($class);
}

sub is_arrow {
	my $self = shift;
	$self->{class_that_should_be}->is_arrow;
}

sub splat { # recursive splatting happens in parametrized type operators, since they have children
	my $self = shift;
	my $accum = shift;

	bless {
		%$self, 
		left => $self->left->splat($accum),
		right => $self->right->splat($accum),
	}, (ref $self);
}

sub free_vars {
	my $self = shift;
	map { $_->free_vars} $self->left, $self->right;
}

package type::operator::nary;
use base qw/type::operator/;

sub new {
	my $class = shift;
	my $self = $class->SUPER::new;
	$self->{name} = shift;
	$self->{param} = shift;

	$self;
}

sub splat {
	my $self = shift;
	my $accum = shift;

	bless {
		%$self,
		param => $self->{param}->splat($accum),
	}, (ref $self);
}

sub free_vars {
	my $self = shift;
	$self->{param}->free_vars;
}


package type::operator::arrow;
use base qw/type::operator::binary/;

sub symbol { "→" }
sub is_arrow { 1 }

package type::operator::pair;
use base qw/type::operator::binary/;

sub symbol { "×" }


package type::variable;
use base qw/type/;

package type::variable::free;
use base qw/type::variable/;

# a free variable is a placeholder
# when going into an AST these are generated
# as the AST is left they are unified
# a free variable can be instantiated to point to another type object
# at this point it reblesses itself

sub new {
	my $class = shift;
	bless {
		instance => undef,
	}, $class;
}

sub instantiate {
	my $self = shift;
	$self->{instance} = shift;
	bless $self, "type::variable::instantiated";
}

sub splat {
	my $self = shift;
	my $accum = shift;

	push @$accum, my $entry = {
		orig => $self,
		new => undef,
	};
	
	$entry->{new} = $self->SUPER::splat($accum);
}

sub free_vars { $_[0] }

package type::variable::instantiated;
use base qw/type::variable/;

sub is_arrow {
	my $self = shift;
	$self->{instance}->is_arrow;
}

sub splat { # this simplifies the AG above, and avoids unnecessary duplication
	my $self = shift;
	$self->{instance}->splat(@_);
}

sub left {
	my $self = shift;
	$self->{instance}->left;
}

sub right {
	my $self = shift;
	$self->{instance}->right;
}

sub free_vars {
	my $self = shift;
	$self->{instance}->free_vars;
}

package node;
# all AST nodes inherit from this

sub new {
	my $class = shift;
	bless {
		t => undef,
		(@_ == 1 ? (v => $_[0]) : @_), # an easy constructor
	}, $class;
}

# functions are applied 
package fun;
use base qw/node/;

package comb;
use base qw/node/;

package ide;
use base qw/node/;

package let;
use base qw/node/;

package cond;
use base qw/node/;

package val;
use base qw/node/;

sub new {
	my $self = shift->SUPER::new(@_);
	$self->{t} ||= type::operator::nullary->new("int");
	$self;
}


package types;

use Class::Multimethods::Pure;
use Data::Dumper;


# The unification of the various types depends on their data...
# this is why this code is so big.

# in a language with native multimetods this would be much more concise

# a free variable instantiates into whatever it is combined with (except instantiated variables, which delegate. see below)
multi unify => ( 'type::variable::free', any(qw/type::variable::free type::operator/) ) => sub {
	return if ($_[0] == $_[1]); # in a recursive let expr a generic recursive function will be reunified with it's still free variable
	$_[0]->instantiate($_[1]);
};
multi unify => ('type::operator', 'type::variable::free') => sub { $_[1]->instantiate($_[0]) };

# an instantiated variable will unify it's instantiated value instead of itself
multi unify => ('type::variable::instantiated', 'type') => sub { unify($_[0]{instance}, $_[1], @_[2, 3]) };
multi unify => ('type', 'type::variable::instantiated') => sub { unify($_[1]{instance}, $_[0], @_[2, 3]) };
multi unify => ('type::variable::instantiated', 'type::variable::instantiated') => sub { unify($_[0]{instance}, $_[1]{instance}, @_[2, 3]) };

# nullary operators much have the same name (e.g. int == int, str == str, int != str)
multi unify => ('type::operator::nullary', 'type::operator::nullary') => sub {
	my ($x, $y) = (shift, shift);

	$x->{name} eq $y->{name}
		or die "can't unify $x->{name} with $y->{name} in " . Dumper(@_);
};

# binary operators must have the same symbol, and are also unified recursively
multi unify => ('type::operator::binary', 'type::operator::binary') => sub {
	my ($x, $y) = (shift, shift);
	
	$x->{symbol} eq $y->{symbol}
		or die "Can't unify $x->{symbol} with $y->{symbol} in " . Dumper(@_);
	
	unify($x->left, $y->left, @_);
	unify($x->right, $y->right, @_);
};

multi unify => ('type::operator::nary', 'type::operator::nary') => sub {
	my ($x, $y) = (shift, shift);

	$x->{name} eq $y->{name}
		or die "Can't unify $x->{name} with $y->{name} in " . Dumper(@_);

	unify($x->{param}, $y->{param}, @_);
};

multi unify => ('type', 'type') => sub {
	die "trying to unify incompatible types:\n" . Dumper(@_);
};


# these are the type signatures of some builtins
our %prelude = (
	# int -> int -> int
	add => type::operator::arrow->new(
		type::operator::nullary->new("int"),	
		type::operator::arrow->new(
			type::operator::nullary->new("int"),
			type::operator::nullary->new("int"),
		),
	),
	sub => type::operator::arrow->new(
		type::operator::nullary->new("int"),	
		type::operator::arrow->new(
			type::operator::nullary->new("int"),
			type::operator::nullary->new("int"),
		),
	),
	mul => type::operator::arrow->new(
		type::operator::nullary->new("int"),	
		type::operator::arrow->new(
			type::operator::nullary->new("int"),
			type::operator::nullary->new("int"),
		),
	),
	# a -> list a -> list a
	cons => do {
		my $a = type::variable::free->new;
		type::operator::arrow->new(
			$a,
			type::operator::arrow->new(
				type::operator::nary->new(list => $a),
				type::operator::nary->new(list => $a),
			),
		);
	},
	# list a
	nil => type::operator::nary->new(list => type::variable::free->new),
	# list a -> bool
	'is_nil?' => type::operator::arrow->new(
		type::operator::nary->new(list => type::variable::free->new),
		type::operator::nullary->new("bool"),
	),
	# list a -> a
	head => do {
		my $a = type::variable::free->new;
		type::operator::arrow->new(
			type::operator::nary->new(list => $a),
			$a,
		);
	},
	# list a -> list a
	tail => do {
		my $a = type::variable::free->new;
		my $l = type::operator::nary->new(list => $a);
		type::operator::arrow->new( $l, $l );
	},
	# a -> b -> (a x b)
	pair => do {
		my $a = type::variable::free->new;
		my $b = type::variable::free->new;
		
		type::operator::arrow->new(
			$a,
			type::operator::arrow->new(
				$b,
				type::operator::pair->new( $a, $b ),
			),
		);
	},
	# (a x b) -> a
	fst => do {
		my $a = type::variable::free->new;
		my $b = type::variable::free->new;
		
		type::operator::arrow->new(
			type::operator::pair->new( $a, $b ),
			$a,
		);
	},
	# (a x b) -> b
	snd => do {
		my $a = type::variable::free->new;
		my $b = type::variable::free->new;
		
		type::operator::arrow->new(
			type::operator::pair->new( $a, $b ),
			$b,
		);
	},
	# int -> int -> bool
	'==' => type::operator::arrow->new(
		type::operator::nullary->new("int"),
		type::operator::arrow->new(
			type::operator::nullary->new("int"),
			type::operator::nullary->new("bool"),
		),
	),
);


package main;

use strict;
use warnings;

use Test::More 'no_plan';
use Test::Exception;
use Data::Dumper;
use Language::AttributeGrammar;

{
	# please forgive me
	my $orig = \&Test::Builder::output;
	no warnings 'redefine';
	*Test::Builder::output = sub {
		my $ret = &$orig(@_);
		binmode $ret, ":utf8";
		$ret;
	};
}

$Data::Dumper::Terse = 1; # no need for pointless $VARx
$Data::Dumper::Indent = 1;


sub AUTOLOAD { # an easy constructor... fun() instead of fun->new()
	(my $class) = ((our $AUTOLOAD) =~ /([^:]+) $/x);
	$class->new(@_);
}


# this is the actual inferrence engine
my $i = Language::AttributeGrammar->new(<<'#\'EOG');
#\

# at the root we initialize the predefined symbols, and the initial set of generic variables
ROOT: env($$) = { Clone::clone(\%types::prelude) }
ROOT: generic_in($$) = {
	my $env = env($$);

	Set::Object->new(
		grep { $_->free_vars }
			map { $_->left }
				grep { $_->is_arrow }
					values %$env
	);
}

# entering a function creates a new env (the parameter is associated with a new type variable)
# this new env is passed down to the body, but is also reused in the 'type' attr
fun: env($.body) = { new_env($$) }
fun: new_env($$) = {
	my $orig = env($$);
	my %new = (
		%$orig,
		$.param => type::variable::free->new()
	);
	\%new;
}

# inherit the set of generic vars to our body
fun: generic_in($.body) = { generic_in($$) }

# possibly amend the list of generic variables if the paramter type of the function is free
fun: generic($$) = {
	my $t_of_param = type($$)->left;
	my $prev_generic = generic($.body);

	if ($t_of_param->free_vars) {
		return $prev_generic + Set::Object->new($t_of_param);
	} else {
		return $prev_generic;
	}
}

# the type of the function the arrow operator, applied to the type of the parameter (as allocated in the new env), and the type of the body
fun: type($$) = {
	type::operator::arrow->new(
		new_env($$)->{ $.param },
		type($.body),
	),
}

# identifiers pass the generic variables unchanged
ide: generic($$) = { generic_in($$) }

# if an identifier is not in the lexical scope we can't know it's type
ide: type($$) = {
	my $t = env($$)->{$.v};

	die "no such identifier " . $.v unless defined $t;
	
	$t;
}

# values also pass the set of generics unchanged
val: generic($$) = { generic_in($$) }
# vals are assumed to know their own type (see package val above)
val: type($$) = { $.t }

# combs (applications) pass their env to eval their children
comb: env($.fun) = { env($$) }
comb: env($.param) = { env($$) }

# combs combine the generic values from their children
comb: generic_in($.fun) = { generic_in($$) }
comb: generic_in($.param) = { generic_in($$) }
comb: generic($$) = { generic($.fun) + generic($.param) }

comb: type($$) = {
	my $f = type($.fun);
	my $p = type($.param);

	types::unify(
		$f,
		type::operator::arrow->new(
			type::variable::free->new,
			type::variable::free->new,
		),
		$.fun,
		$.param,
	);

	# first of all the type of the thing we're appling must be a function
	die "Can't apply non function type (" . $.fun . " has type " . $f->stringify
		unless $f->is_arrow;
	
	
	my $p_f = $f->left;
	my $ret = $f->right;
	
	my $generic_vars = generic($.fun);

	# if the function is generic we'll be instantiating it's type variables in the function body
	# for this reason the function needs to be cloned (so it will stay generic when we leave the body of the function)
	if ($generic_vars->includes($p_f)) {
		my $c = $f->clone;
		($p_f, $ret) = ($c->left, $c->right);
	}

	# now we unify the type of the parameter with the type to the left of the arrow in the function's type,
	# this causes the type variables in the cloned function to be instantiated to the concrete values as inferred in the body of the function, based on the param
	types::unify(
		$p, $p_f,
		$.fun, $.param,
	);

	# the value of the comb is the return value from the function
	# since this return value is a type variable that is present inside the function, if it was free it might be unified to a concrete value
	$ret;
}

# in a let block we thread the set of generic type variables, so the body has the generic variables created in the binding
let: generic_in($.is) = { generic_in($$) }
let: generic_in($.in) = { generic($.is) }
let: generic($$) = { generic($.in) }

# the env is augmented to contain the new symbol introduced by the binding
# the body of the binding contains the env with the the symbols of our outer scope
let: new_env($$) = {
	my $orig = env($$);
	my %new = (
		%$orig,
		($.ide) => type::variable::free->new,
	);
	\%new;
}
let: env($.is) = { new_env($$) }
let: env($.in) = { new_env($$) }

let: type($$) = {
	types::unify(
		new_env($$)->{ $.ide }, type($.is),
		$.ide, $.is,
	);


	type($.in);
}

cond: env($.if) = { env($$) }
cond: env($.then) = { env($$) }
cond: env($.else) = { env($$) }

cond: generic_in($.if) = { generic_in($$) }
cond: generic_in($.then) = { generic($.if) }
cond: generic_in($.else) = { generic($.then) }
cond: generic($$) = { generic($.else) }

cond: type($$) = {
	types::unify(
		type($.if), type::operator::nullary->new("bool"),
		$.if, "bool",
	);

	types::unify(
		my $t = type($.then), type($.else),
		$.then, $.else
	);

	$t;
}


#'EOG


# these are some sample ASTs that need to be typed

# this is a function that expects an int and will emit an int
# add (int -> int -> int) is resolved using val(10) into int -> int
# x is inferred to be int
my $plus_ten = fun(
	param => "x",
	body => comb(
		fun => comb(
			fun => ide("add"),
			param => val(10),
		),
		param => ide("x"),	
	),
);

# fun(x) x	-  takes any value and returns it
# x stays free, and thus becomes generic (id is polymorphic)
my $id = fun(
	param => "x",
	body => ide("x"),
);

# plus_ten(15) (int -> int applied to int) is a number
my $fifteen = comb(
	fun => $plus_ten,
	param => val(5),
);

# the pair function takes any param and returns a function that takes any param
# that function will return a pair with the first param and the second param inside a pair
# hence pair is a -> b -> (a x b). So if a is an int, $half_pair is b -> (int x b)
my $half_pair = comb(
	fun => ide("pair"),
	param => val(10),
);

# a fully constructed pair, with a = int and b = str
my $pair = comb(
	fun => $half_pair,
	param => val(v => "foo", t => type::operator::nullary->new("str")),
);

# the polymorphic snd function takes any pair and returns it's second thingy
# (a x b) -> b
# applied to $pair (int x str) this should e str
my $snd = comb(
	fun => ide("snd"),
	param => $pair,
);

# the polymorphic fst function takes any pair and returns it's first thingy
# (a x b) -> a
# applied to $pair (int x str) this should be int
my $fst = comb(
	fun => ide("fst"),
	param => $pair,
);

# this makes sure that ':t id(10)' is the same as ':t 10'
my $app_id = let(
	ide => "id",
	is => $id,
	in => comb(
		fun => ide("id"),
		param => val(10),
	),
);

# this expr is like the following haskell:
# let id x = x
# in id ((id 3), (id "foo"))
# and basically ensures that id is polymorphic when used in a let expr.
my $id_pair = let(
	ide => "id",
	is => $id,
	in => comb(
		fun => ide("id"),
		param => comb(
			fun => comb (
				fun => ide("pair"),
				param => comb(
					fun => ide("id"),
						param => val(10),
				),
			),
			param => comb(
				fun => ide("id"),
				param => val(v => "foo", t => type::operator::nullary->new("str")),
			),
		),
	),
);

# this function cannot be typed, since it wants a function in x, (a -> b), and a is both an int and a str
# while this could seem to make sense for e.g. the 'id' function, this won't work if we pass in $plus_ten
my $flatten_polymorphic = fun(
	param => "f",
	body => comb(
		fun => comb(
			fun => ide("pair"),
			param => comb(
				fun => ide("f"),
				param => val(3),
			),
		),
		param => comb(
			fun => ide("f"),
			param => val(v => "foo", t => type::operator::nullary->new("str")),
		),
	),
);

# if we don't know what symbol it is we can't type it
my $no_such_sym = fun(
	param => "x",
	body => ide("y"),
);

# this is a higher order function that takes a function and applies it to a string
# thiss tests that the inferrence of higher order functions is correct
my $app_to_str = fun(
	param => "x",
	body => comb(
		fun => ide("x"),
		param => val(v => "foo", t => type::operator::nullary->new("str")),
	),
);

# we can apply a polymorphic function to a string by giving it as a param to a higher order function
my $app_id_to_str = comb(
	fun => $app_to_str,
	param => $id,
);

# we can't get app_to_str to apply something of a bad type to a str
my $app_plus_ten_to_str = comb(
	fun => $app_to_str,
	param => $plus_ten,
);

my $fac = fun(
	param => "x",
	body => let(
		ide => "fac'",
		is => fun(
			param => "x",
			body => fun(
				param => "r",
				body => cond(
					if => comb(
						fun => comb(
							fun => ide("=="),
							param => ide("x"),
						),
						param => val(0),
					),
					then => ide("r"),
					else => comb(
						fun => comb(
							fun => ide("fac'"),
							param => comb(
								fun => comb(
									fun => ide("sub"),
									param => ide("x"),
								),
								param => val(1),
							),
						),
						param => comb(
							fun => comb(
								fun => ide("mul"),
								param => ide("r"),
							),
							param => ide("x"),
						),
					),
				),
			),
		),
		in => comb(
			fun => comb(
				fun => ide("fac'"),
				param => ide("x"),
			),
			param => val(1),
		),
	),
);

my $length = fun(
    param => "xs",
    body => let(
        ide => "length'",
        is => fun(
            param => "xs",
            body => fun(
                param => "n",
                body => cond(
                    if => comb(
                        fun => ide("is_nil?"),
                        param => ide("xs"),
                    ),
                    then => val("n"),
                    else => comb(
                        fun => comb(
                            fun => ide("length'"),
                            param => comb(
								fun => ide("tail"),
								param => ide("xs"),
							),
						),
						param => comb(
							fun => comb(
								fun => ide("add"),
								param => ide("n"),
							),
							param => val(1),
						),
					),
				),
            ),
        ),
        in => comb(
            fun => comb(
                fun => ide("length'"),
                param => ide("xs"),
            ),
            param => val(0),
        ),
    ),
);

my $fac_5 = comb(
	fun => $fac,
	param => val(5),
);

my $length_and_first = fun(
	param => "xs",
	body => comb(
		fun => comb(
			fun => ide("pair"),
			param => comb(
				fun => ide("head"),
				param => ide("xs"),
			),
		),
		param => comb(
			fun => $length,
			param => ide("xs"),
		),
	),
);

my $one_elem_list = fun(
	param => "x",
	body => comb(
		fun => comb(
			fun => ide("cons"),
			param => ide("x"),
		),
		param => ide("nil"),
	),
);

my $length_and_first_p = let(
	ide => "listify",
	is => $one_elem_list,
	in => let(
		ide => "length_and_first",
		is => $length_and_first,
		in => comb(
			fun => ide("length_and_first"),
			param => comb(
				fun => ide("listify"),
				param => val(10),
			),
		),
	),
);

my $length_and_first_gen = let(
	ide => "listify",
	is => $one_elem_list,
	in => let(
		ide => "length_and_first",
		is => $length_and_first,
		in => comb(
			fun => comb(
				fun => ide("pair"),
				param => comb(
					fun => ide("length_and_first"),
					param => comb(
						fun => ide("listify"),
						param => val(10),
					),
				),
			),

			param => comb(
				fun => ide("length_and_first"),
				param => comb(
					fun => ide("listify"),
					param => val(v => "foo", t => type::operator::nullary->new("str")),
				),
			),
		),
	),
);

my $map = let(
    ide => "map",
    is => fun(
	param => "f",
	body => fun(
		param => "xs",
		body => cond(
			if => comb(
				fun => ide("is_nil?"),
				param => ide("xs"),
			),
			then => ide("nil"),
			else => comb(
				fun => comb(
					fun => ide("cons"),
					param => comb(
						fun => ide("f"),
						param => comb(
							fun => ide("head"),
							param => ide("xs"),
						),
					),
				),
				param => comb(
					fun => comb(
						fun => ide("map"),
						param => ide("f"),
					),
					param => comb(
						fun => ide("tail"),
						param => ide("xs"),
					),
				),
			),
		),
	),
	),
	in => ide("map"),
);

my $list_plus_ten = comb(
    fun => $map,
    param => $plus_ten
);

# tests for the type pretty printer
{
	my $type = type::operator::arrow->new(
		type::variable::free->new,
		my $b = type::variable::free->new,
	);
	is($type->stringify, "∀α. ∀β. α → β", "pretty print a -> b");

	$b->instantiate( type::operator::nullary->new("int") );
	is($type->stringify, "∀α. α → int", "pretty print a -> b where b = int");
}
{
	my $a = type::variable::free->new;
	my $type = type::operator::arrow->new($a, $a);

	is($type->stringify, "∀α. α → α", "pretty print a -> a");
}
{
	my $type = type::operator::arrow->new(
		my $a = type::variable::free->new,
		my $b = type::variable::free->new,
	);
	$a->instantiate($b);
	is($type->stringify, "∀α. α → α", "pretty print a -> b where a = b");
}
{
	my $type = type::operator::arrow->new(
		type::operator::arrow->new(
			type::variable::free->new,
			type::variable::free->new,
		),
		type::variable::free->new,
	);
	is($type->stringify, "∀α. ∀β. ∀γ. (α → β) → γ", "pretty print (a -> b) -> c");
}
{
	my $type = type::operator::arrow->new(
		type::variable::free->new,
		type::operator::arrow->new(
			type::variable::free->new,
			type::variable::free->new,
		),
	);
	is($type->stringify, "∀α. ∀β. ∀γ. α → β → γ", "pretty print a -> b -> c");
}
{
	my $type = type::operator::pair->new(
		type::variable::free->new,
		type::variable::free->new,
	);
	is($type->stringify, "∀α. ∀β. (α × β)", "pretty print a x b");
}
{
	my $type = type::operator::arrow->new(
		type::variable::free->new,
		type::operator::pair->new(
			type::variable::free->new,
			type::variable::free->new,
		),
	);
	is($type->stringify, "∀α. ∀β. ∀γ. α → (β × γ)", "pretty print a -> b x c");
}
{
	my $type = type::operator::arrow->new(
		type::operator::pair->new(
			type::variable::free->new,
			type::variable::free->new,
		),
		type::variable::free->new,
	);
	is($type->stringify, "∀α. ∀β. ∀γ. (α × β) → γ", "pretty print a x b -> c");
}
{
	my $type = type::operator::nary->new(list => type::variable::free->new);
	is($type->stringify, "∀α. list α", "pretty print list a");
}
{
	my $a = type::variable::free->new;
	my $type = type::operator::arrow->new(
		type::operator::nary->new(list => $a),
		$a,
	);
	is($type->stringify, "∀α. list α → α", "pretty print list a -> a");
}
{
	my $a = type::variable::free->new;
	my $b = type::variable::free->new;
	my $type = type::operator::arrow->new(
		type::operator::nary->new(list => $a),
		type::operator::arrow->new(
            type::operator::nary->new(list => $b),
            type::operator::nary->new(list => type::operator::pair->new( $a, $b )),
		),
	);
	is($type->stringify, "∀α. ∀β. list α → list β → list (α × β)", "pretty print :t zip");
}




# tests for the type inferrencer
is(t($id), "∀α. α → α", ":t id");
is(t($app_id), "int", ":t id(3)");
is(t($plus_ten), "int → int", ":t (+ 10)");
is(t($fifteen), "int", ":t (+ 10) 5");
is(t($half_pair), "∀α. α → (int × α)", ":t pair(10)");
is(t($pair), "(int × str)", ":t pair(10)('foo')");
is(t($fst), "int", ":t fst( pair(10)('foo') )");
is(t($snd), "str", ":t snd( pair(10)('foo') )");
is(t(ide("pair")), "∀α. ∀β. α → β → (α × β)", ":t pair");
is(t(ide("fst")), "∀α. ∀β. (α × β) → α", ":t fst");
is(t(ide("snd")), "∀α. ∀β. (α × β) → β", ":t snd");
is(t($id_pair), "(int × str)", ":t id(pair(id(10))(id('foo')))");
is(t($app_to_str), "∀α. (str → α) → α", ":t (λx. (x 'str'))");
is(t($app_id_to_str), "str", ":t (λx. (x 'str'))(id)");
is(t($fac), "int → int", ":t fac");
is(t($length), "∀α. list α → int", ":t length");
is(t($fac_5), "int", ":t fac 5");
is(t($one_elem_list), "∀α. α → list α", ":t listify(x)");
is(t(comb( fun => $one_elem_list, param => val(5) )), "list int", ":t listify(5)");
is(t($length_and_first), "∀α. list α → (α × int)", ":t length_and_first");
is(t($length_and_first_p), "(int × int)", ":t length_and_first [1]");
is(t($length_and_first_gen), "((int × int) × (str × int))", ":t (length_and_first ['foo'], length_and_first [1])");
is(t($map), "∀α. ∀β. (α → β) → list α → list β", ":t map");
is(t($list_plus_ten), "list int → list int", ":t map (\\x -> 10 + x)");

dies_ok { t($flatten_polymorphic) } "can't type λf.(pair (f 3))(f 'foo')";
dies_ok { t($no_such_sym) } "all identifiers need to be resolvable";
dies_ok { t($app_plus_ten_to_str) } "can't do a higher order app with uneq types";

# a helper that returns the stringified type of an AST
sub t {
	my $ast = shift;
	$i->apply($ast)->type->stringify;
}


