I'm confused about type promotion (was: [Nickle]Tonight's topics)

Keith Packard nickle@nickle.org
Wed, 24 Jul 2002 11:11:34 -0700


Around 13 o'clock on Jul 24, Carl Worth wrote:

> What prompted my train of thought was the surprising behavior of the
> "structure compatibility rule" mentioned in the nickle tutorial,
> (which is coming along quite nicely by the way -- thanks!):
> 
> 	a struct value is compatible with a struct type if
> 	the struct value contains all of the entries in the
> 	type.

This is the extension of the rule allowing assignment of an integer type 
to a real type:

	int	i;
	real	r;

	i = 1;
	r = i;

The essential behaviour is that the variable 'r' can perform any operation 
on the new value that it could perform on any other real value.  Just so 
with structures:

	typedef struct { int i; real r } i_and_r;

	typedef struct { int i; } just_i;

	i_and_r i_and_r_value = { i = 12, r = 37 };

	just_i	i_value;

	i_value = i_and_r_value;

Anywhere the program uses 'just_i' types, i_and_r_value will perform 
correctly.  This is statically checkable.

The funny part comes when you want to go the other direction:


	i_and_r_value = i_value;

This is permitted by the type system as an explicit narrowing of the type, 
just as in:

	real	r = 7;
	int	i;

	i = r;

Both of these cases involve a run-time check to make sure the narrowing 
involves a value of a compatible type.  In the 'int = real' case, the 
program checks to make sure the value is an int, while in the 
'i_and_r_value = i_value' case, the program checks to make sure 'i_value' 
contains a struct with both 'i' and 'r' fields of the correct types.

> I tried to compare this with what happens between the int and rational
> types which seem to have a similar relationship as the just_i and
> i_and_r types above. But, it's an error to try a similar assignment
> with these types:
> 
> 	> rational r = 12 / 37;
> 	> int i;
> 	> i = r
> 	Unhandled exception "invalid_argument"
>         	"Incompatible types in assignment"

Note that this generates a run-time exception, not a compile-time type 
error.  That's because the narrowing is allowed at compile time but 
explicitly checked at runtime.  Try instead:

	> rational r = 12 / 3;
	> int i;
	> i = r;

Note here that the run-time check isn't simply ensuring that the value 
stored in 'r' was originally an 'int', it's actually checking that the
computation resulting in the value produced a value that can be exactly 
represented as an int:

	> rational r = 12 / 37;
	> int i;
	> i = r * 37;

I suspect one source of your confusion is the super/sub type relations
among the numeric and structured types.  A super type is a type which can
represent all of the values of it's subtypes, plus some additional values.
That's most obvious in the numeric types where 'real' is a super-type of
'rational' which is a super-type of 'int'.  In general, it's always
statically type-safe to use a sub-type any place it's super-type is used.

Extending this to structures means that a structure with *more* elements is
a sub-type of a structure with *fewer* elements; that's the way the static
type safety is preserved -- any place the structure type with fewer
elements is used can obviously be satisfied by any value containing more 
elements.

Think of an object system with sub-classes -- the sub-class always extends 
the super-class with *more* members and methods.

> So, perhaps, (with no guessing), nickle could allow the programmer to
> specify default initialization values so that similar behavior could also
> be possible for structured values?

Yes, we could extend the structure type to include default values for all 
of the structure elements:

	typedef struct {
		int	i = 12;
		real	r = 7.1;
	} i_and_r;

But, Bart's plan is to leave the structure variable uninitialized and 
statically analyse the program to ensure the variable is initialized 
before use.  This will catch errors instead of masking them with possibly 
incorrect values.  If you want to have a default value for your structure,
you can just store it in a global variable when you declare the type.

	i_and_r		i_and_r_value;

	if (one_way)
		i_and_r_value = (i_and_r) { i = 1, r = 2 };
	else
		i_and_r_value = (i_and_r) { i = 2, r = 1 };
	return i_and_r_value;

This will pass the simple static analysis, while a future change:

	i_and_r		i_and_r_value;

	if (one_way)
		i_and_r_value = (i_and_r) { i = 1, r = 2 };
	else if (the_other_way)
		i_and_r_value = (i_and_r) { i = 2, r = 1 };
	return i_and_r_value

will elicit a compiler error.  Java does this and describes the simple
static analysis so that programmers aren't (too) surprised.  Our
analyser will be complicated by closures and local functions:


	int() foo () {
		int	j;
		int	bar () { return j; }
		j = 7;
		return bar;
	}

> That might even make it possible for Bart to keep his convenient
> automatic structured value creations, no?

The automatic structured value creations are more likely to mask bugs than
help the programmer.

-keith