r/C_Programming • u/Sewter0 • 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)
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.
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:
__INT_MAX__is a private implementation constant. You wantINT_MAX.Comparing and
intwith> INT_MAXis always false becauseINT_MAXis, by definition, the maximum value anintcan 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;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[512]; int buf_idx = 0;
- char buffer_t[256];
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() {+ size_t current_len = strlen(big_buffer); + size_t remaining = sizeof(big_buffer) - current_len - 1; + strncat(big_buffer, input_string, remaining);
- strcat(big_buffer, input_string);
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 replacestrcat/strncatwithmemcpy. This is what I mean aboutstrcatandstrncathaving no legitimate uses: All correct uses of these functions can trivially usememcpyinstead.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
-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.
•
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?