What is reentrancy in JavaScript?
Asked Answered
D

3

13

I would like to improve my understanding of the word reentrant.

Is this function reentrant?

function* foo() {
  yield 1;
  yield 2;
}

And this one?

function foo() {
  return 1;
}

And this one?

var x = 0;
function foo() {
  return x++;
}

And this one?

function foo() {
  setTimeout(foo, 1000);
}
Dabble answered 7/12, 2015 at 9:13 Comment(4)
See #2799523 as well, it's a C++ related question, but the answer remains the same.Esquimau
In case anyone is wondering - It's not a duplicate question since that one is not about JavaScript and C++ has a different execution model.Bimah
@Ben Hi, did you find the 'correct' answer, would you mind share it to us?Stack
Wow, I nearly finished writing my below answer before I realised that I had already answered What does reentrancy mean in JavaScript?Vshaped
B
8

A reentrent function is a function whose execution can be resumed:

In computing, a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely called again ("re-entered") before its previous invocations complete execution.

In browser/node JavaScript, all multiprocessing is cooperative (no interrupts or context switches). A regular function always runs to completion in JavaScript. (1)

So in your case - the only reentrent function is the first one since it doesn't run its code to completion and can be resumed at a later point.

  • The second function is just a regular function.
  • The third one uses an outer scope, which is kind of similar because it lets a function hold some state. It's not the same thing though since the function can't be resumed.
  • The fourth one just runs to completion immediately (it schedules another invokation of it - but that's up to the platform and not JavaScript).

Indeed - one can say that generators enable cooperative multitasking in JavaScript with a reentrent syntax. Before generators all code ran to completion.

(1) Or it never halts, but it is never interrupted. Also - in common platforms. There are platforms (like Rhino) that break the rule. They're very rare and don't use the same concurrency execution model as browser/node JS.

Bimah answered 7/12, 2015 at 9:15 Comment(8)
Why 2 is not reentrant? While the javascript execution model doesn't allow it to be called multiple times at once, it is still reentrant. All regular functions are reentrant.Warner
Because it cannot be interrupted in the middle of its execution and then safely called again before its previous invocation completes execution - plain and simple. More simply put - it can just not be interrupted in the middle of its execution. This is unlike generators that can declare points of interruption (yields) and be "pumped" to execution completion.Bimah
No. Simply no. That goes against the definition. You can call (2) multiple times at once and it will still return consistently the same result. That is what makes it reentrant. The fact that JS doesn't support parallel execution doesn't have anything to do with it. The definition of reentrancy is not different for different languages.Warner
JS supports parallel execution (just not threads, well, the platforms JS runs on do). I think the bit you're missing is that reentrency requires that the function can be interrupted to begin with.Bimah
@freedomn-m actually, called multiple times with the same arguments resulting in the same result is purity. Idempotency means calling it multiple times has the same effect as calling it once.Bimah
That was just a simplification of the definition. I still disagree. All pure functions (e.g. (2)) are automatically reentrant. One of the basic condition for reentrancy is that "reentrant code cannot call non-reentrant routines". If you say that (2) is not reentrant, you won't be able to fulfil this condition.Warner
@Warner If you disagree with this answer, please provide your own and the community can arrive at a consensus.Dabble
@Warner check my answer below, what do you think?Lynnelle
V
1

Let's start with a definition of re-entrancy (shortened from Wikipedia):

A function or subroutine is said to be reentrant when it can be called and then be safely called again ("re-entered") before its previous invocations complete execution.

The requirements that are usually listed for a function to be called reentrant if satisfied are not so important, they can be derived from this definition. Now there's two core parts of the definition that we need to look at:

  • what does it mean for a call to be "safe"?
  • how can it happen that a function is called again before it finished execution?

The first point is crucial - and it can only be answered specifically per function. In general, all the calls (the first, and the re-entering ones) must fulfill the contract of the function, i.e. do what the programmer expects and what the function is documented to do - i.e. there must be no bugs introduced. For your four concrete examples, you haven't given any contracts, and the functions do rather trivial things that won't get messed up from multiple calls at arbitrary points, so any call to them is "safe". I'll give some more exciting examples below.

Now, how can a call to the function happen while it is already executing? This is where JavaScript specifics come into play:

  • Recursion. Modern languages typically support recursive calls by providing a call stack at the runtime where local variables and return addresses can be stored in stack frames. This is different from some early languages (or languages that offer safety guarantees by limiting stack usage) where all variables where essentially static, and recursive calls were not supported (or essentially became gotos) that the compiler hopefully warned you about.
    This is not an issue in JavaScript, but so it becomes a possible source of re-entrant calls.
  • Preemptive multithreading. With multiple independent threads, the same function might be called in two threads at the same time, with overlapping execution. Modifying any shared state becomes a problem obviously. This does not happen in standard JavaScript runtimes though, where code always runs to completion without being interrupted and cannot share any state between threads.
  • Synchronous callbacks. With first-class functions, it becomes easy to supply dynamic code to be executed during a function call, like in Array.map for example. If your function accepts and calls a callback function, that function might again call your function (usually with different arguments), which becomes another source of re-entrant calls. This becomes particularly ugly if the callbacks to user code are not intended or clearly visible, but part of implicit type coercions (toString, valueOf etc).
  • Cooperative multitasking/concurrency. JavaScript has generator functions and async functions that can be suspended and resumed later (synchronous or not). In between, the function might be called again. So every yield and await becomes another possible source of re-entrant calls.
  • Asynchronous callbacks/continuations. If your function starts a task that "runs" asynchronously and maybe has a callback that is invoked when the task finishes, you could say the "execution" in the definition of re-entrancy refers to the whole task and not just consider the synchronous part of the function. While the function strictly is no longer running, any other call to the same function while the task is still running that time would be considered another source of re-entrancy into that task.

Now what are some examples where the correctness of the program is affected by re-entrant calls?

  • Here we have a recursive function that is obviously broken:

    var sum = 0, i;
    /** traverse the array-or-number tree and sum all numbers */
    function traverse(tree) {
        if (!Array.isArray(tree)) sum += tree;
        else for (i = 0; i<tree.length; i++) traverse(tree[i]);
    }
    

    (it wouldn't be so obvious if I had left i implicitly undeclared). The recursive calls will mess with the i counter variable of the still-running loops in the outer stack frames, leading to incorrect results or infinite loops. Declaring i (and sum) inside the function would make this safe.

  • Less obviously problematic:

    class Account {
        #balance = 0;
        get balance() { return this.#balance; }
        /** Add the positive amount to the account's balance */
        deposit(amount) {
            if (amount < 0) throw new Error();
            this.#balance += amount;
        }
        /** Transfer the positive amount from the first to the second account */
        static transfer(amount, sender, recipient) {
            const source = sender.#balance, target = recipient.#balance;
            amount = Number(amount);
            if (amount < 0 || source < amount) throw new Error();
            sender.#balance = source - amount;
            recipient.#balance = target + amount;
        }
    }
    

    I've hinted at the problem already by not just using -=/+=, and doing the numeric cast suspiciously late. (Still better than leaving it to +/-, which would give the caller a way to pass an object that identifies with different numeric values at different times!). So here's how you could abuse it:

    const accountA = new Account(), accountB = new Account(), accountC = new Account();
    accountA.deposit(50);
    Account.transfer({
        valueOf() { Account.transfer(50, accountA, accountC); return 50; },
    }, accountA, accountB);
    console.log(accountA.balance, accountB.balance, accountC.balance);
    

    Whoops, transer is not re-entrant (although you have to go out of your way to call it again while it's running), and this leads to a bug where the same money can be spent multiple times.

  • An example where a generator function is unsafe to re-enter can be found here

  • Last but not least, an example with an async function where the issue is hopefully apparent by now:

    class Account {
        #balance = 0;
        get balance() { return this.#balance; }
        /** Add the positive amount to the account's balance */
        deposit(amount) {
            if (amount < 0) throw new Error();
            this.#balance += amount;
        }
        static async #processPayout(amount) {
            if (amount < 0) throw new Error();
            if (amount >= 10000) await getDirectorApproval(this, amount);
            return amount + calculateFee(amount);
        }
        /** Transfer the positive amount from the first to the second account */
        static async transfer(amount, sender, recipient) {
            if (typeof amount != "number") throw new Error();
            sender.#balance -= await this.#processPayout(amount);
            recipient.#balance += amount;
        }
    }
    

    When you call transfer before the previous execution has completed, it takes the old sender.#balance into account.

    const accountA = new Account(), accountB = new Account();
    await Account.transfer(50, accountA, accountB);
    console.log(accountA.balance, accountB.balance); // -50, 50 - as expected
    

    but

    const accountA = new Account(), accountB = new Account(), accountC = new Account();
    await Promise.all([
        Account.transfer(50, accountA, accountB),
        Account.transfer(50, accountA, accountC), // call before the transfer to B is complete
    ]);
    console.log(accountA.balance, accountB.balance, accountC.balance); // -50, 50, 50 - this doesn't add up!
    
Vshaped answered 18/5 at 22:5 Comment(0)
L
-2

JavaScript doesn't have the concept of reentrancy, because for reentrancy you need a second thread to call the same function that's being paused.

With even with the yield of example (1) the function is being paused and resumed on next iteration but you cannot have another thread call that same function again while it's paused simply because there is no other thread. If you do it inside a single thread like in the code below and call again the generator function, it returns you a new and different function. That's why it's called a generator function. So you don't call the same one.

function* foo(index) {
  while (index < 2) {
    yield index;
    index++;
  }
}

const iterator = foo(0);

console.log(iterator.next().value);
// Expected output: 0

const iterator2 = foo(0);
if (iterator === iterator2) console.log('same');
else console.log('not same function');

console.log(iterator.next().value);
// Expected output: 1

console.log(iterator2.next().value);
// Expected output: 0

Output:

> 0
> "not same function"
> 1
> 0

All in all to talk about reentrancy you need multithreading. Without multithreading answers are purely theoretical. One could argue that all JavaScript functions are reentrant, because they are not vulnerable to reentrancy issues. And another one could say they are not because they cannot be interrupted.

Even with yield is the function being interrupted? Not really, it just returns a new generated function that will run to completion to cover one iteration.

Again it's just a theoretical discussion because even if we get it wrong nothing bad is going to happen because we don't have a second thread to call it concurrently a second time !

Lynnelle answered 17/5 at 15:50 Comment(1)
This is wrong, in particular "nothing bad is going to happen". JavaScript functions definitely can be re-entered before they're finished, it doesn't need preemptive interruption for that. And the issue is not theoretical at all.Vshaped

© 2022 - 2024 — McMap. All rights reserved.