From 4552b54591eac77f3c4a5fed211cbc5d1c552c4d Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Sun, 1 Nov 2026 13:56:21 -0700 Subject: [PATCH] Give module code a source span --- src/core/IronPython/Compiler/Ast/PythonAst.cs | 20 +++++++---- src/core/IronPython/Compiler/Parser.cs | 2 +- .../suite/modules/system_related/test_sys.py | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/core/IronPython/Compiler/Ast/PythonAst.cs b/src/core/IronPython/Compiler/Ast/PythonAst.cs index e95525361..e809f4113 100644 --- a/src/core/IronPython/Compiler/Ast/PythonAst.cs +++ b/src/core/IronPython/Compiler/Ast/PythonAst.cs @@ -141,6 +141,10 @@ public void ParsingFinished(int[] lineLocations, Statement body, ModuleOptions l _body = body; _lineLocations = lineLocations; _languageFeatures = languageFeatures; + + // As in CPython 3.4, the code starts at its first statement (which gives co_firstlineno) and ends with its body. + int start = Math.Max(body is SuiteStatement { Statements: [var first, ..] } ? first.StartIndex : body.StartIndex, 0); + IndexSpan = new IndexSpan(start, Math.Max(body.EndIndex - start, 0)); } /// @@ -369,7 +373,7 @@ internal MSAst.Expression ReduceWorker() { var end = IndexToLocation(ret.Expression.EndIndex); return Ast.Block( - MakeEntrySequencePoint(ret.Expression.StartIndex), + MakeEntrySequencePoint(), Ast.DebugInfo( _document, start.Line, @@ -385,7 +389,7 @@ internal MSAst.Expression ReduceWorker() { // Add beginning sequence point: the location of the 'call' trace event, as in FunctionDefinition. // Without it, the first statement would take that role, and fire no 'line' event of its own. - block.Add(MakeEntrySequencePoint(_body is SuiteStatement { Statements: [var first, ..] } ? first.StartIndex : _body.StartIndex)); + block.Add(MakeEntrySequencePoint()); AddInitialization(block); string doc = GetDocumentation(_body); @@ -421,10 +425,14 @@ internal MSAst.Expression ReduceWorker() { return body; } - private MSAst.Expression MakeEntrySequencePoint(int index) { - var loc = index >= 0 ? IndexToLocation(index) : new SourceLocation(0, 1, 1); - return AstUtils.AddDebugInfo( - AstUtils.Empty(), _document, new SourceLocation(0, loc.Line, loc.Column), new SourceLocation(0, loc.Line, int.MaxValue)); + /// + /// Makes the sequence point of the 'call' trace event, at the start of the code (see ). + /// + private MSAst.Expression MakeEntrySequencePoint() { + var loc = Start; + return AddDebugInfo( + AstUtils.Empty(), + new SourceSpan(new SourceLocation(0, loc.Line, loc.Column), new SourceLocation(0, loc.Line, int.MaxValue))); } diff --git a/src/core/IronPython/Compiler/Parser.cs b/src/core/IronPython/Compiler/Parser.cs index f2eb69fa7..ab12a834a 100644 --- a/src/core/IronPython/Compiler/Parser.cs +++ b/src/core/IronPython/Compiler/Parser.cs @@ -246,7 +246,7 @@ public PythonAst ParseTopExpression() { // TODO: move from source unit .TrimStart(' ', '\t') _globalParent = new PythonAst(false, _languageFeatures, false, _context); ReturnStatement ret = new ReturnStatement(ParseTestListAsExpression()); - ret.SetLoc(_globalParent, 0, 0); + ret.SetLoc(_globalParent, ret.Expression.IndexSpan); return FinishParsing(ret); } catch (DecoderFallbackException dfe) { throw BadSourceError(dfe); diff --git a/tests/suite/modules/system_related/test_sys.py b/tests/suite/modules/system_related/test_sys.py index fce53209c..7c9fe0eeb 100644 --- a/tests/suite/modules/system_related/test_sys.py +++ b/tests/suite/modules/system_related/test_sys.py @@ -200,6 +200,7 @@ def test_settrace_first_line(self): # The first statement of module code gets a 'line' event of its own, as in a function. # The 'call' event of module code is on the line of its first statement, as in Python 3.4. # CPython changed that later: 3.10 puts it always on line 1, and 3.11 on line 0 + # That line is also the co_firstlineno of module code, which 3.10 and later set to 1. check_module_call_line = is_cli or sys.version_info < (3, 10) cases = [ @@ -208,6 +209,7 @@ def test_settrace_first_line(self): ("'doc'\nx = 1\n", "exec", [('call', 1), ('line', 1), ('line', 2), ('return', 2)]), ("def g(): pass\ng()\n", "exec", [('call', 1), ('line', 1), ('line', 2), ('call', 1), ('line', 1), ('return', 1), ('return', 2)]), ("1 + 2", "eval", [('call', 1), ('line', 1), ('return', 1)]), + ("\n\n(1 +\n 2)", "eval", [('call', 3), ('line', 3), ('return', 3)]), ("x = 1", "single", [('call', 1), ('line', 1), ('return', 1)]), ] @@ -224,6 +226,9 @@ def f(frame, event, arg): expected = [('call', None)] + expected[1:] code = compile(source, '', mode) + if check_module_call_line: + self.assertEqual(code.co_firstlineno, expected[0][1], (source, mode)) + try: sys.settrace(f) if mode == 'eval': @@ -235,6 +240,36 @@ def f(frame, event, arg): self.assertEqual(events, expected, (source, mode)) + def test_settrace_jump_in_module(self): + # A trace function can set f_lineno anywhere within module code, not only on its first line + source = "\n\nout.append(3)\nout.append(4)\n\nout.append(6)\n" + cases = [ + (6, 4, [3, 4, 4, 6]), # backward + (3, 6, [6]), # forward + ] + + orig_sys_trace_func = sys.gettrace() + for jump_from, jump_to, expected in cases: + # TODO: On IronPython, a jump works only in the code compiled first under a given file name, hence a unique name for each case + filename = '' % (jump_from, jump_to) + jumped = False + def f(frame, event, arg): + nonlocal jumped + if frame.f_code.co_filename == filename and event == 'line' and frame.f_lineno == jump_from and not jumped: + jumped = True + frame.f_lineno = jump_to + return f + + out = [] + code = compile(source, filename, 'exec') + try: + sys.settrace(f) + exec(code, {'out': out}) + finally: + sys.settrace(orig_sys_trace_func) + + self.assertEqual(out, expected, (jump_from, jump_to)) + def test_cp30130(self): def f(frame, event, arg): if event == 'exception':