gleam-lang / gleam

ā­ļø A friendly language for building type-safe, scalable systems!
https://gleam.run
Apache License 2.0
17.76k stars 741 forks source link

Apply tail call optimsation on JS when piping into self with a function capture #3167

Open giacomocavalieri opened 4 months ago

giacomocavalieri commented 4 months ago

Using pipes and function captures makes it so the compiler won't TCO a function on the js target. Consider:

pub fn do_len(list, acc) {
  case list {
    [] -> acc
    [_, ..rest] -> do_len(rest, acc + 1)
  }
}

This code gets optimised to:

export function do_len(loop$list, loop$acc) {
  while (true) {
    let list = loop$list;
    let acc = loop$acc;
    if (list.hasLength(0)) {
      return acc;
    } else {
      let rest = list.tail;
      loop$list = rest;
      loop$acc = acc + 1;
    }
  }
}

However, if we rewrite the code using a pipe and a function capture it no longer applies the optimisation:

pub fn do_len(list, acc) {
  case list {
    [] -> acc
    [_, ..rest] -> rest |> do_len(_, acc + 1)
    // Even the opposite wouldn't be optimised:
    // { acc + 1 } |> do_len(rest, _)
  }
}
export function do_len(list, acc) {
  if (list.hasLength(0)) {
    return acc;
  } else {
    let rest = list.tail;
    let _pipe = rest;
    return ((_capture) => { return do_len(_capture, acc + 1); })(_pipe);
  }
}

I would expect the generated code to look roughly the same for both examples. I had a look at the codegen code and it looks like the fix would be matching on TypedExpr::Fn where the body is just a single call (that's what pipes get translated to in the typed AST). That does feel a bit hacky though... maybe this is another point that would benefit from having pipes in the typed AST, then we could be smarter with the code generation and avoid this problem altogether?

lpil commented 4 months ago

Please include the code that doesn't optimise šŸ™

giacomocavalieri commented 4 months ago

Ooh sorry I thought I did šŸ˜… Edited the original message to include the code

lpil commented 4 months ago

This is due to the function capture being used. If you run the formatter it would be removed and the resulting code would be optimised. It would be nice to optimise for the captures too though.

giacomocavalieri commented 4 months ago

Yeah I think this optimisation would be really nice to have, it bit me with glam because in the pretty printing loop I did a lot of this:

list.map(docs, fn(doc) { #(indent, mode, doc) })
|> list.append(rest)
|> do_to_string(acc, max_width, current_width, _)

Thinking the do_to_string function would be optimised and never noticed until someone built a really big document šŸ˜‚

giacomocavalieri commented 4 months ago

I'd like to give this a try!