Cella Documentation 0.1.0-dev Home Source

String templates

A template is a string with holes, handed to a function that writes it out. The language doesn't format anything itself. It gives the receiving function the literal pieces of the string, a format spec for each hole, and a borrow of each hole's value, and that function decides how to turn the values into text and where to put it.

A first template

A parameter of type fstr[K] takes a template. K is a trait that every hole's value must implement, and it's how the receiving code works with values it knows nothing else about:

Display: trait{	print: fun(imm self)} str: impl Display{	print: fun(self) => printf("%.*s", i32(self.byteLength), self.data)} i32: impl Display{	print: fun(self) => printf("%d", self)} say: fun(text: fstr[Display]){	for i in 0..text.holes	{		text.piece(i).print()		text.value(i).print()	} 	text.piece(text.holes).print()} main: fun{	val name: str = "Ada"	val hp = 30	say("{name} has {hp} hp, {hp * 2} at full health\n")}
Output
Ada has 30 hp, 60 at full health

The string passed to say has three holes, each an expression in braces. say walks them in order: it prints the literal piece before each hole, then the hole's value, and finally the piece after the last hole. The pieces are str values, so say prints them through Display as well.

str and i32 take part because the program implements Display for them. A template only accepts holes whose types implement its trait, built-in types included.

Reading a template

An fstr[K] has four members:

MemberTypeWhat it gives
holesusizethe number of holes
piece(i)strthe literal text before hole i; piece(holes) is the text after the last hole
spec(i)strthe format spec of hole i, empty when it has none
value(i)imm[dyn[K]]a read-only borrow of hole i's value

There is always one more piece than there are holes, and a piece can be empty. This function prints a template's parts instead of its text:

dump: fun(text: fstr[Display]){	printf("%zu holes:", text.holes)	for i in 0..=text.holes	{		val piece = text.piece(i)		printf(" '%.*s'", i32(piece.byteLength), piece.data)		if i < text.holes		{			val spec = text.spec(i)			printf(" [%.*s]", i32(spec.byteLength), spec.data)		}	} 	printf("\n")} main: fun{	dump("a{1}b{2:x}c{3:>8}d")	dump("{1}{2}")	dump("no holes")}
Output
3 holes: 'a' [] 'b' [x] 'c' [>8] 'd'
2 holes: '' [] '' [] ''
0 holes: 'no holes'

The indexes are usize values, and an index past the end panics like any other bounds check:

first: fun(text: fstr[Display]) -> usize => text.spec(0).byteLength main: fun{	first("no holes")}
Output
Panic at main.ce:8:45: index out of bounds

The members can't be assigned to:

clear: fun(text: fstr[Display]){	text.holes = 0Cannot reassign read-only properties} main: fun{}

Holes

A hole holds any expression: a name, arithmetic, a call, a comparison, a string literal or a match. Holes are evaluated left to right when the template is made, before the receiving function runs:

next: fun(mut counter: i32) -> i32{	counter += 1	ret counter} main: fun{	var counter = 0	say("{next(mut counter)} {next(mut counter)} {counter}\n")	say("{"quoted"} {match counter { 2 => 20, else => 0 }}\n")}
Output
1 2 2
quoted 20

Every hole's type must implement the template's trait:

Point: rec{	x: i32} say: fun(text: fstr[Display]){} main: fun{	val p = Point(1)	say("{p} {p.x}")'Point' doesn't implement 'Display'}

A hole can't be empty, and its braces must close in the same string. A literal { is written \{, and a } on its own is ordinary text, as in any string (see Braces in strings).

Format specs

A hole may end in a format spec: a : followed by any text up to the closing brace, as in {price:.2}. The spec is passed along as text through spec(i), and what it means is up to the receiving code. Spaces are kept, and escape sequences work as in the rest of the string, so \} puts a brace in a spec. A hole without a :, or with nothing after it, has an empty spec.

This Display takes the spec as a parameter, and i32 prints in hex when the spec is x:

Display: trait{	print: fun(imm self, spec: str)} str: impl Display{	print: fun(self, spec: str) => printf("%.*s", i32(self.byteLength), self.data)} i32: impl Display{	print: fun(self, spec: str)	{		if spec.byteLength == 1 && spec[0] == u8('x')			printf("%x", self)		else			printf("%d", self)	}} say: fun(text: fstr[Display]){	for i in 0..text.holes	{		text.piece(i).print("")		text.value(i).print(text.spec(i))	} 	text.piece(text.holes).print("")} main: fun{	val color = 16753920	say("{color} is {color:x} in hex\n")}
Output
16753920 is ffa500 in hex

Strings without holes

A template parameter also takes a string literal without holes or a str value. Either becomes a template with no holes and one piece:

title: fun -> str => "Cella\n" main: fun{	val name: str = "Ada\n"	say("plain\n")	say(name)	say(title())}
Output
plain
Ada
Cella

Nothing else converts to a template, not even a cstr:

say: fun(text: fstr[Display]){} main: fun{	val c: cstr = "x"	say(c)Cannot convert type 'cstr' to 'fstr[Display]'	say(5)Cannot convert type 'integer literal' to 'fstr[Display]'}

Outside a template, a string with holes is a constant: every hole must be a constant string, and the compiler joins the pieces into one string (see Interpolated strings).

When a function is overloaded for str and for a template, a string without holes goes to the str overload, and a string with holes goes to the template, even when its holes are constants:

show: fun(text: str) => printf("str\n")show: fun(text: fstr[Display]) => printf("template, holes: %zu\n", text.holes) unit: const str = "items" main: fun{	val count = 3	show("plain")	show("{unit}")	show("{count} {unit}")}
Output
str
template, holes: 1
template, holes: 2

Templates and traits

value(i) gives each hole as a borrowed trait object, so the receiving code can call the trait's members on it, and the members of the traits it requires. A record takes part by implementing the trait, and its implementation can use a template itself:

Point: rec impl Display{	x: i32	y: i32 	print: fun(self) => say("({self.x}, {self.y})")} log[T: Display]: fun(label: str, imm value: T) => say("{label}: {value}\n") relay: fun(imm value: dyn[Display]) => say("relayed {value}\n") main: fun{	val spot = Point(3, 4)	say("at {spot}\n")	log("point", imm spot)	log("number", 5)	relay(imm spot)}
Output
at (3, 4)
point: (3, 4)
number: 5
relayed (3, 4)

A hole can hold a generic value whose bound is the template's trait, as in log, or a dyn value of that trait, as in relay.

The argument of fstr must be a trait, exactly one:

a: fun(text: fstr[i32])'i32' is not a trait{} b: fun(text: fstr)'fstr' takes one generic argument{} c: fun(text: fstr[Display, Display])'fstr' takes one generic argument{} main: fun{}

A generic function can leave the trait open with a K: trait parameter, written out at the call:

count[K: trait]: fun(text: fstr[K]) -> usize => text.holes main: fun{	val n = 9	printf("%zu\n", count[Display]("{n} {n} {n}"))}
Output
3

Templates as values

fstr[K] is an ordinary type. A template can be made explicitly with fstr[Display](...), copied, passed on and returned:

Message: ref rec{	text: fstr[Display]} pass: fun(text: fstr[Display]) -> fstr[Display] => text main: fun{	val count = 3	val template = fstr[Display]("{count} items\n")	val copy = template	say(pass(copy)) 	val message = Message("message {count}\n")	say(message.text)}
Output
3 items
message 3

A template borrows its hole values, so it follows the rules for borrows. It can't outlive the values in its holes, a record that holds one must be a ref rec, and a module variable can't hold one:

Holder: rec{	text: fstr[Display]Cannot store borrows in records that aren't 'ref'Did you mean 'Holder: ref rec'?} saved: var fstr[Display] = "x"Cannot store borrows in module variables make: fun -> fstr[Display]{	val local = 5	ret "{local}"Cannot return borrows of local variables} compute: fun -> i32 => 4 show: fun(text: fstr[Display]){} main: fun{	val later: fstr[Display] = "{compute()}"Cannot borrow unstored values	show(later)}

The last error is about compute(): its result is a temporary that ends with its statement, so a template that keeps it can't be used afterwards.

Since a template holds borrows of Cella values, it can't appear in an ext signature:

ext{	log: fun(text: fstr[Display])Cannot use 'fstr[Display]' in 'ext' signatures} main: fun{}