-
Notifications
You must be signed in to change notification settings - Fork 3.3k
/
evaluator.ex
235 lines (196 loc) · 6.93 KB
/
evaluator.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
defmodule IEx.Evaluator do
@moduledoc false
@doc """
Eval loop for an IEx session. Its responsibilities include:
* loading of .iex files
* evaluating code
* trapping exceptions in the code being evaluated
* keeping expression history
"""
def init(command, server, leader, opts) do
old_leader = Process.group_leader
Process.group_leader(self(), leader)
state = loop_state(opts)
command == :ack && :proc_lib.init_ack(self())
try do
loop(server, IEx.History.init, state)
after
Process.group_leader(self(), old_leader)
end
end
defp loop(server, history, state) do
receive do
{:eval, ^server, code, iex_state} ->
{result, history, state} = eval(code, iex_state, history, state)
send server, {:evaled, self(), result}
loop(server, history, state)
{:peek_env, receiver} ->
send receiver, {:peek_env, state.env}
loop(server, history, state)
{:done, ^server} ->
:ok
end
end
defp loop_state(opts) do
env =
if env = opts[:env] do
:elixir.env_for_eval(env, [])
else
:elixir.env_for_eval(file: "iex")
end
{_, _, env, scope} = :elixir.eval('import IEx.Helpers', [], env)
binding = Keyword.get(opts, :binding, [])
state = %{binding: binding, scope: scope, env: env}
case opts[:dot_iex_path] do
"" -> state
path -> load_dot_iex(state, path)
end
end
defp load_dot_iex(state, path) do
candidates = if path do
[path]
else
Enum.map [".iex.exs", "~/.iex.exs"], &Path.expand/1
end
path = Enum.find candidates, &File.regular?/1
if is_nil(path) do
state
else
eval_dot_iex(state, path)
end
end
defp eval_dot_iex(state, path) do
try do
code = File.read!(path)
env = :elixir.env_for_eval(state.env, file: path, line: 1)
# Evaluate the contents in the same environment server_loop will run in
{_result, binding, env, _scope} =
:elixir.eval(String.to_charlist(code), state.binding, env)
%{state | binding: binding, env: :elixir.env_for_eval(env, file: "iex", line: 1)}
catch
kind, error ->
io_result "Error while evaluating: #{path}"
print_error(kind, error, System.stacktrace)
System.halt(1)
end
end
# Instead of doing just :elixir.eval, we first parse the expression to see
# if it's well formed. If parsing succeeds, we evaluate the AST as usual.
#
# If parsing fails, this might be a TokenMissingError which we treat in
# a special way (to allow for continuation of an expression on the next
# line in IEx). In case of any other error, we let :elixir_translator
# to re-raise it.
#
# Returns updated state.
#
# The first two clauses provide support for the break-trigger allowing to
# break out from a pending incomplete expression. See
# https://github.com/elixir-lang/elixir/issues/1089 for discussion.
@break_trigger '#iex:break\n'
defp eval(code, iex_state, history, state) do
try do
do_eval(String.to_charlist(code), iex_state, history, state)
catch
kind, error ->
print_error(kind, error, System.stacktrace)
{%{iex_state | cache: ''}, history, state}
end
end
defp do_eval(@break_trigger, %IEx.State{cache: ''} = iex_state, history, state) do
{iex_state, history, state}
end
defp do_eval(@break_trigger, iex_state, _history, _state) do
:elixir_errors.parse_error(iex_state.counter, "iex", "incomplete expression", "")
end
defp do_eval(latest_input, iex_state, history, state) do
code = iex_state.cache ++ latest_input
line = iex_state.counter
Process.put(:iex_history, history)
handle_eval(Code.string_to_quoted(code, [line: line, file: "iex"]), code, line, iex_state, history, state)
after
Process.delete(:iex_history)
end
defp handle_eval({:ok, forms}, code, line, iex_state, history, state) do
{result, binding, env, scope} =
:elixir.eval_forms(forms, state.binding, state.env, state.scope)
unless result == IEx.dont_display_result, do: io_inspect(result)
iex_state =
%{iex_state | cache: '',
counter: iex_state.counter + 1}
state =
%{state | env: env,
scope: scope,
binding: binding}
{iex_state, update_history(history, line, code, result), state}
end
defp handle_eval({:error, {_, _, ""}}, code, _line, iex_state, history, state) do
# Update iex_state.cache so that IEx continues to add new input to
# the unfinished expression in "code"
{%{iex_state | cache: code}, history, state}
end
defp handle_eval({:error, {line, error, token}}, _code, _line, _iex_state, _, _state) do
# Encountered malformed expression
:elixir_errors.parse_error(line, "iex", error, token)
end
defp update_history(history, counter, cache, result) do
IEx.History.append(history, {counter, cache, result}, IEx.Config.history_size)
end
defp io_inspect(result) do
io_result inspect(result, IEx.inspect_opts)
end
defp io_result(result) do
IO.puts :stdio, IEx.color(:eval_result, result)
end
defp io_error(result) do
IO.puts :stdio, IEx.color(:eval_error, result)
end
## Error handling
defp print_error(kind, reason, stacktrace) do
Exception.format_banner(kind, reason, stacktrace) |> io_error
stacktrace |> prune_stacktrace |> format_stacktrace |> io_error
end
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
:elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
:elixir_def, :elixir_map]
defp prune_stacktrace(stacktrace) do
# The order in which each drop_while is listed is important.
# For example, the user my call Code.eval_string/2 in IEx
# and if there is an error we should not remove erl_eval
# and eval_bits information from the user stacktrace.
stacktrace
|> Enum.reverse()
|> Enum.drop_while(&(elem(&1, 0) == :proc_lib))
|> Enum.drop_while(&(elem(&1, 0) == __MODULE__))
|> Enum.drop_while(&(elem(&1, 0) == :elixir))
|> Enum.drop_while(&(elem(&1, 0) in [:erl_eval, :eval_bits]))
|> Enum.reverse()
|> Enum.reject(&(elem(&1, 0) in @elixir_internals))
end
@doc false
def format_stacktrace(trace) do
entries =
for entry <- trace do
split_entry(Exception.format_stacktrace_entry(entry))
end
width = Enum.reduce entries, 0, fn {app, _}, acc ->
max(String.length(app), acc)
end
" " <> Enum.map_join(entries, "\n ", &format_entry(&1, width))
end
defp split_entry(entry) do
case entry do
"(" <> _ ->
case :binary.split(entry, ") ") do
[left, right] -> {left <> ") ", right}
_ -> {"", entry}
end
_ ->
{"", entry}
end
end
defp format_entry({app, info}, width) do
app = String.pad_leading(app, width)
IEx.color(:stack_app, app) <> IEx.color(:stack_info, info)
end
end