03 — Declaring and Resolving Names
The resolver's two core operations are declaring a name (introducing a new binding) and resolving a reference (computing its depth).
Declaring a name
void Resolver::declare(const std::string& name) {
if (scopes_.empty()) return; // global — skip
auto& scope = scopes_.back();
if (scope.count(name))
reportError("Variable '" + name + "' already declared in this scope.");
scope[name] = false; // declared, not yet defined
}
void Resolver::define(const std::string& name) {
if (scopes_.empty()) return; // global — skip
scopes_.back()[name] = true; // fully defined
}
The declare/define split means:
let x = x;→declare("x")setsx→false, resolve initialiserx→ findsfalse→ error "can't read local in its own initialiser".let x = 1;→declare("x"), resolve initialiser1(no names),define("x")→ no error.
The visitLet method
void Resolver::visitLet(LetStmt& s) {
declare(s.name);
if (s.init) resolve(*s.init); // initialiser can NOT see s.name yet
define(s.name);
}
The visitVar / visitFunction
var works the same as let for the resolver — mutability is an
interpreter-level concern (cp-04 step 05), not a resolution concern.
Functions:
void Resolver::resolveFunction(FnExpr& fn) {
beginScope();
for (auto& param : fn.params) {
declare(param);
define(param); // params are immediately defined
}
resolve(*fn.body);
endScope();
}
Parameters are both declared and defined before the body is resolved. There's no initialiser for parameters — they're always provided by the caller.
The visitVarExpr method
void Resolver::visitVarExpr(VarExpr& e) {
if (!scopes_.empty()) {
auto it = scopes_.back().find(e.name);
if (it != scopes_.back().end() && it->second == false)
reportError("Can't read '" + e.name + "' in its own initialiser.");
}
resolveLocal(&e, e.name);
}
The self-initialiser check only looks at scopes_.back() — the current
scope. If the name is in an outer scope and is false, it's a different
(outer) variable in mid-initialisation, not a problem for the current
reference.
Block scopes don't "hoist"
In JavaScript, var declarations are hoisted to the top of the function
scope. In MiniLang, let and var are not hoisted — a reference
before the declaration is a static error:
print x; // error: "x" not found (resolver reports it)
let x = 1;
The resolver only adds a name to scopes_.back() when it encounters the
let/var statement. Any VarExpr seen before that point falls through
the entire scope stack and is treated as a global. If x is not a global
either, the error is caught at resolve time. This is strictly better than
runtime "undefined variable" errors.