phase / mars

Minimal programming language with type inference, structures, control flow, and more.
https://jadon.io/mars/
Mozilla Public License 2.0
20 stars 0 forks source link

Polymorphism #7

Open phase opened 7 years ago

phase commented 7 years ago

When designing polymorphism, we need to take the LLVM backend into account, as it is the main backend (and currently the only one that exists).

Inheritance subtype polymorphism

Here is some example syntax for how I want to do this.

class A
    let a : Int32
;

class B : A
    let b : Int32
;

initB () : B
    let b = new B(),
    b.a = 7,
    b.b = 8,
    b

Classes in the AST will hold one parent Class.

class Clazz(val parent : Clazz, ...)

This will create two types in IR. There are two different ways to express these types.

%A = type { i32 }
%B = type { i32, i32 }
%A = type { i32 }
%B = type { %A*, i32 }

The second way poses a problem: Is %A* the parent class or a field? With the first way, we know every element in the type is a field, with some of them coming from the parent class.

Generics parametric polymorphism

Generics are a great for code reuse and other stuff, so here are some designs for them.

class Box<T>
    let value : T
;

main ()
    let boxInt32 = new Box<Int32>(),
    boxInt32.value = 32,
    let boxInt64 = new Box<Int64>(),
    boxInt64.value = 64l,
    0

Generics can be stored inside the Type class.

class Type(val generics : List<Type> = listOf(), ...)

Only the needed types will be generated.

%Box_Int32 = type { i32 }
%Box_Int64 = type { i64 }

These will be generated in the getLLVMType function. When parsing, we need to send the generic types in the current Class to getType. Then we can check if the fields of a Class are one of the generic types.

phase commented 7 years ago

Ugh. I attempted generics in https://github.com/phase/lang-kotlin-antlr-compiler/commit/a238a0c6bef831e4e4571ba31d2c6f3496235379, but it went downhill quickly. I think restarting it after I plan it out a little will be better. Class generation went the same way.

phase commented 7 years ago

Inheritence: Embedded Fields

Classes can have multiple parents.

class W
    let w = 0
;

class A : W
    let a = 0
;

class B : W
    let b = 0
;

class C : A, B
    let c = 0
;

These will create IR types with the fields embedded in them.

%W = type { i32 }

%A = type {
    i32, ; w from W
    i32  ; a from A
}

%B = type {
    i32, ; w from W
    i32  ; b from B
}

%C = type {
    i32, ; w from W
    i32, ; a from A
    i32, ; b from B
    i32  ; c from C
}

We can then bitcast types to their parents.

; Assume this function modifies the fields in A
declare void something(%W*)

define void something_calling_something(%C*) {
    %C_as_W = bitcast %C* %0 to %W*
    ; This will treat the beginning bits as W's fields
    call void @something(%W* %C_as_W)
    ret void
}

Where does this go horribly wrong? Downcasting. What happens when we cast a %C* to a %B*?

define void wrong(%C*) {
    %C_as_B = bitcast %C* %0 to %B*
}

What's going on in the memory?

%C = type {
    i32, ; w from W
    i32, ; a from A
    i32, ; b from B ; This is the third field
    i32  ; c from C
}

%B = type {
    i32, ; w from W
    i32  ; b from B - This is the second field
}

%C_as_B = type {
    i32, ; w from W
    i32  ; a from A - Oh shit
}

The fields of B are offset because we needed to make room for A's fields. This version of storing fields doesn't work for multiple parents.

phase commented 7 years ago

Another thing that needs to be figured out is calling an overridden method. If B overrides a method in A, and we have an instance of A that was downcasted from B, how do we know which method implementation to call?

This should be done in Type Checking (leaving this here because this thought has come to my mind more than once and I keep forgetting the solution).

phase commented 7 years ago

Types dependent on values and types can be used to define Integer types with variable bits or Arrays with a fixed size.

// identifier -> generic type
// a : b -> value of type b
class Array<T, size : T>

class Int<size : Int8>

This could be used for internal types, but user defined types wouldn't have much of a use because you coud easily pass in a parameter to the constructor.

let a = new Int(29, 7)

This could create a new Int29 with a value of 7.

phase commented 7 years ago

Single inheritence solves the downcasting problem.

Interfaces/Traits are another thing to be added, prefering the latter.