Skip to content

fix(runtime): quote atoms under ~w and ~p as OTP does - #11

Merged
mremond merged 1 commit into
mainfrom
fm/task-25dd220d
Nov 12, 2026
Merged

mremond merged 1 commit into
mainfrom
fm/task-25dd220d

Conversation

@mremond

@mremond mremond commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

io:format and io_lib:format printed every atom bare, so a trapped exit came out as {EXIT,bye} and atoms such as 'Foo' or 'hello world' did not read back as the same term, unlike on BEAM.

What changed

  • ~w, ~p, ~W and ~P quote atoms with OTP's rules: bare only when an atom starts with a lowercase letter, holds only letters, digits, _ and @, and is not a reserved word (maybe and else included).
  • Quotes, backslashes and control characters are escaped inside the quotes; characters above 255 print as \x{...} unless the t modifier is given.
  • A new io_format_atom_quoting fixture runs natively and on BEAM.
  • The Cowlib demo's two 'HTTP/1.1' lines now match OTP, so they leave its known differences; FEATURES, CHANGELOG, KNOWN_LIMITS and the compatibility records say so.

Before and after

Before: Programs printed {EXIT,bye}, Foo and hello world; 43 of 44 lines of an atom probe differed from OTP 29.

After: Programs print {'EXIT',bye}, 'Foo' and 'hello world'; all 44 probe lines are byte-identical with OTP 29.

The change is in text a program prints, so the before and after are shown as program output rather than pictures.

Checked

  • Atom probe (44 lines: uppercase, empty, punctuated, reserved, escaped, Latin-1 and non-Latin-1 atoms through ~w, ~p, ~tw, ~tp, ~W, ~P and field widths) compared with OTP 29.1.2: Before: 43 of 44 lines differed. After: identical, including after bringing the branch up to date with the native timer change.
  • io_format_atom_quoting regression: Fails before the change (prints {EXIT,bye}); passes after, 10 of 10 runs natively and 10 of 10 on BEAM.
  • Cowlib 2.22.0 demo compared with OTP 29: 77 of 85 calls byte-identical (was 75); sorted comparison leaves only the 2 known byte-order lines.
  • Full end-to-end suite: 468 passed, 0 failed, 2 existing skips.
How it was checked

cargo build --release --locked -p nera-runtime; cargo build --release --locked -p nera

cargo clippy --workspace --all-targets --locked -- -D warnings: clean

cargo test --workspace --locked --no-fail-fast: 2,139 passed, 0 failed, 1 ignored

./target/release/nera test -j 6 --timeout 30: 468 passed, 0 failed, 2 skipped (onecached fixtures needing external modules)

nera test io_format_atom_quoting ten times in a row: 10/10; erlc + erl running the same module ten times: 10/10

python3 scripts/test-cowlib-demo.py with the updated known differences: 4/4 tests pass, no known line newly identical

Probe module compiled with the base and patched compilers and with erlc, outputs diffed against OTP 29.1.2 on macOS arm64

After bringing the branch up to date with the native timer change: rebuilt, then io_format_atom_quoting, the six io_format/io_lib fixtures and the three timer fixtures pass (10/10), and the probe is still identical with OTP

Risk

Risk above low: Blast radius, medium — The shared term writer is used by every ~w/~p/~W/~P call and by crash messages; Behavior, medium — Printed output changes by default for atoms that need quotes, now matching BEAM

Full risk assessment
Dimension Level Why
Blast radius medium The shared term writer is used by every ~w/~p/~W/~P call and by crash messages
Behavior medium Printed output changes by default for atoms that need quotes, now matching BEAM
Undo low Revert and done; no saved data or settings
Evidence low Byte-identical with OTP 29 on a 44-line probe, regression on both engines, full suite
Sensitivity low Formatting only
Novelty low One helper beside the existing term writer, following OTP's io_lib
Dependencies low No packages added or updated
Not done

Nera's own crash message (Error in process ...) now quotes atoms too, since it shares the term writer.

The separate error_logger renderer keeps its own simpler quoting rule and is unchanged.

@mremond mremond added this to the 0.1.1 milestone Nov 12, 2026
io:format/2 and io_lib:format/2 printed every atom bare, so a caught
exit came out as {EXIT,bye} and atoms such as 'Foo' or 'hello world'
did not read back as the same term.

~w, ~p, ~W and ~P now write atoms with OTP's io_lib rules: an atom is
bare only when it starts with a Latin-1 lowercase letter, holds only
Latin-1 letters, digits, _ and @, and is not a reserved word (maybe and
else included). Quotes, backslashes and control characters are escaped
inside the quotes, and characters above 255 print as \x{...} unless the
t modifier is given.

The io_format_atom_quoting fixture runs natively and on BEAM. The
Cowlib demo's two 'HTTP/1.1' lines now match OTP, so they leave its
known differences, and the docs that recorded them are updated.
@mremond
mremond merged commit 4f104af into main Nov 12, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant