STRING is valid UTF-8 text. Its contents are immutable: an operation returns a new string, and a variable can be assigned that new value. This keeps text operations predictable even when other code has read access to the original.
Positions count Unicode scalars
text = "Aé🌍"
PRINT LEN(text)
PRINT ByteCount(text)
PRINT MID(text, 1, 1)
PRINT text[-1]
PRINT text[0:2]The length is 3 Unicode scalar values but 7 UTF-8 bytes. Positions are zero-based; negative positions count backward from the end. A slice includes its start and excludes its finish. A visible character can contain multiple scalars, such as a letter followed by a combining accent. Scalar positions are not display columns or grapheme-cluster positions. Use explicit byte APIs for binary protocols.
LEFT, MID, RIGHT, search, trim, case conversion, padding and replacement are
provided by the string reference
. $ aliases such as
MID$ exist, but arbitrary dollar-suffixed variable names are not supported.
Write text across several source lines
Triple double quotes introduce a block literal when the opening quotes are immediately followed by a newline. This is useful for SQL, templates, messages and sample data that would otherwise need many quoted fragments.
name = "Ada"
message = DEDENT TRIM f"""
Hello, {name}!
Ticket: {42:04d}
"""
PRINT messageOutput:
Hello, Ada!
Ticket: 0042
The raw block preserves indentation and original newline bytes. DEDENT removes
the exact common space/tab prefix of nonblank content lines. TRIM removes only
boundary newline sequences; it does not strip horizontal whitespace. Together,
they dedent first and then trim. Either modifier order is accepted, but duplicate
modifiers are errors. Ordinary TRIM(text) remains a separate whitespace-trimming
function.
Use f""" for interpolation and plain triple quotes for literal braces. Layout
processing happens before interpolation, so indentation inside a substituted
value is preserved. Interpolation expressions themselves must remain single-line.
The opening and closing delimiter lines’ whitespace matters: closing indentation
is content unless removed by the layout rules. Tabs are literal characters,
independent of the editor’s display width.
The newline after the opener distinguishes a block from existing doubled-quote
syntax: """word""" still means an ordinary string containing quoted word.
Six consecutive quotes inside a block represent three literal quotes. No
backslash escapes are added. Blocks work in arguments, checked constant defaults
and FORMAT options under the usual constant-expression rules. The
multiline-string reference
specifies blank lines,
quote runs and CR/LF preservation. The IDE highlights blocks across scrolling
and edits without treating their contents as new declarations.
Split, join and search
words = SPLIT("red,green,blue", ",")
PRINT LEN(words)
PRINT JOIN(words, " / ")
PRINT UPPER("hello")
PRINT TRIM(" BASIC ")
PRINT LEN(SPLIT("Aé"))SPLIT with a nonempty separator splits on that exact text. SPLIT with no separator, or an empty separator, produces one string per scalar; it is not whitespace splitting. Empty input produces an empty list in that form. For line processing, use the dedicated LINES API and its documented line-ending behavior.
Exact string equality compares the encoded text; it does not silently normalize Unicode or apply locale rules. UPPER/LOWER and case folding are distinct from locale-aware collation. To compare text for an application’s identifiers, choose and document the normalization/case policy you require.
Format output
name = "Ada"
count = 42
price = 12.3456
PRINT f"Hello, {name}!"
PRINT f"Count: {count:04d}"
PRINT f"Price: {price:.2f}"
PRINT f"{{literal braces}}"The f must immediately precede the opening quote. Expressions inside braces
evaluate once from left to right. A colon introduces a static format specification.
Common presentations are d for decimal integers, x for hexadecimal, f for
fixed decimals, e for scientific notation and % for percentages. Width is a
minimum; .2f requests two fractional decimal places. Strings support alignment
and a scalar precision limit.
Specifications support <, >, ^ alignment, fill, sign, base prefixes, zero
padding and grouping. Read the included format-string reference for the complete
mini-language. Dynamic nested width/precision, debug =, custom record formatting
and locale presentation are not implemented. Invalid specifications are compiler
errors, including in unused functions.
For repeated construction, use StringBuilder and append operations from the buffer reference rather than repeatedly concatenating an ever larger prefix. Only initialized content is visible; reserving capacity is not creating text.
Regular expressions
pattern = CompileRegex("[0-9]+")
IF LET found = RegexFind(pattern, "item 42") THEN
PRINT found.Text
PRINT found.Start
END IF
PRINT RegexReplace("[0-9]+", "item 42", "number")Compile once when using a pattern repeatedly. The compiled Regex is an owned resource, while matches and captured text are independent values. String-pattern overloads compile on each call. The engine is now implemented in BASIL itself; no Rust runtime or separate regex library is needed. The default Native dialect retains the pinned Rust-regex syntax baseline: Unicode classes and named captures are supported, but lookaround and backreferences reject. Choose the EcmaScript dialect explicitly for those constructs.
PRINT RegexIsMatch("^([a-z]+)-\1$", "blue-blue", Dialect=RegexDialect.EcmaScript)
PRINT RegexIsMatch("^(?!admin$)[a-z]+$", "user", Dialect=RegexDialect.EcmaScript)
PRINT RegexIsMatch("^ada$", "ADA", Flags="i")Dialect changes pattern matching, not replacement syntax. Both dialects expand
$1, ${name} and $$; these are separate from BASIL interpolation.
EcmaScript is the stateless ECMAScript 2025 Unicode-mode pattern profile, not a
JavaScript RegExp object. Flags d/g/v/y are rejected. Its backreferences have no
linear-time guarantee. Configure RegexLimits for untrusted work; limits measure
logical work, memory and results rather than wall-clock deadlines.
TRY
limits = RegexLimits {MaxPatternBytes=3}
pattern = CompileRegex("abcd", Limits=limits)
CATCH exhausted AS RegexLimitError
PRINT "Pattern limit reached"
END TRYAll regex operations declare RegexError, including searches with a compiled pattern. A limit failure is an error, never FALSE, NONE or a partial success. Syntax and unsupported-profile failures have specific error types too. Normal compiler scheduling and cancellation apply during native regex work. The regex reference explains captures, empty matches, flags and every limit.
Convert external encodings
A STRING always contains valid UTF-8. A legacy file is bytes until you explicitly decode it; opening a file does not guess its encoding. Use ReadBinaryFile with DecodeText, then EncodeText before writing external bytes. No import is required.
bytes = EncodeText("café", TextEncoding.Windows1252)
PRINT LEN(bytes)
PRINT DecodeText(bytes, TextEncoding.Windows1252)
result = EncodeTextWithInfo("a€😀", TextEncoding.ASCII, TextErrorPolicy.Replace)
PRINT DecodeUtf8(result.Bytes)
PRINT result.Info.ReplacementsStrict conversion is the default: malformed bytes and unrepresentable characters
raise TextEncodingError. Replace is an explicit lossy policy: decoding inserts
U+FFFD; legacy encoding inserts ?. WithInfo returns output plus counters, so a
successful replacement need not silently hide lost characters. Error Offset and
Length count source bytes, including UTF-8 bytes when encoding a STRING; they are
not the scalar indexes used by ordinary string operations.
TRY
text = DecodeText([BYTE(255)], TextEncoding.UTF8)
CATCH err AS TextEncodingError
PRINT err.Offset
PRINT err.Length
END TRYProfiles cover UTF-8, explicit-endian UTF-16/32, ASCII, Latin1, Windows1252, CP437, ShiftJIS, Big5, EUCKR and separate GB18030_2000/GB18030_2022 versions. Choose the version required by your data: these are precise mapping profiles, not aliases for every regional Windows codec. No detection, normalization or newline conversion is implicit. The reference lists the exact profiles.
BOM handling is explicit. Decode defaults to Preserve; Consume removes a matching initial BOM and Require demands it. Neither changes the selected byte order. Encode defaults to Omit; Emit writes one initial BOM, even for empty text. These BOM options reject legacy encodings. maxOutputBytes defaults to 64 MiB per call or chunk and counts emitted bytes; it does not bound an entire stream.
Decode text arriving in chunks
A network read can split a character between packets. A TextDecoder retains the unfinished sequence until the next chunk rather than treating each read as a complete document.
decoder = NewTextDecoder(TextEncoding.UTF16LE)
first = DecodeTextChunk(BYREF decoder, [BYTE(65)])
PRINT LEN(first)
PRINT DecodeTextChunk(BYREF decoder, [BYTE(0)], final=TRUE)
PRINT DecoderInfo(decoder).FinishedFinishTextDecoder supplies the final empty chunk when the input ends separately. NewTextEncoder, EncodeTextChunk and FinishTextEncoder provide the other direction. A converter finishes exactly once; later chunk/finish calls fail. Converters are copyable opaque values: copying one forks its state independently. BYREF gives exclusive access while advancing it. A recoverable chunk failure leaves that state unchanged and returns no partial chunk; already written earlier chunks remain written. Info snapshots contain cumulative byte counters, replacement counts and the first replacement offset. Keep an application-level total limit when processing untrusted streams.