はじめに
こんにちは!24th Dev Advent Calendar 2025の22日目を担当するnakadate00です。
本記事では、Cursorのエディタ上で対話型AIを利用し、日本語プログラミング言語である「なでしこ」について学んだことをお話しします。
AIを活用することで、技術のキャッチアップ速度も向上させられるのではないかという検証も兼ねていますので、「短時間で言語仕様を理解するための学び方」という観点でも参考になれば幸いです。
技術選定の理由
今回なでしこを学習対象として選定した理由は、一年前の自分が残した課題だったからです。
昨年の記事では、命名についての文脈で日本語プログラミング言語に初めて触れました。
簡単なアプリケーションを実装したものの、言語の深い仕様については学べておらず、機会があれば学ぶというのが次のアクションでした。
今年もまとまった学習の時間が取れず、断念しようとしていたのですが、「AIを活用すれば短時間でもプログラミング言語の深い仕様について学ぶことが可能なのではないか?」と考えました。
そこで、普段の業務でも活用しているCursorの利用を検討しました。
エージェント型のAIではなく、対話型のAIであれば、自分の学びたい箇所についてコードを読みながらピンポイントで学ぶことができると考えたからです。
学習の流れ
今回は事前に以下の学習の流れを計画しました。
1. 基本となるコードをAIで生成する
2. 生成されたコードの中から気になる箇所を一つずつピックアップする
3. ピックアップした箇所に対してAIに「このコードでこの結果になる理由を、言語仕様の観点で説明して」と質問する
4. 気になる箇所が無くなるまで、2~3を繰り返す
実践
基本となるコードの生成
まずは、以下の命令で基本となるFizzBuzzのコードを生成します。
ここではCursorのエディタでOpus 4.5モデルを選択し、Agent Modeで命令しました。
fizzbuzz.nako3になでしこでFizzBuzzを実装してください
生成されたコードを実行し、結果が正しいことも確認しました。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
この時点での率直な感想としては、「繰り返す」や「もし」が他のプログラミング言語のforやifに対応していることは分かりますが、どうしてこれで正しく処理できているのかは全く分かりません。
次に、生成されたコードの中から気になる箇所を1つずつピックアップして、深掘りします。
「繰り返す」について
まず「繰り返す」について深掘りしてみます。
CursorのエディタでOpus 4.5モデルを選択したまま、Ask Modeに切り替え、以下を命令しました。
fizzbuzz.nako3:1 このコードでこの結果になる理由を、言語仕様の観点で説明して
出力結果は以下の通りです。
構文解析(パーサー)の処理
nako_parser3.mts の yFor() メソッドで、この構文は以下のようにパースされます:
const vTo = this.popStack(['まで', 'を']); // 範囲オブジェクトの場合もあり
const vFrom = this.popStack(['から']) || this.yNop();
const vWord = this.popStack(['を', 'で']);
let wordStr = '';
if (vWord !== null) { // 変数
if (vWord.type !== 'word') {
throw NakoSyntaxError.fromNode('『(変数名)をAからBまで繰り返す』で指定してください。', vWord);
}
wordStr = vWord.value;
}
スタックから助詞「を」「から」「まで」を手がかりに、逆順に値を取り出します:
vTo ← 「100まで」の100
vFrom ← 「1から」の1
vWord ← 「Nを」のN(ループ変数名)
コード生成(ジェネレーター)の処理
nako_gen.mts の convFor() で、以下のJavaScriptコードが生成されます:
let loopVarSetter = ''
if (node.word !== '') { // ループ変数を使う時
const varName = node.word
this.varsSet.names.add(varName)
loopVarSetter = this.varname_set(varName, varI)
}
そして生成されるコードは:
`if (${varFrom} <= ${varTo}) { // up\n` +
` if (${flagUp}) {` +
` for (let ${varI} = ${varFrom}; ${varI} <= ${varTo}; ${varI}+= ${inc}) {\n` +
` ${sorePrefex};${loopVarSetter}\n` +
' // for block begin\n' +
` ${block}\n` +
' // for block end\n'
つまり、何が起きているか
「Nを1から100まで繰り返す」は、概念的に以下のJavaScriptに変換されます:
for (let $nako_i0 = 1; $nako_i0 <= 100; $nako_i0++) {
それ = $nako_i0; // 暗黙の変数「それ」にも代入
N = $nako_i0; // ループ変数Nに現在の値を代入
// ... ループ本体 ...
}
重要なポイント
1.「を」で指定した変数(N)は自動的にループカウンタになる
パーサーが助詞「を」を見て変数名を抽出し、ジェネレーターがその変数にループカウンタ値を毎回代入するコードを生成します。
2.「それ」にも自動代入される
sorePrefex = this.varname_set('それ', varI) により、システム変数「それ」にも現在のカウンタ値が入ります。
3.「から」「まで」で範囲指定
助詞が構文の区切りとして機能し、開始値と終了値を特定します。
これがなでしこ3の助詞ベースのパースの典型例です。日本語の助詞(を、から、まで)が文法構造を定義し、変数への自動代入が行われることで、自然な日本語のような記述でループ処理が実現されています。
簡単にまとめると、以下のようになります。
- 構文解析とコード生成の処理がある
- 「繰り返す」という文字列があると、構文解析時にyFor()メソッドが使われる
- スタックという一文単位のコードに含まれる助詞「を」「から」「まで」を元に値を取り出している
- 「繰り返す」という文字列があると、コード生成時にconvFor()が使われる
- nako_gen.mtsでJavaScriptコードに変換している
- 暗黙の変数として存在している「それ」にも現在のカウンタ値が自動代入される
ここで試したいことが出てきたので、以下のように助詞を変えてコードを実行してみました。
Nの1から100まで繰り返す // 「Nを」から「Nの」に変更
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
すると、const vWord = this.popStack(['を', 'で']);で定義されていない「の」を使用したため、文法エラーになりました。
[文法エラー]fizzbuzz.nako3(3行目): 未解決の単語があります: [単語『fizzbuzz__N』の]
次の命令の可能性があります:
今度は以下のように、const vWord = this.popStack(['を', 'で']);で定義されている「を」を「で」に変更してみました。
Nで1から100まで繰り返す // 「Nを」から「Nで」に変更
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
今回は正しくFizzBuzzの結果が出力されました。
AIの出力結果にあった構文解析(パーサー)の処理のコードを読み取り、出力を変えずにコードの書き換えを行うことができました。
他にも以下のように、「それ」を利用してコードが実行できるか試してみました。
Nを1から100まで繰り返す
もし、(それ%15)=0ならば // 「N」を「それ」に変更
「FizzBuzz」を表示
違えば、もし、(それ%3)=0ならば // 「N」を「それ」に変更
「Fizz」を表示
違えば、もし、(それ%5)=0ならば // 「N」を「それ」に変更
「Buzz」を表示
違えば
それを表示 // 「N」を「それ」に変更
ここまで
ここまで
今回も正しくFizzBuzzの結果が出力されました。
これが、暗黙の変数として存在している「それ」にも現在のカウンタ値が自動代入されるということなのだと理解することができました。
また、1行目の「N」も自分で定義した値であるため、「あれ」に変更しても正しく動作しました。
あれを1から100まで繰り返す // 「N」を「あれ」に変更
もし、(それ%15)=0ならば // 「N」を「それ」に変更
「FizzBuzz」を表示
違えば、もし、(それ%3)=0ならば // 「N」を「それ」に変更
「Fizz」を表示
違えば、もし、(それ%5)=0ならば // 「N」を「それ」に変更
「Buzz」を表示
違えば
それを表示 // 「N」を「それ」に変更
ここまで
ここまで
「もし〜ならば」について
次に「もし〜ならば」について深掘りしてみます。
今回もCursorのエディタをAsk Modeのまま、以下を命令しました。
fizzbuzz.nako3:2 このコードでこの結果になる理由を、言語仕様の観点で説明して
出力結果は以下になります。
言語仕様からの解説
1. 演算子の定義
なでしこでは、以下の演算子が使用されています:
% 演算子(剰余演算子)
{ name: '÷', pattern: /^(÷|\/)/ }, // 普通の割り算
{ name: '%', pattern: /^%/ },
= 演算子(比較演算子)
{ name: 'eq', pattern: /^(==|🟰🟰)/ },
{ name: 'eq', pattern: /^(=|🟰)/ },
= は eq(等値比較)トークンとして認識されます。
2. 演算子の優先順位
export const opPriority: {[key: string]: number} = {
// and or
'and': 1,
'or': 1,
// compare
'eq': 2,
// ... 省略 ...
'%': 5,
// ^
'^': 6,
'**': 6
}
- %(剰余)の優先順位: 5
- eq(=)の優先順位: 2
優先順位が高いほど先に評価されるため、N%15 が先に計算され、その結果が 0 と比較されます。
3. JavaScriptへの変換
const OP_TBL: {[key: string]: string} = {
'&': '+""+',
eq: '==',
noteq: '!=',
// ...
なでしこの = は JavaScript の ==(等価演算子)に変換されます。
4. 「もし〜ならば」構文
/** 「もし」文の条件を取得 */
yIFCond (): Ast {
const map = this.peekSourceMap()
let a: Ast | null = this.yGetArg()
if (!a) {
throw NakoSyntaxError.fromNode(
'「もし」文の条件式に間違いがあります。' + this.nodeToStr(this.peek(), { depth: 1 }, false), map)
}
// チェック : Aならば
if (a.josi === 'ならば') { return a }
ならば は助詞として認識され、条件式の終端を示します。
まとめ:コードの評価の流れ
もし、(N%15)=0ならば
1.字句解析: もし → ( → N → % → 15 → ) → = → 0 → ならば
2.演算子優先順位: %(優先順位5) > =(優先順位2) なので、先に N%15 が評価される
3.JavaScriptへの変換: ((N % 15) == 0) という式に変換
4.実行: N を 15 で割った余りが 0 なら真(true)
なお、(N%15) のカッコは演算子優先順位的には冗長ですが、可読性のために付けられています(カッコがなくても同じ結果になります)。
簡単にまとめると、以下のようになります。
- nako_lex_rules.mtsで、演算子が定義されている
- =も==もeqとして認識され、JavaScriptへの変換時に、eqは==に変換される
- nako_parser_const.mtsで、演算子の優先順位が定義されており、優先順位が高いほど先に評価される
- 「もし」という文字列があると、構文解析時にyIFCond()メソッドが使われる
- nako_gen.mtsでJavaScriptコードに変換している
今回も試してみたいことが出てきたので、以下のようにコードを変更して実行してみました。
Nを1から100まで繰り返す
もし、(N%15)==0ならば // 「=」を「==」に変更
「FizzBuzz」を表示
違えば、もし、(N%3)==0ならば // 「=」を「==」に変更
「Fizz」を表示
違えば、もし、(N%5)==0ならば // 「=」を「==」に変更
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
nako_lex_rules.mtsで、{ name: 'eq', pattern: /^(==|🟰🟰)/ }と定義されていたので、問題なく実行できると考えましたが、文法エラーになってしまいました。
[文法エラー]fizzbuzz.nako3(2行目): 『もし』文の条件で次のエラーがあります。
[文法エラー]fizzbuzz.nako3(2行目): 計算式で演算子『=』後に値がありません
[バージョン] 3.3.13
原因を突き止めるために、AIに以下の質問をしてみました。
nako_lex_rules.mtsで、`{ name: 'eq', pattern: /^(==|🟰🟰)/ }`と定義されているのに、 (N%15)==0を実行すると、「計算式で演算子『=』後に値がありません」という文法エラーになるのはなぜですか?
以下のような回答が返ってきました。
まとめ
現在のなでしこ3では、(N%15)==0 は正常に動作します。
考えられるエラーの原因
もしエラーが発生しているなら、以下の可能性があります:
1.別のなでしこ環境(ブラウザ版、なでしこパッド、Webエディタなど)で実行している
2.古いバージョンのなでしこ3を使用している(== のルールが追加される前)
ファイルを実行していたエディタの拡張機能で使用されているなでしこ3のバージョンが3.3.13だったため、エラーになっていました。
記事執筆時点での最新バージョンである3.7.11であれば、「==」に変更しても正しくコードを実行することができます。
次に、以下のように演算子の順番を入れ替えてコードを実行してみました。
Nを1から100まで繰り返す
もし、0=N%15ならば // 「(N%15)=0」を「0=N%15」に変更
「FizzBuzz」を表示
違えば、もし、0=N%3ならば // 「(N%3)=0」を「0=N%3」に変更
「Fizz」を表示
違えば、もし、0=N%5ならば // 「(N%5)=0」を「0=N%5」に変更
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
こちらも問題なく実行できました。
nako_parser_const.mtsで、「% > =」という優先順位の定義通りに、0=Nではなく、正しくN%15が評価されることを確認することができました。
最後に、「繰り返す」と同じように、「もし〜ならば」も他の助詞であっても同じ結果が出力されるかを確認してみます。
yIFCond()メソッドでは、「ならば」や「でなければ」の条件分岐は存在しましたが、他には確認できなかったため、AIに質問してみました。
fizzbuzz.nako3:2 「もし、〜ならば」とありますが、他の助詞を使って同じ出力結果にすることはできますか?
出力は以下になります。
見つかりました!なでしこ3では「もし」文で使える助詞が nako_josi_list.mts で定義されています。
// 「もし」文で使う助詞
export const tararebaJosiList: string[] = [
'でなければ', 'なければ', 'ならば', 'なら', 'たら', 'れば'
]
書き換え例
3行目は以下のように書き換えても同じ結果になります:
もし、(N%15)=0なら
もし、(N%15)=0であれば
また、「AがBならば」の形式も使えます(パーサーの400-432行目で処理されています):
もし、(N%15)が0ならば
もし、(N%15)が0なら
もし、(N%15)が0であれば
注意点
でなければ と なければ は条件を反転させる意味を持つので、同じ結果を得るには条件式を逆にする必要があります:
もし、(N%15)!=0でなければ # N%15が0でないことでなければ → N%15=0と同じ意味
この結果を元に、以下のようにコードを変更して実行してみました。
Nを1から100まで繰り返す
もし、(N%15)が0であれば // 「=」を「が」に、「ならば」を「であれば」に変更
「FizzBuzz」を表示
違えば、もし、(N%3)が0であれば // 「=」を「が」に、「ならば」を「であれば」に変更
「Fizz」を表示
違えば、もし、(N%5)が0であれば // 「=」を「が」に、「ならば」を「であれば」に変更
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
しかし、文法エラーが発生しました。
[エラー][文法エラー]fizzbuzz.nako3(2行目): 『もし』文の条件で次のエラーがあります。
[文法エラー]fizzbuzz.nako3(2行目): 不完全な文です。演算子『%』、数値0、単語『fizzbuzz__あ』が解決していません。演算子『%』は『単語『fizzbuzz__N』と数値15に演算子『%』を適用した式』として使われています。
これはtararebaJosiListで定義されている助詞リストに「であれば」がないのに、使おうとしたことが原因だと予想しました。
つまり、AIが出力した「『もし、(N%15)=0であれば』で書き換えても同じ結果になります」が間違っていたことになります。
今度は、tararebaJosiListで定義されている「れば」に変更してみます。
Nを1から100まで繰り返す
もし、(N%15)が0れば // 「=」を「が」に、「ならば」を「れば」に変更
「FizzBuzz」を表示
違えば、もし、(N%3)が0れば // 「=」を「が」に、「ならば」を「れば」に変更
「Fizz」を表示
違えば、もし、(N%5)が0れば // 「=」を「が」に、「ならば」を「れば」に変更
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
すると、コードが正しく実行されました。
AIが出力した結果を全て鵜呑みにせず、自分の環境のバージョンも確認した上で、ソースコードを読み取る必要がありました。
「表示」について
最後に「表示」について深掘りしてみます。
今回もCursorのエディタをAsk Modeのまま、以下を命令しました。
fizzbuzz.nako3:3 このコードでこの結果になる理由を、言語仕様の観点で説明して
出力結果は以下になります。
1. 文字列リテラルのトークナイズ
nako_lex_rules.mts で定義されているように、「」 で囲まれた文字列は string_ex (展開可能文字列)としてパースされます:
{ name: 'string_ex', pattern: /^「/, cbParser: src => cbString('「', '」', src) },
{ name: 'string', pattern: /^『/, cbParser: src => cbString('『', '』', src) },
- 「」 → string_ex: 変数展開が可能(例: 「Nは{N}です」)
- 『』 → string: 変数展開しない生文字列
2. 助詞「を」による引数指定
なでしこ3は助詞で引数と関数の関係を定義します。「FizzBuzz」**を**表示 の「を」が、引数と関数を結びつけています。
3. 「表示」命令の定義
plugin_system.mts で定義されています:
'表示': { // @Sを表示 // @ひょうじ
type: 'func',
josi: [['を', 'と']],
pure: true,
fn: function (s: string, sys: any) {
// 継続表示の一時プールを出力
s = String(sys.__printPool) + s
sys.__printPool = ''
//
sys.__setSysVar('表示ログ', String(sys.__getSysVar('表示ログ')) + s + '\n')
sys.logger.send('stdout', s + '')
},
return_none: true
},
- josi: [['を', 'と']]: 引数が「を」または「と」で修飾されることを期待
- return_none: true: 戻り値がない(副作用のみの関数)
- 内部では sys.logger.send('stdout', s + '') で標準出力へ送信
実行フロー
1.レキサーが 「FizzBuzz」 を string_ex トークンとして認識
2.助詞「を」により、この文字列が後続の関数への引数と認識
3.パーサーが「表示」関数呼び出しとして構文木を構築
4.コードジェネレータがJavaScriptコードに変換
5.実行時に plugin_system の 表示 関数が呼ばれ、標準出力に "FizzBuzz" が出力される
簡単にまとめると、以下のようになります。
- nako_lex_rules.mtsで、「」と『』で囲まれた文字のルールを定義している
- 助詞の「を」によって引数と関数を結びつける
- 「表示」という文字列があると、実行時にplugin_system.mtsの表示関数が呼ばれる
今回はまず、「」と『』に違いがあるのかを確認してみます。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
『FizzBuzz』を表示 // 「」を『』に変更
違えば、もし、(N%3)=0ならば
『Fizz』を表示 // 「」を『』に変更
違えば、もし、(N%5)=0ならば
『Buzz』を表示 // 「」を『』に変更
違えば
Nを表示
ここまで
ここまで
出力結果は同じでした。
これは「FizzBuzz」と『FizzBuzz』のどちらもFizzBuzzという文字列を表示しているだけだと考えました。
次は以下のように、Nの表示箇所を変えてみました。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
「N」を表示 // 「N」に変更
ここまで
ここまで
出力結果は以下のようになりました。
N
N
Fizz
N
Buzz
Fizz
N
N
Fizz
Buzz
N
Fizz
N
N
FizzBuzz
…
「」で囲むと変数展開されるのではないかと考えていましたが、Nという文字列が出力されてしまいました。
今回もAIに質問してみました。
「」で囲むと変数展開されると考えていたが、「N」を表示でも出力結果にNと表示されてしまうのはなぜですか
以下のような回答が返ってきました。
結論:なでしこ3の変数展開には {変数名} という波括弧が必要です
正しい書き方
変数 N の値を表示したい場合は:
「{N}」を表示
変数展開するために波括弧が必要であることが分かったので、以下のように書き換えて再度実行しました。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
「{N}」を表示 // Nを「{N}」に変更
ここまで
ここまで
今回は正しくFizzBuzzの結果が出力されました。
そして以下のように『』で囲むと変数展開されずに、{N}という文字列が出力されました。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
『{N}』を表示 // Nを『{N}』に変更
ここまで
ここまで
また、助詞については「を」以外にも「と」も「表示」を修飾する助詞として定義されていたため、以下でもFizzBuzzの結果が正しく出力されました。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
「FizzBuzz」と表示 // 「を」を「と」に変更
違えば、もし、(N%3)=0ならば
「Fizz」と表示 // 「を」を「と」に変更
違えば、もし、(N%5)=0ならば
「Buzz」と表示 // 「を」を「と」に変更
違えば
Nと表示 // 「を」を「と」に変更
ここまで
ここまで
学習した内容を踏まえてコードを書き換える
ここまで学習した結果を元に、基本となるFizzBuzzのコードと同じ出力結果のまま、以下のように書き換えることができました。
Deep Diveすることで、簡単な処理であってもさまざまな書き方ができるということを学びました。
Nを1から100まで繰り返す
もし、(N%15)=0ならば
「FizzBuzz」を表示
違えば、もし、(N%3)=0ならば
「Fizz」を表示
違えば、もし、(N%5)=0ならば
「Buzz」を表示
違えば
Nを表示
ここまで
ここまで
あれで1から100まで繰り返す
もし、0がそれ%15れば
『FizzBuzz』と表示
違えば、もし、0がそれ%3れば
『Fizz』と表示
違えば、もし、0がそれ%5れば
『Buzz』と表示
違えば
「{それ}」と表示
ここまで
ここまで
学習の流れ(改善版)
実際に学習してみて、事前に計画していた学習の流れをブラッシュアップすることができたので、修正しました。
特に、AIが出力したコードの説明を鵜呑みにせず、自分であれこれと試してみて確証を得ることが大事だと思います。
1. 基本となるコードをAIで生成する
2. 生成されたコードの中から気になる箇所を一つずつピックアップする
3. ピックアップした箇所に対してAIに「このコードでこの結果になる理由を、言語仕様の観点で説明して」と質問する
4. 説明を読み「こういう書き方もできるのではないか」とコードを書き換えて試行する
5. 試行の結果、さらに気になる箇所が出てきたり、エラーになったりしたらAIに質問して解決する
6. 気になる箇所が無くなるまで、2~5を繰り返す
さいごに
AIと対話形式でコードを読み進めることで、なでしこで書かれたコードがどのように実行されているのかの理解を深めることができました。
今年で20周年を迎えた「なでしこ」というプログラミング言語について、少しでも学びを進めることができて良かったです。
また、難しいプロンプトを設計することもなく、AIに「このコードでこの結果になる理由を、言語仕様の観点で説明して」と質問するだけで、コードを読み進めることができたので、学習時間を短縮できたとも言えるのではないでしょうか。
言語のキャッチアップで悩んでいる方への1つの選択肢として、本記事が参考になれば幸いです。
来年は別の日本語プログラミング言語をDeep Diveしてなでしこと比較してみたり、なでしこで書いたコードをGolangで動かせたら面白そうだなと、自分で自分の首を絞めておきます。


