In this page
- 8.4
- 8.3
- 8.2
- 8.1
- 8.0
- 7.8
- 7.7.190-stable
- 7.0
- Equality Check (==) for floats
- File
- Anonymous Objects
- removed columns from CsvFormat
- Permissions
- Change to module variables
- multi write
- @write deprecated
- New perf log (replacing debug)
- New typed Readers
- Protected field name
- New @volatile pragma
- New auto generated directories
- Tasks
- Tuples short notation
- Meta Programming
- Type pointer usage for dynamic mapper
- New keyword:
typeof - Generic function
- Type pointer
- Precision Decorator
- Private type attribute
- New command: install
- New command: print
- Removed module: math
- Updated types: Date, time
- Updated type: Quantizer
- 6.10
- 5.x to 6.x
Core Changelog
8.4
Concurrency
enum MergeStrategy { fusion_by_key_fail_on_conflict, fusion_by_key_last_win, no_fusion_fail_on_conflict }andTask::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_winkeeps the value the task wrote, key by key: a field, a key of aMap, a node’s slot, or a node key of anodeIndexthe task changed differently from the commit before.no_fusion_fail_on_conflictfails the commit, or the merge of the jobs of anawait, 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
Mapmerges key by key when two tasks change it concurrently, as a plain object merges field by field: writes to different keys all land, and aMapvalue that is a plain object merges field by field under its key. Writes to the same key still conflict. Before, a concurrent change of anArrayor aMapattribute was silently overwritten by the last commit, and now conflicts as anintattribute 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_aliveon, which is now the default. Identity::createon 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 tocreatelet an anonymous caller lock any account out,rootincluded./filesrefuses a path that starts with/after the/files/prefix, such as/files//etc/hostnameor/files/%2Fetc/hostname, with404. AGETopened it as an absolute path, so an admin token could read or list any file the server process can read./filesrefuses a path with a.segment followed by more path, such as/files/./alice/data.csv, with404. The first segment names the owner of a path, so an identity named.ownedfiles/./alice/, which isfiles/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élisewritten with a combining accent, was created next toélise, and itsfiles/Élise/wasfiles/é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, andIdentity::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 ofgcdata/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 tofiles/tasks/<id>/, the folder of an identity namedtasks, 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>/togcdata/jobs/<id>/: its arguments file, the transaction logs and snapshot of anawait, and the arguments, results and transaction logs of its jobs./filesdoes 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 theresult.gcbof a task only. - The
Runtime::on_files_putandRuntime::on_files_gethandlers both receive the path of the file relative to the project,files/alice/x.csv, with/separators and without the query string, whichFileand the readers open as it is. ThePUThandler received the request path,/files/alice/x.csvand its query string included, and theGEThandler the path belowfiles/,alice/x.csv. - A range over a
nodeGeo,for (g, v in idx[a..b]), yields the keys of the box spanned byaandband nothing else, andrangeSize(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, andrangeSizecounted 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 withGeoBox.containskeeps working and can drop the filter. nodeGeo::samplesamples the box of its two corners, in every mode, and returns no row outside it.denselists the keys of the box up tomaxRows;adaptativekeeps every k-th of them so that at mostmaxRowsremain;fixedandfixed_reglaymaxRowsgrid 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_regblends the two). Before, every mode walked the whole stretch of the curve between the corners:denseandadaptativereturned keys outside the box and spentmaxRowson them, andfixedstepped along that stretch, so close to half of its rows fell outside the box. Anullcorner 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::sampleandnodeIndex::samplewithmaxRows: 1return 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.isDirandFile.sha256, the paths of every reader and writer,System::execandSystem::spawn(the command and its arguments),System::getEnv, andIdentity::create(an identity name is itsfiles/<name>/directory). The call raises<what> contains a NUL byte. String.uppercase()andlowercase()map non-ASCII letters too (Unicode simple case mapping):"é".uppercase()is"É","ß".uppercase()is"ẞ". A mapped character may encode to another number of bytes, sosize()can change. Bytes that are not UTF-8 are kept as they are.int::parse,float::parseandparseNumbertake 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 bytei, where it returned the byte as achar; 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 nowString.nfkd_casefold_chars().perf(v)is gone fromstd: calling it is a compile error. Useinfo(v)ortrace(v)instead. Theperflog level is gone as well.LogLevel::perfno longer exists,--log=perf/GREYCAT_LOG=perfis an invalid value, and so aregc_log_level_perfin the C API (gc_log_level_traceis now 4) andLogLevel::Perfin 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.binto a named table,w0, ofgcdata/index.bin(world 0’s: a world’s index isw<id>, its commitsc<id>, main’s commitsc0), 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 intoindex.bin(about 1 s for 20M blocks) andmeta.binis removed. The migration is one way: an earlier version started on a migrated store finds no index and creates an emptymeta.binof 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::S3andio::XmlReaderare gone fromstd; both now ship as their own libraries.XmlReadermoves toxmland keeps its shape (read/can_read/available), gaining aselectfield and anXmlElementtree API alongside it, so a call site only needs@library("xml", "<version>"). S3 is a rewrite rather than a move: thes3library is pure GCL overhttpandxml, every type is private and reached by its qualified name (s3::Client,s3::PutObjectRequest,sigv4::Credentials), and the methods do not match the sixstdexposed –list_objectstakes a request object,get_object(bucket, key, filepath)is nowget_object_to_file, andS3Bucketiss3::Bucket. Existing call sites must be rewritten, not just re-imported. Dropping both also retires the vendoredsxmldependency, which nothing else used.servekeeps connections alive by default. A client sendingConnection: keep-alivenow reuses one connection for several requests instead of being answeredConnection: close.--keep_alive=falserestores 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 aKeep-Aliveit would then break by closing.runtime::Requestis gone: itsheader,headers,uriandbodyare nowTask::header,Task::headers,Task::uriandTask::body, reading the request the current task answers. All four answernullin a task that answers no request (task: true,greycat run, the scheduler, anawaitjob);headers()is nowMap<String, String>?instead of an empty map there.--req_workers/GREYCAT_REQ_WORKERSis 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.envsetting it falls back to the default.--workersis 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_smalldefaults to 20% of--workersinstead of 2, and--workers_largeto 10% instead of 0. A.envwithGREYCAT_WORKERS=8andGREYCAT_WORKERS_SMALL=2now starts 8 workers, not 10.RuntimeInfo.fg_threads,bg_threadsandmem_workerare gone.RuntimeInfo.small,mediumandlarge(RuntimeInfoClass) give each class’sworkers, the object cache budget of one of its workers (mem_worker) and itscache_ratio;mem_workersis the part of--cachethe workers share.fg_threadswassmall.workers,bg_threadsismedium.workers + large.workers.- Log records go to the
logstream ofroot,files/root/streams/log.ndjson, oneruntime::LogJSON object per line.files/root/log.csvis no longer written, and a file left by an earlier version is not touched. Read the stream withStream<Log>::get("log").reader(from)asroot, or withjqfrom a shell.--logfile/GREYCAT_LOGFILEis gone: the flag is refused and the variable is ignored. Log.idandLog.id2are nowLog.task_idandLog.job_id, in thelogstream’s JSON as well.task_idis the task a record comes from – for anawaitjob, the task that awaits it – andjob_idthe job’s index in that await,nullfor the task itself:id2was0for both a task and its first job. Records from outside any task carrynullfor 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.0for a task.- Each metric has its own stream:
HostMetricrecords go tofiles/root/streams/host_metrics.csvandTaskMetricrecords tofiles/root/streams/task_metrics.csv, read withStream<HostMetric>::get("host_metrics")orStream<TaskMetric>::get("task_metrics"). Each line is the record itself as one CSV line, with the defaultCsvFormat, no header and no wrapper.TaskMetricnow carriesuser_id,task_id,job_idandsrc. They are not log records, and--logdoes not switch them on.GREYCAT_METRICS/--metricstakes a comma-separated list of the metrics to record,hostandtask(GREYCAT_METRICS=host,task); unset, none is recorded. The option applies torunandserve. See Metrics. RuntimeUsage,WorkerUsage,ZoneUsage,Runtime::usage()andRuntimeUsage::collect()are gone, and so is theruntime::usagestime series they fed. Host statistics are now theHostMetricrecords of thehost_metricsstream (see Runtime).--usage_step/GREYCAT_USAGE_STEPis renamed--metrics_host_step/GREYCAT_METRICS_HOST_STEPand defaults to 60s instead of 10s; the old name is refused as a flag and ignored as a variable.LogDataUsageis nowTaskMetric, written once per task instead of once per transaction. WithtaskinGREYCAT_METRICS, every task – RPC calls, background tasks, scheduled tasks and eachawaitjob – writes it once it ended and its transaction committed, covering the whole task (anawaitsuspension 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 theworld_idthe task ran in (0for main), timings (wait,exec,run,suspended,commit),awaitactivity (awaits,jobs,fusion_conflicts),catches, object cache behaviour (cache_misses,cache_evictions, andcache_blocks,memoryandcache_budgetagainst 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_bytesandstatus.TaskMetricinruntime.gcldocuments every field;skills/reference/runtime.mdhas 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 turningtaskon costs no measurable time – only about 550 bytes per task. A task’sstartis now when a worker first picked it up, argument binding included.- An RPC call cut short by
--request_ttlanswers503with the error messagetimeout, whether it was still queued or already running; a running one used to answer400. Array.sort,Array.sort_byandTable.sortplace null keys last whatever the order, keeping their relative order.Arrayused to put them first in both orders;Tablecompared them as zero bits, which put them first ascending and last descending, and dragged the rest of the column into that raw-bits comparison.awaittakes the jobs only:await(jobs).runtime::MergeStrategyis gone with itsfirst_winsandlast_wins, which silently dropped one side’s writes (and, on anodeTime/nodeIndex/nodeList/nodeGeo, could mix the tree blocks of two transactions); every merge now behaves asstrictdid, made finer by the key-level merge below. Drop the second argument of everyawaitcall.- A static function called on a specialization,
X<int>::f(x), checks its params and return declaredTagainstintat runtime, as an instance method ofX<int>does; a wrong argument used to pass silently. AFoo<T> {}literal (Array<T> {},Map<String, T> {}, a user type) in a method ofX<T>now buildsFoo<int>when the method runs onX<int>, instead of the bareFoo: a method declared: Array<T>that returns one no longer fails its own return check, and the collection it builds is typed, so adding aStringto thatArray<int>raises. A literal in a lambda, or in a method a subtype inherits from its generic parent, still builds the bareFoo. Aproject.gcpbuilt 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 ?? ;ortrue && 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 withError { message: null }or used whatever value was left on the stack, asx ?? ;and1 == ;did. - A
.or->that names no member, as inn->,n->->,n->[0],a.ora?., no longer compiles. A bare->dereferenced the node as*ndoes 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,\fand\vare gone, and an unknown escape such as\dor\.no longer stands for its character, so a regex pattern needs"\\d". Write\u{1b}for ESC. A\uescape that names no codepoint (a surrogate such as\ud800, above10FFFF, 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_typesholds 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 readinggc_program_type_tmust be rebuilt against this header, and the Rust SDK’sgreycat-sysbindings follow. Aproject.gcpbuilt by an earlier version must be rebuilt.--worlds/GREYCAT_WORLDSis gone: the flag is refused and the variable is ignored. It never did anything; worlds are created at runtime withWorld::create(see Runtime). The options after it ingc_env_options_offset_tmove down one slot, so a native library readinggc_host__optionsmust be rebuilt against this header.- A node declared with a generic alone, such as
node<Box>,nodeList<Array>,nodeTime<Map>ornodeIndex<int, Box>, now saves each value with its own type. Such nodes written by an earlier version read asnullhere, 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 thatlogin(name, "")accepted.loginrefuses 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 withset_password. - A
.gzor.gzipfile is served withContent-Encoding: gziponly when the extension under it has a type of its own, asdata.csv.gzis (text/csv, which the client decodes). Any other one, such asbackup.tar.gz, is served asapplication/gzipwithoutContent-Encoding, so a browser or an HTTP client stores the file as it is. Every.gzfile used to be sent withContent-Encoding: gzip, and was decompressed on download. greycat user remove, andIdentity::remove, delete the folderfiles/<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 removekept it. A name that is not one path segment, such as..ora/b, keeps its folder.greycat user add,Identity::createandIdentity::create_with_passwordrefuse a new name whose folderfiles/<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::createandIdentity::create_with_passwordthrowinvalid 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
Identitycall 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 thegreycat usercommands) 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.comsigns in as and namesalice@corp.com,Straßenamesstrasse, andélisesent with a combining accent, as macOS input often is, namesélise. Accents count:elisedoes not nameélise. Each of these named no identity before:get_by_namereturnednull,loginwas rejected, andcreaterefused 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.createon such a name now sets the role of the existing identity, andcreate_with_passwordthrowsidentity already exists. A name that several identities created by an earlier version read as, such asÉLISEnext toÉliseandélise, names none of them, andcreateon it throwsidentity name reads as several existing ones; each is still named by its exact name. A new name is stored in NFC.users.mdbholds 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
forloop walks anodeTime,nodeList,nodeIndexornodeGeo, or anodeTimeCursorstands on one of its values, adding or removing a key of that node (setAt,setoraddwith 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, andremoveAllfreed 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
***ndereferences anode<node<node<int>>>three times and!!bor- -xapply twice, where a second prefix operator was a syntax error. A prefix-may be followed by whitespace, so- 1and- 1.5compile as-1and-1.5do.--xstays 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, sois, the parameter contract, attribute writes and the_typeJSON 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 inextends, also one a type reaches without being part of it, is reported asself inheritance: <a type of the cycle>. - A type or an enum named
Rootinproject.gclfails to compile withRoot is reserved in project.gcl: that name holds the module variables ofproject.gcl, so the type compiled but lost its fields, andRoot { n: 5 }threwcannot assign int to Root and offset 0. ARootdeclared in another module is not affected.
Standard library
nodeList.add(v)writes at the index after the last one, as documented, instead of atsize(). Once aremoveor a sparsesetleft a gap,size()fell on an index that could hold a value, andaddoverwrote it without an error:add("a"),add("b"),remove(0),add("c")lost"b". On a list whose last index is the largestint,addnow fails instead of wrapping around.- A lookup in a
nodeTime,nodeListornodeIndexfinds 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.nextreturned a time far outside the series, andresolveAtorgetAtcould crash the worker.nextalso returned a time before the one asked when that time fell past the block’s last sorted value: with150and200set, then170, then200removed,next(160)gave150. - Setting in a
nodeTime,nodeListornodeIndexa 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 withinconsistency: gc_sbi_loador crashed the worker. - A
fixedorfixed_regsample of anodeTimeornodeListfrom 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
adaptativesample from a later key to an earlier one returns every value of the range whenmaxRowsallows it. On a large series it stopped after a part of the range. - A
fixedsample 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
fixedorfixed_regsample of anodeTimeornodeListthat starts past the last key returns its rows, eachnullunless the last key is withinmaxDephasingof 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
fixedorfixed_regsample of anodeTimeornodeListholding a single value holds it in the row at its key and, on anodeTime, in the rows past it, in the direction of the sample, less thanmaxDephasingaway. Every row held it whenever its key fell inside the sampled range. - A
fixedsample 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
nodeTimeornodeListfinds a value attime::minorint::minthat was set after larger keys and not yet saved.resolveAt,resolve,prevandnextskipped it and returnednull. nodeTime.prev(time::min)andnodeTime.next(time::max)returnnull.prevreturned the series’ last time, andnexton a large series returned one of its times.nodeTime.setAllstops 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
nodeTimeCursorwalks the series as it is whenfirst(),last()orlessOrEq()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 aremoveAllit could read freed memory and crash the worker. - Keys set in a
nodeTime,nodeListornodeIndexbetween 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,isNullandisNotNullfail with a message, asequalsdoes: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 reportedError: null.- String functions work on the whole
size()and treat a NUL byte (U+0000) as content:contains,indexOfString,indexOfStringFromandreplacefind text after a NUL byte and no longer match a needle cut at its own NUL,startsWithandendsWithcompare the whole value, andcompareorders by code point over the whole content, so it agrees with==(a prefix sorts first).replacewith a pattern holding a NUL byte read past the end of the string. The searches are also faster:containson 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: whatchars()returned before. Unlikenfkd_casefold()it keepsø,ł,þ,œandæas they are. nfkd_casefold()andnfkd_casefold_chars()decompose in one pass instead of two, and take ASCII as it is: on 1 KB of textnfkd_casefold()takes about 92% less time on ASCII and 63% less on accented text; the output is unchanged.indexOf,indexOfFromandlastIndexOfwith a non-ASCIIchar, andspliton 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>andio::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 byGcxWriter.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 inlib/std/io.md. - New
io::Stream<T>, an append-only buffer ofTvalues that absorbs concurrent writes and hands them to a callback in batches, kept infiles/<user>/streams/<name>.ndjson(.csvfor a csv stream).Stream<T>::new(name, callback, max_dephasing, options: StreamOptions?)registers a stream andStream<T>::get(name)finds it, both returning aStream<T>.write(v)appends a value to the stream’s buffer, which reaches the file onceflush_sizebytes are pending (64KB when null,0writes each value at once), at the latestflush_stepafter the first of them (1s when null), whenreader()orlast_written_pos()is called, when the file is read through/files(aGETorHEAD, so an HTTP client polling it withRange: 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 nextflush()throws it. The callback runs as a task at mostmax_dephasingafter values reach the file, and drains them withreader(from), aReader<T>bounded bylast_written_pos()(aJsonReader<T>on an ndjson stream, aCsvReader<T>on a csv one).StreamOptions { format, flush_size, flush_step }holds the format (StreamFormat::ndjsonwhen null, orStreamFormat::csv, one CSV line per value with the defaultCsvFormat) and the two flush settings.format()returns the format of a stream, andbind_reader(reader, from)points a reader of that format at it, asreader()does. Removing the identity that registered a stream stops it: its buffered values are dropped, its callback no longer runs, andgetand earlier handles throw. TimeWindow<T>::minandmaxreturn aTuple<time, T>, andMultiQuantizer<T>::slot_vectoranArray<int>. They returned the bareTupleandArray, which the runtime refuses where the specialization is declared:fn f(w: TimeWindow<int>): Tuple<time, int>? { return w.min(); }raisedwrong return type.Array.sort,Array.sort_byandTable.sortsort a mix ofintandfloatkeys 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.sortkeeps 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_rowthrows when the table does not hold one column per field ofT, asset_rowdoes (get_row: the table has 3 columns where 'P' has 2 fields), and when a cell does not fit its field, such as anullin a non-null field (get_row: cannot assign null to field 'project::P::b'). With more columns thanThas 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). CsvWriterquotes a String the reader would otherwise take for a null: an empty or blank one, andnullin any case, so""," "and"null"read back as written. Written bare, they read back asnullinto a nullable column, also with the defaultCsvFormat. Thehttp_metricsstream file writes an empty header value the same way, which tells it from an absent one.GeoCircle.sw()andne()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 thencontainsmissed points. Such a circle’s box now spans every longitude.CsvReader.last_line()returns the row the lastread()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. WithCsvReader.sharding, aread()not preceded bycan_read()made it return a row of another shard.Array.sort,Array.sort_byandTable.sortare 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.0still ties with0.0. Measured on 1M random keys:int46 ms to 23 ms,float66 ms to 19 ms, aTablefloat column 67 ms to 21 ms.System::execfails with the end of what the command wrote, up to its last 4 KB, afterexec 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 befiles/<user>/requests/<timestamp>/. Outside any task (greycat test) it is the user directory.- A task stopped by
Task::cancelends with the error messagecancelled. - New
runtime::TaskStatusvalues,gc_task_status_*in the C SDK andTaskStatus::*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 anawait, and it is stopped once its jobs end. All three are still in flight:Task::runninglists them, MCPtasks/getreports themworking, and neither the scheduler nor a stream starts its next run before they end. Code that testsrunningorawaitto know whether a task is still in flight should also accept them. runtime::TaskStatus::ended_with_errors(gc_task_status_ended_with_errors,TaskStatus::EndedWithErrorsin the Rust SDK) is deprecated and will be removed in v9.x. The runtime never produced it: a task whose jobs failed endsendedorerror.- Tasks can pick their worker class (
runtime::TaskClass:small,medium,large):Job.task_classforspawnandawaitjobs,PeriodicOptions.task_classfor the scheduler, and the value of thetaskheader for RPC (task: large;task: truestaysmedium, an unknown value is answered400).nullkeeps the previous class.Task::task_class()answers the class the current task runs in.gc_task_class_tis now public, and the Cgc_periodic_options_tgainstask_class(agc_task_class_t;{}givessmall, so set it) in a former padding byte; the Rust SDK’sTaskOptionsgainstask_class. runtime::Task.task_classgives the worker class a task runs in, after falling back to a class that has workers, inTask::running,Task::tasks, the answer to atask:call and everyTask::eventsframe. It isnullfor a task read back from the history, which does not record it.runtime::Worldcreates, 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, withcreate, sit behind the newworldpermission (theadminrole holds it,userdoes not); a call that comes into the world while either works finds no world. All three log aninforecord whosedatais the world’sWorldInfo, itsresettime set byresetand itsremovedtime byremove;World::id(name),World::current()(the calling task’s world,"main"for main) andWorld::list()(WorldInfo: id, name, mode, creation time,user_idof 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_.-],mainis the main store’s, and ids are never reused. A snapshot or overlay world starts from its base: main (basenull 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::snapshotstarts from a copy of its base;WorldMode::overlaystarts empty and reads its base for everything it has not modified;WorldMode::standalonetakes no base, starts empty and reads no other index, like a new store, its module variables empty nodes of its own, andWorldChangesReaderlists every node it holds rather than comparing it with a base.Job.worldruns aspawnin a world ("main"for main,nullfor the spawning task’s), andPeriodicOptions.worlda scheduled function (null: main; a run is skipped while its world does not exist). Anawaitjob always runs in its task’s world.Task.worldnames the world of a running task, asTask::running()and atask: truecall report it, and is null inTask::history(), which does not record it. The Cgc_periodic_options_tgainsworld_idin 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, andStoreStats.worldslists 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 (nullor"main": main), which only describes it: no call reads it, and a client that wants it sends it in theworldheader.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:403over HTTP, an error fromspawnwithJob.worldand fromScheduler::addwithPeriodicOptions.world. An admin, and a world’s creator, use it whatever the list says.Identity.worldandIdentity.worldsread them back by name, leaving out removed worlds;Identity::removeclears them. They are stored in two new tables ofgcdata/security/users.mdb, which an earlier version ignores, enforcing nothing.Identity::create_with_password(name, role, password)creates a new identity holdingpassword, so it signs in at once. It throws whennameis taken (an existing identity is never changed), whenpasswordis empty, or whenroleis not declared. Likecreate, it isadminover HTTP and not permission-checked from GCL, which lets a@permission("public")sign-up endpoint call it.createfollowed byset_passwordcannot work there, becauseset_passwordrefuses an anonymous caller.Identity::createreports a name that several identities read as (see the identity name entry of Breaking changes) asidentity name reads as several existing ones: <name>, a name it refuses asinvalid identity name: <name>, and other failures aserror while creating identity. Both used to raiseerror while reading identities, with the collision printed on stderr.String.uppercase(),lowercase(),nfkd_casefold()andnfkd_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/Chisinaufollows the EU transition times since 2022. time.startOfWeekandendOfWeekwith a time zone start the week on the first day of its region in CLDR 49: Sunday inAmerica/New_YorkandAsia/Tokyo, Monday inAsia/Dubai, Saturday inAfrica/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 ofRuntime::export_storeinto a fresh store (a newgcdata/), every node under its original reference, so node handles in values stay valid. It builds the project asrundoes: 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>andgreycat call --world=<name>(GREYCAT_WORLD) run the function in a world:runresolves it in its own process,callsends it as theworldheader.mainis main; an unknown name fails before anything runs. Unset, both run in main.greycat user,backup,statsand the other store commands always run in main.greycat user world <name> <world|main>andgreycat user worlds <name> <w1,w2,...|->set the default and allowed worlds of an identity (Identity::set_world,Identity::set_worlds;-for every world), andgreycat user showprints 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 tokenservewrites togcdata/security/token, and targets--urlorhttp://localhost:<port>. It never opens the store, so it runs alongsideserve. Ctrl-C cancels the remote task.greycat statsprintsRuntime::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 pythonin a project that does not declare@library("python")fails with a message naming the library and the legacypython2generator. It exited with code 5 and printed nothing.greycat runandgreycat servein a directory holding only aproject.gcpthat 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 servebinds 8086 whatever.envsays, andSystem::getEnvreturns the environment’s value..envoverwrote it, so only a--flagcould override a checked-in.env. - The installer built into the core, which
greycat installuses when the language tooling is not installed, installs again a library whoselib/<name>/folder is missing althoughlib/installedlists it. It reported italready installed. - A Ctrl-C during
greycat runlets the running jobs finish, then exits with code 1 andmain did not complete: stoppedwhen 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 showlists undercan read:andcan write:the folders the user may open, and underreadable by:andwritable by:the identities its grants let intofiles/<name>/. The two pairs were swapped. The help ofgreycat user grantandrevokeand their messages say who gets access to which folder, as ingranted read to 'carol' on files/alice/.
Runtime
- Outbound TLS also trusts the CA certificates of
SSL_CERT_FILE(a PEM file) andSSL_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, sogreycat installand thehttplibrary trust them as well. From 8.2, which started exporting that chain,greycat installignored them. - A
--ca_path/GREYCAT_CA_PATHthat 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 ashttpfell 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_metricsandtask_metricsstreams buffer their records ashttp_metricsdoes, and reach their file once--log_flush_size,--metrics_host_flush_sizeor--metrics_task_flush_sizebytes are pending (GREYCAT_LOG_FLUSH_SIZE,GREYCAT_METRICS_HOST_FLUSH_SIZE,GREYCAT_METRICS_TASK_FLUSH_SIZE, default64KB), and at the latest--log_flush_step,--metrics_host_flush_stepor--metrics_task_flush_stepafter the first of them (default1s). AGETorHEADof the file through/fileswrites the buffer first. A stopping process writes what is left, but a killed or crashed one loses what was still buffered. A flush size of0writes each record at once. --log_flush_on_error/GREYCAT_LOG_FLUSH_ON_ERROR(defaultfalse): anerrorrecord writes the buffered log records tofiles/root/streams/log.ndjsonat 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>.gceand returns the path; run it withgreycat run runtime::Runtime::export_store. The file is a gcx file (seeGcxWriter): an infoMap(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.NodeReaderlists 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(); }.typekeeps the nodes of one node type (nodeTime<float>) or of a node kind (nodeTime), andlastlets a reader resume after the last node it read. A full scan reads the header of every block of the store.WorldChangesReaderlists 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 nodenof the current world differs from its base’s current version: eachread()gives aWorldDiffwith itskind(WorldDiffKind::added,removedorchanged),key, andbaseandworldvalues, skipping keys whose two values are deep-equal;lastlets 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,nodeListornodeGeowritten by two transactions – twoawaitjobs, 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 anawaitmerges 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 inTask::running,Task::historyand the SSE task events;Task::cancelstops it;Task::id,Task::expected_stepsand friends work inside it; and anawaitin 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;awaitjobs run in their parent’s class. Small workers never take the defrag lead, so a zone defrag never delays RPC calls. - The
result.gcbof a task appears whole, once the task’s transaction committed: the task writesresult.gcb.tmpin the same folder and renames it. It was written in place before the commit, so a client polling/filescould read an empty or partial file, or a result that a failed commit then replaced with its error. - On Linux the default
--workersis 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.5gives 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 withunable to pin worker thread. - On Linux the default
--cacheis 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_smallunits (default0.4) and a large one--cache_ratio_largeunits (default3). With the defaults a medium worker keeps about the budget it had, small workers about 8% of the cache and large ones about 30%.servelogs 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_budgetandHostMetricClass.cache_budgetfollow 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_ttlnow 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 itsawaitjobs; the client gets503and the task ends aserror, where aTask::cancelends it ascancelled.- 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: aTask::canceleither takes effect or changes nothing, decided by the task’s status at that instant, and once a task iscommittingit answersfalseand the task ends as it would have. A task reportsended,errororcancelled(Task::running, MCPtasks/get,gc_host__get_task_statusin the C SDK) only once its transaction is committed or rolled back, andcommittingin between (see Standard library), so a task started after seeingendedreads what the finished one wrote. A cancellation landing while a task suspended in anawaitno longer leaks the pool slots of the jobs it had created. - A task whose
awaitjob pauses on abreakpointreadsawaitagain once the job is resumed, and resumes from itsawaitwhen its jobs end. It readrunningwhile still suspended, and never ended. - A debug call (an RPC call with the
x-gc-debugheader, run on a worker paused on abreakpoint) that throws ends aserror. It was reported asended, with the error as its result. A debug call cancelled before the paused worker picks it up never runs and ends ascancelled. Once it runs it cannot be cancelled, since it runs on the paused task’s machine:Task::cancelreturnsfalsefor it, where it used to mark itcancelledwhile 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 answered503. It was answered200with awaitingtask that never ran and held its pool slot until restart. A call still pending when the paused task is stopped ends ascancelled. - With
hostinGREYCAT_METRICS, the server writes oneHostMetricrecord 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 countersRuntimeUsage::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 onetask_infocall: walking every memory region, as before, took about 90ms per sample with 130 workers. - With
httpinGREYCAT_METRICS, the server writes oneHttpMetricrecord per HTTP request tofiles/root/streams/http_metrics.csv, read withStream<HttpMetric>::get("http_metrics"):time,user_id,method(aruntime::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_bytesandresponse_byteson the wire, thetask_idof a call, and whether the exchange wasaborted. The client address,X-Forwarded-ForandUser-Agentare personal data, recorded only when named inGREYCAT_METRICS_HTTP_FIELDS(--metrics_http_fields=ip,forwarded_for,user_agent), and null otherwise. Records are batched and written once--metrics_http_flush_size(default64KB) is pending, and at the latest--metrics_http_flush_step(default1s) after the first of them. A flush size of0writes 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 atask: truecall’s already was, instead of being held in the request buffer for as long as the call runs – anawaitincluded. The headers stay readable throughTask::header;Task::body()answersnull. 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
taskheader, aspawn, anawaitjob, an MCPtools/callandgreycat run, the transaction logs of anawaitand its jobs, the snapshot of a task suspended onawait, and theresult.gcbof anawaitjob. Nothing reads them after a restart. Theresult.gcbof 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.binnext to main’s (the registry sits ingcdata/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 anodeTime,nodeIndex,nodeListornodeGeothe 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/registryrecords 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,awaitjobs 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 defragupdates every world’s index too. Runtime::backup_full(andgreycat 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 restorereplaces 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 earliergreycat restore, which ignores the worlds and those settings but leavesgcdata/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 anode<Box>,nodeList<Box>,nodeTime<Box>,nodeIndex<int, Box>ornodeGeo<Box>, reads back as it was written. The store saved it in the layout ofBox<float>but tagged it asBox, so a reload read everyTfield asnull(minandmaxof aGaussian<float>in anodeList<Gaussian>), and a node holding more than a few such values failed to load withblock load fail ! block_id:<n>. Such a value also keeps its specialization on reload, so anArray<int>in anodeList<Array>comes back asArray<int>instead ofArray. - 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 offlinegreycat defragthen 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_grantsand the other writes failed witherror while creating identityorerror while setting grants, andgreycat restoreof 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 withF_FULLFSYNC: it relied onO_DSYNC, which macOS does not honour. - The
Erroranawaitthrows for a failed job, once the task resumed from thatawait, has theawaitcall in itsstack. Its frame readline: 0, column: 0. job.result()stays readable after laterawaits, 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 threwJob is not executed yetfor every chunk but the last. The results now stay in files of the task,result.gcb, orerror.gcbfor a job that threw, whichawaitno longer loads: it reads the first error only, for the error it throws, and eachresult()call reads its file, so a large fan-out can be read one result at a time. The files go when the task ends, andresult()on aJobkept past it throws. A job that returns anErrorwithout throwing no longer makesawaitthrow.Jobhas two private nullable fields that locate the result:task_id, andjob_id, the job’s number among all the jobs the task awaited. The jobs awaited outside of a task, undergreycat 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 -9the next tasks took ids thatTask::historyalready held, and startup deleted thefiles/<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 ofgcdata/history, and deletes nothing. Task.worker_idis the index of the worker running a task,nullwhile it waits or is suspended in anawait, inTask::running(),Task::tasks(ids)and the SSE task frames.Task::running()also fillsTask.jobswith the jobs of a task’sawaitthat have not ended, each with its index in the awaited array as itstask_idand its ownworker_id, so a monitoring panel can show what every worker runs.
Server
- The
world: <name>request header runs an RPC call in that world,mainin main; no header runs it in main, whatever the caller’s default world. A name no world has, or an empty one, is answered400, and a world the caller may not use (Identity::set_worlds)403, each with acore::Errorwhosemessagesays which, encoded as the call’s own errors are (GCB or JSON; a JSON-RPC call gets it as the error’smessage). - The
x-gc-worldrequest header is no longer read. It stored its value as the request’s body length, so sent afterContent-Lengthit truncated the body –x-gc-world: 3cut a JSON-RPC or path-RPC call to its first 3 bytes and answered400– 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::eventsstreams 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 headersse: <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 oneevent: task-startedframe when its code starts running (not again when it resumes from anawait),event: task-progressframes as it reports steps (one per whole percent),event: task-breakpointandevent: task-resumedwhen it pauses on abreakpointand goes on, and oneevent: task-completeframe when it ends, cancelled or in error included; thedata:line is theTaskas JSON, or as base64 GCB for a subscriber that sentAccept: application/octet-stream.: pingcomments 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, somax_sse(default 2048) andmax_sse_per_user(default 16) bound them; past either the caller gets429. Not available on Windows, where the server cannot park a connection.GET /files/<user>/tasks/<id>/result.gcb?jsonrenders the result as JSON. A string result used to come out without its quotes underapplication/json.servewrites a root token togcdata/security/token(mode 0600) next to the rootpassword, 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.serveno longer risks hanging on stop. Each poll set carries a wake-up (an eventfd on Linux, anEVFILT_USERknote 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 onSIGUSR1alone, 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 withConnection: keep-alive. Every response previously claimedHTTP/1.0while 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 underwrkwent from 164k to 362k req/s as a result. Connectionis read as the comma-separated, case-insensitive list it is, soKeep-Alive, Upgradeand a capitalisedcloseare 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_capacitydefaults 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 withsendfile), 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-Lengthand 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 with400, 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_offwas 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 empty400 Bad Request, or a connection reset once the rest landed on a closed socket. Clients that write headers and body separately – Python’shttpx, 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 with202 Acceptedand no body, as Streamable HTTP requires. They were answered200with a-32601 Method not founderror carrying noid, a reply JSON-RPC forbids for a notification. - MCP
tasks/resultreads 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 gotunable to find result.gcb. - Idle connections are closed at the advertised
Keep-Alive: timeouton macOS and FreeBSD. The reaper armed its wake-up withEV_ENABLEon a knote it never registered, sokeventansweredENOENTand the connection was held – with its request pool slot – until the client gave up. /filesrefuses 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. APUTorDELETEon thefiles/root itself answers 403: theDELETEanswered 401, and thePUTsent back no valid HTTP response at all.- A file is served with the type of its extension, matched whole and in any case.
.ndjsonand.jsonl, stream files among them, are served asapplication/x-ndjson,.csvastext/csv,.mjsand.cjsastext/javascript, and.md,.xml,.gif,.webp,.avif,.mp4,.webmand.zipwith their own type, where all of them wereapplication/octet-stream. The extension was matched as a prefix, so.jsonlwas served asapplication/json,.jsxastext/javascriptand.htmlxastext/html, and an upper case one such as.PNGasapplication/octet-stream. - A
PUTto/fileswhose file cannot be created, such as one whose path is an existing folder, answers500. It answered nothing: the request bytes were sent back without a status line and the connection hung. - A
PUTorDELETEto/filesleaves the query string out of the file name, as aGETdoes:PUT /files/alice/x.csv?v=2writesx.csv. It wrote a file namedx.csv?v=2, which noGETcould then fetch. On every method, an encoded%3Fis part of the name instead of starting the query. - The
OPTIONSpreflight allowsDELETEand theAuthorizationheader, so a browser app on another origin can delete a file and send its token. It allowedPOST,GETandPUTand the headers*, which does not coverAuthorization, so every cross-origin request carrying a token failed its preflight.
Libraries & SDKs
- New public C stream API,
gc/stream.h, which the GCLStreamnatives wrap:gc_stream__register(with itsflush_sizeandflush_step_us),gc_stream__find,gc_stream__appendandgc_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__pathandgc_stream__type. - The published
lib/std/libgreycat_core.ais self-contained: it now bundles ryu, the float formatter the core calls into, and links with nothing but-lm. It used to leaved2s_buffered_nandd2fixed_buffered_nundefined, so linking it outside the GreyCat build failed – the Rust SDK instandalonemode 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 withclang. greycat codegen tsgives the generated.spawnof every function a trailing optional$taskClass('small','medium'or'large'), after$gand$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 tospawn, and an older SDK ignores it and runs the task inmedium.THIRD_PARTY.mdlists 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 indeps/, piposort, NetBSDstrftimeand dlmalloc (wasm only) as code vendored in the tree.
8.3
Breaking changes
Json<type>/Json<function>(and CSVtype/function/fieldcells) 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.csvrecords no longer carry the unused tag column.
Standard library
int::parse(value: String): intandfloat::parse(value: String): float: strict, typed number parsing that throws on malformed or out-of-range input (unlikeparseNumber, which returnsany).runtime::Log.timeis declared with@format(DurationUnit::microseconds)to match what the log writer emits, sofiles/root/log.csvcan be read back withCsvReader<runtime::Log>.JsonReaderandBinReaderopen an empty file as an exhausted reader (can_read()isfalse,available()is0) instead of throwing.CsvWritercreates the file and writes the header on flush even when no row was written.File::sha256reads throughread(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
greycatwith no command now runsserve; options without a command imply serve too (greycat --port=8081).greycat docgenerates an API reference for the project, delegated to the language tooling.- Registry support:
--registry/GREYCAT_REGISTRYpointsinstall,upgradeandnewat a JSON-RPC registry (defaultlegacy, i.e.get.greycat.io), with--registry_token/GREYCAT_REGISTRY_TOKENfor 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-houtput. greycat upgraderesolves thelangtooling 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(defaulttrue) lists every@exposed function in the spec, not only@tag("openapi")ones; paths are filtered by the caller’s permissions, andOpenApi::v3is now@permission("public"). --defrag_ratiodefault is now2.0(blocks held per live block tolerated before a zone is defragged); values in(0, 1.0]are refused at startup,<= 0disables 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::runningand MCPtasks/listlist 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.
stdis 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<->Dateconversion works over the whole range (includingtime::min/time::max), west-of-UTC%zoffsets are signed correctly, and no signed overflow neartime::min. parseNumberno 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
Mapkeys, non-scalarnodeIndexkeys andTablecells;awaitrollback on failed sub-task fusion; jobs directories cleaned up; poison pill cleared between tasks; snapshots dropped for cancelled/bailed awaits. - Core: a
Stringalready attached elsewhere is auto-cloned when stored as anodeIndexvalue past 32 entries; overwrittenTablecells are detached from their block; “already attached” errors name the object;is_instance_ofaccepts 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 usercommands load the program so identity roles resolve;greycat restoreno 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 ofstdinto the separatehttplibrary; add@library("http", ...)to keep using it. Csv::generate,Csv::analyzeandCsv::sampleare now@permission("admin")instead of open to anyapicaller.- 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 (seeCsvFormat.null_in_quotesto opt back into the old null behavior). - Identities whose names differ only by case (e.g.
alice/Alice) are now refused when created.
Language
@librarypragmas 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): intenqueues 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.nestedwith the newCsvNestedenum (flatten,json) writes/reads arrays, maps and objects either flattened into columns or as one JSON cell.CsvFormat.null_in_quotesopts back into reading quoted empty fields asnull.Crypto::md5andCrypto::md5hex, plus documentation for allCryptomethods.- MCP:
prompts/listandresources/listmethods (withMcpPrompt,McpResourceand related types) so clients probing those capabilities no longer getMethod not found. File::renamenow replaces an existing target on Windows too, matching Unix.
var task_id = spawn(Job { function: rebuild_cache, arguments: [42] });
CLI
greycat installis 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 pinnedbin/greycatto run library install hooks.greycat installtolerates libraries that are missing or fail to load, since it is what fetches them.greycat statsgains aMAXcolumn: the largest single block payload per type.- Commands that never open the store (
version,help,new,codegen, …) no longer creategcdata/andfiles/. greycat codegen python(and auto-detection) runs thepythonlibrary’s codegen hook when@library("python")is present, instead of emittingrequirements.txt.- The startup redirect to the local
bin/greycatis kept even when the path cannot be fully resolved (deep Windows paths). - Loading now reports every
native fnleft 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 projectwebroot/, with the project one taking precedence on conflicts. - HTTP
Range/If-Rangerequests are supported for static files (single range,206 Partial Content), andHEADrequests are answered likeGETwithout a body (and no longer trigger the filesGEThook). - New
--mcp_content/GREYCAT_MCP_CONTENT(both,structured,text) controls how MCPtools/callships results;--mcp_instructions/GREYCAT_MCP_INSTRUCTIONSsets theinstructionsreturned ininitialize. - Lenient argument binding for JSON-bodied calls (MCP, JSON-RPC, path-RPC, task arguments):
"null","None","nil"or""bindnullfor 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, safeTensorwrapper, 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 rustemits a native trait per type, native state can be carried on GreyCat types (refcell-style guards), and the newgreycat-buildcrate configures.gcliblinking and runs codegen against the pinned runtime;panic=abortis rejected in a.gclib. - Native libraries can register
installandcodegenhooks (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_formatto 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::rangeSizeis exact on deep trees and out-of-order series.- S3: correct request signing for empty paths and default ports, and
S3.hostmay include ahttp(s)://scheme. - Tasks: suspend/restore of
awaitinside afor-inloop, stale paths corrupting retainednodeIndex<String, _>keys on suspend, aborted transactions left reachable from recycled task slots, and failure paths when the task pool is full. for-inover 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_BINARYhandling.
8.1
Breaking changes
Identity::set_rolenow requires theadminpermission over HTTP and returnsbool(falseif the user does not exist). Setting a role not declared with@rolenow throws.
Standard library
Runtime::on_files_get(handler: function?)registers a callback, called with the filepathon every download from/files/. Passnullto disable it.- The OpenAPI
InfoObjectgainssummary,description,termsOfService,contact(ContactObject) andlicense(LicenseObject). Table::initnow 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 newcommand scaffolds a project:project.gclpinned to the lateststd,src/api.gcl, a test,.gitignore,AGENTS.mdandCLAUDE.md. It refuses to overwrite an existing project. greycat devwatches the project’s.gclfiles 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 statshas 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 servestartup banner now prints the runtime version and the program version. greycat upgradeshows up-to-date components with the verb “current”, in the same color as “installed”.
greycat new my-project
Runtime & server
- The built-in
publicandrootusers 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/GetProcAddressfailures withGetLastError, and link or start returning false.
Libraries & SDKs
- New Python SDK core artifact (
core/sdk/python,_greycatC extension). It passes live GreyCat objects to Python without serialization:box/unboxfor 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 ingc/type.h), so native libraries link on Windows with MinGW/lld. - Rust SDK: bindings regenerated;
gc_buffer__add_strlength widened fromu32tou64. - C SDK: the
gc_slot_tunion 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/webUI 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/lspasgreycatcommands, improve library discovery, and no longer promote theopenapitag 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
awaitof 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,ArrayandTable. - Schema upgrades: dropping an object or tuple attribute across an ABI change no longer corrupts or leaks memory.
nodeIndex::setnow returns the stored value on every code path.nodeTimeCursornow moves correctly on series with fewer than two values.Runtime::backup_fullnow 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/geofields are now released. Table: sorting a table with no rows works, and allocation failures ininitare handled.- Resolving nodes in storage (SBI blocks) now checks the expected type id.
- HAL: directory handles are closed properly; Windows recursive deletes use
rmdirfor directories.
8.0
Breaking changes
- Security model rewritten around
Identity: theUser,SecurityEntity,UserGroup,UserGroupPolicy,SecurityPolicy,SecurityFields,OpenIDConnectandUserCredentialtypes are removed fromruntime. - Migration from 7.x:
User::login(credentials, use_cookie)becomesIdentity::login(login, password)(always sets thegreycatcookie);User::current()becomesIdentity::current_id();User::me()becomesIdentity::current();hasPermissionbecomeshas_permission;setPasswordbecomesset_password;getByName/getbecome the admin-onlyget_by_name/get_by_id. User::tokenLogin,User::renewandUser::checkPasswordare removed. OpenID Connect / Keycloak support is dropped, along with theGREYCAT_OID_PUBLIC_KEY,GREYCAT_OID_CLIENT_ID,GREYCAT_OID_CONFIG_URLandGREYCAT_OID_KEYS_URLoptions.- Users are now stored in an LMDB database (
gcdata/security/users.mdb), outside the graph. On first start,public(id 0) androot(id 1, roleadmin) are created, and a random root password is written togcdata/security/password. - The
filespermission is removed (theadminanduserroles 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 anIdentityGrantfrom the owner. webroot/login.htmlis removed; the built-inwebroot/index.htmlnow handles sign-in through/runtime::Identity::login.for ... inno longer supports thesampling,skipandlimitclauses, and these words are no longer keywords. UsenodeTimeCursoror explicit counters instead.- The packed primitive types
t2,t3,t4,t2f,t3f,t4fandstrare removed, along with their codegen mappings. UseString,Tupleor user types instead. Plot(Plot::scatter_plot) is removed fromutil.Task.durationis replaced byTask.completion: time?, andTaskgainsuser_name.nodeTimeCursoris now@volatile(it can no longer be persisted), and itsreq_timefield is removed.- ABI protocol bumped to 3:
.gcbfiles, 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.xgcdata/. - CLI: the old internal
upgradestep, which reloadsproject.gcp, is renamedupdate.greycat upgradenow 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/0and size suffixes (KB,MiB, …) are also parsed strictly. --user/GREYCAT_USER(impersonation) now takes a user name instead of a numeric id.RuntimeUsageis no longer collected automatically byserve/run; callRuntimeUsage::collect()yourself if needed.- A float literal can no longer be used as a time literal:
1000.5_timeis now a compile error.
Language
Mapkeys may now be any object. User-type keys are hashed and compared by value (deep equality), not only primitives andString.- The compiler is hardened against malicious or pathological
.gclinput (deep nesting, oversized literals).
Standard library
- New
IdentityAPI: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 thepathof every file uploaded with HTTPPUTto/files/.Task::live(ids)returns whether each task is still active;Task::tasks(ids)fetches several tasks at once.Task::is_runningis 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: newbearing(value),geo::distance_to_segment(point, a, b)andgeo::meters_per_deg_lon(lat).GeoPoly: newbbox(),is_closed(),perimeter(),area(),centroid(),mean_centroid()andinterpolate(step), all antimeridian-safe.GeoBox: newGeoBox::from_point(center, radius),intersects,union,intersection,expand,center,width,heightandsplit(rows, cols).nodeGeo.search(center, max)returns the nearest entries asSearchResult<geo, T>.- New
atan2(y, x)math function. TensorDistancegains the hyperbolic metricslorentzandpoincare.ProgressTrackergainsspeed_smoothed(an exponential moving average of per-update speed), a configurablesmoothing(defaultProgressTracker::DEFAULT_SMOOTHING = 0.1) and a steadierremainingestimate.HttpRequest.max_response_sizecaps the response body size. The HTTP client now decodeschunkedresponses and follows up to 5 redirects (301/302/303 downgrade to GET; 307/308 keep the method and body).Gaussian.add,addxandadd_gaussiannow returnbooland reject NaN/infinite values.int::to_string(value, thousands_separator)now declares itsStringreturn type.PeriodicOptions.start/immediateare fixed: a futurestartdelays the first run only whenimmediateisfalse.
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 usercommand (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 devcommand: it runsserveand spawns a frontend watcher (vp build --watch, falling back topnpm/npx vite build --watch). Use--with=<cmd>/GREYCAT_WITHfor a custom command. - New
greycat lint,greycat fmtandgreycat lspcommands 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 branchstable) and refreshes the language tooling. Options:--force,-y/--yes,--with_lang,--lang=<version>.installandupgradenever redirect to a project-local binary. greycat testprints an end-of-run summary of failures and reports per-module memory leaks.greycat serve/runtake an exclusivegcdata/lock, so two processes cannot open the same store.- The license is again looked up in
$HOME/.greycat/licenseas 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) andgreycat restorenow include the user database. Restore with verification is fixed. - MCP endpoints (
initialize,tools/list,tools/call,tasks/*) are reachable withpublicpermission.tools/listonly 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 tsemits plain function signatures again, instead ofExposedFnwrappers. The Java and Python generators drop the removed tuple/strtypes.- The C SDK exposes more public API: scheduler functions in
gc/host.h, node essentials ingc/node.h,gc_log__machinef, and allocator getters for machine/host. The Rust SDK is regenerated for the new allocator API and gainsalloc,log,machine,object,programandslotmodules. - Native libraries are no longer loaded with
RTLD_DEEPBIND. - GreyCat AI-agent skills (
skills/SKILL.mdplus 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.
Arraystorage is now sized in powers of two, andMapis 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) anddurationoverflow. - HTTP client: fixes for SSL
want_read,HEADresponses and buffer truncation. SMTP:cc/bccdelivery, theBccheader,MAIL FROMand SSL retry. - Fixed
nodeTimeCursor,removeAll,nodeIndexinheritance,Arraysort with abstract element types, deepclone, andnode<String>wrapping of an already-persisted string. - Fixed bugs in
Gaussian,HistogramandTensor, andfloat::to_stringoutput. - Windows: fixed a boot loop when the project path contains spaces. File copy and
sha256of 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 onIdentity::tokenandset_password. - Broad stability pass over leaks, ASan/UBSan findings, corrupted-GCB handling, defrag and restart paths.
7.8
Breaking changes
- JSON serialization of
field,functionandtypevalues is now FQN-based strings (e.g."mod::Type::field","mod::fn") instead of numeric program offsets;Jsonparsing accepts the same strings (bare names are accepted for functions and types). The TS and Java codegen are updated to match. Csv::analyzenow takespaths: Array<String>instead offiles: 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 overflowruntime error. - Periodic tasks now run immediately when scheduled (
PeriodicOptions.immediatedefaults totrue). Setimmediate: falseto keep the old behavior of waiting for the first period. - The session cookie no longer carries the
Secureattribute (Path=/; SameSite=Strict; HttpOnly), which fixes login on Safari.
Standard library
- New
RuntimeUsagetype (OS and process memory,global_memory,memory_drift, per-worker and per-zone usage) and newRuntime::usage()endpoint (debugpermission). - Runtime usage is now sampled periodically (see
GREYCAT_USAGE_STEP) and stored in a privatenodeTime<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 asArray<Tuple<int, int>>. Jsonparsing now honors@formaton fields: custom date patterns fortime(with the field’s time zone), andDurationUnitscaling for numerictimeanddurationvalues.Jsonparsing of abstract types now resolves concrete subtypes through the_typediscriminator, including multi-level hierarchies (e.g. parsing asJson<LandVehicle>whereCar extends LandVehicle extends Vehicle).- JSON Schema (
SchemaObject) gainedpattern. Node types,type,functionandfieldvalues 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/charparameters are quoted automatically; trailing nullable parameters can be omitted).greycat runnow prints the function’s return value. Output is controlled by--format(jsonorgcl) and--pretty, which now defaults totrue.- 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 inproject. If the name is ambiguous,greycat runreports an error that lists the candidate FQNs. greycat restoregained--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) forrunandserve: 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/callaccepts ataskparameter, tools advertiseexecution.taskSupport: optional, and new types includeMcpTask,McpTaskStatusandMcpTaskSupport. - MCP tool schemas now describe abstract/concrete type hierarchies and
type/function/fieldparameters 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.hwithgc_node__resolve(), plusgc_program_type__extract_field_format()/gc_program_type__extract_format_pragma_arg()for reading@formatmetadata.
Performance
- Tensor distance kernels (
TensorDistance) now use NumKong instead of SimSIMD. - Faster worst-case line splitting in readers (
memchr-based).
Fixes
- Arithmetic: fixed
durationmultiplication and division withint/floatoperands. MultiQuantizer::boundsno 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-inindex.htmlis 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.tstype definitions - Simplified code generation
- Restored time helpers for
sdk/web - Improved ABI API and function loading
- Removed debug
console.infofrom WASM importObject
Rust SDK
- Initial setup and base for Rust SDK
- Moved Rust SDK to
core/sdk/rust, WASM moved tocore/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 .envimprovements (quoted values, comments, etc.)- CLI flags and environment variables now support:
byteandduration
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
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.
@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,
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=jsonflag 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
coremodule being always available in scope, it does not require any additionalusestatement now to use the oldmathsymbols.
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/licenceto~/.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/greycatgreycat 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.