ExpandCollapse

+ 1 Pointer modes

In Felix, pointers have an associated access mode, these are called cv-qualifiers in C++. The original modes are shown in the diagram below:

    N
   / \
  R   W
  \  /
   RW

The lower modes are submodes of the higher modes, and pointers to the same type are subtypes if their modes are submodes. The abbreviations mean:
NNo access
RRead only
WWrite only
RWRead and write

The type and expression annotations are illustrated below:

  var x : int = 1;
  var rwp : &int = &x; // read write pointer
  var rp : &<int = &<x; // read only pointer
  var wp : &>nt = &>x; // write only pointer
  // currently N mode has no syntax

We need to recall for pointers:

  var x = 1;
  var p = &x;
  var q = *p;  // fetch operation
  p <- 2;      // store operation

The * operator is called dereference from C, however in Felix it does not create a reference or lvalue, it just fetches a value. The only references in Felix are variables.

Although in principle R pointers are covariant in their target, and W pointers contravariant (making RW pointers invariant), Felix current only supports invariant targets.

There is a well known problem with read-only pointers: the read only attribute only applies to the pointed at object. If you consider a list in which nodes contain a value and a pointer to the next node, a read only pointer to a node does not prevent writing to the next node if the next pointer is read-write. That is, the read-only property only has a shallow effect when we really wanted a deep impact. Here's an example:

  var x   : int      = 1;
  var p   : &int     = &x;   // read write pointer to int
  var rpp : &<(&int) = &<p;  // read only pointer to read-write pointer to int
  var rwp : &int     = *rpp; // read write pointer to int
  rwp <- 2;                  // write to x
  println$ x;                // x is 2 now

+ 2 Views

Given a pointer p. we define the data structure associated with p as the set of all the objects reachable from p.

A functional view of a data structure is a read only view of the whole data structure. A view pointer mode, abbreviated V, also called propagating const in C++, is a pointer mode with the semantics that ensure that no object of the data structure is writable (via that pointer), and thus provides the required deep effect.

We use the following annotation for view mode pointers:

  var x : T;
  var vp : &<<T = &<<x;

To see how these work, we repeat the previous example but using a view:

  var x   : int        = 1;
  var p   : &int       = &x;    // read write pointer to int
  var vpp : &<<(&int)  = &<<p;  // view pointer to read-write pointer to int
  var vp  : &<<int     = *vpp;  // view pointer to int
  // rwp <- 2;                  // TYPE ERROR

In this example, the propagation of the view mode occurs when the view pointer is dereferenced: type usual fetched type &int is hacked with a reinterpret cast, in effect, to have type &<<int instead, this preventing any write derived from the view pointer by fetch operation.

+ 3 Viewification

The implementation is not entirely straight forward. The coercion is entirely at the type level and does not impact values: it is called viewification.

+ 3.1 Viewifying a pointer type

Clearly, when we have a view pointer whose target is a pointer, fetching the target must discard write ability, so the following conversions are used on the mode of the target pointer:

V -> V
R -> V
RW -> V
W -> N
N -> N

Indeed we consider V a submode of N, and R a submode or V, and thus the corresponding pointer subtyping relations. Think of subtyping coercions as "throwing out capability".

+ 3.2 Viewifying a primitive type

However the target of a pointer need not be a pointer. If the target is a primitive type such as int the coercion is the identity and the target is unchanged. Felix currently applies this rule to all types lifted from C/C++ (even though it is clearly inadequate) because to do more would require additional annotations on the type (in particular a view mode version of the type would have to be cited at the point of definition).

+ 3.3 Viewifying a structural type

If the target is a structural type such as a product (tuple or record) or sum type (sum or polymorphic variant) then the coercion is applied to each component (recursively).

+ 3.4 Viewifying a type variable

If the target is a type variable, then the type variable is marked as being a view. Subsequently when the type variable is replaced, the replacement is first viewified.

Note that if the target is a pointer to a type variable, the type variables is not viewified. There is no need, because the pointer mode has been viewified.

+ 3.5 Viewifying a structured nominal type

There is one final case: the target is a struct, cstruct or variant; that is, a nominally typed product or sum type. In this case, actually coercing the type would require introducing a viewified version of it which would be a supertype of the original. This would require a notation, and we would also need to add a subtyping coercion to the symbol table: and we'd need to do this even if the viewified version was never used.

Even worse, it would no longer be possible to just change a routine which currently visits a data structure without modifying it to take a view pointer, so as to actually prove this fact, because the nominal types involved would also change. This would have a cascading effect on the standard library and create a serious maintenance problem. I would note this has indeed happened historically when const was introduced to C: and a lot of code was broken.

Instead, we simply mark references to nominal types as views, and convert the components "on the fly" when they're accessed.

+ 4 An realistic example

Here is an example:

  begin
    // option type
    variant opt[T] = | None | Some of T ;
  
    // list node type
    struct Node { a:int; next: opt[&Node]; }
  
    // construct a sample list
    var px = unbox (new Node (1, None[&Node] ));
    px = unbox (new Node (2, Some (px)));
    px = unbox (new Node (3, Some (px)));
  
    // a pretty printer for the list
    // which proves the list is not changed
    fun pNode (var px:&<<Node) =>
      (px*.a).str + ", " +
      match  px*.next with
      | None => ";"
      | Some px => pNode px
      endmatch
    ;
  
    println$ px.pNode;
  end

Notice the node type uses a RW pointer, but the print routine uses a V pointer. The routine is recursive, and the view mode propagates through the recursion.

The node type is a nominal product, and the option type is a nominal variant, the same as the one in the standard library, and involves a type variable.

The critical point is that *px is a viewified version of Node, and so the projection next applied to it is a viewified version of opt[&Node] which if populated has the Some constructor with argument of type viewified &Node which is type &<<Node as required for the recursion to go through.

+ 5 Application to C++

I note finally that these rules could easily be applied in C++ by adding a "propagating const" cv-qualifier, in fact the implementation would actually be simpler because C++ does not support type checked polymorphism, so there is no need to mark type variables (that is; none of the rules are needed until after template instantiation). C++ also has no write only mode, and no proper no-access mode: a void* may suffice, however in Felix N mode pointers, whilst not supporting access to the target, do support address calculations.

+ 6 A need for mode polymorphism

As a final note, the existence of these 5 modes in Felix would mean that generalised pointer projections would be impossible to express; for example given a pointer into one data structure, you might want to find a corresponding pointer into another, with the same mode. To do that you'd actually need to write the algorithm out 5 times. To fix that, we'd need mode polymorphism!