iyi

Playground

samples/iyi/immutable.iyi

The shareable immutable sequence, exercised — SPEC.md III.4.7.

samples/iyi/immutable.iyi · iyi-immutable.wasm · 191 KB
# The shareable immutable sequence, exercised — SPEC.md III.4.7.
#
#   ./bin/iyi run samples/iyi/immutable.iyi
#
# `std/list.iyi` holds the type. This file is the evidence for the two claims
# made about it, because a type that merely compiles has proved neither:
#
#   * It is genuinely immutable — nothing a caller keeps a hold of can change
#     a list after it exists, which is what makes it shareable under III.4.4.
#   * It costs one `each` and gets the rest, which is II.6's claim tested on a
#     second type after `Enumerable` itself, and the first time on a *generic*
#     one.

module samples/immutable

import std/list::{List}

# Two ways of naming the same type, both across a module boundary. The second
# lets the argument be inferred, and it is written that way on purpose: the
# artifact carries no machine code for a generic, so `List(String)` is a type
# this file makes, and inference puts `new` on `List(T)` instead of on the
# instance. That difference once linked undefined and nothing here caught it,
# because every line in this file wrote the argument out.
nums = List(Int32).new([3, 1, 4, 1, 5])
words = List.new(["pear", "fig", "plum", "kiwi"])

# ---------------------------------------------------------------------------
# Its own surface: small on purpose
# ---------------------------------------------------------------------------
puts "size            #{nums.size}"
puts "empty?          #{nums.empty?} #{List(Int32).new([] of Int32).empty?}"
puts "at              #{nums.at(0)} #{nums.at(4)}"

# ---------------------------------------------------------------------------
# Immutability, demonstrated rather than asserted
# ---------------------------------------------------------------------------
#
# `appended` returns a new list and leaves this one alone. By III.1.7 the plain
# verb names the mutating form, so the participle is the only name this type
# can have — `push` and `sort` describe something it cannot do. The naming
# convention was settled before any library code existed precisely so that it
# would fall out here rather than be applied here.
longer = nums.appended(9)
puts "appended        #{longer.size} from #{nums.size}, original #{nums.to_a.join(",")}"
puts "concatenated    #{nums.concatenated(List(Int32).new([7])).to_a.join(",")} / #{nums.size}"

# The constructor copies, and this is the line that shows why it must. `source`
# is still held by this file; if the list had kept it rather than copying, a
# task holding `taken` would see it change underneath. That copy is the price
# of having no borrow checker to say "I own this now" — one copy, at the
# boundary, once.
source = [1, 2]
taken = List(Int32).new(source)
source << 3
puts "copied          list #{taken.size}, source #{source.size}"

# ---------------------------------------------------------------------------
# Everything below here is the trait's, from one `each`
# ---------------------------------------------------------------------------
puts "to_a            #{nums.to_a.join(",")}"
puts "all?/any?       #{nums.all? { |x| x > 0 }} #{nums.any? { |x| x > 4 }}"
puts "none?/one?      #{nums.none? { |x| x > 9 }} #{nums.one? { |x| x == 4 }}"
puts "count           #{nums.count { |x| x == 1 }}"
puts "find            #{nums.find { |x| x > 3 }} #{nums.find { |x| x > 9 }.inspect}"
puts "index           #{nums.index { |x| x == 4 }} #{nums.index { |x| x == 9 }.inspect}"
puts "first?/first    #{nums.first?} #{nums.first}"
puts "map             #{nums.map { |x| x * 2 }.join(",")}"
puts "flat_map        #{nums.flat_map { |x| [x, x] }.join(",")}"
puts "map_with_index  #{nums.map_with_index { |x, i| x * i }.join(",")}"
puts "select/reject   #{nums.select { |x| x > 2 }.join(",")} #{nums.reject { |x| x > 2 }.join(",")}"
puts "partition       #{nums.partition { |x| x > 2 }.map(&.join("+")).join(" | ")}"
puts "take/skip       #{nums.take(2).join(",")} #{nums.skip(3).join(",")}"
puts "take_while      #{nums.take_while { |x| x < 4 }.join(",")}"
puts "skip_while      #{nums.skip_while { |x| x < 4 }.join(",")}"
puts "reduce          #{nums.reduce(0) { |a, x| a + x }}"
puts "each_with_object #{nums.each_with_object([] of Int32) { |e, o| o << e * 10 }.join(",")}"
puts "join            #{nums.join("-")}"
puts "includes?       #{nums.includes?(4)} #{nums.includes?(9)}"
puts "max/min         #{nums.max} #{nums.min}"
puts "sum/product     #{nums.sum} #{nums.product}"
puts "tally           #{nums.tally[1]}"
puts "sorted          #{nums.sorted.join(",")}"
puts "max_by          #{words.max_by { |s| s.size }}"
puts "group_by        #{words.group_by { |s| s.size }[4].join(",")}"

# The trait reaches a generic type, which is the part that was not built until
# this file needed it. `type Elem = T` binds the associated type to the impl's
# own parameter, so `List(Int32)` enumerates `Int32` and `List(String)`
# enumerates `String` from the same impl — II.6 × II.7, which had never met.
puts "two elem types  #{nums.map { |x| x + 1 }.join(",")} / #{words.map { |s| s.size }.join(",")}"

# ---------------------------------------------------------------------------
# All of the above, with `src/std` deleted
# ---------------------------------------------------------------------------
#
#     ./bin/crystal build --no-codegen --emit-iyimod /tmp/mods samples/iyi/immutable.iyi
#     mv src/std /tmp/std-elsewhere
#     ./bin/crystal build --no-codegen --use-iyimod /tmp/mods samples/iyi/immutable.iyi   # compiles
#     mv /tmp/std-elsewhere src/std
#
# This file is the hard case for R-1's contract, not `modules.iyi`: what has to
# survive the round trip is a generic struct, a 575-line trait with an
# associated type, and a generic impl that answers it. `mod dump --declarations
# /tmp/mods/std/list.iyimod` is what the second build reads — the twelve
# headers with no bodies, and `impl … for Std::List::List(T) forall T` with its
# `type Elem = T`. Every call above is checked against those and against
# nothing else (SPEC.md IV.1f).
#
# Not codegen: artifacts from a `--no-codegen` build carry no object code, so
# the second build passes `--no-codegen` too and produces no program - one that
# links against them is refused by name. `./bin/crystal run` above is still an
# ordinary build that reads the sources.
output
Press Run.

recorded with iyi 0.18.0 (built on Crystal 1.22.0-dev [2a9653052] (2026-10-18)) · Linux 7.0.0-38-generic x86_64, QEMU Virtual CPU version 2.5+ · 2026-10-19

You can type in the pane: nothing here compiles, so what you type cannot run, and the page says which bytes it is running the moment the text stops being the recorded file. There is no standard input, no filesystem and no threads on this target, and one sample fails on purpose for exactly that reason.

Run executes iyi-immutable.wasm, the module the compiler built from this file, in your browser. How this works

Try it on your machine

Copy the program into immutable.iyi, change something, and run it. Only a compiler on your own machine can run what you wrote.

sh, after installing
$ iyi run immutable.iyi