Skip to content

Commit 25a86df

Browse files
authored
Merge pull request #831 from aallan/feat/645-explicit-utf8-encoding
feat(tooling): explicit encoding="utf-8" at every text-mode file call + gate (#645)
2 parents 70031fb + d932554 commit 25a86df

51 files changed

Lines changed: 963 additions & 229 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,12 @@ jobs:
2424

2525
name: test (${{ matrix.os }}, ${{ matrix.python-version }})${{ matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest' && ' + coverage' || '' }}
2626
runs-on: ${{ matrix.os }}
27-
env:
28-
# #641: force Python UTF-8 mode (PEP 540) so file I/O without an
29-
# explicit `encoding=` kwarg defaults to UTF-8 on every platform
30-
# rather than the locale's preferred encoding (cp1252 on Windows
31-
# in en-US locales). Without this, tests that read/write source
32-
# files containing `→` (right arrow) or `—` (em-dash) tripped
33-
# `'charmap' codec can't encode` errors on Windows. Locally,
34-
# users without `PYTHONUTF8=1` set in their shell environment
35-
# will still hit the bug on the affected files — the durable
36-
# fix is to audit `open()` / `read_text()` / `write_text()`
37-
# call sites for explicit `encoding='utf-8'`. Tracked as a
38-
# follow-up.
39-
PYTHONUTF8: 1
27+
# No PYTHONUTF8 env: #645 made text I/O UTF-8 without the PEP-540 backstop
28+
# (#641). Every text-mode open()/read_text()/write_text() and every
29+
# subprocess text=True capture passes an explicit encoding="utf-8" (enforced
30+
# by scripts/check_explicit_encoding.py), and the `vera` CLI reconfigures its
31+
# stdout/stderr to UTF-8 at startup — so a Vera program printing `→` / `—` is
32+
# UTF-8 on every runner regardless of locale, cp1252 Windows included.
4033

4134
steps:
4235
- uses: actions/checkout@v7
@@ -158,6 +151,9 @@ jobs:
158151
- name: Check every diagnostic carries rationale/fix/spec_ref (#682)
159152
run: python scripts/check_diagnostic_fields.py
160153

154+
- name: Check text-mode file I/O passes explicit encoding (#645)
155+
run: python scripts/check_explicit_encoding.py
156+
161157
- name: Check site assets are up-to-date
162158
run: python scripts/check_site_assets.py
163159

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ repos:
165165
# citation — include spec markdown so that edit re-runs the check too.
166166
files: '(vera/.*\.py$|spec/.*\.md$|scripts/check_diagnostic_fields\.py$)'
167167

168+
- id: explicit-encoding
169+
name: explicit encoding
170+
entry: .venv/bin/python scripts/check_explicit_encoding.py
171+
language: system
172+
pass_filenames: false
173+
# Auto-discovers every text-mode open()/read_text()/write_text() under
174+
# vera/, scripts/, tests/ (#645), so the trigger must be at least that
175+
# broad (same reasoning as diagnostic-fields / walker-coverage).
176+
files: '((vera|scripts|tests)/.*\.py$)'
177+
168178
- id: limitations-sync
169179
name: limitations sync
170180
entry: .venv/bin/python scripts/check_limitations_sync.py

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
## [0.0.190] - 2026-07-01
10+
11+
### Changed
12+
13+
- **Text I/O is UTF-8 regardless of the host locale, enforced by a gate** ([#645](https://github.com/aallan/vera/issues/645)). Python's text-mode `open()` / `Path.read_text()` / `Path.write_text()` — and `subprocess.run/Popen/check_output(..., text=True)` captures — fall back to `locale.getpreferredencoding()` (cp1252 on en-US Windows) when no `encoding=` is given, so a Vera source / doc / fixture / program output containing `→`, `—`, or any non-ASCII byte failed on a locale-default Windows shell. [#641](https://github.com/aallan/vera/issues/641) papered over CI with a `PYTHONUTF8=1` backstop; this is the durable fix. A new `scripts/check_explicit_encoding.py` AST-audits every text-mode `open()` / `read_text()` / `write_text()` **and** every `subprocess(..., text=True)` capture under `vera/`, `scripts/`, `tests/`, requiring an explicit `encoding="utf-8"` literal (binary / bytes mode skipped; a deliberate exception opts out with `# encoding-exempt: <reason>`), wired into pre-commit and the CI `lint` job. It found and fixed **160 bare file-I/O sites across 16 files** plus **97 subprocess text captures across 18 files** (far more than the ~30 the issue estimated — the suite has grown ~5× since #641). The audit's scope also covers `tempfile.NamedTemporaryFile` / `TemporaryFile` / `SpooledTemporaryFile` opened in *text* mode (they default to binary, but a text one is a locale-encoded write just like `open(..., "w")` — the idiom test helpers use to stage `.vera` source containing `→` / `—`; **29 such sites across 9 files** fixed). The `vera` CLI reconfigures **stdin** as well as stdout/stderr to UTF-8 at startup, so a Vera program's non-ASCII output *and* input (e.g. `IO.read_char` on piped UTF-8) round-trip on any locale. Together these made text I/O locale-independent and the `PYTHONUTF8=1` CI backstop (#641) was **removed** — verified by a clean Windows matrix with the backstop gone (the tempfile and stdin gaps were surfaced by that matrix, not the audit, which is why removing the backstop is its own acceptance test). Pinned by `tests/test_check_explicit_encoding.py` (checker logic, scope-discovery coverage, and a repo-clean assertion). (Folds in the stdio / subprocess work that had briefly been split to [#832](https://github.com/aallan/vera/issues/832).)
14+
915
## [0.0.189] - 2026-07-01
1016

1117
### Changed
@@ -2695,7 +2701,8 @@ Small docs sweep — closes six aging documentation issues in one PR. No code c
26952701
- Grammar: handler body simplified to avoid LALR reduce/reduce conflict
26962702
- `pyproject.toml`: corrected build backend, package discovery, PEP 639 compliance
26972703

2698-
[Unreleased]: https://github.com/aallan/vera/compare/v0.0.189...HEAD
2704+
[Unreleased]: https://github.com/aallan/vera/compare/v0.0.190...HEAD
2705+
[0.0.190]: https://github.com/aallan/vera/compare/v0.0.189...v0.0.190
26992706
[0.0.189]: https://github.com/aallan/vera/compare/v0.0.188...v0.0.189
27002707
[0.0.188]: https://github.com/aallan/vera/compare/v0.0.187...v0.0.188
27012708
[0.0.187]: https://github.com/aallan/vera/compare/v0.0.186...v0.0.187

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ python scripts/check_faq_examples.py # Verify FAQ code blocks parse
6464
python scripts/check_html_examples.py # Verify HTML code blocks parse + check + verify
6565
python scripts/check_doc_builtin_shadowing.py # Verify no doc example redefines a built-in (E151; #819)
6666
python scripts/check_diagnostic_fields.py # Verify every diagnostic carries rationale + spec_ref (+ fix for errors; warnings exempt) or a # diag-fields-exempt reason (#682)
67+
python scripts/check_explicit_encoding.py # Verify every text-mode open()/read_text()/write_text() passes explicit encoding='utf-8' (#645)
6768
python scripts/build_site.py # Regenerate AI-readable site assets (llms.txt, etc.)
6869
python scripts/check_site_assets.py # Verify site assets are up-to-date
6970
python scripts/check_version_sync.py # Verify version consistency
@@ -207,4 +208,4 @@ The CI matrix tests on `{ubuntu-latest, macos-15, macos-26, windows-latest} × {
207208

208209
- `tempfile.NamedTemporaryFile` handed off to a subprocess MUST use `delete=False` + manual `Path.unlink()` (Windows can't reopen a held file).
209210
- Paths embedded into Vera string literals MUST be POSIX-form (`Path(tmp_path).as_posix()`); Windows backslashes trip Vera's `\U` escape grammar.
210-
- `open()` / `read_text()` / `write_text()` should pass `encoding="utf-8"` explicitly; CI sets `PYTHONUTF8=1` as a backstop, but the explicit form is portable to local Windows shells without that variable.
211+
- Text I/O MUST pass `encoding="utf-8"` explicitly, enforced by `scripts/check_explicit_encoding.py` (pre-commit + CI lint, #645): every text-mode `open()` / `read_text()` / `write_text()` **and** every `subprocess.run/Popen/check_output(..., text=True)` capture. A deliberate non-UTF-8 site opts out with `# encoding-exempt: <reason>`. The `vera` CLI also reconfigures its stdout/stderr to UTF-8 at startup, so a Vera program printing `` / `` is UTF-8 on any locale. Together these replaced the `PYTHONUTF8=1` CI backstop (#641), which has been removed — no reliance on the runner's or a local Windows shell's locale.

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ dependencies. CI enforces that `uv.lock` stays current.
8989

9090
### Pre-commit Hooks
9191

92-
Every push is checked by 30 configured hooks across two stages: 28 are configured at the commit stage (run after `pre-commit install`), and 2 (`check-changelog-updated` and `uv-lock-check`, described below) are configured at the push stage (run after `pre-commit install --hook-type pre-push`). Most commit-stage hooks have per-hook `files:` / `types:` filters — the `python` type-check only runs when Python files are staged; `check_readme_examples.py` only runs when `README.md` or Vera sources change, etc. A plain-text commit touching only one markdown file triggers a small subset; a compiler-level commit triggers most of them.
92+
Every push is checked by 31 configured hooks across two stages: 29 are configured at the commit stage (run after `pre-commit install`), and 2 (`check-changelog-updated` and `uv-lock-check`, described below) are configured at the push stage (run after `pre-commit install --hook-type pre-push`). Most commit-stage hooks have per-hook `files:` / `types:` filters — the `python` type-check only runs when Python files are staged; `check_readme_examples.py` only runs when `README.md` or Vera sources change, etc. A plain-text commit touching only one markdown file triggers a small subset; a compiler-level commit triggers most of them.
9393

94-
The **commit-stage** hooks (28, each gated to relevant files) include:
94+
The **commit-stage** hooks (29, each gated to relevant files) include:
9595

9696
- Trailing whitespace and file endings
9797
- YAML/TOML validity

HISTORY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,9 @@ A sustained audit of every place where `vera verify` could prove a postcondition
412412
| v0.0.185 | 28 Jun | Redefining an opaque, verifier-modelled built-in is now a checker error, closing a `verify`/`run` soundness hole ([#815](https://github.com/aallan/vera/issues/815)). |
413413
| v0.0.186 | 29 Jun | `@Nat``@Int` widening is now sound: a value above i64.MAX no longer reinterprets to a negative `@Int` ([#813](https://github.com/aallan/vera/issues/813)). |
414414
| v0.0.187 | 30 Jun | Integer-overflow runtime traps now carry a precise `overflow` trap kind instead of the generic `unreachable` ([#808](https://github.com/aallan/vera/issues/808)). |
415-
| v0.0.188 | 30 Jun | The no-silent-failures discipline reaches the diagnostics layer: every diagnostic is gated for complete, spec-accurate metadata, with 54 checker sites backfilled and 30-plus stale spec references corrected ([#682](https://github.com/aallan/vera/issues/682)). |
416-
| v0.0.189 | 1 Jul | The UTF-8 "safe decode" invariant is centralised into one `safe_utf8_decode` helper, reached only through a shared `_slice_and_decode` helper that the three WASM-memory string readers (which the host imports delegate to) route through, replacing six brittle source-grep tests with a unit test plus three wire-real end-to-end tests ([#592](https://github.com/aallan/vera/issues/592)). |
415+
| v0.0.188 | 30 Jun | Every diagnostic is gated for spec-accurate `rationale` and `spec_ref` metadata, plus a `fix` for errors ([#682](https://github.com/aallan/vera/issues/682)). |
416+
| v0.0.189 | 1 Jul | The UTF-8 "safe decode" invariant is centralised behind one `safe_utf8_decode` helper, replacing six brittle source-grep tests with behavioural coverage ([#592](https://github.com/aallan/vera/issues/592)). |
417+
| v0.0.190 | 1 Jul | Text I/O (files and `subprocess` captures) is UTF-8 regardless of host locale, gated in pre-commit/CI, letting the `PYTHONUTF8` backstop be removed ([#645](https://github.com/aallan/vera/issues/645)). |
417418

418419
---
419420

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ cp /path/to/vera/SKILL.md ~/.claude/skills/vera-language/SKILL.md
215215

216216
## Project status
217217

218-
Vera is in **active development** at v0.0.189 — 1,700+ commits, 189 releases, 5,503 tests, 91% code coverage, 103 conformance programs, 35 examples, and a 13-chapter specification. See **[HISTORY.md](HISTORY.md)** for how the compiler was built.
218+
Vera is in **active development** at v0.0.190 — 1,700+ commits, 190 releases, 5,557 tests, 91% code coverage, 103 conformance programs, 35 examples, and a 13-chapter specification. See **[HISTORY.md](HISTORY.md)** for how the compiler was built.
219219

220220
The reference compiler — parser, AST, type checker, contract verifier (Z3), WASM code generator, module system, browser runtime, and runtime contract insertion — is working. The language specification is in draft across [13 chapters](spec/).
221221

ROADMAP.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Priority lives in this file and nowhere else — issues carry kind and area labe
88

99
## Where we are
1010

11-
5,503 tests, 103 conformance programs, 35 examples, 13 spec chapters.
11+
5,557 tests, 103 conformance programs, 35 examples, 13 spec chapters.
1212

1313
## The roadmap
1414

@@ -20,7 +20,6 @@ The infrastructure that catches the next regression before a user does, plus the
2020

2121
| Issue | What |
2222
|---|---|
23-
| [#645](https://github.com/aallan/vera/issues/645) | Explicit `encoding='utf-8'` at every text-mode file call, with a pre-commit check to hold the line. |
2423
| [#657](https://github.com/aallan/vera/issues/657) | Convert `INVARIANT_DEFENSIVE` sites and audit `PROPAGATE` cleanup (follow-up to the #626 error-handling taxonomy). |
2524
| [#419](https://github.com/aallan/vera/issues/419) | Split `tests/test_codegen.py` (21,093 lines — the largest file in the tree, and the codegen oracle for the mutation sweep) into feature-focused test files. Promoted from Tier 3. |
2625
| [#420](https://github.com/aallan/vera/issues/420) | Split `tests/test_checker.py` (6,347 lines) into phase-focused test files. Promoted from "Not doing now". |

0 commit comments

Comments
 (0)