From 62cba63a3ddfd0799733edb0a39e8d612bfb1966 Mon Sep 17 00:00:00 2001 From: "Documenter.jl" Date: Thu, 19 Sep 2024 07:31:17 +0000 Subject: [PATCH] build based on 68e96c6 --- previews/PR117/.documenter-siteinfo.json | 2 +- previews/PR117/api/index.html | 16 ++++++++-------- previews/PR117/edges/index.html | 2 +- previews/PR117/index.html | 2 +- previews/PR117/signatures/index.html | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/previews/PR117/.documenter-siteinfo.json b/previews/PR117/.documenter-siteinfo.json index ad45021..ead63c6 100644 --- a/previews/PR117/.documenter-siteinfo.json +++ b/previews/PR117/.documenter-siteinfo.json @@ -1 +1 @@ -{"documenter":{"julia_version":"1.10.5","generation_timestamp":"2024-09-11T04:50:20","documenter_version":"1.7.0"}} \ No newline at end of file +{"documenter":{"julia_version":"1.10.5","generation_timestamp":"2024-09-19T07:31:13","documenter_version":"1.7.0"}} \ No newline at end of file diff --git a/previews/PR117/api/index.html b/previews/PR117/api/index.html index efbfa90..5b138bd 100644 --- a/previews/PR117/api/index.html +++ b/previews/PR117/api/index.html @@ -3,12 +3,12 @@ sig1 = Core.svec(typeof(f), AbstractArray{T}) sig2 = Core.svec(T) sigsv = Core.svec(sig1, sig2) -sig = signature(sigsv)source
sigt, lastpc = signature(recurse, frame, pc)
-sigt, lastpc = signature(frame, pc)

Compute the signature-type sigt of a method whose definition in frame starts at pc. Generally, pc should point to the Expr(:method, methname) statement, in which case lastpc is the final statement number in frame that is part of the signature (i.e, the line above the 3-argument :method expression). Alternatively, pc can point to the 3-argument :method expression, as long as all the relevant SSAValues have been assigned. In this case, lastpc == pc.

If no 3-argument :method expression is found, sigt will be nothing.

source
LoweredCodeUtils.methoddef!Function
ret = methoddef!(recurse, signatures, frame; define=true)
-ret = methoddef!(signatures, frame; define=true)

Compute the signature of a method definition. frame.pc should point to a :method expression. Upon exit, the new signature will be added to signatures.

There are several possible return values:

pc, pc3 = ret

is the typical return. pc will point to the next statement to be executed, or be nothing if there are no further statements in frame. pc3 will point to the 3-argument :method expression.

Alternatively,

pc = ret

occurs for "empty method" expressions, e.g., :(function foo end). pc will be nothing.

By default the method will be defined (evaluated). You can prevent this by setting define=false. This is recommended if you are simply extracting signatures from code that has already been evaluated.

source
LoweredCodeUtils.rename_framemethods!Function
methranges = rename_framemethods!(frame)
-methranges = rename_framemethods!(recurse, frame)

Rename the gensymmed methods in frame to match those that are currently active. The issues are described in https://github.com/JuliaLang/julia/issues/30908. frame will be modified in-place as needed.

Returns a vector of name=>start:stop pairs specifying the range of lines in frame at which method definitions occur. In some cases there may be more than one method with the same name in the start:stop range.

source
LoweredCodeUtils.bodymethodFunction
mbody = bodymethod(m::Method)

Return the "body method" for a method m. mbody contains the code of the function body when m was defined.

source

Edges

LoweredCodeUtils.CodeEdgesType
edges = CodeEdges(src::CodeInfo)

Analyze src and determine the chain of dependencies.

  • edges.preds[i] lists the preceding statements that statement i depends on.
  • edges.succs[i] lists the succeeding statements that depend on statement i.
  • edges.byname[v] returns information about the predecessors, successors, and assignment statements for an object v::GlobalRef.
source
LoweredCodeUtils.lines_requiredFunction
isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController])
-isrequired = lines_required(idx::Int,       src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController])

Determine which lines might need to be executed to evaluate obj or the statement indexed by idx. If isrequired[i] is false, the ith statement is not required. In some circumstances all statements marked true may be needed, in others control-flow will end up skipping a subset of such statements, perhaps while repeating others multiple times.

See also lines_required! and selective_eval!.

source
LoweredCodeUtils.lines_required!Function
lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
+sig = signature(sigsv)
source
sigt, lastpc = signature(recurse, frame, pc)
+sigt, lastpc = signature(frame, pc)

Compute the signature-type sigt of a method whose definition in frame starts at pc. Generally, pc should point to the Expr(:method, methname) statement, in which case lastpc is the final statement number in frame that is part of the signature (i.e, the line above the 3-argument :method expression). Alternatively, pc can point to the 3-argument :method expression, as long as all the relevant SSAValues have been assigned. In this case, lastpc == pc.

If no 3-argument :method expression is found, sigt will be nothing.

source
LoweredCodeUtils.methoddef!Function
ret = methoddef!(recurse, signatures, frame; define=true)
+ret = methoddef!(signatures, frame; define=true)

Compute the signature of a method definition. frame.pc should point to a :method expression. Upon exit, the new signature will be added to signatures.

There are several possible return values:

pc, pc3 = ret

is the typical return. pc will point to the next statement to be executed, or be nothing if there are no further statements in frame. pc3 will point to the 3-argument :method expression.

Alternatively,

pc = ret

occurs for "empty method" expressions, e.g., :(function foo end). pc will be nothing.

By default the method will be defined (evaluated). You can prevent this by setting define=false. This is recommended if you are simply extracting signatures from code that has already been evaluated.

source
LoweredCodeUtils.rename_framemethods!Function
methranges = rename_framemethods!(frame)
+methranges = rename_framemethods!(recurse, frame)

Rename the gensymmed methods in frame to match those that are currently active. The issues are described in https://github.com/JuliaLang/julia/issues/30908. frame will be modified in-place as needed.

Returns a vector of name=>start:stop pairs specifying the range of lines in frame at which method definitions occur. In some cases there may be more than one method with the same name in the start:stop range.

source
LoweredCodeUtils.bodymethodFunction
mbody = bodymethod(m::Method)

Return the "body method" for a method m. mbody contains the code of the function body when m was defined.

source

Edges

LoweredCodeUtils.CodeEdgesType
edges = CodeEdges(src::CodeInfo)

Analyze src and determine the chain of dependencies.

  • edges.preds[i] lists the preceding statements that statement i depends on.
  • edges.succs[i] lists the succeeding statements that depend on statement i.
  • edges.byname[v] returns information about the predecessors, successors, and assignment statements for an object v::GlobalRef.
source
LoweredCodeUtils.lines_requiredFunction
isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController])
+isrequired = lines_required(idx::Int,       src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController])

Determine which lines might need to be executed to evaluate obj or the statement indexed by idx. If isrequired[i] is false, the ith statement is not required. In some circumstances all statements marked true may be needed, in others control-flow will end up skipping a subset of such statements, perhaps while repeating others multiple times.

See also lines_required! and selective_eval!.

source
LoweredCodeUtils.lines_required!Function
lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
                 [controller::SelectiveEvalController];
-                norequire = ())

Like lines_required, but where isrequired[idx] has already been set to true for all statements that you know you need to evaluate. All other statements should be marked false at entry. On return, the complete set of required statements will be marked true.

norequire keyword argument specifies statements (represented as iterator of Ints) that should not be marked as a requirement. For example, use norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges) if you're extracting method signatures and not evaluating new definitions.

source
LoweredCodeUtils.selective_eval!Function
selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)

Execute the code in frame in the manner of JuliaInterpreter.finish_and_return!, but skipping all statements that are marked false in isrequired. See lines_required. Upon entry, if needed the caller must ensure that frame.pc is set to the correct statement, typically findfirst(isrequired). See selective_eval_fromstart! to have that performed automatically.

The default value for recurse is JuliaInterpreter.finish_and_return!. isrequired pertains only to frame itself, not any of its callees.

When recurse::SelectiveEvalController is specified, the selective evaluation execution becomes fully correct. Conversely, with the default finish_and_return!, selective evaluation may not be necessarily correct for all possible Julia code (see https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).

Ensure that the specified controller is properly synchronized with isrequired. Additionally note that, at present, it is not possible to recurse the controller. In other words, there is no system in place for interprocedural selective evaluation.

This will return either a BreakpointRef, the value obtained from the last executed statement (if stored to frame.framedata.ssavlues), or nothing. Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.

source
LoweredCodeUtils.selective_eval_fromstart!Function
selective_eval_fromstart!([recurse], frame, isrequired, istoplevel=false)

Like selective_eval!, except it sets frame.pc to the first true statement in isrequired.

source
LoweredCodeUtils.SelectiveEvalControllerType
controller::SelectiveEvalController

When this object is passed as the recurse argument of selective_eval!, the selective execution is adjusted as follows:

  • Implicit return: In Julia's IR representation (CodeInfo), the final block does not necessarily return and may goto another block. And if the return statement is not included in the slice in such cases, it is necessary to terminate selective_eval! when execution reaches such implicit return statements. controller.implicit_returns records the PCs of such return statements, and selective_eval! will return when reaching those statements.

  • CFG short-cut: When the successors of a conditional branch are inactive, and it is safe to move the program counter from the conditional branch to the nearest common post-dominator of those successors, this short-cut is taken. This short-cut is not merely an optimization but is actually essential for the correctness of the selective execution. This is because, in CodeInfo, even if we simply fall-through dead blocks (i.e., increment the program counter without executing the statements of those blocks), it does not necessarily lead to the nearest common post-dominator block.

These adjustments are necessary for performing selective execution correctly. lines_required or lines_required! will update the SelectiveEvalController passed as an argument to be appropriate for the program slice generated.

source

Internal utilities

LoweredCodeUtils.print_with_codeFunction
print_with_code(io, src::CodeInfo, cl::CodeLinks)

Interweave display of code and links.

Julia 1.6

This function produces dummy output if suitable support is missing in your version of Julia.

source
print_with_code(io, src::CodeInfo, edges::CodeEdges)

Interweave display of code and edges.

Julia 1.6

This function produces dummy output if suitable support is missing in your version of Julia.

source
print_with_code(io, src::CodeInfo, isrequired::AbstractVector{Bool})

Mark each line of code with its requirement status.

Julia 1.6

This function produces dummy output if suitable support is missing in your version of Julia.

source
LoweredCodeUtils.next_or_nothingFunction
nextpc = next_or_nothing([recurse], frame, pc)
-nextpc = next_or_nothing!([recurse], frame)

Advance the program counter without executing the corresponding line. If frame is finished, nextpc will be nothing.

source
LoweredCodeUtils.skip_untilFunction
nextpc = skip_until(predicate, [recurse], frame, pc)
-nextpc = skip_until!(predicate, [recurse], frame)

Advance the program counter until predicate(stmt) return true.

source
LoweredCodeUtils.MethodInfoType
MethodInfo(start, stop, refs)

Given a frame and its CodeInfo, start is the line of the first Expr(:method, name), whereas stop is the line of the last Expr(:method, name, sig, src) expression for name. refs is a vector of line numbers of other references. Some of these will be the location of the "declaration" of a method, the :thunk expression containing a CodeInfo that just returns a 1-argument :method expression. Others may be :global declarations.

In some cases there may be more than one method with the same name in the start:stop range.

source
LoweredCodeUtils.identify_framemethod_callsFunction
methodinfos, selfcalls = identify_framemethod_calls(frame)

Analyze the code in frame to locate method definitions and "self-calls," i.e., calls to methods defined in frame that occur within frame.

methodinfos is a Dict of name=>info pairs, where info is a MethodInfo.

selfcalls is a list of SelfCall(linetop, linebody, callee, caller) that holds the location of calls the methods defined in frame. linetop is the line in frame (top meaning "top level"), which will correspond to a 3-argument :method expression containing a CodeInfo body. linebody is the line within the CodeInfo body from which the call is made. callee is the Symbol of the called method.

source
LoweredCodeUtils.iscalltoFunction
iscallto(stmt, name, src)

Returns true is stmt is a call expression to name.

source
LoweredCodeUtils.getcalleeFunction
getcallee(stmt)

Returns the function (or Symbol) being called in a :call expression.

source
LoweredCodeUtils.find_name_caller_sigFunction
pctop, isgen = find_name_caller_sig(recurse, frame, pc, name, parentname)

Scans forward from pc in frame until a method is found that calls name. pctop points to the beginning of that method's signature. isgen is true if name corresponds to sa GeneratedFunctionStub.

Alternatively, this returns nothing if pc does not appear to point to either a keyword or generated method.

source
LoweredCodeUtils.replacename!Function
replacename!(stmts, oldname=>newname)

Replace a Symbol oldname with newname in stmts.

source
LoweredCodeUtils.VariableType

Variable holds information about named variables. Unlike SSAValues, a single Variable can be assigned from multiple code locations.

If v is a Variable, then

  • v.assigned is a list of statement numbers on which it is assigned
  • v.preds is the set of statement numbers upon which this assignment depends
  • v.succs is the set of statement numbers which make use of this variable

preds and succs are short for "predecessors" and "successors," respectively. These are meant in the sense of execution order, not statement number; depending on control-flow, a variable may have entries in preds that are larger than the smallest entry in assigned.

source
+ norequire = ())

Like lines_required, but where isrequired[idx] has already been set to true for all statements that you know you need to evaluate. All other statements should be marked false at entry. On return, the complete set of required statements will be marked true.

norequire keyword argument specifies statements (represented as iterator of Ints) that should not be marked as a requirement. For example, use norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges) if you're extracting method signatures and not evaluating new definitions.

source
LoweredCodeUtils.selective_eval!Function
selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)

Execute the code in frame in the manner of JuliaInterpreter.finish_and_return!, but skipping all statements that are marked false in isrequired. See lines_required. Upon entry, if needed the caller must ensure that frame.pc is set to the correct statement, typically findfirst(isrequired). See selective_eval_fromstart! to have that performed automatically.

The default value for recurse is JuliaInterpreter.finish_and_return!. isrequired pertains only to frame itself, not any of its callees.

When recurse::SelectiveEvalController is specified, the selective evaluation execution becomes fully correct. Conversely, with the default finish_and_return!, selective evaluation may not be necessarily correct for all possible Julia code (see https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).

Ensure that the specified controller is properly synchronized with isrequired. Additionally note that, at present, it is not possible to recurse the controller. In other words, there is no system in place for interprocedural selective evaluation.

This will return either a BreakpointRef, the value obtained from the last executed statement (if stored to frame.framedata.ssavlues), or nothing. Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.

source
LoweredCodeUtils.selective_eval_fromstart!Function
selective_eval_fromstart!([recurse], frame, isrequired, istoplevel=false)

Like selective_eval!, except it sets frame.pc to the first true statement in isrequired.

source
LoweredCodeUtils.SelectiveEvalControllerType
controller::SelectiveEvalController

When this object is passed as the recurse argument of selective_eval!, the selective execution is adjusted as follows:

  • Implicit return: In Julia's IR representation (CodeInfo), the final block does not necessarily return and may goto another block. And if the return statement is not included in the slice in such cases, it is necessary to terminate selective_eval! when execution reaches such implicit return statements. controller.implicit_returns records the PCs of such return statements, and selective_eval! will return when reaching those statements.

  • CFG short-cut: When the successors of a conditional branch are inactive, and it is safe to move the program counter from the conditional branch to the nearest common post-dominator of those successors, this short-cut is taken. This short-cut is not merely an optimization but is actually essential for the correctness of the selective execution. This is because, in CodeInfo, even if we simply fall-through dead blocks (i.e., increment the program counter without executing the statements of those blocks), it does not necessarily lead to the nearest common post-dominator block.

These adjustments are necessary for performing selective execution correctly. lines_required or lines_required! will update the SelectiveEvalController passed as an argument to be appropriate for the program slice generated.

source

Internal utilities

LoweredCodeUtils.print_with_codeFunction
print_with_code(io, src::CodeInfo, cl::CodeLinks)

Interweave display of code and links.

Julia 1.6

This function produces dummy output if suitable support is missing in your version of Julia.

source
print_with_code(io, src::CodeInfo, edges::CodeEdges)

Interweave display of code and edges.

Julia 1.6

This function produces dummy output if suitable support is missing in your version of Julia.

source
print_with_code(io, src::CodeInfo, isrequired::AbstractVector{Bool})

Mark each line of code with its requirement status.

Julia 1.6

This function produces dummy output if suitable support is missing in your version of Julia.

source
LoweredCodeUtils.next_or_nothingFunction
nextpc = next_or_nothing([recurse], frame, pc)
+nextpc = next_or_nothing!([recurse], frame)

Advance the program counter without executing the corresponding line. If frame is finished, nextpc will be nothing.

source
LoweredCodeUtils.skip_untilFunction
nextpc = skip_until(predicate, [recurse], frame, pc)
+nextpc = skip_until!(predicate, [recurse], frame)

Advance the program counter until predicate(stmt) return true.

source
LoweredCodeUtils.MethodInfoType
MethodInfo(start, stop, refs)

Given a frame and its CodeInfo, start is the line of the first Expr(:method, name), whereas stop is the line of the last Expr(:method, name, sig, src) expression for name. refs is a vector of line numbers of other references. Some of these will be the location of the "declaration" of a method, the :thunk expression containing a CodeInfo that just returns a 1-argument :method expression. Others may be :global declarations.

In some cases there may be more than one method with the same name in the start:stop range.

source
LoweredCodeUtils.identify_framemethod_callsFunction
methodinfos, selfcalls = identify_framemethod_calls(frame)

Analyze the code in frame to locate method definitions and "self-calls," i.e., calls to methods defined in frame that occur within frame.

methodinfos is a Dict of name=>info pairs, where info is a MethodInfo.

selfcalls is a list of SelfCall(linetop, linebody, callee, caller) that holds the location of calls the methods defined in frame. linetop is the line in frame (top meaning "top level"), which will correspond to a 3-argument :method expression containing a CodeInfo body. linebody is the line within the CodeInfo body from which the call is made. callee is the Symbol of the called method.

source
LoweredCodeUtils.iscalltoFunction
iscallto(stmt, name, src)

Returns true is stmt is a call expression to name.

source
LoweredCodeUtils.getcalleeFunction
getcallee(stmt)

Returns the function (or Symbol) being called in a :call expression.

source
LoweredCodeUtils.find_name_caller_sigFunction
pctop, isgen = find_name_caller_sig(recurse, frame, pc, name, parentname)

Scans forward from pc in frame until a method is found that calls name. pctop points to the beginning of that method's signature. isgen is true if name corresponds to sa GeneratedFunctionStub.

Alternatively, this returns nothing if pc does not appear to point to either a keyword or generated method.

source
LoweredCodeUtils.replacename!Function
replacename!(stmts, oldname=>newname)

Replace a Symbol oldname with newname in stmts.

source
LoweredCodeUtils.VariableType

Variable holds information about named variables. Unlike SSAValues, a single Variable can be assigned from multiple code locations.

If v is a Variable, then

  • v.assigned is a list of statement numbers on which it is assigned
  • v.preds is the set of statement numbers upon which this assignment depends
  • v.succs is the set of statement numbers which make use of this variable

preds and succs are short for "predecessors" and "successors," respectively. These are meant in the sense of execution order, not statement number; depending on control-flow, a variable may have entries in preds that are larger than the smallest entry in assigned.

source
diff --git a/previews/PR117/edges/index.html b/previews/PR117/edges/index.html index 13f5181..6140b23 100644 --- a/previews/PR117/edges/index.html +++ b/previews/PR117/edges/index.html @@ -219,4 +219,4 @@ 0 julia> s -9 # random

You can see that k was not reset to its value of 11 when we ran this code selectively, but that s was updated (to a random value) each time.

+9 # random

You can see that k was not reset to its value of 11 when we ran this code selectively, but that s was updated (to a random value) each time.

diff --git a/previews/PR117/index.html b/previews/PR117/index.html index ad13ee7..ba1ede5 100644 --- a/previews/PR117/index.html +++ b/previews/PR117/index.html @@ -1,2 +1,2 @@ -Home · LoweredCodeUtils

LoweredCodeUtils.jl

This package performs operations on Julia's lowered AST. An introduction to this representation can be found at JuliaInterpreter.

Lowered AST (like other ASTs, type-inferred AST and SSA IR form) is generally more amenable to analysis than "surface" Julia expressions. However, sophisticated analyses can nevertheless require a fair bit of infrastructure. The purpose of this package is to standardize a few operations that are important in some applications.

Currently there are two major domains of this package: the "signatures" domain and the "edges" domain.

Signatures

A major role of this package is to support extraction of method signatures, in particular to provide strong support for relating keyword-method "bodies" to their parent methods. The central challenge this addresses is the lowering of keyword-argument functions and the fact that the "gensymmed" names are different each time you lower the code, and therefore you don't recover the actual (running) keyword-body method. The technical details are described in this Julia issue and on the next page. This package provides a workaround to rename gensymmed variables in newly-lowered code to match the name of the running keyword-body method, and provides a convenience function, bodymethod, to obtain that otherwise difficult-to-discover method.

Edges

Sometimes you want to run only a selected subset of code. For instance, Revise tracks methods by their signatures, and therefore needs to compute signatures from the lowered representation of code. Doing this robustly (including for @evaled methods, etc.) requires running module top-level code through the interpreter. For reasons of performance and safety, it is important to minimize the amount of code that gets executed when extracting the signature.

This package provides a general framework for computing dependencies in code, through the CodeEdges constructor. It allows you to determine the lines on which any given statement depends, the lines which "consume" the result of the current line, and any "named" dependencies (Symbol and GlobalRef dependencies). In particular, this resolves the line-dependencies of all SlotNumber variables so that their own dependencies will be handled via the code-line dependencies.

+Home · LoweredCodeUtils

LoweredCodeUtils.jl

This package performs operations on Julia's lowered AST. An introduction to this representation can be found at JuliaInterpreter.

Lowered AST (like other ASTs, type-inferred AST and SSA IR form) is generally more amenable to analysis than "surface" Julia expressions. However, sophisticated analyses can nevertheless require a fair bit of infrastructure. The purpose of this package is to standardize a few operations that are important in some applications.

Currently there are two major domains of this package: the "signatures" domain and the "edges" domain.

Signatures

A major role of this package is to support extraction of method signatures, in particular to provide strong support for relating keyword-method "bodies" to their parent methods. The central challenge this addresses is the lowering of keyword-argument functions and the fact that the "gensymmed" names are different each time you lower the code, and therefore you don't recover the actual (running) keyword-body method. The technical details are described in this Julia issue and on the next page. This package provides a workaround to rename gensymmed variables in newly-lowered code to match the name of the running keyword-body method, and provides a convenience function, bodymethod, to obtain that otherwise difficult-to-discover method.

Edges

Sometimes you want to run only a selected subset of code. For instance, Revise tracks methods by their signatures, and therefore needs to compute signatures from the lowered representation of code. Doing this robustly (including for @evaled methods, etc.) requires running module top-level code through the interpreter. For reasons of performance and safety, it is important to minimize the amount of code that gets executed when extracting the signature.

This package provides a general framework for computing dependencies in code, through the CodeEdges constructor. It allows you to determine the lines on which any given statement depends, the lines which "consume" the result of the current line, and any "named" dependencies (Symbol and GlobalRef dependencies). In particular, this resolves the line-dependencies of all SlotNumber variables so that their own dependencies will be handled via the code-line dependencies.

diff --git a/previews/PR117/signatures/index.html b/previews/PR117/signatures/index.html index c8b1677..2e390cf 100644 --- a/previews/PR117/signatures/index.html +++ b/previews/PR117/signatures/index.html @@ -177,4 +177,4 @@ │ f │ ($(QuoteNode(ifelse)))(false, false, %J25) └── return %J26 -)

While there are a few differences in representation stemming from converting it to a frame, you can see that the #f#2s have been changed to #f#1s to match the currently-running names.

+)

While there are a few differences in representation stemming from converting it to a frame, you can see that the #f#2s have been changed to #f#1s to match the currently-running names.