aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Perry <np@nathanperry.dev>2022-12-13 05:30:46 -0500
committerNathan Perry <np@nathanperry.dev>2022-12-13 05:30:46 -0500
commitd0ae07a93cb7c32f6af3def019403d9fc4f71d55 (patch)
tree7419870a2ac0c2c8f3d68eaa0da35cdbf1a0a279
parentd876225c920b94798c01fd01f7a21aaf3aed9a55 (diff)
new: port some logic, write first nif
-rw-r--r--.gitignore2
-rw-r--r--config/config.exs2
-rw-r--r--lib/application.ex13
-rw-r--r--lib/audio/server.ex61
-rw-r--r--lib/audio/supervisor.ex2
-rw-r--r--lib/calc.ex6
-rw-r--r--lib/command/command.ex57
-rw-r--r--lib/command/meme.ex58
-rw-r--r--lib/command/util.ex18
-rw-r--r--lib/consumer.ex25
-rw-r--r--lib/message.ex10
-rw-r--r--mix.exs7
-rw-r--r--mix.lock4
-rw-r--r--native/thulani_calc/.cargo/config.toml5
-rw-r--r--native/thulani_calc/.gitignore1
-rw-r--r--native/thulani_calc/Cargo.lock537
-rw-r--r--native/thulani_calc/Cargo.toml20
-rw-r--r--native/thulani_calc/README.md20
-rw-r--r--native/thulani_calc/src/calc.pest79
-rw-r--r--native/thulani_calc/src/lib.rs215
20 files changed, 1122 insertions, 20 deletions
diff --git a/.gitignore b/.gitignore
index 7389f60..ffbe771 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,5 @@ thulani-*.tar
# Temporary files, for example, from tests.
/tmp/
+
+/priv/
diff --git a/config/config.exs b/config/config.exs
index 2cac044..53acb96 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -10,7 +10,7 @@ config :nostrum,
config :logger,
compile_time_purge_matching: [
- [application: :nostrum, level_lower_than: :info]
+ [application: :nostrum, level_lower_than: :warning]
]
config :logger, :console,
diff --git a/lib/application.ex b/lib/application.ex
index 3445cf0..be6d76f 100644
--- a/lib/application.ex
+++ b/lib/application.ex
@@ -3,8 +3,17 @@ require Logger
defmodule Thulani do
use Application
+ @children [
+ Thulani.DiscordConsumer,
+ {Phoenix.PubSub, name: :thulani_pubsub},
+ Thulani.Command,
+ {Registry, keys: :unique, name: Thulani.Audio.Registry}
+ ]
+
def start(_typ, _arg) do
- Logger.info('starting up')
- Supervisor.start_link([Thulani.Consumer], strategy: :one_for_one)
+ Supervisor.start_link(
+ @children,
+ strategy: :one_for_one
+ )
end
end
diff --git a/lib/audio/server.ex b/lib/audio/server.ex
new file mode 100644
index 0000000..0ec901f
--- /dev/null
+++ b/lib/audio/server.ex
@@ -0,0 +1,61 @@
+alias Nostrum.Struct
+alias Nostrum.Snowflake
+
+defmodule Audio.Server do
+ use GenServer
+
+ @type which :: String.t()
+ @type audio_ref :: {}
+
+ def start_link(arg) do
+ {:ok, _} = DynamicSupervisor.start_link(__MODULE__, arg, name: __MODULE__.Supervisor)
+ {:ok, _} = Registry.start_link(keys: :unique, name: __MODULE__.Registry)
+ end
+
+ @spec play_now(Struct.Guild.id(), audio_ref) :: which
+ def play_now(guild, audio_ref) do
+ {:ok, which} = GenServer.call(__MODULE__, {:now, guild, audio_ref})
+ which
+ end
+
+ @spec enqueue(Struct.Guild.id(), audio_ref) :: which
+ def enqueue(guild, audio_ref) do
+ GenServer.call(__MODULE__, {:enqueue})
+ end
+
+ @spec cancel(which) :: GenServer.call()
+ def cancel(which) do
+ GenServer.call(__MODULE__, {:cancel, which})
+ end
+
+ @impl true
+ def init(guild) do
+ {:ok, {guild, :queue.new()}}
+ end
+
+ @impl true
+ def handle_call({:now, {type, ref, opts}}, _from, {guild, q}) do
+ if Nostrum.Voice.playing?(guild) do
+ :ok = Nostrum.Voice.stop(guild)
+ end
+
+ id = UUID.uuid4()
+ Nostrum.Voice.play(guild, ref, type, opts)
+
+ q = :queue.in_r({id, ref}, q)
+ {:reply, :ok, {guild, q}}
+ end
+
+ @impl true
+ def handle_call({:enqueue, audio_ref}, _from, {_guild, q}) do
+ id = UUID.uuid4()
+ q = :queue.in({id, audio_ref}, q)
+ {:reply, {:ok, id}, {id, q}}
+ end
+
+ @impl true
+ def handle_call({:cancel, audio_ref}, _from, {guild, q}) do
+ q = :queue.delete_with(fn _elem -> false end, q)
+ {:reply, :ok, {guild, q}}
+ end
+end
diff --git a/lib/audio/supervisor.ex b/lib/audio/supervisor.ex
new file mode 100644
index 0000000..0b86a1c
--- /dev/null
+++ b/lib/audio/supervisor.ex
@@ -0,0 +1,2 @@
+defmodule Thulani.Audio.Supervisor do
+end
diff --git a/lib/calc.ex b/lib/calc.ex
new file mode 100644
index 0000000..0e53c06
--- /dev/null
+++ b/lib/calc.ex
@@ -0,0 +1,6 @@
+defmodule Thulani.Calc do
+ use Rustler, otp_app: :thulani, crate: "thulani_calc"
+
+ @spec eval(String.t()) :: {:ok, number | :inf | :ninf | :nan} | {:error, String.t()}
+ def eval(_expr), do: :erlang.nif_error(:nif_not_loaded)
+end
diff --git a/lib/command/command.ex b/lib/command/command.ex
new file mode 100644
index 0000000..f83f02b
--- /dev/null
+++ b/lib/command/command.ex
@@ -0,0 +1,57 @@
+alias Phoenix.PubSub
+alias Nostrum.Api
+
+defmodule Thulani.Command do
+ use GenServer
+ use Thulani.Command.Util
+ require Logger
+
+ def start_link(arg) do
+ GenServer.start_link(__MODULE__, arg)
+ end
+
+ @impl true
+ def init(_) do
+ PubSub.subscribe(:thulani_pubsub, "cmd")
+ {:ok, %{}}
+ end
+
+ @impl true
+ def handle_info(Util.command("meme", "", msg), state) do
+ {:noreply, state}
+ end
+
+ @impl true
+ def handle_info(Util.command("meme", rest, msg), state) do
+ case rest do
+ "" -> Thulani.Meme
+ end
+
+ {:noreply, state}
+ end
+
+ def handle_info(Util.command("http", rest, msg), state) do
+ end
+
+ def handle_info(Util.command("roll", rest, msg), state) do
+ Logger.info("hi")
+
+ case rest do
+ " " <> expr ->
+ case Thulani.Calc.eval(expr) do
+ {:ok, val} -> Api.create_message!(msg.channel_id, "#{val}")
+ {:error, err} -> Logger.error("BAD: #{err}")
+ end
+
+ _ ->
+ Logger.error("nothing else in the message")
+ end
+
+ {:noreply, state}
+ end
+
+ def handle_info(_msg, state) do
+ Logger.debug("unhandled message")
+ {:noreply, state}
+ end
+end
diff --git a/lib/command/meme.ex b/lib/command/meme.ex
new file mode 100644
index 0000000..3c51334
--- /dev/null
+++ b/lib/command/meme.ex
@@ -0,0 +1,58 @@
+alias Nostrum.Api
+alias Nostrum.Struct.Message
+alias Thulani.Command.Util
+
+defmodule Thulani.Command.Meme do
+ use GenServer
+
+ @type audio_permission :: :unrestrict | boolean
+
+ defmodule Request do
+ alias Thulani.Command.Meme
+
+ @type t ::
+ %__MODULE__{
+ name: :random,
+ audio: Meme.audio_permission(),
+ msg: Message.t()
+ }
+ | %__MODULE__{
+ name: String.t(),
+ audio: nil,
+ msg: Message.t()
+ }
+
+ @enforce_keys [:msg]
+ defstruct name: :random, audio: :unrestrict, msg: nil
+ end
+
+ @spec random_meme(Message.t()) :: GenServer.cast()
+ def random_meme(msg) do
+ GenServer.cast(__MODULE__, %Request{msg: msg})
+ end
+
+ @spec meme(Message.t(), String.t()) :: GenServer.cast()
+ @spec meme(Message.t(), String.t(), audio_permission) :: GenServer.cast()
+ def meme(msg, name, audio \\ :unrestrict) do
+ GenServer.cast(__MODULE__, %Request{name: name, msg: msg, audio: audio})
+ end
+
+ def start_link(arg) do
+ GenServer.start_link(__MODULE__, arg)
+ end
+
+ @impl true
+ def init(_) do
+ {:ok, %{}}
+ end
+
+ @impl true
+ def handle_cast(%Request{name: :random, msg: msg, audio: audio}, state) do
+ {:noreply, state}
+ end
+
+ @impl true
+ def handle_cast(%Request{name: name, msg: msg}, state) do
+ {:noreply, state}
+ end
+end
diff --git a/lib/command/util.ex b/lib/command/util.ex
new file mode 100644
index 0000000..8f2018f
--- /dev/null
+++ b/lib/command/util.ex
@@ -0,0 +1,18 @@
+defmodule Thulani.Command.Util do
+ alias Nostrum.Snowflake
+ @type qualified_channel :: {Snowflake.t(), Snowflake.t()}
+
+ defmacro __using__(_opts) do
+ mod = __MODULE__
+
+ quote do
+ alias unquote(mod)
+ end
+ end
+
+ defmacro command(word, rest, msg) do
+ quote do
+ {:cmd, " #{unquote(word)}" <> unquote(rest), unquote(msg)}
+ end
+ end
+end
diff --git a/lib/consumer.ex b/lib/consumer.ex
index ace0ba8..614c261 100644
--- a/lib/consumer.ex
+++ b/lib/consumer.ex
@@ -1,24 +1,29 @@
require Logger
-require OK
-alias Nostrum.Api
+alias Nostrum.Struct.Message
+alias Phoenix.PubSub
-defmodule Thulani.Consumer do
+defmodule Thulani.DiscordConsumer do
use Nostrum.Consumer
def start_link do
- Logger.info('starting consumer')
Nostrum.Consumer.start_link(__MODULE__)
end
@spec handle_event(Nostrum.Consumer.message_create()) :: nil
- def handle_event({:MESSAGE_CREATE, message, _ws_state}) do
- case Thulani.Message.message(message) do
- {:ok, content} -> Api.create_message!(message.channel_id, content)
- :ignore -> nil
- end
+ def handle_event({
+ :MESSAGE_CREATE,
+ message = %Message{},
+ _ws_state
+ }) do
+ case Thulani.Message.validate(message) do
+ {:ok, command} ->
+ Logger.debug(%{command: command})
+ PubSub.broadcast!(:thulani_pubsub, "cmd", {:cmd, command, message})
- nil
+ :ignore ->
+ nil
+ end
end
def handle_event(_event), do: :noop
diff --git a/lib/message.ex b/lib/message.ex
index f04e1e7..60d3aa8 100644
--- a/lib/message.ex
+++ b/lib/message.ex
@@ -7,8 +7,8 @@ alias Nostrum.Api
defmodule Thulani.Message do
@prefix Application.compile_env(:thulani, :prefix, "!thulani")
- @spec message(Message.t()) :: {:ok, String.t()} | :ignore
- def message(%Message{
+ @spec validate(Message.t()) :: {:ok, String.t()} | :ignore
+ def validate(%Message{
content: @prefix <> rest,
guild_id: guild_id,
author: %User{
@@ -19,7 +19,7 @@ defmodule Thulani.Message do
{:ok, rest}
end
- def message(%Message{
+ def validate(%Message{
guild_id: guild_id,
author: %User{
username: username,
@@ -39,8 +39,8 @@ defmodule Thulani.Message do
:ignore
end
- def message(msg) do
- Logger.debug(%{message: "unmatched message", msg: msg})
+ def validate(msg) do
+ Logger.debug(%{validate: "unmatched validate", msg: msg})
:ignore
end
diff --git a/mix.exs b/mix.exs
index 2fb5b1b..c18bc1f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -14,7 +14,7 @@ defmodule Thulani.MixProject do
def application do
[
mod: {Thulani, []},
- extra_applications: [:logger]
+ extra_applications: [:logger, :crypto]
]
end
@@ -25,7 +25,10 @@ defmodule Thulani.MixProject do
{:algae, "~> 1.2"},
{:quark, "~> 2.3"},
{:exceptional, "~> 2.1"},
- {:ok, "~> 2.3"}
+ {:ok, "~> 2.3"},
+ {:phoenix_pubsub, "~>2.1"},
+ {:uuid, "~> 1.1"},
+ {:rustler, "~> 0.26"}
]
end
end
diff --git a/mix.lock b/mix.lock
index 03f2879..c486c6d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -15,9 +15,13 @@
"nostrum": {:hex, :nostrum, "0.6.1", "aaad13e8e8ce8ace1f218685c6824972e7ea4f979e5ba3242a98f22bda52e605", [:mix], [{:certifi, "~> 2.8", [hex: :certifi, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.11 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:gun, "2.0.1", [hex: :remedy_gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "27925a47e413766664928fbeb38c9887a7fd23066cdba831d5fd8241072bf76a"},
"ok": {:hex, :ok, "2.3.0", "0a3d513ec9038504dc5359d44e14fc14ef59179e625563a1a144199cdc3a6d30", [:mix], [], "hexpm", "f0347b3f8f115bf347c704184b33cf084f2943771273f2b98a3707a5fa43c4d5"},
"operator": {:hex, :operator, "0.2.1", "4572312bbd3e63a5c237bf15c3a7670d568e3651ea744289130780006e70e5f5", [:mix], [], "hexpm", "1990cc6dc651d7fff04636eef06fc64e6bc1da83a1da890c08ca3432e17e267a"},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
"poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"},
"quark": {:hex, :quark, "2.3.2", "066e0d431440d077684469967f54d732443ea2a48932e0916e974633e8b39c95", [:mix], [], "hexpm", "2f6423779b02afe7e3e4af3cfecfcd94572f2051664d4d8329ffa872d24b10a8"},
+ "rustler": {:hex, :rustler, "0.26.0", "06a2773d453ee3e9109efda643cf2ae633dedea709e2455ac42b83637c9249bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "42961e9d2083d004d5a53e111ad1f0c347efd9a05cb2eb2ffa1d037cdc74db91"},
"salsa20": {:hex, :salsa20, "1.0.3", "fb900fc9b26b713a98618f3c6d6b6c35a5514477c6047caca8d03f3a70175ab0", [:mix], [], "hexpm", "91cbfa537f369d074a79f926f36a6c2ac24fba12cbadcb23aa04a759282887fe"},
+ "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"type_class": {:hex, :type_class, "1.2.8", "349db84be8c664e119efaae1a09a44b113bc8e81af1d032f4e3e38feef4fac32", [:mix], [{:exceptional, "~> 2.1", [hex: :exceptional, repo: "hexpm", optional: false]}], "hexpm", "bb93de2cacfd6f0ee43f4616f7a139816a73deba4ae8ee3364bcfa4abe3eef3e"},
+ "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
"witchcraft": {:hex, :witchcraft, "1.0.4", "8733ac0ee769d4d2f73610de5a2b601a4ccbe385d1fca6419280f2511d21d0c9", [:mix], [{:exceptional, "~> 2.1", [hex: :exceptional, repo: "hexpm", optional: false]}, {:operator, "~> 0.2", [hex: :operator, repo: "hexpm", optional: false]}, {:quark, "~> 2.2", [hex: :quark, repo: "hexpm", optional: false]}, {:type_class, "~> 1.2", [hex: :type_class, repo: "hexpm", optional: false]}], "hexpm", "a380f439f1962d2e56cdad874ed7eb4612ddd6ec5ee3c6ad0c5d63e60539e6b0"},
}
diff --git a/native/thulani_calc/.cargo/config.toml b/native/thulani_calc/.cargo/config.toml
new file mode 100644
index 0000000..20f03f3
--- /dev/null
+++ b/native/thulani_calc/.cargo/config.toml
@@ -0,0 +1,5 @@
+[target.'cfg(target_os = "macos")']
+rustflags = [
+ "-C", "link-arg=-undefined",
+ "-C", "link-arg=dynamic_lookup",
+]
diff --git a/native/thulani_calc/.gitignore b/native/thulani_calc/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/native/thulani_calc/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/native/thulani_calc/Cargo.lock b/native/thulani_calc/Cargo.lock
new file mode 100644
index 0000000..3504dc8
--- /dev/null
+++ b/native/thulani_calc/Cargo.lock
@@ -0,0 +1,537 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.138"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
+
+[[package]]
+name = "libm"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matrixmultiply"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84"
+dependencies = [
+ "rawpointer",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "nalgebra"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d506eb7e08d6329505faa8a3a00a5dcc6de9f76e0c77e4b75763ae3c770831ff"
+dependencies = [
+ "approx",
+ "matrixmultiply",
+ "nalgebra-macros",
+ "num-complex",
+ "num-rational",
+ "num-traits",
+ "rand",
+ "rand_distr",
+ "simba",
+ "typenum",
+]
+
+[[package]]
+name = "nalgebra-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01fcc0b8149b4632adc89ac3b7b31a12fb6099a0317a4eb2ebff574ef7de7218"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "paste"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
+
+[[package]]
+name = "pest"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha1",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_distr"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
+dependencies = [
+ "num-traits",
+ "rand",
+]
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustler"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61e8ddf75de20513455d7b6f17241a595abbb01b53a6340cecc798a1b13422d"
+dependencies = [
+ "lazy_static",
+ "rustler_codegen",
+ "rustler_sys",
+]
+
+[[package]]
+name = "rustler_codegen"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa2e45c0165272070f80ce93bcd7dd5407a3c84a1ef73ab9900e00f00ef3d36"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "rustler_sys"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff26a42e62d538f82913dd34f60105ecfdffbdb25abdc3c3580b0c622285332"
+dependencies = [
+ "regex",
+ "unreachable",
+]
+
+[[package]]
+name = "safe_arch"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "794821e4ccb0d9f979512f9c1973480123f9bd62a90d74ab0f9426fcf8f4a529"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "simba"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0b7840f121a46d63066ee7a99fc81dcabbc6105e437cae43528cea199b5a05f"
+dependencies = [
+ "approx",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "wide",
+]
+
+[[package]]
+name = "statrs"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d08e5e1748192713cc281da8b16924fb46be7b0c2431854eadc785823e5696e"
+dependencies = [
+ "approx",
+ "lazy_static",
+ "nalgebra",
+ "num-traits",
+ "rand",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thulani_calc"
+version = "0.1.0"
+dependencies = [
+ "lazy_static",
+ "log",
+ "pest",
+ "pest_derive",
+ "rand",
+ "rustler",
+ "statrs",
+ "thiserror",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unreachable"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
+dependencies = [
+ "void",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wide"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae41ecad2489a1655c8ef8489444b0b113c0a0c795944a3572a0931cf7d2525c"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
diff --git a/native/thulani_calc/Cargo.toml b/native/thulani_calc/Cargo.toml
new file mode 100644
index 0000000..8ba8a75
--- /dev/null
+++ b/native/thulani_calc/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "thulani_calc"
+version = "0.1.0"
+authors = []
+edition = "2018"
+
+[lib]
+name = "thulani_calc"
+path = "src/lib.rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+rustler = "0.26.0"
+pest = "2.5"
+pest_derive = "2.5"
+lazy_static = "1.4"
+statrs = "0.16"
+thiserror = "1.0"
+rand = "0.8"
+log = "0.4"
diff --git a/native/thulani_calc/README.md b/native/thulani_calc/README.md
new file mode 100644
index 0000000..03e1510
--- /dev/null
+++ b/native/thulani_calc/README.md
@@ -0,0 +1,20 @@
+# NIF for Elixir.Thulani.Calc
+
+## To build the NIF module:
+
+- Your NIF will now build along with your project.
+
+## To load the NIF:
+
+```elixir
+defmodule Thulani.Calc do
+ use Rustler, otp_app: :thulani, crate: "thulani_calc"
+
+ # When your NIF is loaded, it will override this function.
+ def add(_a, _b), do: :erlang.nif_error(:nif_not_loaded)
+end
+```
+
+## Examples
+
+[This](https://github.com/rusterlium/NifIo) is a complete example of a NIF written in Rust.
diff --git a/native/thulani_calc/src/calc.pest b/native/thulani_calc/src/calc.pest
new file mode 100644
index 0000000..07eeddb
--- /dev/null
+++ b/native/thulani_calc/src/calc.pest
@@ -0,0 +1,79 @@
+num = {
+ hex
+ | oct
+ | binary
+ | float
+}
+
+float = @{ int ~ ( "." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? }
+ int = { "-"? ~ ASCII_DIGIT+ }
+
+hex = @{ "0x" ~ ASCII_HEX_DIGIT+ }
+oct = @{ "0o" ~ ASCII_OCT_DIGIT+ }
+binary = @{ "0b" ~ ASCII_BIN_DIGIT+ }
+
+infix = _{ add | sub | mul | div | modulo }
+ add = { "+" }
+ sub = { "-" }
+ modulo = { "%" | "mod" }
+ mul = { "*" }
+ div = { "/" }
+
+tight_infix = _{ dice | pow }
+ dice = { "d" }
+ pow = { "^" }
+
+trig = _{ sin | cos | tan | asin | acos | atan }
+ sin = { "sin" }
+ cos = { "cos" }
+ tan = { "tan" }
+ asin = { "asin" }
+ acos = { "acos" }
+ atan = { "atan" }
+
+htrig = _{ sinh | cosh | tanh | asinh | acosh | atanh }
+ sinh = { "sinh" }
+ cosh = { "cosh" }
+ tanh = { "tanh" }
+ asinh = { "asinh" }
+ acosh = { "acosh" }
+ atanh = { "atanh" }
+
+unary_prefix = _{ log | sqrt | sgn | htrig | trig | exp | abs | ceil | floor | round }
+ log = { "log" | "ln" }
+ sqrt = { "sqrt" }
+ sgn = { "sgn" }
+ exp = { "exp" }
+ abs = { "abs" }
+ ceil = { "ceil" }
+ floor = { "floor" }
+ round = { "round" }
+
+binary_prefix = _{ min | max | atan2 }
+ min = { "min" }
+ max = { "max" }
+ atan2 = { "atan2" }
+
+suffix = _{ factorial }
+ factorial = { "!" }
+
+term = _{ num | "(" ~ expr ~ ")" }
+
+suffix_expr = { term ~ suffix }
+unary_expr = ${ unary_prefix ~ ws+ ~ outfix_expr }
+binary_expr = ${ binary_prefix ~ ws+ ~ outfix_expr ~ ws+ ~ outfix_expr }
+
+tight = _{ (suffix_expr | term) ~ (tight_infix ~ tight)* }
+
+expr = { outfix_expr ~ (infix ~ outfix_expr)* }
+
+outfix_expr = _{
+ tight |
+ binary_expr |
+ unary_expr
+}
+
+calc = _{ SOI ~ expr ~ EOI }
+
+ws = _{ " " | "\t" | "\n" }
+WHITESPACE = _{ ws }
diff --git a/native/thulani_calc/src/lib.rs b/native/thulani_calc/src/lib.rs
new file mode 100644
index 0000000..75a6b37
--- /dev/null
+++ b/native/thulani_calc/src/lib.rs
@@ -0,0 +1,215 @@
+use log::error;
+use rand::prelude::*;
+use statrs;
+use thiserror::Error;
+use lazy_static::lazy_static;
+use rustler::Encoder;
+
+mod atoms {
+ rustler::atoms! {
+ ok,
+ error,
+ inf,
+ ninf,
+ nan,
+ }
+}
+
+#[rustler::nif]
+pub fn eval<'a>(env: rustler::Env<'a>, args: String) -> Result<rustler::Term<'a>, String> {
+ Calc::eval(&args)
+ .map(|x| match x {
+ f64::INFINITY => atoms::inf().encode(env),
+ f64::NEG_INFINITY => atoms::ninf().encode(env),
+ x if x.is_nan() => atoms::nan().encode(env),
+ x => x.encode(env),
+ })
+ .map_err(|e| e.to_string())
+}
+
+rustler::init!("Elixir.Thulani.Calc", [eval]);
+
+
+#[derive(pest_derive::Parser)]
+#[grammar = "calc.pest"]
+struct Calc;
+
+#[derive(Copy, Clone, Error, Debug, PartialEq, Eq, Hash)]
+pub(crate) enum CalcError {
+ #[error("pest was unable to parse the input")]
+ Pest,
+
+ #[error("invalid number format")]
+ NumberFormat,
+
+ #[error("bad argument count")]
+ ArgCount,
+}
+
+impl Calc {
+ fn eval<S: AsRef<str>>(s: S) -> Result<f64, CalcError> {
+ use pest::{
+ Parser,
+ prec_climber::PrecClimber,
+ iterators::{Pair, Pairs},
+ };
+
+ use self::Rule::*;
+
+ lazy_static! {
+ static ref CLIMBER: PrecClimber<self::Rule> = {
+ use pest::prec_climber::{
+ Operator,
+ Assoc::*,
+ };
+
+ PrecClimber::new(vec![
+ Operator::new(add, Left) | Operator::new(sub, Left) | Operator::new(modulo, Left),
+ Operator::new(mul, Left) | Operator::new(div, Left),
+ Operator::new(dice, Left),
+ Operator::new(pow, Right),
+ ])
+ };
+ }
+
+ let result = Calc::parse(calc, s.as_ref()).map_err(|_| CalcError::Pest)?;
+
+ fn eval_single_pair(pair: Pair<self::Rule>) -> Result<f64, CalcError> {
+ let result = match pair.as_rule() {
+ oct | hex | binary => {
+ let base = match pair.as_rule() {
+ hex => 16,
+ oct => 8,
+ binary => 2,
+ _ => unreachable!(),
+ };
+
+ u64::from_str_radix(&pair.as_str()[2..], base).map_err(|_| CalcError::NumberFormat)? as f64
+ },
+ float => pair.as_str().parse::<f64>().map_err(|_| CalcError::NumberFormat)?,
+ expr | num => eval_expr(pair.into_inner())?,
+ unary_expr => {
+ let mut p = pair.into_inner();
+
+ let op = p.next().ok_or(CalcError::ArgCount)?;
+ let arg = eval_expr(p)?;
+
+ match op.as_rule() {
+ log => arg.ln(),
+ sqrt => arg.sqrt(),
+ sgn => arg.signum(),
+
+ sin => arg.sin(),
+ cos => arg.cos(),
+ tan => arg.tan(),
+ asin => arg.asin(),
+ acos => arg.acos(),
+ atan => arg.atan(),
+
+ sinh => arg.sinh(),
+ cosh => arg.cosh(),
+ tanh => arg.tanh(),
+ asinh => arg.asinh(),
+ acosh => arg.acosh(),
+ atanh => arg.atanh(),
+
+ exp => arg.exp(),
+ abs => arg.abs(),
+ ceil => arg.ceil(),
+ floor => arg.floor(),
+ round => arg.round(),
+ _ => unreachable!(),
+ }
+ },
+ binary_expr => {
+ let mut p = pair.into_inner();
+
+ let op = p.next().ok_or(CalcError::ArgCount)?;
+
+ let arg1 = eval_single_pair(p.next().ok_or(CalcError::ArgCount)?)?;
+ let arg2 = eval_single_pair(p.next().ok_or(CalcError::ArgCount)?)?;
+
+ assert!(p.next().is_none());
+
+ match op.as_rule() {
+ min => arg1.min(arg2),
+ max => arg1.max(arg2),
+ atan2 => arg1.atan2(arg2),
+ _ => unreachable!(),
+ }
+ },
+ suffix_expr => {
+ let mut p = pair.into_inner();
+
+ let arg = eval_expr(p.next().ok_or(CalcError::ArgCount)?.into_inner())?;
+ let op = p.next().ok_or(CalcError::ArgCount)?;
+
+ assert!(p.next().is_none());
+
+ match op.as_rule() {
+ factorial => statrs::function::gamma::gamma(arg + 1.),
+ _ => unreachable!(),
+ }
+ },
+ _ => unreachable!(),
+ };
+
+ Ok(result)
+ }
+
+ fn eval_expr(p: Pairs<self::Rule>) -> Result<f64, CalcError> {
+ CLIMBER.climb(
+ p,
+ eval_single_pair,
+ |lhs, op, rhs| {
+ let lhs = lhs?;
+ let rhs = rhs?;
+
+ let result = match op.as_rule() {
+ add => lhs + rhs,
+ sub => lhs - rhs,
+ mul => lhs * rhs,
+ div => lhs / rhs,
+ pow => lhs.powf(rhs),
+ dice => {
+ let dice_count = lhs as usize;
+ let dice_faces = rhs as usize;
+
+ let mut rng = thread_rng();
+ (0..dice_count).map(|_| rng.gen_range(1..=dice_faces)).sum::<usize>() as f64
+ },
+ _ => unreachable!(),
+ };
+
+ Ok(result)
+ }
+ )
+ }
+
+ eval_expr(result)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_calc_basics() {
+ assert_eq!(3., Calc::eval("1 + 2").unwrap());
+ assert_eq!(3.0f64.ln(), Calc::eval("log 3").unwrap());
+ assert!(6. - Calc::eval("3!").unwrap() < 0.0001);
+ assert_eq!(3., Calc::eval("max 3 2").unwrap());
+ }
+
+ #[test]
+ fn test_binary_unary() {
+ assert_eq!(3.0f64.ln(), Calc::eval("max log 3 log 2").unwrap());
+ }
+
+ #[test]
+ fn test_prefix_suffix() {
+ assert!(6. - Calc::eval("abs 3!").unwrap() < 0.0001);
+ }
+}
+