Skip to content

Commit

Permalink
[wip][compiler] Infer optional dependencies
Browse files Browse the repository at this point in the history
Updates PropagateScopeDeps and DeriveMinimalDeps to understand optional dependency paths (`a?.b`). There a few key pieces to this:

In PropagateScopeDeps we jump through some hoops to work around the awkward structure of nested OptionalExpressions. This is much easier in HIR form, but I managed to get this pretty close and i think it will be landable with further cleanup. A good chunk of this is avoiding prematurely registering a value as a dependency - there are a bunch of indirections in the ReactiveFunction structure:

```
t0 = OptionalExpression
  SequenceExpression
    t0 = Sequence
      ...
    LoadLocal t0
```

Where if at any point we call `visitOperand()` we'll prematurely register a dependency instead of declareProperty(). The other bit is that optionals can be optional=false for nested member expressions where not all the parts are actually optional (`foo.bar?.bar.call()`). And of course, parts of an optional chain can still be conditional even when optional=true (for example the `x` in `foo.bar?.[x]?.baz`). Not all of this is tested yet so there are likely bugs still.

The other bit is DeriveMinimalDeps, which is thankfully easier. We add OptionalAccess and OptionalDep and update the merge and reducing logic for these cases. There is probably still more to update though, for things like merging subtrees. There are a lot of ternaries that assume a result can be exactly one of two states (conditional/unconditional, dependency/access) and these assumptions don't hold anymore. I'd like to refactor to dependency/access separate from conditional/optional/unconditional. Also, the reducing logic isn't quite right: once a child is optional we keep inferring all the parents as optional too, losing some precision. I need to adjust the reducing logic to let children decide whether their path token is optional or not.

ghstack-source-id: 4da2bdabd95ea214cf86a335fa521a6bc50a78e2
Pull Request resolved: #30819
  • Loading branch information
josephsavona committed Aug 27, 2024
1 parent ac01cf7 commit aae660e
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
case 'branch': {
value = `[${terminal.id}] Branch (${printPlace(terminal.test)}) then:bb${
terminal.consequent
} else:bb${terminal.alternate}`;
} else:bb${terminal.alternate} fallthrough:bb${terminal.fallthrough}`;
break;
}
case 'logical': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1446,9 +1446,19 @@ function codegenDependency(
dependency: ReactiveScopeDependency,
): t.Expression {
let object: t.Expression = convertIdentifier(dependency.identifier);
if (dependency.path !== null) {
if (dependency.path.length !== 0) {
const hasOptional = dependency.path.some(path => path.optional);
for (const path of dependency.path) {
object = t.memberExpression(object, t.identifier(path.property));
if (hasOptional) {
object = t.optionalMemberExpression(
object,
t.identifier(path.property),
false,
path.optional,
);
} else {
object = t.memberExpression(object, t.identifier(path.property));
}
}
}
return object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {CompilerError} from '../CompilerError';
import {DependencyPath, Identifier, ReactiveScopeDependency} from '../HIR';
import {printIdentifier} from '../HIR/PrintHIR';
import {assertExhaustive} from '../Utils/utils';
import {printDependency} from './PrintReactiveFunction';

/*
* We need to understand optional member expressions only when determining
Expand Down Expand Up @@ -60,13 +61,14 @@ export class ReactiveScopeDependencyTree {
const {path} = dep;
let currNode = this.#getOrCreateRoot(dep.identifier);

const accessType = inConditional
? PropertyAccessType.ConditionalAccess
: PropertyAccessType.UnconditionalAccess;

for (const item of path) {
// all properties read 'on the way' to a dependency are marked as 'access'
let currChild = getOrMakeProperty(currNode, item.property);
const accessType = inConditional
? PropertyAccessType.ConditionalAccess
: item.optional
? PropertyAccessType.OptionalAccess
: PropertyAccessType.UnconditionalAccess;
currChild.accessType = merge(currChild.accessType, accessType);
currNode = currChild;
}
Expand All @@ -77,7 +79,9 @@ export class ReactiveScopeDependencyTree {
*/
const depType = inConditional
? PropertyAccessType.ConditionalDependency
: PropertyAccessType.UnconditionalDependency;
: isOptional(currNode.accessType)
? PropertyAccessType.OptionalDependency
: PropertyAccessType.UnconditionalDependency;

currNode.accessType = merge(currNode.accessType, depType);
}
Expand All @@ -88,7 +92,9 @@ export class ReactiveScopeDependencyTree {
const deps = deriveMinimalDependenciesInSubtree(rootNode);
CompilerError.invariant(
deps.every(
dep => dep.accessType === PropertyAccessType.UnconditionalDependency,
dep =>
dep.accessType === PropertyAccessType.UnconditionalDependency ||
dep.accessType == PropertyAccessType.OptionalDependency,
),
{
reason:
Expand Down Expand Up @@ -173,6 +179,27 @@ export class ReactiveScopeDependencyTree {
}
return res.flat().join('\n');
}

debug(): string {
const buf: Array<string> = [`tree() [`];
for (const [rootId, rootNode] of this.#roots) {
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
this.#debugImpl(buf, rootNode, 1);
}
buf.push(']');
return buf.length > 2 ? buf.join('\n') : buf.join('');
}

#debugImpl(
buf: Array<string>,
node: DependencyNode,
depth: number = 0,
): void {
for (const [property, childNode] of node.properties) {
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
this.#debugImpl(buf, childNode, depth + 1);
}
}
}

/*
Expand All @@ -196,8 +223,10 @@ export class ReactiveScopeDependencyTree {
*/
enum PropertyAccessType {
ConditionalAccess = 'ConditionalAccess',
OptionalAccess = 'OptionalAccess',
UnconditionalAccess = 'UnconditionalAccess',
ConditionalDependency = 'ConditionalDependency',
OptionalDependency = 'OptionalDependency',
UnconditionalDependency = 'UnconditionalDependency',
}

Expand All @@ -211,9 +240,16 @@ function isUnconditional(access: PropertyAccessType): boolean {
function isDependency(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.ConditionalDependency ||
access === PropertyAccessType.OptionalDependency ||
access === PropertyAccessType.UnconditionalDependency
);
}
function isOptional(access: PropertyAccessType): boolean {
return (
access === PropertyAccessType.OptionalAccess ||
access === PropertyAccessType.OptionalDependency
);
}

function merge(
access1: PropertyAccessType,
Expand All @@ -222,6 +258,7 @@ function merge(
const resultIsUnconditional =
isUnconditional(access1) || isUnconditional(access2);
const resultIsDependency = isDependency(access1) || isDependency(access2);
const resultIsOptional = isOptional(access1) || isOptional(access2);

/*
* Straightforward merge.
Expand All @@ -237,6 +274,12 @@ function merge(
} else {
return PropertyAccessType.UnconditionalAccess;
}
} else if (resultIsOptional) {
if (resultIsDependency) {
return PropertyAccessType.OptionalDependency;
} else {
return PropertyAccessType.OptionalAccess;
}
} else {
if (resultIsDependency) {
return PropertyAccessType.ConditionalDependency;
Expand All @@ -256,19 +299,25 @@ type ReduceResultNode = {
accessType: PropertyAccessType;
};

const promoteUncondResult = [
const promoteUncondResult: Array<ReduceResultNode> = [
{
relativePath: [],
accessType: PropertyAccessType.UnconditionalDependency,
},
];

const promoteCondResult = [
const promoteCondResult: Array<ReduceResultNode> = [
{
relativePath: [],
accessType: PropertyAccessType.ConditionalDependency,
},
];
const promoteOptionalResult: Array<ReduceResultNode> = [
{
relativePath: [],
accessType: PropertyAccessType.OptionalDependency,
},
];

/*
* Recursively calculates minimal dependencies in a subtree.
Expand All @@ -284,7 +333,7 @@ function deriveMinimalDependenciesInSubtree(
({relativePath, accessType}) => {
return {
relativePath: [
{property: childName, optional: false},
{property: childName, optional: isOptional(accessType)},
...relativePath,
],
accessType,
Expand All @@ -302,7 +351,8 @@ function deriveMinimalDependenciesInSubtree(
if (
results.every(
({accessType}) =>
accessType === PropertyAccessType.UnconditionalDependency,
accessType === PropertyAccessType.UnconditionalDependency ||
accessType === PropertyAccessType.OptionalDependency,
)
) {
// all children are unconditional dependencies, return them to preserve granularity
Expand All @@ -315,6 +365,27 @@ function deriveMinimalDependenciesInSubtree(
return promoteUncondResult;
}
}
case PropertyAccessType.OptionalDependency: {
return promoteOptionalResult;
}
case PropertyAccessType.OptionalAccess: {
if (
results.every(
({accessType}) =>
accessType === PropertyAccessType.UnconditionalDependency ||
accessType === PropertyAccessType.OptionalDependency,
)
) {
// all children are unconditional dependencies, return them to preserve granularity
return results;
} else {
/*
* at least one child is accessed conditionally, so this node needs to be promoted to
* unconditional dependency
*/
return promoteOptionalResult;
}
}
case PropertyAccessType.ConditionalAccess:
case PropertyAccessType.ConditionalDependency: {
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
const identifier =
printIdentifier(dependency.identifier) +
printType(dependency.identifier.type);
return `${identifier}${dependency.path.map(token => `.${token.property}`).join('')}`;
return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`;
}

export function printReactiveInstructions(
Expand Down
Loading

0 comments on commit aae660e

Please sign in to comment.