# frozen_string_literal: true # Load all the classes implementing AST nodes require_relative '../ast/all_lox_nodes' require_relative 'binary_operator' require_relative 'lox_function' require_relative 'symbol_table' require_relative 'unary_operator' module Loxxy module BackEnd # A class aimed to perform variable resolution when it visits the parse tree. # Resolving means retrieve the declaration of a variable/function everywhere it # is referenced. class Resolver # A stack of Hashes of the form String => Boolean # @return [Array Boolean}>] attr_reader :scopes # A map from a LoxNode involving a variable and the number of enclosing scopes # where it is declared. # @return [Hash {LoxNode => Integer}] attr_reader :locals def initialize @scopes = [] @locals = {} end # Given an abstract syntax parse tree visitor, launch the visit # and execute the visit events in the output stream. # @param aVisitor [AST::ASTVisitor] # @return [Loxxy::Datatype::BuiltinDatatype] def analyze(aVisitor) begin_scope aVisitor.subscribe(self) aVisitor.start aVisitor.unsubscribe(self) end_scope end # block statement introduces a new scope def before_block_stmt(_aBlockStmt) begin_scope end def after_block_stmt(_aBlockStmt) end_scope end def before_for_stmt(aForStmt) before_block_stmt(aForStmt) end def after_for_stmt(aForStmt, aVisitor) aForStmt.test_expr.accept(aVisitor) aForStmt.body_stmt.accept(aVisitor) aForStmt.update_expr&.accept(aVisitor) after_block_stmt(aForStmt) end def after_if_stmt(anIfStmt, aVisitor) anIfStmt.then_stmt.accept(aVisitor) anIfStmt.else_stmt&.accept(aVisitor) end def after_while_stmt(aWhileStmt, aVisitor) aWhileStmt.body.accept(aVisitor) aWhileStmt.condition.accept(aVisitor) end # A variable declaration adds a new variable to current scope def before_var_stmt(aVarStmt) declare(aVarStmt.name) end def after_var_stmt(aVarStmt) define(aVarStmt.name) end # Assignment expressions require their variables resolved def after_assign_expr(anAssignExpr, aVisitor) resolve_local(anAssignExpr, aVisitor) end # Variable expressions require their variables resolved def before_variable_expr(aVarExpr) var_name = aVarExpr.name if !scopes.empty? && (scopes.last[var_name] == false) raise StandardError, "Can't read variable #{var_name} in its own initializer" end end def after_variable_expr(aVarExpr, aVisitor) resolve_local(aVarExpr, aVisitor) end def after_call_expr(aCallExpr, aVisitor) # Evaluate callee part aCallExpr.callee.accept(aVisitor) aCallExpr.arguments.reverse_each { |arg| arg.accept(aVisitor) } end # function declaration creates a new scope for its body & binds its parameters for that scope def before_fun_stmt(aFunStmt, aVisitor) declare(aFunStmt.name) define(aFunStmt.name) resolve_function(aFunStmt, aVisitor) end private def begin_scope scopes.push({}) end def end_scope scopes.pop end def declare(aVarName) return if scopes.empty? curr_scope = scopes.last # The initializer is not yet processed. # Mark the variable as 'not yet ready' = exists but may not be referenced yet curr_scope[aVarName] = false end def define(aVarName) return if scopes.empty? curr_scope = scopes.last # The initializer (if any) was processed. # Mark the variable as alive (= can be referenced in an expression) curr_scope[aVarName] = true end def resolve_local(aVarExpr, _visitor) max_i = i = scopes.size - 1 scopes.reverse_each do |scp| if scp.include?(aVarExpr.name) # Keep track of the difference of nesting levels between current scope # and the scope where the variable is declared @locals[aVarExpr] = max_i - i break end i -= 1 end end def resolve_function(aFunStmt, aVisitor) begin_scope aFunStmt.params&.each do |param_name| declare(param_name) define(param_name) end body = aFunStmt.body unless body.nil? || body.kind_of?(Ast::LoxNoopExpr) body.subnodes.first&.accept(aVisitor) end end_scope end end # class end # mmodule end # module