01 / Structure

Modules and imports

An entry .zi file starts a module. #load "relative/file.zi"; adds another file to that module. Extensionless imports load Ziran modules transitively; pass --module-path DIR for library directories. Use Alias :: #import "module";, Alias :: #import, file "relative/path.zi";, or Alias :: #import, dir "relative/directory"; to access public declarations as Alias.Name. Directory modules use module.zi. #import, string "..."; compiles embedded source, including raw #string bodies; its named form exposes declarations as Alias.Name. C headers are not Ziran modules.

#import "text"

Answer :: () -> bool {
    return StartsWithFoldASCII("Ziran", "zi");
}

Use #scope_file, #scope_module, or #scope_export to set visibility for following declarations. Loaded files share module-scoped declarations while file-private declarations stay in their source file.

02 / Bindings

Declarations and calls

Name :: value defines a compile-time value or a named type. Name :: (args) -> Result { ... } defines a procedure. File-scope variables use name: Type; local values use typed declarations or inferred :=. An inferred integer local uses s64 storage.

Limit :: 8;
counter: s32 = 0;

Add :: (a: s32, b: s32) -> s32 {
    result := a + b;
    return cast(s32)result;
}

A standalone #program_export before a function preserves its native symbol name. A named procedure type such as Child :: #type (s32) -> (); can pass a named function as a callback; capturing closures are not supported.

03 / Values

Types, records, and enums

The tested surface includes bool, signed and unsigned integers, float32, float64, immutable string, records, enums, fixed arrays, and borrowed slices. int aliases s64. Native targets also support checked raw pointer forms; raw pointers are outside portable .zib.

Box :: struct($T: Type) {
    value: T;
}
NumberBox :: Box(s32);
box: NumberBox = NumberBox.{value = 42};

Mode :: enum u8 {
    Idle :: 0;
    Running :: 1;
}

Generic records apply as Box(s32) and can nest in fields and signatures. enum_flags assigns successive bits by default. #specified requires explicit member values. Fixed arrays use a checked capacity; slices borrow array storage and obey lifetime limits.

A polymorphic procedure names a type parameter with $T and infers it from an argument, including the element type of a slice ([]$T) or the pointee of a pointer (*$T), so one procedure serves every element type:

Largest :: (values: []$T) -> T {
    best: T = values[0]
    for value: values {
        if value > best { best = value }
    }
    return best
}

scores: [3]s32 = .[3, 9, 4]
print("largest %\n", Largest(scores[:]))  // largest 9

The standard sort module is written this way.

04 / Execution

Control flow and expressions

Functions use if/else with an optional then, while, lexical blocks, return, and unreachable. for i: 1..3 counts through an inclusive range, for < counts down, and for value, index: items walks an array or slice; a loop without a binder names the element it and its position it_index. break and continue can name an enclosing loop. defer runs a statement or block when its scope ends, last deferred first, including on early returns. ifx condition then a else b is a conditional expression, and if value == { case ... } picks a branch by value; over an enum, #complete requires every member.

Sign :: (value: s32) -> string {
    if value == {
        case 0; return "zero";
    }
    return ifx value < 0 then "negative" else "positive";
}

main :: () {
    defer print("done\n");
    scores := s32.[3, -1, 0, 8];
    for score, index: scores {
        if score == 8 { break; }
        print("% is %\n", index, Sign(score));
    }
    for step: 1..3 {
        print("step %\n", step);
    }
}

It prints 0 is positive, 1 is negative, 2 is zero, then step 1 to step 3, and finally done. print replaces each % in its format with the next argument on every target. A statement ends at the end of its line; a semicolon separates several statements on one line.

Top-level && and || preserve short-circuit evaluation in supported statements and loop conditions. Lazy expressions embedded inside outer calls are still a documented gap. C-style three-clause for headers are rejected.

05 / Compile time

Constant forms and laws

#if selects declarations or statements before checking. #ifx selects a constant, global initializer, or function expression arm. Conditions accept the current constant evaluator, including compiler-host OS comparisons with .WINDOWS, .MACOS, and .LINUX. Unselected arms do not enter saved IR.

size_of(Type) folds for supported scalars, pointers, fixed arrays, and nonempty plain records. In function bodies, size_of(type_of(expression)) uses the inferred type without evaluating the expression. Pure procedures with bounded local control flow can run during translation with integer, boolean, string, or floating-point values. Earlier imports and #run values can feed #if; selected results are stored in checked IR. Aggregate results, build-time I/O, and general metaprogramming remain unsupported.

#law states an obligation that ziran check must prove, and the build fails when it cannot. A forall law checks every combination of integer ranges, enum members, and bounded integer sequences such as xs: [4]0..9, up to one million cases:

Clamp :: (value: s32, low: s32, high: s32) -> s32 {
    if value < low { return low }
    if value > high { return high }
    return value
}

#law ClampInRange forall v: -20..20 => Clamp(v, 0, 10) >= 0 && Clamp(v, 0, 10) <= 10;

ziran check reports "status":"proved","detail":"held for all 41 cases". Change the bound to <= 9 and it reports law ClampInRange is disproved: counterexample v=10. Laws also cover types, bounds, sizes, effects, ABI shapes, and custom boolean procedures; see language direction for the full contract.

06 / Effects

Foreign and host calls

Foreign procedures use a Jai-style library declaration followed by #foreign. The special host_api system library denotes an explicit portable host capability, available only when the host supplies a matching binding.

libc :: #system_library "libc";
Abs :: (value: s32) -> s32 #foreign libc "abs";

host_api :: #system_library "host_api";
EchoHost :: (value: s32) -> s32 #foreign host_api;

The portable runner checks required capabilities before execution. Supported host values are narrower than native FFI; see the C host API.

07 / Boundaries

What to check before building

Ziran rejects the former Kryon-specific #ui modifier and app/route syntax. It also rejects #enum, C-style switch, goto labels, raw C statements, variant, payload match, postfix ?, and capturing slot bodies. Jai compatibility is still being audited.

Native and portable backends do not yet accept the same full feature set. Read the status summary, then use implementation status for the exact current boundary. The intended syntax and future contracts are described in language direction.