diff --git a/README.md b/README.md index 33637c4..7d9fc0c 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,12 @@ CHECK TYPE score IS NUMBER TEXT CHECK TYPE score has extra text after type NUMBER. Keep only the expression, IS, and one type. ``` +If `RETURNS` is present without a type, `claro typecheck` explains what is missing and shows a beginner-friendly repair: + +```text +Function greet needs a return type after RETURNS. Add a type such as 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 diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index dd211c6..c8af82b 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -59,6 +59,7 @@ Ready now: - `CHECK TYPE` rejects unknown expected type names with a beginner-facing list of supported types - `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` 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.` - 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 - 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`, `DO object.method ...`, and compatibility `CALL object.method WITH ...` arguments, including checked methods that appear after another method in the same class, report missing checked function and method arguments, report missing unchecked arguments for simple functions and simple object methods in both modern `DO` and compatibility `CALL ... WITH` forms, treat empty compatibility calls such as `CALL greet WITH` and `CALL player.rename WITH` as missing-argument mistakes, catch extra arguments to simple functions and checked methods even when the checked method appears after another method in the same class, catch modern `DO` and compatibility `CALL ... WITH` calls to undeclared simple functions, explain when `DO object.method ...` or compatibility `CALL object.method ...` happens before the object is created with `NEW` even if the method name is also wrong, and catch modern plus compatibility calls to undeclared object methods with a class-specific `TEACH` hint diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c768460..801a98b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -81,6 +81,7 @@ Keep declared return types honest: `claro typecheck` now reports friendly diagno 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.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`. ### 1. Strong static types diff --git a/src/claro.c b/src/claro.c index f061919..428ae12 100644 --- a/src/claro.c +++ b/src/claro.c @@ -627,7 +627,7 @@ static TypeFieldCheck *collect_class_field_type_checks(FILE *f){ static TypeMethodCheck *collect_class_method_type_checks(FILE *f){ TypeMethodCheck *checks=NULL; char line[4096]; char current_class[128]=""; 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,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(current_class[0]&&!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,¶ms,&pcnt); add_type_method_check(&checks,current_class,trim_inplace(fname)); 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; } } } 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; TypeFieldCheck *field_checks=NULL; TypeMethodCheck *method_checks=NULL; TypeArityCheck *arity_checks=NULL; TypeReturnCheck *return_checks=NULL; char current_func[256]=""; char current_method[256]=""; char current_class[128]=""; char declared_classes[128][256]; char declared_functions[128][256]; char declared_methods[128][256]; char declared_fields[128][256]; char declared_objects[128][256]; int declared_class_count=0,declared_function_count=0,declared_method_count=0,declared_field_count=0,declared_object_count=0; int in_teach=0,return_seen=0,top_level_return_seen=0,nested_blocks=0,conditional_paths_complete=1; int branch_returned[64],branch_has_else[64]; if(!f){ fprintf(stderr,"Could not open %s\n",path); return 1; } param_checks=collect_function_param_type_checks(f); arity_checks=collect_function_arity_checks(f); return_checks=collect_function_return_type_checks(f); field_checks=collect_class_field_type_checks(f); method_checks=collect_class_method_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,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); char *name=trim_inplace(cls); char *extra=trim_inplace((char *)pcur); int duplicate=0,ci; for(ci=0;cinext) if(ci_eq(fc->cls,current_class)) type_env_set(&types,fc->field,fc->type); } { const char *declared_return=type_return_check_for(return_checks,full_name); if(declared_return&&!claro_is_type_word(declared_return)){ printf("%s:%d: %s %s declares an unknown return type %s. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.\n",path,line_no,strchr(full_name,'.')?"Method":"Function",full_name,declared_return); errs++; } else if(declared_return) snprintf(current_func,sizeof(current_func),"%s",full_name); } } +else if(!strcmp(up,"TEACH")){ char teachline[4096],full_name[256],*fname=NULL,**params=NULL; int pcnt=0; int duplicate_method=0; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; parse_teach_parts(teachline,&fname,¶ms,&pcnt); { const char *returns=find_word_ci(t,"RETURNS"); if(returns){ const char *rp=returns+7; char ret[128],first_ret[128],*extra; while(*rp&&isspace((unsigned char)*rp)) rp++; snprintf(ret,sizeof(ret),"%s",rp); if(sscanf(ret,"%127s",first_ret)==1){ extra=trim_inplace((char *)rp+strlen(first_ret)); if(*extra){ printf("%s:%d: %s %s has extra text after return type %s. Keep only the function name, parameters, and one return type.\n",path,line_no,current_class[0]?"Method":"Function",fname,first_ret); errs++; } } else { printf("%s:%d: %s %s needs a return type after RETURNS. Add a type such as NUMBER.\n",path,line_no,current_class[0]?"Method":"Function",fname); errs++; } } } if(!fname||!*trim_inplace(fname)){ if(current_class[0]) printf("%s:%d: Method %s needs a method name. Add a name after TEACH, such as TEACH show.\n",path,line_no,current_class); else printf("%s:%d: TEACH needs a function name. Add a name after TEACH, such as TEACH greet.\n",path,line_no); errs++; } if(!fname) fname=xstrdup(""); if(current_class[0]) snprintf(full_name,sizeof(full_name),"%s.%s",current_class,fname); else snprintf(full_name,sizeof(full_name),"%s",fname); if(current_class[0]){ int mi; for(mi=0;minext) if(ci_eq(fc->cls,current_class)) type_env_set(&types,fc->field,fc->type); } { const char *declared_return=type_return_check_for(return_checks,full_name); if(declared_return&&!claro_is_type_word(declared_return)){ printf("%s:%d: %s %s declares an unknown return type %s. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.\n",path,line_no,strchr(full_name,'.')?"Method":"Function",full_name,declared_return); errs++; } else if(declared_return) snprintf(current_func,sizeof(current_func),"%s",full_name); } } else if(!strcmp(up,"IF")){ if(in_teach){ if(nested_blocks<64){ branch_returned[nested_blocks]=0; branch_has_else[nested_blocks]=0; } nested_blocks++; } } else if(!strcmp(up,"ELSE")){ if(in_teach&&nested_blocks>0&&nested_blocks<=64){ if(!branch_returned[nested_blocks-1]) conditional_paths_complete=0; branch_has_else[nested_blocks-1]=1; branch_returned[nested_blocks-1]=0; } } else if(!strcmp(up,"END")||!strcmp(up,"LEARNED")||!strcmp(up,"ENDIF")){ if(in_teach&&nested_blocks>0){ int nested_complete=(nested_blocks<=64&&branch_has_else[nested_blocks-1]&&branch_returned[nested_blocks-1]); if(nested_blocks<=64&&(!branch_has_else[nested_blocks-1]||!branch_returned[nested_blocks-1])) conditional_paths_complete=0; nested_blocks--; if(nested_complete&&nested_blocks>0&&nested_blocks<=64) branch_returned[nested_blocks-1]=1; } else if(in_teach){ if(current_func[0]&&!return_seen){ const char *need=type_return_check_for(return_checks,current_func); printf("%s:%d: %s %s declares RETURNS %s but has no RETURN statement. Add RETURN with a %s value.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need,need); errs++; } else if(current_func[0]&&(!top_level_return_seen&&!conditional_paths_complete)){ const char *need=type_return_check_for(return_checks,current_func); printf("%s:%d: %s %s declares RETURNS %s but does not return a %s value on every path. Add RETURN to each branch or after the conditional.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need,need); errs++; } in_teach=0; current_func[0]=0; current_method[0]=0; } else current_class[0]=0; } diff --git a/tests/typecheck_missing_return_type_bad.claro b/tests/typecheck_missing_return_type_bad.claro new file mode 100644 index 0000000..e87fd82 --- /dev/null +++ b/tests/typecheck_missing_return_type_bad.claro @@ -0,0 +1,3 @@ +TEACH greet RETURNS + RETURN 1 +END \ No newline at end of file diff --git a/tools/validate_typecheck_diagnostics.py b/tools/validate_typecheck_diagnostics.py index a185da7..11027ce 100644 --- a/tools/validate_typecheck_diagnostics.py +++ b/tools/validate_typecheck_diagnostics.py @@ -311,6 +311,9 @@ EXPECTED = { "tests/typecheck_extra_teach_tokens_bad.claro": [ "tests/typecheck_extra_teach_tokens_bad.claro:1: Function greet has extra text after return type NUMBER. Keep only the function name, parameters, and one return type.", ], + "tests/typecheck_missing_return_type_bad.claro": [ + "tests/typecheck_missing_return_type_bad.claro:1: Function greet needs a return type after RETURNS. Add a type such as NUMBER.", + ], "tests/typecheck_extra_check_type_tokens_bad.claro": [ "tests/typecheck_extra_check_type_tokens_bad.claro:2: CHECK TYPE score has extra text after type NUMBER. Keep only the expression, IS, and one type.", ],