Skip to content

Commit ee4aa56

Browse files
committed
Handle initialisers
1 parent 8d8fac0 commit ee4aa56

6 files changed

Lines changed: 107 additions & 42 deletions

File tree

src/compiler.cpp

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ Parser::Parser(std::unique_ptr<Scanner> scanner, ObjFunction* fnptr, GC& gc)
130130
: scanner(std::move(scanner)), current(SENTINEL_EOF),
131131
previous(SENTINEL_EOF), errmsg(std::nullopt), gc(gc),
132132
compiler(
133-
std::make_unique<Compiler>(fnptr, nullptr, FunctionType::TOPLEVEL)) {}
133+
std::make_unique<Compiler>(fnptr, nullptr, FunctionType::TOPLEVEL)),
134+
current_class(nullptr) {}
134135

135136
void Parser::advance() {
136137
previous = current;
@@ -151,9 +152,12 @@ void Parser::function(bool is_class_method) {
151152
size_t arity = 0;
152153
// TODO: This copies the string. Do we need to?
153154
auto new_fnptr = gc.alloc<ObjFunction>(fn_name, arity);
154-
auto new_compiler = std::make_unique<Compiler>(
155-
new_fnptr, std::move(compiler),
156-
(is_class_method ? FunctionType::CLASSMETHOD : FunctionType::FUNCTION));
155+
FunctionType fn_type = is_class_method
156+
? (fn_name == "init" ? FunctionType::CLASSINIT
157+
: FunctionType::CLASSMETHOD)
158+
: FunctionType::FUNCTION;
159+
auto new_compiler =
160+
std::make_unique<Compiler>(new_fnptr, std::move(compiler), fn_type);
157161
compiler = std::move(new_compiler);
158162
compiler->begin_scope();
159163
// Parse parameters (if there are any).
@@ -257,9 +261,10 @@ ObjFunction* Parser::finalise_function() {
257261
<< "\n";
258262
return nullptr;
259263
} else {
260-
// If there's no explicit return statement, we tack one on the end that
261-
// returns nil.
262-
emit_constant(std::monostate());
264+
// Just tack a return at the end of the function body. If there was already
265+
// one, it won't matter that we do this: it'll be unreachable. But if we
266+
// fall off the end of a function body, this will catch us.
267+
emit_auto_return_value();
263268
emit(lox::OpCode::RETURN);
264269
// Get the function object from the current compiler, and pop it off the
265270
// compiler stack.
@@ -413,7 +418,7 @@ void Parser::class_declaration() {
413418
emit(lox::OpCode::CLASS);
414419
emit(name_constant_index);
415420

416-
compiler->push_current_class();
421+
push_current_class();
417422

418423
// This call will emit code to read from the top of the stack and create
419424
// either a local or global variable with the class.
@@ -437,7 +442,7 @@ void Parser::class_declaration() {
437442
// any more.
438443
emit(lox::OpCode::POP);
439444

440-
compiler->pop_current_class();
445+
pop_current_class();
441446
}
442447

443448
void Parser::method() {
@@ -596,15 +601,34 @@ void Parser::statement() {
596601
}
597602
}
598603

604+
// This function is hit whenever we have a plain `return;` (inside
605+
// `return_statement`) or if we fall off the end of a function without an
606+
// explicit return (inside `finalise_function`). It pushes the implicit return
607+
// value onto the stack (but does not emit the RETURN instruction).
608+
void Parser::emit_auto_return_value() {
609+
if (compiler->get_function_type() == FunctionType::CLASSINIT) {
610+
// return the instance, which is happily always at local slot 0.
611+
emit(lox::OpCode::GET_LOCAL);
612+
emit(static_cast<uint8_t>(0));
613+
} else {
614+
// No return value; return nil
615+
emit_constant(std::monostate());
616+
}
617+
}
618+
599619
void Parser::return_statement() {
600620
if (compiler->get_function_type() == FunctionType::TOPLEVEL) {
601621
error("cannot return from top-level code", previous.line);
602622
}
603623
// Return value
604624
if (consume_if(TokenType::SEMICOLON)) {
605-
// No return value; return nil
606-
emit_constant(std::monostate());
625+
// No explicit return value.
626+
emit_auto_return_value();
607627
} else {
628+
// Explicit return value.
629+
if (compiler->get_function_type() == FunctionType::CLASSINIT) {
630+
error("cannot return a value from an initializer", previous.line);
631+
}
608632
expression();
609633
consume_or_error(TokenType::SEMICOLON, "expected ';' after return value");
610634
}
@@ -726,13 +750,17 @@ void Parser::number(bool _) {
726750
}
727751

728752
void Parser::this_(bool _) {
729-
// 1. We can't assign to this, hence the false.
730-
// 2. We just treat 'this' like a local variable that happens to be at index
731-
// zero.
732-
if (!compiler->is_in_class()) {
753+
// Note that we can't just check if the current function is CLASSMETHOD ||
754+
// CLASSINIT, because we might be e.g. inside a nested function inside a
755+
// class method. That's why we have a separate bit of info in the Parser that
756+
// tracks whether we're currently allowed to use `this` or not.
757+
if (!is_in_class()) {
733758
error("cannot use 'this' outside of a class", previous.line);
734759
return;
735760
}
761+
// We can't assign to this, hence the false. But apart from that, we just
762+
// treat 'this' like a local variable that happens to be at index 0 (which the
763+
// constructor of Compiler does for us).
736764
variable(false);
737765
}
738766

src/compiler.hpp

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ enum class FunctionType {
3131
TOPLEVEL,
3232
FUNCTION,
3333
CLASSMETHOD,
34+
CLASSINIT,
3435
};
3536

3637
struct Local {
@@ -45,8 +46,8 @@ class Compiler {
4546
Compiler(ObjFunction* fnptr, std::unique_ptr<Compiler> parent,
4647
FunctionType fn_type)
4748
: scope_depth(0), current_function(fnptr), fn_type(fn_type),
48-
parent(std::move(parent)), current_class(nullptr) {
49-
if (fn_type == FunctionType::CLASSMETHOD) {
49+
parent(std::move(parent)) {
50+
if (fn_type == FunctionType::CLASSMETHOD || fn_type == FunctionType::CLASSINIT) {
5051
// For a class method, we reserve slot 0 for the 'this' variable.
5152
declare_local("this");
5253
} else {
@@ -95,28 +96,12 @@ class Compiler {
9596
current_function->arity = new_arity;
9697
}
9798

98-
bool is_in_class() const { return current_class != nullptr; }
99-
void push_current_class() {
100-
auto new_current_class = std::make_unique<CurrentClass>();
101-
new_current_class->enclosing = std::move(current_class);
102-
current_class = std::move(new_current_class);
103-
}
104-
void pop_current_class() {
105-
if (current_class == nullptr) {
106-
throw std::runtime_error(
107-
"Compiler::pop_current_class: no current class to pop");
108-
} else {
109-
current_class = std::move(current_class->enclosing);
110-
}
111-
}
112-
11399
private:
114100
std::vector<Local> locals;
115101
size_t scope_depth;
116102
ObjFunction* current_function;
117103
FunctionType fn_type;
118104
std::unique_ptr<Compiler> parent;
119-
std::unique_ptr<CurrentClass> current_class;
120105
};
121106

122107
class Parser {
@@ -134,6 +119,22 @@ class Parser {
134119
GC& gc;
135120
std::unique_ptr<Compiler> compiler;
136121

122+
bool is_in_class() const { return current_class != nullptr; }
123+
void push_current_class() {
124+
auto new_current_class = std::make_unique<CurrentClass>();
125+
new_current_class->enclosing = std::move(current_class);
126+
current_class = std::move(new_current_class);
127+
}
128+
void pop_current_class() {
129+
if (current_class == nullptr) {
130+
throw std::runtime_error(
131+
"Parser::pop_current_class: no current class to pop");
132+
} else {
133+
current_class = std::move(current_class->enclosing);
134+
}
135+
}
136+
std::unique_ptr<CurrentClass> current_class;
137+
137138
// Interact with scanner
138139
void advance();
139140
bool consume_or_error(scanner::TokenType type,
@@ -180,6 +181,7 @@ class Parser {
180181
void define_variable(std::string_view var_name);
181182
void define_global_variable(std::string_view name);
182183
void named_variable(std::string_view lexeme, bool can_assign);
184+
void emit_auto_return_value();
183185

184186
// Interact with chunk
185187
void emit(uint8_t byte) { compiler->emit(byte, previous.line); }

src/gc.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class GC {
3535
obj->size = obj_size;
3636
#ifdef LOX_GC_DEBUG
3737
std::cerr << " allocated object " << obj->to_repr() << " of size "
38-
<< obj_size;
38+
<< obj_size << "\n";
3939
#endif
4040
return obj;
4141
}

src/vm.cpp

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ VM::VM(std::unique_ptr<scanner::Scanner> scanner, GC gc)
7171
stack.reserve(MAX_STACK_SIZE);
7272
auto top_level_fn = _gc.alloc<ObjFunction>("#toplevel#", size_t(0));
7373
parser = std::make_unique<Parser>(std::move(scanner), top_level_fn, _gc);
74+
initString = _gc.get_string_ptr("init");
7475

7576
// Aggressive GC: run it every time we allocate
7677
_gc.set_alloc_callback([this]() { this->maybe_gc(); });
@@ -586,13 +587,36 @@ InterpretResult VM::run() {
586587
}
587588
case ObjType::CLASS: {
588589
auto classptr = static_cast<ObjClass*>(objptr);
589-
// TODO: we are ignoring arguments right now.
590-
if (nargs > 0) {
591-
throw std::runtime_error(
592-
"class constructors don't take arguments (yet)");
593-
}
594590
ObjInstance* inst = _gc.alloc<ObjInstance>(classptr);
595-
stack_replace_top(inst);
591+
// Replace the class pointer on the stack with the instance pointer
592+
// (so that it doesn't get GC'd). Recall that at this point the class
593+
// will have been pushed to the stack, followed by any function
594+
// arguments.
595+
stack[stack.size() - 1 - nargs] = inst;
596+
// Check for an initialiser.
597+
auto init_method_itr = classptr->methods.find(initString);
598+
if (init_method_itr != classptr->methods.end()) {
599+
ObjClosure* init_closure = init_method_itr->second;
600+
call(init_closure, nargs);
601+
// Once we've finished calling the initialiser, the return value of
602+
// init() will be on top of the stack. However, we don't want that.
603+
// We want to replace it with the instance that we just created.
604+
// Unfortunately, we can't handle that at the VM level -- when we
605+
// do call(init_closure, nargs) we modify the call frame to
606+
// enter the initialiser, but we don't have any hook into when
607+
// its execution finishes. So we have to handle it in the compiler:
608+
// when it is compiling an initialiser, any return statements will
609+
// have to compile to something that returns the instance.
610+
} else {
611+
// If there's no init method, we are done in terms of the stack --
612+
// we just need to check that the user didn't try to pass any
613+
// arguments to the nonexistent initialiser.
614+
if (nargs > 0) {
615+
throw std::runtime_error(
616+
"class has no initialiser but was initialised with " +
617+
std::to_string(nargs) + " arguments");
618+
}
619+
}
596620
break;
597621
}
598622
case ObjType::BOUND_METHOD: {
@@ -651,8 +675,9 @@ InterpretResult VM::run() {
651675

652676
void VM::maybe_gc() {
653677
if (_gc.should_gc()) {
654-
655678
// Mark roots as grey.
679+
_gc.mark_as_grey(initString);
680+
656681
for (const auto& v : stack) {
657682
_gc.mark_as_grey(v);
658683
}

src/vm.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class VM {
6969
// sorted upvalues that haven't been closed yet. They sort in decreasing order
7070
// of the stack slot that they point to
7171
std::vector<ObjUpvalue*> open_upvalues;
72+
// Interned string for "init", which we use when we need to look up the
73+
// initialiser method of a class.
74+
ObjString* initString;
7275

7376
CallFrame& current_frame() { return call_frames.back(); }
7477
Chunk& get_chunk() { return current_frame().closure->function->chunk; }

test.lox

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
print this;
1+
class Num {
2+
init(x) {
3+
this.x = x;
4+
}
5+
}
6+
7+
var n = Num(3);
8+
print n.x;

0 commit comments

Comments
 (0)