LoginSignup
1
0

More than 1 year has passed since last update.

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

Posted at

TL; DR

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

はじめに

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

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

便利なので拙作の言語 Pangaea でも defer 文を導入しています。

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

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

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

deferを実装してみる

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

return文の実装

本題に入る前に return の実装についてです。「Go言語でつくるインタプリタ」の設計を参考にしています。

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

以下のソースコードで、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 でラップしています。

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}
	// ...
}
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
func main() {
	x := 1
	defer fmt.Println(x) // 1
	x = 2
}

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

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

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

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

おわりに

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

  1. defer以外にも面白いクイズがたくさんあるので、ゲーム感覚で勉強会の題材にするのにおススメです。

  2. 「これがPangaeaの正しい仕様です!」と言い張ることも可能

  3. とはいえ、現行のPangaeaでは関数オブジェクト以外にスコープを作る方法が無いためラップしなくても問題ありません。あくまで将来の拡張性のための機能です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0