Simple syntax, native performance, transparent Rust FFI.
An indentation-sensitive programming language with an LLVM backend. OtterLang compiles to native binaries with a focus on simplicity and performance.
git clone https://github.com/jonathanmagambo/otterlang.git
cd otterlang
# Using Nix (recommended)
nix develop
cargo +nightly build --release
# Create and run your first program
cat > hello.ot << 'EOF'
use otter:fmt
fn main():
fmt.println("Hello from OtterLang!")
EOF
otter run hello.otnix develop
cargo +nightly build --releaseThe Nix flake automatically provides Rust nightly, LLVM 18, and all dependencies.
Prerequisites:
- Rust (via rustup) - nightly required for FFI features
- LLVM 18
macOS:
brew install llvm@18
export LLVM_SYS_181_PREFIX=$(brew --prefix llvm@18)
export LLVM_SYS_180_PREFIX=$LLVM_SYS_181_PREFIX
export PATH="$LLVM_SYS_181_PREFIX/bin:$PATH"
rustup toolchain install nightly
cargo +nightly build --releaseUbuntu/Debian:
sudo apt-get install -y llvm-18 llvm-18-dev clang-18
export LLVM_SYS_181_PREFIX=/usr/lib/llvm-18
export LLVM_SYS_180_PREFIX=$LLVM_SYS_181_PREFIX
rustup toolchain install nightly
cargo +nightly build --releaseWindows:
# Install LLVM 18.1 using llvmenv (recommended)
cargo install llvmenv --locked
llvmenv install 18.1
llvmenv global 18.1
# Set environment variables
$llvmPath = llvmenv prefix
$env:LLVM_SYS_181_PREFIX = $llvmPath
$env:LLVM_SYS_180_PREFIX = $llvmPath
$env:Path = "$llvmPath\bin;$env:Path"
# Alternative: Install using winget or Chocolatey
# winget install --id LLVM.LLVM --silent --accept-package-agreements --accept-source-agreements
# choco install llvm -y
# $env:LLVM_SYS_181_PREFIX = "C:\Program Files\LLVM"
# $env:LLVM_SYS_180_PREFIX = $env:LLVM_SYS_181_PREFIX
# $env:Path = "$env:LLVM_SYS_181_PREFIX\bin;$env:Path"
# Install Rust nightly
rustup toolchain install nightly
rustup default nightly
# Build
cargo +nightly build --releaseNote for Windows: If using winget/Chocolatey, LLVM may be installed in C:\Program Files\LLVM or C:\Program Files (x86)\LLVM.
Important: On Windows, you must use the x64 Native Tools Command Prompt for VS 2022 to build. The MSVC linker requires environment variables that are automatically set in the Developer Command Prompt. Open it from the Start menu, then navigate to your project directory and run the build commands. Regular PowerShell/CMD will not have the MSVC environment configured.
Once the build completes successfully, you can:
Run a program:
cargo +nightly run --release --bin otterlang -- run examples/basic/hello.otBuild an executable:
cargo +nightly run --release --bin otterlang -- build examples/basic/hello.ot -o helloRun tests:
cargo +nightly test --releaseUse the compiler directly:
# The binary is located at:
# target/release/otterlang (or target/release/otterlang.exe on Windows)
./target/release/otterlang run program.ot
# Or on Windows:
# target\release\otterlang.exe run program.otClean indentation-based syntax with modern features:
use otter:fmt
use otter:math
fn greet(name: str) -> str:
return "Hello, " + name + "!"
struct Point:
x: float
y: float
fn distance(self) -> float:
return math.sqrt(self.x * self.x + self.y * self.y)
fn main():
let message = greet("World")
fmt.println(message)
let p = Point(x=3.0, y=4.0)
let dist = p.distance()
fmt.println("Point: (" + stringify(p.x) + ", " + stringify(p.y) + "), distance: " + stringify(dist))
if len(message) > 10:
fmt.println("Long message")
for i in 0..10:
fmt.println(stringify(i))
Automatically use any Rust crate without manual configuration:
use rust:rand
use otter:fmt
fn main():
let random = rand.random_f64()
fmt.println("Random: " + stringify(random))
Key advantages:
- No manual bindings needed
- Automatic API extraction via rustdoc (requires Rust nightly)
- Memory management handled automatically
- Async/await support for Rust Futures
- Type checking integrated
See docs/FFI_TRANSPARENT.md for details.
Built-in modules:
otter:math- Mathematical functionsotter:io- File I/Ootter:time- Time utilitiesotter:task- Task-based concurrencyotter:rand- Random numbersotter:json- JSON parsingotter:net- Networkingotter:http- HTTP client/server
Modern exception handling with zero-cost success path:
use otter:fmt
fn divide(x: int, y: int) -> int:
if y == 0:
raise "Division by zero"
return x / y
fn safe_operation():
try:
let result = divide(10, 0)
fmt.println("Result: " + stringify(result))
except Error as e:
fmt.println("Caught error: " + stringify(e))
else:
fmt.println("No errors occurred")
finally:
fmt.println("Cleanup always runs")
fn nested_exceptions():
try:
try:
raise "Inner error"
except Error:
fmt.println("Handled inner error")
raise "Outer error"
except Error:
fmt.println("Handled outer error")
Features:
try/except/else/finallyblocks- Exception propagation with automatic cleanup
- Zero-cost abstractions (no overhead on success path)
- Type-safe error handling at compile time
Note: Benchmarks are currently being retested and properly specified. Comprehensive performance metrics will be available in a future update. OtterLang compiles to native code via LLVM and is designed for high performance, with automatic memory management and zero-cost abstractions.
otterlang run program.ot # Run program
otterlang build program.ot -o out # Build executable
otterlang build program.ot --target wasm32-unknown-unknown -o out.wasm # Build to WebAssembly
otterlang fmt # Format code
otterlang repl # Start REPL
otterlang profile memory program.ot # Profile memoryOtterLang can compile to WebAssembly! Use the --target flag:
# Compile to WebAssembly (wasm32-unknown-unknown)
otterlang build program.ot --target wasm32-unknown-unknown -o program.wasm
# Compile to WebAssembly System Interface (wasm32-wasi)
otterlang build program.ot --target wasm32-wasi -o program.wasmRequirements:
- LLVM 18 with WebAssembly target support
clangandwasm-ldin your PATH (usually included with LLVM)
When targeting wasm32-wasi the generated binary can talk directly to WASI's
stdio and wall-clock APIs. For the more barebones wasm32-unknown-unknown
target we import a minimal host surface so you can decide how to surface
output:
env.otter_write_stdout(ptr: i32, len: i32)– write UTF-8 data to stdoutenv.otter_write_stderr(ptr: i32, len: i32)– write UTF-8 data to stderrenv.otter_time_now_ms() -> i64– optional wall-clock timestamp in ms
A tiny JavaScript host that wires these up under Node.js looks like:
import fs from 'node:fs';
const memory = new WebAssembly.Memory({ initial: 8 });
const decoder = new TextDecoder();
const env = {
memory,
otter_write_stdout(ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len);
process.stdout.write(decoder.decode(bytes));
},
otter_write_stderr(ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len);
process.stderr.write(decoder.decode(bytes));
},
otter_time_now_ms() {
return BigInt(Date.now());
},
};
const { instance } = await WebAssembly.instantiate(fs.readFileSync('program.wasm'), { env });
instance.exports.main?.();The generated .wasm file can be run in any WebAssembly runtime (Node.js, browsers, wasmtime, etc.).
Basic Programs:
examples/basic/hello.ot- Hello worldexamples/basic/exception_basics.ot- Exception handling basicsexamples/basic/exception_advanced.ot- Advanced exceptionsexamples/basic/exception_resource.ot- Resource managementexamples/basic/exception_validation.ot- Data validationexamples/basic/struct_methods_demo.ot- Struct methodsexamples/basic/struct_demo.ot- Struct usageexamples/basic/advanced_pipeline.ot- Complex computationexamples/basic/task_benchmark.ot- Task benchmarksexamples/basic/fibonacci.ot- Fibonacci sequenceexamples/basic/pythonic_demo.ot- Pythonic styleexamples/basic/multiline_test.ot- Multi-line strings
FFI Examples:
examples/ffi/ffi_rand_demo.ot- Random number generationexamples/ffi/ffi_rand_advanced.ot- Advanced FFI usage
Below are illustrative examples of how unit tests will look in OtterLang. A built-in otter test runner is planned (see roadmap.md).
use otter:fmt
struct User:
id: int
name: str
fn make_users() -> list<User>:
return [User(id=1, name="Ana"), User(id=2, name="Bo")]
fn to_map(users: list<User>) -> dict<int, str>:
let m = { }
for u in users:
m[u.id] = u.name
return m
fn test_user_list_basic():
let xs = make_users()
assert len(xs) == 2
assert xs[0].name == "Ana"
fn test_dict_building():
let m = to_map(make_users())
assert m[1] == "Ana"
assert m.get(3, default="none") == "none"
fn test_nested_structs_and_lists():
struct Team:
name: str
members: list<User>
let team = Team(name="core", members=make_users())
assert team.members[1].id == 2
The test runner will discover functions prefixed with test_ and report pass/fail results with spans and diffs.
Early Access (v0.1.0) - Experimental, not production-ready.
- Type inference is limited (explicit types recommended)
- Module system has some limitations
- Requires LLVM 18 and Rust nightly (for FFI features)
Contributions welcome! See CONTRIBUTING.md.
MIT License - see LICENSE.