bonobo-lang / bonobo

Strongly-typed, safe, opinionated systems language that compiles to C.
https://bonobo-lang.github.io
Apache License 2.0
14 stars 1 forks source link

Compilation strategy for polymorphism #60

Open thosakwe opened 6 years ago

thosakwe commented 6 years ago

In Bonobo, we have support for inheritance. Obviously, C does not. What C does have, though, are overloads for different types.

To compile methods that take parameters of types that have multiple forms, we can generate overloads.

For example, take the type Int. This is not 100% a perfect example, because integers are primitives, but Int in reality can have different sizes: Int8, Int16, Int32, Int64, all of which map to specific C types.

Say you have a function, add, in a module foo, that takes two Ints:

fn add(a: Int, b: Int): Int => a + b;

Creating overloads that take the different variants of Int, compiled to C, is not that difficult:

int32_t add(int8_t a, int8_t b);
int32_t add(int8_t a, int16_t b);
int32_t add(int8_t a, int32_t b);
int32_t add(int8_t a, int64_t b);
int32_t add(int16_t a, int8_t b);
int32_t add(int16_t a, int16_t b);
int32_t add(int16_t a, int32_t b);
// and so on...

This way, the C compiler would not compile about the types of the passed integers.

thosakwe commented 6 years ago

However, the question is: How to determine the correct return type? Obviously it makes no sense for each overload to return int32_t.

thosakwe commented 6 years ago

What would be smart is for BonoboTypes, or at least number types to have a size member, so that the compiler could easily deduce the right return type.

If the signature is:

fn add3(a: Int, b: Int, c: Int): Int

Then for each combination (ex. int32_t, int8_t, int16_t), the return type would be the variant of the largest size.

Thus, we can create the following:

int8_t add(int8_t a, int8_t b);
int16_t add(int8_t a, int16_t b);
int32_t add(int8_t a, int32_t b);
int64_t add(int8_t a, int64_t b);
int16_t add(int16_t a, int8_t b);
int16_t add(int16_t a, int16_t b);
int32_t add(int16_t a, int32_t b);
// and so on...

int16_t add3(int8_t a, int16_t b, int8_t c);
int16_t add3(int8_t a, int16_t b, int16_t c);
int32_t add3(int8_t a, int16_t b, int32_t c);
int64_t add3(int8_t a, int16_t b, int64_t c);
// and so on...
thosakwe commented 6 years ago
class Size {
  final int c, llvm;
  const Size({@required this.c, @required this.llvm});
}

class BonoboType {
  Size get size;
}
thosakwe commented 6 years ago

The strategy of determining the correct parameters works nicely for numbers, but what about non-numbers?

type Automobile { fn run(fuel: Double): Void }

class Car : Automobile {
  run(fuel: Double) => print('Vroom!')
}

class Plane : Automobile {
  run(fuel: Double) => print('Whoosh!')
}

The generated C might look something like this:

typedef struct {} Automobile;

void Automobile_run(Car* this, double fuel) {
  Car_run(this, fuel);
} 

void Automobile_run(Plane* this, double fuel) {
  Plane_run(this, fuel);
}

typedef struct {
  Automobile* super;
} Car;

Car* Car_new() {
  Car* instance = (Car*) malloc(sizeof(Car));
  return instance;
}

void Car_run(Car* this, double fuel) {
  print("Vroom!");
}

typedef struct {
  Automobile* super;
} Plane;

Plane* Plane_new() {
  Plane* instance = (Plane*) malloc(sizeof(Plane));
  return instance;
}

void Plane_run(Plane* this, double fuel) {
  print("Whoosh!");
}
thosakwe commented 6 years ago

The question is, what about a return type? Obviously, structs can't be inherited, but using void* is to haphazard.

What if:

typedef struct {
  Car *car = NULL;
  Plane *plane = NULL;
} Automobile;

This solution works well, IMO.

thosakwe commented 6 years ago
fn carFactory: Car => Car::new()

Generates:

Automobile* carFactory() {
  Car *car = Car_new();
  Automobile *automobile = (Automobile*) malloc(sizeof(Automobile));
  automobile->car = car;
  return car;
}

The compiler is definitely going to have to do a lot of heavy lifting, but I feel that this is a viable way to handle classes and polymorphism in the compiler.