mirror of
https://bitbucket.org/smil3y/kde-extraapps.git
synced 2025-02-25 03:12:53 +00:00
668 lines
22 KiB
C++
668 lines
22 KiB
C++
/***************************************************************************
|
|
* This file is part of KDevelop *
|
|
* Copyright 2008 Andreas Pakulat <apaku@gmx.de> *
|
|
* Copyright 2006 Roberto Raggi <roberto@kdevelop.org> *
|
|
* Copyright 2006-2008 Hamish Rodda <rodda@kde.org> *
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU Library General Public License as *
|
|
* published by the Free Software Foundation; either version 2 of the *
|
|
* License, or (at your option) any later version. *
|
|
* *
|
|
* This program is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Library General Public *
|
|
* License along with this program; if not, write to the *
|
|
* Free Software Foundation, Inc., *
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
|
|
***************************************************************************/
|
|
|
|
#ifndef KDEVPLATFORM_ABSTRACTABSTRACTCONTEXTBUILDER_H
|
|
#define KDEVPLATFORM_ABSTRACTABSTRACTCONTEXTBUILDER_H
|
|
|
|
#include <climits>
|
|
|
|
#include <QtCore/qmutex.h>
|
|
|
|
#include <ktexteditor/range.h>
|
|
|
|
#include "../topducontext.h"
|
|
#include "../duchainpointer.h"
|
|
#include "../duchainlock.h"
|
|
#include "../duchain.h"
|
|
#include "../ducontext.h"
|
|
#include "../identifier.h"
|
|
#include "../indexedstring.h"
|
|
#include "../parsingenvironment.h"
|
|
|
|
namespace KDevelop
|
|
{
|
|
/**
|
|
* \short Abstract definition-use chain context builder class
|
|
*
|
|
* The AbstractContextBuilder is a convenience class template for creating customized
|
|
* definition-use chain context builders from an AST. It simplifies:
|
|
* - use of your editor integrator
|
|
* - creating or modifying an existing DUContext tree
|
|
* - following a DUContext tree for second and subsequent passes, if required
|
|
* - opening and closing DUContext instances
|
|
* - tracking which DUContext instances are still present when recompiling, and removing DUContexts which no longer exist in the source code.
|
|
*
|
|
* \author Hamish Rodda \<rodda@kde.org\>
|
|
*/
|
|
template<typename T, typename NameT>
|
|
class AbstractContextBuilder
|
|
{
|
|
public:
|
|
/// Constructor.
|
|
AbstractContextBuilder() : m_compilingContexts( false )
|
|
, m_recompiling( false )
|
|
, m_lastContext( 0 )
|
|
{
|
|
}
|
|
|
|
/// Destructor. Deletes the editor integrator, if one was created specifically for this builder only.
|
|
virtual ~AbstractContextBuilder()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Entry point for building a definition-use chain with this builder.
|
|
*
|
|
* This function determines whether we are updating a chain, or creating a new one. If we are
|
|
* creating a new chain, a new TopDUContext is created and registered with DUChain.
|
|
*
|
|
* \param url Url of the document being parsed.
|
|
* \param node AST node to start building from.
|
|
* \param updateContext TopDUContext to update if a duchain was previously created for this url, otherwise pass a null pointer.
|
|
*
|
|
* \returns the newly created or updated TopDUContext pointer.
|
|
*/
|
|
virtual ReferencedTopDUContext build( const IndexedString& url, T* node,
|
|
ReferencedTopDUContext updateContext
|
|
= ReferencedTopDUContext() )
|
|
{
|
|
m_compilingContexts = true;
|
|
m_url = url;
|
|
|
|
ReferencedTopDUContext top;
|
|
{
|
|
DUChainWriteLocker lock( DUChain::lock() );
|
|
top = updateContext.data();
|
|
|
|
if( top )
|
|
{
|
|
m_recompiling = true;
|
|
Q_ASSERT(top->type() == DUContext::Global);
|
|
Q_ASSERT(DUChain::self()->chainForIndex(top->ownIndex()) == top);
|
|
}
|
|
else
|
|
{
|
|
top = newTopContext( RangeInRevision( CursorInRevision( 0, 0 ), CursorInRevision( INT_MAX, INT_MAX ) ) );
|
|
DUChain::self()->addDocumentChain( top );
|
|
top->setType( DUContext::Global );
|
|
}
|
|
|
|
setEncountered( top );
|
|
setContextOnNode( node, top );
|
|
}
|
|
|
|
supportBuild( node, top );
|
|
|
|
m_compilingContexts = false;
|
|
return top;
|
|
}
|
|
|
|
protected:
|
|
/**
|
|
* Support another builder by tracking the current context.
|
|
* @param context the context to use. Must be set when the given node has no context. When it has one attached, this parameter is not needed.
|
|
*/
|
|
virtual void supportBuild( T* node, DUContext* context = 0 )
|
|
{
|
|
if (!context)
|
|
context = contextFromNode(node);
|
|
|
|
Q_ASSERT(context);
|
|
|
|
openContext( context );
|
|
|
|
startVisiting(node);
|
|
|
|
closeContext();
|
|
|
|
Q_ASSERT(m_contextStack.isEmpty());
|
|
}
|
|
|
|
/**
|
|
* Entry point to your visitor. Reimplement and call the appropriate visit function.
|
|
*
|
|
* \param node AST node to visit.
|
|
*/
|
|
virtual void startVisiting( T* node ) = 0;
|
|
|
|
/**
|
|
* Associate a \a context with a given AST \a node. Once called on a \a node, the
|
|
* contextFromNode() function should return this \a context when called.
|
|
*
|
|
* \param node AST node to associate
|
|
* \param context DUContext to associate
|
|
*/
|
|
virtual void setContextOnNode( T* node, DUContext* context ) = 0;
|
|
|
|
/**
|
|
* Retrieve an associated DUContext from the given \a node. Used on second and
|
|
* subsequent passes of the context builder (for supporting other builds)
|
|
*
|
|
* \param node AST node which was previously associated
|
|
* \returns the DUContext which was previously associated
|
|
*/
|
|
virtual DUContext* contextFromNode( T* node ) = 0;
|
|
|
|
/**
|
|
* Retrieves a text range from the given nodes.
|
|
*
|
|
* As editor integrators have to be extended to determine ranges from AST nodes,
|
|
* this function must be reimplemented to allow generic retrieving of rangs from nodes.
|
|
*
|
|
* \param fromNode the AST node to start from (on the start boundary)
|
|
* \param toNode the AST node to end at (on the end boundary)
|
|
*
|
|
* \returns the text range encompassing the given AST node(s)
|
|
*/
|
|
virtual RangeInRevision editorFindRange( T* fromNode, T* toNode ) = 0;
|
|
|
|
/**
|
|
* Retrieve a text range for the given nodes. This is a special function required
|
|
* by c++ support as a different range may need to be retrieved depending on
|
|
* whether macros are involved. It is not usually required to implement this
|
|
* function separately to editorFindRange() for other languages.
|
|
*
|
|
* \param fromNode the AST node to start from (on the start boundary)
|
|
* \param toNode the AST node to end at (on the end boundary)
|
|
*
|
|
* \returns the text range encompassing the given AST node(s)
|
|
*/
|
|
virtual RangeInRevision editorFindRangeForContext( T* fromNode, T* toNode )
|
|
{
|
|
return editorFindRange(fromNode, toNode);
|
|
}
|
|
|
|
/**
|
|
* Determine the QualifiedIdentifier which corresponds to the given ast \a node.
|
|
*
|
|
* \param node ast node which represents an identifier
|
|
* \return the qualified identifier determined from \a node
|
|
*/
|
|
virtual QualifiedIdentifier identifierForNode( NameT* node ) = 0;
|
|
|
|
/**
|
|
* Create a new DUContext from the given \a range.
|
|
*
|
|
* This exists so that you can create custom DUContext subclasses for your
|
|
* language if you need to.
|
|
*
|
|
* \param range range for the new context to encompass
|
|
* \returns the newly created context
|
|
*/
|
|
virtual DUContext* newContext(const RangeInRevision& range)
|
|
{
|
|
return new DUContext(range, currentContext());
|
|
}
|
|
|
|
/**
|
|
* Create a new TopDUContext from the given \a range.
|
|
*
|
|
* This exists so that you can create custom TopDUContext subclasses for your
|
|
* language if you need to.
|
|
*
|
|
* \returns the newly created context
|
|
*/
|
|
virtual TopDUContext* newTopContext(const RangeInRevision& range, ParsingEnvironmentFile* file = 0)
|
|
{
|
|
return new TopDUContext(m_url, range, file);
|
|
}
|
|
|
|
/// Determine the currently open context. \returns the current context.
|
|
inline DUContext* currentContext() const { return m_contextStack.top(); }
|
|
/// Determine the last closed context. \returns the last closed context.
|
|
inline DUContext* lastContext() const { return m_lastContext; }
|
|
/// Clears the last closed context.
|
|
inline void clearLastContext() { m_lastContext = 0; }
|
|
|
|
inline void setLastContext(DUContext* context) { m_lastContext = context; }
|
|
|
|
TopDUContext* topContext() const {
|
|
return currentContext()->topContext();
|
|
}
|
|
|
|
/**
|
|
* Determine if we are recompiling an existing definition-use chain, or if
|
|
* a new chain is being created from scratch.
|
|
*
|
|
* \returns true if an existing duchain is being updated, otherwise false.
|
|
*/
|
|
inline bool recompiling() const { return m_recompiling; }
|
|
|
|
/**
|
|
* Tell the context builder whether we are recompiling an existing definition-use chain, or if
|
|
* a new chain is being created from scratch.
|
|
*
|
|
* \param recomp set to true if an existing duchain is being updated, otherwise false.
|
|
*/
|
|
inline void setRecompiling(bool recomp) { m_recompiling = recomp; }
|
|
|
|
/**
|
|
* Determine whether this pass will create DUContext instances.
|
|
*
|
|
* On the first pass of definition-use chain compiling, DUContext instances
|
|
* are created to represent contexts in the source code. These contexts are
|
|
* associated with their AST nodes at the time (see setContextOnNode()).
|
|
*
|
|
* On second and subsequent passes, the contexts already exist and thus can be
|
|
* retrieved through contextFromNode().
|
|
*
|
|
* \returns true if compiling contexts (ie. 1st pass), otherwise false.
|
|
*/
|
|
inline bool compilingContexts() const { return m_compilingContexts; }
|
|
|
|
/**
|
|
* Sets whether we need to create ducontexts, ie. if this is the first pass.
|
|
*
|
|
* \sa compilingContexts()
|
|
*/
|
|
inline void setCompilingContexts(bool compilingContexts) { m_compilingContexts = compilingContexts; }
|
|
|
|
/**
|
|
* Create child contexts for only a portion of the document.
|
|
*
|
|
* \param node The AST node which corresponds to the context to parse
|
|
* \param parent The DUContext which encompasses the \a node.
|
|
* \returns The DUContext which was reparsed, ie. \a parent.
|
|
*/
|
|
DUContext* buildSubContexts( T *node, DUContext* parent )
|
|
{
|
|
// m_compilingContexts = true;
|
|
// m_recompiling = false;
|
|
setContextOnNode( node, parent );
|
|
{
|
|
openContext( contextFromNode( node ) );
|
|
startVisiting( node );
|
|
closeContext();
|
|
}
|
|
|
|
m_compilingContexts = false;
|
|
|
|
if ( contextFromNode( node ) == parent )
|
|
{
|
|
kDebug() << "Error in AbstractContextBuilder::buildSubContexts(...): du-context was not replaced with new one";
|
|
DUChainWriteLocker lock( DUChain::lock() );
|
|
deleteContextOnNode( node );
|
|
}
|
|
|
|
return contextFromNode( node );
|
|
}
|
|
|
|
/**
|
|
* Delete the DUContext which is associated with the given \a node,
|
|
* and remove the association.
|
|
*
|
|
* \param node Node which is associated with the context to delete.
|
|
*/
|
|
void deleteContextOnNode( T* node )
|
|
{
|
|
delete contextFromNode( node );
|
|
setContextOnNode( node, 0 );
|
|
}
|
|
|
|
/**
|
|
* Open a context, and create / update it if necessary.
|
|
*
|
|
* \param rangeNode The range which encompasses the context.
|
|
* \param type The type of context to open.
|
|
* \param identifier The range which encompasses the name of this context, if one exists.
|
|
* \returns the opened context.
|
|
*/
|
|
DUContext* openContext( T* rangeNode, DUContext::ContextType type, NameT* identifier = 0)
|
|
{
|
|
if ( m_compilingContexts )
|
|
{
|
|
DUContext* ret = openContextInternal( editorFindRangeForContext( rangeNode, rangeNode ), type, identifier ? identifierForNode( identifier ) : QualifiedIdentifier() );
|
|
setContextOnNode( rangeNode, ret );
|
|
return ret;
|
|
}
|
|
else
|
|
{
|
|
openContext( contextFromNode(rangeNode) );
|
|
return currentContext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a context, and create / update it if necessary.
|
|
*
|
|
* \param node The range to associate with the context.
|
|
* \param range A custom range which the context should encompass.
|
|
* \param type The type of context to open.
|
|
* \param identifier The range which encompasses the name of this context, if one exists.
|
|
* \returns the opened context.
|
|
*/
|
|
DUContext* openContext(T* node, const RangeInRevision& range, DUContext::ContextType type, NameT* identifier = 0)
|
|
{
|
|
if (m_compilingContexts) {
|
|
DUContext* ret = openContextInternal(range, type, identifier ? identifierForNode(identifier) : QualifiedIdentifier());
|
|
setContextOnNode( node, ret );
|
|
return ret;
|
|
|
|
} else {
|
|
openContext( contextFromNode(node) );
|
|
return currentContext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a context, and create / update it if necessary.
|
|
*
|
|
* \param node The range to associate with the context.
|
|
* \param range A custom range which the context should encompass.
|
|
* \param type The type of context to open.
|
|
* \param identifier The identifier for this context
|
|
* \returns the opened context.
|
|
*/
|
|
DUContext* openContext(T* node, const RangeInRevision& range, DUContext::ContextType type, const QualifiedIdentifier& id)
|
|
{
|
|
if (m_compilingContexts) {
|
|
DUContext* ret = openContextInternal(range, type, id);
|
|
setContextOnNode( node, ret );
|
|
return ret;
|
|
|
|
} else {
|
|
openContext( contextFromNode(node) );
|
|
return currentContext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a context, and create / update it if necessary.
|
|
*
|
|
* \param rangeNode The range which encompasses the context.
|
|
* \param type The type of context to open.
|
|
* \param identifier The identifier which corresponds to the context.
|
|
* \returns the opened context.
|
|
*/
|
|
DUContext* openContext( T* rangeNode, DUContext::ContextType type, const QualifiedIdentifier& identifier )
|
|
{
|
|
if ( m_compilingContexts )
|
|
{
|
|
DUContext* ret = openContextInternal( editorFindRangeForContext( rangeNode, rangeNode ), type, identifier );
|
|
setContextOnNode( rangeNode, ret );
|
|
return ret;
|
|
}
|
|
else
|
|
{
|
|
openContext( contextFromNode(rangeNode) );
|
|
return currentContext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a context, and create / update it if necessary.
|
|
*
|
|
* \param fromRange The range which starts the context.
|
|
* \param toRange The range which ends the context.
|
|
* \param type The type of context to open.
|
|
* \param identifier The identifier which corresponds to the context.
|
|
* \returns the opened context.
|
|
*/
|
|
DUContext* openContext( T* fromRange, T* toRange, DUContext::ContextType type, const QualifiedIdentifier& identifier = QualifiedIdentifier() )
|
|
{
|
|
if ( m_compilingContexts )
|
|
{
|
|
DUContext* ret = openContextInternal( editorFindRangeForContext( fromRange, toRange ), type, identifier );
|
|
setContextOnNode( fromRange, ret );
|
|
return ret;
|
|
}
|
|
else
|
|
{
|
|
openContext( contextFromNode(fromRange) );
|
|
return currentContext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open a newly created or previously existing context.
|
|
*
|
|
* The open context is put on the context stack, and becomes the new
|
|
* currentContext().
|
|
*
|
|
* \warning When you call this, you also have to open a range! If you want to re-use
|
|
* the range associated to the context, use injectContext
|
|
*
|
|
* \param newContext Context to open.
|
|
*/
|
|
virtual void openContext( DUContext* newContext )
|
|
{
|
|
m_contextStack.push( newContext );
|
|
m_nextContextStack.push( 0 );
|
|
}
|
|
|
|
/**
|
|
* This can be used to temporarily change the current context.
|
|
* \param range The range that will be used as new current range, or zero(then the range associated to the context is used)
|
|
* */
|
|
void injectContext( DUContext* ctx ) {
|
|
openContext( ctx );
|
|
}
|
|
|
|
/**
|
|
* Use this to close the context previously injected with injectContext.
|
|
* */
|
|
void closeInjectedContext() {
|
|
m_contextStack.pop();
|
|
m_nextContextStack.pop();
|
|
}
|
|
|
|
/**
|
|
* Close the current DUContext. When recompiling, this function will remove any
|
|
* contexts that were not encountered in this passing run.
|
|
* \note The DUChain write lock is already held here.
|
|
*/
|
|
virtual void closeContext()
|
|
{
|
|
{
|
|
DUChainWriteLocker lock( DUChain::lock() );
|
|
//Remove all slaves that were not encountered while parsing
|
|
if(m_compilingContexts)
|
|
currentContext()->cleanIfNotEncountered( m_encountered );
|
|
setEncountered( currentContext() );
|
|
|
|
m_lastContext = currentContext();
|
|
}
|
|
|
|
m_contextStack.pop();
|
|
m_nextContextStack.pop();
|
|
}
|
|
|
|
/**
|
|
* Remember that a specific item has been encoutered while parsing.
|
|
* All items that are not encountered will be deleted at some stage.
|
|
*
|
|
* \param item duchain item that was encountered.
|
|
* */
|
|
void setEncountered( DUChainBase* item )
|
|
{
|
|
m_encountered.insert( item );
|
|
}
|
|
|
|
/**
|
|
* Determine whether the given \a item is in the set of encountered items.
|
|
*
|
|
* @return true if the \a item has been encountered, otherwise false.
|
|
* */
|
|
bool wasEncountered( DUChainBase* item )
|
|
{
|
|
return m_encountered.contains( item );
|
|
}
|
|
|
|
/**
|
|
* Set the current identifier to \a id.
|
|
*
|
|
* \param id the new current identifier.
|
|
*/
|
|
void setIdentifier( const QString& id )
|
|
{
|
|
m_identifier = Identifier( id );
|
|
m_qIdentifier.push( m_identifier );
|
|
}
|
|
|
|
/**
|
|
* Determine the current identifier.
|
|
* \returns the current identifier.
|
|
*/
|
|
QualifiedIdentifier qualifiedIdentifier() const
|
|
{
|
|
return m_qIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Clears the current identifier.
|
|
*/
|
|
void clearQualifiedIdentifier()
|
|
{
|
|
m_qIdentifier.clear();
|
|
}
|
|
|
|
/**
|
|
* Retrieve the current context stack. This function is not expected
|
|
* to be used often and may be phased out.
|
|
*
|
|
* \todo Audit whether access to the context stack is still required, and provide
|
|
* replacement functionality if possible.
|
|
*/
|
|
const QStack<DUContext*>& contextStack() const
|
|
{
|
|
return m_contextStack;
|
|
}
|
|
|
|
/**
|
|
* Access the index of the child context which has been encountered.
|
|
*
|
|
* \todo further delineate the role of this function and rename / document better.
|
|
* \todo make private again?
|
|
*/
|
|
int& nextContextIndex()
|
|
{
|
|
return m_nextContextStack.top();
|
|
}
|
|
|
|
/**
|
|
* Open a context, either creating it if it does not exist, or referencing a previously existing
|
|
* context if already encountered in a previous duchain parse run (when recompiling()).
|
|
*
|
|
* \param range The range of the context.
|
|
* \param type The type of context to create.
|
|
* \param identifier The identifier which corresponds to the context.
|
|
* \returns the opened context.
|
|
*/
|
|
|
|
virtual DUContext* openContextInternal( const RangeInRevision& range, DUContext::ContextType type, const QualifiedIdentifier& identifier )
|
|
{
|
|
Q_ASSERT( m_compilingContexts );
|
|
DUContext* ret = 0L;
|
|
|
|
{
|
|
if ( recompiling() )
|
|
{
|
|
DUChainReadLocker readLock( DUChain::lock() );
|
|
const QVector<DUContext*>& childContexts = currentContext()->childContexts();
|
|
|
|
int currentIndex = nextContextIndex();
|
|
|
|
for ( ; currentIndex < childContexts.count(); ++currentIndex )
|
|
{
|
|
DUContext* child = childContexts.at( currentIndex );
|
|
RangeInRevision childRange = child->range();
|
|
|
|
if (child->type() != type) {
|
|
continue;
|
|
}
|
|
// We cannot update a contexts local scope identifier, that will break many other parts, like e.g.
|
|
// the CodeModel of child contexts or declarations.
|
|
// For unnamed child-ranges, we still do range-comparison, because we cannot distinguish them in other ways
|
|
if ((!identifier.isEmpty() && child->localScopeIdentifier() == identifier)
|
|
|| (identifier.isEmpty() && child->localScopeIdentifier().isEmpty() && !childRange.isEmpty() && childRange == range))
|
|
{
|
|
// Match
|
|
ret = child;
|
|
readLock.unlock();
|
|
DUChainWriteLocker writeLock( DUChain::lock() );
|
|
|
|
ret->clearImportedParentContexts();
|
|
++currentIndex;
|
|
break;
|
|
}
|
|
}
|
|
if(ret)
|
|
nextContextIndex() = currentIndex; //If we had a match, jump forward to that position
|
|
///@todo We should also somehow make sure we don't get quadratic worst-case effort while updating.
|
|
}
|
|
|
|
if ( !ret )
|
|
{
|
|
DUChainWriteLocker writeLock( DUChain::lock() );
|
|
|
|
ret = newContext( range );
|
|
ret->setType( type );
|
|
|
|
if ( !identifier.isEmpty() )
|
|
ret->setLocalScopeIdentifier( identifier );
|
|
|
|
setInSymbolTable( ret );
|
|
}else{
|
|
DUChainWriteLocker writeLock( DUChain::lock() );
|
|
Q_ASSERT(ret->localScopeIdentifier() == identifier);
|
|
if(ret->parentContext())
|
|
ret->setRange( range );
|
|
}
|
|
}
|
|
|
|
m_encountered.insert( ret );
|
|
openContext( ret );
|
|
return ret;
|
|
}
|
|
|
|
///This function should call context->setInSymbolTable(..) with an appropriate decision. The duchain is write-locked when this is called.
|
|
virtual void setInSymbolTable(DUContext* context) {
|
|
if(!context->parentContext()->inSymbolTable()) {
|
|
context->setInSymbolTable(false);
|
|
return;
|
|
}
|
|
DUContext::ContextType type = context->type();
|
|
context->setInSymbolTable(type == DUContext::Class || type == DUContext::Namespace || type == DUContext::Global || type == DUContext::Helper || type == DUContext::Enum);
|
|
}
|
|
|
|
/// @returns the current url/path ot the document we are parsing
|
|
IndexedString document() const {
|
|
return m_url;
|
|
}
|
|
|
|
private:
|
|
|
|
Identifier m_identifier;
|
|
IndexedString m_url;
|
|
QualifiedIdentifier m_qIdentifier;
|
|
bool m_compilingContexts : 1;
|
|
bool m_recompiling : 1;
|
|
QStack<int> m_nextContextStack;
|
|
DUContext* m_lastContext;
|
|
//Here all valid declarations/uses/... will be collected
|
|
QSet<DUChainBase*> m_encountered;
|
|
QStack<DUContext*> m_contextStack;
|
|
};
|
|
|
|
}
|
|
|
|
#endif
|