diff --git a/README.md b/README.md index 4d35c36..f5164f5 100644 --- a/README.md +++ b/README.md @@ -1291,6 +1291,41 @@ print(doesANestedThing()) _**Note:** The implementation of `with` blocks is incomplete; exceptions raised from within a `with` that are not caught within the block will cause `__exit__` to not be called._ +### Special Decorators + +The compiler implements special handling when the decorators `@staticmethod` and `@property` are used with methods of a class. + +`@staticmethod` will mark the decorated method as a regular function - it will not receive an implicit "self" and it will be attached to the `fields` table of the class and instances of that class, rather than the `methods` table. + +`@property` will mark the decorated method as a property object. When it is retrieved or assigned to from the class or instance through a dot-accessor (eg. `foo.bar`), the wrapped method will be called intead. + +Properties work differently from in Python: + +```py +class Foo(): + def __init__(self): + self._bar = 0 + @property + def bar(self, *setter): + if setter: + print("Setting bar:", setter[0]) + self._bar = setter[0] + else: + print("Getting bar.") + return self._bar +let f = Foo() +print(f.bar) +f.bar = 42 +print(f.bar) +# → Getting bar. +# 0 +# Setting bar: 42 +# Getting bar. +# 42 +``` + +_**Note:** Special handling when using `del` on a property is not yet implemented._ + ## About the REPL Kuroko's repl provides an interactive environment for executing code and seeing results. diff --git a/src/chunk.h b/src/chunk.h index e9a9185..03d9ef4 100644 --- a/src/chunk.h +++ b/src/chunk.h @@ -83,6 +83,8 @@ typedef enum { OP_INVOKE_DELETE, OP_IMPORT_FROM, + OP_CREATE_PROPERTY, + OP_CONSTANT_LONG = 128, OP_DEFINE_GLOBAL_LONG, OP_GET_GLOBAL_LONG, diff --git a/src/compiler.c b/src/compiler.c index a35b4db..f88a692 100644 --- a/src/compiler.c +++ b/src/compiler.c @@ -92,6 +92,8 @@ typedef enum { TYPE_METHOD, TYPE_INIT, TYPE_LAMBDA, + TYPE_STATIC, + TYPE_PROPERTY, } FunctionType; typedef struct Compiler { @@ -133,6 +135,10 @@ static KrkChunk * currentChunk() { #define EMIT_CONSTANT_OP(opc, arg) do { if (arg < 256) { emitBytes(opc, arg); } \ else { emitBytes(opc ## _LONG, arg >> 16); emitBytes(arg >> 8, arg); } } while (0) +static int isMethod(int type) { + return type == TYPE_METHOD || type == TYPE_INIT || type == TYPE_PROPERTY; +} + static void initCompiler(Compiler * compiler, FunctionType type) { compiler->enclosing = current; current = compiler; @@ -159,7 +165,7 @@ static void initCompiler(Compiler * compiler, FunctionType type) { current->function->name = krk_copyString(parser.previous.start, parser.previous.length); } - if (type == TYPE_INIT || type == TYPE_METHOD) { + if (isMethod(type)) { Local * local = ¤t->locals[current->localCount++]; local->depth = 0; local->isCaptured = 0; @@ -876,7 +882,7 @@ static void function(FunctionType type, size_t blockWidth) { beginScope(); - if (type == TYPE_METHOD || type == TYPE_INIT) current->function->requiredArgs = 1; + if (isMethod(type)) current->function->requiredArgs = 1; int hasCollectors = 0; @@ -885,7 +891,7 @@ static void function(FunctionType type, size_t blockWidth) { if (!check(TOKEN_RIGHT_PAREN)) { do { if (match(TOKEN_SELF)) { - if (type != TYPE_INIT && type != TYPE_METHOD) { + if (!isMethod(type)) { error("Invalid use of `self` as a function paramenter."); } continue; @@ -995,6 +1001,9 @@ static void method(size_t blockWidth) { if (!match(TOKEN_EOL) && !match(TOKEN_EOF)) { errorAtCurrent("Expected end of line after class attribut declaration"); } + } else if (match(TOKEN_PASS)) { + /* bah */ + consume(TOKEN_EOL, "Expected linefeed after 'pass' in class body."); } else { consume(TOKEN_DEF, "expected a definition, got nothing"); consume(TOKEN_IDENTIFIER, "expected method name"); @@ -1134,8 +1143,33 @@ static KrkToken decorator(size_t level, FunctionType type) { size_t blockWidth = (parser.previous.type == TOKEN_INDENTATION) ? parser.previous.length : 0; advance(); /* Collect the `@` */ - /* Collect an identifier */ - expression(); + KrkToken funcName = {0}; + int haveCallable = 0; + + /* hol'up, let's special case some stuff */ + KrkToken at_staticmethod = syntheticToken("staticmethod"); + KrkToken at_property = syntheticToken("property"); + if (identifiersEqual(&at_staticmethod, &parser.current)) { + if (level != 0 || type != TYPE_METHOD) { + error("Invalid use of @staticmethod, which must be the top decorator of a class method."); + return funcName; + } + advance(); + type = TYPE_STATIC; + emitBytes(OP_DUP, 0); /* SET_PROPERTY will pop class */ + } else if (identifiersEqual(&at_property, &parser.current)) { + if (level != 0 || type != TYPE_METHOD) { + error("Invalid use of @property, which must be the top decorator of a class method."); + return funcName; + } + advance(); + type = TYPE_PROPERTY; + emitBytes(OP_DUP, 0); + } else { + /* Collect an identifier */ + expression(); + haveCallable = 1; + } consume(TOKEN_EOL, "Expected line feed after decorator."); if (blockWidth) { @@ -1143,7 +1177,6 @@ static KrkToken decorator(size_t level, FunctionType type) { if (parser.previous.length != blockWidth) error("Expected next line after decorator to have same indentation."); } - KrkToken funcName = {0}; if (check(TOKEN_DEF)) { /* We already checked for block level */ advance(); @@ -1160,7 +1193,8 @@ static KrkToken decorator(size_t level, FunctionType type) { return funcName; } - emitBytes(OP_CALL, 1); + if (haveCallable) + emitBytes(OP_CALL, 1); if (level == 0) { if (type == TYPE_FUNCTION) { @@ -1168,6 +1202,15 @@ static KrkToken decorator(size_t level, FunctionType type) { declareVariable(); size_t ind = (current->scopeDepth > 0) ? 0 : identifierConstant(&funcName); defineVariable(ind); + } else if (type == TYPE_STATIC) { + size_t ind = identifierConstant(&funcName); + EMIT_CONSTANT_OP(OP_SET_PROPERTY, ind); + emitByte(OP_POP); + } else if (type == TYPE_PROPERTY) { + emitByte(OP_CREATE_PROPERTY); + size_t ind = identifierConstant(&funcName); + EMIT_CONSTANT_OP(OP_SET_PROPERTY, ind); + emitByte(OP_POP); } else { size_t ind = identifierConstant(&funcName); EMIT_CONSTANT_OP(OP_METHOD, ind); diff --git a/src/debug.c b/src/debug.c index 483ad30..f4e8eb8 100644 --- a/src/debug.c +++ b/src/debug.c @@ -122,6 +122,7 @@ size_t krk_disassembleInstruction(FILE * f, KrkFunction * func, size_t offset) { SIMPLE(OP_FINALIZE) SIMPLE(OP_IS) SIMPLE(OP_POW) + SIMPLE(OP_CREATE_PROPERTY) OPERANDB(OP_DUP,(void)0) OPERANDB(OP_EXPAND_ARGS,EXPAND_ARGS_MORE) CONSTANT(OP_DEFINE_GLOBAL,(void)0) diff --git a/src/memory.c b/src/memory.c index 70b7d1f..7d5caad 100644 --- a/src/memory.c +++ b/src/memory.c @@ -87,6 +87,10 @@ static void freeObject(KrkObj * object) { FREE(KrkBytes, bytes); break; } + case OBJ_PROPERTY: { + FREE(KrkProperty, object); + break; + } } } @@ -178,6 +182,11 @@ static void blackenObject(KrkObj * object) { markArray(&tuple->values); break; } + case OBJ_PROPERTY: { + KrkProperty * property = (KrkProperty *)object; + krk_markValue(property->method); + break; + } case OBJ_NATIVE: case OBJ_STRING: case OBJ_BYTES: diff --git a/src/object.c b/src/object.c index 4733e5c..89fd6bc 100644 --- a/src/object.c +++ b/src/object.c @@ -248,6 +248,12 @@ KrkUpvalue * krk_newUpvalue(int slot) { return upvalue; } +KrkProperty * krk_newProperty(KrkValue method) { + KrkProperty * property = ALLOCATE_OBJECT(KrkProperty, OBJ_PROPERTY); + property->method = method; + return property; +} + KrkClass * krk_newClass(KrkString * name, KrkClass * baseClass) { KrkClass * _class = ALLOCATE_OBJECT(KrkClass, OBJ_CLASS); _class->name = name; diff --git a/src/object.h b/src/object.h index 0c8f846..a275d04 100644 --- a/src/object.h +++ b/src/object.h @@ -28,6 +28,8 @@ #define IS_TUPLE(value) isObjType(value, OBJ_TUPLE) #define AS_TUPLE(value) ((KrkTuple *)AS_OBJECT(value)) +#define IS_PROPERTY(value) isObjType(value, OBJ_PROPERTY) +#define AS_PROPERTY(value) ((KrkProperty *)AS_OBJECT(value)) typedef enum { OBJ_FUNCTION, @@ -40,6 +42,7 @@ typedef enum { OBJ_BOUND_METHOD, OBJ_TUPLE, OBJ_BYTES, + OBJ_PROPERTY, } ObjType; struct Obj { @@ -173,6 +176,11 @@ typedef struct { KrkValueArray values; } KrkTuple; +typedef struct { + KrkObj obj; + KrkValue method; +} KrkProperty; + typedef struct { KrkInstance inst; KrkValueArray values; @@ -200,6 +208,7 @@ extern KrkClass * krk_newClass(KrkString * name, KrkClass * base); extern KrkInstance * krk_newInstance(KrkClass * _class); extern KrkBoundMethod * krk_newBoundMethod(KrkValue receiver, KrkObj * method); extern KrkTuple * krk_newTuple(size_t length); +extern KrkProperty * krk_newProperty(KrkValue method); extern void * krk_unicodeString(KrkString * string); extern uint32_t krk_unicodeCodepoint(KrkString * string, size_t index); diff --git a/src/vm.c b/src/vm.c index a777303..8379451 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1411,6 +1411,7 @@ KrkValue krk_callSimple(KrkValue value, int argCount, int isMethod) { } else if (result == 1) { return krk_runNext(); } + if (!IS_NONE(vm.currentException)) return NONE_VAL(); return krk_runtimeError(vm.exceptions.typeError, "Invalid internal method call: %d ('%s')", result, krk_typeName(value)); } @@ -4133,6 +4134,10 @@ static int valueGetProperty(KrkString * name) { if (IS_INSTANCE(krk_peek(0))) { KrkInstance * instance = AS_INSTANCE(krk_peek(0)); if (krk_tableGet(&instance->fields, OBJECT_VAL(name), &value)) { + if (IS_PROPERTY(value)) { + krk_push(krk_callSimple(AS_PROPERTY(value)->method, 1, 0)); + return 1; + } krk_pop(); krk_push(value); return 1; @@ -4142,6 +4147,10 @@ static int valueGetProperty(KrkString * name) { KrkClass * _class = AS_CLASS(krk_peek(0)); if (krk_tableGet(&_class->fields, OBJECT_VAL(name), &value) || krk_tableGet(&_class->methods, OBJECT_VAL(name), &value)) { + if (IS_PROPERTY(value)) { + krk_push(krk_callSimple(AS_PROPERTY(value)->method, 1, 0)); + return 1; + } krk_pop(); krk_push(value); return 1; @@ -4584,12 +4593,17 @@ static KrkValue run() { case OP_SET_PROPERTY_LONG: case OP_SET_PROPERTY: { KrkString * name = READ_STRING(operandWidth); - if (IS_INSTANCE(krk_peek(1))) { - KrkInstance * instance = AS_INSTANCE(krk_peek(1)); - krk_tableSet(&instance->fields, OBJECT_VAL(name), krk_peek(0)); - } else if (IS_CLASS(krk_peek(1))) { - KrkClass * _class = AS_CLASS(krk_peek(1)); - krk_tableSet(&_class->fields, OBJECT_VAL(name), krk_peek(0)); + KrkTable * table = NULL; + if (IS_INSTANCE(krk_peek(1))) table = &AS_INSTANCE(krk_peek(1))->fields; + else if (IS_CLASS(krk_peek(1))) table = &AS_CLASS(krk_peek(1))->fields; + if (table) { + KrkValue previous; + if (krk_tableGet(table, OBJECT_VAL(name), &previous) && IS_PROPERTY(previous)) { + krk_push(krk_callSimple(AS_PROPERTY(previous)->method, 2, 0)); + break; + } else { + krk_tableSet(table, OBJECT_VAL(name), krk_peek(0)); + } } else { krk_runtimeError(vm.exceptions.attributeError, "'%s' object has no attribute '%s'", krk_typeName(krk_peek(0)), name->chars); goto _finishException; @@ -4754,6 +4768,12 @@ static KrkValue run() { krk_push(handler); break; } + case OP_CREATE_PROPERTY: { + KrkProperty * newProperty = krk_newProperty(krk_peek(0)); + krk_pop(); + krk_push(OBJECT_VAL(newProperty)); + break; + } } if (likely(!(vm.flags & KRK_HAS_EXCEPTION))) continue; _finishException: diff --git a/test/testClassMethod.krk b/test/testClassMethod.krk index c1688d7..82fb2a7 100644 --- a/test/testClassMethod.krk +++ b/test/testClassMethod.krk @@ -1,14 +1,9 @@ -def staticmethod(func): - def _wrapper(*args,**kwargs): - return func(None,*args,**kwargs) - return _wrapper - class Foo(): def amethod(): print("If this were Python, I'd be impossible to call.") @staticmethod def astatic(): - print("Since `None` is always implicit, it's been set by @staticmethod here to None:", self) + print("these are special now") # This doesn't work because amethod is unbound and needs an instance. try: diff --git a/test/testClassMethod.krk.expect b/test/testClassMethod.krk.expect index cf4b4e3..c5dcb5d 100644 --- a/test/testClassMethod.krk.expect +++ b/test/testClassMethod.krk.expect @@ -1,3 +1,3 @@ amethod() takes exactly 1 argument (0 given) If this were Python, I'd be impossible to call. -Since `None` is always implicit, it's been set by @staticmethod here to None: None +these are special now diff --git a/test/testSpecialDecorators.krk b/test/testSpecialDecorators.krk new file mode 100644 index 0000000..9f51270 --- /dev/null +++ b/test/testSpecialDecorators.krk @@ -0,0 +1,39 @@ + +def classmethod(func): + def bindClassMethod(callee, *args): + if args: raise ValueError("can not reassign class method") + let cls = callee if isinstance(callee, type) else callee.__class__ + def _bound(*args, **kwargs): + return func(None, cls, *args, **kwargs) + return _bound + return bindClassMethod + +class Foo(object): + myBar = 42 + @staticmethod + def foo(): + print("No args!") + @property + def bar(*setter): + if setter: + print("Called as a setter:", setter) + self.myBar = setter[0] + return self.myBar + @property + @classmethod + def fromString(cls, string): + print(cls, string) + +class Bar(Foo): + +Foo.foo() +print(Foo().bar) +let f = Foo() +f.myBar = 48 +print(f.bar) +f.bar = 102 +print(f.bar) + +Foo.fromString("test") +f.fromString("test") +Bar.fromString("test") diff --git a/test/testSpecialDecorators.krk.expect b/test/testSpecialDecorators.krk.expect new file mode 100644 index 0000000..8438225 --- /dev/null +++ b/test/testSpecialDecorators.krk.expect @@ -0,0 +1,8 @@ +No args! +42 +48 +Called as a setter: [102] +102 + test + test + test