🎯 feat: add checked as-cast expression - #213
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 971a60d03f
ℹ️ 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: 276046af45
ℹ️ 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: affb860f8a
ℹ️ 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".
## Context
The compiler had two stack ABIs: most expressions leave one value, but
`while`, `for` (block body), `let`, named `fn`, and `struct` left
nothing. Which ABI applied was tracked by the syntactic `produces_value`
side-channel, and every consumer of an expression had to guess correctly
or corrupt the stack. On master today:
- `print(while false {})` → `error[vm]: Unable to invoke Int as a
function` — the missing argument shifts the stack so the VM calls a
local as the callee.
- `let y = while false {};` → accepted, `y` silently binds garbage.
- `while true { f(1 + continue); }` → leaks two stack values per
iteration; in a `for` loop the leak lands on top of the iterator and
corrupts iteration.
The `as`-cast PR (#213) kept tripping over the same seam (Codex found
three variants), which prompted fixing the root cause instead of
patching consumers. This is a precursor to that PR.
## Changes
- **One invariant**: `compile_expr` leaves exactly one result value on
the stack, on top of any local slots it declared. Diverging expressions
(`break`/`continue`/`return`) are the only exception — control never
returns to observe a result. The invariant is documented on
`compile_expr`.
- `while`, `for` with a block body, `let`, `struct`, and named `fn`
declarations now push `()` as their result; `Statement` unconditionally
pops; **`produces_value` is deleted**.
- **`break`/`continue` clean up pending operands.** Every `OpCode`
declares its stack effect in `OpCode::stack_delta` (an exhaustive match,
so a new opcode cannot be added without it), and `OptimizerIr::write`
simulates the stack depth from those deltas automatically — compiler
arms carry no manual bookkeeping. `compile_expr` re-anchors the
simulation at every expression boundary (instructions reached only by a
jump, like a loop's exit pop, leave the linear simulation off). Each
`LoopContext` records the depth at loop entry, and `break`/`continue`
pop back down to it before jumping. This gives them the same safety
`return` always had via frame truncation, so expression-position
divergence (`f(1 + break)`, and the existing `return (return 5) + 1`
corpus test) is well-defined instead of corrupting.
- The parser no longer consumes the `;` after a `let` inside
`let_statement` — the shared `expression_or_statement` path consumes it
and wraps the declaration in a `Statement` like every other
`;`-terminated expression (the semicolon is still required).
- Repeated inline unit pushes are consolidated into an `emit_unit`
helper.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
affb860 to
8455662
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8455662fde
ℹ️ 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".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b6a426c9e
ℹ️ 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".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a13948b93d
ℹ️ 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".
| } => { | ||
| let target = self.lower_type_expr(annotation); | ||
| let found = self.analyse_with_expected(value, &target); | ||
| if found.is_incompatible_with(&target) { |
There was a problem hiding this comment.
Permit casts across overlapping container hierarchies
This bidirectional subtype test rejects types that overlap even though neither is directly a subtype of the other. For example, let xs: List<Any> = [1]; xs as Sequence<Int> is reported as an impossible cast, although the runtime Sequence conformance branch explicitly scans lists and this value satisfies the target; List<Int> is a common subtype of both declared types. Use an overlap/compatibility check that accounts for container hierarchies so these valid checked casts reach CheckType.
Useful? React with 👍 / 👎.
|
Superseded by #215, which combines this with the other branch so the precursor lands first. All review findings from this PR are addressed there. |
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>
Context
Review feedback on the explicit-numeric-modes PR asked to remove the typed-container scanning from
Value::matches_param, since RFC #191 explicitly decided against asum(Sequence<Any>)fallback. Without it, values whose static type degrades toAny(e.g.Map.keysreturningList<Any>) have no way to reach typed overloads. This PR adds the agreed middle ground as a standalone precursor: an explicit, runtime-checked cast —values.keys as List<Int>— so the element scan only happens where the user wrote it, never inside overload dispatch.Changes
askeyword and a postfix cast level between^and unary (Rust-like:a + b as Intcasts onlyb). Casts chain, and the type side reuses the existingTypeExprparser.5 as Floatis an error. Casts between types with no subtype relation ("foo" as Int) are rejected at compile time.requires_checkwhen the value's static type already proves the cast, so provable upcasts compile to nothing. This is a correctness rule, not an optimization: runtime iterators reportIterator<Any>and would falsely fail a redundant re-check.CheckType(Rc<StaticType>)opcode backed byValue::conforms_to, a deep, non-consuming check. Empty containers conform vacuously ([] as List<Int>succeeds). Kept separate frommatches_param, whose refusal to scan stays dispatch policy.🤖 Generated with Claude Code