Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/v2/guide/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ fun main() {
}
```

A `String` is a byte string, and it is not indexable: `s[i]` is a
compile error. `length()` counts bytes, so reach for the byte-oriented
accessors in [`std/string`](standard-library.md#stdstring) instead —
`char_at(i)` returns the one-byte substring at byte offset `i`, and
`byte_at(i)` returns that byte as an `Int`. Both take a byte offset, so
for a multi-byte character they address one byte of its encoding rather
than the whole character.

```rust
import std/string

fun main() {
let s = "abc"
print(s.char_at(0)) // a
print(s.byte_at(0)) // 97
}
```

A block string uses triple quotes and is raw: no escapes are processed
and newlines are preserved exactly.

Expand Down
18 changes: 17 additions & 1 deletion src/tycheck/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3016,7 +3016,23 @@ impl<'a, 'b> Checker<'a, 'b> {
let recv_resolved = self.infer.resolve(&recv);
match recv_resolved.strip_self() {
Ty::List(t) => Ok(*t.clone()),
Ty::Str => Ok(Ty::Char),
// A `String` is a byte string, so `s[i]` has no well defined
// element type: `length()` counts bytes and `char_at` is a byte
// offset, but `Char` is a Unicode scalar, and one byte of a
// multi-byte encoding is not one. Indexing used to type check as
// `Char` here while the back end had no `String` case, so the
// index lowered through the list layout and read the string
// header as a list header, segfaulting at run time. Point at the
// byte-oriented accessors, which are unambiguous.
Ty::Str => Err(RavenError::ty(
TypeError::Custom("cannot index into a `String`".to_string()),
span.clone(),
)
.with_hint(
"a String is a byte string: use `char_at(i)` for the one-byte \
substring at byte offset `i`, or `byte_at(i)` for that byte \
as an `Int`; both come from `std/string`",
)),
Ty::Error => Ok(Ty::Error),
Ty::Var(_) => {
// Unify the receiver with a list of fresh element type.
Expand Down
39 changes: 39 additions & 0 deletions src/tycheck/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,45 @@ fn unary_address_of_is_rejected() {
}
}

#[test]
fn indexing_a_string_is_rejected() {
// `s[i]` used to type check as `Char` while the back end had no `String`
// case for index access, so the index lowered through the list layout and
// read the string header as a list header, segfaulting at run time
// (issue #894). It is a type error now, pointing at the byte accessors.
let err = check("fun main() {\n let t = \"abc\"\n let c = t[0]\n}\n").unwrap_err();
match err {
RavenError::Type(b, _, _) => assert!(
matches!(*b, TypeError::Custom(ref m) if m.contains("cannot index into a `String`")),
"got: {:?}",
b
),
other => panic!("expected a type error, got {:?}", other),
}
}

#[test]
fn assigning_through_a_string_index_is_rejected() {
// The assignment target goes through the same check, so the write side of
// the crash in issue #894 is rejected too.
let err = check("fun main() {\n let t = \"abc\"\n t[0] = 'z'\n}\n").unwrap_err();
match err {
RavenError::Type(b, _, _) => assert!(
matches!(*b, TypeError::Custom(ref m) if m.contains("cannot index into a `String`")),
"got: {:?}",
b
),
other => panic!("expected a type error, got {:?}", other),
}
}

#[test]
fn indexing_a_list_is_still_allowed() {
// The guard above is specific to `String`; list indexing keeps working and
// still yields the element type.
check("fun main() {\n let xs = [1, 2, 3]\n let n: Int = xs[1]\n}\n").unwrap();
}

#[test]
fn inferred_type_violating_a_bound_is_rejected() {
// A call that infers a type argument violating the bound is rejected the
Expand Down
Loading