Compiler
The compiler is the Gizlang package compiler/. Its package.json names each module (no sha256:
git owns the bytes). The compiler has one entry, compile(input: bytes, w: MutableView<i64>, out: MutableView<u64>): i64.
It changes source, a canonical graph or a package into LLVM IR or a diagnostic. The C driver
(runtime/compiler_driver.c) only reads files, allocates the workspace and writes the result.
Contents
- Pipeline
- Workspace and limits
- Driver
- Stable names
- Native and live units
- Edits
- Bootstrap
- Tests
- Canonical graph
Pipeline
| Stage | Modules |
|---|---|
| Scan bytes into tokens. | lexer |
| Parse source into nodes. | parser, syntax (node arena) |
Or import a .gizgraph. | graph, json, index |
| Or load a package (two passes, refer to Packages). | package, integrity (sha256) |
| Add the SDK modules that a program imports. | sdk, sdk_sources (generated) |
| Resolve modules, imports and exports. | modules |
| Specialize generics. Then check types, effects, borrows, contracts and constants. | specialization, checker, queries |
Lower markup (TSX) to gizlang:html calls on a hidden buffer, when the checker gets to it. | markup |
| Emit LLVM IR. | llvm (programs, functions, statements and the recursive expression emitter), emit (shared emit pieces), managed (rooting), heaptype (heap types, layouts, trace), places (in-place analysis), runtimecall (runtime and C calls), ownership (buffers and frames), entry (process main, test runner, live link, host exports), ir, text |
| Remove the internal functions that nothing reaches. This step does not apply to live generations, whose units are keyed. | prune |
| Or print source, inspect, or plan. | projection, inspection |
| Entry, tool protocol, error names. | main |
Markup lowering
- The lexer scans markup with its tag and children states. A
<opens a tag only where an expression can start. - The parser makes a
NODE_ELEMENT. A component tag is already a call. The parser declares a hiddenletbuffer before the statement that holds the element. - The checker changes each element in place into
markupOpen,markupText,markupChild,markupAttributeandmarkupClosecalls on that buffer. It joins the fixed parts intoFLAG_MARKUPliterals in scratch nodes. It keeps the element as written in word 13, for projection. - After this step, only ordinary calls and string literals remain.
JavaScript entry
A JavaScript or TypeScript program is not compiled: it runs on the VM (VM, vm/), as a Python program does.
giz FILE.js (.mjs, .cjs, .ts, .mts, .cts, an npm bin, or a folder with a package main) builds the
VM and runs the script; giz build FILE.js -o app writes the VM with the files of the script's project. Node's
own lib/ runs on the VM over the bindings in vm/node/. The speed plan is in JS server ecosystem and
the final design note.
Checks.
- test262, by section.
- The top 1,000 npm packages, each with its own test suite, published per package. The target is a pass rate of about 100%.
- For each package: the speed against Node and Bun.
Python entry
A Python program is not compiled: it runs on the VM (PYTHON, VM). build FILE.py is refused.
JVM entry
Status: design. Nothing is built yet. Java and every JVM language (Kotlin, Scala, Clojure) enter as bytecode: class files and JARs from Maven Central, Gradle or Maven builds. The ecosystem ships bytecode, not source, so one front end covers all of them. It uses the same pipeline as the JavaScript entry: one front end, profiles, guards, generic bodies and live tier-up.
Requirements.
- Run unchanged.
giz run app.jar, or a Maven or Gradle project, runs as HotSpot runs it (Java 21 and 25 LTS), with Spring Boot, Quarkus and Micronaut apps unmodified. - Import from Gizlang.
import {Lists} from "maven:com.google.guava:guava"gives typed, direct calls, with the types taken from the class files. - Speed and footprint. HotSpot's peak throughput, with native-image's start time (milliseconds) and memory, and no warm-up.
- No configuration. Reflection, resources, proxies and serialization work without hand-written configuration files.
What we take and what we overcome.
| System | Take | Overcome |
|---|---|---|
| GraalVM Native Image | Closed-world AOT; heap snapshot at build time; instant start and a small footprint | Reflection, JNI, proxy and resource configuration by hand or by a tracing agent; long builds; lower peak than HotSpot without PGO; dynamic class loading refused. We take the profile from the project's own tests automatically, keep generic bodies for what the profile missed, and load new classes as live units. |
| HotSpot C2 | Speculative specialization (class hierarchy analysis, inline caches, escape analysis) gives top peak | Warm-up, JIT memory, deoptimization machinery. We specialize ahead of time under guards and tier up by live swap. |
| Project Leyden, CRaC, OpenJ9 AOT cache | Shift work to build time; keep the trained state | They stay on the JVM, and CRaC needs checkpoint discipline. We compile to native once and keep the state through gizlang:live. |
| TeaVM, J2ObjC, Kotlin/Native | Bytecode or source to another target works | Partial class libraries and different semantics. We compile OpenJDK's own class library from bytecode, so the semantics are the JDK's. |
Design.
- Class library.
java.baseand the rest of OpenJDK's library compile from OpenJDK's own class files (GPLv2 with the Classpath Exception, which allows linking with our runtime). A small native layer replaces HotSpot intrinsics and JNI calls with Gizlang SDK calls (files, sockets, threads, time). - Objects and dispatch. Classes have fixed layouts, so fields are direct offsets. Virtual and interface calls are devirtualized by class hierarchy analysis over the closed world, under a guard that a newly loaded class invalidates (an epoch, as for JavaScript prototypes).
- Dynamic features.
- Reflection and
MethodHandle: metadata is kept for the classes that the profile or any string-constant analysis reaches; for all others it is generated lazily. invokedynamic: lambdas and string concatenation are resolved at build time; the other bootstrap methods run once and are cached.- Dynamic proxies: generated as live units.
- Class loading:
Class.forNameand custom class loaders load bytecode at run time as live units through the same compiler.
- Reflection and
- Memory and threads.
- The Java Memory Model (volatile, final fields, happens-before) maps to atomics.
synchronizeduses thin locks that inflate under contention.- Virtual threads lower onto Gizlang tasks; platform threads onto system threads.
- The collector keeps weak, soft and phantom references and
Cleanerwith Java semantics;finalizeis supported where code uses it.
- Native code. JNI and the FFM API (
java.lang.foreign) call throughgizlang:native. - Cache and prebuilt. Keyed by artifact hash, the dependency lock (Gradle or Maven), compiler, target and profile. gizstack.com serves prebuilt units for Maven Central artifacts that are licensed for it, as for npm.
Checks.
- OpenJDK's jtreg tests, by area. The JCK is licensed separately: we do not claim "Java SE compatible" without it.
- The top 1,000 Maven Central artifacts' own tests.
- Spring PetClinic and Quarkus quickstarts run unmodified.
- DaCapo and Renaissance against HotSpot and GraalVM Native Image, for start time, memory and throughput, measured alternately on one machine.
Host libraries
Status: design (Biseo for the language maintainers). It holds for JavaScript, Python and the JVM alike. Each ecosystem's standard library has the same three parts: code in the language itself, a native boundary, and C or C++ below that boundary. Each part gets one answer, the same for the three languages.
| Part | Node | CPython | OpenJDK | Answer |
|---|---|---|---|---|
| Library code in the language | lib/*.js (MIT) | Lib/*.py (PSF) | class files (GPLv2 + CE) | Compiled unchanged. Fetched by version and hash, never copied into the repository or edited. Its own tests are the measure. |
| Native boundary | internalBinding(name) | C modules (_io, posix, _hashlib, zlib, _socket, …) | JNI natives, HotSpot intrinsics | One thin adapter per binding, onto the Gizlang SDK. The only native code written for a language. |
| C/C++ below the boundary | V8 bindings, libuv, OpenSSL, zlib, llhttp, ada, simdutf, c-ares, nghttp2, brotli | libc wrappers, OpenSSL, zlib, expat | HotSpot, libjava | Not compiled. Each one duplicates an SDK capability. |
Node's JavaScript dependencies that its lib requires as internal/deps/... (undici for fetch, Request, Response, Headers,
WebSocket and EventSource; acorn; minimatch) are library code too: fetched with lib/ at the same commit and placed where
js2c places them (tools/cli/bundle.giz nodeLibrary).
Why.
- One implementation per capability. Files, processes, sockets, TLS, HTTP, time, randomness, hashes, ciphers, compression, text codecs (UTF-8/16, base64, hex) and the WHATWG URL parser live once in the SDK, in Gizlang. Three languages share them, and the SDK's own programs use them too. A capability the SDK lacks (brotli, RSA and ECDSA, HTTP/2) is added to the SDK once. It is never added to one language's layer.
- The library's semantics come from the library. The APIs, argument validation, error codes and messages, and event order come from Node's or CPython's or OpenJDK's own code, by construction. A rewrite chases them one bug at a time. Node's error codes and messages are what npm packages test against.
- The C below the boundary is not reused. Node's
src/is written against the V8 API. Its dependencies each duplicate an SDK capability, so compiling them would give two implementations of each capability and a C toolchain in every user's build. - Python and Java need nothing new. A Python or Java adapter maps its own boundary onto the same SDK calls. Marshalling
is one compiler rule per language, not hand code per function: bytes to
Uint8Array/bytes/byte[], strings, numbers, and anIoErrorto the language's exception with errno, code, syscall and path.
Speed. The library code costs nothing extra, for four reasons.
- Node's
primordialsare frozen copies of builtins, and the world is closed. The compiler binds them as intrinsics:FunctionPrototypeCall(f, x)becomes a direct call, with no guard. - Hot work (codecs, hashing, compression, file and socket I/O) runs in SDK code.
- Dead code goes at link time: a program keeps only the library functions it reaches.
- Node's library runs on the VM, which boots it at the first builtin
requireor Node global (VM).
Not done.
- The hand-written Node API layer is not kept. A JS shim of
fs,streamorbufferis a second implementation of Node's semantics. The formersdk/nodeadapters are gone; Node'slibruns overvm/node/. - V8 (see Embedded V8 above), and any C dependency, unless a measured gap remains after the SDK path.
Order. Each step is accepted by the library's own tests.
- Node's library boots lazily on the VM (built).
- The Node loader:
internal/*resolution,primordials,internalBindingadapters forpath,events,util,buffer,string_decoder,url,fs,os,process_methods,crypto(hashes),zlibandspawn_sync. - Node's
test/parallelfor those modules, then the top npm packages' own suites. - The remaining adapters:
tcp_wrap,tls_wrap,http_parser,cares_wrap,fs_event_wrap, andworker. - CPython's C modules and OpenJDK's natives over the same SDK calls.
Workspace and limits
All compiler data is in one i64 workspace:
- A header of 128 words.
- Four words for each token.
- Nodes of 16 words: kind, token, type, a, b, c, d, next, binding, flags, scope, emitted, and four words specific to the kind.
compiler/layout.giz names each header slot, node kind, flag and error code. Read it before
you read any other module.
- Checker and lowering loop stacks, graph import depth, projection traversal, and inspection counters and indexes have separate header slots.
- The preparation step clears the full header for each request. Each phase initializes its own scratch area when it starts.
- Tokens stay indexes into the token table. Node identities stay aligned arena offsets. The named syntax owners access them.
- The compiler allocates nothing on the heap. It never recurses past fixed depth limits (
E_LIMIT). - For this reason, the compiler has its own small text, JSON, hex and Base64 readers (
text,json,lexer.hexValue). The SDK readers allocate and grow values, and the workspace does not permit this. Each reader exists one time in the compiler.
Driver limits
| Limit | Value |
|---|---|
| Input | 16 MiB |
| Selected source | 16,000,000 bytes |
| Workspace | 128,000,000 i64 words |
| Files in package selection and transport | 1,024 |
| Fields in a record or in one union payload | 64. Union cases share payload storage, so their total field count is not limited by this value. |
| Nesting depth | 16 |
| Output | Starts at 128,000,000 words |
- The output grows only when the shared text codec requests a precise capacity. The host
checks the growth arithmetic and allocation, then compiles the same immutable input again.
A semantic
E_LIMITfailure never causes growth. - A record-limit diagnostic names the type or case, the measured count and the limit.
- The flattened size of a record or a union is not limited. A record value is one LLVM aggregate. A
union is a tag word and the words of its largest case. A read or a write of a case field that is a
record goes through a slot of the union type in the entry block of the function (
ir.gizunionSlot): one store, onegetelementptrand one load or store. LLVM's SROA then keeps only the parts that are used in registers. Thus the code of a case access does not grow with the leaves. A record of more than six words goes to a call as a pointer (emit.gizbyReference), and a returned aggregate goes through a pointer to the result. - Checker diagnostics keep the first generic instance that fails. Thus a later invalid specialization points to its own caller, also when an earlier valid specialization shares the same template tokens.
- Named function values use the ordinary closure adapter with their own parameter scope. Synthesized adapters have no captures of caller locals. Real lambdas keep their capture and parameter-shadow checks.
- The growing integrated DB graph reached the former output limit. After this correction it passed the source check. This is not a claim of bounded RSS.
Driver
gizlang-compiler --check FILE # .giz or .gizgraph
gizlang-compiler --explain FILE # the same, errors as text (explain.giz)
gizlang-compiler --emit-llvm FILE -o OUT.ll
gizlang-compiler --check-package package.json # run from the package directory
gizlang-compiler --emit-llvm-package package.json -o OUT.ll
gizlang-compiler --emit-llvm-debug FILE -o OUT.ll # with source places for a debugger (-package too)
gizlang-compiler --project FILE -o OUT.giz # canonical source projection
gizlang-compiler --inspect FILE -o OUT.json # entities and dependencies
gizlang-compiler --outline FILE -o OUT.json # declarations, effects, docs, annotations, hash
gizlang-compiler --outline-package package.json -o OUT.json
gizlang-compiler --edit FILE -o OUT < REQUEST # edit transaction (--edit-package too)
gizlang-compiler --emit-tool REQUEST -o OUT # GIZLANG-TOOL-1 request
gizlang-compiler --imports FILE -o OUT.json # import and re-export specifiersbin/giz wraps these modes.
Debug information
--emit-llvm-debug is --emit-llvm with the source places as comments. Each function starts with
; giz.function OFFSET LINE COLUMN NAME, and each statement has ; giz.at OFFSET LINE COLUMN before it. OFFSET is in
the prepared input; LINE and COLUMN are in the file. After the module, ; giz.file START END PATH names each file by its
bytes. A named local or parameter has ; giz.local SLOT LINE COLUMN NAME TYPE (or ; giz.param) when it is
bound, and before it each record type it holds has ; giz.record IR-TYPE NAME, then a tab, a field name and its type
for each field. TYPE is i64, u8, f64, bool, string, bytes, Vec<T>, a record's IR type, or ? for
another type. Other emits write none of them. giz build --debug (tools/cli/debuginfo.giz) turns them into DWARF and links
the module whole: the unit split does not carry the metadata.
Diagnostics and warnings
- A diagnostic is one JSON line on stderr, with exit status 1.
--explain(used bygiz check) writes the same errors as text. Each error is one lineLINE:COLUMN: error CODE in DECLARATION: meaning, with the reason (W_DETAIL: expected and actual types, argument counts, a missing case or field), the source line, a caret and a hint.- Lines count in the checked file. In a package, lines count in the module that contains the error, and the module path starts the line.
- A check that passes prints its warnings, if any, on stdout:
{"warnings":[{code, stable_id, location, message}]}. With--explain, the warnings are text lines withwarningin place oferror. - The only warning that the driver gives is
W_DEPRECATED(compiler/doc.giz). Thus a warning never changes a build. - An error inside embedded SDK code is reported at the call in the checked file that reaches
it. As text, that line names the SDK function. As JSON,
call_siteis beside the SDKlocation. - A JSON
locationorcall_siteaddresses the prepared input, which starts with the tool header. Itssourcegives the span in the checked file: a package module names its path, a single file has the path"". - An error that has one sure fix carries
fix:{"location": L, "text": T}, which replaces the span L with T. Today onlyE_IMMUTABLEof an assignedconstlocal has one:letin place of itsconst. Editors offer it as a quick fix. - If
mainhas no parameters, the program gets a processmainthat prints the result:%lld,%llu,%.17g, or 0 or 1 for abool.
Docs and annotations
Docs and annotations are workspace roots (NODE_DOC, NODE_ANNOTATION). They are before the
declaration that they belong to, and their target is in binding. No node word or header
word was added for them.
- Graph v8 imports documentation and nominal annotations into the same roots. This includes
the field and function
@wasnames that Live migration uses. - Graph v7 keeps its explicit three-field schema and has no metadata.
Packages
The driver runs the entry two times for a package:
GIZLANG-PACKAGE-PATHS-1letspackage.gizparse the manifest and list the selected relative paths.- The driver opens only those paths, with directory-relative opens that do not follow links
(no
.., no symbolic links, no FIFOs). It sends their bytes back asGIZLANG-PACKAGE-1. - The compiler checks every pin (
E_PACKAGE_INTEGRITY) before it parses. The package's own files may go unpinned; a dependency's manifest and files may not (E_PACKAGE).
Library modules
The toolchain libraries are GizHW (hw/), GizML (ml/), GizFS (fs/), GizDB (db/) and GizWeb
(web/). Each is a package with its own name: a program imports it by that name, not by a relative path, as it
imports a dependency (package:module).
| Import | File |
|---|---|
gizdb, gizweb, gizfs | db/main.giz, web/main.giz, fs/main.giz: the public facade. |
gizdb:PATH, gizweb:PATH, gizfs:PATH, gizml:PATH, gizhw:PATH | db/PATH.giz, …: for example gizml:tensor is ml/tensor.giz. |
gizlang:NAMEis only the standard library (the SDK). A dependency cannot take a library's name.The CLI finds these files when it reads the imports of a script (
sources.giz libraryFile). It gives each one the module name_gizlang_lib_+ the folder path, with/as__(libraryModule;librarySpecifieris the way back).The compiler resolves a library name to the module of that name (
modules.giz libraryOf,libraryMatches). The compiler and the CLI keep the same table of names and folders. A name with no file fails with the path that is missing.A library file that imports a sibling by a relative path gets the same module, because a module is one file.
Library modules are not SDK modules. They get no SDK permissions, and they do not count against the limit of 62 SDK modules.
A release ships the libraries as interfaces and objects, not as source (
builder.giz libraryArtifacts):- The builder makes one package for each library, in the order
LIBRARY_ORDER(hw, ml, fs, db, web): each imports only the ones before it. A package holds the library's files and the interfaces of the libraries before it, each with itsgizlang:binarygrant, as an installed CLI gives them. Each module has the publication fields["*"]. - The compiler checks each package once. With the selection
*,--interface-module-packagegives the interface of each module, and--emit-library-module-packagegives one IR of the library, in which only the exports of its modules are global. Each IR is a unit of its own in the units store, so a change to one library builds only that library's object (a storage fix: 78 s instead of 264 s). - The interfaces go below
lib/gizlang/libraries, with each import changed to its library name. The objects go intolibgizlang_libraries.a. - The installed CLI finds
gizdb:PATH(and the others) belowlib/gizlang/libraries. It grants each interface itsgizlang:binaryfunctions and links the library archive before the SDK archive. The library objects have a section for each function, and the link removes the sections that the program does not reach (--gc-sections).
- The builder makes one package for each library, in the order
In a publication field,
"*"approves each declaration of the module that its public API needs. It makes no declaration a root of its own. The SDK names each declaration for review.Known gap: the wasm target has no library archive.
Tool requests
A tool request has the form GIZLANG-TOOL-1\n<operation>\n<payload>. Tool requests are
read-only. A tool payload cannot request files.
| Operation | Result |
|---|---|
check, explain | Diagnostics. |
project | Canonical source. |
inspect | Entities and dependencies. |
plan <0..4> <stable-id> | The impact plan for an edit of one graph entity. |
regenerate-sdk | sdk_sources.giz, the module names of sdk/package.json. The module sources are not in the compiler: the host appends them to each request (the bundle beside the binary, BINARY.sdk). |
imports | A JSON array of the import and re-export specifiers, in source order, before resolution. |
imports parses source declarations with the same lexer, parser and Unicode string decoder. The native driver gives it
as --imports FILE -o OUTPUT. The script traversal of the CLI uses this metadata. It does not interpret source lines.
Empty named imports stay dependencies. Native library declarations are not included.
Resident check
gizlang --serve is one process that keeps the checked program between requests. It reads lines on stdin.
gizlang --serve-socket PATH PID returns once it listens on the Unix socket PATH; a process of its own session then takes
connections there, one after the other, with the same requests, until the process PID ends. A fast live host uses one
(below).
| Request | Meaning |
|---|---|
open check|explain PACKAGE.JSON | Check the package, as --check-package or --explain-package. The program stays in the workspace, with the text that the compiler checked (the input and what it added to it). |
move PACKAGE.JSON | The program is now this package. INCREMENTAL is 1 when its manifest is the one checked; else nothing is kept until the next open or edit. |
edit RELATIVE-PATH | The file under the package folder changed on disk. |
emit OUTPUT | Write the kept program's live IR to OUTPUT, the bytes that --emit-live-package writes. |
quit | End. |
Each reply is a line ok|err INCREMENTAL LENGTH and then LENGTH bytes: the output of the check, or its diagnostic.
INCREMENTAL is 1 when the kept program took the edit.
An edit is a delta after the kept text: the new text of the file. The compiler scans and parses it behind the old tokens
(compiler/resident.giz), so every token and node of the program stays where it was. When the file differs from the old
one only in the bodies of functions, those functions take the new bodies and are checked again (recheckBody); nothing
else is checked. Any other difference (a header, a type, a declaration added or removed, a generic or native function, a
file that does not scan or parse) leaves the program as it was (W_DELTA_AT is -1) and the driver checks the whole
program again. A body that does not check also makes the driver check the whole program (W_DELTA_AT -2), so a diagnostic
is the one of a fresh check. Slots 132 to 134 of the header carry the delta offset and the kept text; slot 135 is
W_SDK_LOADED_HIGH.
After an edit, the program is the one a fresh check of the new text makes: the header nodes of an edited function take
the new header's tokens, and the nodes of its old body, and those of the delta that the program did not take, become
scratch, which every pass over the heap skips (settle). A unit key counts a node by the bytes of its token, so a
token that a lowering spelled is told by its bytes, not by its number (units.giz place). String constants are written
in the order of their names. So the resident emit writes the same bytes as --emit-live-package.
An emit changes the workspace. So a child process of the server runs it, on a copy-on-write copy of the workspace, and
replies; the server keeps the checked program for the next edit. The emit request is W_DELTA_AT = -3
(RESIDENT_EMIT).
The units a live generation does not emit are the ones that its // gizlang-cached: line lists. The lists are no source:
emit OUTPUT UNITS names their file, and --emit-live reads it from GIZLANG_LIVE_UNITS. The driver puts them after
the program, at W_UNITS_AT. In the entry, they changed it every generation, so the resident compiler checked the
program whole each time.
A fast live host builds each generation through its resident compiler (tools/cli/compile.giz residentEmit): the
CLI sends move with the generation's package, an edit for each file that changed since the last generation and for
the entry, and emit. When it cannot, the generation is emitted whole, which also explains a failure.
Test: tests/compiler/resident.
Stable names
Source ranges
The parser records the first and last lexical token of each source declaration in the
numeric index owner (names.recordSourceRange, W_SOURCE_RANGES).
- Content hashes, meaning hashes and declaration edit spans use that range.
- Lexical tokens added after synthetic documentation tokens do not change the range and identity of an unchanged declaration.
- Metadata roots stay separate. Edit spans still include their bound docs and annotations.
- Derived nodes without a parser-recorded source range use the earlier scan.
- Canonical graph JSON has no lexical declaration spans. Graph unit and interface keys conservatively include the full tokenized graph. Declaration meanings also include the stable IR name.
- White space does not invalidate graph units. Any change of graph content invalidates every graph unit. This conservative limit prevents stale linked code and distinguishes empty graph functions. It does not claim fine-grained reuse of the graph cache.
Definition digest
The definition_digest of the outline is a conservative 256-bit definition of a declaration
(64 hex digits). It is independent of the local 64-bit cache hints and of the source-edit CAS hash.
meaningshares its dependency discovery and SCC traversal between the two modes.- Strong mode frames the declaration modifiers, the bound annotation tokens, the body and
the referenced 256-bit definitions through the
integritySHA owner of the compiler. - The shapes of SCC members set their order. Ties conservatively keep the source order.
- Graph mode frames all graph tokens and a member ordinal.
- Unsupported declaration forms return
null. When the scratch area is full, the outline is refused. - The digest is not mathematical equivalence. It is not an artifact signature, a persistent layout compatibility, or a distributed Live barrier.
Verification:
- The strengthened definition reaches a new bootstrap fixed point.
tools/unitspasses the raw SHA oracle of the native transport, lexical formatting, consistent renaming, dependency and SCC changes, declaration modifiers, bound annotation changes and scratch refusal.- The
tools/unitsscenario also compiles an internal Gizlang range fixture through the compiler transport driver. This is a prerequisite of representation only. - Persistent callers must require exactly 32 digest bytes.
IR names
IR names do not depend on node addresses (compiler/names.giz).
- A function has the name of its module and its name. An instance also has its type arguments. A closure has the function that contains it and its capture, parameter and result types.
- A type has the name of its layout key.
- Nodes with the same identity are numbered in node order, so names stay unique.
- Equal string constants share one name and one definition.
- Locals (
%p,%task, ...) are numbered in each function, in the order of first use.
Thus, when you add or change one function, the IR of every other function stays
byte-identical. Per-function compile caches depend on this. tests/tools/stable/run.giz is
the contract.
Native and live units
Constructors
- Record and union constructors consume each owned operand in source evaluation order, before later operands can change its origin.
- Partial construction uses the owned result slot. Thus a failure releases every completed field.
- The order of named fields stays the source order, independently of the physical field layout.
- No additional value store or copy rule is added.
- The native constructor regression covers Map snapshots, positional and named records, union payloads and interrupted construction.
Fill loops
The scalar fill lowering recognizes a literal pushed into a new empty Vec in a counted loop that starts at zero.
- It accepts only a literal or a name as the upper bound, and only one
pushstatement. - Calls in bounds, aliases, computed elements and additional statements keep the ordinary loop semantics.
- The literal fill and
filledshare one owner of allocation and lowering. tests/language/control/fill-loopspecifies negative and empty bounds and observable calls in bounds.
Build model
Every build uses one object store and one rule: an object is compiled once for each (IR bytes, flags, backend,
target), and a build that finds it does not compile it again. The store is $GIZLANG_CACHE
(~/.cache/gizlang by default). Sessions, slots, CI and release builds on one machine share it.
| Store | Holds | Key |
|---|---|---|
units/KEY-sdk.ll | the IR of one SDK module (sdkModuleIRs) | the compiler binary, the SDK manifest, the module and its SDK closure |
units/KEY-sdk.o, units/KEY-release.o | the object of one SDK module (moduleObjects) | the IR bytes, the optimization, the flags, the backend and the target |
units/KEY-O2-TARGET.o | one ThinLTO part of a large module (partObjects) | the part bytes, the flags, the backend and the target |
units/KEY-runtime.o, runtime/libuv-KEY.a | one C file of the runtime, and libuv (bin/giz runtime_archive) | the preprocessed source, the flags and the compiler binary; for libuv, its pin, the flags and the compiler |
units/kKEY-COMPILER-O0.o | one live unit (Live units) | the unit key |
thinlto/ | the code that lld made from ThinLTO parts | the LLVM key of each module and its imports |
linked/KEY | a linked program (storedLink) | the bytes of everything that the link reads, and its arguments |
compilers/, tools/ | the compiler and the toolchain programs (Shared caches) | their sources |
- Keys hold content, not paths. A key holds the digest of the backend binary, never the path of a build folder. Thus a release that starts in a clean folder uses the objects of the last build.
- Large modules compile in units. IR larger than 1 MB is cut at the compiler's unit marks (
partObjects, tools/cli/split.giz). Each unit is compiled at-O2with-flto=thinand kept inunits/by its bytes. The linker optimizes across the units with ThinLTO and keeps its output inthinlto/. One SDK module is up to 21 MB of IR: one clang took 36 s for it on 16 cores (221 s on a loaded host), and the units take 14 s. An edit keeps every other unit byte-identical, so it compiles only the units that changed. - The compiler links in units too. bin/giz
ir_objectscuts the compiler's IR (33 MB) withtools/split, one unit for each function (split.giz EACH). A compiler code change compiles one unit and links in 1.3 s. llvm-split's 32 parts each imported from all the others, so any change missed the whole ThinLTO cache: a 26 s link. - The compiler is its IR. bin/giz
compiler_irhashesbuild/gizlang.lland the runtime compiler C files. The tools and the prebuilt SDK are keyed by it, not by the compiler sources, so a comment change rebuilds neither. - An SDK module stays one object.
moduleObjectsjoins the parts of a module withld.lld -rand makes local again the symbols that the split made global (llvm-objcopy --localize-hidden). Thus each module keeps its internal helpers, and an archive of the modules links as before. - The SDK emits in groups.
sdkModuleIRsgives the modules whose IR is missing to at mostemitJobscompiler processes. Each one checks the SDK once and emits each module of its group (--emit-library-modules-package). One process per module checked the SDK 49 times: 46 s after a compiler change, 23 s in one process, 3.7 s in five. Each process also emits the IR prologue once (llvm.gizemitPrologue: the whole-program passes, types, layouts and strings, which read no module selection) and then only each module's functions. All 49 modules in one process went from 11.6 s to 7.1 s with byte-identical output. - A program link is the last step.
linkParts(the CLI) andlinkNative(the release) link the parts of the program with the objects of the SDK and the runtime. They keep the result inlinked/. If nothing that the link reads changed, the program is a copy. - Three links use the same objects.
runandbuildlink the program at-O2.--fastlinks it with the prebuilt SDK archive and the shared objects of its library units.- A release links each toolchain program statically, with ThinLTO and its cache.
giz livelinks live units at-O0into a shared object for each generation.
- The runtime library is an archive of stored objects.
bin/giz runtime-archive CLANG ARCHIVER OUTPUT FLAGSmakes it for any target; the host build uses the same function. An unchanged runtime compiles nothing (0.15 s), and an edit to one file compiles that file (0.3 s). One serial build of all files took 645 s in a release (2026-10-13). - One slot for each build. A release construction holds one build slot for all of its work. Its compiles and
its 8 programs run in parallel in that slot, in groups of
buildJobs.
Known gap: the separate prebuilt archive. The plan is that live units become the only path for SDK code, and that the prebuilt archive is then removed.
Live units
The compiler compiles the functions of a live generation in units (compiler/units.giz).
- Each function belongs to the unit
hash(name) % parts. A unit has approximately 32 functions. - The key of a unit is a hash of:
- the names and signatures (with inferred effects) of its functions;
- the source text that they come from (the function of a closure, the template of an instance);
- the signatures of the functions that they call;
- the shared interface: native declarations, types, interfaces and constants.
- Thus a signature edit rebuilds the units of the function and of its callers. A body edit that changes inferred effects also rebuilds the units of its callers.
giz livelists the keys that it has compiled in a last source line. The compiler then emits only adeclarefor the functions of those units.tools/cli/split.gizsplits the IR into the units, a base unit (helpers, entries) and a data unit (globals).- The CLI (
tools/cli/units.giz, in its own process: only clang and the linker run) uses$GIZLANG_CACHE/units/kKEY-COMPILER-O0.oagain for listed units, and caches the other units by content.$GIZLANG_CACHEis~/.cache/gizlangby default, one store for each machine. The key also includes the sources of the compiler and the runtime, and the target (compiler_id).
tests/tools/units/run.giz is the contract: for all kinds of edits, an unchanged key means
byte-identical IR, and a partial generation emits the same units as a full generation.
Shared caches
bin/gizkeeps bootstrapped compilers in$GIZLANG_CACHE/compilers/KEY(the binary and its IR). KEY is the hash of the compiler sources,runtime/glibc235.cand the clang version. No CPU identity enters a key: every build is for the baseline CPU (the language reference). It does not include the seed, because the fixed point makes one compiler from one source. A changed compiler source gives a new KEY, so a stored compiler is never used for other sources.- The compiler sources include
compiler/sdk.pin, not the SDK. The compiler is built from the SDK modules that it imports as that file names them: the git blob of each one at the lastbin/giz pin compiler. Thus an edit tosdk/never bootstraps. The compiler reads the current SDK beside it (gizlang.sdk) for the programs that it compiles.giz cialso runsbin/giz bootstrap --current-sdk, which checks the fixed point against the current SDK. - It keeps toolchain programs in
$GIZLANG_CACHE/tools/NAME-KEY. The key is the compiler key, the SDK modules that the program imports and the program sources. - A new build folder copies a stored compiler and CLI instead of a bootstrap and a link. A build
folder whose compiler or tool is current but not in the store puts it back.
bin/giz bootstrapalways bootstraps and checks the fixed point, then stores the result. - A write goes to a temporary name and is published with one rename. When two writers store the same KEY, the first one stays. A compiler is stored only if its sources did not change during the bootstrap.
- A hit or a use touches the entry. The CLI evicts, at most once a day, every entry that nobody touched for seven
days (
tools/cli/files.gizpruneArtifacts), in every cache folder of the store: a file by itself in the folders of files, a whole entry incompilers/,tools/andruntime/. An entry is renamed out of the way and then removed, so no reader copies half of it. Test data, package sources and the bench store are not caches and keep what they hold. - Test passes proved on a different machine are read from
$GIZLANG_SHARED_CACHE. This is the store of that machine as a folder, or served over HTTP. The CLI never writes there. - A shared pass skips only a test whose key matches. The key is the meaning of the test, the sources of the compiler and the runtime, clang and the target.
- An executable comes from the shared cache only when it is signed (
NAME.sig, written by a machine withGIZLANG_SIGNING_KEY) with a key inGIZLANG_TRUSTED_KEYS. If not, the CLI builds it on this machine. - The same rule applies to a missing keyed live unit (
units/NAME.o.sig), before clang compiles the units that are still missing.
Build locks
- Cold executable builds share a native file lock for each content key under
$GIZLANG_CACHE/locks. - A caller that waits checks the published executable again. Thus concurrent cold requests compile it one time.
- Process exit releases the lock after a failure.
- Empty lock files are not removed by executable eviction. They stay in place, so that competing lock inodes cannot occur.
- Warm hits keep their direct path.
tests/tools/scripts/cache_build.gizchecks concurrent compilation, changed keys, failed builders and repair through the CLI suite. It does not measure link speed, and it adds no other executable cache.
Edits
Agents and tools change a program by declaration, not by text offsets (compiler/transaction.giz).
Outline
giz outline FILE (or a package.json) prints each declaration on a line: its file and line, its
signature and inferred effects, and its doc summary under it. giz outline --json FILE lists every
declaration (name, kind, byte span, inferred effects, doc, annotations, fields, cases, parameters,
result) and the source hash, for scripts.
The pending 2026-10-12 source batch adds checked files, symbols and bindings metadata to
the same outline:
declarations, hashes and transport byte spans keep their edit meaning.- Each symbol has a file-relative
locationandname_location. - Each binding names a symbol index and its file-relative use span.
- Bindings follow nodes that the checker resolved and generic templates. They never follow matches of spelling.
- Only declarations in the supplied source or package transport are shown. Embedded private SDK declarations are not shown.
- Unsupported targets have no binding.
- Diagnostic byte locations also include the exact selected source span when it is available. Otherwise the span is null.
Completion
gizlang --complete FILE -o OUT (or --complete-package) completes a member. The editor writes the
member __complete__ at the cursor (p.__complete__). The check types p and stops at that member,
and OUT lists what the type of p offers:
{"receiver":"Point","items":[{"label":"x","kind":"field","detail":"i64"},{"label":"norm","kind":"method","detail":"i64"}]}- The fields of a record come first, then the methods of its type that are visible there.
- A Vec also has
length, its builtinsmap,pushandslice, and the exported functions ofgizlang:collectionsthat take a Vec first (v.filter(f)). A string haslengthand the functions ofgizlang:textthat take a string first. detailis the type of a field or the result of a function. A function also hasparameters([{"name":"by","type":"i64"}]), without the receiver; signature help reads them.- When the check stops before the member, there are no items.
Hints
gizlang --hints FILE -o OUT (or --hints-package) gives the type of each local whose type is not
written: const x = e;, let x = e; and the item of for (const x of v). Each hint is at the name of the
local. When the check stops, the locals that it typed are still listed.
{"hints":[{"location":{"path":"main.giz","start":110,"end":111},"label":": i64"}]}An instance of a generic function repeats its locals, each with its type in that instance.
Context
giz context FILE WORD... gives, in one call, what a change about those words must read:
- In full, every declaration whose name or code contains a word (in an identifier, in any case).
- The first line of each declaration that they use or that uses them.
For example, for depot and a request about holds, the output is 6 KB of the 21 KB program
(tests/tools/edit).
Edit requests
giz edit FILE reads one request on stdin:
{"base": "<hash>", "edits": [{"replace": "Point.move", "with": "function Point.move(...) {...}"},
{"delete": "unused"}, {"insert": "function added(): i64 { return 1; }"}]}- To name a declaration of another module of the program, use its file relative to FILE, as
the report names it:
{"replace": "lib/money.giz:Money.add", "with": "..."}. "from"puts an insertion at the end of that module, not of FILE:{"insert": "...", "from": "./lib/money.giz"}. Write the path as an import in FILE writes it.
Rename
{"rename": "total", "to": "totalCents"} renames a declaration of FILE and every use of it.
- A use is each identifier with the same spelling. Member and method names after a dot, and the field names of types, are not uses.
- The new name must not occur anywhere in FILE. Thus nothing is captured, and the program has the same meaning.
- A request that renames holds only renames. Each rename starts from the base, and no new name is used two times.
- To rename a method, replace it.
{"rename": "Counter.label", "to": "title"} renames a record field by the checked types, not
by the spelling. It changes the declaration, every member bound to it, and the field names of
its object literals. The label fields of other types do not change. The doc of the field
gets @was label, so live state keeps the value (SYSTEM).
Move
{"move": "Money", "to": "./money.giz"} moves a declaration (a type with its methods) to a
different module of the program. "from" names its module when it is not FILE. Write both as
an import in FILE writes them.
- The text and docs of the declaration move to the end of the new module.
- What it uses from its old module is exported there and imported into the new module.
- Every importer, also the old module, imports it from the new module. A namespace use
old.MoneybecomesMoney. - It is exported when another module uses it.
- A name that the new module already binds refuses the move (
E_NAME). A re-export of the old module also refuses it (E_MODULE). - A request holds one move.
Modernize
{"modernize": ""} rewrites old spellings to the modern ones in FILE and every module of its
program. The checker records a rewrite only where it proves that both spellings have the same
types and meaning:
| Old | Modern | Where |
|---|---|---|
Option.some(x) | x (({ ... }) after =>) | an Option is expected; an object literal with no kind |
Option.none() | undefined | an Option is expected |
let o = Option.none<T>() | let o: T | undefined = undefined | a local with no written type |
join(v, ","), Text.trim(s) | v.join(","), s.trim() | a prelude function that is a method of the first argument's type |
newMap<K, V>(), ns.newX() | new Map<K, V>(), new ns.X() | a constructor function of a type that the call site names |
v.sorted(f) | v.toSorted(f) | the call reaches the prelude function by its former name (modern.giz renamed) |
x.unwrap() | x!, or x where x !== undefined holds | x is an Option or a Result (x alone: an Option) |
x.isSome() | x !== undefined | x is an Option |
return x; after while (true) {...} or a return | nothing | the function has a written type |
x.kind === "some", !x.isSome() | x !== undefined, x === undefined | x is an Option |
(i: Item): bool => i.ok | i => i.ok | the call or the expected type gives the parameter types (pass {"modernize": "lambdas"}) |
"n=" + string(n), `a` + string(n) | `n=${n}`, `a${n}` | a string + chain with text, a template or string(x), and values |
string(n), string(s) | `${n}`, s | n a number or bool, s a string; string(bytes) (a UTF-8 decode) stays |
match (e) { Option.some(x) => {A} Option.none() => {B} } | const x = e; if (x !== undefined) {A} else {B} | e is an Option; if (e !== undefined) when A does not read x, which no other token of the function may spell; an empty arm goes |
- An SDK import (
gizlang:...) or a prelude name that the program no longer uses is removed. A relative import stays, as other code can need the methods of its module.{"modernize": "sdk"}(giz fmtsends it for a module of the SDK, under the package.json of@gizlang/sdk) keeps all imports: the SDK builds its modules without the prelude. - An edit inside another edit waits for the next request.
giz fmtsends the request up to 6 times, until nothing changes. - An edit with
"from"(a unit path) rewrites only that file; several such edits name several files.giz fmtnames its files, one request per program and round: a module they import is not rewritten (it is its own run). - The request holds no other edit. The base may be empty.
{"modernize": "lambdas"}makes only the lambda rewrites, so a lambda whose types do not come back without its annotations keeps the other rewrites of its file;giz fmtsends it after the other rounds. {"modernize": "bytes"}(giz fmt --bytes) makes only the byte-name rewrites of a string's members:s.length,s[i],s.slice(a, b),s.indexOf(x)ands.lastIndexOf(x)becomes.byteLength,s.byteAt(i),s.byteSlice(a, b),s.byteIndexOf(x)ands.byteLastIndexOf(x). The other passes make none of them.- A module whose rewritten program fails the check keeps its old text; the others change.
Transaction rules
The request is one transaction over the whole program of FILE: FILE and every module that it imports (the relative imports of a script, or the modules of a package).
- The request applies only to the program that its
basenames. Otherwise the result isE_CONFLICT. - Each name must identify exactly one declaration (
E_NAME). - The edited program must pass the full check: types, effects, ownership and contracts.
- Each edit is a set of splices of the base. In a package, the compiler frames and pins each changed file again. Thus it checks the edited program exactly as its files will be read.
- A rejection changes nothing. The
stable_idof its diagnostic names the declaration at fault. - On success, each changed file is replaced. This occurs only if no file of the program changed in the meantime. The pins of a package follow.
The report lists:
- the
changed,addedandremoveddeclarations of the program; - every function whose effect row changed (
beforeandafter); - the
affectedfunctions: the changed functions and their callers, transitively; - the
testsamong them, whichgiz test FILEruns again; - the
fileswritten.
tests/tools/edit/run.giz is the contract.
Bootstrap
bootstrap/compiler.ll.xz is the IR of the compiler, compiled by itself. giz bootstrap
does these steps:
- It builds the seed.
- It compiles
compiler/with the seed (stage1). - It compiles
compiler/with stage1 (stage2). - It requires that the IR of stage1 and stage2 is the same.
A change to code generation shows first in stage2. In that case, the bootstrap builds stage3 with stage2, and requires that the IR of stage2 and stage3 is the same.
Procedure to change the compiler
- Edit
compiler/*.giz. A new file also goes incompiler/package.json(its path; no sha256). - Run
bin/giz bootstrap && bin/giz test. - Commit the sources. Run
bin/giz pin compilerfirst only to build the compiler from the current SDK (it rewritescompiler/sdk.pin).
Do not commit a new seed with each change. Any seed that compiles the sources bootstraps them
to a fixed point. Commit bin/giz seed output only when the committed seed cannot
bootstrap the sources (refer to New language features).
Local loop and CI
The local loop is only the procedure above (nothing that is not necessary before a push).
CI (bin/giz ci, ours: a fresh clone of each new main) runs every check:
the live, edit, stable, unit and fault suites, the reference goldens, and the regression and
agent benchmarks. Run one of those locally only when the change is in its area. While you
work, use bin/giz test NAME for only the cases that a change touches.
bin/giz test runs cases and suites in sequence at -O0. Each build owns its backend
parallelism:
- Backend workers and ThinLTO jobs use the lower of
Task.cores()and one worker for each two GiB of half the observed available memory. - If the memory capacity is unknown, one worker is admitted.
System.availableMemory()owns the physical headroom of Linux, including every visible cgroup ancestor. This observation decreases concurrency. It is not a reservation.bench/perfmeasures optimized code.test --shard I/Ndistributes independent tests across machines.
CI resource limits
The execution host requires a systemd user manager. CI owns gizlang-ci.slice on that host:
| Limit | Value |
|---|---|
| Hard memory | 8 GiB |
| Memory pressure threshold | 6 GiB |
| Swap | 0 |
| Tasks | 128 |
| CPU | 2 CPU equivalents |
- Bootstrap, dependency installation, test builds and delegated suite scopes all join this slice.
- CI sets
GIZLANG_THREADS=1. $GIZLANG_CI/runner.lockserializes coordinator runs. On the execution host,$XDG_RUNTIME_DIR/gizlang-ci-admission.lockprotects the slice.- CI runs where builds and tests run. The coordinator writes a temporary slots file with only the
remote
buildhosts ofGIZLANG_SLOTS_CONFIG. The slot scheduler selects the host with the most free slots and memory, and waits while all are full. - Only when the file has no remote
buildhost does CI run locally. Then the coordinator checks local physical headroom against the 8 GiB budget plus a 4 GiB reserve, and refuses with exit status 3 if the headroom is low or unknown. - The coordinator sends a temporary Gizlang suite through
bin/giz test. The suite and its copy of the coordinator are intests/ci-execution, not intools/cli: the launcher buildstools/cli, and a secondci.gizthere fails it. The suite removes its driver files, then runs bootstrap, dependency installation and all tests. Nested builds keep the selected slot. - A local execution host checks headroom again before it installs the slice limits. Remote slots use the bounded scope and the fleet headroom policy. CI does not compare the remaining capacity of their own 8 GiB scope with a second 12 GiB admission requirement.
- The coordinator stays outside the execution slice. It records an admission refusal,
a signal, or an infrastructure exit as
error. The reason names the step. A headroom refusal includes available, budget, reserve and required bytes. Test failures remainfail. - The slot wait and execution have a one-hour deadline.
timeoutreturns exit status 124 at the deadline. It sends SIGKILL after a further 30 seconds if the command does not stop. - Each attempt has one row in
results/index.tsv. CI publishes anerrorrow before each coordinator step, then replaces that row with the observed result. If the coordinator itself is killed, the row keeps the last step and states that its signal or exit code is unavailable. A process cannot observe its own SIGKILL. ci nextuses the last indexed verdict. It retries anerrorat most twice, for three attempts in total. The row stayserrorafter that limit. It does not retrypassorfail. After main has a terminal result, it retries older errors within the same bound.ci run COMMITpermits an explicit further attempt. A log file alone does not stop a retry.ci statusprints the rows and their reasons. Each row also has an attempt identity. An infrastructure error does not updateresults/full.time.- The result log contains launcher output. A failed temporary suite also exposes step output. A successful temporary suite exposes its summary.
- Other workloads can change after admission. The slice limits the share of CI, but it does not keep memory for unrelated services.
- A slice OOM records an error. When CI stops the slice that it owns, the remaining descendants stop.
New language features
A new language feature that the compiler itself will use takes two commits:
- The first commit teaches the feature to the compiler without use of it, and stores a new seed.
- The second commit uses it.
Module constants came into the compiler in this way. The second commit replaced the magic
numbers of the compiler with layout.giz names. Its IR was identical to the IR of the
previous compiler.
IR names are stable (IR names). Thus, to prove that a refactor changes nothing, compare the IR of the two compilers.
Tests
Each golden case has an .expected file. A golden case is a .giz file, a package folder or a
.gizgraph file anywhere under tests/ (tests/README). The .expected
file contains ok, error CODE, the output of the program, or runtime RNNN.
bin/giz test compiles, links, runs and compares each case. It also runs every example,
and checks that sdk_sources.giz agrees with sdk/package.json.
Result reuse
- Only cases and test functions that the compiler certifies as pure can use a passing result again. The reuse key includes the compiler, the runtime, the sources and the golden. Artifact reuse also includes the emitted IR and the link recipe.
- Cases and test functions with effects run again.
- Every scenario suite (a folder of
tests/withrun.giz) runs on each selected run. - A repository read set does not prove that the environment, hardware, external services, cgroups or clocks are unchanged.
- Earlier cache timings do not certify this implementation.
Sharding and optimized links
giz test --shard I/Nruns every Nth case and suite, from the Ith. Several machines can share a run. The wall time is then one bootstrap plus the slowest shard.- An -O2 link divides IR larger than 8 MiB into parts of approximately 8 MiB, to a maximum of
32 parts. Smaller IR uses one
clang -O2(tools/cli/toolchain.giz). - CPU admission independently limits concurrent optimizer children and ThinLTO jobs, also for a single worker. The LLVM splitter sets the actual balance of the parts.
- This is a source policy.
Ownership of link and file operations
tools/cli/toolchain.gizowns the link policy for native programs, shared generations and Wasm, including split objects;tools/cli/levels.gizowns the CPU level copies of vector functions.bin/gizkeeps only the prerequisite seed bootstrap to a fixed point, the build of the native C and libuv runtime, and the link of the first CLI executable.- Steady CLI file operations use the bounded copy, tree and atomic publication owners of the SDK.
- HTTP downloads use the URL and streaming HTTP codecs of the SDK.
- Package discovery and pinning use directory capabilities. Worker counts use
Task.cores. CI timestamps useTime.iso. - The compression of the seed still runs the external
xztool, because the SDK has no XZ encoder. Its output uses the same staged publisher.
How goldens are written
- Write a golden by hand from the specification. Check it against the native run.
- Each
tests/language/graphs/NAME.gizkeeps its committed canonicalNAME.gizgraph. - Each case also has a second route to its golden, with no second implementation to maintain: its canonical projection must be a fixed point and print the golden at -O2. Thus this check is sharded and cached with the case.
Canonical graph
.gizgraph files are versioned JSON: gizlang.graph.v7 or gizlang.graph.v8.
- Every declaration and node has a stable ID. Edges are
{"$node": id}. - Roots are
fn,record,variant,generic,protocol,moduleandconst. - A constant use is
{"kind": "name", "data": {"const": id}}. - The compiler reads graphs as well as source.
tests/language/graphsholds its fixtures.
Graph v8 metadata
v8 adds one required top-level metadata array, beside format, nodes and roots.
- Each ordered entry has
target(a stable graph ID), an optionaldoc(normalized documentation text) andannotations(an array). - An annotation has
type({"$type": record-id}) andarguments(ordinary literal graph edges). - Targets are declarations, fields, union cases or interface methods. Metadata cannot bind to other executable nodes.
- The shared checker validates documentation tags, the nominal types of annotations, literal kinds and argument count.
- Unknown targets, duplicate docs and extra schema fields fail explicitly.
- The importer decodes only documentation literals, through the shared Unicode codec, into bounded token backing bytes. It does not parse generated program text.
- Projection, outline and Live use the metadata roots and keep
@wasorigins.
The compiler has no public exporter from source to graph. The supported graph routes are committed graphs and the projection from graph to source.