M craftinginterpreters/notes.md => craftinginterpreters/notes.md +95 -0
@@ 711,6 711,101 @@ Partially because doing `functions` was one of my goals,
but also because I've been tired. Only 2 more chapters to
go so lets finish `jlox` and then move to `clox`
+### Notes
+
+This whole resolver class pretty much just runs it's own visitor
+pattern stuff, overriding both Expr and Stmt.
+
+Pretty much what it does is it walks the tree (over everything in the tree without eval)
+and whenever it encounters a variable assignment it creates a hashmap to
+declare it, setting it to false, and then definining it, setting it to true.
+
+Instead of saving the _value_ of variables it saves a pointer to
+which scope to use for the variable. Then saves that in the
+interpreter as a "side-table" lookup.
+
+This way when it encounters a function declaration it eventually
+"resolves" and saves where in the environment to look up the variable.
+
+This works for the example provided:
+```
+var a = "global";
+{
+ fun showA() {
+ print a;
+ }
+
+ showA();
+ var a = "block";
+ showA();
+}
+```
+
+but only with respect to the enclosing block.
+The variable itself is still dynamic in scope.
+So it doesn't actually save the variable
+_at the time of function declaration_ it saves
+_which variable_ is pointed to.
+
+It kind of seems like instead of resolving _where_
+a variable is in scope, maybe we could just
+rewrite literals that point to that variable.
+Maybe that's part of `clox` I don't know.
+
+Otherwise that's a lot of code for what seems like
+we could have done otherwise. But it does show
+the usefulness of checking if in a function or not
+and other aspects of semantic analysis.
+
### Terms
+
+* semantic analysis
+* static analysis
+* lookup resolutions (e.g., how scheme handles environments)
+* persistent data structures for environments
+* variable resolution pass
+ * no side effects
+ * no control flow (both branches are visited)
+
+```
+* A block statement introduces a new scope for the statements it contains.
+* A function declaration introduces a new scope for its body and binds its parameters in that scope.
+* A variable declaration adds a new variable to the current scope.
+* Variable and assignment expressions need to have their variables resolved.
+```
+
### Challenges
+
+Challenges
+
+1. Why is it safe to eagerly define the variable bound to a function’s name when other variables must wait until after they are initialized before they can be used?
+
+As the chapter said, recursion. Probably also mutual recursion, and not having to declare before use.
+
+2. How do other languages you know handle local variables that refer to the same name in their initializer, like:
+```
+
+ var a = "outer";
+ {
+ var a = a;
+ }
+```
+
+Is it a runtime error? Compile error? Allowed? Do they treat global variables differently? Do you agree with their choices? Justify your answer.
+
+Lexical Scope shadows. It's probably not the most "correct" way to do it, but it's
+really useful anyway and I would miss programming without it.
+
+3. Extend the resolver to report an error if a local variable is never used.
+
+TODO: We can probably just add a table of used variables, and when the scope dies,
+if a variable is unused, throw an exception. We can mark its use in `resolveLocal`
+
+4. Our resolver calculates which environment the variable is found in, but it’s still looked up by name in that map. A more efficient environment representation would store local variables in an array and look them up by index.
+
+Extend the resolver to associate a unique index for each local variable declared in a scope. When resolving a variable access, look up both the scope the variable is in and its index and store that. In the interpreter, use that to quickly access a variable by its index instead of using a map.
+
+TODO
+
+
### Extras
M craftinginterpreters/src/main/java/craftinginterpreters/App.java => craftinginterpreters/src/main/java/craftinginterpreters/App.java +4 -0
@@ 63,6 63,10 @@ public class App
// Expr expression = parser.parse();
+
+ Resolver resolver = new Resolver(interpreter);
+ resolver.resolve(statements);
+
if (hadError) return;
interpreter.interpret(statements, prompt);
M craftinginterpreters/src/main/java/craftinginterpreters/Environment.java => craftinginterpreters/src/main/java/craftinginterpreters/Environment.java +16 -0
@@ 36,6 36,22 @@ class Environment {
values.put(name, value);
}
+ Environment ancestor(int distance) {
+ Environment environment = this;
+ for (int i = 0; i < distance; i++) {
+ environment = environment.enclosing;
+ }
+ return environment;
+ }
+
+ Object getAt(int distance, String name) {
+ return ancestor(distance).values.get(name);
+ }
+
+ void assignAt(int distance, Token name, Object value) {
+ ancestor(distance).values.put(name.lexeme, value);
+ }
+
Environment() {
enclosing = null;
}
M craftinginterpreters/src/main/java/craftinginterpreters/Interpreter.java => craftinginterpreters/src/main/java/craftinginterpreters/Interpreter.java +33 -1
@@ 1,13 1,16 @@
package craftinginterpreters;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
class Interpreter implements Expr.Visitor<Object>,
Stmt.Visitor<Void> {
final Environment globals = new Environment();
private Environment environment = globals;
+ private final Map<Expr, Integer> locals = new HashMap<>();
private Token PROMPT = new Token(TokenType.DEBUG, "PROMPT", null, 0);
private Token PROMPT_LAST = new Token(TokenType.DEBUG, "PROMPT_LAST", null, 0);
@@ 27,6 30,7 @@ class Interpreter implements Expr.Visitor<Object>,
});
}
+
void interpret(List<Stmt> statements,
Boolean prompt) {
@@ 87,6 91,10 @@ class Interpreter implements Expr.Visitor<Object>,
stmt.accept(this);
}
+ void resolve(Expr expr, int depth) {
+ locals.put(expr, depth);
+ }
+
@Override
public Void visitBlockStmt(Stmt.Block stmt){
executeBlock(stmt.statements, new Environment(environment));
@@ 178,17 186,41 @@ class Interpreter implements Expr.Visitor<Object>,
@Override
public Object visitAssignExpr(Expr.Assign expr) {
Object value = evaluate(expr.value);
- environment.assign(expr.name, value);
+
+ Integer distance = locals.get(expr);
+ if (distance != null) {
+ environment.assignAt(distance, expr.name, value);
+ } else {
+ globals.assign(expr.name, value);
+ }
+
+ // replaced in resolving and binding
+ //environment.assign(expr.name, value);
+
return value;
}
@Override
public Object visitVariableExpr(Expr.Variable expr) {
+ /*
+ // This existed but resolving and binding replaced the return
+ // we can fix this later
Object res = environment.get(expr.name);
if (res == null) {
Token err = new Token(TokenType.DEBUG, stringify(expr.name), null, 0);
throw new RuntimeError(err, "Variable: " + expr.name.lexeme + " was not assigned");}
return res;
+ */
+ return lookUpVariable(expr.name, expr);
+ }
+
+ private Object lookUpVariable(Token name, Expr expr) {
+ Integer distance = locals.get(expr);
+ if (distance != null) {
+ return environment.getAt(distance, name.lexeme);
+ } else {
+ return globals.get(name);
+ }
}
@Override
A craftinginterpreters/src/main/java/craftinginterpreters/Resolver.java => craftinginterpreters/src/main/java/craftinginterpreters/Resolver.java +211 -0
@@ 0,0 1,211 @@
+package craftinginterpreters;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
+ private final Interpreter interpreter;
+ private final Stack<Map<String, Boolean>> scopes = new Stack<>();
+ private FunctionType currentFunction = FunctionType.NONE;
+
+ Resolver(Interpreter interpreter) {
+ this.interpreter = interpreter;
+ }
+
+ void resolve(List<Stmt> statements) {
+ for (Stmt statement : statements) {
+ resolve(statement);
+ }
+ }
+
+ private void resolve(Stmt stmt) {
+ stmt.accept(this);
+ }
+
+ private void resolve(Expr expr){
+ expr.accept(this);
+ }
+
+ private void resolveFunction(Stmt.Function function,
+ FunctionType type) {
+ FunctionType enclosingFunction = currentFunction;
+ currentFunction = type;
+ beginScope();
+ for (Token param : function.params) {
+ declare(param);
+ define(param);
+ }
+
+ resolve(function.body);
+ endScope();
+ currentFunction = enclosingFunction;
+ }
+
+ private void beginScope() {
+ scopes.push(new HashMap<String, Boolean>());
+ }
+
+ private void endScope() {
+ scopes.pop();
+ }
+
+ private void declare(Token name) {
+ if (scopes.isEmpty()) return;
+
+ Map<String, Boolean> scope = scopes.peek();
+ if (scope.containsKey(name.lexeme)) {
+ App.error(name,
+ "Already a variable with this name in this scope.");
+ }
+ scope.put(name.lexeme, false);
+ }
+
+ private void define(Token name) {
+ if (scopes.isEmpty()) return;
+ scopes.peek().put(name.lexeme, true);
+ }
+
+ private void resolveLocal(Expr expr, Token name){
+ for (int i = scopes.size() - 1; i >= 0; i--) {
+ if (scopes.get(i).containsKey(name.lexeme)){
+ interpreter.resolve(expr, scopes.size() - 1 - i);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public Void visitBlockStmt(Stmt.Block stmt) {
+ beginScope();
+ resolve(stmt.statements);
+ endScope();
+ return null;
+ }
+
+ @Override
+ public Void visitExpressionStmt(Stmt.Expression stmt) {
+ resolve(stmt.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitIfStmt(Stmt.If stmt) {
+ resolve(stmt.condition);
+ resolve(stmt.thenBranch);
+ if (stmt.elseBranch != null) resolve(stmt.elseBranch);
+ return null;
+ }
+
+ @Override
+ public Void visitPrintStmt(Stmt.Print stmt) {
+ resolve(stmt.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitReturnStmt(Stmt.Return stmt) {
+
+ if (currentFunction == FunctionType.NONE) {
+ App.error(stmt.keyword, "Can't return from top-level code.");
+ }
+
+ if (stmt.value != null) {
+ resolve(stmt.value);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitWhileStmt(Stmt.While stmt) {
+ resolve(stmt.condition);
+ resolve(stmt.body);
+ return null;
+ }
+
+ @Override
+ public Void visitBinaryExpr(Expr.Binary expr) {
+ resolve(expr.left);
+ resolve(expr.right);
+ return null;
+ }
+
+ @Override
+ public Void visitCallExpr(Expr.Call expr) {
+ resolve(expr.callee);
+
+ for (Expr argument : expr.arguments) {
+ resolve(argument);
+ }
+
+ return null;
+ }
+
+ @Override
+ public Void visitGroupingExpr(Expr.Grouping expr) {
+ resolve(expr.expression);
+ return null;
+ }
+
+ @Override
+ public Void visitLiteralExpr(Expr.Literal expr) {
+ return null;
+ }
+
+ @Override
+ public Void visitLogicalExpr(Expr.Logical expr) {
+ resolve(expr.left);
+ resolve(expr.right);
+ return null;
+ }
+
+ @Override
+ public Void visitUnaryExpr(Expr.Unary expr) {
+ resolve(expr.right);
+ return null;
+ }
+
+ @Override
+ public Void visitFunctionStmt(Stmt.Function stmt) {
+ declare(stmt.name);
+ define(stmt.name);
+
+ resolveFunction(stmt, FunctionType.FUNCTION);
+ return null;
+ }
+
+ @Override
+ public Void visitVarStmt(Stmt.Var stmt) {
+ declare(stmt.name);
+ if (stmt.initializer != null) {
+ resolve(stmt.initializer);
+ }
+ define(stmt.name);
+ return null;
+ }
+
+ @Override
+ public Void visitAssignExpr(Expr.Assign expr) {
+ resolve(expr.value);
+ resolveLocal(expr, expr.name);
+ return null;
+ }
+
+ @Override
+ public Void visitVariableExpr(Expr.Variable expr) {
+ if (!scopes.isEmpty() &&
+ scopes.peek().get(expr.name.lexeme) == Boolean.FALSE) {
+ App.error(expr.name,
+ "Can't read local variable in its own initializer.");
+ }
+
+ resolveLocal(expr, expr.name);
+ return null;
+ }
+
+ private enum FunctionType {
+ NONE, FUNCTION
+ }
+
+}