playground

Programs run in your browser, under the reference interpreter.

Ctrl+Enter

The editor auto-indents and pairs brackets (interpolation braces pair inside strings). Ctrl+Alt+↑/↓ adds a cursor, Esc collapses, Alt+click adds one at the click. Tab/Shift+Tab indents, Ctrl+/ comments, Ctrl+D selects the next occurrence. On a Mac, Cmd where you expect it.

On some Windows laptops Ctrl+Alt+↑/↓ never reaches the browser: the graphics driver claims it first and rotates the screen instead — Intel's control panel is the classic one, and it can turn its hotkeys off. If that is your machine, Alt+click and Ctrl+D add cursors without going through the driver.

Output

What is running

lupin is the wolf reference interpreter. It is a separate implementation from the compiler, written against the specification, and the two share no code. The build in this page is that program, compiled to WebAssembly from the revision this site was built from. Nothing is uploaded: the source stays in the tab, and no request leaves the page after the module and the samples have loaded.

The revision of the specification it reads is the compiler's own, and at these pins it is not the compiler's newest. This interpreter, lupin 0.1.52, was built to 294d626, which is a release tag and not a revision of the compiler's trunk; the compiler has tagged release after release since while the interpreter kept that pin, so the text this tab reads is five releases behind the one install advertises, six hundred and eleven commits of the compiler's own history back. The gap is a step wider than at v0.2.28's pin, when this tab read the same tag four steps back: the interpreter released once more without re-pinning, and mirrored instead what the compiler had added — at v0.2.26's pin volatile access and the atomics on raw pointers, the kernel's papercuts, a child's descriptors and copy region (wolf-interp#185, #194, #200), at v0.2.28's the unsigned and wrapping integers, a repeated json name, env_unset and the calls that change a file, and now as much of the shell's signals as it can serve without unsafe — so the programs those releases brought run here, where a page has what they need, while the text the tab reads has fallen further behind. From v0.2.19's pin to v0.2.25's the gap was a single step at every pair this site served. The compiler's own second line names the interpreter in this tab: the release install advertises was tested against the interpreter version above, and this site serves it. At v0.2.15's pin it did not, and the reason was a fact about the two repositories rather than a judgement made here. The interpreter release of that day recorded, as the specification checkout it was built to, a compiler revision that the released compiler's history does not contain — a branch head that the rebase-merge landing it rewrote under other shas, preserved by a tag so it can still be fetched, but not on the line. A distance along one line is exactly what this paragraph states, so there was none to state, and the check that holds these sentences refused rather than printing a number with no referent. The interpreter re-pinned on the released line at v0.2.16's pin and has stayed on it since.

That is the open end of an ordinary cycle, and the page has now stood at every point of it more than once: an untagged commit short of a release, the release itself, a gap after the compiler tagged again, the mirror caught up, a gap again, the mirror caught up again, a gap the mirror could not take because the pin it offered was off the line, the mirror back on the line one step behind, the compiler tagging again and the mirror following it one step so the gap held its width at a later pair, the compiler tagging once more while the mirror released without re-pinning so the gap widened a step, the mirror re-pinning on the compiler's last tag but one so the gap was back to a single step, the compiler tagging four times more with the mirror following each time onto its last tag but one, so the gap held that single step across all four, the mirror releasing once more without re-pinning, two steps behind, at a pair this site never served, and then re-pinning two steps forward, so the gap was the single step again, the compiler tagging once more and the mirror releasing beside it on the same pin, so the gap was two steps at v0.2.26's pin, the first pair two steps apart this site had served since v0.2.18's, the compiler tagging twice more with the mirror releasing beside it each time, still on that pin, so the gap was four steps at v0.2.28's pin — and now the compiler tagged once more and the mirror released beside it on that pin again, so the gap this page shows is a step wider still, the widest the check that holds these sentences has counted. The two implementations tag on their own schedules and the interpreter follows the compiler, so a gap opens at every compiler release and closes when the mirror catches up. A program that answers differently here from a 0.2.29 you download today may be a disagreement about the text or may only be a difference of vintage, and the specification is what gets read when it is the first. Counting by ancestry is what makes this sayable at a pin that names no tag, which is a case this page has been in: the check that measures it asks the compiler's history which release the revision descends from and how far the tag is, so neither end has to be a tag, and it requires both pages to spell what it counted.

Where the vintage shows: in no refusal at all. Every phase: run program in the pinned corpus goes through the module this page serves, and at these pins none of the ones the compiler runs is refused here, in the tab or at a terminal. What remains is declines, each by name and the same at a terminal: the three programs that store a whole struct through a raw pointer, corpus/memory/raw_repr_c_layout.lu, raw_repr_packed_layout.lu and raw_repr_align_layout.lu, which the interpreter declines as the compiler's own checked machine does, and the two that need a link or a C ABI the interpreter does not model, corpus/membrane/extern_let_image.lu and extern_libc.lu. At v0.2.25's pin the list was longer. The atomics the release before it added, corpus/conc/atomic_fence.lu, atomic_orders.lu and atomic_widths.lu, answered that fence or Order.seq_cst does not resolve, corpus/memory/volatile_widths.lu that lupin 0.1.48 had no write_volatile, and corpus/memory/packed_field_raw_read.lu that a raw pointer's element, read as bytes, "u8 has no member limit" (wolf-interp#194, #185, #205). All five print here what the compiler prints now. corpus/conc/atomic_counter.lu is the one atomics program still declined, and for the tab's reason, not the interpreter's: it spawns tasks, which a page has no thread for, and at a terminal it prints 400000 100000. At v0.2.23's pin the tab refused five, and none was a decline: corpus/comptime/layout_query_repr_c.lu, corpus/memory/packed_fields_at_offset_of.lu, corpus/memory/raw_repr_packed_layout.lu and corpus/memory/raw_repr_align_layout.lu answered fail(E0817), because lupin 0.1.46's closed attribute set listed #[repr(c, packed)] and #[repr(c, align(N))] as not implemented (wolf-interp#188), and corpus/membrane/extern_let_image.lu answered fail(E0201), its extern "c" let a line that interpreter could not parse (wolf-interp#190). The first two print here what the compiler prints; the other three are among the declines above.

This tab reads cfg as an x86-64 Linux host. The interpreter evaluates a #[cfg(target = …)] against the target it was built for, and this module was built for WebAssembly, a target no gate in the corpus names. So the tab observes every program as x86_64-unknown-linux-gnu, the target the interpreter's own conformance bundle is observed as, and says so in the line above the editor. corpus/grammar/cfg_target_arch.lu, which defines one function twice, gated to two architectures, prints 64 here as it does on every tier of the compiler and under this interpreter at a terminal. Before this tab took that target it read cfg as WebAssembly, both definitions were dropped, and the program was declined, naming arch_bits as a function that does not resolve (wolf-web#55).

Nothing parts in its output. A verdict census cannot see a disagreement that exits 0 on both sides, so every running program in the corpus was walked a second time, through this module and through the one this page served before it, and no program prints differently under the two. One program from an earlier release parts from its header, and it is the tab's: corpus/os/chdir_relative.lu moves the working directory, a page has none to move, and this build's os_chdir answers the io row rather than declining, so the program exits 1 printing error: io, where at a terminal this interpreter prints its header's lines and exits 0 (wolf-interp#222). At v0.2.22's pin that walk is where corpus/rows/unit_discard_if_value.lu showed: it exited 0 here as there and printed () none where the compiler prints () (), because lupin 0.1.45 bound the row's tag where an else-less if is () whatever its branch does — a disagreement inside the text both read ([type.unit.context], wolf-interp#179). This interpreter prints () (). At that pin the tab also refused five programs: two that gate code by #[cfg(target = …)], which lupin 0.1.45 did not read, and three with a *T in a module-private signature or across the C membrane, which it refused in every signature, fail(E1302) (wolf-interp#181). Three of them run here now and print what the compiler prints, membrane/extern_libc.lu is declined by name for its bodyless C declaration, and the fifth is the program above. At v0.2.18's pin the second walk is where corpus/memory/ctl_store_order_nested_index.lu showed, whose index calls lupin 0.1.41 ran twice (wolf-interp#145). At v0.2.21's pin the list held one program, corpus/rows/eu_bind_empty_row_handled.lu, which lupin 0.1.44 refused fail(E0801) where every tier of the compiler prints 43 42 42, reading a binding's inferred empty row as open (wolf-interp#176). This interpreter reads the row as the specification makes it and prints what the compiler prints.

The compiler's newest program asks for a process. Besides the programs above, the release install advertises brought one program into the corpus's running set, corpus/os/signal_shell_rows.lu, which walks the shapes of the shell's signal calls, and its first call is a poll of a signal set, which this build declines with the process tier. The release's new calls answer the same way when a program makes one alone: os_pid, os_ppid, os_signal_send, os_signal_number and os_signal_ignored each report unsupported here, naming the call. At a terminal this interpreter prints the program's header: it serves the two ids and the number, answers every shape row, and refuses the send and the ignored set by name, because it admits no unsafe. lupin 0.1.51, the interpreter this page served until now, declined the program at the same poll and answered that each of the new names does not resolve. No program the corpus already carried answers differently from the last pin; corpus/os/job_rows.lu, the one the release edited, was declined at its job spawn before and is now. The measurement is four walks: both corpora through both modules, and no program both corpora carry changed its answer in any of them. At v0.2.28's pin the additions were unsigned and wrapping integers that print and divide as the values their types hold, and the faults that trap where those values leave their type; repeated json names that answer last-wins; a variable removed with env_unset; octal and binary literals; and calls that keep or send a string they are handed and stay legal. Under lupin 0.1.49, the interpreter this page served before that pin, the json programs answered first-wins, corpus/os/env_unset.lu was declined, and corpus/typecheck/int_truth_wrap_mul_wide.lu trapped overflow where the compiler prints the wrapped product; the rest of those additions were declined for the tab's reasons, and still are — the calls that change a file with the filesystem, os_exec, a job and the umask with the process tier, a net read with the net tier, a task-spawning channel program with the task tier, and corpus/link/module_c.lu, which names a sibling module. At v0.2.26's pin the additions were a field stored through a raw pointer to a struct, ! on an integer as its complement, functions declared -> never, blocks that keep their value with copy region and a byte scan over a List[byte]; under lupin 0.1.48 most of them were declined or refused, and three of the copy region programs trapped region-fault, reading the block as a plain region. At v0.2.25's pin the additions were a module var and a raw pointer's memory written by a callee and read back after the call, branches on the top half of an unsigned shift, module strings that hold their value, and raw accesses at their pointee's alignment, which lupin 0.1.46 printed the same. At v0.2.23's pin the additions were integers and pointers converted both ways, prefix *p over signed and unsigned pointees, module const, let and var, and else-less ifs at a fallible function's tail whose raise is discarded; under lupin 0.1.45 two of the conversions were declined, a narrowing cast printed 0 0 0 and the trapping one exited 0 (wolf-interp#184), and four of the discards handed the raise to the caller (wolf-interp#179). At v0.2.22's pin the additions were integer casts that keep their value or trap, an export called from wolf, a for piece used inside its region, the first-diagnostic rule's running witness and the discards of a row nobody consumes, and a unit main whose tail raises exited 1 in lupin 0.1.44; at v0.2.21's pin they were a match over a fallible value, an errdefer run by its block's own error value, an else that handles only its scrutinee's row and two-phase arguments, which trapped exclusivity in lupin 0.1.42; at v0.2.19's pin they were element claims, and three of them trapped in lupin 0.1.41; at v0.2.18's pin memory/list_session_struct.lu, which ran here at v0.2.17's pin, read an element it had moved, compiled only through the hole that release closed, and trapped under lupin 0.1.41 until the corpus copied the element instead.

The one program named here at v0.2.16's pin is a decline, not a parting. corpus/conc/proc_join_param.lu wrote Proc as the type of a parameter before the interpreter knew the name. The interpreter has known it since lupin 0.1.39, so the program gets as far as spawning its proc, and the proc tier is one this build declines (below): it answers unsupported with a reason naming the thread it cannot spawn. The interpreter's own release notes record the program as a match at a terminal. At v0.2.17's pin two of that release's programs, memory/index_store_copies_list.lu and index_store_copies_map.lu, would have parted with the interpreter this site had served until then — it trapped a use after move where the ruling copies — and lupin 0.1.40 was the first to answer them as ruled.

The list this page carried at v0.2.15's pin is closed, all of it. Twenty-two programs stood on it — the list literal, a nullary variant as a pattern, the range types, the error alias for a row, and the method surface with its combinators — and every one of them came from a release the interpreter had been built before. The interpreter re-pinned on the compiler's released line and has read every one of them since. The module's move closed all twenty-two at once and woke three programs that had been declining for want of a builtin.

Six of those twenty-two did not start running; they changed which word they are refused with, and the difference matters. The method-surface programs — methods/comb_map_filter_fold.lu, comb_sort_enumerate_zip.lu, comb_sorted_ord.lu, method_is_free_call.lu, wordcount_serial.lu and wordcount_par.lu — were fail(E0201) here with the dot unparsed. This interpreter parses the dot. What it then has to do is reach the receiver's home module, because List.map is no builtin: [type.method.resolve] sends it to std.list, and a browser tab has no standard library on disk to send it to. So they answer unsupported now, with a reason that names --std-root, and they are a decline of this build rather than a refusal of this interpreter — run them under lupin at a terminal with a std root and they answer. The last two are the book's word counter, serial and parallel, which is where par reaches this census. Nothing on this site would have told the two cases apart from the verdict alone, which is why the sentence is here.

And the one program that was worse than a refusal is fixed. At v0.2.15's pin corpus/grammar/range_header_inclusive_max.lu did not answer the module at all: it reached the module's own unreachable and took the instance with it, so the tab stopped rather than printing a verdict and the page had to be reloaded. The cause was a range materialized instead of walked — one allocation large enough that a machine with memory to spare ran out of it, and a sandbox has none to spare. The interpreter walks the range now. Fed to this module explicitly, the same file exits 0 and prints its two lines, and so does grammar/range_value_wide_iter.lu beside it. It is still not on the menu and the census is still the only thing that feeds it here, which is the reason it was fed on purpose rather than waited for.

What emptied an earlier list was the range arm arriving on this side. At that pin match had gained range arms in the compiler and this interpreter had been built before that text existed, so it stopped at the .. inside a pattern and said E0201 — the refusal working as designed, rather than running the program and answering it wrongly. The interpreter has the pattern form now, and all four of the witnesses that ruling brought in agree with the compiler: the two that run (grammar/match_range.lu and grammar/match_range_char.lu) exit 0 here, an empty range is E0815 here as there, and an open end is still the slice spelling and still E0201. That closes wolf-interp#83.

The two partings this page named at an earlier pin are closed. They were corpus negatives, refused on both sides and parting in which refusal, which is why no table could hold them: rows/negative/row_operand_compare.lu puts a bare row beside a comparison and answered E0401 here against the compiler's E0409, and typecheck/str_slice_assign.lu writes through a slice of a str, which was a tier refusal here against the compiler's E0416. This interpreter answers E0409 and E0416. That closed wolf-interp#85, and it was the reason the pin moved at that bump.

The parting that pointed the other way is closed too, inside a single release. At an earlier pin this page said something it had never had to say: the interpreter took a one-line if — if c then a else b — that the compiler refused, because the spelling landed on the compiler's trunk after its release was tagged. The compiler this page now advertises has it. All nine of the witnesses that ruling brought in run here and exit 0, and the three that are refusals are refused on both sides. A parting recorded while it was open, and closed by the side that was behind, is the whole argument for writing them down.

It is a gate now and not only a paragraph. The counted distance above compares the texts the two machines were built to read, which is why it was blind to that case: at the pin where it happened the distance was zero, and the tab still ran something the compiler refused, because the interpreter had mirrored a clause early. A distance of zero says the two were built to the same text; it does not say neither ran ahead of it. So the build takes that program, and every other spelling the interpreter may mirror early, through the module it is about to publish, and refuses to ship if one of them runs here against a clause the advertised release does not declare.

And the parting that pointed the usual way is closed, at the interpreter's next release exactly as this page said it would be. v0.2.11 ruled how a Map answers and how an operator reaches a trait, and the interpreter of that day had been built before either text existed, so five programs parted. Every one of them runs here now and exits 0: corpus/memory/map_count.lu — counting words with a Map and nothing else, which is exactly what chapter 5 of the book teaches — and corpus/memory/map_int_keys.lu, and corpus/traits/op_money.lu and corpus/traits/op_ord_struct.lu, where + and < now reach their impl, and corpus/traits/op_total_num.lu, which was refused outright at E0201 and is the book's chapter-5 total under a bound. All five of the new Map witnesses run, where three did before it, and the census behind this page has counted none of them answering differently from the compiler since: the mirror that was partial is whole.

Those five were worth naming while they were open and are worth keeping named now they are closed, because of what they were not. They reported unsupported, the same word the tab gives net_listen, and they were never the same kind of thing. A tier refusal is permanent and by design: a page has no sockets, and no release of anything will change it. Theirs said `+` is not defined on Money and Money — an interpreter honestly behind a clause it had not mirrored yet, which is a thing that ends. It ended here. The distinction is the reason a reader can tell, from the same word, which refusals are worth waiting on. Among the programs you can run, the tab has none of the second kind at these pins: the declines named above are the whole-struct stores, which the interpreter declines as the compiler's checked machine does, and the link symbol and the C call, which an interpreter has no link or C ABI to serve, and neither is waiting on a release. The atomics, the volatile program and the struct field that waited at v0.2.25's pin had their mirrors released, and run.

And the gap that opened after that cost nothing while it was open, and is closed now. The compiler tagged once more while the interpreter still read the release before it, and what that distance bought you was nothing: the release brought six programs into the corpus's running set — a lint about arithmetic beside an else, a unit-typed tail that is discarded rather than returned, the float remainder, a Map written through a generic, a struct literal in parentheses promoted from a static witness to a running one, and a trait reached through an imported module — and all but one of them ran in this tab and exited 0 before the interpreter had been rebuilt to the text, because it already answered every call they make. This interpreter is built to that text, and they still do. The one that does not, corpus/traits/op_eq_imported/main.lu, does not — and the reason is this page's rather than the interpreter's vintage: it is a module spread across sibling files, and the editor here is one buffer. No release of anything will change that one.

One of that release's new negatives parted in which refusal rather than in whether, which is the shape no table can hold, and that parting is closed too: corpus/typecheck/interp_spec_on_union.lu formats an error union with a width, the compiler refuses it at E0413, and the interpreter of the day reported unsupported because its format-spec grammar spoke about strings. This interpreter answers E0413. It is a resolve-phase witness, which puts it outside the running census above, and it stays written down because a parting recorded only when it is convenient is not a record — and one closed by deleting the sentence leaves no record it was ever there.

What the distance does not explain is what this page declines. net_writev, net_nodelay and fs_fstat are all in this interpreter — the filesystem tier is the newest tier in it — and all three report unsupported here whatever the pins say: a tab has no sockets and no files to open. Run the same programs under lupin at a terminal and all three answer. Those refusals are the tier and nothing else, and the gap closing has not moved them.

The interpreter is not the compiler, and it reaches a different set of programs. It runs the memory model as a dynamic machine, so every ownership rule is a check at run time and every fault cites the clause it came from. It has no type checker and no borrow checker, because those are the compiler's half of the split, so a program that the compiler would reject at typecheck can still run here. When a program is outside what this implementation covers, the verdict is unsupported and the reason says which tier declined.

What this build declines

Eight tiers report unsupported here that work when you run lupin at a terminal. Tasks and procs each need an operating system thread, and a WebAssembly module in a page has none to spawn. The time builtins need a clock the module cannot read, the process trio needs processes the page cannot spawn — and the interpreter builds the rest of what it knows about processes only where it has that tier, so this process's id and its parent's, a signal's number, a send to another process and the set of meanings a program ignores are declined with it — the net tier needs sockets the page cannot open, os_random needs an entropy source the module has no way to ask for, os_signal_listen needs signals nothing in a tab can send, and the filesystem tier needs files a module in a page has no way to open. That last one is the newest of them: through lupin 0.1.35 the interpreter declined the filesystem in every build, by design, and the terminal build serves it now — real files, at the paths the program names — so only this build is left declining it, for the tab's reason and not the interpreter's. A program whose module graph names sibling files is likewise unsupported: the editor is one buffer, and there is no directory beside it.

os_cpus() is the near miss, and it is worth a sentence because the census caught it. This build answers the call now rather than declining it — but what it answers is the io row, because a wasm module has no way to ask how many cores a process may be scheduled on, and [os.cpus] says a host that cannot answer takes that row instead of quietly saying one. So corpus/os/cpus.lu stopped reporting unsupported when the interpreter grew the call, and exits 0 with answered false on the line at every pin since, which is the program working as written. It is not on the menu, because its own header says answered true and a sample shown beside a header it contradicts looks broken. corpus/os/chdir_relative.lu is the second near miss, and the one that shows: os_chdir answers the same io row here, and the program, which stops on it, exits 1 (above).

Eighteen of the corpus's net programs land in that count, and the newest of them is v0.2.14's: writev_head_gather.lu, the witness for a response head leaving as a str beside the gathered body, [os.net.writev.head]. The three before it are v0.2.8's — syscall_first.lu, writev_gather.lu and nodelay.lu, the witnesses for a read that no longer takes a round trip through the reactor, for a head and a body leaving as one write, and for Nagle being off on every stream the runtime hands out. A tab declines all four for one reason only, which is the socket: the three older calls have been in this interpreter since 0.1.28 and the newest since 0.1.37, and while the three were not in it, a sentence here said the refusal had two causes. That count was wrong until ww19, incidentally. The page said twelve from ww16, when thirteen was already the number; nothing gates a count written by hand, and re-measuring the census is what caught it.

Every sample on the menu runs

All thirty-six of them, and none carries a note. The newest is match on a range — corpus/grammar/match_range.lu, which was kept off the menu at exactly one pin and for exactly one reason: it answered E0201 in this tab, and an entry that answers fail looks broken whatever the page says beside it. The reason expired when the interpreter gained the pattern form, so the program a reader once could not run here is the one the menu now opens with. Press Run and the tab agrees with the compiler byte for byte. Beside it is match as a switch — corpus/grammar/match_switch.lu, which was on the menu through that whole pin, because its guarded arm is a lowering the compiler gained and not a pattern form, so this interpreter had always run it. The two sat one ruling apart and on opposite sides of the menu, and the distance between them was the whole of what the vintage cost. The one before them was text to a number, and the row when it is not — corpus/strings/to_int.lu, the call the book's chapter 1 makes in its cold open. It reached the menu when the compiler gained the call rather than when the interpreter did, and the reason runs against the usual direction: the interpreter has answered str.to_int all along, so the program the tab could always have run is the one that only just started compiling. That was not true at the lupin 0.1.24 pin: the menu's byte, and its cast ladder — corpus/typecheck/byte_casts.lu — answered fail(E0301) here and carried a note on this page saying so, because byte was the compiler's headline at v0.2.4 and the interpreter beside it had been built against the release before. lupin 0.1.25 closed that and it has stayed closed. Press Run on it and it prints the line its own header claims:

widen 200 200 | trunc 0 255 0 255 44 | arith 201 400 -1 -200 | order true true true | eq true true

which is the whole ruling in one line: 200 as byte widens back to 200 and never -56, 256 truncates to 0 and -1 to 255 and 300 to 44 without a trap or a warning, arithmetic widens to int before it computes so 200 + 200 is 400, and the comparisons are octet order.

Nothing on this page decided that by hand. A gate in this site's CI feeds every program in the menu to the module the build publishes and requires each one to answer in the class the page claims for it — so a sample that stops running goes red, and so does a note that outlives the refusal it explains. That is how the note above came off: the gate went red at this pin, naming the sample, and the sentence had to go.

The verdict vocabulary

verdictwhat happened
exit(N)The program ran to completion and returned N.
trap(kind)A checked fault stopped it: overflow, a bad index, a use after move. Each kind is a closed vocabulary entry and cites a clause.
fail(CODE)The program was rejected before it ran, with that diagnostic code.
ub(anchor)The provenance oracle found undefined behavior, and names the row of the memory model that licenses it.
unsupportedOutside this implementation's coverage. The reason names the tier.

Mahler kept revising the Fifth's orchestration between performances, and the parts he left behind disagree with each other.