Inside the terminal, you can’t distinguish shift-return from return. However you can distinguish meta-return from return and utop already binds meta-return to inserting a newline without ending the expression.
BTW, you can add this in your ~/.ocamlinit
to make return automatically insert the double semicolon:
LTerm_read_line.bind
[ { control = false; meta = false; shift = false; code = Enter } ]
[ UTop.end_and_accept_current_phrase ]