AGENTS.md: Geblang for AI agents
Dense cheatsheet for AI coding agents writing Geblang. Read once at the start of a session.
What Geblang is
A statically-typed scripting language for backend services,
implemented in Go. Files end in .gb. Two backends share the
language: an evaluator (geblang test) and a bytecode VM
(geblang run, geblang build). They must produce identical
output; report a divergence as a bug rather than working around
it.
Project layout
A package has a geblang.yaml manifest:
name: myapp
version: 0.1.0
source: src
paths: []
dependencies:
somelib:
path: ../somelib
Source files live under src/ (whatever source points at).
Tests are *_test.gb files next to source.
geblang run main.gb # run a script
geblang test tests/ # run *_test.gb under a path
geblang check src/ # static check (no execution)
geblang fmt src/ # format in place (--clean, --strip-comments)
geblang doc src/ # print signatures for a file or dir
geblang build --entry main --out app <package-dir> # add --native or --docker
geblang build takes a MODULE name for --entry (the module must
export func main(list<string> args)), not a file path.
Syntax
Comments
# line comment
io.println(x); # trailing line comment
/* block comment
on multiple lines */
// is integer division, not a comment. Never use // for
comments anywhere.
Imports and modules
import io;
import datetime as dt; # alias
from crypt import passwordHash; # selective
from bytes import toHex as hex; # selective with alias
Imports go at file top. Stdlib modules don't need a path. Local
modules resolve through the manifest's paths or dependencies.
A module file starts with module name; and exports its public
surface explicitly; everything else is private to the module:
module app.users;
func normalizeId(string id): string { ... } # private
export func findUser(string id): ?User { ... } # public
export const int MAX_PAGE = 100;
export class User { ... }
Variables
let x = 1; # inferred (int)
int y = 2; # explicit
?string maybe = null; # nullable
const PI = 3.14; # immutable; PI is decimal
let x = 1.0 infers decimal, not float. Use 1.0f or
1.0 as float when you specifically want the float type.
decimal is the default for floating-point literals because
money and precise arithmetic are more common than fast IEEE math.
Division / is true division and yields decimal (or float),
so int n = a / b is a compile error; use // for the integer
(floor) quotient or (a / b) as int to truncate.
Primitive types
int, float, decimal, string, bool, bytes, any,
null. Integers are arbitrary precision (no overflow). Strings
are UTF-8 and interpolate inside double quotes:
let name = "Ada";
io.println("hello ${name}"); # interp
io.println('hello ${name}'); # literal (single quotes are raw)
io.println("${3.14159:.2f}"); # format spec -> 3.14
io.println("${42:04d}"); # -> 0042
Collections
list<int> xs = [1, 2, 3]; # int[] is an alias
dict<string, int> d = {"a": 1, "b": 2};
set<int> s = {1, 2, 3}; # brace literal w/o ':' is a set
- List mutators work in place (1.16.0+):
push,pop,prepend,insert,removeAt,remove,reverse,sort,sortBymutate the receiver AND return it (chainable).sorted(),reversed(),copy(),deepCopy()are the copy variants. - Dicts iterate in insertion order, including
.keys(),.values(),.items(), andfor (k, v in d). - An EMPTY
{}is a dict; an empty set needs a typed declaration. - Slicing is Python-style on lists and strings:
xs[1:3],xs[:2],s[-3:]. Negative indexes count from the end. - Comprehensions:
[x * x for x in 1..5],[x for x in xs if x > 0],{k: v * 2 for k, v in d}. instanceof list<T>works at runtime (reified generics).xs.search(v)/xs.search(pred)/s.searchPattern(re)return every matching locator (list indices, dict keys, string positions);xs.fill(v, n)appends n copies in place.- Numeric-check predicates test before converting:
s.isInt()/s.isDecimal()/s.isNumeric()on strings;f.isInt()/d.isInt()on float / decimal.
Ranges
for (i in 1..5) { } # inclusive: 1,2,3,4,5
for (i in 0..<10) { } # exclusive end
let xs = (1..5).toList();
range(1, 9, 2); # builtin -> [1, 3, 5, 7, 9] (inclusive)
zrange(0, 5); # exclusive, Python-style -> [0,1,2,3,4]; zrange(n) from 0
r.length; r.first; r.last; r.contains(3);
Control flow
if (cond) { } else if (other) { } else { }
for (x in xs) { }
for (k, v in d) { }
for (let int i = 0; i < n; i++) { }
while (cond) { }
do { } while (cond);
outer: for (i in 1..3) { continue outer; } # labeled loops
let grade = score >= 50 ? "pass" : "fail"; # ternary
match (x) {
case int n if (n > 0) => handlePositive(n);
case [first, second] => handlePair(first, second);
case ["go", n] => handleGo(n); # literal elements pin positions
case string s => handleString(s);
default => handleOther();
}
match uses =>, guards are if (cond) after the pattern, and
patterns match by type, literal value, or list shape and bind
names. As an expression (let y = match (x) { ... };) each arm
yields a value; an unmatched expression with no default throws
MatchError.
defer expr; runs at function exit (LIFO order), like Go.
Null handling
?string name = lookup(id);
if (name != null) {
io.println(name.length()); # narrowed to string inside
}
let display = name ?? "anonymous"; # null coalesce
let len = user?.profile?.bio ?? ""; # optional chaining
Functions
func add(int a, int b): int { return a + b; }
let inc = func(int n): int { return n + 1; };
func greet(string name, string greeting = "Hello"): string { ... }
greet("Ada", greeting: "Hi"); # named argument
func sum(int start, int ...rest): int { # variadic: rest is list<int>
return start + rest.reduce(func(int a, int b): int { return a + b; }, 0);
}
sum(1, 2, 3);
sum(...[1, 2, 3]); # list spread
greet(...{"name": "Ada"}); # dict spread -> named args
- Defaults, named args, variadics, and spread combine freely and work in every dispatch context: functions, lambdas, methods, static methods, constructors (1.17.0).
- Dict spread silently drops keys that name no parameter.
- Overloading: the same name with different signatures resolves by argument count and type at the call site.
- Closures capture by reference; captured variables can be reassigned through the closure.
- Pipe:
x |> f(y)callsf(x, y); chains read left to right. - Partial application: a call with
_placeholders returns a new callable with those positions open (add(_, 10),wrap(_, "-", _)), across every dispatch context. Non-hole arguments are captured once at creation.
Generics
func head<T>(list<T> xs): T { return xs[0]; }
class Box<T> {
T item;
func Box(T v) { this.item = v; }
func get(): T { return this.item; }
}
func topBy<T implements Scored>(list<T> items): T { ... }
Generics are reified: instanceof T, instanceof Box<string>,
reflect.typeBindings(x), and element-type enforcement on list<T>.push all
work at runtime. Explicit constructor type arguments (Box<string>(42)) and
class-type-parameter method parameters are enforced on both backends, and
constraints accept interface, class, and primitive leaves combined with |/&
(<T implements string|int>, or the bare <T string|int>).
Generators
func counter(): generator<int> {
yield 1;
yield 2;
}
for (n in counter()) { io.println(n); }
Return type is generator<T>. Use iterable<T> for parameters
that accept any iterable (generators, lists, sets, ranges).
Manual stepping: next() advances (null once exhausted), done() peeks for
exhaustion, close() ends early; these compose with for-in.
Errors thrown inside a generator keep their class in the
consuming loop's catch.
Async (true parallelism)
import async;
func work(int n): int { return n * n; }
let t1 = async.run(func(): int { return work(1); });
let t2 = async.run(func(): int { return work(2); });
io.println(async.all([t1, t2])); # parallel on real cores
import async.channel as ch;
let c = ch.Channel<int>(8);
c.send(1);
io.println(c.recv()); # channels also iterate: for (x in c)
Tasks are goroutines: CPU-bound fan-out scales across cores (no
GIL, no event loop). async.await(t), async.all, async.race,
async.timeout(ms, fn), async.cancel compose tasks; channels
and select coordinate them.
Classes
class Counter {
int value;
static int created = 0;
const string KIND = "counter";
func Counter(int start = 0) {
this.value = start;
Counter.created += 1;
}
func bump(): int {
this.value += 1;
return this.value;
}
static func total(): int { return Counter.created; }
}
class TaskCounter extends Counter {
string label;
func TaskCounter(string label) {
parent(0); # calls parent CONSTRUCTOR
this.label = label;
}
func bump(): int {
return parent.bump() + 100; # calls parent METHOD
}
}
parent(args)calls the parent CONSTRUCTOR;parent.method(args)calls a parent METHOD.superis not a keyword.@abstracton a class blocks direct instantiation; on a method it forces subclasses to override.@immutableon a class makes fields set-once (construction only).- No
public/private/protectedmodifiers; moduleexportis the visibility boundary.
Operator overloading (dunders)
class Money {
int cents;
func Money(int c) { this.cents = c; }
func __add(Money o): Money { return Money(this.cents + o.cents); }
func __eq(Money o): bool { return this.cents == o.cents; }
func __lt(Money o): bool { return this.cents < o.cents; }
func __string(): string { return "$${this.cents / 100}"; }
}
Dunders: __add __sub __mul __div __intdiv __mod __pow, ordering
__lt __lte __gt __gte (defining ONE ordering dunder derives the
other three operators), __eq, bitwise __bitand __bitor __bitxor __lshift __rshift, prefix __not __neg __bitnot, plus __index,
__contains (for in), __iter/__next/__done, __call,
__invoke (callable objects), and cast dunders __string __int __float __bool __decimal __bytes (drive as TYPE, println, and
interpolation).
Interfaces and enums
interface Notifier {
func send(string msg): void;
func sendAll(list<string> ms): void { # default body allowed
for (m in ms) { this.send(m); }
}
}
enum Color { Red, Green, Blue }
enum Shape { Circle(decimal), Rect(decimal, decimal) }
let area = match (shape) {
case Shape.Circle(decimal r) => 3.14 * r * r;
case Shape.Rect(decimal w, decimal h) => w * h;
};
Enums can also declare instance methods (match this inside the body) and
implements an interface, and expose EnumName.values() (nullary variants in
declaration order) and EnumName.fromName(s) (lookup by exact name, else
null).
Errors
try {
risky();
} catch (ValueError e) {
io.println("${e.class}: ${e.message}");
} finally {
cleanup();
}
class MyError extends ValueError {
func MyError(string m) { parent(m); }
}
throw MyError("boom");
Catches dispatch by class hierarchy (subclasses match parent
catches). Built-in classes include Error, RuntimeError,
ValueError, TypeError, IOError, NetworkError,
DatabaseError, NotFoundError, TimeoutError, TlsError,
PermissionError, AssertionError (TimeoutError/TlsError are IOError
subclasses; PermissionError guards capability-gated ops). errors.wrap(inner, msg), errors.is(e, cls), errors.stackTrace(e), errors.frames(e) are the
stdlib helpers.
Context managers and decorators
with (f = io.open("data.txt")) {
io.println(f.readAll());
}
# the with-binding (f) does not escape the block
import profiler;
let t = profiler.timer();
with (t) { doWork(); }
io.println(t.elapsedMs());
@memoize
func fib(int n): int { ... } # built-in caching decorator
func logged(func f): func { # custom wrapping decorator
return func(int x): int { io.println("call ${x}"); return f(x); };
}
@logged
func double(int x): int { return x * 2; }
Decorators are callable wrappers when the name resolves to a
function, otherwise inspectable metadata (reflect.decorators).
Reflection
import reflect;
reflect.typeOf(value); # "int", "list", class name...
reflect.class("module.ClassName"); # resolve class by name
reflect.classes(); # every loaded class
reflect.fields(cls); # field metadata
reflect.methods(cls); # method names
reflect.parameters(fn); # param metadata dicts
reflect.decorators(target); # [{name, args, namedArgs}]
reflect.hasDecorator(target, "Get");
reflect.typeBindings(value); # {"T": "int"} for reified generics
reflect.function("math.sqrt"); # resolve function by name (native too)
reflect.interfaces(cls) returns the DIRECT implements clause
only; walk reflect.parent(cls) for the full chain. typeof(x)
(parens required), dir(value), and dump(value) are ambient
introspection builtins.
Idioms
- Type cast for access:
(value as Foo).method(). Casts are CHECKED and throw on mismatch; useinstanceofto test first. - Stringify: interpolate (
"${n}"),n.toString(), or(n as string). String concat does not coerce. - Joint dict iteration:
for (k, v in d) { }. - Accumulate with push:
xs.push(item);mutates in place. - Hot-loop strings:
strbuilderor+=on a local (the VM fuses the accumulation); avoid rebuilding vias = s + xon fields. - Single-token YAML/parameter: a
"%key%"string that wraps exactly one marker preserves the referenced value's native type.
Anti-idioms
- Don't use
//for comments (it's integer division). - Don't write
"hello" + 5. Concat does not coerce; interpolate or cast. - Don't use
super. Useparent(args)/parent.method(args). - Don't name a source file the same as a stdlib module you import (the resolver picks the local file by filename).
- Don't reassign list mutator results expecting a copy:
let ys = xs.sort();leavesysandxsthe SAME mutated list. Usesorted()for a copy. - Don't write multi-paragraph code comments. One short line for WHY, never WHAT.
Stdlib at a glance
| Module | Purpose |
|---|---|
io |
stdin/stdout, files (open, readText, writeText, readBytes), exists, tempDir. |
sys |
env vars, args, cwd, exit, goroutineId. |
path / pathlib |
join, dir, base, ext, abs; object-style paths. |
json, yaml, toml, xml, csv, msgpack |
parse / stringify; insertion-ordered. |
encoding |
base64, hex, URL encoding. |
crypt |
AES, HMAC, SHA, bcrypt password hashing, JWT (jwtSign(p, k, {alg})). |
secrets |
constant-time compare, random tokens. |
bytes |
hex/base64, concat, slice. |
strings |
compare, equalsFold, fromCodePoint, splitting helpers. |
re, pcre |
regex (Go engine / PCRE-style). |
db |
SQL connect/query/exec; sqlite, postgres, mysql; transactions, prepared statements. |
redis |
client incl. pub/sub. |
http |
client (get, post, request(url) fluent builder, fetchAll) and server (serve, TLS, autocert). |
sockets, net |
TCP/UDP, low-level networking. |
time |
now, sleep, monotonicNs. |
datetime |
parse, format, Instant, zones, arithmetic. |
async |
tasks, channels, select, worker pools. |
store |
synchronised cross-request/goroutine state. |
reflect |
class/function/decorator metadata. |
collections |
maxBy, groupBy, chunk, sortBy, lazy lazyMap/lazyFilter. |
streams |
lazy iteration pipelines. |
strbuilder |
O(n) string accumulation. |
math |
trig, log, floor/ceil/round, prime tests, special functions (gamma, error functions, Bessel). |
ndarray |
N-dimensional numeric arrays: broadcasting, views, reductions, linear algebra, seeded random. |
dataframe |
columnar frames: typed columns, expression/predicate filters, groupBy().agg(), joins, pivot, CSV/JSON/SQL IO. |
process |
own pid/uid/gid; inspect (list/info/exists); gated control (kill/signal/setuid) behind --allow-process-control. |
file |
File object wrapping a handle with method-style read/write/seek and with-block auto-close. |
llm |
provider-neutral chat/embed client (OpenAI/Anthropic/Bedrock): chat, chatStream, tool calling, embed/embedBatch. |
profiler |
timer(), profile() context managers; CPU/memory/wall. |
image |
decode/encode PNG/JPEG/GIF/WebP, resize, crop, rotate. |
errors |
wrap, is, stack traces. |
ffi, clib.* |
call C libraries (zstd, magic, ncurses, systemd). |
messaging |
RabbitMQ / Kafka / SQS / STOMP producers and consumers. |
test |
the test framework (below). |
geblang doc <file-or-dir> prints exact signatures; dir(module)
lists members at runtime.
Testing
import test;
func half(int n): int {
if (n % 2 != 0) { throw ValueError("odd"); }
return n / 2;
}
class MathTest extends test.Test {
@test
func addsTwoIntegers(): void {
this.assertEquals(5, 2 + 3);
}
@test
func throwsOnInvalidInput(): void {
this.assertThrows(func(): void { half(3); });
}
}
setup() / teardown() (per method) and setupClass() /
teardownClass() (per class) are optional lifecycle hooks. The test
runner picks up Test subclasses; no manual test.run(). Run with
geblang test path/. Asserters include assertEquals,
assertNotEquals, assertTrue, assertFalse, assertNull,
assertNotNull, assertContains, assertEmpty, assertGreaterThan,
assertThrows, and assertThrowsOf; this.skip([reason]) skips a
method and @tag("name") categorizes it for --tag.
To test a module's PRIVATE members, declare the SAME module name
in a sibling *_test.gb (1.17.0): the test then runs inside the
module and sees private functions, classes, consts, and state.
# users.gb: module app.users; func normalizeId(...) ...
# users_test.gb: module app.users; import test; class ... extends test.Test
Pitfalls in priority order
//is integer division, not a comment.- List mutators are in-place (1.16.0+):
push/sort/etc. mutate AND return the receiver.sorted()/reversed()/copy()are the copy variants. asis a checked cast, not a type test; it throws on mismatch. Useinstanceoffor tests.- Floating literals are
decimalby default; suffixf(1.5f) or cast for IEEEfloat. - Don't shadow stdlib module filenames in your source files.
- Dict iteration is insertion order, not alphabetical.
- String interpolation requires double quotes; single quotes
are raw (no
\n, no${}). typeofneeds parentheses:typeof(x).reflect.interfaces(cls)returns the DIRECTimplementsclause only; walkreflect.parent(cls)for the chain.- Mixed numeric arithmetic does not coerce between
int/decimalandfloat; cast explicitly.
When in doubt
Write a five-line script and run it with geblang run or
geblang check. Both backends are fast on tiny programs and the
error messages name the line and the missing piece.