Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

C# コンパイラーの書き換え作業の話

1,196 views

Published on

.NETラボ 勉強会 2020年6月
https://dotnetlab.connpass.com/event/177504/presentation/
にて登壇。

概要:
最近、暇を見てはC#コンパイラー(Roslyn)の修正作業をしています。C#は現在、Unicodeサロゲートペアな文字を識別子として受け付けない問題があって、その修正作業をしています。
本日は、自分が修正作業をしている経緯や、C#以外のプログラミング言語を含めて、識別子として使える文字に関する話、ソースコードを眺めていて初めて気付いたくらい自分も知らなかったレアなC#構文について紹介しようかと思います。

Published in: Technology
  • Be the first to comment

C# コンパイラーの書き換え作業の話

  1. 1. C#コンパイラーの 書き換え作業の話 岩永信之
  2. 2. 今日話すこと • C#コンパイラー(Roslyn)を書き換えてみてる • どの部分を書き換えてるか → 識別子のサロゲートペア対応 • どうしてそういう作業をしているか → 需要的に微妙なものは外部貢献でないと進まない • 何が課題か → UTF-16の面倒な点 • ソースコードを眺めていて初めて知ったことが結構ある • 未だ知らなかった言語仕様もいっぱい
  3. 3. サロゲートペア識別子 C#、実は…
  4. 4. まずこちらをご覧ください • 何気ないVisual Studio上のコードの様子
  5. 5. ここで漢字クイズ • 何と読むでしょう? 𩸽 𩹉 … ホッケ … トビウオ お魚大好き日本人の日本人 による日本人のための漢字 普通使わない (カタカナで書く方が普通)
  6. 6. まあ、問題は読み方ではなく • Unicode的にどう表すでしょう? 𩸽 𩹉 … ホッケ … トビウオ 16進数5ケタ(2バイトに収まらない) UTF-16での表現が面倒 U+29E3D U+29E49 Unicode符号点
  7. 7. 2バイトを超える文字は後付け • Unicodeは元々2バイト固定長のつもりだった • 65535文字あれば足りると思ってた • 全然足りてなくて値を追加 • ちなみに現在(Unicode 13.0時点)、14万文字使ってる a … U+0061 (ラテン文字の) α … U+03B1 (ギリシャ文字) あ … U+3042 (ひらがな) … U+1F60A (絵文字) 追加
  8. 8. いわゆるサロゲートペア • 追加分をどう表すか a … U+0061 α … U+03B1 あ … U+3042 … U+1F60A UTF-16 UTF-8 0061 03B1 3042 D83D DE0A 61 CE B1 E3 81 82 F0 9F 98 8A 元々固定長だった前提が崩れて混乱 元々可変長だったので ダメージ少なめ2文字を使って1文字を代用(surrogate)する = surrogate pair
  9. 9. .NETのstring • .NETのstring型はUTF-16 • 1990年代のプログラミング環境はUTF-16が多かった • その流れでJavaとか.NETはUTF-16 0061 03B1 3042 D83D DE0A a α あ 2文字(4バイト)で1組ほとんどの文字が2バイト固定長
  10. 10. 今のC#コンパイラー • 今のC#コンパイラーはC#で書かれてる • 普通にstringを使っているので、UTF-16 • で、サロゲートペアへの対処が入ってない • 2文字扱いする • 文字カテゴリーを正しく見てない 0061 03B1 3042 D83D DE0A a α あ Llカテゴリー: Ll Lo Cs Cs 本来のカテゴリーは So (Symbol, Other) 2文字別々に判定されてて Cs (Other, Surrogate)に
  11. 11. 改めて、こちらをご覧ください • 何気ない(?) Visual Studio上のコードの様子 普通のVisual Studio手元のVisual Studio 𩸽、𩹉はLo (Letter, Other) なので識別子にできる Cs (Other, Surrogate)扱い になって識別子にできない
  12. 12. “手元の”Visual Studio • 要するに自分で修正作業中 https://github.com/ufcpp/roslyn/tree/surrogate-pair-identifier
  13. 13. ちなみに • サロゲートペア、かつ、letter (識別子利用可)な文字の他の例 楔形文字 (古代シュメール) ヒエログリフ (古代エジプト) 異体字 (葛の字の別書体)
  14. 14. 経緯 自分がこの作業をやるまでの流れ
  15. 15. 知ってた • 相当昔から既知の問題 • 少なくとも5年前にブログを書いてる • サロゲートペア識別子 • 少なくとも2016年にはマイクロソフト内の人も認識 • Compiler does not correctly interpret surrogate pairs #9731 • 分かってれば直るというものじゃない
  16. 16. 誰得 • 需要が低すぎて直す動機にならない • そもそもnon-ASCIIな文字自体が識別子に使われない • Unicode登場以前はそもそも使えなかった • 英語に混ざると気持ち悪い • いちいちIMEをオン/オフするのがめんどくさい • IMEとコード補完の相性が悪すぎる
  17. 17. 誰得 • ましてサロゲートペア • 使いたい文字、ある? • 難読漢字 … 𩸽、𩹉 • 異体字 … 葛󠄀 (葛の異体字) • 古語 … ヒエログリフ、楔形文字、等々(現存話者0) • しいて言うなら中国人にはちょっとだけ需要がある • 𩸽よりは使う漢字があるっぽい • ときどき「使いたい文字はあるか?」とアンケートを取るけども… 「ヒエログリフの方がまだ使いたい」 (難読漢字よりは)
  18. 18. ちなみに、どのみち絵文字は使えない • 絵文字 = 欧米人が一般的に認識してる唯一のサロゲートペア • 需要があるとしたら絵文字 • 絵文字は全部Symbol, Other (C#での識別子利用不可) • サロゲートペアでなくても識別子に使えない • ◇(ひし形)とか∂(数学記号)とかと同じカテゴリー • 絵文字を使えるプログラミング言語でも… • カンファレンスでの「スライド映え」扱い • エンターテインメントであって実用じゃない
  19. 19. 需要に対して、かかるコスト • C#言語仕様的には変更不要 • 仕様書には「Unicodeカテゴリーを見て判定」としか書かれてない • 「サロゲートペアに対応しない」とも書かれてない • 「プラットフォームのUnicodeバージョンに従う」と書かれてる • 今の𩸽とかを受け付けない状態は単に実装上のバグ 言語仕様としては変更0 = ノーコスト
  20. 20. 需要に対して、かかるコスト • コンパイラーのパフォーマンス • どのみちASCII文字だけ特殊対応したfast pathがある • slow pathでの処理が多少増えても大した影響はない C#チーム的にも許容可能とのこと private bool ScanIdentifier(ref TokenInfo info) { return ScanIdentifier_FastPath(ref info) || : ScanIdentifier_SlowPath(ref info)); } ASCII文字しか見てない 普段こっちに来ない
  21. 21. 需要に対して、かかるコスト • ソースコードの修正コスト • UTF-16でサロゲートペア対応するのはそれなりに面倒 • 識別子以外の部分にも影響あり (全部テストが必要) • csc /define オプション • DLLからのメタデータ読み込み • 文字列リテラルと共通処理あり C#コンパイラー(Roslyn)の 単体テストは15万件超えてる C#チーム自ら取り組みたくはない(あまりにも低需要) ただし、コミュニティ貢献は受け付ける まあ、暇をみて自分がやるわ (今ここ)
  22. 22. 自分がやる動機 • 今時サロゲートペアに対応してないのはみっともない • Go, Java, JavaScript辺りは似たような言語仕様でサロゲートペアに対応 してるのに • Unicodeベースの仕様なのにサロゲートペアに対応できないのは中途 半端 • いっそ「ASCIIのみ受け付け」くらい割り切ってるならともかく これだけがモチベーション 別に使いたい文字はない
  23. 23. 自分も最近までやる気になってなかった • .NET標準のUnicodeカテゴリー判定APIがいまいちで… • CharUnicodeInfoクラス • .NET Core 3.0 (2019年9月)でようやく UnicodeCategory GetUnicodeCategory(char ch); UnicodeCategory GetUnicodeCategory(string s, int index); サロゲートペア未対応(戦犯) いちいちstring化が必要で非効率 UnicodeCategory GetUnicodeCategory(int codePoint); やっとまともにサロゲートペアに対応
  24. 24. これがないと始まらない • というか、このAPI自体自分が提案を出してる • GetUnicodeCategory(int codePoint) を提案してみた • 元からprivateメソッドしてはあった • (string s, int index) のオーバーロードが内部的に呼んでた • それをpublicにしてくれと頼んだだけ • すぐ(2018年1月)に対応してもらえたけど… • もう.NET Core 3.0 (2019年9月)までメジャー リリースがなかった UnicodeCategory GetUnicodeCategory(int codePoint);
  25. 25. それが来たならやる気出そうか • 今年1月に、C#コンパイラーが .NET Core 3.1 に対応 • Target netcoreapp3.1 #40861 ※ ※ netstandard2.0とnetcoreapp3.1のマルチターゲット ちなみに、Visual Studioは.NET Frameworkで動いてるので どのみちnetstandard2.0のまま… これを見てやる気に
  26. 26. ということで、じわじわと作業中 • 2月くらいから着手 • 片手間でちょっとずつ • (今も、この発表資料作りで作業止まってる) • 5月下旬にやっとそれっぽく動くように • まだバグあり • テスト足りてない • パフォーマンス計測もしたい • 正式提案文章書きたい
  27. 27. ソースコードを触ってみて ソースコードを触ってみてて初めて知った挙動とか 思ってたよりも大変そうな仕様とか
  28. 28. ソースコードを触ってみて初めて知る • 今までまったく知らなかった機能もちらほら • 仕様書上どこにもない実装もちらほら • ソースコードを触ろうとすると思った以上に面倒だった仕様も • ここから先の話はその手のトリビア
  29. 29. Unicodeエスケープ • u+4桁、U+8桁の16進数で、Unicode符号点の数値直打ち "u0041u03B1u3042U0001F60A" "aαあ "
  30. 30. Unicodeエスケープなサロゲートペア • u の後ろにサロゲートペアは書けるべきか 1F60A Unicode符号点 D83D DE0A UTF-16 "U0001F60A" "uD83DuDE0A" U+8桁エスケープ: u+4桁エスケープ: • 現在ではあまり推奨されてない (U+8桁推奨) • C#みたいなUTF-16言語は受け付けちゃう • Javaとかは U エスケープがなくてこう書くしかない • GoみたいなUTF-8言語は受け付けない
  31. 31. Unicodeエスケープ識別子 • 文字列リテラルの外でもUnicodeエスケープ可能 • Javaに至ってはキーワードや (), {} などもエスケープ可能 var u0061 = 1; Write(u0061); var a = 1; Write(a); var u0061 = 1; Write(a); var a = 1; Write(u0061); 4つとも同じ意味 class A{} u0063u006Cu0061u0073u0073u0020u0041u007Bu007D 同じ意味 (JavaとかJavaScriptでも この手のエスケープ可能)
  32. 32. じゃあ、サロゲートペアは? • u エスケープしたサロゲートペアは有効な識別子? • Javaは受け付ける • JavaScriptは受け付けない • C#をサロゲートペア対応させる場合は? (受け付けるように作業中) var 𩸽 = 1; Write(𩸽); var 𩸽= 1; Write(uD867uDE3D); 𩸽をUTF-16化したもの
  33. 33. 片方だけエスケープした場合は… • 例えば以下のJavaScriptコードは有効 • さすがに「半分だけエスケープ」には対応させたくない… • サロゲートペアの片割れ単体はUTF-16からの復号時点でエラー処理すべき var 𩸽 = 1; eval(eval(""uD867uDE3D"")) 1重エスケープ 2重エスケープ 1回目のeval: 2回目のeval: デコードされ切る 1回目のまま uDE3D になる デコードされ切る
  34. 34. 実は3系統ある識別子lexer • 3系統あって、それぞれちょっとずつ字句解析仕様が違う ディレクティブ cref (XMLドキュメントコメント) 通常のC#コード
  35. 35. ディレクティブは@を受け付けない • キーワードの前に@を付けるとキーワードじゃなくなるやつ • ディレクティブの中では受け付けない @を書くとエラーになる
  36. 36. というか、classがキーワードじゃない • ディレクティブ中はキーワード判定がまるで別物 普通にclassを書ける
  37. 37. ディレクティブ中限定キーワード • むしろ、見慣れないキーワードがいくつかある • ディレクティブの引数になるもの ディレクティブ中でだけキーワードなやつ (defineとかで使うとエラー) 通常のキーワードはむしろディレク ティブ中ではキーワードじゃない
  38. 38. ディレクティブ中限定キーワード • 単体テストコードを見てて初めて知った… [Fact] [Trait("Feature", "Directives")] public void TestNegDefKeywordExhaustive() { var text = @"#define true #define false #define default #define hidden #define checksum #define disable #define restore "; ... 「これが全部エラーを起こす」 というテスト
  39. 39. この話、仕様書のどこにもなかったり • 仕様書上はこう pp_declaration : whitespace? '#' whitespace? 'define' whitespace conditional_symbol pp_new_line | whitespace? '#' whitespace? 'undef' whitespace conditional_symbol pp_new_line ; conditional_symbol : '<Any identifier_or_keyword except true or false>' ; trueとfalseを除く任意の 「識別子もしくはキーワード」 #define, #undefの 後ろに書けるものは…
  40. 40. enable, disableはわかりやすい • #pragma warningとか#nullable 未使用privateフィールド の警告抑止 null検証だけ警告になってる null検証を有効化 無警告 (点線が出てるのは別件)
  41. 41. hiddenは#lineディレクティブで使う • デバッグシンボル上、行を隠す F10ステップイン実行で この行には止まらなくなる
  42. 42. checksumは#pragmaがある • 曰く「Visual Studio デバッガーは、常に正しいソースを検出す るために、チェックサムを使用します」 • ASP.NET (たぶん、Razorコード生成)とかで使うらしい • Roslynとかdotnet/runtimeとかの中では単体テストを除いて用例0件
  43. 43. XML部分は文法チェック緩い • XML文法違反してても正しく識別子を拾えてる "" がないけど識別子Aは 解釈できてる Aの情報が参照できてる 「""が抜けてるよ」 という専用警告メッセージ
  44. 44. “”‘’ 特殊対応がある • Non-ASCII引用符判定メソッド internal static bool IsNonAsciiQuotationMark(char ch) { switch (ch) { case '¥u2018': //LEFT SINGLE QUOTATION MARK case '¥u2019': //RIGHT SINGLE QUOTATION MARK return true; case '¥u201C': //LEFT DOUBLE QUOTATION MARK case '¥u201D': //RIGHT DOUBLE QUOTATION MARK return true; default: return false; } } “quote” ‘quote’ U+201C U+201D U+2018 U+2019
  45. 45. “”‘’ 特殊対応がある • 用途: エラーメッセージ変える これでもなおAの情報 が参照できてる 専用警告メッセージ 「Non-ASCIIな引用符はダメ!」
  46. 46. “”‘’ 特殊対応がある • お前か!お前のせいか!
  47. 47. cref中専用構文 • XMLなので <> が使いにくい → {} で代用を認めてる • XMLなのでXML仕様のエンティティ宣言を使える /// <summary> /// <see cref="Abc{int}"/> /// </summary> struct Abc<T> { } Abc{int} と書いて Abc<int> の意味になる /// <summary> /// <see cref="Abc{int}"/> /// </summary> struct Abc<T> { } A と書いて A の文字を表せる
  48. 48. crefはディレクティブほど特殊じゃない • {}とエンティティ宣言以外は通常C#と共通 /// <summary> /// <see cref="u0041bc{int}"/> /// </summary> struct Abc<T> { } /// <summary> /// <see cref="@class"/> /// </summary> class @class { } u のUnicodeエスケープもできる (&#エスケープとの混在も可) @ のverbatim識別子も使える (キーワードも識別子利用可能)
  49. 49. 3重保守 • 似て非なるlexerが3つ bool ScanDirectiveToken(ref TokenInfo info) { ... } bool ScanIdentifier_CrefSlowPath(ref TokenInfo info) { ... } bool ScanIdentifier_SlowPath(ref TokenInfo info) { ... } ScanIdentifier_SlowPath と似たようなコード ScanIdentifier_SlowPath と似たようなコード 180行くらいのswitch 150行くらいのswitch 150行くらいのswitch サロゲートペア対応すると 20行ずつくらい増える
  50. 50. Unicodeエスケープを受け付けない場面 • Unicodeエスケープが元の文字と扱い違う構文があった • string interpolation中の {} string interpolationの中で { を使いたいなら Unicodeエスケープじゃなくて {{ を使って { が U+007B で } が U+007D
  51. 51. Unicodeバージョンと.NET • Visual Studio はいまだに .NET Framework で動いてる • .NET Framework 4.8の Unicode バージョンは 9.0 で止まってる • .NET Core 3.1は Unicode バージョン 11.0 • Unicode 10.0とか11.0で追加された文字を使うと… • dotnet build できるけど Visual Studio 上でエラー
  52. 52. Unicodeには文字の追加がある • サロゲートペアじゃない文字でも… • 一部の漢字はU+9F00近辺に追加されてる (大部分の追加漢字はサロゲートペア) • Unicode 8.0 : U+9FCD〜U+9FD5 の9文字 • Unicode 10.0 : U+9FD6〜U+9FEA の21文字 • Unicode 11.0 : U+9FEB〜U+9FEF の5文字 • Unicode 13.0 : U+9FEB〜U+9FFC の13文字 例えばこんな文字らしい (Windowsにはフォント入ってない)
  53. 53. Unicode 10.0で追加された漢字 dotnet build できるけど Visual Studio 上でエラー
  54. 54. まとめ • C#コンパイラーの修正作業中 • 動機「今時サロゲートペアに対応してないのはみっともない」 • 需要はない… • 自分でソースコードを触っていて初めて知ることも多々 • 仕様書にないものを発掘したりも • UTF-16めんどくさい…

×