Johan-Mi / scratch-compiler-2

Yet another language that compiles to Scratch
The Unlicense
3 stars 0 forks source link

Return variable clobbering #3

Open Johan-Mi opened 10 months ago

Johan-Mi commented 10 months ago

If an expression contains multiple calls to the same function, the return variable gets clobbered:

fn f(_ a: Num, _ b: Num) -> Num {
    a + b
}

let n = f(1, 2) + f(3, 4)

# =>

block f(a, b) {
    _f = a + b
}

f(1, 2)
f(3, 4)
# Whoops!
let n = _f + _f

Then there's recursion but that's too much of a pain for me to even think about.

LukeGrahamLandry commented 10 months ago

(just in case an idea is helpful:) You could implicitly declare a list and use that as a stack to pass return values. So the code above code would de-sugar like:

let n = f(1, 2) + f(3, 4)

# =>

block f(a, b) {
    push(_stack, a + b)
}

f(1, 2)
f(3, 4)
let n = get(_stack, len(_stack) - 1) + get(_stack, len(_stack))
pop(_stack)
pop(_stack)

And then I think you get recursion for free, because each function cleans up after all its internal calls like the native call-stack,

fn fib(_ n: Num) -> Num {
    if n < 2 {
        n
    } else { 
       fib(n - 1) + fib(n - 2)
   }
}

let a = f(3)

# =>

block fib(n) {
     if n < 2 {
        push(_stack, n)
    } else {
       fib(n - 1)
       fib(n - 2)
       # clobbering here is fine because it's never held across a call, storing it just makes the indices easier to think about
       let _temp = get(_stack, len(_stack) - 1) + get(_stack, len(_stack))
       pop(_stack)
       pop(_stack)
       push(_stack, n)
   }
}

fib(3)
let a = get(_stack, len(_stack))
pop(_stack)

Presumably there's a performance cost over just using a variable for functions that are only ever called once in an expression but perhaps an optimisation could notice that case and switch calling conventions when possible.

Johan-Mi commented 10 months ago

One idea would be to use variables by default, perform a flattening step to put all sub-expressions into variables (that might get optimized away later if deemed unnecessary) and then do liveness analysis to detect clobbering and switch to the stack approach.

Johan-Mi commented 10 months ago

Another problem: what if two scripts call a function at the same time? I really don't know what to do about that.

Johan-Mi commented 10 months ago

The MIR-based codegen has unintentionally fixed non-recursive non-parallel clobbering since it currently puts every expression in a new variable.

Johan-Mi commented 10 months ago

This will, however, break again when I implement linearity analysis.

Johan-Mi commented 9 months ago

Here's a really dumb idea: custom block parameters can't be clobbered, so the compiler could use a Lisp-like implementation of let that desugars variables into immediately invoked functions:


(let ((a 1))
 (use a))

; =>

((lambda (a) (use a)) 1)