Module 1 · Meet C: Your First Programs, Symbol by Symbol
Syntax and Semantics: Two Different Kinds of Wrong
In this lesson
- Tell a syntax error from a semantic error, in one sentence each.
- Read eleven common compiler messages and say what actually happened.
- Explain why a compiler that refuses your program is doing you a favour.
"The bicycle drank the mountain." Every grammar rule is obeyed. The sentence still means nothing.
Programs go wrong in exactly these two ways. Either the compiler cannot read what you wrote, or it reads it perfectly and does something you did not want. The first kind stops you in one second. The second kind can hide for a month.
Maria asks Bob why his rectangle program prints 8 for a 5 by 3 room. Bob says the compiler gave no errors. That is true, and it is not a defence.
Two kinds of wrong, one sentence each
Syntax is the grammar. Is this order of symbols legal C? The compiler decides, and it is never in doubt.
Semantics is the meaning. Suppose it is legal C. What does it actually do? Only you can answer that, because only you know what you wanted.
So a syntax error is a sentence the machine cannot parse, and a semantic error is a sentence it parses into the wrong instruction.
Syntax errors: eleven messages you will actually see
Every message in this table is real output from GCC 12, the compiler behind the Playground. Read it as a lookup, not as a list to memorise. Find the shape of your message and the row tells you what happened.
| What you wrote | What GCC 12 says | What actually happened |
|---|---|---|
No ; after a statement | error: expected ';' before 'return' | The statement never ended. The fault is the line above the one named |
| A block left open | error: expected declaration or statement at end of input | A brace has no partner, and the compiler only noticed at the end of the file |
Printf("Hi\n"); | warning: implicit declaration of function 'Printf', then undefined reference to 'Printf' | C is case sensitive, so this is a different name, and nothing defines it |
No #include <stdio.h> | warning: implicit declaration of function 'printf' | The compiler had never heard of printf, guessed its shape, and built anyway |
int 2marks = 5; | error: invalid suffix "marks" on integer constant | A name may not start with a digit, so the compiler read 2marks as a number |
int total-marks = 5; | error: expected '=', ',', ';', 'asm' or '__attribute__' before '-' token | The hyphen is the minus operator. Use an underscore in names |
int int = 5; | error: two or more data types in declaration specifiers | A keyword is reserved and can never be a name |
| A name you never declared | error: 'count' undeclared (first use in this function) | A typo, or a name that lives inside a different block |
#include <studio.h> | fatal error: studio.h: No such file or directory | The preprocessor stopped at stage 1. Nothing was compiled at all |
/* outer /* inner */ outer */ | error: unknown type name 'outer' | Comments do not nest. The first */ closed it, and the rest is code |
if (marks = 90) | nothing on the Playground; warning: suggest parentheses around assignment used as truth value under -Wall | Legal syntax, almost certainly the wrong meaning. See the next section |
A version note. Rows three and four are warnings on GCC 12, which is what the Playground runs. GCC 14 makes both of them errors. If your own compiler refuses a program the Playground builds, this is usually why.
So a syntax error is never a mystery. It is a sentence in a table you will know by heart within a month.
Semantic errors: the compiler has no opinion
Here is Bob's program, in full. It compiles with zero warnings.
#include <stdio.h>
int main(void)
{
int length = 5;
int width = 3;
int area = length + width; /* legal C, wrong meaning */
printf("Area = %d\n", area);
return 0;
}
Area = 8
The area of a 5 by 3 room is 15. The compiler does not know what the word "area" means. It knows you asked it to add two integers, and adding two integers is perfectly legal.
This is the sentence to carry out of this lesson. The compiler guards your grammar. Only you guard your meaning.
So a clean build tells you the machine understood your instructions, and nothing at all about whether they were the right ones.
The = versus == trap
In C, = stores a value and == compares two values. They are one keystroke apart and they mean opposite things.
The reason this is dangerous is that = is not a statement. It is an expression, and an expression has a value. The value of marks = 60 is 60, so you can print it, which is what Example 3 does.
Later, in Module 5, you will put these inside if. There, any value that is not zero counts as true. So if (marks = 90) stores 90, sees a non-zero value, and always runs the branch. The book industry has lost a great deal of money to that single keystroke.
One defence is to write the constant first: if (90 == marks). Slip and type one = there and the compiler refuses, because you cannot store into the number 90. The habit is called a Yoda condition, because it sounds backwards.
So == asks a question, = gives an order, and the compiler is happy with either.
The one-second fix and the one-month fix
A refused build feels like the compiler being difficult. It is the opposite. Every error it prints is a bug you will not be hunting next week.
The gap between syntax and semantics is where real bugs live, and two famous ones show the size of the gap.
In 2014, Apple shipped a TLS bug known as goto fail. One goto fail; line was typed twice. The syntax was perfect, the second copy skipped a certificate check, and connections that should have been rejected were accepted.
In 2008, a Debian maintainer removed two lines from OpenSSL to silence a memory-checking warning. The code still compiled. It also crippled the random number generator, so every key generated on Debian for two years came from a tiny set.
So the compiler catches the cheap mistakes, and the expensive ones are all semantic.
How to read a message: three habits
Read the first error only. One mistake often produces twenty messages, because after the first confusion the compiler is reading your program wrongly. Fix the first, build again.
Check the line above the one named. A missing semicolon is reported where the next statement starts, not where the semicolon should be.
Copy the exact words. Search for expected declaration or statement at end of input, not "weird brace error". Exact words are a debugging tool, which is why the next lesson teaches the name of every symbol.
The shape of every GCC message
hello.c : 5 : 12 : error : expected ';' before 'return'
| | | | |
file line column severity what the compiler expected
- file and line say where the compiler was when it gave up, not always where you went wrong.
- column is the character position on that line, and it is usually exact.
- severity is
error(nothing is built),warning(it is built anyway) orfatal error(it stopped at once). - The text after it is the compiler's best guess at your intention. Read it as a guess.
The smallest semantic error there is. One operator, one wrong answer, no complaint.
#include <stdio.h>
int main(void)
{
int length = 5;
int width = 3;
printf("Area = %d\n", length + width);
return 0;
}
Area = 8
Run it and change the numbers. It is wrong for every pair except the ones where the sum happens to match the product.
Run in CompilerOne character changes. Notice that the comment now says what the line is for, so the next reader can check the meaning without knowing geometry.
#include <stdio.h>
int main(void)
{
int length = 5;
int width = 3;
/* Area of a rectangle is length times width, in square metres. */
printf("Area = %d\n", length * width);
return 0;
}
Area = 15
A semantic bug is fixed by thinking, not by reading a message. That is why tracing a program on paper is a real skill and not a beginner's crutch.
Run in CompilerThis program prints the value of an assignment and the value of a comparison, side by side. It is the clearest way to see that they are different things.
#include <stdio.h>
int main(void)
{
int marks = 90;
printf("marks is %d\n", marks);
printf("marks == 90 gives %d\n", marks == 90);
printf("marks = 60 gives %d\n", marks = 60);
printf("marks is now %d\n", marks);
return 0;
}
marks is 90
marks == 90 gives 1
marks = 60 gives 60
marks is now 60
A comparison gives 1 for true and 0 for false. An assignment gives the value it stored, and it changes the variable on the way. Line three did both, which is exactly why the trap works.
Run in CompilerZara adds two subject marks and prints the average. Every line is legal, every name is honest, and the answer is still wrong.
#include <stdio.h>
int main(void)
{
int physics = 75;
int chemistry = 80;
int total = physics + chemistry;
int average = total / 2;
printf("Total: %d\n", total);
printf("Average: %d\n", average);
return 0;
}
Total: 155
Average: 77
The average of 75 and 80 is 77.5. Dividing one whole number by another in C throws away everything after the point. No warning is printed, because nothing is ungrammatical. Module 4 gives you the repair.
Run in CompilerWhere this is used
- Apple, 2014. The
goto failbug in Apple's TLS code was a duplicated line with perfect syntax. It skipped the check that a certificate belonged to the site you were visiting. - Debian OpenSSL, 2008. Two lines were deleted to silence a warning from a memory checker. The build stayed clean and the random number generator became predictable for two years.
- Every CI pipeline. A merge blocked by "build failed" is this lesson in production: the compiler refusing a syntax error before a human has to find it.
- Static analysis. Tools like
clang-tidyand Coverity exist to guess at semantics, and large projects run them on every change. They catch some of the gap, never all of it.
Common mistakes
1. Fixing the line the message names.
int marks = 90
printf("%d\n", marks);
GCC says error: expected ';' before 'printf' and points at line 2. Line 2 is fine. The declaration on line 1 never ended, and the compiler noticed only when a word arrived that cannot continue it.
2. Assuming a silent compiler means a correct program.
int average = total / count; /* count might be 0 */
No message, at any warning level. Division by zero is not a grammar mistake, so the compiler passes it on. If count is 0 the program dies at runtime, which is the brain teaser below.
3. Writing = where you meant ==.
if (marks = 90) {
printf("Top of the class\n");
}
The Playground says nothing at all, because the check for this lives behind -Wall. On your own machine, gcc -Wall gives warning: suggest parentheses around assignment used as truth value. The program stores 90 and always prints the line.
4. Nesting comments.
/* outer /* inner */ outer */
You get error: unknown type name 'outer', which looks unrelated to comments. The first */ ended the comment, so the words after it became code. To comment out a block that already has comments in it, use // on each line.
Bob's doubling program has three syntax errors and nothing else wrong. Repair it until it compiles, then check that it prints the right answer.
Input. One line with one integer n.
Output. One line: Double of n is 2n, with the two numbers filled in.
Constraints. 0 <= n <= 1000.
Sample. Input 21 gives Double of 21 is 42.
#include <stdio.h>
int main(void)
{
int n
scanf("%d", &n);
Printf("Double of %d is %d\n", n, n * 2);
return 0;
Rules. Fix only what the compiler complains about. Read the first message, repair one thing, build again, and count how many builds it takes you.
Graded against hidden tests in the module's Problems lesson, as fix-the-syntax.
This one compiles on the first try and is still wrong. Maria wants the area of a square tile, and the program prints the distance around it.
Input. One line with one integer s, the side of the square in centimetres.
Output. One line: Area is X, where X is the area in square centimetres.
Constraints. 1 <= s <= 100.
Sample. Input 4 gives Area is 16. The starter prints 16 as well, which is the trap: 4 is the one side length where the perimeter and the area agree.
#include <stdio.h>
int main(void)
{
int s;
scanf("%d", &s);
printf("Area is %d\n", 4 * s);
return 0;
}
Check yourself. Try s equal to 5 before and after your fix. A test case that passes for the wrong reason is how semantic bugs survive.
Graded as square-area.
One character is wrong here, and it is the one from this lesson. The program should answer the question "are these two marks equal", printing 1 for yes and 0 for no.
Input. One line with two integers a b.
Output. One line: 1 if they are equal, otherwise 0.
Constraints. 0 <= a, b <= 100.
Sample. Input 90 90 gives 1. Input 90 60 gives 60 from the starter, which is not even one of the two allowed answers.
#include <stdio.h>
int main(void)
{
int a, b;
scanf("%d %d", &a, &b);
printf("%d\n", a = b);
return 0;
}
Check yourself. After the fix, input 0 0 must print 1. Before the fix it prints 0, which looks correct and is an accident.
Common doubts
If it compiles, is my program correct?
No. It means the grammar is legal. Example 1 in this lesson compiles cleanly and gives the wrong answer every time. Only your own test inputs can tell you about meaning.
Can a compiler ever catch a semantic error?
Sometimes, by accident of types:
-Wallspots a%dfed a string, and a sanitizer catches a bad memory access at runtime. None of them knows what your program is for.Why do I get twenty errors from one mistake?
Because after the first confusion the compiler is parsing your program wrongly and keeps reporting what it finds. Fix the first one and rebuild. Most of the rest usually vanish.
Is a warning something I can ignore?
Treat it as an error that has not bitten you yet. Every warning in the table above describes genuinely broken code, and professional projects build with warnings turned into errors.
Do I have to memorise these messages?
No. You will recognise the first five within a week of writing code. Keep this table open while you work, and copy the exact words when you search.
Key takeaways
- Syntax is the grammar the compiler checks; semantics is the meaning only you can check.
- A syntax error is cheap: one second, one message, one fix.
- A clean build says nothing about whether the answer is right.
=stores and has a value;==compares and gives 1 or 0.- Read the first message, look one line above it, and copy its exact words.
- The famous bugs (goto fail, Debian OpenSSL) were all perfectly legal C.
Next you will learn the name of every symbol in C, so that the messages in this table become sentences you can read out loud.
End of lesson 2
Mark it done, and your progress moves with you.
Next: The Symbol Dictionary: Every Sign in C and What It Says