Skip to content

🐛 fix: report error for undefined field access on closed records#1365

Merged
harehare merged 5 commits intomainfrom
fix/typechecker-undefined-field-error
Mar 1, 2026
Merged

🐛 fix: report error for undefined field access on closed records#1365
harehare merged 5 commits intomainfrom
fix/typechecker-undefined-field-error

Conversation

@harehare
Copy link
Copy Markdown
Owner

@harehare harehare commented Mar 1, 2026

  • 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

…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
Copilot AI review requested due to automatic review settings March 1, 2026 01:15
@harehare harehare changed the title fix(typechecker): report error for undefined field access on closed r… fix(typechecker): report error for undefined field access on closed records Mar 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::UndefinedField with a miette diagnostic code and source labeling.
  • Reports UndefinedField when 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
    );

Comment on lines +1145 to +1174
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
);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot
Can you fix it?

Comment on lines +1142 to +1186
// --- 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
);
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 1, 2026

@harehare I've opened a new pull request, #1366, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 3 commits March 1, 2026 02:25
Fix undefined field access tests to assert TypeError::UndefinedField specifically
Copilot AI review requested due to automatic review settings March 1, 2026 04:17
@harehare harehare changed the title fix(typechecker): report error for undefined field access on closed records 🐛 fix: report error for undefined field access on closed records Mar 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines +1903 to +1911
// 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),
});
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot generated this review using guidance from repository custom instructions.
}

// Resolve deferred selector field accesses (.field on records)
Self::resolve_selector_field_accesses(&mut ctx);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +1145 to +1161
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!(
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot generated this review using guidance from repository custom instructions.
@harehare harehare merged commit 76e5ed6 into main Mar 1, 2026
10 checks passed
@harehare harehare deleted the fix/typechecker-undefined-field-error branch March 1, 2026 04:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants