今年やったこと2つ。

今年は以前と違ってニートでなかった時期が比較的長く、また「部屋が汚いから自宅勤務をやりたくない」という個人的な理由により、なかなか調査時間を取るのに難航しました。「今年多少なりとも関わってなくもない事」リストを眺めていたところ、何とか一昨年と去年Advent Calendarで解説していないものが2件あるので、一応メモしておきます。

  1. tdf#138646 内容:[ツール]-[オプション]、ツリーの[LibreOffice Calc]-[数式]の[数式の構文]の値の考慮漏れ。XCellRange::getCellRangebyNameの引数に与えられた名前に関連付けられたデータを取得し、範囲に関する情報("A1:A2"など)を文字列で得ることが出来、当該文字列をパースし直して始点や終点となるセルの情報を得るような実装になっているのだが、文字列を返すときの文法と、後でパースするときに考慮する文法が一致しなかったため、RuntimeExceptionが発生していた。
  2. tdf#145117 内容: Hackfestでも多少述べたが、liborcusでは今まで要素名にus-asciiなalphabetsまたはunderscoreしか使えなかったため、一旦はElement name character rangeという修正が入ったものの、これが不十分で、他の箇所で余分に行っていたチェックのせいで、「要素名の最初の文字がus-asciiなalphabetsまたはunderscoreであるとき」に例外が発生する状況が残ってしまっていた。その修正。

Unicode Collation Algorithm

この記事はLibreOffice Advent Calendar 2020の記事です。時間オーバーして2020-12-24に公開していますが、2020-12-23の記事です。

自分がなにか書く時って大抵最近(?)出会ったバグレポをとっかかりにすることが多く、今回もネタとしては、tdf#125363 - UI: LibreOffice Calc's AutoFilter treats combining and modifier letters the same as plain letters in the value listからとなります。自分のtwitterでも確か何度か出していたと思うので、そこからもう既に原因を調べた人には本記事は無用です。

このバグがなぜ起きているかを理解するためには、Unicode Collation Algorithmを理解する必要があり、本記事はそれを解説しようとして能力不足で箇条書きに留まったもの。この記事はLibreOffice Advent Calendarだというのに(汗

文字の並び順は、色んな要素によって変わります。その要素の一つが言語。スウェーデン語もドイツ語もzやöという文字を使いますが、スウェーデン語では普通のアルファベットとは別の文字としてzの後に来ます。ドイツ語の辞書でöはoの仲間として登場するから、zよりも前に来ます。ところで、ドイツ語の辞書ではoはöよりも前に来るのに、アドレス帳ではöはoより前に来ます。また、小文字のほうが先に並ぶようにしたいときもあれば、大文字を先に並べたいときもあるでしょう。「Ж, ש, ♫, ∞, ◊, ⌂」などの文字のように順番が決まってないものもあります。並び方はどんな使い方をするかによって様々に変わります。当然Unicodeのコードポイント順とは異なります。

あ、ちなみにUnicodeで同じ文字列を表現できる方法は複数あるけれど、それらの並び順が変わったりしないように、NFD(Normalization Form D)っていう正規化形式に一旦直してから考えます。

今はもう多言語を同時に扱えること、なんてのはアプリケーションとして一般的なことであって特別なことじゃないけど、どうしよう?ってな動機で作られたのがこの規格です。

DUCET(Default Unicode Collation Element Table)って表を規格側で用意しました。実際には全ての文字列は載っておらず、日本語など、コードポイント等から計算するものもありますが。なお、並び順自体はカスタマイズ可能な方法が残されています。 ユーザーに深く関わるロケールはカスタマイズされることを前提に、それ以外のロケールの文字のそれほど不自然ではない並び順を用意しましょう、と。

この表はフォーマットの仕様によると、11C32 ; [.320D.0020.0002]0E40 0E01 ; [.3217.0020.0002][.3251.0020.0002]のように、基本的には「16進数で表されたコードポイント1つ以上」「区切りとしてセミコロン」「『角括弧内にピリオド区切りされた16進数3つ』のセットが1つ以上」を1行として、これが羅列されます。用語の定義によればこの320Dや0020や0002の非負整数をWeight、 角括弧で区切られた数値の塊をElement、数値の位置をLevelというそうです。このファイルではWeightは3つですが、Variable Weighting(後述)のようにレベル4以上を持つElementを揃えることも出来ます。Level 1のWeightをPrimary Weight, Level 2のWeightをSecondary Weight, Level 3のWeightをTertiary Weight, Level 4のWeightを Quaternary Weightとも言います。同様に、Level 1からLevel 3までのWeightが全て0なElementを Quaternary Collation Element,Level 1からLevel 2までが0なElementをTertiary Collation Element, Level 1が0のElementをSecondary Collation Element, 全てのWeightが非ゼロなときPrimary Collation Elementといいます。

各Levelの大まかな役割は、Level 1が基本文字、Level 2がアクセント、Level 3が大文字小文字、Level 4が句読点等。

これらのElement達はDUCET内で整合性(Well-Formedness)を満たす順番で並んでいます。

  • 一部の特例を除き、あるElementの、自然数なWeightの後には0となるWeightは来ない
  • n個の0であるWeightを飛ばし、最初に自然数となるWeightがあった以降のWeightは、それ以前のElementの同じLevelのWeightよりも大きい
  • VariableなElement(後述)のPrimary Weightは0ではない
  • 2つの異なるVariableなElementの間にあるElementはVariable
  • 言語によっては連続するNのCodePointに、N-1個以下のElementが載っているが、このような事例において、その最後のCodePointが結合文字等Non-Starterであるときは、最後の文字を除いたN-1個の文字もN-2以下のElementが対応する

で、ある文字列があったときに、DUCET第一列に存在する、できるだけ長い部分文字列を、その対応する1以上のElementにして順につなげることを繰り返します。この際、繋げるElementは、アクセントの扱いや、句読点の処理のため、Variable Weightingという処理を通ってからにします。

こうすると一つの文字列から1つ以上のElementが順に並んだものが生成されます。

文字列同士を比較できるキーを生成するため、各ElementのLevel 1のゼロ以外のWeightを連結したもの、それに続けてLevel 2のゼロ以外のWeightを連結したもの、という連結処理を繰り返し、これらのキー同士を比較することで文字列比較に用いることができるようになっています。

後回しにしたVariable Weightingについて。句読点をはじめとした文字達はPrimary Weightの先頭に*がついています。句読点も普通の文字として普通にDUCETのElementをくっつける方法をNon-ignorable、句読点は無いものと見なすため、Elementの全てのWeightを0として扱うのがBlanked、句読点等はほぼ無いものとみなすが句読点等同士については区別するため、0から3番目のWeightを0にしつつ4番目のWeightを設定するのがShiftedというもので、句読点等の扱いを区別したいときのカスタマイズ方法として残されています。

文字列のマッチングについては、各レベルに与えられた役割のうち、どこまでを対象とするかを設定できるようになっています。たとえばLevel 2まで対象とするななら、アクセントのある文字とない文字は別の文字と見なすが、大文字小文字は同じ文字と見なす、というものになります

ここまでの説明が非常に長かったと思いますが、準備は整ったので本題のtdf#125363について短く説明します。CalcにおけるオートフィルタはLevel 2までを対象とするため、 [.2217.0020.0002] [.213C.0020.0002] であるuoと[.2217.0020.0002] [.213C.0020.0004]を同一の文字列とみなします。以上。

WIP:StarBasicのコード(C++)を読もう!

まず、StarBasicのコードがどのように実行されているかを順番に示す。

  1. SbiTokenizerは、StarBasic中の各語(トークン)が何であるかという情報を割り当てる。(字句解析)。なお、次のトークンを切り出す際にはSbiScanner::NextSymの力を借りている。その際色々フラグを立てる(例:数値フラグbNumberが立っていればNUMBER)。
  2. SbiParserはそれぞれの語がどのような順番に並んでいるかという情報から、どの構文であるかを判断する(構文解析)、それと同時にこれまでに宣言した識別子と現在解析中の宣言文の識別子(SYMBOL)が同名でないかやその識別子のスコープを決定する。なお、SbiParser::Peek()は次に読み込むべきトークンの情報を内部の状態を変更せずに取得する。
  3. このとき(例:SbiParser::DoLoop())、SbiExpressionなどを通じて呼ばれるSbiCodeGenは、引数の数に応じたGenメソッドのオーバーロードを用いてアセンブリのコードのようなものを生成する。
  4. ここで生成したコードはSbiRuntimeに渡されSbiRuntime::Stepで処理される。(参考: SbiRuntime::aStep0, SbiRuntime::aStep1, SbiRuntime::aStep2、およびSbiOpCode)
  5. 一例として、関数ポインタの配列たるaStep1に存在するSbiRuntime::StepErrorは色々経由して、nErrorを設定する。
  6. エラーが発生した、つまりnErrorが0でないとき、エラーハンドラが何れの親ランタイムにも設定されていないならばSbiRuntime::Abortを実行する。
  7. コンパイルの問題であるときはSbiParserやSbiTokenizerを、実行時の処理が問題になっているときはSbiRuntimeを見る。

ここまでが前座。似たような追い方をして色々なコードを読んでみよう、という独立した話が以下続く。

65758:If ステートメント中にあるErrorで処理が止まらない

SbiTokenizerでの処理の際、関数名は識別子であり、それ以外はそのトークン用の列挙体メンバが用意されているのだが、トークンによっては同一文字列が識別子として使われることも、構文の一部として登場することもある。一例がErrorであり、前記の通りErrorステートメントのときは、指定された数値に対応したエラーを起こして処理を中断、Error関数のときはそれとは処理が異なり、与えられた引数に対応するエラーメッセージを返すものである。

どのようなトークンとして解釈するかは、SbiParser::Parseに入った直後のPeek()呼び出しの前後でフラグが切り替えられる

bool SbiParser::Parse()
{
    if( bAbort ) return false;

    EnableErrors();

    bErrorIsSymbol = false;
    Peek();
    bErrorIsSymbol = true;
SbiToken SbiTokenizer::Next()
{
        // (中略)
    
        else if( eCurTok >= DATATYPE1 && eCurTok <= DATATYPE2 && (bErrorIsSymbol || eCurTok != ERROR_) )
        {
            eCurTok = SYMBOL;
        }

ところが、SbiParser::Ifの中の条件文でSbiParse::Parse()が帰ってきた「後」にPeekしたトークン判別している。このときのErrorはSYMBOLだからError関数であって、エラー自体を発生させて止まるErrorステートメントとは別物だ。

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( !( eTok == ELSEIF || eTok == ELSE || eTok == ENDIF ) &&
                !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();
            std::unique_ptr<SbiExpression> pCond(new SbiExpression( this ));
            pCond->Gen();
            nEndLbl = aGen.Gen( SbiOpcode::JUMPF_, 0 );
            pCond.reset();
            TestToken( THEN );
            eTok = Peek();
            while( !( eTok == ELSEIF || eTok == ELSE || eTok == ENDIF ) &&
                    !bAbort && Parse() )
            {
                eTok = Peek();
                if( IsEof() )
                {
                    Error( ERRCODE_BASIC_BAD_BLOCK, ELSEIF );  bAbort = true; return;
                }
            }

125627:仮引数と実引数の型が一致しないとき、ByValで、一致するときはByRef

関数の各実引数はpParamやrParamに、SbxVariableのインスタンスとして格納されている。なお、添字0番の領域は戻り値を格納するためのスペースである。

以下の条件のいずれかを満たした場合、ByValとして扱われる。

void SbiRuntime::SetParameters( SbxArray* pParams )
{
    refParams = new SbxArray;
    // for the return value
    refParams->Put32( pMeth, 0 );

    SbxInfo* pInfo = pMeth ? pMeth->GetInfo() : nullptr;
    sal_uInt32 nParamCount = pParams ? pParams->Count32() : 1;
    assert(nParamCount <= std::numeric_limits<sal_uInt16>::max());
    if( nParamCount > 1 )
    {
        for( sal_uInt32 i = 1 ; i < nParamCount ; i++ )
        {
            const SbxParamInfo* p = pInfo ? pInfo->GetParam( sal::static_int_cast<sal_uInt16>(i) ) : nullptr;

            // #111897 ParamArray
            // (ParamArrayに関する話なので中略)

            SbxVariable* v = pParams->Get32( i );
            // methods are always byval!
            bool bByVal = dynamic_cast<const SbxMethod>(v) != nullptr;
            SbxDataType t = v->GetType();
            bool bTargetTypeIsArray = false;
            if( p )
            {
                bByVal |= ( p->eType & SbxBYREF ) == 0;
                t = static_cast<SbxDatatype>( p->eType & 0x0FFF );

                if( !bByVal && t != SbxVARIANT &&
                    (!v->IsFixed() || static_cast<SbxDatatype>(v->GetType() & 0x0FFF ) != t) )
                {
                    bByVal = true;
                }

                bTargetTypeIsArray = (p->nUserData & PARAM_INFO_WITHBRACKETS) != 0;
            }
            
            
            if( bByVal )
            {
                if( bTargetTypeIsArray )
                {
                    t = SbxOBJECT;
                }
                SbxVariable* v2 = new SbxVariable( t );
                v2->SetFlag( SbxFlagBits::ReadWrite );
                *v2 = *v;
                refParams->Put32( v2, i );
            }
            else
            {
                if( t != SbxVARIANT && t != ( v->GetType() & 0x0FFF ) )
                {
                    if( p && (p->eType & SbxARRAY) )
                    {
                        Error( ERRCODE_BASIC_CONVERSION );
                    }
                    else
                    {
                        v->Convert( t );
                    }
                }
                refParams->Put32( v, i );
            }
            if( p )
            {
                refParams->PutAlias32( p->aName, i );
            }
        }
    }

    // ParamArray for missing parameter
    // (ParamArray「可変長引数」の話なので中略)
}

(FIXED)123025:Split関数の戻り値の配列をRedim Preserveで保持できない!(本項WIP)

StarBasicのコード中、定義でない場所で識別子が登場した場合、それは代入文か、関数・サブルーチンの呼び出しである。

// assignment or subroutine call

void SbiParser::Symbol( const KeywordSymbolInfo* pKeywordSymbolInfo )
{
    SbiExprMode eMode = bVBASupportOn ? EXPRMODE_STANDALONE : EXPRMODE_STANDARD;
    SbiExpression aVar( this, SbSYMBOL, eMode, pKeywordSymbolInfo );

    //(配列の代入の話なので中略)

    RecursiveMode eRecMode = ( bEQ ? PREVENT_CALL : FORCE_CALL );
    bool bSpecialMidHandling = false;
    SbiSymDef* pDef = aVar.GetRealVar();
    if( bEQ && pDef && pDef->GetScope() == SbRTL )
    {
    // (Mid関数やMid文は特殊な文法を採るため。中略)
    }
    // 関数呼び出し
    aVar.Gen( eRecMode );
    if( !bSpecialMidHandling )
    {
        if( !bEQ )
        {

            aGen.Gen( SbiOpcode::GET_ );
        }
        else
        {
            // so it must be an assignment!
            // (代入の話なので中略)            
            aGen.Gen( eOp );
        }
    }
}

void SbiExpression::Gen( RecursiveMode eRecMode )
{
    // special treatment for WITH
    // If pExpr == .-term in With, approximately Gen for Basis-Object
    pExpr->Gen( pParser->aGen, eRecMode );
    if( bByVal )
    {
        pParser->aGen.Gen( SbiOpcode::BYVAL_ );
    }
    if( bBased )
    {
        sal_uInt16 uBase = pParser->nBase;
        if( pParser->IsCompatible() )
        {
            uBase |= 0x8000;        // #109275 Flag compatibility
        }
        pParser->aGen.Gen( SbiOpcode::BASED_, uBase );
        pParser->aGen.Gen( SbiOpcode::ARGV_ );
    }
}

void SbiExpression::Gen( RecursiveMode eRecMode )
{
    // special treatment for WITH
    // If pExpr == .-term in With, approximately Gen for Basis-Object
    pExpr->Gen( pParser->aGen, eRecMode );
    if( bByVal )
    {
        pParser->aGen.Gen( SbiOpcode::BYVAL_ );
    }
    if( bBased )
    {
        sal_uInt16 uBase = pParser->nBase;
        if( pParser->IsCompatible() )
        {
            uBase |= 0x8000;        // #109275 Flag compatibility
        }
        pParser->aGen.Gen( SbiOpcode::BASED_, uBase );
        pParser->aGen.Gen( SbiOpcode::ARGV_ );
    }
}

// Output of an element
void SbiExprNode::Gen( SbiCodeGen& rGen, RecursiveMode eRecMode )
{
    sal_uInt16 nStringId;

    if( IsConstant() )
    {
        //(定数の話じゃないから中略)
    }
    else if( IsOperand() )
    {
        SbiExprNode* pWithParent_ = nullptr;
        SbiOpcode eOp;
        if( aVar.pDef->GetScope() == SbPARAM )
        {
            eOp = SbiOpcode::PARAM_;
            if( aVar.pDef->GetPos() == 0 )
            {
                bool bTreatFunctionAsParam = true;
                if( eRecMode == FORCE_CALL )
                {
                    bTreatFunctionAsParam = false;
                }
                else if( eRecMode == UNDEFINED )
                {
                    if( aVar.pPar && aVar.pPar->IsBracket() )
                    {
                         bTreatFunctionAsParam = false;
                    }
                }
                if( !bTreatFunctionAsParam )
                {
                    eOp = aVar.pDef->IsGlobal() ? SbiOpcode::FIND_G_ : SbiOpcode::FIND_;
                }
            }
        }
        // special treatment for WITH
        else if( (pWithParent_ = pWithParent) != nullptr )
        {
            eOp = SbiOpcode::ELEM_;            // .-Term in WITH
        }
        else
        {
            eOp = ( aVar.pDef->GetScope() == SbRTL ) ? SbiOpcode::RTL_ :
                (aVar.pDef->IsGlobal() ? SbiOpcode::FIND_G_ : SbiOpcode::FIND_);
        }

        if( eOp == SbiOpcode::FIND_ )
        {

            SbiProcDef* pProc = aVar.pDef->GetProcDef();
            if ( rGen.GetParser()->bClassModule )
            {
                eOp = SbiOpcode::FIND_CM_;
            }
            else if ( aVar.pDef->IsStatic() || (pProc && pProc->IsStatic()) )
            {
                eOp = SbiOpcode::FIND_STATIC_;
            }
        }
        for( SbiExprNode* p = this; p; p = p->aVar.pNext )
        {
            if( p == this && pWithParent_ != nullptr )
            {
                pWithParent_->Gen(rGen);
            }
            
            p->GenElement( rGen, eOp );
            // (多分メソッドチェーンを想定している)
            eOp = SbiOpcode::ELEM_;
   
        }
    }
    else if( eNodeType == SbxTYPEOF )
    {
        pLeft->Gen(rGen);
        rGen.Gen( SbiOpcode::TESTCLASS_, nTypeStrId );
    }
    else if( eNodeType == SbxNEW )
    {
        rGen.Gen( SbiOpcode::CREATE_, 0, nTypeStrId );
    }
    else
    {
        //(演算子の話なので中略)
    }
}
// part of the runtime-library?
SbiSymDef* SbiParser::CheckRTLForSym(const OUString& rSym, SbxDataType eType)
{
    SbxVariable* pVar = GetBasic()->GetRtl()->Find(rSym, SbxClassType::DontCare);
    if (!pVar)
        return nullptr;

    if (SbxMethod* pMethod = dynamic_cast<SbxMethod*>(pVar))
    {
        SbiProcDef* pProc_ = aRtlSyms.AddProc( rSym );
        if (pMethod->IsRuntimeFunction())
        {
            pProc_->SetType( pMethod->GetRuntimeFunctionReturnType() );
        }
        else
        {
            pProc_->SetType( pVar->GetType() );
        }
        return pProc_;
    }


    SbiSymDef* pDef = aRtlSyms.AddSym(rSym);
    pDef->SetType(eType);
    return pDef;
}
void SbiRuntime::StepRTL( sal_uInt32 nOp1, sal_uInt32 nOp2 )
{
    PushVar( FindElement( rBasic.pRtl.get(), nOp1, nOp2, ERRCODE_BASIC_PROC_UNDEFINED, false ) );
}    
SbiParser::SbiParser( StarBASIC* pb, SbModule* pm )
        : SbiTokenizer( pm->GetSource32(), pb ),
          aGlobals( aGblStrings, SbGLOBAL, this ),
          aPublics( aGblStrings, SbPUBLIC, this ),
          aRtlSyms( aGblStrings, SbRTL, this ),
          aGen( *pm, this, 1024 )
SbiProcDef* SbiSymPool::AddProc( const OUString& rName )
{
    SbiProcDef* p = new SbiProcDef( pParser, rName );
    p->nPos    = m_Data.size();
    p->nId     = rStrings.Add( rName );
    // procs are always local
    p->nProcId = 0;
    p->pIn     = this;
    m_Data.insert( m_Data.begin() + p->nPos, std::unique_ptr(p) );
    return p;
}
StarBASIC::StarBASIC( StarBASIC* p, bool bIsDocBasic  )
    : SbxObject("StarBASIC"), bDocBasic( bIsDocBasic )
{
    SetParent( p );
    bNoRtl = bBreak = false;
    bVBAEnabled = false;

    if( !GetSbData()->nInst++ )
    {
        GetSbData()->pSbFac.reset( new SbiFactory );
        AddFactory( GetSbData()->pSbFac.get() );
        GetSbData()->pTypeFac = new SbTypeFactory;
        AddFactory( GetSbData()->pTypeFac );
        GetSbData()->pClassFac = new SbClassFactory;
        AddFactory( GetSbData()->pClassFac );
        GetSbData()->pOLEFac = new SbOLEFactory;
        AddFactory( GetSbData()->pOLEFac );
        GetSbData()->pFormFac = new SbFormFactory;
        AddFactory( GetSbData()->pFormFac );
        GetSbData()->pUnoFac.reset( new SbUnoFactory );
        AddFactory( GetSbData()->pUnoFac.get() );
    }
    pRtl = new SbiStdObject(RTLNAME, this );
    // Search via StarBasic is always global
    SetFlag( SbxFlagBits::GlobalSearch );
    pVBAGlobals = nullptr;
    bQuit = false;

    if( bDocBasic )
    {
        lclInsertDocBasicItem( *this );
    }
}

static Methods aMethods[] = {
// (中略)
{"Split",          SbxOBJECT,      3 | FUNCTION_, RTLNAME(Split),0         },
  { "expression",   SbxSTRING, 0,nullptr,0 },
  { "delimiter",    SbxSTRING, 0,nullptr,0 },
  { "count",        SbxLONG, 0,nullptr,0 },
// (中略)
}

SbxVariable* SbiStdObject::Find( const OUString& rName, SbxClassType t )
{
    // entered already?
    SbxVariable* pVar = SbxObject::Find( rName, t );
    if( !pVar )
    {
        // else search one
        sal_uInt16 nHash_ = SbxVariable::MakeHashCode( rName );
        Methods* p = aMethods;
        bool bFound = false;
        short nIndex = 0;
        sal_uInt16 nSrchMask = TYPEMASK_;
        switch( t )
        {
            case SbxClassType::Method:   nSrchMask = METHOD_; break;
            case SbxClassType::Property: nSrchMask = PROPERTY_; break;
            case SbxClassType::Object:   nSrchMask = OBJECT_; break;
            default: break;
        }
        while( p->nArgs != -1 )
        {
            if( ( p->nArgs & nSrchMask )
             && ( p->nHash == nHash_ )
             && ( rName.equalsIgnoreAsciiCaseAscii( p->pName ) ) )
            {
                bFound = true;
                if( p->nArgs & COMPTMASK_ )
                {
                    bool bCompatibility = false;
                    SbiInstance* pInst = GetSbData()->pInst;
                    if (pInst)
                    {
                        bCompatibility = pInst->IsCompatibility();
                    }
                    else
                    {
                        // No instance running => compiling a source on module level.
                        const SbModule* pModule = GetSbData()->pCompMod;
                        if (pModule)
                            bCompatibility = pModule->IsVBACompat();
                    }
                    if ((bCompatibility && (NORMONLY_ & p->nArgs)) || (!bCompatibility && (COMPATONLY_ & p->nArgs)))
                        bFound = false;
                }
                break;
            }
            nIndex += ( p->nArgs & ARGSMASK_ ) + 1;
            p = aMethods + nIndex;
        }

        if( bFound )
        {
            // isolate Args-fields:
            SbxFlagBits nAccess = static_cast<SbxFlagBits>(( p->nArgs & RWMASK_ ) >> 8);
            short nType   = ( p->nArgs & TYPEMASK_ );
            if( p->nArgs & CONST_ )
                nAccess |= SbxFlagBits::Const;
            OUString aName_ = OUString::createFromAscii( p->pName );
            SbxClassType eCT = SbxClassType::Object;
            if( nType & PROPERTY_ )
            {
                eCT = SbxClassType::Property;
            }
            else if( nType & METHOD_ )
            {
                eCT = SbxClassType::Method;
            }
            pVar = Make( aName_, eCT, p->eType, ( p->nArgs & FUNCTION_ ) == FUNCTION_ );
            pVar->SetUserData( nIndex + 1 );
            pVar->SetFlags( nAccess );
        }
    }
    return pVar;
}
// If a new object will be established, this object will be indexed,
// if an object of this name exists already.

SbxVariable* SbxObject::Make( const OUString& rName, SbxClassType ct, SbxDataType dt, bool bIsRuntimeFunction )
{
    // Is the object already available?
    SbxArray* pArray = nullptr;
    switch( ct )
    {
    case SbxClassType::Variable:
    case SbxClassType::Property: pArray = pProps.get();    break;
    case SbxClassType::Method:   pArray = pMethods.get();  break;
    case SbxClassType::Object:   pArray = pObjs.get();     break;
    default: SAL_WARN( "basic.sbx", "Invalid SBX-Class" ); break;
    }
    if( !pArray )
    {
        return nullptr;
    }
    // Collections may contain objects of the same name
    if( !( ct == SbxClassType::Object && dynamic_cast<const SbxCollection>( this ) != nullptr ) )
    {
        SbxVariable* pRes = pArray->Find( rName, ct );
        if( pRes )
        {
            return pRes;
        }
    }
    SbxVariable* pVar = nullptr;
    switch( ct )
    {
    case SbxClassType::Variable:
    case SbxClassType::Property:
        pVar = new SbxProperty( rName, dt );
        break;
    case SbxClassType::Method:
        pVar = new SbxMethod( rName, dt, bIsRuntimeFunction );
        break;
    case SbxClassType::Object:
        pVar = CreateObject( rName );
        break;
    default:
        break;
    }
    pVar->SetParent( this );
    pArray->Put32( pVar, pArray->Count32() );
    SetModified( true );
    // The object listen always
    StartListening(pVar->GetBroadcaster(), DuplicateHandling::Prevent);
    return pVar;
}  
void SbiRuntime::StepGET()
{
    SbxVariable* p = GetTOS();
    p->Broadcast( SfxHintId::BasicDataWanted );
}

// Perhaps some day one could cut the parameter 0.
// Then the copying will be dropped...

void SbxVariable::Broadcast( SfxHintId nHintId )
{
    if( mpBroadcaster && !IsSet( SbxFlagBits::NoBroadcast ) )
    {
        // Because the method could be called from outside, check the
        // rights here again
        if( nHintId == SfxHintId::BasicDataWanted )
        {
            if( !CanRead() )
            {
                return;
            }
        }
        if( nHintId == SfxHintId::BasicDataChanged )
        {
            if( !CanWrite() )
            {
                return;
            }
        }

        //fdo#86843 Add a ref during the following block to guard against
        //getting deleted before completing this method
        SbxVariableRef aBroadcastGuard(this);

        // Avoid further broadcasting
        std::unique_ptr<SfxBroadcaster> pSave = std::move(mpBroadcaster);
        SbxFlagBits nSaveFlags = GetFlags();
        SetFlag( SbxFlagBits::ReadWrite );
        if( mpPar.is() )
        {
            // Register this as element 0, but don't change over the parent!
            mpPar->GetRef32( 0 ) = this;
        }
        pSave->Broadcast( SbxHint( nHintId, this ) );
        mpBroadcaster = std::move(pSave);
        SetFlags( nSaveFlags );
    }
}

SfxBroadcaster& SbxVariable::GetBroadcaster()
{
    if( !mpBroadcaster )
    {
        mpBroadcaster.reset( new SfxBroadcaster );
    }
    return *mpBroadcaster;
}

// broadcast immediately

void SfxBroadcaster::Broadcast( const SfxHint &rHint )
{
    // notify all registered listeners exactly once
    for (size_t i = 0; i < mpImpl->m_Listeners.size(); ++i)
    {
        SfxListener *const pListener = mpImpl->m_Listeners[i];
        if (pListener)
            pListener->Notify( *this, rHint );
    }
}

void SbiStdObject::Notify( SfxBroadcaster& rBC, const SfxHint& rHint )

{
    const SbxHint* pHint = dynamic_cast<const SbxHint*>(&rHint);
    if( pHint )
    {
        SbxVariable* pVar = pHint->GetVar();
        SbxArray* pPar_ = pVar->GetParameters();
        const sal_uInt16 nCallId = static_cast<sal_uInt16>(pVar->GetUserData());
        if( nCallId )
        {
            const SfxHintId t = pHint->GetId();
            if( t == SfxHintId::BasicInfoWanted )
                pVar->SetInfo( GetInfo( static_cast<short>(pVar->GetUserData()) ) );
            else
            {
                bool bWrite = false;
                if( t == SfxHintId::BasicDataChanged )
                    bWrite = true;
                if( t == SfxHintId::BasicDataWanted || bWrite )
                {
                    RtlCall p = aMethods[ nCallId-1 ].pFunc;
                    SbxArrayRef rPar( pPar_ );
                    if( !pPar_ )
                    {
                        rPar = pPar_ = new SbxArray;
                        pPar_->Put32( pVar, 0 );
                    }
                    p( static_cast<StarBASIC*>(GetParent()), *pPar_, bWrite );
                    return;
                }
            }
        }
        SbxObject::Notify( rBC, rHint );
    }
}

各種変数や各種一時変数はSbxVariableやSbxValueを使う。このコンストラクタに与えるのは宣言時の型であるが、これがSbxVARIANTの場合、StarBasicやVBAでは「なんでも入る変数」となる。このときはFixedフラグが設定されていない。

SbxValue::SbxValue( SbxDataType t ) : SbxBase()
{
    int n = t & 0x0FFF;

    if( n == SbxVARIANT )
        n = SbxEMPTY;
    else
        SetFlag( SbxFlagBits::Fixed );
    aData.clear(SbxDataType( n ));
}
// REDIM_COPY
// TOS  = Array-Variable, Reference to array is copied
//        Variable is cleared as in ERASE

void SbiRuntime::StepREDIMP_ERASE()
{
    SbxVariableRef refVar = PopVar();
    refRedim = refVar;
    SbxDataType eType = refVar->GetType();
    if( eType & SbxARRAY )
    {
        SbxBase* pElemObj = refVar->GetObject();
        SbxDimArray* pDimArray = dynamic_cast<SbxDimArray*>( pElemObj );
        if( pDimArray )
        {
            refRedimpArray = pDimArray;
        }

    }
    else if( refVar->IsFixed() )
    {
        refVar->Clear();
    }
    else
    {
        refVar->SetType( SbxEMPTY );
    }
}
void SbiRuntime::StepREDIMP()
{
    SbxVariableRef refVar = PopVar();
    DimImpl( refVar );

    // Now check, if we can copy from the old array
    if( refRedimpArray.is() )
    {
        if (SbxDimArray* pNewArray = dynamic_cast<SbxDimArray*>(refVar->GetObject()))
            implRestorePreservedArray(pNewArray, refRedimpArray);
    }
}
// Returns true when actually restored
static bool implRestorePreservedArray(SbxDimArray* pNewArray, SbxArrayRef& rrefRedimpArray, bool* pbWasError = nullptr)
{
    assert(pNewArray);
    bool bResult = false;
    if (pbWasError)
        *pbWasError = false;
    if (rrefRedimpArray)
    {
        SbxDimArray* pOldArray = static_cast<SbxDimArray*>(rrefRedimpArray.get());
        const sal_Int32 nDimsNew = pNewArray->GetDims32();
        const sal_Int32 nDimsOld = pOldArray->GetDims32();

        if (nDimsOld != nDimsNew)
        {
            StarBASIC::Error(ERRCODE_BASIC_OUT_OF_RANGE);
            if (pbWasError)
                *pbWasError = true;
        }
        else if (nDimsNew > 0)
        {
            // Store dims to use them for copying later
            std::unique_ptr<sal_Int32[]> pLowerBounds(new sal_Int32[nDimsNew]);
            std::unique_ptr<sal_Int32[]> pUpperBounds(new sal_Int32[nDimsNew]);
            std::unique_ptr<sal_Int32[]> pActualIndices(new sal_Int32[nDimsNew]);
            bool bNeedsPreallocation = true;

            // Compare bounds
            for (sal_Int32 i = 1; i <= nDimsNew; i++)
            {
                sal_Int32 lBoundNew, uBoundNew;
                sal_Int32 lBoundOld, uBoundOld;
                pNewArray->GetDim32(i, lBoundNew, uBoundNew);
                pOldArray->GetDim32(i, lBoundOld, uBoundOld);
                lBoundNew = std::max(lBoundNew, lBoundOld);
                uBoundNew = std::min(uBoundNew, uBoundOld);
                sal_Int32 j = i - 1;
                pActualIndices[j] = pLowerBounds[j] = lBoundNew;
                pUpperBounds[j] = uBoundNew;
                if (lBoundNew > uBoundNew) // No elements in the dimension -> no elements to restore
                    bNeedsPreallocation = false;
            }

            // Optimization: pre-allocate underlying container
            if (bNeedsPreallocation)
                pNewArray->Put32(nullptr, pUpperBounds.get());

            // Copy data from old array by going recursively through all dimensions
            // (It would be faster to work on the flat internal data array of an
            // SbyArray but this solution is clearer and easier)
            implCopyDimArray(pNewArray, pOldArray, nDimsNew - 1, 0, pActualIndices.get(),
                             pLowerBounds.get(), pUpperBounds.get());
            bResult = true;
        }

        rrefRedimpArray.clear();
    }
    return bResult;
}
// Helper function for StepREDIMP and StepDCREATE_IMPL / bRedimp = true
static void implCopyDimArray( SbxDimArray* pNewArray, SbxDimArray* pOldArray, sal_Int32 nMaxDimIndex,
    sal_Int32 nActualDim, sal_Int32* pActualIndices, sal_Int32* pLowerBounds, sal_Int32* pUpperBounds )
{
    sal_Int32& ri = pActualIndices[nActualDim];
    for( ri = pLowerBounds[nActualDim] ; ri <= pUpperBounds[nActualDim] ; ri++ )
    {
        if( nActualDim < nMaxDimIndex )
        {
            implCopyDimArray( pNewArray, pOldArray, nMaxDimIndex, nActualDim + 1,
                pActualIndices, pLowerBounds, pUpperBounds );
        }
        else
        {
            SbxVariable* pSource = pOldArray->Get32( pActualIndices );
            pNewArray->Put32(pSource, pActualIndices);
        }
    }
}

それを踏まえてSbRtl_Splitを見てみる。

void SbRtl_Split(StarBASIC *, SbxArray & rPar, bool)
{
    (中略)
    SbxDimArray* pArray = new SbxDimArray( SbxVARIANT );
    pArray->unoAddDim32( 0, nArraySize-1 );

    // insert parameter(s) into the array
    for(sal_Int32 i = 0 ; i < nArraySize ; i++ )
    {
        SbxVariableRef xVar = new SbxVariable( SbxVARIANT );
        xVar->PutString( vRet[i] );
        pArray->Put32( xVar.get(), &i );
    }

    // return array
    SbxVariableRef refVar = rPar.Get32(0);
    SbxFlagBits nFlags = refVar->GetFlags();
    refVar->ResetFlag( SbxFlagBits::Fixed );
    refVar->PutObject( pArray );
    refVar->SetFlags( nFlags );
    refVar->SetParameters( nullptr );
}

(FIXED)85371:関数名をループ変数に使うことができない

以下のコードを書いた時、アセンブリ言語的にはSbiOpCode::INITFOR_,SbiOpCode::NEXT_,SbiOpCode::JUMP_(範囲外になったとき)の3種類が出力されている。

For i = 1 to 10
' ほげふが
Next    
// FOR var = expr TO expr STEP

void SbiParser::For()
{
    bool bForEach = ( Peek() == EACH );
    if( bForEach )
        Next();
    SbiExpression aLvalue( this, SbOPERAND );
    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();
}
    
void SbiRuntime::StepINITFOR()
{
    PushFor();
}

void SbiRuntime::StepINITFOREACH()
{
    PushForEach();
}

// increment FOR-variable

void SbiRuntime::StepNEXT()
{
    if( !pForStk )
    {
        StarBASIC::FatalError( ERRCODE_BASIC_INTERNAL_ERROR );
        return;
    }
    if( pForStk->eForType == ForType::To )
    {
        pForStk->refVar->Compute( SbxPLUS, *pForStk->refInc );
    }
}
void SbiRuntime::PushFor()
{
    SbiForStack* p = new SbiForStack;
    p->eForType = ForType::To;
    p->pNext = pForStk;
    pForStk = p;

    p->refInc = PopVar();
    p->refEnd = PopVar();
    SbxVariableRef xBgn = PopVar();
    p->refVar = PopVar();
    *(p->refVar) = *xBgn;
    nForLvl++;
}
bool SbxValue::Compute( SbxOperator eOp, const SbxValue& rOp )
{
#if !HAVE_FEATURE_SCRIPTING
    const bool bVBAInterop = false;
#else
    bool bVBAInterop =  SbiRuntime::isVBAEnabled();
#endif
    SbxDataType eThisType = GetType();
    SbxDataType eOpType = rOp.GetType();
    ErrCode eOld = GetError();
    if( eOld != ERRCODE_NONE )
        ResetError();
    if( !CanWrite() )
        SetError( ERRCODE_BASIC_PROP_READONLY );
    else if( !rOp.CanRead() )
        SetError( ERRCODE_BASIC_PROP_WRITEONLY );
    // Special rule 1: If one operand is null, the result is null
    else if( eThisType == SbxNULL || eOpType == SbxNULL )
        SetType( SbxNULL );
    else
    {

代入を担当する以下のコード、戻り値を担当するrefVar(SbxVariableを継承するSbxMethodが格納されていて通常の変数と同様に扱える)が現在走っているpMethodと等しいときのみSbxFlagBits::Writeを設定しており、それ以外のときにはSbxFlagBits::Readになっているため、前記のコードのCanWriteがエラーを返します。なので、少なくとも現状、For文の変数にメソッド名そのものを使うことはできません。


void SbiRuntime::StepPUT()
{
    SbxVariableRef refVal = PopVar();
    SbxVariableRef refVar = PopVar();
    // store on its own method (inside a function)?
    bool bFlagsChanged = false;
    SbxFlagBits n = SbxFlagBits::NONE;
    if( refVar.get() == pMeth )
    {
        bFlagsChanged = true;
        n = refVar->GetFlags();
        refVar->SetFlag( SbxFlagBits::Write );
    }

(FIXED)36737:String型やObject型の引数のデフォルト値が使用されない

次項と引用するコードが非常に近いのでそこで取り扱います。

(FIXED)79426:エラーオブジェクトを関数に渡して処理することができない

関数の引数には式の後にSbiOpCode::PARAM_が置かれ、これがSbiRuntime::StepPARAMによって処理される。

void SbiRuntime::StepPARAM( sal_uInt32 nOp1, sal_uInt32 nOp2 )
{
    sal_uInt16 i = static_cast<sal_uInt16>( nOp1 & 0x7FFF );
    SbxDataType t = static_cast<SbxDataType>(nOp2);
    SbxVariable* p;

    // #57915 solve missing in a cleaner way
    sal_uInt32 nParamCount = refParams->Count32();
    if( i >= nParamCount )
    {
        sal_uInt16 iLoop = i;
        while( iLoop >= nParamCount )
        {
            p = new SbxVariable();

            if( SbiRuntime::isVBAEnabled() &&
                (t == SbxOBJECT || t == SbxSTRING) )
            {
                if( t == SbxOBJECT )
                {
                    p->PutObject( nullptr );
                }
                else
                {
                    p->PutString( OUString() );
                }
            }
            else
            {
                p->PutErr( 448 );       // like in VB: Error-Code 448 (ERRCODE_BASIC_NAMED_NOT_FOUND)
            }
            refParams->Put32( p, iLoop );
            iLoop--;
        }
    }
    p = refParams->Get32( i );

    if( p->GetType() == SbxERROR && i )
    {
        // if there's a parameter missing, it can be OPTIONAL
        bool bOpt = false;
        if( pMeth )
        {
            SbxInfo* pInfo = pMeth->GetInfo();
            if ( pInfo )
            {
                const SbxParamInfo* pParam = pInfo->GetParam( i );
                if( pParam && ( pParam->nFlags & SbxFlagBits::Optional ) )
                {
                    // Default value?
                    sal_uInt16 nDefaultId = static_cast<sal_uInt16>(pParam->nUserData & 0x0ffff);
                    if( nDefaultId > 0 )
                    {
                        OUString aDefaultStr = pImg->GetString( nDefaultId );
                        p = new SbxVariable(pParam-> eType);
                        p->PutString( aDefaultStr );
                        refParams->Put32( p, i );
                    }
                    bOpt = true;
                }
            }
        }
        if( !bOpt )
        {
            Error( ERRCODE_BASIC_NOT_OPTIONAL );
        }
    }
    else if( t != SbxVARIANT && static_cast<SbxDataType>(p->GetType() & 0x0FFF ) != t )
    {
        SbxVariable* q = new SbxVariable( t );
        aRefSaved.emplace_back(q );
        *q = *p;
        p = q;
        if ( i )
        {
            refParams->Put32( p, i );
        }
    }
    SetupArgs( p, nOp1 );
    PushVar( CheckArray( p ) );
}

47214:複数の論理行を1行の物理行内に書くためにコロンを使うが、このあとにあるLINE INPUT文が認識されない

直前に取得したトークンが「なし(ファイル始端)」「コメントのためのREM」「行終端」「THEN」「ELSE」の何れかである場合は行の開始とみなす。

SbiToken SbiTokenizer::Next()
{
    //(中略)
    bool bStartOfLine = (eCurTok == NIL || eCurTok == REM || eCurTok == EOLN || eCurTok == THEN || eCurTok == ELSE); // single line If

取得したトークンがNAMEかLINEであるときは、そのトークンはSYMBOL、つまり識別子である。ということはNAMEステートメントやLINEステートメントの一部じゃない


if( !bStartOfLine && (tp->t == NAME || tp->t == LINE) )
{
        return eCurTok = SYMBOL;
}

じゃあ、コロンはSbiTokenizer/SbiParserではどういう扱いなのかというと、「Goto等の移動先のラベル」を明示するのに使われている。

123263:ある要素型の配列変数に別の要素型の配列が代入できてしまう。

配列変数を宣言したときにどのような処理が走るかを見ていこう。

void SbiParser::Dim()
{
    DefVar( SbiOpcode::DIM_, pProc && bVBASupportOn && pProc->IsStatic() );
}

void SbiParser::DefVar( SbiOpcode eOp, bool bStatic )
{
    SbiSymPool* pOldPool = pPool;
    bool bSwitchPool = false;
    bool bPersistentGlobal = false;
    SbiToken eFirstTok = eCurTok;

    if( pProc && ( eCurTok == GLOBAL || eCurTok == PUBLIC || eCurTok == PRIVATE ) )
        Error( ERRCODE_BASIC_NOT_IN_SUBR, eCurTok );
    if( eCurTok == PUBLIC || eCurTok == GLOBAL )
    {
        bSwitchPool = true;     // at the right moment switch to the global pool
        if( eCurTok == GLOBAL )
            bPersistentGlobal = true;
    }
    // behavior in VBA is that a module scope variable's lifetime is
    // tied to the document. e.g. a module scope variable is global
    if(  GetBasic()->IsDocBasic() && bVBASupportOn && !pProc )
        bPersistentGlobal = true;
    // PRIVATE is a synonymous for DIM
    // _CONST_?
    bool bConst = false;
    if( eCurTok == CONST_ )
        bConst = true;
    else if( Peek() == CONST_ )
    {
        Next();
        bConst = true;
    }

    //(今回定義するのは変数だから中略)

    // SHARED were ignored
    if( Peek() == SHARED ) Next();

    // PRESERVE only at REDIM
    if( Peek() == PRESERVE )
    {
        Next();
        if( eOp == SbiOpcode::REDIM_ )
            eOp = SbiOpcode::REDIMP_;
        else
            Error( ERRCODE_BASIC_UNEXPECTED, eCurTok );
    }
    SbiSymDef* pDef;
    SbiExprListPtr pDim;

    // #40689, Statics -> Module-Initialising, skip in Sub
    sal_uInt32 nEndOfStaticLbl = 0;
    if( !bVBASupportOn && bStatic )
    {
        nEndOfStaticLbl = aGen.Gen( SbiOpcode::JUMP_, 0 );
        aGen.Statement();   // catch up on static here
    }

    bool bDefined = false;
    while( ( pDef = VarDecl( &pDim, bStatic, bConst ) ) != nullptr )
    {
        /*fprintf(stderr, "Actual sub: \n");
        fprintf(stderr, "Symbol name: %s\n",OUStringToOString(pDef->GetName(),RTL_TEXTENCODING_UTF8).getStr());*/
        EnableErrors();
        // search variable:
        if( bSwitchPool )
            pPool = &aGlobals;
        SbiSymDef* pOld = pPool->Find( pDef->GetName() );
        // search also in the Runtime-Library
        bool bRtlSym = false;
        if( !pOld )
        {
            pOld = CheckRTLForSym( pDef->GetName(), SbxVARIANT );
            if( pOld )
                bRtlSym = true;
        }
        if( pOld && !(eOp == SbiOpcode::REDIM_ || eOp == SbiOpcode::REDIMP_) )
        {
            if( pDef->GetScope() == SbLOCAL )
                if (auto eOldScope = pOld->GetScope(); eOldScope != SbLOCAL && eOldScope != SbPARAM)
                    pOld = nullptr;
        }
        if( pOld )
        {
            // (二重定義チェック、またはReDim, ReDim Preserve関連なので中略)
        }
        else
            pPool->Add( pDef );

        // #36374: Create the variable in front of the distinction IsNew()
        // Otherwise error at Dim Identifier As New Type and option explicit
        if( !bDefined && !(eOp == SbiOpcode::REDIM_ || eOp == SbiOpcode::REDIMP_)
                      && ( !bConst || pDef->GetScope() == SbGLOBAL ) )
        {
            // Declare variable or global constant
            SbiOpcode eOp2;
            switch ( pDef->GetScope() )
            {
                case SbGLOBAL:  eOp2 = bPersistentGlobal ? SbiOpcode::GLOBAL_P_ : SbiOpcode::GLOBAL_;
                                goto global;
                case SbPUBLIC:  eOp2 = bPersistentGlobal ? SbiOpcode::PUBLIC_P_ : SbiOpcode::PUBLIC_;
                                // #40689, no own Opcode anymore
                                if( bVBASupportOn && bStatic )
                                {
                                    eOp2 = SbiOpcode::STATIC_;
                                    break;
                                }
                global:         aGen.BackChain( nGblChain );
                                nGblChain = 0;
                                bGblDefs = bNewGblDefs = true;
                                break;
                default:        eOp2 = SbiOpcode::LOCAL_;
            }
            sal_uInt32 nOpnd2 = sal::static_int_cast< sal_uInt16 >( pDef->GetType() );
            if( pDef->IsWithEvents() )
                nOpnd2 |= SBX_TYPE_WITH_EVENTS_FLAG;

            if( bCompatible && pDef->IsNew() )
                nOpnd2 |= SBX_TYPE_DIM_AS_NEW_FLAG;

            short nFixedStringLength = pDef->GetFixedStringLength();
            if( nFixedStringLength >= 0 )
                nOpnd2 |= (SBX_FIXED_LEN_STRING_FLAG + (sal_uInt32(nFixedStringLength) << 17));     // len = all bits above 0x10000

            if( pDim != nullptr && pDim->GetDims() > 0 )
                nOpnd2 |= SBX_TYPE_VAR_TO_DIM_FLAG;

            aGen.Gen( eOp2, pDef->GetId(), nOpnd2 );
        }

        // Initialising for self-defined data types
        // and per NEW created variable
        if( pDef->GetType() == SbxOBJECT
         && pDef->GetTypeId() )
        {
            // (Dim x As New UserDifined_Type、みたいなヤツ。中略)
        }
        else
        {
            if( bConst )
            {
                // (定数の話なので中略)
                
            }
            else if( pDim )
            {
        // (RedimやReDim Preserveに絡む再設定)
                pDef->SetDims( pDim->GetDims() );
                if( bPersistentGlobal )
                    pDef->SetGlobal( true );
                SbiExpression aExpr( this, *pDef, std::move(pDim) );
                aExpr.Gen();
                pDef->SetGlobal( false );
                aGen.Gen( (eOp == SbiOpcode::STATIC_) ? SbiOpcode::DIM_ : eOp );
            }
        }
        if( !TestComma() )
            goto MyBreak;

        // Implementation of bSwitchPool (see above): pPool must not be set to &aGlobals
        // at the VarDecl-Call.
        // Apart from that the behavior should be absolutely identical,
        // i.e., pPool had to be reset always at the end of the loop.
        // also at a break
        pPool = pOldPool;
        continue;       // Skip MyBreak
    MyBreak:
        pPool = pOldPool;
        break;
    }

    // #40689, finalize the jump over statics declarations
    if( !bVBASupportOn && bStatic )
    {
        // maintain the global chain
        nGblChain = aGen.Gen( SbiOpcode::JUMP_, 0 );
        bGblDefs = bNewGblDefs = true;

        // Register for Sub a jump to the end of statics
        aGen.BackChain( nEndOfStaticLbl );
    }

}
SbiSymDef* SbiParser::VarDecl( SbiExprListPtr* ppDim, bool bStatic, bool bConst )
{
    bool bWithEvents = false;
    if( Peek() == WITHEVENTS )
    {
        Next();
        bWithEvents = true;
    }
    // 括弧を除く変数名
    if( !TestSymbol() ) return nullptr;

    //// 型指定子があった場合
    SbxDataType t = eScanType;
    SbiSymDef* pDef = bConst ? new SbiConstDef( aSym ) : new SbiSymDef( aSym );

    // ここから、配列の添字の処理
    SbiExprListPtr pDim; 
    // Brackets?
    if( Peek() == LPAREN )
    {
        pDim = SbiExprList::ParseDimList( this );
        if( !pDim->GetDims() )
            pDef->SetWithBrackets();
    }
    
    pDef->SetType( t );
    if( bStatic )
        pDef->SetStatic();
    if( bWithEvents )
        pDef->SetWithEvents();

    // As 以下があるような場合。
    // つーか、型指定子よりもこっちの方が一般的だよね?
    // 型指定子とAs以下の型が矛盾しないかどうかのチェックも行うハズ。
    TypeDecl( *pDef );
    if( !ppDim && pDim )
    {
        if(pDim->GetDims() )
            Error( ERRCODE_BASIC_EXPECTED, "()" );
    }
    else if( ppDim )
        *ppDim = std::move(pDim);
    return pDef;
}
// Resolving of an AS-Type-Declaration
// The data type were inserted into the handed over variable

void SbiParser::TypeDecl( SbiSymDef& rDef, bool bAsNewAlreadyParsed )
{
    SbxDataType eType = rDef.GetType();
    if( bAsNewAlreadyParsed || Peek() == AS )
    {
        short nSize = 0;
        if( !bAsNewAlreadyParsed )
            Next();
        rDef.SetDefinedAs();
        SbiToken eTok = Next();
        if( !bAsNewAlreadyParsed && eTok == NEW )
        {
            rDef.SetNew();
            eTok = Next();
        }
        switch( eTok )
        {
            case ANY:
                if( rDef.IsNew() )
                    Error( ERRCODE_BASIC_SYNTAX );
                eType = SbxVARIANT; break;
            case TINTEGER:
            case TLONG:
            case TSINGLE:
            case TDOUBLE:
            case TCURRENCY:
            case TDATE:
            case TSTRING:
            case TOBJECT:
            case ERROR_:
            case TBOOLEAN:
            case TVARIANT:
            case TBYTE:
                if( rDef.IsNew() )
                    Error( ERRCODE_BASIC_SYNTAX );
                eType = (eTok==TBYTE) ? SbxBYTE : SbxDataType( eTok - TINTEGER + SbxINTEGER );
                if( eType == SbxSTRING )
                {
                    // STRING*n ?
                    if( Peek() == MUL )
                    {       // fixed size!
                        // (固定長文字列の話。中略)
                    }
                }
                break;
            case SYMBOL: // can only be a TYPE or an object class!
                // (ユーザー定義型とかの話。中略)
                break;
            case FIXSTRING: // new syntax for complex UNO types
                rDef.SetTypeId( aGblStrings.Add( aSym ) );
                eType = SbxOBJECT;
                break;
            default:
                Error( ERRCODE_BASIC_UNEXPECTED, eTok );
                Next();
        }
        // The variable could have been declared with a suffix
        if( rDef.GetType() != SbxVARIANT )
        {
            if( rDef.GetType() != eType )
                Error( ERRCODE_BASIC_VAR_DEFINED, rDef.GetName() );
            else if( eType == SbxSTRING && rDef.GetLen() != nSize )
                Error( ERRCODE_BASIC_VAR_DEFINED, rDef.GetName() );
        }
        rDef.SetType( eType );
        rDef.SetLen( nSize );
    }
}

こういうコードになっている。つまり、Dim x(1) As Integerのx、例えばVB.NETなんかだとIntegerの配列なのだけれども、StarBasicではあくまでInteger型に括弧が付いたものなんだよ。式上、識別子の後に「括弧がついているか」もしくは「GetDims()の戻り値はどうか」、それは配列なのかを気にしなきゃいけなくなるので個人的にはStarBasicの設計で一番モヤっとしているところ。

余談:これが、個人的にあまり好きじゃなかったから、個人的なビルドの方には括弧を処理しているときに配列だと判断して、TypeDeclが処理する際に、rDefが配列だという情報も一緒に型として持たせているわけだ(笑)。

少し先の話になるけれど、exprtree.cxxを読むときは、「SbxOBJECTとSbxARRAYが別フラグだ!」ってことを非常に強く意識して欲しい。「SbxOBJECTとSbxARRAYが別フラグだ!」「SbxOBJECTとSbxARRAYが別フラグだ!」大事なことだから3回繰り返した。

// assignment or subroutine call

void SbiParser::Symbol( const KeywordSymbolInfo* pKeywordSymbolInfo )
{
    SbiExprMode eMode = bVBASupportOn ? EXPRMODE_STANDALONE : EXPRMODE_STANDARD;
    SbiExpression aVar( this, SbSYMBOL, eMode, pKeywordSymbolInfo );

    bool bEQ = ( Peek() == EQ );
    if( !bEQ && bVBASupportOn && aVar.IsBracket() )
        Error( ERRCODE_BASIC_EXPECTED, "=" );

    RecursiveMode eRecMode = ( bEQ ? PREVENT_CALL : FORCE_CALL );
    // (今回は標準ライブラリ内関数の呼び出しではないので中略)
    aVar.Gen( eRecMode );
    if( !bSpecialMidHandling )
    {
        if( !bEQ )
        {
            // (関数呼び出し。中略)
        }
        else
        {
            // (代入)
            // so it must be an assignment!
            if( !aVar.IsLvalue() )
                Error( ERRCODE_BASIC_LVALUE_EXPECTED );
            TestToken( EQ );
            SbiExpression aExpr( this );
            aExpr.Gen();
            SbiOpcode eOp = SbiOpcode::PUT_;
            if( pDef )
            {
                if( pDef->GetConstDef() )
                    Error( ERRCODE_BASIC_DUPLICATE_DEF, pDef->GetName() );
                if( pDef->GetType() == SbxOBJECT )
                {
                    eOp = SbiOpcode::SET_;
                    if( pDef->GetTypeId() )
                    {
                        aGen.Gen( SbiOpcode::SETCLASS_, pDef->GetTypeId() );
                        return;
                    }
                }
            }
            aGen.Gen( eOp );
        }
    }
}
void SbiRuntime::StepPUT()
{
    SbxVariableRef refVal = PopVar();
    SbxVariableRef refVar = PopVar();
    // store on its own method (inside a function)?
    bool bFlagsChanged = false;
    SbxFlagBits n = SbxFlagBits::NONE;
    if( refVar.get() == pMeth )
    {
        bFlagsChanged = true;
        n = refVar->GetFlags();
        refVar->SetFlag( SbxFlagBits::Write );
    }

    // (デフォルトプロパティ絡みのごちゃごちゃ。基本的にVBAにしか関係ない話なので中略)

    //ええっ!条件これだけ??
    if ( !checkUnoStructCopy( bVBAEnabled, refVal, refVar ) )
        *refVar = *refVal;

    if( bFlagsChanged )
        refVar->SetFlags( n );
}
    

refVarが代入される側の変数で、refValが代入される値なのだが、この型チェックがガバガバすぎるだろう、と考えて型チェックを突っ込んだ。

もうひとつ重要なこと言わないといけない。C言語などでは、配列の添字は角括弧で、配列の引数は丸括弧だが、StarBasic含むBasic系言語は両方とも丸括弧

だから識別子の後に括弧が来た場合、それがどちらなのか考えないといけない。とりあえず、関数でないかどうかをチェックし、その後に出てくる、括弧つき識別子を配列として扱っている。ただし、デフォルトプロパティがある場合を除く。

std::unique_ptr SbiExpression::Term( const KeywordSymbolInfo* pKeywordSymbolInfo )
{
    // (中略)
    SbiSymDef* pDef = pParser->pPool->Find( aSym );
    if( !pDef )
    {
        // Part of the Runtime-Library?
        // from 31.3.1996: swapped out to parser-method
        // (is also needed in SbiParser::DefVar() in DIM.CXX)
        pDef = pParser->CheckRTLForSym( aSym, eType );
        // #i109184: Check if symbol is or later will be defined inside module
        SbModule& rMod = pParser->aGen.GetModule();
        if( rMod.FindMethod( aSym, SbxClassType::DontCare ) )
        {
            pDef = nullptr;
        }
    }
    // (中略)
}    

それと、式の値がSbxOBJECTだったときは、ドットを置くことでメンバを参照できる。

ちなみに、VBAの場合はデフォルトプロパティが絡むからもっとややこしくなる。

とりあえず、Term()は文中に登場する最初の識別子の処理で、ObjTerm()が!や.でつながった後の識別子に対する処理。

もう一つ、自分が不満に思っていたのが identifier1(1).identifier2のようなコードで、identifier1が配列、という可能性は十分考えられたのに、今までの処理の流れと自分のパッチを当てる前の修正前コードでは、 identifier1が配列の要素、それにくっついている括弧となってしまっていた。自分は、これを補正して、identifier1を配列、その後の添字で初めてSbxOBJECTの可能性が生まれる、としたかった。

もちろんこの値はSbxOBJECTとは限らず、SbxINTEGERとかかもしれないし、(SbxARRAY | SbxINTEGER)かもしれない。

自分の修正前後の比較を置いておきたい。

52601:LIKEの処理

If Not "X" Like "Y" Then

上記コードがコンパイルエラーに成る原因をSbiParserから追っていく。

void SbiParser::If()
{
    sal_uInt32 nEndLbl;
    SbiToken eTok = NIL;
    // ignore end-tokens
    SbiExpression aCond( this );
    aCond.Gen();
    TestToken( THEN );

まず、SbiParser::IfはIfの次に式が来て、その後にTHENというトークンが来るとしている。

SbiExpressionはそのコンストラクタで第一引数にSbiParserをとり、SbiExpression::Booleanを呼び出す。

std::unique_ptr<SbiExprNode> SbiExpression::Boolean()
{
    std::unique_ptr<SbiExprNode> pNd = Like();
    if( m_eMode != EXPRMODE_EMPTY_PAREN )
    {
        for( ;; )
        {
            SbiToken eTok = pParser->Peek();
            if( (eTok != AND) && (eTok != OR) &&
                (eTok != XOR) && (eTok != EQV) &&
                (eTok != IMP) && (eTok != IS) )
            {
                break;
            }
            eTok = pParser->Next();
            pNd = std::make_unique<SbiExprNode>( std::move(pNd), eTok, Like() );
        }
    }
    return pNd;
}

SbiExpression::Booleanは任意の個数のSbiExpression::Like()を呼び出すが、複数回実行されるときは、その間に論理演算子が来る。

SbiExpression::Like()は内部でそれがVBA向けかStarBasic向けかによって異なる処理を呼び出す。いずれの場合も、Like()から呼び出されるComp()またはVBA_Not()以下再帰を追っていった地点でNotが登場する。

std::unique_ptr<SbiExprNode> SbiExpression::VBA_Not()
{
    std::unique_ptr<SbiExprNode> pNd;

    SbiToken eTok = pParser->Peek();
    if( eTok == NOT )
    {
        pParser->Next();
        pNd = std::make_unique<SbiExprNode>( VBA_Not(), eTok, nullptr );
    }
    else
    {
        pNd = Comp();
    }
    return pNd;
}

メソッド呼び出しの深い順から、SbiExprNodeを作っていって上部につなげているので、Not "X" Like "Y"はVBASupport 1でもVBASupport 0でも((Not "X") Like "Y")として処理されるが、Not "X"は引数の型が期待どおりでないため、エラーとなる。

Errata

  • 関数呼び出しの場所が間違っていた。GET_じゃない。そのまえにSbiExpressionが存在していてそこでFIND_やFIND_G_やFIND_CM_が行われる。自分でも書いてて何か変だと思ってたんだ
  • 内容的には正しいが、不適切な以下の文章を削除してここに持ってきた。

    StarBasicにおける関数はどのように管理されているのかを見ていきましょう。

    StarBasicにおける関数定義はFunction 識別子(識別子 [As 識別子][,識別子 [As 識別子]]+) As 識別子という構文です。サブルーチンのときはFunctionの代わりにSubとなり、戻り値の型が消えます。

    Functionというトークンにあたると、SbiParser::SubFuncが呼ばれます。

    { FUNCTION, &SbiParser::SubFunc,    Y, N, }, // FUNCTION
    void SbiParser::SubFunc()
    {
        DefProc( false, false );
    }
    
    // Read in of a procedure
    
    void SbiParser::DefProc( bool bStatic, bool bPrivate )
    {
        sal_uInt16 l1 = nLine;
        bool bSub = ( eCurTok == SUB );
        bool bProperty = ( eCurTok == PROPERTY );
        // (プロパティではないので中略)
        SbiToken eExit = eCurTok;
        SbiProcDef* pDef = ProcDecl( false );
        if( !pDef )
        {
            return;
        }
        pDef->setPropertyMode( ePropertyMode );
    
        // Is the Proc already declared?
        SbiSymDef* pOld = aPublics.Find( pDef->GetName() );
        if( pOld )
        {
            //(既に同名の識別子が使われていた場合のエラー処理。中略)
        }
        else
        {
            aPublics.Add( pDef );
        }
    
    
    // Procedure-Declaration
    // the first Token is already read in (SUB/FUNCTION)
    // xxx Name [LIB "name"[ALIAS "name"]][(Parameter)][AS TYPE]
    
    SbiProcDef* SbiParser::ProcDecl( bool bDecl )
    {
        bool bFunc = ( eCurTok == FUNCTION );
        bool bProp = ( eCurTok == GET || eCurTok == SET || eCurTok == LET );
        if( !TestSymbol() ) return nullptr;
        OUString aName( aSym );
        SbxDataType eType = eScanType;
        SbiProcDef* pDef = new SbiProcDef( this, aName, true );
        pDef->SetType( eType );
        // (外部DLLの関数の呼び出し関連処理。それと引数リスト。中略)
        TypeDecl( *pDef ); // 戻り値の型の処理
        (中略)
        return pDef;
    }
    

    関数の定義(関数名含む)はパブリック変数等の定義を格納しているaPublicsに追加されている。

  • この下りも不要

    実際の処理はSbiRuntimeで行われる。

    void SbiRuntime::StepFIND( sal_uInt32 nOp1, sal_uInt32 nOp2 )
    {
        // 第一引数はモジュール
        StepFIND_Impl( pMod, nOp1, nOp2, ERRCODE_BASIC_PROC_UNDEFINED );
    }
    void SbiRuntime::StepFIND_Impl( SbxObject* pObj, sal_uInt32 nOp1, sal_uInt32 nOp2,
                                    ErrCode nNotFound, bool bStatic )
    {
        if( !refLocals.is() )
        {
            refLocals = new SbxArray;
        }
        // ローカル、つまりモジュールの中から探す
        // 探したものをスタックに突っ込んでいる
        PushVar( FindElement( pObj, nOp1, nOp2, nNotFound, true/*bLocal*/, bStatic ) );
    }
    SbxVariable* SbiRuntime::FindElement( SbxObject* pObj, sal_uInt32 nOp1, sal_uInt32 nOp2,
                                          ErrCode nNotFound, bool bLocal, bool bStatic )
    {
        bool bIsVBAInterOp = SbiRuntime::isVBAEnabled();
        if( bIsVBAInterOp )
        {
            // (VBAの話。中略)
        }
    
        SbxVariable* pElem = nullptr;
        if( !pObj )
        {
            Error( ERRCODE_BASIC_NO_OBJECT );
            pElem = new SbxVariable;
        }
        else
        {
            bool bFatalError = false;
            SbxDataType t = static_cast<SbxDataType>(nOp2);
            OUString aName( pImg->GetString( static_cast<short>( nOp1 & 0x7FFF ) ) );
            // Hacky capture of Evaluate [] syntax
            // this should be tackled I feel at the pcode level
        // (角括弧の評価の話。中略)
            if( bLocal )
            {
                if ( bStatic && pMeth )
                {
                    pElem = pMeth->GetStatics()->Find( aName, SbxClassType::DontCare );
                }
    
                if ( !pElem )
                {
                    pElem = refLocals->Find( aName, SbxClassType::DontCare );
                }
            }
            if( !pElem )
            {
                // (メソッド・関数が見つからなかったときの話。中略)
            }
            // #39108 Args can already be deleted!
            if( !bFatalError )
            {
                SetupArgs( pElem, nOp1 );
            }
            // because a particular call-type is requested
            if (SbxMethod* pMethod = dynamic_cast<SbxMethod*>(pElem))
            {
                // shall the type be converted?
                SbxDataType t2 = pElem->GetType();
                bool bSet = false;
                if( (pElem->GetFlags() & SbxFlagBits::Fixed) == SbxFlagBits::NONE )
                {
                    if( t != SbxVARIANT && t != t2 &&
                        t >= SbxINTEGER && t <= SbxSTRING )
                    {
                        pElem->SetType( t );
                        bSet = true;
                    }
                }
                // assign pElem to a Ref, to delete a temp-var if applicable
                SbxVariableRef refTemp = pElem;
    
                // remove potential rests of the last call of the SbxMethod
                // free Write before, so that there's no error
                SbxFlagBits nSavFlags = pElem->GetFlags();
                pElem->SetFlag( SbxFlagBits::ReadWrite | SbxFlagBits::NoBroadcast );
                pElem->SbxValue::Clear();
                pElem->SetFlags( nSavFlags );
    
                // don't touch before setting, as e. g. LEFT()
                // has to know the difference between Left$() and Left()
    
                // because the methods' parameters are cut away in PopVar()
                SbxVariable* pNew = new SbxMethod(*pMethod);
                //OLD: SbxVariable* pNew = new SbxVariable( *pElem );
    
                pElem->SetParameters(nullptr);
                pNew->SetFlag( SbxFlagBits::ReadWrite );
    
                if( bSet )
                {
                    pElem->SetType( t2 );
                }
                pElem = pNew;
            }
            // consider index-access for UnoObjects
            // definitely we want this for VBA where properties are often
            // collections ( which need index access ), but lets only do
            // this if we actually have params following
            else if( bVBAEnabled && dynamic_cast<const SbUnoProperty*>( pElem) != nullptr && pElem->GetParameters() )
            {
                SbxVariableRef refTemp = pElem;
    
                // dissolve the notify while copying variable
                SbxVariable* pNew = new SbxVariable( *pElem );
                pElem->SetParameters( nullptr );
                pElem = pNew;
            }
        }
        return CheckArray( pElem );
    }
    
    SbiSymDef* SbiSymPool::Find( const OUString& rName, bool bSearchInParents )
    {
        sal_uInt16 nCount = m_Data.size();
        for( sal_uInt16 i = 0; i < nCount; i++ )
        {
            SbiSymDef &r = *m_Data[ nCount - i - 1 ];
            if( ( !r.nProcId || ( r.nProcId == nProcId)) &&
                ( r.aName.equalsIgnoreAsciiCase(rName)))
            {
                return &r;
            }
        }
        if( bSearchInParents && pParent )
        {
            return pParent->Find( rName );
        }
        else
        {
            return nullptr;
        }
    }
    
        
    

俺が列名変換問題を解くと…

今回はLibreOfficeは直接関係ない話。

まずは俺の解き方の紹介から。

条件変更: 問題1と問題2は同時に公開されて出題されたものとする。アルゴリズムを考えればOK。Perl?知らない子ですね。

ある数の上位の桁が書かれていないときって、そこに0が書かれているって考えてもいいよね。基本的には。
例として3桁で揃えた場合、1なら001でもいいし…。
{ {A, B , C …,X , Y , Z } , {AA,AB,AC…AZ,BA,BB,BC…ZZ},{AAA,AAB,AAC…AAZ,ABA,ABB,ABC…AZZ, BAA ,…ZZZ} }
を1次配列にflattenしたやつ(以下arrとする)を、各桁{0,A,B…,Z}の27文字からなる普通の27進法として捉えてはいけない「はず」、本来ならば。
{ 000, 00A, 00B,00C 00Z, 0A0,0AA 0AB,…,0ZZ,A00, A0A, A0B, … ,ZZZ }
というパターンと一致しないから。まぁこういう形で求めておいて、後で、上記と一致しないものをカウントして差し引きして調整してもいいけどコードがごちゃごちゃしそう。似たような横着して昔音楽の筆記小テストでメジャーコードマイナーコードを書く問題でボロボロになっちゃった身としてはなおさら。

愚直に上記の群数列の何番目の群の、何番目の項目か、を計算して求めたほうが、間違いのないスッキリしたコードになりそう。各群内の何番目になるか、はA=0,B=1,Z=25として26進数扱いして求めても問題ない。

n桁の文字からなる文字列(3桁の文字列の例:XYZ)はarr内で何番目から何番目に存在しているのだろう?0スタートの列番号をxとして
{{0,1,…25},{25+1=26,25+2,…,25+26^2},{25+26^2+1=26+26^2,25+26^2+2,…,25+26^2+26^3}…}
だから
n >= 2のとき
S[n] = (∑(k=1からn-1) 26^k)
S[n+1] = (∑(k=1からn) 26^k)
と定めると
S[n] <= x < S[n+1]
か。
なお、n = 1のときは
0 <= x < 26
n >= 2においては、等比数列の和の公式から
S[n] = (26^n - 26) / (26 - 1)
S[n+1] = (26^(n + 1) - 26)/(26 - 1)
よって
(26^n - 26) / (26 - 1) <= x < (26^(n + 1) - 26)/(26 - 1)
(26^n - 26) <= (26 - 1) * x < (26^(n + 1) - 26)
26^n <= (26 - 1) * x + 26 < 26^(n + 1)
対数関数は単調増加関数だから26を底とする対数をとっても不等号の向きはそのままで
n <= Log ( (26 - 1) * x + 26, 26) < n + 1
となり
n = Floor( Log( (26 - 1) * x + 26, 26 ) )
とするとスッキリする。

後は x - S[n] を 26進数として処理すれば、xからアルファベットの列名を取り出せる。
あ、一応、xは0からカウントしていることに留意する。

例題を解いてみる。ええと、1から数えたときの16384列目は
0から数えたときはx = 16383で、Wolfram AlphaさんはC#とかと違ってLog関数の第一引数に底を取るからそのへん調整して
3桁の文字列だと分かるから
16383 - (26^3 - 26) / (26 - 1) = 15681
あとは、
15681 Mod 26 = 3
((15681 - 3) / 26) Mod 26 = 5
((((15681 - 3) / 26) - 5) / 26) Mod 26 = 23
0カウントだから
0 : A, 1 : B, 2 : C , 3 : D…
ええとXFD…かな?正解ですね。
逆にXFDを数値に直したいときは、3桁だから
(26^3 - 26) / (26 - 1) + 26^2 * 23 + 26^1 * 5 + 26^0 * 3 = 16383が求められる。1からカウントにしたければ1を足して16384

ここまでの議論を前座として

当該記事の前編と後編を通して読むと、こんなグチャグチャした場合分けしてない。うーん、最初の方で言ったとおり、そもそも各群の元の順番がぜんぜん違うんだから、普通に考えたらその計算方法では求められないはず。でも問題1の答えはあってるっぽい。検証してみるか。うーん場合分けが必要だった俺のやり方と何が違うんだ、どうしてこんなやり方で求まるんだどうして問題2で26進数扱いして戻しただけじゃ答えが一致しないんだ。なんで?どうして?

X、F、Dを後ろから一文字ずつ取得する。
D は アルファベットの4番目でかつ、後ろから1番目(0)なので「26の0乗 × 4 = 1 × 4 = 4」と評価する。
F は アルファベットの6番目でかつ、後ろから2番目(1)なので、「26の1乗 × 6 = 26 × 6 = 156」と評価する。
X は アルファベットの24番目でかつ、後ろから3番目(2)なので、「26の2乗 × 24 = 676 × 24 = 16224」と評価する。
3つの値を合計した「4 + 156 + 16224 = 16384」が答えとなる。

…結構時間がかかったが、しばらく後に重大なことに気がついた。自分はA=0とカウントしているから、D=3なのに、この文章では桁に"0"という文字があることを前提としているからD=4として扱っている。あとなぜ27進数じゃない!?もしかして。

俺は自分が使った式を見返してみる。
(26^3 - 26) / (26 - 1) + 26^2 * 23 + 26^1 * 5 + 26^0 * 3 = 16383
これはそもそも、
(26^2 + 26) + 26^2 * 23 + 26^1 * 5 + 26^0 * 3 = 16383
だったはず。
(26^2 + 26) + 26^2 * 23 + 26^1 * 5 + 26^0 * 3 = 16383
これを変形して、
26^2 * (23 + 1) + 26^1 * (5 + 1) + 26^0 * 3 = 16383
あとは、自分が、0スタートで考えたことを補正すると、
26^2 * (23 + 1) + 26^1 * (5 + 1) + 26^0 * (3+1) = 16384

つまり、文字列→数値は常に一致する。偶然に
偶然だから第二問で逆変換を行おうとしたときに元に戻せなかったんだ。(= 罠)
例えば680とか当該記事の方法でちゃんとアルファベットに出来る?
きちんと場合分けして考える分には罠でも何でもない。

ここで、後編では、時間制限無しで考えてきてレビューをし合う、ということをやった、とある。もし自分が遠慮せずにレビューをするなら、その説明タイムのときに、「どうして逆変換じゃ出来ないのか?何が違うのか?」を考えたか?説明したか?が自分の最大の評価ポイントになるし、そこが説明に出てきてなかったら点数半分からスタートだろうな、と読みながら当時思った覚えがある。マサカリとかが怖かったから、当時は自分から関わろうとはしてなかった。別に当事者にそういうキツイ空気を作ろうという人は少なかっただろうし、ある程度は自分もそういう状況であることは理解していたけど、中々勇気がでなかったな。完全に当時の俺の問題。残念ながら性格・思考傾向は今もあんまりその辺変わってない。おっと愚痴になっちゃったな。ここらで記事を締めるか。

「オプションの違い」による「印刷」と「エクスポート」の違い

LibreOffice Advent Calendar 2018の22日目の記事です。前回はRakugouさんのデータの前処理で躓いた時の話でした。

再現手順(With LibreOffice master[6.3])

  1. LibreOffice Writerを起動する
  2. メインメニューから[書式]→[ページ]
  3. [背景]タブ→[色]→[赤]→[OK]
  4. メインメニューから[ツール]→[オプション]
  5. 左のツリーから[LibreOffice Writer]→[印刷]と辿る
  6. 右側[内容]の[ページの背景]にチェックを入れる
  7. [OK]でダイアログを閉じる
  8. メインメニューから[ファイル]→[印刷]○
  9. 左下[Preview]のチェックを入れる
  10. 表示されたプレビューが赤いことを確認。
  11. [General]タブのPrinterを[Microsoft Print to PDF]に。別にプリンタ固有の問題じゃないからPrimoPDFとかでも問題ない。紙とインクがもったいないというだけの話。
  12. [OK]
  13. どっかにPDFを保存
  14. PDFのデータ側も赤くなっていることを確認
  15. LibreOffice Writerに戻って、メインメニューから[ファイル]→[次の形式でエクスポート]→[PDFとして直接エクスポート]
  16. 適当にPDFを保存
  17. エクスポートされたPDFも赤いことを確認。●
  18. メインメニューから[ツール]→[オプション]
  19. 左のツリーから[LibreOffice Writer]→[印刷]と辿る
  20. 右側[内容]の[ページの背景]のチェックを外す
  21. [OK]でダイアログを閉じる
  22. ○から●まで繰り返す
  23. 通常の編集画面のページ背景色は赤いままだが、今回はプレビュー、印刷されたもの、エクスポートされたもの全てのページ背景色が白であることを確認。
  24. 連動しているのか、…と思うでしょ?
  25. なんとなくページの背景を「なし」に戻します。(さっきわかりやすそうな色を選んだけど、それはつまり使われすぎたり見続けていると目の負担になるからで、特に意味はない)
  26. 最初の段落に"abc"と入力してこの文字列を選択状態にする
  27. メインメニューから[書式]→[文字]
  28. [フォントの効果]タブの[フォントの色]を[緑]にして[OK]
  29. メインメニューから[ツール]→[オプション]
  30. 左のツリーから[LibreOffice Writer]→[印刷]と辿る
  31. 右側[内容]の[文字を黒で印刷]のチェックを外す
  32. [OK]でダイアログを閉じる
  33. ○から●まで繰り返す
  34. 編集画面、プレビュー、印刷されたもの、エクスポートされたもの全ての文字色が緑であることを確認。
  35. ここまで雑魚戦、ここから中ボス戦。
  36. メインメニューから[ツール]→[オプション]
  37. 左のツリーから[LibreOffice Writer]→[印刷]と辿る
  38. 右側[内容]の[文字を黒で印刷]のチェックを入れる
  39. [OK]でダイアログを閉じる
  40. ○から●まで繰り返す
  41. 編集画面の文字色が緑、プレビュー、印刷されたものの文字色が黒で、エクスポートされたものの文字色が緑であることを確認。tdf#113866 印刷とPDFエクスポートは同じものか否か
  42. メインメニューの[フォーム]→[デザインモード]が選択されていることを確認し、[ボタン]をクリック、編集画面上で適当にドラッグして大きさ決めてドロップ。
  43. メインメニューから[ツール]→[オプション]
  44. 左のツリーから[LibreOffice Writer]→[印刷]と辿る
  45. 右側[内容]の[フォームコントロール]のチェックを入れる
  46. [OK]でダイアログを閉じる
  47. ○から●まで繰り返す
  48. 編集画面、プレビュー、印刷されたもの、エクスポートされたもの全てでコントロールが出力されていることを確認。
  49. メインメニューから[ツール]→[オプション]
  50. 左のツリーから[LibreOffice Writer]→[印刷]と辿る
  51. 右側[内容]の[フォームコントロール]のチェックを外す
  52. [OK]でダイアログを閉じる
  53. ○から●まで繰り返す
  54. 編集画面ではコントロールが出力され、プレビュー、印刷されたものでは高さが確保されてはいるもののコントロールが出力されずエクスポートではコントロール自体が出力されていることを確認。
  55. 以上で手順は終了です。お疲れ様でした。他にもいろいろ試すパターンはあると思うけど、とりあえず手元で確認しているのはこんなもん。宿題として「以上の動作と一致するよう、LibreOfficeのコードを引用して説明しなさい」

明日は、Enokiさんの「2018年の日本でのLibreOfficeイベントを振り返る」です。今年も残り少なくなりましたが、最後まで突っ走りましょう。

動機というかなんというか

理解されなかったり誤解されやすい関数でいつか説明したいとは思ってた。どのタイミングでどうやって切り出すか迷ってたけどいい機会なので。英辞郎によるto dateの意味

明日のLibreOffice Advent Calendar 2018はMucky999さんの「LibreOffice Conference 2018 Tirana の話」です。期待しています。

補遺

四則演算の演算子(+-*/)を関数に置き換えましたが、^とか他にも演算子はいっぱいある。>演算子とか。左側の式が大きかったときに1を右側の式が大きかったときは0を返すわけだ。
Style関数は、適用されるスタイルを変更する。
今までの議論を踏まえてCurrent関数のマニュアルのサンプルを読み、どういう結果になっているのか思いを巡らせてみると面白いと思う。