From 139838d56ec40f8df6181cea925b7f225747c77c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 16 Sep 2026 13:50:00 +0000 Subject: [PATCH] typecheck: diagnose unknown NEW classes --- README.md | 6 ++++++ docs/ADVANCED_STATIC_TYPING.md | 8 ++++++++ docs/CURRENT_STATUS.md | 2 +- docs/ROADMAP.md | 2 ++ src/claro.c | 2 +- tests/typecheck_unknown_class_bad.claro | 4 ++++ tools/validate_typecheck_diagnostics.py | 3 +++ 7 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/typecheck_unknown_class_bad.claro diff --git a/README.md b/README.md index c5c896c..01748bf 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,12 @@ OBJECT CLASS player AS kind OBJECT FIELDS player AS fields ``` +When a script already declares classes, `claro typecheck` also catches a typo in a `NEW` class name and explains how to repair it: + +```text +Class Plaeyr is not known yet. Check the class name or declare CLASS Plaeyr before creating player. +``` + Inside a method, an inline field type must agree with the class declaration. For example, `HAS score NUMBER` must not be assigned with `SET score TEXT 10`; `claro typecheck` explains the conflict and suggests `NUMBER`. The same check applies to older `TEACH ... TAKES ...` / `LEARNED` methods, so compatibility lessons get the same feedback. If an inline method-field annotation is not a Claro type, `claro typecheck` names the field and method and suggests the class declaration. For example, `SET score AS BANANA TO 10` reports that `BANANA` is unknown and recommends `NUMBER`. diff --git a/docs/ADVANCED_STATIC_TYPING.md b/docs/ADVANCED_STATIC_TYPING.md index 2b47710..8dbf138 100644 --- a/docs/ADVANCED_STATIC_TYPING.md +++ b/docs/ADVANCED_STATIC_TYPING.md @@ -715,6 +715,14 @@ Output: Object Player has no method fly. Check the method name or add TEACH fly inside CLASS Player. ``` +When a script declares classes, `NEW` also checks that the requested class exists. A typo such as `NEW Plaeyr player` receives a repair hint: + +```text +Class Plaeyr is not known yet. Check the class name or declare CLASS Plaeyr before creating player. +``` + +For backward compatibility, a script with no `CLASS` declarations keeps the older permissive `NEW` behavior. + ## Object field assignment checks Claro also has a narrow static diagnostic for direct object-field assignments. If a class declares a typed field and a script creates a simple object with `NEW Class name`, `claro typecheck` remembers the field type: diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 0e20503..0d2879c 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -58,7 +58,7 @@ Ready now: - function and method declarations now reject repeated parameter names with a direct repair hint before calls are checked, including modern and compatibility `TAKES` / `LEARNED` functions and methods; object methods with the same name are also rejected within a class with a rename hint in both modern and compatibility method syntax - class declarations now reject repeated field names within the same class with a direct repair hint before field types are used, so two `HAS score ...` lines cannot silently choose conflicting metadata; different classes may independently use the same field name - class declarations now reject repeated class names with a direct repair hint, so two `CLASS Player` blocks cannot silently compete for the same name -- object creation now rejects repeated object names with a direct repair hint, so two `NEW Player player` statements cannot silently replace one another during type checking +- object creation now rejects repeated object names with a direct repair hint, so two `NEW Player player` statements cannot silently replace one another during type checking; when a script declares at least one class, a misspelled `NEW` class name gets a direct declaration hint without changing the older permissive no-class form - 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, explicitly typed field assignments, explicitly typed method-body assignments to undeclared fields with a `HAS field TYPE` repair hint in modern and compatibility method syntax, dedicated positive NUMBER/TEXT/YESNO `CHECK TYPE` metadata fixtures, simple and chained object aliases for both field assignments and `CHECK TYPE` metadata (including a chained TEXT-field check), aliased object-method calls including chained aliases in modern `DO` and compatibility `CALL ... WITH` forms (with a dedicated positive modern `DO` chained-alias fixture), field-to-field, compound arithmetic field expressions, and arithmetic/text expression result types, 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, field collection when a `HAS` field appears after a simple method, NUMBER/TEXT/YESNO-valued unknown-field diagnostics for direct assignments to undeclared fields including explicitly typed assignments, numeric and text results from simple arithmetic/text expressions in object-field assignments, plus method-body field assignment and `CHECK TYPE` diagnostics for NUMBER, TEXT, and YESNO fields in modern and compatibility syntax, including compatibility YESNO `CHECK TYPE` metadata coverage, direct method assignments to undeclared fields now produce a beginner-facing missing-field diagnostic when the assigned expression has a known type, and a plain beginner-facing unknown-field diagnostic when the assigned expression is not inferable yet in both modern and compatibility method syntax; method-body `CHECK TYPE` now reports an undeclared bare field with the expected type as a repair hint in both modern and compatibility method syntax, including compatibility `TAKES` / ...; explicitly typed assignments to declared method fields now validate the class-declared field type instead of trusting only the inline type annotation, so a wrong value such as `SET score TEXT "oops"` reports the method and field in the diagnostic Still needed: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 67f303c..b4d10e6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -58,6 +58,8 @@ Keep declared return types honest: `claro typecheck` now reports friendly diagno 8k. Keep object names unambiguous: repeated `NEW Player player` statements now get a declaration-time diagnostic with a rename hint, preventing one object type environment from silently replacing another. +8l. Keep object creation understandable: when classes are declared in a script, an unknown `NEW` class name now gets a direct class-name diagnostic and repair hint; the older permissive `NEW` behavior remains when no class declarations are present. + ### 1. Strong static types Goal: make larger beginner programs safer without making first scripts harder. diff --git a/src/claro.c b/src/claro.c index d36d031..1a163ac 100644 --- a/src/claro.c +++ b/src/claro.c @@ -638,7 +638,7 @@ else if(!strcmp(up,"IF")){ if(in_teach){ if(nested_blocks<64){ branch_returned[n 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")){ 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); int duplicate=0,oi; 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); } } } 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); int duplicate=0,class_known=0,oi,ci; 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); } } } 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&&*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); } 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]&¤t_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&¤t_method[0]&¤t_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++; } } diff --git a/tests/typecheck_unknown_class_bad.claro b/tests/typecheck_unknown_class_bad.claro new file mode 100644 index 0000000..81e06ce --- /dev/null +++ b/tests/typecheck_unknown_class_bad.claro @@ -0,0 +1,4 @@ +CLASS Player +END + +NEW Plaeyr player diff --git a/tools/validate_typecheck_diagnostics.py b/tools/validate_typecheck_diagnostics.py index aabcc57..eabed4f 100644 --- a/tools/validate_typecheck_diagnostics.py +++ b/tools/validate_typecheck_diagnostics.py @@ -34,6 +34,9 @@ EXPECTED = { "tests/typecheck_duplicate_object_bad.claro": [ "tests/typecheck_duplicate_object_bad.claro:6: Object player is created more than once. Give each object a different name.", ], + "tests/typecheck_unknown_class_bad.claro": [ + "tests/typecheck_unknown_class_bad.claro:4: Class Plaeyr is not known yet. Check the class name or declare CLASS Plaeyr before creating player.", + ], "tests/typecheck_bad.claro": [ "tests/typecheck_bad.claro:2: Type mismatch for score: it was first set as NUMBER, but this value looks like TEXT.", "tests/typecheck_bad.claro:3: Type mismatch for name: expected TEXT, but this value looks like NUMBER.",