Cone Language Reference

Text: str, String and cstr

On this page

Note: Making text from bytes, bytes as &str, does not yet check that the bytes are UTF-8, nor that nothing still writes them while the text is used.

Text comes in a few forms in Cone, each for a job. A string literal is a &str, a borrow of text that lasts as long as the program; a String, from the collections package, is text that grows; an owner of text, Uni[str] or Rc[str], owns fixed text; and a cstr is text on its way to a C function. They share one set of methods, written once on str, and one convention: text is UTF-8, and every length and position in it is a number of bytes, so it costs nothing to find. Where a position must fall between characters, the method says so. The String page of the Guide introduces all of this; this page lists it.

The methods of str#

A literal, a &str, a Uni[str] or Rc[str], and any type that lends one, a String among them, reaches them all.

  • len (bytes, not characters), isEmpty, bytes() (the &Array[u8] of the text), s[i] (the byte, a u8; an index past the end is a program error, as for a slice) and isCharBoundary(i) (a character starts at i, or the text ends there).
  • A part of the text: &s[a..<b], &s[a..] and &s[a..b] (the last byte included), each a &str borrowed from s, as a slice's range is a slice. They are the calls slice(a, b), sliceFrom(a) and sliceThrough(a, b). A bound that is out of order, past the end, or inside a character is a program error (a panic at the line of the slice): a &str is always whole characters. The range makes a part only when borrowed; the text itself has no size to hold.
  • Code points: chars(), charIndices() and charCount(). chars() is a cursor whose next() gives each code point in turn as a char, an Option[char], None past the last. charIndices() gives each with the byte index it starts at, a StrCharIndex with the fields offset and ch (what a lexer needs, and every offset is a place a slice may begin). charCount() counts them by walking the text, O(n), where len is the bytes. A byte that is not part of well-formed UTF-8 reads as U+FFFD, one for each such byte, so every char given is valid. These are code points, not graphemes: a letter and its combining accent are two.
  • startsWith(p), endsWith(p), find(p) (an Option of the byte index where p first starts, 0 for an empty p), findFrom(p, i) (at or after i) and contains(p).
  • trim(), trimStart() and trimEnd(): a &str without the ASCII whitespace (space, tab, line feed, form feed, carriage return) at the ends.
  • ==, !=, <, <=, >, >= and compare(o) (-1, 0 or 1): the bytes compared, and UTF-8's byte order is the order of the code points, so every capital comes before every small letter. They work between &str, Uni[str], Rc[str], a literal and a String, in any pair. === asks whether two references are the same place.
  • hash(h): feeds a hasher the bytes, then the terminator (h.writeStr), as a String does, so any kind of text hashes as the others of the same bytes do. A Dict[String, V] is asked by a &str for that reason.
  • cstr(), on an owner of text only: C strings, below.

How an owner of text (Uni[str], Rc[str]) is counted and freed is described with owning references.

String, the growable string#

String, from the collections package, is a list of bytes and one spare byte, so its block always has a NUL after the text, which the length leaves out and each change rewrites, for C. A String that holds nothing has no block at all. s.view() lends it as a &str (the static empty literal when it holds nothing), and the String folds the methods of str in as its own, so s.startsWith("a"), s.find(",") and &s[a..<b] work on it directly; == and the orderings compare its bytes with any other text. A literal is copied into a String wherever one is wanted (let s String = "core", names.push("core")). A String moves; copy() makes a second one.

Making one:

  • String.empty(), and String.withCapacity(n), with room for n bytes before it grows.
  • String.from(text), a copy of any text.
  • String.fromBytes(bytes), a copy of bytes that are text, such as a buffer read from a file or a socket. Nothing checks that they are UTF-8, as bytes as &str does not.
  • String.fromCStr(c), a copy of a cstr's bytes up to its zero. It takes a cstr and no raw pointer, so a pointer C may leave null is checked first and given as cstr.fromPtr(p).
  • String.join(parts, sep), the parts, an &Array[String] (list.view() of a List[String]), with sep between each two.

Adding to the end: s <- text or append(text), and s <- n for an integer as its digits (appendInt, appendUInt); appendBytes(bytes) (bytes that are text, unchecked like fromBytes), appendByte(b) and appendHex(n, width), the hex digits of n, padded with zeros to width.

Changing it in place, each on a var String: insert(i, text) (i between characters), remove(start, end) (the bytes from start up to end, both between characters), truncate(n), clear(), replace(from, to) (every occurrence), replaceByte(from, to), padStart(width, fill) and padEnd(width, fill) (to width bytes, the fill byte a space unless given), and lowerAscii() and upperAscii(), which change only the 26 ASCII letters.

Reading it: len(), isEmpty(), s[i] (a byte), equals(text) and equalsIgnoreAsciiCase(text), and, folded in from str, compare, startsWith, endsWith, find, findFrom, contains, chars, charIndices and the rest.

Copies of parts, which are Strings: sub(start, end), trim(), lines() and split(sep), the last two a List[String], split on a byte such as ','.

Finishing it: s.freeze() consumes the String and moves its block, shrunk to the text and its NUL, into a Uni[str]: the bytes are not copied. s.cstr() is its C string, the NUL already there, so nothing is copied; it is good until the String next changes, and a NUL inside the text is a panic, at the caller's line, since C would stop reading there.

mod strings;

import stdio use *;
import collections use *;

fn main() {
  var s = String.from("hello");
  s <- ", ";                               // append text
  s <- "world", 33i64;                     // and a number
  s.appendByte('!');
  print <- s.view(), "\n", s.len(), "\n";
  if s.startsWith("hello") { print <- "starts with hello\n"; }
  s.insert(5usize, "!!");
  s.replace("l", "L");
  s.upperAscii();
  print <- s.view(), "\n";
  let parts = String.from("a,b,c").split(',');   // copies, which are Strings
  print <- parts.len(), " ", parts[1usize].view(), "\n";
  let joined = String.join(parts.view(), "-");
  print <- joined.view(), "\n";
  let frozen = s.freeze();                 // the bytes move into a Uni[str]
  print <- frozen.len, "\n";
}

It prints:

hello, world33!
15
starts with hello
HELLO!!, WORLD33!
3 b
a-b-c
17

Text and bytes#

Text is a &str throughout, and a &Array[u8] is bytes. Any &str is a slice of bytes wherever one is wanted, so a literal goes where a slice of bytes is taken. The way back is written, bytes as &str, since bytes need not be UTF-8 (as):

fn say(bytes &Array[u8]) { print <- bytes as &str, "\n"; }

say("Padma");                       // the literal, as a slice of bytes

A string literal on its own is not an array but a &str, a borrow of its text that lasts as long as the program (managed references).

C strings: cstr#

A C function that takes text takes a const char *: one pointer to bytes that end in a zero, whose length it finds by scanning for the zero. Cone's name for that is cstr, a borrowed C string, and a C binding declares its strings with it: extern fn puts(s cstr) i32;. It is a pointer and no more (a one-field struct that crosses to C as one), and it promises the zero. Nothing else does: a &str may be part of a longer text and a slice never has one, so neither converts to a cstr, nor does a raw pointer. What does:

  • A literal, wherever a cstr is wanted: puts("hi"). A literal with a zero inside ("ab\x00cd") is refused there, since C would read only ab; the same text is fine as a &str, a String or an owner.
  • A String, s.cstr(), with no copy: its block always keeps a zero after its bytes, rewritten by each change, and a String that holds nothing has a static empty literal for one. A zero inside the text would end the string early for C, so cstr() scans for one (at memchr's speed) and panics, at the caller's line, if it finds it. It is good until the String next changes or dies.
  • An owner of text keeps the zero after its bytes too, however it was made (copied from a literal or a view, or by s.freeze()): owner.cstr() on a Uni[str] is its C string, with no copy, checking the text for a zero inside as a String's cstr() does (cstr.fromOwned(&owner) is the same call). A plain &str has no cstr(): it promises no zero.
  • A general &str, which promises nothing after its bytes, reaches C by a copy: String.from(s).cstr(). There is no owned C-string type; a String is one.
  • A raw pointer the caller knows ends in a zero, cstr.fromPtr(p): an argv entry, what getenv answers when it is not null. A C function that may take or answer a null string stays a *u8 in its binding.

From C, c.view() is the &str of a cstr, found by scanning for the zero and not checked to be UTF-8 (text is UTF-8 by convention), c.len() the count of bytes before it, and c.ptr() the pointer, for a function that takes a buffer. Copy the view into a String to keep it (String.fromCStr(c)); whether C or Cone frees what a C function answered is the binding's knowledge, which copies and then calls C's free.

let name String = "core";
libc.remove(name.cstr());                    // a pointer to its bytes and the zero, no copy
libc.remove("old.tmp");                      // a literal is a cstr where one is wanted
let owner Uni[str] = "scratch.tmp";
libc.remove(owner.cstr());                   // an owner of text is one too, no copy
let text = libc.strerror(2i32).view();       // a &str of C's answer, scanned
libc.fwrite(bytes as *u8, 1usize, bytes.len, f);  // a buffer and a count: a pointer, asked for

A slice is an address and a count, so it is not a pointer: a slice does not convert to a raw pointer by itself, and xs as *T takes the address of a slice's first element, for a C function that takes a buffer and a length (a slice coerces to no pointer). A buffer and its length are two parameters in C, so they stay a *u8 and a count. Talking to C has the rest of a binding.