Why LLMs Can't Write Q/Kdb+: Writing Code Right-to-Left

Jul 9, 2025 - 13:00
 0  0
Why LLMs Can't Write Q/Kdb+: Writing Code Right-to-Left

Why LLMs Can’t Write q/kdb+: Writing code Right-to-Left

7 min read·1 day ago

--

In my previous post I showed how LLMs struggle with coding in q/kdb+; having immediate feedback loops using the q-MCP server does help a bit, but could we do better? Diving deep, a first problem becomes obvious: evaluation order! Claude incorrectly wrote 0.5*y+x%2*y for the Newton’s Method update. The expression would be correct, except for missing some parantheses. But which ones?

  • in Python, the correct expression would be 0.5*y+x/(2*y)
  • in q/kdb+ it’d be (0.5*y)+x%2*y

Why the difference? Because q/kdb+ as well as its ancestors APL, J, and k, feature Right-to-Left with No Operator Precedence (RL-NOP) evaluation. Claude is aware of that, but it struggled to write correct code based on those rules; in fact, it wrote something that’s half-Python, half-q, and correct in neither. In general LLMs struggle with RL-NOP.

However, RL-NOP is a fundamental design choice for APL and its descendants. Turing-award winner and creator of APL, Ken Iverson, wrote:

3. An expression evaluated from right to left is the easiest to read from left to right. For example, the expression

a+x×b+x×c+x×d+x×e+x×f

(for the efficient evaluation of a polynomial) is read as a plus the entire expression following, or as a plus x times the following expression, or as a plus x times b plus the following expression, and so on.

4. In the definition

F/x ≡ x1 F x2 F x3 F … F x⍴x

the right-to-left convention leads to a more useful definition for nonassociative functions F than does the left-to-right convention. For example, -/x denotes the alternating sum of the components of x , whereas in a left-to-right convention it would denote the first component minus the sum of the remaining components. Thus if d is the vector of decimal digits representing the number n , then the value of the expression 0=9|+/d determines the divisibility of n by 9 ; in the right-to-left convention, the similar expression 0=11|-/d determines divisibility by 11 .

Far from me to disagree with a Turing-award winner on which direction is easiest to read! But writing from right to left is simply something LLMs are bad at. And it’s not the same as translation to Arabic or Hebrew; direction here refers to the temporal order in which the tokens are produced; even for right-to-left languages, the order in which the tokens get produced remains unchanged; rather, a thin display layer handles the visual presentation. As a fun fact, this layer is missing when using Claude Code CLI, leading to:

> Translate "Algebra" to Hebrew● הרבגלא

Notice how the translation starts with aleph (hopefully familiar to mathematicians) on the left even though it definitely should be on the right; in fact, let’s look at it through the UI:

Note how the blue selection-block appears to have two different chunks, whereas logically it’s one contiguous chunk. The thin UI layer on top of the LLM output causes it to appear as mixing R-to-L and L-to-R; and makes drag-select lead to non-contiguous blocks. And my argument is that translating between normal languages and RL-NOP languages can similarly be done with a thin “UI” layer, albeit one that involves AST parsing rather than just direction reversal.

There are indeed true right-to-left writing tasks when coding, for example converting nested function calls to sequential form:

h(c(d(c(a(f(c(g(h(f(g(c(x))))))))))))

which Claude translated as:

temp1 = g(x)temp2 = c(temp1)

rather than:

temp1 = c(x)temp2 = g(temp1)

Ultimately here we are asking Claude to write the future before writing the past; and sometimes q coders do that: they write the end of expressions, then use the left cursor key to write what comes before it. Furthermore, one could argue that in this case the LLM doesn’t have to write right-to-left, it simply needs to read right-to-left the original and write the translation left-to-right. But the fact that it’s failing at this task points towards a weak area of LLMs or how they’re trained.

Why This Probably Won’t Be Fixed Soon

Training LLMs to write right-to-left, no-precedence evaluation faces several challenges:

1. No economic incentive: users of RL-NOP languages like q, J, and APL represent a tiny fraction of the market

2. Training data scarcity: There’s simply far less publicly available q code compared to Python/JavaScript

3. Fundamental architecture, and this is speculating: I expect thinking in reverse temporal order to require qualitatively different changes in the model parameters, which might interfere with more of the common tasks the LLMs are good at, and so perhaps best to avoid risking breaking things; granted it could be done by chain-of-thought, as right-to-left reading is a much easier task than writing; yet the example above still shows failure.

The industry won’t rewire multi-billion dollar models for niche languages. So what other solutions are there?

Introducing Qython: A Pragmatic Bridge

Just like with Arabic and Hebrew, perhaps a thin layer on top of the LLM output displaying things backwards is all that is needed! Well, something a bit more complicated, as programming languages need to be parsed into AST trees, then reconverted back into code, but the idea stays the same: take a Python expression like 0.5*y+x/(2*y) and convert it to q expression (0.5*y)+x%2*y.

To extend the idea to all of q’s features and quirks, I propose Qython: a Python-like language that compiles to q. Here’s the key insight: LLMs excel at Python, so let’s give them Python syntax that translates cleanly to q.

Here’s Qython in action:

def factorial(n):    def multiply(x, y):     return x*y return reduce(multiply, 1+range(n))

This translates to:

factorial:{[n]     multiply:{[x;y]         :x*y        };    :multiply/[1+til[n]]    }

The magic? LLMs can write Qython naturally because it follows Python’s left-to-right conventions and syntax. The translator handles the precedence and evaluation order conversion.

Proof of concept

Using the updated q-MCP server with Qython support, and instructing Claude to write in Qython first, here’s an actual transcript (reformatted for readability; codeblocks originally written using \n instead of new-line).

╭───────────────────────────────────────────────────╮│ ✻ Welcome to Claude Code!                         ││                                                   ││   /help for help, /status for your current setup  ││                                                   ││   cwd: /home                                 │╰───────────────────────────────────────────────────╯

> Connect to q port 5001. Write a q function nmsq to compute square rootvia Newton's Method using Qython as an intermediate language. Define it in the session and test it.
● I'll connect to q port 5001 and create the Newton's Method square rootfunction using Qython.
● Update Todos ⎿ ☐ Connect to q port 5001 ☐ Write nmsq function in Qython for Newton's Method square root ☐ Translate Qython to q code ☐ Define function in q session and test it
● qmcp:connect_to_q (MCP)(host: "5001") ⎿ Connected to q server. Timeouts: async_switch=1s, interrupt=10s, connection=2s)
● Update Todos ⎿ ☒ Connect to q port 5001 ☐ Write nmsq function in Qython for Newton's Method square root ☐ Translate Qython to q code ☐ Define function in q session and test it
● qmcp:translate_qython_to_q (MCP)(qython_code: " def nmsq(x): if x <= 0: return 0 guess = x / 2
def improve_guess(current_guess): return (current_guess + x / current_guess) / 2 result = converge(improve_guess, starting_from=guess) return result") ⎿ nmsq:{[x] if[x<=0; :0 … +8 lines (ctrl+r to expand)
● Update Todos ⎿ ☒ Connect to q port 5001 ☒ Write nmsq function in Qython for Newton's Method square root ☒ Translate Qython to q code ☐ Define function in q session and test it
● qmcp:query_q (MCP)(command: " nmsq:{[x] if[x<=0; :0 ]; guess:x%2; improve_guess:{[x;current_guess] :(current_guess+x%current_guess)%2 }[x]; result:improve_guess/[guess]; :result } ") ⎿ None
● qmcp:query_q (MCP)(command: "nmsq 25") ⎿ 5
● qmcp:query_q (MCP)(command: "nmsq each 4 9 16 25 36 49 64 81 100") ⎿ 0 2.0 1 3.0 2 4.0 … +7 lines (ctrl+r to expand)
● Update Todos ⎿ ☒ Connect to q port 5001 ☒ Write nmsq function in Qython for Newton's Method square root ☒ Translate Qython to q code ☒ Define function in q session and test it
● Connected to q port 5001. The nmsq function is now defined and testedsuccessfully. It computes square roots using Newton's Method withfunctional convergence - tested on perfect squares 4-100, returningaccurate results (2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0).

The LLM writes familiar Python-style code, the translator converts it to q, and in this case the result worked perfectly. No more fighting with evaluation order or precedence rules.

Qython is still under construction, and the current version is an MVP with a limited vocabulary, but I hope to keep adding features to map out the entire language.

And of course, for Claude to understand q code, we’d need a q→Qython translator as well; this is harder due to the fact that q doesn’t come with a unified, publicly-available syntax document; but also due to its operator overloading based on input type. The solution to that might be an orchestrator which translates the code, then calls an LLM to intelligently deduce parameter types from the code; more on this in my next post.

Second in a series on specialized AI tooling for q/kdb+ development. The updated q-MCP server with Qython support is available at https://pypi.org/project/qmcp/ First article available here.

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0