This is a Meta-ism, but adding it for now to unblock. We special-case the
`<fbt>` element for translation purposes, and have a transform that requires the
children of this element to be a limited subset of nodes. Notably, any dynamic
translation values must appear as `<fbt:param>` children — we disallow
identifiers as children of `<fbt>` nodes.
This PR adds a new pass which finds `<fbt>` nodes and ensures their immediate
operands are not independently memoized. Note that this still allows the values
of `<fbt:param>` to be independently memoized, as demonstrated in the unit test.
```js
// here, `a?.b.c` is a single optional chain
// (evaluates to undefined if a is nullish)
a?.b.c;
// here, 'a?.b` is an optional chain, and `.c` is an unconditional load
// (nullthrows if a is nullish)
(a?.b).c;
```
---
Next PR in stack will add a bailout for `(a?.b).c`.
(If we want to properly handle `(a?.b).c`, we might want to model optional
chains explicitly in the HIR. We currently assume that any `PropertyLoad` whose
lhs is an optional property load is read conditionally.)
This is incredibly obvious in hindsight, but for exposure logging to work
correctly we need to *call* the underlying `MobileConfig.getBool` function at
the callsite – otherwise the bool is evaluated once (and only once) when the
module is loaded.
Tested internally and verified that in dogfooding the exposure logging was
working correctly
- Fixes a missing break in InferTypes - I disabled no-fallthrough previously
because it would erroneously report that certain cases with non-builtin throws
(eg `invariant`) would fall through. This brings the rule back but allows
disabling it with a `// break omitted` comment, since it's still helpful in
catching some actual missing breaks.
- Add `DEFAULT_SHAPES` ShapeRegistry, which holds builtins and all
`ObjectShapes` used in `DEFAULT_GLOBALS`.
- Add a few typed objects / functions into `DEFAULT_GLOBALS` (used for tests)
- Add type inference and `infer-global-object` test
Adds `GlobalRegistry`, which holds the names and types of known global objects,
i.e.
```js
type GlobalRegistry = Map<string, PrimitiveType | ObjectType | FunctionType |
HookType | PolyType>;
// ...
globalRegistry.get("NaN"); // {kind: "Primitive"}
globalRegistry.get("parseInt"); // {kind: "Function", shapeId: "..."}
globalRegistry.get("Math"); // {kind: "Object", shapeId: "..."}
```
Since we currently do not track module imports and module-level declarations,
builtin and custom hooks currently also live in GlobalRegistry.
```js
globalRegistry.get("useState"); // {kind: "Hook", definition: {...}}
globalRegistry.get("useFreeze"); // {kind: Hook, definition: {...}}
```
This PR does not allow Forget users to define their own globals. When we add
this as a configuration, we should not expose `ShapeRegistry` to the user, as a
user-provided ShapeRegistry may accidentally be not well formed. (i.e. missing
(1) required shapes (BuiltInArray for [] and BuiltInObject for {}) or (2) some
recursive shapeIds)
```js
export type UserType = UserObject | UserFunction | "Primitive" | "BuiltinObject"
| ...;
export type UserObject = {
kind: "Object",
properties: Map<string, UserType>
}
export type UserFunction = {
kind: "Function",
properties: Map<string, UserType>,
signature: ...
}
export type UserGlobals = Map<string, UserType>;
class Environment {
constructor(globals: Map<string, UserType>, ...) {
// ...
addUserDefinedGlobals(this.#globals, this.#shapes);
```
---
Simplify Environment options by:
- EnvironmentOptions -> EnvironmentConfig
config is now directly passed around instead of being eagerly merged.
- Moving merging / initialization logic into `Environment` constructor. From my
understanding, there is no need to decouple merged options from an environment.
This prepares Environment for the next PR, which adds non-stateful properties to
Environment (i.e. a `GlobalRegistry`) that should be converted from config
values (i.e. not directly exposed to the user due to potentially inconsistent
inputs)
---
#1254 added inference for hooks loaded from globals. This is the only time we
need to generate a type equation assigning `lval` to a resolved`Hook` type.
@gsathya Would love to get your feedback here on the change. From my
understanding, this change is technically incorrect, since the type equation we
generate should be dependent on the `callee` type (i.e. `Hook` if callee is a
hook, `Function` if callee is a function).
Would the next step be to consolidate `Hook` and `Function` types?
```js
type Function {
...
isHook: boolean, // set by inference
}
type FunctionSignature {
isHook: boolean, // set when adding to ShapeRegistry
}
```
I already taught `lowerAssignment()` to handle assignment patterns for
destructuring, we just have to call this helper for assignment pattern params
too.
In a lambda, a return/throw terminal could return a captured context ref needs
to be treated as a mutation to correctly alias the returned context ref and the
lvalue.
Terminal operands are generally not mutating so this hasn't mattered so far. But
in a lambda, a return terminal could return a captured context ref which needs
to be treated as a mutation to correctly alias the returned context ref and the
lvalue.
`JSXEmptyExpression` is never added to a React element's children [in
`react.buildChildren`](https://github.com/babel/babel/blob/main/packages/babel-types/src/builders/react/buildChildren.ts),
which is [used
by](https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-react-jsx/src/create-plugin.ts#L649)
`plugin-transform-react-jsx`].
An alternative would be to represent JSX expressions differently in HIR, then
codegen `JSXEmptyExpression`s back when we encounter an `EmptyExpression`
```js
- children: Array<Place>,
- children: Array<Place | "EmptyExpression">,
```
(We could also retain `JSXEmptyExpression` as an `InstructionValue` that
produces a Primitive. However, this would make babel types in Codegen a bit more
messy, as `JSXEmptyExpression` does not extend `Expression` (which currently is
the result of every `InstructionValue`).)
Creates a new helper, `const temp: Place = lowerValueToTemporary(builder,
value)` which creates a new temporary and an instruction to write that value to
the temporary. We have this pattern all over BuildHIR, and the new helper makes
this all a bit tidier.
Adds support for `await` expressions. We have primarily seen await used inside
callbacks, not directly within component render logic, but because we construct
HIR for lambdas it is helpful to be able to model await rather than require
everyone to rewrite to use the Promise API. Note a subtlety: awaiting a promise
is a mutative operation, so we a) model it as a Mutate effect and b) avoid DCE
of await expressions since they may cause side effects. See the test cases for
examples.
Adds a new helper method that we can use when processing expressions whose
evaluation ordering may not be preserved. This was previously the case only for
switch test case values, but we can use this for AssignmentPattern
(destructuring default values) as well.
"Supports" default values in destructuring (AssignmentPattern) by lowering to a
ternary, even in the output. Examples:
```javascript
// Input:
const [x = 'default'] = y;
// Output:
const [t0] = y;
const x = t0 === undefined ? 'default' : t0;
```
```javascript
// Input 2
const [{x} = makeObject()] = y;
// Output 2
const [t0] = y;
const {x} = t0 === undefined ? makeObject() : t0;
```
Note that this is how Babel lowers AssignmentPattern, so it isn't too bad. This
should help avoid the need to update product code, even if the output isn't
perfectly ideal.
This is kind of a hack, but i think it's worth it given that JSXNamespacedName
is relatively uncommon. Adding a new InstructionValue variant to represent a
namespaced name is one option, but then that isn't a valid expression and can't
appear as an operand anywhere else. Instead, we lower namespaced names as a
primitive (string) as `${namespace}:${name}` — exploiting the fact the namespace
and name can't have a colon, and non-namespaced tagnames also can't have colons.
It's a bit of a hack but it's contained to the JSX processing code. If folks
have strong opinions on this i'm happy to change but this felt reasonable as a
quick and reliable way to unblock support.
NOTE: there is a larger question of what to do about compiling `fbt` tags.
Before we can do anything with them, though, we need to parse them.
We need to check reactivity of both the operand and its resolved source (if
operand is produced by a LoadLocal / PropertyLoad / ComputedLoad).
Both the operand and its source can have reactivity.
e.g.
```js
const o = makeObject(); // source has no reactivity
const x = o[props.x]; // x is reactive
```
Rather than having a special FunctionCall type that deduces the return type,
change the FunctionType to include the return type.
This return type is inferred as part of unification.
---
Every `OptionalMemberExpression` rvalue has the form
`<requiredPath>?.<optionalPath>`.
```
// required = [a], optional: [b, c]
props.a?.b.c;
props.a?.b?.c;
```
When calculating reactive dependencies, recall that it is always correct to add
a subpath of a dependency (e.g. we can always take `props.a` instead of
`props.a.b` as a dependency). See comments in `DeriveMinimalDependencies` for a
longer explanation.
There are two ways we can deal with `OptionalMemberExpression`:
- We can always truncate a OptionalMemberExpression dependency to its
`requiredPath`, taking only the required path as a dependency.
- this is the simpler approach, but it potentially loses granularity.
e.g.
```
// here, since props.a is already unconditionally accessed,
// we can safely add props.a.b as a dependency and preserve both
// nullthrows and the correct dependency set.
scope @0 {
let x = [];
x.push(props.a?.b);
x.push(props.a.b);
}
```
(See added test case `reduce-reactive-cond-memberexpr-join` + its comment block
for a more detailed explanation`
- (the approach taken by this PR)
We can add the `requiredPath` as a potentially unconditional access (dependent
on other control flow) and `requiredPath + optionalPath` as a conditional
dependency.
---
Previously, both `path=null` and `path=[]` could represent a dependency with no
property path (i.e. the result of a LoadLocal with no PropertyLoad).
Make path non-nullable so we don't have to add null checks everywhere.
---
We don't need to store whether a `PropertyLoad` happens within a conditional
(within its reactive scope). In fact, the PropertyLoad producing a rval often is
in a different ReactiveScope from where the rval is used.
We only need to add `#inConditionalWithinScope` when we actually visit a
reactive dependency.
Earlier PRs bailed out when the callee of an OptionalCallExpression was a
MemberExpression or OptionalMemberExpression (ie for optional method calls).
This PRs expands support for optional method calls, including when the receiver,
method, or both are optional. Even better, we don't need to add any additional
terminals or instruction variants for this case - the one new OptionalCall
terminal from earlier in the stack works for all these cases.
Tests, focusing on two key behaviors:
* Dependencies of the args are treated as conditional, since the call may not
happen
* Args cannot be memoized independently, even when that would be valid for a
non-optional call.
Implements HIR->ReactiveFunction conversion and Codegen for optional calls. We
add a new OptionalCall variant of ReactiveValue, which is a SequenceExpression
that describes the evaluation of the args and the call itself. This is then
straightforward to codgen.
Implements lowering for a subset of optional calls - specifically, we don't
(yet) support when the callee is a member expression or an optional member
expression. So `foo?.()` works but we bailout on `object?.foo()` and
`object.foo?.()`.
For `<calleee>?.(<args>)` we lower as roughly:
```
bb0:
t0 = <callee>
OptionalCall test=bb1 fallthrough=
bb1 (value):
Branch t0 consequent=bb2 alternate=bb3
bb2 (value):
...lower <args> here...
t1 = Call t0, args
StoreLocal res, t1
Goto bb4
bb3 (value):
t2 = undefined
StoreLocal res, t2
Goto bb4
bb4:
// result in `res` here
```
Adds a new `optional-call` terminal and sets up the appropriate handling in the
visitors, with lowering/reactivefunction/codegen as todos for now and
implemented in follow-ups.
---
Expand Hindley Milner type inference to infer dependent types.
Say `t` is a typevar and `t'` is some type (a built-in type, phi node, or
another typevar).
Our type equations are as follows (please edit/correct notation 😅)
- type substitution: `t = t'`,
- ~~dependent~~ polymorphic property load: `t = t'.prop`
- polymorphic function call `t = fnCall{returnType}`
- ~~dependent property call: `t = t'.prop` (only if t'.prop is a function
type)~~
- ~~dependent return type: `t = t'.[[returntype]]`~~
---
+10 −1,698 lines [[insert impacc macro]]
The ObjectShape stacks (#1350, #1358) used these tests to record changes in
inferred types (and associated ObjectShapes), reference effects, and mutable
ranges.
Now that those PRs have landed, we can delete these tests. They are somewhat
fragile (changing anytime HIR / printHIR is changed) and easily cause
rebase/merge conflicts.
---
This PR does not add inference for normal `CallExpression`s, since built-in
functions for `Array` and `Object` are usually only valid if called with a
correctly-typed `this`. If we want codegen to preserve source code semantics,
Forget should only add inferred types it is confident about.
This PR also adds `returnEffect` to FunctionSignature. `returnEffect = Store` if
this function is known to always return a captured value from `receiver` or
`args`.
---
Expand Hindley Milner type inference to infer dependent types.
Say `t` is a typevar and `t'` is some type (a built-in type, phi node, or
another typevar).
Our type equations are as follows (please edit/correct notation 😅)
- type substitution: `t = t'`,
- ~~dependent~~ polymorphic property load: `t = t'.prop`
- polymorphic function call `t = fnCall{returnType}`
- ~~dependent property call: `t = t'.prop` (only if t'.prop is a function
type)~~
- ~~dependent return type: `t = t'.[[returntype]]`~~
We limit the types of expressions allowed as switch case test values because we
our HIR doesn't yet preserve order-of-evaluation for switch test values (we
model them as being evaluated prior to entering the switch, as opposed to
lazily, when the case is reached). One common pattern internally is test case
values that are properties of a global, eg you have some bag of enum values and
are comparing against that:
```javascript
// at module scope, or imported from another module:
const OPTIONS = {FOO: 'foo'};
// in a component
switch (value) {
case OPTIONS.FOO: { ... }
}
```
This PR allows this specific case, ie member expressions where the innermost
object is a global identifier.