Back to the log
[project]2025.03.158 min readdrawn by Muhammed Musthafa S · Founder & Lead Developer

How I Built a Programming Language from Scratch

A compiler design assignment that turned into a language with six data types, a pixel-art game engine, a WASM runtime, and a keyword system where the vocabulary is a JSON file.

Every compiler design course ends the same way. You write a calculator. It parses 2 + 3 * 4, respects precedence, prints 14, and you get your marks. Then you close the folder and never open it again.

I got as far as the calculator and could not stop. Not because the calculator was interesting — it wasn't — but because for the first time I could see the whole machine. Text goes in one end. Meaning comes out the other. Every language I had ever used was doing this, and I had never once looked inside.

PewPew is what happened when I kept going. It is a programming language with six data types, a twelve-module standard library, an HTTP server framework, a pixel-art game engine, a WebAssembly runtime, and a browser IDE themed after Borland Turbo C++ from 1992. One person, roughly nine months, and a class assignment that got badly out of hand.

What the language actually looks like

The vocabulary is the first thing people notice, and it is the least interesting thing about it.

do fizzbuzz(n) {
  repeat n times {
    check (i % 15 == 0) { yell "fizzbuzz" }
    nah check (i % 3 == 0) { yell "fizz" }
    nah check (i % 5 == 0) { yell "buzz" }
    nah { yell i }
  }
  gimme n
}

do declares a function. gimme returns. yell prints. check / nah is if/else. The six types are whole, dotted, words, vibe, bag, and kit — int, float, string, bool, array, map.

The tagline I settled on was: Python owns data and AI. JavaScript owns the web. C++ owns the metal. PewPew owns the personality. That is a joke, but it is a joke with a design decision underneath it. A language nobody is forced to use has exactly one job: make somebody curious enough to type one line. The vocabulary is the hook. Everything behind it is conventional and boring on purpose.

The pipeline is the textbook pipeline

There is a temptation, when building your first language, to invent. Resist it. The classical pipeline exists because it is correct.

StageToolWhat it produces
LexingFlexToken stream
ParsingBisonParse actions building an AST
EvaluationC++17Tree-walk interpreter
Runtimelibcurl, pthreads, SDL2Standard library surface

Flex and Bison are older than I am and they are still the right answer. You write a .l file describing what a token looks like, a .y file describing what a valid program looks like, and you get a parser. What took me longest was not the tooling. It was accepting that the grammar is the hard part and everything else is bookkeeping.

I rewrote the expression grammar four times. Each rewrite was me discovering another precedence conflict Bison had been politely warning me about in a log I wasn't reading. Bison tells you exactly what is wrong with your language design; it says it in shift/reduce conflicts, which read like insults until you learn to hear them as free code review.

Evaluation is a tree-walk interpreter — no bytecode, no VM, no JIT. It walks the AST and evaluates nodes. This is the slow option and I picked it deliberately. A bytecode VM is another month of work to make a language that was never going to be in a hot loop run 5× faster than a language nobody is benchmarking.

TIP — If you are building your first language

Use Flex and Bison, write a tree-walk interpreter, and skip the VM. You will learn 90% of what there is to learn about language implementation, and you will finish. A half-built bytecode VM teaches you nothing that a finished interpreter doesn't.

The skin system, which is the part I am actually proud of

Here is the problem that produced the only genuinely novel idea in the project.

The Gen-Z vocabulary is fun and it is also a wall. Someone wants to show a PewPew program to a colleague, or use it in a class, or write something they aren't embarrassed by, and yell and nah become a liability. The obvious fix is a second language — a "professional PewPew" — which means a second grammar, a second parser, and two implementations that drift apart within a month.

The actual fix: the keyword table is data, not grammar.

The lexer has one rule for identifiers. When it matches an identifier it does not decide anything on its own. It looks the text up in a keyword map loaded from a JSON file at startup, and if it finds a hit, it emits that token type instead.

skins/professional.json
{
  "print":  "YELL",
  "return": "GIMME",
  "else":   "NAH",
  "func":   "DO"
}

The grammar never changes. The AST never changes. The interpreter never changes. Swap the JSON and the same compiler now speaks a different language. There are four skins shipped — the Gen-Z default, a professional/corporate one, a pirate one, and a meme dialect — and adding a fifth is a file, not a feature.

The reason this works is that keywords were never structural to begin with. if and else are arbitrary noises we agreed to make; the structure is the conditional. Once you separate the noise from the structure, the noise becomes configuration. I have not seen another language do this and I do not know whether that is because it is a good idea or because nobody else needed it.

Getting it into a browser without a server

The install instructions for a hobby language are where hobby languages go to die. Nobody is installing your toolchain to try your syntax. It has to run in a tab.

Emscripten compiles the C++ interpreter to WebAssembly, which is the easy sentence hiding a genuinely hard problem: the game loop blocks.

A PewPew game does what every game does — loop forever, draw, wait 16 milliseconds, repeat. On a desktop that is SDL_Delay. In a browser there is one thread and it belongs to the page. Block it and the tab dies; the user gets a "page unresponsive" dialog and closes it.

Emscripten's Asyncify is the answer. It rewrites the compiled WASM so that a blocking call can unwind the entire C++ call stack, yield control back to the browser's event loop, and then rewind that stack and continue as though nothing happened. The C++ still reads as a straightforward blocking loop. The browser still gets its frames. The cost is a bigger binary and some performance, and it is worth it every time.

Getting pixels out was the other half. The engine renders into a plain RGBA byte buffer. The browser reaches into the WASM heap and takes a view of exactly that region:

const frame = Module.HEAPU8.subarray(ptr, ptr + width * height * 4);
ctx.putImageData(new ImageData(new Uint8ClampedArray(frame), width, height), 0, 0);

No copy, no serialization, no message passing. The canvas reads the same bytes C++ wrote. That is the entire graphics bridge.

The game engine is 476 lines

There is no external engine underneath. The whole thing is a canvas, a handful of drawing primitives, and one collision function.

game->canvas(320, 180)
game->art("ship", [...])
game->stamp("ship", x, y)
check (game->hit(ship, rock)) { yell "dead" }

Pixels, boxes, circles, lines, text, sprite stamping from text art, keyboard and mouse input, AABB collision, RGB colours. That is the surface, and it is enough for Pong, Snake, a lunar lander, a Space Invaders clone, and a falling-star catcher — five complete games shipped as examples.

The same .pew file runs on all three targets. SDL2 on desktop, canvas in the browser through WASM, and headless for tests. One file, three backends, chosen at build time.

A language without a game engine is a calculator with opinions. The engine is what makes anyone type the second line.

What the project actually is now

  • The language: lexer, parser, tree-walk interpreter, 12 stdlib modules across 30 files, 4 skins
  • A web backend framework — you can write an HTTP server in .pew, with routing, cookies, forms, JSON, and pthread-backed concurrency
  • The browser playground: Monaco with custom PewPew highlighting, the WASM runner, a skin picker, live examples
  • PewPew Academy: 8 progressive courses with XP, ranks from Visitor to Graduate, and a diploma
  • A Hall of Fame, a leaderboard, and public profiles with member numbers
  • 2,136 lines of standard library documentation, which is longer than most of the code it documents

What I would tell myself at the start

Write the docs while you write the feature. STDLIB.md ended up at 2,136 lines and about a third of it was archaeology — reading my own code to remember what a function did. The third I wrote same-day took a tenth of the time.

The grammar is the product. Every hour spent on the grammar saved five in the interpreter. Every hour saved in the grammar cost ten later.

Blocking is a lie you can only tell on a desktop. Everything about porting to the browser came back to one assumption baked into a hundred places: that the program owns the thread. It does not.

Finish the boring 20%. The language was "done" in month four. Months five through nine were the playground, the docs, the academy, the deploy — the part that is the difference between a repo and a thing people use.

PewPew is live at pewpew.outshorts.in. Write a line, hit run, watch a language that started as homework put pixels on your screen.

#programming language#C++#WebAssembly#compiler#game engine

Enjoyed this entry?

Project

Outshorts

AI platforms, full-stack SaaS, custom systems, and ready-to-ship solutions — built by a studio that ships fast.

Title block

DRAWN BY
MUSTHAFA
SHEET
OUTSHORTS.IN
REV
2026

© 2026 OUTSHORTS — ALL SHEETS CURRENT