ForInStatement: specialize inference/types

Replaces the use of `NextIterableOf` in for-in with a new `NextPropertyOf` 
instruction. The key distinction is `for-of` invokes an arbitrary iterator, 
which means a) each iteration may mutate the collection being iterated and b) 
the returned value may be mutable. However, `for-in` invokes a language-level 
mechanism to iterate: simply iterating alone _cannot_ modify the collection, and 
the returned value is known to be a primitive.
This commit is contained in:
Joe Savona
2023-09-11 16:54:05 -07:00
parent 51497c8c59
commit ade3235ca3
12 changed files with 104 additions and 6 deletions
@@ -943,7 +943,7 @@ function lowerStatement(
initBlock
);
// The init of a ForOf statement is compound over a left (VariableDeclaration | LVal) and
// The init of a ForIn statement is compound over a left (VariableDeclaration | LVal) and
// right (Expression), so we synthesize a new InstrValue and assignment (potentially multiple
// instructions when we handle other syntax like Patterns)
const left = stmt.get("left");
@@ -952,14 +952,14 @@ function lowerStatement(
if (left.isVariableDeclaration()) {
const declarations = left.get("declarations");
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
description: null,
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get("id");
const nextIterableOf = lowerValueToTemporary(builder, {
kind: "NextIterableOf", // TODO: change this to reflect for-in semantics (returns immutable keys, does not modify collection)
kind: "NextPropertyOf",
loc: leftLoc,
value,
});
@@ -973,7 +973,7 @@ function lowerStatement(
test = lowerValueToTemporary(builder, assign);
} else {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForOfStatement`,
reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForInStatement`,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
@@ -788,6 +788,11 @@ export type InstructionValue =
value: Place; // the collection
loc: SourceLocation;
}
| {
kind: "NextPropertyOf";
value: Place; // the collection
loc: SourceLocation;
}
// Models a prefix update expression such as --x or ++y
// This instructions increments or decrements the <lvalue>
// but evaluates to the value of <value> prior to the update.
@@ -542,6 +542,10 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
value = `NextIterableOf ${printPlace(instrValue.value)}`;
break;
}
case "NextPropertyOf": {
value = `NextPropertyOf ${printPlace(instrValue.value)}`;
break;
}
case "Debugger": {
value = `Debugger`;
break;
@@ -193,6 +193,10 @@ export function* eachInstructionValueOperand(
yield instrValue.value;
break;
}
case "NextPropertyOf": {
yield instrValue.value;
break;
}
case "PostfixUpdate":
case "PrefixUpdate": {
yield instrValue.value;
@@ -476,6 +480,10 @@ export function mapInstructionOperands(
instrValue.value = fn(instrValue.value);
break;
}
case "NextPropertyOf": {
instrValue.value = fn(instrValue.value);
break;
}
case "PostfixUpdate":
case "PrefixUpdate": {
instrValue.value = fn(instrValue.value);
@@ -1020,6 +1020,12 @@ function inferBlock(
valueKind = ValueKind.Mutable;
break;
}
case "NextPropertyOf": {
effectKind = Effect.Read;
lvalueEffect = Effect.Store;
valueKind = ValueKind.Immutable;
break;
}
default: {
assertExhaustive(instrValue, "Unexpected instruction kind");
}
@@ -217,9 +217,11 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
// Potentially safe to prune, since they should just be creating new values
return false;
}
case "NextPropertyOf":
case "NextIterableOf": {
// Technically a NextIterableOf will never be unused because it's always used later by
// another StoreLocal or Destructure instruction, but conceptually we can't prune
// Technically a NextIterableOf/NextPropertyOf will never be unused because it's
// always used later by another StoreLocal or Destructure instruction, but conceptually
// we can't prune
return false;
}
case "LoadContext":
@@ -1212,6 +1212,10 @@ function codegenInstructionValue(
value = codegenPlace(cx, instrValue.value);
break;
}
case "NextPropertyOf": {
value = codegenPlace(cx, instrValue.value);
break;
}
case "PostfixUpdate": {
value = t.updateExpression(
instrValue.operation,
@@ -244,6 +244,7 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
case "TemplateLiteral":
case "Primitive":
case "NextIterableOf":
case "NextPropertyOf":
case "Debugger": {
return false;
}
@@ -430,6 +430,7 @@ function computeMemoizationInputs(
rvalues: value.children,
};
}
case "NextPropertyOf":
case "Debugger":
case "ComputedDelete":
case "PropertyDelete":
@@ -260,6 +260,11 @@ function* generateInstructionTypes(
break;
}
case "NextPropertyOf": {
yield equation(left, { kind: "Primitive" });
break;
}
case "DeclareLocal":
case "NewExpression":
case "JsxExpression":
@@ -0,0 +1,46 @@
## Input
```javascript
const { identity, mutate } = require("shared-runtime");
function Component(props) {
let x;
const object = { ...props.value };
for (const y in object) {
x = y;
}
mutate(x); // can't modify, x is known primitive!
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: { a: "a", b: "B", c: "C!" } }],
};
```
## Code
```javascript
const { identity, mutate } = require("shared-runtime");
function Component(props) {
let x;
const object = { ...props.value };
for (const y in object) {
x = y;
}
mutate(x);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: { a: "a", b: "B", c: "C!" } }],
};
```
@@ -0,0 +1,16 @@
const { identity, mutate } = require("shared-runtime");
function Component(props) {
let x;
const object = { ...props.value };
for (const y in object) {
x = y;
}
mutate(x); // can't modify, x is known primitive!
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: { a: "a", b: "B", c: "C!" } }],
};