Skip to content

v0.0.123: tee IO.print writes live to sys.stdout (#543)#544

Merged
aallan merged 4 commits into
mainfrom
v0.0.123-io-print-tee
Apr 27, 2026
Merged

v0.0.123: tee IO.print writes live to sys.stdout (#543)#544
aallan merged 4 commits into
mainfrom
v0.0.123-io-print-tee

Conversation

@aallan

@aallan aallan commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Closes #543.

Summary

IO.print writes were accumulated in an in-memory output_buf (the v0.0.120 implementation of #522 trap preservation) and only flushed to sys.stdout after execute() returned. That was correct for trap preservation and for --json output, but it had an unintended side effect: programs using ANSI escape sequences (cursor home, clear screen) for animation, progress bars, REPLs, or any other interactive pattern were invisible until exit.

The 470-line Conway implementation that surfaced #515 made it visible: ~16 seconds (200 generations × 80ms IO.sleep) of nothing on screen, then exit fired and only the final frame was visible because the 199 preceding cursor-home + clear-screen escape sequences processed within microseconds and the eye couldn't resolve them.

Fix

Tee host_print (vera/codegen/api.py):

  • The in-memory output_buf still receives every byte (so WasmTrapError.stdout, ExecuteResult.stdout, and the --json envelope's stdout field are unchanged — the IO.print output lost when program traps (stdout fully buffered) #522 trap-preservation contract still holds).
  • Writes are also mirrored to sys.stdout with an explicit per-write flush() when execute(tee_stdout=True).

New tee_stdout: bool = False parameter on execute(). Default off so test helpers (_run_io, _run in tests/test_codegen.py) get silent runs. cmd_run text mode opts in (tee_stdout=not as_json); JSON mode stays off (live writes would corrupt the envelope for downstream consumers parsing our stdout).

The cmd_run text-mode and WasmTrapError-handler paths now skip re-writing exec_result.stdout / exc.stdout (those bytes already streamed live), only emitting a closing \n if the program's last write didn't include one — without that change every transcript would have double-printed.

Verification

public fn main(@Unit -> @Unit)
  requires(true) ensures(true) effects(<IO>)
{
  IO.print("tick 1\n");
  IO.sleep(500);
  IO.print("tick 2\n");
  IO.sleep(500);
  IO.print("tick 3\n")
}

Pre-fix: nothing visible for 1 second, then all three lines appear at once. Post-fix: each tick appears live as IO.sleep returns.

Tests

New TestStdoutTee543 class in tests/test_runtime_traps.py (6 tests):

  • Live streaming in text mode (each IO.print appears exactly once)
  • Write order preserved
  • JSON-mode tee suppression (envelope-corruption guard)
  • Trap preservation invariant still holds (IO.print output lost when program traps (stdout fully buffered) #522 regression guard)
  • Per-write flush count matches per-call IO.print count
  • Default execute() behaviour stays silent for the test suite

Documentation

  • SKILL.mdIO.print row in the IO operation table notes "no implicit newline; flushes per call"; new paragraph after the table explains the buffering / live-write semantics for text mode and JSON mode and the trap-preservation guarantee.

Note on merge order

This PR builds on top of #542 (v0.0.122 GC fix). Recommended merge order: #542 first, then this. No code conflict either way; if this lands first, #542 needs a small CHANGELOG/HISTORY/version-link rebase.

Test plan

  • pytest tests/ -q — 3,561 passed, 14 skipped
  • mypy vera/ — clean
  • python scripts/check_doc_counts.py — consistent (3,575 tests)
  • python scripts/check_version_sync.py — version 0.0.123 consistent
  • All pre-commit hooks pass
  • Manual: animated IO.print + IO.sleep program renders live
  • Manual: 470-line Conway runs end-to-end and animates frame-by-frame

Co-Authored-By: Claude noreply@anthropic.invalid

Summary by CodeRabbit

  • New Features

    • IO.print now streams live to the console in text-mode runs with per-write flushing for interactive output.
  • Bug Fixes

    • Prevented double-emitting of captured stdout during normal and trapped runs.
    • Improved conservative garbage-collector bounds checking.
  • Documentation

    • Clarified IO.print newline/flush and runtime output behaviour across modes; updated changelogs and status counts.
  • Tests

    • Added end-to-end tests validating streaming order, JSON-envelope integrity, trap preservation, and flush/count behaviour.

Closes #543.

IO.print writes were accumulated in an in-memory output_buf (the
v0.0.120 implementation of #522 trap preservation) and only flushed to
sys.stdout after execute() returned.  That was correct for trap
preservation and for --json output (where the transcript packs into
the envelope), but it had an unintended side effect: any program using
ANSI escape sequences for animation, progress bars, REPLs, or any
other interactive pattern was invisible until exit.  The 470-line
Conway implementation that surfaced #515 made it visible: the user
saw nothing for ~16 seconds (200 generations x 80ms IO.sleep) and
then exit fired and only the final frame was visible because the 199
preceding cursor-home + clear-screen sequences processed within
microseconds.

Fix is a tee: host_print always writes to output_buf (preserving the
#522 trap-preservation contract) AND writes to sys.stdout with an
explicit per-write flush() when execute(tee_stdout=True).  New
tee_stdout: bool = False parameter on execute() defaults off so
test helpers (_run_io, _run in test_codegen.py) still get silent
runs.  cmd_run text mode opts in (tee_stdout=not as_json); JSON
mode stays off (live writes would split the envelope for downstream
consumers parsing our stdout).

The cmd_run text-mode and WasmTrapError-handler paths now skip
re-writing exec_result.stdout / exc.stdout (those bytes already
streamed live), only emitting a closing newline if the last write
did not include one -- without that change every transcript would
double-print.

Tests: new TestStdoutTee543 class in tests/test_runtime_traps.py
(6 tests): live streaming in text mode, write count and order
preservation, JSON-mode tee suppression, trap-preservation
invariant still holds (#522 regression guard), per-call flush
count matches per-call IO.print count, default execute() stays
silent.

Docs: SKILL.md IO.print row notes per-call flush; new paragraph
explains the buffering / live-write semantics for both text and
JSON modes.

Note: this builds on top of #542 (v0.0.122 GC fix); merge order
should be #542 then this PR, with a rebase if needed.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4cf2b12d-4957-458c-b468-78cfb882184c

📥 Commits

Reviewing files that changed from the base of the PR and between 510135a and eee5e3b.

📒 Files selected for processing (2)
  • HISTORY.md
  • TESTING.md

📝 Walkthrough

Walkthrough

Implements live stdout streaming for IO.print during vera run text mode via a new tee_stdout option on execute(). Each IO.print call is appended to the in-memory capture and, when enabled, mirrored to sys.stdout with per-write flush; JSON mode remains unchanged to avoid envelope corruption.

Changes

Cohort / File(s) Summary
Version & Package Metadata
vera/__init__.py, pyproject.toml, README.md
Bumped package/version identifiers to 0.0.123 and updated reported release/test counts.
Changelog & Release History
CHANGELOG.md, HISTORY.md, ROADMAP.md
Added 0.0.123 (and 0.0.122) release entries and updated compare links and quantitative metrics.
Documentation: IO.print Contract
SKILL.md
Clarified IO.print semantics: no implicit newline; per-call flush; text-mode live tee to sys.stdout and JSON-mode suppression documented.
Test Suite Metrics & Docs
TESTING.md
Updated global test counts and expanded test_runtime_traps.py description to include live-tee semantics and related assertions.
Implementation: Core Runtime
vera/codegen/api.py, vera/cli.py
Added execute(..., tee_stdout: bool = False); host_print now conditionally mirrors to sys.stdout with flush while still appending to output_buf; cmd_run enables tee in text mode and avoids re-emitting captured stdout (emits trailing newline when needed); JSON mode unchanged.
Script: ALLOWLIST Synchronisation
scripts/check_skill_examples.py
Adjusted ALLOWLIST line-number keys to match SKILL.md shifts (rekeyed allowlist entries).
Tests: Runtime Output Tee Validation
tests/test_runtime_traps.py
Added TestStdoutTee543 end-to-end tests validating streaming order/flush, JSON envelope integrity, trap-preservation under tee, and default non-tee execute() behaviour.

Sequence Diagram

sequenceDiagram
    participant CLI as cmd_run
    participant Exec as execute()
    participant Host as host_print
    participant Buf as output_buf
    participant Out as sys.stdout
    participant JSON as JSON_Mode

    rect rgba(100,200,150,0.5)
    Note over CLI,JSON: Text mode (tee_stdout = True)
    CLI->>Exec: execute(tee_stdout=True)
    Exec->>Host: IO.print("msg")
    activate Host
    Host->>Buf: write(text)
    Host->>Out: write(text)
    Host->>Out: flush()
    deactivate Host
    Exec-->>CLI: ExecuteResult(stdout=buf)
    end

    rect rgba(150,150,200,0.5)
    Note over CLI,JSON: JSON mode (tee_stdout = False)
    CLI->>Exec: execute(tee_stdout=False)
    Exec->>Host: IO.print("msg")
    activate Host
    Host->>Buf: write(text)
    Host--xJSON: (no live write)
    deactivate Host
    Exec-->>CLI: ExecuteResult(stdout=buf) wrapped in JSON envelope
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Labels

compiler, tests, ci, docs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a tee mechanism for IO.print to stream output live to sys.stdout, with version bump to 0.0.123 and issue reference.
Linked Issues check ✅ Passed All primary coding requirements from #543 are met: tee_stdout parameter added to execute(), host_print mirrors to sys.stdout with per-write flush, cmd_run enables tee for text mode only, trap-preservation semantics preserved, and comprehensive tests added.
Out of Scope Changes check ✅ Passed All changes align with #543 objectives: version bumps (pyproject.toml, vera/init.py), documentation updates reflecting the new behavior (SKILL.md, ROADMAP.md, README.md, HISTORY.md, TESTING.md, CHANGELOG.md), script allowlist adjustments for SKILL.md line shifts, and the new test suite are all in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v0.0.123-io-print-tee

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Apr 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.02%. Comparing base (d101fe7) to head (eee5e3b).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
vera/cli.py 75.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #544   +/-   ##
=======================================
  Coverage   91.02%   91.02%           
=======================================
  Files          58       58           
  Lines       22033    22034    +1     
  Branches      259      259           
=======================================
+ Hits        20055    20056    +1     
  Misses       1971     1971           
  Partials        7        7           
Flag Coverage Δ
javascript 56.83% <ø> (ø)
python 94.97% <88.88%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@HISTORY.md`:
- Around line 259-260: Collapse the two multi-line HISTORY entries for v0.0.122
and v0.0.123 into single-line summary rows (e.g., "v0.0.122 | 27 Apr |
Conservative GC bounds-checked against $heap_ptr" and "v0.0.123 | 27 Apr |
IO.print writes mirror live to sys.stdout"), and update the tagged-release
roll-up string that currently reads "121 tagged releases" to "123 tagged
releases" in both HISTORY.md and the paired ROADMAP.md so the totals stay in
sync.

In `@ROADMAP.md`:
- Line 11: The ROADMAP.md line currently states "33 examples" but the
repository's checked count and the docs invariant expect "32"; update the phrase
"33 examples" to "32 examples" in ROADMAP.md and scan other documentation
summaries (e.g., README.md, TESTING.md, CLAUDE.md and any other prose that
mentions the example count) to change any "33" occurrences to "32" so they stay
in sync with scripts/check_doc_counts.py and the repo-wide checked count.

In `@tests/test_codegen.py`:
- Around line 1925-1927: Replace the brittle string assertions that only check
comment markers ("Layer 2 (issue `#515`)" / "Layer 1 (issue `#515`)") with
assertions that inspect the WebAssembly text in the variable wat for the actual
guard instruction patterns around the i32.load in the scan loop; specifically,
locate the i32.load occurrence for the scan path and assert the surrounding
instruction sequence includes the expected heap-pointer guard pattern (e.g.,
explicit i32.const/i32.add bounds computation and a conditional/br_if or
i32.lt_u check) for both the per-iteration (Layer 1) and outer (Layer 2) cases,
using regex or index-based substring checks rather than comment text so the test
fails if the guard instructions regress.

In `@tests/test_runtime_traps.py`:
- Around line 479-508: The test test_json_mode_does_not_tee_to_stdout currently
only checks captured.out but not captured.err; after calling captured =
capsys.readouterr() add an assertion that captured.err == "" to ensure no
human-readable stderr leaked in JSON mode (this applies to any other tests that
call cmd_run(..., as_json=True) — update those similarly). Locate the assertion
block in test_json_mode_does_not_tee_to_stdout and insert the captured.err check
immediately after reading outputs from capsys to fail tests on any stderr
leakage.

In `@vera/cli.py`:
- Around line 624-636: The code that appends a trailing newline when
exec_result.stdout doesn't end with one should only do so for interactive
terminals; update the branches that currently call sys.stdout.write("\n")/flush
(the normal branch handling exec_result.stdout and the trap/alternative branch
around the same logic) to first check sys.stdout.isatty() and only write/flush
the "\n" when isatty() is True; locate the logic around the exec_result.stdout
checks in cli.py (and the similar block at the other occurrence noted) and gate
the extra newline on sys.stdout.isatty() in both places.

In `@vera/codegen/assembly.py`:
- Around line 680-702: The code currently only bounds-checks a decoded obj_size
but can still treat non-header payload as a header and flip the mark bit; add an
explicit header-validity check immediately after computing local $header and
local $obj_size (before any mark-store or clearing/setting of the mark bit) that
verifies the word at local $header satisfies the header tag/format (e.g.,
mask/compare against the known HEADER_TAG or other header invariants used
elsewhere in the runtime) and that obj_ptr and obj_size are in-range; if the
header test fails, branch to $m_loop (same path used for the size-overflow
check). Alternatively (or additionally), move the mark bit out of object payload
state so mark writes never touch decoded payload words — but the minimum fix is
to add the header-format test using the same header-mask/constant used by the
allocator/GC before any mark-store.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 588d693b-4911-4eb8-8e63-2dd3bf721141

📥 Commits

Reviewing files that changed from the base of the PR and between 40396b6 and 2f3c3e3.

⛔ Files ignored due to path filters (5)
  • docs/SKILL.md is excluded by !docs/**
  • docs/index.html is excluded by !docs/**
  • docs/index.md is excluded by !docs/**
  • docs/llms-full.txt is excluded by !docs/**
  • docs/llms.txt is excluded by !docs/**
📒 Files selected for processing (16)
  • CHANGELOG.md
  • HISTORY.md
  • KNOWN_ISSUES.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_skill_examples.py
  • tests/test_codegen.py
  • tests/test_runtime_traps.py
  • vera/README.md
  • vera/__init__.py
  • vera/cli.py
  • vera/codegen/api.py
  • vera/codegen/assembly.py
💤 Files with no reviewable changes (1)
  • KNOWN_ISSUES.md

Comment thread ROADMAP.md
Comment thread tests/test_codegen.py Outdated
Comment thread tests/test_runtime_traps.py
Comment thread vera/cli.py
Comment thread vera/codegen/assembly.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
HISTORY.md (1)

297-297: ⚠️ Potential issue | 🟠 Major

Fix tagged-release total mismatch in HISTORY.md footer.

Line 297 still reports “122 tagged releases”, but the file now includes v0.0.123 (Line 260~) and ROADMAP.md shows 123 tagged releases (Line 274~). This violates the repo rule that those totals are incremented together.

🔧 Proposed fix
-Total: **810+ commits, 122 tagged releases, 40 active development days.**
+Total: **810+ commits, 123 tagged releases, 40 active development days.**
[/raise_major_issue]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HISTORY.md` at line 297, Update the tagged-releases total in the HISTORY.md
footer: change the string "Total: **810+ commits, 122 tagged releases, 40 active
development days.**" to reflect 123 tagged releases (matching the presence of
v0.0.123 in this file and ROADMAP.md’s count), ensuring the numeric total for
"tagged releases" is incremented from 122 to 123 while keeping the rest of the
footer text unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@HISTORY.md`:
- Line 260: Edit the v0.0.123 row in HISTORY.md so it contains only the
user-visible outcome (the concise release summary) and remove the internal
“Discovered while …” troubleshooting/process note; specifically trim the text
for the entry referencing v0.0.123 (the row that mentions IO.print writing
mirror live to sys.stdout and the linked issue `#543`) to a single-sentence
outcome-only line and move any explanatory discovery details to CHANGELOG if you
want to preserve them.

---

Outside diff comments:
In `@HISTORY.md`:
- Line 297: Update the tagged-releases total in the HISTORY.md footer: change
the string "Total: **810+ commits, 122 tagged releases, 40 active development
days.**" to reflect 123 tagged releases (matching the presence of v0.0.123 in
this file and ROADMAP.md’s count), ensuring the numeric total for "tagged
releases" is incremented from 122 to 123 while keeping the rest of the footer
text unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f6dd192e-e614-4ab8-be8b-24422756f556

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3c3e3 and fae9515.

📒 Files selected for processing (3)
  • HISTORY.md
  • ROADMAP.md
  • TESTING.md

Comment thread HISTORY.md Outdated
Three fixes from the post-rebase CodeRabbit pass on #544:

* HISTORY.md footer: "122 tagged releases" -> "123 tagged releases".
  CodeRabbit reported it as "121 -> 123" but the prior fix in #542
  had already taken it to 122; bumping it the rest of the way for
  this release.

* tests/test_runtime_traps.py: add captured.err == "" assertion in
  test_json_mode_does_not_tee_to_stdout.  Mirrors the JSON-mode
  invariant that TestStdoutOnTrap522 already pins on its four JSON
  tests -- in JSON mode the entire output is the envelope on stdout
  and stderr must stay empty so downstream consumers parsing our
  output see exactly one machine-readable document.

* uv.lock: regenerated for the 0.0.122 -> 0.0.123 version bump.
  Lint job was failing on uv lock --check.

Three other CodeRabbit findings verified as either incorrect or
out of scope (replying inline on the PR):

* HISTORY.md "collapse multi-line entries" -- the v0.0.122 / v0.0.123
  rows are already single rows; CodeRabbit was reading wrapped
  display as multi-line content.

* ROADMAP.md "33 examples -> 32 examples" -- ls examples/*.vera
  reports 33; check_doc_counts.py and the live count agree.
  CodeRabbit's expected count is wrong.

* tests/test_codegen.py opcode assertions -- already strengthened
  in #542 (commit 75682a1 "PR #542 review fixes").  Markers stay
  as discoverability anchors; opcode patterns are the contract.

* vera/cli.py isatty() gating on the trailing newline -- pre-existing
  behaviour from before #543, unchanged by this PR.  Out of scope:
  changing the trailing-newline policy is a separate concern with
  its own backwards-compat surface.

* vera/codegen/assembly.py header-validity check -- same finding
  as the one already discussed and resolved on #542.  No separate
  HEADER_TAG invariant exists in this codebase (header is
  (size << 1) | mark with no marker bit beyond mark itself).  The
  obj_ptr + obj_size <= heap_ptr bound check is the strongest
  validation possible without introducing a new tag scheme.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Apr 27, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai thanks. Two of six findings actioned in 510135a; four verified as either incorrect or already-addressed (details below). Plus the uv.lock refresh required by the version bump.

Actioned:

  • HISTORY.md tagged-releases count — fixed: "122" → "123". (Your finding said "121 → 123"; the prior #542 review fix had already taken it to 122, so this commit takes it the rest of the way.)
  • test_json_mode_does_not_tee_to_stdout missing captured.err == "" check — added. Mirrors the JSON-mode invariant TestStdoutOnTrap522 already pins on its four JSON tests. Good catch.

Not actioned (verified):

  • HISTORY.md "collapse multi-line entries for v0.0.122 / v0.0.123" — they're already single rows. sed -n '259,260p' HISTORY.md returns two lines, one per row. CodeRabbit appears to be reading wrapped display as multi-line content.

  • ROADMAP.md "33 examples → 32 examples"ls examples/*.vera | wc -l returns 33; python scripts/check_doc_counts.py reports "33 examples" as the live count and confirms all docs are consistent. CodeRabbit's expected count is wrong.

  • tests/test_codegen.py opcode assertions — already strengthened in v0.0.122: bound conservative GC scan against $heap_ptr (#515) #542 (commit 75682a1 "PR v0.0.122: bound conservative GC scan against $heap_ptr (#515) #542 review fixes"). Look at lines 1962-1995: the test now asserts the exact opcode pattern (local.get $obj_ptr local.get $obj_size i32.add global.get $heap_ptr i32.gt_u if br $m_loop) adjacent to each marker, not just the marker text. Marker comments stay as discoverability anchors; opcode patterns are the contract.

  • vera/cli.py isatty() gating on the trailing newline — pre-existing behaviour from before IO.print buffered until exit — animations and TUIs invisible #543, unchanged by this PR. The same trailing-\n-when-missing logic was there before, just below an unconditional sys.stdout.write(exec_result.stdout). This PR moves the write but keeps the newline policy. Changing to isatty()-only is a separate concern with its own backwards-compatibility surface (would break any scripts assuming vera run always ends with a newline) and tests would need to be added that distinguish piped vs TTY behaviour. Out of scope for "make IO.print writes flush live"; happy to file as a follow-up issue if useful.

  • vera/codegen/assembly.py header-validity check — same finding as the one already discussed and resolved on v0.0.122: bound conservative GC scan against $heap_ptr (#515) #542 (reply). No separate HEADER_TAG invariant exists in this codebase: the header format is (size << 1) | mark with bit 0 = mark and bits 1-31 = size — there's no marker bit beyond mark itself to validate against. The obj_ptr + obj_size <= heap_ptr bound check (Layer 2) is the strongest validation possible without introducing a new tag scheme. The "alternatively, move the mark bit out of object payload state" suggestion is a separate-mark-bitmap refactor — substantial change, out of scope for this PR.

@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Thanks for the thorough breakdown — this is exactly the level of detail that keeps the learning loop tight.

Actioned — acknowledged:

  • The HISTORY.md count walk (121 → 122 in #542, then 122 → 123 here) makes sense given the two-PR sequence. Good to have it pinned correctly now.
  • The captured.err == "" addition is the right belt-and-suspenders approach given the JSON-mode stderr contract.

Not-actioned — all accepted:

  • HISTORY.md wrapping — understood; that was a display-rendering false positive on my end. I'll stop reading wrapped lines as multi-line entries.
  • ROADMAP.md example countcheck_doc_counts.py is authoritative; my expected count of 32 was wrong.
  • Opcode assertions — lines 1962–1995 in the post-v0.0.122: bound conservative GC scan against $heap_ptr (#515) #542 state already pin the full opcode sequence as the contract. Marker comments as discoverability anchors is the right pattern; I'll treat the adjacent opcode assertion as sufficient.
  • isatty() gating — agreed it is pre-existing behaviour and out of scope. The backwards-compatibility surface you describe (scripts relying on a guaranteed terminal newline) is real. Would you like me to open a follow-up issue for it?
  • assembly.py header-validity — noted. (size << 1) | mark with bit 0 = mark, bits 1–31 = size; no HEADER_TAG marker bit exists; obj_ptr + obj_size <= heap_ptr is the correct Layer 2 bound check. Won't re-flag this.

✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/544

Timestamp: 2026-04-27T16:14:35.756Z
Learning: In `vera/codegen/assembly.py` (aallan/vera), the GC object header format is `(size << 1) | mark`: bit 0 is the mark bit, bits 1–31 encode the object size. There is NO separate `HEADER_TAG` or marker-tag field. The strongest structural invariant available without a new tag scheme is the Layer 2 bound check `obj_ptr + obj_size <= heap_ptr`. Do NOT flag absence of a HEADER_TAG validity check — this was confirmed in PR `#542` and again in PR `#544`. A separate-mark-bitmap refactor is a substantial out-of-scope change.

Learnt from: aallan
URL: https://github.com/aallan/vera/pull/544

Timestamp: 2026-04-27T16:14:35.756Z
Learning: In `vera/cli.py` (aallan/vera), the trailing-newline-when-missing logic (emit `\n` if the last IO.print write did not end with `\n`) is intentional pre-existing behaviour, NOT conditioned on `isatty()`. Scripts rely on `vera run` always ending with a newline. Changing this to `isatty()`-only gating is a separate backwards-compatibility concern requiring new piped-vs-TTY test coverage. Do not flag the unconditional trailing-newline policy as a bug in PRs that merely move the write site.

Learnt from: aallan
URL: https://github.com/aallan/vera/pull/544

Timestamp: 2026-04-27T16:14:35.756Z
Learning: In `aallan/vera` `ROADMAP.md` and `examples/`, the canonical example count is **33** `.vera` files (verified by `ls examples/*.vera | wc -l` and `python scripts/check_doc_counts.py`). Do not flag a count of 33 as incorrect — 32 is the wrong expected value. `check_doc_counts.py` is the authoritative source for this count.

Learnt from: aallan
URL: https://github.com/aallan/vera/pull/544

Timestamp: 2026-04-27T16:14:35.756Z
Learning: In `HISTORY.md` for `aallan/vera`, stage-11 version rows are **single-line entries** in the markdown table. GitHub's display can wrap long rows visually, making them appear multi-line. Do not treat a visually wrapped table row as a multi-line entry requiring collapse — verify with `sed -n 'Xp' HISTORY.md` before flagging.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-29T10:15:51.538Z
Learning: In aallan/vera, general housekeeping PRs (CI hardening, documentation, tooling) may acceptably bundle incidental code fixes (e.g. adding `vera --version` CLI support, HTTP Content-Type headers, urlopen timeouts) without requiring them to be split into separate PRs. Do not flag such bundling as a scope concern in this repository.

Learnt from: aallan
Repo: aallan/vera PR: 536
File: HISTORY.md:258-258
Timestamp: 2026-04-27T12:21:59.379Z
Learning: In `HISTORY.md` and `ROADMAP.md` for aallan/vera, the "tagged releases" total appears in **three separate locations**: (1) the per-row version entry in the Stage 11 table in `HISTORY.md`, (2) the bottom-of-file roll-up footer in `HISTORY.md` (e.g. "810+ commits, 121 tagged releases, 40 active development days"), and (3) the "Where we are" / footer section in `ROADMAP.md`. All three must be incremented together on each release. aallan confirmed in PR `#536` (commit 7de39ff) that this count should be added to the gated list in issue `#528` (auto-generate or gate doc counts via `check_doc_counts.py`). Flag any PR where only a subset of the three locations is updated.

Learnt from: aallan
Repo: aallan/vera PR: 536
File: TESTING.md:76-76
Timestamp: 2026-04-27T12:42:17.520Z
Learning: In `aallan/vera` `TESTING.md`, the `check_doc_counts.py` per-file gate covers the `test_conformance.py` row in the "Test Files" table (around line 76) but does NOT gate the prose count inside the "Running the conformance suite" section (e.g. "Via pytest (parametrized — N tests)"). When a new conformance program is added (+5 parametrised tests per program), both locations must be updated together. This gap is tracked under issue `#528`. As of PR `#536` commit 7296b59, the count is 405 (81 programs × 5 stages).

Learnt from: aallan
Repo: aallan/vera PR: 511
File: tests/test_codegen.py:12473-12510
Timestamp: 2026-04-23T13:51:17.615Z
Learning: Repo aallan/vera — tests/test_codegen.py and tests/test_browser.py: When verifying json_as_int at -2^63 (JNumber(-9223372036854775808.0)), use two indirect probes to avoid the known int_to_string(INT64_MIN) negation-overflow bug (`#475`): (a) assert bool_to_string(Int.0 < 0) == "true"; and (b) assert int_to_string(Int.0 + 1) == "-9223372036854775807". Include an inline "#475" comment. Implemented in commit 3cf5a13 (PR `#511`).

Learnt from: aallan
Repo: aallan/vera PR: 510
File: vera/README.md:94-94
Timestamp: 2026-04-23T10:21:57.603Z
Learning: In `vera/README.md`, the `wasm/` package line count appears in two places: the module table (e.g. line 94) and the "Code Generation" section prose (e.g. line 486, phrase "wasm/ (N lines across 18 modules)"). Both must be kept in sync whenever `wasm/` grows. aallan noted in PR `#510` that ideally a validation script would derive both from a single source (similar to `check_conformance.py`). When reviewing PRs that add files to `vera/wasm/`, flag if only one of the two occurrences is updated.

Learnt from: aallan
Repo: aallan/vera PR: 536
File: TESTING.md:11-12
Timestamp: 2026-04-27T12:42:16.228Z
Learning: In `aallan/vera` `TESTING.md`, the phrase "a collection of N small, focused programs" (around the Conformance Suite intro, ~line 86) is **not** currently gated by `scripts/check_doc_counts.py`. That script only gates on the bare count `80`/`81` appearing in 4 known structured locations in TESTING.md. The ungated prose phrasing is tracked for inclusion in issue `#528`'s gating expansion. When reviewing PRs that bump the conformance count, manually check that this intro sentence is also updated, as it will not be caught by `check_doc_counts.py` until `#528` is resolved.

Learnt from: aallan
Repo: aallan/vera PR: 350
File: tests/test_codegen.py:8630-8633
Timestamp: 2026-03-25T16:32:11.316Z
Learning: Repo aallan/vera PR `#350`: tests/test_codegen.py includes explicit WAT import‑gating coverage for JSON host imports — tests test_json_parse_wat_import, test_json_stringify_wat_import, and test_json_no_imports_when_unused verify that "vera.json_parse"/"vera.json_stringify" imports are emitted only when referenced and absent when unused.

Learnt from: aallan
Repo: aallan/vera PR: 511
File: tests/test_codegen.py:12294-12682
Timestamp: 2026-04-23T13:18:52.667Z
Learning: Repo aallan/vera PR `#511`: In tests/test_codegen.py::TestJsonTypedAccessors, accessor import-gating is now covered by:
- test_accessors_do_not_force_json_imports and test_layer2_accessors_do_not_force_json_imports (ensure json_as_*/json_get_* are pure prelude and do NOT emit '(import "vera" "json_parse")' or '(import "vera" "json_stringify")' in WAT),
- test_json_parse_does_force_its_host_import (ensures json_parse DOES emit its host import).
This prevents false positives about JSON imports when only typed accessors are used.

Learnt from: aallan
Repo: aallan/vera PR: 511
File: tests/test_browser.py:1272-1379
Timestamp: 2026-04-23T13:38:11.484Z
Learning: Repo aallan/vera: When a test in tests/test_browser.py or tests/test_codegen.py needs to verify that json_as_int returns Some(INT64_MIN) for the input -2^63 (i.e. JNumber(0.0 - 9223372036854775808.0)), do NOT call int_to_string(Int.0) directly — there is a pre-existing bug (`#475`, queued for v0.0.120) where int_to_string(INT64_MIN) triggers a negation overflow inside the WAT code. The correct workaround is to probe the value indirectly, e.g. via `bool_to_string(Int.0 < 0)`, and assert the result is "true". Add an inline test-body comment documenting the `#475` bug to prevent future reviewers from replacing the workaround with a direct print.

Learnt from: aallan
Repo: aallan/vera PR: 526
File: scripts/build_site.py:0-0
Timestamp: 2026-04-24T20:41:50.975Z
Learning: In aallan/vera `scripts/build_site.py` and `docs/index.html`, the VeraBench section currently shows results from **v0.0.7** of the benchmark, at which point the suite had **50 problems**. The current VeraBench suite has **60 problems**. When reviewing any prose referencing the VeraBench problem count, check which benchmark release is cited (`Results from VeraBench vX.Y.Z`): the problem count must match the suite size at *that release*, not the current size. As of the fix in PR `#526` commit cac8c3c, all four occurrences (two in `docs/index.html`, two in the `build_index_md` f-string) correctly say 50.

Learnt from: aallan
Repo: aallan/vera PR: 533
File: tests/test_runtime_traps.py:177-195
Timestamp: 2026-04-27T09:34:32.100Z
Learning: In `tests/test_runtime_traps.py` (aallan/vera PR `#533`): `test_text_mode_cross_stream_ordering` uses `redirect_stdout`/`redirect_stderr` into a single `io.StringIO` to assert Python-level write order (IO.print lines before "Error:") in `cmd_run`'s `WasmTrapError` handler. This does NOT cover the OS-level buffering concern that `sys.stdout.flush()` defends against — `io.StringIO.flush()` is a no-op. The OS-level ordering invariant (relevant under shell `2>&1`) would require a subprocess test with a real pipe/TTY and is explicitly deferred. Do not conflate the two concerns in future reviews.

Learnt from: aallan
Repo: aallan/vera PR: 533
File: tests/test_runtime_traps.py:195-263
Timestamp: 2026-04-27T09:26:35.192Z
Learning: In `tests/test_runtime_traps.py` (vera/aallan), all JSON-mode cmd_run tests must assert `captured.err == ""` after `capsys.readouterr()`. This pins the contract that JSON-mode output is self-contained in the envelope and no human-readable `Error: ...` text leaks to real stderr. The implementation guard is in `vera/cli.py`'s `WasmTrapError` handler (~lines 645–664): stream writes are conditioned on `if as_json: ... return`, so stray stderr writes are structurally prevented, but the assertion acts as a regression net. Affected tests: test_json_mode_includes_stdout_in_envelope, test_json_mode_includes_stderr_in_envelope, test_json_mode_includes_trap_kind, test_json_contract_violation_kind.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
HISTORY.md (1)

260-260: ⚠️ Potential issue | 🟡 Minor

Trim the v0.0.123 HISTORY row to an outcome-only summary.

Line 260 is carrying implementation/process narrative (Discovered while ...) and exceeds the repository’s HISTORY row convention.

✂️ Proposed tightening
-| v0.0.123 | 27 Apr | **`IO.print` writes mirror live to `sys.stdout`** ([`#543`](https://github.com/aallan/vera/issues/543)) — `vera run` text mode now flushes per call, so animations, progress bars, REPL-style output, and any program using ANSI cursor / clear-screen escapes render in real time instead of dumping the whole transcript at exit. Tee preserves the in-memory capture, so trap preservation (`#522`) and JSON-envelope packaging still work. Discovered while watching the v0.0.122 GC fix run Conway end-to-end and seeing only the final frame. |
+| v0.0.123 | 27 Apr | **`IO.print` writes mirror live to `sys.stdout`** ([`#543`](https://github.com/aallan/vera/issues/543)) — `vera run` text mode now flushes per call, so animations, progress bars, REPL-style output, and ANSI cursor / clear-screen escapes render in real time instead of dumping output at exit; tee preserves in-memory capture so trap preservation (`#522`) and JSON-envelope packaging still work. |

Based on learnings: In HISTORY.md for aallan/vera, “HISTORY file is single-line summaries. Detail is for the CHANGELOG.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@HISTORY.md` at line 260, The HISTORY.md entry for "v0.0.123" is too verbose
and includes implementation/process narrative; update the single-line row that
starts with "| v0.0.123 | 27 Apr | **`IO.print` writes mirror live to
`sys.stdout`** ..." to a concise outcome-only summary (e.g. "`IO.print` now
flushes live to stdout so animations/progress render in real time; tee still
preserves capture") removing the "Discovered while..." sentence and any extra
context so the row conforms to the single-line HISTORY convention.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TESTING.md`:
- Line 82: The table row describing test_runtime_traps.py is outdated: update
the phrase "all four JSON-emitting tests" to reflect the new count of five
JSON-mode tests; locate the row referencing `test_runtime_traps.py` (the long
description mentioning `trap_kind`, captured `stdout`, captured `stderr`, and
JSON-mode invariants) in TESTING.md and change "four" to "five" so the Markdown
accurately matches the updated `tests/test_runtime_traps.py`.

---

Duplicate comments:
In `@HISTORY.md`:
- Line 260: The HISTORY.md entry for "v0.0.123" is too verbose and includes
implementation/process narrative; update the single-line row that starts with "|
v0.0.123 | 27 Apr | **`IO.print` writes mirror live to `sys.stdout`** ..." to a
concise outcome-only summary (e.g. "`IO.print` now flushes live to stdout so
animations/progress render in real time; tee still preserves capture") removing
the "Discovered while..." sentence and any extra context so the row conforms to
the single-line HISTORY convention.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 55bbf33a-681a-4974-94b2-31fec06e6224

📥 Commits

Reviewing files that changed from the base of the PR and between fae9515 and 510135a.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (3)
  • HISTORY.md
  • TESTING.md
  • tests/test_runtime_traps.py

Comment thread TESTING.md Outdated
Two small follow-ups from the second CodeRabbit pass:

* TESTING.md test_runtime_traps.py row: "all four JSON-emitting
  tests" -> "all five JSON-emitting tests".  Stale after round 1
  added test_json_mode_does_not_tee_to_stdout (which made it five
  JSON-mode tests across the file: four in TestStdoutOnTrap522 +
  one in TestStdoutTee543).

* HISTORY.md v0.0.123 row: removed the "Discovered while watching..."
  trailing sentence per CodeRabbit.  The first two sentences are
  outcome-focused (what changed, what's preserved); the discovery
  narrative was process not outcome.  Other rows in the table
  follow the same outcome-only convention.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan aallan merged commit c62ee44 into main Apr 27, 2026
20 checks passed
@aallan aallan deleted the v0.0.123-io-print-tee branch April 27, 2026 17:56
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.

IO.print buffered until exit — animations and TUIs invisible

1 participant