fir-lang / fir

MIT License
23 stars 3 forks source link

Unboxing fields #10

Open osa1 opened 2 months ago

osa1 commented 2 months ago

We should provide a way to unbox fields.

The problem we should solve is the indirections added as you "wrap" a type in other types, to add functionality.

For example, I have an immutable string type in the standard library:

type Str:
    # UTF-8 encoding of the string.
    data: Array[U8]

and I want to implement another string type in another library that adds a memoized hash code field to the type:

# A string with memoized hash code.
type HashStr:
    str: Str
    hash: Option[U64]

This implementation will have a layer of indirection when accessing the string.

Another example would be implementing a stack using a Vec:

# Same as `Vec`, but doesn't allow random access.
# Not an alias to avoid using it as `Vec`.
type Stack[T]:
    vec: Vec[T]

fn Stack.push(self, elem: T) = self.vec.push(elem)
fn Stack.pop(self): Option[T] = self.vec.pop()

Note that in this example fields of the Vec in array will change as a result of some of the operations: push will re-allocate a new array and update the Vec's array field.

Another, and very common, example is iterator types: array and array iterator, string in character iterators etc. For example:

type StrIter:
    str: Str            # indirection when accessing the str contents
    byteIdx: Usize

type VecIter[T]:
    vec: Vec[T]         # indirection when accessing the vec contents
    idx: Usize

Notes

Unboxing a type in a field is only possible if the unboxed type has no mutable fields, or the type with the unboxed field owns (i.e. initializes with the fields) the value.

As an example to the first case:

type Str:
    data: Array[U8]

type StrIter:
    str: Str
    byteIdx: Usize

fn StrIter.new(str: Str) =
  StrIter(str = str, byteIdx = 0)

Since strings are immutable (we don't have a syntax for mutable/immutable fields yet), the data field will never change once initialized. So StrIter constructor can take a Str argument and unbox it in its str field.

As an example to the second case:

type Vec[T]:
    array: Array[T]
    len: Usize

type Stack[T]:
    vec: Vec[T]

Stack can't take a Vec argument in its constructor and unbox it. The argument may be aliased, and other references to it can push elements and cause the len and array fields to change.

However it can unbox the Vec field if it allocates it (so there are no other aliasing at the time of allocating Stack), and we allow interior pointers so others can alias the Vec in Stack (e.g. if we provide a method that returns self.vec).

A variation of the second idea is we allow unboxing just one field, and generate code so that the values of the unboxing type can work as a value of the unboxed type. In the example above, this means Stack can be passed to a function that expects Vec. I.e. non-coercive subtyping, this is basically the approach some OO langs take when extending classes.

osa1 commented 1 month ago

A variation of the second idea is we allow unboxing just one field, and generate code so that the values of the unboxing type can work as a value of the unboxed type. In the example above, this means Stack can be passed to a function that expects Vec. I.e. non-coercive subtyping, this is basically the approach some OO langs take when extending classes.

Relevant Rust RFC from 2014: RFC 223.