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 greycat install <name>, which writes the @library pragma and installs it. 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, @precision, @raw, @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, greycat doc: lint rules, suppression directives, formatter modes and pragmas, API reference. 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 (always terminated by ;)
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 doc # API reference of the project's declarations (--expose: endpoints only)
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; // read-public; writable only through `this` or in the object literal
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; // no body; calling it un-overridden throws
}
Every attribute ends with ;; after a method body ; is optional. 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 node<T> and nodeTime<T>. Other node tags and user types cannot use it. |
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 node / nodeTime (on nodeList, nodeIndex and nodeGeo it throws at runtime).
? 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: reads as `null` until assigned, even for a non-null type (not checked)
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
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"; using its call as a value yields null.
Precedence (high to low):
postfix: . -> :: () [] ++ -- !! // member, call, offset, increment
?? // nullish coalescing (binds tighter than prefix)
prefix: - ! * ++ -- // negation, deref, increment
^ // power (right-associative)
* / %
+ -
< <= > >=
== !=
is as // type test / cast
&&
||
= ?= // assignment (?= means "assign if null")
Notes:
?? binds tighter than ^ โ count ?? 0 > 0 parses as (count ?? 0) > 0.?? also binds tighter than prefix operators - -a ?? b parses as -(a ?? b).^ is right-associative - 2 ^ 3 ^ 2 is 2 ^ 9.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; writable only through this or in the object literal. |
Not allowed (syntax error). |
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. |
Modifier order is fixed: private abstract native type T, static private attr: T.
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 through it are runtime-checked. Instance methods (obj.method, non-static Foo::method) are NOT first-class values; the workaround is a lambda that takes the receiver as a parameter (fn (r: Foo) { r.method(); }), since lambdas don't capture enclosing locals or this.Array<int?> is not assignable to Array<int>. Same for Map, nodeIndex, etc.-> is reserved for node and nodeTime. Only node<T> and nodeTime<T> support ->; on nodeList, nodeIndex and nodeGeo it throws at runtime. User types cannot opt in โ use . instead.private โ "hidden." A private type is still visible across modules via its fully-qualified name; only bare-name lookup is blocked. A private attr is read-public, write-private. Never gate same-module access on private.@library / @include), not from import/use statements.as is unchecked for object types. Casts to primitives (int, float, bool, ...) are checked at runtime, and int <-> float converts. Casts to String, user and other object types are dropped, so the lang's static check is the only safety net; a wrong cast throws on the first mismatched member access.; after } is lint-rejected. A method/function body's closing brace stands alone โ greycat lint fires warning[redundant-semicolon] (auto-fixable) although greycat build accepts it.gcdata/, bin/, or lib/. They are runtime / install state, not source. gcdata/ is the durable application state โ back it up but never check it in.@expose to authenticated. @permission("public") requires user opt-in. A bare @expose already requires the api permission (any authenticated caller). Reaching for @permission("public") because "the frontend doesn't have auth wired up yet" hands every anonymous caller on the network the same write access. Only add it when the user explicitly asks for an anonymous endpoint (typical examples: login, an unauthenticated health probe). If the answer is "I made it public so the demo would work," remove it and wire login instead. See reference/idioms.md ยง HTTP / @expose patterns.curl. greycat install --bump (bound patch by default, latest to cross a major.minor) rewrites every @library pin in project.gcl and installs what it wrote; --branch=<name> moves the whole project onto another branch. Reach for curl .../latest only to read a version, or for non-GCL artifacts like the web SDK tarball. See reference/libraries.md.greycat serve --user=<name> / GREYCAT_USER=<name>. That flag makes every request โ from anyone, anywhere on the network โ execute as that user, with their full permission set. It is not a "dev convenience"; it removes auth entirely. If the user explicitly asks for it for a one-off local test, fine; never propose it yourself, never put it in a recipe, never bake it into a .env. The correct dev pattern is Identity::login from a script or the browser, or greycat token --user=<name> to mint a short-lived token and pass it via header as Authorization: <TOKEN> (no Bearer) / cookie. See reference/runtime.md ยง Identity and permissions.When uncertain whether a construct is valid GreyCat:
greycat lint โ fastest oracle, catches shape drift the runtime accepts silently (unused locals, unreachable branches, redundant null-checks, -> on non-deref receivers, โฆ). See reference/lang.md.lib/std/*.gcl) for real examples of the construct.greycat run against a minimal project.gcl โ the runtime is the oracle for valid programs.
After any non-trivial .gcl edit, run greycat fmt --mode=check + greycat lint as the definition of done โ greycat build happily produces a .gcp from code that still has warnings or formatting drift.new, no ternary, -> vs ., private semantics).