🐛 fix: report error for undefined field access on closed records#1365
🐛 fix: report error for undefined field access on closed records#1365
Conversation
…ecords - Add UndefinedField error type and diagnostics - Report error when accessing non-existent field on closed record via selector or bracket notation - Add integration tests for undefined field access
There was a problem hiding this comment.
Pull request overview
Adds a dedicated typechecker diagnostic for accessing missing fields on closed record types, and wires the check into both selector and bracket-access resolution paths.
Changes:
- Introduces
TypeError::UndefinedFieldwith amiettediagnostic code and source labeling. - Reports
UndefinedFieldwhen accessing a non-existent field on a closed record (including deferred-resolution cases). - Adds integration tests around undefined field access on closed records (currently bracket-only).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| crates/mq-typechecker/tests/integration_test.rs | Adds integration tests for undefined field access on closed records (bracket access; selector coverage still missing). |
| crates/mq-typechecker/src/lib.rs | Adds UndefinedField error variant and post-unification resolution for deferred selector accesses. |
| crates/mq-typechecker/src/infer.rs | Adds deferred-selector access tracking in the inference context to support post-unification checking. |
| crates/mq-typechecker/src/constraint.rs | Emits UndefinedField for selector access on closed records when record type is known; defers selector checks when the piped type isn’t yet resolved. |
Comments suppressed due to low confidence (1)
crates/mq-typechecker/tests/integration_test.rs:1152
- The two tests named
test_selector_*are exercising bracket access (v[:c]/v[:a]), not selector access (e.g.| .c). This makes the test names and failure messages misleading (e.g. message mentions.c). Rename them or change the queries to actually use selector syntax so the intent matches the exercised behavior.
fn test_selector_undefined_field_on_closed_record() {
// Accessing a non-existent field via bracket notation on a closed record should produce an error
let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:c]"#);
assert!(
!result.is_empty(),
"Accessing undefined field .c on closed record should fail: {:?}",
result
);
| fn test_selector_undefined_field_on_closed_record() { | ||
| // Accessing a non-existent field via bracket notation on a closed record should produce an error | ||
| let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:c]"#); | ||
| assert!( | ||
| !result.is_empty(), | ||
| "Accessing undefined field .c on closed record should fail: {:?}", | ||
| result | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_selector_defined_field_on_closed_record() { | ||
| // Accessing an existing field via bracket notation on a closed record should succeed | ||
| let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:a]"#); | ||
| assert!( | ||
| result.is_empty(), | ||
| "Bracket access to defined field :a on closed record should succeed: {:?}", | ||
| result | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_bracket_access_undefined_field_on_closed_record() { | ||
| // Accessing a non-existent field via bracket notation should produce an error | ||
| let result = check_types(r#"var v = {"key": 1, "value": "hello"} | v[:missing]"#); | ||
| assert!( | ||
| !result.is_empty(), | ||
| "Bracket access to undefined field :missing on closed record should fail: {:?}", | ||
| result | ||
| ); |
There was a problem hiding this comment.
These new "undefined field access" tests only assert that some error is produced (!result.is_empty()), which could pass for an unrelated type error. Since this PR introduces TypeError::UndefinedField, the tests should assert that the returned errors contain (and ideally are dominated by) UndefinedField for the relevant field name.
| // --- Undefined field access tests --- | ||
|
|
||
| #[test] | ||
| fn test_selector_undefined_field_on_closed_record() { | ||
| // Accessing a non-existent field via bracket notation on a closed record should produce an error | ||
| let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:c]"#); | ||
| assert!( | ||
| !result.is_empty(), | ||
| "Accessing undefined field .c on closed record should fail: {:?}", | ||
| result | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_selector_defined_field_on_closed_record() { | ||
| // Accessing an existing field via bracket notation on a closed record should succeed | ||
| let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:a]"#); | ||
| assert!( | ||
| result.is_empty(), | ||
| "Bracket access to defined field :a on closed record should succeed: {:?}", | ||
| result | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_bracket_access_undefined_field_on_closed_record() { | ||
| // Accessing a non-existent field via bracket notation should produce an error | ||
| let result = check_types(r#"var v = {"key": 1, "value": "hello"} | v[:missing]"#); | ||
| assert!( | ||
| !result.is_empty(), | ||
| "Bracket access to undefined field :missing on closed record should fail: {:?}", | ||
| result | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_bracket_access_defined_field_on_closed_record() { | ||
| // Accessing an existing field via bracket notation should succeed | ||
| let result = check_types(r#"var v = {"key": 1, "value": "hello"} | v[:key]"#); | ||
| assert!( | ||
| result.is_empty(), | ||
| "Bracket access to defined field :key on closed record should succeed: {:?}", | ||
| result | ||
| ); | ||
| } |
There was a problem hiding this comment.
PR description mentions reporting errors for undefined field access via selector or bracket notation, but the added integration coverage here only exercises bracket access (v[:missing], etc.). Add at least one failing and one passing test for selector access on a closed record (e.g. {"a": 1} | .missing and {"a": 1} | .a) to ensure the deferred selector-resolution path is covered.
… access tests Co-authored-by: harehare <[email protected]>
Fix undefined field access tests to assert TypeError::UndefinedField specifically
| // Piped type not yet resolved — defer check to post-unification | ||
| if let Some(name) = field_name { | ||
| ctx.add_deferred_selector_access(crate::infer::DeferredSelectorAccess { | ||
| symbol_id, | ||
| piped_ty: piped_ty.clone(), | ||
| field_name: name.to_string(), | ||
| range: get_symbol_range(hir, symbol_id), | ||
| }); | ||
| } |
There was a problem hiding this comment.
The PR description mentions reporting UndefinedField via both selector (.field) and bracket ([:field]) notation, but the added integration tests only exercise bracket access. Add at least one test that uses selector syntax (both defined and undefined fields) to cover the new deferred selector resolution path.
| } | ||
|
|
||
| // Resolve deferred selector field accesses (.field on records) | ||
| Self::resolve_selector_field_accesses(&mut ctx); |
There was a problem hiding this comment.
resolve_selector_field_accesses adds new Constraint::Equal constraints (to bind selector type to the resolved record field type), but TypeChecker::check doesn’t run unification afterwards. If there are no later phases that call unify::solve_constraints (e.g. no deferred overloads/user calls), selector result types can remain as unbound type variables and downstream type inference can be incorrect. Consider making resolve_selector_field_accesses return a bool like resolve_record_field_accesses and re-running unify::solve_constraints when any selector was resolved (or bind the symbol type directly instead of adding a constraint).
| Self::resolve_selector_field_accesses(&mut ctx); | |
| Self::resolve_selector_field_accesses(&mut ctx); | |
| // Re-run unification to propagate newly added selector field constraints. | |
| // This ensures that selector result types are fully resolved before | |
| // subsequent phases rely on them. | |
| unify::solve_constraints(&mut ctx); |
| fn test_selector_undefined_field_on_closed_record() { | ||
| // Accessing a non-existent field via bracket notation on a closed record should produce an error | ||
| let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:c]"#); | ||
| assert!( | ||
| result | ||
| .iter() | ||
| .any(|e| matches!(e, TypeError::UndefinedField { field, .. } if field == "c")), | ||
| "Accessing undefined field :c on closed record should produce UndefinedField error: {:?}", | ||
| result | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_selector_defined_field_on_closed_record() { | ||
| // Accessing an existing field via bracket notation on a closed record should succeed | ||
| let result = check_types(r#"var v = {"a": 1, "b": 2} | v[:a]"#); | ||
| assert!( |
There was a problem hiding this comment.
These test names/comments say “selector” but the queries are bracket accesses (v[:c], v[:a]). This makes it hard to tell what syntax is actually being covered. Either rename these to test_bracket_access_* or change the queries to use selector syntax (e.g. v.c) so the test intent matches the code.
Uh oh!
There was an error while loading. Please reload this page.