bun icon indicating copy to clipboard operation
bun copied to clipboard

Bun does not throw when call stack is exceeded

Open max opened this issue 2 years ago • 3 comments

Version

0.1.5

Platform

Darwin maxbookpro.local 22.0.0 Darwin Kernel Version 22.0.0: Tue Jun 28 20:46:36 PDT 2022; root:xnu-8792.0.134.131.2~1/RELEASE_ARM64_T6000 arm64

What steps will reproduce the bug?

bun run the following code:

const foo = () => bar();
const bar = () => foo();
foo();

How often does it reproduce? Is there a required condition?

Every time

What is the expected behavior?

bun should throw similar to jsc:

Exception: RangeError: Maximum call stack size exceeded.
[email protected]:2:22
[email protected]:1:22
...

What do you see instead?

The call stack grows until bun consumes 100% of the CPU.

Additional information

No response

max avatar Jul 31 '22 01:07 max

call stack isn't actually growing because of tail call optimization, the question is do we want to limit how much you can take advantage of it

evanwashere avatar Jul 31 '22 08:07 evanwashere

Test case can be simplified.

const foo = () => foo();
foo(); // does not throw
const foo = function () {
  return foo();
}
foo(); // does throw

@evanwashere In my opinion, it should be limited, because infinite loops should be avoided.

JL102 avatar Aug 02 '22 23:08 JL102

A lot of current JS is being written in a more functional style, making TCO quite favorable

vrmiguel avatar Aug 05 '22 02:08 vrmiguel

@JL102 That second example no longer throws, the tail call is being optimised per ES6

ethanqm avatar Oct 09 '22 06:10 ethanqm

I recommend to resolve this as "no issue." This is working as intended by bun, and also implemented as specified.

Node.js does not implement TCO (See: https://node.green and this blog and their proposal) but there is no rule that bun or other JS runtimes (system, browser, embedded, etc) must follow Node.js instead of the language specification. Straying from the spec makes code less portable and less valuable.

booniepepper avatar Sep 21 '23 20:09 booniepepper

BTW if anyone legitimately needs a workaround to force a stack overflow, and you own the code then you can simply make your recursive call not in the tail position.

Example with one extra line for a variable bind:

const noTco = () => {
  const res = noTco();
  return res;
}

Demo with bun-repl:

$ bunx bun-repl
Welcome to Bun v1.0.1
Type ".help" for more information.
[!] Please note that the REPL implementation is still experimental!
    Don't consider it to be representative of the stability or behavior of Bun overall.
> const noTco = () => { const res = noTco(); return res; }
undefined
> noTco()
1 | const noTco = ()=>{
2 |
3 |     const res = noTco();
                   ^
RangeError: Maximum call stack size exceeded.
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)
      at noTco (:3:16)

booniepepper avatar Sep 21 '23 21:09 booniepepper

This is because WebKit supports tail-call optimization, which is not supported by V8 or Node.js. While it has its downsides, like the example you provided, it overall is an improvement to performance.

Electroid avatar Oct 24 '23 22:10 Electroid