fixes #1987; show template calls as inlined frames when debugging - #2240
fixes #1987; show template calls as inlined frames when debugging#2240Redict wants to merge 3 commits into
Conversation
|
Instead of |
Araq on nim-lang#2240. Better name: the tag records that a statement list came from expanding something, which is a fact about provenance rather than about templates. `intramodinliner` wants the same shape for inlined proc bodies, and it would have had to either reuse a tag named after templates or add a second one. Mechanical: `tmplbody` -> `comesfrom`, `TmplbodyS` -> `ComesfromS`, `genTmplBodyLLVM` -> `genComesFromLLVM`. Tag value stays 348. Comments that said "the template's symbol" now say "the origin symbol" where the code is not template-specific; `sem.nim`'s re-sem branch keeps the template wording because it genuinely is `semTemplateCall`'s path. Goldens re-recorded for the new spelling. The `.ll` goldens are untouched: the tag name never reaches DWARF. 644/644, llvmdebug 2/2.
e2eeddf to
39d1551
Compare
|
Renamed, pushed as a fourth commit. Better name than mine: it says the statements came from expanding something, without claiming what. Comments that read "the template's symbol" now say "the origin symbol" where the code isn't template-specific. Tag value stays 348. The |
39d1551 to
8931ec4
Compare
Araq on nim-lang#2240. Better name: the tag records that a statement list came from expanding something, which is a fact about provenance rather than about templates. `intramodinliner` wants the same shape for inlined proc bodies, and it would have had to either reuse a tag named after templates or add a second one. Mechanical: `tmplbody` -> `comesfrom`, `TmplbodyS` -> `ComesfromS`, `genTmplBodyLLVM` -> `genComesFromLLVM`. Tag value stays 348. Comments that said "the template's symbol" now say "the origin symbol" where the code is not template-specific; `sem.nim`'s re-sem branch keeps the template wording because it genuinely is `semTemplateCall`'s path. Goldens re-recorded for the new spelling. The `.ll` goldens are untouched: the tag name never reaches DWARF. 644/644, llvmdebug 2/2.
Araq on nim-lang#2240. Better name: the tag records that a statement list came from expanding something, which is a fact about provenance rather than about templates. `intramodinliner` wants the same shape for inlined proc bodies, and it would have had to either reuse a tag named after templates or add a second one. Mechanical: `tmplbody` -> `comesfrom`, `TmplbodyS` -> `ComesfromS`, `genTmplBodyLLVM` -> `genComesFromLLVM`. Tag value stays 348. Comments that said "the template's symbol" now say "the origin symbol" where the code is not template-specific; `sem.nim`'s re-sem branch keeps the template wording because it genuinely is `semTemplateCall`'s path. Goldens re-recorded for the new spelling. The `.ll` goldens are untouched: the tag name never reaches DWARF. 644/644, llvmdebug 2/2.
8931ec4 to
16a0145
Compare
|
Rebased on master, resolving against my own The rebase also surfaced a third pass with the same bug as Worth noting CI then found a fourth: arkham, in the sibling That is five passes across two repos, each found by breakage in a different suite, and all for the same reason: 655/655 and boot pass locally. |
3b4e709 to
16a0145
Compare
nimony is gaining `(comesfrom SYM S*)` in nim-lang/nimony#2240: a transparent wrapper marking statements produced by expanding SYM (a template today), so the debug backend can emit them as a DWARF inlined frame. It opens no scope and has no semantics of its own. Without this every native test aborts with arkham x64n: genStmt2 comesfrom [AssertionDefect] The wrapper's first child is the origin SYMBOL, not a statement, so the four passes that walk statement children generically each step over it before treating the rest as `stmts`: - codegen_x64 genStmt2 / asmStmt, codegen_a64 genStmt2: emit the body, no `enterScope` (unlike `ScopeS` - the locals an expansion declares belong to the enclosing scope). - analyser: the generic `analyseChildren` path would analyse the leading symbol as a symbol *use* and record a read of the expanded routine here. - register_allocator: same, and it would carry that phantom read into the live ranges. Each branch sits behind `when declared(ComesfromS)` so arkham compiles both against a nimony that has the tag and one that does not. Without the guard the two repos deadlock: this PR cannot build until #2240 merges, and #2240's linux-amd64 job cannot pass until this one does. The guards can be dropped once #2240 is in.
16a0145 to
6a0e8e0
Compare
|
Sorry for this late change-of-mind but comesfrom should be a pure line-info carrying node that is the first child of the existing ExprX construct, this should simplify things quite a bit. |
|
A pure line-info leaf is clearly better than a wrapper, and it kills the whole operand-headed problem: nothing has to step over a leading symbol, so the 16 dispatcher branches and the four regressions all go away. #2255 becomes unnecessary. Before I rewrite it, one thing does not fit and I would rather ask than guess.
sitting directly among statements, holding Three readings, and I do not know which you mean:
I lean 2 if the goal is a pure line-info node, and 1 if Worth flagging on cost either way: the tag's value is derived from its row index in Separately, nim-lang/nativenif#95 is now guarded with |
|
There is a better design still. Capturing line information well was a first class criterion when designing NIF. All we need to do here is to forge a special filename that encodes the real filename as well as Then LLVM etc can simply extract the comesfrom information by |
|
This one removes the tag entirely, which takes #2255 with it. I checked the mechanism holds:
Two things a prefix alone does not carry. The synthetic Nesting is the other. Neither is fatal, but both push toward encoding a small record in the filename, so I would rather have your call than my guess. On keeping line information precise: it is an existing requirement, but it is not currently met on this path, and that is the real risk here. Happy to build it either way. Since it is the third shape, I would rather pin the encoding down before re-recording the goldens again. |
|
Instead of Statement-only inling expansion information is not good enough, we want to do this for every template expansion and expression producing templates are very common. This means that any explicit node-based solution would pay a heavy implementation price as expressions are more frequent than statements. |
6a0e8e0 to
c8d262e
Compare
…ging
Stepping over a template call jumps into the template's body in another
file with no frame to attribute it to. This carries the expansion's
provenance to the debug backend, which turns it into DWARF
`DISubprogram` + `inlinedAt`.
The provenance rides in the line-info *filename*, per Araq's design on
the PR. Expanded code gets a forged name:
__crucial\0setElem.0.foo\1foo.nim\115\0[]=.0.system\1system.nim\133\0system.nim
^prefix ^--------- outermost ---------^ ^--------- innermost --------^ ^real
The chain is outermost-first, so its length is the inlining depth. Each
entry carries the expanded routine plus its declaration site, because
that cannot be recovered later: a template decl does not survive into
Leng, and the expanded code's own info points at wherever the body came
from. Everything after the last NUL is the real file, which is what
`realFile()` returns for every consumer that does not care about frames.
A filename cannot otherwise contain a NUL or a `\1`, which is what makes
the encoding unambiguous - `|` would not do, since Nim lets an operator
be named `|`. `nifbuilder.needsEscape` covers `c < ' '`, so both survive
text NIF as `\00` / `\01`, and `bif` writes filenames length-prefixed.
No new tag, so no dispatcher branches: every pass that walks statements
is untouched, and expression-producing templates are covered for free.
That is the advantage over a wrapper node - expressions outnumber
statements, and a node-based marker pays per node.
Cost measured on `system.nim`: the `.s.nif` grows 4.01%, from 63 distinct
forged names covering every expansion in the module. Filenames are
interned, so a chain is paid for once per distinct expansion, not per
token.
Four places had to learn about it:
- `visibilityModule` compares the *real* file. It judges private-field
access by which module code was written in, and a forged name has its
own FileId, so raw id comparison missed and `tfieldvisplugin` broke.
- `reporters.infoToStr` and the other path printers call `realFile`, or
an error message leaks the encoding.
- `(err ...)` subtrees are copied verbatim. Their dot tokens are the
instantiation contexts behind `Trace: instantiation from here`;
re-emitting them duplicated the trace for a template erroring inside
another expansion (`tinvalidrecursion`).
- Dot tokens elsewhere keep their line info. `buildErr` stores the error
contexts there, so dropping it silently lost every trace line.
The forge runs *after* the body is sem-checked, not before: a template
called inside this one has already expanded and forged its own name by
then, so the outer level prepends onto the existing chain and the order
comes out outermost-first, which is what nesting `inlinedAt` needs.
Tests: `tests/llvmdebug`, a golden suite over the `.ll`'s debug metadata
and the first tests for the debug backend. Two cases: a nested expansion
(`setElem` expands `[]=`, so the chain is two deep) and a `var` declared
inside a template, where the `DILocalVariable` scope and its `DILocation`
must agree or the verifier rejects the module. Both verified with
`llvm-as`. `hastur.mode = skip` since the LLVM backend cannot build the
full stdlib yet; run with `hastur tests/llvmdebug`.
Both frames name the template's *declaration* site. A wrapper design can
only reach the body's first statement, which is one line off.
672/672 and `hastur boot`. `tvarargs.nif`, `tresemtype.nif` and
`ttemplate.nif` are re-recorded: toplevel `echo` is a varargs template,
so its expansion now carries a forged name.
c8d262e to
bc2ddb6
Compare
| let li = rawLineInfo(c) | ||
| if li.file.isValid and c.pool != nil: c.pool.filenames[li.file] else: "" | ||
|
|
||
| # ── expansion provenance encoded in the filename ───────────────────────── |
There was a problem hiding this comment.
Move all of this logic to a new module named src/lib/comesfrom.nim
There was a problem hiding this comment.
Done: src/lib/comesfrom.nim. It has no NIF dependency, just string handling, so nifpools and nifcoreparse re-export it and every consumer keeps reaching realFile without a separate import.
Co-authored-by: Andreas Rumpf <araq4k@proton.me>
| origin: SymId; declInfo: NifLineInfo; | ||
| callInfo: NifLineInfo) = | ||
| ## Rewrite the line info of everything `dest` gained from `start` onward so | ||
| ## it records that the code came from expanding `origin` (#1987). |
There was a problem hiding this comment.
What?! Why? Sounds terrible. Only the templates body's first token needs the comesfrom information.
There was a problem hiding this comment.
I tried it. Forging only the body's first token gives zero inlined frames:
!5 = DISubprogram(name: "run2", ...)
!21 = DISubprogram(name: "X60Qini_0_ttewk27gl1", ...)
!49 = DISubprogram(name: "main", ...)
No withTemp subprogram, no inlinedAt anywhere, and tests/llvmdebug drops to 0/1. The forged name does reach the .c.nif, exactly one occurrence on the head, so nothing is losing it in the pipeline.
What breaks is the consumption side. The backend has no notion of a current node to inherit from: setLoc is called from 46 places, each reading info off whatever cursor that lowering happens to hold, and emit stamps currentProc.dbgLoc onto every instruction. An instruction lowered from the third statement of a template body therefore gets that statement's own info. With only the head marked, everything after the first instruction falls back to the enclosing proc and the frame collapses.
So head-only needs the backend to track the active expansion across a subtree walk, which is a frame stack in lengc - the wrapper node again, without the node. Your call which way to go; I did not want to guess a fourth time.
On the cost, since that is the part that sounded terrible: filenames are interned. All of system.nim comes to 63 distinct forged names covering 8897 token references, and the .s.nif grows 4.01%. The re-emit is a single pass over the expansion buffer at expansion time. Per distinct expansion, not per token.
Both review comments are in: the logic moved to src/lib/comesfrom.nim, and I took the else: branch. The committed version of it has else: one space too deep and does not compile - templates.nim(145, 6) Error: invalid indentation - so my commit fixes the indentation on top of yours.
There was a problem hiding this comment.
Make no sense to me, in the codegen trExpr before it recurses it checks for a comes-from line information, pushes it onto a stack or whatever and pops it later, bam, done, the backend has DISubprogram information.
There was a problem hiding this comment.
Doing it that way. The stack in the walker means only the head of an expansion needs a forged name, so the per-token re-emit goes away and most of the size growth should go with it. I'll push once tests/llvmdebug passes again.
…ging
Stepping over a template call jumps into the template's body in another
file with no frame to attribute it to. This carries the expansion's
provenance to the debug backend, which turns it into DWARF
`DISubprogram` + `inlinedAt`.
The provenance rides in the line-info *filename*, per Araq's design on
the PR. Expanded code gets a forged name:
__crucial\0setElem.0.foo\1foo.nim\115\0[]=.0.system\1system.nim\133\0system.nim
^prefix ^--------- outermost ---------^ ^--------- innermost --------^ ^real
The chain is outermost-first, so its length is the inlining depth. Each
entry carries the expanded routine plus its declaration site, because
that cannot be recovered later: a template decl does not survive into
Leng, and the expanded code's own info points at wherever the body came
from. Everything after the last NUL is the real file, which is what
`realFile()` returns for every consumer that does not care about frames.
A filename cannot otherwise contain a NUL or a `\1`, which is what makes
the encoding unambiguous - `|` would not do, since Nim lets an operator
be named `|`. `nifbuilder.needsEscape` covers `c < ' '`, so both survive
text NIF as `\00` / `\01`, and `bif` writes filenames length-prefixed.
No new tag, so no dispatcher branches: every pass that walks statements
is untouched, and expression-producing templates are covered for free.
That is the advantage over a wrapper node - expressions outnumber
statements, and a node-based marker pays per node.
Cost measured on `system.nim`: the `.s.nif` grows 4.01%, from 63 distinct
forged names covering every expansion in the module. Filenames are
interned, so a chain is paid for once per distinct expansion, not per
token.
Four places had to learn about it:
- `visibilityModule` compares the *real* file. It judges private-field
access by which module code was written in, and a forged name has its
own FileId, so raw id comparison missed and `tfieldvisplugin` broke.
- `reporters.infoToStr` and the other path printers call `realFile`, or
an error message leaks the encoding.
- `(err ...)` subtrees are copied verbatim. Their dot tokens are the
instantiation contexts behind `Trace: instantiation from here`;
re-emitting them duplicated the trace for a template erroring inside
another expansion (`tinvalidrecursion`).
- Dot tokens elsewhere keep their line info. `buildErr` stores the error
contexts there, so dropping it silently lost every trace line.
The forge runs *after* the body is sem-checked, not before: a template
called inside this one has already expanded and forged its own name by
then, so the outer level prepends onto the existing chain and the order
comes out outermost-first, which is what nesting `inlinedAt` needs.
Tests: `tests/llvmdebug`, a golden suite over the `.ll`'s debug metadata
and the first tests for the debug backend. Two cases: a nested expansion
(`setElem` expands `[]=`, so the chain is two deep) and a `var` declared
inside a template, where the `DILocalVariable` scope and its `DILocation`
must agree or the verifier rejects the module. Both verified with
`llvm-as`. `hastur.mode = skip` since the LLVM backend cannot build the
full stdlib yet; run with `hastur tests/llvmdebug`.
Both frames name the template's *declaration* site. A wrapper design can
only reach the body's first statement, which is one line off.
672/672 and `hastur boot`. `tvarargs.nif`, `tresemtype.nif` and
`ttemplate.nif` are re-recorded: toplevel `echo` is a varargs template,
so its expansion now carries a forged name.
Stepping over a template call jumps into the template's body in another file, with no frame to attribute it to. This carries the expansion's provenance to the debug backend, which turns it into DWARF
DISubprogram+inlinedAt.Rewritten to the filename encoding you asked for, so the
comesfromtag is gone: no node, no dispatcher branches, and #2255 is no longer needed. Expanded code gets a forged name:The chain is outermost-first, so its length is the inlining depth. Everything after the last NUL is the real file, which is what
realFile()returns for every consumer that does not care about frames.Two things beyond the sketch
Each entry carries
<sym>\1<declfile>\1<declline>, not just the symbol. The declaration site cannot be recovered downstream: a template decl does not survive into Leng, and the expanded code's own info points at wherever the body came from. Without it a frame for a template declared in the test file claimed to live insystem.nim. This also makes the result better than the wrapper design, which could only reach the body's first statement and was one line off.\1rather than|as the field separator, since Nim lets an operator be named|.Result
setElemexpands[]=, so the chain nests two deep, and each frame points at its own declaration.Cost
system.nim's.s.nifgrows 4.01%, from 63 distinct forged names covering every expansion in the module. Filenames are interned, so a chain is paid for once per distinct expansion rather than per token. I had assumed this would be much worse before measuring it.Four things had to learn about the encoding
visibilityModulecompares the real file. It judges private-field access by which module code was written in, and a forged name has its own FileId, so raw id comparison missed andtfieldvispluginbroke.reporters.infoToStrand the other path printers callrealFile, or an error message leaks the encoding to the user.(err ...)subtrees are copied verbatim. Their dot tokens are the instantiation contexts behindTrace: instantiation from here; re-emitting them duplicated the trace for a template erroring inside another expansion (tinvalidrecursion).buildErrstores the error contexts there, so dropping it silently lost every trace line.The forge runs after the body is sem-checked, not before: a template called inside this one has already expanded and forged its own name by then, so the outer level prepends onto the existing chain and the order comes out outermost-first, which is what nesting
inlinedAtneeds.One more constraint worth recording: nimony has no closures, so the pass cannot use captured locals.
hastur bootis what catches that.Tests
tests/llvmdebug, a golden suite over the.ll's debug metadata and the first tests for the debug backend. Two cases: the nested expansion above, and avardeclared inside a template, where theDILocalVariablescope and itsDILocationmust agree or the verifier rejects the module. Both verified withllvm-as.hastur.mode = skipsince the LLVM backend cannot build the full stdlib yet; run withhastur tests/llvmdebug.672/672 and
hastur boot.tvarargs.nif,tresemtype.nifandttemplate.nifare re-recorded: toplevelechois a varargs template, so its expansion now carries a forged name.Not done
intramodinlinerwraps inlined bodies in(scope ...)and has the same missing-inlinedAtbug. It can reuse this encoding as-is, since nothing about it is template-specific.