hphp/hack/doc/HIPs/memoization_options.md
This document describes proposed changes to the already defined Implicit Context feature in pursuit of the following goals:
<<__Memoize>> todayThis feature is also referred to as the "Dynamically Enforced Implicit Context" feature.
The Implicit Context feature provides a value that is implicitly propagated to callees recursively. For example:
// Native, in HH namespace
abstract class ImplicitContext {
abstract const type T as nonnull;
public static function set<Tout>(
this::T $context,
(function ()[zoned]: Tout) $f
)[zoned]: Tout { ... }
public static function get()[zoned]: ?this::T;
}
// Userland
final class MyPolicyImplicitContext extends HH\ImplicitContext {
...
}
MyPolicyImplicitContext::set(
$my_policy,
()[zoned] ==> do_stuff(),
);
...
function some_recursive_callee_of_do_stuff()[zoned]: void {
$_ = MyPolicyImplicitContext::get(); // returns $my_policy
}
In order to prevent poisoning memoization caches (fetching the cached results computed under one IC value when executing under a different IC value), we designed the IC feature to be coupled with the coeffects feature which applies static recursive restrictions. We required that:
[zoned] but we ignore this context in this document)[zoned] function must either:
[zoned])ImplicitPolicy capability which is required to access the IC (e.g. [leak_safe])While tying the IC to contexts and capabilities gives us static guarantees about code, adding these more restrictive contexts to code requires a lot of effort, and some developers want the benefits of the IC without the requirement of using contexts.
We propose allowing setting an IC when executing functions requiring [defaults]. The new signature for this method on the ImplicitContext:: class will be:
public static function runWith<Tout>(
this::T $context,
(function ()[_]: Tout) $f
)[ctx $f, zoned]: Tout { ... }
In the above, we have also renamed
ImplicitContext::settoImplicitContet::runWithin order to avoid an incorrect assumption that the IC is set to the given value after the completion of this method call. We use the new “runWith” name for the remainder of this document.
We will avoid poisoning memoization caches by dynamically requiring that memoized functions executing under an IC fall into one of two safe categories:
__Memoize(#KeyedByIC) attribute (formerly known as __PolicyShardedMemoize) instead of regular __Memoize. We will allow this attribute to be used with any function with the ImplicitPolicy capability including [defaults] functions.__Memoize(#MakeICInaccessible) instead of regular __Memoize. This attribute will act like __Memoize except: attempting to fetch the IC or calling an uncategorized memoized function from a function with this attribute will throw. This behavior applies to immediate and recursive callees until an IC value is set again (if ever).__Memoize and requiring a context that does not have the ImplicitPolicy capability. Because fetching the IC requires [zoned], we know that recursive callees cannot access the IC. (Given the current set of contexts, this is the set of contexts as capable as or less capable than [leak_safe, globals].)__Memoize without an categorization argument or coeffect, it will be treated as __Memoize(#MakeICInaccessible).In the above, we are adding an optional enum class label argument to the
__Memoizeattribute. See the “Syntax” section.
Note: In this document, we describe semantics in terms of __Memoize and __Memoize(). The same statements apply to __MemoizeLSB and new __MemoizeLSB().
Under this proposal, the IC can be in one of three states:
null : The IC has not been set
value(T) : The IC has been set to some value
inaccessible : Fetching the IC or calling uncategorized memoized functions will result in an exception
The following actions will have the following behavior:
__Memoize that has the ImplicitPolicy capability (e.g. [defaults])This is treated the same as __Memoize(#MakeICInaccessible)
* : Run, do not key cache with IC, transition to inaccessible state with this function as blame
__Memoize(#KeyedByIC)This does not affect the state of the IC.
null: Run and use null as the IC key
value: Run and use the IC value as the IC key
inaccessible: Run and use the inaccessible state (singleton value) as the IC key
__Memoize(#MakeICInaccessible)* : Run, do not key cache with IC, transition to inaccessible state with this function as blame
__Memoize that does not have the ImplicitPolicy capabilityThis does not affect the state of the IC.
* : Run, do not key with IC
ImplicitContext::getThis does not affect the state of the IC.
null: return null
value: return value
inaccessible: throw
ImplicitContext::isInaccessibleThis does not affect the state of the IC.
null: return false
value: return false
inaccessible: true
ImplicitContext::runWith* : set the IC to a value, transition to the value state with this function call as blame
HH\Coeffects\backdoor or aliasesThese backdoors allow calling code requiring [defaults] from various, less-capable contexts.
* : move to null state (clears any IC value)
HH\Coeffects\fb\backdoor_to_globals_leak_safe__DO_NOT_USEThis backdoor allows calling code requiring at most [leak_safe, globals] from any context.
* : no op, no state change
Note that by enforcing that the code executed via this backdoor can only require at most [globals, leak_safe], we prevent problematic calls to unsafe memoized functions and calls to fetch the IC.
This section describes rules for what functions with what coeffects can use these new attributes.
__Memoize(#KeyedByIC) will only be permitted on functions that are known to have the ImplicitPolicy capability at compile time.
[defaults] or contain one of the following contexts explicitly: defaults, zoned, zoned_shallow, zoned_local.ImplicitPolicy capability is not affected by the Implicit Context. However, this would not be the case if we allowed #KeyedByIC — e.g. imagine a memoized, leak-safe function that returns a random number.__Memoize(#MakeICInaccessible) will only be allowed on functions with any of the following contexts: defaults, leak_safe_shallow, leak_safe_local
ImplicitPolicy capability, the IC is already inaccessible and the function can use regular __Memoize. For functions with zoned, using this attribute seems contradictory.zoned context must use #KeyedByIC.A developer that wants to enable adopt DEIC in a codebase:
ImplicitContext::runWithImplicitContext::isInaccessible where you expect to fetch the implicit context via ImplicitContext::get and log all violations.__Memoize functions to be #KeyedByIC).ImplicitContext::get to influence runtime.Allowing use of a dynamically-enforced Implicit Context is compatible with a statically-enforced Implicit Context using Contexts and Capabilities. In fact, the work to specify IC-handling for memoized functions is a subset of the work required to make functions callable from [zoned] contexts.
In abstract, you can think of functions using __Memoize(#KeyedByIC) as being functions where the intention is to eventually require [zoned] (if it does not already) and think of functions using __Memoize(#MakeICInaccessible) as being functions where the intention is to eventually require [leak_safe] or some more restrictive context.
The current proposal introduces an optional, enum class label argument to the existing __Memoize (and __MemoizeLSB) attributes e.g. __Memoize(#KeyedByIC). This would require adding the ability to use enum class labels in attribute argument positions.
enum class MemoizeOption: string {
string KeyedByIC = 'KeyedByIC';
string MakeICInaccessible = 'MakeICInaccessible';
}
Currently, the __Memoize attribute is hard-coded into the typechecker and there is no declaration for it. However, you could declare it like so:
final class __Memoize implements HH\FunctionAttribute, HH\MethodAttribute {
public function __construct(
private ?HH\EnumClass\Label<MemoizeOption, string> $kind,
) {}
}
Alternatives we considered:
__Memoize(KeyedByIC). However, constants are not permitted as attribute arguments as attributes are evaluated at compile time and using evaluation constants would require “decls in compilation.” We also compute attributes in order to build indices for Facts which may never have access to cross-file declarations.__Memoize("KeyedByIC") — this has the downside of a string argument appears to take arbitrary strings, but can only take specific, special strings. This may also require additional changes to support autocompleting these strings in IDEs.__MemoizeKeyedByIC and __MemoizeMakeICInaccessible
__MemoizeWithIC and __MemoizeAndMakeICInaccessible__Memoize e.g. <<__Memoize, __MakeICInaccessible>>
__Memoize(KeyedByIC)__MakeICInaccessible to be used standalone. However, at the moment, we do not see a legitimate use for it without an accompanying __Memoize."KeyedByIC" uses "Keyed" which references the fact that the IC is incorporated into the key used in the memoization cache. We could also possibly use imperative "Key" vs past participle "Keyed." Possible alternatives to the verb "key:"
"MakeICInaccessible" describes how the IC cannot be accessed. A previously proposed name used the verbiage "clear IC," however this created a distinction between a state where the IC was "cleared" vs one where the IC was never set. Primary concerns with this name are aesthetic: "inaccessible" is long and may be awkward to say and spell. A name like <imperative verb> + "IC" may be preferable, but verbs like "prohibit" or "ban" may incorrectly imply immediate exceptional behavior of the function with the attribute instead of restrictions on the function's dependencies.
“IC” as an abbreviation for “Implicit Context” was chosen for brevity and lack of more attractive options. We concluded that brevity was valuable given expected prevalent usage of these attributes and that the disadvantage of possible ambiguity of “IC” would be offset by the fact that the full symbols names would be sufficiently unique. Other alternatives that were considered:
__Memoize(#SoftMakeImplicitContextInaccessible).The current implementation of Implicit Contexts allows for multiple IC “flavors.” You create a flavor by defining a class implementing the native HH\ImplicitContext class. This presents the question for how the above features should work when there is more than one IC flavor.
We have identified a few options:
KeyedByIC and MakeICInaccessible features by flavor, the flavors are already tightly coupled since they will need to agree on when these attributes ought to be used. Coupling is also introduced by sharing related coeffects and the backdoor functions.runWith calls, but MakeICInaccesible would set all flavors’ state to inaccessible. This would likely require more checks at runtime resulting in worsened performance.inaccessible state would resulting in implicitly setting all other flavors’ value to the initial null state.Our recommendation is that we take Option 3 which is closest to the current implementation of Implicit Contexts but require in userland that there is at most a single child of HH\ImplicitContext. This effectively means choosing Option 1 from the perspective of the user. This compromise allows us to make fewer changes to the runtime in the near term. We can choose to simplify the runtime to not support multiple flavors at a later date.