Skip to content

Latest commit

 

History

History
194 lines (148 loc) · 10.4 KB

README.md

File metadata and controls

194 lines (148 loc) · 10.4 KB

zxc

A programming language for AI-written business systems. Write logic in ZX (TypeScript-like), wire it together in RX (XML), and compile to a native executable.

MIT License Experimental Zig 0.17.0

CLI · RX Reference · Compiler · Feature Reference · Design

Example

A checkout flow. RX is the map — every step, and what flows between them, in one file:

<!-- checkout.rx -->
<Module>
  <Call fn="subtotal" in={$in.items} />
  <Call fn="discount" in={{subtotal: $ctx.subtotal, coupon: $in.coupon}} />
  <Call fn="shipping" in={{amount: $ctx.discount, region: $in.region}} />
  <Call fn="total" in={{amount: $ctx.discount, shipping: $ctx.shipping}} />

  <Return value={$ctx.total} />
</Module>

ZX holds the logic — each step is one typed function in its own file:

// shipping.zx
export type Input = {
  amount: u64
  region: string
}

export type Output = u64

export default function (in: Input): Output {
  return match {
    in.amount >= 10000 => 0,
    in.region == "remote" => 1500,
    _ => 600
  }
}

Call no longer accepts out. Each non-void call automatically exposes its result as $ctx.<filename>: discount.zx becomes $ctx.discount. The name comes from the target filename without its suffix; Call has no name attribute. RX attributes assemble data and simple expressions; function calls and collection logic belong in ZX.

The complete checkout example includes every referenced function. From that directory, build and run:

zxc build checkout.rx --out build/checkout

./build/checkout '{"items":[{"price":4500,"quantity":2},{"price":3000,"quantity":1}],"coupon":"SAVE10","region":"remote"}'
# {"payable":10800,"shipping":0}

To change the shipping rule, an AI only needs shipping.zx — checkout.rx already tells it what goes in and what comes out. The compiler infers types across every Call, so a mismatched edit fails at build time, not in production.

Why zxc

  • Structure you can read — RX files are the call graph and data flow. No digging through function bodies.
  • Boundaries the compiler enforces — typed Input / Output per unit, ownership checks, and a dependency graph that must be acyclic.
  • Small, local changes — change a rule in one .zx file; the RX wiring stays untouched.
  • Native output — ZX compiles to Zig; release packages embed the Zig toolchain, and -no-zig packages use the Zig named by ZXC_ZIG.

How It Works

flowchart LR
  RX[".rx modules"] --> Graph["Dependency graph<br/>acyclic check"]
  ZX[".zx functions"] --> Check["Type & ownership<br/>check"]
  Graph --> Infer["Type inference<br/>across Call"]
  Check --> Infer
  Infer --> IR["IR"] --> Zig["Zig"] --> Bin["Native executable"]
Loading

Every .rx and .zx file in the entry's closure is loaded, checked and type-inferred together, then lowered to Zig. zxc embeds the official Zig toolchain, so ordinary native builds need no separate Zig installation. Contract verification requires an external SMT solver.

Compose Modules

A module is just another unit. Call.module replaces the former Call.service attribute. Call it with module and branch on its result:

<!-- order.rx -->
<Module>
  <Call module="checkout" in={$in} />

  <Switch on={$ctx.checkout.shipping}>
    <Case value={0}>
      <Return value={{payable: $ctx.checkout.payable, free_shipping: true}} />
    </Case>
  </Switch>

  <Return value={{payable: $ctx.checkout.payable, free_shipping: false}} />
</Module>

Functions form modules, modules form larger modules, and every level follows the same rules. Cycles are rejected anywhere in the graph.

Contracts

Add requires / ensures to a ZX function and prove it with an SMT solver:

// bounded_discount.zx
export type Input = {
  price: u64
  discount: u64
}

export type Output = u64

export default function (in: Input): Output
  requires (in.discount <= in.price)
  ensures (out <= in.price)
{
  return in.price - in.discount
}
zxc verify bounded_discount.zx --solver z3 --out bounded_discount.smt2
# verified: entry paths satisfy supported safety obligations and reached contracts

The command assumes Z3 is on PATH; otherwise pass its executable path to --solver.

Remove the requires and verification fails with a concrete counterexample: the subtraction can underflow. Verification covers fixed-width integers, booleans, static objects and branches.

RX Tags

Tag Purpose
<Module> Root of an .rx file; the module is identified by its file path
<Call> Call a ZX function (fn) or another RX module (module)
<Return> Return a value from the module
<Task> Group steps; optional out={...} exposes $ctx.task.<name>
<Switch> Branch on a value
<Case> A branch of Switch matching one value
<Default> The fallback branch of Switch
<Import> Declare a module dependency without calling it
<Parallel> Run supported pure Call/Task branches concurrently
<Emit> Reserved event syntax; execution is not implemented
<Store> Define application-lifetime shared memory in *.store.rx
<Object> A state object inside Store
<Field> A typed field with an initial value inside Object
<Gateway> Expose modules over HTTP in *.gateway.rx
<Group> A route prefix inside Gateway
<Route> Map a path and method to a module

See the RX Reference for attributes and nesting rules.

ZX Language

Area What ZX offers
Units One default-exported function per file, with typed Input and Output
Types u8–u64, i32, i64, f32, f64, bool, string, objects, enums, T? optionals, T[] lists, [A, B] tuples
Bindings const only, destructuring and spread; no semicolons
Control flow if, switch, match and loop expressions
Collections Capture-free map, filter, reduce
Errors const [err, res] = try expression; finite error sets and checked success narrowing
Concurrency async expression, await task, cancel task and parallel({name: () => expression})
Ownership No pointers and no clone; the compiler decides how values move
Imports Relative paths, @/ project paths and packages; imports must be acyclic
Standard lib Encoding, crypto, paths, URLs, compression, target info, files, processes and HTTP; see the module index
Native code Explicitly declared zig: and c: interfaces
Contracts requires / ensures, proven with an SMT solver

See typed errors and ZX async and parallel for examples. A task handle can be consumed at most once by await or cancel. cancel requests cooperative cancellation and waits for the task to finish. Task execution requires an I/O-capable host.

CLI

Command Description
zxc build <entry> --out <path> Build a native executable from an .rx or .zx entry
zxc build <entry> --watch Rebuild whenever sources or dependencies change
zxc build <entry> --watch --run Restart the native app after successful rebuilds
zxc build <entry> --mode lib --out <dir> Publish a library for zxc and Zig consumers
zxc build ... --target --cpu --optimize Cross-compile and tune the output
zxc build ... --asm <file> Also write the generated assembly
zxc <file.zx> --out <file.zig> Emit Zig source only
zxc fmt <source> Format ZX, RX or package configuration
zxc verify <file.zx> --solver <z3> Prove contracts and safety obligations
zxc fpga <file.zx> --out <file.sv> Generate a SystemVerilog kernel
zxc check-rx --entry <file.rx> Check RX structure and call targets
zxc pkg install Resolve and install dependencies from pkg.yaml

See the CLI docs for every option.

License

MIT