Project 5: Type Promotion Tester

Build an interactive tool demonstrating C’s integer promotion and conversion rules that catch even experienced programmers off guard.


Quick Reference

Attribute Value
Language C
Difficulty Level 2 (Beginner-Intermediate)
Time Weekend (6-12 hours)
Book Reference Expert C Programming, Chapter 2
Coolness Bug Hunter’s Essential - Catches silent bugs
Portfolio Value High - Demonstrates deep C understanding

1. Learning Objectives

By completing this project, you will:

  1. Master integer promotion rules - Understand why char and short become int before any arithmetic
  2. Understand usual arithmetic conversions - Know the “balancing” rules when mixing types
  3. Recognize signed/unsigned comparison traps - Catch the infamous -1 > 0u bug
  4. Predict expression results - Know what type and value any expression produces
  5. Debug silent conversion bugs - Recognize when implicit conversions cause incorrect results
  6. Write safer comparisons - Avoid the traps that cause security vulnerabilities
  7. Read compiler warnings intelligently - Understand what -Wsign-compare is telling you
  8. Build defensive code - Apply patterns that prevent type conversion bugs

2. Theoretical Foundation

2.1 Core Concepts

Integer Promotion Rules

Integer promotion is C’s first step before any arithmetic operation. It widens small types to int:

INTEGER PROMOTION RULE:
=======================

If an integer type has fewer bits than int, it is promoted to int
before ANY arithmetic operation.

Types that get promoted:
  - char       → int
  - signed char → int
  - unsigned char → int (usually fits in int)
  - short      → int
  - unsigned short → int (if int can represent all values)
  - _Bool      → int

WHY? Historical efficiency - CPUs are optimized for int-sized operations.

EXAMPLE:
========
char a = 100, b = 100;
char c = a + b;    // What happens?

Step 1: a is promoted to int (100)
Step 2: b is promoted to int (100)
Step 3: Addition happens: 100 + 100 = 200 (as int)
Step 4: Result truncated to char: 200 → -56 (if char is signed)

The addition NEVER overflows! It happens in int.
The overflow happens during the assignment back to char.

Usual Arithmetic Conversions (The “Balancing” Rules)

When two operands have different types, C applies the “usual arithmetic conversions” to find a common type:

USUAL ARITHMETIC CONVERSIONS:
=============================

Step 1: Integer promotions happen first (char/short → int)

Step 2: If types differ, convert to the "higher" type:

   ┌─────────────────────────────────────────────────────┐
   │        TYPE CONVERSION HIERARCHY                    │
   │                                                     │
   │   long double     ← highest                         │
   │        ↑                                            │
   │   double                                            │
   │        ↑                                            │
   │   float                                             │
   │        ↑                                            │
   │   ─────────────── integer types below ───────────── │
   │        ↑                                            │
   │   unsigned long long                                │
   │        ↑                                            │
   │   long long                                         │
   │        ↑                                            │
   │   unsigned long                                     │
   │        ↑                                            │
   │   long                                              │
   │        ↑                                            │
   │   unsigned int                                      │
   │        ↑                                            │
   │   int             ← lowest after promotion          │
   └─────────────────────────────────────────────────────┘

THE CRITICAL RULE FOR SIGNED/UNSIGNED:
======================================

When signed and unsigned integers of the same rank meet:
  → THE SIGNED VALUE IS CONVERTED TO UNSIGNED

This is the source of COUNTLESS bugs!

The Infamous -1 > 0u Bug

This is the most famous type conversion trap in C:

THE DISASTER SCENARIO:
======================

int a = -1;
unsigned int b = 1;

if (a < b) {
    printf("-1 is less than 1\n");     // You expect this
} else {
    printf("-1 is NOT less than 1\n"); // THIS PRINTS!
}

WHY?
====

Step 1: Compare a (signed int) with b (unsigned int)
Step 2: Usual arithmetic conversions apply
Step 3: Signed int converts to unsigned int
Step 4: -1 as unsigned int = UINT_MAX = 4294967295 (on 32-bit)
Step 5: 4294967295 > 1, so a is "greater" than b!

VISUAL:
=======

        Signed int:           Unsigned int:
        ───────────           ─────────────
        -1 (0xFFFFFFFF)  →    4294967295
        -2 (0xFFFFFFFE)  →    4294967294
        ...
        0  (0x00000000)  →    0
        1  (0x00000001)  →    1

The bit pattern stays the same, but the interpretation changes!

2.2 Why This Matters

Security Vulnerabilities

Type conversion bugs have caused real-world security disasters:

REAL-WORLD BUG PATTERN #1: Length Check Bypass
==============================================

void copy_data(char *dest, char *src, int len) {
    if (len > MAX_SIZE) {
        return;  // Safety check
    }
    memcpy(dest, src, len);  // len becomes size_t (unsigned)!
}

// Attack: pass len = -1
// Check: -1 > MAX_SIZE is false (signed comparison)
// memcpy: (size_t)-1 = huge number → buffer overflow!


REAL-WORLD BUG PATTERN #2: Array Index Bug
==========================================

int arr[100];
int index = user_input();  // Returns -5

if (index < sizeof(arr) / sizeof(arr[0])) {  // BUG!
    printf("%d\n", arr[index]);  // Reads before array!
}

// sizeof returns size_t (unsigned)
// index (-5) converts to unsigned → huge positive number
// Check fails, reads out of bounds


REAL-WORLD BUG PATTERN #3: Loop Counter Underflow
=================================================

unsigned int count = get_count();
for (int i = count - 1; i >= 0; i--) {  // BUG!
    process(data[i]);
}

// If count = 0, then count - 1 = UINT_MAX (wraps around)
// i starts at UINT_MAX, will never be < 0
// Infinite loop or massive out-of-bounds access

Subtle Arithmetic Errors

Even without security implications, these bugs cause incorrect calculations:

SUBTLE BUG: Temperature Sensor
==============================

unsigned char sensor_reading = 250;  // Raw ADC value
char offset = -10;                   // Calibration offset

int temperature = sensor_reading + offset;
// Expected: 240
// Actual: 240 (this works!)

// But what if:
unsigned char sensor_reading = 5;
char offset = -10;

int temperature = sensor_reading + offset;
// Expected: -5
// Actual: -5 (this also works!)

// The promotion to int before addition saves us here.
// But storing back to unsigned would lose the sign!

unsigned int temp_unsigned = sensor_reading + offset;
// temp_unsigned = -5 as unsigned = 4294967291!

2.3 Historical Context

C’s type conversion rules emerged from several historical factors:

WHY C HAS THESE RULES:
======================

1. PDP-11 HERITAGE (1970s)
   ─────────────────────────
   The PDP-11 had 16-bit words.
   char operations promoted to int because:
   - Register operations were int-sized
   - Byte operations were less efficient
   - Memory was accessed in words anyway

2. "PRESERVE VALUE" vs "PRESERVE SIGNEDNESS"
   ─────────────────────────────────────────
   C89 chose "preserve value" for integer promotion:
   - unsigned char → int (if int can hold all values)
   - This is usually safe on modern systems

   For mixed signed/unsigned operations, C chose:
   - Convert to unsigned (preserve bit pattern)
   - This is often NOT what programmers expect

3. BACKWARDS COMPATIBILITY
   ─────────────────────────
   Once established, these rules couldn't change:
   - Existing code depends on them
   - Changing would break millions of programs
   - The C standard codified existing practice

4. PERFORMANCE CONSIDERATIONS
   ─────────────────────────────
   Promoting to int was the "natural" size:
   - Fastest for arithmetic on target CPUs
   - Avoids overflow in many calculations
   - Matches register size

2.4 Common Misconceptions

MISCONCEPTION #1: "Small types stay small during arithmetic"
============================================================
WRONG: char a = 100, b = 100; char c = a + b;
       // Addition happens as int, not char!

REALITY: All arithmetic promotes to at least int first.


MISCONCEPTION #2: "Overflow is detected"
========================================
WRONG: People expect wrap-around or errors.

REALITY: Signed overflow is UNDEFINED BEHAVIOR.
         Unsigned overflow is well-defined (wraps).
         Type conversions can overflow silently.


MISCONCEPTION #3: "sizeof returns int"
======================================
WRONG: int i = -1; if (i < sizeof(array)) ...

REALITY: sizeof returns size_t (unsigned).
         Comparison converts i to unsigned!


MISCONCEPTION #4: "Casting fixes everything"
============================================
WRONG: if ((unsigned)x < y) ... // "Now it's safe"

REALITY: You've just converted x to unsigned,
         which changes -1 to UINT_MAX!


MISCONCEPTION #5: "Compilers warn about all problems"
=====================================================
WRONG: "I use -Wall, I'm safe."

REALITY: Many conversion bugs don't trigger warnings
         unless you also use -Wconversion, -Wsign-compare.

3. Project Specification

3.1 What You Will Build

An interactive demonstration tool called typepromo that shows C’s type conversion rules in action through live experiments:

$ ./typepromo
╔════════════════════════════════════════════════════════════════╗
║           C TYPE PROMOTION TESTER                              ║
║    Exposing the Silent Bugs in Integer Conversions            ║
╚════════════════════════════════════════════════════════════════╝

Select an experiment:
  1. Integer Promotion Basics
  2. Signed vs Unsigned Comparisons
  3. The -1 > 0u Bug
  4. char Arithmetic Surprises
  5. sizeof Comparison Traps
  6. Loop Counter Hazards
  7. Bitwise Operation Types
  8. All Experiments
  9. Interactive Mode (enter expressions)
  0. Exit

Choice: 3

═══════════════════════════════════════════════════════════════════
 EXPERIMENT: The Infamous -1 > 0u Bug
═══════════════════════════════════════════════════════════════════

Code being tested:
┌────────────────────────────────────────────────────────────────┐
│  int a = -1;                                                   │
│  unsigned int b = 1;                                           │
│  printf("Is a < b? %s\n", (a < b) ? "YES" : "NO");            │
└────────────────────────────────────────────────────────────────┘

What you might expect: YES (-1 is less than 1)
What actually happens:  NO!

WHY?
────
  1. a (-1) and b (1u) have different types
  2. Usual arithmetic conversions apply
  3. int converts to unsigned int
  4. -1 as unsigned = 4294967295 (0xFFFFFFFF)
  5. 4294967295 > 1, so (a < b) is FALSE

Memory representation:
  a (signed int):   0xFFFFFFFF = -1
  a (as unsigned):  0xFFFFFFFF = 4294967295

┌────────────────────────────────────────────────────────────────┐
│ WARNING: This is a common source of security vulnerabilities! │
│          Always be careful comparing signed with unsigned.    │
└────────────────────────────────────────────────────────────────┘

3.2 Functional Requirements

  1. Integer Promotion Demonstration:
    • Show how char, short promote to int
    • Demonstrate promotion before arithmetic
    • Show value vs type after promotion
    • Visualize with hex and decimal
  2. Usual Arithmetic Conversions:
    • Show the type hierarchy
    • Demonstrate mixed-type operations
    • Show which type “wins”
    • Display the conversion steps
  3. Signed/Unsigned Comparison Tests:
    • The -1 > 0u classic bug
    • Negative array index comparisons
    • sizeof comparison traps
    • Loop counter hazards
  4. char Arithmetic Experiments:
    • Overflow during promotion
    • Truncation on assignment
    • Signed vs unsigned char
    • Character arithmetic surprises
  5. Interactive Expression Tester:
    • Enter arbitrary expressions
    • Show resulting type
    • Show value in multiple formats
    • Explain the conversion steps
  6. Quiz Mode:
    • Present code snippets
    • Ask user to predict output
    • Reveal answer with explanation

3.3 Non-Functional Requirements

  • Educational: Each experiment explains WHY, not just WHAT
  • Visual: ASCII diagrams showing memory representations
  • Safe: No undefined behavior in the test tool itself
  • Portable: Works on 32-bit and 64-bit systems (shows both where relevant)
  • Interactive: User controls which experiments to run
  • Color-coded (optional): Highlight warnings in red, success in green

3.4 Example Usage / Output

$ ./typepromo --experiment char-arithmetic

═══════════════════════════════════════════════════════════════════
 EXPERIMENT: char Arithmetic Surprises
═══════════════════════════════════════════════════════════════════

Scenario 1: Adding two chars
────────────────────────────

Code:
┌────────────────────────────────────────────────────────────────┐
│  char c1 = 100, c2 = 100;                                      │
│  char sum = c1 + c2;                                           │
│  printf("sum = %d\n", sum);                                    │
└────────────────────────────────────────────────────────────────┘

Step-by-step execution:
  1. c1 (100) promotes to int: 100
  2. c2 (100) promotes to int: 100
  3. Addition in int: 100 + 100 = 200
  4. Result (200) assigned to char
  5. 200 doesn't fit in signed char (-128 to 127)
  6. Truncation/wrap: 200 - 256 = -56

Result: sum = -56 (not 200!)

Memory view:
  200 as int:  0x000000C8
  200 as char: 0xC8 = -56 (signed) or 200 (unsigned)

Key insight: The addition itself doesn't overflow!
             Overflow happens on assignment to the smaller type.

────────────────────────────────────────────────────────────────

Scenario 2: unsigned char multiplication
────────────────────────────────────────

Code:
┌────────────────────────────────────────────────────────────────┐
│  unsigned char a = 200, b = 200;                               │
│  unsigned char product = a * b;                                │
│  printf("product = %u\n", product);                            │
└────────────────────────────────────────────────────────────────┘

Step-by-step:
  1. a (200) promotes to int: 200
  2. b (200) promotes to int: 200
  3. Multiplication in int: 200 * 200 = 40000
  4. 40000 truncated to unsigned char (0-255)
  5. 40000 % 256 = 64

Result: product = 64 (not 40000!)

────────────────────────────────────────────────────────────────

$ ./typepromo --interactive

INTERACTIVE TYPE TESTER
Enter expressions to see their types and values.
Type 'help' for commands, 'quit' to exit.

> -1 < 1u
Expression: -1 < 1u

Type analysis:
  Left operand:  -1 (int, value -1)
  Right operand: 1u (unsigned int, value 1)

Conversion:
  int converts to unsigned int (same rank, unsigned wins)
  -1 (int) → 4294967295 (unsigned int)

Comparison: 4294967295 < 1
Result: 0 (false)

WARNING: This is probably NOT what you intended!

> (char)200 + 1
Expression: (char)200 + 1

Type analysis:
  (char)200 = -56 (signed char interpretation)
  Promotes to int: -56
  1 is int: 1
  Addition: -56 + 1 = -55

Result: -55 (type: int)

> sizeof(int) - 5
Expression: sizeof(int) - 5

Type analysis:
  sizeof(int) = 4 (type: size_t, which is unsigned)
  5 is int: 5
  5 converts to size_t: 5
  Subtraction: 4 - 5 as size_t

Result: 18446744073709551615 (huge number due to unsigned wrap!)

WARNING: Subtracting from sizeof can wrap around!

3.5 Real World Outcome

After completing this project, you will:

SKILLS GAINED:
==============

1. BUG DETECTION
   - Spot type conversion bugs in code review
   - Recognize dangerous patterns immediately
   - Understand compiler warnings deeply

2. SECURE CODING
   - Write correct comparison functions
   - Handle user input sizes safely
   - Avoid integer overflow vulnerabilities

3. DEBUGGING
   - Explain mysterious calculation results
   - Trace type conversions step by step
   - Use tools (-Wconversion) effectively

4. SYSTEMS UNDERSTANDING
   - Know what the compiler does with your code
   - Predict assembly output for type operations
   - Understand ABI implications

PATTERNS YOU'LL RECOGNIZE:
==========================

Before this project:
  "Why is my loop infinite?"
  "Why did my bounds check fail?"
  "Why is this negative number huge?"

After this project:
  "Ah, it's a signed/unsigned comparison."
  "sizeof returns size_t, that's the bug."
  "The char wrapped around on assignment."

4. Solution Architecture

4.1 High-Level Design

┌──────────────────────────────────────────────────────────────────┐
│                    TYPE PROMOTION TESTER                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐       │
│  │   CLI/Menu   │───▶│  Experiment  │───▶│   Output     │       │
│  │   Handler    │    │   Engine     │    │   Formatter  │       │
│  └──────────────┘    └──────────────┘    └──────────────┘       │
│         │                   │                   │                │
│         │                   │                   │                │
│         ▼                   ▼                   ▼                │
│  ┌──────────────────────────────────────────────────────┐       │
│  │                    EXPERIMENTS                        │       │
│  │                                                       │       │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │       │
│  │  │  Integer    │  │  Signed vs  │  │    char     │   │       │
│  │  │  Promotion  │  │  Unsigned   │  │  Arithmetic │   │       │
│  │  └─────────────┘  └─────────────┘  └─────────────┘   │       │
│  │                                                       │       │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │       │
│  │  │   sizeof    │  │    Loop     │  │   Bitwise   │   │       │
│  │  │   Traps     │  │   Counter   │  │    Types    │   │       │
│  │  └─────────────┘  └─────────────┘  └─────────────┘   │       │
│  │                                                       │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │                    UTILITIES                          │       │
│  │                                                       │       │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │       │
│  │  │  Type Info  │  │   Memory    │  │   Format    │   │       │
│  │  │  Printer    │  │   Dumper    │  │   Helpers   │   │       │
│  │  └─────────────┘  └─────────────┘  └─────────────┘   │       │
│  │                                                       │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

4.2 Key Components

COMPONENT RESPONSIBILITIES:
===========================

1. CLI/Menu Handler
   - Parse command-line arguments
   - Display menu and get user choice
   - Handle interactive mode

2. Experiment Engine
   - Run specific experiments
   - Execute code snippets
   - Capture results for display

3. Output Formatter
   - Create ASCII diagrams
   - Format type information
   - Color output (if terminal supports)

4. Type Info Printer
   - Display type names
   - Show sizeof for types
   - Print min/max values

5. Memory Dumper
   - Show hex representation
   - Visualize bit patterns
   - Display signed vs unsigned interpretation

4.3 Data Structures

/* Experiment definition */
typedef struct {
    const char *name;
    const char *description;
    void (*run_func)(void);
} Experiment;

/* Type information */
typedef struct {
    const char *name;
    size_t size;
    int is_signed;
    long long min_value;
    unsigned long long max_value;
} TypeInfo;

/* Expression result (for interactive mode) */
typedef struct {
    const char *type_name;
    union {
        long long signed_val;
        unsigned long long unsigned_val;
        double float_val;
    } value;
    int is_signed;
    int is_floating;
} ExprResult;

/* Conversion step (for explanation) */
typedef struct {
    const char *from_type;
    const char *to_type;
    const char *value_before;
    const char *value_after;
    const char *reason;
} ConversionStep;

4.4 Algorithm Overview

EXPERIMENT EXECUTION FLOW:
==========================

1. Display experiment header and code
2. Show "what you might expect"
3. Execute the actual code
4. Display the actual result
5. Explain WHY with step-by-step analysis
6. Show memory representation
7. Provide warning/advice

TYPE ANALYSIS ALGORITHM:
========================

1. Parse the expression (for interactive mode)
2. Identify operand types
3. Apply integer promotion rules
4. Apply usual arithmetic conversions
5. Determine result type
6. Calculate result value
7. Format for display

MEMORY VISUALIZATION:
=====================

1. Get value and type
2. Convert to byte array
3. Display in hex
4. Show signed interpretation
5. Show unsigned interpretation
6. Highlight sign bit if relevant

5. Implementation Guide

5.1 Development Environment Setup

# Required tools
gcc --version          # GCC 7+ recommended for good warnings
clang --version        # Alternative compiler

# Recommended compiler flags for development
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -g
CFLAGS += -Wconversion -Wsign-conversion -Wsign-compare
CFLAGS += -fsanitize=undefined

# For seeing type sizes
echo '#include <stdio.h>
int main() {
    printf("char: %zu, short: %zu, int: %zu, long: %zu\n",
           sizeof(char), sizeof(short), sizeof(int), sizeof(long));
    return 0;
}' | gcc -x c - -o /tmp/sizes && /tmp/sizes

5.2 Project Structure

typepromo/
├── src/
│   ├── main.c                 # Entry point, menu handling
│   ├── experiments.c          # All experiment implementations
│   ├── experiments.h          # Experiment declarations
│   ├── type_utils.c           # Type information utilities
│   ├── type_utils.h           # Type utility declarations
│   ├── display.c              # Output formatting
│   ├── display.h              # Display function declarations
│   └── interactive.c          # Interactive expression mode
├── include/
│   └── common.h               # Common definitions
├── tests/
│   └── test_experiments.c     # Unit tests
├── Makefile
└── README.md

5.3 The Core Question You’re Answering

“How does C silently convert between integer types, and what bugs result?”

Every experiment in this tool answers a piece of this question:

  • Integer promotion: Small types become int
  • Usual arithmetic conversions: Mixed types find a common type
  • Assignment conversions: Large types truncate into small types
  • Comparison conversions: Signed becomes unsigned when mixed

5.4 Concepts You Must Understand First

Before implementing, be sure you can answer:

  1. Why does char + char produce int?
    • Integer promotion rule: types smaller than int promote to int before any operation
    • This prevents overflow in intermediate calculations
    • The result type is int, not char
  2. What are the “usual arithmetic conversions”?
    • After integer promotion, if operands differ:
    • Float types: convert to higher precision
    • Integer types: convert to higher rank, preferring unsigned
  3. Why is -1 > 0u true?
    • Comparing int with unsigned int
    • Same rank, unsigned wins
    • -1 converts to UINT_MAX (all bits set = maximum unsigned value)
    • UINT_MAX > 0 is true
  4. What does sizeof return?
    • Returns size_t, which is an unsigned type
    • Comparing with negative values converts the negative to unsigned

5.5 Questions to Guide Your Design

For the main structure:

  • How will you organize experiments for easy navigation?
  • How will you make it easy to add new experiments?
  • What information should every experiment display?

For type display:

  • How will you show both signed and unsigned interpretations?
  • How will you visualize bit patterns?
  • How will you explain the conversion rules?

For correctness:

  • How will you ensure your tool itself doesn’t have type bugs?
  • How will you handle platform differences (32-bit vs 64-bit)?
  • How will you test that explanations match actual behavior?

5.6 Thinking Exercise

Before coding, work through these conversions by hand:

Exercise 1: Trace the types

char a = 100;
char b = 50;
int result = a + b;
// Q: What type is (a + b) before assignment?
// Q: What is the value at each step?

Exercise 2: Predict the output

unsigned char x = 255;
char y = 1;
printf("%d\n", x + y);
// Q: What type is the result?
// Q: What is printed?

Exercise 3: Find the bug

size_t len = strlen(str);
for (int i = len - 1; i >= 0; i--) {
    putchar(str[i]);
}
// Q: What happens if str is empty?
// Q: What type is (len - 1)?

Exercise 4: Why no warning?

int x = -1;
if (x < sizeof(int)) {  // No warning with -Wall
    printf("x is small\n");
}
// Q: Why does this print "x is small" is printed?
// Q: Is x really "small"?

5.7 Hints in Layers

Hint 1: Starting Point

Begin with the simplest experiment: showing type sizes and ranges.

void experiment_type_info(void) {
    printf("Type sizes on this platform:\n");
    printf("  char:          %zu bytes (signed: %d)\n",
           sizeof(char), CHAR_MIN < 0);
    printf("  short:         %zu bytes\n", sizeof(short));
    printf("  int:           %zu bytes\n", sizeof(int));
    printf("  long:          %zu bytes\n", sizeof(long));
    printf("  size_t:        %zu bytes\n", sizeof(size_t));

    printf("\nRanges:\n");
    printf("  signed char:   %d to %d\n", SCHAR_MIN, SCHAR_MAX);
    printf("  unsigned char: 0 to %u\n", UCHAR_MAX);
    // ... etc
}

This establishes a foundation and helps users understand their platform.

Hint 2: Demonstrating Integer Promotion

Use _Generic (C11) or explicit casting to show type changes:

// Helper macro to print type name
#define typename(x) _Generic((x), \
    char: "char", \
    signed char: "signed char", \
    unsigned char: "unsigned char", \
    short: "short", \
    unsigned short: "unsigned short", \
    int: "int", \
    unsigned int: "unsigned int", \
    long: "long", \
    unsigned long: "unsigned long", \
    default: "unknown")

void experiment_promotion(void) {
    char c = 1;
    printf("Type of c:     %s\n", typename(c));      // char
    printf("Type of c + 0: %s\n", typename(c + 0));  // int!
    printf("Type of c + c: %s\n", typename(c + c));  // int!
}

This shows that even c + 0 promotes c to int.

Hint 3: Visualizing Memory Representation

Show the bit pattern to explain signed/unsigned differences:

void show_as_bytes(void *ptr, size_t size, const char *label) {
    unsigned char *bytes = (unsigned char *)ptr;
    printf("%s bytes: ", label);
    for (size_t i = 0; i < size; i++) {
        printf("%02x ", bytes[size - 1 - i]);  // Big-endian display
    }
    printf("\n");
}

void experiment_signed_unsigned(void) {
    int neg = -1;
    unsigned int pos = (unsigned int)neg;

    printf("int -1:\n");
    show_as_bytes(&neg, sizeof(neg), "  ");
    printf("  As signed:   %d\n", neg);
    printf("  As unsigned: %u\n", pos);
}
Hint 4: Making Comparisons Explicit

Show exactly what values are being compared:

void experiment_comparison_bug(void) {
    int a = -1;
    unsigned int b = 1;

    // Show the conversion explicitly
    unsigned int a_as_unsigned = (unsigned int)a;

    printf("int a = -1\n");
    printf("unsigned int b = 1\n");
    printf("\n");
    printf("Comparison: a < b\n");
    printf("Step 1: Convert a to unsigned int\n");
    printf("        -1 as unsigned = %u\n", a_as_unsigned);
    printf("Step 2: Compare %u < %u\n", a_as_unsigned, b);
    printf("Result: %s\n", (a_as_unsigned < b) ? "true" : "false");
    printf("\nActual (a < b) result: %s\n", (a < b) ? "true" : "false");
}
Hint 5: Interactive Expression Testing

For a simple interactive mode, pre-define common expressions:

typedef struct {
    const char *expression;
    const char *expected;
    const char *explanation;
} TestCase;

TestCase cases[] = {
    {"-1 < 1u", "false (0)",
     "-1 converts to UINT_MAX, which is > 1"},
    {"(char)200 + 1", "-55",
     "200 as signed char is -56, plus 1 is -55"},
    {"sizeof(int) - 5", "depends on platform...",
     "If int is 4 bytes: 4 - 5 wraps to SIZE_MAX - 1"},
};

// Run each and compare

A full expression parser is complex; start with predefined cases.

Hint 6: Platform Awareness

Handle 32-bit vs 64-bit differences:

void show_platform_info(void) {
    printf("Platform information:\n");
    printf("  sizeof(int):    %zu\n", sizeof(int));
    printf("  sizeof(long):   %zu\n", sizeof(long));
    printf("  sizeof(void*):  %zu\n", sizeof(void*));
    printf("  sizeof(size_t): %zu\n", sizeof(size_t));

    #if UINT_MAX == 0xFFFFFFFF
    printf("  unsigned int is 32-bit\n");
    printf("  -1 as unsigned int = %u\n", (unsigned int)-1);
    #endif

    #if ULONG_MAX == 0xFFFFFFFFFFFFFFFF
    printf("  unsigned long is 64-bit\n");
    #endif
}

5.8 The Interview Questions They’ll Ask

After completing this project, you will be ready for these questions:

  1. “What is integer promotion in C?”
    • Explain: char/short/bool become int before any arithmetic
    • Why: Historical (PDP-11), efficiency, overflow prevention
    • Consequence: Result of char + char is int, not char
  2. “Explain the usual arithmetic conversions.”
    • After promotion, if types differ, convert to common type
    • For integers: higher rank wins, unsigned beats signed at same rank
    • Example: int + unsigned int → both become unsigned int
  3. “Why is -1 > 0u true in C?”
    • Comparing int (-1) with unsigned int (0u)
    • Same rank, unsigned wins
    • -1 converts to UINT_MAX (all bits set)
    • UINT_MAX > 0, so true
  4. “What bugs can type conversions cause?”
    • Security: Length checks bypassed, buffer overflows
    • Logic: Comparisons behave unexpectedly
    • Arithmetic: Truncation, wrap-around
    • Example: if (user_len > MAX) fails if user_len is negative
  5. “How do you prevent signed/unsigned comparison bugs?”
    • Use consistent types in comparisons
    • Cast explicitly when necessary
    • Use compiler warnings: -Wsign-compare, -Wconversion
    • Validate input before using in comparisons
  6. “What does sizeof return and why does it matter?”
    • Returns size_t, an unsigned type
    • Comparing signed values with sizeof converts to unsigned
    • if (index < sizeof(arr)) can fail for negative index

5.9 Books That Will Help

Topic Book Chapter
Type conversions Expert C Programming Ch. 2 “This is Not a Bug, It’s a Language Feature”
Integer representation CS:APP Ch. 2 “Representing and Manipulating Information”
Secure coding Effective C Ch. 3 “Arithmetic Types”
C standard C11/C17 Standard Section 6.3 “Conversions”
Common pitfalls C Traps and Pitfalls Ch. 2 “Lexical Pitfalls”

5.10 Implementation Phases

Phase 1: Foundation (2-3 hours)

  • Set up project structure
  • Implement type info display
  • Create basic menu system
  • Test on your platform

Phase 2: Core Experiments (3-4 hours)

  • Integer promotion demo
  • Signed vs unsigned comparison
  • char arithmetic
  • sizeof traps

Phase 3: Visualization (2-3 hours)

  • Memory dump functions
  • ASCII diagrams
  • Step-by-step explanations
  • Format output nicely

Phase 4: Polish (1-2 hours)

  • Add more test cases
  • Interactive mode (basic)
  • Quiz mode
  • Documentation

5.11 Key Implementation Decisions

Decision 1: How to display types?

  • Option A: Use _Generic (C11 required, clean syntax)
  • Option B: Use explicit sizeof comparisons (C99 compatible)
  • Recommendation: Use _Generic if C11 is available

Decision 2: How detailed should explanations be?

  • Option A: Just show the result
  • Option B: Show conversion steps
  • Option C: Show memory representation too
  • Recommendation: Option C for maximum learning

Decision 3: How to handle platform differences?

  • Option A: Assume 64-bit LP64
  • Option B: Detect and adapt at compile time
  • Option C: Detect and adapt at runtime
  • Recommendation: Compile-time detection with runtime display

6. Testing Strategy

Test Categories

Category Purpose Example
Correctness Verify explanations match behavior Predicted output == actual output
Platform Work on different architectures 32-bit vs 64-bit results
Edge Cases Handle extreme values INT_MIN, UINT_MAX
Display Output is readable No truncation, aligned

Test Cases

// Test: Integer promotion occurs
void test_promotion(void) {
    char a = 1, b = 2;
    // Verify the addition is done as int
    assert(sizeof(a + b) == sizeof(int));
}

// Test: Signed to unsigned conversion
void test_signed_unsigned(void) {
    int neg = -1;
    unsigned int un = (unsigned int)neg;
    assert(un == UINT_MAX);
}

// Test: Comparison behavior
void test_comparison(void) {
    int a = -1;
    unsigned int b = 0;
    // This should be false (counter-intuitive!)
    assert((a < b) == 0);
}

// Test: char wrap-around
void test_char_wrap(void) {
    char c = 127;
    c = c + 1;  // Wraps to -128 (on most systems)
    // Note: This is implementation-defined for signed char overflow
    // For unsigned char it's well-defined
    unsigned char uc = 255;
    uc = uc + 1;
    assert(uc == 0);  // Well-defined wrap
}

Verification Commands

# Build with all warnings
gcc -Wall -Wextra -Wconversion -Wsign-compare -o typepromo src/*.c

# Run specific experiment
./typepromo --experiment signed-unsigned

# Run all experiments
./typepromo --all

# Check for undefined behavior
gcc -fsanitize=undefined -o typepromo_ub src/*.c
./typepromo_ub --all

# Compare 32-bit vs 64-bit (if cross-compilation available)
gcc -m32 -o typepromo32 src/*.c
gcc -m64 -o typepromo64 src/*.c

7. Common Pitfalls & Debugging

Pitfall Symptom Solution
Using undefined behavior in examples Inconsistent results Use well-defined constructs; show UB consequences safely
Assuming type sizes Wrong output on different platforms Use sizeof, limits.h macros
Hardcoding -1 conversion Wrong on non-2’s-complement Use UINT_MAX or compute
Printf format mismatch Garbage output Match %d/%u/%zu to type
Signed char vs char Platform differences Use explicit signed/unsigned char
Forgetting promotion Incorrect type display Remember: all arithmetic promotes

Debugging Strategies

// Debug: Print sizes and ranges at start
void debug_platform(void) {
    printf("Debug info:\n");
    printf("  CHAR_BIT = %d\n", CHAR_BIT);
    printf("  sizeof(char) = %zu\n", sizeof(char));
    printf("  sizeof(int) = %zu\n", sizeof(int));
    printf("  CHAR_MIN = %d, CHAR_MAX = %d\n", CHAR_MIN, CHAR_MAX);
    printf("  INT_MIN = %d, INT_MAX = %d\n", INT_MIN, INT_MAX);
    printf("  UINT_MAX = %u\n", UINT_MAX);
}

// Debug: Verify conversion actually happened
void debug_conversion(void) {
    int a = -1;
    unsigned int b = (unsigned int)a;
    printf("Debug: -1 as int = %d (0x%x)\n", a, a);
    printf("Debug: -1 as uint = %u (0x%x)\n", b, b);
    printf("Debug: Are bytes same? %s\n",
           memcmp(&a, &b, sizeof(int)) == 0 ? "yes" : "no");
}

8. Extensions & Challenges

Beginner Extensions

  • Add color output for warnings/errors (using ANSI codes)
  • Add more edge case examples (INT_MIN, UINT_MAX)
  • Create a “quiz mode” that tests the user
  • Add compiler warning examples (show -Wconversion output)

Intermediate Extensions

  • Parse simple expressions interactively
  • Show corresponding assembly for conversions
  • Add floating-point conversion experiments
  • Create a library of real-world CVEs caused by type bugs

Advanced Extensions

  • Integrate with LLVM to show IR for type operations
  • Create a static analyzer plugin to detect these bugs
  • Add support for C++ with its stricter rules
  • Build a web version for online learning

9. Real-World Connections

CVEs Caused by Type Conversion Bugs

CVE-2009-1385 (Linux Kernel)
════════════════════════════
Vulnerability: Integer underflow in e1000 network driver
Bug: signed/unsigned comparison in packet length handling
Impact: Denial of service, possible code execution

CVE-2014-1266 (Apple "goto fail")
═════════════════════════════════
While not purely a type bug, it shows how subtle C issues
cause major security failures.

CVE-2021-3156 (Sudo "Baron Samedit")
════════════════════════════════════
Vulnerability: Heap overflow in sudo
Root cause: Signed/unsigned confusion in argument parsing
Impact: Local privilege escalation to root

GENERAL PATTERN:
═══════════════
1. User input stored in signed type
2. Compared against unsigned size/length
3. Negative input becomes huge positive
4. Buffer overflow or logic bypass

Industry Standards Addressing This

CERT C SECURE CODING STANDARD:
═══════════════════════════════
INT02-C: Understand integer conversion rules
INT31-C: Ensure that integer conversions do not result in lost or
         misinterpreted data
INT32-C: Ensure that operations on signed integers do not result
         in overflow

MISRA C (Automotive):
════════════════════
Rule 10.1: Operands shall not be of an inappropriate essential type
Rule 10.3: The value of an expression shall not be assigned to an
           object with a narrower essential type
Rule 10.4: Both operands of an operator in which the usual
           arithmetic conversions are performed shall have the
           same essential type category

These rules exist because type bugs cause REAL HARM in:
- Automotive systems (crashes)
- Medical devices (patient harm)
- Aviation (catastrophic failure)

10. Resources

Online Resources

Documentation

  • C11 Standard, Section 6.3: Conversions
  • GCC Manual: Integer overflow and type warnings
  • Clang Documentation: -Wconversion and friends

Papers

  • “Understanding Integer Overflow in C/C++” - Dietz et al.
  • “Undefined Behavior: What Happened to My Code?” - Wang et al.

11. Self-Assessment Checklist

Understanding

  • I can explain why char + char produces int
  • I can trace the usual arithmetic conversions for any expression
  • I know why -1 > 0u is true
  • I understand when sizeof comparisons are dangerous
  • I can identify type conversion bugs in code review

Implementation

  • All experiments produce correct output
  • Explanations match actual C behavior
  • Tool works on both 32-bit and 64-bit (if applicable)
  • Output is clear and educational
  • No undefined behavior in the tool itself

Testing

  • Edge cases tested (INT_MIN, UINT_MAX, etc.)
  • Output verified against compiler behavior
  • Works with both GCC and Clang
  • Quiz mode tests user correctly

Growth

  • I can write code that avoids these bugs
  • I use -Wconversion and -Wsign-compare regularly
  • I can explain these issues to other programmers
  • I recognize these patterns in security vulnerabilities

12. Submission / Completion Criteria

Minimum Viable Completion

  • Type info display works (sizes, ranges)
  • At least 3 experiments implemented:
    • Integer promotion basics
    • Signed vs unsigned comparison
    • char arithmetic surprises
  • Output is clear with step-by-step explanations
  • Compiles without warnings using -Wall -Wextra

Full Completion

  • All 7+ experiments implemented
  • Memory visualization (hex dumps)
  • Platform-aware display (adapts to 32/64 bit)
  • Interactive mode (at least predefined expressions)
  • Quiz mode functional
  • Comprehensive explanations with ASCII diagrams

Excellence (Going Above & Beyond)

  • Custom expression parser for true interactive mode
  • Assembly output integration (shows actual instructions)
  • Multiple output formats (terminal, HTML, JSON)
  • Comparison with other languages (show how Rust/Go handle this)
  • Integration with compiler warnings (show -Wconversion output)
  • Database of real CVEs with demonstrations

Thinking Exercise Solutions

Exercise 1 Solution:

char a = 100;
char b = 50;
int result = a + b;

// Step 1: a (char) promotes to int: 100
// Step 2: b (char) promotes to int: 50
// Step 3: 100 + 50 = 150 (type: int)
// Step 4: 150 assigned to result (type: int)
// Answer: (a + b) is type int, value 150

Exercise 2 Solution:

unsigned char x = 255;
char y = 1;
printf("%d\n", x + y);

// Step 1: x (unsigned char) promotes to int: 255
// Step 2: y (char/signed char) promotes to int: 1
// Step 3: 255 + 1 = 256 (type: int)
// Answer: Prints 256 (type is int, fits fine)

Exercise 3 Solution:

size_t len = strlen(str);  // If str is "", len = 0
for (int i = len - 1; i >= 0; i--) { ... }

// Bug: len - 1 when len = 0
// size_t is unsigned, so 0 - 1 wraps to SIZE_MAX
// i = (int)SIZE_MAX = implementation-defined, often -1
// But this is still a bug! The subtraction itself is wrong.
// Better: for (size_t i = len; i > 0; i--) { use i-1 }

Exercise 4 Solution:

int x = -1;
if (x < sizeof(int)) { ... }

// sizeof(int) returns size_t (unsigned)
// Comparing int (-1) with size_t triggers conversion
// x converts to size_t: (size_t)-1 = SIZE_MAX
// SIZE_MAX < 4 is false!
// So "x is small" does NOT print (counter to intuition)
// Wait - the question says it prints? Let me re-check...
// Actually: -Wall doesn't warn about this, but it's a bug.
// The comparison fails because SIZE_MAX is huge.

This guide was expanded from EXPERT_C_PROGRAMMING_DEEP_DIVE.md. For the complete learning path, see the project index.