Use when working with .gcl files or GreyCat projects - efficient language with unified temporal/graph/vector database, built-in web server, native MCP for billion-scale digital twins
GreyCat is one language and one runtime in one binary. A project lives in a directory rooted at project.gcl. The greycat binary compiles it, runs it, serves it as an HTTP server, manages users, and stores its state in gcdata/. There is no separate database, queue, or web server.
.gcl source files are organized into projects with a single entrypoint named project.gcl, whose @include pragmas (valid in that file only) plus the @library pragmas of every module they reach form the closure of analyzed modules. Compiled and run by the greycat runtime; statically analyzed by greycat lint and formatted by greycat fmt.
Beyond the required std, GreyCat publishes optional domain libraries (Kafka, MQTT, OPC UA, SSH/FTP, PostgreSQL, LLM inference, linear algebra, full-text search, OpenStreetMap, and more). Before hand-rolling a domain integration, check the catalog in reference/libraries.md for one that already fits, then pull it in with an @library pragma and greycat install. Let greycat install --bump (optionally --branch=<name>) resolve and write the version pins; do not curl the registry and hand-edit project.gcl.
GreyCat is not Java, Rust, Kotlin, Python, or TypeScript. It has its own conventions and a small grammar. Before writing GCL by analogy to another language, check reference/idioms.md — most "obvious" guesses are wrong (no new, no ternary, no switch, no import, private ≠ "hidden", -> ≠ ., etc.).
When uncertain about a construct: read lib/std/*.gcl for real examples, then run greycat run against a minimal project.gcl. The runtime is the oracle.
This file covers the 80% you need across language and tooling. Drill into a reference file when the task touches its area:
Language:
'…', typed-suffix numbers).is/as.@expose, @permission, @reserved, @volatile, @format, @test, @tag) and every modifier (private, static, abstract, native). Doc-comment tags like @param.void, no ::new(), function slot semantics, private semantics, generic invariance).Tooling / project / runtime:
@library / @include resolution, lib/<name>/ layout, FQN, multi-project workspaces.kafka, mqtt, opcua, sql, ai, algebra, text_search, ...) with what each pulls in, how to discover a library's latest version, and the per-library skill to load from lib/<name>/skills/SKILL.md after installing one. Check here before hand-rolling a domain integration.greycat CLI: every command (run, serve, dev, build, test, install, codegen, user, backup, restore, …), every option, the .env file.greycat lint, greycat fmt, greycat lsp: lint rules, suppression directives, formatter modes. The pre-commit / definition-of-done tooling.gcdata/), workers and tasks, the HTTP server (JSON-RPC / path-RPC / /files / webroot), identity and permissions, the scheduler, backups, logging.static styles + Web Awesome wa-* components themed by --wa-* tokens + app/theme.css brand overrides — Web Awesome ships its own agent skills to load alongside this one), app/ sources bundled into webroot/ via greycat dev, and calling the backend through the headless @greycat/web/sdk (greycat codegen ts, which owns every type crossing the wire).playwright.config.ts driving greycat dev, authenticate-once via storageState, piercing the Lit/Web Awesome shadow DOM, and isolating specs from the persistent gcdata/ store.A .gcl module is a flat sequence of declarations and pragmas. No top-level expressions. No imports — visibility is governed by the project graph.
@library("std", "1.2.3"); // pragma: depend on std at this version
/// Doc comment for the type.
type Point<T> extends Shape { // generic, inheriting
x: T; // attribute (terminated by ; or newline)
static ORIGIN_X: int = 0; // only `static` attributes may have an initializer
private label: String?; // private attr = read-public, write-private
fn distance(other: Point<T>): float {
return sqrt((this.x - other.x) ^ 2 + (this.y - other.y) ^ 2);
}
static fn origin(): Point<int> {
return Point<int> { x: 0, y: 0 };
}
}
enum Color { red, green, blue }
var threshold: node<float?>; // module-level var: must be node<T?>, nodeList<T>, nodeIndex<K, V>, nodeTime<T>, nodeGeo<T>
@expose
fn ping(): String {
return "pong";
}
my-project/
├── project.gcl # @library + @include pragmas — the only file where @include is allowed
├── .env # optional GREYCAT_* config picked up at startup
├── bin/ # `greycat install` populates with the pinned core binary
├── lib/ # `greycat install` populates from @library pragmas
│ └── std/ # the stdlib
├── src/ # @include("src"); — your code
├── test/ # @include("test"); — *_test.gcl stripped by `greycat build`
├── files/ # served at /files/<user_name>/... — user uploads
├── gcdata/ # graph storage. DO NOT COMMIT. Back this up.
└── webroot/ # public static assets, served at /
Everything in bin/, lib/, gcdata/, and usually files/ is gitignored. The source of truth is project.gcl + src/ + (optional) test/, plus webroot/ when its assets are hand-authored - a webroot/ generated by a frontend bundler is build output and is gitignored instead. See reference/project.md and reference/runtime.md for the role of each directory.
greycat install # download libs + pinned core binary; --bump first rewrites @library pins
greycat serve # build + run as long-lived HTTP server (port 8080 by default)
greycat dev # serve + spawn a frontend watcher (vp/vite/--with=<cmd>)
greycat run [fn] # build + run `fn` (default: `main`). One-shot.
greycat test # build + run every @test function
greycat build # produce project.gcp (strips *_test.gcl)
greycat lint # static analysis of the .gcl closure (--fix applies auto-fixes)
greycat fmt # canonical formatting (--mode=check as a CI gate)
greycat lsp # language server over stdio, for editors
greycat codegen # generate typed client SDKs (c/ts/python/rust/java)
greycat user list # admin LMDB-backed user database
greycat backup # snapshot gcdata/ into ./backup/
greycat restore <archive>
Options can be flags (--name=value) or env vars (GREYCAT_NAME=value). greycat <command> -h lists the options that apply, with their currently-resolved values. See reference/cli.md for the full table.
type T {} // open user type
private type T {} // visible cross-module only via mod::T
abstract type T {} // cannot be instantiated; methods may lack body
native type T {} // runtime-implemented
type Sub extends Base {} // single inheritance
type G<T, U> {} // generics
enum E { a, b(1), "c-with-dash" } // entries optionally carry a value
fn name(p: T): R {} // function
fn name<T>(p: T): T {} // generic function
var globalName: T; // module-level variable (must have type; node-tag only)
Each can be prefixed with /// doc comments and annotations.
A type body contains attributes and methods. There is no constructor syntax — instances are built with object-init expressions (see "Construction" below).
type User {
/// Doc on the attribute.
id: int;
name: String;
private password_hash: String; // outside ctor: read-public, write-forbidden
static MAX_NAME_LEN: int = 64; // static (class-level) attribute — readonly
fn rename(new_name: String) {
this.name = new_name; // `this` is implicit in methods
}
static fn validate_name(name: String): bool {
return name.size() <= User::MAX_NAME_LEN;
}
native fn hash_password(); // body provided by runtime
abstract fn validate(): bool; // requires `abstract type`; no body
}
Trailing ; between members is optional but always safe. Methods can omit return type (means "returns nothing").
Everything is typed. Type references appear after : and inside generic brackets.
| Category | Names |
| ----------------------------------- | ------------------------------------------------------------------------ |
| Primitives | bool, int (i64), float (f64), char, String |
| Native containers | Array<T>, Map<K, V>, Tuple<T, U>, Buffer, Table<T>, Tensor |
| Native node types (graph-persisted) | node<T>, nodeTime<T>, nodeList<T>, nodeIndex<K, V>, nodeGeo<T> |
| Native value types | time, duration, geo, function, field, type, any, null |
| User-defined | Anything declared with type / enum |
A type without ? is non-null; appending ? makes it nullable.
var a: int = 0; // ok
var b: int = null; // ERROR — int is non-null
var c: int? = null; // ok
var d: String? = "hi"; // ok — non-null value assigns into nullable slot
Operations on a nullable value require the lang to see the null possibility eliminated, either by == null / != null narrowing or by !! (force non-null, throws at runtime if null).
fn use(x: String?) {
if (x != null) {
println(x.size()); // narrowed to String here
}
println(x!!.size()); // force; runtime error if x == null
println(x?.size()); // optional chaining — propagates null
println((x ?? "default").size()); // nullish coalescing
}
GreyCat has no new keyword and no ::new() convention. Build with object-init expressions:
// Field form (named): works for any user type.
var u = User { id: 1, name: "alice", password_hash: "..." };
// Positional form: only for Array, Map, node, geo.
var arr = Array<int> { 1, 2, 3 };
var g = geo { 49.6, 6.1 }; // (lat, lng)
// Array literal sugar: [..] is sugar for Array<inferred>{..}.
var xs = [1, 2, 3]; // Array<int>
// Tuple sugar: (a, b) is sugar for Tuple<any?, any?>{a, b}.
var pair = (1, "hi"); // Tuple<any?, any?>
// Node literals wrap a payload:
var n = node<User> { User { id: 1, name: "alice", password_hash: "" } };
Array<T>::new() and Map<K,V>::new() do not exist. Always use the brace form.
Three distinct operators — pick by what's on the left:
| Form | Meaning | When |
| --------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| obj.field / obj.method() | Access the value's own field or method | Always for user types and value types |
| n->field | Deref then access: resolves the node payload, then .field on the result | Only on the stdlib node tags: node<T>, nodeTime<T>, nodeIndex<K,V>, nodeList<T>, nodeGeo<T>. User types cannot opt in. |
| Type::member / Module::Type::member | Static / namespaced access | Static fields, static methods, enum entries, fully-qualified names |
var u: User = expr;
u.name; // own field
u.rename("bob"); // own method
var nu: node<User> = expr;
nu.resolve(); // node<T>'s OWN method (the deref method)
nu->name; // == nu.resolve().name — deref then field
nu->rename("bob"); // == nu.resolve().rename("bob")
User::MAX_NAME_LEN; // static attr
User::validate_name("a"); // static fn
Color::red; // enum entry
MathConstants::pi; // module-qualified static (defined in std/core)
-> is not optional sugar for . — it errors on any receiver other than a stdlib node tag.
? propagates null along a chain:
obj?.field // null if obj is null, else obj.field
n?->field // null if n is null, else n->field
arr?[i] // null if arr is null, else arr[i]
var x = 1; // type inferred from rhs
var x: int = 1; // type annotated
var x: int; // no init (non-null types must be assigned before use)
if (cond) {} else if (cond) {} else {}
while (cond) {}
do {} while (cond);
for (var i = 0; i < n; i = i + 1) {} // C-style; var declares the iterator
for (k, v in arr) {} // iterate Array (k=index), Map (k=key)
for (k, v in map) {} // unpack key/value pairs
for (t, v in node[from..to]) {} // time-window query on nodeTime
for (k, v in idx) {} // iterate nodeIndex/nodeList
for (t, v in series[from..to] limit 100 skip 10) {} // sampling clauses on series slice
return; return expr;
throw error;
break; continue; breakpoint; // breakpoint pauses the worker
try {} catch (e) {} // catch ident optional
at (targetTime) {} // time-aware-scope binding
There is no ternary (?:), no switch/match, and no void keyword. A function with no : T return type "returns nothing"; calling it in an expression position is an error.
Precedence (high to low):
postfix: . -> :: () [] ++ -- !! // member, call, offset, increment
prefix: - ! + * ++ -- // negation, deref-mul, increment
?? // nullish coalescing (highest binary)
^ // power
* / %
+ -
< <= > >=
== !=
is as // type test / cast
&&
||
= ?= // assignment (?= means "assign if null")
Notes:
?? binds tighter than ^ — count ?? 0 > 0 parses as (count ?? 0) > 0.is T and as T take a type, not an expression. is narrows on the then-branch.?= is "assign only if LHS is null".++ / -- exist in both prefix and postfix forms.*x is the deref-op (different in context from binary *).true false null this
42 // int
3.14 // float
1.5e-10 // float (scientific)
1_000_000 // int (underscores ignored)
42_time 42time // time literal (suffix form)
1.79e+308_f // float (typed suffix)
60s 2hour_42ms // duration (compound suffix)
'a' // char
'\n' '\0' '\\' 'é' // char escapes, and a UTF-8 char
'2025-05-22T16:47:42Z' // time literal (ISO 8601 inside '...')
"hello"
"hello ${name}, total = ${count + 1}" // template substitution
"line1\nline2" // escapes
Decl-level annotations modify the declaration that follows. Module-level pragmas (@library, @include, @permission, @role) appear standalone with a trailing ;.
@expose // expose this fn as an HTTP/RPC endpoint
@expose("renamed") // expose under a different path
@permission("admin") // require this permission
@tag("openapi", "mcp") // include in OpenAPI spec and MCP tool list
@reserved // server-only impl; not callable from GCL
fn admin_op() {}
@test // marks a test fn; discovered by `greycat test`
fn test_my_thing() {}
@volatile // type cannot be persisted to graph storage
type Cache {}
@format(DurationUnit::milliseconds) // serialization hint (ms instead of µs)
ttl: duration?;
@deref and @iterable also exist as type-shape annotations, but they are tooling hints on the stdlib node tags only — adding them to a user type does NOT opt the type into -> or for-in.
Module pragmas (must terminate with ;; @include must appear in project.gcl, the others may sit in any module):
@library("std", "1.2.3"); // bring std at this version into scope
@include("models"); // include all .gcl under ./models/
@permission("audit", "read audit logs"); // declare a permission
@role("auditor", "public", "audit"); // declare a role
See reference/annotations.md for the full list and semantics.
| Modifier | On decl | On attribute | On method |
| ---------- | ---------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------- |
| private | Cross-module access requires FQN (mod::T). Same-module unrestricted. | Read-public, write-private (only the constructor can assign). | Cross-module call requires FQN. |
| static | — | Class-level attribute (one shared value). | Class-level fn; access via Type::name(). |
| abstract | Type cannot be instantiated. | — | Method has no body; concrete subtypes must provide one. |
| native | Type/method body is implemented by the runtime. | — | Body must be absent. |
private is not "hidden." See reference/annotations.md for the full semantics and a worked example.
Every project has a single entrypoint named project.gcl. @include pragmas must appear in this file. @library, @permission and @role are accepted in any module of the closure, but keep them in project.gcl too so the dependency set is readable in one place.
// project.gcl
@library("std", "1.2.3"); // resolved against <project>/lib/std/ or GreyCat home
@include("src"); // recursively loads .gcl files under <project>/src/
@library("name", "version") resolves to <project>/lib/<name>/; the std library may also resolve under the GreyCat install's home.@include("relative/dir") resolves to <project>/relative/dir/ and recursively loads .gcl there..gcl files — always start from the entrypoint.See reference/project.md for cross-module visibility rules and FQN resolution, and reference/workflow.md for the bootstrap-to-deploy flow.
@expose
fn add(a: int, b: int): int {
return a + b;
}
Reachable at POST /<module>::add (path-RPC) and "<module>.add" via JSON-RPC. Without @permission, requires the api permission. See reference/runtime.md for the request lifecycle.
fn readings(sensor: nodeTime<float>, from: time, to: time) {
for (t, v in sensor[from..to]) {
println("${t}: ${v}");
}
}
type Sensor {
name: String;
measurements: nodeTime<float>;
}
fn record(s: Sensor, v: float) {
s.measurements.setAt(time::now(), v);
}
fn label(u: User?): String {
if (u == null) {
return "anonymous";
}
return u.name; // narrowed: u: User here
}
fn nightly_backup() {
Runtime::backup_delta();
}
@expose
@permission("admin")
fn install_schedule() {
Scheduler::add(
nightly_backup,
DailyPeriodicity { hour: 2 },
null,
);
}
The most-bitten gotchas (full list in reference/idioms.md):
if (c) { return a; } return b;, not c ? a : b.void keyword. Omit the : T return-type clause.::new(). Always Type {} or Type<G> {}. Unless an explicit Type { static fn new(): Type { /*...*/ } } exists.function parameters are opaque. Lambdas and static fn references carry their signature in source position (var f = fn(a: int): int {...} → f: fn(int): int), but once a value flows into a function-typed slot the signature is gone and calls throughSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer