Early June Update

· 8 min read

Early June Update

It’s been a while since the last update, and a lot of small, sharp changes have piled up — most of them the language quietly settling into a more coherent shape. No single grand feature this time. Just a stretch of “make it say one thing one way,” and a couple of structural moves underneath. Here’s the roundup.

The surface is settling

The biggest visible change: kebab-case is legal in names, and the separators each do exactly one job now.

import std/io
std/io:print.ln("Hello, {{ name:s }}!")

~pub event delete { directory-name: string }
| my-branch b |> ...

Three cleanups landed together there:

  • - is a name-char. my-branch, directory-name. Flows read like prose instead of like C. It lowers to _ for the host target, so the Zig side still sees directory_name — the dash lives entirely in the Koru contract.
  • / is the namespace separator at the call site, not just in imports. You used to import "std/io" with a slash and then call std.io: with a dot — two spellings for one module. Now it’s std/io: everywhere, matching the import and the file on disk (std/io.k).
  • Bare imports. import std/io, not import "std/io". A module path is an identifier-path, not a string; the quotes were borrowed ceremony.

Stacked up, every separator finally means one thing: / namespace, : module→symbol pivot, . member access, - word-glue, _ digit separator in numbers. One glyph, one job.

That sits on top of a few other surface changes from the same stretch:

  • The => construct glyph. Construction is now marked explicitly — => builds a value (a branch constructor) everywhere, = only opens a flow, and a bare name { ... } is always an invocation. The parser stops guessing from layout; the glyph tells it. Multi-line branch constructors come along for free.
  • .k files are pure Koru. A .k is the event contract and nothing else — no ~, no host lines. The ~ leader only makes sense where you switch into Koru from a host language, and in a pure-Koru file there’s nothing to switch from. .kz (Zig procs) and .kjs (JS procs) keep it.
  • Dollar-imports are gone. $std/... retired in favor of plain std/.... One import form.
  • Inline |> is enforced. A trailing |> with the body on the next line is now a hard parse error. The chain operator never starts a line.

None of these is huge on its own. Together they’re the language pressing its own shape — removing second ways to say things, which is the whole posture here: greenfield, no users, so a duplicate form is pure debt.

Underneath: const blocks and typed bodies

Two structural moves landed that aren’t surface sugar.

const {} blocks now lower as a per-target template over the field parser. A module-level

const {
    name: "Claude"
    count: 42
}

declares constants the rest of the file can interpolate directly — no host-side companion needed — and it lowers per target (Zig, JS) rather than being special-cased.

Values can carry a type, riding the same [ ] annotation slot the rest of the language uses:

const {
    count: 42[i32]
    size: 7[u8]
    plain: 100
}

42[i32] is the value 42, typed i32; plain stays inferred. It’s an early seed of pulling more of the type surface into Koru itself.

And ProcDecl.body is now a typed Source, not a raw string. Proc bodies used to be opaque text forwarded to the host compiler. Making the body a typed Source is the groundwork for treating host code as a first-class payload instead of a blob — the same “source blocks are opaque payloads to userland” idea, now with a real type behind it.

The metalanguage layer

The templating and expression machinery got real plumbing:

  • A liquid-style tree-walker plus a JSON parser landed, and with it the range path (0..3) finally lowers correctly through the template layer.
  • Capture dissolved. Instead of a bespoke ~capture construct, an accumulator is just an effect-cell (! as acc) written to by ordinary events, with a struct-literal walker doing the rewrite. One fewer special form; capture becomes an application of primitives that already exist.

Cross-target and the harness

  • JS keeps pace. Cross-target std/args and multi-branch terminal dispatch on the JS target — the same flow runs on Zig and JavaScript, dispatching the same branches.
  • The regression harness learned ARGS argv passthrough, so a test can feed argv to both runtimes and compare a single expected output.

The suite got more honest

A quieter thread, but the one we care about most: several tests that were masked — passing for the wrong reason, or pinning behavior the language had outgrown — got flipped to honest positives. The inline-|> enforcement drove a suite-wide layout migration. Dead crud got dropped. A failing test you understand is worth more than a green one you don’t.

Where it stands

After all of it, the merged suite is at 606 / 653 in-scope passing — 92.8%, higher than before the busy stretch started, with the kebab foundation and the const-lowering work both landed and stepping on nothing.

The things still red are red on purpose — they’re the to-do list staked into the suite: the snake→kebab corpus migration, rejecting _ in names (the forcing function that makes kebab canonical), retiring println (a non-interpolating print.ln twin), and stage-aware stdlib facets so a comptime transform can’t be called from the pipeline that is the compiler.

A lot of small coherences. The shape’s getting cleaner.