%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2011. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% %% %% Parse transform for generating record access functions %% %% This parse transform can be used to reduce compile-time %% dependencies in large systems. %% %% In the old days, before records, Erlang programmers often wrote %% access functions for tuple data. This was tedious and error-prone. %% The record syntax made this easier, but since records were implemented %% fully in the pre-processor, a nasty compile-time dependency was %% introduced. %% %% This module automates the generation of access functions for %% records. While this method cannot fully replace the utility of %% pattern matching, it does allow a fair bit of functionality on %% records without the need for compile-time dependencies. %% %% Whenever record definitions need to be exported from a module, %% inserting a compiler attribute, %% %% export_records([RecName, ...]) %% %% causes this transform to lay out access functions for the exported %% records: %% %% -module(foo) %% -compile({parse_transform, diameter_exprecs}). %% %% -record(r, {a, b, c}). %% -export_records([a]). %% %% -export(['#info-'/1, '#info-'/2, %% '#new-'/1, '#new-'/2, %% '#get-'/2, '#set-'/2, %% '#new-a'/0, '#new-a'/1, %% '#get-a'/2, '#set-a'/2, %% '#info-a'/1]). %% %% '#info-'(RecName) -> %% '#info-'(RecName, fields). %% %% '#info-'(r, Info) -> %% '#info-r'(Info). %% %% '#new-'(r) -> #r{}. %% '#new-'(r, Vals) -> '#new-r'(Vals) %% %% '#new-r'() -> #r{}. %% '#new-r'(Vals) -> '#set-r'(Vals, #r{}). %% %% '#get-'(Attrs, #r{} = Rec) -> %% '#get-r'(Attrs, Rec). %% %% '#get-r'(Attrs, Rec) when is_list(Attrs) -> %% ['#get-r'(A, Rec) || A <- Attrs]; %% '#get-r'(a, Rec) -> Rec#r.a; %% '#get-r'(b, Rec) -> Rec#r.b; %% '#get-r'(c, Rec) -> Rec#r.c. %% %% '#set-'(Vals, #r{} = Rec) -> %% '#set-r'(Vals, Rec). %% %% '#set-r'(Vals, Rec) when is_list(Vals) -> %% lists:foldl(fun '#set-r'/2, Rec, Vals); %% '#set-r'({a,V}, Rec) -> Rec#r{a = V}; %% '#set-r'({b,V}, Rec) -> Rec#r{b = V}; %% '#set-r'({c,V}, Rec) -> Rec#r{c = V}. %% %% '#info-r'(fields) -> record_info(fields, r); %% '#info-r'(size) -> record_info(size, r); %% '#info-r'({index, a}) -> 1; %% '#info-r'({index, b}) -> 2; %% '#info-r'({index, c}) -> 3; %% -module(diameter_exprecs). -export([parse_transform/2]). %% Form tag with line number. -define(F(T), T, ?LINE). %% Yes, that's right. The replacement is to the first unmatched ')'. -define(attribute, ?F(attribute)). -define(clause, ?F(clause)). -define(function, ?F(function)). -define(call, ?F(call)). -define('fun', ?F('fun')). -define(generate, ?F(generate)). -define(lc, ?F(lc)). -define(match, ?F(match)). -define(remote, ?F(remote)). -define(record, ?F(record)). -define(record_field, ?F(record_field)). -define(record_index, ?F(record_index)). -define(tuple, ?F(tuple)). -define(ATOM(T), {atom, ?LINE, T}). -define(VAR(V), {var, ?LINE, V}). -define(CALL(F,A), {?call, ?ATOM(F), A}). -define(APPLY(M,F,A), {?call, {?remote, ?ATOM(M), ?ATOM(F)}, A}). %% parse_transform/2 parse_transform(Forms, _Options) -> Rs = [R || {attribute, _, record, R} <- Forms], case lists:append([E || {attribute, _, export_records, E} <- Forms]) of [] -> Forms; Es -> {H,T} = lists:splitwith(fun is_head/1, Forms), H ++ [a_export(Es) | f_accessors(Es, Rs)] ++ T end. is_head(T) -> not lists:member(element(1,T), [function, eof]). %% a_export/1 a_export(Exports) -> {?attribute, export, [{fname(info), 1}, {fname(info), 2}, {fname(new), 1}, {fname(new), 2}, {fname(get), 2}, {fname(set), 2} | lists:flatmap(fun export/1, Exports)]}. export(Rname) -> New = fname(new, Rname), [{New, 0}, {New, 1}, {fname(get, Rname), 2}, {fname(set, Rname), 2}, {fname(info, Rname), 1}]. %% f_accessors/2 f_accessors(Es, Rs) -> ['#info-/1'(), '#info-/2'(Es), '#new-/1'(Es), '#new-/2'(Es), '#get-/2'(Es), '#set-/2'(Es) | lists:flatmap(fun(N) -> accessors(N, fields(N, Rs)) end, Es)]. accessors(Rname, Fields) -> ['#new-X/0'(Rname), '#new-X/1'(Rname), '#get-X/2'(Rname, Fields), '#set-X/2'(Rname, Fields), '#info-X/1'(Rname, Fields)]. fields(Rname, Recs) -> {Rname, Fields} = lists:keyfind(Rname, 1, Recs), lists:map(fun({record_field, _, {atom, _, N}}) -> N; ({record_field, _, {atom, _, N}, _}) -> N end, Fields). fname_prefix(Op) -> "#" ++ atom_to_list(Op) ++ "-". fname(Op) -> list_to_atom(fname_prefix(Op)). fname(Op, Rname) -> Prefix = fname_prefix(Op), list_to_atom(Prefix ++ atom_to_list(Rname)). %% Generated functions. '#info-/1'() -> Fname = fname(info), {?function, Fname, 1, [{?clause, [?VAR('RecName')], [], [?CALL(Fname, [?VAR('RecName'), ?ATOM(fields)])]}]}. '#info-/2'(Exports) -> {?function, fname(info), 2, lists:map(fun 'info-'/1, Exports)}. 'info-'(R) -> {?clause, [?ATOM(R), ?VAR('Info')], [], [?CALL(fname(info, R), [?VAR('Info')])]}. '#new-/1'(Exports) -> {?function, fname(new), 1, lists:map(fun 'new-'/1, Exports)}. 'new-'(R) -> {?clause, [?ATOM(R)], [], [{?record, R, []}]}. '#new-/2'(Exports) -> {?function, fname(new), 2, lists:map(fun 'new--'/1, Exports)}. 'new--'(R) -> {?clause, [?ATOM(R), ?VAR('Vals')], [], [?CALL(fname(new, R), [?VAR('Vals')])]}. '#get-/2'(Exports) -> {?function, fname(get), 2, lists:map(fun 'get-'/1, Exports)}. 'get-'(R) -> {?clause, [?VAR('Attrs'), {?match, {?record, R, []}, ?VAR('Rec')}], [], [?CALL(fname(get, R), [?VAR('Attrs'), ?VAR('Rec')])]}. '#set-/2'(Exports) -> {?function, fname(set), 2, lists:map(fun 'set-'/1, Exports)}. 'set-'(R) -> {?clause, [?VAR('Vals'), {?match, {?record, R, []}, ?VAR('Rec')}], [], [?CALL(fname(set, R), [?VAR('Vals'), ?VAR('Rec')])]}. '#new-X/0'(Rname) -> {?function, fname(new, Rname), 0, [{?clause, [], [], [{?record, Rname, []}]}]}. '#new-X/1'(Rname) -> {?function, fname(new, Rname), 1, [{?clause, [?VAR('Vals')], [], [?CALL(fname(set, Rname), [?VAR('Vals'), {?record, Rname, []}])]}]}. '#set-X/2'(Rname, Fields) -> {?function, fname(set, Rname), 2, [{?clause, [?VAR('Vals'), ?VAR('Rec')], [[?CALL(is_list, [?VAR('Vals')])]], [?APPLY(lists, foldl, [{?'fun', {function, fname(set, Rname), 2}}, ?VAR('Rec'), ?VAR('Vals')])]} | lists:map(fun(A) -> 'set-X'(Rname, A) end, Fields)]}. 'set-X'(Rname, Attr) -> {?clause, [{?tuple, [?ATOM(Attr), ?VAR('V')]}, ?VAR('Rec')], [], [{?record, ?VAR('Rec'), Rname, [{?record_field, ?ATOM(Attr), ?VAR('V')}]}]}. '#get-X/2'(Rname, Fields) -> FName = fname(get, Rname), {?function, FName, 2, [{?clause, [?VAR('Attrs'), ?VAR('Rec')], [[?CALL(is_list, [?VAR('Attrs')])]], [{?lc, ?CALL(FName, [?VAR('A'), ?VAR('Rec')]), [{?generate, ?VAR('A'), ?VAR('Attrs')}]}]} | lists:map(fun(A) -> 'get-X'(Rname, A) end, Fields)]}. 'get-X'(Rname, Attr) -> {?clause, [?ATOM(Attr), ?VAR('Rec')], [], [{?record_field, ?VAR('Rec'), Rname, ?ATOM(Attr)}]}. '#info-X/1'(Rname, Fields) -> {?function, fname(info, Rname), 1, [{?clause, [?ATOM(fields)], [], [?CALL(record_info, [?ATOM(fields), ?ATOM(Rname)])]}, {?clause, [?ATOM(size)], [], [?CALL(record_info, [?ATOM(size), ?ATOM(Rname)])]} | lists:map(fun(A) -> 'info-X'(Rname, A) end, Fields)]}. 'info-X'(Rname, Attr) -> {?clause, [{?tuple, [?ATOM(index), ?ATOM(Attr)]}], [], [{?record_index, Rname, ?ATOM(Attr)}]}.