自作言語にdefer文を実装した（がGoと微妙に挙動が違う）

# TL; DR

- 本家(Go)は関数呼び出しのみ遅延
- 自作言語の実装では式の評価全体を遅延してしまった


# はじめに

Go言語の `defer` 文を使うと、関数を抜ける際に必ず処理を実行することが可能です。
panicした場合も後処理の実行が保証され重宝しています。

```go
file, err := os.Open("file.go")
if err != nil {
    // ...
}
defer f.Close()
```

https://go.dev/ref/spec#Defer_statements

便利なので拙作の言語 [Pangaea](https://github.com/Syuparn/Pangaea) でも `defer` 文を導入しています。

（Pangaea言語自体は過去の記事でも紹介しておりますので、よろしければご覧ください）

https://qiita.com/Syuparn/items/87cafc7fd206016a0f8d

しかし、先日 「Go Quizzes 101」というGoの文法のクイズ[^quiz]を解いている際に **Pangaeaのdeferの実装が間違っている**ことに気づきました[^wrong]。

https://go101.org/quizzes/defer-1.html

そこで本記事では、Pangaeaに`defer`を実装した方法と、本家を再現できていない部分について紹介したいと思います。
まだ想定する実装に修正はできていませんが、反面教師として`defer`を自作言語に導入したい方の参考になれば幸いです。

# deferを実装してみる

defer文に指定した式のASTを保存しておき、関数呼び出しの終了時に評価しています。
構文が似ている `return` 文と同じような実装方法にしました。

## return文の実装

本題に入る前に `return` の実装についてです。「[Go言語でつくるインタプリタ](https://www.amazon.co.jp/Go%E8%A8%80%E8%AA%9E%E3%81%A7%E3%81%A4%E3%81%8F%E3%82%8B%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF-Thorsten-Ball/dp/4873118220)」の設計を参考にしています。

`return` 文を評価する際、式の結果そのものではなく、 `object.ReturnObj` にラップして返しています。そして、関数呼び出し全体を評価している箇所でアンラップしています。
ラップしないと 早期returnがブロックの中にある場合に正しく評価されないためです[^wrap]。

以下のソースコードで、If文は `ReturnObj{0}` として評価されます。そして、関数呼び出し全体はいずれかの文が `ReturnObj` に評価された時点で処理を止め、中身をアンラップして関数呼び出しの評価値とします。

```
# NOTE: 以下は疑似言語。Pangaeaにif文は存在しない
div := {|x, y|
  if y == 0 {
    return 0 # この行の評価時に結果をラップ
  }

  return x / y
}
```

もしラップをしない場合、if文が0と評価され、早期returnが無視されてしまいます。

```
# NOTE: 以下は疑似言語。Pangaeaにif文は存在しない
div := {|x, y|
  if y == 0 {
    return 0 # returnが見つかり、ブロック(=if文)を0と評価。しかし関数全体を脱出できていないので、後続も評価されてしまう...
  }

  return x / y
}
```

## defer文の実装
本題の `defer` 文です。`return` 同様、式を `DeferObj` でラップしています。ラップしたオブジェクトは関数を抜ける際、 `return` or 例外発生 (Pangaeaには大域脱出があります)の後に評価します。

ただし、`defer` 文は関数呼び出しの最後に評価する必要があるため、式は評価せずASTのまま`DeferObj` でラップしています。

```go:evaluator/eval_jumpstmt.go
func evalJumpStmt(node *ast.JumpStmt, env *object.Env) object.PanObject {
	// defer文の式は評価せずASTのまま返す
	if node.JumpType == ast.DeferJump {
		return &object.DeferObj{Node: node.Val}
	}

	// 式を評価
	val := Eval(node.Val, env)
	// ...
	switch node.JumpType {
	case ast.ReturnJump: // 一方、return文の場合は評価した値をラップして返す
		return &object.ReturnObj{PanObject: val}
	// ...
}
```

```go:evaluator/eval_program.go
func evalStmts(stmts []ast.Stmt, env *object.Env) object.PanObject {
	// Stmtsの評価として、文全体を評価した値(=戻り値)とdefer文のAST一覧を返す
	ret, deferObjs := _evalStmts(stmts, env)
	err := evalDefer(deferObjs, env)
	if err != nil {
		return err
	}

	return ret
}

func _evalStmts(
	stmts []ast.Stmt,
	env *object.Env,
) (object.PanObject, []object.DeferObj) {
	// defer文で評価を後回しにしている式のキュー
	deferObjs := []object.DeferObj{}

	for _, stmt := range stmts {
		// 次の行を評価
		val = Eval(stmt, env)

		if err, ok := val.(*object.PanErr); ok {
			// 例外発生時もdeferを直後に評価したいので、deferObjsも返す
			return appendStackTrace(err, stmt.Source()), deferObjs
		}

		// return文の評価。ReturnObjは単純にアンラップ（deferを直後に評価したいので、deferObjsも返す）
		if ret, ok := val.(*object.ReturnObj); ok {
			return ret.PanObject, deferObjs
		}

		// defer文の評価。キューdeferObjsに追加
		if _defer, ok := val.(*object.DeferObj); ok {
			deferObjs = append(deferObjs, *_defer)
		}
	}

	return val, deferObjs
}
```

# Goのdeferの実際の挙動

しかし、上記の実装はGoのdefer文の再現にはなっていませんでした。
deferの実際の挙動は、「関数**呼び出し**の評価をreturn/panicの後まで遅延させる」です。関数の**引数はdeferを定義した時点で評価されます**。

```go:Go
func main() {
	x := 1
	defer fmt.Println(x) // 1
	x = 2
}
```

Pangaeaではdefer文の式まるごと評価を遅らせているため、変数の中身が変わってしまっています。

```pangaea:Pangaea
{
  x := 1
  defer x.p # 2
  x := 2
}()
```

設計をさぼってreturnを流用したのがあだとなりました。ちゃんと実装する場合は以下の手順を踏む必要がありそうです。

- defer文の式のASTを、定義された時点で評価
- 式が関数呼び出し1つになるまで評価出来たら、引数のみ評価し `Stmts` のキューに保存
- Stmtsの評価が終わった時点でキューから関数呼び出しを取り出し評価

# おわりに

以上、`defer`の実装の紹介と、仕様を誤解し挙動が異なってしまった箇所の振り返りでした。
言語を作るのであれば、仕様はコーナーケースまで理解すべきだと痛感しました...うまく改修して「真のdefer」を手に入れたいと思います。

[^quiz]: defer以外にも面白いクイズがたくさんあるので、ゲーム感覚で勉強会の題材にするのにおススメです。
[^wrong]: ~~「これがPangaeaの正しい仕様です！」と言い張ることも可能~~
[^wrap]: とはいえ、現行のPangaeaでは関数オブジェクト以外にスコープを作る方法が無いためラップしなくても問題ありません。あくまで将来の拡張性のための機能です。