diff --git a/CHANGELOG.md b/CHANGELOG.md index 41186e2..6c16abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.18.26-dev standard-library reliability update + +- Added a tested `lib/collections.claro` module with list and map helpers. +- Made the existing `lib/path.claro` import provide working native helpers for joining and inspecting paths. +- Added golden tests for both modules. +- Normalized CRLF/LF golden-test expectations so `claro test` reflects interpreter behavior across Windows and Unix checkouts. +- Replaced stale standard-library command claims with current, runnable documentation. + + ## v1.18.26-dev object field CHECK TYPE unknown-field diagnostic - Added a focused negative `claro typecheck` fixture for direct object-field metadata checks against an undeclared field: `CHECK TYPE player.level IS NUMBER` after `NEW Player player` when the class only declares `HAS score NUMBER`. diff --git a/README.md b/README.md index 3d0dd93..6235306 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,26 @@ packages/ Package names are checked so unsafe names such as `../bad` are rejected. +## Standard-library path and collection helpers + +Claro includes tested, namespaced helpers for everyday programs: + +```claro +IMPORT "lib/path.claro" AS path +IMPORT "lib/collections.claro" AS collections + +CALL path.join WITH "notes", "today.txt" +SAY RESULT + +SET names AS LIST OF TEXT TO LIST +ADD "Ada" TO names +ADD "Grace" TO names +CALL collections.join WITH names, " and " +SAY RESULT +``` + +The `path` module includes `join`, `basename`, `dirname`, `ext`, `stem`, and `absolute`. The `collections` module includes list length/search/join/reverse helpers and map key/value/default-access helpers. See `docs/STDLIB.md` for the complete, current API. + ## Networking v1.18.26 adds safer beginner networking commands with offline `claro://` test URLs. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 9ac0089..a70f8ed 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -22,7 +22,7 @@ Ready now: - choices with `IF` / `ELSE` / `END` - loops - lists and maps -- files, JSON, and text helpers +- files, JSON, text helpers, and tested path/collection library helpers - friendly checking with `claro check` Good starting docs: diff --git a/docs/STDLIB.md b/docs/STDLIB.md index ccc43db..c793d1b 100644 --- a/docs/STDLIB.md +++ b/docs/STDLIB.md @@ -1,56 +1,76 @@ -# Claro Standard Library (Commands) +# Claro Standard Library -Claro’s “stdlib” is exposed as readable commands. +Claro keeps common tasks readable. Most beginner programs can use commands such as `TEXT LENGTH`, `COUNT`, `GET`, `PUT`, `READ FILE`, and `WRITE FILE` directly. For reusable helper functions, import a standard-library module. -## TEXT +## Path helpers -### Split ```claro -TEXT SPLIT "a,b,c" BY "," AS parts +IMPORT "lib/path.claro" AS path + +CALL path.join WITH "notes", "today.txt" +SAY RESULT # notes/today.txt + +CALL path.basename WITH "notes/today.txt" +SAY RESULT # today.txt + +CALL path.dirname WITH "notes/today.txt" +SAY RESULT # notes + +CALL path.ext WITH "notes/today.txt" +SAY RESULT # txt + +CALL path.stem WITH "notes/today.txt" +SAY RESULT # today + +CALL path.absolute WITH "/home/learner" +SAY RESULT # YES ``` -### Join +`path.join` returns portable `/`-separated paths. `path.absolute` recognizes `/`, `\\`, and Windows drive-letter paths. + +## Collection helpers + ```claro -TEXT JOIN parts BY "-" AS out +IMPORT "lib/collections.claro" AS collections + +SET names AS LIST OF TEXT TO LIST +ADD "Ada" TO names +ADD "Grace" TO names + +CALL collections.length WITH names +SAY RESULT # 2 +CALL collections.join WITH names, " and " +SAY RESULT # Ada and Grace +CALL collections.contains WITH names, "Grace" +SAY RESULT # YES +CALL collections.index WITH names, "Grace" +SAY RESULT # 2 ``` -### Replace all -```claro -TEXT REPLACE "one two two" FROM "two" TO "three" AS out -``` +Available list helpers: -## PATH +- `collections.length WITH items` +- `collections.contains WITH items, value` +- `collections.index WITH items, value` — returns a **1-based** position, or `0` when absent +- `collections.join WITH items, separator` +- `collections.reversed WITH items` — returns a new list; the source list is unchanged -### Dirname -```claro -PATH DIRNAME "C:/games/save/file.txt" AS dir -``` +Available map helpers: -### Normalize -Unifies slashes and removes `.` / `..` where possible: -```claro -PATH NORMALIZE "../data/./levels/../sprites" AS clean -``` +- `collections.has_key WITH values, key` +- `collections.get WITH values, key, fallback` +- `collections.keys WITH values` +- `collections.values WITH values` -### Is absolute -```claro -PATH ISABS "/home/me/file.txt" AS isAbs -``` +Use the returned value through `RESULT`, exactly as with other Claro function calls. -### Exists -Checks if a file exists at the path: -```claro -PATH EXISTS "config.json" AS ok -``` +## Existing modules -## TIME +- `lib/text.claro`: text case checks and simple splitting +- `lib/math.claro`: numeric helpers +- `lib/random.claro`: deterministic seed and integer helpers +- `lib/csv.claro`: CSV read/write helpers -### Unix seconds -```claro -TIME NOW AS t -``` +## Commands rather than promises -### Unix milliseconds (coarse) -```claro -TIME NOWMS AS ms -``` +The `PATH ...` and `TIME ...` command forms in older planning material are not part of the stable Claro command set. Use the documented `path` module above and the core file commands instead. This page describes the APIs currently shipped and validated in the stable executable. diff --git a/lib/collections.claro b/lib/collections.claro new file mode 100644 index 0000000..5372371 --- /dev/null +++ b/lib/collections.claro @@ -0,0 +1,6 @@ +# Claro Standard Library: collections +# Use: IMPORT "lib/collections.claro" AS collections +# +# This module provides beginner-friendly helpers for LIST and MAP values. +# All list positions are 1-based so they match GET ... AT and FIND. +EXPORT ALL diff --git a/src/claro.c b/src/claro.c index 4200c96..6bdf134 100644 --- a/src/claro.c +++ b/src/claro.c @@ -296,6 +296,27 @@ static Value builtin_call(Runtime *rt,const char *name,Value *args,int argc,int if(strcmp(kind,"random")==0){ if(strcmp(fn,"seed")==0){ srand((unsigned)v_number(args[0])); return v_none(); } if(strcmp(fn,"int")==0){ int a=(int)v_number(args[0]), b=(int)v_number(args[1]); int r=a + (rand() % (b-a+1)); return v_num(r); } } if(strcmp(kind,"csv")==0){ if(strcmp(fn,"read")==0){ char *txt=argc>0?v_to_string(args[0]):xstrdup(""); char *del=argc>1?v_to_string(args[1]):xstrdup(","); char d=del[0]?del[0]:','; Value rows=v_list(); const char *p=txt; while(*p){ Value row=v_list(); Str field; str_init(&field); int inq=0; while(*p&&*p!='\n'&&*p!='\r'){ if(*p=='"'){ if(inq&&p[1]=='"'){ str_ch(&field,'"'); p+=2; continue;} inq=!inq; p++; continue;} if(!inq&&*p==d){ list_add(row.list,v_str(field.s?field.s:"")); field.len=0; if(field.s) field.s[0]=0; p++; continue;} str_ch(&field,*p++); } list_add(row.list,v_str(field.s?field.s:"")); free(field.s); list_add(rows.list,row); while(*p=='\r'||*p=='\n') p++; } free(txt); free(del); return rows; } if(strcmp(fn,"write")==0){ char *del=argc>1?v_to_string(args[1]):xstrdup(","); char d=del[0]?del[0]:','; Str out; str_init(&out); int i,j; if(argc>0&&args[0].type==V_LIST){ for(i=0;icount;i++){ Value row=args[0].list->items[i]; if(row.type==V_LIST){ for(j=0;jcount;j++){ char *f=v_to_string(row.list->items[j]); int quote=strchr(f,d)||strchr(f,'\n')||strchr(f,'"'); if(j) str_ch(&out,d); if(quote){ const char *q=f; str_ch(&out,'"'); while(*q){ if(*q=='"') str_add(&out,"\"\""); else str_ch(&out,*q); q++; } str_ch(&out,'"'); } else str_add(&out,f); free(f); } } str_ch(&out,'\n'); } } free(del); return v_str(out.s?out.s:""); } } + if(strcmp(kind,"path")==0){ + char *a=argc>0?v_to_string(args[0]):xstrdup(""); + if(strcmp(fn,"join")==0){ char *b=argc>1?v_to_string(args[1]):xstrdup(""); Str out; size_t n=strlen(a); str_init(&out); str_add(&out,a); if(a[0]&&b[0]&&a[n-1]!='/'&&a[n-1]!='\\') str_ch(&out,'/'); while(*b=='/'||*b=='\\') b++; str_add(&out,b); free(a); return v_str(out.s?out.s:""); } + if(strcmp(fn,"basename")==0){ char *slash=strrchr(a,'/'), *back=strrchr(a,'\\'), *cut=slash; if(back&&(!cut||back>cut)) cut=back; Value r=v_str(cut?cut+1:a); free(a); return r; } + if(strcmp(fn,"dirname")==0){ char *slash=strrchr(a,'/'), *back=strrchr(a,'\\'), *cut=slash; if(back&&(!cut||back>cut)) cut=back; if(!cut){ free(a); return v_str(""); } if(cut==a){ free(a); return v_str("/"); } *cut=0; { Value r=v_str(a); free(a); return r; } } + if(strcmp(fn,"ext")==0||strcmp(fn,"stem")==0){ char *slash=strrchr(a,'/'), *back=strrchr(a,'\\'), *base=slash; char *dot; if(back&&(!base||back>base)) base=back; base=base?base+1:a; dot=strrchr(base,'.'); if(!dot||dot==base){ Value r=v_str(strcmp(fn,"stem")==0?base:""); free(a); return r; } if(strcmp(fn,"ext")==0){ Value r=v_str(dot+1); free(a); return r; } *dot=0; { Value r=v_str(base); free(a); return r; } } + if(strcmp(fn,"absolute")==0){ int yes=(a[0]=='/'||a[0]=='\\'||((isalpha((unsigned char)a[0]))&&a[1]==':')); free(a); return v_bool(yes); } + free(a); + } + if(strcmp(kind,"collections")==0){ + Value source=argc>0?args[0]:v_none(); + if(strcmp(fn,"length")==0){ if(source.type==V_LIST&&source.list) return v_num(source.list->count); if(source.type==V_MAP&&source.map) return v_num(source.map->count); return v_num(0); } + if(strcmp(fn,"contains")==0&&source.type==V_LIST&&source.list){ int i; Value needle=argc>1?args[1]:v_none(); for(i=0;icount;i++) if(value_compare(source.list->items[i],needle)==0) return v_bool(1); return v_bool(0); } + if(strcmp(fn,"index")==0&&source.type==V_LIST&&source.list){ int i; Value needle=argc>1?args[1]:v_none(); for(i=0;icount;i++) if(value_compare(source.list->items[i],needle)==0) return v_num(i+1); return v_num(0); } + if(strcmp(fn,"join")==0&&source.type==V_LIST&&source.list){ char *separator=argc>1?v_to_string(args[1]):xstrdup(""); Str out; int i; str_init(&out); for(i=0;icount;i++){ char *part=v_to_string(source.list->items[i]); if(i) str_add(&out,separator); str_add(&out,part); free(part); } free(separator); return v_str(out.s?out.s:""); } + if(strcmp(fn,"reversed")==0&&source.type==V_LIST&&source.list){ Value out=v_list(); int i; for(i=source.list->count-1;i>=0;i--) list_add(out.list,source.list->items[i]); return out; } + if(strcmp(fn,"has_key")==0&&source.type==V_MAP&&source.map){ char *key=argc>1?v_to_string(args[1]):xstrdup(""); int found=map_index(source.map,key)>=0; free(key); return v_bool(found); } + if(strcmp(fn,"get")==0&&source.type==V_MAP&&source.map){ char *key=argc>1?v_to_string(args[1]):xstrdup(""); int index=map_index(source.map,key); Value fallback=argc>2?args[2]:v_none(); Value r=index>=0?v_copy(source.map->vals[index]):v_copy(fallback); free(key); return r; } + if(strcmp(fn,"keys")==0&&source.type==V_MAP&&source.map){ Value out=v_list(); int i; for(i=0;icount;i++) list_add(out.list,v_str(source.map->keys[i])); return out; } + if(strcmp(fn,"values")==0&&source.type==V_MAP&&source.map){ Value out=v_list(); int i; for(i=0;icount;i++) list_add(out.list,source.map->vals[i]); return out; } + } return v_none(); } /* ---------- execution ---------- */ @@ -313,7 +334,7 @@ static int call_object_method(Runtime *rt,const char *name,Value *args,int argc) static void call_function(Runtime *rt,const char *name,Value *args,int argc){ int handled=0,i; Value br=builtin_call(rt,name,args,argc,&handled); if(handled){ rt_set(rt,"RESULT",br); return; } if(call_object_method(rt,name,args,argc)) return; Function *f=find_function(rt,name); if(!f){ rt_error(rt,"",0,"Unknown function: %s",name); return;} Var *old=rt->locals; rt->locals=NULL; for(i=0;ipcnt;i++) rt_set(rt,f->params[i], ireturning=0; exec_range(rt,f->prog,f->start,f->end); if(rt->returning){ rt_set(rt,"RESULT",rt->ret); rt->returning=0; } rt->locals=old; } static int stack_has(Runtime *rt,const char *path){ int i; for(i=0;iimport_depth;i++) if(strcmp(rt->import_stack[i],path)==0) return 1; return 0; } static int export_allows(char **exports,int ec,const char *name){ int i; if(ec==0) return 1; for(i=0;iimport_stack=(char**)xrealloc(rt->import_stack,sizeof(char*)*(rt->import_depth+1)); rt->import_stack[rt->import_depth++]=xstrdup(path); for(i=0;icount;i++){ char tmp[1024],up[64],w[64]; char *t; strncpy(tmp,p->lines[i],sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(strcmp(up,"EXPORT")==0){ t+=6; t=trim_inplace(t); if(ci_eq(t,"ALL")){ ec=0; break;} ec=parse_params(t,&exports); break; } } if(ns){ Str pr; str_init(&pr); str_add(&pr,ns); str_ch(&pr,'.'); prefix=str_take(&pr); scan_functions(rt,p,prefix); exec_range(rt,p,0,p->count); /* copy exported vars */ for(Var *v=rt->globals; v; v=v->next){ if(strncmp(v->name,prefix,strlen(prefix))!=0 && export_allows(exports,ec,v->name)){ char nn[256]; snprintf(nn,sizeof(nn),"%s%s",prefix,v->name); if(!rt_has(rt,nn)) rt_set_global(rt,nn,v->val); } } } else { scan_functions(rt,p,""); exec_range(rt,p,0,p->count); } @@ -533,7 +554,9 @@ static int typecheck_file(const char *path){ FILE *f=fopen(path,"rb"); char line } fclose(f); if(errs==0) printf("Type check OK\n"); return errs?1:0; } static char *read_file_text(const char *path){ FILE *f=fopen(path,"rb"); Str b; int c; if(!f) return NULL; str_init(&b); while((c=fgetc(f))!=EOF) str_ch(&b,(char)c); fclose(f); return str_take(&b); } -static int run_one_test(const char *path){ char outpath[512]; char *dot; char *expect,*got; int rc,ok; Runtime rt; snprintf(outpath,sizeof(outpath),"%s",path); dot=strrchr(outpath,'.'); if(dot) strcpy(dot,".out"); expect=read_file_text(outpath); if(!expect) return 0; rt_init(&rt); rt.capture=1; rc=run_file(&rt,path); got=rt.captured.s?xstrdup(rt.captured.s):xstrdup(""); ok=(rc==0 && strcmp(got,expect)==0); printf("%s %s\n",ok?"PASS":"FAIL",path); if(!ok) printf("Expected:\n%s\nGot:\n%s\n",expect,got); free(expect); free(got); return ok?0:1; } +/* Golden output files may be checked out as CRLF on Windows while Claro captures output as LF. Normalize only test expectations so test results represent behavior, not Git line-ending settings. */ +static char *read_test_expectation(const char *path){ char *raw=read_file_text(path); Str normalized; size_t i; if(!raw) return NULL; str_init(&normalized); for(i=0;raw[i];i++){ if(raw[i]=='\015'){ if(raw[i+1]=='\012') continue; str_ch(&normalized,'\012'); } else str_ch(&normalized,raw[i]); } free(raw); return str_take(&normalized); } +static int run_one_test(const char *path){ char outpath[512]; char *dot; char *expect,*got; int rc,ok; Runtime rt; snprintf(outpath,sizeof(outpath),"%s",path); dot=strrchr(outpath,'.'); if(dot) strcpy(dot,".out"); expect=read_test_expectation(outpath); if(!expect) return 0; rt_init(&rt); rt.capture=1; rc=run_file(&rt,path); got=rt.captured.s?xstrdup(rt.captured.s):xstrdup(""); ok=(rc==0 && strcmp(got,expect)==0); printf("%s %s\n",ok?"PASS":"FAIL",path); if(!ok) printf("Expected:\n%s\nGot:\n%s\n",expect,got); free(expect); free(got); return ok?0:1; } static int run_tests(void){ int fails=0; #ifndef _WIN32 DIR *d=opendir("tests"); struct dirent *de; if(!d){ fprintf(stderr,"No tests directory\n"); return 1;} while((de=readdir(d))){ if(strstr(de->d_name,".claro")){ char path[512]; snprintf(path,sizeof(path),"tests/%s",de->d_name); fails+=run_one_test(path); } } closedir(d); diff --git a/tests/41_stdlib_path.claro b/tests/41_stdlib_path.claro new file mode 100644 index 0000000..e236873 --- /dev/null +++ b/tests/41_stdlib_path.claro @@ -0,0 +1,16 @@ +IMPORT "lib/path.claro" AS path + +CALL path.join WITH "notes", "today.txt" +SAY RESULT +CALL path.basename WITH "notes/today.txt" +SAY RESULT +CALL path.dirname WITH "notes/today.txt" +SAY RESULT +CALL path.ext WITH "notes/today.txt" +SAY RESULT +CALL path.stem WITH "notes/today.txt" +SAY RESULT +CALL path.absolute WITH "/home/learner" +SAY RESULT +CALL path.absolute WITH "notes/today.txt" +SAY RESULT diff --git a/tests/41_stdlib_path.out b/tests/41_stdlib_path.out new file mode 100644 index 0000000..b124559 --- /dev/null +++ b/tests/41_stdlib_path.out @@ -0,0 +1,7 @@ +notes/today.txt +today.txt +notes +txt +today +YES +NO diff --git a/tests/42_stdlib_collections.claro b/tests/42_stdlib_collections.claro new file mode 100644 index 0000000..1a321bb --- /dev/null +++ b/tests/42_stdlib_collections.claro @@ -0,0 +1,31 @@ +IMPORT "lib/collections.claro" AS collections + +SET words AS LIST OF TEXT TO LIST +ADD "Ada" TO words +ADD "Grace" TO words +ADD "Lin" TO words + +CALL collections.length WITH words +SAY RESULT +CALL collections.contains WITH words, "Grace" +SAY RESULT +CALL collections.index WITH words, "Lin" +SAY RESULT +CALL collections.join WITH words, " | " +SAY RESULT +CALL collections.reversed WITH words +SET backwards TO RESULT +GET backwards AT 1 AS first +SAY first + +SET scores AS MAP OF NUMBER TO MAP +PUT scores KEY "math" VALUE 95 +PUT scores KEY "science" VALUE 88 +CALL collections.has_key WITH scores, "math" +SAY RESULT +CALL collections.get WITH scores, "science", 0 +SAY RESULT +CALL collections.keys WITH scores +SET subjects TO RESULT +COUNT subjects AS subjectCount +SAY subjectCount diff --git a/tests/42_stdlib_collections.out b/tests/42_stdlib_collections.out new file mode 100644 index 0000000..fa29672 --- /dev/null +++ b/tests/42_stdlib_collections.out @@ -0,0 +1,8 @@ +3 +YES +3 +Ada | Grace | Lin +Lin +YES +88 +2