GizStack
View .mdOpen in ChatGPTOpen in Claude

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

  1. Pipeline
  2. Workspace and limits
  3. Driver
  4. Stable names
  5. Native and live units
  6. Edits
  7. Bootstrap
  8. Tests
  9. Canonical graph

Pipeline

StageModules
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

  1. The lexer scans markup with its tag and children states. A < opens a tag only where an expression can start.
  2. The parser makes a NODE_ELEMENT. A component tag is already a call. The parser declares a hidden let buffer before the statement that holds the element.
  3. The checker changes each element in place into markupOpen, markupText, markupChild, markupAttribute and markupClose calls on that buffer. It joins the fixed parts into FLAG_MARKUP literals in scratch nodes. It keeps the element as written in word 13, for projection.
  4. 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.

  1. 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.
  2. Import from Gizlang. import {Lists} from "maven:com.google.guava:guava" gives typed, direct calls, with the types taken from the class files.
  3. Speed and footprint. HotSpot's peak throughput, with native-image's start time (milliseconds) and memory, and no warm-up.
  4. No configuration. Reflection, resources, proxies and serialization work without hand-written configuration files.

What we take and what we overcome.

SystemTakeOvercome
GraalVM Native ImageClosed-world AOT; heap snapshot at build time; instant start and a small footprintReflection, 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 C2Speculative specialization (class hierarchy analysis, inline caches, escape analysis) gives top peakWarm-up, JIT memory, deoptimization machinery. We specialize ahead of time under guards and tier up by live swap.
Project Leyden, CRaC, OpenJ9 AOT cacheShift work to build time; keep the trained stateThey stay on the JVM, and CRaC needs checkpoint discipline. We compile to native once and keep the state through gizlang:live.
TeaVM, J2ObjC, Kotlin/NativeBytecode or source to another target worksPartial 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.base and 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.forName and custom class loaders load bytecode at run time as live units through the same compiler.
  • Memory and threads.
    • The Java Memory Model (volatile, final fields, happens-before) maps to atomics.
    • synchronized uses 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 Cleaner with Java semantics; finalize is supported where code uses it.
  • Native code. JNI and the FFM API (java.lang.foreign) call through gizlang: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.

PartNodeCPythonOpenJDKAnswer
Library code in the languagelib/*.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 boundaryinternalBinding(name)C modules (_io, posix, _hashlib, zlib, _socket, …)JNI natives, HotSpot intrinsicsOne thin adapter per binding, onto the Gizlang SDK. The only native code written for a language.
C/C++ below the boundaryV8 bindings, libuv, OpenSSL, zlib, llhttp, ada, simdutf, c-ares, nghttp2, brotlilibc wrappers, OpenSSL, zlib, expatHotSpot, libjavaNot 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 an IoError to the language's exception with errno, code, syscall and path.

Speed. The library code costs nothing extra, for four reasons.

  • Node's primordials are 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 require or Node global (VM).

Not done.

  • The hand-written Node API layer is not kept. A JS shim of fs, stream or buffer is a second implementation of Node's semantics. The former sdk/node adapters are gone; Node's lib runs over vm/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.

  1. Node's library boots lazily on the VM (built).
  2. The Node loader: internal/* resolution, primordials, internalBinding adapters for path, events, util, buffer, string_decoder, url, fs, os, process_methods, crypto (hashes), zlib and spawn_sync.
  3. Node's test/parallel for those modules, then the top npm packages' own suites.
  4. The remaining adapters: tcp_wrap, tls_wrap, http_parser, cares_wrap, fs_event_wrap, and worker.
  5. 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

LimitValue
Input16 MiB
Selected source16,000,000 bytes
Workspace128,000,000 i64 words
Files in package selection and transport1,024
Fields in a record or in one union payload64. Union cases share payload storage, so their total field count is not limited by this value.
Nesting depth16
OutputStarts 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_LIMIT failure 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.giz unionSlot): one store, one getelementptr and 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.giz byReference), 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

sh
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 specifiers

bin/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 by giz check) writes the same errors as text. Each error is one line LINE: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 with warning in place of error.
  • 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_site is beside the SDK location.
  • A JSON location or call_site addresses the prepared input, which starts with the tool header. Its source gives 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 only E_IMMUTABLE of an assigned const local has one: let in place of its const. Editors offer it as a quick fix.
  • If main has no parameters, the program gets a process main that prints the result: %lld, %llu, %.17g, or 0 or 1 for a bool.

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 @was names 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:

  1. GIZLANG-PACKAGE-PATHS-1 lets package.giz parse the manifest and list the selected relative paths.
  2. 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 as GIZLANG-PACKAGE-1.
  3. 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).

ImportFile
gizdb, gizweb, gizfsdb/main.giz, web/main.giz, fs/main.giz: the public facade.
gizdb:PATH, gizweb:PATH, gizfs:PATH, gizml:PATH, gizhw:PATHdb/PATH.giz, …: for example gizml:tensor is ml/tensor.giz.
  • gizlang:NAME is 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; librarySpecifier is 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):

    1. 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 its gizlang:binary grant, as an installed CLI gives them. Each module has the publication fields ["*"].
    2. The compiler checks each package once. With the selection *, --interface-module-package gives the interface of each module, and --emit-library-module-package gives 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).
    3. The interfaces go below lib/gizlang/libraries, with each import changed to its library name. The objects go into libgizlang_libraries.a.
    4. The installed CLI finds gizdb:PATH (and the others) below lib/gizlang/libraries. It grants each interface its gizlang:binary functions 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).
  • 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.

OperationResult
check, explainDiagnostics.
projectCanonical source.
inspectEntities and dependencies.
plan <0..4> <stable-id>The impact plan for an edit of one graph entity.
regenerate-sdksdk_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).
importsA 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).

RequestMeaning
open check|explain PACKAGE.JSONCheck 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.JSONThe 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-PATHThe file under the package folder changed on disk.
emit OUTPUTWrite the kept program's live IR to OUTPUT, the bytes that --emit-live-package writes.
quitEnd.

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.

  • meaning shares 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 integrity SHA 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/units passes 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/units scenario 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 push statement.
  • Calls in bounds, aliases, computed elements and additional statements keep the ordinary loop semantics.
  • The literal fill and filled share one owner of allocation and lowering.
  • tests/language/control/fill-loop specifies 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.

StoreHoldsKey
units/KEY-sdk.llthe 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.othe object of one SDK module (moduleObjects)the IR bytes, the optimization, the flags, the backend and the target
units/KEY-O2-TARGET.oone ThinLTO part of a large module (partObjects)the part bytes, the flags, the backend and the target
units/KEY-runtime.o, runtime/libuv-KEY.aone 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.oone live unit (Live units)the unit key
thinlto/the code that lld made from ThinLTO partsthe LLVM key of each module and its imports
linked/KEYa 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 -O2 with -flto=thin and kept in units/ by its bytes. The linker optimizes across the units with ThinLTO and keeps its output in thinlto/. 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_objects cuts the compiler's IR (33 MB) with tools/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_ir hashes build/gizlang.ll and 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. moduleObjects joins the parts of a module with ld.lld -r and 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. sdkModuleIRs gives the modules whose IR is missing to at most emitJobs compiler 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.giz emitPrologue: 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) and linkNative (the release) link the parts of the program with the objects of the SDK and the runtime. They keep the result in linked/. If nothing that the link reads changed, the program is a copy.
  • Three links use the same objects.
    • run and build link the program at -O2. --fast links 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 live links live units at -O0 into a shared object for each generation.
  • The runtime library is an archive of stored objects. bin/giz runtime-archive CLANG ARCHIVER OUTPUT FLAGS makes 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 live lists the keys that it has compiled in a last source line. The compiler then emits only a declare for the functions of those units.
  • tools/cli/split.giz splits 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.o again for listed units, and caches the other units by content. $GIZLANG_CACHE is ~/.cache/gizlang by 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/giz keeps bootstrapped compilers in $GIZLANG_CACHE/compilers/KEY (the binary and its IR). KEY is the hash of the compiler sources, runtime/glibc235.c and 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 last bin/giz pin compiler. Thus an edit to sdk/ never bootstraps. The compiler reads the current SDK beside it (gizlang.sdk) for the programs that it compiles. giz ci also runs bin/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 bootstrap always 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.giz pruneArtifacts), in every cache folder of the store: a file by itself in the folders of files, a whole entry in compilers/, tools/ and runtime/. 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 with GIZLANG_SIGNING_KEY) with a key in GIZLANG_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.giz checks 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 location and name_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:

json
{"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 builtins map, push and slice, and the exported functions of gizlang:collections that take a Vec first (v.filter(f)). A string has length and the functions of gizlang:text that take a string first.
  • detail is the type of a field or the result of a function. A function also has parameters ([{"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.

json
{"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:

json
{"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.Money becomes Money.
  • 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:

OldModernWhere
Option.some(x)x (({ ... }) after =>)an Option is expected; an object literal with no kind
Option.none()undefinedan Option is expected
let o = Option.none<T>()let o: T | undefined = undefineda 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 holdsx is an Option or a Result (x alone: an Option)
x.isSome()x !== undefinedx is an Option
return x; after while (true) {...} or a returnnothingthe function has a written type
x.kind === "some", !x.isSome()x !== undefined, x === undefinedx is an Option
(i: Item): bool => i.oki => i.okthe 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}`, sn 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 fmt sends 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 fmt sends 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 fmt names 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 fmt sends 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) and s.lastIndexOf(x) become s.byteLength, s.byteAt(i), s.byteSlice(a, b), s.byteIndexOf(x) and s.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).

  1. The request applies only to the program that its base names. Otherwise the result is E_CONFLICT.
  2. Each name must identify exactly one declaration (E_NAME).
  3. The edited program must pass the full check: types, effects, ownership and contracts.
  4. 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.
  5. A rejection changes nothing. The stable_id of its diagnostic names the declaration at fault.
  6. 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, added and removed declarations of the program;
  • every function whose effect row changed (before and after);
  • the affected functions: the changed functions and their callers, transitively;
  • the tests among them, which giz test FILE runs again;
  • the files written.

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:

  1. It builds the seed.
  2. It compiles compiler/ with the seed (stage1).
  3. It compiles compiler/ with stage1 (stage2).
  4. 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

  1. Edit compiler/*.giz. A new file also goes in compiler/package.json (its path; no sha256).
  2. Run bin/giz bootstrap && bin/giz test.
  3. Commit the sources. Run bin/giz pin compiler first only to build the compiler from the current SDK (it rewrites compiler/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/perf measures optimized code.
  • test --shard I/N distributes independent tests across machines.

CI resource limits

The execution host requires a systemd user manager. CI owns gizlang-ci.slice on that host:

LimitValue
Hard memory8 GiB
Memory pressure threshold6 GiB
Swap0
Tasks128
CPU2 CPU equivalents
  • Bootstrap, dependency installation, test builds and delegated suite scopes all join this slice.
  • CI sets GIZLANG_THREADS=1.
  • $GIZLANG_CI/runner.lock serializes coordinator runs. On the execution host, $XDG_RUNTIME_DIR/gizlang-ci-admission.lock protects the slice.
  • CI runs where builds and tests run. The coordinator writes a temporary slots file with only the remote build hosts of GIZLANG_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 build host 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 in tests/ci-execution, not in tools/cli: the launcher builds tools/cli, and a second ci.giz there 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 remain fail.
  • The slot wait and execution have a one-hour deadline. timeout returns 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 an error row 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 next uses the last indexed verdict. It retries an error at most twice, for three attempts in total. The row stays error after that limit. It does not retry pass or fail. After main has a terminal result, it retries older errors within the same bound. ci run COMMIT permits an explicit further attempt. A log file alone does not stop a retry.
  • ci status prints the rows and their reasons. Each row also has an attempt identity. An infrastructure error does not update results/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:

  1. The first commit teaches the feature to the compiler without use of it, and stores a new seed.
  2. 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/ with run.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.
  • giz test --shard I/N runs 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.
  • tools/cli/toolchain.giz owns the link policy for native programs, shared generations and Wasm, including split objects; tools/cli/levels.giz owns the CPU level copies of vector functions.
  • bin/giz keeps 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 use Time.iso.
  • The compression of the seed still runs the external xz tool, 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.giz keeps its committed canonical NAME.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, module and const.
  • A constant use is {"kind": "name", "data": {"const": id}}.
  • The compiler reads graphs as well as source. tests/language/graphs holds 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 optional doc (normalized documentation text) and annotations (an array).
  • An annotation has type ({"$type": record-id}) and arguments (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 @was origins.

The compiler has no public exporter from source to graph. The supported graph routes are committed graphs and the projection from graph to source.