diff --git a/sdk/angelscript/include/angelscript.h b/sdk/angelscript/include/angelscript.h
index c76fd185e..fbd340e9d 100644
--- a/sdk/angelscript/include/angelscript.h
+++ b/sdk/angelscript/include/angelscript.h
@@ -191,6 +191,7 @@ enum asEEngineProp
asEP_MEMBER_INIT_MODE = 38,
asEP_BOOL_CONVERSION_MODE = 39,
asEP_FOREACH_SUPPORT = 40,
+ asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS = 41,
asEP_LAST_PROPERTY
};
diff --git a/sdk/angelscript/source/as_bytecode.cpp b/sdk/angelscript/source/as_bytecode.cpp
index c0be0392a..d15789c95 100644
--- a/sdk/angelscript/source/as_bytecode.cpp
+++ b/sdk/angelscript/source/as_bytecode.cpp
@@ -422,7 +422,11 @@ bool asCByteCode::RemoveUnusedValue(asCByteInstruction *curr, asCByteInstruction
// NOT, BNOT, IncV, DecV, NEG, iTOf (and all other type casts)
// The value isn't used for anything
- if( curr->op != asBC_FREE && // Can't remove the FREE instruction
+ if( curr->op != asBC_FREE && // Can't remove the FREE instruction
+ curr->op != asBC_ClrVPtr && // Can't remove the ClrVPtr instruction either: clearing a variable
+ // can matter to a later REFCPY/RefCpyV targeting it (it condition-
+ // ally releases whatever it finds there), even though that isn't
+ // visible to this analysis as a "read" of the variable
(asBCInfo[curr->op].type == asBCTYPE_wW_rW_rW_ARG ||
asBCInfo[curr->op].type == asBCTYPE_wW_rW_ARG ||
asBCInfo[curr->op].type == asBCTYPE_wW_rW_DW_ARG ||
diff --git a/sdk/angelscript/source/as_compiler.cpp b/sdk/angelscript/source/as_compiler.cpp
index 246fc8800..ebf2c1e52 100644
--- a/sdk/angelscript/source/as_compiler.cpp
+++ b/sdk/angelscript/source/as_compiler.cpp
@@ -13895,8 +13895,7 @@ int asCCompiler::CompileExpressionPreOp(asCScriptNode *node, asCExprContext *ctx
}
if( ctx->property_get || ctx->property_set )
{
- Error(TXT_INVALID_REF_PROP_ACCESS, node);
- return -1;
+ return ProcessPropertyIncDecAccessor(ctx, (eTokenType)op, false, node);
}
if( !ctx->type.isLValue )
{
@@ -14382,10 +14381,11 @@ int asCCompiler::ProcessPropertyGetSetAccessor(asCExprContext *ctx, asCExprConte
return -1;
}
- // Property accessors on value types (or scoped references types) are not supported since
- // it is not possible to guarantee that the object will stay alive between the two calls
+ // Property accessors on value types (or scoped references types) are not supported by default
+ // since it is not possible to guarantee that the object will stay alive between the two calls.
+ // The application may opt-in to accept this risk via asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS.
asCScriptFunction *func = engine->scriptFunctions[lctx->property_set];
- if( func->objectType && (func->objectType->flags & (asOBJ_VALUE | asOBJ_SCOPED)) )
+ if( func->objectType && (func->objectType->flags & (asOBJ_VALUE | asOBJ_SCOPED)) && !engine->ep.enableValueTypedCompoundPropertyAccessors )
{
// Process the property to free the memory
ProcessPropertySetAccessor(lctx, rctx, errNode);
@@ -14425,7 +14425,20 @@ int asCCompiler::ProcessPropertyGetSetAccessor(asCExprContext *ctx, asCExprConte
}
asCExprContext before(engine);
- if( func->objectType && (func->objectType->flags & (asOBJ_REF|asOBJ_SCOPED)) == asOBJ_REF )
+ bool takeExtraRef = func->objectType && (func->objectType->flags & (asOBJ_REF|asOBJ_SCOPED)) == asOBJ_REF;
+ // When compound assignment on value/scoped types has been allowed by the application (see above),
+ // the object cannot be given a protective extra reference the way ordinary reference types are
+ // below: value types have no refcounting at all, and scoped types don't permit taking a second
+ // owning reference (no AddRef). Instead the already computed address is copied into a non-owning
+ // local reference variable purely so it can be reused for the set call; this carries exactly the
+ // same risk as the script writing "obj.x = obj.x + 1" as two separate statements. Note that since
+ // this variable never owns what it points to, it must be explicitly cleared (see below) rather
+ // than released when done with it - and for scoped types (unlike value types) REFCPY's conditional
+ // release of the destination's *previous* content means that clear is mandatory, not optional: an
+ // uncleared, reused slot would otherwise be mistaken for a previously (validly) held reference and
+ // released out from under its real owner.
+ bool materializeUnsafeRef = !takeExtraRef && func->objectType && (func->objectType->flags & (asOBJ_VALUE|asOBJ_SCOPED));
+ if( takeExtraRef || materializeUnsafeRef )
{
// Keep a reference to the object in a local variable
before.bc.AddCode(&lctx->bc);
@@ -14434,8 +14447,22 @@ int asCCompiler::ProcessPropertyGetSetAccessor(asCExprContext *ctx, asCExprConte
rctx->bc.GetVarsUsed(reservedVariables);
before.bc.GetVarsUsed(reservedVariables);
- asCDataType dt = asCDataType::CreateObjectHandle(func->objectType, false);
- int offset = AllocateVariable(dt, true);
+ asCDataType dt;
+ int offset;
+ if( takeExtraRef )
+ {
+ dt = asCDataType::CreateObjectHandle(func->objectType, false);
+ offset = AllocateVariable(dt, true);
+ }
+ else
+ {
+ // Allocate a plain (non-owning) reference variable. Being a reference rather than a
+ // handle, no destructor/release will ever be emitted for it (see CallDestructor), which
+ // is correct since we don't own the object.
+ dt = asCDataType::CreateType(func->objectType, false);
+ offset = AllocateVariable(dt, true, false, true);
+ dt.MakeReference(true);
+ }
reservedVariables.SetLength(len);
@@ -14492,7 +14519,22 @@ int asCCompiler::ProcessPropertyGetSetAccessor(asCExprContext *ctx, asCExprConte
MergeExprBytecodeAndType(ctx, &llctx);
if( before.type.stackOffset )
- ReleaseTemporaryVariable(before.type.stackOffset, &ctx->bc);
+ {
+ if( takeExtraRef )
+ ReleaseTemporaryVariable(before.type.stackOffset, &ctx->bc);
+ else
+ {
+ // This variable never owned the reference it aliased (see above), so it must not be
+ // released - instead it is explicitly cleared to null. This matters for value types only
+ // for hygiene (their memory is otherwise left with a stale, meaningless pointer), but for
+ // scoped types it is required for correctness: the variable's slot may be reused by a
+ // later, unrelated compound assignment, and if it were left holding a non-null value,
+ // that later use's REFCPY would mistake it for a previously (validly) held reference and
+ // release it out from under its real owner.
+ ctx->bc.InstrSHORT(asBC_ClrVPtr, (short)before.type.stackOffset);
+ DeallocateVariable(before.type.stackOffset);
+ }
+ }
MergeExprBytecode(ctx, &before);
ProcessDeferredParams(ctx);
@@ -14500,6 +14542,196 @@ int asCCompiler::ProcessPropertyGetSetAccessor(asCExprContext *ctx, asCExprConte
return 0;
}
+// Prefix/postfix ++ and -- on property accessors are essentially compound assignment (x += 1 or
+// x -= 1) with a synthesized constant 1, and so are subject to the exact same requirements and
+// limitations as ProcessPropertyGetSetAccessor above (both get and set accessors are required, and
+// value/scoped types require the application to opt-in via
+// asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS).
+//
+// Unlike compound assignment, both the *old* (pre-update) and *new* (post-update) value are needed
+// here - postfix returns the old value, prefix returns the new one - so this doesn't simply delegate
+// to ProcessPropertyGetSetAccessor (whose own result reflects whatever the set accessor call itself
+// returns, typically nothing usable, since setters are usually declared to return void). Instead the
+// get accessor is called exactly once, its result kept in its own temporary (the eventual old-value
+// result), the new value computed from that, and finally the set accessor called with the new value -
+// mirroring the address materialization/reuse done in ProcessPropertyGetSetAccessor (see there for
+// the full rationale) so that value/scoped types are supported under the same conditions.
+int asCCompiler::ProcessPropertyIncDecAccessor(asCExprContext *ctx, eTokenType op, bool isPostFix, asCScriptNode *errNode)
+{
+ asASSERT( op == ttInc || op == ttDec );
+
+ if( ctx->property_arg != 0 )
+ {
+ ProcessPropertyGetAccessor(ctx, errNode);
+ Error(TXT_COMPOUND_ASGN_WITH_IDX_PROP, errNode);
+ return -1;
+ }
+
+ if( ctx->property_set == 0 || ctx->property_get == 0 )
+ {
+ ProcessPropertyGetAccessor(ctx, errNode);
+ Error(TXT_COMPOUND_ASGN_REQUIRE_GET_SET, errNode);
+ return -1;
+ }
+
+ asCScriptFunction *func = engine->scriptFunctions[ctx->property_set];
+ if( func->objectType && (func->objectType->flags & (asOBJ_VALUE | asOBJ_SCOPED)) && !engine->ep.enableValueTypedCompoundPropertyAccessors )
+ {
+ ProcessPropertyGetAccessor(ctx, errNode);
+ Error(TXT_INVALID_REF_PROP_ACCESS, errNode);
+ return -1;
+ }
+
+ asCExprContext result(engine);
+
+ // Materialize the object's address into a local variable exactly like ProcessPropertyGetSetAccessor
+ // does internally (see there for the full rationale), so it can safely be reused: once for the get
+ // call below, and again further down for the set call. Note that unlike a plain resolved value,
+ // ctx must keep carrying real bytecode (a fresh PSF instruction, in the materialized case) up until
+ // the get call below actually consumes it to build the call.
+ bool takeExtraRef = func->objectType && (func->objectType->flags & (asOBJ_REF|asOBJ_SCOPED)) == asOBJ_REF;
+ bool materializeUnsafeRef = !takeExtraRef && func->objectType && (func->objectType->flags & (asOBJ_VALUE|asOBJ_SCOPED));
+ int materializedOffset = 0;
+ if( takeExtraRef || materializeUnsafeRef )
+ {
+ asCExprContext before(engine);
+ before.bc.AddCode(&ctx->bc);
+
+ asUINT len = reservedVariables.GetLength();
+ before.bc.GetVarsUsed(reservedVariables);
+
+ asCDataType dt;
+ int offset;
+ if( takeExtraRef )
+ {
+ dt = asCDataType::CreateObjectHandle(func->objectType, false);
+ offset = AllocateVariable(dt, true);
+ }
+ else
+ {
+ dt = asCDataType::CreateType(func->objectType, false);
+ offset = AllocateVariable(dt, true, false, true);
+ dt.MakeReference(true);
+ }
+
+ reservedVariables.SetLength(len);
+
+ if( ctx->property_ref )
+ before.bc.Instr(asBC_RDSPtr);
+ before.bc.InstrSHORT(asBC_PSF, (short)offset);
+ before.bc.InstrPTR(asBC_REFCPY, func->objectType);
+ before.bc.Instr(asBC_PopPtr);
+
+ if( ctx->type.isTemporary )
+ {
+ asSDeferredParam deferred;
+ deferred.origExpr = 0;
+ deferred.argInOutFlags = asTM_INREF;
+ deferred.argNode = 0;
+ deferred.argType.SetVariable(ctx->type.dataType, ctx->type.stackOffset, true);
+ before.deferredParams.PushLast(deferred);
+ }
+
+ // ctx keeps a fresh, single PSF instruction, ready to be consumed by the get call below
+ ctx->bc.InstrSHORT(asBC_PSF, (short)offset);
+ ctx->type.SetVariable(dt, offset, true);
+ ctx->property_ref = true;
+ ctx->type.isTemporary = false;
+ materializedOffset = offset;
+
+ result.bc.AddCode(&before.bc);
+ for( asUINT n = 0; n < before.deferredParams.GetLength(); n++ )
+ result.deferredParams.PushLast(before.deferredParams[n]);
+ }
+
+ // Keep the (now safely reusable) property/address information for the set call further down,
+ // before it gets consumed/cleared by fetching the current value below
+ asCExprContext llctx(engine);
+ llctx.type = ctx->type;
+ llctx.property_arg = ctx->property_arg;
+ llctx.property_const = ctx->property_const;
+ llctx.property_get = ctx->property_get;
+ llctx.property_handle = ctx->property_handle;
+ llctx.property_ref = ctx->property_ref;
+ llctx.property_set = ctx->property_set;
+
+ // Fetch the current value (the single call to the get accessor), using whatever addressing
+ // bytecode ctx currently carries (either the fresh PSF from materialization above, or its
+ // original, untouched addressing bytecode if no materialization was needed)
+ if( ProcessPropertyGetAccessor(ctx, errNode) < 0 )
+ return -1;
+
+ if( !ctx->type.dataType.IsPrimitive() )
+ {
+ Error(TXT_ILLEGAL_OPERATION, errNode);
+ return -1;
+ }
+
+ // Preserve the fetched value in its own temporary, independent from whatever the operator
+ // computation below does with it, since it may be the result of the whole expression (postfix)
+ ConvertToTempVariable(ctx);
+ asCExprValue oldValue = ctx->type;
+ ctx->type.isTemporary = false;
+ result.bc.AddCode(&ctx->bc);
+
+ // Compute the new value from the old one
+ asCExprContext one(engine);
+ one.type.SetConstantDW(asCDataType::CreatePrimitive(ttInt, true), 1);
+ asCExprContext newValue(engine);
+ if( CompileOperator(errNode, ctx, &one, &newValue, op == ttInc ? ttPlus : ttMinus, false) < 0 )
+ return -1;
+
+ // Preserve the new value in its own temporary too, independent from the argument-passing below
+ // (which would otherwise release its temp variable as soon as it's been passed to the set
+ // accessor), since it may be the result of the whole expression (prefix)
+ ConvertToTempVariable(&newValue);
+ asCExprValue newValueType = newValue.type;
+ newValue.type.isTemporary = false;
+
+ // If the address was materialized above it must be reused (a fresh instruction is needed each
+ // time, since bytecode - unlike the plain metadata above - cannot simply be copied/shared)
+ if( takeExtraRef || materializeUnsafeRef )
+ llctx.bc.InstrSHORT(asBC_PSF, (short)materializedOffset);
+
+ // Write the new value back through the set accessor
+ if( ProcessPropertySetAccessor(&llctx, &newValue, errNode) < 0 )
+ return -1;
+ MergeExprBytecode(&result, &llctx);
+
+ // Release (or clear, for the non-owning value/scoped case) our own materialized address variable,
+ // exactly as ProcessPropertyGetSetAccessor does for its own (single) use of the same pattern
+ if( takeExtraRef || materializeUnsafeRef )
+ {
+ if( takeExtraRef )
+ ReleaseTemporaryVariable(materializedOffset, &result.bc);
+ else
+ {
+ result.bc.InstrSHORT(asBC_ClrVPtr, (short)materializedOffset);
+ DeallocateVariable(materializedOffset);
+ }
+ }
+
+ // Release whichever of the old/new values isn't going to be the result of the expression
+ if( isPostFix )
+ {
+ ReleaseTemporaryVariable(newValueType.stackOffset, &result.bc);
+ oldValue.isTemporary = true;
+ result.type = oldValue;
+ }
+ else
+ {
+ ReleaseTemporaryVariable(oldValue.stackOffset, &result.bc);
+ newValueType.isTemporary = true;
+ result.type = newValueType;
+ }
+
+ ProcessDeferredParams(&result);
+
+ MergeExprBytecodeAndType(ctx, &result);
+
+ return 0;
+}
+
int asCCompiler::ProcessPropertyGetAccessor(asCExprContext *ctx, asCScriptNode *node)
{
// If no property accessor has been prepared then don't do anything
@@ -14666,8 +14898,7 @@ int asCCompiler::CompileExpressionPostOp(asCScriptNode *node, asCExprContext *ct
}
if( ctx->property_get || ctx->property_set )
{
- Error(TXT_INVALID_REF_PROP_ACCESS, node);
- return -1;
+ return ProcessPropertyIncDecAccessor(ctx, (eTokenType)op, true, node);
}
if( !ctx->type.isLValue )
{
diff --git a/sdk/angelscript/source/as_compiler.h b/sdk/angelscript/source/as_compiler.h
index 9169f508a..9e69305fb 100644
--- a/sdk/angelscript/source/as_compiler.h
+++ b/sdk/angelscript/source/as_compiler.h
@@ -337,6 +337,7 @@ class asCCompiler
int ProcessPropertyGetAccessor(asCExprContext *ctx, asCScriptNode *node);
int ProcessPropertySetAccessor(asCExprContext *ctx, asCExprContext *arg, asCScriptNode *node);
int ProcessPropertyGetSetAccessor(asCExprContext *ctx, asCExprContext *lctx, asCExprContext *rctx, eTokenType op, asCScriptNode *errNode);
+ int ProcessPropertyIncDecAccessor(asCExprContext *ctx, eTokenType op, bool isPostFix, asCScriptNode *errNode);
int FindPropertyAccessor(const asCString &name, asCExprContext *ctx, asCScriptNode *node, asSNameSpace *ns, bool isThisAccess = false);
int FindPropertyAccessor(const asCString &name, asCExprContext *ctx, asCExprContext *arg, asCScriptNode *node, asSNameSpace *ns, bool isThisAccess = false);
void PrepareTemporaryVariable(asCScriptNode *node, asCExprContext *ctx, bool forceOnHeap = false, bool forceValueCopy = false);
diff --git a/sdk/angelscript/source/as_scriptengine.cpp b/sdk/angelscript/source/as_scriptengine.cpp
index 82d578aeb..216318a11 100644
--- a/sdk/angelscript/source/as_scriptengine.cpp
+++ b/sdk/angelscript/source/as_scriptengine.cpp
@@ -498,6 +498,10 @@ int asCScriptEngine::SetEngineProperty(asEEngineProp property, asPWORD value)
tok.InitJumpTable();
break;
+ case asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS:
+ ep.enableValueTypedCompoundPropertyAccessors = value ? true : false;
+ break;
+
default:
return asINVALID_ARG;
}
@@ -630,6 +634,9 @@ asPWORD asCScriptEngine::GetEngineProperty(asEEngineProp property) const
case asEP_FOREACH_SUPPORT:
return ep.foreachSupport;
+ case asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS:
+ return ep.enableValueTypedCompoundPropertyAccessors;
+
default:
return 0;
}
@@ -708,6 +715,7 @@ asCScriptEngine::asCScriptEngine()
ep.memberInitMode = 1; // 0 = pre 2.38.0, members with init expr in declaration are initialized after super(), 1 = all members initialized in beginning, except if explicitly initialized in body
ep.boolConversionMode = 0; // 0 = only do use opImplConv for registered value type, 1 = use also opConv in contextual conversion even for reference types
ep.foreachSupport = true;
+ ep.enableValueTypedCompoundPropertyAccessors = false; // 0 = disabled (default), 1 = allow compound assignment through property accessors on value/scoped types
}
gc.engine = this;
diff --git a/sdk/angelscript/source/as_scriptengine.h b/sdk/angelscript/source/as_scriptengine.h
index 5cb21aa02..aadff7687 100644
--- a/sdk/angelscript/source/as_scriptengine.h
+++ b/sdk/angelscript/source/as_scriptengine.h
@@ -529,6 +529,7 @@ class asCScriptEngine : public asIScriptEngine
asUINT memberInitMode;
asUINT boolConversionMode;
bool foreachSupport;
+ bool enableValueTypedCompoundPropertyAccessors;
} ep;
// Callbacks
diff --git a/sdk/docs/doxygen/source/angelscript.h b/sdk/docs/doxygen/source/angelscript.h
index 60ad5da65..9813d8366 100644
--- a/sdk/docs/doxygen/source/angelscript.h
+++ b/sdk/docs/doxygen/source/angelscript.h
@@ -299,6 +299,8 @@ enum asEEngineProp
asEP_BOOL_CONVERSION_MODE = 39,
//! Enable foreach support. Default: true
asEP_FOREACH_SUPPORT = 40,
+ //! Allow compound assignment operators (e.g. +=) to be used with property accessors on value types and scoped reference types. Default: false
+ asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS = 41,
asEP_LAST_PROPERTY
};
diff --git a/sdk/docs/doxygen/source/doc_adv_custom_options.h b/sdk/docs/doxygen/source/doc_adv_custom_options.h
index 66ebe0dc6..d24019b9a 100644
--- a/sdk/docs/doxygen/source/doc_adv_custom_options.h
+++ b/sdk/docs/doxygen/source/doc_adv_custom_options.h
@@ -180,9 +180,17 @@ backwards compatibility for existing scripts before 2.38.0 that may be using the
When this property to 0, the class members with an initialization expression in the declaration will always be initialized
after the call to super(). It is also not possible to explicitly initialize members within the body of the constructor. When
-set to 1 (default), the class members will be initialized as described in \ref doc_script_class_memberinit. This mode was
+set to 1 (default), the class members will be initialized as described in \ref doc_script_class_memberinit. This mode was
added to provide backwards compatibility with versions before 2.38.0.
+\ref asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS
+
+Compound assignment operators (e.g. +=) used with property accessors (get_x/set_x) are normally
+rejected by the compiler when the property belongs to a value type or a scoped reference type, since the object cannot be
+guaranteed to stay alive between the get and set call. Turning on this option allows the compiler to accept such compound
+assignments anyway, at the application's own risk. Note that this does not introduce a new risk compared to a script writing
+the equivalent two statements manually, e.g. obj.x = obj.x + 1, which is always allowed.
+
diff --git a/sdk/tests/test_feature/source/test_getset.cpp b/sdk/tests/test_feature/source/test_getset.cpp
index 635c2c2f8..8fd30824a 100644
--- a/sdk/tests/test_feature/source/test_getset.cpp
+++ b/sdk/tests/test_feature/source/test_getset.cpp
@@ -98,6 +98,23 @@ void StringReplace(asIScriptGeneric *gen)
gen->SetReturnObject(&s);
}
+struct SCompoundValueType
+{
+ int value;
+};
+int CompoundValueType_GetValue(SCompoundValueType &obj) { return obj.value; }
+void CompoundValueType_SetValue(SCompoundValueType &obj, int v) { obj.value = v; }
+
+class CCompoundScopedType
+{
+public:
+ static CCompoundScopedType *Factory() { return new CCompoundScopedType(); }
+ void Release() { delete this; }
+ int value;
+};
+int CompoundScopedType_GetValue(CCompoundScopedType *obj) { return obj->value; }
+void CompoundScopedType_SetValue(CCompoundScopedType *obj, int v) { obj->value = v; }
+
bool Test()
{
RET_ON_MAX_PORT
@@ -1106,6 +1123,161 @@ bool Test()
engine->ShutDownAndRelease();
}
+ // Compound assignments on value type properties are allowed when the application
+ // opts in via asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS
+ {
+ engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
+ engine->SetMessageCallback(asMETHOD(CBufferedOutStream, Callback), &bout, asCALL_THISCALL);
+ engine->SetEngineProperty(asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS, 1);
+ engine->RegisterGlobalFunction("void assert(bool)", asFUNCTION(Assert), asCALL_GENERIC);
+
+ engine->RegisterObjectType("type", sizeof(SCompoundValueType), asOBJ_VALUE | asOBJ_POD);
+ engine->RegisterObjectMethod("type", "int get_prop() const property", asFUNCTION(CompoundValueType_GetValue), asCALL_CDECL_OBJFIRST);
+ engine->RegisterObjectMethod("type", "void set_prop(int) property", asFUNCTION(CompoundValueType_SetValue), asCALL_CDECL_OBJFIRST);
+
+ if( engine->GetEngineProperty(asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS) != 1 )
+ TEST_FAILED;
+
+ bout.buffer = "";
+ r = ExecuteString(engine, "type t; t.prop = 1; t.prop += 2; t.prop *= 3; assert(t.prop == 9);");
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
+ if( bout.buffer != "" )
+ {
+ PRINTF("%s", bout.buffer.c_str());
+ TEST_FAILED;
+ }
+
+ engine->ShutDownAndRelease();
+ }
+
+ // Compound assignments on scoped reference type properties are also allowed when the
+ // application opts in via asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS. Unlike value
+ // types, scoped types are reference counted (just without AddRef), so this also exercises that
+ // the object doesn't get released prematurely nor more than once - both within a single compound
+ // assignment statement, and when the same statement is compiled once but executed repeatedly
+ // (e.g. in a loop), which reuses the same temporary variable slot across iterations.
+ {
+ engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
+ engine->SetMessageCallback(asMETHOD(CBufferedOutStream, Callback), &bout, asCALL_THISCALL);
+ engine->SetEngineProperty(asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS, 1);
+ engine->RegisterGlobalFunction("void assert(bool)", asFUNCTION(Assert), asCALL_GENERIC);
+
+ engine->RegisterObjectType("scopedtype", 0, asOBJ_REF | asOBJ_SCOPED);
+ engine->RegisterObjectBehaviour("scopedtype", asBEHAVE_FACTORY, "scopedtype @f()", asFUNCTION(CCompoundScopedType::Factory), asCALL_CDECL);
+ engine->RegisterObjectBehaviour("scopedtype", asBEHAVE_RELEASE, "void f()", asMETHOD(CCompoundScopedType, Release), asCALL_THISCALL);
+ engine->RegisterObjectMethod("scopedtype", "int get_prop() const property", asFUNCTION(CompoundScopedType_GetValue), asCALL_CDECL_OBJFIRST);
+ engine->RegisterObjectMethod("scopedtype", "void set_prop(int) property", asFUNCTION(CompoundScopedType_SetValue), asCALL_CDECL_OBJFIRST);
+
+ bout.buffer = "";
+ r = ExecuteString(engine, "scopedtype s; s.prop = 1; s.prop += 2; s.prop *= 3; assert(s.prop == 9);");
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
+ if( bout.buffer != "" )
+ {
+ PRINTF("%s", bout.buffer.c_str());
+ TEST_FAILED;
+ }
+
+ bout.buffer = "";
+ r = ExecuteString(engine,
+ "int sum = 0; \n"
+ "for( int i = 0; i < 5; i++ ) { \n"
+ " scopedtype s2; s2.prop = i; s2.prop += 1; \n"
+ " sum += s2.prop; \n"
+ "} \n"
+ "assert(sum == 15);"); // (0+1)+(1+1)+(2+1)+(3+1)+(4+1) = 15
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
+ if( bout.buffer != "" )
+ {
+ PRINTF("%s", bout.buffer.c_str());
+ TEST_FAILED;
+ }
+
+ engine->ShutDownAndRelease();
+ }
+
+ // Prefix/postfix ++ and -- on value type property accessors are also allowed when the application
+ // opts in via asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS, just like compound assignment
+ {
+ engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
+ engine->SetMessageCallback(asMETHOD(CBufferedOutStream, Callback), &bout, asCALL_THISCALL);
+ engine->SetEngineProperty(asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS, 1);
+ engine->RegisterGlobalFunction("void assert(bool)", asFUNCTION(Assert), asCALL_GENERIC);
+
+ engine->RegisterObjectType("type", sizeof(SCompoundValueType), asOBJ_VALUE | asOBJ_POD);
+ engine->RegisterObjectMethod("type", "int get_prop() const property", asFUNCTION(CompoundValueType_GetValue), asCALL_CDECL_OBJFIRST);
+ engine->RegisterObjectMethod("type", "void set_prop(int) property", asFUNCTION(CompoundValueType_SetValue), asCALL_CDECL_OBJFIRST);
+
+ bout.buffer = "";
+ r = ExecuteString(engine,
+ "type t; t.prop = 1; \n"
+ "assert(t.prop++ == 1); assert(t.prop == 2); \n" // postfix: returns old value
+ "assert(++t.prop == 3); assert(t.prop == 3); \n" // prefix: returns new value
+ "assert(t.prop-- == 3); assert(t.prop == 2); \n"
+ "assert(--t.prop == 1); assert(t.prop == 1); \n");
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
+ if( bout.buffer != "" )
+ {
+ PRINTF("%s", bout.buffer.c_str());
+ TEST_FAILED;
+ }
+
+ engine->ShutDownAndRelease();
+ }
+
+ // Prefix/postfix ++ and -- on scoped reference type property accessors are also allowed under the
+ // same engine property. As with compound assignment, this also exercises that the object isn't
+ // released prematurely nor more than once, including when the statement executes repeatedly in a
+ // loop (reusing the same temporary variable slot across iterations).
+ {
+ engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
+ engine->SetMessageCallback(asMETHOD(CBufferedOutStream, Callback), &bout, asCALL_THISCALL);
+ engine->SetEngineProperty(asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS, 1);
+ engine->RegisterGlobalFunction("void assert(bool)", asFUNCTION(Assert), asCALL_GENERIC);
+
+ engine->RegisterObjectType("scopedtype", 0, asOBJ_REF | asOBJ_SCOPED);
+ engine->RegisterObjectBehaviour("scopedtype", asBEHAVE_FACTORY, "scopedtype @f()", asFUNCTION(CCompoundScopedType::Factory), asCALL_CDECL);
+ engine->RegisterObjectBehaviour("scopedtype", asBEHAVE_RELEASE, "void f()", asMETHOD(CCompoundScopedType, Release), asCALL_THISCALL);
+ engine->RegisterObjectMethod("scopedtype", "int get_prop() const property", asFUNCTION(CompoundScopedType_GetValue), asCALL_CDECL_OBJFIRST);
+ engine->RegisterObjectMethod("scopedtype", "void set_prop(int) property", asFUNCTION(CompoundScopedType_SetValue), asCALL_CDECL_OBJFIRST);
+
+ bout.buffer = "";
+ r = ExecuteString(engine,
+ "scopedtype s; s.prop = 1; \n"
+ "assert(s.prop++ == 1); assert(s.prop == 2); \n" // postfix: returns old value
+ "assert(++s.prop == 3); assert(s.prop == 3); \n" // prefix: returns new value
+ "assert(s.prop-- == 3); assert(s.prop == 2); \n"
+ "assert(--s.prop == 1); assert(s.prop == 1); \n");
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
+ if( bout.buffer != "" )
+ {
+ PRINTF("%s", bout.buffer.c_str());
+ TEST_FAILED;
+ }
+
+ bout.buffer = "";
+ r = ExecuteString(engine,
+ "int sum = 0; \n"
+ "for( int i = 0; i < 5; i++ ) { \n"
+ " scopedtype s2; s2.prop = i; s2.prop++; \n"
+ " sum += s2.prop; \n"
+ "} \n"
+ "assert(sum == 15);"); // (0+1)+(1+1)+(2+1)+(3+1)+(4+1) = 15
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
+ if( bout.buffer != "" )
+ {
+ PRINTF("%s", bout.buffer.c_str());
+ TEST_FAILED;
+ }
+
+ engine->ShutDownAndRelease();
+ }
+
// Test memory leak with shared classes and virtual properties
// http://www.gamedev.net/topic/644919-memory-leak-in-virtual-properties/
@@ -1567,34 +1739,39 @@ bool Test()
TEST_FAILED;
}
- // Test pre and post ++. Should fail, since the expression is not a variable
- const char *script9 =
+ // Pre and post ++/-- on property accessors are allowed for ordinary reference types (unlike
+ // value/scoped types, which additionally require asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS)
+ const char *script9 =
"class Test \n"
"{ \n"
- " uint get_p() property {return 0;} \n"
- " void set_p(uint) property {} \n"
+ " uint _p; \n"
+ " uint get_p() property {return _p;} \n"
+ " void set_p(uint v) property {_p = v;} \n"
"} \n"
"void main() \n"
"{ \n"
" Test t; \n"
- " t.p++; \n"
- " --t.p; \n"
+ " t.p = 1; \n"
+ " assert(t.p++ == 1); \n" // postfix: returns old value (1), t.p becomes 2
+ " assert(--t.p == 1); \n" // prefix: returns new value (1), t.p becomes 1
+ " assert(t.p == 1); \n"
"} \n";
mod->AddScriptSection("script", script9);
bout.buffer = "";
r = mod->Build();
- if( r >= 0 )
+ if( r < 0 )
{
TEST_FAILED;
- PRINTF("Didn't fail to compile the script\n");
+ PRINTF("%s", bout.buffer.c_str());
}
- if( bout.buffer != "script (6, 1) : Info : Compiling void main()\n"
- "script (9, 6) : Error : Invalid reference. Property accessors cannot be used in combined read/write operations\n"
- "script (10, 3) : Error : Invalid reference. Property accessors cannot be used in combined read/write operations\n" )
+ if( bout.buffer != "" )
{
PRINTF("%s", bout.buffer.c_str());
TEST_FAILED;
}
+ r = ExecuteString(engine, "main()", mod);
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
// Test using property accessors from within class methods without 'this'
// Test accessor where the object is a handle
@@ -2472,13 +2649,16 @@ bool Test()
engine->Release();
}
- // Test member property accessors with ++ where the set accessor takes a reference
+ // Test member property accessors with ++ where the set accessor takes a reference. This is now
+ // allowed for ordinary reference types like CTest here (see asEP_ENABLE_VALUE_TYPED_COMPOUND_PROPERTY_ACCESSORS
+ // for the value/scoped type case, which additionally requires opting in).
{
engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
engine->SetMessageCallback(asMETHOD(CBufferedOutStream, Callback), &bout, asCALL_THISCALL);
+ engine->RegisterGlobalFunction("void assert(bool)", asFUNCTION(Assert), asCALL_GENERIC);
bout.buffer = "";
- const char *script =
+ const char *script =
"class CTest \n"
"{ \n"
" double _vol; \n"
@@ -2489,19 +2669,25 @@ bool Test()
"void main() \n"
"{ \n"
" for( t.vol = 0; t.vol < 10; t.vol++ ); \n"
+ " assert(t.vol == 10); \n"
"} \n";
mod = engine->GetModule(0, asGM_ALWAYS_CREATE);
mod->AddScriptSection("script", script);
r = mod->Build();
- if( r >= 0 )
+ if( r < 0 )
+ {
+ PRINTF("%s", bout.buffer.c_str());
TEST_FAILED;
- if( bout.buffer != "script (8, 1) : Info : Compiling void main()\n"
- "script (10, 36) : Error : Invalid reference. Property accessors cannot be used in combined read/write operations\n" )
+ }
+ if( bout.buffer != "" )
{
PRINTF("%s", bout.buffer.c_str());
TEST_FAILED;
}
+ r = ExecuteString(engine, "main()", mod);
+ if( r != asEXECUTION_FINISHED )
+ TEST_FAILED;
engine->Release();
}
diff --git a/sdk/tests/test_feature/source/test_saveload.cpp b/sdk/tests/test_feature/source/test_saveload.cpp
index dcc8abe9d..79fceae12 100644
--- a/sdk/tests/test_feature/source/test_saveload.cpp
+++ b/sdk/tests/test_feature/source/test_saveload.cpp
@@ -1933,7 +1933,7 @@ bool Test()
engine->ShutDownAndRelease();
- if( bout.buffer != "config (65, 0) : Warning : Cannot register template callback without the actual implementation\n" )
+ if( bout.buffer != "config (66, 0) : Warning : Cannot register template callback without the actual implementation\n" )
{
PRINTF("%s", bout.buffer.c_str());
TEST_FAILED;
@@ -2007,6 +2007,7 @@ bool Test()
"ep 38 1\n"
"ep 39 0\n"
"ep 40 1\n"
+ "ep 41 0\n"
"\n"
"// Enums\n"
"\n"