l>
Back to blog
-jl

Why l.

Twenty-five years with k and q. I did not want a new language. I wanted to rebuild the engine underneath it.

essay

I have been working with K and Q since 2001, nearly twenty-five years now. I will say the obvious part first: Arthur Whitney is a genius.

K and Q remain extraordinary languages. Their design is small, dense, and beautiful. But despite years of optimization, extension, and correction, the core implementation model is still rooted in Arthur’s original insight from the 1990s.

I did not want to change the language. I am not arrogant enough to think I should invent a new one. What I wanted to do was reimagine the implementation for the machines we actually use today.

Computers in 2026 are not the computers of 1997. CPUs are wider. Memory is relatively slower. Every machine has multiple cores. Many have GPUs, NPUs, AMX units, NEON, AVX, VNNI, and other specialized execution engines. The hardware has changed dramatically. The programming model has not kept up.

l is my attempt to keep the language, K, Q, qSQL, IPC, the whole world of existing code, and rebuild the execution engine underneath it. The implementation is built around four ideas: SIMD, parallelism, fusion, and compression.

SIMD as a first-class primitive

I wanted every operator to take advantage of the hardware automatically. Before clusters, GPUs, or distributed systems, a simpler question: how fast can every primitive be on a single core?

That means making every cycle count. It means treating SIMD not as an optional optimization but as the default path for primitive execution. If the hardware can process many values per instruction, the language should use that automatically, no rewrites, no vector intrinsics, no thinking about it. The primitive should just be fast.

Parallelism by default

Parallelism has always been natural for array languages. APL, K, and Q all express work as large, regular operations over arrays, exactly the structure modern CPUs can exploit.

But I wanted to think about it from first principles. Instead of bolting threads on as an external tool, l treats parallel execution as part of the primitive runtime. Small arrays stay on the fast single-core SIMD path. Larger arrays split across worker threads automatically. The user never decides when to parallelize; the runtime decides, from the size, shape, and cost of the operation.

And as arrays get larger still, the same idea extends to accelerators. On a modern Apple machine, many workloads are too small to justify moving data to a GPU, but some are not. Small values stay on CPU pipelines. Larger values use threads. The largest operations move to a GPU, NPU, or other backend when it pays. You should not need a different version of the language because the hardware changed.

Transparent fusion

When I wrote K and Q in financial services, the language was usually elegant. When it became slow, the cause was rarely the primitive itself. It was intermediate allocation.

You write something beautiful:

q) sum sqrt x

Conceptually simple: square-root each value, then sum. But a traditional implementation may allocate an entire intermediate vector for sqrt x, only to consume it immediately in sum. That allocation is waste.

l makes fusion transparent. The runtime chains primitive operations and eliminates intermediate objects wherever it can. In sum sqrt x, the summing loop consumes the square-rooted values directly. Where safe, it modifies temporaries in place. Where possible, it collapses the whole expression into one pipeline. You still write normal K or Q. The implementation makes the obvious thing fast.

Compression as a runtime concept

The fourth idea, the hardest, and probably the most important, is compression. It should not be a storage feature off to the side. It should be a first-class concept inside the runtime.

An operator should not care whether the object it operates on is compressed. The runtime should know how to operate on compressed data directly. For compression, SIMD, threading, and fusion to compose correctly, they have to be designed together. That is the core implementation challenge.

With few exceptions, most primitives can work directly on compressed data. A huge vector can stay compressed, move fewer bytes through memory, fit better in cache, and still produce the same result.

Memory bandwidth is the enemy. Modern CPUs are incredibly fast. The bottleneck is rarely arithmetic. It is moving data: RAM to cache, cache to registers, core to core, CPU to accelerator.

The simplest way to make software faster is to move less data over a shorter distance. That is why compression is central to l. If a vector can be represented in fewer bytes, l keeps it that way. If the data can stay in cache, it stays in cache. If an operation can execute without decompressing the full vector, it does. The runtime moves the minimum number of bytes required to compute the correct answer.

The guardrail: full K and Q compatibility

The guardrail for all of this was compatibility. I wanted l to be K and Q, not “inspired by K,” not “similar to Q.” I wanted existing commercial k4 and Q code to run without modification. I did not want to leave behind more than two decades of code, puzzles, and a community I grew up with.

That means implementing the language, qSQL, IPC, and the surrounding execution model, and taking real code, not just examples, and asking whether it can actually run inside a larger system. Once you commit to that, the problem becomes much harder, and much more meaningful.

The point of l is not to create a new language and ask people to migrate. The point is to preserve the language and replace the machinery underneath it. Same language. New engine.

K and Q gave us one of the most powerful array programming models ever built. l asks what that model looks like when the implementation is designed for modern hardware from the ground up: SIMD by default, parallel by default, fused by default, and compressed by default.

That is why I built l.