From 6be50880003cbb4e8091e5743a9ba73205a97e2a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 20 Sep 2026 21:07:29 +0000 Subject: [PATCH] typecheck: clarify unknown object field annotations --- docs/CURRENT_STATUS.md | 2 +- docs/ROADMAP.md | 2 +- src/claro.c | 2 +- tests/typecheck_object_field_typed_unknown_type_bad.claro | 6 ++++++ tools/validate_typecheck_diagnostics.py | 3 +++ 5 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/typecheck_object_field_typed_unknown_type_bad.claro diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index f9e6b62..9e90b6e 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -83,7 +83,7 @@ Ready now: - 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 missing class name or object name gets a direct example repair; extra words after the object name get a direct repair hint instead of being silently ignored - 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, dedicated positive and explicitly typed NUMBER/TEXT 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 unknown field with a repair hint in both method syntaxes, including compatibility `TAKES` / `LEARNED` methods; 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. - inline method-field annotations are checked against the class `HAS` declaration in modern and compatibility methods; conflicting known types and unknown annotation names produce field-specific repair guidance, with positive and negative focused fixtures for both syntaxes. -- Direct object-field validation also has positive coverage for explicit `NUMBER`, `TEXT`, and `YESNO` annotations, such as `SET player.ready YESNO YES`, while the shorter unannotated form remains supported; a conflicting annotation such as `SET player.score TEXT "oops"` now explains the class-declared type and how to repair it. +- Direct object-field validation also has positive coverage for explicit `NUMBER`, `TEXT`, and `YESNO` annotations, such as `SET player.ready YESNO YES`, while the shorter unannotated form remains supported; conflicting annotations such as `SET player.score TEXT "oops"` explain the class-declared type and how to repair it, and unknown annotations such as `SET player.score AS BANANA TO 10` identify the invalid type and suggest the declared field type. Still needed: - Keep the focused typecheck validator's fixture list complete as new positive and negative examples are added. `claro validate` now executes the complete focused fixture matrix as well, so release validation cannot silently omit a listed typecheck example. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d7859c8..f5c3380 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -48,7 +48,7 @@ Keep declared return types honest: `claro typecheck` now reports friendly diagno 8a. Keep method-body text expressions balanced: modern TEXT field concatenation is covered when the field is on either side of `+`, so both common beginner word-order patterns remain accepted. 8b. Keep compatibility method-body text expressions aligned: `TAKES` / `LEARNED` methods now have positive TEXT concatenation coverage in both operand orders beside the modern `TEACH` / `END` examples. 8c. Keep explicit method-field annotations aligned with class declarations: `SET score TEXT "oops"` inside a method with `HAS score NUMBER` now reports the declared field mismatch instead of accepting the inline annotation; broader annotation consistency remains planned. -8d. Keep inline field annotations consistent with `HAS` declarations: `claro typecheck` now rejects conflicting annotations on method-body and direct object-field assignments, and explains which type to use even when the assigned value itself would otherwise look valid. +8d. Keep inline field annotations consistent with `HAS` declarations: `claro typecheck` now rejects conflicting annotations on method-body and direct object-field assignments, explains which type to use, and gives a known-type repair hint for unknown direct object-field annotations. 8e. Keep inline method-field annotation coverage aligned across syntax generations: compatibility `TAKES` / `LEARNED` methods now have matching positive and negative fixtures, so older lessons retain the same class-declared-type guidance. 8f. Keep inline method-field annotations learner-facing: an unknown annotation such as `BANANA` now names the field and method and suggests the `HAS` type instead of silently treating the annotation as an expression. 8g. Keep unknown inline method-field annotation diagnostics aligned across syntax generations: compatibility `TAKES` / `LEARNED` methods now have matching focused coverage for misspelled annotations. diff --git a/src/claro.c b/src/claro.c index b7160fd..1a3fb60 100644 --- a/src/claro.c +++ b/src/claro.c @@ -647,7 +647,7 @@ else if(!strcmp(up,"IF")){ if(in_teach){ if(nested_blocks<64){ branch_returned[n 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; } else if(!strcmp(up,"RETURN")&¤t_func[0]){ const char *need=type_return_check_for(return_checks,current_func); char *return_expr=trim_inplace(t+6); const char *got=simple_expr_type(types,return_expr); return_seen=1; if(nested_blocks==0) top_level_return_seen=1; else if(nested_blocks<=64) branch_returned[nested_blocks-1]=1; if(need&&!*return_expr){ printf("%s:%d: %s %s needs a %s value after RETURN. Add a %s expression.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need,need); errs++; } else if(need&&!got){ char first_value[128]; if(return_value_extra_tokens(return_expr,first_value,sizeof(first_value))){ printf("%s:%d: %s %s has extra text after return value %s. Keep only one expression after RETURN.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,first_value); } else { printf("%s:%d: %s %s's return value could not be understood yet. Use a %s expression after RETURN.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need); } errs++; } else if(need&&got&&!type_words_match(need,got)){ char text_operator=(ci_eq(need,"NUMBER")&&ci_eq(got,"TEXT"))?text_operator_for_number_return(types,return_expr):0; const char *text_operand=text_operator?text_operand_for_operator(types,return_expr,text_operator):NULL; if(text_operand){ printf("%s:%d: Type mismatch for return from %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,current_func,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); } else { printf("%s:%d: Type mismatch for return from %s: expected %s, but this value looks like %s.\n",path,line_no,current_func,need,got); } errs++; } } - else if(!strcmp(up,"SET")){ char *name=NULL,*type=NULL,*expr=NULL; const char *got,*old; parse_set_for_typecheck2(t,&name,&type,&expr); if(name&&*trim_inplace(name)){ char *nt=trim_inplace(name); char *tt=type?trim_inplace(type):NULL; got=simple_expr_type(types,expr); if(current_method[0]&¤t_class[0]&&!strchr(nt,'.')&&!type_env_get(types,nt)&&!type_field_check_for(field_checks,current_class,nt)){ if(got){ 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,nt,nt,got); } else { printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,current_class,nt); } errs++; free(name); free(type); free(expr); continue; } old=type_env_get(types,nt); if(tt&&*tt){ if(current_method[0]&¤t_class[0]&&!strchr(nt,'.')){ const char *declared=type_field_check_for(field_checks,current_class,nt); if(declared&&tt&&!claro_is_type_word(tt)){ printf("%s:%d: Field %s in %s needs a known type such as NUMBER, TEXT, or YESNO, but %s is not a Claro type. Use %s for %s.\n",path,line_no,nt,current_method,tt,declared,nt); errs++; got=NULL; } else if(declared&&tt&&!type_words_match(declared,tt)){ printf("%s:%d: Type mismatch for field %s in %s: class declares %s, but this assignment says %s. Use %s for %s.\n",path,line_no,nt,current_method,declared,tt,declared,nt); errs++; got=NULL; } else if(declared&&got&&!type_words_match(declared,got)){ printf("%s:%d: Type mismatch for field %s in %s: expected %s, but this value looks like %s.\n",path,line_no,nt,current_method,declared,got); errs++; } } if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); 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&&tt&&!type_words_match(declared,tt)){ printf("%s:%d: Type mismatch for field %s: class declares %s, but this assignment says %s. Use %s for %s.\n",path,line_no,nt,declared,tt,declared,fieldname); errs++; } else if(declared&&got&&!type_words_match(declared,got)){ char text_operator=(ci_eq(declared,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand){ printf("%s:%d: Type mismatch for field %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); } else { printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,declared,got); } 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,tt); errs++; } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } type_env_set(&types,nt,tt); } else if(old){ if(got&&!type_words_match(old,got)){ if(strchr(nt,'.')){ char text_operator=(ci_eq(old,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand) printf("%s:%d: Type mismatch for field %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); else printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,old,got); } else if(in_teach&¤t_class[0]&&type_field_check_for(field_checks,current_class,nt)){ const char *declared=type_field_check_for(field_checks,current_class,nt); char text_operator=(ci_eq(declared,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand) printf("%s:%d: Type mismatch for field %s in %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,current_method,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); else printf("%s:%d: Type mismatch for field %s in %s: expected %s, but this value looks like %s.\n",path,line_no,nt,current_method,declared,got); errs++; } else printf("%s:%d: Type mismatch for %s: it was first set as %s, but this value looks like %s.\n",path,line_no,nt,old,got); errs++; } } else if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *cls=objtype+7; if(!type_field_check_for(field_checks,cls,fieldname)){ if(got&&!strchr(expr,'+')&&!strchr(expr,'-')&&!strchr(expr,'*')&&!strchr(expr,'/')) 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,cls,fieldname,fieldname,got); else printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,cls,fieldname); errs++; } else { const char *fieldtype=type_field_check_for(field_checks,cls,fieldname); if(got&&!type_words_match(fieldtype,got)){ printf("%s:%d: Type mismatch for field %s.%s: expected %s, but this value looks like %s.\n",path,line_no,objname,fieldname,fieldtype,got); errs++; } } } else { printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before setting %s.\n",path,line_no,objname,objname,nt); errs++; } } else if(got) type_env_set(&types,nt,got); else type_env_set(&types,nt,"ANY"); } free(name); free(type); free(expr); } + else if(!strcmp(up,"SET")){ char *name=NULL,*type=NULL,*expr=NULL; const char *got,*old; parse_set_for_typecheck2(t,&name,&type,&expr); if(name&&*trim_inplace(name)){ char *nt=trim_inplace(name); char *tt=type?trim_inplace(type):NULL; got=simple_expr_type(types,expr); if(current_method[0]&¤t_class[0]&&!strchr(nt,'.')&&!type_env_get(types,nt)&&!type_field_check_for(field_checks,current_class,nt)){ if(got){ 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,nt,nt,got); } else { printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,current_class,nt); } errs++; free(name); free(type); free(expr); continue; } old=type_env_get(types,nt); if(tt&&*tt){ if(current_method[0]&¤t_class[0]&&!strchr(nt,'.')){ const char *declared=type_field_check_for(field_checks,current_class,nt); if(declared&&tt&&!claro_is_type_word(tt)){ printf("%s:%d: Field %s in %s needs a known type such as NUMBER, TEXT, or YESNO, but %s is not a Claro type. Use %s for %s.\n",path,line_no,nt,current_method,tt,declared,nt); errs++; got=NULL; } else if(declared&&tt&&!type_words_match(declared,tt)){ printf("%s:%d: Type mismatch for field %s in %s: class declares %s, but this assignment says %s. Use %s for %s.\n",path,line_no,nt,current_method,declared,tt,declared,nt); errs++; got=NULL; } else if(declared&&got&&!type_words_match(declared,got)){ printf("%s:%d: Type mismatch for field %s in %s: expected %s, but this value looks like %s.\n",path,line_no,nt,current_method,declared,got); errs++; } } if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); 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&&tt&&!claro_is_type_word(tt)){ printf("%s:%d: Field %s needs a known type such as NUMBER, TEXT, or YESNO, but %s is not a Claro type. Use %s for %s.\n",path,line_no,nt,tt,declared,fieldname); errs++; } else if(declared&&tt&&!type_words_match(declared,tt)){ printf("%s:%d: Type mismatch for field %s: class declares %s, but this assignment says %s. Use %s for %s.\n",path,line_no,nt,declared,tt,declared,fieldname); errs++; } else if(declared&&got&&!type_words_match(declared,got)){ char text_operator=(ci_eq(declared,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand){ printf("%s:%d: Type mismatch for field %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); } else { printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,declared,got); } 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,tt); errs++; } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } type_env_set(&types,nt,tt); } else if(old){ if(got&&!type_words_match(old,got)){ if(strchr(nt,'.')){ char text_operator=(ci_eq(old,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand) printf("%s:%d: Type mismatch for field %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); else printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,old,got); } else if(in_teach&¤t_class[0]&&type_field_check_for(field_checks,current_class,nt)){ const char *declared=type_field_check_for(field_checks,current_class,nt); char text_operator=(ci_eq(declared,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand) printf("%s:%d: Type mismatch for field %s in %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,current_method,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); else printf("%s:%d: Type mismatch for field %s in %s: expected %s, but this value looks like %s.\n",path,line_no,nt,current_method,declared,got); errs++; } else printf("%s:%d: Type mismatch for %s: it was first set as %s, but this value looks like %s.\n",path,line_no,nt,old,got); errs++; } } else if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *cls=objtype+7; if(!type_field_check_for(field_checks,cls,fieldname)){ if(got&&!strchr(expr,'+')&&!strchr(expr,'-')&&!strchr(expr,'*')&&!strchr(expr,'/')) 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,cls,fieldname,fieldname,got); else printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,cls,fieldname); errs++; } else { const char *fieldtype=type_field_check_for(field_checks,cls,fieldname); if(got&&!type_words_match(fieldtype,got)){ printf("%s:%d: Type mismatch for field %s.%s: expected %s, but this value looks like %s.\n",path,line_no,objname,fieldname,fieldtype,got); errs++; } } } else { printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before setting %s.\n",path,line_no,objname,objname,nt); errs++; } } else if(got) type_env_set(&types,nt,got); else type_env_set(&types,nt,"ANY"); } free(name); free(type); free(expr); } else if(!strcmp(up,"ADD")){ const char *to=find_word_ci(t,"TO"); if(to){ char *expr=substr(t+3,to); char *name=xstrdup(trim_inplace((char*)to+2)); const char *listtype=type_env_get(types,name); const char *need=container_member_type(listtype,"LIST"); const char *got=simple_expr_type(types,expr); if(need&&got&&!type_words_match(need,got)){ printf("%s:%d: Type mismatch for list %s: expected %s item, but this value looks like %s.\n",path,line_no,name,need,got); errs++; } free(expr); free(name); } } 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); } diff --git a/tests/typecheck_object_field_typed_unknown_type_bad.claro b/tests/typecheck_object_field_typed_unknown_type_bad.claro new file mode 100644 index 0000000..f92d4e1 --- /dev/null +++ b/tests/typecheck_object_field_typed_unknown_type_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.score AS BANANA TO 10 diff --git a/tools/validate_typecheck_diagnostics.py b/tools/validate_typecheck_diagnostics.py index d993ae4..d2bba5c 100644 --- a/tools/validate_typecheck_diagnostics.py +++ b/tools/validate_typecheck_diagnostics.py @@ -260,6 +260,9 @@ EXPECTED = { "tests/typecheck_object_field_typed_conflict_bad.claro": [ "tests/typecheck_object_field_typed_conflict_bad.claro:6: Type mismatch for field player.score: class declares NUMBER, but this assignment says TEXT. Use NUMBER for score.", ], + "tests/typecheck_object_field_typed_unknown_type_bad.claro": [ + "tests/typecheck_object_field_typed_unknown_type_bad.claro:6: Field player.score needs a known type such as NUMBER, TEXT, or YESNO, but BANANA is not a Claro type. Use NUMBER for score.", + ], "tests/typecheck_object_field_typed_text_bad.claro": [ "tests/typecheck_object_field_typed_text_bad.claro:6: Type mismatch for field player.name: class declares TEXT, but this assignment says NUMBER. Use TEXT for name.", ],