← back to lessons
lesson 06 — intermediate

Variable scoping with JMap

Open-addressing hash maps, parent-pointer scope chains, and why redeclaration is a runtime error.

~25 min · environment.c jmap.c

Variables need to live somewhere between being declared and being used. In first, that somewhere is an Environment — a struct that wraps a hash map and a parent pointer.

JMap: a custom hash map

Rather than pulling in a third-party library, first implements its own hash map in src/jmap.c. It uses open addressing with linear probing: when two keys hash to the same slot, the second occupies the next available slot in the array. The key is always a char* variable name; the value is a RuntimeValue*.

Scope chaining

Each Environment has a parent pointer (NULL at the top level, set by environment_create(parent) — which is exactly what eval_block() calls for every { }). A recursive helper, resolve(), finds which environment in the chain actually owns a name:

Environment *resolve(Environment *self, char *name) {
  if (jmap_has(self->map, name)) return self;
  if (self->parent == NULL) return NULL;
  return resolve(self->parent, name);
}

environment_lookup_variable() calls resolve() then reads the value straight out of the owning scope's map.

Declare vs. assign

environment_declare_variable() calls resolve() first — if any scope in the chain (not just the current one) already owns the name, it errors and exits; otherwise it inserts into the current scope's map. This is the only path interpreter.c currently uses for every binding, including let and the chained let x = y = 5 form.

environment_assign_variable() is currently unreachable. It's meant to update an existing binding by walking up to parent scopes, but as written it unconditionally errors in both branches of its own if — and more importantly, nothing in interpreter.c calls it. There is no working bare x = expr reassignment in first today; every apparent "assignment" you can write from the grammar is actually a fresh declaration.

exercise

Create a new child environment from an existing one and declare a variable in the child. Can you access it from the parent? Can you access a parent variable from the child? Then look at environment_assign_variable() in environment.c and fix it so it actually updates the owning scope's binding via resolve() + jmap_update() — then wire a real reassignment statement into the grammar and eval_assignment_expression() to use it. Try it by modifying the test suite in test/.