A most definitely WIP logo for this WIP language.
Tahini is a paste made from sesame seeds that can be used as a dip, spread, or dressing. It's versatile, flavorful, and adds a unique touch to many dishes, while being a healthy choice. Inspired by the simplicity, flexibility, and richness of tahini, we present Tahini, a programming language that aims to be a joy to use, with a focus on simplicity, expressiveness, and extensive testing support.
Tahini is a lightweight, tree-based interpreted programming language that is written using Java, and which runs on the JVM (Java Virtual Machine), inspired by Lox and Python. It aims to provide simplicity and expressiveness alongside extensive testing and contract support, making it a joy for developers to use. Currently, Tahini supports a number of core language and testing features, with an exciting roadmap of future capabilities.
scoop "./kitchen.tah" into kitchen;
fun totalIngredients(ingredientQuantities)
// contract
postcondition: total >= 0
{
var total = 0;
for (var i = 0; i < len(ingredientQuantities); i = i + 1) {
total = total + ingredientQuantities[i];
}
return total;
}
fun prepareDish() {
return kitchen::bake(100, kitchen::ovenTemperature);
}
test "totalIngredients test" {
// Test case: summing 3 ingredients
assertion: totalIngredients([1, 2, 3]) == 6, "Should be 6!";
// Test case: summing 0 ingredients
assertion: totalIngredients([]) == 0, "Should be 0!";
}
var flour = 2;
var sugar = 1;
var eggs = 3;
var ingredientsList = [flour, sugar, eggs];
print "Total ingredients needed: " + totalIngredients(ingredientsList);
print prepareDish();
- Tahini Language
- The Theory Behind This Implementation of Tahini
Tahini currently implements:
- Variables: Declare mutable variables using a simple and concise syntax.
- Loops: Supports
while
andfor
loops to handle iteration. - Conditionals: If-else statements for decision-making.
- Functions: First class citizens of Tahini. Define and call reusable blocks of code, with support for contracts (
precondition
,postcondition
, andassertion
). - Classes: Object-oriented features to group variables and methods (halted in favour of a lean towards a functional paradigm).
- Advanced Data Structures: Basic support for lists, maps, and other data structures.
- Error Handling: Support for user-defined exceptions and error handling (in progress).
- Stack Traces: Detailed error messages with line numbers and function names.
- Unit Tests: Write test blocks directly in the source file to validate code correctness.
- Import System: Import other Tahini files to reuse code and create modular applications.
- Standard Library: A growing set of built-in functions and utilities, called the
larder
.
Planned features include cross-language support.
You can download the latest prebuilt binaries for your operating system from the Releases page. Follow these steps to get started quickly.
-
Download the Latest Binary for your OS: Latest Release
-
Make the Binary Executable (Linux/macOS)
chmod +x tahini-linux # For Linux
chmod +x tahini-macos # For macOS
- Run Tahini (start up the REPL)
./tahini-linux # For Linux
./tahini-macos # For macOS
tahini.exe # For Windows
Welcome to Tahini. Type in your code below:
>
- Run Tahini Code (execute a Tahini script from file)
./tahini-linux path/to/file.tah # For Linux
./tahini-macos path/to/file.tah # For macOS
tahini.exe path/to/file.tah # For Windows
If you prefer to build Tahini from the source code, follow these instructions:
To get started with Tahini, clone the repository and build the project using Maven or Gradle. Since Tahini is built on top of the JVM, ensure you have a valid Java JDK installed (>=21).
git clone
cd tahini
gradle build
Warning
If you get an error, it may be due to the Java version. Ensure you have Java 21 or higher installed, or use the ./gradlew
wrapper to run the project.
./gradlew run --args="path/to/file.tah"
To ensure that Tahini is correctly installed, you can run the test suite using the provided script:
(>_>) ./run_tests.sh
Building the project...
BUILD SUCCESSFUL in 439ms
5 actionable tasks: 5 up-to-date
Running regular tests...
Test tests/recursion.tah passed.
Test tests/ternary.tah passed.
... (more tests)
Running flag tests...
Test tests/flag/basic.tah passed.
... (more tests)
Tests completed.
Tahini comes with a simple REPL (Read-Eval-Print Loop) to run your code interactively. You can also execute scripts via the command line.
To start the REPL:
(>_>) java -jar app/build/libs/app.jar
Welcome to Tahini. Type in your code below:
> 3+4;
7
> print "hello there!";
hello there!
> Exiting prompt.
To run a Tahini script:
java -jar app/build/libs/app.jar "../test.tah"
Tip
Check out the VSCode extension for Tahini for a more integrated development experience.
All variables in Tahini are dynamically typed, and you can declare them using the var
keyword. Variables can be reassigned, and their scope is determined by the block in which they are declared.
var a = "global a";
{
var a = "local a";
print a;
}
print a;
var x = 10;
var y = 20;
print x + y;
var name = input();
print name;
fun greet(name) {
print clock();
print "Hello, " + name + "!";
}
greet("Name");
Inspired by the documentation of Dlang: https://dlang.org/spec/function.html#contracts, as well as the following proposal for C++: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0380r1.pdf
For example, the following function calculates the square root of a number using the Newton-Raphson method, with a precondition
that the input value must be non-negative, and a postcondition
that helps confirm that the sqrt function produced an acceptable result:
fun safeSqrt(value)
precondition: value >= 0
postcondition: x >= 0
{
var x = value;
var tolerance = 0.00001; // Define a tolerance level for the approximation
var difference = x;
while (difference > tolerance) {
var newX = 0.5 * (x + value / x);
difference = x - newX;
if (difference < 0) {
difference = -difference;
}
x = newX;
}
return x;
}
If a contract is violated, a runtime error will be thrown. Preconditions are checked before the function is executed, and postconditions are checked after the function body, before it returns.
Precondition failed.
[line 3] in <fn withdraw>
[line 15]
assertion
works in a similar way, except it can be used anywhere in the function body, and outside of functions as well. A non-critical form of assertion
is also available — check
. This will log a warning to stderr, but will not interrupt the program execution by throwing an error.
var recipeName = "Muffins";
var sugar = 50;
assertion: recipeName != nil, "Recipe name cannot be nil!";
check: sugar < 40, "Sugar might be too much.";
In the above example, if recipeName
is nil
, a critical error will be thrown, but if sugar
is more than/equal to 40, a warning will be logged to stderr.
Inspired by https://ziglang.org/documentation/master/#Zig-Test and https://dlang.org/spec/unittest.html
You can define test blocks — any statement (block, declaration or individual statement) that is prefixed with the test
keyword. The test block can contain assertions that check the correctness of the code, and will be ignored during normal execution but will be run when the file is executed with the --test
flag. This allows you to write unit tests for your code directly in the source file.
If a test block fails (i.e., when any statement within it throws a RuntimeError) while running with the test flag, the test block name and the line number of the failing assertion will be printed to the console.
// Fibonacci function
fun fib(n) {
if (n <= 1) return n;
return fib(n - 2) + fib(n - 1);
}
// Regular code block
var x = 10;
print "Fib(x): " + fib(x);
// Test block to check Fibonacci function
test "checking this out" {
assertion: fib(0) == 0;
assertion: fib(1) == 1;
assertion: fib(2) == 1;
assertion: fib(3) == 2;
assertion: fib(4) == 3;
assertion: fib(5) == 5;
assertion: fib(6) == 8;
}
// Test block that should fail
test "this should fail" {
assertion: fib(0) == 0;
assertion: fib(1) == 1;
assertion: fib(2) == 1222; // This will fail
assertion: fib(3) == 2;
}
// Another regular code block
var y = 20;
print "Value of y: " + y;
// Test block to check variable values
test "variable check" {
assertion: x == 10;
assertion: y == 20;
assertion: fib(4) == 3;
}
Running the file normally will ignore the test blocks:
Fib(x): 55
Value of y: 20
Running the file with the --test
flag will execute the test blocks:
Fib(x): 55
Value of y: 20
Test Results:
PASS (line 12): checking this out
FAIL (line 23): this should fail (assertion contract failed (null))
PASS (line 35): variable check
Arrays are implemented as an ArrayList. You can create an array via [...]
syntax, and access elements using the []
operator. Arrays can contain any object values, including functions (since functions are first-class citizens in Tahini), and can be sliced and concatenated.
var arr = [1, 2, "string", fib, 5];
print arr[0]; // 1
varr arr2 = arr[1:3];
print arr2; // [2, "string"]
You can write basic basic append and remove function to manipulate arrays (utilising the inbuilt len
function):
fun append(arr, value) {
return arr + [value];
}
fun remove(arr, index) {
return arr[0:index] + arr[index+1:len(arr)];
}
Maps are implemented as a HashMap. You can create a map via {...}
syntax, and access elements using the []
operator. Maps can contain any object keys or values.
var map = {"key": "value", 1: 2, "fib": fib};
print map["key"]; // value
var a = 10;
if (a > 5) {
print "Greater than 5";
} else {
print "Less than or equal to 5";
}
// ternary
var b = a > 5 ? "Greater than 5" : "Less than or equal to 5";
for (var i = 0; i < 10; i = i + 1) {
if (i == 5) break;
print i;
}
var i = 0;
while (i < 5) {
print i;
i = i + 1;
}
Tahini supports importing other Tahini files to reuse code and create modular applications. You can import a file using the scoop
keyword, followed by the path to the file. The imported file will be executed in the current scope, allowing you to access its variables and functions.
scoop "../kitchen.tah";
function_from_kitchen();
The above would do a flat import of the kitchen.tah
file, executing it in the current scope, and making every variable and function in kitchen.tah
available in the current file's global scope without any prefix. This should be used with caution, as it can lead to naming conflicts, pollution and unintended side effects.
To avoid polluting the global environment, it is recommended to use namespaced imports.
scoop "../kitchen.tah" into kitchen;
kitchen::function_from_kitchen();
With this, all functions and variables from kitchen.tah are accessible only through the kitchen namespace. If kitchen.tah
defines a function prepare()
, you would now call it as kitchen::prepare()
in your current file.
Tahini also allows nested imports, so if a file you import also imports other files, they will follow the same flat or namespaced rules. For example:
// C.tah
fun function_from_c() {...}
// D.tah
fun function_from_d() {...}
// B.tah
scoop "C.tah";
scoop "D.tah" into D;
fun function_from_b() {...}
// A.tah
scoop "B.tah" into B;
B::function_from_b();
B::function_from_c();
B::D::function_from_d();
See tests/namescoop for an example of how imports work.
Apart from its standard library (larder
), Tahini provides a set of built-in functions in the default namespace for common operations:
input()
- Read a line of string input from the user.clock()
- Get the current time in seconds since the Unix epoch.len(arr)
- Get the length of an array.typeOf(value)
- Get the type of a value as a string.stronum(string)
- Convert a string to a number.
Tahini comes with a growing standard library, called the larder
, which provides a set of built-in functions and utilities to simplify common tasks. The larder
includes functions for string manipulation, file I/O, math operations, and more.
Here are some of the modules and functions available in the larder
:
larder/math
- Mathematical functions likesqrt
,pow
,sin
,round
etc.larder/string
- String manipulation functions likesplit
,join
,replace
etc.larder/io
- File I/O functions likereadFile
,writeFile
etc.larder/collections
- Collection functions likevalues
,keys
,append
,remove
etc.larder/time
- Time functions likenow
,format
etc.larder/random
- Random number generation functions likerandom
,randomInt
etc.larder/http
- HTTP request functions likeget
(onlyget
for now).
You can import the larder
modules in your Tahini code using the scoop
keyword, similar to importing other Tahini files.
scoop "larder/math" into math;
scoop "larder/io";
var x = math::sqrt(25);
print x;
writeFile("output.txt", "Hello, Tahini!");
print typeOf(x); // "number"
Note: we will do in-depth feasability analysis on these goals, and decide on attempting to accomplish them based on our provided timeline.
Looking ahead, Tahini has ambitious stretch goals that would set it apart as a versatile and flexible language:
-
Cross-Language Imports (Stretch Goal)
- Integrate cross-language importing capabilities, allowing you to import Java
.java
files directly into Tahini code.
- Integrate cross-language importing capabilities, allowing you to import Java
-
In-line and In-built SQL Query Support (Stretch Goal)
- Integrate SQL query support directly into the language, enabling easy database queries within the code.
Tahini is implemented as a tree-walk interpreter running on the Java Virtual Machine (JVM). This architecture leverages the simplicity of an interpreted language model while taking advantage of the powerful runtime capabilities of the JVM. Here's a theoretical breakdown of this approach:
A tree-walk interpreter is a simple form of interpreter that executes programs by directly traversing an abstract syntax tree (AST) after parsing the source code. The AST is a hierarchical representation of the structure of the program, where nodes represent operations, expressions, statements, and other language constructs.
In Tahini's case:
- Parsing: The source code is tokenized and parsed into an AST.
Tip
Note: you can see the AST for a Tahini program by running the --visualize
flag with the Tahini interpreter.
- Interpretation: The interpreter walks the tree recursively, evaluating expressions, executing statements, and manipulating variables as it encounters them.
This approach is straightforward because the AST is used directly without translating the code into an intermediate form or machine-level bytecode.
While more complex implementations (such as bytecode interpreters or ahead-of-time compilers) are possible, a tree-walk interpreter has several advantages for a language like Tahini:
-
Simplicity and Flexibility: The tree-walk interpreter is easier to implement and modify, making it ideal for rapid development and iteration. New language features (like functions, loops, and conditionals) can be integrated into the AST, and the interpreter can be adapted accordingly.
-
Development Speed: By focusing on high-level language features rather than low-level optimizations, the initial development of Tahini can be more focused on usability, syntax, and expressiveness, rather than performance overhead.
-
Integration with Java: Since Tahini is written in Java and runs on the JVM, it allows deep integration with existing Java libraries and tooling. Even in a tree-walk interpreter model, Java's mature ecosystem can be leveraged to provide features like I/O, networking, or concurrency, without needing to reinvent those aspects in Tahini.
There are, of course, trade-offs to this approach. Tree-walk interpreters can be slower than compiled languages or more sophisticated interpreters, as they reevaluate the AST on each execution. In the future, Tahini could explore more advanced techniques like JIT compilation, bytecode generation, or runtime optimizations to improve performance. For example, we could implement Tahini in C, and translate Tahini code into an efficient Bytecode representation, which can be executed by a custom VM. We would have to go deeper into the implementation of features such as garbage collection, memory management, etc, which are currently handled by Java and the JVM for us.
We hope you enjoy using Tahini! Stay tuned for more features and updates.