M craftinginterpreters/notes.md => craftinginterpreters/notes.md +128 -0
@@ 809,3 809,131 @@ TODO
### Extras
+
+## Chapter 12 -- Classes
+
+```bnf
+declaration → classDecl
+ | funDecl
+ | varDecl
+ | statement ;
+
+classDecl → "class" IDENTIFIER "{" function* "}" ;
+---
+function → IDENTIFIER "(" parameters? ")" block ;
+parameters → IDENTIFIER ( "," IDENTIFIER )* ;
+```
+
+
+Add the dot for Foo.bar
+```bnf
+call → primary ( "(" arguments? ")" | "." IDENTIFIER )* ;
+```
+
+Add setters
+```bnf
+assignment → ( call "." )? IDENTIFIER "=" assignment
+ | logic_or ;
+```
+### Notes
+
+This felt like a long chapter, and I kind of just went through the motions because
+
+1. I don't really care about classes
+2. Mixing creating classes with java classes got confusing.
+
+The whole "closure" aspect is probably correct, but it didn't _feel_ like a closure,
+mostly because we were directly closing over environments (the ones we create). But
+I think I got the gist of it.
+
+Essentially we want to return a new "instance" everytime we do something that changes
+the "instance" environment. It seems like we don't actually have to use closures for that,
+such as by rewriting lookups for fields and methods that belong to an instance.
+
+The reason we want to capture the environment is because a class has a class environment,
+and an instance of that object has an environment that is unique to that instance.
+
+i.e.,
+
+```python
+foo = Foo()
+bar = Foo()
+foo.property = "foo"
+bar.property = "bar"
+```
+
+are clearly independent of each other. _bar_ doesn't have a "foo" value as a property.
+
+
+### Terms
+
+* fields -- "named bits of state stored directly in an instance"
+* properties -- "named [...] things, that a get expression may return."
+
+"Every field is a property, but as we'll see later, not every property is a field."
+
+### Challenges
+
+
+
+1. We have methods on instances, but there is no way to define “static” methods that can be called directly on the class object itself. Add support for them. Use a class keyword preceding the method to indicate a static method that hangs off the class object.
+
+```
+ class Math {
+ class square(n) {
+ return n * n;
+ }
+ }
+
+ print Math.square(3); // Prints "9".
+```
+
+You can solve this however you like, but the “metaclasses” used by Smalltalk and Ruby are a particularly elegant approach. Hint: Make LoxClass extend LoxInstance and go from there.
+
+Answer: I suppose we can just treat a class as an instance in the environment so that we can call methods
+on it and just have instances have an "initialized" and an "uninitialized" property for when "init" happens.
+That way we don't have to bind "this" to another environment.
+
+TODO
+
+
+2. Most modern languages support “getters” and “setters”—members on a class that look like field reads and writes but that actually execute user-defined code. Extend Lox to support getter methods. These are declared without a parameter list. The body of the getter is executed when a property with that name is accessed.
+
+```
+ class Circle {
+ init(radius) {
+ this.radius = radius;
+ }
+
+ area {
+ return 3.141592653 * this.radius * this.radius;
+ }
+ }
+
+ var circle = Circle(4);
+ print circle.area; // Prints roughly "50.2655".
+```
+
+Answer: TODO. It seems like it's just an implicit function call on a field lookup.
+
+3. Python and JavaScript allow you to freely access an object’s fields from outside of its own methods. Ruby and Smalltalk encapsulate instance state. Only methods on the class can access the raw fields, and it is up to the class to decide which state is exposed. Most statically typed languages offer modifiers like private and public to control which parts of a class are externally accessible on a per-member basis.
+
+What are the trade-offs between these approaches and why might a language prefer one or the other?
+
+Answer: I despise the idea of encapsulation. The pro is that it creates an internal and external
+api so that there is an implicit contract for use. The con is that it doesn't trust the programmer
+to use the object. "Works when I add a print statement, don't remove" type stuff. More seriously
+it makes it so that all object functions are internal to the object, and you can't pass it around
+like you would a function.
+
+
+### Extras
+
+Multiple Dispatch (familiar from Clojure) but apparently there
+is also [predicate dispatch](https://en.wikipedia.org/wiki/Predicate_dispatch)
+Maybe want to implement that at some point
+
+CLOS is probably more interesting than this implementation. So look that up at some point.
+LISP (Lisp in Small Pieces) gets to classes fairly early and I plan to read that soon.
+
+
M craftinginterpreters/src/main/java/craftinginterpreters/Expr.java => craftinginterpreters/src/main/java/craftinginterpreters/Expr.java +45 -0
@@ 7,9 7,12 @@ abstract class Expr {
R visitAssignExpr(Assign expr);
R visitBinaryExpr(Binary expr);
R visitCallExpr(Call expr);
+ R visitGetExpr(Get expr);
R visitGroupingExpr(Grouping expr);
R visitLiteralExpr(Literal expr);
R visitLogicalExpr(Logical expr);
+ R visitSetExpr(Set expr);
+ R visitThisExpr(This expr);
R visitUnaryExpr(Unary expr);
R visitVariableExpr(Variable expr);
}
@@ 59,6 62,20 @@ abstract class Expr {
final Token paren;
final List<Expr> arguments;
}
+ static class Get extends Expr {
+ Get(Expr object, Token name) {
+ this.object = object;
+ this.name = name;
+ }
+
+ @Override
+ <R> R accept(Visitor<R> visitor) {
+ return visitor.visitGetExpr(this);
+ }
+
+ final Expr object;
+ final Token name;
+ }
static class Grouping extends Expr {
Grouping(Expr expression) {
this.expression = expression;
@@ 99,6 116,34 @@ abstract class Expr {
final Token operator;
final Expr right;
}
+ static class Set extends Expr {
+ Set(Expr object, Token name, Expr value) {
+ this.object = object;
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ <R> R accept(Visitor<R> visitor) {
+ return visitor.visitSetExpr(this);
+ }
+
+ final Expr object;
+ final Token name;
+ final Expr value;
+ }
+ static class This extends Expr {
+ This(Token keyword) {
+ this.keyword = keyword;
+ }
+
+ @Override
+ <R> R accept(Visitor<R> visitor) {
+ return visitor.visitThisExpr(this);
+ }
+
+ final Token keyword;
+ }
static class Unary extends Expr {
Unary(Token operator, Expr right) {
this.operator = operator;
M craftinginterpreters/src/main/java/craftinginterpreters/Interpreter.java => craftinginterpreters/src/main/java/craftinginterpreters/Interpreter.java +46 -1
@@ 77,6 77,25 @@ class Interpreter implements Expr.Visitor<Object>,
}
@Override
+ public Object visitSetExpr(Expr.Set expr) {
+ Object object = evaluate(expr.object);
+
+ if (!(object instanceof LoxInstance)) {
+ throw new RuntimeError(expr.name,
+ "Only instances have fields.");
+ }
+
+ Object value = evaluate(expr.value);
+ ((LoxInstance)object).set(expr.name, value);
+ return value;
+ }
+
+ @Override
+ public Object visitThisExpr(Expr.This expr) {
+ return lookUpVariable(expr.keyword, expr);
+ }
+
+ @Override
public Object visitGroupingExpr(Expr.Grouping expr) {
return evaluate(expr.expression);
}
@@ 101,6 120,21 @@ class Interpreter implements Expr.Visitor<Object>,
return null;
}
+ @Override
+ public Void visitClassStmt(Stmt.Class stmt) {
+ environment.define(stmt.name.lexeme, null);
+
+ Map<String, LoxFunction> methods = new HashMap<>();
+ for (Stmt.Function method : stmt.methods) {
+ LoxFunction function = new LoxFunction(method, environment,
+ method.name.lexeme.equals("init"));
+ methods.put(method.name.lexeme, function);
+ }
+ LoxClass klass = new LoxClass(stmt.name.lexeme, methods);
+ environment.assign(stmt.name, klass);
+ return null;
+ }
+
void executeBlock(List<Stmt> statements,
Environment environment) {
Environment previous = this.environment;
@@ 130,7 164,7 @@ class Interpreter implements Expr.Visitor<Object>,
@Override
public Void visitFunctionStmt(Stmt.Function stmt) {
- LoxFunction function = new LoxFunction(stmt, environment);
+ LoxFunction function = new LoxFunction(stmt, environment, false);
environment.define(stmt.name.lexeme, function);
return null;
}
@@ 324,6 358,17 @@ class Interpreter implements Expr.Visitor<Object>,
return function.call(this, arguments);
}
+ @Override
+ public Object visitGetExpr(Expr.Get expr) {
+ Object object = evaluate(expr.object);
+ if (object instanceof LoxInstance) {
+ return ((LoxInstance) object).get(expr.name);
+ }
+
+ throw new RuntimeError(expr.name,
+ "Only instanaces have properties.");
+ }
+
private boolean isEqual(Object a, Object b) {
if (a == null && b == null) return true;
if (a == null) return false;
A craftinginterpreters/src/main/java/craftinginterpreters/LoxCallable.java => craftinginterpreters/src/main/java/craftinginterpreters/LoxCallable.java +8 -0
@@ 0,0 1,8 @@
+package craftinginterpreters;
+
+import java.util.List;
+
+interface LoxCallable {
+ int arity();
+ Object call(Interpreter interpreter, List<Object> arguments);
+}
A craftinginterpreters/src/main/java/craftinginterpreters/LoxClass.java => craftinginterpreters/src/main/java/craftinginterpreters/LoxClass.java +53 -0
@@ 0,0 1,53 @@
+package craftinginterpreters;
+
+import java.util.List;
+import java.util.Map;
+
+class LoxClass implements LoxCallable{
+ final String name;
+ final LoxClass superclass;
+ private final Map<String, LoxFunction> methods;
+
+ LoxClass(String name, LoxClass superclass,
+ Map<String, LoxFunction> methods) {
+ this.superclass = superclass;
+ this.name = name;
+ this.methods = methods;
+ }
+
+ LoxFunction findMethod(String name) {
+ if (methods.containsKey(name)) {
+ return methods.get(name);
+ }
+
+ if (superclass != null) {
+ return superclass.findMethod(name);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public Object call(Interpreter interpreter,
+ List<Object> arguments) {
+ LoxInstance instance = new LoxInstance(this);
+ LoxFunction initializer = findMethod("init");
+ if (initializer != null) {
+ initializer.bind(instance).call(interpreter, arguments);
+ }
+
+ return instance;
+ }
+
+ @Override
+ public int arity() {
+ LoxFunction initializer = findMethod("init");
+ if (initializer == null) return 0;
+ return initializer.arity();
+ }
+}
A craftinginterpreters/src/main/java/craftinginterpreters/LoxFunction.java => craftinginterpreters/src/main/java/craftinginterpreters/LoxFunction.java +52 -0
@@ 0,0 1,52 @@
+package craftinginterpreters;
+
+import java.util.List;
+
+class LoxFunction implements LoxCallable {
+ private final Stmt.Function declaration;
+ private final Environment closure;
+ private final boolean isInitializer;
+
+ LoxFunction(Stmt.Function declaration, Environment closure,
+ boolean isInitializer) {
+ this.isInitializer = isInitializer;
+ this.closure = closure;
+ this.declaration = declaration;
+ }
+
+ LoxFunction bind(LoxInstance instance) {
+ Environment environment = new Environment(closure);
+ environment.define("this", instance);
+ return new LoxFunction(declaration, environment, isInitializer);
+ }
+
+ @Override
+ public int arity() {
+ return declaration.params.size();
+ }
+
+ @Override
+ public Object call(Interpreter interpreter,
+ List<Object> arguments) {
+ Environment environment = new Environment(closure);
+ for (int i = 0; i < declaration.params.size(); i++) {
+ environment.define(declaration.params.get(i).lexeme,
+ arguments.get(i));
+ }
+
+ try {
+ interpreter.executeBlock(declaration.body, environment);
+ } catch (Return returnValue) {
+ if (isInitializer) return closure.getAt(0, "this");
+ return returnValue.value;
+ }
+
+ if (isInitializer) return closure.getAt(0, "this");
+ return null;
+ }
+
+ @Override
+ String toString() {
+ return "<fn " + declaration.name.lexeme + ">";
+ }
+}
A craftinginterpreters/src/main/java/craftinginterpreters/LoxInstance.java => craftinginterpreters/src/main/java/craftinginterpreters/LoxInstance.java +34 -0
@@ 0,0 1,34 @@
+package craftinginterpreters;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class LoxInstance {
+ private LoxClass klass;
+ private final Map<String, Object> fields = new HashMap<>();
+
+ LoxInstance(LoxClass klass) {
+ this.klass = klass;
+ }
+
+ Object get(Token name) {
+ if (fields.containsKey(name.lexeme)) {
+ return fields.get(name.lexeme);
+ }
+
+ LoxFunction method = klass.findMethod(name.lexeme);
+ if (method != null) return method.bind(this);
+
+ throw new RuntimeError(name,
+ "undefined property '" + name.lexeme + "'.");
+ }
+
+ void set(Token name, Object value) {
+ fields.put(name.lexeme, value);
+ }
+
+ @Override
+ public String toString() {
+ return klass.name + " instance";
+ }
+}
M craftinginterpreters/src/main/java/craftinginterpreters/Parser.java => craftinginterpreters/src/main/java/craftinginterpreters/Parser.java +25 -0
@@ 109,6 109,7 @@ class Parser {
private Stmt declaration() {
try {
+ if (match(CLASS)) return classDeclaration();
if (match(FUN)) return function("function");
if (match(VAR))
return varDeclaration();
@@ 119,6 120,20 @@ class Parser {
}
}
+ private Stmt classDeclaration() {
+ Token name = consume(IDENTIFIER, "Expect class name.");
+ consume(LEFT_BRACE, "Expect '{' before class body.");
+
+ List<Stmt.Function> methods = new ArrayList<>();
+ while (!check(RIGHT_BRACE) && !isAtEnd()) {
+ methods.add(function("method"));
+ }
+
+ consume(RIGHT_BRACE, "Expect '}' after class body.");
+
+ return new Stmt.Class(name, methods);
+ }
+
private Stmt varDeclaration() {
Token name = consume(IDENTIFIER, "Expect variable name");
@@ 203,6 218,9 @@ class Parser {
if (expr instanceof Expr.Variable) {
Token name = ((Expr.Variable)expr).name;
return new Expr.Assign(name, value);
+ } else if (expr instanceof Expr.Get) {
+ Expr.Get get = (Expr.Get)expr;
+ return new Expr.Set(get.object, get.name, value);
}
error(equals, "Invalid assignment target.");
@@ 313,6 331,11 @@ class Parser {
while (true) {
if (match(LEFT_PAREN)) {
expr = finishCall(expr);
+ } else if (match(DOT)) {
+ Token name = consume(IDENTIFIER,
+ "Expect property name after '.'.");
+ expr = new Expr.Get(expr, name);
+
} else {
break;
}
@@ 328,6 351,8 @@ class Parser {
if (match(NUMBER, STRING)) {
return new Expr.Literal(previous().literal);
}
+
+ if (match(THIS)) return new Expr.This(previous());
if (match(IDENTIFIER)) {
return new Expr.Variable(previous());
}
M craftinginterpreters/src/main/java/craftinginterpreters/Resolver.java => craftinginterpreters/src/main/java/craftinginterpreters/Resolver.java +60 -1
@@ 85,6 85,29 @@ class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
}
@Override
+ public Void visitClassStmt(Stmt.Class stmt) {
+ ClassType enclosingClass = currentClass;
+ currentClass = ClassType.CLASS;
+ declare(stmt.name);
+ define(stmt.name);
+
+ beginScope();
+ scopes.peek().put("this", true);
+
+ for (Stmt.Function method : stmt.methods) {
+ FunctionType declaration = FunctionType.METHOD;
+ if (method.name.lexeme.equals("init")) {
+ declaration = FunctionType.INITIALIZER;
+ }
+ resolveFunction(method, declaration);
+ }
+
+ endScope();
+ currentClass = enclosingClass;
+ return null;
+ }
+
+ @Override
public Void visitExpressionStmt(Stmt.Expression stmt) {
resolve(stmt.expression);
return null;
@@ 112,8 135,13 @@ class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
}
if (stmt.value != null) {
+ if (currentFunction == FunctionType.INITIALIZER){
+ App.error(stmt.keyword,
+ "Can't return a value from an initializer.");
+ }
resolve(stmt.value);
}
+
return null;
}
@@ 143,6 171,12 @@ class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
}
@Override
+ public Void visitGetExpr(Expr.Get expr) {
+ resolve(expr.object);
+ return null;
+ }
+
+ @Override
public Void visitGroupingExpr(Expr.Grouping expr) {
resolve(expr.expression);
return null;
@@ 161,6 195,24 @@ class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
}
@Override
+ public Void visitSetExpr(Expr.Set expr) {
+ resolve(expr.value);
+ resolve(expr.object);
+ return null;
+ }
+
+ @Override
+ public Void visitThisExpr(Expr.This expr) {
+ if (currentClass == ClassType.NONE) {
+ App.error(expr.keyword,
+ "Can't use 'this' outside of a class.");
+ return null;
+ }
+ resolveLocal(expr, expr.keyword);
+ return null;
+ }
+
+ @Override
public Void visitUnaryExpr(Expr.Unary expr) {
resolve(expr.right);
return null;
@@ 205,7 257,14 @@ class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
}
private enum FunctionType {
- NONE, FUNCTION
+ NONE, FUNCTION, INITIALIZER, METHOD
+ }
+
+ private enum ClassType {
+ NONE,
+ CLASS
}
+ private ClassType currentClass = ClassType.NONE;
+
}
M craftinginterpreters/src/main/java/craftinginterpreters/Stmt.java => craftinginterpreters/src/main/java/craftinginterpreters/Stmt.java +15 -0
@@ 5,6 5,7 @@ import java.util.List;
abstract class Stmt {
interface Visitor<R> {
R visitBlockStmt(Block stmt);
+ R visitClassStmt(Class stmt);
R visitExpressionStmt(Expression stmt);
R visitFunctionStmt(Function stmt);
R visitIfStmt(If stmt);
@@ 25,6 26,20 @@ abstract class Stmt {
final List<Stmt> statements;
}
+ static class Class extends Stmt {
+ Class(Token name, List<Stmt.Function> methods) {
+ this.name = name;
+ this.methods = methods;
+ }
+
+ @Override
+ <R> R accept(Visitor<R> visitor) {
+ return visitor.visitClassStmt(this);
+ }
+
+ final Token name;
+ final List<Stmt.Function> methods;
+ }
static class Expression extends Stmt {
Expression(Expr expression) {
this.expression = expression;
M craftinginterpreters/src/main/java/craftinginterpreters/tool/GenerateAst.java => craftinginterpreters/src/main/java/craftinginterpreters/tool/GenerateAst.java +4 -0
@@ 15,13 15,17 @@ public class GenerateAst {
defineAst(outputDir, "Expr", Arrays.asList("Assign : Token name, Expr value",
"Binary : Expr left, Token operator, Expr right",
"Call : Expr callee, Token paren, List<Expr> arguments",
+ "Get : Expr object, Token name",
"Grouping : Expr expression", "Literal : Object value",
"Logical : Expr left, Token operator, Expr right",
+ "Set : Expr object, Token name, Expr value",
+ "This : Token keyword",
"Unary : Token operator, Expr right",
"Variable : Token name"));
defineAst(outputDir, "Stmt", Arrays.asList(
"Block : List<Stmt> statements",
+ "Class : Token name, List<Stmt.Function> methods",
"Expression : Expr expression",
"Function : Token name, List<Token> params," +
" List<Stmt> body",