Viscosity
Review
Part III · A tree-walking interpreter · Day 10

39. Functions in Drip

A Drip function is a runtime value with one special power: you can call it. Calling it means a fresh environment, the parameters written into it, and the body run inside.

About 18 minutes, including the exercises and quiz
By the end you will be able to
  • Explain what a callable is, and what the interpreter checks before calling one.
  • Trace a call of a Drip function, from the callee lookup to the new environment and the body.
  • Explain arity, and why Drip is stricter about it than JavaScript.
  • Add a native function to the interpreter, and say why a language needs them.

Naming a piece of work

Drip can loop and choose now, but every piece of work has to be written out where it’s used. Chapter 1 started JavaScript with the cure for that, and Drip gets it now:

fn add(a, b) {
  print a + b;
}

add(1, 2); // 3
add(3, 4); // 7

Two new pieces of syntax. fn add(a, b) { ... } is a declaration, which the parser turns into a node holding the name, the parameter tokens and the body’s statements. add(1, 2) is a call expression. In the Pratt table, ( is an infix operator with the highest binding power (tied with .): whatever expression is on its left is the callee. So f(1)(2) parses fine: it calls whatever f(1) returns.

A function is a value

Intuition

A function declaration is a recipe card being filed under a name. Filing it cooks nothing. Calling the function is taking the card out, clearing a fresh bit of worktop, laying out the ingredients you were handed, and following the steps on that worktop.

Executing the declaration only files the card:

case 'function':
  this.env.define(s.name.lexeme,
    new DripFunction(s, this.env));
  return;

(The real line passes one more argument, a flag that only matters for classes.) A DripFunction holds the declaration node and the environment that was current when it was declared (its closure; chapter 41 is all about why). Then add is defined like any other variable. Functions are first-class runtime values in Drip, just as in JavaScript: you can copy one with let g = add;, and printing one shows <fn add>.

What makes a function special is a single extra ability. In the companion’s runtime.ts, anything you can call has this shape:

interface DripCallable {
  arity(): number;
  call(interp: CallContext,
       args: Value[]): Value;
}
Definition

A callable is a runtime value that can be called. In Drip it has two methods: arity(), the number of arguments it expects, and call(interp, args), which does the work and returns a value. Drip functions are callables; so are native functions, and, in chapter 43, classes. Numbers, strings, nil and booleans are not.

Evaluating a call

Here is the call case of evaluate, with long lines folded:

case 'call': {
  const callee = this.evaluate(e.callee);
  const args = e.args.map(
    (a) => this.evaluate(a));
  if (!isCallable(callee)) {
    throw new RuntimeError(e.paren,
      'Can only call functions ' +
      'and classes.');
  }
  if (args.length !== callee.arity()) {
    throw new RuntimeError(e.paren,
      `Expected ${callee.arity()} ` +
      `arguments but got ${args.length}.`);
  }
  return callee.call(this, args);
}

Evaluate the callee, then the arguments, left to right. Then two checks: is the callee callable at all (123() is not), and does the number of arguments match? Only then is it called. (e.paren is the ) token, kept so the error can report a line.)

And here is what calling a DripFunction does, minus a detail for classes:

call(interp, args) {
  const env = new Environment(
    this.closure);
  this.decl.params.forEach((p, i) =>
    env.define(p.lexeme, args[i]));
  return interp.executeFunctionBody(
    this.decl, env);
}

A brand-new environment, whose parent is the function’s stored environment. Each parameter is defined in it, holding its argument. Then the body runs with that environment as the current one, through the same executeBlock you wrote for blocks. Step through two calls:

Step throughWhat add(1, 2) does
let k = 10;
fn add(a, b) {
let s = a + b;
print s + k;
}
add(1, 2);
add(3, 4);
The globals, the function value, the environment made for a call, and the checks a call expression makesglobalsk = 10output:
let k defines k in the globals, as before.
1/11

Because every call makes its own environment, two calls never share parameters or locals. That’s exactly what recursion needs: each call of a recursive function gets its own n.

Notice too which environment is the parent: the one where the function was declared, not the one it’s called from. That is lexical scope from chapter 6, and you can see it in this example:

Try itThe parent is where the function was written
fn show() {
  print x;
}

let x = "global";
{
  let x = "local";
  show();
}

Arity

Definition

A function’s arity is the number of arguments it takes: for a Drip function, the number of parameters it declares. Drip checks arity on every call, and a mismatch is a runtime error: Expected 2 arguments but got 3. The callee isn’t run at all.

JavaScript is far more relaxed. It reports a function’s expected count as f.length[3], but doesn’t enforce it. A missing argument just makes the parameter undefined, and extra ones are quietly kept aside[2]. Drip chooses to fail loudly, which catches a forgotten argument at the call that forgot it.

Native functions

Some things can’t be written in Drip at all. Drip has no way to read the time: no expression or statement asks the outside world anything. So the interpreter supplies one ready-made function, written in TypeScript:

class NativeFunction {
  constructor(n, fn) { ... }
  arity() { return this.n; }
  call(_, args) {
    return this.fn(args);
  }
}

// in the Interpreter constructor:
this.globals.define('clock',
  new NativeFunction(0, () =>
    performance.now() / 1000));

clock() returns seconds as a number, from JavaScript’s performance.now(), a high-resolution timer in milliseconds[5]. From Drip’s side it’s an ordinary global holding a callable: clock() passes the same two checks as add(1, 2). Printing it shows <native fn>.

Try itTiming a loop with a native
print clock;

let start = clock();
let sum = 0;
for (let i = 0; i < 100000; i = i + 1) {
  sum = sum + i;
}
print sum;
print clock() - start;
Definition

A native function is a callable implemented in the host language (here TypeScript) instead of in the language being run. Natives are how a language reaches anything outside itself, such as the clock, files or the screen, and how it offers operations that would be too slow or impossible to write in the language itself.

Under the hood

JavaScript’s functions are objects too[4], with properties like length. Drip’s are TypeScript objects with a decl and a closure. Look at what one call costs here: an args array, a new Environment with its own Map, and a define per parameter, before the body even starts. Chapter 56 shows how the bytecode VM does a call with no maps at all, by giving each call a window onto one shared stack. Drip’s design of callables, arity checks and the clock native follows Crafting Interpreters[1].

Your turn

ExerciseCalls with arity checks

Write the two halves of a call.

  • makeNative(n, fn) returns a callable: an object with callable: true, arity() returning n, and call(args) returning fn(args).
  • callValue(callee, args) calls callee with the array args. If callee is not an object with callable === true (numbers, strings and null aren't), throw an Error with the message Can only call functions and classes. If args.length differs from callee.arity(), throw Expected N arguments but got M. without calling it. Otherwise return what callee.call(args) returns.
function makeNative(n, fn) {
  // your code here
}

function callValue(callee, args) {
  // your code here
}

const add = makeNative(2,
  (xs) => xs[0] + xs[1]);
console.log(callValue(add, [1, 2]));
Extra practiceYour first Drip function

The loop below calls square, which doesn’t exist yet. Above it, declare a Drip function square(n) that prints n * n, so the program prints 1, 4, 9, 16, 25.

// declare square here

for (let i = 1; i <= 5; i = i + 1) {
  square(i);
}

What you learned

  • Executing fn doesn’t run anything: it makes a function value holding the declaration and its environment, and defines the name.
  • A callable is a value with arity() and call(). A call expression evaluates the callee and the arguments, checks that the callee is callable and that the arity matches, then calls it.
  • Calling a Drip function makes a fresh environment whose parent is the function’s own, defines the parameters there, and runs the body in it.
  • A native function is implemented in the host language, like clock. It’s how Drip reaches outside itself.

Next: return, and how the interpreter escapes from deep inside a body.

Check your understanding

0/6
  1. 1.

    What does this print?

    fn greet(name) {
      print "hi " + name;
    }
    print "start";
  2. 2.

    fn f(a, b, c) {} then f(1, 2); What happens?

  3. 3.

    Which of these is NOT a callable in Drip?

  4. 4.

    When add(1, 2) is called, where are a and b defined?

  5. 5.

    What does this print?

    fn show() { print x; }
    let x = "global";
    {
      let x = "local";
      show();
    }
  6. 6.

    Why does the interpreter provide clock as a native function?

References

  1. [1] Robert Nystrom (2021). Crafting Interpreters. Free web edition at craftinginterpreters.com (© 2015–2021); source, text and test suite at github.com/munificent/craftinginterpreters. github.com/munificent/craftinginterpreters (checked 2026-10-31)
  2. [2] MDN contributors. Functions (JavaScript Guide). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions (checked 2026-10-31)
  3. [3] MDN contributors. Function: length. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length (checked 2026-10-31)
  4. [4] MDN contributors. Function. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function (checked 2026-10-31)
  5. [5] MDN contributors. Performance: now() method. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/API/Performance/now (checked 2026-10-31)