Skip to content

fix(analyser): make a type parameter one type across a call, and op= a call like any other ⚖️ - #332

Merged
timfennis merged 2 commits into
feat/type-checkerfrom
feature/one-type-per-type-parameter
Nov 13, 2026
Merged

timfennis merged 2 commits into
feat/type-checkerfrom
feature/one-type-per-type-parameter

Conversation

@timfennis

Copy link
Copy Markdown
Owner

Picking an overload matched each argument against its parameter on its own, so a type parameter could stand for a different type at each position: max(T, T) -> T looked like it took a Float and an Int. The solver's unification got it right for ordinary calls, but three places worked around the picker instead, and a op= b went through the picker alone. So best max= x, with best a Float and x an Int, passed analysis and failed when it ran, while best = max(best, x) was rejected (found in an Advent of Code solution).

  • The picker now makes each type parameter one type across the call. Inside a container a type parameter is the type there (a List<Any> makes T Any), and a value passed for T itself has to fit it, so push(List<Any>, String) still fits push(List<T>, T). An argument of type Any as a whole agrees with anything, as it's checked where it's passed.
  • a op= b is now a call constraint like any other, as a = op(a, b): one path instead of two (the separate one that picked overloads for known operand types is gone), and an in-place overload such as ++= is used where it takes the operands exactly. An operator with no plain counterpart, like a set's -=, is called when no plain operator takes the operands.
  • The workarounds go: the filter on runtime picks in resolve_overloads, and the eligibility gate for exact in-place picks.

Behaviour changes:

  • best max= x with a Float and an Int is an analysis error, as max(best, x) is.
  • a op= b with a right operand of type Any is no longer an error. It works like a = a op b: the operator makes a new value, which is checked where it's written back, so a value that doesn't fit never reaches the target. As nothing says the operand fits, it doesn't change the target in place; a cast to the target's type does.
  • values ++= ["two"] on a List<Int> now reports that no ++ takes List<Int>, List<String> instead of a type mismatch.

The manual's augmented-assignment and casts pages describe the new rules.

🤖 Generated with Claude Code

…a call like any other ⚖️

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T16:34:47.702007Z 73adb98 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ab1afe5aa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 43 to +45
CallNodes {
arguments: vec![None, Some(r_value.id)],
result: None,
result: Some(l_value.id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check an Any-typed target before calling a resolved operator

When the target has type Any and only one ordinary operator overload is viable, resolve_call_now promotes the dynamic pick to a resolved call, but expected_of_any cannot emit a check for the first argument because its node is None. For example, fn choose(a: Int, b: Int) => a; let x: Any = "wrong"; x choose= 1; can pass a String directly into the Int parameter, whereas x = choose(x, 1) checks and rejects it. Previously, fully known operand types used dynamic dispatch here, which checked the actual left operand before selecting the overload. Preserve that check when routing augmented assignments through ordinary call constraints; checking the result at writeback does not validate the argument before the function executes.

Useful? React with 👍 / 👎.

…ls 🎯

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timfennis

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 73adb9878f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@timfennis
timfennis merged commit acce12d into feat/type-checker Nov 13, 2026
1 check passed
@timfennis
timfennis deleted the feature/one-type-per-type-parameter branch November 13, 2026 16:35
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.

1 participant