From 95004fbea79207c3a7f9007e2e9ab0b3564f9d76 Mon Sep 17 00:00:00 2001 From: Michel Hermier Date: Thu, 16 Mar 2023 14:52:39 +0100 Subject: [PATCH] Add `Object.freeze`, `Object.isFrozen` and `Object.validateIsFrozen`. Wren lacks the ability to declare variable constants. The foolwing patch allow to make objects immutable, giving a similar functionnality. NOTE: The implemetation while functionnal for `ObjectInstance` is opt-in for foreign and primitive methods. --- src/include/wren.h | 6 +++ src/vm/wren_core.c | 43 +++++++++++++++++++ src/vm/wren_core.wren | 1 + src/vm/wren_core.wren.inc | 1 + src/vm/wren_primitive.h | 11 +++++ src/vm/wren_value.c | 3 ++ src/vm/wren_value.h | 15 +++++++ src/vm/wren_vm.c | 19 ++++++++ test/core/bool/is_frozen.wren | 3 ++ test/core/class/is_frozen.wren | 16 +++++++ test/core/list/is_frozen.wren | 2 + test/core/map/is_frozen.wren | 2 + test/core/map_entry/is_frozen.wren | 2 + test/core/null/is_frozen.wren | 2 + test/core/number/is_frozen.wren | 5 +++ test/core/object/freeze.wren | 17 ++++++++ .../object/frozen_objects_are_immutable.wren | 7 +++ test/core/range/is_frozen.wren | 2 + test/core/string/is_frozen.wren | 2 + 19 files changed, 159 insertions(+) create mode 100644 test/core/bool/is_frozen.wren create mode 100644 test/core/class/is_frozen.wren create mode 100644 test/core/list/is_frozen.wren create mode 100644 test/core/map/is_frozen.wren create mode 100644 test/core/map_entry/is_frozen.wren create mode 100644 test/core/null/is_frozen.wren create mode 100644 test/core/number/is_frozen.wren create mode 100644 test/core/object/freeze.wren create mode 100644 test/core/object/frozen_objects_are_immutable.wren create mode 100644 test/core/range/is_frozen.wren create mode 100644 test/core/string/is_frozen.wren diff --git a/src/include/wren.h b/src/include/wren.h index 7845911ce..7e22c551c 100644 --- a/src/include/wren.h +++ b/src/include/wren.h @@ -401,6 +401,12 @@ WREN_API void wrenEnsureSlots(WrenVM* vm, int numSlots); // Gets the type of the object in [slot]. WREN_API WrenType wrenGetSlotType(WrenVM* vm, int slot); +// Try to freeze the object in [slot]. +WREN_API bool wrenFreezeSlot(WrenVM* vm, int slot, bool isFrozen); + +// Test if the object in [slot] is frozen. +WREN_API bool wrenIsFrozenSlot(WrenVM* vm, int slot); + // Reads a boolean value from [slot]. // // It is an error to call this if the slot does not contain a boolean value. diff --git a/src/vm/wren_core.c b/src/vm/wren_core.c index d0a121f8c..d49cd40e6 100644 --- a/src/vm/wren_core.c +++ b/src/vm/wren_core.c @@ -321,6 +321,8 @@ DEF_PRIMITIVE(list_new) DEF_PRIMITIVE(list_add) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + wrenValueBufferWrite(vm, &AS_LIST(args[0])->elements, args[1]); RETURN_VAL(args[1]); } @@ -330,6 +332,8 @@ DEF_PRIMITIVE(list_add) // minimize stack churn. DEF_PRIMITIVE(list_addCore) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + wrenValueBufferWrite(vm, &AS_LIST(args[0])->elements, args[1]); // Return the list. @@ -338,6 +342,8 @@ DEF_PRIMITIVE(list_addCore) DEF_PRIMITIVE(list_clear) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + wrenValueBufferClear(vm, &AS_LIST(args[0])->elements); RETURN_NULL; } @@ -349,6 +355,8 @@ DEF_PRIMITIVE(list_count) DEF_PRIMITIVE(list_insert) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + ObjList* list = AS_LIST(args[0]); // count + 1 here so you can "insert" at the very end. @@ -392,6 +400,8 @@ DEF_PRIMITIVE(list_iteratorValue) DEF_PRIMITIVE(list_removeAt) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + ObjList* list = AS_LIST(args[0]); uint32_t index = validateIndex(vm, args[1], list->elements.count, "Index"); if (index == UINT32_MAX) return false; @@ -400,6 +410,8 @@ DEF_PRIMITIVE(list_removeAt) } DEF_PRIMITIVE(list_removeValue) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + ObjList* list = AS_LIST(args[0]); int index = wrenListIndexOf(vm, list, args[1]); if(index == -1) RETURN_NULL; @@ -414,6 +426,8 @@ DEF_PRIMITIVE(list_indexOf) DEF_PRIMITIVE(list_swap) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + ObjList* list = AS_LIST(args[0]); uint32_t indexA = validateIndex(vm, args[1], list->elements.count, "Index 0"); if (indexA == UINT32_MAX) return false; @@ -461,6 +475,8 @@ DEF_PRIMITIVE(list_subscript) DEF_PRIMITIVE(list_subscriptSetter) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + ObjList* list = AS_LIST(args[0]); uint32_t index = validateIndex(vm, args[1], list->elements.count, "Subscript"); @@ -488,6 +504,8 @@ DEF_PRIMITIVE(map_subscript) DEF_PRIMITIVE(map_subscriptSetter) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + if (!validateKey(vm, args[1])) return false; wrenMapSet(vm, AS_MAP(args[0]), args[1], args[2]); @@ -499,6 +517,8 @@ DEF_PRIMITIVE(map_subscriptSetter) // minimize stack churn. DEF_PRIMITIVE(map_addCore) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + if (!validateKey(vm, args[1])) return false; wrenMapSet(vm, AS_MAP(args[0]), args[1], args[2]); @@ -509,6 +529,8 @@ DEF_PRIMITIVE(map_addCore) DEF_PRIMITIVE(map_clear) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + wrenMapClear(vm, AS_MAP(args[0])); RETURN_NULL; } @@ -560,6 +582,8 @@ DEF_PRIMITIVE(map_iterate) DEF_PRIMITIVE(map_remove) { + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + if (!validateKey(vm, args[1])) return false; RETURN_VAL(wrenMapRemoveKey(vm, AS_MAP(args[0]), args[1])); @@ -863,6 +887,11 @@ DEF_PRIMITIVE(object_bangeq) RETURN_BOOL(!wrenValuesEqual(args[0], args[1])); } +DEF_PRIMITIVE(object_freeze) +{ + RETURN_VAL(wrenFreeze(args[0], true)); +} + DEF_PRIMITIVE(object_is) { if (!IS_CLASS(args[1])) @@ -885,6 +914,11 @@ DEF_PRIMITIVE(object_is) RETURN_BOOL(false); } +DEF_PRIMITIVE(object_isFrozen) +{ + RETURN_BOOL(wrenIsFrozen(args[0])); +} + DEF_PRIMITIVE(object_toString) { Obj* obj = AS_OBJ(args[0]); @@ -897,6 +931,12 @@ DEF_PRIMITIVE(object_type) RETURN_OBJ(wrenGetClass(vm, args[0])); } +DEF_PRIMITIVE(object_validateIsNotFrozen) +{ + VALIDATE_VALUE_IS_NOT_FROZEN(args[0]); + RETURN_VAL(args[0]); +} + DEF_PRIMITIVE(range_from) { RETURN_NUM(AS_RANGE(args[0])->from); @@ -1247,9 +1287,12 @@ void wrenInitializeCore(WrenVM* vm) PRIMITIVE(vm->objectClass, "!", object_not); PRIMITIVE(vm->objectClass, "==(_)", object_eqeq); PRIMITIVE(vm->objectClass, "!=(_)", object_bangeq); + PRIMITIVE(vm->objectClass, "freeze()", object_freeze); PRIMITIVE(vm->objectClass, "is(_)", object_is); + PRIMITIVE(vm->objectClass, "isFrozen", object_isFrozen); PRIMITIVE(vm->objectClass, "toString", object_toString); PRIMITIVE(vm->objectClass, "type", object_type); + PRIMITIVE(vm->objectClass, "validateIsNotFrozen()", object_validateIsNotFrozen); // Now we can define Class, which is a subclass of Object. vm->classClass = defineClass(vm, coreModule, "Class"); diff --git a/src/vm/wren_core.wren b/src/vm/wren_core.wren index f073062c2..998d45ecc 100644 --- a/src/vm/wren_core.wren +++ b/src/vm/wren_core.wren @@ -409,6 +409,7 @@ class MapEntry { construct new(key, value) { _key = key _value = value + freeze() } key { _key } diff --git a/src/vm/wren_core.wren.inc b/src/vm/wren_core.wren.inc index be296cdf7..d785f7193 100644 --- a/src/vm/wren_core.wren.inc +++ b/src/vm/wren_core.wren.inc @@ -411,6 +411,7 @@ static const char* coreModuleSource = " construct new(key, value) {\n" " _key = key\n" " _value = value\n" +" freeze()\n" " }\n" "\n" " key { _key }\n" diff --git a/src/vm/wren_primitive.h b/src/vm/wren_primitive.h index eb55fe840..d32fad5f1 100644 --- a/src/vm/wren_primitive.h +++ b/src/vm/wren_primitive.h @@ -63,6 +63,17 @@ return false; \ } while (false) +#define ERROR_MSG_OBJECT_IS_FROZEN "Object is frozen" + +#define VALIDATE_VALUE_IS_NOT_FROZEN(value) \ + do \ + { \ + if (wrenIsFrozen(value)) \ + { \ + RETURN_ERROR(ERROR_MSG_OBJECT_IS_FROZEN); \ + } \ + } while (false) + // Validates that the given [arg] is a function. Returns true if it is. If not, // reports an error and returns false. bool validateFn(WrenVM* vm, Value arg, const char* argName); diff --git a/src/vm/wren_value.c b/src/vm/wren_value.c index c49a3b6be..d0899e914 100644 --- a/src/vm/wren_value.c +++ b/src/vm/wren_value.c @@ -38,6 +38,7 @@ static void initObj(WrenVM* vm, Obj* obj, ObjType type, ObjClass* classObj) { obj->type = type; obj->isDark = false; + obj->isFrozen = false; obj->classObj = classObj; obj->next = vm->first; vm->first = obj; @@ -664,6 +665,7 @@ Value wrenNewRange(WrenVM* vm, double from, double to, bool isInclusive) { ObjRange* range = ALLOCATE(vm, ObjRange); initObj(vm, &range->obj, OBJ_RANGE, vm->rangeClass); + range->obj.isFrozen = true; range->from = from; range->to = to; range->isInclusive = isInclusive; @@ -716,6 +718,7 @@ Value wrenNewStringLength(WrenVM* vm, const char* text, size_t length) ASSERT(length == 0 || text != NULL, "Unexpected NULL string."); ObjString* string = allocateString(vm, length); + string->obj.isFrozen = true; // Copy the string (if given one). if (length > 0 && text != NULL) memcpy(string->value, text, length); diff --git a/src/vm/wren_value.h b/src/vm/wren_value.h index 2ca0cfdcd..1511957d3 100644 --- a/src/vm/wren_value.h +++ b/src/vm/wren_value.h @@ -110,6 +110,7 @@ struct sObj { ObjType type; bool isDark; + bool isFrozen; // The object's class. ObjClass* classObj; @@ -829,6 +830,20 @@ static inline bool wrenIsBool(Value value) #endif } +static inline Value wrenFreeze(Value value, bool isFrozen) +{ + if (IS_OBJ(value)) AS_OBJ(value)->isFrozen = isFrozen; + + return value; +} + +static inline bool wrenIsFrozen(Value value) +{ + if (IS_OBJ(value)) return AS_OBJ(value)->isFrozen; + + return true; +} + // Returns true if [value] is an object of type [type]. Do not call this // directly, instead use the [IS___] macro for the type in question. static inline bool wrenIsObjType(Value value, ObjType type) diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index 96c570a14..9bcd4c196 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -885,6 +885,9 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) } \ } while (false) + #define ERROR_ON_VALUE_IS_NOT_FROZEN(value) \ + ERROR_ON(wrenIsFrozen(value), ERROR_MSG_OBJECT_IS_FROZEN) + #if WREN_DEBUG_TRACE_INSTRUCTIONS // Prints the stack and instruction before each instruction is executed. #define DEBUG_TRACE_INSTRUCTIONS() \ @@ -1128,6 +1131,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) uint8_t field = READ_BYTE(); Value receiver = stackStart[0]; ASSERT(IS_INSTANCE(receiver), "Receiver should be instance."); + ERROR_ON_VALUE_IS_NOT_FROZEN(receiver); ObjInstance* instance = AS_INSTANCE(receiver); ASSERT(field < instance->obj.classObj->numFields, "Out of bounds field."); instance->fields[field] = PEEK(); @@ -1150,6 +1154,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) uint8_t field = READ_BYTE(); Value receiver = POP(); ASSERT(IS_INSTANCE(receiver), "Receiver should be instance."); + ERROR_ON_VALUE_IS_NOT_FROZEN(receiver); ObjInstance* instance = AS_INSTANCE(receiver); ASSERT(field < instance->obj.classObj->numFields, "Out of bounds field."); instance->fields[field] = PEEK(); @@ -1683,6 +1688,20 @@ WrenType wrenGetSlotType(WrenVM* vm, int slot) return WREN_TYPE_UNKNOWN; } +bool wrenFreezeSlot(WrenVM* vm, int slot, bool isFrozen) +{ + validateApiSlot(vm, slot); + + return wrenIsFrozen(wrenFreeze(vm->apiStack[slot], isFrozen)); +} + +WREN_API bool wrenIsFrozenSlot(WrenVM* vm, int slot) +{ + validateApiSlot(vm, slot); + + return wrenIsFrozen(vm->apiStack[slot]); +} + bool wrenGetSlotBool(WrenVM* vm, int slot) { validateApiSlot(vm, slot); diff --git a/test/core/bool/is_frozen.wren b/test/core/bool/is_frozen.wren new file mode 100644 index 000000000..f55fe7e60 --- /dev/null +++ b/test/core/bool/is_frozen.wren @@ -0,0 +1,3 @@ + +System.print(false.isFrozen) // expect: true +System.print(true.isFrozen) // expect: true diff --git a/test/core/class/is_frozen.wren b/test/core/class/is_frozen.wren new file mode 100644 index 000000000..a97de1c5e --- /dev/null +++ b/test/core/class/is_frozen.wren @@ -0,0 +1,16 @@ + +class Foo { +} + +// Check core bootstrap classes +System.print(Class.isFrozen) // expect: false +System.print(Object.isFrozen) // expect: false +System.print(Object.type.isFrozen) // expect: false + +// Check a core class +System.print(List.isFrozen) // expect: false +System.print(List.type.isFrozen) // expect: false + +// Check a user defined class +System.print(Foo.isFrozen) // expect: false +System.print(Foo.type.isFrozen) // expect: false diff --git a/test/core/list/is_frozen.wren b/test/core/list/is_frozen.wren new file mode 100644 index 000000000..e89c94ebe --- /dev/null +++ b/test/core/list/is_frozen.wren @@ -0,0 +1,2 @@ + +System.print([].isFrozen) // expect: false diff --git a/test/core/map/is_frozen.wren b/test/core/map/is_frozen.wren new file mode 100644 index 000000000..53b03723f --- /dev/null +++ b/test/core/map/is_frozen.wren @@ -0,0 +1,2 @@ + +System.print({}.isFrozen) // expect: false diff --git a/test/core/map_entry/is_frozen.wren b/test/core/map_entry/is_frozen.wren new file mode 100644 index 000000000..3ad3baff7 --- /dev/null +++ b/test/core/map_entry/is_frozen.wren @@ -0,0 +1,2 @@ + +System.print(MapEntry.new("key", "value").isFrozen) // expect: true diff --git a/test/core/null/is_frozen.wren b/test/core/null/is_frozen.wren new file mode 100644 index 000000000..c60c13603 --- /dev/null +++ b/test/core/null/is_frozen.wren @@ -0,0 +1,2 @@ + +System.print(null.isFrozen) // expect: true diff --git a/test/core/number/is_frozen.wren b/test/core/number/is_frozen.wren new file mode 100644 index 000000000..2be0e0759 --- /dev/null +++ b/test/core/number/is_frozen.wren @@ -0,0 +1,5 @@ + +System.print(0.0.isFrozen) // expect: true +System.print(100.isFrozen) // expect: true +System.print(Num.infinity.isFrozen) // expect: true +System.print(Num.nan.isFrozen) // expect: true diff --git a/test/core/object/freeze.wren b/test/core/object/freeze.wren new file mode 100644 index 000000000..5809470e5 --- /dev/null +++ b/test/core/object/freeze.wren @@ -0,0 +1,17 @@ + +class Foo { + construct new() { } +} + +var foo = Foo.new() + +System.print(foo.isFrozen) // expect: false + +// Check `freeze()` mark the object frozen and return itself +System.print(Object.same(foo.freeze(), foo)) // expect: true + +System.print(foo.isFrozen) // expect: true + +// Check `freeze` can be repeated without error +foo.freeze() +System.print(foo.isFrozen) // expect: true diff --git a/test/core/object/frozen_objects_are_immutable.wren b/test/core/object/frozen_objects_are_immutable.wren new file mode 100644 index 000000000..739c556e9 --- /dev/null +++ b/test/core/object/frozen_objects_are_immutable.wren @@ -0,0 +1,7 @@ + +class Foo { + construct new() { freeze() } + mutate() { _value = 42 } // expect runtime error: Object is frozen +} + +System.print(Foo.new().mutate()) diff --git a/test/core/range/is_frozen.wren b/test/core/range/is_frozen.wren new file mode 100644 index 000000000..86a3fdf20 --- /dev/null +++ b/test/core/range/is_frozen.wren @@ -0,0 +1,2 @@ + +System.print((0..1).isFrozen) // expect: true diff --git a/test/core/string/is_frozen.wren b/test/core/string/is_frozen.wren new file mode 100644 index 000000000..42fbc2d01 --- /dev/null +++ b/test/core/string/is_frozen.wren @@ -0,0 +1,2 @@ + +System.print("".isFrozen) // expect: true