In short: VHDL ranks its operators in seven precedence levels, with logical operators the loosest and **, abs and not the tightest, so mixed expressions need parentheses, and the classic bugs come from & sharing a level with +, from unary not binding before =, and from arithmetic that silently drops the carry.
Why operator rules deserve an interview-grade look
Most VHDL bugs that survive simulation are not exotic. They are an expression that parsed differently from what the author imagined, or a compile error that was "fixed" by sprinkling parentheses until it stopped complaining. Synthesis tools such as Synopsys Design Compiler, Vivado and Quartus all follow the IEEE 1076 grammar exactly, so an expression means the same thing everywhere. Knowing that grammar lets you read a colleague's RTL quickly and predict the hardware before you run a tool. If you are still deciding between languages, our comparison of Verilog vs VHDL shows how differently the two treat operators: VHDL is strict and verbose, Verilog is permissive and terse.
The operator families
VHDL groups its predefined operators by what they do. Each family works on specific types, and the standard libraries (ieee.std_logic_1164 and ieee.numeric_std) overload them for std_logic, unsigned and signed.
| Family | Operators | Result |
|---|---|---|
| Logical | and, or, nand, nor, xor, xnor, not | Same type as operands |
| Relational | =, /=, <, <=, >, >= | boolean |
| Adding | +, -, & (concatenation) | Numeric, or a longer array for & |
| Multiplying | *, /, mod, rem | Numeric |
| Miscellaneous | **, abs | Numeric |
| Shift | sll, srl, sla, sra, rol, ror | Same as left operand |
Key fact: relational operators return boolean, not std_logic. That is why if a = b then works while y <= (a = b); is a type error for a std_logic target (VHDL-2008 relaxes some of this with the ?= matching operators).
The precedence ladder
From loosest to tightest, the levels are: logical, relational, shift, adding, sign (unary plus and minus), multiplying, and finally the miscellaneous group. The ladder below shows them with the tightest binding on top.
Logical operators: no precedence among peers
Unlike C, VHDL gives and and or the same rank. The result is a hard rule: you may chain the same operator (a and b and c), or chain or, and, xor only in a single repeated kind, but a and b or c is a compile error. The tools are protecting you from the sum-of-products ambiguity that costs real silicon respins in other languages. Write (a and b) or c.
nand and nor are not associative, so even a nand b nand c is rejected. Spell it out: not (a and b and c) is a single three-input NAND, while (a nand b) nand c is two gates with a very different truth table.
The second trap is the unary not. Because it sits at the top of the ladder, not a = b means (not a) = b, never not (a = b). With vectors that is often still legal, so the bug compiles and simulates wrong. Use a /= b or parenthesise.
Relational operators and the vector-comparison trap
Equality on arrays compares element by element, which is what you expect. Ordering is the hazard. On a plain std_logic_vector the operators < and > are not defined by std_logic_1164 at all, and the old non-standard std_logic_unsigned package that supplied them is best avoided. With numeric_std you convert explicitly:
signal raw : std_logic_vector(7 downto 0);
...
if unsigned(raw) > 200 then
alarm <= '1';
end if;
Remember also that = compares the nine std_logic values literally, so '-' never equals '1' in =, even when you intended a don't-care. Use std_match or the VHDL-2008 ?= operator when don't-cares matter.
Arithmetic: width is your job
With numeric_std, adding two unsigned operands of widths N and M gives a result of width max(N, M). There is no carry-out bit. Add 255 and 1 in eight bits and you get 0, not 256. Multiplication is kinder: the result width is N + M, so an 18-bit by 18-bit product is 36 bits, which happens to match the DSP48-class multipliers in AMD and Intel devices. Division, mod and rem are synthesisable by most tools only when the divisor is a constant power of two; otherwise expect a large combinational block or a rejection. Addition and subtraction map onto dedicated hardware, as explained in our piece on FPGA carry chains, so a clean + is almost always the fastest option you can write.
Key fact: mod takes the sign of the right operand, rem takes the sign of the left. For unsigned data they agree, but for signed values -7 mod 4 = 1 while -7 rem 4 = -3.
Sign operators bind looser than multiplication, so -a * b is -(a * b). Conversely, a * -b is illegal: a sign cannot follow a multiplying operator directly, so write a * (-b). The same rule rejects 2 ** -1.
Concatenation: same rank as plus
The ampersand joins arrays and elements into a longer array, and it is the standard way to extend a word before arithmetic. Its precedence equals that of binary + and -, and operators at one level evaluate left to right. Therefore '0' & a + 1 is ('0' & a) + 1, not '0' & (a + 1).
Key fact: concatenation of a std_logic literal with an unsigned yields an unsigned in numeric_std, so '0' & a feeds straight into + without a type conversion.
A short checklist before you commit RTL
- Parenthesise every mix of logical operators and every mix of logical with relational.
- Size results explicitly with
resizeinstead of trusting implicit widths. - Use
unsignedorsignedfor arithmetic, neverstd_logic_vectordirectly. - Run lint (Siemens Questa, Synopsys SpyGlass or the Vivado linter) so width mismatches are flagged before simulation.
- Remember that concurrent and sequential code parse operators identically; only where the result is assigned differs, as covered in concurrent vs sequential statements in VHDL.
Teams that train juniors on these habits catch most operator bugs in code review rather than in the lab. If your team wants structured RTL training, the instructors at ChipXpert run hands-on VLSI and FPGA programs, and you can browse our own CourseTron courses for self-paced options.
Ready to write clean, synthesisable VHDL? Explore the FPGA tracks in our CourseTron courses and practise these operators on real designs.
Frequently asked questions
Why does a and b or c fail to compile in VHDL?
Because all binary logical operators share one precedence level and VHDL does not define an order among different ones. Add parentheses to state your intent, for example (a and b) or c.
Does the ampersand have higher precedence than plus?
No. Concatenation and binary plus and minus sit on the same adding level and evaluate left to right, so '0' & a + 1 is ('0' & a) + 1.
How do I keep the carry when adding two unsigned numbers?
Resize or prepend a zero to each operand first, such as ('0' & a) + ('0' & b), so the result is one bit wider than the inputs.
Is not a = b the same as a /= b?
No. not binds tighter, so it parses as (not a) = b. Use a /= b for inequality.
References & Further Reading
- IEEE Standards Association, home of IEEE 1076 (VHDL Language Reference Manual)
- VHDL overview on Wikipedia
- AMD Technical Information Portal (Vivado synthesis guide)
- Siemens Questa simulation and verification