Skip to content

Conversation

@aviatesk
Copy link
Member

Align the recurse argument to something like the base Compiler's AbstractInterpreter and make JuliaInterpreter routines overloadable properly.

This change is quite breaking (thus bumping the minor version of this package), but necessary to enhance the customizability of JI. For example, it will make it easier to add changes like #682 in a nicer way, but also should enable better designs in packages such as Revise and JET.

aviatesk added a commit to JuliaDebug/LoweredCodeUtils.jl that referenced this pull request Apr 21, 2025
This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.
`selective_eval!` could now be rewritten as follows using the new
interface, that change is not made in this commit for minimizing the
diff:
```diff
diff --git a/src/codeedges.jl b/src/codeedges.jl
index 3cf2a17..5eba604 100644
--- a/src/codeedges.jl
+++ b/src/codeedges.jl
@@ -1021,6 +1021,33 @@ function add_inplace!(isrequired, src, edges, norequire)
     return changed
 end

+struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
+    inner::S
+    isrequired::T
+end
+function JuliaInterpreter.step_expr!(interp::SelectiveInterpreter, frame::Frame, istoplevel::Bool)
+    pc = frame.pc
+    if interp.isrequired[pc]
+        step_expr!(interp.inner, frame::Frame, istoplevel::Bool)
+    else
+        next_or_nothing!(interp, frame)
+    end
+end
+function JuliaInterpreter.get_return(interp::SelectiveInterpreter, frame::Frame)
+    pc = frame.pc
+    node = pc_expr(frame, pc)
+    if is_return(node)
+        if interp.isrequired[pc]
+            return lookup_return(frame, node)
+        end
+    else
+        if isassigned(frame.framedata.ssavalues, pc)
+            return frame.framedata.ssavalues[pcexec]
+        end
+    end
+    return nothing
+end
+
 """
     selective_eval!([interp::Interpreter=RecursiveInterpreter()], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)

@@ -1037,27 +1064,10 @@ This will return either a `BreakpointRef`, the value obtained from the last exec
 Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
 """
 function selective_eval!(interp::Interpreter, frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
-    pc = pcexec = pclast = frame.pc
-    while isa(pc, Int)
-        frame.pc = pc
-        pclast = pcexec::Int
-        if isrequired[pc]
-            pcexec = pc = step_expr!(interp, frame, istoplevel)
-        else
-            pc = next_or_nothing!(interp, frame)
-        end
-    end
-    isa(pc, BreakpointRef) && return pc
-    pcexec = (pcexec === nothing ? pclast : pcexec)::Int
-    frame.pc = pcexec
-    node = pc_expr(frame)
-    is_return(node) && return isrequired[pcexec] ? lookup_return(frame, node) : nothing
-    isassigned(frame.framedata.ssavalues, pcexec) && return frame.framedata.ssavalues[pcexec]
-    return nothing
+    return JuliaInterpreter.finish_and_return!(SelectiveInterpreter(interp, isrequired), frame, istoplevel)
 end
-function selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
+selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false) =
     selective_eval!(RecursiveInterpreter(), frame, isrequired, istoplevel)
-end

 """
     selective_eval_fromstart!([interp::Interpreter=RecursiveInterpreter()], frame, isrequired, istoplevel=false)
```
@aviatesk aviatesk force-pushed the avi/interp-interface branch 2 times, most recently from d26f140 to 8a12ee5 Compare April 21, 2025 16:05
@aviatesk
Copy link
Member Author

If I have one concern about this PR, which is that the change from the old argument type declaration @nospecialize(recurse) to interp::Interpreter in this PR might lead to different code specialization, potentially impacting Revise's latency.

However it might be positive, as interp::Interpreter should allow for simpler code specialization than @nospecialize(recurse). And now that package precompilation is possible, interp::Interpreter might actually achieve better latency.

aviatesk added a commit to timholy/Revise.jl that referenced this pull request Apr 21, 2025
This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.

In practice, `methods_by_execution!` is quite complex, and using the
`Interpreter` interface may not allow us to simplify its implementation.

That said, this commit seems to achieve a modest latency improvement by
changing the argument type declaration from `@nospecialize(recurse)` to
`interp::Interpreter` (with easier code specialization).
@aviatesk
Copy link
Member Author

I tried using Revise on Example.jl with timholy/Revise.jl#911 locally, and it seems like it have slightly improved the latency. I'm not sure how to accurately measure the latency of the First-Time-To-Revise, so it could be just a margin of error.

Align the `recurse` argument to something like the base Compiler's
`AbstractInterpreter` and make JuliaInterpreter routines overloadable
properly.

This change is quite breaking (thus bumping the minor version of this
package), but necessary to enhance the customizability of JI.
For example, it will make it easier to add changes like
#682 in a nicer way, but also should
enable better designs in packages such as Revise and JET.
@aviatesk aviatesk force-pushed the avi/interp-interface branch from b6d255c to eaf72f0 Compare April 22, 2025 12:07
aviatesk added a commit to JuliaDebug/LoweredCodeUtils.jl that referenced this pull request Apr 22, 2025
This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.
`selective_eval!` could now be rewritten as follows using the new
interface, that change is not made in this commit for minimizing the
diff:
```diff
diff --git a/src/codeedges.jl b/src/codeedges.jl
index 3cf2a17..5eba604 100644
--- a/src/codeedges.jl
+++ b/src/codeedges.jl
@@ -1021,6 +1021,33 @@ function add_inplace!(isrequired, src, edges, norequire)
     return changed
 end

+struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
+    inner::S
+    isrequired::T
+end
+function JuliaInterpreter.step_expr!(interp::SelectiveInterpreter, frame::Frame, istoplevel::Bool)
+    pc = frame.pc
+    if interp.isrequired[pc]
+        step_expr!(interp.inner, frame::Frame, istoplevel::Bool)
+    else
+        next_or_nothing!(interp, frame)
+    end
+end
+function JuliaInterpreter.get_return(interp::SelectiveInterpreter, frame::Frame)
+    pc = frame.pc
+    node = pc_expr(frame, pc)
+    if is_return(node)
+        if interp.isrequired[pc]
+            return lookup_return(frame, node)
+        end
+    else
+        if isassigned(frame.framedata.ssavalues, pc)
+            return frame.framedata.ssavalues[pcexec]
+        end
+    end
+    return nothing
+end
+
 """
     selective_eval!([interp::Interpreter=RecursiveInterpreter()], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)

@@ -1037,27 +1064,10 @@ This will return either a `BreakpointRef`, the value obtained from the last exec
 Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
 """
 function selective_eval!(interp::Interpreter, frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
-    pc = pcexec = pclast = frame.pc
-    while isa(pc, Int)
-        frame.pc = pc
-        pclast = pcexec::Int
-        if isrequired[pc]
-            pcexec = pc = step_expr!(interp, frame, istoplevel)
-        else
-            pc = next_or_nothing!(interp, frame)
-        end
-    end
-    isa(pc, BreakpointRef) && return pc
-    pcexec = (pcexec === nothing ? pclast : pcexec)::Int
-    frame.pc = pcexec
-    node = pc_expr(frame)
-    is_return(node) && return isrequired[pcexec] ? lookup_return(frame, node) : nothing
-    isassigned(frame.framedata.ssavalues, pcexec) && return frame.framedata.ssavalues[pcexec]
-    return nothing
+    return JuliaInterpreter.finish_and_return!(SelectiveInterpreter(interp, isrequired), frame, istoplevel)
 end
-function selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
+selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false) =
     selective_eval!(RecursiveInterpreter(), frame, isrequired, istoplevel)
-end

 """
     selective_eval_fromstart!([interp::Interpreter=RecursiveInterpreter()], frame, isrequired, istoplevel=false)
```
aviatesk added a commit to JuliaDebug/LoweredCodeUtils.jl that referenced this pull request Apr 22, 2025
This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.
`selective_eval!` could now be rewritten as follows using the new
interface, that change is not made in this commit for minimizing the
diff:
```diff
diff --git a/src/codeedges.jl b/src/codeedges.jl
index 3cf2a17..5eba604 100644
--- a/src/codeedges.jl
+++ b/src/codeedges.jl
@@ -1021,6 +1021,33 @@ function add_inplace!(isrequired, src, edges, norequire)
     return changed
 end

+struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
+    inner::S
+    isrequired::T
+end
+function JuliaInterpreter.step_expr!(interp::SelectiveInterpreter, frame::Frame, istoplevel::Bool)
+    pc = frame.pc
+    if interp.isrequired[pc]
+        step_expr!(interp.inner, frame::Frame, istoplevel::Bool)
+    else
+        next_or_nothing!(interp, frame)
+    end
+end
+function JuliaInterpreter.get_return(interp::SelectiveInterpreter, frame::Frame)
+    pc = frame.pc
+    node = pc_expr(frame, pc)
+    if is_return(node)
+        if interp.isrequired[pc]
+            return lookup_return(frame, node)
+        end
+    else
+        if isassigned(frame.framedata.ssavalues, pc)
+            return frame.framedata.ssavalues[pcexec]
+        end
+    end
+    return nothing
+end
+
 """
     selective_eval!([interp::Interpreter=RecursiveInterpreter()], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)

@@ -1037,27 +1064,10 @@ This will return either a `BreakpointRef`, the value obtained from the last exec
 Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
 """
 function selective_eval!(interp::Interpreter, frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
-    pc = pcexec = pclast = frame.pc
-    while isa(pc, Int)
-        frame.pc = pc
-        pclast = pcexec::Int
-        if isrequired[pc]
-            pcexec = pc = step_expr!(interp, frame, istoplevel)
-        else
-            pc = next_or_nothing!(interp, frame)
-        end
-    end
-    isa(pc, BreakpointRef) && return pc
-    pcexec = (pcexec === nothing ? pclast : pcexec)::Int
-    frame.pc = pcexec
-    node = pc_expr(frame)
-    is_return(node) && return isrequired[pcexec] ? lookup_return(frame, node) : nothing
-    isassigned(frame.framedata.ssavalues, pcexec) && return frame.framedata.ssavalues[pcexec]
-    return nothing
+    return JuliaInterpreter.finish_and_return!(SelectiveInterpreter(interp, isrequired), frame, istoplevel)
 end
-function selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
+selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false) =
     selective_eval!(RecursiveInterpreter(), frame, isrequired, istoplevel)
-end

 """
     selective_eval_fromstart!([interp::Interpreter=RecursiveInterpreter()], frame, isrequired, istoplevel=false)
```
@aviatesk
Copy link
Member Author

This PR is pretty big, but I've been using it locally since yesterday and haven't run into any issues. So, if there are no major objections, I plan to merge it tomorrow.

@timholy Sorry to ping you again, but if you have a moment, it would be great to get your thoughts, even just on the general idea.
That said, I'll take responsibility for handling any issues that might come up from this refactor. So, if you don't have specific feedback, please feel free to ignore this.

Regarding the main concern about the impact on Revise's latency, this PR (and the related Revise PR) actually seems to improve latency slightly. So, I think we're okay to move forward with this direction regarding this aspect.

aviatesk added a commit to timholy/Revise.jl that referenced this pull request Apr 22, 2025
This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.

In practice, `methods_by_execution!` is quite complex, and using the
`Interpreter` interface may not allow us to simplify its implementation.

That said, this commit seems to achieve a modest latency improvement by
changing the argument type declaration from `@nospecialize(recurse)` to
`interp::Interpreter` (with easier code specialization).
Copy link
Collaborator

@serenity4 serenity4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very nice refactor! The type annotations are also very welcome.

I made a few (very minor) suggestions, but I think the design is great. There remain uses of Compiled in the codebase, perhaps those should be replaced to NonRecursiveInterpreter so we only ever use Compiled for defining the alias and as export (but never use that name internally)?

@aviatesk
Copy link
Member Author

There remain uses of Compiled in the codebase, perhaps those should be replaced to NonRecursiveInterpreter so we only ever use Compiled for defining the alias and as export (but never use that name internally)?

I reviewed the places where Compiled is being used throughout the codebase and replaced it with NonRecursiveInterpreter where appropriate.
I found that in files like localmethtable.jl and optimize.jl, Compiled is used more like a token rather than an actual interpreter. For these situations, it seems better to use a separate token type that isn't a subtype of Interpreter.
I plan to do this refactoring later, so for now, I've left the usage of Compiled unchanged in those specific places.

@aviatesk aviatesk force-pushed the avi/interp-interface branch from 7f708ce to 82e9948 Compare April 23, 2025 09:25
@aviatesk
Copy link
Member Author

@serenity4 Thanks for your reviews! Very much appreciated.

@aviatesk aviatesk merged commit a3ed4cc into master Apr 23, 2025
10 checks passed
@aviatesk aviatesk deleted the avi/interp-interface branch April 23, 2025 09:44
aviatesk referenced this pull request Apr 23, 2025
aviatesk added a commit to JuliaDebug/LoweredCodeUtils.jl that referenced this pull request Apr 23, 2025
* update to JuliaInterpreter 0.10

This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.
`selective_eval!` could now be rewritten as follows using the new
interface, that change is not made in this commit for minimizing the
diff:
```diff
diff --git a/src/codeedges.jl b/src/codeedges.jl
index 3cf2a17..5eba604 100644
--- a/src/codeedges.jl
+++ b/src/codeedges.jl
@@ -1021,6 +1021,33 @@ function add_inplace!(isrequired, src, edges, norequire)
     return changed
 end

+struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
+    inner::S
+    isrequired::T
+end
+function JuliaInterpreter.step_expr!(interp::SelectiveInterpreter, frame::Frame, istoplevel::Bool)
+    pc = frame.pc
+    if interp.isrequired[pc]
+        step_expr!(interp.inner, frame::Frame, istoplevel::Bool)
+    else
+        next_or_nothing!(interp, frame)
+    end
+end
+function JuliaInterpreter.get_return(interp::SelectiveInterpreter, frame::Frame)
+    pc = frame.pc
+    node = pc_expr(frame, pc)
+    if is_return(node)
+        if interp.isrequired[pc]
+            return lookup_return(frame, node)
+        end
+    else
+        if isassigned(frame.framedata.ssavalues, pc)
+            return frame.framedata.ssavalues[pcexec]
+        end
+    end
+    return nothing
+end
+
 """
     selective_eval!([interp::Interpreter=RecursiveInterpreter()], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)

@@ -1037,27 +1064,10 @@ This will return either a `BreakpointRef`, the value obtained from the last exec
 Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
 """
 function selective_eval!(interp::Interpreter, frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
-    pc = pcexec = pclast = frame.pc
-    while isa(pc, Int)
-        frame.pc = pc
-        pclast = pcexec::Int
-        if isrequired[pc]
-            pcexec = pc = step_expr!(interp, frame, istoplevel)
-        else
-            pc = next_or_nothing!(interp, frame)
-        end
-    end
-    isa(pc, BreakpointRef) && return pc
-    pcexec = (pcexec === nothing ? pclast : pcexec)::Int
-    frame.pc = pcexec
-    node = pc_expr(frame)
-    is_return(node) && return isrequired[pcexec] ? lookup_return(frame, node) : nothing
-    isassigned(frame.framedata.ssavalues, pcexec) && return frame.framedata.ssavalues[pcexec]
-    return nothing
+    return JuliaInterpreter.finish_and_return!(SelectiveInterpreter(interp, isrequired), frame, istoplevel)
 end
-function selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false)
+selective_eval!(frame::Frame, isrequired::AbstractVector{Bool}, istoplevel::Bool=false) =
     selective_eval!(RecursiveInterpreter(), frame, isrequired, istoplevel)
-end

 """
     selective_eval_fromstart!([interp::Interpreter=RecursiveInterpreter()], frame, isrequired, istoplevel=false)
```

* a bit better `selective_eval`

* rm no longer used `finish_and_return!` import

* propagate `interp` context to `lookup`

* fix docs
aviatesk added a commit to timholy/Revise.jl that referenced this pull request Apr 23, 2025
* update to JuliaInterpreter 0.10

This commit implements the migration to the new JuliaInterpreter
interface proposed in JuliaDebug/JuliaInterpreter.jl#683.

It purely performs the migration to the new interface and does not
include any refactoring based on it.

In practice, `methods_by_execution!` is quite complex, and using the
`Interpreter` interface may not allow us to simplify its implementation.

That said, this commit seems to achieve a modest latency improvement by
changing the argument type declaration from `@nospecialize(recurse)` to
`interp::Interpreter` (with easier code specialization).

* checking in docs/Manifest.toml
@timholy
Copy link
Member

timholy commented Apr 23, 2025

Sorry I missed this before merging! I'm very supportive of this general idea. With the hopes that JuliaLowering will become a thing in the forseeable future, Revise probably won't even need to depend on JuliaInterpreter forever. So things that advance the state of the art in JuliaInterpreter should not be held back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants