fix: release GET index expression values
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
### Release GET list-index expression temporaries
|
||||
|
||||
- `GET ... AT ... AS ...` now releases its evaluated index value after converting it to a list position.
|
||||
- Added `tools/validate_get_at_expression_cleanup.py` with a repeated string-index regression check under ASan/UBSan/LSan.
|
||||
|
||||
### Release caught runtime diagnostic values
|
||||
|
||||
- Runtime errors caught by `TRY` now release their prior owned message and file strings when the error state is cleared.
|
||||
|
||||
@@ -48,6 +48,8 @@ The `RUN COMMAND` path now decodes POSIX `pclose()` wait status before storing `
|
||||
|
||||
`GET ... KEY ... AS ...` now releases its temporary map and key values after lookup. `python3 tools/validate_get_key_expression_cleanup.py` reproduced 1,004,000 leaked bytes before the change and passes after it with ASan/UBSan/LSan enabled. `EXISTS FILE ... AS ...` now releases its evaluated path value after checking it; `python3 tools/validate_exists_expression_cleanup.py` first reproduced 62,000 leaked bytes across 2,000 calls, then passed with ASan/UBSan/LSan enabled. These are narrow expression-temporary cleanup slices, not a claim that all runtime allocations are leak-free.
|
||||
|
||||
`GET ... AT ... AS ...` now releases its evaluated index expression after converting it to a list position. `python3 tools/validate_get_at_expression_cleanup.py` first reproduced 4,000 leaked bytes across 2,000 string-valued indices, then passed under ASan/UBSan/LSan with the selected value preserved. The validator is wired into `tools/validate_v1_17.py`; this is a narrow ownership fix, not a claim that all runtime allocations are leak-free.
|
||||
|
||||
`LIST FOLDER ... AS ...` now releases its evaluated path and the temporary folder-list value after runtime storage copies the list. `python3 tools/validate_list_folder_expression_cleanup.py` reproduced 220,000 leaked bytes across 2,000 operations under ASan/UBSan/LSan before the change and passes after it with no leak report. The working-tree checks `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This is a narrow ownership improvement, not a claim that all runtime allocations are leak-free.
|
||||
The `DELETE FILE ...` and `DELETE FOLDER ...` commands now release their evaluated path values after converting the paths to owned strings. `python3 tools/validate_delete_expression_cleanup.py` first reproduced 178,000 leaked bytes across 2,000 `DELETE FILE` expression evaluations under ASan/UBSan/LSan, then passed after cleanup. This check covers expression ownership; malformed-input diagnostic paths still have the previously documented `rt_error` message leak. The `COPY FILE` and `MOVE FILE` commands now also release their evaluated source/destination path values after conversion to strings; `python3 tools/validate_copy_move_expression_cleanup.py` reproduced 74,000 bytes leaked across 1,000 copy/move pairs before the fix and is the focused ASan/UBSan/LSan regression gate. `CREATE FOLDER ...` now releases its evaluated path value after converting it to an owned path string; `python3 tools/validate_create_folder_expression_cleanup.py` reproduced 200,000 leaked bytes across 2,000 calls before the fix and passes with ASan/UBSan/LSan enabled. This narrow cleanup slice does not establish that all runtime allocations are leak-free.
|
||||
|
||||
|
||||
@@ -112,6 +112,8 @@ The validator runs 2,000 prompt/input operations with AddressSanitizer, Undefine
|
||||
|
||||
`GET ... KEY ... AS ...` releases its evaluated map copy and key value after storing the selected value. `python3 tools/validate_get_key_expression_cleanup.py` runs 2,000 lookups under ASan/UBSan/LSan; before the cleanup it reproduced 1,004,000 bytes leaked, and after it passes with the expected output and no reported leaks.
|
||||
|
||||
`GET ... AT ... AS ...` now releases its evaluated index expression after converting it to a list position. The regression validator `python3 tools/validate_get_at_expression_cleanup.py` repeats a string-valued index 2,000 times under ASan/UBSan/LSan; before the fix, LeakSanitizer reported 4,000 leaked bytes, and after it passes with the expected selected value.
|
||||
|
||||
## COPY/MOVE expression temporary cleanup
|
||||
|
||||
`COPY FILE ... TO ...` and `MOVE FILE ... TO ...` release both evaluated path values after converting them to owned path strings. `python3 tools/validate_copy_move_expression_cleanup.py` runs 1,000 copy/move pairs under ASan/UBSan/LSan; before the cleanup it reproduced 74,000 bytes leaked across 4,000 path-expression values, and after it passes with no reported leaks.
|
||||
|
||||
+1
-1
@@ -438,7 +438,7 @@ static void exec_line(Runtime *rt,Program *p,int *pcp,const char *raw){ char buf
|
||||
if(strcmp(up,"IMPORT")==0){ const char *pcur=t+6; char *path=unquote_token(&pcur); char *ns=NULL; while(*pcur&&isspace((unsigned char)*pcur)) pcur++; if(starts_ci(pcur,"AS")){ pcur+=2; ns=xstrdup(trim_inplace((char*)pcur)); } do_import(rt,path,ns,p->path,pc+1); free(path); free(ns); return; }
|
||||
if(strcmp(up,"ADD")==0){ const char *to=find_word_ci(t,"TO"); if(to){ char *ex=substr(t+3,to); char *var=xstrdup(trim_inplace((char*)to+2)); Value v=eval_expr(rt,ex); Value l=rt_get(rt,var); if(l.type==V_LIST){ list_add(l.list,v); rt_set(rt,var,l); } free(ex); free(var); value_free(v); value_free(l);} return; }
|
||||
if(strcmp(up,"COUNT")==0){ const char *as=find_word_ci(t,"AS"); if(as){ if(starts_ci(t+5," ARGUMENTS")){ char *var=xstrdup(trim_inplace((char*)as+2)); rt_set(rt,var,v_num(rt->script_argc)); free(var); } else { char *ex=substr(t+5,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value v=eval_expr(rt,ex); rt_set(rt,var,v_num(v.type==V_LIST?v.list->count:(v.type==V_MAP?v.map->count:0))); value_free(v); free(ex); free(var); } } return; }
|
||||
if(strcmp(up,"GET")==0){ const char *at=find_word_ci(t,"AT"), *key=find_word_ci(t,"KEY"), *as=find_word_ci(t,"AS"); if(as&&starts_ci(t+3," ALL ARGUMENTS")){ char *var=xstrdup(trim_inplace((char*)as+2)); Value arr=v_list(); int i; for(i=0;i<rt->script_argc;i++) list_add(arr.list,v_str(rt->script_argv[i])); rt_set(rt,var,arr); free(var); } else if(as&&starts_ci(t+3," ARGUMENT")){ char *ie=substr(t+12,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value index_value=eval_expr(rt,ie); int idx=(int)v_number(index_value); Value argument_value; value_free(index_value); argument_value=v_str(idx>=1&&idx<=rt->script_argc?rt->script_argv[idx-1]:""); rt_set(rt,var,argument_value); value_free(argument_value); free(ie); free(var); } else if(as&&starts_ci(t+3," ENV")){ const char *pcur=t+7; char *name=substr(pcur,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value nv=eval_expr(rt,name); char *ns=v_to_string(nv); const char *ev=getenv(ns); Value result=v_str(ev?ev:""); rt_set(rt,var,result); value_free(result); free(ns); value_free(nv); free(name); free(var); } else if(as&&at){ char *ce=substr(t+3,at); char *ie=substr(at+2,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value c=eval_expr(rt,ce); int idx=(int)v_number(eval_expr(rt,ie)); if(c.type==V_LIST && idx>=1 && idx<=c.list->count) rt_set(rt,var,c.list->items[idx-1]); else { Value empty=v_str(""); rt_set(rt,var,empty); value_free(empty); } value_free(c); free(ce); free(ie); free(var); } else if(as&&key){ char *ce=substr(t+3,key); char *ke=substr(key+3,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value c=eval_expr(rt,ce); Value kv=eval_expr(rt,ke); char *ks=v_to_string(kv); if(c.type==V_MAP){ Value got=map_get(c.map,ks); rt_set(rt,var,got); value_free(got); } else rt_set(rt,var,v_str("")); free(ks); value_free(c); value_free(kv); free(ce); free(ke); free(var); } return; }
|
||||
if(strcmp(up,"GET")==0){ const char *at=find_word_ci(t,"AT"), *key=find_word_ci(t,"KEY"), *as=find_word_ci(t,"AS"); if(as&&starts_ci(t+3," ALL ARGUMENTS")){ char *var=xstrdup(trim_inplace((char*)as+2)); Value arr=v_list(); int i; for(i=0;i<rt->script_argc;i++) list_add(arr.list,v_str(rt->script_argv[i])); rt_set(rt,var,arr); free(var); } else if(as&&starts_ci(t+3," ARGUMENT")){ char *ie=substr(t+12,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value index_value=eval_expr(rt,ie); int idx=(int)v_number(index_value); Value argument_value; value_free(index_value); argument_value=v_str(idx>=1&&idx<=rt->script_argc?rt->script_argv[idx-1]:""); rt_set(rt,var,argument_value); value_free(argument_value); free(ie); free(var); } else if(as&&starts_ci(t+3," ENV")){ const char *pcur=t+7; char *name=substr(pcur,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value nv=eval_expr(rt,name); char *ns=v_to_string(nv); const char *ev=getenv(ns); Value result=v_str(ev?ev:""); rt_set(rt,var,result); value_free(result); free(ns); value_free(nv); free(name); free(var); } else if(as&&at){ char *ce=substr(t+3,at); char *ie=substr(at+2,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value c=eval_expr(rt,ce), iv=eval_expr(rt,ie); int idx=(int)v_number(iv); value_free(iv); if(c.type==V_LIST && idx>=1 && idx<=c.list->count) rt_set(rt,var,c.list->items[idx-1]); else { Value empty=v_str(""); rt_set(rt,var,empty); value_free(empty); } value_free(c); free(ce); free(ie); free(var); } else if(as&&key){ char *ce=substr(t+3,key); char *ke=substr(key+3,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value c=eval_expr(rt,ce); Value kv=eval_expr(rt,ke); char *ks=v_to_string(kv); if(c.type==V_MAP){ Value got=map_get(c.map,ks); rt_set(rt,var,got); value_free(got); } else rt_set(rt,var,v_str("")); free(ks); value_free(c); value_free(kv); free(ce); free(ke); free(var); } return; }
|
||||
if(strcmp(up,"PUT")==0){ const char *key=find_word_ci(t,"KEY"), *val=find_word_ci(t,"VALUE"), *as=find_word_ci(t,"AS"), *into=find_word_ci(t,"INTO"); if(key&&val){ char *me=substr(t+3,key); char *ke=substr(key+3,val); char *ve=xstrdup(val+5); Value m=eval_expr(rt,me); Value k=eval_expr(rt,ke); Value v=eval_expr(rt,ve); char *ks=v_to_string(k); if(m.type==V_MAP){ char *mn=trim_inplace(me); map_put(m.map,ks,v); rt_set(rt,mn,m); } free(ks); free(me); free(ke); free(ve); value_free(m); value_free(k); value_free(v); } else if(as&&into){ char *ke=substr(t+3,as); char *ve=substr(as+2,into); char *mn=xstrdup(trim_inplace((char*)into+4)); Value m=rt_get(rt,mn); Value k=eval_expr(rt,ke); Value v=eval_expr(rt,ve); char *ks=v_to_string(k); if(m.type==V_MAP){ map_put(m.map,ks,v); rt_set(rt,mn,m); } free(ks); free(ke); free(ve); free(mn); } return; }
|
||||
if(strcmp(up,"WRITE")==0||strcmp(up,"APPEND")==0){ if(starts_ci(t+strlen(w)," FILE")){ const char *pcur=t+strlen(w)+5; const char *with=find_word_ci(pcur,"WITH"); if(with){ char *pe=substr(pcur,with); char *ve=xstrdup(with+4); Value pv=eval_expr(rt,pe); Value vv=eval_expr(rt,ve); char *path=v_to_string(pv), *text=v_to_string(vv); FILE *f=fopen(path,strcmp(up,"APPEND")==0?"ab":"wb"); if(!f) rt_error(rt,p->path,pc+1,"Could not write file: %s",strerror(errno)); else { fwrite(text,1,strlen(text),f); fclose(f);} free(path); free(text); value_free(pv); value_free(vv); free(pe); free(ve); } } return; }
|
||||
if(strcmp(up,"READ")==0){ const char *pcur=t+4; const char *as=find_word_ci(pcur,"AS"); if(as&&starts_ci(pcur," FILE")){ char *pe=substr(pcur+5,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value pv=eval_expr(rt,pe); char *path=v_to_string(pv); FILE *f=fopen(path,"rb"); if(!f) rt_error(rt,p->path,pc+1,"Could not read file: %s",strerror(errno)); else { Str b; int c; Value contents; str_init(&b); while((c=fgetc(f))!=EOF) str_ch(&b,(char)c); fclose(f); contents=v_str(b.s?b.s:""); rt_set(rt,var,contents); value_free(contents); free(b.s);} free(path); value_free(pv); free(pe); free(var); } return; }
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that GET ... AT ... releases its evaluated index value."""
|
||||
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="get-at-cleanup-") as tmp:
|
||||
binary = Path(tmp) / "claro-asan"
|
||||
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
|
||||
script = Path(tmp) / "get-at.claro"
|
||||
script.write_text(
|
||||
'SET items TO LIST\n'
|
||||
'ADD "hello" TO items\n'
|
||||
'DO 2000 TIMES\n'
|
||||
'GET items AT "1" AS item\n'
|
||||
'END\n'
|
||||
'SAY item\n', encoding="utf-8"
|
||||
)
|
||||
run = subprocess.run(
|
||||
[str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||
)
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: GET ... AT ... retains expression allocations")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if run.stdout != "hello\n":
|
||||
print(f"FAIL: unexpected output: {run.stdout!r}")
|
||||
return 1
|
||||
print("PASS: repeated GET ... AT ... is leak-free and preserves the selected value")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -50,6 +50,7 @@ def main():
|
||||
run([sys.executable, "tools/validate_runtime_error_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_get_argument_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_get_env_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_get_at_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_read_file_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_write_file_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_version_convention.py"])
|
||||
|
||||
Reference in New Issue
Block a user