Sindarin is a statically-typed procedural programming language that compiles to C. It features clean arrow-based syntax, powerful string interpolation, and built-in array operations.
.sn source → Sn Compiler → C code → GCC → executable
-
Explicit Over Implicit - All types are explicitly annotated. No type inference means code is always clear about what types are being used.
-
Safety First - Panic on errors rather than returning null or error codes. This keeps code clean and avoids pervasive null checks.
-
Simple Memory Model - Arena-based memory management with clear ownership semantics. No manual malloc/free, no garbage collector pauses.
-
Clean Syntax - Arrow-based blocks (
=>) provide consistent, readable structure. No curly braces for blocks. -
Batteries Included - Built-in string methods, array operations, and file I/O. Common tasks don't require external libraries.
-
C Interoperability - Compiles to readable C code. Easy to integrate with existing C libraries and tools.
- Learning: Clear syntax and explicit types make it easy to understand what code does
- Scripting: Built-in file I/O and string processing for automation tasks
- Performance: Compiles to native code via C with no runtime overhead
- Simplicity: Small language with consistent rules, easy to master
Sindarin uses => to introduce code blocks instead of curly braces:
fn greet(name: str): void =>
print($"Hello, {name}!\n")
if condition =>
doSomething()
else =>
doOtherThing()
while running =>
processNext()
Variables are declared with var and require type annotations:
var name: str = "Sindarin"
var count: int = 42
var pi: double = 3.14159
var active: bool = true
var letter: char = 'S'
Functions use the fn keyword with explicit parameter and return types:
fn add(a: int, b: int): int =>
return a + b
fn factorial(n: int): int =>
if n <= 1 =>
return 1
return n * factorial(n - 1)
For simple functions that return a single expression, use the expression-bodied syntax:
fn add(a: int, b: int): int => a + b
fn square(x: int): int => x * x
fn greet(name: str): str => $"Hello, {name}!"
fn isEven(n: int): bool => n % 2 == 0
The expression after => is implicitly returned. This is equivalent to:
fn add(a: int, b: int): int =>
return a + b
Expression-bodied syntax works with all function types including native functions:
native fn double_it(x: int): int => x * 2
Embed expressions in strings with $"..." syntax:
var name: str = "World"
var count: int = 42
print($"Hello, {name}! Count is {count}.\n")
Arrays use curly braces for literals and have built-in methods:
var numbers: int[] = {1, 2, 3, 4, 5}
numbers.push(6)
var first: int = numbers[0]
var last: int = numbers[-1]
var slice: int[] = numbers[1..4]
Structs group related data with named fields:
struct Point =>
x: double
y: double
struct Config =>
timeout: int = 30
enabled: bool = true
var p: Point = Point { x: 10.0, y: 20.0 }
var cfg: Config = Config { timeout: 60 } // enabled uses default
// If-else
if condition =>
doSomething()
else =>
doOtherThing()
// While loop
while i < 10 =>
process(i)
i = i + 1
// For loop
for var i: int = 0; i < 10; i++ =>
print($"{i}\n")
// For-each loop
for item in items =>
process(item)
if hasTicket && hasID =>
print("Entry allowed\n")
if isAdmin || isModerator =>
print("Can moderate\n")
if !isBlocked =>
print("Access granted\n")
fn main(): void =>
print("Hello, World!\n")
fn main(): void =>
for var i: int = 1; i <= 100; i++ =>
if i % 15 == 0 =>
print("FizzBuzz\n")
else if i % 3 == 0 =>
print("Fizz\n")
else if i % 5 == 0 =>
print("Buzz\n")
else =>
print($"{i}\n")
fn is_prime(n: int): bool =>
if n <= 1 =>
return false
var i: int = 2
while i * i <= n =>
if n % i == 0 =>
return false
i = i + 1
return true
fn find_primes(limit: int): int[] =>
var primes: int[] = {}
for var n: int = 2; n <= limit; n++ =>
if is_prime(n) =>
primes.push(n)
return primes
fn main(): void =>
var primes: int[] = find_primes(50)
print($"Found {primes.length} primes: {primes.join(\", \")}\n")
fn main(): void =>
// Read file and count lines
var content: str = TextFile.readAll("data.txt")
var lines: str[] = content.splitLines()
var nonEmpty: int = 0
for line in lines =>
if !line.isBlank() =>
nonEmpty = nonEmpty + 1
print($"Total lines: {lines.length}\n")
print($"Non-empty lines: {nonEmpty}\n")
fn main(): void =>
var text: str = " Hello, World! "
// Method chaining
var cleaned: str = text.trim().toLower()
print($"Cleaned: '{cleaned}'\n")
// Splitting and joining
var words: str[] = "apple,banana,cherry".split(",")
var sentence: str = words.join(" and ")
print($"Fruits: {sentence}\n")
Split code across files with imports:
// utils.sn
fn helper(): void =>
print("I'm a helper!\n")
// main.sn
import "utils"
fn main(): void =>
helper()
Sindarin uses arena-based memory with optional control:
// Shared function - uses caller's arena
fn helper(a: int, b: int) shared: int =>
return a + b
// Private block - isolated arena, freed on exit
private =>
var temp: int[] = {1, 2, 3}
// temp freed here
// Value copy semantics
var original: int[] = {1, 2, 3}
var copy: int[] as val = original // Independent copy
See Memory for full documentation.
See Building for instructions on building the compiler from source.
# Compile to executable
bin/sn source.sn -o program
./program
# Emit C code only
bin/sn source.sn --emit-c -o output.c
# Debug build with symbols
bin/sn source.sn -g -o programThe C compiler backend can be configured via environment variables:
| Variable | Purpose | Default |
|---|---|---|
SN_CC |
C compiler command | gcc |
SN_STD |
C standard | c99 |
SN_DEBUG_CFLAGS |
Debug mode flags | -no-pie -fsanitize=address -fno-omit-frame-pointer -g |
SN_RELEASE_CFLAGS |
Release mode flags | -O3 -flto |
SN_CFLAGS |
Additional compiler flags | (empty) |
SN_LDFLAGS |
Additional linker flags | (empty) |
SN_LDLIBS |
Additional libraries | (empty) |
Examples:
# Use clang instead of gcc (requires runtime rebuilt without GCC LTO)
SN_CC=clang bin/sn source.sn -o program
# Add extra compiler flags
SN_CFLAGS="-march=native" bin/sn source.sn -o program
# Disable sanitizers in debug mode
SN_DEBUG_CFLAGS="-g" bin/sn source.sn -g -o program
# Link additional libraries
SN_LDLIBS="-lssl -lcrypto" bin/sn source.sn -o programNote: The default runtime objects are compiled with GCC's LTO. To use a different compiler like clang, you may need to rebuild the runtime without LTO or adjust SN_RELEASE_CFLAGS.
- Building - Build instructions for Linux, macOS, Windows
- Strings - String methods and interpolation
- Arrays - Array operations and slicing
- Structs - Struct declarations and C interop
- Lambdas - Lambda expressions and closures
- Memory - Arena memory management
- Threading - Threading with
&spawn and!sync - Namespaces - Namespaced imports for collision resolution
- Interop - C interoperability and native functions
- Interceptors - Function interception for debugging and mocking
See the SDK documentation for built-in modules including Date, Time, File I/O, Networking, Random, UUID, Environment, and Process operations.