Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,15 @@ jobs:
- name: Build Brainrot
run: |
make
make brainrot-valgrind

- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: brainrot
path: |
brainrot
brainrot-valgrind
libstdrot.so

test:
Expand All @@ -98,7 +100,7 @@ jobs:
path: .

- name: Grant execute permissions to Brainrot
run: chmod +x brainrot
run: chmod +x brainrot brainrot-valgrind

- name: Prepare shared library for tests
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.vscode
brainrot
brainrot-valgrind
lang.tab.c
lang.tab.h
lex.yy.c
Expand Down
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ make test

### Running Memory Leak Tests

Make `run_valgrind_tests.sh` executable:
Memory leak tests are run using Valgrind against the non-sanitized binary (`brainrot-valgrind`):

```bash
sudo chmod +x run_valgrind_tests.sh
make valgrind
```

Run:
Alternatively, make `run_valgrind_tests.sh` executable and run it directly (defaults to `./brainrot-valgrind`):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This documents the broken path as a supported workflow. After make, ./brainrot-valgrind does not exist, and run_valgrind_tests.sh will still "pass" because it only treats exit code 100 as failure.

Either delete this alternative or make the script refuse to run without an executable target. Do not tell people to invoke a checker that fail-opens.


```bash
chmod +x run_valgrind_tests.sh
./run_valgrind_tests.sh
```

Expand Down
16 changes: 12 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ PYTHON := python3
EMCC := emcc

# Compiler and linker flags
CFLAGS := -Wall -Wextra -Wpedantic -Werror -O2 -Wuninitialized -fsanitize=address,undefined -fno-omit-frame-pointer -g
SANITIZER_FLAGS := -fsanitize=address,undefined
CFLAGS := -Wall -Wextra -Wpedantic -Werror -O2 -Wuninitialized $(SANITIZER_FLAGS) -fno-omit-frame-pointer -g
VALGRIND_CFLAGS := $(filter-out $(SANITIZER_FLAGS),$(CFLAGS))
LDFLAGS := -lfl -lm -ldl -rdynamic
SO_CFLAGS := -fPIC -shared

Expand All @@ -24,6 +26,7 @@ STDROT_LIB := libstdrot.so

# Output files
TARGET := brainrot
VALGRIND_TARGET := brainrot-valgrind

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should-fix: .gitignore only lists brainrot, so this binary is untracked. Add brainrot-valgrind next to it.

BISON_OUTPUT := lang.tab.c
FLEX_OUTPUT := lex.yy.c

Expand Down Expand Up @@ -91,6 +94,11 @@ $(TARGET): $(ALL_SRCS) $(STDROT_LIB)
$(CC) $(CFLAGS) -o $@ $(ALL_SRCS) $(LDFLAGS)
@echo "Skibidi toilet: $(TARGET) compiled with max gyatt."

# Valgrind executable build (built without ASan/UBSan for Valgrind compatibility)
$(VALGRIND_TARGET): $(ALL_SRCS) $(STDROT_LIB)
$(CC) $(VALGRIND_CFLAGS) -o $@ $(ALL_SRCS) $(LDFLAGS)
@echo "brainrot-valgrind compiled without sanitizers."

# WebAssembly build: stdrot sources go straight into the same binary
# instead of a separate $(STDROT_LIB), so this does NOT depend on it.
.PHONY: wasm
Expand Down Expand Up @@ -118,15 +126,15 @@ test: ensure-stdrot $(TARGET)
# Clean build artifacts
.PHONY: clean
clean:
rm -f $(TARGET) $(STDROT_LIB) $(GENERATED_SRCS) lang.tab.h
rm -f $(TARGET) $(VALGRIND_TARGET) $(STDROT_LIB) $(GENERATED_SRCS) lang.tab.h
rm -f $(WASM_TARGET) $(WASM_JS)
rm -f *.o
@echo "Blud cleaned up the mess like a true sigma coder."

# Run Valgrind on all .brainrot tests
.PHONY: valgrind
valgrind: ensure-stdrot $(TARGET)
@./run_valgrind_tests.sh
valgrind: ensure-stdrot $(VALGRIND_TARGET)
@./run_valgrind_tests.sh ./$(VALGRIND_TARGET)
@echo "Valgrind check done. If anything was sus, it'll show up with a non-zero exit code. No cap."

# Install target
Expand Down
15 changes: 8 additions & 7 deletions lang.y
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ static Interpreter *global_interpreter = NULL;
EnumConstant *econst;
}

/* Free heap-allocated strings when bison discards symbols during parse error recovery */
%destructor { SAFE_FREE($$.data); } <strval>
%destructor { SAFE_FREE($$.name.data); } <declarator>
Comment thread
leo-aa88 marked this conversation as resolved.

/* Define token types */
%token SKIBIDI RIZZ YAP BAKA MAIN BUSSIN FLEX CAP RANT
%token PLUS MINUS TIMES DIVIDE MOD SEMICOLON COLON COMMA
Expand Down Expand Up @@ -242,13 +246,10 @@ struct_field
(sizeof(uintptr_t)) doesn't depend on the pointee being
complete yet.

Note: we deliberately don't YYABORT here. Aborting mid-way
through the still-open outer struct_def rule would strand
that rule's own pending IDENTIFIER token on the parser value
stack un-freed (this grammar has no %destructor entries), so
instead we record the error and let parsing finish normally;
main() checks struct_def_had_error after yyparse() and exits
the same way it does for a hard parse failure. */
Note: we deliberately don't YYABORT here. Finishing the outer
struct_def rule keeps struct_def_had_error and the partially
computed layout coherent; main() checks the flag after
yyparse() and exits as it does for a hard parse failure. */
bool is_self = current_struct_def_name.data &&
strcmp($2.data, current_struct_def_name.data) == 0;
if (is_self && $3.pointer_level == 0)
Expand Down
50 changes: 36 additions & 14 deletions run_valgrind_tests.sh
Original file line number Diff line number Diff line change
@@ -1,32 +1,54 @@
#!/bin/bash

TARGET="${1:-./brainrot-valgrind}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MAJOR. Default is a binary make does not build. Only make valgrind / make brainrot-valgrind produces it.

CONTRIBUTING now documents ./run_valgrind_tests.sh with no args as equivalent to make valgrind. After a normal make, that path hits a missing file. Combined with the -eq 100 check below, the script still exits 0.

make valgrind passes the path explicitly, so that target is fine. CI is green only because the build job happens to upload the artifact first. That is job ordering, not a script invariant.

Fail closed here: [[ -x "$TARGET" ]] and command -v valgrind, or this default is a silent-success footgun.


if ! command -v valgrind >/dev/null 2>&1; then
echo "Error: valgrind is not installed or not in PATH" >&2
exit 1
fi

if [[ ! -x "$TARGET" ]]; then
echo "Error: Valgrind target '$TARGET' is not executable" >&2
exit 1
fi

for f in test_cases/*.brainrot; do
echo "Running Valgrind on $f..."
base=$(basename "$f" .brainrot)

case "$base" in
slorp_int) input="42" ;;
slorp_short) input="69" ;;
slorp_float) input="3.14" ;;
slorp_double) input="3.141592" ;;
slorp_char) input="c" ;;
slorp_string) input="skibidi bop bop yes yes" ;;
*) input="" ;;
slorp_int) input="42" ;;
slorp_short) input="69" ;;
slorp_float) input="3.14" ;;
slorp_double) input="3.141592" ;;
slorp_char) input="c" ;;
slorp_string) input="skibidi bop bop yes yes" ;;
native_call_self_init) input="42" ;;
native_call_loop) input=$'1\n2\n3' ;;
native_call_string_arg) input=$'skibidi\nq' ;;
native_call_do_while) input=$'5\n50\n6\n150' ;;
*) input="" ;;
esac

if [[ -n "$input" ]]; then
echo "$input" | valgrind --leak-check=full --error-exitcode=100 ./brainrot "$f"
echo "$input" | valgrind --leak-check=full --error-exitcode=100 "$TARGET" "$f"
else
valgrind --track-origins=yes --leak-check=full --error-exitcode=100 ./brainrot "$f"
valgrind --leak-check=full --error-exitcode=100 "$TARGET" "$f"
fi

valgrind_exit_code=$? # Capture only valgrind’s exit code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MAJOR. This is the same success policy that made #186 silent.

The next lines only fail on valgrind exit code 100. Missing binary is 127. Valgrind not installed is 127. The ASan SIGSEGV that #186 caught was also not 100. That check is unchanged.

Verified: missing binary → valgrind exit 127 → loop continues → script exit 0.

--error-exitcode=100 only fires when valgrind itself classifies a client error. "Could not run the client" is not that. After 135 files of No such file or directory, this still reports a clean leak run.

Treat 127, and any failure to start the target, as a hard error. Do not make "not 100" mean "no leaks."

#186 was not "wrong binary." It was "the job can succeed without testing leaks." The sanitizer split removed one trigger. This check is the mechanism.


if [[ $valgrind_exit_code -eq 100 ]]; then
echo "Valgrind detected memory issues in $f"
exit 1
fi
case $valgrind_exit_code in
0|1) ;;
100)
echo "Valgrind detected memory issues in $f" >&2
exit 1
;;
*)
echo "Valgrind failed while running $f (exit $valgrind_exit_code)" >&2
exit 1
;;
esac

echo
done