degory / ghul

compiler for the ghūl programming language
https://ghul.dev
GNU Affero General Public License v3.0
4 stars 0 forks source link

Union variant member access and variant type test properties #1139

Open degory opened 8 months ago

degory commented 8 months ago

Variants provide access to their fields via member syntax. However unions do not provide a clean way to determine what variant they contain or to read the current variant, and we don't yet support pattern matching (#1134).

A union value can be tested to determine if it holds a given variant with (isa SomeVariant(union_instance)) and then that variant can be extracted with a cast (cast SomeVariant(union_instance)) but this is rather ugly and wordy.

While we don't have pattern matching, we need an interim mechanism to allow convenient testing of what variant a union holds and safe access to union variants without casting.

A simple solution would be to add properties to the union object so it can be interrogated to determine whether it holds a particular variant and to get each variant. Attempting to access a variant that the union is not holding should throw an exception.

For additional convenience, unions that are option shaped (i.e. contain only one non-unit variant) should support the ? ('has value') and ! ('unwrap') operators by providing the has_value and value properties. It might also be useful to be able to define a particular variant as being the main value for unions with more than one non-unit variant, and to unwrap the union to this variant with .value / !

union Tree[T] is
    NODE(left: Tree[T], right: Tree[T]);
    LEAF;
si

union Result[T,E] is
    OK(value: T);
    ERROR(error: E);
si

union Option[T] is
    SOME(value: T);
    NONE;
si

testing if a union holds a given variant:

let t: Tree[string];

if t.is_node then
    ...
fi 

testing if an option shaped union holds the main variant

let o: Option[int];

if o.has_value then
   ...
fi

// since o has a `has_value: bool` property, the `?` operator can
// be used to test `has_value`:
if o? then
   ...
fi

extracting given multi field variants from a union

let t: Tree[int];

let node: Tree[int].NODE;
let left: Tree[int];
let right: Tree[int];

node = t.node; // will throw an exception if t isn't holding a NODE

// node is the NODE(Tree[int], Tree[int]) variant, so it has left and right fields
left = node.left;
right = node.right;

extracting single field variants from a union

let r: Result[int,string];

let good_result: int;
let error_result: string;

// single field variants are extracted as the value of their field:
good_result = r.ok; // r.ok is an int, not Result[int,string].OK
error_result = r.error; // r.error is a string, not Result[int,string].ERROR

extracting the main variant from an option shaped union

let o: Option[int]

let value = o.value;

If a union is option shaped and the single non-unit variant has only a single field, as above, then value is the value of that variant's single field. Otherwise value returns the whole variant.

let o: Option[T];

// does o hold the SOME variant?
if o.is_some then
    // get the SOME variant
    let s = o.some;
fi

// since Option[T] is option shaped (has a exactly one non-unit variant):
if o.has_value then
    let v = o.value;
fi

if o? then
    let v = o!;
fi

let r: Result[int, string];

if r.is_ok then
    let v = r.ok;
fi

if r.is_error then
    let e = r.error;
fi

Note that the variant names are all transformed to lower case in the names of these properties. While we could use MACRO_CASE for the variant names where they occur in the property names, this would be inconsistent with how casing is applied elsewhere: in ghūl the casing conventions for symbol names generally override any convention that might apply to the symbol name in other contexts. For example acronyms that might be in upper case in normal text are still lower-cased in ghūl method names and properties.

A fairly straightforward and performant way to implement these properties would be to inject appropriate property definition parse trees with virtual read bodies into the classes that represent unions and variants. In the union the properties would throw (if getting a value) or return false (if checking if the union holds a given variant). Then in the variants, the methods could be overridden to return the appropriate value.

This is complicated by scoping and possible name clashes between variant fields and the accessor methods. For example we want to be able to support these two common patterns, both of which would alias variant field names and corresponding union properties:

union Option[T] is
    SOME(value: T); // we want to be able to name variant fields 'value' even if their union might get a synthesized `value` property
    NONE;
si

union Result[T,E] is
    OK(value: T);
    ERROR(error: T); // we want to be able to name variant fields the same as the variant, even though their union will get a synthesized property with the same name
si

So rather than using properties with inheritance, we can use a slightly more involved scheme with both methods and properties. The methods can be given mangled names and hidden so they can't conflict with any fields in the variants. The properties can be configured to call the methods directly to avoid a double call (we don't have syntax for this but the underlying implementation of properties supports it)

class Option[T] is
    is_some: bool => false;
    $get_is_some() -> bool => false; // property read accessor method for is_some, which is already mangled and hidden

    some: T; // property for some
    $get_some() -> T => throw ... // base implementation of the read accessor

    has_value: bool => false;
    $get_has_value() -> bool => false;

    value: T => throw ...
    $get_value() -> T => throw ...
si

class Option$SOME[T]: Option[T] is
   value: T field; // the variant's field, hides doesn't override union's value property

   $get_is_some() -> bool => true; // override read accessor for is_some in union without actually adding a property
   $get_some() -> T => value; // override the read accessor for some in union without adding a property 
   $get_value() -> T => value; // override the read accessor for value in union without adding a property
si  

Ideally we should fix the inheritance mechanism for variants so that they do not inherit properties from their union parent