Skip to content

Conversation

@JebediahS
Copy link

@JebediahS JebediahS commented Sep 5, 2025

Filling the stack should start from element with index 0.

Summary by CodeRabbit

  • Bug Fixes

    • Corrected stack behavior in the expression calculator for proper indexing.
    • Added detection and a clear "Error: Operator is missing!" message when inputs lack an operator.
  • Tests

    • Added a comprehensive test script validating arithmetic, complex expressions, division-by-zero, and the missing-operator error handling.

✏️ Tip: You can customize this high-level summary in your review settings.

Copy link
Owner

@ohkimur ohkimur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! The change from pre-increment/decrement to post-increment/decrement looks correct and fixes the off‑by‑one behavior:

  • In push, stack[stack_pointer++] = element; is the right choice if stack_pointer tracks the next free slot.
  • In pop, return stack[--stack_pointer]; correctly moves back to the last pushed element before reading.

A couple of suggestions to strengthen this and future contributions:

  • Add brief comments documenting the stack invariant: “stack_pointer points to the next free slot.”
  • Include small tests (or a demo main) for: push/pop single item, push until full, pop until empty, and verify strict LIFO.
  • Consider returning a status from push/pop (e.g., int/bool with out‑param) instead of printing errors; returning 0 from pop is ambiguous if 0.0 is a valid value.
  • Use size_t for stack_pointer and comparisons with STACK_SIZE.
  • Optionally guard against overflow/underflow with asserts in debug builds.

Overall, nice, focused fix with clear correctness improvement. If you’re up for it, similar sanity checks and invariants across other stack/queue exercises would be super valuable.

@JebediahS
Copy link
Author

Thanks for the PR! The change from pre-increment/decrement to post-increment/decrement looks correct and fixes the off‑by‑one behavior:

* In `push`, `stack[stack_pointer++] = element;` is the right choice if `stack_pointer` tracks the next free slot.

* In `pop`, `return stack[--stack_pointer];` correctly moves back to the last pushed element before reading.

A couple of suggestions to strengthen this and future contributions:

* Add brief comments documenting the stack invariant: “`stack_pointer` points to the next free slot.”

* Include small tests (or a demo `main`) for: push/pop single item, push until full, pop until empty, and verify strict LIFO.

* Consider returning a status from `push`/`pop` (e.g., int/bool with out‑param) instead of printing errors; returning `0` from `pop` is ambiguous if `0.0` is a valid value.

* Use `size_t` for `stack_pointer` and comparisons with `STACK_SIZE`.

* Optionally guard against overflow/underflow with asserts in debug builds.

Overall, nice, focused fix with clear correctness improvement. If you’re up for it, similar sanity checks and invariants across other stack/queue exercises would be super valuable.

Thank you for the suggestions, I would be happy to contribute more (if my time allows)!

@ohkimur
Copy link
Owner

ohkimur commented Dec 9, 2025

@JebediahS Hello, are you still interested to contribute on this PR, or shall I close it?

@JebediahS
Copy link
Author

@JebediahS Hello, are you still interested to contribute on this PR, or shall I close it?

Sorry for the delay, still interested!

Copy link
Owner

@ohkimur ohkimur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JebediahS Hello, are you still interested to contribute on this PR, or shall I close it?

Sorry for the delay, still interested!

Great! Whenever you want to merge this PR, just make sure you checkout the new CONTRIBUTIONS.md file, as this a new addition to the repo so it makes the code more consistent and a bit more modern.

Copy link
Owner

@ohkimur ohkimur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JebediahS Also, you'll need to merge the new changes from main first in your local branch to solve the merge conflicts.

@JebediahS
Copy link
Author

Thanks for the PR! The change from pre-increment/decrement to post-increment/decrement looks correct and fixes the off‑by‑one behavior:

* In `push`, `stack[stack_pointer++] = element;` is the right choice if `stack_pointer` tracks the next free slot.

* In `pop`, `return stack[--stack_pointer];` correctly moves back to the last pushed element before reading.

A couple of suggestions to strengthen this and future contributions:

* Add brief comments documenting the stack invariant: “`stack_pointer` points to the next free slot.”

* Include small tests (or a demo `main`) for: push/pop single item, push until full, pop until empty, and verify strict LIFO.

* Consider returning a status from `push`/`pop` (e.g., int/bool with out‑param) instead of printing errors; returning `0` from `pop` is ambiguous if `0.0` is a valid value.

* Use `size_t` for `stack_pointer` and comparisons with `STACK_SIZE`.

* Optionally guard against overflow/underflow with asserts in debug builds.

Overall, nice, focused fix with clear correctness improvement. If you’re up for it, similar sanity checks and invariants across other stack/queue exercises would be super valuable.

To not mess up your style: In which form do you expect the tests to be set up? Should there be a test-folder exercise_X_XX/test/ folder with a expr_test.c or a shell expr_test.sh script which covers the critical cases?

@ohkimur
Copy link
Owner

ohkimur commented Dec 15, 2025

@coderabbitai Can you help us here? What would be the best way to create tests in this repo, considering it's K&R C Programming Book solutions and it should be accessible to newcomers?

Offer us some useful approaches here.

@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Explore the repository structure to understand organization
echo "=== Repository structure ==="
ls -la

echo -e "\n=== Looking for test directories or files ==="
fd -t d -i test

echo -e "\n=== Looking for existing test files ==="
fd -e c -e sh | grep -i test

echo -e "\n=== Finding all C source files to understand organization ==="
fd -e c | head -20

echo -e "\n=== Looking for the expr.c file mentioned in PR ==="
fd expr.c

echo -e "\n=== Checking for build system files ==="
ls -la | grep -E "(Makefile|CMakeLists|build)"

echo -e "\n=== Examining directory structure ==="
tree -L 2 -d 2>/dev/null || find . -maxdepth 2 -type d

Length of output: 4580


🏁 Script executed:

#!/bin/bash

# Examine the existing test file to understand the pattern
echo "=== Existing test file content ==="
cat chapter_1/exercise_1_11/word_count_test.c

echo -e "\n=== Examining the Makefile ==="
cat Makefile

echo -e "\n=== Examining the expr.c file ==="
cat chapter_5/exercise_5_10/expr.c

echo -e "\n=== Checking README for project goals ==="
cat README.md

echo -e "\n=== Looking at exercise_5_10 directory contents ==="
ls -la chapter_5/exercise_5_10/

Length of output: 7328


@ohkimur Great question! Based on the repository structure, I can see this is an educational repo focused on K&R exercises. I found one existing test file (chapter_1/exercise_1_11/word_count_test.c), which is a simple standalone C program. Let me suggest several approaches, ordered from simplest to most comprehensive:

Approach 1: Inline Test Main (Recommended for newcomers)

Add a test main() in the same file using conditional compilation:

// At the end of expr.c, after the existing main()

#ifdef TEST_MODE
int main(void)
{
    printf("Testing expr stack operations...\n");
    
    // Test 1: Push and pop single element
    printf("Test 1: Push 5.0, pop should return 5.0\n");
    push(5.0);
    float result = pop();
    printf("  Result: %.1f %s\n", result, (result == 5.0) ? "PASS" : "FAIL");
    
    // Test 2: LIFO order
    printf("Test 2: Push 1,2,3, pop should return 3,2,1\n");
    push(1.0);
    push(2.0);
    push(3.0);
    float r3 = pop();
    float r2 = pop();
    float r1 = pop();
    printf("  Result: %.1f,%.1f,%.1f %s\n", r3, r2, r1, 
           (r3 == 3.0 && r2 == 2.0 && r1 == 1.0) ? "PASS" : "FAIL");
    
    // Test 3: Fill stack
    printf("Test 3: Fill stack to STACK_SIZE\n");
    for (int i = 0; i < STACK_SIZE; i++) {
        push((float)i);
    }
    printf("  Stack pointer: %d (should be %d) %s\n", stack_pointer, STACK_SIZE,
           (stack_pointer == STACK_SIZE) ? "PASS" : "FAIL");
    
    // Test 4: Empty stack
    printf("Test 4: Empty entire stack\n");
    for (int i = 0; i < STACK_SIZE; i++) {
        pop();
    }
    printf("  Stack pointer: %d (should be 0) %s\n", stack_pointer,
           (stack_pointer == 0) ? "PASS" : "FAIL");
    
    return 0;
}
#endif

Compile and run:

gcc -DTEST_MODE -o expr_test chapter_5/exercise_5_10/expr.c -lm
./expr_test

Pros: No extra files, easy for beginners, tests live with code
Cons: Clutters main file slightly


Approach 2: Separate Test File (Consistent with existing pattern)

Create chapter_5/exercise_5_10/expr_test.c:

#include <stdio.h>
#include <stdlib.h>

// Declare the external functions and variables
extern unsigned char stack_pointer;
extern float stack[];
extern void push(float element);
extern float pop(void);

void test_push_pop_single(void) {
    printf("Test: Push and pop single element... ");
    push(42.5);
    float result = pop();
    printf("%s\n", (result == 42.5 && stack_pointer == 0) ? "PASS" : "FAIL");
}

void test_lifo_order(void) {
    printf("Test: LIFO order... ");
    push(1.0);
    push(2.0);
    push(3.0);
    int pass = (pop() == 3.0 && pop() == 2.0 && pop() == 1.0);
    printf("%s\n", pass ? "PASS" : "FAIL");
}

void test_fill_stack(void) {
    printf("Test: Fill stack to capacity... ");
    for (int i = 0; i < 15; i++) {
        push((float)i);
    }
    printf("%s\n", (stack_pointer == 15) ? "PASS" : "FAIL");
    // Clear for next test
    for (int i = 0; i < 15; i++) pop();
}

int main(void) {
    printf("=== Stack Implementation Tests ===\n");
    test_push_pop_single();
    test_lifo_order();
    test_fill_stack();
    printf("=== Tests Complete ===\n");
    return 0;
}

You'll need to modify expr.c to expose the stack functions (remove static if present, or extract push/pop to a separate file).

Compile and run:

gcc -o expr_test chapter_5/exercise_5_10/expr.c chapter_5/exercise_5_10/expr_test.c -lm
./expr_test

Pros: Follows existing pattern, separates concerns
Cons: Requires exposing internal functions, slightly more complex


Approach 3: Shell Script Test Runner

Create chapter_5/exercise_5_10/test_expr.sh:

#!/bin/bash

echo "=== Testing expr calculator ==="

# Build the program
gcc -o expr chapter_5/exercise_5_10/expr.c -lm

# Test 1: Simple addition
result=$(./expr 2 3 +)
expected="result: 5.000"
if [[ "$result" == "$expected" ]]; then
    echo "✓ Test 1 PASS: 2 + 3 = 5"
else
    echo "✗ Test 1 FAIL: Expected '$expected', got '$result'"
fi

# Test 2: Subtraction
result=$(./expr 10 3 -)
expected="result: 7.000"
if [[ "$result" == "$expected" ]]; then
    echo "✓ Test 2 PASS: 10 - 3 = 7"
else
    echo "✗ Test 2 FAIL: Expected '$expected', got '$result'"
fi

# Test 3: Complex expression
result=$(./expr 5 1 2 + 4 \* + 3 -)
expected="result: 14.000"
if [[ "$result" == "$expected" ]]; then
    echo "✓ Test 3 PASS: Complex RPN expression"
else
    echo "✗ Test 3 FAIL: Expected '$expected', got '$result'"
fi

# Test 4: Division by zero
result=$(./expr 5 0 / 2>&1)
if [[ "$result" == *"division by zero"* ]]; then
    echo "✓ Test 4 PASS: Division by zero detected"
else
    echo "✗ Test 4 FAIL: Should detect division by zero"
fi

# Cleanup
rm -f expr

echo "=== Tests Complete ==="

Run: bash chapter_5/exercise_5_10/test_expr.sh

Pros: Tests end-to-end behavior, no code changes needed, easy to understand
Cons: Doesn't test internal stack invariants


My Recommendation for This Repository

For a K&R solutions repo targeting newcomers, I'd suggest a hybrid approach:

  1. Use Approach 3 (shell scripts) for exercises with command-line I/O like expr.c
  2. Use Approach 2 (separate test file) for exercises that need unit-level testing
  3. Keep a simple structure: exercise_X_YY/test_solution.sh or exercise_X_YY/solution_test.c

This maintains simplicity while providing practical testing examples that teach good habits. The shell script approach is especially good here because:

  • Beginners can see inputs/outputs clearly
  • No modifications to the original solution needed
  • Easy to add more test cases
  • Demonstrates real-world program testing

Would you like me to prepare a complete test suite example for the expr.c exercise using any of these approaches?

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

Adds operator counting and missing-operator detection, adjusts stack push/pop to 0-based semantics, and introduces a new Bash test script exercising arithmetic, RPN expression, division-by-zero, and missing-operator cases.

Changes

Cohort / File(s) Summary
Expr implementation
chapter_5/exercise_5_10/expr.c
Adds an op_count in main to track executed operators; increments op_count for +, -, *, /; prints an error and exits if no operator found when argc > 2. Changes stack semantics: pop uses pre-decrement (return stack[--stack_pointer];) and push uses post-increment (stack[stack_pointer++] = element;).
Test script
chapter_5/exercise_5_10/expr_test.sh
New Bash test harness that verifies expr is executable and runs six tests: addition, commutativity, subtraction, a complex RPN expression, division-by-zero handling, and missing-operator error detection, printing PASS/FAIL for each.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Review chapter_5/exercise_5_10/expr.c for correct stack-indexing semantics and off-by-one risks in all push/pop call sites.
  • Verify op_count increments are applied consistently for all operator cases and the missing-operator error condition matches intended CLI usage.
  • Run chapter_5/exercise_5_10/expr_test.sh and confirm expected outputs (especially division-by-zero and missing-operator messages).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Update expr.c' is overly vague and generic; it does not convey the specific nature of the fix (off-by-one stack semantics correction) or the addition of tests. Revise the title to be more specific and descriptive, such as 'Fix off-by-one bug in expr stack operations and add test suite' to clearly indicate the main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 218cece and 68fe0cb.

📒 Files selected for processing (1)
  • chapter_5/exercise_5_10/expr.c (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
chapter_5/exercise_5_10/expr.c (3)
chapter_4/exercise_4_11/getop.c (2)
  • push (75-81)
  • pop (83-90)
chapter_4/exercise_4_10/calculator.c (2)
  • push (141-147)
  • pop (149-157)
chapter_7/exercise_7_05/calculator.c (2)
  • push (69-75)
  • pop (77-84)
🔇 Additional comments (4)
chapter_5/exercise_5_10/expr.c (4)

16-16: LGTM! Operator tracking is a sensible addition.

The op_count variable correctly tracks executed operators and enables detection of missing-operator errors later in the code.


33-33: LGTM! Operator counting is implemented consistently.

The operator counter is incremented uniformly across all four arithmetic operators, correctly placed after validation and before pushing results.

Also applies to: 38-38, 44-44, 49-49


108-108: Excellent fix! Pre-decrement correctly implements 0-based stack semantics.

The change to return stack[--stack_pointer]; fixes the off-by-one bug and aligns with the standard K&R stack idiom demonstrated in exercises 4-10, 4-11, and 7-5. Combined with the post-increment in push, the invariant is now correct: stack_pointer always points to the next free slot.


117-117: Excellent fix! Post-increment correctly implements 0-based stack semantics.

The change to stack[stack_pointer++] = element; fixes the off-by-one bug and aligns with the standard K&R stack idiom demonstrated in exercises 4-10, 4-11, and 7-5. This ensures the stack correctly fills from index 0, maintaining the invariant that stack_pointer points to the next available slot.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (20)
chapter_4/exercise_4_14/swap.c-3-9 (1)

3-9: Remove trailing semicolon and wrap macro in do-while(0) for safety.

The macro has two issues that make it unsafe in certain contexts:

  1. The trailing semicolon on Line 9 causes swap(int, x, y); to expand to { ... };;, creating a double semicolon. While harmless here, it's non-idiomatic.

  2. More importantly, the macro lacks a do-while(0) wrapper, which is the standard idiom for multi-statement macros. Without it, the macro can break in control-flow contexts (e.g., if-else chains without braces).

Apply this diff to fix both issues:

-#define swap(t, x, y)                                                          \
-    {                                                                          \
-        t temp;                                                                \
-        temp = x;                                                              \
-        x = y;                                                                 \
-        y = temp;                                                              \
-    };
+#define swap(t, x, y)                                                          \
+    do {                                                                       \
+        t temp;                                                                \
+        temp = x;                                                              \
+        x = y;                                                                 \
+        y = temp;                                                              \
+    } while (0)

This ensures the macro behaves like a single statement and can be safely used with a semicolon at the call site.

chapter_5/exercise_5_13/tail.c-55-62 (1)

55-62: Fix strlen in loop and handle empty string edge case.

The function has two issues:

  1. Calling strlen in the loop condition results in O(n²) complexity.
  2. Empty string returns 1 (valid), which allows "-" as a valid argument.

Apply this diff:

 int is_str_uint(char *str) {
-    for (size_t i = 0; i < strlen(str); ++i) {
+    if (*str == '\0') {
+        return 0;
+    }
+    for (size_t i = 0; str[i] != '\0'; ++i) {
         if (!isdigit(str[i])) {
             return 0;
         }
     }
     return 1;
 }
chapter_5/exercise_5_13/tail.c-33-37 (1)

33-37: Use strtoul instead of atoi for parsing unsigned values.

atoi returns int and cannot properly validate or parse values that exceed INT_MAX or handle overflow. Since nr_of_lines_to_print is size_t, use strtoul for proper unsigned parsing with error detection.

Apply this diff:

-    size_t nr_of_lines_to_print = DEFAULT_NR_OF_LINES_TO_PRINT;
-
-    if (argc == 2) {
-        nr_of_lines_to_print = atoi(argv[argc - 1] + 1);
-    }
+    size_t nr_of_lines_to_print = DEFAULT_NR_OF_LINES_TO_PRINT;
+
+    if (argc == 2) {
+        char *endptr;
+        unsigned long val = strtoul(argv[argc - 1] + 1, &endptr, 10);
+        if (*endptr != '\0' || val > SIZE_MAX) {
+            puts("Error: invalid number.\n");
+            return EXIT_FAILURE;
+        }
+        nr_of_lines_to_print = (size_t)val;
+    }
chapter_1/exercise_1_18/trailing_blanks.c-38-46 (1)

38-46: Missing bounds check can cause buffer underrun.

The loop for (i = length - 2; line[i] == ' ' || line[i] == '\t'; --i) lacks a lower bound check. If a line consists entirely of whitespace (e.g., " \n"), i will decrement past 0, causing undefined behavior when accessing line[i].

 void remove_trailing_blanks(char line[], int length) {
     int i;

-    for (i = length - 2; line[i] == ' ' || line[i] == '\t'; --i)
+    for (i = length - 2; i >= 0 && (line[i] == ' ' || line[i] == '\t'); --i)
         ;

     line[i + 1] = '\n';
     line[i + 2] = '\0';
 }
chapter_1/exercise_1_11/word_count_test.c-16-17 (1)

16-17: Use int for getchar() return value to correctly detect EOF.

getchar() returns int to distinguish EOF (-1) from valid characters. Using char can cause EOF detection to fail (on unsigned char) or misinterpret valid 0xFF bytes as EOF (on signed char).

-    char c;
-    while ((c = getchar()) != EOF) {
+    int c;
+    while ((c = getchar()) != EOF) {
chapter_5/exercise_5_05/strncat.c-19-28 (1)

19-28: Off-by-one error and missing null termination.

The loop condition (*s++ = *t++) != '\0' && n-- has two issues:

  1. Off-by-one: The post-decrement n-- is evaluated after the character is copied due to short-circuit evaluation. When n=5, this copies 6 characters before n-- returns 0 (false).

  2. Missing null terminator: If t is longer than n characters, the result is not null-terminated.

Apply this diff to fix both issues:

 void strcat_ptr(char *s, char *t, size_t n) {
     // Find the end of s
     size_t s_length = strlen(s);

     // Move the s pointer to the end of the s string.
     s += s_length;

     // copy t to the end of s
-    while ((*s++ = *t++) != '\0' && n--)
-        ;
+    while (n-- > 0 && (*s++ = *t++) != '\0')
+        ;
+    if (*(s - 1) != '\0')
+        *s = '\0';
 }

Alternatively, a cleaner approach:

void strcat_ptr(char *s, char *t, size_t n) {
    s += strlen(s);
    while (n-- > 0 && *t != '\0')
        *s++ = *t++;
    *s = '\0';
}
chapter_5/exercise_5_14/sort.c-42-49 (1)

42-49: Potential underflow when input is empty.

If read_lines returns 0 (empty input), the expression nr_of_lines - 1 wraps to SIZE_MAX since size_t is unsigned. This passes an enormous range to quick_sort, causing undefined behavior.

Add a check for zero lines before calling quick_sort:

     if ((nr_of_lines = read_lines(line_ptr, MAX_NR_OF_LINES)) != (size_t)-1) {
+        if (nr_of_lines > 0) {
             quick_sort((void **)line_ptr, 0, nr_of_lines - 1,
                        (int (*)(void *, void *))comp);
+        }
         write_lines(line_ptr, nr_of_lines);
     } else {
chapter_5/exercise_5_19/undcl.c-95-103 (1)

95-103: Off-by-one: null terminator can write past buffer if name fills max_len.

If exactly max_len alphanumeric characters are read, i becomes max_len and dest[i] = '\0' writes one byte past the buffer. The loop should use i < max_len - 1 to reserve space for the null terminator.

 void get_name(char *dest, const size_t max_len) {
     int c;
     size_t i = 0;
-    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_len) {
+    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_len - 1) {
         dest[i++] = c;
     }
     dest[i] = '\0';
     ungetc(c, stdin);
 }
chapter_4/exercise_4_05/math.c-150-154 (1)

150-154: Same clear() bug: signed integer underflow.

Identical issue to chapter_4/exercise_4_06/variables.c: when sp is 0, the do-while still executes and decrements sp to -1, causing undefined behavior on subsequent operations.

 void clear(void) {
-    do {
-        stack[sp] = 0.0;
-    } while (sp--);
+    sp = 0;
 }
chapter_4/exercise_4_06/variables.c-105-109 (1)

105-109: Missing bounds check for varindex may cause buffer overflow.

var_buff has VARNUM (26) slots, but varindex is incremented without validation. If more than 26 variables are assigned, this writes past the array bounds.

         case VARSET:
+            if (varindex >= VARNUM) {
+                printf("Error: variable limit reached.\n");
+                break;
+            }
             var_buff[varindex++] = pop();
             printf("variable %c: %.3f\n", 'a' + varindex - 1,
                    var_buff[varindex - 1]);
             break;
chapter_5/exercise_5_19/undcl.c-82-90 (1)

82-90: Bug: block comment parsing doesn't correctly find */.

The loop at line 83 stops at the first '*' character, not at */. For a comment like /* a * b */, it would stop at the first * (after a), then read the space, which is not /, and the function returns with incorrect state.

The logic should look for */ as a pair:

         } else if (c == '*') {
-            while ((c = getc(stdin)) != '*' && c != EOF)
-                ;
-            c = getc(stdin);
-            if (c == '/') {
-                ungetc('\n', stdin);
-                return;
+            int prev = 0;
+            while ((c = getc(stdin)) != EOF) {
+                if (prev == '*' && c == '/') {
+                    ungetc('\n', stdin);
+                    return;
+                }
+                prev = c;
             }
         }
chapter_4/exercise_4_06/variables.c-186-190 (1)

186-190: Undefined behavior: clear() causes signed integer underflow and out-of-bounds access.

When sp is 0 (empty stack), the do-while loop still executes once, accessing stack[0] (harmless), but then sp-- decrements sp to -1. The condition sp-- evaluates the old value (0), which is falsy, so the loop exits—but sp is now -1. Subsequent push/pop calls will misbehave. Additionally, if sp > 0, the loop clears stack[sp] which is one past the last valid element.

Consider using a simple assignment instead:

 void clear(void) {
-    do {
-        stack[sp] = 0.0;
-    } while (sp--);
+    sp = 0;
 }

Zeroing stack contents is unnecessary since sp controls valid entries.

chapter_5/exercise_5_20/dcl.c-112-120 (1)

112-120: Block comment parsing exits prematurely on any *.

Same issue as in count_c_keywords.c: the while loop exits on any *, not just the closing */. A comment like /* ptr * 2 */ would be incorrectly parsed.

See the fix suggested for chapter_6/exercise_6_01/count_c_keywords.c which uses a two-character lookahead to properly detect */.

chapter_4/exercise_4_10/calculator.c-118-120 (1)

118-120: Missing validation for variable index in VARGET.

var - 'a' is used as an index without validation. If var is uppercase or outside 'a'-'z', the index could be out of bounds (e.g., 'A' - 'a' = -32).

             case VARGET:
+                if (var < 'a' || var > 'z') {
+                    printf("Error: invalid variable '%c'.\n", var);
+                    break;
+                }
                 push(var_buff[var - 'a']);
                 break;
chapter_4/exercise_4_10/calculator.c-112-116 (1)

112-116: Missing bounds check on varindex can cause buffer overflow.

varindex is incremented without checking against VARNUM (26). If the user executes the _ command more than 26 times, var_buff[varindex++] writes past the array bounds.

             case VARSET:
+                if (varindex >= VARNUM) {
+                    printf("Error: variable storage full.\n");
+                    break;
+                }
                 var_buff[varindex++] = pop();
                 printf("variable %c: %.3f\n", 'a' + varindex - 1,
                        var_buff[varindex - 1]);
                 break;
chapter_6/exercise_6_01/count_c_keywords.c-70-78 (1)

70-78: Block comment parsing exits prematurely on any *.

The while loop at Line 71 exits when it encounters any *, not just the closing */. For a comment like /* hello * world */, it stops at the first * after "hello", then checks if the next character is /. Since it's a space, the function returns early, leaving * world */ in the input stream.

         } else if (c == '*') {
-            while ((c = getc(stdin)) != '*' && c != EOF)
-                ;
-            c = getc(stdin);
-            if (c == '/') {
-                ungetc('\n', stdin);
-                return;
+            int prev = 0;
+            while ((c = getc(stdin)) != EOF) {
+                if (prev == '*' && c == '/') {
+                    ungetc('\n', stdin);
+                    return;
+                }
+                prev = c;
             }
         }
chapter_6/exercise_6_06/define.c-103-122 (1)

103-122: Memory leak if str_dup(definition) fails after node allocation.

When lookup(name) returns NULL, a new node is allocated and node_p->name is set. If str_dup(definition) on line 117 subsequently fails and returns NULL, the function returns NULL but the allocated node (with its duplicated name) remains in the hash table, causing a memory leak and a corrupted entry.

Consider removing the node from the hash table before returning NULL:

     if ((node_p->definition = str_dup(definition)) == NULL) {
+        // Clean up: remove partially initialized node from hash table
+        if (prev_lookup_result == NULL) {
+            // Node was just inserted; need to remove it
+            size_t hv = hash(name);
+            hash_table[hv] = node_p->next;
+            free(node_p->name);
+            free(node_p);
+        }
         return NULL;
     }

Alternatively, defer insertion into the hash table until both allocations succeed.

Committable suggestion skipped: line range outside the PR's diff.

chapter_6/exercise_6_03/cross_referencer.c-155-166 (1)

155-166: Missing NULL check after malloc in add_to_list.

If malloc fails on line 158, the subsequent dereferences on lines 159-160 will cause a null pointer dereference.

 struct list_node *add_to_list(struct list_node *list_node_p,
                               size_t line_number) {
     if (list_node_p == NULL) {
         list_node_p = (struct list_node *)malloc(sizeof(struct list_node));
+        if (list_node_p == NULL) {
+            return NULL;
+        }
         list_node_p->line_number = line_number;
         list_node_p->next = NULL;
     } else {
         list_node_p->next = add_to_list(list_node_p->next, line_number);
     }

     return list_node_p;
 }
chapter_5/exercise_5_11/detab.c-26-31 (1)

26-31: Potential division by zero when tab_stop is 0.

If a user passes "0" as a tab stop argument, is_str_uint will accept it (since '0' is a digit), but atoi(argv[arg_pos++]) on line 27 will set tab_stop = 0. This causes undefined behavior on line 33 due to line_pos % tab_stop.

Consider rejecting zero or non-positive tab stops in the validation function:

 int is_str_uint(char *str) {
+    if (str[0] == '\0') {
+        return 0;
+    }
     for (size_t i = 0; i < strlen(str); ++i) {
         if (!isdigit(str[i])) {
             return 0;
         }
     }
-    return 1;
+    return atoi(str) > 0;
 }

Committable suggestion skipped: line range outside the PR's diff.

chapter_6/exercise_6_05/undef.c-79-98 (1)

79-98: Memory leak and corrupted hash table on allocation failure in install.

If str_dup(definition) fails on line 93 for a new node, the function returns NULL but leaves a partially initialized node in the hash table (inserted at lines 87-88). This node has node_p->definition uninitialized, which will cause issues on subsequent lookups or when freeing.

Similarly, if str_dup(name) fails on line 83, node_p is leaked.

Consider restructuring to avoid inserting until all allocations succeed:

 struct list_node *install(char *name, char *definition) {
     struct list_node *node_p;
     if ((node_p = lookup(name)) == NULL) {
         node_p = (struct list_node *)malloc(sizeof(*node_p));
-        if (node_p == NULL || (node_p->name = str_dup(name)) == NULL) {
+        if (node_p == NULL) {
+            return NULL;
+        }
+        node_p->name = str_dup(name);
+        node_p->definition = str_dup(definition);
+        if (node_p->name == NULL || node_p->definition == NULL) {
+            free(node_p->name);
+            free(node_p->definition);
+            free(node_p);
             return NULL;
         }
         size_t hash_value = hash(name);
         node_p->next = hash_table[hash_value];
         hash_table[hash_value] = node_p;
-    } else {
-        free(node_p->definition);
+        return node_p;
     }
 
-    if ((node_p->definition = str_dup(definition)) == NULL) {
+    // Update existing node
+    char *new_def = str_dup(definition);
+    if (new_def == NULL) {
         return NULL;
     }
-
+    free(node_p->definition);
+    node_p->definition = new_def;
     return node_p;
 }
🟡 Minor comments (26)
chapter_1/exercise_1_13/histogram.c-47-48 (1)

47-48: Add bounds check to prevent potential buffer overflow.

If histogram_length reaches BUFFER - 1 (99), the assignment on line 48 accesses histogram[BUFFER], which is out of bounds. While this requires processing a word of 100 characters, it's still a correctness issue.

Apply this diff to add a bounds check:

     // Add in the histogram array a end of useful data char
-    histogram[histogram_length + 1] = '$';
+    if (histogram_length + 1 < BUFFER) {
+        histogram[histogram_length + 1] = '$';
+    }

Alternatively, ensure that word_count_index never exceeds BUFFER - 1 during the counting phase by adding a check before incrementing on line 43.

chapter_5/exercise_5_04/strend.c-7-15 (1)

7-15: Minor: User-facing text contains typos.

The output strings contain repeated spelling errors:

  • Line 13, 15, 19, 21: "orrurs" should be "occurs"
  • Line 13, 19: "orrur" should be "occur"

Apply this diff to fix the typos:

-        puts("The string t1 orrurs at the end of the string s.");
+        puts("The string t1 occurs at the end of the string s.");
     else
-        puts("The string t1 doesn't orrur at the end of the string s.");
+        puts("The string t1 doesn't occur at the end of the string s.");
 
     // Test if the string t2 occurs at the end of string s.
     if (strend(s, t2))
-        puts("The string t2 orrurs at the end of the string s.");
+        puts("The string t2 occurs at the end of the string s.");
     else
-        puts("The string t2 doesn't orrur at the end of the string s.");
+        puts("The string t2 doesn't occur at the end of the string s.");

Also applies to: 18-21

chapter_5/exercise_5_10/expr.c-97-101 (1)

97-101: Handle the empty argument case.

When no arguments are provided (argc == 1), the condition on line 97 is false, and line 100 executes pop() on an empty stack. This produces confusing output: the error message "Error: the stack is empty." followed by "result: 0.000".

Additionally, line 98 is missing a newline character, and the final result print will execute even when an operator is missing.

Apply this diff to handle edge cases:

     if (op_count == 0 && argc > 2) {
-        printf("Error: Operator is missing!");
+        printf("Error: Operator is missing!\n");
+        return EXIT_FAILURE;
+    } else if (stack_pointer == 0) {
+        printf("Error: No arguments provided.\n");
+        return EXIT_FAILURE;
+    } else if (stack_pointer > 1) {
+        printf("Error: Too many operands, missing operators.\n");
+        return EXIT_FAILURE;
     } else {
         printf("result: %.3f", pop());
     }
chapter_5/exercise_5_10/expr.c-26-26 (1)

26-26: Remove overly restrictive stack size check.

The condition stack_pointer < STACK_SIZE is unnecessary here. Binary operations pop 2 elements and push 1 (net reduction of 1), so they can safely execute even when stack_pointer == STACK_SIZE (full stack). The current check incorrectly prevents valid operations when the stack is full.

Apply this diff:

-            if (stack_pointer >= 2 && stack_pointer < STACK_SIZE) {
+            if (stack_pointer >= 2) {
                 float number2 = pop();
chapter_1/exercise_1_11/word_count_test.c-7-9 (1)

7-9: Use int instead of char for counters.

Declaring nl, nw, and nc as char limits their range to ~127 (signed) or 255 (unsigned), causing overflow for any moderately sized input. These should be int to handle realistic counts.

-    char nl;
-    char nw;
-    char nc;
+    int nl;
+    int nw;
+    int nc;
chapter_2/exercise_2_03/htoi.c-53-60 (1)

53-60: Error handling doesn't halt conversion.

When an invalid hex character is detected, an error message is printed but the loop continues, and the invalid character still contributes to result via the temp value from the previous checks. Consider returning early or using break.

         if ((hex[i] >= '0' && hex[i] <= '9') ||
             (hex[i] >= 'a' && hex[i] <= 'f') ||
             (hex[i] >= 'A' && hex[i] <= 'F')) {
             result += temp * (int)pow(16, len - i - 1);
         } else {
             printf("Error: Not a valid hex value.\n Try this format: 0xHHHH, "
                    "where H is a hex digit.\n");
+            return -1;  // or another sentinel value
         }
chapter_2/exercise_2_03/htoi.c-38-41 (1)

38-41: Prefix check runs on every iteration.

The 0x/0X prefix check is inside the while loop, so it could match mid-string (e.g., input "120xAB" would skip to "AB" partway through). This is likely unintended.

Move the prefix handling before the loop:

+    if (len >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X')) {
+        i = 2;
+    }
+
     while (i < len) {
-        if (hex[i] == '0' && (hex[i + 1] == 'x' || hex[i + 1] == 'X')) {
-            i += 2;
-        }
-
         int temp = tolower(hex[i]);
chapter_5/exercise_5_05/strncpy.c-17-21 (1)

17-21: Remove extraneous semicolon.

Line 20 contains a standalone semicolon that serves no purpose and reduces code clarity.

Apply this diff:

 void strcpy_ptr(char *s, char *t, size_t n) {
     while ((*s++ = *t++) && --n)
         ;
-    ;
 }
chapter_1/exercise_1_15/temperature_conversion.c-5-25 (1)

5-25: Fix typo in comment.

Line 13 contains a typo: "abouve" should be "above".

Apply this diff:

-    // Printing a heading abouve the table
+    // Printing a heading above the table
chapter_5/exercise_5_09/date_conversion_pointers.c-70-78 (1)

70-78: yearday lower bound not validated.

The check validates upper bounds but doesn't reject yearday <= 0. A zero or negative yearday would skip the loop entirely, returning *pmonth = 0 and *pda = yearday, which are invalid results without any error indication.

     // Additional check for yearday.
-    if ((!leap && yearday > 365) || (leap && yearday > 366)) {
+    if (yearday < 1 || (!leap && yearday > 365) || (leap && yearday > 366)) {
         // Reset the provided month and day.
         *pmonth = 0;
         *pda = 0;
-        printf("Error: year %d has %d days.\n", year, leap ? 366 : 365);
+        printf("Error: yearday must be between 1 and %d for year %d.\n",
+               leap ? 366 : 365, year);

         return;
     }
chapter_5/exercise_5_09/date_conversion_pointers.c-45-50 (1)

45-50: Month validation misses lower bound check.

The validation only checks month > 12 but doesn't validate month < 1. Passing month = 0 or negative values would access daytab[leap][0] (the sentinel) or cause undefined behavior.

     // Additional checks for month.
-    if (month > 12) {
+    if (month < 1 || month > 12) {
         printf("Error: a year has 12 months, so please choose a number "
                "betweeen 1 and 12.\n");
         return -1;
     }
chapter_7/exercise_7_03/minprintf.c-36-39 (1)

36-39: %X format specifier prints lowercase instead of uppercase.

The case 'X' falls through to case 'x' and uses "%x", so uppercase %X in the format string produces lowercase hex output. To match standard printf behavior, handle them separately.

         case 'x':
-        case 'X':
             printf("%x", va_arg(arg_p, int));
             break;
+
+        case 'X':
+            printf("%X", va_arg(arg_p, int));
+            break;
chapter_7/exercise_7_03/minprintf.c-57-65 (1)

57-65: %E and %G format specifiers also print lowercase.

Same issue as %X: the case 'E' falls through to case 'e' using "%e", and case 'G' falls through to case 'g' using "%g". Both should preserve the uppercase output format.

         case 'e':
-        case 'E':
             printf("%e", va_arg(arg_p, double));
             break;

+        case 'E':
+            printf("%E", va_arg(arg_p, double));
+            break;
+
         case 'g':
-        case 'G':
             printf("%g", va_arg(arg_p, double));
             break;
+
+        case 'G':
+            printf("%G", va_arg(arg_p, double));
+            break;
chapter_5/exercise_5_16/sort.c-43-51 (1)

43-51: Empty input causes underflow in quick_sort call.

When read_lines returns 0 (no lines read), nr_of_lines - 1 underflows to SIZE_MAX. Although quick_sort handles this via the (long)start >= (long)end check, passing SIZE_MAX as end is semantically incorrect and relies on implementation details.

     if ((nr_of_lines = read_lines(line_ptr, MAX_NR_OF_LINES)) != (size_t)-1) {
-        quick_sort((void **)line_ptr, 0, nr_of_lines - 1,
-                   (int (*)(void *, void *))comp);
-        write_lines(line_ptr, nr_of_lines);
+        if (nr_of_lines > 0) {
+            quick_sort((void **)line_ptr, 0, nr_of_lines - 1,
+                       (int (*)(void *, void *))comp);
+        }
+        write_lines(line_ptr, nr_of_lines);
     } else {
chapter_5/exercise_5_15/sort.c-43-50 (1)

43-50: Empty input causes underflow in quick_sort call.

Same issue as exercise_5_16: when nr_of_lines is 0, nr_of_lines - 1 underflows to SIZE_MAX. Guard with an empty check before calling quick_sort.

     if ((nr_of_lines = read_lines(line_ptr, MAX_NR_OF_LINES)) != (size_t)-1) {
-        quick_sort((void **)line_ptr, 0, nr_of_lines - 1,
-                   (int (*)(void *, void *))comp);
-        write_lines(line_ptr, nr_of_lines);
+        if (nr_of_lines > 0) {
+            quick_sort((void **)line_ptr, 0, nr_of_lines - 1,
+                       (int (*)(void *, void *))comp);
+        }
+        write_lines(line_ptr, nr_of_lines);
     } else {
chapter_7/exercise_7_06/compare.c-34-48 (1)

34-48: Resource leak: files are not closed when files match completely.

If the while loop completes without finding a mismatch (files are identical), neither file_1 nor file_2 is closed before exit(EXIT_SUCCESS). While the OS reclaims resources on exit, explicitly closing files is good practice and ensures buffers are flushed.

         ++line_number;
     }
 
+    fclose(file_1);
+    fclose(file_2);
     exit(EXIT_SUCCESS);
 }
chapter_4/exercise_4_06/variables.c-227-235 (1)

227-235: Logic issue: returning next discards the minus sign and breaks token string.

When c == '-' and next is neither whitespace, digit, nor '.', line 228 returns next directly. However, s[0] still contains '-', so subsequent error messages using s will be misleading. The character next should be ungetch'd and '-' returned as an operator, or the token string should be updated.

         } else if (!isdigit(next) && next != '.') {
-            return next; // not a number
+            ungetch(next);
+            return c; // return '-' as operator
chapter_4/exercise_4_05/math.c-186-187 (1)

186-187: Same issue: returning next discards the minus sign.

Same pattern as other calculator files. When - is followed by a non-numeric, non-whitespace character, next is returned but the token string s is inconsistent.

         } else if (!isdigit(next) && next != '.') {
-            return next; // not a number
+            ungetch(next);
+            return c; // return '-' as operator
chapter_4/exercise_4_03/calculator.c-122-123 (1)

122-123: Same issue: returning next discards the minus sign.

Same as in variables.c: when - is followed by a non-numeric, non-whitespace character, next is returned but s[0] still contains '-'. Consider ungetch'ing next and returning '-' as the operator.

         } else if (!isdigit(next) && next != '.') {
-            return next; // not a number
+            ungetch(next);
+            return c; // return '-' as operator
chapter_6/exercise_6_01/count_c_keywords.c-121-125 (1)

121-125: Off-by-one: null terminator can write past buffer.

If a word reaches exactly max_word_len characters, the while loop exits with i == max_word_len, and Line 125 writes word[max_word_len] = '\0', which is one byte past the allocated buffer.

-    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_word_len) {
+    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_word_len - 1) {
         word[i++] = c;
     }
chapter_5/exercise_5_20/dcl.c-125-133 (1)

125-133: Off-by-one: null terminator can write past buffer.

If an identifier reaches exactly max_len characters, the loop exits with i == max_len, and Line 131 writes dest[max_len] = '\0', which is out of bounds.

-    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_len) {
+    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_len - 1) {
         dest[i++] = c;
     }
chapter_4/exercise_4_04/stack.c-167-178 (1)

167-178: Potential token loss when '-' is followed by non-numeric, non-whitespace character.

When - is followed by a character that is neither whitespace/newline nor a digit/dot (e.g., -x), Line 173 returns next directly. This discards the - character that was stored in s[0], which may cause unexpected behavior for inputs like -abc.

Consider whether the - should be ungetch'd so subsequent calls can process it:

         } else if (!isdigit(next) && next != '.') {
-            return next; // not a number
+            ungetch(next);
+            return c; // return '-' as operator, put back next
         } else           // number like "-5", "-.6" etc, next is digit or '.'
chapter_5/exercise_5_18/dcl.c-93-101 (1)

93-101: Off-by-one risk when max_len characters are read.

If exactly max_len characters are read, the loop terminates but dest[i] writes the null terminator at index max_len, which is one past the valid range if the caller sized the buffer exactly to max_len.

Consider using i < max_len - 1 to reserve space for the null terminator:

-    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_len) {
+    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_len - 1) {
chapter_6/exercise_6_06/define.c-170-179 (1)

170-179: Potential buffer overflow when max_str_len characters are read.

Similar to get_name in dcl.c, if exactly max_str_len characters are read, the null terminator at str[i] writes past the buffer.

-    while (isalnum(c = getc(stdin)) && i < max_str_len) {
+    while (isalnum(c = getc(stdin)) && i < max_str_len - 1) {
chapter_5/exercise_5_06/itoa.c-32-43 (1)

32-43: itoa fails for n = 0 and negative numbers.

The while (n) loop condition means when n is 0, the loop body never executes, resulting in an empty string. Additionally, negative numbers will produce incorrect results due to the modulo behavior with negative operands.

For a complete implementation:

 void itoa(char *s, int n) {
     char *t = s;
+    int sign = n;
+
+    if (n < 0) {
+        n = -n;
+    }
+
+    do {
+        *(t++) = n % 10 + '0';
+        n /= 10;
+    } while (n);
 
-    while (n) {
-        *(t++) = n % 10 + '0';
-        n /= 10;
-    }
+    if (sign < 0) {
+        *(t++) = '-';
+    }
 
     *t = '\0';
 
     reverse(s);
 }
chapter_6/exercise_6_06/define.c-161-165 (1)

161-165: Same buffer boundary issue in get_word.

The loop can fill up to max_word_len characters before null-terminating at word[i], risking a write past the buffer.

-    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_word_len) {
+    while ((isalnum(c = getc(stdin)) || c == '_') && i < max_word_len - 1) {
🧹 Nitpick comments (38)
chapter_2/exercise_2_06/setbits.c (1)

24-33: Excellent fix for undefined behavior.

Switching masks from signed 0 to unsigned 0U prevents undefined behavior when left-shifting. Left-shifting negative signed values is UB in C, and ~0 (when int is signed) produces a negative value. Using ~0U ensures the mask operations are well-defined. The comment clearly documents this important safety improvement.

Minor note: The function returns unsigned int but accepts int parameters. While this works through implicit conversion, consider making parameter types unsigned int as well for consistency. Given this is educational K&R exercise code, the current approach is acceptable.

chapter_1/exercise_1_14/frequency_histogram.c (1)

27-31: Simplify the redundant condition.

The else if (i >= ALPHA_NR) condition on line 29 is redundant since we're already in the else block where i < ALPHA_NR is false. Simplify to just else for clarity.

Apply this diff:

         if (i < ALPHA_NR) {
             printf("%c: ", 'a' + i);
-        } else if (i >= ALPHA_NR) {
+        } else {
             printf("%c: ", '0' + i - ALPHA_NR);
         }
chapter_2/exercise_2_07/invert.c (2)

28-31: Consider simplifying with XOR.

The current logic is correct but can be simplified. Since mask1 has 1s at positions to invert, XOR directly flips those bits:

Apply this diff:

-    unsigned int mask1 = ~(~0U << n) << p;
-    unsigned int mask2 = ~mask1 & x;
-
-    return mask2 | ~x;
+    unsigned int mask = ~(~0U << n) << p;
+    return x ^ mask;

This achieves the same result more concisely by XORing with the mask, which flips only the targeted bits.


23-24: Optional: Align parameter types with implementation.

The function signature accepts int x but the implementation treats it as unsigned. Consider updating the signature for consistency:

-unsigned int invert(int x, int p, int n) {
-    ++p; // First position is 0
+unsigned int invert(unsigned int x, unsigned int p, unsigned int n) {
+    ++p; // Adjust p for 0-based indexing (shift amount)

Benefits:

  • Eliminates implicit signed→unsigned conversion for x
  • unsigned int for p and n better represents valid bit positions (non-negative)
  • Clearer comment explains the adjustment
chapter_5/exercise_5_13/tail.c (2)

91-112: Consider using SIZE_MAX or a sentinel value explicitly.

Returning -1 from a size_t function relies on implicit cast to SIZE_MAX. While technically defined, it's less clear than using SIZE_MAX explicitly or redesigning the function to use a different error indicator (e.g., return int status and use output parameter for count).

Apply this diff to use SIZE_MAX explicitly:

 size_t read_lines(char *line_ptr[], const size_t max_nr_of_lines) {
     size_t line_length;
     size_t nr_of_lines = 0;
 
     char *current_line = alloc(MAX_LINE_LEN);
     char *current_line_copy = NULL;
 
     while ((line_length = get_line(current_line, MAX_LINE_LEN))) {
         if (nr_of_lines >= max_nr_of_lines ||
             (current_line_copy = alloc(line_length)) == NULL) {
-            return -1;
+            return SIZE_MAX;
         } else {
             current_line[line_length - 1] = '\0';
             strcpy(current_line_copy, current_line);
             line_ptr[nr_of_lines++] = current_line_copy;
         }
     }
 
     afree(current_line);
 
     return nr_of_lines;
 }

And update the check in main:

-    if ((total_nr_of_lines = read_lines(line_ptr, MAX_NR_OF_LINES)) !=
-        (size_t)-1) {
+    if ((total_nr_of_lines = read_lines(line_ptr, MAX_NR_OF_LINES)) !=
+        SIZE_MAX) {
         write_lines(line_ptr, nr_of_lines_to_print, total_nr_of_lines);
     } else {

128-137: Consider defensive check for pointer validity.

The cast of ptrdiff_t to size_t at line 131 could mask bugs if alloc_p somehow exceeds alloc_buf + ALLOC_SIZE. While this shouldn't happen in correct usage, a defensive check would improve robustness.

Apply this diff:

 char *alloc(size_t size) {
-    // Cast pointer difference to size_t to match unsigned size parameter
-    // This prevents sign comparison warnings
-    if ((size_t)(alloc_buf + ALLOC_SIZE - alloc_p) >= size) {
+    if (alloc_p >= alloc_buf && alloc_p <= alloc_buf + ALLOC_SIZE &&
+        (size_t)(alloc_buf + ALLOC_SIZE - alloc_p) >= size) {
         alloc_p += size;
         return alloc_p - size;
     }
 
     return NULL;
 }
chapter_5/exercise_5_10/expr.c (5)

8-8: Consider using size_t for the stack pointer.

While unsigned char is sufficient for the current STACK_SIZE of 15, using size_t is more idiomatic for array indexing and avoids future issues if STACK_SIZE increases. It also matches the type typically used for array indices in C.

Based on learnings, JebediahS suggested this improvement in the PR discussion.

Apply this diff:

-unsigned char stack_pointer = 0; // points to next free slot
+size_t stack_pointer = 0; // points to next free slot

15-15: Consider using an enum or bool for error tracking.

The char Error variable works, but using an enum for error codes or separate bool flags would improve code clarity and type safety.

Example with enum:

+typedef enum {
+    ERROR_NONE = 0,
+    ERROR_INVALID_ARG = 1,
+    ERROR_STACK_MISMATCH = 2,
+    ERROR_INVALID_OP = 3,
+    ERROR_DIV_BY_ZERO = 4
+} ErrorCode;
+
-    char Error = 0;
+    ErrorCode Error = ERROR_NONE;

106-117: Consider returning status instead of printing errors.

The current implementation prints errors directly and returns 0.0, which is ambiguous since 0.0 is a valid stack value. This reduces the function's reusability and makes it difficult for callers to distinguish between success and failure.

Based on learnings, JebediahS suggested using a status-based return pattern.

Apply this diff for a status-based approach:

-float pop(void) 
+int pop(float *result) 
 {
     if (stack_pointer > 0) 
     {
         // since stack_pointer points to the next free slot
         // it has to be decremented to point to a non-empty
         // place
-        return stack[--stack_pointer];
+        *result = stack[--stack_pointer];
+        return 1; // success
     }
-    printf("Error: the stack is empty.\n");
-    return 0;
+    return 0; // failure: stack empty
 }

Then update callers to check the return value:

-                float number2 = pop();
-                float number1 = pop();
+                float number1, number2;
+                if (!pop(&number2) || !pop(&number1)) {
+                    Error = 2; // stack underflow
+                    continue;
+                }

119-126: Consider returning status for consistency.

Similar to pop(), push() should return a status code instead of printing errors directly. This improves reusability and allows callers to handle overflow conditions appropriately.

Based on learnings, JebediahS suggested using a status-based return pattern.

Apply this diff for consistency with the suggested pop() changes:

-void push(float element) {
+int push(float element) {
     if (stack_pointer < STACK_SIZE) {
         // starts filling at stack[0]
         stack[stack_pointer++] = element;
+        return 1; // success
     } else {
-        printf("Error: the stack is full.\n");
+        return 0; // failure: stack full
     }
 }

Then update callers:

-            push(number);
+            if (!push(number)) {
+                printf("Error: stack overflow.\n");
+                return EXIT_FAILURE;
+            }

14-104: Tests are needed to verify the stack fixes.

As discussed in the PR comments, adding tests would help verify the off-by-one fix and prevent regressions. The conversation identified several testing approaches suitable for this K&R solutions repository.

Based on the PR discussion, would you like me to help generate test code? I can provide:

  1. Shell script tests (test_expr.sh) that invoke the compiled program with various argument combinations and verify output — following the pattern suggested by CodeRabbit for CLI programs.

  2. Test cases covering:

    • Push/pop single item
    • Push until full (15 elements)
    • Pop until empty
    • LIFO order verification
    • Edge cases: empty arguments, missing operators, division by zero
    • Zero value handling: "0", "0.0", "-0"

Let me know if you'd like me to open a new issue or provide the test implementation directly.

chapter_3/exercise_3_06/itoa.c (1)

1-1: Consider removing unused include.

<limits.h> doesn't appear to be used in this file—abs() comes from <stdlib.h>.

-#include <limits.h>
chapter_2/exercise_2_09/bitcount.c (1)

24-26: Prefer bit-shift over pow() for bit operations.

Using pow(2, i) involves floating-point arithmetic, which is slower and can introduce precision issues. Use bit-shift for integer operations:

-        (x & (unsigned int)pow(2, i)) ? putchar('1') : putchar('0');
+        (x & (1U << i)) ? putchar('1') : putchar('0');

This also removes the dependency on <math.h> for this function.

chapter_2/exercise_2_10/lower.c (1)

13-13: Consider using addition instead of compound assignment in ternary.

The expression c += 'a' - 'A' works but unnecessarily mutates the parameter. A cleaner form would be:

-char lower(char c) { return (c >= 'A' && c <= 'Z') ? c += 'a' - 'A' : c; }
+char lower(char c) { return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; }

This avoids the side effect and is clearer in intent.

chapter_1/exercise_1_22/fold_line.c (1)

65-69: Potential edge case with last_blank check.

Using if (last_blank) treats index 0 as "no blank found." While this is safe here since split only becomes TRUE at column 30+, making last_blank at index 0 impossible, consider using a sentinel value like -1 for clarity:

-    int last_blank = 0;
+    int last_blank = -1;
...
-            if (last_blank) {
+            if (last_blank >= 0) {
                 fold_str[last_blank] = '\n';
                 column = j - last_blank;
-                last_blank = 0;
+                last_blank = -1;
chapter_1/exercise_1_23/c_remove_comments.c (1)

76-80: Redundant condition on line 78.

The else if (!line_comment || !block_comment) condition is always true when reached, since the preceding if already handles the case where either flag is set. This can be simplified to else:

             if (line_comment || block_comment) {
                 ++i;
-            } else if (!line_comment || !block_comment) {
+            } else {
                 no_com_str[j++] = str[i++];
             }
chapter_5/exercise_5_14/sort.c (1)

68-71: Unreachable code after return.

The break on line 70 is dead code since it follows a return statement.

                 default:
                     return 0;
-                    break;
                 }
chapter_4/exercise_4_08/getch.c (1)

19-30: Consider using int for temp to match buf and return type.

temp is declared as char but buf is int and getch returns int. For characters with values > 127 (extended ASCII), assigning to char may cause sign extension issues on some platforms.

 int getch(void) {
-    char temp;
+    int temp;
 
     if (buf != -1) {
         temp = buf;
chapter_6/exercise_6_02/var_group.c (1)

94-100: Consider adding null checks after malloc.

The malloc calls in str_dup (line 95), add_to_tree (line 201), and add_to_list (line 225) don't verify allocation success before dereferencing. While this appears to be a pre-existing pattern in the codebase, adding null checks would improve robustness.

Example for str_dup:

 char *str_dup(char *src) {
     char *dest = (char *)malloc(strlen(src) + 1);
-    if (dest != NULL) {
+    if (dest == NULL) {
+        return NULL;
+    }
-        strcpy(dest, src);
-    }
+    strcpy(dest, src);
     return dest;
 }

Similar checks should be added for add_to_tree and add_to_list.

Also applies to: 197-235

chapter_7/exercise_7_03/minprintf.c (1)

41-43: %u should extract unsigned int from va_list.

The %u format specifier is for unsigned integers, but va_arg(arg_p, int) extracts a signed int. This can produce incorrect output for large unsigned values.

         case 'u':
-            printf("%u", va_arg(arg_p, int));
+            printf("%u", va_arg(arg_p, unsigned int));
             break;
chapter_2/exercise_2_08/rightrot.c (1)

24-26: Prefer bit shift over pow() for bit extraction.

Using pow(2, i) involves floating-point arithmetic, which is slower and can introduce precision issues for large values. Use (1U << i) instead for integer bit operations.

     int i;
     for (i = n * 8 - 1; i >= 0; --i) {
-        (x & (unsigned int)pow(2, i)) ? putchar('1') : putchar('0');
+        (x & (1U << i)) ? putchar('1') : putchar('0');
     }

This also allows removing the #include <math.h> dependency.

chapter_5/exercise_5_16/sort.c (1)

78-81: Unreachable break after return.

The break on line 80 is never executed since return 0 exits the function first.

                 default:
                     return 0;
-                    break;
                 }
chapter_5/exercise_5_15/sort.c (1)

73-76: Unreachable break after return.

                 default:
                     return 0;
-                    break;
                 }
chapter_7/exercise_7_06/compare.c (1)

25-28: Minor: file_1 not closed if file_2 fails to open.

If opening file_2 fails, file_1 remains open when the program exits. Consider closing file_1 before exiting for consistency.

     if ((file_2 = fopen(argv[2], "r")) == NULL) {
         fprintf(stderr, "%s: can't open %s.\n", program_name, argv[2]);
+        fclose(file_1);
         exit(EXIT_FAILURE);
     }
chapter_4/exercise_4_03/calculator.c (1)

59-61: Result printed even when stack is empty.

On newline, pop() is always called. If the stack is empty, this prints "Error: stack empty." followed by "result: 0" which is confusing. Consider checking if the stack is empty first, similar to chapter_4/exercise_4_06/variables.c lines 120-124.

         case '\n':
-            printf("result: %.8g\n", pop());
+            if (sp > 0) {
+                printf("result: %.8g\n", pop());
+            }
             break;
chapter_4/exercise_4_05/math.c (1)

94-96: Result printed even when stack is empty.

Same issue as chapter_4/exercise_4_03/calculator.c: on newline, pop() is called unconditionally. This prints an error message followed by a misleading "result: 0" if the stack is empty.

         case '\n':
-            printf("result: %.8g\n", pop());
+            if (sp > 0) {
+                printf("result: %.8g\n", pop());
+            }
             break;
chapter_4/exercise_4_04/stack.c (1)

122-134: Consider guarding duplicate() and swap() against empty/underflow.

Both functions call pop() without checking stack state first. If the stack is empty or has insufficient elements, pop() prints an error but returns 0.0, which then gets pushed—silently corrupting the stack state.

 void duplicate(void) {
+    if (sp < 1) {
+        printf("Error: stack empty, can't duplicate.\n");
+        return;
+    }
     double temp = pop();
     push(temp);
     push(temp);
 }

 void swap(void) {
+    if (sp < 2) {
+        printf("Error: stack needs at least 2 elements to swap.\n");
+        return;
+    }
     double temp1 = pop();
     double temp2 = pop();

     push(temp1);
     push(temp2);
 }
chapter_5/exercise_5_12/entab.c (2)

86-93: Empty string returns true; strlen() called repeatedly in loop.

  1. An empty string "" passes validation since the loop never executes, returning 1. This could accept invalid arguments like ./entab + (where argv[i]+1 is empty).

  2. strlen(str) is called on every iteration, making this O(n²). Consider caching or using a pointer-based approach.

 int is_str_uint(char *str) {
-    for (size_t i = 0; i < strlen(str); ++i) {
-        if (!isdigit(str[i])) {
+    if (*str == '\0') {
+        return 0;  // empty string is not a valid uint
+    }
+    for (; *str != '\0'; ++str) {
+        if (!isdigit((unsigned char)*str)) {
             return 0;
         }
     }
     return 1;
 }

54-57: Complex condition may benefit from clarifying comments.

The tab insertion condition spans multiple lines with three distinct cases. A brief inline comment explaining each case would improve readability for future readers.

chapter_6/exercise_6_01/count_c_keywords.c (1)

23-33: Note: size_t is a typedef, not a C keyword.

The keyword table includes size_t, which is technically a typedef from <stddef.h> rather than a reserved C keyword. This is a minor inaccuracy in the educational context.

chapter_5/exercise_5_20/dcl.c (1)

221-239: Unbounded strcat() on out[] could overflow.

The out buffer (1000 chars) is concatenated repeatedly without bounds checking. While unlikely with typical input, deeply nested declarations or many function parameters could overflow the buffer.

Consider using snprintf() or tracking the remaining buffer size to prevent potential overflow in edge cases.

chapter_5/exercise_5_06/getop.c (1)

33-35: Redundant condition: c != '.' is unnecessary.

Since '.' is not a digit, isdigit(c) already excludes it. The && c != '.' part is redundant.

-    if (isdigit(c) && c != '.')
+    if (isdigit(c))
         while (isdigit(*(++s) = c = getchar()))
             ;
.github/workflows/c.yml (1)

1-22: CI workflow looks good; consider adding a test step.

The workflow structure is sound: format checking and linting run before the build, which is the correct order. Given the PR discussion mentions adding tests to the repository, consider adding a test step once tests are available:

       - name: Build
         run: make
+
+      - name: Test
+        run: make test

This would require a corresponding test target in the Makefile.

chapter_1/exercise_1_20/detab.c (1)

10-16: Tab expansion uses fixed 8 spaces instead of position-aware expansion.

The current implementation always outputs 8 spaces per tab, regardless of the current column position. True tab behavior expands to the next tab stop (e.g., at column 3, a tab would insert 5 spaces to reach column 8). For K&R exercise 1-20, this simplified approach may be acceptable if the exercise doesn't require position-aware expansion.

If position-aware expansion is desired:

 int main(void) {
     int c;
-    unsigned int nr_of_spaces;
+    unsigned int line_pos = 0;

     while ((c = getchar()) != EOF) {
         if (c == '\t') {
-            nr_of_spaces = TAB_LENGTH;
-
-            while (nr_of_spaces) {
+            do {
                 putchar(' ');
-                --nr_of_spaces;
-            }
+                ++line_pos;
+            } while (line_pos % TAB_LENGTH != 0);
         } else {
             putchar(c);
+            if (c == '\n') {
+                line_pos = 0;
+            } else {
+                ++line_pos;
+            }
         }
     }
Makefile (2)

26-31: install-hooks assumes scripts/pre-commit exists without verification.

If scripts/pre-commit doesn't exist, the cp command will fail silently (depending on shell behavior) or with an error. Consider adding a check for the source file.

 install-hooks:
-	@if [ -d .git ] && [ ! -f .git/hooks/pre-commit ]; then \
+	@if [ -d .git ] && [ ! -f .git/hooks/pre-commit ] && [ -f scripts/pre-commit ]; then \
 		cp scripts/pre-commit .git/hooks/pre-commit; \
 		chmod +x .git/hooks/pre-commit; \
 		echo "Pre-commit hook installed."; \
 	fi

40-40: Consider adding a test target placeholder.

As discussed in the PR comments, the repository is exploring testing approaches. Adding a placeholder test target now would make CI integration smoother later.

+test:
+	@echo "No tests configured yet. See PR discussion for testing approaches."
+
-.PHONY: all lint format format-check check install-hooks clean
+.PHONY: all lint format format-check check install-hooks clean test
chapter_6/exercise_6_04/words_frequency.c (1)

134-151: Consider adding malloc failure handling.

Lines 138-141 allocate and immediately use a tree_node without checking if malloc returned NULL. If allocation fails, this leads to null pointer dereferences.

Apply this diff to add proper error handling:

     if (node_p == NULL) {
         node_p = (struct tree_node *)malloc(sizeof(struct tree_node));
+        if (node_p == NULL) {
+            fprintf(stderr, "Error: memory allocation failed\n");
+            exit(EXIT_FAILURE);
+        }
         node_p->word = str_dup(word);
+        if (node_p->word == NULL) {
+            free(node_p);
+            fprintf(stderr, "Error: memory allocation failed\n");
+            exit(EXIT_FAILURE);
+        }
         node_p->count = 1;
         node_p->left = node_p->right = NULL;

Based on static analysis hints.

chapter_5/exercise_5_12/detab.c (1)

81-88: Optionally cache strlen result in loop.

Line 82 calls strlen(str) on every iteration. For better performance, consider caching the length:

 int is_str_uint(char *str) {
-    for (size_t i = 0; i < strlen(str); ++i) {
+    size_t len = strlen(str);
+    for (size_t i = 0; i < len; ++i) {
         if (!isdigit(str[i])) {
             return 0;
         }
     }
     return 1;
 }

Note: The same pattern exists in entab.c (shown in relevant snippets), so consider applying this optimization to both files.

Comment on lines 22 to 25
char c;
int word_count_index = 0;
while ((c = getchar())) {
if (c == ' ' || c == '\t' || c == '\n' || c == EOF) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix EOF handling by using int type for getchar().

The variable c is declared as char on line 22, but getchar() returns int to distinguish EOF from valid byte values. Comparing a char with EOF (typically -1) is undefined behavior and fails on platforms where char is unsigned. This causes the EOF check on line 25 to malfunction, potentially leading to an infinite loop or incorrect termination.

Apply this diff to fix the type and loop condition:

     // Count the words length and store in histogram array at the
     // specific index
-    char c;
+    int c;
     int word_count_index = 0;
-    while ((c = getchar())) {
+    while ((c = getchar()) != EOF) {
         if (c == ' ' || c == '\t' || c == '\n' || c == EOF) {

Note: After this fix, the explicit c == EOF check on line 25 becomes redundant with the loop condition, but keeping it doesn't harm correctness.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char c;
int word_count_index = 0;
while ((c = getchar())) {
if (c == ' ' || c == '\t' || c == '\n' || c == EOF) {
int c;
int word_count_index = 0;
while ((c = getchar()) != EOF) {
if (c == ' ' || c == '\t' || c == '\n' || c == EOF) {
🤖 Prompt for AI Agents
In chapter_1/exercise_1_13/histogram.c around lines 22 to 25, the variable c is
declared as char but getchar() returns int so EOF cannot be reliably detected;
change c's type to int and update the loop to while ((c = getchar()) != EOF)
(you may keep or remove the explicit c == EOF check inside the loop since the
loop condition already handles EOF), ensuring all comparisons to EOF use the int
variable.

Comment on lines 84 to 92
if (x == v[low - 1]) {
return low - 1;
}

return -1;
return -1;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Out-of-bounds array access when low is 0.

When searching for a value smaller than all elements, the loop terminates with low == 0. The expression v[low - 1] then accesses v[-1], which is undefined behavior.

The test on line 22 uses x = -1, which triggers this exact condition since all vector elements are non-negative.

Add a bounds check:

-    if (x == v[low - 1]) {
+    if (low > 0 && x == v[low - 1]) {
         return low - 1;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (x == v[low - 1]) {
return low - 1;
}
return -1;
return -1;
if (low > 0 && x == v[low - 1]) {
return low - 1;
}
return -1;
🤖 Prompt for AI Agents
In chapter_3/exercise_3_01/binsearch.c around lines 84 to 88, the final check
uses v[low - 1] which is out-of-bounds when low == 0; change the logic to first
verify low > 0 before accessing v[low - 1], returning low - 1 only when low > 0
and v[low - 1] == x, otherwise return -1.

Comment on lines 189 to 193
void clear(void) {
do {
stack[sp] = 0.0;
} while (sp--);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: clear() causes stack pointer underflow.

The do-while loop always executes at least once and decrements sp unconditionally. When sp == 0, this sets sp = -1, causing subsequent push() calls to write to stack[-1] (memory corruption).

 void clear(void) {
-    do {
-        stack[sp] = 0.0;
-    } while (sp--);
+    while (sp > 0) {
+        stack[--sp] = 0.0;
+    }
 }
🤖 Prompt for AI Agents
In chapter_4/exercise_4_10/calculator.c around lines 189 to 193, clear() uses a
do-while that always decrements sp and can underflow when sp == 0; replace the
do-while with a loop that only runs while sp >= 0 (for example: while (sp >= 0)
{ stack[sp] = 0.0; sp--; } or iterate from i = sp down to 0 to zero elements and
then set sp = -1) so sp never becomes negative and the stack is properly
cleared.

Comment on lines 92 to 133
int getop(char s[]) {
int i = 0, c;
static int buf = EOF;

while ((s[0] = c = getchar()) == ' ' || c == '\t')
;

s[1] = '\0';

while ((s[0] = c = getchar()) == ' ' || c == '\t')
;
if (!isdigit(c) && c != '.' && c != '-') {
return c;
}

if (c == '-') {
int next = getchar();
if (!isdigit(next) && next != '.') {
return next;
}

s[i] = c;
c = next = buf;
} else {
c = getchar();
}

s[1] = '\0';
if (isdigit(c)) {
while (isdigit(s[++i] = c = getchar()))
;
}

if (!isdigit(c) && c != '.' && c != '-')
{
return c;
}
if (c == '.') {
while (isdigit(s[++i] = c = getchar()))
;
}

if (c == '-')
{
int next = getchar();
if (!isdigit(next) && next != '.')
{
return next;
if (c != EOF) {
buf = c;
}

s[i] = c;
c = next = buf;
}
else
{
c = getchar();
}

if (isdigit(c))
{
while (isdigit(s[++i] = c = getchar()))
;
}

if (c == '.')
{
while (isdigit(s[++i] = c = getchar()))
;
}

if (c != EOF)
{
buf = c;
}

return NUMBER;
return NUMBER;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical logic errors in negative number handling.

The negative number parsing has several issues:

  1. Line 108: Returns next directly without buffering it, causing the character to be lost.
  2. Line 112: The assignment c = next = buf is incorrect. It should buffer next into buf and then use next as the current character. Currently, it assigns EOF to both variables.
  3. The logic flow between lines 105-115 is fundamentally broken.

The correct approach should:

  • After reading the minus sign, check the next character
  • If next is not a digit/period, return minus and buffer next
  • If next is a digit/period, continue parsing the number

Apply this diff to fix the logic:

     if (c == '-') {
         int next = getchar();
         if (!isdigit(next) && next != '.') {
-            return next;
+            if (next != EOF) {
+                buf = next;
+            }
+            return c;
         }
 
-        s[i] = c;
-        c = next = buf;
+        s[++i] = c;
+        c = next;
     } else {
         c = getchar();
     }
🤖 Prompt for AI Agents
In chapter_4/exercise_4_11/getop.c around lines 92-132 the negative-number
handling is wrong: after reading '-' you must peek the next char and if it is
not a digit or '.' you must buffer that peeked char into buf and return '-' (not
the peeked char); if it is a digit or '.' you must treat '-' as the start of a
number by storing '-' and the peeked char into s (set i appropriately), set c to
the peeked char to continue numeric parsing, and do NOT overwrite these with EOF
— implement buffering as buf = next when you need to save the peeked char, and
set c = next when you continue parsing so the subsequent while loops read the
rest of the number correctly.

Comment on lines 39 to 42
while (*s-- == *t--)
--t_length;

return !t_length;
return !t_length;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n chapter_5/exercise_5_04/strend.c

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 1605


🏁 Script executed:

find . -name "strend.c" -type f

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Find and display the strend.c file with full context
if [ -f "chapter_5/exercise_5_04/strend.c" ]; then
  echo "=== File found: chapter_5/exercise_5_04/strend.c ==="
  cat -n chapter_5/exercise_5_04/strend.c
else
  echo "File not found at expected path. Searching for it..."
  find . -name "strend.c" -type f 2>/dev/null
fi

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 1658


Critical: Buffer underflow in backward string comparison loop.

The while condition at line 39 lacks a guard for when t_length reaches zero. After matching all characters in the shorter string t, the loop continues if the current characters still match, causing pointer t to decrement into invalid memory offsets, resulting in undefined behavior.

Example failure case:

  • strend("test", "st") should return 1
  • After first match ('t'=='t'): t moves from index 1 to 0, t_length becomes 1
  • After second match ('s'=='s'): t moves from index 0 to -1 (out of bounds) before t_length is decremented to 0
  • Third iteration attempts comparison with memory before the buffer

Add the t_length boundary check:

-    while (*s-- == *t--)
+    while (t_length > 0 && *s-- == *t--)
         --t_length;

This ensures the loop exits immediately when all characters in t have been matched, preventing out-of-bounds pointer access.

🤖 Prompt for AI Agents
In chapter_5/exercise_5_04/strend.c around lines 39 to 42, the backward
comparison loop can decrement t past the start of the buffer causing undefined
behavior; modify the loop to check that t_length is greater than zero before
doing the character comparison so the loop exits immediately when all characters
of t have been matched (i.e., add a t_length guard in the while condition),
preserving the existing pointer semantics and the final return of !t_length.

Comment on lines 38 to 40
char *p =
stored_lines + strlen(stored_lines); // Init p with the first empty
// position from stored_lines
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Calling strlen on uninitialized memory causes undefined behavior.

This line calls strlen on the stored_lines buffer, which is passed in uninitialized from main. The function will read arbitrary memory until it encounters a null byte, leading to unpredictable behavior.

Based on the comment, the intent is to support appending to an existing buffer, but the caller must ensure the buffer is properly initialized (either zeroed or null-terminated).

The fix in the previous comment (initializing stored_lines in main) will resolve this issue.

🧰 Tools
🪛 Cppcheck (2.18.0)

[warning] 39-39: Uninitialized variable

(uninitvar)

🤖 Prompt for AI Agents
In chapter_5/exercise_5_07/readlines.c around lines 38 to 40, the code calls
strlen(stored_lines) on a buffer that may be uninitialized, causing undefined
behavior; fix by ensuring the buffer is null-terminated before using strlen
(e.g., initialize stored_lines[0] = '\0' or memset the buffer to 0 in main) or
change the function signature to accept and use a known current length instead
of calling strlen on caller-provided memory.

Comment on lines 129 to 133
if (node_p == NULL) {
node_p = (struct tree_node *)malloc(sizeof(struct tree_node));
node_p->line_numbers = add_to_list(node_p->line_numbers, line_number);
node_p->word = str_dup(word);
node_p->left = node_p->right = NULL;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use of uninitialized node_p->line_numbers and missing NULL check after malloc.

On line 131, node_p->line_numbers is passed to add_to_list before being initialized. Since malloc doesn't zero memory, this passes garbage to add_to_list, which then checks if (list_node_p == NULL). This is undefined behavior.

Additionally, if malloc returns NULL, the subsequent dereferences on lines 131-133 will crash.

Initialize line_numbers to NULL and add a null check:

     if (node_p == NULL) {
         node_p = (struct tree_node *)malloc(sizeof(struct tree_node));
+        if (node_p == NULL) {
+            return NULL;
+        }
+        node_p->line_numbers = NULL;
         node_p->line_numbers = add_to_list(node_p->line_numbers, line_number);
         node_p->word = str_dup(word);
         node_p->left = node_p->right = NULL;
🧰 Tools
🪛 Cppcheck (2.18.0)

[warning] 131-131: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 131-131: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 132-132: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 133-133: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 133-133: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🤖 Prompt for AI Agents
In chapter_6/exercise_6_03/cross_referencer.c around lines 129 to 133, malloc'd
node_p is not NULL-checked and node_p->line_numbers is used uninitialized;
change the allocation handling to check if malloc returned NULL and handle the
error (return, print and exit, or propagate) and explicitly initialize
node_p->line_numbers = NULL before calling add_to_list so add_to_list receives a
valid NULL list pointer; also continue to set node_p->word, left and right after
successful allocation.

Comment on lines 114 to 132
void quick_sort(void *v[], size_t start, size_t end,
int (*comp)(void *, void *)) {
if ((long)start >= (long)end) {
return;
}

swap(v, start, (start + end) / 2);

size_t last = start;
for (size_t i = start + 1; i <= end; ++i) {
if ((*comp)(v[i], v[start]) < 0) {
swap(v, ++last, i);
}
}
}

swap(v, start, last);
quick_sort(v, start, last - 1, comp);
quick_sort(v, last + 1, end, comp);
swap(v, start, last);
quick_sort(v, start, last - 1, comp);
quick_sort(v, last + 1, end, comp);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check current version of the function
echo "=== Current function (quick_sort) ==="
grep -n "void quick_sort" chapter_6/exercise_6_04/words_frequency.c

# Show the full current function
sed -n '114,132p' chapter_6/exercise_6_04/words_frequency.c

echo ""
echo "=== Checking git history ==="
# Check if the file was in previous commit
git log --oneline chapter_6/exercise_6_04/words_frequency.c | head -5

echo ""
echo "=== Previous version of quick_sort ==="
# Show the function from the previous commit
git show HEAD~1:chapter_6/exercise_6_04/words_frequency.c 2>/dev/null | sed -n '/void quick_sort/,/^}/p' || echo "Previous version not available"

echo ""
echo "=== Analyzing the potential underflow path ==="
# Show lines around the recursive call
sed -n '120,132p' chapter_6/exercise_6_04/words_frequency.c | cat -n

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 1283


🏁 Script executed:

#!/bin/bash

echo "=== Checking if there are multiple quick_sort implementations ==="
grep -n "void quick_sort" chapter_6/exercise_6_04/words_frequency.c

echo ""
echo "=== First quick_sort implementation (around line 29) ==="
sed -n '29,50p' chapter_6/exercise_6_04/words_frequency.c

echo ""
echo "=== Checking full git log ==="
git log --oneline | head -10

echo ""
echo "=== Analyzing underflow path logic ==="
# The issue occurs when: last == start (no swaps happened in loop)
# Then last - 1 where start is 0 -> underflows to SIZE_MAX
# Check if this scenario is possible

cat << 'EOF'
Logic analysis:
- last starts at: start
- last can only increase or stay same (incremented in swap condition)
- If no elements < v[start], last stays equal to start
- If start == 0, then last - 1 underflows to SIZE_MAX
- The cast to (long) in the guard would then convert SIZE_MAX to -1
- Guard check (long)0 >= (long)SIZE_MAX becomes 0 >= -1, which is TRUE
- So recursion stops, but this is UB-dependent behavior
EOF

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 1501


🏁 Script executed:

#!/bin/bash

echo "=== Checking the initial call to quick_sort ==="
sed -n '40,50p' chapter_6/exercise_6_04/words_frequency.c

echo ""
echo "=== Check how nr_of_nodes is used ==="
grep -n "nr_of_nodes" chapter_6/exercise_6_04/words_frequency.c | head -10

echo ""
echo "=== Verify: Can start == 0 and last == start after partitioning? ==="
cat << 'EOF'
Scenario verification:
1. Initial call: quick_sort(tree_node_list, 0, nr_of_nodes - 1, comp)
   - start = 0, end = nr_of_nodes - 1
   
2. If nr_of_nodes == 1:
   - Guard check: (long)0 >= (long)0 returns TRUE, so function returns early (SAFE)
   
3. If nr_of_nodes == 2:
   - start = 0, end = 1
   - Guard: (long)0 >= (long)1 is FALSE, continue
   - swap(v, 0, (0+1)/2 = 0)  -> no change
   - Loop: i from 1 to 1
     - If v[1] >= v[0], last stays 0
   - swap(v, 0, 0) -> no change
   - Recursive call 1: quick_sort(v, 0, 0-1, comp)
     - start = 0, end = SIZE_MAX (UNDERFLOW!)
     - Guard: (long)0 >= (long)SIZE_MAX
     - SIZE_MAX as signed long = -1 (2's complement)
     - Result: 0 >= -1 is TRUE, so returns (doesn't crash but UB)
     
This scenario IS reachable in practice!
EOF

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 1630


🏁 Script executed:

#!/bin/bash

echo "=== Additional verification: initial call with nr_of_nodes == 0 ==="
cat << 'EOF'
Initial call: quick_sort(tree_node_list, 0, nr_of_nodes - 1, comp)

If nr_of_nodes == 0:
- start = 0, end = SIZE_MAX (0 - 1 with size_t underflow!)
- Guard check: (long)0 >= (long)SIZE_MAX
- SIZE_MAX as signed long = -1
- Result: 0 >= -1 is TRUE, returns early (masked by guard)

This shows the guard check itself relies on undefined behavior to prevent crashes.
EOF

echo ""
echo "=== Conclusion ==="
cat << 'EOF'
VERIFIED: The size_t underflow bug is real and reachable.

1. Underflow occurs at line 130: quick_sort(v, start, last - 1, comp)
2. Scenario: When last == start (no partitioning happened)
3. If start == 0, then last - 1 underflows to SIZE_MAX
4. The guard check masks the bug by relying on undefined behavior
5. This is reachable in practice (e.g., 2-element arrays, or initial call with empty array)

The bug should be fixed by checking last > 0 before the recursive call, or by using signed integers consistently.
EOF

Repository: ohkimur/the-c-programming-language-2nd-edition-solutions

Length of output: 1066


Fix size_t underflow in recursive call at line 130.

The expression last - 1 where last is size_t underflows to SIZE_MAX when last == start (which occurs when no elements are less than the pivot). This is reachable in practice with 2-element arrays or when the initial call has an empty array. While the guard check masks the crash through undefined behavior, it relies on implementation-specific casting rather than proper bounds checking.

Fix: Add a bounds check before the recursive call:

     swap(v, start, last);
-    quick_sort(v, start, last - 1, comp);
+    if (last > 0) {
+        quick_sort(v, start, last - 1, comp);
+    }
     quick_sort(v, last + 1, end, comp);
🤖 Prompt for AI Agents
In chapter_6/exercise_6_04/words_frequency.c around lines 114 to 132, the
recursive call quick_sort(v, start, last - 1, comp) can underflow because last
is size_t and may equal start; change the logic to perform an explicit bounds
check (e.g., only call quick_sort on the left partition if last > start) instead
of relying on signed casts, and keep the existing call for the right partition
as-is; also ensure the initial guard uses proper start >= end check with
size_t-safe comparisons.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
chapter_5/exercise_5_10/expr.c (4)

8-8: Consider using size_t for stack_pointer.

While unsigned char works for the current STACK_SIZE of 15, using size_t is more idiomatic for array indices and provides better portability and future extensibility.

Apply this diff to use size_t:

-unsigned char stack_pointer = 0; // points to next free slot
+size_t stack_pointer = 0; // points to next free slot

You'll also need to update the comparison on line 26:

-            if (stack_pointer >= 2 && stack_pointer < STACK_SIZE) {
+            if (stack_pointer >= 2) {

The upper bound check is redundant since push already guards against overflow.


21-24: Zero-value parsing improved but still has edge cases.

The current fix (line 23) is better than the previous version which only checked strcmp(argv[i], "0"). However, it still accepts malformed inputs like "0abc" as valid zero values because atof returns 0.0 and the first character '0' is in the valid set.

Consider using strtof with end-pointer validation for robust parsing:

-        float number = atof(argv[i]);
+        char *endptr;
+        float number = strtof(argv[i], &endptr);
 
-        if (number != 0.0 || strchr("-+.0123456789", argv[i][0]) != NULL) {
+        // Valid number if conversion consumed the entire string
+        if (endptr != argv[i] && *endptr == '\0') {
             push(number);

26-26: Remove redundant upper bound check.

The condition stack_pointer < STACK_SIZE is redundant. If stack_pointer >= 2, we already know we're below STACK_SIZE since valid values are 0 to STACK_SIZE. The lower bound check alone is sufficient to ensure two operands are available for the operation.

-            if (stack_pointer >= 2 && stack_pointer < STACK_SIZE) {
+            if (stack_pointer >= 2) {

106-117: Stack pop logic is correct!

The pre-decrement approach correctly implements the "next free slot" semantics and fixes the off-by-one bug. The explanatory comment is helpful.

Note: Returning 0 on error is ambiguous when 0.0 is a valid stack value. Consider returning a status code via a return value and using an out-parameter for the popped value, though this would be a larger refactor.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34ed7ba and 6808f61.

📒 Files selected for processing (2)
  • chapter_5/exercise_5_10/expr.c (1 hunks)
  • chapter_5/exercise_5_10/expr_test.sh (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
chapter_5/exercise_5_10/expr.c (1)
chapter_7/exercise_7_05/calculator.c (2)
  • pop (77-84)
  • push (69-75)
🪛 Shellcheck (0.11.0)
chapter_5/exercise_5_10/expr_test.sh

[warning] 79-79: 'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|.

(SC2307)

🔇 Additional comments (8)
chapter_5/exercise_5_10/expr.c (2)

1-7: LGTM!

The includes are appropriate for the functionality used, and the stack size is reasonable for a calculator exercise.


119-128: Stack push logic is correct!

The post-increment approach correctly implements the "next free slot" semantics and fixes the off-by-one bug. The comment clearly documents the behavior.

chapter_5/exercise_5_10/expr_test.sh (6)

1-12: LGTM! File existence checks are correct.

The error message on line 5 is now correct (previously flagged in past review as inverted). Both presence and executability checks are properly implemented.


16-34: LGTM! Basic addition tests are correct.

Both tests properly validate addition and commutativity using RPN notation, with expected output matching the format from expr.c.


36-44: LGTM! Subtraction test is correct.

The test correctly validates RPN subtraction: 10 3 - evaluates to 10 - 3 = 7.


46-66: LGTM! Excellent complex expression test with detailed documentation.

The step-by-step stack evolution comments (lines 48-59) make it easy to verify the correctness of the complex RPN expression. The proper escaping of * is noted on line 60.


68-75: LGTM! Division by zero test is correct.

The test properly validates that division by zero is detected and reported, using substring matching to allow flexibility in the exact error message format.


77-85: Test correctly validates missing operator error message.

The test properly checks that two operands without an operator trigger the appropriate error message.

Note on shellcheck warning: The SC2307 warning is a false positive. Shellcheck assumes this is the shell built-in expr command, but the test is actually invoking the custom ./expr executable which has different argument requirements.

Consider verifying exit codes: The tests currently only check output messages but don't verify exit codes. Since error cases should return EXIT_FAILURE, consider adding exit code checks:

# Test 6: Missing operator
T=6
./expr 5 1
exit_code=$?
result=$(./expr 5 1)
expected="Error: Operator is missing!"
if [[ "$result" == "$expected" && $exit_code -ne 0 ]]; then
    echo "Test $T PASS: Missing operator"
else
    echo "Test $T FAIL: Expected '$expected' with non-zero exit, got '$result' with exit code $exit_code"
fi

However, this would require fixing the exit code issue in expr.c first (see review comment on lines 97-103).

Comment on lines 97 to 103
if (op_count == 0 && argc > 2) {
printf("Error: Operator is missing!");
} else {
printf("result: %.3f", pop());
}

return EXIT_SUCCESS;
return EXIT_SUCCESS;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix inconsistent error handling for missing operator.

The missing operator check (line 97-99) prints an error but still returns EXIT_SUCCESS, which is inconsistent with other error handling. Additionally:

  1. The error message lacks a trailing newline.
  2. If multiple operands are pushed without operators, pop() only retrieves the top value, leaving others on the stack.
     if (op_count == 0 && argc > 2) {
-        printf("Error: Operator is missing!");
+        printf("Error: Operator is missing!\n");
+        return EXIT_FAILURE;
     } else {
         printf("result: %.3f", pop());
     }
 
     return EXIT_SUCCESS;

Alternatively, validate that exactly one value remains on the stack:

     if (op_count == 0 && argc > 2) {
-        printf("Error: Operator is missing!");
-    } else {
-        printf("result: %.3f", pop());
+        printf("Error: Operator is missing!\n");
+        return EXIT_FAILURE;
+    }
+
+    if (stack_pointer != 1) {
+        printf("Error: invalid expression (stack has %u values).\n", stack_pointer);
+        return EXIT_FAILURE;
     }
 
+    printf("result: %.3f\n", pop());
     return EXIT_SUCCESS;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (op_count == 0 && argc > 2) {
printf("Error: Operator is missing!");
} else {
printf("result: %.3f", pop());
}
return EXIT_SUCCESS;
return EXIT_SUCCESS;
if (op_count == 0 && argc > 2) {
printf("Error: Operator is missing!\n");
return EXIT_FAILURE;
} else {
printf("result: %.3f", pop());
}
return EXIT_SUCCESS;
🤖 Prompt for AI Agents
In chapter_5/exercise_5_10/expr.c around lines 97 to 103, the missing-operator
branch currently only prints an error without newline and returns EXIT_SUCCESS
while still calling pop(), and it doesn't detect extra values left on the stack;
change the logic to validate that exactly one value remains on the stack after
processing (e.g., check stack_count == 1 or equivalent), if not print a clear
error message ending with a newline and return EXIT_FAILURE without calling
pop(), otherwise pop the single result, print it with a trailing newline and
return EXIT_SUCCESS.

@JebediahS JebediahS requested a review from ohkimur December 17, 2025 16:14
@ohkimur
Copy link
Owner

ohkimur commented Dec 17, 2025

@JebediahS Great to see you contributing to this PR! Before me taking a look, can you first resolve the merge conflicts and address all issues found by @coderabbitai?

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Fetch PR details including merge status
echo "=== PR #91 Details ==="
gh pr view 91 --json title,state,mergeable,mergeStateStatus,additions,deletions,changedFiles

echo -e "\n=== PR #91 Files Changed ==="
gh pr view 91 --json files --jq '.files[] | "\(.path) (+\(.additions)/-\(.deletions))"'

echo -e "\n=== Review Comments by coderabbitai ==="
gh pr view 91 --json reviews --jq '.reviews[] | select(.author.login == "coderabbitai") | {state: .state, body: .body, submittedAt: .submittedAt}'

echo -e "\n=== Review Comments (detailed) ==="
gh api repos/ohkimur/the-c-programming-language-2nd-edition-solutions/pulls/91/comments --jq '.[] | select(.user.login == "coderabbitai") | {path: .path, line: .line, body: .body}'

echo -e "\n=== Checking mergeable status ==="
gh pr view 91 --json mergeable,mergeStateStatus --jq '{mergeable: .mergeable, mergeStateStatus: .mergeStateStatus}'

Length of output: 80244


@ohkimur, I've gathered the details about the merge conflicts and review issues. Here's a summary to help @JebediahS understand what needs to be addressed:

🔴 Merge Conflicts

The PR currently has merge conflicts that must be resolved first:

  • Status: CONFLICTING
  • Files affected: The PR touches 120 files with extensive changes (+6968/-8421 lines)

You'll need to:

git fetch upstream
git merge upstream/main
# Resolve conflicts
git commit
git push

🔍 CodeRabbit Review Issues

CodeRabbit found several categories of issues across the codebase. Here are the key issues specific to expr.c (the main file in this PR):

Critical Issues in expr.c:

  1. Empty argument handling (Lines 97-101):

    • Missing proper error handling when no arguments provided
    • Missing newline in error message on line 98
    • Should return EXIT_FAILURE after errors instead of continuing
  2. Redundant stack check (Line 26):

    • The condition stack_pointer < STACK_SIZE is unnecessary for binary operations
    • Binary ops reduce stack size, so they're safe even when stack is full
  3. Number parsing issue (Lines 21-24):

    • Current code accepts malformed inputs like "0abc" as valid zero
    • Consider using strtof with end-pointer validation for robust parsing

Suggestions for expr.c:

  1. Consider using size_t for stack_pointer (Line 8):

    • More idiomatic for array indices than unsigned char
  2. Consider status-based returns for push/pop:

    • Current approach returns 0.0 on error, which is ambiguous since 0.0 is valid
    • Suggested by JebediahS in earlier comments
  3. Add tests:

    • The test file expr_test.sh is included, which is great!
    • Consider adding exit code verification in tests

Other Notable Issues Across the Codebase:

Since this PR includes formatting changes across 120 files, CodeRabbit found issues in many other files:

  • Buffer overflow vulnerabilities (chapter_1/exercise_1_18, chapter_6/exercise_6_01, etc.)
  • Memory leaks (chapter_6/exercise_6_05, chapter_6/exercise_6_06)
  • Undefined behavior (chapter_4/exercise_4_06, chapter_4/exercise_4_05)
  • Off-by-one errors in multiple files

📋 Recommended Next Steps for @JebediahS:

  1. First: Resolve the merge conflicts
  2. Second: Address the critical issues in expr.c listed above
  3. Third: Review and address the other coderabbitai comments as time permits
  4. Consider: Splitting this PR into two:
    • One PR for the expr.c stack fix + tests
    • A separate PR for the code formatting/linting changes across all files

This would make the review process more manageable and reduce the risk of merge conflicts.

Would you like me to provide specific code fixes for any of the expr.c issues mentioned above?

Filling the stack should start from element with index 0.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
chapter_5/exercise_5_10/expr.c (1)

91-93: Add newline and validate stack state before printing result.

Two issues:

  1. The result output lacks a trailing newline, inconsistent with error messages.
  2. No validation that exactly one value remains on the stack. If multiple values remain (e.g., ./expr 5 1 2 without an operator), only the top value is printed, silently ignoring the incomplete expression.
Apply this diff to add validation and fix formatting:
+    // Validate exactly one result remains
+    if (stack_pointer == 0) {
+        printf("Error: no result (empty stack).\n");
+        return EXIT_FAILURE;
+    }
+    if (stack_pointer > 1) {
+        printf("Error: incomplete expression (%u values remain on stack).\n", stack_pointer);
+        return EXIT_FAILURE;
+    }
+
-    printf("result: %.3f", pop());
+    printf("result: %.3f\n", pop());
 
     return EXIT_SUCCESS;
♻️ Duplicate comments (1)
chapter_5/exercise_5_10/expr_test.sh (1)

4-7: Fix inverted error message.

The condition [ ! -f expr ] is true when the file does not exist, but line 5 prints "File 'expr' found!" which is the opposite. This contradicts the past review comment marked as "addressed."

Apply this diff:
 if [ ! -f expr ]; then
-    echo "File 'expr' found!"
+    echo "File 'expr' not found!"
     exit 1
 fi
🧹 Nitpick comments (4)
chapter_5/exercise_5_10/expr.c (4)

98-98: Stack indexing fix looks correct; consider documenting the invariant.

The pre-decrement in pop and post-increment in push correctly implement 0-based indexing with stack_pointer pointing to the next free slot. This resolves the off-by-one bug described in the PR.

Consider adding a brief comment documenting the stack pointer invariant:
+// Invariant: stack_pointer always points to the next free slot (0-based indexing)
 unsigned char stack_pointer = 0;
 float stack[STACK_SIZE];

Also applies to: 107-107


20-23: Consider using strtof for robust numeric parsing.

The current approach with atof and strcmp(argv[i], "0") == 0 has limitations:

  • atof("0.0") returns 0, failing the if (number ...) check, then falls back to the strcmp which only matches "0" exactly, causing valid inputs like "0.0" or "-0" to be rejected.
  • atof accepts malformed input like "123abc", silently truncating to 123.

Using strtof with end-pointer validation ensures only well-formed numeric strings are accepted.

Apply this diff to use strtof with proper validation:
-        float number = atof(argv[i]);
+        char *endptr;
+        float number = strtof(argv[i], &endptr);
 
-        if (number || strcmp(argv[i], "0") == 0) {
+        // Valid if conversion consumed chars and reached end (or whitespace)
+        if (endptr != argv[i] && (*endptr == '\0' || isspace(*endptr))) {
             push(number);

8-8: Consider using size_t for stack_pointer.

Using unsigned char limits the stack pointer to 255, which is sufficient for STACK_SIZE of 15 but not idiomatic. size_t is the standard type for array indices and sizes, improving clarity and consistency with comparisons against STACK_SIZE.

Apply this diff:
-unsigned char stack_pointer = 0;
+size_t stack_pointer = 0;

You'll also need to update format specifiers in error messages from %u to %zu if you add any that print stack_pointer.


96-103: Returning 0 from pop on error is ambiguous.

If the stack is empty, pop() returns 0, which is indistinguishable from a valid 0.0 value popped from the stack. This makes error detection unreliable for callers.

Consider returning status via an out-parameter or using a dedicated error-signaling mechanism (e.g., return bool/int success status with value via pointer). However, given the current usage in main (line 91) where errors are already handled before calling pop, and the fact that this is K&R-style educational code, this refactor is optional.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6808f61 and 218cece.

📒 Files selected for processing (2)
  • chapter_5/exercise_5_10/expr.c (2 hunks)
  • chapter_5/exercise_5_10/expr_test.sh (1 hunks)
🧰 Additional context used
🪛 Shellcheck (0.11.0)
chapter_5/exercise_5_10/expr_test.sh

[warning] 79-79: 'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|.

(SC2307)

🔇 Additional comments (2)
chapter_5/exercise_5_10/expr_test.sh (2)

77-85: Shellcheck warning is a false positive.

The static analysis warning on line 79 ('expr' expects 3+ arguments, but sees 2) is intentional—this test deliberately provides insufficient arguments to verify the missing-operator error case.

This is not an issue; the warning can be safely ignored or suppressed with a shellcheck directive if desired:

# shellcheck disable=SC2307
result=$(./expr 5 1)

14-87: Good test coverage.

The test suite covers key scenarios effectively:

  • Basic arithmetic operations (addition, subtraction)
  • Commutativity verification
  • Complex RPN expression with helpful inline trace
  • Edge case error handling (division by zero, missing operator)

The inline step-by-step comments for Test 4 (lines 48-59) are particularly helpful for understanding the expected evaluation order.

Comment on lines +79 to +85
result=$(./expr 5 1)
expected="Error: Operator is missing!"
if [[ "$result" == "$expected" ]]; then
echo "Test $T PASS: Missing operator"
else
echo "Test $T FAIL: Expected '$expected', got '$result'"
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test 6 expected output is inconsistent with error message formatting.

The test expects "Error: Operator is missing!" without a trailing newline. While this matches the current behavior of expr.c (line 91), it's inconsistent with:

  • Other error messages in expr.c which include \n
  • The general convention that command-line tools print newlines after output

This is a minor cosmetic issue, but fixing it in both expr.c and this test would improve consistency.

Related: The suggested fix in the review comment for expr.c lines 91-93 addresses this by adding proper validation and a newline to the result output.

🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 79-79: 'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|.

(SC2307)

🤖 Prompt for AI Agents
In chapter_5/exercise_5_10/expr_test.sh around lines 79 to 85, the test expects
the error string "Error: Operator is missing!" without a trailing newline which
is inconsistent with other error messages and the intended fix in expr.c; update
the test to expect a newline-terminated string (e.g., "Error: Operator is
missing!\n") or use a comparison that ignores trailing newlines (for example,
compare trimmed output) so the test matches the corrected expr.c behavior that
prints the error message followed by a newline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants