amuletml / amulet

An ML-like functional programming language
https://amulet.works/
BSD 3-Clause "New" or "Revised" License
328 stars 16 forks source link

Core improvements #15

Open SquidDev opened 6 years ago

SquidDev commented 6 years ago

This is a collection of improvements we could make to the core IR or tools which deal with the IR. Some of these may be totally stupid, but some may be worth pursuing.

Join points

One of the problems with the current IR, is that any match expression in a non-tail position is just bound to a normal let. This reduces our ability to do optimisations like match-of-match. I propose adding join points (or continuations, or basic blocks) to the IR. Namely,

let a = match ... of
        | Cons (x, xs) -> Cons (f x, xs)
        | Nil -> Nil
match a of
| Cons (x, xs) -> x
| Nil -> 0

gets lowered to:

let cont = join x -> match ... of
                     | Cons (x, xs) -> x
                     | Nil -> 0
in match ... of
   | Cons (x, xs) -> cont (Cons (f x, xs)
   | Nil -> cont Nil

Join points act identically to normal lambdas, but with several restrictions:

This ensures we always know what join point we are calling, and all join points know where they are called: this should hopefully allow for additional optimisations.

Unboxed tuples

The issue with the current backend is that it generates a fresh closure for every argument. This is fantastic for currying, but less optimal for efficiency of the generated code. Ideally we could compile fun x y z -> ... into function(x, y, z) ... end most of the time.

It may be possible to get the backend to perform this optimisation, but that seems rather flaky (and will not work well with higher-order functions). Instead I propose adding unboxed tuples to the core. This would reduce fun x y z -> ... into fun a -> match a with | (# x, y, z #) -> .... Passes which do this optimisation may chose to only lower a subset of the arguments (say if a function is often partially applied like compose).

It may also be possible to lower the type arguments to higher order functions (such as foldr, which will never curry the function). This'll be much harder to implement though. Something which would be really fancy would be converting functions which operate on records to operate on unboxed tuples instead.

Various optimisations

Backend improvements

let map f xss = match xss with
              | Nil -> Nil
              | Cons (x, xs) -> Cons (f x, map f xs)

this can be compiled to something like:

local function map(f, xss)
  if xss[1] == "Cons" then
      local res = { "Cons", { f(xs[2][1]), nil } }
      while true then
        -- For the rest of the loop, patch up the previous term and continue
        if xss[1] == "Cons" then
          local this = { "Cons", { f(xs[2][1]), nil } }
          last[2] = this
          xss, last = xss[2][2], this
        else
          last[2] = Nil
          break
        end
      end
      return res
  else
    return Nil
  end
end

Obviously this requires some non-trivial code generation (and duplicates a reasonable amount of code), so it's something we're going to have to think about. It would be amazing to have, as it makes the map implementation much nicer, but it doesn't result in the nicest code.

plt-amy commented 6 years ago

Unboxed tuples

Evil! We should do this in a separate IR with first-class support for multiple return/result instead of piling it on Core.

plt-amy commented 6 years ago

However, as Lua 5.1/5.2 operate on floating points we can't guarantee they are associative.

Thankfully our semantics don't depend on Lua.