diff --git a/lib/elixir_stan.ex b/lib/elixir_stan.ex index 357c4e8..ac507d1 100644 --- a/lib/elixir_stan.ex +++ b/lib/elixir_stan.ex @@ -1,18 +1,91 @@ +require Logger + defmodule ElixirStan do @moduledoc """ Documentation for `ElixirStan`. """ @doc """ - Hello world. + Get the version of the stanc3 executable bundled with the library. + """ + @spec stanc_version :: String.t() | {:error, String.t()} + def stanc_version() do + case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), ["--version"]) do + {version_string, 0} -> String.trim(version_string) + {error, _} -> {:error, error} + end + end + + @doc """ + Computes the hash of a file used for caching. + + Computes SHA256 of the file and truncates it to 8 bytes, then encodes it in a hex string. + """ + @spec hash_file(binary()) :: String.t() + def hash_file(file) when is_binary(file) do + <> = :crypto.hash(:sha256, file) + + Base.encode16 hash + end - ## Examples + @spec model_tmp_dir(Path.t()) :: binary | {:error, atom} + def model_tmp_dir(path) do + with {:ok, model_code} <- File.read(path) do + model_name = Path.basename(path, ".stan") + model_hash = hash_file(model_code) - iex> ElixirStan.hello() - :world + Path.join([ + System.tmp_dir(), + "elixir_stan", + model_name <> model_hash <> String.replace(stanc_version(), " ", "_") + ]) + end + end + + @doc """ + Transpiles the model into C++ and stores it in a temporary directory. + Temporary directory is based on `System.tmp_dir()`. An `elixir_stan` directory is created in the system temporary + directory and the model is stored in a subdirectory of that. The model directory is based on the model name, the hash + of the model code and the version of stanc used to compile it. """ - def hello do - :world + @spec transpile_model(Path.t()) :: :ok | {:error, any()} + def transpile_model(path) do + with {:ok, model_code} <- File.read(path) do + model_tmp_path = model_tmp_dir(path) + + # Create model dir + :ok = File.mkdir_p(model_tmp_path) + + model_tmp_file = Path.join(model_tmp_path, Path.basename(path)) + + # Write model code + :ok = File.write(model_tmp_file, model_code) + + case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), [model_tmp_file]) do + {_, 0} -> :ok + end + end + end + + @doc """ + Cleans the model tmp directory. + """ + @spec clean_model(Path.t()) :: :ok | {:error, String.t()} + def clean_model(path) do + model_tmp_path = model_tmp_dir(path) + + # Remove model dir + case File.rm_rf(model_tmp_path) do + {:ok, _} -> + :ok + + {:error, error, file} -> + Logger.error( + "Could not clean model: #{path}\n\t Error: #{:file.format_error(error)}\n\t File: #{file}" + ) + + {:error, :file.format_error(error)} + end end end diff --git a/mix.exs b/mix.exs index 6a25309..fd7c3d7 100644 --- a/mix.exs +++ b/mix.exs @@ -14,7 +14,7 @@ defmodule ElixirStan.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger] + extra_applications: [:logger, :crypto] ] end diff --git a/priv/bin/stanc b/priv/bin/stanc new file mode 100755 index 0000000..fe0f704 Binary files /dev/null and b/priv/bin/stanc differ diff --git a/test/data/normal.stan b/test/data/normal.stan new file mode 100644 index 0000000..1aafeba --- /dev/null +++ b/test/data/normal.stan @@ -0,0 +1,13 @@ +data { + int N; + vector[N] y; +} + +parameters { + real mu; + real sigma; +} + +model { + y ~ normal(mu, sigma); +} diff --git a/test/elixir_stan_test.exs b/test/elixir_stan_test.exs index d14fba0..06179ca 100644 --- a/test/elixir_stan_test.exs +++ b/test/elixir_stan_test.exs @@ -2,7 +2,33 @@ defmodule ElixirStanTest do use ExUnit.Case doctest ElixirStan - test "greets the world" do - assert ElixirStan.hello() == :world + test "check stanc version" do + assert ElixirStan.stanc_version() == "stanc3 v2.31.0 (Unix)" + end + + test "hash test" do + assert ElixirStan.hash_file("a") == "CA978112CA1BBDCA" + end + + test "check basic temp path" do + # assert Application.app_dir(:elixir_stan, "test/data/normal.stan") == "a" + assert ElixirStan.model_tmp_dir("test/data/normal.stan") == + "/tmp/elixir_stan/normalA32F3F2E4143ABA4stanc3_v2.31.0_(Unix)" + end + + test "check basic transpile and clean" do + assert ElixirStan.transpile_model("test/data/normal.stan") == :ok + + assert File.exists?( + Path.join(ElixirStan.model_tmp_dir("test/data/normal.stan"), "normal.stan") + ) + + assert File.exists?( + Path.join(ElixirStan.model_tmp_dir("test/data/normal.stan"), "normal.hpp") + ) + + assert ElixirStan.clean_model("test/data/normal.stan") == :ok + + assert !File.exists?(ElixirStan.model_tmp_dir("test/data/normal.stan")) end end