A high-level application language with concise syntax, value-oriented defaults, and explicit escape hatches when shared mutable state is needed.
Important
Galvan is a work in progress and still a hobby project. The core examples in this README reflect implemented behavior unless a section has an explicit implementation warning. If you like the ideas here and want to help, feel free to contact me or start a discussion on GitHub.
Note
Galvan is a companion language that transpiles to Rust. It is not intended to replace Rust for low-level systems work; it is aimed at application code such as CLI tools, services, and other high-level programs that still benefit from the Rust ecosystem.
Galvan focuses on the parts of day-to-day application programming where a language can provide strong defaults: concise declarations, ergonomic collections, predictable ownership behavior, and integrated tooling for tests and command-line interfaces.
Galvan is not intended for low-level programming. You should not use it to write a parser, compiler, allocator, or audio compression library. It is designed for code where readability and fast iteration matter more than controlling every implementation detail.
Note
Rust is excellent when explicit control over memory, lifetimes, and low-level performance is the central requirement. Galvan keeps Rust interoperability as a goal while moving common application patterns into simpler syntax.
Galvan programs use a regular main function:
fn main() {
let name = "Galvan"
print("Welcome to \(name)!")
}
The main function can optionally receive the process argument vector. The first element is the executable name:
fn main(args: [String]) {
print args
}
Strings support inline interpolation:
let count = 3
print("Packed \(count) orders")
Interpolation can contain member access and expressions. Strings also support escaped quotes, literal braces, and Unicode escapes:
let dog = Dog(name: "Milo")
print("Hi, \(dog.name)!")
print("3 + 7 = \(3 + 7)")
print("{\(3 + 7)}")
print("\u{1F600}")
Character literals use single quotes and support common escape sequences:
let first = 'a'
let newline = '\n'
let quote = '\''
let greeting = "hello" ++ '!'
Functions are declared with fn. A function returns the value of its final
expression unless it returns early:
fn add(a: Int, b: Int) -> Int {
a + b
}
Types are declared with the type keyword:
pub type Color {
r: Int
g: Int
b: Int
}
pub type Person(name: String, age: Int)
pub type Human = Person
pub type Couple(Person, Person)
Enums use the same type keyword. Variants can have no values, tuple-like
values, or named values:
pub type Theme(name: String) {
Plain
Monochrome(Color)
Dark(background: Color, foreground: Color)
Light(background: Color, foreground: Color)
}
The name field in Theme(name: String) is common data shared by all variants.
Variant-specific fields are declared on each case.
Enum variants are constructed and referenced with Type::Variant syntax:
pub type ColorChoice {
Transparent
Gray(U8)
Rgb(r: U8, g: U8, b: U8)
}
let transparent = ColorChoice::Transparent
let gray = ColorChoice::Gray(128)
let rgb = ColorChoice::Rgb(r: 100, g: 10, b: 150)
Struct fields can provide default values:
type Book {
title: String = "Field Notes"
content: String = "No notes yet"
}
fn main() {
let book = Book()
}
Note
When a type can be constructed without arguments, Galvan automatically emits a
Rust Default implementation for the generated type.
All functions are declared top-level. If the first parameter is named self,
the function can be called as a member function:
pub type Dog { name: String }
fn bark(self: Dog) {
print("\(self.name) barks")
}
fn main() {
let dog = Dog(name: "Milo")
dog.bark()
}
Items in the same crate are available unqualified. Items from other crates can
be imported with use mycrate, or more narrowly with path syntax such as
use mycrate::my_item.
Note
use mycrate imports all public items from that crate, similar to
use mycrate::* in Rust.
Galvan also allows methods to be added to types you do not own. Outside the defining crate, use either a namespace-qualified call or import the namespace:
fn score_book() {
let book = Book()
let score = book.reader::read_and_judge()
}
use reader
fn score_book_after_import() {
let book = Book()
let score = book.read_and_judge()
}
If method names from two imported crates clash, use qualified syntax.
Limited overloading is supported through argument labels:
fn pick(value: U8) -> U8 {
value
}
fn pick(value: U8, plus increment: U8) -> U8 {
value + increment
}
fn pick(value: U8, plus increment: U8, ~ fallback: U8) -> U8 {
value + increment + fallback
}
fn main() {
assert pick(1) == 1
assert pick(2, plus: 3) == 5
assert pick(4, plus: 5, fallback: 6) == 15
}
The ~ marker means the call label should be the same as the parameter name.
Note
Generated Rust function names are label-mangled, such as pick,
pick__plus, and pick__plus__fallback. Galvan names forbid double
underscores to avoid clashes with generated names.
Galvan has concise syntax for common collection types:
pub type IntArray = [Int]
pub type StringSet = {String}
pub type Inventory = {String: Int}
pub type DailyMenu = [String: Int]
Ordered collection types use []; unordered collection types use {}.
Note
These collection forms map to common Rust collection types: arrays are backed
by Vec, sets by HashSet, dictionaries by HashMap, and ordered
dictionaries by IndexMap.
Arguments are passed by value by default. If a function needs to mutate the
caller-owned value, mark the parameter as mut:
fn make_uppercase(mut name: String) {
name = name.to_uppercase()
}
fn shouted(name: String) -> String {
name.to_uppercase()
}
When calling a function with a mut parameter, the caller must annotate the
argument:
fn main() {
mut name = "milo"
make_uppercase(mut name)
}
Argument modifiers can also be written in postfix form:
fn main() {
mut name = "milo"
make_uppercase(name.mut)
}
The same explicit modifiers are required when the receiver of a member function
is declared as mut self or ref self:
fn rename(mut self: Dog, name: String) {
self.name = name
}
fn replace(ref self: Dog, replacement: Dog) {
self = replacement
}
fn main() {
mut dog = Dog(name: "Milo")
dog.mut.rename("Scout")
ref shared_dog = Dog(name: "Rex")
shared_dog.ref.replace(Dog(name: "Lassie"))
}
Stored references are declared with ref. They use reference semantics and can
be shared through structs or variables:
pub type Person {
name: String
age: Int
ref dog: Dog
}
fn main() {
ref dog = Dog(name: "Milo", age: 5)
let person = Person(name: "Jochen", age: 67, dog: ref dog)
dog.age += 1
print(person.dog.age) // 6
print(dog.age) // 6
}
Passing a ref value into a ref parameter also requires an explicit call-site
modifier:
fn store(ref value: String) {
// ...
}
fn main() {
ref label = "shared"
store(ref label)
store(label.ref)
}
Note
Galvan's mut value: T is generated as a mutable Rust reference. Galvan does
not expose immutable references directly; immutable values are treated with
copy-on-write-style defaults, while ref values use heap-backed reference
semantics.
ref variables can also be reassigned by reference:
fn main() {
ref message = "Hello"
ref alias = ref message
message = "Hi"
print alias // "Hi"
ref farewell = "Bye"
alias = ref farewell
print message // "Hi"
print alias // "Bye"
}
Galvan uses ? for optional types and ! for result types:
type OptionalInt = Int?
type FileOrFlexibleError = File!
type FileOrIoError = File!IoError
The error type is written after !. If it is omitted, Galvan uses a flexible
error type.
The postfix ! operator unwraps a result and returns early if it contains an
error:
fn read_title(path: String) -> String! {
let file = File::open(path)!
file.read_to_string()!
}
Note
Galvan's postfix ! operator corresponds to Rust's ? operator for early
error return. Galvan uses ? for safe calls instead.
The safe-call operator ?. continues only when the receiver is not none and
not an error. else can provide a fallback for optionals and results:
let displayed_name = maybe_user?.name else { "Anonymous" }
let points = load_reward_points() else { 0 }
Values are automatically wrapped when an optional or result is expected:
let selected_count: Int? = 5
let loaded_count: Int!String = 7
fn count_or_default(count: Int?) -> Int {
count else { 0 }
}
assert count_or_default(21) == 21
if can be used as a statement or as an expression:
let shipping_status = if paid {
"ready"
} else if inventory_reserved {
"waiting for pickup"
} else {
"blocked"
}
An if expression without an else produces an optional value:
let discount = if customer_is_member { 10 }
match expressions can return branch values and destructure enum variants:
fn classify_color(color: Color) -> String {
match color {
Transparent { "transparent" }
Gray(value) { "gray \(value)" }
Rgb(r: red, b: blue, g: _) { "rgb \(red) \(blue)" }
_ { "unknown" }
}
}
try unwraps an optional or result. The unwrapped value is available through
it, or through an explicitly named binding:
try load_reward_points() {
print("Loaded \(it) points")
} else {
print("Could not load points: \(it)")
}
try maybe_user |user| {
print(user.name)
} else {
print("No user selected")
}
Like if, try can also be used without an else branch:
try maybe_user |user| {
print(user.name)
}
let selected_name = try maybe_user |user| { user.name }
For loops work over ranges, collections, optionals, and results:
for 0..<n {
print(it)
}
for 0..<n |index| {
print(index)
}
For loops can also be expressions. In expression position, they collect each iteration result into an array:
let doubled_even: [Int] = for 0..=n {
if it % 2 == 1 { continue }
it * 2
}
Range bounds use ..< for exclusive upper bounds and ..= for inclusive upper
bounds.
Warning
General loop { ... } expressions are not implemented yet. for loops are
implemented for arrays, sets, dictionaries, ordered dictionaries, optionals,
results, and ranges. Tuple iteration is still incomplete.
Return values are implicit, but return can be used for early returns:
fn fib(n: Int) -> Int {
if n <= 1 {
return n
}
fib(n - 1) + fib(n - 2)
}
Errors are returned early with throw:
fn checked_divide(a: Float, b: Float) -> Float! {
if b == 0 {
throw "Division by zero"
}
a / b
}
Type identifiers start with an uppercase letter. A lowercase type name introduces a type parameter:
type Container {
value: t
}
fn get_value(self: Container<t>) -> t {
self.value
}
Bounds can be specified with where:
fn concat_text(self: t, other: t) -> String where t: ToString {
self.to_string() ++ other.to_string()
}
Warning
Generic syntax, basic generic functions, methods, and where clauses are
implemented, but generic type inference and compatibility checking are still
incomplete. Some generic cases may produce warnings or fall back to broad
compatibility.
Closures use parameter-list syntax:
let add = |a, b| a + b
Closure types use the same |_| shape:
fn map(self: [t], f: |t| u) -> [u] {
mut result = []
for self {
result.push(f(it))
}
result
}
Functions with trailing closures can omit the regular argument parentheses:
orders
.map |order| { order.total }
.filter |total| { total > 0 }
.fold 0 |acc, total| { acc + total }
Warning
Numbered closure parameters such as #0 and #1 are planned but not
implemented yet.
In a statement or on the right-hand side of an assignment, function-call parentheses can be omitted:
fn add(a: Int, b: Int) -> Int {
a + b
}
fn main() {
let result = add 2, 3
print result
}
This syntax is not allowed in function arguments, and calls with no arguments still require parentheses to avoid ambiguity with variables.
Arithmetic operators:
+: Addition-: Subtraction*: Multiplication/: Division%: Remainder^: Exponentiation
Logical operators:
and,&&: Logical andor,||: Logical orxor: Logical xornot,!: Logical not
Note
Galvan does not provide Unicode alternatives for logical operators because
∧ and ∨ can be confused with v and ^. Custom operators can provide
those spellings later.
Bitwise operators:
|: Bitwise or&: Bitwise and~: Bitwise xor<<: Bitwise left shift>>: Bitwise right shift
Comparison operators:
==: Equality!=,≠: Inequality<: Less than<=,≤: Less than or equal>: Greater than>=,≥: Greater than or equal===,≡: Pointer equality for heap references!==,≢: Pointer inequality for heap references
Collection operators:
++: Concatenation--: Removal**: Repetition[]: Indexing[:]: Slicingin,∈,∊: Membership
Range operators:
..<: Exclusive range..=: Inclusive range+-,±: Inclusive range around a value
Warning
Some listed operators are still incomplete. In particular, collection removal
(--), repetition (**), slicing ([:]), unary logical not, and custom
operators need additional implementation work.
Galvan's intended operator model is structural: operators should be derived for types whose members support the same operation.
type Vec2 {
x: Float
y: Float
}
test "Automatically derive addition for struct" {
let this_vec = Vec2(x: 5.0, y: 10.0)
let that_vec = Vec2(x: 7.0, y: 1.0)
let result = this_vec + that_vec
assert result.x == this_vec.x + that_vec.x
assert result.y == this_vec.y + that_vec.y
}
Warning
Canonical operator implementation is not implemented yet.
Galvan's intended union syntax uses | where a type identifier is expected:
fn print_value(value: Int | String) {
print("Value: \(value)")
}
Warning
Union types are not implemented yet.
Galvan uses semicolons to separate statements, but infers semicolons on newlines when:
- the next line starts with an alpha character or underscore as the first non-whitespace character
- the next line starts with
{,(,[,', or"as the first non-whitespace character
Galvan does not infer a semicolon when the current line itself is not a valid statement. It also infers commas for struct type declarations when fields are separated by newlines.
Galvan provides a concise syntax for unit tests in any .galvan file:
test {
assert 2 == 2
}
test "Ensure that addition works correctly" {
assert 2 + 2 == 4
}
Test descriptions are optional but encouraged.
Galvan has built-in support for CLI apps with arguments and subcommands:
cmd main(
/// Optional name to greet when no subcommand is selected
n name: String?
) {
try name |name| {
print "Hello \(name)!"
} else {
print "Hello World!"
}
}
/// Greets the user
cmd greet(
/// First name of the person to greet
n name: String,
/// Surname of the person that should be greeted
s surname: String?
) {
try surname |surname| {
print "Hello \(name) \(surname)!"
} else {
print "Hello \(name)!"
}
}
Note
The CLI support is generated through the Rust clap crate. Top-level
cmd main arguments become top-level flags, and other cmd declarations
become subcommands.
The generated CLI looks like this:
$ my-app --help
Commands:
greet Greets the user
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
$ my-app greet --help
Greets the user
Usage: my-app greet [OPTIONS] --name <NAME>
Options:
-n, --name <NAME> First name of the person to greet
-s, --surname <SURNAME> Surname of the person that should be greeted
-h, --help Print help