iyi

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.

  1. 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. import forms a DAG. Compiling a module reads only its dependencies' export metadata, never their bodies.

    grounded insamples/iyi/modules.iyibreakstwo modules that import each other
  2. 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.

    grounded insamples/iyi/calc.iyibreaksan exported def that does not say what it returns · a name the module never exported · a body that does not honour its written return type
  3. 3R-2b

    One keyword, and your own namespace

    import loads 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 X loads X and leaves its names qualified (X::f); import X::{a, b} also brings a and b into unqualified scope; import X::* brings every exported name. pub import X re-exports the module, never names.

    grounded insamples/iyi/modules.iyibreaksan unqualified name no import brought in
  4. 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 forall binds the parameters an impl or a method quantifies over.

    Open classes are gone. impl Trait for Type must live in the module defining the trait or the type.

    grounded insamples/iyi/hello.iyibreaksan impl in a module that owns neither the trait nor the type · a type that does not implement a required trait
  5. 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). pub is not the condition: a module's unmarked function, a type's private def, a type the module never marked pub, and a method an impl block 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 →

    grounded insamples/iyi/collections.iyibreaksa body typed at its definition, reaching outside its bound
  6. 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.

    grounded insamples/iyi/immutable.iyibreaksthe annotation R-4 names, asked of a compiler that has no such name
  7. 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.

    grounded insamples/iyi/derive.iyibreaksa derive that asks about the whole program
  8. 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 Result wrapper, no exception hierarchy, no new type machinery: unions already exist and already carry a type id.

    grounded insamples/iyi/errors.iyibreaksan error member left unhandled

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.

  1. 01

    Welcome

    One program, to see the shape of the thing.
  2. 02

    Basics

    Modules, variables, types and functions: what every program is made of.
  3. 03

    Flow control

    Loops, ranges, conditions, case, and cleanup with defer.
  4. 04

    Data

    Structs, classes, arrays, hashes, tuples and blocks.
  5. 05

    Methods and traits

    Behaviour on types, traits that name it, unions and errors as values.
  6. 06

    Generics

    Functions and types that work for any type, and bounds that say which.
  7. 07

    Concurrency

    Tasks in a group, and channels between them.
native tasks and channels have no wasm target yet, so those show what the program printed natively.Start the tour at hello →

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.