Add serialization/deserialization.
This commit is contained in:
parent
c4adf27a6e
commit
286a8c1913
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,3 +9,4 @@ rel/example_project
|
|||||||
.concrete/DEV_MODE
|
.concrete/DEV_MODE
|
||||||
.rebar
|
.rebar
|
||||||
aeb_asm_scan.erl
|
aeb_asm_scan.erl
|
||||||
|
aefa_asm_scan.erl
|
@ -79,9 +79,9 @@
|
|||||||
-define('ABORT' , 16#50).
|
-define('ABORT' , 16#50).
|
||||||
-define('EXIT' , 16#51).
|
-define('EXIT' , 16#51).
|
||||||
-define('DEACTIVATE' , 16#52).
|
-define('DEACTIVATE' , 16#52).
|
||||||
-define('INT_TO_STR' , 16#53).
|
-define('INC' , 16#53).
|
||||||
|
-define('DEC' , 16#54).
|
||||||
|
-define('INT_TO_STR' , 16#55).
|
||||||
-define('SPEND' , 16#56).
|
-define('SPEND' , 16#56).
|
||||||
-define('ORACLE_REGISTER', 16#57).
|
-define('ORACLE_REGISTER', 16#57).
|
||||||
-define('ORACLE_QUERY' , 16#58).
|
-define('ORACLE_QUERY' , 16#58).
|
||||||
@ -101,6 +101,7 @@
|
|||||||
-define('SHA256' , 16#66).
|
-define('SHA256' , 16#66).
|
||||||
-define('BLAKE2B' , 16#67).
|
-define('BLAKE2B' , 16#67).
|
||||||
|
|
||||||
|
-define('FUNCTION' , 16#fe).
|
||||||
-define('EXTEND' , 16#ff).
|
-define('EXTEND' , 16#ff).
|
||||||
|
|
||||||
-define( COMMENT(X), {comment, X}).
|
-define( COMMENT(X), {comment, X}).
|
||||||
|
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
{erl_opts, [debug_info]}.
|
{erl_opts, [debug_info]}.
|
||||||
|
|
||||||
{deps, []}.
|
{deps, [
|
||||||
|
{enacl, {git, "https://github.com/aeternity/enacl.git",
|
||||||
|
{ref, "26180f4"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
{dialyzer, [
|
{dialyzer, [
|
||||||
{warnings, [unknown]},
|
{warnings, [unknown]},
|
||||||
@ -11,9 +14,9 @@
|
|||||||
]}.
|
]}.
|
||||||
|
|
||||||
{relx, [{release, {aessembler, "0.0.1"},
|
{relx, [{release, {aessembler, "0.0.1"},
|
||||||
[aebytecode]},
|
[aebytecode, enacl]},
|
||||||
|
|
||||||
{dev_mode, true},
|
{dev_mode, true},
|
||||||
{include_erts, false},
|
{include_erts, false},
|
||||||
|
|
||||||
{extended_start_script, true}]}.
|
{extended_start_script, true}]}.
|
||||||
|
@ -1 +1,4 @@
|
|||||||
[].
|
[{<<"enacl">>,
|
||||||
|
{git,"https://github.com/aeternity/enacl.git",
|
||||||
|
{ref,"26180f42c0b3a450905d2efd8bc7fd5fd9cece75"}},
|
||||||
|
0}].
|
||||||
|
186
src/aefa_asm.erl
186
src/aefa_asm.erl
@ -43,12 +43,15 @@
|
|||||||
|
|
||||||
-module(aefa_asm).
|
-module(aefa_asm).
|
||||||
|
|
||||||
-export([ file/2
|
-export([ asm_to_bytecode/2
|
||||||
|
, bytecode_to_fate_code/2
|
||||||
, pp/1
|
, pp/1
|
||||||
|
, read_file/1
|
||||||
, to_hexstring/1
|
, to_hexstring/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-include_lib("aebytecode/include/aefa_opcodes.hrl").
|
-include_lib("aebytecode/include/aefa_opcodes.hrl").
|
||||||
|
-define(HASH_BYTES, 32).
|
||||||
|
|
||||||
|
|
||||||
pp(Asm) ->
|
pp(Asm) ->
|
||||||
@ -60,17 +63,18 @@ format(Asm) -> format(Asm, 0).
|
|||||||
format([{comment, Comment} | Rest], Address) ->
|
format([{comment, Comment} | Rest], Address) ->
|
||||||
";; " ++ Comment ++ "\n" ++ format(Rest, Address);
|
";; " ++ Comment ++ "\n" ++ format(Rest, Address);
|
||||||
format([Mnemonic | Rest], Address) ->
|
format([Mnemonic | Rest], Address) ->
|
||||||
_Op = aefa_opcode:m_to_op(Mnemonic),
|
_Op = aefa_opcodes:m_to_op(Mnemonic),
|
||||||
" " ++ atom_to_list(Mnemonic) ++ "\n"
|
" " ++ atom_to_list(Mnemonic) ++ "\n"
|
||||||
++ format(Rest, Address + 1);
|
++ format(Rest, Address + 1);
|
||||||
format([],_) -> [].
|
format([],_) -> [].
|
||||||
|
|
||||||
|
|
||||||
|
read_file(Filename) ->
|
||||||
|
|
||||||
file(Filename, Options) ->
|
|
||||||
{ok, File} = file:read_file(Filename),
|
{ok, File} = file:read_file(Filename),
|
||||||
{ok, Tokens, _} = aefa_asm_scan:scan(binary_to_list(File)),
|
binary_to_list(File).
|
||||||
|
|
||||||
|
asm_to_bytecode(AssemblerCode, Options) ->
|
||||||
|
{ok, Tokens, _} = aefa_asm_scan:scan(AssemblerCode),
|
||||||
|
|
||||||
case proplists:lookup(pp_tokens, Options) of
|
case proplists:lookup(pp_tokens, Options) of
|
||||||
{pp_tokens, true} ->
|
{pp_tokens, true} ->
|
||||||
@ -79,7 +83,9 @@ file(Filename, Options) ->
|
|||||||
ok
|
ok
|
||||||
end,
|
end,
|
||||||
|
|
||||||
Env = to_bytecode(Tokens, none, #{}, [], Options),
|
Env = to_bytecode(Tokens, none, #{ functions => #{}
|
||||||
|
, symbols => #{}
|
||||||
|
}, [], Options),
|
||||||
|
|
||||||
ByteList = serialize(Env),
|
ByteList = serialize(Env),
|
||||||
|
|
||||||
@ -92,11 +98,141 @@ file(Filename, Options) ->
|
|||||||
|
|
||||||
{Env, list_to_binary(ByteList)}.
|
{Env, list_to_binary(ByteList)}.
|
||||||
|
|
||||||
serialize(Env) ->
|
bytecode_to_fate_code(ByteCode,_Options) ->
|
||||||
|
deserialize(ByteCode, #{ function => none
|
||||||
|
, bb => 0
|
||||||
|
, current_bb_code => []
|
||||||
|
, functions => #{}
|
||||||
|
, code => #{}
|
||||||
|
}).
|
||||||
|
|
||||||
|
deserialize(<<?FUNCTION:8, A, B, C, D, Rest/binary>>,
|
||||||
|
#{ function := none
|
||||||
|
, bb := 0
|
||||||
|
, current_bb_code := []
|
||||||
|
} = Env) ->
|
||||||
|
{Sig, Rest2} = deserialize_signature(Rest),
|
||||||
|
Env2 = Env#{function => {<<A,B,C,D>>, Sig}},
|
||||||
|
deserialize(Rest2, Env2);
|
||||||
|
deserialize(<<?FUNCTION:8, A, B, C, D, Rest/binary>>,
|
||||||
|
#{ function := F
|
||||||
|
, bb := BB
|
||||||
|
, current_bb_code := Code
|
||||||
|
, code := Program
|
||||||
|
, functions := Funs} = Env) ->
|
||||||
|
{Sig, Rest2} = deserialize_signature(Rest),
|
||||||
|
case Code of
|
||||||
|
[] ->
|
||||||
|
Env2 = Env#{ bb => 0
|
||||||
|
, current_bb_code => []
|
||||||
|
, function => {<<A,B,C,D>>, Sig}
|
||||||
|
, code => #{}
|
||||||
|
, functions => Funs#{F => Program}},
|
||||||
|
deserialize(Rest2, Env2);
|
||||||
|
_ ->
|
||||||
|
Env2 = Env#{ bb => 0
|
||||||
|
, current_bb_code => []
|
||||||
|
, function => {<<A,B,C,D>>, Sig}
|
||||||
|
, code => #{}
|
||||||
|
, functions =>
|
||||||
|
Funs#{F => Program#{ BB => lists:reverse(Code)}}},
|
||||||
|
deserialize(Rest2, Env2)
|
||||||
|
end;
|
||||||
|
deserialize(<<Op:8, Rest/binary>>,
|
||||||
|
#{ bb := BB
|
||||||
|
, current_bb_code := Code
|
||||||
|
, code := Program} = Env) ->
|
||||||
|
{Rest2, OpCode} = deserialize_op(Op, Rest, Code),
|
||||||
|
case aefa_opcodes:end_bb(Op) of
|
||||||
|
true ->
|
||||||
|
deserialize(Rest2, Env#{ bb => BB+1
|
||||||
|
, current_bb_code => []
|
||||||
|
, code => Program#{BB =>
|
||||||
|
lists:reverse(OpCode)}});
|
||||||
|
false ->
|
||||||
|
deserialize(Rest2, Env#{ current_bb_code => OpCode})
|
||||||
|
end;
|
||||||
|
deserialize(<<>>, #{ function := F
|
||||||
|
, bb := BB
|
||||||
|
, current_bb_code := Code
|
||||||
|
, code := Program
|
||||||
|
, functions := Funs} = Env) ->
|
||||||
|
FunctionCode =
|
||||||
|
case Code of
|
||||||
|
[] -> Program;
|
||||||
|
_ -> Program#{ BB => lists:reverse(Code)}
|
||||||
|
end,
|
||||||
|
Env#{ bb => 0
|
||||||
|
, current_bb_code => []
|
||||||
|
, function => none
|
||||||
|
, code => #{}
|
||||||
|
, functions => Funs#{F => FunctionCode}}.
|
||||||
|
|
||||||
|
deserialize_op(Op, Rest, Code) ->
|
||||||
|
OpName = aefa_opcodes:mnemonic(Op),
|
||||||
|
case aefa_opcodes:args(Op) of
|
||||||
|
0 -> {Rest, [OpName | Code]};
|
||||||
|
1 -> %% TODO: use rlp encoded int.
|
||||||
|
<<Arg:8, Rest2/binary>> = Rest,
|
||||||
|
{Rest2, [Arg, OpName | Code]};
|
||||||
|
hash ->
|
||||||
|
<<A, B, C, D, Rest2/binary>> = Rest,
|
||||||
|
Code2 = [<<A,B,C,D>>, OpName | Code],
|
||||||
|
{Rest2, Code2}
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
serialize(#{functions := Functions} = Env) ->
|
||||||
%% TODO: add serialization of immediates
|
%% TODO: add serialization of immediates
|
||||||
%% TODO: add serialization of function definitions
|
%% TODO: add serialization of function definitions
|
||||||
Code = [C || {_Name, {_Sig, C}} <- maps:to_list(Env)],
|
Code = [[?FUNCTION, Name, serialize_signature(Sig), C] ||
|
||||||
Code.
|
{Name, {Sig, C}} <- maps:to_list(Functions)],
|
||||||
|
lists:flatten(Code).
|
||||||
|
|
||||||
|
serialize_signature({Args, RetType}) ->
|
||||||
|
[serialize_type({tuple, Args}) |
|
||||||
|
serialize_type(RetType)].
|
||||||
|
|
||||||
|
serialize_type(integer) -> [0];
|
||||||
|
serialize_type(boolean) -> [1];
|
||||||
|
serialize_type({list, T}) -> [2 | serialize_type(T)];
|
||||||
|
serialize_type({tuple, Ts}) ->
|
||||||
|
case length(Ts) of
|
||||||
|
N when N =< 255 ->
|
||||||
|
[3, N | [serialize_type(T) || T <- Ts]]
|
||||||
|
end;
|
||||||
|
serialize_type(address) -> 4;
|
||||||
|
serialize_type(bits) -> 5;
|
||||||
|
serialize_type({map, K, V}) -> [6 | serialize_type(K) ++ serialize_type(V)].
|
||||||
|
|
||||||
|
|
||||||
|
deserialize_signature(Binary) ->
|
||||||
|
{{tuple, Args}, Rest} = deserialize_type(Binary),
|
||||||
|
{RetType, Rest2} = deserialize_type(Rest),
|
||||||
|
{{Args, RetType}, Rest2}.
|
||||||
|
|
||||||
|
deserialize_type(<<0, Rest/binary>>) -> {integer, Rest};
|
||||||
|
deserialize_type(<<1, Rest/binary>>) -> {boolean, Rest};
|
||||||
|
deserialize_type(<<2, Rest/binary>>) ->
|
||||||
|
{T, Rest2} = deserialize_type(Rest),
|
||||||
|
{{list, T}, Rest2};
|
||||||
|
deserialize_type(<<3, N, Rest/binary>>) ->
|
||||||
|
{Ts, Rest2} = deserialize_types(N, Rest, []),
|
||||||
|
{{tuple, Ts}, Rest2};
|
||||||
|
deserialize_type(<<4, Rest/binary>>) -> {address, Rest};
|
||||||
|
deserialize_type(<<5, Rest/binary>>) -> {bits, Rest};
|
||||||
|
deserialize_type(<<6, Rest/binary>>) ->
|
||||||
|
{K, Rest2} = deserialize_type(Rest),
|
||||||
|
{V, Rest3} = deserialize_type(Rest2),
|
||||||
|
{{map, K, V}, Rest3}.
|
||||||
|
|
||||||
|
deserialize_types(0, Binary, Acc) ->
|
||||||
|
{lists:reverse(Acc), Binary};
|
||||||
|
deserialize_types(N, Binary, Acc) ->
|
||||||
|
{T, Rest} = deserialize_type(Binary),
|
||||||
|
deserialize_types(N-1, Rest, [T | Acc]).
|
||||||
|
|
||||||
|
|
||||||
to_hexstring(ByteList) ->
|
to_hexstring(ByteList) ->
|
||||||
"0x" ++ lists:flatten(
|
"0x" ++ lists:flatten(
|
||||||
@ -108,7 +244,7 @@ to_bytecode([{function,_line, 'FUNCTION'}|Rest], Address, Env, Code, Opts) ->
|
|||||||
{Fun, Rest2} = to_fun_def(Rest),
|
{Fun, Rest2} = to_fun_def(Rest),
|
||||||
to_bytecode(Rest2, Fun, Env2, [], Opts);
|
to_bytecode(Rest2, Fun, Env2, [], Opts);
|
||||||
to_bytecode([{mnemonic,_line, Op}|Rest], Address, Env, Code, Opts) ->
|
to_bytecode([{mnemonic,_line, Op}|Rest], Address, Env, Code, Opts) ->
|
||||||
OpCode = aefa_opcode:m_to_op(Op),
|
OpCode = aefa_opcodes:m_to_op(Op),
|
||||||
%% TODO: arguments
|
%% TODO: arguments
|
||||||
to_bytecode(Rest, Address, Env, [OpCode|Code], Opts);
|
to_bytecode(Rest, Address, Env, [OpCode|Code], Opts);
|
||||||
to_bytecode([{int,_line, Int}|Rest], Address, Env, Code, Opts) ->
|
to_bytecode([{int,_line, Int}|Rest], Address, Env, Code, Opts) ->
|
||||||
@ -116,7 +252,8 @@ to_bytecode([{int,_line, Int}|Rest], Address, Env, Code, Opts) ->
|
|||||||
to_bytecode([{hash,_line, Hash}|Rest], Address, Env, Code, Opts) ->
|
to_bytecode([{hash,_line, Hash}|Rest], Address, Env, Code, Opts) ->
|
||||||
to_bytecode(Rest, Address, Env, [Hash|Code], Opts);
|
to_bytecode(Rest, Address, Env, [Hash|Code], Opts);
|
||||||
to_bytecode([{id,_line, ID}|Rest], Address, Env, Code, Opts) ->
|
to_bytecode([{id,_line, ID}|Rest], Address, Env, Code, Opts) ->
|
||||||
to_bytecode(Rest, Address, Env, [{ref, ID}|Code], Opts);
|
{ok, Hash} = lookup_symbol(ID, Env),
|
||||||
|
to_bytecode(Rest, Address, Env, [Hash|Code], Opts);
|
||||||
to_bytecode([{label,_line, Label}|Rest], Address, Env, Code, Opts) ->
|
to_bytecode([{label,_line, Label}|Rest], Address, Env, Code, Opts) ->
|
||||||
to_bytecode(Rest, Address, Env#{Label => Address}, Code, Opts);
|
to_bytecode(Rest, Address, Env#{Label => Address}, Code, Opts);
|
||||||
to_bytecode([], Address, Env, Code, Opts) ->
|
to_bytecode([], Address, Env, Code, Opts) ->
|
||||||
@ -190,5 +327,26 @@ expand_args([OP | Rest]) ->
|
|||||||
expand_args([]) -> [].
|
expand_args([]) -> [].
|
||||||
|
|
||||||
insert_fun(none, [], Env) -> Env;
|
insert_fun(none, [], Env) -> Env;
|
||||||
insert_fun({Name, Type, RetType}, Code, Env) ->
|
insert_fun({Name, Type, RetType}, Code, #{functions := Functions} = Env) ->
|
||||||
Env#{Name => {{Type, RetType}, lists:reverse(Code)}}.
|
{Hash, Env2} = insert_symbol(Name, Env),
|
||||||
|
Env2#{
|
||||||
|
functions => Functions#{Hash => {{Type, RetType}, lists:reverse(Code)}}
|
||||||
|
}.
|
||||||
|
|
||||||
|
insert_symbol(Id, Env) ->
|
||||||
|
%% Use first 4 bytes of blake hash
|
||||||
|
{ok, <<A:8, B:8, C:8, D:8,_/binary>> } = enacl:generichash(?HASH_BYTES, list_to_binary(Id)),
|
||||||
|
insert_symbol(Id, <<A,B,C,D>>, Env).
|
||||||
|
|
||||||
|
insert_symbol(Id, Hash, #{symbols := Symbols} = Env) ->
|
||||||
|
case maps:find(Hash, Symbols) of
|
||||||
|
{ok, Id} -> {Hash, Env};
|
||||||
|
{ok, Id2} ->
|
||||||
|
%% Very unlikely...
|
||||||
|
exit({two_symbols_with_same_hash, Id, Id2});
|
||||||
|
error ->
|
||||||
|
{Hash, Env#{symbols => Symbols#{ Id => Hash
|
||||||
|
, Hash => Id}}}
|
||||||
|
end.
|
||||||
|
lookup_symbol(Id, #{symbols := Symbols} = Env) ->
|
||||||
|
maps:find(Id, Symbols).
|
||||||
|
@ -47,6 +47,9 @@ DIV : {token, {mnemonic, TokenLine, 'DIV'}}.
|
|||||||
MOD : {token, {mnemonic, TokenLine, 'MOD'}}.
|
MOD : {token, {mnemonic, TokenLine, 'MOD'}}.
|
||||||
POW : {token, {mnemonic, TokenLine, 'POW'}}.
|
POW : {token, {mnemonic, TokenLine, 'POW'}}.
|
||||||
|
|
||||||
|
INC : {token, {mnemonic, TokenLine, 'INC'}}.
|
||||||
|
DEC : {token, {mnemonic, TokenLine, 'DEC'}}.
|
||||||
|
|
||||||
LT : {token, {mnemonic, TokenLine, 'LT'}}.
|
LT : {token, {mnemonic, TokenLine, 'LT'}}.
|
||||||
GT : {token, {mnemonic, TokenLine, 'GT'}}.
|
GT : {token, {mnemonic, TokenLine, 'GT'}}.
|
||||||
EQ : {token, {mnemonic, TokenLine, 'EQ'}}.
|
EQ : {token, {mnemonic, TokenLine, 'EQ'}}.
|
||||||
|
@ -5,9 +5,11 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
-module(aefa_opcode).
|
-module(aefa_opcodes).
|
||||||
|
|
||||||
-export([ mnemonic/1
|
-export([ args/1
|
||||||
|
, end_bb/1
|
||||||
|
, mnemonic/1
|
||||||
, m_to_op/1
|
, m_to_op/1
|
||||||
, opcode/1
|
, opcode/1
|
||||||
]).
|
]).
|
||||||
@ -23,6 +25,12 @@ opcode(X) when X >= 0, X =< 255 -> X;
|
|||||||
opcode({comment,X}) -> ?COMMENT(X).
|
opcode({comment,X}) -> ?COMMENT(X).
|
||||||
|
|
||||||
mnemonic(?NOP) -> 'NOP' ;
|
mnemonic(?NOP) -> 'NOP' ;
|
||||||
|
mnemonic(?RETURN) -> 'RETURN' ;
|
||||||
|
mnemonic(?PUSH) -> 'PUSH' ;
|
||||||
|
mnemonic(?JUMP) -> 'JUMP' ;
|
||||||
|
mnemonic(?INC) -> 'INC' ;
|
||||||
|
mnemonic(?CALL) -> 'CALL' ;
|
||||||
|
mnemonic(OP) -> {OP, nothandled} ;
|
||||||
mnemonic({comment,_}) -> 'COMMENT' .
|
mnemonic({comment,_}) -> 'COMMENT' .
|
||||||
|
|
||||||
m_to_op('NOP') -> ?NOP ;
|
m_to_op('NOP') -> ?NOP ;
|
||||||
@ -30,5 +38,20 @@ m_to_op('COMMENT') -> ?COMMENT("") ;
|
|||||||
m_to_op('RETURN') -> ?RETURN ;
|
m_to_op('RETURN') -> ?RETURN ;
|
||||||
m_to_op('PUSH') -> ?PUSH ;
|
m_to_op('PUSH') -> ?PUSH ;
|
||||||
m_to_op('JUMP') -> ?JUMP ;
|
m_to_op('JUMP') -> ?JUMP ;
|
||||||
|
m_to_op('INC') -> ?INC ;
|
||||||
|
m_to_op('CALL') -> ?CALL ;
|
||||||
m_to_op(Data) when 0=<Data, Data=<255 -> Data.
|
m_to_op(Data) when 0=<Data, Data=<255 -> Data.
|
||||||
|
|
||||||
|
args(?NOP) -> 0;
|
||||||
|
args(?RETURN) -> 0;
|
||||||
|
args(?PUSH) -> 1;
|
||||||
|
args(?JUMP) -> 1;
|
||||||
|
args(?INC) -> 0;
|
||||||
|
args(?CALL) -> hash;
|
||||||
|
args(_) -> 0. %% TODO do not allow this
|
||||||
|
|
||||||
|
end_bb(?RETURN) -> true;
|
||||||
|
end_bb(?JUMP) -> true;
|
||||||
|
end_bb(?CALL) -> true;
|
||||||
|
end_bb(_) -> false.
|
||||||
|
|
@ -13,7 +13,13 @@ FUNCTION jumps() -> integer
|
|||||||
JUMP 1
|
JUMP 1
|
||||||
|
|
||||||
FUNCTION inc(integer) -> integer
|
FUNCTION inc(integer) -> integer
|
||||||
INC arg0
|
INC
|
||||||
|
INC
|
||||||
|
RETURN
|
||||||
|
|
||||||
|
FUNCTION call(integer) -> integer
|
||||||
|
INC
|
||||||
|
CALL inc
|
||||||
INC
|
INC
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
@ -21,3 +27,4 @@ FUNCTION inc(integer) -> integer
|
|||||||
;; _build/default/rel/aessembler/bin/aessembler console
|
;; _build/default/rel/aessembler/bin/aessembler console
|
||||||
|
|
||||||
;; aeb_aefa:file("../../../../test/asm_code/test.fate", []).
|
;; aeb_aefa:file("../../../../test/asm_code/test.fate", []).
|
||||||
|
;; f(Asm), f(Env), f(BC), Asm = aefa_asm:read_file("../../../../test/asm_code/test.fate"), {Env, BC} = aefa_asm:asm_to_bytecode(Asm, []), aefa_asm:bytecode_to_fate_code(BC, []).
|
Loading…
x
Reference in New Issue
Block a user