typecheck: clarify missing CHECK TYPE IS

This commit is contained in:
Hermes Agent
2026-09-17 16:42:56 +00:00
parent 67ba1b1403
commit 1204731785
6 changed files with 16 additions and 2 deletions
+6
View File
@@ -141,6 +141,12 @@ SAY kind
CHECK TYPE score IS NUMBER
```
`CHECK TYPE` also gives a direct repair hint when `IS` is missing:
```text
CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER.
```
`CHECK TYPE` names a real Claro type. If a type name is misspelled, the checker explains the allowed beginner types instead of reporting a confusing value mismatch:
```text
+1
View File
@@ -51,6 +51,7 @@ Status: **Foundation present**
Ready now:
- typed variables such as `SET score NUMBER 10`
- `TYPE OF` and `CHECK TYPE`
- `CHECK TYPE` gives a direct repair hint when the learner forgets the `IS` keyword, for example `CHECK TYPE score` reports `CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER.`
- `CHECK TYPE` rejects unknown expected type names with a beginner-facing list of supported types
- typed list/map checks through `claro typecheck`, including a nested `LIST OF MAP` insertion example
- simple function and object-method return checks with `TEACH name ... RETURNS TYPE`, including learner-facing diagnostics for unknown declared return types, mismatched or not-yet-inferable `RETURN` expressions (including method-specific diagnostics naming the class and method, with modern and compatibility method coverage), empty `RETURN` statements, declarations that contain no `RETURN`, and declarations whose only return is inside an incomplete conditional; `RETURNS` is case-insensitive like other Claro keywords and has positive coverage in both modern and compatibility function forms, including lowercase compatibility declarations and lowercase compatibility method declarations, plus a lowercase compatibility-method mismatch diagnostic; `CHECK TYPE` parameter metadata also informs arithmetic return-expression checking, including method return expressions; positive function-return coverage includes numeric multiplication; compatibility methods now have dedicated positive numeric addition, subtraction, division, and multiplication return fixtures plus compatibility addition, subtraction, division, and multiplication diagnostic fixtures; NUMBER return diagnostics name a known TEXT operand and arithmetic operation instead of hiding the cause behind a generic mismatch, with focused addition, subtraction, multiplication, and division coverage for functions and methods; typed methods can also return class-declared fields by simple name, with positive and mismatch coverage in both modern and compatibility method syntax
+2
View File
@@ -71,6 +71,8 @@ Keep declared return types honest: `claro typecheck` now reports friendly diagno
8p. Keep object declarations understandable: a `NEW` statement with a class but no object name now gets a direct diagnostic with a complete `NEW Player player` repair example; a bare `NEW` now separately explains that both a class and object name are required.
8q. Keep `CHECK TYPE` syntax understandable: a missing `IS` now gets a direct repair hint with a complete `CHECK TYPE score IS NUMBER` example.
### 1. Strong static types
Goal: make larger beginner programs safer without making first scripts harder.
+2 -2
View File
@@ -372,7 +372,7 @@ static void exec_line(Runtime *rt,Program *p,int *pcp,const char *raw){ char buf
rt_error(rt,p->path,pc+1,"HTTP needs GET, STATUS, SAVE, or CHECK. Try: HTTP GET \"claro://hello\" AS page"); return; }
if(strcmp(up,"OBJECT")==0){ const char *as=find_word_ci(t,"AS"); if(as && starts_ci(t+6," CLASS")){ char *oe=substr(t+12,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value ov=eval_expr(rt,oe); const char *cn=object_class_name(ov); rt_set_checked(rt,p->path,pc+1,var,"TEXT",v_str(cn?cn:"")); free(oe); free(var); } else if(as && starts_ci(t+6," FIELDS")){ char *oe=substr(t+13,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value ov=eval_expr(rt,oe); Value arr=v_list(); int i; if(ov.type==V_MAP&&ov.map){ for(i=0;i<ov.map->count;i++){ if(strcmp(ov.map->keys[i],"class")!=0 && !starts_ci(ov.map->keys[i],"__type_")) list_add(arr.list,v_str(ov.map->keys[i])); } } rt_set_checked(rt,p->path,pc+1,var,"LIST",arr); free(oe); free(var); } else rt_error(rt,p->path,pc+1,"OBJECT needs CLASS or FIELDS. Try: OBJECT CLASS player AS kind"); return; }
if(strcmp(up,"TYPE")==0 && starts_ci(t,"TYPE OF")){ const char *as=find_word_ci(t,"AS"); if(as){ char *expr=substr(t+7,as); char *var=xstrdup(trim_inplace((char*)as+2)); char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); rt_set_checked(rt,p->path,pc+1,var,"TEXT",v_str(decl?decl:claro_value_type(v))); free(expr); free(var); } else rt_error(rt,p->path,pc+1,"TYPE OF needs AS. Try: TYPE OF score AS kind"); return; }
if(strcmp(up,"CHECK")==0 && starts_ci(t,"CHECK TYPE")){ const char *is=find_word_ci(t,"IS"); if(is){ char *expr=substr(t+10,is); char *ty=xstrdup(trim_inplace((char*)is+2)); char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); if(decl && !ci_eq(decl,ty) && !ci_eq(ty,"ANY")){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but %s is %s.",ty,trim_inplace(expr),decl); } else if(!decl && !claro_type_matches(ty,v)){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but got %s.",ty,claro_value_type(v)); } free(expr); free(ty); } else rt_error(rt,p->path,pc+1,"CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER"); return; }
if(strcmp(up,"CHECK")==0 && starts_ci(t,"CHECK TYPE")){ const char *is=find_word_ci(t,"IS"); if(is){ char *expr=substr(t+10,is); char *ty=xstrdup(trim_inplace((char*)is+2)); char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); if(decl && !ci_eq(decl,ty) && !ci_eq(ty,"ANY")){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but %s is %s.",ty,trim_inplace(expr),decl); } else if(!decl && !claro_type_matches(ty,v)){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but got %s.",ty,claro_value_type(v)); } free(expr); free(ty); } else rt_error(rt,p->path,pc+1,"CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER."); return; }
if(strcmp(up,"SET")==0){ char *rest=t+3; const char *to=find_word_ci(rest,"TO"); const char *as=find_word_ci(rest,"AS"); if(as&&to&&as<to){ char *name=substr(rest,as); char *type=substr(as+2,to); char *n=trim_inplace(name); char *ty=trim_inplace(type); Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,ty,v); free(name); free(type); }
else if(to){ char *name=substr(rest,to); char *n=trim_inplace(name); char *space=strchr(n,' '); const char *type=NULL; if(space){ *space=0; if(claro_is_type_word(trim_inplace(space+1))) type=trim_inplace(space+1); else { *space=' '; } } { Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,type,v); } free(name); }
else { const char *pcur=rest; char *name=unquote_token(&pcur); char *n=trim_inplace(name); const char *save=pcur; char *maybe=unquote_token(&pcur); char *ty=trim_inplace(maybe); if(claro_is_type_word(ty)){ char *after=trim_inplace((char*)pcur); Value v=*after?eval_expr(rt,after):claro_default_for_type(ty); if(*n) rt_set_checked(rt,p->path,pc+1,n,ty,v); } else { Value v=eval_expr(rt,save); if(*n) rt_set_checked(rt,p->path,pc+1,n,NULL,v); } free(maybe); free(name); } return; }
@@ -641,7 +641,7 @@ else if(!strcmp(up,"IF")){ if(in_teach){ if(nested_blocks<64){ branch_returned[n
else if(!strcmp(up,"NEW")){ const char *as=find_word_ci(t,"AS"); char *name=NULL; char *cls=NULL; char *extra=NULL; char objtype[256]; if(as){ const char *pcur=as+2; cls=substr(t+3,as); name=unquote_token(&pcur); extra=xstrdup(trim_inplace((char *)pcur)); } else { const char *pcur=t+3; cls=unquote_token(&pcur); name=unquote_token(&pcur); extra=xstrdup(trim_inplace((char *)pcur)); } snprintf(objtype,sizeof(objtype),"OBJECT:%s",trim_inplace(cls)); if(name&&*trim_inplace(name)){ TypeFieldCheck *fc; char fullname[256]; char *nt=trim_inplace(name); char *ct=trim_inplace(cls); int duplicate=0,class_known=0,oi,ci; if(extra&&*extra){ printf("%s:%d: NEW %s has extra text after object name %s. Keep only the class and object names.\n",path,line_no,ct,nt); errs++; } for(ci=0;ci<declared_class_count;ci++) if(ci_eq(declared_classes[ci],ct)){ class_known=1; break; } if(declared_class_count>0&&!class_known){ printf("%s:%d: Class %s is not known yet. Check the class name or declare CLASS %s before creating %s.\n",path,line_no,ct,ct,nt); errs++; } for(oi=0;oi<declared_object_count;oi++) if(ci_eq(declared_objects[oi],nt)){ duplicate=1; break; } if(duplicate){ printf("%s:%d: Object %s is created more than once. Give each object a different name.\n",path,line_no,nt); errs++; } else if(declared_object_count<128) snprintf(declared_objects[declared_object_count++],sizeof(declared_objects[0]),"%s",nt); type_env_set(&types,nt,objtype); for(fc=field_checks;fc;fc=fc->next){ if(ci_eq(fc->cls,ct)){ snprintf(fullname,sizeof(fullname),"%s.%s",nt,fc->field); type_env_set(&types,fullname,fc->type); } } } else if(cls&&*trim_inplace(cls)){ printf("%s:%d: NEW %s needs an object name. Add a name after the class, such as NEW %s player.\n",path,line_no,trim_inplace(cls),trim_inplace(cls)); errs++; } else { printf("%s:%d: NEW needs a class name. Add a class and object name, such as NEW Player player.\n",path,line_no); errs++; } free(cls); free(name); free(extra); }
else if((!strcmp(up,"DO")&&!find_word_ci(t,"TIMES"))||!strcmp(up,"CALL")){ char *fname=NULL; char **parts=NULL; int ac=0,i,is_method=0; char lookup[256],display[256]; if(!strcmp(up,"DO")){ const char *pcur=t+2; fname=unquote_token(&pcur); if(*trim_inplace((char*)pcur)) ac=split_args(pcur,&parts); } else { char *rest=t+4; const char *with=find_word_ci(rest,"WITH"); fname=with?substr(rest,with):xstrdup(rest); if(with&&*trim_inplace((char*)with+4)) ac=split_args(with+4,&parts); } snprintf(lookup,sizeof(lookup),"%s",trim_inplace(fname)); snprintf(display,sizeof(display),"%s",lookup); { char *dot=strchr(lookup,'.'); if(dot){ char objname[128]; const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-lookup),lookup); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ char method[128]; const char *cls=objtype+7; snprintf(method,sizeof(method),"%s",dot+1); if(!type_method_check_for(method_checks,cls,method)){ printf("%s:%d: Object %s has no method %s. Check the method name or add TEACH %s inside CLASS %s.\n",path,line_no,cls,method,method,cls); errs++; } snprintf(lookup,sizeof(lookup),"%s.%s",cls,method); snprintf(display,sizeof(display),"%s",lookup); is_method=1; }
else if(!objtype||(!starts_ci(objtype,"MODULE")&&(!strcmp(up,"DO")||type_method_name_exists(method_checks,dot+1)))){ printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before calling %s.\n",path,line_no,objname,objname,lookup); errs++; } } } for(i=0;i<ac;i++){ const char *param=NULL; const char *need=type_param_check_for(param_checks,lookup,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 %s %s: parameter %s needs %s, but this argument looks like %s.\n",path,line_no,is_method?"method":"function",display,param?param:"argument",need,got); errs++; } } { TypeParamCheck *pc=param_checks; while(pc){ if(ci_eq(pc->func,lookup)&&pc->pos>=ac){ printf("%s:%d: %s %s needs argument %s as %s, but this call does not provide it.\n",path,line_no,is_method?"Method":"Function",display,pc->param,pc->type); errs++; } pc=pc->next; } } { int expected=type_arity_check_for(arity_checks,lookup); if(expected>=0&&ac>expected){ printf("%s:%d: %s %s only accepts %d %s, but this call gives %d. Remove the extra %s.\n",path,line_no,is_method?"Method":"Function",display,expected,expected==1?"argument":"arguments",ac,ac-expected==1?"argument":"arguments"); errs++; } else if(expected>=0&&ac<expected){ int has_checked_missing=0; TypeParamCheck *mpc=param_checks; while(mpc){ if(ci_eq(mpc->func,lookup)&&mpc->pos>=ac){ has_checked_missing=1; break; } mpc=mpc->next; } if(!has_checked_missing){ printf("%s:%d: %s %s needs %d %s, but this call gives %d. Add the missing %s.\n",path,line_no,is_method?"Method":"Function",display,expected,expected==1?"argument":"arguments",ac,expected-ac==1?"argument":"arguments"); errs++; } } else if((!strcmp(up,"DO")||!strcmp(up,"CALL"))&&!is_method&&!strchr(lookup,'.')&&expected<0){ printf("%s:%d: Function %s is not known yet. Check the function name or add TEACH %s before calling it.\n",path,line_no,lookup,lookup); 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)); char *et=trim_inplace(expr); if(current_func[0]&&!strchr(et,'.')){ TypeParamCheck *pc; for(pc=param_checks;pc;pc=pc->next){ if(ci_eq(pc->func,current_func)&&ci_eq(pc->param,et)){ type_env_set(&types,et,pc->type); break; } } } const char *got=simple_expr_type(types,et); if(!claro_is_type_word(need)){ printf("%s:%d: CHECK TYPE needs a known type such as NUMBER, TEXT, YESNO, LIST, or MAP, but %s is not a Claro type.\n",path,line_no,need); errs++; } else if(got&&!type_words_match(need,got)){ const char *method_field=(current_method[0]&&current_class[0]&&!strchr(et,'.'))?type_field_check_for(field_checks,current_class,et):NULL; if(method_field){ printf("%s:%d: Type check failed in %s: expected %s, but %s looks like %s.\n",path,line_no,current_method,need,et,got); } else { printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,et,got); } errs++; } else if(!got&&current_method[0]&&current_class[0]&&!strchr(et,'.')&&!type_env_get(types,et)&&!type_field_check_for(field_checks,current_class,et)){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,current_class,et,et,need); errs++; } else if(!got&&strchr(et,'.')){ char objname[128],fieldname[128]; char *dot=strchr(et,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-et),et); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *declared=type_field_check_for(field_checks,objtype+7,fieldname); if(declared&&!type_words_match(need,declared)){ printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,et,declared); errs++; } else if(!declared){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,objtype+7,fieldname,fieldname,need); errs++; } } else if(!objtype){ printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before checking %s.\n",path,line_no,objname,objname,et); 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)); char *et=trim_inplace(expr); if(current_func[0]&&!strchr(et,'.')){ TypeParamCheck *pc; for(pc=param_checks;pc;pc=pc->next){ if(ci_eq(pc->func,current_func)&&ci_eq(pc->param,et)){ type_env_set(&types,et,pc->type); break; } } } const char *got=simple_expr_type(types,et); if(!claro_is_type_word(need)){ printf("%s:%d: CHECK TYPE needs a known type such as NUMBER, TEXT, YESNO, LIST, or MAP, but %s is not a Claro type.\n",path,line_no,need); errs++; } else if(got&&!type_words_match(need,got)){ const char *method_field=(current_method[0]&&current_class[0]&&!strchr(et,'.'))?type_field_check_for(field_checks,current_class,et):NULL; if(method_field){ printf("%s:%d: Type check failed in %s: expected %s, but %s looks like %s.\n",path,line_no,current_method,need,et,got); } else { printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,et,got); } errs++; } else if(!got&&current_method[0]&&current_class[0]&&!strchr(et,'.')&&!type_env_get(types,et)&&!type_field_check_for(field_checks,current_class,et)){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,current_class,et,et,need); errs++; } else if(!got&&strchr(et,'.')){ char objname[128],fieldname[128]; char *dot=strchr(et,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-et),et); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *declared=type_field_check_for(field_checks,objtype+7,fieldname); if(declared&&!type_words_match(need,declared)){ printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,et,declared); errs++; } else if(!declared){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,objtype+7,fieldname,fieldname,need); errs++; } } else if(!objtype){ printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before checking %s.\n",path,line_no,objname,objname,et); 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; }
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); }
+2
View File
@@ -0,0 +1,2 @@
SET score NUMBER 10
CHECK TYPE score
+3
View File
@@ -290,6 +290,9 @@ EXPECTED = {
"tests/typecheck_invalid_expected_type_bad.claro": [
"tests/typecheck_invalid_expected_type_bad.claro:2: CHECK TYPE needs a known type such as NUMBER, TEXT, YESNO, LIST, or MAP, but BANANA is not a Claro type.",
],
"tests/typecheck_missing_is_bad.claro": [
"tests/typecheck_missing_is_bad.claro:2: CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER.",
],
"tests/typecheck_invalid_return_type_bad.claro": [
"tests/typecheck_invalid_return_type_bad.claro:1: Function square declares an unknown return type BANANA. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.",
],