Skip to content

[compiler] Allow macro methods #30589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 12, 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
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ export const InstrumentationSchema = z

export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;

export const MacroMethodSchema = z.union([
z.object({type: z.literal('wildcard')}),
z.object({type: z.literal('name'), name: z.string()}),
]);

// Would like to change this to drop the string option, but breaks compatibility with existing configs
export const MacroSchema = z.union([
z.string(),
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);

export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;

const HookSchema = z.object({
/*
* The effect of arguments to this hook. Describes whether the hook may or may
Expand Down Expand Up @@ -133,7 +147,7 @@ const EnvironmentConfigSchema = z.object({
* plugin since it looks specifically for the name of the function being invoked, not
* following aliases.
*/
customMacros: z.nullable(z.array(z.string())).default(null),
customMacros: z.nullable(z.array(MacroSchema)).default(null),

/**
* Enable a check that resets the memoization cache when the source code of the file changes.
Expand Down Expand Up @@ -490,7 +504,19 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
}

if (key === 'customMacros' && val) {
maybeConfig[key] = [val];
const valSplit = val.split('.');
if (valSplit.length > 0) {
const props = [];
for (const elt of valSplit.slice(1)) {
if (elt === '*') {
props.push({type: 'wildcard'});
} else if (elt.length > 0) {
props.push({type: 'name', name: elt});
}
}
console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanup

maybeConfig[key] = [[valSplit[0], props]];
}
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Place,
ReactiveValue,
} from '../HIR';
import {Macro, MacroMethod} from '../HIR/Environment';
import {eachReactiveValueOperand} from './visitors';

/**
Expand Down Expand Up @@ -43,15 +44,17 @@ import {eachReactiveValueOperand} from './visitors';
export function memoizeFbtAndMacroOperandsInSameScope(
fn: HIRFunction,
): Set<IdentifierId> {
const fbtMacroTags = new Set([
...FBT_TAGS,
const fbtMacroTags = new Set<Macro>([
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
...(fn.env.config.customMacros ?? []),
]);
const fbtValues: Set<IdentifierId> = new Set();
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
while (true) {
let size = fbtValues.size;
visit(fn, fbtMacroTags, fbtValues);
if (size === fbtValues.size) {
let vsize = fbtValues.size;
let msize = macroMethods.size;
visit(fn, fbtMacroTags, fbtValues, macroMethods);
if (vsize === fbtValues.size && msize === macroMethods.size) {
break;
}
}
Expand All @@ -71,8 +74,9 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([

function visit(
fn: HIRFunction,
fbtMacroTags: Set<string>,
fbtMacroTags: Set<Macro>,
fbtValues: Set<IdentifierId>,
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
): void {
for (const [, block] of fn.body.blocks) {
for (const instruction of block.instructions) {
Expand All @@ -83,7 +87,7 @@ function visit(
if (
value.kind === 'Primitive' &&
typeof value.value === 'string' &&
fbtMacroTags.has(value.value)
matchesExactTag(value.value, fbtMacroTags)
) {
/*
* We don't distinguish between tag names and strings, so record
Expand All @@ -92,10 +96,38 @@ function visit(
fbtValues.add(lvalue.identifier.id);
} else if (
value.kind === 'LoadGlobal' &&
fbtMacroTags.has(value.binding.name)
matchesExactTag(value.binding.name, fbtMacroTags)
) {
// Record references to `fbt` as a global
fbtValues.add(lvalue.identifier.id);
} else if (
value.kind === 'LoadGlobal' &&
matchTagRoot(value.binding.name, fbtMacroTags) !== null
) {
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
macroMethods.set(lvalue.identifier.id, methods);
} else if (
value.kind === 'PropertyLoad' &&
macroMethods.has(value.object.identifier.id)
) {
const methods = macroMethods.get(value.object.identifier.id)!;
const newMethods = [];
for (const method of methods) {
if (
method.length > 0 &&
(method[0].type === 'wildcard' ||
(method[0].type === 'name' && method[0].name === value.property))
) {
if (method.length > 1) {
newMethods.push(method.slice(1));
} else {
fbtValues.add(lvalue.identifier.id);
}
}
}
if (newMethods.length > 0) {
macroMethods.set(lvalue.identifier.id, newMethods);
}
} else if (isFbtCallExpression(fbtValues, value)) {
const fbtScope = lvalue.identifier.scope;
if (fbtScope === null) {
Expand Down Expand Up @@ -167,25 +199,57 @@ function visit(
}
}

function matchesExactTag(s: string, tags: Set<Macro>): boolean {
return Array.from(tags).some(macro =>
typeof macro === 'string'
? s === macro
: macro[1].length === 0 && macro[0] === s,
);
}

function matchTagRoot(
s: string,
tags: Set<Macro>,
): Array<Array<MacroMethod>> | null {
const methods: Array<Array<MacroMethod>> = [];
for (const macro of tags) {
if (typeof macro === 'string') {
continue;
}
Comment on lines +216 to +218
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok, so this is specifically for the case where we're loading a macro that has sub-properties we care about. other macros don't care about property loads

const [tag, rest] = macro;
if (tag === s && rest.length > 0) {
methods.push(rest);
}
}
if (methods.length > 0) {
return methods;
} else {
return null;
}
}

function isFbtCallExpression(
fbtValues: Set<IdentifierId>,
value: ReactiveValue,
): boolean {
return (
value.kind === 'CallExpression' && fbtValues.has(value.callee.identifier.id)
(value.kind === 'CallExpression' &&
fbtValues.has(value.callee.identifier.id)) ||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
);
}

function isFbtJsxExpression(
fbtMacroTags: Set<string>,
fbtMacroTags: Set<Macro>,
fbtValues: Set<IdentifierId>,
value: ReactiveValue,
): boolean {
return (
value.kind === 'JsxExpression' &&
((value.tag.kind === 'Identifier' &&
fbtValues.has(value.tag.identifier.id)) ||
(value.tag.kind === 'BuiltinTag' && fbtMacroTags.has(value.tag.name)))
(value.tag.kind === 'BuiltinTag' &&
matchesExactTag(value.tag.name, fbtMacroTags)))
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

## Input

```javascript
// @customMacros(idx.*.b)

function Component(props) {
// outlined
const groupName1 = idx(props, _ => _.group.label);
// outlined
const groupName2 = idx.a(props, _ => _.group.label);
// not outlined
const groupName3 = idx.a.b(props, _ => _.group.label);
// not outlined
const groupName4 = idx.hello_world.b(props, _ => _.group.label);
// outlined
const groupName5 = idx.hello_world.b.c(props, _ => _.group.label);
return (
<div>
{groupName1}
{groupName2}
{groupName3}
{groupName4}
{groupName5}
</div>
);
}

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime"; // @customMacros(idx.*.b)

function Component(props) {
const $ = _c(16);
let t0;
if ($[0] !== props) {
t0 = idx(props, _temp);
$[0] = props;
$[1] = t0;
} else {
t0 = $[1];
}
const groupName1 = t0;
let t1;
if ($[2] !== props) {
t1 = idx.a(props, _temp2);
$[2] = props;
$[3] = t1;
} else {
t1 = $[3];
}
const groupName2 = t1;
let t2;
if ($[4] !== props) {
t2 = idx.a.b(props, (__1) => __1.group.label);
$[4] = props;
$[5] = t2;
} else {
t2 = $[5];
}
const groupName3 = t2;
let t3;
if ($[6] !== props) {
t3 = idx.hello_world.b(props, (__2) => __2.group.label);
$[6] = props;
$[7] = t3;
} else {
t3 = $[7];
}
const groupName4 = t3;
let t4;
if ($[8] !== props) {
t4 = idx.hello_world.b.c(props, _temp3);
$[8] = props;
$[9] = t4;
} else {
t4 = $[9];
}
const groupName5 = t4;
let t5;
if (
$[10] !== groupName1 ||
$[11] !== groupName2 ||
$[12] !== groupName3 ||
$[13] !== groupName4 ||
$[14] !== groupName5
) {
t5 = (
<div>
{groupName1}
{groupName2}
{groupName3}
{groupName4}
{groupName5}
</div>
);
$[10] = groupName1;
$[11] = groupName2;
$[12] = groupName3;
$[13] = groupName4;
$[14] = groupName5;
$[15] = t5;
} else {
t5 = $[15];
}
return t5;
}
function _temp3(__3) {
return __3.group.label;
}
function _temp2(__0) {
return __0.group.label;
}
function _temp(_) {
return _.group.label;
}

```

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @customMacros(idx.*.b)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a test case where the wildcard is at the end? i'm assuming that idx.a.* would match both idx.a.b and idx.a.c.d (?)


function Component(props) {
// outlined
const groupName1 = idx(props, _ => _.group.label);
// outlined
const groupName2 = idx.a(props, _ => _.group.label);
// not outlined
const groupName3 = idx.a.b(props, _ => _.group.label);
// not outlined
const groupName4 = idx.hello_world.b(props, _ => _.group.label);
// outlined
const groupName5 = idx.hello_world.b.c(props, _ => _.group.label);
return (
<div>
{groupName1}
{groupName2}
{groupName3}
{groupName4}
{groupName5}
</div>
);
}
Loading