diff --git a/compiler/forget/src/HIR/EliminateRedundantPhi.ts b/compiler/forget/src/HIR/EliminateRedundantPhi.ts new file mode 100644 index 0000000000..0e4158ca20 --- /dev/null +++ b/compiler/forget/src/HIR/EliminateRedundantPhi.ts @@ -0,0 +1,139 @@ +/** + * 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 invariant from "invariant"; +import { assertExhaustive } from "../Common/utils"; +import { BlockId, HIRFunction, Identifier, Place } from "./HIR"; +import { collectInputs } from "./InferMutableLifetimes"; + +/** + * Pass to eliminate redundant phi nodes: + * - all operands are the same identifier, ie `x2 = phi(x1, x1, x1)`. + * - all operands are the same identifier *or* the output of the phi, ie `x2 = phi(x1, x2, x1, x2)`. + * + * In both these cases, the phi is eliminated and all usages of the phi identifier + * are replaced with the other operand (ie in both cases above, all usages of `x2` are replaced with `x1` . + * + * The algorithm is inspired by that in https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf + * but modified to reduce passes over the CFG. We visit the blocks in reverse postorder. Each time a redundant + * phi is encountered we add a mapping (eg x2 -> x1) to a rewrite table. Subsequent instructions, terminals, + * and phis rewrite all their identifiers based on this table. The algorithm loops over the CFG repeatedly + * until there are no new rewrites: for a CFG without back-edges it completes in a single pass. + */ +export function eliminateRedundantPhi(fn: HIRFunction) { + const ir = fn.body; + const rewrites: Map = new Map(); + + // Whether or the CFG has a back-edge (a loop). We determine this dynamically + // during the first iteration over the CFG by recording which blocks were already + // visited, and checking if a block has any predecessors that weren't visited yet. + // Because blocks are in reverse postorder, the only time this can occur is a loop. + let hasBackEdge = false; + const visited: Set = new Set(); + + // size tracks the number of rewrites at the beginning of each iteration, so we can + // compare to see if any new rewrites were added in that iteration. + let size = rewrites.size; + do { + size = rewrites.size; + for (const [blockId, block] of ir.blocks) { + // On the first iteration of the loop check for any back-edges. + // if there aren't any then there won't be a second iteration + if (!hasBackEdge) { + for (const pred of block.preds) { + if (!visited.has(pred.id)) { + hasBackEdge = true; + } + } + } + visited.add(blockId); + + // Find any redundant phis + phis: for (const phi of block.phis) { + let same: Identifier | null = null; + for (const [_, operand] of phi.operands) { + const ident = rewrites.get(operand) ?? operand; + if ( + (same !== null && ident.id === same.id) || + ident.id === phi.id.id + ) { + // This operand is the same as the phi or is the same as the + // previous non-phi operands + continue; + } else if (same !== null) { + // There are multiple operands not equal to the phi itself, + // this phi can't be eliminated. + continue phis; + } else { + // First non-phi operand + same = ident; + } + } + invariant(same !== null, "Expected phis to be non-empty"); + rewrites.set(phi.id, same); + block.phis.delete(phi); + } + + // Rewrite all instruction lvalues and operands + for (const instr of block.instructions) { + for (const place of collectInputs(instr)) { + rewritePlace(place, rewrites); + } + const { lvalue } = instr; + if (lvalue !== null) { + rewritePlace(lvalue.place, rewrites); + } + } + + // Rewrite all terminal operands + const { terminal } = block; + switch (terminal.kind) { + case "if": { + rewritePlace(terminal.test, rewrites); + break; + } + case "switch": { + rewritePlace(terminal.test, rewrites); + for (const case_ of terminal.cases) { + if (case_.test === null) { + continue; + } + rewritePlace(case_.test, rewrites); + } + break; + } + case "return": + case "throw": { + if (terminal.value !== null) { + rewritePlace(terminal.value, rewrites); + } + break; + } + case "goto": { + // no-op + break; + } + default: { + assertExhaustive( + terminal, + `Unexpected terminal kind '${(terminal as any).kind}'` + ); + } + } + } + // We only need to loop if there were newly eliminated phis in this iteration + // *and* the CFG has loops. If there are no loops, then all eliminated phis + // have already propagated forwards since we visit in reverse postorder. + } while (rewrites.size > size && hasBackEdge); +} + +function rewritePlace(place: Place, rewrites: Map) { + const rewrite = rewrites.get(place.identifier); + if (rewrite != null) { + place.identifier = rewrite; + } +} diff --git a/compiler/forget/src/HIR/InferMutableLifetimes.ts b/compiler/forget/src/HIR/InferMutableLifetimes.ts index da9e00dbe7..06b4b81bd5 100644 --- a/compiler/forget/src/HIR/InferMutableLifetimes.ts +++ b/compiler/forget/src/HIR/InferMutableLifetimes.ts @@ -106,7 +106,7 @@ export function inferMutableRanges(func: HIRFunction) { } } -function* collectInputs(instr: Instruction) { +export function* collectInputs(instr: Instruction) { const instrValue = instr.value; switch (instrValue.kind) { case "NewExpression": @@ -142,6 +142,12 @@ function* collectInputs(instr: Instruction) { } break; } + case "JsxFragment": { + for (const c of instrValue.children) { + yield c; + } + break; + } case "ObjectExpression": { if (instrValue.properties !== null) { const props = instrValue.properties; @@ -163,7 +169,10 @@ function* collectInputs(instr: Instruction) { break; } default: { - console.log(`unhandled instruction: ${printInstruction(instr)}`); + assertExhaustive( + instrValue, + `Unexpected instruction kind '${(instrValue as any).kind}'` + ); } } } diff --git a/compiler/forget/src/__tests__/fixtures/hir/component.expect.md b/compiler/forget/src/__tests__/fixtures/hir/component.expect.md index 7fcd194f5b..dec530f244 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/component.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/component.expect.md @@ -37,23 +37,18 @@ function Component(props) { bb0: [1] Const mutate items$27 = read props$26.items [2] Const mutate maxItems$28 = read props$26.maxItems - [3] Const mutate renderedItems$29 = Array [] - [4] Const mutate seen$30 = New mutate Set$6() + [3] Const mutate renderedItems$29[3:14] = Array [] + [4] Const mutate seen$30[4:11] = New mutate Set$6() [5] Const mutate $31 = 0 [6] Const mutate max$32 = Call mutate Math$8.max(read $31, read maxItems$28) Goto bb1 bb1: predecessor blocks: bb0 bb4 bb7 - items$33: phi(bb0: items$27, bb4: items$33, bb7: items$33) - item$35[-1:11]: phi(bb0: item$10, bb4: item$35, bb7: item$35) - seen$38[-1:11]: phi(bb0: seen$30, bb4: seen$38, bb7: seen$38) - renderedItems$46[-1:14]: phi(bb0: renderedItems$29, bb4: renderedItems$46, bb7: renderedItems$46) - max$48: phi(bb0: max$32, bb4: max$48, bb7: max$48) - If (read items$33) then:bb3 else:bb2 + If (read items$27) then:bb3 else:bb2 bb3: predecessor blocks: bb1 [7] Const mutate $34 = null - [8] Const mutate $36 = Binary read item$35 == read $34 + [8] Const mutate $36 = Binary read item$10 == read $34 If (read $36) then:bb8 else:bb9 bb8: predecessor blocks: bb3 @@ -61,7 +56,7 @@ bb8: Goto bb7 bb9: predecessor blocks: bb3 - [10] Const mutate $39 = Call mutate seen$38.has(mutate item$35) + [10] Const mutate $39 = Call mutate seen$30.has(mutate item$10) Goto bb7 bb7: predecessor blocks: bb9 bb8 @@ -69,15 +64,15 @@ bb7: If (read $40) then:bb1 else:bb4 bb4: predecessor blocks: bb7 - [11] Call mutate seen$38.add(mutate item$35) + [11] Call mutate seen$30.add(mutate item$10) [12] Const mutate $43 = "div" - [13] Const mutate $44 = JSX {read item$35} - [14] Call mutate renderedItems$46.push(read $44) - [15] Const mutate $49 = Binary read renderedItems$46.length >= read max$48 + [13] Const mutate $44 = JSX {read item$10} + [14] Call mutate renderedItems$29.push(read $44) + [15] Const mutate $49 = Binary read renderedItems$29.length >= read max$32 If (read $49) then:bb2 else:bb1 bb2: predecessor blocks: bb1 bb4 - [16] Const mutate count$52 = read renderedItems$46.length + [16] Const mutate count$52 = read renderedItems$29.length [17] Const mutate $53 = "div" [18] Const mutate $54 = "\n " [19] Const mutate $55 = "h1" @@ -85,7 +80,7 @@ bb2: [21] Const mutate $57 = JSX {freeze count$52}{read $56} [22] Const mutate $58 = "\n " [23] Const mutate $59 = "\n " - [24] Const mutate $60 = JSX {read $54}{read $57}{read $58}{freeze renderedItems$46}{read $59} + [24] Const mutate $60 = JSX {read $54}{read $57}{read $58}{freeze renderedItems$29}{read $59} Return read $60 ``` 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 index 433d12ce41..097321fec6 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/mutable-lifetime-loops.expect.md @@ -84,8 +84,6 @@ bb1: b$20: phi(bb0: b$14, bb3: b$23) c$22: phi(bb0: c$15, bb3: c$25) d$24: phi(bb0: d$16, bb3: d$26) - mutate$27[-1:14]: phi(bb0: mutate$7, bb3: mutate$27) - cond$28[-1:12]: phi(bb0: cond$8, bb3: cond$28) [5] Const mutate $17 = true If (read $17) then:bb3 else:bb2 bb3: @@ -95,8 +93,8 @@ bb3: [8] Reassign mutate b$23[8:11] = read c$22 [9] Reassign mutate c$25 = read d$24 [10] Reassign mutate d$26 = read z$19 - [11] Call mutate mutate$27(mutate a$21, mutate b$23) - [12] Const mutate $29 = Call mutate cond$28(mutate a$21) + [11] Call mutate mutate$7(mutate a$21, mutate b$23) + [12] Const mutate $29 = Call mutate cond$8(mutate a$21) If (read $29) then:bb2 else:bb1 bb2: predecessor blocks: bb1 bb3 @@ -117,7 +115,7 @@ bb11: bb13: predecessor blocks: bb11 [13] Const mutate $34 = null - [14] Call mutate mutate$27(mutate d$33, read $34) + [14] Call mutate mutate$7(mutate d$33, read $34) Return ``` diff --git a/compiler/forget/src/__tests__/fixtures/hir/mutable-liverange-loop.expect.md b/compiler/forget/src/__tests__/fixtures/hir/mutable-liverange-loop.expect.md index 42554bc7ca..3525d20b4b 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/mutable-liverange-loop.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/mutable-liverange-loop.expect.md @@ -68,42 +68,36 @@ function cond$0() { ``` bb0: - [1] Let mutate a$12 = Object { } - [2] Let mutate b$13 = Object { } + [1] Let mutate a$12[1:7] = Object { } + [2] Let mutate b$13[2:6] = Object { } [3] Let mutate c$14 = Object { } - [4] Let mutate d$15 = Object { } + [4] Let mutate d$15[4:9] = Object { } Goto bb1 bb1: predecessor blocks: bb0 bb3 - mutate$17[-1:9]: phi(bb0: mutate$6, bb3: mutate$17) - a$18[-1:7]: phi(bb0: a$12, bb3: a$18) - b$19[-1:6]: phi(bb0: b$13, bb3: b$19) - cond$20[-1:7]: phi(bb0: cond$7, bb3: cond$20) - c$25: phi(bb0: c$14, bb3: c$25) - d$27[-1:9]: phi(bb0: d$15, bb3: d$27) [5] Const mutate $16 = true If (read $16) then:bb3 else:bb2 bb3: predecessor blocks: bb1 - [6] Call mutate mutate$17(mutate a$18, mutate b$19) - [7] Const mutate $21 = Call mutate cond$20(mutate a$18) + [6] Call mutate mutate$6(mutate a$12, mutate b$13) + [7] Const mutate $21 = Call mutate cond$7(mutate a$12) If (read $21) then:bb2 else:bb1 bb2: predecessor blocks: bb1 bb3 - If (read a$18) then:bb7 else:bb7 + If (read a$12) then:bb7 else:bb7 bb7: predecessor blocks: bb2 - If (read b$19) then:bb9 else:bb9 + If (read b$13) then:bb9 else:bb9 bb9: predecessor blocks: bb7 - If (read c$25) then:bb11 else:bb11 + If (read c$14) then:bb11 else:bb11 bb11: predecessor blocks: bb9 - If (read d$27) then:bb13 else:bb13 + If (read d$15) then:bb13 else:bb13 bb13: predecessor blocks: bb11 [8] Const mutate $28 = null - [9] Call mutate mutate$17(mutate d$27, read $28) + [9] Call mutate mutate$6(mutate d$15, read $28) Return ``` diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-for-of.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-for-of.expect.md index 25089585b1..1a77838fda 100644 --- a/compiler/forget/src/__tests__/fixtures/hir/ssa-for-of.expect.md +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-for-of.expect.md @@ -23,20 +23,18 @@ bb0: Goto bb1 bb1: predecessor blocks: bb0 bb3 bb5 - items$6: phi(bb0: items$5, bb3: items$6, bb5: items$6) - cond$8: phi(bb0: cond$4, bb3: cond$8, bb5: cond$8) - If (read items$6) then:bb3 else:bb2 + If (read items$5) then:bb3 else:bb2 bb3: predecessor blocks: bb1 [2] Let mutate y$7 = 0 - If (read cond$8) then:bb5 else:bb1 + If (read cond$4) then:bb5 else:bb1 bb5: predecessor blocks: bb3 [3] Reassign mutate y$9 = 1 Goto bb1 bb2: predecessor blocks: bb1 - Return freeze items$6 + Return freeze items$5 ``` ## Code diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.expect.md new file mode 100644 index 0000000000..9fa01d8484 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +function foo(a, b, c) { + let x = 0; + while (a) { + while (b) { + while (c) { + x + 1; + } + } + } + return x; +} + +``` + +## HIR + +``` +bb0: + [1] Let mutate x$9 = 0 + Goto bb1 +bb1: + predecessor blocks: bb0 bb4 + If (read a$6) then:bb4 else:bb2 +bb4: + predecessor blocks: bb1 bb7 + If (read b$7) then:bb7 else:bb1 +bb7: + predecessor blocks: bb4 bb9 + If (read c$8) then:bb9 else:bb4 +bb9: + predecessor blocks: bb7 + [2] Const mutate $17 = 1 + [3] Binary read x$9 + read $17 + Goto bb7 +bb2: + predecessor blocks: bb1 + Return read x$9 +``` + +## Code + +```javascript +function foo$0(a$6, b$7, c$8) { + let x$9 = 0; + ("<>"); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.js b/compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.js new file mode 100644 index 0000000000..4fed2d62f3 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-nested-loops-no-reassign.js @@ -0,0 +1,11 @@ +function foo(a, b, c) { + let x = 0; + while (a) { + while (b) { + while (c) { + x + 1; + } + } + } + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.expect.md b/compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.expect.md new file mode 100644 index 0000000000..7ccca6c7e2 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +function foo() { + let x = 1; + while (x < 10) { + x + 1; + } + + return x; +} + +``` + +## HIR + +``` +bb0: + [1] Let mutate x$5 = 1 + Goto bb1 +bb1: + predecessor blocks: bb0 bb3 + [2] Const mutate $6 = 10 + [3] Const mutate $8 = Binary read x$5 < read $6 + If (read $8) then:bb3 else:bb2 +bb3: + predecessor blocks: bb1 + [4] Const mutate $9 = 1 + [5] Binary read x$5 + read $9 + Goto bb1 +bb2: + predecessor blocks: bb1 + Return read x$5 +``` + +## Code + +```javascript +function foo$0() { + let x$5 = 1; + ("<>"); +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.js b/compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.js new file mode 100644 index 0000000000..e0bd40a21e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/ssa-while-no-reassign.js @@ -0,0 +1,8 @@ +function foo() { + let x = 1; + while (x < 10) { + x + 1; + } + + return x; +} diff --git a/compiler/forget/src/__tests__/hir-test.ts b/compiler/forget/src/__tests__/hir-test.ts index d7db98f479..43fcb8c913 100644 --- a/compiler/forget/src/__tests__/hir-test.ts +++ b/compiler/forget/src/__tests__/hir-test.ts @@ -16,6 +16,7 @@ import path from "path"; import prettier from "prettier"; import { lower } from "../HIR/BuildHIR"; import codegen from "../HIR/Codegen"; +import { eliminateRedundantPhi } from "../HIR/EliminateRedundantPhi"; import { HIRFunction } from "../HIR/HIR"; import { Environment } from "../HIR/HIRBuilder"; import { inferMutableRanges } from "../HIR/InferMutableLifetimes"; @@ -64,6 +65,7 @@ describe("React Forget (HIR version)", () => { const env: Environment = new Environment(); const ir: HIRFunction = lower(nodePath, env); buildSSA(ir, env); + eliminateRedundantPhi(ir); inferReferenceEffects(ir); inferMutableRanges(ir); // const lifetimeGraph = buildDefUseGraph(ir); diff --git a/compiler/forget/yarn.lock b/compiler/forget/yarn.lock index 658cc29de7..368dc81b42 100644 --- a/compiler/forget/yarn.lock +++ b/compiler/forget/yarn.lock @@ -83,12 +83,12 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/generator@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.6.tgz#9e481a3fe9ca6261c972645ae3904ec0f9b34a1d" - integrity sha512-oHGRUQeoX1QrKeJIKVe0hwjGqNnVYsM5Nep5zo0uE0m42sLH+Fsd2pStJ5sRM1bNyTUUoz0pe2lTeMJrb/taTA== +"@babel/generator@^7.20.1": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.2.tgz#c2e89e22613a039285c1e7b749e2cd0b30b9a481" + integrity sha512-SD75PMIK6i9H8G/tfGvB4KKl4Nw6Ssos9nGgYwxbgyTP0iX/Z55DveoH86rmUB/YHTQQ+ZC0F7xxaY8l2OF44Q== dependencies: - "@babel/types" "^7.19.4" + "@babel/types" "^7.20.2" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" @@ -260,10 +260,10 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a" integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ== -"@babel/parser@^7.19.6": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.6.tgz#b923430cb94f58a7eae8facbffa9efd19130e7f8" - integrity sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA== +"@babel/parser@^7.20.1": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.2.tgz#9aeb9b92f64412b5f81064d46f6a1ac0881337f4" + integrity sha512-afk318kh2uKbo7BEj2QtEi8HVCGrwHUffrYDy7dgVcSa2j9lY3LDjPzcyGdpX7xgm35aWqvciZJ4WKmdF/SxYg== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -484,18 +484,18 @@ lodash "^4.17.10" "@babel/traverse@^7.19.1": - version "7.19.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.6.tgz#7b4c865611df6d99cb131eec2e8ac71656a490dc" - integrity sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ== + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" + integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.6" + "@babel/generator" "^7.20.1" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.19.6" - "@babel/types" "^7.19.4" + "@babel/parser" "^7.20.1" + "@babel/types" "^7.20.0" debug "^4.1.0" globals "^11.1.0" @@ -534,6 +534,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.20.0", "@babel/types@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" + integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"