Repository: Uni-Gal/Ayaka (language: rust)

Issue:
New proposal about `exec` and `switches`
## 解耦合 `exec`

将 `exec` 做成 line plugin。允许一个插件调用另一个插件（ #14 ）。

## 简化 `switches`

不再允许每个选项后面给出代码，这样能够解决 #24 。

每个选项默认绑定其顺序对应的数字名称变量，默认值为 `true`；被选择的选项将进入 `$?`。例如：

``` yaml
- exec: $1 = false
- switches:
  - Switch 1
  - Switch 2
  - Switch 3
- exec: $n = $?
```

在上述示例中，第一、三个选项可用而第二个选项不可用；`$n` 的值可能取0或2。

有关默认值的考虑：
* 如果设定成 `false` 可用而 `true` 不可用，太扭曲了。
* 但是一个 `bool` 值默认是 `true` 也很扭曲。
* 因此决定，如果这个变量是 `null` 的时候为 `true`，其它情况与正常变量保持相同。这并不构成 Ayaka Script 的例外规则，而是 `switches` 的特殊规则。


Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 06a3b650..1796543d 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ release:
 release-cross:
 	cd bins && $(MAKE) release-cross TARGET=$(TARGET)
 
-EXAMPLES:=Fibonacci Fibonacci2 Gacha Live2D Orga Styles
+EXAMPLES:=Basic Fibonacci Fibonacci2 Gacha Live2D Orga Styles
 
 define example-tpl
 .PHONY: example-$(1) example-$(1)-gui example-$(1)-release example-$(1)-gui-release examples/$(1)/config.tex
diff --git a/bins/Cargo.lock b/bins/Cargo.lock
index 981d0175..08ff4883 100644
--- a/bins/Cargo.lock
+++ b/bins/Cargo.lock
@@ -204,6 +204,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "addr2line"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+dependencies = [
+ "gimli",
+]
+
 [[package]]
 name = "adler"
 version = "1.0.2"
@@ -276,13 +285,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
 
 [[package]]
-name = "ascii-canvas"
-version = "3.0.0"
+name = "arrayvec"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
-dependencies = [
- "term",
-]
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
 
 [[package]]
 name = "askama_escape"
@@ -346,7 +352,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 name = "ayaka-bindings-types"
 version = "0.1.0"
 dependencies = [
- "ayaka-script",
+ "ayaka-primitive",
  "fallback",
  "log",
  "serde",
@@ -406,13 +412,21 @@ dependencies = [
 ]
 
 [[package]]
-name = "ayaka-plugin-wasmi"
+name = "ayaka-plugin-wasmtime"
 version = "0.1.0"
 dependencies = [
  "ayaka-plugin",
- "scopeguard",
- "wasmi",
- "wasmi_wasi",
+ "wasmtime",
+ "wasmtime-wasi",
+]
+
+[[package]]
+name = "ayaka-primitive"
+version = "0.1.0"
+dependencies = [
+ "regex",
+ "serde",
+ "serde_with 2.1.0",
 ]
 
 [[package]]
@@ -423,14 +437,15 @@ dependencies = [
  "ayaka-bindings-types",
  "ayaka-plugin",
  "ayaka-plugin-nop",
- "ayaka-plugin-wasmi",
- "ayaka-script",
+ "ayaka-plugin-wasmtime",
+ "ayaka-primitive",
  "cfg-if",
  "dirs",
  "fallback",
  "futures-util",
  "icu_locid",
  "language-matcher",
+ "log",
  "serde",
  "serde_json",
  "serde_yaml",
@@ -440,20 +455,6 @@ dependencies = [
  "trylog",
 ]
 
-[[package]]
-name = "ayaka-script"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "lalrpop",
- "lalrpop-util",
- "log",
- "regex",
- "serde",
- "serde_with 2.1.0",
- "trylog",
-]
-
 [[package]]
 name = "base64"
 version = "0.13.1"
@@ -469,21 +470,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "bit-set"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
-dependencies = [
- "bit-vec",
-]
-
-[[package]]
-name = "bit-vec"
-version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
-
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -496,6 +482,15 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.3"
@@ -911,6 +906,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "cpp_demangle"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.5"
@@ -920,6 +924,104 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "cranelift-bforest"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "593b398dd0c5b1e2e3a9c3dae8584e287894ea84e361949ad506376e99196265"
+dependencies = [
+ "cranelift-entity",
+]
+
+[[package]]
+name = "cranelift-codegen"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afc0d8faabd099ea15ab33d49d150e5572c04cfeb95d675fd41286739b754629"
+dependencies = [
+ "arrayvec",
+ "bumpalo",
+ "cranelift-bforest",
+ "cranelift-codegen-meta",
+ "cranelift-codegen-shared",
+ "cranelift-entity",
+ "cranelift-isle",
+ "gimli",
+ "log",
+ "regalloc2",
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-codegen-meta"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac1669e42579476f001571d6ba4b825fac686282c97b88b18f8e34242066a81"
+dependencies = [
+ "cranelift-codegen-shared",
+]
+
+[[package]]
+name = "cranelift-codegen-shared"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2a1b1eef9640ab72c1e7b583ac678083855a509da34b4b4378bd99954127c20"
+
+[[package]]
+name = "cranelift-entity"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea4e17c3791fd8134640b26242a9ddbd7c67db78f0bad98cb778bf563ef81a0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cranelift-frontend"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fca1474b5302348799656d43a40eacd716a3b46169405a3af812832c9edf77b4"
+dependencies = [
+ "cranelift-codegen",
+ "log",
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-isle"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77aa537f020ea43483100153278e7215d41695bdcef9eea6642d122675f64249"
+
+[[package]]
+name = "cranelift-native"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bdc6b65241a95b7d8eafbf4e114c082e49b80162a2dcd9c6bcc5989c3310c9e"
+dependencies = [
+ "cranelift-codegen",
+ "libc",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-wasm"
+version = "0.89.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb6359f606a1c80ccaa04fae9dbbb504615ec7a49b6c212b341080fff7a65dd"
+dependencies = [
+ "cranelift-codegen",
+ "cranelift-entity",
+ "cranelift-frontend",
+ "itertools",
+ "log",
+ "smallvec",
+ "wasmparser",
+ "wasmtime-types",
+]
+
 [[package]]
 name = "crc32fast"
 version = "1.3.2"
@@ -940,19 +1042,37 @@ dependencies = [
 ]
 
 [[package]]
-name = "crossbeam-utils"
-version = "0.8.14"
+name = "crossbeam-deque"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
 dependencies = [
  "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
 ]
 
 [[package]]
-name = "crunchy"
-version = "0.2.2"
+name = "crossbeam-epoch"
+version = "0.9.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset 0.7.1",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+dependencies = [
+ "cfg-if",
+]
 
 [[package]]
 name = "crypto-common"
@@ -1135,10 +1255,13 @@ dependencies = [
 ]
 
 [[package]]
-name = "diff"
-version = "0.1.13"
+name = "digest"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
 
 [[package]]
 name = "digest"
@@ -1146,10 +1269,20 @@ version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.10.3",
  "crypto-common",
 ]
 
+[[package]]
+name = "directories-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
 [[package]]
 name = "dirs"
 version = "4.0.0"
@@ -1208,12 +1341,6 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "downcast-rs"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
-
 [[package]]
 name = "dtoa"
 version = "0.4.8"
@@ -1248,21 +1375,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
 
 [[package]]
-name = "ena"
-version = "0.14.0"
+name = "encoding_rs"
+version = "0.8.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
 dependencies = [
- "log",
+ "cfg-if",
 ]
 
 [[package]]
-name = "encoding_rs"
-version = "0.8.31"
+name = "env_logger"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
 dependencies = [
- "cfg-if",
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
 ]
 
 [[package]]
@@ -1306,6 +1437,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+
 [[package]]
 name = "fastrand"
 version = "1.8.0"
@@ -1321,10 +1458,20 @@ version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
 dependencies = [
- "memoffset",
+ "memoffset 0.6.5",
  "rustc_version 0.3.3",
 ]
 
+[[package]]
+name = "file-per-thread-logger"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21e16290574b39ee41c71aeb90ae960c504ebaf1e2a1c87bd52aa56ed6e1a02f"
+dependencies = [
+ "env_logger",
+ "log",
+]
+
 [[package]]
 name = "filetime"
 version = "0.2.18"
@@ -1337,12 +1484,6 @@ dependencies = [
  "windows-sys 0.42.0",
 ]
 
-[[package]]
-name = "fixedbitset"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
-
 [[package]]
 name = "flate2"
 version = "1.0.25"
@@ -1615,6 +1756,17 @@ dependencies = [
  "wasi 0.11.0+wasi-snapshot-preview1",
 ]
 
+[[package]]
+name = "gimli"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+dependencies = [
+ "fallible-iterator",
+ "indexmap",
+ "stable_deref_trait",
+]
+
 [[package]]
 name = "gio"
 version = "0.15.12"
@@ -1799,6 +1951,9 @@ name = "hashbrown"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
 
 [[package]]
 name = "heck"
@@ -1882,6 +2037,12 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
 [[package]]
 name = "iana-time-zone"
 version = "0.1.53"
@@ -2045,12 +2206,6 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "indexmap-nostd"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590"
-
 [[package]]
 name = "infer"
 version = "0.7.0"
@@ -2125,7 +2280,7 @@ checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
 dependencies = [
  "hermit-abi 0.2.6",
  "io-lifetimes 1.0.3",
- "rustix 0.36.4",
+ "rustix 0.36.5",
  "windows-sys 0.42.0",
 ]
 
@@ -2150,6 +2305,26 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
 
+[[package]]
+name = "ittapi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8c4f6ff06169ce7048dac5150b1501c7e3716a929721aeb06b87e51a43e42f4"
+dependencies = [
+ "anyhow",
+ "ittapi-sys",
+ "log",
+]
+
+[[package]]
+name = "ittapi-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e078cce01485f418bae3beb34dd604aaedf2065502853c7da17fbce8e64eda"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "javascriptcore-rs"
 version = "0.16.0"
@@ -2213,9 +2388,9 @@ dependencies = [
 
 [[package]]
 name = "json-patch"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f995a3c8f2bc3dd52a18a583e90f9ec109c047fa1603a853e46bcda14d2e279d"
+checksum = "eb3fa5a61630976fc4c353c70297f2e93f1930e3ccee574d59d618ccbd5154ce"
 dependencies = [
  "serde",
  "serde_json",
@@ -2234,38 +2409,6 @@ dependencies = [
  "selectors",
 ]
 
-[[package]]
-name = "lalrpop"
-version = "0.19.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b30455341b0e18f276fa64540aff54deafb54c589de6aca68659c63dd2d5d823"
-dependencies = [
- "ascii-canvas",
- "atty",
- "bit-set",
- "diff",
- "ena",
- "itertools",
- "lalrpop-util",
- "petgraph",
- "pico-args",
- "regex",
- "regex-syntax",
- "string_cache",
- "term",
- "tiny-keccak",
- "unicode-xid",
-]
-
-[[package]]
-name = "lalrpop-util"
-version = "0.19.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcf796c978e9b4d983414f4caedc9273aa33ee214c5b887bd55fde84c85d2dc4"
-dependencies = [
- "regex",
-]
-
 [[package]]
 name = "language-matcher"
 version = "0.1.2"
@@ -2304,12 +2447,6 @@ version = "0.2.138"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
 
-[[package]]
-name = "libm"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
-
 [[package]]
 name = "line-wrap"
 version = "0.1.1"
@@ -2405,6 +2542,15 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
 
+[[package]]
+name = "mach"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "malloc_buf"
 version = "0.0.6"
@@ -2455,6 +2601,15 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
+[[package]]
+name = "memfd"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b20a59d985586e4a5aef64564ac77299f8586d8be6cf9106a5a40207e8908efb"
+dependencies = [
+ "rustix 0.36.5",
+]
+
 [[package]]
 name = "memoffset"
 version = "0.6.5"
@@ -2464,6 +2619,15 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "memoffset"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "mime"
 version = "0.3.16"
@@ -2640,12 +2804,30 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "object"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "crc32fast",
+ "hashbrown",
+ "indexmap",
+ "memchr",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
 
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
 [[package]]
 name = "os_str_bytes"
 version = "6.4.1"
@@ -2728,16 +2910,6 @@ dependencies = [
  "ucd-trie",
 ]
 
-[[package]]
-name = "petgraph"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143"
-dependencies = [
- "fixedbitset",
- "indexmap",
-]
-
 [[package]]
 name = "phf"
 version = "0.8.0"
@@ -2836,12 +3008,6 @@ dependencies = [
  "siphasher",
 ]
 
-[[package]]
-name = "pico-args"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
-
 [[package]]
 name = "pin-project"
 version = "1.0.12"
@@ -2978,6 +3144,15 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "psm"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.21"
@@ -3077,6 +3252,29 @@ dependencies = [
  "cty",
 ]
 
+[[package]]
+name = "rayon"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b"
+dependencies = [
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.16"
@@ -3097,6 +3295,18 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "regalloc2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91b2eab54204ea0117fe9a060537e0b07a4e72f7c7d182361ecc346cab2240e5"
+dependencies = [
+ "fxhash",
+ "log",
+ "slice-group-by",
+ "smallvec",
+]
+
 [[package]]
 name = "regex"
 version = "1.7.0"
@@ -3154,6 +3364,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "rustc-demangle"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
+
 [[package]]
 name = "rustc_version"
 version = "0.3.3"
@@ -3190,9 +3406,9 @@ dependencies = [
 
 [[package]]
 name = "rustix"
-version = "0.36.4"
+version = "0.36.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23"
+checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588"
 dependencies = [
  "bitflags",
  "errno",
@@ -3296,9 +3512,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.148"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
+checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055"
 dependencies = [
  "serde_derive",
 ]
@@ -3317,9 +3533,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.148"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
+checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3463,7 +3679,20 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
 dependencies = [
  "cfg-if",
  "cpufeatures",
- "digest",
+ "digest 0.10.6",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
 ]
 
 [[package]]
@@ -3474,7 +3703,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
 dependencies = [
  "cfg-if",
  "cpufeatures",
- "digest",
+ "digest 0.10.6",
 ]
 
 [[package]]
@@ -3519,6 +3748,12 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "slice-group-by"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec"
+
 [[package]]
 name = "smallvec"
 version = "1.10.0"
@@ -3563,12 +3798,6 @@ dependencies = [
  "system-deps 5.0.0",
 ]
 
-[[package]]
-name = "spin"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
-
 [[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
@@ -3773,6 +4002,12 @@ dependencies = [
  "xattr",
 ]
 
+[[package]]
+name = "target-lexicon"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d"
+
 [[package]]
 name = "tauri"
 version = "1.2.1"
@@ -3852,7 +4087,7 @@ dependencies = [
  "semver 1.0.14",
  "serde",
  "serde_json",
- "sha2",
+ "sha2 0.10.6",
  "tauri-utils",
  "thiserror",
  "time",
@@ -3979,17 +4214,6 @@ dependencies = [
  "utf-8",
 ]
 
-[[package]]
-name = "term"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
-dependencies = [
- "dirs-next",
- "rustversion",
- "winapi",
-]
-
 [[package]]
 name = "termcolor"
 version = "1.1.3"
@@ -4067,15 +4291,6 @@ dependencies = [
  "time-core",
 ]
 
-[[package]]
-name = "tiny-keccak"
-version = "2.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
-dependencies = [
- "crunchy",
-]
-
 [[package]]
 name = "tinystr"
 version = "0.7.0"
@@ -4104,9 +4319,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.22.0"
+version = "1.23.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
+checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
 dependencies = [
  "autocfg",
  "bytes",
@@ -4119,7 +4334,7 @@ dependencies = [
  "signal-hook-registry",
  "socket2",
  "tokio-macros",
- "winapi",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -4235,18 +4450,18 @@ source = "git+https://github.com/Pernosco/tryiterator.git#859bbd68c67f64fc6e6233
 
 [[package]]
 name = "trylog"
-version = "0.2.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f693ba1b7784211afaca884e0dffe0c6086a1113c44ee7030f3277bcd63fde2a"
+checksum = "5c3c554f1b36be2f3c2233dafea1bbaf3e5d07d3b81db54d62606d31805ac7b1"
 dependencies = [
  "log",
 ]
 
 [[package]]
 name = "typenum"
-version = "1.15.0"
+version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
 
 [[package]]
 name = "ucd-trie"
@@ -4485,58 +4700,252 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
 
 [[package]]
-name = "wasmi"
+name = "wasm-encoder"
 version = "0.20.0"
-source = "git+https://github.com/paritytech/wasmi.git#532d96861313d7529e18ce8c293dbeb65b1fa521"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05632e0a66a6ed8cca593c24223aabd6262f256c3693ad9822c315285f010614"
 dependencies = [
- "spin",
- "wasmi_arena",
- "wasmi_core",
- "wasmparser-nostd",
+ "leb128",
 ]
 
 [[package]]
-name = "wasmi_arena"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/wasmi.git#532d96861313d7529e18ce8c293dbeb65b1fa521"
+name = "wasmparser"
+version = "0.92.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da34cec2a8c23db906cdf8b26e988d7a7f0d549eb5d51299129647af61a1b37"
+dependencies = [
+ "indexmap",
+]
 
 [[package]]
-name = "wasmi_core"
-version = "0.5.0"
-source = "git+https://github.com/paritytech/wasmi.git#532d96861313d7529e18ce8c293dbeb65b1fa521"
+name = "wasmtime"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743d37c265fa134a76de653c7e66be22590eaccd03da13cee99f3ac7a59cb826"
 dependencies = [
- "downcast-rs",
- "libm",
- "num-traits",
+ "anyhow",
+ "async-trait",
+ "bincode",
+ "cfg-if",
+ "indexmap",
+ "libc",
+ "log",
+ "object",
+ "once_cell",
+ "paste",
+ "psm",
+ "rayon",
+ "serde",
+ "target-lexicon",
+ "wasmparser",
+ "wasmtime-cache",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+ "wasmtime-fiber",
+ "wasmtime-jit",
+ "wasmtime-runtime",
+ "wat",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
-name = "wasmi_wasi"
-version = "0.1.0"
-source = "git+https://github.com/paritytech/wasmi.git#532d96861313d7529e18ce8c293dbeb65b1fa521"
+name = "wasmtime-asm-macros"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de327cf46d5218315957138131ed904621e6f99018aa2da508c0dcf0c65f1bf2"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "wasmtime-cache"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42bd53d27df1076100519b680b45d8209aed62b4bbaf0913732810cb216f7b2b"
+dependencies = [
+ "anyhow",
+ "base64",
+ "bincode",
+ "directories-next",
+ "file-per-thread-logger",
+ "log",
+ "rustix 0.35.13",
+ "serde",
+ "sha2 0.9.9",
+ "toml",
+ "windows-sys 0.36.1",
+ "zstd",
+]
+
+[[package]]
+name = "wasmtime-cranelift"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "017c3605ccce867b3ba7f71d95e5652acc22b9dc2971ad6a6f9df4a8d7af2648"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen",
+ "cranelift-entity",
+ "cranelift-frontend",
+ "cranelift-native",
+ "cranelift-wasm",
+ "gimli",
+ "log",
+ "object",
+ "target-lexicon",
+ "thiserror",
+ "wasmparser",
+ "wasmtime-environ",
+]
+
+[[package]]
+name = "wasmtime-environ"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aec5c1f81aab9bb35997113c171b6bb9093afc90e3757c55e0c08dc9ac612e4"
+dependencies = [
+ "anyhow",
+ "cranelift-entity",
+ "gimli",
+ "indexmap",
+ "log",
+ "object",
+ "serde",
+ "target-lexicon",
+ "thiserror",
+ "wasmparser",
+ "wasmtime-types",
+]
+
+[[package]]
+name = "wasmtime-fiber"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1075aa43857086ef89afbe87602fe2dae98ad212582e722b6d3d2676bb5ee141"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "rustix 0.35.13",
+ "wasmtime-asm-macros",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "wasmtime-jit"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c683893dbba3986aa71582a5332b87157fb95d34098de2e5f077c7f078726d"
+dependencies = [
+ "addr2line",
+ "anyhow",
+ "bincode",
+ "cfg-if",
+ "cpp_demangle",
+ "gimli",
+ "ittapi",
+ "log",
+ "object",
+ "rustc-demangle",
+ "rustix 0.35.13",
+ "serde",
+ "target-lexicon",
+ "thiserror",
+ "wasmtime-environ",
+ "wasmtime-jit-debug",
+ "wasmtime-runtime",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "wasmtime-jit-debug"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2f8f15a81292eec468c79a4f887a37a3d02eb0c610f34ddbec607d3e9022f18"
+dependencies = [
+ "object",
+ "once_cell",
+ "rustix 0.35.13",
+]
+
+[[package]]
+name = "wasmtime-runtime"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09af6238c962e8220424c815a7b1a9a6d0ba0694f0ab0ae12a6cda1923935a0d"
+dependencies = [
+ "anyhow",
+ "cc",
+ "cfg-if",
+ "indexmap",
+ "libc",
+ "log",
+ "mach",
+ "memfd",
+ "memoffset 0.6.5",
+ "paste",
+ "rand 0.8.5",
+ "rustix 0.35.13",
+ "thiserror",
+ "wasmtime-asm-macros",
+ "wasmtime-environ",
+ "wasmtime-fiber",
+ "wasmtime-jit-debug",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "wasmtime-types"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dc3dd9521815984b35d6362f79e6b9c72475027cd1c71c44eb8df8fbf33a9fb"
+dependencies = [
+ "cranelift-entity",
+ "serde",
+ "thiserror",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmtime-wasi"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3bba5cc0a940cef3fbbfa7291c7e5fe0f7ec6fb2efa7bd1504032ed6202a1c0"
 dependencies = [
+ "anyhow",
  "wasi-cap-std-sync",
  "wasi-common",
- "wasmi",
+ "wasmtime",
  "wiggle",
 ]
 
 [[package]]
-name = "wasmparser-nostd"
-version = "0.91.0"
+name = "wast"
+version = "35.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c37f310b5a62bfd5ae7c0f1d8e6f98af16a5d6d84ba764e9c36439ec14e318b"
+checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68"
 dependencies = [
- "indexmap-nostd",
+ "leb128",
 ]
 
 [[package]]
 name = "wast"
-version = "35.0.2"
+version = "50.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68"
+checksum = "a2cbb59d4ac799842791fe7e806fa5dbbf6b5554d538e51cc8e176db6ff0ae34"
 dependencies = [
  "leb128",
+ "memchr",
+ "unicode-width",
+ "wasm-encoder",
+]
+
+[[package]]
+name = "wat"
+version = "1.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "584aaf7a1ecf4d383bbe1a25eeab0cbb8ff96acc6796707ff65cde48f4632f15"
+dependencies = [
+ "wast 50.0.0",
 ]
 
 [[package]]
@@ -4645,8 +5054,8 @@ dependencies = [
  "bitflags",
  "thiserror",
  "tracing",
+ "wasmtime",
  "wiggle-macro",
- "witx",
 ]
 
 [[package]]
@@ -4955,7 +5364,7 @@ dependencies = [
  "anyhow",
  "log",
  "thiserror",
- "wast",
+ "wast 35.0.2",
 ]
 
 [[package]]
@@ -4966,9 +5375,9 @@ checksum = "f8e6ab4f5da1b24daf2c590cfac801bacb27b15b4f050e84eb60149ea726f06b"
 
 [[package]]
 name = "wry"
-version = "0.22.5"
+version = "0.22.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3b4906cc7b3c5959893f3984bb60882ec94539eb14622077e6529f5b3d008ee"
+checksum = "f20dd05f35fa0f8d107a4596b50d4e813b2e4f48cdec1f39854f092f7bbacc99"
 dependencies = [
  "base64",
  "block",
@@ -4990,7 +5399,7 @@ dependencies = [
  "once_cell",
  "serde",
  "serde_json",
- "sha2",
+ "sha2 0.10.6",
  "soup2",
  "tao",
  "thiserror",
diff --git a/bins/ayaka-gui/src-tauri/Cargo.toml b/bins/ayaka-gui/src-tauri/Cargo.toml
index 08b9004d..25cecb3a 100644
--- a/bins/ayaka-gui/src-tauri/Cargo.toml
+++ b/bins/ayaka-gui/src-tauri/Cargo.toml
@@ -15,7 +15,7 @@ tauri = { version = "1.2", features = ["cli", "window-all"] }
 tauri-plugin-window-state = "0.1"
 actix-web = "4"
 actix-files = "0.6"
-trylog = "0.2"
+trylog = "0.3"
 
 [features]
 default = [ "custom-protocol" ]
diff --git a/bins/ayaka-gui/src-tauri/src/main.rs b/bins/ayaka-gui/src-tauri/src/main.rs
index 6375fbe2..68a7178b 100644
--- a/bins/ayaka-gui/src-tauri/src/main.rs
+++ b/bins/ayaka-gui/src-tauri/src/main.rs
@@ -21,7 +21,7 @@ use std::{
 use tauri::{
     async_runtime::Mutex, command, utils::config::AppUrl, AppHandle, Manager, State, WindowUrl,
 };
-use trylog::TryLog;
+use trylog::macros::*;
 
 type CommandResult<T> = std::result::Result<T, CommandError>;
 
@@ -140,18 +140,22 @@ async fn open_game(handle: AppHandle, storage: State<'_, Storage>) -> CommandRes
     window.set_title(&ctx.game.config.title)?;
     let settings = {
         OpenGameStatus::LoadSettings.emit(&handle)?;
-        load_settings(&storage.ident).unwrap_or_default_log("Load settings failed")
+        unwrap_or_default_log!(load_settings(&storage.ident), "Load settings failed")
     };
     *storage.settings.lock().await = Some(settings);
 
     OpenGameStatus::LoadGlobalRecords.emit(&handle)?;
-    let global_record = load_global_record(&storage.ident, &ctx.game.config.title)
-        .unwrap_or_default_log("Load global records failed");
+    let global_record = unwrap_or_default_log!(
+        load_global_record(&storage.ident, &ctx.game.config.title),
+        "Load global records failed"
+    );
     *storage.global_record.lock().await = Some(global_record);
 
     OpenGameStatus::LoadRecords.emit(&handle)?;
-    *storage.records.lock().await = load_records(&storage.ident, &ctx.game.config.title)
-        .unwrap_or_default_log("Load records failed");
+    *storage.records.lock().await = unwrap_or_default_log!(
+        load_records(&storage.ident, &ctx.game.config.title),
+        "Load records failed"
+    );
     *storage.context.lock().await = Some(ctx);
 
     OpenGameStatus::Loaded.emit(&handle)?;
@@ -357,13 +361,15 @@ fn get_actions(
     settings: &Settings,
     raw_ctx: &RawContext,
 ) -> (Action, Option<Action>) {
-    let action = context
-        .get_action(&settings.lang, raw_ctx)
-        .unwrap_or_default_log("Cannot get action");
+    let action = unwrap_or_default_log!(
+        context.get_action(&settings.lang, raw_ctx),
+        "Cannot get action"
+    );
     let base_action = settings.sub_lang.as_ref().map(|sub_lang| {
-        context
-            .get_action(sub_lang, raw_ctx)
-            .unwrap_or_default_log("Cannot get sub action")
+        unwrap_or_default_log!(
+            context.get_action(sub_lang, raw_ctx),
+            "Cannot get sub action"
+        )
     });
     (action, base_action)
 }
@@ -421,8 +427,8 @@ async fn history(storage: State<'_, Storage>) -> CommandResult<Vec<(Action, Opti
 }
 
 fn main() -> Result<()> {
-    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
-    let port = listener.local_addr().unwrap().port();
+    let listener = TcpListener::bind("127.0.0.1:0")?;
+    let port = listener.local_addr()?.port();
     tauri::Builder::default()
         .plugin(asset_resolver::init(listener))
         .plugin(tauri_plugin_window_state::Builder::default().build())
diff --git a/book/src/config/i18n.md b/book/src/config/i18n.md
index 25c473bc..de704ab3 100644
--- a/book/src/config/i18n.md
+++ b/book/src/config/i18n.md
@@ -10,9 +10,9 @@ For example, the original text (`ja`)
 - bg: 0
 - \ch{rd}団長！車の用意できました！
 - switches:
-  - おう！||$end = false
-  - 止まるんじゃねぇぞ！||$end = true
-  - 止まれ！|false
+  - おう！
+  - 止まるんじゃねぇぞ！
+  - 止まれ！
 ```
 could be translated as (`zh-Hans`)
 ``` ignore
diff --git a/book/src/config/script.md b/book/src/config/script.md
index 3e713720..a7739ad4 100644
--- a/book/src/config/script.md
+++ b/book/src/config/script.md
@@ -9,6 +9,11 @@ pub enum RawValue {
     Str(String),
 }
 ```
+
+## Using `ayacript`
+`ayacript` is the plugin that provides Ayaka script functionalities.
+You need to add `ayascript` to the config file. See [Plugin](../plugin/summary.md).
+
 ## Execute scripts
 Execute a piece of script(we call it *program*) with `exec` command:
 ``` yaml
diff --git a/book/src/config/switches.md b/book/src/config/switches.md
index 1cf6ceeb..558035fa 100644
--- a/book/src/config/switches.md
+++ b/book/src/config/switches.md
@@ -1,16 +1,17 @@
 # Switches
 The switches could be specified with `switches` command.
-The parameters in one line of text are separated by `|`.
 
-The first parameter is the display text;
-the third parameter is a boolean expression of whether the switch is enabled.
-the second parameter is the action script;
-For the second and the third parameter, see [Script](./script.md).
+Some special global variables are used.
+`$?` is the index of selected switch (start from 0).
+`$<num>` indicates whether a switch is enabled, and is cleared after one switch is selected.
+If a specific `$<num>` variable is not defined or defined as `null` or `~`, it is treated as `true`.
 
 ``` yaml
 - 'Choose a switch:'
+- exec: $3 = false
 - switches:
-  - Switch 1||$s = 1
-  - Switch 2||$s = 2
-  - Not enabled|false
+  - Switch 1
+  - Switch 2
+  - Not enabled
+- You chose switch \var{?}
 ```
diff --git a/book/src/plugin/line_plugin.md b/book/src/plugin/line_plugin.md
index e26e8fbc..7ed090a1 100644
--- a/book/src/plugin/line_plugin.md
+++ b/book/src/plugin/line_plugin.md
@@ -34,6 +34,7 @@ Hello world!
 The `LineProcessResult` object contains the global variables and temp variables. The temp variables will only apply to this specific line.
 
 ## Existing plugins
-| Plugin  | Description          |
-| ------- | -------------------- |
-| `media` | Multimedia commands. |
+| Plugin     | Description          |
+| ---------- | -------------------- |
+| `ayacript` | Ayaka script.        |
+| `media`    | Multimedia commands. |
diff --git a/examples/Basic/config.yaml b/examples/Basic/config.yaml
new file mode 100644
index 00000000..b61bf0c5
--- /dev/null
+++ b/examples/Basic/config.yaml
@@ -0,0 +1,5 @@
+title: Basic
+author: Berrysoft
+base_lang: en
+paras: paras
+start: init
diff --git a/examples/Basic/paras/en/init.yaml b/examples/Basic/paras/en/init.yaml
new file mode 100644
index 00000000..4d52b9b0
--- /dev/null
+++ b/examples/Basic/paras/en/init.yaml
@@ -0,0 +1,29 @@
+- tag: init
+  texts:
+    - ┏━━━┓┏┓━━┏┓┏━━━┓┏┓┏━┓┏━━━┓
+    - ┃┏━┓┃┃┗┓┏┛┃┃┏━┓┃┃┃┃┏┛┃┏━┓┃
+    - ┃┃━┃┃┗┓┗┛┏┛┃┃━┃┃┃┗┛┛━┃┃━┃┃
+    - ┃┗━┛┃━┗┓┏┛━┃┗━┛┃┃┏┓┃━┃┗━┛┃
+    - ┃┏━┓┃━━┃┃━━┃┏━┓┃┃┃┃┗┓┃┏━┓┃
+    - ┗┛━┗┛━━┗┛━━┗┛━┗┛┗┛┗━┛┗┛━┗┛
+    - This is the first line.
+    - This is the second line.
+    - Choose a switch from the following line.
+    - switches:
+      - Jump to paragraph 0
+      - Jump to paragraph 1
+      - Exit
+  next: para\var{?}
+
+- tag: para0
+  texts:
+    - This is paragraph 0.
+    - Note that the start index of switches is 0.
+    - >
+      This paragraph is in the init.yaml file,
+      therefore it could be only seen in this file.
+  next: para1
+
+- tag: para2
+  texts: 
+    - # We use an empty line to pass
diff --git a/examples/Basic/paras/en/para1.yaml b/examples/Basic/paras/en/para1.yaml
new file mode 100644
index 00000000..a306cc45
--- /dev/null
+++ b/examples/Basic/paras/en/para1.yaml
@@ -0,0 +1,6 @@
+- tag: para1
+  texts:
+    - This is paragraph 1.
+    - >
+      This paragraph is in the para1.yaml file,
+      therefore it could be seen in all files.
diff --git a/examples/Basic/paras/ja/init.yaml b/examples/Basic/paras/ja/init.yaml
new file mode 100644
index 00000000..215f9ce5
--- /dev/null
+++ b/examples/Basic/paras/ja/init.yaml
@@ -0,0 +1,21 @@
+- tag: init
+  texts:
+    - # Empty line to fallback to base language
+    - # 空白は基礎の言語に切り戻されます
+    -
+    -
+    -
+    -
+    - １列目
+    - ２列目
+    - 次の列に一つを選んでください
+    - switches:
+      - 第０段落へ
+      - 第１段落へ
+      - 終了する
+
+- tag: para0
+  texts:
+    - 第０段落です
+    - 選択肢の索引は０に始まって注意してください
+    - この段落は init.yaml にあるから、このファイルだけに見られます
diff --git a/examples/Basic/paras/ja/para1.yaml b/examples/Basic/paras/ja/para1.yaml
new file mode 100644
index 00000000..5af46cc5
--- /dev/null
+++ b/examples/Basic/paras/ja/para1.yaml
@@ -0,0 +1,4 @@
+- tag: para1
+  texts:
+    - 第１段落です
+    - この段落は para1.yaml にあるから、全部のファイルに見られます
diff --git a/examples/Basic/paras/zh-Hans/init.yaml b/examples/Basic/paras/zh-Hans/init.yaml
new file mode 100644
index 00000000..0fe1173e
--- /dev/null
+++ b/examples/Basic/paras/zh-Hans/init.yaml
@@ -0,0 +1,21 @@
+- tag: init
+  texts:
+    - # Empty line to fallback to base language
+    - # 空行会回退到基础语言
+    -
+    -
+    -
+    -
+    - 这是第一行
+    - 这是第二行
+    - 从下面选择一个选项
+    - switches:
+      - 跳到第0段
+      - 跳到第1段
+      - 退出
+
+- tag: para0
+  texts:
+    - 这是第0段
+    - 注意选项的起始索引为0
+    - 这个段落在 init.yaml 文件中，因此只在本文件中可见
diff --git a/examples/Basic/paras/zh-Hans/para1.yaml b/examples/Basic/paras/zh-Hans/para1.yaml
new file mode 100644
index 00000000..e0236126
--- /dev/null
+++ b/examples/Basic/paras/zh-Hans/para1.yaml
@@ -0,0 +1,4 @@
+- tag: para1
+  texts:
+    - 这是第1段
+    - 这个段落在 para1.yaml 文件中，因此在所有文件中可见
diff --git a/examples/Fibonacci/config.yaml b/examples/Fibonacci/config.yaml
index 0ea92b45..db8fe653 100644
--- a/examples/Fibonacci/config.yaml
+++ b/examples/Fibonacci/config.yaml
@@ -1,5 +1,9 @@
 title: Fibonacci
 author: Berrysoft
+plugins:
+  dir: ../plugins/
+  modules:
+    - ayacript
 base_lang: en
 paras: paras
 start: init
diff --git a/examples/Fibonacci2/config.yaml b/examples/Fibonacci2/config.yaml
index dcf9b9fa..ab21f7ca 100644
--- a/examples/Fibonacci2/config.yaml
+++ b/examples/Fibonacci2/config.yaml
@@ -3,6 +3,7 @@ author: Berrysoft
 plugins:
   dir: ../plugins/
   modules:
+    - ayacript
     - random
 base_lang: en
 paras: paras
diff --git a/examples/Gacha/config.yaml b/examples/Gacha/config.yaml
index 0c3efac2..c092ebfa 100644
--- a/examples/Gacha/config.yaml
+++ b/examples/Gacha/config.yaml
@@ -3,6 +3,7 @@ author: Berrysoft
 plugins:
   dir: ../plugins/
   modules:
+    - ayacript
     - ayalog
     - media
     - random
diff --git a/examples/Gacha/paras/zh-Hans/init.yaml b/examples/Gacha/paras/zh-Hans/init.yaml
index c15fa39e..2155111d 100644
--- a/examples/Gacha/paras/zh-Hans/init.yaml
+++ b/examples/Gacha/paras/zh-Hans/init.yaml
@@ -7,16 +7,17 @@
   texts:
     - 你共抽了\var{total}次，三星\var{t3}个，四星\var{t4}个，五星\var{t5}个
     - switches:
-      - 祈愿一次||$n = 1
-      - 祈愿十次||$n = 10
-      - 退出||$n = -1
+      - 祈愿一次
+      - 祈愿十次
+      - 退出
     - exec: |
+        $n = if($? == 0, 1, 10);
         $total5 = 0;
         $total4 = 0;
         $total3 = 0;
         $temp5 = $c5;
         $temp4 = $c4;
-        $next = if($n > 0, "calc")
+        $next = if($? < 2, "calc")
   next: \var{next}
 
 - tag: calc
diff --git a/examples/Orga/config.yaml b/examples/Orga/config.yaml
index 0f315925..313aa4d8 100644
--- a/examples/Orga/config.yaml
+++ b/examples/Orga/config.yaml
@@ -9,6 +9,7 @@ res: res
 plugins:
   dir: ../plugins/
   modules:
+    - ayacript
     - basictex
     - media
 base_lang: ja
diff --git a/examples/Orga/paras/ja/main.yaml b/examples/Orga/paras/ja/main.yaml
index b134c8fc..8276d289 100644
--- a/examples/Orga/paras/ja/main.yaml
+++ b/examples/Orga/paras/ja/main.yaml
@@ -3,11 +3,14 @@
   texts:
     - bg: 0
     - \ch{rd}団長！車の用意できました！
+    - exec: $2 = false
     - switches:
-      - おう！||$end = false
-      - 止まるんじゃねぇぞ！||$end = true
-      - 止まれ！|false
-    - exec: $next = if($end, "end", "walk")
+      - おう！
+      - 止まるんじゃねぇぞ！
+      - 止まれ！
+    - exec: |
+        $end = $? == 1;
+        $next = if($end, "end", "walk")
   next: \var{next}
 - tag: walk
   title: 静かだ
@@ -16,9 +19,10 @@
     - /kdr//はい、お気をつけて。
     - bg: 1
     - /rd//なんか静かですね。街の中にはギャラルホルンもいないし本部とはえらい違いだ。
+    - exec: $1 = false
     - switches:
       - 安全
-      - 気を付け|false
+      - 気を付け
     - /org//ああ。火星の戦力は軒並み向こうに回してんのかもな。
     - /rd//まっそんなのもう関係ないですけどね！
     - /org//上機嫌だな。
diff --git a/examples/Orga/paras/zh-Hans/main.yaml b/examples/Orga/paras/zh-Hans/main.yaml
index 49f4d429..a79d56e0 100644
--- a/examples/Orga/paras/zh-Hans/main.yaml
+++ b/examples/Orga/paras/zh-Hans/main.yaml
@@ -3,6 +3,7 @@
   texts:
     -
     - 团长！车已经准备好了！
+    -
     - switches:
       - 哦！
       - 不要停下来啊！
@@ -15,6 +16,7 @@
     - 好，路上小心。
     -
     - 感觉真安静啊，街上也没有加拉尔霍恩的人，和总部差别真大啊。
+    -
     - switches:
       - 安全
       - 小心
diff --git a/plugins/Cargo.lock b/plugins/Cargo.lock
index 03efc15f..13a142c6 100644
--- a/plugins/Cargo.lock
+++ b/plugins/Cargo.lock
@@ -52,13 +52,24 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "ayacript"
+version = "0.1.0"
+dependencies = [
+ "ayaka-bindings",
+ "ayaka-bindings-types",
+ "ayaka-plugin",
+ "ayaka-script",
+ "trylog",
+]
+
 [[package]]
 name = "ayaka-bindings"
 version = "0.1.0"
 dependencies = [
  "ayaka-bindings-impl",
  "ayaka-bindings-types",
- "ayaka-script",
+ "ayaka-primitive",
  "log",
  "rmp-serde",
  "scopeguard",
@@ -69,6 +80,7 @@ dependencies = [
 name = "ayaka-bindings-impl"
 version = "0.1.0"
 dependencies = [
+ "proc-macro-crate",
  "proc-macro2",
  "quote",
  "syn",
@@ -78,23 +90,40 @@ dependencies = [
 name = "ayaka-bindings-types"
 version = "0.1.0"
 dependencies = [
- "ayaka-script",
+ "ayaka-primitive",
  "fallback",
  "log",
  "serde",
 ]
 
+[[package]]
+name = "ayaka-plugin"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "rmp-serde",
+ "serde",
+]
+
+[[package]]
+name = "ayaka-primitive"
+version = "0.1.0"
+dependencies = [
+ "regex",
+ "serde",
+ "serde_with",
+]
+
 [[package]]
 name = "ayaka-script"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "ayaka-primitive",
  "lalrpop",
  "lalrpop-util",
  "log",
- "regex",
  "serde",
- "serde_with",
  "trylog",
 ]
 
@@ -649,6 +678,17 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 
+[[package]]
+name = "proc-macro-crate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
+dependencies = [
+ "once_cell",
+ "thiserror",
+ "toml",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.47"
@@ -803,18 +843,18 @@ checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
 
 [[package]]
 name = "serde"
-version = "1.0.148"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
+checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.148"
+version = "1.0.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
+checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -978,11 +1018,20 @@ dependencies = [
  "crunchy",
 ]
 
+[[package]]
+name = "toml"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "trylog"
-version = "0.2.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f693ba1b7784211afaca884e0dffe0c6086a1113c44ee7030f3277bcd63fde2a"
+checksum = "5c3c554f1b36be2f3c2233dafea1bbaf3e5d07d3b81db54d62606d31805ac7b1"
 dependencies = [
  "log",
 ]
diff --git a/plugins/Cargo.toml b/plugins/Cargo.toml
index ab4d1236..b59f116b 100644
--- a/plugins/Cargo.toml
+++ b/plugins/Cargo.toml
@@ -1,7 +1,8 @@
 [workspace]
 members = [
-    "basictex",
+    "ayacript",
     "ayalog",
+    "basictex",
     "live2d",
     "markdown",
     "media",
diff --git a/plugins/ayacript/Cargo.toml b/plugins/ayacript/Cargo.toml
new file mode 100644
index 00000000..81d3e52c
--- /dev/null
+++ b/plugins/ayacript/Cargo.toml
@@ -0,0 +1,17 @@
+cargo-features = ["per-package-target"]
+
+[package]
+name = "ayacript"
+version = "0.1.0"
+edition = "2021"
+forced-target = "wasm32-unknown-unknown"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+ayaka-bindings = { path = "../../utils/ayaka-bindings" }
+ayaka-bindings-types = { path = "../../utils/ayaka-bindings-types" }
+ayaka-script = { path = "../../utils/ayaka-script" }
+ayaka-plugin = { path = "../../utils/ayaka-plugin" }
+trylog = "0.3"
diff --git a/plugins/ayacript/src/lib.rs b/plugins/ayacript/src/lib.rs
new file mode 100644
index 00000000..5322dc22
--- /dev/null
+++ b/plugins/ayacript/src/lib.rs
@@ -0,0 +1,35 @@
+#![feature(once_cell)]
+
+mod plugin;
+mod script;
+
+use ayaka_bindings::*;
+use ayaka_bindings_types::VarMap;
+use ayaka_script::*;
+use plugin::*;
+use script::*;
+use std::sync::LazyLock;
+use trylog::macros::*;
+
+#[export]
+fn plugin_type() -> PluginType {
+    PluginType::builder().line(["exec"]).build()
+}
+
+static RUNTIME: LazyLock<Runtime> = LazyLock::new(Runtime::new);
+
+#[export]
+fn exec(mut ctx: LineProcessContext) -> LineProcessResult {
+    let program = ctx.props["exec"].get_str();
+    unwrap_or_default_log!(
+        program.parse::<Program>().map(|exec| {
+            let mut table = VarTable::new(&RUNTIME, &mut ctx.ctx.locals);
+            table.call(&exec);
+            LineProcessResult {
+                locals: ctx.ctx.locals,
+                vars: VarMap::default(),
+            }
+        }),
+        "Cannot parse program"
+    )
+}
diff --git a/plugins/ayacript/src/plugin.rs b/plugins/ayacript/src/plugin.rs
new file mode 100644
index 00000000..895346b8
--- /dev/null
+++ b/plugins/ayacript/src/plugin.rs
@@ -0,0 +1,101 @@
+use crate::*;
+use ayaka_plugin::*;
+use ayaka_script::*;
+use std::collections::HashMap;
+
+#[import("plugin")]
+extern "C" {
+    fn __modules() -> Vec<String>;
+    fn __call(module: &str, name: &str, args: &[u8]) -> Vec<u8>;
+}
+
+pub struct HostModule {
+    name: String,
+}
+
+impl RawModule for HostModule {
+    type Linker = HostLinker;
+
+    type LinkerHandle<'a> = HostLinker;
+
+    type Func = ();
+
+    fn call<T>(&self, name: &str, args: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
+        let data = __call(&self.name, name, args);
+        f(&data)
+    }
+}
+
+pub struct HostLinker;
+
+impl Linker<HostModule> for HostLinker {
+    fn new(_root_path: impl AsRef<std::path::Path>) -> Result<Self> {
+        unimplemented!()
+    }
+
+    fn create(&self, _binary: &[u8]) -> Result<HostModule> {
+        unimplemented!()
+    }
+
+    fn import(&mut self, _ns: impl Into<String>, _funcs: HashMap<String, ()>) -> Result<()> {
+        Ok(())
+    }
+
+    fn wrap_raw(&self, _f: impl (Fn(Self, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static) {}
+}
+
+impl<'a> LinkerHandle<'a, HostModule> for HostLinker {
+    fn call<T>(
+        &mut self,
+        _m: &HostModule,
+        _name: &str,
+        _args: &[u8],
+        _f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        unimplemented!()
+    }
+
+    fn slice<T>(&self, _start: i32, _len: i32, _f: impl FnOnce(&[u8]) -> T) -> T {
+        unimplemented!()
+    }
+}
+
+pub struct Module {
+    module: PluginModule<HostModule>,
+}
+
+impl Module {
+    fn new(module: HostModule) -> Self {
+        Self {
+            module: PluginModule::new(module),
+        }
+    }
+
+    /// Calls a script plugin method by name.
+    pub fn dispatch_method(&self, name: &str, args: &[RawValue]) -> Result<RawValue> {
+        self.module.call(name, (args,))
+    }
+}
+
+pub struct Runtime {
+    modules: HashMap<String, Module>,
+}
+
+impl Runtime {
+    pub fn new() -> Self {
+        let modules = __modules();
+        let modules = modules
+            .into_iter()
+            .map(|name| {
+                let m = HostModule { name: name.clone() };
+                (name, Module::new(m))
+            })
+            .collect();
+        Self { modules }
+    }
+
+    /// Gets module from name.
+    pub fn module(&self, key: &str) -> Option<&Module> {
+        self.modules.get(key)
+    }
+}
diff --git a/utils/ayaka-runtime/src/script.rs b/plugins/ayacript/src/script.rs
similarity index 67%
rename from utils/ayaka-runtime/src/script.rs
rename to plugins/ayacript/src/script.rs
index 15ae30d7..05a2a8f0 100644
--- a/utils/ayaka-runtime/src/script.rs
+++ b/plugins/ayacript/src/script.rs
@@ -1,25 +1,21 @@
 //! The script interpreter.
 
-use crate::plugin::{BackendModule, Runtime};
-use ayaka_bindings_types::VarMap;
-use ayaka_plugin::RawModule;
-use ayaka_script::*;
-use log::warn;
-use trylog::TryLog;
+use crate::*;
+use trylog::macros::*;
 
 /// The variable table in scripts.
-pub struct VarTable<'a, M: RawModule = BackendModule> {
+pub struct VarTable<'a> {
     /// The plugin runtime.
-    pub runtime: &'a Runtime<M>,
+    pub runtime: &'a Runtime,
     /// The context variables.
     pub locals: &'a mut VarMap,
     /// The locale variables.
     pub vars: VarMap,
 }
 
-impl<'a, M: RawModule> VarTable<'a, M> {
+impl<'a> VarTable<'a> {
     /// Creates a new [`VarTable`].
-    pub fn new(runtime: &'a Runtime<M>, locals: &'a mut VarMap) -> Self {
+    pub fn new(runtime: &'a Runtime, locals: &'a mut VarMap) -> Self {
         Self {
             runtime,
             locals,
@@ -36,17 +32,17 @@ impl<'a, M: RawModule> VarTable<'a, M> {
 /// Represents a callable part of a script.
 pub trait Callable {
     /// Calls the part with the [`VarTable`].
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue;
+    fn call(&self, ctx: &mut VarTable) -> RawValue;
 }
 
 impl<T: Callable> Callable for &T {
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
+    fn call(&self, ctx: &mut VarTable) -> RawValue {
         (*self).call(ctx)
     }
 }
 
 impl<T: Callable> Callable for Option<T> {
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
+    fn call(&self, ctx: &mut VarTable) -> RawValue {
         match self {
             Some(c) => c.call(ctx),
             None => RawValue::Unit,
@@ -55,7 +51,7 @@ impl<T: Callable> Callable for Option<T> {
 }
 
 impl Callable for Program {
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
+    fn call(&self, ctx: &mut VarTable) -> RawValue {
         ctx.vars.clear();
         let mut res = RawValue::Unit;
         for expr in &self.0 {
@@ -66,7 +62,7 @@ impl Callable for Program {
 }
 
 impl Callable for Expr {
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
+    fn call(&self, ctx: &mut VarTable) -> RawValue {
         match self {
             Self::Ref(r) => r.call(ctx),
             Self::Const(c) => c.clone(),
@@ -97,12 +93,7 @@ impl Callable for Expr {
     }
 }
 
-fn bin_val<M: RawModule>(
-    ctx: &mut VarTable<M>,
-    lhs: &Expr,
-    op: &ValBinaryOp,
-    rhs: &Expr,
-) -> RawValue {
+fn bin_val(ctx: &mut VarTable, lhs: &Expr, op: &ValBinaryOp, rhs: &Expr) -> RawValue {
     let lhs = lhs.call(ctx);
     let rhs = rhs.call(ctx);
     let t = lhs.get_type().max(rhs.get_type());
@@ -160,12 +151,7 @@ fn bin_str_val(lhs: RawValue, op: &ValBinaryOp, rhs: RawValue) -> RawValue {
     }
 }
 
-fn bin_logic<M: RawModule>(
-    ctx: &mut VarTable<M>,
-    lhs: &Expr,
-    op: &LogicBinaryOp,
-    rhs: &Expr,
-) -> RawValue {
+fn bin_logic(ctx: &mut VarTable, lhs: &Expr, op: &LogicBinaryOp, rhs: &Expr) -> RawValue {
     let res = match op {
         LogicBinaryOp::And => lhs.call(ctx).get_bool() && rhs.call(ctx).get_bool(),
         LogicBinaryOp::Or => lhs.call(ctx).get_bool() || rhs.call(ctx).get_bool(),
@@ -196,7 +182,7 @@ fn bin_ord_logic<T: Ord>(lhs: &T, op: &LogicBinaryOp, rhs: &T) -> bool {
     }
 }
 
-fn assign<M: RawModule>(ctx: &mut VarTable<M>, e: &Expr, val: RawValue) -> RawValue {
+fn assign(ctx: &mut VarTable, e: &Expr, val: RawValue) -> RawValue {
     match e {
         Expr::Ref(r) => match r {
             Ref::Var(n) => ctx.vars.insert(n.into(), val),
@@ -207,7 +193,7 @@ fn assign<M: RawModule>(ctx: &mut VarTable<M>, e: &Expr, val: RawValue) -> RawVa
     RawValue::Unit
 }
 
-fn call<M: RawModule>(ctx: &mut VarTable<M>, ns: &str, name: &str, args: &[Expr]) -> RawValue {
+fn call(ctx: &mut VarTable, ns: &str, name: &str, args: &[Expr]) -> RawValue {
     if ns.is_empty() {
         match name {
             "if" => if args.get(0).call(ctx).get_bool() {
@@ -220,57 +206,33 @@ fn call<M: RawModule>(ctx: &mut VarTable<M>, ns: &str, name: &str, args: &[Expr]
         }
     } else {
         let args = args.iter().map(|e| e.call(ctx)).collect::<Vec<_>>();
-        ctx.runtime
-            .module(ns)
-            .map(|runtime| {
-                runtime
-                    .dispatch_method(name, &args)
-                    .unwrap_or_default_log_with(|| format!("Calling `{}.{}` error", ns, name))
-            })
-            .unwrap_or_default_log_with(|| format!("Cannot find namespace `{}`", ns))
+        unwrap_or_default_log!(
+            ctx.runtime.module(ns).map(|runtime| {
+                unwrap_or_default_log!(
+                    runtime.dispatch_method(name, &args),
+                    format!("Calling `{}.{}` error", ns, name)
+                )
+            }),
+            format!("Cannot find namespace `{}`", ns)
+        )
     }
 }
 
 impl Callable for Ref {
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
+    fn call(&self, ctx: &mut VarTable) -> RawValue {
         match self {
-            Self::Var(n) => ctx
-                .vars
-                .get(n)
-                .cloned()
-                .unwrap_or_default_log("Cannot find variable"),
-            Self::Ctx(n) => ctx
-                .locals
-                .get(n)
-                .cloned()
-                .unwrap_or_default_log("Cannot find context variable"),
-        }
-    }
-}
-
-impl Callable for Text {
-    fn call<M: RawModule>(&self, ctx: &mut VarTable<M>) -> RawValue {
-        let mut str = String::new();
-        for line in &self.0 {
-            match line {
-                SubText::Str(s) => str.push_str(s),
-                SubText::Cmd(c) => {
-                    let value = match c {
-                        Command::Character(_, _) => RawValue::Unit,
-                        Command::Res(_) | Command::Other(_, _) => {
-                            warn!("Unsupported command in text.");
-                            RawValue::Unit
-                        }
-                        Command::Ctx(n) => ctx
-                            .locals
-                            .get(n)
-                            .cloned()
-                            .unwrap_or_default_log("Cannot find variable"),
-                    };
-                    str.push_str(&value.get_str());
-                }
+            Self::Var(n) => {
+                unwrap_or_default_log!(
+                    ctx.vars.get(n).cloned(),
+                    format!("Cannot find variable {}", n)
+                )
+            }
+            Self::Ctx(n) => {
+                unwrap_or_default_log!(
+                    ctx.locals.get(n).cloned(),
+                    format!("Cannot find variable ${}", n)
+                )
             }
         }
-        RawValue::Str(str.trim().to_string())
     }
 }
diff --git a/plugins/random/Cargo.toml b/plugins/random/Cargo.toml
index d7f477d5..b55ccb1b 100644
--- a/plugins/random/Cargo.toml
+++ b/plugins/random/Cargo.toml
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
 [dependencies]
 ayaka-bindings = { path = "../../utils/ayaka-bindings" }
 rand = "0.8"
-trylog = "0.2"
+trylog = "0.3"
diff --git a/plugins/random/src/lib.rs b/plugins/random/src/lib.rs
index 5f991c81..86e2e1b8 100644
--- a/plugins/random/src/lib.rs
+++ b/plugins/random/src/lib.rs
@@ -3,7 +3,7 @@
 use ayaka_bindings::*;
 use rand::{rngs::StdRng, Rng, SeedableRng};
 use std::sync::{LazyLock, Mutex};
-use trylog::TryLog;
+use trylog::macros::*;
 
 #[export]
 fn plugin_type() -> PluginType {
@@ -14,14 +14,15 @@ static RNG: LazyLock<Mutex<StdRng>> = LazyLock::new(|| Mutex::new(StdRng::from_e
 
 #[export]
 fn rnd(args: Vec<RawValue>) -> RawValue {
-    RNG.lock()
-        .map(|mut rng| {
+    unwrap_or_default_log!(
+        RNG.lock().map(|mut rng| {
             let res = match args.len() {
                 0 => rng.gen(),
                 1 => rng.gen_range(0..args[0].get_num()),
                 _ => rng.gen_range(args[0].get_num()..args[1].get_num()),
             };
             RawValue::Num(res)
-        })
-        .unwrap_or_default_log("Cannot get random engine")
+        }),
+        "Cannot get random engine"
+    )
 }
diff --git a/utils/Cargo.toml b/utils/Cargo.toml
index 8ac2d707..99b29f32 100644
--- a/utils/Cargo.toml
+++ b/utils/Cargo.toml
@@ -1,5 +1,6 @@
 [workspace]
 members = [
+    "ayaka-primitive",
     "ayaka-bindings-types",
     "ayaka-script",
     "ayaka-plugin",
diff --git a/utils/ayaka-bindings-impl/Cargo.toml b/utils/ayaka-bindings-impl/Cargo.toml
index a6c1a7da..345a647c 100644
--- a/utils/ayaka-bindings-impl/Cargo.toml
+++ b/utils/ayaka-bindings-impl/Cargo.toml
@@ -11,3 +11,4 @@ proc-macro = true
 syn = { version = "1.0", features = ["full"] }
 quote = "1.0"
 proc-macro2 = "1.0"
+proc-macro-crate = "1.2"
diff --git a/utils/ayaka-bindings-impl/src/lib.rs b/utils/ayaka-bindings-impl/src/lib.rs
index 5bc002b5..dbfe8261 100644
--- a/utils/ayaka-bindings-impl/src/lib.rs
+++ b/utils/ayaka-bindings-impl/src/lib.rs
@@ -1,4 +1,5 @@
 use proc_macro::TokenStream;
+use proc_macro_crate::{crate_name, FoundCrate};
 use quote::{quote, TokenStreamExt};
 use syn::{
     parse_macro_input, parse_str, AttributeArgs, FnArg, ForeignItem, Ident, ItemFn, ItemForeignMod,
@@ -56,6 +57,13 @@ pub fn import(attr: TokenStream, input: TokenStream) -> TokenStream {
                 let name = sig.ident.clone();
                 let name_str = name.to_string();
                 let impname = parse_str::<Ident>(&format!("__import_{}", name_str)).unwrap();
+                let bindings_crate_name = match crate_name("ayaka-bindings").unwrap() {
+                    FoundCrate::Itself => quote!(crate),
+                    FoundCrate::Name(name) => {
+                        let name = parse_str::<Ident>(&name).unwrap();
+                        quote!(::#name)
+                    }
+                };
                 let import_func = quote! {
                     #[doc(hidden)]
                     #[link(wasm_import_module = #module)]
@@ -65,7 +73,7 @@ pub fn import(attr: TokenStream, input: TokenStream) -> TokenStream {
                     }
                     #[allow(unsafe_code)]
                     #(#attrs)* #vis #sig {
-                        unsafe { ::ayaka_bindings::__import(#impname, (#(#params,)*)) }
+                        unsafe { #bindings_crate_name::__import(#impname, (#(#params,)*)) }
                     }
                 };
                 imports.append_all(import_func);
diff --git a/utils/ayaka-bindings-types/Cargo.toml b/utils/ayaka-bindings-types/Cargo.toml
index 0b8d75d4..73a4ec6e 100644
--- a/utils/ayaka-bindings-types/Cargo.toml
+++ b/utils/ayaka-bindings-types/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Berrysoft <Strawberry_Str@hotmail.com>"]
 edition = "2021"
 
 [dependencies]
-ayaka-script = { path = "../ayaka-script" }
+ayaka-primitive = { path = "../ayaka-primitive" }
 fallback = "0.1"
 serde = { version = "1.0", features = ["derive"] }
 log = { version = "0.4", features = ["serde"] }
diff --git a/utils/ayaka-bindings-types/src/config.rs b/utils/ayaka-bindings-types/src/config.rs
index b4836011..cdc0e514 100644
--- a/utils/ayaka-bindings-types/src/config.rs
+++ b/utils/ayaka-bindings-types/src/config.rs
@@ -1,4 +1,4 @@
-use ayaka_script::RawValue;
+use ayaka_primitive::RawValue;
 use fallback::FallbackSpec;
 use serde::{Deserialize, Serialize};
 use std::{
diff --git a/utils/ayaka-bindings/Cargo.toml b/utils/ayaka-bindings/Cargo.toml
index 76904435..40529dde 100644
--- a/utils/ayaka-bindings/Cargo.toml
+++ b/utils/ayaka-bindings/Cargo.toml
@@ -5,7 +5,7 @@ authors = ["Berrysoft <Strawberry_Str@hotmail.com>"]
 edition = "2021"
 
 [dependencies]
-ayaka-script = { path = "../ayaka-script" }
+ayaka-primitive = { path = "../ayaka-primitive" }
 ayaka-bindings-types = { path = "../ayaka-bindings-types" }
 ayaka-bindings-impl = { path = "../ayaka-bindings-impl" }
 serde = "1.0"
diff --git a/utils/ayaka-bindings/src/lib.rs b/utils/ayaka-bindings/src/lib.rs
index d6c2a787..b5f6ea7f 100644
--- a/utils/ayaka-bindings/src/lib.rs
+++ b/utils/ayaka-bindings/src/lib.rs
@@ -5,7 +5,7 @@
 #[doc(no_inline)]
 pub use ayaka_bindings_types::*;
 #[doc(no_inline)]
-pub use ayaka_script::{RawValue, ValueType};
+pub use ayaka_primitive::{RawValue, ValueType};
 #[doc(no_inline)]
 pub use log;
 
diff --git a/utils/ayaka-bindings/src/logger.rs b/utils/ayaka-bindings/src/logger.rs
index 93c9a6a9..923ba436 100644
--- a/utils/ayaka-bindings/src/logger.rs
+++ b/utils/ayaka-bindings/src/logger.rs
@@ -1,8 +1,9 @@
+use crate::import;
 use log::Log;
 
-#[link(wasm_import_module = "log")]
+#[import("log")]
 extern "C" {
-    fn __log(len: usize, data: *const u8);
+    fn __log(record: ayaka_bindings_types::Record);
     fn __log_flush();
 }
 
@@ -29,13 +30,10 @@ impl Log for PluginLogger {
     }
 
     fn log(&self, record: &log::Record) {
-        let record: ayaka_bindings_types::Record = record.into();
-        let record: ayaka_bindings_types::Record = record.into();
-        let data = rmp_serde::to_vec(&(record,)).unwrap();
-        unsafe { __log(data.len(), data.as_ptr()) };
+        __log(record.into())
     }
 
     fn flush(&self) {
-        unsafe { __log_flush() }
+        __log_flush()
     }
 }
diff --git a/utils/ayaka-plugin-nop/src/lib.rs b/utils/ayaka-plugin-nop/src/lib.rs
index ebd1edf4..0980a0dc 100644
--- a/utils/ayaka-plugin-nop/src/lib.rs
+++ b/utils/ayaka-plugin-nop/src/lib.rs
@@ -12,7 +12,9 @@ use std::{collections::HashMap, path::Path};
 pub struct NopModule;
 
 impl RawModule for NopModule {
-    type Linker = NopStoreLinker;
+    type Linker = NopLinker;
+
+    type LinkerHandle<'a> = NopLinker;
 
     type Func = ();
 
@@ -22,9 +24,9 @@ impl RawModule for NopModule {
 }
 
 /// A no-operation store & linker.
-pub struct NopStoreLinker;
+pub struct NopLinker;
 
-impl StoreLinker<NopModule> for NopStoreLinker {
+impl Linker<NopModule> for NopLinker {
     fn new(_root_path: impl AsRef<Path>) -> Result<Self> {
         Ok(Self)
     }
@@ -37,7 +39,21 @@ impl StoreLinker<NopModule> for NopStoreLinker {
         Ok(())
     }
 
-    fn wrap(&self, _f: impl Fn() + Send + Sync + 'static) {}
+    fn wrap_raw(&self, _f: impl (Fn(Self, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static) {}
+}
+
+impl<'a> LinkerHandle<'a, NopModule> for NopLinker {
+    fn call<T>(
+        &mut self,
+        _m: &NopModule,
+        name: &str,
+        _args: &[u8],
+        _f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        bail!("Trying to call {name}.")
+    }
 
-    fn wrap_with_args_raw(&self, _f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static) {}
+    fn slice<T>(&self, _start: i32, _len: i32, _f: impl FnOnce(&[u8]) -> T) -> T {
+        unimplemented!("Trying to slice.")
+    }
 }
diff --git a/utils/ayaka-plugin-wasmer/Cargo.toml b/utils/ayaka-plugin-wasmer/Cargo.toml
index b93550bc..50b1af2a 100644
--- a/utils/ayaka-plugin-wasmer/Cargo.toml
+++ b/utils/ayaka-plugin-wasmer/Cargo.toml
@@ -7,4 +7,3 @@ edition = "2021"
 ayaka-plugin = { path = "../ayaka-plugin" }
 wasmer = "3.0"
 wasmer-wasi = "3.0"
-scopeguard = "1.1"
diff --git a/utils/ayaka-plugin-wasmer/src/lib.rs b/utils/ayaka-plugin-wasmer/src/lib.rs
index 00781658..dbea66c1 100644
--- a/utils/ayaka-plugin-wasmer/src/lib.rs
+++ b/utils/ayaka-plugin-wasmer/src/lib.rs
@@ -4,7 +4,6 @@
 #![warn(missing_docs)]
 
 use ayaka_plugin::*;
-use scopeguard::defer;
 use std::{
     collections::HashMap,
     path::Path,
@@ -43,111 +42,80 @@ unsafe fn mem_slice_mut<R>(
 
 type HostStore = Arc<Mutex<Store>>;
 
-struct HostInstance {
+/// A Wasmer [`Instance`].
+pub struct WasmerModule {
     store: HostStore,
     instance: Instance,
+    memory: Memory,
+    abi_free: TypedFunction<(i32, i32), ()>,
+    abi_alloc: TypedFunction<i32, i32>,
 }
 
-impl HostInstance {
-    pub fn new(store: HostStore, instance: Instance) -> Self {
-        Self { store, instance }
-    }
-
-    pub fn get_memory(&self) -> Result<HostMemory, ExportError> {
-        self.instance
+impl WasmerModule {
+    /// Loads the WASM [`Module`], with some imports.
+    fn new(store: HostStore, instance: Instance) -> Result<Self> {
+        let memory = instance.exports.get_memory(MEMORY_NAME)?.clone();
+        let inner_store = store.lock().unwrap();
+        let abi_free = instance
             .exports
-            .get_memory(MEMORY_NAME)
-            .map(|mem| HostMemory::new(self.store.clone(), mem.clone()))
-    }
-
-    pub fn get_func(&self, name: &str) -> Result<HostFunction, ExportError> {
-        self.instance
+            .get_typed_function(&inner_store.as_store_ref(), ABI_FREE_NAME)?;
+        let abi_alloc = instance
             .exports
-            .get_function(name)
-            .map(|func| HostFunction::new(self.store.clone(), func.clone()))
+            .get_typed_function(&inner_store.as_store_ref(), ABI_ALLOC_NAME)?;
+        drop(inner_store);
+        Ok(Self {
+            store,
+            instance,
+            memory,
+            abi_free,
+            abi_alloc,
+        })
     }
-}
 
-struct HostMemory {
-    store: HostStore,
-    memory: Memory,
-}
+    fn call_impl<T>(
+        &self,
+        mut store: StoreMut,
+        name: &str,
+        data: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        let func = self
+            .instance
+            .exports
+            .get_typed_function::<(i32, i32), u64>(&store, name)?;
 
-impl HostMemory {
-    pub fn new(store: HostStore, memory: Memory) -> Self {
-        Self { store, memory }
-    }
+        let ptr = self.abi_alloc.call(&mut store, data.len() as i32)?;
+        unsafe {
+            mem_slice_mut(&store, &self.memory, ptr, data.len() as i32, |s| {
+                s.copy_from_slice(data)
+            })
+        };
 
-    pub unsafe fn slice<R>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> R) -> R {
-        let store = self.store.lock().unwrap();
-        mem_slice(&store.as_store_ref(), &self.memory, start, len, f)
-    }
+        let res = func.call(&mut store, data.len() as i32, ptr);
 
-    pub unsafe fn slice_mut<R>(&self, start: i32, len: i32, f: impl FnOnce(&mut [u8]) -> R) -> R {
-        let store = self.store.lock().unwrap();
-        mem_slice_mut(&store.as_store_ref(), &self.memory, start, len, f)
-    }
-}
+        self.abi_free.call(&mut store, ptr, data.len() as i32)?;
 
-struct HostFunction {
-    store: HostStore,
-    func: Function,
-}
+        let res = res?;
+        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
 
-impl HostFunction {
-    pub fn new(store: HostStore, func: Function) -> Self {
-        Self { store, func }
-    }
+        let res_data = unsafe { mem_slice(&store, &self.memory, res, len, |s| f(s)) };
 
-    pub fn call(&self, params: &[Value]) -> Result<Box<[Value]>, RuntimeError> {
-        self.func
-            .call(&mut self.store.lock().unwrap().as_store_mut(), params)
-    }
-}
+        self.abi_free.call(&mut store, res, len)?;
 
-/// A Wasmer [`Instance`].
-pub struct WasmerModule {
-    instance: HostInstance,
-    memory: HostMemory,
-    abi_free: HostFunction,
-    abi_alloc: HostFunction,
-}
-
-impl WasmerModule {
-    /// Loads the WASM [`Module`], with some imports.
-    pub(crate) fn new(store: HostStore, instance: Instance) -> Result<Self> {
-        let instance = HostInstance::new(store, instance);
-        let memory = instance.get_memory()?;
-        let abi_free = instance.get_func(ABI_FREE_NAME)?;
-        let abi_alloc = instance.get_func(ABI_ALLOC_NAME)?;
-        Ok(Self {
-            instance,
-            memory,
-            abi_free,
-            abi_alloc,
-        })
+        let res_data = res_data?;
+        Ok(res_data)
     }
 }
 
 impl RawModule for WasmerModule {
-    type Linker = WasmerStoreLinker;
+    type Linker = WasmerLinker;
+
+    type LinkerHandle<'a> = WasmerLinkerHandle<'a>;
 
     type Func = WasmerFunction;
 
     fn call<T>(&self, name: &str, data: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
-        let memory = &self.memory;
-        let func = self.instance.get_func(name)?;
-
-        let ptr = self.abi_alloc.call(&[Value::I32(data.len() as i32)])?[0].unwrap_i32();
-        defer! { self.abi_free.call(&[Value::I32(ptr), Value::I32(data.len() as i32)]).unwrap(); }
-        unsafe { memory.slice_mut(ptr, data.len() as i32, |s| s.copy_from_slice(data)) };
-
-        let res = func.call(&[Value::I32(data.len() as i32), Value::I32(ptr)])?[0].unwrap_i64();
-        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
-        defer! { self.abi_free.call(&[Value::I32(res), Value::I32(len)]).unwrap(); }
-
-        let res_data = unsafe { memory.slice(res, len, |s| f(s)) }?;
-        Ok(res_data)
+        self.call_impl(self.store.lock().unwrap().as_store_mut(), name, data, f)
     }
 }
 
@@ -155,14 +123,18 @@ impl RawModule for WasmerModule {
 #[derive(Clone)]
 pub struct RuntimeInstanceData {
     memory: Option<Memory>,
+    abi_alloc: Option<TypedFunction<i32, i32>>,
     #[allow(clippy::type_complexity)]
-    func: Arc<dyn (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static>,
+    func: Arc<dyn (Fn(WasmerLinkerHandle, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static>,
 }
 
 impl RuntimeInstanceData {
-    pub fn new(func: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static) -> Self {
+    pub fn new(
+        func: impl (Fn(WasmerLinkerHandle, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static,
+    ) -> Self {
         Self {
             memory: None,
+            abi_alloc: None,
             func: Arc::new(func),
         }
     }
@@ -175,40 +147,51 @@ impl RuntimeInstanceData {
         self.memory.as_ref().unwrap()
     }
 
-    pub fn call(this: FunctionEnvMut<Self>, len: i32, ptr: i32) {
-        let memory = this.data().memory();
+    pub fn set_abi_alloc(&mut self, func: TypedFunction<i32, i32>) {
+        self.abi_alloc = Some(func);
+    }
+
+    pub fn call(mut this: FunctionEnvMut<Self>, len: i32, ptr: i32) -> u64 {
         unsafe {
-            mem_slice(&this, memory, ptr, len, |slice| {
-                (this.data().func)(slice).unwrap();
-            })
-        };
+            let memory = this.data().memory().clone();
+            let data = {
+                let func = this.data().func.clone();
+                let handle = WasmerLinkerHandle {
+                    store: this.as_store_mut(),
+                    memory: memory.clone(),
+                };
+                (func)(handle, ptr, len).unwrap()
+            };
+            let abi_alloc = this.data().abi_alloc.clone().unwrap();
+            let ptr = abi_alloc.call(&mut this, data.len() as i32).unwrap();
+            mem_slice_mut(&this, &memory, ptr, data.len() as i32, |slice| {
+                slice.copy_from_slice(&data);
+            });
+            ((data.len() as u64) << 32) | (ptr as u64)
+        }
     }
 }
 
-/// A Wasmer [`Store`] with some [`ImportObject`]s.
-pub struct WasmerStoreLinker {
+/// A Wasmer [`Store`] with some imports.
+pub struct WasmerLinker {
     store: HostStore,
     wasi_env: WasiEnv,
     imports: HashMap<String, HashMap<String, WasmerFunction>>,
 }
 
-impl WasmerStoreLinker {
+impl WasmerLinker {
     fn wrap_impl(
         store: &mut impl AsStoreMut,
         func: WasmerFunction,
     ) -> (Function, Option<FunctionEnv<RuntimeInstanceData>>) {
-        match func {
-            WasmerFunction::Function(func) => (func, None),
-            WasmerFunction::WithEnv(env_data) => {
-                let env = FunctionEnv::new(store, env_data);
-                let func = Function::new_typed_with_env(store, &env, RuntimeInstanceData::call);
-                (func, Some(env))
-            }
-        }
+        let env_data = func.0;
+        let env = FunctionEnv::new(store, env_data);
+        let func = Function::new_typed_with_env(store, &env, RuntimeInstanceData::call);
+        (func, Some(env))
     }
 }
 
-impl StoreLinker<WasmerModule> for WasmerStoreLinker {
+impl Linker<WasmerModule> for WasmerLinker {
     fn new(root_path: impl AsRef<Path>) -> Result<Self> {
         let store = Store::default();
         let wasi_state = WasiState::new("ayaka-plugin-wasmer")
@@ -255,9 +238,14 @@ impl StoreLinker<WasmerModule> for WasmerStoreLinker {
             let instance = Instance::new(&mut store.as_store_mut(), &module, &wasi_import)?;
             wasi_env.initialize(&mut store.as_store_mut(), &instance)?;
             let memory = instance.exports.get_memory(MEMORY_NAME)?;
+            let abi_alloc = instance
+                .exports
+                .get_typed_function(&store.as_store_ref(), ABI_ALLOC_NAME)?;
+            let mut store = store.as_store_mut();
             for env in &envs {
-                env.as_mut(&mut store.as_store_mut())
-                    .set_memory(memory.clone());
+                let env_mut = env.as_mut(&mut store);
+                env_mut.set_memory(memory.clone());
+                env_mut.set_abi_alloc(abi_alloc.clone());
             }
             instance
         };
@@ -274,24 +262,36 @@ impl StoreLinker<WasmerModule> for WasmerStoreLinker {
         Ok(())
     }
 
-    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> WasmerFunction {
-        WasmerFunction::Function(Function::new_typed(
-            &mut self.store.lock().unwrap().as_store_mut(),
-            f,
-        ))
-    }
-
-    fn wrap_with_args_raw(
+    fn wrap_raw(
         &self,
-        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+        f: impl (Fn(WasmerLinkerHandle, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static,
     ) -> WasmerFunction {
-        WasmerFunction::WithEnv(RuntimeInstanceData::new(f))
+        WasmerFunction(RuntimeInstanceData::new(f))
     }
 }
 
-#[doc(hidden)]
+/// Represents a wrapped wasmer function.
 #[derive(Clone)]
-pub enum WasmerFunction {
-    Function(Function),
-    WithEnv(RuntimeInstanceData),
+pub struct WasmerFunction(RuntimeInstanceData);
+
+/// A Wasmer [`StoreMut`].
+pub struct WasmerLinkerHandle<'a> {
+    store: StoreMut<'a>,
+    memory: Memory,
+}
+
+impl<'a> LinkerHandle<'a, WasmerModule> for WasmerLinkerHandle<'a> {
+    fn call<T>(
+        &mut self,
+        m: &WasmerModule,
+        name: &str,
+        data: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        m.call_impl(self.store.as_store_mut(), name, data, f)
+    }
+
+    fn slice<T>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> T) -> T {
+        unsafe { mem_slice(&self.store.as_store_ref(), &self.memory, start, len, f) }
+    }
 }
diff --git a/utils/ayaka-plugin-wasmi/Cargo.toml b/utils/ayaka-plugin-wasmi/Cargo.toml
index bcd01f0c..97f8172e 100644
--- a/utils/ayaka-plugin-wasmi/Cargo.toml
+++ b/utils/ayaka-plugin-wasmi/Cargo.toml
@@ -7,4 +7,3 @@ edition = "2021"
 ayaka-plugin = { path = "../ayaka-plugin" }
 wasmi = { git = "https://github.com/paritytech/wasmi.git" }
 wasmi_wasi = { git = "https://github.com/paritytech/wasmi.git" }
-scopeguard = "1.1"
diff --git a/utils/ayaka-plugin-wasmi/src/lib.rs b/utils/ayaka-plugin-wasmi/src/lib.rs
index dba3d98b..66909044 100644
--- a/utils/ayaka-plugin-wasmi/src/lib.rs
+++ b/utils/ayaka-plugin-wasmi/src/lib.rs
@@ -3,7 +3,6 @@
 #![warn(missing_docs)]
 
 use ayaka_plugin::*;
-use scopeguard::defer;
 use std::{
     collections::HashMap,
     path::Path,
@@ -38,137 +37,106 @@ unsafe fn mem_slice_mut<'a, T: 'a>(
 
 type HostStore = Arc<Mutex<Store<WasiCtx>>>;
 
-struct HostInstance {
+/// A Wasmi [`Instance`].
+pub struct WasmiModule {
     store: HostStore,
     instance: Instance,
+    memory: Memory,
+    abi_free: TypedFunc<(i32, i32), ()>,
+    abi_alloc: TypedFunc<i32, i32>,
 }
 
-impl HostInstance {
-    pub fn new(store: HostStore, instance: Instance) -> Self {
-        Self { store, instance }
-    }
-
-    pub fn get_memory(&self) -> Option<HostMemory> {
-        self.instance
-            .get_export(self.store.lock().unwrap().as_context_mut(), MEMORY_NAME)
-            .map(|mem| HostMemory::new(self.store.clone(), mem.into_memory().unwrap()))
+impl WasmiModule {
+    fn new(store: HostStore, module: &Module, linker: &wasmi::Linker<WasiCtx>) -> Result<Self> {
+        let mut inner_store = store.lock().unwrap();
+        let instance = linker
+            .instantiate(inner_store.as_context_mut(), module)?
+            .start(inner_store.as_context_mut())?;
+        let memory = instance
+            .get_export(inner_store.as_context(), MEMORY_NAME)
+            .unwrap()
+            .into_memory()
+            .unwrap();
+        let abi_free = instance
+            .get_export(inner_store.as_context(), ABI_FREE_NAME)
+            .unwrap()
+            .into_func()
+            .unwrap()
+            .typed(inner_store.as_context())?;
+        let abi_alloc = instance
+            .get_export(inner_store.as_context_mut(), ABI_ALLOC_NAME)
+            .unwrap()
+            .into_func()
+            .unwrap()
+            .typed(inner_store.as_context())?;
+        drop(inner_store);
+        Ok(Self {
+            store,
+            instance,
+            memory,
+            abi_free,
+            abi_alloc,
+        })
     }
 
-    pub fn get_typed_func<Params: WasmParams, Results: WasmResults>(
+    fn call_impl<T>(
         &self,
+        mut store: StoreContextMut<WasiCtx>,
         name: &str,
-    ) -> Result<HostTypedFunc<Params, Results>, wasmi::Error> {
-        let func = {
-            let mut store = self.store.lock().unwrap();
-            self.instance
-                .get_export(store.as_context_mut(), name)
-                .unwrap()
-                .into_func()
-                .unwrap()
-                .typed(store.as_context())?
+        data: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        let func = self
+            .instance
+            .get_export(&store, name)
+            .unwrap()
+            .into_func()
+            .unwrap()
+            .typed::<(i32, i32), u64>(&store)?;
+
+        let ptr = self.abi_alloc.call(&mut store, data.len() as i32)?;
+        unsafe {
+            mem_slice_mut(&mut store, &self.memory, ptr, data.len() as i32).copy_from_slice(data)
         };
-        Ok(HostTypedFunc::new(self.store.clone(), func))
-    }
-}
-
-struct HostMemory {
-    store: HostStore,
-    memory: Memory,
-}
-
-impl HostMemory {
-    pub fn new(store: HostStore, memory: Memory) -> Self {
-        Self { store, memory }
-    }
 
-    pub unsafe fn slice<R>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> R) -> R {
-        let store = self.store.lock().unwrap();
-        let data = mem_slice(store.as_context(), &self.memory, start, len);
-        f(data)
-    }
+        let res = func.call(&mut store, (data.len() as i32, ptr));
 
-    pub unsafe fn slice_mut<R>(&self, start: i32, len: i32, f: impl FnOnce(&mut [u8]) -> R) -> R {
-        let mut store = self.store.lock().unwrap();
-        let data = mem_slice_mut(store.as_context_mut(), &self.memory, start, len);
-        f(data)
-    }
-}
+        self.abi_free.call(&mut store, (ptr, data.len() as i32))?;
 
-struct HostTypedFunc<Params, Results> {
-    store: HostStore,
-    func: TypedFunc<Params, Results>,
-}
+        let res = res?;
+        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
 
-impl<Params: WasmParams, Results: WasmResults> HostTypedFunc<Params, Results> {
-    pub fn new(store: HostStore, func: TypedFunc<Params, Results>) -> Self {
-        Self { store, func }
-    }
+        let res_data = unsafe { mem_slice(&store, &self.memory, res, len) };
 
-    pub fn call(&self, params: Params) -> Result<Results, Trap> {
-        self.func
-            .call(self.store.lock().unwrap().as_context_mut(), params)
-    }
-}
+        let res_data = f(res_data);
 
-/// A Wasmi [`Instance`].
-pub struct WasmiModule {
-    instance: HostInstance,
-    memory: HostMemory,
-    abi_free: HostTypedFunc<(i32, i32), ()>,
-    abi_alloc: HostTypedFunc<i32, i32>,
-}
+        self.abi_free.call(&mut store, (res, len))?;
 
-impl WasmiModule {
-    pub(crate) fn new(store: HostStore, module: &Module, linker: &Linker<WasiCtx>) -> Result<Self> {
-        let instance = {
-            let mut store = store.lock().unwrap();
-            linker
-                .instantiate(store.as_context_mut(), module)?
-                .start(store.as_context_mut())?
-        };
-        let instance = HostInstance::new(store, instance);
-        let memory = instance.get_memory().unwrap();
-        let abi_free = instance.get_typed_func(ABI_FREE_NAME)?;
-        let abi_alloc = instance.get_typed_func(ABI_ALLOC_NAME)?;
-        Ok(Self {
-            instance,
-            memory,
-            abi_free,
-            abi_alloc,
-        })
+        let res_data = res_data?;
+        Ok(res_data)
     }
 }
 
 impl RawModule for WasmiModule {
-    type Linker = WasmiStoreLinker;
+    type Linker = WasmiLinker;
+
+    type LinkerHandle<'a> = WasmiLinkerHandle<'a>;
 
     type Func = Func;
 
     fn call<T>(&self, name: &str, data: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
-        let memory = &self.memory;
-        let func = self.instance.get_typed_func::<(i32, i32), u64>(name)?;
-
-        let ptr = self.abi_alloc.call(data.len() as i32)?;
-        defer! { self.abi_free.call((ptr, data.len() as i32)).unwrap(); }
-        unsafe { memory.slice_mut(ptr, data.len() as i32, |s| s.copy_from_slice(data)) };
-
-        let res = func.call((data.len() as i32, ptr))?;
-        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
-        defer! { self.abi_free.call((res, len)).unwrap(); }
-
-        let res_data = unsafe { memory.slice(res, len, |s| f(s)) }?;
-        Ok(res_data)
+        self.call_impl(self.store.lock().unwrap().as_context_mut(), name, data, f)
     }
 }
 
 /// A Wasmi [`Store`] with [`Linker`].
-pub struct WasmiStoreLinker {
+pub struct WasmiLinker {
     engine: Engine,
     store: HostStore,
-    linker: Linker<WasiCtx>,
+    linker: wasmi::Linker<WasiCtx>,
 }
 
-impl WasmiStoreLinker {
+impl WasmiLinker {
     fn preopen_root(root_path: impl AsRef<Path>) -> Result<Dir> {
         let mut options = std::fs::OpenOptions::new();
         options.read(true);
@@ -183,7 +151,7 @@ impl WasmiStoreLinker {
     }
 }
 
-impl StoreLinker<WasmiModule> for WasmiStoreLinker {
+impl ayaka_plugin::Linker<WasmiModule> for WasmiLinker {
     fn new(root_path: impl AsRef<Path>) -> Result<Self> {
         let engine = Engine::default();
         let wasi = WasiCtxBuilder::new()
@@ -191,7 +159,7 @@ impl StoreLinker<WasmiModule> for WasmiStoreLinker {
             .preopened_dir(Self::preopen_root(root_path)?, "/")?
             .build();
         let mut store = Store::new(&engine, wasi);
-        let mut linker = Linker::new();
+        let mut linker = wasmi::Linker::new();
         define_wasi(&mut linker, &mut store, |ctx| ctx)?;
         Ok(Self {
             engine,
@@ -214,26 +182,57 @@ impl StoreLinker<WasmiModule> for WasmiStoreLinker {
         Ok(())
     }
 
-    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> Func {
-        Func::wrap(self.store.lock().unwrap().as_context_mut(), f)
-    }
-
-    fn wrap_with_args_raw(
+    fn wrap_raw(
         &self,
-        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+        f: impl (Fn(WasmiLinkerHandle, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static,
     ) -> Func {
         Func::wrap(
             self.store.lock().unwrap().as_context_mut(),
-            move |store: Caller<WasiCtx>, len: i32, data: i32| unsafe {
+            move |mut store: Caller<WasiCtx>, len: i32, data: i32| unsafe {
                 let memory = store
                     .get_export(MEMORY_NAME)
                     .unwrap()
                     .into_memory()
                     .unwrap();
-                let data = mem_slice(store.as_context(), &memory, data, len);
-                f(data).map_err(|e| Trap::new(e.to_string()))?;
-                Ok(())
+                let data = {
+                    let store = store.as_context_mut();
+                    let handle = WasmiLinkerHandle { store, memory };
+                    f(handle, data, len).map_err(|e| Trap::new(e.to_string()))?
+                };
+                let abi_alloc = store
+                    .get_export(ABI_ALLOC_NAME)
+                    .unwrap()
+                    .into_func()
+                    .unwrap()
+                    .typed::<i32, i32>(store.as_context())
+                    .map_err(|e| Trap::new(e.to_string()))?;
+                let ptr = abi_alloc.call(store.as_context_mut(), data.len() as i32)?;
+                mem_slice_mut(store.as_context_mut(), &memory, ptr, data.len() as i32)
+                    .copy_from_slice(&data);
+                Ok(((data.len() as u64) << 32) | (ptr as u64))
             },
         )
     }
 }
+
+/// A Wasmi [`StoreContextMut`].
+pub struct WasmiLinkerHandle<'a> {
+    store: StoreContextMut<'a, WasiCtx>,
+    memory: Memory,
+}
+
+impl<'a> LinkerHandle<'a, WasmiModule> for WasmiLinkerHandle<'a> {
+    fn call<T>(
+        &mut self,
+        m: &WasmiModule,
+        name: &str,
+        data: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        m.call_impl(self.store.as_context_mut(), name, data, f)
+    }
+
+    fn slice<T>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> T) -> T {
+        f(unsafe { mem_slice(self.store.as_context(), &self.memory, start, len) })
+    }
+}
diff --git a/utils/ayaka-plugin-wasmtime/Cargo.toml b/utils/ayaka-plugin-wasmtime/Cargo.toml
index 97632aa4..f174af37 100644
--- a/utils/ayaka-plugin-wasmtime/Cargo.toml
+++ b/utils/ayaka-plugin-wasmtime/Cargo.toml
@@ -7,4 +7,3 @@ edition = "2021"
 ayaka-plugin = { path = "../ayaka-plugin" }
 wasmtime = "2.0"
 wasmtime-wasi = "2.0"
-scopeguard = "1.1"
diff --git a/utils/ayaka-plugin-wasmtime/src/lib.rs b/utils/ayaka-plugin-wasmtime/src/lib.rs
index f023bc53..638ec019 100644
--- a/utils/ayaka-plugin-wasmtime/src/lib.rs
+++ b/utils/ayaka-plugin-wasmtime/src/lib.rs
@@ -3,7 +3,6 @@
 #![warn(missing_docs)]
 
 use ayaka_plugin::*;
-use scopeguard::defer;
 use std::{
     collections::HashMap,
     path::Path,
@@ -38,125 +37,88 @@ unsafe fn mem_slice_mut<'a, T: 'a>(
 
 type HostStore = Arc<Mutex<Store<WasiCtx>>>;
 
-struct HostInstance {
+/// A Wasmtime [`Instance`].
+pub struct WasmtimeModule {
     store: HostStore,
     instance: Instance,
+    memory: Memory,
+    abi_free: TypedFunc<(i32, i32), ()>,
+    abi_alloc: TypedFunc<i32, i32>,
 }
 
-impl HostInstance {
-    pub fn new(store: HostStore, instance: Instance) -> Self {
-        Self { store, instance }
-    }
-
-    pub fn get_memory(&self) -> Option<HostMemory> {
-        self.instance
-            .get_memory(self.store.lock().unwrap().as_context_mut(), MEMORY_NAME)
-            .map(|mem| HostMemory::new(self.store.clone(), mem))
+impl WasmtimeModule {
+    fn new(store: HostStore, module: &Module, linker: &wasmtime::Linker<WasiCtx>) -> Result<Self> {
+        let mut inner_store = store.lock().unwrap();
+        let instance = linker.instantiate(inner_store.as_context_mut(), module)?;
+        let memory = instance
+            .get_memory(inner_store.as_context_mut(), MEMORY_NAME)
+            .unwrap();
+        let abi_free = instance.get_typed_func(inner_store.as_context_mut(), ABI_FREE_NAME)?;
+        let abi_alloc = instance.get_typed_func(inner_store.as_context_mut(), ABI_ALLOC_NAME)?;
+        drop(inner_store);
+        Ok(Self {
+            store,
+            instance,
+            memory,
+            abi_free,
+            abi_alloc,
+        })
     }
 
-    pub fn get_typed_func<Params: WasmParams, Results: WasmResults>(
+    fn call_impl<T>(
         &self,
+        mut store: StoreContextMut<WasiCtx>,
         name: &str,
-    ) -> Result<HostTypedFunc<Params, Results>> {
-        self.instance
-            .get_typed_func(self.store.lock().unwrap().as_context_mut(), name)
-            .map(|func| HostTypedFunc::new(self.store.clone(), func))
-    }
-}
+        data: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        let func = self
+            .instance
+            .get_typed_func::<(i32, i32), u64, _>(&mut store, name)?;
 
-struct HostMemory {
-    store: HostStore,
-    memory: Memory,
-}
+        let ptr = self.abi_alloc.call(&mut store, data.len() as i32)?;
+        unsafe {
+            mem_slice_mut(&mut store, &self.memory, ptr, data.len() as i32).copy_from_slice(data)
+        };
 
-impl HostMemory {
-    pub fn new(store: HostStore, memory: Memory) -> Self {
-        Self { store, memory }
-    }
+        let res = func.call(&mut store, (data.len() as i32, ptr));
 
-    pub unsafe fn slice<R>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> R) -> R {
-        let store = self.store.lock().unwrap();
-        let data = mem_slice(store.as_context(), &self.memory, start, len);
-        f(data)
-    }
-
-    pub unsafe fn slice_mut<R>(&self, start: i32, len: i32, f: impl FnOnce(&mut [u8]) -> R) -> R {
-        let mut store = self.store.lock().unwrap();
-        let data = mem_slice_mut(store.as_context_mut(), &self.memory, start, len);
-        f(data)
-    }
-}
+        self.abi_free.call(&mut store, (ptr, data.len() as i32))?;
 
-struct HostTypedFunc<Params, Results> {
-    store: HostStore,
-    func: TypedFunc<Params, Results>,
-}
+        let res = res?;
+        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
 
-impl<Params: WasmParams, Results: WasmResults> HostTypedFunc<Params, Results> {
-    pub fn new(store: HostStore, func: TypedFunc<Params, Results>) -> Self {
-        Self { store, func }
-    }
+        let res_data = unsafe { mem_slice(&store, &self.memory, res, len) };
 
-    pub fn call(&self, params: Params) -> Result<Results, Trap> {
-        self.func
-            .call(self.store.lock().unwrap().as_context_mut(), params)
-    }
-}
+        let res_data = f(res_data);
 
-/// A Wasmtime [`Instance`].
-pub struct WasmtimeModule {
-    instance: HostInstance,
-    memory: HostMemory,
-    abi_free: HostTypedFunc<(i32, i32), ()>,
-    abi_alloc: HostTypedFunc<i32, i32>,
-}
+        self.abi_free.call(&mut store, (res, len))?;
 
-impl WasmtimeModule {
-    pub(crate) fn new(store: HostStore, module: &Module, linker: &Linker<WasiCtx>) -> Result<Self> {
-        let instance = linker.instantiate(store.lock().unwrap().as_context_mut(), module)?;
-        let instance = HostInstance::new(store, instance);
-        let memory = instance.get_memory().unwrap();
-        let abi_free = instance.get_typed_func(ABI_FREE_NAME)?;
-        let abi_alloc = instance.get_typed_func(ABI_ALLOC_NAME)?;
-        Ok(Self {
-            instance,
-            memory,
-            abi_free,
-            abi_alloc,
-        })
+        let res_data = res_data?;
+        Ok(res_data)
     }
 }
 
 impl RawModule for WasmtimeModule {
-    type Linker = WasmtimeStoreLinker;
+    type Linker = WasmtimeLinker;
+
+    type LinkerHandle<'a> = WasmtimeLinkerHandle<'a>;
 
     type Func = Func;
 
     fn call<T>(&self, name: &str, data: &[u8], f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
-        let memory = &self.memory;
-        let func = self.instance.get_typed_func::<(i32, i32), u64>(name)?;
-
-        let ptr = self.abi_alloc.call(data.len() as i32)?;
-        defer! { self.abi_free.call((ptr, data.len() as i32)).unwrap(); }
-        unsafe { memory.slice_mut(ptr, data.len() as i32, |s| s.copy_from_slice(data)) };
-
-        let res = func.call((data.len() as i32, ptr))?;
-        let (len, res) = ((res >> 32) as i32, (res & 0xFFFFFFFF) as i32);
-        defer! { self.abi_free.call((res, len)).unwrap(); }
-
-        let res_data = unsafe { memory.slice(res, len, |s| f(s)) }?;
-        Ok(res_data)
+        self.call_impl(self.store.lock().unwrap().as_context_mut(), name, data, f)
     }
 }
 
 /// A Wasmtime [`Store`] with [`Linker`].
-pub struct WasmtimeStoreLinker {
+pub struct WasmtimeLinker {
     engine: Engine,
     store: HostStore,
-    linker: Linker<WasiCtx>,
+    linker: wasmtime::Linker<WasiCtx>,
 }
 
-impl WasmtimeStoreLinker {
+impl WasmtimeLinker {
     fn preopen_root(root_path: impl AsRef<Path>) -> Result<Dir> {
         let mut options = std::fs::OpenOptions::new();
         options.read(true);
@@ -171,7 +133,7 @@ impl WasmtimeStoreLinker {
     }
 }
 
-impl StoreLinker<WasmtimeModule> for WasmtimeStoreLinker {
+impl ayaka_plugin::Linker<WasmtimeModule> for WasmtimeLinker {
     fn new(root_path: impl AsRef<Path>) -> Result<Self> {
         let engine = Engine::default();
         let wasi = WasiCtxBuilder::new()
@@ -179,7 +141,7 @@ impl StoreLinker<WasmtimeModule> for WasmtimeStoreLinker {
             .preopened_dir(Self::preopen_root(root_path)?, "/")?
             .build();
         let store = Store::new(&engine, wasi);
-        let mut linker = Linker::new(&engine);
+        let mut linker = wasmtime::Linker::new(&engine);
         add_to_linker(&mut linker, |ctx| ctx)?;
         Ok(Self {
             engine,
@@ -202,13 +164,9 @@ impl StoreLinker<WasmtimeModule> for WasmtimeStoreLinker {
         Ok(())
     }
 
-    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> Func {
-        Func::wrap(self.store.lock().unwrap().as_context_mut(), f)
-    }
-
-    fn wrap_with_args_raw(
+    fn wrap_raw(
         &self,
-        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+        f: impl (Fn(WasmtimeLinkerHandle<'_>, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static,
     ) -> Func {
         Func::wrap(
             self.store.lock().unwrap().as_context_mut(),
@@ -218,10 +176,44 @@ impl StoreLinker<WasmtimeModule> for WasmtimeStoreLinker {
                     .unwrap()
                     .into_memory()
                     .unwrap();
-                let data = mem_slice(store.as_context(), &memory, data, len);
-                f(data).map_err(|e| Trap::new(e.to_string()))?;
-                Ok(())
+                let data = {
+                    let store = store.as_context_mut();
+                    let handle = WasmtimeLinkerHandle { store, memory };
+                    f(handle, data, len).map_err(|e| Trap::new(e.to_string()))?
+                };
+                let abi_alloc = store
+                    .get_export(ABI_ALLOC_NAME)
+                    .unwrap()
+                    .into_func()
+                    .unwrap()
+                    .typed::<i32, i32, _>(store.as_context())?;
+                let ptr = abi_alloc.call(store.as_context_mut(), data.len() as i32)?;
+                mem_slice_mut(store.as_context_mut(), &memory, ptr, data.len() as i32)
+                    .copy_from_slice(&data);
+                Ok(((data.len() as u64) << 32) | (ptr as u64))
             },
         )
     }
 }
+
+/// A Wasmtime [`StoreContextMut`].
+pub struct WasmtimeLinkerHandle<'a> {
+    store: StoreContextMut<'a, WasiCtx>,
+    memory: Memory,
+}
+
+impl<'a> LinkerHandle<'a, WasmtimeModule> for WasmtimeLinkerHandle<'a> {
+    fn call<T>(
+        &mut self,
+        m: &WasmtimeModule,
+        name: &str,
+        data: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T> {
+        m.call_impl(self.store.as_context_mut(), name, data, f)
+    }
+
+    fn slice<T>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> T) -> T {
+        f(unsafe { mem_slice(self.store.as_context(), &self.memory, start, len) })
+    }
+}
diff --git a/utils/ayaka-plugin/src/lib.rs b/utils/ayaka-plugin/src/lib.rs
index 2e76fc55..dee24662 100644
--- a/utils/ayaka-plugin/src/lib.rs
+++ b/utils/ayaka-plugin/src/lib.rs
@@ -3,9 +3,7 @@
 //! This crate provides abstract types and traits
 //! for different plugin backends.
 
-#![feature(fn_traits)]
 #![feature(tuple_trait)]
-#![feature(unboxed_closures)]
 #![warn(missing_docs)]
 #![deny(unsafe_code)]
 
@@ -24,7 +22,10 @@ pub const MEMORY_NAME: &str = "memory";
 /// Represents a raw plugin module.
 pub trait RawModule: Sized {
     /// The linker type that can create raw module.
-    type Linker: StoreLinker<Self>;
+    type Linker: Linker<Self>;
+
+    /// The linker handle type.
+    type LinkerHandle<'a>: LinkerHandle<'a, Self>;
 
     /// The import function type.
     type Func;
@@ -56,12 +57,15 @@ impl<M: RawModule> PluginModule<M> {
             Ok(res)
         })
     }
+
+    /// Get inner raw module.
+    pub fn inner(&self) -> &M {
+        &self.module
+    }
 }
 
-/// Represents the store & linker of plugin modules.
-pub trait StoreLinker<M: RawModule>: Sized {
-    /// Creates a new instance of [`StoreLinker`].
-    ///
+/// Represents the linker of plugin modules.
+pub trait Linker<M: RawModule>: Sized {
     /// The `root_path` is used to preopen the root dir,
     /// and mapped to `/`.
     fn new(root_path: impl AsRef<Path>) -> Result<Self>;
@@ -72,24 +76,53 @@ pub trait StoreLinker<M: RawModule>: Sized {
     /// Import functions by namespace and names.
     fn import(&mut self, ns: impl Into<String>, funcs: HashMap<String, M::Func>) -> Result<()>;
 
-    /// Wrap a simple function.
-    fn wrap(&self, f: impl Fn() + Send + Sync + 'static) -> M::Func;
-
     /// Wrap a function with args in bytes.
-    fn wrap_with_args_raw(
+    fn wrap_raw(
         &self,
-        f: impl (Fn(&[u8]) -> Result<()>) + Send + Sync + 'static,
+        f: impl (Fn(M::LinkerHandle<'_>, i32, i32) -> Result<Vec<u8>>) + Send + Sync + 'static,
     ) -> M::Func;
 
     /// Wrap a function with args.
-    fn wrap_with_args<Params: DeserializeOwned + Tuple>(
+    fn wrap<P: DeserializeOwned + Tuple, R: Serialize>(
+        &self,
+        f: impl (Fn(P) -> Result<R>) + Send + Sync + 'static,
+    ) -> M::Func {
+        self.wrap_raw(move |handle, start, len| {
+            let data = handle.slice(start, len, |data| rmp_serde::from_slice(data))?;
+            let data = f(data)?;
+            let data = rmp_serde::to_vec(&data)?;
+            Ok(data)
+        })
+    }
+
+    /// Wrap a function with args and linker handle.
+    fn wrap_with<P: DeserializeOwned + Tuple, R: Serialize>(
         &self,
-        f: impl Fn<Params, Output = ()> + Send + Sync + 'static,
+        f: impl (Fn(M::LinkerHandle<'_>, P) -> Result<R>) + Send + Sync + 'static,
     ) -> M::Func {
-        self.wrap_with_args_raw(move |data| {
-            let data = rmp_serde::from_slice(data)?;
-            f.call(data);
-            Ok(())
+        self.wrap_raw(move |handle, start, len| {
+            let data = handle.slice(start, len, |data| rmp_serde::from_slice(data))?;
+            let data = f(handle, data)?;
+            let data = rmp_serde::to_vec(&data)?;
+            Ok(data)
         })
     }
 }
+
+/// Represents a handle of linker.
+///
+/// Usually it is not easy to call function inside host functions.
+/// A handle provides methods to do so.
+pub trait LinkerHandle<'a, M: RawModule> {
+    /// Call methods of a module.
+    fn call<T>(
+        &mut self,
+        m: &M,
+        name: &str,
+        args: &[u8],
+        f: impl FnOnce(&[u8]) -> Result<T>,
+    ) -> Result<T>;
+
+    /// Get memory slice.
+    fn slice<T>(&self, start: i32, len: i32, f: impl FnOnce(&[u8]) -> T) -> T;
+}
diff --git a/utils/ayaka-primitive/Cargo.toml b/utils/ayaka-primitive/Cargo.toml
new file mode 100644
index 00000000..795fadd8
--- /dev/null
+++ b/utils/ayaka-primitive/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "ayaka-primitive"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_with = "2.0"
+regex = "1"
+
+[dev-dependencies]
+serde_yaml = "0.9"
diff --git a/utils/ayaka-primitive/src/lib.rs b/utils/ayaka-primitive/src/lib.rs
new file mode 100644
index 00000000..bdb7fc65
--- /dev/null
+++ b/utils/ayaka-primitive/src/lib.rs
@@ -0,0 +1,14 @@
+//! Primitive types.
+
+#![warn(missing_docs)]
+#![deny(unsafe_code)]
+#![feature(iterator_try_collect)]
+#![feature(once_cell)]
+
+mod line;
+mod raw_value;
+mod text;
+
+pub use line::*;
+pub use raw_value::*;
+pub use text::*;
diff --git a/utils/ayaka-primitive/src/line.rs b/utils/ayaka-primitive/src/line.rs
new file mode 100644
index 00000000..0552847b
--- /dev/null
+++ b/utils/ayaka-primitive/src/line.rs
@@ -0,0 +1,66 @@
+use crate::*;
+use serde::Deserialize;
+use serde_with::rust::maps_duplicate_key_is_error;
+use std::collections::HashMap;
+
+/// Represents a line in a prograph.
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[serde(untagged)]
+pub enum Line {
+    /// An empty line, usually fallbacks to the base language one.
+    Empty,
+    /// A text line.
+    Text(Text),
+    /// Some `switches`.
+    Switch {
+        /// The switch items.
+        switches: Vec<String>,
+    },
+    /// Custom line types.
+    #[serde(with = "maps_duplicate_key_is_error")]
+    Custom(HashMap<String, RawValue>),
+}
+
+#[cfg(test)]
+mod test {
+    use std::collections::HashMap;
+
+    use crate::*;
+
+    #[test]
+    fn parse() {
+        let lines = r#"
+- abc
+- exec: $a
+- switches:
+  - a
+  - b
+- video: 0
+-
+        "#;
+        let lines: Vec<Line> = serde_yaml::from_str(lines).unwrap();
+        assert_eq!(lines.len(), 5);
+        assert_eq!(
+            lines[0],
+            Line::Text(Text(vec![SubText::Str("abc".to_string())]))
+        );
+        assert_eq!(
+            lines[1],
+            Line::Custom(HashMap::from([(
+                "exec".to_string(),
+                RawValue::Str("$a".to_string())
+            )]))
+        );
+        assert_eq!(
+            lines[2],
+            Line::Switch {
+                switches: vec!["a".to_string(), "b".to_string()]
+            }
+        );
+        assert_eq!(
+            lines[3],
+            Line::Custom(HashMap::from([("video".to_string(), RawValue::Num(0))]))
+        );
+        assert_eq!(lines[4], Line::Empty);
+    }
+}
diff --git a/utils/ayaka-script/src/primitive.rs b/utils/ayaka-primitive/src/raw_value.rs
similarity index 94%
rename from utils/ayaka-script/src/primitive.rs
rename to utils/ayaka-primitive/src/raw_value.rs
index 538919c9..757e4cef 100644
--- a/utils/ayaka-script/src/primitive.rs
+++ b/utils/ayaka-primitive/src/raw_value.rs
@@ -3,16 +3,17 @@ use std::borrow::Cow;
 
 /// The basic and only type used in scripts.
 /// ```
-/// # use ayaka_script::RawValue;
+/// # use ayaka_primitive::RawValue;
 /// assert_eq!(serde_yaml::from_str::<RawValue>("~").unwrap(), RawValue::Unit);
 /// assert_eq!(serde_yaml::from_str::<RawValue>("true").unwrap(), RawValue::Bool(true));
 /// assert_eq!(serde_yaml::from_str::<RawValue>("123").unwrap(), RawValue::Num(123));
 /// assert_eq!(serde_yaml::from_str::<RawValue>("\"hello\"").unwrap(), RawValue::Str("hello".to_string()));
 /// ```
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
 #[serde(untagged)]
 pub enum RawValue {
     /// The unit type. It is empty, just like [`None`] or [`()`] in Rust.
+    #[default]
     Unit,
     /// The boolean type.
     Bool(bool),
@@ -35,12 +36,6 @@ pub enum ValueType {
     Str,
 }
 
-impl Default for RawValue {
-    fn default() -> Self {
-        Self::Unit
-    }
-}
-
 impl RawValue {
     /// Gets [`ValueType`].
     pub fn get_type(&self) -> ValueType {
@@ -58,7 +53,7 @@ impl RawValue {
     /// * A [`RawValue::Str`] converts to `false` if and only if it's empty.
     ///
     /// ```
-    /// # use ayaka_script::RawValue;
+    /// # use ayaka_primitive::RawValue;
     /// let unit_value = RawValue::Unit;
     /// assert!(!unit_value.get_bool());
     /// let num_value = RawValue::Num(123);
@@ -83,7 +78,7 @@ impl RawValue {
     /// * A [`RawValue::Str`] converts to the length of the string.
     ///
     /// ```
-    /// # use ayaka_script::RawValue;
+    /// # use ayaka_primitive::RawValue;
     /// let unit_value = RawValue::Unit;
     /// assert_eq!(unit_value.get_num(), 0);
     /// let bool_value = RawValue::Bool(true);
@@ -108,7 +103,7 @@ impl RawValue {
     /// Be careful to use `get_str().into_owned()`, if possible, use `into_str()` instead.
     ///
     /// ```
-    /// # use ayaka_script::RawValue;
+    /// # use ayaka_primitive::RawValue;
     /// let unit_value = RawValue::Unit;
     /// assert_eq!(unit_value.get_str(), "");
     /// let bool_value = RawValue::Bool(true);
diff --git a/utils/ayaka-script/src/text.rs b/utils/ayaka-primitive/src/text.rs
similarity index 99%
rename from utils/ayaka-script/src/text.rs
rename to utils/ayaka-primitive/src/text.rs
index 95a40812..fc55ede7 100644
--- a/utils/ayaka-script/src/text.rs
+++ b/utils/ayaka-primitive/src/text.rs
@@ -55,7 +55,7 @@ impl Loc {
     /// Combines a series of [`Loc`].
     ///
     /// ```
-    /// # use ayaka_script::Loc;
+    /// # use ayaka_primitive::Loc;
     /// let full_loc = Loc::from_locs([Loc(1, 2), Loc(4, 6), Loc(5, 8)]);
     /// assert_eq!(full_loc, Loc(1, 8));
     /// ```
@@ -205,8 +205,6 @@ pub enum ParseErrorType {
     CmdInCmd,
     /// The builtin commands check the parameters count.
     InvalidParamsCount(String, usize),
-    /// An error occurred when parsing [`Program`].
-    InvalidProgram(String),
 }
 
 impl Display for ParseErrorType {
@@ -222,7 +220,6 @@ impl Display for ParseErrorType {
                 count,
                 name.escape_default()
             )?,
-            Self::InvalidProgram(err) => write!(f, "Program parse error: {}", err)?,
         }
         Ok(())
     }
diff --git a/utils/ayaka-runtime/Cargo.toml b/utils/ayaka-runtime/Cargo.toml
index 5a252f7e..8939d2b1 100644
--- a/utils/ayaka-runtime/Cargo.toml
+++ b/utils/ayaka-runtime/Cargo.toml
@@ -4,8 +4,8 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
+ayaka-primitive = { path = "../ayaka-primitive" }
 ayaka-bindings-types = { path = "../ayaka-bindings-types" }
-ayaka-script = { path = "../ayaka-script" }
 ayaka-plugin = { path = "../ayaka-plugin" }
 fallback = "0.1"
 language-matcher = "0.1"
@@ -19,7 +19,8 @@ stream-future = "0.3"
 futures-util = "0.3"
 tryiterator = { git = "https://github.com/Pernosco/tryiterator.git" }
 dirs = "4.0"
-trylog = "0.2"
+log = "0.4"
+trylog = "0.3"
 cfg-if = "1.0"
 
 ayaka-plugin-nop = { path = "../ayaka-plugin-nop" }
@@ -32,7 +33,7 @@ tokio = { version = "1", features = ["macros", "parking_lot", "rt"] }
 generic-tests = "0.1"
 
 [features]
-default = ["wasmi"]
+default = ["wasmtime"]
 wasmi = ["ayaka-plugin-wasmi"]
 wasmtime = ["ayaka-plugin-wasmtime"]
 wasmer = ["ayaka-plugin-wasmer"]
diff --git a/utils/ayaka-runtime/src/context.rs b/utils/ayaka-runtime/src/context.rs
index 04daff3f..23661260 100644
--- a/utils/ayaka-runtime/src/context.rs
+++ b/utils/ayaka-runtime/src/context.rs
@@ -4,25 +4,23 @@ use crate::{
 };
 use anyhow::{anyhow, bail, Result};
 use ayaka_bindings_types::*;
-use ayaka_script::{Command, Line, Text};
 use fallback::Fallback;
 use log::error;
-use script::*;
-use std::{collections::HashMap, path::Path};
+use std::{collections::HashMap, path::Path, sync::Arc};
 use stream_future::stream;
-use trylog::TryLog;
+use trylog::macros::*;
 
 /// The game running context.
 pub struct Context {
     /// The inner [`Game`] object.
     pub game: Game,
     frontend: FrontendType,
-    runtime: Runtime,
+    runtime: Arc<Runtime>,
     /// The inner raw context.
     pub ctx: RawContext,
     /// The inner record.
     pub record: ActionRecord,
-    switches: Vec<(bool, Program)>,
+    switches: Vec<bool>,
     vars: VarMap,
 }
 
@@ -156,10 +154,6 @@ impl Context {
         }
     }
 
-    fn table(&mut self) -> VarTable {
-        VarTable::new(&self.runtime, &mut self.ctx.locals)
-    }
-
     fn current_paragraph(&self, loc: &Locale) -> Option<&Paragraph> {
         self.game
             .find_para(loc, &self.ctx.cur_base_para, &self.ctx.cur_para)
@@ -182,15 +176,40 @@ impl Context {
     }
 
     /// Call the part of script with this context.
-    pub fn call(&mut self, expr: &impl Callable) -> RawValue {
-        self.table().call(expr)
+    pub fn call(&mut self, text: &Text) -> String {
+        let mut str = String::new();
+        for line in &text.0 {
+            match line {
+                SubText::Str(s) => str.push_str(s),
+                SubText::Cmd(c) => {
+                    let value = match c {
+                        Command::Character(_, _) => RawValue::Unit,
+                        Command::Res(_) | Command::Other(_, _) => {
+                            log::warn!("Unsupported command in text.");
+                            RawValue::Unit
+                        }
+                        Command::Ctx(n) => unwrap_or_default_log!(
+                            self.ctx.locals.get(n).cloned(),
+                            format!("Cannot find variable {}", n)
+                        ),
+                    };
+                    str.push_str(&value.get_str());
+                }
+            }
+        }
+        str.trim().to_string()
     }
 
     /// Choose a switch item by index, start by 0.
-    pub fn switch(&mut self, i: usize) -> RawValue {
+    pub fn switch(&mut self, i: usize) {
         assert!((0..self.switches.len()).contains(&i));
-        assert!(self.switches[i].0);
-        self.call(&self.switches[i].1.clone())
+        assert!(self.switches[i]);
+        self.ctx
+            .locals
+            .insert("?".to_string(), RawValue::Num(i as i64));
+        for i in 0..self.switches.len() {
+            self.ctx.locals.remove(&i.to_string());
+        }
     }
 
     fn parse_text(&self, loc: &Locale, text: &Text) -> Result<ActionText> {
@@ -216,6 +235,8 @@ impl Context {
                     Command::Ctx(n) => {
                         if let Some(value) = self.ctx.locals.get(n) {
                             action.push_back_block(value.get_str())
+                        } else {
+                            log::warn!("Cannot find variable {}", n)
                         }
                     }
                     Command::Other(cmd, args) => {
@@ -235,11 +256,11 @@ impl Context {
         Ok(action)
     }
 
-    fn parse_switches(&self, s: &[SwitchItem]) -> Vec<Switch> {
+    fn parse_switches(&self, s: &[String]) -> Vec<Switch> {
         s.iter()
             .zip(&self.switches)
-            .map(|(item, (enabled, _))| Switch {
-                text: item.text.clone(),
+            .map(|(item, enabled)| Switch {
+                text: item.clone(),
                 enabled: *enabled,
             })
             .collect()
@@ -248,18 +269,20 @@ impl Context {
     fn process_line(&mut self, t: Line) -> Result<()> {
         match t {
             Line::Empty | Line::Text(_) => {}
-            Line::Exec { exec } => {
-                self.call(&exec);
-            }
             Line::Switch { switches } => {
                 self.switches.clear();
-                for item in switches {
-                    let enabled = item
-                        .enabled
-                        .as_ref()
-                        .map(|p| self.call(p).get_bool())
-                        .unwrap_or(true);
-                    self.switches.push((enabled, item.action));
+                for i in 0..switches.len() {
+                    let enabled = self
+                        .ctx
+                        .locals
+                        .get(&i.to_string())
+                        .unwrap_or(&RawValue::Unit);
+                    let enabled = if let RawValue::Unit = enabled {
+                        true
+                    } else {
+                        enabled.get_bool()
+                    };
+                    self.switches.push(enabled);
                 }
             }
             Line::Custom(props) => {
@@ -397,7 +420,7 @@ impl Context {
                         .and_then(|p| p.next.as_ref())
                         // TODO: reduce clone
                         .cloned()
-                        .map(|text| self.call(&text).into_str())
+                        .map(|text| self.call(&text))
                         .unwrap_or_default();
                     self.ctx.cur_act = 0;
                 }
@@ -418,8 +441,7 @@ impl Context {
         };
 
         let ctx = cur_text_base.cloned().map(|t| {
-            self.process_line(t)
-                .unwrap_or_default_log("Parse line error");
+            unwrap_or_default_log!(self.process_line(t), "Parse line error");
             self.push_history();
             self.ctx.clone()
         });
diff --git a/utils/ayaka-runtime/src/lib.rs b/utils/ayaka-runtime/src/lib.rs
index aca7aa3a..c573893f 100644
--- a/utils/ayaka-runtime/src/lib.rs
+++ b/utils/ayaka-runtime/src/lib.rs
@@ -6,17 +6,13 @@
 
 #![warn(missing_docs)]
 #![deny(unsafe_code)]
-#![feature(fn_traits)]
 #![feature(generators)]
 #![feature(once_cell)]
-#![feature(type_alias_impl_trait)]
-#![feature(unboxed_closures)]
 
 mod config;
 mod context;
 mod locale;
 pub mod plugin;
-pub mod script;
 mod settings;
 
 #[doc(no_inline)]
@@ -24,15 +20,15 @@ pub use anyhow;
 #[doc(no_inline)]
 pub use ayaka_bindings_types::*;
 #[doc(no_inline)]
-pub use ayaka_script::log;
-#[doc(no_inline)]
-pub use ayaka_script::*;
+pub use ayaka_primitive::*;
 pub use config::*;
 pub use context::*;
 #[doc(no_inline)]
 pub use futures_util::{pin_mut, StreamExt, TryStreamExt};
 #[doc(no_inline)]
 pub use locale::*;
+#[doc(no_inline)]
+pub use log;
 pub use settings::*;
 
 /// Get the version of Ayaka runtime.
diff --git a/utils/ayaka-runtime/src/plugin.rs b/utils/ayaka-runtime/src/plugin.rs
index ba418e70..ec21e081 100644
--- a/utils/ayaka-runtime/src/plugin.rs
+++ b/utils/ayaka-runtime/src/plugin.rs
@@ -1,12 +1,16 @@
 //! The plugin utilities.
 
 use crate::*;
-use anyhow::Result;
+use anyhow::{bail, Result};
 use ayaka_plugin::*;
-use std::{collections::HashMap, path::Path};
+use std::{
+    collections::HashMap,
+    path::Path,
+    sync::{Arc, RwLock, Weak},
+};
 use stream_future::stream;
 use tryiterator::TryIteratorExt;
-use trylog::TryLog;
+use trylog::macros::*;
 
 /// The plugin module with high-level interfaces.
 pub struct Module<M: RawModule = BackendModule> {
@@ -20,11 +24,6 @@ impl<M: RawModule> Module<M> {
         }
     }
 
-    /// Calls a script plugin method by name.
-    pub fn dispatch_method(&self, name: &str, args: &[RawValue]) -> Result<RawValue> {
-        self.module.call(name, (args,))
-    }
-
     /// Gets the [`PluginType`].
     pub fn plugin_type(&self) -> Result<PluginType> {
         self.module.call("plugin_type", ())
@@ -61,7 +60,7 @@ impl<M: RawModule> Module<M> {
 }
 
 /// The plugin runtime.
-pub struct Runtime<M: RawModule = BackendModule> {
+pub struct Runtime<M: RawModule + Send + Sync + 'static = BackendModule> {
     modules: HashMap<String, Module<M>>,
     action_modules: Vec<String>,
     text_modules: HashMap<String, String>,
@@ -78,10 +77,13 @@ pub enum LoadStatus {
     LoadPlugin(String, usize, usize),
 }
 
-impl<M: RawModule> Runtime<M> {
-    fn new_linker(root_path: impl AsRef<Path>) -> Result<M::Linker> {
+impl<M: RawModule + Send + Sync + 'static> Runtime<M> {
+    fn new_linker(
+        root_path: impl AsRef<Path>,
+        handle: Arc<RwLock<Weak<Self>>>,
+    ) -> Result<M::Linker> {
         let mut store = M::Linker::new(root_path)?;
-        let log_func = store.wrap_with_args(|data: Record| {
+        let log_func = store.wrap(|(data,): (Record,)| {
             let module_path = format!(
                 "{}::<plugin>::{}",
                 module_path!(),
@@ -97,9 +99,13 @@ impl<M: RawModule> Runtime<M> {
                     .file(data.file.as_deref())
                     .line(data.line)
                     .build(),
-            )
+            );
+            Ok(())
+        });
+        let log_flush_func = store.wrap(|_: ()| {
+            log::logger().flush();
+            Ok(())
         });
-        let log_flush_func = store.wrap(|| log::logger().flush());
         store.import(
             "log",
             HashMap::from([
@@ -107,6 +113,38 @@ impl<M: RawModule> Runtime<M> {
                 ("__log_flush".to_string(), log_flush_func),
             ]),
         )?;
+
+        let h = handle.clone();
+        let modules_func = store.wrap(move |_: ()| {
+            if let Some(this) = h.read().unwrap().upgrade() {
+                Ok(this.modules.keys().cloned().collect::<Vec<_>>())
+            } else {
+                bail!("Runtime hasn't been initialized.")
+            }
+        });
+        let h = handle;
+        let call_func = store.wrap_with(
+            move |mut handle, (module, name, args): (String, String, Vec<u8>)| {
+                if let Some(this) = h.read().unwrap().upgrade() {
+                    Ok(handle.call(
+                        this.modules[&module].module.inner(),
+                        &name,
+                        &args,
+                        |slice| Ok(slice.to_vec()),
+                    )?)
+                } else {
+                    bail!("Runtime hasn't been initialized.")
+                }
+            },
+        );
+        store.import(
+            "plugin",
+            HashMap::from([
+                ("__modules".to_string(), modules_func),
+                ("__call".to_string(), call_func),
+            ]),
+        )?;
+
         Ok(store)
     }
 
@@ -120,12 +158,9 @@ impl<M: RawModule> Runtime<M> {
         dir: impl AsRef<Path> + 'a,
         rel_to: impl AsRef<Path> + 'a,
         names: &'a [impl AsRef<str>],
-    ) -> Result<Self> {
+    ) -> Result<Arc<Self>> {
         let root_path = rel_to.as_ref();
         let path = root_path.join(dir);
-        yield LoadStatus::CreateEngine;
-        let store = Self::new_linker(root_path)?;
-        let mut runtime = Self::new();
         let paths = if names.is_empty() {
             std::fs::read_dir(path)?
                 .try_filter_map(|f| {
@@ -156,6 +191,12 @@ impl<M: RawModule> Runtime<M> {
                 })
                 .collect::<Vec<_>>()
         };
+
+        yield LoadStatus::CreateEngine;
+        let handle = Arc::new(RwLock::new(Weak::new()));
+        let store = Self::new_linker(root_path, handle.clone())?;
+        let mut runtime = Self::new();
+
         let total_len = paths.len();
         for (i, (name, p)) in paths.into_iter().enumerate() {
             yield LoadStatus::LoadPlugin(name.clone(), i, total_len);
@@ -163,6 +204,8 @@ impl<M: RawModule> Runtime<M> {
             let module = Module::new(store.create(&buf)?);
             runtime.insert_module(name, module)?;
         }
+        let runtime = Arc::new(runtime);
+        *handle.write().unwrap() = Arc::downgrade(&runtime);
         Ok(runtime)
     }
 
@@ -177,9 +220,8 @@ impl<M: RawModule> Runtime<M> {
     }
 
     fn insert_module(&mut self, name: String, module: Module<M>) -> Result<()> {
-        let plugin_type = module
-            .plugin_type()
-            .unwrap_or_default_log("Cannot determine module type");
+        let plugin_type =
+            unwrap_or_default_log!(module.plugin_type(), "Cannot determine module type");
         if plugin_type.action {
             self.action_modules.push(name.clone());
         }
diff --git a/utils/ayaka-script/Cargo.toml b/utils/ayaka-script/Cargo.toml
index 58e3b25f..39881675 100644
--- a/utils/ayaka-script/Cargo.toml
+++ b/utils/ayaka-script/Cargo.toml
@@ -4,16 +4,12 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
+ayaka-primitive = { path = "../ayaka-primitive" }
 anyhow = "1.0"
 serde = { version = "1.0", features = ["derive"] }
-serde_with = "2.0"
 lalrpop-util = { version = "0.19", features = ["lexer"] }
-regex = "1"
 log = "0.4"
-trylog = "0.2"
-
-[dev-dependencies]
-serde_yaml = "0.9"
+trylog = "0.3"
 
 [build-dependencies]
 lalrpop = "0.19"
diff --git a/utils/ayaka-script/src/exec/mod.rs b/utils/ayaka-script/src/exec/mod.rs
deleted file mode 100644
index 8eb9f38c..00000000
--- a/utils/ayaka-script/src/exec/mod.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-//! The script parser.
-
-use std::str::FromStr;
-
-use crate::*;
-use lalrpop_util::lalrpop_mod;
-use serde::Deserialize;
-
-/// A full script, a collection of expressions.
-///
-/// The last expression is the final value of the script.
-#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
-#[serde(try_from = "String")]
-pub struct Program(pub Vec<Expr>);
-
-/// An expression.
-///
-/// Two expressions should be splited with `;`.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum Expr {
-    /// A reference to a variable.
-    Ref(Ref),
-    /// A const value.
-    Const(RawValue),
-    /// A unary operation.
-    Unary(UnaryOp, Box<Expr>),
-    /// A binary operation.
-    Binary(Box<Expr>, BinaryOp, Box<Expr>),
-    /// A call to a function.
-    Call(String, String, Vec<Expr>),
-}
-
-/// Unary operations.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum UnaryOp {
-    /// `+`
-    Positive,
-    /// `-`
-    Negative,
-    /// `!`
-    Not,
-}
-
-/// Binary operations.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum BinaryOp {
-    /// Value operations.
-    Val(ValBinaryOp),
-    /// Logical operations.
-    Logic(LogicBinaryOp),
-    /// Assignment.
-    Assign,
-    /// Inplace value operations.
-    Inplace(ValBinaryOp),
-}
-
-/// Value binary operations.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ValBinaryOp {
-    /// `+`
-    Add,
-    /// `-`
-    Minus,
-    /// `*`
-    Mul,
-    /// `/`
-    Div,
-    /// `%`
-    Mod,
-    /// `&`
-    And,
-    /// `|`
-    Or,
-    /// `^`
-    Xor,
-}
-
-/// Logical operations.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum LogicBinaryOp {
-    /// `&&`
-    And,
-    /// `||`
-    Or,
-    /// `==`
-    Eq,
-    /// `!=`
-    Neq,
-    /// `<`
-    Lt,
-    /// `<=`
-    Le,
-    /// `>`
-    Gt,
-    /// `>=`
-    Ge,
-}
-
-/// Reference of a variable.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum Ref {
-    /// A local variable.
-    /// It is only accessible in current program.
-    Var(String),
-    /// A context variable.
-    /// It is stored in the context.
-    /// The variable name is prefixed with `$`.
-    Ctx(String),
-}
-
-lalrpop_mod!(
-    #[allow(missing_docs)]
-    #[allow(dead_code)]
-    #[allow(clippy::all)]
-    grammer,
-    "/exec/grammer.rs"
-);
-
-pub use grammer::{ConstParser, ExprParser, ProgramParser, RefParser};
-
-impl FromStr for Program {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        ProgramParser::new()
-            .parse(s)
-            .map_err(|e| anyhow::anyhow!("{}", e))
-    }
-}
-
-impl TryFrom<String> for Program {
-    type Error = anyhow::Error;
-
-    fn try_from(value: String) -> Result<Self, Self::Error> {
-        value.parse()
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use crate::exec::*;
-
-    fn var(s: &str) -> Expr {
-        Expr::Ref(Ref::Var(s.into()))
-    }
-
-    #[test]
-    fn program() {
-        assert_eq!(
-            ProgramParser::new()
-                .parse(
-                    "foo(a);
-                    foo.bar(a, b)"
-                )
-                .unwrap(),
-            Program(vec![
-                Expr::Call(String::default(), "foo".into(), vec![var("a")]),
-                Expr::Call("foo".into(), "bar".into(), vec![var("a"), var("b")])
-            ])
-        );
-    }
-
-    #[test]
-    fn expr() {
-        assert_eq!(ExprParser::new().parse("a").unwrap(), var("a"));
-        assert_eq!(
-            ExprParser::new().parse("!(a && b || c)").unwrap(),
-            Expr::Unary(
-                UnaryOp::Not,
-                Box::new(Expr::Binary(
-                    Box::new(Expr::Binary(
-                        Box::new(var("a")),
-                        BinaryOp::Logic(LogicBinaryOp::And),
-                        Box::new(var("b"))
-                    )),
-                    BinaryOp::Logic(LogicBinaryOp::Or),
-                    Box::new(var("c"))
-                ))
-            )
-        );
-        assert_eq!(
-            ExprParser::new().parse("foo(a)").unwrap(),
-            Expr::Call(String::default(), "foo".into(), vec![var("a")])
-        );
-        assert_eq!(
-            ExprParser::new().parse("foo.bar(a, b)").unwrap(),
-            Expr::Call("foo".into(), "bar".into(), vec![var("a"), var("b")])
-        );
-        assert_eq!(
-            ExprParser::new().parse("a + (b * (c & d))").unwrap(),
-            Expr::Binary(
-                Box::new(var("a")),
-                BinaryOp::Val(ValBinaryOp::Add),
-                Box::new(Expr::Binary(
-                    Box::new(var("b")),
-                    BinaryOp::Val(ValBinaryOp::Mul),
-                    Box::new(Expr::Binary(
-                        Box::new(var("c")),
-                        BinaryOp::Val(ValBinaryOp::And),
-                        Box::new(var("d"))
-                    ))
-                ))
-            )
-        );
-    }
-
-    #[test]
-    fn r#const() {
-        assert_eq!(ConstParser::new().parse("~").unwrap(), RawValue::Unit);
-
-        assert_eq!(
-            ConstParser::new().parse("true").unwrap(),
-            RawValue::Bool(true)
-        );
-        assert_eq!(
-            ConstParser::new().parse("false").unwrap(),
-            RawValue::Bool(false)
-        );
-
-        assert_eq!(
-            ConstParser::new().parse("114514").unwrap(),
-            RawValue::Num(114514.into())
-        );
-
-        assert_eq!(
-            ConstParser::new().parse("\"Hello world!\"").unwrap(),
-            RawValue::Str("Hello world!".into())
-        );
-    }
-
-    #[test]
-    fn r#ref() {
-        assert_eq!(RefParser::new().parse("a").unwrap(), Ref::Var("a".into()));
-        assert_eq!(RefParser::new().parse("$a").unwrap(), Ref::Ctx("a".into()));
-    }
-}
diff --git a/utils/ayaka-script/src/exec/grammer.lalrpop b/utils/ayaka-script/src/grammer.lalrpop
similarity index 95%
rename from utils/ayaka-script/src/exec/grammer.lalrpop
rename to utils/ayaka-script/src/grammer.lalrpop
index f5d41247..1460aa6b 100644
--- a/utils/ayaka-script/src/exec/grammer.lalrpop
+++ b/utils/ayaka-script/src/grammer.lalrpop
@@ -1,6 +1,6 @@
 use super::*;
 use std::str::FromStr;
-use trylog::TryLog;
+use trylog::macros::*;
 
 grammar;
 
@@ -100,6 +100,8 @@ AssignOp: BinaryOp = {
 pub Ref: Ref = {
     <i:Id> => Ref::Var(i),
     "$" <i:Id> => Ref::Ctx(i),
+    "$?" => Ref::Ctx("?".to_string()),
+    "$" <i:Num> => Ref::Ctx(i.to_string()),
 }
 
 Id: String = <s:r"[A-Za-z]\w*"> => s.into();
@@ -117,6 +119,6 @@ Bool: bool = {
     "false" => false,
 }
 
-Num: i64 = <s:r"[0-9]+"> => i64::from_str(s).unwrap_or_default_log("Parse num error");
+Num: i64 = <s:r"[0-9]+"> => unwrap_or_default_log!(i64::from_str(s), "Parse num error");
 
 Str: String = <s:r##""[^"\\]*(\\.[^"\\]*)*""##> => s[1..s.len() - 1].into();
diff --git a/utils/ayaka-script/src/lib.rs b/utils/ayaka-script/src/lib.rs
index 23413607..cc3432c1 100644
--- a/utils/ayaka-script/src/lib.rs
+++ b/utils/ayaka-script/src/lib.rs
@@ -1,18 +1,238 @@
-//! The script and text parsers.
+//! The script parsers.
 
 #![warn(missing_docs)]
 #![deny(unsafe_code)]
-#![feature(iterator_try_collect)]
-#![feature(once_cell)]
-
-mod exec;
-mod line;
-mod primitive;
-mod text;
-
-pub use exec::*;
-pub use line::*;
-#[doc(no_inline)]
-pub use log;
-pub use primitive::*;
-pub use text::*;
+
+pub use ayaka_primitive::{RawValue, ValueType};
+
+use lalrpop_util::lalrpop_mod;
+use serde::Deserialize;
+use std::str::FromStr;
+
+/// A full script, a collection of expressions.
+///
+/// The last expression is the final value of the script.
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
+#[serde(try_from = "String")]
+pub struct Program(pub Vec<Expr>);
+
+/// An expression.
+///
+/// Two expressions should be splited with `;`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Expr {
+    /// A reference to a variable.
+    Ref(Ref),
+    /// A const value.
+    Const(RawValue),
+    /// A unary operation.
+    Unary(UnaryOp, Box<Expr>),
+    /// A binary operation.
+    Binary(Box<Expr>, BinaryOp, Box<Expr>),
+    /// A call to a function.
+    Call(String, String, Vec<Expr>),
+}
+
+/// Unary operations.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum UnaryOp {
+    /// `+`
+    Positive,
+    /// `-`
+    Negative,
+    /// `!`
+    Not,
+}
+
+/// Binary operations.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum BinaryOp {
+    /// Value operations.
+    Val(ValBinaryOp),
+    /// Logical operations.
+    Logic(LogicBinaryOp),
+    /// Assignment.
+    Assign,
+    /// Inplace value operations.
+    Inplace(ValBinaryOp),
+}
+
+/// Value binary operations.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ValBinaryOp {
+    /// `+`
+    Add,
+    /// `-`
+    Minus,
+    /// `*`
+    Mul,
+    /// `/`
+    Div,
+    /// `%`
+    Mod,
+    /// `&`
+    And,
+    /// `|`
+    Or,
+    /// `^`
+    Xor,
+}
+
+/// Logical operations.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum LogicBinaryOp {
+    /// `&&`
+    And,
+    /// `||`
+    Or,
+    /// `==`
+    Eq,
+    /// `!=`
+    Neq,
+    /// `<`
+    Lt,
+    /// `<=`
+    Le,
+    /// `>`
+    Gt,
+    /// `>=`
+    Ge,
+}
+
+/// Reference of a variable.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Ref {
+    /// A local variable.
+    /// It is only accessible in current program.
+    Var(String),
+    /// A context variable.
+    /// It is stored in the context.
+    /// The variable name is prefixed with `$`.
+    Ctx(String),
+}
+
+lalrpop_mod!(
+    #[allow(missing_docs)]
+    #[allow(dead_code)]
+    #[allow(clippy::all)]
+    grammer
+);
+
+pub use grammer::{ConstParser, ExprParser, ProgramParser, RefParser};
+
+impl FromStr for Program {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        ProgramParser::new()
+            .parse(s)
+            .map_err(|e| anyhow::anyhow!("{}", e))
+    }
+}
+
+impl TryFrom<String> for Program {
+    type Error = anyhow::Error;
+
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        value.parse()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::*;
+
+    fn var(s: &str) -> Expr {
+        Expr::Ref(Ref::Var(s.into()))
+    }
+
+    #[test]
+    fn program() {
+        assert_eq!(
+            ProgramParser::new()
+                .parse(
+                    "foo(a);
+                    foo.bar(a, b)"
+                )
+                .unwrap(),
+            Program(vec![
+                Expr::Call(String::default(), "foo".into(), vec![var("a")]),
+                Expr::Call("foo".into(), "bar".into(), vec![var("a"), var("b")])
+            ])
+        );
+    }
+
+    #[test]
+    fn expr() {
+        assert_eq!(ExprParser::new().parse("a").unwrap(), var("a"));
+        assert_eq!(
+            ExprParser::new().parse("!(a && b || c)").unwrap(),
+            Expr::Unary(
+                UnaryOp::Not,
+                Box::new(Expr::Binary(
+                    Box::new(Expr::Binary(
+                        Box::new(var("a")),
+                        BinaryOp::Logic(LogicBinaryOp::And),
+                        Box::new(var("b"))
+                    )),
+                    BinaryOp::Logic(LogicBinaryOp::Or),
+                    Box::new(var("c"))
+                ))
+            )
+        );
+        assert_eq!(
+            ExprParser::new().parse("foo(a)").unwrap(),
+            Expr::Call(String::default(), "foo".into(), vec![var("a")])
+        );
+        assert_eq!(
+            ExprParser::new().parse("foo.bar(a, b)").unwrap(),
+            Expr::Call("foo".into(), "bar".into(), vec![var("a"), var("b")])
+        );
+        assert_eq!(
+            ExprParser::new().parse("a + (b * (c & d))").unwrap(),
+            Expr::Binary(
+                Box::new(var("a")),
+                BinaryOp::Val(ValBinaryOp::Add),
+                Box::new(Expr::Binary(
+                    Box::new(var("b")),
+                    BinaryOp::Val(ValBinaryOp::Mul),
+                    Box::new(Expr::Binary(
+                        Box::new(var("c")),
+                        BinaryOp::Val(ValBinaryOp::And),
+                        Box::new(var("d"))
+                    ))
+                ))
+            )
+        );
+    }
+
+    #[test]
+    fn r#const() {
+        assert_eq!(ConstParser::new().parse("~").unwrap(), RawValue::Unit);
+
+        assert_eq!(
+            ConstParser::new().parse("true").unwrap(),
+            RawValue::Bool(true)
+        );
+        assert_eq!(
+            ConstParser::new().parse("false").unwrap(),
+            RawValue::Bool(false)
+        );
+
+        assert_eq!(
+            ConstParser::new().parse("114514").unwrap(),
+            RawValue::Num(114514.into())
+        );
+
+        assert_eq!(
+            ConstParser::new().parse("\"Hello world!\"").unwrap(),
+            RawValue::Str("Hello world!".into())
+        );
+    }
+
+    #[test]
+    fn r#ref() {
+        assert_eq!(RefParser::new().parse("a").unwrap(), Ref::Var("a".into()));
+        assert_eq!(RefParser::new().parse("$a").unwrap(), Ref::Ctx("a".into()));
+    }
+}
diff --git a/utils/ayaka-script/src/line.rs b/utils/ayaka-script/src/line.rs
deleted file mode 100644
index acb9bd3f..00000000
--- a/utils/ayaka-script/src/line.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-use crate::*;
-use serde::Deserialize;
-use serde_with::rust::maps_duplicate_key_is_error;
-use std::{collections::HashMap, str::FromStr};
-
-/// Represents a line in a prograph.
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
-#[serde(untagged)]
-pub enum Line {
-    /// An empty line, usually fallbacks to the base language one.
-    Empty,
-    /// A text line.
-    Text(Text),
-    /// An `exec` line, to execute scripts.
-    Exec {
-        /// The program to execute.
-        exec: Program,
-    },
-    /// Some `switches`.
-    Switch {
-        /// The switch items.
-        switches: Vec<SwitchItem>,
-    },
-    /// Custom line types.
-    #[serde(with = "maps_duplicate_key_is_error")]
-    Custom(HashMap<String, RawValue>),
-}
-
-/// A switch item in the `switches`.
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
-#[serde(try_from = "String")]
-pub struct SwitchItem {
-    /// The display text of the item.
-    pub text: String,
-    /// Whether the item is enabled.
-    /// [`None`] is enabled.
-    pub enabled: Option<Program>,
-    /// The action to execute if the item is choosen.
-    pub action: Program,
-}
-
-impl FromStr for SwitchItem {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let mut splits = s.split('|');
-        let text = splits.next().unwrap_or_default().to_string();
-        let enabled = if let Some(s) = splits.next() {
-            if s.is_empty() {
-                None
-            } else {
-                Some(s.parse()?)
-            }
-        } else {
-            None
-        };
-        let action = splits.next().unwrap_or_default().parse()?;
-        Ok(Self {
-            text,
-            enabled,
-            action,
-        })
-    }
-}
-
-impl TryFrom<String> for SwitchItem {
-    type Error = anyhow::Error;
-
-    fn try_from(value: String) -> Result<Self, Self::Error> {
-        value.parse()
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use std::collections::HashMap;
-
-    use crate::*;
-
-    #[test]
-    fn parse() {
-        let lines = r#"
-- abc
-- exec: $a
-- switches:
-  - a||$n = 1
-  - b|false|$n = 0
-- video: 0
--
-        "#;
-        let lines: Vec<Line> = serde_yaml::from_str(lines).unwrap();
-        assert_eq!(lines.len(), 5);
-        assert_eq!(
-            lines[0],
-            Line::Text(Text(vec![SubText::Str("abc".to_string())]))
-        );
-        assert_eq!(
-            lines[1],
-            Line::Exec {
-                exec: Program(vec![Expr::Ref(Ref::Ctx("a".to_string()))])
-            }
-        );
-        assert_eq!(
-            lines[2],
-            Line::Switch {
-                switches: vec![
-                    SwitchItem {
-                        text: "a".to_string(),
-                        enabled: None,
-                        action: Program(vec![Expr::Binary(
-                            Box::new(Expr::Ref(Ref::Ctx("n".to_string()))),
-                            BinaryOp::Assign,
-                            Box::new(Expr::Const(RawValue::Num(1)))
-                        )])
-                    },
-                    SwitchItem {
-                        text: "b".to_string(),
-                        enabled: Some(Program(vec![Expr::Const(RawValue::Bool(false))])),
-                        action: Program(vec![Expr::Binary(
-                            Box::new(Expr::Ref(Ref::Ctx("n".to_string()))),
-                            BinaryOp::Assign,
-                            Box::new(Expr::Const(RawValue::Num(0)))
-                        )])
-                    }
-                ]
-            }
-        );
-        assert_eq!(
-            lines[3],
-            Line::Custom(HashMap::from([("video".to_string(), RawValue::Num(0))]))
-        );
-        assert_eq!(lines[4], Line::Empty);
-    }
-}


Test patch:
diff --git a/utils/ayaka-runtime/tests/runtime.rs b/utils/ayaka-runtime/tests/runtime.rs
index d6c5e985..b89b9781 100644
--- a/utils/ayaka-runtime/tests/runtime.rs
+++ b/utils/ayaka-runtime/tests/runtime.rs
@@ -1,125 +1,120 @@
 use ayaka_plugin::RawModule;
-use ayaka_plugin_nop::NopModule;
-use ayaka_runtime::{plugin::Runtime, script::*, *};
+use ayaka_runtime::{plugin::*, *};
+use std::collections::HashMap;
 
-async fn with_ctx<M: RawModule>(f: impl FnOnce(&mut VarTable<M>)) {
+struct ModuleWrapper<'a, M: RawModule> {
+    module: &'a Module<M>,
+}
+
+impl<'a, M: RawModule> ModuleWrapper<'a, M> {
+    pub fn call(&self, script: &str) -> VarMap {
+        let game_props = HashMap::default();
+        let frontend = FrontendType::Text;
+        let ctx = RawContext::default();
+        let props = VarMap::from([("exec".to_string(), RawValue::Str(script.to_string()))]);
+        let ctx = LineProcessContextRef {
+            game_props: &game_props,
+            frontend,
+            ctx: &ctx,
+            props: &props,
+        };
+        let res = self.module.dispatch_line("exec", ctx).unwrap();
+        res.locals
+    }
+}
+
+async fn with_ctx<M: RawModule + Send + Sync + 'static>(f: impl FnOnce(&ModuleWrapper<M>)) {
     let runtime = Runtime::load(
         "../../examples/plugins",
         env!("CARGO_MANIFEST_DIR"),
-        &["random"],
+        &["ayacript", "random"],
     )
     .await
     .unwrap();
-    let mut locals = VarMap::default();
-    let mut ctx = VarTable::new(&runtime, &mut locals);
-    f(&mut ctx);
+    let module = runtime.line_module("exec").unwrap();
+    let wrapper = ModuleWrapper { module };
+    f(&wrapper);
 }
 
-#[tokio::test]
-async fn vars() {
-    with_ctx::<NopModule>(|ctx| {
-        assert_eq!(
-            ProgramParser::new()
-                .parse(
+#[generic_tests::define(attrs(tokio::test))]
+mod runtime_tests {
+    use super::*;
+    use ayaka_plugin::RawModule;
+    use ayaka_plugin_wasmer::WasmerModule;
+    // use ayaka_plugin_wasmi::WasmiModule;
+    use ayaka_plugin_wasmtime::WasmtimeModule;
+
+    #[tokio::test]
+    async fn vars<M: RawModule + Send + Sync + 'static>() {
+        with_ctx::<M>(|ctx| {
+            assert_eq!(
+                ctx.call(
                     "
                         a = 0;
                         a += 1;
                         a += a;
-                        a
                     "
                 )
-                .ok()
-                .call(ctx),
-            RawValue::Num(2)
-        );
-
-        assert_eq!(
-            ProgramParser::new().parse("a").ok().call(ctx),
-            RawValue::Unit
-        );
+                .get("a"),
+                None
+            );
 
-        assert_eq!(
-            ProgramParser::new()
-                .parse(
+            assert_eq!(
+                ctx.call(
                     "
                         $a = 0;
                         $a += 1;
                         $a += a;
-                        $a
                     "
-                )
-                .ok()
-                .call(ctx),
-            RawValue::Num(1)
-        );
-
-        assert_eq!(
-            ProgramParser::new().parse("$a").ok().call(ctx),
-            RawValue::Num(1)
-        );
-    })
-    .await;
-}
+                )["a"],
+                RawValue::Num(1)
+            );
+        })
+        .await;
+    }
 
-#[tokio::test]
-async fn if_test() {
-    with_ctx::<NopModule>(|ctx| {
-        assert_eq!(
-            ProgramParser::new()
-                .parse(
+    #[tokio::test]
+    async fn if_test<M: RawModule + Send + Sync + 'static>() {
+        with_ctx::<M>(|ctx| {
+            assert_eq!(
+                ctx.call(
                     r##"
-                        if(1 + 1 + 4 + 5 + 1 + 4 == 16, "sodayo", ~)
+                        $a = if(1 + 1 + 4 + 5 + 1 + 4 == 16, "sodayo", ~)
                     "##
-                )
-                .ok()
-                .call(ctx)
-                .get_num(),
-            6
-        );
-        assert_eq!(
-            ProgramParser::new()
-                .parse(
+                )["a"]
+                    .get_num(),
+                6
+            );
+            assert_eq!(
+                ctx.call(
                     r##"
-                        if(true, "sodayo")
+                        $a = if(true, "sodayo")
                     "##
-                )
-                .ok()
-                .call(ctx)
-                .get_str(),
-            "sodayo"
-        );
-    })
-    .await;
-}
-
-#[generic_tests::define(attrs(tokio::test))]
-mod runtime_tests {
-    use super::*;
-    use ayaka_plugin::RawModule;
-    use ayaka_plugin_wasmer::WasmerModule;
-    use ayaka_plugin_wasmi::WasmiModule;
-    use ayaka_plugin_wasmtime::WasmtimeModule;
+                )["a"]
+                    .get_str(),
+                "sodayo"
+            );
+        })
+        .await;
+    }
 
     #[tokio::test]
-    async fn random<M: RawModule>() {
+    async fn random<M: RawModule + Send + Sync + 'static>() {
         with_ctx::<M>(|ctx| {
             assert!((0..10).contains(
-                &ProgramParser::new()
-                    .parse(
-                        r##"
-                            random.rnd(10)
-                        "##
-                    )
-                    .ok()
-                    .call(ctx)
+                &ctx.call(
+                    r##"
+                        $a = random.rnd(10)
+                    "##
+                )["a"]
                     .get_num()
             ))
         })
         .await;
     }
 
-    #[instantiate_tests(<WasmiModule>)]
-    mod inst_wasmi {}
+    // #[instantiate_tests(<WasmiModule>)]
+    // mod inst_wasmi {}
     #[instantiate_tests(<WasmtimeModule>)]
     mod inst_wasmtime {}
     #[instantiate_tests(<WasmerModule>)]