diff options
21 files changed, 290 insertions, 60 deletions
diff --git a/source/slang/core.meta.slang b/source/slang/core.meta.slang index 2dae7b3c4..695423285 100644 --- a/source/slang/core.meta.slang +++ b/source/slang/core.meta.slang @@ -2025,13 +2025,20 @@ T reinterpret(U value); // Use an otherwise unused value // -// This can be used to silence the warning about returning before initializing -// an out paramter. +// This can be used to silence the warning about returning before initializing an out paramter. __generic<T> [__readNone] [ForceInline] +__intrinsic_op($(kIROp_Unmodified)) void unused(inout T){} +// This can be used to silence the warning about not writing to an inout parameter. +__generic<T> +[__readNone] +[ForceInline] +__intrinsic_op($(kIROp_Unmodified)) +void unmodified(out T){} + // Specialized function /// Given a string returns an integer hash of that string. diff --git a/source/slang/diff.meta.slang b/source/slang/diff.meta.slang index c912e026c..a4c468ef7 100644 --- a/source/slang/diff.meta.slang +++ b/source/slang/diff.meta.slang @@ -273,7 +273,12 @@ struct TensorView __subscript(uint index) -> T { [ForceInline] [__NoSideEffect] get { return load(index); } - [ForceInline] set { store(index, newValue); } + + [ForceInline] set + { + unmodified(this); + store(index, newValue); + } [__NoSideEffect] ref @@ -288,7 +293,13 @@ struct TensorView __subscript(uint i1, uint i2) -> T { [ForceInline] [__NoSideEffect] get { return load(i1, i2); } - [ForceInline] set { store(i1, i2, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i1, i2, newValue); + } + [__NoSideEffect] ref { @@ -302,7 +313,13 @@ struct TensorView __subscript(uint2 i) -> T { [ForceInline] [__NoSideEffect] get { return load(i.x, i.y); } - [ForceInline] set { store(i.x, i.y, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i.x, i.y, newValue); + } + [__NoSideEffect] ref { @@ -316,7 +333,13 @@ struct TensorView __subscript(uint i1, uint i2, uint i3) -> T { [ForceInline] [__NoSideEffect] get { return load(i1, i2, i3); } - [ForceInline] set { store(i1, i2, i3, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i1, i2, i3, newValue); + } + [__NoSideEffect] ref { @@ -330,7 +353,13 @@ struct TensorView __subscript(uint3 i) -> T { [ForceInline] [__NoSideEffect] get { return load(i.x, i.y, i.z); } - [ForceInline] set { store(i.x, i.y, i.z, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i.x, i.y, i.z, newValue); + } + [__NoSideEffect] ref { @@ -344,7 +373,13 @@ struct TensorView __subscript(uint i1, uint i2, uint i3, uint i4) -> T { [ForceInline] [__NoSideEffect] get { return load(i1, i2, i3, i4); } - [ForceInline] set { store(i1, i2, i3, i4, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i1, i2, i3, i4, newValue); + } + [__NoSideEffect] ref { @@ -358,7 +393,13 @@ struct TensorView __subscript(uint4 i) -> T { [__NoSideEffect][ForceInline] get { return load(i.x, i.y, i.z, i.w); } - [ForceInline] set { store(i.x, i.y, i.z, i.w, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i.x, i.y, i.z, i.w, newValue); + } + [__NoSideEffect] ref { @@ -372,7 +413,13 @@ struct TensorView __subscript(uint i1, uint i2, uint i3, uint i4, uint i5) -> T { [ForceInline] [__NoSideEffect] get { return load(i1, i2, i3, i4, i5); } - [ForceInline] set { store(i1, i2, i3, i4, i5, newValue); } + + [ForceInline] set + { + unmodified(this); + store(i1, i2, i3, i4, i5, newValue); + } + [__NoSideEffect] ref { diff --git a/source/slang/slang-diagnostic-defs.h b/source/slang/slang-diagnostic-defs.h index 6d7be3f92..44e8aa13d 100644 --- a/source/slang/slang-diagnostic-defs.h +++ b/source/slang/slang-diagnostic-defs.h @@ -752,6 +752,8 @@ DIAGNOSTIC(41018, Warning, returningWithUninitializedOut, "returning without ini DIAGNOSTIC(41019, Warning, returningWithPartiallyUninitializedOut, "returning without fully initializing out parameter '$0'") DIAGNOSTIC(41020, Warning, constructorUninitializedField, "exiting constructor without initializing field '$0'") DIAGNOSTIC(41021, Warning, fieldNotDefaultInitialized, "default initializer for '$0' will not initialize field '$1'") +DIAGNOSTIC(41022, Warning, inOutNeverStoredInto, "inout parameter '$0' is never written to") +DIAGNOSTIC(41023, Warning, methodNeverMutates, "method marked `[mutable]` but never modifies `this`") DIAGNOSTIC(41011, Error, typeDoesNotFitAnyValueSize, "type '$0' does not fit in the size required by its conforming interface.") DIAGNOSTIC(41012, Note, typeAndLimit, "sizeof($0) is $1, limit is $2") diff --git a/source/slang/slang-emit-c-like.cpp b/source/slang/slang-emit-c-like.cpp index dd5ef8c51..fadf76dd2 100644 --- a/source/slang/slang-emit-c-like.cpp +++ b/source/slang/slang-emit-c-like.cpp @@ -2889,6 +2889,9 @@ void CLikeSourceEmitter::_emitInst(IRInst* inst) case kIROp_DebugValue: break; + case kIROp_Unmodified: + break; + // Insts that needs to be emitted as code blocks. case kIROp_CudaKernelLaunch: case kIROp_AtomicCounterIncrement: diff --git a/source/slang/slang-emit-spirv.cpp b/source/slang/slang-emit-spirv.cpp index 23eaa4435..bb0a2565c 100644 --- a/source/slang/slang-emit-spirv.cpp +++ b/source/slang/slang-emit-spirv.cpp @@ -2719,6 +2719,7 @@ struct SPIRVEmitContext case kIROp_Specialize: case kIROp_MissingReturn: case kIROp_StaticAssert: + case kIROp_Unmodified: break; case kIROp_Var: result = emitVar(parent, inst); diff --git a/source/slang/slang-ir-addr-inst-elimination.cpp b/source/slang/slang-ir-addr-inst-elimination.cpp index 2581ea37f..7889a2f61 100644 --- a/source/slang/slang-ir-addr-inst-elimination.cpp +++ b/source/slang/slang-ir-addr-inst-elimination.cpp @@ -169,6 +169,7 @@ struct AddressInstEliminationContext break; case kIROp_GetElementPtr: case kIROp_FieldAddress: + case kIROp_Unmodified: break; default: sink->diagnose(use->getUser()->sourceLoc, Diagnostics::unsupportedUseOfLValueForAutoDiff); diff --git a/source/slang/slang-ir-autodiff-transcriber-base.h b/source/slang/slang-ir-autodiff-transcriber-base.h index 7b4c293e9..f672631e3 100644 --- a/source/slang/slang-ir-autodiff-transcriber-base.h +++ b/source/slang/slang-ir-autodiff-transcriber-base.h @@ -123,8 +123,14 @@ struct AutoDiffTranscriberBase InstPair transcribeBlock(IRBuilder* builder, IRBlock* origBlock) { - HashSet<IRInst*> emptySet; - return transcribeBlockImpl(builder, origBlock, emptySet); + HashSet<IRInst*> ignore; + for (auto inst = origBlock->getFirstInst(); inst; inst = inst->next) + { + if (inst->m_op == kIROp_Unmodified) + ignore.add(inst); + } + + return transcribeBlockImpl(builder, origBlock, ignore); } // Transcribe a generic definition diff --git a/source/slang/slang-ir-inst-defs.h b/source/slang/slang-ir-inst-defs.h index 0e84be2b2..84ee634a1 100644 --- a/source/slang/slang-ir-inst-defs.h +++ b/source/slang/slang-ir-inst-defs.h @@ -931,6 +931,7 @@ INST_RANGE(BindingQuery, GetRegisterIndex, GetRegisterSpace) INST(SemanticDecoration, semantic, 2, 0) INST(ConstructorDecoration, constructor, 1, 0) + INST(MethodDecoration, method, 0, 0) INST(PackOffsetDecoration, packoffset, 2, 0) // Reflection metadata for a shader parameter that provides the original type name. @@ -1094,6 +1095,7 @@ INST(ExtractTaggedUnionPayload, extractTaggedUnionPayload, 1, 0) INST(BitCast, bitCast, 1, 0) INST(Reinterpret, reinterpret, 1, 0) +INST(Unmodified, unmodified, 1, 0) INST(OutImplicitCast, outImplicitCast, 1, 0) INST(InOutImplicitCast, inOutImplicitCast, 1, 0) INST(IntCast, intCast, 1, 0) diff --git a/source/slang/slang-ir-insts.h b/source/slang/slang-ir-insts.h index 8a801e4e7..e30b903b5 100644 --- a/source/slang/slang-ir-insts.h +++ b/source/slang/slang-ir-insts.h @@ -1365,6 +1365,8 @@ struct IRConstructorDecorartion : IRDecoration bool getSynthesizedStatus() { return cast<IRBoolLit>(getOperand(0))->getValue(); } }; +IR_SIMPLE_DECORATION(MethodDecoration) + struct IRPackOffsetDecoration : IRDecoration { enum diff --git a/source/slang/slang-ir-use-uninitialized-values.cpp b/source/slang/slang-ir-use-uninitialized-values.cpp index ca723a97c..b8dfcc33c 100644 --- a/source/slang/slang-ir-use-uninitialized-values.cpp +++ b/source/slang/slang-ir-use-uninitialized-values.cpp @@ -38,30 +38,67 @@ namespace Slang || (inst->m_op == kIROp_Var); } - static bool isPotentiallyUnintended(IRParam* param) + static bool isUnmodifying(IRFunc *func) { - auto outType = as<IROutType>(param->getFullType()); - if (!outType) - return false; - - // Don't check `out Vertices<T>` or `out Indices<T>` parameters - // in mesh shaders. - // TODO: we should find a better way to represent these mesh shader - // parameters so they conform to the initialize before use convention. - // For example, we can use a `OutputVetices` and `OutputIndices` type - // to represent an output, like `OutputPatch` in domain shader. - // For now, we just skip the check for these parameters. - switch (outType->getValueType()->getOp()) - { - case kIROp_VerticesType: - case kIROp_IndicesType: - case kIROp_PrimitivesType: - return false; - default: - break; + auto intr = func->findDecoration<IRIntrinsicOpDecoration>(); + return (intr && intr->getIntrinsicOp() == kIROp_Unmodified); + } + + enum ParameterCheckType + { + Never, // Parameter does NOT to be checked for uninitialization (e.g. is `in` or special type) + AsOut, // Parameter DOES need to be checked for usage before initializations + AsInOut // Parameter DOES need to be checked to see if it is ever written to + }; + + static ParameterCheckType isPotentiallyUnintended(IRParam* param, Stage stage, int index) + { + IRType* type = param->getFullType(); + if (auto out = as<IROutType>(param->getFullType())) + { + // Don't check `out Vertices<T>` or `out Indices<T>` parameters + // in mesh shaders. + // TODO: we should find a better way to represent these mesh shader + // parameters so they conform to the initialize before use convention. + // For example, we can use a `OutputVetices` and `OutputIndices` type + // to represent an output, like `OutputPatch` in domain shader. + // For now, we just skip the check for these parameters. + switch (out->getValueType()->getOp()) + { + case kIROp_VerticesType: + case kIROp_IndicesType: + case kIROp_PrimitivesType: + return Never; + default: + break; + } + + return AsOut; + } + else if (auto inout = as<IRInOutType>(type)) + { + // TODO: some way to check if the method + // is actually used for autodiff + if (as<IRDifferentialPairUserCodeType>(inout->getValueType())) + return Never; + + switch (stage) + { + case Stage::AnyHit: + case Stage::ClosestHit: + // In HLSL the payload is required to be `inout` + return (index == 0) ? Never : AsInOut; + case Stage::Geometry: + // Second parameter is the triangle stream + return (index == 1) ? Never : AsInOut; + default: + break; + } + + return AsInOut; } - return true; + return Never; } static bool isAliasable(IRInst* inst) @@ -275,6 +312,7 @@ namespace Slang // Miscellaenous cases case kIROp_ManagedPtrAttach: + case kIROp_Unmodified: stores.add(user); break; @@ -481,6 +519,62 @@ namespace Slang } } + static void checkParameterAsOut(ReachabilityContext &reachability, IRFunc* func, IRParam* param, DiagnosticSink* sink) + { + auto loads = getUnresolvedParamLoads(reachability, func, param); + for (auto load : loads) + { + sink->diagnose(load, + as<IRTerminatorInst>(load) + ? Diagnostics::returningWithUninitializedOut + : Diagnostics::usingUninitializedOut, + param); + } + } + + static void checkParameterAsInOut(IRParam* param, IRFunc* func, bool isThis, DiagnosticSink* sink) + { + // If the inout is used for the sake of interface conformance, let it be + for (auto use = func->firstUse; use; use = use->nextUse) + { + if (as<IRWitnessTableEntry>(use->getUser())) + return; + } + + // If there is at least one write... + List<IRInst*> stores; + List<IRInst*> loads; + + for (auto alias : getAliasableInstructions(param)) + { + for (auto use = alias->firstUse; use; use = use->nextUse) + { + IRInst* user = use->getUser(); + collectLoadStore(stores, loads, user, alias); + + // ...we will ignore the rest... + if (stores.getCount()) + return; + } + } + + // ...or if there is an intrinsic_asm instruction + for (const auto& b : func->getBlocks()) + { + for (auto inst = b->getFirstInst(); inst; inst = inst->next) + { + if (as<IRGenericAsm>(inst)) + return; + } + } + + sink->diagnose(param, + isThis + ? Diagnostics::methodNeverMutates + : Diagnostics::inOutNeverStoredInto, + param); + } + static void checkUninitializedValues(IRFunc* func, DiagnosticSink* sink) { // Differentiable functions will generate undefined values @@ -495,22 +589,30 @@ namespace Slang ReachabilityContext reachability(func); // Used for a further analysis and to skip usual return checks - auto constructor = func->findDecoration <IRConstructorDecorartion> (); + auto constructor = func->findDecoration<IRConstructorDecorartion>(); + + // Special checks for stages e.g. raytracing shader + Stage stage = Stage::Unknown; + if (auto entry = func->findDecoration<IREntryPointDecoration>()) + stage = entry->getProfile().getStage(); + + bool structMethod = func->findDecoration<IRMethodDecoration>(); // Check out parameters - for (auto param : firstBlock->getParams()) + if (!isUnmodifying(func)) { - if (!isPotentiallyUnintended(param)) - continue; - - auto loads = getUnresolvedParamLoads(reachability, func, param); - for (auto load : loads) + int index = 0; + for (auto param : firstBlock->getParams()) { - sink->diagnose(load, - as<IRTerminatorInst>(load) - ? Diagnostics::returningWithUninitializedOut - : Diagnostics::usingUninitializedOut, - param); + bool isThis = structMethod && (index == 0); + + ParameterCheckType checkType = isPotentiallyUnintended(param, stage, index); + if (checkType == AsOut) + checkParameterAsOut(reachability, func, param, sink); + else if (checkType == AsInOut) + checkParameterAsInOut(param, func, isThis, sink); + + index++; } } diff --git a/source/slang/slang-lower-to-ir.cpp b/source/slang/slang-lower-to-ir.cpp index 66918c2f1..50d017fb9 100644 --- a/source/slang/slang-lower-to-ir.cpp +++ b/source/slang/slang-lower-to-ir.cpp @@ -9509,7 +9509,6 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo> LoweredValInfo lowerFuncDeclInContext(IRGenContext* subContext, IRBuilder* subBuilder, FunctionDeclBase* decl, bool emitBody = true) { - IRGeneric* outerGeneric = nullptr; subContext->funcDecl = decl; @@ -9544,6 +9543,10 @@ struct DeclLoweringVisitor : DeclVisitor<DeclLoweringVisitor, LoweredValInfo> } } + // For diagnostics + if (as<StructDecl>(decl->parentDecl)) + getBuilder()->addSimpleDecoration<IRMethodDecoration>(irFunc); + auto irFuncType = info.type; auto& irResultType = info.resultType; auto& parameterLists = info.parameterLists; diff --git a/tests/autodiff/reverse-inout-param-custom-derivative.slang b/tests/autodiff/reverse-inout-param-custom-derivative.slang index 8769a33c7..c4549e37b 100644 --- a/tests/autodiff/reverse-inout-param-custom-derivative.slang +++ b/tests/autodiff/reverse-inout-param-custom-derivative.slang @@ -5,7 +5,7 @@ //TEST_INPUT:ubuffer(data=[0], stride=4):out,name=outputBuffer RWStructuredBuffer<float> outputBuffer; -float rng(inout int state, float x) +float rng(int state, float x) { return state + x; } diff --git a/tests/bugs/gl-33-ext.slang b/tests/bugs/gl-33-ext.slang index fccde99a3..6df9f286a 100644 --- a/tests/bugs/gl-33-ext.slang +++ b/tests/bugs/gl-33-ext.slang @@ -4,5 +4,5 @@ public struct A { public int state; - [mutating] public int next() { return state; } + public int next() { return state; } }; diff --git a/tests/bugs/optional.slang b/tests/bugs/optional.slang index 3512ba29f..7a220b23d 100644 --- a/tests/bugs/optional.slang +++ b/tests/bugs/optional.slang @@ -14,7 +14,7 @@ struct P } struct Tr { - int test<T:IArithmetic>(T t, inout P p) + int test<T:IArithmetic>(T t, P p) { const IFoo hit = p.f; let castResult = hit as S; diff --git a/tests/diagnostics/inout-never-written.slang b/tests/diagnostics/inout-never-written.slang new file mode 100644 index 000000000..0bc59ecf3 --- /dev/null +++ b/tests/diagnostics/inout-never-written.slang @@ -0,0 +1,48 @@ +//TEST:SIMPLE(filecheck=CHK): -target spirv + +struct State +{ + float3 v; + float3 n; + int rnd; +}; + +//CHK-DAG: ([[# @LINE + 1]]): warning 41022: inout parameter 'x' is never written to +void int_never_assigned(inout int x) {} + +//CHK-DAG: ([[# @LINE + 1]]): warning 41022: inout parameter 'state' is never written to +void state_never_assigned(inout State state, inout float v) +{ + v = state.v.x; +} + +void state_assigned(inout State state) +{ + state.rnd = (int) dot(state.v, state.n); +} + +struct A +{ + int state; + + //CHK-DAG: ([[# @LINE + 1]]): warning 41023: method marked `[mutable]` but never modifies `this` + [mutating] int next() { return state; } + + [mutating] int progress() + { + unmodified(state); + return state; + } +}; + +__generic <T> +struct B +{ + int state; + + //CHK-DAG: ([[# @LINE + 1]]): warning 41023: method marked `[mutable]` but never modifies `this` + [mutating] int next() { return state; } +}; + +//CHK-NOT: warning 41022 +//CHK-NOT: warning 41023
\ No newline at end of file diff --git a/tests/hlsl-intrinsic/matrix-double.slang b/tests/hlsl-intrinsic/matrix-double.slang index 31ec54f8e..6ed1b42bf 100644 --- a/tests/hlsl-intrinsic/matrix-double.slang +++ b/tests/hlsl-intrinsic/matrix-double.slang @@ -46,6 +46,8 @@ IntMatrix makeIntMatrix(int v) void test1(inout FloatMatrix ft, inout FloatMatrix f, int idx) { + unmodified(f); + // fmod ft += FloatMatrix(IntMatrix(((f % makeFloatMatrix(0.11f)) * makeFloatMatrix(100)) + makeFloatMatrix(0.5))); @@ -112,6 +114,8 @@ void test1(inout FloatMatrix ft, inout FloatMatrix f, int idx) void test2(inout FloatMatrix ft, inout FloatMatrix f) { + unmodified(f); + ft += pow(makeFloatMatrix(0.5), f); ft += smoothstep(makeFloatMatrix(0.2), makeFloatMatrix(0.7), f); diff --git a/tests/hlsl-intrinsic/size-of/align-of-3.slang b/tests/hlsl-intrinsic/size-of/align-of-3.slang index 28665b5ab..079de9241 100644 --- a/tests/hlsl-intrinsic/size-of/align-of-3.slang +++ b/tests/hlsl-intrinsic/size-of/align-of-3.slang @@ -10,7 +10,7 @@ RWStructuredBuffer<int> outputBuffer; -int getParamSize(inout uint3 v) +int getParamSize(uint3 v) { return alignof(v); } @@ -19,7 +19,7 @@ int getParamSize(inout uint3 v) // With "natural" layout size must be the same across all targets. // The parameter passing semantic is "value in/value out", so the size // will always be the size of the *value* regardless. So uint3 in this case. -int getParamSizeWithDirection(inout uint3 v) +int getParamSizeWithDirection(uint3 v) { return alignof(v); } diff --git a/tests/hlsl-intrinsic/size-of/size-of-3.slang b/tests/hlsl-intrinsic/size-of/size-of-3.slang index 8a211867f..7e55bd320 100644 --- a/tests/hlsl-intrinsic/size-of/size-of-3.slang +++ b/tests/hlsl-intrinsic/size-of/size-of-3.slang @@ -10,7 +10,7 @@ RWStructuredBuffer<int> outputBuffer; -int getParamSize(inout uint3 v) +int getParamSize(uint3 v) { return sizeof(v); } @@ -19,7 +19,7 @@ int getParamSize(inout uint3 v) // With "natural" layout size must be the same across all targets. // The parameter passing semantic is "value in/value out", so the size // will always be the size of the *value* regardless. So uint3 in this case. -int getParamSizeWithDirection(inout uint3 v) +int getParamSizeWithDirection(uint3 v) { return sizeof(v); } diff --git a/tests/language-feature/struct-field-initializers/struct-field-no-initializer-complex-types.slang b/tests/language-feature/struct-field-initializers/struct-field-no-initializer-complex-types.slang index a55c4725d..66a3a2c64 100644 --- a/tests/language-feature/struct-field-initializers/struct-field-no-initializer-complex-types.slang +++ b/tests/language-feature/struct-field-initializers/struct-field-no-initializer-complex-types.slang @@ -6,7 +6,7 @@ //TEST_INPUT:ubuffer(data=[0 0], stride=4):out,name=outputBuffer RWStructuredBuffer<int> outputBuffer; -struct DefaultData +struct DefaultData : IDefaultInitializable { static const int2 val = int2(0, 1); float2 size; @@ -16,13 +16,10 @@ struct DefaultData extension DefaultData { - int someGet() - { - return val.x; - } + int someGet() { return val.x; } } -int loadDefaultData(inout DefaultData noInit) +int loadDefaultData(DefaultData noInit) { outputBuffer[1] = 1; return noInit.someGet(); diff --git a/tests/language-feature/unsized-array.slang b/tests/language-feature/unsized-array.slang index c4479867e..19ab0bd63 100644 --- a/tests/language-feature/unsized-array.slang +++ b/tests/language-feature/unsized-array.slang @@ -17,6 +17,7 @@ int test(int arr[]) int test2<T>(inout T arr[]) { + unmodified(arr); return arr.getCount(); } diff --git a/tests/pipeline/ray-tracing/trace-ray-inline.slang b/tests/pipeline/ray-tracing/trace-ray-inline.slang index 009dffbc7..b8688c2f5 100644 --- a/tests/pipeline/ray-tracing/trace-ray-inline.slang +++ b/tests/pipeline/ray-tracing/trace-ray-inline.slang @@ -57,6 +57,7 @@ void myTriangleClosestHit(inout MyRayPayload payload) // bool myTriangleAnyHit(inout MyRayPayload payload) { + unmodified(payload); return true; } @@ -75,6 +76,7 @@ void myProceduralClosestHit(inout MyRayPayload payload, MyProceduralHitAttrs att } bool myProceduralAnyHit(inout MyRayPayload payload) { + unmodified(payload); return true; } @@ -87,6 +89,8 @@ bool myProceduralAnyHit(inout MyRayPayload payload) // bool myProceduralIntersection(inout float tHit, inout MyProceduralHitAttrs hitAttrs) { + unmodified(tHit); + unmodified(hitAttrs); return true; } @@ -180,4 +184,4 @@ void main(uint3 tid : SV_DispatchThreadID) } resultBuffer[index] = payload.value; -}
\ No newline at end of file +} |
