From 3d4130140ab6c0dbf88a6898e77a562442d12892 Mon Sep 17 00:00:00 2001 From: Joseph Savona Date: Wed, 12 Oct 2022 10:54:09 -0700 Subject: [PATCH] [new-arch] Scaffolding and fixtures for mutability lifetime inference Scaffolds out mutable lifetime inference and the potential two-pass approach, with motivating examples. Also adds some fixtures that collectively demonstrate a bunch of cases of aliasing: * direct assignment `a = b` * property assignment `a.b = b` * array literals `a = [b]` * object literals `a = {b}` * mutable arguments to the same call `foo(mut a, mut b)` * return values aliasing arguments `a = foo(mut b)` * aliasing that occurs only after multiple loop iterations All of these fixtures use an empty `if (varName) {}` as a way to check that an otherwise readonly usage of a variable is correctly inferred as mutable. --- .../forget/src/HIR/InferMutableLifetimes.ts | 52 +++++ .../hir/mutable-lifetime-loops.expect.md | 119 ++++++++++ .../fixtures/hir/mutable-lifetime-loops.js | 34 +++ .../mutable-lifetime-with-aliasing.expect.md | 221 ++++++++++++++++++ .../hir/mutable-lifetime-with-aliasing.js | 30 +++ 5 files changed, 456 insertions(+) create mode 100644 compiler/forget/src/HIR/InferMutableLifetimes.ts create mode 100644 compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.js create mode 100644 compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.js diff --git a/compiler/forget/src/HIR/InferMutableLifetimes.ts b/compiler/forget/src/HIR/InferMutableLifetimes.ts new file mode 100644 index 0000000000..da282034f6 --- /dev/null +++ b/compiler/forget/src/HIR/InferMutableLifetimes.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { HIRFunction } from "./HIR"; + +/** + * For each usage of a value in the given function, determines if the usage + * may be succeeded by a mutable usage of that same value and if so updates + * the usage to be mutable. + * + * Stated differently, this inference ensures that inferred capabilities of + * each reference are as follows: + * - freeze: the value is frozen at this point + * - readonly: the value is not modified at this point *or any subsequent + * point* + * - mutable: the value is modified at this point *or some subsequent point*. + * + * Note that this refines the capabilities inferered by InferReferenceCapability, + * which looks at individual references and not the lifetime of a value's mutability. + * + * == Algorithm + * + * 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability + * which only tracks aliasing of top-level variables (`y = x`), this analysis needs + * to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks + * all possible locations which may have aliased a value. The concrete result is + * a mapping of each Place to the set of possibly-mutable values it may alias. + * + * ``` + * const x = []; // {x: v0; v0: mutable []} + * const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []} + * y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]} + * read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]} + * mutate(y); // can infer that y mutates v0 and v1 + * ``` + * + * 2. Backward data-flow analysis to compute mutability liveness. Walk backwards over + * the CFG and track which values are mutated in a successor. Then when visiting + * preceding statements, mark any reference to a value that is known mutated as + * mutable. + * + * ``` + * mutate(y); // mutable y => v0, v1 mutated + * read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here + * ... + * ``` + */ +function inferMutableLifetimes(fn: HIRFunction) {} diff --git a/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md new file mode 100644 index 0000000000..b44e4fdf09 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md @@ -0,0 +1,119 @@ + +## Input + +```javascript +function mutate(x, y) {} +function cond(x) {} + +function Component(props) { + let a = {}; + let b = {}; + let c = {}; + let d = {}; + while (true) { + let z = a; + a = b; + b = c; + c = d; + d = z; + mutate(a, b); + if (cond(a)) { + break; + } + } + + // all of these tests are seemingly readonly, since the values are never directly + // mutated again. but they are all aliased by `d`, which is later modified, and + // these are therefore mutable references: + if (a) { + } + if (b) { + } + if (c) { + } + if (d) { + } + + mutate(d, null); +} + +``` + +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function mutate$0(x$1, y$2) { + return; +} + +``` +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function cond$0(x$1) { + return; +} + +``` +## HIR + +``` +bb0: + Let mutable a$2 = Object { } + Let mutable b$3 = Object { } + Let mutable c$4 = Object { } + Let mutable d$5 = Object { } + Goto bb1 +bb1: + Const mutable $10 = true + If (readonly $10) then:bb3 else:bb2 +bb3: + Let mutable z$6 = readonly a$2 + Reassign mutable a$2 = readonly b$3 + Reassign mutable b$3 = readonly c$4 + Reassign mutable c$4 = readonly d$5 + Reassign mutable d$5 = readonly z$6 + Call mutable mutate$7(mutable a$2, mutable b$3) + Const mutable $9 = Call mutable cond$8(mutable a$2) + If (readonly $9) then:bb2 else:bb1 +bb2: + If (readonly a$2) then:bb7 else:bb7 +bb7: + If (readonly b$3) then:bb9 else:bb9 +bb9: + If (readonly c$4) then:bb11 else:bb11 +bb11: + If (readonly d$5) then:bb13 else:bb13 +bb13: + Const mutable $11 = null + Call mutable mutate$7(mutable d$5, readonly $11) + Return +``` + +## Code + +```javascript +function Component$0(props$1) { + let a$2 = {}; + let b$3 = {}; + let c$4 = {}; + let d$5 = {}; + ("<>"); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.js b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.js new file mode 100644 index 0000000000..c59249e543 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.js @@ -0,0 +1,34 @@ +function mutate(x, y) {} +function cond(x) {} + +function Component(props) { + let a = {}; + let b = {}; + let c = {}; + let d = {}; + while (true) { + let z = a; + a = b; + b = c; + c = d; + d = z; + mutate(a, b); + if (cond(a)) { + break; + } + } + + // all of these tests are seemingly readonly, since the values are never directly + // mutated again. but they are all aliased by `d`, which is later modified, and + // these are therefore mutable references: + if (a) { + } + if (b) { + } + if (c) { + } + if (d) { + } + + mutate(d, null); +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.expect.md b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.expect.md new file mode 100644 index 0000000000..4b2c95a6ac --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.expect.md @@ -0,0 +1,221 @@ + +## Input + +```javascript +function mutate(x, y) {} + +function Component(props) { + const a = {}; + const b = [a]; // array elements alias + const c = {}; + const d = { c }; // object values alias + + // capture all the values into this object + const x = {}; + x.b = b; + const y = mutate(x, d); // mutation aliases the arg and return value + + // all of these tests are seemingly readonly, since the values are never directly + // mutated again. but they are all aliased by `x`, which is later modified, and + // these are therefore mutable references: + if (a) { + } + if (b) { + } + if (c) { + } + if (d) { + } + if (y) { + } + + // could in theory mutate any of a/b/c/x/z, so the above should be inferred as mutable + mutate(x, null); +} + +``` + +## HIR + +``` +bb0: + Return +``` + +## Code + +```javascript +function mutate$0(x$1, y$2) { + return; +} + +``` +## HIR + +``` +bb0: + Const mutable a$2 = Object { } + Const mutable b$3 = Array [readonly a$2] + Const mutable c$4 = Object { } + Const mutable d$5 = Object { c: readonly c$4 } + Const mutable x$6 = Object { } + Reassign mutable x$6.b = readonly b$3 + Const mutable y$7 = Call mutable mutate$8(mutable x$6, mutable d$5) + If (readonly a$2) then:bb1 else:bb1 +bb1: + If (readonly b$3) then:bb3 else:bb3 +bb3: + If (readonly c$4) then:bb5 else:bb5 +bb5: + If (readonly d$5) then:bb7 else:bb7 +bb7: + If (readonly y$7) then:bb9 else:bb9 +bb9: + Const mutable $9 = null + Call mutable mutate$8(mutable x$6, readonly $9) + Return +``` + +## Code + +```javascript +function Component$0(props$1) { + const a$2 = {}; + const b$3 = [a$2]; + const c$4 = {}; + const d$5 = { + c: c$4, + }; + const x$6 = {}; + x$6 = b$3; + const y$7 = mutate$8(x$6, d$5); + if (a$2) { + if (b$3) { + if (c$4) { + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (c$4) { + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (b$3) { + if (c$4) { + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (c$4) { + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (d$5) { + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; + } + if (y$7) { + mutate$8(x$6, null); + return; + } + mutate$8(x$6, null); + return; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.js b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.js new file mode 100644 index 0000000000..3791e4b688 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-with-aliasing.js @@ -0,0 +1,30 @@ +function mutate(x, y) {} + +function Component(props) { + const a = {}; + const b = [a]; // array elements alias + const c = {}; + const d = { c }; // object values alias + + // capture all the values into this object + const x = {}; + x.b = b; + const y = mutate(x, d); // mutation aliases the arg and return value + + // all of these tests are seemingly readonly, since the values are never directly + // mutated again. but they are all aliased by `x`, which is later modified, and + // these are therefore mutable references: + if (a) { + } + if (b) { + } + if (c) { + } + if (d) { + } + if (y) { + } + + // could in theory mutate any of a/b/c/x/z, so the above should be inferred as mutable + mutate(x, null); +}