Functions

Published: (January 5, 2026 at 02:04 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Screenshots

What I built

Commits 3da669d, 930014e, 5adb148, and 9d534f3

What I understood

1️⃣ Updating the Grammar

  • Precedence – Function calls now have the highest precedence, above the unary rule.
  • Callable – Introduced a Callable class (actually LoxCallable) to determine whether an entity can be invoked.
    • Example: a string isn’t callable, while classes and functions (user‑defined or native) are.
    • If the callee doesn’t implement LoxCallable, the interpreter raises a runtime error.
  • Currying / Chained Calls – The parser now allows constructs like (( "(" arguments? ")" )*, enabling calls such as function(arg)(arg)(arg).

Grammar diagram

2️⃣ Arguments

  • Arity checking – The interpreter verifies that the number of arguments supplied matches the number of parameters defined.

Arity check screenshot

3️⃣ Native Functions

  • Implemented a clock() native function in Java.
  • It’s an anonymous class that implements LoxCallable and is bound to the identifier clock in the interpreter’s global environment.
  • Returns the current system time in seconds, useful for benchmarking.

4️⃣ Function Declaration

  • Encountering fun creates a callable object (LoxCallable) and binds it to the function’s name in a new environment.
  • The function’s parameters are bound in that local environment.
  • Each function call also creates a fresh environment, enabling recursion.
  • After the function body finishes, the local environment is discarded and execution returns to the caller’s environment.

5️⃣ Return

  • The interpreter looks for a return statement optionally followed by an expression.
  • If no expression is provided, nil is returned implicitly.
  • When return is executed, the interpreter must unwind the current call stack immediately.
  • This is achieved with a simple exception class Return that carries the return value.
  • The call site wraps the function execution in a try‑catch block that catches this exception; if none is thrown, the function completes normally and returns nil.

6️⃣ Scoping

  • Previously Lox used dynamic scoping: a called function’s scope was always the global environment, not the environment where the function was defined.
  • This prevented functions from accessing variables captured at definition time once the defining environment had ended.
  • The fix is to capture the lexical environment (the environment that existed when the function was created) rather than deferring name resolution to the call site.

Example illustrating the problem (and the fix)

fun getName(name) {
    var person = name;
    fun greet() {
        print "Hello, " + person + "!";
    }
    return greet;
}

var momo    = getName("Momo");
var chutney = getName("Chutney");

momo();    // Should print "Hello, Momo!"
chutney(); // Should print "Hello, Chutney!"
  • Before the fix this would raise an error because person was looked up in the global scope.
  • After implementing lexical scoping each returned greet closure correctly captures its own person variable, producing the expected output.

Closures, Scoping, and the Resolver

1. Example of a Closure

function getName(name) {
  var person = "Hello, " + name + "!";
  return function () {
    console.log(person);
  };
}

var momo = getName("Momo");   // Returns a function that closes over `person`
momo();                       // → “Hello, Momo!”

function chutney() {
  console.log("Hello, Chutney!");
}
chutney();                    // → “Hello, Chutney!”

2. Why the First Example Fails

  • When getName("Momo") finishes, the variable person is destroyed because the function’s scope ends.
  • When momo() is later called, it tries to find person in its parent scope, which is now the global scope (which doesn’t contain person). → Error!

3. Lexical (Static) Scoping

  • We solve this problem by using lexical scoping.
  • When a function is declared, it closes over (captures) the environment surrounding it.
  • The inner function holds a live reference to the outer environment, preventing the garbage collector from destroying it even after the outer function returns.
  • When the inner function is later called, the captured environment becomes its parent scope instead of the global scope.

4. Resolver

Implementing closures through lexical scoping introduced another issue:

  • A closure holds a live reference to a mutable map representing the current scope.
  • Its captured view of the scope can change based on later code in the same block, which should not happen.

Because Lox has lexical/static scope, a variable’s usage must always resolve to the same declaration throughout program execution, even if the variable is later redeclared or redefined.

Example

var a = "one";

{
  fun showA() {
    print a;
  }

  showA();          // → "one"

  var a = "two";

  showA();          // Should still print "one", but would print "two" without a resolver
}
  • a belongs to the global scope with the value "one".
  • showA creates a closure that captures the block/environment where it is declared.
  • When showA() is first called, a is not found in the block, so the lookup proceeds to the global scope → prints "one".
  • After var a = "two" redeclares a in the same block, the mutable scope causes the second call to showA() to print "two"—incorrect behavior.

How the Resolver Fixes This

The resolver (added after the parser and before the interpreter) uses persistent data structures:

  • The original data structure cannot be tampered with directly.
  • Any change creates a new structure that contains the original information plus the modification.

This is analogous to a blockchain, where each new block references the previous block’s hash while containing the new change.

When the resolver processes a block and sees a variable being used (after its declaration), it performs a static pass:

Scope 0 (function's scope): a is not found
Scope 1 (block scope):    a is not found (var a = "two" hasn't been defined yet)
Scope 2 (global scope):    a is found
  • Because the variable is found 2 hops away from the innermost scope, the resolver annotates that use with the number 2.
  • At runtime, the interpreter uses this hop count to directly jump to the correct declaration, avoiding mutable‑scope surprises.

5. What’s Next?

We’re going to keep it Classy! 💅
(Classes and inheritance are up next.)

6. Musings

I’ve had the opportunity to witness some of the most wonderful and bewildering things these past few months. It feels like I’ve seen the full spectrum of life, much like Shakespeare’s seven stages of man. In a way, it really humbled me.

For a long time, I was afraid of letting go—of my beliefs, interests, and even some dreams—because I thought I’d lose my very identity. Now I’ve learned that, much like functions, it’s okay to have a template of non‑negotiables, upon which we can let life keep adding its own “implementations.” (I absolutely HAD to use that analogy—please excuse me.)

Change and unpredictability don’t seem so daunting anymore, and maybe (like 0.05 %) they’re good too. I suppose c’est la vie.

Back to Blog

Related posts

Read more »