
A modern object-oriented programming language focused on simplicity & practicality.
[!NOTE] This project is currently under active development. Documentation may be incomplete.
Kira is a modern, pure object-oriented programming language with expressive syntax inspired by Swift, Kotlin, and Dart. It functions as a flexible toolkit—similar to Haxe—supporting transpilation and ahead-of-time (AOT) compilation to multiple targets, including source code, bytecode, bitcode, and machine code.
Kira enforces three core principles: privacy, immutability, and static behavior. All declarations are
private and immutable by default. To enable mutability or public access, use the mut or pub modifiers respectively.
Classes contain only instance-level data; static and companion members are managed via namespaces.
Version 1.1
October 1, 2025
@trace("Hello World!")
Kira supports comments via //. Anything after will be stripped out by the preprocessor.
// I won't appear in the generated output!
Kira is a statically-typed language meaning that all variables must declare a type.
name: Str = "John"
age: Int32 = 34
isOk: Bool = true
These structures help determine which pieces of your code will run. Kira has the following control structures:
if-else selection statementswhile loopsdo-while loopsfor loops// If-Else Selection Statement
if name == "John" {
@trace("Hi John")
} else if name == "Jamie" {
@trace("Hi Jamie")
} else {
@trace("Who are you?")
}
// While Loop
while true {
@trace("Looping forever...")
}
// Do-While Loop
do {
@trace("I will be printed once!")
} while false
// For Loop
for i: Int32 in 1..10 {
@trace(i)
}
Note: These are statements and not expressions (e.g. you cannot return using a control flow structure).
Kira treats functions as first-class citizens, meaning you can use them as values and pass them around.
To return a value, Kira uses the return keyword. If a function has no return, it must
specify its return type as Void or Nothing.
fx sumOf(a: Int32, b: Int32 = 3): Int32 {Inte
return a + b
}
Function parameters can either be specified using their name or using position.
Using the ${} syntax, you can embed expressions directly into strings.
name: Str = "John"
age: Int32 = 34
@trace("My name is ${name} and I am ${age} years old.")
Please note that there is no
$varsyntax without the curly braces. This is to avoid ambiguity and confusion.Additionally, you can escape the dollar sign by using
\$.
Kira uses a module system for you to organize your source structure. Each piece of Kira source file starts off with a module declaration:
module "author:module_name/module_name/submodule_name"
author represents the organization or individual that this belongs to.module_name represents what project this source file falls under.submodule_name represents this individual source file’s nameWhatever is at the end of the module declaration is what the file is named. For example, if the module declaration is
module "kira:lib.types", then the file must be namedtypes.kira.Additionally, all intermediate
module_namemust be directories.
Often times, you would include or import submodules, not entire modules themselves per submodule. This is to save on compile time and also potential bloating. To include another submodule:
use "author:module_name/submodule_name"
For example, to use all builtin types from Kira (e.g. Int32, Str), you need to use the submodule kira:lib.types.
There is no wildcard import. You must explicitly import each submodule you want to use.
Kira supports classes, an object-oriented principle. Class in Kira are very easy and malleable to understand and lightweight; however, they do have some limitations:
pub class Vector2
{
require pub mut x: Float32
require pub mut y: Float32
pub fx dot(other: Vector2): Float32 {
return (other.x * x) + (other.y * y)
}
pub mut fx toStr(): Str {
return "( ${x}, ${y} )"
}
}
a: Vector2 = Vector2 { 3, 3 }
b: Vector2 = Vector2 { 3, 3, toStr = fx() { return "< ${x}, ${y} >" } }
@trace(a.dot(b))
For constructor calls, you can also use named parameters like so:
b: Vector2 = Vector2 { y = 3, x = 3 }They follow the same rules as function parameters with positional and named parameters.
Multi-inheritance is not allowed, but to share common functions across multiple classes, Kira supports traits
Inheritance is very simple, there are only several types of allowed patterns:
However, all of these utilize the format of classes meaning that you can only use ONE of these even if it is Interface-Like.
Here are some examples of the previously mentioned patterns:
All members are implemented, with the only exception being property fields.
pub class Student {
require pub name: String
require pub mut gpa: Float32
pub fx passing(): Boolean {
return gpa > 2.0
}
}
Semi Abstract classes are created where you add unimplemented member function (methods) into the mix of concrete classes. However, they are “semi” abstract because Kira allows anonymous classes to be made everywhere by passing functions directly to the constructor.
pub class Human {
pub scientificName: String= "Homo Sapien"
pub fx speak(): Void
pub fx walk(): Void {
@trace("Walking...")
}
}
This pattern is the most redundant and should be avoided. Instead, prefer to use traits if you need to share common functionalities across multiple classes. In general, interface like classes define no property members and only abstract function members:
pub class Animalia {
pub fx reproduce(): Animalia
pub fx die(): Void
pub fx eat(): Void
pub fx isAlive(): Bool
}
Everything in Kira is immutable or closed by default. This means variables cannot be reassigned/mutated, classes cannot be inherited from, methods in classes cannot be overridden by default.
In order to allow for mutability, specify the mut modifier before the element you want to make mutable.
There are only 2 visibility levels allowed:
pubThe pub modifier specifies that anything outside can look inside. For example, a submodule which has a
class:
class A {
}
pub class B {
}
An external submodule that uses this cannot see class A, but can see class B. Additionally, class B can see
class A and vice versa.
Within classes themselves, the modifier only serves as encapsulation purposes (i.e. hiding data and fields). If
a member of a class does not have the pub modifier, that field can only be access through the setter during
construction or within a neighboring method that can expose it (getter). Additionally, if the class is inheritable, it means that
field is also private from the child and the child cannot access it.
Why no
protected?In other languages like Java, the
protectedkeyword is another visibility layer that allows for only the members of the class and children of that class to view that field.Kira does not utilize this layer simply because it is extra overhead and increases the learning curve. Creating a binary system where it is either the field is visible or not makes it not only easier, but gives more freedom to the programmer in designing their APIs.
When no modifier is used to denote visibility, it implies that the field is not visible anywhere else. Tl;dr, an internal
visibility is mutually exclusive to public visibility. This is similar to other language’s private keyword.
When used within a class, it represents that the field is private and cannot be accessed outside of the class or from a child class.
Kira incorporates sound null-safety as one of its core pillars. It does so using a core type: Maybe< A >.
Without boxing a type in Maybe, you are not allowed to assign the literal null to anything:
a: Int32 = null // error!
b: Maybe<Int32> = null // ok!
Maybe also enables for field-valuation meaning you can achieve the following without using a getter for the internally
held value:
mut a: Maybe<Int32> = null
@trace(a) // null
@trace(a.value) // null
a = 32
@trace(a) // 32
@trace(a.value) // 32
Null Safe Get
Maybe< A >::unwrapOr(option: A)
If the underlying value is null, return option provided as A else return the underlying value.
mut a: Maybe<Int32> = null
if a == null {
@trace("Null")
} else {
@trace(a)
}
// is akin to:
@trace(a.unwrapOr(0))
Sanity Checks
If you do not like using == to check if the value is equal to null, there are 2 methods to help you:
Maybe< A >::isNull(): Bool - returns true if the underlying value is null else falseMaybe< A >::isSome(): Bool - returns true if the underlying value is not null else falseKira only has unchecked exceptions meaning that you do not need to catch everything. Exceptions are only strings that denote what went wrong:
throw "Something went wrong."
Exceptions can be caught by using a try-on expression:
try {
// ...
} on e: Str {
@trace(e)
}
Note: The variable required after
onis always guaranteed to be ofStr
Kira uses an Automatic Reference Counting (ARC) memory model similar to Swift. Every object has a reference counter and when this counter drops to zero, the object is freed from memory. However, in certain scenarios like a circular reference where A and B reference each other, it can cause them to never be deallocated leading to undefined behavior and/or memory faults.
To mitigate this, Kira has 2 types that are intrinsified by the compiler:
Weak< T > - Used to denote a non-strong reference to mitigate circular references and allow for deallocation.Unsafe< T > - Signifies that the reference count should not be increased as the object will outlive the reference.
This is a strictly performance oriented feature.These are special functions that are only callable by the runtime and not from Kira itself. They are meant to be executed right at the time of object allocation and deallocation and are only permitted within classes.
Initializers are ran right at the moment of object construction. It is accomplished by introducing the initially
block:
class Person {
require pub age: Int32
initially {
if age < 0 {
@trace("Age must be >= 0!")
}
}
}
Finalizers are used to clean up external resources from things like File IO. It is accomplished by introducing the
finally block:
class FileIO {
finally {
doCleanup()
}
}
All code in the finally block is called and ran when the object is about to be deallocated.
Core types are intrinsified by the compiler, meaning the compiler both recognizes them and also will optimize specifically for them.
In other languages, such as Java, they are known as primitive types and boxed types. Kira does not perform boxing, everything is an object.
Int32 - 32-bit integerInt64 - 64-bit integerInt16 - 16-bit integerInt8 - 8-bit integerFloat32 - 32-bit floating pointFloat64 - 64-bit floating pointBool - Represents a boolean which can either be true or false. Backed by an 8-bit integer.Void - Specifies no return typeNever - Specifies a function will never complete (i.e. all code after is dead code).Weak< A > - Weak ReferenceUnsafe< A > - Unsafe ReferenceMaybe< A > - Null Safety. Allows for assigning the null literal.Any - Root of all classesType - Runtime type representationFx< R, [ T: Any ] > - Function type representationModule - Module metadata, recursive for submodules.Ref< A > - Holds a reference to an object directly (i.e. boxing A)Result< A, B > - A different approach to null safety where it allows for a reason if A is null via BVal< A > - A trait that is used to denote that it can be referenced directly as A. Used by MaybeStr - String/Character SequenceArr< A > - Static immutable array structureList< A > - Dynamic mutable array structure (not-compiler-optimized)Map< K, V > - Dynamic mutable hash table structure (not-compiler-optimized)Set< A > - Dynamic mutable hash set structure (not-compiler-optimized)Kira does not support multi-inheritance as previously seen; however, in order to suffice for allowing sharing common components across classes, traits are a good alternative.
Traits allow injecting a class with functions/methods at compile time directly. Additionally, it can also serve as a way to inject abstract methods or no-implementation functions that the target class must take care of.
However, traits differ from Mixins and Interfaces in that they are an entirely compile-time feature. This means that you cannot perform runtime checks for if a type has a trait to it (however, this can be implemented by directly checking if a certain method/function exists within).
Within Kira, you are only allowed to define functions within traits, and each function also implicitly points to the current instance like in classes (i.e. there is no self or this or :: operands to access the current scope).
Mutability Note: All functions in a trait are mutable or overrideable, specifying the
mutmodifier will have no effect.Visibility Note: You are able to enforce visibility modifiers on the trait itself and also functions. This is done by using the normal modifiers. However, when a function is marked with or without a modifier, it is not able to be changed by the implementing type.
Implementer Features: The implementer class is allowed to use specific functions in order ot refer
Implementing a trait:
trait Animal {
fx makeNoise(): Void
pub fx canConsume(items: Arr<Str>): Bool {
return items.any([ "water", "air" ])
}
}
class Dog: Animal {
fx makeNoise(): Void {
@trace("Woof")
}
pub fx canConsume(items: Arr<Str>): Bool {
return
}
}
Kira supports compiler-integrated intrinsics for compile-time execution. These are not user-definable and are designed to simplify expressions, enable metaprogramming, and support DSL construction.
Properties:
@a: Map<String, Any> = @json_decode(`
{
"hello": 1,
"world": 2
}
`)
@trace(a["hello"]) // Outputs 1 to debugger
Kira supports operator overloading to make user-defined types feel more natural. Additionally, they are also the mechanic by which the
base types like Int32, Str, etc. are implemented.
To overload an operator, you must define a method/class function member with the name of intrinsic that matches the operator. For example,
to overload the addition operator +, you must define a method named op_add:
class Vector2 {
require pub mut x: Float32
require pub mut y: Float32
pub fx @op_add(other: Vector2): Vector2 {
return Vector2 { x + other.x, y + other.y }
}
}
You can also look under the hood of how this works, as the following two pieces of code are equivalent:
a: Int32 = 1
b: Int32 = 2
c: Int32 = a + b // This is syntactic sugar for:
c: Int32 = a.@op_add(b)
Overloaded operators can return anything and take anything just like a function (recall that a marker intrinsic is just a placeholder for a special symbol). This makes them powerful yet confusing if not used properly and sparingly.