Blog
April 18, 2026
The Road to Lykn 0.5.0 โ Notes from the Workshop
A conversational retrospective on the design and discovery work behind the v0.5.0 release.
When 0.4.1 shipped on April 14th, the compiler was in a good place --
the surface language had equality operators that made sense, bind
finally enforced its own type annotations, and we had set! for
external property mutation. The guides had been ported. The book had
chapter prompts lined up through Chapter 6. By most measures, we'd
just finished a pretty satisfying run.
Three days later, we're releasing 0.5.0.
It turns out that a release focused on documentation -- the JS
guides, the book, the AI-friendly SKILL.md -- surfaced a pile of
questions that weren't really about documentation at all. They were
about shape. What does a Lykn project look like when you're not
just authoring inside a monorepo? How do you ship a macro module?
How do you write tests in Lykn when the entire test suite is still
in JavaScript? What happens when someone writes lykn new my-app
and gets ... nothing, because we hadn't built it yet?
Starting points: three loose threads
Going into this window, we had three unfinished stories pulling at us:
Destructured parameters. DD-25 had actually been designed
before 0.3.0 -- all the way back when we were still arguing about
whether to use obj-pat or reuse object as a pattern form.
(Duncan's "in LFE, we use (cons a b) to get head/tail" settled it
in about six words. The constructor-is-destructor principle from
Erlang/ML became a Lykn principle on the spot.) But the
implementation had lagged, and the book's Chapter 15 still had that
awkward "current limitation" note hanging over it.
Generators. The book's Chapter 21 opened with a reference to
The Holy Grail and closed with a note that said, essentially,
generators are deferred to v0.2.0+ -- Lykn syntax for yield is not
yet defined. Hrm.
Testing and publishing. The testing bootstrap document had been
sitting in the project files since Guide 00 first revealed the
operator bug. We'd designed DD-30 (the testing DSL), DD-31 (the test
runner), and DD-32 (the migration plan from JS tests to Lykn tests).
But we hadn't actually built any of it. And then Duncan asked Claude
Code a perfectly innocuous question about whether lykn publish
would push the testing package to JSR, and he wrote back something
that started with "Here's the picture" -- which, as Duncan pointed out
at the time, is rarely a good sign from CC. The picture was: we had
never thought about publishing a macro module. At all.
The destructuring trilogy
The first substantive work of the cycle was finishing destructured parameters -- which, remember, had been designed almost six months earlier. The DD had three parts:
- DD-25: the basic
(object ...)and(array ...)patterns in:args, with per-field types - DD-25.1: nested destructuring via
alias, anddefaultsub-forms - DD-25.2: top-level
defaultandrestdirectly in:args(the case where you want a defaulted scalar alongside destructured objects --((object :string host) (default :boolean ssl true)))
The three-part structure is a bit unusual. Normally a single DD covers a single feature. But destructuring has this fractal quality where every "what about" question opens up another layer. What about nested patterns? What about defaults inside nested patterns? What about defaults not inside patterns? We cranked away for a few hours, at the end of which came this nice validation from Duncan:
Those all look good -- very similar to LFE, in fact ;-)
Hello, Robert!
Generators ... finally
Chapter 21 of the book had been mocking us for a while. Generators are one of those features that JS developers either never use or use constantly, and Lykn's promise is "thin skin over JS" -- so not supporting them was a papercut every time someone looked at the concept-card list.
The design conversation was surprisingly short. JS has function*
and yield; Lykn needed surface forms that felt like the rest of
the surface language. We'd already settled on the pattern -- func
is the surface cousin of function, fn is the surface cousin of
the arrow. Generators just needed the same treatment: genfunc for
the declaration form, genfn for the expression form.
The one novel bit was the :yields :type clause. Every other type
annotation in Lykn tells you what flows in or out of a function
(:args, :returns, :pre, :post). Generators yield values one
at a time, and we wanted those to be typed too. The syntax reuses
the keyword-clause pattern established by func:
(genfunc range
:args (:number start :number end)
:yields :number
:body
(for (let i start) (< i end) (+= i 1)
(yield i)))
The .lyk extension
Small thing, but it matters. At some point -- I think when we were writing the test runner, but it might have been earlier -- someone (possibly CC, possibly Duncan) [Editor: it was Duncan ;-)] pointed out that we had Lykn source files in two distinct flavors: pure kernel (for low-level things that ship in the compiler itself) and surface (for everything users write). Treating them identically meant the tooling couldn't give different error messages, and it meant the book couldn't tell at a glance which examples needed the surface compiler.
.lyk for kernel, .lykn for surface. Tiny change, no DD
required, and it rippled through the CLI, the test runner, macro
entry resolution, and the publishing pipeline essentially for free.
The great workspace restructuring
This is where the release goes from "add some features" to "actually, let's think about what Lykn is as a project."
The problem was easy to describe and hard to solve: Lykn the
compiler, Lykn the browser bundle, and Lykn the testing framework
were all living together in src/, sharing a single deno.json,
and pretending to be one package. But they were not one package.
They had different publishing stories, different consumers,
different tooling needs. Shoving them all together was going to get
worse, not better, as we added more (a linter package, a formatter
package, probably a language server eventually).
The obvious move was Deno workspaces -- restructure into
packages/lang/, packages/browser/, packages/testing/, with a
root project.json declaring the workspace and each package
getting its own deno.json.
"Obvious" undersells how much had to actually change. Every import
path. Every test fixture. Every CI reference. The book's source
paths. The conversation bootstrap documents. The CLAUDE.md
workflow instructions. The scaffold template (which didn't exist
yet, but whose design had to anticipate the workspace layout).
A unified CLI (mostly)
The other thing DD-28 did was turn lykn into a proper top-level
tool. Before 0.5.0, lykn was essentially a compiler with two or
three subcommands bolted on. After 0.5.0:
lykn new my-app # scaffold a project
lykn run app.lykn # compile + run
lykn test # discover and run tests
lykn lint packages/ # lint compiled JS
lykn build --dist # stage packages for publishing
lykn publish --jsr # publish to JSR
lykn publish --npm # publish to npm
The interesting design call here was deciding that lykn would be
a wrapper around Deno, not a replacement for it. Deno already
runs JS. Deno already runs tests. Deno already publishes to JSR.
We just needed lykn to handle the Lykn-specific steps (compile
.lykn to .js, discover test files by the Lykn conventions,
stage workspace packages) and then hand off to Deno for execution.
This is the same pattern as the testing DSL -- the testing macros
don't implement a test runner, they expand to Deno.test()
calls. Same philosophy at the CLI level: don't reinvent what the
platform gives you. Be thin.
lykn new
Small change, big impact. lykn new <name> scaffolds a fresh workspace
with:
- A
project.jsondeclaring the workspace - A
packages/<name>/with adeno.jsonand a minimal example - A
README.md - A
.gitignore - Everything wired up so
lykn runandlykn testjust work
The hardest part of designing this wasn't the scaffold itself -- it
was deciding what the scaffold implies about the "standard" Lykn
project. Because whatever lykn new emits becomes, by default,
what every Lykn project looks like. If we default to a single-package
layout, we're implicitly saying "most Lykn projects are single
packages." If we default to a workspace layout, we're saying
workspaces are the norm.
The testing trilogy
Then came testing. This was the longest thread of the cycle.
DD-30 was the testing DSL itself -- macros like test,
suite, is, is-equal, ok, is-thrown, matches. The big
design call here was: don't build a test runner, be a macro
module that expands to Deno.test() calls. Deno already has a
test runner. It does parallelism, filtering, watch mode, coverage,
four output formats, and sanitizers. The only thing missing was
Lykn syntax for authoring tests. So that's all DD-30 provides.
The naming was fun to argue about. I had initially proposed
assert-equals and assert-throws -- Deno-compatible naming,
familiar to JS developers. Duncan pushed back on the assert- prefix
as verbose and noisy. We looked at uvu (a minimalist JS test
framework) and liked their short forms, and Duncan mentioned his time spent with Clojure and LFE testing DSLs. is, is-equal, ok --
each one short enough that a test file reads like prose:
(test "arithmetic"
(is-equal (+ 1 1) 2))
DD-31 was the lykn test CLI. Discovery conventions (*_test.lykn,
*.test.lykn, and the .lyk variants), compilation pipeline,
passthrough to Deno. The novel bit is Markdown doc testing -- lykn test can find Lykn code blocks embedded in Markdown files and
check that they compile. This is what unblocks the book's Chapter
30 and the guide system: every code example in the docs can now be
verified automatically. No more silent drift between what the docs
say and what the compiler does.
DD-32 was the migration plan -- five tiers by bootstrapping distance, from "surface tests that treat the compiler as a black box" (easiest, do first) to "expander tests that test the macro system the test macros depend on"
The publishing epiphany
Then came the publishing conversation, which started with Duncan observing:
You know what? For all of our work in the previous conversations, we haven't stopped to think about one very crucial bit ...
And then listing four major design threads -- the book, the testing framework, the JS quality standards, part 1 and part 2 of that -- and noting that none of them had addressed how to ship Lykn code as a consumable package.
CC had given a clear diagnosis: @lykn/testing was a macro module,
its exports pointed to mod.lykn, and neither JSR nor npm would
know what to do with that. The testing package was the canary, but
the same problem applied to any macro module anyone would ever
publish. This wasn't a testing-package problem, it was a Lykn-
publishing problem.
Duncan's predictable response:
we need a solution for this that any project will be able to use. I think we need to double-down on our nascent use of a dist dir and fix this generally
- runtime -- compiled
.jsfiles with rewritten imports (regular Lykn library) - macro-module --
.lyknsources plus a generatedmod.jsstub so npm consumers don't see a file extension they don't understand - tooling -- plain JS, pass through
Every package declares its kind in its deno.json. lykn build --dist produces a dist/ tree where every package has exactly
what it needs for both JSR and npm publishing. lykn publish
then calls deno publish for JSR and iterates the workspace for
npm. Clean pipeline, one command.
DD-34 fell out of DD-33 immediately. Once macro modules could
be published, consumers had to be able to import them. The
current import-macros only understood filesystem paths. It
needed to understand jsr:@lykn/testing and npm:@lykn/testing
and bare names through the workspace import map.
The design call here was architectural: delegate registry
resolution to Deno. We're already running a Deno subprocess for
macro compilation. Adding a "resolve" action to that subprocess
(where the Rust side asks "what's the cached disk path for
jsr:@lykn/testing?" and Deno answers) costs almost nothing.
Every future Deno registry feature -- new registries, new protocols,
whatever -- works for Lykn automatically. The alternative was
implementing JSR/npm resolution in Rust, which would have been
hundreds of lines of code that we'd have to maintain forever,
always playing catch-up with Deno upstream.
I'll admit I floated the catch-up idea briefly. Duncan didn't have to say much -- just "let's not reinvent Deno's resolver" and it was done.
Artwork
Ending on a fun note, the 0.5.0 release of The Lykn Book has got gobs of otherworldly (and this-worldly, tbh) Lichen-inspired artwork: 39 images, 1 for each chapter ... and then some more for dessert at the end :-)