신틸라 어휘분석기를 작성하는 법

어휘분석기는 특정 언어에 대하여 어떻게 지정한 범위의 텍스트에 색을 입혀야 할지 결정한다. 어휘분석기를 작성하는 것은 상대적으로 쉽다. 주어진 텍스트에 색깔만 입히면 되기 때문이다. 얼마나 많은 텍스트를 실제로 색을 입혀야 할지 결정하는 더 어려운 작업은 그 어휘분석기를 호출한 즉, 신틸라가 처리한다.

매개변수

언어 LLL에 대한 어휘분석기는 원형이 다음과 같다:

    static void ColouriseLLLDoc (
        unsigned int startPos, int length,
        int initStyle,
        WordList *keywordlists[],
        Accessor &styler);

styler 매개변수는 Accessor 객체이다. 어휘분석기는 이 객체를 사용하여 색칠 될 텍스트에 접근해야 한다. 어휘분석기는 styler.SafeGetCharAt(i)를 사용하여 i에 위치한 문자를 얻는다; startPos와 length 매개변수는 색칠 될 텍스트 범위를 표시한다; 어휘분석기는 startPos부터 startPos+length까지의 모든 문자에 대하여 적절한 색을 결정해야 한다. initStyle 매개변수는 초기 상태를 가리킨다. 즉, startPos 앞에 있는 문자의 상태를 말한다. 상태는 특정 범위의 텍스트에 사용될 색을 가리키기도 한다.

주의: StartPos 위치의 문자는 줄을 시작하는 것으로 간주되므로, newline이 initStyle 상태로 끝나면 어휘분석기는 기본 상태로 들어가야 한다 (아니면 어떤 상태이든 initStyle을 따르는 상태로).

keywordlists 매개변수는 어휘분석기가 인식해야 하는 키워드를 지정한다 . WordList 클래스 객체는 간결하게 키워드를 인지하는 메쏘드가 포함되어 있다. 현재 어휘분석기는 classifyWordLLL라고 부르는 도움자 함수를 사용하여 키워드를 인지한다. 이 함수들은 keywordlists 매개변수를 사용하여 키워드를 인지하는 법 보여준다. 이 문서는 키워드를 더 이상 언급하지 않는다.

어휘분석기 코드

어휘분석기의 임무는 간략하게 요약할 수 있다: 각 r 범위의 문자들에 대하여 같은 색으로 칠하려면, 어휘분석기는 다음과 같이 호출해야 한다

styler.ColourTo(i, state)

여기에서 i는 범위 r에서 마지막 문자의 위치이다. 어휘분석기는 상태 변수를 위치 i의 문자의 색칠 상태에 설정하고, 계속해서 전체 텍스트를 색칠해야 한다.

주의 1: 스타일러(styler (Accessor)) 객체는 이전에 styler.ColourTo를 호출할 때의 매개변수 i를 기억한다. 그래서 i 매개변수 한 개면 한 범위의 문자들을 표시하는데 충분하다.

주의 2: styler.ColourTo(i,state)를 호출하면 그 부작용으로서, 해당 범위의 모든 문자들의 색칠 상태가 기억되므로 신틸라는 앞으로 어휘분석기를 호출 할 때 initStyle 매개변수를 올바르게 설정할 수 있다.

어휘 분석기의 조직

각 어휘분석기의 코드를 조직하는데 적어도 두 가지 방법이 있다. 현재 어휘분석기는 "문자-기반의" 접근법을 사용한다: 바깥쪽의 회돌이는 각 문자들을 다음과 같이 반복한다:

  lengthDoc = startPos + length ;
  for (unsigned int i = startPos; i < lengthDoc; i++) {
    chNext = styler.SafeGetCharAt(i + 1);
    << handle special cases >>
    switch(state) {
      // 처리자는 ch와 chNext만 조사한다.
      // 처리자는 상태가 변하면 styler.ColorTo(i,state)를 호출한다.
      case state_1: << handle ch in state 1 >>
      case state_2: << handle ch in state 2 >>
      ...
      case state_n: << handle ch in state n >>
    }
    chPrev = ch;
  }
  styler.ColourTo(lengthDoc - 1, state);

대안으로 "상태-기반의" 접근법을 사용할 수도 있겠다. 바깥쪽 회돌이는 다음과 같이 상태를 반복한다:

  lengthDoc = startPos+lenth ;
  for ( unsigned int i = startPos ;; ) {
    char ch = styler.SafeGetCharAt(i);
    int new_state = 0 ;
    switch ( state ) {
      // 다음 상태를 설정하면 스캐너는 new_state를 설정한다  if they set the next state.
      case state_1: << scan to the end of state 1 >> break ;
      case state_2: << scan to the end of state 2 >> break ;
      case default_state:
        << scan to the next non-default state and set new_state >>
    }
    styler.ColourTo(i, state);
    if ( i >= lengthDoc ) break ;
    if ( ! new_state ) {
      ch = styler.SafeGetCharAt(i);
      << set state based on ch in the default state >>
    }
  }
  styler.ColourTo(lengthDoc - 1, state);

이 접근법이 더 자연스러워 보일지도 모르겠다. 상태 스캐너는 문자 스캐너보다 더 간단한데 그 이유는 처리해야할 양이 적기 때문이다. 예를 들어, 스캐너 안에서 C 주석에 대하여 C 문자열의 시작을 테스트할 필요가 없다. 또 이런식으로 하면 여러 스캐너에서 사용될 수 있도록 자연스럽게 루틴을 정의할 수 있다; 예를 들어, scanToEndOfLine 루틴이 그렇다.

그렇지만, 이와 같은 문자-기반의 접근법으로 주 회돌에서 처리되는 특수한 경우라면 각각의 상태 스캐너에서 처리했어야 할 것이다. 그래서 두 접근법 모두 장점이 있다. 이런 특수한 경우들을 아래에 다룬다.

특수 사례: 선두 문자(Lead characters)

선두 바이트(Lead bytes)는 DBCS 처리 과정중의 하나이다. Euc-Kr 인코딩을 사용하는 한국어와 같은 언어를 처리할 때 사용된다. DBCS 인코딩에서는 확장 (16-비트) 문자들은 선두 바이트 다음에 꼬리 바이트가 따르도록 인코드된다.

선두 바이트는 어휘적으로 의미를 갖는 경우가 거의 없다. 보통 문자열과 주석 정도로만 허용될 뿐이다. 그런 문맥에서, styler.IsLeadByte(ch)가 TRUE를 돌려주는 경우 어휘분석기는 ch를 무시해야 한다.

주의: UTF-8은 Euc-Kr 보다 단순하므로, 별 특별한 처리가 필요 없다. 모든 UTF-8 확장 문자들은 >= 128이며 프로그래밍 언어에서 어휘적으로 의미를 갖는 문자는 없다. 지금까지도, 연산자와 주석 등등에 ASCII 문자를 사용한다..

특별한 사례: 접기(Folding)

접기가 어휘분석기 함수에서 수행될 수도 있다. 따로 접기 함수를 사용하는 것이 더 좋다. 그렇게 하면 스타일링과 폴딩 사이의 상호작용에 문제를 피할 수 있기 때문이다. 접기 함수는 접기가 활성화되어 있을 경우 어휘분석기 함수 다음에 실행된다. 지금부터는 어휘분석기 함수 안에서 접기를 수행하는 법을 설명한다.

초기화 중에, 접기 세트를 지원하는 어휘분석기

     bool fold = styler.GetPropertyInt("fold");

편집기에 접기가 켜져 있으면, fold는 TRUE이고 어휘분석기는 다음을 호출해야 한다:

     styler.SetLevel(line, level);

각줄의 끝에서 그리고 종료하기 바로 전에 말이다

line 매개변수는 그냥 새줄 문자의 개수이다. 초기 값은 styler.GetLine(startPos)이고 새줄문자가 나타날 때마다 (styler.SetLevel를 호출한 다음) 증가한다.

level 매개변수는 하위 12 비트에 희망하는 들여쓰기 수준 그와 더불어 상위 4 비트에 플래그 비트이다. 들여쓰기 수준은 언어에 따라 다르다. C++이라면, 어휘분석기가 (물론, 문자열과 주석 바깥에서) '{'를 볼 때 증가하고 '}'를 볼때 감소한다.

다음의 플래그 비트는 Scintilla.h에 정의되어 있는데, flags 매개변수에서 설정하거나 지울 수 있다. SC_FOLDLEVELWHITEFLAG 플래그가 설정되면 어휘분석기는 해당 줄에 오로지 공백문자만 들어 있다고 간주한다. SC_FOLDLEVELHEADERFLAG 플래그는 해당 줄에 접기 점이라고 표시한다. 이는 보통 현재 줄보다 다음 줄이 들여쓰기 수준이 더 크다는 뜻이다. 그렇지만, 어휘분석기는 어떤 다른 기준으로 접기 점을 결정할 수도 있다. 예를 들어, 어휘분석기는 함수 정의의 마지막 줄이 아니라 첫 줄에 헤더 줄을 만들 수도 있다.

SC_FOLDLEVELNUMBERMASK 마스크는 level 매개변수의 하위 12 비트에 있는 레벨 번호를 나타낸다. 이 마스크는 플래그나 레벨 번호를 구분하는데 사용될 수 있다.

예를 들어, C++ 어휘분석기는 새줄이 보이면 다음과 같은 코드가 있다:

  if (fold) {
    int lev = levelPrev;

    // 줄이 비어 있으면 "모두 공백(all whitespace)" 비트를 설정한다.
    if (visChars == 0)
      lev |= SC_FOLDLEVELWHITEFLAG;

    // Set the "header" bit if needed.
    if ((levelCurrent > levelPrev) && (visChars > 0))
      lev |= SC_FOLDLEVELHEADERFLAG;
      styler.SetLevel(lineCurrent, lev);
        
    // 현재 줄을 기술하는 접기 변수들을 재초기화한다.
    lineCurrent++;
    visChars = 0;  // 줄에 있는 비-공백 문자의 개수.
    levelPrev = levelCurrent;
  }

다음 코드는 C++ 어휘분석기에 나타난다. 종료하기 바로 전 코드이다:

  // 나중에 채워질 것이라고 현재 플래그를 기록하면서
  // 다음 줄의 실제 레벨을 채운다. 
  if (fold) {
    // 앞 플래그만 남기면서, 레벨 번호를 가린다.
    int flagsNext = styler.LevelAt(lineCurrent);
    flagsNext &= ~SC_FOLDLEVELNUMBERMASK;
    styler.SetLevel(lineCurrent, levelPrev | flagsNext);
  }

수행성능은 걱정할 필요가 없다

어휘분석기를 작성하는 사람은 수행성능에 대한 고민을 무시해도 안전하다: 화면을 다시 그리는 비용은 함수를 호출하는 등등의 비용에 비해 수 백배는 더 크다. 게다가, 신틸라는 모든 중요한 최적화를 수행한다; 신틸라는실제로 색칠될 텍스트만 색칠하기 위해 어휘분석기를 호출할 것이라고 보증한다. 마지막으로, 불필요하게 styler.ColourTo를 호출하지 않으려고 노력할 필요가 없다:

sytler 개체는 ColourTo에 대한 호출을 버퍼링하여 화면에 여러 번 갱신되는 것을 피한다.

이 페이지는 에드워드 륌(Edward K. Ream)이 공헌함