Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Expand AST using Macro.Env API #1116

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 2 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ jobs:
fail-fast: false
matrix:
include:
- elixir: 1.12.x
otp: 22.x
tests_may_fail: false
- elixir: 1.12.x
otp: 23.x
tests_may_fail: false
- elixir: 1.12.x
otp: 24.x
tests_may_fail: false
- elixir: 1.13.x
otp: 22.x
tests_may_fail: false
Expand Down Expand Up @@ -98,12 +89,6 @@ jobs:
fail-fast: false
matrix:
include:
- elixir: 1.12.x
otp: 22.x
- elixir: 1.12.x
otp: 23.x
- elixir: 1.12.x
otp: 24.x
- elixir: 1.13.x
otp: 22.x
- elixir: 1.13.x
Expand Down Expand Up @@ -165,8 +150,8 @@ jobs:
strategy:
matrix:
include:
- elixir: 1.15.x
otp: 25.x
- elixir: 1.17.x
otp: 27.x
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
Expand Down
101 changes: 65 additions & 36 deletions apps/debug_adapter/lib/debug_adapter/breakpoint_condition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,27 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
@spec register_condition(
module,
module,
[non_neg_integer],
non_neg_integer,
String.t(),
String.t() | nil,
non_neg_integer
String.t()
) ::
{:ok, {module, atom}} | {:error, :limit_reached}
def register_condition(name \\ __MODULE__, module, lines, condition, log_message, hit_count) do
def register_condition(name \\ __MODULE__, module, line, env, condition, log_message, hit_count) do
GenServer.call(
name,
{:register_condition, {module, lines}, condition, log_message, hit_count}
{:register_condition, {module, line}, env, condition, log_message, hit_count}
)
end

@spec unregister_condition(module, module, [non_neg_integer]) :: :ok
def unregister_condition(name \\ __MODULE__, module, lines) do
GenServer.cast(name, {:unregister_condition, {module, lines}})
@spec unregister_condition(module, module, non_neg_integer) :: :ok
def unregister_condition(name \\ __MODULE__, module, line) do
GenServer.cast(name, {:unregister_condition, {module, line}})
end

@spec has_condition?(module, module, [non_neg_integer]) :: boolean
def has_condition?(name \\ __MODULE__, module, lines) do
GenServer.call(name, {:has_condition?, {module, lines}})
@spec has_condition?(module, module, non_neg_integer) :: boolean
def has_condition?(name \\ __MODULE__, module, line) do
GenServer.call(name, {:has_condition?, {module, line}})
end

@spec get_condition(module, non_neg_integer) ::
Expand Down Expand Up @@ -96,7 +96,7 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do

@impl GenServer
def handle_call(
{:register_condition, key, condition, log_message, hit_count},
{:register_condition, key, env, condition, log_message, hit_count},
_from,
%{free: free, conditions: conditions} = state
) do
Expand All @@ -111,7 +111,7 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
state
| free: rest,
conditions:
conditions |> Map.put(key, {number, {condition, log_message, hit_count}})
conditions |> Map.put(key, {number, {env, condition, log_message, hit_count}})
}

{:reply, {:ok, {__MODULE__, :"check_#{number}"}}, state}
Expand All @@ -120,7 +120,8 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
{number, _old_condition} ->
state = %{
state
| conditions: conditions |> Map.put(key, {number, {condition, log_message, hit_count}})
| conditions:
conditions |> Map.put(key, {number, {env, condition, log_message, hit_count}})
}

{:reply, {:ok, {__MODULE__, :"check_#{number}"}}, state}
Expand All @@ -132,11 +133,11 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
end

def handle_call({:get_condition, number}, _from, %{conditions: conditions, hits: hits} = state) do
{condition, log_message, hit_count} =
{env, condition, log_message, hit_count} =
conditions |> Map.values() |> Enum.find(fn {n, _c} -> n == number end) |> elem(1)

hits = hits |> Map.get(number, 0)
{:reply, {condition, log_message, hit_count, hits}, state}
{:reply, {env, condition, log_message, hit_count, hits}, state}
end

def handle_call(:clear, _from, _state) do
Expand Down Expand Up @@ -177,14 +178,20 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
for i <- @range do
@spec unquote(:"check_#{i}")(term) :: boolean
def unquote(:"check_#{i}")(binding) do
{condition, log_message, hit_count, hits} = get_condition(unquote(i))
{env, condition, log_message, hit_count_condition, hits} = get_condition(unquote(i))
elixir_binding = binding |> ElixirLS.DebugAdapter.Binding.to_elixir_variable_names()
result = eval_condition(condition, elixir_binding)
result = eval_condition(condition, elixir_binding, env)

result =
if result do
register_hit(unquote(i))
# do not break if hit count not reached
# the spec requires:
# If both this property and `condition` are specified, `hitCondition` should
# be evaluated only if the `condition` is met, and the debug adapter should
# stop only if both conditions are met.

hit_count = eval_hit_condition(hit_count_condition, elixir_binding, env)
hits + 1 > hit_count
else
result
Expand All @@ -194,20 +201,22 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
# Debug Adapter Protocol:
# If this attribute exists and is non-empty, the backend must not 'break' (stop)
# but log the message instead. Expressions within {} are interpolated.
Output.debugger_console(interpolate(log_message, elixir_binding))
# If either `hitCondition` or `condition` is specified, then the message
# should only be logged if those conditions are met.
Output.debugger_console(interpolate(log_message, elixir_binding, env))
false
else
result
end
end
end

@spec eval_condition(String.t(), keyword) :: boolean
def eval_condition("true", _binding), do: true
@spec eval_condition(String.t(), keyword, Macro.Env.t()) :: boolean
def eval_condition("true", _binding, _env), do: true

def eval_condition(condition, elixir_binding) do
def eval_condition(condition, elixir_binding, env) do
try do
{term, _bindings} = Code.eval_string(condition, elixir_binding)
{term, _bindings} = Code.eval_string(condition, elixir_binding, env)
if term, do: true, else: false
catch
kind, error ->
Expand All @@ -219,9 +228,29 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
end
end

def eval_string(expression, elixir_binding) do
@spec eval_hit_condition(String.t(), keyword, Macro.Env.t()) :: number
def eval_hit_condition("0", _binding, _env), do: 0

def eval_hit_condition(condition, elixir_binding, env) do
try do
{term, _bindings} = Code.eval_string(condition, elixir_binding, env)

if is_number(term) do
term
else
raise "Hit count evaluated to non number #{inspect(term)}"
end
catch
kind, error ->
Output.debugger_important("Error in hit count: " <> Exception.format_banner(kind, error))

0
end
end

def eval_string(expression, elixir_binding, env) do
try do
{term, _bindings} = Code.eval_string(expression, elixir_binding)
{term, _bindings} = Code.eval_string(expression, elixir_binding, env)
to_string(term)
catch
kind, error ->
Expand All @@ -233,39 +262,39 @@ defmodule ElixirLS.DebugAdapter.BreakpointCondition do
end
end

def interpolate(format_string, elixir_binding) do
interpolate(format_string, [], elixir_binding)
def interpolate(format_string, elixir_binding, env) do
interpolate(format_string, [], elixir_binding, env)
|> Enum.reverse()
|> IO.iodata_to_binary()
end

def interpolate(<<>>, acc, _elixir_binding), do: acc
def interpolate(<<>>, acc, _elixir_binding, _env), do: acc

def interpolate(<<"\\{", rest::binary>>, acc, elixir_binding),
do: interpolate(rest, ["{" | acc], elixir_binding)
def interpolate(<<"\\{", rest::binary>>, acc, elixir_binding, env),
do: interpolate(rest, ["{" | acc], elixir_binding, env)

def interpolate(<<"\\}", rest::binary>>, acc, elixir_binding),
do: interpolate(rest, ["}" | acc], elixir_binding)
def interpolate(<<"\\}", rest::binary>>, acc, elixir_binding, env),
do: interpolate(rest, ["}" | acc], elixir_binding, env)

def interpolate(<<"{", rest::binary>>, acc, elixir_binding) do
def interpolate(<<"{", rest::binary>>, acc, elixir_binding, env) do
case parse_expression(rest, []) do
{:ok, expression_iolist, expression_rest} ->
expression =
expression_iolist
|> Enum.reverse()
|> IO.iodata_to_binary()

eval_result = eval_string(expression, elixir_binding)
interpolate(expression_rest, [eval_result | acc], elixir_binding)
eval_result = eval_string(expression, elixir_binding, env)
interpolate(expression_rest, [eval_result | acc], elixir_binding, env)

:error ->
Output.debugger_important("Log message has unpaired or nested `{}`")
acc
end
end

def interpolate(<<char::binary-size(1), rest::binary>>, acc, elixir_binding),
do: interpolate(rest, [char | acc], elixir_binding)
def interpolate(<<char::binary-size(1), rest::binary>>, acc, elixir_binding, env),
do: interpolate(rest, [char | acc], elixir_binding, env)

def parse_expression(<<>>, _acc), do: :error
def parse_expression(<<"\\{", rest::binary>>, acc), do: parse_expression(rest, ["{" | acc])
Expand Down
137 changes: 137 additions & 0 deletions apps/debug_adapter/lib/debug_adapter/code.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
defmodule ElixirLS.DebugAdapter.Code do
if Version.match?(System.version(), ">= 1.14.0-dev") do
defdelegate env_for_eval(env), to: Code
else
def env_for_eval(%{lexical_tracker: pid} = env) do
new_env = %{
env
| context: nil,
context_modules: [],
macro_aliases: [],
versioned_vars: %{}
}

if is_pid(pid) do
if Process.alive?(pid) do
new_env
else
IO.warn("""
an __ENV__ with outdated compilation information was given to eval, \
call Macro.Env.prune_compile_info/1 to prune it
""")

%{new_env | lexical_tracker: nil, tracers: []}
end
else
%{new_env | tracers: []}
end
end

def env_for_eval(opts) when is_list(opts) do
env = elixir_env.new()

line =
case Keyword.get(opts, :line) do
line_opt when is_integer(line_opt) -> line_opt
nil -> Map.get(env, :line)
end

file =
case Keyword.get(opts, :file) do
file_opt when is_binary(file_opt) -> file_opt
nil -> Map.get(env, :file)
end

module =
case Keyword.get(opts, :module) do
module_opt when is_atom(module_opt) -> module_opt
nil -> nil
end

fa =
case Keyword.get(opts, :function) do
{function, arity} when is_atom(function) and is_integer(arity) -> {function, arity}
nil -> nil
end

temp_tracers =
case Keyword.get(opts, :tracers) do
tracers_opt when is_list(tracers_opt) -> tracers_opt
nil -> []
end

aliases =
case Keyword.get(opts, :aliases) do
aliases_opt when is_list(aliases_opt) ->
IO.warn(":aliases option in eval is deprecated")
aliases_opt

nil ->
Map.get(env, :aliases)
end

requires =
case Keyword.get(opts, :requires) do
requires_opt when is_list(requires_opt) ->
IO.warn(":requires option in eval is deprecated")
MapSet.new(requires_opt)

nil ->
Map.get(env, :requires)
end

functions =
case Keyword.get(opts, :functions) do
functions_opt when is_list(functions_opt) ->
IO.warn(":functions option in eval is deprecated")
functions_opt

nil ->
Map.get(env, :functions)
end

macros =
case Keyword.get(opts, :macros) do
macros_opt when is_list(macros_opt) ->
IO.warn(":macros option in eval is deprecated")
macros_opt

nil ->
Map.get(env, :macros)
end

{lexical_tracker, tracers} =
case Keyword.get(opts, :lexical_tracker) do
pid when is_pid(pid) ->
IO.warn(":lexical_tracker option in eval is deprecated")

if Process.alive?(pid) do
{pid, temp_tracers}
else
{nil, []}
end

nil ->
IO.warn(":lexical_tracker option in eval is deprecated")
{nil, []}

_ ->
{nil, temp_tracers}
end

%{
env
| file: file,
module: module,
function: fa,
tracers: tracers,
macros: macros,
functions: functions,
lexical_tracker: lexical_tracker,
requires: requires,
aliases: aliases,
line: line
}
end
end
end
Loading
Loading