mni is an interpreter for Nuru, the programming language with Swahili keywords. It is written in C++20: a lexer, a Pratt parser, a tree-walking evaluator and an optional bytecode VM, with the standalone executable nuru.
Status: work in progress. The language core, packages and imports, and the
hisabati,jsoni,muda,mtandao(HTTP) andosmodules work. See What's missing.
fanya salamu = unda(jina, lugha = "sw") {
kama (lugha == "sw") {
rudisha "Habari, " + jina + "!"
}
rudisha "Hello, " + jina + "!"
}
kwa i, jina ktk ["Juma", "Asha"] {
andika(i, salamu(jina))
}
0 Habari, Juma!
1 Habari, Asha!
mni implements the language as described in Nuru's documentation. It is not affiliated with the Nuru project.
You need CMake 3.24 or newer and a C++20 compiler. Dependencies are downloaded by CMake on first configure, so the first build needs network access: fmt, nlohmann/json, cpr (HTTP) and, for the tests, doctest.
cpr uses the system libcurl on Linux and macOS (macOS ships it; on Debian and Ubuntu install libcurl4-openssl-dev and libssl-dev) and builds curl itself on Windows.
cmake -Sstandalone -Bbuild/standalone -DCMAKE_BUILD_TYPE=Release
cmake --build build/standalone
./build/standalone/nuru --toleoWithout -DCMAKE_BUILD_TYPE=Release the build is unoptimized and noticeably slower.
Pass -DMNI_ENABLE_HTTP=OFF to build without the mtandao module and without libcurl and cpr. Installing the library (cmake --install) currently requires this, because the installed package cannot carry cpr; with HTTP on, install stops with an explanatory error.
nuru start the REPL
nuru file.nr run a file (.nr or .sw)
nuru --ast file.nr print the parsed program instead of running it
nuru --tokens file.nr print the tokens
nuru --disasm file.nr print the bytecode
nuru --toleo print the version (-v, --version)
nuru --msaada print help (-h, --help)
The REPL keeps variables between lines, continues a statement while (, [ or { are open, and prints any result that is not tupu. Leave it with toka(), exit() or end of input.
Errors name the line, in Swahili like Nuru does: Mstari 3: Kitambulishi hakipatikani: x. A runtime error in a file exits with status 1.
Semicolons are optional and newlines are not significant. Comments are // … and /* … */.
Because newlines do not end a statement, a line that starts with ( or [ continues the previous line: x = 3 followed by [1, 2] on the next line parses as 3[1, 2]. End the earlier line with ; when that matters.
| Type | Examples | aina() |
|---|---|---|
| integer | 42, -7 |
NAMBA |
| float | 3.14 |
DESIMALI |
| string | "habari", 'habari' |
NENO |
| boolean | kweli, sikweli |
BOOLEAN |
| null | tupu |
TUPU |
| array | [1, "pili", kweli] |
ORODHA |
| dictionary | {"jina": "Juma", 1: "moja"} |
KAMUSI |
| function | unda(x) { rudisha x } |
KAZI |
Only tupu and sikweli are false; 0 and "" are true. Strings are UTF-8 and are indexed and looped over by character. Dictionary keys can be strings, integers, floats or booleans, and keep their insertion order. Arrays and dictionaries are shared by reference.
fanya x = 10 // always creates x in the current scope
y = 20 // updates an existing y, or creates it
x += 5
y %= 7
x++
2 + 3 * 5 // 17
3 / 5 // 0.6 (exact integer division stays an integer: 6 / 3 is 2)
2 ** 10 // 1024
"ab" * 3; // "ababab"
[1, 2] + [3]; // [1, 2, 3]
20 ktk [10, 20] // kweli (membership: arrays, dictionary keys, substrings)
Integer arithmetic that would overflow continues as a float. Operator precedence follows Nuru, including two oddities: % binds tighter than **, and && and || share one level.
kama (x > 5) { andika("kubwa") } au kama (x == 5) { andika("tano") } sivyo { andika("ndogo") }
wakati (i < 10) { i++; kama (i == 3) { endelea }; kama (i == 8) { vunja } }
kwa v ktk [10, 20] { andika(v) } // values
kwa i, v ktk "abc" { andika(i, v) } // index (or key) and value
badili (x) {
ikiwa 1, 2 { andika("moja au mbili") }
kawaida { andika("nyingine") }
}
fanya jumla = unda(a, b = 10) { rudisha a + b }
jumla(1) // 11
jumla(1, 2) // 3
fanya hesabu = unda() {
n = 0
rudisha unda() { n++; rudisha n } // closures
}
A function returns the value of its last statement unless rudisha is used. Functions create a scope; kama, wakati and kwa blocks do not. Recursion is limited to 1000 nested calls, or fewer if the platform stack runs low; either way it is an error, not a crash.
A pakeji is a named scope. Its variables are its members, and @ inside it refers to the package itself.
pakeji benki {
andaa = unda(salio) { @.salio = salio }
weka = unda(kiasi) { @.salio += kiasi; rudisha @.salio }
}
benki.andaa(100)
benki.weka(50) // 150
benki.salio // 150
tumia name imports a built-in module, or loads name.nr from the directory of the running script (falling back to the working directory). The file must define pakeji name. A file is loaded once, and its top-level variables stay private to it.
tumia hisabati, jsoni
tumia benki // loads benki.nr
Method calls accept named arguments: benki.andaa(salio = 100). Plain calls do not, because f(x = 1) is an assignment.
| Function | |
|---|---|
andika(...) |
print the arguments separated by spaces |
jaza(prompt?) |
read a line of input (tupu at end of input) |
aina(x) |
the type name of x |
mfululizo(end), mfululizo(start, end, step?) |
array of integers |
fungua(path) |
open a file; .soma() returns its contents |
toka(code?), exit(code?) |
exit |
| On | Methods |
|---|---|
| string | idadi(), herufikubwa(), herufindogo(), gawa(separator?) |
| array | idadi(), sukuma(...), yamwisho(), map(f), chuja(f) |
| Module | Contents |
|---|---|
hisabati |
constants PI, e, phi, ln2, ln10, log2e, log10e, sqrt1_2, sqrt2, sqrt3, sqrt5, EPSILON; functions abs sign sqrt cbrt root factorial ceil floor round max min hypot random and the trigonometric, hyperbolic, exponential and logarithmic functions |
jsoni |
dikodi(text) parses JSON into dictionaries and arrays; enkodi(value) produces compact JSON |
muda |
hasahivi(), leo(), tangu(t), lala(seconds), baada_ya(seconds), tofauti(a, b), and t.ongeza(sekunde=, dakika=, masaa=, siku=, wiki=, miezi=, miaka=) |
mtandao |
HTTP: peruzi(url) or peruzi(yuareli=, vichwa=, mwili=) sends a GET (mwili becomes query parameters); tuma(yuareli=, vichwa=, mwili=) sends a POST. vichwa is a dictionary of headers. A dictionary or array mwili is sent as JSON, a string as is. Both return the response body as a string whatever the status code, and raise an error if the request cannot be made (30 second timeout). |
os |
kimbiza(command) runs a shell command and returns what it printed; toka(code?) exits |
- Dictionary methods and
dict.keyaccess (usedict["key"]). - Running scripts you do not trust:
fungua,tumia,os.kimbizaandmtandaocan all reach outside the program, and there are no step or time limits yet. hisabati.roundwith a rounding-method argument.- Memory from closures and self-referencing structures is not reclaimed until the program exits.
cmake -Stest -Bbuild/test
cmake --build build/test
ctest --test-dir build/test --output-on-failurePrograms run on the bytecode VM. Set MNI_BACKEND=tree (or pass Interpreter::Options with Backend::Tree) to use the tree-walking evaluator instead; it is kept as a reference and gives the same values, output and error messages, apart from the cases in docs/vm-divergences.md. VM calls do not use the C++ stack, so a high setMaxCallDepth is safe, while the tree-walker is limited by the thread's stack (on Windows, 1 MB). In a Release build the VM runs fib(30) in about 0.14s, against 0.24s for the tree-walker.
Interpreter::setLimits stops runaway programs with a mni::LimitError: a tick budget (one tick per loop iteration or user function call), a timeout, or a cancel flag you can set from another thread. Native calls such as a shell command or an HTTP request are not interrupted while they run.
#include <mni/interpreter.h>
mni::Interpreter interp; // prints to std::cout, reads std::cin
mni::Value result = interp.run("fanya x = 6 * 7; x");
std::cout << result.inspect(); // 42run throws mni::SyntaxError (all parser messages in errors), mni::NuruError for runtime errors, and mni::ExitRequest for toka(). State persists between run calls.
include/mni/ public headers: token, lexer, ast, parser, symbol, value, environment, interpreter, repl
source/
lexer, parser, ast, token the frontend
value, symbol, environment values, interned names, variable scopes
compiler, bytecode, verify, disasm, vm the optional bytecode backend
interpreter, operators, stack the evaluator (operators.cpp: arithmetic, comparison, indexing;
stack.cpp: turns runaway recursion into an error)
imports.cpp pakeji and tumia: package scopes, module and file loading
builtins.cpp, methods.cpp global functions; string and array methods
hisabati, jsoni, muda, modules the built-in modules and their registry
native.h internal helpers shared by the native code (not installed)
standalone/ the `nuru` executable
test/ doctest unit tests
bench/ benchmark programs; `bench/run.sh` prints their results, time them with a Release build
cmake/ CPM and helper scripts
MIT, see LICENSE. Nuru itself is a separate project under the GPL-2.0.