docs / guide / The Cajeta Guide
Chapters — 13. Strings & formatting
- Introduction
- 0. Introduction
- 1. Installation
- 2. Kick the tires
- 3. Your first project
- 4. Running your application
- 5. Debugging
- 6. Keywords
- 7. Comments
- 8. Native types
- 9. Type kinds
- 10. Allocation
- 11. Ownership & borrowing
- 12. Control flow
- 13. Strings & formatting
- 14. Templates & wildcards
- 15. Lambdas & captures
- 16. Operator overloading
- 17. Inheritance
- 18. Annotations & synthesis
- 19. DI & aspects
- 20. Error handling
- 21. Reflection
- 22. Differentiation
- 23. Code coverage
- 23. Profiling
- 24. Notebooks
- 25. Writing XPU Kernels
13 — Strings & formatting
String (in cajeta.lang) is a class, not a primitive: immutable UTF-8
text. Every transformation returns a fresh String; nothing mutates in
place. Equality is content-based. Like every collection, a String reports
its element count with count() — codepoints here — while size() is the
byte length.
String s = " Hello, Cajeta! ";
String trimmed #= s.trim();
boolean has = trimmed.contains("Cajeta");
boolean hello = trimmed.startsWith("Hello");
String sub #= trimmed.substring(7, 13); // "Cajeta"
String swapped #= trimmed.replace("Cajeta", "World");
String loud #= trimmed.toUpperCase();
int64 codepoints = trimmed.count();
int64 bytes = trimmed.size();
boolean eq = trimmed.equals("Hello, Cajeta!"); // byte-for-byte
Run it: StringDemo.
Literals, owned strings, and windows
One type, three storage shapes underneath:
- A literal is a view of the program’s static data — no allocation, never freed.
- A string a method builds —
replace(),toUpperCase(), a+concatenation,StringBuilder.toString()— owns a heap buffer. Building from your own bytes works too:heap String(#buf, n)takes ownership of the buffer (the#transfers it; the String’s drop frees it). substring()andtrim()are zero-copy: the result is a windowed view sharing the source’s root buffer — no byte copy, whatever the length. Small-string-optimized sources are the exception; a window into an inline buffer would dangle, so those materialize a copy.
Transforming methods are declared #String (they transfer ownership,
chapter 11), so receive the result with #= —
String trimmed #= s.trim(); — and the drop chain reclaims it at scope exit.
The # goes on the store, never on the local’s type; a plain = on a
#String result is CAJETA_ERROR_OWNED_RESULT_NEEDS_TRANSFER. Windows follow
the slice rules from
chapter 11: a window that escapes its scope keeps the root buffer alive —
the root is promoted to the shared state and freed when the last view
drops. You never free a string in any of the three shapes.
substring(begin, end) is byte-indexed and half-open; indices clamp to the
valid range. Full method list: String reference.
Concatenation with +
+ concatenates and stringifies its operands — numbers, booleans, and class
values (via toString()) all work:
int32 version = 1;
String banner = "Cajeta v" + version + ", pi ~ " + 3.14;
Each + copies the whole accumulator, so building a long string with
repeated s = s + chunk in a loop is O(N²). Accumulate in a StringBuilder
instead.
StringBuilder
A growable byte buffer that materializes once. Short builds (up to 64 bytes)
stay in an inline buffer — a stack StringBuilder() build that small does no
heap allocation at all until toString():
StringBuilder sb = stack StringBuilder();
sb.append("Hello, ");
sb.append("world");
int32 soFar = sb.count(); // bytes accumulated
String built #= sb.toString(); // owned #String
Format templates in println
System.stdout.println(fmt, args...) treats {} in the format string as a
placeholder; each one consumes the next argument in order. Arguments dispatch
through a per-type formatter — integers, floats, String, and class values
via toString():
String name = "Cajeta";
int32 version = 1;
float64 pi = 3.14159;
System.stdout.println("hello {}, version {}", name, version);
System.stdout.println("{} + {} = {}, pi ~ {}", 2, 3, 2 + 3, pi);
Run it: FormatStringDemo. Full string design: String spec.
Next: Templates.