# Evidence ledger

Each row is something that was run in this checkout. Confidence is about the claim, not about the general future of Mojo.

| id | hypothesis | command | observed | interpretation | confidence | source |
| --- | --- | --- | --- | --- | --- | --- |
| E1 | The pinned BEAM is OTP 28 / erts 16.2 and Elixir 1.20.2. | `scripts/doctor.sh` | `elixir 1.20.2`, `erlang 28 erts-16.2`, `mojo Mojo 1.1.0 (8189361e)`, `Darwin arm64 26.5` | Toolchain matches the pin. | high | doctor output |
| E2 | Mojo 1.1.0 can emit a shared library. | `mojo build experiments/abi/01_export_add/add.mojo --emit shared-lib` | `nm -gU` shows `T _mojoler_add`. `otool -L` lists the dylib, `@rpath/libKGENCompilerRTShared.dylib`, and `libSystem.B.dylib`. | A C ABI export works. The Mojo compiler runtime is a real dynamic dependency. | high | `experiments/abi/OBSERVED.txt` |
| E3 | The export lowers to a C integer function. | `mojo build ... --emit llvm` | `define dso_local i64 @mojoler_add(i64 noundef %0, i64 noundef %1)` then `add` and `ret`. Target `arm64-apple-darwin25.5.0`, CPU `apple-m1`. | Arguments and the return use the integer registers. This is an add, not a NIF. | high | `experiments/abi/01_export_add/add.ll` |
| E4 | `fn` is gone in this compiler. | `mojo build` of a file that used `fn` | `error: 'fn' has been removed; use 'def' instead` | Tutorials that still show `fn` do not match 1.1.0. | high | compiler diagnostic |
| E5 | OTP 28.3.1 NIF version is 2.17 and `ERL_NIF_TERM` is 8 bytes. | `clang` of `experiments/abi/diagnostic/layout.c`, then run it | `ERL_NIF_MAJOR=2 ERL_NIF_MINOR=17`, `long=8`, `ERL_NIF_TERM size=8`, `ErlNifFunc sizeof=32`, `ErlNifEntry sizeof=96`, `ErlNifResourceTypeInit sizeof=40` | Field offsets in [05-otp-abi-map.md](05-otp-abi-map.md) come from this run, not from a guess. | high | diagnostic program |
| E6 | `enif_get_int64` is not a symbol on this OTP. | `nm -gU` of `beam.smp`; read `erl_nif_api_funcs.h` | `_enif_get_long`, `_enif_make_long`, `_enif_make_badarg` are defined. `SIZEOF_LONG` is 8, and the header macros `enif_get_int64` to `enif_get_long`. | A NIF must call `enif_get_long` / `enif_make_long`. | high | header and `beam.smp` |
| E7 | Mojo struct layout matches those offsets. | `mojo experiments/abi/03_layout/layout.mojo` | `NifFunc sizeof 32`, fields at 0, 8, 16, 24. `NifEntry sizeof 96`, fields at 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88. `optional_none_or 0`. | `RegisterPassable` plus `std.ffi` integer aliases reproduced the C layout. None is a NULL pointer. | high | OBSERVED.txt |
| E8 | A thin function is an 8-byte code pointer. | `mojo experiments/abi/02_pointers/callback.mojo` | `sizeof 8`, `result 42` | Safe to store in `ErlNifFunc.fptr`. `Pointer(to=cb)` was rejected as a technique after LLVM showed it takes the address of a stack slot. | high | IR of an earlier probe; this program's printout |
| E9 | There is no module-level mutable global. | compile a file-scope `var` | `error: global variables are not supported` | `nif_init` uses `calloc` for the entry and the function table. | high | compiler diagnostic |
| E10 | Literal C strings survive `calloc`. | `mojo experiments/abi/04_lifetime/calloc_entry.mojo` | printed `add` and `arity 2` | `ImmStaticOrigin` pointers can be stored in the descriptor. | high | OBSERVED.txt |
| E11 | The built NIF exports only `nif_init` and imports the long-sized enif symbols. | `scripts/inspect-abi.sh` | `T _nif_init`. Undefined: `_abort`, `_calloc`, `_enif_get_long`, `_enif_make_badarg`, `_enif_make_long`, `dyld_stub_binder`. `otool -L` shows KGEN and libSystem. rpath is the venv `modular/lib`. | Link line `-Xlinker -undefined -Xlinker dynamic_lookup` left `enif_*` for dyld to bind inside `beam.smp`. | high | inspect-abi output |
| E12 | LLVM for the NIF calls those symbols directly. | `mojo build native/poc.mojo --emit llvm` | `call i32 @enif_get_long`, `call i64 @enif_make_long`, `call i64 @enif_make_badarg`, `declare` lines for those plus `calloc` and `abort`. Also compiler-inserted `write`, `dup`, `fdopen`, `fflush`, `fclose`, `KGEN_CompilerRT_AlignedFree`. | The NIF body is a direct call, not a C wrapper. The compiler also emits libc calls of its own. | high | `/tmp/poc.ll` from the recorded build |
| E13 | The BEAM loads the library and `add(20, 22)` is 42. | `mix test` | `11 passed` | Includes negatives, overflow, non-integers, arity, 200 repeats, 40 concurrent tasks, a fresh OS process, and the `nm` assertions. | high | ExUnit |
| E14 | A second load is refused by OTP, not by Mojo. | `Mojoler.POC.reload()` inside ExUnit | `{:error, {:reload, 'NIF library already loaded (reload disallowed since OTP 20).'}}` | `load`, `reload`, and `upgrade` are NULL. OTP refuses the second `load_nif` before those callbacks would matter. | high | `test/poc_test.exs` |
| E15 | `initialize_runtime()` can run inside `nif_init`. | build `experiments/abi/05_runtime/rtnif.mojo` and `load_nif` it | `runtime ping 7` after two calls to `initialize_runtime()` | Idempotent enough to call twice during init in this process. The add NIF does not need it. `parallelize` was not available without the `max` package. | medium | elixir one-shot |
| E16 | Small-int Elixir addition is cheaper than this NIF. | `mix run --no-start -e` with warmup 10_000, 5 trials of 100_000 | NIF microseconds `[38188, 37250, 36213, 34686, 36222]` (about 347–382 ns/call including the Elixir loop). Elixir `+` `[24655, 24961, 23204, 23876, 23806]` (about 232–250 ns/call). | The loop is inside both measurements. This is not a pure boundary cost, and it is not a reason to prefer the NIF. | medium | one process, no isolation between trials |
| E17 | A one-element GPU add can return 42 through a NIF on this Mac. | `.venv/bin/mojo experiments/gpu/add_host.mojo`, then `mojo build experiments/gpu/gpu_nif.mojo --emit shared-lib` and `elixir -e` loading `experiments/gpu/libgpu` | Host program printed `gpu_add 42.0`. Fresh BEAM printed `gpu_nif 42` and exited 0. `nm -gU` shows only `_nif_init`. `otool -L` lists `libKGENCompilerRTShared.dylib`, `libAsyncRTMojoBindings.dylib`, and `libSystem.B.dylib`. | Mojo 1.1.0 plus MAX 26.6.0. `std.gpu` is not in this stdlib; the kernel is `max.gpu`. `vector_addition` adds one `float32`, `grid_dim=1`, `block_dim=1`. `DeviceContext` copies the float back and `Int(result)` becomes `enif_make_long`. The callback calls `initialize_runtime()` first. The entry name is `Elixir.Mojoler.GPUProbe`, function `sum/0`, library `experiments/gpu/libgpu.so` (OTP appends `.so`). `has_accelerator()` did not take the "no GPU" branch. One call, one process, Apple GPU. Not NVIDIA, not concurrent, not a long kernel, and not part of `Mojoler.POC.add/2`. | high | this machine, Apple GPU |
| E18 | The NIF struct layout is the same on Linux x86-64. | [Actions run 39451722783](https://github.com/thanos/mojoler/actions/runs/39451722783), job `nif`, `scripts/doctor.sh` then `scripts/run-abi-experiments.sh` on `ubuntu-24.04` | `doctor: ready`. `Linux x86_64`, Elixir 1.20.2, Erlang 28 erts-16.2, Ubuntu clang 18.1.3, Mojo 1.1.0 (`8189361e`). `T mojoler_add` with no leading underscore. Mojo layout: `NifFunc` 32 at 0, 8, 16, 24; `NifEntry` 96 at 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88; `optional_none_or 0`. Clang diagnostic: `long=8`, `ERL_NIF_TERM size=8`, the same offsets, `ErlNifResourceTypeInit sizeof=40`. `runtime ping 7`. | LP64 ELF matches the Darwin measurement. ELF exports have no underscore. `ldd` of the add-only library printed `statically linked`. Crashpad's "Failed to initialize Crashpad" is the Mojo driver; the script still exited 0. | high | [job 118727989464](https://github.com/thanos/mojoler/actions/runs/39451722783/job/118727989464), commit `e6728ea` |
| E19 | `Mojoler.POC.add(20, 22) == 42` on that Linux runner, and the KGEN runtime is not a dynamic dependency of `libpoc.so`. | same run, `scripts/test-isolated.sh`; the `ldd` text is from the preceding red run [39428236198](https://github.com/thanos/mojoler/actions/runs/39428236198), whose only failure was the assertion that expected `libKGENCompilerRTShared` | Green run: `Result: 11 passed` in 0.6 seconds. Red run's `ldd` of `priv/native/libpoc.so`: `linux-vdso.so.1`, `libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6`, `/lib64/ld-linux-x86-64.so.2`. No `libKGENCompilerRTShared`. Doctor on the green run still finds that `.so` inside the venv. Link line that produced the library: `-Xlinker --unresolved-symbols=ignore-all` and `-Xlinker -z -Xlinker undefs`. The earlier flag `--unresolved-symbols=ignore-in-shared-libs` failed in [39365275751](https://github.com/thanos/mojoler/actions/runs/39365275751) with `undefined reference to enif_make_long`. | The CPU NIF loads under ld.so on Ubuntu 24.04 and the ExUnit suite passes, including the fresh OS process. Undefined `enif_*` symbols bind when `beam.smp` loads the library. On Linux this `add` image does not `DT_NEEDED` the KGEN shared library, unlike the Darwin `otool -L` result in E11. The workflow has no GPU. | high | green log plus the `ldd` printed by run 39428236198 |

| E20 | The generated ABI matches this Mac's headers, and a misspelled import is rejected. | `scripts/check-abi.sh` | Exit 0. `abi mojo: matches`, `layout ok`, `unexpected undefined symbol: enif_make_lonq`, `misspelled import rejected`, `imports ok` for `libpoc`, `libtwice`, `libterm`, `libown`, `libsched`, `libdsl_arith`, `libdsl_bytes`, `libdsl_box`, `provenance ok`, `dirty flags ok`. | One LP64 Mojo file matches Apple clang's layout of the allowlisted structs. Linux has not been re-run for this generator. | high | this Mac, Darwin arm64 |
| E21 | The ExUnit suite for stages 1–7 passes in one VM. | `mix test` | `Result: 43 passed` in 4.8 seconds. | Includes `add/2`, `double/1`, term edges, ownership, the heartbeat script, DSL rejection, and the abort probe. | high | this Mac |
| E22 | A dirty-CPU burn lets a one-scheduler VM heartbeat; the same burn on the normal scheduler does not. | `scripts/measure-heartbeat.sh` | `beats 18 elapsed 200` then `beats 0 elapsed 200`, then `heartbeat ok dirty=18 normal=0`. | Flags come from `abi_dirty_cpu()` (1) and `c_uint(0)`. The normal library is built in a temp directory. Elixir `Task.yield` returning nil did not stop `burn/1`. | high | this Mac, `elixir --erl "+S 1"` |
| E23 | An external app, an umbrella child, and a copied release all get 42. | `scripts/release-candidate.sh` | Consumer `1 passed`, again after `mix clean`, umbrella child `1 passed`, `release 42`, `release ok`. | The Mix compiler built the dependency. The copied release still resolved the venv rpath on this machine. KGEN was not bundled. | high | this Mac |
| E24 | CPU demo oracles match Elixir. | `scripts/demo-cpu.sh` | `oracle 42`, `copy {:ok, <<1, 2, 3>>, 3}`, `borrow {:ok, 32, 1}`, `frozen <<4, 9>> token 1`, `retained <<7, 7>>`, `dirty_burn_us 49568`, `warm_add_us 4`. | The warm number is one call, not a throughput claim. `add/2` is not presented as faster than Elixir. | high | this Mac |
| E25 | The historical GPU NIF still returns 42, and a 4-wide batch returns 110, including a repeat and two concurrent calls. | `scripts/gpu-batch.sh`, then a second Elixir process for the concurrent line | `gpu_nif 42`, `gpu_batch 110`, `gpu_batch_repeat 110`. Concurrent `inspect` printed `~c"nn"`, which is the charlist `[110, 110]`. | CPU reference is `1+10+2+20+3+30+4+40`. The batch NIF uses `abi_dirty_cpu()`. `experiments/gpu/gpu_nif.mojo` was not edited. No-device was not observed. NVIDIA and Linux GPU were not run. | high | this Mac, Apple GPU, MAX 26.6.0 |
| E26 | An aborting NIF takes down only the child BEAM. | `scripts/probe-abort.sh` inside `mix test` | The probe test passed. The script exits 0 only when the child exits non-zero. | This is `abort`, not a Mojo `raise` inside safe arithmetic. Product NIFs do not call it. | high | this Mac |

Unknowns still open: NVIDIA, any GPU call on Linux, the `{:error, :no_device}` branch on a machine that actually has no accelerator, macOS ARM64 in Actions (no runner is configured), and the Linux workflow for stages 1–10 (the file is updated; this commit has not been executed there). `Mojoler.Own` frees its private box in `unload`. The entry and function-table `calloc` blocks are still not freed, because OTP retains the entry; a second `load_nif` is rejected, so that residue is one pair of blocks per loaded module per process. Upgrade returns 1 and was not invoked.
