Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive scopes #51434

Open
simonbyrne opened this issue Sep 23, 2023 · 9 comments
Open

Interactive scopes #51434

simonbyrne opened this issue Sep 23, 2023 · 9 comments
Labels
feature Indicates new feature / enhancement requests multithreading Base.Threads and related functionality REPL Julia's REPL (Read Eval Print Loop)

Comments

@simonbyrne
Copy link
Contributor

simonbyrne commented Sep 23, 2023

Looking at how ScopedValues are used for the logger (#50958) and my proposed use for MPFR precision and rounding (#51362), we've ended up with a pattern where if the ScopedValue isn't defined, we fall back on a global Ref value which can be set globally, defining an accessor like:

something(Base.ScopedValues.get(SCOPED_VALUE), GLOBAL_REF[])

If this is going to be common, it seems like a clunky pattern. One straightforward solution would be to make the default field mutable (which would remove the need for this pattern), but it is perhaps worth considering why this is used.

@vchuravy asked if it was important if we be able to set the precision globally. I mentioned backward compatibility, but the primary rationale is for interactivity: if I want to increase the precision for all my last calculation, I can just call setprecision(BigFloat, 1024), and then re-evaluate the expression (hit up arrow twice, and return). If I didn't have this ability, I would have to wrap everything expression in setprecision(BigFloat, 1024) do .... end, which is fiddly and time consuming.

However, if we had some way to evaluate inside a given scope at the REPL, this might not be necessary: basically what I want is something similar to the contextual module REPL, but for scopes.

@vchuravy
Copy link
Member

The scoped value being constant within a particular scope is an important semantic piece. I would loathe to give that up.

The REPL could manage the scope, but Intentionally didn't expose a setscope! function since it makes it much harder to reason about the extent of a scope.

So REPL could have a push/pop scope stack, but I am not sure that it's worth it.

@simonbyrne
Copy link
Contributor Author

The scoped value being constant within a particular scope is an important semantic piece. I would loathe to give that up.

I agree, though the pattern I outlined above is effectively doing that via other means. I don't think it is something we should encourage.

The REPL could manage the scope, but Intentionally didn't expose a setscope! function since it makes it much harder to reason about the extent of a scope.

You mean a function which changes the scope of the current task? That's sort of what I want: can you expand on what your concerns are? Is it that you want the compiler to be able to assume that scoped values are unchanged outside of with blocks?

What if we had something weaker, e.g. changes to the scope would only be reflected at top-level (similar to how eval works)?
I think that would do what I want?

So REPL could have a push/pop scope stack, but I am not sure that it's worth it.

How would this be easier to reason about?

@brenhinkeller brenhinkeller added multithreading Base.Threads and related functionality feature Indicates new feature / enhancement requests labels Sep 24, 2023
@vchuravy
Copy link
Member

You mean a function which changes the scope of the current task? That's sort of what I want: can you expand on what your concerns are? Is it that you want the compiler to be able to assume that scoped values are unchanged outside of with blocks?

Yeah pretty much. I want to leave the door open for #51352

What if we had something weaker, e.g. changes to the scope would only be reflected at top-level

Maybe? That's what I was thinking with push/pop (in terms of how this work in bash). It would need to be semantically equivalent for "restarting" the repl within a new scope.

I agree, though the pattern I outlined above is effectively doing that via other means. I don't think it is something we should encourage.

Yeah, it's not something we can stop people from doing :) You could also put a Ref in there and mutate it directly.
Then you directly opt out of the const semantics.

@simonbyrne
Copy link
Contributor Author

I could see there also being some advantage to allowing something similar in scripts as well, e.g. setting the logger at the start of a program.

@vchuravy
Copy link
Member

I do see the appeal, but I am unsure about the semantics and the implications.

The overall problem is when to restore the previous scope, what is scope exit.

I honestly don't mind making precision a Ref, and then set precision modifying the ref within the current scope.

You still limit the effect of that operation and that's a boon

@simonbyrne
Copy link
Contributor Author

simonbyrne commented Sep 24, 2023

How about this: make @setscope! a macro that can only be called at top-level?

macro setscope!(exprs...)
    Expr(:toplevel, quote
        ct = Base.current_task()
        ct.scope = Base.ScopedValues.Scope(ct.scope, $(map(esc, exprs)...))
    end)
end

then this would prevent it being called inside a function

julia> function setprec(n)
       @setscope!(Base.MPFR.CURRENT_PRECISION => n)
       end
ERROR: syntax: "toplevel" expression not at top level
Stacktrace:
 [1] top-level scope
   @ REPL[9]:1

We could either then implement setprecision as a macro as well? Or have setprecision implemented via eval:

julia> macro setscope!(exprs...)
           Expr(:toplevel, quote
               ct = Base.current_task()
               ct.scope = Base.ScopedValues.Scope(ct.scope, $(map(esc, exprs)...))
           end)
       end
@setscope! (macro with 1 method)

julia> function setprec_eval(n)
         @eval (@setscope!(Base.MPFR.CURRENT_PRECISION => $n))
       end
setprec_eval (generic function with 1 method)

julia> precision(BigFloat)
256

julia> setprec_eval(128)
Base.ScopedValues.Scope(ScopedValue{Int64}@0x17c8c76e2041a828 => 128)

julia> precision(BigFloat)
128

though I'm not sure this would give the semantics you had in mind.

@vchuravy
Copy link
Member

I don't like the use eval there, but making it a macro also seems odd.

I think my issue is that the lifetime/extent of the ScopedValue is ambiguous, but I guess setscope is probably fine (even as a function). Since it just says add this value to the dynamic scope until you exit it, which generally is a fine operation. The manual exit operation is the one that could break things, since you need reference to the previous scope.

@JeffBezanson JeffBezanson added the REPL Julia's REPL (Read Eval Print Loop) label Jan 23, 2024
@JeffBezanson
Copy link
Sponsor Member

I think a good way to implement this would be to have the REPL enter a normal with block and run a nested REPL that you can exit with ^D.

@StefanKarpinski
Copy link
Sponsor Member

Aside: one thing I hate about nested REPLs that you exit with ^D is that it makes it really easy to accidentally exit your whole REPL session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Indicates new feature / enhancement requests multithreading Base.Threads and related functionality REPL Julia's REPL (Read Eval Print Loop)
Projects
None yet
Development

No branches or pull requests

5 participants