Words & interpolation#
> word is the string type. Its methods are compiler builtins, and {path} interpolation has precise rules.
Literals and escapes#
A word literal is double-quoted.
| Escape | Meaning |
|---|---|
\n | newline |
\r | carriage return |
\t | tab |
\" | double quote |
\\ | backslash |
\xNN | byte with hex value NN |
\x1b is the ESC byte, which is how you produce coloured console output — see Coloured output.
\x00 is rejected at lex time: "embedded NUL escape is not supported". V3 words use a NUL-terminated runtime representation, so a word can never contain a zero byte. A malformed hex escape (fewer than two hex digits) is also a lex error.
An unterminated literal reports "unterminated string literal" pointing at the opening quote.
Methods#
All of these are compiler builtins — no import, and they work under freak check as well as freak build.
-- `word` is FREAK's string type. These methods are compiler builtins,
-- available with no import.
task main() -> void {
pilot s: word = " Muv-Luv Alternative "
say s.trim()
say word_from_int(s.length())
say s.trim().to_upper()
say s.trim().to_lower()
say word_from_bool(s.contains("Luv"))
say word_from_bool(s.trim().starts_with("Muv"))
say word_from_bool(s.trim().ends_with("ive"))
say s.trim().replace("Luv", "Love")
say s.trim().substring(0, 3)
say s.trim().char_at(0)
-- Concatenation with `+`, or with the builtin.
say "Muv" + "-" + "Luv"
say word_concat("XM", "3")
-- Numeric conversion in both directions.
say word_from_int("108".to_int())
say format_num("2.5".to_num())
say char_to_word(70)
}
Muv-Luv Alternative
23
MUV-LUV ALTERNATIVE
muv-luv alternative
true
true
true
Muv-Love Alternative
Muv
M
Muv-Luv
XM3
108
2.5
F
| Method | Signature | Notes |
|---|---|---|
.length() | -> int | Character count |
.trim() | -> word | Strips leading and trailing whitespace |
.to_upper() | -> word | |
.to_lower() | -> word | |
.contains(needle) | word -> bool | |
.starts_with(prefix) | word -> bool | |
.ends_with(suffix) | word -> bool | |
.replace(old, new) | word, word -> word | Replaces every occurrence |
.substring(start, len) | int, int -> word | Start index and length, not an end index |
.char_at(i) | int -> word | A one-character word, not a char type |
.to_int() | -> int | |
.to_num() | -> num | |
.checksum() | -> int | Runtime hash |
.parse_int() | -> int | Checked; sets the parse status on failure |
.parse_num() | -> num | Checked; sets the parse status on failure |
.repeated(n) | int -> word | The word repeated n times |
.substring(start, len) takes a length as its second argument. The bible describes slice(from, to); V3's builtin is not that function. "abcdef".substring(1, 3) yields "bcd".
Snapshot helpers#
Seven further word methods exist for the compiler's own line-and-field serialisation format. They are part of the builtin table and will type-check in your code, but they are internal plumbing rather than a general-purpose API:
.snapshot_escape(), .snapshot_unescape(), .snapshot_line_count(), .snapshot_line(i), .snapshot_lines(), .snapshot_field_count(), .snapshot_field_raw(i).
Checked parsing#
.to_int() and .to_num() convert silently — a word that is not a number yields 0, indistinguishable from parsing "0". As of v0.14.2 there is a checked pair that reports failure out of band:
say word_from_int("42".parse_int()) -- 42
say word_from_int(parse_status()) -- 0, clean
say word_from_int("nope".parse_int()) -- 0
say word_from_int(parse_status()) -- 1, failed
parse_clear_status()| Call | Signature | Meaning |
|---|---|---|
w.parse_int() | -> int | Parse, 0 on failure |
w.parse_num() | -> num | Parse, 0.0 on failure |
parse_status() | -> int | 0 clean, non-zero if a parse failed |
parse_clear_status() | -> void | Reset it |
The status is global and sticky — it stays set until you clear it — so check it immediately after the parse you care about, exactly as with ByteBuffer.status(). This is V3's substitute for maybe<int>, which does not exist.
std/convert.fk also offers word_to_int_safe(s), which returns 0 on failure without touching any status.
Concatenation and conversion#
say "Muv" + "-" + "Luv" -- operator
say word_concat("XM", "3") -- builtin, identical+ on two words concatenates, and += appends in place as of v0.14.2. + on a word and a number is a type error — convert first:
pilot n: int = 42
say "answer: " + word_from_int(n)
say "answer: {n}" -- or interpolate| Builtin | Signature |
|---|---|
word_from_int(i) | int -> word |
word_from_bool(b) | bool -> word |
word_to_int(w) | word -> int |
word_concat(a, b) | word, word -> word |
word_join(handle) | int -> word |
char_to_word(code) | int -> word |
format_num(f) | num -> word |
parse_num(w) | word -> num |
format_num prints a num without a trailing .0 when it is integral — format_num(12.0) gives 12.
Indexing#
word[i] yields a one-character word. It is the only indexable type in V3.
pilot s: word = "abc"
say s[1] -- b
say s.char_at(1) -- b, identicalInterpolation#
{path} inside a double-quoted word substitutes a binding's value.
-- `{path}` inside a double-quoted word interpolates a binding.
-- A path is an identifier plus zero or more `.field` hops, and must
-- resolve to word, int, num or bool. Anything that is not a valid path
-- stays literal text.
-- (`Aviator`, not `Pilot`: V3 keywords are case-insensitive, so `Pilot`
-- is the `pilot` keyword and cannot name a shape.)
shape Aviator {
name: word
score: int
}
impl Aviator {
task summary(self) -> word {
give back "{self.name} scored {self.score}"
}
}
task main() -> void {
pilot callsign: word = "Valkyrie"
pilot sorties: int = 12
pilot rate: num = 0.75
pilot active: bool = true
say "callsign={callsign} sorties={sorties}"
say "rate={rate} active={active}"
pilot p: Aviator = Aviator { name: "Meiya", score: 98 }
say "shape: {p.name} / {p.score}"
say p.summary()
-- Not a path, so it is printed literally:
say "literal {1 + 2} braces"
}
callsign=Valkyrie sorties=12
rate=0.75 active=true
shape: Meiya / 98
Meiya scored 98
literal {1 + 2} braces
The rules are exact:
- A path is an identifier followed by zero or more
.fieldhops:name,p.score,self.engine.thrust. - The path must resolve to
word,int,numorbool. Any other type is a type error. - Every segment must be a valid identifier — so it cannot be a keyword, in any casing.
- A matched
{...}whose body is not a valid path is left as literal text, braces included."literal {1 + 2} braces"prints exactly that. - An unmatched
{is literal text. - Unknown bindings are a compile error: "unknown interpolation binding 'x'".
Interpolation substitutes paths only — never calls, never expressions. "{p.sum()}" is not a call; it is literal text, because sum() is not a valid path segment. Compute into a binding first: ``fk pilot total: int = p.sum() say "sum={total}"
Dotted shape interpolation, including {self.field} inside a method, is verified working on the LLVM backend. It is not a claimed executable path on the C backend.
Printing without a newline#
say always appends a newline. To put several pieces on one line, build the word first and say it once — or use carriage-return and cursor-movement escapes to redraw. All three techniques are in Printing on the same line.
Building words incrementally#
Repeated + concatenation copies the whole accumulated word each time. word_builder::* avoids that. The builder is an int handle, not a shape.
-- `word_builder::*` accumulates a word without the quadratic cost of
-- repeated `+` concatenation. The builder is an `int` handle.
task main() -> void {
pilot b: int = word_builder::new()
word_builder::append(b, "Mission: ")
word_builder::append(b, "Valkyries")
word_builder::append_char(b, 32) -- a space, by codepoint
word_builder::append_int(b, 4)
say word_from_int(word_builder::length(b))
-- `finish` consumes the builder and returns the accumulated word.
say word_builder::finish(b)
-- with_capacity pre-allocates; clear reuses; discard frees unused.
pilot c: int = word_builder::with_capacity(64)
word_builder::append(c, "scratch")
word_builder::clear(c)
word_builder::append(c, "reused")
say word_builder::finish(c)
}
20
Mission: Valkyries 4
reused
| Call | Signature | Notes |
|---|---|---|
word_builder::new() | -> int | |
word_builder::with_capacity(n) | int -> int | Pre-allocate |
word_builder::reserve(b, n) | int, int -> void | Grow the capacity |
word_builder::append(b, w) | int, word -> void | |
word_builder::append_char(b, code) | int, int -> void | By codepoint |
word_builder::append_int(b, n) | int, int -> void | |
word_builder::length(b) | int -> int | |
word_builder::capacity(b) | int -> int | |
word_builder::clear(b) | int -> void | Keep the buffer, drop the content |
word_builder::finish(b) | int -> word | Consumes the builder |
word_builder::discard(b) | int -> void | Free without producing a word |
finish consumes the builder — do not reuse the handle afterwards. Use clear to keep building with the same allocation, and discard when you are abandoning it without producing a word.
This is V3's answer to the bible's WordBuilder from §7.2. The API is namespaced calls over a handle rather than methods on an object.
Standard-library word tasks#
std/string.fk is ordinary FREAK source linked in by freak build and freak run. Its tasks are global and take the word as a normal argument.
-- std/string.fk ships as plain FREAK source and is always linked in by
-- `freak build` / `freak run`. Its tasks are global; there is no import.
task main() -> void {
say string_repeat("ha", 3)
say string_reverse("FREAK")
say word_from_int(string_count("banana", "an"))
say string_pad_left("7", 4, "0")
say string_pad_right("7", 4, ".")
say word_from_int(string_index_of("alternative", "native"))
say word_from_bool(is_digit("5"))
say word_from_bool(is_alpha("x"))
say word_from_bool(is_whitespace(" "))
}
hahaha
KAERF
2
0007
7...
5
true
true
true
| Task | Signature |
|---|---|
string_repeat(s, count) | word, int -> word |
string_reverse(s) | word -> word |
string_count(haystack, needle) | word, word -> int |
string_index_of(s, needle) | word, word -> int, -1 when absent |
string_pad_left(s, width, pad) | word, int, word -> word |
string_pad_right(s, width, pad) | word, int, word -> word |
string_split(s, delim) | word, word -> word (an encoded word, not an array) |
string_join(parts, sep) | word, word -> word |
string_trim(s) | word -> word |
string_substring(s, start, end) | word, int, int -> word — an end index, unlike the builtin |
string_replace(s, old, new) | word, word, word -> word |
string_contains / string_starts_with / string_ends_with | -> bool |
is_digit(c) / is_alpha(c) / is_whitespace(c) | word -> bool |
These duplicate several builtin methods. The builtins are cheaper and work under freak check; the std/ tasks exist because the standard library is itself written in FREAK. Where the two disagree — substring — prefer the builtin and mind the length-vs-end-index difference.
Splitting into an array#
string_split returns an encoded word, not an array handle. To get a real array, walk the characters yourself:
-- A complete small program: split a sentence into words, count them,
-- and report the longest one. Uses shapes, arrays, loops and std tasks.
shape Report {
total: int
longest: word
}
impl Report {
task show(self) -> word {
give back "total={self.total} longest={self.longest}"
}
}
task split_words(text: word, out: int) -> void {
pilot mut current: word = ""
pilot mut i: int = 0
repeat text.length() times {
pilot c: word = text.char_at(i)
if c == " " {
if current.length() > 0 {
array_push(out, current)
current = ""
}
} else {
current = current + c
}
i += 1
}
if current.length() > 0 {
array_push(out, current)
}
}
task analyse(text: word) -> Report {
pilot words: int = array_new()
split_words(text, words)
pilot mut longest: word = ""
pilot mut i: int = 0
repeat array_len(words) times {
pilot w: word = array_get(words, i)
if w.length() > longest.length() {
longest = w
}
i += 1
}
-- `result` is a reserved word in V3, so the binding is `summary`.
pilot summary: Report = Report { total: array_len(words), longest: longest }
array_release(words)
give back summary
}
task main() -> void {
pilot r: Report = analyse("the beta will not negotiate with humanity")
say r.show()
say word_from_int(r.total)
say r.longest.to_upper()
}
total=7 longest=negotiate
7
NEGOTIATE