@aayazk/bhojpuri-lang
v1.1.1
Published
A toy programming language with Bhojpuri keywords
Maintainers
Readme
Bhojpuri Lang
A toy programming language with Bhojpuri keywords, inspired by Bhailang. Written in plain JavaScript with zero dependencies. It runs in Node.js and in the browser.
Try it in the playground → New to programming? Press
📘 Seekha there for 12 short lessons in Bhojpuri (or English), each with an example and a small exercise
that checks your answer.
Write code (with colours as you type), run it, and press
Baanta 🔗 to copy a link that opens your code for anyone you send it to. + Naya file adds more files
for le aaw. The Console below the
editor runs one line at a time, like the interactive prompt.
ka ho bhaiya
maan la naam = "duniya";
bol ho "Pranam", naam;
chalat bani bhaiyaBhojpuri me: Bhojpuri Lang ek khilauna programming language ha, jekar keyword Bhojpuri me ba. Browser me playground khol ke turant code likha aur chalawa. 📘 Seekha dabawe pe 12 go chhot-chhot paath milela, har paath ke saath ek abhyas bhi. Apna computer pe chalawe khatir
npx @aayazk/bhojpuri-langlikha. Neeche ke puri jaankari angrezi me ba.
Install
npm install -g @aayazk/bhojpuri-lang # gives you the `bhojpuri` command
bhojpuri program.bhoj # run a program
bhojpuri # open the interactive promptOr run a file without installing anything:
npx @aayazk/bhojpuri-lang program.bhoj| Command | What it does |
| --- | --- |
| bhojpuri program.bhoj | runs the program. poochh reads answers from what you type (or from piped input) |
| bhojpuri | opens the interactive prompt; piped lines run without prompts |
| bhojpuri --help | lists every keyword and built-in |
| bhojpuri --version | prints the version |
The command exits with code 0 when the program finishes, and 1 for a syntax or runtime error (printed
to stderr) or a file that can't be read.
Since 1.0, STABILITY.md says what stays the same between versions: a program that works with 1.x keeps working with every later 1.x.
Quick start (from a clone of this repo)
npm test # run the test suite
node bin/bhojpuri.js examples/fizzbuzz.bhoj
node bin/bhojpuri.js # interactive prompt
npm run playground # browser playground at http://localhost:3000To get a global bhojpuri command while developing:
npm link
bhojpuri examples/hello.bhojInteractive prompt
Run bhojpuri without a file to try things out one line at a time:
$ bhojpuri
Bhojpuri Lang 1.1.0 — "chalat bani bhaiya" likh ke ya Ctrl+D se bahar nikal.
bhojpuri> 2 + 3 * 4
14
bhojpuri> maan la naam = "Ramu"
bhojpuri> bol ho "Pranam,", naam
Pranam, Ramu
bhojpuri> kaam dugna(x) {
... lauta da x * 2;
... }
bhojpuri> dugna(21)
42
bhojpuri> chalat bani bhaiya- No markers needed: there's no
ka ho bhaiya, and the;at the end of the line is optional (as it is before a}everywhere). Two statements on one line still need a;between them. - Values are shown: typing a value shows it, with strings in quotes. Statements, assignments and
khaalishow nothing. - Everything is remembered: variables and functions carry over between lines. You can run
maan la x = ...again to start a variable over. - Multi-line code: while a
{,(or[is still open, the prompt changes to...and keeps reading. - Errors don't end the session. The arrow keys bring back earlier lines, Ctrl+C throws away the current
line, and
chalat bani bhaiyaor Ctrl+D leaves. - A line starting with
{is a block. To see a kosh, wrap it in brackets (({ "a": 1 })) or put it in a variable first. - Piped input: lines piped into
bhojpuriwithout a file run the same way, without the prompts:printf '2 + 3\nsanyog(1, 6)\n' | bhojpuri.
The language
Every program starts with ka ho bhaiya and ends with chalat bani bhaiya.
Statements end with ;, and blocks use { }. The ; can be left out just before a }, so short blocks
like jadi (x > 0) { bol ho "haan" } read cleanly.
| Bhojpuri | Meaning |
| -------------------- | ------------------ |
| ka ho bhaiya | start of program |
| chalat bani bhaiya | end of program |
| maan la | declare a variable (let) |
| bol ho | print |
| jadi | if |
| na ta jadi | else if |
| na ta | else |
| jab le | while |
| har … se … tak | counting for loop |
| har … me | for each item of a list or string, or key of a kosh |
| bas kara | break |
| aage badha | continue |
| kaam | define a function |
| lauta da | return |
| koshish kara … galti pe | try … catch |
| phenk da | throw |
| le aaw | use another file (import) |
| sach / jhooth | true / false |
| khaali | null |
Variables
maan la a = 10;
maan la b = "shabd", c; // several at once; c starts as khaali
a += 5; // also -= *= /= %=Variables are block-scoped. Names can be written in Devanagari too (maan la नाम = "राम";).
Output
bol ho "jawab:", a * 2; // several values are joined with a spaceStrings
Strings use double or single quotes: "Pranam" or 'Pranam'. They end on the same line, and + joins
them. A backslash starts an escape:
| Escape | Gives |
| --- | --- |
| \n \t \r \0 | new line, tab, carriage return, the zero character |
| \" \' \\ | the quote or backslash itself |
| \ + anything else | that character, e.g. \{ in a backtick string |
Text is counted in letters as people see them, so a Devanagari letter and its vowel signs count as one.
This applies to lambai, indexes, ulta, hissa, khoj, tod(text, "") and har … me:
maan la s = "किताब";
bol ho lambai(s), s[0], ulta(s); // 3 कि बताकि
har c "नाम" me { bol ho c; } // ना, then मUnicode calls these letters grapheme clusters. An emoji with a skin tone is also one letter. How some
Devanagari conjuncts (like स्ते in नमस्ते) are grouped follows the Unicode version of the browser or Node.js
running the program. Newer versions keep a conjunct together, so lambai("नमस्ते") is 3.
Text in strings
Write a string with backticks to put values straight into the text. Whatever is inside { } is worked out
and written in, the same way bol ho would show it:
maan la naam = "Ramu", umar = 24;
bol ho `Pranam {naam}, agila saal {umar + 1} ke ho jaiba`; // Pranam Ramu, agila saal 25 ke ho jaiba
bol ho `Saaman: {jod(saaman, ", ")}`;- Any expression works inside
{ }: maths, function calls, indexes, even another string. - Write
\{for a literal brace."..."and'...'strings don't change: braces in them are just text. - Like other strings, a backtick string has to end on the same line.
Conditions
jadi (a > 10) {
bol ho "bada";
} na ta jadi (a > 5) {
bol ho "beech ke";
} na ta {
bol ho "chhota";
}Loops
har counts from one number to another. Both ends are included, just like "1 se 10 tak":
har i = 1 se 10 tak {
bol ho i; // 1, 2, … 10
}
har i = 10 se 0 tak kadam -2 {
bol ho i; // 10, 8, 6, 4, 2, 0 (kadam sets the step; it defaults to 1)
}har … me visits each item of a list, each letter of a string, or each key of a kosh:
har phal ["aam", "kela"] me {
bol ho phal;
}- The loop variable only exists inside the loop.
se,tak,kadamandmeare only special inside aharloop's header. Everywhere else they're ordinary names, so you can still use them as variables.- The start, end and step are worked out once, before the loop starts. Changing the loop variable or the list inside the loop doesn't change which values the loop visits.
- With a fractional step, values are rounded to 15 significant digits, so
har i = 0 se 1 tak kadam 0.1gives0, 0.1, … 1without floating-point leftovers like0.30000000000000004.
jab le repeats while a condition is true:
maan la i = 0;
jab le (i < 10) {
i += 1;
jadi (i == 3) { aage badha; }
jadi (i == 8) { bas kara; }
bol ho i;
}bas kara (break) and aage badha (continue) work in both kinds of loop.
Functions
kaam jodo(a, b) {
lauta da a + b;
}
bol ho jodo(2, 3); // 5lauta da;with no value, or reaching the end of the function, returnskhaali.- Functions can call themselves (recursion). They're values too: you can pass them to other functions or return them.
- Functions remember the variables around them (closures):
kaam counter() {
maan la n = 0;
kaam badhaw() { n += 1; lauta da n; }
lauta da badhaw;
}
maan la ginti = counter();
ginti(); ginti();
bol ho ginti(); // 3Recursion can go about 1,000 calls deep (roughly Python's default). Deeper recursion gives an error instead of crashing.
Functions without names
kaam(x) { ... } without a name makes a function right where a value is expected. That's handy for
passing a small function to another one:
maan la dugna = kaam(x) { lauta da x * 2 };
bol ho dugna(21); // 42
maan la ank = [1, 2, 3, 4, 5, 6];
bol ho badal(ank, kaam(x) { lauta da x * 10 }); // [10, 20, 30, 40, 50, 60]
bol ho chhaan(ank, kaam(x) { lauta da x % 2 == 0 }); // [2, 4, 6]
bol ho badal(["aam", "kela"], bada); // ["AAM", "KELA"]badal ("change") calls the function on every item and gives a new list of the results. chhaan
("sift") gives a new list of the items the function says sach to. Both leave the original list alone, and
any function works: one without a name, one made with kaam naam(...), or a built-in like bada.
Lists
maan la saaman = ["aalu", "pyaaz"];
daal(saaman, "sattu"); // add to the end
bol ho saaman[0]; // aalu
saaman[1] = "tamatar";
bol ho lambai(saaman); // 3
bol ho nikaal(saaman); // sattu (removes the last item)
bol ho saaman; // ["aalu", "tamatar"]Indexes start at 0. Strings can be indexed too ("ghar"[0] is g), but they can't be changed.
Lists are shared by reference, so a function that changes a list changes it for the caller too.
Dictionaries (kosh)
A kosh stores values under keys:
maan la ramu = { "naam": "Ramu", "umar": 24 };
bol ho ramu["naam"]; // Ramu
ramu["gaon"] = "Ballia"; // add a key, or change one that exists
ramu["umar"] += 1;
bol ho lambai(ramu); // 3
bol ho ramu; // {"naam": "Ramu", "umar": 25, "gaon": "Ballia"}
jadi (ba(ramu, "phone")) { // does the key exist?
bol ho ramu["phone"];
}
hataw(ramu, "gaon"); // remove a key; returns its value
har k ramu me { // loop over the keys
bol ho k, "=", ramu[k];
}- Keys can be strings or numbers, and
1and"1"are different keys. - Keys stay in the order they were added.
chaabi(d)gives them as a list. - Reading a key that doesn't exist is an error, so a typo in a key name gets caught. Check first with
ba(d, key).hatawon a missing key just returnskhaali. - Like lists, a kosh is shared by reference, and
==is onlysachfor the very same kosh. - A
{at the start of a statement is still a block. A kosh literal is only read where a value is expected, such as after=, inbol hoor as a function argument.
Built-in functions
| Name | Meaning |
| -------------------- | ---------------------------------------- |
| lambai(x) | length of a list or string, or the number of keys in a kosh |
| daal(list, value) | add value to the end of list |
| nikaal(list) | remove and return the last item (khaali if empty) |
| sankhya(text) | turn text into a number: sankhya("42") is 42 |
| shabd(x) | turn any value into text: shabd(42) is "42" |
| kism(x) | the type of a value: sankhya, shabd, list, kosh, sach/jhooth, khaali or kaam |
| gol(n) | round to the nearest whole number: gol(2.6) is 3. Halves round up: gol(2.5) is 3, gol(-2.5) is -2 |
| gol(n, dashamlav) | round to that many decimal places (0 to 15): gol(2.345, 2) is 2.35, and gol(1.005, 2) is 1.01 |
| neeche(n) | round down: neeche(7 / 2) is 3 |
| upar(n) | round up: upar(7 / 2) is 4 |
| bina_chinh(n) | the number without its sign (absolute value): bina_chinh(-5) is 5 |
| sanyog(a, b) | random whole number from a to b, both included |
| bada(text) | text in UPPER case |
| chhota(text) | text in lower case |
| tod(text, sep) | split text into a list: tod("a,b", ",") is ["a", "b"] |
| jod(list, sep) | join a list into text: jod(["a", "b"], "-") is "a-b" |
| chaabi(kosh) | list of the keys, in the order they were added |
| ba(x, item) | sach if a kosh has the key, a list has the item, or a string contains the text |
| hataw(kosh, key) | remove a key and return its value (khaali if it wasn't there) |
| poochh(question) | ask a question and return the typed answer as text (see Input) |
| chhaant(list) | a new, sorted list: all numbers, or all strings |
| ulta(x) | a list or string, reversed |
| hissa(x, start, end) | part of a list or string (see below) |
| khoj(x, item) | where item first appears in a list (or text in a string), or -1 |
| kul(list) | the total of a list of numbers |
| sabse_bada(list) | the largest item (all numbers, or all strings) |
| sabse_chhota(list) | the smallest item |
| saaf(text) | text without the spaces, tabs and newlines at the start and end |
| jagah(text, purana, naya) | text with every purana replaced by naya |
| shuru_me(text, x) | sach if the text starts with x |
| ant_me(text, x) | sach if the text ends with x |
| badal(list, kaam) | a new list: the kaam applied to every item (see Functions without names) |
| chhaan(list, kaam) | a new list of the items the kaam says sach to |
maan la umar = sankhya("24");
bol ho "Agila saal:", umar + 1; // Agila saal: 25
bol ho jod(tod("aalu pyaaz sattu", " "), ", "); // aalu, pyaaz, sattu
bol ho "Paasa:", sanyog(1, 6);Working with lists:
maan la ank = [42, 7, 19, 3];
bol ho chhaant(ank); // [3, 7, 19, 42] (ank itself doesn't change)
bol ho ulta(ank); // [3, 19, 7, 42]
bol ho hissa(ank, 1, 3); // [7, 19] from index 1 up to (not including) 3
bol ho hissa(ank, -2); // [19, 3] leave out the end to go to the end; negatives count back
bol ho khoj(ank, 19), kul(ank); // 2 71
bol ho ba(ank, 7), ba("namaste", "mas"); // sach sachWorking with text:
maan la naam = saaf(" Ramu "); // "Ramu"
bol ho jagah("aam aam kela", "aam", "seb"); // seb seb kela
bol ho shuru_me(naam, "Ra"), ant_me("x.bhoj", ".bhoj"); // sach sachchhaant, ulta and hissa return new lists and leave the original alone. chhaant sorts numbers by
size and strings by character code, so capital letters come before lowercase ones.
Giving a built-in the wrong kind of value is an error, not a silent wrong answer: sankhya("abc")
stops the program and says "abc" isn't a number.
Built-in names are ordinary variables, so you can reuse the names for your own variables.
Input
poochh asks a question, waits for an answer and returns it as text:
maan la naam = poochh("Tohar naam ka ba? ");
bol ho "Pranam,", naam;
maan la umar = sankhya(poochh("Umar? ")); // turn the answer into a number
bol ho "Agila saal:", umar + 1;- The question is optional:
poochh()just waits for an answer. - The answer is always text. Use
sankhya(...)for numbers. - When there's nothing left to read,
poochhreturnskhaali. That happens at the end of piped input, after Ctrl+D in a terminal, or on Cancel in the playground. Check withjadi (jawab == khaali). - In a terminal, it reads a line of what you type, and piped input works too:
printf '50\n25\n' | bhojpuri examples/andaaz.bhoj. - In the playground, it opens the browser's question box. The box also shows what was printed since the last question, and the question and answer are added to the output.
Catching errors
koshish kara ("try doing") runs a block. If something goes wrong inside it, even deep inside a function
it calls, the galti pe ("on error") block runs instead of the program stopping:
koshish kara {
maan la umar = sankhya(poochh("Umar? "));
bol ho "Agila saal:", umar + 1;
} galti pe (g) {
bol ho "Galti bhail:", g; // g is the error message
}phenk da ("throw it") raises your own error. It can be any value, and galti pe (g) gets it unchanged:
kaam bhugtaan(paisa) {
jadi (paisa < 100) {
phenk da { "kod": 402, "sandesh": "Paisa kam ba" };
}
lauta da "ho gail";
}
koshish kara {
bhugtaan(50);
} galti pe (g) {
bol ho g["sandesh"]; // Paisa kam ba
}- The
(g)is optional:} galti pe {is fine when you don't need the error. - A
phenk dathat nobody catches stops the program and shows the value as the error message. - Runtime errors can be caught, including runaway recursion and the playground's loop guard. Syntax errors can't, because they're found before the program starts.
bas kara,aage badhaandlauta dawork inside both blocks.- Write
galti peon the same line as the}that closeskoshish kara, as withna ta. This matters most at the interactive prompt. - On their own,
koshish,galtiandphenkare still ordinary names. Only the two-word forms are keywords.
Using other files: le aaw
le aaw ("bring it") runs another .bhoj file and makes its top-level functions and variables available:
// lib/ganit.bhoj
ka ho bhaiya
maan la PI = 3.14159;
kaam varg(n) { lauta da n * n }
chalat bani bhaiya// hisaab.bhoj
ka ho bhaiya
le aaw "lib/ganit.bhoj";
bol ho varg(7), PI; // 49 3.14159
chalat bani bhaiya- Paths: they're relative to the file that says
le aaw, and.bhojcan be left off (le aaw "lib/ganit"). - Each file runs once, however many files bring it in.
- Loops and clashes: files that bring each other in are an error, and so is a name that already exists in the file doing the bringing.
- Errors name the file they come from, with its own line:
Chalat samay galti (lib/ganit.bhoj, line 4, col 23): .... - Where it works: everywhere. In the
bhojpuricommand and the interactive prompt it reads files from disk. In the playground, + Naya file adds a file tab, andle aawfinds the other tabs; the program always starts frommain.bhoj, and share links carry every file. See examples/hisaab.bhoj, which is also in the playground's example list.
Operators
+ - * / %, == != < > <= >=, && || !. + joins strings, for example "umar: " + 20
(a backtick string like `umar: {20}` is often easier to read).
The falsy values are jhooth, khaali, 0 and "". Everything else counts as true.
Decimals are shown to 15 significant digits, so bol ho 0.1 + 0.2 prints 0.3 instead of
0.30000000000000004. Only the display is rounded, not the value. Like most languages, 0.1 + 0.2 == 0.3
is still jhooth, so compare decimals with gol (for example gol(x * 100) == 30) rather than ==.
Comments use // ... and /* ... */.
Errors
Errors point to the exact line and column:
Chalat samay galti (line 3, col 14): "b" naam ke koi variable na ba. Pahile "maan la b" likh ke banaw.
3 | bol ho a + b;
| ^When a name looks like a typo of one that exists, the error suggests it. This works for variables, functions, built-ins and kosh keys:
Chalat samay galti (line 3, col 10): "naam" naam ke koi variable na ba. Kahin "naamm" ta na?Changing the keywords
All keywords and built-in function names live in src/keywords.js, and all error text lives in
src/messages.js. Edit a value there and the tokenizer, CLI, playground and error
messages pick it up, and so do the playground's colours. After renaming keywords:
- regenerate the VS Code grammar with
node scripts/build-vscode-grammar.js(the tests fail if it's out of date); - update the
.bhojexamples and the tests.
Editor support
editors/vscode/ is a VS Code extension for .bhoj files. It colours keywords, strings,
numbers, comments, built-in calls and function names, including the {…} in backtick strings. It also
handles comment toggling, bracket and quote closing, and indentation. To install it from this repository:
cp -r editors/vscode ~/.vscode/extensions/aayazk.bhojpuri-lang-0.1.0Then restart VS Code. See its README for details.
How it works
source code ──► tokenizer ──► tokens ──► parser ──► AST ──► interpreter ──► output
src/tokenizer.js src/parser.js src/interpreter.js- Tokenizer splits the source into keywords, numbers, strings, names and operators. Multi-word keywords are matched longest-first.
- Parser is a recursive-descent parser that builds an abstract syntax tree (AST). The full grammar is at the top of
src/parser.js. - Interpreter walks the AST with a chain of scopes.
bas kara,aage badhaandlauta daare returned as signals up to the enclosing loop or function. Functions keep a reference to the scope they were defined in, which is what makes closures work.src/values.js: the kinds of value and how they're shown.src/scope.js: variables.src/builtins.js: the built-in functions, grouped by topic.
Using it as a library
// npm install @aayazk/bhojpuri-lang
import { run, formatError, BhojpuriError } from "@aayazk/bhojpuri-lang";
const lines = [];
try {
run(source, {
print: (line) => lines.push(line), // where bol ho goes (default: console.log)
input: (question) => "Ramu", // answers poochh; return null for "no more input"
maxLoopIterations: 100_000, // stop runaway loops
});
} catch (err) {
if (err instanceof BhojpuriError) console.error(formatError(err, source));
}To build your own prompt, use Session. It keeps variables between inputs, and run returns the value to show:
import { Session } from "@aayazk/bhojpuri-lang";
const session = new Session({ print: console.log });
session.run(`maan la x = 20`);
session.run(`x + 1`); // { exit: false, result: "21" }
session.isComplete(`kaam f() {`); // false: still waiting for the closing }For le aaw, pass loadFile(path, from), which returns { id, name, source } for the file (or null),
and file, the main program's id. id is how a file is recognised (e.g. its full path), name is shown
in error messages, and from is the id of the file that asks.
Without an input option, poochh stops with a Bhojpuri error, because there's no one to answer it.
There's also a random option that replaces Math.random for sanyog, which is handy in tests.
Everything the package exports:
| Export | What it is |
| --- | --- |
| run(source, options) | tokenizes, parses and runs a program. Options: print, input, maxLoopIterations, random, loadFile, file (all described above) |
| Session | the interactive prompt: new Session(options), then run(source) → { exit, result } and isComplete(source) |
| BhojpuriError | what run throws for a mistake in the program |
| formatError(err, source) | the error as text, with the line and a ^ under the column |
| tokenize(source), parse(tokens), Interpreter | the three steps run uses, for tools that need them separately |
| display(value) | a value as bol ho would print it |
| KEYWORDS, KEYWORD_MEANINGS, LOOP_WORDS, BUILTINS, BUILTIN_MEANINGS | the names of everything, and what each means in English |
A BhojpuriError has these fields:
kind:"SyntaxError"or"RuntimeError".message: the Bhojpuri message. Its wording may improve between versions.line,col: where the mistake is.file,source: the name and code of the file it's in, when that's a file brought in withle aawrather than the main program.value: the value given tophenk da, for an uncaughtphenk da.
Project layout
bin/bhojpuri.js CLI
src/ tokenizer, parser, interpreter, values, scope, builtins, keywords, messages,
suggest (did-you-mean hints), session (interactive prompt)
examples/*.bhoj sample programs (examples/lib/ holds a file that hisaab.bhoj brings in)
playground/ browser playground (uses src/ directly as ES modules)
editors/vscode/ VS Code extension (colours, comments, brackets)
scripts/serve.js zero-dependency static server for the playground
scripts/build-vscode-grammar.js generates the VS Code grammar from src/keywords.js
test/ node:test suite, one *.test.js file per topicContributing
Changes go through Git Flow: branch off develop, document the change in CHANGELOG.md,
and open a pull request. main only receives releases. See CONTRIBUTING.md.
License
MIT © Ayyaz Khan
