Skip to content

feat(analyser): make Any a top type that takes a cast to get a value out 📦 - #353

Merged
timfennis merged 2 commits into
feat/type-checkerfrom
feature/explicit-any-pr
Nov 14, 2026
Merged

timfennis merged 2 commits into
feat/type-checkerfrom
feature/explicit-any-pr

Conversation

@timfennis

Copy link
Copy Markdown
Owner

Closes #331. Any was gradual: a value of type Any went anywhere a type was expected, with a check the analyser inserted, and a call with an Any argument picked its overload as the program ran. Deciding where those checks went was spread over many places, and every new feature opened another hole (#328, #329, the op= and slice-write holes, function values through Any). Now Any is a top type, like Rust's Box<dyn Any>, Kotlin's Any or TypeScript's unknown:

let a: Any = 5;            // anything goes into Any
let n: Int = a;            // error: found Any but expected Int
let m = a as Int;          // the way out: fails the program if it isn't one
print((a as? Int).is_some(), a);   // a test, and anything that takes Any
  • A value of type Any only goes where Any is taken. Passing, storing, iterating, calling it or using an operator on it is an error unless an overload takes Any there, with help to cast it with as or test it with as?. A call with an Any argument only considers overloads that take Any at that position. Inside containers Any is exact, as before.
  • Enforced in bind_argument, may_fit, type_parameters_agree, could_take/could_be_callable and iteration. Failures give the error type from fix(analyser): don't report errors that follow from an earlier one 🧯 #348, so each mistake is one error.
  • print, dbg and the variadic zip take each argument as its own type instead of turning it into Any; no built-in signature changed.
  • Two weaknesses the permissive Any had hidden are fixed: a generic function's own body (what runs for a function value) picks its calls with type parameters rather than Any, which kept + on tuples working there; and a literal whose element type became a parameter's type no longer falls back to Any.

Tests: 16 became error tests, 26 now cast with as, 4 got an annotation instead of a stray Any, and 6 that would have silently changed which overload runs were rewritten with a type test. The manual's sections on Any (types, casts, augmented assignment, function values, the overload troubleshooting page) describe the explicit rule.

All advent-of-brian solutions give their answers; in advent-of-code-ndc two more programs need an annotation (a loop that falls off the end into print, and a map default %{:fn() => []} whose list doesn't learn from later pushes). Analysis time and VM instruction counts are unchanged.

Known gaps, for follow-ups: values the analyser can't type still fall back to Any and then show up in errors (a lambda nothing calls, a REPL line let xs = [];), and a user function whose body ties two parameters together accepts f(a, 1) but not f(1, a) with a: Any. The runtime checks the analyser used to insert for Any (ExpectType, type_checks, the element-scanning pass of runtime overload picking) are no longer reachable and go next.

🤖 Generated with Claude Code

…out 📦

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

chatgpt-codex-connector Bot commented Nov 14, 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-14T09:01:07.500466Z 58993f3 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: b1c1d0187b

ℹ️ 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 435 to +436
argument.replace(&|part| {
matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)
matches!(part, StaticType::Any).then(|| StaticType::Generic("T".into()))

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 Specialize generic bodies for actual Any arguments

When a generic wrapper receives an actual Any, its copied constraints now select only the overload accepting Any, but instance_for still reuses the unspecialized body whenever every key type is Any. That body retains the permissive runtime dispatch recorded here. For example, with fn pick(x: Int) => 1; fn pick(x: Any) => "fallback"; fn wrap(x) => pick(x); let a: Any = 1;, wrap(a) is inferred as String but executes the Int overload and returns 1. Besides violating the new overload rule, this lets subsequent statically typed operations receive the wrong runtime type. Actual Any calls need their own specialized picks rather than reusing the generic runtime-dispatched body.

Useful? React with 👍 / 👎.

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.

Fixed in 58993f3: a call with Any arguments now gets its own instance (test 017_dynamic_any/027).

A generic function called with an Any argument reused its own body,
whose calls dispatch at runtime, so its result could disagree with its type.

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

Copy link
Copy Markdown
Owner Author

@codex review

@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: 58993f3d09

ℹ️ 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 +242 to 243
if !self.declared_in_batch.contains(&function) {
return function;

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 Preserve Any-specific overload picks across REPL batches

When a generic function was declared on an earlier REPL line, this branch still bypasses specialization. For example, evaluate fn pick(x: Int) => 1; fn pick(x: Any) => "fallback"; fn wrap(x) => pick(x); in one batch, then let a: Any = 1; let s: String = wrap(a); print(s); in the next. The copied constraints infer String, but the previously compiled body dynamically selects pick(Int) and returns 1, giving a statically typed variable the wrong runtime type. Although same-batch calls now get an instance, the remaining earlier-batch bypass is fresh evidence that the mismatch persists in the REPL. Earlier-batch calls also need to preserve the overload selection established by their actual argument types.

Useful? React with 👍 / 👎.

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.

Real, but it needs later REPL lines to compile instances of earlier lines' functions; tracked in #354.

@timfennis
timfennis merged commit 3403ef1 into feat/type-checker Nov 14, 2026
1 check passed
@timfennis
timfennis deleted the feature/explicit-any-pr branch November 14, 2026 09:05
timfennis added a commit that referenced this pull request Nov 14, 2026
…ed 🧹 (#356)

Since #353 an `Any` value only leaves `Any` through a cast, so the
checks the analyser added where an `Any` was passed or stored never fire
in a program that analyses without errors. Part of #331.

- Delete the `ExpectType` opcode, the `type_checks` tables, the analyser
code that recorded them (`expect`, `expected_of_any`, the instance
checks), and the argument nodes calls carried only for them.
- An early pick is always a direct call now, and the rule that turns a
runtime pick among one overload into a direct call lives in one helper.
- Delete branches that only `Any` reached: the `Dynamic` arm of
`choose_function_value`, the `Any` relaxation for function types in
`StaticType::matches`, and the string branches of the generic
`prefixes`/`suffixes`.

The VM still picks overloads by looking inside containers, for a generic
function's own body when it's used as a value (#355).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <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