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