diff --git a/Cargo.lock b/Cargo.lock
index dd93c797..7cd49507 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -101,8 +101,8 @@ version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
- "cfg-if",
- "getrandom",
+ "cfg-if 1.0.0",
+ "getrandom 0.2.15",
  "once_cell",
  "version_check",
  "zerocopy",
@@ -312,7 +312,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
 dependencies = [
  "async-lock 2.8.0",
  "autocfg",
- "cfg-if",
+ "cfg-if 1.0.0",
  "concurrent-queue",
  "futures-lite 1.13.0",
  "log",
@@ -331,7 +331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964"
 dependencies = [
  "async-lock 3.4.0",
- "cfg-if",
+ "cfg-if 1.0.0",
  "concurrent-queue",
  "futures-io",
  "futures-lite 2.3.0",
@@ -379,7 +379,7 @@ dependencies = [
  "async-lock 2.8.0",
  "async-signal",
  "blocking",
- "cfg-if",
+ "cfg-if 1.0.0",
  "event-listener 3.1.0",
  "futures-lite 1.13.0",
  "rustix 0.38.34",
@@ -406,7 +406,7 @@ dependencies = [
  "async-io 2.3.3",
  "async-lock 3.4.0",
  "atomic-waker",
- "cfg-if",
+ "cfg-if 1.0.0",
  "futures-core",
  "futures-io",
  "rustix 0.38.34",
@@ -528,13 +528,34 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
+[[package]]
+name = "block-buffer"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
+dependencies = [
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array 0.12.4",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.7",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+dependencies = [
+ "byte-tools",
 ]
 
 [[package]]
@@ -603,6 +624,12 @@ version = "3.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
 
+[[package]]
+name = "byte-tools"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
+
 [[package]]
 name = "bytemuck"
 version = "1.16.0"
@@ -629,6 +656,16 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
+[[package]]
+name = "bytes"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
+dependencies = [
+ "byteorder",
+ "iovec",
+]
+
 [[package]]
 name = "bytes"
 version = "1.6.0"
@@ -687,6 +724,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
 
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -859,7 +902,7 @@ version = "4.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
 dependencies = [
- "bytes",
+ "bytes 1.6.0",
  "memchr",
 ]
 
@@ -870,10 +913,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2a2dc81369dde6d31456eedbb4fd3d320f0b9713573dfe06e569e2bce7607f2"
 dependencies = [
  "castaway",
- "cfg-if",
+ "cfg-if 1.0.0",
  "itoa",
  "rustversion",
  "ryu",
+ "serde",
  "static_assertions",
 ]
 
@@ -941,7 +985,7 @@ version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -968,12 +1012,12 @@ dependencies = [
  "bitflags 2.5.0",
  "crossterm_winapi",
  "libc",
- "mio",
+ "mio 0.8.11",
  "parking_lot",
  "serde",
  "signal-hook",
  "signal-hook-mio",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -982,7 +1026,7 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
 dependencies = [
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -991,7 +1035,7 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
- "generic-array",
+ "generic-array 0.14.7",
  "typenum",
 ]
 
@@ -1023,13 +1067,22 @@ dependencies = [
  "syn 2.0.66",
 ]
 
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array 0.12.4",
+]
+
 [[package]]
 name = "digest"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
- "block-buffer",
+ "block-buffer 0.10.4",
  "crypto-common",
 ]
 
@@ -1113,7 +1166,7 @@ dependencies = [
  "wasm-bindgen-futures",
  "web-sys",
  "web-time",
- "winapi",
+ "winapi 0.3.9",
  "winit",
 ]
 
@@ -1293,6 +1346,12 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "fake-simd"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+
 [[package]]
 name = "fastrand"
 version = "1.9.0"
@@ -1314,7 +1373,7 @@ version = "4.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "rustix 0.38.34",
  "windows-sys 0.52.0",
 ]
@@ -1334,7 +1393,7 @@ version = "0.2.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
  "redox_syscall 0.4.1",
  "windows-sys 0.52.0",
@@ -1407,6 +1466,22 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags 1.3.2",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
 [[package]]
 name = "futures-core"
 version = "0.3.30"
@@ -1475,6 +1550,15 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -1495,15 +1579,26 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
 ]
 
 [[package]]
@@ -1622,7 +1717,7 @@ dependencies = [
  "log",
  "presser",
  "thiserror",
- "winapi",
+ "winapi 0.3.9",
  "windows 0.52.0",
 ]
 
@@ -1668,7 +1763,7 @@ dependencies = [
  "libloading 0.8.3",
  "thiserror",
  "widestring",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -1704,6 +1799,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "httparse"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+
 [[package]]
 name = "iana-time-zone"
 version = "0.1.60"
@@ -1917,7 +2018,7 @@ version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
 ]
 
 [[package]]
@@ -1941,6 +2042,15 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "is_terminal_polyfill"
 version = "1.70.0"
@@ -1978,7 +2088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
 dependencies = [
  "cesu8",
- "cfg-if",
+ "cfg-if 1.0.0",
  "combine",
  "jni-sys",
  "log",
@@ -2011,6 +2121,16 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
 [[package]]
 name = "khronos-egl"
 version = "6.0.0"
@@ -2048,6 +2168,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
 [[package]]
 name = "libc"
 version = "0.2.155"
@@ -2060,8 +2186,8 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
 dependencies = [
- "cfg-if",
- "winapi",
+ "cfg-if 1.0.0",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -2070,7 +2196,7 @@ version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "windows-targets 0.52.5",
 ]
 
@@ -2198,6 +2324,25 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
 [[package]]
 name = "mio"
 version = "0.8.11"
@@ -2206,10 +2351,34 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
  "log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "mio-extras"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
+dependencies = [
+ "lazycell",
+ "log",
+ "mio 0.6.23",
+ "slab",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
 [[package]]
 name = "naga"
 version = "0.19.2"
@@ -2261,6 +2430,17 @@ dependencies = [
  "jni-sys",
 ]
 
+[[package]]
+name = "net2"
+version = "0.2.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "nix"
 version = "0.26.4"
@@ -2268,7 +2448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
 dependencies = [
  "bitflags 1.3.2",
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
  "memoffset 0.7.1",
 ]
@@ -2280,7 +2460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
 dependencies = [
  "bitflags 2.5.0",
- "cfg-if",
+ "cfg-if 1.0.0",
  "cfg_aliases",
  "libc",
 ]
@@ -2315,7 +2495,7 @@ dependencies = [
  "kqueue",
  "libc",
  "log",
- "mio",
+ "mio 0.8.11",
  "walkdir",
  "windows-sys 0.48.0",
 ]
@@ -2525,6 +2705,12 @@ version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
 [[package]]
 name = "orbclient"
 version = "0.3.47"
@@ -2585,7 +2771,7 @@ version = "0.9.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "libc",
  "redox_syscall 0.5.1",
  "smallvec",
@@ -2664,7 +2850,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
 dependencies = [
  "autocfg",
  "bitflags 1.3.2",
- "cfg-if",
+ "cfg-if 1.0.0",
  "concurrent-queue",
  "libc",
  "log",
@@ -2678,7 +2864,7 @@ version = "3.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "concurrent-queue",
  "hermit-abi",
  "pin-project-lite",
@@ -2751,6 +2937,19 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+]
+
 [[package]]
 name = "rand"
 version = "0.8.5"
@@ -2758,8 +2957,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
 ]
 
 [[package]]
@@ -2769,7 +2978,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
 ]
 
 [[package]]
@@ -2778,7 +2996,16 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
 ]
 
 [[package]]
@@ -2957,24 +3184,36 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.203"
+version = "1.0.204"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.203"
+version = "1.0.204"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
 dependencies = [
  "proc-macro2",
  "quote",
  "syn 2.0.66",
 ]
 
+[[package]]
+name = "serde_json"
+version = "1.0.121"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "serde_repr"
 version = "0.1.19"
@@ -2986,15 +3225,27 @@ dependencies = [
  "syn 2.0.66",
 ]
 
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
+ "fake-simd",
+ "opaque-debug",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "cpufeatures",
- "digest",
+ "digest 0.10.7",
 ]
 
 [[package]]
@@ -3014,7 +3265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
 dependencies = [
  "libc",
- "mio",
+ "mio 0.8.11",
  "signal-hook",
 ]
 
@@ -3109,7 +3360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
 dependencies = [
  "libc",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3136,8 +3387,11 @@ dependencies = [
  "crossterm",
  "notify",
  "reedline",
+ "serde",
+ "serde_json",
  "stack-core",
  "stack-std",
+ "ws",
 ]
 
 [[package]]
@@ -3146,6 +3400,8 @@ version = "0.1.0"
 dependencies = [
  "compact_str",
  "internment",
+ "serde",
+ "serde_json",
  "test-case",
  "unicode-segmentation",
  "yansi",
@@ -3258,7 +3514,7 @@ version = "3.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "fastrand 2.1.0",
  "rustix 0.38.34",
  "windows-sys 0.52.0",
@@ -3288,7 +3544,7 @@ version = "3.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "proc-macro2",
  "quote",
  "syn 2.0.66",
@@ -3335,7 +3591,7 @@ dependencies = [
  "arrayref",
  "arrayvec",
  "bytemuck",
- "cfg-if",
+ "cfg-if 1.0.0",
  "log",
  "tiny-skia-path",
 ]
@@ -3463,7 +3719,7 @@ checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
 dependencies = [
  "memoffset 0.9.1",
  "tempfile",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3561,6 +3817,12 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
@@ -3573,7 +3835,7 @@ version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "wasm-bindgen-macro",
 ]
 
@@ -3598,7 +3860,7 @@ version = "0.4.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
 dependencies = [
- "cfg-if",
+ "cfg-if 1.0.0",
  "js-sys",
  "wasm-bindgen",
  "web-sys",
@@ -3786,7 +4048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cbd7311dbd2abcfebaabf1841a2824ed7c8be443a0f29166e5d3c6a53a762c01"
 dependencies = [
  "arrayvec",
- "cfg-if",
+ "cfg-if 1.0.0",
  "cfg_aliases",
  "js-sys",
  "log",
@@ -3867,7 +4129,7 @@ dependencies = [
  "wasm-bindgen",
  "web-sys",
  "wgpu-types",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -3887,6 +4149,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
 
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -3897,6 +4165,12 @@ dependencies = [
  "winapi-x86_64-pc-windows-gnu",
 ]
 
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
 [[package]]
 name = "winapi-i686-pc-windows-gnu"
 version = "0.4.0"
@@ -4265,6 +4539,34 @@ version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
 
+[[package]]
+name = "ws"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25fe90c75f236a0a00247d5900226aea4f2d7b05ccc34da9e7a8880ff59b5848"
+dependencies = [
+ "byteorder",
+ "bytes 0.4.12",
+ "httparse",
+ "log",
+ "mio 0.6.23",
+ "mio-extras",
+ "rand 0.7.3",
+ "sha-1",
+ "slab",
+ "url",
+]
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
 [[package]]
 name = "x11-dl"
 version = "2.21.0"
@@ -4395,14 +4697,14 @@ dependencies = [
  "nix 0.26.4",
  "once_cell",
  "ordered-stream",
- "rand",
+ "rand 0.8.5",
  "serde",
  "serde_repr",
  "sha1",
  "static_assertions",
  "tracing",
  "uds_windows",
- "winapi",
+ "winapi 0.3.9",
  "xdg-home",
  "zbus_macros",
  "zbus_names",
diff --git a/Cargo.toml b/Cargo.toml
index ab0bfd9f..2591731a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,9 @@ members = ["stack-core", "stack-std", "stack-cli", "stack-debugger"]
 
 [workspace.dependencies]
 unicode-segmentation = "1"
-compact_str = "=0.8.0-beta"
+compact_str = { version = "=0.8.0-beta", features = ["serde"] }
 
 test-case = "3"
-clap = { version = "4", features = ["derive"] }
\ No newline at end of file
+clap = { version = "4", features = ["derive"] }
+serde = { version = "1.0.204", features = ["derive"] }
+serde_json = "1.0.121"
diff --git a/justfile b/justfile
index 549b8a02..2cb0f628 100644
--- a/justfile
+++ b/justfile
@@ -9,3 +9,9 @@ debug file:
 
 serve:
   cd docs; mdbook serve --open
+
+ws-serve:
+  cargo run -p stack-cli -- serve
+
+ws-connect:
+  rlwrap websocat ws://localhost:5001
diff --git a/stack-cli/Cargo.toml b/stack-cli/Cargo.toml
index 9d831234..a41a874a 100644
--- a/stack-cli/Cargo.toml
+++ b/stack-cli/Cargo.toml
@@ -17,6 +17,11 @@ stack-core = { path = "../stack-core" }
 stack-std = { path = "../stack-std", optional = true }
 codespan-reporting = "0.11.1"
 
+# server
+serde = { workspace = true }
+ws = { version = "0.9.2" }
+serde_json.workspace = true
+
 [[bin]]
 name = "stack"
 path = "src/main.rs"
diff --git a/stack-cli/src/lib.rs b/stack-cli/src/lib.rs
new file mode 100644
index 00000000..87721235
--- /dev/null
+++ b/stack-cli/src/lib.rs
@@ -0,0 +1,58 @@
+use core::fmt;
+use std::io::{self, prelude::Write};
+
+use crossterm::{
+  cursor::{self, MoveTo},
+  style::Print,
+  terminal, QueueableCommand,
+};
+use stack_core::prelude::*;
+
+pub mod server;
+
+pub fn ok_or_exit<T, E>(result: Result<T, E>) -> T
+where
+  E: fmt::Display,
+{
+  match result {
+    Ok(x) => x,
+    Err(e) => {
+      eprintln!("error: {e}");
+      std::process::exit(1);
+    }
+  }
+}
+
+pub fn print_stack(context: &Context) {
+  print!("stack:");
+
+  core::iter::repeat(" ")
+    .zip(context.stack())
+    .for_each(|(sep, x)| print!("{sep}{x:#}"));
+
+  println!()
+}
+
+pub fn eprint_stack(context: &Context) {
+  eprint!("stack:");
+
+  core::iter::repeat(" ")
+    .zip(context.stack())
+    .for_each(|(sep, x)| eprint!("{sep}{x:#}"));
+
+  eprintln!()
+}
+
+pub fn clear_screen() -> io::Result<()> {
+  let mut stdout = std::io::stdout();
+
+  stdout.queue(cursor::Hide)?;
+  let (_, num_lines) = terminal::size()?;
+  for _ in 0..2 * num_lines {
+    stdout.queue(Print("\n"))?;
+  }
+  stdout.queue(MoveTo(0, 0))?;
+  stdout.queue(cursor::Show)?;
+
+  stdout.flush()
+}
diff --git a/stack-cli/src/main.rs b/stack-cli/src/main.rs
index b544dc59..3f90ca8c 100644
--- a/stack-cli/src/main.rs
+++ b/stack-cli/src/main.rs
@@ -1,6 +1,5 @@
-use core::fmt;
 use std::{
-  io::{self, prelude::Write, Read},
+  io::Read,
   path::{Path, PathBuf},
   sync::Arc,
 };
@@ -14,15 +13,13 @@ use codespan_reporting::{
     termcolor::{ColorChoice, StandardStream},
   },
 };
-use crossterm::{
-  cursor::{self, MoveTo},
-  style::Print,
-  terminal, QueueableCommand,
-};
 use notify::{
   Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
 };
 use reedline::{DefaultPrompt, DefaultPromptSegment, Reedline, Signal};
+use stack_cli::{
+  clear_screen, eprint_stack, ok_or_exit, print_stack, server::listen,
+};
 use stack_core::prelude::*;
 
 fn main() {
@@ -223,56 +220,10 @@ fn main() {
         }
       }
     }
+    Subcommand::Serve => listen(),
   }
 }
 
-fn ok_or_exit<T, E>(result: Result<T, E>) -> T
-where
-  E: fmt::Display,
-{
-  match result {
-    Ok(x) => x,
-    Err(e) => {
-      eprintln!("error: {e}");
-      std::process::exit(1);
-    }
-  }
-}
-
-fn print_stack(context: &Context) {
-  print!("stack:");
-
-  core::iter::repeat(" ")
-    .zip(context.stack())
-    .for_each(|(sep, x)| print!("{sep}{x:#}"));
-
-  println!()
-}
-
-fn eprint_stack(context: &Context) {
-  eprint!("stack:");
-
-  core::iter::repeat(" ")
-    .zip(context.stack())
-    .for_each(|(sep, x)| eprint!("{sep}{x:#}"));
-
-  eprintln!()
-}
-
-fn clear_screen() -> io::Result<()> {
-  let mut stdout = std::io::stdout();
-
-  stdout.queue(cursor::Hide)?;
-  let (_, num_lines) = terminal::size()?;
-  for _ in 0..2 * num_lines {
-    stdout.queue(Print("\n"))?;
-  }
-  stdout.queue(MoveTo(0, 0))?;
-  stdout.queue(cursor::Show)?;
-
-  stdout.flush()
-}
-
 #[derive(Debug, Clone, PartialEq, Eq, Default, clap::Parser)]
 #[command(author, version, about, long_about = None)]
 #[command(propagate_version = true)]
@@ -329,4 +280,7 @@ enum Subcommand {
     #[arg(short, long)]
     watch: bool,
   },
+
+  // TODO: add host and port as options
+  Serve,
 }
diff --git a/stack-cli/src/server.rs b/stack-cli/src/server.rs
new file mode 100644
index 00000000..103f6478
--- /dev/null
+++ b/stack-cli/src/server.rs
@@ -0,0 +1,282 @@
+use stack_core::prelude::*;
+use std::{collections::HashMap, mem, rc::Rc, sync::Mutex};
+
+use serde::{Deserialize, Serialize};
+use ws::{Message, Sender};
+
+use crate::{eprint_stack, print_stack};
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum Incoming {
+  // Execution
+  Run(RunPayload),
+  RunNew(RunPayload),
+
+  // Querying
+  Stack(BasePayload),
+  Context(BasePayload),
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct RunPayload {
+  pub id: u32,
+  pub code: String,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct BasePayload {
+  pub id: u32,
+}
+
+impl Incoming {
+  pub fn id(&self) -> u32 {
+    match self {
+      Incoming::Run(payload) | Incoming::RunNew(payload) => payload.id,
+      Incoming::Stack(payload) | Incoming::Context(payload) => payload.id,
+    }
+  }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "error", rename_all = "snake_case")]
+pub enum OutgoingError {
+  RunError(RunErrorPayload),
+  ParseError(ParseErrorPayload),
+  CommandError(CommandErrorPayload),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct RunErrorPayload {
+  pub for_id: u32,
+  pub value: RunError,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ParseErrorPayload {
+  pub for_id: u32,
+  pub value: ParseError,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CommandErrorPayload {
+  pub for_id: u32,
+  pub value: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "status", rename_all = "snake_case")]
+pub enum Outgoing {
+  Ok(OkPayload),
+  Error(OutgoingError),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum OkPayload {
+  Single(SinglePayload),
+  Null(NullPayload),
+  Many(ManyPayload),
+  Map(MapPayload),
+  Context(ContextPayload),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SinglePayload {
+  pub for_id: u32,
+  pub value: Expr,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct NullPayload {
+  pub for_id: u32,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ManyPayload {
+  pub for_id: u32,
+  pub value: Vec<Expr>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MapPayload {
+  pub for_id: u32,
+  pub value: HashMap<String, Expr>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ContextPayload {
+  pub for_id: u32,
+  pub value: Context,
+}
+
+impl Outgoing {
+  pub fn for_id(&self) -> u32 {
+    match self {
+      Outgoing::Ok(payload) => match payload {
+        OkPayload::Single(p) => p.for_id,
+        OkPayload::Null(p) => p.for_id,
+        OkPayload::Many(p) => p.for_id,
+        OkPayload::Map(p) => p.for_id,
+        OkPayload::Context(p) => p.for_id,
+      },
+      Outgoing::Error(error) => match error {
+        OutgoingError::RunError(p) => p.for_id,
+        OutgoingError::ParseError(p) => p.for_id,
+        OutgoingError::CommandError(p) => p.for_id,
+      },
+    }
+  }
+}
+
+#[allow(clippy::result_large_err)]
+fn run(
+  code: String,
+  eng_mutex: Rc<Mutex<Engine>>,
+  ctx_mutex: Rc<Mutex<Context>>,
+  out: &Sender,
+  id: u32,
+  reset: bool,
+) -> ws::Result<()> {
+  let source = Source::new("runner", code);
+  let mut lexer = Lexer::new(source);
+  let exprs = match parse(&mut lexer) {
+    Ok(e) => e,
+    Err(err) => {
+      return out.send(
+        serde_json::to_string(&Outgoing::Error(OutgoingError::ParseError(
+          ParseErrorPayload {
+            for_id: id,
+            value: err,
+          },
+        )))
+        .unwrap(),
+      );
+    }
+  };
+
+  match (eng_mutex.try_lock(), ctx_mutex.try_lock()) {
+    (Ok(engine), Ok(mut guard)) => {
+      if reset {
+        let _ = mem::replace(&mut *guard, Context::new());
+      }
+
+      let context = mem::take(&mut *guard);
+      let result = engine.run(context, exprs);
+
+      match result {
+        Ok(ctx) => {
+          print_stack(&ctx);
+          *guard = ctx;
+
+          match guard.stack().last().cloned() {
+            Some(expr) => out.send(
+              serde_json::to_string(&Outgoing::Ok(OkPayload::Single(
+                SinglePayload {
+                  for_id: id,
+                  value: expr,
+                },
+              )))
+              .unwrap(),
+            ),
+            None => out.send(
+              serde_json::to_string(&Outgoing::Ok(OkPayload::Null(
+                NullPayload { for_id: id },
+              )))
+              .unwrap(),
+            ),
+          }
+        }
+        Err(e) => {
+          eprintln!("error: {e}");
+          eprint_stack(&e.context);
+
+          out.send(
+            serde_json::to_string(&Outgoing::Error(OutgoingError::RunError(
+              RunErrorPayload {
+                for_id: id,
+                value: e,
+              },
+            )))
+            .unwrap(),
+          )
+        }
+      }
+    }
+    _ => todo!("mutex not lock"),
+  }
+}
+
+#[allow(clippy::result_large_err)]
+pub fn handle(
+  out: &Sender,
+  msg: &Message,
+  eng_mutex: Rc<Mutex<Engine>>,
+  ctx_mutex: Rc<Mutex<Context>>,
+) -> ws::Result<()> {
+  if let Message::Text(string) = msg {
+    let request = serde_json::from_str::<Incoming>(string);
+
+    match request {
+      Ok(incoming) => match incoming {
+        Incoming::RunNew(RunPayload { id, code }) => {
+          run(code, eng_mutex, ctx_mutex, out, id, true)
+        }
+        Incoming::Run(RunPayload { id, code }) => {
+          run(code, eng_mutex, ctx_mutex, out, id, false)
+        }
+
+        Incoming::Stack(BasePayload { id }) => match ctx_mutex.try_lock() {
+          Ok(context) => out.send(
+            serde_json::to_string(&Outgoing::Ok(OkPayload::Many(
+              ManyPayload {
+                for_id: id,
+                value: context.stack().to_vec(),
+              },
+            )))
+            .unwrap(),
+          ),
+          Err(_) => todo!(),
+        },
+        Incoming::Context(BasePayload { id }) => match ctx_mutex.try_lock() {
+          Ok(context) => out.send(
+            serde_json::to_string(&Outgoing::Ok(OkPayload::Context(
+              ContextPayload {
+                for_id: id,
+                value: context.clone(),
+              },
+            )))
+            .unwrap(),
+          ),
+          Err(_) => todo!(),
+        },
+      },
+      Err(parse_error) => out.send(
+        serde_json::to_string(&Outgoing::Error(OutgoingError::CommandError(
+          CommandErrorPayload {
+            // TODO: we don't get an ID here so this is a special case
+            for_id: 0,
+            value: parse_error.to_string(),
+          },
+        )))
+        .unwrap(),
+      ),
+    }
+  } else {
+    todo!("message not text")
+  }
+}
+
+pub fn listen() {
+  let eng_mutex = Rc::new(Mutex::new(Engine::new()));
+  let ctx_mutex = Rc::new(Mutex::new(Context::new()));
+
+  println!("Websocket server running on ws://localhost:5501");
+  ws::listen("localhost:5001", |out| {
+    let eng_mutex = eng_mutex.clone();
+    let ctx_mutex = ctx_mutex.clone();
+
+    move |msg| handle(&out, &msg, eng_mutex.clone(), ctx_mutex.clone())
+  })
+  .unwrap();
+}
diff --git a/stack-core/Cargo.toml b/stack-core/Cargo.toml
index e4a7effb..87f19513 100644
--- a/stack-core/Cargo.toml
+++ b/stack-core/Cargo.toml
@@ -8,6 +8,8 @@ internment = "0.7.4"
 unicode-segmentation.workspace = true
 compact_str.workspace = true
 yansi = "1"
+serde.workspace = true
 
 [dev-dependencies]
 test-case.workspace = true
+serde_json.workspace = true
diff --git a/stack-core/src/chain.rs b/stack-core/src/chain.rs
index f34a390f..1d57a950 100644
--- a/stack-core/src/chain.rs
+++ b/stack-core/src/chain.rs
@@ -1,10 +1,10 @@
 use core::{cell::RefCell, fmt};
-use std::{borrow::BorrowMut, sync::Arc};
+use std::{borrow::BorrowMut, rc::Rc};
 
 #[derive(PartialEq, Clone)]
 pub struct Chain<T> {
-  value: Arc<RefCell<T>>,
-  child: Option<Arc<RefCell<Chain<T>>>>,
+  value: Rc<RefCell<T>>,
+  child: Option<Rc<RefCell<Chain<T>>>>,
   root: bool,
 }
 
@@ -20,14 +20,14 @@ where
 impl<T> Chain<T> {
   pub fn new(value: T) -> Self {
     Self {
-      value: Arc::new(RefCell::new(value)),
+      value: Rc::new(RefCell::new(value)),
       child: None,
       root: true,
     }
   }
 
-  pub fn link(&mut self) -> Arc<RefCell<Self>> {
-    let child = Arc::new(RefCell::new(Self {
+  pub fn link(&mut self) -> Rc<RefCell<Self>> {
+    let child = Rc::new(RefCell::new(Self {
       value: self.value.clone(),
       child: None,
       root: false,
@@ -37,7 +37,7 @@ impl<T> Chain<T> {
     child
   }
 
-  pub fn root(&self) -> Arc<RefCell<T>> {
+  pub fn root(&self) -> Rc<RefCell<T>> {
     self.value.clone()
   }
 
@@ -54,7 +54,7 @@ where
     self.value.borrow().clone()
   }
 
-  fn unlink_with_rc(&mut self, value: Arc<RefCell<T>>, new_root: bool) {
+  fn unlink_with_rc(&mut self, value: Rc<RefCell<T>>, new_root: bool) {
     let mut new_root = new_root;
 
     if new_root {
@@ -70,7 +70,7 @@ where
   }
 
   pub fn unlink_with(&mut self, val: T) {
-    self.unlink_with_rc(Arc::new(RefCell::new(val)), true);
+    self.unlink_with_rc(Rc::new(RefCell::new(val)), true);
   }
 
   pub fn set(&mut self, val: T) {
diff --git a/stack-core/src/context.rs b/stack-core/src/context.rs
index 13434577..a1318521 100644
--- a/stack-core/src/context.rs
+++ b/stack-core/src/context.rs
@@ -1,4 +1,6 @@
-use std::{cell::RefCell, collections::HashMap, sync::Arc};
+use std::{cell::RefCell, collections::HashMap, rc::Rc};
+
+use serde::{Deserialize, Serialize};
 
 use crate::{
   chain::Chain,
@@ -12,7 +14,7 @@ use crate::{
 };
 
 // TODO: This API could be a lot nicer.
-#[derive(Debug, Clone, PartialEq, Default)]
+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
 pub struct Context {
   stack: Vec<Expr>,
   scopes: VecOne<Scope>,
@@ -162,7 +164,7 @@ impl Context {
   #[inline]
   pub fn scope_items(
     &self,
-  ) -> impl Iterator<Item = (&Symbol, &Arc<RefCell<Chain<Option<Expr>>>>)> {
+  ) -> impl Iterator<Item = (&Symbol, &Rc<RefCell<Chain<Option<Expr>>>>)> {
     self.scopes.last().items.iter()
   }
 
@@ -239,3 +241,23 @@ impl Context {
     self.scopes.try_pop();
   }
 }
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_ser_and_de() {
+    let mut context = Context::new();
+    context.stack_push(ExprKind::Integer(2).into()).unwrap();
+    context.def_scope_item(
+      Symbol::from_ref("foo"),
+      ExprKind::Symbol(Symbol::from_ref("bar")).into(),
+    );
+
+    let json = serde_json::to_string(&context).unwrap();
+    let ser_context: Context = serde_json::from_str(json.as_str()).unwrap();
+
+    assert_eq!(context, ser_context);
+  }
+}
diff --git a/stack-core/src/engine.rs b/stack-core/src/engine.rs
index 8c0382a5..384424d7 100644
--- a/stack-core/src/engine.rs
+++ b/stack-core/src/engine.rs
@@ -5,6 +5,8 @@ use std::{
   time::{Duration, Instant},
 };
 
+use serde::{Deserialize, Serialize};
+
 use crate::{
   context::Context,
   expr::{Expr, ExprKind, FnScope},
@@ -350,7 +352,7 @@ impl Engine {
   }
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub struct RunError {
   pub reason: RunErrorReason,
   pub context: Context,
@@ -371,7 +373,7 @@ impl fmt::Display for RunError {
   }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum RunErrorReason {
   StackUnderflow,
   DoubleError,
@@ -613,4 +615,20 @@ mod tests {
       vec![&ExprKind::Integer(1), &ExprKind::Integer(2),]
     );
   }
+
+  #[test]
+  fn test_ser_and_de() {
+    let source = Source::new("", "0 'a def 2 2 + '(fn)");
+    let mut lexer = Lexer::new(source);
+    let exprs = crate::parser::parse(&mut lexer).unwrap();
+
+    let engine = Engine::new();
+    let mut context = Context::new().with_stack_capacity(32);
+    context = engine.run(context, exprs).unwrap();
+
+    let json = serde_json::to_string(&context).unwrap();
+    let ser_context: Context = serde_json::from_str(json.as_str()).unwrap();
+
+    assert_eq!(context, ser_context);
+  }
 }
diff --git a/stack-core/src/expr.rs b/stack-core/src/expr.rs
index 989f303d..b8636470 100644
--- a/stack-core/src/expr.rs
+++ b/stack-core/src/expr.rs
@@ -3,11 +3,12 @@ use std::collections::HashMap;
 
 use compact_str::CompactString;
 use internment::Intern;
-use yansi::Paint;
+use serde::Deserialize;
+use serde::Serialize;
 
 use crate::{lexer::Span, scope::Scope, source::Source, symbol::Symbol};
 
-#[derive(Clone)]
+#[derive(Clone, Serialize, Deserialize)]
 pub struct Expr {
   pub kind: ExprKind,
   pub info: Option<ExprInfo>,
@@ -98,7 +99,7 @@ pub fn display_fn_scope(scope: &FnScope) -> String {
   .into()
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub enum FnScope {
   Scoped(Scope),
   Scopeless,
@@ -116,7 +117,7 @@ impl FnScope {
   }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub enum ExprKind {
   Nil,
 
@@ -216,6 +217,17 @@ impl PartialEq for ExprKind {
       (Self::List(lhs), Self::List(rhs)) => lhs == rhs,
       (Self::Record(lhs), Self::Record(rhs)) => lhs == rhs,
 
+      (
+        Self::Function {
+          scope: lhs_scope,
+          body: lhs_body,
+        },
+        Self::Function {
+          scope: rhs_scope,
+          body: rhs_body,
+        },
+      ) => lhs_scope == rhs_scope && lhs_body == rhs_body,
+
       (
         Self::SExpr {
           call: lhs_call,
@@ -337,6 +349,8 @@ impl ops::Rem for ExprKind {
 
 impl fmt::Display for ExprKind {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    use yansi::Paint;
+
     // TODO: Is there a nicer way to do this that avoids the duplication?
     if f.alternate() {
       match self {
@@ -511,7 +525,7 @@ impl fmt::Display for ErrorInner {
   }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct ExprInfo {
   pub source: Source,
   pub span: Span,
diff --git a/stack-core/src/journal.rs b/stack-core/src/journal.rs
index deb372a0..dff6ea7a 100644
--- a/stack-core/src/journal.rs
+++ b/stack-core/src/journal.rs
@@ -1,13 +1,15 @@
 use core::fmt;
 use std::collections::HashMap;
 
+use serde::{Deserialize, Serialize};
+
 use crate::{
   expr::{Expr, ExprInfo, ExprKind},
   scope::Scope,
   symbol::Symbol,
 };
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub struct JournalEntry {
   pub ops: Vec<JournalOp>,
   pub scope_level: usize,
@@ -39,7 +41,7 @@ impl JournalEntry {
   }
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub enum JournalOp {
   Call(Expr),
   SCall(Expr),
@@ -123,7 +125,7 @@ impl From<Scope> for JournalScope {
   }
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 // TODO: implement this as a ring buffer with max_commits so we never go over
 pub struct Journal {
   ops: Vec<JournalOp>,
diff --git a/stack-core/src/lexer.rs b/stack-core/src/lexer.rs
index 56e9f563..6256abaf 100644
--- a/stack-core/src/lexer.rs
+++ b/stack-core/src/lexer.rs
@@ -1,8 +1,10 @@
 use core::{fmt, ops::Range};
 
+use serde::{Deserialize, Serialize};
+
 use crate::source::Source;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Token {
   pub kind: TokenKind,
   pub span: Span,
@@ -14,7 +16,7 @@ impl fmt::Display for Token {
   }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Span {
   /// The lower byte bound (inclusive).
   pub start: usize,
@@ -33,7 +35,7 @@ impl Span {
   }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum TokenKind {
   Invalid,
   Eof,
diff --git a/stack-core/src/parser.rs b/stack-core/src/parser.rs
index bf241099..dd669e4f 100644
--- a/stack-core/src/parser.rs
+++ b/stack-core/src/parser.rs
@@ -1,5 +1,6 @@
 use compact_str::ToCompactString;
 use core::fmt;
+use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 
 use crate::{
@@ -256,7 +257,7 @@ fn parse_record(
   }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct ParseError {
   pub source: Source,
   pub kind: ParseErrorKind,
@@ -280,7 +281,7 @@ impl fmt::Display for ParseError {
   }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
 pub enum ParseErrorKind {
   UnexpectedToken(Token),
   InvalidLiteral(Token),
diff --git a/stack-core/src/scope.rs b/stack-core/src/scope.rs
index 11b7cc23..0f1c4b01 100644
--- a/stack-core/src/scope.rs
+++ b/stack-core/src/scope.rs
@@ -1,15 +1,101 @@
 use core::fmt;
-use std::{cell::RefCell, collections::HashMap, fmt::Formatter, sync::Arc};
+use std::{cell::RefCell, collections::HashMap, fmt::Formatter, rc::Rc};
+
+use serde::{
+  ser::{Serialize, SerializeMap},
+  Deserialize, Deserializer,
+};
 
 use crate::{chain::Chain, expr::FnScope, prelude::*};
 
-pub type Val = Arc<RefCell<Chain<Option<Expr>>>>;
+pub type Val = Rc<RefCell<Chain<Option<Expr>>>>;
 
-#[derive(Default, PartialEq)]
+#[derive(Default)]
 pub struct Scope {
   pub items: HashMap<Symbol, Val>,
 }
 
+impl PartialEq for Scope {
+  fn eq(&self, other: &Self) -> bool {
+    let a: HashMap<Symbol, Expr> = HashMap::from_iter(
+      self
+        .items
+        .iter()
+        .map(|(k, v)| (*k, v.borrow().val().clone().unwrap())),
+    );
+
+    let b: HashMap<Symbol, Expr> = HashMap::from_iter(
+      other
+        .items
+        .iter()
+        .map(|(k, v)| (*k, v.borrow().val().clone().unwrap())),
+    );
+
+    a == b
+  }
+}
+
+impl Serialize for Scope {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    let mut map = serializer.serialize_map(Some(self.items.len()))?;
+    for (k, v) in self.items.iter() {
+      // TODO: don't unwrap and handle the error somehow
+      let expr: Expr = v.borrow().val().unwrap();
+      map.serialize_entry(k, &expr)?;
+    }
+    map.end()
+  }
+}
+
+impl<'de> Deserialize<'de> for Scope {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    // Helper struct to deserialize the items
+    #[derive(Deserialize)]
+    #[serde(transparent)]
+    struct ScopeHelper {
+      items: HashMap<String, DeserializeVal>,
+    }
+
+    // Helper enum to deserialize Val
+    #[derive(Deserialize)]
+    #[serde(untagged)]
+    enum DeserializeVal {
+      Some(Expr),
+      None,
+    }
+
+    // Deserialize into the helper struct
+    let helper = ScopeHelper::deserialize(deserializer)?;
+
+    // Convert DeserializeVal to Val
+    let mut items: HashMap<String, Val> = helper
+      .items
+      .into_iter()
+      .map(|(k, v)| {
+        let val = match v {
+          DeserializeVal::Some(expr) => {
+            Rc::new(RefCell::new(Chain::new(Some(expr))))
+          }
+          DeserializeVal::None => Rc::new(RefCell::new(Chain::new(None))),
+        };
+        (k, val)
+      })
+      .collect();
+    let mut scope: HashMap<Symbol, Val> = HashMap::new();
+    for (k, v) in items.drain() {
+      scope.insert(Symbol::from_ref(k.as_str()), v);
+    }
+
+    Ok(Scope { items: scope })
+  }
+}
+
 impl fmt::Debug for Scope {
   fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     let iter = self.items.iter().map(|(name, item)| (name.as_str(), item));
@@ -18,7 +104,7 @@ impl fmt::Debug for Scope {
 }
 
 impl Clone for Scope {
-  /// Clones the scope, using the same Arc's as self
+  /// Clones the scope, using the same Rc's as self
   fn clone(&self) -> Self {
     let mut items = HashMap::new();
 
@@ -53,7 +139,7 @@ impl Scope {
 
       c.clone()
     } else {
-      let val = Arc::new(RefCell::new(Chain::new(Some(item))));
+      let val = Rc::new(RefCell::new(Chain::new(Some(item))));
       self.items.insert(name, val.clone());
 
       val
@@ -64,7 +150,7 @@ impl Scope {
     self
       .items
       .entry(name)
-      .or_insert_with(|| Arc::new(RefCell::new(Chain::new(None))));
+      .or_insert_with(|| Rc::new(RefCell::new(Chain::new(None))));
   }
 
   pub fn set(
diff --git a/stack-core/src/source.rs b/stack-core/src/source.rs
index 5e96f3b8..0b1eae4e 100644
--- a/stack-core/src/source.rs
+++ b/stack-core/src/source.rs
@@ -3,6 +3,7 @@
 use core::{fmt, num::NonZeroUsize};
 use std::{fs, io, path::Path, sync::Arc};
 
+use serde::{Deserialize, Deserializer, Serialize};
 use unicode_segmentation::UnicodeSegmentation;
 
 /// Contains metadata for a source.
@@ -11,6 +12,28 @@ use unicode_segmentation::UnicodeSegmentation;
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Source(Arc<SourceInner>);
 
+impl Serialize for Source {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    self.0.serialize(serializer)
+  }
+}
+
+impl<'de> Deserialize<'de> for Source {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    // First, deserialize SourceInner
+    let inner = SourceInner::deserialize(deserializer)?;
+
+    // Then wrap it in an Arc and return Source
+    Ok(Source(Arc::new(inner)))
+  }
+}
+
 impl Source {
   /// Creates a new [`Source`].
   pub fn new<N, S>(name: N, source: S) -> Self
@@ -126,7 +149,8 @@ impl fmt::Display for Location {
   }
 }
 
-#[derive(Debug, Clone, Eq)]
+#[derive(Debug, Clone, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
 struct SourceInner {
   name: String,
   source: String,
diff --git a/stack-core/src/symbol.rs b/stack-core/src/symbol.rs
index 56b66fe2..0db01b92 100644
--- a/stack-core/src/symbol.rs
+++ b/stack-core/src/symbol.rs
@@ -2,6 +2,10 @@ use core::{borrow::Borrow, fmt, hash::Hash};
 
 use compact_str::{CompactString, ToCompactString};
 use internment::Intern;
+use serde::{
+  de::{self, Visitor},
+  Deserialize, Deserializer, Serialize,
+};
 
 use crate::expr::ExprKind;
 
@@ -9,6 +13,53 @@ use crate::expr::ExprKind;
 #[repr(transparent)]
 pub struct Symbol(Intern<CompactString>);
 
+impl Serialize for Symbol {
+  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+  where
+    S: serde::Serializer,
+  {
+    // TODO: is this okay to make Symbol transparently a string?
+    serializer.serialize_str(self.as_str())
+  }
+}
+
+impl<'de> Deserialize<'de> for Symbol {
+  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+  where
+    D: Deserializer<'de>,
+  {
+    struct SymbolVisitor;
+
+    impl<'de> Visitor<'de> for SymbolVisitor {
+      type Value = Symbol;
+
+      fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("a string")
+      }
+
+      fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+      where
+        E: de::Error,
+      {
+        // Convert the &str to CompactString and intern it
+        let compact_string = CompactString::new(value);
+        Ok(Symbol(Intern::new(compact_string)))
+      }
+
+      fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
+      where
+        E: de::Error,
+      {
+        // Convert the String to CompactString and intern it
+        let compact_string = CompactString::new(value);
+        Ok(Symbol(Intern::new(compact_string)))
+      }
+    }
+
+    deserializer.deserialize_str(SymbolVisitor)
+  }
+}
+
 impl Symbol {
   /// Creates a [`Symbol`].
   #[inline]
diff --git a/stack-core/src/vec_one.rs b/stack-core/src/vec_one.rs
index 3d55cc48..f72717b5 100644
--- a/stack-core/src/vec_one.rs
+++ b/stack-core/src/vec_one.rs
@@ -3,8 +3,21 @@
 use core::slice::{Iter, IterMut, SliceIndex};
 use std::vec::IntoIter;
 
+use serde::{Deserialize, Serialize};
+
 /// A [`Vec`] with at least one element.
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
+#[derive(
+  Debug,
+  Clone,
+  PartialEq,
+  Eq,
+  PartialOrd,
+  Ord,
+  Hash,
+  Default,
+  Serialize,
+  Deserialize,
+)]
 pub(crate) struct VecOne<T> {
   vec: Vec<T>,
 }