diff --git a/README.md b/README.md index b468dd2..53a4ff6 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,22 @@ o.printFoo() _**Note:** As `self` is implicit, it can not be renamed; other argument names listed in a method signature will refer to additional arguments._ +Classes can also define fields, which can be accessed from the class or through an instance. + +```py +class Foo(): + bar = "baz" + def printBar(self): + print(self.bar) +let o = Foo() +o.printBar() +# → baz +print(Foo.bar) +# → baz +``` + +_**Note:** Instances receive a copy of their class's fields upon creation. If a class field is mutable, the instance's copy will refer to the same underlying object. Assignments to the instance's copy of a field will refer to a new object. If a new field is added to a class after instances have been created, the existing instances will not be able to reference the new field._ + When a class is instantiated, if it has an `__init__` method it will be called automatically. `__init__` may take arguments as well. ```py diff --git a/compiler.c b/compiler.c index bfb53dd..6beb43d 100644 --- a/compiler.c +++ b/compiler.c @@ -862,6 +862,16 @@ static void method(size_t blockWidth) { * as a redundant thing, just to make more Python stuff work with changes. */ if (check(TOKEN_AT)) { decorator(0, TYPE_METHOD); + } else if (match(TOKEN_IDENTIFIER)) { + emitBytes(OP_DUP, 0); /* SET_PROPERTY will pop class */ + size_t ind = identifierConstant(&parser.previous); + consume(TOKEN_EQUAL, "Class field must have value."); + expression(); + EMIT_CONSTANT_OP(OP_SET_PROPERTY, ind); + emitByte(OP_POP); /* Value of expression replaces dup of class*/ + if (!match(TOKEN_EOL) && !match(TOKEN_EOF)) { + errorAtCurrent("Expected end of line after class attribut declaration"); + } } else { consume(TOKEN_DEF, "expected a definition, got nothing"); consume(TOKEN_IDENTIFIER, "expected method name"); diff --git a/object.c b/object.c index 96a8000..da107f3 100644 --- a/object.c +++ b/object.c @@ -114,6 +114,7 @@ KrkClass * krk_newClass(KrkString * name) { _class->docstring = NULL; _class->base = NULL; krk_initTable(&_class->methods); + krk_initTable(&_class->fields); _class->_getter = NULL; _class->_setter = NULL; @@ -131,6 +132,7 @@ KrkInstance * krk_newInstance(KrkClass * _class) { KrkInstance * instance = ALLOCATE_OBJECT(KrkInstance, OBJ_INSTANCE); instance->_class = _class; krk_initTable(&instance->fields); + krk_tableAddAll(&_class->fields, &instance->fields); instance->_internal = NULL; /* To be used by C-defined types to track internal objects. */ return instance; } diff --git a/object.h b/object.h index 829c0a3..8be9bc6 100644 --- a/object.h +++ b/object.h @@ -100,6 +100,7 @@ typedef struct KrkClass { KrkString * docstring; struct KrkClass * base; KrkTable methods; + KrkTable fields; /* Quick access for common stuff */ KrkObj * _getter; diff --git a/src/fileio.c b/src/fileio.c index d19fd69..b1985e6 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -288,6 +288,7 @@ KrkValue krk_module_onload_fileio(void) { krk_attachNamedObject(&module->fields,chr_File,(KrkObj*)FileClass); /* Inherit from object */ krk_tableAddAll(&vm.objectClass->methods, &FileClass->methods); + krk_tableAddAll(&vm.objectClass->fields, &FileClass->fields); krk_pop(); krk_pop(); diff --git a/src/os.c b/src/os.c index 6d8a381..38e201d 100644 --- a/src/os.c +++ b/src/os.c @@ -56,6 +56,7 @@ static void _loadEnviron(KrkInstance * module) { KrkValue dictClass; krk_tableGet(&vm.builtins->fields,OBJECT_VAL(S("dict")), &dictClass); krk_tableAddAll(&AS_CLASS(dictClass)->methods, &environClass->methods); + krk_tableAddAll(&AS_CLASS(dictClass)->fields, &environClass->fields); environClass->base = AS_CLASS(dictClass); /* Add our set method that should also call dict's set method */ diff --git a/test/testClassFields.krk b/test/testClassFields.krk new file mode 100644 index 0000000..4ade538 --- /dev/null +++ b/test/testClassFields.krk @@ -0,0 +1,27 @@ +class Foo(): + a = None + b = 42 + c = "string" + l = [] + def test(self): + return (self.a,self.b,self.c,self.l) + +class Bar(Foo): + c = 96 + +let f = Foo() +let b = Foo() +def testAll(): + print(Foo.test(Foo), f.test(), b.test(), Bar.test(Bar)) + +testAll() +f.a = 42 +testAll() +f.l.append("hi") +testAll() +f.l = [] +testAll() +f.l.append("bacon") +testAll() +b.b = "derp" +testAll() diff --git a/test/testClassFields.krk.expect b/test/testClassFields.krk.expect new file mode 100644 index 0000000..5a8f6b3 --- /dev/null +++ b/test/testClassFields.krk.expect @@ -0,0 +1,6 @@ +(None, 42, 'string', []) (None, 42, 'string', []) (None, 42, 'string', []) (None, 42, 96, []) +(None, 42, 'string', []) (42, 42, 'string', []) (None, 42, 'string', []) (None, 42, 96, []) +(None, 42, 'string', ['hi']) (42, 42, 'string', ['hi']) (None, 42, 'string', ['hi']) (None, 42, 96, ['hi']) +(None, 42, 'string', ['hi']) (42, 42, 'string', []) (None, 42, 'string', ['hi']) (None, 42, 96, ['hi']) +(None, 42, 'string', ['hi']) (42, 42, 'string', ['bacon']) (None, 42, 'string', ['hi']) (None, 42, 96, ['hi']) +(None, 42, 'string', ['hi']) (42, 42, 'string', ['bacon']) (None, 'derp', 'string', ['hi']) (None, 42, 96, ['hi']) diff --git a/test/testClassMethod.krk b/test/testClassMethod.krk new file mode 100644 index 0000000..c1688d7 --- /dev/null +++ b/test/testClassMethod.krk @@ -0,0 +1,23 @@ +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) + +# This doesn't work because amethod is unbound and needs an instance. +try: + Foo.amethod() +except: + print(exception.arg) +# This works +Foo.amethod(Foo()) +# This should too? +Foo.astatic() + + diff --git a/test/testClassMethod.krk.expect b/test/testClassMethod.krk.expect new file mode 100644 index 0000000..cf4b4e3 --- /dev/null +++ b/test/testClassMethod.krk.expect @@ -0,0 +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 diff --git a/vm.c b/vm.c index 43c4b9b..0d61e68 100644 --- a/vm.c +++ b/vm.c @@ -733,6 +733,21 @@ KrkValue krk_dirObject(int argc, KrkValue argv[]) { } } } else { + if (IS_CLASS(argv[0])) { + KrkClass * _class = AS_CLASS(argv[0]); + for (size_t i = 0; i < _class->methods.capacity; ++i) { + if (_class->methods.entries[i].key.type != VAL_KWARGS) { + krk_writeValueArray(AS_LIST(_list_internal), + _class->methods.entries[i].key); + } + } + for (size_t i = 0; i < _class->fields.capacity; ++i) { + if (_class->fields.entries[i].key.type != VAL_KWARGS) { + krk_writeValueArray(AS_LIST(_list_internal), + _class->fields.entries[i].key); + } + } + } KrkClass * type = AS_CLASS(krk_typeOf(1, (KrkValue[]){argv[0]})); for (size_t i = 0; i < type->methods.capacity; ++i) { @@ -1368,6 +1383,7 @@ static KrkValue _string_init(int argc, KrkValue argv[]) { krk_attachNamedObject(&vm.builtins->fields, name, (KrkObj*)obj); \ obj->base = baseClass; \ krk_tableAddAll(&baseClass->methods, &obj->methods); \ + krk_tableAddAll(&baseClass->fields, &obj->fields); \ } while (0) #define ADD_EXCEPTION_CLASS(obj, name, baseClass) do { \ @@ -1375,6 +1391,7 @@ static KrkValue _string_init(int argc, KrkValue argv[]) { krk_attachNamedObject(&vm.builtins->fields, name, (KrkObj*)obj); \ obj->base = baseClass; \ krk_tableAddAll(&baseClass->methods, &obj->methods); \ + krk_tableAddAll(&baseClass->fields, &obj->fields); \ } while (0) #define BUILTIN_FUNCTION(name, func) do { \ @@ -2680,6 +2697,7 @@ void krk_initVM(int flags) { vm.moduleClass = krk_newClass(S("module")); vm.moduleClass->base = vm.objectClass; krk_tableAddAll(&vm.objectClass->methods, &vm.moduleClass->methods); + krk_tableAddAll(&vm.objectClass->fields, &vm.moduleClass->fields); /* Attach new repr/str */ krk_defineNative(&vm.moduleClass->methods, ".__repr__", _module_repr); @@ -3240,15 +3258,24 @@ int krk_loadModule(KrkString * name, KrkValue * moduleOut) { */ static int valueGetProperty(KrkString * name) { KrkClass * objectClass; + KrkValue value; if (IS_INSTANCE(krk_peek(0))) { KrkInstance * instance = AS_INSTANCE(krk_peek(0)); - KrkValue value; if (krk_tableGet(&instance->fields, OBJECT_VAL(name), &value)) { krk_pop(); krk_push(value); return 1; } objectClass = instance->_class; + } else if (IS_CLASS(krk_peek(0))) { + KrkClass * _class = AS_CLASS(krk_peek(0)); + if (krk_tableGet(&_class->fields, OBJECT_VAL(name), &value) || + krk_tableGet(&_class->methods, OBJECT_VAL(name), &value)) { + krk_pop(); + krk_push(value); + return 1; + } + objectClass = AS_CLASS(krk_typeOf(1, (KrkValue[]){krk_peek(0)})); } else { objectClass = AS_CLASS(krk_typeOf(1, (KrkValue[]){krk_peek(0)})); } @@ -3570,6 +3597,7 @@ static KrkValue run() { _class->filename = frame->closure->function->chunk.filename; _class->base = vm.objectClass; krk_tableAddAll(&vm.objectClass->methods, &_class->methods); + krk_tableAddAll(&vm.objectClass->fields, &_class->fields); break; } case OP_GET_PROPERTY_LONG: @@ -3615,15 +3643,18 @@ static KrkValue run() { case OP_SET_PROPERTY_LONG: case OP_SET_PROPERTY: { KrkString * name = READ_STRING(operandWidth); - if (!IS_INSTANCE(krk_peek(1))) { + 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)); + } else { krk_runtimeError(vm.exceptions.attributeError, "'%s' object has no attribute '%s'", krk_typeName(krk_peek(0)), name->chars); goto _finishException; } - KrkInstance * instance = AS_INSTANCE(krk_peek(1)); - krk_tableSet(&instance->fields, OBJECT_VAL(name), krk_peek(0)); - KrkValue value = krk_pop(); - krk_pop(); /* instance */ - krk_push(value); /* Moves value in */ + krk_swap(1); + krk_pop(); break; } case OP_METHOD_LONG: @@ -3651,6 +3682,7 @@ static KrkValue run() { KrkClass * subclass = AS_CLASS(krk_peek(0)); subclass->base = AS_CLASS(superclass); krk_tableAddAll(&AS_CLASS(superclass)->methods, &subclass->methods); + krk_tableAddAll(&AS_CLASS(superclass)->fields, &subclass->fields); krk_pop(); break; }