3

In C programming, stack overflow errors are outside of the language spec. They represent a fundamental violation of the "contract" of what a function call means. You can overflow the stack halfway through pushing arguments to a function. Or you can overflow it mid-way through a library routine like malloc() (internally to its own implementation might make several function calls that grow the stack, any one of which could overflow). An overflow could happen halfway through the bookkeeping it is doing for the allocation...leaving the heap in a corrupted state that would crash the next malloc() or free() if you tried to keep running.

In theory, it seems JavaScript could do better. It is not running on bare metal...so it could offer some kind of guarantees about operations that would not overflow the stack in mid-run (e.g. by preallocating memory for usermode recursions, vs. making every JS call trigger some C-level recursion in the interpreter). A JS interpreter could give you enough atomic building blocks for making a kind of transaction...where all of a set of operations would run (with no stack overflow) or none of them would (pre-emptively triggering a stack overflow). These transactions could be a wedge for monitoring what you would need to roll back at the moment of catching a RangeError exception upon running out of stack.

In practice, it seems to me that you're not much better off than C. So I think this answer is correct (2 upvotes at time of writing):

"you would have no indication on where your code broke so anything you tried to run would be severely buggy at best."

...and I think this answer is--at minimum--misleading in a generic sense (6 upvotes at time of writing):

"Maximum call stack size exceeded errors can be caught just like any other errors"

As an example, imagine I have this routine, and I wish to maintain the invariant that you never end up in a situation where the two named collections don't each have a value for a given key:

 function addToBothCollections(key, n) {
     collection1[key] = n
     n = n + Math.random()  // what if Math.random() overflows?
     collection2[key] = n
 }

If I had a "wedge" of guaranteed operations that would not overflow the stack, I could come up with a protocol where those operations were used to build some kind of transaction log. Then if an overflow occurred, I could tailor operations like this addToBothCollections() to take advantage of it.

e.g. imagine that negative numbers aren't allowed, so I could say:

 function addToBothCollections(key, n) {
     collection1[key] = n
     collection2[key] = -1
     n = n + Math.random()
     collection2[key] = n
 }

 try {
     /* ...code that makes a bunch of addToBothCollections() calls */ 
 }
 catch (e) {
     for (let key in collection2) {
         if (collection2[key] == -1) {
             delete collection1[key]
             delete collection2[key]
         }
     }
 }

But thinking along these lines can only work if you have guarantees, such as that this sequence will either atomically execute both operations or neither:

collection1[key] = n
collection2[key] = -1

If there's any way that collection2[key] = -1 might do something like trigger a GC that causes a stack overflow, you're back to square one. Some atomicity rules are needed.

So is there anything at a language level--current or future--that articulates this kind of problem? I'm actually curious about how this would apply to mixed JavaScript and WebAssembly programs...so if there's any way that you could cook up the atomicity I speak of in Wasm I'd be fine with that as well.

Andreas Rossberg
  • 34,518
  • 3
  • 61
  • 72
  • I do find this an interesting question. I've no information to offer but I'm curious what (if anything) can others say. With this aside, I think judging the linked answers as "correct" and "incorrect" is both not relevant and probably not very accurate. The inaccurate answer directly relates to the question which is "can I catch this error" and then further elaborates that even catching it as proposed isn't the correct way to handle this. So, to me this is the correct answer *for that question*. The accepted answer is also correct but in a more general case. – VLAZ Nov 18 '19 at 06:41
  • Some BASIC variants that have functions have no call stack because recursion is not permitted. So there is not possibility of stack overflow. Some others allow recursion and have call stack. So stack overflow is possible unless the system has infinity memory. So lets say stack safe are all functions that are non recursive and that call no functions that may call end up in recursion. So you need loop detection for your call tree which can consume time and because of JavaScripts dynamic nature the call tree may change often. – Lee Nov 18 '19 at 07:24
  • By the way the inline functions in c are guaranteed to not exceed call stack. – Lee Nov 18 '19 at 07:31
  • 1
    @Lee I thought C doesn't guarantee to inline an `inline` function though? – Bergi Nov 18 '19 at 09:23
  • @Bergi Yes only when the inline function is an real inline function recursion for example makes you inline function non inline. Never said that the inline keyword would help to find out if function inline or not. – Lee Nov 18 '19 at 10:03
  • @Lee In addition to there being no such thing in the C standard as a "forced fully inline" function...the compiler is free to implement any seemingly simple operations (even something like `+`) using function calls. And while it's very unlikely to do so with simple platform integers or bytes, it can be common when working with types not native to the processor architecture. But that's to say nothing of its freedom to make whatever optimizations it sees fit based on a stack in a dynamic fashion. – HostileFork says dont trust SE Nov 18 '19 at 12:07

1 Answers1

2

Due to its overly dynamic nature with features like getters/setters, proxies, property addition/deletion, mutable global, user-defined implicit conversions, and other features that can cause "action from the distance", almost every language construct in JavaScript can lead to the invocation of arbitrary user code. Even the statement x; can. The only construct that is guaranteed not to is ===.

For example, collection2[key] = -1, could invoke a setter on collection2 or one of its prototypes (including Array.prototype or Object.prototype), it could invoke a getter on the global object if collection2 or key isn't bound locally, it performs string conversion on key, which in turn might invoke toString or valueOf on it, which in turn might be defined by the user or invoke getters. And so on.

As a consequence, it is impossible to guarantee freedom from errors for almost any function but utterly trivial ones like

function f() {}
function g(x, y) { x === y }

Since you asked about Wasm, that has no such implicit features, every call is explicit, so it is way easier to reason about.

Andreas Rossberg
  • 34,518
  • 3
  • 61
  • 72
  • I think the more interesting question would then be: is an operation that does not invoke user code guaranteed not to produce a stackoverflow error, i.e. can these only happening when calling a function? And what about calling builtin functions like `Math.random()`? – Bergi Nov 18 '19 at 09:22
  • 2
    @Bergi, I doubt you can reasonably expect any such general guarantees, especially not across engines. There are way too many variables involved, both explicit and implicit. For example, many built-ins are implemented in JS itself on some engines. A JS function, even if just consisting of "harmless" operations, might have to spill some (or all) intermediate results to the stack in some optimisation modes on some architectures. Inlining might increase the stack use of a caller. And so on. – Andreas Rossberg Nov 18 '19 at 09:41
  • Thanks, that's what I wanted to hear. The argument "*can lead to the invocation of arbitrary user code*" didn't seem convincing :-) – Bergi Nov 18 '19 at 10:32
  • @Bergi, in practice that is the more serious hurdle, though. There simply is no realistic way to defend against that, there are way too many possibilities, and they keep growing with every language version. – Andreas Rossberg Nov 18 '19 at 14:25
  • @AndreasRossberg Thanks for confirming my suspicions *(sidenote: I'm curious how `x;` as a lone statement can run code)*. As you work on WebAssembly itself: I'll mention that my question is motivated by [this emscripten-discuss thread](https://groups.google.com/forum/#!topic/emscripten-discuss/CDyYsskh1D0)--if you might have anything to add there that would be appreciated! I'm not too interested in JavaScript myself, so it's a much bigger question to me if a completely different language built in Wasm could offer some more rigorous guarantees. – HostileFork says dont trust SE Nov 18 '19 at 14:57
  • @HostileFork, try `Object.defineProperty((function() {return this})(), "x", {get() {console.log("boo!")}}); x; x;`. As for Wasm: even there the stack use of a single function depends on architecture, implementation, and optimisations. And if you compile some *other* language to it, then on all the compiler's internals, too. – Andreas Rossberg Nov 18 '19 at 15:19
  • @AndreasRossberg That is unfortunate to hear, e.g. that Wasm isn't aiming for any kind of "quality of service" contracts/guarantees? That's a weakness compared to even small embedded boards; this would be needed for Real-Time OS applications--and I'm always wary of slippery slopes ("if I can't trust this for a car braking system, why should I trust it for [X,Y,Z]"). Alas. And looks like there's a [lot of confusion surrounding stack overflow handling in general](https://stackoverflow.com/questions/427209/is-it-possible-to-predict-a-stack-overflow-in-c-on-linux/431024#comment104098846_431024)... – HostileFork says dont trust SE Nov 19 '19 at 15:30
  • @HostileFork, stack frame sizes depend on many variables, and locking them down would pretty much be equivalent to restricting to a single implementation. I could imagine specific guarantees for Wasm running in very specific host environments, in a spec layered on top of Wasm (such as the implementation limits in the JS API), but I don't see how any such guarantees could reasonably be established across all platforms in the general spec without breaking its general utility. – Andreas Rossberg Nov 19 '19 at 19:52