This page is the recording behind the playground: which bytes came from where, what checked them, and what the compiler actually said when it refused a program.
What is real here, precisely
The modules are real compiler output. Each came from iyi build --cross-compile --target wasm32-wasi on the machine stamped below, then a link step, because for this target the fork prints a cc --target=wasm32-wasi command rather than calling wasm-ld. Both are recorded in records/wasm/manifest.json, and scripts/records.mjs fails the build if a module's digest does not match the bytes on disk or if a sample's source has changed since the module was built from it.
The execution is real, and it is in your tab. The host in src/playground/engines/wasi-preview1.ts implements the WASI preview1 calls the modules import, and it grants what a page can honestly grant: standard output and standard error captured per write, standard input open and at end of file, an empty environment, real clocks, real randomness, and no filesystem at all. A call it does not implement returns an errno rather than trapping, so a program can see the failure instead of dying.
One execution path, whatever produced the module. A recorded sample today, and a program compiled in the tab when iyi's compiler runs there, are the same bytes flowing through the same host, so a defect in either is a defect in both and cannot hide on one side. That is why engines/execute.ts exists once rather than twice.
Nothing is instantiated without a digest, unless this tab made it. Every module the page runs today is a recorded one, checked against the manifest before it is instantiated, because its provenance is a claim about a machine you were not at. A module the compiler in the page produces needs no digest, because its provenance is that this tab made it, and the page will say which of the two it ran.
The coloring is the compiler's lexer or it is nothing, and this build checked that. There is no grammar in the playground and there will not be one. A loaded sample paints from the token stream the compiler's own highlighter recorded. When iyi's compiler runs in the page, text you type paints from that same highlighter, and only after the page recovers the text out of the stream and finds it byte identical to the characters on screen. Otherwise it paints plain ink, because a listing with no emphasis beats a listing whose every keyword sits where no compiler put it. This build turned all 57 recordings back into what a live lexer sends, re-emphasized them through the live path, and required 3036 lines and 136 rule words to come back identical. It also required the check to refuse a stream that lies, and the emphasis pass to leave an already marked recording alone. Any one of those failing stops this page from building.
The type checking here is recorded, and the reason changed. It used to be that the compiler could not report a diagnostic from wasm at all. Work on wasm/compiler-in-browser cleared that, so a checker in the page is reachable now and when one lands it feeds this same event path. The limit is running the program afterwards: a browser has no linker and producing a program out of a module needs one. So the diagnostics below are recorded.
2 of the samples do not print the same thing on both targets, on purpose. The recording marks them, the menu marks them, and the card carries the sentence. They are in the set because a target that cannot open a file is a fact about the target, and hiding them would make the other 55 less believable.
3 samples do not compile for this target at all. It has no module and no page here; the recorder kept the compiler's refusal and the reason, below, so the absence is recorded rather than quiet.
Every recorded module
One card per sample. A byte count and an exit status are recorded numbers, so they appear only inside these frames and never as bare figures; the compiler, the machine and the command that recorded every one of them are in the rail under the last card.
measured · hello.wasm from samples/tour/hello.iyi
The module printed exactly what the native binary printed
The wasm build refuses File at run time by design: src/iyi/prelude.iyi panics with "File is not available on wasm32-wasi: path_open needs a preopened directory fd", because a WASI module reaches the filesystem only through directory handles the host granted it, and this module asks for none.
measured · iyi-format.wasm from samples/iyi/format.iyi
The module printed exactly what the native binary printed
There are no sockets on wasm32-wasi to open: src/std/socket.iyi binds the calls per platform and wasm32 falls to the branch that has none, so the first line panics with "IyiSocket is not supported on this platform" and the module prints nothing. WASI preview1 can accept, read and write a socket the host handed it, and has no call that creates or binds one; natively the same program runs both ends of the exchange on loopback.
measured · iyi-std_iterator.wasm from samples/iyi/std_iterator.iyi
The module printed exactly what the native binary printed
The compiler refused this sample for wasm32-wasi, so there is no module
the refusal
Error: group is not available on wasm32-wasi: WebAssembly cannot switch stacks, wasmtime 48 disables the stack-switching proposal, wasi-threads is unsupported, and asyncify cannot run without a host event loop (SPEC.md III.4)
the lesson
Tasks and channels run on Linux, macOS arm64 and Windows x86-64, and not on wasm32 yet: a program that names `group` or `Channel` there does not compile, because src/iyi/concurrency.iyi refuses to ship a spelling without the feature (SPEC.md III.4.8).
measured · samples/tour/channels.iyi
The compiler refused this sample for wasm32-wasi, so there is no module
the refusal
Error: group is not available on wasm32-wasi: WebAssembly cannot switch stacks, wasmtime 48 disables the stack-switching proposal, wasi-threads is unsupported, and asyncify cannot run without a host event loop (SPEC.md III.4)
the lesson
Tasks and channels run on Linux, macOS arm64 and Windows x86-64, and not on wasm32 yet: a program that names `group` or `Channel` there does not compile, because src/iyi/concurrency.iyi refuses to ship a spelling without the feature (SPEC.md III.4.8).
measured · samples/iyi/workers.iyi
The compiler refused this sample for wasm32-wasi, so there is no module
the refusal
Error: group is not available on wasm32-wasi: WebAssembly cannot switch stacks, wasmtime 48 disables the stack-switching proposal, wasi-threads is unsupported, and asyncify cannot run without a host event loop (SPEC.md III.4)
the lesson
Tasks and channels run on Linux, macOS arm64 and Windows x86-64, and not on wasm32 yet: a program that names `group` or `Channel` there does not compile, because src/iyi/concurrency.iyi refuses to ship a spelling without the feature (SPEC.md III.4.8).
What the compiler says when it refuses
Recorded, not live, because nothing in this page compiles: a browser has no linker and producing a program out of a module needs one. The programs below are real files under records/break/, they are really wrong, and this is the real compiler's output on them, captured verbatim, caret line included. Where the compiler cited the rule it was enforcing, the citation is pulled into the footer, because iyi's errors naming their rule is a feature of the language and the design should say so.
two modules that import each other
records/break/r1_import_cycle.iyi, exit 1
records/break/dep/loop_b.iyiline 5, column 1
# The other half of the cycle. `dep/loop_a` imports this module and this# module imports `dep/loop_a`, which is the edge that closes it.module dep/loop_b
import dep/loop_apubdefb : String"b"end
Showing last frame. Use --error-trace for full trace.
In records/break/dep/loop_b.iyi:5:1
5 | import dep/loop_a
^Error: import cycle
dep/loop_a -> dep/loop_b -> dep/loop_a
`import` forms a DAG (R-1). A cycle has no initialisation order — a module initialises after every module it imports (SPEC.md III.5 rule 1), and here each would have to go after the other — and it is what makes at most one module able to define any given impl (SPEC.md IV.4). Move what the modules in the cycle share into a module they both import.
rule: R-1, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r1_import_cycle.iyi
# Breaks R-2: only what a module marks `pub` is reachable from outside it, and# `greeting` in `dep/greeter` carries no `pub`. Qualifying the call does not get# around that. It has to be genuinely unreachable rather than merely left out# of an import's names, because a `.iyimod` carries a module's exports and nothing# else, so if another module could call an unmarked name that artifact would# not be enough to compile against.module diagnostics/r2_not_exported
import dep/greeter
puts Dep::Greeter.greeting
Showing last frame. Use --error-trace for full trace.
In records/break/r2_not_exported.iyi:11:19
11 | puts Dep::Greeter.greeting
^-------Error: Dep::Greeter does not export 'greeting'. Only what a module marks `pub` is reachable from outside it — see SPEC.md R-2
rule: R-2, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r2_not_exported.iyi
# Breaks R-2: everything a module exports carries full parameter and return# types, because the signature is the whole of what a consumer reads. Drop the# return type and there is nothing to write into the artifact, so the omission# is refused where it is written rather than inferred from the body.module diagnostics/r2_missing_return_type
pubdeftotal(numbers : Array(Int32))
numbers.sum
end
puts total([1, 2, 3])
Showing last frame. Use --error-trace for full trace.
In records/break/r2_missing_return_type.iyi:7:9
7 | pub def total(numbers : Array(Int32))
^----Error: `total` is exported and does not say what it returns
R-2 asks an exported signature for full types. A consumer types a call to this from the return type alone, since the body stays in this module: without one it infers `Nil`, and the symbol it then asks the linker for is not the symbol this module emitted.
Annotate the return type, or leave the def unexported.
rule: R-2, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r2_missing_return_type.iyi
a body that does not honour its written return type
# Breaks R-2 the other way: the type is written, and the body does not honour# it. An exported signature is a promise to every consumer that reads the# artifact instead of the body, so the body is the thing that has to give.module diagnostics/r2_wrong_return_type
pubdeftotal(numbers : Array(Int32)) : Int32
numbers.join(",")
end
puts total([1, 2, 3])
Showing last frame. Use --error-trace for full trace.
In records/break/r2_wrong_return_type.iyi:6:41
6 | pub def total(numbers : Array(Int32)) : Int32
^----Error: method Diagnostics::R2WrongReturnType.total must return Int32 but it is returning String
String is not in the declared type
rule: R-2, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r2_wrong_return_type.iyi
# Breaks R-2b: a bare `import` brings a module into scope under its qualified# name and nothing more. Unqualified names arrive only through the list or the# glob on an import line, `::{...}` or `::*`, which the consumer writes, so# importing a module never quietly changes what a name in this file means.# `polite` is exported and still not in scope here.module diagnostics/r2b_name_not_imported
import dep/greeter
puts polite("iyi")
Showing last frame. Use --error-trace for full trace.
In records/break/r2b_name_not_imported.iyi:10:6
10 | puts polite("iyi")
^-----Error: undefined method 'polite' for Diagnostics::R2bNameNotImported:Module
`polite` is exported by `dep/greeter`, and this file has not brought it into scope. Import it by name, `import dep/greeter::{polite}`, or call it as `Dep::Greeter.polite` (SPEC.md R-2b)
rule: R-2b, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r2b_name_not_imported.iyi
a body typed at its definition, reaching outside its bound
# Breaks R-2c: a def whose parameters and return are all written is typed at# its definition, caller or no caller, and a trait-restricted parameter is# typed against exactly its bound. Two halves of the rule fail here at once.## Nothing in this file calls `loudest`, so under the lazy typing iyi inherited# from Crystal its body would never be visited and the build would report# itself clean. And `User`, the only implementer of `Greet` in the program, has# a `shout` of its own: duck-typed generics would have found it and compiled.# `Greet` declares `greet` and nothing else, so the body reaches outside the# bound it wrote down, and the message names the witness type the compiler# synthesized to type it (SPEC.md R-2c;# src/compiler/iyi/semantic/definition_typing.cr).module diagnostics/r2c_outside_bound
pubtraitGreetabstractdefgreet : StringendpubstructUser
getter name : Stringdefinitialize(@name : String)
end# Present on the concrete type, and irrelevant to the def below: the bound is# what that body is typed against, not whatever happens to implement it.defshout : String"HELLO"endendimplGreetforUserdefgreet : String"Hello, #{name}."endendpubdefloudest(g : Greet) : String g.shoutend
Showing last frame. Use --error-trace for full trace.
In records/break/r2c_outside_bound.iyi:39:5
39 | g.shout
^----Error: undefined method 'shout' for IyiDefTypeWitness_Diagnostics_R2cOutsideBound_Greet
rule: R-2c, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r2c_outside_bound.iyi
an impl in a module that owns neither the trait nor the type
records/break/r3_orphan_impl.iyi, exit 1
records/break/r3_orphan_impl.iyiline 11, column 6
# Breaks R-3: `impl Trait for Type` may live in the module that defines the# trait or the module that defines the type, and nowhere else. `Greet` belongs# to `dep/greeter` and `User` belongs to `dep/user`, so this module owns# neither and is the one place the impl cannot be written. That restriction is# what makes a type's method set knowable from its declarations alone.module diagnostics/r3_orphan_impl
import dep/greeter::*import dep/user::*implGreetforUserdefgreet : String"Hello, #{name}."endend
puts User.new("iyi").greet
Showing last frame. Use --error-trace for full trace.
In records/break/r3_orphan_impl.iyi:11:6
11 | impl Greet for User
^Error: can't implement Dep::Greeter::Greet for Dep::User::User in Diagnostics::R3OrphanImpl: an impl must live in the module that defines the trait (Dep::Greeter) or the module that defines the type (Dep::User). This is R-3, the orphan rule, and it is what lets coherence be checked without a global pass — see SPEC.md IV.4
rule: R-3, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r3_orphan_impl.iyi
# Breaks R-3 from the calling side: a type's method set is closed, so a bound# on a type parameter is a question with a definite answer at the call. `Int32`# has no `impl Greet` and cannot acquire one later, so the call is refused as a# missing trait rather than as a failed overload match.module diagnostics/r3_unmet_trait_bound
pubtraitGreetabstractdefgreet : StringendpubstructUser
getter name : Stringdefinitialize(@name : String)
endendimplGreetforUserdefgreet : String"Hello, #{name}."endendpubdefannounce(item : T) : String forall T : Greet"-> #{item.greet}"end
puts announce(User.new("iyi"))
puts announce(42)
Showing last frame. Use --error-trace for full trace.
In records/break/r3_unmet_trait_bound.iyi:29:6
29 | puts announce(42)
^-------Error: Int32 does not implement Diagnostics::R3UnmetTraitBound::Greet, required by `T` in `announce`
rule: R-3, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r3_unmet_trait_bound.iyi
the annotation R-4 names, asked of a compiler that has no such name
records/break/r4_dictionary.iyi, exit 1
records/break/r4_dictionary.iyiline 16, column 3
# Breaks R-4 by asking for the one clause of it that has nothing behind it.## R-4's third sentence says `@[Monomorphize]` forces specialisation across a# module boundary. SPEC.md III.9 records what that is worth today: nothing in# the compiler mentions the annotation, a dictionary or a GC shape. So this is# not a feature that misbehaves, it is a name the compiler has never heard,# and the rejection below is a compiler declining to accept a promise it# cannot keep rather than compiling the program as though the rule were built.## What runs instead of the dictionary is the rule's second sentence applied# everywhere: a generic's body travels in the artifact and every consumer# compiles it again for the types it uses. `bench/generic_boundary.py` is the# measurement of what that costs a consumer's front end.module diagnostics/r4_dictionary
@[Monomorphize]pubdeftwice(x : T) : Array(T) forall T
[x, x]
end
puts twice(1).size
Showing last frame. Use --error-trace for full trace.
In records/break/r4_dictionary.iyi:16:3
16 | @[Monomorphize]
^-----------Error: undefined constant Monomorphize
rule: R-4, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r4_dictionary.iyi
# A module the break program imports, exporting one derive macro. It compiles:# a macro body is expanded where the `derive` is written, not where the macro# is defined, so the refusal this module earns lands in its consumer's build.## `counted` asks a question about the whole program. `all_subclasses` answers# with every type compiled beside it, which is exactly what R-5 keeps a macro# away from: a derive that read it would generate different code depending on# what else happened to be in the build, and then a `.iyimod` could not carry# what it generated (SPEC.md R-5, II.4).module dep/global_derive
pubmacro counted(declaration)
defcounted : Int32{% count =0%}{%for field in declaration[:fields] %}{% count = field[:type].all_subclasses.size %}{%end%}{{ count }}
endend
Showing last frame. Use --error-trace for full trace.
In records/break/dep/global_derive.iyi:16:31
16 | {% count = field[:type].all_subclasses.size %}
^-------------Error: `all_subclasses` is not available to a derive: it answers with the whole program, and a derive may only read the declaration it is attached to and what that declaration's types implement — see SPEC.md II.4
rule: R-5, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/r5_global_question.iyi
# Breaks the error rule in SPEC.md III.1: an error is an ordinary member of a# union, so handling one needs no new machinery, only the totality check a# `case` already performs. `read` can return an `IOError` and this `case` has# no branch for it, which is the same mistake as ignoring a returned error and# is caught by the same check.module diagnostics/errors_non_exhaustive
pubstructIOError
getter path : Stringdefinitialize(@path : String)
endend# `Error` is the compiler's trait, which is what lets a union member be an# error member without any name in scope deciding it.implErrorforIOErrordefmessage : String"no such file: #{path}"endendpubdefread(path : String) : String|IOErrorreturnIOError.new(path) if path =="missing""12"endcase read("missing")inStringthen puts it
end
Showing last frame. Use --error-trace for full trace.
In records/break/errors_non_exhaustive.iyi:28:1
28 | case read("missing")
^Error: case is not exhaustive.
Missing types:
- Diagnostics::ErrorsNonExhaustive::IOError
rule: III.1, SPEC.md. IYI_PATH=<iyi>/src iyi build --no-codegen --no-color records/break/errors_non_exhaustive.iyi
The interface underneath
Everything the playground knows about compiling iyi arrives through one interface, PlaygroundEngine, resolved in one place. The engine's own account of itself is the only input to how that page is shaped, so an engine that could check source but not link would get a larger playground without a markup change, and one that could do neither would get a smaller one. To swap it, follow the three steps in src/playground/registry.ts: add an implementation, register it there, and report only what it actually does.