Syntax Basics

Comments

# line comment

/* block
   comment */

// is integer division, not a comment.

Doc comments are retained for reflection and documentation tools. Use ## for line docblocks or /** ... */ for block docblocks immediately before the declaration they describe:

## Returns the display name.
func name(): string {
    return "Ada";
}

/**
 * Handles user routes.
 */
class UserController {}

Variables And Constants

string name = "Ada";
int count = 3;
let inferred = 42;
const VERSION = "0.1.0";

Variables are mutable. Constants cannot be reassigned.

Use let when the type is obvious from the initializer. Use an explicit type when it documents an API boundary, narrows a nullable value, or prevents an accidental wider type.

let retries = 3;
dict<string, any> payload = json.parse(text);

The any type

any is Geblang's explicit opt-in for dynamic values. It is useful at boundaries where the concrete shape is not known until runtime: decoded JSON, HTTP request payloads, framework metadata, extension values, and generic dictionary contents.

any value = "queued";
value = 42;
value = {"status": "done"};

The binding is still statically typed: its static type is any. That means the analyzer allows assignments from different runtime types, but it also means you must narrow or cast before using type-specific behaviour:

dict<string, any> payload = json.parse(text);
string name = payload["name"] as string;
int count = payload["count"] as int;

Prefer precise types for application logic and public APIs. Use any only where a value is intentionally dynamic or where a generic container needs to hold mixed values.

Strings

Double-quoted strings process escape sequences and support interpolation:

string msg = "Hello ${name}!\n";

Any expression can appear inside ${...}:

io.println("${a} + ${b} = ${a + b}");
io.println("count: ${items.length()}");

Non-string values are automatically converted to their string representation:

let n = 42;
io.println("n = ${n}");  # n = 42

Single-quoted strings are raw - no escape processing and no interpolation:

string raw = 'Hello ${name}\n';  # literal: Hello ${name}\n

Multiline strings use triple quotes:

string html = """
<h1>${name}</h1>
""";

Choose single-quoted strings for regex patterns, shell fragments, or examples where backslashes should stay literal. Choose triple-quoted strings for HTML, SQL, Markdown, and larger fixture text.

Limitation: double-quoted string literals inside ${...} are not supported. Use single-quoted strings or variables instead:

let label = "items";
io.println("${label}: ${count}");   # ok
io.println("${'items'}: ${count}"); # ok - single-quoted inside expression

Numbers

Three numeric types:

int     count  = 10;
decimal money  = 12.50;
float   ratio  = 0.25f;

Use int for counts, indexes, and whole-number values. Use decimal for money, measurements, and any quantity where rounding errors are unacceptable. Use float for scientific or graphics calculations where IEEE 754 semantics and performance matter more than exactness.

Integer literals support decimal, binary, octal, hexadecimal, and _ separators:

let flags   = 0b1010;
let mode    = 0o644;
let mask    = 0xFF;
let million = 1_000_000;

The decimal type

decimal stores values as exact rational numbers (numerator/denominator pairs). Arithmetic never rounds - 0.1 + 0.2 is exactly 0.3, not a floating-point approximation:

decimal a = 0.1;
decimal b = 0.2;
decimal c = a + b;
io.println(c);   # 0.3000000000 (exact)

Because values are stored as fractions, dividing 1 by 3 produces the exact fraction 1/3, not a truncated approximation:

decimal third = 1.0 / 3.0;
io.println(third);   # 0.3333333333  (displayed to 10 decimal places)
io.println(third * 3);  # 1.0000000000  (exactly 1, no rounding error)

The default display always uses 10 decimal places, rounding the last digit where needed. The stored value is always exact regardless of what the display shows.

Controlling decimal places

toString(scale) and format(scale) format a decimal to a specific number of decimal places:

decimal price = 4.0 / 3.0;

io.println(price.toString());    # 1.3333333333  (default: 10 dp)
io.println(price.toString(2));   # 1.33
io.println(price.toString(4));   # 1.3333
io.println(price.format(2));     # 1.33  (same result; format requires scale)

toString() with no argument is equivalent to toString(10). format(scale) always requires the scale argument.

Casting to string also uses 10 decimal places:

let s = price as string;   # "1.3333333333"

When you need to display a currency value at exactly two places, always call .format(2) or .toString(2) explicitly:

decimal subtotal = 19.99;
decimal tax      = subtotal * 0.2;
io.println("Tax: " + tax.format(2));   # Tax: 4.00

Rounding to an integer

math.floor, math.round, and math.ceil accept decimal and return int:

import math;

io.println(math.floor(2.9 as decimal));   # 2
io.println(math.round(2.5 as decimal));   # 3
io.println(math.ceil(2.1 as decimal));    # 3

To keep the result as a decimal, round to a scale and cast back:

decimal rounded = (2.567).toString(2) as decimal;   # 2.57

Mixed arithmetic

Arithmetic between decimal and int promotes the int to decimal and returns decimal:

decimal price = 9.99;
int     qty   = 3;
decimal total = price * qty;   # 29.97 (exact)

Arithmetic between decimal and float is not directly supported - cast one side explicitly:

decimal d = 1.5;
float   f = 2.0f;
decimal result = d * (f as decimal);

Type conversions

decimal d = 3.75;
int     i = d as int;     # 3  (truncates; error if d has fractional part and it's non-zero)
float   f = d as float;   # 3.75 (approximate; may lose precision)
string  s = d as string;  # "3.7500000000"

# Convert from string or int
decimal fromStr = "12.50" as decimal;
decimal fromInt = 7 as decimal;

Casting a fractional decimal to int truncates toward zero. If exact integer conversion is needed, verify first:

if (d.isZero() || (d - (d as int as decimal)).isZero()) {
    int whole = d as int;
}

Collections

list<int> nums = [1, 2, 3];
dict<string, int> scores = {"ada": 10, "grace": 12};
set<int> ids = {1, 2, 2, 3};

Indexing is zero-based. Negative indexes count from the end:

nums[0];
nums[-1];
scores["ada"];

Use length() to count the number of elements or entries in a collection:

list<int> nums = [1, 2, 3];
set<int> unique = {1, 2, 2, 3};
dict<string, int> scores = {"ada": 10, "grace": 12};

io.println(nums.length());   # 3
io.println(unique.length()); # 3
io.println(scores.length()); # 2

Use isEmpty() when you only need to know whether a collection has no elements:

if (!nums.isEmpty()) {
    io.println(nums.first());
}

Use hasKey or contains for dictionary key membership:

let data = {"name": "Ada", "middle": null};
io.println(data.contains("middle")); # true
io.println(data.hasKey("missing"));  # false

Collection methods mutate when their name implies mutation:

nums.push(4);
nums.removeAt(0);
scores.set("linus", 7);
scores.delete("ada");

Use collections.map, collections.filter, and related helpers when you want to return a transformed collection rather than update the original.

Operators

Arithmetic, comparison, equality, boolean, bitwise, null coalescing, optional chaining, ternary, and casts are available:

let total = price * quantity;
let ok = enabled && count > 0;
let name = maybeName ?? "anonymous";
let city = user?.address?.city;
let text = value as string;
let label = count > 0 ? "items" : "empty";

The ternary operator condition ? then_expr : else_expr is a compact inline conditional. The condition must be a bool - values are never implicitly treated as truthy or falsy:

let status = isActive ? "on" : "off";
io.println(score > 90 ? "A" : (score > 70 ? "B" : "C"));

Compound assignment operators are available for all binary operators:

x += 1;    x -= 1;    x *= 2;    x /= 4;    x //= 3;
x %= 10;   x **= 2;
x &= 0xff; x |= 0x01; x ^= mask; x <<= 2;   x >>= 1;
n ??= "default";   # assigns only if n is null

Conditions are explicit. Values are not implicitly treated as truthy or falsy:

if (name != "") {
    io.println(name);
}

if (items.length() > 0) {
    io.println(items.first());
}