How to evaluate expressions containing record reference?

Hi,

How can I evaluate expressions such as: “X = P#person.age.” using something like erl_eval:exprs and friends?

Sort of like this (non-working code):

my_eval() ->
    PersonDef = {attribute,38,record,                                                                                                                                                                              
                    {person,[{record_field,38,{atom,38,name}},                                                                                                                                              
                             {record_field,38,{atom,38,age}}]}},
                        
    RecordDefs = [{person, 38, PersonDef}],

    Person = #person{name = "John", age = 30},
    Bindings0 = erl_eval:new_bindings(),
    Bindings = erl_eval:add_binding('P', Person, Bindings0),

    eval_exprs("X = P#person.age.", Bindings, RecordDefs).

    eval_exprs(Expression, Bindings, RecordDefs) ->
    
        %% Parse the expression
        {ok, Tokens, _} = erl_scan:string(Expression),
        {ok, [Expr]} = erl_parse:parse_exprs(Tokens),
    
        %% Evaluate the expression
        case erl_eval:exprs([Expr], Bindings, RecordDefs) of
            {value, Value, NewBindings} -> {Value, NewBindings};
            Error -> Error
        end.
1 Like

I don’t think that’s possible, but what is possible is to use erl_expand_records module to expand records to tuples. It will return modified abstract_form()s and you can act like that is what you originally parsed.

1 Like

I believe there is no easy way to achieve that.
Maybe you can try erl_eval_records lib.

Or change your code to call a function

-module(my_person).
-export([ get_person_age/1, my_eval/0 ]).

-record(person, {name, age}).

get_person_age(#person{age = Age}) ->
    Age.

my_eval() ->
    Person = #person{name = "John", age = 30},
    Bindings0 = erl_eval:new_bindings(),
    Bindings = erl_eval:add_binding('P', Person, Bindings0),
    eval_exprs("X = my_person:get_person_age(P).", Bindings). %% <-- Uses a function

eval_exprs(Expression, Bindings) ->

    %% Parse the expression
    {ok, Tokens, _} = erl_scan:string(Expression),
    {ok, [Expr]} = erl_parse:parse_exprs(Tokens),

    %% Evaluate the expression
    case erl_eval:exprs([Expr], Bindings) of
        {value, Value, NewBindings} -> {Value, NewBindings};
        Error -> Error
    end.

Result

1> my_person:my_eval().
{30,[{'P',{person,"John",30}},{'X',30}]}

Mm…thanks for the suggestions.

A pity that eval_exprs/N can’t handle it, perhaps I could wish for a feature enhancement in some future release… :stuck_out_tongue:

1 Like

Using the erl_expand_records that @mmin mentioned, I’ve ended up doing something that can maybe help you. erl_expand_records expects record attributes, and what it expands are functions, so we need to wrap the parsed forms into a function.

Take this (tricky) example (it needs to be improved using erl_syntax):

-module(eval).

-export([string/3, record_attribute/2]).

string(String, Bindings, HeaderForms) ->
    ExprForms = wrap(expr_forms(String)),
    Forms = lists:last(erl_expand_records:module(HeaderForms ++ ExprForms, [])),
    erl_eval:expr(unwrap(Forms), Bindings).

record_attribute(Name, Fields0) when is_atom(Name), is_list(Fields0) ->
    Fields1 = [atom_to_binary(Field) || Field <- Fields0],
    Fields = lists:join(", ", Fields1),
    IOList = io_lib:format("-record(~s, {~s}).", [Name, Fields]),
    merl:quote(iolist_to_binary(IOList)).

%% Internal functions

expr_forms(String) ->
    {ok, Tokens, _} = erl_scan:string(String),
    {ok, Forms} = erl_parse:parse_exprs(Tokens),
    Forms.

wrap(Expr) ->
    [{function, 1, {atom, 1, foo}, 1, [{clause, 1, [], [], Expr}]}].

unwrap({function, 1, {atom, 1, foo}, 1, [{clause, 1, [], [], [Expr]}]}) ->
    Expr.

Result:

1> HeaderForms = [eval:record_attribute(person, [name, age])].
[{attribute,1,record,
            {person,[{record_field,1,{atom,1,name}},
                     {record_field,1,{atom,1,age}}]}}]
2> eval:string("X = P#person.age.", #{'P' => {person, "John", 30}}, HeaderForms).
{value,30,#{'X' => 30,'P' => {person,"John",30},rec0 => 30}}

Hope it can help you.

Thanks!
I’ll have a look.

1 Like