Packages

Ceylan-Myriad, a generic-purpose Erlang toolbox, as an OTP application library here (see http://myriad.esperide.org)

Current section

Files

Jump to
myriad src ast_utils.erl
Raw

src/ast_utils.erl

% 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 <http://www.gnu.org/licenses/> and
% <http://www.mozilla.org/MPL/>.
%
% Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com]
% Creation date: Monday, January 1, 2018.
% Gathering of various convenient facilities to manage ASTs (Abstract Syntax
% Trees): direct bridge towards plain Erlang AST.
%
% Convenient to isolate processings from the current Erlang AST syntax, which
% could change over time (a bit like the erl_syntax standard module, albeit with
% a different set of conventions).
%
% See also:
%
% - the meta_utils module, for meta primitives less directly linked with syntax
%
% - the ast_scan module, to perform a full, strict traversal of an AST
%
-module(ast_utils).
% For the file_info record:
-include_lib("kernel/include/file.hrl").
% Directly obtained from the epp module:
-type include_path() :: [ file_utils:directory_name() ].
-type macro() :: atom() | { atom(), term() }.
-type source_encoding() :: 'latin1' | 'utf8'.
-type preprocessor_option() :: { 'includes', include_path() }
| { 'macros', [ macro() ] }
| { 'default_encoding', source_encoding() }
| 'extra'.
-export_type([ include_path/0, macro/0, source_encoding/0,
preprocessor_option/0 ]).
% For ast_transforms():
-include("ast_transform.hrl").
% Directly inspired from erl_lint:
% Description of a compilation-related issue (error or warning).
-type issue_description() :: term().
% Full information about a compilation-related issue.
%
% The module is the one emitting that issue (ex: erl_lint)
%
-type issue_info() :: { ast_base:line(), module(), issue_description() }.
% A warning regarding a source file, corresponding to a list of error
% informations.
%
-type issue_report() :: { file_utils:file_name(), [ issue_info() ] }.
-export_type([ issue_description/0, issue_info/0, issue_report/0 ]).
% Checking:
-export([ check_ast/1,
check_line/2,
check_module_name/1, check_module_name/2,
check_inline_options/1, check_inline_options/2,
check_arity/1, check_arity/2 ]).
% Converting:
-export([ erl_to_ast/1, erl_to_ast/2,
beam_to_ast/1, term_to_form/1, variable_names_to_ast/2,
string_to_form/1, string_to_form/2,
string_to_expressions/1, string_to_expressions/2,
string_to_value/1 ]).
% Displaying:
-export([ display_debug/1, display_debug/2,
display_trace/1, display_trace/2,
display_info/1, display_info/2,
display_warning/1, display_warning/2,
display_error/1, display_error/2,
display_fatal/1, display_fatal/2 ]).
% Signaling:
-export([ notify_warning/2,
raise_error/1, raise_error/2, raise_error/3,
raise_usage_error/3, raise_usage_error/4,
get_error_form/3, format_error/1 ]).
% Other:
-export([ write_ast_to_file/2 ]).
% Shorthands:
-type ast() :: ast_base:ast().
-type form() :: ast_base:form().
-type line() :: ast_base:line().
-type form_context() :: ast_base:form_context().
-type ast_transforms() :: ast_transform:ast_transforms().
% Checking section.
% Checks whether specified AST is legit: lints it.
-spec check_ast( ast() ) -> basic_utils:void().
check_ast( AST ) ->
%display_debug( "~p", [ AST ] ),
% Directly outputing the warnings or errors is generally useless; for
% example, in addition to:
%
% simple_parse_transform_target.erl:68: type void() undefined
%
% We would get: [{"simple_parse_transform_target.erl",
% [{68,erl_lint,{undefined_type,{void,0}}}]}]
% Finally interpret_issue_reports/1 directly used to output the issues;
% however some are legit (ex: 'type void() undefined'), so we must let them
% go through:
%
case erl_lint:module( AST ) of
{ ok, _Warnings=[] } ->
%display_trace(
% "(no warning or error emitted)~n" ),
ok;
{ ok, Warnings } ->
%display_error(
% "Warnings, reported as errors: ~p~n",
% [ Warnings ] ),
interpret_issue_reports( Warnings ),
%exit( warning_reported );
warning_reported;
{ error, Errors, _Warnings=[] } ->
%display_error( "Errors reported: ~p~n",
% [ Errors ] ),
interpret_issue_reports( Errors ),
%exit( error_reported );
error_reported;
{ error, Errors, Warnings } ->
%display_error( "Errors reported: ~p~n",
% [ Errors ] ),
interpret_issue_reports( Errors ),
%display_error(
% "Warnings, reported as errors: ~p~n", [ Warnings ] ),
interpret_issue_reports( Warnings ),
%exit( error_reported )
error_reported
end.
% Interprets specified list of issue reports.
-spec interpret_issue_reports( [ issue_report() ] ) -> basic_utils:void().
interpret_issue_reports( _IssueReports=[] ) ->
% Should never happen:
display_trace( "(no remark emitted)" );
% No need to further special-case the number of issue reports, as it is not
% meaningful (one may include an arbitrary long list):
%interpret_issue_reports( _IssueReports=[ OneIssueReport ] ) ->
% interpret_issue_report( OneIssueReport );
interpret_issue_reports( IssueReports ) ->
[ interpret_issue_report( R ) || R <- IssueReports ].
%text_utils:format( "~B remarks: ~s", [ length( IssueReports ),
% text_utils:strings_to_string( ReportStrings ) ] ).
% Interprets specific issue report.
-spec interpret_issue_report( issue_report() ) -> basic_utils:void().
interpret_issue_report( _IssueReport={ Filename, IssueInfos } ) ->
% We could normalise it instead, yet file_utils would become a dependency:
CanonicFilename = filename:basename( Filename ),
[ interpret_issue_info( CanonicFilename, E ) || E <- IssueInfos ].
%text_utils:format( "in file '~s': ~s", [ CanonicFilename,
% text_utils:strings_to_string( IssueStrings ) ] ).
% Interprets specific error description.
-spec interpret_issue_info( file_utils:file_name(), issue_info() ) ->
basic_utils:void().
interpret_issue_info( Filename,
_IssueInfo={ Line, DetectorModule, IssueDesc } ) ->
% Module is the detecting one, typically erl_lint:
%text_utils:format( "line #~B, module '~p', ~s", [ Line, Module,
% interpret_issue_description( IssueDesc ) ] ).
%text_utils:format( "line #~B: ~s", [ Line,
% interpret_issue_description( IssueDesc, DetectorModule ) ] ).
io:format( "~s:~B: ~s~n", [ Filename, Line,
interpret_issue_description( IssueDesc, DetectorModule ) ] ).
% Interprets specific issue description, detected by specified module.
%
% Note: full control is offered here to enrich this function at will, if wanted.
%
-spec interpret_issue_description( issue_description(),
basic_utils:module_name() ) -> string().
interpret_issue_description( IssueDescription, DectectorModule ) ->
%For example, the detector module may be erl_lint:
DectectorModule:format_error( IssueDescription ).
% Checks that specified line reference is legit.
-spec check_line( term(), form_context() ) -> line().
check_line( Line, _Context ) when is_integer( Line ) andalso Line >= 0 ->
Line;
check_line( Other, Context ) ->
% Not raise_error/2:
throw( { invalid_line, Other, Context } ).
% Checks that specified module name is legit.
-spec check_module_name( term() ) -> basic_utils:module_name().
check_module_name( Name ) ->
check_module_name( Name, _Context=undefined ).
% Checks that specified module name is legit.
-spec check_module_name( term(), form_context() ) -> basic_utils:module_name().
check_module_name( Name, _Context ) when is_atom( Name ) ->
Name;
check_module_name( Other, Context ) ->
raise_error( [ invalid_module_name, Other ], Context ).
% Checks that specified inline options are legit.
-spec check_inline_options( term() ) -> [ meta_utils:function_id() ].
check_inline_options( FunIds ) ->
check_inline_options( FunIds, _Context=undefined ).
% Checks that specified inline options are legit.
-spec check_inline_options( term(), form_context() ) ->
[ meta_utils:function_id() ].
check_inline_options( FunIds, Context ) when is_list( FunIds ) ->
ast_function:check_function_ids( FunIds, Context );
check_inline_options( Other, Context ) ->
raise_error( [ invalid_inline_options, Other ], Context ).
% Checks that specified (function or type) arity is legit.
-spec check_arity( term() ) -> arity().
check_arity( Arity ) ->
check_arity( Arity, _Context=undefined ).
% Checks that specified (function or type) arity is legit.
-spec check_arity( term(), form_context() ) -> arity().
check_arity( Arity, _Context ) when is_integer( Arity ) andalso Arity >= 0 ->
Arity;
check_arity( Other, Context ) ->
raise_error( [ invalid_arity, Other ], Context ).
% Conversion section.
% Reads specified Erlang source file (*.erl) and returns the corresponding AST,
% based on default preprocessor options.
%
% For example useful to debug a parse transform first separately from the
% compile pipe-line, relying here on the usual, convenient error management
% instead of having little informative messages like: 'undefined parse transform
% 'foobar'' as soon as a call to a non-existing module:function/arity is made.
%
-spec erl_to_ast( file_utils:file_name() ) -> ast().
erl_to_ast( ErlSourceFilename ) ->
erl_to_ast( ErlSourceFilename, _PreprocessorOptions=[] ).
% Reads specified Erlang source file (*.erl) and returns the corresponding AST,
% based on specified preprocessor (eep) options.
%
% For example useful to debug a parse transform first separately from the
% compile pipe-line, relying here on the usual, convenient error management
% instead of having little informative messages like: 'undefined parse transform
% 'foobar'' as soon as a call to a non-existing module:function/arity is made.
%
-spec erl_to_ast( file_utils:file_name(), [ preprocessor_option() ] ) -> ast().
erl_to_ast( ErlSourceFilename, PreprocessorOptions ) ->
case epp:parse_file( ErlSourceFilename, PreprocessorOptions ) of
{ error, Error } ->
throw( { parse_file_failed, ErlSourceFilename, Error } );
{ ok, AST } ->
AST
end.
% Reads the specified BEAM file (expected to be compiled with debug information)
% and returns the corresponding AST.
%
% Note that the filename must be a relative or absolute path pointing directly
% to the BEAM file (it is not searched through the code path).
%
-spec beam_to_ast( file:filename() ) -> ast().
beam_to_ast( BeamFilename ) ->
% We do not use functions from other Myriad modules here (ex: file_utils) as
% they are not expected to be built yet (they will be built with the myriad
% parse transform afterwards).
%
case file:read_link_info( BeamFilename ) of
{ ok, FileInfo } ->
#file_info{ type=regular } = FileInfo,
ok;
{ error, eloop } ->
% Probably a recursive symlink:
throw( { too_many_symlink_levels, BeamFilename } );
{ error, enoent } ->
throw( { non_existing_beam_file, BeamFilename } )
end,
% We could basically list all chunks, but we are only interested here in the
% abstract code:
% Everything:
%Chunks = [ abstract_code, attributes, compile_info, exports,
% labeled_exports, imports, indexed_imports, locals,
% labeled_locals, atoms ],
% Just the code AST:
Chunks = [ abstract_code ],
% Everything but the code AST:
% OtherChunks = [ attributes, compile_info, exports,
% labeled_exports, imports, indexed_imports, locals,
% labeled_locals, atoms ],
%Options = [ allow_missing_chunks ],
Options=[],
MyriadCryptoKeyFun = fun( init ) ->
ok;
( { debug_info, _Mode, _Module, _Filename } ) ->
% Refer to GNUmakevars.inc:
_Key="Ceylan-Myriad";
( clear ) ->
ok
end,
ok = beam_lib:crypto_key_fun( MyriadCryptoKeyFun ),
case beam_lib:chunks( BeamFilename, Chunks, Options ) of
{ ok, { _Module, [ { abstract_code, { _RawAbstractV1,
AbstractCode } } ] } } ->
%display_debug( "Module = ~p.", [ Module ] ),
AbstractCode;
{ error, beam_lib, Reason } ->
throw( { beam_reading_failed, Reason } )
end.
% Section to manage ASTs and forms.
% Converts the specified Erlang term (ex: the float '42.0') into a corresponding
% form (ex: '{ float, _Line=0, 42.0 }').
%
-spec term_to_form( term() ) -> form().
term_to_form( Term ) ->
case erl_syntax:abstract( Term ) of
% Either the doc or the type information for erl_syntax:abstract/1 is
% incorrect:
%badarg ->
% throw( { term_abstraction_failed, Term } );
SyntaxTree ->
% Could be used with erl_syntax:is_tree/1:
% case erl_syntax:revert( SyntaxTree ) of...
erl_syntax:revert( SyntaxTree )
end.
% Converts a list of names of variables into the corresponding AST.
%
% Ex: if wanting to specify '[ V1, Alpha, A ]', we have: variable_names_to_ast(
% [ "V1", "Alpha", "A" ], _Line=0 ) = [ {cons,0, {var,0,'V1'},
% {cons,0,{var,0,'Alpha'}, {cons,0,{var,0,'A'}, {nil,0} } } } ]
%
-spec variable_names_to_ast( [ string() ], line() ) -> ast().
variable_names_to_ast( VariableNames, Line ) ->
% Could be done directly recursively by incrementally 'consing' reversed
% list.
NameListString = "[ " ++ text_utils:join( ", ", VariableNames ) ++ " ].",
string_to_expressions( NameListString, Line ).
% Converts the specified source code of a form (as a string) into its
% corresponding abstract form (assuming being in line #1).
%
% Ex: string_to_form( "f() -> hello_world." ) returns
% { function, 1, f, 0, [ { clause, 1, [], [], [ {atom,1,hello_world} ] } ] }
%
-spec string_to_form( string() ) -> form().
string_to_form( FormString ) ->
string_to_form( FormString, _Loc=1 ).
% Converts the specified source code of a form (i.e., a string) into its
% corresponding abstract form.
%
% Ex: string_to_form( "f() -> hello_world.", 42 ) returns
% { function, 1, f, 0, [ { clause, 42, [], [], [ {atom,1,hello_world} ] } ] }
%
-spec string_to_form( string(), ast_base:file_loc() ) -> form().
string_to_form( FormString, Location ) ->
% First get Erlang tokens from that string:
Tokens = case erl_scan:string( FormString, Location ) of
% Ex: [{atom,1,f},{'(',1},{')',1},{'->',1},{atom,1,hello_world},{dot,1}]
{ ok, Toks, _EndLocation } ->
%display_debug( "Tokens: ~p", [ Toks ] ),
Toks;
ErrorTok ->
throw( { form_tokenizing_error, FormString, ErrorTok } )
end,
% Tokens to erl_parse trees:
case erl_parse:parse_form( Tokens ) of
{ ok, ParseTree } ->
ParseTree;
ErrorPar ->
throw( { form_parsing_error, FormString, ErrorPar } )
end.
% Converts the specified source code of a list of expressions (i.e., a string)
% into its corresponding AST (assuming being in line #1).
%
% Ex: string_to_expressions( "[ { a, 1 }, foobar ]" ) returns
% [ { cons, 1, { tuple, 1, [ {atom,1,a}, {integer,1,1} ] },
% { cons, 1, {atom,1,foobar}, {nil,1} } } ]
%
-spec string_to_expressions( string() ) -> ast().
string_to_expressions( ExpressionString ) ->
string_to_expressions( ExpressionString, _Loc=1 ).
% Converts the specified source code of a term (i.e., a string) and a location
% into the corresponding abstract form.
%
% Ex: string_to_expressions( "[ { a, 1 }, foobar ]", _Loc=42 ) returns
% [ { cons, 42, { tuple, 42, [ {atom,42,a}, {integer,42,1} ] },
% { cons, 42, {atom,42,foobar}, {nil,42} } } ]
%
-spec string_to_expressions( string(), ast_base:file_loc() ) -> ast().
string_to_expressions( ExpressionString, Location ) ->
% First get Erlang tokens from that string:
Tokens = case erl_scan:string( ExpressionString, Location ) of
% Ex: [ {'[',42}, {'{',42}, {atom,42,a}, {',',42}, {integer,42,1},
% {'}',42}, {',',42}, {atom,42,foobar}, {']',42} ]
{ ok, Toks, _EndLocation } ->
%display_debug( "Tokens: ~p", [ Toks ] ),
Toks;
ErrorTok ->
throw( { expression_tokenizing_error, ExpressionString, ErrorTok } )
end,
% Tokens to erl_parse trees:
case erl_parse:parse_exprs( Tokens ) of
{ ok, ParseTree } ->
ParseTree;
ErrorPar ->
throw( { expression_parsing_error, ExpressionString, ErrorPar } )
end.
% Converts the specified source code of a term (i.e., a string) into its
% corresponding value.
%
% Ex: string_to_value( "[ {tiger,[lion,leopard]} ]" ) returns the
% [{tiger,[lion,leopard]}] term.
%
-spec string_to_value( string() ) -> term().
string_to_value( ExpressionString ) ->
% We automatically add the necessary final dot:
[ Expr ] = string_to_expressions( ExpressionString ++ "." ),
{ value, Result, _NewBindings } = erl_eval:expr( Expr, _Bindings=[] ),
Result.
% Subsection for trace outputs that are specific to parse-transforms.
% Displays specified text as debug.
-spec display_debug( text_utils:ustring() ) -> basic_utils:void().
display_debug( String ) ->
io:format( "[debug] ~s~n", [ String ] ).
% Displays specified formatted text as debug.
-spec display_debug( text_utils:format_string(), [ term() ] ) ->
basic_utils:void().
display_debug( FormatString, Values ) ->
display_debug( io_lib:format( FormatString, Values ) ).
% Displays specified text as trace.
-spec display_trace( text_utils:ustring() ) -> basic_utils:void().
display_trace( String ) ->
io:format( "[trace] ~s~n", [ String ] ).
% Displays specified formatted text as trace.
-spec display_trace( text_utils:format_string(), [ term() ] ) ->
basic_utils:void().
display_trace( FormatString, Values ) ->
display_trace( io_lib:format( FormatString, Values ) ).
% Displays specified text as info.
-spec display_info( text_utils:ustring() ) -> basic_utils:void().
display_info( String ) ->
io:format( "[info] ~s~n", [ String ] ).
% Displays specified formatted text as info.
-spec display_info( text_utils:format_string(), [ term() ] ) ->
basic_utils:void().
display_info( FormatString, Values ) ->
display_info( io_lib:format( FormatString, Values ) ).
% Displays specified text as warning.
-spec display_warning( text_utils:ustring() ) -> basic_utils:void().
display_warning( String ) ->
io:format( "[warning] ~s~n", [ String ] ).
% Displays specified formatted text as warning.
-spec display_warning( text_utils:format_string(), [ term() ] ) ->
basic_utils:void().
display_warning( FormatString, Values ) ->
display_warning( io_lib:format( FormatString, Values ) ).
% Displays specified text as error.
-spec display_error( text_utils:ustring() ) -> basic_utils:void().
display_error( String ) ->
io:format( "~n[error] ~s~n", [ String ] ).
% Displays specified formatted text as error.
-spec display_error( text_utils:format_string(), [ term() ] ) ->
basic_utils:void().
display_error( FormatString, Values ) ->
display_error( io_lib:format( FormatString, Values ) ).
% Displays specified text as fatal.
-spec display_fatal( text_utils:ustring() ) -> basic_utils:void().
display_fatal( String ) ->
io:format( "[fatal] ~s~n", [ String ] ).
% Displays specified formatted text as fatal.
-spec display_fatal( text_utils:format_string(), [ term() ] ) ->
basic_utils:void().
display_fatal( FormatString, Values ) ->
display_fatal( io_lib:format( FormatString, Values ) ).
% Notifies a warning, with specified context.
-spec notify_warning( [ term() ], form_context() ) -> basic_utils:void().
notify_warning( Elements, Context ) ->
case get_elements_with_context( Elements, Context ) of
% Supposedly a string:
[ SingleElement ] when is_list( SingleElement ) ->
display_warning( "~s", [ SingleElement ] );
AllElements ->
display_warning( "~p", [ AllElements ] )
end.
% Raises a (compile-time, rather ad hoc) error when applying a parse transform,
% to stop the build on failure and report the actual error, thanks to the
% specified term (often, a list of error elements).
%
% Used to be a simple throw, but then for parse transforms the error message was
% garbled in messages like:
%
% """
% internal error in lint_module;
% crash reason: function_clause
%
% in function erl_lint:'-compiler_options/1-lc$^0/1-0-'/1
% called as erl_lint:'-compiler_options/1-lc$^0/1-0-'({
% table_type_defined_more_than_once,{line,12},foo_hashtable,bar_hashtable})
%
% Note: this function is used to report errors detected by Myriad itself (not by
% the Erlang toolchain).
%
-spec raise_error( term() ) -> no_return().
raise_error( ErrorTerm ) ->
%throw( ErrorTerm )
%display_error( "~p", [ ErrorTerm ] ),
% Does not add any information (just non-relevant erl_parse, epp
% etc. state):
%
%erlang:exit( { ErrorTerm, erlang:get_stacktrace() } ).
%erlang:exit( ErrorTerm ).
% Possibly a list of elements:
raise_error( ErrorTerm, _Context=undefined ).
% Raises an error, with specified context, thanks to the specified term (often,
% a list of error elements), from the Myriad layer.
%
% Ex: raise_error( [ invalid_module_name, Other ], _Context=112 ) shall
% result in throwing { invalid_module_name, Other, { line, 112 } }.
%
% Note: this function is used to report errors detected by Myriad itself (not by
% the Erlang toolchain).
%
-spec raise_error( term(), basic_utils:maybe( form_context() ) ) -> no_return().
raise_error( ErrorTerm, Context ) ->
raise_error( ErrorTerm, Context, _OriginLayer="Myriad" ).
% Raises an error, with specified context, from the specified layer (expected to
% be above Myriad).
%
% Ex: raise_error( [ invalid_module_name, Other ], _Context=112,
% _OriginLayer="FooLayer" ) shall result in throwing { invalid_module_name,
% Other, { line, 112 } }.
%
-spec raise_error( term(), basic_utils:maybe( ast_base:source_context() ),
basic_utils:layer_name() ) -> no_return();
( text_utils:ustring(), ast_transforms(), line() ) ->
no_return().
raise_error( Message, #ast_transforms{ transformed_module_name=ModName },
Line ) ->
io:format( "~s.erl:~B: ~s~n", [ ModName, Line, Message ] ),
halt( 5 );
raise_error( Message, Context, OriginLayer ) ->
%trace_utils:debug_fmt( "Message: ~p, Context: ~p, Layer: ~p",
% [ Message, Context, OriginLayer ] ),
Prefix = case Context of
undefined ->
"Error";
{ Filename, Line } ->
io_lib:format( "~s:~B: error", [ Filename, Line ] );
Filename when is_binary( Filename ) ->
io_lib:format( "Error in ~s", [ Filename ] );
Line when is_integer( Line ) ->
io_lib:format( "Error at line ~B", [ Line ] );
Other ->
io_lib:format( "Error in unexpected context ~p", [ Other ] )
end,
% Used to rely on display_error/1, yet we want to respect the standard error
% report format, so:
%
io:format( "~s raised while performing ~s-level transformations:"
"~n ~p~n", [ Prefix, OriginLayer, Message ] ),
DisplayStacktrace = true,
%DisplayStacktrace = false,
try
case DisplayStacktrace of
true ->
% To get a stack trace:
throw( { ast_transformation_failed_in, OriginLayer } );
false ->
ok
end
catch
% Class is 'throw', R is what we just threw:
% Pre-21.0 code:
%_C:_R ->
%
% % Removing useless {ast_utils,raise_error,2,...:
% ActualStackTrace = tl( erlang:get_stacktrace() ),
% Post-21.0 code:
_C:_R:StackTrace ->
% Used to confirm that, in some cases, the direct, original
% stacktrace returned by the Erlang VM seems limited to the N=8
% latest calls (deeper ones being thus lacking, unfortunately):
%
%io:format( "Full stack trace:~n~p~n", [ StackTrace ] ),
% Removing useless {ast_utils,raise_error,2,...:
ActualStackTrace = tl( StackTrace ),
StackElements =
interpret_stack_trace( ActualStackTrace, _Acc=[], _Count=1 ),
% These are Myriad-internal information, generally of no use to
% understand the problem regarding the code being compiled:
%
display_debug( "Transformation error happened in "
"(latest calls first):~n~s", [ StackElements ] )
end,
% Would not interrupt the processing of the AST anyway:
%throw( list_to_tuple( AllElements ) );
%{ error, AllElements };
%exit( AllElements );
halt( 5 ).
% (helper)
interpret_stack_trace( _StackTrace=[], Acc, _Count ) ->
lists:reverse( Acc );
interpret_stack_trace( _StackTrace=[ { Module, FunName, Arity,
_FileLoc=[ { file, Path }, { line, Line } ] } | T ],
Acc, Count ) ->
Text = io_lib:format( " [~B] ~s:~s/~B [~s, line ~B]~n",
[ Count, Module, FunName, Arity, Path, Line ] ),
interpret_stack_trace( T, [ Text | Acc ], Count+1 );
interpret_stack_trace( _StackTrace=[ H | T ], Acc, Count ) ->
Text = io_lib:format( "~p~n", [ H ] ),
interpret_stack_trace( T, [ Text | Acc ], Count+1 ).
% Raises a (compile-time, rather ad hoc) user-related error, with specified
% source context, to stop the build on failure and report adequately the actual
% error to the user.
%
-spec raise_usage_error( text_utils:format_string(), text_utils:format_values(),
file_utils:filename() ) -> no_return().
raise_usage_error( ErrorFormatString, ErrorValues, Filename ) ->
raise_usage_error( ErrorFormatString, ErrorValues, Filename, _Line=0 ).
% Raises a (compile-time, rather ad hoc) user-related error, with specified
% source context, to stop the build on failure and report adequately the actual
% error to the user.
%
-spec raise_usage_error( text_utils:format_string(), text_utils:format_values(),
file_utils:filename(), ast_base:line() ) -> no_return().
raise_usage_error( ErrorFormatString, ErrorValues, Filename,
_Line=undefined ) ->
raise_usage_error( ErrorFormatString, ErrorValues, Filename,
_ActualLine=0 );
raise_usage_error( ErrorFormatString, ErrorValues, ModuleName, Line )
when is_atom( ModuleName ) ->
Filename = io_lib:format( "~s.erl", [ ModuleName ] ),
raise_usage_error( ErrorFormatString, ErrorValues, Filename, Line );
raise_usage_error( ErrorFormatString, ErrorValues, Filename, Line ) ->
ErrorString = io_lib:format( ErrorFormatString, ErrorValues ),
io:format( "~s:~B: ~s~n", [ Filename, Line, ErrorString ] ),
% Almost the only way to stop the processing of the AST:
halt( 5 ).
% Returns an AST form in order to raise a (compile-time, standard) error when
% applying a parse transform, to stop the build on failure and report the actual
% error.
%
% The specified error term will be transformed by the specified module into a
% (textual) error message (see format_error/1), and then will be reported as
% originating from the specified line in the source file of the module being
% compiled.
%
-spec get_error_form( basic_utils:error_reason(), basic_utils:module_name(),
line() ) -> form().
get_error_form( ErrorTerm, FormatErrorModule, Line ) ->
% Actually the most standard way of reporting an error seems to insert a
% dedicated form in the AST.
% May ultimately report (thanks to ?MODULE:format_error/1), when compiling a
% foobar module and if:
%
% - Line is 15
%
% - 'apply( FormatErrorModule, format_error, [ ErrorTerm ] )' is "my error
% message":
%
% the following error message: "foobar:15: my error message".
%
{ error, { Line, FormatErrorModule, ErrorTerm } }.
% This function (whose name is standard, conventional) is to be defined on a
% per-module basis (typically in the module defining the parse transform being
% applied) and allows to convert error terms (that are, here, related to
% parse-transforms) into textual messages that can be output by the build chain.
%
-spec format_error( basic_utils:error_reason() ) -> string().
format_error( ErrorTerm ) ->
% Of course this is just an example:
text_utils:format( "my ast_utils error reported: ~s", [ ErrorTerm ] ).
% Returns error/warning elements including specified context.
%
% (helper)
%
-spec get_elements_with_context( [ term() ], ast_base:form_context() ) ->
[ term() ].
get_elements_with_context( Elements, _Context=undefined ) ->
Elements;
get_elements_with_context( Elements, _Context={ FilePath, Line } )
when is_binary( FilePath ) andalso is_integer( Line ) ->
%Elements ++ [ { file, text_utils:binary_to_string( FilePath ) },
% { line, Line } ];
% We mimic the default error formatting so that tools (like IDE) have a
% chance to automatically point to the right location in the sources:
%
Prefix = io_lib:format( "~s:~B: ",
[ text_utils:binary_to_string( FilePath ), Line ] ),
[ Prefix | Elements ];
get_elements_with_context( Elements, _Context=Line ) when is_integer( Line ) ->
Elements ++ [ { line, Line } ];
get_elements_with_context( Elements, _Context=FilePath )
when is_binary( FilePath ) ->
Prefix = io_lib:format( "~s:0: ",
[ text_utils:binary_to_string( FilePath ) ] ),
[ Prefix | Elements ];
get_elements_with_context( Elements, Context ) ->
% No list_utils module used from this module:
Elements ++ [ Context ].
% Writes specified AST into specified (text) file.
%
% Useful for example to determine differences between ASTs.
%
-spec write_ast_to_file( ast(), file_utils:file_name() ) -> basic_utils:void().
write_ast_to_file( AST, Filename ) ->
% Note: we cannot actually use file_utils, which is not a prerequisite of
% the 'Myriad' parse transform:
% We overwrite any pre-existing file:
{ ok, File } = file:open( Filename, [ write, raw ] ),
[ ok = file:write( File, io_lib:format( "~p~n", [ F ] ) ) || F <- AST ],
ok = file:close( File ).