% Copyright (C) 2014-2019 Olivier Boudeville % % This file is part of the Ceylan-Myriad library. % % This library is free software: you can redistribute it and/or modify % it under the terms of the GNU Lesser General Public License or % the GNU General Public License, as they are published by the Free Software % Foundation, either version 3 of these Licenses, or (at your option) % any later version. % You can also redistribute it and/or modify it under the terms of the % Mozilla Public License, version 1.1 or later. % % This library is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU Lesser General Public License and the GNU General Public License % for more details. % % You should have received a copy of the GNU Lesser General Public % License, of the GNU General Public License and of the Mozilla Public License % along with this library. % If not, see and % . % % Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Creation date: Friday, December 19, 2014. % Gathering of various higher-level, convenient meta-related facilities, notably % regarding metaprogramming, types and parse transforms. % % See meta_utils_test.erl for the corresponding test, and ast_info.erl for the % more basic services used by this module. % % Note that this module is a prerequisite of at least most of our parse % transforms, hence it must be bootstrapped *before* they are built, and cannot % use them. % % So, to compile it, just go to the root of this layer and execute for example % 'make all'. % % To determine the other bootstrapped modules (i.e. the subset of our modules % that this module can use), see the BOOTSTRAP_MODULES variable in % GNUmakevars.inc. % See also: the type_utils module, about the management of datatypes themselves, % and the ast* modules for lower-level operations. % -module(meta_utils). % For table macro, etc.: -include("meta_utils.hrl"). % For *_info records: -include("ast_info.hrl"). % For ast_transforms record: -include("ast_transform.hrl"). % Key implementation notes: % % - again: any exported function meant to be used by parse transforms shall rely % exclusively (through all its code paths) on bootstrapped modules, as listed in % the BOOTSTRAP_MODULES variable of GNUmakevars.inc % % - see type_utils about how to handle datatypes % Implementation notes about parse transforms: % Here are some resources to better understand parse transforms (PT, here): % % - generic information about PT: in http://www.erlang-factory.com/ : % upload/presentations/521/yrashk_parse_transformations_sf12.pdf % % - Abstract Format: http://www.erlang.org/doc/apps/erts/absform.html (full spec % of the AST format) % % - http://chlorophil.blogspot.fr/2007/04/erlang-macro-processor-v1-part-i.html % http://chlorophil.blogspot.fr/2007/04/atomiser-part-ii.html % http://chlorophil.blogspot.fr/2007/04/atomiser-part-iii.html % http://chlorophil.blogspot.fr/2007/04/atomiser-part-iv.html % http://chlorophil.blogspot.fr/2007/04/atomiser-part-v.html % http://chlorophil.blogspot.fr/2007/04/atomiser-part-vi.html % http://chlorophil.blogspot.fr/2007/04/atomiser-part-vii.html % We consider here that an AST is an ordered list of forms. % % We often use located counterparts of the standard elements (ex: forms, ASTs) % so that we can recreate and modify the order of (possibly transformed, added % or removed) forms in an AST (that order matters, notably for its compilation), % knowing that the embedded source-level line numbers are considerably less % tractable and refer to another view onto the program at hand (and that the % original forms are aggregated internally on a per-category basis rather than % on a source-level one). % % See the definition of the location/0 type for further information. % Standard modules of interest: % % - erl_scan ('The Erlang Token Scanner'): functions for tokenizing characters % into Erlang tokens % % - epp ('An Erlang Code Preprocessor'): functions which are used by compile to % preprocess macros and include files before the actual parsing % % - erl_parse ('The Erlang Parser'): basic Erlang parser % % - erl_eval ('The Erlang Meta Interpreter'): interpreter for Erlang % expressions, in the abstract syntax % % - erl_pp ('The Erlang Pretty Printer'): to display abstract forms % % - erl_lint ('The Erlang Code Linter'): to check Erlang code for illegal % syntax, bugs, unrecommended coding practices, etc. % % - compile ('The Erlang Compiler'): interface to the standard Erlang compiler % Example of PT: http://www.erlang.org/doc/man/erl_id_trans.html % Third-party libraries of interest: % % - https://github.com/uwiger/parse_trans % - https://github.com/uwiger/toker % Useful information about how to convert source code into actual code: on % http://stackoverflow.com/questions/, % 2160660/how-to-compile-erlang-code-loaded-into-a-string % Use -P to see the code generated by a parse-transform; ex: 'erlc -P' or in the % shell as 'c( "X.erl", [ 'P' ] )'. % Options specified to a parse transform at runtime, like: report_warnings, % beam,report_errors, {cwd,"X"}, {outdir,"Y"}, {i,"Z"}, {parse_transform,P}, % debug_info, warnings_as_errors, etc. % % (hence not a list_table, anyway not available here) % -type parse_transform_options() :: proplists:proplist(). %% Module subsection. % The name of a module: -type module_name() :: basic_utils:module_name(). %% Function subsection. % The name of a function: -type function_name() :: basic_utils:function_name(). % The arity of a function: -type function_arity() :: arity(). % Declaration of a function based on a name with an arity (unique function % signature within a module): % -type function_id() :: { function_name(), function_arity() }. % The type of a function (currenty: unclear semantics). -type function_type() :: any(). % The form corresponding to the definition of a clause of a function, typically % { clause, LINE, Rep(Ps), Rep(Gs), Rep(B) } for '( Ps ) when Gs -> B': % -type clause_def() :: form(). % The full type specification (if any) of that function, as an abstract form; % typically: % % { attribute, L, spec, { {foobar,Arity}, [{type,L,'fun', [{type,L,... % -type function_spec() :: form(). % The name of a variable (ex: 'X', or '_' in some cases): -type variable_name() :: atom(). -export_type([ parse_transform_options/0, module_name/0, function_name/0, function_arity/0, function_id/0, function_type/0, clause_def/0, function_spec/0, variable_name/0 ]). % Local shorthands: -type form() :: ast_base:form(). -type module_info() :: ast_info:module_info(). -type type_info() :: ast_info:type_info(). -type function_info() :: ast_info:function_info(). % Parse-transform related functions: -export([ apply_ast_transforms/2, add_function/3, add_function/4, remove_function/2, add_type/2, remove_type/2 ]). % General functions, not operating an ASTs: -export([ list_exported_functions/1, get_arities_for/2, is_function_exported/3, check_potential_call/3 ]). % Function addition/removal section. % Registers (includes exporting) specified (spec-less) function in specified % module. % -spec add_function( function_id(), [ clause_def() ], module_info() ) -> module_info(). add_function( _FunId={ FunctionName, FunctionArity }, Clauses, ModuleInfo ) -> add_function( FunctionName, FunctionArity, Clauses, ModuleInfo ). % Registers (includes exporting) specified (spec-less) function in specified % module. % -spec add_function( basic_utils:function_name(), meta_utils:function_arity(), [ clause_def() ], module_info() ) -> module_info(). add_function( FunctionName, FunctionArity, Clauses, ModuleInfo=#module_info{ %function_exports=ExportTable, functions=FunTable, markers=MarkerTable } ) -> % Let's check first that the function is not already defined: FunId = { FunctionName, FunctionArity }, case ?table:has_entry( FunId, FunTable ) of true -> CurrentFunInfo = ?table:get_value( FunId, FunTable ), CurrentFunString = ast_info:function_info_to_string( CurrentFunInfo ), ast_utils:display_error( "Function ~p already defined, as ~s.", [ FunId, CurrentFunString ] ), throw( { function_already_defined, FunId } ); false -> ok end, DefLoc = ?table:get_value( definition_functions_marker, MarkerTable ), ExportLoc = ast_info:get_default_export_function_location(), FunInfo = #function_info{ name=FunctionName, arity=FunctionArity, location=DefLoc, line=0, clauses=Clauses, spec=undefined, callback=false, % Will be auto-exported once module is recomposed: exported=[ ExportLoc ] }, NewFunTable = ?table:add_entry( FunId, FunInfo, FunTable ), % It is not strictly needed anymore to update accordingly the overall export % table, as would be done automatically when recomposing the AST: % %NewExportTable = ast_info:ensure_function_exported( FunId, [ ExportLoc ], % ModuleInfo, ExportTable ), ModuleInfo#module_info{ %function_exports=NewExportTable, functions=NewFunTable }. % Unregisters specified function from specified module. -spec remove_function( function_info(), module_info() ) -> module_info(). remove_function( FunInfo=#function_info{ exported=ExportLocs }, ModuleInfo=#module_info{ function_exports=ExportTable, functions=FunTable } ) -> FunId = { FunInfo#function_info.name, FunInfo#function_info.arity }, % First forget its description: NewFunTable = case ?table:has_entry( FunId, FunTable ) of true -> ?table:remove_entry( FunId, FunTable ); false -> throw( { non_existing_function_to_remove, FunId } ) end, % Then its exports: NewExportTable = ast_info:ensure_function_not_exported( FunId, ExportLocs, ExportTable ), ModuleInfo#module_info{ function_exports=NewExportTable, functions=NewFunTable }. % Type addition/removal section. % Registers the specified, fully-described type in specified module. -spec add_type( type_info(), module_info() ) -> module_info(). add_type( TypeInfo=#type_info{ variables=TypeVariables, exported=ExportLocs }, ModuleInfo=#module_info{ type_exports=ExportTable, types=TypeTable } ) -> Arity = length( TypeVariables ), % Let's check first that the type is not already defined: TypeId = { TypeInfo#type_info.name, Arity }, case ?table:has_entry( TypeId, TypeTable ) of true -> CurrentTypeInfo = ?table:get_value( TypeId, TypeTable ), CurrentTypeString = ast_info:type_info_to_string( CurrentTypeInfo ), AddedTypeString = ast_info:type_info_to_string( TypeInfo ), ast_utils:display_error( "Type ~p already defined, as ~s, " "whereas to be added, as ~s.", [ TypeId, CurrentTypeString, AddedTypeString ] ), throw( { type_already_defined, TypeId } ); false -> ok end, NewTypeTable = ?table:add_entry( TypeId, TypeInfo, TypeTable ), % Now updating the exports: NewExportTable = ast_info:ensure_type_exported( TypeId, ExportLocs, ModuleInfo, ExportTable ), ModuleInfo#module_info{ type_exports=NewExportTable, types=NewTypeTable }. % Unregisters specified type from specified module. -spec remove_type( type_info(), module_info() ) -> module_info(). remove_type( TypeInfo=#type_info{ variables=TypeVariables, exported=ExportLocs }, ModuleInfo=#module_info{ type_exports=ExportTable, types=TypeTable } ) -> Arity = length( TypeVariables ), TypeId = { TypeInfo#type_info.name, Arity }, % First forget its description: NewTypeTable = case ?table:has_entry( TypeId, TypeTable ) of true -> ?table:remove_entry( TypeId, TypeTable ); false -> throw( { non_existing_type_to_remove, TypeId } ) end, % Then its exports: NewExportTable = ast_info:ensure_type_not_exported( TypeId, ExportLocs, ExportTable ), ModuleInfo#module_info{ type_exports=NewExportTable, types=NewTypeTable }. % Applies specified AST transformations (mostly depth-first) to the specified % module information. % % (helper) % -spec apply_ast_transforms( module_info(), ast_transform:ast_transforms() ) -> { module_info(), ast_transform:ast_transforms() }. apply_ast_transforms( ModuleInfo=#module_info{ types=TypeTable, records=RecordTable, functions=FunctionTable }, Transforms ) -> % Note: we consider that no transformation state is to be carried from a % top-level transformation to another (so we consider that Transforms is % immutable here) % First, update the type definitions accordingly (including in records): %ast_utils:display_trace( "[Myriad] Transforming known types." ), { NewTypeTable, TypeTransforms } = ast_type:transform_type_table( TypeTable, Transforms ), %ast_utils:display_trace( "[Myriad] Transforming known types in records." ), { NewRecordTable, RecTransforms } = ast_type:transform_types_in_record_table( RecordTable, TypeTransforms ), %ast_utils:display_trace( "[Myriad] Transforming all functions." ), { NewFunctionTable, FunTransforms } = ast_function:transform_functions( FunctionTable, RecTransforms ), % Updated module_info returned: { ModuleInfo#module_info{ types=NewTypeTable, records=NewRecordTable, functions=NewFunctionTable }, FunTransforms }. % Lists (in the order of their definition) all the functions ({Name,Arity}) that % are exported by the specified module, expected to be found in the code path. % -spec list_exported_functions( basic_utils:module_name() ) -> [ function_id() ]. list_exported_functions( ModuleName ) -> % To avoid a unclear message like 'undefined function XXX:module_info/1': case code_utils:is_beam_in_path( ModuleName ) of not_found -> throw( { module_not_found_in_path, ModuleName } ); _ -> ok end, ModuleName:module_info( exports ). % Returns a list of the arities for which the specified function of the % specified module is exported. % -spec get_arities_for( basic_utils:module_name(), function_name() ) -> [ arity() ]. get_arities_for( ModuleName, FunctionName ) -> ExportedFuns = list_exported_functions( ModuleName ), % Match on FunctionName: [ Arity || { Name, Arity } <- ExportedFuns, Name =:= FunctionName ]. % Tells whether the specified function (name with arity) is exported by the % specified module. % -spec is_function_exported( basic_utils:module_name(), function_name(), arity() ) -> boolean(). is_function_exported( ModuleName, FunctionName, Arity ) -> lists:member( { FunctionName, Arity }, list_exported_functions( ModuleName ) ). % Checks whether a potential upcoming call to the specified MFA % (Module,Function,Arguments) has a chance of succeeding. % -spec check_potential_call( basic_utils:module_name(), function_name(), [ basic_utils:argument() ] ) -> 'ok' | 'module_not_found' | 'function_not_exported'. check_potential_call( ModuleName, FunctionName, Arguments ) when is_atom( ModuleName ) andalso is_atom( FunctionName ) andalso is_list( Arguments ) -> case code_utils:is_beam_in_path( ModuleName ) of not_found -> module_not_found; _ -> Arity = length( Arguments ), case is_function_exported( ModuleName, FunctionName, Arity ) of true -> ok; false -> function_not_exported end end; check_potential_call( ModuleName, FunctionName, Arguments ) -> case is_atom( ModuleName ) of true -> ok; false -> throw( { non_atom_module_name, ModuleName } ) end, case is_atom( FunctionName ) of true -> ok; false -> throw( { non_atom_function_name, FunctionName } ) end, % Only remaining possibility: throw( { non_list_arguments, Arguments } ).