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 # 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 ## 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. - 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 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 ## Project and package workflow
v1.18.26 hardens Claro's project/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. 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 ## 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` - typed variables such as `SET score NUMBER 10`
- `TYPE OF` and `CHECK TYPE` - `TYPE OF` and `CHECK TYPE`
- typed list/map checks through `claro typecheck` - 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: Still needed:
- typed function parameters and return values - richer typed function signatures and return values
- type checking through branches and loops - type checking through branches and loops
- object method and field type checking - object method and field type checking
- typed imports/modules - 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. Goal: make larger beginner programs safer without making first scripts harder.
Needed next: Needed next:
- typed function parameters - richer typed function signatures
- typed function returns - typed function returns
- type checking across branches and loops - type checking across branches and loops
- typed imports/modules - 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 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 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 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,"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,"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,"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,"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,"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++; } } 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; } 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: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_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 = [ STALE_PHRASES = [
"was first set as NUMBER, but this looks like TEXT", "was first set as NUMBER, but this looks like TEXT",
"name needs TEXT, but this looks like NUMBER", "name needs TEXT, but this looks like NUMBER",
@@ -52,6 +59,23 @@ def run_typecheck(path):
return result.stdout 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(): def main():
if not EXE.exists(): if not EXE.exists():
raise SystemExit(f"Missing Claro executable: {EXE}") raise SystemExit(f"Missing Claro executable: {EXE}")
@@ -59,6 +83,9 @@ def main():
for path in EXPECTED: for path in EXPECTED:
run_typecheck(path) run_typecheck(path)
for path in EXPECTED_OK:
run_typecheck_ok(path)
print("Typecheck diagnostics validation complete") print("Typecheck diagnostics validation complete")