Prolog has a syntax that make simpler symbolic processing. Here is how I would wrote the code:
assign([First, '=' | Expr]) :-
id(First),
expr(Expr).
expr([First | Tail]) :-
id(First),
right(Tail).
expr(['(' | Rest]) :-
append(Expr, [')'|Follow], Rest),
expr(Expr),
right(Follow).
right([]).
right([Op|Expr]) :-
op(Op),
expr(Expr).
note the use of pattern matching, instead of the procedural check based on length/2, etc
assign([First, '=' | Expr]) :-
...
this means: use this clause only if the argument is a list with '=' in second position and the tail is a list we name Expr.
(about syntax: often we don't need to quote atoms, but the rules are somewhat complex. For instance, this is a valid query
?- assign([c,=,a,+,b]).
No need to quote atoms here, so the code could be assign([First, = | Expr]) :- ...
)
Then the body issues the appropriate check for first list'element (i.e. id(First)) and the Expr.
To get the Expr between parenthesis, I used an idiomatic approach
expr(['(' | Rest]) :-
append(Expr, [')'|Follow], Rest),
...
this append/3 can succeed only if Rest contains an Expr followed by ')'.
I think your original approach missed the right(Follow).
We need it because the grammar is recursive, after an operator...