Monkey言語のREPLの入力をブラウザのテキストボックスから行う
環境
・macOS Big Sur ver. 11.5.1(20G80) -> 12.6 (21G115)
・MacBook Air M1, 2020 メモリ 8GB
・go1.18.3 darwin/arm64
背景
ここの記事から引き継いでいる.
Monkey言語というのは,この参考書で作成している独自の言語である.
REPLとは
Read, Eval, Print, Loopの略であり,入力を読み込んで,インタプリタに送って評価し,その結果を出力し,また最初に戻るという繰り返しである.
現状のREPL
参考書で最終的に利用しているREPLと同じものになっていると思う.
package repl
import (
"bufio"
"fmt"
"io"
"monkey/lexer"
"monkey/parser"
"monkey/evaluator"
"monkey/object"
)
const PROMPT = ">> "
func Start(in io.Reader, out io.Writer){
scanner := bufio.NewScanner(in)
env := object.NewEnvironment()
for{
fmt.Printf(PROMPT)
scanned := scanner.Scan()
if !scanned {
return
}
line := scanner.Text()
l := lexer.New(line)
p := parser.New(l)
program := p.ParseProgram()
if len(p.Errors()) != 0{
printParseErrors(out, p.Errors())
continue
}
evaluated := evaluator.Eval(program, env)
if evaluated != nil{
io.WriteString(out, evaluated.Inspect())
io.WriteString(out, "\n")
}
}
}
func printParseErrors(out io.Writer, errors []string){
for _, msg := range errors{
io.WriteString(out, "\t"+msg+"\n")
}
}
入力に関係している部分
func Start(in io.Reader, out io.Writer){
scanner := bufio.NewScanner(in) (1)
env := object.NewEnvironment()
for{
fmt.Printf(PROMPT)
scanned := scanner.Scan() (2)
if !scanned {
return
}
line := scanner.Text() (3)
Scannerの内部構造は次のようになっているらしい.
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
// (以下略)
}^
(1)でio.Readerを引数に渡すNewScanner関数によってbufio.Scannerの作成を行なっている.
これで作成されたスキャナーは,デフォルトで「行」をトークンにするように設定されている.
スキャナーを使ってデータを読みとるためには、「Scan()メソッドで読み込み→Text()メソッドで結果を取り出す」という手順を踏む.
(2)がScan()メソッドの読み込みに該当する.
(3)がText()メソッドによる結果の取り出しである.
この入力の部分にテキストボックスから取得したものを入り込ませればよさそうである.
テキストボックスの入力を取ってきてそうなところ
前回の記事で記しておいたJSスクリプトエンジンを作っているコードで,どの部分がテキストボックスのコードを取得しているのか考える.
ボタンを押すたびに評価していたことを考えると,ボタンのクリック処理の中にあると推測できる.
その部分を下に記す.
btn.Call("addEventListener", "click", js.FuncOf(func(js.Value, []js.Value) interface{} {
mb := NewMusicBox()
window.Set("play", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
mb.Play(Note{args[0].Int(), args[1].Float()})
return nil
}))
code := document.Call("getElementById", "code")
src := code.Get("value")
window.Call("eval", src)
return nil
}))
srcがソースであることを考えると,code.valueにコードを入っているのではないかと考えられるが,いまいちどこでvalueに入れているのかは把握できていない,
消去法的に,
code := document.Call("getElementById", "code")
で入れているのかと考えられる.
Monkeyのどこのファイルに組み込むのか
現状Monkey言語周りのファイル環境は次のようになっている.
(テストファイルは除く)
monkey
├── ast
│ └── ast.go
├── docs
│ ├── build.wasm
│ ├── index.html
│ └── wasm_exec.js
├── evaluator
│ ├── builtins.go
│ └── evaluator.go
├── lexer
│ └── lexer.go
├── object
│ ├── environment.go
│ └── object.go
├── parser
│ └── parser.go
├── repl
│ └── repl.go
├── server
│ └── server.go
├── token
│ └── token.go
├── wasm
│ └── webassembly.go
├── go.mod
├── go.sum
└── main.go
ビルドしなきゃいけないことを考えると無難にwebseembly.goに組み込むのがいいのだろうか.
builtins.go
Monkey言語において組み込み関数の動きはこのファイルに記述されている.
playの実装はこの記事で行なっている.(記事が循環していて申し訳ない)
play部分をいかに記す.
"play": &object.Builtin{
Fn: func(args ...object.Object) object.Object{
if len(args) != 1{
return newError("wrong number of arguments. got=%d, want=1", len(args))
}
switch arg := args[0].(type){
case *object.Integer:
//ここにWebAudioAPIの処理をいれる.
return &object.Integer{Value: arg.Value}
default:
return newError("argument to `play` not supported, got %s", args[0].Type())
}
},
},
書き殴りメモ
ボタンクリックでテキストボックスの内容をREPLに送って,標準入力として捉え,解析,評価する.
言語の命令文によってビルドやファイルの実行を行うことはできるのだろうか.
code sampleは他のテキストファイルの内容を入れる.コンソールの入力をREPLに入れる際にテキストファイルにも書き込み,Monkeyの解析評価とは別にwebassemble.goを同期して動かす,
音を鳴らすplayだけWebAudioAPIにつなげば良い,ループなどはMonkey言語の処理に任せられる.
この記事が気に入ったらサポートをしてみませんか?