Learn
Eight steps, one rule each
iyi is Crystal with one rule added, and that rule changes enough that starting from the syntax would teach you the wrong thing. So these steps start from the rule, in the order the rules depend on each other.
Each step opens with the rule in the specification's own words, shows the program it is grounded in, breaks the rule on purpose, and prints what the compiler said. Every listing is read out of the repository when the page is built, and every error is a recording of a real run.
The path
Each identifier is the specification's own, and the sentence it states for it is beside the step.
- 1R-1
Delete the library, build again
A module is the unit of compilation, and a consumer compiles against what its imports declare rather than against what they do. Build once keeping each module's artifact, delete the library's source, and build again.
A module is the unit of compilation.
importforms a DAG. Compiling a module reads only its dependencies' export metadata, never their bodies. - 2R-2
What a module writes down
If a consumer compiles against declarations, the declarations have to be complete. So an exported name carries full parameter and return types, and everything inside the module goes on inferring.
Everything a module exports (
pub) carries full parameter and return types. Non-exported code infers. - 3R-2b
One keyword, and your own namespace
importloads a module and gives you its qualified name. The list or the glob written after the path,::{...}or::*, is what shortens that, and you write it, so no library can change what a name in your file means by adding an export of its own.One keyword loads a module and names what it brings into scope, written by the consumer.
import XloadsXand leaves its names qualified (X::f);import X::{a, b}also bringsaandbinto unqualified scope;import X::*brings every exported name.pub import Xre-exports the module, never names. - 4R-3
Traits, and the end of open classes
A trait names requirements and an impl satisfies them. The orphan rule decides where that impl is allowed to live, and
forallbinds the parameters an impl or a method quantifies over.Open classes are gone.
impl Trait for Typemust live in the module defining the trait or the type. - 5R-2c
Typed where it is written
A def that writes all of its types is typed at its definition, caller or no caller. A trait-restricted parameter is typed against exactly its bound, so a generic body cannot use what it did not declare.
Definition-site typing. A def whose parameters and return are all written is typed at its definition, caller or no caller — R-2's declared types stand in for the missing call. A trait-restricted parameter is typed too: the bound is written, and a bound is enough — the compiler synthesizes one witness type per simple trait, implements its abstract requirements as stubs, and types the body against exactly the bound, so a generic body cannot quietly use what it did not declare (the half duck-typed generics never check and Rust checks always).
pubis not the condition: a module's unmarked function, a type'sprivate def, a type the module never markedpub, and a method animplblock gives a type are typed at their definition too, since the probe is the definition asking about itself and R-2's wall is for callers. The full rule → - 6R-4
A generic that crosses a boundary
Inside a module a generic monomorphizes. Crossing a module boundary it is supposed to pass a dictionary keyed on GC shape, and that half of the rule has nothing behind it yet, so the artifact carries the body and the consumer compiles it again.
Generic calls crossing a module boundary pass a dictionary keyed on GC shape. Within a module, monomorphisation.
@[Monomorphize]forces specialisation across a boundary. - 7R-5
A macro that can only see one declaration
A macro is attached to a declaration with
derive, it reads that declaration and what its field types implement, and it sees nothing global. So it runs once, in the module that declares the type, and what it generated travels in the artifact.Macros are derive-scoped: they see the declaration they are attached to, and nothing global.
- 8III.1
Errors you cannot ignore
A function that can fail returns a union.
!propagates an error member and narrows the rest, an exhaustive case is already total, and adding an error to a signature stops every caller compiling.Errors are ordinary union members. No
Resultwrapper, no exception hierarchy, no new type machinery: unions already exist and already carry a type id.
The second track
The syntax, in 33 short programs
The rules are what make iyi a different language from the one it forked. The tour is everything else, in the order a person learns a language, each program running in your browser. Read the rules first and the tour reads as confirmation. Read the tour first and the rules read as the reason.
- 01
Welcome
One program, to see the shape of the thing. - 02
Basics
Modules, variables, types and functions: what every program is made of. - 03
Flow control
Loops, ranges, conditions, case, and cleanup with defer. - 04
Data
Structs, classes, arrays, hashes, tuples and blocks. - 05
Methods and traits
Behaviour on types, traits that name it, unions and errors as values. - 06
Generics
Functions and types that work for any type, and bounds that say which. - 07
Concurrency
Tasks in a group, and channels between them.
And the rest of the samples
Beyond both tracks, samples/iyi holds 27 programs, each written to document a part of the design. The ones no step is grounded in are the ones to read next:
init_order.iyiModule initialisation order: SPEC.md III.5.webapp.iyiThe Sinatra feel, kept — but written by the consumer rather than injected by the library.