iyi

Why iyi

Your Crystal code, rebuilt 9x faster, with a compiler your agents can talk to.

One design decision seen from four sides: a module compiles against its dependencies' declarations, never their bodies. Every number on this page was measured by a gate in the repository.

First

It runs your Crystal code. Today.

iyi's compiler is Crystal's compiler. Your .cr files build unchanged, and iyi build --crystal brings Crystal's standard library, require, and your shards. There is no port to do first.

  • kemalbuilds and runs
  • dbbuilds and runs
  • amebabuilds and runs
  • sqlite3builds and runs

Coming from Crystal

What moved, and why.

The syntax is Crystal's. What moved is where things may be written. Macros are kept, and they travel in the artifact; nil, union types, blocks and local inference are kept, unchanged.

  1. require "foo"import app/fooA module has a name, and its path is its file's path.R-1
  2. a required file's names are just thereimport app/foo::{bar}The consumer writes what comes in. A library cannot take a name in your file.R-2b
  3. reopen any class, even Stringyou may not reopenWhat a type is, is settled where it is written.R-3
  4. public unless privateprivate unless pub, and pub writes its typesExports carry full types; everything inside still infers.R-2

Four of the moves, condensed from README.md. Traits are step 4 of Learn; shards and iyi migrate are in the README.

1 · Developer experience

The loop you are actually in is the loop that got fast.

Change one line in a 7,207-line project and rebuild. iyi reads the declarations of the other twenty-nine modules and none of their bodies, on the same compiler binary.

measured · one line changed in one of 30 modules · bars run at real speed
  1. $ iyi build … -o app main.iyi0.13 s
  2. $ crystal build -o app main.cr1.17 s

About 9x less

8.06x to 9.00x across 3 published sessions. The seconds moved; the ratio held, which is why the ratio is the claim.

Errors name the rule, and the fix.

A refusal says which rule it enforces and writes out the line you meant.

A framework's promise is checked at your line.

A Kemal port refuses a handler whose return a body cannot be made of. Kemal accepts the same block and serves an empty body forever.

The tools are the compiler.

iyi lsp answers from the front end a build uses, so the build, iyi check and your editor cannot disagree.

$ iyi build samples/iyi/webapp.iyi
In webapp.iyi:33:1

 33 | get "/bad" do |env|
      ^--
Error: Array(Int32) does not implement Kemal::Router::IntoBody, required by `B` in `get`
The Kemal port refusing a handler. Quoted verbatim from README.md, lines 180 to 185.

2 · Agent experience (AX)

A compiler built to be an agent's tool, not wrapped as one.

A .iyimod holds a module's declarations, with every type written down, and its machine code. A tool can read exactly what a module offers without the repository, and check a change against it without building the world.

iyi mod diff answers "did my change reach anybody?" Rename a local and the interface is unchanged. --exit-code makes that a branch in a script, or in an agent's plan.

The mechanism is built and the claim is young. If the rules turn out not to be enough for agents, that is a thing to measure rather than to promise.

See the agent loop, recorded →
iyi mod diff
$ iyi mod diff before/app/greeter.iyimod after/app/greeter.iyimod
module          app/greeter
interface       changed    what a consumer type-checks against
implementation  unchanged  the bodies a consumer compiles: macros, generics, the initialiser
source          changed    the file
dependencies    unchanged  what this module was compiled against

  gone   def polite(name : String) : String
  gone   def title : String
  new    def polite(name : String, formal : Bool) : String

Consumers have to be rebuilt: what they compile against moved.

3 · Portability

Compiles for 9. Runs on five, every build.

CI type-checks the library for all 9 on every push, and runs five: the check is that they print what the same program printed where it was compiled. The wasm32 build is one self-contained module, which is why the tour runs in your browser.

The five that run are x86-64 glibc, x86-64 musl, aarch64 under emulation, aarch64-darwin natively, wasm32-wasi under wasmtime. Of the 9, seven have the object they emit audited for what it asks the machine for; x86_64-linux-musl and arm-linux-gnueabihf are not. Windows is not in the five, and is no longer only compiled for either: 8 jobs execute on a Windows machine every build, and every one is on the list a tag waits for, so a failure there stops a release. The platforms page names them with their lines.

  • x86_64-linux-gnu● runs
  • x86_64-linux-musl● runs
  • aarch64-linux-gnu● runs
  • aarch64-darwin● runs
  • wasm32-wasi● runs
  • arm-linux-gnueabihf○ compiles
  • x86_64-darwin○ compiles
  • x86_64-w64-mingw32○ compiles
  • x86_64-windows-msvc○ compiles

4 · Performance and efficiency

Native code, a small program, and a front end that answers before you notice.

iyi generates native code through LLVM. At run time the two libraries are within noise where they do the same work. What changes is what a program carries: it links only what it uses, and iyi's own library is 22,456 lines against Crystal's 8,161, 932 KB on disk beside the binary.

measured · puts "hello", drawn at true area
iyi
36 KBstarts in 1.6 ms
Crystal's lib
1,553 KBstarts in 3.2 ms

43x smaller

and starts 2.0x sooner.

measured · the front end answering hello, before any code is generated

0.031 s

The front end answers hello: parse, check, and refuse or accept, before any code is generated. It is why an editor and an agent can ask the compiler on every keystroke.

measured · where it loses · the same program under both libraries

1.80x slower

at building strings, with the collector off. As it runs, the same workload is 1.23x, within noise. The collector masks a slower builder. Cold builds of unseen code are slower; the speed is on the edit loop.

Young parts, named: iyi's own prelude carries the small-tool floor and no more, so it has no TLS and no serialization, and sockets and format strings live in std/socket and std/format, outside the prelude every program carries; concurrency runs on Linux, macOS arm64 and Windows x86-64, and wasm32 gets no runtime at all rather than a sequential imitation of one; dependencies resolve and pin, but there is no registry and no artifact distribution yet, and --crystal still leans on shards.

Check it yourself.

Every number above is one of two things: counted from the tree, where it is the same on your machine as on ours, or recorded, in which case it carries the command that prints it and, where the number is a duration, the machine it ran on.