feat: add checked as-cast expression 🎯 - #215
Conversation
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
f1a112f to
fb87706
Compare
There was a problem hiding this comment.
💡 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".
…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>
fb87706 to
00e15e5
Compare
There was a problem hiding this comment.
💡 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".
00e15e5 to
9964746
Compare
There was a problem hiding this comment.
💡 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".
83e2798 to
6c4c2c6
Compare
`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>
6c4c2c6 to
5ccfadc
Compare
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
Replaces #209 and #213.
What changed
The
asoperator 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>andSequence<Int>can both contain the sameList<Int>, so that cast reaches the runtime check.StaticType::overlapsmodels this rule.Neveroverlaps 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:
>inside the grouped right operand ofvalue as Int < (y > z)no longer closes cast type arguments, and an excess closer in3 as Int < 4 >> 1remains part of the shift operator.Verification
cargo test --workspace(430 functional programs plus unit, compiler, LSP, REPL, and regression suites)cargo build --no-default-featurescargo clippy --workspace --all-targets(passes with existing warnings in the benchmark and test harnesses)cargo fmt --all -- --checkmdbook buildBenchmarks stayed within 0.946–1.045 of the base branch over eight runs.
Known limitations
matches_paramcannot verify concrete element types during dynamic dispatch.pushdoes not check element types, so a cast that trusts a stale static container type can be elided.overlapsignores 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.