typecheck: diagnose missing checked function args

This commit is contained in:
Hermes Agent
2026-09-01 13:21:54 +00:00
parent a9612c5074
commit 48189afd80
7 changed files with 31 additions and 4 deletions
+2 -1
View File
@@ -132,7 +132,7 @@ 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 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 or a missing checked argument get a friendly error:
```claro ```claro
TEACH square amount TEACH square amount
@@ -145,6 +145,7 @@ DO square "oops"
```text ```text
Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT. Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT.
Function square needs argument amount as NUMBER, but this call does not provide it.
``` ```
For functions with more than one checked parameter, Claro reports each mismatched argument with the parameter name: For functions with more than one checked parameter, Claro reports each mismatched argument with the parameter name:
+17
View File
@@ -73,6 +73,23 @@ Output:
Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT. Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT.
``` ```
If the learner forgets a checked argument, `claro typecheck` now names the missing parameter and expected type:
```claro
TEACH square amount
CHECK TYPE amount IS NUMBER
SAY amount
END
DO square
```
Output:
```text
Function square needs argument amount as NUMBER, but this call does not provide it.
```
Multiple checked parameters are reported separately, so a learner can fix each argument one at a time: Multiple checked parameters are reported separately, so a learner can fix each argument one at a time:
```claro ```claro
+1 -1
View File
@@ -52,7 +52,7 @@ 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/method argument check: `CHECK TYPE parameter IS TYPE` inside a function or simple object method lets `claro typecheck` accept correct checked calls, catch mismatched `DO`, `CALL ... WITH`, and `DO object.method ...` arguments, explain when `DO object.method ...` or compatibility `CALL object.method WITH ...` happens before the object is created with `NEW` even if the method name is also wrong, and catch simple calls to undeclared methods with a class-specific `TEACH` hint - a narrow function/method argument check: `CHECK TYPE parameter IS TYPE` inside a function or simple object method lets `claro typecheck` accept correct checked calls, catch mismatched `DO`, `CALL ... WITH`, and `DO object.method ...` arguments, report missing checked function arguments, explain when `DO object.method ...` or compatibility `CALL object.method WITH ...` happens before the object is created with `NEW` even if the method name is also wrong, and catch simple calls to undeclared methods with a class-specific `TEACH` hint
- a narrow object-field assignment/check-type check for simple `NEW Class object` plus direct `SET object.field value` and `CHECK TYPE object.field IS TYPE` cases when the class declares `HAS field TYPE`; validation now covers correct NUMBER, TEXT, and YESNO direct assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field `CHECK TYPE` metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type diagnostics, NUMBER/TEXT/YESNO-valued unknown-field diagnostics for direct assignments to undeclared fields, plus a plain beginner-facing unknown-field diagnostic when the assigned expression type is not inferable yet - a narrow object-field assignment/check-type check for simple `NEW Class object` plus direct `SET object.field value` and `CHECK TYPE object.field IS TYPE` cases when the class declares `HAS field TYPE`; validation now covers correct NUMBER, TEXT, and YESNO direct assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field `CHECK TYPE` metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type diagnostics, NUMBER/TEXT/YESNO-valued unknown-field diagnostics for direct assignments to undeclared fields, plus a plain beginner-facing unknown-field diagnostic when the assigned expression type is not inferable yet
Still needed: Still needed:
+1 -1
View File
@@ -31,7 +31,7 @@ See `CURRENT_STATUS.md` for the detailed feature matrix.
1. Keep beginner-facing docs current and separate from historical release notes. 1. Keep beginner-facing docs current and separate from historical release notes.
2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately. 2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately.
3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, and CI workflow coverage validation). Current package safety validation covers safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, and refusing to write lockfile data for unsafe package names during `package lock`. Current object-method parameter validation covers one correct `DO object.method ...` call, one wrong-type diagnostic, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and a class-specific unknown-method diagnostic when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type direct assignments, simple NUMBER/TEXT/YESNO-valued unknown-field diagnostics after `NEW Class object`, and a beginner-facing fallback when the unknown field's assigned expression type is not inferable yet. 3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, and CI workflow coverage validation). Current package safety validation covers safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, and refusing to write lockfile data for unsafe package names during `package lock`. Current function validation covers correct checked calls, wrong-type arguments, and a missing checked-argument diagnostic; current object-method parameter validation covers one correct `DO object.method ...` call, one wrong-type diagnostic, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and a class-specific unknown-method diagnostic when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type direct assignments, simple NUMBER/TEXT/YESNO-valued unknown-field diagnostics after `NEW Class object`, and a beginner-facing fallback when the unknown field's assigned expression type is not inferable yet.
4. Add small examples for each foundation feature before adding bigger syntax. 4. Add small examples for each foundation feature before adding bigger syntax.
## Complete-platform milestones ## Complete-platform milestones
+1 -1
View File
@@ -556,7 +556,7 @@ static int typecheck_file(const char *path){ FILE *f=fopen(path,"rb"); char line
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; char *cls=NULL; char objtype[256]; if(as){ cls=substr(t+3,as); name=xstrdup(trim_inplace((char*)as+2)); } else { const char *pcur=t+3; cls=unquote_token(&pcur); name=unquote_token(&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); 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); } } } free(cls); free(name); } else if(!strcmp(up,"NEW")){ const char *as=find_word_ci(t,"AS"); char *name=NULL; char *cls=NULL; char objtype[256]; if(as){ cls=substr(t+3,as); name=xstrdup(trim_inplace((char*)as+2)); } else { const char *pcur=t+3; cls=unquote_token(&pcur); name=unquote_token(&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); 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); } } } free(cls); free(name); }
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) 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((!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) 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++; } } free(fname); } 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; } } 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); const char *got=simple_expr_type(types,et); 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,et,got); 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:")&&!type_field_check_for(field_checks,objtype+7,fieldname)){ 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); const char *got=simple_expr_type(types,et); 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,et,got); 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:")&&!type_field_check_for(field_checks,objtype+7,fieldname)){ 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; } fclose(f); if(errs==0) printf("Type check OK\n"); return errs?1:0; }
@@ -0,0 +1,6 @@
TEACH square amount
CHECK TYPE amount IS NUMBER
SAY amount
END
DO square
+3
View File
@@ -23,6 +23,9 @@ EXPECTED = {
"tests/typecheck_function_multi_bad.claro:8: Type mismatch for function label: parameter name needs TEXT, but this argument looks like NUMBER.", "tests/typecheck_function_multi_bad.claro:8: Type mismatch for function label: parameter name needs TEXT, but this argument looks like NUMBER.",
"tests/typecheck_function_multi_bad.claro:8: Type mismatch for function label: parameter age needs NUMBER, but this argument looks like TEXT.", "tests/typecheck_function_multi_bad.claro:8: Type mismatch for function label: parameter age needs NUMBER, but this argument looks like TEXT.",
], ],
"tests/typecheck_function_missing_arg_bad.claro": [
"tests/typecheck_function_missing_arg_bad.claro:6: Function square needs argument amount as NUMBER, but this call does not provide it.",
],
"tests/typecheck_method_bad.claro": [ "tests/typecheck_method_bad.claro": [
"tests/typecheck_method_bad.claro:11: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.", "tests/typecheck_method_bad.claro:11: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.",
], ],