[compiler] Improved terminal/fallthrough structure, support labels

[ghstack-poisoned]
This commit is contained in:
Joe Savona
2025-01-21 15:24:24 -08:00
parent f0fb06a6df
commit 5c027d8fae
5 changed files with 474 additions and 246 deletions
@@ -844,7 +844,7 @@ export function printPlace(place: Place): string {
}
export function printIdentifier(id: Identifier): string {
return `${printName(id.name)}\$${id.id}#${id.declarationId}${printScope(id.scope)}`;
return `${printName(id.name)}\$${id.id}${printScope(id.scope)}`;
}
function printName(name: IdentifierName | null): string {
@@ -5,18 +5,27 @@
* LICENSE file in the root directory of this source tree.
*/
import prettyFormat from 'pretty-format';
import {CompilerError, SourceLocation} from '..';
import {
BlockId,
DeclarationId,
DoWhileTerminal,
ForInTerminal,
ForOfTerminal,
ForTerminal,
GotoVariant,
HIRFunction,
Identifier,
IdentifierId,
IfTerminal,
Instruction,
LabelTerminal,
Place,
ReactiveScope,
ScopeId,
SwitchTerminal,
WhileTerminal,
} from '../HIR';
import {printIdentifier, printInstruction} from '../HIR/PrintHIR';
import {
@@ -26,7 +35,6 @@ import {
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {
BranchNode,
GotoNode,
ControlNode,
EntryNode,
@@ -40,12 +48,16 @@ import {
ReactiveGraph,
ReactiveId,
ReactiveNode,
ReturnNode,
reversePostorderReactiveGraph,
StoreNode,
eachNodeDependency,
FallthroughNode,
printReactiveGraph,
IfNode,
PhiNode,
ThrowNode,
ReturnNode,
LabelNode,
} from './ReactiveIR';
export function buildReactiveGraph(fn: HIRFunction): ReactiveGraph {
@@ -131,6 +143,7 @@ class Builder {
type Fallthrough =
| {kind: 'Function'}
| {kind: 'Label'; block: BlockId; fallthrough: ReactiveId}
| {kind: 'If'; block: BlockId; fallthrough: ReactiveId};
class ControlContext {
@@ -143,6 +156,7 @@ class ControlContext {
> = new Map(),
private scopes: Map<ScopeId, ReactiveId> = new Map(),
private nodes: Set<ReactiveId> = new Set(),
private breakMapping: Map<BlockId, ReactiveId> = new Map(),
private parent: ControlContext | null = null,
) {}
@@ -157,11 +171,44 @@ class ControlContext {
*/
new Map(),
new Map(),
// Track not-yet-depended on nodes in this context so the terminal node can depend on them
new Set(),
this.breakMapping,
this,
);
}
forkValue(): ControlContext {
return new ControlContext(
this.builder,
this.fallthrough,
new Map(),
new Map(),
new Set(),
this.breakMapping,
this,
);
}
putBreakMapping(block: BlockId, node: ReactiveId): void {
this.breakMapping.set(block, node);
}
getBreakMapping(block: BlockId, loc: SourceLocation): ReactiveId {
const node = this.breakMapping.get(block);
if (node == null) {
console.log(
`No break mapping for bb${block}`,
prettyFormat(this.breakMapping),
);
}
CompilerError.invariant(node !== undefined, {
reason: `Unset break mapping for bb${block}`,
loc,
});
return node;
}
controlNode(control: ReactiveId, loc: SourceLocation): ReactiveId {
const node: ControlNode = {
kind: 'Control',
@@ -169,7 +216,6 @@ class ControlContext {
loc,
outputs: [],
control,
dependencies: [],
};
this.putNode(node);
return node.id;
@@ -204,7 +250,10 @@ class ControlContext {
}
loadBreakTarget(target: BlockId, loc: SourceLocation): ReactiveId {
if (this.fallthrough.kind === 'If' && this.fallthrough.block === target) {
if (
(this.fallthrough.kind === 'If' || this.fallthrough.kind === 'Label') &&
this.fallthrough.block === target
) {
return this.fallthrough.fallthrough;
}
if (this.parent != null) {
@@ -489,168 +538,6 @@ function buildBlockScope(
// handle the terminal
const terminal = block.terminal;
switch (terminal.kind) {
case 'if': {
const testDep = context.lookupTemporary(
terminal.test.identifier,
terminal.test.loc,
);
const test: NodeReference = {
node: testDep,
from: {...terminal.test},
as: {...terminal.test},
};
const branchNodeId = context.nextReactiveId;
const fallthroughNodeId = context.nextReactiveId;
const joinFallthrough = {
kind: 'If',
block: terminal.fallthrough,
fallthrough: fallthroughNodeId,
} as const;
const consequentContext = context.fork(joinFallthrough);
const consequentControl = consequentContext.controlNode(
branchNodeId,
terminal.loc,
);
const consequent = buildBlockScope(
fn,
consequentContext,
terminal.consequent,
consequentControl,
);
const alternateContext = context.fork(joinFallthrough);
const alternateControl = alternateContext.controlNode(
branchNodeId,
terminal.loc,
);
const alternate =
terminal.alternate !== terminal.fallthrough
? buildBlockScope(
fn,
alternateContext,
terminal.alternate,
alternateControl,
)
: alternateControl;
const branch: BranchNode = {
kind: 'Branch',
control,
dependencies: [],
id: branchNodeId,
loc: terminal.loc,
outputs: [],
fallthrough: fallthroughNodeId,
terminal: {
kind: 'If',
test,
consequent: {
entry: consequentControl,
exit: consequent,
},
alternate: {
entry: alternateControl,
exit: alternate,
},
},
};
context.putNode(branch);
const ifNode: FallthroughNode = {
kind: 'Fallthrough',
control: branch.id,
id: fallthroughNodeId,
loc: terminal.loc,
outputs: [],
branches: [consequent, alternate],
};
const predecessors: Array<{
enter: ReactiveId;
exit: ReactiveId;
context: ControlContext;
}> = [
{
enter: consequentControl,
exit: consequent,
context: consequentContext,
},
{
enter: alternateControl,
exit: alternate,
context: alternateContext,
},
];
const controlDependencies: Set<ReactiveId> = new Set();
const joinedDeclarations: Map<DeclarationId, 'write' | 'read'> =
new Map();
const joinedScopes: Set<ScopeId> = new Set();
for (const predecessorBlock of predecessors) {
/*
* track scopes that were mutated in any of the branches so that we can
* establish control depends to order the branch/join relative to previous
* subsequent mutations of those scopes
*/
for (const [scope] of predecessorBlock.context.eachScope()) {
joinedScopes.add(scope);
}
/*
* Track variables that were read/reassigned in any of the predecessors
* so that subsequent writes/reads can take the join node as a control
*/
for (const [
declarationId,
{write, read},
] of predecessorBlock.context.eachDeclaration()) {
if (write) {
joinedDeclarations.set(declarationId, 'write');
} else if (read && !joinedDeclarations.has(declarationId)) {
joinedDeclarations.set(declarationId, 'read');
}
}
}
for (const scope of joinedScopes) {
const scopeControl = context.loadScopeControl(scope);
if (scopeControl != null) {
controlDependencies.add(scopeControl);
}
context.recordScopeMutation(scope, ifNode.id);
}
// Add control dependencies and record reads/writes accordingly.
for (const [declarationId, declType] of joinedDeclarations) {
if (declType === 'write') {
/*
* If there was a write in any of the branches, we take a write
* dependency (on the last read/write) and record the if as the
* last write
*/
const writeControl = context.loadDeclarationControlForWrite(
declarationId,
terminal.loc,
);
if (writeControl != null) {
controlDependencies.add(writeControl);
}
context.recordDeclarationWrite(declarationId, ifNode.id);
} else {
/*
* If there were only reads in the branches, we take a read
* dependency (on the last write) and record the if as the
* last read
*/
const readControl =
context.loadDeclarationControlForRead(declarationId);
if (readControl != null) {
controlDependencies.add(readControl);
}
context.recordDeclarationRead(declarationId, ifNode.id);
}
}
branch.dependencies = Array.from(controlDependencies);
context.putNode(ifNode);
lastNode = ifNode.id;
break;
}
case 'return': {
const valueDep = context.lookupTemporary(
terminal.value.identifier,
@@ -676,6 +563,31 @@ function buildBlockScope(
lastNode = returnNode.id;
break;
}
case 'throw': {
const valueDep = context.lookupTemporary(
terminal.value.identifier,
terminal.value.loc,
);
const value: NodeReference = {
node: valueDep,
from: {...terminal.value},
as: {...terminal.value},
};
const throwNode: ThrowNode = {
kind: 'Throw',
id: context.nextReactiveId,
loc: terminal.loc,
outputs: [],
value,
dependencies: context
.uncontolledNodes()
.filter(id => id !== valueDep),
control,
};
context.putNode(throwNode);
lastNode = throwNode.id;
break;
}
case 'goto': {
let target: ReactiveId;
switch (terminal.variant) {
@@ -711,15 +623,50 @@ function buildBlockScope(
control,
};
context.putNode(node);
context.putBreakMapping(block.id, node.id);
lastNode = node.id;
break;
}
default: {
CompilerError.throwTodo({
reason: `Support ${terminal.kind} nodes`,
case 'unreachable': {
CompilerError.invariant(false, {
reason: `Found unreachable code`,
loc: terminal.loc,
});
}
case 'unsupported': {
CompilerError.invariant(false, {
reason: `Found unsupported terminal`,
loc: terminal.loc,
});
}
case 'scope':
case 'pruned-scope': {
CompilerError.throwTodo({
reason: `Support scopes`,
loc: terminal.loc,
});
}
case 'branch':
case 'logical':
case 'ternary':
case 'sequence':
case 'optional': {
CompilerError.throwTodo({
reason: `Support value blocks`,
loc: terminal.loc,
});
}
case 'try':
case 'maybe-throw': {
CompilerError.throwTodo({
reason: `Support try/catch`,
loc: terminal.loc,
});
}
default: {
lastNode = buildTerminal(fn, terminal, context, control);
break;
}
}
// Continue iteration in the fallthrough
@@ -733,6 +680,231 @@ function buildBlockScope(
return lastNode;
}
function buildTerminal(
fn: HIRFunction,
terminal:
| DoWhileTerminal
| ForInTerminal
| ForOfTerminal
| ForTerminal
| IfTerminal
| LabelTerminal
| SwitchTerminal
| WhileTerminal,
context: ControlContext,
control: ReactiveId,
): ReactiveId {
let branches: Array<{
enter: ReactiveId;
exit: ReactiveId;
context: ControlContext;
}>;
const fallthroughNodeId = context.nextReactiveId;
let terminalNode: ReactiveNode;
switch (terminal.kind) {
case 'if': {
const testDep = context.lookupTemporary(
terminal.test.identifier,
terminal.test.loc,
);
const test: NodeReference = {
node: testDep,
from: {...terminal.test},
as: {...terminal.test},
};
const ifNodeId = context.nextReactiveId;
const joinFallthrough = {
kind: 'If',
block: terminal.fallthrough,
fallthrough: fallthroughNodeId,
} as const;
const consequentContext = context.fork(joinFallthrough);
const consequentControl = consequentContext.controlNode(
ifNodeId,
terminal.loc,
);
const consequent = buildBlockScope(
fn,
consequentContext,
terminal.consequent,
consequentControl,
);
const alternateContext = context.fork(joinFallthrough);
const alternateControl = alternateContext.controlNode(
ifNodeId,
terminal.loc,
);
const alternate =
terminal.alternate !== terminal.fallthrough
? buildBlockScope(
fn,
alternateContext,
terminal.alternate,
alternateControl,
)
: alternateControl;
const node: IfNode = {
kind: 'If',
control,
dependencies: [],
id: ifNodeId,
loc: terminal.loc,
outputs: [],
fallthrough: fallthroughNodeId,
test,
consequent: {
entry: consequentControl,
exit: consequent,
},
alternate: {
entry: alternateControl,
exit: alternate,
},
};
context.putNode(node);
branches = [
{
enter: consequentControl,
exit: consequent,
context: consequentContext,
},
{
enter: alternateControl,
exit: alternate,
context: alternateContext,
},
];
terminalNode = node;
break;
}
case 'label': {
const blockContext = context.fork({
kind: 'Label',
block: terminal.fallthrough,
fallthrough: fallthroughNodeId,
});
const labelNodeId = context.nextReactiveId;
const blockControl = blockContext.controlNode(labelNodeId, terminal.loc);
const block = buildBlockScope(
fn,
blockContext,
terminal.block,
blockControl,
);
const node: LabelNode = {
kind: 'Label',
block: {entry: blockControl, exit: block},
control,
id: labelNodeId,
loc: terminal.loc,
outputs: [],
dependencies: [],
};
context.putNode(node);
branches = [{enter: blockControl, exit: block, context: blockContext}];
terminalNode = node;
break;
}
default: {
assertExhaustive(
terminal /* TODO */ as never,
`Unexpected terminal kind '${(terminal as any).kind}'`,
);
}
}
const fallthrough = fn.body.blocks.get(terminal.fallthrough)!;
const phis: Array<PhiNode> = Array.from(fallthrough.phis).map(phi => {
return {
kind: 'Phi',
place: phi.place,
operands: new Map(
Array.from(phi.operands, ([blockId, operand]) => {
return [context.getBreakMapping(blockId, phi.place.loc), operand];
}),
),
};
});
const fallthroughNode: FallthroughNode = {
kind: 'Fallthrough',
control: terminalNode.id,
id: fallthroughNodeId,
loc: terminal.loc,
outputs: [],
branches: branches.map(pred => pred.exit),
phis,
};
context.putNode(fallthroughNode);
const controlDependencies: Set<ReactiveId> = new Set();
const joinedDeclarations: Map<DeclarationId, 'write' | 'read'> = new Map();
const joinedScopes: Set<ScopeId> = new Set();
for (const predecessorBlock of branches) {
/*
* track scopes that were mutated in any of the branches so that we can
* establish control depends to order the branch/join relative to previous
* subsequent mutations of those scopes
*/
for (const [scope] of predecessorBlock.context.eachScope()) {
joinedScopes.add(scope);
}
/*
* Track variables that were read/reassigned in any of the predecessors
* so that subsequent writes/reads can take the join node as a control
*/
for (const [
declarationId,
{write, read},
] of predecessorBlock.context.eachDeclaration()) {
if (write) {
joinedDeclarations.set(declarationId, 'write');
} else if (read && !joinedDeclarations.has(declarationId)) {
joinedDeclarations.set(declarationId, 'read');
}
}
}
for (const scope of joinedScopes) {
const scopeControl = context.loadScopeControl(scope);
if (scopeControl != null) {
controlDependencies.add(scopeControl);
}
context.recordScopeMutation(scope, fallthroughNode.id);
}
// Add control dependencies and record reads/writes accordingly.
for (const [declarationId, declType] of joinedDeclarations) {
if (declType === 'write') {
/*
* If there was a write in any of the branches, we take a write
* dependency (on the last read/write) and record the if as the
* last write
*/
const writeControl = context.loadDeclarationControlForWrite(
declarationId,
terminal.loc,
);
if (writeControl != null) {
controlDependencies.add(writeControl);
}
context.recordDeclarationWrite(declarationId, fallthroughNode.id);
} else {
/*
* If there were only reads in the branches, we take a read
* dependency (on the last write) and record the if as the
* last read
*/
const readControl = context.loadDeclarationControlForRead(declarationId);
if (readControl != null) {
controlDependencies.add(readControl);
}
context.recordDeclarationRead(declarationId, fallthroughNode.id);
}
}
terminalNode.dependencies = Array.from(controlDependencies);
return fallthroughNodeId;
}
function getScopeForInstruction(instr: Instruction): ReactiveScope | null {
let scope: ReactiveScope | null = null;
for (const operand of eachInstructionValueOperand(instr.value)) {
@@ -52,16 +52,19 @@ export function makeReactiveId(id: number): ReactiveId {
}
export type ReactiveNode =
| EntryNode
| LoadNode
| StoreNode
| LoadArgumentNode
| InstructionNode
| BranchNode
| FallthroughNode
| ControlNode
| EntryNode
| FallthroughNode
| GotoNode
| IfNode
| InstructionNode
| LabelNode
| LoadArgumentNode
| LoadNode
| OptionalNode
| ReturnNode
| GotoNode;
| StoreNode
| ThrowNode;
export type NodeReference = {
node: ReactiveId;
@@ -129,6 +132,16 @@ export type ReturnNode = {
control: ReactiveId;
};
export type ThrowNode = {
kind: 'Throw';
id: ReactiveId;
loc: SourceLocation;
value: NodeReference;
outputs: Array<ReactiveId>;
dependencies: Array<ReactiveId>;
control: ReactiveId;
};
export type GotoNode = {
kind: 'Goto';
id: ReactiveId;
@@ -140,24 +153,27 @@ export type GotoNode = {
variant: GotoVariant;
};
export type BranchNode = {
kind: 'Branch';
export type LabelNode = {
kind: 'Label';
id: ReactiveId;
loc: SourceLocation;
outputs: Array<ReactiveId>;
dependencies: Array<ReactiveId>;
control: ReactiveId;
block: {entry: ReactiveId; exit: ReactiveId};
};
export type IfNode = {
kind: 'If';
id: ReactiveId;
loc: SourceLocation;
outputs: Array<ReactiveId>;
dependencies: Array<ReactiveId>; // values/scopes depended on by more than one branch, or by the terminal
control: ReactiveId;
fallthrough: ReactiveId;
terminal: BranchTerminal;
};
export type BranchTerminal = IfBranch;
export type IfBranch = {
kind: 'If';
test: NodeReference;
consequent: {entry: ReactiveId; exit: ReactiveId};
alternate: {entry: ReactiveId; exit: ReactiveId};
fallthrough: ReactiveId;
};
export type FallthroughNode = {
@@ -167,6 +183,13 @@ export type FallthroughNode = {
outputs: Array<ReactiveId>;
control: ReactiveId; // always the corresponding branch node
branches: Array<ReactiveId>; // the other control-flow paths that reach the fallthrough
phis: Array<PhiNode>;
};
export type PhiNode = {
kind: 'Phi';
place: Place;
operands: Map<ReactiveId, Place>;
};
export type ControlNode = {
@@ -174,7 +197,17 @@ export type ControlNode = {
id: ReactiveId;
loc: SourceLocation;
outputs: Array<ReactiveId>;
dependencies: Array<ReactiveId>;
control: ReactiveId;
};
export type OptionalNode = {
kind: 'Optional';
id: ReactiveId;
loc: SourceLocation;
outputs: Array<ReactiveId>;
object: NodeReference;
continuation: NodeReference;
optional: boolean;
control: ReactiveId;
};
@@ -241,30 +274,21 @@ export function reversePostorderReactiveGraph(graph: ReactiveGraph): void {
graph.nodes = nodes;
}
export function* eachBranchTerminalDependency(
terminal: BranchTerminal,
): Iterable<ReactiveId> {
switch (terminal.kind) {
case 'If': {
yield terminal.test.node;
}
}
}
export function* eachNodeDependency(node: ReactiveNode): Iterable<ReactiveId> {
switch (node.kind) {
case 'Control':
case 'Entry':
case 'LoadArgument': {
break;
}
case 'Goto':
case 'Control': {
case 'Label':
case 'Goto': {
yield* node.dependencies;
break;
}
case 'Branch': {
case 'If': {
yield* node.dependencies;
yield* eachBranchTerminalDependency(node.terminal);
yield node.test.node;
break;
}
case 'Fallthrough': {
@@ -279,13 +303,19 @@ export function* eachNodeDependency(node: ReactiveNode): Iterable<ReactiveId> {
yield node.value.node;
break;
}
case 'Throw':
case 'Return': {
yield* node.dependencies;
yield node.value.node;
break;
}
case 'Value': {
yield* [...node.dependencies.keys()];
yield* node.dependencies.keys();
break;
}
case 'Optional': {
yield node.object.node;
yield node.continuation.node;
break;
}
default: {
@@ -297,21 +327,11 @@ export function* eachNodeDependency(node: ReactiveNode): Iterable<ReactiveId> {
}
}
export function* eachBranchTerminalReference(
terminal: BranchTerminal,
): Iterable<NodeReference> {
switch (terminal.kind) {
case 'If': {
yield terminal.test;
break;
}
}
}
export function* eachNodeReference(
node: ReactiveNode,
): Iterable<NodeReference> {
switch (node.kind) {
case 'Label':
case 'Goto':
case 'Entry':
case 'Control':
@@ -326,12 +346,13 @@ export function* eachNodeReference(
yield node.value;
break;
}
case 'Throw':
case 'Return': {
yield node.value;
break;
}
case 'Branch': {
yield* eachBranchTerminalReference(node.terminal);
case 'If': {
yield node.test;
break;
}
case 'Fallthrough': {
@@ -345,6 +366,11 @@ export function* eachNodeReference(
}));
break;
}
case 'Optional': {
yield node.object;
yield node.continuation;
break;
}
default: {
assertExhaustive(node, `Unexpected node kind '${(node as any).kind}'`);
}
@@ -416,9 +442,7 @@ function writeReactiveNodes(
break;
}
case 'Control': {
buffer.push(
`£${id} Control${control} deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]`,
);
buffer.push(`£${id} Control${control}`);
break;
}
case 'Load': {
@@ -431,35 +455,48 @@ function writeReactiveNodes(
);
break;
}
case 'Throw': {
buffer.push(
`£${id} Throw ${printNodeReference(node.value)} deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]${control}`,
);
break;
}
case 'Return': {
buffer.push(
`£${id} Return ${printNodeReference(node.value)} deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]${control}`,
);
break;
}
case 'Branch': {
case 'Label': {
buffer.push(
`£${id} Branch deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]${control}`,
`£${id} Label block=£${node.block.entry}:${node.block.exit} deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]${control}`,
);
break;
}
case 'If': {
buffer.push(`£${id} If test=${printNodeReference(node.test)}`);
buffer.push(
` consequent=£${node.consequent.entry}:${node.consequent.exit} `,
);
buffer.push(
` alternate=£${node.alternate.entry}:${node.alternate.exit} `,
);
buffer.push(
` deps=[${node.dependencies.map(id => `£${id}`).join(', ')}]${control}`,
);
switch (node.terminal.kind) {
case 'If': {
buffer.push(
` If test=${printNodeReference(node.terminal.test)} ` +
`consequent=£${node.terminal.consequent.entry}:${node.terminal.consequent.exit} ` +
`alternate=£${node.terminal.alternate.entry}:${node.terminal.alternate.exit}`,
);
break;
}
default: {
// assertExhaustive(node.terminal, `Unsupported terminal kind ${(node.terminal as any).kind}`);
}
}
break;
}
case 'Fallthrough': {
buffer.push(
`£${id} Fallthrough${control} branches=[${node.branches.map(id => `£${id}`).join(', ')}]`,
);
for (const phi of node.phis) {
const operands = [];
for (const [pred, operand] of phi.operands) {
operands.push(`£${pred} => ${printPlace(operand)}`);
}
buffer.push(` ${printPlace(phi.place)} => [${operands.join(', ')}]`);
}
break;
}
case 'Value': {
@@ -470,6 +507,13 @@ function writeReactiveNodes(
buffer.push(' ' + printInstruction(node.value));
break;
}
case 'Optional': {
buffer.push(
`£${id} Optional ${printNodeReference(node.object)} ` +
`${node.optional ? '?.' : '.'} ${printNodeReference(node.continuation)} ${control}`,
);
break;
}
default: {
assertExhaustive(node, `Unexpected node kind ${(node as any).kind}`);
}
@@ -6,8 +6,12 @@
function Component(props) {
let element = props.default;
let other = element;
if (props.cond) {
element = <div></div>;
label: if (props.cond) {
if (props.ret) {
break label;
} else {
element = <div></div>;
}
} else {
element = <span></span>;
}
@@ -24,15 +28,19 @@ function Component(props) {
const $ = _c(5);
let element = props.default;
const other = element;
if (props.cond) {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div />;
$[0] = t0;
bb0: if (props.cond) {
if (props.ret) {
break bb0;
} else {
t0 = $[0];
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div />;
$[0] = t0;
} else {
t0 = $[0];
}
element = t0;
}
element = t0;
} else {
let t0;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
@@ -2,8 +2,12 @@
function Component(props) {
let element = props.default;
let other = element;
if (props.cond) {
element = <div></div>;
label: if (props.cond) {
if (props.ret) {
break label;
} else {
element = <div></div>;
}
} else {
element = <span></span>;
}