Skip to main content

hello

rusil.
blog

Coding in Nepali

How I built a tree-walking interpreter in Go where you write rakha instead of let, yadi instead of if, and the errors come back in Nepali

· 6 min read

Overview

Every programming language I have used speaks English. let, if, function, return. That is fine, but I kept wondering what it would feel like to write code in the words I actually think in. So I built Lipi (लिपि, "script"), a small programming language with Nepali keywords.

Plaintext
rakha naam = "Ram";
bhana("Namaste, " + naam);

kaam jod(a, b) {
  farkau a + b;
}

yadi (jod(2, 3) > 4) {
  bhana("thulo!");
} natra {
  bhana("sano");
}
Plaintext
$ lipi namaste.lip
Namaste, Ram
thulo!

It is a tree-walking interpreter written in Go. I built the core by working through Thorsten Ball's Writing an Interpreter in Go, then added the Nepali parts on top: the keywords, error messages in Nepali, line numbers in errors, installers, and a website with a playground that runs in the browser.

You can try it at lipilang.tech or read the code on GitHub.

The Language

LipiMeaningExample
rakhaletrakha umer = 21;
kaamfunctionkaam jod(a, b) { ... }
farkaureturnfarkau a + b;
yadi / natraif / elseyadi (umer > 17) { ... } natra { ... }
sahi / galattrue / falserakha thik = sahi;
bhanaprintbhana("Namaste", naam);
lambailength of a stringlambai("Pokhara")

Values are integers, strings, booleans and functions. Functions are first-class, so closures and recursion both work:

Plaintext
kaam gunak(x) {
  farkau kaam(y) { farkau x * y; };
}

rakha tebar = gunak(3);
bhana(tebar(7));        // 21

How It Works

Source code goes through three stages, same as in the book.

  1. Lexer turns text into tokens and records the line of each token
  2. Parser is a Pratt parser that builds a syntax tree
  3. Evaluator walks the tree and runs it

Keywords are just a map

The nicest surprise was how small the "make it Nepali" part is. The lexer reads a word, then checks a map to see if it is a keyword. If it is not, it is a normal name like a variable.

Go
var keywords = map[string]TokenType{
	"kaam":   FUNCTION,
	"rakha":  LET,
	"sahi":   TRUE,
	"galat":  FALSE,
	"yadi":   IF,
	"natra":  ELSE,
	"farkau": RETURN,
}

func LookupIdent(ident string) TokenType {
	if tok, ok := keywords[ident]; ok {
		return tok
	}
	return IDENT
}

So changing a keyword is literally a one-line edit. Everything after the lexer only sees LET or IF, it does not care what word you typed.

Closures come from environments

Variables live in an environment, which is just a map with a pointer to the scope around it. When you look a name up, it checks the current scope first and then walks outward.

Go
type Environment struct {
	store map[string]Object
	outer *Environment
}

func (e *Environment) Get(name string) (Object, bool) {
	obj, ok := e.store[name]
	if !ok && e.outer != nil {
		return e.outer.Get(name)
	}
	return obj, ok
}

When a function is created it keeps a reference to the environment it was made in. That is the whole trick behind closures. tebar above still remembers x = 3 because it holds on to that outer scope.

Errors in Nepali

This is the part I am most happy with. Every error starts with Galti: ("mistake"), tells you the line, and explains what went wrong in Nepali:

Plaintext
Galti: line 3 ma semicolon birsiyo
Galti: line 7 ma 'umer' bhanne naam bhetiyena
Galti: line 2 ma sankhya ra string jodna mildaina

Those read as "you forgot a semicolon on line 3", "no name called 'umer' was found on line 7" and "a number and a string can't be added on line 2".

The book's interpreter does not track lines at all, so I had to add a line number to every token in the lexer and carry it through the parser and evaluator. It was a lot of small changes, but now errors point you to the exact line.

Counting Nepali characters properly

One bug I did not expect: strings can contain Devanagari, and in Go len() counts bytes, not characters. A Devanagari character takes 3 bytes in UTF-8, so lambai("राम") was giving the wrong answer. The fix was counting runes instead:

Go
"lambai": {
	Name: "lambai",
	Fn: func(line int, args ...object.Object) object.Object {
		if len(args) != 1 {
			return newError(line, "'lambai' lai 1 ota input chahinchha, %d ota aayo", len(args))
		}
		s, ok := args[0].(*object.String)
		if !ok {
			return newError(line, "lambai le %s napna sakdaina", args[0].Type())
		}
		return &object.Integer{Value: int64(utf8.RuneCountInString(s.Value))}
	},
},

The Playground

I wanted people to try Lipi without installing anything. Go can compile to WebAssembly, so I compiled the same interpreter to a .wasm file and loaded it in the website. The playground on lipilang.tech runs exactly the same code as the CLI, there is no separate JavaScript version that could behave differently.

The website itself is built with Astro, Tailwind and shadcn/ui, and the editor uses CodeMirror.

Shipping It

A language nobody can install is not very useful, so I spent real time on this part:

  • macOS and Linux: a one-line curl | sh install script that downloads the right binary, verifies its checksum and adds it to your PATH
  • Windows: a setup wizard built with Inno Setup that installs for your user only, no admin prompt
  • Go users: go install github.com/rusilkoirala/lipi@latest

Pushing a version tag runs a GitHub Actions workflow that builds binaries for all three platforms, builds the Windows installer, tests it on a real Windows runner (install, run, uninstall, check PATH), publishes the release, and then tests the install script on Linux and macOS too.

What's Missing

Lipi is still small. There are no loops or arrays yet. Next on the list:

  • a REPL
  • loops (jabasamma, "as long as")
  • arrays
  • <= and >=
  • natra yadi for else-if

What I Learned

Before this project, a programming language felt like magic. Now I know it is a lexer, a parser and a tree walker, and each one is a few hundred lines of Go. The book does an amazing job of teaching that.

The parts that were all mine, the Nepali errors, line tracking, the WebAssembly playground and the installers, taught me something different: building the thing is maybe half the work. Making it easy for someone else to install and use is the other half.

If you read Nepali, give it a try and tell me which keyword feels wrong :)