| name | greycat |
| description | Build, run, and edit GreyCat projects. GreyCat is a statically-typed language plus runtime for graph-persistent, time-series-aware applications. Use when reading or writing `.gcl` source, when the user mentions GreyCat / project.gcl / nodeTime / nodeList / nodeIndex / nodeGeo / @expose / @library, or when the task involves running `greycat <command>`, deploying a project, or reasoning about gcdata/, lib/, files/, webroot/. |
GreyCat
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 @library / @include pragmas (which must appear in that file only) form the closure of analyzed modules. Compiled and run by the greycat runtime; statically analyzed by greycat-lang.
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.
Anti-hallucination rule
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.
When to read which file
This file covers the 80% you need across language and tooling. Drill into a reference file when the task touches its area:
Language:
- reference/syntax.md — Complete grammar reference: every statement, every expression form, operator precedence, literals (string substitution, time
'…', typed-suffix numbers).
- reference/types.md — Type system in depth: nullability, narrowing, generic invariance, casting, inheritance,
is/as.
- reference/stdlib.md — Built-in types by category (collections, node types, time/duration/geo, IO, HTTP, S3, Crypto). Method signatures.
- reference/annotations.md — Every annotation (
@expose, @permission, @reserved, @volatile, @format, @test, @tag) and every modifier (private, static, abstract, native). Doc-comment tags like @param.
- reference/idioms.md — Idiomatic patterns and common pitfalls (no ternary, no
void, no ::new(), function slot semantics, private semantics, generic invariance).
Tooling / project / runtime:
- reference/project.md — Project model: entrypoint,
@library / @include resolution, lib/<name>/ layout, FQN, multi-project workspaces.
- reference/libraries.md — Catalog of publishable domain libraries (
kafka, mqtt, opcua, sql, ai, algebra, text_search, ...) with what each pulls in, plus how to discover a library's latest version. Check here before hand-rolling a domain integration.
- reference/cli.md —
greycat CLI: every command (run, serve, dev, build, test, install, codegen, user, backup, restore, …), every option, the .env file.
- reference/lang.md —
greycat-lang CLI: lint, fmt, LSP server, debug dumps. The pre-commit / definition-of-done tooling.
- reference/runtime.md — What's alive in a running server: the graph store (
gcdata/), workers and tasks, the HTTP server (JSON-RPC / path-RPC / /files / webroot), identity and permissions, the scheduler, backups, logging.
- reference/workflow.md — Operational recipes: bootstrap a project, add an endpoint, add a persisted type, write tests, evolve schemas, generate SDKs, deploy.
- reference/webapp.md — Bundling a webapp: the one prescribed stack (VitePlus + MPA + Lit shadow-DOM components with
static styles + Web Awesome wa-* components themed by --wa-* tokens + app/theme.css brand overrides), app/ sources bundled into webroot/ via greycat dev, and calling the backend through the headless @greycat/web/sdk (greycat codegen ts).
- reference/webapp-tests.md — End-to-end UI testing of a webapp with Playwright:
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.
File anatomy
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 (project.gcl only)
/// Doc comment for the type.
type Point<T> extends Shape { // generic, inheriting
x: T; // attribute (terminated by ; or newline)
y: T = 0; // attribute with init
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";
}
Project anatomy
my-project/
├── project.gcl # @library + @include pragmas — the ONLY file allowed to carry them
├── .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/ + webroot/. See reference/project.md and reference/runtime.md for the role of each directory.
CLI in one minute
greycat install
greycat serve
greycat dev
greycat run [fn]
greycat test
greycat build
greycat codegen
greycat user list
greycat 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.
Declarations
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.
Type bodies
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").
Types
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 |
Nullability
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
}
Construction
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.
Member access
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.
Optional chaining
? 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]
Statements
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.
Operators
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.
- The unary
*x is the deref-op (different in context from binary *).
Literals
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' 'é' '\xff' // char escapes
'2025-05-22T16:47:42Z' // time literal (ISO 8601 inside '...')
"hello"
"hello ${name}, total = ${count + 1}" // template substitution
"line1\nline2" // escapes
Annotations
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 ;, must appear in project.gcl):
@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.
Modifiers
| 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.
Project structure
Every project has a single entrypoint named project.gcl. All @library and @include pragmas must appear in this file.
// 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.
- The closure is computed transitively (each loaded module's pragmas are followed).
- Never flat-walk a directory for
.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.
Quick patterns
HTTP endpoint
@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.
Time-series query
fn readings(sensor: nodeTime<float>, from: time, to: time) {
for (t, v in sensor[from..to]) {
println("${t}: ${v}");
}
}
Graph-persistent record
type Sensor {
name: String;
measurements: nodeTime<float>;
}
fn record(s: Sensor, v: float) {
s.measurements.setAt(time::now(), v);
}
Null-flow + narrowing
fn label(u: User?): String {
if (u == null) {
return "anonymous";
}
return u.name; // narrowed: u: User here
}
Scheduled background task
fn nightly_backup() {
Runtime::backup_delta();
}
@expose
@permission("admin")
fn install_schedule() {
Scheduler::add(
nightly_backup,
DailyPeriodicity { hour: 2 },
null,
);
}
Common pitfalls
The most-bitten gotchas (full list in reference/idioms.md):
- No ternary. Write
if (c) { return a; } return b;, not c ? a : b.
- No
void keyword. Omit the : T return-type clause.
- No
::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.
- Generics are invariantly typed.
Array<int?> is not assignable to Array<int>. Same for Map, nodeIndex, etc.
-> is reserved for stdlib node tags. Only node<T>, nodeTime<T>, nodeIndex<K,V>, nodeList<T>, nodeGeo<T> support ->. 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.
- No imports. Visibility comes from the project graph (
@library / @include), not from import/use statements.
as is unchecked at runtime. The runtime drops as T entirely; the lang's static check is the only safety net.
- Trailing
; after } is lint-rejected. A method/function body's closing brace stands alone — greycat-lang fires warning[redundant-semicolon] (auto-fixable) although greycat build accepts it.
- Don't commit
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.
- Default
@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.
- Never run
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.
Verifying syntax assumptions
When uncertain whether a construct is valid GreyCat:
- Run
greycat-lang lint — fastest oracle, catches shape drift the runtime accepts silently (unused locals, non-exhaustive enum chains, redundant null-checks, -> on non-deref receivers, …). See reference/lang.md.
- Search the stdlib (
lib/std/*.gcl) for real examples of the construct.
- Run
greycat run against a minimal project.gcl — the runtime is the oracle for valid programs.
After any non-trivial .gcl edit, run greycat-lang fmt --mode=check + greycat-lang lint as the definition of done — greycat build happily produces a .gcp from code that still has warnings or formatting drift.
- If still unsure, ask. Do not assume by analogy to TypeScript / Rust / Kotlin / Java — GreyCat has its own conventions (no
new, no ternary, -> vs ., private semantics).