Skip to content

iyi 0.2.0

Choose a tag to compare

@sdogruyol sdogruyol released this 20 Aug 12:39
· 3458 commits to master since this release

A program chooses its library. 0.1.0 had one, 1,184 lines of it, and that
was most of what stood between the language and anybody's real program. It
turned out not to be a library problem: a prelude is a library and the rules
are the language, so a program can keep one and change the other. --crystal
does, and there require means what it means in Crystal. Nine shards were
swept through it and a Kemal server written in iyi serves HTTP; how many of the
rest work is not something this release measured.

Two more entries take the rules further out — pub reaches a macro and a
constant — and closing each found the same hole underneath: a surface nobody
wrote and nobody could refuse.

Master is 0.2.0-dev, and under the artifact rule below that means every build
of it interoperates with nothing but itself. That is the point: a version
between two releases names no compiler.

Changed

  • The tarball carries Crystal's library, so --crystal works in what people
    download.
    It shipped iyi's own 56 KB and nothing else, so the release's
    headline feature answered require "json" with "can't find file" outside a
    checkout. It ships both now — 13.4 MB to 14.9 MB — and CI runs a --crystal
    program out of the unpacked tarball, which is where this was found and where
    it would have been found again.

  • make cli_spec says once when the daemon and the compiler are different
    builds.
    The daemon refuses a client built from another compiler, correctly
    — it holds an analysed prelude — but the spec saw that as nine failures, each
    printing two version strings, with the reason in none of them. It is easy to
    arrive at, too: the build commit comes from git HEAD while make compares file
    times, so a commit can leave two binaries disagreeing about a commit while
    agreeing about every line of code.

  • iyi mod diff says whether a change reaches a module's consumers. The
    three hashes an artifact carries already answered it and nothing asked them.
    It compares two .iyimod files, says which of interface, implementation and
    source moved — with what each of the three means, because the middle one is
    the surprising one — and names the exports that came and went when the
    interface is what moved. --exit-code exits 1 in that case, which is
    git diff's spelling and its reason: the answer is not a failure.

  • An iyi program is run on three targets every build, not one. It compiled
    for eight and was tested on one, which is a weak thing to call portability.
    CI now cross-compiles hello.iyi for musl and for aarch64, links each with
    the target's own cc and libgc — the command --cross-compile prints —
    and runs them: in an Alpine container and under emulation. The check is that
    each prints what the same program printed on the machine that compiled it.

  • bench/runtime.py measures what the library costs at run time. The two
    libraries are within noise where they do the same work; Hash is 6x ahead
    and does less; String is 1.64x behind. The first reading said string
    building was twenty times faster, and it was the collector — a 17 KB binary
    has fewer roots to scan than a 972 KB one — so the bench reports both columns
    and the honest one is the second.

  • iyi describes itself as its own language, compatible with Crystal. "A
    language built for Developer & Agentic Experience, Portability, Performance,
    and Efficiency", and README says what stands behind each of the four and what
    does not: the edit loop and the artifact are built, portability means eight
    targets that compile and one that is tested, the run-time measurement is new
    and says the two libraries are within noise where they do the same work, and
    the agentic claim is a mechanism rather than a result.

    Compatibility is stated as something checkable and in one direction: the same
    compiler builds .cr files, an iyi program can require a shard with
    --crystal, and 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.

    iyi version reads iyi 0.2.0-dev (built on Crystal 1.22.0-dev …): the
    language first and what it is built on after. The licence and NOTICE.md are
    unchanged, because Apache 2.0's attribution is an obligation rather than a
    description.

  • An artifact is read by the release that wrote it, not by the build. A
    .iyimod was locked to the exact compiler build, commit and all, so two
    builds of the same version refused each other's modules and handing one to
    somebody meant handing them your compiler too. The identity is now the
    released version, the target and the flags: every build of iyi 0.1.0 reads
    every other build's artifacts on the same target under the same flags. A
    -dev version keeps the commit, because it names no release. The version
    comes from src/IYI_VERSION, which is also what the binary reports and what
    the tarball is named after.

Added

  • iyi build --crystal compiles a program against Crystal's standard
    library, so require reaches the ecosystem.
    A .iyi file refused
    require because there was nothing to require: the prelude is what a program
    gets. That reason stops being true when the prelude is Crystal's. The rules
    do not change with the library — the module header, pub, import, using,
    traits and impl are all still there, and a shard is ordinary Crystal
    compiled into the program.

    A Kemal server written this way serves HTTP. What it gives up is R-1 for that
    dependency: the shard is read from source, so the edit loop pays for it the
    way Crystal does. Your own modules are unaffected, and --emit-iyimod still
    writes them.

    crystal tool bind writes what it generates with --emit-bind, its own
    switch, because what it writes is a boundary for a shard rather than this
    build's own modules.

    The two libraries are two modes and do not mix on the artifact side:
    --use-iyimod and --emit-iyimod need iyi's own prelude. An artifact's
    object code numbers the types its module made, which under Crystal's library
    include the standard library's own, and a consumer compiling its own copy of
    that library has two of everything. That was an LLVM module which would not
    verify; it is a sentence now.

    Nine shards were swept through it, each built twice — as an iyi program
    and as a Crystal one, so that a difference is this fork's and a shared
    failure is the ecosystem's. 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 and writes a file. All nine behave the same in both
    languages. One needed a word changed and it was the rule working: habitat's
    macro resolves the type it is handed by name, and a class an iyi module
    leaves unmarked is private, so it needs pub class.

    samples/crystal/stdlib.iyi is the program CI builds to keep this true: a
    trait with a default, an impl on a generic, an error union with ! and
    .or, a defer, and JSON, YAML and URI in the same file.

  • pub takes a constant. pub LIMIT = 42 is reachable through the
    module's name; an unmarked constant is the module's own and is refused by the
    sentence an unmarked def gets. Nothing was added to the artifact format,
    because a module's top level already travels as source and the mark travels
    with it.

    The hole under it is the same one pub macro found: a constant's visibility
    was never set, so every constant a module declared was reachable. That is
    what a real shard leans on — Kemal hands out every object it has through one.

  • pub macro, so a macro can cross a module boundary. Every macro a module
    writes already travelled in its artifact, because a body that travels may
    call one, but none of them was reachable: pub did not take a macro. A
    marked one is now reachable exactly as a pub def is, unqualified after
    using or through the module's name, and it works against an artifact with
    the module's source deleted. What it exports is a name and an arity, because
    a macro takes syntax and returns syntax.

    Two things worth knowing. Closing this found the hole under it: a macro's
    visibility was never set, so every module's macros were already callable
    through its name — that is refused now, with the sentence an unexported def
    gets. And macros are not hygienic, so a pub macro that writes tmp = 99
    assigns to the consumer's tmp; SPEC.md IV.4 says so in full.

  • iyi tool format formats iyi. The formatter is Crystal's and knew none
    of iyi's syntax, so a module header, a pub, a trait, an impl, a
    using, a bounded forall, a where, a defer, a ! or an .or sent it
    into "there's a bug formatting this file". It knows all of them now, and a
    directory is searched for .iyi files as well as .cr ones. The prelude and
    the nine samples format to themselves, which is the test that made the last
    three of those show up.

Fixed

  • A generic imported from an artifact links again when its type argument is
    inferred.
    Box(Int32).new(42) always worked and Box.new(42) did not.
    Inference makes new a method on Box(T), which is the artifact's own type,
    and the rule that says "this type's machine code is in the artifact" was
    reading the generic itself. An artifact carries a unit for every non-generic
    type a module declares and none for a generic one, because a generic has
    machine code only once somebody picks its arguments, and the consumer is who
    picks them. So the consumer declared a new that nothing defined and the
    program failed to link, saying undefined reference to Box(T)::new<Int32>.
    The consumer's rule now matches the producer's. Reported after 0.1.0 went
    out.

  • Array#sort sorts the array, and sorted hands back a copy. SPEC.md
    III.1.7(A) settled that pair — the plain verb mutates, the participle copies,
    Swift's convention adopted because ! had to leave identifiers so postfix
    ! could propagate an error — and the prelude did not implement it: sort
    returned a copy and nothing mutated. It does now, and sorted is one line
    over it.

    Worth knowing when moving between the two libraries: Crystal names the same
    pair sort! and sort, so a.sort copies there and sorts here. It is the
    one call in this prelude that means something different under --crystal,
    and the note is in src/iyi/array.iyi where somebody is standing when it
    matters.

  • A using that cannot deliver is refused where it is written. A module
    header makes a type, and inside it that name means the module — so
    using app/count::{Tally} in a module called tally asks for a name it
    cannot have. What it said before was that Tally was not
    App::Count::Tally, at the first line that used one, with nothing pointing
    at the directive. Found by writing a command-line program.

  • String#size counts when nobody counted. Crystal reads @length == 0 on
    a non-empty string as "not counted yet" and scans; this prelude returned the
    field. A string built by Crystal's own to_json therefore printed correctly
    through iyi's puts and answered size 0 — no error, a wrong number.
    Free for every string this prelude makes, because it fills the field. Found
    while measuring what crosses between the two languages.

  • A macro that cannot see a type says why. An unresolved path stays a
    Path, so every method a macro would call on the type is undefined on that,
    and the message named Path rather than the type or the rule:
    undefined macro method 'Path#constant'. It now asks whether the path names
    a type that exists and is unexported, and says so when it does. Found by
    habitat.

  • The front end reads a .iyi file again. Refusing require in a .iyi
    file spares the prelude's own, and the flag that says so was set in the
    driver but not in crystal-front, so the front-end binary refused the line
    it had just written itself. It is bench-only and ships in nothing.

  • A version bump takes effect. src/VERSION and src/IYI_VERSION are
    compiled in with read_file, and the build did not depend on them, so
    changing the number changed nothing until something else did.