Skip to content

fix(io): use U32 main results as exit status - #1474

Closed
oxura wants to merge 2 commits into
bendlang:mainfrom
oxura:fix/1465-io-u32-exit-status-clean-20261010
Closed

oxura wants to merge 2 commits into
bendlang:mainfrom
oxura:fix/1465-io-u32-exit-status-clean-20261010

Conversation

@oxura

@oxura oxura commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Fix

Closes #1465.

Use the returned word of an IO(U32) main as its process exit status in native C, generated JS, and the interpreter. Stderr remains independent: printing a usage error and returning 2 now exits 2 without printing the returned word. IO(Unit) and pure mains keep their existing successful exit behavior.

Validate main and IO.die statuses against 0..255 before process exit. In particular, 256 fails with a diagnostic and exit 1 instead of being silently truncated by the OS to success.

Native root tracking reads the existing Emit payload before dropping it, adds no allocation, and clears the completed root pointer so later spawned tasks cannot overwrite the status. Both runtimes still drain spawned IO. The IO(Unit) native control binary has no io_root or io_result symbols (nm checked).

Migrate three printing/channel/join fixtures from previously ignored nonzero main values to explicit successful returns, preserving their existing output goldens; remove the obsolete comments claiming that main's word is discarded. Update the existing IO guide within its unchanged token cap.

Verification

Linux x86_64, Bun 1.3.13, Clang 22.1.8; native runs used 2 CPU threads with GPU off.

  • Before: the same three issue reproductions on all three backends exited 0, including main-return 256 and IO.die(..., 256, "x").
  • After: five permanent regressions plus the existing IO(Unit) control passed on interpreter, generated JS, and native C: 18/18 real runs. Includes silent status 255, out-of-range main/die, and root status 2 while a child and later grandchild finish with different values.
  • Existing IO regression slice: 27/27 real runs covering sleep ordering, spawn outliving main, fork/join, second-join failure, printing, and stderr/ENOENT exit 2.
  • True pure U32 main control: all three backends printed 42 and exited 0.
  • Real --checkup continued through all six modules, recorded each expected nonzero exit, and reached the final successful control before returning aggregate failure as intended.
  • bun gates/repo.ts: PASS: 54 / 54 with all ten changed source/test/doc files staged. Permanent source and documentation caps unchanged; bend2/bend.ts untouched.

The remote 48-Mac-mini full gate matrix, macOS/Windows, and GPU lanes were not run. This is portable local qualification, not a claim that the complete cluster matrix passed.

@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Thanks for the work on this, @oxura, but I'm closing it: this isn't the design we want to follow.

Making main's IO(U32) result the exit status changes the language's model. Every existing IO(U32) main now exits with whatever it returns, which is why several tests had to change. That's a bigger change than #1465 needs.

The direction we'd rather take is an explicit IO.exit(code) that exits with the code and prints nothing. It's shorter and fits our model better: IO.die stays the way to fail with a message, and IO.exit is the quiet one.

The 0..255 fix is still needed. Codes outside that range shouldn't silently wrap (today IO.die(256, ...) exits 0). A PR with only that, plus IO.exit, would be welcome.

Also, please don't edit the GUIDE with AI. It's written by hand only.

@oxura

oxura commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Understood. I will keep IO(U32) main's existing behavior, remove the GUIDE change from the replacement, and use the narrower design you requested: explicit quiet IO.exit(code) plus strict 0..255 checks for it and IO.die. I'll publish that separately rather than reopen this rejected redesign.

@oxura

oxura commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

The requested narrow replacement is now #1492, commit 3b7ae9d.

IO.exit(code) is a pure checked Base def returning IO(Unit), with a distinct quiet opcode. IO(U32) main still ignores its result; no main/argv redesign and no GUIDE edit. Both runtimes reject codes above 255 for IO.exit and IO.die. IO.die retains its caller message/newline.

Actual verification: six terminal scenarios × interpreter/Bun/Node/native C, including quiet 0/255 (zero stderr bytes), 256/UINT32_MAX (status 1), and preserved empty-message IO.die newline/status 7. A returned-U32 main still prints its output and exits 0 in all four lanes. The installed compiled CLI and final range fixture also pass with empty PATH. Existing alien-request guards and UTF-8 halt behavior are preserved. Genuine BendTT accepts a structural pure-exit proof; repository gate 54/54. Mini-cluster/GPU gates were not run.

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.

main cannot set an exit status without writing to stderr, and IO.die codes above 255 wrap (256 exits 0)

2 participants