% Copyright (C) 2014-2019 Olivier Boudeville % % This file is part of the Ceylan-WOOPER 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: Wednesday, December 24, 2014 % Centralizes, on behalf of the WOOPER parse transform, the support for method % management. % -module(wooper_method_management). -export([ manage_methods/1, get_wooper_transform_table/0, % Sharable, top-level transformers: clause_transformer/2, body_transformer/2, call_transformer/4, if_transformer/3, case_transformer/4, simple_receive_transformer/3, receive_with_after_transformer/5, try_transformer/6, catch_transformer/3, get_blank_transformation_state/0, get_blank_transformation_state/1, ensure_exported/2, ensure_exported_at/2, ensure_all_exported_in/2, methods_to_functions/5, take_spec_into_account/5, get_info_from_clause_spec/3, check_clause_spec/5, check_state_argument/3, function_nature_to_string/1, function_to_request_info/2, request_to_function_info/2, function_to_oneway_info/2, oneway_to_function_info/2, function_to_static_info/2, static_to_function_info/2, format_log/2 ]). % For the function_info record: -include("ast_info.hrl"). % For the ast_transforms record: -include("ast_transform.hrl"). % for the rec_guard macro: -include("ast_utils.hrl"). % For the class_info record: -include("wooper_info.hrl"). % The (WOOPER-level) nature of a given Erlang function. % % ('throw' designates a function whose all clauses throw an exception, and as % such may be temporarily not identified in terms of nature) % -type function_nature() :: 'constructor' | 'destructor' | 'request' | 'oneway' | 'static' | 'function' | 'throw'. % Shorthands: -type function_info() :: ast_info:function_info(). -type function_id() :: meta_utils:function_id(). -type marker_table() :: ast_info:section_marker_table(). -type location() :: ast_base:form_location(). -type function_table() :: ast_info:function_table(). -type line() :: ast_base:line(). -type ast_transforms() :: ast_transform:ast_transforms(). -type ast_transform_table() :: ast_transform:ast_transform_table(). -type ast_expression() :: ast_expression:ast_expression(). -type ast_body() :: ast_clause:ast_body(). -type ast_clause() :: ast_clause:ast_clause(). -type ast_if_clause() :: ast_clause:ast_if_clause(). -type ast_case_clause() :: ast_clause:ast_case_clause(). -type compose_pair() :: wooper_parse_transform:compose_pair(). -type request_info() :: wooper_info:request_info(). -type oneway_info() :: wooper_info:oneway_info(). -type static_info() :: wooper_info:static_info(). -type request_table() :: wooper_info:request_table(). -type oneway_table() :: wooper_info:oneway_table(). -type static_table() :: wooper_info:static_table(). -type method_qualifiers() :: wooper:method_qualifiers(). % Comment to disable logging (too detailed, almost untractable even to display): %-define( log_traversal, ). -ifdef(log_traversal). -define(debug,trace_utils:debug). -define(debug_fmt,trace_utils:debug_fmt). -define(trace,trace_utils:trace). -define(trace_fmt,trace_utils:trace_fmt). -else. % log_traversal -define(debug,trace_utils:void). -define(debug_fmt,trace_utils:void_fmt). -define(trace,trace_utils:void). -define(trace_fmt,trace_utils:void_fmt). -endif. % log_traversal % Implementation notes: % To determine whether a function is a method and, if yes, of what kind it is % (request, oneway, etc.), we have to traverse recursively at least one of its % clauses until finding at least one final expression in order to examine, check % and possibly transform any method terminator found. We chose to traverse all % clauses in order to catch any inconsistency in the user code. % % Recursing in nested local calls is not needed here, as by convention the % method terminators should be local to the method body. % % For that we can either follow exactly the AST legit structure that we can % foresee (typically based on http://erlang.org/doc/apps/erts/absform.html), at % the risk of rejecting correct code - should our traversal be not perfect, or % we can "blindly" rewrite calls for example corresponding to % wooper:return_state_result( S, R ) as { S, R } (and check we have no % incompatible method terminators). % % We preferred initially here the latter solution (lighter, simpler, safer), but % we had already a full logic (in Myriad's meta) to traverse and transform ASTs % or part thereof, so we relied on it for the best, now that it has been % enriched so that it can apply and maintain a transformation state. % % A combination of ast_transform:get_remote_call_transform_table/1 with % ast_expression:transform_expression/2 would have been close to what we needed, % yet we do not want to replace a *call* by another, we want it to be replaced % by a tuple creation, i.e. an expression. Moreover we need to be stateful, to % remember any past method terminator and check their are consistent with the % newer ones being found. % % Finally, a difficulty is that normal functions by nature do not use % terminators, whereas, when handling a method, for example a clause not using % terminators among legit clauses shall be caught, despite the lack of an % explicit trigger. Another difficulty is to handle throws, which are compatible % with all function natures, and thus do not teach anything - even if they can % constitue the sole clauses of a given function (any type spec thereof is then % needed to remove this ambiguity). % So, here are the WOOPER traversal rules to cover these needs: % % - as always with Myriad, it is a depth-first, integral, single-pass traversal; % however we do not attempt anymore to merge this WOOPER pass with the Myriad % one, as for example calls into case patterns are irrelevant here % % - initially, the function nature is 'undefined' % % - we traverse all bodies of all function clauses % - when traversing a body, only its last expression (if any) is of interest for % WOOPER (all preceding expressions are thus skipped, WOOPER-wise) % % - as soon an element is found (ex: method terminator, throw), the % corresponding function nature supersedes the 'undefined' status % % - all (non-undefined) natures (resulting from method terminators) supersede % 'throw' % % - returning from a transformation with an 'undefined' nature implies the new % nature is 'function' (there is no method terminator nor throw in any nested % branch; it is a "by default" property) % % - the traversal does not recurse in calls, as by design a WOOPER terminator is % to be the most global one (i.e. the first one in any function composition) - % however deeply nested in branching clauses % Said otherwise, the traversal starts with the function clauses, which are % managed by our clause_transformer/2, in charge of calling body_transformer/3. % Should no method terminator nor throw be found, the function nature is still % at 'undefined', so we assign it to 'function'. % Extracts the methods found in the specified function table, transforms them, % and interprets that information to update the specified class information. % % Returns an updated pair thereof. % -spec manage_methods( compose_pair() ) -> compose_pair(). manage_methods( { CompleteFunctionTable, ClassInfo=#class_info{ class={ Classname, _ClassForm }, requests=RequestTable, oneways=OnewayTable, statics=StaticTable, markers=MarkerTable } } ) -> AllFunEntries = table:enumerate( CompleteFunctionTable ), % We used to filter-out the WOOPER builtin functions (based on % wooper_info:get_wooper_builtins/0), as they would not teach WOOPER % anything about the class at hand, yet we want them to be correctly % identified by WOOPER (they are of different natures); moreover, not all of % them are already in a final form (ex: they still use method terminators), % so their processing shall not be skipped. ExportLoc = ast_info:get_default_export_function_location( MarkerTable ), % Determined once for all: WOOPERExportSet = wooper:get_exported_functions_set(), % FunctionTable starts from scratch as all functions are to be found in % AllFunEntries: % { NewFunctionTable, NewRequestTable, NewOnewayTable, NewStaticTable } = sort_out_functions( AllFunEntries, _FunctionTable=table:new(), RequestTable, OnewayTable, StaticTable, Classname, ExportLoc, WOOPERExportSet ), % Split as { Functions, Methods }: { NewFunctionTable, ClassInfo#class_info{ requests=NewRequestTable, oneways=NewOnewayTable, statics=NewStaticTable } }. % Transforms and categorises each of the specified functions according to its % real nature (ex: a given Erlang function may actually be a WOOPER oneway). % sort_out_functions( _FunEntries=[], FunctionTable, RequestTable, OnewayTable, StaticTable, _Classname, _ExportLoc, _WOOPERExportSet ) -> { FunctionTable, RequestTable, OnewayTable, StaticTable }; % Checks that all sorted functions have an actual implementation: sort_out_functions( _FunEntries=[ { FunId, #function_info{ clauses=[], spec=Spec } } | _T ], _FunctionTable, _RequestTable, _OnewayTable, _StaticTable, Classname, _ExportLoc, _WOOPERExportSet ) when Spec =/= undefined -> wooper_internals:raise_usage_error( "function ~s/~B has a type specification, yet has never been defined.", pair:to_list( FunId ), Classname ); sort_out_functions( _FunEntries=[ { FunId, FunInfo=#function_info{ clauses=OriginalClauses, spec=Spec } } | T ], FunctionTable, RequestTable, OnewayTable, StaticTable, Classname, ExportLoc, WOOPERExportSet ) -> %?debug_fmt( "Examining Erlang function ~s/~B", % pair:to_list( FunId ) ), % We used to infer the function nature based on its first clause, and then % to make a custom full traversal to transform method terminators. % % Now we reuse the Myriad ast_transforms instead, and perform everything % (guessing/checking/transforming) in one pass: % { NewClauses, FunNature, Qualifiers } = manage_method_terminators( OriginalClauses, FunId, Classname, WOOPERExportSet ), %?debug_fmt( "~p is a ~s whose qualifiers are ~p.", % [ FunId, function_nature_to_string( FunNature ), % Qualifiers ] ), NewFunInfo = FunInfo#function_info{ clauses=NewClauses }, % Last chance to determine the actual nature of a function reported as % 'throw': it may have a type spec to remove ambiguity; any spec used also % to check consistency with the guessed elements: % { FinalNature, FinalQualifiers } = take_spec_into_account( Spec, FunId, FunNature, Qualifiers, Classname ), % Stores the result in the right category and recurses: case FinalNature of function -> NewFunctionTable = table:add_new_entry( FunId, NewFunInfo, FunctionTable ), sort_out_functions( T, NewFunctionTable, RequestTable, OnewayTable, StaticTable, Classname, ExportLoc, WOOPERExportSet ); request -> check_state_argument( NewClauses, FunId, Classname ), ExportedFunInfo = ensure_exported_at( NewFunInfo, ExportLoc ), RequestInfo = function_to_request_info( ExportedFunInfo, FinalQualifiers ), NewRequestTable = table:add_new_entry( FunId, RequestInfo, RequestTable ), sort_out_functions( T, FunctionTable, NewRequestTable, OnewayTable, StaticTable, Classname, ExportLoc, WOOPERExportSet ); oneway -> check_state_argument( NewClauses, FunId, Classname ), ExportedFunInfo = ensure_exported_at( NewFunInfo, ExportLoc ), OnewayInfo = function_to_oneway_info( ExportedFunInfo, FinalQualifiers ), NewOnewayTable = table:add_new_entry( FunId, OnewayInfo, OnewayTable ), sort_out_functions( T, FunctionTable, RequestTable, NewOnewayTable, StaticTable, Classname, ExportLoc, WOOPERExportSet ); static -> ExportedFunInfo = ensure_exported_at( NewFunInfo, ExportLoc ), StaticInfo = function_to_static_info( ExportedFunInfo, FinalQualifiers ), NewStaticTable = table:add_new_entry( FunId, StaticInfo, StaticTable ), sort_out_functions( T, FunctionTable, RequestTable, OnewayTable, NewStaticTable, Classname, ExportLoc, WOOPERExportSet ) end; sort_out_functions( _Functions=[ #function_info{ name=FunName, arity=Arity } | _T ], _FunctionTable, _RequestTable, _OnewayTable, _StaticTable, Classname, _ExportLoc, _WOOPERExportSet ) -> % Error raised directly, could be appended to the class_info.errors: wooper_internals:raise_usage_error( "no clause found for ~s/~B; " "function exported yet not defined?", [ FunName, Arity ], Classname ). % Checks that the method spec (if any) corresponds indeed to the right nature of % function, i.e. relies on the right method terminators with the right % qualifiers, and returns a corresponding pair. % % (helper) % -spec take_spec_into_account( maybe( ast_info:located_form() ), function_id(), function_nature(), method_qualifiers(), wooper:classname() ) -> { function_nature(), method_qualifiers() }. % Function specs are generally optional, yet are useful in all cases, and even % needed in some ones (ex: what is the actual nature of a function whose all % clauses throw?) % take_spec_into_account( _LocSpec=undefined, FunId, _FunNature=throw, _Qualifiers, Classname ) -> wooper_internals:raise_usage_error( "all clauses of ~s/~B throw an exception; as a result this " "function can be of any nature. Please define a type specification for " "that function in order to remove this ambiguity " "(ex: use request_return/1 to mark it as a request).", pair:to_list( FunId ), Classname ); % Special case for a function nature detected as 'throw': any information % collected from its spec is accepted as is (provided there is only one spec). % take_spec_into_account( _LocSpec={ _Loc, { attribute, _, spec, { FunId, [ ClauseSpec ] } } }, FunId, _FunNature=throw, _Qualifiers, Classname ) -> get_info_from_clause_spec( ClauseSpec, FunId, Classname ); % Throw function with a spec comprising multiple clauses is not supported: take_spec_into_account( _LocSpec={ _Loc, { attribute, Line, spec, { FunId, _ClauseSpecs } } }, _FunId, _FunNature=throw, _Qualifiers, Classname ) -> wooper_internals:raise_usage_error( "all clauses of the function ~s/~B throw an exception; as a result this " "function can be of any nature. Its type specification shall however " "comprise a single clause to remove this ambiguity.", pair:to_list( FunId ), Classname, Line ); % No spec defined, non-throw, so input elements are accepted as such: take_spec_into_account( _LocSpec=undefined, _FunId, FunNature, Qualifiers, _Classname ) -> { FunNature, Qualifiers }; % Spec available for a non-throw: take_spec_into_account( _LocSpec={ _Loc, { attribute, _, spec, { FunId, ClauseSpecs } } }, _FunId, FunNature, Qualifiers, Classname ) -> [ check_clause_spec( C, FunNature, Qualifiers, FunId, Classname ) || C <- ClauseSpecs ], % If check not failed, approved, so: { FunNature, Qualifiers }. % Returns a { FunctionNature, Qualifiers } pair deduced from specified clause % spec. % get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, request_return, [ _RType ] } ] }, _FunId, _Classname ) -> { request, [] }; get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, request_return, RTypes } ] }, FunId, Classname ) -> wooper_internals:raise_usage_error( "wrong arity of the specified WOOPER return type for the spec of ~s/~B: " "it should be request_return/1 (not request_return/~B).", pair:to_list( FunId ) ++ [ length( RTypes ) ], Classname, Line ); get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, const_request_return, [ _RType ] } ] }, _FunId, _Classname ) -> { request, [ const ] }; get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_request_return, RTypes } ] }, FunId, Classname ) -> wooper_internals:raise_usage_error( "wrong arity of the specified WOOPER return type for the spec of ~s/~B: " "it should be const_request_return/1 (not const_request_return/~B).", pair:to_list( FunId ) ++ [ length( RTypes ) ], Classname, Line ); get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, oneway_return, [] } ] }, _FunId, _Classname ) -> { oneway, [] }; get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, oneway_return, RTypes } ] }, FunId, Classname ) -> wooper_internals:raise_usage_error( "wrong arity of the specified WOOPER return type for the spec of ~s/~B: " "it should be oneway_return/0 (not oneway_return/~B).", pair:to_list( FunId ) ++ [ length( RTypes ) ], Classname, Line ); get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, const_oneway_return, [] } ] }, _FunId, _Classname ) -> { oneway, [ const ] }; get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_oneway_return, RTypes } ] }, FunId, Classname ) -> wooper_internals:raise_usage_error( "wrong arity of the specified WOOPER return type for the spec of ~s/~B: " "it should be const_oneway_return/0 (not const_oneway_return/~B).", pair:to_list( FunId ) ++ [ length( RTypes ) ], Classname, Line ); get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, static_return, [ _RType ] } ] }, _FunId, _Classname ) -> { static, [] }; get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, static_void_return, [] } ] }, _FunId, _Classname ) -> { static, [] }; get_info_from_clause_spec( _ClauseSpec={ type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, static_return, RTypes } ] }, FunId, Classname ) -> wooper_internals:raise_usage_error( "wrong arity of the specified WOOPER return type for the spec of ~s/~B: " "it should be static_return/1 (not static_return/~B).", pair:to_list( FunId ) ++ [ length( RTypes ) ], Classname, Line ); % For the rest, we assume it designates plain function: % % (wrong spelling, arity, etc. could be detected here as well, like with % check_clause_spec/5) % get_info_from_clause_spec( _ClauseSpec, _FunId, _Classname ) -> { function, [] }. % Checks the specified method clause spec. % % (helper) %% For requests: % Spec implies non-const request: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, request_return, [ _RType ] } ] }, _FunNature=request, Qualifiers, FunId, Classname ) -> case lists:member( const, Qualifiers ) of true -> wooper_internals:raise_usage_error( "the ~s/~B request has been " "detected as const, however its spec uses request_return/1 " "instead of const_request_return/1.", pair:to_list( FunId ), Classname, Line ); false -> ok end; % Spec implies const request: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_request_return, [ _RType ] } ] }, _FunNature=request, Qualifiers, FunId, Classname ) -> case lists:member( const, Qualifiers ) of true -> ok; false -> wooper_internals:raise_usage_error( "the ~s/~B request has been " "detected as non-const, however its spec uses " "const_request_return/1 instead of request_return/1.", pair:to_list( FunId ), Classname, Line ) end; % Spec implies (non-const) request whereas is not: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, request_return, [ _RType ] } ] }, NonReqFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B has been detected as a ~s, " "not as a (non-const) request, however its spec uses request_return/1.", pair:to_list( FunId ) ++ [ function_nature_to_string( NonReqFunNature ) ], Classname, Line ); % Spec implies (const) request whereas is not: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_request_return, [ _RType ] } ] }, NonReqFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B has been detected as a ~s, " "not as a (const) request, however its spec uses " "const_request_return/1.", pair:to_list( FunId ) ++ [ function_nature_to_string( NonReqFunNature ) ], Classname, Line ); % Wrong arity for request_return/1: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, request_return, Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses request_return/~B, " "which does not exist; its correct arity is 1.", pair:to_list( FunId ) ++ [ length( Types ) ], Classname, Line ); % Wrong arity for const_request_return/1: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_request_return, Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses const_request_return/~B, " "which does not exist; its correct arity is 1.", pair:to_list( FunId ) ++ [ length( Types ) ], Classname, Line ); % *_result used instead of *_return: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_request_result, _Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses the (unknown) " "'const_request_result' type: probably that " "const_request_return/1 was meant instead.", pair:to_list( FunId ), Classname, Line ); check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, request_result, _Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses the (unknown) " "'request_result' type: probably that " "request_return/1 was meant instead.", pair:to_list( FunId ), Classname, Line ); %% For oneways: % Spec implies non-const oneway: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, oneway_return, [] } ] }, _FunNature=oneway, Qualifiers, FunId, Classname ) -> case lists:member( const, Qualifiers ) of true -> wooper_internals:raise_usage_error( "the ~s/~B oneway has been " "detected as const, however its spec uses oneway_return/0 " "instead of const_oneway_return/0.", pair:to_list( FunId ), Classname, Line ); false -> ok end; % Spec implies const oneway: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_oneway_return, [] } ] }, _FunNature=oneway, Qualifiers, FunId, Classname ) -> case lists:member( const, Qualifiers ) of true -> ok; false -> wooper_internals:raise_usage_error( "the ~s/~B oneway has been " "detected as non-const, however its spec uses " "const_oneway_return/0 instead of oneway_return/0.", pair:to_list( FunId ), Classname, Line ) end; % Spec implies (non-const) oneway whereas is not: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, oneway_return, [] } ] }, NonOnwFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B has been detected as a ~s, " "not as a (non-const) oneway, however its spec uses oneway_return/0.", pair:to_list( FunId ) ++ [ function_nature_to_string( NonOnwFunNature ) ], Classname, Line ); % Spec implies (const) oneway whereas is not: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_oneway_return, [] } ] }, NonOnwFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B has been detected as a ~s, " "not as a (const) oneway, however its spec uses const_oneway_return/0.", pair:to_list( FunId ) ++ [ function_nature_to_string( NonOnwFunNature ) ], Classname, Line ); % Wrong arity for oneway_return/0: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, oneway_return, Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses oneway_return/~B, " "which does not exist; its correct arity is 0.", [ length( Types ) | pair:to_list( FunId ) ], Classname, Line ); % Wrong arity for const_oneway_return/0: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_oneway_return, Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses const_oneway_return/~B, " "which does not exist; its correct arity is 0.", [ length( Types ) | pair:to_list( FunId ) ], Classname, Line ); % *_result used instead of *_return: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, const_oneway_result, _Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses the (unknown) " "'const_oneway_result' type: probably that " "const_oneway_return/0 was meant instead.", pair:to_list( FunId ), Classname, Line ); check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, oneway_result, _Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses the (unknown) " "'oneway_result' type: probably that " "oneway_return/0 was meant instead.", pair:to_list( FunId ), Classname, Line ); %% For static methods: % Spec implies static method: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, static_return, [ _RType ] } ] }, _FunNature=static, _Qualifiers, _FunId, _Classname ) -> ok; % Void return: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, _, static_void_return, [] } ] }, _FunNature=static, _Qualifiers, _FunId, _Classname ) -> ok; % Spec implies static method whereas is not: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, static_return, [ _RType ] } ] }, NonStatFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B has been detected as a ~s, " "not as a static method, however its spec uses static_return/1.", pair:to_list( FunId ) ++ [ function_nature_to_string( NonStatFunNature ) ], Classname, Line ); % Wrong arity for static_return/1: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, static_return, Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses static_return/~B, " "which does not exist; its correct arity is 1.", [ length( Types ) | pair:to_list( FunId ) ], Classname, Line ); % Wrong arity for static_void_return/1: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, static_void_return, Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses static_void_return/~B, " "which does not exist; its correct arity is 0.", [ length( Types ) | pair:to_list( FunId ) ], Classname, Line ); % *_result used instead of *_return: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={ user_type, Line, static_result, _Types } ] }, _AnyFunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "~s/~B uses the (unknown) " "'static_result' type: probably that " "static_return/1 was meant instead.", pair:to_list( FunId ), Classname, Line ); %% For functions, nothing special to check: check_clause_spec( { type, _, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType ] }, _FunNature=function, _Qualifiers, _FunId, _Classname ) -> ok; %% For unmatched spec returns: % A method is not supposed to return explicity a state(): check_clause_spec( { type, Line, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={remote_type,_,[ {atom,_,wooper}, {atom,_,state},[] ] } ] }, _FunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "the type specification of ~s/~B " "is not expected to directly specify wooper:state() as a " "returned type. Maybe oneway_return() or alike was meant instead?", pair:to_list( FunId ), Classname, Line ); % Presumably a result spec wrongly qualified as wooper:S instead of S: check_clause_spec( { type, Line, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType={remote_type,_,[ {atom,_,wooper}, _, _ ] } ] }, _FunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "the type specification of ~s/~B " "is not expected to rely on a terminator prefixed with the wooper " "module (just remove 'wooper:').", pair:to_list( FunId ), Classname, Line ); % Presumably a rogue request (not using the right return type): check_clause_spec( { type, Line, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType ] }, _FunNature=request, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "the clauses of ~s/~B indicate " "that this is a request, yet in the type specification no known " "request terminator is used.", pair:to_list( FunId ), Classname, Line ); % Rogue oneway: check_clause_spec( { type, Line, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType ] }, _FunNature=oneway, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "the clauses of ~s/~B indicate " "that this is a oneway, yet in the type specification no known " "oneway terminator is used.", pair:to_list( FunId ), Classname, Line ); % Rogue static method: check_clause_spec( { type, Line, 'fun', _Seqs=[ _TypeProductForArgs, _ResultType ] }, _FunNature=static, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "the clauses of ~s/~B indicate " "that this is a static method, yet in the type specification no known " "static method terminator is used.", pair:to_list( FunId ), Classname, Line ); check_clause_spec( { type, _, 'fun', [ _Args, _ResultType={ user_type, Line, TypeName, RTypes } ] }, FunNature, _Qualifiers, FunId, Classname ) -> %code_utils:display_stacktrace(), wooper_internals:raise_usage_error( "unexpected return type (~s/~B) " "in the type specification of ~s/~B (which is detected as a ~s).", [ TypeName, length( RTypes ) | pair:to_list( FunId ) ] ++ [ function_nature_to_string( FunNature ) ], Classname, Line ); check_clause_spec( _UnexpectedTypeForm, FunNature, _Qualifiers, FunId, Classname ) -> wooper_internals:raise_usage_error( "unexpected return type in the type " "specification of ~s/~B (which is detected as a ~s).", pair:to_list( FunId ) ++ [ function_nature_to_string( FunNature ) ], Classname, _Line=0 ). % Returns a textual description of the specified function nature. -spec function_nature_to_string( maybe( function_nature() ) ) -> text_utils:ustring(). function_nature_to_string( request ) -> "request"; function_nature_to_string( oneway ) -> "oneway"; function_nature_to_string( static ) -> "static method"; function_nature_to_string( function ) -> "plain function"; function_nature_to_string( throw ) -> "throw-only function"; function_nature_to_string( undefined ) -> "undefined type of function"; % An unknown nature is not deemed an error here, so that upper layers do not % have to define their own, very similar version of check_clause_spec/5 for % example. % function_nature_to_string( Other ) when is_atom( Other ) -> text_utils:format( "function whose nature, '~s', is not known of WOOPER", [ Other ] ). % Checks that, in the specified clauses of specified function (corresponding to % a request or a oneway), the first parameter is 'State' indeed. % % Note: enforces a very welcome convention, but also complies with the % expression that the support for example of const_return_result/1 % introduces (ex: { var, LineCall, 'State'} added in the AST, hence the enforced % variable name). % -spec check_state_argument( [ meta_utils:clause_def() ], function_id(), wooper:classname() ) -> void(). check_state_argument( Clauses, FunId, Classname ) -> [ check_clause_for_state( C, FunId, Classname ) || C <- Clauses ]. % (helper) % check_clause_for_state( _Clause={ clause, _, _Params=[ {var,_,'State'} | _ ], _Guards, _Body }, _FunId, _Classname ) -> ok; % Tolerated iff throwing afterwards: check_clause_for_state( _Clause={ clause, _, _Params=[ {var,_,'_State'} | _ ], _Guards, _Body }, _FunId, _Classname ) -> ok; check_clause_for_state( _Clause={ clause, _, _Params=[ {var,Line,NonState} | _ ], _Guards, _Body }, FunId, Classname ) -> wooper_internals:raise_usage_error( "the first parameter of this clause of " "method ~s/~B shall be named 'State', not '~s'.", pair:to_list( FunId ) ++ [ NonState ], Classname, Line ); % Should a non-var form be found, we were considering not halting the % transformation (as the compiler would raise an error afterwards), however it % would then report "variable 'State' is unbound", which is less clear than: % check_clause_for_state( _Clause, FunId, Classname ) -> wooper_internals:raise_usage_error( "the first parameter of this clause of " "method ~s/~B shall be named 'State'.", pair:to_list( FunId ), Classname ). % Infers the nature of the corresponding function and any relevant method % qualifier(s), ensures that all method terminators correspond, and transforms % them appropriately (for WOOPER), in one pass. % % We consider that no method is to be explicitly exported and that all actual % clauses of a method must explicitly terminate with a WOOPER method terminator % (the same for all clauses, except regarding constness), rather than calling an % helper function that would use such a terminator (otherwise the nature of % methods could not be auto-detected, as there would be no way to determine % whether said helper should be considered as a method or not). % -spec manage_method_terminators( meta_utils:clause_def(), function_id(), wooper:classname(), wooper:function_export_set()) -> { meta_utils:clause_def(), function_nature(), method_qualifiers(), wooper:function_export_set() }. manage_method_terminators( _Clauses=[], FunId, Classname, _WOOPERExportSet ) -> wooper_internals:raise_usage_error( "the function ~s/~B is exported yet not defined.", pair:to_list( FunId ), Classname ); manage_method_terminators( Clauses, FunId, Classname, WOOPERExportSet ) -> ?trace_fmt( "Studying ~s/~B...", pair:to_list( FunId ) ), TransformTable = get_wooper_transform_table(), Transforms = #ast_transforms{ transformed_module_name=Classname, transform_table=TransformTable, transformed_function_identifier=FunId, transformation_state=get_blank_transformation_state( WOOPERExportSet ) }, %?debug_fmt( "transforming now ~p.", [ FunId ] ), { NewClauses, NewTransforms } = ast_clause:transform_function_clauses( Clauses, Transforms ), % Unless found different, a function is a (plain) function: { FunNature, Qualifiers } = case NewTransforms#ast_transforms.transformation_state of { undefined, _, _WOOPERExportSet } -> %?debug_fmt( "~s/~B detected as a plain function.", % pair:to_list( FunId ) ), { function, _Qualifiers=[] }; % Ex: { request, [ const ], _ } { OtherNature, SomeQualifiers, _WOOPERExportSet } -> %?debug_fmt( "~s/~B detected as: ~p (qualifiers: ~w)", % pair:to_list( FunId ) ++ [ OtherNature, Qualifiers ] ), { OtherNature, SomeQualifiers } end, { NewClauses, FunNature, Qualifiers }. % Returns a blank transformation state, based on the WOOPER exports. % % Defined to be safely reused from various locations. % get_blank_transformation_state() -> WOOPERExportSet = wooper:get_exported_functions_set(), get_blank_transformation_state( WOOPERExportSet ). % Returns a suitable, blank transformation state, from specified export set. % % Defined to be safely reused from various locations. % get_blank_transformation_state( ExportSet ) -> { _FunctionNature=undefined, _Qualifiers=[], ExportSet }. % Returns the WOOPER-specific transform table. -spec get_wooper_transform_table() -> ast_transform_table(). get_wooper_transform_table() -> % We define first the transformation functions in charge of the % guessing/checking/transforming of the method terminators. % % The clause transform-fun allows to handle plain functions, knowing that, % when no method terminator is found in a code branch, no specific behaviour % can be triggered (used to result in such branches to be basically % ignored). % % The body transform-fun will be used to skip all expressions of a body but % the last one, while the call transform-fun will be applied to call % expressions found there, as: % % { NewExpr, NewTransforms } = TransformFun( LineCall, FunctionRef, % Params, Transforms ) % % Transforms is an ast_transforms record that contains a % transformation_state field, which itself contains a value of type: % { maybe( function_nature() ), method_qualifiers(), } ). % % Indeed it starts as 'undefined', then the first terminal call branch of % the first clause allows to detect and set the function nature, and % afterwards all other branches are checked against it, and transformed. % Note that a member method is const iff all its clauses are const; so a % method is flagged içnitially as const depending on the constness of its % first clause, and loses any initial const status as soon as one clause % happens to be non-const (otherwise a non_const flag/qualifier would have % to be introduced). % So this (WOOPER-level) transformation is quite different (ex: partial vs % exhaustive) from the Myriad-level one that will be performed near the end % of the overall processing (merging these two would be tricky and would not % have much interest). % WOOPER traverses quite specifically the AST, so we override the default % traversal with transformation triggers (ex: we must trigger our % call-transformer only on final elements of bodies, not on test expressions % of cases): % table:new( [ { 'clause', fun clause_transformer/2 }, % Finally the traversal is not driven by the overall Myriad logic: % we drive it explicitly here, so the standard body_transformer/2 is % expected never to be triggered: instead we explicitly trigger, % from the WOOPER transformers, our body_transformer/3 variation; we % nevertheless define the standard version to detect if ever it was % called, and in this case report a blocking error: % { 'body', fun body_transformer/2 }, % Expression-level triggers: { 'call', fun call_transformer/4 }, { 'if', fun if_transformer/3 }, { 'case', fun case_transformer/4 }, { 'simple_receive', fun simple_receive_transformer/3 }, { 'receive_with_after', fun receive_with_after_transformer/5 }, { 'try', fun try_transformer/6 }, { 'catch', fun catch_transformer/3 } ] ). % List of WOOPER-specific transformers. % Drives the AST transformation of a clause. % % For any clause, we are to transform here, for WOOPER, only the last expression % (not the preceding ones) of the body (hence not touching parameters or % guards). % % The goal is to feed the call-transformer only with relevant expressions. % -spec clause_transformer( ast_clause(), ast_transforms() ) -> { ast_clause(), ast_transforms() }. clause_transformer( Clause={ clause, Line, Params, Guards, Body }, Transforms ) ?rec_guard -> %Transforms=#ast_transforms{ %transformed_function_identifier=FunId, %transformation_state={ InitialNature, InitialQualifiers, % WOOPERExportSet } } ) -> ?debug_fmt( "Transforming for WOOPER clause ~p", [ Clause ] ), % No need to reset transformation state, as done by body_transformer/3: { NewBody, BodyTransforms } = body_transformer( Body, Transforms, Line ), NewClause = { clause, Line, Params, Guards, NewBody }, UpdatedTransforms = update_transformation_state( Transforms, BodyTransforms, Line ), { NewClause, UpdatedTransforms }. % The standard body transformer, expected never to be called due to the % WOOPER-driven traversal of branching constructs. % -spec body_transformer( ast_body(), ast_transforms() ) -> { ast_body(), ast_transforms() }. body_transformer( Body, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) -> wooper_internals:raise_usage_error( "standard body transform called when " "traversing the AST of the ~s/~B function (abnormal); " "body was:~n ~p;~nCorresponding stacktrace was: ~s", pair:to_list( FunId ) ++ [ Body, code_utils:interpret_stacktrace() ], Transforms, _Line=0 ). % Drives the AST transformation of a body: processes specifically each last % expression of a body to be transformed (and only them), as it is the place % where we can guess the nature of a function and possibly, if it is a method, % at least some of its qualifiers. % % Used to be an anonymous function, yet now exported so that it can be re-used % by upper layers. % % Note: resets its transforms by itself. % -spec body_transformer( ast_body(), ast_transforms(), line() ) -> { ast_body(), ast_transforms() }. % As empty bodies may happen (ex: 'receive' without an 'after'): % (nevertheless should never happen in this WOOPER traversal) body_transformer( _BodyExprs=[], Transforms, _Line ) -> ?trace( "Transforming for WOOPER empty body." ), %UpdatedTransforms = update_transformation_state( Transforms, % Transforms, Line ), { _Exprs=[], Transforms }; % Commented-out as the last expression is managed differently (we cannot recurse % easily), but the spirit remains: % %body_transformer( _BodyExprs=[ LastExpr ], Transforms ) -> % ast_expression:transform_expressions( LastExpr, Transforms ); % At least an element exists here: body_transformer( BodyExprs, Transforms, Line ) -> % superfluous: when is_list( BodyExprs ) ?trace_fmt( "Transforming for WOOPER body ~p", [ BodyExprs ] ), % Warning: we currently skip intermediate expressions as a whole (we do not % transform them at all, as currently WOOPER does not have any need for % that), but maybe in the future this will have to be changed. % % We cannot use easily a Y-combinator here, as the signature of this % anonymous function is constrained: { [ Expr | SomeFun(T) ] }, Transforms } % % More efficient than list_utils:extract_last_element/2 and then recreating % the list: % [ LastExpr | RevFirstExprs ] = lists:reverse( BodyExprs ), ?trace_fmt( "Requesting the transformation of last expression ~p", [ LastExpr ] ), ResetTransforms = reset_transformation_state( Transforms ), % This may or may not trigger in turn our transformers, knowing that the % standard body_transformer/2 is expected never to be called (always % intercepted): % % (in practice, either one expression is returned, or in some cases none at % all; at least they are supposed to be already in-order - no need to % reverse them) % { NewLastExprs, NewTransforms } = ast_expression:transform_expression( LastExpr, ResetTransforms ), NewExprs = case lists:reverse( RevFirstExprs ) ++ NewLastExprs of % An empty body may happen (ex: if defining a static method returning % void whilr having no prior expression), which would not be legit: % [] -> PlaceholderAtom = wooper_void_return, %trace_utils:warning_fmt( % "Empty method body, replaced by the returning " % "of atom '~s'.", [ PlaceholderAtom ] ), % We return this, as something have to be returned: [ {atom,Line,PlaceholderAtom} ]; NonEmptyExprs -> NonEmptyExprs end, UpdatedTransforms = update_transformation_state( Transforms, NewTransforms, Line ), ?trace_fmt( "Nature after body transformation: ~p", [ element( 1, UpdatedTransforms#ast_transforms.transformation_state ) ] ), { NewExprs, UpdatedTransforms }. % Drives the AST transformation of a call: in charge of detecting the method % terminators and qualifiers, in a WOOPER context. % % Only the top-level call is of interest here (no need to recurse to hunt for % method terminators). % % (anonymous mute variables correspond to line numbers) % -spec call_transformer( line(), ast_expression:function_ref_expression(), ast_expression:params_expression(), ast_transforms() ) -> { [ ast_expression:ast_expression() ], ast_transforms() }. % Initial case: a (necessarily local) call to throw shall be intercepted, as any % clause of a method may throw, and it should not be interpreted as a non-method % clause; so: % call_transformer( LineCall, FunctionRef={atom,_,throw}, Params, Transforms=#ast_transforms{ transformation_state={ _Nature, _Qualifiers, WOOPERExportSet } } ) -> ?trace_fmt( "Transforming for WOOPER throw call, params: ~p.", [ Params ] ), %?trace( "throw expression intercepted" ), % Reconstructs the original throw expression: NewExpr = { call, LineCall, FunctionRef, Params }, % A throw cannot tell whether a given function clause is a method or a plain % function (so we do not return 'undefined', which would be translated as % plain function): % NewTransforms = Transforms#ast_transforms{ transformation_state={ throw, [], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % First, requests: % Better use the most precise return pseudo-function if this clause is const % (since returning the initial State): % call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state_result} }, _Params=[ _StateExpr={ var, Line, 'State' }, _ResExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) -> wooper_internals:raise_usage_error( "this const clause of " "request ~s/~B shall use wooper:const_return_result/1 (instead " "of wooper:return_state_result/2).", pair:to_list( FunId ), Transforms, Line ); % First (correct, non-const) request detection: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state_result} }, Params=[ _StateExpr, _ResExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, _, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a non-const request.", pair:to_list( FunId ) ), % So that wooper:return_state_result( S, R ) becomes simply { S, R }: NewExpr = { tuple, LineCall, Params }, NewTransforms = Transforms#ast_transforms{ transformation_state={ request, [], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Already detected as a request, checking qualifiers: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state_result} }, Params=[ _StateExpr, _ResExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ request, Qualifiers, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B confirmed as a non-const request.", pair:to_list( FunId ) ), % 'const' may or may not be still there, and will surely not: NewQualifiers = lists:delete( const, Qualifiers ), NewExpr = { tuple, LineCall, Params }, NewTransforms = Transforms#ast_transforms{ transformation_state={ request, NewQualifiers, WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Faulty return_state_result/2 arity: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state_result} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) when length( Params ) =/= 2 -> wooper_internals:raise_usage_error( "wrong arity (~B) specified " "for wooper:return_state_result/2, for request ~s/~B.", [ length( Params ) | pair:to_list( FunId ) ], Transforms, LineCall ); % Nature mismatch: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state_result} }, _Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ OtherNature, _Qualifiers, _WOOPERExportSet } } ) -> wooper_internals:raise_usage_error( "method terminator mismatch " "for method ~s/~B: wooper:return_state_result/2 implies " "request, whereas was detected as a ~s.", pair:to_list( FunId ) ++ [ OtherNature ], Transforms, LineCall ); % First (correct, a priori const) request detection: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return_result} }, Params=[ _ResExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, [], WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a const request.", pair:to_list( FunId ) ), % So that wooper:const_return_result( R ) becomes simply { S, R }: NewExpr = { tuple, LineCall, [ { var, LineCall, 'State' } | Params ] }, NewTransforms = Transforms#ast_transforms{ transformation_state={ request, [ const ], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Already detected as a request, this clause is const, this will not change % overall constness status: % call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return_result} }, Params=[ _ResExpr ], Transforms=#ast_transforms{ transformation_state={ request, _Qualifiers, _WOOPERExportSet } } ) -> NewExpr = { tuple, LineCall, [ { var, LineCall, 'State' } | Params ] }, { [ NewExpr ], Transforms }; % Faulty const_return_result/1 arity: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return_result} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) when length( Params ) =/= 1 -> wooper_internals:raise_usage_error( "wrong arity (~B) specified " "for wooper:const_return_result/1, for request ~s/~B.", [ length( Params ) | pair:to_list( FunId ) ], Transforms, LineCall ); % Nature mismatch: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return_result} }, _Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ OtherNature, _Qualifiers, _WOOPERExportSet } } ) -> wooper_internals:raise_usage_error( "method terminator mismatch " "for method ~s/~B: wooper:const_return_result/1 implies " "request, whereas was detected as a ~s.", pair:to_list( FunId ) ++ [ OtherNature ], Transforms, LineCall ); % Second, oneways: % Better use the most precise return pseudo-function if this clause is const % (since returning the initial State): % call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state} }, _Params=[ _StateExpr={ var, Line, 'State'} ], Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) -> wooper_internals:raise_usage_error( "this const clause of oneway " "~s/~B shall use wooper:const_return/0 " "(instead of wooper:return_state/1).", pair:to_list( FunId ), Transforms, Line ); % First (correct, non-const) oneway detection: call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state} }, _Params=[ StateExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, _Qualifiers, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a non-const oneway.", pair:to_list( FunId ) ), % So that wooper:return_state( S ) becomes simply S: NewExpr = StateExpr, NewTransforms = Transforms#ast_transforms{ transformation_state={ oneway, [], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Already detected as a oneway, checking qualifiers: call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state} }, _Params=[ StateExpr ], Transforms=#ast_transforms{ transformation_state={ oneway, Qualifiers, WOOPERExportSet } } ) -> % 'const' may or may not be still there, and will surely not: NewQualifiers = lists:delete( const, Qualifiers ), NewExpr = StateExpr, NewTransforms = Transforms#ast_transforms{ transformation_state={ oneway, NewQualifiers, WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Faulty return_state/1 arity: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) when length( Params ) =/= 1 -> wooper_internals:raise_usage_error( "wrong arity (~B) specified " "for wooper:return_state/1, for oneway ~s/~B.", [ length( Params ) | pair:to_list( FunId ) ], Transforms, LineCall ); % Nature mismatch: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_state} }, _Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ OtherNature, _Qualifiers, _WOOPERExportSet } } ) -> wooper_internals:raise_usage_error( "method terminator mismatch " "for method ~s/~B: wooper:return_state/1 implies " "oneway, whereas was detected as a ~s.", pair:to_list( FunId ) ++ [ OtherNature ], Transforms, LineCall ); % First (correct, a priori const) oneway detection: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return} }, _Params=[], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, _, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a const oneway.", pair:to_list( FunId ) ), % So that wooper:const_return() becomes simply S: NewExpr = { var, LineCall, 'State' }, NewTransforms = Transforms#ast_transforms{ transformation_state={ oneway, [ const ], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Already detected as a oneway, this clause is const, this will not change % overall constness status: % call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return} }, _Params=[], Transforms=#ast_transforms{ transformation_state={ oneway, _Qualifiers, _WOOPERExportSet } } ) -> NewExpr = { var, LineCall, 'State' }, { [ NewExpr ], Transforms }; % Faulty const_return/0 arity: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) when Params =/= [] -> wooper_internals:raise_usage_error( "wrong arity (~B) specified " "for wooper:const_return/0, for oneway ~s/~B.", [ length( Params ) | pair:to_list( FunId ) ], Transforms, LineCall ); % Nature mismatch: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,const_return} }, _Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ OtherNature, _Qualifiers, _WOOPERExportSet } } ) -> wooper_internals:raise_usage_error( "method terminator mismatch " "for method ~s/~B: wooper:const_return/0 implies " "oneway, whereas was detected as a ~s.", pair:to_list( FunId ) ++ [ OtherNature ], Transforms, LineCall ); % Third, static methods: % First (correct) static method detection of void return: call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_static_void} }, _Params=[], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, _, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a static method (void return).", pair:to_list( FunId ) ), % So that wooper:return_static( void ) becomes a no-op: NewTransforms = Transforms#ast_transforms{ transformation_state={ static, [], WOOPERExportSet } }, { [], NewTransforms }; call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_static} }, _Params=[ ResultExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, _, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a static method.", pair:to_list( FunId ) ), % So that wooper:return_static( R ) becomes simply R: NewExpr = ResultExpr, NewTransforms = Transforms#ast_transforms{ transformation_state={ static, [], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Already detected as a static: % % (mostly the same clause as above, as qualifiers do not matter for static): % call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_static} }, _Params=[ ResultExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ static, _Qualifiers, _WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B confirmed as a static method.", pair:to_list( FunId ) ), % So that wooper:return_static( R ) becomes simply R: NewExpr = ResultExpr, NewTransforms = Transforms, { [ NewExpr ], NewTransforms }; % Faulty static arity: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_static} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) when length( Params ) =/= 1 -> wooper_internals:raise_usage_error( "wrong arity (~B) specified " "for wooper:return_static/1, for static method ~s/~B.", [ length( Params ) | pair:to_list( FunId ) ], Transforms, LineCall ); % Nature mismatch: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,return_static} }, _Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ OtherNature, _Qualifiers, _WOOPERExportSet } } ) -> wooper_internals:raise_usage_error( "method terminator mismatch " "for method ~s/~B: wooper:return_static/1 implies " "static method, whereas was detected as a ~s.", pair:to_list( FunId ) ++ [ OtherNature ], Transforms, LineCall ); % Fourth, throwing declarations, to mark the return of a given call as a throw: % First (correct) throwing helper detection: call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,throwing} }, _Params=[ ResultExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ undefined, _, WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B detected as a throwing method.", pair:to_list( FunId ) ), % So that wooper:throwing( R ) becomes simply R (assimilated to a mere % throw): % NewExpr = ResultExpr, NewTransforms = Transforms#ast_transforms{ transformation_state={ throw, [], WOOPERExportSet } }, { [ NewExpr ], NewTransforms }; % Already detected as a throw: % % (mostly the same clause as above, as qualifiers do not matter for throwing): % call_transformer( _LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,throwing} }, _Params=[ ResultExpr ], Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ throw, _Qualifiers, _WOOPERExportSet } } ) -> ?debug_fmt( "~s/~B confirmed as a throw method.", pair:to_list( FunId ) ), % So that wooper:throwing( R ) becomes simply R: NewExpr = ResultExpr, NewTransforms = Transforms, { [ NewExpr ], NewTransforms }; % Faulty throwing arity: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,throwing} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId } ) when length( Params ) =/= 1 -> wooper_internals:raise_usage_error( "wrong arity (~B) specified " "for wooper:throwing/1, for method ~s/~B.", [ length( Params ) | pair:to_list( FunId ) ], Transforms, LineCall ); % Nature mismatch: call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,throwing} }, _Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ OtherNature, _Qualifiers, _WOOPERExportSet } } ) -> wooper_internals:raise_usage_error( "method terminator mismatch " "for method ~s/~B: wooper:throwing/1 implies " "throwing method, whereas was detected as a ~s.", pair:to_list( FunId ) ++ [ OtherNature ], Transforms, LineCall ); % The commented clause below cannot be kept, as a plain function may for example % terminate with a call to wooper:execute_request/4, with is licit and should % not be interpreted as an invalid method terminator: % % Invalid method terminator: %call_transformer( LineCall, _FunctionRef={ remote, _, {atom,_,wooper}, % {atom,_,UnexpectedTerminator} }, % _Params, % Transforms=#ast_transforms{ % transformed_function_identifier=FunId } ) -> % wooper_internals:raise_usage_error( "invalid method terminator specified " % "for ~s/~B: wooper:~s does not exist (for any arity).", % pair:to_list( FunId ) ++ [ UnexpectedTerminator ], Transforms, % LineCall ); % So we selectively accept the WOOPER non-terminator functions, and reject the % others, i.e. the ones that the wooper module does not export: % % (the purpose is to intercept any wrong method terminator that would be % introduced by the user) % call_transformer( LineCall, FunctionRef={ remote, _, {atom,_,wooper}, {atom,_,FunctionName} }, Params, Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ _Nature, _Qualifiers, WOOPERExportSet } } ) -> CallFunId = { FunctionName, length( Params ) }, % So this call does not correspond to any known/expected method terminator; % let's check whether it is a legit WOOPER call (allowed as last expression % of a body) or a WOOPER-unknown one (hence most probably a faulty % terminator): % case set_utils:member( CallFunId, WOOPERExportSet ) of true -> % OK, terminating a body with a call to a function of the wooper % module is allowed (provided it is not a method but a plain % function), so we let this call pass through: ?debug_fmt( "~s/~B detected as a plain function;~n" " - function ref is:~n~p~n - transform is:~n~p~n", pair:to_list( FunId ) ++ [ FunctionRef, Transforms ] ), SameExpr = { call, LineCall, FunctionRef, Params }, { [ SameExpr ], Transforms }; false -> %?debug_fmt( "Known functions exported by the wooper " % "module: ~s", % [ set_utils:to_string( WOOPERExportSet ) ] ), %?debug_fmt( "Known functions exported by the wooper " % "module:~n ~s", % [ table:to_string( WOOPERExportSet ) ] ), wooper_internals:raise_usage_error( "invalid method terminator " "specified for ~s/~B: wooper:~s/~B is neither a known terminator " "nor a WOOPER-exported function.", pair:to_list( FunId ) ++ pair:to_list( CallFunId ), Transforms, LineCall ) end; % Finally, of course calls unrelated to WOOPER shall go through as well - % provided this is either 'undefined' (single, direct clause) or 'function': % call_transformer( LineCall, FunctionRef, Params, %Transforms ) -> Transforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ Nature, Qualifiers, _WOOPERExportSet } } ) -> % No nature restriction, as even from a method we can explore 'normal' calls: %when Nature =:= undefined orelse Nature =:= function -> ?debug_fmt( "Deducing that ~s/~B is a plain function " "(nature: ~p, qualifiers: ~p)", pair:to_list( FunId ) ++ [ Nature, Qualifiers ] ), SameExpr = { call, LineCall, FunctionRef, Params }, %?debug_fmt( "Letting call remaining as ~p, while nature is ~p", % [ SameExpr, Nature ] ), { [ SameExpr ], Transforms }. % To help debugging any non-match: % call_transformer( _LineCall, FunctionRef, _Params, Transforms ) -> % % trace_utils:error_fmt( "Unexpected transforms for call ~p:~n ~s", % [ FunctionRef, % ast_transform:ast_transforms_to_string( Transforms ) ] ), % % throw( { unexpected_transforms, Transforms } ). % In the transformers below, we drive the traversal as needed, for example not % to recurse into case test expressions and alike (which must not be considered % for function nature diagnosis). % % So we define here the WOOPER counterpart versions of the % ast_clause:transform_*_clause functions. % Drives the AST transformation of a 'if' construct. % % (see ast_expression:transform_if/3) % -spec if_transformer( line(), [ ast_if_clause() ], ast_transforms() ) -> { [ ast_expression() ], ast_transforms() }. if_transformer( Line, IfClauses, Transforms ) ?rec_guard -> { NewIfClauses, NewTransforms } = lists:mapfoldl( fun if_clause_transformer/2, _Acc0=Transforms, _List=IfClauses ), NewExpr = { 'if', Line, NewIfClauses }, { [ NewExpr ], NewTransforms }. % Drives the AST transformation of a 'case' construct. % % (see ast_expression:transform_case/4) % -spec case_transformer( line(), ast_expression(), [ ast_case_clause() ], ast_transforms() ) -> { [ ast_expression() ], ast_transforms() }. case_transformer( Line, TestExpression, CaseClauses, Transforms ) ?rec_guard -> % Test expression not traversed here. { NewCaseClauses, CaseTransforms } = lists:mapfoldl( fun case_clause_transformer/2, _Acc0=Transforms, _List=CaseClauses ), NewExpr = { 'case', Line, TestExpression, NewCaseClauses }, { [ NewExpr ], CaseTransforms }. % Drives the AST transformation of a 'simple_receive' construct. % % (see ast_expression:transform_simple_receive/3) % -spec simple_receive_transformer( line(), [ ast_case_clause() ], ast_transforms() ) -> { [ ast_expression() ], ast_transforms() }. simple_receive_transformer( Line, ReceiveClauses, Transforms ) ?rec_guard -> % Receive clauses are actually case clauses: { NewReceiveClauses, NewTransforms } = lists:mapfoldl( fun case_clause_transformer/2, _Acc0=Transforms, _List=ReceiveClauses ), NewExpr = { 'receive', Line, NewReceiveClauses }, { [ NewExpr ], NewTransforms }. % Drives the AST transformation of a 'receive_with_after' construct. % % (see ast_expression:transform_receive_with_after/5) % -spec receive_with_after_transformer( line(), [ ast_case_clause() ], ast_expression(), ast_body(), ast_transforms() ) -> { [ ast_expression() ], ast_transforms() }. receive_with_after_transformer( Line, ReceiveClauses, AfterTest, AfterBody, Transforms ) ?rec_guard -> % Receive clauses are actually case clauses: { NewReceiveClauses, ReceiveTransforms } = lists:mapfoldl( fun case_clause_transformer/2, _Acc0=Transforms, _List=ReceiveClauses ), % Note that all "actual" updates in the transformation state are done by the % call_transformer, which is to be called (almost) solely by % body_transformer/3 - which is expected to catch and check state updates; % that function is called in each sub-transformer above and called below as % well, so no state change is expected to be missed: % Test unchanged. { NewAfterBody, AfterTransforms } = body_transformer( AfterBody, ReceiveTransforms, Line ), NewExpr = { 'receive', Line, NewReceiveClauses, AfterTest, NewAfterBody }, { [ NewExpr ], AfterTransforms }. % Drives the AST transformation of a 'try' construct. % % Actually it is rather tricky, as a try, in terms of return value, can have 3 % different forms (cf. http://erlang.org/doc/reference_manual/expressions.html): % % (1) try EXPR catch (CATCH_PATTERN_1 -> BODY_1), (CATCH_PATTERN_2 -> BODY_2) % % (2) try EXPR of (PATTERN_1 -> BODY_1), (PATTERN_2 -> BODY_2) catch % (CATCH_PATTERN_A -> BODY_A), (CATCH_PATTERN_B -> BODY_B),... % % (3) like (2), with an additional AFTER_BODY, whose value is lost % % So, here, for (1) EXPR is a possible return value, whereas not for (2) and % (3), and we have to discriminate among these cases. % % (see also ast_expression:transform_try/6) % -spec try_transformer( line(), ast_body(), [ ast_case_clause() ], [ ast_case_clause() ], ast_body(), ast_transforms() ) -> { [ ast_expression() ], ast_transforms() }. % Form (1): no try clauses (and no after): try_transformer( Line, TryBody, TryClauses=[], CatchClauses, AfterBody=[], Transforms ) ?rec_guard -> % Only returns through TryBody or CatchClauses: { NewTryBody, TryBodyTranforms } = body_transformer( TryBody, Transforms, Line ), { NewCatchClauses, CatchTransforms } = lists:mapfoldl( fun catch_clause_transformer/2, _Acc0=TryBodyTranforms, _List=CatchClauses ), % After body not transformed, as actually never returned: %{ NewAfterBody, AfterTransforms } = % body_transformer( AfterBody, CatchTransforms, Line ), NewExpr = { 'try', Line, NewTryBody, TryClauses, NewCatchClauses, AfterBody }, { [ NewExpr ], CatchTransforms }; % Form (2) and (3): we have try clauses (and after body can be ignored): try_transformer( Line, TryBody, TryClauses, CatchClauses, AfterBody, Transforms ) ?rec_guard -> % Only returns through TryClauses (not TryBody) or CatchClauses: % These are case clauses: { NewTryClauses, TryTransforms } = lists:mapfoldl( fun case_clause_transformer/2, _FirstAcc0=Transforms, _FirstList=TryClauses ), % These are catch clauses: { NewCatchClauses, CatchTransforms } = lists:mapfoldl( fun catch_clause_transformer/2, _SecondAcc0=TryTransforms, _SecondList=CatchClauses ), % After body not transformed, as actually never returned: %{ NewAfterBody, AfterTransforms } = % body_transformer( AfterBody, CatchTransforms, Line ), NewExpr = { 'try', Line, TryBody, NewTryClauses, NewCatchClauses, AfterBody }, { [ NewExpr ], CatchTransforms }. % Drives the AST transformation of a 'catch' construct. % % Not: for catch as an expression, not as a component of try. % -spec catch_transformer( line(), ast_expression(), ast_transforms() ) -> { [ ast_expression() ], ast_transforms() }. catch_transformer( Line, Expression, Transforms ) ?rec_guard -> % Only known place where a call_transformer might be called out of the % context of body_transformer/3: ResetTransforms = reset_transformation_state( Transforms ), { [ NewExpression ], NewTransforms } = ast_expression:transform_expression( Expression, ResetTransforms ), UpdatedTransforms = update_transformation_state( Transforms, NewTransforms, Line ), NewExpr = { 'catch', Line, NewExpression }, { [ NewExpr ], UpdatedTransforms }. % Subsection for WOOPER helper transformers. % % They may be used by multiple top-level transformers. % Transforms an 'if' clause just for the sake of WOOPER. % % (corresponds to ast_clause:transform_if_clause/2) % if_clause_transformer( _Clause={ 'clause', Line, HeadPatternSequence=[], GuardSequence, BodyExprs }, Transforms ) ?rec_guard -> % Non-existing head and guards not traversed. % No need to reset transformation state, as done by body_transformer/3: % We only transform the body here (and this integrates the update % logic regarding function detection): % { NewBodyExprs, BodyTransforms } = body_transformer( BodyExprs, Transforms, Line ), NewExpr = { 'clause', Line, HeadPatternSequence, GuardSequence, NewBodyExprs }, Res = { NewExpr, BodyTransforms }, %ast_utils:display_debug( "... returning if clause and state ~p", [ Res ] ), Res. % Transforms a 'case' clause just for the sake of WOOPER. % % (corresponds to ast_clause:transform_case_clause/2) % case_clause_transformer( _Clause={ 'clause', Line, CaseHead=[ _Pattern ], GuardSequence, BodyExprs }, Transforms ) ?rec_guard -> % Pattern and guards not traversed. % No need to reset transformation state, as done by body_transformer/3: % We only transform the body here (and this integrates the update % logic regarding function detection): % { NewBodyExprs, BodyTransforms } = body_transformer( BodyExprs, Transforms, Line ), NewExpr = { 'clause', Line, CaseHead, GuardSequence, NewBodyExprs }, Res = { NewExpr, BodyTransforms }, %ast_utils:display_debug( "... returning case clause and state ~p", % [ Res ] ), Res. % Transforms a 'catch' clause just for the sake of WOOPER. % % (corresponds to ast_clause:transform_catch_clause/2) % catch_clause_transformer( _Clause={ 'clause', Line, Throw=[ { throw, _Pattern, _Any } ], GuardSequence, BodyExprs }, Transforms ) ?rec_guard -> %ast_utils:display_debug( "Intercepting catch clause ~p...", [ Clause ] ), % We believe that only the body is to be traversed here: % No need to reset transformation state, as done by body_transformer/3: { NewBodyExprs, BodyTransforms } = body_transformer( BodyExprs, Transforms, Line ), NewExpr = { 'clause', Line, Throw, GuardSequence, NewBodyExprs }, Res = { NewExpr, BodyTransforms }, %ast_utils:display_debug( "... returning catch clause and state ~p", % [ Res ] ), Res; catch_clause_transformer( _Clause={ 'clause', Line, Head=[ _HeadPattern={ _X, _P, _Any } ], GuardSequence, BodyExprs }, Transforms ) ?rec_guard -> %ast_utils:display_debug( "Intercepting catch clause with variable ~p...", % [ Clause ] ), % We believe that only the body is to be traversed here: % No need to reset transformation state, as done by body_transformer/3: { NewBodyExprs, BodyTransforms } = body_transformer( BodyExprs, Transforms, Line ), NewExpr = { 'clause', Line, Head, GuardSequence, NewBodyExprs }, Res = { NewExpr, BodyTransforms }, %ast_utils:display_debug( "... returning catch clause with variable " % "and state ~p", [ Res ] ), Res. % Resets the transformation state, so that new findings can be compared to % previous knowledge. % % Otherwise, for example if having already a 'request' nature found and finding % afterwards a 'function' expression (i.e. a nature established by default), the % recorded nature would remain to 'request' and thus the mismatching 'function' % clause would not be detected. % -spec reset_transformation_state( ast_transforms() ) -> ast_transforms(). reset_transformation_state( Transforms=#ast_transforms{ transformation_state={ _Nature, _Qualifiers, WOOPERExportSet } } ) -> Transforms#ast_transforms{ transformation_state={ undefined, [], WOOPERExportSet } }. % Returns an updated transformation state, based on an initial one and one % returned by a transformation. % % Note that the base one provided to the transformation in-between shall have % been reset (see reset_transformation_state/1). % -spec update_transformation_state( ast_transforms(), ast_transforms(), line() ) -> ast_transforms(). update_transformation_state( _InitialTransforms=#ast_transforms{ transformation_state={ InitialNature, InitialQualifiers, _WOOPERExportSet } }, NewTransforms=#ast_transforms{ transformed_function_identifier=FunId, transformation_state={ NewRawNature, NewQualifiers, WOOPERExportSet } }, Line ) -> % As soon as we return from the transformation whereas the nature is still % 'undefined', this means that: % % 1. no method terminator has been found in the full branch (as we explore % the AST of a function depth-first) % % 2. it was the first terminal branch explored (as we assign systematically % the function nature when returning from a branch) % % So it was a plain function, we record that nature for further checking of % the other terminal branches. As a result this transformer allows to % discriminate 'no information yet' (hence function nature can be anything) % from 'no method terminator found' (so it must be a plain function). NewPair = { NewActualNature, NewActualQualifiers } = case NewRawNature of undefined -> { function, [] }; _ -> { NewRawNature, NewQualifiers } end, % Regarding function nature, following rules apply: % % 1. if the initial nature is 'undefined', any new nature (throw or method % one) will supersede it % % 2. if the initial nature is 'throw', the same applies (either superseded % by throw or a method one) % % 3. if the initial nature is a method one, this nature will be: % % - confirmed if new nature is the same % - kept as is if new nature is throw (no additional information) % - mismatching if new nature is a different method one { ResultingNature, ResultingQualifiers } = case InitialNature of undefined -> NewPair; throw -> NewPair; NewActualNature -> % Confirmed nature, only case to handle is loss of constness: case lists:member( const, InitialQualifiers ) andalso not lists:member( const, NewActualQualifiers ) of % Actually is non-const then: true -> { NewActualNature, NewActualQualifiers }; % As const as used to be: false -> { NewActualNature, InitialQualifiers } end; _MismatchingNature -> case NewActualNature of throw -> % Can happen for example if first clause of function of % interest is a 'function' one, next one is a 'throw': % % (by design InitialNature cannot be 'undefined' or 'throw' % here) % { InitialNature, InitialQualifiers }; _ -> wooper_internals:raise_usage_error( "the ~s/~B function was " "detected as a ~s, yet the clause at line #~B indicates " "it is a ~s.", pair:to_list( FunId ) ++ [ function_nature_to_string( InitialNature ), Line, function_nature_to_string( NewActualNature ) ], NewTransforms, Line ) end end, ?debug_fmt( "Nature of ~s/~B: initial=~p, new raw=~p, " "new actual=~p, final=~p.", pair:to_list( FunId ) ++ [ InitialNature, NewRawNature, NewActualNature, ResultingNature ] ), NewTransforms#ast_transforms{ transformation_state={ ResultingNature, ResultingQualifiers, WOOPERExportSet } }. % Ensures that the specified information is exported, auto-exporting it if % necessary. % % See also: ast_info:ensure_function_exported/4. % -spec ensure_exported( function_info(), marker_table() ) -> function_info(). ensure_exported( FunInfo=#function_info{ exported=[] }, MarkerTable ) -> %?debug_fmt( "- auto-exporting ~s", % [ ast_info:function_info_to_string( FunInfo ) ] ), % Not exported yet, hence to export: ExportLoc = ast_info:get_default_export_function_location( MarkerTable ), FunInfo#function_info{ exported=[ ExportLoc ] }; % Already exported, thus nothing to do: ensure_exported( FunInfo, _MarkerTable ) -> FunInfo. % Ensures that all functions in specified function table are exported, % auto-exporting them if necessary. % -spec ensure_all_exported_in( function_table(), ast_info:location() ) -> function_table(). ensure_all_exported_in( FunctionTable, ExportLoc ) -> table:map_on_values( fun( FunInfo ) -> ensure_exported_at( FunInfo, ExportLoc ) end, FunctionTable ). % (helper) ensure_exported_at( FunInfo=#function_info{ exported=[] }, ExportLoc ) -> FunInfo#function_info{ exported=[ ExportLoc ] }; ensure_exported_at( FunInfo=#function_info{ name=Name, arity=Arity }, _ExportLoc ) -> wooper_internals:notify_warning( [ text_utils:format( "~s/~B should not be explicitly exported, since methods " "are automatically exported.", [ Name, Arity ] ) ] ), FunInfo. % Section for the conversion of information records. % Converts specified (Myriad-level) function information into a (WOOPER-level) % request information. % -spec function_to_request_info( function_info(), method_qualifiers() ) -> request_info(). function_to_request_info( #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false, exported=[] }, Qualifiers ) -> #request_info{ name=Name, arity=Arity, qualifiers=Qualifiers, location=Loc, line=Line, clauses=Clauses, spec=Spec }; function_to_request_info( #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false %exported }, Qualifiers ) -> #request_info{ name=Name, arity=Arity, qualifiers=Qualifiers, location=Loc, line=Line, clauses=Clauses, spec=Spec }; function_to_request_info( Other, _Qualifiers ) -> throw( { unexpected_function_info, Other, request } ). % Converts specified (WOOPER-level) request information into a (Myriad-level) % function information. % -spec request_to_function_info( request_info(), location() ) -> function_info(). request_to_function_info( #request_info{ name=Name, arity=Arity, % Unused: qualifiers location=Loc, line=Line, clauses=Clauses, spec=Spec }, Location ) -> #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false, exported=[ Location ] }; request_to_function_info( Other, _Location ) -> throw( { unexpected_request_info, Other, request } ). % Converts specified (Myriad-level) function information into a (WOOPER-level) % oneway information. % -spec function_to_oneway_info( function_info(), method_qualifiers() ) -> oneway_info(). function_to_oneway_info( #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false, exported=[] }, Qualifiers ) -> #oneway_info{ name=Name, arity=Arity, qualifiers=Qualifiers, location=Loc, line=Line, clauses=Clauses, spec=Spec }; function_to_oneway_info( #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false %exported }, Qualifiers ) -> #oneway_info{ name=Name, arity=Arity, qualifiers=Qualifiers, location=Loc, line=Line, clauses=Clauses, spec=Spec }; function_to_oneway_info( Other, _Qualifiers ) -> throw( { unexpected_function_info, Other, oneway } ). % Converts specified (WOOPER-level) oneway information into a (Myriad-level) % function information. % -spec oneway_to_function_info( oneway_info(), location() ) -> function_info(). oneway_to_function_info( #oneway_info{ name=Name, arity=Arity, % Unused: qualifiers location=Loc, line=Line, clauses=Clauses, spec=Spec }, Location ) -> #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false, exported=[ Location ] }; oneway_to_function_info( Other, _Location ) -> throw( { unexpected_oneway_info, Other, oneway } ). % Converts specified (Myriad-level) function information into a (WOOPER-level) % static information. % -spec function_to_static_info( function_info(), method_qualifiers() ) -> static_info(). function_to_static_info( #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false, exported=[] }, Qualifiers ) -> #static_info{ name=Name, arity=Arity, qualifiers=Qualifiers, location=Loc, line=Line, clauses=Clauses, spec=Spec }; function_to_static_info( #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false %exported }, Qualifiers ) -> #static_info{ name=Name, arity=Arity, qualifiers=Qualifiers, location=Loc, line=Line, clauses=Clauses, spec=Spec }; function_to_static_info( Other, _Qualifiers ) -> throw( { unexpected_function_info, Other, static } ). % Converts specified (WOOPER-level) static information into a (Myriad-level) % function information. % -spec static_to_function_info( static_info(), location() ) -> function_info(). static_to_function_info( #static_info{ name=Name, arity=Arity, % Unused: qualifiers location=Loc, line=Line, clauses=Clauses, spec=Spec }, Location ) -> #function_info{ name=Name, arity=Arity, location=Loc, line=Line, clauses=Clauses, spec=Spec, callback=false, exported=[ Location ] }; static_to_function_info( Other, _Location ) -> throw( { unexpected_static_info, Other, static } ). % Transforms the methods in the specified tables into functions, and adds them % in specified function table. % -spec methods_to_functions( request_table(), oneway_table(), static_table(), function_table(), marker_table() ) -> function_table(). methods_to_functions( RequestTable, OnewayTable, StaticTable, InitFunctionTable, MarkerTable ) -> % Methods shall not be exported, but their corresponding functions surely % should, otherwise they could be reported as unused: % ExportLoc = ast_info:get_default_export_function_location( MarkerTable ), RequestPairs = table:enumerate( RequestTable ), RequestAsFunPairs = [ { ReqId, request_to_function_info( ReqInfo, ExportLoc ) } || { ReqId, ReqInfo } <- RequestPairs ], WithRequestsFunTable = table:add_new_entries( RequestAsFunPairs, InitFunctionTable ), OnewayPairs = table:enumerate( OnewayTable ), OnewayAsFunPairs = [ { OnwId, oneway_to_function_info( OnwInfo, ExportLoc ) } || { OnwId, OnwInfo } <- OnewayPairs ], WithOnewaysFunTable = table:add_new_entries( OnewayAsFunPairs, WithRequestsFunTable ), StaticPairs = table:enumerate( StaticTable ), StaticAsFunPairs = [ { StId, static_to_function_info( StInfo, ExportLoc ) } || { StId, StInfo } <- StaticPairs ], table:add_new_entries( StaticAsFunPairs, WithOnewaysFunTable ). % To help debugging: format_log( FormatString, [ { Exprs, #ast_transforms{ transformation_state={Nature,_Qualifiers,_WOOPERExportSet} } } ] ) -> Message = text_utils:format( "whose nature is '~s'; expressions are:~n ~p", [ Nature, Exprs ] ), text_utils:format( FormatString, [ Message ] ); format_log( FormatString, Other ) -> text_utils:format( FormatString, [ Other ] ).