Skip to content

fix(analyser): use type overlap instead of subtyping for dispatch feasibility ➿ - #209

Closed
timfennis wants to merge 1 commit into
masterfrom
bugfix/overlap-based-dispatch-feasibility
Closed

fix(analyser): use type overlap instead of subtyping for dispatch feasibility ➿#209
timfennis wants to merge 1 commit into
masterfrom
bugfix/overlap-based-dispatch-feasibility

Conversation

@timfennis

Copy link
Copy Markdown
Owner

Context

Running the advent-of-brian suite against 0.3.0 surfaced two analyser regressions — programs that ran fine on 0.2.1 were rejected at compile time:

Both share one root cause: is_incompatible_with was defined as "no subtype relation in either direction", but dispatch feasibility needs type overlap — "could any runtime value satisfy both types?". Those differ in the mixed-direction case: Sequence<String> and List<Any> are not subtypes of each other, yet List<String> inhabits both. And Any overlaps everything, so an unknown operand must defer to runtime dispatch rather than be rejected.

Changes

  • ndc_core: new StaticType::overlaps — structural common-inhabitant test mirroring is_subtype, handling mixed-direction sequence-family relationships. Element types are treated as inhabited, so List<Int> vs List<String> stays disjoint. is_incompatible_with is now !overlaps (this makes find_function_candidates and could_accept_call overlap-based).
  • ndc_analyser: augmented_rhs_is_compatible uses overlaps instead of is_subtype, so specialized op= bindings are only rejected on a provable mismatch.
  • Manual: documented that only provable mismatches are rejected; unknown RHS types are checked at runtime.
  • Regression tests: unit tests for overlaps, plus two functional tests covering both reported failures.

Notes for reviewers

The deliberately-deferred strictness from the #192 review is untouched: [1] ++= ["two"] and the map -=/&= cases still reject, because those types are provably disjoint. All 552 tests pass and every advent-of-brian puzzle now runs on this branch.

🤖 Generated with Claude Code

@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: daa3ba4b70

ℹ️ 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 thread ndc_analyser/src/analyser.rs Outdated
(StaticType::Tuple(left), right) => left.iter().all(|left| right.is_subtype(left)),
(left, right) => right.is_subtype(left),
(StaticType::Tuple(left), right) => left.iter().all(|left| right.overlaps(left)),
(left, right) => right.overlaps(left),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require subtype safety for mutating augmented assignments

When the RHS is a supertype of the target, overlap is insufficient to preserve the target's concrete type. For example, let values: List<Int> = [1]; let rhs: List<Number> = [0.5]; values ++= rhs; now passes this gate because List<Int> overlaps List<Number>, but the native ++= implementation appends the float without checking element types while the analyser continues treating values as List<Int>. This silently violates the annotation and can make subsequent statically resolved operations receive the wrong runtime values; only unknown types such as Any should be deferred, while known RHS types still need the previous subtype guarantee.

Useful? React with 👍 / 👎.

| Self::MaxHeap(u)
| Self::Deque(u),
Self::Sequence(t),
) => t.overlaps(u),

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 Support newly overlapping typed containers at runtime

For a widened Sequence<Int> argument and a List<Number> parameter, this branch now creates a dynamic candidate because a runtime List<Int> could satisfy both. However, Value::matches_param explicitly returns false for every typed List, Sequence, Map, or Tuple parameter during dynamic dispatch. Thus a value widened from an iterator and then assigned [1], for example, is accepted by the analyser when passed to fn consume(xs: List<Number>) but is guaranteed to fail overload resolution even though its runtime value matches. The runtime matcher must be able to verify the typed-container cases enabled here, or this overlap must not advertise them as dispatchable.

Useful? React with 👍 / 👎.

@timfennis
timfennis force-pushed the bugfix/overlap-based-dispatch-feasibility branch from daa3ba4 to 0e43d20 Compare September 2, 2026 10:40

@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: 0e43d20079

ℹ️ 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".

matches!((key.as_ref(), value.as_ref()), (Self::Any, Self::Any))
}
Self::Tuple(elements) => elements.is_empty(),
_ => true,

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 Treat typed iterators and heaps as uncheckable

The newly added runtime-checkability guard still falls through to true for Iterator<T>, MinHeap<T>, and MaxHeap<T> with concrete T. For example, let xs: Sequence<Int> = 1..3; consume(xs) with an Iterator<Number> parameter is retained as a dynamic candidate because the types overlap, but Value::matches_param falls back to the runtime type Iterator<Any>, which cannot match Iterator<Number>, so the call is guaranteed to fail at runtime rather than being rejected by the analyser. The heap runtime types similarly erase their elements to Any; these variants should only be considered checkable when their element type is Any.

Useful? React with 👍 / 👎.

…sibility ➿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@timfennis
timfennis force-pushed the bugfix/overlap-based-dispatch-feasibility branch from 0e43d20 to 1ab78d3 Compare September 2, 2026 10:56

@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: 1ab78d352d

ℹ️ 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".

Self::String.overlaps(t)
}
(Self::Sequence(t), Self::Tuple(elems)) | (Self::Tuple(elems), Self::Sequence(t)) => {
elems.iter().all(|elem| elem.overlaps(t))

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 Reject empty tuples in sequence overlap

When elems is empty, all is vacuously true, so Sequence<Int> is considered to overlap (). A call such as fn consume(x: ()) => x; let xs: Sequence<Int> = [1]; consume(xs) is therefore retained for dynamic dispatch, although no Sequence<Int> value can satisfy the unit parameter and Value::matches_param rejects every Tuple parameter. This turns a statically provable mismatch into a guaranteed runtime “no matching overload” error; special-case the empty tuple before applying the element-wise test.

Useful? React with 👍 / 👎.

@timfennis

Copy link
Copy Markdown
Owner Author

Superseded by #215, which combines this with the other branch so the precursor lands first. All review findings from this PR are addressed there.

@timfennis timfennis closed this Sep 2, 2026
timfennis added a commit that referenced this pull request Sep 2, 2026
Replaces #209 and #213.

## What changed

The `as` operator asserts that a value has a type without converting it.
The analyser removes checks it can prove from the operand's static type.
The VM checks the remaining casts at the cast site. Container checks
scan nested elements, stop on cyclic containers, and validate map
defaults because a missing-key lookup inserts the default.

The analyser rejects a cast only when the operand and target types
cannot share a value. `List<Any>` and `Sequence<Int>` can both contain
the same `List<Int>`, so that cast reaches the runtime check.
`StaticType::overlaps` models this rule. `Never` overlaps no type
because it has no values.

Dispatch keeps its existing subtype rules. Users can cast widened values
before calls that need a concrete container type. The analyser adds a
cast hint when a same-arity overload could accept narrower argument
types; unknown names, wrong arity, and disjoint argument types keep the
existing error.

The language grammar, tree-sitter grammar, TextMate grammar, CLI
highlighter, LSP traversal, completion list, and diagnostics all
understand `as`. Both parsers prefer generic arguments when the tokens
form a complete type, and otherwise leave `<`, `>`, `>=`, and `>>` to
the expression parser.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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