From 72a5589a3e9a3593ca97953e04edf53dc1d155d1 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 21 Sep 2026 07:27:18 +0000 Subject: [PATCH] typecheck: reject extra AS TO field tokens --- docs/ADVANCED_STATIC_TYPING.md | 11 +++++++++++ docs/CURRENT_STATUS.md | 1 + docs/ROADMAP.md | 1 + src/claro.c | 2 +- tests/test_validate_typecheck_diagnostics.py | 6 ++++++ ...ck_object_field_typed_as_to_extra_tokens_bad.claro | 6 ++++++ tools/validate_typecheck_diagnostics.py | 3 +++ 7 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/typecheck_object_field_typed_as_to_extra_tokens_bad.claro diff --git a/docs/ADVANCED_STATIC_TYPING.md b/docs/ADVANCED_STATIC_TYPING.md index b3dda3f..5a3a287 100644 --- a/docs/ADVANCED_STATIC_TYPING.md +++ b/docs/ADVANCED_STATIC_TYPING.md @@ -825,6 +825,17 @@ SET player.score AS NUMBER TO 10 Both explicit forms must agree with the class field declaration. +The separated form accepts one value expression only. Extra words are rejected +with a repair hint instead of being silently ignored: + +```claro +SET player.score AS NUMBER TO 10 extra +``` + +```text +SET player.score has extra text after value 10. Keep only the field name, type, TO, and one expression. +``` + The `AS ... TO` spelling also works inside modern and compatibility object methods: ```claro diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 6082baf..e436b67 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -83,6 +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 `AS ... TO` assignments reject extra words after the value with a repair hint, so `SET player.score AS NUMBER TO 10 extra` cannot silently ignore the trailing text. - Direct object-field validation also has positive coverage for explicit `NUMBER`, `TEXT`, and `YESNO` annotations, such as `SET player.ready YESNO YES`, plus the compatibility-shaped `AS ... TO` form such as `SET player.score AS NUMBER TO 10`, while the shorter unannotated form remains supported; method-body field assignments have matching positive coverage for both modern and compatibility `TEACH` forms using `SET score AS NUMBER TO 10`, and those two `AS ... TO` method fixtures are now included in `claro validate`; 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` or `SET player.score BANANA 10` identify the invalid type and suggest the declared field type. Still needed: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6324210..e7aaec8 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -52,6 +52,7 @@ Keep declared return types honest: `claro typecheck` now reports friendly diagno 8d.1. Keep direct field annotation forms balanced: the compatibility-shaped `SET player.score AS NUMBER TO 10` form now has a dedicated positive fixture beside short explicit annotations, so both accepted spellings remain protected by focused validation. 8d.2. Keep method field annotation forms balanced: modern and compatibility methods now have dedicated positive fixtures for `SET score AS NUMBER TO 10`, so the compatibility-shaped annotation remains protected inside both supported method syntaxes. 8d.3. Keep release validation aligned with method annotation coverage: the modern and compatibility `AS ... TO` method fixtures now run through `claro validate`, not only the focused Python diagnostic validator. +8d.4. Keep separated direct field assignments unambiguous: `SET player.score AS NUMBER TO 10 extra` now reports the trailing text and explains the one-expression form instead of accepting a partially parsed value. 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 b6ce3bb..020d3fe 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&&!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,"SET")){ char *name=NULL,*type=NULL,*expr=NULL; const char *got,*old; const char *as=find_word_ci(t+3,"AS"), *to=find_word_ci(t+3,"TO"); 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; if(as&&to&&as