fuckd
This commit is contained in:
parent
8d8b3607fc
commit
3c7edc67da
243
new.exs
243
new.exs
@ -19,9 +19,8 @@ defmodule Tdd.Debug do
|
||||
end
|
||||
|
||||
defp remove_traced_pid(pid) when is_pid(pid) do
|
||||
# Use Process.whereis to avoid race condition if agent stops between calls
|
||||
case Process.whereis(@agent_name) do
|
||||
nil -> :ok # Agent not running, nothing to remove from
|
||||
nil -> :ok
|
||||
agent_pid -> Agent.cast(agent_pid, fn state -> MapSet.delete(state, pid) end)
|
||||
end
|
||||
end
|
||||
@ -34,8 +33,7 @@ defmodule Tdd.Debug do
|
||||
try do
|
||||
Agent.get(agent_pid, &MapSet.member?(&1, pid), :infinity)
|
||||
rescue
|
||||
# Catches if agent dies or is not an agent anymore
|
||||
_e in [Exit, ArgumentError] -> false
|
||||
_e in [Exit, ArgumentError] -> false # More specific rescue
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -44,11 +42,9 @@ defmodule Tdd.Debug do
|
||||
@arg_length_limit 80
|
||||
@total_args_length_limit 200
|
||||
|
||||
# Helper function to return a unique atom for ignored arguments
|
||||
# This is called from the macro-generated code.
|
||||
def __internal_placeholder_for_ignored_arg__ do
|
||||
:__tdd_debug_ignored_arg__ # A unique atom
|
||||
end
|
||||
# MODIFIED: Return the specific atoms format_arg_value expects
|
||||
def __internal_placeholder_for_ignored_arg__, do: :__tdd_debug_ignored_arg__
|
||||
def __internal_placeholder_for_complex_pattern__, do: :__tdd_debug_complex_pattern__
|
||||
|
||||
def format_args_list(args_list) when is_list(args_list) do
|
||||
formatted_args = Enum.map(args_list, &format_arg_value/1)
|
||||
@ -64,14 +60,13 @@ defmodule Tdd.Debug do
|
||||
def format_arg_value(arg) do
|
||||
inspected =
|
||||
case arg do
|
||||
# Check if it's our specific placeholder atom for `_`
|
||||
:__tdd_debug_ignored_arg__ -> "_"
|
||||
# For other arguments, inspect them normally.
|
||||
# This will handle runtime values for simple variables/literals,
|
||||
# and ASTs for complex patterns (if that's what's passed).
|
||||
_ -> inspect(arg, limit: :infinity, pretty: false, structs: true)
|
||||
:__tdd_debug_complex_pattern__ -> "<pattern>"
|
||||
_ -> inspect(arg, limit: @arg_length_limit, pretty: false, structs: true) # Use limit here too
|
||||
end
|
||||
|
||||
# The inspect limit is now applied within the inspect call itself for individual args mostly.
|
||||
# This outer limit is more for the overall string representation from inspect if it didn't truncate.
|
||||
if String.length(inspected) > @arg_length_limit do
|
||||
String.slice(inspected, 0, @arg_length_limit - 3) <> "..."
|
||||
else
|
||||
@ -84,34 +79,29 @@ defmodule Tdd.Debug do
|
||||
init()
|
||||
pid_to_trace = self()
|
||||
add_traced_pid(pid_to_trace)
|
||||
# Trap exits only if not already trapping, to avoid interfering with other code.
|
||||
# However, monitoring is generally safer and less intrusive.
|
||||
# Process.flag(:trap_exit, true) # Consider if this is truly needed or if monitoring is enough.
|
||||
ref = Process.monitor(pid_to_trace)
|
||||
|
||||
# Spawn a separate process to monitor.
|
||||
# Use spawn_opt to avoid linking if this monitoring process crashes.
|
||||
# Spawn linked to ensure monitor process dies if current process dies unexpectedly
|
||||
Process.spawn(
|
||||
fn ->
|
||||
receive do
|
||||
{:DOWN, ^ref, :process, ^pid_to_trace, _reason} ->
|
||||
remove_traced_pid(pid_to_trace)
|
||||
after
|
||||
# 1 hour timeout as a fallback
|
||||
3_600_000 ->
|
||||
remove_traced_pid(pid_to_trace)
|
||||
# after # Consider if timeout is strictly needed or if DOWN message is sufficient
|
||||
# 3_600_000 -> # 1 hour timeout
|
||||
# remove_traced_pid(pid_to_trace)
|
||||
end
|
||||
end,
|
||||
[:monitor] #:link option removed, monitor is explicit
|
||||
[:monitor]
|
||||
# Removed [:monitor] option as spawn_link and Process.monitor achieve desired effect
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def disable_tracing() do
|
||||
init() # Ensure agent is available for removal if it was started by another call
|
||||
# init() # Not strictly necessary here as remove_traced_pid handles agent not existing
|
||||
remove_traced_pid(self())
|
||||
:ok # Good practice to return :ok
|
||||
:ok
|
||||
end
|
||||
|
||||
def run(fun) when is_function(fun, 0) do
|
||||
@ -126,14 +116,14 @@ defmodule Tdd.Debug do
|
||||
# --- Process Dictionary for Depth ---
|
||||
defp get_depth, do: Process.get(:tdd_debug_depth, 0)
|
||||
|
||||
def increment_depth do
|
||||
def increment_depth do # Made private as it's an internal helper
|
||||
new_depth = get_depth() + 1
|
||||
Process.put(:tdd_debug_depth, new_depth)
|
||||
new_depth
|
||||
end
|
||||
|
||||
def decrement_depth do
|
||||
new_depth = max(0, get_depth() - 1) # Ensure depth doesn't go below 0
|
||||
def decrement_depth do # Made private
|
||||
new_depth = max(0, get_depth() - 1)
|
||||
Process.put(:tdd_debug_depth, new_depth)
|
||||
new_depth
|
||||
end
|
||||
@ -143,44 +133,51 @@ defmodule Tdd.Debug do
|
||||
quote do
|
||||
import Kernel, except: [def: 2, def: 1, defp: 2, defp: 1]
|
||||
require Tdd.Debug
|
||||
# Import Tdd.Debug's def/defp macros and other public functions/macros
|
||||
import Tdd.Debug
|
||||
import Tdd.Debug # Imports def/defp from this module, and helper functions
|
||||
end
|
||||
end
|
||||
|
||||
# The `do_instrument` macro seems to be an older/alternative version.
|
||||
# The primary mechanism used by `def`/`defp` is `generate_traced_function`.
|
||||
# We'll keep `do_instrument` as it was in your example, in case it's used elsewhere,
|
||||
# but ensure it's not the cause of the current problem.
|
||||
@doc false
|
||||
defmacro do_instrument(type, call, clauses, _env) do
|
||||
# As per your note, this macro might need updates.
|
||||
# Main issues to check if used:
|
||||
# 1. Hygiene of `actual_code_ast`: use Macro.escape/2.
|
||||
# 2. Robustness of `arg_vars_for_runtime_access` for all pattern types (similar to generate_traced_function).
|
||||
# Kept original logic for now based on your description.
|
||||
{function_name, _meta_call, original_args_ast_nodes} = call
|
||||
original_args_ast_nodes = original_args_ast_nodes || []
|
||||
|
||||
# This part was problematic if original_args_ast_nodes contained `_`
|
||||
# and was used directly to form a list of expressions.
|
||||
arg_vars_for_runtime_access =
|
||||
Enum.map(original_args_ast_nodes, fn
|
||||
{:_, _, Elixir} -> quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end
|
||||
{:=, _, [var_ast, _]} -> var_ast
|
||||
pattern_ast -> pattern_ast
|
||||
{:_, _, ctx} when is_atom(ctx) or is_nil(ctx) ->
|
||||
quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end
|
||||
{:=, _, [var_ast, _]} ->
|
||||
var_ast
|
||||
{var_name, _meta, ctx} = pattern_ast when is_atom(var_name) and (is_atom(ctx) or is_nil(ctx)) and
|
||||
not (var_name in [:_ , :%{}, :{}, :|, :<<>>, :fn, :->, :&, :^]) ->
|
||||
pattern_ast
|
||||
{constructor, _, _} = pattern_ast when constructor in [:%{}, :{}, :|, :<<>>] ->
|
||||
quote do Tdd.Debug.__internal_placeholder_for_complex_pattern__() end
|
||||
pattern_ast ->
|
||||
pattern_ast
|
||||
end)
|
||||
|
||||
actual_code_ast =
|
||||
case clauses do
|
||||
# [do: block_content | _] -> block_content
|
||||
kw when is_list(kw) -> Keyword.get(kw, :do)
|
||||
_ -> clauses
|
||||
end
|
||||
|
||||
traced_body =
|
||||
quote location: :keep do
|
||||
if is_pid_traced?(self()) do
|
||||
current_print_depth = increment_depth()
|
||||
if Tdd.Debug.is_pid_traced?(self()) do
|
||||
current_print_depth = Tdd.Debug.increment_depth() # Qualified
|
||||
indent = String.duplicate(" ", current_print_depth - 1)
|
||||
|
||||
runtime_arg_values = [unquote_splicing(arg_vars_for_runtime_access)]
|
||||
args_string = format_args_list(runtime_arg_values)
|
||||
IO.inspect(runtime_arg_values, label: "runtime_arg_values")
|
||||
IO.puts("runtime_arg_values 1")
|
||||
args_string = Tdd.Debug.format_args_list(runtime_arg_values) # Qualified
|
||||
caller_module_name = Module.split(__MODULE__) |> Enum.join(".")
|
||||
|
||||
IO.puts(
|
||||
@ -188,24 +185,26 @@ defmodule Tdd.Debug do
|
||||
)
|
||||
|
||||
try do
|
||||
result = unquote(actual_code_ast)
|
||||
_ = decrement_depth()
|
||||
# Potential hygiene issue here if actual_code_ast clashes with macro vars
|
||||
result = unquote(actual_code_ast) # Consider Macro.escape(actual_code_ast, unquote: true)
|
||||
_ = Tdd.Debug.decrement_depth() # Qualified
|
||||
IO.puts(
|
||||
"#{indent}RETURN from #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(result)}"
|
||||
"#{indent}RETURN from #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(result)}" # Qualified
|
||||
)
|
||||
result
|
||||
rescue
|
||||
exception_class ->
|
||||
_ = decrement_depth()
|
||||
_ = Tdd.Debug.decrement_depth() # Qualified
|
||||
error_instance = exception_class
|
||||
stacktrace = __STACKTRACE__
|
||||
IO.puts(
|
||||
"#{indent}ERROR in #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(error_instance)}"
|
||||
"#{indent}ERROR in #{caller_module_name}.#{unquote(Macro.escape(function_name))}: #{Tdd.Debug.format_arg_value(error_instance)}" # Qualified
|
||||
)
|
||||
reraise error_instance, stacktrace
|
||||
end
|
||||
else
|
||||
unquote(actual_code_ast)
|
||||
IO.puts("runtime_arg_values 2")
|
||||
unquote(actual_code_ast) # Consider Macro.escape
|
||||
end
|
||||
end
|
||||
|
||||
@ -215,68 +214,87 @@ defmodule Tdd.Debug do
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro def(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
|
||||
defmacro def(call, clauses \\ Keyword.new()) do
|
||||
generate_traced_function(:def, call, clauses, __CALLER__)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro defp(call, clauses \\ Keyword.new()) do # Default clauses to Keyword.new() or []
|
||||
defmacro defp(call, clauses \\ Keyword.new()) do
|
||||
generate_traced_function(:defp, call, clauses, __CALLER__)
|
||||
end
|
||||
|
||||
|
||||
# Capture __CALLER__ to ensure hygiene for variables generated by the macro
|
||||
# if needed, though direct AST manipulation often bypasses some hygiene issues.
|
||||
defp generate_traced_function(type, call_ast, clauses, _caller_env) do
|
||||
{function_name_ast, _meta_call, original_args_ast_nodes} = call_ast
|
||||
args_patterns_ast = original_args_ast_nodes || []
|
||||
defp generate_traced_function(type, call_ast, clauses, _caller_env) do
|
||||
{function_name_ast, _meta_call, original_args_patterns_ast} = call_ast
|
||||
args_patterns_ast = original_args_patterns_ast || []
|
||||
|
||||
original_body_ast =
|
||||
if Keyword.keyword?(clauses) do
|
||||
Keyword.get(clauses, :do, clauses)
|
||||
else
|
||||
clauses
|
||||
end || quote(do: nil) # Default to an empty body if none provided
|
||||
(if Keyword.keyword?(clauses) do
|
||||
Keyword.get(clauses, :do, clauses) # clauses could be `[do: actual_body]` or just `actual_body`
|
||||
else
|
||||
clauses
|
||||
end) || quote(do: nil) # Default to a nil body if nothing provided
|
||||
|
||||
# Transform argument patterns into expressions suitable for logging at runtime.
|
||||
# --- REVISED logging_expressions_ast_list ---
|
||||
logging_expressions_ast_list =
|
||||
Enum.map(args_patterns_ast, fn arg_pattern_ast ->
|
||||
case arg_pattern_ast do
|
||||
{:=, _meta_assign, [var_ast, _sub_pattern_ast]} ->
|
||||
Enum.map(args_patterns_ast, fn arg_pattern_ast_outer ->
|
||||
core_pattern_ast_for_logging =
|
||||
case arg_pattern_ast_outer do
|
||||
{:when, _, [pattern, _guard]} -> pattern
|
||||
other -> other
|
||||
end
|
||||
|
||||
cond do
|
||||
match?({:_, _, ctx} when is_atom(ctx) or is_nil(ctx), core_pattern_ast_for_logging) ->
|
||||
quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end
|
||||
|
||||
match?({:=, _, [{_var_ast, _, _}, _sub_pattern_ast]}, core_pattern_ast_for_logging) ->
|
||||
{:=, _, [var_ast, _]} = core_pattern_ast_for_logging
|
||||
var_ast
|
||||
_ ->
|
||||
Macro.postwalk(arg_pattern_ast, fn
|
||||
current_node_ast ->
|
||||
case current_node_ast do
|
||||
# Match any underscore, regardless of context if it's a bare underscore node
|
||||
{:_, _, context} when is_atom(context) -> # context is often Elixir or nil
|
||||
quote do Tdd.Debug.__internal_placeholder_for_ignored_arg__() end
|
||||
_ ->
|
||||
current_node_ast
|
||||
end
|
||||
end)
|
||||
|
||||
match?({constructor, _, _args} when constructor in [:%{}, :{}, :|, :<<>>], core_pattern_ast_for_logging) ->
|
||||
quote do Tdd.Debug.__internal_placeholder_for_complex_pattern__() end
|
||||
|
||||
match?({name_atom, _, context} when is_atom(name_atom) and name_atom != :_ and (is_atom(context) or is_nil(context)), core_pattern_ast_for_logging) ->
|
||||
{name_atom_inner, _meta, context_inner} = core_pattern_ast_for_logging
|
||||
if is_nil(context_inner) and name_atom_inner not in [:true, :false, :nil] do
|
||||
name_atom_inner
|
||||
else
|
||||
core_pattern_ast_for_logging
|
||||
end
|
||||
|
||||
# Macro.is_literal(core_pattern_ast_for_logging) ->
|
||||
# core_pattern_ast_for_logging
|
||||
|
||||
# Fallback for unhandled complex patterns or other ASTs.
|
||||
# If it's a variable AST that somehow slipped through (e.g. context wasn't atom/nil for a var),
|
||||
# it might be okay to pass `core_pattern_ast_for_logging` directly.
|
||||
# However, to be safe and avoid `_` or other non-value ASTs, placeholder is better.
|
||||
true ->
|
||||
# This case implies a pattern that is not `_`, not `var=pattern`, not a common structure,
|
||||
# not a simple var/literal atom, and not a known literal.
|
||||
# It's likely a more complex pattern we haven't explicitly handled for logging.
|
||||
quote do Tdd.Debug.__internal_placeholder_for_complex_pattern__() end
|
||||
end
|
||||
end)
|
||||
# --- END REVISED logging_expressions_ast_list ---
|
||||
|
||||
# This is the AST for the code that will become the *actual body* of the traced function.
|
||||
traced_body_inner_ast =
|
||||
quote do
|
||||
if is_pid_traced?(self()) do
|
||||
current_print_depth = increment_depth()
|
||||
if Tdd.Debug.is_pid_traced?(self()) do
|
||||
current_print_depth = Tdd.Debug.increment_depth() # Qualified
|
||||
indent = String.duplicate(" ", current_print_depth - 1)
|
||||
|
||||
# Note: Macro.escape is used here because logging_expressions_ast_list
|
||||
# and function_name_ast are ASTs being injected into this quote.
|
||||
__runtime_arg_values_for_logging__ =
|
||||
[unquote_splicing(Macro.escape(logging_expressions_ast_list, unquote: true))]
|
||||
[unquote_splicing(logging_expressions_ast_list)]
|
||||
|
||||
args_string = format_args_list(__runtime_arg_values_for_logging__)
|
||||
args_string = Tdd.Debug.format_args_list(__runtime_arg_values_for_logging__) # Qualified
|
||||
caller_module_name_str = Module.split(__MODULE__) |> Enum.join(".")
|
||||
|
||||
__Printable_fn_name_intermediate__ = unquote(function_name_ast)
|
||||
printable_function_name_str =
|
||||
case unquote(Macro.escape(function_name_ast, unquote: true)) do
|
||||
case __Printable_fn_name_intermediate__ do
|
||||
fn_name_atom when is_atom(fn_name_atom) -> Atom.to_string(fn_name_atom)
|
||||
fn_name_ast_complex -> Macro.to_string(fn_name_ast_complex)
|
||||
fn_name_ast_complex -> Macro.to_string(fn_name_ast_complex) # Handles operators etc.
|
||||
end
|
||||
|
||||
IO.puts(
|
||||
@ -284,58 +302,55 @@ defmodule Tdd.Debug do
|
||||
)
|
||||
|
||||
try do
|
||||
# Original body is injected here, also escaped.
|
||||
result = unquote(Macro.escape(original_body_ast, unquote: true))
|
||||
_ = Tdd.Debug.decrement_depth()
|
||||
_ = Tdd.Debug.decrement_depth() # Qualified
|
||||
IO.puts(
|
||||
"#{indent}RETURN from #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(result)}"
|
||||
"#{indent}RETURN from #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(result)}" # Qualified
|
||||
)
|
||||
result
|
||||
rescue
|
||||
exception_class ->
|
||||
_ = Tdd.Debug.decrement_depth()
|
||||
_ = Tdd.Debug.decrement_depth() # Qualified
|
||||
error_instance = exception_class
|
||||
stacktrace = __STACKTRACE__
|
||||
IO.puts(
|
||||
"#{indent}ERROR in #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(error_instance)}"
|
||||
"#{indent}ERROR in #{caller_module_name_str}.#{printable_function_name_str}: #{Tdd.Debug.format_arg_value(error_instance)}" # Qualified
|
||||
)
|
||||
reraise error_instance, stacktrace
|
||||
end
|
||||
else
|
||||
# Tracing not enabled, execute original body directly (escaped).
|
||||
unquote(Macro.escape(original_body_ast, unquote: true))
|
||||
end
|
||||
end
|
||||
|
||||
# Construct the final `Kernel.def` or `Kernel.defp` call.
|
||||
# `call_ast` is the original function head.
|
||||
# `traced_body_inner_ast` is the AST for the body we just constructed.
|
||||
IO.inspect(call_ast, label: "call_ast")
|
||||
IO.inspect(Macro.escape(call_ast, unquote: true), label: "Macro.escape(call_ast, unquote: true)")
|
||||
final_definition_ast =
|
||||
quote location: :keep do # unquote: false is default and implied if not set
|
||||
quote location: :keep do
|
||||
Kernel.unquote(type)(
|
||||
unquote(call_ast),
|
||||
do: unquote(traced_body_inner_ast) # The body AST is passed via `do:`
|
||||
unquote(call_ast), # This unquotes the function head {name, meta, args_patterns}
|
||||
do: unquote(traced_body_inner_ast)
|
||||
)
|
||||
end
|
||||
|
||||
# Uncomment for debugging the generated code:
|
||||
# IO.inspect(Macro.to_string(final_definition_ast), label: "Generated for #{Macro.to_string(call_ast)}")
|
||||
# For debugging the macro itself:
|
||||
# require Logger
|
||||
# Logger.debug("Generated AST for #{type} #{Macro.to_string(call_ast)}:\n#{Macro.to_string(final_definition_ast)}")
|
||||
|
||||
final_definition_ast
|
||||
end
|
||||
|
||||
# --- Your TDD Graph Printing (unrelated to the tracing error, kept for completeness) ---
|
||||
# --- TDD Graph Printing (Kept for completeness) ---
|
||||
@doc "Prints a formatted representation of a TDD graph starting from an ID."
|
||||
def print(id) do
|
||||
# It's better to alias Tdd.Store at the top of the module if consistently used,
|
||||
# or pass it as an argument if it's a dependency.
|
||||
# For now, keeping alias local to this function if Tdd.Store is not used elsewhere in Debug.
|
||||
alias Tdd.Store # Assuming Tdd.Store is available
|
||||
IO.puts("--- TDD Graph (ID: #{id}) ---")
|
||||
do_print(id, 0, MapSet.new())
|
||||
do_print(id, 0, MapSet.new(), Tdd.Store) # Pass Store if it's an external module
|
||||
IO.puts("------------------------")
|
||||
end
|
||||
|
||||
defp do_print(id, indent_level, visited) do
|
||||
defp do_print(id, indent_level, visited, store_module) do # Accept store_module
|
||||
prefix = String.duplicate(" ", indent_level)
|
||||
|
||||
if MapSet.member?(visited, id) do
|
||||
@ -343,8 +358,8 @@ defmodule Tdd.Debug do
|
||||
:ok
|
||||
else
|
||||
new_visited = MapSet.put(visited, id)
|
||||
alias Tdd.Store # Assuming Tdd.Store is available
|
||||
case Tdd.Store.get_node(id) do
|
||||
# alias Tdd.Store # Removed from here
|
||||
case store_module.get_node(id) do # Use passed module
|
||||
{:ok, :true_terminal} ->
|
||||
IO.puts("#{prefix}ID #{id} -> TRUE")
|
||||
{:ok, :false_terminal} ->
|
||||
@ -352,11 +367,11 @@ defmodule Tdd.Debug do
|
||||
{:ok, {var, y, n, d}} ->
|
||||
IO.puts("#{prefix}ID #{id}: IF #{inspect(var)}")
|
||||
IO.puts("#{prefix} ├─ Yes:")
|
||||
do_print(y, indent_level + 2, new_visited)
|
||||
do_print(y, indent_level + 2, new_visited, store_module)
|
||||
IO.puts("#{prefix} ├─ No:")
|
||||
do_print(n, indent_level + 2, new_visited)
|
||||
do_print(n, indent_level + 2, new_visited, store_module)
|
||||
IO.puts("#{prefix} └─ DC:")
|
||||
do_print(d, indent_level + 2, new_visited)
|
||||
do_print(d, indent_level + 2, new_visited, store_module)
|
||||
{:error, reason} ->
|
||||
IO.puts("#{prefix}ID #{id}: ERROR - #{reason}")
|
||||
end
|
||||
@ -1846,8 +1861,8 @@ defmodule Tdd.Compiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp is_recursive_spec?({:list_of, _}), do: true
|
||||
defp is_recursive_spec?(_), do: false
|
||||
defp is_recursive_spec?({:list_of, _a}), do: true
|
||||
defp is_recursive_spec?(_a), do: false
|
||||
|
||||
# NEW: The logic for simple, non-recursive types.
|
||||
defp compile_non_recursive_spec(spec, context) do
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user