Type Conversions
Why compilers convert between types, the difference between implicit coercion and explicit casting, the widening hierarchy, and how an SDD inserts coercions automatically during type checking.
Why Conversions Are Needed
Definition. A type conversion changes a value from one type to another so that an operator can work on it.
The reason: an operator expects all its operands to be the same type. The hardware has separate instructions for each type.
IADDadds two integers.FADDadds two IEEE-754 floats.
There is no instruction that adds an int to a float. So in 2 + 3.14, the compiler must convert 2 to a float first, then use FADD.
Deciding which operand to convert, and recording it, is the job of the type checker during semantic analysis.
Coercion vs. Cast
There are two ways a conversion appears.
Coercion (implicit) — a conversion inserted automatically by the compiler. The programmer writes nothing.
Cast (explicit) — a conversion written by the programmer using (type) expr.
| Coercion | Cast | |
|---|---|---|
| Written by | Compiler | Programmer |
| Syntax | None (invisible) | (type) expr |
| Used for | Safe conversions | Risky conversions |
Rule: the compiler coerces only when it is safe. Anything risky must be an explicit cast.
Widening vs. Narrowing
Widening = converting to a type that can hold more values (a "wider" type). It is safe — no value is lost — so the compiler does it implicitly.
Narrowing = converting to a "smaller" type that may not fit the value. It can lose data, so it needs an explicit cast.
The Widening Hierarchy
Move right = widening (implicit, safe). Move left = narrowing (explicit cast, may lose data).
Examples
One caveat:
int → floatis widening but very large ints (> 2^24) can lose their last digits, since a float has only ~7 significant digits.
An SDD That Inserts Coercions
The type checker uses an SDD to compute each expression's type and insert coercions. Two helpers do the work.
max(t1, t2)— returns the wider of two types.widen(a, t, w)— emits code to convert addressafrom typetto wider typew. Ift == wit does nothing and returnsa.
Attributes
Each E carries:
E.type— the expression's type.E.addr— the temporary holding its value.
Rule for E → E1 + E2
The same pattern applies to -, \*, and /.
widen(a, t, w)
Real compilers use a bigger table, but the idea is the same: look up
(t, w)and emit the matching coercion instruction.
Worked Example: f = i + f * 2
Trace the conversion-insertion on this statement, given int i; and float f;.
The right side parses as i + (f \* 2) (\* binds tighter):
Step 1 — f \* 2
Step 2 — i + (f \* 2)
Step 3 — assignment f = ...
Final Three-Address Code
Both int operands (
iand2) are widened tofloatbecausefloatis the widest type in the expression. No narrowing needed.
Pitfalls
Narrowing truncates, it does not round.
Widening to float can still lose precision for very large integers (past 2^24), because a float keeps only ~7 significant digits.
Signed/unsigned mixing surprises. In C, mixing signed and unsigned promotes to unsigned:
Implicit narrowing is dangerous. Java, C#, and Rust forbid it. C/C++ allow it with a warning — compile with -Wconversion.
Core rule: only widen implicitly; never narrow implicitly. Insert a coercion only in the widening direction; any narrowing must be an explicit cast.
Key Points
- Why: hardware has separate instructions per type, so operands of a mixed expression must be converted to one type.
- Coercion = implicit, compiler-inserted. Cast = explicit, programmer-written.
- Widening (
char → short → int → long → float → double) is safe and implicit. Narrowing may lose data and needs a cast. - SDD:
max(t1, t2)gives the result type;widen(a, t, w)emits a coercion (likeinttofloat) only whent != w. - Coercions show up as ordinary three-address instructions in the IR.