Resolver in Action

The resolver plays a crucial role in handling variable scopes and ensuring that identifiers are correctly bound to their corresponding values within the program. Let's delve deeper into the role of the resolver in Lox:

#1 Scope Resolution:

  • The resolver is responsible for identifying the scope of each variable within the program. It tracks the nesting of scopes, including global scope, function scopes, and potentially nested block scopes.
  • When encountering variable references (identifiers), the resolver determines the scope in which the variable is declared or defined. It ensures that variables are resolved within their closest enclosing scope.

#2 Variable Declaration and Initialization:

  • The resolver handles variable declarations and ensures that each variable is properly declared before use. It tracks variable declarations and bindings within their respective scopes.
  • If a variable is declared within a function or block scope, the resolver ensures that it is initialized or assigned a value before being accessed.

#3 Resolution of Variables:

  • When encountering variable references (such as reading or assigning a variable), the resolver determines the scope in which the variable is defined.
  • If a variable is referenced, the resolver traverses the scope hierarchy to find the closest declaration of that variable. If the variable is not found, an error is raised indicating an undeclared variable.

#4 Function Scopes and Closure Resolution:

  • In languages like FarmScript that support closures, the resolver ensures that variables captured by closures are correctly resolved.
  • It tracks and manages the capture of variables within closure environments, ensuring that they maintain their references to the correct scope even after the enclosing function has exited.

#5 Error Reporting:

  • The resolver is responsible for detecting and reporting various scope-related errors, such as referencing undeclared variables, redeclaring variables in the same scope, or accessing variables before they are initialized.
  • It provides meaningful error messages to aid developers in identifying and fixing issues in their code.

#6 Demystify Resolver Code

	Resolver resolver(&interpreter);
    resolver.resolve(statements);

#6.1 Resolver Initialization:

Resolver resolver(&interpreter);

An instance of the Resolver class is created, presumably with a constructor that takes an Interpreter object as a parameter. This initialization likely establishes a connection between the resolver and the interpreter, allowing the resolver to access interpreter functionalities during the resolution process.

Resolver::Resolver(Interpreter *interpreter)
{
    this->interpreter = interpreter;
    this->currentFunction = FunctionType_None;
    this->currentClass = ClassType_None;
}

1 Initialization of Interpreter Pointer:

The constructor takes a pointer to an Interpreter object as a parameter and assigns it to the interpreter member variable of the Resolver instance. This establishes a connection between the resolver and the interpreter, allowing the resolver to interact with the interpreter during the resolution process.

Resolver::Resolver(Interpreter *interpreter) {
    this->interpreter = interpreter;

2 Initialization of Current Function and Class Types:

The constructor initializes the currentFunction and currentClass member variables to indicate the current function and class context during the resolution process. These variables are typically used to track the scope context while resolving identifiers and handling nested functions or classes.

    this->currentFunction = FunctionType_None;
    this->currentClass = ClassType_None;
  • currentFunction: This variable tracks the current type of function being resolved, such as whether it's a top-level function, method, or initializer.
  • currentClass: This variable tracks the current type of class being resolved, such as whether it's a top-level class or nested within another class.

#6.2 Statement Resolution:

resolver.resolve(statements);

The resolve() method of the resolver is called, passing in the vector of statements parsed from the FarmScript source code. This method is responsible for traversing the AST (Abstract Syntax Tree) represented by the parsed statements and resolving variable bindings, scoping rules, and any other relevant semantic analysis.

Here, statements is vector of Statements (Stmt*).

Let's delve into the resolving part:

void Resolver::resolve(std::vector<Stmt*> statements)
{
    // Iterate over each statement in the vector
    for (auto &stmt : statements)
    {
        // Resolve the current statement
        resolve(stmt);
    }
}

The resolve in the Resolver class takes a vector of statement pointers (std::vector<Stmt*> statements) as input and iterates over each statement in the vector, resolving them one by one. 

  • The method takes a vector of statement pointers (statements) as input.
  • It uses a range-based for loop to iterate over each statement pointer (stmt) in the vector.
  • For each statement, it calls the resolve method with the current statement pointer as an argument. This will resolve the statement, handling any resolution tasks associated with that statement type.
  • After iterating over all statements in the vector and resolving them, the method completes its execution.

#6.2.1 resolve()

The resolve method in the Resolver class handles the resolution of individual statements.

void Resolver::resolve(Stmt *stmt)
{
    if (stmt)
    {
        switch (stmt->type)
        {
            case StmtType_Print: visitPrint((Print *)stmt); break;
            case StmtType_Expression: visitExpression((Expression *)stmt); break;
            case StmtType_Var: visitVar((Var *)stmt); break;
            case StmtType_Block: visitBlock((Block *)stmt); break;
            case StmtType_If: visitIf((If *)stmt); break;
            case StmtType_While: visitWhile((While *)stmt); break;
            case StmtType_Break: visitBreak((Break *)stmt); break;
            case StmtType_Function: visitFunction((Function *)stmt); break;
            case StmtType_Class: visitClass((Class *)stmt); break;
            case StmtType_Return: visitReturn((Return *)stmt); break;
            default: FarmScript::error(0, "Invalid statement type.");
        }
    }
}
  • The method takes a pointer to a statement (Stmt *stmt) as input.
  • It checks if the statement pointer is not null before proceeding with resolution.
  • Inside the switch statement, each case corresponds to a different type of statement (StmtType). Depending on the type of statement, the method calls a specific visit method to handle the resolution tasks associated with that statement type.
  • For example, if the statement type is StmtType_Print, the visitPrint method is called with the statement pointer cast to the appropriate type (Print *). This method would handle the resolution tasks specific to print statements.
  • Similarly, for other statement types like variable declarations, block statements, if statements, function declarations, etc., corresponding visit methods are called to perform the resolution tasks associated with those statement types.
  • If the statement type does not match any of the known types (as indicated by the default case), an error is reported indicating an invalid statement type.