fix: reject indexing a String instead of segfaulting - #896
Conversation
`s[i]` type checked as `Char` while the code generator had no `String` case for index access. `lower_index_access` lowers every index through the list layout, so a `String` header was reinterpreted as a `List` header: the bounds check ran against a garbage length and the load dereferenced a garbage pointer. An out-of-bounds read was reachable from ordinary Raven code with no FFI and no raw pointers. Reject the receiver in `check_index` rather than implementing it. Under the current String model there is nothing coherent for `s[i]` to return: `length()` counts bytes and `char_at` takes a byte offset, but `Char` is a Unicode scalar and one byte of a multi-byte encoding is not one. Returning the i-th codepoint instead would make `[]` the only codepoint-indexed operation in an otherwise byte-indexed API and would make indexing an O(n) scan. The error points at `char_at(i)` and `byte_at(i)`, which already cover the use case unambiguously. Both the read and the assignment-target paths go through this check. Nothing depended on the old behavior: it crashed every time, and no example, corpus entry, or stdlib module indexes a String. Closes #894 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TC4Ms6squc3YU27Tz3MmMU
📝 WalkthroughWalkthroughThe type checker now rejects ChangesString indexing validation
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/tycheck/tests.rs (1)
134-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the accessor hint in the rejection tests.
These tests verify
cannot index into a \String`, but they do not verify the promisedchar_at(i)andbyte_at(i)` guidance. Assert that the diagnostic hint or rendered diagnostic contains both accessor names.Also applies to: 151-164
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/tycheck/tests.rs` around lines 134 - 149, Update the rejection tests around indexing a String, including indexing through the related access path, to assert that the rendered diagnostic or hint contains both `char_at(i)` and `byte_at(i)`. Preserve the existing assertion for the `cannot index into a `String`` message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/tycheck/tests.rs`:
- Around line 134-149: Update the rejection tests around indexing a String,
including indexing through the related access path, to assert that the rendered
diagnostic or hint contains both `char_at(i)` and `byte_at(i)`. Preserve the
existing assertion for the `cannot index into a `String`` message.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7a03d071-ef54-4a4c-8646-a53b1e1efd64
📒 Files selected for processing (3)
docs/v2/guide/language-reference.mdsrc/tycheck/expr.rssrc/tycheck/tests.rs
Summary
s[i]on aStringpassed the type checker and then segfaulted at run time. The type checker gave the expression typeChar, but the code generator has noStringcase for index access —lower_index_accesslowers every index through theListpath, so aStringheader was reinterpreted as aListheader. The bounds check ran against a garbage length and the load dereferenced a garbage pointer, making an out-of-bounds read reachable from ordinary Raven code with no FFI and no raw pointers.This rejects the receiver in the type checker rather than implementing
Stringindexing, because under the current String model there is nothing coherent fors[i]to return:Stringis a byte string.length()counts bytes, andchar_at(i)is documented as "Byte offset, not codepoint: for a multi-byte character this returns one byte of the encoding."Charis a Unicode scalar value, and one byte of a multi-byte encoding is not a valid one — so returning the byte at offsetias aCharis not well defined.[]the only codepoint-indexed operation in an otherwise byte-indexed API (inconsistent withlength()), and would turn indexing into an O(n) scan — a performance trap inside a loop.The error points at the byte-oriented accessors that already cover the use case unambiguously.
If O(1) codepoint indexing is wanted later, that is a separate design change (a codepoint-indexed string type, or a
chars()view) rather than a fix for this crash.Before
After
Changes
src/tycheck/expr.rs—check_indexreplaces theTy::Str => Ok(Ty::Char)arm with aTypeError::Customcarrying a hint that nameschar_atandbyte_at. The comment records why the receiver is rejected rather than lowered, so the arm is not "fixed" back into a crash later.src/tycheck/tests.rs— three regression tests: indexing aStringis rejected, assigning through aStringindex is rejected, and list indexing still type checks to the element type.docs/v2/guide/language-reference.md— a note in the strings section stating that aStringis not indexable and pointing at the byte accessors, with a worked example.Both the read path and the assignment-target path (
t[0] = 'z') route throughcheck_index, so the write side of the crash is covered by the same change.Test plan
cargo checkcargo test— 971 passed, 0 failed across the workspace (968 before this branch, plus the 3 new tests). No regressions.let c = t[0]andt[0] = 'z'are both rejected with the new diagnostic.xs[1]on aList<Int>,char_at(0), andbyte_at(0)all still compile and produce2,a,97.Related issues
Closes #894
Notes for reviewers
The main thing to sanity-check is the decision to reject rather than implement. The alternative — lowering
Strindexing to a bounds-checked runtime call — is not much more code, so the argument for rejecting is a semantic one, laid out above and in the issue. If you would rathers[i]work and return a codepoint, that changes the String model (indexing andlength()would disagree about units) and is worth deciding deliberately.Nothing depended on the old behavior: it crashed 100% of the time, and no example, corpus entry, or stdlib module indexes a
String. Thes[0]inexamples/v2/use_cmp.rvis aList<Int>returned bysort, not a string.Not addressed here, since they are separate issues: the corrupt binary-operator diagnostic (#895), and the fact that the type checker's
Ty::Strsupport for indexing was never exercised by a codegen test — a golden case per indexable receiver type would have caught this.Generated by Claude Code
Summary by CodeRabbit
Bug Fixes
char_at(i)orbyte_at(i)for string access.Documentation