Skip to content
This repository was archived by the owner on Oct 9, 2024. It is now read-only.

Support side effects #8

Merged
merged 6 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Type: `Feature` - `Bug` - `Enhancement` - `Refactor` - `Unknown`
- [ ] `Enhancement` Skip memoization of useState setter functions
- [ ] `Enhancement` When unwrapping array and object patterns, optimize the unwrapping by limiting it to component variables
- [ ] `Enhancement` Support React.createElement calls
- [ ] `Enhancement` Hot module reloading improvement utilizing a checksum to invalidate cache
- [ ] `Feature` Memoize array map items

## `Feature` ESLint Plugin
Expand All @@ -30,3 +31,4 @@ The following are unknowns that need to be researched and tested.
- [ ] Assignment expressions in JSX `<div>{(a = 1)} {a}</div>` - is the order of execution guaranteed?
- [ ] Source maps - is it possible to generate source maps for the transformed code?
- [ ] Hot reloading - how does hot reloading work with the transformed code?
- [ ] Memoization of values declared after the first return statement
2 changes: 1 addition & 1 deletion packages/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dev": "yarn build --watch",
"build": "tsup src/main.ts --dts --format cjs,esm",
"test": "jest",
"lint": "eslint src/**/* --max-warnings 10"
"lint": "eslint src/**/* --max-warnings 15"
},
"devDependencies": {
"@babel/plugin-syntax-jsx": "^7.23.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as t from "@babel/types";
import { DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME } from "~/utils/constants";

export function convertStatementToSegmentCallable(
statement: babel.NodePath<babel.types.Statement>,
{
initialValue,
cacheNullValue,
segmentCallableId = statement.scope.generateUidIdentifier(
DEFAULT_SEGMENT_CALLABLE_VARIABLE_NAME
),
}: {
initialValue?: t.Expression;
performReplacement?: boolean;
cacheNullValue?: t.Expression;
segmentCallableId?: t.Identifier;
}
) {
const parentDeclaration = statement.find((p) =>
p.isVariableDeclaration()
) as babel.NodePath<babel.types.VariableDeclaration> | null;

const makeSegmentCallable = (statements: t.Statement[]) => {
return t.variableDeclaration("const", [
t.variableDeclarator(
segmentCallableId,
t.arrowFunctionExpression(
[],
t.blockStatement(
statements.concat(
cacheNullValue ? [t.returnStatement(cacheNullValue)] : []
)
)
)
),
]);
};

let replacements: t.Node[] | null = null;
if (parentDeclaration) {
const newKind = parentDeclaration.node.kind === "var" ? "var" : "let";
const newDeclaration = t.variableDeclaration(
newKind,
parentDeclaration.node.declarations.map((declaration) => {
return t.variableDeclarator(declaration.id, initialValue);
})
);

const assignmentExpressionStatements = parentDeclaration.node.declarations
.map((declarator) => {
return declarator.init
? t.expressionStatement(
t.assignmentExpression("=", declarator.id, declarator.init)
)
: null;
})
.filter((v): v is t.ExpressionStatement => Boolean(v));

replacements = [
newDeclaration,
makeSegmentCallable(assignmentExpressionStatements),
];
} else {
replacements = [makeSegmentCallable([statement.node])];
}

const prformTransformation = () =>
replacements ? statement.replaceWithMultiple(replacements) : null;

return {
segmentCallableId,
replacements,
prformTransformation,
};
}
121 changes: 37 additions & 84 deletions packages/compiler/src/ast-factories/make-dependency-condition.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,48 @@
import * as t from "@babel/types";
import { ComponentVariable } from "~/classes/ComponentVariable";
import { isReferenceIdentifier } from "~/utils/is-reference-identifier";
import { memberExpressionToDotNotation } from "~/utils/member-expression-to-dot-notation";

export function makeDependencyCondition(componentVariable: ComponentVariable) {
const dependencies = componentVariable.getDependencies();
const isNotSetCondition =
componentVariable.getCacheIsNotSetAccessExpression();

const comparisonsMap = new Map<
string,
[babel.types.Expression, babel.types.Expression]
import { ComponentMutableSegment } from "~/classes/ComponentMutableSegment";

export function makeDependencyCondition(
mutableSegment: ComponentMutableSegment
): t.Expression | null {
const dependencies = mutableSegment.getDependencies();
const isNotSetCondition: t.Expression | null =
mutableSegment.isComponentVariable()
? mutableSegment.getCacheIsNotSetAccessExpression()
: null;

const comparisonTuples = new Set<
[left: babel.types.Expression, right: babel.types.Expression]
>();

dependencies.forEach((dependency) => {
const dependencyId = t.identifier(dependency.name);

// When the variable is used in a member expression, we should optimize comparisons to the last member of member expression as well
const path = componentVariable.binding.path;
const parentPath = path.find((p) => p.isStatement());

const references = new Set<babel.NodePath<babel.types.Identifier>>();

parentPath?.traverse({
Identifier(innerPath) {
if (isReferenceIdentifier(innerPath)) {
references.add(innerPath);
}
},
});

references.forEach((reference) => {
const refParent = reference.parentPath;
const actualDependencyForThisReference =
componentVariable.component.getComponentVariable(reference.node.name);

// The reference is not a component variable
if (!actualDependencyForThisReference) {
return;
}

const dependencyCacheValueAccessor =
actualDependencyForThisReference.getCacheValueAccessExpression();

if (refParent.isMemberExpression()) {
const memberObject = refParent.get("object");
if (
memberObject.isIdentifier() &&
memberObject.node.name === dependencyId.name
) {
const makeMemberExpressionForCheck = (id: babel.types.Expression) =>
t.memberExpression(
id,
refParent.node.property,
refParent.node.computed
);

const referenceMemberExpression = makeMemberExpressionForCheck(
reference.node
);
const id = memberExpressionToDotNotation(referenceMemberExpression);

if (!comparisonsMap.has(id)) {
const cacheMemberExpression = makeMemberExpressionForCheck(
dependencyCacheValueAccessor
);
comparisonsMap.set(id, [
referenceMemberExpression,
cacheMemberExpression,
]);
}
}
} else {
const id = memberExpressionToDotNotation(reference.node);

if (!comparisonsMap.has(id)) {
comparisonsMap.set(id, [
reference.node,
dependencyCacheValueAccessor,
]);
}
}
});
const allDependencies = [...dependencies.values()];

allDependencies.forEach((dependency) => {
const dependencyId = dependency.componentVariable
? t.identifier(dependency.componentVariable.name)
: null;

if (!dependencyId) {
return;
}

comparisonTuples.add([
dependency.getMemberExpression(
t.identifier(dependency.componentVariable.name)
),
dependency.getMemberExpression(
dependency.componentVariable.getCacheValueAccessExpression()
),
]);
});

return Array.from(comparisonsMap.values()).reduce(
return Array.from(comparisonTuples.values()).reduce(
(condition, [left, right]) => {
const binaryExpression = t.binaryExpression("!==", left, right);

return t.logicalExpression("||", condition, binaryExpression);
return condition
? t.logicalExpression("||", condition, binaryExpression)
: binaryExpression;
},
isNotSetCondition as babel.types.Expression
isNotSetCondition as babel.types.Expression | null
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ export function makeUnwrappedDeclarations(
const unwrappedEntries = unwrapPatternAssignment(id, tempVariableId);

const unwrappedDeclarations = unwrappedEntries.map((entry) => {
return t.variableDeclaration(kind, [
t.variableDeclarator(entry.id, entry.value),
]);
const binding = id.scope.getBinding(entry.name);

return [
t.variableDeclaration(kind, [
t.variableDeclarator(entry.id, entry.value),
]),
binding,
] as const;
});

return {
Expand Down
Loading