I recently built a programming language called AntiLang. The main idea was to keep all the logical parts of the language the same, while reversing its structure—making it logically correct but structurally inverted.
I built this language as a fun project. I always wanted to make a language, and I was bored with my day-to-day work, so I found it to be an escape while learning something new.
This means that a simple FizzBuzz program would be readable, but might give you a headache and make you wonder why you’re reading it.
,20 = count let
,0 = i let
{count > i} while [
{i % 3 == 0 && i % 5 == 0} if [
,{$FizzBuzz$}print
] {i % 3 == 0} if else [
,{$Fizz$}print
] {i % 5 == 0} if else [
,{$Buzz$}print
] else [
,{i}print
]
,1 += i
]
You can try the language using the online interpreter. (Hope it gives you a headache 🙂)
The first draft
- invert all brackets -> exchange opening n closing brackets
- all control structures are defined at the end of the block
- for )i++; i<n;0=i(
- reverse assignment
- reverse var rules (const is mutable and non-const is immutable)
- array starts from 0 to -n+1
- arg first name second
- reverse header, define, typedef
Kartik Soneji and I were on an OTC CatchUp call until 3 or 4 AM, discussing the creation of a language with such a weird syntax—which resulted in this initial draft. (I’m glad I didn’t implement it all.)
The initial idea was to create a transpiler that converts this language into a C program and then passes it to GCC. We settled on the name reverse-c. Want to steal the initial draft? Go ahead!
How I Learned About Building Languages
All programming languages are essentially structured text written in files, serving as input for software such as compilers or interpreters.
These compilers and interpreters give your language its identity—you provide code, and it compiles or interprets it accordingly.
Thus, you essentially need to write either a compiler or an interpreter.
I wrote an interpreter while reading Write an Interpreter in Go and assumed compilers would be considerably more challenging to build. The same author later published another book called Write a Compiler in Go (the name says it all).
I would recommend you give it a read if you are really interested. Now let me tell you how I built AntiLang (or the AntiLang interpreter).
How the AntiLang Interpreter Works
At a high level, it follows three steps:
- Lexer – converts text into tokens.
- Parser – transforms tokens into an Abstract Syntax Tree (AST).
- Evaluator – evaluates the AST (essentially running your program).
For example, consider this simple loop:
,0 = i let
{i < 10} while [
,1 += i
]
The Lexer
The lexer’s job is to convert this code into tokens. A token is “your code minus all the unnecessary things,” i.e., all the literals, keywords, operators, and identifiers. For us, all the whitespace and newline characters are unnecessary text, but they are also tokens for languages like Python, which depends on indentation.
Each special character and operator is validated, while the rest are keywords or identifiers. The lexer processes the input character by character, returning a token for each character or group of characters.
For our first line, the tokens would be, in sequence: ,
, 0
, =
, i
, and let
, where ,
and =
are special characters, 0
is a literal, i
is an identifier, and let
is a keyword.
At this stage, we don’t care how the tokens are placed. let i = 0,
is also valid, as we are only concerned about all the tokens being legal.
You can have a look at the code of the lexer at #AntiLang/src/lexer/.
The Parser
At this stage, we are not concerned with how tokens are placed, as we are going to build our AST.
AST or Abstract Syntax Tree is a tree used to represent code in a tree format.
For each feature, we need to decide on a structure.
In AntiLang, all the let statements start with a ,
, end with a let
, between =
and let
we can have one and only one identifier, and between ,
and =
we can have an expression.
,<expression> = <identifier> let
An expression can be a literal or something that requires a bit of computation to generate a value, and an identifier is a variable.
,2 + 2 - 4 = zero let
The above example is a valid let statement with 2 + 2 - 4
as an expression and zero
as an identifier. At this stage, we won’t compute the value of the expression; we will simply create the AST and pass it to the evaluator, which will do all this.
The AST of the above example will look like…
We set zero as an identifier which will hold the value of the expression, which would be evaluated later.
Now that you have understood let statements, I will show how while is parsed.
{<expression>} while [ <other statements> ]
For while, we only care about the expression (condition) between {
and }
and the statements between [
and ]
. We can write some code to get an AST like this:
The AST for this program would look like…
,0 = i let
{i < 10} while [
,1 += i
]
It looks complex, right? Sure it is. I cannot tell you how many test cases I have written to cover those weird edge cases, and I must say I might have missed a few.
Now that you have the AST for your program, you can pass it to the evaluator. But before that, I need to tell you a few things.
How your language would follow the BODMAS rule
We understand from childhood that the value of 2 * 2 - 2
is 2 and not 0. Because we look at it like (2 * 2) - 2
, which means we need to multiply first, then subtract, and not the other way around—thanks to BODMAS. I hope you all know what BODMAS is, but for those who don’t: It defines the order of operations.
But it’s very difficult for computer code to understand, but Vaughan Pratt described how someone can do it. His algorithm is called Pratt Parsing or Top Down Operator Precedence.
Explaining that algorithm could be another blog, but Bob Nystrom did a great job in Pratt Parsers: Expression Parsing Made Easy explaining how it’s done. I would urge you to implement it yourself.
You don’t need to write your own parser
Sorry, I forgot to tell you that you don’t need to write a parser by hand; you can use tools like ANTLR which can do it for you. I didn’t use it as I wanted to learn how things work and I didn’t want to learn how to use another tool.
But writing this parser was the most fun part, as I had to find a lot of hacky ways and fight against stupid suggestions from GitHub Copilot. Whenever something clicked, I got so, so happy.
The Evaluator
Finally, we pass the AST to the evaluator to run the program.
The evaluator processes each child node of the Program root node, executing your program. It also holds information about all the built-in functions and stores the values of your variables and function definitions.
In AntiLang, we leverage Go for built-in functions (for example, we pass data directly to fmt.Println
to print it), and Go handles garbage collection, so we don’t have to.
For a let statement, we simply evaluate the expression on the left-hand side and store the resulting literal in the environment.
case *ast.LetStatement:
val := Eval(node.Value, env)
if isError(val) {
return val
}
return env.Set(node.Name.Value, val)
Example:
,2 * 2 - 2 = two let
The value of two
will be stored as 2
, not as 2 * 2 - 2
, because we recursively call the Eval
function on the expression before setting it.
For a while loop, we repeatedly evaluate the condition and run the loop body until the condition becomes false.
case *ast.WhileExpression:
for {
condition := Eval(node.Condition, env)
if isError(condition) {
return condition
}
if !isTruthy(condition) {
break
}
rt := Eval(node.Body, env)
if rt.Type() == object.RETURN_VALUE_OBJ || rt.Type() == object.ERROR_OBJ {
return rt
}
}
Finally, to handle increments and reassignments, we first check whether the variable (identifier) exists in the environment. If it does, we perform the necessary operation and update the environment with the new value.
func evalAssignExpression(name, operator string, value object.Object, env *object.Environment) object.Object {
current, ok := env.Get(name)
if !ok {
return newError("identifier not found: %s", name)
}
switch operator {
case "=":
env.Set(name, value)
case "+=":
env.Set(name, evalInfixExpression("+", current, value))
// ...other cases...
}
}
For example, consider:
,0 = i let
{i < 10} while [
,1 += i
]
It will first execute the LetStatement branch and set i
as 0
.
ENV: i = 0
Then it will execute the WhileExpression branch and check the condition, which is true
as 0 < 10, and then it will execute the body, which adds 1 to i
. Our evalAssignExpression
method handles this case and updates the environment.
ENV: i = 1
This continues until i
is set to 9
, at which point the condition is no longer true
. Then it will return control back to the program node, which checks for the next statement to execute.
As there is none, it will exit the program.
What are my learnings?
I’ve learned a ton from this project. It’s my first major project built with Go, which is also my first programming language.
Most importantly, while developing the parser, I learned that if something isn’t working, it’s best not to dwell on the issue. Instead, taking a break, getting some fresh air, and returning to the problem with a clear mind is much more effective.
I also learned how to compile the interpreter to WebAssembly (WASM). A key challenge was figuring out how to access the standard out within the browser environment, so that printed results from AntiLang could be displayed. That was a learning curve in itself.
Writing a grammar for syntax highlighting a new language using the Monaco editor was another major challenge. I had no idea if something like this existed, even though it’s something that powers VSCode and I use it every day. It was fascinating to see how syntax highlighting actually works under the hood. These two learnings are substantial enough for their own blog posts, which I plan to write in the coming weeks and will link here.
This project has given me a deep appreciation for the complexity of language development. Even this simple, unoptimized language took a month to build. I have immense respect for those who work on compilers and interpreters.
Conclusion
So, there you have it – a peek into the weird and wonderful world of AntiLang. Building it was a rollercoaster, from those late-night brainstorming sessions to wrestling with the parser (and occasionally, GitHub Copilot’s “helpful” suggestions). It’s wild to think how much goes into even a simple language like this. Honestly, if you’re even remotely curious about compilers or interpreters, dive in. You’ll learn a ton, and maybe even get a few headaches along the way. And hey, if you ever decide to build your own crazy language, drop me a line. I’d love to see what you come up with.