UPLC source view: source-mapped UPLC artifact for the VS Code profiler extension - #338
Open
nau wants to merge 12 commits into
Open
UPLC source view: source-mapped UPLC artifact for the VS Code profiler extension#338nau wants to merge 12 commits into
nau wants to merge 12 commits into
Conversation
Design for showing compiled UPLC per source line/definition/function in the Scalus Profiler VS Code extension: functionName in UplcAnnotation, span-aware rendering via paiges zero-width markers, uplc.json artifact in the profile manifest, and a bidirectional side-by-side view in the extension.
…ing V3 lowering Every LoweredValue now captures the name of the user function being lowered (LoweringContext.currentFunction, set by Lowering.lowerLet around lambda-valued bindings) and copies it into the UplcAnnotation of every term it emits. This gives tooling, e.g. the VS Code UPLC source view, a way to group compiled UPLC by source function. It is pure metadata and does not change generated code. The capture happens at LoweredValue construction, not in termInternal: term generation is a separate pass that runs after lowering has finished, so a LoweringContext read there would always see the restored empty name. The name therefore lives in a thread-local, since several lowerings can run at once in one JVM. Binding names are reduced to their source-level form: the owner prefix that linked top-level defs carry and the -<symbolId> suffix that the plugin appends to local bindings are both stripped. Two positional-fill passes in Term now key on the position alone instead of on whole-annotation emptiness, and preserve the rest of the annotation, so a term that knows its function but not its position still gets a position filled in. The UplcAnnotation sites in LoweredValue.scala that were left as-is are the ones outside a LoweredValue: none remain in that file, but ScalusRuntime.scala, simple/BaseSimpleLowering.scala and typegens/* still build terms from helper objects with no value to capture from. Those stay unattributed for now. Adds two MiMa filters: the new trait val adds an abstract accessor to the compiler-internal LoweredValue interface, which has no supported external implementors.
…n fill from it Review follow-up to the previous commit, three findings. The ChooseList/ChooseData spines ended in withPosIfEmpty(pos), a term annotation site the first pass missed because it does not spell UplcAnnotation(. They now use withAnnotationIfEmpty(ann(pos)). Term.withAnnotationIfEmpty still keyed on whole-annotation emptiness. With functionName populated, a subterm that knew its function but not its position read as already annotated, so it was skipped and the recursion into its whole subtree stopped, losing positions that used to be filled. It now keys on the position alone and merges instead of overwriting, matching the two fillEmptyPos passes. ScalusRuntime.initSupportBindings lowered every support-module def with no function scope, so the whole prelude was unattributed. It now goes through Lowering.loweringBinding, the same helper and the same lambda-only gate as user bindings; simpleBindingName/loweringBinding/LocalBindingIdSuffix were widened to private[lowering] for that. A compile of List.single(3).map(_ + 1).length now names 83 of 167 terms (foldLeft, foldRight, length, map), up from none. The 27 remaining UplcAnnotation sites in ScalusRuntime and typegens are all inside ComplexLoweredValue bodies where ann(...) is in scope, contrary to the earlier rationale, so they are converted too. Only the non-V3 simple backend still emits bare annotations.
Generalize the two Term fill passes from ScalusSourcePos to the whole UplcAnnotation, so a back-filled spine node inherits the enclosing function name of the code it operates on, not only its position. fillEmptyAnnotationsBottomUp/fillEmptyAnnotationsTopDown are the new private[scalus] passes; fillEmptyPosBottomUp/fillEmptyPosTopDown keep their signatures and delegate. Both fields are filled independently and an existing one is never overwritten, so Task 1 semantics (position fill decoupled from the function name) are preserved.
Move the Pretty[Term] printer body into a private[scalus] object TermPrinter that takes a (Term, Doc) => Doc decorator, applied to the Doc of every printed node. The given Pretty[Term] keeps sanitizing names exactly once and delegates with an identity decorator, so pretty/show output is byte-identical. prettySanitized assumes an already-sanitized term. Inner Apply nodes of a flattened application chain are not printed individually, so they are not decorated.
…an map Renders a Term to the same text as Term.show plus a table mapping character ranges of that text back to Scala source positions and enclosing function names. Offsets are recovered by passing zero-width paiges markers through the shared pretty-printer, so the rendered UPLC is byte-identical to what every other Scalus output shows. Span node indices are post-order, which keeps them stable when a compiled program is later wrapped in Apply nodes to apply parameters. Serializes to the schemaVersion 1 <key>.uplc.json document the VS Code UPLC source view consumes.
A string constant can contain the renderer's marker characters. The scanner's guard only caught a spoof that arrived after the marker it imitated; a spoof printed before that node was consumed as the real thing, which corrupted the constant's text, broke the render(term).uplc == term.show invariant and left spans that partially overlap – the one thing the view's innermost-span cursor lookup relies on not happening. Verify the stripped text against an unmarked render of the same term and, on a mismatch, return the clean text with no spans. Degrading to a spanless view is recoverable, wrong UPLC text is not. Also pin the ordering of UplcSourceMap.spans in its scaladoc: marker-emission order, children before parent, s not monotonic, so consumers must sort before binary-searching by offset.
…ports ProfileReportWriter.write takes the evaluated term as an optional last parameter. At ProfileLevel.Full, when the term still carries the compiler's source annotations, it writes a <key>.uplc.json source map next to the profile files and indexes it in profile-manifest.json as format "uplc". The ledger evaluator passes the script's unapplied program term (by name, so a CBOR decode only happens when a profile was produced) and ScalusTest does the same for test-side profiling.
…ve its test to JVM The test used java.nio.file from the shared test tree, so Scala.js could not link js/test; it moves to scalus-core/jvm/src/test. The source map is now written only when the run also rendered at least one profile file. A console-only report at ProfileLevel.Full used to write the artifact and, because manifest runs are replaced by (scriptHash, tag, index), rewrite an existing run's file list to the source map alone, hiding the profile files consumers look for.
Replace em dashes with en dashes in the annotation fill-pass comments added on this branch (Compiled.scala, Term.scala). Drop the stale duplicated scaladoc above PlutusScriptEvaluator.renderProfile, which still referenced the removed writeProfileManifest and the pre-refactor behavior.
Replace the two LoweredValue.functionName member filters with documented package-level exclusions for scalus.compiler.sir.lowering, .linking, scalus.compiler.intrinsics and scalus.uplc.builtin.internal - the packages the stability docs already carve out of the compatibility promise. Note the concrete list in README and Claude.md, and mark InteropSurfaceTest as planned (M2), not existing. MIXED packages keep per-symbol filters only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Profiled runs now write a UPLC source map next to the profile reports: the pretty-printed UPLC text plus a span table mapping text ranges to Scala source positions and enclosing function names. The Scalus Profiler VS Code extension (companion PR in scalus-vscode-extension) uses it for a side-by-side, bidirectionally synced Scala/UPLC view.
UplcAnnotation(previously a dead field); the position fill passes now carry the whole annotation.TermPrinter: the pretty-printer body extracted with a per-node decorator hook; default output is byte-identical (verified by a sha256 differential over 1200 terms x 5 render modes).UplcSourceMapRenderer: renders via paiges zero-width markers to recover exact character offsets; the text invariant is total (render(term).uplc == term.show, degrading to zero spans on pathological marker collisions). Spans carry 0-based source ranges, function indices and post-order node indices (stable underApplywrapping, following Aiken's unmerged source-map design).ProfileReportWriterwrites<key>.uplc.jsonand indexes it asformat: "uplc"inprofile-manifest.jsonwhen the profile level is Full and at least one profile file was rendered. Console-only runs stay off disk. Manifest schema stays v1; older extension versions ignore the new entry.scalus.compiler.sir.lowering,.linking,scalus.compiler.intrinsics,scalus.uplc.builtin.internal) are now exempt via documented wildcard filters instead of per-member entries; README and Claude.md state the policy.Design docs
docs/superpowers/specs/2026-07-31-uplc-source-view-design.mddocs/superpowers/plans/2026-07-31-uplc-source-view.mdTesting
sbtn quickgreen after rebase on master (3462 core + 811 + 448 + 22 tests, 0 failures);sbtn mimagreen; JS/Native compile andscalusJS/Test/fastLinkJSverified.FunctionNameAnnotationTest,FillAnnotationsTest,PrettyDecoratedTest,UplcSourceMapRendererTest(14 cases incl. marker-collision spoof),ProfileReportWriterUplcTest(JVM, 5 cases incl. console-only gating).Notes