This describes the target contract and the experimental subset that ships now. See Implementation status for the remaining work.

The file roles are .zi for source, .zir for checked intermediate modules, and .zx for portable executables and libraries. The portable tool is zi2zx, and its target name is zx. This is a permanent format cutover: rebuild previous .zib programs from source or current .zir; changing a filename alone does not convert the binary format. The previous tool and target names are not aliases.

Running bundles

Use ziran run app.zx for both console and graphical programs. The linker includes the reachable code of imported modules, including a UI library. Platform effects such as windows and input are supplied by an installed runtime provider. Ziran discovers providers generically, without UI-library names or widget-specific startup behavior. A console bundle with no required host capabilities runs directly and never starts a graphical provider.

Packages install a .host registration containing one absolute executable path under share/ziran/hosts. Discovery searches $XDG_DATA_HOME/ziran/hosts (default $HOME/.local/share/ziran/hosts), then each $XDG_DATA_DIRS entry (default /usr/local/share:/usr/share). Registrations are tried in filename order within each directory. ZIRAN_HOST_PATH overrides these locations with colon-separated absolute registry directories; empty and relative entries are ignored. Bundle directories and embedded assets are never provider search roots.

A provider implements --check FILE.zx, returning zero only when it can supply all required capabilities. Checking must not execute application initializers or entries or open a window. The selected executable then receives FILE.zx, or --vm FILE.zx when portable execution was requested. Its exit status is the run status; an execution failure never retries the app on another provider. Malformed bundles fail before discovery. Missing providers produce a diagnostic before the application executes. Provider registration is part of the library platform installation and adds no requirement to a console-only Ziran install.

For example, Kryon make install-user installs its reusable desktop provider. A graphical .zx then runs with ziran run app.zx without application sources, a manifest or generated native output. The portable bundle carries its linked UI code and embedded assets; the installed provider bridges the local OS.

Experimental versions 26 and 27

Run source in one command: ziran run hello.zi or zi2zx hello.zi. Both check the file and execute its main; its folder is the default module root. The direct runner also accepts saved .zir and portable .zx files.

zi2zx bundle --root DIR [--module-path DIR] [--define NAME] [--asset-dir NAME=DIR] [--bind caller:capability=provider:function] [--bind-host MODULE] --entry module:function -o FILE file.zi|file.zir ... loads explicit inputs and their extensionless imports, then links reachable modules. zi2zx run [--vm] FILE validates and executes a bundle file, or builds one in memory from FILE.zi with the bundle options, its folder as the default root and its main as the default entry, and executes zero-argument integer, bool, or void entry functions, as native code where the platform compiles it and on the portable VM elsewhere or with --vm. Integer entries support s8, s16, s32, s64, u8, u16, u32, and u64; the command prints unsigned results over their full range. C hosts recover a u64 entry result by casting the returned long long to uint64_t. Pointer-sized isize and usize remain outside the portable execution subset. Called functions can use these integer types, bool, float32, and float64, plus immutable string values, enums, and plain records with scalar, enum, string, or nested record fields. Strings support UTF-8 literals, byte length and read-only indexing, equality, parameters, returns, and record fields. Record defaults and literals, member reads and writes, scalar compound assignments, parameters, returns, and value copies execute in the interpreter. The unsigned integer subset includes bitwise &, |, ^, ~, shifts, and their compound assignments; u64 comparisons, arithmetic, and shifts use the full unsigned range. Signed s64 arithmetic, casts, bitwise operations, and shifts are also supported. Portable enum initializers support integer literals and references to preceding members joined by + or -; other constant expressions remain outside this subset. The bundle is ZX plus two zero bytes, a little-endian version, length-prefixed entry module and function names, a host capability count and its required module/function names, and a length-prefixed version 63 .zir payload. Named law and waiver tables precede that payload; each law includes its method, declared domain, concrete case count, counterexample, and waiver state. The loader rechecks embedded theorem certificates and compares the outer tables against the checked IR. Verification roots retain their imported laws, proof certificates, scalar types, constants, and called procedures even when the executable entry does not use them. See Laws and scalar proofs. The current linker follows direct function calls from the entry, keeps record and enum declarations used by those functions (including types from imported modules and nested record fields), and retains imported startup paths even when no symbol from them is called. It removes other unreachable functions, modules, types, and imports before writing the payload. Bundle serialization keeps procedures' #asm variants and writes local #asm blocks as their checked bodies in ordinary blocks. Only zi2zx run FILE.zi and ziran run FILE.zi, which build the bundle from local source in memory, run those variants natively; a bundle file, however it was built or obtained, and the portable VM run each procedure's portable body, so a downloaded bundle cannot run instructions of its own. A run that links libraries from other bundles runs no variants either. Code compiled with the variants is cached apart from the same bundle run without them. Typed expression graphs, source locations, certificates, capabilities and assets remain intact. Source and saved .zir keep every native variant for native builds. Run sh tests/zx_native_asm.sh build/bin/ziran.

The bundle writer first validates and measures the portable payload, then streams it directly to the destination. It needs neither a temporary payload file nor destination seeking, including for pipes and append streams. The input graph remains immutable. This performs a second field traversal but avoids a whole-payload buffer or temporary-file copy. Output failures are reported by the writer.

Compile-time definitions and imported modules needed by array bound expressions remain available when the bundle rechecks saved IR. Enum member references keep their declaration even when the expression uses the member as an integer without an explicit enum cast. The reader rejects unsupported versions, truncated or trailing data, malformed embedded IR, semantic errors and divergent fields in saved IR, unresolved imports, unsupported capabilities, and functions outside the current portable subset. Reachable #foreign host_api calls with scalar, string, enum, plain record, or void signatures become explicit capabilities. A bundle may instead bind one such call to an exported Ziran function supplied among its input modules with --bind. The linker checks that the provider is reachable and its parameter and return types match; a bound call executes inside the same VM instance and is absent from the external capability list. An embedding C host links build/libziran.a, includes ziran_host.h, opens a bundle with BundleOpen, and inspects its required module/function names with BundleCapabilityCount, BundleCapabilityModule, and BundleCapabilityFunction. It passes HostBinding entries to BundleRun. The runner checks that every required binding is present before executing the entry function. The standalone zi2zx run command has no host bindings and rejects a call that needs one. A run is not bounded: like the program's native builds, it continues until the entry returns. Only the web playground stops a run after 1,000,000 statements (VmRunBounded), so a runaway loop cannot hang the page, and it says so in the diagnostic. Embedding hosts can instead call BundleInstantiateLimited(bundle, bindings, count, max_steps) with a positive statement budget. This explicitly selects the portable VM and applies one cumulative budget to global initialization and every entry run; initialization failure returns NULL. In Ziran, std/bundle_host exposes the same operation as InstantiateBundleLimited. The ordinary BundleInstantiate and later BundleInstanceLimitSteps retain their per-entry limit behavior. Hosts needing the portable engine alone link build/libziran-vm.a, built by make build/libziran-vm.a; that archive excludes the native code compiler. A function holds as many locals as it declares. Calls nest until the C stack is nearly full, where the thread's stack bounds are known (Linux); zi2zx run runs the program on a 256 MiB stack, room for about 60,000 nested calls, and a runaway recursion stops with "N nested calls filled the stack". Elsewhere, such as in the playground, calls nest 128 deep. Functions take the language's full 64 parameters. The loader compares the capability list with the linked IR; the VM checks signatures before execution and verifies returned record field names and types. Record fields may themselves contain records, strings, or enums. Numeric, boolean, string, and plain-record slice parameters use VM_HOST_SLICE and a mutable elements array. Hosts keep the element count and pointer intact; the VM validates each element and copies changes back into the caller's slice after a successful synchronous call. Overlapping mutable slice arguments are rejected. Declared pointer types cross as VM_HOST_POINTER with the raw address in pointer. The VM treats handles as opaque: bundles store, compare against null and each other, and pass them to host capabilities, but cannot dereference, index, or do arithmetic on them. The host owns the storage behind every handle it hands out. Source and saved-IR bundle bytes match in scalar, record, and enum tests, including Kryon's geometry, layout, and popup ownership tests. Synchronous procedure type aliases with imported named functions run from source and saved IR, including pointer and slice arguments, nullable values, global initializers, and calls through record fields and fixed arrays. Fixed arrays with numeric or resolved integer constant-expression capacities support defaults, positional literals, element reads and writes, and value copies, including nested arrays and u8 arrays inside imported records. Indexing is bounds-checked. Versions 26 and 27 are experimental and have no compatibility promise. Host calls with procedure slots remain unsupported. Default-initialized module globals of portable value types work within one BundleRun; each call starts with fresh global values. BundleInstantiate creates an instance whose globals persist across BundleInstanceRun calls. Checked runtime global initializers run once when an instance opens, after imported modules initialize. Native pointer casts and callbacks across the host boundary remain unsupported; required capabilities must use the portable signatures described above.

Opening a bundle verifies its final linked code, including shared libraries. Each fresh instance validates its host bindings and creates independent globals; it reuses verification of the immutable bundle. Separate instances can execute concurrently while the bundle and their binding contexts remain alive. Capability enumeration reuses the final code's immutable list, including library requirements. Instantiation copies binding names into compact storage and indexes them by module/function, so caller order does not affect dispatch. Missing or duplicate bindings fail before any global initializer or entry executes.

Local slices may borrow fixed arrays or other slices, cross ordinary Ziran function calls, return views of permitted backing storage, and read or write indexed elements. The loader checks the existing slice lifetime rules; the VM checks ranges and indexes during execution. A live view keeps its backing array identity across an array value assignment.

Target contract

A .zx is a versioned, portable executable bundle linked from checked .zir modules. Its format identifier is ZX. It contains the program's portable executable content, data, entry point, linked module identities, exports, and explicit host capability requirements. The exact instruction and section encoding will be specified before the format is called stable.

The linker resolves imports and includes reachable code from ordinary libraries. That includes Kryon only when the program imports it. A non-UI command-line program is a first-class .zx; it launches without a graphical host. Kryon UI code is portable Ziran code in the bundle, with rendering and input supplied through declared host capabilities. The same bundle format serves graphical and non-graphical programs.

Before execution, the loader validates the format version, section bounds, symbols, entry point, and capability contract. Missing symbols, unsupported versions, malformed bundles, and unavailable required capabilities fail with diagnostics. A portable bundle cannot contain an undeclared dependency on target-specific C or Go code. Hosts may implement the declared capability interfaces with native libraries.

Shared libraries and partial modules

--library FILE.zx writes a thin bundle checked against that exact library. --include-library FILE.zx writes the composed program and assets as a standalone bundle. Repeated libraries must provide disjoint module identities.

A shared library is pruned for its entry and retained roots. An add-on may need further declarations from a module that already occurs in that library: for example, a nested record type discarded by the library's entry. The producer subtracts matching portable declarations individually, retaining new functions, types, constants, globals, laws and proof certificates with their required import links. Conflicting declarations reject the build. The loader composes the disjoint fragments into one module with its original identity; it rejects overlapping declarations, inconsistent imports and duplicate library providers before execution. Source locations and private visibility remain part of declaration identity. Existing library record layouts must match exactly; this does not extend an already retained record's fields.

Ownership comes from the payload and supplied library boundaries, not from module name or untrusted serialized annotations. The loader checks the payload's capability and law tables against its own declarations, then validates the composed graph. Capability names remain module/function pairs; a capability called by both fragments appears once in the final requirement list. Library executable bodies are shared after validation, with their original compilation contexts retained by linked copies. Each instance has one independent global state for the composed module, including both library globals and add-on globals.

Versions 26 and 27 share one encoding. The writer stores 27 only in a thin add-on that composes declarations into a module its library already provides, and 26 in every other bundle, so hosts built before 27 still open standalone bundles, included libraries and add-ons that share no module. Older hosts reject a version 27 header before reading capability requirements or executing initializers. This reader accepts both and rejects a version 26 add-on that needs composition. The saved .zir version did not change: its declaration encoding and the behavior of source and native builds are unchanged. tests/bundle_partial_modules.sh exercises source and saved IR, native C and both ZX engines, with capability, private access, duplicate provider, overlapping declaration, format rejection and instance isolation cases. With ZIRAN_ZX26_ROOT naming a built toolchain from before version 27, it also checks that host opens current plain bundles and refuses composed add-ons.

Component identity, state, and replacement

A component is an ordinary .zx entry plus its declared host capabilities; Ziran has no UI- or application-specific component names. BundleCodeIdentity returns a 32-byte content identity for the bundle's immutable program bytes and the exact supplied library identities, while BundleEntryModuleName and BundleEntryFunctionName expose the entry identity. BundleCapabilityAbiAt returns each capability's module, function, signature, argument types, return type, varargs flag, and argument-read flag. BundleAbiCompatible requires the same entry identity and entry signature and the same capability ABI set in both directions. Format version checking happens while opening the bundle. The std/bundle_host wrappers expose the same current C ABI to bundle code.

BundleComponentOpen retains immutable bundle code, copies the host bindings, and creates one independently initialized portable-VM instance with a positive work budget. Opening another component, including another instance of the same bundle, does not share or reset its globals. The host owns component and handle lifetimes; handles become stale when their generation is replaced or their component closes, and running a stale handle fails without touching the current instance.

BundleInstanceExportState snapshots all global values as recursive BundleStateValue data. Owned strings, fixed arrays, records, enums, scalar lanes, and SIMD vectors are deep-copied; pointers, slices, growable Vec backing storage, and other borrowed or host resources are rejected. Snapshots are bounded by the VM's representation depth and 16 MiB of copied payload. BundleStateValueBytes reports that exported payload size, not allocator overhead. A migration callback receives the old snapshot and a mutable snapshot of the replacement's initialized state, then BundleInstanceImportState validates and converts every value before changing any target global.

BundleComponentReplace is transactional at the component boundary. It checks the ABI, retains the replacement, initializes it under the component budget, exports both states, migrates and imports, and calls the optional verification callback while the old instance remains live. Only after all those steps pass does it close the old instance, retain the replacement as current, and advance the handle generation. ABI, initialization, state, migration, import, or verification failure leaves the previous instance and bundle live. Callers serialize run and replacement operations for one component; distinct components remain independent. tests/bundle_component.sh builds source through checked IR into .zx, proves independent state, successful nested-state migration, rollback on migration, schema, resource, initialization, and verification failure, stale-reference rejection, typed-ABI rejection, and an unrelated live component. It prints environment-dependent load, instantiate, and swap timings.

BundleComponentLiveValueBytes reports the live portable instance's global slot storage plus VM-owned record, array, and string payload. It deliberately does not claim shared immutable code, bundle bytes, host allocations, compiler cache, or total process RSS.

Native execution

On Linux and Android hosts with x86-64, 32-bit ARM or ARM64 processors, libziran runs bundles as native code. BundleInstantiate lowers the verified program to C with the backend ziran build --target=c uses, compiles it in memory with the bundled TinyCC (third_party/tinycc, LGPL-2.1; applications that ship it include its notice) and calls it. Host capabilities receive the same VmHostValue shapes the portable VM passes.

Compiled code is cached under a key of the SHA-256 of the bundle's bytes before its assets, its libraries, the CPU and the compiler's own sources, in $ZIRAN_NATIVE_CACHE, $XDG_CACHE_HOME/ziran-native, ~/.cache/ziran-native or $TMPDIR/ziran-native-cache; ZIRAN_NATIVE_CACHE=0 turns it off. Kryon's Android hosts point TMPDIR at the app's private files. Only this compiler writes that code, from a checked program, so a bundle without libraries whose code is cached opens without checking its program again; it is checked when the cached code is gone or BundleProgram asks for the checked program.

A bundle that cannot be compiled fails with zx.native; there is no fallback. Runtime failures stop the run as in the VM: bounds, unreachable code, a host breaking the value contract, the step limit and nested calls filling the stack. Native code counts the step limit in function calls and loop iterations rather than statements; a loop over literal bounds that nothing else leaves or steps is charged all its iterations as it starts. It does not check pointer lifetimes at run time: it trusts bundle code as a native build of the program would.

#parallel for regions run on worker threads each instance starts at its first region, one per processor up to 16; ZIRAN_PAR_THREADS in the host's environment sets the count, and 1 runs regions in place. The calling thread waits while the workers take chunks of the range in order. A failure inside a worker stops the run with the failure a serial run would meet first. Each worker counts steps against what the run has left, and the run is charged for all of them when the region ends.

Windows and macOS hosts run the portable VM, and zi2zx run --vm and libziran-vm.a run it everywhere for tests of the VM itself. ZIRAN_NATIVE_TIMING=1 reports where compilation time goes and ZIRAN_NATIVE_KEEP=1 keeps the generated C.

Linked code in the browser

A browser page cannot compile at run time, so the bundles it ships are compiled when the page is built. zi2zx native-c [--embedded] -o DIR file.zx ... writes each bundle's checked program as the same C, one folder per program listed in DIR/programs.txt, with zx_native.json naming its sources and compile flags; --embedded adds the bundles packed in it that open without libraries, such as a collection's games and Apps controller. scripts/linked-zx.py DIR --output OBJ --cc emcc --nm emnm compiles them, renaming each program's external symbols under a prefix of its own so programs and the host's own compiled modules never collide, and prints the objects to link into the page beside the WebAssembly libziran.

Each program's constructor registers it under the identity above. When a page opens a bundle with that identity, it opens without checking again and BundleInstantiate runs it as the linked code; every other bundle runs on the portable VM. The immutable program and its compiled services are shared, while every linked instance owns a private copy of the global data section and swaps that copy in only at its run boundary. #parallel for regions run in place, and runtime failures stop the run as natively. tests/zx_linked_web.sh checks independent coexistent linked instances end to end with emcc and node. Lifecycle components use the portable VM explicitly, so nested state and replacement semantics are the same in native, WebAssembly, and linked-browser hosts.

WebAssembly execution tier

A host that installs a WebAssembly provider with ZxWasmSetPlatform (cmd/zir/zir_zx_wasm_platform.h) runs suitable bundles as WebAssembly: BundleInstantiate lowers the bundle's verified IR to a wasm32 module with the same emitter as the zi2wasm tool, instantiates it through the provider, and counts the run budget like the native tier, one function entry and one loop iteration at a time. The current tier supports scalars, owned strings, fixed arrays, records, and typed s64 -> s64 host capabilities. Each instance keeps its own bindings and linear memory; a state table identifies its globals for owned export and import. Other constructs run on the portable VM, as do hosts without a provider. tests/zx_wasm_engine_test.c checks the seam and fallback, and tests/zx_web_lifecycle.sh runs source through checked IR and .zx in a private headless Chromium without Node. It checks actual Wasm execution, nested state migration through the provider's memory interface, independent linked instances, and portable component replacement and rollback in both browser hosts.

Embedded files and application hosts

--define NAME selects the same source branches as the native compiler. --asset-dir assets=assets embeds regular files recursively under assets/. The argument's left side is the path prefix inside the bundle, and its right side is the source directory or file. Multiple mappings may be supplied. Names are sorted bytewise for deterministic output. Duplicate names, symlinks, absolute paths, backslashes, empty components, and . or .. components are rejected.

After the checked IR, both versions store a little-endian u32 file count. Each file has a u32 name length, name bytes, a u32 data length, and exact binary bytes. Empty files are supported. The loader checks lengths against remaining input before allocating and rejects noncanonical names/order and trailing data. BundleAssetCount, BundleAssetName, BundleAssetData, and BundleAssetSize expose immutable files until BundleClose. Ziran hosts import std/bundle_host for file access and enumeration of the exact required capability names. BundleOpenBytes / OpenBundleBytes validate embedded or downloaded bytes through the same reader and verifier, without requiring an application file path. The caller may release the input bytes after opening the bundle. Unix, Android (including API 21) and Web hosts read the input and embedded IR through read-only memory streams; loading needs no temporary directory. Windows hosts use seekable temporary streams.

Kryon's reusable desktop player supplies rendering and input to a ZX instance; the application's widgets and state run inside the VM. A downloaded graphical program launches with ziran run app.zx after the desktop provider is installed, including embedded images. See Kryon project profiles. Application-specific storage, audio, and other native interfaces still require declared host capabilities with portable signatures. A native application's pointer casts or calls through native callbacks cannot simply be bundled.