はじめに
Rubyはバージョン3.2からWebAssembly(WASM)に対応をしています。
詳しくは以下の記事をご覧ください。
RubyでWASMを触っているうちに、「Rubyがブラウザで動くってことはRubyでプログラミング言語のインタプリタを作れればどんな言語でもブラウザで動かせるのでは...!?」という当たり前なことに気づきました。
この記事では、ruby.wasmで簡単なプログラミング言語のインタプリタを作成した話を紹介します。
半分ネタ記事です。
やること
今回は以下のようなステップでランタイムを作っていきます。
- プログラミング言語の構文を定義し、それをパース、実行できるRubyのプログラムを実装する
- ブラウザでプログラムを入力、結果を出力できるようにする
プログラミング言語の構文を定義し、それをパース、実行できるRubyのプログラムを実装する
まずは、プログラミング言語の構文を定義し、それをパース、実行できるRubyのプログラムを実装します。
今回は簡単に作れるBrainf*ckのランタイムを実装します。
今回は以下のようにランタイムを実装してみました。
class BrainfuckInterpreter
def initialize
@tape = Array.new(30000, 0)
@pointer = 0
@output = ""
end
def run(code)
loop_stack = []
code_ptr = 0
while code_ptr < code.length
case code[code_ptr]
when ">"
@pointer += 1
when "<"
@pointer -= 1
when "+"
@tape[@pointer] = (@tape[@pointer] + 1) % 256
when "-"
@tape[@pointer] = (@tape[@pointer] - 1) % 256
when "."
@output += @tape[@pointer].chr
when ","
@tape[@pointer] = $stdin.getbyte
when "["
if @tape[@pointer] == 0
loop_level = 1
while loop_level > 0
code_ptr += 1
if code[code_ptr] == "["
loop_level += 1
elsif code[code_ptr] == "]"
loop_level -= 1
end
end
else
loop_stack.push(code_ptr)
end
when "]"
if @tape[@pointer] == 0
loop_stack.pop
else
code_ptr = loop_stack.last - 1
end
end
code_ptr += 1
end
@output
end
end
ブラウザでプログラムを入力、結果を出力できるようにする
RubyでBrainf*ckは実行できるようになったので、次はブラウザでプログラムを入力、結果を出力できるようにします。
今回は、以前作成したruby.wasmの仮想DOMを使って実装していきます。
今回は以下のようなコードを実装します。
state = {
code: '>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.>>>++++++++[<++++>-]<.>>>++++++++++[<+++++++++>-]<---.<<<<.+++.------.--------.>>+.>++++++++++.',
output: ''
}
actions = {
update_code: -> (state, value) {
state[:code] = value
},
run_code: ->(state, e) {
e.preventDefault()
interpreter = BrainfuckInterpreter.new
output = interpreter.run(state[:code])
state[:output] = output
}
}
view = ->(state, actions) {
eval DomParser.parse(<<-DOM)
<div class="max-w-lg mx-auto bg-white p-4 rounded-lg shadow">
<h1 class="text-2xl font-bold mb-4">Brainf*ck Interpreter</h1>
<form onsubmit='{->(e) { actions[:run_code].call(state, e) }}'>
<div class="code-container">
<label for="code-input" class="block font-medium">Code:</label>
<textarea
id="code-input"
class="code-input border border-gray-300 rounded-lg p-2 w-full h-40"
oninput='{->(e) { actions[:update_code].call(state, e[:target][:value].to_s) }}'
>
{state[:code]}
</textarea>
</div>
<div class="mt-4">
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600"
>Execute</button>
</div>
</form>
<div class="mt-8">
<h2 class="text-lg font-bold mb-4">Result:</h2>
<div class="result-container">
{state[:output]}
</div>
</div>
</div>
DOM
}
App.new(
el: "#app",
state:,
view:,
actions:
)
出来上がったもの
実際に実装したものをGitHub Actionsで公開しました。
以下のようにBrainf*ckを実行することができます。
リポジトリはこちらです。
まとめ
Rubyでインタプリタを作ればブラウザで動かせました!