adam-mcdaniel/oakc

Typecheck against non-void statement expressions, and add return statement

Closed this issue · 5 comments

Currently, the stack is managed poorly. Consider the following program.

fn five() -> num { 5 }

fn main() {
     five();
     five();
     five();
}

In this program, we call a function five which returns a number: a value with a size of 1 on the stack. This is bad. It creates something like a "stack memory leak", the value that five returns is left on the stack, and it is never consumed throughout the program.

I think that discarding the value would work just fine, but I think typechecking against non-void statements would be more elegant.

fn side_effect() -> void {
    // ...
}

fn five() -> num { 5 }

fn main() {
    // type error: non-void expression is not used
    five();
    // works fine
    side_effect();
    // works fine
    let n = five();
}

This would mean that functions that return multi-cell values must change.

type Date(3) {
    fn new(m: num, d: num, y: num) -> Date {
        // this now throws a type error
        m; d; y
    }
}

Instead, the return statement should be introduced. Each function must either not have a return statement, or have a return statement at the very end of the function body. Additionally, with an explicit return statement, typechecking the result of a function is now possible.

type Date(3) {
    fn new(m: num, d: num, y: num) -> Date {
        // pushes m, d, and y onto the stack
        return m, d, y;
    }
}

fn test1() {
    // type error: void function returns non-void expression
    return 5;
}

fn test2() -> num {
    // type error: non-void function does not return expression
}

fn test3() -> num {
    // works
    return 1;
}

This is addressed in PR #16

Hm, so i assume [1, 1, 1] groups multiple types to get a sum of its size? Type casting is must have, but I noticed a lot of examples are forced to cast. I assume you have type inference in mind in the future? So if a function is returning a type bool(1) and its return value is of size 1 should it be infered to be a cast?
But the programmer also has to be careful with references and non-references.

Hm, so i assume [1, 1, 1] groups multiple types to get a sum of its size?

Yes, [1, 1, 1] has a size of 3, which is compared to the size of the return type. Return statements with multiple arguments are not checked for type equality. The return of a single value, though, is directly compared to the return type using type equality. The rules for comparing type equality can be adjusted in the PartialEq implementation for MirType. Right now, the rules are as following:

  1. If two types are identical in every way, they are equal.
  2. The types char and num are interchangable.
  3. If both types have a single pointer depth (one & in front of the type name), and one of the types is a void pointer, then both types are equal.

Do you have any ideas for changes I should make to the rules? Keep in mind that I want to eliminate as many type errors as possible while still maintaining flexibility in the type system.

Rule nr 1 is a bit unclear in what counts as identical are references and nested references considered equal? I assume &&T considered not equal to &N since they are different types. But you could make it work with casting if you want to shoot yourself in the foot?

Im not that well versed in type theory so this is not where I can contribute much. I think the current rules should work great until you find a case where they wont. Incremental steps seems like a solid plan for now.

For two types to by identical, they must have the exact same type name and exactly the same depth of references. So, T is only identical to T, &T is only identical to &T, &&T is only equal to &&T, etc.

I think the current rules are fine for now, but I agree that a certain degree of type inference / lenience should be added in the future for member getters.