Hejlsberg の発表を見た
TS Go の主眼はやはりコンパイラを並列にすること。Parser と Binder(シンボル割当)はやるとして、今のボトルネックの TypeChecker(型解析機) を並列化する。
初期設定では TypeChecker を 4つ作成して、読み込んだソースを sharding して割当して、解析結果を共有メモリに置く。
現在の進捗
最初に 2ヶ月掛けて parser と scanner を手作業で移植した。
Go版 ASTができたら line-to-line で置き換える AST Transformer を作成し、一気に変換した。
厳密なトランスパイラではない。発生したエラーは手作業で潰した。
LSP を実装するが、LanguageService は移植しない。
代わりに、IPC によるAPI層を作っている(WIP)。 ts-morph や linter 連携はここになりそう。
@typescript/native-preview を動かす。
このzod のコードを対象にする
import z from "zod";
const schema = z.object({
name: z.string(),
age: z.number().int().positive(),
email: z.string().email(),
isActive: z.boolean().optional(),
});
type Result = z.infertypeof schema>;
const _: Result = {
name: "John Doe",
age: "Invalid Age",
email: "[email protected]",
isActive: true,
};
zod は内部でかなり複雑な型推論をしているので、これが動けばそこそこ信頼できる。
インストール。
npm add @typescript/native-preview
このパッケージに tsgo
コマンドがある。これが tsc 相当
cli から動かす。
$ npx tsc -p . --noEmit
src/main.ts:15:3 - error TS2322: Type 'string' is not assignable to type 'number'.
15 age: "Invalid Age", // This will cause a type error
~~~
src/main.ts:5:3
5 age: z.number().int().positive(),
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The expected type comes from property 'age' which is declared here on type '{ name: string; age: number; email: string; isActive?: boolean | undefined; }'
Found 1 error in src/main.ts:15
vscode の LSPで動かしたければ、これをいれる。
.vscode/settings.json
{
"typescript.experimental.useTsgo": true
}
推論は同じように動いてそうだが、 ファイル出力は怪しい。少なくとも declaration が無視されて、.d.ts
の出力ができなかった。 sourcemap はいけた。
typescript-go を読む
Gemini 2.5 に要約させる。
# TypeScript Go 移植実装リポジトリ解説
このドキュメントは、Microsoft による TypeScript の Go 移植実装リポジトリの全体構造、特に Language Server Protocol (LSP) とプロセス間通信 (IPC) の実装に焦点を当てて解説します。
## 1. リポジトリの全体構造
このリポジリは、TypeScript のコンパイラと Language Service の機能を Go 言語で再実装したものです。主要なディレクトリとパッケージは以下の役割を担っています。
- **`cmd/tsgo/`**:
- `main.go`: エントリポイント。`tsgo` コマンドの実行を制御します。
- `lsp.go`: LSP サーバーのエントリポイント。LSP クライアント(VS Code など)との通信を確立し、Language Service の機能を提供します。
- `api.go`: CLI 経由で利用可能な API コマンドを定義します。
- **`internal/`**:
- このディレクトリには、TypeScript のコアロジックを Go で実装した内部パッケージ群が含まれています。
- `internal/ast/`: 抽象構文木 (AST) の定義と操作に関するコードが含まれます。TypeScript のソースコードを解析して AST を構築する基盤となります。
- `internal/checker/`: 型チェッカーの実装です。AST を走査し、TypeScript の型システムに基づいて型チェックを行います。
- `internal/compiler/`: コンパイラの主要なロジックが含まれます。プログラムの構築、ソースファイルの管理、コンパイルオプションの処理などを行います。
- `internal/ls/`: Language Service (LSP 機能) の実装です。LSP リクエストを処理し、補完、定義へのジャンプ、ホバー情報などの機能を提供します。
- `languageservice.go`: `LanguageService` 構造体の中心的な定義と、LSP 機能へのエントリポイントとなるメソッドが含まれます。
- `api.go`: `LanguageService` が提供するセマンティックな情報取得 API(例: `GetSymbolAtPosition`)が含まれます。
- `completions.go`: 補完機能 (`textDocument/completion`) の詳細なロジックが実装されています。
- `definition.go`: 定義へのジャンプ機能 (`textDocument/definition`) が実装されています。
- `hover.go`: ホバー情報表示機能 (`textDocument/hover`) が実装されています。
- `host.go`: `Host` インターフェースを定義します。これは TypeScript の `LanguageServiceHost` に相当し、`LanguageService` がコンパイラプログラムやファイルシステムにアクセスするための抽象化レイヤーを提供します。
- `internal/lsp/lsproto/`: Language Server Protocol (LSP) の Go 言語バインディング(型定義)が含まれます。LSP メッセージの送受信に使用されます。
- `internal/core/`: 共通のユーティリティ関数、データ構造、コンパイラオプションなどが定義されています。
- `internal/bundled/`: TypeScript の標準ライブラリ定義ファイル(例: `lib.es2023.d.ts`)など、バンドルされたリソースが含まれます。
- `internal/vfs/osvfs/`: 仮想ファイルシステム (VFS) の OS 依存実装です。ファイルシステムへのアクセスを抽象化します。
- **`_packages/`**:
- TypeScript の元の実装から変換された型定義や API 定義などが含まれる可能性があります。
- **`testdata/`**:
- 各種テストに使用されるデータファイルが含まれます。
## 2. LSP (Language Server Protocol) の実装詳細
LSP の実装は、主に `cmd/tsgo/lsp.go` と `internal/ls/` パッケージに分散しています。
### エントリポイントとサーバーの起動
LSP サーバーは、`cmd/tsgo/lsp.go` の `runLSP` 関数によって起動されます。
```go
func runLSP(args []string) int {
flag := flag.NewFlagSet("lsp", flag.ContinueOnError)
stdio := flag.Bool("stdio", false, "use stdio for communication")
pprofDir := flag.String("pprofDir", "", "Generate pprof CPU/memory profiles to the given directory.")
pipe := flag.String("pipe", "", "use named pipe for communication")
_ = pipe
socket := flag.String("socket", "", "use socket for communication")
_ = socket
if err := flag.Parse(args); err != nil {
return 2
}
if *pprofDir != "" {
fmt.Fprintf(os.Stderr, "pprof profiles will be written to: %v\n", *pprofDir)
profileSession := pprof.BeginProfiling(*pprofDir, os.Stderr)
defer profileSession.Stop()
}
fs := bundled.WrapFS(osvfs.FS())
defaultLibraryPath := bundled.LibPath()
s := lsp.NewServer(&lsp.ServerOptions{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
Cwd: core.Must(os.Getwd()),
FS: fs,
DefaultLibraryPath: defaultLibraryPath,
})
if err := s.Run(); err != nil && !errors.Is(err, io.EOF) {
return 1
}
return 0
}
```
ここで `lsp.NewServer` が呼び出され、`internal/lsp` パッケージで定義されている `Server` 構造体が初期化されます。この `Server` は、LSP メッセージの送受信を管理し、受信したリクエストを `internal/ls.LanguageService` にディスパッチします。
### Language Service の役割
`internal/ls/languageservice.go` で定義される `LanguageService` 構造体は、LSP の各機能(補完、定義、ホバーなど)の具体的なロジックをカプセル化しています。
```go
type LanguageService struct {
ctx context.Context
host Host
converters *Converters
}
func NewLanguageService(ctx context.Context, host Host) *LanguageService {
return &LanguageService{
host: host,
converters: NewConverters(host.GetPositionEncoding(), host.GetLineMap),
}
}
```
`LanguageService` は `Host` インターフェースを介して、コンパイラプログラム (`compiler.Program`) やファイルシステムにアクセスします。
### 主要な LSP 機能
- **補完 (`textDocument/completion`)**:
- `internal/ls/completions.go` の `ProvideCompletion` メソッドが担当します。
- コードの現在の位置に基づいて、利用可能なシンボル、キーワード、リテラルなどの補完候補を生成します。
- `internal/ast` (AST)、`internal/checker` (型チェッカー)、`internal/compiler` (コンパイラ) などの情報を用いて、文脈に応じた適切な補完を提供します。
- **定義へのジャンプ (`textDocument/definition`)**:
- `internal/ls/definition.go` の `ProvideDefinition` メソッドが担当します。
- 指定されたコード位置にあるシンボルを特定し、そのシンボルが宣言されているソースコード上の位置を返します。
- エイリアスの解決も行い、最終的な宣言箇所を指し示します。
- **ホバー情報 (`textDocument/hover`)**:
- `internal/ls/hover.go` の `ProvideHover` メソッドが担当します。
- 指定されたコード位置にある要素(変数、関数、型など)に関するクイック情報(型定義、ドキュメントなど)を取得し、Markdown 形式で表示します。
- `checker.GetQuickInfoAtLocation` を利用して、型チェッカーから情報を取得します。
### Language Service Host
`internal/ls/host.go` で定義される `Host` インターフェースは、`LanguageService` が外部環境とやり取りするための抽象化レイヤーです。
```go
type Host interface {
GetProgram() *compiler.Program
GetPositionEncoding() lsproto.PositionEncodingKind
GetLineMap(fileName string) *LineMap
}
```
これは TypeScript の `LanguageServiceHost` と同様の役割を果たし、LSP サーバーがファイルシステムの状態を読み取ったり、コンパイラプログラムを取得したりするためのメカニズムを提供します。
## 3. IPC (Inter-Process Communication) による API の実装箇所
現在のところ、IPC は標準入出力 (stdio) を介して行われています。
- **`cmd/tsgo/lsp.go`**:
- `runLSP` 関数内で `os.Stdin` と `os.Stdout` が LSP サーバーの入力および出力ストリームとして設定されています。
- `flag` パッケージで `pipe` (`-pipe`) と `socket` (`-socket`) のオプションが定義されていますが、これらは現在の実装では使用されておらず、コメントアウトされています。これは、将来的に名前付きパイプやソケットを介した通信がサポートされる可能性を示唆していますが、現状では未実装です。
```go
func runLSP(args []string) int {
flag := flag.NewFlagSet("lsp", flag.ContinueOnError)
stdio := flag.Bool("stdio", false, "use stdio for communication")
pprofDir := flag.String("pprofDir", "", "Generate pprof CPU/memory profiles to the given directory.")
pipe := flag.String("pipe", "", "use named pipe for communication")
_ = pipe
socket := flag.String("socket", "", "use socket for communication")
_ = socket
if err := flag.Parse(args); err != nil {
return 2
}
if !*stdio {
fmt.Fprintln(os.Stderr, "only stdio is supported")
return 1
}
if *pprofDir != "" {
fmt.Fprintf(os.Stderr, "pprof profiles will be written to: %v\n", *pprofDir)
profileSession := pprof.BeginProfiling(*pprofDir, os.Stderr)
defer profileSession.Stop()
}
fs := bundled.WrapFS(osvfs.FS())
defaultLibraryPath := bundled.LibPath()
s := lsp.NewServer(&lsp.ServerOptions{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
Cwd: core.Must(os.Getwd()),
FS: fs,
DefaultLibraryPath: defaultLibraryPath,
})
if err := s.Run(); err != nil && !errors.Is(err, io.EOF) {
return 1
}
return 0
}
```
LSP プロトコル自体が IPC の抽象化を提供しているため、具体的な API は LSP メッセージの形式に従います。つまり、LSP クライアントとサーバーは JSON-RPC 2.0 を介してメッセージを交換し、そのメッセージの内容が API 呼び出しに相当します。
## 4. TypeScript Language Service に似た API の実装
TypeScript の Language Service が提供するようなセマンティックな情報取得 API は、`internal/ls/api.go` に実装されています。
```go
func (l *LanguageService) GetSymbolAtPosition(ctx context.Context, fileName string, position int) (*ast.Symbol, error) {
program, file := l.tryGetProgramAndFile(fileName)
if file == nil {
return nil, fmt.Errorf("%w: %s", ErrNoSourceFile, fileName)
}
node := astnav.GetTokenAtPosition(file, position)
if node == nil {
return nil, fmt.Errorf("%w: %s:%d", ErrNoTokenAtPosition, fileName, position)
}
checker, done := program.GetTypeCheckerForFile(ctx, file)
defer done()
return checker.GetSymbolAtLocation(node), nil
}
func (l *LanguageService) GetTypeOfSymbol(ctx context.Context, symbol *ast.Symbol) *checker.Type {
program := l.GetProgram()
checker, done := program.GetTypeChecker(ctx)
defer done()
return checker.GetTypeOfSymbolAtLocation(symbol, nil)
}
```
これらのメソッドは、TypeScript の Language Service が提供する `getSymbolAtLocation` や `getTypeOfSymbolAtLocation` といった機能に直接対応しています。Go の型システムと AST、型チェッカーの連携を通じて、これらの情報が提供されます。
## 5. 主要コンポーネントの連携図
```mermaid
graph TD
A[LSP Client (e.g., VS Code)] -- JSON-RPC over Stdio --> B[cmd/tsgo/lsp.go]
B -- サーバー初期化 --> C[internal/lsp.Server]
C -- リクエストディスパッチ --> D[internal/ls.LanguageService]
D -- Hostインターフェース経由 --> E[internal/compiler.Program]
E -- 利用 --> F[internal/ast (AST)]
E -- 利用 --> G[internal/checker (型チェッカー)]
D -- 提供 --> H[LSP機能: 補完, 定義, ホバー]
H -- 内部API利用 --> I[internal/ls/api.go (セマンティックAPI)]
```
この図は、LSP クライアントからリクエストがどのように LSP サーバーに到達し、`LanguageService` が内部のコンパイラ、AST、型チェッカーと連携して LSP 機能を提供し、最終的にクライアントに応答を返すまでの主要なコンポーネント間の流れを示しています。
手元でビルド
$ git clone https://github.com/microsoft/typescript-go
$ cd typescript-go
$ git submodule update --init --recursive
$ npm insatll
$ hereby install-tools
$ hereby build
$ hereby test
最終的に cmd/tsgo/main.go
が .built/local/tsgo
にビルドされる。
ソースを読む感じ、 tsgo --lsp -stdid
で LSP が立ち上がる。
また tsgo --api
で内部サーバー用のIPCサーバーが立ち上がるっぽいが、どこにもまだドキュメントがない。
LSP Client から叩く
Cline にクライアントを書かせる。
LSP Client として、これを呼び出すプログラムを書いて
中身はLSP仕様に従うので略。
結果の一部
7. ファイル変更をテスト中...
送信メッセージ: {
"jsonrpc": "2.0",
"method": "textDocument/didChange",
"params": {
"textDocument": {
"uri": "file:///home/mizchi/repo/microsoft/typescript-go/.mizchi/test-lsp-client/test-files/sample.ts",
"version": 2
},
"contentChanges": [
{
"text": "\n\n// 新しく追加されたコード\nconst newVariable = \"test\";"
}
]
}
}
ソースから逆算したが、 npx tsgo --lsp -stdio
も同じ結果だったので、コード解析ツールを作りたい人は、このプロセスを握ると良さそう。
おわり
一旦ここで満足
Views: 0