Skip to content

Commit

Permalink
Chapter 12: Classes (#9)
Browse files Browse the repository at this point in the history
This implements [chapter 12](http://craftinginterpreters.com/classes.html) to support classes.

In this implementation classes do not just extend `LoxFunction`. That's why the call logic is duplicated. Mutable state such as the environments and class instances are passed around in an `Object` that holds an `Rc<RefCell<...>>` to the actual state. It is not easy to track but it works. When an `Object` is cloned the reference is cloned. This mirrors somewhat how the
Java implementation works but is not really "rusty".

There are a lot of clone calls but it should not cost too much overhead since we mostly clone 
references and vectors.
  • Loading branch information
jeschkies committed Dec 17, 2019
1 parent bd29522 commit 337896b
Show file tree
Hide file tree
Showing 11 changed files with 425 additions and 25 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Each commit corresponds to one chapter in the book:
* [Chapter 7: Evaluating Expressions](https://github.com/jeschkies/lox-rs/commit/fd90ef985c88832c9af6f193e0614e41dd13ef28)
* [Chapter 8: Statements and State](https://github.com/jeschkies/lox-rs/commit/941cbba900acb5876dbe6031b24ef31ff81ca99e)
* [Chapter 9: Control Flow](https://github.com/jeschkies/lox-rs/commit/d1f8d67f65fa4d66e24e654fec7bd8d1529b124d)
* [Chapter 10: Functions](https://github.com/jeschkies/lox-rs/commit/0e10d13944a6cd77d37f9cdf393ed81ba9573172)
* [Chapter 10: Functions](https://github.com/jeschkies/lox-rs/commit/0e10d13944a6cd77d37f9cdf393ed81ba9573172)
* [Chapter 11: Resolving and Binding](https://github.com/jeschkies/lox-rs/commit/bd2952230567df568d77855f730540462f350a45)
51 changes: 51 additions & 0 deletions examples/class.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
class DevonshireCream {
serveOn() {
return "Scones";
}
}

print DevonshireCream;

class Bagel {
init() {
this.topping = "cream";
}
}
var bagel = Bagel();
print bagel;
print "Topping: " + bagel.topping;

bagel.topping = "whipped cream";
print "Topping: " + bagel.topping;

class Bacon {
eat() {
print "Crunch crunch crunch!";
}
}

Bacon().eat();

class Cake {
taste() {
var adjective = "delicious";
print "The " + this.flavor + " cake is " + adjective + "!";
}
}

var cake = Cake();
cake.flavor = "German chocolate";
cake.taste();

class Thing {
getCallback() {
fun localFunction() {
print this;
}

return localFunction;
}
}

var callback = Thing().getCallback();
callback();
59 changes: 59 additions & 0 deletions src/class.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::error::Error;
use crate::function::Function;
use crate::object::Object;
use crate::token::Token;

use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

#[derive(Debug)]
pub struct LoxClass {
pub name: String,
pub methods: HashMap<String, Function>,
}

impl LoxClass {
pub fn find_method(&self, name: &str) -> Option<&Function> {
self.methods.get(name)
}
}

#[derive(Debug)]
pub struct LoxInstance {
pub class: Rc<RefCell<LoxClass>>,
fields: HashMap<String, Object>,
}

impl LoxInstance {
/// Returns a new `LoxInstance` wrapped in an `Object::Instance`.
pub fn new(class: &Rc<RefCell<LoxClass>>) -> Object {
let instance = LoxInstance {
class: Rc::clone(class),
fields: HashMap::new(),
};
Object::Instance(Rc::new(RefCell::new(instance)))
}

/// Returns a member field of this instance.
///
/// # Args
/// * name - The name of the member.
/// * instance - A reference to this instance as an object.
pub fn get(&self, name: &Token, instance: &Object) -> Result<Object, Error> {
if let Some(field) = self.fields.get(&name.lexeme) {
Ok(field.clone())
} else if let Some(method) = self.class.borrow().find_method(&name.lexeme) {
Ok(Object::Callable(method.bind(instance.clone())))
} else {
Err(Error::Runtime {
token: name.clone(),
message: format!("Undefined property '{}'.", name.lexeme),
})
}
}

pub fn set(&mut self, name: &Token, value: Object) {
self.fields.insert(name.lexeme.clone(), value);
}
}
4 changes: 3 additions & 1 deletion src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;

#[derive(Debug)]
pub struct Environment {
enclosing: Option<Rc<RefCell<Environment>>>, // Parent
values: HashMap<String, Object>,
Expand Down Expand Up @@ -41,7 +42,8 @@ impl Environment {

// Get next ancestors
for i in 1..distance {
let parent = self
let parent = environment
.borrow()
.enclosing
.clone()
.expect(&format!("No enclosing environment at {}", i));
Expand Down
52 changes: 49 additions & 3 deletions src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::error::Error;
use crate::interpreter::Interpreter;
use crate::object::Object;
use crate::syntax::Stmt;
use crate::token::Token;
use crate::token::{Token, TokenType};

use std::cell::RefCell;
use std::fmt;
Expand All @@ -23,6 +23,7 @@ pub enum Function {
params: Vec<Token>,
body: Vec<Stmt>,
closure: Rc<RefCell<Environment>>,
is_initializer: bool,
},
}

Expand All @@ -38,6 +39,7 @@ impl Function {
params,
body,
closure,
is_initializer,
..
} => {
let mut environment = Rc::new(RefCell::new(Environment::from(closure)));
Expand All @@ -47,9 +49,53 @@ impl Function {
.define(param.lexeme.clone(), argument.clone());
}
match interpreter.execute_block(body, environment) {
Err(Error::Return { value }) => Ok(value),
Err(Error::Return { value }) => {
if *is_initializer {
Ok(closure
.borrow()
.get_at(0, &Token::new(TokenType::This, "this", 0))
.expect("Initializer should return 'this'."))
} else {
Ok(value)
}
},
Err(other) => Err(other),
Ok(..) => Ok(Object::Null), // We don't have a return statement.
// We don't have a return statement.
Ok(..) => {
if *is_initializer {
Ok(closure
.borrow()
.get_at(0, &Token::new(TokenType::This, "this", 0))
.expect("Initializer should return 'this'."))
} else {
Ok(Object::Null)
}
}
}
}
}
}

pub fn bind(&self, instance: Object) -> Self {
match self {
Function::Native { body, .. } => unreachable!(),
Function::User {
name,
params,
body,
closure,
is_initializer,
} => {
let mut environment = Rc::new(RefCell::new(Environment::from(closure)));
environment
.borrow_mut()
.define("this".to_string(), instance);
Function::User {
name: name.clone(),
params: params.clone(),
body: body.clone(),
closure: environment,
is_initializer: *is_initializer,
}
}
}
Expand Down
125 changes: 110 additions & 15 deletions src/interpreter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::class::{LoxClass, LoxInstance};
use crate::env::Environment;
use crate::error::Error;
use crate::function::Function;
Expand Down Expand Up @@ -91,7 +92,11 @@ impl Interpreter {
fn stringify(&self, object: Object) -> String {
match object {
Object::Boolean(b) => b.to_string(),
Object::Class(class) => class.borrow().name.clone(),
Object::Callable(f) => f.to_string(),
Object::Instance(instance) => {
format!("{} instance", instance.borrow().class.borrow().name)
}
Object::Null => "nil".to_string(),
Object::Number(n) => n.to_string(),
Object::String(s) => s,
Expand Down Expand Up @@ -200,24 +205,58 @@ impl expr::Visitor<Object> for Interpreter {
.collect();
let args = argument_values?;

if let Object::Callable(function) = callee_value {
let args_size = args.len();
if args_size != function.arity() {
Err(Error::Runtime {
token: paren.clone(),
message: format!(
"Expected {} arguments but got {}.",
function.arity(),
args_size
),
})
} else {
function.call(self, &args)
match callee_value {
Object::Callable(function) => {
let args_size = args.len();
if args_size != function.arity() {
Err(Error::Runtime {
token: paren.clone(),
message: format!(
"Expected {} arguments but got {}.",
function.arity(),
args_size
),
})
} else {
function.call(self, &args)
}
}
} else {
Err(Error::Runtime {
Object::Class(ref class) => {
// This is the call method of a class.
let args_size = args.len();
let instance = LoxInstance::new(class);
if let Some(initializer) = class.borrow().find_method("init") {
if args_size != initializer.arity() {
return Err(Error::Runtime {
token: paren.clone(),
message: format!(
"Expected {} arguments but got {}.",
initializer.arity(),
args_size
),
});
} else {
initializer.bind(instance.clone()).call(self, &args)?;
}
}

Ok(instance)
}
_ => Err(Error::Runtime {
token: paren.clone(),
message: "Can only call functions and classes.".to_string(),
}),
}
}

fn visit_get_expr(&mut self, object: &Expr, name: &Token) -> Result<Object, Error> {
let object = self.evaluate(object)?;
if let Object::Instance(ref instance) = object {
instance.borrow().get(name, &object)
} else {
Err(Error::Runtime {
token: name.clone(),
message: "Only instances have properties.".to_string(),
})
}
}
Expand Down Expand Up @@ -255,6 +294,31 @@ impl expr::Visitor<Object> for Interpreter {
self.evaluate(right)
}

fn visit_set_expr(
&mut self,
object: &Expr,
property_name: &Token,
value: &Expr,
) -> Result<Object, Error> {
let mut object = self.evaluate(object)?;

if let Object::Instance(ref instance) = object {
let value = self.evaluate(value)?;
instance.borrow_mut().set(property_name, value);
let r = Object::Instance(Rc::clone(instance));
Ok(r)
} else {
Err(Error::Runtime {
token: property_name.clone(),
message: "Only instances have fields.".to_string(),
})
}
}

fn visit_this_expr(&mut self, keyword: &Token) -> Result<Object, Error> {
self.look_up_variable(keyword)
}

fn visit_unary_expr(&mut self, operator: &Token, right: &Expr) -> Result<Object, Error> {
let right = self.evaluate(right)?;

Expand Down Expand Up @@ -295,6 +359,36 @@ impl stmt::Visitor<()> for Interpreter {
Ok(())
}

fn visit_class_stmt(&mut self, class_name: &Token, methods: &Vec<Stmt>) -> Result<(), Error> {
self.environment
.borrow_mut()
.define(class_name.lexeme.clone(), Object::Null);

let mut class_methods: HashMap<String, Function> = HashMap::new();
for method in methods {
if let Stmt::Function { name, params, body } = method {
let function = Function::User {
name: name.clone(),
params: params.clone(),
body: body.clone(),
closure: Rc::clone(&self.environment),
is_initializer: name.lexeme == "init",
};
class_methods.insert(name.lexeme.clone(), function);
} else {
unreachable!()
}
}

let lox_class = LoxClass {
name: class_name.lexeme.clone(),
methods: class_methods,
};
let class = Object::Class(Rc::new(RefCell::new(lox_class)));
self.environment.borrow_mut().assign(class_name, class);
Ok(())
}

fn visit_expression_stmt(&mut self, expression: &Expr) -> Result<(), Error> {
self.evaluate(expression)?;
Ok(())
Expand All @@ -311,6 +405,7 @@ impl stmt::Visitor<()> for Interpreter {
params: params.clone(),
body: body.clone(),
closure: Rc::clone(&self.environment),
is_initializer: false,
};
self.environment
.borrow_mut()
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod class;
mod env;
mod error;
mod function;
Expand Down
Loading

0 comments on commit 337896b

Please sign in to comment.