JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.4k stars 5.45k forks source link

Interactive scopes #51434

Open simonbyrne opened 11 months ago

simonbyrne commented 11 months ago

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 commented 11 months ago

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 commented 11 months ago

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?

vchuravy commented 11 months ago

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 commented 11 months ago

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 commented 11 months ago

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 commented 11 months ago

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 commented 11 months ago

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 commented 7 months ago

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 commented 7 months ago

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.