feat(arxjit): add source extraction for decorated functions - #96
Conversation
Add arxjit.source with extract_source, the first pipeline stage: retrieve the decorated function's source with inspect.getsourcelines, normalize indentation with textwrap.dedent, strip the decorator lines, and parse the function definition with ast.parse. The returned ExtractedSource keeps real file line numbers on the parsed node so later validation diagnostics can point into the user's file directly; raw ast col_offset values are left untouched per the documented column contract. A retrieved source that is not a single function definition (for example a lambda) raises SourceExtractionError with a structured diagnostic; translating the remaining failure modes (unretrievable or unparsable source) into clear errors lands in the follow-up PR.
|
thanks @Jaskirat-s7 for working on that PR review: changes requested some coments from my side:
textwrap.dedent calculates indentation across every physical line, including lines inside triple-quoted strings. A nested function can legally contain string lines at column zero: def outer(): For this function, textwrap.dedent removes nothing because second and the closing quotes have no indentation. The subsequent ast.parse() raises IndentationError, even though the original function is valid Python. This directly contradicts the intended support for nested functions and methods. The indentation should be removed using the function definition’s leading prefix rather than the minimum indentation of the entire block, or the block should initially be parsed inside a synthetic wrapper that preserves lexical content. Location: packages/arxjit/src/arxjit/source.py:142
Dedenting a method or nested function shifts all AST column offsets to the left. ast.increment_lineno() restores line numbers only; it does not restore col_offset or end_col_offset. For example: def outer(): The return begins at zero-based column 8 in the file, but the returned AST reports column 4. Converting that byte offset to a one-based character column still produces the wrong location. This conflicts with the diagnostic contract, which says diagnostics point into the user’s source and store one-based character columns. The ExtractedSource documentation currently implies the offsets only require byte-to-character conversion, but the removed indentation must also be accounted for. Location: packages/arxjit/src/arxjit/source.py:142–163
JitFunction uses functools.update_wrapper, so inspect.getsourcelines() unwraps it and successfully retrieves the original function’s source. However, _filename_of() runs against the wrapper itself before inspection and only looks for code. JitFunction has no code, so this returns "". Consequently: @arxjit.jit arxjit.extract_source(add).filename ""That is especially surprising because the module and public function describe extraction of @jit-decorated functions. Unwrapping once with inspect.unwrap() before deriving the filename would keep source retrieval, naming, and filename attribution consistent. Location: packages/arxjit/src/arxjit/source.py:140 |
Address all three review findings on arxlang#96: 1. Replace textwrap.dedent with a synthetic 'if True:' wrapper for indented definitions. dedent computes common indentation across every physical line, including lines inside triple-quoted strings, so a valid nested function with zero-indent string lines failed to parse with IndentationError. The wrapper parses the block verbatim, so string literal content is preserved exactly. 2. Because the source is no longer dedented, every node's col_offset and end_col_offset now point into the real file line; only the documented byte-to-character conversion remains for the diagnostic boundary. ExtractedSource.source is now the verbatim file text from the def line (original indentation preserved) instead of a dedented copy. 3. Unwrap __wrapped__ chains before deriving the filename, matching what inspect.getsourcelines does for source retrieval, so extracting an actual @jit result (JitFunction sets __wrapped__ via update_wrapper) is attributed to the wrapped function's file instead of '<unknown>'. Regression tests added for each finding, including extract_source on a real JitFunction and on a nested function containing a multiline string.
|
Thanks for the detailed review @xmnlab! All three findings are addressed in the new commit:
14 extraction tests now (one regression test per finding), arxjit coverage still 100%, all gates green locally including douki. Ready for another look! |
|
The latest changes address all three previously reported correctness issues, and the added regression tests cover them well. I do not see a code-level blocker to merging this PR. One contract detail should be handled as a follow-up: "ExtractedSource.source" now preserves the original indentation, so nested-function source is not standalone-parseable and should not currently be described as normalized. Please update the PR description to reflect the new behavior, and create a follow-up issue to either introduce a separate normalized source representation or define explicit normalization when the cache-key work is implemented. Approved from my side. |
|
Thanks for the approval @xmnlab! Both follow-ups are done: 1.The PR description is updated to reflect the current behavior: ExtractedSource.source is now described as the verbatim file slice (original indentation preserved, not standalone-parseable for nested functions, not a normalized representation), with the parsed node as the canonical artifact for the later stages. |
* feat(arxjit): add source extraction for decorated functions Add arxjit.source with extract_source, the first pipeline stage: retrieve the decorated function's source with inspect.getsourcelines, normalize indentation with textwrap.dedent, strip the decorator lines, and parse the function definition with ast.parse. The returned ExtractedSource keeps real file line numbers on the parsed node so later validation diagnostics can point into the user's file directly; raw ast col_offset values are left untouched per the documented column contract. A retrieved source that is not a single function definition (for example a lambda) raises SourceExtractionError with a structured diagnostic; translating the remaining failure modes (unretrievable or unparsable source) into clear errors lands in the follow-up PR. * fix(arxjit): preserve lexical content and columns during extraction Address all three review findings on arxlang#96: 1. Replace textwrap.dedent with a synthetic 'if True:' wrapper for indented definitions. dedent computes common indentation across every physical line, including lines inside triple-quoted strings, so a valid nested function with zero-indent string lines failed to parse with IndentationError. The wrapper parses the block verbatim, so string literal content is preserved exactly. 2. Because the source is no longer dedented, every node's col_offset and end_col_offset now point into the real file line; only the documented byte-to-character conversion remains for the diagnostic boundary. ExtractedSource.source is now the verbatim file text from the def line (original indentation preserved) instead of a dedented copy. 3. Unwrap __wrapped__ chains before deriving the filename, matching what inspect.getsourcelines does for source retrieval, so extracting an actual @jit result (JitFunction sets __wrapped__ via update_wrapper) is attributed to the wrapped function's file instead of '<unknown>'. Regression tests added for each finding, including extract_source on a real JitFunction and on a nested function containing a multiline string.
Summary
Sprint 2, source extraction (wiki "Proposed Issue 3: Implement Python function source extraction"). This is the second of the four small Sprint 2 PRs agreed on Discord, building on the diagnostics/error layer merged in #95.
It adds
arxjit.source.extract_source(), the first stage of the@jitpipeline: given the decorated Python function, produce a decorator-free source string and a parsedastfunction node that the upcoming validation pass (and later the Python-AST -> ASTx lowering) can consume.Issue 3 checklist coverage:
inspect.getsourcelines(follows__wrapped__, so an actualJitFunctionresolves to the function it wraps)astmoduleSourceExtractionErrorwith a structuredDiagnostic, because returning anExtractedSourcefor it would be wrong.What it returns
ExtractedSource(frozen dataclass):filename"<unknown>"sourcedefline to the end of the function; decorator lines removed, original indentation preservedlinenodefstatement infilenamenodeast.FunctionDef | ast.AsyncFunctionDefwith an empty decorator listTwo properties worth calling out:
ast.increment_linenoafter the parse). The validation pass can putnode.linenostraight into aDiagnosticwithout any offset bookkeeping.col_offset/end_col_offsetindex into the real file line (as 0-based UTF-8 byte offsets). Per the column contract from feat(arxjit): add structured diagnostics and the public error hierarchy #95, the only remaining step at the diagnostic-producing boundary is the byte-to-character conversion.How stripping works
The block returned by
inspect.getsourcelinesincludes the decorators. If its first line is indented, the block is parsed as"if True:\n" + block— this preserves every byte of the original text (multiline strings with zero-indent lines parse fine, unlike withtextwrap.dedent).FunctionDef.linenoalways points at thedefline (decorators live separately indecorator_list), so the verbatimsourceslice starts there and the node'sdecorator_listis emptied. The result has no decorators by construction rather than by pattern-matching.Design decisions to review
async defextracts successfully (hence theFunctionDef | AsyncFunctionDefunion). Rejecting async is the validation pass's job, so users get a proper "unsupported construct" diagnostic with a location instead of a confusing extraction failure.Diagnostic.codestaysNone— no stable code scheme (AJ001, ...) exists yet; I'd propose introducing one in the validation PR where the per-construct diagnostics land.extract_source/ExtractedSourceare exported fromarxjit— useful for the debug/introspection sprint later; easy to make private if you prefer.PyFuncalias is duplicated fromcore.pyinstead of imported, to avoid acore <-> sourceimport cycle once the decorator wiring PR makescoreimportsource.Verification
col_offset/end_col_offseton a nested function, andextract_sourceon an actual@jitresult.FunctionDef.linenois thedefline under multi-line decorators,increment_linenoshiftsend_linenotoo, and constructs that are valid syntax but unsupported semantics (nonlocal,yield, methods) parse fine standalone and flow through to validation as intended.@jit-decorated function defined in a cell extracts cleanly — decorator stripped,<ipython-input-...>filename (including when extracting theJitFunctionitself), cell-relative line numbers.douki syncidempotent.