galvan 0.0.0-dev03

A higher-level programming language that transpiles to Rust
Documentation

Galvan

A high-level companion language for Rust.

[!IMPORTANT] This is a work in progress and under development. It currently is in a state of a hobby project and most features described here are not yet implemented. I am working on this project in my free time - if you like the ideas presented here, and want to help, feel free to contact me or start a discussion here on GitHub.

A Tour of Galvan

Introduction to Galvan

Galvan is a modern programming language that transpiles to Rust. It provides a concise syntax while leveraging the full power of Rust's ecosystem.

Basic Syntax and String Formatting

main {
    let name = "Galvan"
    print("Welcome to {name}, the modern language!")
}

Note that Galvan strings always support inline format arguments.

Functions

Like in Rust, functions are defined with the fn keyword and return the value of the last expression:

fn add(a: Int, b: Int) -> Int {
    a + b
}

Very short functions can also be defined with = and have their return type inferred:

fn add(a: Int, b: Int) = a + b

Those functions are not allowed to have newlines in their body.

Types

Types in Galvan are defined with the type keyword.

/// A struct definition
pub type Person {
    name: String
    age: Int
}

/// A type alias
pub type Human = Person

/// A tuple type
pub type Couple = (Person, Person)

/// An enum type
pub type Theme {
    Plain
    // Like in Rust, enum variants can have associated values, either named or unnamed
    Monochrome(Color)
    Dark { background: Color, foreground: Color }
    Light { background: Color, foreground: Color }
}

Member Functions

All functions are declared top-level. If their first parameter is named self, they can be called as member functions:

pub type Dog {
    name: String
}

fn bark(self: Dog) {
    print("{self.name} barks")
}

Collections

Galvan features intuitive syntax for collections:

pub type IntArray = [Int]
pub type StringSet = {String}
pub type MyDict = {String: Int}
pub type OrderedDict = [String: Int]

Ordered types use [], unordered types use {}.

Optionals and Result Types

Galvan provides concise syntax for optionals and result types:

type OptionalInt = Int?
type FileOrErr = File!
type FileOrIoErr = File!IoError

The error variant is specified after the ! symbol. If it is not given, a flexible error type is used.

fn open_file(path: String) -> File! {
    let file = File::open(path)!
    let contents = file.read_to_string()?.find("foo")?.uppercase()
}

The ! operator unwraps the result and early returns if the result is an error. This is identical to the ? operator in Rust. The ? is the safe call operator in Galvan. The subsequent expression is only evaluated if the result is not an error and not None.

Union Types

Galvan supports union types everywhere where a type identifier is expected:

fn print_value(value: Int | String) {
    print("Value: {value}")
}

Pass-by-Value and Pass-by-Reference

By default, arguments are passed by value. If the argument needs to be mutated, the mut keyword can be used to pass it by reference: For consistency, the let keyword is allowed as well but redundant as parameters are passed by value by default.

fn add_one(mut value: Int) {
    value += 1
}

// Using `let` is not necessary here but allowed
fn incremented(let value : Int) -> Int {
    value + 1
} 

Galvan's mut value: T would be equivalent to Rust's value: &mut T. Galvan does not have immutable references, as all values are copy-on-write.

// No copy is happening here as the value is not mutated
// Arguments are passed by value by default
fn bark_at(self: Dog, other: Dog) {
    print("{self.name} barks at {other.name}")
}
// A copy is happening here as the value is mutated
fn shout_at(self: Dog, other: Dog) {
    // Redeclaring is neccessary as value parameters cannot be mutated
    let other = other
    // Copy is happening here
    other.name = other.name.uppercase()
    print("{self.name} shouts at {other.name}")
}
fn grow(mut self: Dog) {
    // This mutates the original value as it is passed by reference
    self.age += 1
}

Stored References

References that are allowed to be stored in structs have to be declared as heap references, this is done by prefixing the declaration with ref:

pub type Person {
    name: String
    age: Int
    // This is a heap reference
    ref dog: Dog
}

main {
    ref dog = Dog { name: "Bello", age: 5 }
    // The `dog` field now points to the same entity as the `dog` variable 
    let person = Person { name: "Jochen, age: 67, ref dog }
    dog.age += 1
    
    print(person.dog.age) // 6
    print(dog.age) // 6
}

Heap references use atomic reference counting to be auto-freed when no longer needed and are always mutable. In contrast to let and mut values, ref values. They follow reference semantics, meaning that they point to the same object. For this reason, they are always mutable.

Control Flow

Loops

Like in Rust, loops can yield a value:

mut i = 0
let j = loop {
    if i == 15 {
        return i
    }
    i += 1
}
print(j) // 15
print(i) // 15

For loops are also supported:

for 0..<n {
    print(it)
}

The loop variable is available via the it keyword, but can also be named explicitly using closure parameter syntax:

for 0..<n |i| {
    print(i)
}

Note that ranges are declared using ..< (exclusive upper bound) or ..= (inclusive upper bound).

If-Else
if condition {
    print("Condition is true")
} else if other_condition {
    print("Other condition is true")
} else {
    print("No condition is true")
}

Try

You can use try to unwrap a result or optional:

try potential_error {
    print("Optional was {it}")
} else {
    print("Error occured: {it}")
}

The unwrapped variant is available via the it keyword, like in closures. You can also name it using closure parameter syntax to declare them explicitly:

try potential_error |value| {
    print("Optional was {value}")
} else |error| {
    print("Error occured: {error}")
}

Return and Throw

Return values are implicit, however you can use the return keyword to return early:

fn fib(n: Int) -> Int {
    if n <= 1 {
        return n
    }
    fib(n - 1) + fib(n - 2)
}

Returning an error early is done using the throw keyword:

fn checked_divide(a: Float, b: Float) -> Float! {
    if b == 0 {
        throw "Division by zero"
    }
    a / b
}

Generics

In Galvan, type identifiers are always starting with an upper case letter. Using a lower case letter instead introduces a type parameter:

type Container {
    value: t
}

fn get_value(self: Container<t>) -> t {
    self.value
}

Bounds can be specified using the where keyword:

fn concat_hash(self: t, other: t) -> t where t: Hash {
    self.hash() ++ other.hash()
}

Operators

Arithmetic operators:

  • +: Addition
  • -: Subtraction
  • *: Multiplication
  • /: Division
  • %: Remainder
  • ^: Exponentiation

Logical operators:

  • and, &&: Logical and
  • or, ||: Logical or
  • not, !: Logical not

Bitwise operators are prefixed with b:

  • b|: Bitwise or
  • b&: Bitwise and
  • b^: Bitwise xor
  • b<<: Bitwise left shift
  • b>>: Bitwise right shift
  • b~: Bitwise not

Comparison operators:

  • ==: Equality
  • !=, : Inequality
  • <: Less than
  • <=, : Less than or equal
  • > Greater than
  • >=, :: Greater than or equal
  • ===: Pointer equality, only works for heap references
  • !==: Pointer inequality, only works for heap references

Collection operators:

  • ++: Concatenation
  • --: Removal
  • []: Indexing
  • [:]: Slicing
  • in: Membership

Unicode and Custom Operators

Galvan supports Unicode and custom operators:

@infix("⨁")
fn custom_add(lhs: n, rhs: n) = lhs + rhs

@prefix("√")
fn sqrt(n: Float) = n.sqrt()

main {
    let sum = 5 ⨁ 10
    let value = √16.0
}

This section defines custom infix and prefix operators. Note that no whitespace is allowed between a prefix operator and the operands. Infix operators have to be surrounded by whitespace.

Closures

Closures are defined using the parameter list syntax:

let add = |a, b| a + b

Closure types use the arrow syntax:

fn map(self: [t], f: t -> u) -> [u] {
    mut result = []
    for self {
        result.push(f(it))
    }
    result
}

Functions with trailing closures are allowed to omit the parameter list and the () around the parameter list:

iter
    .map { it * 2 }
    // Trailing closures with only one parameter can use the it keyword instead of naming it explicitly
    .filter { it % 2 == 0 }
    // The parameter list before the trailing closure can be omitted
    .reduce start |acc, e| { acc + e }

Trailing closures can also use numbered parameters instead of giving a parameter list

iter
    .map { #0 * 2 }
    .filter { #0 % 2 == 0 }
    .reduce start { #0 + #1 }