content: package lox;
  
  import java.util.ArrayList;
  import java.util.List;
  
  class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
  
      final Environment globals = new Environment();
      private Environment environment = globals;
  
      private static class BreakException extends RuntimeException {
      }
  
      private static class ContinueException extends RuntimeException {
      }
  
      Interpreter() {
          globals.define("clock", new LoxCallable() {
              @Override
              public int arity() {
                  return 0;
              }
  
              @Override
              public Object call(Interpreter interpreter,
                      List<Object> arguments) {
                  return (double) System.currentTimeMillis() / 1000.0;
              }
  
              @Override
              public String toString() {
                  return "<native fn>";
              }
          });
      }
  
      void interpret(List<Stmt> statements) {
          try {
              for (Stmt statement : statements) {
                  execute(statement);
              }
          } catch (RuntimeError error) {
              Lox.runtimeError(error);
          }
      }
  
      @Override
      public Object visitGroupingExpr(Expr.Grouping expr) {
          return evaluate(expr.expression);
      }
  
      public Object visitUnaryExpr(Expr.Unary expr) {
          Object right = evaluate(expr.right);
          if (right instanceof Nox)
              return Nox.INSTANCE;
          switch (expr.operator.type) {
              case BANG:
                  // Optionally: if (right instanceof Nox) return Nox.INSTANCE;
                  return !isTruthy(right);
              case MINUS:
                  checkNumberOperand(expr.operator, right);
                  return -(double) right;
          }
          return null;
      }
  
      @Override
      public Object visitVariableExpr(Expr.Variable expr) {
          return environment.get(expr.name);
      }
  
      private void checkNumberOperand(Token operator, Object operand) {
          if (operand instanceof Double)
              return;
          throw new RuntimeError(operator, "Operand must be a number.");
      }
  
      @Override
      public Object visitLiteralExpr(Expr.Literal expr) {
          return expr.value;
      }
  
      @Override
      public Object visitLogicalExpr(Expr.Logical expr) {
          Object left = evaluate(expr.left);
          if (left instanceof Nox)
              return Nox.INSTANCE;
          if (expr.operator.type == TokenType.OR) {
              if (isTruthy(left))
                  return left;
          } else {
              if (!isTruthy(left))
                  return left;
          }
          Object right = evaluate(expr.right);
          return right;
      }
  
      private void checkNumberOperands(Token operator,
              Object left, Object right) {
          if (left instanceof Double && right instanceof Double)
              return;
  
          throw new RuntimeError(operator, "Operands must be numbers.");
      }
  
      private boolean isTruthy(Object object) {
          if (object instanceof Nox)
              return false;
          if (object == null)
              return false;
          if (object instanceof Boolean)
              return (boolean) object;
          return true;
      }
  
      private boolean isEqual(Object a, Object b) {
          if (a == null && b == null)
              return true;
          if (a == null)
              return false;
          return a.equals(b);
      }
  
      private String stringify(Object object) {
          if (object instanceof Nox)
              return "nox";
          if (object == null)
              return "nil";
          if (object instanceof Double) {
              String text = object.toString();
              if (text.endsWith(".0")) {
                  text = text.substring(0, text.length() - 2);
              }
              return text;
          }
          return object.toString();
      }
  
      private Object evaluate(Expr expr) {
          return expr.accept(this);
      }
  
      private void execute(Stmt stmt) {
          stmt.accept(this);
      }
  
      void executeBlock(List<Stmt> statements, Environment environment) {
          Environment previous = this.environment;
          try {
              this.environment = environment;
              for (Stmt statement : statements) {
                  execute(statement);
              }
          } finally {
              this.environment = previous;
          }
      }
  
      @Override
      public Object visitBinaryExpr(Expr.Binary expr) {
          Object left = evaluate(expr.left);
          Object right = evaluate(expr.right);
          // Propagate nox if present.
          if (left instanceof Nox || right instanceof Nox) {
              return Nox.INSTANCE;
          }
          switch (expr.operator.type) {
              case BANG_EQUAL:
                  return !isEqual(left, right);
              case EQUAL_EQUAL:
                  return isEqual(left, right);
              case GREATER:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left > (double) right;
              case GREATER_EQUAL:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left >= (double) right;
              case LESS:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left < (double) right;
              case LESS_EQUAL:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left <= (double) right;
              case MINUS:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left - (double) right;
              case PLUS:
                  if (left instanceof Double && right instanceof Double) {
                      return (double) left + (double) right;
                  }
                  if (left instanceof String && right instanceof String) {
                      return (String) left + (String) right;
                  }
                  break;
              case SLASH:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left / (double) right;
              case STAR:
                  checkNumberOperands(expr.operator, left, right);
                  return (double) left * (double) right;
          }
          // Unreachable.
          return null;
      }
  
      @Override
      public Object visitCallExpr(Expr.Call expr) {
          Object callee = evaluate(expr.callee);
          List<Object> arguments = new ArrayList<>();
          for (Expr argument : expr.arguments) {
              arguments.add(evaluate(argument));
          }
          if (!(callee instanceof LoxCallable)) {
              throw new RuntimeError(expr.paren, "Can only call functions and classes.");
          }
          LoxCallable function = (LoxCallable) callee;
          if (arguments.size() != function.arity()) {
              throw new RuntimeError(expr.paren, "Expected " + function.arity() +
                      " arguments but got " + arguments.size() + ".");
          }
          return function.call(this, arguments);
      }
  
      @Override
      public Void visitReturnStmt(Stmt.Return stmt) {
          Object value = null;
          if (stmt.value != null)
              value = evaluate(stmt.value);
          throw new Return(value);
      }
  
      @Override
      public Void visitBlockStmt(Stmt.Block stmt) {
          executeBlock(stmt.statements, new Environment(environment));
          return null;
      }
  
      @Override
      public Void visitExpressionStmt(Stmt.Expression stmt) {
          evaluate(stmt.expression);
          return null;
      }
  
      @Override
      public Void visitFunctionStmt(Stmt.Function stmt) {
          LoxFunction function = new LoxFunction(stmt, environment);
          environment.define(stmt.name.lexeme, function);
          return null;
      }
  
      @Override
      public Void visitIfStmt(Stmt.If stmt) {
          Object conditionValue = evaluate(stmt.condition);
          if (conditionValue == Nox.INSTANCE) {
              if (stmt.elseBranch != null) {
                  // Execute both branches.
                  execute(stmt.thenBranch);
                  execute(stmt.elseBranch);
              }
              // No else: do nothing.
          } else if (isTruthy(conditionValue)) {
              execute(stmt.thenBranch);
          } else if (stmt.elseBranch != null) {
              execute(stmt.elseBranch);
          }
          return null;
      }
  
      @Override
      public Void visitPrintStmt(Stmt.Print stmt) {
          Object value = evaluate(stmt.expression);
          System.out.println(stringify(value));
          return null;
      }
  
      @Override
      public Void visitVarStmt(Stmt.Var stmt) {
          Object value = null;
          if (stmt.initializer != null) {
              value = evaluate(stmt.initializer);
          }
          environment.define(stmt.name.lexeme, value);
          return null;
      }
  
      @Override
      public Void visitBreakStmt(Stmt.Break stmt) {
          throw new BreakException();
      }
  
      @Override
      public Void visitContinueStmt(Stmt.Continue stmt) {
          throw new ContinueException();
      }
  
      // TODO: Tail Call Optimization (TCO)
      @Override
      public Void visitWhileStmt(Stmt.While stmt) {
          try {
              while (isTruthy(evaluate(stmt.condition))) {
                  try {
                      execute(stmt.body);
                  } catch (ContinueException e) {
                      // Just continue the loop
                  } catch (BreakException e) {
                      break;
                  }
              }
          } catch (BreakException e) {
              // Exit the loop
          }
          return null;
      }
  
      @Override
      public Object visitAssignExpr(Expr.Assign expr) {
          Object value = evaluate(expr.value);
          environment.assign(expr.name, value);
          return value;
      }
  }