Strings Learn Their Characters
Unicode graphemes arrive for strings, leading-dot spelling gets friendlier, generic effects specialize cleanly, and Neovim setup becomes one command.
Today's Talk issue is about making the friendly spelling match the hard machinery underneath. Strings now speak in user-perceived Unicode characters, leading-dot enum cases need less qualification, generic effects carry their type arguments through handlers, and Neovim setup moves from README chore to CLI command.
The front-page change is Unicode for strings. String and
Substring now iterate over Character, where a character is an extended
grapheme
cluster from UAX #29, Unicode 17.0.0. Combining marks, emoji ZWJ sequences, flags, and Indic conjuncts
each
count as one user-visible character.
count() is documented as O(n), iteration allocates nothing, and Talk deliberately avoids
integer
character indexing. There is no s[i] hiding a scan or splitting a cluster. Positional work
goes
through iteration, find, or byte offsets returned by the byte API itself.
print("héllo 👋🏽".count()) print( "👨👩👧👦".count() ) print("🇬🇧🇬🇧🇬🇧".count()) for ch in "héllo" { print(ch) }
Byte access did not disappear; it became explicit. s.utf8() returns a
borrowed UTF8View with count(), at(index), and
slice(start, byte_count). The old byte-shaped operations leave String and
Substring, and the old length field is renamed byte_count so it
cannot
be mistaken for a character count.
find and find_from now return Int? instead of -1.
The
successful value is still a UTF-8 byte offset, valid for utf8().slice. Equality remains
byte
equality, and ill-formed UTF-8 is safe to iterate: invalid bytes classify as replacement units while
each
Character still views the original raw bytes.
let s = "café" print(s.count()) print(s.utf8().count()) match s.find("fé") { .some(offset) -> print(offset) .none -> print(0 - 1) }
Characters are for people; bytes are for people who asked for bytes.- The day's release, in one line
Unqualified dot syntax keeps getting more useful. The VM and type tests now cover an
enum
value passed as .a(123) where the parameter type supplies the enum, and core library code
is
beginning to read naturally with return .some(value) and tail-position .none.
The editor side also got sturdier. Completion preserves the dotted receiver while users type incomplete control-flow conditions, call and effect arguments, arrays, tuples, records, record spreads, match scrutinees, and match arm bodies. Parser recovery keeps enough of the half-written expression for the LSP to type the receiver before the member itself resolves.
enum AB { case a(Int) case b(Int) } func callMe(param: AB) -> Int { match param { .a(x) -> x .b(x) -> x } } callMe(.a(123))
Effect handlers now handle generic effects end to end. A declaration such as
effect 'state<T>(value: T) -> T produces rows that remember each
instantiation,
like ! <'state<Int>>, while one @handle 'state covers
every
instantiation in its dynamic extent.
The handler body is generic over the effect parameters, declared bounds still apply, and each instantiation receives its own specialized capability behind the scenes. The real-program corpus now includes generic effects at one and several instantiations, so this is checked as ordinary executable Talk, not just a solver stunt.
effect 'ask<T>(value: T) -> T func probe<T>(x: T) -> T { 'ask(x) } func run() -> Int { @handle 'ask { v in continue v } let n = probe(21) let b = probe(true) let direct = 'ask(n + n) if b { direct } else { 0 } } print(run())
talk setup nvim now installs TalkTalk Neovim runtime files directly.
The
command downloads the bundled ftdetect, ftplugin, indent, and
syntax files into Neovim's stdpath('data')/site runtime root.
If Neovim is not available, setup falls back through NVIM_APPNAME,
XDG_DATA_HOME,
and HOME/.local/share. --force overwrites differing TalkTalk runtime files,
while
--target-dir installs into an explicit runtime root for development or unusual editor
layouts.
talk setup nvim talk setup nvim --force talk setup nvim --target-dir ~/.local/share/nvim/site
The Unicode work is mostly self-hosted. The UTF-8 decoder, the UAX #29 break engine,
and
the Character layer live in core/Unicode.tlk; the runtime still knows only
bytes.
Break categories sit in a generated core/UnicodeData.tlk table packed into a small
7-bit-clean
string literal and binary-searched in place.
All 766 official GraphemeBreakTest-17.0.0 cases pass, alongside a semantic corpus run on both engines.
The
compiler addition that made the Talk implementation possible is small but important:
Byte._toInt(), backed by the new btoi IR operation, gives Talk source
arithmetic
over bytes.