feat: add typed function argument diagnostics

This commit is contained in:
Hermes Agent
2026-06-08 10:20:30 -06:00
parent 078b7874d3
commit 9c5202cd2c
9 changed files with 99 additions and 4 deletions
+6
View File
@@ -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.
+15
View File
@@ -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.
+31 -1
View File
@@ -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.
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
+7 -1
View File
@@ -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&&as<to){ *name=substr(rest,as); *type=substr(as+2,to); *expr=xstrdup(to+2); return; } if(to){ char *before=substr(rest,to); const char *pcur=before; char *var=unquote_token(&pcur); char *maybe=unquote_token(&pcur); char *mt=trim_inplace(maybe); *name=xstrdup(trim_inplace(var)); if(claro_is_type_word(mt)) *type=xstrdup(mt); *expr=xstrdup(to+2); free(before); free(var); free(maybe); return; } { const char *pcur=rest; char *var=unquote_token(&pcur); const char *after_var=pcur; char *maybe=unquote_token(&pcur); char *mt=trim_inplace(maybe); *name=xstrdup(trim_inplace(var)); if(claro_is_type_word(mt)){ *type=xstrdup(mt); *expr=xstrdup(trim_inplace((char*)pcur)); } else { *expr=xstrdup(trim_inplace((char*)after_var)); } free(var); free(maybe); } }
static int typecheck_file(const char *path){ FILE *f=fopen(path,"rb"); char line[4096]; int line_no=0,errs=0; Var *types=NULL; if(!f){ fprintf(stderr,"Could not open %s\n",path); return 1; } 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); }
typedef struct TypeParamCheck { char *func; char *param; char *type; int pos; struct TypeParamCheck *next; } TypeParamCheck;
static void add_type_param_check(TypeParamCheck **checks,const char *func,const char *param,const char *type,int pos){ TypeParamCheck *c; if(!func||!*func||!param||!*param||!type||!*type) return; c=(TypeParamCheck*)xmalloc(sizeof(TypeParamCheck)); c->func=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<pcnt;i++){ strncpy(tmp,params[i],sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; p=trim_inplace(tmp); if(ci_eq(p,name)) return i; } return -1; }
static TypeParamCheck *collect_function_param_type_checks(FILE *f){ TypeParamCheck *checks=NULL; char line[4096]; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; 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,"TEACH")){ char teachline[4096]; char *fname=NULL; char **params=NULL; int pcnt=0; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; parse_teach_parts(teachline,&fname,&params,&pcnt); while(fgets(line,sizeof(line),f)){ char inner[4096],iup[64],iw[128]; char *it; strncpy(inner,line,sizeof(inner)-1); inner[sizeof(inner)-1]=0; it=trim_inplace(inner); if(is_blank_or_comment(it)) continue; first_word(it,iw,sizeof(iw)); upper_copy(iup,iw,sizeof(iup)); if(!strcmp(iup,"END")||!strcmp(iup,"LEARNED")) break; if(!strcmp(iup,"CHECK")&&starts_ci(it,"CHECK TYPE")){ const char *is=find_word_ci(it,"IS"); if(is){ char *expr=substr(it+10,is); char *need=xstrdup(trim_inplace((char*)is+2)); char *en=trim_inplace(expr); int idx=param_index_named(params,pcnt,en); if(idx>=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<ac;i++){ const char *param=NULL; const char *need=type_param_check_for(param_checks,trim_inplace(fname),i,&param); const char *got=simple_expr_type(types,parts[i]); if(need&&got&&!type_words_match(need,got)){ printf("%s:%d: Type mismatch for function %s: parameter %s needs %s, but this argument looks like %s.\n",path,line_no,trim_inplace(fname),param?param:"argument",need,got); errs++; } } free(fname); }
else if(!strcmp(up,"CHECK")&&starts_ci(t,"CHECK TYPE")){ const char *is=find_word_ci(t,"IS"); if(is){ char *expr=substr(t+10,is); char *need=xstrdup(trim_inplace((char*)is+2)); const char *got=simple_expr_type(types,expr); if(got&&!type_words_match(need,got)){ printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,trim_inplace(expr),got); errs++; } free(expr); free(need); } else { printf("%s:%d: CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER\n",path,line_no); errs++; } }
}
fclose(f); if(errs==0) printf("Type check OK\n"); return errs?1:0; }
+5
View File
@@ -0,0 +1,5 @@
TEACH square amount
CHECK TYPE amount IS NUMBER
SAY amount
END
DO square "oops"
+5
View File
@@ -0,0 +1,5 @@
TEACH square amount
CHECK TYPE amount IS NUMBER
SAY amount
END
DO square 4
+27
View File
@@ -16,8 +16,15 @@ EXPECTED = {
"tests/typecheck_container_bad.claro:3: Type mismatch for list names: expected TEXT item, but this value looks like NUMBER.",
"tests/typecheck_container_bad.claro:6: Type mismatch for map scores: expected NUMBER value, but this value looks like TEXT.",
],
"tests/typecheck_function_bad.claro": [
"tests/typecheck_function_bad.claro:5: Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT.",
],
}
EXPECTED_OK = [
"tests/typecheck_function_good.claro",
]
STALE_PHRASES = [
"was first set as NUMBER, but this looks like TEXT",
"name needs TEXT, but this looks like NUMBER",
@@ -52,6 +59,23 @@ def run_typecheck(path):
return result.stdout
def run_typecheck_ok(path):
result = subprocess.run(
[str(EXE), "typecheck", path],
cwd=ROOT,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
print("$", EXE.name, "typecheck", path)
print(result.stdout, end="")
if result.returncode != 0:
raise SystemExit(f"Expected {path} to pass typecheck")
lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
if lines != ["Type check OK"]:
raise SystemExit("Unexpected success output for " + path + "\nGot:\n" + "\n".join(lines))
def main():
if not EXE.exists():
raise SystemExit(f"Missing Claro executable: {EXE}")
@@ -59,6 +83,9 @@ def main():
for path in EXPECTED:
run_typecheck(path)
for path in EXPECTED_OK:
run_typecheck_ok(path)
print("Typecheck diagnostics validation complete")