ufcpp-live / UfcppLiveAgenda

@ufcpp live streaming agenda
MIT License
24 stars 2 forks source link

Roslyn サロゲートペア対応 #6

Open ufcpp opened 4 years ago

ufcpp commented 4 years ago

配信すると決めたわけじゃないけど、メモ書き。 というか、配信で収まるメモ書き量じゃなくなってる気がする。

Unicode サロゲートペア対応

C# の識別子をサロゲートペアに対応させたいという話。

作業 branch: https://github.com/ufcpp/roslyn/tree/surrogate-pair-identifier

サロゲートペア

UTF-16 で1ユニットにならないやつ(サロゲートペアになるやつ)。 なんていうのが正式なのかいまだにわからず。

90年代~2000年前後の言語は内部的に UTF-16 で文字列を持ってるせいで、これに対応するのが大変。

C# もまさに。

C# の lexer 仕様

C# 言語仕様 lexical-structure

識別子として使える文字は Unicode カテゴリーで指定してる。 サロゲートペア文字にもちゃんとカテゴリーが割り当たってるのに、C# はこれをちゃんと見れてない。

エジプトヒエログリフ、楔形文字、非常用漢字とかが該当。「𩸽」とかは本来識別子として使えるはずだけど、C# では使えない。

補足: 他の言語

同じ「UTF-16 世代な言語」である Java は対応してる。 JavaScript も Chrome/Edge のやつは対応してる(IE のは対応してない)。 PHP も対応してそう(Web向け言語だし結構前から内部的に UTF-8 かも)。

もっと後に出てる「UTF-8 世代な言語」である Go は当然対応してる。 Python も 3 系は UTF-8 らしく、対応してそう。

Rust は潔くて、元からローマンアルファベット(ASCII の範囲内)しか認めてない(言語仕様に明記)。 Swift もある意味潔くて、U+10000 以上の文字は全部受け付けてる(これも明記)。

複雑なの、識別子だけ

identifier_or_keyword
    : identifier_start_character identifier_part_character*
    ;

identifier_start_character
    : letter_character
    | '_'
    ;

identifier_part_character
    : letter_character
    | decimal_digit_character
    | connecting_character
    | combining_character
    | formatting_character
    ;

letter_character
    : '<A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or Nl>'
    | '<A unicode_escape_sequence representing a character of classes Lu, Ll, Lt, Lm, Lo, or Nl>'
    ;

combining_character
    : '<A Unicode character of classes Mn or Mc>'
    | '<A unicode_escape_sequence representing a character of classes Mn or Mc>'
    ;

decimal_digit_character
    : '<A Unicode character of the class Nd>'
    | '<A unicode_escape_sequence representing a character of the class Nd>'
    ;

connecting_character
    : '<A Unicode character of the class Pc>'
    | '<A unicode_escape_sequence representing a character of the class Pc>'
    ;

formatting_character
    : '<A Unicode character of the class Cf>'
    | '<A unicode_escape_sequence representing a character of the class Cf>'
    ;

特に、 unicode_escape_sequence が厄介。

補足: 他、Unicode 関係しそうなところ

改行はカテゴリー指定じゃなくて、1文字1文字指定。

new_line_character
    : '<Carriage return character (U+000D)>'
    | '<Line feed character (U+000A)>'
    | '<Next line character (U+0085)>'
    | '<Line separator character (U+2028)>'
    | '<Paragraph separator character (U+2029)>'
    ;

空白には Zs カテゴリーが出てくるけど、これは特に追加されてる文字ない。全部 U+3000 以下。

whitespace
    : '<Any character with Unicode class Zs>'
    | '<Horizontal tab character (U+0009)>'
    | '<Vertical tab character (U+000B)>'
    | '<Form feed character (U+000C)>'
    ;

Identifier 的なものの lex 処理、3系統ある

参考: XML の lexer 仕様

XML の lexer 仕様

U+10000 以上全部受け付けてる。 Swift はもしかしたらこれに倣ってるのかも?(要確認)

NameStartChar ::= ... | [#x10000-#xEFFFF]

特に面倒なのが unicode_escape_sequence

↓が有効な C# コード。

#define cl\u0061ss

#if class

class @class
{
    /// <summary>
    /// <see cref=‘@class’/>
    /// </summary>
    public cl\u0061ss(int x) { }

    /// <summary>
    /// <see cref=“cl\u0061ss”/>
    /// </summary>
    public @class(double x) { }

    public static c\u200dl\u200Da\U0000200ds\U0000200Ds c;
}

#endif

↑の Visual Studio 上での見た目:

image

ufcpp commented 4 years ago

単体テストの場所を特定するところから。 とりあえず、 CharacterInfo.IsValidIdentifier/define:SYMBOL オプションのパースで呼ばれてて、CommandLineTest.cs から呼ばれてること確認済み。 この辺りにサロゲートペアなシンボル渡すテストを足してみる。

ufcpp commented 4 years ago

Visual Studio for Windows が .NET Framework 上で動いてるせいで Unicode 8か9 辺りで止まってる可能性高いみたい。

U+9FD6~U+9FFC 辺りの文字(Unicode 10.0~13.0 で追加された漢字。Other Letter)のコンパイルに失敗する。 ↓ここの辺りの文字。 image

この理屈だと、dotnet コマンドでビルドすると通るけど VS for Win ではコンパイルできない C# コードになってるかも?

ufcpp commented 4 years ago

ほんとになった。VS でエラーになって dotnet run でエラーにならない。

image

ufcpp commented 4 years ago

Swift はかなり意図的に「ほぼ何でも」っぽい。 https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID412

「BMP の non-combining な alphanumeric と、私用面じゃない non-BMP 全部」って書いてある。

ufcpp-live commented 4 years ago

cref の中は TryScanXmlEntity も対応が必要かもしれない。 &#x... みたいなやつ。 例えば、cl\u0061s&#x73;class 扱いになって有効な識別子になる。

ufcpp-live commented 4 years ago

https://youtu.be/nzLEgW0HmTQ

ufcpp-live commented 4 years ago

とりあえず放送中に当たりが付いたことまとめ:

今日、見つけた単体テストプロジェクトは2か所

これ以外に、たぶん Microsoft.CodeAnalysis.CSharp.Symbol.UnitTests も関係すると思う。 (参照した dll のメタデータ中にサロゲートペアを含む場合)

具体的な単体テストクラス:

LexicalTests はさすがにそこそこしっかりしてて、「この名前は識別子にできない」みたいなテストもあるし、非 ASCII 文字のテストもある。 \U のテストもあるけど、\U00001234 しかテストデータがない(当然、サロゲートペアのテストはない)。 cref, directive のテストは割とガバガバ…

U+1234 は見るからに適当な値だけど、エチオピアの文字らしい。有効な letter。

ufcpp commented 4 years ago

http://eel.is/c++draft/lex.name#1

Swift のは C++ に沿ってるっぽい? GCC は2015年くらいからこの仕様認めてそう。 確かでも、MS の C++ は UAX#31 だったような…MS C++ も上記仕様だった

一方で、C++ 23 目標で C++ も UAX#31 に従うべき見たいな提案見かけた。 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1949r1.html

C++/Swift のルールがどこから来たのか気になってたけど、「John looked at Unicode and cobbled the list of valid ranges manually.」(Unicode を眺めてとりあえず手動で範囲を決めた)とか書かれてて受けるww

p1949r1 の提案、UAX#31 の問題もきれいにまとまってるなぁ:

ufcpp commented 4 years ago

https://unicode.org/reports/tr31/#Immutable_Identifier_Syntax 旧 (現) C++ ルールは Immutable Identifiers って言うらしい。 一応、Unicode バージョンに依存しないって意味ではメリットあるみたい。 white space, private use, surrogate, control っていう「追加しない」って言われてるカテゴリーをベースに決めてそう。

ただ、これを使うとセキュリティガバガバだから推奨はされてないとか。

ufcpp commented 4 years ago

ここを surrogate-pair-identifier の作業メモに使うのもどうかと思いつつ…

今、抜いてるテストケースいくつか:

"a\uD800", "\uD800a", "a\uDF00", "\uDF00a", "\uD800\u200D\uDF00", // ZWJ between a valid surrogate pair 𐌀 (Old Italic A)

// Surrogate Pairs "\U000104A0\U000104A1", // 𐒠𐒡 (Osmanya Digit) "\U00020000\U0001F600\U00020000", // (Emoji between Letters) "\U0001D7D8\U0001D538", // double-struck 0A

影響を及ぼしそうな(テストの追加が必要そうな)個所:

WithCrefTypeParametersBinder.AddTypeParameters CSharpCommandLineParser.ParseConditionalCompilationSymbols CSharpCommandLineParser.ParseAssemblyReferences CSharpParseOptions.ValidateOptions PEMethodSymbol.ComputeMethodKind ISymbol.CanBeReferencedByName (Symbol, PreprocessingSymbol) SourceMethodSymbolWithAttributes.ValidateConditionalAttribute SourceNamedTypeSymbol.ValidateConditionalAttribute SourcePropertySymbol.ValidateIndexerNameAttribute