Skip to content

feat(arxjit): add clear errors when source extraction is unavailable#98

Merged
xmnlab merged 3 commits into
arxlang:mainfrom
Jaskirat-s7:feat/arxjit-extraction-errors
Jul 20, 2026
Merged

feat(arxjit): add clear errors when source extraction is unavailable#98
xmnlab merged 3 commits into
arxlang:mainfrom
Jaskirat-s7:feat/arxjit-extraction-errors

Conversation

@Jaskirat-s7

Copy link
Copy Markdown
Contributor

Summary

Sprint 2, source extraction part 2 — the follow-up promised in #96, completing the last bullet of wiki "Proposed Issue 3":

  • Provide clear errors when source extraction is unavailable.

With this PR, every extraction failure raises SourceExtractionError carrying one located, structured Diagnostic (contract from #95) instead of leaking a raw stdlib exception:

failure previously now
exec-/REPL-defined function (OSError) raw OSError SourceExtractionError, filename from co_filename (e.g. "<string>")
C builtin, no code object (TypeError) raw TypeError SourceExtractionError, filename "<unknown>"
self-referential __wrapped__ cycle (ValueError) raw ValueError SourceExtractionError; filename attribution falls back to the wrapper's own file
block that fails ast.parse (SyntaxError) raw SyntaxError SourceExtractionError with the error mapped to the real file line and column
not a single function definition (e.g. lambda) already SourceExtractionError (#96) unchanged

Details worth reviewing

  • SyntaxError location mapping goes through the synthetic wrapper. feat(arxjit): add source extraction for decorated functions #96 parses indented definitions inside "if True:\n" + block, which shifts SyntaxError.lineno by one relative to the block; the translation uses the same line_delta as the success path, so the diagnostic points at the real file line either way. There's a dedicated test for the indented case (test_indented_unparsable_source_maps_to_file_lines).
  • Column contract: SyntaxError.offset is already a one-based character column (probed on CPython 3.10/3.11/3.13/3.14), and since feat(arxjit): add source extraction for decorated functions #96 parses the text with its original indentation it points into the real file line — so it's stored directly, satisfying the Diagnostic.column contract from feat(arxjit): add structured diagnostics and the public error hierarchy #95. A zero offset (no usable column) is normalized to None.
  • __wrapped__ cycles: inspect.unwrap inside _filename_of can raise ValueError on a self-referential wrapper; it now falls back to attributing the wrapper's own file and lets the source retrieval report the cycle, which is then translated like the other retrieval failures — so the documented "failures raise SourceExtractionError" contract has no gaps.
  • Original exceptions are kept as __cause__ (raise ... from exc) for debugging.

Verification

  • 5 new tests (52 total: builtin, exec-defined, __wrapped__ cycle, unparsable block, unparsable indented block), arxjit coverage 100%.
  • All gates green locally: pytest + the 86% coverage gate, ruff format/check, mypy strict, bandit, vulture, mccabe, and douki sync idempotent.

Refs: #96 (extraction pipeline), #95 (diagnostics/error layer). The normalized-source follow-up remains tracked in #97.

Complete the extraction stage's error contract: every failure now raises
SourceExtractionError with a located diagnostic. Unretrievable source
(exec- or REPL-defined functions, C builtins, self-referential wrappers)
and unparsable source blocks are translated at the inspect/ast boundary.
SyntaxError locations are mapped to real file lines, including through
the synthetic wrapper used for indented definitions, and the offset is
used as the one-based character column the Diagnostic contract requires
(a zero offset is normalized to None). A __wrapped__ cycle during
filename attribution falls back to the wrapper's own file instead of
letting ValueError escape.
@xmnlab

xmnlab commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

thanks for working on that @Jaskirat-s7

I found two compatibility gaps in the new error-translation contract.

  1. Medium — ast.parse() can still leak a raw ValueError

The parse wrapper catches only SyntaxError.

On Python 3.10, 3.11, and 3.12, ast.parse() is documented to raise ValueError when the retrieved source contains a null character. That means a corrupted or subsequently modified source file can still bypass SourceExtractionError, contradicting the new “every failure” guarantee.

A regression test could make getsourcelines() return:

[
"def broken():\n",
" return '\0'\n",
]

On those Python versions, the raw ValueError escapes.

Suggested fix: catch ValueError around ast.parse() and translate it to SourceExtractionError, generally without line or column information because ValueError does not provide structured syntax-location attributes.

Location: packages/arxjit/src/arxjit/source.py:178–193

  1. Medium — SyntaxError.offset is not always a real-file column

The implementation stores exc.offset directly under the assumption that it always points into the original source line.

Python explicitly documents an exception for malformed f-string replacement fields: their offsets may refer to a synthetic string constructed from the replacement expression rather than the original file line. For example, an error in f"prefix {a b}" can carry an offset into text such as (a b)\n. Storing that offset directly produces a misleading diagnostic column.

A cross-version regression test should cover a malformed f-string, especially on Python 3.10 and 3.11. A safe fallback is to set column=None when exc.text does not match the corresponding retrieved source line; alternatively, implement explicit f-string location recovery.

Location: packages/arxjit/src/arxjit/source.py:185–189

Summary:

The general design is good: retrieval exceptions are consistently chained, the synthetic-wrapper line adjustment is correct for ordinary syntax errors, and the added tests are focused. All three workflows currently pass.

I would address these two cases before merging because the PR’s primary promise is that all extraction failures become correctly located SourceExtractionError instances.

…ation

Address both review findings on arxlang#98:

1. ast.parse raises ValueError instead of SyntaxError for source
   containing null bytes on older Python versions (observed on 3.10;
   documented for 3.10-3.12). The parse wrapper now translates ValueError
   to SourceExtractionError as well, without location information since
   ValueError carries no syntax-location attributes.
2. SyntaxError.offset is not always a real-file column: for malformed
   f-string replacement fields, Python 3.10/3.11 report the offset into a
   synthetic string built from the replacement expression. The diagnostic
   column is now produced by _column_of, which trusts the offset only
   when SyntaxError.text matches the parsed line it claims to come from
   and falls back to None otherwise; newer versions report real file
   locations and keep their column.

The parse-failure handling moved into a _parse_block helper, keeping
extract_source below the mccabe threshold. Regression tests: the
null-byte block, a deterministic ValueError-translation test (stubbed
parser, since only 3.10 triggers it naturally), a cross-version
malformed-f-string test (strict expectations on locally verified
versions, either-way on the unverified 3.12 transition), and direct
branch tests of _column_of with synthetic SyntaxError instances.
@Jaskirat-s7

Copy link
Copy Markdown
Contributor Author

Thanks for catching these @xmnlab! Both gaps are closed in new commit:

  1. Raw ValueError from ast.parse , the parse wrapper (now a small _parse_block helper) catches ValueError and translates it to SourceExtractionError with no line/column, exactly as you suggested, since it carries no location attributes. I probed the matrix versions locally: the null-byte ValueError actually fires only on 3.10 (3.11+ raise SyntaxError with the same message), so alongside your suggested null-byte regression test there's a stubbed-parser test that exercises the translation deterministically on every version.

  2. SyntaxError.offset on malformed f-strings , implemented your safe fallback as a _column_of validator: the offset is trusted only when SyntaxError.text matches the parsed line it claims to come from, otherwise the column is None. Verified locally: on 3.10/3.11 the synthetic '(a b)\n' text mismatches and the column correctly drops to None (the line still points at the real file line); on 3.13/3.14 the text matches and the real column is kept. I went with the fallback rather than explicit f-string location recovery since the v1 subset will reject f-strings in the validation pass anyway , happy to revisit if you'd prefer recovery. Tests cover the cross-version f-string case plus every _column_of branch directly with synthetic SyntaxErrors, so behavior is pinned regardless of which errors each parser version produces.

56 tests, coverage still 100%. Ready for another look!

@xmnlab

xmnlab commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

thanks for addressing the previous issues @Jaskirat-s7

Just one comment from my side:

Medium — source retrieval can still leak tokenizer exceptions

The retrieval wrapper catches only:

except (OSError, TypeError, ValueError)

However, inspect.getsourcelines() tokenizes the source block internally. CPython’s inspect.getblock() calls tokenize.generate_tokens() but does not catch tokenize.TokenError; it can also re-raise some SyntaxError instances.

A concrete case:

  1. Import a module containing a valid function.

  2. Modify its source file afterward so the function contains an unterminated triple-quoted string.

  3. Clear linecache.

  4. Call extract_source(function).

inspect.getsourcelines() raises a raw tokenize.TokenError("EOF in multi-line string", ...), bypassing the new SourceExtractionError contract.

I recommend translating tokenize.TokenError and retrieval-time SyntaxError in the same block. A TokenError includes a (line, column) tuple, but returning no location would also be acceptable unless its offset is carefully mapped to the original file.

Summary

Changes requested. The two original compatibility gaps are fixed well, but the retrieval-time tokenizer leak still contradicts this PR’s central guarantee that every extraction failure becomes a SourceExtractionError.

inspect.getsourcelines tokenizes the source file, so a corrupted or
since-modified source (for example an unterminated triple-quoted string)
can surface a raw tokenize.TokenError, which is not a SyntaxError
subclass and escaped the retrieval except clause, bypassing the PR's
guarantee that every extraction failure becomes a SourceExtractionError.

Add tokenize.TokenError and SyntaxError to the retrieval except clause
and translate both like every other retrieval failure. No file location
is attached because retrieval fails before the block's start line is
known, so mapping the error's position to the original file is not
possible. A _retrieval_reason helper keeps only the human-readable
message from each exception type (TokenError's (message, position) tuple
reduces to its message; SyntaxError drops its embedded line-number
suffix), so no misleading position leaks into the diagnostic text.

Tests: a real end-to-end reproduction (import a module, corrupt its file,
clear linecache, extract) plus deterministic stubs for the TokenError and
retrieval-time SyntaxError branches and a direct test of _retrieval_reason.
@Jaskirat-s7

Copy link
Copy Markdown
Contributor Author

Thanks @xmnlab , good catch, this is a real leak. Fixed in new commit:

inspect.getsourcelines tokenizes the file, and tokenize.TokenError isn't a SyntaxError subclass, so it slipped past the retrieval except. I reproduced your exact scenario (import a module, corrupt its file with an unterminated triple-quote, clear linecache, extract) on 3.10/3.11/3.13/3.14 — all four leak TokenError("EOF in multi-line string", ...). Now both tokenize.TokenError and retrieval-time SyntaxError are translated in the same except block, exactly as you suggested. I went with no location: the failure happens before getsourcelines returns, so block_start doesn't exist yet and the error's (line, col) genuinely can't be mapped to the file , your "returning no location would also be acceptable" applies directly.

One extra thing I tightened while in there: I made sure the message text never embeds a location either. str(TokenError) would leak the raw (message, position) tuple and str(SyntaxError) appends its own block-relative line number, so a small helper now keeps only the clean human message from each. Tests: your real corrupted-file reproduction, plus deterministic stubs for each branch, all verified to fail without the fix. 60 tests, coverage still 100%.

@xmnlab
xmnlab merged commit 0cc3c12 into arxlang:main Jul 20, 2026
37 of 39 checks passed
@xmnlab

xmnlab commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

thanks for working on that @Jaskirat-s7 ! appreciate it

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