diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72dfb93..8207518 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,10 +8,42 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## Unreleased
+## `0.7.7` - August 23rd, 2023
### Added
+- Added a [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) to Lune. ([#83])
+
+ This allows you to run scripts within Lune without writing files!
+
+ Example usage, inside your favorite terminal:
+
+ ```bash
+ # 1. Run the Lune executable, without any arguments
+ lune
+
+ # 2. You will be shown the current Lune version and a blank prompt arrow:
+ Lune v0.7.7
+ >
+
+ # 3. Start typing, and hit enter when you want to run your script!
+ # Your script will run until completion and output things along the way.
+ > print(2 + 3)
+ 5
+ > print("Hello, lune changelog!")
+ Hello, lune changelog!
+
+ # 4. You can also set variables that will get preserved between runs.
+ # Note that local variables do not get preserved here.
+ > myVariable = 123
+ > print(myVariable)
+ 123
+
+ # 5. Press either of these key combinations to exit the REPL:
+ # - Ctrl + D
+ # - Ctrl + C
+ ```
+
- Added a new `luau` built-in library for manually compiling and loading Luau source code. ([#82])
Example usage:
@@ -31,14 +63,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
callableFn2()
```
-- Implemented support for a variable number of arguments for `CFrame` methods in the `roblox` built-in library. ([#85])
+### Changed
+
+- Update to Luau version `0.591`
+- Lune's internal task scheduler and `require` functionality has been completely rewritten.
+ The new scheduler is much more stable, conforms to a larger test suite, and has a few additional benefits:
+
+ - Built-in libraries are now lazily loaded, meaning nothing gets allocated until the built-in library gets loaded using `require("@lune/builtin-name")`. This also improves startup times slightly.
+ - Spawned processes using `process.spawn` now run on different thread(s), freeing up resources for the main thread where luau runs.
+ - Serving requests using `net.serve` now processes requests on background threads, also freeing up resources. In the future, this will also allow us to offload heavy tasks such as compression/decompression to background threads.
+ - Groundwork for custom / user-defined require aliases has been implemented, as well as absolute / cwd-relative requires. These will both be exposed as options and be made available to use some time in the future.
+
+- When using the `serde` built-in library, keys are now sorted during serialization. This means that the output of `encode` is now completely deterministic, and wont cause issues when committing generated files to git etc.
### Fixed
- Fixed not being able to pass arguments to the thread using `coroutine.resume`. ([#86])
+- Fixed a large number of long-standing issues, from the task scheduler rewrite:
+
+ - Fixed `require` hanging indefinitely when the module being require-d uses an async function in its main body.
+ - Fixed background tasks (such as `net.serve`) not keeping Lune alive even if there are no lua threads to run.
+ - Fixed spurious panics and error messages such as `Tried to resume next queued future but none are queued`.
+ - Fixed not being able to catch non-string errors properly, errors were accidentally being wrapped in an opaque `userdata` type.
[#82]: https://github.com/filiptibell/lune/pull/82
-[#85]: https://github.com/filiptibell/lune/pull/85
+[#83]: https://github.com/filiptibell/lune/pull/83
[#86]: https://github.com/filiptibell/lune/pull/86
## `0.7.6` - August 9th, 2023
diff --git a/Cargo.lock b/Cargo.lock
index 10c8d8e..a3744ca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
-version = "0.20.0"
+version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
@@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
-version = "1.0.2"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a"
dependencies = [
"memchr",
]
@@ -73,9 +73,9 @@ dependencies = [
[[package]]
name = "anstyle"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
[[package]]
name = "anstyle-parse"
@@ -107,9 +107,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.72"
+version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "arrayref"
@@ -145,13 +145,13 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.72"
+version = "0.1.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
+checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
@@ -162,9 +162,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
-version = "0.3.68"
+version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
dependencies = [
"addr2line",
"cc",
@@ -201,9 +201,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
-version = "2.3.3"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "blake2b_simd"
@@ -296,9 +296,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]]
name = "cc"
-version = "1.0.82"
+version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
@@ -340,9 +340,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.3.21"
+version = "4.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd"
+checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487"
dependencies = [
"clap_builder",
"clap_derive",
@@ -351,9 +351,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.3.21"
+version = "4.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa"
+checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e"
dependencies = [
"anstream",
"anstyle",
@@ -370,7 +370,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
@@ -488,9 +488,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "deranged"
-version = "0.3.7"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929"
+checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
[[package]]
name = "dialoguer"
@@ -573,9 +573,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
-version = "0.8.32"
+version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
"cfg-if",
]
@@ -607,9 +607,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
-version = "0.3.28"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070"
+checksum = "837c0466252947ada828b975e12daf82e18bb5444e4df87be6038d4469e2a3d2"
dependencies = [
"serde",
]
@@ -664,9 +664,9 @@ dependencies = [
[[package]]
name = "flate2"
-version = "1.0.26"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -710,7 +710,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
@@ -774,9 +774,9 @@ dependencies = [
[[package]]
name = "gimli"
-version = "0.27.3"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]]
name = "glam"
@@ -792,9 +792,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "h2"
-version = "0.3.20"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
+checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [
"bytes",
"fnv",
@@ -872,9 +872,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "humantime"
@@ -1086,22 +1086,22 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.19"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "luau0-src"
-version = "0.6.0+luau588"
+version = "0.7.1+luau591"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c628f5525cc62a89a2d478b2ee619c77b35da55c8e3231752f3b8fe528a6c49"
+checksum = "2fb600eccdbc0bb69e746fddb756559a67b5fcfc01c8a142c6853fec76b6bfc7"
dependencies = [
"cc",
]
[[package]]
name = "lune"
-version = "0.7.6"
+version = "0.7.7"
dependencies = [
"anyhow",
"async-compression",
@@ -1124,6 +1124,7 @@ dependencies = [
"mlua",
"once_cell",
"os_str_bytes",
+ "path-clean",
"pin-project",
"rand",
"rbx_binary",
@@ -1142,6 +1143,8 @@ dependencies = [
"tokio",
"tokio-tungstenite",
"toml",
+ "tracing",
+ "tracing-subscriber",
"urlencoding",
]
@@ -1174,6 +1177,15 @@ dependencies = [
"twox-hash",
]
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
[[package]]
name = "memchr"
version = "2.5.0"
@@ -1208,9 +1220,9 @@ dependencies = [
[[package]]
name = "mlua"
-version = "0.9.0-rc.3"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01a6500a9fb74b519a85ac206cd57f9f91b270ce39d6cb12ab06a8ed29c3563d"
+checksum = "6c3a7a7ff4481ec91b951a733390211a8ace1caba57266ccb5f4d4966704e560"
dependencies = [
"bstr",
"erased-serde",
@@ -1224,9 +1236,9 @@ dependencies = [
[[package]]
name = "mlua-sys"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa5b61f6c943d77dd6ab5f670865670f65b978400127c8bf31c2df7d6e76289a"
+checksum = "3ec8b54eddb76093069cce9eeffb4c7b3a1a0fe66962d7bd44c4867928149ca3"
dependencies = [
"cc",
"cfg-if",
@@ -1255,6 +1267,16 @@ dependencies = [
"static_assertions",
]
+[[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 = "num-integer"
version = "0.1.45"
@@ -1286,9 +1308,9 @@ dependencies = [
[[package]]
name = "object"
-version = "0.31.1"
+version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
+checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
dependencies = [
"memchr",
]
@@ -1323,6 +1345,12 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
[[package]]
name = "parking_lot"
version = "0.12.1"
@@ -1343,7 +1371,7 @@ dependencies = [
"libc",
"redox_syscall 0.3.5",
"smallvec",
- "windows-targets 0.48.1",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -1352,6 +1380,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+[[package]]
+name = "path-clean"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
+
[[package]]
name = "percent-encoding"
version = "2.3.0"
@@ -1375,7 +1409,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
@@ -1407,7 +1441,7 @@ dependencies = [
"line-wrap",
"quick-xml",
"serde",
- "time 0.3.25",
+ "time 0.3.27",
]
[[package]]
@@ -1447,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "097bf8b99121dfb8c75eed54dfbdbdb1d53e372c53d2353e8a15aad2a479249d"
dependencies = [
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
@@ -1461,9 +1495,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.32"
+version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
@@ -1535,7 +1569,7 @@ dependencies = [
"log",
"plist",
"winapi",
- "winreg",
+ "winreg 0.10.1",
]
[[package]]
@@ -1654,8 +1688,17 @@ checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata",
- "regex-syntax",
+ "regex-automata 0.3.6",
+ "regex-syntax 0.7.4",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
]
[[package]]
@@ -1666,9 +1709,15 @@ checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax",
+ "regex-syntax 0.7.4",
]
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
[[package]]
name = "regex-syntax"
version = "0.7.4"
@@ -1677,9 +1726,9 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "reqwest"
-version = "0.11.18"
+version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
+checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
"base64 0.21.2",
"bytes",
@@ -1710,8 +1759,8 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
- "webpki-roots 0.22.6",
- "winreg",
+ "webpki-roots 0.25.2",
+ "winreg 0.50.0",
]
[[package]]
@@ -1786,11 +1835,11 @@ dependencies = [
[[package]]
name = "rustix"
-version = "0.38.7"
+version = "0.38.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399"
+checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
dependencies = [
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys",
@@ -1805,7 +1854,7 @@ checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb"
dependencies = [
"log",
"ring",
- "rustls-webpki 0.101.3",
+ "rustls-webpki 0.101.4",
"sct",
]
@@ -1820,9 +1869,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.100.1"
+version = "0.100.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
+checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab"
dependencies = [
"ring",
"untrusted",
@@ -1830,9 +1879,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.101.3"
+version = "0.101.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0"
+checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
dependencies = [
"ring",
"untrusted",
@@ -1844,7 +1893,7 @@ version = "12.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9"
dependencies = [
- "bitflags 2.3.3",
+ "bitflags 2.4.0",
"cfg-if",
"clipboard-win",
"fd-lock",
@@ -1915,9 +1964,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
-version = "1.0.183"
+version = "1.0.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
+checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
dependencies = [
"serde_derive",
]
@@ -1934,20 +1983,20 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.183"
+version = "1.0.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816"
+checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
name = "serde_json"
-version = "1.0.104"
+version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
+checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"indexmap 2.0.0",
"itoa",
@@ -2015,6 +2064,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+[[package]]
+name = "sharded-slab"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+dependencies = [
+ "lazy_static",
+]
+
[[package]]
name = "shell-words"
version = "1.1.0"
@@ -2032,9 +2090,9 @@ dependencies = [
[[package]]
name = "slab"
-version = "0.4.8"
+version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
@@ -2166,9 +2224,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.28"
+version = "2.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
dependencies = [
"proc-macro2",
"quote",
@@ -2177,9 +2235,9 @@ dependencies = [
[[package]]
name = "tempfile"
-version = "3.7.1"
+version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651"
+checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand",
@@ -2199,22 +2257,32 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "1.0.44"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
+checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.44"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
+checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
]
[[package]]
@@ -2245,15 +2313,15 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.25"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea"
+checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07"
dependencies = [
"deranged",
"itoa",
"serde",
"time-core",
- "time-macros 0.2.11",
+ "time-macros 0.2.13",
]
[[package]]
@@ -2274,9 +2342,9 @@ dependencies = [
[[package]]
name = "time-macros"
-version = "0.2.11"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd"
+checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9"
dependencies = [
"time-core",
]
@@ -2311,9 +2379,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.30.0"
+version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d3ce25f50619af8b0aec2eb23deebe84249e19e2ddd393a6e16e3300a6dadfd"
+checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
"backtrace",
"bytes",
@@ -2325,6 +2393,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.5.3",
"tokio-macros",
+ "tracing",
"windows-sys 0.48.0",
]
@@ -2336,7 +2405,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
]
[[package]]
@@ -2427,9 +2496,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if",
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
+[[package]]
+name = "tracing-attributes"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
[[package]]
name = "tracing-core"
version = "0.1.31"
@@ -2437,6 +2518,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+dependencies = [
+ "lazy_static",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -2555,6 +2666,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+[[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.4"
@@ -2619,7 +2736,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
"wasm-bindgen-shared",
]
@@ -2653,7 +2770,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.28",
+ "syn 2.0.29",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -2674,34 +2791,21 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "webpki"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.22.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
-dependencies = [
- "webpki",
-]
-
[[package]]
name = "webpki-roots"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
- "rustls-webpki 0.100.1",
+ "rustls-webpki 0.100.2",
]
+[[package]]
+name = "webpki-roots"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
+
[[package]]
name = "winapi"
version = "0.3.9"
@@ -2739,7 +2843,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
- "windows-targets 0.48.1",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -2757,7 +2861,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
- "windows-targets 0.48.1",
+ "windows-targets 0.48.5",
]
[[package]]
@@ -2777,17 +2881,17 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.48.1"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
- "windows_aarch64_gnullvm 0.48.0",
- "windows_aarch64_msvc 0.48.0",
- "windows_i686_gnu 0.48.0",
- "windows_i686_msvc 0.48.0",
- "windows_x86_64_gnu 0.48.0",
- "windows_x86_64_gnullvm 0.48.0",
- "windows_x86_64_msvc 0.48.0",
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
]
[[package]]
@@ -2798,9 +2902,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
@@ -2810,9 +2914,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
@@ -2822,9 +2926,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
@@ -2834,9 +2938,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
@@ -2846,9 +2950,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -2858,9 +2962,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
@@ -2870,15 +2974,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.48.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
-version = "0.5.4"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64"
+checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
dependencies = [
"memchr",
]
@@ -2892,6 +2996,16 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
[[package]]
name = "xml-rs"
version = "0.8.16"
diff --git a/Cargo.toml b/Cargo.toml
index fa3c198..1aab077 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "lune"
-version = "0.7.6"
+version = "0.7.7"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/filiptibell/lune"
@@ -71,18 +71,17 @@ async-trait = "0.1"
dialoguer = "0.10"
dunce = "1.0"
lz4_flex = "0.11"
+path-clean = "1.0"
pin-project = "1.0"
os_str_bytes = "6.4"
urlencoding = "2.1"
### RUNTIME
-mlua = { version = "0.9.0-beta.3", features = [
- "luau",
- "luau-jit",
- "serialize",
-] }
-tokio = { version = "1.24", features = ["full"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+mlua = { version = "0.9.0", features = ["luau", "luau-jit", "serialize"] }
+tokio = { version = "1.24", features = ["full", "tracing"] }
### SERDE
diff --git a/src/cli/repl.rs b/src/cli/repl.rs
index 6639424..71b4923 100644
--- a/src/cli/repl.rs
+++ b/src/cli/repl.rs
@@ -32,7 +32,7 @@ pub async fn show_interface() -> Result {
let mut prompt_state = PromptState::Regular;
let mut source_code = String::new();
- let lune_instance = Lune::new();
+ let mut lune_instance = Lune::new();
loop {
let prompt = match prompt_state {
diff --git a/src/lune/builtins/date_time.rs b/src/lune/builtins/datetime/mod.rs
similarity index 100%
rename from src/lune/builtins/date_time.rs
rename to src/lune/builtins/datetime/mod.rs
diff --git a/src/lune/lua/fs/copy.rs b/src/lune/builtins/fs/copy.rs
similarity index 99%
rename from src/lune/lua/fs/copy.rs
rename to src/lune/builtins/fs/copy.rs
index 00f37c3..677d0b2 100644
--- a/src/lune/lua/fs/copy.rs
+++ b/src/lune/builtins/fs/copy.rs
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
use mlua::prelude::*;
use tokio::fs;
-use super::FsWriteOptions;
+use super::options::FsWriteOptions;
pub struct CopyContents {
// Vec<(relative depth, path)>
diff --git a/src/lune/lua/fs/metadata.rs b/src/lune/builtins/fs/metadata.rs
similarity index 100%
rename from src/lune/lua/fs/metadata.rs
rename to src/lune/builtins/fs/metadata.rs
diff --git a/src/lune/builtins/fs.rs b/src/lune/builtins/fs/mod.rs
similarity index 96%
rename from src/lune/builtins/fs.rs
rename to src/lune/builtins/fs/mod.rs
index fd2db01..210608d 100644
--- a/src/lune/builtins/fs.rs
+++ b/src/lune/builtins/fs/mod.rs
@@ -4,10 +4,15 @@ use std::path::{PathBuf, MAIN_SEPARATOR};
use mlua::prelude::*;
use tokio::fs;
-use crate::lune::lua::{
- fs::{copy, FsMetadata, FsWriteOptions},
- table::TableBuilder,
-};
+use crate::lune::util::TableBuilder;
+
+mod copy;
+mod metadata;
+mod options;
+
+use copy::copy;
+use metadata::FsMetadata;
+use options::FsWriteOptions;
pub fn create(lua: &'static Lua) -> LuaResult {
TableBuilder::new(lua)?
diff --git a/src/lune/lua/fs/options.rs b/src/lune/builtins/fs/options.rs
similarity index 100%
rename from src/lune/lua/fs/options.rs
rename to src/lune/builtins/fs/options.rs
diff --git a/src/lune/builtins/luau.rs b/src/lune/builtins/luau/mod.rs
similarity index 85%
rename from src/lune/builtins/luau.rs
rename to src/lune/builtins/luau/mod.rs
index 6d0c7e4..b8c989b 100644
--- a/src/lune/builtins/luau.rs
+++ b/src/lune/builtins/luau/mod.rs
@@ -1,13 +1,13 @@
use mlua::prelude::*;
-use crate::lune::lua::{
- luau::{LuauCompileOptions, LuauLoadOptions},
- table::TableBuilder,
-};
+use crate::lune::util::TableBuilder;
+
+mod options;
+use options::{LuauCompileOptions, LuauLoadOptions};
const BYTECODE_ERROR_BYTE: u8 = 0;
-pub fn create(lua: &'static Lua) -> LuaResult {
+pub fn create(lua: &Lua) -> LuaResult {
TableBuilder::new(lua)?
.with_function("compile", compile_source)?
.with_function("load", load_source)?
diff --git a/src/lune/lua/luau/options.rs b/src/lune/builtins/luau/options.rs
similarity index 100%
rename from src/lune/lua/luau/options.rs
rename to src/lune/builtins/luau/options.rs
diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs
index 312305d..2fc47cf 100644
--- a/src/lune/builtins/mod.rs
+++ b/src/lune/builtins/mod.rs
@@ -1,12 +1,85 @@
-pub mod fs;
-pub mod luau;
-pub mod net;
-pub mod process;
-pub mod serde;
-pub mod stdio;
-pub mod task;
-pub mod top_level;
-pub mod date_time;
+use std::str::FromStr;
+
+use mlua::prelude::*;
+
+mod fs;
+mod luau;
+mod net;
+mod process;
+mod serde;
+mod stdio;
+mod task;
#[cfg(feature = "roblox")]
-pub mod roblox;
+mod roblox;
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub enum LuneBuiltin {
+ Fs,
+ Luau,
+ Net,
+ Task,
+ Process,
+ Serde,
+ Stdio,
+ #[cfg(feature = "roblox")]
+ Roblox,
+}
+
+impl<'lua> LuneBuiltin
+where
+ 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it
+{
+ pub fn name(&self) -> &'static str {
+ match self {
+ Self::Fs => "fs",
+ Self::Luau => "luau",
+ Self::Net => "net",
+ Self::Task => "task",
+ Self::Process => "process",
+ Self::Serde => "serde",
+ Self::Stdio => "stdio",
+ #[cfg(feature = "roblox")]
+ Self::Roblox => "roblox",
+ }
+ }
+
+ pub fn create(&self, lua: &'lua Lua) -> LuaResult> {
+ let res = match self {
+ Self::Fs => fs::create(lua),
+ Self::Luau => luau::create(lua),
+ Self::Net => net::create(lua),
+ Self::Task => task::create(lua),
+ Self::Process => process::create(lua),
+ Self::Serde => serde::create(lua),
+ Self::Stdio => stdio::create(lua),
+ #[cfg(feature = "roblox")]
+ Self::Roblox => roblox::create(lua),
+ };
+ match res {
+ Ok(v) => v.into_lua_multi(lua),
+ Err(e) => Err(e.context(format!(
+ "Failed to create builtin library '{}'",
+ self.name()
+ ))),
+ }
+ }
+}
+
+impl FromStr for LuneBuiltin {
+ type Err = String;
+ fn from_str(s: &str) -> Result {
+ match s.trim().to_ascii_lowercase().as_str() {
+ "fs" => Ok(Self::Fs),
+ "luau" => Ok(Self::Luau),
+ "net" => Ok(Self::Net),
+ "task" => Ok(Self::Task),
+ "process" => Ok(Self::Process),
+ "serde" => Ok(Self::Serde),
+ "stdio" => Ok(Self::Stdio),
+ #[cfg(feature = "roblox")]
+ "roblox" => Ok(Self::Roblox),
+ _ => Err(format!("Unknown builtin library '{s}'")),
+ }
+ }
+}
diff --git a/src/lune/lua/net/client.rs b/src/lune/builtins/net/client.rs
similarity index 56%
rename from src/lune/lua/net/client.rs
rename to src/lune/builtins/net/client.rs
index 2120c58..23636ff 100644
--- a/src/lune/lua/net/client.rs
+++ b/src/lune/builtins/net/client.rs
@@ -5,6 +5,8 @@ use mlua::prelude::*;
use hyper::{header::HeaderName, http::HeaderValue, HeaderMap};
use reqwest::{IntoUrl, Method, RequestBuilder};
+const REGISTRY_KEY: &str = "NetClient";
+
pub struct NetClientBuilder {
builder: reqwest::ClientBuilder,
}
@@ -44,6 +46,35 @@ impl NetClient {
pub fn request(&self, method: Method, url: U) -> RequestBuilder {
self.0.request(method, url)
}
+
+ pub fn into_registry(self, lua: &Lua) {
+ lua.set_named_registry_value(REGISTRY_KEY, self)
+ .expect("Failed to store NetClient in lua registry");
+ }
+
+ pub fn from_registry(lua: &Lua) -> Self {
+ lua.named_registry_value(REGISTRY_KEY)
+ .expect("Failed to get NetClient from lua registry")
+ }
}
impl LuaUserData for NetClient {}
+
+impl<'lua> FromLua<'lua> for NetClient {
+ fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult {
+ if let LuaValue::UserData(ud) = value {
+ if let Ok(ctx) = ud.borrow::() {
+ return Ok(ctx.clone());
+ }
+ }
+ unreachable!("NetClient should only be used from registry")
+ }
+}
+
+impl<'lua> From<&'lua Lua> for NetClient {
+ fn from(value: &'lua Lua) -> Self {
+ value
+ .named_registry_value(REGISTRY_KEY)
+ .expect("Missing require context in lua registry")
+ }
+}
diff --git a/src/lune/lua/net/config.rs b/src/lune/builtins/net/config.rs
similarity index 100%
rename from src/lune/lua/net/config.rs
rename to src/lune/builtins/net/config.rs
diff --git a/src/lune/builtins/net.rs b/src/lune/builtins/net/mod.rs
similarity index 56%
rename from src/lune/builtins/net.rs
rename to src/lune/builtins/net/mod.rs
index 0be55b5..615136d 100644
--- a/src/lune/builtins/net.rs
+++ b/src/lune/builtins/net/mod.rs
@@ -2,32 +2,34 @@ use std::collections::HashMap;
use mlua::prelude::*;
-use console::style;
-use hyper::{
- header::{CONTENT_ENCODING, CONTENT_LENGTH},
- Server,
-};
-use tokio::{sync::mpsc, task};
+use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH};
-use crate::lune::lua::{
- net::{
- NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig,
- ServeConfig,
- },
- serde::{decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat},
- table::TableBuilder,
- task::{TaskScheduler, TaskSchedulerAsyncExt},
+use crate::lune::{scheduler::Scheduler, util::TableBuilder};
+
+use self::server::create_server;
+
+use super::serde::{
+ compress_decompress::{decompress, CompressDecompressFormat},
+ encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat},
};
+mod client;
+mod config;
+mod processing;
+mod response;
+mod server;
+mod websocket;
+
+use client::{NetClient, NetClientBuilder};
+use config::{RequestConfig, ServeConfig};
+use server::bind_to_localhost;
+use websocket::NetWebSocket;
+
pub fn create(lua: &'static Lua) -> LuaResult {
- // Create a reusable client for performing our
- // web requests and store it in the lua registry,
- // allowing us to reuse headers and internal structs
- let client = NetClientBuilder::new()
+ NetClientBuilder::new()
.headers(&[("User-Agent", create_user_agent_header())])?
- .build()?;
- lua.set_named_registry_value("net.client", client)?;
- // Create the global table for net
+ .build()?
+ .into_registry(lua);
TableBuilder::new(lua)?
.with_function("jsonEncode", net_json_encode)?
.with_function("jsonDecode", net_json_decode)?
@@ -59,12 +61,12 @@ fn net_json_decode<'lua>(lua: &'lua Lua, json: LuaString<'lua>) -> LuaResult(
- lua: &'static Lua,
- config: RequestConfig<'lua>,
-) -> LuaResult> {
+async fn net_request<'lua>(lua: &'lua Lua, config: RequestConfig<'lua>) -> LuaResult>
+where
+ 'lua: 'static, // FIXME: Get rid of static lifetime bound here
+{
// Create and send the request
- let client: LuaUserDataRef = lua.named_registry_value("net.client")?;
+ let client = NetClient::from_registry(lua);
let mut request = client.request(config.method, &config.url);
for (query, value) in config.query {
request = request.query(&[(query.to_str()?, value.to_str()?)]);
@@ -122,73 +124,28 @@ async fn net_request<'lua>(
.build_readonly()
}
-async fn net_socket<'lua>(lua: &'static Lua, url: String) -> LuaResult {
+async fn net_socket<'lua>(lua: &'lua Lua, url: String) -> LuaResult
+where
+ 'lua: 'static, // FIXME: Get rid of static lifetime bound here
+{
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua_table(lua)
}
async fn net_serve<'lua>(
- lua: &'static Lua,
+ lua: &'lua Lua,
(port, config): (u16, ServeConfig<'lua>),
-) -> LuaResult> {
- // Note that we need to use a mpsc here and not
- // a oneshot channel since we move the sender
- // into our table with the stop function
- let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
- let server_request_callback = lua.create_registry_value(config.handle_request)?;
- let server_websocket_callback = config.handle_web_socket.map(|handler| {
- lua.create_registry_value(handler)
- .expect("Failed to store websocket handler")
- });
+) -> LuaResult>
+where
+ 'lua: 'static, // FIXME: Get rid of static lifetime bound here
+{
let sched = lua
- .app_data_ref::<&TaskScheduler>()
- .expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption");
- // Bind first to make sure that we can bind to this address
- let bound = match Server::try_bind(&([127, 0, 0, 1], port).into()) {
- Err(e) => {
- return Err(LuaError::external(format!(
- "Failed to bind to localhost on port {port}\n{}",
- format!("{e}").replace(
- "error creating server listener: ",
- &format!("{}", style("> ").dim())
- )
- )));
- }
- Ok(bound) => bound,
- };
- // Register a background task to prevent the task scheduler from
- // exiting early and start up our web server on the bound address
- let task = sched.register_background_task();
- let server = bound
- .http1_only(true) // Web sockets can only use http1
- .http1_keepalive(true) // Web sockets must be kept alive
- .executor(NetLocalExec)
- .serve(NetService::new(
- lua,
- server_request_callback,
- server_websocket_callback,
- ))
- .with_graceful_shutdown(async move {
- task.unregister(Ok(()));
- shutdown_rx
- .recv()
- .await
- .expect("Server was stopped instantly");
- shutdown_rx.close();
- });
- // Spawn a new tokio task so we don't block
- task::spawn_local(server);
- // Create a new read-only table that contains methods
- // for manipulating server behavior and shutting it down
- let handle_stop = move |_, _: ()| match shutdown_tx.try_send(()) {
- Ok(_) => Ok(()),
- Err(_) => Err(LuaError::RuntimeError(
- "Server has already been stopped".to_string(),
- )),
- };
- TableBuilder::new(lua)?
- .with_function("stop", handle_stop)?
- .build_readonly()
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+
+ let builder = bind_to_localhost(port)?;
+
+ create_server(lua, &sched, config, builder)
}
fn net_url_encode<'lua>(
diff --git a/src/lune/builtins/net/processing.rs b/src/lune/builtins/net/processing.rs
new file mode 100644
index 0000000..837fd3e
--- /dev/null
+++ b/src/lune/builtins/net/processing.rs
@@ -0,0 +1,101 @@
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use hyper::{body::to_bytes, Body, Request};
+
+use mlua::prelude::*;
+
+use crate::lune::util::TableBuilder;
+
+static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub(super) struct ProcessedRequestId(usize);
+
+impl ProcessedRequestId {
+ pub fn new() -> Self {
+ // NOTE: This may overflow after a couple billion requests,
+ // but that's completely fine... unless a request is still
+ // alive after billions more arrive and need to be handled
+ Self(ID_COUNTER.fetch_add(1, Ordering::Relaxed))
+ }
+}
+
+pub(super) struct ProcessedRequest {
+ pub id: ProcessedRequestId,
+ method: String,
+ path: String,
+ query: Vec<(String, String)>,
+ headers: Vec<(String, Vec)>,
+ body: Vec,
+}
+
+impl ProcessedRequest {
+ pub async fn from_request(req: Request) -> LuaResult {
+ let (head, body) = req.into_parts();
+
+ // FUTURE: We can do extra processing like async decompression here
+ let body = match to_bytes(body).await {
+ Err(_) => return Err(LuaError::runtime("Failed to read request body bytes")),
+ Ok(b) => b.to_vec(),
+ };
+
+ let method = head.method.to_string().to_ascii_uppercase();
+
+ let mut path = head.uri.path().to_string();
+ if path.is_empty() {
+ path = "/".to_string();
+ }
+
+ let query = head
+ .uri
+ .query()
+ .unwrap_or_default()
+ .split('&')
+ .filter_map(|q| q.split_once('='))
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+
+ let mut headers = Vec::new();
+ let mut header_name = String::new();
+ for (name_opt, value) in head.headers.into_iter() {
+ if let Some(name) = name_opt {
+ header_name = name.to_string();
+ }
+ headers.push((header_name.clone(), value.as_bytes().to_vec()))
+ }
+
+ let id = ProcessedRequestId::new();
+
+ Ok(Self {
+ id,
+ method,
+ path,
+ query,
+ headers,
+ body,
+ })
+ }
+
+ pub fn into_lua_table(self, lua: &Lua) -> LuaResult {
+ // FUTURE: Make inner tables for query keys that have multiple values?
+ let query = lua.create_table_with_capacity(0, self.query.len())?;
+ for (key, value) in self.query.into_iter() {
+ query.set(key, value)?;
+ }
+
+ let headers = lua.create_table_with_capacity(0, self.headers.len())?;
+ for (key, value) in self.headers.into_iter() {
+ headers.set(key, lua.create_string(value)?)?;
+ }
+
+ let body = lua.create_string(self.body)?;
+
+ TableBuilder::new(lua)?
+ .with_value("method", self.method)?
+ .with_value("path", self.path)?
+ .with_value("query", query)?
+ .with_value("headers", headers)?
+ .with_value("body", body)?
+ .build_readonly()
+ }
+}
diff --git a/src/lune/lua/net/response.rs b/src/lune/builtins/net/response.rs
similarity index 74%
rename from src/lune/lua/net/response.rs
rename to src/lune/builtins/net/response.rs
index fa2e748..d14646a 100644
--- a/src/lune/lua/net/response.rs
+++ b/src/lune/builtins/net/response.rs
@@ -9,7 +9,7 @@ pub enum NetServeResponseKind {
Table,
}
-#[derive(Debug, Clone)]
+#[derive(Debug)]
pub struct NetServeResponse {
kind: NetServeResponseKind,
status: u16,
@@ -81,26 +81,3 @@ impl<'lua> FromLua<'lua> for NetServeResponse {
}
}
}
-
-impl<'lua> IntoLua<'lua> for NetServeResponse {
- fn into_lua(self, lua: &'lua Lua) -> LuaResult> {
- if self.headers.len() > i32::MAX as usize {
- return Err(LuaError::ToLuaConversionError {
- from: "NetServeResponse",
- to: "table",
- message: Some("Too many header values".to_string()),
- });
- }
- let body = self.body.map(|b| lua.create_string(b)).transpose()?;
- let headers = lua.create_table_with_capacity(0, self.headers.len())?;
- for (key, value) in self.headers {
- headers.set(key, lua.create_string(&value)?)?;
- }
- let table = lua.create_table_with_capacity(0, 3)?;
- table.set("status", self.status)?;
- table.set("headers", headers)?;
- table.set("body", body)?;
- table.set_readonly(true);
- Ok(LuaValue::Table(table))
- }
-}
diff --git a/src/lune/builtins/net/server.rs b/src/lune/builtins/net/server.rs
new file mode 100644
index 0000000..167f10b
--- /dev/null
+++ b/src/lune/builtins/net/server.rs
@@ -0,0 +1,203 @@
+use std::{collections::HashMap, convert::Infallible, net::SocketAddr, sync::Arc};
+
+use hyper::{
+ server::{conn::AddrIncoming, Builder},
+ service::{make_service_fn, service_fn},
+ Server,
+};
+
+use hyper_tungstenite::{is_upgrade_request, upgrade, HyperWebsocket};
+use mlua::prelude::*;
+use tokio::sync::{mpsc, oneshot, Mutex};
+
+use crate::lune::{
+ scheduler::Scheduler,
+ util::{traits::LuaEmitErrorExt, TableBuilder},
+};
+
+use super::{
+ config::ServeConfig, processing::ProcessedRequest, response::NetServeResponse,
+ websocket::NetWebSocket,
+};
+
+pub(super) fn bind_to_localhost(port: u16) -> LuaResult> {
+ let addr = match SocketAddr::try_from(([127, 0, 0, 1], port)) {
+ Ok(a) => a,
+ Err(e) => {
+ return Err(LuaError::external(format!(
+ "Failed to bind to localhost on port {port}\n{e}"
+ )))
+ }
+ };
+ match Server::try_bind(&addr) {
+ Ok(b) => Ok(b),
+ Err(e) => Err(LuaError::external(format!(
+ "Failed to bind to localhost on port {port}\n{}",
+ e.to_string()
+ .replace("error creating server listener: ", "> ")
+ ))),
+ }
+}
+
+pub(super) fn create_server<'lua>(
+ lua: &'lua Lua,
+ sched: &'lua Scheduler,
+ config: ServeConfig<'lua>,
+ builder: Builder,
+) -> LuaResult>
+where
+ 'lua: 'static, // FIXME: Get rid of static lifetime bound here
+{
+ // Note that we need to use a mpsc here and not
+ // a oneshot channel since we move the sender
+ // into our table with the stop function
+ let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
+
+ // Communicate between background thread(s) and main lua thread using mpsc and oneshot
+ let (tx_request, mut rx_request) = mpsc::channel::(64);
+ let (tx_websocket, mut rx_websocket) = mpsc::channel::(64);
+ let tx_request_arc = Arc::new(tx_request);
+ let tx_websocket_arc = Arc::new(tx_websocket);
+
+ let response_senders = Arc::new(Mutex::new(HashMap::new()));
+ let response_senders_bg = Arc::clone(&response_senders);
+ let response_senders_lua = Arc::clone(&response_senders_bg);
+
+ // Create our background service which will accept
+ // requests, do some processing, then forward to lua
+ let has_websocket_handler = config.handle_web_socket.is_some();
+ let hyper_make_service = make_service_fn(move |_| {
+ let tx_request = Arc::clone(&tx_request_arc);
+ let tx_websocket = Arc::clone(&tx_websocket_arc);
+ let response_senders = Arc::clone(&response_senders_bg);
+
+ let handler = service_fn(move |mut req| {
+ let tx_request = Arc::clone(&tx_request);
+ let tx_websocket = Arc::clone(&tx_websocket);
+ let response_senders = Arc::clone(&response_senders);
+ async move {
+ // FUTURE: Improve error messages when lua is busy and queue is full
+ if has_websocket_handler && is_upgrade_request(&req) {
+ let (response, ws) = match upgrade(&mut req, None) {
+ Err(_) => return Err(LuaError::runtime("Failed to upgrade websocket")),
+ Ok(v) => v,
+ };
+ if (tx_websocket.send(ws).await).is_err() {
+ return Err(LuaError::runtime("Lua handler is busy"));
+ }
+ Ok(response)
+ } else {
+ let processed = ProcessedRequest::from_request(req).await?;
+ let request_id = processed.id;
+ if (tx_request.send(processed).await).is_err() {
+ return Err(LuaError::runtime("Lua handler is busy"));
+ }
+ let (response_tx, response_rx) = oneshot::channel::();
+ response_senders
+ .lock()
+ .await
+ .insert(request_id, response_tx);
+ match response_rx.await {
+ Err(_) => Err(LuaError::runtime("Internal Server Error")),
+ Ok(r) => r.into_response(),
+ }
+ }
+ }
+ });
+
+ async move { Ok::<_, Infallible>(handler) }
+ });
+
+ // Start up our service
+ sched.spawn(async move {
+ let result = builder
+ .http1_only(true) // Web sockets can only use http1
+ .http1_keepalive(true) // Web sockets must be kept alive
+ .serve(hyper_make_service)
+ .with_graceful_shutdown(async move {
+ shutdown_rx.recv().await;
+ });
+ if let Err(e) = result.await {
+ eprintln!("Net serve error: {e}")
+ }
+ });
+
+ // Spawn a local thread with access to lua and the same lifetime
+ sched.spawn_local(async move {
+ loop {
+ // Wait for either a request or a websocket to handle,
+ // if we got neither it means both channels were dropped
+ // and our server has stopped, either gracefully or panic
+ let (req, sock) = tokio::select! {
+ req = rx_request.recv() => (req, None),
+ sock = rx_websocket.recv() => (None, sock),
+ };
+
+ // NOTE: The closure here is not really necessary, we
+ // make the closure so that we can use the `?` operator
+ let handle_req_or_sock = || async {
+ match (req, sock) {
+ (None, None) => Ok::<_, LuaError>(true),
+ (Some(req), _) => {
+ let req_id = req.id;
+ let req_handler = config.handle_request.clone();
+ let req_table = req.into_lua_table(lua)?;
+
+ let thread_id = sched.push_back(lua, req_handler, req_table)?;
+ let thread_res = sched.wait_for_thread(lua, thread_id).await?;
+
+ let response = NetServeResponse::from_lua_multi(thread_res, lua)?;
+ let response_sender = response_senders_lua
+ .lock()
+ .await
+ .remove(&req_id)
+ .expect("Response channel was removed unexpectedly");
+
+ // NOTE: We ignore the error here, if the sender is no longer
+ // being listened to its because our client disconnected during
+ // handler being called, which is fine and should not emit errors
+ response_sender.send(response).ok();
+
+ Ok(false)
+ }
+ (_, Some(sock)) => {
+ let sock = sock.await.into_lua_err()?;
+
+ let sock_handler = config
+ .handle_web_socket
+ .as_ref()
+ .cloned()
+ .expect("Got web socket but web socket handler is missing");
+ let sock_table = NetWebSocket::new(sock).into_lua_table(lua)?;
+
+ // NOTE: Web socket handler does not need to send any
+ // response back, the websocket upgrade response is
+ // automatically sent above in the background thread(s)
+ let thread_id = sched.push_back(lua, sock_handler, sock_table)?;
+ let _thread_res = sched.wait_for_thread(lua, thread_id).await?;
+
+ Ok(false)
+ }
+ }
+ };
+
+ match handle_req_or_sock().await {
+ Ok(true) => break,
+ Ok(false) => continue,
+ Err(e) => lua.emit_error(e),
+ }
+ }
+ });
+
+ // Create a new read-only table that contains methods
+ // for manipulating server behavior and shutting it down
+ let handle_stop = move |_, _: ()| match shutdown_tx.try_send(()) {
+ Ok(_) => Ok(()),
+ Err(_) => Err(LuaError::RuntimeError(
+ "Server has already been stopped".to_string(),
+ )),
+ };
+ TableBuilder::new(lua)?
+ .with_function("stop", handle_stop)?
+ .build_readonly()
+}
diff --git a/src/lune/lua/net/websocket.rs b/src/lune/builtins/net/websocket.rs
similarity index 89%
rename from src/lune/lua/net/websocket.rs
rename to src/lune/builtins/net/websocket.rs
index 1057b1b..2bd7c5a 100644
--- a/src/lune/lua/net/websocket.rs
+++ b/src/lune/builtins/net/websocket.rs
@@ -22,7 +22,7 @@ use hyper_tungstenite::{
};
use tokio_tungstenite::MaybeTlsStream;
-use crate::lune::lua::table::TableBuilder;
+use crate::lune::util::TableBuilder;
const WEB_SOCKET_IMPL_LUA: &str = r#"
return freeze(setmetatable({
@@ -89,20 +89,19 @@ where
type NetWebSocketStreamClient = MaybeTlsStream;
impl NetWebSocket {
pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult {
+ let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?;
+ let table_freeze = lua
+ .globals()
+ .get::<_, LuaTable>("table")?
+ .get::<_, LuaFunction>("freeze")?;
let socket_env = TableBuilder::new(lua)?
.with_value("websocket", self)?
.with_function("close_code", close_code::)?
.with_async_function("close", close::)?
.with_async_function("send", send::)?
.with_async_function("next", next::)?
- .with_value(
- "setmetatable",
- lua.named_registry_value::("tab.setmeta")?,
- )?
- .with_value(
- "freeze",
- lua.named_registry_value::("tab.freeze")?,
- )?
+ .with_value("setmetatable", setmetatable)?
+ .with_value("freeze", table_freeze)?
.build_readonly()?;
Self::into_lua_table_with_env(lua, socket_env)
}
@@ -111,20 +110,19 @@ impl NetWebSocket {
type NetWebSocketStreamServer = Upgraded;
impl NetWebSocket {
pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult {
+ let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?;
+ let table_freeze = lua
+ .globals()
+ .get::<_, LuaTable>("table")?
+ .get::<_, LuaFunction>("freeze")?;
let socket_env = TableBuilder::new(lua)?
.with_value("websocket", self)?
.with_function("close_code", close_code::)?
.with_async_function("close", close::)?
.with_async_function("send", send::)?
.with_async_function("next", next::)?
- .with_value(
- "setmetatable",
- lua.named_registry_value::("tab.setmeta")?,
- )?
- .with_value(
- "freeze",
- lua.named_registry_value::("tab.freeze")?,
- )?
+ .with_value("setmetatable", setmetatable)?
+ .with_value("freeze", table_freeze)?
.build_readonly()?;
Self::into_lua_table_with_env(lua, socket_env)
}
diff --git a/src/lune/builtins/process.rs b/src/lune/builtins/process.rs
deleted file mode 100644
index 51e7c6f..0000000
--- a/src/lune/builtins/process.rs
+++ /dev/null
@@ -1,296 +0,0 @@
-use std::{
- collections::HashMap,
- env::{self, consts},
- path::{self, PathBuf},
- process::{ExitCode, Stdio},
-};
-
-use directories::UserDirs;
-use dunce::canonicalize;
-use mlua::prelude::*;
-use os_str_bytes::RawOsString;
-use tokio::process::Command;
-
-use crate::lune::lua::{
- process::pipe_and_inherit_child_process_stdio, table::TableBuilder, task::TaskScheduler,
-};
-
-const PROCESS_EXIT_IMPL_LUA: &str = r#"
-exit(...)
-yield()
-"#;
-
-pub fn create(lua: &'static Lua, args_vec: Vec) -> LuaResult {
- let cwd_str = {
- let cwd = canonicalize(env::current_dir()?)?;
- let cwd_str = cwd.to_string_lossy().to_string();
- if !cwd_str.ends_with(path::MAIN_SEPARATOR) {
- format!("{cwd_str}{}", path::MAIN_SEPARATOR)
- } else {
- cwd_str
- }
- };
- // Create constants for OS & processor architecture
- let os = lua.create_string(&consts::OS.to_lowercase())?;
- let arch = lua.create_string(&consts::ARCH.to_lowercase())?;
- // Create readonly args array
- let args_tab = TableBuilder::new(lua)?
- .with_sequential_values(args_vec)?
- .build_readonly()?;
- // Create proxied table for env that gets & sets real env vars
- let env_tab = TableBuilder::new(lua)?
- .with_metatable(
- TableBuilder::new(lua)?
- .with_function(LuaMetaMethod::Index.name(), process_env_get)?
- .with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
- .with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
- .build_readonly()?,
- )?
- .build_readonly()?;
- // Create our process exit function, this is a bit involved since
- // we have no way to yield from c / rust, we need to load a lua
- // chunk that will set the exit code and yield for us instead
- let process_exit_env_yield: LuaFunction = lua.named_registry_value("co.yield")?;
- let process_exit_env_exit: LuaFunction = lua.create_function(|lua, code: Option| {
- let exit_code = code.map_or(ExitCode::SUCCESS, ExitCode::from);
- let sched = lua
- .app_data_ref::<&TaskScheduler>()
- .expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption");
- sched.set_exit_code(exit_code);
- Ok(())
- })?;
- let process_exit = lua
- .load(PROCESS_EXIT_IMPL_LUA)
- .set_name("=process.exit")
- .set_environment(
- TableBuilder::new(lua)?
- .with_value("yield", process_exit_env_yield)?
- .with_value("exit", process_exit_env_exit)?
- .build_readonly()?,
- )
- .into_function()?;
- // Create the full process table
- TableBuilder::new(lua)?
- .with_value("os", os)?
- .with_value("arch", arch)?
- .with_value("args", args_tab)?
- .with_value("cwd", cwd_str)?
- .with_value("env", env_tab)?
- .with_value("exit", process_exit)?
- .with_async_function("spawn", process_spawn)?
- .build_readonly()
-}
-
-fn process_env_get<'lua>(
- lua: &'lua Lua,
- (_, key): (LuaValue<'lua>, String),
-) -> LuaResult> {
- match env::var_os(key) {
- Some(value) => {
- let raw_value = RawOsString::new(value);
- Ok(LuaValue::String(
- lua.create_string(raw_value.as_raw_bytes())?,
- ))
- }
- None => Ok(LuaValue::Nil),
- }
-}
-
-fn process_env_set<'lua>(
- _: &'lua Lua,
- (_, key, value): (LuaValue<'lua>, String, Option),
-) -> LuaResult<()> {
- // Make sure key is valid, otherwise set_var will panic
- if key.is_empty() {
- Err(LuaError::RuntimeError("Key must not be empty".to_string()))
- } else if key.contains('=') {
- Err(LuaError::RuntimeError(
- "Key must not contain the equals character '='".to_string(),
- ))
- } else if key.contains('\0') {
- Err(LuaError::RuntimeError(
- "Key must not contain the NUL character".to_string(),
- ))
- } else {
- match value {
- Some(value) => {
- // Make sure value is valid, otherwise set_var will panic
- if value.contains('\0') {
- Err(LuaError::RuntimeError(
- "Value must not contain the NUL character".to_string(),
- ))
- } else {
- env::set_var(&key, &value);
- Ok(())
- }
- }
- None => {
- env::remove_var(&key);
- Ok(())
- }
- }
- }
-}
-
-fn process_env_iter<'lua>(
- lua: &'lua Lua,
- (_, _): (LuaValue<'lua>, ()),
-) -> LuaResult> {
- let mut vars = env::vars_os().collect::>().into_iter();
- lua.create_function_mut(move |lua, _: ()| match vars.next() {
- Some((key, value)) => {
- let raw_key = RawOsString::new(key);
- let raw_value = RawOsString::new(value);
- Ok((
- LuaValue::String(lua.create_string(raw_key.as_raw_bytes())?),
- LuaValue::String(lua.create_string(raw_value.as_raw_bytes())?),
- ))
- }
- None => Ok((LuaValue::Nil, LuaValue::Nil)),
- })
-}
-
-async fn process_spawn<'lua>(
- lua: &'static Lua,
- (mut program, args, options): (String, Option>, Option>),
-) -> LuaResult> {
- // Parse any given options or create defaults
- let (child_cwd, child_envs, child_shell, child_stdio_inherit) = match options {
- Some(options) => {
- let mut cwd = env::current_dir()?;
- let mut envs = HashMap::new();
- let mut shell = None;
- let mut inherit = false;
- match options.raw_get("cwd")? {
- LuaValue::Nil => {}
- LuaValue::String(s) => {
- cwd = PathBuf::from(s.to_string_lossy().to_string());
- // Substitute leading tilde (~) for the actual home dir
- if cwd.starts_with("~") {
- if let Some(user_dirs) = UserDirs::new() {
- cwd = user_dirs.home_dir().join(cwd.strip_prefix("~").unwrap())
- }
- };
- if !cwd.exists() {
- return Err(LuaError::RuntimeError(
- "Invalid value for option 'cwd' - path does not exist".to_string(),
- ));
- }
- }
- value => {
- return Err(LuaError::RuntimeError(format!(
- "Invalid type for option 'cwd' - expected 'string', got '{}'",
- value.type_name()
- )))
- }
- }
- match options.raw_get("env")? {
- LuaValue::Nil => {}
- LuaValue::Table(t) => {
- for pair in t.pairs::() {
- let (k, v) = pair?;
- envs.insert(k, v);
- }
- }
- value => {
- return Err(LuaError::RuntimeError(format!(
- "Invalid type for option 'env' - expected 'table', got '{}'",
- value.type_name()
- )))
- }
- }
- match options.raw_get("shell")? {
- LuaValue::Nil => {}
- LuaValue::String(s) => shell = Some(s.to_string_lossy().to_string()),
- LuaValue::Boolean(true) => {
- shell = match env::consts::FAMILY {
- "unix" => Some("/bin/sh".to_string()),
- "windows" => Some("/bin/sh".to_string()),
- _ => None,
- };
- }
- value => {
- return Err(LuaError::RuntimeError(format!(
- "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
- value.type_name()
- )))
- }
- }
- match options.raw_get("stdio")? {
- LuaValue::Nil => {}
- LuaValue::String(s) => {
- match s.to_str()? {
- "inherit" => {
- inherit = true;
- },
- "default" => {
- inherit = false;
- }
- _ => return Err(LuaError::RuntimeError(
- format!("Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", s.to_string_lossy()),
- ))
- }
- }
- value => {
- return Err(LuaError::RuntimeError(format!(
- "Invalid type for option 'stdio' - expected 'string', got '{}'",
- value.type_name()
- )))
- }
- }
- Ok::<_, LuaError>((cwd, envs, shell, inherit))
- }
- None => Ok((env::current_dir()?, HashMap::new(), None, false)),
- }?;
- // Run a shell using the command param if wanted
- let child_args = if let Some(shell) = child_shell {
- let shell_args = match args {
- Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
- None => vec!["-c".to_string(), program],
- };
- program = shell;
- Some(shell_args)
- } else {
- args
- };
- // Create command with the wanted options
- let mut cmd = match child_args {
- None => Command::new(program),
- Some(args) => {
- let mut cmd = Command::new(program);
- cmd.args(args);
- cmd
- }
- };
- // Set dir to run in and env variables
- cmd.current_dir(child_cwd);
- cmd.envs(child_envs);
- // Spawn the child process
- let child = cmd
- .stdin(Stdio::null())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped())
- .spawn()?;
- // Inherit the output and stderr if wanted
- let result = if child_stdio_inherit {
- pipe_and_inherit_child_process_stdio(child).await
- } else {
- let output = child.wait_with_output().await?;
- Ok((output.status, output.stdout, output.stderr))
- };
- // Extract result
- let (status, stdout, stderr) = result?;
- // NOTE: If an exit code was not given by the child process,
- // we default to 1 if it yielded any error output, otherwise 0
- let code = status.code().unwrap_or(match stderr.is_empty() {
- true => 0,
- false => 1,
- });
- // Construct and return a readonly lua table with results
- TableBuilder::new(lua)?
- .with_value("ok", code == 0)?
- .with_value("code", code)?
- .with_value("stdout", lua.create_string(&stdout)?)?
- .with_value("stderr", lua.create_string(&stderr)?)?
- .build_readonly()
-}
diff --git a/src/lune/builtins/process/mod.rs b/src/lune/builtins/process/mod.rs
new file mode 100644
index 0000000..212d325
--- /dev/null
+++ b/src/lune/builtins/process/mod.rs
@@ -0,0 +1,219 @@
+use std::{
+ env::{self, consts},
+ path,
+ process::{ExitStatus, Stdio},
+};
+
+use dunce::canonicalize;
+use mlua::prelude::*;
+use os_str_bytes::RawOsString;
+
+use crate::lune::{scheduler::Scheduler, util::TableBuilder};
+
+mod tee_writer;
+
+mod pipe_inherit;
+use pipe_inherit::pipe_and_inherit_child_process_stdio;
+
+mod options;
+use options::ProcessSpawnOptions;
+
+const PROCESS_EXIT_IMPL_LUA: &str = r#"
+exit(...)
+yield()
+"#;
+
+pub fn create(lua: &'static Lua) -> LuaResult {
+ let cwd_str = {
+ let cwd = canonicalize(env::current_dir()?)?;
+ let cwd_str = cwd.to_string_lossy().to_string();
+ if !cwd_str.ends_with(path::MAIN_SEPARATOR) {
+ format!("{cwd_str}{}", path::MAIN_SEPARATOR)
+ } else {
+ cwd_str
+ }
+ };
+ // Create constants for OS & processor architecture
+ let os = lua.create_string(&consts::OS.to_lowercase())?;
+ let arch = lua.create_string(&consts::ARCH.to_lowercase())?;
+ // Create readonly args array
+ let args_vec = lua
+ .app_data_ref::>()
+ .ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))?
+ .clone();
+ let args_tab = TableBuilder::new(lua)?
+ .with_sequential_values(args_vec)?
+ .build_readonly()?;
+ // Create proxied table for env that gets & sets real env vars
+ let env_tab = TableBuilder::new(lua)?
+ .with_metatable(
+ TableBuilder::new(lua)?
+ .with_function(LuaMetaMethod::Index.name(), process_env_get)?
+ .with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
+ .with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
+ .build_readonly()?,
+ )?
+ .build_readonly()?;
+ // Create our process exit function, this is a bit involved since
+ // we have no way to yield from c / rust, we need to load a lua
+ // chunk that will set the exit code and yield for us instead
+ let coroutine_yield = lua
+ .globals()
+ .get::<_, LuaTable>("coroutine")?
+ .get::<_, LuaFunction>("yield")?;
+ let set_scheduler_exit_code = lua.create_function(|lua, code: Option| {
+ let sched = lua
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+ sched.set_exit_code(code.unwrap_or_default());
+ Ok(())
+ })?;
+ let process_exit = lua
+ .load(PROCESS_EXIT_IMPL_LUA)
+ .set_name("=process.exit")
+ .set_environment(
+ TableBuilder::new(lua)?
+ .with_value("yield", coroutine_yield)?
+ .with_value("exit", set_scheduler_exit_code)?
+ .build_readonly()?,
+ )
+ .into_function()?;
+ // Create the full process table
+ TableBuilder::new(lua)?
+ .with_value("os", os)?
+ .with_value("arch", arch)?
+ .with_value("args", args_tab)?
+ .with_value("cwd", cwd_str)?
+ .with_value("env", env_tab)?
+ .with_value("exit", process_exit)?
+ .with_async_function("spawn", process_spawn)?
+ .build_readonly()
+}
+
+fn process_env_get<'lua>(
+ lua: &'lua Lua,
+ (_, key): (LuaValue<'lua>, String),
+) -> LuaResult> {
+ match env::var_os(key) {
+ Some(value) => {
+ let raw_value = RawOsString::new(value);
+ Ok(LuaValue::String(
+ lua.create_string(raw_value.as_raw_bytes())?,
+ ))
+ }
+ None => Ok(LuaValue::Nil),
+ }
+}
+
+fn process_env_set<'lua>(
+ _: &'lua Lua,
+ (_, key, value): (LuaValue<'lua>, String, Option),
+) -> LuaResult<()> {
+ // Make sure key is valid, otherwise set_var will panic
+ if key.is_empty() {
+ Err(LuaError::RuntimeError("Key must not be empty".to_string()))
+ } else if key.contains('=') {
+ Err(LuaError::RuntimeError(
+ "Key must not contain the equals character '='".to_string(),
+ ))
+ } else if key.contains('\0') {
+ Err(LuaError::RuntimeError(
+ "Key must not contain the NUL character".to_string(),
+ ))
+ } else {
+ match value {
+ Some(value) => {
+ // Make sure value is valid, otherwise set_var will panic
+ if value.contains('\0') {
+ Err(LuaError::RuntimeError(
+ "Value must not contain the NUL character".to_string(),
+ ))
+ } else {
+ env::set_var(&key, &value);
+ Ok(())
+ }
+ }
+ None => {
+ env::remove_var(&key);
+ Ok(())
+ }
+ }
+ }
+}
+
+fn process_env_iter<'lua>(
+ lua: &'lua Lua,
+ (_, _): (LuaValue<'lua>, ()),
+) -> LuaResult> {
+ let mut vars = env::vars_os().collect::>().into_iter();
+ lua.create_function_mut(move |lua, _: ()| match vars.next() {
+ Some((key, value)) => {
+ let raw_key = RawOsString::new(key);
+ let raw_value = RawOsString::new(value);
+ Ok((
+ LuaValue::String(lua.create_string(raw_key.as_raw_bytes())?),
+ LuaValue::String(lua.create_string(raw_value.as_raw_bytes())?),
+ ))
+ }
+ None => Ok((LuaValue::Nil, LuaValue::Nil)),
+ })
+}
+
+async fn process_spawn(
+ lua: &Lua,
+ (program, args, options): (String, Option>, ProcessSpawnOptions),
+) -> LuaResult {
+ /*
+ Spawn the new process in the background, letting the tokio
+ runtime place it on a different thread if possible / necessary
+
+ Note that we have to use our scheduler here, we can't
+ use anything like tokio::task::spawn because our lua
+ scheduler will not drive those futures to completion
+ */
+ let sched = lua
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+
+ let (status, stdout, stderr) = sched
+ .spawn(spawn_command(program, args, options))
+ .await
+ .expect("Failed to receive result of spawned process")?;
+
+ // NOTE: If an exit code was not given by the child process,
+ // we default to 1 if it yielded any error output, otherwise 0
+ let code = status.code().unwrap_or(match stderr.is_empty() {
+ true => 0,
+ false => 1,
+ });
+
+ // Construct and return a readonly lua table with results
+ TableBuilder::new(lua)?
+ .with_value("ok", code == 0)?
+ .with_value("code", code)?
+ .with_value("stdout", lua.create_string(&stdout)?)?
+ .with_value("stderr", lua.create_string(&stderr)?)?
+ .build_readonly()
+}
+
+async fn spawn_command(
+ program: String,
+ args: Option>,
+ options: ProcessSpawnOptions,
+) -> LuaResult<(ExitStatus, Vec, Vec)> {
+ let inherit_stdio = options.inherit_stdio;
+
+ let child = options
+ .into_command(program, args)
+ .stdin(Stdio::null())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?;
+
+ if inherit_stdio {
+ pipe_and_inherit_child_process_stdio(child).await
+ } else {
+ let output = child.wait_with_output().await?;
+ Ok((output.status, output.stdout, output.stderr))
+ }
+}
diff --git a/src/lune/builtins/process/options.rs b/src/lune/builtins/process/options.rs
new file mode 100644
index 0000000..688caa3
--- /dev/null
+++ b/src/lune/builtins/process/options.rs
@@ -0,0 +1,177 @@
+use std::{
+ collections::HashMap,
+ env::{self},
+ path::PathBuf,
+};
+
+use directories::UserDirs;
+use mlua::prelude::*;
+use tokio::process::Command;
+
+#[derive(Debug, Clone, Default)]
+pub struct ProcessSpawnOptions {
+ pub(crate) cwd: Option,
+ pub(crate) envs: HashMap,
+ pub(crate) shell: Option,
+ pub(crate) inherit_stdio: bool,
+}
+
+impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
+ fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult {
+ let mut this = Self::default();
+ let value = match value {
+ LuaValue::Nil => return Ok(this),
+ LuaValue::Table(t) => t,
+ _ => {
+ return Err(LuaError::FromLuaConversionError {
+ from: value.type_name(),
+ to: "ProcessSpawnOptions",
+ message: Some(format!(
+ "Invalid spawn options - expected table, got {}",
+ value.type_name()
+ )),
+ })
+ }
+ };
+
+ /*
+ If we got a working directory to use:
+
+ 1. Substitute leading tilde (~) for the users home dir
+ 2. Make sure it exists
+ */
+ match value.get("cwd")? {
+ LuaValue::Nil => {}
+ LuaValue::String(s) => {
+ let mut cwd = PathBuf::from(s.to_str()?);
+ if let Ok(stripped) = cwd.strip_prefix("~") {
+ let user_dirs = UserDirs::new().ok_or_else(|| {
+ LuaError::runtime(
+ "Invalid value for option 'cwd' - failed to get home directory",
+ )
+ })?;
+ cwd = user_dirs.home_dir().join(stripped)
+ }
+ if !cwd.exists() {
+ return Err(LuaError::runtime(
+ "Invalid value for option 'cwd' - path does not exist",
+ ));
+ };
+ this.cwd = Some(cwd);
+ }
+ value => {
+ return Err(LuaError::RuntimeError(format!(
+ "Invalid type for option 'cwd' - expected string, got '{}'",
+ value.type_name()
+ )))
+ }
+ }
+
+ /*
+ If we got environment variables, make sure they are strings
+ */
+ match value.get("env")? {
+ LuaValue::Nil => {}
+ LuaValue::Table(e) => {
+ for pair in e.pairs::() {
+ let (k, v) = pair.context("Environment variables must be strings")?;
+ this.envs.insert(k, v);
+ }
+ }
+ value => {
+ return Err(LuaError::RuntimeError(format!(
+ "Invalid type for option 'env' - expected table, got '{}'",
+ value.type_name()
+ )))
+ }
+ }
+
+ /*
+ If we got a shell to use:
+
+ 1. When given as a string, use that literally
+ 2. When set to true, use a default shell for the platform
+ */
+ match value.get("shell")? {
+ LuaValue::Nil => {}
+ LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()),
+ LuaValue::Boolean(true) => {
+ this.shell = match env::consts::FAMILY {
+ "unix" => Some("/bin/sh".to_string()),
+ "windows" => Some("/bin/sh".to_string()),
+ _ => None,
+ };
+ }
+ value => {
+ return Err(LuaError::RuntimeError(format!(
+ "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
+ value.type_name()
+ )))
+ }
+ }
+
+ /*
+ If we got options for stdio handling, make sure its one of the constant values
+ */
+ match value.get("stdio")? {
+ LuaValue::Nil => {}
+ LuaValue::String(s) => match s.to_str()? {
+ "inherit" => this.inherit_stdio = true,
+ "default" => this.inherit_stdio = false,
+ _ => {
+ return Err(LuaError::RuntimeError(format!(
+ "Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'",
+ s.to_string_lossy()
+ )))
+ }
+ },
+ value => {
+ return Err(LuaError::RuntimeError(format!(
+ "Invalid type for option 'stdio' - expected 'string', got '{}'",
+ value.type_name()
+ )))
+ }
+ }
+
+ Ok(this)
+ }
+}
+
+impl ProcessSpawnOptions {
+ pub fn into_command(self, program: impl Into, args: Option>) -> Command {
+ let mut program = program.into();
+
+ // Run a shell using the command param if wanted
+ let pargs = match self.shell {
+ None => args,
+ Some(shell) => {
+ let shell_args = match args {
+ Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
+ None => vec!["-c".to_string(), program.to_string()],
+ };
+ program = shell.to_string();
+ Some(shell_args)
+ }
+ };
+
+ // Create command with the wanted options
+ let mut cmd = match pargs {
+ None => Command::new(program),
+ Some(args) => {
+ let mut cmd = Command::new(program);
+ cmd.args(args);
+ cmd
+ }
+ };
+
+ // Set dir to run in and env variables
+ if let Some(cwd) = self.cwd {
+ cmd.current_dir(cwd);
+ }
+ if !self.envs.is_empty() {
+ cmd.envs(self.envs);
+ }
+
+ cmd
+ }
+}
diff --git a/src/lune/lua/process/mod.rs b/src/lune/builtins/process/pipe_inherit.rs
similarity index 96%
rename from src/lune/lua/process/mod.rs
rename to src/lune/builtins/process/pipe_inherit.rs
index b74cd00..0e4b9a3 100644
--- a/src/lune/lua/process/mod.rs
+++ b/src/lune/builtins/process/pipe_inherit.rs
@@ -3,8 +3,7 @@ use std::process::ExitStatus;
use mlua::prelude::*;
use tokio::{io, process::Child, task};
-mod tee_writer;
-use tee_writer::AsyncTeeWriter;
+use super::tee_writer::AsyncTeeWriter;
pub async fn pipe_and_inherit_child_process_stdio(
mut child: Child,
diff --git a/src/lune/lua/process/tee_writer.rs b/src/lune/builtins/process/tee_writer.rs
similarity index 100%
rename from src/lune/lua/process/tee_writer.rs
rename to src/lune/builtins/process/tee_writer.rs
diff --git a/src/lune/builtins/roblox.rs b/src/lune/builtins/roblox/mod.rs
similarity index 93%
rename from src/lune/builtins/roblox.rs
rename to src/lune/builtins/roblox/mod.rs
index 2141d5c..9d86927 100644
--- a/src/lune/builtins/roblox.rs
+++ b/src/lune/builtins/roblox/mod.rs
@@ -1,25 +1,28 @@
use mlua::prelude::*;
use once_cell::sync::OnceCell;
-use crate::roblox::{
- self,
- document::{Document, DocumentError, DocumentFormat, DocumentKind},
- instance::Instance,
- reflection::Database as ReflectionDatabase,
+use crate::{
+ lune::util::TableBuilder,
+ roblox::{
+ self,
+ document::{Document, DocumentError, DocumentFormat, DocumentKind},
+ instance::Instance,
+ reflection::Database as ReflectionDatabase,
+ },
};
use tokio::task;
-use crate::lune::lua::table::TableBuilder;
-
static REFLECTION_DATABASE: OnceCell = OnceCell::new();
pub fn create(lua: &'static Lua) -> LuaResult {
let mut roblox_constants = Vec::new();
+
let roblox_module = roblox::module(lua)?;
for pair in roblox_module.pairs::() {
roblox_constants.push(pair?);
}
+
TableBuilder::new(lua)?
.with_values(roblox_constants)?
.with_async_function("deserializePlace", deserialize_place)?
diff --git a/src/lune/lua/serde/compress_decompress.rs b/src/lune/builtins/serde/compress_decompress.rs
similarity index 100%
rename from src/lune/lua/serde/compress_decompress.rs
rename to src/lune/builtins/serde/compress_decompress.rs
diff --git a/src/lune/lua/serde/encode_decode.rs b/src/lune/builtins/serde/encode_decode.rs
similarity index 99%
rename from src/lune/lua/serde/encode_decode.rs
rename to src/lune/builtins/serde/encode_decode.rs
index edbcc1b..1f83ce4 100644
--- a/src/lune/lua/serde/encode_decode.rs
+++ b/src/lune/builtins/serde/encode_decode.rs
@@ -10,6 +10,7 @@ const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
.serialize_unit_to_null(false);
const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
+ .sort_keys(true)
.deny_recursive_tables(false)
.deny_unsupported_types(true);
diff --git a/src/lune/builtins/serde.rs b/src/lune/builtins/serde/mod.rs
similarity index 84%
rename from src/lune/builtins/serde.rs
rename to src/lune/builtins/serde/mod.rs
index 5069337..4e76bce 100644
--- a/src/lune/builtins/serde.rs
+++ b/src/lune/builtins/serde/mod.rs
@@ -1,11 +1,12 @@
use mlua::prelude::*;
-use crate::lune::lua::{
- serde::{
- compress, decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat,
- },
- table::TableBuilder,
-};
+pub(super) mod compress_decompress;
+pub(super) mod encode_decode;
+
+use compress_decompress::{compress, decompress, CompressDecompressFormat};
+use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};
+
+use crate::lune::util::TableBuilder;
pub fn create(lua: &'static Lua) -> LuaResult {
TableBuilder::new(lua)?
diff --git a/src/lune/builtins/stdio.rs b/src/lune/builtins/stdio/mod.rs
similarity index 52%
rename from src/lune/builtins/stdio.rs
rename to src/lune/builtins/stdio/mod.rs
index acd3727..139a643 100644
--- a/src/lune/builtins/stdio.rs
+++ b/src/lune/builtins/stdio/mod.rs
@@ -1,59 +1,68 @@
-use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
use mlua::prelude::*;
+
+use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
use tokio::{
io::{self, AsyncWriteExt},
task,
};
-use crate::lune::lua::{
- stdio::{
- formatting::{
- format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
- },
- prompt::{PromptKind, PromptOptions, PromptResult},
+use crate::lune::util::{
+ formatting::{
+ format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
},
- table::TableBuilder,
+ TableBuilder,
};
-pub fn create(lua: &'static Lua) -> LuaResult {
+mod prompt;
+use prompt::{PromptKind, PromptOptions, PromptResult};
+
+pub fn create(lua: &'static Lua) -> LuaResult> {
TableBuilder::new(lua)?
- .with_function("color", |_, color: String| {
- let ansi_string = format_style(style_from_color_str(&color)?);
- Ok(ansi_string)
- })?
- .with_function("style", |_, style: String| {
- let ansi_string = format_style(style_from_style_str(&style)?);
- Ok(ansi_string)
- })?
- .with_function("format", |_, args: LuaMultiValue| {
- pretty_format_multi_value(&args)
- })?
- .with_async_function("write", |_, s: LuaString| async move {
- let mut stdout = io::stdout();
- stdout.write_all(s.as_bytes()).await?;
- stdout.flush().await?;
- Ok(())
- })?
- .with_async_function("ewrite", |_, s: LuaString| async move {
- let mut stderr = io::stderr();
- stderr.write_all(s.as_bytes()).await?;
- stderr.flush().await?;
- Ok(())
- })?
- .with_async_function("prompt", |_, options: PromptOptions| async move {
- task::spawn_blocking(move || prompt(options))
- .await
- .into_lua_err()?
- })?
+ .with_function("color", stdio_color)?
+ .with_function("style", stdio_style)?
+ .with_function("format", stdio_format)?
+ .with_async_function("write", stdio_write)?
+ .with_async_function("ewrite", stdio_ewrite)?
+ .with_async_function("prompt", stdio_prompt)?
.build_readonly()
}
-fn prompt_theme() -> ColorfulTheme {
- ColorfulTheme::default()
+fn stdio_color(_: &Lua, color: String) -> LuaResult {
+ let ansi_string = format_style(style_from_color_str(&color)?);
+ Ok(ansi_string)
+}
+
+fn stdio_style(_: &Lua, color: String) -> LuaResult {
+ let ansi_string = format_style(style_from_style_str(&color)?);
+ Ok(ansi_string)
+}
+
+fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult {
+ pretty_format_multi_value(&args)
+}
+
+async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
+ let mut stdout = io::stdout();
+ stdout.write_all(s.as_bytes()).await?;
+ stdout.flush().await?;
+ Ok(())
+}
+
+async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
+ let mut stderr = io::stderr();
+ stderr.write_all(s.as_bytes()).await?;
+ stderr.flush().await?;
+ Ok(())
+}
+
+async fn stdio_prompt(_: &Lua, options: PromptOptions) -> LuaResult {
+ task::spawn_blocking(move || prompt(options))
+ .await
+ .into_lua_err()?
}
fn prompt(options: PromptOptions) -> LuaResult {
- let theme = prompt_theme();
+ let theme = ColorfulTheme::default();
match options.kind {
PromptKind::Text => {
let input: String = Input::with_theme(&theme)
@@ -74,7 +83,7 @@ fn prompt(options: PromptOptions) -> LuaResult {
Ok(PromptResult::Boolean(result))
}
PromptKind::Select => {
- let chosen = Select::with_theme(&prompt_theme())
+ let chosen = Select::with_theme(&theme)
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
@@ -84,7 +93,7 @@ fn prompt(options: PromptOptions) -> LuaResult {
})
}
PromptKind::MultiSelect => {
- let chosen = MultiSelect::with_theme(&prompt_theme())
+ let chosen = MultiSelect::with_theme(&theme)
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
diff --git a/src/lune/lua/stdio/prompt.rs b/src/lune/builtins/stdio/prompt.rs
similarity index 100%
rename from src/lune/lua/stdio/prompt.rs
rename to src/lune/builtins/stdio/prompt.rs
diff --git a/src/lune/builtins/task.rs b/src/lune/builtins/task.rs
deleted file mode 100644
index 98a0383..0000000
--- a/src/lune/builtins/task.rs
+++ /dev/null
@@ -1,168 +0,0 @@
-use mlua::prelude::*;
-
-use crate::lune::lua::{
- async_ext::LuaAsyncExt,
- table::TableBuilder,
- task::{
- LuaThreadOrFunction, LuaThreadOrTaskReference, TaskKind, TaskReference, TaskScheduler,
- TaskSchedulerScheduleExt,
- },
-};
-
-const SPAWN_IMPL_LUA: &str = r#"
-scheduleNext(thread())
-local task = scheduleNext(...)
-yield()
-return task
-"#;
-
-pub fn create(lua: &'static Lua) -> LuaResult> {
- lua.app_data_ref::<&TaskScheduler>()
- .expect("Missing task scheduler in app data");
- /*
- 1. Schedule the current thread at the front
- 2. Schedule the wanted task arg at the front,
- the previous schedule now comes right after
- 3. Give control over to the scheduler, which will
- resume the above tasks in order when its ready
-
- The spawn function needs special treatment,
- we need to yield right away to allow the
- spawned task to run until first yield
- */
- let task_spawn_env_yield: LuaFunction = lua.named_registry_value("co.yield")?;
- let task_spawn = lua
- .load(SPAWN_IMPL_LUA)
- .set_name("task.spawn")
- .set_environment(
- TableBuilder::new(lua)?
- .with_function("thread", |lua, _: ()| Ok(lua.current_thread()))?
- .with_value("yield", task_spawn_env_yield)?
- .with_function(
- "scheduleNext",
- |lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- sched.schedule_blocking(tof.into_thread(lua)?, args)
- },
- )?
- .build_readonly()?,
- )
- .into_function()?;
- // Functions in the built-in coroutine library also need to be
- // replaced, these are a bit different than the ones above because
- // calling resume or the function that wrap returns must return
- // whatever lua value(s) that the thread or task yielded back
- let globals = lua.globals();
- let coroutine = globals.get::<_, LuaTable>("coroutine")?;
- coroutine.set("status", lua.create_function(coroutine_status)?)?;
- coroutine.set("resume", lua.create_function(coroutine_resume)?)?;
- coroutine.set("wrap", lua.create_function(coroutine_wrap)?)?;
- // All good, return the task scheduler lib
- TableBuilder::new(lua)?
- .with_value("wait", lua.create_waiter_function()?)?
- .with_value("spawn", task_spawn)?
- .with_function("cancel", task_cancel)?
- .with_function("defer", task_defer)?
- .with_function("delay", task_delay)?
- .build_readonly()
-}
-
-/*
- Basic task functions
-*/
-
-fn task_cancel(lua: &Lua, task: LuaUserDataRef) -> LuaResult<()> {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- sched.remove_task(*task)?;
- Ok(())
-}
-
-fn task_defer(
- lua: &Lua,
- (tof, args): (LuaThreadOrFunction, LuaMultiValue),
-) -> LuaResult {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- sched.schedule_blocking_deferred(tof.into_thread(lua)?, args)
-}
-
-fn task_delay(
- lua: &Lua,
- (secs, tof, args): (f64, LuaThreadOrFunction, LuaMultiValue),
-) -> LuaResult {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- sched.schedule_blocking_after_seconds(secs, tof.into_thread(lua)?, args)
-}
-
-/*
- Coroutine library overrides for compat with task scheduler
-*/
-
-fn coroutine_status<'a>(
- lua: &'a Lua,
- value: LuaThreadOrTaskReference<'a>,
-) -> LuaResult> {
- Ok(match value {
- LuaThreadOrTaskReference::Thread(thread) => {
- let get_status: LuaFunction = lua.named_registry_value("co.status")?;
- get_status.call(thread)?
- }
- LuaThreadOrTaskReference::TaskReference(task) => {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- sched
- .get_task_status(task)
- .unwrap_or_else(|| lua.create_string("dead").unwrap())
- }
- })
-}
-
-fn coroutine_resume<'lua>(
- lua: &'lua Lua,
- (value, arguments): (LuaThreadOrTaskReference, LuaMultiValue<'lua>),
-) -> LuaResult<(bool, LuaMultiValue<'lua>)> {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- if sched.current_task().is_none() {
- return Err(LuaError::RuntimeError(
- "No current task to inherit".to_string(),
- ));
- }
- let current = sched.current_task().unwrap();
- let result = match value {
- LuaThreadOrTaskReference::Thread(t) => {
- let task = sched.create_task(TaskKind::Instant, t, Some(arguments), true)?;
- sched.resume_task(task, None)
- }
- LuaThreadOrTaskReference::TaskReference(t) => sched.resume_task(t, Some(Ok(arguments))),
- };
- sched.force_set_current_task(Some(current));
- match result {
- Ok(rets) => Ok((true, rets.1)),
- Err(e) => Ok((false, e.into_lua_multi(lua)?)),
- }
-}
-
-fn coroutine_wrap<'lua>(lua: &'lua Lua, func: LuaFunction) -> LuaResult> {
- let task = lua.app_data_ref::<&TaskScheduler>().unwrap().create_task(
- TaskKind::Instant,
- lua.create_thread(func)?,
- None,
- false,
- )?;
- lua.create_function(move |lua, args: LuaMultiValue| {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- if sched.current_task().is_none() {
- return Err(LuaError::RuntimeError(
- "No current task to inherit".to_string(),
- ));
- }
- let current = sched.current_task().unwrap();
- let result = lua
- .app_data_ref::<&TaskScheduler>()
- .unwrap()
- .resume_task(task, Some(Ok(args)));
- sched.force_set_current_task(Some(current));
- match result {
- Ok(rets) => Ok(rets.1),
- Err(e) => Err(e),
- }
- })
-}
diff --git a/src/lune/builtins/task/mod.rs b/src/lune/builtins/task/mod.rs
new file mode 100644
index 0000000..189df32
--- /dev/null
+++ b/src/lune/builtins/task/mod.rs
@@ -0,0 +1,126 @@
+use std::time::Duration;
+
+use mlua::prelude::*;
+
+use tokio::time::{self, Instant};
+
+use crate::lune::{scheduler::Scheduler, util::TableBuilder};
+
+mod tof;
+use tof::LuaThreadOrFunction;
+
+/*
+ The spawn function needs special treatment,
+ we need to yield right away to allow the
+ spawned task to run until first yield
+
+ 1. Schedule this current thread at the front
+ 2. Schedule given thread/function at the front,
+ the previous schedule now comes right after
+ 3. Give control over to the scheduler, which will
+ resume the above tasks in order when its ready
+*/
+const SPAWN_IMPL_LUA: &str = r#"
+push(currentThread())
+local thread = push(...)
+yield()
+return thread
+"#;
+
+pub fn create(lua: &'static Lua) -> LuaResult> {
+ let coroutine_running = lua
+ .globals()
+ .get::<_, LuaTable>("coroutine")?
+ .get::<_, LuaFunction>("running")?;
+ let coroutine_yield = lua
+ .globals()
+ .get::<_, LuaTable>("coroutine")?
+ .get::<_, LuaFunction>("yield")?;
+ let push_front =
+ lua.create_function(|lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
+ let thread = tof.into_thread(lua)?;
+ let sched = lua
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+ sched.push_front(lua, thread.clone(), args)?;
+ Ok(thread)
+ })?;
+ let task_spawn_env = TableBuilder::new(lua)?
+ .with_value("currentThread", coroutine_running)?
+ .with_value("yield", coroutine_yield)?
+ .with_value("push", push_front)?
+ .build_readonly()?;
+ let task_spawn = lua
+ .load(SPAWN_IMPL_LUA)
+ .set_name("task.spawn")
+ .set_environment(task_spawn_env)
+ .into_function()?;
+
+ TableBuilder::new(lua)?
+ .with_function("cancel", task_cancel)?
+ .with_function("defer", task_defer)?
+ .with_function("delay", task_delay)?
+ .with_value("spawn", task_spawn)?
+ .with_async_function("wait", task_wait)?
+ .build_readonly()
+}
+
+fn task_cancel(lua: &Lua, thread: LuaThread) -> LuaResult<()> {
+ let close = lua
+ .globals()
+ .get::<_, LuaTable>("coroutine")?
+ .get::<_, LuaFunction>("close")?;
+ match close.call(thread) {
+ Err(LuaError::CoroutineInactive) => Ok(()),
+ Err(e) => Err(e),
+ Ok(()) => Ok(()),
+ }
+}
+
+fn task_defer<'lua>(
+ lua: &'lua Lua,
+ (tof, args): (LuaThreadOrFunction<'lua>, LuaMultiValue<'_>),
+) -> LuaResult> {
+ let thread = tof.into_thread(lua)?;
+ let sched = lua
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+ sched.push_back(lua, thread.clone(), args)?;
+ Ok(thread)
+}
+
+// FIXME: `self` escapes outside of method because we are borrowing `tof` and
+// `args` when we call `schedule_future_thread` in the lua function body below
+// For now we solve this by using the 'static lifetime bound in the impl
+fn task_delay<'lua>(
+ lua: &'lua Lua,
+ (secs, tof, args): (f64, LuaThreadOrFunction<'lua>, LuaMultiValue<'lua>),
+) -> LuaResult>
+where
+ 'lua: 'static,
+{
+ let thread = tof.into_thread(lua)?;
+ let sched = lua
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+
+ let thread2 = thread.clone();
+ sched.spawn_thread(lua, thread.clone(), async move {
+ let duration = Duration::from_secs_f64(secs);
+ time::sleep(duration).await;
+ sched.push_back(lua, thread2, args)?;
+ Ok(())
+ })?;
+
+ Ok(thread)
+}
+
+async fn task_wait(_: &Lua, secs: Option) -> LuaResult {
+ let duration = Duration::from_secs_f64(secs.unwrap_or_default());
+
+ let before = Instant::now();
+ time::sleep(duration).await;
+ let after = Instant::now();
+
+ Ok((after - before).as_secs_f64())
+}
diff --git a/src/lune/builtins/task/tof.rs b/src/lune/builtins/task/tof.rs
new file mode 100644
index 0000000..e63cd2b
--- /dev/null
+++ b/src/lune/builtins/task/tof.rs
@@ -0,0 +1,30 @@
+use mlua::prelude::*;
+
+#[derive(Clone)]
+pub(super) enum LuaThreadOrFunction<'lua> {
+ Thread(LuaThread<'lua>),
+ Function(LuaFunction<'lua>),
+}
+
+impl<'lua> LuaThreadOrFunction<'lua> {
+ pub(super) fn into_thread(self, lua: &'lua Lua) -> LuaResult> {
+ match self {
+ Self::Thread(t) => Ok(t),
+ Self::Function(f) => lua.create_thread(f),
+ }
+ }
+}
+
+impl<'lua> FromLua<'lua> for LuaThreadOrFunction<'lua> {
+ fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult {
+ match value {
+ LuaValue::Thread(t) => Ok(Self::Thread(t)),
+ LuaValue::Function(f) => Ok(Self::Function(f)),
+ value => Err(LuaError::FromLuaConversionError {
+ from: value.type_name(),
+ to: "LuaThreadOrFunction",
+ message: Some("Expected thread or function".to_string()),
+ }),
+ }
+ }
+}
diff --git a/src/lune/builtins/top_level.rs b/src/lune/builtins/top_level.rs
deleted file mode 100644
index a101fb0..0000000
--- a/src/lune/builtins/top_level.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use mlua::prelude::*;
-use std::io::{self, Write as _};
-
-#[cfg(feature = "roblox")]
-use crate::roblox::datatypes::extension::RobloxUserdataTypenameExt;
-
-use crate::lune::lua::{
- stdio::formatting::{format_label, pretty_format_multi_value},
- task::TaskReference,
-};
-
-pub fn print(_: &Lua, args: LuaMultiValue) -> LuaResult<()> {
- let formatted = format!("{}\n", pretty_format_multi_value(&args)?);
- let mut stdout = io::stdout();
- stdout.write_all(formatted.as_bytes())?;
- stdout.flush()?;
- Ok(())
-}
-
-pub fn warn(_: &Lua, args: LuaMultiValue) -> LuaResult<()> {
- let formatted = format!(
- "{}\n{}",
- format_label("warn"),
- pretty_format_multi_value(&args)?
- );
- let mut stdout = io::stdout();
- stdout.write_all(formatted.as_bytes())?;
- stdout.flush()?;
- Ok(())
-}
-
-// HACK: We need to preserve the default behavior of
-// the lua error function, for pcall and such, which
-// is really tricky to do from scratch so we will
-// just proxy the default function here instead
-
-pub fn error(lua: &Lua, (arg, level): (LuaValue, Option)) -> LuaResult<()> {
- let error: LuaFunction = lua.named_registry_value("error")?;
- let trace: LuaFunction = lua.named_registry_value("dbg.trace")?;
- error.call((
- LuaError::CallbackError {
- traceback: format!("override traceback:{}", trace.call::<_, String>(())?),
- cause: LuaError::external(format!(
- "{}\n{}",
- format_label("error"),
- pretty_format_multi_value(&arg.into_lua_multi(lua)?)?
- ))
- .into(),
- },
- level,
- ))?;
- Ok(())
-}
-
-pub fn proxy_type<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> {
- if let LuaValue::UserData(u) = &value {
- if u.is::() {
- return lua.create_string("thread");
- }
- }
- lua.named_registry_value::("type")?.call(value)
-}
-
-pub fn proxy_typeof<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> {
- if let LuaValue::UserData(u) = &value {
- if u.is::() {
- return lua.create_string("thread");
- }
- #[cfg(feature = "roblox")]
- {
- if let Some(type_name) = u.roblox_type_name() {
- return lua.create_string(type_name);
- }
- }
- }
- lua.named_registry_value::("typeof")?
- .call(value)
-}
-
-// TODO: Add an override for tostring that formats errors in a nicer way
diff --git a/src/lune/error.rs b/src/lune/error.rs
index c4d9f8c..bde101a 100644
--- a/src/lune/error.rs
+++ b/src/lune/error.rs
@@ -5,7 +5,7 @@ use std::{
use mlua::prelude::*;
-use crate::lune::lua::stdio::formatting::pretty_format_luau_error;
+use crate::lune::util::formatting::pretty_format_luau_error;
/**
An opaque error type for formatted lua errors.
@@ -16,6 +16,8 @@ pub struct LuneError {
disable_colors: bool,
}
+// TODO: Rename this struct to "RuntimeError" instead for
+// the next breaking release, it's a more fitting name
impl LuneError {
/**
Enables colorization of the error message when formatted using the [`Display`] trait.
@@ -64,6 +66,15 @@ impl From for LuneError {
}
}
+impl From<&LuaError> for LuneError {
+ fn from(value: &LuaError) -> Self {
+ Self {
+ error: value.clone(),
+ disable_colors: false,
+ }
+ }
+}
+
impl Display for LuneError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
diff --git a/src/lune/globals/g_table.rs b/src/lune/globals/g_table.rs
new file mode 100644
index 0000000..8c007c8
--- /dev/null
+++ b/src/lune/globals/g_table.rs
@@ -0,0 +1,5 @@
+use mlua::prelude::*;
+
+pub fn create(lua: &Lua) -> LuaResult> {
+ lua.create_table()
+}
diff --git a/src/lune/globals/mod.rs b/src/lune/globals/mod.rs
new file mode 100644
index 0000000..48e1deb
--- /dev/null
+++ b/src/lune/globals/mod.rs
@@ -0,0 +1,28 @@
+use mlua::prelude::*;
+
+use super::util::TableBuilder;
+
+mod g_table;
+mod print;
+mod require;
+mod r#typeof;
+mod version;
+mod warn;
+
+pub fn inject_all(lua: &'static Lua) -> LuaResult<()> {
+ let all = TableBuilder::new(lua)?
+ .with_value("_G", g_table::create(lua)?)?
+ .with_value("_VERSION", version::create(lua)?)?
+ .with_value("print", print::create(lua)?)?
+ .with_value("require", require::create(lua)?)?
+ .with_value("typeof", r#typeof::create(lua)?)?
+ .with_value("warn", warn::create(lua)?)?
+ .build_readonly()?;
+
+ for res in all.pairs() {
+ let (key, value): (LuaValue, LuaValue) = res.unwrap();
+ lua.globals().set(key, value)?;
+ }
+
+ Ok(())
+}
diff --git a/src/lune/globals/print.rs b/src/lune/globals/print.rs
new file mode 100644
index 0000000..57b849b
--- /dev/null
+++ b/src/lune/globals/print.rs
@@ -0,0 +1,14 @@
+use mlua::prelude::*;
+use tokio::io::{self, AsyncWriteExt};
+
+use crate::lune::{scheduler::LuaSchedulerExt, util::formatting::pretty_format_multi_value};
+
+pub fn create(lua: &'static Lua) -> LuaResult> {
+ lua.create_async_function(|_, args: LuaMultiValue| async move {
+ let formatted = format!("{}\n", pretty_format_multi_value(&args)?);
+ let mut stdout = io::stdout();
+ stdout.write_all(formatted.as_bytes()).await?;
+ stdout.flush().await?;
+ Ok(())
+ })
+}
diff --git a/src/lune/globals/require/alias.rs b/src/lune/globals/require/alias.rs
new file mode 100644
index 0000000..f8fd15e
--- /dev/null
+++ b/src/lune/globals/require/alias.rs
@@ -0,0 +1,16 @@
+use mlua::prelude::*;
+
+use super::context::*;
+
+pub(super) async fn require<'lua, 'ctx>(
+ _ctx: &'ctx RequireContext<'lua>,
+ alias: &str,
+ name: &str,
+) -> LuaResult>
+where
+ 'lua: 'ctx,
+{
+ Err(LuaError::runtime(format!(
+ "TODO: Support require for built-in libraries (tried to require '{name}' with alias '{alias}')"
+ )))
+}
diff --git a/src/lune/globals/require/builtin.rs b/src/lune/globals/require/builtin.rs
new file mode 100644
index 0000000..c4550dd
--- /dev/null
+++ b/src/lune/globals/require/builtin.rs
@@ -0,0 +1,14 @@
+use mlua::prelude::*;
+
+use super::context::*;
+
+pub(super) async fn require<'lua, 'ctx>(
+ ctx: &'ctx RequireContext<'lua>,
+ name: &str,
+) -> LuaResult>
+where
+ 'lua: 'ctx,
+ 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it
+{
+ ctx.load_builtin(name)
+}
diff --git a/src/lune/globals/require/context.rs b/src/lune/globals/require/context.rs
new file mode 100644
index 0000000..5f17f00
--- /dev/null
+++ b/src/lune/globals/require/context.rs
@@ -0,0 +1,310 @@
+use std::{
+ collections::HashMap,
+ env,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+
+use mlua::prelude::*;
+use tokio::{
+ fs,
+ sync::{
+ broadcast::{self, Sender},
+ Mutex as AsyncMutex,
+ },
+};
+
+use crate::lune::{
+ builtins::LuneBuiltin,
+ scheduler::{IntoLuaThread, Scheduler},
+};
+
+/**
+ Context containing cached results for all `require` operations.
+
+ The cache uses absolute paths, so any given relative
+ path will first be transformed into an absolute path.
+*/
+#[derive(Debug, Clone)]
+pub(super) struct RequireContext<'lua> {
+ lua: &'lua Lua,
+ use_cwd_relative_paths: bool,
+ working_directory: PathBuf,
+ cache_builtins: Arc>>>,
+ cache_results: Arc>>>,
+ cache_pending: Arc>>>,
+}
+
+impl<'lua> RequireContext<'lua> {
+ /**
+ Creates a new require context for the given [`Lua`] struct.
+
+ Note that this require context is global and only one require
+ context should be created per [`Lua`] struct, creating more
+ than one context may lead to undefined require-behavior.
+ */
+ pub fn new(lua: &'lua Lua) -> Self {
+ // FUTURE: We could load some kind of config or env var
+ // to check if we should be using cwd-relative paths
+ let cwd = env::current_dir().expect("Failed to get current working directory");
+ Self {
+ lua,
+ use_cwd_relative_paths: false,
+ working_directory: cwd,
+ cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())),
+ cache_results: Arc::new(AsyncMutex::new(HashMap::new())),
+ cache_pending: Arc::new(AsyncMutex::new(HashMap::new())),
+ }
+ }
+
+ /**
+ Resolves the given `source` and `path` into require paths
+ to use, based on the current require context settings.
+
+ This will resolve path segments such as `./`, `../`, ..., and
+ if the resolved path is not an absolute path, will create an
+ absolute path by prepending the current working directory.
+ */
+ pub fn resolve_paths(
+ &self,
+ source: impl AsRef,
+ path: impl AsRef,
+ ) -> LuaResult<(PathBuf, PathBuf)> {
+ let path = if self.use_cwd_relative_paths {
+ PathBuf::from(path.as_ref())
+ } else {
+ PathBuf::from(source.as_ref())
+ .parent()
+ .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))?
+ .join(path.as_ref())
+ };
+
+ let rel_path = path_clean::clean(path);
+ let abs_path = if rel_path.is_absolute() {
+ rel_path.to_path_buf()
+ } else {
+ self.working_directory.join(&rel_path)
+ };
+
+ Ok((rel_path, abs_path))
+ }
+
+ /**
+ Checks if the given path has a cached require result.
+ */
+ pub fn is_cached(&self, abs_path: impl AsRef) -> LuaResult {
+ let is_cached = self
+ .cache_results
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads")
+ .contains_key(abs_path.as_ref());
+ Ok(is_cached)
+ }
+
+ /**
+ Checks if the given path is currently being used in `require`.
+ */
+ pub fn is_pending(&self, abs_path: impl AsRef) -> LuaResult {
+ let is_pending = self
+ .cache_pending
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads")
+ .contains_key(abs_path.as_ref());
+ Ok(is_pending)
+ }
+
+ /**
+ Gets the resulting value from the require cache.
+
+ Will panic if the path has not been cached, use [`is_cached`] first.
+ */
+ pub fn get_from_cache(&self, abs_path: impl AsRef) -> LuaResult> {
+ let results = self
+ .cache_results
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads");
+
+ let cached = results
+ .get(abs_path.as_ref())
+ .expect("Path does not exist in results cache");
+ match cached {
+ Err(e) => Err(e.clone()),
+ Ok(k) => {
+ let multi_vec = self
+ .lua
+ .registry_value::>(k)
+ .expect("Missing require result in lua registry");
+ Ok(LuaMultiValue::from_vec(multi_vec))
+ }
+ }
+ }
+
+ /**
+ Waits for the resulting value from the require cache.
+
+ Will panic if the path has not been cached, use [`is_cached`] first.
+ */
+ pub async fn wait_for_cache(
+ &self,
+ abs_path: impl AsRef,
+ ) -> LuaResult> {
+ let mut thread_recv = {
+ let pending = self
+ .cache_pending
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads");
+ let thread_id = pending
+ .get(abs_path.as_ref())
+ .expect("Path is not currently pending require");
+ thread_id.subscribe()
+ };
+
+ thread_recv.recv().await.into_lua_err()?;
+
+ self.get_from_cache(abs_path.as_ref())
+ }
+
+ async fn load(
+ &self,
+ abs_path: impl AsRef,
+ rel_path: impl AsRef,
+ ) -> LuaResult {
+ let abs_path = abs_path.as_ref();
+ let rel_path = rel_path.as_ref();
+
+ let sched = self
+ .lua
+ .app_data_ref::<&Scheduler>()
+ .expect("Lua struct is missing scheduler");
+
+ // Read the file at the given path, try to parse and
+ // load it into a new lua thread that we can schedule
+ let file_contents = fs::read(&abs_path).await?;
+ let file_thread = self
+ .lua
+ .load(file_contents)
+ .set_name(rel_path.to_string_lossy().to_string())
+ .into_function()?
+ .into_lua_thread(self.lua)?;
+
+ // Schedule the thread to run, wait for it to finish running
+ let thread_id = sched.push_back(self.lua, file_thread, ())?;
+ let thread_res = sched.wait_for_thread(self.lua, thread_id).await;
+
+ // Return the result of the thread, storing any lua value(s) in the registry
+ match thread_res {
+ Err(e) => Err(e),
+ Ok(v) => {
+ let multi_vec = v.into_vec();
+ let multi_key = self
+ .lua
+ .create_registry_value(multi_vec)
+ .expect("Failed to store require result in registry - out of memory");
+ Ok(multi_key)
+ }
+ }
+ }
+
+ /**
+ Loads (requires) the file at the given path.
+ */
+ pub async fn load_with_caching(
+ &self,
+ abs_path: impl AsRef,
+ rel_path: impl AsRef,
+ ) -> LuaResult> {
+ let abs_path = abs_path.as_ref();
+ let rel_path = rel_path.as_ref();
+
+ // Set this abs path as currently pending
+ let (broadcast_tx, _) = broadcast::channel(1);
+ self.cache_pending
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads")
+ .insert(abs_path.to_path_buf(), broadcast_tx);
+
+ // Try to load at this abs path
+ let load_res = self.load(abs_path, rel_path).await;
+ let load_val = match &load_res {
+ Err(e) => Err(e.clone()),
+ Ok(k) => {
+ let multi_vec = self
+ .lua
+ .registry_value::>(k)
+ .expect("Failed to fetch require result from registry");
+ Ok(LuaMultiValue::from_vec(multi_vec))
+ }
+ };
+
+ // NOTE: We use the async lock and not try_lock here because
+ // some other thread may be wanting to insert into the require
+ // cache at the same time, and that's not an actual error case
+ self.cache_results
+ .lock()
+ .await
+ .insert(abs_path.to_path_buf(), load_res);
+
+ // Remove the pending thread id from the require context,
+ // broadcast a message to let any listeners know that this
+ // path has now finished the require process and is cached
+ let broadcast_tx = self
+ .cache_pending
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads")
+ .remove(abs_path)
+ .expect("Pending require broadcaster was unexpectedly removed");
+ broadcast_tx.send(()).ok();
+
+ load_val
+ }
+
+ /**
+ Loads (requires) the builtin with the given name.
+ */
+ pub fn load_builtin(&self, name: impl AsRef) -> LuaResult>
+ where
+ 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it
+ {
+ let builtin: LuneBuiltin = match name.as_ref().parse() {
+ Err(e) => return Err(LuaError::runtime(e)),
+ Ok(b) => b,
+ };
+
+ let mut cache = self
+ .cache_builtins
+ .try_lock()
+ .expect("RequireContext may not be used from multiple threads");
+
+ if let Some(res) = cache.get(&builtin) {
+ return match res {
+ Err(e) => return Err(e.clone()),
+ Ok(key) => {
+ let multi_vec = self
+ .lua
+ .registry_value::>(key)
+ .expect("Missing builtin result in lua registry");
+ Ok(LuaMultiValue::from_vec(multi_vec))
+ }
+ };
+ };
+
+ let result = builtin.create(self.lua);
+
+ cache.insert(
+ builtin,
+ match result.clone() {
+ Err(e) => Err(e),
+ Ok(multi) => {
+ let multi_vec = multi.into_vec();
+ let multi_key = self
+ .lua
+ .create_registry_value(multi_vec)
+ .expect("Failed to store require result in registry - out of memory");
+ Ok(multi_key)
+ }
+ },
+ );
+
+ result
+ }
+}
diff --git a/src/lune/globals/require/mod.rs b/src/lune/globals/require/mod.rs
new file mode 100644
index 0000000..f54ab21
--- /dev/null
+++ b/src/lune/globals/require/mod.rs
@@ -0,0 +1,98 @@
+use mlua::prelude::*;
+
+use crate::lune::{scheduler::LuaSchedulerExt, util::TableBuilder};
+
+mod context;
+use context::RequireContext;
+
+mod alias;
+mod builtin;
+mod path;
+
+const REQUIRE_IMPL: &str = r#"
+return require(source(), ...)
+"#;
+
+pub fn create(lua: &'static Lua) -> LuaResult> {
+ lua.set_app_data(RequireContext::new(lua));
+
+ /*
+ Require implementation needs a few workarounds:
+
+ - Async functions run outside of the lua resumption cycle,
+ so the current lua thread, as well as its stack/debug info
+ is not available, meaning we have to use a normal function
+
+ - Using the async require function directly in another lua function
+ would mean yielding across the metamethod/c-call boundary, meaning
+ we have to first load our two functions into a normal lua chunk
+ and then load that new chunk into our final require function
+
+ Also note that we inspect the stack at level 2:
+
+ 1. The current c / rust function
+ 2. The wrapper lua chunk defined above
+ 3. The lua chunk we are require-ing from
+ */
+
+ let require_fn = lua.create_async_function(require)?;
+ let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) {
+ None => Err(LuaError::runtime(
+ "Failed to get stack info for require source",
+ )),
+ Some(info) => match info.source().source {
+ None => Err(LuaError::runtime(
+ "Stack info is missing source for require",
+ )),
+ Some(source) => lua.create_string(source.as_bytes()),
+ },
+ })?;
+
+ let require_env = TableBuilder::new(lua)?
+ .with_value("source", get_source_fn)?
+ .with_value("require", require_fn)?
+ .build_readonly()?;
+
+ lua.load(REQUIRE_IMPL)
+ .set_name("require")
+ .set_environment(require_env)
+ .into_function()
+}
+
+async fn require<'lua>(
+ lua: &'lua Lua,
+ (source, path): (LuaString<'lua>, LuaString<'lua>),
+) -> LuaResult>
+where
+ 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it
+{
+ let source = source
+ .to_str()
+ .into_lua_err()
+ .context("Failed to parse require source as string")?
+ .to_string();
+
+ let path = path
+ .to_str()
+ .into_lua_err()
+ .context("Failed to parse require path as string")?
+ .to_string();
+
+ let context = lua
+ .app_data_ref()
+ .expect("Failed to get RequireContext from app data");
+
+ if let Some(builtin_name) = path
+ .strip_prefix("@lune/")
+ .map(|name| name.to_ascii_lowercase())
+ {
+ builtin::require(&context, &builtin_name).await
+ } else if let Some(aliased_path) = path.strip_prefix('@') {
+ let (alias, name) = aliased_path.split_once('/').ok_or(LuaError::runtime(
+ "Require with custom alias must contain '/' delimiter",
+ ))?;
+ alias::require(&context, alias, name).await
+ } else {
+ path::require(&context, &source, &path).await
+ }
+}
diff --git a/src/lune/globals/require/path.rs b/src/lune/globals/require/path.rs
new file mode 100644
index 0000000..0e443fd
--- /dev/null
+++ b/src/lune/globals/require/path.rs
@@ -0,0 +1,98 @@
+use std::path::{Path, PathBuf};
+
+use mlua::prelude::*;
+
+use super::context::*;
+
+pub(super) async fn require<'lua, 'ctx>(
+ ctx: &'ctx RequireContext<'lua>,
+ source: &str,
+ path: &str,
+) -> LuaResult>
+where
+ 'lua: 'ctx,
+{
+ let (abs_path, rel_path) = ctx.resolve_paths(source, path)?;
+
+ // 1. Try to require the exact path
+ if let Ok(res) = require_inner(ctx, &abs_path, &rel_path).await {
+ return Ok(res);
+ }
+
+ // 2. Try to require the path with an added "luau" extension
+ let (luau_abs_path, luau_rel_path) = (
+ append_extension(&abs_path, "luau"),
+ append_extension(&rel_path, "luau"),
+ );
+ if let Ok(res) = require_inner(ctx, &luau_abs_path, &luau_rel_path).await {
+ return Ok(res);
+ }
+
+ // 3. Try to require the path with an added "lua" extension
+ let (lua_abs_path, lua_rel_path) = (
+ append_extension(&abs_path, "lua"),
+ append_extension(&rel_path, "lua"),
+ );
+ if let Ok(res) = require_inner(ctx, &lua_abs_path, &lua_rel_path).await {
+ return Ok(res);
+ }
+
+ // We didn't find any direct file paths, look
+ // for directories with "init" files in them...
+ let abs_init = abs_path.join("init");
+ let rel_init = rel_path.join("init");
+
+ // 4. Try to require the init path with an added "luau" extension
+ let (luau_abs_init, luau_rel_init) = (
+ append_extension(&abs_init, "luau"),
+ append_extension(&rel_init, "luau"),
+ );
+ if let Ok(res) = require_inner(ctx, &luau_abs_init, &luau_rel_init).await {
+ return Ok(res);
+ }
+
+ // 5. Try to require the init path with an added "lua" extension
+ let (lua_abs_init, lua_rel_init) = (
+ append_extension(&abs_init, "lua"),
+ append_extension(&rel_init, "lua"),
+ );
+ if let Ok(res) = require_inner(ctx, &lua_abs_init, &lua_rel_init).await {
+ return Ok(res);
+ }
+
+ // Nothing left to try, throw an error
+ Err(LuaError::runtime(format!(
+ "No file exist at the path '{}'",
+ rel_path.display()
+ )))
+}
+
+async fn require_inner<'lua, 'ctx>(
+ ctx: &'ctx RequireContext<'lua>,
+ abs_path: impl AsRef,
+ rel_path: impl AsRef,
+) -> LuaResult>
+where
+ 'lua: 'ctx,
+{
+ let abs_path = abs_path.as_ref();
+ let rel_path = rel_path.as_ref();
+
+ if ctx.is_cached(abs_path)? {
+ ctx.get_from_cache(abs_path)
+ } else if ctx.is_pending(abs_path)? {
+ ctx.wait_for_cache(&abs_path).await
+ } else {
+ ctx.load_with_caching(&abs_path, &rel_path).await
+ }
+}
+
+fn append_extension(path: impl Into, ext: &'static str) -> PathBuf {
+ let mut new = path.into();
+ match new.extension() {
+ // FUTURE: There's probably a better way to do this than converting to a lossy string
+ Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())),
+ None => new.set_extension(ext),
+ };
+ new
+}
diff --git a/src/lune/globals/typeof.rs b/src/lune/globals/typeof.rs
new file mode 100644
index 0000000..f4f4631
--- /dev/null
+++ b/src/lune/globals/typeof.rs
@@ -0,0 +1,27 @@
+use mlua::prelude::*;
+
+use crate::roblox::datatypes::extension::RobloxUserdataTypenameExt;
+
+const REGISTRY_KEY: &str = "LuauTypeof";
+
+pub fn create(lua: &Lua) -> LuaResult> {
+ let original = lua.globals().get::<_, LuaFunction>("typeof")?;
+ #[cfg(feature = "roblox")]
+ {
+ lua.set_named_registry_value(REGISTRY_KEY, original)
+ .expect("Failed to store typeof function in registry");
+ lua.create_function(|lua, value: LuaValue| {
+ if let LuaValue::UserData(u) = &value {
+ if let Some(type_name) = u.roblox_type_name() {
+ return lua.create_string(type_name);
+ }
+ }
+ let original_fn: LuaFunction = lua
+ .named_registry_value(REGISTRY_KEY)
+ .expect("Missing typeof function in registry");
+ original_fn.call(value)
+ })
+ }
+ #[cfg(not(feature = "roblox"))]
+ original
+}
diff --git a/src/lune/globals/version.rs b/src/lune/globals/version.rs
new file mode 100644
index 0000000..3f72b5c
--- /dev/null
+++ b/src/lune/globals/version.rs
@@ -0,0 +1,24 @@
+use mlua::prelude::*;
+
+pub fn create(lua: &Lua) -> LuaResult> {
+ let luau_version_full = lua
+ .globals()
+ .get::<_, LuaString>("_VERSION")
+ .expect("Missing _VERSION global");
+
+ let luau_version = luau_version_full
+ .to_str()?
+ .strip_prefix("Luau 0.")
+ .expect("_VERSION global is formatted incorrectly")
+ .trim();
+
+ if luau_version.is_empty() {
+ panic!("_VERSION global is missing version number")
+ }
+
+ lua.create_string(format!(
+ "Lune {lune}+{luau}",
+ lune = env!("CARGO_PKG_VERSION"),
+ luau = luau_version,
+ ))
+}
diff --git a/src/lune/globals/warn.rs b/src/lune/globals/warn.rs
new file mode 100644
index 0000000..ba98b8a
--- /dev/null
+++ b/src/lune/globals/warn.rs
@@ -0,0 +1,21 @@
+use mlua::prelude::*;
+use tokio::io::{self, AsyncWriteExt};
+
+use crate::lune::{
+ scheduler::LuaSchedulerExt,
+ util::formatting::{format_label, pretty_format_multi_value},
+};
+
+pub fn create(lua: &'static Lua) -> LuaResult> {
+ lua.create_async_function(|_, args: LuaMultiValue| async move {
+ let formatted = format!(
+ "{}\n{}",
+ format_label("warn"),
+ pretty_format_multi_value(&args)?
+ );
+ let mut stdout = io::stderr();
+ stdout.write_all(formatted.as_bytes()).await?;
+ stdout.flush().await?;
+ Ok(())
+ })
+}
diff --git a/src/lune/importer/mod.rs b/src/lune/importer/mod.rs
deleted file mode 100644
index de3cdf6..0000000
--- a/src/lune/importer/mod.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use mlua::prelude::*;
-
-mod require;
-mod require_waker;
-
-use crate::lune::builtins::{self, top_level};
-
-pub fn create(lua: &'static Lua, args: Vec) -> LuaResult<()> {
- // Create all builtins
- let builtins = vec![
- ("fs", builtins::fs::create(lua)?),
- ("net", builtins::net::create(lua)?),
- ("process", builtins::process::create(lua, args)?),
- ("serde", builtins::serde::create(lua)?),
- ("stdio", builtins::stdio::create(lua)?),
- ("task", builtins::task::create(lua)?),
- ("luau", builtins::luau::create(lua)?),
- #[cfg(feature = "roblox")]
- ("roblox", builtins::roblox::create(lua)?),
- ];
-
- // Create our importer (require) with builtins
- let require_fn = require::create(lua, builtins)?;
-
- // Create all top-level globals
- let globals = vec![
- ("require", require_fn),
- ("print", lua.create_function(top_level::print)?),
- ("warn", lua.create_function(top_level::warn)?),
- ("error", lua.create_function(top_level::error)?),
- ("type", lua.create_function(top_level::proxy_type)?),
- ("typeof", lua.create_function(top_level::proxy_typeof)?),
- ];
-
- // Set top-level globals
- let lua_globals = lua.globals();
- for (name, global) in globals {
- lua_globals.set(name, global)?;
- }
-
- Ok(())
-}
diff --git a/src/lune/importer/require.rs b/src/lune/importer/require.rs
deleted file mode 100644
index 113c593..0000000
--- a/src/lune/importer/require.rs
+++ /dev/null
@@ -1,319 +0,0 @@
-use std::{
- cell::RefCell,
- collections::{HashMap, HashSet},
- env::current_dir,
- path::{self, PathBuf},
- sync::Arc,
-};
-
-use mlua::prelude::*;
-use tokio::fs;
-use tokio::sync::Mutex as AsyncMutex;
-
-use crate::lune::lua::{
- table::TableBuilder,
- task::{TaskScheduler, TaskSchedulerScheduleExt},
-};
-
-use super::require_waker::{RequireWakerFuture, RequireWakerState};
-
-const REQUIRE_IMPL_LUA: &str = r#"
-local source = info(1, "s")
-if source == '[string "require"]' then
- source = info(2, "s")
-end
-load(context, source, ...)
-return yield()
-"#;
-
-type RequireWakersVec<'lua> = Vec>>>;
-
-fn append_extension_and_canonicalize(
- path: impl Into,
- ext: &'static str,
-) -> Result {
- let mut new = path.into();
- match new.extension() {
- // FUTURE: There's probably a better way to do this than converting to a lossy string
- Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())),
- None => new.set_extension(ext),
- };
- dunce::canonicalize(new)
-}
-
-#[derive(Debug, Clone, Default)]
-struct RequireContext<'lua> {
- // NOTE: We need to use arc here so that mlua clones
- // the reference and not the entire inner value(s)
- builtins: Arc>>,
- cached: Arc>>>>,
- wakers: Arc>>>,
- locks: Arc>>,
- pwd: String,
-}
-
-impl<'lua> RequireContext<'lua> {
- pub fn new(lua: &'lua Lua, builtins_vec: Vec<(K, V)>) -> LuaResult
- where
- K: Into,
- V: IntoLua<'lua>,
- {
- let mut pwd = current_dir()
- .expect("Failed to access current working directory")
- .to_string_lossy()
- .to_string();
- if !pwd.ends_with(path::MAIN_SEPARATOR) {
- pwd = format!("{pwd}{}", path::MAIN_SEPARATOR)
- }
- let mut builtins = HashMap::new();
- for (key, value) in builtins_vec {
- builtins.insert(key.into(), value.into_lua_multi(lua)?);
- }
- Ok(Self {
- pwd,
- builtins: Arc::new(builtins),
- ..Default::default()
- })
- }
-
- pub fn is_locked(&self, absolute_path: &str) -> bool {
- self.locks.borrow().contains(absolute_path)
- }
-
- pub fn set_locked(&self, absolute_path: &str) -> bool {
- self.locks.borrow_mut().insert(absolute_path.to_string())
- }
-
- pub fn set_unlocked(&self, absolute_path: &str) -> bool {
- self.locks.borrow_mut().remove(absolute_path)
- }
-
- pub fn try_acquire_lock_sync(&self, absolute_path: &str) -> bool {
- if self.is_locked(absolute_path) {
- false
- } else {
- self.set_locked(absolute_path);
- true
- }
- }
-
- pub fn set_cached(&self, absolute_path: &str, result: &LuaResult>) {
- self.cached
- .borrow_mut()
- .insert(absolute_path.to_string(), result.clone());
- if let Some(wakers) = self.wakers.borrow_mut().remove(absolute_path) {
- for waker in wakers {
- waker
- .try_lock()
- .expect("Failed to lock waker")
- .finalize(result.clone());
- }
- }
- }
-
- pub fn wait_for_cache(self, absolute_path: &str) -> RequireWakerFuture<'lua> {
- let state = RequireWakerState::new();
- let fut = RequireWakerFuture::new(&state);
- self.wakers
- .borrow_mut()
- .entry(absolute_path.to_string())
- .or_insert_with(Vec::new)
- .push(Arc::clone(&state));
- fut
- }
-
- pub fn get_paths(
- &self,
- require_source: String,
- require_path: String,
- ) -> LuaResult<(String, String)> {
- if require_path.starts_with('@') {
- return Ok((require_path.clone(), require_path));
- }
- let path_relative_to_pwd = PathBuf::from(
- &require_source
- .trim_start_matches("[string \"")
- .trim_end_matches("\"]"),
- )
- .parent()
- .unwrap()
- .join(&require_path);
- // Try to normalize and resolve relative path segments such as './' and '../'
- let file_path = match (
- append_extension_and_canonicalize(&path_relative_to_pwd, "luau"),
- append_extension_and_canonicalize(&path_relative_to_pwd, "lua"),
- ) {
- (Ok(luau), _) => luau,
- (_, Ok(lua)) => lua,
- // If we did not find a luau/lua file at the wanted path,
- // we should also look for "init" files in directories
- _ => {
- let init_dir_path = path_relative_to_pwd.join("init");
- match (
- append_extension_and_canonicalize(&init_dir_path, "luau"),
- append_extension_and_canonicalize(&init_dir_path, "lua"),
- ) {
- (Ok(luau), _) => luau,
- (_, Ok(lua)) => lua,
- _ => {
- return Err(LuaError::RuntimeError(format!(
- "File does not exist at path '{require_path}'"
- )));
- }
- }
- }
- };
- let absolute = file_path.to_string_lossy().to_string();
- let relative = absolute.trim_start_matches(&self.pwd).to_string();
- Ok((absolute, relative))
- }
-}
-
-impl<'lua> LuaUserData for RequireContext<'lua> {}
-
-fn load_builtin<'lua>(
- _lua: &'lua Lua,
- context: RequireContext<'lua>,
- module_name: String,
- _has_acquired_lock: bool,
-) -> LuaResult> {
- match context.builtins.get(&module_name) {
- Some(module) => Ok(module.clone()),
- None => Err(LuaError::RuntimeError(format!(
- "No builtin module exists with the name '{}'",
- module_name
- ))),
- }
-}
-
-async fn load_file<'lua>(
- lua: &'lua Lua,
- context: RequireContext<'lua>,
- absolute_path: String,
- relative_path: String,
- has_acquired_lock: bool,
-) -> LuaResult> {
- let cached = { context.cached.borrow().get(&absolute_path).cloned() };
- match cached {
- Some(cached) => cached,
- None => {
- if !has_acquired_lock {
- return context.wait_for_cache(&absolute_path).await;
- }
- // Try to read the wanted file, note that we use bytes instead of reading
- // to a string since lua scripts are not necessarily valid utf-8 strings
- let contents = fs::read(&absolute_path).await.into_lua_err()?;
- // Use a name without extensions for loading the chunk, some
- // other code assumes the require path is without extensions
- let path_relative_no_extension = relative_path
- .trim_end_matches(".lua")
- .trim_end_matches(".luau");
- // Load the file into a thread
- let loaded_func = lua
- .load(contents)
- .set_name(path_relative_no_extension)
- .into_function()?;
- let loaded_thread = lua.create_thread(loaded_func)?;
- // Run the thread and wait for completion using the native task scheduler waker
- let task_fut = {
- let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
- let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?;
- sched.wait_for_task_completion(task)
- };
- // Wait for the thread to finish running, cache + return our result,
- // notify any other threads that are also waiting on this to finish
- let rets = task_fut.await;
- context.set_cached(&absolute_path, &rets);
- rets
- }
- }
-}
-
-async fn load<'lua>(
- lua: &'lua Lua,
- context: LuaUserDataRef<'lua, RequireContext<'lua>>,
- absolute_path: String,
- relative_path: String,
- has_acquired_lock: bool,
-) -> LuaResult> {
- let result = if absolute_path == relative_path && absolute_path.starts_with('@') {
- if let Some(module_name) = absolute_path.strip_prefix("@lune/") {
- load_builtin(
- lua,
- context.clone(),
- module_name.to_string(),
- has_acquired_lock,
- )
- } else {
- // FUTURE: '@' can be used a special prefix for users to set their own
- // paths relative to a project file, similar to typescript paths config
- // https://www.typescriptlang.org/tsconfig#paths
- Err(LuaError::RuntimeError(
- "Require paths prefixed by '@' are not yet supported".to_string(),
- ))
- }
- } else {
- load_file(
- lua,
- context.clone(),
- absolute_path.to_string(),
- relative_path,
- has_acquired_lock,
- )
- .await
- };
- if has_acquired_lock {
- context.set_unlocked(&absolute_path);
- }
- result
-}
-
-pub fn create(lua: &'static Lua, builtins: Vec<(K, V)>) -> LuaResult
-where
- K: Clone + Into,
- V: Clone + IntoLua<'static>,
-{
- let require_context = RequireContext::new(lua, builtins)?;
- let require_yield: LuaFunction = lua.named_registry_value("co.yield")?;
- let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
- let require_print: LuaFunction = lua.named_registry_value("print")?;
-
- let require_env = TableBuilder::new(lua)?
- .with_value("context", require_context)?
- .with_value("yield", require_yield)?
- .with_value("info", require_info)?
- .with_value("print", require_print)?
- .with_function(
- "load",
- |lua,
- (context, require_source, require_path): (
- LuaUserDataRef,
- String,
- String,
- )| {
- let (absolute_path, relative_path) =
- context.get_paths(require_source, require_path)?;
- // NOTE: We can not acquire the lock in the async part of the require
- // load process since several requires may have happened for the
- // same path before the async load task even gets a chance to run
- let has_lock = context.try_acquire_lock_sync(&absolute_path);
- let fut = load(lua, context, absolute_path, relative_path, has_lock);
- let sched = lua
- .app_data_ref::<&TaskScheduler>()
- .expect("Missing task scheduler as a lua app data");
- sched.queue_async_task_inherited(lua.current_thread(), None, async {
- let rets = fut.await?;
- let mult = rets.into_lua_multi(lua)?;
- Ok(Some(mult))
- })
- },
- )?
- .build_readonly()?;
-
- let require_fn_lua = lua
- .load(REQUIRE_IMPL_LUA)
- .set_name("require")
- .set_environment(require_env)
- .into_function()?;
- Ok(require_fn_lua)
-}
diff --git a/src/lune/importer/require_waker.rs b/src/lune/importer/require_waker.rs
deleted file mode 100644
index 4a43b87..0000000
--- a/src/lune/importer/require_waker.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use std::{
- future::Future,
- pin::Pin,
- sync::Arc,
- task::{Context, Poll, Waker},
-};
-
-use tokio::sync::Mutex as AsyncMutex;
-
-use mlua::prelude::*;
-
-#[derive(Debug, Clone)]
-pub(super) struct RequireWakerState<'lua> {
- rets: Option>>,
- waker: Option,
-}
-
-impl<'lua> RequireWakerState<'lua> {
- pub fn new() -> Arc> {
- Arc::new(AsyncMutex::new(RequireWakerState {
- rets: None,
- waker: None,
- }))
- }
-
- pub fn finalize(&mut self, rets: LuaResult>) {
- self.rets = Some(rets);
- if let Some(waker) = self.waker.take() {
- waker.wake();
- }
- }
-}
-
-#[derive(Debug)]
-pub(super) struct RequireWakerFuture<'lua> {
- state: Arc>>,
-}
-
-impl<'lua> RequireWakerFuture<'lua> {
- pub fn new(state: &Arc>>) -> Self {
- Self {
- state: Arc::clone(state),
- }
- }
-}
-
-impl<'lua> Clone for RequireWakerFuture<'lua> {
- fn clone(&self) -> Self {
- Self {
- state: Arc::clone(&self.state),
- }
- }
-}
-
-impl<'lua> Future for RequireWakerFuture<'lua> {
- type Output = LuaResult>;
- fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
- let mut shared_state = self.state.try_lock().unwrap();
- if let Some(rets) = shared_state.rets.clone() {
- Poll::Ready(rets)
- } else {
- shared_state.waker = Some(cx.waker().clone());
- Poll::Pending
- }
- }
-}
diff --git a/src/lune/lua/async_ext.rs b/src/lune/lua/async_ext.rs
deleted file mode 100644
index afdd29b..0000000
--- a/src/lune/lua/async_ext.rs
+++ /dev/null
@@ -1,93 +0,0 @@
-use async_trait::async_trait;
-use futures_util::Future;
-use mlua::prelude::*;
-
-use crate::lune::{lua::table::TableBuilder, lua::task::TaskScheduler};
-
-use super::task::TaskSchedulerAsyncExt;
-
-const ASYNC_IMPL_LUA: &str = r#"
-resumeAsync(...)
-return yield()
-"#;
-
-const WAIT_IMPL_LUA: &str = r#"
-resumeAfter(...)
-return yield()
-"#;
-
-#[async_trait(?Send)]
-pub trait LuaAsyncExt {
- fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult>
- where
- A: FromLuaMulti<'static>,
- R: IntoLuaMulti<'static>,
- F: 'static + Fn(&'lua Lua, A) -> FR,
- FR: 'static + Future