Strings and text
Strings hold text, and Salam is Unicode from the ground up, so text in any script behaves like any other text. The one thing to keep in mind is that a string is stored as UTF-8 bytes, and some operations count bytes while others count characters.
Ways to write a string
"..."is an ordinary string. Escapes work inside it:\n(a new line),\t(a tab),\",\\, and\uHHHHfor any Unicode character by its code.`...`(backticks) is a raw string: everything inside stays exactly as written, backslashes and quotes included, and it may span several lines. It is the easy way to write file paths, regular expressions and JSON."""..."""is a multi-line string in which escapes still work.
func main:
println "first line\nsecond line"
println "column\tcolumn"
println "She said: \"hello\""
println "price in \u20ac, greeting \u0633\u0644\u0627\u0645"
println `the path C:\temp and "quotes", with nothing escaped`
poem := """The children of Adam are limbs of one body,
having been created of one essence."""
println poem
endAn ordinary "..." string has to end on the line where it starts; for text over several lines use """ or backticks. Persian files have one more form, «...», which an English file does not accept.
Joining strings
Join strings with +. When one side is a number or a bool, it is turned into text for you, so no separate conversion is needed. += adds to the end of a mut string. Salam has no "{name}" style interpolation; + is how text is put together.
func main:
name := "Arash"
age := 25
message := "Hello " + name + ", you are " + age + " years old."
println message
println "Total: " + 2 + 3
println "Total: " + (2 + 3)
println "price " + 2.5 + ", paid " + true
mut log := "started"
log += ", connected"
log += ", " + 3 + " files sent"
println log
endLook at the second line of output. + works from left to right, so "Total: " + 2 is already text by the time 3 arrives, and the result is Total: 23. Put the arithmetic in parentheses when you want the sum.
Bytes and characters
A string is stored as UTF-8. An English letter takes one byte, but é takes two, and so does a Persian letter. That is why there are two kinds of length and two kinds of position:
len(),substrandfindcount bytes. They are fast, and exact for ASCII text.char_count(),char_substr,char_atandchar_findcount characters. Use them whenever the text might not be plain English.
func preview(title: str, limit: int): str:
if title.char_count() <= limit:
ret title
end
ret title.char_substr(0, limit) + "..."
end
func main:
word := "café"
println word.len(), word.char_count()
greeting := "سلام"
println greeting.len(), greeting.char_count()
println greeting.char_at(0), greeting.char_find("م"), greeting.find("م")
println word[0], word[0] as int
println preview("Short title", 24)
println preview("A long headline about the weather in Tabriz", 24)
end"سلام" is 8 bytes but 4 characters, and the letter م is character 3 but byte 6. The preview function cuts a title to a number of characters, which is what a reader expects. Cutting it with substr could split a letter in half and print a broken symbol. Indexing with word[0] also reads a single byte, as a char, so it is only the right tool for ASCII.
String methods
These methods are built into every string. None of them changes the string it is called on; they give back a new value.
func main:
text := " the Salam programming language "
clean := text.trim()
println clean
println "salam".len(), "salam lang".substr(0, 5)
println "salam lang".find("lang")
parts := "apple,pomegranate,grape".split(",")
println parts.len(), parts.get(1)
println "hello".concat("!")
println "report.pdf".ends_with(".pdf"), "salam lang".starts_with("sal"), "salam lang".includes("m l")
println "salam".upper(), "SALAM".lower(), "-".repeat(10)
println "42".to_int() + 1, "2.5".to_float() * 2.0
end| Method | What it does |
|---|---|
len() | The length of the string in bytes |
char_count() | The number of characters, which differs from the byte count outside ASCII |
substr(start, count) | Cut a piece out of the string, counted in bytes |
char_substr(start, count), char_at(i) | The same, counted in characters |
find(text), char_find(text) | Where some text starts (in bytes or in characters), or -1 |
starts_with(text), ends_with(text), includes(text) | Test the start, the end, or anywhere in the string |
split(separator) | Break the string into a Vector<str> of pieces |
trim() | Remove the spaces at both ends |
concat(text) | Join text onto the end |
upper(), lower(), repeat(n) | Change the case, or repeat the text n times |
to_int(), to_float() | Turn text into a number |
A string can never be changed in place: s[0] = 'x' is a compile error. Build a new string instead.
The str package
For more than the built-in methods, import the str package. It splits and joins, trims prefixes and suffixes, replaces, counts and changes case. The example reads a settings line of the kind you find in a config file or a form.
import str
func main:
line := "name = Sara; city = Shiraz; lang = fa"
fields := str.Split(line, ";")
defer fields.free()
each field in fields:
pair := str.Split(field, "=")
println str.Trim(pair.get(0)), "->", str.Trim(pair.get(1))
pair.free()
end
path := "/home/sara/notes/todo.txt"
println str.TrimPrefix(path, "/home/sara/"), str.LastIndex(path, "/")
println str.Replace("2026-10-07", "-", "/"), str.Count("banana", "a")
words := str.Fields(" too many spaces ")
defer words.free()
println str.Join(words, " ")
println str.Title("the little prince"), str.Contains(path, "notes")
endSplit and Fields return a Vector, which has to be freed, hence the defer ... free() lines. Fields splits on any run of spaces, so it is the one to use on words a person typed.
Building text in pieces
Every + makes a new string. That is fine for a line or two, but text built up in a loop is better made with a StringBuilder: str.NewBuilder starts one, str.BufAppend adds text, str.BufStr reads out the result and str.BufFree releases it. fmt.PadLeft and fmt.PadRight from the fmt package pad text to a width in characters, which lines up the columns of this receipt.
import str
import fmt
func main:
items := ["tea", "bread", "cheese"]
prices := [45, 30, 120]
mut b := str.NewBuilder()
defer str.BufFree(b)
mut total := 0
each i, item in items:
str.BufAppend(b, fmt.PadRight(item, 10))
str.BufAppend(b, fmt.PadLeft("" + prices[i], 6))
str.BufAppend(b, "\n")
total += prices[i]
end
str.BufAppend(b, str.Repeat("-", 16) + "\n")
str.BufAppend(b, fmt.PadRight("total", 10))
str.BufAppend(b, fmt.PadLeft("" + total, 6))
println str.BufStr(b)
endComparing strings
== and != compare the contents of two strings, and < and > put them in order. str.Compare gives -1, 0 or 1, the shape a sort function wants. str.EqualFold compares without caring about upper and lower case, which is what you want for user names and email addresses.
import str
func main:
println "apple" == "apple", "apple" < "banana", "Zebra" < "apple"
println str.Compare("apple", "banana"), str.Compare("pear", "pear"), str.Compare("pear", "apple")
typed := "Sara"
stored := "SARA"
println typed == stored, str.EqualFold(typed, stored)
println "گل" < "لاله"
endThe order is the order of the character codes, not dictionary order. Every capital letter comes before every small one, so "Zebra" < "apple" is true. Persian has the same catch: the letters پ چ ژ گ have higher codes than the rest of the alphabet, so "گل" < "لاله" is false even though گ comes before ل in Persian.
Text and numbers
to_int() and to_float() read a number from the start of a string, and they accept Persian and Arabic digits as well as English ones. They never report an error: they stop at the first character that is not part of the number, and text with no number at all gives 0. So when the text comes from a user, check it first. The example cleans up a phone number typed with Persian digits and then checks that only digits are left.
import str
func clean_phone(raw: str): str:
ascii := str.ToEnglishDigits(raw)
ret str.Replace(str.Replace(ascii, " ", ""), "-", "")
end
func all_digits(s: str): bool:
if s.len() == 0:
ret false
end
codes := str.CodePoints(s)
defer codes.free()
each c in codes:
if not str.IsAsciiDigit(c):
ret false
end
end
ret true
end
func main:
phone := clean_phone("۰۹۱۲ ۳۴۵-۶۷۸۹")
println phone, phone.len(), all_digits(phone)
println all_digits(clean_phone("0912 34x 6789"))
println "۲۵۰۰۰".to_int() + 1, "12abc".to_int(), "abc".to_int()
println str.ToPersianDigits("Invoice 1405, total " + 25000)
endstr.ToEnglishDigits and str.ToPersianDigits convert the digits inside a string and leave everything else alone. Salam always prints numbers with English digits, so ToPersianDigits is the last step when the output is for Persian readers.
Things to watch for
len()counts bytes. For anything a person reads (a character limit, a column width, a preview) usechar_count()and thechar_methods.+goes left to right:"n" + 3 + 4is"n34", and3 + 4 + "n"is"7n"."12abc".to_int()is12and"abc".to_int()is0; neither tells you something was wrong.- A string ends at its first zero byte:
"a\0b".len()is1. Keep binary data in a byte buffer, not in astr. - Ordering with
<follows character codes, not the alphabet of any language.
The full list of text functions, including Unicode classification, normalization and grapheme clusters, is on the str page of the library reference.