diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | config/config.exs | 2 | ||||
| -rw-r--r-- | lib/application.ex | 13 | ||||
| -rw-r--r-- | lib/audio/server.ex | 61 | ||||
| -rw-r--r-- | lib/audio/supervisor.ex | 2 | ||||
| -rw-r--r-- | lib/calc.ex | 6 | ||||
| -rw-r--r-- | lib/command/command.ex | 57 | ||||
| -rw-r--r-- | lib/command/meme.ex | 58 | ||||
| -rw-r--r-- | lib/command/util.ex | 18 | ||||
| -rw-r--r-- | lib/consumer.ex | 25 | ||||
| -rw-r--r-- | lib/message.ex | 10 | ||||
| -rw-r--r-- | mix.exs | 7 | ||||
| -rw-r--r-- | mix.lock | 4 | ||||
| -rw-r--r-- | native/thulani_calc/.cargo/config.toml | 5 | ||||
| -rw-r--r-- | native/thulani_calc/.gitignore | 1 | ||||
| -rw-r--r-- | native/thulani_calc/Cargo.lock | 537 | ||||
| -rw-r--r-- | native/thulani_calc/Cargo.toml | 20 | ||||
| -rw-r--r-- | native/thulani_calc/README.md | 20 | ||||
| -rw-r--r-- | native/thulani_calc/src/calc.pest | 79 | ||||
| -rw-r--r-- | native/thulani_calc/src/lib.rs | 215 |
20 files changed, 1122 insertions, 20 deletions
@@ -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 @@ -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 @@ -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); + } +} + |
