悪用厳禁？Praat Scriptの黒魔術、謎文法

# はじめに

Praat Scriptは今年で25周年を迎え、メジャーバージョンも現在6系です。一方、後方互換を重視していて、（今とは似ても似つかない）過去の文法のコードもちゃんと動きます。
新旧の文法が混在する...そう、Praat Scriptは**黒魔術の宝庫**でもあります。

本記事では、Praat Scriptの文法を悪用しメタプログラミングする方法をご紹介します。**実用性、可読性は保証しません**[^1]。実用的なテクニックについては以前の記事をご覧ください。

Praat Scriptシリーズ一覧

1. [Praat Script基礎文法最速マスター2022](https://qiita.com/Syuparn/items/eaac2dae596546e75208)
2. [Effective Praat Script](https://qiita.com/Syuparn/items/f2e108f370836c4567cb)
3. 悪用厳禁？Praat Scriptの黒魔術、謎文法 (当記事)
4. [Praat Scriptでゲームを作ってみよう](https://qiita.com/Syuparn/items/06e9136a715290fb34a3)

# すべての元凶「変数置換」

文字列変数をシングルクォーテーションでくくる( `'x$'` )と、**中身をコードとして展開する**ことができます。
（イメージとしては**マクロの実行時版**です）

```praat
s$ = "1 + 2"
# 展開した結果の `appendInfoLine: 1 + 2` が評価される
appendInfoLine: 's$' ; 3
```

そしてこの展開、ASTに一切関係なく**任意の文字列**を**動的**にコードに埋め込めます。激アツですね。

## 生まれた経緯

もちろんこの構文はメタプログラミングのためのものではありません :innocent:

古い文法では、引数は `""` で括らなくても文字列リテラルとして扱われました。

```praat
# 今でいう `appendInfoLine: "foo"`
printline foo
```

そのままでは変数を渡せないので、変数置換 `''` という構文が導入されたのだと思います[^single]。

```praat
s$ = "abc"
# これだと "s$" が表示されてしまう
printline s$
# 変数をコード上に展開することで `echo abc` が評価され、"abc" が表示される
printline 's$'
```

旧文法、新文法の差分は以下の記事で詳しく紹介されています。

https://id.fnshr.info/2013/11/10/praat-script-renewal/

そして、この `''` が使える場所に制限が無いので、激アツ黒魔術になったというわけです。

## どこで使える？

行まるごとだろうがトークンの途中だろうが、文のほぼすべての箇所で使えます。

```praat
# 行まるごと
line$ = "appendInfoLine: ""Hello, world!"""
'line$' ; Hello, world!

# 予約語
if$ = "if"
'if$' 1 == 1
  appendInfoLine: "yes" ; yes
end'if$'

# トークンを跨いでも問題なし！
s$ = "foLine: ""He"
appendIn's$'llo, world!" ; Hello, world!
```

特に、最後の例のように**文構造を完全に無視して置換できる**のが強力な点です。

実装を読んでいないので予想ですが、Praat Scriptはコード全体をトークン列、ASTに変換してから評価するのではなく、1行ずつ読み込んでいるように見えます（でないと、前の行の変数の評価が終わった後にその値をトークンの一部に利用することはできないので）。

## 制約

強力ですが、使えない場所もあります（裏技なのでやむなし）。

### 制御構造 (`for` や `procedure` 等)

ジャンプ先がの行が認識できずエラーになります。

```praat
# 予約語

for$ = "for"
'for$' i from 1 to 3
  appendInfoLine: i
end'for$' ; Unmatched endfor
```

### 複数行を一度に置換

1行で続けて書いたように認識されるので構文エラーになります。

```praat
s$ = "n = 1 + 2" + newline$ + "appendInfoLine: n"
's$' ; expected the end of the formula, but found "appendInfoLine"
```

### 多重置換

置換は1度しかできません。

```praat
inner$ = "1 + 2"
outer$ =  "'" + "inner$" + "'"
appendInfoLine: 'outer$' ; unknown symbol
```

### 変数が未定義かつ文字列リテラル内で置換した場合、ただの文字列になる

```praat
# s$ は未定義
appendInfoLine: "'s$'" ; 's$' (そのまま表示)
s$ = "foo"
appendInfoLine: "'foo'" ; foo (展開される)
```

## 応用例

### 参照渡しの代わり

公式リファレンスに、「procedureを関数の代わりに使う方法（非推奨）」として紹介されています。

https://www.fon.hum.uva.nl/praat/manual/Scripting_5_5__Procedures.html

> Another way to emulate functions is to use a variable name as an argument:
> （procedureで）関数を再現するもう一つの方法として、変数名を引数として使用します。

```praat
@squareNumber: 5, "square5"
writeInfoLine: "The square of 5 is ", square5, "."
procedure squareNumber: .number, .squareVariableName$
    '.squareVariableName$' = .number ^ 2
endproc
```

> However, this uses variable substitution, a trick better avoided.
> しかし、この方法は変数の置換を利用する、避けたほうが良いトリックです。

### 早期return

「[Effective Praat Script](https://qiita.com/Syuparn/items/f2e108f370836c4567cb)」の方でも紹介しましたが、変数置換で `endproc` をprocedureに2つ以上書けるようになる（インタープリターを騙す）ので早期returnができます。

```praat
endproc$ = "endproc"

procedure isEven: .x
	# 整数以外を弾いておく
	if floor(.x) != .x
		.return = false
		# インタープリターをだます！（コード読み込み時は `endproc` は1つしかないので構文エラーにならないが、この行を「評価」したら `endproc` になる）
		'endproc$'
	endif

	if .x mod 2 == 0
		.return = false
		'endproc$'
	endif

	.return = true
endproc

@isEven: 2
appendInfoLine: isEven.return ; 0
@isEven: 2.5
appendInfoLine: isEven.return ; 0
@isEven: 5
appendInfoLine: isEven.return ; 1
```

この後紹介する他の黒魔術でも、色々な所で変数置換が登場します。

# 再帰

これまでの記事でも、**procedureを「名前空間変更を伴うgoto」** と表現してきました。
スコープを抜けても変数が死なないので、普通に書いたら再帰はできません。

```praat:これは無理
procedure factorial: .n
  if .n < 0
    .return = undefined
  else
    # factorialをもう一度呼んだ時点で、**同じスコープの** .n が上書きされてしまう
    @factorial: .n - 1
    .return = .factorial.return * .n
  endif
endproc
```

諦めるのはまだ早い、上書きが困るのであれば**元に戻せばいい**のです。

```praat
clearinfo
endproc$ = "endproc"
procedure factorial: .n
	if .n < 0
		.return = undefined
		'endproc$'
	endif

	if .n <= 1
		.return = 1
		'endproc$'
	endif
	
	@factorial: .n - 1
	# 再帰の外側のスコープの.nの値に戻すためインクリメント
	.n += 1
	.return = factorial.return * .n
endproc

@factorial(0)
appendInfoLine(factorial.return) ;1
@factorial(1)
appendInfoLine(factorial.return) ;1
@factorial(-1)
appendInfoLine(factorial.return); undefined
@factorial(5)
appendInfoLine(factorial.return) ; 120
@factorial(10)
appendInfoLine(factorial.return); 3628800
```

# `~` でQuine

`~` で文字列リテラルを作成できます。範囲は行末までで、**`""`と違ってエスケープ不要**です（重要）。

```praat
s$ = ~Hello, world! ; コメントアウトとか関係なく全部文字列化
appendInfoLine: s$
s2$ = ~"foo"
appendInfoLine: s2$
```

```:出力
Hello, world! ; コメントアウトとか関係なく全部文字列化
"foo"
```

エスケープ不要なので、**Quine(自分自身を出力するコード)に利用**できます。

```praat
s$ = ~s$ = ...writeInfo(replace$(s$, "...", "~" + s$ + newline$, 1))
writeInfo(replace$(s$, "...", "~" + s$ + newline$, 1))
```

```:出力
s$ = ~s$ = ...writeInfo(replace$(s$, "...", "~" + s$ + newline$, 1))
writeInfo(replace$(s$, "...", "~" + s$ + newline$, 1))
```

Quineの作り方は「あなたの知らない超絶技巧プログラミングの世界」を参考にしました。

https://gihyo.jp/book/2015/978-4-7741-7643-7

# `;` はコメントアウトとは限らない！

Praat Scriptのparser、かなり複雑そうですね...

```praat
# `;` がprocedure名！
procedure ;
  appendInfoLine: "a"
endproc

@;: ; a
```

# 条件付きgoto

gotoの引数で真偽値を与えると、trutyの場合のみジャンプします。

```praat
goto BAR 0 ;falseなので飛ばない
goto FOO 1 ; trueなので飛ぶ
exitScript: 0

label FOO
appendInfoLine: "foooooooooooooo!" ; foooooooooooooo!
exitScript: 0

label BAR
appendInfoLine: "baaaaaaaaaaaaar!"
```

# sendSocketでHTTPリクエスト？

`sendSocket` は、TCPソケット通信をするための関数です。なぜか送信専用で受信はできません。一応HTTPリクエストを送り付けることができます（だからどうした）。将来receiveSocketができればPraat Scriptでサーバーが立てられる！（妄想）

```praat
clearinfo
# HTTPリクエストべた書き
req$ = "GET / HTTP/1.1'newline$'Host: Praat"
# リクエスト送信
sendsocket localhost:8000", 'req$'
```

```bash
# ちゃんと届いた！
$ python -m http.server 8000
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:127.0.0.1 - - [11/Apr/2022 21:34:19] "GET / HTTP/1.1" 200 -
```

# `~` で疑似ブロック

Rubyのブロックを再現しました。

```praat
@times: 4, ~
... appendInfoLine("hello!") \
... appendInfoLine("hello, world!")
```

```
hello!
hello, world!
hello!
hello, world!
hello!
hello, world!
hello!
hello, world!
```

種明かしをすると、`~` でブロックとして使いたい文を文字列化して、変数置換で実行しています。
また、行の区切り文字として `\` を使用しています（Praat Scriptにこんな文法はありません）。

```praat
procedure yield(.blk$)
	# replace backslashes to newlines
	.blk$ = replace_regex$(.blk$, "\\", newline$, 0)

	while .blk$ <> ""
		# extract line
		.line$ = extractLine$(.blk$, "")
		# trim .line$ from .blk$
		if index_regex(.blk$, "\n")
			.blk$ = replace_regex$(.blk$, "^[^\n]*\n", "", 1)
		else
			.blk$ = ""
		endif

		# trim spaces, otherwise an error occurs)
		.line$ = replace_regex$(.line$, "(^\s+|\s+$)", "", 0)
		# eval .line$
		'.line$'
	endwhile
endproc

procedure times(.n, .blk$)
	for .i to .n
		@yield(.blk$)
	endfor
endproc
```

正規表現をもっと使えば `each` や `map` も作れそうです。

# sumOverで1行ループ処理

sumOverの第2引数は、numberに評価される式であればなんでも入れられます。`appendInfoLine` は `1` を返すので、1からnまでの整数を1行で出力できます！

```praat
sumOver(i to 10, appendInfoLine: i)

# 以下と同じ
for i to 10
  appendInfoLine: i
endfor
```

# include＋procedureで疑似OOPしたかった...(未完成)

（この項目だけは未完成です）

Praat Scriptにはもちろんインスタンスやメソッドという概念はありません。しかし、procedureが状態を持つことを応用すれば、インスタンスのように使うことができそうです。

とっかかりはいくつか思いついたのですが、苦節五年、残念ながらいまだにうまくいく方法が見つかっていません... :cry: [^trick]
できないものを紹介しても仕方がないのですが、**読者の方が将来OOPの黒魔術を思いつく際の参考になれば**（？）と思い途中経過を公開します。

## procedure名に変数置換を使う

procedure名の探索に失敗するのでうまくいきません。

```praat:失敗例
procedure 'self$': .name$, .age
endproc

@person1: "taro", 21 ; person1 not found
```

どうやらprocedure名は変数置換よりも前に解決されるようです。

## includeのファイル名に変数置換を使う

ならば、読み込み時にprocedure名を書き換えてしまえば良いのでは？と思いましたが、includeは変数置換できませんでした。

```praat
s$ = "foo.praat"
include 's$' ; cannot open 's$'
```

## 実行中にソースコードを書き換える

それなら、 `include` でファイル名を差し込んでみるのはどうでしょう？ `...` を使えば行中でも改行できるので、procedure名の差し替えたい部分だけ改行して、includeで差し替えるようにします。

```praat:person.praat
# ... の行継続を使って、行の途中をincludeで差し替える

# procedure 'self$': .name$, .age
procedure 
include self.praat
...: .name$, .age
endproc

# procedure 'self$'.canDrink:
procedure 
include self.praat
....canDrink:
	.self$ = "
include self.praat
..."
	.return = '.self$'.age >= 20
endproc

# procedure 'self$'.greet:
procedure 
include self.praat
....greet:
	.self$ = "
include self.praat
..."
	appendInfoLine: "I am ", '.self$'.name$, "!"
endproc
```

試しに `self.praat` に適当な名前を入れてみます。

```praat:self.praat
...person1
```

```praat:main.praat
include person.praat

@person1: "taro", 21
@person1.canDrink:
appendInfoLine: person1.canDrink.return ; 1
@person1.greet: ; I am taro!
```

それでは、いよいよ`self.praat` を動的に書き換えてインスタンスを生成...できませんでした。


```praat:main.praat
clearinfo

procedure setSelf: .name$
	.fileName$ = defaultDirectory$ + "\" + "self.praat"
	writeFileLine: .fileName$, "..." + .name$
endproc

self$ = "hanako"
include person.praat

@hanako: "Hanako", 23 ; procedure hanako not found
```

`include` は評価よりも先に行われるので、実行時に `self.praat` を書き換えても手遅れでした。お手上げです :sob:

# おわりに

しょうもないネタにお付き合いいただきありがとうございました。みなさんもぜひPraat Scriptの新たな可能性を開いてみてはいかがでしょうか？（~~なお研究のまじめなコードで使っても責任は負いかねますのでご了承ください~~）
次回はシリーズ最後の記事、UI操作でゲームを作る方法を紹介します！

[^1]: 大半は学生時代、研究に行き詰まった現実逃避で発見したネタです。温かい目で見守ってやってください()
[^single]: 昔の文法なので、導入理由の文献は見つかりませんでした（探せばあるかもしれませんが...）
[^trick]: 人はこれを時間の無駄という。