I’ve been putting this one off too long.
Felix has two boolean types: bool and cbool. cbool is just C++ bool, which is
typically one byte. If you want to store a boolean value used as a component
in a cstruct, you have to use cbool because bool is 64 bits. bool and cbool are
inter-convertable with explicit constructors.
At present bool is the native meaning comparisons all generate bool not cbool.
The definitions are:
typedef bool = 2;
type cbool = "bool" requires index TYPE_cbool;
It’s worth notion that
//$ Short cut and via closure
noinline fun andthen (x: bool, y:1->bool) : bool =>
if x then #y else false
;
//$ Short cut and via closure
noinline fun orelse (x: bool, y:1->bool) : bool =>
if x then true else #y
;
These are expensive! Although
fun land: bool * bool -> bool = "$1&&$2"; // x and y
devolves to C shortcut operator, Felix can and does unravel function arguments,
which can defeat the lazy evaluation.
Now you may ask, and this is the point of this article, why 64 bits?
The answer is that bool = 2 is just one of a family of special types called
compact linear types. Products of compact linear types are compacted
into a single 64 bit unsigned integer. For example
2 * 3
occupies only the values 0..5. In particular it uses the standard representation
for variable radix natural numbers so that
true, false, true
is actually using just 3 bits. So an array of 64 bools uses a single 64 bit
machine word.
Surprisingly at first, the components of a compact linear product are
addressable. For example
var x = true, false, true;
px = &x.1;
assert *px == false;
px <- true;
assert x == true, true, true;
Compact linear pointers use three machine words: the base pointer of the
64 bit word, an integer to divide by, and an integer to find the remainder
with respect to. For powers of 2, these are just right shift and mask
operations, respectively.
Now, the main reason for compact linear types is to provide polyadic arrays.
If you have an array:
var x : double ^ (2 * 3);
then you can index it with a value like (1,2), so it’s a matrix, but you can
also coerce it to the type
double ^ 2 ^ 3
and index it like x . 2 . 1 (note reversed order!) OR you can coerce it to
a integer index
double ^ 6
and put it in a SINGLE loop to scan all elements. ALL array indices in Felix
are compact linear and so ALL arrays, irrespective of index type, can be
coerced to a linear array, allowing all the elements to be scanned
in a single loop. The reason is that arrays on a 64 bit machine simply
cannot be larger than 2^64 elements, not because your computer doesn’t
have enough memory .. but because it doesn’t have enough address space
to do anything larger. So the restriction to 64 bits is not a restriction in practice
if you’re doing arrays with compact linear index types.
Of course if the compact linear type is ITSELF an array, then even the
smallest value type, a bool, leads to a restriction of only 64 elements,
and less if the types are bigger.
Given a CONCRETE type, that is, a monomorphic type, Felix figures out
when it can use compact linear types automatically. This could lead
to a surprise that type 2^128 doesn’t work! You have to use cbool ^ 128.
The restriction could be removed, but it’s tricky for say base 3.
However the REAL problems start with polymorphism:
2 * T
You don’t know if this is compact linear or not until T is replaced.
If T = int, its not compact linear, if T = bool it is.
Now 2 * T is a tuple type but it is NOT an array, even if T is eventually
replaced by type 2. This aint C++ crap, Felix respects parametric polymorphism.
What this means is you’re only allowed to do things to polymorphic types
that work for ALL possible replacements of T, and parametricity says,
that can be ANY type at all.
Now the magic now says, well, we don’t care, we can still do pointer
operations on any product, since compact linear types have pointers.
The problem is the pointers are different types! Here’s the crux of the problem:
For ordinary types, a pointer projections are associatiive, which means,
a pointer to a type T nested in a more complex type, is just a pointer to T.
If a T is a product, we can point to one of its components and that
pointer is stand alone, we don’t care how we got into there.
This is NOT true for compact linear pointers, because they have to point
first to a 64 bit machine word, and then we use math operations to extract
or set component values. All these operations compose just fine for
monomorphic types because we know when we hit the word boundary
to switch pointer models.
But we do NOT know with poiymorphic types.
The problem is that ordinary products
A * B
and compact linear products use the same notation. The interpretation
for ordinary products is “Addressable byte sequence with padding for
alignment” whereas for compact linear products it is all math operations
on a fixed size integer. So when you have a a deeply nested product
with a type variable deep:
A * (B * ( C * (D * T)))
you really don’t know how to address ANY of the components until after
T is replaced. If A,B,C,D are compact linear, the addressing operations
depends on whether T is or not. But polymorphism says that isn’t allowed.
What it comes down to is we don’t know if the * is an ordinary product
or a compact linear product.
Now in theory, the solution is just laziness. The problem is a polymorphic
pointer now needs to keep the CONTEXT of the addressable component
around until we know where the compact linear type boundary is.
We will know after monomorphisation, but the problem is that polymorphically
we have to keep a whole list representing a projection chain. In other
words, a pointer to the T in the above type is not just &T, but actually
&(A * .,…) . .1.1.1
We can’t just write &T because if the type is compact linear, we have lost
the location of the 64 bit machine word.
—
John Skaller
ska...@internode.on.net