Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

24 changed files with 387 additions and 2494 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# Temporarily highlight luau as normal lua files
# until we get native linguist support for Luau
*.luau linguist-language=Lua

View file

@ -1,49 +0,0 @@
name: CI
on:
push:
pull_request:
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install tooling
uses: ok-nick/setup-aftman@v0.4.2
with:
cache: true
- name: Check formatting
run: stylua -c -v .
lint:
needs: ["fmt"]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install tooling
uses: ok-nick/setup-aftman@v0.4.2
with:
cache: true
- name: Install dependencies
run: wally install
- name: Setup lune typedefs
run: lune setup
- name: Analyze
run: luau-lsp analyze --ignore="Packages/**" --settings=".vscode/settings.json" lib/ examples/ mod.luau
- name: lint
run: selene .

View file

@ -1,45 +0,0 @@
name: Docs
on:
push:
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
defaults:
run:
shell: bash
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4.0.2
- name: Install Moonwave CLI
run: npm i -g moonwave
- name: Build static site
run: moonwave build
- name: Setup GitHub pages
uses: actions/configure-pages@v3
- name: Upload repository artifact
uses: actions/upload-pages-artifact@v2
with:
path: './build'
- name: Deploy to GitHub pages
id: deployment
uses: actions/deploy-pages@v2

6
.gitignore vendored
View file

@ -1,5 +1 @@
# Installed wally packages
Packages/
# Moonwave compiled docs
build
Packages/

View file

@ -1,42 +0,0 @@
@import url("https://Cinnab0nBak3ry.github.io/AppleFontsCSS/Apple-fonts.css");
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
:root {
--ifm-color-primary: #cba6f7;
--ifm-color-primary-dark: #b580f4;
--ifm-color-primary-darker: #aa6df2;
--ifm-color-primary-darkest: #8934ed;
--ifm-color-primary-light: #e1ccfa;
--ifm-color-primary-lighter: #ecdffc;
--ifm-color-primary-lightest: #ffffff;
--ifm-footer-color: ##a6adc8;
--ifm-footer-background-color: #45475a;
--ifm-font-family-base: "SF Pro Display";
--ifm-font-family-monospace: "JetBrains Mono";
--ifm-font-family-monospace: "JetBrains Mono";
--ifm-code-font-size: 80%;
}
/* Catppuccin Mocha from https://github.com/catppuccin/catppuccin?tab=readme-ov-file#-palette */
[data-theme='dark']:root {
--ifm-color-scheme: dark;
--ifm-background-color: #1e1e2e;
--ifm-background-surface-color: #313244;
--ifm-color-black: #11111b;
--ifm-font-color-base: #cdd6f4;
}
/*
TODO: Light theme catppuccin latte
[data-theme='light']:root {
--ifm-color-scheme: light;
--ifm-background-color: ##eff1f5;
--ifm-background-surface-color: #ccd0da;
--ifm-color-white: #e6e9ef;
--ifm-font-color-base: ##4c4f69;
}
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View file

@ -8,37 +8,6 @@ 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).
## `0.2.0` - June 21st, 2024
This release includes various changes to API interfaces, documentation, and includes new
implementations.
- Fixed inconsistencies `Option` & `Result` implementations
- Implemented `Future`, a pollable asynchronous idiom, alternative to promises
```luau
local net = require("@lune/net")
local fut: Future<Result<string, string>> = Future.try(function(url)
local resp = net.request({
url = url,
method = "GET",
})
assert(resp.ok)
return resp.body
end, { "https://jsonplaceholder.typicode.com/posts/1" })
local resp: Result<string, string> = fut:await()
print(net.jsonDecode(resp:unwrap()))
```
- Added documentation for all available implementations
- Included CI action
- Added examples for `Result`
- Removed incomplete `Iter` implementation
## `0.1.0` - April 2nd, 2024
The very first release of rusty-luau.

View file

@ -1,26 +0,0 @@
<img align="right" src="https://rusty-luau.devcomp.xyz/logo.png" />
[![wally][wally-image]][wally-link]
[![AGPL-3.0 licensed][license-image]][license-link]
[![docs][docs-image]][docs-link]
[![CI][ci-image]][ci-link]
## [read the docs](https://rusty-luau.devcomp.xyz/api/)
strongly typed implementations of various rust idioms in luau.
currently, the following implementations are available:
- [Future](https://rusty-luau.devcomp.xyz/api/Future)
- [Option](https://rusty-luau.devcomp.xyz/api/Option)
- [Result](https://rusty-luau.devcomp.xyz/api/Result)
[//]: # (badges)
[wally-image]: https://img.shields.io/github/v/tag/CompeyDev/rusty-luau?label=wally&logo=lua
[wally-link]: https://wally.run/package/compeydev/rusty-luau
[docs-image]: https://github.com/CompeyDev/rusty-luau/actions/workflows/docs.yaml/badge.svg
[docs-link]: https://rusty-luau.devcomp.xyz/
[license-image]: https://img.shields.io/github/license/CompeyDev/rusty-luau
[license-link]: https://github.com/CompeyDev/rusty-luau/blob/main/LICENSE.md
[ci-image]: https://github.com/CompeyDev/rusty-luau/actions/workflows/ci.yaml/badge.svg
[ci-link]: https://github.com/CompeyDev/rusty-luau/actions/workflows/ci.yaml

View file

@ -1,7 +1,6 @@
[tools]
lune = "lune-org/lune@0.8.5"
lune = "lune-org/lune@0.8.2"
stylua = "JohnnyMorganz/StyLua@0.20.0"
luau-lsp = "JohnnyMorganz/luau-lsp@1.27.0"
darklua = "seaofvoices/darklua@0.13.0"
wally = "UpliftGames/wally@0.3.2"
selene = "Kampfkarren/selene@0.26.1"

View file

@ -1,20 +0,0 @@
local result = require("../lib/result")
type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
local function canError(): Result<number, string>
if math.round(math.random()) == 1 then
return Err("you DIED")
end
return Ok(69)
end
function main()
local val = canError():unwrap()
print("got value: ", val)
end
return main()

View file

@ -19,197 +19,79 @@ export type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
--[=[
@within Result
Converts from [Result]`<T, E>` to [Option]`<T>`.
Converts `self` into an [Option]`<T>`, and discarding the error, if any.
```lua
local x: Result<number, string> = Ok(2)
assert(x:ok() == Some(2))
x = Err("Nothing here")
assert(x:ok() == None())
```
@param self Result<T, E>
@return Option<T>
]=]
function Result.ok<T, E>(self: Result<T, E>): Option<T>
if self:isOk() then
if self._value == nil then
return None()
end
if self:isOk() then
if self._value == nil then
return None()
end
return Some(self._value)
end
return Some(self._value)
end
return None()
return None()
end
--[=[
@within Result
Converts from [Result]`<T, E>` to [Option]`<E>`.
Converts `self` into an [Option]`<E>`, and discarding the success value, if any.
```lua
local x: Result<number, string> = Ok(2)
assert(x:ok() == Some(2))
x = Err("Nothing here")
assert(x:ok() == None())
```
@param self Result<T, E>
@return Option<E>
]=]
function Result.err<T, E>(self: Result<T, E>): Option<E>
if self:isErr() then
return Option.new(self._error) :: Option<E>
end
if self:isErr() then
return Option.new(self._error) :: Option<E>
end
return None()
return None()
end
--[=[
@within Result
Transposes a [Result] of an [Option] into an [Option] of a [Result].
[Result:Ok]\([Option:None]\) will be mapped to [Option:None].
[Result:Ok]\([Option:Some]`(_)`\) and [Result:Err]\(`_`\) will be mapped to
[Option:Some]\([Result:Ok]`(_)`\) and [Option:Some]\([Option:Err]`(_)`\).
```lua
type SomeErr = {}
local x: Result<Option<number>, SomeErr> = Ok(Some(2))
local y: Option<Result<number, SomeErr>> = Some(Ok(2))
assert(x:transpose() == y)
```
@param self Result<Option<T>, E>
@return Option<Result<T, E>>
]=]
function Result.transpose<T, E>(self: Result<Option<T>, E>): Option<Result<T, E>>
if self._value == None() then
return None()
elseif
self:isOkAnd(function(val): boolean
return val._optValue == nil
end)
then
return Some(Ok(self._value._optValue))
elseif self:isErr() then
return Some(Err(self._error))
end
-- TODO: Instead of checking whether values are nil, use
-- utility methods for Options once available
if self._value == None() then
return None()
elseif self:isOkAnd(function(val): boolean
return val._optValue == nil
end) then
return Some(Ok(self._value._optValue))
elseif self:isErr() then
return Some(Err(self._error))
end
error("`Result` is not transposable")
error("`Result` is not transposable")
end
--[=[
@within Option
Transforms the [Option]`<T>` into a [Result]`<T, E>`, mapping [Option:Some]`(v)`
to [Result:Ok]`(v)` and [Option:None] to [Result:Err]`(err)`.
Arguments passed to [Option:okOr] are eagerly evaluated; if you are passing the result
of a function call, it is recommended to use [Option:okOrElse], which is lazily evaluated.
```lua
local x: Option<string> = Some("foo")
assert(x:okOr(0) == Ok("foo"))
x = None()
assert(x:okOr(0) == Err(0))
```
@param self Option<T>
@param err E
@return Result<T, E>
]=]
function Option.okOr<T, E>(self: Option<T>, err: E): Result<T, E>
if self:isSome() then
return Ok(self._optValue)
end
if self:isSome() then
return Ok(self._optValue)
end
return Err(err)
return Err(err)
end
--[=[
@within Option
Transforms the [Option]`<T>` into a [Result]`<T, E>`, mapping [Option:Some]`(v)` to
[Result:Ok]`(v)` and [Option:None] to [Result:Err]`(err())`.
```lua
local x: Option<string> = Some("foo")
assert(x:okOrElse(function() return 0 end) == Ok("foo"))
x = None()
assert(x:okOrElse(function() return 0 end) == Err(0))
```
@param self Option<T>
@param err () -> E
@return Result<T, E>
]=]
function Option.okOrElse<T, E>(self: Option<T>, err: () -> E): Result<T, E>
if self:isSome() then
return Ok(self._optValue :: T)
end
if self:isSome() then
return Ok(self._optValue :: T)
end
return Err(err())
return Err(err())
end
--[=[
@within Option
Transposes a [Option] of an [Result] into an [Result] of a [Option].
[Option:None] will be mapped to [Result:Ok]\([Option:None]\).
[Option:Some]\([Result:Ok]`(_)`\) and [Option:Some]\([Result:Err]\(`_`\)\) will
be mapped to [Result:Ok]\([Option:Some]`(_)`\) and [Result:Err]`(_)`.
```lua
type SomeErr = {}
local x: Result<Option<number>, SomeErr> = Ok(Some(5))
local y: Option<Result<number, SomeErr>> = Some(Ok(5))
assert(x == y:transpose())
```
@param self Option<Result<T, E>>
@return Result<Option<T>, E>
]=]
function Option.transpose<T, E>(self: Option<Result<T, E>>): Result<Option<T>, E>
if self:isSome() then
local inner = self._optValue
assert(
self.typeId == "Option" and inner.typeId == "Result",
"Only an `Option` of a `Result` can be transposed"
)
if self:isSome() then
local inner = self._optValue
assert(self.typeId == "Option" and inner.typeId == "Result", "Only an `Option` of a `Result` can be transposed")
if inner:isOk() then
return Some(Ok(inner._value))
elseif inner:isErr() then
return Some(Err(inner._error))
end
end
if inner:isOk() then
return Some(Ok(inner._value))
elseif inner:isErr() then
return Some(Err(inner._error))
end
end
return Ok(None())
return Ok(None())
end
return {
Ok = Ok,
Err = Err,
Result = Result,
Ok = Ok,
Err = Err,
Result = Result,
Some = Some,
None = None,
Option = Option,
Some = Some,
None = None,
Option = Option,
}

View file

@ -1,217 +0,0 @@
local task = require("@lune/task")
local mod = require("../mod")
local Signal = mod.signal
type Signal<T...> = mod.Signal<T...>
local result = require("result")
type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
local option = require("option")
type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
--[=[
@class Future
A future represents an asynchronous computation.
A future is a value that might not have finished computing yet. This kind of asynchronous value
makes it possible for a thread to continue doing useful work while it waits for the value to
become available.
### The [Future:poll] Method
The core method of future, poll, attempts to resolve the future into a final value. This method does
not block if the value is not ready. Instead, the current task is executed in the background, and
its progress is reported when polled. When using a future, you generally wont call poll directly,
but instead [Future:await] the value.
```lua
local net = require("@lune/net")
local fut: Future<Result<string, string>> = Future.try(function(url)
local resp = net.request({
url = url,
method = "GET",
})
assert(resp.ok)
return resp.body
end, { "https://jsonplaceholder.typicode.com/posts/1" })
local resp: Result<string, string> = fut:await()
print(net.jsonDecode(resp:unwrap()))
```
]=]
local Future = {}
--[=[
@private
@type Status "initialized" | "pending" | "cancelled" | "ready"
@within Future
Represents the status of a [Future].
]=]
export type Status = "initialized" | "pending" | "cancelled" | "ready"
--[=[
@private
@interface Future<T>
@within Future
Represents the internal state of a [Future].
@field _thread thread -- The background coroutine spawned for execution
@field _ret T -- The value returned once execution has halted
@field _spawnEvt Signal<()> -- Event for internal communication among threads pre execution
@field _retEvt Signal<T | Result<T, string>, Status> -- Event for internal communication among threads post execution
@field _status Status -- The status of the Future
]=]
export type Future<T> = typeof(Future) & {
_ret: T,
_thread: thread,
_spawnEvt: Signal<()>,
_retEvt: Signal<T | Result<T, string>, Status>,
_status: Status,
}
local function _constructor<T>(fn: (Signal<()>, Signal<T, Status>) -> ())
return setmetatable(
{
_thread = coroutine.create(fn),
_spawnEvt = Signal.new(),
_retEvt = Signal.new(),
_status = "initialized",
} :: Future<T>,
{
__index = Future,
}
)
end
--[=[
@within Future
Constructs a [Future] from a function to be run asynchronously.
:::caution
If a the provided function has the possibility to throw an error, instead of any
other rusty-luau types like [Result] or [Option], use [Future:try] instead.
:::
@param fn -- The function to be executed asynchronously
@param args -- The arguments table to be passed to to the function
@return Future<T> -- The constructed future
]=]
function Future.new<T>(fn: (...any) -> T, args: { any })
return _constructor(
function(spawnEvt: Signal<()>, retEvt: Signal<T, Status>)
spawnEvt:Fire()
local ret = fn(table.unpack(args))
retEvt:Fire(ret, "ready")
end
)
end
--[=[
@within Future
Constructs a fallible [Future] from a function to be run asynchronously.
@param fn -- The fallible function to be executed asynchronously
@param args -- The arguments table to be passed to to the function
@return Future<Result<T>> -- The constructed future
]=]
function Future.try<T>(fn: (...any) -> T, args: { any })
return _constructor(
function(
spawnEvt: Signal<()>,
retEvt: Signal<Result<T, string>, Status>
)
spawnEvt:Fire()
local ok, ret = pcall(fn, table.unpack(args))
local res: Result<T, string> = if ok then Ok(ret) else Err(ret)
retEvt:Fire(res, "ready")
end
)
end
--[=[
@within Future
Polls a [Future] to completion.
@param self Future<T>
@return (Status, Option<T>) -- Returns the [Status] and an optional return if completed
]=]
function Future.poll<T>(self: Future<T>): (Status, Option<T>)
if self._status == "initialized" then
self._retEvt:Connect(function(firedRet, status: Status)
self._status = status
self._ret = firedRet
-- Cleanup
coroutine.yield(self._thread)
coroutine.close(self._thread)
self._spawnEvt:DisconnectAll()
self._retEvt:DisconnectAll()
end)
self._spawnEvt:Connect(function()
self._status = "pending"
end)
coroutine.resume(self._thread, self._spawnEvt, self._retEvt)
end
if self._status == "pending" then
-- Just wait a bit more for the signal to fire
task.wait(0.01)
end
local retOpt = if self._ret == nil then None() else Some(self._ret)
return self._status, retOpt
end
--[=[
@within Future
Cancels a [Future].
@param self Future<T>
]=]
function Future.cancel<T>(self: Future<T>)
self._retEvt:Fire(nil :: any, "cancelled")
self._status = "cancelled"
end
--[=[
@within Future
Suspend execution until the result of a [Future] is ready.
This method continuosly polls a [Future] until it reaches completion.
@param self Future<T>
@return T -- The value returned by the function on completion
]=]
function Future.await<T>(self: Future<T>): T
while true do
local status: Status, ret: Option<T> = self:poll()
if status == "ready" then
-- Safe to unwrap, we know it must not be nil
return ret:unwrap()
end
end
end
return Future

View file

@ -1,122 +0,0 @@
--[=[
@class Match
A match expression branches on a pattern.
Match expressions must be exhaustive, meaning that every possible
pattern must be matched, i.e., have an arm handling it. The catch all
arm (`_`), which matches any value, can be used to do this.
Match expressions also ensure that all of the left-sided arms match the
same type of the scrutinee expression, and the right-sided arms are all
of the same type.
```lua
local word = match "hello" {
["hello"] = "hi",
["bonjour"] = "salut",
["hola"] = "hola",
_ = "<unknown>",
}
assert(word == "hi")
```
]=]
local Match = {}
--[=[
@private
@interface Arm<L, R>
@within Match
Represents an arm (right-side) of a match expression.
@type type L -- The type of the scrutinee expression or the left side of the arm
@field result R -- The resolved value of the match expression or the right side of the arm
]=]
type Arm<L, R> = R | (L?) -> R
--[=[
@private
@interface Arms<T, U>
@within Match
Represents a constructed matcher.
@type type T -- The type of the scrutinee expression or the left side of the arm
@field result U -- The resolved value of the match expression or the right side of the arm
]=]
type Arms<T, U> = ({ [T]: Arm<T, U> }) -> U
--[=[
@function match
@within Match
A match expression branches on a pattern. A match expression has a
scrutinee expression (the value to match on) and a list of patterns.
:::note
Currently, most traditional pattern matching is not supported, with
the exception of the catch all pattern (`_`).
:::
A common use-case of match is to prevent repetitive `if` statements, when
checking against various possible values of a variable:
```lua
local function getGreetingNumber(greeting: string): number
return match(greeting) {
["hello, world"] = 1,
["hello, mom"] = 2,
_ = function(val)
return #val
end,
}
end
assert(getGreetingNumber("hello, world") == 1)
assert(getGreetingNumber("hello, mom") == 2)
assert(getGreetingNumber("hello, john") == 11)
@param value T -- The value to match on
@return matcher Arms<T, U> -- A matcher function to call with the match arms
```
]=]
function Match.match<T, U>(value: T): Arms<T, U>
local function handleArmType(arm: Arm<T, U>, fnArg: T?): U
if typeof(arm) == "function" then
return arm(fnArg)
end
return arm
end
return function(arms)
for l, r in arms do
-- Skip the catch all arm for now, get back to it
-- when we have finished checking for all other
-- arms
if l == "_" then
continue
end
if value == l then
local ret = handleArmType(r, nil)
return ret
end
end
-- Since we didn't get any matches, we invoke the catch
-- all arm, giving it the value we have
local catchAll = arms["_"]
or error(
`Non exhaustive match pattern, arm not satisfied for: {value}`
)
return handleArmType(catchAll, value)
end
end
return Match.match

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,38 +0,0 @@
-- From https://gist.github.com/sapphyrus/fd9aeb871e3ce966cc4b0b969f62f539
local function tableEq(tbl1, tbl2)
if tbl1 == tbl2 then
return true
elseif type(tbl1) == "table" and type(tbl2) == "table" then
for key1, value1 in pairs(tbl1) do
local value2 = tbl2[key1]
if value2 == nil then
-- avoid the type call for missing keys in tbl2 by directly comparing with nil
return false
elseif value1 ~= value2 then
if type(value1) == "table" and type(value2) == "table" then
if not tableEq(value1, value2) then
return false
end
else
return false
end
end
end
-- check for missing keys in tbl1
for key2, _ in pairs(tbl2) do
if tbl1[key2] == nil then
return false
end
end
return true
end
return false
end
return {
tableEq = tableEq,
}

View file

@ -1,6 +0,0 @@
---
base: luau
globals:
warn:
args:
- type: ...

View file

@ -1,16 +0,0 @@
local fs = require("@lune/fs")
local luau = require("@lune/luau")
local SIGNAL_PATH =
"Packages/_Index/ffrostflame_luausignal@0.2.4/luausignal/src/init.luau"
local _signal = require(SIGNAL_PATH)
export type Signal<T...> = _signal.luauSignal<T...>
local signal: {
new: <T...>() -> Signal<T...>,
} = luau.load(
'local task = require("@lune/task")\n' .. fs.readFile(SIGNAL_PATH)
)()
return {
signal = signal,
}

View file

@ -1,24 +0,0 @@
title = "rusty-luau"
gitRepoUrl = "https://github.com/CompeyDev/rusty-luau"
gitSourceBranch = "main"
changelog = true
[docusaurus]
onBrokenLinks = "throw"
onBrokenMarkdownLinks = "warn"
favicon = "/logo.png"
organizationName = "CompeyDev"
url = "https://rusty-luau.devcomp.xyz"
baseUrl = "/"
tagline = "Strongly typed implementations of various rust idioms in luau."
# TODO: Disable theme toggle
# [docusaurus.themeConfig.colorMode]
# defaultMode = "dark"
# disableSwitch = true
# respectPrefersColorScheme = false
[footer]
style = "light"
copyright = "Copyright © 2021 Erica Marigold."

View file

@ -1,2 +0,0 @@
std = "lune"
exclude = ["Packages/*"]

View file

@ -1,10 +0,0 @@
line_endings = "Unix"
quote_style = "AutoPreferDouble"
indent_type = "Spaces"
call_parentheses = "NoSingleTable"
indent_width = 4
column_width = 80
[sort_requires]
enabled = true

20
test.luau Normal file
View file

@ -0,0 +1,20 @@
local result = require("lib/result")
type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
local function canError(): Result<number, string>
if math.round(math.random()) == 1 then
return Err("you DIED")
end
return Ok(69)
end
function main()
local val = canError():unwrap()
print("got value: ", val)
end
return main()

View file

@ -1,13 +0,0 @@
# This file is automatically @generated by Wally.
# It is not intended for manual editing.
registry = "test"
[[package]]
name = "compeydev/rusty-luau"
version = "0.1.0"
dependencies = [["luausignal", "ffrostflame/luausignal@0.2.4"]]
[[package]]
name = "ffrostflame/luausignal"
version = "0.2.4"
dependencies = []

View file

@ -4,16 +4,7 @@ version = "0.1.0"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
license = "AGPL-3.0"
exclude = [
"tests/**",
"examples/**",
".vscode/**",
"aftman.toml",
".gitignore",
".gitattributes",
"CHANGELOG.md",
]
include = ["lib/*.luau", "LICENSE.md", "README.md", "wally.toml"]
exclude = ["tests/**", "examples/**", "**"]
include = ["lib/**", "LICENSE.md", "README.md", "wally.toml"]
[dependencies]
luausignal = "ffrostflame/luausignal@0.2.4"