# Stage 1 — ABI foundation

Verdict on this Mac: **PASS**. Linux: **PENDING**. The workflow step is `scripts/check-abi.sh`. It has not run on Ubuntu for this tree.

`tools/abi/generate.py` reads `clang -ast-dump=json` and `clang -dM -E` for the allowlisted slice of this OTP's `erl_nif.h`. A C probe prints `sizeof` and `offsetof`. That probe is compiled by the check script and is not an input in `priv/native/link-inputs.txt`.

`native/mojoler/abi/otp_nif.mojo` is the committed output. `python3 tools/abi/generate.py --check` diffs it. A requested triple other than the host is refused. File-scope `alias` does not parse in Mojo 1.1.0, so version numbers and dirty flags are functions.

`experiments/stage0/poc_direct.mojo` is the pre-extraction `poc.mojo`. The live file imports the generated module and still implements `Elixir.Mojoler.POC.add/2`. `native/twice.mojo` uses the same module and does not declare `ErlNifFunc` or `ErlNifEntry`.

`scripts/check-imports.sh` fails the build of `experiments/abi/negative/bad_import.mojo` because `enif_make_lonq` is not on the allowlist. E20 records the successful check, including `dirty flags ok`.

## Descriptor lifetime

`nif_init` `calloc`s the entry and the function table and returns the entry pointer. OTP stores it. Unload is not a safe place to free it: the loader can still read the struct while the library is mapped. Both blocks live until the OS reaps the process.

A second `load_nif` of the same module returns `{:error, {:reload, _}}` (E14). Growth is therefore two blocks per successfully loaded module per OS process, not per call. Stage 3 frees a different allocation, the `Mojoler.Own` private box, in `unload`, with `free`. That box is not the entry.
