diff --git a/llvm/docs/ReleaseNotes.rst b/llvm/docs/ReleaseNotes.rst --- a/llvm/docs/ReleaseNotes.rst +++ b/llvm/docs/ReleaseNotes.rst @@ -99,6 +99,9 @@ Changes to TableGen ------------------- +* Named arguments are supported. Arguments can be specified in the form of + ``name=value``. + Changes to Interprocedural Optimizations ---------------------------------------- diff --git a/llvm/docs/TableGen/ProgRef.rst b/llvm/docs/TableGen/ProgRef.rst --- a/llvm/docs/TableGen/ProgRef.rst +++ b/llvm/docs/TableGen/ProgRef.rst @@ -475,7 +475,7 @@ def Foo#i; .. productionlist:: - SimpleValue8: `ClassID` "<" `ValueListNE` ">" + SimpleValue8: `ClassID` "<" `ArgValueList` ">" This form creates a new anonymous record definition (as would be created by an unnamed ``def`` inheriting from the given class with the given template @@ -642,12 +642,31 @@ RecordBody: `ParentClassList` `Body` ParentClassList: [":" `ParentClassListNE`] ParentClassListNE: `ClassRef` ("," `ClassRef`)* - ClassRef: (`ClassID` | `MultiClassID`) ["<" [`ValueList`] ">"] + ClassRef: (`ClassID` | `MultiClassID`) ["<" [`ArgValueList`] ">"] + ArgValueList: `PostionalArgValueList` [","] `NamedArgValueList` + PostionalArgValueList: [`Value` {"," `Value`}*] + NamedArgValueList: [`NameValue` "=" `Value` {"," `NameValue` "=" `Value`}*] A :token:`ParentClassList` containing a :token:`MultiClassID` is valid only in the class list of a ``defm`` statement. In that case, the ID must be the name of a multiclass. +The argument values can be specified in two forms: + +* Positional argument (``value``). The value is assigned to the argument in the + corresponding position. For ``Foo``, ``a0`` will be assigned to first + argument and ``a1`` will be assigned to second argument. +* Named argument (``name=value``). The value is assigned to the argument with + the specified name. For ``Foo``, ``a0`` will be assigned to the + argument with name ``a`` and ``a1`` will be assigned to the argument with + name ``b``. + +Required arguments can alse be specified as named argument. + +Note that the argument can only be specified once regardless of the way (named +or positional) to specify and positional arguments should be put before named +arguments. + .. productionlist:: Body: ";" | "{" `BodyItem`* "}" BodyItem: (`Type` | "code") `TokIdentifier` ["=" `Value`] ";" diff --git a/llvm/include/llvm/TableGen/Record.h b/llvm/include/llvm/TableGen/Record.h --- a/llvm/include/llvm/TableGen/Record.h +++ b/llvm/include/llvm/TableGen/Record.h @@ -36,6 +36,7 @@ #include #include #include +#include #include namespace llvm { @@ -482,11 +483,21 @@ }; // Represent an argument. +using ArgAuxType = std::variant; class ArgumentInit : public Init, public FoldingSetNode { +public: + enum Kind { + Positional, + Named, + }; + +private: Init *Value; + ArgAuxType Aux; protected: - explicit ArgumentInit(Init *Value) : Init(IK_ArgumentInit), Value(Value) {} + explicit ArgumentInit(Init *Value, ArgAuxType Aux) + : Init(IK_ArgumentInit), Value(Value), Aux(Aux) {} public: ArgumentInit(const ArgumentInit &) = delete; @@ -496,14 +507,33 @@ RecordKeeper &getRecordKeeper() const { return Value->getRecordKeeper(); } - static ArgumentInit *get(Init *Value); + static ArgumentInit *get(Init *Value, ArgAuxType Aux); + + bool isPositional() const { return Aux.index() == Positional; } + bool isNamed() const { return Aux.index() == Named; } Init *getValue() const { return Value; } + unsigned getIndex() const { + assert(isPositional() && "Should be positional!"); + return std::get(Aux); + } + Init *getName() const { + assert(isNamed() && "Should be named!"); + return std::get(Aux); + } + ArgumentInit *cloneWithValue(Init *Value) const { return get(Value, Aux); } void Profile(FoldingSetNodeID &ID) const; Init *resolveReferences(Resolver &R) const override; - std::string getAsString() const override { return Value->getAsString(); } + std::string getAsString() const override { + if (isPositional()) + return utostr(getIndex()) + ": " + Value->getAsString(); + if (isNamed()) + return getName()->getAsString() + ": " + Value->getAsString(); + llvm_unreachable("Unsupported argument type!"); + return ""; + } bool isComplete() const override { return false; } bool isConcrete() const override { return false; } diff --git a/llvm/lib/TableGen/Record.cpp b/llvm/lib/TableGen/Record.cpp --- a/llvm/lib/TableGen/Record.cpp +++ b/llvm/lib/TableGen/Record.cpp @@ -367,17 +367,24 @@ return const_cast(this); } -static void ProfileArgumentInit(FoldingSetNodeID &ID, Init *Value) { +static void ProfileArgumentInit(FoldingSetNodeID &ID, Init *Value, + ArgAuxType Aux) { + auto I = Aux.index(); + ID.AddInteger(I); + if (I == ArgumentInit::Positional) + ID.AddInteger(std::get(Aux)); + if (I == ArgumentInit::Named) + ID.AddPointer(std::get(Aux)); ID.AddPointer(Value); } void ArgumentInit::Profile(FoldingSetNodeID &ID) const { - ProfileArgumentInit(ID, Value); + ProfileArgumentInit(ID, Value, Aux); } -ArgumentInit *ArgumentInit::get(Init *Value) { +ArgumentInit *ArgumentInit::get(Init *Value, ArgAuxType Aux) { FoldingSetNodeID ID; - ProfileArgumentInit(ID, Value); + ProfileArgumentInit(ID, Value, Aux); RecordKeeper &RK = Value->getRecordKeeper(); detail::RecordKeeperImpl &RKImpl = RK.getImpl(); @@ -385,7 +392,7 @@ if (ArgumentInit *I = RKImpl.TheArgumentInitPool.FindNodeOrInsertPos(ID, IP)) return I; - ArgumentInit *I = new (RKImpl.Allocator) ArgumentInit(Value); + ArgumentInit *I = new (RKImpl.Allocator) ArgumentInit(Value, Aux); RKImpl.TheArgumentInitPool.InsertNode(I, IP); return I; } @@ -393,7 +400,7 @@ Init *ArgumentInit::resolveReferences(Resolver &R) const { Init *NewValue = Value->resolveReferences(R); if (NewValue != Value) - return ArgumentInit::get(NewValue); + return cloneWithValue(NewValue); return const_cast(this); } @@ -2219,13 +2226,16 @@ ArrayRef TArgs = Class->getTemplateArgs(); MapResolver R(NewRec); - for (unsigned i = 0, e = TArgs.size(); i != e; ++i) { - if (i < args_size()) - R.set(TArgs[i], getArg(i)->getValue()); - else - R.set(TArgs[i], NewRec->getValue(TArgs[i])->getValue()); + for (unsigned I = 0, E = TArgs.size(); I != E; ++I) { + R.set(TArgs[I], NewRec->getValue(TArgs[I])->getValue()); + NewRec->removeValue(TArgs[I]); + } - NewRec->removeValue(TArgs[i]); + for (auto *Arg : args()) { + if (Arg->isPositional()) + R.set(TArgs[Arg->getIndex()], Arg->getValue()); + if (Arg->isNamed()) + R.set(Arg->getName(), Arg->getValue()); } NewRec->resolveReferences(R); diff --git a/llvm/lib/TableGen/TGParser.h b/llvm/lib/TableGen/TGParser.h --- a/llvm/lib/TableGen/TGParser.h +++ b/llvm/lib/TableGen/TGParser.h @@ -289,7 +289,8 @@ void ParseValueList(SmallVectorImpl &Result, Record *CurRec, RecTy *ItemType = nullptr); bool ParseTemplateArgValueList(SmallVectorImpl &Result, - Record *CurRec, Record *ArgsRec); + Record *CurRec, Record *ArgsRec, + bool IsDefm = false); void ParseDagArgList( SmallVectorImpl> &Result, Record *CurRec); diff --git a/llvm/lib/TableGen/TGParser.cpp b/llvm/lib/TableGen/TGParser.cpp --- a/llvm/lib/TableGen/TGParser.cpp +++ b/llvm/lib/TableGen/TGParser.cpp @@ -570,20 +570,35 @@ assert(ArgValues.size() <= ArgNames.size() && "Too many template arguments allowed"); - // Loop over the template argument names. If a value was specified, - // handle the (name, value) pair. If not and there was no default, complain. - for (unsigned I = 0, E = ArgNames.size(); I != E; ++I) { - if (I < ArgValues.size()) - ArgValueHandler(ArgNames[I], ArgValues[I]->getValue()); - else { - Init *Default = Rec->getValue(ArgNames[I])->getValue(); - if (!Default->isComplete()) - return Error(Loc, "Value not specified for template argument '" + - ArgNames[I]->getAsUnquotedString() + "' (#" + - Twine(I) + ") of parent class '" + - Rec->getNameInitAsString() + "'"); - ArgValueHandler(ArgNames[I], Default); + // Loop over the template arguments and handle the (name, value) pair. + SmallVector UnsolvedArgNames(ArgNames); + for (auto *Arg : ArgValues) { + Init *ArgName = nullptr; + Init *ArgValue = Arg->getValue(); + if (Arg->isPositional()) + ArgName = ArgNames[Arg->getIndex()]; + if (Arg->isNamed()) + ArgName = Arg->getName(); + + // We can only specify the template argument once. + if (!is_contained(UnsolvedArgNames, ArgName)) + return Error(Loc, "We can only specify the template argument '" + + ArgName->getAsUnquotedString() + "' once"); + + ArgValueHandler(ArgName, ArgValue); + llvm::erase_value(UnsolvedArgNames, ArgName); + } + + // For unsolved arguments, if there is no default value, complain. + for (auto *UnsolvedArgName : UnsolvedArgNames) { + Init *Default = Rec->getValue(UnsolvedArgName)->getValue(); + if (!Default->isComplete()) { + return Error(Loc, "value not specified for template argument (" + + UnsolvedArgName->getAsUnquotedString() + + ") of multiclass '" + Rec->getNameInitAsString() + + "'"); } + ArgValueHandler(UnsolvedArgName, Default); } return false; @@ -719,7 +734,7 @@ /// multiclass. This returns a SubClassRefTy with a null Record* on error. /// /// SubClassRef ::= ClassID -/// SubClassRef ::= ClassID '<' ValueList '>' +/// SubClassRef ::= ClassID '<' ArgValueList '>' /// SubClassReference TGParser:: ParseSubClassReference(Record *CurRec, bool isDefm) { @@ -740,7 +755,8 @@ return Result; } - if (ParseTemplateArgValueList(Result.TemplateArgs, CurRec, Result.Rec)) { + if (ParseTemplateArgValueList(Result.TemplateArgs, CurRec, Result.Rec, + isDefm)) { Result.Rec = nullptr; // Error parsing value list. return Result; } @@ -760,7 +776,7 @@ /// Record* on error. /// /// SubMultiClassRef ::= MultiClassID -/// SubMultiClassRef ::= MultiClassID '<' ValueList '>' +/// SubMultiClassRef ::= MultiClassID '<' ArgValueList '>' /// SubMultiClassReference TGParser:: ParseSubMultiClassReference(MultiClass *CurMC) { @@ -777,7 +793,7 @@ } if (ParseTemplateArgValueList(Result.TemplateArgs, &CurMC->Rec, - &Result.MC->Rec)) { + &Result.MC->Rec, true)) { Result.MC = nullptr; // Error parsing value list. return Result; } @@ -2579,10 +2595,13 @@ case tgtok::Id: { SMRange NameLoc = Lex.getLocRange(); StringInit *Name = StringInit::get(Records, Lex.getCurStrVal()); - if (Lex.Lex() != tgtok::less) // consume the Id. - return ParseIDValue(CurRec, Name, NameLoc, Mode); // Value ::= IDValue + tgtok::TokKind Next = Lex.Lex(); + if (Next == tgtok::equal) // Named argument. + return Name; + if (Next != tgtok::less) // consume the Id. + return ParseIDValue(CurRec, Name, NameLoc, Mode); // Value ::= IDValue - // Value ::= CLASSID '<' ValueListNE '>' (CLASSID has been consumed) + // Value ::= CLASSID '<' ArgValueList '>' (CLASSID has been consumed) // This is supposed to synthesize a new anonymous definition, deriving // from the class with the template arguments, but no body. Record *Class = Records.getClass(Name->getValue()); @@ -3113,34 +3132,72 @@ // ParseTemplateArgValueList - Parse a template argument list with the syntax // shown, filling in the Result vector. The open angle has been consumed. -// An empty argument list is allowed. Return false if okay, true if an +// An empty argument list is allowed. Return false if okay, true if an // error was detected. // -// TemplateArgList ::= '<' [Value {',' Value}*] '>' +// ArgValueList ::= '<' PostionalArgValueList [','] NamedArgValueList '>' +// PostionalArgValueList ::= [Value {',' Value}*] +// NamedArgValueList ::= [NameValue '=' Value {',' NameValue '=' Value}*] bool TGParser::ParseTemplateArgValueList( - SmallVectorImpl &Result, Record *CurRec, Record *ArgsRec) { - + SmallVectorImpl &Result, Record *CurRec, Record *ArgsRec, + bool IsDefm) { assert(Result.empty() && "Result vector is not empty"); ArrayRef TArgs = ArgsRec->getTemplateArgs(); - unsigned ArgIndex = 0; - RecTy *ItemType; if (consume(tgtok::greater)) // empty value list return false; + bool HasNamedArg = false; + unsigned ArgIndex = 0; while (true) { if (ArgIndex >= TArgs.size()) { TokError("Too many template arguments: " + utostr(ArgIndex + 1)); return true; } - const RecordVal *Arg = ArgsRec->getValue(TArgs[ArgIndex]); - assert(Arg && "Template argument record not found"); - ItemType = Arg->getType(); - Init *Value = ParseValue(CurRec, ItemType); + SMLoc ValueLoc = Lex.getLoc(); + // If we are parsing named argument, we don't need to know the argument name + // and argument type will be resolved after we know the name. + Init *Value = ParseValue( + CurRec, + HasNamedArg ? nullptr : ArgsRec->getValue(TArgs[ArgIndex])->getType()); if (!Value) return true; - Result.push_back(ArgumentInit::get(Value)); + + // If we meet '=', then we are parsing named arguments. + if (Lex.getCode() == tgtok::equal) { + if (!isa(Value)) + return Error(ValueLoc, + "The name of named argument should be a valid identifier"); + + auto *Name = cast(Value); + Init *QualifiedName = + QualifyName(*ArgsRec, CurMultiClass, Name, IsDefm ? "::" : ":"); + auto *NamedArg = ArgsRec->getValue(QualifiedName); + if (!NamedArg) + return Error(ValueLoc, + "Argument " + Name->getAsString() + " doesn't exist"); + + Lex.Lex(); // eat the '='. + ValueLoc = Lex.getLoc(); + Value = ParseValue(CurRec, NamedArg->getType()); + // Named value can't be uninitialized. + if (isa(Value)) + return Error(ValueLoc, + "The value of named argument should be initialized, " + "but we got '" + + Value->getAsString() + "'"); + + Result.push_back(ArgumentInit::get(Value, QualifiedName)); + HasNamedArg = true; + } else { + // Positional arguments should be put before named arguments. + if (HasNamedArg) + return Error(ValueLoc, + "Positional argument should be put before named argument"); + + Result.push_back(ArgumentInit::get(Value, ArgIndex)); + } if (consume(tgtok::greater)) // end of argument list? return false; @@ -4248,9 +4305,15 @@ ArrayRef TArgs = ArgsRec->getTemplateArgs(); for (unsigned I = 0, E = Values.size(); I < E; ++I) { - RecordVal *Arg = ArgsRec->getValue(TArgs[I]); - RecTy *ArgType = Arg->getType(); auto *Value = Values[I]; + Init *ArgName = nullptr; + if (Value->isPositional()) + ArgName = TArgs[Value->getIndex()]; + if (Value->isNamed()) + ArgName = Value->getName(); + + RecordVal *Arg = ArgsRec->getValue(ArgName); + RecTy *ArgType = Arg->getType(); if (TypedInit *ArgValue = dyn_cast(Value->getValue())) { auto *CastValue = ArgValue->getCastTo(ArgType); @@ -4258,14 +4321,13 @@ assert((!isa(CastValue) || cast(CastValue)->getType()->typeIsA(ArgType)) && "result of template arg value cast has wrong type"); - Values[I] = ArgumentInit::get(CastValue); + Values[I] = Value->cloneWithValue(CastValue); } else { - PrintFatalError(Loc, - "Value specified for template argument '" + - Arg->getNameInitAsString() + "' (#" + Twine(I) + - ") is of type " + ArgValue->getType()->getAsString() + - "; expected type " + ArgType->getAsString() + ": " + - ArgValue->getAsString()); + PrintFatalError(Loc, "Value specified for template argument '" + + Arg->getNameInitAsString() + "' is of type " + + ArgValue->getType()->getAsString() + + "; expected type " + ArgType->getAsString() + + ": " + ArgValue->getAsString()); } } } diff --git a/llvm/test/TableGen/named-arguments.td b/llvm/test/TableGen/named-arguments.td new file mode 100644 --- /dev/null +++ b/llvm/test/TableGen/named-arguments.td @@ -0,0 +1,139 @@ +// RUN: llvm-tblgen %s | FileCheck %s +// RUN: not llvm-tblgen -DERROR1 %s 2>&1 | FileCheck --check-prefix=ERROR1 %s +// RUN: not llvm-tblgen -DERROR2 %s 2>&1 | FileCheck --check-prefix=ERROR2 %s +// RUN: not llvm-tblgen -DERROR3 %s 2>&1 | FileCheck --check-prefix=ERROR3 %s +// RUN: not llvm-tblgen -DERROR4 %s 2>&1 | FileCheck --check-prefix=ERROR4 %s +// RUN: not llvm-tblgen -DERROR5 %s 2>&1 | FileCheck --check-prefix=ERROR5 %s +// RUN: not llvm-tblgen -DERROR6 %s 2>&1 | FileCheck --check-prefix=ERROR6 %s +// RUN: not llvm-tblgen -DERROR7 %s 2>&1 | FileCheck --check-prefix=ERROR7 %s +// RUN: not llvm-tblgen -DERROR8 %s 2>&1 | FileCheck --check-prefix=ERROR8 %s + +class TestClass { + int value = !add(a, b, c); +} +// CHECK: def testClass1 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass2 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass3 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass4 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass5 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass6 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass7 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testClass8 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +def testClass1: TestClass<1>; +def testClass2: TestClass<1, 2>; +def testClass3: TestClass<1, 2, 3>; +def testClass4: TestClass<1, b=2>; +def testClass5: TestClass<1, c=3>; +def testClass6: TestClass<1, b=2, c=3>; +def testClass7: TestClass<1, c=3, b=2>; +def testClass8: TestClass; + +multiclass TestMultiClass { + def "": TestClass; +} + +// CHECK: def testMultiClass1 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass2 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass3 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass4 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass5 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass6 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass7 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +// CHECK: def testMultiClass8 { +// CHECK-NEXT: int value = 6; +// CHECK-NEXT: } +defm testMultiClass1: TestMultiClass<1>; +defm testMultiClass2: TestMultiClass<1, 2>; +defm testMultiClass3: TestMultiClass<1, 2, 3>; +defm testMultiClass4: TestMultiClass<1, b=2>; +defm testMultiClass5: TestMultiClass<1, c=3>; +defm testMultiClass6: TestMultiClass<1, b=2, c=3>; +defm testMultiClass7: TestMultiClass<1, c=3, b=2>; +defm testMultiClass8: TestMultiClass; + +class TestSubroutine{ + int value=!add(a, b); +} + +// CHECK: def testSubroutine { +// CHECK-NEXT: int value1 = 2; +// CHECK-NEXT: int value2 = 2; +// CHECK-NEXT: int value3 = 2; +// CHECK-NEXT: } +def testSubroutine { + int value1=TestSubroutine<1>.value; + int value2=TestSubroutine<1, b=1>.value; + int value3=TestSubroutine.value; +} + +#ifdef ERROR1 +// ERROR1: Argument "d" doesn't exist +def testError1: TestClass<1, d=3>; +#endif + +#ifdef ERROR2 +// ERROR2: The name of named argument should be a valid identifier +def testError2: TestClass<1, 3=0>; +#endif + +#ifdef ERROR3 +// ERROR3: Positional argument should be put before named argument +def testError3: TestClass<1, b=1, 2>; +#endif + +#ifdef ERROR4 +// ERROR4: The value of named argument should be initialized, but we got '?' +def testError4: TestClass<1, b=?>; +#endif + +#ifdef ERROR5 +// ERROR5: We can only specify the template argument 'TestClass:a' once +def testError5: TestClass<1, a=1>; +#endif + +#ifdef ERROR6 +// ERROR6: We can only specify the template argument 'TestMultiClass::a' once +defm testError6: TestMultiClass<1, a=1>; +#endif + +#ifdef ERROR7 +// ERROR7: We can only specify the template argument 'TestSubroutine:a' once +def testError7 { + int value=TestSubroutine<1, a=1>.value; +} +#endif + +#ifdef ERROR8 +// ERROR8: We can only specify the template argument 'TestClass:b' once +def testError8: TestClass; +#endif diff --git a/llvm/test/TableGen/template-args.td b/llvm/test/TableGen/template-args.td --- a/llvm/test/TableGen/template-args.td +++ b/llvm/test/TableGen/template-args.td @@ -25,7 +25,7 @@ } #ifdef ERROR1 -// ERROR1: Value specified for template argument 'Class1:nm' (#0) is of type int +// ERROR1: Value specified for template argument 'Class1:nm' is of type int def Rec2 : Class1<42> { } @@ -52,7 +52,7 @@ } #ifdef ERROR2 -// ERROR2: Value specified for template argument 'Class2:cd' (#0) is of type string +// ERROR2: Value specified for template argument 'Class2:cd' is of type string def Rec5 : Class2<"oops"> { list CodeList = [Code]; @@ -69,7 +69,7 @@ } #ifdef ERROR3 -// ERROR3: Value specified for template argument 'Class1:nm' (#0) is of type int +// ERROR3: Value specified for template argument 'Class1:nm' is of type int def Rec7 { string Name = Class1<42>.Name; @@ -84,7 +84,7 @@ } #ifdef ERROR4 -// ERROR4: Value specified for template argument 'Class2:cd' (#0) is of type string +// ERROR4: Value specified for template argument 'Class2:cd' is of type string def Rec9 { list CodeList = [Class2<"huh?">.Code]; @@ -110,7 +110,7 @@ defm RecMC1 : MC1<"Carol">; #ifdef ERROR5 -// ERROR5: Value specified for template argument 'MC1::nm' (#0) is of type int +// ERROR5: Value specified for template argument 'MC1::nm' is of type int defm RecMC2 : MC1<42>; #endif @@ -137,7 +137,7 @@ defm RecMC3 : MC2<42>; #ifdef ERROR6 -// ERROR6: Value specified for template argument 'MC2::cd' (#0) is of type string +// ERROR6: Value specified for template argument 'MC2::cd' is of type string defm RecMC4 : MC2<"Bob">; #endif