Kaori Programming Language
Kaori is a dynamically typed programming language designed to be simple, expressive, and readable.
It draws inspiration from modern languages like Python and Rust, combining their clarity and robustness in a minimal design.
Here’s a quick look at some syntax:
fun main() {
n := 5;
print(fib(n));
}
fun fib(n) {
if n < 2 {
return n;
}
return fib(n - 1) + fib(n - 2);
}
Introduction
Welcome to Kaori, a programming language created as a personal learning journey into the world of compilers and language design.
Purpose
Kaori is an educational project built to explore and understand how programming languages work from the ground up. This is a learning-focused endeavor aimed at demystifying the concepts behind:
- Lexical analysis and parsing
- Semantic analysis
- Dynamic typing and runtime type checking
- Compiler optimization techniques
- Optimal bytecode generation and virtual machines
The primary goal is education—both for myself as the creator and for anyone interested in learning about compiler construction and language implementation.
Who is This For?
This project and documentation are intended for:
- Students interested in compiler design
- Developers curious about how languages are built
- Anyone who wants to peek under the hood of a programming language
The documentation aims to be as informative as possible, explaining not just how to use the language, but also the reasoning behind design decisions and implementation details.
Grammar and the Parsing
A programming language also has its own grammar. In English grammar classes, we learned the rules to build our first sentences formed with words. In the compilers world, statements, expressions and declarations are built with tokens that are formed by a sequence of one or more characters.
We are going to enforce a set of rules known as grammar. This is a very important step to develop a compiler, going from a sequence of tokens to an Abstract Syntax Tree that can represent a program in a more meaningful way.
Here is a non-EBNF grammar with custom syntax highlight, created with regular expression, so that non-compiler developers can also understand it without having to dive into EBNF syntax:
Declarations
program -> declaration* "EOF"
declaration -> function_declaration
parameters -> (identifier ("," identifier)*)?
function_body -> "{" statement* "}"
function_declaration -> "fun" identifier "(" parameters ")" function_body
Statements
statement -> block_statement
| expression_statement ";"
| return_statement ";"
| if_statement
| while_statement
| for_statement
block_statement -> "{" statement* "}"
expression_statement -> expression
return_statement -> "return" (expression)?
if_statement -> "if" expression block_statement ("else" (if_statement | block_statement))?
while_statement -> "while" expression block_statement
for_statement -> "for" expression ";" expression ";" expression_statement block_statement
Operator Precedence
There are still unanswered questions about our parsing. Look at the following example:
2 + 3 * 5;
Mathematicians, a long time ago, created the order of operations convention. It ensures we don’t have to put as many parentheses in an expression to be able to express it in a way others would understand with no ambiguity issues. They killed two birds with one stone: the expression becomes way less verbose to read and the ambiguity is gone! So here is the question: what is the answer to that expression according to them?
The multiplication is done before the addition and the result is obviously: 17. Multiplication and division are both part of the factor rule because they share the same precedence level. Addition and subtraction are part of the term rule.
term -> factor (("+" | "-") factor)*
factor -> prefix_unary (("*" | "/") prefix_unary)*
To be able to parse an addition or a subtraction, the parser tries to parse a factor on the left and on the right side of it to ensure all the multiplications or divisions or operators with higher precedence are parsed before. This is how the order of operations are enforced by the grammar.
Expressions
expression -> declare_assign | compound_assign | logic_or
declare_assign -> logic_or ":=" logic_or
compound_assign -> logic_or ("+=" | "-=" | "*=" | "/=" | "%=") logic_or
logic_or -> logic_and ("or" logic_and)*
logic_and -> equality ("and" equality)*
equality -> comparison (("!=" | "==") comparison)*
comparison -> term ((">" | ">=" | "<" | "<=") term)*
term -> factor (("+" | "-") factor)*
factor -> power (("*" | "/" | "%") power)*
power -> prefix_unary ("**" power)?
prefix_unary -> "-" prefix_unary | "not" logic_or
arguments -> (expression ("," expression)*)?
primary -> number_literal
| string_literal
| boolean_literal
| dict_literal
| identifier (postfix_unary)*
| "(" expression ")"
postfix_unary -> function_call | member_access
function_call -> "(" arguments ")"
member_access -> "." identifier
dict_field -> expression (":" expression)?
dict_literal -> "{" (dict_field ("," dict_field)* ","?)? "}"
The First Hello World
Writing your first program in Kaori is quite simple and the main function does not need a return type annotation, because the entry point of the program does not need to return values.
fun main() {
print("hello world");
}
That’s it! Just define a main function and use the print statement to output text to the console.
Kaori Programming Language Documentation
Introduction
Kaori is a dynamically typed, simple programming language inspired by Python. It features:
- Minimal variable declarations using
:= - Simple function syntax with
fun - Dictionaries for structured data (used as simple objects)
- C-style
forloops with inline variable declarations - No classes or complex OOP features — objects are just dictionaries
Variables and Data Types
Variables in Kaori are declared using the := operator. A variable is automatically created and initialized when you first assign to it. Since Kaori is dynamically typed, the type of a variable is determined at runtime by the value it holds.
The most basic types are number, bool, nil and string:
fun main() {
foo := 5; // number
bar := true; // bool
name := "Alice"; // string
cat = {age: 4};
print(cat.name) // nil
}
Dynamic Typing
Variables are not bound to a specific type at compile time. The runtime tracks the type of each value and will raise an error if an operation is performed on an incompatible type:
fun main() {
count := 42; // holds a number at runtime
active := true; // holds a bool at runtime
}
Dictionaries
Kaori uses simple key-value dictionaries for structured data, keys can be any hashable expression.
fun main() {
dict := {a: 5, b: 7, 2 * 3: "some value", "c": 8, true: "true"};
}
You can access values using dot notation if the key is a string:
fun main() {
person := {name: "Alice", age: 30};
print(person.name); // "Alice"
}
Expressions and Operators
Operators are the building blocks of expressions, and each operator has a fixed precedence, which determines the order in which expressions are evaluated when multiple operators appear together.
Arithmetic Operators
Kaori supports the standard arithmetic operators for mathematical calculations:
+Addition-Subtraction*Multiplication/Division%Modulo (remainder)**Exponentiation (power)
Multiplication, division, and modulo have higher precedence than addition and subtraction. Exponentiation (**) has the highest precedence among arithmetic operators:
3 + 4 * 5; // 23
(3 + 4) * 5; // 35
10 % 3; // 1
2 ** 10; // 1024
2 + 3 ** 2; // 11 (exponentiation before addition)
Comparison Operators
Comparison operators like >, <, >=, <=, ==, and != always evaluate to a boolean value:
12 > 7; // true
7 == 12; // false
95 >= 95; // true
10 != 5; // true
Logical Operators
Logical operators such as and, or, and not allow combining boolean expressions:
true and false; // false
true or false; // true
not 5 == 6; // true
Assignment Operators
The assignment operator (=) has the lowest precedence, ensuring that the expression on the right-hand side is fully evaluated before being assigned to the variable on the left:
a = 3 + 4 * 2; // a = 11
Compound Assignment Operators
Kaori provides compound assignment operators that combine an arithmetic operation with assignment. These operators are shorthand for performing an operation and then assigning the result back to the variable:
+=Add and assign-=Subtract and assign*=Multiply and assign/=Divide and assign%=Modulo and assign**=Exponentiate and assign
fun main() {
x := 10;
x += 5; // Equivalent to: x = x + 5 (x is now 15)
x -= 3; // Equivalent to: x = x - 3 (x is now 12)
x *= 2; // Equivalent to: x = x * 2 (x is now 24)
x /= 4; // Equivalent to: x = x / 4 (x is now 6)
x %= 4; // Equivalent to: x = x % 4 (x is now 2)
x **= 3; // Equivalent to: x = x ** 3 (x is now 8)
}
Compound assignment operators are particularly useful in loops and when updating values incrementally:
fun main() {
sum := 0;
for i := 1; i <= 5; i += 1 {
sum += i; // Add each number to the sum
}
print(sum); // 15
}
The right-hand side of a compound assignment can be any valid expression:
fun main() {
total := 100;
multiplier := 3;
total *= multiplier + 2; // total = total * (3 + 2) = 500
total **= 2; // total = total ** 2 = 250000
}
Parentheses and Precedence
Parentheses can always be used to make evaluation order explicit. If parentheses are omitted, then the parsing follows operator precedence.
Control Flow
Control flow allows you to decide how the code executes: you can branch into different paths or repeat code with loops. Kaori provides several control flow mechanisms to help you write expressive and efficient code.
If Statements
An if statement runs a block of code only if its condition is true. You can chain multiple conditions using else if and provide a fallback with else.
fun main() {
if 10 > 5 {
print("10 is bigger");
} else if 2 < 3 {
print("2 is smaller");
} else {
print("all the other branches condition were false");
}
}
The condition must evaluate to a boolean value. You can use comparison operators (>, <, >=, <=, ==, !=) and logical operators (and, or, not) to build complex conditions.
While Loops
A while loop runs a block of code repeatedly as long as the condition remains true. The condition is checked before each iteration.
fun main() {
i := 0;
while i < 3 {
print(i);
i += 1;
}
}
For Loops
A for loop provides a more compact way to write loops with variable initialization, condition, and increment logic.
fun main() {
for i := 0; i < 3; i += 1 {
print(i);
}
}
The variable declared in the for loop is scoped to the loop body and cannot be accessed outside of it.
Nested Loops
Loops can be nested, which is useful for iterating over multiple dimensions or working with matrices and grids.
fun main() {
for x := 0; x < 2; x += 1 {
for y := 0; y < 2; y += 1 {
print(x + y);
}
}
}
Break Statement
The break statement immediately exits the innermost loop, skipping any remaining iterations. This is useful when you want to stop a loop early based on a condition.
fun main() {
for i := 0; i < 10; i += 1 {
if i == 5 {
break; // Exit the loop when i equals 5
}
print(i);
}
print("Loop finished");
}
In nested loops, break only exits the innermost loop:
fun main() {
for x := 0; x < 3; x += 1 {
for y := 0; y < 3; y += 1 {
if y == 2 {
break; // Only exits the inner loop
}
print(x + y);
}
}
}
Continue Statement
The continue statement skips the rest of the current iteration and moves to the next iteration of the loop. This is useful when you want to skip specific cases without exiting the entire loop.
fun main() {
for i := 0; i < 5; i += 1 {
if i == 2 {
continue; // Skip printing when i equals 2
}
print(i);
}
}
You can use continue to filter out unwanted iterations:
fun main() {
// Print only even numbers
for i := 0; i < 10; i += 1 {
if i % 2 != 0 {
continue; // Skip odd numbers
}
print(i);
}
}
Functions
A function is declared with the fun keyword, followed by its name, parameters, and an optional return type.
Basic Functions
fun square(n) {
return n * n;
}
fun main() {
result := square(5);
print(result); // 25
}
Recursive Functions
Functions can also call themselves recursively. Just remember to include a base case! :D
fun foo(n) {
print(n);
if n > 0 {
bar(n - 1);
}
}
fun bar(n) {
print(n);
if n > 0 {
foo(n - 1);
}
}
fun main() {
foo(10);
}
Object-Oriented Programming
Kaori does not have classes or traditional object-oriented features. Instead, it uses dictionaries to represent objects. This keeps the language simple and flexible.
fun Cat(name, age, color) {
return {
name,
age,
color,
};
}
You can create object-like structures by returning dictionaries from functions.
Creating and Using Objects
fun Cat(name, age, color) {
return {
name,
age,
color,
};
}
fun main() {
cat := Cat("meow", 5, "black");
print(cat.name); // "meow"
print(cat.age); // 5
}
Updating fields
Fields can be modified directly:
fun Cat(name, age, color) {
return {
name,
age,
color,
};
}
fun main() {
cat := Cat("meow", 5, "black");
cat.age += 1;
print(cat.age); // 6
}
Functions as Methods
Functions can operate on objects by receiving them as arguments:
fun Cat(name, age, color) {
return {
name,
age,
color,
};
}
fun greet(self) {
print(self.name);
}
fun main() {
cat := Cat("meow", 5, "black");
greet(cat);
}
This approach provides a simple and flexible way to structure data and behavior without introducing complex class systems.
Error Reporting
Error reporting is one of the core features. A programming language without clear diagnostics misses one of the most important pillars of usability. In the current implementation, it provides detailed error messages, showing both the line and the column where the error occurred and pointing exactly to the problematic code. This makes debugging much easier and helps developers understand what went wrong.
Example: Syntax Error
fun main() {
print(2 +);
}
What do we expect to happen in the code above? Can you guess? It’s a syntax error—an addition operation expects to have a left and a right operand, but a right parenthesis is not a valid operand.
Kaori’s error reporting will pinpoint exactly where the problem is, showing you the line, column, and the specific token that caused the issue, making it clear what needs to be fixed.
The Future
I believe we can now confidently call Kaori a Turing-complete programming language. Many core features have already been implemented, and the journey so far has been both fun and challenging.
Performance
Here’s how Kaori compares to other dynamic languages when computing Fibonacci numbers:
| Language | Version | Iterative (ms) | Iterative ± | Recursive (ms) | Recursive ± |
|---|---|---|---|---|---|
| Kaori | 1.0.0 | 246.296 | 1.40% | 26.969 | 0.86% |
| Lua | 5.5.0 | 201.600 | 5.01% | 36.500 | 3.14% |
| Python | 3.14.4 | 3083.135 | 11.00% | 102.384 | 2.50% |
| PyPy | 7.3.20 | 47.110 | 1.67% | 21.881 | 3.56% |
Iterative: fib(30) computed 10^6 times using an iterative algorithm
Recursive: fib(30) computed once using a naive recursive algorithm (no memoization)
Environment
- CPU: AMD Ryzen 5 5600 (6-core, 12-thread, 3.5GHz base / 4.4GHz boost)
- RAM: 16GB DDR4 3000MHz
- Motherboard: ASUS A320M
- OS: Windows 11
Note: These benchmarks are based on a specific Fibonacci implementation and should not be taken as comprehensive performance claims. Kaori is still in early development, and performance characteristics may vary significantly across different workloads and use cases. Real-world performance depends on many factors beyond microbenchmarks. Benchmark results may also be slightly outdated at times as the language continues to evolve.
Development roadmap
- Error reporting
- Code comments
- Arithmetic operators
- Comparison operators
- Logical operators
- Prefix unary operators
- Print statement
- Variable declaration
- Compound assignment operators
- If / else statement
- For loop
- While loop
- Functions
- Bytecode instructions
- Register virtual machine
- Optimizations
- Dictionaries
- Vectors
- Standard library
- Garbage collector
- Error handling mechanisms
- Module system
Name Inspiration
The name “Kaori” is inspired by Kaori Miyazono from the anime “Your Lie in April”. She represents inspiration, motivation, and the desire to create something different from the standard—the same spirit behind creating this programming language. ❤️