curayious Language Lab

Elixir — lab notes

The toolchain, third time lucky

No erl, no elixir, no mix. Not in MSYS2 either — pacman -Ss erlang returns nothing, so the route that rescued the OCaml track was unavailable.

winget failed differently this time, and the difference is worth recording:

$ winget search elixir
Failed in attempting to update the source: winget
Failed when searching source; results will not be included: winget
No package found matching input criteria.

Previously winget search worked and only install hung. Here the source index itself would not refresh. Two distinct winget failures across three tracks; it is not a reliable route from this context in any form.

The official answer is good and nearly worked. elixir-lang.org/install publishes an install.bat that needs no administrator rights:

curl.exe -fsSO https://elixir-lang.org/install.bat
.\install.bat elixir@1.20.4 otp@28.4

Reading it before running it — 6,483 bytes, and worth the minute — it does four things: reads the latest release via a HEAD redirect, curls two zips from github.com/erlang/otp and github.com/elixir-lang/elixir, extracts them with Expand-Archive, and runs the bundled vc_redist.exe only if vcruntime140.dll is missing from system32. No registry, no PATH mutation, no elevation except that last conditional step.

It then died partway through the 170 MB OTP archive:

downloading https://github.com/erlang/otp/releases/download/OTP-28.4/otp_win64_28.4.zip...
curl: (23) client returned ERROR on write of 13780 bytes

curl exit 23 is a write failure, not a network failure — it could not put the bytes where it wanted them. Rather than fight the script's choice of temp directory, tools/install-elixir.ps1 does the same four steps with Invoke-WebRequest, an explicit download directory, and the same destination layout so elixir-lang.org's own documentation still describes what is on disk:

%USERPROFILE%\.elixir-install\installs\otp\28.4
%USERPROFILE%\.elixir-install\installs\elixir\1.20.4-otp-28
Erlang/OTP 28 [erts-16.3] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns]
Elixir 1.20.4 (compiled with Erlang/OTP 28)

The script does not touch PATH. Two Erlang installations on one PATH is a worse problem than resolving the path at call time, so verify.py globs ~/.elixir-install/installs/{otp,elixir}/*/bin and prepends the newest of each for the suite's lifetime only.

Side effect worth banking: this installed Erlang/OTP 28 as well, so track 50 no longer has a toolchain problem.

Three things the code got right for non-obvious reasons

\A and \z, not ^ and $

defp all_digits?(s), do: String.match?(s, ~r/\A[0-9]+\z/)

Elixir's ~r is PCRE, where ^ and $ match at line boundaries by default. So ~r/^[0-9]+$/ matches "200\nrm -rf /" — the first line satisfies it and the rest is along for the ride. The anchors meaning start-and-end-of-string are \A and \z.

The fixture would not have caught this. No field in access.log contains a newline, so the wrong regex passes the golden diff and is still wrong. Same class as the C track's %-20.20s truncation: found by reading the spec, not by running the tests.

Python's re has the same ^/$ behaviour under re.MULTILINE only, and Go's regexp the same. Java's Matcher.matches() anchors implicitly, which is why the Java track's ^[0-9]{3}$ is belt-and-braces rather than load-bearing. Four tracks, four slightly different default anchoring rules.

IO.binwrite, not IO.write

IO.write runs the term through the Unicode encoder and hands it to the device with line handling applied. IO.binwrite writes bytes. On Windows that is the difference between \n and \r\n and therefore the whole diff — sixth track, sixth spelling of the same fact — and it is also faster, because the encoding step is skipped for data that is already a binary.

The flush before System.halt

IO.binwrite(:stdio, render(Path.basename(path), acc))
:ok = :io.put_chars(:standard_io, [])
System.halt(if acc.malformed == [], do: 0, else: 4)

System.halt stops the VM immediately. Standard output on the BEAM goes through an asynchronous port process, so bytes handed to IO.binwrite may still be in flight when the VM dies, and the report truncates at whatever had made it out. The empty :io.put_chars/2 is a synchronous round trip to that port — it returns only once the port has processed everything queued ahead of it, which is the flush.

Neither C's exit() nor Go's os.Exit has this hazard, because their stdout is a file descriptor rather than a process. This is the cost of the actor model reaching all the way down to I/O, and it is the one place in the track where "everything is a process" is a nuisance rather than a feature.

Two claims checked before repeating them

The reduction budget is 4000, not 2000

Every account of BEAM scheduling says a process gets 2000 reductions before preemption. The BEAM Book's scheduling chapter records that CONTEXT_REDS was 2000 before OTP 20 and has been 4000 since. The commonly repeated figure is nine years stale. Corrected in Q6.

The nine nines are Armstrong's own caveat

The AXD301 99.9999999% uptime figure appears in every Erlang pitch. It is in Armstrong's thesis — and so is his statement that evidence for the system's long-term stability "had also not been collected in any systematic way", the number coming from a customer's slide deck by unclear means.

Repeating the figure without the caveat misrepresents the source. Q1 quotes both. This is the kind of number the deepening runbook means when it says to replace a hedged number with a cited one or cut the claim — here the citation is what adds the hedge.

Things not done yet