talk_dev's Journal

Talk of the Town

We hold these trues to be self-evident.
talk_dev posted a new release

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.

Strings Count What Readers See

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)
}
Bytes Move Behind an Explicit View

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
Leading Dots Need Less Paperwork

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))
Generic Effects Carry Their Own Types

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.

Generic Effects in the Wild
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())
Neovim Gets a Front Door

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 Engine Is Talk Source

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.

permalinkrecent entriesarchive
The Daily Talk - What's New in Talk - Release Notes for July 4, 2026