プログラミングを始めようとして、最初の壁になるのが「環境構築」です。
Pythonをインストールしたり、パスを通したり、コンパイラを準備したり……。
これらに躓いて学習を諦めてしまうのは非常にもったいないことです。
そこで、 「環境構築不要」「その場で実践」「AIがサポート」 をコンセプトにしたプログラミング学習サイト my.code(); を開発しました。
本記事では、このサイトの概要と、それを支える技術的な裏側(特にブラウザ上での多言語実行環境の実装)について詳しく紹介します。
この記事の文章の半分くらいはGeminiが書きました。文体がAIっぽいですが気にしないでください
1. my.code(); とは?
my.code(); は、ブラウザさえあれば誰でもすぐにプログラミング学習を始められるプラットフォームです。
主な特徴
- 環境構築一切不要: Python, Ruby, JavaScript, TypeScript, C++, Rust などのコードがブラウザ上でそのまま動きます。
- インタラクティブな教材: 解説文の中にREPL(対話型評価環境)やコードエディタが埋め込まれており、読みながらその場でコードを試せます。
- AIアシスタント: 学習中にわからないことがあれば、いつでもAIに質問可能。AIは「今読んでいるセクション」や「エディタに書いたコード」を理解した上で回答してくれます。
2. 技術スタック
モダンなWeb技術を組み合わせて構築しています。
- フロントエンド: Next.js (App Router)
- CSS: Tailwind CSS / DaisyUI
- データベース: PostgreSQL / Drizzle ORM
- AI呼び出し: OpenRouter
- コードエディタ・ターミナル機能: react-ace / xterm.js
3. 多言語実行環境の裏側
「ブラウザだけでコードが動く」を実現するために、言語ごとに異なるアプローチを採用しています。
-
ブラウザ内完結(Client-side Runtime)
ユーザーのPCリソースを最大限活用し、サーバー負荷を抑えるために、可能な限りブラウザ内で完結させています。
メインスレッドをブロックしないよう、 Web Worker 上で動作させています。 -
APIベース(Server-side Runtime)
ブラウザ内での動作が重い、あるいは困難なコンパイル言語については、外部APIを活用しています。
Python
Pyodide を使用しています。Pyodideは、CPython(Pythonのインタプリター)をWebAssemblyにコンパイルしたプロジェクトで、ブラウザ上でPythonコードを実行できます。
参考:
iOSではWebWorker内でPyodideとruby.wasmが正しく動作しないようです。 (issue #38, pyodide/pyodide issue #441)
Ruby
ruby.wasm を使用しています。ruby.wasmは、CRubyをWebAssemblyにビルドしたもので、Pyodideと同様にブラウザ上でRubyコードを実行できます。
参考:
JavaScript
JavaScriptはブラウザのネイティブ言語なので、特別なランタイムを用意せずとも eval を使ってコードを実行できます。 eval はセキュリティ上危険とよく言われますが、my.code();では 間接eval をWebWorker上で動かしているのでメインスレッドやサイトそのものへの影響はありません。
しかし、ブラウザの eval に単に文字列を流し込むだけではプログラミング学習における「REPL(対話型環境)」の挙動を再現できないという課題がありました。my.code();では、以下のような工夫を凝らしています。
-
変数の永続化:
constやletはブロックスコープを持つため、eval("const x = 1")の後にeval("x")を実行しても通常はエラーになります。一方varで宣言した変数であれば後続のevalからアクセス可能です。そのため、ユーザーが入力したコードの先頭がconst/letだった場合はそれを動的にvarに書き換えて実行することで、セッション中ずっと変数が保持されるようにしています。
またclass Foo {...}も同様にvar Foo = class {...}に書き換えています。
ただし副作用として 「const なのに再代入できてしまう」「let なのに再宣言できてしまう」 という本来の仕様とは異なる挙動が発生してしまいます。
-
オブジェクトリテラルの判別:
{ a: 1 }という入力は、Node.jsではオブジェクトとして扱われますが、evalには「ラベル付きのブロック」と評価されてしまいます。これを( { a: 1 } )のように括弧で囲って評価し直すことで、オブジェクトとして評価されるようにしています。
ただし、{console.log("a")}のようにオブジェクトではなく本当に文であるケースも考えられます。
my.code(); ではユーザーのコードが波括弧で囲われている場合はまず括弧をつけてパースし、SyntaxErrorが発生する場合のみ括弧をつけずに再度評価するというロジックにしました。 -
Top-level await のサポート:
eval内のトップレベルでawaitは使えません。 my.code();では、awaitキーワードで始まるコードを検知した場合、awaitを除いた部分のみをevalで評価し、その評価結果として得られるPromiseを待機(await)してから結果を返す仕組みを導入しています。
ただし、 const res = await fetch('https://...'); のように変数宣言が絡むコードを処理できません。今後対策を検討します。(issue #126)
| 入力例 | Node.js の動作 |
eval の動作 |
my.code(); の動作 |
|---|---|---|---|
a = 1 |
グローバルな変数 a | グローバルな変数 a | グローバルな変数 a |
var a = 1 |
グローバルな変数 a | グローバルな変数 a | グローバルな変数 a |
const a = 1 |
const変数 a | 変数aは消える | グローバルな変数 a |
let a = 1 |
let変数 a | 変数aは消える | グローバルな変数 a |
{a: 1} |
オブジェクト {a: 1}
|
ラベル a: と文 1
|
オブジェクト ({a: 1})
|
{console.log("a")} |
"a"を表示する | "a"を表示する | "a"を表示する |
await p |
pの完了を待つ | SyntaxError | await eval("p") |
TypeScript
TypeScriptのコンパイラtsc (npmパッケージ typescript) は、TypeScript製です。そのため、実はコンパイラそのものをブラウザ上で動かすことが可能です。
@typescript/vfs パッケージを使って、ブラウザ内でTypeScriptのコードをtscコンパイラに渡してコンパイルし、得られるJavaScriptコードを実行しています。TS Playground でも使われているものです。
C++
Wandbox API を使用しています。READMEのAPIドキュメントのリンクはリンク切れしていたので、AIにwandboxのソースコードを投げてAPIの使い方を解析してもらいました。
C++はエラー時にスタックトレース等が出ない、初心者に優しくない言語です。そこで、my.code();ではユーザーのコードをそのままAPIに投げるのではなく、手を加えています。ユーザーのmain()関数が実行される前にシグナルハンドラーをセットし、Boost.Stacktrace ライブラリを使ってスタックトレースを出力するコードを挿入しています。
本来シグナルハンドラー内で std::cerr とかを呼び出すのは安全ではないとされていますが。
Wandboxは公式には複数のソースファイルをコンパイルする機能を提供していませんが、 Compiler options: にファイル名を入れるとそのままg++に渡されてコンパイルできてしまうというハックを利用しています。
Rust
C++と同様にWandbox APIを使用しています。
Rustには標準でスタックトレースを表示する機能がありますが、環境変数 RUST_BACKTRACE=1 が必要であり、Wandboxは環境変数を設定する機能を提供していません。そのため、やはりユーザーのコードに手を加え、main関数に std::env::set_var("RUST_BACKTRACE", "1"); を挿入しています。
4. コンテンツ管理とMarkdown拡張
教材コンテンツはすべて Markdown で管理されていますが、単なるテキスト表示ではなく、独自のコードブロック拡張を行っています。
```js-repl
> console.log("Hello")
Hello
```
```python:main.py
print("Hello World")
```
```python-exec:main.py
Hello World
```
このように記述するだけで、教材の中に 「本物のREPL」「編集可能なエディタ」「実行ボタンと出力結果」 が自動的にレンダリングされます。
これを実現するために、react-markdown のコンポーネントを拡張し、特定の言語指定(-repl, -exec など)を検知して独自の React コンポーネントへ差し替える処理を行っています。
5. AIチャットのコンテキスト注入
my.code(); のAIチャットが「賢い」理由は、単にAPIを叩いているだけでなく、 「ユーザーが今何を見ているか」 をプロンプトに動的に注入しているからです。
- 現在のセクション: ユーザーが読んでいる解説文の内容。
- エディタの内容: ユーザーが編集中の全ファイルの内容。
- 実行結果: 直近で実行したコードの出力。
これらをLLMに渡すことで、「このエラーの原因を教えて」といった曖昧な質問に対しても、的確なアドバイスが可能になっています。
AI によるドキュメント書き換え機能
さらに my.code(); のAIアシスタントの特徴的な機能として、AIチャットから教材の内容を書き換えることもできるという点があります。ユーザーの質問への回答は、ユーザー向けのメッセージとは別に、教材の内容を書き換えるための命令も含んでいます。
具体的には、AIは以下の形式の出力をします。
<<<<<<< SEARCH
修正したい元の文章の塊(一字一句違わずに)
=======
修正後の新しい文章の塊
>>>>>>> REPLACE
サイトを開いた際には、あらかじめ用意された教材に対して過去のチャットでのAIによる変更をすべて適用した状態でコンテンツが表示されるようになっています。
ユーザーがドキュメントを再度見返した際には過去のチャットでのAIからのフィードバックが反映された内容になっているため、チャット履歴を1つ1つ見返すことなく、過去に自分がつまずいたポイントなどを復習することができます。
6. まとめとこれから
「my.code();」は、プログラミングを始めたいすべての人が、最初の「環境構築」という壁を越えて、楽しく学び続けられる場所を目指しています。
現在は基本的な言語のチュートリアルが中心ですが、さらに多くのプログラミング言語への対応や、ユーザーごとの学習進捗管理などの機能追加も計画しています。
興味のある方は、ぜひ一度 my.code(); を触ってみてください!
また、サイトの改善案やフィードバックもお待ちしております。