Cella Documentation 0.1.0-dev Home Source

Bool, char and strings

This chapter covers bool, char for single characters, and the two string types: str, a pointer and a length, and cstr, a C string. It also covers string literals, their escape sequences and suffixes, interpolated strings with constant holes, and nameOf.

bool

A bool is true or false and takes one byte. ! negates it, and && and || are logical and and or. They stop as soon as the result is known, so the right side runs only when it's needed. &, | and ^ also work on bools and always evaluate both sides; ^ is true when exactly one side is. == and != compare two bools.

check: fun(name: cstr, value: bool) -> bool{	printf("%s ", name)	ret value}main: fun{	val a = check("first", false) && check("second", true)	printf("-> %d\n", a)	val b = check("first", false) & check("second", true)	printf("-> %d\n", b)	printf("%d %d %d\n", !a, true ^ false, a == b)}
Output
first -> 0
first second -> 0
1 1 1

&=, |= and ^= assign the result back. bools have no ordering, and no arithmetic.

main: fun{	var ready = true	val failed = false	ready &= !failed	ready ^= true	printf("%d\n", ready)}
Output
0

Conditions in if, while and the other control-flow forms must be bools. Integers aren't true or false on their own; compare them instead, as in count != 0. A bool converts to an integer with i32(flag), as 0 or 1, but nothing converts to bool.

main: fun{	val count = 1	val flag: bool = 1Cannot convert type 'integer literal' to 'bool'	val made = bool(count)No constructor for 'bool' accepts argument of type 'i32'	val inverted = !countOperator '!' cannot be applied to 'i32'	val ordered = true < falseNo operation defined for '<' between 'bool' and 'bool'}

char

A char is one Unicode character, stored as its code point in 4 bytes. Any code point from U+0000 to U+10FFFF is a valid char except the surrogates U+D800 to U+DFFF. A char literal is a character in single quotes, and the source file's UTF-8 can hold any character directly:

main: fun{	val letter = 'a'	val accented = 'é'	val emoji = '😀'	printf("%u %u %u %zu\n", u32(letter), u32(accented), u32(emoji), sizeOf(letter))}
Output
97 233 128512 4

A literal holds exactly one character, or one escape sequence:

main: fun{	val empty = ''Invalid character literal	val two = 'ab'Invalid character literal	val unknown = '\q'Invalid character literal}

Operations on char

Adding an integer to a char, or subtracting one, gives another char. Subtracting two chars gives the distance between them as an i32. chars compare by code point, and a for loop can walk a range of them, as Control flow shows.

main: fun{	val letter = 'c'	val next = letter + 1	val back = letter - 2	val offset = letter - 'a'	printf("%c %c %d\n", next, back, offset)	printf("%d %d\n", letter < 'd', letter == 'c')}
Output
d a 2
1 1

A char isn't an integer: other arithmetic, the bitwise operators and mixing a char with an integer type are errors.

main: fun{	val letter = 'a'	val byte: u8 = 97	val sum = 'a' + 'b'No operation defined for '+' between 'char' and 'char'	val doubled = letter * 2No operation defined for '*' between 'char' and 'integer literal'	val masked = letter & 1No operation defined for '&' between 'char' and 'integer literal'	val same = byte == letterNo operation defined for '==' between 'u8' and 'char'}

A step past U+10FFFF or into the surrogates is checked like integer overflow. With constants it's a compile error, "Addition overflows 'char'", and at run time a panic with "char overflow" in builds with overflow checks.

Converting chars

A char converts to any integer type with T(c), giving its code point, and an integer converts to a char with char(n). Neither conversion happens implicitly. Both are checked: a code point that doesn't fit the integer type, or an integer that isn't a valid char, is an error for a constant and a "conversion overflow" panic at run time.

toChar: fun(code: u32) -> char => char(code)main: fun{	val code = u32('A')	printf("%u %c\n", code, toChar(code + 1))	printf("%c\n", toChar(55296))}
Output
65 B
Panic at main.ce:8:35: conversion overflow
implicit: fun(letter: char) -> u32 => letterCannot convert type 'char' to 'u32'narrow: fun -> u8 => u8('€')'8364' doesn't fit in 'u8'surrogate: fun -> char => char(55296)'55296' doesn't fit in 'char'

Character literals with a suffix

An integer type suffix after a char literal makes it an integer holding the code point, as in 'a'u8. This is how byte-oriented code compares bytes with characters, since a u8 and a char don't mix. The code point must fit the type.

isDigit: fun(byte: u8) -> bool => byte >= '0'u8 && byte <= '9'u8main: fun{	val text: str = "a7b"	val digit = text[1] - '0'u8	printf("%d %d %d\n", isDigit(text[0]), isDigit(text[1]), digit)	printf("%d %d\n", '€'u16, '\n'i32)}
Output
0 1 7
8364 10
main: fun{	val a = '€'u8'€' doesn't fit in 'u8'	val b = 'a'f32Cannot use 'f32' on char literals}

Escape sequences

char and string literals share these escape sequences:

EscapeMeaning
\n, \r, \tnew line, carriage return, tab
\0the zero character
\\, \', \"a backslash, a single quote, a double quote
\a, \b, \f, \vbell, backspace, form feed, vertical tab
\xHHthe character with hex code HH, from 00 to 7F
\oNNNthe character with octal code NNN, from 000 to 177
\uXXXXthe code point XXXX, exactly four hex digits
\UXXXXXXXXthe code point XXXXXXXX, exactly eight hex digits

Strings also take \{ and \} for braces, which interpolated strings otherwise use. \0 always means the zero character, so "\012" is a zero followed by the characters 1 and 2; octal codes are written with \o. \x and \o cover only the ASCII range. \u and \U take any code point that is a valid char, and a string stores it as its two to four UTF-8 bytes.

dump: fun(text: str){	for i in 0..text.byteLength		printf(" %02x", text[i]) 	printf("\n")}main: fun{	dump("tab\there")	dump("C:\\new")	dump("\x41\o102\"")	dump("caf\u00e9 \U0001F600")	dump("\012")}
Output
 74 61 62 09 68 65 72 65
 43 3a 5c 6e 65 77
 41 42 22
 63 61 66 c3 a9 20 f0 9f 98 80
 00 31 32

Any other backslash sequence, or one with the wrong digits, is an error:

main: fun{	val a = "\q"Invalid escape sequence in string literal	val b = "\x80"Invalid escape sequence in string literal	val c = "\u12"Invalid escape sequence in string literal	val d = "\uD800"Invalid escape sequence in string literal}

str

A str is a read-only view of UTF-8 text: a pointer to its first byte and the number of bytes, 16 bytes in all on a 64-bit target. The two parts are its read-only properties data, a ptr[u8], and byteLength, a usize. A str doesn't own its bytes, and nothing guarantees a zero byte after them.

The length counts bytes, not characters: a character outside ASCII takes two to four bytes.

main: fun{	val plain: str = "hello"	val accented: str = "héllo"	val emoji: str = "😀"	printf("%zu %zu %zu %zu\n", plain.byteLength, accented.byteLength, emoji.byteLength, sizeOf(plain))}
Output
5 6 4 16

C's printf prints a str with %.*s, which takes the length as an int and then the pointer:

show: fun(text: str) => printf("[%.*s]\n", i32(text.byteLength), text.data)main: fun{	show("hello world")	show("")}
Output
[hello world]
[]

Indexing bytes

text[i] reads the byte at index i as a u8. The index is a usize, or a type that converts to one implicitly, and it's checked against byteLength: reading past the end stops the program with "index out of bounds".

byteAt: fun(text: str, index: usize) -> u8 => text[index]main: fun{	val text: str = "abc"	var total: u32 = 0	for i in 0..text.byteLength		total += text[i] 	printf("%c %u\n", byteAt(text, 2), total)	printf("%c\n", byteAt(text, 3))}
Output
c 294
Panic at main.ce:8:47: index out of bounds

A str can't be changed through: its bytes and its properties are read-only.

main: fun{	var text: str = "hello"	text[0] = 72Cannot write through 'str'	text.byteLength = 3Cannot reassign read-only properties}

Building a str

str(data, byteLength) makes a str from a ptr[u8] and a length, the reverse of its two properties. This is how code takes part of a string, or views bytes from elsewhere as text, such as a buffer or a C string. str() is the empty string. Below, text.data + start points start bytes further on; Pointers and raw memory covers pointer arithmetic.

show: fun(text: str) => printf("[%.*s]\n", i32(text.byteLength), text.data)slice: fun(text: str, start: usize, length: usize) -> str => str(text.data + start, length)main: fun{	val text: str = "hello world"	show(slice(text, 6, 5))	show(str(text.data, 4)) 	val name: cstr = "from C"	show(str(name, strlen(name))) 	val empty = str()	printf("%zu\n", empty.byteLength)}
Output
[world]
[hell]
[from C]
0

The compiler doesn't check that the pointer and length describe valid UTF-8. A str built from a pointer borrows whatever the pointer came from, so one built from a local buffer can't be returned; Borrows covers the rules.

local: fun -> str{	var buffer: array[u8, 4]	buffer[0] = 65	ret str(@buffer[0], 1)Cannot return borrows of local variables}

== and + don't work on str values. + works only on constant strings, described below.

cstr

A cstr is a pointer to a zero-terminated string, which is what C functions take and return. It's 8 bytes on a 64-bit target and has no length; C functions such as strlen find the end by looking for the zero byte. c[i] reads byte i as a u8; with no length to compare against, the index isn't checked. A cstr can be null, and it converts implicitly to a plain ptr, the untyped pointer described in Pointers and raw memory.

main: fun{	val word: cstr = "world"	printf("%s %zu %c %d\n", word, strlen(word), word[0], word[5]) 	var missing: cstr = null	printf("%d\n", missing == null)	missing = word	printf("%d %zu\n", missing == null, sizeOf(missing))}
Output
world 5 w 0
1
0 8

A str doesn't convert to a cstr, since its bytes need not be followed by a zero, and a cstr has no arithmetic.

main: fun{	val text: str = "hello"	puts(text)Cannot convert type 'str' to 'cstr'	val word: cstr = "world"	val rest = word + 1No operation defined for '+' between 'cstr' and 'integer literal'}

String literals

A string literal in double quotes has no type of its own, like a number. It becomes a str or a cstr where it's used. As a cstr, its bytes are followed by a zero; the zero isn't counted in a str's byteLength. A literal's byteLength and data can be read directly, and byteLength is a constant.

show: fun(text: str) => printf("[%.*s]\n", i32(text.byteLength), text.data)main: fun{	show("as a str")	puts("as a cstr")	var buffer: array[u8, "abc".byteLength]	printf("%zu %zu\n", "héllo".byteLength, buffer.length)}
Output
[as a str]
as a cstr
6 3

A local declared from a literal takes its type from how the function uses it, and is a str when nothing decides. A literal used both ways needs two literals, or an explicit type:

show: fun(text: str){}main: fun{	val greeting = "hi"Cannot infer the type of 'greeting' from 'str' and 'cstr'	show(greeting)	puts(greeting)}

A string literal must end on the line it starts on, and two literals next to each other don't join; + joins constant strings.

String literal suffixes

A suffix of u8, u16 or u32 turns a string literal into an array of its encoded text, with no zero at the end: u8 gives the UTF-8 bytes, u16 the UTF-16 code units and u32 the code points. This is useful for byte signatures and for APIs that take UTF-16.

main: fun{	val magic = "GIF89a"u8	val accented = "hé"u8	val units = "hé"u16	val points = "h😀"u32	printf("%zu %d %d\n", magic.length, magic[0], magic[5])	printf("%zu %d %d %d\n", accented.length, accented[0], accented[1], accented[2])	printf("%zu %d %d\n", units.length, units[0], units[1])	printf("%zu %d %d\n", points.length, points[0], points[1])}
Output
6 71 97
3 104 195 169
2 104 233
2 104 128512

magic is an array[u8, 6]; Arrays covers arrays. Other suffixes are errors:

main: fun{	val a = "abc"i32Cannot use 'i32' on string literals	val b = "abc"f64Cannot use 'f64' on string literals}

Constant strings

String literals, const declarations of type str or cstr, and the results of nameOf are constant strings. A val isn't one, even at the top level of a module. + joins constant strings into a new one, at compile time. A str constant and a cstr constant don't join with each other.

title: const str = "Cella"version: const cstr = "1.0"full: const str = title + " " + "language"tagged: const cstr = version + "-beta"show: fun(text: str) => printf("%.*s\n", i32(text.byteLength), text.data)main: fun{	show(full)	printf("%s\n", tagged)	val joined = "x" + "y" + "z"	show(joined)}
Output
Cella language
1.0-beta
xyz
title: const str = "Cella"label: const cstr = "c"main: fun{	var name = "world"	val joined = title + name'name' isn't a constant	val mixed = title + labelNo operation defined for '+' between 'str' and 'cstr'	val number = "a" + 5No operation defined for '+' between 'string literal' and 'integer literal'}

Interpolated strings

A string literal can contain holes in braces, as in "{title} v{version}". Outside a template, an interpolated string is a constant string: every hole must hold a constant string, and the compiler joins the pieces. The result is a string literal, usable as a str or a cstr. Holes can hold str and cstr constants alike, nameOf, + of constant strings, and other string literals, including interpolated ones.

title: const str = "Cella"version: const cstr = "1.0"banner: const cstr = "{title} v{version}!"Point: rec{	x: i32}main: fun{	const name = "world"	printf("%s\n", banner)	printf("%s\n", "Hello, {name}! {nameOf(Point)} {"a" + "b"} [{"{title}"}]")}
Output
Cella v1.0!
Hello, world! Point ab [Cella]

A hole with a value that isn't a constant string is an error. Holes that hold numbers or other run-time values, and holes with format specs like {price:.2}, belong to string templates, where a function receives the pieces and the values and decides how to write them.

title: const str = "Cella"main: fun{	val count = 5	val name: str = "world"	printf("count = {count}\n")'i32' is not a string	printf("hello {name}\n")'name' isn't a constant	printf("spec {title:x}\n")Cannot use format specs in constant strings}

Braces in strings

A { in any string starts a hole, so a literal brace is written \{. A } on its own is an ordinary character, and \} is accepted too. This matters for C format strings and for text like code or JSON:

main: fun{	printf("\{%d}\n", 5)	printf("struct \{ int x; }\n")}
Output
{5}
struct { int x; }
main: fun{	printf("{%d}\n", 5)Expected an expression after '{'	printf("x{}y\n")Expected an expression}

A string literal with holes can't take a suffix: "a{x}"u8 is "Cannot use 'u8' on interpolated strings".

nameOf

nameOf(x) is a constant string holding the name of x as it's written in the code, without its qualifiers: nameOf(geometry.area) is "area". It takes anything with a name, such as a type, function, module, variable, parameter, field, method, enum case or label, and checks that the name exists. Like a string literal, it's usable as a str or a cstr.

nameOf only looks the name up. Inside a type, its fields, methods and cases can be named on their own, as in nameOf(unwrap) or nameOf(none), even where using them would need a receiver or a known type. Type arguments pick the type or function and aren't part of the name: nameOf(Option[i32].none) is "none".

Point: rec{	x: i32	y: i32}Team: enum{	red	blue 	favorite: fun -> cstr => nameOf(blue)}Option[T]: enum{	none	some(value: T)}describe: fun(count: i32) -> cstr => nameOf(count)main: fun{	val point = Point(1, 2)	printf("%s %s %s\n", nameOf(point), nameOf(point.y), nameOf(Point.x))	printf("%s %s %s %s\n", nameOf(Team), nameOf(Team.blue), nameOf(describe), describe(1))	printf("%s %s\n", Team.favorite(), nameOf(Option[i32].none))	val text = nameOf(Point)	printf("%zu\n", text.byteLength)}
Output
point y x
Team blue describe count
blue none
5

A name that doesn't exist is reported as usual, and anything other than a name is an error:

Point: rec{	x: i32}first: fun -> str => nameOf(Point.z)Type 'Point' has no member 'z'second: fun -> str => nameOf(5)Expected a namethird: fun -> str => nameOf("main")Expected a namemain: fun{}