-module(elixir_exp). -export([expand/2, expand_args/2, expand_arg/2]). -import(elixir_errors, [compile_error/3, compile_error/4]). -include("elixir.hrl"). %% = expand({'=', Meta, [Left, Right]}, E) -> assert_no_guard_scope(Meta, '=', E), {ERight, ER} = expand(Right, E), {ELeft, EL} = elixir_exp_clauses:match(fun expand/2, Left, E), {{'=', Meta, [ELeft, ERight]}, elixir_env:mergev(EL, ER)}; %% Literal operators expand({'{}', Meta, Args}, E) -> {EArgs, EA} = expand_args(Args, E), {{'{}', Meta, EArgs}, EA}; expand({'%{}', Meta, Args}, E) -> elixir_map:expand_map(Meta, Args, E); expand({'%', Meta, [Left, Right]}, E) -> elixir_map:expand_struct(Meta, Left, Right, E); expand({'<<>>', Meta, Args}, E) -> elixir_bitstring:expand(Meta, Args, E); %% Other operators expand({'__op__', Meta, [_, _] = Args}, E) -> {EArgs, EA} = expand_args(Args, E), {{'__op__', Meta, EArgs}, EA}; expand({'__op__', Meta, [_, _, _] = Args}, E) -> {EArgs, EA} = expand_args(Args, E), {{'__op__', Meta, EArgs}, EA}; expand({'->', Meta, _Args}, E) -> compile_error(Meta, ?m(E, file), "unhandled operator ->"); %% __block__ expand({'__block__', _Meta, []}, E) -> {nil, E}; expand({'__block__', _Meta, [Arg]}, E) -> expand(Arg, E); expand({'__block__', Meta, Args}, E) when is_list(Args) -> {EArgs, EA} = expand_many(Args, E), {{'__block__', Meta, EArgs}, EA}; %% __aliases__ expand({'__aliases__', _, _} = Alias, E) -> case elixir_aliases:expand(Alias, ?m(E, aliases), ?m(E, macro_aliases), ?m(E, lexical_tracker)) of Receiver when is_atom(Receiver) -> elixir_lexical:record_remote(Receiver, ?m(E, lexical_tracker)), {Receiver, E}; Aliases -> {EAliases, EA} = expand_args(Aliases, E), case lists:all(fun is_atom/1, EAliases) of true -> Receiver = elixir_aliases:concat(EAliases), elixir_lexical:record_remote(Receiver, ?m(E, lexical_tracker)), {Receiver, EA}; false -> {{{'.', [], [elixir_aliases, concat]}, [], [EAliases]}, EA} end end; %% alias expand({alias, Meta, [Ref]}, E) -> expand({alias, Meta, [Ref,[]]}, E); expand({alias, Meta, [Ref, KV]}, E) -> assert_no_match_or_guard_scope(Meta, alias, E), {ERef, ER} = expand(Ref, E), {EKV, ET} = expand_opts(Meta, alias, [as, warn], no_alias_opts(KV), ER), if is_atom(ERef) -> {{alias, Meta, [ERef, EKV]}, expand_alias(Meta, true, ERef, EKV, ET)}; true -> compile_error(Meta, ?m(E, file), "invalid argument for alias, expected a compile time atom or alias, got: ~ts", ['Elixir.Kernel':inspect(ERef)]) end; expand({require, Meta, [Ref]}, E) -> expand({require, Meta, [Ref, []]}, E); expand({require, Meta, [Ref, KV]}, E) -> assert_no_match_or_guard_scope(Meta, require, E), {ERef, ER} = expand(Ref, E), {EKV, ET} = expand_opts(Meta, require, [as, warn], no_alias_opts(KV), ER), if is_atom(ERef) -> elixir_aliases:ensure_loaded(Meta, ERef, ET), {{require, Meta, [ERef, EKV]}, expand_require(Meta, ERef, EKV, ET)}; true -> compile_error(Meta, ?m(E, file), "invalid argument for require, expected a compile time atom or alias, got: ~ts", ['Elixir.Kernel':inspect(ERef)]) end; expand({import, Meta, [Left]}, E) -> expand({import, Meta, [Left, []]}, E); expand({import, Meta, [Ref, KV]}, E) -> assert_no_match_or_guard_scope(Meta, import, E), {ERef, ER} = expand(Ref, E), {EKV, ET} = expand_opts(Meta, import, [only, except, warn], KV, ER), if is_atom(ERef) -> elixir_aliases:ensure_loaded(Meta, ERef, ET), {Functions, Macros} = elixir_import:import(Meta, ERef, EKV, ET), {{import, Meta, [ERef, EKV]}, expand_require(Meta, ERef, EKV, ET#{functions := Functions, macros := Macros})}; true -> compile_error(Meta, ?m(E, file), "invalid argument for import, expected a compile time atom or alias, got: ~ts", ['Elixir.Kernel':inspect(ERef)]) end; %% Pseudo vars expand({'__MODULE__', _, Atom}, E) when is_atom(Atom) -> {?m(E, module), E}; expand({'__DIR__', _, Atom}, E) when is_atom(Atom) -> {filename:dirname(?m(E, file)), E}; expand({'__CALLER__', _, Atom} = Caller, E) when is_atom(Atom) -> {Caller, E}; expand({'__ENV__', Meta, Atom}, E) when is_atom(Atom) -> Env = elixir_env:linify({?line(Meta), E}), {{'%{}', [], maps:to_list(Env)}, E}; expand({{'.', DotMeta, [{'__ENV__', Meta, Atom}, Field]}, CallMeta, []}, E) when is_atom(Atom), is_atom(Field) -> Env = elixir_env:linify({?line(Meta), E}), case maps:is_key(Field, Env) of true -> {maps:get(Field, Env), E}; false -> {{{'.', DotMeta, [{'%{}', [], maps:to_list(Env)}, Field]}, CallMeta, []}, E} end; %% Quote expand({Unquote, Meta, [_]}, E) when Unquote == unquote; Unquote == unquote_splicing -> compile_error(Meta, ?m(E, file), "~p called outside quote", [Unquote]); expand({quote, Meta, [Opts]}, E) when is_list(Opts) -> case lists:keyfind(do, 1, Opts) of {do, Do} -> expand({quote, Meta, [lists:keydelete(do, 1, Opts), [{do,Do}]]}, E); false -> compile_error(Meta, ?m(E, file), "missing do keyword in quote") end; expand({quote, Meta, [_]}, E) -> compile_error(Meta, ?m(E, file), "invalid arguments for quote"); expand({quote, Meta, [KV, Do]}, E) when is_list(Do) -> Exprs = case lists:keyfind(do, 1, Do) of {do, Expr} -> Expr; false -> compile_error(Meta, E#elixir_scope.file, "missing do keyword in quote") end, ValidOpts = [context, location, line, file, unquote, bind_quoted], {EKV, ET} = expand_opts(Meta, quote, ValidOpts, KV, E), Context = case lists:keyfind(context, 1, EKV) of {context, Ctx} when is_atom(Ctx) and (Ctx /= nil) -> Ctx; {context, Ctx} -> compile_error(Meta, ?m(E, file), "invalid :context for quote, " "expected non nil compile time atom or alias, got: ~ts", ['Elixir.Kernel':inspect(Ctx)]); false -> case ?m(E, module) of nil -> 'Elixir'; Mod -> Mod end end, {File, Line} = case lists:keyfind(location, 1, EKV) of {location, keep} -> {elixir_utils:relative_to_cwd(?m(E, file)), false}; false -> { case lists:keyfind(file, 1, EKV) of {file, F} -> F; false -> nil end, case lists:keyfind(line, 1, EKV) of {line, L} -> L; false -> false end } end, {Binding, DefaultUnquote} = case lists:keyfind(bind_quoted, 1, EKV) of {bind_quoted, BQ} -> {BQ, false}; false -> {nil, true} end, Unquote = case lists:keyfind(unquote, 1, EKV) of {unquote, U} when is_boolean(U) -> U; false -> DefaultUnquote end, Q = #elixir_quote{line=Line, file=File, unquote=Unquote, context=Context}, {Quoted, _Q} = elixir_quote:quote(Exprs, Binding, Q, ET), expand(Quoted, ET); expand({quote, Meta, [_, _]}, E) -> compile_error(Meta, ?m(E, file), "invalid arguments for quote"); %% Functions expand({'&', _, [Arg]} = Original, E) when is_integer(Arg) -> {Original, E}; expand({'&', Meta, [Arg]}, E) -> assert_no_match_or_guard_scope(Meta, '&', E), case elixir_fn:capture(Meta, Arg, E) of {local, Fun, Arity} -> {{'&', Meta, [{'/', [], [{Fun, [], nil}, Arity]}]}, E}; {expanded, Expr, EE} -> expand(Expr, EE) end; expand({fn, Meta, Pairs}, E) -> assert_no_match_or_guard_scope(Meta, fn, E), elixir_fn:expand(Meta, Pairs, E); %% Case/Receive/Try expand({'cond', Meta, [KV]}, E) -> assert_no_match_or_guard_scope(Meta, 'cond', E), {EClauses, EC} = elixir_exp_clauses:'cond'(Meta, KV, E), {{'cond', Meta, [EClauses]}, EC}; expand({'case', Meta, [Expr, KV]}, E) -> assert_no_match_or_guard_scope(Meta, 'case', E), {EExpr, EE} = expand(Expr, E), {EClauses, EC} = elixir_exp_clauses:'case'(Meta, KV, EE), FClauses = case (lists:keyfind(optimize_boolean, 1, Meta) == {optimize_boolean, true}) and elixir_utils:returns_boolean(EExpr) of true -> rewrite_case_clauses(EClauses); false -> EClauses end, {{'case', Meta, [EExpr, FClauses]}, EC}; expand({'receive', Meta, [KV]}, E) -> assert_no_match_or_guard_scope(Meta, 'receive', E), {EClauses, EC} = elixir_exp_clauses:'receive'(Meta, KV, E), {{'receive', Meta, [EClauses]}, EC}; expand({'try', Meta, [KV]}, E) -> assert_no_match_or_guard_scope(Meta, 'try', E), {EClauses, EC} = elixir_exp_clauses:'try'(Meta, KV, E), {{'try', Meta, [EClauses]}, EC}; %% Comprehensions expand({for, Meta, [_|_] = Args}, E) -> elixir_for:expand(Meta, Args, E); %% Super expand({super, Meta, Args}, E) when is_list(Args) -> assert_no_match_or_guard_scope(Meta, super, E), {EArgs, EA} = expand_args(Args, E), {{super, Meta, EArgs}, EA}; %% Vars expand({'^', Meta, [Arg]}, #{context := match} = E) -> case expand(Arg, E) of {{Name, _, Kind} = EArg, EA} when is_atom(Name), is_atom(Kind) -> {{'^', Meta, [EArg]}, EA}; _ -> Msg = "invalid argument for unary operator ^, expected an existing variable, got: ^~ts", compile_error(Meta, ?m(E, file), Msg, ['Elixir.Macro':to_string(Arg)]) end; expand({'^', Meta, [Arg]}, E) -> compile_error(Meta, ?m(E, file), "cannot use ^~ts outside of match clauses", ['Elixir.Macro':to_string(Arg)]); expand({'_', _, Kind} = Var, E) when is_atom(Kind) -> {Var, E}; expand({Name, Meta, Kind} = Var, #{context := match, export_vars := Export} = E) when is_atom(Name), is_atom(Kind) -> Pair = {Name, var_kind(Meta, Kind)}, NewVars = ordsets:add_element(Pair, ?m(E, vars)), NewExport = case (Export /= nil) of true -> ordsets:add_element(Pair, Export); false -> Export end, {Var, E#{vars := NewVars, export_vars := NewExport}}; expand({Name, Meta, Kind} = Var, #{vars := Vars} = E) when is_atom(Name), is_atom(Kind) -> case lists:member({Name, var_kind(Meta, Kind)}, Vars) of true -> {Var, E}; false -> VarMeta = lists:keyfind(var, 1, Meta), if VarMeta == {var, true} -> compile_error(Meta, ?m(E, file), "expected var \"~ts\"~ts to expand to an existing variable " "or be part of a match", [Name, elixir_scope:context_info(Kind)]); true -> expand({Name, Meta, []}, E) end end; %% Local calls expand({Atom, Meta, Args}, E) when is_atom(Atom), is_list(Meta), is_list(Args) -> assert_no_ambiguous_op(Atom, Meta, Args, E), elixir_dispatch:dispatch_import(Meta, Atom, Args, E, fun() -> expand_local(Meta, Atom, Args, E) end); %% Remote calls expand({{'.', DotMeta, [Left, Right]}, Meta, Args}, E) when (is_tuple(Left) orelse is_atom(Left)), is_atom(Right), is_list(Meta), is_list(Args) -> {ELeft, EL} = expand(Left, E), elixir_dispatch:dispatch_require(Meta, ELeft, Right, Args, EL, fun(AR, AF, AA) -> expand_remote(AR, DotMeta, AF, Meta, AA, E, EL) end); %% Anonymous calls expand({{'.', DotMeta, [Expr]}, Meta, Args}, E) when is_list(Args) -> {EExpr, EE} = expand(Expr, E), if is_atom(EExpr) -> compile_error(Meta, ?m(E, file), "invalid function call :~ts.()", [EExpr]); true -> {EArgs, EA} = expand_args(Args, elixir_env:mergea(E, EE)), {{{'.', DotMeta, [EExpr]}, Meta, EArgs}, elixir_env:mergev(EE, EA)} end; %% Invalid calls expand({_, Meta, Args} = Invalid, E) when is_list(Meta) and is_list(Args) -> compile_error(Meta, ?m(E, file), "invalid call ~ts", ['Elixir.Macro':to_string(Invalid)]); expand({_, _, _} = Tuple, E) -> compile_error([{line,0}], ?m(E, file), "invalid quoted expression: ~ts", ['Elixir.Kernel':inspect(Tuple, [])]); %% Literals expand({Left, Right}, E) -> {[ELeft, ERight], EE} = expand_args([Left, Right], E), {{ELeft, ERight}, EE}; expand(List, #{context := match} = E) when is_list(List) -> expand_list(List, fun expand/2, E, []); expand(List, E) when is_list(List) -> {EArgs, {EC, EV}} = expand_list(List, fun expand_arg/2, {E, E}, []), {EArgs, elixir_env:mergea(EV, EC)}; expand(Function, E) when is_function(Function) -> case (erlang:fun_info(Function, type) == {type, external}) andalso (erlang:fun_info(Function, env) == {env, []}) of true -> {Function, E}; false -> compile_error([{line,0}], ?m(E, file), "invalid quoted expression: ~ts", ['Elixir.Kernel':inspect(Function)]) end; expand(Other, E) when is_number(Other); is_atom(Other); is_binary(Other); is_pid(Other) -> {Other, E}; expand(Other, E) -> compile_error([{line,0}], ?m(E, file), "invalid quoted expression: ~ts", ['Elixir.Kernel':inspect(Other)]). %% Helpers expand_list([{'|', Meta, [_, _] = Args}], Fun, Acc, List) -> {EArgs, EAcc} = lists:mapfoldl(Fun, Acc, Args), expand_list([], Fun, EAcc, [{'|', Meta, EArgs}|List]); expand_list([H|T], Fun, Acc, List) -> {EArg, EAcc} = Fun(H, Acc), expand_list(T, Fun, EAcc, [EArg|List]); expand_list([], _Fun, Acc, List) -> {lists:reverse(List), Acc}. expand_many(Args, E) -> lists:mapfoldl(fun expand/2, E, Args). %% Variables in arguments are not propagated from one %% argument to the other. For instance: %% %% x = 1 %% foo(x = x + 2, x) %% x %% %% Should be the same as: %% %% foo(3, 1) %% 3 %% %% However, lexical information is. expand_arg(Arg, Acc) when is_number(Arg); is_atom(Arg); is_binary(Arg); is_pid(Arg) -> {Arg, Acc}; expand_arg(Arg, {Acc1, Acc2}) -> {EArg, EAcc} = expand(Arg, Acc1), {EArg, {elixir_env:mergea(Acc1, EAcc), elixir_env:mergev(Acc2, EAcc)}}. expand_args([Arg], E) -> {EArg, EE} = expand(Arg, E), {[EArg], EE}; expand_args(Args, #{context := match} = E) -> expand_many(Args, E); expand_args(Args, E) -> {EArgs, {EC, EV}} = lists:mapfoldl(fun expand_arg/2, {E, E}, Args), {EArgs, elixir_env:mergea(EV, EC)}. %% Match/var helpers var_kind(Meta, Kind) -> case lists:keyfind(counter, 1, Meta) of {counter, Counter} -> Counter; false -> Kind end. %% Locals assert_no_ambiguous_op(Name, Meta, [Arg], E) -> case lists:keyfind(ambiguous_op, 1, Meta) of {ambiguous_op, Kind} -> case lists:member({Name, Kind}, ?m(E, vars)) of true -> compile_error(Meta, ?m(E, file), "\"~ts ~ts\" looks like a function call but " "there is a variable named \"~ts\", please use explicit parenthesis or even spaces", [Name, 'Elixir.Macro':to_string(Arg), Name]); false -> ok end; _ -> ok end; assert_no_ambiguous_op(_Atom, _Meta, _Args, _E) -> ok. expand_local(Meta, Name, Args, #{local := nil, function := nil} = E) -> compile_error(Meta, ?m(E, file), "undefined function ~ts/~B", [Name, length(Args)]); expand_local(Meta, Name, Args, #{local := nil, module := Module, function := Function} = E) -> elixir_locals:record_local({Name, length(Args)}, Module, Function), {EArgs, EA} = expand_args(Args, E), {{Name, Meta, EArgs}, EA}; expand_local(Meta, Name, Args, E) -> expand({{'.', Meta, [?m(E, local), Name]}, Meta, Args}, E). %% Remote expand_remote(Receiver, DotMeta, Right, Meta, Args, E, EL) -> if is_atom(Receiver) -> elixir_lexical:record_remote(Receiver, ?m(E, lexical_tracker)); true -> ok end, {EArgs, EA} = expand_args(Args, E), {{{'.', DotMeta, [Receiver, Right]}, Meta, EArgs}, elixir_env:mergev(EL, EA)}. %% Lexical helpers expand_opts(Meta, Kind, Allowed, Opts, E) -> {EOpts, EE} = expand(Opts, E), validate_opts(Meta, Kind, Allowed, EOpts, EE), {EOpts, EE}. validate_opts(Meta, Kind, Allowed, Opts, E) when is_list(Opts) -> [begin compile_error(Meta, ?m(E, file), "unsupported option ~ts given to ~s", ['Elixir.Kernel':inspect(Key), Kind]) end || {Key, _} <- Opts, not lists:member(Key, Allowed)]; validate_opts(Meta, Kind, _Allowed, _Opts, S) -> compile_error(Meta, S#elixir_scope.file, "invalid options for ~s, expected a keyword list", [Kind]). no_alias_opts(KV) when is_list(KV) -> case lists:keyfind(as, 1, KV) of {as, As} -> lists:keystore(as, 1, KV, {as, no_alias_expansion(As)}); false -> KV end; no_alias_opts(KV) -> KV. no_alias_expansion({'__aliases__', Meta, [H|T]}) when (H /= 'Elixir') and is_atom(H) -> {'__aliases__', Meta, ['Elixir',H|T]}; no_alias_expansion(Other) -> Other. expand_require(Meta, Ref, KV, E) -> RE = E#{requires := ordsets:add_element(Ref, ?m(E, requires))}, expand_alias(Meta, false, Ref, KV, RE). expand_alias(Meta, IncludeByDefault, Ref, KV, #{context_modules := Context} = E) -> New = expand_as(lists:keyfind(as, 1, KV), Meta, IncludeByDefault, Ref, E), %% Add the alias to context_modules if defined is true. %% This is used by defmodule in order to store the defined %% module in context modules. NewContext = case lists:keyfind(defined, 1, Meta) of {defined, Mod} when is_atom(Mod) -> [Mod|Context]; false -> Context end, {Aliases, MacroAliases} = elixir_aliases:store(Meta, New, Ref, KV, ?m(E, aliases), ?m(E, macro_aliases), ?m(E, lexical_tracker)), E#{aliases := Aliases, macro_aliases := MacroAliases, context_modules := NewContext}. expand_as({as, true}, _Meta, _IncludeByDefault, Ref, _E) -> elixir_aliases:last(Ref); expand_as({as, false}, _Meta, _IncludeByDefault, Ref, _E) -> Ref; expand_as({as, Atom}, Meta, _IncludeByDefault, _Ref, E) when is_atom(Atom) -> case length(string:tokens(atom_to_list(Atom), ".")) of 1 -> compile_error(Meta, ?m(E, file), "invalid value for keyword :as, expected an alias, got atom: ~ts", [elixir_aliases:inspect(Atom)]); 2 -> Atom; _ -> compile_error(Meta, ?m(E, file), "invalid value for keyword :as, expected an alias, got nested alias: ~ts", [elixir_aliases:inspect(Atom)]) end; expand_as(false, _Meta, IncludeByDefault, Ref, _E) -> if IncludeByDefault -> elixir_aliases:last(Ref); true -> Ref end; expand_as({as, Other}, Meta, _IncludeByDefault, _Ref, E) -> compile_error(Meta, ?m(E, file), "invalid value for keyword :as, expected an alias, got: ~ts", ['Elixir.Macro':to_string(Other)]). %% Assertions rewrite_case_clauses([{do,[ {'->', FalseMeta, [ [{'when', _, [Var, {'__op__', _,[ 'orelse', {{'.', _, [erlang, '=:=']}, _, [Var, nil]}, {{'.', _, [erlang, '=:=']}, _, [Var, false]} ]}]}], FalseExpr ]}, {'->', TrueMeta, [ [{'_', _, _}], TrueExpr ]} ]}]) -> [{do, [ {'->', FalseMeta, [[false], FalseExpr]}, {'->', TrueMeta, [[true], TrueExpr]} ]}]; rewrite_case_clauses(Clauses) -> Clauses. assert_no_match_or_guard_scope(Meta, Kind, E) -> assert_no_match_scope(Meta, Kind, E), assert_no_guard_scope(Meta, Kind, E). assert_no_match_scope(Meta, _Kind, #{context := match, file := File}) -> compile_error(Meta, File, "invalid expression in match"); assert_no_match_scope(_Meta, _Kind, _E) -> []. assert_no_guard_scope(Meta, _Kind, #{context := guard, file := File}) -> compile_error(Meta, File, "invalid expression in guard"); assert_no_guard_scope(_Meta, _Kind, _E) -> [].