From 9c5202cd2c013576efc995cbd09926193283c086 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 8 Jun 2026 10:20:30 -0600 Subject: [PATCH] feat: add typed function argument diagnostics --- CHANGELOG.md | 6 +++++ README.md | 15 ++++++++++++ docs/ADVANCED_STATIC_TYPING.md | 32 ++++++++++++++++++++++++- docs/CURRENT_STATUS.md | 3 ++- docs/ROADMAP.md | 2 +- src/claro.c | 8 ++++++- tests/typecheck_function_bad.claro | 5 ++++ tests/typecheck_function_good.claro | 5 ++++ tools/validate_typecheck_diagnostics.py | 27 +++++++++++++++++++++ 9 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/typecheck_function_bad.claro create mode 100644 tests/typecheck_function_good.claro diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ec8980..d02e037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v1.18.26-dev typed function diagnostic foundation + +- Added a narrow `claro typecheck` foundation for function argument diagnostics: if a function body declares a parameter expectation with `CHECK TYPE parameter IS TYPE`, calls to that function are checked against the expected argument type. +- Added positive and negative fixtures for the function-argument diagnostic path. +- Extended `tools/validate_typecheck_diagnostics.py` to verify both the new friendly error and a matching successful call. + ## v1.18.26-dev learner documentation status cleanup - Grouped the README documentation map by current feature status so new learners can see what is ready, what is foundation-level, and what is still planned or experimental. diff --git a/README.md b/README.md index 57e19bc..a7fcb6c 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,21 @@ SET scores AS MAP OF NUMBER TO MAP PUT scores KEY "math" VALUE 98 ``` +`claro typecheck` also has an early function-argument diagnostic foundation. A function can state a parameter expectation with `CHECK TYPE`, and calls with the wrong value type get a friendly error: + +```claro +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO square "oops" +``` + +```text +Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT. +``` + ## Project and package workflow v1.18.26 hardens Claro's project/package workflow. diff --git a/docs/ADVANCED_STATIC_TYPING.md b/docs/ADVANCED_STATIC_TYPING.md index e16d428..d1f11b6 100644 --- a/docs/ADVANCED_STATIC_TYPING.md +++ b/docs/ADVANCED_STATIC_TYPING.md @@ -43,6 +43,36 @@ Output: Type mismatch for map scores: expected NUMBER value, but this value looks like TEXT. ``` +## Function parameter checks + +Claro now has a small static-checking foundation for function arguments. Keep the beginner-friendly function syntax, then put the expected type inside the function with `CHECK TYPE`: + +```claro +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO square 4 +``` + +If a learner calls the function with the wrong kind of value, `claro typecheck` explains which parameter needs which type: + +```claro +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO square "oops" +``` + +Output: + +```text +Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT. +``` + ## Status -This is currently a static checker feature. It improves `claro typecheck` and validation confidence. Runtime enforcement for every container mutation can be added later after the syntax is classroom-tested. +This is currently a static checker feature. It improves `claro typecheck` and validation confidence. Runtime enforcement for every container mutation and richer function signatures can be added later after the syntax is classroom-tested. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index faf147f..6ee8d03 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -52,9 +52,10 @@ Ready now: - typed variables such as `SET score NUMBER 10` - `TYPE OF` and `CHECK TYPE` - typed list/map checks through `claro typecheck` +- a narrow function argument check: `CHECK TYPE parameter IS TYPE` inside a function lets `claro typecheck` catch mismatched call arguments Still needed: -- typed function parameters and return values +- richer typed function signatures and return values - type checking through branches and loops - object method and field type checking - typed imports/modules diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7d8fd64..da714e5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -41,7 +41,7 @@ See `CURRENT_STATUS.md` for the detailed feature matrix. Goal: make larger beginner programs safer without making first scripts harder. Needed next: -- typed function parameters +- richer typed function signatures - typed function returns - type checking across branches and loops - typed imports/modules diff --git a/src/claro.c b/src/claro.c index d30c1f2..8a814b6 100644 --- a/src/claro.c +++ b/src/claro.c @@ -512,12 +512,18 @@ static const char *simple_expr_type(Var *types,const char *expr){ char tmp[512]; static int type_words_match(const char *need,const char *got){ if(!need||!*need||ci_eq(need,"ANY")) return 1; if(!got||!*got) return 1; if(ci_eq(need,got)) return 1; if(starts_ci(need,"LIST OF") && ci_eq(got,"LIST")) return 1; if(starts_ci(need,"MAP OF") && ci_eq(got,"MAP")) return 1; if((ci_eq(need,"YESNO")||ci_eq(need,"BOOL")||ci_eq(need,"BOOLEAN")) && (ci_eq(got,"YESNO")||ci_eq(got,"BOOL")||ci_eq(got,"BOOLEAN"))) return 1; if(ci_eq(need,"OBJECT") && ci_eq(got,"MAP")) return 1; return 0; } static const char *container_member_type(const char *type,const char *kind){ const char *p; static char buf[128]; if(!type||!kind) return NULL; if(!starts_ci(type,kind)) return NULL; p=type+strlen(kind); while(*p&&isspace((unsigned char)*p)) p++; if(!starts_ci(p,"OF")) return NULL; p+=2; while(*p&&isspace((unsigned char)*p)) p++; if(!*p) return NULL; snprintf(buf,sizeof(buf),"%s",p); return trim_inplace(buf); } static void parse_set_for_typecheck2(const char *t,char **name,char **type,char **expr){ const char *rest=t+3; const char *to=find_word_ci(rest,"TO"); const char *as=find_word_ci(rest,"AS"); *name=NULL; *type=NULL; *expr=NULL; if(as&&to&&asfunc=xstrdup(func); c->param=xstrdup(param); c->type=xstrdup(type); c->pos=pos; c->next=*checks; *checks=c; } +static const char *type_param_check_for(TypeParamCheck *checks,const char *func,int pos,const char **param){ TypeParamCheck *c=checks; while(c){ if(c->pos==pos&&ci_eq(c->func,func)){ if(param) *param=c->param; return c->type; } c=c->next; } return NULL; } +static int param_index_named(char **params,int pcnt,const char *name){ int i; char tmp[256]; char *p; for(i=0;i=0) add_type_param_check(&checks,fname,en,need,idx); free(expr); free(need); } } } } } rewind(f); return checks; } +static int typecheck_file(const char *path){ FILE *f=fopen(path,"rb"); char line[4096]; int line_no=0,errs=0; Var *types=NULL; TypeParamCheck *param_checks=NULL; if(!f){ fprintf(stderr,"Could not open %s\n",path); return 1; } param_checks=collect_function_param_type_checks(f); type_env_set(&types,"RESULT","ANY"); type_env_set(&types,"LASTERROR","TEXT"); type_env_set(&types,"LASTERRORFILE","TEXT"); type_env_set(&types,"LASTERRORLINE","NUMBER"); type_env_set(&types,"LASTEXIT","NUMBER"); while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; line_no++; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"SET")){ char *name=NULL,*type=NULL,*expr=NULL; const char *got,*old; parse_set_for_typecheck2(t,&name,&type,&expr); if(name&&*trim_inplace(name)){ char *nt=trim_inplace(name); char *tt=type?trim_inplace(type):NULL; got=simple_expr_type(types,expr); old=type_env_get(types,nt); if(tt&&*tt){ if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } type_env_set(&types,nt,tt); } else if(old){ if(got&&!type_words_match(old,got)){ printf("%s:%d: Type mismatch for %s: it was first set as %s, but this value looks like %s.\n",path,line_no,nt,old,got); errs++; } } else if(got) type_env_set(&types,nt,got); else type_env_set(&types,nt,"ANY"); } free(name); free(type); free(expr); } else if(!strcmp(up,"ADD")){ const char *to=find_word_ci(t,"TO"); if(to){ char *expr=substr(t+3,to); char *name=xstrdup(trim_inplace((char*)to+2)); const char *listtype=type_env_get(types,name); const char *need=container_member_type(listtype,"LIST"); const char *got=simple_expr_type(types,expr); if(need&&got&&!type_words_match(need,got)){ printf("%s:%d: Type mismatch for list %s: expected %s item, but this value looks like %s.\n",path,line_no,name,need,got); errs++; } free(expr); free(name); } } else if(!strcmp(up,"PUT")){ const char *key=find_word_ci(t,"KEY"), *val=find_word_ci(t,"VALUE"); if(key&&val){ char *name=substr(t+3,key); char *expr=xstrdup(trim_inplace((char*)val+5)); char *nt=trim_inplace(name); const char *maptype=type_env_get(types,nt); const char *need=container_member_type(maptype,"MAP"); const char *got=simple_expr_type(types,expr); if(need&&got&&!type_words_match(need,got)){ printf("%s:%d: Type mismatch for map %s: expected %s value, but this value looks like %s.\n",path,line_no,nt,need,got); errs++; } free(name); free(expr); } } else if(!strcmp(up,"ASK")){ const char *as=find_word_ci(t,"AS"); char *name=NULL,*type=NULL; if(as){ const char *pcur=as+2; name=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } } else { const char *pcur=t+3; char *prompt=unquote_token(&pcur); free(prompt); name=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } } if(name&&*trim_inplace(name)) type_env_set(&types,trim_inplace(name),type?type:"TEXT"); free(name); free(type); } else if(!strcmp(up,"TYPE")&&starts_ci(t,"TYPE OF")){ char *name=check_var_after_as(t); if(name){ type_env_set(&types,name,"TEXT"); free(name); } } else if(!strcmp(up,"NEW")){ const char *as=find_word_ci(t,"AS"); char *name=NULL; if(as) name=xstrdup(trim_inplace((char*)as+2)); else { const char *pcur=t+3; char *cls=unquote_token(&pcur); free(cls); name=unquote_token(&pcur); } if(name&&*trim_inplace(name)) type_env_set(&types,trim_inplace(name),"OBJECT"); free(name); } + else if(!strcmp(up,"DO")&&!find_word_ci(t,"TIMES")){ const char *pcur=t+2; char *fname=unquote_token(&pcur); char **parts=NULL; int ac=0,i; if(*trim_inplace((char*)pcur)) ac=split_args(pcur,&parts); for(i=0;i