Rec function called by Promise is not compiled to loop

This is the playground link:
example

@val external setTimeout: ((.unit) => unit, int) => int = "setTimeout"

let sleep = (time: int) => {
  Js.Promise.make((~resolve, ~reject) => {
    let _ = setTimeout((.) => {
      resolve(. ignore())
    }, time)
  })
}

let rec play = (index: int) => {
  Js.Promise.then_(_ => {
    play(index + 1)
  }, sleep(1000)) 
}

let rec normalRec = (index: int) => {
  Js.log2(`call with index: `, index)
  normalRec(index + 1)
}
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';


function sleep(time) {
  return new Promise((function (resolve, reject) {
                setTimeout((function () {
                        return resolve(undefined);
                      }), time);
                
              }));
}

function play(index) {
  return sleep(1000).then(function (param) {
              return play(index + 1 | 0);
            });
}

function normalRec(_index) {
  while(true) {
    var index = _index;
    console.log("call with index: ", index);
    _index = index + 1 | 0;
    continue ;
  };
}

exports.sleep = sleep;
exports.play = play;
exports.normalRec = normalRec;
/* No side effect */

The recruise function play is not compiled into while loop, this may leads to some memory leak, is that can be comiled into while loop?

Do you have a more realistic example? This function play is basically a no-op. It just sleeps for a second at a time, yields, then sleeps, and so on, forever.

This is a more realistic example writted in ts.
ts example

type Item = {
  delay: number
}

async function sleep(time: number) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(time)
    }, time)
  })
}

function getFrame(): Item {
  return {
    delay: Math.random() * 1000
  }
}

async function playFrame(item: Item) {
  // do something...
  // wait for delay
  await sleep(item.delay)
}

// play 10000 frames
async function playFrames() {
  for (let i=0; i<10000; i++) {
    // simulate getting a frame to play 
    const frame = getFrame()
    await playFrame(frame)
  }
}

This code use will never cause memory leak because of using async/await and for loop,but if you use recurise function, it will crached because of stack overflow.

Hi, play is a not a tailcall by definition. Do you have logs for the stack overflow?

My point is that why the function play is not compiled into loop.

If you transpile your TS example to ES5 you may notice that your “for … await” cycles are not as simple as you think.

1 Like

In order for ReScript to compile recursive function calls into a loop the function must be defined in a way that uses tail-recursion.