Releases
What changed in each version of ktm. Binaries for Linux x86_64, Linux arm64 and macOS are attached to each release on GitHub.
v0.28.3 (2026-10-16)
A program that clears the history before it ends leaves nothing for memsafe to report.
Fixes
term.historyClear()frees the history's list as well as its lines, and the path given toterm.historyFileis kept without allocating, so-Memsafeno longer lists them as still in use at exit. The file keeps its lines, and lines added after the clear still go into it.
v0.28.2 (2026-10-16)
A prompt of several lines for term.readLine.
Fixes
term.readLinewith a prompt that holds a newline ("[bold]app>[/]\n") wrote the prompt again at every key, so the screen filled with prompts and the cursor drifted. The lines before the prompt's last are written once now, and only the last is redrawn with what is typed.
v0.28.1 (2026-10-16)
Two fixes found by a program built on term.readLine.
Fixes
- A parameter's default value is checked against its type, as an argument is. A literal for a
Stringparameter (String path = ".history") compiled and handed the function a String of garbage, which crashed later in whatever used it; it is an error now that says to take au8[]. A default that would only warn as an argument (an integer for a pointer) warns. - An uncaught error that passed through a
deferis traced from where it was thrown. The trace began at the function whose defer ran, and the calls below it were missing: an error deep in a package showed up as a line ofmain. Athrow(e)from a catch is shown at its own line, not at the catch's closing brace.
v0.28.0 (2026-10-16)
Reading what people type, with line editing and a history, as GNU readline does.
term: reading lines
term.readLine(prompt, &line)shows a prompt (markup allowed) and reads a line;falsewhen the input ends (Ctrl-D on an empty line, or the end of a pipe). On a terminal the line can be edited with the keys of bash: Left/Right, Home/End, Ctrl-A/E/B/F, a word at a time with Ctrl-Left/Right and Alt-B/F, Backspace, Delete, Ctrl-W, Ctrl-U, Ctrl-K; Ctrl-L clears the screen and Ctrl-C drops the line. A long line scrolls sideways. From a pipe or a file, lines are read as they come.- The history: Up/Down (Ctrl-P/N) go through the lines typed before, which
readLineadds by itself (not empty lines or a repeat).term.historyFile(path)keeps them between runs;historyAdd,historyAuto,historyMax,historyLen,historyAtandhistoryClearmanage it. term.readPassword(prompt, &line)reads a line without showing it and keeps it out of the history.term.clear()clears the terminal.- The terminal guide builds a small interactive program with them.
v0.27.1 (2026-10-16)
Range for over a map, closable sidebars and chapter descriptions on the docs site, and a steadier test.
Fixes
for (auto e : m)over amap.Mapgives each entry, withe.keyande.value, in the order the keys were added; deleted keys are skipped. It works on a map held by value, through a pointer or as a field, withbreakandcontinue. Before, it was refused with "Range for loop must be on a struct with both a 'size' and 'entries' property". Ranging overm.entriesnow says to range over the map itself.- Docs site: the language chapters and the Quick Guides list their descriptions on the index pages, and every chapter page has a meta description.
- Docs site: every sidebar can be closed: the chapter lists, "On this page" and both panes of the library browser. Each keeps its tab where it was, and the site remembers what you closed.
- Test 534 (
coro.waitChild) no longer fails now and then on a loaded machine: its children end in a fixed order whatever their start-up time.
v0.27.0 (2026-10-16)
A new standard library module for the output of command-line programs, and two Quick Guides.
term: terminal output
term.print(fmt, ...)isprintfwith colors written as markup:"[green]passed[/] %d tests, [bold red]%d failed[/]". Colors, backgrounds ([white on blue]) and styles (bold,dim,italic,underline,reverse,strike);[/]closes the last tag. The values filled in by%sare never read as markup.term.eprintwrites to stderr,term.formatinto aString.term.success,term.info,term.warnandterm.errorwrite one line after a green✓, a blue•, a yellow!or a red✗(warnings and errors to stderr);term.titlea bold line.- Colors are on for a terminal and off for a pipe or a file, with
NO_COLORset orTERM=dumb;FORCE_COLORandterm.setColorsturn them on. Without colors the markup is removed and the text stays. term.table(headers...)andt.row(cells...): cells of any type (text, numbers, bools, enums,Strings) in lined-up columns, numbers on the right, markup that doesn't count in the width.t.align,t.border(boxlines or amarkdowntable) andt.format()for the table as aString.term.progress(label, total): a progress bar on stderr, redrawn in place on a terminal, with the time taken atp.done(). Lines written withtermwhile it runs go above it; in a pipe only the last line is written.
Documentation
- Two new Quick Guides:
Command-line flags with the
flagmodule, and Terminal output withterm.
v0.26.0 (2026-10-16)
SQLite with arguments and structs, go-to-definition into packages, man pages, and Quick Guides.
SQLite: queries with arguments, rows into structs
db.exec(query, args...)runs one statement with its?s filled from the arguments and answers how many rows it changed. An argument can be an integer,bool, enum (stored by name), float, text (u8[],String,u8 *),sql.blob(bytes), a struct or slice (as JSON), or a pointer to one of them; a null pointer is NULL.sql.queryAll<T>(db, query, args...)returns every row as avec.Vec<T>,sql.queryOne<T>the first (sql.ERR_NO_ROWSwhen there is none). Columns fill a struct's fields by name (@key,@skip); a pointer field is null for NULL; any otherTtakes the first column, sosql.queryOne<i64>(db, "INSERT ... RETURNING id", ...)gives the new id.sql.free(reflect.of(x))frees what they allocated.- Failures throw
sql.ERR_SQLwith SQLite's message;db.errorCode()tells a broken constraint apart (sql.CONSTRAINT_UNIQUE,NOTNULL,FOREIGNKEY,CHECK,PRIMARYKEY).db.lastInsertId()is the last row's id. - The
sql.Param,sql.selectand prepared-statement calls work as before. The SQLite guide uses the new calls.
LSP
- Go to definition and hover reach a package's names:
pg.connect,pg.Conn, a sub-package (postgres.driver) and an import under another name. Before, only the standard library's and methods on a package's types did. - Go to definition reaches generic functions:
vec.new<i64>,json.decode<T>,pg.queryAll<T>. - Hover shows a package's names as the code writes them (
pg.Conn *pg.connect(...)), not the compiler's internal path. A package under areplaceopens at its real path.
Documentation
man ktm: the language guide and the standard library as man pages (ktm-<module>(3),ktm-guide-<chapter>(7)), installed on Linux.- Quick Guides: one task each, from an empty directory to a working program: database migrations, logging, SQLite and PostgreSQL. Every code block has a Copy button.
v0.25.0 (2026-10-15)
Two changes to the language that make a program that compiled before fail to: both say what to write instead.
Changes you may notice
adis gone (v0.24.0 renamed itscoped).adin front of a declaration is an error that says to writescoped; anywhere elseadis an ordinary name. The packages moved too: ktm-pg v0.2.1, ktm-jwt v0.2.1 and ktm-migrate v0.1.1 usescoped(ktm getthem; older versions don't build any more).- A program or package can't give
StringorErrormethods of its own (void String.shout()), as it can't for any library type: their methods are the standard library's. Write a function that takes one instead:void shout(String *s). See Methods.
v0.24.0 (2026-10-15)
Threads without C, a clearer name for ad, and an LSP that imports for you.
Changes you may notice
adis spelledscoped:scoped String s = str.from("x");freessat the end of its block, asaddid.adstill works, with a warning at it that says to writescoped(none for the files of a package in the module cache); it will be removed in a later minor. See defer and scoped.thread.Pool's fieldrunis nowrunning(the name is the newrun()method).poolNew,poolEnqueue,poolWait,poolStopandpoolReleasework as before, for one more minor.
The thread module
thread.spawn(&f, &arg)runsf(T *)on a new thread, typed, with no casts;join()waits and throws what the thread threw, with its code, message, file and line. A spawned thread has ktm's runtime: crash reports,try,threadlocal.thread.Mutex(withguard()forscoped),Cond,RwLockandOnce: ready as declared, no setup, nothing to free.thread.go(&f, &arg, &wg)with athread.WaitGroup,thread.each(items, &f)over a slice on every core,thread.Channel<T>(buffered or not) and a typedthread.pool(n)withrun()andwait(); each wait throws the first error its threads threw.- The threads chapter teaches the module, with C's threads last.
LSP
- Completion offers names from modules a file doesn't import yet (the library, the project's own
modules and its
ktm.modpackages) and adds theimportwhen one is picked; quick fixes add a missing import for an unknown name. - A correct file no longer shows errors: files of a package repository (a
ktm.modwithmodule pg;in a directory named otherwise) were read as stray programs, open files kept errors after the file they import was fixed, and an imported file's warnings showed up in every importer. - An error in a
returnno longer also warns that the function may not return a value.
Fixes
- macOS: a caught
throwleft the thread's signal mask garbled, so a thread started or a program run afterwards blocked SIGSEGV, and a crash in it spun forever instead of being reported.
v0.23.2 (2026-10-15)
Fixes, most of them to the compiler. Two make programs that were wrong fail loudly: a printf-style format whose values don't match it no longer compiles, and an integer division by zero on arm64 crashes as it does on x86_64.
- An inlined call whose body declares a local from a parameter (
i64 *p = (i64 *)w;) kept the argument's value: given an array element, a dereference or a field (f(slot[0])), the parameter read garbage. Wrong code since inlining came in; plain locals as arguments were right. - printf-style formats (
printf,str.format,appendFormatand your own functions that take one) check each value's type against its conversion, not only their count:%sof a number,%dof text or a float and%fof an integer are compile errors that say which value is wrong. - An integer division or
%by zero crashes on arm64 with "integer division by zero" and exit status 136, as on x86_64, compiled and in the JIT. It gave 0 and the program went on. - The smallest
i64divided by -1 (and%-1) crashes on every platform with "integer overflow in division"; x86_64 called it a division by zero and arm64 gave the dividend. - A crash inside C code (printf given a bad
%s) names the ktm line that called it and every caller up tomain. x86_64 could name rt.crash or a function that had already returned and losemain; Linux arm64 showed only "in C code". - A module's
extern "c"function is called by the module's name for it (libc.strlen): a program function with the same C name no longer takes those calls. - A method can be defined through a type alias of its struct.
- An error in an
asmfunction is reported on its instruction's line, not the line after it.
v0.23.1 (2026-10-15)
Two fixes to prebuilt packages (v0.22.0), found building ktm-pg
v0.2.0 and ktm-jwt v0.2.0 from GitHub. With v0.22.0 or v0.23.0,
KTM_PREBUILD=0 (or -jit) avoids both.
- A package whose generic code calls its own
staticfunctions or methods linked: they stayed local to its library while the generic is made in the program (pg.queryAll,copyIntoandqueryResultdidn't build). - A library's own use of a generic on its types (
json.decode<Header>insidejwt.header) has its types' descriptions filled in: they were set up only when a program starts, which a library's code never sees, sojwt.headerfailed for every token. The same forktm -libof a module or a tree.
v0.23.0 (2026-10-15)
The cryptography that signed and encrypted tokens need, and new versions of two packages.
crypto: Ed25519 keys (generateKey(KeyKind.ed25519), PEM,ed25519PublicKey; from Monocypher 4.0.2); P-384 and P-521 keys (KeyKind.ec384,ec521,Key.ecPoint); RSA-PSS (Key.signPss,verifyPss); and the building blocks of encrypted tokens: AES-128/192-GCM, AES-CBC, AES key wrap, RSA-OAEP (Key.encryptOaep,decryptOaep) and ECDH (Key.ecdh). See Cryptography.- A function can have 256
tryblocks anddefers (eachadvariable is one) in scope at once, not 32, and going past that says where. - ktm-jwt v0.2.0: EdDSA, ES384/ES512 and PS256/384/512 tokens;
encrypted tokens (JWE:
dir, AES key wrap, RSA-OAEP-256 and ECDH-ES with AES-GCM or AES-CBC-HMAC), signed tokens inside encrypted ones;critheaders. - ktm-pg v0.2.0:
pg.queryResult<T>keeps a big result's text in a few blocks freed at once (about 20% faster thanqueryAll), andc.copyInto<T>(table)copies structs into a table in PostgreSQL's binary format (2.5x faster than text rows).
v0.22.0 (2026-10-15)
Conditions without parentheses, thread-local globals, crash reports that follow packages and inlined calls, JWTs, and packages compiled once.
- The condition of
if,while,do ... while,forandswitchcan be written without parentheses, as in Go:if x > 0 {,for i64 i = 0; i < n; i++ {, plusfor cond {andfor {. With them works as before, and one program can mix both. See Control flow. threadlocalglobals:threadlocal i64 x;is one variable per thread, one instruction to reach in compiled code. The runtime keeps its per-thread state in one, so it no longer calls into pthreads to find it:coro.current()is 2x faster, atryabout 15%. See Variables.ad T x;without a value: it starts empty and is freed at the end of its block (a pointer only if set). An@jsonor@yamlstruct with nofree()is freed with itsfreeDecoded(), soad User u = json.decode<User>(...)works.for (k = 0, i = n; ...): several comma-separated expressions before the first;, not only declarations.- Crash reports and uncaught errors: an uncaught error prints the calls that led to the throw; a package's
function shows as
pg.Pool.execatgithub.com/x/ktm-pg/pool.ktm; a throw in a package names the package's file; a function the compiler inlined from another file shows as(inlined)at its own line. - Packages: a package from the module cache is compiled once into the cache and linked from there by later builds
(Prebuilt packages;
KTM_PREBUILD=0turns it off). Options can come before a command:ktm --install-dir=X mod init. - Struct copies of up to 32 bytes (a
u8[], aString) are a few moves instead of a call tomemcpy: a loop that returns slices runs 2.2x faster. crypto:sha384,hmacSha384; RSA keys (generateRsaKey, PEM,Key.signWith/verifyWithwith SHA-256/384/512); keys from a JWK's parts (ecPublicKey,rsaPublicKey).coro.detach(fd)hands an fd to another thread's loop.- New package: ktm-jwt v0.1.0: HS256/384/512, ES256 and RS256/384/512 tokens, claims checks, JWK Sets from text or a URL.
Changes you may notice: threadlocal is a keyword now, so a variable can't be called that.
v0.21.0 (2026-10-14)
Everything a REST API needs, and a guide that builds one: HTTP programming takes a bookmarks service from ten lines to PostgreSQL, accounts, tests and production.
- Guides: a new section, starting with HTTP programming, twelve chapters on one project, every listing run for real.
ktm testand thetestingmodule:void testXxx(testing.T *t)in*_test.ktmfiles,t.check,t.equal,t.fatal,t.skip,-run,-v,./...,-jit -Memsafe; each directory's tests run in that directory. See Testing.validate:@required,@min,@max,@email,@url,@oneOfon fields, with readable errors;http.decodeValid<T>(r)answers 422 with every bad field.config.fromEnvchecks the same rules and names the variable.net.http.middleware: request IDs, the real client address behind trusted proxies, CORS, rate limits shared by every thread, bearer tokens;mux.useWithfor middleware with settings of its own.net.http:r.remoteAddr()andremotePort();r.set/r.getto hand values from middleware to handlers; cookies (r.cookie,w.setCookie,w.deleteCookie); a group's route for its exact prefix (handle("GET", h)in group"/bookmarks"); a throw in a handler is the same 400, 422 or 500 under test as on the server; background work withcoro.goin a handler is documented and tested.- Generics: methods with type parameters of their own (
U Pair<T>.fold<U>(...)); a library's generic used from inside a package; inference with@callerparameters; overloads by arity with string arguments. - Packages:
ktm getandktm buildfetch only the modules that imported packages need (as Go 1.17). New packages: ktm-migrate v0.1.0 (SQL migrations for PostgreSQL and SQLite) and ktm-pg v0.1.1 (time limits that hold when the server freezes). @keyand@skipgo on any struct (ktm-pg reads them for columns).
v0.20.1 (2026-10-14)
A fix for the release downloads.
- The Linux and macOS zips of v0.19.0 and v0.20.0 couldn't build programs that import
net.http,net.url,encoding.base64orencoding.hex: the link failed with "multiple definition ofnet.http.CONN_OUT_FLUSH". Their prebuilt-module markers were written one directory deep, so nested modules were compiled into every program again, andlogandnet.httplacked their start-up line. The release build now makes the library as./build.shdoes, checks every module's marker, and smoke-tests such a program, compiled and in the JIT. ktm built from source was never affected. - A timing test (
coro.readgives way) failed once on a machine running other test suites; its margin is wider.
v0.20.0 (2026-10-14)
Packages, as Go has them, and the libraries a web service needs: TLS, HTTPS, an HTTP client, cryptography and password hashing, URLs, UUIDs, base64, command-line flags and settings from the environment.
- Packages:
import "github.com/ozzenerol/ktm-pg";, aktm.modwith one major version per package,ktm.sumhashes,ktm get,ktm mod init/tidy/download/verify/why/list, minimal version selection, a module cache,ktm buildandktm runfor a project; warnings from a dependency's code aren't shown. See Packages. The first package is ktm-pg v0.1.0, a PostgreSQL client written in ktm (no libpq). net.tls: TLS clients and servers (mbedTLS 3.6 inside, built intolibktmtls, so programs need nothing installed).net.httpsserves anet.httpmux over TLS and gives the clienthttps://. See TLS.- An HTTP client in
net.http:http.get,http.post,http.postJson,http.request,http.decodeResponse<T>; kept-alive connections per host, redirects, time limits, a size limit, a retry when a kept-alive connection was closed. See HTTP. crypto: secure random bytes, SHA-256/512, BLAKE2b, HMAC, HKDF, PBKDF2, a constant-time compare, AES-256-GCM and ChaCha20-Poly1305, ECDSA P-256 keys and signatures; passwords withcrypto.hashPassword(Argon2id, PHC strings),crypto.bcrypt,crypto.verifyPassword(both kinds) andcrypto.needsRehash, as fast as Go's. See Cryptography.coro.offload(fn, arg)runs heavy work on worker threads while the coroutine waits, so its loop keeps serving; password hashing uses it by itself (a/pingduring logins: 0.3 ms, not 100 ms). Alsocoro.parkandcoro.unpark(from any thread), andcoro.inLoop().net.url(parse, build, query strings, RFC 3986 resolving),uuid(v4 and time-ordered v7),encoding.base64andencoding.hex.flag: command-line flags as Go's, with GNU's forms (--port=80,-p 80,-vq), subcommands and a help text.config: environment variables and.envfiles into a struct, with the new field attributes@env("NAME")and@required.- The language:
any ...argsparameters (each argument areflect.Value); a blank importimport _ = m;for a module's side effects;@jsontakes pointers to scalars (i64 *: the value ornull). - Fixes:
(i32)of au32widened toi64didn't sign-extend; overloads of a generic that differed only in a pointer parameter all called the last one; a store to a global with a long name was cut short on x86_64; an error on a module-qualified name underlined too much; acoro.unparkthat came as a park's time limit ran out was lost;./build.sh --installrewrotelibktm.soin place, so ktm and JIT programs running at the time could crash (it now installs new files and renames them over the old ones).
v0.19.0 (2026-10-14)
HTTP servers: net.http, Go's net/http and ServeMux for ktm, about 3 times as fast.
import net.http;:http.Muxroutes by Go 1.22 patterns ("GET /users/{id}",{rest...},{$}, subtrees; the most specific wins, conflicting patterns are refused, 404 and 405 withAllow). A handler isvoid h(http.Writer *w, http.Request *r):r.param,r.query,r.header,r.body,http.decode<T>(r);w.json,w.text,w.error. What a request hands a handler is freed when it returns. A throw is a 500 (and a log line), bad JSON a 400.http.listen(port, &mux, app)serves on every core until Ctrl-C or SIGTERM and finishes the requests in progress.- Middleware (
mux.use), groups with a prefix (mux.group), static files (mux.files), an access log (http.Options.accessLog), andhttp.testRequest/http.recorder()to test handlers without a socket. See HTTP servers. - Measured against Go 1.27
net/httpon the same 2 cores: 2.7 to 3.75 times the requests per second with 16 or more connections, 2.2 to 2.6 times the requests per CPU-second, lower median and p99 latency everywhere (p99 at 1024 connections: 4.5 to 6 ms against Go's 21 to 25 ms), a quarter of the memory. tryanddefercost less: 5 ns and 9 ns, from 12 and 13.json.encodeTo(&s, v)andjson.quoteTo(&s, text)add JSON to a String you have.- Fixes: a type alias to a function type in a module (
pub type H = fn(...) void;) was "already defined";b.lenof a parameter with a default (u8[] b = "") stopped the compiler; a library's generic that calls astaticfunction of its module didn't link in programs.
v0.18.0 (2026-10-14)
Memory that frees itself where it should, clearer -Memsafe reports, switch on strings, and
a round of stdlib additions.
vec.Vec,list.List,map.Mapandmap.Treefree the Strings they hold:free()andclear()free them, as do a Map or Treedeleteand asetthat replaces one.pop,removeAt,shiftandremovehand the String back to you.map.keys()andvalues()give copies of their own.-Memsafe: a leak is a block nothing points to any more. What globals or live variables still hold at exit is listed apart asstill in use at exit ... not leaks. Every report names the file and line in your code and the public library call it went through (at cli/cli.ktm:16 in cli's globals, via vec.new<String>; under-Memsafelibrary functions are not inlined into your code, so the name is the one you called), and a program that ends withexit()gets its report too.- A warning when a function changes a Vec, List, Map, Tree, Set or String it got by value (
push,removeAt,append...): only its copy changes. Take a pointer. switchon au8[]or String with string cases:case "init":. Case-sensitive; a duplicate case is an error.- Every local starts zeroed, not only Strings (
Config c;no longer holds garbage). Large plain arrays (over 4 KiB, no Strings or slices in them) are left as they are. T[] s = {a, b, c};makes a slice of a hidden array; it compiled with warnings and crashed before.{ptr, len}still sets the slice's own fields.- A function of your own with
u8 *fmt, ...argstakes Strings for%sas printf does. - A String variable or field converts to
u8 *where one is expected (its bytes always end in a NUL):cstr.len(s)and C calls without.cstr(). str.join(parts, sep)from a slice or avec.Vec;vec.Vec.contains(x);str.toInt,str.toUintandstr.toFloatread a wholeu8[]as a number;proc.expandEnv(text)fills in${NAME}and${NAME:default}.- Logging: the messages line up in one column (the level and the file:line part are padded); lines logged after
log.shutdown()keep the level and output they had and say once where they came from;log.infoand the others take Strings andu8[]for%s. - The language server completes every method of a generic instance (
vec.Vec<String>), not only the ones already called, and a module's own globals. - Fixes: a statement that is only a field (
self.port;) stopped the compiler; code inlined from another file was reported at a line of the wrong file; overloads with a generic parameter gave an assembler error.
Changes you may notice
- Freeing a String through a copy and then freeing its container is now a double free:
v.get(i).free(), or freeing a value frommap.next(), beforev.free()orm.free().v.at(i).free()is still safe (it clears the String in place).fs.freeList(&names)is now the same asnames.free(). fs.makeDir,makeDirAll,remove,removeDir,rename,move,copyFileandchangeDirreturn abool, true when it worked (false withos.errno()set), asfs.writeAllandfs.existsalready did. They returned 0 on success, soif (!fs.makeDir(d))failed when it worked. A check written asfs.remove(p) == 0now means it failed: writeif (fs.remove(p)).- Log lines have more spaces: the level and the file:line part are padded.
-Memsafeno longer calls what a global holds at exit a leak.
v0.17.2 (2026-10-13)
One logging fix.
log.Options.toStdoutwith apathwrites each line to the file and to stdout. Before, the file won andtoStdoutwas ignored without a word.
v0.17.1 (2026-10-13)
Program arguments, and a flaky test.
- With
-runor-jit, everything after the source file goes to the program, even words that look like ktm's options:ktm -run app.ktm -vgives the program-v(ktm took it before).--ends ktm's options and is no longer passed on. - The
coro.servetest could end itself with SIGTERM when its signal coroutine ran late; it now installs the handler first, assignal.stopInitasks.
v0.17.0 (2026-10-13)
C's stdio in libc, and logging to stdout.
libc.fprintf(stream, fmt, ...)withlibc.stdin,libc.stdoutandlibc.stderr, andlibc.EXIT_SUCCESS/libc.EXIT_FAILUREforlibc.exit.fprintf(libc.stdout, ...)stays in order withprintf.log.Options.toStdout: with nopath, a Logger or the default logger writes to stdout instead of stderr, in order withprintf._externbinds C data by its symbol too:_extern ___stderrp FILE *stderr;, and in a module it has the module's name.
Changes you may notice
- The format strings of
libc.printf,libc.snprintfandlibc.fprintfare now checked against their arguments, asprintf's always was, so a wrong count is now a compile error. A baresnprintforsprintfdeclared withextern "c"is checked too (its format was looked for in the wrong argument before).
v0.16.0 (2026-10-13)
A default logger, so a program logs from anywhere without passing a Logger around.
log.setup(&o)once inmain, thenlog.info(...),log.debug,log.warnandlog.errorfrom any function in any module. The file, line and function are still the caller's.- Until
log.setupis called they write to stderr at INFO, so a small program needs no setup.log.setLevelchanges the level,log.shutdowncloses the file andlog.logger()is the default Logger itself. log.openandlog.Loggerare unchanged.
v0.15.2 (2026-10-13)
One fix for the language server.
- The language server could send a semantic-tokens reply with stray bytes in it when a file had
nothing to highlight (a file that is only
module cli;, say). Neovim then reportedINVALID_SERVER_JSON: Expected value but found invalid token. The reply is now an empty list. The cause was in the compiler's string buffers, which now always start empty.
v0.15.1 (2026-10-13)
Two fixes for compile errors.
- A range-for with nothing to range over crashed the compiler. An array literal,
for (auto name : ["a", "b"]), now says ktm has no array literals and shows how to declare the array first;for (auto x : )says what is missing. - A type or name from a module the file doesn't import, as in
ad vec.Vec<i64> v = m.values();orvec.Vec<i64> v;withoutimport vec, saysmodule `vec` is not imported: add `import vec;`. Anaddeclaration with an unknown type names that type instead of "Type `ad` has not been declared".
v0.15.0 (2026-10-13)
Logging, and functions that know where they were called from.
- A new
logmodule:lg.debug,info,warnanderrorwrite one short line each, to stderr or a file:2026-10-13T14:02:11.482Z INFO listening on :8080.showFileandshowFuncadd the call'smain.ktm:42and function. A file rotates by size (maxBytes,keep), andthreadSafelets several threads share one Logger without their lines mixing. See Logging. @caller("file"),@caller("line")and@caller("func")before a parameter: every call fills it with where the call is written, and callers never pass it. They work in methods, before...args, next to defaults and throughautofunction pointers. Signature help in the language server skips them. See Where a call comes from.- Known issues lists two bugs found along the way, both planned for v0.15.1: a range-for over
an array literal crashes the compiler, and a confusing error for an
addeclaration whose module isn't imported.
v0.14.0 (2026-10-13)
JSON and YAML without reflection. Mark a struct @json or @yaml and
the compiler writes its encoder and decoder.
- Attributes:
@jsonand@yamlbefore a struct, and@key("e-mail"),@skipand@omitEmptybefore its fields. An unknown or misplaced one is a compile error,reflectshows them, and hover in the language server shows them too. - A
@jsonstruct getstoJson,fromJsonandfreeDecodedmethods;@yamladdstoYamlandfromYaml. A field type that can't be written is a compile error at that field.ktm --expandprints the generated code. json.encode,json.decodeandjson.freeuse those methods by themselves, at any depth, so existing code gets faster by adding@json. On a 1 KB order, decode takes 6.6 us instead of 20.6 us and encode 10.5 us instead of 15.4 us.json.decodereads the text with the newjson.Readerand builds no tree, which also makes it about twice as fast for structs without@json.json.Reader, a streaming reader for JSON text, andjson.WritergainsrawKeyanduint.- A new
yamlmodule:yaml.Readerandyaml.Writerfor block and flow mappings and sequences, plain and quoted scalars, block scalars and comments. Anchors, tags, merge keys and more than one document are reported as not supported.
Changes you may notice
- Decode errors start with the line and column in the text:
1:22: tags[0].weight: expected an integer, got a string. The rest of each message is the same as before. - JSON text that ends too early says
expected a value, got the end of the inputinstead ofnot JSON: .... - With
json.STRICT, an unknown key is reported before a missing one, since the text is read in order. @outside anasmblock starts an attribute, so it is no longer an "unexpected character".
v0.13.3 (2026-10-13)
Fixes for struct values passed by value, and clearer errors.
- Passing
*p(a struct, by value) to a function no longer breaks the other uses ofpin the same function. Calls likef(p)elsewhere in it got a copy of the struct instead of the pointer, even when the*pcall never ran. - Reading a
Stringfield straight off a call result works:mk().name,v.get(0).name,(a = b).name. The compiler used to stop with "Slice AST_CLASS_REF assign: unsupported cls kind AST_FUNCALL", and(a = b).namepassed to a function crashed at run time. - Calling a field as if it were a method (
s.len()wherelenis a field) is a compile error, "`len` is a field, not a method: read it without `()`". The compiler used to crash. Function-pointer fields are still called as before. adon a struct field says thatadgoes on a local variable, and errors inside a struct or union body say "struct" or "union" instead of "class".- Hover in the language server shows a
nulldefault parameter asnull, not0(void *).
v0.13.2 (2026-10-13)
A vec.Vec is a JSON array.
json.decodefills avec.Vec<T>field (or a top-levelvec.Vec<T>) from a JSON array, the same way it fills aT[]slice. It used to throw "expected an object, got an array". The Vec it makes is an ordinary one:pushgrows it andjson.freefrees its items and its block.json.encodewrites avec.Vec<T>as an array of its items. It used to write the Vec's own fields,{"data":5,"len":2,"cap":8}.- A
vec.Vec<u8>is a string both ways, as au8[]is, so what encode writes decodes back. A struct of your own withdata,lenandcapfields is still an object: a Vec is known by its type.
v0.13.1 (2026-10-13)
A clean build and a tidier library reference.
- Building ktm prints no C compiler warnings. One was a real risk: a loop counter that was
live across a
setjmpcould come back stale after an error inside a generic instance. - Building the standard library prints no warnings either. "module rt is imported but not
used" was wrong for files whose only use of
rtis athrowwith a message, and the imports that really were unused are gone. fsno longer has its ownopendirandreaddirdeclarations, which repeatedlibc's and showed up in the library reference asfs.opendir,fs.readdirandfs.libc.*.- The Neovim setup in Tools covers lazy.nvim, which resets the
runtime path: list ktm's vim directory in
performance.rtp.paths.
v0.13.0 (2026-10-13)
Ahead of Go on every load. On Linux the coroutine loop now runs on io_uring, and a
coroutine-per-connection server written the plain way (coro.serve,
coro.read, coro.write) does more work per CPU-second than Go 1.27's
goroutine-per-connection server in every scenario of the echo benchmark, with less memory
(server on 2 cores, client on 6, so the client is never the limit):
| Scenario | ktm | Go | per server CPU-second |
|---|---|---|---|
| 64 B request/response, 64 clients | 368k req/s | 168k req/s | +45% |
| 64 B request/response, 1024 clients | 379k req/s | 133k req/s | +54% |
| 4 KiB request/response, 64 clients | 267k req/s | 157k req/s | +45% |
| 64 KiB streams, 16 connections | 7.5 GB/s | 6.6 GB/s | +12% |
| 64 KiB streams, 64 connections | 7.4 GB/s | 6.5 GB/s | +9% |
Coroutines and networking
- The coroutine loop runs on io_uring on Linux 6.1 and later: reads, writes and accepts go
to the kernel directly, all of a turn in one system call. It falls back to epoll (an older
kernel, a container that forbids io_uring) and then to
poll; macOS uses kqueue.coro.backend()says which one a thread runs, andKTM_CORO_BACKEND=uring|epoll|kqueue|pollpicks one. coro.readkeeps reading a busy stream for up to 4 ms before it lets the loop's other coroutines run, instead of 32 reads: bulk transfers over many connections move about a third more per CPU-second, and another coroutine on the same loop waits at most about 8 ms (Go preempts a busy goroutine every 10 ms).coro.dial(addr, port, ms): connect from a loop coroutine without blocking the loop's thread.net.dialwaits inconnectand stops every coroutine of its loop meanwhile; its docs now say so.
Changes you may notice
- On Linux, programs using the coroutine loop run on io_uring by default. Set
KTM_CORO_BACKEND=epollto keep the previous loop.
v0.12.0 (2026-10-12)
Memory. A coroutine-per-connection server now uses less memory than Go's goroutine-per-connection server at every load of the echo benchmark: with 1024 clients, 10.7 MB against Go's 11.6 MB (ktm v0.11.0 took 72 MB), and it keeps v0.11.0's speed (request/response 3% to 35% ahead of Go, a quarter of Go's p99 latency at 64 clients).
Big stack frames
- A function whose stack frame is bigger than a page used to touch every page of it on the
way in, so that a stack overflow couldn't jump over the guard page. That made every page of a
big local buffer real memory, used or not: a handler with
u8 buf[65536]cost 64 KiB per connection. Now such a function asks the runtime whether the frame fits on its stack, and touches nothing: the buffer costs only the pages it uses. AOT and JIT, x86_64 and arm64.
Coroutines
- Released coroutine stacks kept for reuse give their memory back once they are older than
the four most recently released ones (
madviseon Linux, a fresh mapping on macOS), so a burst of connections that ends doesn't leave its memory behind. Starting and releasing short coroutines costs the same as before.
Changes you may notice
- Running off the end of a stack (deep recursion, a frame too big for a thread's stack) now
prints
stack overflowand ends the program with SIGABRT, instead of a bare SIGSEGV (SIGBUS on macOS). - The last 64 KiB of a thread's own stack count as overflow: a thread can use a little less stack than before before it is stopped.
v0.11.0 (2026-10-12)
Networking that is fast by default. A TCP server written the plain way, a coroutine per
connection with coro.serve, now uses every core and the kernel's event queue, and
beats Go 1.27's goroutine-per-connection server on request/response traffic (an echo
benchmark on one Linux machine, both servers untuned):
| 64-byte request/response | ktm | Go |
|---|---|---|
| 16 clients | 325k req/s | 281k req/s |
| 64 clients | 379k req/s, p99 0.54 ms | 282k req/s, p99 2.63 ms |
| 256 clients | 365k req/s | 294k req/s |
| 1024 clients | 333k req/s | 321k req/s |
With 4 KiB messages ktm serves 32% more requests with a fifth of Go's p99. A single bulk stream runs 22% faster; with 16 and 64 streams at once ktm and Go are about even (within 3% either way, run to run).
Coroutines and networking
coro.serve(port, &handler, arg, threads, bindaddr): a whole TCP server in one call. It runshandler(fd, arg)as a coroutine for each client on one loop per CPU (SO_REUSEPORTon Linux; on macOS the loops share one listener).coro.serveStop()stops it from any thread or coroutine, and the SIGINT/SIGTERM coroutine of the docs works unchanged. Handlers run on several threads at once: shared globals need theatomicmodule or a lock.coro.read(fd, buf, ms)andcoro.write(fd, data, ms): read what is there and write all of it, pausing the coroutine instead of the thread, on sockets and pipes. A read goes straight on while a stream keeps filling the buffer and waits first otherwise, so a connection costs no more system calls than it must.- The coroutine loop waits on epoll (Linux) and kqueue (macOS) instead of
poll: a turn costs the fds that are ready, not every fd that is open. Nothing to change in existing programs; a program that has run out of fds when its loop starts getspollas before.
Changes you may notice
- An fd waited on with
coro.waitstays registered with the kernel between waits, so close it withcoro.close, as the docs already said for connections: a plainclose(2)of a waited-on fd whose number is then reused for an fd that is not fromcoro.acceptcan leave a wait on the new fd unanswered. coro.waitChildcloses its exit fd withcoro.close.
v0.10.1 (2026-10-12)
Editor support, fixed and made rich: the language server understands the whole current
language and gains the features editors expect, and the Vim syntax is rewritten for today's
ktm. Also: directories and file operations in fs.
The language server
- Fixed: only the first module of an import list (
import mem, str, json;) was seen, so hover, definition and completion failed on every stdlib call after it. - Fixed: opening one of a module's files (the standard library's too) showed a false "not part of a module" error and more after it. A module's file is now checked as its module, with the unsaved text of its other files.
- Fixed: enums had no hover (it failed), no definition and no completion; now
Color,Color.greenandgeo.Kind.rectshow their members and values, jump to their declaration, andColor.completes the members. - Fixed: module names (in imports and as prefixes, aliases too) and fields at their declaration had no hover or definition; a module's name jumps to its files.
- Fixed: completion gave keywords the "operator" kind and offered internal names
(
_K3geo4area,Vec<i64>). - New: hover shows a declaration's doc comment, the standard library's included.
- New: signature help in calls (the parameter you are on, every overload), the document
outline, workspace symbol search, semantic highlighting (modules, types, enum members,
functions and methods, fields, parameters, locals, constants, macros, the stdlib's marked),
folding ranges, inlay hints with the type of each
autovariable, and completion of the modules animportcan name.
Vim and Neovim
- The syntax file is rewritten for the current language: lowercase types,
fn,moduleandimportlists,pub,defer,ad,type, enums, generics,String,true/false/null, digit separators, ktm's escapes and format conversions. It comes with filetype detection (Neovim took.ktmfor another language) and an ftplugin, and installs into<prefix>/share/ktm/vim(see Tools).
The standard library
fs.openDir(path)andd.next()read a directory: each entry's name, kind (a symlink is a link) and joined path, without.and..; close it withdefer d.close();orad.fs.list(path)gives the sorted names as avec.Vec<String>(fs.freeListfrees them), andfs.walk(root, &visit, arg)visits every entry at any depth without following symlinks.fs.join,fs.exists,fs.makeDirAll,fs.copyFile(with the permission bits),fs.rename, andfs.move, which copies and removes across filesystems. See the library guide.- libc's constants passed its C-only check in a normal build but not in the REPL; fixed.
v0.10.0 (2026-10-12)
The standard library's containers, rebuilt on generics. vec.Vec<T>,
map.Map<K, V>, map.Tree<K, V>, set.Set<T> and
list.List<T> hold any type with no void * and no casts, are values
(made with mod.new<T>(), freed with free(), so ad works),
and replace IntVec, PtrVec, FloatVec, IntMap,
StrMap, RBTree, IntSet and the void * list, which
are gone. New: module sort and json.decode<T>.
import json, list, map, set, vec;
struct Point { i64 x; i64 y; };
i64 byX(Point *a, Point *b) { return a.x - b.x; }
void main()
{
ad vec.Vec pts = vec.new();
Point a = {3, 1}, b = {1, 2};
pts.push(a);
pts.push(b);
pts.sort(&byX);
for (auto p : pts) { printf("(%d,%d) ", p.x, p.y); }
printf("\n");
ad map.Map counts = map.new();
u8[] words[] = {"to", "be", "or", "not", "to", "be"};
for (auto w : words) { counts.set(w, counts.getOr(w, 0) + 1); }
i64 it = 0, n;
u8[] word;
while (counts.next(&it, &word, &n)) { printf("%s=%d ", word, n); }
printf("\n");
ad set.Set seen = set.new();
printf("%d %d\n", seen.add(4), seen.add(4));
ad list.List jobs = list.new();
auto build = jobs.append("build");
jobs.append("test");
jobs.remove(build);
for (auto j = jobs.head; j; j = j.next) { printf("%s\n", j.value); }
Point p = json.decode("{\"x\": 5, \"y\": 6}");
printf("%d %d\n", p.x, p.y);
}
(1,2) (3,1) to=2 be=2 or=1 not=1 1 0 test 5 6
The standard library
vec.Vec<T>:vec.new<T>(cap),push,pop,get,at(a pointer to an item),set,last,insert,removeAt,reserve,items()(aT[]view),sort(cmp),sortAsc(),clear,free, and thelenfield. A rangeforwalks it directly:for (auto x : v).map.Map<K, V>, a hash map that keeps insertion order:map.new<K, V>(cap),set,get(k, &out),getOr(k, def),ref(k)(a pointer to the value),has,delete,next(&it, &k, &v)to walk it,keys()andvalues()as avec.Vec,clear,free. Keys of any integer type, pointers,u8[]andStringwork as they are (the map copies and ownsu8[]andStringkeys); other key types are compared byte by byte, or by your ownmap.hashOfandmap.keyEqoverloads.map.Tree<K, V>, the ordered map (a red-black tree):map.treeNew<K, V>(), the sameset/get/getOr/has/delete, and an in-order walk withfirst()/after(n)(orlast()/before(n)). Keys are ordered bymap.compareoverloads.set.Set<T>, a hash set:addanddelete(both say whether they changed it),has,next,items(), andunionWith,diff,intersection,symDiff(new sets),isSubset,equals.list.List<T>, a doubly linked list:append,prepend,insertBeforeandinsertAfterreturn the node, soremove(node)takes it out in O(1);shift,pop,first,last,mergeAppend,mergePrepend; walk it withfor (auto n = l.head; n; n = n.next).- New module
sort:sort.slice(s, cmp)sorts any slice in place with a comparator,sort.asc(s)sorts numbers or pointers smallest first. json.decode<T>(text)returns the JSON as aT(a struct, a slice, a pointer to a struct, ...). What it filled in is freed again if it throws. Thejson.decode(text, reflect.of(x))form stays.- The new
Vec<i64>ran about 14% faster thanIntVec, andMap<i64, i64>about 10% faster thanIntMap, in our measurements.
The language
- A generic struct's method is compiled the first time it is called, so a method that only
works for some types (
sortAscneeds<) does not stopVec<Point>from being used. - Generic instances work across modules (
list.List<fzf.Term *>, a module's generic used with another module's type), andf<T>(...)can be called when plain overloads offexist; a plain overload still wins over the generic. - A range
forwalks any struct withlenanddatafields (avec.Vec), held in a local, a global, a field or through a pointer.
Changes you may notice
- The old containers are removed. They map to the new ones as follows:
Before Now vec.intNew(),vec.ptrNew(),vec.floatNew()vec.new<i64>(),vec.new<User *>(),vec.new<f64>()(a value, not a pointer)v.size,v.entries,v.capacity()v.len,v.data,v.capv.release(),v.release(&freeItem)v.free(); free the items yourself first:for (auto u : v) { freeItem(u); }v.sort()on anIntVecv.sortAsc()v.sort(&less),less(a, b)true when a goes firstv.sort(&cmp),cmp(T *a, T *b)negative, 0 or positive as C'sqsortmap.intNew(),map.strNew()map.new<i64, V>(),map.new<u8[], V>()m.get(k)(avoid *, null if missing),m.get(k, &ok)m.getOr(k, def),m.get(k, &out)(true if found),m.ref(k)m.setFreeValue(&f),m.release()m.free(); values that own memory are freed by you first (walk withnext)m.iter,m.keyIter,m.valueIteri64 it = 0; while (m.next(&it, &k, &v))(either pointer may be null), orkeys()/values()map.rbNew(...)(RBTree)map.treeNew<K, V>();iterNew/rbIterNextbecomefirst()/after(n)set.intNew()(IntSet),s.size(),s.eq(o),s.toVec()set.new<i64>(),s.len,s.equals(&o),s.items()(insertion order:sortAsc()it for the old sorted order)list.new(),l.count(),l.isEmpty(), the circular walk froml.nextlist.new<T>(),l.len,!l.len,for (auto n = l.head; n; n = n.next) - An index out of range (
get,set,removeAt, ...) throwsERR_BOUNDS, which atrycan catch; the old vectors printed a message and exited. - The containers no longer free their values:
free()releases the container (and theu8[]/Stringkeys it copied), and what the values point to is yours, as with any other struct.
Fixes
- A range
forover a call that returns a slice (for (auto w : str.split(s, " "))) ran the call again for the length and for every element, leaking what each call allocated. It runs once. reflect.ofinside a generic of a library module (asjson.decode<T>does) described an empty type unless the program also usedreflect.ofon that type.
v0.9.0 (2026-10-12)
Generics and type aliases. A struct, its methods and a function can take type parameters,
and each use with other types is compiled for those types: the same code you would write by
hand, with no void * and no casts.
import mem;
struct Stack { T *items; i64 len; i64 cap; };
void Stack.push(T x)
{
if (self.len == self.cap) {
self.cap = self.cap ? self.cap * 2 : 4;
self.items = mem.realloc(self.items, sizeof(T) * self.cap);
}
self.items[self.len++] = x;
}
T Stack.pop() { return self.items[--self.len]; }
void Stack.free() { mem.free(self.items); }
T max(T a, T b) { return a > b ? a : b; }
type Ints = Stack;
void main()
{
ad Ints nums = {null, 0, 0};
nums.push(1);
nums.push(2);
ad Stack words = {null, 0, 0};
words.push("hello");
printf("%d %s %d %.1f\n", nums.pop(), words.pop(), max(3, 7), max(1.5, 0.5));
}
The language
- Generics:
struct Stack<T>, its methodsT Stack<T>.pop(), and functionsT max<T>(T a, T b), with any number of parameters. A type is written with its arguments (Stack<i64>,Stack<Stack<i64>>); a call works them out from the arguments (through pointers, slices,fntypes and generic structs) or takes them written (max<f64>(1, 2.5)). Generic bases, self-referencing structs,ad, modules (coll.Vec<i64>,pubas for any declaration) and-liblibraries work with them. An error in an instance names it and the use that asked for it. The language server shows instances and goes to the generic's definition. A genericVec<i64>ran faster thanvec.IntVecin our measurements. See Generics. - Type aliases:
type Name = T;gives any type a second name (slices, pointers,fntypes, fixed arrays, structs, generic instances, other aliases). An alias is its type, so values pass between them with no conversion; in a modulepub typeis used asmod.Nameand aliases may come in any order. The language server shows, completes and jumps to them.
Fixes
puborprivateon an enum, struct or union applied to the next declaration of a module as well: a private function after apub enumcould be called from outside, and apubdeclaration after aprivate enumwas an error.
v0.8.0 (2026-10-12)
Autodefer, one-instruction bit operations, pipelines and process groups in proc,
and UDP with binary addresses in net.
import math, str;
void main()
{
ad String text = str.from("one two three");
ad u8[][] words = str.split(text, " "); // freed at the end of main
u32 h = 0x811C9DC5;
for (auto w : words) { h = math.rotl((u32)(h ^ w.len), 5); }
printf("%d words, hash %x\n", words.len, h);
}
The language
ad(autodefer):ad T name = value;freesnameat the end of its scope, as ifdefer name.free();followed, on every way out. It takes a String, any slice, or a struct with afree()method (or a pointer to one). Returning anadvariable is an error, and so is a type with nofree().adis not reserved: a variable can still be namedad. See ad (autodefer).
The standard library
math.rotl,math.rotr,math.bswap,math.clzandmath.ctzon au32oru64(bswapalso on au16) compile to one instruction where they are called, on x86_64 and arm64, AOT and JIT;math.popcountcounts the one bits.hash's SHA-256 uses them and runs about 17% faster.proccan build pipelines:proc.FDgives a program an fd of yours as its stdin, stdout or stderr (OptsinFd/outFd/errFd),Opts.pgidjoins an existing process group,Opts.dupsmaps more fds in order,proc.pipemakes the connecting pipes andproc.disownfrees aProcyou reap yourself;proc.killreaches the group a process joined.net.Addrkeeps an IPv4 or IPv6 address in binary:net.recvFromAddrandnet.sendToAddrlet a UDP server answer each datagram without turning the sender into text and back;net.makeAddrbuilds one from a numeric address, andAddr.ip(),port(),family(),equals()andsameIp()read it.net.localAddr,net.remoteAddrandnet.setBlockingare public.
Fixes
- macOS: a string literal with a NUL byte inside (
"\x00\x08\x00\x00") lost bytes in a compiled program: the linker splits the section literals went in at each NUL and merges the pieces. Such literals now go in a section it leaves alone. - Docs: the sockets section showed a bare
signal(SIGPIPE, SIG_IGN), which doesn't compile; it issignal.handle(libc.SIGPIPE, libc.SIG_IGN)orlibc.signal(...), now with a runnable example (it readserrnoright after the failed write, which macOS needs).
v0.7.0 (2026-10-12)
Code generation: the compiler inlines small functions, and a new module atomic
shares counters and flags between threads without a mutex. Locals are now always at their
type's alignment, which atomics and C code need.
import atomic, libc;
i64 gHits = 0;
i64 next(i64 i) { return i + 1; } // small: compiled into its callers
void *worker(void *unused)
{
for (i64 i = 0; i < 100000; i = next(i)) { atomic.add(&gHits, 1); }
return null;
}
void main()
{
libc.pthread_t *th[4];
for (i64 i = 0; i < 4; i++) { libc.pthread_create(&th[i], null, &worker, null); }
for (i64 i = 0; i < 4; i++) { libc.pthread_join(th[i]); }
printf("%d hits\n", atomic.load(&gHits));
}
The compiler
- Inlining: a call to a short function of the program whose parameters and result are numbers or pointers is compiled as the function's body in the caller, which saves the call. It behaves exactly like the call (arguments convert to the parameter types, a result to the return type, recursion is never inlined), and the function is still compiled on its own. Loops calling small helpers ran 1.2 to 1.8 times as fast in our measurements.
- Every local is at its type's natural alignment in the frame: an
i64,f64or pointer declared after a narrower local was 4-aligned, which atomic instructions fault on (arm64) or split (x86_64), and C code may not expect.
The standard library
- New module
atomic:load,store,add,sub,swap,and,or,xorandcasoni64,i32,u64andu32(the pointer type picks the width), pointer versions (loadPtr,storePtr,swapPtr,casPtr) andfence(), all sequentially consistent. See Atomics.
Changes you may notice
atomicis no longer a keyword: it was accepted and ignored, soatomic i64 n;made an ordinary variable. It is now an error that points to the module.
Fixes
- An unknown name followed by another word (
foo bar;) is reported at the name; as a block's first statement it used to point at the block's{.
v0.6.0 (2026-10-12)
Function overloading, and the library pieces the example projects still wrote by hand: hashing, case-insensitive and URL/HTML text helpers, portable file metadata, and waiting for a child process inside the coroutine loop.
import hash, str;
void send(i64 n) { printf("number %d\n", n); }
void send(u8[] text) { printf("text %.*s\n", (i32)text.len, text.ptr); }
void main()
{
send(42);
send("hello");
u8 digest[32];
hash.sha256("abc", digest);
String hex = hash.hex(digest);
defer hex.free();
String q = str.urlEncode("a b&c");
defer q.free();
printf("%.8s %s %d\n", hex.cstr(), q.cstr(), str.equalsFold("GET", "get"));
}
The language
- Overloading: functions, methods and a module's functions can share a name when their
parameter types differ, and a call picks the one its arguments fit best (an exact type over
a conversion in the same family over an integer for a float; on a tie, the one that needs no
default and no
T ...nameextras). An ambiguous call, or one no overload takes, is an error that lists the overloads.&ftakes the overload of the function type it goes to.pubis per overload. The language server shows and jumps to the overload a call picks. See Overloading. - A function with no overloads keeps its symbol, so existing libraries and C callers see no change; once a name has two or more, each one's symbol carries its parameter types.
The standard library
- New module
hash: SHA-256 (streaminghash.Sha256and one-shothash.sha256) withhash.hex, the IEEE CRC-32 used by zip, gzip and PNG (chainable), 64-bit FNV-1a for tables, andhash.equalCt, a constant-time compare for digests and tokens (the length is not secret). strgets ASCII case-insensitiveequalsFold,compareFold,findFold,containsFold,startsWithFoldandendsWithFold; URL percent-encoding (urlEncodefor a component,urlEncodePathkeeping/,formEncodewith+for spaces) and decoding (urlDecode,formDecode, false on a bad%XX); andhtmlEscape.fsgetslstatandfstatnext tostat,FileStat.kind()(fs.FileKindfile, dir, link, other) withisFile,isDirandisLink, so code never needslibc.cStat's per-platform layout; plusfs.realPath,fs.preadandfs.pwrite.coro.waitChild(pid, ms)waits for a child process inside the coroutine loop without blocking the other coroutines (pidfd on Linux, kqueue on macOS, polling where neither exists), returning its wait status or -1 on timeout.proc.exitFd(pid)gives the pollable fd for your own poll loop, andproc.setStatus(p, st)records a status got that way in aproc.Proc.
Changes you may notice
- A second function of a name with other parameter types used to be the error "conflicting types"; it is now an overload. The same parameters with another return type is still that error.
v0.5.1 (2026-10-12)
Two fixes, and a clean-out of the old bug queue (every other entry in it was already fixed, or no longer applies to the frozen syntax).
Fixes
- The old C declarator of a function that returns a function pointer,
i64 (*getHandler())(i64) { ... }, still compiled; it is now the same error as the variable form, naming the way to write it:fn(i64) i64 getHandler(). - A
catchblock takes its frame's address again after the throw lands, asdeferalready did, instead of relying on the value staying in place across the try body. It never went wrong in practice (the code layout kept it alive), but nothing guaranteed it.
v0.5.0 (2026-10-12)
A systems library: what the example servers and tools used to write by hand. A coroutine loop for servers, a byte buffer, monotonic clocks and HTTP dates, the OS calls shells and daemons need, and small helpers. Nothing that compiled before changes meaning.
import bytes, coro, date, libc, net;
void *client(void *arg)
{// reads 4-byte big-endian lengths and their messages, answers with the time
i32 fd = (i32)(i64)arg;
bytes.Buffer in = bytes.bufferNew();
defer in.free();
while (coro.waitIdle(fd, libc.POLLIN, 5000) > 0 && in.readFrom(fd) > 0) {
while (in.len >= 4 && in.len >= 4 + bytes.be32(in.data())) {
in.consume(4 + bytes.be32(in.data()));
String now = date.httpDate(date.unixNow());
net.send(fd, now);
now.free();
}
}
coro.close(fd);
return null;
}
void *acceptor(void *arg)
{
i32 lfd = (i32)(i64)arg, fd;
while ((fd = coro.accept(lfd)) != -1) { coro.go(&client, (void *)(i64)fd); }
return null;
}
void main()
{
coro.go(&acceptor, (void *)(i64)net.listen(9000, "*", 64));
coro.run();
}
The standard library
- A coroutine loop in
coro:coro.gostarts a coroutine in the thread's loop andcoro.runruns them all over onepollper turn. Inside themcoro.wait(fd, events, ms),coro.waitIdle,coro.sleepandcoro.acceptpause the coroutine instead of the thread.coro.stop()winds the loop down gracefully, and an accept that runs out of fds neither spins nor drops clients: it rests untilcoro.closefrees one, and for a while after it only accepts when two fds are free (on macOS a failed accept resets the client it took off the queue). Misuse throwscoro.ERR_LOOP. - Module
bytes: abytes.Bufferthat grows at the end and is consumed from the front (append,consume,room/commit,readFrom/writeTo,appendBe32and the other byte orders), andbytes.be16/32/64,le16/32/64andputBe*/putLe*on anyu8[], bounds-checked. date.monoMs()anddate.monoNs(), a clock that only goes forward, for timeouts;date.sleepMs,date.unixNow;date.formatUnix(t, fmt)(strftime, UTC or local);date.httpDateanddate.parseHttpDate(RFC 9110, all three forms).- The environment and the process:
proc.getenv,hasEnv,setenv,unsetenv;proc.selfPath(),proc.cpuCount();proc.exited,exitCode,signaled,termSignal,stopped,stopSignalandproc.statusCode(as$?) for wait statuses. libcdeclaresdup,dup2,getpid,getppid,getpgrp,setpgid,setsid,tcgetpgrp,tcsetpgrp,pread,pwrite,fsync,ftruncate,readlink,symlink,rename,access,realpath,sysconf,setenv,getrlimit/setrlimitand moreerrnocodes (EEXIST,ENOTEMPTY,ENOSPC,ELOOP, ...), each with its platform's value.os.eprintf(printf to stderr, format-checked) andos.flush;math.min,math.max,math.clamp;removeAt(i)onPtrVec,IntVecandFloatVec;json.Writer.cstr(u8 *)for C strings (a null pointer isnull).
Fixes
- arm64: a struct of 32 KB or more passed or returned by value failed to assemble (AOT) or used wrong addresses (JIT). Structs from 256 bytes up are now copied by a loop, which also keeps the code small.
v0.4.0 (2026-10-12)
Reflection. A program can ask the compiler about its types at run time, and the
json module uses that to turn any struct into JSON and back without hand-written
code. Plus a JSON writer, enum names at run time, and typed comparators and callbacks.
import json, mem, reflect;
enum Role { guest, admin };
struct User { i64 id; String name; Role role; u8[][] tags; };
void main()
{
User u;
mem.set(&u, 0, sizeof(User));
json.decode("{\"id\": 7, \"name\": \"Ada\", \"role\": \"admin\", \"tags\": [\"math\"]}",
reflect.of(u));
defer json.free(reflect.of(u));
u.id++;
String text = json.encode(reflect.of(u));
defer text.free();
printf("%s\n", text); // {"id":8,"name":"Ada","role":"admin","tags":["math"]}
}
The language
reflect.typeOf(T)(a type or any expression) gives areflect.Type: kind, name, size, signedness, element and length, a struct's fields (name, type, offset, private or not; inherited ones first) and an enum's members.reflect.of(x)gives areflect.Value, x's address and type, so one function can take any value;reflect.field,getInt/setIntandgetFloat/setFloatread and write through it. A description is made at compile time, only for the types a program asks about; compare two withreflect.same.- Enum names at run time:
reflect.name(Role.admin)is"admin",reflect.enumValue(t, "admin", &v)goes the other way.printfof an enum still prints its number. - A function whose parameters are typed pointers now converts to a function pointer whose
matching parameters are
void *, so comparators and callbacks are written with their real types and no casts (i64 byAge(User *a, User *b)goes straight toPtrVec.sort); the return type and every other parameter must still match.
The standard library
json.encode(reflect.of(x), indent)writes any value as JSON;json.decode(text, reflect.of(x), flags)fills it in andjson.freefrees what decode allocated. Fields by name, private ones skipped, enums by name, Strings, slices, arrays, nested structs and pointers. Unknown keys are ignored and missing fields kept, unlessjson.STRICT. A value that doesn't fit throwsjson.ERR_DECODEwith where it is (items[2].price: expected a number, got a string).json.Writerbuilds JSON into a String:beginObject,key,int/float/bool/str/nil,beginArray,end,raw,take; escapes, commas and an optional indent done for you, and misuse throwsjson.ERR_WRITER.
v0.3.1 (2026-10-12)
The rest of the library on u8[] and String, and the compiler catching
more mistakes it used to let through. Code that uses proc, csv or
sql needs updating, and a few things that compiled before (and did the wrong thing)
are errors now.
Changes you may notice
proc:proc.shelltakes au8[];proc.shellQuote,proc.shellOutputandproc.runreturn aString;proc.communicate(p, input, &out, &err)takes its input as au8[]and gives stdout and stderr as Strings.proc.shellfandproc.shellOutputare format-checked likeprintf, and their%stakes a String or a slice.csv:csv.init(&p, text)takes au8[], andcsv.nextfills acsv.Itemwhosetextis a view of the input (it was acsv.Sliceof offsets).sql:open,prepare,exec,selectandexecRawtake the SQL as au8[];execRawreturns its error as a String (empty when it ran); a textParamand a textColumnareu8[]; theDbkeeps its own copy of its name.- A write through a string literal the compiler can see is an error (the literal itself, or a local initialised from one and never given anything else); other writes fault at run time, in the JIT too, which kept literals writable.
- A struct (a String, a slice, any struct) used as a condition is an error. It was tested by
its address, so
if (err)on a String was always true. - Returning a struct from a function that returns something else, or another struct, is an error. It was a warning, and the caller got garbage.
New
- A
caseis a block of its own: it can declare variables and usedeferwithout braces. - Bounds-check elision:
s[i]infor (i64 i = 0; i < s.len; i++)is not checked when nothing else changesiors.
Fixes
- A
#defineof several strings (#define APP PREFIX "/app") gave au8[]that was too short, and au8 b[] = APP;array of 0 bytes. - An error on a later module of an import list (
import cstr, rt;) printed megabytes of^. - arm64: add and subtract with an immediate past 12 bits were truncated instead of split, and
two JIT encoders did the wrong thing with
sp. f + 0on a function pointer crashed the compiler; it is a type error.- The test runners only run
NN_name.ktmfiles. - LSP: hover and go-to-definition find a module's own names from its other files.
v0.3.0 (2026-10-11)
Safe strings and slices. ktm gets a slice type that knows its length, a native String,
bounds checks that throw, and a standard library that works with both. The old u8 * buffer
functions are gone or moved to a C-string module, so code written for v0.2 needs updating where it
uses them.
import fs, str;
i32 main() {
String text = fs.readAll("notes.txt");
defer text.free();
u8[][] lines = text.split("\n");
defer lines.free();
for (auto line : lines) {
u8[] t = line.trim();
if (t.startsWith("#")) {
continue;
}
printf("%s (%d bytes)\n", t, t.len);
}
String out = str.format("%d lines", lines.len);
defer out.free();
out.append("!");
printf("%s\n", out);
return 0;
}
The language
- Slices
T[]: a pointer and a length (16 bytes, passed in two registers),s.len,s.ptr,s[i](read and write), viewss[a:b],s[a:],s[:b]with no copy, range-for, slices of structs, slice fields, nested slices. An array converts to a slice, a string literal converts to au8[](no copy), andp[0:n]makes a slice from a raw pointer.==on slices is an error: usea.equals(b)or compare.ptr. - Bounds checks on every index and sub-slice, always on: a bad index throws
ERR_BOUNDS("index 5 out of range for length 5", with the file and line ine.fileande.line), which a server can catch.s.ptr[i]is the unchecked way. String: an owned, growable, NUL-terminated UTF-8 string (str.new(),str.from(...),str.format(...)), freed withdefer s.free(). Methods:append,appendFormat,insert,remove,replace,clone,toLower/toUpper,cstr(). A String converts to au8[]view wherever one is expected.- Text methods on any
u8[], literal or String:find,contains,startsWith,endsWith,equals,compare,trim(a view),split(views),decodeRune,runeCount. %stakes a slice or a String directly inprintf,str.formatandthrow.- Named varargs are real slices (
i64 ...numsis ani64[]), and...xspreads any slice. -Memsafeand the REPL see every allocation, the library's own included: a double free through a copied String is reported with all three lines.
The standard library
stris String andu8[]text; the NUL-terminated C-string helpers moved to the new modulecstr(cstr.len,cstr.cmp,cstr.dup,cstr.format, ...), withcstr.view(p)to turn a C string into au8[].fs: paths areu8[],fs.readAll(path)returns a String,fs.writeAll(path, data)takes au8[].json:json.parse(u8[] text)(parse part of a text by passingtext[0:n]),toStr/errorStrreturn Strings, a value'sstrandkeyareu8[]views the document owns, andobjectGetmatches the whole key.net: addresses and names areu8[](empty means any),net.send/sendTotake au8[],net.recvFromreads into au8[]and gives the sender as a String,net.peerAddrandnet.addrToStrreturn Strings.date:format,formatDate,formatTime,timestampNowandisoFromTimespecreturn Strings;date.parseandparsePartstake au8[].map: aStrMapkey is au8[]matched on all its bytes, and the map copies and frees its keys itself (setFreeKey,getLenandhasLenare gone: look up part of a buffer withbuf[0:n]);node.keyandkeyItergive views of the map's copy.toStrandkeysToStrofStrMap,IntMapandset.IntSetreturn Strings.proc,csvandsqlkeep theiru8 *functions in this release; they move tou8[]and String in v0.3.1.
Watch out
- An array becomes a slice of its full length: a
u8 buf[64]passed where au8[]is expected is all 64 bytes, not the text up to its NUL. Passcstr.view(buf)for the C string in it.
Fixes
- The format engine never checked its argument count: a format asking for more arguments than it got now throws instead of reading past them.
- A module can call its own global function pointer by name.
- A method can be called on a call's result (
fs.readAll(p).take()). return f();of a function returning a struct crashed the compiler;c ? a : bof structs works, and mixing a struct with a pointer in a conditional is an error (it compiled to garbage); a field named like an imported module (v.str) is a field.- A vector's index error names the method:
IntVec.get: index 5 out of range (size 3)(it saidIntVecGet). - A slice in an initialiser list takes one item (
Path p = {"name", points};), in locals, globals, structs and arrays.
Known issue
- A write through a slice of a string literal is not caught at compile time yet (planned for
v0.3.1): copy the literal with
str.frombefore changing it.
v0.2.1 (2026-10-11)
A cleanup release: the macOS JIT fork crash, module libraries with run-time initialisers, readable names in every message, and a set of smaller fixes.
Fixes
- On macOS, a child forked by a program run with
-jitno longer crashes now and then right afterfork()(its thread came back with Apple's JIT write protection off). The known issue is gone. - A library that reads or writes another prebuilt library's globals links (they are reached through the GOT).
- Messages name functions, not their symbols: the JIT's unresolved-symbol errors, the linker's errors
and notes, and the REPL say
geo.useHelper, never_K3geo9useHelper. - A module global whose type doesn't exist is reported once, not twice.
#define KTM_NO_REPL_HELLOin~/.ktm_rc.ktmhides the REPL greeting, as documented.- A program's own global named like a C one (
environ,errno) no longer replaces it: program globals get their own symbols, as functions did in v0.2.0. A child started byproc.runfrom a program that had its ownenvirongot an empty environment. - In the JIT, a global declared with
externand never defined is an error instead of a crash at address 0. auto s = "x";no longer warns "u8[2] is not assignable to u8[2]".&of something without an address (&5,&E.a,&f()) is a parse error instead of a code generator failure.- Duplicate
casevalues are reported by value (duplicate case value Color.red), including a value inside an earlier range; integer duplicates were not reported at all. argc/argvoutsidemain's parameters are an error (they were silently 0 and null).json.parserejects invalid UTF-8 in strings (stray continuation bytes, cut-off and overlong sequences, surrogates, values past U+10FFFF) with the newjson.INVALID_UTF8and the position of the bad byte.
Changes you may notice
extern "c" T x;declares C data (extern "c" u8 **environ;); a plainextern T x;is a ktm global defined elsewhere.- A
forinit can declare several variables (for (i64 i = 0, j = 10; i < j; i++, j--)), and its step can be several expressions. - A module built with
ktm -libcan have globals with run-time initialisers: each module gets an init function that runs once, its imports first, before the program's own initialisers. - All REPL builtins are camelCase:
replListFn,replFindFn,replReloadRc,replListGVars,replClassRep,replEval,replHeap(andreplDisasm/replDelfrom v0.2.0).
v0.2.0 (2026-10-11)
A new language. ktm now has its own C-like syntax, modules, methods and a standard library organised into modules. The old HolyC syntax is gone: every program, test, doc page and example in the repository was converted once with an internal tool, which was then deleted. Code written for v0.1.0 does not compile with v0.2.0.
enum Kind { circle, rect };
struct Shape { Kind kind; f64 w, h; };
f64 Shape.area() {
switch (self.kind) {
case Kind.circle: return 3.14159 * self.w * self.w;
case Kind.rect: return self.w * self.h;
}
throw(1, "unknown kind %d", self.kind);
return 0;
}
i32 main(i32 argc, u8 **argv) {
defer printf("bye\n");
Shape s = {Kind.rect, 2, 3};
printf("%.1f\n", s.area());
return 0;
}
The language
- C types and literals:
void,i8tou64,f32,f64,bool,true/false/null; digit separators (1_000_000); a character literal holds one character. struct, C prefix casts(T)x,printf,i32 main(...), calls always with parentheses, file scope holds declarations only.- Typed constants:
const i64 MAX = 4;(compile-time constants when the value is). - Scoped enums:
enum Color { red, green };used asColor.red, with an optional integer type (enum Status : u16 { ... }); converting an integer to an enum needs a cast. - Function types:
fn(i64) i64,fn(u8 *) u8 *,(fn(i64) i64) *for a pointer to one. A function pointer only takes a function with the same signature. - Named varargs:
i64 sum(i64 ...nums)withnums.len, indexing, range-for and spreading (sum(...nums)). defer: block-scoped, runs on every exit (return, break, continue, goto, exceptions).- Errors:
throw(code)orthrow(code, "format", ...), caught ascatch (Error e)withe.code,e.message,e.fileande.line;throw(e)rethrows.tryanddeferno longer make a system call each (7 to 12 times faster). - switch cases end at the next label;
fallthrough;continues into the next one. - Braces are required on
if/else/for/while/dobodies. - Methods:
f64 Shape.area() { ... self ... }, calleds.area(); derived structs inherit methods, a same-named method hides the base one, no virtual dispatch..works on a value or a pointer (p.xisp->x). - Modules: a directory is a module (
module net;in every file),import net, h = net.http;,pubexports, names private by default,privatestruct fields hidden outside their module, files of a module see each other in any order.ktm -libbuilds a module or a tree of modules into a library that importers link automatically. A local may shadow a module name.
The standard library
- Organised into modules:
str,mem,fs,os,proc,signal,net,json,csv,sql,date,coro,thread,map,set,vec,list,math,fzf, pluslibcfor the C and POSIX functions under their C names. Names are camelCase:StrLen(s)is nowcstr.len(s),StrMapSet(m, k, v)ism.set(k, v). - A prelude every file sees without an import:
Error, the integer limits (I8_MINtoU64_MAX),EXIT_OK/EXIT_FAIL. - Dates are formatted with
date.format(d, "%Y-%m-%d"); printf's%D,%Tand%Fare gone. - The TempleOS leftovers are gone (
Dir, the directory printer, the verbose remove, theFstask block).
Removed
- The HolyC syntax:
U0/I64/F64/Bool,class/public, postfix casts, print statements, calls without parentheses,Main, code at file scope,(...)with implicitargc/argv, C function-pointer declarators, multi-character literals. std.ktmhanddefs.ktmh(the library is imported as modules).-transpile(it could not read the new syntax).- The REPL builtins
UfandReplDel: they arereplDisasm("name")andreplDel("name").
Fixes
- A program's own function named like a C library symbol (
read,environ,free) no longer replaces it: program symbols are now named like module symbols. - Variadic C calls through a module's
extern "c"pass their extra arguments correctly on Apple arm64 (open's mode was garbage). - A struct holding a not-yet-complete struct by value is an error (it compiled with size 0).
- Statements after a
{ }block inside a switch case were dropped. - Global
= nullinitialisers are constant data; a field of the compiler state was read uninitialised; narrow literals in untyped varargs filled only one byte;#undeftook the next line's first word; several test races on macOS and the LSP debounce test.
Known issue
On macOS, a child forked by a program run with -jit can crash right after
fork(), rarely and under load. Compiled programs are fine; see
Known issues for the workaround.
v0.1.0 (2026-10-10)
Renamed from hcc v0.19.3. The language, the standard library and their behaviour are unchanged; only names are new.
Changes you may notice
- The compiler is
ktm(washcc). Sources are.ktm, headers.ktmh;.HCand.HHare rejected with a message, on the command line and in#include. - The library header is
std.ktmh(wastos.HH), the librarylibktm(waslibtos, built with-lib ktm), its sourcessrc/std/. - Environment variables, macros and defines are
KTM_*(wasHCC_*), e.g.KTM_INSTALL_DIR,KTM_TEST_TIMEOUT,__KTM_JIT__; temporary files and the REPL's files are namedktmtoo (~/.ktm_rc.ktm,~/.ktm_repl_history). - The
--terryoption is gone; the history and credits are in the README.