box.pl

Here's the behavior of box/2:

?- box(3,5).
*****
*****
*****
true.
Here's the code:
row(Cols) :- between(1,Cols,_), write('*'), fail.
row(_) :- nl.

box(Rows,Cols) :- between(1,Rows,_), row(Cols), fail.
box(_,_).

box uses between...fail to call row(Cols) once for each row. In turn, row uses between...fail to print an asterisk for each column.

Note the use of anonymous variables in several places to avoid warnings about singleton variables.

Tracing with trace, box(3,5). might help you understand what's going on, but the interleaving of tracing output with the asterisks muddies the water.