LLMにドキュメントを書かせる

# はじめに
[Handy Tools](https://marketplace.visualstudio.com/items?itemName=taqu29.HandyTools64)の開発記のようなものです.
私がほしいものを入れていく拡張になります. ローカルＰＣで動かせる程度のLLMでできることを拡張に入れていきたいと思っています.

# 関数宣言のドキュメント自動生成
LLMを使ってできることで, 私がほしいものは何かなと考えて作った機能です.
関数宣言に対して自動でDoxygenのコメントを生成させることができると便利だと思います.
コード生成とかには価値が見いだせないです. 既存のコードから学習したものには価値がないですから.

## Doxygenコメント生成
Doxygenのコメントを生成させることができるのかと実験しました.
コードは[Wikipedia クイックソート](https://ja.wikipedia.org/wiki/%E3%82%AF%E3%82%A4%E3%83%83%E3%82%AF%E3%82%BD%E3%83%BC%E3%83%88)より引用しました.

<details><summary>プロンプト</summary>

```c
Create a doxygen comment for the following C++ Function. doxygen comment only

void* 
partition(
    void* a,
    int (*cmp)(void const*, void const*),
    size_t sz,
    size_t n
) {
    // void* に対して直接ポインタ演算はできないので予め char* へ変換する
    char* const base = a;
    if (n <= 1) return base + sz;
    char* lo = base;
    char* hi = &base[sz * (n - 1)];
    char* m  = lo + sz * ((hi - lo) / sz / 2);
    // m が median-of-3 を指すようソート
    if (cmp(lo, m) > 0) {
        swap(lo, m, sz);
    }
    if (cmp(m, hi) > 0) {
        swap(m, hi, sz);
        if (cmp(lo, m) > 0) {
            swap(lo, m, sz);
        }
    }
    while (1) {
        while (cmp(lo, m) < 0) lo += sz; // ピボット以上の要素を下から探す
        while (cmp(m, hi) < 0) hi -= sz; // ピボット以下の要素を上から探す
        if (lo >= hi) return hi + sz;
        swap(lo, hi, sz);
        // ピボットがスワップされた場合、スワップ先を指すよう m を更新する
        if (lo == m) {
            m = hi;
        } else if (hi == m) {
            m = lo;
        }
        lo += sz;
        hi -= sz;
    }
}
```
</details>

Gemma 7Bでも説明の正しさはともかく, 初版としては使えそうです.
<details><summary>回答</summary>

```c
/**
 * Partition function to find the median-of-3 in a sorted array.
 *
 * @param a Pointer to the array.
 * @param cmp Comparison function to compare two elements.
 * @param sz Size of each element in the array.
 * @param n Number of elements in the array.
 *
 * @return Pointer to the median-of-3 element.
 */
void* partition(void* a, int (*cmp)(void const*, void const*), size_t sz, size_t n) {
    // void* に対して直接ポインタ演算はできないので予め char* へ変換する
    char* const base = a;

    // If the number of elements is less than or equal to 1, return the base pointer plus the size of each element.
    if (n <= 1) {
        return base + sz;
    }

    char* lo = base;
    char* hi = &base[sz * (n - 1)];
    char* m  = lo + sz * ((hi - lo) / sz / 2);

    // m が median-of-3 を指すようソート
    if (cmp(lo, m) > 0) {
        swap(lo, m, sz);
    }
    if (cmp(m, hi) > 0) {
        swap(m, hi, sz);
        if (cmp(lo, m) > 0) {
            swap(lo, m, sz);
        }
    }

    while (1) {
        while (cmp(lo, m) < 0) lo += sz; // ピボット以上の要素を下から探す
        while (cmp(m, hi) < 0) hi -= sz; // ピボット以下の要素を上から探す
        if (lo >= hi) return hi + sz;
        swap(lo, hi, sz);

        // ピボットがスワップされた場合、スワップ先を指すよう m を更新する
        if (lo == m) {
            m = hi;
        } else if (hi == m) {
            m = lo;
        }

        lo += sz;
        hi -= sz;
    }
}
```
</details>

その他実験の記録 [LLMはDoxygenのコメントを生成できるのか](https://qiita.com/taqu/items/c64da5c7d9c184fc38b5).

## 宣言から定義を検索する

FileCodeModelでできるらしい, カーソル位置の範囲にある宣言から, 定義のコードを取得しています.

<details><summary>定義探索</summary>

```csharp
FileCodeModel fileCodeModel = projectItem.FileCodeModel;
CodeElement codeElement = FindCodeElement(fileCodeModel.CodeElements, selection);
```

```csharp
public static CodeElement FindCodeElement(CodeElements elements, SnapshotSpan selection)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    //EnvDTE.TextPoint has one base offset, so offset selection points.
    int selectionStart = selection.Start.Position + 1;
    int selectionEnd = selection.End.Position + 1;
    foreach (CodeElement codeElement in elements)
    {
        if (0 <= Array.IndexOf(IgnoredElements, codeElement.Kind))
        {
            continue;
        }
        if (codeElement.Kind != vsCMElement.vsCMElementFunction || !(codeElement is VCCodeFunction))
        {
            CodeElement recurse = FindCodeElementRecursive(codeElement.Children, selectionStart, selectionEnd);
            if (null != recurse)
            {
                return recurse;
            }
            continue;
        }
        VCCodeFunction codeFunction = codeElement as VCCodeFunction;
        TextPoint startPoint = codeFunction.get_StartPointOf(vsCMPart.vsCMPartWholeWithAttributes, vsCMWhere.vsCMWhereDeclaration);
        TextPoint endPoint = codeFunction.get_EndPointOf(vsCMPart.vsCMPartWholeWithAttributes, vsCMWhere.vsCMWhereDeclaration);
        if (endPoint.AbsoluteCharOffset < selectionStart)
        {
            continue;
        }
        if (selectionEnd < startPoint.AbsoluteCharOffset)
        {
            continue;
        }
        return codeElement;
    }
    return null;
}

public static CodeElement FindCodeElementRecursive(CodeElements elements, int selectionStart, int selectionEnd)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    if (null == elements)
    {
        return null;
    }
    foreach (CodeElement codeElement in elements)
    {
        if (0 <= Array.IndexOf(IgnoredElements, codeElement.Kind))
        {
            continue;
        }
        if (codeElement.Kind != vsCMElement.vsCMElementFunction || !(codeElement is VCCodeFunction))
        {
            CodeElement recurse = FindCodeElementRecursive(codeElement.Children, selectionStart, selectionEnd);
            if (null != recurse)
            {
                return recurse;
            }
            continue;
        }
        VCCodeFunction codeFunction = codeElement as VCCodeFunction;
        TextPoint startPoint = codeFunction.get_StartPointOf(vsCMPart.vsCMPartWholeWithAttributes, vsCMWhere.vsCMWhereDeclaration);
        TextPoint endPoint = codeFunction.get_EndPointOf(vsCMPart.vsCMPartWholeWithAttributes, vsCMWhere.vsCMWhereDeclaration);
        if (endPoint.AbsoluteCharOffset < selectionStart)
        {
            continue;
        }
        if (selectionEnd < startPoint.AbsoluteCharOffset)
        {
            continue;
        }
        return codeElement;
    }
    return null;
}
```
</details>

## LLMの生成
プロンプトのテンプレートに関数定義のコードを入れて, LLMに投げれば完成です.
`/** */`で囲まれた文章がない場合などはエラーとして処理します.

![HandyTools00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27065/85432046-c957-1f09-2eb7-1a676f6ed59a.gif)

# まとめ
ローカルで動作できるLLMだと, 生成されたドキュメントをそのまま使うことができる品質ではないですが, ドキュメントしようとするモチベーションは上がった気がします.