From 9beb0e167e0b86b162525d88adac8afc5f4de6c4 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 19 Sep 2026 18:14:52 +0000 Subject: [PATCH] typecheck: diagnose missing TYPE OF expressions --- docs/ADVANCED_STATIC_TYPING.md | 14 ++++++++++++++ docs/CURRENT_STATUS.md | 1 + docs/ROADMAP.md | 1 + src/claro.c | 2 +- tests/test_validate_typecheck_diagnostics.py | 6 ++++++ .../typecheck_missing_type_of_expression_bad.claro | 1 + tools/validate_typecheck_diagnostics.py | 3 +++ 7 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/typecheck_missing_type_of_expression_bad.claro diff --git a/docs/ADVANCED_STATIC_TYPING.md b/docs/ADVANCED_STATIC_TYPING.md index 30fd858..c71a36f 100644 --- a/docs/ADVANCED_STATIC_TYPING.md +++ b/docs/ADVANCED_STATIC_TYPING.md @@ -2,6 +2,20 @@ Claro v1.18.26 adds the first advanced static-typing foundation: typed containers checked by `claro typecheck`. +## Complete `TYPE OF` statements + +`TYPE OF` needs an expression, `AS`, and a result name. If the expression is missing, `claro typecheck` gives a repair example instead of silently creating the result variable: + +```claro +TYPE OF AS kind +``` + +```text +TYPE OF needs an expression before AS. Try: TYPE OF score AS kind. +``` + +Add the value whose type should be stored, such as `TYPE OF score AS kind`. + Class fields need a type after the field name. If a learner writes `HAS score` instead of `HAS score NUMBER`, `claro typecheck` explains the missing piece: ```text diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 8c6cd3a..af82925 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -60,6 +60,7 @@ Ready now: - `CHECK TYPE` rejects unknown expected type names with a beginner-facing list of supported types - typed function return diagnostics also identify an unknown return expression in compatibility `TAKES` / `LEARNED` functions, matching the modern function and object-method guidance - `TYPE OF` reports a direct repair hint when the learner forgets `AS`, for example `TYPE OF score` suggests `TYPE OF score AS kind` +- `TYPE OF` reports a direct repair hint when the learner forgets the expression before `AS`, for example `TYPE OF AS kind` reports `TYPE OF needs an expression before AS. Try: TYPE OF score AS kind.` - `TYPE OF` rejects extra words after its result name with a direct repair hint, so `TYPE OF score AS kind extra` explains that only one result name belongs there; typed `TEACH ... RETURNS TYPE` declarations likewise reject extra words after the return type with a direct repair hint - typed `TEACH ... RETURNS` declarations reject a missing return type with a direct repair hint, such as `Function greet needs a return type after RETURNS. Add a type such as NUMBER.` The same diagnostic names the complete class and method, including compatibility `TAKES` / `LEARNED` methods: `Method Player.score needs a return type after RETURNS. Add a type such as NUMBER.` - typed list/map checks through `claro typecheck`, including a nested `LIST OF MAP` insertion example diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ca81456..b4da557 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -79,6 +79,7 @@ Keep declared return types honest: `claro typecheck` now reports friendly diagno 8q.3. Keep `CHECK TYPE` declarations complete: a bare `CHECK TYPE` now explains that the expression, `IS`, and expected type are all required, with a complete repair example. 8q.4. Keep `CHECK TYPE` declarations ordered: `CHECK TYPE IS NUMBER` now explains that an expression belongs before `IS`, with a complete repair example. 8q.5. Keep `TYPE OF` declarations understandable: a missing `AS` now gets a direct repair hint with a complete `TYPE OF score AS kind` example. +8q.5.1. Keep `TYPE OF` declarations complete: a missing expression before `AS` now gets a direct repair hint with a complete `TYPE OF score AS kind` example. 8q.6. Keep `TYPE OF` declarations unambiguous: extra words after the result name now get a direct repair hint instead of being silently included in the variable name. 8q.7. Keep typed `TEACH` declarations unambiguous: extra words after a declared return type now get a direct repair hint instead of being treated as part of an unknown type. 8q.8. Keep typed `TEACH` declarations complete: a bare `RETURNS` now gets a direct repair hint naming the missing type and showing a supported example such as `NUMBER`. diff --git a/src/claro.c b/src/claro.c index f6f0448..1a5f9a8 100644 --- a/src/claro.c +++ b/src/claro.c @@ -652,7 +652,7 @@ else if(!strcmp(up,"IF")){ if(in_teach){ if(nested_blocks<64){ branch_returned[n 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,"IMPORT")){ const char *as=find_word_ci(t,"AS"); if(as){ const char *pcur=as+2; char *alias=unquote_token(&pcur); if(alias&&*trim_inplace(alias)) type_env_set(&types,trim_inplace(alias),"MODULE"); free(alias); } } - else if(!strcmp(up,"TYPE")&&starts_ci(t,"TYPE OF")){ const char *as=find_word_ci(t,"AS"); if(!as){ printf("%s:%d: TYPE OF needs AS. Try: TYPE OF score AS kind.\n",path,line_no); errs++; } else { char *expr=substr(t+7,as); const char *pcur=as+2; char *name=unquote_token(&pcur); char *extra=xstrdup(trim_inplace((char *)pcur)); if(extra&&*extra){ printf("%s:%d: TYPE OF %s has extra text after result name %s. Keep only the expression, AS, and one result name.\n",path,line_no,trim_inplace(expr),trim_inplace(name)); errs++; } else if(name&&*trim_inplace(name)) type_env_set(&types,name,"TEXT"); free(expr); free(name); free(extra); } } + else if(!strcmp(up,"TYPE")&&starts_ci(t,"TYPE OF")){ const char *as=find_word_ci(t,"AS"); if(!as){ printf("%s:%d: TYPE OF needs AS. Try: TYPE OF score AS kind.\n",path,line_no); errs++; } else { char *expr=substr(t+7,as); const char *pcur=as+2; char *name=unquote_token(&pcur); char *extra=xstrdup(trim_inplace((char *)pcur)); if(extra&&*extra){ printf("%s:%d: TYPE OF %s has extra text after result name %s. Keep only the expression, AS, and one result name.\n",path,line_no,trim_inplace(expr),trim_inplace(name)); errs++; } else if(!*trim_inplace(expr)){ printf("%s:%d: TYPE OF needs an expression before AS. Try: TYPE OF score AS kind.\n",path,line_no); errs++; } else if(name&&*trim_inplace(name)) type_env_set(&types,name,"TEXT"); free(expr); free(name); free(extra); } } 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;ci0&&!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;oinext){ 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;ifunc,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&&acfunc,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); } diff --git a/tests/test_validate_typecheck_diagnostics.py b/tests/test_validate_typecheck_diagnostics.py index 3d00e06..7a5d1a8 100644 --- a/tests/test_validate_typecheck_diagnostics.py +++ b/tests/test_validate_typecheck_diagnostics.py @@ -89,6 +89,12 @@ class ValidateTypecheckDiagnosticsTests(unittest.TestCase): MODULE.EXPECTED, ) + def test_includes_missing_type_of_expression_fixture(self): + self.assertIn( + "tests/typecheck_missing_type_of_expression_bad.claro", + MODULE.EXPECTED, + ) + def test_includes_lowercase_compatibility_return_success_fixture(self): self.assertIn( "tests/typecheck_function_lowercase_return_good.claro", diff --git a/tests/typecheck_missing_type_of_expression_bad.claro b/tests/typecheck_missing_type_of_expression_bad.claro new file mode 100644 index 0000000..6a037db --- /dev/null +++ b/tests/typecheck_missing_type_of_expression_bad.claro @@ -0,0 +1 @@ +TYPE OF AS kind diff --git a/tools/validate_typecheck_diagnostics.py b/tools/validate_typecheck_diagnostics.py index 67957bc..5cf2819 100644 --- a/tools/validate_typecheck_diagnostics.py +++ b/tools/validate_typecheck_diagnostics.py @@ -7,6 +7,9 @@ ROOT = Path(__file__).resolve().parents[1] EXE = ROOT / ("claro.exe" if os.name == "nt" else "claro") EXPECTED = { + "tests/typecheck_missing_type_of_expression_bad.claro": [ + "tests/typecheck_missing_type_of_expression_bad.claro:1: TYPE OF needs an expression before AS. Try: TYPE OF score AS kind.", + ], "tests/typecheck_function_duplicate_param_bad.claro": [ "tests/typecheck_function_duplicate_param_bad.claro:1: Function greet declares parameter name more than once. Give each parameter a different name.", ],