From caec0b8ac9e4ea197220f9b2ef347600cd0361a1 Mon Sep 17 00:00:00 2001 From: liquidev Date: Sat, 10 Aug 2024 23:10:03 +0200 Subject: [PATCH] initial commit --- .editorconfig | 15 + .gitignore | 1 + Cargo.lock | 1058 +++++++++++++++++++++++++++++ Cargo.toml | 15 + Justfile | 5 + crates/canvane/Cargo.toml | 15 + crates/canvane/src/live_reload.rs | 23 + crates/canvane/src/main.rs | 70 ++ crates/haku-cli/Cargo.toml | 7 + crates/haku-cli/src/main.rs | 91 +++ crates/haku-wasm/Cargo.toml | 14 + crates/haku-wasm/src/lib.rs | 349 ++++++++++ crates/haku-wasm/src/logging.rs | 44 ++ crates/haku-wasm/src/panicking.rs | 20 + crates/haku/Cargo.toml | 6 + crates/haku/src/bytecode.rs | 266 ++++++++ crates/haku/src/compiler.rs | 625 +++++++++++++++++ crates/haku/src/lib.rs | 11 + crates/haku/src/render.rs | 144 ++++ crates/haku/src/sexp.rs | 476 +++++++++++++ crates/haku/src/system.rs | 440 ++++++++++++ crates/haku/src/value.rs | 161 +++++ crates/haku/src/vm.rs | 486 +++++++++++++ crates/haku/tests/language.rs | 256 +++++++ static/index.html | 18 + static/index.js | 154 +++++ static/live-reload.js | 16 + 27 files changed, 4786 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Justfile create mode 100644 crates/canvane/Cargo.toml create mode 100644 crates/canvane/src/live_reload.rs create mode 100644 crates/canvane/src/main.rs create mode 100644 crates/haku-cli/Cargo.toml create mode 100644 crates/haku-cli/src/main.rs create mode 100644 crates/haku-wasm/Cargo.toml create mode 100644 crates/haku-wasm/src/lib.rs create mode 100644 crates/haku-wasm/src/logging.rs create mode 100644 crates/haku-wasm/src/panicking.rs create mode 100644 crates/haku/Cargo.toml create mode 100644 crates/haku/src/bytecode.rs create mode 100644 crates/haku/src/compiler.rs create mode 100644 crates/haku/src/lib.rs create mode 100644 crates/haku/src/render.rs create mode 100644 crates/haku/src/sexp.rs create mode 100644 crates/haku/src/system.rs create mode 100644 crates/haku/src/value.rs create mode 100644 crates/haku/src/vm.rs create mode 100644 crates/haku/tests/language.rs create mode 100644 static/index.html create mode 100644 static/index.js create mode 100644 static/live-reload.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d5e22e2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.js] +max_line_length = 100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dd3b395 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1058 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "canvane" +version = "0.1.0" +dependencies = [ + "axum", + "color-eyre", + "copy_dir", + "eyre", + "haku", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "cc" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + +[[package]] +name = "dlmalloc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264b043b8e977326c1ee9e723da2c1f8d09a99df52cacf00b4dbce5ac54414d" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "haku" +version = "0.1.0" + +[[package]] +name = "haku-cli" +version = "0.1.0" +dependencies = [ + "haku", +] + +[[package]] +name = "haku-wasm" +version = "0.1.0" +dependencies = [ + "arrayvec", + "dlmalloc", + "haku", + "log", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..de31203 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +resolver = "2" +members = ["crates/*"] + +[workspace.dependencies] +haku.path = "crates/haku" +log = "0.4.22" + +[profile.wasm-dev] +inherits = "dev" +panic = "abort" + +[profile.wasm-release] +inherits = "release" +panic = "abort" diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..18e5720 --- /dev/null +++ b/Justfile @@ -0,0 +1,5 @@ +serve wasm_profile="wasm-dev": (wasm wasm_profile) + cargo run -p canvane + +wasm profile="wasm-dev": + cargo build -p haku-wasm --target wasm32-unknown-unknown --profile {{profile}} diff --git a/crates/canvane/Cargo.toml b/crates/canvane/Cargo.toml new file mode 100644 index 0000000..6dc648c --- /dev/null +++ b/crates/canvane/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "canvane" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.7.5" +color-eyre = "0.6.3" +copy_dir = "0.1.3" +eyre = "0.6.12" +haku.workspace = true +tokio = { version = "1.39.2", features = ["full"] } +tower-http = { version = "0.5.2", features = ["fs"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["fmt"] } diff --git a/crates/canvane/src/live_reload.rs b/crates/canvane/src/live_reload.rs new file mode 100644 index 0000000..909b0b9 --- /dev/null +++ b/crates/canvane/src/live_reload.rs @@ -0,0 +1,23 @@ +use std::time::Duration; + +use axum::{routing::get, Router}; +use tokio::time::sleep; + +pub fn router() -> Router { + Router::new() + .route("/stall", get(stall)) + .route("/back-up", get(back_up)) + .with_state(()) +} + +async fn stall() -> String { + loop { + // Sleep for a day, I guess. Just to uphold the connection forever without really using any + // significant resources. + sleep(Duration::from_secs(60 * 60 * 24)).await; + } +} + +async fn back_up() -> String { + "".into() +} diff --git a/crates/canvane/src/main.rs b/crates/canvane/src/main.rs new file mode 100644 index 0000000..f76adfd --- /dev/null +++ b/crates/canvane/src/main.rs @@ -0,0 +1,70 @@ +use std::{ + fs::{copy, create_dir_all, remove_dir_all}, + path::Path, +}; + +use axum::Router; +use copy_dir::copy_dir; +use eyre::Context; +use tokio::net::TcpListener; +use tower_http::services::{ServeDir, ServeFile}; +use tracing::{info, info_span}; +use tracing_subscriber::fmt::format::FmtSpan; + +#[cfg(debug_assertions)] +mod live_reload; + +struct Paths<'a> { + target_dir: &'a Path, +} + +fn build(paths: &Paths<'_>) -> eyre::Result<()> { + let _span = info_span!("build").entered(); + + _ = remove_dir_all(paths.target_dir); + create_dir_all(paths.target_dir).context("cannot create target directory")?; + copy_dir("static", paths.target_dir.join("static")).context("cannot copy static directory")?; + + create_dir_all(paths.target_dir.join("static/wasm")) + .context("cannot create static/wasm directory")?; + copy( + "target/wasm32-unknown-unknown/wasm-dev/haku_wasm.wasm", + paths.target_dir.join("static/wasm/haku.wasm"), + ) + .context("cannot copy haku.wasm file")?; + + Ok(()) +} + +#[tokio::main] +async fn main() { + color_eyre::install().unwrap(); + tracing_subscriber::fmt() + .with_span_events(FmtSpan::ACTIVE) + .init(); + + let paths = Paths { + target_dir: Path::new("target/site"), + }; + + match build(&paths) { + Ok(()) => (), + Err(error) => eprintln!("{error:?}"), + } + + let app = Router::new() + .route_service( + "/", + ServeFile::new(paths.target_dir.join("static/index.html")), + ) + .nest_service("/static", ServeDir::new(paths.target_dir.join("static"))); + + #[cfg(debug_assertions)] + let app = app.nest("/dev/live-reload", live_reload::router()); + + let listener = TcpListener::bind("0.0.0.0:8080") + .await + .expect("cannot bind to port"); + info!("listening on port 8080"); + axum::serve(listener, app).await.expect("cannot serve app"); +} diff --git a/crates/haku-cli/Cargo.toml b/crates/haku-cli/Cargo.toml new file mode 100644 index 0000000..883fe18 --- /dev/null +++ b/crates/haku-cli/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "haku-cli" +version = "0.1.0" +edition = "2021" + +[dependencies] +haku.workspace = true diff --git a/crates/haku-cli/src/main.rs b/crates/haku-cli/src/main.rs new file mode 100644 index 0000000..43be465 --- /dev/null +++ b/crates/haku-cli/src/main.rs @@ -0,0 +1,91 @@ +// NOTE: This is a very bad CLI. +// Sorry! + +use std::{error::Error, fmt::Display, io::BufRead}; + +use haku::{ + bytecode::{Chunk, Defs}, + compiler::{compile_expr, Compiler, Source}, + sexp::{parse_toplevel, Ast, Parser}, + system::System, + value::{BytecodeLoc, Closure, FunctionName, Ref, Value}, + vm::{Vm, VmLimits}, +}; + +fn eval(code: &str) -> Result> { + let mut system = System::new(1); + + let ast = Ast::new(1024); + let mut parser = Parser::new(ast, code); + let root = parse_toplevel(&mut parser); + let ast = parser.ast; + let src = Source { + code, + ast: &ast, + system: &system, + }; + + let mut defs = Defs::new(256); + let mut chunk = Chunk::new(65536).unwrap(); + let mut compiler = Compiler::new(&mut defs, &mut chunk); + compile_expr(&mut compiler, &src, root)?; + let diagnostics = compiler.diagnostics; + let defs = compiler.defs; + println!("{chunk:?}"); + + for diagnostic in &diagnostics { + eprintln!( + "{}..{}: {}", + diagnostic.span.start, diagnostic.span.end, diagnostic.message + ); + } + + if !diagnostics.is_empty() { + return Err(Box::new(DiagnosticsEmitted)); + } + + let mut vm = Vm::new( + defs, + &VmLimits { + stack_capacity: 256, + call_stack_capacity: 256, + ref_capacity: 256, + fuel: 32768, + }, + ); + let chunk_id = system.add_chunk(chunk)?; + let closure = vm.create_ref(Ref::Closure(Closure { + start: BytecodeLoc { + chunk_id, + offset: 0, + }, + name: FunctionName::Anonymous, + param_count: 0, + captures: Vec::new(), + }))?; + Ok(vm.run(&system, closure)?) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct DiagnosticsEmitted; + +impl Display for DiagnosticsEmitted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("diagnostics were emitted") + } +} + +impl Error for DiagnosticsEmitted {} + +fn main() -> Result<(), Box> { + let stdin = std::io::stdin(); + for line in stdin.lock().lines() { + let line = line?; + match eval(&line) { + Ok(value) => println!("{value:?}"), + Err(error) => eprintln!("error: {error}"), + } + } + + Ok(()) +} diff --git a/crates/haku-wasm/Cargo.toml b/crates/haku-wasm/Cargo.toml new file mode 100644 index 0000000..efcc2f4 --- /dev/null +++ b/crates/haku-wasm/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "haku-wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +arrayvec = { version = "0.7.4", default-features = false } +dlmalloc = { version = "0.2.6", features = ["global"] } +haku.workspace = true +log.workspace = true + diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs new file mode 100644 index 0000000..15257b0 --- /dev/null +++ b/crates/haku-wasm/src/lib.rs @@ -0,0 +1,349 @@ +#![no_std] + +extern crate alloc; + +use core::{alloc::Layout, ffi::CStr, slice, str}; + +use alloc::{boxed::Box, vec::Vec}; +use haku::{ + bytecode::{Chunk, Defs, DefsImage}, + compiler::{compile_expr, CompileError, Compiler, Diagnostic, Source}, + render::{Bitmap, Renderer, RendererLimits}, + sexp::{self, parse_toplevel, Ast, Parser}, + system::{ChunkId, System, SystemImage}, + value::{BytecodeLoc, Closure, FunctionName, Ref, Value}, + vm::{Exception, Vm, VmImage, VmLimits}, +}; +use log::info; + +pub mod logging; +mod panicking; + +#[global_allocator] +static ALLOCATOR: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; + +#[no_mangle] +unsafe extern "C" fn haku_alloc(size: usize, align: usize) -> *mut u8 { + alloc::alloc::alloc(Layout::from_size_align(size, align).unwrap()) +} + +#[no_mangle] +unsafe extern "C" fn haku_free(ptr: *mut u8, size: usize, align: usize) { + alloc::alloc::dealloc(ptr, Layout::from_size_align(size, align).unwrap()) +} + +#[derive(Debug, Clone, Copy)] +struct Limits { + max_chunks: usize, + max_defs: usize, + ast_capacity: usize, + chunk_capacity: usize, + stack_capacity: usize, + call_stack_capacity: usize, + ref_capacity: usize, + fuel: usize, + bitmap_stack_capacity: usize, + transform_stack_capacity: usize, +} + +impl Default for Limits { + fn default() -> Self { + Self { + max_chunks: 2, + max_defs: 256, + ast_capacity: 1024, + chunk_capacity: 65536, + stack_capacity: 1024, + call_stack_capacity: 256, + ref_capacity: 2048, + fuel: 65536, + bitmap_stack_capacity: 4, + transform_stack_capacity: 16, + } + } +} + +#[derive(Debug, Clone)] +struct Instance { + limits: Limits, + + system: System, + system_image: SystemImage, + defs: Defs, + defs_image: DefsImage, + vm: Vm, + vm_image: VmImage, + exception: Option, +} + +#[no_mangle] +unsafe extern "C" fn haku_instance_new() -> *mut Instance { + // TODO: This should be a parameter. + let limits = Limits::default(); + let system = System::new(limits.max_chunks); + + let defs = Defs::new(limits.max_defs); + let vm = Vm::new( + &defs, + &VmLimits { + stack_capacity: limits.stack_capacity, + call_stack_capacity: limits.call_stack_capacity, + ref_capacity: limits.ref_capacity, + fuel: limits.fuel, + }, + ); + + let system_image = system.image(); + let defs_image = defs.image(); + let vm_image = vm.image(); + + let instance = Box::new(Instance { + limits, + system, + system_image, + defs, + defs_image, + vm, + vm_image, + exception: None, + }); + Box::leak(instance) +} + +#[no_mangle] +unsafe extern "C" fn haku_instance_destroy(instance: *mut Instance) { + drop(Box::from_raw(instance)); +} + +#[no_mangle] +unsafe extern "C" fn haku_has_exception(instance: *mut Instance) -> bool { + (*instance).exception.is_some() +} + +#[no_mangle] +unsafe extern "C" fn haku_exception_message(instance: *const Instance) -> *const u8 { + (*instance).exception.as_ref().unwrap().message.as_ptr() +} + +#[no_mangle] +unsafe extern "C" fn haku_exception_message_len(instance: *const Instance) -> u32 { + (*instance).exception.as_ref().unwrap().message.len() as u32 +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +enum StatusCode { + Ok, + ChunkTooBig, + DiagnosticsEmitted, + TooManyChunks, + OutOfRefSlots, + EvalException, + RenderException, +} + +#[no_mangle] +extern "C" fn haku_is_ok(code: StatusCode) -> bool { + code == StatusCode::Ok +} + +#[no_mangle] +extern "C" fn haku_status_string(code: StatusCode) -> *const i8 { + match code { + StatusCode::Ok => c"ok", + StatusCode::ChunkTooBig => c"compiled bytecode is too large", + StatusCode::DiagnosticsEmitted => c"diagnostics were emitted", + StatusCode::TooManyChunks => c"too many registered bytecode chunks", + StatusCode::OutOfRefSlots => c"out of ref slots (did you forget to restore the VM image?)", + StatusCode::EvalException => c"an exception occurred while evaluating your code", + StatusCode::RenderException => c"an exception occurred while rendering your brush", + } + .as_ptr() +} + +#[derive(Debug, Default)] +enum BrushState { + #[default] + Default, + Ready(ChunkId), +} + +#[derive(Debug, Default)] +struct Brush { + diagnostics: Vec, + state: BrushState, +} + +#[no_mangle] +extern "C" fn haku_brush_new() -> *mut Brush { + Box::leak(Box::new(Brush::default())) +} + +#[no_mangle] +unsafe extern "C" fn haku_brush_destroy(brush: *mut Brush) { + drop(Box::from_raw(brush)) +} + +#[no_mangle] +unsafe extern "C" fn haku_num_diagnostics(brush: *const Brush) -> u32 { + (*brush).diagnostics.len() as u32 +} + +#[no_mangle] +unsafe extern "C" fn haku_diagnostic_start(brush: *const Brush, index: u32) -> u32 { + (*brush).diagnostics[index as usize].span.start as u32 +} + +#[no_mangle] +unsafe extern "C" fn haku_diagnostic_end(brush: *const Brush, index: u32) -> u32 { + (*brush).diagnostics[index as usize].span.end as u32 +} + +#[no_mangle] +unsafe extern "C" fn haku_diagnostic_message(brush: *const Brush, index: u32) -> *const u8 { + (*brush).diagnostics[index as usize].message.as_ptr() +} + +#[no_mangle] +unsafe extern "C" fn haku_diagnostic_message_len(brush: *const Brush, index: u32) -> u32 { + (*brush).diagnostics[index as usize].message.len() as u32 +} + +#[no_mangle] +unsafe extern "C" fn haku_compile_brush( + instance: *mut Instance, + out_brush: *mut Brush, + code_len: u32, + code: *const u8, +) -> StatusCode { + info!("compiling brush"); + + let instance = &mut *instance; + let brush = &mut *out_brush; + + *brush = Brush::default(); + + let code = core::str::from_utf8(slice::from_raw_parts(code, code_len as usize)) + .expect("invalid UTF-8"); + + let ast = Ast::new(instance.limits.ast_capacity); + let mut parser = Parser::new(ast, code); + let root = parse_toplevel(&mut parser); + let ast = parser.ast; + + let src = Source { + code, + ast: &ast, + system: &instance.system, + }; + + let mut chunk = Chunk::new(instance.limits.chunk_capacity).unwrap(); + let mut compiler = Compiler::new(&mut instance.defs, &mut chunk); + if let Err(error) = compile_expr(&mut compiler, &src, root) { + match error { + CompileError::Emit => return StatusCode::ChunkTooBig, + } + } + + if !compiler.diagnostics.is_empty() { + brush.diagnostics = compiler.diagnostics; + return StatusCode::DiagnosticsEmitted; + } + + let chunk_id = match instance.system.add_chunk(chunk) { + Ok(chunk_id) => chunk_id, + Err(_) => return StatusCode::TooManyChunks, + }; + brush.state = BrushState::Ready(chunk_id); + + info!("brush compiled into {chunk_id:?}"); + + StatusCode::Ok +} + +struct BitmapLock { + bitmap: Option, +} + +#[no_mangle] +extern "C" fn haku_bitmap_new(width: u32, height: u32) -> *mut BitmapLock { + Box::leak(Box::new(BitmapLock { + bitmap: Some(Bitmap::new(width, height)), + })) +} + +#[no_mangle] +unsafe extern "C" fn haku_bitmap_destroy(bitmap: *mut BitmapLock) { + drop(Box::from_raw(bitmap)) +} + +#[no_mangle] +unsafe extern "C" fn haku_bitmap_data(bitmap: *mut BitmapLock) -> *mut u8 { + let bitmap = (*bitmap) + .bitmap + .as_mut() + .expect("bitmap is already being rendered to"); + + bitmap.pixels[..].as_mut_ptr() as *mut u8 +} + +#[no_mangle] +unsafe extern "C" fn haku_render_brush( + instance: *mut Instance, + brush: *const Brush, + bitmap: *mut BitmapLock, +) -> StatusCode { + let instance = &mut *instance; + let brush = &*brush; + + let BrushState::Ready(chunk_id) = brush.state else { + panic!("brush is not compiled and ready to be used"); + }; + + let Ok(closure_id) = instance.vm.create_ref(Ref::Closure(Closure { + start: BytecodeLoc { + chunk_id, + offset: 0, + }, + name: FunctionName::Anonymous, + param_count: 0, + captures: Vec::new(), + })) else { + return StatusCode::OutOfRefSlots; + }; + + let scribble = match instance.vm.run(&instance.system, closure_id) { + Ok(value) => value, + Err(exn) => { + instance.exception = Some(exn); + return StatusCode::EvalException; + } + }; + + let bitmap_locked = (*bitmap) + .bitmap + .take() + .expect("bitmap is already being rendered to"); + + let mut renderer = Renderer::new( + bitmap_locked, + &RendererLimits { + bitmap_stack_capacity: instance.limits.bitmap_stack_capacity, + transform_stack_capacity: instance.limits.transform_stack_capacity, + }, + ); + match renderer.render(&instance.vm, scribble) { + Ok(()) => (), + Err(exn) => { + instance.exception = Some(exn); + return StatusCode::RenderException; + } + } + + let bitmap_locked = renderer.finish(); + + (*bitmap).bitmap = Some(bitmap_locked); + instance.vm.restore_image(&instance.vm_image); + + StatusCode::Ok +} diff --git a/crates/haku-wasm/src/logging.rs b/crates/haku-wasm/src/logging.rs new file mode 100644 index 0000000..a0c5010 --- /dev/null +++ b/crates/haku-wasm/src/logging.rs @@ -0,0 +1,44 @@ +use alloc::format; + +use log::{info, Log}; + +extern "C" { + fn trace(message_len: u32, message: *const u8); + fn debug(message_len: u32, message: *const u8); + fn info(message_len: u32, message: *const u8); + fn warn(message_len: u32, message: *const u8); + fn error(message_len: u32, message: *const u8); +} + +struct ConsoleLogger; + +impl Log for ConsoleLogger { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let s = record + .module_path() + .map(|module_path| format!("{module_path}: {}", record.args())) + .unwrap_or_else(|| format!("{}", record.args())); + unsafe { + match record.level() { + log::Level::Error => error(s.len() as u32, s.as_ptr()), + log::Level::Warn => warn(s.len() as u32, s.as_ptr()), + log::Level::Info => info(s.len() as u32, s.as_ptr()), + log::Level::Debug => debug(s.len() as u32, s.as_ptr()), + log::Level::Trace => trace(s.len() as u32, s.as_ptr()), + } + } + } + + fn flush(&self) {} +} + +#[no_mangle] +extern "C" fn haku_init_logging() { + log::set_logger(&ConsoleLogger).unwrap(); + log::set_max_level(log::LevelFilter::Trace); + info!("enabled logging"); +} diff --git a/crates/haku-wasm/src/panicking.rs b/crates/haku-wasm/src/panicking.rs new file mode 100644 index 0000000..50f2dfa --- /dev/null +++ b/crates/haku-wasm/src/panicking.rs @@ -0,0 +1,20 @@ +use core::fmt::Write; + +use alloc::string::String; + +extern "C" { + fn panic(message_len: u32, message: *const u8) -> !; +} + +fn panic_impl(info: &core::panic::PanicInfo) -> ! { + let mut message = String::new(); + _ = write!(&mut message, "{info}"); + + unsafe { panic(message.len() as u32, message.as_ptr()) }; +} + +#[cfg(not(test))] +#[panic_handler] +fn panic_handler(info: &core::panic::PanicInfo) -> ! { + panic_impl(info) +} diff --git a/crates/haku/Cargo.toml b/crates/haku/Cargo.toml new file mode 100644 index 0000000..12dbb16 --- /dev/null +++ b/crates/haku/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "haku" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/haku/src/bytecode.rs b/crates/haku/src/bytecode.rs new file mode 100644 index 0000000..82932bc --- /dev/null +++ b/crates/haku/src/bytecode.rs @@ -0,0 +1,266 @@ +use core::{ + fmt::{self, Display}, + mem::transmute, +}; + +use alloc::{borrow::ToOwned, string::String, vec::Vec}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Opcode { + // Push literal values onto the stack. + Nil, + False, + True, + Number, // (float: f32) + + // Duplicate existing values. + /// Push a value relative to the bottom of the current stack window. + Local, // (index: u8) + /// Push a captured value. + Capture, // (index: u8) + /// Get the value of a definition. + Def, // (index: u16) + /// Set the value of a definition. + SetDef, // (index: u16) + + /// Drop `number` values from the stack. + /// + DropLet, // (number: u8) + + // Create literal functions. + Function, // (params: u8, then: u16), at `then`: (capture_count: u8, captures: [(source: u8, index: u8); capture_count]) + + // Control flow. + Jump, // (offset: u16) + JumpIfNot, // (offset: u16) + + // Function calls. + Call, // (argc: u8) + /// This is a fast path for system calls, which are quite common (e.g. basic arithmetic.) + System, // (index: u8, argc: u8) + + Return, + // NOTE: There must be no more opcodes after this. + // They will get treated as invalid. +} + +// Constants used by the Function opcode to indicate capture sources. +pub const CAPTURE_LOCAL: u8 = 0; +pub const CAPTURE_CAPTURE: u8 = 1; + +#[derive(Debug, Clone)] +pub struct Chunk { + pub bytecode: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Offset(u16); + +impl Chunk { + pub fn new(capacity: usize) -> Result { + if capacity <= (1 << 16) { + Ok(Chunk { + bytecode: Vec::with_capacity(capacity), + }) + } else { + Err(ChunkSizeError) + } + } + + pub fn offset(&self) -> Offset { + Offset(self.bytecode.len() as u16) + } + + pub fn emit_bytes(&mut self, bytes: &[u8]) -> Result { + if self.bytecode.len() + bytes.len() > self.bytecode.capacity() { + return Err(EmitError); + } + + let offset = Offset(self.bytecode.len() as u16); + self.bytecode.extend_from_slice(bytes); + + Ok(offset) + } + + pub fn emit_opcode(&mut self, opcode: Opcode) -> Result { + self.emit_bytes(&[opcode as u8]) + } + + pub fn emit_u8(&mut self, x: u8) -> Result { + self.emit_bytes(&[x]) + } + + pub fn emit_u16(&mut self, x: u16) -> Result { + self.emit_bytes(&x.to_le_bytes()) + } + + pub fn emit_u32(&mut self, x: u32) -> Result { + self.emit_bytes(&x.to_le_bytes()) + } + + pub fn emit_f32(&mut self, x: f32) -> Result { + self.emit_bytes(&x.to_le_bytes()) + } + + pub fn patch_u8(&mut self, offset: Offset, x: u8) { + self.bytecode[offset.0 as usize] = x; + } + + pub fn patch_u16(&mut self, offset: Offset, x: u16) { + let b = x.to_le_bytes(); + let i = offset.0 as usize; + self.bytecode[i] = b[0]; + self.bytecode[i + 1] = b[1]; + } + + pub fn patch_offset(&mut self, offset: Offset, x: Offset) { + self.patch_u16(offset, x.0); + } + + // NOTE: I'm aware these aren't the fastest implementations since they validate quite a lot + // during runtime, but this is just an MVP. It doesn't have to be blazingly fast. + + pub fn read_u8(&self, pc: &mut usize) -> Result { + let x = self.bytecode.get(*pc).copied(); + *pc += 1; + x.ok_or(ReadError) + } + + pub fn read_u16(&self, pc: &mut usize) -> Result { + let xs = &self.bytecode[*pc..*pc + 2]; + *pc += 2; + Ok(u16::from_le_bytes(xs.try_into().map_err(|_| ReadError)?)) + } + + pub fn read_u32(&self, pc: &mut usize) -> Result { + let xs = &self.bytecode[*pc..*pc + 4]; + *pc += 4; + Ok(u32::from_le_bytes(xs.try_into().map_err(|_| ReadError)?)) + } + + pub fn read_f32(&self, pc: &mut usize) -> Result { + let xs = &self.bytecode[*pc..*pc + 4]; + *pc += 4; + Ok(f32::from_le_bytes(xs.try_into().map_err(|_| ReadError)?)) + } + + pub fn read_opcode(&self, pc: &mut usize) -> Result { + let x = self.read_u8(pc)?; + if x <= Opcode::Return as u8 { + Ok(unsafe { transmute::(x) }) + } else { + Err(ReadError) + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkSizeError; + +impl Display for ChunkSizeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "chunk size must be less than 64 KiB") + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EmitError; + +impl Display for EmitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "out of space in chunk") + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ReadError; + +impl Display for ReadError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid bytecode: out of bounds read or invalid opcode") + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct DefId(u16); + +impl DefId { + pub fn to_u16(self) -> u16 { + self.0 + } +} + +#[derive(Debug, Clone)] +pub struct Defs { + defs: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct DefsImage { + defs: usize, +} + +impl Defs { + pub fn new(capacity: usize) -> Self { + assert!(capacity < u16::MAX as usize + 1); + Self { + defs: Vec::with_capacity(capacity), + } + } + + pub fn len(&self) -> u16 { + self.defs.len() as u16 + } + + pub fn is_empty(&self) -> bool { + self.len() != 0 + } + + pub fn get(&mut self, name: &str) -> Option { + self.defs + .iter() + .position(|n| *n == name) + .map(|index| DefId(index as u16)) + } + + pub fn add(&mut self, name: &str) -> Result { + if self.defs.iter().any(|n| n == name) { + Err(DefError::Exists) + } else { + if self.defs.len() >= self.defs.capacity() { + return Err(DefError::OutOfSpace); + } + let id = DefId(self.defs.len() as u16); + self.defs.push(name.to_owned()); + Ok(id) + } + } + + pub fn image(&self) -> DefsImage { + DefsImage { + defs: self.defs.len(), + } + } + + pub fn restore_image(&mut self, image: &DefsImage) { + self.defs.resize_with(image.defs, || { + panic!("image must be a subset of the current defs") + }); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DefError { + Exists, + OutOfSpace, +} + +impl Display for DefError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + DefError::Exists => "definition already exists", + DefError::OutOfSpace => "too many definitions", + }) + } +} diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs new file mode 100644 index 0000000..fd8b080 --- /dev/null +++ b/crates/haku/src/compiler.rs @@ -0,0 +1,625 @@ +use core::{ + error::Error, + fmt::{self, Display}, +}; + +use alloc::vec::Vec; + +use crate::{ + bytecode::{Chunk, DefError, DefId, Defs, EmitError, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL}, + sexp::{Ast, NodeId, NodeKind, Span}, + system::System, +}; + +pub struct Source<'a> { + pub code: &'a str, + pub ast: &'a Ast, + pub system: &'a System, +} + +#[derive(Debug, Clone, Copy)] +pub struct Diagnostic { + pub span: Span, + pub message: &'static str, +} + +#[derive(Debug, Clone, Copy)] +struct Local<'a> { + name: &'a str, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Variable { + Local(u8), + Captured(u8), +} + +struct Scope<'a> { + locals: Vec>, + captures: Vec, +} + +pub struct Compiler<'a, 'b> { + pub defs: &'a mut Defs, + pub chunk: &'b mut Chunk, + pub diagnostics: Vec, + scopes: Vec>, +} + +impl<'a, 'b> Compiler<'a, 'b> { + pub fn new(defs: &'a mut Defs, chunk: &'b mut Chunk) -> Self { + Self { + defs, + chunk, + diagnostics: Vec::with_capacity(16), + scopes: Vec::from_iter([Scope { + locals: Vec::new(), + captures: Vec::new(), + }]), + } + } + + pub fn diagnose(&mut self, diagnostic: Diagnostic) { + if self.diagnostics.len() >= self.diagnostics.capacity() { + return; + } + + if self.diagnostics.len() == self.diagnostics.capacity() - 1 { + self.diagnostics.push(Diagnostic { + span: Span::new(0, 0), + message: "too many diagnostics emitted, stopping", // hello clangd! + }) + } else { + self.diagnostics.push(diagnostic); + } + } +} + +type CompileResult = Result; + +pub fn compile_expr<'a>( + c: &mut Compiler<'a, '_>, + src: &Source<'a>, + node_id: NodeId, +) -> CompileResult { + let node = src.ast.get(node_id); + match node.kind { + NodeKind::Eof => unreachable!("eof node should never be emitted"), + + NodeKind::Nil => compile_nil(c), + NodeKind::Ident => compile_ident(c, src, node_id), + NodeKind::Number => compile_number(c, src, node_id), + NodeKind::List(_, _) => compile_list(c, src, node_id), + NodeKind::Toplevel(_) => compile_toplevel(c, src, node_id), + + NodeKind::Error(message) => { + c.diagnose(Diagnostic { + span: node.span, + message, + }); + Ok(()) + } + } +} + +fn compile_nil(c: &mut Compiler<'_, '_>) -> CompileResult { + c.chunk.emit_opcode(Opcode::Nil)?; + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CaptureError; + +fn find_variable( + c: &mut Compiler<'_, '_>, + name: &str, + scope_index: usize, +) -> Result, CaptureError> { + let scope = &c.scopes[scope_index]; + if let Some(index) = scope.locals.iter().rposition(|l| l.name == name) { + let index = u8::try_from(index).expect("a function must not declare more than 256 locals"); + Ok(Some(Variable::Local(index))) + } else if scope_index > 0 { + // Search upper scope if not found. + if let Some(variable) = find_variable(c, name, scope_index - 1)? { + let scope = &mut c.scopes[scope_index]; + let capture_index = scope + .captures + .iter() + .position(|c| c == &variable) + .unwrap_or_else(|| { + let new_index = scope.captures.len(); + scope.captures.push(variable); + new_index + }); + let capture_index = u8::try_from(capture_index).map_err(|_| CaptureError)?; + Ok(Some(Variable::Captured(capture_index))) + } else { + Ok(None) + } + } else { + Ok(None) + } +} + +fn compile_ident<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, node_id: NodeId) -> CompileResult { + let ident = src.ast.get(node_id); + let name = ident.span.slice(src.code); + + match name { + "false" => _ = c.chunk.emit_opcode(Opcode::False)?, + "true" => _ = c.chunk.emit_opcode(Opcode::True)?, + _ => match find_variable(c, name, c.scopes.len() - 1) { + Ok(Some(Variable::Local(index))) => { + c.chunk.emit_opcode(Opcode::Local)?; + c.chunk.emit_u8(index)?; + } + Ok(Some(Variable::Captured(index))) => { + c.chunk.emit_opcode(Opcode::Capture)?; + c.chunk.emit_u8(index)?; + } + Ok(None) => { + if let Some(def_id) = c.defs.get(name) { + c.chunk.emit_opcode(Opcode::Def)?; + c.chunk.emit_u16(def_id.to_u16())?; + } else { + c.diagnose(Diagnostic { + span: ident.span, + message: "undefined variable", + }); + } + } + Err(CaptureError) => { + c.diagnose(Diagnostic { + span: ident.span, + message: "too many variables captured from outer functions in this scope", + }); + } + }, + } + + Ok(()) +} + +fn compile_number(c: &mut Compiler<'_, '_>, src: &Source<'_>, node_id: NodeId) -> CompileResult { + let node = src.ast.get(node_id); + + let literal = node.span.slice(src.code); + let float: f32 = literal + .parse() + .expect("the parser should've gotten us a string parsable by the stdlib"); + + c.chunk.emit_opcode(Opcode::Number)?; + c.chunk.emit_f32(float)?; + + Ok(()) +} + +fn compile_list<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, node_id: NodeId) -> CompileResult { + let NodeKind::List(function_id, args) = src.ast.get(node_id).kind else { + unreachable!("compile_list expects a List"); + }; + + let function = src.ast.get(function_id); + let name = function.span.slice(src.code); + + if function.kind == NodeKind::Ident { + match name { + "fn" => return compile_fn(c, src, args), + "if" => return compile_if(c, src, args), + "let" => return compile_let(c, src, args), + _ => (), + }; + } + + let mut argument_count = 0; + let mut args = args; + while let NodeKind::List(head, tail) = src.ast.get(args).kind { + compile_expr(c, src, head)?; + argument_count += 1; + args = tail; + } + + let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { + c.diagnose(Diagnostic { + span: src.ast.get(args).span, + message: "function call has too many arguments", + }); + 0 + }); + + if let (NodeKind::Ident, Some(index)) = (function.kind, (src.system.resolve_fn)(name)) { + c.chunk.emit_opcode(Opcode::System)?; + c.chunk.emit_u8(index)?; + c.chunk.emit_u8(argument_count)?; + } else { + // This is a bit of an oddity: we only emit the function expression _after_ the arguments, + // but since the language is effectless this doesn't matter in practice. + // It makes for less code in the compiler and the VM. + compile_expr(c, src, function_id)?; + c.chunk.emit_opcode(Opcode::Call)?; + c.chunk.emit_u8(argument_count)?; + } + + Ok(()) +} + +struct WalkList { + current: NodeId, + ok: bool, +} + +impl WalkList { + fn new(start: NodeId) -> Self { + Self { + current: start, + ok: true, + } + } + + fn expect_arg( + &mut self, + c: &mut Compiler<'_, '_>, + src: &Source<'_>, + message: &'static str, + ) -> NodeId { + if !self.ok { + return NodeId::NIL; + } + + if let NodeKind::List(expr, tail) = src.ast.get(self.current).kind { + self.current = tail; + expr + } else { + c.diagnose(Diagnostic { + span: src.ast.get(self.current).span, + message, + }); + self.ok = false; + NodeId::NIL + } + } + + fn expect_nil(&mut self, c: &mut Compiler<'_, '_>, src: &Source<'_>, message: &'static str) { + if src.ast.get(self.current).kind != NodeKind::Nil { + c.diagnose(Diagnostic { + span: src.ast.get(self.current).span, + message, + }); + // NOTE: Don't set self.ok to false, since this is not a fatal error. + // The nodes returned previously are valid and therefore it's safe to operate on them. + // Just having extra arguments shouldn't inhibit emitting additional diagnostics in + // the expression. + } + } +} + +fn compile_if<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult { + let mut list = WalkList::new(args); + + let condition = list.expect_arg(c, src, "missing `if` condition"); + let if_true = list.expect_arg(c, src, "missing `if` true branch"); + let if_false = list.expect_arg(c, src, "missing `if` false branch"); + list.expect_nil(c, src, "extra arguments after `if` false branch"); + + if !list.ok { + return Ok(()); + } + + compile_expr(c, src, condition)?; + + c.chunk.emit_opcode(Opcode::JumpIfNot)?; + let false_jump_offset_offset = c.chunk.emit_u16(0)?; + + compile_expr(c, src, if_true)?; + c.chunk.emit_opcode(Opcode::Jump)?; + let true_jump_offset_offset = c.chunk.emit_u16(0)?; + + let false_jump_offset = c.chunk.offset(); + c.chunk + .patch_offset(false_jump_offset_offset, false_jump_offset); + compile_expr(c, src, if_false)?; + + let true_jump_offset = c.chunk.offset(); + c.chunk + .patch_offset(true_jump_offset_offset, true_jump_offset); + + Ok(()) +} + +fn compile_let<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult { + let mut list = WalkList::new(args); + + let binding_list = list.expect_arg(c, src, "missing `let` binding list ((x 1) (y 2) ...)"); + let expr = list.expect_arg(c, src, "missing expression to `let` names into"); + list.expect_nil(c, src, "extra arguments after `let` expression"); + + if !list.ok { + return Ok(()); + } + + // NOTE: Our `let` behaves like `let*` from Lisps. + // This is because this is generally the more intuitive behaviour with how variable declarations + // work in traditional imperative languages. + // We do not offer an alternative to Lisp `let` to be as minimal as possible. + + let mut current = binding_list; + let mut local_count: usize = 0; + while let NodeKind::List(head, tail) = src.ast.get(current).kind { + if !matches!(src.ast.get(head).kind, NodeKind::List(_, _)) { + c.diagnose(Diagnostic { + span: src.ast.get(head).span, + message: "`let` binding expected, like (x 1)", + }); + current = tail; + continue; + } + + let mut list = WalkList::new(head); + let ident = list.expect_arg(c, src, "binding name expected"); + let value = list.expect_arg(c, src, "binding value expected"); + list.expect_nil(c, src, "extra expressions after `let` binding value"); + + if src.ast.get(ident).kind != NodeKind::Ident { + c.diagnose(Diagnostic { + span: src.ast.get(ident).span, + message: "binding name must be an identifier", + }); + } + + // NOTE: Compile expression _before_ putting the value into scope. + // This is so that the variable cannot refer to itself, as it is yet to be declared. + compile_expr(c, src, value)?; + + let name = src.ast.get(ident).span.slice(src.code); + let scope = c.scopes.last_mut().unwrap(); + if scope.locals.len() >= u8::MAX as usize { + c.diagnose(Diagnostic { + span: src.ast.get(ident).span, + message: "too many names bound in this function at a single time", + }); + } else { + scope.locals.push(Local { name }); + } + + local_count += 1; + current = tail; + } + + compile_expr(c, src, expr)?; + + let scope = c.scopes.last_mut().unwrap(); + scope + .locals + .resize_with(scope.locals.len() - local_count, || unreachable!()); + + // NOTE: If we reach more than 255 locals declared in our `let`, we should've gotten + // a diagnostic emitted in the `while` loop beforehand. + let local_count = u8::try_from(local_count).unwrap_or(0); + c.chunk.emit_opcode(Opcode::DropLet)?; + c.chunk.emit_u8(local_count)?; + + Ok(()) +} + +fn compile_fn<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult { + let mut list = WalkList::new(args); + + let param_list = list.expect_arg(c, src, "missing function parameters"); + let body = list.expect_arg(c, src, "missing function body"); + list.expect_nil(c, src, "extra arguments after function body"); + + if !list.ok { + return Ok(()); + } + + let mut locals = Vec::new(); + let mut current = param_list; + while let NodeKind::List(ident, tail) = src.ast.get(current).kind { + if let NodeKind::Ident = src.ast.get(ident).kind { + locals.push(Local { + name: src.ast.get(ident).span.slice(src.code), + }) + } else { + c.diagnose(Diagnostic { + span: src.ast.get(ident).span, + message: "function parameters must be identifiers", + }) + } + current = tail; + } + + let param_count = u8::try_from(locals.len()).unwrap_or_else(|_| { + c.diagnose(Diagnostic { + span: src.ast.get(param_list).span, + message: "too many function parameters", + }); + 0 + }); + + c.chunk.emit_opcode(Opcode::Function)?; + c.chunk.emit_u8(param_count)?; + let after_offset = c.chunk.emit_u16(0)?; + + c.scopes.push(Scope { + locals, + captures: Vec::new(), + }); + compile_expr(c, src, body)?; + c.chunk.emit_opcode(Opcode::Return)?; + + let after = u16::try_from(c.chunk.bytecode.len()).expect("chunk is too large"); + c.chunk.patch_u16(after_offset, after); + + let scope = c.scopes.pop().unwrap(); + let capture_count = u8::try_from(scope.captures.len()).unwrap_or_else(|_| { + c.diagnose(Diagnostic { + span: src.ast.get(body).span, + message: "function refers to too many variables from the outer function", + }); + 0 + }); + c.chunk.emit_u8(capture_count)?; + for capture in scope.captures { + match capture { + // TODO: There's probably a more clever way to encode these than wasting an entire byte + // on what's effectively just a bool per each capture. + Variable::Local(index) => { + c.chunk.emit_u8(CAPTURE_LOCAL)?; + c.chunk.emit_u8(index)?; + } + Variable::Captured(index) => { + c.chunk.emit_u8(CAPTURE_CAPTURE)?; + c.chunk.emit_u8(index)?; + } + } + } + + Ok(()) +} + +fn compile_toplevel<'a>( + c: &mut Compiler<'a, '_>, + src: &Source<'a>, + node_id: NodeId, +) -> CompileResult { + let NodeKind::Toplevel(mut current) = src.ast.get(node_id).kind else { + unreachable!("compile_toplevel expects a Toplevel"); + }; + + def_prepass(c, src, current)?; + + let mut had_result = false; + while let NodeKind::List(expr, tail) = src.ast.get(current).kind { + match compile_toplevel_expr(c, src, expr)? { + ToplevelExpr::Def => (), + ToplevelExpr::Result => had_result = true, + } + + if had_result && src.ast.get(tail).kind != NodeKind::Nil { + c.diagnose(Diagnostic { + span: src.ast.get(tail).span, + message: "result value may not be followed by anything else", + }); + break; + } + + current = tail; + } + + if !had_result { + c.chunk.emit_opcode(Opcode::Nil)?; + } + c.chunk.emit_opcode(Opcode::Return)?; + + Ok(()) +} + +fn def_prepass<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, node_id: NodeId) -> CompileResult { + // This is a bit of a pattern matching tapeworm, but Rust unfortunately doesn't have `if let` + // chains yet to make this more readable. + let mut current = node_id; + while let NodeKind::List(expr, tail) = src.ast.get(current).kind { + if let NodeKind::List(head_id, tail_id) = src.ast.get(expr).kind { + let head = src.ast.get(head_id); + let name = head.span.slice(src.code); + if head.kind == NodeKind::Ident && name == "def" { + if let NodeKind::List(ident_id, _) = src.ast.get(tail_id).kind { + let ident = src.ast.get(ident_id); + if ident.kind == NodeKind::Ident { + let name = ident.span.slice(src.code); + match c.defs.add(name) { + Ok(_) => (), + Err(DefError::Exists) => c.diagnose(Diagnostic { + span: ident.span, + message: "redefinitions of defs are not allowed", + }), + Err(DefError::OutOfSpace) => c.diagnose(Diagnostic { + span: ident.span, + message: "too many defs", + }), + } + } + } + } + } + + current = tail; + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ToplevelExpr { + Def, + Result, +} + +fn compile_toplevel_expr<'a>( + c: &mut Compiler<'a, '_>, + src: &Source<'a>, + node_id: NodeId, +) -> CompileResult { + let node = src.ast.get(node_id); + + if let NodeKind::List(head_id, tail_id) = node.kind { + let head = src.ast.get(head_id); + if head.kind == NodeKind::Ident { + let name = head.span.slice(src.code); + if name == "def" { + compile_def(c, src, tail_id)?; + return Ok(ToplevelExpr::Def); + } + } + } + + compile_expr(c, src, node_id)?; + Ok(ToplevelExpr::Result) +} + +fn compile_def<'a>(c: &mut Compiler<'a, '_>, src: &Source<'a>, args: NodeId) -> CompileResult { + let mut list = WalkList::new(args); + + let ident = list.expect_arg(c, src, "missing definition name"); + let value = list.expect_arg(c, src, "missing definition value"); + list.expect_nil(c, src, "extra arguments after definition"); + + if !list.ok { + return Ok(()); + } + + let name = src.ast.get(ident).span.slice(src.code); + // NOTE: def_prepass collects all definitions beforehand. + // In case a def ends up not existing, that means we ran out of space for defs - so emit a + // zero def instead. + let def_id = c.defs.get(name).unwrap_or_default(); + + compile_expr(c, src, value)?; + c.chunk.emit_opcode(Opcode::SetDef)?; + c.chunk.emit_u16(def_id.to_u16())?; + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompileError { + Emit, +} + +impl From for CompileError { + fn from(_: EmitError) -> Self { + Self::Emit + } +} + +impl Display for CompileError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + CompileError::Emit => "bytecode is too big", + }) + } +} + +impl Error for CompileError {} diff --git a/crates/haku/src/lib.rs b/crates/haku/src/lib.rs new file mode 100644 index 0000000..22aecc8 --- /dev/null +++ b/crates/haku/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] + +extern crate alloc; + +pub mod bytecode; +pub mod compiler; +pub mod render; +pub mod sexp; +pub mod system; +pub mod value; +pub mod vm; diff --git a/crates/haku/src/render.rs b/crates/haku/src/render.rs new file mode 100644 index 0000000..8068c5d --- /dev/null +++ b/crates/haku/src/render.rs @@ -0,0 +1,144 @@ +use core::iter; + +use alloc::vec::Vec; + +use crate::{ + value::{Ref, Rgba, Scribble, Shape, Stroke, Value, Vec4}, + vm::{Exception, Vm}, +}; + +pub struct Bitmap { + pub width: u32, + pub height: u32, + pub pixels: Vec, +} + +impl Bitmap { + pub fn new(width: u32, height: u32) -> Self { + Self { + width, + height, + pixels: Vec::from_iter( + iter::repeat(Rgba::default()).take(width as usize * height as usize), + ), + } + } + + pub fn pixel_index(&self, x: u32, y: u32) -> usize { + x as usize + y as usize * self.width as usize + } + + pub fn get(&self, x: u32, y: u32) -> Rgba { + self.pixels[self.pixel_index(x, y)] + } + + pub fn set(&mut self, x: u32, y: u32, rgba: Rgba) { + let index = self.pixel_index(x, y); + self.pixels[index] = rgba; + } +} + +pub struct RendererLimits { + pub bitmap_stack_capacity: usize, + pub transform_stack_capacity: usize, +} + +pub struct Renderer { + bitmap_stack: Vec, + transform_stack: Vec, +} + +impl Renderer { + pub fn new(bitmap: Bitmap, limits: &RendererLimits) -> Self { + assert!(limits.bitmap_stack_capacity > 0); + assert!(limits.transform_stack_capacity > 0); + + let mut blend_stack = Vec::with_capacity(limits.bitmap_stack_capacity); + blend_stack.push(bitmap); + + let mut transform_stack = Vec::with_capacity(limits.transform_stack_capacity); + transform_stack.push(Vec4::default()); + + Self { + bitmap_stack: blend_stack, + transform_stack, + } + } + + fn create_exception(_vm: &Vm, _at: Value, message: &'static str) -> Exception { + Exception { message } + } + + fn transform(&self) -> &Vec4 { + self.transform_stack.last().unwrap() + } + + fn transform_mut(&mut self) -> &mut Vec4 { + self.transform_stack.last_mut().unwrap() + } + + fn bitmap(&self) -> &Bitmap { + self.bitmap_stack.last().unwrap() + } + + fn bitmap_mut(&mut self) -> &mut Bitmap { + self.bitmap_stack.last_mut().unwrap() + } + + pub fn translate(&mut self, translation: Vec4) { + let transform = self.transform_mut(); + transform.x += translation.x; + transform.y += translation.y; + transform.z += translation.z; + transform.w += translation.w; + } + + pub fn to_bitmap_coords(&self, point: Vec4) -> Option<(u32, u32)> { + let transform = self.transform(); + let x = point.x + transform.x; + let y = point.y + transform.y; + if x >= 0.0 && y >= 0.0 { + let (x, y) = (x as u32, y as u32); + if x < self.bitmap().width && y < self.bitmap().height { + Some((x, y)) + } else { + None + } + } else { + None + } + } + + pub fn render(&mut self, vm: &Vm, value: Value) -> Result<(), Exception> { + static NOT_A_SCRIBBLE: &str = "cannot draw something that is not a scribble"; + let (_id, scribble) = vm + .get_ref_value(value) + .ok_or_else(|| Self::create_exception(vm, value, NOT_A_SCRIBBLE))?; + let Ref::Scribble(scribble) = scribble else { + return Err(Self::create_exception(vm, value, NOT_A_SCRIBBLE)); + }; + + match scribble { + Scribble::Stroke(stroke) => self.render_stroke(vm, value, stroke)?, + } + + Ok(()) + } + + fn render_stroke(&mut self, _vm: &Vm, _value: Value, stroke: &Stroke) -> Result<(), Exception> { + match stroke.shape { + Shape::Point(vec) => { + if let Some((x, y)) = self.to_bitmap_coords(vec) { + // TODO: thickness + self.bitmap_mut().set(x, y, stroke.color); + } + } + } + + Ok(()) + } + + pub fn finish(mut self) -> Bitmap { + self.bitmap_stack.drain(..).next().unwrap() + } +} diff --git a/crates/haku/src/sexp.rs b/crates/haku/src/sexp.rs new file mode 100644 index 0000000..5444688 --- /dev/null +++ b/crates/haku/src/sexp.rs @@ -0,0 +1,476 @@ +use core::{cell::Cell, fmt}; + +use alloc::vec::Vec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl Span { + pub fn new(start: usize, end: usize) -> Self { + Self { start, end } + } + + pub fn slice<'a>(&self, source: &'a str) -> &'a str { + &source[self.start..self.end] + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeId(usize); + +impl NodeId { + pub const NIL: NodeId = NodeId(0); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NodeKind { + Nil, + Eof, + + // Atoms + Ident, + Number, + + List(NodeId, NodeId), + Toplevel(NodeId), + + Error(&'static str), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Node { + pub span: Span, + pub kind: NodeKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Ast { + pub nodes: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AstWriteMode { + Compact, + Spans, +} + +impl Ast { + pub fn new(capacity: usize) -> Self { + assert!(capacity >= 1, "there must be space for at least a nil node"); + + let mut ast = Self { + nodes: Vec::with_capacity(capacity), + }; + + ast.alloc(Node { + span: Span::new(0, 0), + kind: NodeKind::Nil, + }) + .unwrap(); + + ast + } + + pub fn alloc(&mut self, node: Node) -> Result { + if self.nodes.len() >= self.nodes.capacity() { + return Err(NodeAllocError); + } + + let index = self.nodes.len(); + self.nodes.push(node); + Ok(NodeId(index)) + } + + pub fn get(&self, node_id: NodeId) -> &Node { + &self.nodes[node_id.0] + } + + pub fn get_mut(&mut self, node_id: NodeId) -> &mut Node { + &mut self.nodes[node_id.0] + } + + pub fn write( + &self, + source: &str, + node_id: NodeId, + w: &mut dyn fmt::Write, + mode: AstWriteMode, + ) -> fmt::Result { + #[allow(clippy::too_many_arguments)] + fn write_list( + ast: &Ast, + source: &str, + w: &mut dyn fmt::Write, + mode: AstWriteMode, + mut head: NodeId, + mut tail: NodeId, + sep_element: &str, + sep_tail: &str, + ) -> fmt::Result { + loop { + write_rec(ast, source, w, mode, head)?; + match ast.get(tail).kind { + NodeKind::Nil => break, + NodeKind::List(head2, tail2) => { + w.write_str(sep_element)?; + (head, tail) = (head2, tail2); + } + _ => { + w.write_str(sep_tail)?; + write_rec(ast, source, w, mode, tail)?; + break; + } + } + } + Ok(()) + } + + // NOTE: Separated out to a separate function in case we ever want to introduce auto-indentation. + fn write_rec( + ast: &Ast, + source: &str, + w: &mut dyn fmt::Write, + mode: AstWriteMode, + node_id: NodeId, + ) -> fmt::Result { + let node = ast.get(node_id); + match &node.kind { + NodeKind::Nil => write!(w, "()")?, + NodeKind::Eof => write!(w, "")?, + NodeKind::Ident | NodeKind::Number => write!(w, "{}", node.span.slice(source))?, + + NodeKind::List(head, tail) => { + w.write_char('(')?; + write_list(ast, source, w, mode, *head, *tail, " ", " . ")?; + w.write_char(')')?; + } + + NodeKind::Toplevel(list) => { + let NodeKind::List(head, tail) = ast.get(*list).kind else { + unreachable!("child of Toplevel must be a List"); + }; + + write_list(ast, source, w, mode, head, tail, "\n", " . ")?; + } + + NodeKind::Error(message) => write!(w, "#error({message})")?, + } + + if mode == AstWriteMode::Spans { + write!(w, "@{}..{}", node.span.start, node.span.end)?; + } + + Ok(()) + } + + write_rec(self, source, w, mode, node_id)?; + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct NodeAllocError; + +pub struct Parser<'a> { + pub ast: Ast, + input: &'a str, + position: usize, + fuel: Cell, + alloc_error: NodeId, +} + +impl<'a> Parser<'a> { + const FUEL: usize = 256; + + pub fn new(mut ast: Ast, input: &'a str) -> Self { + let alloc_error = ast + .alloc(Node { + span: Span::new(0, 0), + kind: NodeKind::Error("program is too big"), + }) + .expect("there is not enough space in the arena for an error node"); + + Self { + ast, + input, + position: 0, + fuel: Cell::new(Self::FUEL), + alloc_error, + } + } + + pub fn current(&self) -> char { + assert_ne!(self.fuel.get(), 0, "parser is stuck"); + self.fuel.set(self.fuel.get() - 1); + + self.input[self.position..].chars().next().unwrap_or('\0') + } + + pub fn advance(&mut self) { + self.position += self.current().len_utf8(); + self.fuel.set(Self::FUEL); + } + + pub fn alloc(&mut self, expr: Node) -> NodeId { + self.ast.alloc(expr).unwrap_or(self.alloc_error) + } +} + +pub fn skip_whitespace_and_comments(p: &mut Parser<'_>) { + loop { + match p.current() { + ' ' | '\t' | '\n' => { + p.advance(); + continue; + } + ';' => { + while p.current() != '\n' { + p.advance(); + } + } + _ => break, + } + } +} + +fn is_decimal_digit(c: char) -> bool { + c.is_ascii_digit() +} + +pub fn parse_number(p: &mut Parser<'_>) -> NodeKind { + while is_decimal_digit(p.current()) { + p.advance(); + } + if p.current() == '.' { + p.advance(); + if !is_decimal_digit(p.current()) { + return NodeKind::Error("missing digits after decimal point '.' in number literal"); + } + while is_decimal_digit(p.current()) { + p.advance(); + } + } + + NodeKind::Number +} + +fn is_ident(c: char) -> bool { + // The identifier character set is quite limited to help with easy expansion in the future. + // Rationale: + // - alphabet and digits are pretty obvious + // - '-' and '_' can be used for identifier separators, whichever you prefer. + // - '+', '-', '*', '/', '^' are for arithmetic. + // - '=', '!', '<', '>' are fore comparison. + // - '\' is for builtin string constants, such as \n. + // For other operators, it's generally clearer to use words (such as `and` and `or`.) + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '+' | '*' | '/' | '\\' | '^' | '!' | '=' | '<' | '>') +} + +pub fn parse_ident(p: &mut Parser<'_>) -> NodeKind { + while is_ident(p.current()) { + p.advance(); + } + + NodeKind::Ident +} + +struct List { + head: NodeId, + tail: NodeId, +} + +impl List { + fn new() -> Self { + Self { + head: NodeId::NIL, + tail: NodeId::NIL, + } + } + + fn append(&mut self, p: &mut Parser<'_>, node: NodeId) { + let node_span = p.ast.get(node).span; + + let new_tail = p.alloc(Node { + span: node_span, + kind: NodeKind::List(node, NodeId::NIL), + }); + if self.head == NodeId::NIL { + self.head = new_tail; + self.tail = new_tail; + } else { + let old_tail = p.ast.get_mut(self.tail); + let NodeKind::List(expr_before, _) = old_tail.kind else { + return; + }; + *old_tail = Node { + span: Span::new(old_tail.span.start, node_span.end), + kind: NodeKind::List(expr_before, new_tail), + }; + self.tail = new_tail; + } + } +} + +pub fn parse_list(p: &mut Parser<'_>) -> NodeId { + // This could've been a lot simpler if Rust supported tail recursion. + + let start = p.position; + + p.advance(); // skip past opening parenthesis + skip_whitespace_and_comments(p); + + let mut list = List::new(); + + while p.current() != ')' { + if p.current() == '\0' { + return p.alloc(Node { + span: Span::new(start, p.position), + kind: NodeKind::Error("missing ')' to close '('"), + }); + } + + let expr = parse_expr(p); + skip_whitespace_and_comments(p); + + list.append(p, expr); + } + p.advance(); // skip past closing parenthesis + + // If we didn't have any elements, we must not modify the initial Nil with ID 0. + if list.head == NodeId::NIL { + list.head = p.alloc(Node { + span: Span::new(0, 0), + kind: NodeKind::Nil, + }); + } + + let end = p.position; + p.ast.get_mut(list.head).span = Span::new(start, end); + + list.head +} + +pub fn parse_expr(p: &mut Parser<'_>) -> NodeId { + let start = p.position; + let kind = match p.current() { + '\0' => NodeKind::Eof, + c if is_decimal_digit(c) => parse_number(p), + // NOTE: Because of the `match` order, this prevents identifiers from starting with a digit. + c if is_ident(c) => parse_ident(p), + '(' => return parse_list(p), + _ => { + p.advance(); + NodeKind::Error("unexpected character") + } + }; + let end = p.position; + + p.alloc(Node { + span: Span::new(start, end), + kind, + }) +} + +pub fn parse_toplevel(p: &mut Parser<'_>) -> NodeId { + let start = p.position; + + let mut nodes = List::new(); + + skip_whitespace_and_comments(p); + while p.current() != '\0' { + let expr = parse_expr(p); + skip_whitespace_and_comments(p); + + nodes.append(p, expr); + } + + let end = p.position; + + p.alloc(Node { + span: Span::new(start, end), + kind: NodeKind::Toplevel(nodes.head), + }) +} + +#[cfg(test)] +mod tests { + use core::error::Error; + + use alloc::{boxed::Box, string::String}; + + use super::*; + + #[track_caller] + fn parse( + f: fn(&mut Parser<'_>) -> NodeId, + source: &str, + expected: &str, + ) -> Result<(), Box> { + let ast = Ast::new(16); + let mut p = Parser::new(ast, source); + let node = f(&mut p); + let ast = p.ast; + + let mut s = String::new(); + ast.write(source, node, &mut s, AstWriteMode::Spans)?; + + assert_eq!(s, expected); + + Ok(()) + } + + #[test] + fn parse_number() -> Result<(), Box> { + parse(parse_expr, "123", "123@0..3")?; + parse(parse_expr, "123.456", "123.456@0..7")?; + Ok(()) + } + + #[test] + fn parse_ident() -> Result<(), Box> { + parse(parse_expr, "abc", "abc@0..3")?; + parse(parse_expr, "abcABC_01234", "abcABC_01234@0..12")?; + parse(parse_expr, "+-*/\\^!=<>", "+-*/\\^!=<>@0..10")?; + Ok(()) + } + + #[test] + fn parse_list() -> Result<(), Box> { + parse(parse_expr, "()", "()@0..2")?; + parse(parse_expr, "(a a)", "(a@1..2 a@3..4)@0..5")?; + parse(parse_expr, "(a a a)", "(a@1..2 a@3..4 a@5..6)@0..7")?; + parse(parse_expr, "(() ())", "(()@1..3 ()@4..6)@0..7")?; + parse( + parse_expr, + "(nestedy (nest OwO))", + "(nestedy@1..8 (nest@10..14 OwO@15..18)@9..19)@0..20", + )?; + Ok(()) + } + + #[test] + fn oom() -> Result<(), Box> { + parse(parse_expr, "(a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..17")?; + parse(parse_expr, "(a a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..19")?; + parse(parse_expr, "(a a a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..21")?; + parse(parse_expr, "(a a a a a a a a a a a)", "(a@1..2 a@3..4 a@5..6 a@7..8 a@9..10 a@11..12 a@13..14 . #error(program is too big)@0..0)@0..23")?; + Ok(()) + } + + #[test] + fn toplevel() -> Result<(), Box> { + parse( + parse_toplevel, + r#" + (hello world) + (abc) + "#, + "(hello@18..23 world@24..29)@17..30\n(abc@48..51)@47..52@0..65", + )?; + Ok(()) + } +} diff --git a/crates/haku/src/system.rs b/crates/haku/src/system.rs new file mode 100644 index 0000000..15bf591 --- /dev/null +++ b/crates/haku/src/system.rs @@ -0,0 +1,440 @@ +use core::{ + error::Error, + fmt::{self, Display}, +}; + +use alloc::vec::Vec; + +use crate::{ + bytecode::Chunk, + value::Value, + vm::{Exception, FnArgs, Vm}, +}; + +pub type SystemFn = fn(&mut Vm, FnArgs) -> Result; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkId(u32); + +#[derive(Debug, Clone)] +pub struct System { + /// Resolves a system function name to an index into `fn`s. + pub resolve_fn: fn(&str) -> Option, + pub fns: [Option; 256], + pub chunks: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct SystemImage { + chunks: usize, +} + +macro_rules! def_fns { + ($($index:tt $name:tt => $fnref:expr),* $(,)?) => { + pub(crate) fn init_fns(system: &mut System) { + $( + debug_assert!(system.fns[$index].is_none()); + system.fns[$index] = Some($fnref); + )* + } + + pub(crate) fn resolve(name: &str) -> Option { + match name { + $($name => Some($index),)* + _ => None, + } + } + }; +} + +impl System { + pub fn new(max_chunks: usize) -> Self { + assert!(max_chunks < u32::MAX as usize); + + let mut system = Self { + resolve_fn: Self::resolve, + fns: [None; 256], + chunks: Vec::with_capacity(max_chunks), + }; + Self::init_fns(&mut system); + system + } + + pub fn add_chunk(&mut self, chunk: Chunk) -> Result { + if self.chunks.len() >= self.chunks.capacity() { + return Err(ChunkError); + } + + let id = ChunkId(self.chunks.len() as u32); + self.chunks.push(chunk); + Ok(id) + } + + pub fn chunk(&self, id: ChunkId) -> &Chunk { + &self.chunks[id.0 as usize] + } + + pub fn image(&self) -> SystemImage { + SystemImage { + chunks: self.chunks.len(), + } + } + + pub fn restore_image(&mut self, image: &SystemImage) { + self.chunks.resize_with(image.chunks, || { + panic!("image must be a subset of the current system") + }); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ChunkError; + +impl Display for ChunkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("too many chunks") + } +} + +impl Error for ChunkError {} + +pub mod fns { + use crate::{ + value::{Ref, Rgba, Scribble, Shape, Stroke, Value, Vec4}, + vm::{Exception, FnArgs, Vm}, + }; + + use super::System; + + impl System { + def_fns! { + 0x00 "+" => add, + 0x01 "-" => sub, + 0x02 "*" => mul, + 0x03 "/" => div, + + 0x40 "not" => not, + 0x41 "=" => eq, + 0x42 "<>" => neq, + 0x43 "<" => lt, + 0x44 "<=" => leq, + 0x45 ">" => gt, + 0x46 ">=" => geq, + + 0x80 "vec" => vec, + 0x81 ".x" => vec_x, + 0x82 ".y" => vec_y, + 0x83 ".z" => vec_z, + 0x84 ".w" => vec_w, + + 0x85 "rgba" => rgba, + 0x86 ".r" => rgba_r, + 0x87 ".g" => rgba_g, + 0x88 ".b" => rgba_b, + 0x89 ".a" => rgba_a, + + 0xc0 "to-shape" => to_shape_f, + 0xc1 "stroke" => stroke, + } + } + + pub fn add(vm: &mut Vm, args: FnArgs) -> Result { + let mut result = 0.0; + for i in 0..args.num() { + result += args.get_number(vm, i, "arguments to (+) must be numbers")?; + } + Ok(Value::Number(result)) + } + + pub fn sub(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() < 1 { + return Err(vm.create_exception("(-) requires at least one argument to subtract from")); + } + + static ERROR: &str = "arguments to (-) must be numbers"; + + if args.num() == 1 { + Ok(Value::Number(-args.get_number(vm, 0, ERROR)?)) + } else { + let mut result = args.get_number(vm, 0, ERROR)?; + for i in 1..args.num() { + result -= args.get_number(vm, i, ERROR)?; + } + + Ok(Value::Number(result)) + } + } + + pub fn mul(vm: &mut Vm, args: FnArgs) -> Result { + let mut result = 1.0; + for i in 0..args.num() { + result *= args.get_number(vm, i, "arguments to (*) must be numbers")?; + } + + Ok(Value::Number(result)) + } + + pub fn div(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() < 1 { + return Err(vm.create_exception("(/) requires at least one argument to divide")); + } + + static ERROR: &str = "arguments to (/) must be numbers"; + let mut result = args.get_number(vm, 0, ERROR)?; + for i in 1..args.num() { + result /= args.get_number(vm, i, ERROR)?; + } + + Ok(Value::Number(result)) + } + + pub fn not(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(not) expects a single argument to negate")); + } + + let value = args.get(vm, 0); + Ok(Value::from(value.is_falsy())) + } + + pub fn eq(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 2 { + return Err(vm.create_exception("(=) expects two arguments to compare")); + } + + let a = args.get(vm, 0); + let b = args.get(vm, 1); + Ok(Value::from(a == b)) + } + + pub fn neq(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 2 { + return Err(vm.create_exception("(<>) expects two arguments to compare")); + } + + let a = args.get(vm, 0); + let b = args.get(vm, 1); + Ok(Value::from(a != b)) + } + + pub fn lt(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 2 { + return Err(vm.create_exception("(<) expects two arguments to compare")); + } + + let a = args.get(vm, 0); + let b = args.get(vm, 1); + Ok(Value::from(a < b)) + } + + pub fn leq(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 2 { + return Err(vm.create_exception("(<=) expects two arguments to compare")); + } + + let a = args.get(vm, 0); + let b = args.get(vm, 1); + Ok(Value::from(a <= b)) + } + + pub fn gt(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 2 { + return Err(vm.create_exception("(>) expects two arguments to compare")); + } + + let a = args.get(vm, 0); + let b = args.get(vm, 1); + Ok(Value::from(a > b)) + } + + pub fn geq(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 2 { + return Err(vm.create_exception("(>=) expects two arguments to compare")); + } + + let a = args.get(vm, 0); + let b = args.get(vm, 1); + Ok(Value::from(a >= b)) + } + + pub fn vec(vm: &mut Vm, args: FnArgs) -> Result { + static ERROR: &str = "arguments to (vec) must be numbers (vec x y z w)"; + match args.num() { + 0 => Ok(Value::Vec4(Vec4 { + x: 0.0, + y: 0.0, + z: 0.0, + w: 0.0, + })), + 1 => { + let x = args.get_number(vm, 0, ERROR)?; + Ok(Value::Vec4(Vec4 { + x, + y: 0.0, + z: 0.0, + w: 0.0, + })) + } + 2 => { + let x = args.get_number(vm, 0, ERROR)?; + let y = args.get_number(vm, 1, ERROR)?; + Ok(Value::Vec4(Vec4 { + x, + y, + z: 0.0, + w: 0.0, + })) + } + 3 => { + let x = args.get_number(vm, 0, ERROR)?; + let y = args.get_number(vm, 1, ERROR)?; + let z = args.get_number(vm, 2, ERROR)?; + Ok(Value::Vec4(Vec4 { x, y, z, w: 0.0 })) + } + 4 => { + let x = args.get_number(vm, 0, ERROR)?; + let y = args.get_number(vm, 1, ERROR)?; + let z = args.get_number(vm, 2, ERROR)?; + let w = args.get_number(vm, 3, ERROR)?; + Ok(Value::Vec4(Vec4 { x, y, z, w })) + } + _ => Err(vm.create_exception("(vec) expects 0-4 arguments (vec x y z w)")), + } + } + + pub fn vec_x(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.x) expects a single argument (.x vec)")); + } + + let vec = args.get_vec4(vm, 0, "argument to (.x vec) must be a (vec)")?; + Ok(Value::Number(vec.x)) + } + + pub fn vec_y(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.y) expects a single argument (.y vec)")); + } + + let vec = args.get_vec4(vm, 0, "argument to (.y vec) must be a (vec)")?; + Ok(Value::Number(vec.y)) + } + + pub fn vec_z(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.z) expects a single argument (.z vec)")); + } + + let vec = args.get_vec4(vm, 0, "argument to (.z vec) must be a (vec)")?; + Ok(Value::Number(vec.z)) + } + + pub fn vec_w(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.w) expects a single argument (.w vec)")); + } + + let vec = args.get_vec4(vm, 0, "argument to (.w vec) must be a (vec)")?; + Ok(Value::Number(vec.w)) + } + + pub fn rgba(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 4 { + return Err(vm.create_exception("(rgba) expects four arguments (rgba r g b a)")); + } + + static ERROR: &str = "arguments to (rgba r g b a) must be numbers"; + let r = args.get_number(vm, 0, ERROR)?; + let g = args.get_number(vm, 1, ERROR)?; + let b = args.get_number(vm, 2, ERROR)?; + let a = args.get_number(vm, 3, ERROR)?; + + Ok(Value::Rgba(Rgba { r, g, b, a })) + } + + pub fn rgba_r(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.r) expects a single argument (.r rgba)")); + } + + let rgba = args.get_rgba(vm, 0, "argument to (.r rgba) must be an (rgba)")?; + Ok(Value::Number(rgba.r)) + } + + pub fn rgba_g(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.g) expects a single argument (.g rgba)")); + } + + let rgba = args.get_rgba(vm, 0, "argument to (.g rgba) must be an (rgba)")?; + Ok(Value::Number(rgba.g)) + } + + pub fn rgba_b(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.b) expects a single argument (.b rgba)")); + } + + let rgba = args.get_rgba(vm, 0, "argument to (.b rgba) must be an (rgba)")?; + Ok(Value::Number(rgba.r)) + } + + pub fn rgba_a(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(.a) expects a single argument (.a rgba)")); + } + + let rgba = args.get_rgba(vm, 0, "argument to (.a rgba) must be an (rgba)")?; + Ok(Value::Number(rgba.r)) + } + + fn to_shape(value: Value, _vm: &Vm) -> Option { + match value { + Value::Nil + | Value::False + | Value::True + | Value::Number(_) + | Value::Rgba(_) + | Value::Ref(_) => None, + Value::Vec4(vec) => Some(Shape::Point(vec)), + } + } + + pub fn to_shape_f(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 1 { + return Err(vm.create_exception("(shape) expects 1 argument (shape value)")); + } + + if let Some(shape) = to_shape(args.get(vm, 0), vm) { + let id = vm.create_ref(Ref::Shape(shape))?; + Ok(Value::Ref(id)) + } else { + Ok(Value::Nil) + } + } + + pub fn stroke(vm: &mut Vm, args: FnArgs) -> Result { + if args.num() != 3 { + return Err( + vm.create_exception("(stroke) expects 3 arguments (stroke thickness color shape)") + ); + } + + let thickness = args.get_number( + vm, + 0, + "1st argument to (stroke) must be a thickness in pixels (number)", + )?; + let color = args.get_rgba(vm, 1, "2nd argument to (stroke) must be a color (rgba)")?; + if let Some(shape) = to_shape(args.get(vm, 2), vm) { + let id = vm.create_ref(Ref::Scribble(Scribble::Stroke(Stroke { + thickness, + color, + shape, + })))?; + Ok(Value::Ref(id)) + } else { + Ok(Value::Nil) + } + } +} diff --git a/crates/haku/src/value.rs b/crates/haku/src/value.rs new file mode 100644 index 0000000..834b56c --- /dev/null +++ b/crates/haku/src/value.rs @@ -0,0 +1,161 @@ +use alloc::vec::Vec; + +use crate::system::ChunkId; + +// TODO: Probably needs some pretty hardcore space optimization. +// Maybe when we have static typing. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub enum Value { + Nil, + False, + True, + Number(f32), + Vec4(Vec4), + Rgba(Rgba), + Ref(RefId), +} + +impl Value { + pub fn is_falsy(&self) -> bool { + matches!(self, Self::Nil | Self::False) + } + + pub fn is_truthy(&self) -> bool { + !self.is_falsy() + } + + pub fn to_number(&self) -> Option { + match self { + Self::Number(v) => Some(*v), + _ => None, + } + } + + pub fn to_vec4(&self) -> Option { + match self { + Self::Vec4(v) => Some(*v), + _ => None, + } + } + + pub fn to_rgba(&self) -> Option { + match self { + Self::Rgba(v) => Some(*v), + _ => None, + } + } +} + +impl From<()> for Value { + fn from(_: ()) -> Self { + Self::Nil + } +} + +impl From for Value { + fn from(value: bool) -> Self { + match value { + true => Self::True, + false => Self::False, + } + } +} + +impl From for Value { + fn from(value: f32) -> Self { + Self::Number(value) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct Vec4 { + pub x: f32, + pub y: f32, + pub z: f32, + pub w: f32, +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +#[repr(C)] +pub struct Rgba { + pub r: f32, + pub g: f32, + pub b: f32, + pub a: f32, +} + +// NOTE: This is not a pointer, because IDs are safer and easier to clone. +// +// Since this only ever refers to refs inside the current VM, there is no need to walk through all +// the values and update pointers when a VM is cloned. +// +// This ensures it's quick and easy to spin up a new VM from an existing image, as well as being +// extremely easy to serialize a VM image into a file for quick loading back later. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RefId(pub(crate) u32); + +impl RefId { + // DO NOT USE outside tests! + #[doc(hidden)] + pub fn from_u32(x: u32) -> Self { + Self(x) + } +} + +#[derive(Debug, Clone)] +pub enum Ref { + Closure(Closure), + Shape(Shape), + Scribble(Scribble), +} + +impl Ref { + pub fn as_closure(&self) -> Option<&Closure> { + match self { + Self::Closure(v) => Some(v), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BytecodeLoc { + pub chunk_id: ChunkId, + pub offset: u16, +} + +#[derive(Debug, Clone, Copy)] +pub struct BytecodeSpan { + pub loc: BytecodeLoc, + pub len: u16, +} + +#[derive(Debug, Clone, Copy)] +pub enum FunctionName { + Anonymous, +} + +#[derive(Debug, Clone)] +pub struct Closure { + pub start: BytecodeLoc, + pub name: FunctionName, + pub param_count: u8, + pub captures: Vec, +} + +#[derive(Debug, Clone)] +pub enum Shape { + Point(Vec4), +} + +#[derive(Debug, Clone)] +pub struct Stroke { + pub thickness: f32, + pub color: Rgba, + pub shape: Shape, +} + +#[derive(Debug, Clone)] +pub enum Scribble { + Stroke(Stroke), +} diff --git a/crates/haku/src/vm.rs b/crates/haku/src/vm.rs new file mode 100644 index 0000000..a73331d --- /dev/null +++ b/crates/haku/src/vm.rs @@ -0,0 +1,486 @@ +use core::{ + error::Error, + fmt::{self, Display}, + iter, +}; + +use alloc::vec::Vec; + +use crate::{ + bytecode::{self, Defs, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL}, + system::{ChunkId, System}, + value::{BytecodeLoc, Closure, FunctionName, Ref, RefId, Rgba, Value, Vec4}, +}; + +pub struct VmLimits { + pub stack_capacity: usize, + pub call_stack_capacity: usize, + pub ref_capacity: usize, + pub fuel: usize, +} + +#[derive(Debug, Clone)] +pub struct Vm { + stack: Vec, + call_stack: Vec, + refs: Vec, + defs: Vec, + fuel: usize, +} + +#[derive(Debug, Clone, Copy)] +pub struct VmImage { + refs: usize, + defs: usize, + fuel: usize, +} + +#[derive(Debug, Clone)] +struct CallFrame { + closure_id: RefId, + chunk_id: ChunkId, + pc: usize, + bottom: usize, +} + +struct Context { + fuel: usize, +} + +impl Vm { + pub fn new(defs: &Defs, limits: &VmLimits) -> Self { + Self { + stack: Vec::with_capacity(limits.stack_capacity), + call_stack: Vec::with_capacity(limits.call_stack_capacity), + refs: Vec::with_capacity(limits.ref_capacity), + defs: Vec::from_iter(iter::repeat(Value::Nil).take(defs.len() as usize)), + fuel: limits.fuel, + } + } + + pub fn remaining_fuel(&self) -> usize { + self.fuel + } + + pub fn set_fuel(&mut self, fuel: usize) { + self.fuel = fuel; + } + + pub fn image(&self) -> VmImage { + assert!( + self.stack.is_empty() && self.call_stack.is_empty(), + "cannot image VM while running code" + ); + VmImage { + refs: self.refs.len(), + defs: self.defs.len(), + fuel: self.fuel, + } + } + + pub fn restore_image(&mut self, image: &VmImage) { + assert!( + self.stack.is_empty() && self.call_stack.is_empty(), + "cannot restore VM image while running code" + ); + self.refs.resize_with(image.refs, || { + panic!("image must be a subset of the current VM") + }); + self.defs.resize_with(image.defs, || { + panic!("image must be a subset of the current VM") + }); + self.fuel = image.fuel; + } + + pub fn apply_defs(&mut self, defs: &Defs) { + assert!( + defs.len() as usize >= self.defs.len(), + "defs must be a superset of the current VM" + ); + self.defs.resize(defs.len() as usize, Value::Nil); + } + + fn push(&mut self, value: Value) -> Result<(), Exception> { + if self.stack.len() >= self.stack.capacity() { + // TODO: can this error message be made clearer? + return Err(self.create_exception("too many local variables")); + } + self.stack.push(value); + Ok(()) + } + + fn get(&mut self, index: usize) -> Result { + self.stack.get(index).copied().ok_or_else(|| { + self.create_exception("corrupted bytecode (local variable out of bounds)") + }) + } + + fn pop(&mut self) -> Result { + self.stack + .pop() + .ok_or_else(|| self.create_exception("corrupted bytecode (value stack underflow)")) + } + + fn push_call(&mut self, frame: CallFrame) -> Result<(), Exception> { + if self.call_stack.len() >= self.call_stack.capacity() { + return Err(self.create_exception("too much recursion")); + } + self.call_stack.push(frame); + Ok(()) + } + + fn pop_call(&mut self) -> Result { + self.call_stack + .pop() + .ok_or_else(|| self.create_exception("corrupted bytecode (call stack underflow)")) + } + + pub fn run(&mut self, system: &System, mut closure_id: RefId) -> Result { + let closure = self + .get_ref(closure_id) + .as_closure() + .expect("a Closure-type Ref must be passed to `run`"); + + let mut chunk_id = closure.start.chunk_id; + let mut chunk = system.chunk(chunk_id); + let mut pc = closure.start.offset as usize; + let mut bottom = self.stack.len(); + let mut fuel = self.fuel; + + #[allow(unused)] + let closure = (); // Do not use `closure` after this! Use `get_ref` on `closure_id` instead. + + self.push_call(CallFrame { + closure_id, + chunk_id, + pc, + bottom, + })?; + + loop { + fuel = fuel + .checked_sub(1) + .ok_or_else(|| self.create_exception("code ran for too long"))?; + + let opcode = chunk.read_opcode(&mut pc)?; + match opcode { + Opcode::Nil => self.push(Value::Nil)?, + Opcode::False => self.push(Value::False)?, + Opcode::True => self.push(Value::True)?, + + Opcode::Number => { + let x = chunk.read_f32(&mut pc)?; + self.push(Value::Number(x))?; + } + + Opcode::Local => { + let index = chunk.read_u8(&mut pc)? as usize; + let value = self.get(bottom + index)?; + self.push(value)?; + } + + Opcode::Capture => { + let index = chunk.read_u8(&mut pc)? as usize; + let closure = self.get_ref(closure_id).as_closure().unwrap(); + self.push(closure.captures.get(index).copied().ok_or_else(|| { + self.create_exception("corrupted bytecode (capture index out of bounds)") + })?)?; + } + + Opcode::Def => { + let index = chunk.read_u16(&mut pc)? as usize; + self.push(self.defs.get(index).copied().ok_or_else(|| { + self.create_exception("corrupted bytecode (def index out of bounds)") + })?)? + } + + Opcode::SetDef => { + let index = chunk.read_u16(&mut pc)? as usize; + let value = self.pop()?; + if let Some(def) = self.defs.get_mut(index) { + *def = value; + } else { + return Err(self + .create_exception("corrupted bytecode (set def index out of bounds)")); + } + } + + Opcode::DropLet => { + let count = chunk.read_u8(&mut pc)? as usize; + if count != 0 { + let new_len = self.stack.len().checked_sub(count).ok_or_else(|| { + self.create_exception( + "corrupted bytecode (Drop tried to drop too many values off the stack)", + ) + })?; + let value = self.pop()?; + self.stack.resize_with(new_len, || unreachable!()); + self.push(value)?; + } + } + + Opcode::Function => { + let param_count = chunk.read_u8(&mut pc)?; + let then = chunk.read_u16(&mut pc)? as usize; + let body = pc; + pc = then; + + let capture_count = chunk.read_u8(&mut pc)? as usize; + let mut captures = Vec::with_capacity(capture_count); + for _ in 0..capture_count { + let capture_kind = chunk.read_u8(&mut pc)?; + let index = chunk.read_u8(&mut pc)? as usize; + captures.push(match capture_kind { + CAPTURE_LOCAL => self.get(bottom + index)?, + CAPTURE_CAPTURE => { + let closure = self.get_ref(closure_id).as_closure().unwrap(); + closure.captures.get(index).copied().ok_or_else(|| { + self.create_exception( + "corrupted bytecode (captured capture index out of bounds)", + ) + })? + } + _ => Value::Nil, + }) + } + + let id = self.create_ref(Ref::Closure(Closure { + start: BytecodeLoc { + chunk_id, + offset: body as u16, + }, + name: FunctionName::Anonymous, + param_count, + captures, + }))?; + self.push(Value::Ref(id))?; + } + + Opcode::Jump => { + let offset = chunk.read_u16(&mut pc)? as usize; + pc = offset; + } + + Opcode::JumpIfNot => { + let offset = chunk.read_u16(&mut pc)? as usize; + let value = self.pop()?; + if !value.is_truthy() { + pc = offset; + } + } + + Opcode::Call => { + let argument_count = chunk.read_u8(&mut pc)? as usize; + + let function_value = self.pop()?; + let Some((called_closure_id, Ref::Closure(closure))) = + self.get_ref_value(function_value) + else { + return Err(self.create_exception("attempt to call non-function value")); + }; + + // TODO: Varargs? + if argument_count != closure.param_count as usize { + // Would be nice if we told the user the exact counts. + return Err(self.create_exception("function parameter count mismatch")); + } + + let frame = CallFrame { + closure_id, + chunk_id, + pc, + bottom, + }; + + closure_id = called_closure_id; + chunk_id = closure.start.chunk_id; + chunk = system.chunk(chunk_id); + pc = closure.start.offset as usize; + bottom = self + .stack + .len() + .checked_sub(argument_count) + .ok_or_else(|| { + self.create_exception( + "corrupted bytecode (not enough values on the stack for arguments)", + ) + })?; + + self.push_call(frame)?; + } + + Opcode::System => { + let index = chunk.read_u8(&mut pc)? as usize; + let argument_count = chunk.read_u8(&mut pc)? as usize; + let system_fn = system.fns.get(index).copied().flatten().ok_or_else(|| { + self.create_exception("corrupted bytecode (invalid system function index)") + })?; + + self.store_context(Context { fuel }); + let result = system_fn( + self, + FnArgs { + base: self + .stack + .len() + .checked_sub(argument_count) + .ok_or_else(|| self.create_exception("corrupted bytecode (not enough values on the stack for arguments)"))?, + len: argument_count, + }, + )?; + Context { fuel } = self.restore_context(); + + self.stack + .resize_with(self.stack.len() - argument_count, || unreachable!()); + self.push(result)?; + } + + Opcode::Return => { + let value = self.pop()?; + let frame = self.pop_call()?; + + debug_assert!(bottom <= self.stack.len()); + self.stack.resize_with(bottom, || unreachable!()); + self.push(value)?; + + // Once the initial frame is popped, halt the VM. + if self.call_stack.is_empty() { + self.store_context(Context { fuel }); + break; + } + + CallFrame { + closure_id, + chunk_id, + pc, + bottom, + } = frame; + chunk = system.chunk(chunk_id); + } + } + } + + Ok(self + .stack + .pop() + .expect("there should be a result at the top of the stack")) + } + + fn store_context(&mut self, context: Context) { + self.fuel = context.fuel; + } + + fn restore_context(&mut self) -> Context { + Context { fuel: self.fuel } + } + + pub fn create_ref(&mut self, r: Ref) -> Result { + if self.refs.len() >= self.refs.capacity() { + return Err(self.create_exception("too many value allocations")); + } + + let id = RefId(self.refs.len() as u32); + self.refs.push(r); + Ok(id) + } + + pub fn get_ref(&self, id: RefId) -> &Ref { + &self.refs[id.0 as usize] + } + + pub fn get_ref_value(&self, value: Value) -> Option<(RefId, &Ref)> { + match value { + Value::Ref(id) => Some((id, self.get_ref(id))), + _ => None, + } + } + + pub fn create_exception(&self, message: &'static str) -> Exception { + Exception { message } + } +} + +pub struct FnArgs { + base: usize, + len: usize, +} + +impl FnArgs { + pub fn num(&self) -> usize { + self.len + } + + pub fn try_get(&self, vm: &Vm, index: usize) -> Option { + if index < self.len { + Some(vm.stack[self.base + index]) + } else { + None + } + } + + // The following are #[inline(never)] wrappers for common operations to reduce code size. + + #[inline(never)] + pub fn get(&self, vm: &Vm, index: usize) -> Value { + self.try_get(vm, index) + .expect("argument was expected, but got None") + } + + #[inline(never)] + pub fn get_number( + &self, + vm: &Vm, + index: usize, + message: &'static str, + ) -> Result { + self.get(vm, index) + .to_number() + .ok_or_else(|| vm.create_exception(message)) + } + + #[inline(never)] + pub fn get_vec4( + &self, + vm: &Vm, + index: usize, + message: &'static str, + ) -> Result { + self.get(vm, index) + .to_vec4() + .ok_or_else(|| vm.create_exception(message)) + } + + #[inline(never)] + pub fn get_rgba( + &self, + vm: &Vm, + index: usize, + message: &'static str, + ) -> Result { + self.get(vm, index) + .to_rgba() + .ok_or_else(|| vm.create_exception(message)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Exception { + pub message: &'static str, +} + +impl From for Exception { + fn from(_: bytecode::ReadError) -> Self { + Self { + message: "corrupted bytecode", + } + } +} + +impl Display for Exception { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // NOTE: This is not a user-friendly representation! + write!(f, "{self:#?}") + } +} + +impl Error for Exception {} diff --git a/crates/haku/tests/language.rs b/crates/haku/tests/language.rs new file mode 100644 index 0000000..746068a --- /dev/null +++ b/crates/haku/tests/language.rs @@ -0,0 +1,256 @@ +use std::error::Error; + +use haku::{ + bytecode::{Chunk, Defs}, + compiler::{compile_expr, Compiler, Source}, + sexp::{self, Ast, Parser}, + system::System, + value::{BytecodeLoc, Closure, FunctionName, Ref, RefId, Value}, + vm::{Vm, VmLimits}, +}; + +fn eval(code: &str) -> Result> { + let mut system = System::new(1); + + let ast = Ast::new(1024); + let mut parser = Parser::new(ast, code); + let root = sexp::parse_toplevel(&mut parser); + let ast = parser.ast; + let src = Source { + code, + ast: &ast, + system: &system, + }; + + let mut defs = Defs::new(256); + let mut chunk = Chunk::new(65536).unwrap(); + let mut compiler = Compiler::new(&mut defs, &mut chunk); + compile_expr(&mut compiler, &src, root)?; + let defs = compiler.defs; + + for diagnostic in &compiler.diagnostics { + println!( + "{}..{}: {}", + diagnostic.span.start, diagnostic.span.end, diagnostic.message + ); + } + + if !compiler.diagnostics.is_empty() { + panic!("compiler diagnostics were emitted") + } + + let limits = VmLimits { + stack_capacity: 256, + call_stack_capacity: 256, + ref_capacity: 256, + fuel: 32768, + }; + let mut vm = Vm::new(defs, &limits); + let chunk_id = system.add_chunk(chunk)?; + println!("bytecode: {:?}", system.chunk(chunk_id)); + + let closure = vm.create_ref(Ref::Closure(Closure { + start: BytecodeLoc { + chunk_id, + offset: 0, + }, + name: FunctionName::Anonymous, + param_count: 0, + captures: Vec::new(), + }))?; + let result = vm.run(&system, closure)?; + + println!("used fuel: {}", limits.fuel - vm.remaining_fuel()); + + Ok(result) +} + +#[track_caller] +fn expect_number(code: &str, number: f32, epsilon: f32) { + match eval(code) { + Ok(Value::Number(n)) => assert!((n - number).abs() < epsilon, "expected {number}, got {n}"), + other => panic!("expected ok/numeric result, got {other:?}"), + } +} + +#[test] +fn literal_nil() { + assert_eq!(eval("()").unwrap(), Value::Nil); +} + +#[test] +fn literal_number() { + expect_number("123", 123.0, 0.0001); +} + +#[test] +fn literal_bool() { + assert_eq!(eval("false").unwrap(), Value::False); + assert_eq!(eval("true").unwrap(), Value::True); +} + +#[test] +fn function_nil() { + assert_eq!(eval("(fn () ())").unwrap(), Value::Ref(RefId::from_u32(1))); +} + +#[test] +fn function_nil_call() { + assert_eq!(eval("((fn () ()))").unwrap(), Value::Nil); +} + +#[test] +fn function_arithmetic() { + expect_number("((fn (x) (+ x 2)) 2)", 4.0, 0.0001); +} + +#[test] +fn function_let() { + expect_number("((fn (add-two) (add-two 2)) (fn (x) (+ x 2)))", 4.0, 0.0001); +} + +#[test] +fn function_closure() { + expect_number("(((fn (x) (fn (y) (+ x y))) 2) 2)", 4.0, 0.0001); +} + +#[test] +fn if_literal() { + expect_number("(if 1 1 2)", 1.0, 0.0001); + expect_number("(if () 1 2)", 2.0, 0.0001); + expect_number("(if false 1 2)", 2.0, 0.0001); + expect_number("(if true 1 2)", 1.0, 0.0001); +} + +#[test] +fn def_simple() { + let code = r#" + (def x 1) + (def y 2) + (+ x y) + "#; + expect_number(code, 3.0, 0.0001); +} + +#[test] +fn def_fib_recursive() { + let code = r#" + (def fib + (fn (n) + (if (< n 2) + n + (+ (fib (- n 1)) (fib (- n 2)))))) + + (fib 10) + "#; + expect_number(code, 55.0, 0.0001); +} + +#[test] +fn def_mutually_recursive() { + let code = r#" + (def f + (fn (x) + (if (< x 10) + (g (+ x 1)) + x))) + + (def g + (fn (x) + (if (< x 10) + (f (* x 2)) + x))) + + (f 0) + "#; + expect_number(code, 14.0, 0.0001); +} + +#[test] +fn let_single() { + let code = r#" + (let ((x 1)) + (+ x 1)) + "#; + expect_number(code, 2.0, 0.0001); +} + +#[test] +fn let_many() { + let code = r#" + (let ((x 1) + (y 2)) + (+ x y)) + "#; + expect_number(code, 3.0, 0.0001); +} + +#[test] +fn let_sequence() { + let code = r#" + (let ((x 1) + (y (+ x 1))) + (+ x y)) + "#; + expect_number(code, 3.0, 0.0001); +} + +#[test] +fn let_subexpr() { + let code = r#" + (+ + (let ((x 1) + (y 2)) + (* x y))) + "#; + expect_number(code, 2.0, 0.0001); +} + +#[test] +fn let_empty() { + let code = r#" + (let () 1) + "#; + expect_number(code, 1.0, 0.0001); +} + +#[test] +fn let_subexpr_empty() { + let code = r#" + (+ (let () 1) (let () 1)) + "#; + expect_number(code, 2.0, 0.0001); +} + +#[test] +fn let_subexpr_many() { + let code = r#" + (+ + (let ((x 1) + (y 2)) + (* x y)) + (let () 1) + (let ((x 1)) x)) + "#; + expect_number(code, 3.0, 0.0001); +} + +#[test] +fn system_arithmetic() { + expect_number("(+ 1 2 3 4)", 10.0, 0.0001); + expect_number("(+ (* 2 1) 1 (/ 6 2) (- 10 3))", 13.0, 0.0001); +} + +#[test] +fn practical_fib_recursive() { + let code = r#" + ((fn (fib) + (fib fib 10)) + + (fn (fib n) + (if (< n 2) + n + (+ (fib fib (- n 1)) (fib fib (- n 2)))))) + "#; + expect_number(code, 55.0, 0.0001); +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..84c237c --- /dev/null +++ b/static/index.html @@ -0,0 +1,18 @@ + + + + + canvane + + + + + +
+ Please enable JavaScript +
+ +

+
+ + diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..5855207 --- /dev/null +++ b/static/index.js @@ -0,0 +1,154 @@ +let panicImpl; +let logImpl; + +function makeLogFunction(level) { + return (length, pMessage) => { + logImpl(level, length, pMessage); + }; +} + +let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming( + fetch(import.meta.resolve("./wasm/haku.wasm")), + { + env: { + panic(length, pMessage) { + panicImpl(length, pMessage); + }, + trace: makeLogFunction("trace"), + debug: makeLogFunction("debug"), + info: makeLogFunction("info"), + warn: makeLogFunction("warn"), + error: makeLogFunction("error"), + }, + }, +); + +let memory = hakuInstance.exports.memory; +let w = hakuInstance.exports; + +let textEncoder = new TextEncoder(); +function allocString(string) { + let size = string.length * 3; + let align = 1; + let pString = w.haku_alloc(size, align); + + let buffer = new Uint8Array(memory.buffer, pString, size); + let result = textEncoder.encodeInto(string, buffer); + + return { + ptr: pString, + length: result.written, + size, + align, + }; +} + +function freeString(alloc) { + w.haku_free(alloc.ptr, alloc.size, alloc.align); +} + +let textDecoder = new TextDecoder(); +function readString(size, pString) { + let buffer = new Uint8Array(memory.buffer, pString, size); + return textDecoder.decode(buffer); +} + +function readCString(pCString) { + let memoryBuffer = new Uint8Array(memory.buffer); + + let pCursor = pCString; + while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) { + pCursor++; + } + + let size = pCursor - pCString; + return readString(size, pCString); +} + +class Panic extends Error { + name = "Panic"; +} + +panicImpl = (length, pMessage) => { + throw new Panic(readString(length, pMessage)); +}; + +logImpl = (level, length, pMessage) => { + console[level](readString(length, pMessage)); +}; + +w.haku_init_logging(); + +/* ------ */ + +let renderCanvas = document.getElementById("render"); +let codeTextArea = document.getElementById("code"); +let outputP = document.getElementById("output"); + +let ctx = renderCanvas.getContext("2d"); + +function rerender() { + console.log("rerender"); + + let width = renderCanvas.width; + let height = renderCanvas.height; + + let logs = []; + + let pInstance = w.haku_instance_new(); + let pBrush = w.haku_brush_new(); + let pBitmap = w.haku_bitmap_new(width, height); + let code = allocString(codeTextArea.value); + let deallocEverything = () => { + freeString(code); + w.haku_bitmap_destroy(pBitmap); + w.haku_brush_destroy(pBrush); + w.haku_instance_destroy(pInstance); + outputP.textContent = logs.join("\n"); + }; + + let compileStatusCode = w.haku_compile_brush(pInstance, pBrush, code.length, code.ptr); + let pCompileStatusString = w.haku_status_string(compileStatusCode); + logs.push(`compile: ${readCString(pCompileStatusString)}`); + + for (let i = 0; i < w.haku_num_diagnostics(pBrush); ++i) { + let start = w.haku_diagnostic_start(pBrush, i); + let end = w.haku_diagnostic_end(pBrush, i); + let length = w.haku_diagnostic_message_len(pBrush, i); + let pMessage = w.haku_diagnostic_message(pBrush, i); + let message = readString(length, pMessage); + logs.push(`${start}..${end}: ${message}`); + } + + if (w.haku_num_diagnostics(pBrush) > 0) { + deallocEverything(); + return; + } + + let renderStatusCode = w.haku_render_brush(pInstance, pBrush, pBitmap); + let pRenderStatusString = w.haku_status_string(renderStatusCode); + logs.push(`render: ${readCString(pRenderStatusString)}`); + + if (w.haku_has_exception(pInstance)) { + let length = w.haku_exception_message_len(pInstance); + let pMessage = w.haku_exception_message(pInstance); + let message = readString(length, pMessage); + logs.push(`exception: ${message}`); + + deallocEverything(); + return; + } + + let pBitmapData = w.haku_bitmap_data(pBitmap); + let bitmapDataBuffer = new Float32Array(memory.buffer, pBitmapData, width * height * 4); + let imageData = new ImageData(width, height); + for (let i = 0; i < bitmapDataBuffer.length; ++i) { + imageData.data[i] = bitmapDataBuffer[i] * 255; + } + ctx.putImageData(imageData, 0, 0); + + deallocEverything(); +} + +rerender(); +codeTextArea.addEventListener("input", rerender); diff --git a/static/live-reload.js b/static/live-reload.js new file mode 100644 index 0000000..b43b973 --- /dev/null +++ b/static/live-reload.js @@ -0,0 +1,16 @@ +// NOTE: The server never fulfills this request, it stalls forever. +// Once the connection is closed, we try to connect with the server until we establish a successful +// connection. Then we reload the page. +await fetch("/dev/live-reload/stall").catch(async () => { + while (true) { + try { + let response = await fetch("/dev/live-reload/back-up"); + if (response.status == 200) { + window.location.reload(); + break; + } + } catch (e) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } +});