mirror of
https://github.com/wren-lang/wren.git
synced 2026-01-11 22:28:45 +01:00
Allow code inside class bodies.
Every statement that isn't a method definition gets compiled into a special "<body>" method in the class. Every constructor in the class implicitly calls that before executing. Since it's an instance method, it executes in the context of the class: it can write fields, access "this", etc.
This commit is contained in:
2
Makefile
2
Makefile
@ -48,7 +48,7 @@ test: debug
|
||||
|
||||
benchmark: release
|
||||
@ $(MAKE) -f util/wren.mk test
|
||||
@ ./util/benchmark.py $(suite)
|
||||
@ ./util/benchmark.py -l wren $(suite)
|
||||
|
||||
# Generate the Wren site.
|
||||
docs:
|
||||
|
||||
@ -277,21 +277,7 @@ typedef struct
|
||||
int arity;
|
||||
} Signature;
|
||||
|
||||
// Bookkeeping information for compiling a class definition.
|
||||
typedef struct
|
||||
{
|
||||
// Symbol table for the fields of the class.
|
||||
SymbolTable fields;
|
||||
|
||||
// True if the class being compiled is a foreign class.
|
||||
bool isForeign;
|
||||
|
||||
// True if the current method being compiled is static.
|
||||
bool inStatic;
|
||||
|
||||
// The signature of the method being compiled.
|
||||
Signature* signature;
|
||||
} ClassCompiler;
|
||||
typedef struct sClassCompiler ClassCompiler;
|
||||
|
||||
struct sCompiler
|
||||
{
|
||||
@ -353,6 +339,28 @@ struct sCompiler
|
||||
IntBuffer debugSourceLines;
|
||||
};
|
||||
|
||||
// Bookkeeping information for compiling a class definition.
|
||||
struct sClassCompiler
|
||||
{
|
||||
// Symbol table for the fields of the class.
|
||||
SymbolTable fields;
|
||||
|
||||
// True if the class being compiled is a foreign class.
|
||||
bool isForeign;
|
||||
|
||||
// True if the current method being compiled is static.
|
||||
bool inStatic;
|
||||
|
||||
// The signature of the method being compiled.
|
||||
Signature* signature;
|
||||
|
||||
// The compiler used for body code.
|
||||
//
|
||||
// Any code inside a class body that is not a method definition will get
|
||||
// compiled into this function which is then called by each constructor.
|
||||
Compiler body;
|
||||
};
|
||||
|
||||
// The stack effect of each opcode. The index in the array is the opcode, and
|
||||
// the value is the stack effect of that instruction.
|
||||
static const int stackEffects[] = {
|
||||
@ -1436,6 +1444,22 @@ static void loadLocal(Compiler* compiler, int slot)
|
||||
emitByteArg(compiler, CODE_LOAD_LOCAL, slot);
|
||||
}
|
||||
|
||||
// Loads the receiver of the currently enclosing method. Correctly handles
|
||||
// functions defined inside methods.
|
||||
static void loadThis(Compiler* compiler)
|
||||
{
|
||||
Code loadInstruction;
|
||||
int index = resolveNonmodule(compiler, "this", 4, &loadInstruction);
|
||||
if (loadInstruction == CODE_LOAD_LOCAL)
|
||||
{
|
||||
loadLocal(compiler, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
emitByteArg(compiler, loadInstruction, index);
|
||||
}
|
||||
}
|
||||
|
||||
// Discards memory owned by [compiler].
|
||||
static void freeCompiler(Compiler* compiler)
|
||||
{
|
||||
@ -1616,31 +1640,6 @@ static bool finishBlock(Compiler* compiler)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parses a method or function body, after the initial "{" has been consumed.
|
||||
//
|
||||
// It [isInitializer] is `true`, this is the body of a constructor initializer.
|
||||
// In that case, this adds the code to ensure it returns `this`.
|
||||
static void finishBody(Compiler* compiler, bool isInitializer)
|
||||
{
|
||||
bool isExpressionBody = finishBlock(compiler);
|
||||
|
||||
if (isInitializer)
|
||||
{
|
||||
// If the initializer body evaluates to a value, discard it.
|
||||
if (isExpressionBody) emitOp(compiler, CODE_POP);
|
||||
|
||||
// The receiver is always stored in the first local slot.
|
||||
emitOp(compiler, CODE_LOAD_LOCAL_0);
|
||||
}
|
||||
else if (!isExpressionBody)
|
||||
{
|
||||
// Implicitly return null in statement bodies.
|
||||
emitOp(compiler, CODE_NULL);
|
||||
}
|
||||
|
||||
emitOp(compiler, CODE_RETURN);
|
||||
}
|
||||
|
||||
// The VM can only handle a certain number of parameters, so check that we
|
||||
// haven't exceeded that and give a usable error.
|
||||
static void validateNumParameters(Compiler* compiler, int numArgs)
|
||||
@ -1815,6 +1814,43 @@ static void callMethod(Compiler* compiler, int numArgs, const char* name,
|
||||
emitShortArg(compiler, (Code)(CODE_CALL_0 + numArgs), symbol);
|
||||
}
|
||||
|
||||
// Parses a method or function body, after the initial "{" has been consumed.
|
||||
//
|
||||
// It [isInitializer] is `true`, this is the body of a constructor initializer.
|
||||
// In that case, this adds the code to execute the code in the class body and
|
||||
// make the method return `this`.
|
||||
static void finishBody(Compiler* compiler, bool isInitializer)
|
||||
{
|
||||
if (isInitializer)
|
||||
{
|
||||
// Execute the class body code before the initializer code.
|
||||
loadThis(compiler);
|
||||
callMethod(compiler, 0, "<body>", 6);
|
||||
emitOp(compiler, CODE_POP);
|
||||
// TODO: This regresses performance on classes that don't have any code in
|
||||
// their body and where calling this is a no-op. See if there's a way we
|
||||
// can elide the call when not needed.
|
||||
}
|
||||
|
||||
bool isExpressionBody = finishBlock(compiler);
|
||||
|
||||
if (isInitializer)
|
||||
{
|
||||
// If the initializer body evaluates to a value, discard it.
|
||||
if (isExpressionBody) emitOp(compiler, CODE_POP);
|
||||
|
||||
// The receiver is always stored in the first local slot.
|
||||
emitOp(compiler, CODE_LOAD_LOCAL_0);
|
||||
}
|
||||
else if (!isExpressionBody)
|
||||
{
|
||||
// Implicitly return null in statement bodies.
|
||||
emitOp(compiler, CODE_NULL);
|
||||
}
|
||||
|
||||
emitOp(compiler, CODE_RETURN);
|
||||
}
|
||||
|
||||
// Compiles an (optional) argument list for a method call with [methodSignature]
|
||||
// and then calls it.
|
||||
static void methodCall(Compiler* compiler, Code instruction,
|
||||
@ -1915,22 +1951,6 @@ static void namedCall(Compiler* compiler, bool allowAssignment,
|
||||
}
|
||||
}
|
||||
|
||||
// Loads the receiver of the currently enclosing method. Correctly handles
|
||||
// functions defined inside methods.
|
||||
static void loadThis(Compiler* compiler)
|
||||
{
|
||||
Code loadInstruction;
|
||||
int index = resolveNonmodule(compiler, "this", 4, &loadInstruction);
|
||||
if (loadInstruction == CODE_LOAD_LOCAL)
|
||||
{
|
||||
loadLocal(compiler, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
emitByteArg(compiler, loadInstruction, index);
|
||||
}
|
||||
}
|
||||
|
||||
// Pushes the value for a module-level variable implicitly imported from core.
|
||||
static void loadCoreVariable(Compiler* compiler, const char* name)
|
||||
{
|
||||
@ -3128,25 +3148,21 @@ static void defineMethod(Compiler* compiler, int classSlot, bool isStatic,
|
||||
//
|
||||
// Returns `true` if it compiled successfully, or `false` if the method couldn't
|
||||
// be parsed.
|
||||
static bool method(Compiler* compiler, int classSlot)
|
||||
static bool method(Compiler* compiler, int classSlot, bool isForeign,
|
||||
bool isConstructor)
|
||||
{
|
||||
// TODO: What about foreign constructors?
|
||||
bool isForeign = match(compiler, TOKEN_FOREIGN);
|
||||
compiler->enclosingClass->inStatic = match(compiler, TOKEN_STATIC);
|
||||
|
||||
SignatureFn signatureFn;
|
||||
|
||||
// Methods are declared using "construct" for constructors or "def" for
|
||||
// everything else.
|
||||
if (match(compiler, TOKEN_CONSTRUCT))
|
||||
if (isConstructor)
|
||||
{
|
||||
signatureFn = constructorSignature;
|
||||
}
|
||||
else
|
||||
{
|
||||
consume(compiler, TOKEN_DEF, "Expect 'def' before method definition.");
|
||||
signatureFn = rules[compiler->parser->current.type].method;
|
||||
nextToken(compiler->parser);
|
||||
nextToken(compiler->parser);
|
||||
}
|
||||
|
||||
if (signatureFn == NULL)
|
||||
@ -3264,6 +3280,8 @@ static void classDefinition(Compiler* compiler, bool isForeign)
|
||||
wrenSymbolTableInit(&classCompiler.fields);
|
||||
|
||||
compiler->enclosingClass = &classCompiler;
|
||||
|
||||
initCompiler(&classCompiler.body, compiler->parser, compiler, false);
|
||||
|
||||
// Compile the method definitions.
|
||||
consume(compiler, TOKEN_LEFT_BRACE, "Expect '{' after class declaration.");
|
||||
@ -3271,14 +3289,52 @@ static void classDefinition(Compiler* compiler, bool isForeign)
|
||||
|
||||
while (!match(compiler, TOKEN_RIGHT_BRACE))
|
||||
{
|
||||
if (!method(compiler, slot)) break;
|
||||
// Determine if we're defining a method or executing code in the class body.
|
||||
// Methods can have a couple of different preambles using some combinations
|
||||
// of "foreign", "static", "construct", and "def".
|
||||
compiler->enclosingClass->inStatic = false;
|
||||
|
||||
if (match(compiler, TOKEN_FOREIGN))
|
||||
{
|
||||
compiler->enclosingClass->inStatic = match(compiler, TOKEN_STATIC);
|
||||
consume(compiler, TOKEN_DEF,
|
||||
"Expect 'def' before foreign method declaration.");
|
||||
method(compiler, slot, true, false);
|
||||
}
|
||||
else if (match(compiler, TOKEN_STATIC))
|
||||
{
|
||||
compiler->enclosingClass->inStatic = true;
|
||||
consume(compiler, TOKEN_DEF, "Expect 'def' after 'static'.");
|
||||
method(compiler, slot, false, false);
|
||||
}
|
||||
else if (match(compiler, TOKEN_DEF))
|
||||
{
|
||||
method(compiler, slot, false, false);
|
||||
}
|
||||
else if (match(compiler, TOKEN_CONSTRUCT))
|
||||
{
|
||||
method(compiler, slot, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Any other code gets compiled into the body method.
|
||||
statement(&classCompiler.body);
|
||||
}
|
||||
|
||||
// Don't require a newline after the last definition.
|
||||
// Don't require a newline after the last definition or statement.
|
||||
if (match(compiler, TOKEN_RIGHT_BRACE)) break;
|
||||
|
||||
consumeLine(compiler, "Expect newline after definition in class.");
|
||||
}
|
||||
|
||||
// Define a method to contain all the code in the class body.
|
||||
// TODO: Better debug name including class name.
|
||||
emitOp(&classCompiler.body, CODE_NULL);
|
||||
emitOp(&classCompiler.body, CODE_RETURN);
|
||||
endCompiler(&classCompiler.body, "<body>", 6);
|
||||
int bodySymbol = methodSymbol(compiler, "<body>", 6);
|
||||
defineMethod(compiler, slot, false, bodySymbol);
|
||||
|
||||
// Update the class with the number of fields.
|
||||
if (!isForeign)
|
||||
{
|
||||
|
||||
28
test/language/class/code_in_body.wren
Normal file
28
test/language/class/code_in_body.wren
Normal file
@ -0,0 +1,28 @@
|
||||
class Foo {
|
||||
System.print("one")
|
||||
|
||||
construct new() {
|
||||
System.print("constructor")
|
||||
}
|
||||
|
||||
System.print("two")
|
||||
|
||||
construct other() {
|
||||
System.print("other constructor")
|
||||
}
|
||||
}
|
||||
|
||||
// Does not execute body until object is constructed.
|
||||
System.print("before") // expect: before
|
||||
|
||||
// Executes body before constructor body.
|
||||
Foo.new() // expect: one
|
||||
// expect: two
|
||||
// expect: constructor
|
||||
|
||||
// Executes body for each constructor.
|
||||
Foo.other() // expect: one
|
||||
// expect: two
|
||||
// expect: other constructor
|
||||
|
||||
// TODO: Calling (and not calling) superclass constructor in subclass.
|
||||
9
test/language/class/this_in_body.wren
Normal file
9
test/language/class/this_in_body.wren
Normal file
@ -0,0 +1,9 @@
|
||||
class Foo {
|
||||
construct new() {}
|
||||
|
||||
System.print(this)
|
||||
|
||||
def toString { "foo" }
|
||||
}
|
||||
|
||||
Foo.new() // expect: foo
|
||||
16
test/language/field/field_in_body.wren
Normal file
16
test/language/field/field_in_body.wren
Normal file
@ -0,0 +1,16 @@
|
||||
class Foo {
|
||||
construct new() {
|
||||
System.print("constructor %(_field)")
|
||||
}
|
||||
|
||||
System.print("before %(_field)")
|
||||
|
||||
_field = "initialized"
|
||||
|
||||
System.print("after %(_field)")
|
||||
}
|
||||
|
||||
Foo.new()
|
||||
// expect: before null
|
||||
// expect: after initialized
|
||||
// expect: constructor initialized
|
||||
Reference in New Issue
Block a user