Fusion icon indicating copy to clipboard operation
Fusion copied to clipboard

Scope context

Open dphfox opened this issue 1 year ago • 6 comments

One year on from the original introduction of scopes as a concept, they've single-handedly come to dominate how Fusion thinks about memory management. The reason is simple; it allows developers to define the behaviour of a whole block of code at once, with clean and explicit ways of inserting sub-blocks or isolated islands, and perpendicular ways of inheriting behaviour across those boundaries.

There's another problem for which this is useful: contextual cascading values.

In an ideal world, cascading values act identically to prop drilling: with lexical scope. This is very, very hard to do in practice - without some constraints, the problem is impossible to generally solve in Luau.

So right now, contextuals use an approximation - a call-stack-based approach - by exposing a value only for the duration of a callback. This works so long as the receiving code isn't re-entered once the callback returns - when that happens, the temporary value will have been erased by then. Additionally, other values might leak through if the contextual is set for the code causing the re-entry.

Scopes are actually a great candidate for replacing this. Scopes are "lexical-ish" because they are explicitly drilled down through components, though they're not guaranteed to be unique for each lexical scope. However, this nuance doesn't matter, because the only time scopes have to be distinct from each other is when a distinct observable effect is wanted; either a different lifetime or a different __index table.

The concept of contextual values fits neatly into this model: the observable effect is the value the contextual takes on, which is represented by a distinct scope in which the contextual has a different value.

To make this work, we will need to decide on some mechanism to inherit data into derived and inner scopes, and should consider renaming scoped to more clearly be a detached 'starting point' or 'root'.

dphfox avatar Aug 31 '24 03:08 dphfox

One way we could do this would be to take advantage of the inherited nature of the __index table - especially if the current outer merge in deriveScopeImpl is substituted for a metatable chain to allow for dynamic contents. But this might affect the performance of method lookups for especially nested scopes, which seems undesirable.

Instead, it might be wiser to attach ancestry information to scopes, probably as part of the metatable. This can be as simple as a reference to the outer scope.

Interested code can then associate scopes with contextual values, and walk up the ancestry to learn about contextual values defined in ancestors.

dphfox avatar Aug 31 '24 03:08 dphfox

Storing ancestry information also has the potential to significantly improve lifetime analysis, by allowing analysis to flow up to common ancestors rather than having to flow down from the scopes being analysed.

dphfox avatar Aug 31 '24 03:08 dphfox

Proposed user facing API surface:

Contextual:is(value):inside(function(scope)
    -- ...
end)

The returned scope is internally weakly mapped to value.

dphfox avatar Aug 31 '24 03:08 dphfox

Alternatively, it'd be possible to ship a design that weakly maps the current scope to a value, but this is undesirable because it forces the developer to explicitly introduce scopes where they want the value to be independent. By creating the scope when a value is changed, we can create that breakpoint automatically.

dphfox avatar Aug 31 '24 03:08 dphfox

As a further consideration, :now() is not scope-aware. It would need to be given a scope to inspect.

For this purpose, we could introduce a new :here(scope) accessor.

Alternatively, we could introduce some kind of object that, when constructed in a scope, can read the Contextual, but this is heavier memory-wise and probably overengineered.

dphfox avatar Aug 31 '24 03:08 dphfox

The mechanisms for :during()/:now() and :inside()/:here() should not be mixed or interact in any way. This would introduce asymmetry (where :now() is scope-unaware but :here() is call-stack-aware) and could confuse users.

We might want to force one or the other to be used per-contextual. It's weird to want to mix them. Alternatively, call-stack-based contextuals could be removed outright.

dphfox avatar Aug 31 '24 04:08 dphfox