ai newsSo., 18. Okt.
EilmeldungClaude Code 2.1.302 Flips Two claude -p Defaults: Permission Mode and Fork Subagents

What I learned using a 7B local LLM as the front end of a programming language

Pseudocode is how we teach algorithms, yet it has never been executable. You can write "repeat divisor from 2 to 50" on a whiteboard, but not run it. I wanted to change that without inventing yet another syntax, so I built Pseudo.NET : you write pseudocode however you like, and a local language mod

dev.toSonntag 14:50 · 18.10.5 Min. Lesezeit
Bild: dev.to

Pseudocode is how we teach algorithms, yet it has never been executable. You can write "repeat divisor from 2 to 50" on a whiteboard, but not run it. I wanted to change that without inventing yet another syntax, so I built Pseudo.NET: you write pseudocode however you like, and a local language model turns it into a program.

This is not a launch post. It is what I learned about putting a small LLM at the front of a compiler. If you are tempted to try something similar, maybe some of this saves you a few evenings.

free-form pseudocode -> local LLM -> JSON AST -> validate / repair -> deterministic C# backend -> normal build Enter fullscreen mode Exit fullscreen mode The model never writes the final program. Its job ends at a structured tree (a JSON AST with a fixed set of node kinds). Everything after that is ordinary, testable code: a validator, a type inference pass, and a backend that emits C#. That split is the single most important design decision, and everything below follows from it.

Here is a program, written with tabs only, no braces, no end if:

We want to find all the prime numbers up to 50. primes = a new list define whether counter is prime, which returns a boolean isPrime = true if counter is less than 2 then isPrime = false repeat divisor from 2 to 50 if divisor is less than counter then remainder = counter modulo divisor if remainder is equal to 0 then isPrime = false return isPrime repeat counter from 2 to 50 candidate = whether counter is prime if candidate then add counter to primes print the text "Prime numbers up to 50:" for each number in primes print number Enter fullscreen mode Exit fullscreen mode The same program works written in French ("définir est-ce que compteur est premier…"), and in other languages the model understands. The tool does not decide what the syntax is; the user does.

My first AST had flags like "tapped": true for keyboard input ("is the left arrow held or was it just pressed?"). The 7B model quietly omitted them. Not always, but often enough that games behaved randomly: a piece that should move continuously moved once.

What worked was replacing flags by named values: "press": "held" or "press": "just_pressed", and for drawing "style": "filled" or "style": "outline". A string with a name is much harder to forget than a missing true. Field order mattered too: putting style right after kind made the model fill it in far more reliably.

A simplified example of what the model now returns for "while the left key is held":

{ "kind": "KeyPressed", "key": "left", "press": "held" } Enter fullscreen mode Exit fullscreen mode Lesson 2: concrete examples beat abstract rules, but long prompts backfire Rules written in prose ("a negated condition is a unary NOT") were followed inconsistently. A couple of tiny concrete JSON examples in the prompt fixed the same problems more reliably.

The catch: every addition makes the prompt longer, and a longer prompt made the model forget something else. I added a section about drawing and, a day later, noticed negations had regressed ("if not X" quietly became "X!= false" and a game showed a black screen). The only defence I found was a regression net: a script that regenerates a set of sample programs from scratch with the real model and checks the generated C# against expectations. Every prompt change goes through it. If you do this, build that net first, not after the third surprise.

Models make mistakes in the shape of their output (a field named operator instead of op, a kind called Call instead of FunctionCallStmt, an array where an object was expected). Repairing those is fine, and I do it in a small normalisation pass.

What I refuse to do is repair the user's words: no keyword tables for "if / si / wenn" and no rewriting of text to make the model's life easier. The moment I did that, I would have reintroduced a syntax, just a hidden one. So the rule is:

For what remains, there is a validator that checks the tree structurally. If it finds problems, the compiler sends them back to the model for correction, at most twice. If the answer is still invalid, you get a warning, and the invalid answer is never cached.

Even at temperature 0, I do not want to depend on "the model happens to answer the same way tomorrow". Answers are cached under a hash of (model + prompt + pseudocode), so an unchanged program never calls the model again, rebuilds are instant, and the same input always gives the same C#. The six sample programs even ship their cached answer, so a first build works without Ollama installed.

If you edit the generated C# by hand, the tool keeps your edit until the pseudocode changes, then regenerates and saves the old file as a backup. The model is an interactive front end; the build stays boring.

For education, I think it is worth trying: the barrier to running an idea drops, and the generated C# is a real program a student can open afterwards. For anything else, probably not, because a grammar is exactly the thing that makes a language precise and predictable. I am deliberately trading that away, and the validator, the cache and the deterministic backend are how I try to get some of it back.

I would love to hear where it breaks, and whether you have tried a model as a parser rather than a chatbot.

Disclosure: I used an AI assistant to help draft and edit this article and to build parts of the tool. The experiments and conclusions come from my own work on the project.

Quelle: dev.to (dev.to). Original öffnen