{"id":18376226,"url":"https://github.com/bunji2/practiceofdsl","last_synced_at":"2025-04-11T04:42:25.700Z","repository":{"id":95709981,"uuid":"248918531","full_name":"bunji2/practiceofdsl","owner":"bunji2","description":"Practice of DSL (described in Japanese)","archived":false,"fork":false,"pushed_at":"2020-04-30T13:54:55.000Z","size":1296,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-11T04:42:22.696Z","etag":null,"topics":["abstract-syntax-tree","ast","domain-specific-language","dsl","go-z3","golang","smt-solver","z3"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bunji2.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-03-21T06:16:39.000Z","updated_at":"2024-05-26T06:47:31.000Z","dependencies_parsed_at":"2023-05-21T17:30:24.814Z","dependency_job_id":null,"html_url":"https://github.com/bunji2/practiceofdsl","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bunji2%2Fpracticeofdsl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bunji2%2Fpracticeofdsl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bunji2%2Fpracticeofdsl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bunji2%2Fpracticeofdsl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bunji2","download_url":"https://codeload.github.com/bunji2/practiceofdsl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248345281,"owners_count":21088242,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["abstract-syntax-tree","ast","domain-specific-language","dsl","go-z3","golang","smt-solver","z3"],"created_at":"2024-11-06T00:22:23.020Z","updated_at":"2025-04-11T04:42:25.679Z","avatar_url":"https://github.com/bunji2.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Practice of DSL\n\nここでは、DSL のデザインをステップ・バイ・ステップで行っていくメモを記していく。\n\n----\n\n## はじめに\n\nDSL とは Domain Specific Language の略であり日本語では「ドメイン固有言語」と言うらしい。\n\n汎用的なプログラミング言語は DSL とは呼ばない。あくまで用途が限定されるような場合に DSL と呼ぶようだ。\n\nここでは、SMT Solver を使うための DSL を考える。\n\nなお、以下では [Z3](https://github.com/Z3Prover/z3) の golang フロントエンドである [go-z3](https://github.com/mitchellh/go-z3) を使うが、Z3 や go-z3 のインストール手順などについてはそれぞれののサイトを参照いただきたい。\n\n## 基本方針\n\nSMT Solver 向けの DSL を考えるにあたって次の方針とする。\n\n* 実装に使用するプログラミング言語は golang とする。\n* 新しく作る DSL の構文仕様は golang の構文仕様と親和性のあるものとする。\n* SMT Solver として \"Z3\" を使うが、フロントエンドは go-binding である \"go-z3\" を使う。\n* \"go-z3\" 以外は、すべて標準パッケージのみを使って実装する。\n* 新たにスクラッチからパーサを実装することはしない。\n\n今回の DSL の利用者は以下を想定する。\n\n* 論理学の知識があり SMT Solver の制約条件の検討ができる。\n* 基本的な IT リテラシはあるものとし、OS などのプラットフォームの基本操作は一通りできる。\n* プログラミングスキルはない。\n* 手順書や業務手順書が与えられれば、未知のシステムでも手順通りに操作する勤勉さはある。\n\n制約式の検討はできてもプログラミングスキルがないことを前提とする。\n一方で、利用者のプログラミングを教育していくのもそれなりにコストが発生するのでそれも避けたい。\n\nこのような条件下で利用者が SMT Solver を使えるよう運用していくには、DSL が適切な抽象レベルでかつ適切なパワーを持つよう工夫することがキモとなる。\n\n----\n\n## ステップ0. 最初のサンプルコード\n\nSMT Solver とは Satisfiable Modulo Theories Solver の略で、一階述語論理式で記述された制約条件を満たす値を解決してくれるシステムである。\n\n例題として、次のような条件式を満たす整数 x と y の解決を考える。\n\n![](https://latex.codecogs.com/gif.latex?x\u0026plus;y=24\\wedge{x-y=2})\n\nこの例を解決するサンプルコードを示す。\n\n```golang\npackage main\n\nimport (\n\t\"fmt\"\n\t\"github.com/mitchellh/go-z3\"\n)\n\nfunc main() {\n\t// コンテクストの作成\n\tconfig := z3.NewConfig()\n\tctx := z3.NewContext(config)\n\tconfig.Close()\n\tdefer ctx.Close()\n\n\t// ソルバーの作成\n\tsolver := ctx.NewSolver()\n\tdefer solver.Close()\n\n\t// 制約変数の定義\n\tx := ctx.Const(ctx.Symbol(\"x\"), ctx.IntSort())\n\ty := ctx.Const(ctx.Symbol(\"y\"), ctx.IntSort())\n\n\t// 制約条件\n\t// x + y = 24\n\tsolver.Assert(x.Add(y).Eq(ctx.Int(24, ctx.IntSort())))\n\t// x - y = 2\n\tsolver.Assert(x.Sub(y).Eq(ctx.Int(2, ctx.IntSort())))\n\n\t// 解決可能化チェック\n\tif v := solver.Check(); v != z3.True {\n\t\tfmt.Println(\"解決不能\")\n\t\treturn\n\t}\n\n\t// 結果の表示\n\tm := solver.Model()\n\tvalues := m.Assignments()\n\tm.Close()\n\tfmt.Printf(\"x = %s\\n\", values[\"x\"])\n\tfmt.Printf(\"y = %s\\n\", values[\"y\"])\n}\n```\n\n実行結果：\n\n```\n% go run sample.go \nx = 13\ny = 11\n```\n\n実行すると制約条件を満たす整数 x と y の値が表示される。\n\n実際はこの例題のように一次方程式を解くような単純なものだけでなく、もっと複雑な制約条件を扱うわけだが今回は省略する。\n\nさて、利用者の立場で考えてみると、上のサンプルコードのうち SMT Solver に渡す制約条件の記述については興味はあるが、専門外のライブラリのインポート・コンテクストやソルバーの作成・制約変数の定義など、SMT Solver を動かすためのコードの記述は意味不明であり、煩わしいだけだ。\n\n![fig01.png](fig/fig01.png)\n\n以降は上のサンプルコードをステップ0 として、ステップ・バイ・ステップで DSL 化を勧めていくことにする。\n\nまずは、上の雑多になっているコードをライブラリ化し整理することで、どれだけ記述が簡単になるかをみてみる。\n\n\n----\n\n## ステップ1. ライブラリ化\n\nステップ1 のサンプルコード sample1.go を示す。\n\n```\npackage main\n\nfunc main() {\n\t// コンテクストとソルバーの作成\n\tc := NewContext()\n\tdefer c.Close()\n\n\t// 制約変数\n\tx := c.IntVar(\"x\")\n\ty := c.IntVar(\"y\")\n\n\t// 制約条件\n\t// x + y = 24\n\tc.Assert(x.Add(y).Eq(c.IntVal(24)))\n\t// x - y = 2\n\tc.Assert(x.Sub(y).Eq(c.IntVal(2)))\n\n\t// 解決結果の表示\n\tc.Solve(\"x\", \"y\")\n}\n```\n\n前ステップのコードからの変更を示す。\n\n![fig02.png](fig/fig02.png)\n\n次のように実行する。\n\n```\n% go run sample1.go lib.go\nx = 13\ny = 11\n```\n\n作成したライブラリ [lib.go](lib.go) のポイントは下の通りである。\n\n* z3 のコンテクストやソルバーをメンバーとしてもつ構造体型 Context を導入する。\n* Context 型のメソッドとして、変数定義などのサンプルコードで使用する関数を実装する。\n* go-z3 への依存性をすべて lib.go に寄せることにより、サンプルコードからの go-z3 のインポートを不要にする。\n\n\nステップ1 はステップ0 よりも、ライブラリ化によって利用者が記述するコード量は減少した。\nしかしそれでも golang 特有のパッケージ宣言や main 関数の宣言など、毎回同じ内容を記述するのは無駄が多い。\nまた、利用者の入力ミスなどのおそれもあるのでなくしてしまいたい。\n\n![fig04](fig/fig04.png)\n\n----\n\n## ステップ2. 差分テキスト化\n\nステップ2 のサンプルコード sample2.txt を示す。\n\n```\n// 制約変数\nx := c.IntVar(\"x\")\ny := c.IntVar(\"y\")\n\n// 制約条件\n// x + y = 24\nc.Assert(x.Add(y).Eq(c.IntVal(24)))\n// x - y = 2\nc.Assert(x.Sub(y).Eq(c.IntVal(2)))\n\n// 解決結果の表示\nc.Solve(\"x\", \"y\")\n```\n\n前ステップのコードからの変更を示す。\n\n![fig05](fig/fig05.png)\n\n次のように実行する。\n\n```\n% run.sh sample2.txt\nx = 13\ny = 11\n```\n\n使用するシェルスクリプト run.sh 内の処理は次の通り。\n\n![fig06](fig/fig06.png)\n\nrun.sh の中で golang 特有のパッケージ宣言や main 関数の宣言を補完し、go run コマンドで実行する。\n\n具体的な実装は次の通りである。\n\n```\n#!/bin/sh\n\nfilename=`basename $1 .txt`$$.go\n\n(\n    echo \"package main\"\n    echo \"func main() {\"\n    echo \"c := NewContext()\"\n    echo \"defer c.Close()\"\n    cat $1\n    echo \"}\"\n) \u003e $filename\n\ngo run $filename lib.go\n\nrm $filename\n```\n\nステップ2 では差分テキスト化することによって golang 特有のパッケージ宣言や main 関数の宣言などがなくなり、利用者の記述量はさらに減少した。\n\nしかし差分テキスト化の副作用として、各関数のプレフィクスの \"c.\" がもはや意味をなさなくなってしまった。\n無駄な上に、これもまた利用者が入力ミスをおかすおそれがあるのでなくしてしまいたい。\n\n![fig07](fig/fig07.png)\n\n----\n\n## ステップ3. ライブラリ化その２\n\nステップ3 のサンプルコード sample3.txt を示す。\n\n```\n// 制約変数\nx := IntVar(\"x\")\ny := IntVar(\"y\")\n\n// 制約条件\n// x + y = 24\nAssert(x.Add(y).Eq(IntVal(24)))\n// x - y = 2\nAssert(x.Sub(y).Eq(IntVal(2)))\n\n// 解決結果の表示\nSolve(\"x\", \"y\")\n```\n\n前ステップのコードからの変更を示す。\n\n![fig08](fig/fig08.png)\n\n実行はステップ2 と同じである。\n\n```\n% run.sh sample3.txt\nx = 13\ny = 11\n```\n\nrun.sh 內部の処理は次の通り。\n\n![fig09](fig/fig09.png)\n\nステップ2 との run.sh の差分は、ライブラリ [lib2.go](lib2.go) が増えたことである。\n追加したライブラリにより、コンテクスト変数のグローバル化を行なった。\n\n```\n#!/bin/sh\n\nfilename=`basename $1 .txt`$$.go\n\n(\n    echo \"package main\"\n    echo \"func main() {\"\n    echo \"ccc = NewContext()\"\n    echo \"defer ccc.Close()\"\n    cat $1\n    echo \"}\"\n) \u003e $filename\n\ngo run $filename lib.go lib2.go\n\nrm $filename\n```\n\n無駄な \"c.\" プレフィクスがなくなって、利用者の記述量はまた低減された。\n\nしかしそれでも、制約条件が直感的ではないという問題が残っている。\n式が複雑になってくると、またしても利用者が入力ミスをおかすおそれがある。\n特に括弧を適切に対応させねばならないのは面倒かもしれない。\n\n利用者としては「数学的な条件式」を使いたい。\n\nこの他、制約変数で使っている文字列表記は冗長なので単純化したい。\n\n![fig10](fig/fig10.png)\n\n----\n\n## ステップ4. 制約条件の数式化\n\nステップ4 のサンプルコード sample4.txt を示す。\n\n```golang\n// 制約変数\nvar x, y Int\n\n// 制約条件\nAssert(x + y == 24)\nAssert(x - y == 2)\n\n// 解決結果の表示\nSolve(x, y)\n```\n\n前ステップのコードからの変更を示す。\n\n![fig11](fig/fig11.png)\n\n上のような変更を行なうには Assert 関数の引数の式の構造を自動的に変換する必要があるが、\nライブラリ化や差分テキスト化だけでは対応できない。\n\n今回は Assert 関数の引数の式の \"AST\" を加工することで対応した。\n\n\"AST\" とは \"Abstract Syntax Tree\" の略であり、日本語では「抽象構文木」と呼ばれる。\nやや端折って簡単に言い切ってしまうと、ソースコードの構文に対応する木構造のことである。\n\nAST の例を以下に示す。\n\n![AST](fig/fig_ast.png)\n\n\nシェルスクリプト run.sh 內部の処理を示す。\n\n![fig12](fig/fig12.png)\n\n\n```\n#!/bin/sh\n\nfilename=`basename $1 .txt`.go\n\nconv $1 $filename\n\ngo run $filename lib.go lib2.go\n\nrm $filename\n```\n\nconv コマンドの中で変換している AST の前後を以下に示す。\n\n![fig1](fig1.png)\n\n(図中の同じ色の箇所は前後で対応する箇所)\n\n\n以下、conv コマンドにおけるコードを抜粋して処理を解説する。\n\n### 差分テキストの補完処理とパージング処理\n\n差分テキストの補完処理は単に入力コードの前後に補完コードの文字列を連結するだけである。\nパージング処理には go の標準パッケージである go/parser パッケージを利用する。\nこのようにしてパージング処理の結果、上の例では fileNode 変数に AST が格納される。\n\n```golang\n// 入力コードの読み出し\nsrc := readSrc(os.Args[1])\n\n// 差分テキストの補完処理。入力コードの前後に補完コードを連結\nsrc = `package main\nfunc main() {\nccc = NewContext()\ndefer ccc.Close()` + src + \"}\"\n\n// Golang のソースコードとしてパースし、AST を取得\nfileNode, err := parser.ParseFile(fset, \"\", src, 0)\n```\n\n### 変換箇所の特定\n\n各ステートメントの中から Assert 関数のステートメントをみつけ、変換の対象となる式を特定する。\n\n```golang\n// 各ステートメントの処理\nfor i, stmt := range stmts {\n\tswitch stmt.(type) {\n\t...\n\tcase *ast.ExprStmt: // 式のステートメント\n\t\tes := stmt.(*ast.ExprStmt)\n\t\tif isAssert(es.X) { // \"Assert\" 関数のとき\n\t\t\tce := es.X.(*ast.CallExpr)\n\t\t\t// 第一引数を書き換え\n\t\t\tce.Args[0] = convExpr(ce.Args[0])\n```\n\n### 式のASTの変換\n\n「式」には、二項演算式、単項演算式、関数呼び出し、など、複数のケースがあるため、\nそれぞれのケースに応じて switch 文で分岐しながら AST の木構造をトラバースしていく。\nただし変換の必要のない式もあることに注意。\n\n```golang\n// convExpr は Assert 関数の引数で指定された式のASTを変換する関数\nfunc convExpr(expr ast.Expr) (r ast.Expr) {\n\tswitch expr.(type) {\n\tcase *ast.BinaryExpr:\n\t\tr = convBinaryExpr(expr.(*ast.BinaryExpr))\t// 二項演算式の変換\n\tcase *ast.UnaryExpr:\n\t\tr = convUnaryExpr(expr.(*ast.UnaryExpr))\t// 単項演算式の変換\n\tcase *ast.CallExpr:\n\t\tr = convCallExpr(expr.(*ast.CallExpr))\t\t// 関数呼び出し式の変換\n\tcase *ast.ParenExpr:\n\t\tr = convExpr(expr.(*ast.ParenExpr).X)\t\t// 括弧で囲まれた式の変換\n\tcase *ast.Ident:\n\t\tr = convIdent(expr.(*ast.Ident))\t\t// 識別子からなる式の変換\n\tcase *ast.BasicLit:\n\t\tr = convBasicLit(expr.(*ast.BasicLit))\t\t// 整数などのリテラルからなる式の変換\n\tdefault:\n\t\t// 上記以外は変換しない。\n\t\tr = expr\n\t}\n\treturn\n}\n```\n\n以下は単項演算式（Not）の例である。\n入力の AST ノードに対して、対応する AST ノードを生成しリターンすることで変換している。\nここでも Not の対象となる式を再帰的に変換している。\n\n```golang\n// convUnaryExpr は単行演算式を変換する関数\nfunc convUnaryExpr(expr *ast.UnaryExpr) (r ast.Expr) {\n\tif expr.Op != token.NOT {\n\t\tr = expr\n\t\treturn\n\t}\n\tr = \u0026ast.CallExpr{\t// 対応する AST ノードの生成\n\t\tFun: \u0026ast.SelectorExpr{\n\t\t\tX:   convExpr(expr.X), // NOT演算子の引数の変換（再帰的な変換）\n\t\t\tSel: ast.NewIdent(\"Not\"),\n\t\t},\n\t}\n\treturn\n}\n```\n\n\n上の Assert 関数の引数の変換の他に、次の変数宣言と Solve 関数の引数の変換も行なう。\n\n### 変数宣言の変換\n\n制約変数の変数宣言では通常の \"int\" の変数宣言と区別がつくよう、\"Int\" という型で表現する。\n制約変数の宣言は IntVar 関数を用いた表現に変換することでコンパイルできるようにする。\n\n```\n// 変換前\nvar x, y Int\n// 変換後\nx, y := IntVar(\"x\"), IntVar(\"y\")\n```\n\n変換前後の AST を示す。\n\n![AST2](fig/AST2.png)\n\n### Solve関数の引数の変換\n\n表示する制約変数の指定時に引用符をつけなくてもよいようにしたので、Solve 関数の引数に並ぶ変数名を文字列に変換する。\n\n```\n// 変換前\nSolve(x, y)\n// 変換後\nSolve(\"x\", \"y\")|\n```\n\n変換前後の AST を示す。\n\n![AST3](fig/AST3.png)\n\n\n## おわりに\n\n* 今回は SMT Solver 用の DSL のデザインをステップ・バイ・ステップで行なった。\n* 最後の STEP4 のサンプルコードの行数・バイト数を STEP0 と比べると、それぞれ 20%・15% まで低減された。\n* 制約条件式も数式表現が使えるので入力ミスをある程度は防ぐことができる。\n* 今回のアプローチではパーサの実装は不要となったのはよかったが、式のパターンが多いため AST の変換処理はやや面倒なものとなった。\n* 実際のコーディング作業にはそれでも 3 時間くらい要した。\n* DSL そのものについて利用者から現時点で不満は出ていない。\n\n今後について\n\n* 今回使用した go-z3 の能力不足な面が明らかになってきた。\n - 配列を扱えない、\n - Z3 の API をすべて実装していない、など\n* 別の SMT Solver のパッケージへの移行を検討中\n\n----\n\n## サンプル\n\n今回デザインした DSL のサンプルを示す。\n\n### 虫食い計算\n\ngoogle の入社試験を解いてみる。\n\n```\n// mushikui.txt\n\n//   WWWDOT\n// - GOOGLE\n// --------\n//   DOTCOM\n\n// 各アルファベットは整数\nvar W, D, O, T, G, L, E, C, M Int\n\n// 各アルファベットは一意な数字\nAssert(Distinct(W, D, O, T, G, L, E, C, M))\n\n// 各アルファベットは一桁の整数（0以上10未満）\n// かつ、先頭の W, G, D は 0 以外\nAssert(W\u003e=1 \u0026\u0026 W\u003c10)\nAssert(D\u003e=1 \u0026\u0026 D\u003c10)\nAssert(O\u003e=0 \u0026\u0026 O\u003c10)\nAssert(T\u003e=0 \u0026\u0026 T\u003c10)\nAssert(G\u003e=1 \u0026\u0026 G\u003c10)\nAssert(L\u003e=0 \u0026\u0026 L\u003c10)\nAssert(E\u003e=0 \u0026\u0026 E\u003c10)\nAssert(C\u003e=0 \u0026\u0026 C\u003c10)\nAssert(M\u003e=0 \u0026\u0026 M\u003c10)\n\n// ボロウ b1～b5 は筆算で上の位から借りてくる値\n//   W  W  W  D  O  T\n//   b5 b4 b3 b2 b1\n// - G  O  O  G  L  E\n// --------------\n//   D  O  T  C  O  M\n\nvar b1, b2, b3, b4, b5 Int\n\n// ボロウは 0 または 1\nAssert(b1 == 0 || b1 == 1)\nAssert(b2 == 0 || b2 == 1)\nAssert(b3 == 0 || b3 == 1)\nAssert(b4 == 0 || b4 == 1)\nAssert(b5 == 0 || b5 == 1)\n\n// 筆算（各桁ごとの関係）\nAssert(T + b1*10 -E     == M)\nAssert(O + b2*10 -L -b1 == O)\nAssert(D + b3*10 -G -b2 == C)\nAssert(W + b4*10 -O -b3 == T)\nAssert(W + b5*10 -O -b4 == O)\nAssert(W         -G -b5 == D)\n\nSolve(W, D, O, T, G, L, E, C, M)\n```\n\n実行結果。\n\n```\nC:\\work\u003esh run.sh mushikui.txt\nW = 7.\nD = 5.\nO = 8.\nT = 9.\nG = 1.\nL = 0.\nE = 3.\nC = 4.\nM = 6.\n```\n\n----\n\n# 付録: [配列対応](array/README.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbunji2%2Fpracticeofdsl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbunji2%2Fpracticeofdsl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbunji2%2Fpracticeofdsl/lists"}