diff --git a/doc/site/classes.markdown b/doc/site/classes.markdown index ff680d9b..1267ae00 100644 --- a/doc/site/classes.markdown +++ b/doc/site/classes.markdown @@ -635,6 +635,131 @@ class Derived is Base { } + +## Attributes + +**experimental stage**: subject to minor changes + +A class and methods within a class can be tagged with 'meta attributes'. + +Like this: + +
+#hidden = true
+class Example {}
+
+ +These attributes are metadata, they give you a way to annotate and store +any additional information about a class, which you can optionally access at runtime. +This information can also be used by external tools, to provide additional +hints and information from code to the tool. + + +Since this feature has just been introduced, **take note**. + +**Currently** there are no attributes with a built-in meaning. +Attributes are user-defined metadata. This may not remain +true as some may become well defined through convention or potentially +through use by Wren itself. + + +Attributes are placed before a class or method definition, +and use the `#` hash/pound symbol. + +They can be + +- a `#key` on it's own +- a `#key = value` +- a `#group(with, multiple = true, keys = "value")` + +An attribute _key_ can only be a `Name`. This is the same type of name +as a method name, a class name or variable name, an identifier that matches +the Wren identifier rules. A name results in a String value at runtime. + +An attribute _value_ can be any of these literal values: `Name, String, Bool, Num`. +Values cannot contain expressions, just a value, there is no compile time +evaluation. + +Groups can span multiple lines, methods have their own attributes, and duplicate +keys are valid. + +
+#key
+#key = value
+#group(
+  multiple,
+  lines = true,
+  lines = 0
+)
+class Example {
+  #test(skip = true, iterations = 32)
+  doStuff() {}
+}
+
+ +### Accessing attributes at runtime + +By default, attributes are compiled out and ignored. + +For an attribute to be visible at runtime, mark it for runtime +access using an exclamation: + +
+#doc = "not runtime data"
+#!runtimeAccess = true
+#!maxIterations = 16
+
+ +Attributes at runtime are stored on the class. You can access them via +`YourClass.attributes`. The `attributes` field on a class will +be null if a class has no attributes or if it's attributes aren't marked. + +If the class contains class or method attributes, it will be an object with +two getters: + +- `YourClass.attributes.self` for the class attributes +- `YourClass.attributes.methods` for the method attributes + +Attributes are stored by group in a regular Wren Map. +Keys that are not grouped, use `null` as the group key. + +Values are stored in a list, since duplicate keys are allowed, multiple +values need to be stored. They're stored in order of definition. + +Method attributes are stored in a map by method signature, and each method +has it's own attributes that match the above structure. The method signature +is prefixed by `static` or `foreign static` as needed. + +Let's see what that looks like: + +
+// Example.attributes.self = 
+// { 
+//   null: { "key":[null] }, 
+//   group: { "key":[value, 32, false] }
+// }
+
+#!key
+#ignored //compiled out
+#!group(key=value, key=32, key=false)
+class Example {
+  #!getter
+  getter {}
+
+  // { regular(_,_): { regular:[null] } }
+  #!regular
+  regular(arg0, arg1) {}
+
+  // { static other(): { isStatic:[true] } }
+  #!isStatic = true
+  static other()
+  
+  // { foreign static example(): { isForeignStatic:[32] } }
+  #!isForeignStatic=32
+  foreign static example()
+}
+
+

Concurrency → ← Functions diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index bd153e92..c0ed2a5c 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -66,6 +66,7 @@ typedef enum TOKEN_STAR, TOKEN_SLASH, TOKEN_PERCENT, + TOKEN_HASH, TOKEN_PLUS, TOKEN_MINUS, TOKEN_LTLT, @@ -293,6 +294,11 @@ typedef struct // The name of the class. ObjString* name; + // Attributes for the class itself + ObjMap* classAttributes; + // Attributes for methods in this class + ObjMap* methodAttributes; + // Symbol table for the fields of the class. SymbolTable fields; @@ -360,6 +366,15 @@ struct sCompiler // Whether or not the compiler is for a constructor initializer bool isInitializer; + + // The number of attributes seen while parsing. + // We track this separately as compile time attributes + // are not stored, so we can't rely on attributes->count + // to enforce an error message when attributes are used + // anywhere other than methods or classes. + int numAttributes; + // Attributes for the next class or method. + ObjMap* attributes; }; // Describes where a variable is declared. @@ -386,6 +401,14 @@ typedef struct Scope scope; } Variable; +// Forward declarations +static void disallowAttributes(Compiler* compiler); +static void addToAttributeGroup(Compiler* compiler, Value group, Value key, Value value); +static void emitClassAttributes(Compiler* compiler, ClassInfo* classInfo); +static void copyAttributes(Compiler* compiler, ObjMap* into); +static void copyMethodAttributes(Compiler* compiler, bool isForeign, + bool isStatic, const char* fullSignature, int32_t length); + // 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[] = { @@ -557,6 +580,8 @@ static void initCompiler(Compiler* compiler, Parser* parser, Compiler* parent, compiler->scopeDepth = 0; } + compiler->numAttributes = 0; + compiler->attributes = wrenNewMap(parser->vm); compiler->fn = wrenNewFunction(parser->vm, parser->module, compiler->numLocals); } @@ -789,11 +814,16 @@ static void readNumber(Parser* parser) } // Finishes lexing an identifier. Handles reserved words. -static void readName(Parser* parser, TokenType type) +static void readName(Parser* parser, TokenType type, char firstChar) { + ByteBuffer string; + wrenByteBufferInit(&string); + wrenByteBufferWrite(parser->vm, &string, firstChar); + while (isName(peekChar(parser)) || isDigit(peekChar(parser))) { - nextChar(parser); + char c = nextChar(parser); + wrenByteBufferWrite(parser->vm, &string, c); } // Update the type if it's a keyword. @@ -808,6 +838,10 @@ static void readName(Parser* parser, TokenType type) } } + parser->next.value = wrenNewStringLength(parser->vm, + (char*)string.data, string.count); + + wrenByteBufferClear(parser->vm, &string); makeToken(parser, type); } @@ -1057,6 +1091,17 @@ static void nextToken(Parser* parser) case ',': makeToken(parser, TOKEN_COMMA); return; case '*': makeToken(parser, TOKEN_STAR); return; case '%': makeToken(parser, TOKEN_PERCENT); return; + case '#': { + // Ignore shebang on the first line. + if (parser->currentLine == 1 && peekChar(parser) == '!' && peekNextChar(parser) == '/') + { + skipLineComment(parser); + break; + } + // Otherwise we treat it as a token a token + makeToken(parser, TOKEN_HASH); + return; + } case '^': makeToken(parser, TOKEN_CARET); return; case '+': makeToken(parser, TOKEN_PLUS); return; case '-': makeToken(parser, TOKEN_MINUS); return; @@ -1141,7 +1186,7 @@ static void nextToken(Parser* parser) } case '_': readName(parser, - peekChar(parser) == '_' ? TOKEN_STATIC_FIELD : TOKEN_FIELD); + peekChar(parser) == '_' ? TOKEN_STATIC_FIELD : TOKEN_FIELD, c); return; case '0': @@ -1155,15 +1200,9 @@ static void nextToken(Parser* parser) return; default: - if (parser->currentLine == 1 && c == '#' && peekChar(parser) == '!') - { - // Ignore shebang on the first line. - skipLineComment(parser); - break; - } if (isName(c)) { - readName(parser, TOKEN_NAME); + readName(parser, TOKEN_NAME, c); } else if (isDigit(c)) { @@ -2719,6 +2758,7 @@ GrammarRule rules[] = /* TOKEN_STAR */ INFIX_OPERATOR(PREC_FACTOR, "*"), /* TOKEN_SLASH */ INFIX_OPERATOR(PREC_FACTOR, "/"), /* TOKEN_PERCENT */ INFIX_OPERATOR(PREC_FACTOR, "%"), + /* TOKEN_HASH */ UNUSED, /* TOKEN_PLUS */ INFIX_OPERATOR(PREC_TERM, "+"), /* TOKEN_MINUS */ OPERATOR("-"), /* TOKEN_LTLT */ INFIX_OPERATOR(PREC_BITWISE_SHIFT, "<<"), @@ -2841,6 +2881,7 @@ static int getByteCountForArguments(const uint8_t* bytecode, case CODE_FOREIGN_CONSTRUCT: case CODE_FOREIGN_CLASS: case CODE_END_MODULE: + case CODE_END_CLASS: return 0; case CODE_LOAD_LOCAL: @@ -3296,12 +3337,96 @@ static int declareMethod(Compiler* compiler, Signature* signature, return symbol; } +static Value consumeLiteral(Compiler* compiler, const char* message) +{ + if(match(compiler, TOKEN_FALSE)) return FALSE_VAL; + if(match(compiler, TOKEN_TRUE)) return TRUE_VAL; + if(match(compiler, TOKEN_NUMBER)) return compiler->parser->previous.value; + if(match(compiler, TOKEN_STRING)) return compiler->parser->previous.value; + if(match(compiler, TOKEN_NAME)) return compiler->parser->previous.value; + + error(compiler, message); + nextToken(compiler->parser); + return NULL_VAL; +} + +static bool matchAttribute(Compiler* compiler) { + + if(match(compiler, TOKEN_HASH)) + { + compiler->numAttributes++; + bool runtimeAccess = match(compiler, TOKEN_BANG); + if(match(compiler, TOKEN_NAME)) + { + Value group = compiler->parser->previous.value; + TokenType ahead = peek(compiler); + if(ahead == TOKEN_EQ || ahead == TOKEN_LINE) + { + Value key = group; + Value value = NULL_VAL; + if(match(compiler, TOKEN_EQ)) + { + value = consumeLiteral(compiler, "Expect a Bool, Num, String or Identifier literal for an attribute value."); + } + if(runtimeAccess) addToAttributeGroup(compiler, NULL_VAL, key, value); + } + else if(match(compiler, TOKEN_LEFT_PAREN)) + { + ignoreNewlines(compiler); + if(match(compiler, TOKEN_RIGHT_PAREN)) + { + error(compiler, "Expected attributes in group, group cannot be empty."); + } + else + { + while(peek(compiler) != TOKEN_RIGHT_PAREN) + { + consume(compiler, TOKEN_NAME, "Expect name for attribute key."); + Value key = compiler->parser->previous.value; + Value value = NULL_VAL; + if(match(compiler, TOKEN_EQ)) + { + value = consumeLiteral(compiler, "Expect a Bool, Num, String or Identifier literal for an attribute value."); + } + if(runtimeAccess) addToAttributeGroup(compiler, group, key, value); + ignoreNewlines(compiler); + if(!match(compiler, TOKEN_COMMA)) break; + ignoreNewlines(compiler); + } + + ignoreNewlines(compiler); + consume(compiler, TOKEN_RIGHT_PAREN, + "Expected ')' after grouped attributes."); + } + } + else + { + error(compiler, "Expect an equal, newline or grouping after an attribute key."); + } + } + else + { + error(compiler, "Expect an attribute definition after #."); + } + + consumeLine(compiler, "Expect newline after attribute."); + return true; + } + + return false; +} + // Compiles a method definition inside a class body. // // Returns `true` if it compiled successfully, or `false` if the method couldn't // be parsed. static bool method(Compiler* compiler, Variable classVariable) { + // Parse any attributes before the method and store them + if(matchAttribute(compiler)) { + return method(compiler, classVariable); + } + // TODO: What about foreign constructors? bool isForeign = match(compiler, TOKEN_FOREIGN); bool isStatic = match(compiler, TOKEN_STATIC); @@ -3338,6 +3463,9 @@ static bool method(Compiler* compiler, Variable classVariable) int length; signatureToString(&signature, fullSignature, &length); + // Copy any attributes the compiler collected into the enclosing class + copyMethodAttributes(compiler, isForeign, isStatic, fullSignature, length); + // Check for duplicate methods. Doesn't matter that it's already been // defined, error will discard bytecode anyway. // Check if the method table already contains this symbol @@ -3431,6 +3559,15 @@ static void classDefinition(Compiler* compiler, bool isForeign) classInfo.isForeign = isForeign; classInfo.name = className; + // Allocate attribute maps if necessary. + // A method will allocate the methods one if needed + classInfo.classAttributes = compiler->attributes->count > 0 + ? wrenNewMap(compiler->parser->vm) + : NULL; + classInfo.methodAttributes = NULL; + // Copy any existing attributes into the class + copyAttributes(compiler, classInfo.classAttributes); + // Set up a symbol table for the class's fields. We'll initially compile // them to slots starting at zero. When the method is bound to the class, the // bytecode will be adjusted by [wrenBindMethod] to take inherited fields @@ -3456,6 +3593,20 @@ static void classDefinition(Compiler* compiler, bool isForeign) consumeLine(compiler, "Expect newline after definition in class."); } + // If any attributes are present, + // instantiate a ClassAttributes instance for the class + // and send it over to CODE_END_CLASS + bool hasAttr = classInfo.classAttributes != NULL || + classInfo.methodAttributes != NULL; + if(hasAttr) { + emitClassAttributes(compiler, &classInfo); + loadVariable(compiler, classVariable); + // At the moment, we don't have other uses for CODE_END_CLASS, + // so we put it inside this condition. Later, we can always + // emit it and use it as needed. + emitOp(compiler, CODE_END_CLASS); + } + // Update the class with the number of fields. if (!isForeign) { @@ -3571,16 +3722,26 @@ static void variableDefinition(Compiler* compiler) // like the non-curly body of an if or while. void definition(Compiler* compiler) { + if(matchAttribute(compiler)) { + definition(compiler); + return; + } + if (match(compiler, TOKEN_CLASS)) { classDefinition(compiler, false); + return; } else if (match(compiler, TOKEN_FOREIGN)) { consume(compiler, TOKEN_CLASS, "Expect 'class' after 'foreign'."); classDefinition(compiler, true); + return; } - else if (match(compiler, TOKEN_IMPORT)) + + disallowAttributes(compiler); + + if (match(compiler, TOKEN_IMPORT)) { import(compiler); } @@ -3748,13 +3909,222 @@ void wrenMarkCompiler(WrenVM* vm, Compiler* compiler) { wrenGrayObj(vm, (Obj*)compiler->fn); wrenGrayObj(vm, (Obj*)compiler->constants); + wrenGrayObj(vm, (Obj*)compiler->attributes); if (compiler->enclosingClass != NULL) { wrenBlackenSymbolTable(vm, &compiler->enclosingClass->fields); + + if(compiler->enclosingClass->methodAttributes != NULL) + { + wrenGrayObj(vm, (Obj*)compiler->enclosingClass->methodAttributes); + } + if(compiler->enclosingClass->classAttributes != NULL) + { + wrenGrayObj(vm, (Obj*)compiler->enclosingClass->classAttributes); + } } compiler = compiler->parent; } while (compiler != NULL); } + +// Helpers for Attributes + +// Throw an error if any attributes were found preceding, +// and clear the attributes so the error doesn't keep happening. +static void disallowAttributes(Compiler* compiler) +{ + if (compiler->numAttributes > 0) + { + error(compiler, "Attributes can only specified before a class or a method"); + wrenMapClear(compiler->parser->vm, compiler->attributes); + compiler->numAttributes = 0; + } +} + +// Add an attribute to a given group in the compiler attribues map +static void addToAttributeGroup(Compiler* compiler, + Value group, Value key, Value value) +{ + WrenVM* vm = compiler->parser->vm; + + if(IS_OBJ(group)) wrenPushRoot(vm, AS_OBJ(group)); + if(IS_OBJ(key)) wrenPushRoot(vm, AS_OBJ(key)); + if(IS_OBJ(value)) wrenPushRoot(vm, AS_OBJ(value)); + + Value groupMapValue = wrenMapGet(compiler->attributes, group); + if(groupMapValue == UNDEFINED_VAL) + { + groupMapValue = OBJ_VAL(wrenNewMap(vm)); + wrenMapSet(vm, compiler->attributes, group, groupMapValue); + } + + //we store them as a map per so we can maintain duplicate keys + //group = { key:[value, ...], } + ObjMap* groupMap = AS_MAP(groupMapValue); + + //var keyItems = group[key] + //if(!keyItems) keyItems = group[key] = [] + Value keyItemsValue = wrenMapGet(groupMap, key); + if(keyItemsValue == UNDEFINED_VAL) + { + keyItemsValue = OBJ_VAL(wrenNewList(vm, 0)); + wrenMapSet(vm, groupMap, key, keyItemsValue); + } + + //keyItems.add(value) + ObjList* keyItems = AS_LIST(keyItemsValue); + wrenValueBufferWrite(vm, &keyItems->elements, value); + + if(IS_OBJ(group)) wrenPopRoot(vm); + if(IS_OBJ(key)) wrenPopRoot(vm); + if(IS_OBJ(value)) wrenPopRoot(vm); +} + + +// Emit the attributes in the give map onto the stack +static void emitAttributes(Compiler* compiler, ObjMap* attributes) +{ + // Instantiate a new map for the attributes + loadCoreVariable(compiler, "Map"); + callMethod(compiler, 0, "new()", 5); + + // The attributes are stored as group = { key:[value, value, ...] } + // so our first level is the group map + for(uint32_t groupIdx = 0; groupIdx < attributes->capacity; groupIdx++) + { + const MapEntry* groupEntry = &attributes->entries[groupIdx]; + if(groupEntry->key == UNDEFINED_VAL) continue; + //group key + emitConstant(compiler, groupEntry->key); + + //group value is gonna be a map + loadCoreVariable(compiler, "Map"); + callMethod(compiler, 0, "new()", 5); + + ObjMap* groupItems = AS_MAP(groupEntry->value); + for(uint32_t itemIdx = 0; itemIdx < groupItems->capacity; itemIdx++) + { + const MapEntry* itemEntry = &groupItems->entries[itemIdx]; + if(itemEntry->key == UNDEFINED_VAL) continue; + + emitConstant(compiler, itemEntry->key); + // Attribute key value, key = [] + loadCoreVariable(compiler, "List"); + callMethod(compiler, 0, "new()", 5); + // Add the items to the key list + ObjList* items = AS_LIST(itemEntry->value); + for(int itemIdx = 0; itemIdx < items->elements.count; ++itemIdx) + { + emitConstant(compiler, items->elements.data[itemIdx]); + callMethod(compiler, 1, "addCore_(_)", 11); + } + // Add the list to the map + callMethod(compiler, 2, "addCore_(_,_)", 13); + } + + // Add the key/value to the map + callMethod(compiler, 2, "addCore_(_,_)", 13); + } + +} + +// Methods are stored as method <-> attributes, so we have to have +// an indirection to resolve for methods +static void emitAttributeMethods(Compiler* compiler, ObjMap* attributes) +{ + // Instantiate a new map for the attributes + loadCoreVariable(compiler, "Map"); + callMethod(compiler, 0, "new()", 5); + + for(uint32_t methodIdx = 0; methodIdx < attributes->capacity; methodIdx++) + { + const MapEntry* methodEntry = &attributes->entries[methodIdx]; + if(methodEntry->key == UNDEFINED_VAL) continue; + emitConstant(compiler, methodEntry->key); + ObjMap* attributeMap = AS_MAP(methodEntry->value); + emitAttributes(compiler, attributeMap); + callMethod(compiler, 2, "addCore_(_,_)", 13); + } +} + + +// Emit the final ClassAttributes that exists at runtime +static void emitClassAttributes(Compiler* compiler, ClassInfo* classInfo) +{ + loadCoreVariable(compiler, "ClassAttributes"); + + classInfo->classAttributes + ? emitAttributes(compiler, classInfo->classAttributes) + : null(compiler, false); + + classInfo->methodAttributes + ? emitAttributeMethods(compiler, classInfo->methodAttributes) + : null(compiler, false); + + callMethod(compiler, 2, "new(_,_)", 8); +} + +// Copy the current attributes stored in the compiler into a destination map +// This also resets the counter, since the intent is to consume the attributes +static void copyAttributes(Compiler* compiler, ObjMap* into) +{ + compiler->numAttributes = 0; + + if(compiler->attributes->count == 0) return; + if(into == NULL) return; + + WrenVM* vm = compiler->parser->vm; + + // Note we copy the actual values as is since we'll take ownership + // and clear the original map + for(uint32_t attrIdx = 0; attrIdx < compiler->attributes->capacity; attrIdx++) + { + const MapEntry* attrEntry = &compiler->attributes->entries[attrIdx]; + if(attrEntry->key == UNDEFINED_VAL) continue; + wrenMapSet(vm, into, attrEntry->key, attrEntry->value); + } + + wrenMapClear(vm, compiler->attributes); +} + +// Copy the current attributes stored in the compiler into the method specific +// attributes for the current enclosingClass. +// This also resets the counter, since the intent is to consume the attributes +static void copyMethodAttributes(Compiler* compiler, bool isForeign, + bool isStatic, const char* fullSignature, int32_t length) +{ + compiler->numAttributes = 0; + + if(compiler->attributes->count == 0) return; + + WrenVM* vm = compiler->parser->vm; + + // Make a map for this method to copy into + ObjMap* methodAttr = wrenNewMap(vm); + wrenPushRoot(vm, (Obj*)methodAttr); + copyAttributes(compiler, methodAttr); + + // Include 'foreign static ' in front as needed + int32_t fullLength = length; + if(isForeign) fullLength += 8; + if(isStatic) fullLength += 7; + char fullSignatureWithPrefix[MAX_METHOD_SIGNATURE + 8 + 7]; + const char* foreignPrefix = isForeign ? "foreign " : ""; + const char* staticPrefix = isStatic ? "static " : ""; + sprintf(fullSignatureWithPrefix, "%s%s%.*s", foreignPrefix, staticPrefix, + length, fullSignature); + fullSignatureWithPrefix[fullLength] = '\0'; + + if(compiler->enclosingClass->methodAttributes == NULL) { + compiler->enclosingClass->methodAttributes = wrenNewMap(vm); + } + + // Store the method attributes in the class map + Value key = wrenNewStringLength(vm, fullSignatureWithPrefix, fullLength); + wrenMapSet(vm, compiler->enclosingClass->methodAttributes, key, OBJ_VAL(methodAttr)); + + wrenPopRoot(vm); +} diff --git a/src/vm/wren_core.c b/src/vm/wren_core.c index ec5238d5..d0a121f8 100644 --- a/src/vm/wren_core.c +++ b/src/vm/wren_core.c @@ -50,6 +50,11 @@ DEF_PRIMITIVE(class_toString) RETURN_OBJ(AS_CLASS(args[0])->name); } +DEF_PRIMITIVE(class_attributes) +{ + RETURN_VAL(AS_CLASS(args[0])->attributes); +} + DEF_PRIMITIVE(fiber_new) { if (!validateFn(vm, args[1], "Argument")) return false; @@ -1252,6 +1257,7 @@ void wrenInitializeCore(WrenVM* vm) PRIMITIVE(vm->classClass, "name", class_name); PRIMITIVE(vm->classClass, "supertype", class_supertype); PRIMITIVE(vm->classClass, "toString", class_toString); + PRIMITIVE(vm->classClass, "attributes", class_attributes); // Finally, we can define Object's metaclass which is a subclass of Class. ObjClass* objectMetaclass = defineClass(vm, coreModule, "Object metaclass"); diff --git a/src/vm/wren_core.wren b/src/vm/wren_core.wren index 58033e7c..e1192b8d 100644 --- a/src/vm/wren_core.wren +++ b/src/vm/wren_core.wren @@ -471,3 +471,13 @@ class System { } } } + +class ClassAttributes { + self { _attributes } + methods { _methods } + construct new(attributes, methods) { + _attributes = attributes + _methods = methods + } + toString { "attributes:%(_attributes) methods:%(_methods)" } +} \ No newline at end of file diff --git a/src/vm/wren_core.wren.inc b/src/vm/wren_core.wren.inc index 4b706f42..b4f7e1a8 100644 --- a/src/vm/wren_core.wren.inc +++ b/src/vm/wren_core.wren.inc @@ -472,4 +472,15 @@ static const char* coreModuleSource = " writeString_(\"[invalid toString]\")\n" " }\n" " }\n" +"}\n" +"\n" +"class ClassAttributes {\n" +" self { _attributes }\n" +" methods { _methods }\n" +" construct new(attributes, methods) {\n" +" _attributes = attributes\n" +" _methods = methods\n" +" }\n" +" toString { \"attributes:%(_attributes) methods:%(_methods)\" }\n" "}\n"; + diff --git a/src/vm/wren_debug.c b/src/vm/wren_debug.c index 0ed8231f..2c466486 100644 --- a/src/vm/wren_debug.c +++ b/src/vm/wren_debug.c @@ -296,6 +296,7 @@ static int dumpInstruction(WrenVM* vm, ObjFn* fn, int i, int* lastLine) } case CODE_FOREIGN_CLASS: printf("FOREIGN_CLASS\n"); break; + case CODE_END_CLASS: printf("END_CLASS\n"); break; case CODE_METHOD_INSTANCE: { diff --git a/src/vm/wren_opcodes.h b/src/vm/wren_opcodes.h index 0fa7f781..46ba8b47 100644 --- a/src/vm/wren_opcodes.h +++ b/src/vm/wren_opcodes.h @@ -169,6 +169,10 @@ OPCODE(FOREIGN_CONSTRUCT, 0) // the name of the class. Byte [arg] is the number of fields in the class. OPCODE(CLASS, -1) +// Ends a class. +// Atm the stack contains the class and the ClassAttributes (or null). +OPCODE(END_CLASS, -2) + // Creates a foreign class. Top of stack is the superclass. Below that is a // string for the name of the class. OPCODE(FOREIGN_CLASS, -1) diff --git a/src/vm/wren_value.c b/src/vm/wren_value.c index 92bea794..ad703e5d 100644 --- a/src/vm/wren_value.c +++ b/src/vm/wren_value.c @@ -50,6 +50,7 @@ ObjClass* wrenNewSingleClass(WrenVM* vm, int numFields, ObjString* name) classObj->superclass = NULL; classObj->numFields = numFields; classObj->name = name; + classObj->attributes = NULL_VAL; wrenPushRoot(vm, (Obj*)classObj); wrenMethodBufferInit(&classObj->methods); @@ -1028,6 +1029,8 @@ static void blackenClass(WrenVM* vm, ObjClass* classObj) wrenGrayObj(vm, (Obj*)classObj->name); + if(classObj->attributes != NULL_VAL) wrenGrayObj(vm, AS_OBJ(classObj->attributes)); + // Keep track of how much memory is still in use. vm->bytesAllocated += sizeof(ObjClass); vm->bytesAllocated += classObj->methods.capacity * sizeof(Method); diff --git a/src/vm/wren_value.h b/src/vm/wren_value.h index 98ec952a..2ca0cfdc 100644 --- a/src/vm/wren_value.h +++ b/src/vm/wren_value.h @@ -410,6 +410,9 @@ struct sObjClass // The name of the class. ObjString* name; + + // The ClassAttribute for the class, if any + Value attributes; }; typedef struct diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index df4d706c..254d0b03 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -607,6 +607,27 @@ static void bindForeignClass(WrenVM* vm, ObjClass* classObj, ObjModule* module) } } +// Completes the process for creating a new class. +// +// The class attributes instance and the class itself should be on the +// top of the fiber's stack. +// +// This process handles moving the attribute data for a class from +// compile time to runtime, since it now has all the attributes associated +// with a class, including for methods. +static void endClass(WrenVM* vm) +{ + // Pull the attributes and class off the stack + Value attributes = vm->fiber->stackTop[-2]; + Value classValue = vm->fiber->stackTop[-1]; + + // Remove the stack items + vm->fiber->stackTop -= 2; + + ObjClass* classObj = AS_CLASS(classValue); + classObj->attributes = attributes; +} + // Creates a new class. // // If [numFields] is -1, the class is a foreign class. The name and superclass @@ -1274,6 +1295,13 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) DISPATCH(); } + CASE_CODE(END_CLASS): + { + endClass(vm); + if (wrenHasError(fiber)) RUNTIME_ERROR(); + DISPATCH(); + } + CASE_CODE(CLASS): { createClass(vm, READ_BYTE(), NULL); diff --git a/test/language/class/attributes/attributes.wren b/test/language/class/attributes/attributes.wren new file mode 100644 index 00000000..2bfb743a --- /dev/null +++ b/test/language/class/attributes/attributes.wren @@ -0,0 +1,21 @@ +// Test the basic states. Keys without a group +// go into a group with null as the key + +#!key +class Attr {} + +System.print(Attr.attributes != null) // expect: true +System.print(Attr.attributes.self != null) // expect: true +System.print(Attr.attributes.methods) // expect: null + +var attr = Attr.attributes.self +var nullGroup = attr[null] +System.print(nullGroup != null) // expect: true +System.print(nullGroup.count) // expect: 1 +System.print(nullGroup.containsKey("key")) // expect: true + +var keyItems = nullGroup["key"] +System.print(keyItems != null) // expect: true +System.print(keyItems is List) // expect: true +System.print(keyItems.count) // expect: 1 +System.print(keyItems[0]) // expect: null diff --git a/test/language/class/attributes/compile_only.wren b/test/language/class/attributes/compile_only.wren new file mode 100644 index 00000000..a9c056aa --- /dev/null +++ b/test/language/class/attributes/compile_only.wren @@ -0,0 +1,9 @@ +// Attributes without a ! shouldn't be +// passed to the runtime, they're compiled out + +#compileonly +class WithNonRuntime { + #unused + method() {} +} +System.print(WithNonRuntime.attributes == null) // expect: true diff --git a/test/language/class/attributes/duplicate_keys.wren b/test/language/class/attributes/duplicate_keys.wren new file mode 100644 index 00000000..bce448ab --- /dev/null +++ b/test/language/class/attributes/duplicate_keys.wren @@ -0,0 +1,11 @@ +// Duplicate keys add multiple values to +// the attribute's key, in parse order +#!key +#!key = value +#!key=other +class DuplicateKeys {} + +var dupeGroup = DuplicateKeys.attributes.self[null] +System.print(dupeGroup.count) // expect: 1 +System.print(dupeGroup["key"].count) // expect: 3 +System.print(dupeGroup["key"]) // expect: [null, value, other] diff --git a/test/language/class/attributes/groups.wren b/test/language/class/attributes/groups.wren new file mode 100644 index 00000000..c1c2ae3f --- /dev/null +++ b/test/language/class/attributes/groups.wren @@ -0,0 +1,17 @@ +// Groups store attributes by named group + +#!key //not combined +#!group(key=combined) +#!group(key=value, key=2, key=false) +class GroupedKeys {} + +var ungroupedKeys = GroupedKeys.attributes.self[null] +var groupedKeys = GroupedKeys.attributes.self["group"] + +System.print(ungroupedKeys.count) // expect: 1 +System.print(groupedKeys.count) // expect: 1 +System.print(ungroupedKeys.containsKey("key")) // expect: true +var groupedKey = groupedKeys["key"] +System.print(groupedKey.count) // expect: 4 +System.print(groupedKey) // expect: [combined, value, 2, false] + diff --git a/test/language/class/attributes/invalid_expression.wren b/test/language/class/attributes/invalid_expression.wren new file mode 100644 index 00000000..0a9235cd --- /dev/null +++ b/test/language/class/attributes/invalid_expression.wren @@ -0,0 +1,12 @@ + +// When used in an expression location, +// the error remains Error at '#': Expected expression + +#valid +class Example { + + #valid + method() { + return #invalid 1 // expect error + } +} \ No newline at end of file diff --git a/test/language/class/attributes/invalid_scope.wren b/test/language/class/attributes/invalid_scope.wren new file mode 100644 index 00000000..5037abc8 --- /dev/null +++ b/test/language/class/attributes/invalid_scope.wren @@ -0,0 +1,11 @@ + + +#valid +class Example { + + #valid + method() { + #invalid // expect error + var a = 3 + } +} \ No newline at end of file diff --git a/test/language/class/attributes/invalid_toplevel.wren b/test/language/class/attributes/invalid_toplevel.wren new file mode 100644 index 00000000..68b9f441 --- /dev/null +++ b/test/language/class/attributes/invalid_toplevel.wren @@ -0,0 +1,4 @@ + + +#meta // expect error +var A = 3 \ No newline at end of file diff --git a/test/language/class/attributes/literals.wren b/test/language/class/attributes/literals.wren new file mode 100644 index 00000000..118358ce --- /dev/null +++ b/test/language/class/attributes/literals.wren @@ -0,0 +1,20 @@ +// Keys must be a name, and values can be any literal value + +#!name = name +#!string = "string" +#!integer = 32 +#!number = 2.5 +#!bool = true +class Literals {} + +var literalGroup = Literals.attributes.self[null] + +System.print(literalGroup.count) // expect: 5 +System.print(literalGroup["string"][0] is String) // expect: true +System.print(literalGroup["string"][0]) // expect: string +System.print(literalGroup["integer"][0] is Num) // expect: true +System.print(literalGroup["integer"][0]) // expect: 32 +System.print(literalGroup["number"][0] is Num) // expect: true +System.print(literalGroup["number"][0]) // expect: 2.5 +System.print(literalGroup["bool"][0] is Bool) // expect: true +System.print(literalGroup["bool"][0]) // expect: true diff --git a/test/language/class/attributes/methods.wren b/test/language/class/attributes/methods.wren new file mode 100644 index 00000000..fd13c01e --- /dev/null +++ b/test/language/class/attributes/methods.wren @@ -0,0 +1,32 @@ + +class Methods { + + #!getter + method {} + + method() {} + + #!regular = 2 + #!group(key, other=value, string="hello") + method(arg0, arg1) {} + + #!is_static = true + static method() {} + +} + +var methodAttr = Methods.attributes.methods +var getter = methodAttr["method"] +var none = methodAttr["method()"] +var regular = methodAttr["method(_,_)"] +var aStatic = methodAttr["static method()"] + +// (Be wary of relying on map order) + +System.print(getter) // expect: {null: {getter: [null]}} +System.print(none) // expect: null +System.print(regular[null]) // expect: {regular: [2]} +System.print(regular["group"]["key"]) // expect: [null] +System.print(regular["group"]["other"]) // expect: [value] +System.print(regular["group"]["string"]) // expect: [hello] +System.print(aStatic[null]) // expect: {is_static: [true]} diff --git a/test/language/class/attributes/without.wren b/test/language/class/attributes/without.wren new file mode 100644 index 00000000..803fad25 --- /dev/null +++ b/test/language/class/attributes/without.wren @@ -0,0 +1,4 @@ +// With no attributes defined, no ClassAttributes should be allocated + +class Without {} +System.print(Without.attributes == null) // expect: true