228 lines
9.0 KiB
Erlang
228 lines
9.0 KiB
Erlang
-module(elixir_object).
|
|
-export([build/1, scope_for/2, transform/6, compile/7]).
|
|
-include("elixir.hrl").
|
|
|
|
%% EXTERNAL API
|
|
|
|
% Build an object from the given name. Used by elixir_constants to
|
|
% build objects from their compiled modules. Assumes the given name
|
|
% exists.
|
|
build(Name) ->
|
|
Module = Name:module_info(attributes),
|
|
Mixins = proplists:get_value(mixins, Module),
|
|
Protos = proplists:get_value(protos, Module),
|
|
Parent = case proplists:get_value(parent, Module) of
|
|
[] -> [];
|
|
Else -> hd(Else)
|
|
end,
|
|
#elixir_object__{name=Name, parent=Parent, mixins=Mixins, protos=Protos}.
|
|
|
|
%% TEMPLATE BUILDING FOR MODULE COMPILATION
|
|
|
|
% Build a template of an object or module used on compilation.
|
|
% TODO Copy data from parent
|
|
build_template(Kind, Name, Template) ->
|
|
Parent = default_parent(Kind, Name),
|
|
Mixins = default_mixins(Kind, Name, Template),
|
|
Protos = default_protos(Kind, Name, Template),
|
|
Data = default_data(Template),
|
|
|
|
AttributeTable = ?ELIXIR_ATOM_CONCAT([aex_, Name]),
|
|
ets:new(AttributeTable, [set, named_table, private]),
|
|
|
|
ets:insert(AttributeTable, { mixins, Mixins }),
|
|
ets:insert(AttributeTable, { protos, Protos }),
|
|
ets:insert(AttributeTable, { data, Data }),
|
|
|
|
Object = #elixir_object__{name=Name, parent=Parent, mixins=[], protos=[], data=AttributeTable},
|
|
{ Object, AttributeTable }.
|
|
|
|
% Returns the parent object based on the declaration.
|
|
default_parent(_, 'Object') -> [];
|
|
default_parent(object, _Name) -> 'Object';
|
|
default_parent(module, _Name) -> 'Module'.
|
|
|
|
% Default mixins based on the declaration type.
|
|
default_mixins(_, 'Object', _Template) -> ['Object::Methods']; % object Object
|
|
default_mixins(_, 'Module', _Template) -> ['Module::Methods']; % object Module
|
|
default_mixins(object, _Name, []) -> ['Module::Methods']; % object Post
|
|
default_mixins(module, Name, _Template) -> [Name]; % module Numeric
|
|
default_mixins(object, _Name, Template) -> % object SimplePost from Post
|
|
Template#elixir_object__.mixins ++ ['Module::Methods'].
|
|
|
|
% Default prototypes. Modules have themselves as the default prototype.
|
|
default_protos(_, 'Object', _Template) -> ['Object::Methods']; % object Object
|
|
default_protos(_, 'Module', _Template) -> ['Module::Methods']; % object Module
|
|
default_protos(object, _Name, []) -> []; % object Post
|
|
default_protos(module, Name, _Template) -> [Name]; % module Numeric
|
|
default_protos(object, _Name, Template) -> Template#elixir_object__.protos. % object SimplePost from Post
|
|
|
|
% Returns the default data from parents.
|
|
default_data([]) -> orddict:new();
|
|
default_data(Template) -> Template#elixir_object__.data.
|
|
|
|
%% USED ON TRANSFORMATION AND MODULE COMPILATION
|
|
|
|
% Returns the new module name based on the previous scope.
|
|
scope_for([], Name) -> Name;
|
|
scope_for(Scope, Name) -> ?ELIXIR_ATOM_CONCAT([Scope, '::', Name]).
|
|
|
|
% Generates module transform. It wraps the module definition into
|
|
% a function that will be invoked by compile/5 passing self as argument.
|
|
% We need to wrap them into anonymous functions so nested module
|
|
% definitions have the variable self shadowed.
|
|
transform(Kind, Line, Filename, Name, Parent, Body) ->
|
|
Clause = { clause, Line, [{var, Line, self}], [], Body },
|
|
Fun = { 'fun', Line, { clauses, [Clause] } },
|
|
Args = [{atom, Line, Kind}, {integer, Line, Line}, {string, Line, Filename},
|
|
{var, Line, self}, {atom, Line, Name}, {atom, Line, Parent}, Fun],
|
|
?ELIXIR_WRAP_CALL(Line, ?MODULE, compile, Args).
|
|
|
|
% Initial step of template compilation. Generate a method
|
|
% table and pass it forward to the next compile method.
|
|
compile(Kind, Line, Filename, Current, Name, Template, Fun) ->
|
|
MethodTable = elixir_methods:new_method_table(Name),
|
|
|
|
try
|
|
compile(Kind, Line, Filename, Current, Name, Template, Fun, MethodTable)
|
|
after
|
|
ets:delete(MethodTable)
|
|
end.
|
|
|
|
% Receive the module function to be invoked, invoke it passing
|
|
% self and then compile the added methods into an Erlang module
|
|
% and loaded it in the VM.
|
|
compile(Kind, Line, Filename, Current, Name, Template, Fun, MethodTable) ->
|
|
{ Object, AttributeTable } = build_template(Kind, Name, Template),
|
|
|
|
try
|
|
Result = Fun(Object),
|
|
compile_kind(Kind, Line, Filename, Current, Object, MethodTable),
|
|
Result
|
|
after
|
|
ets:delete(AttributeTable)
|
|
end.
|
|
|
|
% Handle compilation logic specific to objects or modules.
|
|
% TODO Allow object reopening but without method definition.
|
|
% TODO Do not allow module reopening.
|
|
compile_kind(module, Line, Filename, Current, Object, MethodTable) ->
|
|
Name = Object#elixir_object__.name,
|
|
{ Callbacks, Functions } = elixir_methods:unwrap_stored_methods(MethodTable),
|
|
Behavior = elixir_module_methods:behavior(Object),
|
|
compile_callbacks(Behavior, Line, Filename, Object, Callbacks),
|
|
load_form(build_erlang_form(Line, Object, Functions), Filename),
|
|
add_implicit_mixins(Current, Name);
|
|
|
|
% Compile an object. If methods were defined in the object scope,
|
|
% we create a Proto module and automatically include it.
|
|
compile_kind(_Kind, Line, Filename, Current, Object, MethodTable) ->
|
|
case elixir_methods:is_empty_table(MethodTable) of
|
|
true -> [];
|
|
false ->
|
|
Name = ?ELIXIR_ATOM_CONCAT([Object#elixir_object__.name, '::', 'Proto']),
|
|
compile(module, Line, Filename, Object, Name, [], fun(X) -> [] end, MethodTable)
|
|
end,
|
|
load_form(build_erlang_form(Line, Object), Filename).
|
|
|
|
% Handle callbacks compilation.
|
|
compile_callbacks([], _Line, _Filename, _Object, _Callbacks) -> [];
|
|
compile_callbacks(Behavior, Line, Filename, Object, Callbacks) ->
|
|
elixir_module_methods:define_erlang_attribute(Object, behavior, elixir_callbacks),
|
|
Form = elixir_callbacks:build_module_form(Line, Object#elixir_object__.name, Behavior, Callbacks),
|
|
load_form(Form, Filename).
|
|
|
|
% Check if the module currently defined is inside an object
|
|
% definition an automatically include it.
|
|
add_implicit_mixins(#elixir_object__{name=Name} = Self, ModuleName) ->
|
|
Proto = lists:concat([Name, '::', 'Proto']),
|
|
Mixin = lists:concat([Name, '::', 'Mixin']),
|
|
case atom_to_list(ModuleName) of
|
|
Proto -> elixir_object_methods:proto(Self, build(ModuleName));
|
|
Mixin -> elixir_object_methods:mixin(Self, build(ModuleName));
|
|
Else -> []
|
|
end;
|
|
|
|
add_implicit_mixins(_, _) -> [].
|
|
|
|
% Retrieve all attributes in the attribute table and generate
|
|
% an Erlang Abstract Form that defines an Erlang module.
|
|
build_erlang_form(Line, Object) ->
|
|
build_erlang_form(Line, Object, {[],[],[]}).
|
|
|
|
build_erlang_form(Line, Object, {Export, Protected, Functions}) ->
|
|
Name = Object#elixir_object__.name,
|
|
Parent = Object#elixir_object__.parent,
|
|
AttrTable = Object#elixir_object__.data,
|
|
Transform = fun(X, Acc) -> [transform_attribute(Line, X)|Acc] end,
|
|
Base = ets:foldr(Transform, Functions, AttrTable),
|
|
[{attribute, Line, module, Name}, {attribute, Line, parent, Parent}, {attribute, Line, compile, no_auto_import()},
|
|
{attribute, Line, export, Export}, {attribute, Line, protected, Protected} | Base].
|
|
|
|
no_auto_import() ->
|
|
{no_auto_import, [
|
|
{size, 1}, {length, 1}, {error, 1}, {throw, 1}, {exit, 1}
|
|
]}.
|
|
|
|
transform_attribute(Line, {mixins, List}) ->
|
|
{attribute, Line, mixins, lists:delete('Module::Methods', List)};
|
|
|
|
transform_attribute(Line, X) ->
|
|
{attribute, Line, element(1, X), element(2, X)}.
|
|
|
|
% Compile and load given forms as an Erlang module.
|
|
load_form(Forms, Filename) ->
|
|
case compile:forms(Forms, [return]) of
|
|
{ok, ModuleName, Binary, Warnings} ->
|
|
format_warnings(Filename, Warnings),
|
|
code:load_binary(ModuleName, Filename, Binary);
|
|
{error, Errors, Warnings} ->
|
|
format_warnings(Filename, Warnings),
|
|
format_errors(Filename, Errors)
|
|
end.
|
|
|
|
format_errors(Filename, []) ->
|
|
elixir_errors:raise(bad, "compilation failed but no error was raised");
|
|
|
|
format_errors(Filename, Errors) ->
|
|
lists:foreach(fun ({_, Each}) ->
|
|
lists:foreach(fun (Error) -> format_error(Filename, Error) end, Each)
|
|
end, Errors).
|
|
|
|
format_warnings(Filename, Warnings) ->
|
|
lists:foreach(fun ({_, Each}) ->
|
|
lists:foreach(fun (Warning) -> format_warning(Filename, Warning) end, Each)
|
|
end, Warnings).
|
|
|
|
|
|
% Handle warnings
|
|
|
|
format_warning(Filename, {Line,_,{unused_var,self}}) ->
|
|
[];
|
|
|
|
format_warning(Filename, {Line,_,nomatch_clause_type}) ->
|
|
[];
|
|
|
|
format_warning(Filename, {Line, _, {unused_function, {Name, Arity}}}) ->
|
|
Message = io_lib:format("unused local method ~s/~w\n", [Name, Arity-1]),
|
|
io:format(elixir_errors:file_format(Line, Filename, Message));
|
|
|
|
format_warning(Filename, {Line,Module,Desc}) ->
|
|
Message = Module:format_error(Desc),
|
|
io:format(elixir_errors:file_format(Line, Filename, Message) ++ [$\n]).
|
|
|
|
% Handle errors
|
|
|
|
format_error(Filename, {Line,Module,{undefined_function,{Name, Arity}}}) ->
|
|
Message = io_lib:format("undefined local method ~s/~w", [Name, Arity-1]),
|
|
elixir_errors:file_error(undefined_local_method, Line, Filename, Message);
|
|
|
|
format_error(Filename, {Line,Module,Desc}) ->
|
|
Message = Module:format_error(Desc),
|
|
elixir_errors:file_error(reason_from_desc(Desc), Line, Filename, Message).
|
|
|
|
reason_from_desc(Desc) when is_tuple(Desc) ->
|
|
element(1, Desc);
|
|
|
|
reason_from_desc(Desc) ->
|
|
Desc. |