Skip to content

Step 1 of 3 · Reading · ~3 min

Return and Recursive Functions

Control Flow & Functions

Return Values & Recursion

Now that functions can be called, they need a way to hand a value back to the caller from anywhere in the body — not just the last line. return is unusual among statements: it has to abandon whatever nested blocks, loops, or if branches it's currently inside and jump straight back to the point of the call.

return as non-local control flow

A tree-walking interpreter executes statements by recursively calling execute(). return needs to unwind an arbitrary number of stack frames of that recursion — through nested if/while/block — without every intermediate execute() call needing to know about it. The idiomatic way to do this in a tree-walker is to raise an internal signal and catch it exactly at the function-call boundary:

python

And in LoxFunction.call, catch it right where the function's own body finishes executing:

python

This is not "using exceptions for errors" — it's using the exception mechanism's unwinding behavior to implement a control-flow feature, which is a completely different use case and a standard technique in interpreter implementations (the "Crafting Interpreters" book calls this pattern out explicitly).

Recursion falls out for free

Nothing about recursion needs special-casing once functions and return work correctly, because every call to LoxFunction.call — including a call from within the function's own body — creates its own fresh Environment. fib(5) calling fib(4) and fib(3) doesn't reuse or clobber the first call's n; each activation has its own environment on the (Python) call stack, chained to the function's closure, not to each other.

fun fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}
print fib(10); // 55

Walk through why this works: return n inside the if raises ReturnSignal(n), which propagates up out of the if's block execution and is caught by that specific call's call() frame — not some other call's. Because you're piggybacking on Python's own native call stack for your interpreter's recursion, Lox recursion depth is bounded by Python's recursion limit; deep recursion (e.g. naive fib(35)) will be slow and can hit RecursionError if you don't raise sys.setrecursionlimit.

Edge cases to watch

  • Return with no value (return;) must produce nil, not raise an error and not accidentally reuse a stale value from a previous statement.
  • Return inside nested blocks/loops: return inside a while body or a nested { } block must still unwind all the way to the enclosing function call, not just the innermost block.
  • Return at top level / outside a function is usually a parse-time or resolve-time error in a well-formed language — worth deciding how strict you want to be.
Up nextClosures — Functions That Capture StateClosures & Classes

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…