GENO

Docs/Getting Started

Language Tour

This guide covers Geno's core features with runnable examples.

Variables and Types

let x: Int = 42           // immutable
var count: Int = 0         // mutable
let name: String = "Geno"
let pi: Float = 3.14
let done: Bool = true

Local bindings can include an explicit type annotation, and Geno can also infer the type from an unambiguous initializer:

let inferred = 42
var total = 0

let bindings are immutable; use var when you need mutation. Update a var by assigning to it (total = total + 1). Writing var total = ... again rebinds it in the same block, or declares a separate variable in a nested block. geno check warns about either redeclaration (W300).

Module Constants

A let written outside any function declares a module constant, which every function in the file can read. Use it for usage strings, lookup tables and other fixed values you would otherwise duplicate or wrap in a helper function:

let usage: String = "roman <number>"
let numerals: List[String] = ["I", "V", "X", "L", "C", "D", "M"]

func help_text() -> String
    example () -> "roman <number>"
    return usage
end func

func largest_numeral() -> String
    example () -> "M"
    return numerals[6]
end func

func main() -> String
    return help_text() + " uses " + largest_numeral()
end func

The initializer must be a literal: a number, string or boolean, a negated number, or a list or tuple of those. A call, an operator expression or a reference to another binding is rejected, so a module never runs code when it is imported:

let limit = max_size() + 1
    ^
Module constant 'limit' must be initialized by a literal, but found an operator
expression. Move the computation into a function and call it where the value is
needed.

Three more rules follow from that:

  • Module constants are immutable. var at module level is rejected.
  • They are named in snake_case, like any other binding. let USAGE is a parse error, because leading-uppercase names are reserved for types and constructors.
  • They are private to their own file. export let is not supported, and an importing module does not see the name. Return the value from an exported function when another module needs it.

Functions

Function parameters and return values require type annotations. Functions also need at least one example clause (except main):

func add(x: Int, y: Int) -> Int
    example 2, 3 -> 5
    example 0, 0 -> 0
    return x + y
end func

Example clauses serve as both documentation and tests. They are verified at runtime.

Float Examples

Float examples allow small rounding differences, but the safest habit is to keep the full value printed by a real run:

func ratio(a: Float, b: Float) -> Float
    example (160.0, 7.0) -> 22.857142857142858
    return a / b
end func

A convenience rounding like 22.857 fails. The tolerance deliberately does not stretch that far: an example that passed on a value correct to only four digits would hide a real error rather than catch it.

geno test compares a Float result to a relative tolerance of 1e-9, so an expectation rounded to ten or more significant digits passes at any magnitude. Taking the expected value from a real run and keeping all its digits is still the simplest habit. When the domain allows it, prefer Int arithmetic instead, which compares exactly: cents rather than fractional currency, for instance.

Named Parameters

Functions with 3 or more parameters require named arguments at the call site:

func clamp(value: Int, lo: Int, hi: Int) -> Int
    example 5, 0, 10 -> 5
    example -1, 0, 10 -> 0
    if value < lo then return lo end if
    if value > hi then return hi end if
    return value
end func

let result: Int = clamp(value: 15, lo: 0, hi: 10)

Default Parameters

Parameters can have default values. Required parameters must come first:

func greet(name: String, greeting: String = "Hello") -> String
    example "Alice" -> "Hello, Alice"
    return greeting + ", " + name
end func

let msg: String = greet("Alice")              // "Hello, Alice"
let msg2: String = greet("Bob", "Hi")         // "Hi, Bob"

Contracts

func factorial(n: Int) -> Int
    requires n >= 0
    ensures result >= 1
    example 5 -> 120
    example 0 -> 1

    if n <= 1 then return 1 end if
    return n * factorial(n - 1)
end func

Type Definitions (ADTs)

type Shape = Circle(radius: Float)
           | Rectangle(width: Float, height: Float)
           | Triangle(base: Float, height: Float)

A leading | before the first variant is also accepted, which reads better for larger domain models:

type Tx =
    | Deposit(amount: Int)
    | Withdraw(amount: Int)
    | Audit

Constructors are called by name:

let s: Shape = Circle(3.14)
let r: Shape = Rectangle(10.0, 5.0)

Pattern Matching

Every case must be handled:

func area(s: Shape) -> Float
    example Circle(1.0) -> 3.14159
    match s with
        | Circle(r) -> return 3.14159 * r * r
        | Rectangle(w, h) -> return w * h
        | Triangle(b, h) -> return 0.5 * b * h
    end match
end func

Tuples can be destructured directly in a match arm:

func describe(point: (Int, Int)) -> String
    example (0, 0) -> "origin"
    match point with
        | (0, 0) -> return "origin"
        | (x, y) -> return to_string(x) + "," + to_string(y)
    end match
end func

A tuple pattern must have the same number of elements as the tuple type. Since every tuple has that fixed arity, an arm like | (x, y) -> already covers every value and needs no default arm.

Option and Result

// Option[T] = Some(value: T) | None
func find_first_even(nums: List[Int]) -> Option[Int]
    example [1, 3, 4, 5] -> Some(4)
    example [1, 3, 5] -> None
    let filtered: List[Int] = filter(nums, fn(x: Int) -> x % 2 == 0)
    if length(filtered) == 0 then
        return None
    end if
    return Some(head(filtered))
end func

// Result[T, E] = Ok(value: T) | Err(error: E)
func safe_divide(a: Int, b: Int) -> Result[Int, String]
    example 10, 2 -> Ok(5)
    example 10, 0 -> Err("division by zero")
    if b == 0 then
        return Err("division by zero")
    end if
    return Ok(a / b)
end func

Parsing Returns Option, Not Result

parse_int and parse_float return Option[Int] and Option[Float], not Result. Match on Some/None (not Ok/Err), and wrap the value in Ok/Err yourself when a caller needs a Result:

// parse_int returns Option[Int], not Result: match on Some/None
func parse_port(arg: String) -> Result[Int, String]
    example "8080" -> Ok(8080)
    example "eighty" -> Err("not a number: eighty")
    match parse_int(arg) with
        | None -> return Err("not a number: " + arg)
        | Some(n) -> return Ok(n)
    end match
end func

The ? Propagation Operator

func process(data: Option[String]) -> Option[Int]
    let s: String = data?         // returns None if data is None
    return parse_int(s)           // Option[Int], so it can be returned directly
end func

Pipelines

Left-to-right data flow with |>:

func process(data: List[Int]) -> List[Int]
    example [1, 2, 3, 4, 5] -> [4, 16]
    return data
        |> filter(_, fn(x: Int) -> x % 2 == 0)
        |> map(_, fn(x: Int) -> x * x)
end func

The _ placeholder marks where the piped value goes.

Traits

trait Describable
    func describe(self: Self) -> String
end trait

type Circle = Circle(radius: Float)

impl Describable for Circle
    func describe(self: Circle) -> String
        example Circle(5.0) -> "circle with radius 5.0"
        return f"circle with radius {self.radius}"
    end func
end impl

Error Handling

Try/Catch

func safe_head(items: List[Int]) -> Int
    example [1, 2, 3] -> 1
    example [] -> 0
    try
        return head(items)
    catch e: String
        return 0
    end try
end func

Throw and Structured Errors

Throw string errors or user-defined error types:

type ValidationError = ValidationError(field: String, message: String)

func validate_age(age: Int) -> Int
    example 25 -> 25
    if age < 0 then
        throw ValidationError("age", "must be non-negative")
    end if
    return age
end func

func main() -> String
    try
        let age: Int = validate_age(-1)
        return to_string(age)
    catch e: ValidationError
        return f"Invalid {e.field}: {e.message}"
    end try
end func

catch e: String catches runtime errors and thrown strings. catch e: MyType catches only thrown values of that type — unmatched types propagate outward.

Async/Await

async func compute(x: Int) -> Int
    return x * x
end func

func main() -> Int
    let a: Int = await compute(5)
    let b: Int = await compute(3)
    return a + b
end func

async func returns an Async[T] value. Use await to resolve it. await works inside async func and main(). Async functions are exempt from example clause requirements.

F-Strings

let name: String = "Alice"
let age: Int = 30
let msg: String = f"Hello {name}, you are {age} years old"

Collections

Lists (Immutable)

let nums: List[Int] = [1, 2, 3, 4, 5]
let doubled: List[Int] = map(nums, fn(x: Int) -> x * 2)
let evens: List[Int] = filter(nums, fn(x: Int) -> x % 2 == 0)
let total: Int = fold(list: nums, initial: 0, reducer: fn(acc: Int, x: Int) -> acc + x)

Replacing one element returns a new list rather than mutating in place. set_at is a prelude builtin, so it needs no import:

let nums: List[Int] = [1, 2, 3, 4, 5]
let patched: List[Int] = set_at(list: nums, index: 2, value: 99)  // [1, 2, 99, 4, 5]
// nums is unchanged

It raises if the index is out of range. Algorithms that write to the same collection repeatedly -- in-place sorts and the like -- are usually clearer with Vec[T] and vec_set, covered under Mutable Collections below.

List Comprehensions

A concise syntax for transforming and filtering lists:

let nums: List[Int] = [1, 2, 3, 4, 5]
let doubled: List[Int] = [x * 2 for x: Int in nums]          // [2, 4, 6, 8, 10]
let evens: List[Int] = [x for x: Int in nums if x % 2 == 0]  // [2, 4]

Maps (Immutable)

let m: Map[String, Int] = {}
let m2: Map[String, Int] = map_insert(m, "a", 1)
let val: Option[Int] = map_get(m2, "a")  // Some(1)

Mutable Collections

// Array[T] -- fixed size
let arr: Array[Int] = array_new(size: 10, default: 0)
array_set(arr, 0, 42)

// Vec[T] -- growable
let v: Vec[Int] = vec_new()
vec_push(v, 1)
vec_push(v, 2)

// MutableMap[K, V]
let mm: MutableMap[String, Int] = mutable_map_new()
mutable_map_set(mm, "key", 42)

JSON

Parse and generate JSON using the built-in JsonValue type:

let parsed: Result[JsonValue, String] = json_parse(text: "{\"name\": \"Alice\"}")
match parsed with
    | Ok(val) ->
        match val with
            | JsonObject(entries) -> // work with entries
            | _ -> // handle other types
        end match
    | Err(msg) -> // handle parse error
end match

let output: String = json_stringify(value: JsonObject([("key", JsonString("value"))]))
// output: {"key":"value"}

Note: json_parse returns Result[JsonValue, String], not the original Geno type. A json_parse → json_stringify round-trip preserves the JSON structure faithfully, but the result is always a JsonValue tree — not your original Geno types. Use pattern matching to extract values from the JsonValue variants.

Type Aliases

type Coordinate = (Int, Int)
type Predicate = (Int) -> Bool
type Pair[T] = (T, T)

CSV and TOML Parsing

Parse structured text formats with built-in functions:

// Parse CSV into rows
let rows: List[List[String]] = csv_parse("name,age\nAlice,30\nBob,25")

// Parse CSV with headers into maps
let records: List[Map[String, String]] = csv_parse_with_headers("name,age\nAlice,30")

// Parse TOML into JsonValue
let config: Result[JsonValue, String] = toml_parse("title = \"My App\"\nport = 8080")

Process Execution

Run shell commands with the process capability:

// geno run myfile.geno --unsafe --cap process
func main() -> String
  let result: Result[ProcessResult, String] = exec("echo hello")
  match result with
  | Ok(pr) ->
    match pr with
    | ProcessResult(code, out, err) -> return out
    end match
  | Err(msg) -> return msg
  end match
end func

exec_with_input(command, stdin) pipes text to stdin.

Modules

Split code across files:

// Utils.geno
func helper(x: Int) -> Int
    example 1 -> 2
    return x + 1
end func
// Main.geno
import Utils

func main() -> Int
    return helper(41)
end func

Exports

Control which symbols are visible to importers with export:

// MathLib.geno
export func add(x: Int, y: Int) -> Int
    example 2, 3 -> 5
    return x + y
end func

func internal_helper(x: Int) -> Int    // not visible to importers
    example 1 -> 2
    return x + 1
end func

Migration note: Modules with no export keywords currently expose all symbols for backward compatibility. This implicit export-all behavior is temporary. Add explicit export to any function or type that other modules should use.

Dependencies

Declare git dependencies in geno.toml:

[dependencies.my-lib]
git = "https://github.com/user/my-lib.git"

Then run geno install. The module resolver automatically finds imported modules in geno_modules/.

Rendered from docs/guide/language-tour.md in the Geno repository.