From d844d8cb853cd4c4b590c07ba37006e0adc2debb Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 4 Oct 2026 00:30:22 +0000 Subject: [PATCH] fix: release check-mode expression tokens --- docs/CURRENT_STATUS.md | 2 ++ src/claro.c | 3 ++ tools/validate_check_token_cleanup.py | 48 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 tools/validate_check_token_cleanup.py diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 693ad14..062711f 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -51,6 +51,8 @@ The `DELETE FILE ...` and `DELETE FOLDER ...` commands now release their evaluat `typecheck` now frees its temporary type environment on return, including the owned variable names and type-string values; replacing an inferred type releases the previous value first. `python3 tools/validate_typecheck_token_cleanup.py` reproduced 479 bytes of leaked type-environment allocations before the change, then passed under ASan/UBSan/LSan with the fix. The validator is named for the original local work file; it specifically checks type-environment cleanup, not tokenizer allocations. This is a narrow cleanup slice and does not establish that all typecheck metadata allocations are leak-free. +`claro check` now releases expression-token arrays and strings after variable-name analysis, and frees its temporary variable environment at the end of each check. The untracked focused regression validator `tools/validate_check_token_cleanup.py` first reproduced 310,447 leaked bytes across 2,000 repeated-string expressions; after the cleanup, `python3 tools/validate_check_token_cleanup.py` passes under ASan/UBSan/LSan. Fresh verification: `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This is a narrow, runtime-verified cleanup for check mode; it does not establish that all allocations are leak-free. + ## Feature matrix ### Beginner scripting core diff --git a/src/claro.c b/src/claro.c index 3d3a36f..0ac689a 100644 --- a/src/claro.c +++ b/src/claro.c @@ -480,6 +480,7 @@ static char *check_var_after_as(const char *line){ const char *as=find_word_ci(l static char *check_between_words(const char *line,const char *a,const char *b){ const char *pa=find_word_ci(line,a); const char *pb; if(!pa) return NULL; pa+=strlen(a); pb=find_word_ci(pa,b); if(!pb) return NULL; return substr(pa,pb); } typedef struct CheckBlock { const char *expect; const char *open; int line; } CheckBlock; +static void check_vars_free(Var **vars){ Var *v,*next; if(!vars) return; for(v=*vars;v;v=next){ next=v->next; free(v->name); value_free(v->val); free(v); } *vars=NULL; } static int check_file(const char *path){ FILE *f=fopen(path,"rb"); char line[4096]; int line_no=0,errs=0,sp=0; CheckBlock stack[256]; Var *vars=NULL; check_add_var(&vars,"RESULT"); check_add_var(&vars,"LASTERROR"); check_add_var(&vars,"LASTERRORFILE"); check_add_var(&vars,"LASTERRORLINE"); check_add_var(&vars,"LASTEXIT"); check_add_var(&vars,"LASTHTTP"); @@ -570,11 +571,13 @@ static int check_file(const char *path){ errs++; } } + tokens_free(&ts); } } fclose(f); while(sp>0){ CheckBlock b=stack[--sp]; printf("%s:%d: %s block opened here needs %s\n",path,b.line,b.open,b.expect); errs++; } if(errs==0) printf("OK\n"); + check_vars_free(&vars); return errs?1:0; } diff --git a/tools/validate_check_token_cleanup.py b/tools/validate_check_token_cleanup.py new file mode 100644 index 0000000..00364f9 --- /dev/null +++ b/tools/validate_check_token_cleanup.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Check that check-mode releases tokens allocated for each expression.""" +from pathlib import Path +import os +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parent.parent + + +def main() -> int: + if os.name == "nt": + print("SKIP: LeakSanitizer check is POSIX-only") + return 0 + with tempfile.TemporaryDirectory(prefix="check-token-cleanup-") as tmp: + tmp_path = Path(tmp) + binary = tmp_path / "claro-lsan" + script = tmp_path / "tokens.claro" + script.write_text('SAY "a repeated string literal"\n' * 2000, encoding="utf-8") + build = subprocess.run( + ["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined", + "src/claro.c", "-o", str(binary), "-lm"], + cwd=ROOT, text=True, capture_output=True, + ) + if build.returncode: + print(build.stdout, end="") + print(build.stderr, end="", file=sys.stderr) + return build.returncode + run = subprocess.run( + [str(binary), "check", str(script)], cwd=ROOT, text=True, + capture_output=True, env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"}, + ) + if run.returncode: + print("FAIL: check-mode token cleanup probe failed") + print(run.stdout, end="") + print(run.stderr, end="", file=sys.stderr) + return 1 + if "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr: + print("FAIL: check-mode token allocations still leak") + print(run.stderr, end="", file=sys.stderr) + return 1 + print("PASS: check-mode expression tokens are released") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())