% Copyright (C) 2018-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: Sunday, January 21, 2018. % Module in charge of scanning an AST, a prerequisite notably for later % transformations (see ast_transform.erl for that). % -module(ast_scan). % For the table macro for example: -include("meta_utils.hrl"). % For the module_info record: -include("ast_info.hrl"). % Name of any parse attribute: % % (typically in Form={ attribute, Line, AttributeName, AttributeValue }) % -type parse_attribute_name() :: atom(). -type scan_context() :: { ast_base:file_reference(), ast_base:line() }. -type error_report() :: text_utils:ustring(). -export_type([ parse_attribute_name/0, scan_context/0, error_report/0 ]). -export([ scan/1, check_parse_attribute_name/1, check_parse_attribute_name/2 ]). % Shorthands: -type ast() :: ast_base:ast(). -type form_context() :: ast_base:form_context(). -type module_info() :: ast_info:module_info(). -type compile_option_table() :: ast_info:compile_option_table(). -type located_form() :: ast_info:located_form(). -type marker_table() :: ast_info:section_marker_table(). % Implementation notes: % % The integral scanning of ASTs got inspiration from erl_id_trans.erl. % % Numbered sections refer to the ones in % http://erlang.org/doc/apps/erts/absform.html, with additional subsections for % each bullet. % % Note: we used to traverse the AST recursively, "blindly", i.e. without % expecting the intended structure of AST elements (ex: the structure of a tuple % corresponding to a 'receive' statement). % % Then, for a more complete control, we match the AST against its intended, % "official" structure, as defined in % http://erlang.org/doc/apps/erts/absform.html; however, for a mere scan, this % is mostly useless (as a more basic traversal is sufficient), and moreover % counter-productive as we might want to deviate from the base, Erlang rules in % the *input* AST (but of course the output one shall be legit). % % Now we thus perform a minimal traversal, i.e. one that is just deep enough in % order that we can fill appropriately our module_info record. % % Moreover, as this scanning is meant to operate on an input AST (from the point % of view of Ceylan-Myriad), we adhere to the Ceylan-Myriad conventions rather % than to the vanilla Erlang ones (that apply to the final, output AST). % % Deep, strict traversal is of use mainly for transformation (see % ast_transform.erl) rather than for scanning. % % Ultimately, resulting forms are to be validated anyway by the compiler (most % precise and efficient Erlang-level checking). % % We tried to order scan clauses roughly in decreasing average frequency of % appearance in the actual code to transform, yet often the order of clauses % matters (as it has a meaning and an impact). % Order of the forms in the AST % % A parse transform receives an (ordered) list of forms as input AST: forms are % ordered only by their position in that list (line numbers are contextual to % the last file declared as included, hence form order matters, and a form % cannot be ordered by itself), stored in a module_info record. % % Knowing that forms may have to be added, moved, updated, removed, etc. during % the transformation of an AST, positioning forms at specified points in an AST % stream is not straightforward (ex: the mere index of a form in the AST is not % stable, as any addition before this form would require this index to be % updated); moreover being able to keep track of their original order (to which % the parse transform tries to stick as much as possible) is certainly a safe % measure for debuggability. % % So, instead of storing forms in an AST, we store located forms in a located % AST: instead of having [ F1, F2, ..., Fn ], we may have for example [ { Loc2, % F2 }, { Loc1, F1 }, ..., { Locn, Fn } ], where Loc are (stable, immutable) % locations, i.e. form_location() - which are themselves id_utils:sortable_id(). % % It allows to store, in a located AST, (located) forms in an arbitrary order: % sorting them according to their location reorders them as intended (note that % the AST scan results in an in-order located AST, which is more convenient for % insertions). % % Special AST pseudo-locations have been defined, including 'auto_locate_after', % which is, when iterating through a located AST, to be replaced by an actual % location just after the current one (relative positioning, based on the % current order of the stream). % % We also found it useful to define section markers (see % ast_info:section_marker/0), which are standard reference points within an AST, % to offer absolute positioning. % % Scanning an AST tries to automatically set these section markers, based on the % content found. % % For example, one may want a given form to be placed at the beginning of the % section for function exports; then the location of this form shall be % specified as being after the corresponding marker, i.e. after the location % stored in marker 'export_functions_marker'. % % However, depending on the AST content, markers may not be set, or may even not % be sorted according to our canonical order (ex: 'export_functions_marker' may % be set before 'export_types_marker', it is still legit compilation-wise). % Scans the specified AST, expected to correspond to a module definition, and % returns the corresponding module_info record. % -spec scan( ast() ) -> module_info(). scan( AST ) -> %ast_utils:display_trace( "scanning AST" ), InitModuleInfo = ast_info:init_module_info(), FirstLocation = id_utils:get_initial_sortable_id(), FirstMarkerTable = ?table:add_new_entry( begin_marker, FirstLocation, InitModuleInfo#module_info.markers ), FirstModuleInfo = InitModuleInfo#module_info{ markers=FirstMarkerTable }, %ast_utils:display_trace( "beginning scan" ), % Application of 7.1.1: % % "If D is a module declaration consisting of the forms F_1, ..., F_k, then % Rep(D) = [Rep(F_1), ..., Rep(F_k)]." % % So iterating in the list of forms that make for the AST: % case scan_forms( AST, FirstModuleInfo, _NextLocation=FirstLocation, _CurrentFileRef=undefined ) of M=#module_info{ errors=[] } -> %ast_utils:display_trace( "no error to report" ), % No error, let's continue: M; _M=#module_info{ errors=Errors } -> ReorderedErrors = lists:reverse( Errors ), %ast_utils:display_debug( "reporting errors: ~p", % [ ReorderedErrors ] ), [ report_error( E ) || E <- ReorderedErrors ], % No need to be that violent: %erlang:halt( 1 ) %exit( Errors ) %exit( errors_reported ) exit( "fix your code :)" ) % If wanting instead to ignore errors, at least at this step: %M end. % Reports specified error, using the same format as erlc, so that tools can % parse these errors as well. % % Ex: foo.erl:102: can't find include file "bar.hrl" % -spec report_error( { ast_scan:scan_context(), basic_utils:error_reason() } ) -> basic_utils:void(). report_error( { Context, Error } ) -> % No trace_utils yet here: %io:format( "Reporting error ~p in context ~p.", [ Error, Context ] ), ErrorString = case Error of % Ex: Msg="head mismatch" { parse_error, Msg } when is_list( Msg ) -> %text_utils:format( "parse error: ~s", [ Msg ] ); text_utils:format( "~s", [ Msg ] ); { undefined_macro_variable, VariableName } when is_atom( VariableName ) -> text_utils:format( "undefined macro variable '~s'", [ VariableName ] ); { epp_error, { undefined, MacroName, Arity } } when is_atom( MacroName ) andalso is_integer( Arity ) -> text_utils:format( "undefined macro ~s/~B", [ MacroName, Arity ] ); % Ex: DirectiveName=ifdef { epp_error, { bad, DirectiveName } } when is_atom( DirectiveName ) -> text_utils:format( "invalid '~s' preprocessor directive", [ DirectiveName ] ); % Ex: Problem="unbalanced" { epp_error, { illegal, Problem, DirectiveName } } when is_atom( DirectiveName ) -> text_utils:format( "illegal ~s '~s' preprocessor directive", [ Problem, DirectiveName ] ); % Ex: MacroName=debug { epp_error, { mismatch, MacroName } } -> text_utils:format( "mismatch regarding macro '~s' (wrong arity?)", [ MacroName ] ); String when is_list( String ) -> text_utils:format( "~s", [ String ] ); Other -> text_utils:format( "~p", [ Other ] ) end, io:format( "~s: ~s~n", [ context_to_string( Context ), ErrorString ] ). % Returns a textual representation of specified compilation context. -spec context_to_string( ast_scan:scan_context() ) -> string(). context_to_string( { Filename, Line } ) -> % Respects the standard formatting: % % (note: using file_utils:normalise_path/1 would suppose file_utils is % already built...) % text_utils:format( "~s:~B", [ Filename, Line ] ). %% %% First part: top-level forms. %% % Main scanning function. % % Here all relevant parts of the specified AST (located forms) are matched in % turn, and stored in the specified module_info once located using % ast_base:form_location/0 identifiers, which allow easy insertions and % reordering. % -spec scan_forms( ast(), module_info(), ast_base:form_location(), ast_base:file_reference() ) -> module_info(). % Overall structure (based on http://erlang.org/doc/apps/erts/absform.html): % % Section 7.1: Module Declarations and Forms: % - (7.1.1: Module declaration, already done) % - 7.1.2: Function export % - 7.1.3: Function import % - 7.1.4: Module % - 7.1.5: File reference (include) % - 7.1.6: Function definition % - 7.1.7: Local function type specification % - 7.1.8: Remote function type specification % - 7.1.9: Record definition % - 7.1.10: Type definition % - 7.1.11: Other, "wild" parse attributes. % - 7.1.12: Type export [lacking in reference page] % - 7.1.13: Compile export [lacking in reference page] % - 7.1.14 : Parse errors % - 7.1.15 : End of file % Extra Section 7.9: Catch-all for unexpected forms %% %% Section 7.1: Module Declarations and Forms. %% % (7.1.1: Module declaration, already done) % 7.1.2: Function export handling. % % "If F is an attribute -export([Fun_1/A_1, ..., Fun_k/A_k]), then Rep(F) = % {attribute,LINE,export,[{Fun_1,A_1}, ..., {Fun_k,A_k}]}." % scan_forms( _AST=[ _Form={ 'attribute', Line, 'export', FunctionIds } | T ], M=#module_info{ function_exports=ExportTable, functions=FunctionTable, markers=MarkerTable }, NextLocation, CurrentFileReference ) -> %ast_utils:display_debug( "function export declaration for ~p", % [ FunctionIds ] ), Context = { CurrentFileReference, Line }, % Records this export regarding each of these functions: NewFunctionTable = lists:foldl( fun( FunId, FunTableAcc ) -> { Name, Arity } = ast_function:check_function_id( FunId, Context ), NewFunInfo = case ?table:lookup_entry( FunId, FunTableAcc ) of key_not_found -> % New entry then: #function_info{ name=Name, arity=Arity, % Implicit: %location=undefined %line=undefined %clauses=[], %spec=undefined, %callback=undefined, exported=[ NextLocation ] }; % A function *might* be exported more than once: { value, FunInfo } -> % F=#function_info{ exported=[] } } -> % Just add the fact that the function is exported then: NewExp = [ NextLocation | FunInfo#function_info.exported ], FunInfo#function_info{ exported=NewExp } end, ?table:add_entry( FunId, NewFunInfo, FunTableAcc ) end, _Acc0=FunctionTable, _List=FunctionIds ), % At least initially, exactly one export entry per location: NewExportTable = ?table:add_new_entry( NextLocation, { Line, FunctionIds }, ExportTable ), NewMarkerTable = case ?table:has_entry( export_functions_marker, MarkerTable ) of true -> % Already found, nothing to do. MarkerTable; false -> ?table:add_entry( export_functions_marker, NextLocation, MarkerTable ) end, % And then recurses with the next forms: scan_forms( T, M#module_info{ function_exports=NewExportTable, functions=NewFunctionTable, markers=NewMarkerTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.3: Function import handling. % % "If F is an attribute -import(Mod,[Fun_1/A_1, ..., Fun_k/A_k]), then Rep(F) = % {attribute,LINE,import,{Mod,[{Fun_1,A_1}, ..., {Fun_k,A_k}]}}." % scan_forms( _AST=[ Form={ 'attribute', Line, 'import', { ModuleName, FunIds } } | T ], M=#module_info{ function_imports=ImportTable, function_imports_defs=ImportDefs, markers=MarkerTable }, NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line }, ast_utils:check_module_name( ModuleName, Context ), ast_function:check_function_ids( FunIds, Context ), NewImportTable = ?table:append_list_to_entry( ModuleName, FunIds, ImportTable ), NewImportDefs = [ { NextLocation, Form } | ImportDefs ], NewMarkerTable = case ?table:has_entry( import_functions_marker, MarkerTable ) of true -> % Already found, nothing to do. MarkerTable; false -> ?table:add_entry( import_functions_marker, NextLocation, MarkerTable ) end, scan_forms( T, M#module_info{ function_imports=NewImportTable, function_imports_defs=NewImportDefs, markers=NewMarkerTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.4: Module handling. % % "If F is an attribute -module(Mod), then Rep(F) = % {attribute,LINE,module,Mod}." % scan_forms( _AST=[ Form={ 'attribute', Line, 'module', ModuleName } | T ], M=#module_info{ module=undefined, markers=MarkerTable }, NextLocation, CurrentFileReference ) -> %ast_utils:display_debug( "module declaration for ~s", [ ModuleName ] ), Context = { CurrentFileReference, Line }, ast_utils:check_module_name( ModuleName, Context ), % Note: when processing X.beam, we should not remove the lines like: % {attribute,37,file,{"X.erl",37}} as they allow to report errors % appropriately. LocForm = { NextLocation, Form }, NewMarkerTable = ?table:add_new_entry( module_marker, NextLocation, MarkerTable ), scan_forms( T, M#module_info{ module={ ModuleName, LocForm }, markers=NewMarkerTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % Any lacking, invalid or duplicated module declaration will be caught by % the compiler anyway. % scan_forms( _AST=[ _Form={ 'attribute', Line, 'module', ModuleName } | _T ], #module_info{ module={ PreviousModuleName, _PreviousLocDef } }, _NextLocation, CurrentFileReference ) -> throw( { multiple_module_definitions, PreviousModuleName, ModuleName, { CurrentFileReference, Line } } ); % 7.1.5: File reference (include) handling. % % "If F is an attribute -file(File,Line), then Rep(F) = % {attribute,LINE,file,{File,Line}}." % % Allows to keep track of when an included file begins and also ends, i.e. to % determine the current file to which any new line number corresponds. % scan_forms( _AST=[ Form={ 'attribute', _Line, 'file', { FilePath, _FileLine } } | T ], M=#module_info{ includes=Inc, include_defs=IncDefs }, NextLocation, _CurrentFileReference ) -> %ast_utils:display_debug( "file declaration with ~s at #~B", % [ FilePath, FileLine ] ), % We used to normalise paths, however then 'file_utils' would have to be % bootstrapped as well, which does not seem desirable. %NewCurrentFileReference = text_utils:string_to_binary( % file_utils:normalise_path( Filepath ) ), NewCurrentFileReference = text_utils:string_to_binary( FilePath ), % Avoids duplicates (in 'includes' only, not in definitions): % NewFilePaths = case lists:member( NewCurrentFileReference, Inc ) of true -> Inc; false -> [ NewCurrentFileReference | Inc ] end, LocForm = { NextLocation, Form }, scan_forms( T, M#module_info{ includes=NewFilePaths, include_defs=[ LocForm | IncDefs ] }, id_utils:get_next_sortable_id( NextLocation ), NewCurrentFileReference ); % 7.1.6: Function definition handling. % % "If F is a function declaration Name Fc_1 ; ... ; Name Fc_k, where each Fc_i % is a function clause with a pattern sequence of the same length Arity, then % Rep(F) = {function,LINE,Name,Arity,[Rep(Fc_1), ...,Rep(Fc_k)]}." % scan_forms( [ _Form={ 'function', Line, FunctionName, FunctionArity, Clauses } | T ], M=#module_info{ functions=FunctionTable, markers=MarkerTable, errors=Errors }, %unhandled_forms=UnhandledForms }, NextLocation, CurrentFileReference ) -> %ast_utils:display_debug( "function definition for ~p/~p", % [ FunctionName, FunctionArity ] ), % The non-first clauses could be checked as well: % % (when adding a function, we may not check if ever there was a pre-existing % one - multiple definitions will be rejected by the compiler anyway) Context = { CurrentFileReference, Line }, ast_function:check_function_name( FunctionName, Context ), ast_utils:check_arity( FunctionArity, Context ), ast_clause:check_function_clauses( Clauses, FunctionArity, Context ), FunId = { FunctionName, FunctionArity }, { FunInfo, MaybeError } = case ?table:lookup_entry( FunId, FunctionTable ) of key_not_found -> % New entry then: Finfo = #function_info{ name=FunctionName, arity=FunctionArity, location=NextLocation, line=Line, clauses=Clauses % Implicit: %spec=undefined, %callback=undefined, %exported=[] }, { Finfo, undefined }; { value, F=#function_info{ clauses=[] } } -> % Already here because of an export; just add the missing % information then: Finfo = F#function_info{ location=NextLocation, line=Line, clauses=Clauses }, { Finfo, undefined }; % Here a definition was already set: (previous line not necessarily in % the context of the same file) % We tried to silence that error (let it go through) in order to rely on % the compiler, yet commenting-out this led to having a double % definition not properly handled: % % Would result in ignoring this definition, hence compiling incorrectly: % %{ value, F } -> % { F, ok } % We could crash directly, yet the error message would not be nicely % integrated in an IDE (as not standard): %{ value, #function_info{ line=PreviousLine } } -> % ast_utils:raise_error( [ multiple_definitions_for, FunId, % { previous_line, PreviousLine } ], % Context ) % A better approach could have been to store a list of such list of % clauses, and to reinject them as they are (as unhandled forms), so % that the compiler sees them and complains in a standard manner % afterwards; but then it triggers a warning of ours (that we want to % keep) about unhandled forms, and the standard message is less % informative than ours (only "FILE:LINE: function foo/1 already % defined"), so we finally prefer ours: { value, F=#function_info{ name=FunctionName, arity=FunctionArity, line=SomeLine } } -> % Note that the past error was not necessarily in the same file: % % (this information could be determined thanks to the located forms, % yet you require quite a lot of development) Report = text_utils:format( "multiple definitions for ~s/~B " "(past definition at line ~B)", [ FunctionName, FunctionArity, SomeLine ] ), Error = { Context, Report }, %UnhandledLocForm = { NextLocation, Form }, { F, Error } % { F, UnhandledLocForm } end, NewFunctionTable = ?table:add_entry( _K=FunId, _V=FunInfo, FunctionTable ), NewMarkerTable = case ?table:has_entry( definition_functions_marker, MarkerTable ) of true -> % Already found, nothing to do. MarkerTable; false -> ?table:add_entry( definition_functions_marker, NextLocation, MarkerTable ) end, NewErrors = case MaybeError of undefined -> Errors; _ -> [ MaybeError | Errors ] end, %ast_utils:display_debug( "function ~s/~B with ~B clauses registered.", % [ FunctionName, FunctionArity, length( Clauses ) ] ), scan_forms( T, M#module_info{ functions=NewFunctionTable, markers=NewMarkerTable, errors=NewErrors }, %unhandled_forms=NewUnhandledForms }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.7: Local function type specification handling (including callbacks). % % "If F is a function specification -Spec Name Ft_1; ...; Ft_k, where Spec is % either the atom spec or the atom callback, and each Ft_i is a possibly % constrained function type with an argument sequence of the same length Arity, % then Rep(F) = {attribute,Line,Spec,{{Name,Arity},[Rep(Ft_1), ..., % Rep(Ft_k)]}}." % scan_forms( [ Form={ 'attribute', Line, SpecType, { FunId, FunctionTypes } } | T ], M=#module_info{ functions=FunctionTable }, NextLocation, CurrentFileReference ) when SpecType == 'spec' orelse SpecType == 'callback' -> Context = { CurrentFileReference, Line }, { FunctionName, FunctionArity } = ast_function:check_function_id( FunId, Context ), ast_function:check_function_types( FunctionTypes, FunctionArity, Context ), %ast_utils:display_debug( "~s definition for ~p/~p", % [ SpecType, FunctionName, FunctionArity ] ), LocatedSpec = { NextLocation, Form }, IsCallback = case SpecType of callback -> true; spec -> false end, FunInfo = case ?table:lookup_entry( FunId, FunctionTable ) of key_not_found -> % New entry then: #function_info{ name=FunctionName, arity=FunctionArity, % Implicit: %location=undefined, %line=undefined, %clauses=[] spec=LocatedSpec, callback=IsCallback }; { value, F=#function_info{ spec=undefined } } -> % Just add that spec form and callback status then: F#function_info{ spec=LocatedSpec, callback=IsCallback }; % Here a spec was already set: %_ -> % ast_utils:raise_error( [ multiple_specs_for, FunId ], Context ) % Finally we prefer letting the compiler complain by itself about these % multiple specs: % { value, F } -> F end, NewFunctionTable = ?table:add_entry( _K=FunId, _V=FunInfo, FunctionTable ), %ast_utils:display_debug( "spec for function ~s/~B registered.", % [ FunctionName, FunctionArity ] ), scan_forms( T, M#module_info{ functions=NewFunctionTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % (optional callbacks, not specified in the spec yet known of the id parse % transform) % scan_forms( [ Form={ 'attribute', Line, _AttributeName='optional_callbacks', _AttributeValue=FunIds } | T ], M=#module_info{ optional_callbacks_defs=LocatedDefs }, NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line }, % Surprisingly, in erl_id_trans.erl, the corresponding check may fail (as is % in a try/catch clause), and in this case is replaced by its original % value. % ast_function:check_function_ids( FunIds, Context ), LocForm = { NextLocation, Form }, scan_forms( T, M#module_info{ optional_callbacks_defs=[ LocForm | LocatedDefs ] }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % (asm attribute, not specified in the spec yet known of the id parse transform; % checked and then treated as any wild parse attribute) % scan_forms( [ Form={ 'attribute', _Line, AttributeName='asm', Def={ 'function', _N, _A, _Code } } | T ], M=#module_info{ parse_attributes=ParseAttributeTable }, NextLocation, CurrentFileReference ) -> %ast_utils:display_debug( "Asm attribute definition: '~p'.", [ Def ] ), LocForm = { NextLocation, Form }, % Expected once: scan_forms( T, M#module_info{ parse_attributes=?table:append_to_entry( AttributeName, { _AttributeValue=Def, LocForm }, ParseAttributeTable ) }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.8: Remote function type specification handling. % % "If F is a function specification -spec Mod:Name Ft_1; ...; Ft_k, where each % Ft_i is a possibly constrained function type with an argument sequence of the % same length Arity, then Rep(F) = % {attribute,Line,spec,{{Mod,Name,Arity},[Rep(Ft_1), ..., Rep(Ft_k)]}}." % scan_forms( [ Form={ 'attribute', Line, 'spec', _MFA={ ModuleName, FunctionName, FunctionArity }, FunctionTypes } | T ], M=#module_info{ remote_spec_defs=RemoteSpecDefs }, NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line }, FunId = { FunctionName, FunctionArity }, ast_utils:check_module_name( ModuleName, Context ), ast_function:check_function_id( FunId, Context ), ast_function:check_function_types( FunctionTypes, FunctionArity, Context ), %ast_utils:display_debug( "remote spec definition for ~p/~p", % [ FunctionName, FunctionArity ] ), % Specs for remote functions not specifically processed. LocatedSpec = { NextLocation, Form }, NewRemoteSpecDefs = [ LocatedSpec | RemoteSpecDefs ], %ast_utils:display_debug( "remote spec for function ~s/~B registered.", % [ FunctionName, FunctionArity ] ), scan_forms( T, M#module_info{ remote_spec_defs=NewRemoteSpecDefs }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.9: Record definition handling. % % "If F is a record declaration -record(Name,{V_1, ..., V_k}), where each V_i is % a record field, then Rep(F) = {attribute,LINE,record,{Name,[Rep(V_1), ..., % Rep(V_k)]}}. For Rep(V), see below. % scan_forms( _AST=[ _Form={ 'attribute', Line, 'record', { RecordName, DescFields } } | T ], M=#module_info{ records=RecordTable, markers=MarkerTable }, NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line }, ast_type:check_record_name( RecordName, Context ), % Finally we let the compiler complain: %case ?table:has_entry( RecordName, RecordTable ) of % % true -> % ast_utils:raise_error( % [ multiple_definitions_for_record, RecordName ], Context ); % % false -> % ok % %end, FieldTable = scan_field_descriptions( DescFields, CurrentFileReference ), NewRecordDef = { FieldTable, NextLocation, Line }, %ast_utils:display_debug( "Adding for record '~p' following " % "definition:~n~p", [ RecordName, NewRecordDef ] ), % New entry by design: NewRecordTable = ?table:add_entry( RecordName, NewRecordDef, RecordTable ), NewMarkerTable = case ?table:has_entry( definition_records_marker, MarkerTable ) of true -> % Already found, nothing to do. MarkerTable; false -> ?table:add_entry( definition_records_marker, NextLocation, MarkerTable ) end, scan_forms( T, M#module_info{ records=NewRecordTable, markers=NewMarkerTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.10: Type definition handling. % % "If F is a type declaration -Type Name(V_1, ..., V_k) :: T, where Type is % either the atom type or the atom opaque, each V_i is a variable, and T is a % type, then Rep(F) = {attribute,LINE,Type,{Name,Rep(T),[Rep(V_1), ..., % Rep(V_k)]}}." % scan_forms( _AST=[ _Form={ 'attribute', Line, TypeDesignator, { TypeName, TypeDef, TypeVariables } } | T ], M=#module_info{ types=TypeTable, markers=MarkerTable }, NextLocation, CurrentFileReference ) when TypeDesignator == 'type' orelse TypeDesignator == 'opaque' -> %ast_utils:display_debug( "type declaration for ~p: ~p", [ % TypeName, Form ] ), Context = { CurrentFileReference, Line }, ast_type:check_type_name( TypeName, Context ), ast_type:check_type_definition( TypeDef, Context ), ast_type:check_type_variables( TypeVariables, Context ), IsOpaque = case TypeDesignator of type -> false; _Opaque -> true end, TypeArity = length( TypeVariables ), TypeId = { TypeName, TypeArity }, NewTypeInfo = case ?table:lookup_entry( TypeId, TypeTable ) of % If a TypeInfo is found for that type name, it must be only because it % has already been exported (not expected to be already defined): { value, #type_info{ line=LineDef, exported=[] } } -> % We have to report this multiple definition, otherwise it will not % be seen by the compiler afterwards (the extra type definition % would be skipped before reaching the compiler) % "some line" as we do not know here to which source files this % corresponds: % ast_utils:raise_usage_error( "type ~s/~B defined here, whereas " "it had already been defined at (some) line #~B.", [ TypeName, TypeArity, LineDef ], CurrentFileReference, Line ); { value, ExportTypeInfo } -> ExportTypeInfo#type_info{ name=TypeName, variables=TypeVariables, opaque=IsOpaque, location=NextLocation, line=Line, definition=TypeDef %exported: already set }; % Usual case: key_not_found -> %ast_utils:display_debug( "New type '~s' defined as:~n~p", % [ TypeName, TypeDef ] ), #type_info{ name=TypeName, variables=TypeVariables, opaque=IsOpaque, location=NextLocation, line=Line, definition=TypeDef %exported=[] } end, NewTypeTable = ?table:add_entry( TypeId, NewTypeInfo, TypeTable ), NewMarkerTable = case ?table:has_entry( definition_types_marker, MarkerTable ) of true -> % Already found, nothing to do. MarkerTable; false -> ?table:add_entry( definition_types_marker, NextLocation, MarkerTable ) end, scan_forms( T, M#module_info{ types=NewTypeTable, markers=NewMarkerTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.11: Other, "wild" parse attributes: this section will come later, so that % it matches only if none of the other attribute-related ones (such as for % 'export_type') matched. % 7.1.12: Type export handling [lacking in reference page]. % % Supposedly: % % "If F is an attribute -export_type([Type_1/A_1, ..., Type_k/A_k]), then Rep(F) % = {attribute,LINE,export_type,[{Type_1,A_1}, ..., {Type_k,A_k}]}." % scan_forms( _AST=[ _Form={ 'attribute', Line, 'export_type', TypeIds } | T ], M=#module_info{ type_exports=ExportTable, types=TypeTable, markers=MarkerTable }, NextLocation, CurrentFileReference ) -> %ast_utils:display_debug( "Type export declaration for ~p", [ TypeIds ] ), Context = { CurrentFileReference, Line }, % Records for each of these types this export: NewTypeTable = lists:foldl( fun( TypeId, TypeTableAcc ) -> { Name, _Arity } = ast_type:check_type_id( TypeId, Context ), NewTypeInfo = case ?table:lookup_entry( TypeId, TypeTableAcc ) of key_not_found -> % New entry then (arity known, but not yet the detailed % variables): % #type_info{ name=Name, % Implicit: %variables=undefined, %opaque=undefined %location=undefined %line=undefined %definition=[], exported=[ NextLocation ] }; % A type *might* be exported more than once: { value, TypeInfo } -> % F=#type_info{ exported=[] } } -> % Just add the fact that the type is exported then: NewExp = [ NextLocation | TypeInfo#type_info.exported ], TypeInfo#type_info{ exported=NewExp } end, ?table:add_entry( TypeId, NewTypeInfo, TypeTableAcc ) end, _Acc0=TypeTable, _List=TypeIds ), % Initially, exactly one export entry per location: NewExportTable = ?table:add_new_entry( NextLocation, { Line, TypeIds }, ExportTable ), NewMarkerTable = case ?table:has_entry( export_types_marker, MarkerTable ) of true -> % Already found, nothing to do. MarkerTable; false -> ?table:add_entry( export_types_marker, NextLocation, MarkerTable ) end, scan_forms( T, M#module_info{ type_exports=NewExportTable, types=NewTypeTable, markers=NewMarkerTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.13: Compilation option handling [lacking in reference page]. % % We may have here full or regular inlining (single function or list thereof), % and possibly other options. % % Note that options can also be specified through the command line (ex: % "-Ddebug_mode"). % scan_forms( _AST=[ Form={ 'attribute', Line, 'compile', CompileInfo } | T ], M=#module_info{ compilation_options=CompileTable, compilation_option_defs=CompileDefs }, NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line }, %ast_utils:display_debug( "Registration compilation option:~n~p~n", % [ CompileInfo ] ), NewCompileTable = register_compile_attribute( CompileInfo, CompileTable, Context ), NewCompileDefs = [ { NextLocation, Form } | CompileDefs ], scan_forms( T, M#module_info{ compilation_options=NewCompileTable, compilation_option_defs=NewCompileDefs }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.11: Other, "wild" parse attributes. % % (section body is here, to match iff none of the other attribute-related % sections matched) % scan_forms( [ Form={ 'attribute', Line, AttributeName, AttributeValue } | T ], M=#module_info{ parse_attributes=ParseAttributeTable }, NextLocation, CurrentFileReference ) -> %ast_utils:display_debug( "Parse attribute definition for '~p': ~p", % [ AttributeName, AttributeValue ] ), Context = { CurrentFileReference, Line }, check_parse_attribute_name( AttributeName, Context ), LocForm = { NextLocation, Form }, % As a wild attribute may be defined more than once: NewParseAttributeTable = ?table:append_to_entry( AttributeName, { AttributeValue, LocForm }, ParseAttributeTable ), scan_forms( T, M#module_info{ parse_attributes=NewParseAttributeTable }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.14 : Parse errors handling. % Handling errors: % % Apparently parse transforms *have* to manage errors by themselves. Indeed we % stored them in the unhandled_forms field of the module_info record and % reinjected them in the returned AST, hoping that the compiler chain would % complain appropriately (at least as it does without parse transforms). % % However it does not seem to be the case, as reinserting the error forms (where % they were, thanks to their location information) results in (when adding an % intentional error in the source file): % % foo.erl: internal error in lint_module; % crash reason: badarg % % in function erl_anno:set/3 % called as erl_anno:set(file,"class_Mesh.erl",undefined) % in call from erl_lint:set_form_file/2 (erl_lint.erl, line 690) % in call from erl_lint:eval_file_attr/2 (erl_lint.erl, line 679) % in call from erl_lint:forms/2 (erl_lint.erl, line 641) % in call from erl_lint:module/3 (erl_lint.erl, line 498) % in call from compile:lint_module/1 (compile.erl, line 999) % in call from compile:'-internal_comp/4-anonymous-1-'/2 (compile.erl, line 295) % in call from compile:fold_comp/3 (compile.erl, line 321) % % So we manage the errors by ourselves, aggregating them first, then reporting % them (along with warnings) in a standard format so that tools can accommodate % them. % % (for some reason, the reported lines are incremented, we have to decrement % them) % Preprocessor (eep) errors: % eep include error: scan_forms( _AST=[ _Form={ 'error', { Line, 'epp', { 'include', 'file', FileName } } } | _T ], _M=#module_info{ errors=_Errors }, _NextLocation, CurrentFileReference ) -> % Better error message if managed directly: ast_utils:raise_usage_error( "include file not found: '~s'.", [ FileName ], CurrentFileReference, Line ); %NewError = { Context, { include_file_not_found, FileName } }, %scan_forms( T, M#module_info{ errors=[ NewError | Errors ] }, % id_utils:get_next_sortable_id( NextLocation ), % CurrentFileReference ); % eep undefined macro variable error: scan_forms( _AST=[ _Form={ 'error', { Line, 'epp', { 'undefined', VariableName, 'none' } } } | T ], M=#module_info{ errors=Errors }, NextLocation, CurrentFileReference ) -> % Wrong: Context = { CurrentFileReference, Line-1 }, Context = { CurrentFileReference, Line }, %ast_utils:raise_error( [ undefined_macro_variable, VariableName ], % Context ); NewError = { Context, { undefined_macro_variable, VariableName } }, scan_forms( T, M#module_info{ errors=[ NewError | Errors ] }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % eep general errors: scan_forms( _AST=[ _Form={ 'error', { Line, 'epp', Reason } } | T ], M=#module_info{ errors=Errors }, NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line-1 }, %ast_utils:raise_error( [ preprocessing_failed, Reason ], Context ); NewError = { Context, { epp_error, Reason } }, scan_forms( T, M#module_info{ errors=[ NewError | Errors ] }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % Parser (erl_parse) errors: scan_forms( _AST=[ _Form={ 'error', { Line, 'erl_parse', Reason } } | T ], M=#module_info{ errors=Errors }, NextLocation, CurrentFileReference ) -> %Context = { CurrentFileReference, Line-1 }, Context = { CurrentFileReference, Line }, %ast_utils:raise_error( [ parsing_failed, io_lib:format( Reason, [] ) ], % Context ); NewError = { Context, { parse_error, Reason } }, scan_forms( T, M#module_info{ errors=[ NewError | Errors ] }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % Any kind of other error: scan_forms( _AST=[ _Form={ 'error', ErrorTerm } | _T ], _ModuleInfo, _NextLocation, CurrentFileReference ) -> % No line information available, so: ast_utils:raise_error( [ scan_error, ErrorTerm ], _Context=CurrentFileReference ); % Any kind of warning: scan_forms( _AST=[ _Form={ 'warning', WarningTerm } | T ], ModuleInfo, NextLocation, CurrentFileReference ) -> % No line information available, so: ast_utils:notify_warning( [ scan_warning, WarningTerm ], _Context=CurrentFileReference ), scan_forms( T, ModuleInfo, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % 7.1.15 : End of file handling. % We expect the module name to be known when ending the processing (so that we % can remove its corresponding file from the includes): % % Here, no module found: scan_forms( _AST=[ _Form={ 'eof', Line } ], M=#module_info{ module=undefined, errors=Errors }, _NextLocation, CurrentFileReference ) -> Context = { CurrentFileReference, Line }, NewError = { Context, "end of file reached whereas no suitable module " "declaration (ex: '-module(foobar).') found." }, % Directly returned: M#module_info{ errors=[ NewError | Errors ] }; % Form expected to be defined once, and to be the last one: scan_forms( _AST=[ Form={ 'eof', _Line } ], M=#module_info{ module={ ModuleName, _ModuleLocDef }, includes=Inc, function_exports=ExportTable, last_line=undefined, markers=MarkerTable }, NextLocation, _CurrentFileReference ) -> %ast_utils:display_debug( "eof declaration at ~p.", [ Line ] ), % Certainly better than id_utils:get_sortable_id_upper_bound(), as for % example we may want to add form after that original end: % EndLocation = NextLocation, NewMarkerTable = finalize_marker_table( EndLocation, MarkerTable ), LocForm = { NextLocation, Form }, % End of file found, doing some housekeeping. % We generate here a default export form, supposedly at line 0 for easier % interpretation; it will start empty but may be enriched by parse % transforms, when they need to export functions that they just added. ExportLoc = ast_info:get_default_export_function_location(), NewExportTable = ?table:add_entry( ExportLoc, { _FirstLine=0, _Empty=[] }, ExportTable ), % We just do not want to have the filename of the currently processed module % among the includes: % Reconstructs the supposedly deduced module filename: BinModFilename = text_utils:string_to_binary( atom_to_list( ModuleName ) ++ ".erl" ), % Due to the removal of include duplicates, can be listed only up to once: NoModInc = lists:delete( BinModFilename, Inc ), % Only "normal", non-recursing exit of that longer function: M#module_info{ includes=NoModInc, function_exports=NewExportTable, last_line=LocForm, markers=NewMarkerTable }; %% %% Section 7.9: Catch-all, to ensure that we captured all possible forms. %% scan_forms( _AST=[ UnhandledForm | T ], M=#module_info{ unhandled_forms=UnhandledForms }, NextLocation, CurrentFileReference ) -> ast_utils:display_error( "Unhandled form '~p' not managed.~n", [ UnhandledForm ] ), %throw( { unhandled_form, UnhandledForm, { location, NextLocation }, % { file, CurrentFileReference } } ). LocForm = { NextLocation, UnhandledForm }, NewUnhandledForms = [ LocForm | UnhandledForms ], scan_forms( T, M#module_info{ unhandled_forms=NewUnhandledForms }, id_utils:get_next_sortable_id( NextLocation ), CurrentFileReference ); % The scan termination is expected to happen *only* when eof is found: scan_forms( _AST=[], _ModuleInfo, _NextLocation, CurrentFileReference ) -> ast_utils:raise_error( [ no_eof_found ], CurrentFileReference ); scan_forms( Unexpected, _ModuleInfo, _NextLocation, CurrentFileReference ) -> ast_utils:raise_error( { not_an_ast, Unexpected }, CurrentFileReference ). % Registers specified parse attribute regarding compilation. -spec register_compile_attribute( term(), compile_option_table(), scan_context() ) -> { compile_option_table(), [ located_form() ] }. % Full inlining requested: register_compile_attribute( _CompileInfo='inline', CompileTable, _Context ) -> % Overrides any previously existing inline entry: ?table:add_entry( inline, all, CompileTable ); % Regular inlining: register_compile_attribute( _CompileInfo={ 'inline', InlineValues }, CompileTable, Context ) when is_list( InlineValues ) -> ast_utils:check_inline_options( InlineValues, Context ), NewInlineValues = case ?table:lookup_entry( inline, CompileTable ) of { value, all } -> all; { value, InlineList } -> InlineValues ++ InlineList; key_not_found -> InlineValues end, ?table:add_entry( inline, NewInlineValues, CompileTable ); % Non-inlining, compile option with multiple values specified: register_compile_attribute( _CompileInfo={ CompileOpt, OptValues }, CompileTable, _Context ) when is_atom( CompileOpt ) andalso is_list( OptValues ) -> ?table:append_list_to_entry( CompileOpt, OptValues, CompileTable ); % Non-inlining, single compile option (hence not a list): register_compile_attribute( _CompileInfo={ CompileOpt, OptValue }, CompileTable, _Context ) when is_atom( CompileOpt ) -> ?table:append_to_entry( CompileOpt, OptValue, CompileTable ); register_compile_attribute( _CompileInfo=[], CompileTable, _Context ) -> CompileTable; register_compile_attribute( _CompileInfo=[ CpInfo | T ], CompileTable, Context ) -> NewCompileTable = register_compile_attribute( CpInfo, CompileTable, Context ), register_compile_attribute( T, NewCompileTable, Context ). % Processes the fields of a given record definition. % % Note: field names could be full expressions here, but only atoms are allowed % by the parser (dixit the erl_id_trans parse transform). % -spec scan_field_descriptions( [ ast_base:ast_element() ], ast_base:file_reference() ) -> ast_info:field_table(). scan_field_descriptions( FieldDescriptions, CurrentFileReference ) -> scan_field_descriptions( FieldDescriptions, CurrentFileReference, _FieldTable=[] ). % (helper) scan_field_descriptions( _FieldDescriptions=[], _CurrentFileReference, FieldTable ) -> % Preserve original field order: lists:reverse( FieldTable ); % Here no type or default value are specified for that field: scan_field_descriptions( _FieldDescriptions=[ { 'record_field', FirstLine, { atom, SecondLine, FieldName } } | T ], CurrentFileReference, FieldTable ) -> FieldDesc = { _FieldType=undefined, _DefaultValue=undefined, FirstLine, SecondLine }, NewFieldTable = [ { FieldName, FieldDesc } | FieldTable ], scan_field_descriptions( T, CurrentFileReference, NewFieldTable ); % Here only a type is specified for that field: scan_field_descriptions( _FieldDescriptions=[ { 'typed_record_field', { 'record_field', FirstLine, { atom, SecondLine, FieldName } }, FieldType } | T ], CurrentFileReference, FieldTable ) -> FieldDesc = { FieldType, _DefaultValue=undefined, FirstLine, SecondLine }, NewFieldTable = [ { FieldName, FieldDesc } | FieldTable ], scan_field_descriptions( T, CurrentFileReference, NewFieldTable ); % Here only a default value is specified for that field: scan_field_descriptions( _FieldDescriptions=[ { 'record_field', FirstLine, { atom, SecondLine, FieldName }, DefaultValue } | T ], CurrentFileReference, FieldTable ) -> FieldDesc = { _FieldType=undefined, DefaultValue, FirstLine, SecondLine }, NewFieldTable = [ { FieldName, FieldDesc } | FieldTable ], scan_field_descriptions( T, CurrentFileReference, NewFieldTable ); % Here a type and a default, immediate value are specified for that field: scan_field_descriptions( _FieldDescriptions=[ { 'typed_record_field', { 'record_field', FirstLine, { atom, SecondLine, FieldName }, DefaultValue }, FieldType } | T ], CurrentFileReference, FieldTable ) -> %ast_utils:display_debug( "Field default value: ~p.", [ DefaultValue ] ), %ast_utils:display_debug( "Field type: ~p.", [ FieldType ] ), FieldDesc = { FieldType, DefaultValue, FirstLine, SecondLine }, NewFieldTable = [ { FieldName, FieldDesc } | FieldTable ], scan_field_descriptions( T, CurrentFileReference, NewFieldTable ); scan_field_descriptions( _FieldDescriptions=[ UnexpectedDesc | _T ], CurrentFileReference, _FieldTable ) -> ast_utils:raise_error( [ unexpected_field_description, UnexpectedDesc ], CurrentFileReference ). % Checks that specified parse attribute name is legit. -spec check_parse_attribute_name( term(), form_context() ) -> parse_attribute_name(). check_parse_attribute_name( Name, _Context ) when is_atom( Name ) -> Name; check_parse_attribute_name( Other, Context ) -> ast_utils:raise_error( [ invalid_parse_attribute_name, Other ], Context ). % Checks that specified parse attribute name is legit. -spec check_parse_attribute_name( term() ) -> parse_attribute_name(). check_parse_attribute_name( Name ) -> check_parse_attribute_name( Name, _Context=undefined ). % Finalizes the marker table, to ensure that, in all cases, after a scan all % markers are (adequately) defined (even if no clause in the AST triggered their % specific setting). % -spec finalize_marker_table( ast_info:location(), marker_table() ) -> marker_table(). finalize_marker_table( EndMarkerLoc, MarkerTable ) -> % Useful for a later stack-based placement; we obtain a list of % {MarkerName,MarkerLoc} pairs, sorted by increasing locations: % OriginalPairs = get_ordered_marker_location_pairs( MarkerTable ), % No bound kept: [ { begin_marker, BeginMarkerLoc } | BoundlessPairs ] = OriginalPairs, % First a basic bound check: case BeginMarkerLoc < EndMarkerLoc of true -> ok; false -> throw( { end_before_begin, EndMarkerLoc, BeginMarkerLoc } ) end, % By design begin_marker has already been set in the table, and end_marker % will be registered now: % EndMarkerTable = ?table:add_new_entry( end_marker, EndMarkerLoc, MarkerTable ), % We have now to handle all cases about whether each of the other, % intermediate markers is already available or not, so that ultimately, all % of them are set, and in the expected order. % % This can be done only in an approximate manner, as the vanilla AST may % rely on a different - yet, compilation-wise, still legit - section order. % % The main rule to abide is that exports and imports shall precede % definitions. The rest is mostly a matter of convention. % % For that, we proceed first per marker name in ascending order (as defined % in ast_info:section_marker/0), and for each of them, in turn, we simply % unstack location pairs accordingly. % Marker shorthands: ModMarker = module_marker, InterMarkers = [ export_types_marker, export_functions_marker, import_functions_marker, definition_records_marker, definition_types_marker ], F = _FunDefMarker = definition_functions_marker, % The newer, simpler algorithm supposes that the only order that matters % between markers is that: % % - the begin marker comes first % - then the module marker (ModMarker) % - then the intermediate markers (InterMarkers), in no specific order % - then the definition marker (FunDefMarker, a.k.a. as F) % - then the end marker ModMarkerLoc = case ?table:lookup_entry( ModMarker, EndMarkerTable ) of key_not_found -> % An AST without a module definition will probably be a problem; % trying to mitigate it as we can: BeginMarkerLoc; { value, ModLoc } -> ModLoc end, { NewFLoc, DefaultLoc } = case ?table:lookup_entry( F, EndMarkerTable ) of key_not_found -> % F is not located yet, we will find a proper location then: case lists:reverse( BoundlessPairs ) of [] -> % Here no intermediate marker at all was defined, we thus % define two of them, a first (for the non-F markers) and a % second, at its right (for F): % NonFLoc = id_utils:get_sortable_id_between( ModMarkerLoc, EndMarkerLoc ), FLoc = id_utils:get_sortable_id_between( NonFLoc, EndMarkerLoc ), { FLoc, NonFLoc }; [ { _LastMarker, LastMarkerLoc } | _Others ] -> % Here we have at least one non-F location, that will be % used for all non-F markers that are not located yet, and % also to establish the location of F: % FLoc = id_utils:get_sortable_id_between( LastMarkerLoc, EndMarkerLoc ), { FLoc, LastMarkerLoc } end; { value, FLoc } -> DefLoc = id_utils:get_sortable_id_between( ModMarkerLoc, FLoc ), { FLoc, DefLoc } end, case NewFLoc > DefaultLoc of true -> ok; false -> throw( { wrong_location_order, DefaultLoc, NewFLoc } ) end, MarkerEntries = [ { ModMarker, ModMarkerLoc }, { F, NewFLoc } ], UpdatedMarkerTable = ?table:add_entries( MarkerEntries, EndMarkerTable ), add_missing_markers( InterMarkers, DefaultLoc, UpdatedMarkerTable ). % (helper) add_missing_markers( _Markers=[], _DefaultLoc, MarkerTable ) -> MarkerTable; add_missing_markers( _Markers=[ M | T ], DefaultLoc, MarkerTable ) -> NewMarkerTable = case ?table:has_entry( M, MarkerTable ) of true -> MarkerTable; false -> ?table:add_entry( M, DefaultLoc, MarkerTable ) end, add_missing_markers( T, DefaultLoc, NewMarkerTable ). % Returns a list of {MarkerName,MarkerLoc} pairs, sorted by increasing % locations. % % (helper) % -spec get_ordered_marker_location_pairs( marker_table() ) -> [ { ast_info:section_marker(), ast_info:location() } ]. get_ordered_marker_location_pairs( MarkerTable ) -> lists:keysort( _Index=2, ?table:enumerate( MarkerTable ) ).