diff --git a/lib/livebook/runtime/evaluator.ex b/lib/livebook/runtime/evaluator.ex index b1c9ccb23..a5e9a4c10 100644 --- a/lib/livebook/runtime/evaluator.ex +++ b/lib/livebook/runtime/evaluator.ex @@ -808,17 +808,47 @@ defmodule Livebook.Runtime.Evaluator do defp elixir_to_erlang_var(name) do name |> :erlang.atom_to_binary() - |> Macro.camelize() + |> toggle_var_case() |> :erlang.binary_to_atom() end defp erlang_to_elixir_var(name) do name |> :erlang.atom_to_binary() - |> Macro.underscore() + |> toggle_var_case() |> :erlang.binary_to_atom() end + # Unambiguously maps variable names from camel case to underscore + # case, and vice-versa. The mapping is defined as follows: + # + # 1. The first character case is changed + # + # 2. Underscore followed by lower character maps to upper character, + # and vice-versa + # + defp toggle_var_case(<>) do + do_toggle_var_case(<>, t) + end + + defp do_toggle_var_case(acc, <>) when h >= ?a and h <= ?z do + do_toggle_var_case(<>, t) + end + + defp do_toggle_var_case(acc, <>) when h >= ?A and h <= ?Z do + do_toggle_var_case(<>, t) + end + + defp do_toggle_var_case(acc, <>) do + do_toggle_var_case(<>, t) + end + + defp do_toggle_var_case(acc, <<>>), do: acc + + defp toggle_char_case(char) when char >= ?a and char <= ?z, do: char - 32 + defp toggle_char_case(char) when char >= ?A and char <= ?Z, do: char + 32 + defp toggle_char_case(char), do: char + defp filter_erlang_code_markers(code_markers) do Enum.reject(code_markers, &(&1.line == 0)) end diff --git a/test/livebook/runtime/evaluator_test.exs b/test/livebook/runtime/evaluator_test.exs index d9def1dba..f2c16a143 100644 --- a/test/livebook/runtime/evaluator_test.exs +++ b/test/livebook/runtime/evaluator_test.exs @@ -1268,6 +1268,48 @@ defmodule Livebook.Runtime.EvaluatorTest do assert [{:z, 1}, {:y, 1}, {:x, 1}] == binding end + test "uses unambiguous camelization for erlang/elixir bindings", %{evaluator: evaluator} do + Evaluator.evaluate_code(evaluator, :erlang, "{JSON, JsOn, JsON} = {1, 2, 3}.", :code_1, []) + + assert_receive {:runtime_evaluation_response, :code_1, terminal_text(_), metadata()} + + Evaluator.evaluate_code( + evaluator, + :elixir, + """ + assertion1 = {j_s_o_n, js_on, js_o_n} == {1, 2, 3} + {j_s_o_n, js_on, js_o_n} = {11, 12, 13} + """, + :code_2, + [:code_1] + ) + + assert_receive {:runtime_evaluation_response, :code_2, terminal_text(_), metadata()} + + Evaluator.evaluate_code( + evaluator, + :erlang, + """ + Assertion2 = {JSON, JsOn, JsON} =:= {11, 12, 13}. + """, + :code_3, + [:code_2] + ) + + assert_receive {:runtime_evaluation_response, :code_3, terminal_text(_), metadata()} + + %{binding: binding} = + Evaluator.get_evaluation_context(evaluator, [:code_3, :code_2, :code_1]) + + assert [ + {:assertion2, true}, + {:js_on, 12}, + {:js_o_n, 13}, + {:j_s_o_n, 11}, + {:assertion1, true} + ] == binding + end + test "inspects erlang results using erlang format", %{evaluator: evaluator} do code = ~S"#{x=>1}." Evaluator.evaluate_code(evaluator, :erlang, code, :code_1, [], file: "file.ex")