curayious Language Lab

OCaml — lab notes

Three walls, in order

1. winget again

The official Windows route is winget install Git.Git OCaml.opam then opam init. winget install blocks indefinitely with no output under automation — the same failure as the Go track, for the same reason: it wants a session it can raise an elevation prompt from.

Worth recording that winget search works fine and fast:

$ winget search --id OCaml.opam
Name Id         Version Source
opam OCaml.opam 2.5.2   winget

So winget is not broken here; install specifically is unusable from this context. That is a more precise statement than the Go notes made, and it corrects them.

The way around it was not a workaround at all: MSYS2 already has OCaml, in the same UCRT64 repo the C track's compiler comes from.

pacman -S --needed mingw-w64-ucrt-x86_64-ocaml       # 28 MB down, 174 MB installed
pacman -S --needed mingw-w64-ucrt-x86_64-flexdll     # needed to link, see wall 3

No elevation, two minutes, OCaml 5.4.1. What it does not give you is opam, and therefore any library at all. Fine for a stdlib-only program; a dead end the moment the track needs one.

2. Error: Unbound module Stdlib

First compile, on a two-line hello world:

File "command line", line 1:
Error: Unbound module Stdlib

The compiler cannot find its own standard library. ocamlopt -config says why:

standard_library_default: /ucrt64/lib/ocaml
standard_library:         /ucrt64/lib/ocaml
os_type:                  Win32

/ucrt64/lib/ocaml is an MSYS path. It resolves inside an MSYS shell and nowhere else — not from PowerShell, not from subprocess.run, not from git-bash calling the binary directly. The override is OCAMLLIB:

export OCAMLLIB='C:\msys64\ucrt64\lib\ocaml'

This is the same bug, in the same MSYS2 installation, as the COBOL lab's COB_CONFIG_DIR trap — a native Windows binary with a compiled-in Unix-style path. Second occurrence, so it is a pattern rather than an incident: any compiler installed from MSYS2 that shells out to its own data directory will need its path variable overridden with a real Windows path. Worth checking first, not fifth, next time a UCRT64 toolchain fails to find itself.

3. Device Guard blocks the linker

With OCAMLLIB set, compilation succeeded and linking did not:

'C:\msys64\ucrt64\bin\flexlink.exe' was blocked by your organization's Device Guard policy.
File "caml_startup", line 1:
Error: Error during linking (exit code 4551)

flexlink is the linker wrapper ocamlopt shells out to for native Windows executables. ocamlc.exe and ocamlc.byte.exe are blocked by the same policy; ocamlopt.exe and ocamlrun.exe are not. The allowlist is not obviously principled from the outside, and it is not mine to change.

This is a machine policy, not a puzzle. No attempt was made to get around it. What the track does instead is take the route the policy already permits:

ocamlrun ocamlc.byte -o build/logagg.byte logagg.ml    # compile to bytecode
ocamlrun build/logagg.byte access.log                  # run it

ocamlc.byte is itself an OCaml bytecode image, so it is driven by ocamlrun rather than executed. ocamlrun is a permitted interpreter reading a data file — structurally the same as java -cp build LogAgg in the Java track, python logagg.py in the Python one, and cobcrun in the COBOL lab, which made the same accommodation for the same policy.

What it costs: bytecode is roughly an order of magnitude slower than native for compute-bound work. Irrelevant for a 200-line fixture. It is the reason --shard N is still outstanding — benchmarking a parallel implementation on an interpreter would measure the interpreter.

  ocamlopt ──► .cmx ──► .o ──► flexlink ╳ Device Guard    (native, blocked)
  ocamlc   ╳ Device Guard                                  (blocked directly)
  ocamlrun ocamlc.byte ──► .byte ──► ocamlrun ✓            (what this track does)

The compile error that actually cost time

Not a type error. A paren:

| _ -> assert false
(* ...comment... *));

*)); is *) then one ). Two were open — the (if … else …) and the | None -> ( — so the file ran off the end looking for the second, and the error surfaced 17 lines later:

File "logagg.ml", line 197, characters 6-8:
197 |       in
            ^^
Error: Syntax error: ) expected
File "logagg.ml", line 177, characters 12-13:
  This ( might be unmatched

The second half of that message — pointing at the probable opening paren — is better than most compilers manage, and it was still 17 lines from the real fix. Rewriting the block with begin … end instead of parentheses fixed it and is more readable besides. OCaml's begin and end are exactly ( and ); using them for multi-line blocks is the convention for this reason.

Everything after that compiled clean under -w +a-4-9-40-41-42-44-45-70 and matched the golden file on the first run. The whole-file experience: one syntax error, zero type errors, zero logic errors. That is the inference doing its job — the places a type error would have appeared are the places the other tracks put runtime bugs.

Verified: the tie-break holds under randomised hashing

Same experiment as the Go track's, and the result differs in an instructive way.

OCaml's Hashtbl can randomise its seed — Hashtbl.create ~random:true per table, or R in OCAMLRUNPARAM for every table in the process. Unlike Go, randomisation is off by default, so the Go-style "fails immediately on your laptop" property is opt-in here.

for i in 1 2 3; do OCAMLRUNPARAM=R ocamlrun build/logagg.byte access.log > r$i.out; done

All three byte-identical to bench/expected/logagg.txt. SPEC 3's mandatory tie-break is what makes that true; without it the bucket order would leak into the report.

So: Go found the justification for that clause by breaking without it, and OCaml confirms it by not breaking with randomisation forced on. Two tracks, two directions, same conclusion — which is worth more than either on its own.

One flake, recorded

A verify.py run immediately after mv-ing the track directory died with:

OSError: [WinError 4551] An Application Control policy has blocked this file

Re-running it, unchanged, gave 5 passed, 0 failed, 0 skipped. The most likely explanation is a race between a freshly written build artifact and the Application Control scan of it — verify.py rebuilds every track on every run, so there is always a new unsigned binary a few milliseconds old. Not reproduced since. Logged rather than explained, because an intermittent policy block on a machine that also blocks flexlink is worth being able to look up later.

Things not done yet