8.4.633-stable Switch to dev

Core Changelog

8.4

Concurrency

  • enum MergeStrategy { fusion_by_key_fail_on_conflict, fusion_by_key_last_win, no_fusion_fail_on_conflict } and Task::set_merge_strategy(strategy) choose how the commit of the current task resolves a conflict. fusion_by_key_fail_on_conflict, the default, fails the commit. fusion_by_key_last_win keeps the value the task wrote, key by key: a field, a key of a Map, a node’s slot, or a node key of a nodeIndex the task changed differently from the commit before. no_fusion_fail_on_conflict fails the commit, or the merge of the jobs of an await, when a structured node (nodeTime, nodeIndex, nodeList, nodeGeo) was changed by the task and by another task at once, instead of merging it key by key. Plain fields and objects still merge field by field, and the stable branch refuses the same concurrent node writes. The strategy is a per-task flag, set during the task and not inherited by the tasks it spawns.
  • A Map merges key by key when two tasks change it concurrently, as a plain object merges field by field: writes to different keys all land, and a Map value that is a plain object merges field by field under its key. Writes to the same key still conflict. Before, a concurrent change of an Array or a Map attribute was silently overwritten by the last commit, and now conflicts as an int attribute does.

Security

  • A recycled connection no longer hands the next request the previous caller’s credentials. The token was cleared when a connection was accepted but not when it was kept alive, so on a reused connection an unauthenticated request was answered with the identity and permissions of the last authenticated one. Only reachable with keep_alive on, which is now the default.
  • Identity::create on a name that already exists no longer clears its password. It still sets the role and returns the identity, but the password, the grants and the open sessions are kept. A @permission("public") function forwarding a name to create let an anonymous caller lock any account out, root included.
  • /files refuses a path that starts with / after the /files/ prefix, such as /files//etc/hostname or /files/%2Fetc/hostname, with 404. A GET opened it as an absolute path, so an admin token could read or list any file the server process can read.
  • /files refuses a path with a . segment followed by more path, such as /files/./alice/data.csv, with 404. The first segment names the owner of a path, so an identity named . owned files/./alice/, which is files/alice/, and could read, write and delete the files of every identity. Browsers and most HTTP clients remove such segments before sending, a raw request does not.
  • A new identity cannot take the folder of another one on macOS and Windows, whose file systems ignore case and Unicode normalization. Names were compared with ASCII case folding only, so Élise, or élise written with a combining accent, was created next to élise, and its files/Élise/ was files/élise/. A name is now compared to the existing ones in NFKC with Unicode case folding: one that reads as the name of an existing identity names it, and a new name whose folder already exists is refused.
  • Removing an identity (greycat user remove, and Identity::remove) ends its sessions and leaves nothing for a later identity to inherit. A token issued to it before the removal is refused, so a call carrying one runs as an anonymous caller: it kept reading the files other identities had granted the removed one. Those grants are dropped with it. Ids only grow, kept in a new table of gcdata/security/users.mdb: removing the identity with the highest id handed that id to the next identity created, which then held every grant made to the removed one, and the removed one’s tokens answered as it. A store from an earlier version starts past every id in use or named by a grant. Its queued, running and awaiting tasks are cancelled, so none goes on with its permissions. A task of a removed identity that ended wrote its result to files/tasks/<id>/, the folder of an identity named tasks, and now writes none.
  • Mbed TLS is 3.6.7, with the fixes of its July 2026 security advisories: among them a remote buffer overflow in the TLS 1.2 ECDHE-PSK client handshake, a buffer overflow in mbedtls_ecdh_calc_secret(), ChaCha20 keystream reuse once its counter overflows, and a timing side channel in RSA PKCS#1 v1.5 decryption.

Breaking changes

  • What the server keeps for a task while it runs moved from files/<user>/tasks/<id>/ to gcdata/jobs/<id>/: its arguments file, the transaction logs and snapshot of an await, and the arguments, results and transaction logs of its jobs. /files does not serve that folder, so a queued task’s arguments can no longer be downloaded or overwritten there, and the process empties it when it starts, since nothing there is read after a restart. files/<user>/tasks/<id>/ holds the result.gcb of a task only.
  • The Runtime::on_files_put and Runtime::on_files_get handlers both receive the path of the file relative to the project, files/alice/x.csv, with / separators and without the query string, which File and the readers open as it is. The PUT handler received the request path, /files/alice/x.csv and its query string included, and the GET handler the path below files/, alice/x.csv.
  • A range over a nodeGeo, for (g, v in idx[a..b]), yields the keys of the box spanned by a and b and nothing else, and rangeSize(a, b) counts them. Both used to take the two keys as one stretch of the space-filling curve the index is sorted by, which also passes through places far from the box: a 1 km box over 1M points read 72 times the keys it held on average, 1,600 times at worst, and rangeSize counted them all. The walk now jumps over the stretches that leave the box, so the cost follows the keys inside it (the same 50 boxes: 765k keys read before, 10.6k after). The two keys may be any two opposite corners; a walk from the higher key runs backwards, as before. Code that filtered the keys with GeoBox.contains keeps working and can drop the filter.
  • nodeGeo::sample samples the box of its two corners, in every mode, and returns no row outside it. dense lists the keys of the box up to maxRows; adaptative keeps every k-th of them so that at most maxRows remain; fixed and fixed_reg lay maxRows grid points over the box, evenly spaced along the curve from the south-west corner, each carrying the value of the stored key before it on the curve when one follows it too (fixed_reg blends the two). Before, every mode walked the whole stretch of the curve between the corners: dense and adaptative returned keys outside the box and spent maxRows on them, and fixed stepped along that stretch, so close to half of its rows fell outside the box. A null corner now means the world’s, where it meant the index’s first or last key, which does not bound the index’s other keys on both axes.
  • nodeTime::sample, nodeList::sample and nodeIndex::sample with maxRows: 1 return one row, where they returned an empty table.
  • A String holding a NUL byte is refused where it reaches the OS as a C string, which would cut it there and act on another name: File::open, delete, rename, copy, mkdir, ls, File.isDir and File.sha256, the paths of every reader and writer, System::exec and System::spawn (the command and its arguments), System::getEnv, and Identity::create (an identity name is its files/<name>/ directory). The call raises <what> contains a NUL byte.
  • String.uppercase() and lowercase() map non-ASCII letters too (Unicode simple case mapping): "é".uppercase() is "É", "ß".uppercase() is "ẞ". A mapped character may encode to another number of bytes, so size() can change. Bytes that are not UTF-8 are kept as they are.
  • int::parse, float::parse and parseNumber take the whole string: text after a NUL byte or a newline is a bad number format, where "12<NUL>abc" gave 12 and "1.5\nabc" gave 1.5. Trailing spaces, tabs and newlines are still accepted.
  • String.get(i) returns the character that starts at byte i, where it returned the byte as a char; a byte inside a character reads as U+FFFD.
  • String.chars() returns the code points of the String as they are: "Éa fi".chars() is É, a, , fi. It returned those of its compatibility decomposition, case folded and without marks (e, a, , f, i); that is now String.nfkd_casefold_chars().
  • perf(v) is gone from std: calling it is a compile error. Use info(v) or trace(v) instead. The perf log level is gone as well. LogLevel::perf no longer exists, --log=perf / GREYCAT_LOG=perf is an invalid value, and so are gc_log_level_perf in the C API (gc_log_level_trace is now 4) and LogLevel::Perf in the Rust SDK. The runtime metrics have their own switch, GREYCAT_METRICS (see Runtime).
  • The block index moves from the unnamed table of gcdata/meta.bin to a named table, w0, of gcdata/index.bin (world 0’s: a world’s index is w<id>, its commits c<id>, main’s commits c0), so that a store can hold more tables in the same file. A store written by an earlier version is migrated the first time this version opens it: its entries are copied into index.bin (about 1 s for 20M blocks) and meta.bin is removed. The migration is one way: an earlier version started on a migrated store finds no index and creates an empty meta.bin of its own, after which this version refuses to start until that file is removed, with a message saying so; keep a backup before downgrading, as before.
  • io::S3 and io::XmlReader are gone from std; both now ship as their own libraries. XmlReader moves to xml and keeps its shape (read / can_read / available), gaining a select field and an XmlElement tree API alongside it, so a call site only needs @library("xml", "<version>"). S3 is a rewrite rather than a move: the s3 library is pure GCL over http and xml, every type is private and reached by its qualified name (s3::Client, s3::PutObjectRequest, sigv4::Credentials), and the methods do not match the six std exposed – list_objects takes a request object, get_object(bucket, key, filepath) is now get_object_to_file, and S3Bucket is s3::Bucket. Existing call sites must be rewritten, not just re-imported. Dropping both also retires the vendored sxml dependency, which nothing else used.
  • serve keeps connections alive by default. A client sending Connection: keep-alive now reuses one connection for several requests instead of being answered Connection: close. --keep_alive=false restores the previous behaviour. Windows is unaffected: it serves a connection synchronously off the acceptor queue with no way to hand it back, so it declines the option rather than advertise a Keep-Alive it would then break by closing.
  • runtime::Request is gone: its header, headers, uri and body are now Task::header, Task::headers, Task::uri and Task::body, reading the request the current task answers. All four answer null in a task that answers no request (task: true, greycat run, the scheduler, an await job); headers() is now Map<String, String>? instead of an empty map there.
  • --req_workers / GREYCAT_REQ_WORKERS is renamed --workers_small / GREYCAT_WORKERS_SMALL: the request workers are now the task workers of the small class, taken out of --workers (see Runtime). The old flag is refused; the old variable is ignored, so a .env setting it falls back to the default.
  • --workers is the total of task workers: small workers are no longer added on top of it, so a 16-core host starts 16 task workers by default instead of 18. --workers_small defaults to 20% of --workers instead of 2, and --workers_large to 10% instead of 0. A .env with GREYCAT_WORKERS=8 and GREYCAT_WORKERS_SMALL=2 now starts 8 workers, not 10.
  • RuntimeInfo.fg_threads, bg_threads and mem_worker are gone. RuntimeInfo.small, medium and large (RuntimeInfoClass) give each class’s workers, the object cache budget of one of its workers (mem_worker) and its cache_ratio; mem_workers is the part of --cache the workers share. fg_threads was small.workers, bg_threads is medium.workers + large.workers.
  • Log records go to the log stream of root, files/root/streams/log.ndjson, one runtime::Log JSON object per line. files/root/log.csv is no longer written, and a file left by an earlier version is not touched. Read the stream with Stream<Log>::get("log").reader(from) as root, or with jq from a shell. --logfile / GREYCAT_LOGFILE is gone: the flag is refused and the variable is ignored.
  • Log.id and Log.id2 are now Log.task_id and Log.job_id, in the log stream’s JSON as well. task_id is the task a record comes from – for an await job, the task that awaits it – and job_id the job’s index in that await, null for the task itself: id2 was 0 for both a task and its first job. Records from outside any task carry null for both (a request used to carry its request slot number). The terminal form follows: <user>/<task_id> for a task, <user>/<task_id>.<job_id> for a job, <user>/- outside any task, where it used to end in .0 for a task.
  • Each metric has its own stream: HostMetric records go to files/root/streams/host_metrics.csv and TaskMetric records to files/root/streams/task_metrics.csv, read with Stream<HostMetric>::get("host_metrics") or Stream<TaskMetric>::get("task_metrics"). Each line is the record itself as one CSV line, with the default CsvFormat, no header and no wrapper. TaskMetric now carries user_id, task_id, job_id and src. They are not log records, and --log does not switch them on. GREYCAT_METRICS / --metrics takes a comma-separated list of the metrics to record, host and task (GREYCAT_METRICS=host,task); unset, none is recorded. The option applies to run and serve. See Metrics.
  • RuntimeUsage, WorkerUsage, ZoneUsage, Runtime::usage() and RuntimeUsage::collect() are gone, and so is the runtime::usages time series they fed. Host statistics are now the HostMetric records of the host_metrics stream (see Runtime). --usage_step / GREYCAT_USAGE_STEP is renamed --metrics_host_step / GREYCAT_METRICS_HOST_STEP and defaults to 60s instead of 10s; the old name is refused as a flag and ignored as a variable.
  • LogDataUsage is now TaskMetric, written once per task instead of once per transaction. With task in GREYCAT_METRICS, every task – RPC calls, background tasks, scheduled tasks and each await job – writes it once it ended and its transaction committed, covering the whole task (an await suspension included) rather than whatever the worker had accumulated: the traffic counters were never reset between tasks, so a record summed everything its worker had done since it started. It adds the world_id the task ran in (0 for main), timings (wait, exec, run, suspended, commit), await activity (awaits, jobs, fusion_conflicts), catches, object cache behaviour (cache_misses, cache_evictions, and cache_blocks, memory and cache_budget against the budget), the commit’s size (dirty_blocks) and the dirty blocks written out before it for lack of memory (dirty_evictions), scheduling (task_class, borrowed, queued), args_bytes, result_bytes and status. TaskMetric in runtime.gcl documents every field; skills/reference/runtime.md has the full guide: where the records are, units, derived signals, cost, and a dashboard example. Every counter is bumped off the hot paths only: at the default level, RPC throughput and store-heavy tasks measured the same as before within run-to-run noise, and turning task on costs no measurable time – only about 550 bytes per task. A task’s start is now when a worker first picked it up, argument binding included.
  • An RPC call cut short by --request_ttl answers 503 with the error message timeout, whether it was still queued or already running; a running one used to answer 400.
  • Array.sort, Array.sort_by and Table.sort place null keys last whatever the order, keeping their relative order. Array used to put them first in both orders; Table compared them as zero bits, which put them first ascending and last descending, and dragged the rest of the column into that raw-bits comparison.
  • await takes the jobs only: await(jobs). runtime::MergeStrategy is gone with its first_wins and last_wins, which silently dropped one side’s writes (and, on a nodeTime/nodeIndex/nodeList/nodeGeo, could mix the tree blocks of two transactions); every merge now behaves as strict did, made finer by the key-level merge below. Drop the second argument of every await call.
  • A static function called on a specialization, X<int>::f(x), checks its params and return declared T against int at runtime, as an instance method of X<int> does; a wrong argument used to pass silently. A Foo<T> {} literal (Array<T> {}, Map<String, T> {}, a user type) in a method of X<T> now builds Foo<int> when the method runs on X<int>, instead of the bare Foo: a method declared : Array<T> that returns one no longer fails its own return check, and the collection it builds is typed, so adding a String to that Array<int> raises. A literal in a lambda, or in a method a subtype inherits from its generic parent, still builds the bare Foo. A project.gcp built by an earlier version must be rebuilt.
  • <, <=, > and >= with a type operand (T, T<U>) no longer compile: types have no order, and the comparison used to fail only at runtime. == and != stay valid.
  • An operator whose operand is missing, as in 1 + ;, (2 * ), x ?? ; or true && 1 + ;, no longer compiles and is a syntax error at the token where the operand should be. It compiled into a program that failed at runtime with Error { message: null } or used whatever value was left on the stack, as x ?? ; and 1 == ; did.
  • A . or -> that names no member, as in n->, n->->, n->[0], a. or a?., no longer compiles. A bare -> dereferenced the node as *n does and a bare . was ignored, so write *n, *(*n) or (*n)[0] instead.
  • Strings and chars share one escape set: \n \r \t \0 \\ \" \' \$, \u{1F600} (1 to 6 hex digits) and \u00E9 (exactly 4). Any other escape is a compile error, reported at its column: \a, \b, \f and \v are gone, and an unknown escape such as \d or \. no longer stands for its character, so a regex pattern needs "\\d". Write \u{1b} for ESC. A \u escape that names no codepoint (a surrogate such as \ud800, above 10FFFF, the wrong digit count) is an error too, and a char literal holds exactly one codepoint: 'ab', '' and 'e\u{301}' no longer compile. The same holds in annotations and pragmas (@expose, @tag, @permission, @library, @role).
  • gc_program_type_t.super_types holds a type’s nearest 8 ancestors instead of 4 (see Language), so the struct grows by 16 bytes and every field after it moves: a native library reading gc_program_type_t must be rebuilt against this header, and the Rust SDK’s greycat-sys bindings follow. A project.gcp built by an earlier version must be rebuilt.
  • --worlds / GREYCAT_WORLDS is gone: the flag is refused and the variable is ignored. It never did anything; worlds are created at runtime with World::create (see Runtime). The options after it in gc_env_options_offset_t move down one slot, so a native library reading gc_host__options must be rebuilt against this header.
  • A node declared with a generic alone, such as node<Box>, nodeList<Array>, nodeTime<Map> or nodeIndex<int, Box>, now saves each value with its own type. Such nodes written by an earlier version read as null here, and an earlier version fails to load the ones this version writes (corrupted zone binary cache). Their content must be written again after the upgrade. Nodes declared with their type parameters (nodeList<Array<int>>, nodeTime<float>) keep the format they had.
  • An empty password never signs in. Identity::set_password(name, "") now removes the password, so the identity cannot sign in until it is given one again, where it used to store the empty string as a password that login(name, "") accepted. login refuses an empty password before checking it, so an identity that was given an empty password by an earlier version cannot sign in either. Give it a real one with set_password.
  • A .gz or .gzip file is served with Content-Encoding: gzip only when the extension under it has a type of its own, as data.csv.gz is (text/csv, which the client decodes). Any other one, such as backup.tar.gz, is served as application/gzip without Content-Encoding, so a browser or an HTTP client stores the file as it is. Every .gz file used to be sent with Content-Encoding: gzip, and was decompressed on download.
  • greycat user remove, and Identity::remove, delete the folder files/<name>/ of the identity with every file in it, so an identity created later under the same name does not own the removed one’s files. greycat user remove kept it. A name that is not one path segment, such as .. or a/b, keeps its folder. greycat user add, Identity::create and Identity::create_with_password refuse a new name whose folder files/<name>/ already exists, so a folder made before its identity must be moved away first.
  • A new identity name must be a folder name on every file system: 1 to 255 bytes of UTF-8, without control, format (bidi, zero-width), default ignorable, private use, surrogate or unassigned characters, line or paragraph separators, or any of / \ * ? " < > |, without a space first or last, and neither ., .., a name ending in . nor a Windows device name (con, nul.txt, com1, …). A :, as in a name from SSO (<provider>:<sub>), is refused on a Windows server only. greycat user add, Identity::create and Identity::create_with_password throw invalid identity name: <name> for any other. Spaces inside a name and any script are fine. Identities an earlier version created under such a name keep working, and can be removed.
  • Every Identity call that takes a name (login, get_by_name, create, create_with_password, set_password, set_role, set_grants, grant, revoke, remove, set_world, set_worlds, and the greycat user commands) names the identity named exactly so, or else the one whose name reads as it, the same in NFKC with Unicode case folding: Alice@Corp.com signs in as and names alice@corp.com, Straße names strasse, and élise sent with a combining accent, as macOS input often is, names élise. Accents count: elise does not name élise. Each of these named no identity before: get_by_name returned null, login was rejected, and create refused a name differing in ASCII case only and made a second identity of any other, which shared its folder with the first on macOS and Windows. create on such a name now sets the role of the existing identity, and create_with_password throws identity already exists. A name that several identities created by an earlier version read as, such as ÉLISE next to Élise and élise, names none of them, and create on it throws identity name reads as several existing ones; each is still named by its exact name. A new name is stored in NFC. users.mdb holds an index of the normalized names, built on the first start and rebuilt after a version that does not keep it wrote the store, so these lookups cost one read.
  • While a for loop walks a nodeTime, nodeList, nodeIndex or nodeGeo, or a nodeTimeCursor stands on one of its values, adding or removing a key of that node (setAt, set or add with a new key, setAll, remove, removeAt, removeAll) fails with a concurrent modification error. It failed only when the write landed in the block the loop stood on: elsewhere it went through and the loop skipped or repeated keys, and removeAll freed the blocks under the loop, which could crash the worker. A node that holds a single value still accepts them, since the loop reads that value before its body runs. Setting a new value at a key the node holds still works; collect the keys to add or remove and change the node after the loop.

Language

  • Prefix operators stack, so ***n dereferences a node<node<node<int>>> three times and !!b or - -x apply twice, where a second prefix operator was a syntax error. A prefix - may be followed by whitespace, so - 1 and - 1.5 compile as -1 and -1.5 do. --x stays a decrement.
  • An inheritance chain may be of any depth: a type with more than 3 ancestors no longer fails to compile with too depth inheritance. A type keeps its nearest 8 ancestors at hand, so is, the parameter contract, attribute writes and the _type JSON discriminator answer as fast as before up to 8 ancestors, and walk up the chain past them, about 2.5 ns per further ancestor. A cycle in extends, also one a type reaches without being part of it, is reported as self inheritance: <a type of the cycle>.
  • A type or an enum named Root in project.gcl fails to compile with Root is reserved in project.gcl: that name holds the module variables of project.gcl, so the type compiled but lost its fields, and Root { n: 5 } threw cannot assign int to Root and offset 0. A Root declared in another module is not affected.

Standard library

  • nodeList.add(v) writes at the index after the last one, as documented, instead of at size(). Once a remove or a sparse set left a gap, size() fell on an index that could hold a value, and add overwrote it without an error: add("a"), add("b"), remove(0), add("c") lost "b". On a list whose last index is the largest int, add now fails instead of wrapping around.
  • A lookup in a nodeTime, nodeList or nodeIndex finds the right entry in the task that removed entries from it, before the task commits. A value set before the last one waits unsorted in its block until the block is saved, and once a task removed every sorted value of a block, a lookup between its remaining values read past the end of the block: nodeTime.next returned a time far outside the series, and resolveAt or getAt could crash the worker. next also returned a time before the one asked when that time fell past the block’s last sorted value: with 150 and 200 set, then 170, then 200 removed, next(160) gave 150.
  • Setting in a nodeTime, nodeList or nodeIndex a value that already belongs to another node fails and leaves the node as it was. Between two keys of a full block, the failed set left the block’s last value detached while the block still held it, and the worker could later read freed memory. Below every key of a node holding a tree of blocks, it left a broken block behind, and the next lookup failed with inconsistency: gc_sbi_load or crashed the worker.
  • A fixed or fixed_reg sample of a nodeTime or nodeList from a later key to an earlier one gives each row the value at or after its time. On a series large enough to hold a tree of blocks, rows took the value of another part of the series.
  • An adaptative sample from a later key to an earlier one returns every value of the range when maxRows allows it. On a large series it stopped after a part of the range.
  • A fixed sample that starts exactly on a stored key returns its rows. When that key was the first value of a block that also leads to other blocks, the sample was empty.
  • A fixed or fixed_reg sample of a nodeTime or nodeList that starts past the last key returns its rows, each null unless the last key is within maxDephasing of it. Its first row held the last value however far it was, and the sample, forward or backward, came back empty when the tree of blocks of the series ended with a block rather than a value: a series of 2048 keys set in ascending order, or of more than 1024 set in descending order.
  • A fixed or fixed_reg sample of a nodeTime or nodeList holding a single value holds it in the row at its key and, on a nodeTime, in the rows past it, in the direction of the sample, less than maxDephasing away. Every row held it whenever its key fell inside the sampled range.
  • A fixed sample taken in the task that set values out of order holds the right value in each row. A row could hold the value of an earlier key, repeated over the rows that followed.
  • A nodeTime or nodeList finds a value at time::min or int::min that was set after larger keys and not yet saved. resolveAt, resolve, prev and next skipped it and returned null.
  • nodeTime.prev(time::min) and nodeTime.next(time::max) return null. prev returned the series’ last time, and next on a large series returned one of its times.
  • nodeTime.setAll stops at the first value it cannot set, such as one that belongs to another node, and fails with its error. It skipped that value without an error and went on with the next ones.
  • A nodeTimeCursor walks the series as it is when first(), last() or lessOrEq() is called. Once a series of more than one value had been walked, the cursor kept walking it as it was then: values set since were missing, and after a removeAll it could read freed memory and crash the worker.
  • Keys set in a nodeTime, nodeList or nodeIndex between keys it already holds fill its blocks. Setting the even keys, then the odd ones in ascending order, left a block of one or two values behind each block-full of even keys: 100,000 keys took 611 blocks, 16% full, instead of about 100. They now take 101, setting them is 20% faster and walking them 28% faster.
  • Assert::isTrue, isFalse, isNull and isNotNull fail with a message, as equals does: expected true, got false, expected false, got true, expected null, got <value>, expected a non-null value, got null. They failed with none, so a test that caught the error and rethrew its message reported Error: null.
  • String functions work on the whole size() and treat a NUL byte (U+0000) as content: contains, indexOfString, indexOfStringFrom and replace find text after a NUL byte and no longer match a needle cut at its own NUL, startsWith and endsWith compare the whole value, and compare orders by code point over the whole content, so it agrees with == (a prefix sorts first). replace with a pattern holding a NUL byte read past the end of the string. The searches are also faster: contains on a 2 KB string takes about 85% less time.
  • New String.nfkd_casefold_chars(): Chars, the code points of the compatibility decomposition, case folded and without combining marks or default ignorables: what chars() returned before. Unlike nfkd_casefold() it keeps ø, ł, þ, œ and æ as they are.
  • nfkd_casefold() and nfkd_casefold_chars() decompose in one pass instead of two, and take ASCII as it is: on 1 KB of text nfkd_casefold() takes about 92% less time on ASCII and 63% less on accented text; the output is unchanged.
  • indexOf, indexOfFrom and lastIndexOf with a non-ASCII char, and split on one, find its UTF-8 bytes: "aé€b".indexOf('é') is 1 (it was -1) and "a€b".split('€') gives two parts (it gave one).
  • New io::GcxWriter<T> and io::GcxReader<T> write and read .gcx, a binary stream of values that does not depend on the ABI, so a program whose types changed, or a tool outside GreyCat, can read it. Values are tagged like GCB, objects carry their field names, and type, field and enum entry names are written once in a dictionary at the end of the file by GcxWriter.close(), which makes the file readable. Read into changed types, a removed field is skipped, a missing nullable field reads as null, and an unknown type or enum entry fails with its name. The byte layout is in lib/std/io.md.
  • New io::Stream<T>, an append-only buffer of T values that absorbs concurrent writes and hands them to a callback in batches, kept in files/<user>/streams/<name>.ndjson (.csv for a csv stream). Stream<T>::new(name, callback, max_dephasing, options: StreamOptions?) registers a stream and Stream<T>::get(name) finds it, both returning a Stream<T>. write(v) appends a value to the stream’s buffer, which reaches the file once flush_size bytes are pending (64KB when null, 0 writes each value at once), at the latest flush_step after the first of them (1s when null), when reader() or last_written_pos() is called, when the file is read through /files (a GET or HEAD, so an HTTP client polling it with Range: bytes=<offset>- gets every value written so far), and at a clean shutdown. Values still in the buffer are lost when the process is killed or crashes. flush() writes the buffer and syncs the file to disk, and is the durability point. A failed write to the file is logged rather than thrown, the file is cut back to its last complete record, and the next flush() throws it. The callback runs as a task at most max_dephasing after values reach the file, and drains them with reader(from), a Reader<T> bounded by last_written_pos() (a JsonReader<T> on an ndjson stream, a CsvReader<T> on a csv one). StreamOptions { format, flush_size, flush_step } holds the format (StreamFormat::ndjson when null, or StreamFormat::csv, one CSV line per value with the default CsvFormat) and the two flush settings. format() returns the format of a stream, and bind_reader(reader, from) points a reader of that format at it, as reader() does. Removing the identity that registered a stream stops it: its buffered values are dropped, its callback no longer runs, and get and earlier handles throw.
  • TimeWindow<T>::min and max return a Tuple<time, T>, and MultiQuantizer<T>::slot_vector an Array<int>. They returned the bare Tuple and Array, which the runtime refuses where the specialization is declared: fn f(w: TimeWindow<int>): Tuple<time, int>? { return w.min(); } raised wrong return type.
  • Array.sort, Array.sort_by and Table.sort sort a mix of int and float keys numerically, as floats (ints beyond 2^53 lose precision in the comparison only). Keys of other mixed types sort by type, numbers first, then Strings, then each other type, and by value within each type (an object other than a String by identity); a descending sort reverses the type order too. A mix used to fall back to comparing raw bits, which ordered negative ints after positive ones, floats arbitrarily and Strings by their address in memory.
  • Table.sort keeps a nullable single-typed column sorted as that type; one null cell used to demote the whole column to the raw-bits comparison.
  • Table<T>.get_row throws when the table does not hold one column per field of T, as set_row does (get_row: the table has 3 columns where 'P' has 2 fields), and when a cell does not fit its field, such as a null in a non-null field (get_row: cannot assign null to field 'project::P::b'). With more columns than T has fields it wrote the extra cells past the object it returned, which corrupted memory and could crash the worker; with fewer columns, or a cell its field could not take, it returned the object with those fields left unset.
  • A JSON reader (Json<T>::parse, JsonReader<T>) that refuses a value nested in a field names the field that refused it, then the fields it sits in: json mismatched type for field 'snow' at line 1, column 61 got 'null' while 'float' expected, in field 'currentConditions'. The message named the outermost field instead, with an expectation the value met (got 'null' while 'Conditions?' expected).
  • CsvWriter quotes a String the reader would otherwise take for a null: an empty or blank one, and null in any case, so "", " " and "null" read back as written. Written bare, they read back as null into a nullable column, also with the default CsvFormat. The http_metrics stream file writes an empty header value the same way, which tells it from an absent one.
  • GeoCircle.sw() and ne() return a box that contains the whole circle. Its longitude extent was too narrow away from the equator (a 1000 km circle at latitude 60 missed points at its east and west edges), and a circle crossing the antimeridian or holding a pole had its box cut at ±180, so a box prefilter then contains missed points. Such a circle’s box now spans every longitude.
  • CsvReader.last_line() returns the row the last read() parsed as the file holds it, quotes included, without its line break. It rebuilt the row from its parsed fields, so the quotes were gone. Where the reader had refilled its 4 KB buffer within the row, about once every 140 rows of 30 bytes, it returned up to 4 KB of the rows before it instead. With CsvReader.sharding, a read() not preceded by can_read() made it return a row of another shard.
  • Array.sort, Array.sort_by and Table.sort are 2 to 4 times faster on unordered data, and sort already ordered or reversed data in near-linear time. They use fluxsort (deps/fluxsort.sh) instead of piposort, and a radix sort for numeric keys from 512 elements on. The order is unchanged: equal keys keep their input order in both directions, NaN still sorts last and -0.0 still ties with 0.0. Measured on 1M random keys: int 46 ms to 23 ms, float 66 ms to 19 ms, a Table float column 67 ms to 21 ms.
  • System::exec fails with the end of what the command wrote, up to its last 4 KB, after exec failed with exit code <n>. The message held only the exit code, since the output had already been read. On success it returns stdout and stderr combined, as it did; its doc said stdout only.
  • File::workingDir() of an RPC call is its task directory, files/<user>/tasks/<id>/, like any task’s; it used to be files/<user>/requests/<timestamp>/. Outside any task (greycat test) it is the user directory.
  • A task stopped by Task::cancel ends with the error message cancelled.
  • New runtime::TaskStatus values, gc_task_status_* in the C SDK and TaskStatus::* in the Rust SDK: committing (9), the task’s code returned and its result is being written and its transaction committed or rolled back; cancelling (10), a cancel was accepted while it ran or before a worker started it, and it is being stopped; await_cancelling (11), a cancel was accepted while it was suspended in an await, and it is stopped once its jobs end. All three are still in flight: Task::running lists them, MCP tasks/get reports them working, and neither the scheduler nor a stream starts its next run before they end. Code that tests running or await to know whether a task is still in flight should also accept them.
  • runtime::TaskStatus::ended_with_errors (gc_task_status_ended_with_errors, TaskStatus::EndedWithErrors in the Rust SDK) is deprecated and will be removed in v9.x. The runtime never produced it: a task whose jobs failed ends ended or error.
  • Tasks can pick their worker class (runtime::TaskClass: small, medium, large): Job.task_class for spawn and await jobs, PeriodicOptions.task_class for the scheduler, and the value of the task header for RPC (task: large; task: true stays medium, an unknown value is answered 400). null keeps the previous class. Task::task_class() answers the class the current task runs in. gc_task_class_t is now public, and the C gc_periodic_options_t gains task_class (a gc_task_class_t; {} gives small, so set it) in a former padding byte; the Rust SDK’s TaskOptions gains task_class.
  • runtime::Task.task_class gives the worker class a task runs in, after falling back to a class that has workers, in Task::running, Task::tasks, the answer to a task: call and every Task::events frame. It is null for a task read back from the history, which does not record it.
  • runtime::World creates, resets and removes worlds, named branches of the store (see Runtime): World::create(name, mode, base) returns its id; World::reset(name) empties a world of what its tasks wrote, as it was when created from its base as the base is now (an overlay world reads its base again for every node, a snapshot world copies it again, a standalone world holds nothing), keeping its id, name, creator and the identities allowed to use it; World::remove(name) removes it. Both fail while a task runs in the world, a task in it included, or while another world is based on it, change only a world the caller created unless the caller is an admin, and, with create, sit behind the new world permission (the admin role holds it, user does not); a call that comes into the world while either works finds no world. All three log an info record whose data is the world’s WorldInfo, its reset time set by reset and its removed time by remove; World::id(name), World::current() (the calling task’s world, "main" for main) and World::list() (WorldInfo: id, name, mode, creation time, user_id of the user whose task created it, base; exposed, the worlds the caller may use) read them. Names take 1 to 128 of [A-Za-z0-9_.-], main is the main store’s, and ids are never reused. A snapshot or overlay world starts from its base: main (base null or "main") or another world, and the caller must be allowed to use it. An overlay world’s base may be an overlay world, which reads its own base in turn, up to 8 overlay worlds in a chain; a snapshot world based on an overlay one starts from what that world reads, writing the nodes the chain modified. WorldMode::snapshot starts from a copy of its base; WorldMode::overlay starts empty and reads its base for everything it has not modified; WorldMode::standalone takes no base, starts empty and reads no other index, like a new store, its module variables empty nodes of its own, and WorldChangesReader lists every node it holds rather than comparing it with a base. Job.world runs a spawn in a world ("main" for main, null for the spawning task’s), and PeriodicOptions.world a scheduled function (null: main; a run is skipped while its world does not exist). An await job always runs in its task’s world. Task.world names the world of a running task, as Task::running() and a task: true call report it, and is null in Task::history(), which does not record it. The C gc_periodic_options_t gains world_id in a former padding word ({} is main), and the Rust SDK’s bindings with it.
  • New Runtime::store_stats() (admin) surveys the store: its live blocks and the payload bytes they hold, by type, against the bytes the zone files take on disk (what exceeds the live bytes is fragmentation), the size of the ABI file and of the block index, the blocks the index lists that would not read, and the zones that could not be opened. It reads every live block, so it lasts as long as a full backup. A block main or a world reaches counts once, and StoreStats.worlds lists each world with its index entries, the blocks it holds that main does not and their bytes, and its index size. It reads main’s index whichever world the calling task runs in.
  • Identity::set_world(name, world) sets the default world of an identity (null or "main": main), which only describes it: no call reads it, and a client that wants it sends it in the world header. Identity::set_worlds(name, worlds) sets the worlds its calls may run in besides main, which every identity may use (null: every world; []: main only). Both are admin over HTTP. A call landing in a world the identity may not use fails: 403 over HTTP, an error from spawn with Job.world and from Scheduler::add with PeriodicOptions.world. An admin, and a world’s creator, use it whatever the list says. Identity.world and Identity.worlds read them back by name, leaving out removed worlds; Identity::remove clears them. They are stored in two new tables of gcdata/security/users.mdb, which an earlier version ignores, enforcing nothing.
  • Identity::create_with_password(name, role, password) creates a new identity holding password, so it signs in at once. It throws when name is taken (an existing identity is never changed), when password is empty, or when role is not declared. Like create, it is admin over HTTP and not permission-checked from GCL, which lets a @permission("public") sign-up endpoint call it. create followed by set_password cannot work there, because set_password refuses an anonymous caller.
  • Identity::create reports a name that several identities read as (see the identity name entry of Breaking changes) as identity name reads as several existing ones: <name>, a name it refuses as invalid identity name: <name>, and other failures as error while creating identity. Both used to raise error while reading identities, with the collision printed on stderr.
  • String.uppercase(), lowercase(), nfkd_casefold() and nfkd_casefold_chars() follow Unicode 18 (utf8proc 2.12.0), where they followed Unicode 17.
  • Time zones follow tzdb 2026e, where they followed 2025c: British Columbia (America/Vancouver) stays on -07 since 2026-03-09, Alberta (America/Edmonton) on -06 since 2026-06-18, the Northwest Territories (America/Inuvik, America/Yellowknife) on -06 since 2026-08-21, Manitoba (America/Winnipeg) on -05 from 2026-10-31, and Morocco (Africa/Casablanca, Africa/El_Aaiun) on +00 from 2026-09-20, none of them changing clocks again. Europe/Chisinau follows the EU transition times since 2022.
  • time.startOfWeek and endOfWeek with a time zone start the week on the first day of its region in CLDR 49: Sunday in America/New_York and Asia/Tokyo, Monday in Asia/Dubai, Saturday in Africa/Cairo. About half of the time zones read the first day of another one: New York and Tokyo started the week on Monday, Dubai on Sunday.

CLI

  • greycat import <file.gce> reads a store export of Runtime::export_store into a fresh store (a new gcdata/), every node under its original reference, so node handles in values stay valid. It builds the project as run does: a field or module variable the program no longer has is skipped and reported, one it added gets a new node, and a type it lacks fails the import, which then commits nothing and exits non-zero.
  • greycat run --world=<name> and greycat call --world=<name> (GREYCAT_WORLD) run the function in a world: run resolves it in its own process, call sends it as the world header. main is main; an unknown name fails before anything runs. Unset, both run in main. greycat user, backup, stats and the other store commands always run in main.
  • greycat user world <name> <world|main> and greycat user worlds <name> <w1,w2,...|-> set the default and allowed worlds of an identity (Identity::set_world, Identity::set_worlds; - for every world), and greycat user show prints both.
  • greycat call <fn> [json args...] calls an exposed function on a running server as a task, waits for it with a progress line on a TTY, and prints the result as JSON. Each argument is a JSON value. It authenticates with --token, or by default with the root token serve writes to gcdata/security/token, and targets --url or http://localhost:<port>. It never opens the store, so it runs alongside serve. Ctrl-C cancels the remote task.
  • greycat stats prints Runtime::store_stats() (live data, space on disk, fragmentation, size and count by type, damaged blocks), in this process, or on the server when one holds the store. It used to print zone sizes, fragmentation and type counts.
  • greycat codegen python in a project that does not declare @library("python") fails with a message naming the library and the legacy python2 generator. It exited with code 5 and printed nothing.
  • greycat run and greycat serve in a directory holding only a project.gcp that cannot be loaded, truncated or built by another version, fail with a message naming the file and this binary’s version. They exited with code 2 and printed nothing.
  • A variable set in the process environment takes precedence over the same one in the project’s .env, as documented: GREYCAT_PORT=8086 greycat serve binds 8086 whatever .env says, and System::getEnv returns the environment’s value. .env overwrote it, so only a --flag could override a checked-in .env.
  • The installer built into the core, which greycat install uses when the language tooling is not installed, installs again a library whose lib/<name>/ folder is missing although lib/installed lists it. It reported it already installed.
  • A Ctrl-C during greycat run lets the running jobs finish, then exits with code 1 and main did not complete: stopped when the function had not returned. While the function awaited jobs, the ones still queued never started and the process waited for them until a second Ctrl-C, then exited 0.
  • greycat user show lists under can read: and can write: the folders the user may open, and under readable by: and writable by: the identities its grants let into files/<name>/. The two pairs were swapped. The help of greycat user grant and revoke and their messages say who gets access to which folder, as in granted read to 'carol' on files/alice/.

Runtime

  • Outbound TLS also trusts the CA certificates of SSL_CERT_FILE (a PEM file) and SSL_CERT_DIR (directories separated by :, ; on Windows), the variables OpenSSL-based tools read. They are added on top of the system store and --ca_path, where OpenSSL takes them instead of its default store, so setting one never drops a root that already verifies. A file or directory that yields no certificate is skipped with a warning. The chain is the one the runtime hands to libraries, so greycat install and the http library trust them as well. From 8.2, which started exporting that chain, greycat install ignored them.
  • A --ca_path / GREYCAT_CA_PATH that yields no certificate (a mistyped or missing directory, one without certificates) is skipped with a warning naming it, and the system store is still loaded. A directory that could not be opened used to drop the whole trust chain without a word: every TLS connection of the runtime then failed, and libraries such as http fell back to a store of their own. A process that ends up with no CA certificate at all logs an error at startup.
  • The log, host_metrics and task_metrics streams buffer their records as http_metrics does, and reach their file once --log_flush_size, --metrics_host_flush_size or --metrics_task_flush_size bytes are pending (GREYCAT_LOG_FLUSH_SIZE, GREYCAT_METRICS_HOST_FLUSH_SIZE, GREYCAT_METRICS_TASK_FLUSH_SIZE, default 64KB), and at the latest --log_flush_step, --metrics_host_flush_step or --metrics_task_flush_step after the first of them (default 1s). A GET or HEAD of the file through /files writes the buffer first. A stopping process writes what is left, but a killed or crashed one loses what was still buffered. A flush size of 0 writes each record at once.
  • --log_flush_on_error / GREYCAT_LOG_FLUSH_ON_ERROR (default false): an error record writes the buffered log records to files/root/streams/log.ndjson at once, so the errors that precede a crash reach the file.
  • Runtime::export_store() (admin) writes every node of the store, as the current task sees it, to <backup_path>/export-<µs>.gce and returns the path; run it with greycat run runtime::Runtime::export_store. The file is a gcx file (see GcxWriter): an info Map (format, next_key, time, world), the root object with the module variables, then for each node a begin record (tag 25, its handle and type) followed by its value, or its key and value pairs. Values are written by name, so the export can be read after the program changed or by another tool. greycat import <file.gce> (see CLI) reads it back into a fresh store.
  • NodeReader lists every node the store holds, reachable from a module variable or not, as the current task sees the store: while (r.can_read()) { var n = r.read(); }. type keeps the nodes of one node type (nodeTime<float>) or of a node kind (nodeTime), and last lets a reader resume after the last node it read. A full scan reads the header of every block of the store.
  • WorldChangesReader lists the nodes the current world modified, each once: those whose world version differs from its base’s current version, among the blocks the world wrote. It reads the world’s commits table and compares block addresses with the base’s index, so a commit pays nothing for it. A task in main gets an error. WorldDiffReader { node: n } lists, key by key, how the node n of the current world differs from its base’s current version: each read() gives a WorldDiff with its kind (WorldDiffKind::added, removed or changed), key, and base and world values, skipping keys whose two values are deep-equal; last lets a new reader resume after the last entry, and the walk loads every internal block of the node and the leaves that differ, stepping over the leaves both sides share: a few edits to a million-key index cost a few dozen blocks. In an overlay world, a node whose tree the world never wrote costs its head blocks only.
  • A nodeTime, nodeIndex, nodeList or nodeGeo written by two transactions – two await jobs, a job and its task, or two tasks committing – merges key by key: different keys always merge, and only the same key written with different values is a conflict (objects at the same key merge field by field). Writing to one such node used to rewrite every block up to its root, so any two writers of the same node conflicted whatever keys they touched. The merge costs nothing while tasks run: the changed keys are read from the blocks each side wrote, at commit and when an await merges its jobs.
  • Every RPC call (path-RPC, JSON-RPC, MCP) runs as a task. The request worker threads are gone: a call is a task of the small class, executed by the task workers exactly like a background task, and only its way back to the client differs – its arguments come from the request and its result is written to the still-connected client, after its transaction committed, instead of to result.gcb. It has its own task id and shows in Task::running, Task::history and the SSE task events; Task::cancel stops it; Task::id, Task::expected_steps and friends work inside it; and an await in it suspends it like any task, freeing the worker while its jobs run, instead of running the jobs inline.
  • Task workers are split into classes, all taken out of --workers (default: the CPU count): small (--workers_small, default 20%, the RPC calls), large (--workers_large, default 10%) and medium, which takes the rest and keeps at least one worker. A worker runs its own class first and, when that queue is empty, any lighter class, so large workers also drain medium and small tasks; await jobs run in their parent’s class. Small workers never take the defrag lead, so a zone defrag never delays RPC calls.
  • The result.gcb of a task appears whole, once the task’s transaction committed: the task writes result.gcb.tmp in the same folder and renames it. It was written in place before the commit, so a client polling /files could read an empty or partial file, or a result that a failed commit then replaced with its error.
  • On Linux the default --workers is the number of CPUs the process may run on (taskset, docker --cpuset-cpus), lowered to the CPU quota of its cgroups rounded up (docker --cpus=1.5 gives 2). It was the host’s CPU count, so a container limited to 2 CPUs on a 64-core host ran 64 workers. A worker is pinned to one of the CPUs the process may run on: in a container limited to some CPUs, pinning a worker to a CPU outside of them failed with unable to pin worker thread.
  • On Linux the default --cache is 75% of the lowest memory limit of the cgroups the process belongs to when it is lower than the RAM. In a container started with a memory limit (docker run -m 3g), it took 75% of the host’s RAM, so the cache could grow past the limit and the process was killed for running out of memory.
  • The workers’ object cache (3/4 of --cache) is split by class instead of equally: a medium worker holds one unit, a small one --cache_ratio_small units (default 0.4) and a large one --cache_ratio_large units (default 3). With the defaults a medium worker keeps about the budget it had, small workers about 8% of the cache and large ones about 30%. serve logs the split at startup: Workers: 16 (small 3 x 810.2MiB, medium 11 x 1.98GiB, large 2 x 5.93GiB), cache: 48.0GiB (workers 36.0GiB, zones 12.0GiB). TaskMetric.cache_budget and HostMetricClass.cache_budget follow the class’s budget.
  • A new task goes straight to an idle worker of the lowest class that can run it, and every wake-up now delivers exactly one task. Workers used to wait on one shared condition and re-signal it after every dequeue, which issued two wake-ups per task and woke a worker for nothing about half the time.
  • --request_ttl now cancels an RPC call through the medium task cancellation, and applies only to RPC calls: a background task outlives it. A call still queued never starts, a running one is interrupted, and a suspended one is cancelled with its await jobs; the client gets 503 and the task ends as error, where a Task::cancel ends it as cancelled.
  • A cancellation arriving as a task finishes can no longer roll back a transaction the task already reported as successful, nor turn the error of a task that threw into cancelled: a Task::cancel either takes effect or changes nothing, decided by the task’s status at that instant, and once a task is committing it answers false and the task ends as it would have. A task reports ended, error or cancelled (Task::running, MCP tasks/get, gc_host__get_task_status in the C SDK) only once its transaction is committed or rolled back, and committing in between (see Standard library), so a task started after seeing ended reads what the finished one wrote. A cancellation landing while a task suspended in an await no longer leaks the pool slots of the jobs it had created.
  • A task whose await job pauses on a breakpoint reads await again once the job is resumed, and resumes from its await when its jobs end. It read running while still suspended, and never ended.
  • A debug call (an RPC call with the x-gc-debug header, run on a worker paused on a breakpoint) that throws ends as error. It was reported as ended, with the error as its result. A debug call cancelled before the paused worker picks it up never runs and ends as cancelled. Once it runs it cannot be cancelled, since it runs on the paused task’s machine: Task::cancel returns false for it, where it used to mark it cancelled while the call went on to the end. Its writes join the paused task’s transaction and are committed or rolled back with it. A debug call that no paused worker takes (the worker was resumed, or already has a call pending) is answered 503. It was answered 200 with a waiting task that never ran and held its pool slot until restart. A call still pending when the paused task is stopped ends as cancelled.
  • With host in GREYCAT_METRICS, the server writes one HostMetric record every --metrics_host_step (default 60s): process CPU time and host load, host and process memory, OS-level and store I/O, free disk and metadata size, live tasks, open connections and event streams, HTTP bytes in and out and files served and pushed, one entry per worker class (workers, busy share, queue depth, idle workers, ended / errors / cancelled / timeouts, memory against budget), and a summary of the store zones (size, committed / reserved / written blocks, binary cache, the most fragmented zone, defrag). Counters cover the period since the previous record, so nothing is ever reset under a reader – Runtime::usage() used to subtract what it read from the counters RuntimeUsage::collect() relied on. A record is one CSV line of about 300 bytes; it is built from counters and a few syscalls by the serve loop, in well under a millisecond with 130 workers. The only per-request cost is the HTTP traffic counting – a relaxed add next to each socket read or write into a slot each IO thread owns; RPC throughput and file serving measured the same as before within noise. On macOS the process memory figures now come from one task_info call: walking every memory region, as before, took about 90ms per sample with 130 workers.
  • With http in GREYCAT_METRICS, the server writes one HttpMetric record per HTTP request to files/root/streams/http_metrics.csv, read with Stream<HttpMetric>::get("http_metrics"): time, user_id, method (a runtime::HttpMethod, null when the request line could not be read), path (without its query string), kind (HttpMetricKind: file, upload, rpc, task, sse, other), status, ttfb, duration, request_bytes and response_bytes on the wire, the task_id of a call, and whether the exchange was aborted. The client address, X-Forwarded-For and User-Agent are personal data, recorded only when named in GREYCAT_METRICS_HTTP_FIELDS (--metrics_http_fields=ip,forwarded_for,user_agent), and null otherwise. Records are batched and written once --metrics_http_flush_size (default 64KB) is pending, and at the latest --metrics_http_flush_step (default 1s) after the first of them. A flush size of 0 writes each record at once.
  • --max_args_memory (default 1 MiB) bounds the arguments an RPC call keeps in memory. A larger body is streamed to the task’s arguments file while it is read, as a task: true call’s already was, instead of being held in the request buffer for as long as the call runs – an await included. The headers stay readable through Task::header; Task::body() answers null. The file is removed once the task read it, or as soon as the client goes away mid-upload. JSON-RPC envelopes stay in memory, since their method and params are only found by parsing them.
  • The files a task writes for its own process to read back are no longer synced to disk: the arguments file of an RPC call, a call with a task header, a spawn, an await job, an MCP tools/call and greycat run, the transaction logs of an await and its jobs, the snapshot of a task suspended on await, and the result.gcb of an await job. Nothing reads them after a restart. The result.gcb of a task is still synced, since a client may fetch it after one, and so is a file uploaded through /files.
  • A commit writes its blocks to the metadata store in block key order, through one cursor, instead of in hash order: each block used to cost two cold B-tree searches, and new blocks half-filled the pages they split. The order is sorted before the commit takes the global lock. On commits of millions of blocks the metadata writes measured 3 to 12 times faster; commits of a few thousand blocks take the same time, and the metadata store grows about 25% less for the same blocks.
  • Worlds are branches of the store, each with its own block index, a table of gcdata/index.bin next to main’s (the registry sits in gcdata/worlds/): a task running in a world reads and commits to the world’s index, never main’s. Worlds and main share the zone files and the blocks they have in common, so a world takes the space of what it writes, plus its index. A snapshot world copies its base’s index into its table when created (about 50 ms per million blocks, during which commits wait) and sees nothing the base commits afterwards. An overlay world starts empty and reads its base through a second read transaction for whatever it has not written: a head block it modifies is stored with only the node slots it changed, the others still read from the base, and the first commit modifying a nodeTime, nodeIndex, nodeList or nodeGeo the base holds copies the index entries of that node’s tree into the world, which then keeps the node as it left it. gcdata/worlds/registry records each world’s base. Two tasks of an overlay world making that first write to the same node at once conflict; any later concurrent writes merge as in main, await jobs included. Merging a world back into main is not available.
  • Defrag relocates the blocks of every world with main’s, in the same transaction, and reclaims a zone only once every reader opened after that relocation. Offline greycat defrag updates every world’s index too.
  • Runtime::backup_full (and greycat backup) includes the worlds: after main’s blocks, the archive holds the world registry, the blocks only worlds reference, once each, and every world’s index, sharing main’s blocks rather than copying them. A backup called from a task running in a world raises, since it would back that world up as main. greycat restore replaces the worlds of the store with the archive’s, rebuilt against the addresses it rewrites; an archive without worlds removes them. The archive also holds the identities’ default and allowed worlds and the id the next identity takes, in a section of its own after the worlds. An archive made by this version restores its main store with an earlier greycat restore, which ignores the worlds and those settings but leaves gcdata/worlds/ behind: remove it before starting this version again on that store.
  • A specialization stored in a node declared with its generic alone, such as a Box<float> in a node<Box>, nodeList<Box>, nodeTime<Box>, nodeIndex<int, Box> or nodeGeo<Box>, reads back as it was written. The store saved it in the layout of Box<float> but tagged it as Box, so a reload read every T field as null (min and max of a Gaussian<float> in a nodeList<Gaussian>), and a node holding more than a few such values failed to load with block load fail ! block_id:<n>. Such a value also keeps its specialization on reload, so an Array<int> in a nodeList<Array> comes back as Array<int> instead of Array.
  • A defrag, online or offline, and Runtime::backup_full (greycat backup) fail when they cannot read the block index to its end. A scan stopped by an LMDB error, or by a record that is not a 64-bit key and address, was taken for the whole index: an offline greycat defrag then rewrote every zone with only the blocks listed before that record and reported success, losing all the others, and a backup left them out.
  • The user database (gcdata/security/users.mdb) grows as it fills. It kept LMDB’s default map of 1MB, so past about 3000 identities, or with a large grant list, Identity::create, set_grants and the other writes failed with error while creating identity or error while setting grants, and greycat restore of a larger one failed. The map now doubles before a write once half of it is in use, and the database reopens with the size it reached.
  • jemalloc is 5.4.0, which fixes a possible deadlock in an arena reset, run to reclaim a worker’s memory after a leak and when a worker stops.
  • LMDB is at the latest commit of its 0.9 branch (74dd6c3ec648), in the same on-disk format: a store opens with this version and with the previous one either way. On macOS a write to the user database (gcdata/security) is flushed to the disk with F_FULLFSYNC: it relied on O_DSYNC, which macOS does not honour.
  • The Error an await throws for a failed job, once the task resumed from that await, has the await call in its stack. Its frame read line: 0, column: 0.
  • job.result() stays readable after later awaits, until the task ends. A job’s outcome was held outside its fields, which the task’s snapshot does not keep, so awaiting a fan-out chunk by chunk and reading the results at the end threw Job is not executed yet for every chunk but the last. The results now stay in files of the task, result.gcb, or error.gcb for a job that threw, which await no longer loads: it reads the first error only, for the error it throws, and each result() call reads its file, so a large fan-out can be read one result at a time. The files go when the task ends, and result() on a Job kept past it throws. A job that returns an Error without throwing no longer makes await throw. Job has two private nullable fields that locate the result: task_id, and job_id, the job’s number among all the jobs the task awaited. The jobs awaited outside of a task, under greycat test, keep their results the same way, until the process ends.
  • A task id is not handed out again after a crash. The generator is persisted by a commit, so a task that commits nothing, such as a read-only RPC call, left it behind: after a kill -9 the next tasks took ids that Task::history already held, and startup deleted the files/<user>/tasks/<id>/ folders of the tasks that had run. A clean stop now marks the generator it stores as complete, and the next start takes the mark back. A start that finds none, after a crash, raises the generator past the ids of the task folders and of the last records of gcdata/history, and deletes nothing.
  • Task.worker_id is the index of the worker running a task, null while it waits or is suspended in an await, in Task::running(), Task::tasks(ids) and the SSE task frames. Task::running() also fills Task.jobs with the jobs of a task’s await that have not ended, each with its index in the awaited array as its task_id and its own worker_id, so a monitoring panel can show what every worker runs.

Server

  • The world: <name> request header runs an RPC call in that world, main in main; no header runs it in main, whatever the caller’s default world. A name no world has, or an empty one, is answered 400, and a world the caller may not use (Identity::set_worlds) 403, each with a core::Error whose message says which, encoded as the call’s own errors are (GCB or JSON; a JSON-RPC call gets it as the error’s message).
  • The x-gc-world request header is no longer read. It stored its value as the request’s body length, so sent after Content-Length it truncated the body – x-gc-world: 3 cut a JSON-RPC or path-RPC call to its first 3 bytes and answered 400 – and nothing else used it.
  • An upload takes at most 1 MiB off its socket per poll event, then lets the other connections of its http thread have a turn; a peer keeping the socket full used to hold the thread for the whole upload. The loop also stops on a server stop, so an upload in flight no longer delays it.
  • GET /runtime::Task::events streams task events over Server-Sent Events to an authenticated caller. Its first frame, event: connected, carries the id of the stream, and it is the back-channel of the calls that name it: the task of an RPC call that carries the request header sse: <id> and is made by the stream’s owner (a call without the header, or naming another user’s or a closed stream, runs unreported) produces one event: task-started frame when its code starts running (not again when it resumes from an await), event: task-progress frames as it reports steps (one per whole percent), event: task-breakpoint and event: task-resumed when it pauses on a breakpoint and goes on, and one event: task-complete frame when it ends, cancelled or in error included; the data: line is the Task as JSON, or as base64 GCB for a subscriber that sent Accept: application/octet-stream. : ping comments every 15s keep proxies and browsers from dropping the stream, and a subscriber whose token expired, whose identity was removed, or that stopped reading is closed. A stream holds a request pool slot while open, so max_sse (default 2048) and max_sse_per_user (default 16) bound them; past either the caller gets 429. Not available on Windows, where the server cannot park a connection.
  • GET /files/<user>/tasks/<id>/result.gcb?json renders the result as JSON. A string result used to come out without its quotes under application/json.
  • serve writes a root token to gcdata/security/token (mode 0600) next to the root password, before the port opens. It is valid for one year, independent of --validity, and unlike the password is re-minted on every boot. A server that cannot write it refuses to start, like one that cannot write the password.
  • serve no longer risks hanging on stop. Each poll set carries a wake-up (an eventfd on Linux, an EVFILT_USER knote on kqueue) that stopping raises after the stop flag is set; it stays readable, so an http thread or the acceptor returns from its wait whether it was already inside it or about to enter it. Stopping used to rely on SIGUSR1 alone, which does nothing to a thread that checked the flag but had not yet entered its wait: that thread then blocked for good, and the process with it, until some client connected or hung up.
  • Responses answer in the version the request used, and connections follow that version’s default (RFC 9112 9.3): HTTP/1.1 is persistent unless the peer sends Connection: close, HTTP/1.0 only when it asks with Connection: keep-alive. Every response previously claimed HTTP/1.0 while requiring the header on both versions, so curl, Go, Python and every generated SDK client – none of which send it, they rely on the 1.1 default – reconnected on every request. Serving a small static file under wrk went from 164k to 362k req/s as a result.
  • Connection is read as the comma-separated, case-insensitive list it is, so Keep-Alive, Upgrade and a capitalised close are no longer missed.
  • The acceptor no longer spins when the request pool is full. The listening socket is level-triggered, so a poll that found a connection pending kept reporting it: the acceptor took the same event, failed to allocate a slot, and came straight back, holding a core at ~98% for as long as the pool stayed full. Keeping connections alive is what makes that state last, since a slot is then pinned for the whole idle window rather than just for a request.
  • request_pool_capacity defaults to 8192, up from 512. It bounds concurrent connections now that a kept-alive one holds its slot while idle, where it used to bound only in-flight requests – at the old default a few hundred idle clients could make a new arrival wait out the keep-alive window. Costs 320 bytes per slot, ~2.3 MB at the new default.
  • Accepted connections set TCP_NODELAY. A response leaves in more than one write (headers, then a body a static file sends with sendfile), and Nagle held everything after the first segment until the peer acknowledged it. Closing the connection used to flush that tail, so the stall only surfaced once a connection was kept alive – where it cost the peer’s delayed-ACK timer, 40ms on Linux, on every request. Reusing a connection to serve a static file went from 43ms to 0.09ms.
  • Pipelined requests are answered instead of dropped. Bytes of the next request arriving in the same read are held across the recycle and replayed, rather than being discarded with the buffer; because they were already off the socket, the connection used to stall until it timed out.
  • A body is read up to its Content-Length and no further when the next request on a kept-alive connection is already in the buffer. Everything past the headers used to count as the body, so the next request’s bytes reached the call: binary (GCB) arguments were refused with 400, a task was created but failed when it ran, a /files/ upload stored them at the end of the file, Request::body() returned them and broke signature checks, and a query-string call (GET /fn?a=1) was refused for carrying arguments twice. JSON arguments ignore trailing bytes, which hid it.
  • Headers split across two reads resolve to the right body offset. body_off was recorded relative to the scanned window rather than to the buffer, which only agree on the first read of a connection.
  • A kept-alive connection releases the sanitized URI of each /files/ request. It was freed only when the connection ended, so every reuse leaked one copy, and the survivor was freed against whatever length the last request had – a size its block was never allocated with.
  • A JSON-RPC call (POST /, so every MCP request) whose body arrives in a later read than its headers is answered instead of refused. The envelope was parsed from whatever the first read held, and a body still in flight got an empty 400 Bad Request, or a connection reset once the rest landed on a closed socket. Clients that write headers and body separately – Python’s httpx, hence the official MCP SDK and Open WebUI – lost their MCP session on most calls. The body is now waited for, as path-RPC already did.
  • MCP notifications (notifications/initialized, notifications/cancelled, …) are acknowledged with 202 Accepted and no body, as Streamable HTTP requires. They were answered 200 with a -32601 Method not found error carrying no id, a reply JSON-RPC forbids for a notification.
  • MCP tasks/result reads the result of a task from the folder of the user who started it. It looked in the folder of the caller, so an admin asking for the result of another user’s task got unable to find result.gcb.
  • Idle connections are closed at the advertised Keep-Alive: timeout on macOS and FreeBSD. The reaper armed its wake-up with EV_ENABLE on a knote it never registered, so kevent answered ENOENT and the connection was held – with its request pool slot – until the client gave up.
  • /files refuses a path the caller may not reach with 403 when the request is authenticated, and keeps 401 for a request without valid credentials, as RPC does. Every refusal used to answer 401, which a client reads as a lost session. A PUT or DELETE on the files/ root itself answers 403: the DELETE answered 401, and the PUT sent back no valid HTTP response at all.
  • A file is served with the type of its extension, matched whole and in any case. .ndjson and .jsonl, stream files among them, are served as application/x-ndjson, .csv as text/csv, .mjs and .cjs as text/javascript, and .md, .xml, .gif, .webp, .avif, .mp4, .webm and .zip with their own type, where all of them were application/octet-stream. The extension was matched as a prefix, so .jsonl was served as application/json, .jsx as text/javascript and .htmlx as text/html, and an upper case one such as .PNG as application/octet-stream.
  • A PUT to /files whose file cannot be created, such as one whose path is an existing folder, answers 500. It answered nothing: the request bytes were sent back without a status line and the connection hung.
  • A PUT or DELETE to /files leaves the query string out of the file name, as a GET does: PUT /files/alice/x.csv?v=2 writes x.csv. It wrote a file named x.csv?v=2, which no GET could then fetch. On every method, an encoded %3F is part of the name instead of starting the query.
  • The OPTIONS preflight allows DELETE and the Authorization header, so a browser app on another origin can delete a file and send its token. It allowed POST, GET and PUT and the headers *, which does not cover Authorization, so every cross-origin request carrying a token failed its preflight.

Libraries & SDKs

  • New public C stream API, gc/stream.h, which the GCL Stream natives wrap: gc_stream__register (with its flush_size and flush_step_us), gc_stream__find, gc_stream__append and gc_stream__write (which buffer a record), gc_stream__flush (which writes the buffer and syncs the file), gc_stream__last_written_pos, gc_stream__reader, gc_stream__name, gc_stream__path and gc_stream__type.
  • The published lib/std/libgreycat_core.a is self-contained: it now bundles ryu, the float formatter the core calls into, and links with nothing but -lm. It used to leave d2s_buffered_n and d2fixed_buffered_n undefined, so linking it outside the GreyCat build failed – the Rust SDK in standalone mode included. ryu is now compiled as part of the core rather than linked as its own archive. The Rust SDK workspace no longer overrides the linker with clang.
  • greycat codegen ts gives the generated .spawn of every function a trailing optional $taskClass ('small', 'medium' or 'large'), after $g and $signal, to pick the worker class the task runs in: gc.project.import_data.spawn(url, undefined, undefined, 'large'). Existing calls are unchanged. The web SDK passes it on to spawn, and an older SDK ignores it and runs the task in medium.
  • THIRD_PARTY.md lists the third-party code compiled into the runtime, the core archive and the wasm build, with version, license and use: jemalloc 5.4.0, Mbed TLS 3.6.7, NumKong 7.8.5, zip 0.3.15 (miniz 3.1.2), subprocess.h, LMDB, utf8proc 2.12.0, Ryū and fluxsort as dependencies pinned in deps/, piposort, NetBSD strftime and dlmalloc (wasm only) as code vendored in the tree.

8.3

Breaking changes

  • Json<type> / Json<function> (and CSV type/function/field cells) now read and write fully-qualified names; short names such as "MyType" are rejected on parse.
  • A CSV format placing a flattened array field anywhere but in the last column is now rejected at init.
  • files/root/log.csv records no longer carry the unused tag column.

Standard library

  • int::parse(value: String): int and float::parse(value: String): float: strict, typed number parsing that throws on malformed or out-of-range input (unlike parseNumber, which returns any).
  • runtime::Log.time is declared with @format(DurationUnit::microseconds) to match what the log writer emits, so files/root/log.csv can be read back with CsvReader<runtime::Log>.
  • JsonReader and BinReader open an empty file as an exhausted reader (can_read() is false, available() is 0) instead of throwing.
  • CsvWriter creates the file and writes the header on flush even when no row was written.
  • File::sha256 reads through read(2) and reports unreadable or changing files.
  • The %+ time format directive (ISO 8601 with offset) is restored.
var n = int::parse("-1_000");      // -1000
var f = float::parse("-1.5e3");    // -1500.0
int::parse("9223372036854775808"); // throws: out of int range

CLI

  • greycat with no command now runs serve; options without a command imply serve too (greycat --port=8081).
  • greycat doc generates an API reference for the project, delegated to the language tooling.
  • Registry support: --registry / GREYCAT_REGISTRY points install, upgrade and new at a JSON-RPC registry (default legacy, i.e. get.greycat.io), with --registry_token / GREYCAT_REGISTRY_TOKEN for authenticated packages.
  • Registry origin and per-origin tokens can be stored in ~/.greycat/registries (or $GREYCAT_HOME/registries); resolved values are exported to the language tooling, and the token is masked as <set> in -h output.
  • greycat upgrade resolves the lang tooling as a library on an RPC registry.
# ~/.greycat/registries
default = https://registry.example.com
https://registry.example.com = <token>

Runtime & server

  • OpenAPI: --openapi / GREYCAT_OPENAPI (default true) lists every @exposed function in the spec, not only @tag("openapi") ones; paths are filtered by the caller’s permissions, and OpenApi::v3 is now @permission("public").
  • --defrag_ratio default is now 2.0 (blocks held per live block tolerated before a zone is defragged); values in (0, 1.0] are refused at startup, <= 0 disables auto-defrag. Defrag zone selection is improved.
  • Role permissions of every user are refreshed from the program at boot, and the root password is only minted when its file is absent.
  • A store format mismatch is reported explicitly instead of as a raw LMDB error.
  • Task cancellation stops queued and awaiting tasks instead of only flagging them; Task::running and MCP tasks/list list only root tasks.

Libraries & SDKs

  • C SDK: gc_machine__create / gc_machine__destroy, gc_slot__* functions and incremental sha256 (init/update/done) are part of the public API.
  • JS/wasm SDK: the wasm allocator moved from walloc to dlmalloc.
  • std is also published to the new registry.

Fixes

  • Storage: LMDB pinned back to 0.9.70, the on-disk format of existing stores.
  • VectorIndex: reverse links added across transactions are now persisted, and exact duplicates stay reachable once neighbour lists saturate.
  • Time: ISO timestamps written for pre-1970 or small/large years are read back correctly, time <-> Date conversion works over the whole range (including time::min / time::max), west-of-UTC %z offsets are signed correctly, and no signed overflow near time::min.
  • parseNumber no longer wraps integers in [2^63, 2^64) into negative values.
  • CSV: array rows written/read one cell per element, header written with the configured separator, quoted and zero-padded cells kept as text, fields containing " quoted, qualified function names accepted, native types rejected in flatten mode, trailing cells no longer lost.
  • Tasks: suspend handles objects owned through Map keys, non-scalar nodeIndex keys and Table cells; await rollback on failed sub-task fusion; jobs directories cleaned up; poison pill cleared between tasks; snapshots dropped for cancelled/bailed awaits.
  • Core: a String already attached elsewhere is auto-cloned when stored as a nodeIndex value past 32 entries; overwritten Table cells are detached from their block; “already attached” errors name the object; is_instance_of accepts monomorphized types; static natives bind on ABI-rebuilt generic specializations.
  • Function/type name resolution for HTTP, RPC and CLI (greycat run) now mirrors GCL qualified-name rules, with clearer errors for methods addressed by a bare name.
  • CLI: greycat user commands load the program so identity roles resolve; greycat restore no longer crashes when the identity database cannot open.
  • C codegen quotes generated identifiers starting with a digit.

8.2

Breaking changes

  • The HTTP client (Http<T>, HttpRequest, HttpResponse<T>, HttpMethod) moved out of std into the separate http library; add @library("http", ...) to keep using it.
  • Csv::generate, Csv::analyze and Csv::sample are now @permission("admin") instead of open to any api caller.
  • CSV quoting semantics changed: a quoted field is always read as a value (never null), backslashes inside quoted fields are no longer un-escaped, and fields are written verbatim instead of with C-style escapes (see CsvFormat.null_in_quotes to opt back into the old null behavior).
  • Identities whose names differ only by case (e.g. alice / Alice) are now refused when created.

Language

  • @library pragmas declared for the same library by several modules are merged with semver semantics: the highest requested version wins, and a versionless @library("name") defers to any versioned one.

Standard library

  • spawn(job: Job): int enqueues a job in the background and returns its task id, without waiting or merging; the job starts from the last committed graph state and commits independently.
  • CsvFormat.nested with the new CsvNested enum (flatten, json) writes/reads arrays, maps and objects either flattened into columns or as one JSON cell.
  • CsvFormat.null_in_quotes opts back into reading quoted empty fields as null.
  • Crypto::md5 and Crypto::md5hex, plus documentation for all Crypto methods.
  • MCP: prompts/list and resources/list methods (with McpPrompt, McpResource and related types) so clients probing those capabilities no longer get Method not found.
  • File::rename now replaces an existing target on Windows too, matching Unix.
var task_id = spawn(Job { function: rebuild_cache, arguments: [42] });

CLI

  • greycat install is delegated to the language tooling (libgreycat_lang) when available, enabling --bump / --branch; the built-in installer remains as a fallback, and install then hands over to the freshly pinned bin/greycat to run library install hooks.
  • greycat install tolerates libraries that are missing or fail to load, since it is what fetches them.
  • greycat stats gains a MAX column: the largest single block payload per type.
  • Commands that never open the store (version, help, new, codegen, …) no longer create gcdata/ and files/.
  • greycat codegen python (and auto-detection) runs the python library’s codegen hook when @library("python") is present, instead of emitting requirements.txt.
  • The startup redirect to the local bin/greycat is kept even when the path cannot be fully resolved (deep Windows paths).
  • Loading now reports every native fn left without a bound body in one message, instead of crashing on first call.

Runtime & server

  • Multiple webroots: every library shipping a lib/<name>/webroot/ directory is served alongside the project webroot/, with the project one taking precedence on conflicts.
  • HTTP Range / If-Range requests are supported for static files (single range, 206 Partial Content), and HEAD requests are answered like GET without a body (and no longer trigger the files GET hook).
  • New --mcp_content / GREYCAT_MCP_CONTENT (both, structured, text) controls how MCP tools/call ships results; --mcp_instructions / GREYCAT_MCP_INSTRUCTIONS sets the instructions returned in initialize.
  • Lenient argument binding for JSON-bodied calls (MCP, JSON-RPC, path-RPC, task arguments): "null", "None", "nil" or "" bind null for nullable parameters, and a string holding a JSON document is re-parsed for object/array parameters (only after strict parsing failed).
  • Only the first = separates an option name from its value, so free-text options may contain =.
  • Stale task directories whose id is ahead of the store’s task generator (after a crash, wipe or restore) are removed at startup instead of being adopted by new tasks.
  • Task queries (Task::is_running, cancel, …) only match root tasks, not sub-jobs sharing a parent id.
  • Server rejects Windows-hazardous names in client-supplied file paths (:, reserved device names, trailing dots/spaces) on Windows.

Libraries & SDKs

  • Rust SDK rewritten and published as a library: object get/set API, Table, safe Tensor wrapper, time/duration/geo/node helpers, JSON parsing, value equality/hashing, crypto (sha256, HMAC, base64), host handle with logging, tasks and periodic scheduler, and callbacks into GreyCat.
  • Rust: greycat codegen rust emits a native trait per type, native state can be carried on GreyCat types (refcell-style guards), and the new greycat-build crate configures .gclib linking and runs codegen against the pinned runtime; panic=abort is rejected in a .gclib.
  • Native libraries can register install and codegen hooks (gc_program_library__set_install_hook, gc_program_library__set_codegen_hook).
  • C SDK: exposes all buffer APIs, resolved host options, the resolved CA bundle (re-encoded as PEM), and gc_dtz_time__parse_format to parse a time with a custom format.
  • C codegen only emits an enum’s variant macro where its ordinals exist.

Fixes

  • CSV: per-column formats indexed by field footprint rather than physical column; cells whose exponent is followed by garbage or out of range stay text.
  • nodeTime::rangeSize is exact on deep trees and out-of-order series.
  • S3: correct request signing for empty paths and default ports, and S3.host may include a http(s):// scheme.
  • Tasks: suspend/restore of await inside a for-in loop, stale paths corrupting retained nodeIndex<String, _> keys on suspend, aborted transactions left reachable from recycled task slots, and failure paths when the task pool is full.
  • for-in over an invalid source no longer unbalances iterator workspaces.
  • Defrag: rebuilds beside the zone and commits meta before swapping files (fixes multi-store deployments and Windows), excludes every failed zone to avoid livelocks, and reports the LMDB error and zone.
  • greycat restore: explicit archive paths work again, failures are reported with a reason and no longer report success, and descriptors are no longer leaked.
  • HTTP server: request lifecycle races between the TTL reaper and request workers, fd leaks on cached ABI responses, and 503 responses on macOS.
  • Program loading refuses corrupted .gcp/program files whose symbols do not land at their saved offsets.
  • Memory: use-after-free in CA handling, mbedtls allocations charged to the host allocator, UUID generator freed on thread exit, various leaks.
  • Windows: directory tree deletion without nftw, atomic replacing rename, O_BINARY handling.

8.1

Breaking changes

  • Identity::set_role now requires the admin permission over HTTP and returns bool (false if the user does not exist). Setting a role not declared with @role now throws.

Standard library

  • Runtime::on_files_get(handler: function?) registers a callback, called with the file path on every download from /files/. Pass null to disable it.
  • The OpenAPI InfoObject gains summary, description, termsOfService, contact (ContactObject) and license (LicenseObject).
  • Table::init now validates its arguments: negative or out-of-bounds sizes, capacity overflow and out-of-memory raise errors.
  • Identity::set_grants: a non-admin caller can only update their own grants.

CLI

  • New greycat new command scaffolds a project: project.gcl pinned to the latest std, src/api.gcl, a test, .gitignore, AGENTS.md and CLAUDE.md. It refuses to overwrite an existing project.
  • greycat dev watches the project’s .gcl files and logs changes. Each platform uses its native backend (inotify on Linux, FSEvents on macOS, kqueue on BSD, ReadDirectoryChangesW on Windows), with an mtime-polling fallback.
  • greycat stats has been rewritten. It computes in parallel and reports block count, live data size, on-disk size, fragmentation, ABI/meta sizes, and a per-type SIZE/COUNT table. It lists damaged blocks without stopping the walk.
  • The greycat serve startup banner now prints the runtime version and the program version.
  • greycat upgrade shows up-to-date components with the verb “current”, in the same color as “installed”.
greycat new my-project

Runtime & server

  • The built-in public and root users are now set up on every start, not only when the user database is empty.
  • The server exits with an error if it cannot create a worker thread; it no longer continues silently.
  • Library loading errors are clearer: library not found, LoadLibrary/GetProcAddress failures with GetLastError, and link or start returning false.

Libraries & SDKs

  • New Python SDK core artifact (core/sdk/python, _greycat C extension). It passes live GreyCat objects to Python without serialization:
    • box/unbox for primitives and value types (time, duration, geo)
    • wrappers for objects, Array, Map, Tensor (PEP 3118 buffers), Table, and every node type
    • call-scope invalidation of wrappers
    • resolution of native functions by fully-qualified name (FQN)
  • The C codegen marks the library entry point with GC_LIB_ENTRY (now in gc/type.h), so native libraries link on Windows with MinGW/lld.
  • Rust SDK: bindings regenerated; gc_buffer__add_str length widened from u32 to u64.
  • C SDK: the gc_slot_t union fields are reordered so that {}/{0} zero-initialization covers the full 64-bit slot.

Agent skills

  • The bundled webapp skill now prescribes Web Awesome (wa-*) instead of Shoelace and the @greycat/web UI components. It also covers:
    • multi-page apps with Lit shadow-DOM pages and design tokens
    • wire types from greycat codegen
    • a new Playwright end-to-end testing reference
  • Skills document lint/fmt/lsp as greycat commands, improve library discovery, and no longer promote the openapi tag or --user=<name>.

Fixes

  • Storage robustness:
    • fixes for zone reclaim/defrag and block-failure handling
    • reclaimed zones are truncated
    • after a crash, the zone frontier is recovered from the file end instead of a stale header
    • buffer/compressor reads of corrupted blocks are hardened
  • Tasks: an await of jobs that fails part-way now rolls back the sub-tasks it already spawned, instead of leaking task-pool slots. Suspending and restoring a task now handles every node type, Map, Array and Table.
  • Schema upgrades: dropping an object or tuple attribute across an ABI change no longer corrupts or leaks memory.
  • nodeIndex::set now returns the stored value on every code path.
  • nodeTimeCursor now moves correctly on series with fewer than two values.
  • Runtime::backup_full now deletes the right old backups when rotating.
  • Compiler: the '\r' char literal was compiled as '\t'; use-after-free issues fixed.
  • JSON reader: empty arrays are now parsed correctly, and values parsed for skipped Tuple/geo fields are now released.
  • Table: sorting a table with no rows works, and allocation failures in init are handled.
  • Resolving nodes in storage (SBI blocks) now checks the expected type id.
  • HAL: directory handles are closed properly; Windows recursive deletes use rmdir for directories.

8.0

Breaking changes

  • Security model rewritten around Identity: the User, SecurityEntity, UserGroup, UserGroupPolicy, SecurityPolicy, SecurityFields, OpenIDConnect and UserCredential types are removed from runtime.
  • Migration from 7.x: User::login(credentials, use_cookie) becomes Identity::login(login, password) (always sets the greycat cookie); User::current() becomes Identity::current_id(); User::me() becomes Identity::current(); hasPermission becomes has_permission; setPassword becomes set_password; getByName/get become the admin-only get_by_name/get_by_id.
  • User::tokenLogin, User::renew and User::checkPassword are removed. OpenID Connect / Keycloak support is dropped, along with the GREYCAT_OID_PUBLIC_KEY, GREYCAT_OID_CLIENT_ID, GREYCAT_OID_CONFIG_URL and GREYCAT_OID_KEYS_URL options.
  • Users are now stored in an LMDB database (gcdata/security/users.mdb), outside the graph. On first start, public (id 0) and root (id 1, role admin) are created, and a random root password is written to gcdata/security/password.
  • The files permission is removed (the admin and user roles no longer list it). webroot/ is now public. /files/<user_name>/... is readable and writable only by the owner, by admins, or by identities with an IdentityGrant from the owner.
  • webroot/login.html is removed; the built-in webroot/index.html now handles sign-in through /runtime::Identity::login.
  • for ... in no longer supports the sampling, skip and limit clauses, and these words are no longer keywords. Use nodeTimeCursor or explicit counters instead.
  • The packed primitive types t2, t3, t4, t2f, t3f, t4f and str are removed, along with their codegen mappings. Use String, Tuple or user types instead.
  • Plot (Plot::scatter_plot) is removed from util.
  • Task.duration is replaced by Task.completion: time?, and Task gains user_name.
  • nodeTimeCursor is now @volatile (it can no longer be persisted), and its req_time field is removed.
  • ABI protocol bumped to 3: .gcb files, stored program ABIs and binary-protocol payloads from 7.x are rejected. Regenerate SDKs/codegen output, and plan a data export/import when moving a 7.x gcdata/.
  • CLI: the old internal upgrade step, which reloads project.gcp, is renamed update. greycat upgrade now updates the GreyCat install itself (see CLI).
  • CLI/env options are now matched by their exact name; prefixes are no longer accepted. true/false/on/off/1/0 and size suffixes (KB, MiB, …) are also parsed strictly.
  • --user / GREYCAT_USER (impersonation) now takes a user name instead of a numeric id.
  • RuntimeUsage is no longer collected automatically by serve/run; call RuntimeUsage::collect() yourself if needed.
  • A float literal can no longer be used as a time literal: 1000.5_time is now a compile error.

Language

  • Map keys may now be any object. User-type keys are hashed and compared by value (deep equality), not only primitives and String.
  • The compiler is hardened against malicious or pathological .gcl input (deep nesting, oversized literals).

Standard library

  • New Identity API: current_id, current, get_by_id, get_by_name, all, create, token(id, ttl), login, logout, permissions, has_permission, set_password, set_role, set_grants.
  • New IdentityGrant / IdentityGrantType (read, write, read_write, none) control cross-user access to /files/.
  • Runtime::on_files_put(handler) registers a function that is called with the path of every file uploaded with HTTP PUT to /files/.
  • Task::live(ids) returns whether each task is still active; Task::tasks(ids) fetches several tasks at once. Task::is_running is no longer @reserved. Task lookups honour the caller’s grants.
  • New Uuid::v4() / Uuid::v7() produce cryptographically strong UUIDs; Uuid::v7() is time-sortable.
  • New Random.uuid() / Random.uuid_v7() produce seedable, reproducible UUIDs.
  • geo: new bearing(value), geo::distance_to_segment(point, a, b) and geo::meters_per_deg_lon(lat).
  • GeoPoly: new bbox(), is_closed(), perimeter(), area(), centroid(), mean_centroid() and interpolate(step), all antimeridian-safe.
  • GeoBox: new GeoBox::from_point(center, radius), intersects, union, intersection, expand, center, width, height and split(rows, cols).
  • nodeGeo.search(center, max) returns the nearest entries as SearchResult<geo, T>.
  • New atan2(y, x) math function.
  • TensorDistance gains the hyperbolic metrics lorentz and poincare.
  • ProgressTracker gains speed_smoothed (an exponential moving average of per-update speed), a configurable smoothing (default ProgressTracker::DEFAULT_SMOOTHING = 0.1) and a steadier remaining estimate.
  • HttpRequest.max_response_size caps the response body size. The HTTP client now decodes chunked responses and follows up to 5 redirects (301/302/303 downgrade to GET; 307/308 keep the method and body).
  • Gaussian.add, addx and add_gaussian now return bool and reject NaN/infinite values. int::to_string(value, thousands_separator) now declares its String return type.
  • PeriodicOptions.start / immediate are fixed: a future start delays the first run only when immediate is false.
fn upload_hook(path: String) {
    info("uploaded ${path}");
}

fn main() {
    Runtime::on_files_put(upload_hook);
    var me = Identity::current();
    println("${me.name} (${me.role}) id=${Identity::current_id()} uuid=${Uuid::v7()}");
}

CLI

  • New greycat user command (list, show, add <name> [role], remove|rm, role <name> <role>, grant|revoke <name> r|w|rw <target>) manages the LMDB user database offline.
  • New greycat dev command: it runs serve and spawns a frontend watcher (vp build --watch, falling back to pnpm/npx vite build --watch). Use --with=<cmd> / GREYCAT_WITH for a custom command.
  • New greycat lint, greycat fmt and greycat lsp commands delegate to the language tooling library (lib/lang/ in the project or ~/.greycat/lib/lang/), forwarding all arguments.
  • New greycat upgrade [branch|version] updates the global GreyCat install (default branch stable) and refreshes the language tooling. Options: --force, -y/--yes, --with_lang, --lang=<version>. install and upgrade never redirect to a project-local binary.
  • greycat test prints an end-of-run summary of failures and reports per-module memory leaks.
  • greycat serve/run take an exclusive gcdata/lock, so two processes cannot open the same store.
  • The license is again looked up in $HOME/.greycat/license as a fallback, on every platform.
greycat user add alice            # role defaults to "user"
greycat user grant bob r alice    # alice may read /files/bob/...
greycat upgrade -y                # update the global install to latest stable
greycat dev --with="vp build --watch"

Runtime & server

  • Backups (backup_full/backup_delta) and greycat restore now include the user database. Restore with verification is fixed.
  • MCP endpoints (initialize, tools/list, tools/call, tasks/*) are reachable with public permission. tools/list only returns tools the caller may call, and MCP task access honours per-user grants.
  • /files/ listing and uploads are reworked around per-user directories and grants. Admins can access every directory.
  • Task suspension (await) snapshots are streamed to disk, so very large stacks or objects can be suspended and resumed.
  • Workers now withstand memory accounting drift instead of aborting. Hard stop (Ctrl-C) no longer hangs on internal locks.
  • Buffers and IO paths now use 64-bit sizes, so payloads, files and backups larger than 4 GiB are handled.
  • Bad JSON bodies for Http<T> responses, enum values and nullable function parameters now produce clear errors.

Libraries & SDKs

  • greycat codegen ts emits plain function signatures again, instead of ExposedFn wrappers. The Java and Python generators drop the removed tuple/str types.
  • The C SDK exposes more public API: scheduler functions in gc/host.h, node essentials in gc/node.h, gc_log__machinef, and allocator getters for machine/host. The Rust SDK is regenerated for the new allocator API and gains alloc, log, machine, object, program and slot modules.
  • Native libraries are no longer loaded with RTLD_DEEPBIND.
  • GreyCat AI-agent skills (skills/SKILL.md plus references covering syntax, stdlib, CLI, runtime, webapp and more) now ship with the repository and the Rust SDK package.

Performance

  • New memory allocator architecture: every allocation goes through explicit allocators, and jemalloc replaces the process malloc (except on Windows).
  • Interpreter fast paths for arithmetic and frames, flattened test opcodes, and direct SBI dispatch from the machine.
  • Array storage is now sized in powers of two, and Map is reworked.
  • Tensor casts and distances use NumKong SIMD kernels more widely.

Fixes

  • Hardened parsing of untrusted input across CSV readers and writers, JSON, the binary reader, GCB, XML (vendored sxml), time/date (ISO-8601 range validation) and duration overflow.
  • HTTP client: fixes for SSL want_read, HEAD responses and buffer truncation. SMTP: cc/bcc delivery, the Bcc header, MAIL FROM and SSL retry.
  • Fixed nodeTimeCursor, removeAll, nodeIndex inheritance, Array sort with abstract element types, deep clone, and node<String> wrapping of an already-persisted string.
  • Fixed bugs in Gaussian, Histogram and Tensor, and float::to_string output.
  • Windows: fixed a boot loop when the project path contains spaces. File copy and sha256 of empty files are hardened.
  • Security hardening: CSPRNG-generated passwords and keys, key files written with O_TRUNC, stricter permission checks in the file server, and caller checks on Identity::token and set_password.
  • Broad stability pass over leaks, ASan/UBSan findings, corrupted-GCB handling, defrag and restart paths.

7.8

Breaking changes

  • JSON serialization of field, function and type values is now FQN-based strings (e.g. "mod::Type::field", "mod::fn") instead of numeric program offsets; Json parsing accepts the same strings (bare names are accepted for functions and types). The TS and Java codegen are updated to match.
  • Csv::analyze now takes paths: Array<String> instead of files: Array<File>.
  • The VM stack now has a fixed size (1 MiB of slots) instead of growing up to 64 MiB. Very deep recursion now fails earlier with a stack overflow runtime error.
  • Periodic tasks now run immediately when scheduled (PeriodicOptions.immediate defaults to true). Set immediate: false to keep the old behavior of waiting for the first period.
  • The session cookie no longer carries the Secure attribute (Path=/; SameSite=Strict; HttpOnly), which fixes login on Safari.

Standard library

  • New RuntimeUsage type (OS and process memory, global_memory, memory_drift, per-worker and per-zone usage) and new Runtime::usage() endpoint (debug permission).
  • Runtime usage is now sampled periodically (see GREYCAT_USAGE_STEP) and stored in a private nodeTime<RuntimeUsage> history.
  • New Task::no_history(v: bool) to exclude the current task from the task history.
  • New PeriodicOptions.immediate: bool? to choose whether a periodic task runs as soon as it is added.
  • New Runtime::sbi_tree(node) to inspect a node’s internal block relations as Array<Tuple<int, int>>.
  • Json parsing now honors @format on fields: custom date patterns for time (with the field’s time zone), and DurationUnit scaling for numeric time and duration values.
  • Json parsing of abstract types now resolves concrete subtypes through the _type discriminator, including multi-level hierarchies (e.g. parsing as Json<LandVehicle> where Car extends LandVehicle extends Vehicle).
  • JSON Schema (SchemaObject) gained pattern. Node types, type, function and field values now get proper string schemas, and abstract types are listed with their concrete subtypes.

CLI

  • greycat run <fn> [args...] now forwards extra CLI arguments as function parameters (parsed as JSON; String/char parameters are quoted automatically; trailing nullable parameters can be omitted).
  • greycat run now prints the function’s return value. Output is controlled by --format (json or gcl) and --pretty, which now defaults to true.
  • Unqualified function names (in greycat run, greycat test, path-RPC and JSON-RPC) now resolve to the unique non-private function with that name in any module, not only in project. If the name is ambiguous, greycat run reports an error that lists the candidate FQNs.
  • greycat restore gained --verify (GREYCAT_VERIFY) to check every block of the backup while restoring, and now shows a progress indicator (percentage and bytes).
  • New option --usage_step / GREYCAT_USAGE_STEP (default 10s) for run and serve: the interval between runtime usage samples.
  • New option --worlds / GREYCAT_WORLDS (default 1): groundwork for many-worlds exploration.
greycat run mymod::add 1 2
greycat run greet "alice"
greycat restore --verify

Runtime & server

  • MCP: support for task-based async execution (tasks/get, tasks/result, tasks/list, tasks/cancel). tools/call accepts a task parameter, tools advertise execution.taskSupport: optional, and new types include McpTask, McpTaskStatus and McpTaskSupport.
  • MCP tool schemas now describe abstract/concrete type hierarchies and type/function/field parameters properly.
  • New memory accounting across workers, object caches and zone binary caches, used by Runtime::usage().
  • Clearer log messages when a store zone fails.
  • Linux release binaries are now built on AlmaLinux 8, lowering the glibc baseline. Raspberry Pi builds are supported.

Libraries & SDKs

  • Native C SDK: new gc/node.h with gc_node__resolve(), plus gc_program_type__extract_field_format() / gc_program_type__extract_format_pragma_arg() for reading @format metadata.

Performance

  • Tensor distance kernels (TensorDistance) now use NumKong instead of SimSIMD.
  • Faster worst-case line splitting in readers (memchr-based).

Fixes

  • Arithmetic: fixed duration multiplication and division with int/float operands.
  • MultiQuantizer::bounds no longer crashes when the return type is not generic (#1369).
  • JSON Schema for abstract types and node types (#1368).
  • HTTP: fixed an object leak and a memory error when listing directories as JSON under /files. The built-in index.html is now served correctly.
  • greycat restore: fixed progress reporting during recovery. Program files are now synced and closed.
  • macOS: skip loading keychain CA certificates when a debugger is attached, which avoids crashes under lldb.
  • Fixed the wasm build (object handling).

7.7.190-stable

Features

Native Function Nodes

  • Added support for native function nodes

WebAssembly (WASM) SDK

  • New JS/WASM SDK with walloc allocator and custom std wrapping
  • WASM build & test pipeline
  • Rewrote JSDoc to .d.ts type definitions
  • Simplified code generation
  • Restored time helpers for sdk/web
  • Improved ABI API and function loading
  • Removed debug console.info from WASM importObject

Rust SDK

  • Initial setup and base for Rust SDK
  • Moved Rust SDK to core/sdk/rust, WASM moved to core/sdk/js

Complex Numbers

  • Added complex number operations to core
  • Generalized complex API usage
  • Naming consistency: arg, abs

Progress API

  • New API for progress reporting

Old:

var progress = ProgressTracker {
    start: time::now(),
    total: max_steps,
};
for (var i = 0; i < max_steps; i++) {
    progress.update(i);
    Task::progress(progress.progress ?? 0.0);
    // ...
}

New:

Task::expected_steps(max_steps);
for (var i = 0; i < max_steps; i++) {
    Task::add_steps(1);
    // ...
}

Environment

  • Added System::get_all_envs() for server admins
  • .env improvements (quoted values, comments, etc.)
  • CLI flags and environment variables now support: byte and duration
GREYCAT_VALIDITY=30day greycat serve --cache=20GB

Authentication & Security

  • Direct login support for root via URL
  • New greycat token --user=<int> --validity=<duration> command to generate tokens
TOKEN=$(greycat token --user=1 --validity=120day)
  • Fixed ACL basic rules for security entities and set password

Core

  • ABI tests for WASM + standalone
  • Default cache set to 75% of total available memory
  • Reflection support to list all types in the program (type::all())

Infrastructure

  • AlmaLinux 8, 9, 10 with rustup/cargo and Node.js
  • Adopted Ryu for float-to-string conversion

Bug Fixes

  • JSON: Fixed map export with node refs
  • Duration: Fixed binary operand handling
  • Environment: Fixed duration-related env options, munmap, skip empty values
  • mvar: Fixed wrong realignment
  • ABI: Fixed ABI upgrade
  • Stats: Type IDs now sourced from ABI, not program
  • Tensor: Renamed reel → real
  • Time zones: Fixed time zone usage
  • Compilation: Protected against compilation issues and wrong assignment
  • C++ compatibility: Multiple fixes
  • DLL export: Fixed DLL export issue
  • UTF-8: Proper UTF-8 conversion support
  • Fixed issues #1364 and #1365
  • GCC patch for compatibility
  • Fixed duplicate
  • Docker: Stable Node.js download links

Housekeeping

  • Login page HTML cleanup
  • Various cleanups, formatting, and CI fixes

7.0

Equality Check (==) for floats

Floats are now checked for strict equality instead of an epsilon based check

var x = 1e-8;

Assert::equals(x, 0.0); // False in v7 , True in v6

If you want the old behavior use:

Assert::equalsd(x, 0.0, 1e-8); // The following check will throw if false
// OR
if(abs(x - 0.0) < 1e-8){}

File

File::taskDir() is deprecated replaced by File::workingDir()

Anonymous Objects

The following is no longer allowed, a lang fix is available that will generate the type automatically for you.

var foo = { bar: "hello };

removed columns from CsvFormat

CsvColumn and all it’s associated types have been removed in favor of the new typed reader.

CsvFormat has new attribute called format and tz to pass the date custom format and timezone if the csv doesn’t have an iso8061 date

In case you have multiple dates in different formats you can specify each format individually

type CsvLine {
  date1: time; // will parse as iso8601
  @format("d/m/Y")
  date2: time; // will parse using the custom format
}

Permissions

The new default permissions are

Permission Description
public Default, associated with anonymous users.
admin Allows to administrate anything on the server.
api Allows access to exposed functions.
debug Allows access to low-level graph manipulation functions.
files Allows access to files under /files/ & /webroot/ according to ACL.

The new default roles are

Role Permissions
public public
admin public, admin, api, debug, files
user public, api, files

To create a new Permissions

@permission("foo", "The description for my custom permission");

To create a new role

// The first value is the name of the role, the rest are its permissions
@role("custom", "foo", "api");

To edit an existing role, you can only add new permissions to a default role

@role("user", "debug");

Change to module variables

Non **node** module variables are not supported anymore

The advantage of this no need to optionally create a nodeIndex / nodeTime / nodeList / nodeGeo anymore with “?=”, they will be automatically created for you, this is only valid for the specified node indexes in module variables (outside functions) and not valid for node

var s: Map?; // wrong
var s2: int // wrong
var s3: nodeIndex<String, String>; // correct
var s4: node<int?>; // correct
fn main(){
  s3.set("hello", 42); // s3 already exists
}

multi write

Greycat can now write in parallel, this comes with a few limitation.

The same node can’t be edited in two separate jobs running in parallel this will cause the final commit to fail saying that it failed to merge.

You have to either in a pre or post step update your global indexes.

@write deprecated

There are no more distinction between read and write function, every function can be both, the write check is made at the commit phase meaning when the function ends.

If you relied on the @write to make write permission checks, it’s better to now add @permission(“graph.write”) to error out early, instead of error at the end the function.

New perf log (replacing debug)

//TODO give an example

perf level enable any part of the code to emit a performance related log to trace user defined KPI

Use cases

  • line per seconds
  • bytes per seconds

New typed Readers

type CsvReader<T> {
  fn read(): T;
}

type CsvLine {
  a: String;
  b: String;
}

fn main(){
  var reader = CsvReader<CsvLine>{path : "file.csv"};
  reader.read(); // return now type CsvLine! both in lang and runtime
}

Every GreyCat reader is an iterator, calling read() on it will yield a record and advance the internal cursor pos.

while (reader.can_read()) {
  var record = reader.read();
  // do something with the validated record
}

For more information read the dedicated pages for each reader in the io section

Protected field name

We now support complex field name to match JSON capability

type A {
    "my field &": String;
}

fn main(){
  var a = A {
     "my field &": "hello"
  };
  pprint(a."my field &"); // pretty print
}

New @volatile pragma

To facilitate the new Type everything philosophy and alleviate upgrades a new pragma for types has been introduced

This is primarily intended to only be used for frontend GUI, intermediate parsers (Csv, Json, Text), anything that is not to be stored on the graph.

Do not Store a volatile type in the graph
@volatile
type Foo {
  bar: String;
}
@volatile
type Foo {
  /* ... */
  baz: String; // new non null field, No need to remove the state anymore
}

New auto generated directories

There a few new directories generated with greycat now along side /gcdata.

As previously mentioned , /bin and /lib , as well as a /files directory will be auto generated.

The files directory contains a global log file (all logs will now be printed inside here even logs created from inside tasks) and all tasks result will be located here.

Tasks

v7 comes with several changes on how tasks are used, a new Task API and 2 new concepts: Job, await

Firstly, the keyword task has been removed; now every function can be spawned as a task!

On v6

fn run_task() { // cannot be an entrypoint
  Task::spawn(my_task, my_args); // spawns and forget
}

on v7

fn main() {
  var jobs = [Job { function: my_task }]; // describes a list of jobs
  await(jobs); // actively awaits jobs' completion
  Assert::equals(jobs[0].result(), 42); // lazily evaluates a Job's result
}

fn my_task() {
  return 42;
}

Functions (here, main) are now always run in a task context, eg. an asynchronous context

Asynchronicity allows us to enable await, which will pause the current task while awaiting for its jobs to complete

A Job also provides a way to retrieve the result of its execution (its returned value) via job.result()

await will throw if at least one job has raised an exception, you can try/catch it and iterate over the job’s result to find the error

Creating a Job does nothing on its own, it is just a GreyCat object, await-ing jobs schedules them for completion (which, if the queue is full, might be long)

Tuples short notation

Tuple now can be instantiated with a short notation such as the following:

var t = ("my_name", {v: "content"});

Meta Programming

The v7 introduces as full new meta programming API. Type pointers can now be used to compare and introspect values for fields or enumerations.

Core module method sameType(x,y) is replaced by type::of(x) == type::of(y)

Similarly, the method valueEnum is replaced by type::enum_value. The signature:

native static fn enum_value<T>(enum_type: typeof T, offset: int): T;

Illustrates the new usage of generic type to keep typing til the result of the meta fetch methods.

Enumerations can also be introspected using the following method to get the number of available values or directly values.

core::DurationUnit.enum_values();
core::DurationUnit.nb_enum_values();

Same approach exists for field in object.

type type {
    /// Return the number of field of this type
    native fn nb_fields(): int;
    /// Return all field descriptor from this type
    native fn fields(): Array<TypeField>;
    /// Return field offset or null from this type and given a field name
    native fn field_by_name(name: String): int?;
}
type Sensor {
  name: String;
  value: float;
}
fn main(){
  project::Sensor.fields();
  var offset_name = project::Sensor.field_by_name("value"); // return 1
  var sensor = Sensor {"sensor_1", 42.5};
  println(type::field_get(sensor, 0)); // print sensor_1
  println(type::field_set(sensor, 1, 142.5));
}

Type pointer usage for dynamic mapper

Newly introduced type pointer can now be used within Map together with function pointer to create dynamic mapper. Please find below an example of usage:

type MyType {}
fn main(){
    var m = Map<type, function>{};
    m.set(project::MyType, fn(t: MyType){  println(t); });
    var obj = MyType{};
    var f = m.get(type::of(obj));
    info(f(obj));
}

New keyword: typeof

Function with generic parameter can now map generic parameter with the type passed as parameter. This enables the use of dynamic type mapper such as in parser for instance.

native fn parse<T>(str: String, s: typeof T) : T;

Generic function

Function can now have a generic parameter allowing to specify return type according to parameter type for instance.

native fn clone<T>(v: T): T;

Type pointer

Any GCL type can now be reference by a qualified name

/// project.gcl
type Sensor {

}
fn main(){
  var sensor_type: type = project::Hello;
  println(sensor_type);
}

Precision Decorator

Float values can now be decorated by an annotation @precision(0.01). The annotation value defined the expected precision kept by GreyCat by the dev. This simple annotation can compress by an order of magnitude the size of GreyCat store.

Private type attribute

Keyword private is now available to protect type attributes from external mutations. The following example illustrates the semantics associated to the keyword.

Essentially, only local methods are allowed to mutate private attributes.

type Foo {
  private x: int;

  fn set(x: int) {
    this.x = x; // correct
  }
}

fn main() {
  var foo = Foo { x: 5 };
  foo.set(100);
  Assert::equals(foo.x, 100); // allowed to read private attribute

  foo.x = 42; // this will raise an error
}

New command: install

A new command install is available in the greycat CLI

This command will download the required libraries and greycat version defined in the project.gcl:

@library("std", "7.0.1304-dev"); // greycat core version
@library("useragent", "7.0.22-dev");  // user agent parser library

Before the first greycat run (or serve), user should run:

greycat install

It should output something like:

Download: https://get.greycat.io/files/core/dev/7.0/x64-linux/7.0.1304-dev.zip
Extracted: /home/max/Projects/analytics/bin/greycat
Extracted: /home/max/Projects/analytics/lib/std/core.gcl
Extracted: /home/max/Projects/analytics/lib/std/io.gcl
Extracted: /home/max/Projects/analytics/lib/std/runtime.gcl
Extracted: /home/max/Projects/analytics/lib/std/util.gcl
Extracted: /home/max/Projects/analytics/lib/std/include/greycat.h
Extracted: /home/max/Projects/analytics/lib/std/include/greycat.def
Download: https://get.greycat.io/files/useragent/dev/7.0/x64-linux/7.0.22-dev.zip
Extracted: /home/max/Projects/analytics/lib/std/core.gcl
Extracted: /home/max/Projects/analytics/lib/std/io.gcl
Extracted: /home/max/Projects/analytics/lib/std/runtime.gcl
Extracted: /home/max/Projects/analytics/lib/std/util.gcl
Extracted: /home/max/Projects/analytics/lib/std/include/greycat.h
Extracted: /home/max/Projects/analytics/lib/std/include/greycat.def
Extracted: /home/max/Projects/analytics/lib/useragent/useragent.gcl
Extracted: /home/max/Projects/analytics/lib/useragent/useragent.gclib

As you can see the greycat executable is now located locally in the bin directory,

All previous scripts using the global greycat path should instead use the local one

The libraries are located in a local lib directory

New command: print

A new command print is available in the greycat CLI

This command prints to stdout the content of the given GreyCat binary file (.gcb)

greycat print result.gcb

A --format=json flag can be specified to have it output JSON instead:

greycat print --format=json result.gcb

Removed module: math

Module math as been removed from the std library.

All math functions are now directly accessible from the core module.

The core module being always available in scope, it does not require any additional use statement now to use the old math symbols.

Updated types: Date, time

Date is no longer a native type, it is now designed for human presentation, while time is designed for calculations.
This means that most of the v6 Date API has been moved to time.

API Breaking changes

v6 v7
Date::new(): Date Date {year: , month: , ...} normal object creation
Date::fromEpoch(): Date Date::fromTime(time, TimeZone)
Date::toTime(): time Date::toTime(TimeZone): time
Date::add() removed, time::calendar_add(value, CalendarUnit, TimeZone)
Date::substract() removed, time::calendar_add(value, CalendarUnit, TimeZone) negative value
Date::floor() removed, time::calendar_floor(CalendarUnit, TimeZone): time
Date::ceiling() removed, time::calendar_ceiling(CalendarUnit, TimeZone): time
Date::startOfWeek() removed, time::startOfWeek(TimeZone): time
Date::dayOfYear() removed, time::dayOfYear(TimeZone)
Date::dayOfWeek() removed, time::dayOfWeek(TimeZone)
Date::weekOfYear() removed, time::weekOfYear()
Date::leapYear() removed
Date::isLeap() removed, time::isLeap(year: int)
Date::daysInYear() removed
Date::daysInMonth() removed
Date::totalDaysInYear() removed, use time::totalDaysInYear(year)
Date::totalDaysInMonth() removed, use time::totalDaysInMonth(month, year)
Date::startOfWeek() removed, use time::startOfWeek(Timezone):time
Date::endOfWeek() removed, use time::endOfWeek(Timezone):time
Date::set() removed, either set field, or use time methods (safer)
Date::setTimeZone() removed, timezone is not stored
Date::getTimeZone() removed, timezone is not stored
Date::shiftTimeZone() removed, timezone is not stored
Date::clone() removed
Date::toString() removed
Date::get(DatePart::year),… removed, use Date.year, Date.month, etc.
Date::year(), … removed, use Date.year, Date.month, etc.
Date::diff() removed, use an operation eg. date1.year - date2.year, or use times
Date::equals() removed, use an operation eg. date1.year == date2.year, or use times
Date::min(), max() removed, use time:min(), time::max()
time::toDate(): Date removed, Date::fromTime(time, TimeZone): Date
time::toDateUTC(): Date removed, Date::fromTime(time, TimeZone): Date
Duration::years removed,
Duration::months removed,

The new CalendarUnit replaces DatePart:

enum CalendarUnit {
  year(0);
  month(1);
  day(2);
  hour(3);
  minute(4);
  second(5);
  microsecond(6);
}

Updated type: Quantizer

Quantizer is no longer a native type, its configuration is set at object creation.

The configuration per dimension is set with the appropriate type: QuantizerSparseDim and QuantizerDenseDim. Their use is unchanged from v6.

API Breaking changes

v6 v7
Quantizer::configure(dim: Array<any>): Quantizer see below
Quantizer::new(): Quantizer Quantizer {dimensions: Array<QuantizerDim>} normal object creation
Quantizer::dimensions(): int removed, available from dimensions field

6.10

  • migrate mbedtls to 3.6.0
  • add support for Google OAuth2 SSO
  • add protection and raise error for modification of nodeX while iterating over it
  • add support for Key rotation of SSO such as Azure AD
  • add support for Linux AArch64 platform and glibc binaries
  • add executable signature for Apple related binaries
  • add executable signature for Windows related binaries
  • add skeleton for Apple .app
  • cleanup STD GCL files according to newly introduced LSP server with better nullable checkes
  • enable support for weakly formed CSV with nullable empty fields
  • fix http get support for server that do not add binary len property
  • fix ABi protocol for broken Array message
  • fix codegen for SDK Java to allows unaligned version of ABI server and generated code
  • fix support for breaking statement within for-in loops
  • fix rangeSize in nodeTime and nodeList
  • fix CSV support for trailing null values

5.x to 6.x

  • Installation Install GreyCat v6 by visiting here, then:
  • Rename: ~/.greycat/licence to ~/.greycat/license (notice that ‘c’ becomes ‘s’)

API breaking changes

v5 v6
Env::get System::getEnv()
Table::new(2, true) Table::new(2)
Table<T>::new(8, true) Table<T>::new(8)
CSVColumnString CsvColumnString
CSVColumnFloat CsvColumnFloat
CSVColumnInteger CsvColumnInteger
CSVColumnTime CsvColumnTime
CSVColumn CsvColumn
CSVFormat::new() CsvFormat {}
CSVFormat CsvFormat
BoxPlotF64 BoxPlotFloat
PCA::new() PCA {}
GaussianND::new() GaussianND {}
Tensor::new() Tensor {}
Gaussian::new() Gaussian {}
HistogramF64::new() HistogramFloat {}
HistogramI64::new() HistogramInt {}
gaussian.min() gaussian.min
ProgressTracker::new(); p.start(); ProgressTracker { start: time::now() };
p.steps() p.counter
p.throughput() * 1000000 p.speed (speed now is counter / s before it was counter / us)
p.duration() p.duration
.bin extension renamed to .gcb
@library("network"); no longer needed, lib network has been merged in std::io
Directory::new File::mkdir
Directory::open FileWalker::new
FileWriter::new("${filename}.json", false) JsonWriter::new("${filename}.json")
FileWriter::new("${filename}.bin", false) GcbWriter::new("${filename}.gcb")
fw.writeBin(metaTypeIndex) fw.write(metaTypeIndex);
File::open has different semantic now, it returns a file descriptor and not a reader. To read we need a specific type based on the format (eg. JsonReader, CsvReader, etc.)
File::open("${filename}.bin"); GcbReader::new("${filename}.gcb")
file.readBin() file.read()
file.close() file = null;
File::open(csvPath) csvFile = CsvReader::new(csvPath, csvFormat)
csvFile.read(csvFormat) csvFile.read() Ideally replace to csvFile.readTo(obj)
csvFile.name() doesn't exist anymore
csvFile.path() path
use http;, use smtp; use io;
JSON::parseFile(file) JsonReader::new(file.path).read()
gaussian.clear() gaussian = Gaussian{}
var clonedGaussian = gaussian.clone() var clonedGaussian = clone(gaussian)
Task::spawn("module.function", [params]); Task::spawn(module::function, [params]);
SmtpAuthMode SmtpAuth
SmtpConfig {...}; Smtp {...};
Smtp::send(...); var smtp = Smtp {...}; smtp.send(...);
Email {..., contentType: EmailContentType::html, ...} Email {..., body_is_html: true, ...}
@public @permission("public")
  • File iterator

    • Old v5 way:
    for (i, entry in dir) {
      if (entry is Directory) {
        // entry is dir
      } else if (entry is File) {
        // entry is file
      }
    }
    
    • New v6 way:
    var fw = FileWalker::new(path);
    if (fw != null) {
      while (!fw.isEmpty()) {
        var entry = fw.next();
        if (entry != null) {
          if (entry.isDir()) {
            // entry is dir
          } else {
            // entry is file
          }
        }
      }
    }
    
  • Update webpack proxy middleware

module.exports = (app) => {
  app.use(
    // ensures all non-GET requests are sent to GreyCat instead of Webpack-dev-server
    createProxyMiddleware((path, req) => req.method !== 'GET' || path.match('^/files'), {
      // your GreyCat endpoint URL
      target: greycatProxy,
    }),
  );
};
  • Get files (JavaScript) See source

  • Upload files (JavaScript) See source

  • Download files in the browser (JavaScript)

const link = document.createElement('a');
link.download = 'myFile.csv'; // the name of the downloaded file in the browser
link.href = '/files/path/to/myFile.csv'; // the actual path to download the file on GreyCat
document.body.appendChild(link);
link.click(); // triggers the download
document.body.removeChild(link);
  • Download file from task result
export const downloadResource = async (task: runtime.Task) => {
  fetch(`/files/${task.user_id}/tasks/${task.task_id}/result.gcb?json`)
    .then((res) => {
      return res.text();
    })
    .then((data) => {
      const fileName = data.trim();
      const link = document.createElement('a');
      link.download = fileName;
      const url = new URL(
        `/files/${task.user_id}/tasks/${task.task_id}/${fileName}`,
        window.location.origin,
      );
      link.href = url.toString();
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    });
};
  • Task API
const task = await myModule.myTask();
const handler = new TaskHandler(task);
const info = await handler.start(pollingDelayInMs, (info) => {
  // if you want to show progress somewhere
});
// getting files is using the standard "files" API
const result = await greycat.default.getFile(`${task.user_id}/tasks/${task.task_id}/result.gcb`);
const args = await greycat.default.getFile(`${task.user_id}/tasks/${task.task_id}/arguments.gcb`);
  • New Enums

Get an Enum by accessor method:

<module>.<enum>.<enumAccessor>()

e.g.:

const role = tenant.TenantRole.superadmin();

Get an Enum by string:

<module>.<enum>[<string> as <module>.<enum>.Field]()

e.g.:

const field = "superadmin"
const role = tenant.TenantRole[field as tenant.TenantRole.Field]();

Compare Enums Just compare them directly, no need to use value nor field (now called key).

  • New concepts
  • File now is just the file descriptor (name, path, size etc)
  • For every specific format, there is dedicated class for example: JsonReader, CsvReader etc.
  • You can get specific file paths by calling these static methods:
    • File::baseDir() translates to "./gcdata/files/"
    • File::userDir() translates to "./gcdata/files/<user_id>/"
    • File::taskDir() translates to "./gcdata/files/<user_id>/tasks/<task_id>"

where <user_id> and <task_id> are contextual.

  • Code generation

    v5 v6
    greycat-lang gen -o ./src/greycat greycat codegen project.gcl ./src/greycat
  • Styling if using MUI

@import '@greycat/web/css/greycat.base.css';

else

@import '@greycat/web/css/greycat.css';
  • Breaking changes

Can not spawn Tasks from a spawned Task , only one level possible.

Progress tracker in Task is disabled

All usage of structuredClone need to be changed.