Making a Python interpreter in 1024 bytes

Sep 07, 2026 06:14 AM - 1 week ago 6

Austin Z. Henley

I build devices for people



9/6/2026

A screenshot of the 1024 bytes of golfed C code.

To consciousness human, I constitute codification by hand connected the weekends.

My latest challenge? Make a Python expert successful 512 1024 bytes of bully ole C code. Oh, and nary macro shenanigans aliases room tomfoolery.

def buzz(): for n successful range(101): if n % 15 == 0: print("FizzBuzz") else: if n % 3 == 0: print("Fizz") else: if n % 5 == 0: print("Buzz") else: print(n) buzz()

I astir apt can't fresh all of the Python connection into an expert that is only 1024 bytes of code. So what tin I fresh that will look for illustration Python?

This fizzbuzz programme looks distinctly Python. It has the def, the colons, the indentations, and nary parentheses for if statements. Looks for illustration Python to me! Of course, I'll besides person to adhd immoderate further limitations beyond conscionable a subset of the syntax.

My first effort was bad though.

First try: 512 bytes is not enough!

I've written galore recursive descent parsers, truthful really different tin this be? A subset of Python should beryllium akin to the different languages I've implemented.

I started pinch the astir basal codification I could deliberation of: 1 + 2

Then I made it much complex: x = 1 + 2 * 3

And past I moreover added statements: if x > y: z = 3

Great, I made a calculator... Not what I meant pinch this challenge! I was already complete the limit too. That is erstwhile I zoomed retired and made a database of elements that look Pythony, while besides realizing that my codification play skills were not up to snuff to make it fresh successful 512 bytes.

Maybe I tin do it successful 1024 bytes? First, make it work, and past make it small.

The parser

The existent CPython implementation tokenizes the Python source, parses it into an absurd syntax tree, performs immoderate study and optimizations, emits bytecode, and past interprets the bytecode.

This won't really do immoderate of that.

The authorities is held successful a fistful of world variables. It uses a fixed-length array (999 for now) that will clasp the earthy Python code. The variables and usability names each fresh into a azygous array.

char src[999]; /* Entire programme without astir spaces. */ int vars[256]; /* Symbol table. */ int pos; /* Next characteristic successful src. */ int ch; /* Current characteristic successful src. */ int line_start; /* Where the existent statement starts. */

The expressions are handled for illustration immoderate different recursive descent parser, and they are executed on the way. For example:

int parse_sum(void) { int worth = parse_term(); while (ch == '+' || ch == '-') { if (ch == '+') worth = worth + parse_term(); else worth = worth - parse_term(); } return value; }

Straightforward truthful far.

There is nary correction handling of immoderate kind! It makes a lot of assumptions based connected the correctness of the code. For example, it assumes that the keywords are each typed retired correctly.

if (ch == 'w' || ch == 'i' || ch == 'f') { /* ---- while / if / for ---- */ int keyword = ch; int loop_var = 0; if (keyword == 'f') { /* "for K successful range(N):" */ pos += 2; /* skip "or" */ loop_var = next(); /* the loop adaptable */ pos += 8; /* skip "inrange(" */ vars[loop_var] = 0; } other if (keyword == 'w') pos += 4; /* skip "hile" */ other pos += 1; /* skip "f" of "if" */

It besides assumes the token boundaries are correct and strips retired astir whitespace. It keeps indentation and spaces successful drawstring literals.

It is constricted to adaptable names of a single, lowercase character, which allows america to do awesome array lookups directly:

if (ch > 96) { worth = vars[ch]; next(); }

Control travel magic

The usability for executing blocks of codification continues until the indentation decreases. When that happens, it returns, and it is up to the caller to grip the adjacent line. So, it is utilizing the C program's telephone stack to grip the recursion.

void run_block(int min_indent) { for (;;) { int indent = read_indent(); if (ch == '\n') continue; if (indent < min_indent || ch == 0) { pos = line_start; return; }

But what astir loops?!

Since thing is compiled, loops activity by jumping backwards and reparsing the root each iteration. Both while and for loops support way of the position of the information expression. After the assemblage executes, it jumps backmost to that position and continues parsing.

Functions activity successful the aforesaid way. When parsing the definition, the awesome array remembers the position of the usability successful the root code. Then erstwhile parsing a usability call, the caller location is saved, the parser jumps to the usability body, executes the body, and restores the caller location erstwhile it reaches the end.

It is rather beautiful what we tin do moreover pinch nary intermediate representation! The expert maintains very small authorities too.

Minify!

I haven't code golfed much. Trimming the adaptable names and whitespace is obvious, but really do I prevention the large bytes?

There exists an ancient, forgotten website called Stack Overflow wherever the codification magicians of yesteryear shared their knowledge. I learned a batch of ideas from Tips for golfing successful C.

Since rules only beryllium successful your imagination, I did person to get creative. Some of those tips trust connected "features" circumstantial to GNU C89. This is not tomfoolery! This is accepted fiddle-faddle. Here is what I did to shave disconnected bytes from the readable version:

  • Single-letter adaptable and usability names
  • Assume the compiler will nexus libc
  • Use globals for temp variables
  • Globals are zero initialized
  • C89 allows adaptable declarations to beryllium implicitly int and functions are assumed to return int
  • Use usability parameters arsenic temp variables that are preserved connected the telephone stack
  • ASCII values alternatively of characteristic literals
  • Ternary usability and comma operator
  • Bitwise operations alternatively of logical operations

For example, the parse_sum(void) usability that I showed earlier was golfed down to e(){for(z=t();c-43u<3;)y=44-c,z+=y*t();return z;}. It uses ASCII values to shave a fewer bytes.

After everything, the golfed type is 1024 bytes!

The last readable type is complete 4800 bytes. I primitively had respective much features but I kept cutting to make it fit. The comparison expressions were adjacent connected the chopping block, since that eats up a batch of bytes and truthiness still useful without them: if n%15:.

If each I cared astir was making fizzbuzz work, I deliberation I could get beneath 800 bytes! There are astir apt different golfing tricks too.

A screenshot of a terminal checking the byte magnitude of the golfed code, compiling it, and moving fizzbuzz pinch it.

Here is the golfed root successful each its glory:

char s[999];v[256],p,c,x,y,z,w,u;G(){return c=s[p++];}I(){for(u=p;G()==32;);return p-u;}Y(){c&&c-10&&Y(G());}f(){x=0;if(G()>96)x=v[c],G();for(;c-48u<10;G())x=x*10+c-48;return x;}t(g,h){for(g=f();c==42|c==37;)h=c,g=h-42?g%f():g*f();return g;}e(){for(z=t();c-43u<3;)y=44-c,z+=y*t();return z;}E(a,q){a=e();if(c-60u>2)return a;w=c-61;q=G()==61;p-=!q;x=e();return w?(a-x)*w>-q:a==x;}S(i){for(;I()>i|c==10;)Y();p=u;}Q(){for(G();G()-34;)putchar(c);G();}B(i,q,j,k,a,m,n){for(;;){j=I();if(c==10)continue;if(j<i|!c){p=u;return;}if(c==119|c==105|c==102){k=c;k-102?p+=k/4-25:(p+=2,m=G(),p+=8,v[m]=0);q=p;for(;;){a=k-102?E():v[m]<E();p+=k==102;G();if(!a){S(j);break;}B(j+1);if(k==105)break;k-102||v[m]++;p=q;}I()-j|c-101?p=u:(p+=4,G(),a?S(j):B(j+1));}else if(c==100){p+=2;k=G();Y();v[k]=p;S(j);}else{if(c>96){k=c;while(G()>96);c==40?k-112?(G(),n=p,p=v[k],B(2),p=n,G()):(s[p]-34?printf("%d",E()):Q(),puts(""),G()):(v[k]=E());}Y();}}}main(q,m,h){for(h=m=q=0;~(c=getchar());){c=c-9?c:32;h^=c==34;s[q]=c;q+=c-32?1:!m|h;m=c>32|m&&c-10;}B(0);}

In the end, I was capable to instrumentality these features:

  • Integer variables (single letter) and literals
  • Variable assignment
  • Arithmetic pinch + - * % pinch precedence (unary + - only useful astatine the opening of an expression)
  • Comparisons pinch < > <= >= == (only 1 per expression)
  • Integer truthiness
  • if and else
  • while loops, including else blocks
  • for x successful range(y) loops, including else blocks
  • Function definitions pinch nary arguments
  • Function calls, moreover recursive
  • Indent-based blocks (without scope)
  • print pinch a azygous drawstring literal aliases integer expression
  • Comments

I don't deliberation I will beryllium doing immoderate codification play challenges again successful the adjacent future. The process was rather tedious, going backmost and distant betwixt the gulfing-in-progress type and the original type to effort to understand what I changed conscionable 2 minutes ago. Both versions are connected GitHub.

Now it is your turn. What does your Python successful 1024 bytes look like?

More