LoginSignup
0
0

TypeScriptの字句解析考察

Posted at

Typescriptのソースコード考察

本文はTypeScript言語の字句解析部分についての考察になります。

今回考察したソースファイル

src/compiler/scanner.ts
https://github.com/microsoft/TypeScript/blob/main/src/compiler/scanner.ts

・ TypeScirptの字句解析

文字

プログラムのソースコードは一つ一つの文字で組み合わせております。
(英文字、数字、空白、符号など)

一つ一つの文字は各自のコードがあります。
例えば一般的に使われている Unicodeの中で 「A」 は 41 (UTF-8)
ある文字が英文字かどうかを判別したい場合、コードが 「A」コードから「Z」の間がどうかで判別することができます。

TypeScirptの中では、CharacterCodesのenumでこれらを定義されています。

/* コードの一部だけ切り抜き */
/** @internal */
export const enum CharacterCodes {
    // ... ヌル文字、改行符などいろいろ
    nullCharacter = 0,
    maxAsciiCharacter = 0x7F,

    lineFeed = 0x0A,              // \n
    carriageReturn = 0x0D,        // \r
    lineSeparator = 0x2028,
    paragraphSeparator = 0x2029,
    nextLine = 0x0085,
    
    // ... 数字
    _0 = 0x30,
    _1 = 0x31,
    _2 = 0x32,
    _3 = 0x33,
    _4 = 0x34,

    // ... 英文字
    a = 0x61,
    b = 0x62,
    c = 0x63,
    d = 0x64,
    e = 0x65,
    f = 0x66,
    
    // ... 符号
    ampersand = 0x26,             // &
    asterisk = 0x2A,              // *
    at = 0x40,                    // @
    backslash = 0x5C,             // \

数字の判断

文字コードが 「0」 (0x30) と「9」 (0x39)の間のコードかどうかで判断することができます。

function isDigit(ch: number) {
    // TODO(cyrusn): Find a way to support this for unicode digits.
    return ch >= CharacterCodes._0 && ch <= CharacterCodes._9;
}

改行コードの判断

4種類の改行コードが対応されています。

export function isLineBreak(ch: number): boolean {
    // ES5 7.3:
    // The ECMAScript line terminator characters are listed in Table 3.
    //     Table 3: Line Terminator Characters
    //     Code Unit Value     Name                    Formal Name
    //     \u000A              Line Feed               <LF>
    //     \u000D              Carriage Return         <CR>
    //     \u2028              Line separator          <LS>
    //     \u2029              Paragraph separator     <PS>
    // Only the characters in Table 3 are treated as line terminators. Other new line or line
    // breaking characters are treated as white space but not as line terminators.

    return ch === CharacterCodes.lineFeed ||
        ch === CharacterCodes.carriageReturn ||
        ch === CharacterCodes.lineSeparator ||
        ch === CharacterCodes.paragraphSeparator;
}

空白の判断

export function isWhiteSpaceLike(ch: number): boolean {
    return isWhiteSpaceSingleLine(ch) || isLineBreak(ch);
}

/** Does not include line breaks. For that, see isWhiteSpaceLike. */
export function isWhiteSpaceSingleLine(ch: number): boolean {
    // Note: nextLine is in the Zs space, and should be considered to be a whitespace.
    // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript.
    return ch === CharacterCodes.space ||
        ch === CharacterCodes.tab ||
        ch === CharacterCodes.verticalTab ||
        ch === CharacterCodes.formFeed ||
        ch === CharacterCodes.nonBreakingSpace ||
        ch === CharacterCodes.nextLine ||
        ch === CharacterCodes.ogham ||
        ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace ||
        ch === CharacterCodes.narrowNoBreakSpace ||
        ch === CharacterCodes.mathematicalSpace ||
        ch === CharacterCodes.ideographicSpace ||
        ch === CharacterCodes.byteOrderMark;
}

変数名判断

JavaScriptの中で変数名に対していくつかのルールが設けています。
TypeScript ソースの isUnicodeIdentifierStart と isUnicodeIdentifierPart 
で変数名はルールに従えているかどうかを判断します。

/** @internal */ export function isUnicodeIdentifierStart(code: number, languageVersion: ScriptTarget | undefined) {
    return languageVersion! >= ScriptTarget.ES2015 ?
        lookupInUnicodeMap(code, unicodeESNextIdentifierStart) :
        languageVersion === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierStart) :
            lookupInUnicodeMap(code, unicodeES3IdentifierStart);
}

function isUnicodeIdentifierPart(code: number, languageVersion: ScriptTarget | undefined) {
    return languageVersion! >= ScriptTarget.ES2015 ?
        lookupInUnicodeMap(code, unicodeESNextIdentifierPart) :
        languageVersion === ScriptTarget.ES5 ? lookupInUnicodeMap(code, unicodeES5IdentifierPart) :
            lookupInUnicodeMap(code, unicodeES3IdentifierPart);
}

変数名が長い場合もありますので、中身は二分探索法を使ってメモリを節約します。


const unicodeES5IdentifierStart = [170, 170, 181, ........]
const unicodeES5IdentifierPart = [170, 170, 181, ........]

function lookupInUnicodeMap(code: number, map: readonly number[]): boolean {
    // Bail out quickly if it couldn't possibly be in the map.
    if (code < map[0]) {
        return false;
    }

    // Perform binary search in one of the Unicode range maps
    let lo = 0;
    let hi: number = map.length;
    let mid: number;

    while (lo + 1 < hi) {
        mid = lo + (hi - lo) / 2;
        // mid has to be even to catch a range's beginning
        mid -= mid % 2;
        if (map[mid] <= code && code <= map[mid + 1]) {
            return true;
        }

        if (code < map[mid]) {
            hi = mid;
        }
        else {
            lo = mid + 2;
        }
    }

    return false;
}

isUnicodeIdentifierStart と isUnicodeIdentifierPart 合わせて、変数名のバリデーションが正しいかどうか判断することができます

/** @internal */
export function isIdentifierText(name: string, languageVersion: ScriptTarget | undefined, identifierVariant?: LanguageVariant): boolean {
    let ch = codePointAt(name, 0);
    if (!isIdentifierStart(ch, languageVersion)) {
        return false;
    }

    for (let i = charSize(ch); i < name.length; i += charSize(ch)) {
        if (!isIdentifierPart(ch = codePointAt(name, i), languageVersion, identifierVariant)) {
            return false;
        }
    }

    return true;
}

おわり

普段使っているTypeScriptの中身を見ると意外におもしろいかと思いました。
次回はchecker.tsのエラー分析なども見たいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0