Skip to content

Latest commit

 

History

History
 
 

dncl

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

DNCL モード(日本語プログラミング)

🆕 Smalruby 独自 — upstream に存在しない、Smalruby のために新規追加された機能

概要

Ruby タブで 日本語のプログラミング言語(DNCL モード) を使えるようにする機能。a = 1, if, while ではなく a ← 1, もし...ならば, を繰り返す のように、英語キーワードを一切使わずにプログラムを書ける。

DNCL は大学入学共通テスト手順記述標準言語(DNCLv2)を参考にした日本語ベースの記法で、Smalruby ではこれを内部的に Ruby コードにトランスパイルしてから Scratch ブロックに変換して実行する。

DNCL コード → (DNCLv2 pre-processor) → Smalruby DNCL → Ruby コード → Scratch ブロック → 実行

DNCLv2 互換: 共通テスト 例題サイト(nodai2hitc.github.io/ictl_example)で公開されている DNCLv2 形式のプログラムを そのままコピペして実行 できる。(N) 行番号、| / ⎿ インデントマーカー、もし ... ならば: 末尾コロン、の間繰り返す: / 増やしながら繰り返す: 形式、hidari = 0 , migi = kazu - 1 のような同一行複数代入、and / or、NAME(ARGS) を定義する 形式の関数定義などをサポート。詳細は「DNCLv2 互換」セクション参照。

ブロック → Ruby → DNCL の逆方向変換にも対応しているため、ビジュアルブロックで作った作品を日本語コードで読むことも可能(turn-around 後は Smalruby DNCL 形式に正規化される)。

ユーザーストーリー

  • 小学校高学年として、英語を覚えなくても本格的なプログラミングをしたい
  • 共通テスト対策中の高校生として、DNCL を実際に動くプログラムとして書いて練習したい
  • 教師として、英語キーワードに躓く生徒に「日本語でプログラミングできる」段階的な学習体験を提供したい
  • プログラミング初心者として、if (x > 0) { ... } のような記号より「もし x > 0 ならば」のように母国語で読みたい

UI / 操作フロー

Ruby タブの ruby-toolbar の「日本語(DNCL)」ボタン (ruby-toolbar-mode-dncl) で切替。

モード 表示内容
Ruby if x > 0
日本語 (DNCL) もし x > 0 ならば

DNCL モードに切り替えると:

  • Monaco Editor の言語定義が DNCL 用に切り替わる(シンタックスハイライト、補完)
  • 既存の Ruby コードは自動的に DNCL 表記に変換されて表示される
  • 編集後 Ruby に戻すと、DNCL 表記が Ruby に戻って表示される

DNCL モードとふりがなは排他(DNCL 自体が日本語のためふりがな不要)。

ブロック制限の通知と DNCL モードからの復帰

DNCL モードではコードタブのブロックが制限される(後述の「関連ブロック」参照)ため、想定外に DNCL モードになっているユーザーが「ブロックが少ない・拡張機能が選べない」状態に気づけるよう、2 つの復帰導線を用意している。

通知バナー(コードタブ): DNCL モード中はワークスペース右上に「日本語モード:ブロックが制限されています」バナー(data-testid: dncl-mode-notice)を常時表示する。「Rubyふりがなモードに戻す」ボタン(data-testid: dncl-mode-notice-exit-button)を押すと confirm ダイアログを表示し、OK でふりがなモードに戻してタブ切り替えなしで全ブロックを即時復元する。

拡張機能ボタンの confirm ダイアログ: DNCL モード中に拡張機能ボタン(data-testid: extension-button)を押すと、「日本語モードでは拡張機能は使えません。Rubyふりがなモードに戻すと拡張機能が使えるようになります。戻しますか?」の confirm ダイアログを表示する。OK でふりがなモードに戻して拡張機能ライブラリを開く。キャンセルでは何も変わらない(DNCL モード継続)。

どちらの導線も Redux signal パターンで実装している:

バナー / 拡張機能ボタン (OK)
  → dispatch(setDnclMode(false))             ← blocks.jsx が即座に全ブロック表示に更新
  → dispatch(requestExternalExitDnclMode())  ← ruby-tab の wasDncl 復元をスキップするシグナル

Ruby タブを次に開いたとき、exitDnclModeExternallyRequested フラグが立っていれば DNCL 表示の復元(wasDncl パス)をスキップして Ruby コードを表示し、フラグをクリアする。これにより「バナーで戻したのに Ruby タブを開くと日本語モードに戻ってしまう」事故を防ぐ。

主要ファイル

scratch-gui

DNCL ↔ Ruby トランスパイラ

packages/scratch-gui/src/lib/dncl/:

ファイル 役割
dncl-to-ruby.js DNCL → Ruby 変換のエントリポイント(pre-processor → validator → line converter)
dncl-v2-preprocessor.js DNCLv2 → Smalruby DNCL 正規化(行番号、コロン、| / ⎿、複数代入、and / or、関数定義 opener)
dncl-paste-tidy.js 貼り付け時の整形(日本語モードのエディタに貼り付けた行番号付きのテキストの行番号を消し、// コメントを # に直す。下の「貼り付け時の整形」)と、// のある行の変換エラーに「// はコメントではない」と添える withDoubleSlashHint
dncl-line-converter.js 行単位の DNCL → Ruby 変換
dncl-identifier-converter.js 識別子(変数名)の変換
dncl-builtins.js DNCL 組み込み関数の定義(表示する, 要素数 など)。表示する 多引数は puts(... + ...) に展開
dncl-keywords.js DNCL キーワード定義(もし, ならば, を繰り返す など)
dncl-validator.js DNCL コードの構文検証
dncl-state.js パーサー状態管理
dncl-source-map.js DNCL 行 ↔ Ruby 行のマッピング(エラー位置表示用。詳細は下の「エラー行の対応(ソースマップ)」)
dncl-block-filter.js DNCL モードで使えるブロックのフィルタ
paren-utils.js 括弧解析ユーティリティ
ruby-to-dncl.js Ruby → DNCL 逆変換のエントリポイント
ruby-to-dncl-line-converter.js 行単位の Ruby → DNCL 変換
ruby-to-dncl-identifier.js 識別子の逆変換
ruby-to-dncl-builtins.js 組み込み関数の逆変換。puts(... + ...) の左寄せ + チェーンを 表示する(...) 多引数にフラット化(display fragment 限定)

エラー行の対応(ソースマップ)

DNCL モードでは、DNCL を Ruby に変換してからブロックに変換するため、変換エラーは Ruby の行番号で返ってくる。DNCL と Ruby の行は 1:1 に対応しないので、DnclSourceMap で DNCL の行に戻してからエディタにエラーマーカーを表示する(ruby-tab.jsx の showErrors)。Ruby の桁は DNCL の桁と対応しないため、マーカーは DNCL の行全体に付ける(付け替えは DnclSourceMap#rubyErrorsToDncl)。Ruby の最終行より後ろ(または 1 行目より前)を指すエラーは、Ruby の最終行(1 行目)に対応する DNCL の行に寄せる。Ruby の方が行数が多いのが普通なので、行番号をそのまま使うと無関係な DNCL の行や存在しない行を指してしまうため。

行数が変わる変換と、対応させる DNCL の行:

変換 行数の変化 対応させる DNCL の行
行番号付きの行の継続行を連結(括弧が閉じるまで) 複数行 → 1 行 連結した先頭の行(継続行から Ruby の行を引くと、連結後の行)
a = 0 , b = 1 の複数代入を分割 1 行 → 複数行 元の行
⎿ からブロックの終了行(を実行する など)を追加 +1 行 ⎿ のある行
インデントだけで書いたブロックに終了行を自動で追加 +1 行 閉じるブロックの最後のコード行(空行・コメントは除く)
【外部からの入力】 / 【整数を入力】 / 【文字列を入力】 を ask + 代入に展開 1 行 → 2 行 入力の行
増やしながら / 減らしながら を初期化 + while、終了行を加算 + end に展開 1 行 → 2 行 それぞれ元の行

対応表は変換しながら記録する(行数の比較から推定しない)。dnclV2PreprocessWithLineMap が前処理後の各行の元の行番号(lineMap)を返し、dnclToRuby が行変換で出力した Ruby の各行にそれを引き継いで sourceMap として返す。validateDncl の検証エラーの行も lineMap で DNCL の行に戻す。

Monaco Editor 言語定義

  • packages/scratch-gui/src/containers/ruby-tab/dncl-mode.js — Monaco の DNCL 言語定義(トークナイザ、シンタックスハイライト)
  • packages/scratch-gui/src/containers/ruby-tab/dncl-snippets.js — DNCL コード補完スニペット

UI / 状態管理

  • packages/scratch-gui/src/components/ruby-toolbar/ruby-toolbar.jsx — DNCL モード切替ボタン
  • packages/scratch-gui/src/containers/ruby-tab.jsx — DNCL モード state、エディタ切替制御、外部終了要求時の wasDncl 復元スキップ
  • packages/scratch-gui/src/reducers/dncl-mode.js — DNCL モードの Redux state(dnclMode + exitDnclModeExternallyRequested シグナル)
  • packages/scratch-gui/src/lib/locale-utils.js — isJapaneseLocale (DNCL は日本語ロケール限定)
  • packages/scratch-gui/src/components/dncl-mode-notice/ — コードタブの「日本語モード:ブロックが制限されています」通知バナー
  • packages/scratch-gui/src/components/extension-button/extension-button.jsx — DNCL モード時の confirm ダイアログ(upstream ファイル、Smalruby マーカー付き)
  • packages/scratch-gui/src/components/gui/gui.jsx / src/containers/gui.jsx — DnclModeNotice の配置と onRequestExitDnclMode の Redux 配線(upstream ファイル、Smalruby マーカー付き)
  • packages/scratch-gui/src/containers/blocks.jsx — DNCL モードのブロックフィルタ適用と即時再レンダリング(upstream ファイル、Smalruby マーカー付き)

scratch-vm

なし(GUI 側のトランスパイルで完結)。

infra

なし。

関連ブロック

DNCL モードは特定のブロックに紐づくものではなく、Ruby に変換できるすべてのブロックを DNCL でも書ける。ただし dncl-block-filter.js により、DNCL モードでは表示しないブロック(class 定義など Ruby 高度機能)がある。

設定・データ永続化

localStorage

  • smalruby:dnclMode — DNCL モードのオン/オフ
    • 値が 'true' のときのみ ON(デフォルト OFF)
    • 日本語以外のロケールでは無視される

URL パラメータ

  • ?rubyMode=dncl または ?rubyMode=ja または ?rubyMode=japanese — DNCL モード強制 ON
  • ?rubyMode=furigana または ?rubyMode=ruby — DNCL モード強制 OFF

Block ↔ Ruby ↔ DNCL の変換マトリクス

Smalruby の Ruby タブは 3 つのモード (Ruby / DNCL / ふりがな) を切り替えられ、それぞれが内部の Ruby 表現を異なる形式で表示します。さらに Scratch ブロックタブ とも双方向変換します。

ほとんどの構文は 3 方向すべてが round-trip 安定 ですが、いくつかの 意図的に不可逆な変換 があり、設計の根拠とともに本セクションで明示します。

表示する (display) の変換マトリクス

方向 変換結果 備考
Block looks_sayforsecs(msg, n) → Ruby say(msg, n) 秒数を保持
Block looks_sayforsecs(msg, n) → DNCL 表示する(msg) 秒数 n は無視
DNCL 表示する(args) → Ruby puts(args) 不可逆: say には戻さない
DNCL 表示する(args) → Block looks_sayforsecs(...) + @ruby:method:puts コメント + secs=1 Ruby に戻すと puts(args)

不可逆な変換とその理由

1. Block say(msg, n) → DNCL 表示する(msg) (秒数喪失)

理由: DNCL は「Console に値を表示する」という抽象的な操作を表現する言語で、秒数 (Scratch の吹き出し表示時間) は概念として持っていません。

結果: Block で秒数を 2 にしても DNCL では 表示する(msg) (秒数情報なし) になり、再度 Ruby/Block に戻すと 秒数は 1 (デフォルト) にリセットされます。

2. DNCL 表示する → Ruby puts (say には戻さない)

理由: puts の方が「Console に出力する」という意味に近く、DNCL の 表示する のセマンティクスと一致します。say は Scratch の吹き出し演出を含むため、DNCL の純粋な「表示」とは概念的に異なります。

結果: 一度 DNCL モードを経由した say(msg, n) は puts(msg) に変換されたまま戻らない。秒数情報も失われます。

これは「DNCL モードに切り替えると、その時点でブロックが『表示する』ブロックに正規化される」という設計意図を反映した仕様で、教育的な観点で「DNCL 的な書き方」に揃えるためのものです。

変換マトリクスを lock-in するテスト

このマトリクスを保証する単体テスト: packages/scratch-gui/test/unit/lib/dncl/dncl-say-block-roundtrip.test.js

DNCLv2 互換

Smalruby は DNCLv2 形式のプログラムをそのまま実行 できる。これは「pre-processor が DNCLv2 構文を Smalruby DNCL に正規化してから既存パイプラインに流す」設計で実現している(dncl-v2-preprocessor.js)。

対応する DNCLv2 構文

DNCLv2 構文 例 正規化後
行番号 (1) Data = [1, 2, 3] / (00-1) Data = [1] Data = [1, 2, 3] / Data = [1]
末尾コロン(半角 : / 全角 :) もし a > 0 ならば: もし a > 0 ならば
条件の括弧がキーワードに密着 もし( a < b ) and ( c == 0 )ならば: もし ( a < b ) かつ ( c == 0 ) ならば
行番号の無い継続行 (10) 表示する(i, "人目",
⎿ x, "分間")
表示する(i, "人目", x, "分間")(行番号付きの行の続きのみ。継続行の ⎿ も end として数える)
// a = 合計 // 個数 常に整数除算(行番号付きの行でも)。スモウルビーでは // はコメントではない。行番号付きで貼り付けた共通テストの // コメントは、下の「貼り付け時の整形」で # に直す
配列の初期化 配列変数 Hirabun を初期化する Hirabun = []
入力(整数 / 文字列) n =【整数を入力】 / s =【文字列を入力】 ask("") + @n = answer.to_i / @s = answer
の間繰り返す a > 0 の間繰り返す: a > 0 の間
増やしながら繰り返す i を ... 増やしながら繰り返す: i を ... 増やしながら
減らしながら繰り返す i を ... 減らしながら繰り返す: i を ... 減らしながら
インデントマーカー (|) | a = 1 a = 1(2 スペース/marker)
暗黙の end (⎿) ⎿ a = 1 (1 marker) a = 1 + 自動で を実行する 追加
同一行複数 ⎿ ⎿ ⎿ a = 1 2 個の end を一度に追加
インデントだけのブロック(| / ⎿ も終了行も無し) もし a > 0 ならば:
a = 1
b = 2
末尾コロンの行の後にインデントした本体があれば、インデントが開始行と同じか浅い行の前(またはプログラム末尾)で を実行する / を繰り返す を自動で追加。同じ深さの そうでなくもし / そうでなければ は もし の続き。空行・コメント行では閉じない。明示の終了行・⎿ と二重には閉じない
同一行複数代入 a = X , b = Y 2 行に分割
and / or a > 0 and b == 1 a > 0 かつ b == 1
関数定義 calc(x) を定義する: 関数 calc(x)
表示する 多引数 表示する(a, "は", b) puts(@a.to_s + "は" + @b.to_s)(1 つの吹き出し)

貼り付け時の整形

スモウルビーの日本語では // は常に整数除算(合計 // 個数)で、コメントではない(スモウルビーの制限)。コメントと整数除算を同じ記号から確実に見分ける方法は無いため、言語としては共通テストの例題が使う // コメント(n = 1 // 生徒番号)を扱わない。

ただし、行番号付きのテキスト(例題サイトからコピーしたことが明らかなもの)を日本語モードの Ruby タブのエディタに貼り付けたとき(Ctrl+V / ⌘V と右クリックメニューの「貼り付け」の両方)だけ、貼り付けるテキストを 1 回だけ整形する(dncl-paste-tidy.js の tidyPastedDncl。エディタ側は src/containers/ruby-tab/editor-setup.js の registerCustomPasteAction / setupPasteTransform)。

  • 行番号((00) / (00-1) / 1:)が 1 行でもあるときだけ整形する。行番号の無いテキストは // も含めて何も変えない(n = 1 // 生徒番号 を行番号なしで貼るとそのまま残り、変換エラーになる)
    • 1: は後ろに空白(または行末)があるときだけ行番号とみなす(10:30 は行番号ではない)
  • 行番号を消す。行番号付きの行の続き(継続行)は、行番号を消す前に 1 行に結合する
  • // がプログラムに見えれば整数除算として残し、後ろが変数でも数でもなく、演算子が 1 つも無いときだけコメントとして # に直す(n = 1 // 生徒番号 → n = 1 # 生徒番号)。プログラムに見えるのは次のとき:
    • 後ろが数か英字の名前で始まる(10 // 3、x // y)
    • 後ろの日本語の語が、貼り付けたテキストかエディタの既存コードの中で変数として使われている(合計 // 個数 と 個数 = 3)
    • 後ろに演算子がある(+ - * / % = < > ! & |、÷ × ≦ ≧ ≠、空白で区切った かつ / または。例: 合計 // 個数 + 1)。文字列と # コメントの中の演算子は数えない。釣り合った括弧は演算子ではない(n = 1 // 生徒番号 (1から) はコメント)
    • // が括弧の中にある(表示する(合計 // 個数))
  • // の後ろに何も無いときはコメント
  • a = 合計 // 個数 と n = 1 // 生徒番号 は字面では区別できないため、個数 がどこにも変数として出てこないとき(空のエディタに行番号付きで a = 合計 // 個数 だけを貼るなど)はコメントになる(a = 合計 # 個数)。整数除算にしたいときは、変数を使うコードと一緒に貼るか、貼り付けた後で # を // に戻す
  • 文字列リテラルの中の //(表示する("http://..."))には触らない
  • Ruby モード・ふりがなモードでは整形しない

// をコメントのつもりで書いた行(行番号なしで貼った例題など)は変換エラーになる。日本語モードでは、// のある行のエラーマーカーの先頭に「スモウルビーでは // はコメントではありません(整数の割り算です)。コメントには # をつかってください。」(gui.rubyTab.dnclDoubleSlashHint)を添えて、# を使うよう伝える(ruby-tab.jsx の showErrors が withDoubleSlashHint を呼ぶ)。

対応しない DNCLv2 機能

以下は 未対応(必要なら別 issue で対応):

  • 要素数 N の配列 A 形式の配列宣言
  • 配列 A のすべての要素に 0 を代入する(要素数が書かれないため、生成する Ruby の配列サイズが決まらない)
  • 例題中で定義されていない関数の呼び出し(差分(...) / 最大値(...) / 枚数(...) など。問題文で「用意されている」とされる関数)
  • 変数 に 式 を入れる 形式の自然言語代入
  • 全角ブラケット []

Smalruby 独自の差分

  • ブロック終端が日本語キーワード(を実行する, を繰り返す, と定義する)。turn-around 後はこの形式に正規化される
  • Scratch ブロックに変換されるため、使える機能が Scratch の範囲に限定される
  • 変数に @, $ プレフィックスは使えない(Ruby の変数記法は自動処理)

動作確認用例題

完全な DNCLv2 例題(線形探索)の round-trip テスト: test/unit/lib/dncl/dncl-v2-example.test.js

言語仕様

完全な構文・機能リストは docs/smalruby-dncl-spec.ja.md を参照。以下の要素を網羅:

  • プログラム構造(文、コメント)
  • 変数と代入
  • リテラル(数値、文字列、真偽値、配列、ハッシュ)
  • 演算子
  • 制御構造(条件分岐、繰り返し)
  • 関数定義
  • DNCLv2 との違い

テスト

  • 単体テスト: packages/scratch-gui/test/unit/lib/dncl/
    • dncl-v2-preprocessor.test.js — DNCLv2 正規化テスト(53 cases)
    • dncl-v2-example.test.js — DNCLv2 例題の end-to-end 変換テスト
    • dncl-v2-ictl-samples.test.js — 共通テスト例題で使われる記法(もし(・(00-1)・全角コロン・継続行・// が常に整数除算であること・入力形式・インデントだけのブロック)と、例題プログラムが prism で構文エラーにならないことの検証
    • dncl-paste-tidy.test.js — 貼り付け時の整形(行番号の削除・// コメント → #・整数除算と文字列の // は不変)
    • dncl-display-multi-arg.test.js — 表示する 多引数 / puts(... + ...) round-trip
    • dncl-to-ruby.test.js — DNCL → Ruby 変換ユニットテスト
    • ruby-to-dncl.test.js — Ruby → DNCL 逆変換ユニットテスト
    • dncl-roundtrip.test.js — DNCL ↔ Ruby round-trip 安定性
  • Round-trip テスト (左結合演算子の括弧除去): packages/scratch-gui/test/unit/lib/ruby-roundtrip-puts-concat.test.js
  • ブロック制限 UX の単体テスト:
    • packages/scratch-gui/test/unit/reducers/dncl-mode-reducer.test.js — exitDnclModeExternallyRequested シグナル
    • packages/scratch-gui/test/unit/components/extension-button-dncl.test.jsx — 拡張機能ボタンの confirm フロー
    • packages/scratch-gui/test/unit/components/dncl-mode-notice.test.jsx — 通知バナー
  • 結合テスト: packages/scratch-gui/test/integration/dncl-mode-validation.test.js(DNCL バリデーション + 通知バナー/confirm フロー)

関連ドキュメント

関連 Issue / PR

主要 PR は履歴を参照(feat:.*dncl で grep)。