refactor: replace Foo.new constructor syntax with new Foo and simplify superclass constructor calls
Remove the `this new` constructor declaration syntax in favor of a `new` keyword that creates instances directly. Superclass constructors are now invoked via bare `super(args)` instead of `super.new(args)`, eliminating the semantic weirdness of calling a constructor on a partially-constructed instance. Update the compiler to treat `new` as a keyword token, replace the `isMethod` flag with `methodName`/`methodLength` fields, and change the VM's `NEW` opcode to pop the class from the stack before creating the instance. Add a default `new` native method on Object that returns `this`. Update all benchmarks and tests to use the new syntax.
This commit is contained in:
+98
-104
@@ -77,6 +77,7 @@ typedef enum
|
||||
TOKEN_FN,
|
||||
TOKEN_IF,
|
||||
TOKEN_IS,
|
||||
TOKEN_NEW,
|
||||
TOKEN_NULL,
|
||||
TOKEN_RETURN,
|
||||
TOKEN_STATIC,
|
||||
@@ -190,9 +191,6 @@ typedef struct sCompiler
|
||||
// currently inside a class.
|
||||
SymbolTable* fields;
|
||||
|
||||
// If the function being compiled is a method.
|
||||
bool isMethod;
|
||||
|
||||
// The number of local variables currently in scope.
|
||||
int numLocals;
|
||||
|
||||
@@ -201,6 +199,14 @@ typedef struct sCompiler
|
||||
// in effect at all. Any variables declared will be global.
|
||||
int scopeDepth;
|
||||
|
||||
// The name of the method this compiler is compiling, or NULL if this
|
||||
// compiler is not for a method. Note that this is just the bare method name,
|
||||
// and not its full signature.
|
||||
const char* methodName;
|
||||
|
||||
// The length of the method name being compiled.
|
||||
int methodLength;
|
||||
|
||||
// The currently in scope local variables.
|
||||
Local locals[MAX_LOCALS];
|
||||
|
||||
@@ -220,13 +226,14 @@ static int addConstant(Compiler* compiler, Value constant)
|
||||
}
|
||||
|
||||
// Initializes [compiler].
|
||||
static int initCompiler(Compiler* compiler, Parser* parser,
|
||||
Compiler* parent, bool isMethod)
|
||||
static int initCompiler(Compiler* compiler, Parser* parser, Compiler* parent,
|
||||
const char* methodName, int methodLength)
|
||||
{
|
||||
compiler->parser = parser;
|
||||
compiler->parent = parent;
|
||||
compiler->numCodes = 0;
|
||||
compiler->isMethod = isMethod;
|
||||
compiler->methodName = methodName;
|
||||
compiler->methodLength = methodLength;
|
||||
|
||||
if (parent == NULL)
|
||||
{
|
||||
@@ -433,6 +440,7 @@ static void readName(Parser* parser, TokenType type)
|
||||
if (isKeyword(parser, "fn")) type = TOKEN_FN;
|
||||
if (isKeyword(parser, "if")) type = TOKEN_IF;
|
||||
if (isKeyword(parser, "is")) type = TOKEN_IS;
|
||||
if (isKeyword(parser, "new")) type = TOKEN_NEW;
|
||||
if (isKeyword(parser, "null")) type = TOKEN_NULL;
|
||||
if (isKeyword(parser, "return")) type = TOKEN_RETURN;
|
||||
if (isKeyword(parser, "static")) type = TOKEN_STATIC;
|
||||
@@ -652,6 +660,7 @@ static void nextToken(Parser* parser)
|
||||
case TOKEN_ELSE:
|
||||
case TOKEN_IF:
|
||||
case TOKEN_IS:
|
||||
case TOKEN_NEW:
|
||||
case TOKEN_STATIC:
|
||||
case TOKEN_SUPER:
|
||||
case TOKEN_VAR:
|
||||
@@ -1084,17 +1093,10 @@ static void parameterList(Compiler* compiler, char* name, int* length)
|
||||
}
|
||||
}
|
||||
|
||||
// Compiles the method name and argument list for a "<...>.name(...)" call.
|
||||
static void namedCall(Compiler* compiler, bool allowAssignment,
|
||||
Code instruction)
|
||||
// Compiles an (optional) argument list and then calls it.
|
||||
static void methodCall(Compiler* compiler, Code instruction,
|
||||
char name[MAX_METHOD_SIGNATURE], int length)
|
||||
{
|
||||
// Build the method name.
|
||||
consume(compiler, TOKEN_NAME, "Expect method name after '.'.");
|
||||
char name[MAX_METHOD_SIGNATURE];
|
||||
int length = copyName(compiler, name);
|
||||
|
||||
// TODO: Check for "=" here and set assignment and return.
|
||||
|
||||
// Parse the argument list, if any.
|
||||
int numArgs = 0;
|
||||
if (match(compiler, TOKEN_LEFT_PAREN))
|
||||
@@ -1127,6 +1129,20 @@ static void namedCall(Compiler* compiler, bool allowAssignment,
|
||||
emit(compiler, symbol);
|
||||
}
|
||||
|
||||
// Compiles the method name and argument list for a "<...>.name(...)" call.
|
||||
static void namedCall(Compiler* compiler, bool allowAssignment,
|
||||
Code instruction)
|
||||
{
|
||||
// Build the method name.
|
||||
consume(compiler, TOKEN_NAME, "Expect method name after '.'.");
|
||||
char name[MAX_METHOD_SIGNATURE];
|
||||
int length = copyName(compiler, name);
|
||||
|
||||
// TODO: Check for "=" here and set assignment and return.
|
||||
|
||||
methodCall(compiler, instruction, name, length);
|
||||
}
|
||||
|
||||
static void grouping(Compiler* compiler, bool allowAssignment)
|
||||
{
|
||||
expression(compiler);
|
||||
@@ -1183,7 +1199,8 @@ static void boolean(Compiler* compiler, bool allowAssignment)
|
||||
static void function(Compiler* compiler, bool allowAssignment)
|
||||
{
|
||||
Compiler fnCompiler;
|
||||
int constant = initCompiler(&fnCompiler, compiler->parser, compiler, 0);
|
||||
int constant = initCompiler(&fnCompiler, compiler->parser, compiler,
|
||||
NULL, 0);
|
||||
|
||||
parameterList(&fnCompiler, NULL, NULL);
|
||||
|
||||
@@ -1322,13 +1339,28 @@ static bool isInsideMethod(Compiler* compiler)
|
||||
// Walk up the parent chain to see if there is an enclosing method.
|
||||
while (compiler != NULL)
|
||||
{
|
||||
if (compiler->isMethod) return true;
|
||||
if (compiler->methodName != NULL) return true;
|
||||
compiler = compiler->parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void new_(Compiler* compiler, bool allowAssignment)
|
||||
{
|
||||
// TODO: Instead of an expression, explicitly only allow a dotted name.
|
||||
// Compile the class.
|
||||
parsePrecedence(compiler, false, PREC_CALL);
|
||||
|
||||
// Create the instance of the class.
|
||||
emit(compiler, CODE_NEW);
|
||||
|
||||
// Invoke the constructor on the new instance.
|
||||
char name[MAX_METHOD_SIGNATURE];
|
||||
strcpy(name, "new");
|
||||
methodCall(compiler, CODE_CALL_0, name, 3);
|
||||
}
|
||||
|
||||
static void super_(Compiler* compiler, bool allowAssignment)
|
||||
{
|
||||
if (!isInsideMethod(compiler))
|
||||
@@ -1343,10 +1375,37 @@ static void super_(Compiler* compiler, bool allowAssignment)
|
||||
emit(compiler, 0);
|
||||
|
||||
// TODO: Super operator calls.
|
||||
consume(compiler, TOKEN_DOT, "Expect '.' after 'super'.");
|
||||
// TODO: A bare "super" should call the superclass method of the same name
|
||||
// with no arguments.
|
||||
|
||||
// Compile the superclass call.
|
||||
namedCall(compiler, allowAssignment, CODE_SUPER_0);
|
||||
// See if it's a named super call, or an unnamed one.
|
||||
if (match(compiler, TOKEN_DOT))
|
||||
{
|
||||
// Compile the superclass call.
|
||||
namedCall(compiler, allowAssignment, CODE_SUPER_0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No explicit name, so use the name of the enclosing method.
|
||||
char name[MAX_METHOD_SIGNATURE];
|
||||
int length = 0;
|
||||
|
||||
Compiler* thisCompiler = compiler;
|
||||
while (thisCompiler != NULL)
|
||||
{
|
||||
if (thisCompiler->methodName != NULL)
|
||||
{
|
||||
length = thisCompiler->methodLength;
|
||||
strncpy(name, thisCompiler->methodName, length);
|
||||
break;
|
||||
}
|
||||
|
||||
thisCompiler = thisCompiler->parent;
|
||||
}
|
||||
|
||||
// Call the superclass method with the same name.
|
||||
methodCall(compiler, CODE_SUPER_0, name, length);
|
||||
}
|
||||
}
|
||||
|
||||
static void this_(Compiler* compiler, bool allowAssignment)
|
||||
@@ -1487,6 +1546,13 @@ void mixedSignature(Compiler* compiler, char* name, int* length)
|
||||
}
|
||||
}
|
||||
|
||||
// Compiles a method signature for a constructor.
|
||||
void constructorSignature(Compiler* compiler, char* name, int* length)
|
||||
{
|
||||
// Add the parameters, if there are any.
|
||||
parameterList(compiler, name, length);
|
||||
}
|
||||
|
||||
// This table defines all of the parsing rules for the prefix and infix
|
||||
// expressions in the grammar. Expressions are parsed using a Pratt parser.
|
||||
//
|
||||
@@ -1533,6 +1599,7 @@ GrammarRule rules[] =
|
||||
/* TOKEN_FN */ PREFIX(function),
|
||||
/* TOKEN_IF */ UNUSED,
|
||||
/* TOKEN_IS */ INFIX(PREC_IS, is),
|
||||
/* TOKEN_NEW */ { new_, NULL, constructorSignature, PREC_NONE, NULL },
|
||||
/* TOKEN_NULL */ PREFIX(null),
|
||||
/* TOKEN_RETURN */ UNUSED,
|
||||
/* TOKEN_STATIC */ UNUSED,
|
||||
@@ -1584,61 +1651,19 @@ void expression(Compiler* compiler)
|
||||
void method(Compiler* compiler, Code instruction, bool isConstructor,
|
||||
SignatureFn signature)
|
||||
{
|
||||
Compiler methodCompiler;
|
||||
int constant = initCompiler(&methodCompiler, compiler->parser, compiler, 1);
|
||||
|
||||
// Build the method name.
|
||||
char name[MAX_METHOD_SIGNATURE];
|
||||
int length = copyName(compiler, name);
|
||||
|
||||
Compiler methodCompiler;
|
||||
int constant = initCompiler(&methodCompiler, compiler->parser, compiler,
|
||||
name, length);
|
||||
|
||||
// Compile the method signature.
|
||||
signature(&methodCompiler, name, &length);
|
||||
|
||||
int symbol = ensureSymbol(&compiler->parser->vm->methods, name, length);
|
||||
|
||||
if (isConstructor)
|
||||
{
|
||||
// See if there is a superclass constructor call, which comes before the
|
||||
// opening '{'.
|
||||
if (match(compiler, TOKEN_SUPER))
|
||||
{
|
||||
// Push a copy of the class onto the stack so it can be the receiver for
|
||||
// the superclass constructor call.
|
||||
emit(&methodCompiler, CODE_LOAD_LOCAL);
|
||||
emit(&methodCompiler, 0);
|
||||
|
||||
consume(compiler, TOKEN_DOT, "Expect '.' after 'super'.");
|
||||
|
||||
// Compile the superclass call.
|
||||
// TODO: What if there turns out to not be a superclass constructor that
|
||||
// matches this?
|
||||
// TODO: This is being compiled in an instance context even though the
|
||||
// instance hasn't been created yet. Consider something like:
|
||||
//
|
||||
// class Foo is Bar {
|
||||
// new super(_someField) { ... }
|
||||
// }
|
||||
//
|
||||
// The access of _someField in the super() expression is going to do bad
|
||||
// things since the receiver at that point in time is still the class.
|
||||
// Need to handle this better.
|
||||
namedCall(&methodCompiler, false, CODE_SUPER_0);
|
||||
|
||||
// The superclass call will return the new instance, so store it back
|
||||
// into slot 0 to replace the receiver with the new object.
|
||||
emit(&methodCompiler, CODE_STORE_LOCAL);
|
||||
emit(&methodCompiler, 0);
|
||||
|
||||
// Then remove it from the stack.
|
||||
emit(&methodCompiler, CODE_POP);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, just create the new instance.
|
||||
emit(&methodCompiler, CODE_NEW);
|
||||
}
|
||||
}
|
||||
|
||||
consume(compiler, TOKEN_LEFT_BRACE, "Expect '{' to begin method body.");
|
||||
|
||||
finishBlock(&methodCompiler);
|
||||
@@ -1650,16 +1675,15 @@ void method(Compiler* compiler, Code instruction, bool isConstructor,
|
||||
// The receiver is always stored in the first local slot.
|
||||
emit(&methodCompiler, CODE_LOAD_LOCAL);
|
||||
emit(&methodCompiler, 0);
|
||||
|
||||
emit(&methodCompiler, CODE_RETURN);
|
||||
}
|
||||
else
|
||||
{
|
||||
// End the method's code.
|
||||
// Implicitly return null in case there is no explicit return.
|
||||
emit(&methodCompiler, CODE_NULL);
|
||||
emit(&methodCompiler, CODE_RETURN);
|
||||
}
|
||||
|
||||
emit(&methodCompiler, CODE_RETURN);
|
||||
|
||||
endCompiler(&methodCompiler, constant);
|
||||
|
||||
// Compile the code to define the method.
|
||||
@@ -1692,27 +1716,6 @@ void block(Compiler* compiler)
|
||||
emit(compiler, CODE_POP);
|
||||
}
|
||||
|
||||
// Defines a default constructor method with an empty body.
|
||||
static void defaultConstructor(Compiler* compiler)
|
||||
{
|
||||
Compiler ctorCompiler;
|
||||
int constant = initCompiler(&ctorCompiler, compiler->parser, compiler, 1);
|
||||
|
||||
// Create the instance of the class.
|
||||
emit(&ctorCompiler, CODE_NEW);
|
||||
|
||||
// Then return the receiver which is in the first local slot.
|
||||
emit(&ctorCompiler, CODE_LOAD_LOCAL);
|
||||
emit(&ctorCompiler, 0);
|
||||
emit(&ctorCompiler, CODE_RETURN);
|
||||
|
||||
endCompiler(&ctorCompiler, constant);
|
||||
|
||||
// Define the constructor method.
|
||||
emit(compiler, CODE_METHOD_STATIC);
|
||||
emit(compiler, ensureSymbol(&compiler->parser->vm->methods, "new", 3));
|
||||
}
|
||||
|
||||
// Compiles a statement. These can only appear at the top-level or within
|
||||
// curly blocks. Unlike expressions, these do not leave a value on the stack.
|
||||
void statement(Compiler* compiler)
|
||||
@@ -1748,9 +1751,6 @@ void statement(Compiler* compiler)
|
||||
initSymbolTable(&fields);
|
||||
compiler->fields = &fields;
|
||||
|
||||
// Classes with no explicitly defined constructor get a default one.
|
||||
bool hasConstructor = false;
|
||||
|
||||
// Compile the method definitions.
|
||||
consume(compiler, TOKEN_LEFT_BRACE, "Expect '}' after class body.");
|
||||
while (!match(compiler, TOKEN_RIGHT_BRACE))
|
||||
@@ -1765,14 +1765,10 @@ void statement(Compiler* compiler)
|
||||
// Currently, they're compiled as instance fields, which will be wrong
|
||||
// wrong wrong given that the receiver is actually the class obj.
|
||||
}
|
||||
else if (match(compiler, TOKEN_THIS))
|
||||
else if (peek(compiler) == TOKEN_NEW)
|
||||
{
|
||||
// If the method name is prefixed with "this", it's a constructor.
|
||||
// If the method name is "new", it's a constructor.
|
||||
isConstructor = true;
|
||||
hasConstructor = true;
|
||||
|
||||
// Constructors are defined on the class.
|
||||
instruction = CODE_METHOD_STATIC;
|
||||
}
|
||||
|
||||
SignatureFn signature = rules[compiler->parser->current.type].method;
|
||||
@@ -1789,8 +1785,6 @@ void statement(Compiler* compiler)
|
||||
"Expect newline after definition in class.");
|
||||
}
|
||||
|
||||
if (!hasConstructor) defaultConstructor(compiler);
|
||||
|
||||
// Update the class with the number of fields.
|
||||
compiler->fn->bytecode[numFieldsInstruction] = fields.count;
|
||||
|
||||
@@ -1918,7 +1912,7 @@ ObjFn* wrenCompile(WrenVM* vm, const char* source)
|
||||
nextToken(&parser);
|
||||
|
||||
Compiler compiler;
|
||||
initCompiler(&compiler, &parser, NULL, 0);
|
||||
initCompiler(&compiler, &parser, NULL, NULL, 0);
|
||||
|
||||
PinnedObj pinned;
|
||||
pinObj(vm, (Obj*)compiler.fn, &pinned);
|
||||
|
||||
@@ -320,6 +320,13 @@ DEF_NATIVE(object_bangeq)
|
||||
return BOOL_VAL(!wrenValuesEqual(args[0], args[1]));
|
||||
}
|
||||
|
||||
DEF_NATIVE(object_new)
|
||||
{
|
||||
// This is the default argument-less constructor that all objects inherit.
|
||||
// It just returns "this".
|
||||
return args[0];
|
||||
}
|
||||
|
||||
DEF_NATIVE(object_type)
|
||||
{
|
||||
return OBJ_VAL(wrenGetClass(vm, args[0]));
|
||||
@@ -440,6 +447,7 @@ void wrenInitializeCore(WrenVM* vm)
|
||||
vm->objectClass = defineClass(vm, "Object", NULL);
|
||||
NATIVE(vm->objectClass, "== ", object_eqeq);
|
||||
NATIVE(vm->objectClass, "!= ", object_bangeq);
|
||||
NATIVE(vm->objectClass, "new", object_new);
|
||||
NATIVE(vm->objectClass, "type", object_type);
|
||||
|
||||
// The "Class" class is the superclass of all metaclasses.
|
||||
|
||||
+3
-7
@@ -996,13 +996,9 @@ Value interpret(WrenVM* vm, Value function)
|
||||
|
||||
CASE_CODE(NEW):
|
||||
{
|
||||
// The current receiver is the class itself.
|
||||
Value receiver = fiber->stack[frame->stackStart];
|
||||
Value instance = wrenNewInstance(vm, AS_CLASS(receiver));
|
||||
|
||||
// Store the new instance back in the receiver slot so that now it can be
|
||||
// "this" in the body of the constructor and returned by it.
|
||||
fiber->stack[frame->stackStart] = instance;
|
||||
// TODO: Handle object not being a class.
|
||||
ObjClass* classObj = AS_CLASS(POP());
|
||||
PUSH(wrenNewInstance(vm, classObj));
|
||||
DISPATCH();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user