forked from Mirror/wren
Introduce Attributes (#962)
* introduce Attributes for classes and methods
This commit is contained in:
@ -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 →</a>
|
||||
<a href="functions.html">← Functions</a>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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)" }
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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:
|
||||
{
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -410,6 +410,9 @@ struct sObjClass
|
||||
|
||||
// The name of the class.
|
||||
ObjString* name;
|
||||
|
||||
// The ClassAttribute for the class, if any
|
||||
Value attributes;
|
||||
};
|
||||
|
||||
typedef struct
|
||||
|
||||
@ -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);
|
||||
|
||||
21
test/language/class/attributes/attributes.wren
Normal file
21
test/language/class/attributes/attributes.wren
Normal 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
|
||||
9
test/language/class/attributes/compile_only.wren
Normal file
9
test/language/class/attributes/compile_only.wren
Normal 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
|
||||
11
test/language/class/attributes/duplicate_keys.wren
Normal file
11
test/language/class/attributes/duplicate_keys.wren
Normal 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]
|
||||
17
test/language/class/attributes/groups.wren
Normal file
17
test/language/class/attributes/groups.wren
Normal 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]
|
||||
|
||||
12
test/language/class/attributes/invalid_expression.wren
Normal file
12
test/language/class/attributes/invalid_expression.wren
Normal 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
|
||||
}
|
||||
}
|
||||
11
test/language/class/attributes/invalid_scope.wren
Normal file
11
test/language/class/attributes/invalid_scope.wren
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
#valid
|
||||
class Example {
|
||||
|
||||
#valid
|
||||
method() {
|
||||
#invalid // expect error
|
||||
var a = 3
|
||||
}
|
||||
}
|
||||
4
test/language/class/attributes/invalid_toplevel.wren
Normal file
4
test/language/class/attributes/invalid_toplevel.wren
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
#meta // expect error
|
||||
var A = 3
|
||||
20
test/language/class/attributes/literals.wren
Normal file
20
test/language/class/attributes/literals.wren
Normal 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
|
||||
32
test/language/class/attributes/methods.wren
Normal file
32
test/language/class/attributes/methods.wren
Normal 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]}
|
||||
4
test/language/class/attributes/without.wren
Normal file
4
test/language/class/attributes/without.wren
Normal file
@ -0,0 +1,4 @@
|
||||
// With no attributes defined, no ClassAttributes should be allocated
|
||||
|
||||
class Without {}
|
||||
System.print(Without.attributes == null) // expect: true
|
||||
Reference in New Issue
Block a user