643 lines
18 KiB
C++
643 lines
18 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/*
|
|
* This file is part of the LibreOffice project.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*
|
|
* This file incorporates work covered by the following license notice:
|
|
*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed
|
|
* with this work for additional information regarding copyright
|
|
* ownership. The ASF licenses this file to you under the Apache
|
|
* License, Version 2.0 (the "License"); you may not use this file
|
|
* except in compliance with the License. You may obtain a copy of
|
|
* the License at http://www.apache.org/licenses/LICENSE-2.0 .
|
|
*/
|
|
|
|
|
|
#include <parser.hxx>
|
|
#include <memory>
|
|
|
|
#include <basic/sberrors.hxx>
|
|
#include <basic/sbmod.hxx>
|
|
|
|
#include <rtl/ustrbuf.hxx>
|
|
|
|
static bool EndsIfBranch(SbiToken eTok)
|
|
{
|
|
return eTok == ELSEIF || eTok == ELSE || eTok == ENDIF;
|
|
}
|
|
|
|
// Single-line IF and Multiline IF
|
|
|
|
void SbiParser::If()
|
|
{
|
|
sal_uInt32 nEndLbl;
|
|
SbiToken eTok = NIL;
|
|
// ignore end-tokens
|
|
SbiExpression aCond( this );
|
|
aCond.Gen();
|
|
TestToken( THEN );
|
|
if( IsEoln( Next() ) )
|
|
{
|
|
// At the end of each block a jump to ENDIF must be inserted,
|
|
// so that the condition is not evaluated again at ELSEIF.
|
|
// The table collects all jump points.
|
|
#define JMP_TABLE_SIZE 100
|
|
sal_uInt32 pnJmpToEndLbl[JMP_TABLE_SIZE]; // 100 ELSEIFs allowed
|
|
sal_uInt16 iJmp = 0; // current table index
|
|
|
|
// multiline IF
|
|
nEndLbl = aGen.Gen( SbiOpcode::JUMPF_, 0 );
|
|
eTok = Peek();
|
|
while (!EndsIfBranch(eTok) && !bAbort && Parse())
|
|
{
|
|
eTok = Peek();
|
|
if( IsEof() )
|
|
{
|
|
Error( ERRCODE_BASIC_BAD_BLOCK, IF ); bAbort = true; return;
|
|
}
|
|
}
|
|
while( eTok == ELSEIF )
|
|
{
|
|
// jump to ENDIF in case of a successful IF/ELSEIF
|
|
if( iJmp >= JMP_TABLE_SIZE )
|
|
{
|
|
Error( ERRCODE_BASIC_PROG_TOO_LARGE ); bAbort = true; return;
|
|
}
|
|
pnJmpToEndLbl[iJmp++] = aGen.Gen( SbiOpcode::JUMP_, 0 );
|
|
|
|
Next();
|
|
aGen.BackChain( nEndLbl );
|
|
|
|
aGen.Statement();
|
|
auto pCond = std::make_unique<SbiExpression>( this );
|
|
pCond->Gen();
|
|
nEndLbl = aGen.Gen( SbiOpcode::JUMPF_, 0 );
|
|
pCond.reset();
|
|
TestToken( THEN );
|
|
eTok = Peek();
|
|
while (!EndsIfBranch(eTok) && !bAbort && Parse())
|
|
{
|
|
eTok = Peek();
|
|
if( IsEof() )
|
|
{
|
|
Error( ERRCODE_BASIC_BAD_BLOCK, ELSEIF ); bAbort = true; return;
|
|
}
|
|
}
|
|
}
|
|
if( eTok == ELSE )
|
|
{
|
|
Next();
|
|
sal_uInt32 nElseLbl = nEndLbl;
|
|
nEndLbl = aGen.Gen( SbiOpcode::JUMP_, 0 );
|
|
aGen.BackChain( nElseLbl );
|
|
|
|
aGen.Statement();
|
|
StmntBlock( ENDIF );
|
|
}
|
|
else if( eTok == ENDIF )
|
|
Next();
|
|
|
|
|
|
while( iJmp > 0 )
|
|
{
|
|
iJmp--;
|
|
aGen.BackChain( pnJmpToEndLbl[iJmp] );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// single line IF
|
|
bSingleLineIf = true;
|
|
nEndLbl = aGen.Gen( SbiOpcode::JUMPF_, 0 );
|
|
Push( eCurTok );
|
|
// tdf#128263: update push positions to correctly restore in Next()
|
|
nPLine = nLine;
|
|
nPCol1 = nCol1;
|
|
nPCol2 = nCol2;
|
|
|
|
while( !bAbort )
|
|
{
|
|
if( !Parse() ) break;
|
|
eTok = Peek();
|
|
if( eTok == ELSE || eTok == EOLN || eTok == REM )
|
|
break;
|
|
}
|
|
if( eTok == ELSE )
|
|
{
|
|
Next();
|
|
sal_uInt32 nElseLbl = nEndLbl;
|
|
nEndLbl = aGen.Gen( SbiOpcode::JUMP_, 0 );
|
|
aGen.BackChain( nElseLbl );
|
|
while( !bAbort )
|
|
{
|
|
if( !Parse() ) break;
|
|
eTok = Peek();
|
|
if( eTok == EOLN || eTok == REM )
|
|
break;
|
|
}
|
|
}
|
|
bSingleLineIf = false;
|
|
}
|
|
aGen.BackChain( nEndLbl );
|
|
}
|
|
|
|
// ELSE/ELSEIF/ENDIF without IF
|
|
|
|
void SbiParser::NoIf()
|
|
{
|
|
Error( ERRCODE_BASIC_NO_IF );
|
|
StmntBlock( ENDIF );
|
|
}
|
|
|
|
// DO WHILE...LOOP
|
|
// DO ... LOOP WHILE
|
|
|
|
void SbiParser::DoLoop()
|
|
{
|
|
sal_uInt32 nStartLbl = aGen.GetPC();
|
|
OpenBlock( DO );
|
|
SbiToken eTok = Next();
|
|
if( IsEoln( eTok ) )
|
|
{
|
|
// DO ... LOOP [WHILE|UNTIL expr]
|
|
StmntBlock( LOOP );
|
|
eTok = Next();
|
|
if( eTok == UNTIL || eTok == WHILE )
|
|
{
|
|
SbiExpression aExpr( this );
|
|
aExpr.Gen();
|
|
aGen.Gen( eTok == UNTIL ? SbiOpcode::JUMPF_ : SbiOpcode::JUMPT_, nStartLbl );
|
|
} else
|
|
if (eTok == EOLN || eTok == REM)
|
|
aGen.Gen (SbiOpcode::JUMP_, nStartLbl);
|
|
else
|
|
Error( ERRCODE_BASIC_EXPECTED, WHILE );
|
|
}
|
|
else
|
|
{
|
|
// DO [WHILE|UNTIL expr] ... LOOP
|
|
if( eTok == UNTIL || eTok == WHILE )
|
|
{
|
|
SbiExpression aCond( this );
|
|
aCond.Gen();
|
|
}
|
|
sal_uInt32 nEndLbl = aGen.Gen( eTok == UNTIL ? SbiOpcode::JUMPT_ : SbiOpcode::JUMPF_, 0 );
|
|
StmntBlock( LOOP );
|
|
TestEoln();
|
|
aGen.Gen( SbiOpcode::JUMP_, nStartLbl );
|
|
aGen.BackChain( nEndLbl );
|
|
}
|
|
CloseBlock();
|
|
}
|
|
|
|
// WHILE ... WEND
|
|
|
|
void SbiParser::While()
|
|
{
|
|
SbiExpression aCond( this );
|
|
sal_uInt32 nStartLbl = aGen.GetPC();
|
|
aCond.Gen();
|
|
sal_uInt32 nEndLbl = aGen.Gen( SbiOpcode::JUMPF_, 0 );
|
|
StmntBlock( WEND );
|
|
aGen.Gen( SbiOpcode::JUMP_, nStartLbl );
|
|
aGen.BackChain( nEndLbl );
|
|
}
|
|
|
|
// FOR var = expr TO expr STEP
|
|
|
|
void SbiParser::For()
|
|
{
|
|
bool bForEach = ( Peek() == EACH );
|
|
if( bForEach )
|
|
Next();
|
|
SbiExpression aLvalue( this, SbOPERAND );
|
|
if (!aLvalue.IsVariable())
|
|
{
|
|
bAbort = true;
|
|
return; // the error is already set in SbiExpression ctor
|
|
}
|
|
aLvalue.Gen(); // variable on the Stack
|
|
|
|
if( bForEach )
|
|
{
|
|
TestToken( IN_ );
|
|
SbiExpression aCollExpr( this, SbOPERAND );
|
|
aCollExpr.Gen(); // Collection var to for stack
|
|
TestEoln();
|
|
aGen.Gen( SbiOpcode::INITFOREACH_ );
|
|
}
|
|
else
|
|
{
|
|
TestToken( EQ );
|
|
SbiExpression aStartExpr( this );
|
|
aStartExpr.Gen();
|
|
TestToken( TO );
|
|
SbiExpression aStopExpr( this );
|
|
aStopExpr.Gen();
|
|
if( Peek() == STEP )
|
|
{
|
|
Next();
|
|
SbiExpression aStepExpr( this );
|
|
aStepExpr.Gen();
|
|
}
|
|
else
|
|
{
|
|
SbiExpression aOne( this, 1, SbxINTEGER );
|
|
aOne.Gen();
|
|
}
|
|
TestEoln();
|
|
// The stack has all 4 elements now: variable, start, end, increment
|
|
// bind start value
|
|
aGen.Gen( SbiOpcode::INITFOR_ );
|
|
}
|
|
|
|
sal_uInt32 nLoop = aGen.GetPC();
|
|
// do tests, maybe free the stack
|
|
sal_uInt32 nEndTarget = aGen.Gen( SbiOpcode::TESTFOR_, 0 );
|
|
OpenBlock( FOR );
|
|
StmntBlock( NEXT );
|
|
aGen.Gen( SbiOpcode::NEXT_ );
|
|
aGen.Gen( SbiOpcode::JUMP_, nLoop );
|
|
// are there variables after NEXT?
|
|
if( Peek() == SYMBOL )
|
|
{
|
|
SbiExpression aVar( this, SbOPERAND );
|
|
if( aVar.GetRealVar() != aLvalue.GetRealVar() )
|
|
Error( ERRCODE_BASIC_EXPECTED, aLvalue.GetRealVar()->GetName() );
|
|
}
|
|
aGen.BackChain( nEndTarget );
|
|
CloseBlock();
|
|
}
|
|
|
|
// WITH .. END WITH
|
|
|
|
namespace
|
|
{
|
|
// Generate a '{_with_library.module_offset} = rVar'
|
|
// Use the {_with_library.module_offset} in OpenBlock
|
|
// The name of the variable can't be used by user: a name like [{_with_library.module_offset}]
|
|
// is valid, but not without the square brackets
|
|
struct WithLocalVar
|
|
{
|
|
WithLocalVar(SbiParser& rParser, SbiExpression& rVar)
|
|
: m_rParser(rParser)
|
|
, m_aWithParent(createLocalVar(rParser))
|
|
{
|
|
// Assignment
|
|
m_aWithParent.Gen();
|
|
rVar.Gen();
|
|
m_rParser.aGen.Gen(SbiOpcode::PUTC_);
|
|
}
|
|
|
|
~WithLocalVar()
|
|
{
|
|
// {_with_library.module_offset} = Nothing
|
|
m_aWithParent.Gen();
|
|
m_rParser.aGen.Gen(SbiOpcode::RTL_, m_rParser.aGblStrings.Add(u"Nothing"_ustr), SbxOBJECT);
|
|
m_rParser.aGen.Gen(SbiOpcode::PUTC_);
|
|
}
|
|
|
|
static SbiExpression createLocalVar(SbiParser& rParser)
|
|
{
|
|
// Create the unique name
|
|
OUStringBuffer moduleName(rParser.aGen.GetModule().GetName());
|
|
for (auto parent = rParser.aGen.GetModule().GetParent(); parent;
|
|
parent = parent->GetParent())
|
|
moduleName.insert(0, parent->GetName() + ".");
|
|
|
|
OUString uniqueName
|
|
= "{_with_" + moduleName + "_" + OUString::number(rParser.aGen.GetOffset()) + "}";
|
|
while (rParser.pPool->Find(uniqueName) != nullptr)
|
|
{
|
|
static sal_Int64 unique_suffix;
|
|
uniqueName = "{_with_" + moduleName + "_" + OUString::number(rParser.aGen.GetOffset())
|
|
+ "_" + OUString::number(unique_suffix++) + "}";
|
|
}
|
|
SbiSymDef* pWithParentDef = new SbiSymDef(uniqueName);
|
|
pWithParentDef->SetType(SbxOBJECT);
|
|
rParser.pPool->Add(pWithParentDef);
|
|
|
|
// DIM local variable: work with Option Explicit
|
|
rParser.aGen.Gen(SbiOpcode::LOCAL_, pWithParentDef->GetId(), pWithParentDef->GetType());
|
|
|
|
return SbiExpression(&rParser, *pWithParentDef);
|
|
}
|
|
|
|
SbiParser& m_rParser;
|
|
SbiExpression m_aWithParent;
|
|
};
|
|
}
|
|
|
|
void SbiParser::With()
|
|
{
|
|
SbiExpression aVar( this, SbOPERAND );
|
|
|
|
SbiExprNode *pNode = aVar.GetExprNode()->GetRealNode();
|
|
if (!pNode)
|
|
return;
|
|
SbiSymDef* pDef = pNode->GetVar();
|
|
// Variant, from 27.6.1997, #41090: empty -> must be Object
|
|
if( pDef->GetType() == SbxVARIANT || pDef->GetType() == SbxEMPTY )
|
|
pDef->SetType( SbxOBJECT );
|
|
else if( pDef->GetType() != SbxOBJECT )
|
|
Error( ERRCODE_BASIC_NEEDS_OBJECT );
|
|
|
|
pNode->SetType( SbxOBJECT );
|
|
|
|
std::optional<WithLocalVar> oLocalVar;
|
|
if (pDef->GetProcDef())
|
|
oLocalVar.emplace(*this, aVar);
|
|
|
|
OpenBlock(NIL, oLocalVar ? oLocalVar->m_aWithParent.GetExprNode() : aVar.GetExprNode());
|
|
StmntBlock( ENDWITH );
|
|
CloseBlock();
|
|
}
|
|
|
|
// LOOP/NEXT/WEND without construct
|
|
|
|
void SbiParser::BadBlock()
|
|
{
|
|
if( eEndTok )
|
|
Error( ERRCODE_BASIC_BAD_BLOCK, eEndTok );
|
|
else
|
|
Error( ERRCODE_BASIC_BAD_BLOCK, u"Loop/Next/Wend"_ustr );
|
|
}
|
|
|
|
// On expr Goto/Gosub n,n,n...
|
|
|
|
void SbiParser::OnGoto()
|
|
{
|
|
SbiExpression aCond( this );
|
|
aCond.Gen();
|
|
sal_uInt32 nLabelsTarget = aGen.Gen( SbiOpcode::ONJUMP_, 0 );
|
|
SbiToken eTok = Next();
|
|
if( eTok != GOTO && eTok != GOSUB )
|
|
{
|
|
Error( ERRCODE_BASIC_EXPECTED, u"GoTo/GoSub"_ustr );
|
|
eTok = GOTO;
|
|
}
|
|
|
|
sal_uInt32 nLbl = 0;
|
|
do
|
|
{
|
|
Next(); // get label
|
|
if( MayBeLabel() )
|
|
{
|
|
sal_uInt32 nOff = pProc->GetLabels().Reference( aSym );
|
|
aGen.Gen( SbiOpcode::JUMP_, nOff );
|
|
nLbl++;
|
|
}
|
|
else Error( ERRCODE_BASIC_LABEL_EXPECTED );
|
|
}
|
|
while( !bAbort && TestComma() );
|
|
if( eTok == GOSUB )
|
|
nLbl |= 0x8000;
|
|
aGen.Patch( nLabelsTarget, nLbl );
|
|
}
|
|
|
|
// GOTO/GOSUB
|
|
|
|
void SbiParser::Goto()
|
|
{
|
|
SbiOpcode eOp = eCurTok == GOTO ? SbiOpcode::JUMP_ : SbiOpcode::GOSUB_;
|
|
Next();
|
|
if( MayBeLabel() )
|
|
{
|
|
sal_uInt32 nOff = pProc->GetLabels().Reference( aSym );
|
|
aGen.Gen( eOp, nOff );
|
|
}
|
|
else Error( ERRCODE_BASIC_LABEL_EXPECTED );
|
|
}
|
|
|
|
// RETURN [label]
|
|
|
|
void SbiParser::Return()
|
|
{
|
|
Next();
|
|
if( MayBeLabel() )
|
|
{
|
|
sal_uInt32 nOff = pProc->GetLabels().Reference( aSym );
|
|
aGen.Gen( SbiOpcode::RETURN_, nOff );
|
|
}
|
|
else aGen.Gen( SbiOpcode::RETURN_, 0 );
|
|
}
|
|
|
|
// SELECT CASE
|
|
|
|
void SbiParser::Select()
|
|
{
|
|
TestToken( CASE );
|
|
SbiExpression aCase( this );
|
|
SbiToken eTok = NIL;
|
|
aCase.Gen();
|
|
aGen.Gen( SbiOpcode::CASE_ );
|
|
TestEoln();
|
|
sal_uInt32 nNextTarget = 0;
|
|
sal_uInt32 nDoneTarget = 0;
|
|
bool bElse = false;
|
|
|
|
while( !bAbort )
|
|
{
|
|
eTok = Next();
|
|
if( eTok == CASE )
|
|
{
|
|
if( nNextTarget )
|
|
{
|
|
aGen.BackChain( nNextTarget );
|
|
nNextTarget = 0;
|
|
}
|
|
aGen.Statement();
|
|
|
|
bool bDone = false;
|
|
sal_uInt32 nTrueTarget = 0;
|
|
if( Peek() == ELSE )
|
|
{
|
|
// CASE ELSE
|
|
Next();
|
|
bElse = true;
|
|
}
|
|
else while( !bDone )
|
|
{
|
|
if( bElse )
|
|
Error( ERRCODE_BASIC_SYNTAX );
|
|
SbiToken eTok2 = Peek();
|
|
if( eTok2 == IS || ( eTok2 >= EQ && eTok2 <= GE ) )
|
|
{ // CASE [IS] operator expr
|
|
if( eTok2 == IS )
|
|
Next();
|
|
eTok2 = Peek();
|
|
if( eTok2 < EQ || eTok2 > GE )
|
|
Error( ERRCODE_BASIC_SYNTAX );
|
|
else Next();
|
|
SbiExpression aCompare( this );
|
|
aCompare.Gen();
|
|
nTrueTarget = aGen.Gen(
|
|
SbiOpcode::CASEIS_, nTrueTarget,
|
|
sal::static_int_cast< sal_uInt16 >(
|
|
SbxEQ + ( eTok2 - EQ ) ) );
|
|
}
|
|
else
|
|
{ // CASE expr | expr TO expr
|
|
SbiExpression aCase1( this );
|
|
aCase1.Gen();
|
|
if( Peek() == TO )
|
|
{
|
|
// CASE a TO b
|
|
Next();
|
|
SbiExpression aCase2( this );
|
|
aCase2.Gen();
|
|
nTrueTarget = aGen.Gen( SbiOpcode::CASETO_, nTrueTarget );
|
|
}
|
|
else
|
|
// CASE a
|
|
nTrueTarget = aGen.Gen( SbiOpcode::CASEIS_, nTrueTarget, SbxEQ );
|
|
|
|
}
|
|
if( Peek() == COMMA ) Next();
|
|
else
|
|
{
|
|
TestEoln();
|
|
bDone = true;
|
|
}
|
|
}
|
|
|
|
if( !bElse )
|
|
{
|
|
nNextTarget = aGen.Gen( SbiOpcode::JUMP_, nNextTarget );
|
|
aGen.BackChain( nTrueTarget );
|
|
}
|
|
// build the statement body
|
|
while( !bAbort )
|
|
{
|
|
eTok = Peek();
|
|
if( eTok == CASE || eTok == ENDSELECT )
|
|
break;
|
|
if( !Parse() ) goto done;
|
|
eTok = Peek();
|
|
if( eTok == CASE || eTok == ENDSELECT )
|
|
break;
|
|
}
|
|
if( !bElse )
|
|
nDoneTarget = aGen.Gen( SbiOpcode::JUMP_, nDoneTarget );
|
|
}
|
|
else if( !IsEoln( eTok ) )
|
|
break;
|
|
}
|
|
done:
|
|
if( eTok != ENDSELECT )
|
|
Error( ERRCODE_BASIC_EXPECTED, ENDSELECT );
|
|
if( nNextTarget )
|
|
aGen.BackChain( nNextTarget );
|
|
aGen.BackChain( nDoneTarget );
|
|
aGen.Gen( SbiOpcode::ENDCASE_ );
|
|
}
|
|
|
|
// ON Error/Variable
|
|
|
|
void SbiParser::On()
|
|
{
|
|
SbiToken eTok = Peek();
|
|
OUString aString = SbiTokenizer::Symbol(eTok);
|
|
if (aString.equalsIgnoreAsciiCase("ERROR"))
|
|
{
|
|
eTok = ERROR_; // Error comes as SYMBOL
|
|
}
|
|
if( eTok != ERROR_ && eTok != LOCAL )
|
|
{
|
|
OnGoto();
|
|
}
|
|
else
|
|
{
|
|
if( eTok == LOCAL )
|
|
{
|
|
Next();
|
|
}
|
|
Next (); // no more TestToken, as there'd be an error otherwise
|
|
|
|
Next(); // get token after error
|
|
if( eCurTok == GOTO )
|
|
{
|
|
// ON ERROR GOTO label|0
|
|
Next();
|
|
bool bError_ = false;
|
|
if( MayBeLabel() )
|
|
{
|
|
if( eCurTok == NUMBER && !nVal )
|
|
{
|
|
aGen.Gen( SbiOpcode::STDERROR_ );
|
|
}
|
|
else
|
|
{
|
|
sal_uInt32 nOff = pProc->GetLabels().Reference( aSym );
|
|
aGen.Gen( SbiOpcode::ERRHDL_, nOff );
|
|
}
|
|
}
|
|
else if( eCurTok == MINUS )
|
|
{
|
|
Next();
|
|
if( eCurTok == NUMBER && nVal == 1 )
|
|
{
|
|
aGen.Gen( SbiOpcode::STDERROR_ );
|
|
}
|
|
else
|
|
{
|
|
bError_ = true;
|
|
}
|
|
}
|
|
if( bError_ )
|
|
{
|
|
Error( ERRCODE_BASIC_LABEL_EXPECTED );
|
|
}
|
|
}
|
|
else if( eCurTok == RESUME )
|
|
{
|
|
TestToken( NEXT );
|
|
aGen.Gen( SbiOpcode::NOERROR_ );
|
|
}
|
|
else Error( ERRCODE_BASIC_EXPECTED, u"GoTo/Resume"_ustr );
|
|
}
|
|
}
|
|
|
|
// RESUME [0]|NEXT|label
|
|
|
|
void SbiParser::Resume()
|
|
{
|
|
sal_uInt32 nLbl;
|
|
|
|
switch( Next() )
|
|
{
|
|
case EOS:
|
|
case EOLN:
|
|
aGen.Gen( SbiOpcode::RESUME_, 0 );
|
|
break;
|
|
case NEXT:
|
|
aGen.Gen( SbiOpcode::RESUME_, 1 );
|
|
Next();
|
|
break;
|
|
case NUMBER:
|
|
if( !nVal )
|
|
{
|
|
aGen.Gen( SbiOpcode::RESUME_, 0 );
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
case SYMBOL:
|
|
if( MayBeLabel() )
|
|
{
|
|
nLbl = pProc->GetLabels().Reference( aSym );
|
|
aGen.Gen( SbiOpcode::RESUME_, nLbl );
|
|
Next();
|
|
break;
|
|
}
|
|
[[fallthrough]];
|
|
default:
|
|
Error( ERRCODE_BASIC_LABEL_EXPECTED );
|
|
}
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|