# SPDX-FileCopyrightText: 2024 Ɓukasz Niemier <#@hauleth.dev> # # SPDX-License-Identifier: MIT defmodule Defconstant do @moduledoc """ Helper functions for defining constant values in your modules. ## Usage ```elixir defmodule Foo do import #{inspect(__MODULE__)} defconst comptime do # This will be evaluated at compile time Enum.sum([ 0, 2, 3, 4, 5, 6, 8, 12, 30, 32, 33, 34, 35, 36, 38, 40, 42, 43, 44, 45, 46, 48, 50, 52, 53, 54, 55, 56, 58, 60, 62, 63, 64, 65, 66, 68, 80, 82, 83, 84, 85, 86, 88 ]) end defonce runtime do 2 * 1068 + 1 end end ``` """ defguardp is_empty_args(value) when is_atom(value) or value == [] @doc """ Defines function that will be evaluated once, *in compile time*, and will return computation result. Defined function can only be 0-ary. Example: defmodule MyConstants do import Defconstant defconst the_answer, do: 42 def my_func do the_answer() * 2 # returns 84 end end """ defmacro defconst(call, opts), do: do_defconst(:def, call, __CALLER__, opts) @doc """ Defines private function that will be evaluated in compile time. For details see `defconst/2`. """ defmacro defconstp(call, opts), do: do_defconst(:defp, call, __CALLER__, opts) defp do_defconst(type, {name, _, args}, _ctx, do: body) when is_empty_args(args) do fbody = quote unquote: false do unquote(result) end quote do result = unquote(body) unquote(type)(unquote(name)(), do: unquote(fbody)) end end defp do_defconst(type, {name, _, args}, %Macro.Env{} = ctx, _) when length(args) > 0 do raise CompileError, description: "`defconst#{if type == :defp, do: "p", else: ""}` can define only 0-ary functions, tried to define #{name}/#{length(args)}-ary.", line: ctx.line, file: ctx.file end @doc """ Defines function that will be evaluated once, *in runtime*, and will cache the result. Defined function can only be 0-ary. """ defmacro defonce(call, opts), do: do_defonce(:def, call, __CALLER__, opts) @doc """ Defines private function that will be evaluated once, *in runtime*, and will cache the result. For details see `defonce/2`. """ defmacro defoncep(call, opts), do: do_defonce(:defp, call, __CALLER__, opts) @max_hash 4_294_967_295 defp do_defonce(type, {name, _, args}, ctx, do: body) when is_atom(args) or args == [] do body_hash = :erlang.phash2(body, @max_hash) if Atom.to_string(name) =~ ~r/[!?]$/ do raise CompileError, description: "`defonce#{if type == :defp, do: "p", else: ""}` cannot end with `!` nor `?` as `#{name}!/0` is used to force recompilation", line: ctx.line, file: ctx.file end quote do unquote(type)(unquote(name)()) do case :persistent_term.get({__MODULE__, unquote(name)}, :__no_val__) do {unquote(body_hash), value} -> value _ -> unquote(:"#{name}!")() end end @doc unquote(""" Force recomputation of `#{name}/0` """) unquote(type)(unquote(:"#{name}!")()) do result = unquote(body) :persistent_term.put({__MODULE__, unquote(name)}, {unquote(body_hash), result}) result end end end defp do_defonce(type, {name, _, args}, ctx, _) when length(args) > 0 do raise CompileError, description: "`defonce#{if type == :defp, do: "p", else: ""}` can define only 0-ary functions, tried to define #{name}/#{length(args)}-ary.", line: ctx.line, file: ctx.file end end