Skip to content

Commit

Permalink
Add Object.freeze, Object.isFrozen and Object.validateIsFrozen.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mhermier committed Mar 16, 2023
1 parent a28ac44 commit 95004fb
Show file tree
Hide file tree
Showing 19 changed files with 159 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/include/wren.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions src/vm/wren_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand All @@ -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.
Expand All @@ -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;
}
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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]);
Expand All @@ -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]);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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]));
Expand Down Expand Up @@ -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]))
Expand All @@ -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]);
Expand All @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/vm/wren_core.wren
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ class MapEntry {
construct new(key, value) {
_key = key
_value = value
freeze()
}

key { _key }
Expand Down
1 change: 1 addition & 0 deletions src/vm/wren_core.wren.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions src/vm/wren_primitive.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/vm/wren_value.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 15 additions & 0 deletions src/vm/wren_value.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ struct sObj
{
ObjType type;
bool isDark;
bool isFrozen;

// The object's class.
ObjClass* classObj;
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions src/vm/wren_vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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() \
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions test/core/bool/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

System.print(false.isFrozen) // expect: true
System.print(true.isFrozen) // expect: true
16 changes: 16 additions & 0 deletions test/core/class/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions test/core/list/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

System.print([].isFrozen) // expect: false
2 changes: 2 additions & 0 deletions test/core/map/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

System.print({}.isFrozen) // expect: false
2 changes: 2 additions & 0 deletions test/core/map_entry/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

System.print(MapEntry.new("key", "value").isFrozen) // expect: true
2 changes: 2 additions & 0 deletions test/core/null/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

System.print(null.isFrozen) // expect: true
5 changes: 5 additions & 0 deletions test/core/number/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions test/core/object/freeze.wren
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions test/core/object/frozen_objects_are_immutable.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

class Foo {
construct new() { freeze() }
mutate() { _value = 42 } // expect runtime error: Object is frozen
}

System.print(Foo.new().mutate())
2 changes: 2 additions & 0 deletions test/core/range/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

System.print((0..1).isFrozen) // expect: true
2 changes: 2 additions & 0 deletions test/core/string/is_frozen.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

System.print("".isFrozen) // expect: true

0 comments on commit 95004fb

Please sign in to comment.