1
0
forked from Mirror/wren

Introduce Attributes (#962)

* introduce Attributes for classes and methods
This commit is contained in:
ruby
2021-04-08 21:30:09 -07:00
committed by GitHub
parent 5244a9d001
commit a4ae905384
20 changed files with 713 additions and 11 deletions

View File

@ -635,6 +635,131 @@ class Derived is Base {
}
</pre>
## Attributes
<small>**experimental stage**: subject to minor changes</small>
A class and methods within a class can be tagged with 'meta attributes'.
Like this:
<pre class="snippet">
#hidden = true
class Example {}
</pre>
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.
<small>
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.
</small>
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.
<pre class="snippet">
#key
#key = value
#group(
multiple,
lines = true,
lines = 0
)
class Example {
#test(skip = true, iterations = 32)
doStuff() {}
}
</pre>
### 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:
<pre class="snippet">
#doc = "not runtime data"
#!runtimeAccess = true
#!maxIterations = 16
</pre>
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:
<pre class="snippet">
// 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()
}
</pre>
<br><hr>
<a class="right" href="concurrency.html">Concurrency &rarr;</a>
<a href="functions.html">&larr; Functions</a>

View File

@ -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);
}

View File

@ -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");

View File

@ -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)" }
}

View File

@ -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";

View File

@ -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:
{

View File

@ -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)

View File

@ -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);

View File

@ -410,6 +410,9 @@ struct sObjClass
// The name of the class.
ObjString* name;
// The ClassAttribute for the class, if any
Value attributes;
};
typedef struct

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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]

View File

@ -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
}
}

View File

@ -0,0 +1,11 @@
#valid
class Example {
#valid
method() {
#invalid // expect error
var a = 3
}
}

View File

@ -0,0 +1,4 @@
#meta // expect error
var A = 3

View File

@ -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

View File

@ -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]}

View File

@ -0,0 +1,4 @@
// With no attributes defined, no ClassAttributes should be allocated
class Without {}
System.print(Without.attributes == null) // expect: true