Skip to content

feat(io): add positioned byte writes - #1887

Merged
Lorenzobattistela merged 4 commits into
bendlang:mainfrom
vicmcorrea:feat/file-write-at
Oct 27, 2026
Merged

Lorenzobattistela merged 4 commits into
bendlang:mainfrom
vicmcorrea:feat/file-write-at

Conversation

@vicmcorrea

@vicmcorrea vicmcorrea commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Problem and fix

r+ permits sequential overwrites, but Base has no positioned write that preserves the sequential cursor. Add File.write_at(file, offset, bytes) using C pwrite and an explicit JS fs.writeSync position. Related to #1843.

Append handles now return EINVAL before writing, including empty data, avoiding Linux silently appending at the wrong position. C checks descriptor flags; JS records append opens and clears them on close. The Base comment documents that a later failure can leave a written prefix.

Blast radius

One additive Base definition, the existing C/JS file-write effects, JS open/close tracking, and one shared JS-runtime Set in comp.ts so separately scoped effect sources can share flags. Four regression files; ten files overall. No parser, emitter logic, dependency, gate, or cap changes. Existing sequential writes and #1817's shipped stdout flush remain intact.

Starting offsets remain U32; this does not complete #1843's 64-bit offsets/sizes or sync proposal. No atomicity, rollback, or durability guarantee is added.

Regression evidence

The ordinary fixture covers binary bytes, unchanged neighbors, cursor preservation, EOF growth, holes, empty writes, byte validation, and handle reuse after failure. Its append extension rejects nonempty and empty positioned writes, verifies unchanged content after an allowed sequential append, and reopens for a successful positioned write. This extension fails against the previous PR head 5d90248 on source CLI, native C, Bun, and Node.

The controlled fixture exercises short writes, C EINTR retries, zero progress, and errors after partial progress. Independent mutations detect missing JS append registration, missing close cleanup, and misclassifying a failed C flags query as append.

Checks

  • Repository gate 54/54 with CI-pinned ttok 0.3; git diff --check passes.
  • Both fixtures and existing binary, interrupted/zero-write, r+, byte-path, and stdout-order regressions pass source CLI, native C, emitted Bun, and emitted Node on macOS arm64.
  • 96 deterministic random writes and a sparse write crossing 4 GiB match independent Python byte/size checks on all four paths.
  • Linux arm64 Docker: 8/8 native C/Node runs pass, including append rejection and independent complete-file/sparse-region checks.
  • Independent runtime controls pass for open_bytes, empty append writes, exact descriptor reuse, foreign handles without an open effect, and closed-descriptor errors. The gate-style --checkup aggregate also passes with both new fixtures, interrupted writes, and stdout ordering.

Validation limits

Local macOS uses Bun 1.3.11, Node 26.8.2, and clang 23.1.4; Linux uses Node 26.12.1 and clang 14.0.6. Full mini-cluster test/performance/ping gates, GPU, Windows, x86-64 Linux, and Linux Bun were not run for this revision. Short/EINTR/zero-progress controls use fault injection; ordinary file operations and append rejection were observed through the OS. JS tracks append flags for handles opened through Bend; flags changed by foreign code are outside this check.

Existing file_open_mode passes three macOS paths; its Node error-message mismatch reproduces unchanged on the baseline. The foreign control fixture's --check-only reports foreign-dependent definitions; its executable paths pass. #1817 is now merged and included in the tested branch. Older #1601's complete integration remains unverified.

@Lorenzobattistela Lorenzobattistela self-assigned this Oct 27, 2026

@Lorenzobattistela Lorenzobattistela left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a clean addition, and the fault-injection fixtures are good. Merged onto main 71ff27d, every gate is green: repo 54/54, test 1750/1750, perf 124/124, ping 81/81, safe with 0 false rejects. Two things to fix before merge:

  1. Reject append handles instead of documenting them.
    On Linux, pwrite and fs.writeSync(position) on an O_APPEND fd ignore the offset and append. So File.open(p, "a") then File.write_at(f, 0, [66]) on a file holding AAAA gives AAAAB on Linux, BAAA on macOS, and returns Done on both. The doc comment says "on a non-append handle", but nothing enforces it, so on Linux the bytes silently land in the wrong place. Please fail with EINVAL before writing:

    • C: check fcntl(fd, F_GETFL) & O_APPEND in file_write_at_run.
    • JS: Node can't read the open flags back from an fd, so record append handles in effs/file_open.js (e.g. a Set of fds opened with "a", cleared in file_close) and check it in file_write_at.
      Add a case to file_write_at.bend that opens with "a", expects 22, and checks the file is unchanged.
  2. Document partial writes in base.bend.
    If a later chunk fails or makes no progress, write_at returns Fail, but the earlier chunks are already in the file (your control trials 1 and 2 show [0, 255] at 17-18 after Fail 5/13). The PR description says this, but the def comment doesn't. One line is enough, e.g. "A Fail may leave a prefix of the bytes written."

Optional: file_write_at_run copies file_write_bytes_run except for w->made and the call function. A shared run helper (like file_read_start in file_read.c) would remove the copy and the per-mode wrappers.

@vicmcorrea

Copy link
Copy Markdown
Contributor Author

@Lorenzobattistela addressed both points, lmk if anything else is needed.

@Lorenzobattistela
Lorenzobattistela merged commit 757180e into bendlang:main Oct 27, 2026
1 check passed
nicolas-abril added a commit that referenced this pull request Oct 27, 2026
#1887 added File.write_at on file_write_buffer, which no longer flushes
held stdout itself, so on JS the positioned write ran before the text
IO.print held. It now parks like File.write and File.write_bytes,
keeping its Fail(22) for bad bytes and append files.

Also: process_run names its parameters, the comment above file_open
goes, and the FIFO test fails at setup if mkfifo, pipe or fork fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants