Strings
Unsettled String Internals
Zena's string architecture is actively evolving. While the high-level String
type is stable, several core changes are under consideration:
- Low-level operations like
getByteAtandsliceBytesand the direct constructornew(data, start, end, encoding)are currently exposed as temporary implementation details that will become private or restricted. - Multi-encoding support is partial (only WTF-8 is currently instantiated at runtime) and needs to be finished.
- Host-backed strings using the WebAssembly
js-string-builtinsproposal are under active development. - Value semantics are not fully implemented.
Strings in Zena are immutable sequences of Unicode characters represented by the
String class.
Strings are being designed to find a good balance of familiar, ergonomic, correct, portable, fast, and compact:
- Strings limit Unicode-unsafe operations, both for correctness and portability across hosts with different native encodings.
- Strings have simple implementations for code size, which may sacrifice performance on certain operations like concatenation.
- Strings sometimes trade off performance and memory overhead, for instance splitting strings is fast, but retains memory from the original source. Hashing can either be done on-demand, or cache the hash code, taking memory.
For the formal syntax specification, literal escape tables, and API signatures, see the Strings Reference.
Literals and templates ​
Zena supports string literals, multi-line continuations, and template literals.
String literals ​
String literals are enclosed in single quotes ('...') or double quotes ("..."):
let single = 'hello';
let double = "world";
Both forms produce instances of String and support standard escape sequences:
let message = "First line\nSecond line\t(indented)";
let quote = "He said, \"Hello!\"";
Template literals and interpolation ​
Template literals are enclosed in backticks (`...`) and support expression
interpolation via ${expression}:
let name = "Alice";
let count = 3;
let message = `Hello, ${name}! You have ${count} unread messages.`;
Expressions inside ${...} evaluate at runtime. In addition to strings, template
expressions directly format primitive numbers (i32, u32, i64, u64, f32, f64,
and narrow integers) and boolean values:
let a = 10;
let b = 20;
let summary = `${a} + ${b} = ${a + b}`; // "10 + 20 = 30"
let status = `Active: ${true}`; // "Active: true"
Arbitrary objects do not implicitly convert to strings in template expressions.
To interpolate a class instance, invoke an explicit method that returns a String.
A universal toString() protocol Planned is under
consideration, subject to dead-code elimination constraints so that unused formatting
logic is not preserved across the entire type hierarchy.
Template literals construct their result in a single pass, calculating the total length upfront to avoid intermediate string allocations.
Multi-line strings ​
Template literals preserve source line breaks and indentation:
let query = `
SELECT id, title
FROM articles
WHERE published = true
`;
Stripping indentation with dedent ​
When writing multi-line template literals inside indented functions or blocks, the
surrounding indentation is included in the string output. To format template literals
cleanly in source code while stripping common leading whitespace, use the dedent
template tag from zena:core:
import {dedent} from 'zena:core';
let printHelp = () => {
let usage = dedent`
Usage: zena <command> [options]
Commands:
build Compile the project
run Run the program
`;
console.log(usage);
};
dedent drops the opening and closing blank lines and removes the longest common
whitespace prefix across all content lines. Any interpolated values must be String
instances.
Line continuations ​
To format a long string literal across multiple lines in source code without
inserting newline characters into the string value, use a line continuation:
place a trailing backslash (\) immediately before the line break:
let description = "This is a single long line of text that spans \
multiple lines in the source file without inserting newline characters.";
Line continuations work in both standard string literals ("...", '...') and
template literals (`...`).
Operations and equality ​
Strings provide operators and methods for concatenation, comparison, searching, and case conversion.
Value equality ​
The == and != operators compare strings by value. Two strings are equal
if they contain identical character sequences:
let a = "hello";
let b = "hel" + "lo";
let sameValue = a == b; // true
let different = a != "world"; // true
Value equality checks compare character content efficiently, checking lengths and cached hashes before inspecting individual bytes.
Reference identity (`===`) is discouraged
Because String is currently implemented as a class, the compiler allows the
reference identity operators === and !==. However, strings have value semantics,
and separately allocated strings with identical contents do not share pointer
identity ("hello" === ("hel" + "lo") evaluates to false).
Pointer identity is an implementation detail that should be non-observable for
value types. Reference identity comparison on strings is unstable and planned to
be disallowed by the compiler, matching records and tuples. Always use == and !=
for string comparisons.
Concatenation ​
Use the + or += operator to join strings:
let greeting = "Hello, " + name + "!";
var path = "/usr";
path += "/bin";
For formatting strings with variables or expressions, template literals
are generally preferred over chaining + operators.
When assembling strings dynamically or repeatedly inside loops, chaining +
allocates a new string and copies bytes on every iteration. Use
StringBuilder instead to accumulate
content with buffered geometric growth.
Searching and splitting ​
The String class provides core search and splitting methods:
let text = "image.png";
let isImage = text.endsWith(".png"); // true
let isDoc = text.startsWith("doc_"); // false
let hasDot = text.contains("."); // true
let csv = "apple,banana,cherry";
let fruits = csv.split(","); // FixedArray<String> with 3 elements
For cursor-based tokenization, custom parsing, or scanning character by character
across safe Unicode boundaries, use StringReader
from zena:core.
ASCII case conversion ​
The asciiLowerCase() and asciiUpperCase() methods transform ASCII characters:
let title = "Zena Language";
let lower = title.asciiLowerCase(); // "zena language"
let upper = title.asciiUpperCase(); // "ZENA LANGUAGE"
These methods operate strictly on ASCII bytes (0x41..0x5A and 0x61..0x7A).
Restricting case mapping to ASCII avoids embedding large Unicode case-folding
tables into WebAssembly binaries. When a string contains no characters that need
changing, these methods return this directly with zero allocations.
Slices, views, and copies ​
Zena implements strings using a view-based architecture similar to Go.
Every String instance is a lightweight header consisting of:
- A reference to a backing
ByteArray - An inclusive
#startbyte offset - An exclusive
#endbyte offset - An encoding tag
Zero-copy slicing ​
Slicing a string reuses the existing bytes without duplication. Calling .sliceBytes()
creates a new String header referencing the existing ByteArray with shifted
offsets:
let full = "Hello, world!";
let hello = full.sliceBytes(0, 5); // String view: start = 0, end = 5
Slicing runs in O(1) constant time and allocates only the small String header.
The memory retention hazard ​
Because a slice holds a reference to its entire parent ByteArray, the garbage
collector cannot reclaim the backing array as long as any slice remains reachable.
This introduces a memory retention hazard when storing small substrings extracted from large documents:
// Reads a 50MB document into memory
let largeDocument = readFile("dataset.json");
// Extracting a small key as a slice
let key = largeDocument.sliceBytes(120, 130);
// Hazard: The entire 50MB ByteArray remains pinned in memory
// because `key` holds a reference to it.
Detaching with copy() ​
To release parent memory, call .copy() on any slice you plan to retain
long-term:
let key = largeDocument.sliceBytes(120, 130).copy();
The .copy() method allocates a new ByteArray sized precisely to the slice
(10 bytes in this example) and copies the bytes over. The parent largeDocument
can then be garbage collected as soon as it goes out of scope.
Use zero-copy slices for short-lived intermediate parsing, and call .copy()
on substrings that will be stored in long-lived data structures.
Unicode and safety ​
String safety in Zena centers on preventing encoding corruption and providing safe text parsing.
Subscript indexing and Unicode safety ​
Direct integer subscripting (str[i]) produces a compile-time error:
let text = "Zena";
let char = text[0];
Error:
In UTF-8, characters frequently span between one and four bytes. Subscripting by raw integer offsets risks splitting multi-byte code points, corrupting text.
Zena draws inspiration from Swift's string model: strings present views over
Unicode characters rather than raw byte arrays, and navigation uses opaque index
objects (String.Index). Future releases will introduce opaque index types and
Unicode code-point and grapheme-cluster iterators. For text parsing today, use
StringReader.
Safe parsing with StringReader ​
To navigate and parse strings safely at Unicode code point boundaries, use
StringReader from zena:core:
import {StringReader} from 'zena:core';
let parseWord = (input: String): String? => {
let reader = new StringReader(input);
reader.skipWhitespace();
if (reader.isAtEnd) {
return null;
}
// Mark the start position at a valid code point boundary
let start = reader.mark();
// Decode and advance across code points (1 to 4 bytes each)
while (!reader.isAtEnd && isLetter(reader.peek())) {
reader.advance();
}
// Extract a verified substring slice
return reader.sliceFrom(start);
};
StringReader provides:
- Code point decoding:
peek()andadvance()decode full 1- to 4-byte UTF-8 sequences and return 32-bit Unicode code points. - Byte inspection for ASCII:
peekByte()andadvanceByte()provide fast paths for known single-byte delimiters ({,},,,:). - Safe slicing with mark():
reader.mark()captures the byte offset at a verified code point boundary. Slices extracted viasliceFrom(mark)orsliceRange(start, end)are guaranteed to produce well-formed UTF-8. - Scanning utilities:
skipWhitespace(),readUntil(needle), andskipBytesWhile(predicate).
Encodings and WTF-8 ​
Zena adopts the WTF-8 ("Wobbly Transformation Format") encoding standard.
Standard UTF-8 rejects surrogate code points (U+D800 through U+DFFF).
However, JavaScript strings are sequences of 16-bit code units that may contain
unpaired surrogates. WTF-8 extends UTF-8 to permit well-formed surrogate sequences,
guaranteeing lossless round-tripping when exchanging strings with JavaScript
and DOM APIs.
Strings created inside Zena are always well-formed Unicode.
Performance and representations ​
Efficient string processing in Zena involves choosing the right construction tools and understanding how strings are represented at runtime.
The concatenation performance hazard ​
The + operator allocates a new ByteArray equal to the combined length of both
operands and copies bytes from each:
let greeting = "Hello, " + name;
While suitable for simple expressions, using + inside loops is a severe
performance hazard. Similar to Java, repeated concatenation allocates and
copies increasingly large buffers on every iteration, leading to O(n²) time
complexity:
// Hazard: Allocates and copies a new buffer on every iteration
var csv = "";
for (let item in items) {
csv += item + ","; // Quadratic buffer copying
}
For static multi-variable formatting, use template literals, which measure the total length and allocate the result buffer once.
Building strings with StringBuilder ​
For iterative string construction across loops or conditional logic, use
StringBuilder from zena:core:
import {StringBuilder} from 'zena:core';
let buildCsv = (items: Array<String>): String => {
let builder = new StringBuilder(64); // Initial capacity in bytes
for (let item in items) {
builder.append(item);
builder.appendByte(44); // 44 = ASCII comma ','
}
return builder.toString();
};
StringBuilder offers several performance advantages:
- Chunked geometric growth: Appends write directly into preallocated chunk buffers, avoiding full-array reallocations.
- Direct primitive formatting: Methods such as
appendI32,appendU32,appendI64, andappendF64write numbers directly into the buffer as decimal ASCII without creating temporary string objects. - Zero-copy completion: When accumulated content fits in the initial chunk,
toString()returns a zero-copyStringview sharing that buffer.
Polymorphic backing storage ​
Zena is designed around a single String API backed by specialized internal
storage implementations:
| Representation | Storage | Use case | Status |
|---|---|---|---|
| GC strings | WebAssembly GC ByteArray |
Literals, concatenations, general computation | Active (default) |
| Host strings | externref via js-string-builtins |
Zero-copy sharing with JavaScript and the DOM | In development |
| Linear memory strings | Pointer and byte length | Zero-copy WASI filesystem, network, and C FFI | Planned |
| Ropes | Tree of string segments | High-throughput text editing and append trees | Planned |
Today, all strings in Zena are backed by WebAssembly GC byte arrays. Active
compiler development is integrating the WebAssembly js-string-builtins proposal,
which will enable JavaScript host strings to enter Zena with zero transcoding
overhead.