A language built for Developer & Agentic Experience, Portability, Performance, and Efficiency. (iyi is Turkish for "good".)
Those four are one design decision seen from four sides. A module is the unit of compilation and it is compiled against its dependencies' declarations, never their bodies — so a build reads less, a program carries less, a tool can read an interface without a repository, and a person waits less.
| measured | |
|---|---|
| Developer experience | edit one module in a 7,207-line project and rebuild: 0.13 s, against Crystal's 1.17 s and go build's 0.16 s |
| Agentic experience | iyi mod context hands an edit the exact surface of every import at 52–59% of the sources' size, and a model writing against it spent 31–43% fewer prompt tokens (bench/context_pack.py, AI_FIRST.md §5) |
| Portability | an iyi program compiles for nine targets and is run on five of them every build: x86-64 glibc, x86-64 musl, aarch64 under emulation, wasm32-wasi under wasmtime, and aarch64-darwin natively |
| Performance | native code through LLVM, and a front end that answers hello in 0.034 s. At run time, within noise of Crystal's library wherever both do the same work |
| Efficiency | that hello is a 36 KB binary that starts in 1.6 ms; the same program with Crystal's library is 1,553 KB and 3.2 ms |
Build times: Linux, AMD Ryzen AI 9 465 under WSL2. Binary size and start-up:
macOS arm64. Every number in this README names the command that prints it, and
BENCHMARKS.md has the machines, the commands and the full
tables. Line counts are wc -l, and python3 bench/doc_numbers.py fails when
one drifts from the tree.
Linux x86-64, or macOS 14.4 or later on arm64, into ~/.local:
curl -fsSL https://raw.githubusercontent.com/iyilang/iyi/master/install.sh | sh
~/.local/bin/iyi run ~/.local/share/iyi/samples/hello.iyiWindows x86-64, with the Visual C++ build tools installed:
irm https://raw.githubusercontent.com/iyilang/iyi/master/install.ps1 | iex
& "$env:LOCALAPPDATA\Programs\iyi\bin\iyi.exe" run "$env:LOCALAPPDATA\Programs\iyi\share\iyi\samples\hello.iyi"Both check the release's checksums before unpacking anything. Installing by hand, building from source, and what a compiled program needs on the machine it runs on are in INSTALL.md.
Two files. app/greeter.iyi is a module, and its path is its file's path:
module app/greeter
pub def polite(name : String) : String
"Hello, #{name}."
endmain.iyi imports it, and names what it wants in scope on the same line. The
library does not get to put them there:
module main
import app/greeter::{polite}
puts polite("world")$ iyi run main.iyi
Hello, world.Now the part the language is for. Build it once, keeping each module's artifact, then delete the library's source and build again:
$ iyi build --emit-iyimod mods -o hello main.iyi
$ rm -r app # the library, gone
$ iyi build --use-iyimod mods -o hello main.iyi
$ ./hello
Hello, world.The second build never sees polite's body. It reads
mods/app/greeter.iyimod, which carries the declarations it type-checks
against and the machine code it links, and that is R-1: a module compiles
against what its imports say, not against what they do.
iyi is its own language, and it is compatible with
Crystal. The same compiler builds .cr files,
iyi build --crystal gives a program Crystal's standard library, and
require "kemal" in an iyi file serves HTTP. A .iyi file also has rules
Crystal does not have and refuses things Crystal accepts, and those rules are
the whole of what follows. The compiler is built on Crystal's, which is
recorded in the licence and NOTICE.md.
One script writes the same program in iyi, in Crystal and in Go: 30 modules, 10 types each, 7,207 lines. The benchmark changes a single number in one function of one module, then rebuilds:
The bars run in real time, and … is --use-iyimod mods --emit-iyimod mods.
Drawn by the run that measured it: python3 bench/incremental.py --svg doc/assets/edit-loop.anim.svg.
The Crystal column is this same compiler under the one rule iyi drops. A
Crystal class is open until the last line of the last file, so no build may
trust anything it read last time, and every rebuild reads all 7,207 lines
again. Take that rule away and the line you just changed costs 9x less:
the twenty-nine modules you did not touch arrive as declarations.
bench/incremental.py refuses to start the clock until all three programs
print the same total. BENCHMARKS.md has the
generated module, every row of the table, and the sessions it was taken in.
The design is in SPEC.md, which records what was measured, what was thrown away and why.
Where it loses, and what is young:
- A full build of 6,912 never-seen lines is 0.24 s against
go build's 0.09 s (BENCHMARKS.md). - iyi's own prelude holds what a small command-line tool needs —
puts, stdin, whole-fileFile,Program.args/.env— and no more. Sockets, format strings and TLS are opt-in modules (import std/socket,std/format,std/tls).std/tlshas not been audited and is not to be called secure until it is; SECURITY_REVIEW_TLS.md is an automated review, not that audit. - Concurrency (
group/spawn,Channel, cancellation; SPEC.md III.4) runs on Linux (x86_64, aarch64), macOS arm64 and Windows x86-64, and not on wasm32. One verb starts a task:g.spawn { }runs it on a pool of one worker per core where the compiler can prove that safe and on the spawner's own thread otherwise, andiyi checknotes each one it kept there, with why;g.spawn(unit: :new)asks for the pool, for work that computes, and may capture onlySharevalues;g.spawn(serial: true)keeps the task on its spawner's thread. std/http'sServer.serveuses the pool by default (BENCHMARKS.md measures it against Go). - Dependencies resolve and pin (
iyi.mod,iyi get,iyi.sum; SPEC.md III.7), but there is no registry and no artifact distribution yet, and--crystalstill leans on shards.
A web framework's DSL, with the Sinatra feel intact. The library exports names. The program picks which ones it wants unqualified. Nothing gets injected into anybody's namespace.
module samples/webapp
import kemal/dsl::* # get, post, mount: because this file asked
import kemal/router::{Router, Context}
get "/" do |env|
"home"
end
get "/count" do |env|
42 # anything implementing IntoBody, not just String
end
users = Router.new
users.namespace "/admin" do |admin|
admin.get "/dashboard" do |env|
"dashboard"
end
end
mount "/v1", usersThat file is samples/iyi/webapp.iyi, a port of
Kemal's router, and it runs. Now add a route whose
handler returns something a body cannot be made of:
get "/bad" do |env|
[1, 2, 3]
end$ 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`Kemal cannot say this. It accepts the same block and serves an empty body, on every request, forever. Here the handler's return type is a type parameter bounded by a trait, so the framework's promise is checked at the line where the mistake is, by a module that has never heard of your types.
Build it with --emit-iyimod mods, rm -rf samples/iyi/kemal, and build it
again with --use-iyimod mods: it links, runs and prints the same, without
the framework's source on the machine.
| Rule | |
|---|---|
| R-1 | A module is the unit of compilation. import forms a DAG. Compiling a module reads its dependencies' declarations, never their bodies. |
| R-2 | Everything a module exports (pub) writes down full parameter and return types. Unexported code infers as usual. |
| R-2b | import X::{a, b} brings exported names into unqualified scope, import X::* every one; plain import X keeps them qualified; pub import X re-exports the module. The consumer writes it, not the library. |
| R-2c | A def whose parameters and return are all written is typed at its definition, caller or no caller. A build, check and the LSP cannot disagree about "clean". |
| R-3 | No open classes. impl Trait for Type lives in the module that declares the trait or the type. |
SPEC.md has the full text, and R-4 and R-5 beyond these.
R-1 is what a .iyimod file is: a module's declarations, the bodies a consumer
has to compile for itself, and its machine code, in one file. R-3 is what lets a
consumer answer "does this type implement that trait?" without reading anything
else.
flowchart LR
S["app/greeter.iyi<br/>source"] -->|"iyi build --emit-iyimod"| A["app/greeter.iyimod<br/>declarations · macros · object code"]
A -->|"iyi build --use-iyimod"| P["your program<br/>type-checked, linked, run"]
S -.->|"never read by the consumer"| P
The dotted line is the whole design. A consumer type-checks against the declarations and links against the object code, and the source of the module it imports may not exist on the machine at all.
Agentic experience — the mechanism is built, the claim is young. A module's interface is a file rather than a convention, so a tool can read what a module offers without the repository that produced it, and check a change against it without building the world. "Did this change reach anybody?" has an answer:
$ 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.Rename a local and it says the interface is unchanged; --exit-code makes the
verdict a branch in a script. The loop is one verb per step:
iyi mod context --jsonprints the surface an edit may rely on, with no body in it;--budget Ncuts it further. The 52–59% above was measured on two samples whose imports carry bodies (the Kemal port andcalc); it is not a general ratio, and on some modules a pack is no smaller than the sources.iyi check -f jsonis the verdict as data, withsuggested_editspans, andiyi fixapplies them until nothing changes.iyi test --affectedruns only what an edit can reach, andiyi run --sandboxruns generated code against a runtime holding nothing.iyi lspandiyi mcpserve the same verbs over stdio, byte for byte what the CLI prints. There is no agent mode.
The token figures are the model's, over the runs written up in AI_FIRST.md §5: 31–43% fewer prompt tokens, with rounds to green tied three times and lost three.
Portability — compiles for nine, runs on five. CI type-checks the library
for all nine targets every build, runs a program on five and compares what it
prints with what it printed on the machine that compiled it. Windows x86-64 is
not one of the five, but it is built and tested natively: eight CI jobs run on
Windows, and a failure in any of them stops a release. On Linux,
iyi build --static gives a binary with no dynamic loader at all.
INSTALL.md lists the nine and what each is held to.
Performance. Native code through LLVM. At run time iyi's library is within
noise of Crystal's where the two do the same work, ahead on Hash and behind
on String building (BENCHMARKS.md). The collector
is iyi's own, precise for typed objects and marking in parallel beside the
program; python3 bench/gc_race.py has iyi's wall time ahead of Boehm's and
Go's on all three of its programs, on the machine GC_DESIGN.md
records, along with the one pause the design does not bound.
Efficiency — mostly subtraction. Of the thirteen libraries Crystal's published list requires, a plain build with iyi's own prelude links the platform's C library and nothing else: no libgc, no libevent, no openssl, no zlib (INSTALL.md has each platform). A program links only what it uses, and iyi's own library is 28,806 lines, written to be linked that way. That library is 1,213 KB on disk beside the binary.
Sizes and start times are a plain iyi build on macOS arm64 with LLVM 22,
and move with the platform; python3 bench/machine_probe.py prints your
machine beside three reference timings.
iyi init kemal
iyi run kemal.iyi # hello from kemaliyi init MODULE [DIR] writes iyi.mod, naming the module by the path other
projects would import it by (example.com/me/kemal, or a bare kemal), and
kemal.iyi, the root module that path promises. With no MODULE the current
directory's name is the module: iyi init in my-project writes module my-project and my_project.iyi. It never writes over a file that is there.
A test is a *_test.iyi beside it, a program that passes by exiting 0, run by
iyi test. iyi test --chaos runs each test under 100 seeded schedules, so a
test that passes only because its tasks happened to run in one order fails on
some seed. --chaos=SEED runs that failing schedule again (SPEC.md III.4.17).
A dependency is one line in iyi.mod, require example.com/someone/lib v1.2.0, and iyi get example.com/someone/lib writes it: the latest release
tag, or @v1.2.0, @main, @a1b2c3d, resolved by minimal version selection
over git tags and recorded in iyi.sum. From there:
iyi get -uupgrades every requirement (--checklists which are behind), andiyi get PATH --as webgives one a short name forimport web/dsl::{get}.iyi mod tidyadds what the source imports and removes what it does not.getsays what a new or upgraded package reaches outside the language (the std modules that call C,File, the C it declares),iyi mod reachlists the whole build's, andrequire ... reaches std/filemakes that a limit.iyi mod release v1.3.0refuses a version that says less than the exported surface changed since the last tag.replace example.com/someone/lib => ../libbuilds one from a directory beside the project.
Looking for a package? iyipacks is a community directory of iyi's standard library modules and iyi packages on GitHub.
iyi lsp speaks the Language Server Protocol over stdio; there is nothing
to configure. Neovim 0.11+:
vim.filetype.add { extension = { iyi = "iyi" } }
vim.lsp.config("iyi", { cmd = { "iyi", "lsp" }, filetypes = { "iyi" } })
vim.lsp.enable("iyi")Any LSP client works the same way: command iyi lsp, filetype .iyi;
editors/ carries a VS Code extension and configs for Helix and
Zed. Diagnostics arrive on every keystroke from a real compile of the module
alone — 36 ms p50 / 55 ms p95 across the sample corpus
(bench/lsp_latency.py). Completion, references, rename and hover ride the
typed graph, and two requests beyond the protocol, iyi/contextPack and
iyi/surface, return what the CLI prints. A session rests at about 100 MB
(bench/lsp_memory.py), and iyi tool format formats.
Traits, and impls for a generic type. Nothing to reopen, nothing to monkey
patch. A type gets behaviour from an impl, and that impl lives with the
trait or with the type.
pub trait Show
abstract def show : String
end
pub struct Box(T)
getter value : T
def initialize(@value : T)
end
end
impl Show for Box(T) forall T # forall introduces T, and is required
def show : String
"Box(#{value})"
end
end
puts Box.new(41).show # => Box(41)
puts Box.new("hi").show # => Box(hi)forall is not ceremony: without it, whether T names a new parameter or a
type already in scope would depend on what the file imports, and a library
could change the meaning of your impl by adding an export.
One each, seventy more methods. src/std/enumerable.iyi is
Enumerable ported to a trait: one requirement, 70 defaults. An
impl Enumerable for Nums that writes each and answers type Elem = Int32
gets map, sorted, first and the rest, checked once at the impl, not at
every call.
Errors are ordinary union members. No Result wrapper, no exception
hierarchy, no new machinery. A union already carries a type id. What makes a
member an error member is that its type implements Error.
pub def load(path : String) : Int32 | IOError | ParseError
text = read(path)! # ! returns the error member from here,
to_number(text)! # or narrows to the value and carries on
end
case load(path)
in Int32 then puts "read #{it}"
in IOError then puts "io: #{it.message}"
in ParseError then puts "parse: #{it.message}"
endAdd an error to load's return type and every caller hears about it, because an
incomplete case does not compile. Delete the ParseError branch from the
sample and the compiler names what is missing:
$ iyi build samples/iyi/errors.iyi
In errors.iyi:64:3
64 | case load(path)
^
Error: case is not exhaustive.
Missing types:
- Samples::Errors::ParseErrorThe rules are new, so the compiler teaches them where they are broken. Import a module and call one of its functions the way every other language would:
Error: undefined method 'polite' for App::Main:Module
`polite` is exported by `app/greeter`, and this file has not brought it into
scope. Import it by name, `import app/greeter::{polite}`, or call it as
`App::Greeter.polite` (SPEC.md R-2b)Call App::Greeter.polite from a file that never imported app/greeter,
because some other file did:
Error: `App::Greeter` is not imported here — it is in the program only because
some other file imported it. Add an import in this file, or have a module this
file imports re-export it with `pub import` (SPEC.md R-1)Write an impl in a module that owns neither the trait nor the type, which is
the one R-3 exists for:
Error: can't implement Lib::Shape::Drawable for Lib::Shape::Circle in App::Main:
an impl must live in the module that defines the trait (Lib::Shape) or the
module that defines the type (Lib::Shape). This is R-3, the orphan rule, and it
is what lets coherence be checked without a global passEach one names the rule and the line to write.
Twenty-eight programs in samples/iyi. Twenty document a
part of the design: hello (traits and impl), modules (import across
files), generics, errors, collections, immutable (a shareable
collection), init_order, webapp, workers (a pool and a typed pair of
tasks), cores (spawn(unit: :new), a server on every core, @[SafeReach]),
derive, files, formatting, format (format strings), enums,
io (standard input a line at a time), socket (TCP on loopback), and
std_iterator, std_text and std_time. calc is a language. The other
seven — basics, inventory, config, grid, shapes, sessions,
visited — are the programs a person writes in a first hour, and they are
there because the prelude grows only when a program in this repository needs
something.
R-1 is checked rather than asserted: bash bench/samples_roundtrip.sh builds
the twelve samples that import anything, deletes every imported module's
source, builds again from the artifacts and compares what the two programs
print. CI runs it on every push to master. An artifact is readable too —
iyi mod dump prints one as text — and each section carries a checksum, so a
truncated or damaged .iyimod is refused by name rather than compiled
against.
The same compiler builds .cr files unchanged, and iyi build --crystal lets
an iyi program require any of them — Kemal, db, ameba, the standard
library. The other direction does not work: a Crystal program cannot require an
iyi module, because R-2's written types and R-3's closed types are what an
artifact is made of.
The syntax is Crystal's. What moved is where things may be written:
| Crystal | iyi | why |
|---|---|---|
require "foo" pulls a file into the program |
import app/foo names a module, and the module's path is its file's path |
R-1: a module is a unit, so it has a name rather than a location in a concatenation |
a required file's names are simply there |
import app/foo::{bar} brings them in, written by the consumer |
R-2b: a library cannot take a name in your file |
reopen any class, anywhere, including String |
you may not reopen | R-3: what a type is, is settled where it is written |
include/extend a module into a class |
trait and impl Trait for Type, in the trait's module or the type's |
R-3's orphan rule, which is what makes coherence checkable without reading the program |
abstract def in a module |
abstract def in a trait, and the trait is a type |
II.6 |
everything is public unless private |
everything is the module's own unless pub, and pub writes its types |
R-2 |
shards, shard.yml |
--crystal can require shards from IYI_PATH; iyi bind puts every shard under lib/ behind a boundary, one .iyimod each, and a program imports it |
a required shard's source is compiled into the program, a bound one's object code is linked (SPEC.md III.6) |
| a Crystal project you already have | iyi migrate SRC --out DIR --annotate writes it as iyi modules |
below |
| macros | kept, and they travel in the artifact | |
Nil, union types, blocks, local inference |
kept, unchanged |
A prelude is a library, and the rules are the language, so a program can keep
one and change the other. iyi build --crystal gives a program Crystal's
standard library, and there require means what it means in Crystal. This is
a Kemal server, and it runs:
module site
require "kemal"
get "/" do |env|
"Hello from an iyi program"
end
get "/json" do |env|
env.response.content_type = "application/json"
{message: "iyi"}.to_json
end
Kemal.run$ iyi build --crystal -o site site.iyi
$ ./site &
$ curl localhost:3000/json
{"message":"iyi"}Nothing about the language changes. The module header, import, pub,
traits, impl … forall, error unions and !, .or, or_panic, defer — all
of them work in a program that requires a shard, and R-2 still refuses an
export that does not write its types. What changes is what the program has:
Crystal's standard library instead of 28,806 lines of iyi's own prelude.
One class of names is unreachable. ! in iyi propagates an error, so a
method whose name ends in one cannot be called from a .iyi file. Crystal's
standard library has 50 such names, not_nil!, sort!, map!, select!
and uniq! among them. What replaces them is what Crystal writes anyway when
it wants a copy or a narrowing: a = a.sort, if home = maybe,
maybe.as(String). Shard code is .cr and unaffected.
What it costs is R-1, for that dependency. A required shard is read from
source, so your edit loop pays for it the way Crystal's does; your own modules
still get artifacts, since --crystal and --use-iyimod work together. On a
twelve-module app the modules cost 0.16 s from source and nothing from
artifacts, against 3.1 s for Crystal's library itself. iyi daemon start
keeps that library analysed between builds, about 0.3 s off the front end for
roughly 200 MB of memory; build with iyi daemon build, or set
CRYSTAL_DAEMON_SOCKET and keep iyi build (SPEC.md IV.1d).
The two libraries do not mix. A .iyimod records which one it was built
against, and importing across is refused by name: String is a different type
in each, and a mixed program would link anyway.
Nine shards were swept through it, each built as an iyi program and as a
Crystal one: kemal, db, ameba, habitat, baked_file_system, radix,
sqlite3, the standard library's own json/yaml/uri/http, and a program
that round-trips JSON::Serializable. All nine behave the same in both
languages. One needed a word changed, and it was the rule working: habitat's
macro reaches a type in your module by name, so the class needs pub.
$ iyi migrate src --out iyi --annotate --check
$ cd iyi && iyi build --crystal -o app <entry>.iyiThe namespace becomes the path, qualified names become names on import
lines, an import cycle becomes one module, and a reopened foreign type stays
Crystal beside its module. --annotate writes the types R-2 asks for by
reading them off the program the compiler already typed. --check compiles
each module written and, for each refusal, says where the cause lies: the
environment, your code, or a limit of migrate itself. On a 99-file,
8,079-line Kemal application that is 91 modules, all of which compile, and the
program answers the Crystal build byte for byte on every route tried.
Is this Crystal with a flag? No. A .iyi file has rules Crystal does not
have and refuses things Crystal accepts: a module header that makes the file a
compilation unit, pub with types on everything exported, no open classes,
impl where the trait or the type lives, errors as ordinary union members.
Separate compilation is not a feature you add to a language with open classes;
it is a rule a language is designed around.
Is this meant to replace Crystal? No. Crystal is not going to drop open classes, and it should not.
Will it merge back? The bug fixes found in Crystal's own compiler while building iyi should, and they are separate commits for that reason. The rules will not.
Is the syntax stable? No. 0.21.0 is a release of a language that is still moving, and the parts of SPEC.md marked PROPOSED are exactly the parts that will move.
Who is this for right now? Somebody who wants to check the claim, read the
design, or argue with a number. With --crystal, a program that requires
shards is buildable today; what should give you pause is the language moving,
not the library.
- iyi's own library is 28,806 lines, and its IO is
puts,print, the three standard streams andFile.gets,read_all,writeandflushon the streams;File.read,File.write,File.exists?,File.deleteandFile.open(path, mode). Its collections carry a method because a program in this repository needed it, so much of what you reach for is insrc/std/instead (std/enumerableas trait defaults) or under--crystal. - wasm32 has no concurrency runtime. A
groupthere fails to compile rather than run sequentially. - Only the threads and parallel tasks are gated by
Share. A block passed toIyiThread.start,spawn(unit: :new)or a def marked@[Share](std/http'sServer.serve, unless givenworkers: 1) may capture onlySharevalues; aspawnis not gated, and one outside the pool is never preempted; a parallel task gives its worker up after 5 to 10 ms of computing. - No registry and no self-hosting. Packages resolve from git; the compiler is still a Crystal program.
- Windows has gaps. No IANA zone database of its own (local time comes
from
GetTimeZoneInformation), symbolic links need Developer Mode or an administrator, the daemon is POSIX-only, and arm64 Windows runs the x86-64 build under emulation. - Artifacts are identified by released version, target and flag set. Any
other
.iyimodis rejected and rebuilt, never migrated, and a-devbuild reads only its own. - A derive reads upwards.
derive <macro>runs once, in the module that declares the type, and sees only the declarations above it. - Macros are not hygienic. A macro is pasted text, so one that writes
tmp = 99assigns to yourtmpif you have one — Crystal's semantics, kept.
| SPEC.md | the design, and the record of what measurement settled |
| INSTALL.md | installing by hand, building from source, targets, and what a program depends on |
| BENCHMARKS.md | the build, run-time and server measurements, with their machines and commands |
samples/iyi |
twenty-eight programs: twenty documenting a part of it, seven being a first hour, and calc, a language |
samples/crystal/kemal |
a kemal application, from shard.yml: built from source and across four .iyimod boundaries |
src/iyi |
iyi's own library, 28,806 lines. --crystal swaps it for Crystal's |
src/std |
the standard library, in iyi. Opt-in with import std/..., and not counted against the prelude's size limit (SPEC.md I.4) |
src/compiler/iyi/iyimod.cr |
the artifact format |
bench/ |
every script a number in these documents names |
| GC_DESIGN.md | the collector |
| AI_FIRST.md | the agent loop, and every recorded run |
| CHANGELOG.md | what is in a release, and what a later one has to keep faith with |
| README.crystal.md | Crystal's own README, kept |
- Discord: questions, help with a build, and talking through a design before it becomes an issue.
- r/iyilang: longer posts, things you built, and discussion that should outlive a chat scroll.
- @iyi_lang on X: releases and announcements.
- GitHub issues: bugs and feature requests, ideally with the smallest program that shows it.
A vulnerability goes to a private security advisory, not to any of the above; SECURITY.md says where, and which code it covers. CONTRIBUTING.md is how to send a change, and CODE_OF_CONDUCT.md applies in every project space.
iyi's compiler is built on the Crystal compiler and carries Crystal's licence
and copyright: Apache 2.0, Copyright 2012-2026 Manas Technology Solutions. See
LICENSE and NOTICE.md. Everything here that is not
Crystal's is a change to Crystal's source. iyi --version reports
iyi 0.21.0 (built on Crystal 1.22.0-dev): the language first, then what it
is built on. The compatibility binary in the same checkout still reports itself
as Crystal 1.22.0-dev, because that is what it is. This paragraph is a licence
obligation and an accurate one; the language above it is iyi's own.
