Skip to content

feat: add checked as-cast expression 🎯 - #215

Merged
timfennis merged 8 commits into
masterfrom
feature/checked-casts
Sep 2, 2026
Merged

feat: add checked as-cast expression 🎯#215
timfennis merged 8 commits into
masterfrom
feature/checked-casts

Conversation

@timfennis

@timfennis timfennis commented Sep 2, 2026

Copy link
Copy Markdown
Owner

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.

The review-hardening commits:

  • Fix two cast/comparison ambiguities: a > inside the grouped right operand of value as Int < (y > z) no longer closes cast type arguments, and an excess closer in 3 as Int < 4 >> 1 remains part of the shift operator.
  • Keep declarations wrapped in casts visible in the LSP document outline.
  • Align the cast manual with the overlap rule.
  • Bound failed-cast type rendering by both recursion depth and total inspected values, and memoize recursive conformance checks by object/type identity so repeated aliases cannot multiply either path.

Verification

  • cargo test --workspace (430 functional programs plus unit, compiler, LSP, REPL, and regression suites)
  • cargo build --no-default-features
  • cargo clippy --workspace --all-targets (passes with existing warnings in the benchmark and test harnesses)
  • cargo fmt --all -- --check
  • tree-sitter corpus: 24/24
  • VS Code TypeScript compilation
  • mdbook build

Benchmarks stayed within 0.946–1.045 of the base branch over eight runs.

Known limitations

  • matches_param cannot verify concrete element types during dynamic dispatch.
  • push does not check element types, so a cast that trusts a stale static container type can be elided.
  • overlaps ignores the shared empty value when comparing containers with disjoint element types. This preserves compile-time errors for non-empty values.

AI disclosure

Claude Opus 5 implemented the original feature and several review fixes. OpenAI Codex audited the final branch and implemented the parser, LSP, VM diagnostic, test, and documentation fixes in the review-hardening commits.

Comment thread manual/src/features/augmented-assignment.md

@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: 6e8dd9d352

ℹ️ 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_core/src/static_type.rs

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

ℹ️ 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_core/src/static_type.rs Outdated
Comment thread ndc_analyser/src/scope.rs Outdated
@timfennis
timfennis force-pushed the feature/checked-casts branch from f1a112f to fb87706 Compare September 2, 2026 11:29

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

ℹ️ 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
…ent 🧪

These all pass today; nothing here changes behaviour. They pin the rules the
cast expression is about to lean on, so a later relaxation has to be deliberate:

- a call is rejected when neither the argument nor the parameter type is a
  subtype of the other, even across the container hierarchy
- a `Never` argument diverges before the call, so it is not a type error
- a specialized `op=` needs the right operand to provably fit, and an `Any`
  operand does not

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timfennis
timfennis force-pushed the feature/checked-casts branch from fb87706 to 00e15e5 Compare September 2, 2026 11:53

@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: 00e15e5d7d

ℹ️ 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_parser/src/parser.rs Outdated
@timfennis
timfennis force-pushed the feature/checked-casts branch from 00e15e5 to 9964746 Compare September 2, 2026 12:01

@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: 83e279853e

ℹ️ 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_parser/src/parser.rs Outdated
@timfennis
timfennis force-pushed the feature/checked-casts branch from 83e2798 to 6c4c2c6 Compare September 2, 2026 12:17
timfennis and others added 4 commits September 2, 2026 14:19
`as` asserts that a value already has the given type; it never converts. When
the analyser can prove the cast from the operand's static type it is free,
otherwise the VM checks the value at the cast site. Container checks scan
elements, terminate on cyclic containers, and validate a map's default value
since a missing-key lookup inserts it.

The cast is rejected at compile time only when no single value could satisfy
both types. A subtype test is too strict: `List<Any>` and `Sequence<Int>` share
`List<Int>` as a common subtype without either being a subtype of the other, so
that cast is possible and gets checked at runtime. `StaticType::overlaps`
answers this, and `Never` overlaps nothing since no value inhabits it.

A `<` after the cast type may open a type argument list or be a less-than
operator. It only opens one when the tokens really form a list: closed by a
token supplying exactly the brackets still open, with no bare `=` left over and
no `)` belonging to an enclosing expression. Anything else is an operator, so
`x as Int < y >> z` and `(x as Int < y) >= z` parse as they would without the
cast, while `List<List<Int>>` still closes on its `>>`.

This makes the cast the way to state what a value holds wherever the analyser
will not guess: after a reassignment widens a variable, or before an augmented
assignment whose operand type is unknown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the cast expression to the tree-sitter grammar, the TextMate grammar and
the CLI highlighter, and captures `as` as a keyword operator in the highlight
queries alongside `in`.

A `<` after a cast type is ambiguous — it may open a type argument list or be a
less-than operator. A declared conflict lets the parser explore both branches
and a dynamic precedence prefers the type argument list when it parses, which
matches the recursive-descent parser.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A call rejected because the analyser inferred a wider type than the overload
accepts is fixable, but the error said only that nothing matched. It now says
to cast, and only when a cast would actually help: every parameter of some
same-arity overload has to overlap the argument in that position, with at least
one argument not already fitting. An unknown name, a wrong arity or a genuine
type mismatch gets the plain message.

`AnalysisError` gains a `help_text` field alongside the ones `ndc_lexer` and
`ndc_parser` errors already carry, so the advice renders as its own note under
the snippet instead of lengthening the message.

The lookup is read-only and walks the scope chain directly, so producing a
diagnostic cannot create upvalues the way call resolution does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`invalid_type_annotation` concatenated its help into the message, so an unknown
type read as one long sentence. `StaticTypeConstructionError` already carries
the help separately, so pass it through the `help_text` field and let the
reporter place it under the snippet like every other help text.

An LSP diagnostic has no separate note channel, so the conversion folds the help
back into the message there — unchanged from what editors showed before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timfennis
timfennis force-pushed the feature/checked-casts branch from 6c4c2c6 to 5ccfadc Compare September 2, 2026 12:20

@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: 5ccfadc741

ℹ️ 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_parser/src/parser.rs Outdated

@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: 3fb7cc5741

ℹ️ 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_vm/src/vm.rs Outdated

@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: 34a672a0a8

ℹ️ 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_vm/src/value/mod.rs Outdated
@timfennis
timfennis merged commit 4d5eced into master Sep 2, 2026
1 check passed
@timfennis
timfennis deleted the feature/checked-casts branch September 2, 2026 13:24
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