Skip to content

natty-light/sydney

Repository files navigation

Sydney - A language that's the way I want it

Sydney is a compiled, statically-typed programming language. It compiles to bytecode and runs on a custom VM, or compiles to native binaries via LLVM IR.

Installation

Prerequisites

  • Go (1.21+) — builds the Sydney compiler
  • Rust/Cargo — builds the runtime library (required for native compilation)
  • LLVM toolchain (llc, clang) — assembles and links native binaries
  • OpenSSL (libssl, libcrypto) — required for TLS support in native binaries
  • just (optional) — command runner for convenience recipes
brew install go rust llvm openssl just

Building from source

# Clone the repository
git clone https://github.com/natty-light/sydney.git
cd sydney

# Build the compiler (Go)
go build -o sydney

# Build the runtime library (Rust, needed for native compilation)
cd sydney_rt && cargo build --release && cd ..

# Or build both at once with just:
just build

Verify the installation

# Run a program on the VM
./sydney run examples/hello.sy

# Start the REPL
./sydney

# Compile to a native binary
./sydney compile examples/hello.sy
llc -filetype=obj hello.ll -o hello.o
clang hello.o -Lsydney_rt/target/release -lsydney_rt -lssl -lcrypto -o hello
./hello

Running tests

# Test the compiler (Go tests)
go test ./...

# Test Sydney programs
./sydney test stdlib/    # run stdlib test suites

Values, literals, and types

Primitive types

Sydney supports the following primitive types: int, float, string, bool, byte, null, and any.

mut int i = 10;
mut float f = 10.5;
mut string s = "hello";
mut bool b = true;
mut byte c = 'a';
mut null n = null;
const any x = 42;

The any type can hold a value of any type and is used with type match expressions for dynamic dispatch.

Strings support escape sequences: \n, \t, \r, \\, \", \', and \0. Byte literals support the same escapes:

const msg = "hello\tworld\n";
const newline = '\n';

Type conversions

Convert between primitive types using the int(), float(), byte(), and char() builtins:

const code = int('a');       // 97
const b = byte(65);          // byte with value 65
const ch = char(byte(72));   // "H"
const f = float(42);         // 42.0

Variables

A variable can either be mutable or constant. This is specified with the mut and const keywords.

Variable type annotations are optional for initialized variables:

mut x = 5;
const y = 5;

Constant variables must be initialized, mutable variables do not.

const x = 5;
mut int y;

Functions

There are two ways of creating functions. Anonymous functions are treated as values, and can be assigned to variables:

const add = func(int a, int b) -> int {
    a + b;
};

Functions can also be declared in the traditional sense:

func add(int a, int b) -> int {
    a + b;
}

The last expression in a function body is implicitly returned, or return can be used explicitly.

Closures are supported:

const f = func(int x) -> fn<() -> int> {
    return func() -> int {
        return x;
    };
};

const store5 = f(5);
store5(); // returns 5;

Extern functions

Functions implemented in the runtime can be declared with extern:

pub extern func args() -> array<string>;

Extern functions have no body — they are resolved at link time against the runtime library or built-in function table.

Maps

Maps are dictionaries with strict typings. The keys of a map must all be of the same type, as with the values. Map values can be updated by key with square bracket index notation.

Accessing a map by key returns option<T>, since the key may not exist:

const map<string, int> m = { "hello": 0, "world": 1 };
m["hello"] = 2;

const val = match m["hello"] {
    some(v) -> { v; },
    none -> { 0; },
};

Arrays

Arrays also have strict types. The values must be homogenous. Square bracket index notation is again used to access and update by index. Indices start at 0.

const array<int> a = [1, 2, 3];
a[0]; // 1
a[2] = 1;
a; // [1, 2, 1];

const b = append(a, 4); // [1, 2, 1, 4]

Slicing

Arrays and strings support slice syntax for extracting subsequences:

const a = [1, 2, 3, 4, 5];
a[1:3]; // [2, 3]
a[2:];  // [3, 4, 5]
a[:3];  // [1, 2, 3]

const s = "hello";
s[1:4]; // "ell"

Structs

Structs allow for the creation of custom data types with named fields. They must be defined before they can be used.

define struct Point {
    x int,
    y int
}

const Point p = Point { x: 0, y: 0 };
p.x = 10;
p.x; // 10

Structs can be nested and passed as arguments to functions:

define struct Circle {
    center Point,
    radius int
}

func isOrigin(Point p) -> bool {
    return p.x == 0 && p.y == 0;
}

const Circle c = Circle { center: Point { x: 0, y: 0 }, radius: 5 };
isOrigin(c.center); // true

Functions that take a struct as their first argument can be called as methods on that struct using dot syntax:

func sum(Point p) -> int {
    return p.x + p.y;
}

const Point p = Point { x: 3, y: 4 };
sum(p);     // these two calls
p.sum();    // are equivalent

Generics

Functions and structs can be parameterized with type variables:

func identity<T>(T val) -> T {
    return val;
}

identity<int>(42);       // 42
identity<string>("hi");  // "hi"

Generic structs:

define struct Box<T> {
    value T
}

const b = Box<int> { value: 42 };
b.value; // 42

Generic types are monomorphized at compile time — the compiler generates specialized versions for each concrete type used.

Control flow

If-else expressions

if-else blocks are expressions and produce a value, allowing:

const int bit = if (true) { 1 } else { 0 };

They can also be used as statements:

if (x > 0) {
    print("positive");
} else {
    print("non-positive");
}

For loops

for loops support both a condition-only form and a three-part form with init, condition, and post:

for (mut i = 0; i < 10; i = i + 1) {
    print(i);
}

mut x = 0;
for (x < 10) {
    x = x + 1;
}

Loop variables declared in the init clause are scoped to the loop.

For-in loops

for-in iterates over arrays and maps:

const nums = [1, 2, 3];
for (n in nums) {
    print(n);
}

const m = {"a": 1, "b": 2};
for (k, v in m) {
    print(k, v);
}

An optional index variable can be added when iterating arrays:

const nums = [10, 20, 30];
for (i, n in nums) {
    print(i, n); // 0 10, 1 20, 2 30
}

Break and continue

break exits a loop early. continue skips to the next iteration:

for (mut i = 0; i < 10; i = i + 1) {
    if (i == 5) { break; }
    if (i % 2 == 0) { continue; }
    print(i);
}

Match expressions

match is used to deconstruct result and option types. It is exhaustive — all arms must be provided:

const answer = match divide(10, 2) {
    ok(val) -> { val; },
    err(msg) -> { 0; },
};

const m = { "key": 42 };
const val = match m["key"] {
    some(v) -> { v; },
    none -> { 0; },
};

Type match expressions

match can also dispatch on the runtime type of an any value. Each arm binds the unwrapped value to a variable:

const any val = 42;
const desc = match val {
    int(i) -> { "an integer"; },
    float(f) -> { "a float"; },
    string(s) -> { "a string"; },
    bool(b) -> { "a bool"; },
    byte(b) -> { "a byte"; },
    _ -> { "something else"; },
};

Arms can match on primitive types (int, float, string, bool, byte) as well as interfaces. The _ arm is a catch-all default.

Result type

The result<T> type represents a value that may be an error. Construct with ok(val) or err(msg), deconstruct with match:

func safeParse(string s) -> result<int> {
    return err("not implemented");
}

Option type

The option<T> type represents a value that may or may not exist. Construct with some(val) or none, deconstruct with match:

const opt = some(42);
const val = match opt {
    some(v) -> { v; },
    none -> { 0; },
};

Map access returns option<T>:

const map<string, int> m = { "a": 1 };
match m["b"] {
    some(v) -> { print(v); },
    none -> { print("not found"); },
};

Concurrency

Sydney supports concurrency with fibers and channels. On the VM, fibers are cooperatively scheduled lightweight threads that yield at channel operations. When compiled to native, fibers map to OS threads with channels backed by std::sync::mpsc.

Spawning fibers

Use spawn to run a function concurrently:

spawn func() {
    print("running in a fiber");
}();

Channels

Channels are typed conduits for communication between fibers. Create them with chan<T>() for unbuffered or chan<T>(n) for buffered:

const ch = chan<int>();      // unbuffered
const bch = chan<int>(5);    // buffered, capacity 5

Send with <- and receive with <-:

ch <- 42;           // send
const val = <- ch;  // receive

Example

const ch = chan<int>(1);

spawn func() {
    ch <- 10;
    ch <- 20;
    ch <- 30;
}();

spawn func() {
    const a = <- ch;
    const b = <- ch;
    const c = <- ch;
    print(a, b, c);
}();

Unbuffered channels synchronize sender and receiver — a send blocks until a receiver is ready, and vice versa. Buffered channels allow sends to proceed without blocking until the buffer is full.

Operators

Sydney supports standard arithmetic, comparison, and logical operators.

Arithmetic

  • +: Addition (and string concatenation)
  • -: Subtraction
  • *: Multiplication
  • /: Division
  • %: Modulo

Comparison

  • ==: Equal to
  • !=: Not equal to
  • <: Less than
  • >: Greater than
  • <=: Less than or equal to
  • >=: Greater than or equal to

Logical

  • &&: Logical AND
  • ||: Logical OR
  • !: Logical NOT

Built-in Functions

Sydney provides several built-in functions:

  • len(iterable): Returns the length of an array, string, or map.
  • print(args...): Prints the provided arguments to the console.
  • append(array, element): Returns a new array with the element appended.
  • keys(map): Returns an array of the map's keys.
  • values(map): Returns an array of the map's values.
  • panic(msg): Terminates execution with an error message.
  • int(byte): Converts a byte to an integer.
  • float(int): Converts an integer to a float.
  • byte(int): Converts an integer to a byte.
  • char(byte): Converts a byte to a single-character string.

File I/O

  • fopen(path): Opens a file and returns a file descriptor.
  • fcreate(path): Creates a new file and returns a file descriptor.
  • fread(fd): Reads the entire contents of a file.
  • freadn(fd, n): Reads up to n bytes from a file.
  • nb_freadn(fd, n): Non-blocking read of up to n bytes.
  • fwrite(fd, data): Writes data to a file.
  • fclose(fd): Closes a file descriptor.

Modules

Sydney supports a module system for organizing code across files. Modules are defined with the module keyword and imported with import. Public functions are exported with pub:

// strings.sy
module "strings"

pub func repeat(string str, int count) -> string {
    mut string r = "";
    for (mut i = 0; i < count; i = i + 1) {
        r = r + str;
    }
    return r;
}
// main.sy
import "strings"

print(strings:repeat("ha", 3)); // "hahaha"

Module functions are accessed with the : scope operator.

Standard library

Sydney ships with standard library modules in stdlib/:

  • strings — string manipulation (repeat, contains, split, join, index_of, trim, etc.)
  • conv — type conversions (itoa, atoi, atof, ftoa, bool_to_str, parse_bool)
  • math — mathematical functions (abs, sqrt, pow_int, exp, ln, factorial, min, max)
  • stats — statistical functions (mean, median)
  • sort — sorting algorithms (quicksort via Sortable interface)
  • heap — min-heap data structure
  • slice — generic sortable collection interface and Slice<V> wrapper
  • io — file I/O wrappers with result types
  • net — TCP networking (connect, listen, accept, read, write, TLS support)
  • http — HTTP client and server with a simple router
  • json — JSON parsing and marshaling utilities
  • fmt — string formatting (sprintf, printf, println with format specifiers)
  • os — operating system utilities (args)
  • vec — 2D and 3D vector math (dot product, cross product, magnitude)
  • term — terminal control (raw mode, reset)
  • testing — test assertion utilities (assert, assert_eq)

Interfaces and Implementations

Sydney supports interfaces, which allow for polymorphism and dynamic dispatch. An interface defines a set of method signatures that a struct can implement.

Defining an Interface

An interface defines a set of method signatures. It is defined using the define interface keywords.

define interface Area {
    area() -> float
}

Implementing an Interface

Structs define implementations implicitly, provided all functions from the interface are present

define struct Circle {
    radius float
}

define struct Rect {
    w float,
    h float
}

func area(Circle c) -> float {
    const pi = 3.14;
    return c.radius * c.radius * pi;
}

func area(Rect r) -> float {
    return r.w * r.h;
}

Polymorphism and Dynamic Dispatch

Interfaces can be used as parameter types in functions. This allows for polymorphism, where the same function can operate on different types that implement the same interface.

func printArea(Area a) {
    print(a.area());
}

const c = Circle { radius: 5.0 };
const r = Rect { w: 10.0, h: 2.0 };

printArea(c); // Works with Circle
printArea(r); // Works with Rect

When a concrete struct is passed to a function expecting an interface, Sydney "boxes" the struct into an interface object. This object contains the original struct value and a method table (itab) that allows the VM to perform dynamic dispatch—finding and calling the correct method at runtime even when the concrete type is hidden behind the interface.

The type checker verifies that all required methods are implemented with matching signatures before allowing an implementation to be defined.

Macros

Sydney supports a macro system that allows for code generation and transformation. Macros are defined using the macro keyword and can take arguments.

const ifelse = macro(condition, consequence, alternative) {
    quote(if (unquote(condition)) {
        unquote(consequence);
    } else {
        unquote(alternative);
    });
};

ifelse(10 > 5, print("true"), print("false"));

The quote and unquote functions are used within macros to manipulate AST nodes. quote returns the AST of its argument, and unquote evaluates an expression and inserts the resulting AST into a quoted block.

Annotations

#[derive(json)]

Annotating a struct with #[derive(json)] automatically generates two functions for that struct:

  • unmarshal_json_<Name>(string) -> result<Name> — parses JSON into the struct
  • marshal_json_<Name>(Name) -> string — serializes the struct to JSON
#[derive(json)]
define struct Point {
    x int,
    y int
}

const p = match unmarshal_json_Point("{\"x\":1,\"y\":2}") {
    ok(v) -> { v; },
    err(msg) -> { Point { x: 0, y: 0 }; },
};

print(marshal_json_Point(p)); // {"x":1,"y":2}

Supported field types: int, float, string, bool, arrays of primitives, arrays of structs, nested structs. The json and conv stdlib modules are automatically imported when #[derive(json)] is used.

Compilation

Sydney has two compilation targets:

VM (bytecode)

./sydney run file.sy

Compiles to bytecode and executes on a stack-based virtual machine with a cooperative fiber scheduler.

Native (LLVM IR)

./sydney compile file.sy    # emits file.ll
llc -filetype=obj file.ll -o file.o
clang file.o -Lsydney_rt/target/release -lsydney_rt -o file
./file

Compiles to LLVM IR, then assembles and links against a Rust runtime library that provides garbage collection, string operations, print functions, and thread-based concurrency.

REPL

./sydney

Running without arguments starts an interactive REPL backed by the VM.

Version

./sydney version

Testing Sydney programs

./sydney test [directory]

Runs all *_test.sy files in the given directory (or current directory). Test files define functions prefixed with test_; the runner compiles and executes each one independently, reporting PASS/FAIL with a summary. Test files use the testing stdlib module for assertions.

Debug flags

The following flags work with run and compile:

  • --dump-ast — print the AST after parsing
  • --dump-types — print the AST after type checking

Building

go build -o sydney                              # build the compiler
cd sydney_rt && cargo build --release            # build the runtime

Testing the compiler

go test ./...

Emitter end-to-end tests require llc and clang (LLVM toolchain) to be available on the path.

About

Sydney Compiler

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages