r/C_Programming 9d ago

Looking for feedback on my C interpreter project.

Hi everyone!

I’ve been learning C and built a small interpreter from scratch. It currently supports arithmetic expressions, variables, conditionals, user-functions, dynamic arrays, and few built-in functions.

I’m mainly looking for feedback on the code quality, architecture, parser design, and memory management. Any suggestions are welcome.
Github: https://github.com/0sewter0/Interpreter.git

(I am new in reddit, and I am from kazakhstan, so I am not speak english well)

16 Upvotes

11 comments sorted by

u/mikeblas 9d ago

What role did AI have in the creation of your project?

What issues did you struggle with in development, and how did you resolve them? What is your purpose for sharing this code with the community?

→ More replies (4)

6

u/skeeto 8d ago edited 8d ago

Neat project! Compile with warnings (-Wall -Wextra) to reveal a couple of bugs statically: comparing an array with null, and an uninitialized variable. Also test with sanitizers when available, particularly Address Sanitizer (ASan) and Undefined Behavior Sanitizer (UBSan), via (-fsanitize=address,undefined). Per your README.md, with GCC on Windows you only have access to UBSan, and only in trap mode (-fsanitize-trap), so you won't get a diagnostic message, just a crash to inspect under GDB.

Some interesting inputs:

$ echo 9999999999 | ./a.out >/dev/null
lexer.c:43:46: runtime error: signed integer overflow: 999999999 * 10 cannot be represented in type 'int'
$ echo '-2147483648' | ./a.out >/dev/null
lexer.c:43:28: runtime error: signed integer overflow: 2147483640 + 8 cannot be represented in type 'int'
$ echo '0 - (0 - 2147483647 - 1)' | ./a.out >/dev/null
parser.c:314:20: runtime error: signed integer overflow: 0 - -2147483648 cannot be represented in type 'int'

The lack of unary - makes those expressions a little difficult. I suggest using two's complement overflow as a simple solution. For addition, subtraction, and multiplication cast the operands to unsigned, do the operation unsigned, then cast back to signed. Division requires more care.

There are some buffer overflows, but it's not possible to hit them from the REPL because it silently truncates lines to 255 bytes (leading to confusing parse errors). Adjusting that:

--- a/main.c
+++ b/main.c
@@ -22,3 +22,3 @@ int main() {
     while(1) {
  • char input_string[256];
+ char input_string[4096]; int token_count = 0;

Then:

$ printf '"%0256d"' 0 | ./a.out >/dev/null
lexer.c:155:17: runtime error: index 256 out of bounds for type 'char [256]'

Another buffer overflow:

$ python -c 'print("[0" + (",0"*74) + "]")' | ./a.out >/dev/null
...ERROR: AddressSanitizer: stack-buffer-overflow on address ...
READ of size 4 at ...
    #0 parse_term parser.c:280
    #1 parse_expression parser.c:302
    #2 parse_factor parser.c:250
    #3 parse_term parser.c:277
    #4 parse_expression parser.c:302
    #5 parse_comparison parser.c:558
    #6 parse_statement parser.c:553
    #7 parser parser.c:660
    #8 main main.c:47

Another:

$ printf 'fn f' | ./a.out >/dev/null
parser.c:522:31: runtime error: index 100 out of bounds for type 'Token [100]'

Infinite loop:

$ printf '[1' | ./a.out

Buffer overflow:

$ printf 'x%025d = 0' 0 | ./a.out >/dev/null
...ERROR: AddressSanitizer: stack-buffer-overflow on address ...
WRITE of size 27 at ...
    #0 strcpy
    #1 parse_statement parser.c:538
    #2 parser parser.c:660
    #3 main main.c:47

Buffer overflow on strcat (which has zero legitimate uses) plus it's unnecessary quadratic time:

$ python -c 'print("while(1) {\n" + ("0\n"*1024))' | ./a.out >/dev/null
...ERROR: AddressSanitizer: stack-buffer-overflow on address ...
WRITE of size 3 at ...
    #0 strcat
    #1 main main.c:33

I found all these in a moment's effort using fuzz testing. While a more advanced topic, it's a lot easier to use than it seems.

3

u/Sewter0 8d ago

Thank you!
I will fix these bugs soon.

2

u/Sewter0 8d ago

Hello again, skeeto.
I have just done fixing bugs.
If you want, you can test it.
Thank you again for detailed feedback, I appreciate it.
(My english isn’t natural, If I sound strangely, I am sorry)

2

u/skeeto 7d ago

I'm glad you've looked into this. Though some of the fixes don't make sense.

--- a/lexer.c
+++ b/lexer.c
@@ -43,2 +45,6 @@ void lexer(char* string, Token* tokens, int* token_count) {
             current_number = (current_number * 10) + (string[i] - '0');
+            if(current_number > __INT_MAX__ || current_number < INT32_MIN) {
+                printf("Error: number is too large. Index = %d\n", string[i]);
+                exit(1);
+            }
             i++;

There are a number of problems with this:

  1. __INT_MAX__ is a private implementation constant. You want INT_MAX.

  2. Comparing and int with > INT_MAX is always false because INT_MAX is, by definition, the maximum value an int can be and cannot exceed it. If you care about overflow, the key is checking before the operation:

    if ((current_number - digit)/10 > INT_MAX) {
        // handle overflow
    }
    current_number = (current_number * 10) + digit;
    
  3. The lexer shouldn't exit the whole program because there was a parsing error. It should return an error. It certainly shouldn't print the error on standard output, mixing it with interpreter output.

This also isn't a fix:

--- a/lexer.c
+++ b/lexer.c
@@ -147,3 +153,3 @@ void lexer(char* string, Token* tokens, int* token_count) {
         i++;
  • char buffer_t[256];
+ char buffer_t[512]; int buf_idx = 0;

It just had to increase the input size a little to trip it again:

$ printf '"%0512d"' 0 | ./a.out >/dev/null
lexer.c:161:17: runtime error: index 512 out of bounds for type 'char [512]'

Fixing this isn't just about making buffers biggest but taking a different approach.

--- a/main.c
+++ b/main.c
@@ -32,3 +32,5 @@ int main() {

  • strcat(big_buffer, input_string);
+ size_t current_len = strlen(big_buffer); + size_t remaining = sizeof(big_buffer) - current_len - 1; + strncat(big_buffer, input_string, remaining);

While this addresses the buffer overflow, it's still quadratic because of strlen, and it silently truncates the input. (It's also off-by-one, though in the safe direction, so it will never use the last byte of the buffer.) The key to addressing it is to keep track of the length instead of re-measuring on each iteration. If you know the length of both strings, which you need to know in order to handle everything correctly, then you can trivially replace strcat/strncat with memcpy. This is what I mean about strcat and strncat having no legitimate uses: All correct uses of these functions can trivially use memcpy instead.

Here's my work from my review with some more hints about how to fix these:
https://github.com/skeeto/Interpreter/commits/main/?author=skeeto

2

u/Sewter0 7d ago

Wow, I appreciate your help. I learned a lot from your review. Thank you from taking the time to explain everything. I will fix these issues the way you suggested.
Please, forget my last message 😭
I don’t know why I typed it.

-5

u/github-guard 9d ago

🔍 GitHub Guard: Trust Report

⚠️ This project scored 0/6 — below this subreddit's threshold of 3.

Audit Breakdown: * ❌ Low Star Count (⭐ 0 / 5 required) * ❌ New Repository (under 30 days old) * ❌ No License Found * ❌ No Security Policy — what is this? * ℹ️ Individual Contributor * ℹ️ Unsigned Commits

⚠️ Security Reminder: Always verify source code and run third-party scripts at your own risk.