Open-addressing hash maps, parent-pointer scope chains, and why redeclaration is a runtime error.
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.
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*.
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.
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.
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/.