Skip to content
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

WIP: Eliminate gotos when migrating #139

Open
wants to merge 29 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
09e0203
convert gotos to conditional form
corevo Mar 22, 2018
82cdd43
added elimination transformation tests
corevo Mar 22, 2018
baacf4e
added outward movement transformation tests
corevo Mar 22, 2018
864be6e
label elimination test
corevo Mar 22, 2018
8315f4a
conditional goto elimination
corevo Mar 22, 2018
4798990
repitition goto elimination
corevo Mar 22, 2018
eb6db87
inward movement tests
corevo Mar 22, 2018
3b87af8
block level helper functions
corevo Mar 22, 2018
990cf5c
inward movement tests
corevo Apr 1, 2018
d254b63
loop outward movement transformation implementation
corevo Apr 1, 2018
87bf805
conditional outward movement transformation implementation
corevo Apr 1, 2018
748caf0
inward movement tests
corevo Apr 1, 2018
9114ead
lift transformation
corevo Apr 1, 2018
3e842d6
goto elimination stub
corevo Apr 2, 2018
4decabc
automatically replace undefines in input
corevo Apr 2, 2018
af272ac
eliminate gotos on migration
corevo Apr 2, 2018
96b95a3
keep the same goto pointer throughout the elimination
corevo Apr 2, 2018
411ca48
relationship definition
corevo Apr 2, 2018
ea6cfa9
relationship takes conditional gotos into account
corevo Apr 2, 2018
2cce536
renamed storeValue to store
corevo Apr 2, 2018
a02b79f
fixed lift goto lookup
corevo Apr 2, 2018
859d76f
inward loop movement transformation
corevo Apr 3, 2018
8272f39
pretty printer for procedures
corevo Apr 3, 2018
b13dde7
Merge branch 'master' of github.com:SeleniumHQ/selenium-ide into goto…
corevo Apr 3, 2018
de5c6f0
skip else as it is improbable we'll face that case
corevo Apr 3, 2018
6102816
inward then movement transformation
corevo Apr 3, 2018
282faca
fix broken test
corevo Apr 3, 2018
1c3ca59
eliminate labels
corevo Apr 3, 2018
5087e2b
remove empty blocks
corevo Apr 3, 2018
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
367 changes: 367 additions & 0 deletions packages/selenium-ide/src/neo/IO/legacy/goto-elimination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,367 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

// GENENRAL NOTE: procedure is non-mutable while p is
// Do not mutate statements, create new references
// Don't break goto references, you do not need to change them anyway (just their location)
export function eliminate(procedure) {
let p = [...procedure];

// Change all gotos to conditionals
const unconditionalGotos = p.filter(statement => (statement.command === "goto" || statement.command === "gotoIf"));
unconditionalGotos.forEach(goto => {
const index = p.indexOf(goto);
p.splice(index, 1, ...transformToConditional(goto));
});

const labels = p.filter(statement => statement.command === "label");
const gotos = p.filter(statement => statement.command === "goto");

// start eliminating
while (gotos.length) {
const goto = gotos[0];
const label = findMatchingLabel(labels, goto);

const relationship = relation(p, goto, label);
if (relationship === Relationship.IndirectlyRelated) {
// Make goto and label directly related
// Ignoring switch and else statements
p = transformOutward(p, goto);
} else if (relationship === Relationship.DirectlyRelated) {
// Make goto and label siblings
if (calculateLevel(p, goto) > calculateLevel(p, label)) {
p = transformOutward(p, goto);
} else {
if (p.indexOf(goto) > p.indexOf(label)) {
p = lift(p, goto, label);
}
// Inward transformation
p = transformInward(p, goto, label);
}
} else {
// goto and label are siblings

// eliminate goto
p = eliminateGoto(p, goto, label);
// remove it from the list of gotos
gotos.shift();
}
}

return optimize(eliminateLabels(p));
}

export function transformToConditional(goto) {
return ([
{
command: "if",
target: goto.command === "gotoIf" ? goto.target : "true"
},
{
command: "goto",
target: goto.command === "gotoIf" ? goto.value : goto.target
},
{
command: "end"
}
]);
}

function optimize(procedure) {
return procedure.filter((statement, i) => {
if (isBlock(statement)) return !isBlockEnd(procedure[i + 1]);
if (isBlockEnd(statement)) return !isBlock(procedure[i - 1]);
return true;
});
}

export function eliminateLabels(procedure) {
return procedure.filter(s => s.command !== "label");
}

export function eliminateGoto(procedure, goto, label) {
// only for siblings
const p = [...procedure];
const gotoIndex = procedure.indexOf(goto), labelIndex = procedure.indexOf(label);
if (gotoIndex < labelIndex) {
// eliminate using conditional
const ifIndex = gotoIndex - 1;
p[ifIndex] = Object.assign({...p[ifIndex]}, {target: `!${p[ifIndex].target}`});
p.splice(labelIndex, 0, { command: "end" });
p.splice(gotoIndex, 2);
} else {
// eliminate using do while
const ifIndex = gotoIndex - 1;
p.splice(ifIndex, 3, { command: "endDo", target: p[ifIndex].target });
p.splice(labelIndex, 0, { command: "do" });
}
return p;
}

export function transformOutward(procedure, goto) {
const block = findEnclosingBlock(procedure, goto);
const end = findBlockClose(procedure, block);
const p = [...procedure];
if (block.command !== "if") {
// outward loop movement
transformOutwardLoop(p, goto, block, end);
} else {
// outward conditional movement
transformOutwardConditional(p, goto, block, end);
}
return p;
}

function transformOutwardLoop(p, goto, block, end) {
const ifIndex = p.indexOf(goto) - 1;
p.splice(ifIndex, 3,
{ command: "store", target: p[ifIndex].target, value: goto.target },
{ command: "if", target: `\${${goto.target}}` },
{ command: "break" },
{ command: "end" }
);
const endIndex = p.indexOf(end);
p.splice(endIndex + 1, 0,
{ command: "if", target: `\${${goto.target}}` },
goto,
{ command: "end" }
);
}

function transformOutwardConditional(p, goto, block, end) {
const ifIndex = p.indexOf(goto) - 1;
p.splice(ifIndex, 3,
{ command: "store", target: p[ifIndex].target, value: goto.target },
{ command: "if", target: `!\${${goto.target}}` }
);
const endIndex = p.indexOf(end);
p.splice(endIndex + 1, 0,
{ command: "end" },
{ command: "if", target: `\${${goto.target}}` },
goto,
{ command: "end" }
);
}

export function transformInward(procedure, goto, label) {
const block = findFirstEnclosingBlock(procedure, procedure.indexOf(goto), label);
const p = [...procedure];
if (block.command !== "else") {
// outward loop movement
transformInwardLoopConditional(p, goto, block, label);
} else {
// outward conditional movement
transformInwardElseConditional(p, goto, block, label);
}
return p;
}

function transformInwardLoopConditional(p, goto, block, label) {
const ifIndex = p.indexOf(goto) - 1;
p.splice(ifIndex, 3,
{ command: "store", target: p[ifIndex].target, value: goto.target },
{ command: "if", target: `!\${${goto.target}}` }
);
const blockIndex = p.indexOf(block);
p.splice(blockIndex, 1,
{ command: "end" },
{ command: block.command, target: `\${${goto.target}} || ${block.target}` },
{ command: "if", target: `\${${goto.target}}` },
goto,
{ command: "end" }
);
const labelIndex = p.indexOf(label);

// make sure that the next time the while condition isn't shorted
if (block.command === "while") {
p.splice(labelIndex + 1, 0,
{ command: "store", target: "false", value: goto.target }
);
}
return p;
}

function transformInwardElseConditional() {
throw new Error("not implemented");
}

export function lift(procedure, goto, label) {
const p = [
{ command: "store", target: "false", value: goto.target },
...procedure
];
const labelIndex = p.indexOf(label);
p.splice(labelIndex, 0,
{
command: "do"
},
{
command: "if",
target: `\${${goto.target}}`
},
goto,
{
command: "end"
}
);
const ifIndex = p.lastIndexOf(goto) - 1;
p.splice(ifIndex, 3,
{
command: "store",
target: "condition",
value: goto.target
},
{
command: "endDo",
target: `\${${goto.target}}`
}
);
return p;
}

function findFirstEnclosingBlock(procedure, offset, statement) {
const blocks = [];
for (let i = offset; i < procedure.length; i++) {
const block = procedure[i];
if (isBlock(block)) {
blocks.push(block);
} else if (isBlockEnd(block)) {
blocks.pop(block);
} else if (block === statement) {
return blocks[0];
}
}
}

function findEnclosingBlock(procedure, goto) {
// remember to skip the enclosing if
for (let i = procedure.indexOf(goto) - 2; i >= 0; i--) {
if (isBlock(procedure[i])) return procedure[i];
}
}

function findBlockClose(procedure, block) {
let level = 1, index = procedure.indexOf(block);
while (level !== 0) {
index++;
level = levelIncrement(procedure[index], level);
}
return procedure[index];
}

function findMatchingLabel(listOfLabels, goto) {
return listOfLabels.find(statement => (statement.command === "label" && statement.target === goto.target));
}

function isBlock(statement) {
switch(statement.command) {
case "if":
case "do":
case "while":
return true;
default:
return false;
}
}

function isBlockEnd(statement) {
switch(statement.command) {
case "end":
case "endDo":
return true;
default:
return false;
}
}

function levelIncrement(statement, level = 0) {
if (isBlock(statement)) {
level++;
} else if (isBlockEnd(statement)) {
level--;
}
return level;
}

function calculateLevel(procedure, statement) {
// ignore the goto condition in terms of level
let level = statement.command === "goto" ? -1 : 0;
for (let index = 0; index < procedure.length; index++) {
level = levelIncrement(procedure[index], level);
if (procedure[index] === statement) return level;
}
}

function isConditionalGoto(procedure, goto) {
const index = procedure.indexOf(goto);
return (procedure[index].command === "goto" && procedure[index - 1].command === "if" && procedure[index + 1].command === "end");
}

// for this calculation a tree would've been a better data structure
export function relation(procedure, goto, label) {
let level, first, outOfBlock;
for (let index = 0; index < procedure.length; index++) {
const statement = procedure[index];
// if its a conditional goto, decrease the level, and skip the end
if (isConditionalGoto(procedure, statement)) {
level--;
index++;
} else {
level = levelIncrement(statement, level);
}
// 0 is truthy
if (first !== undefined && outOfBlock === undefined && level < first) {
outOfBlock = level;
} else if (first !== undefined && outOfBlock !== undefined && level > outOfBlock && !isBlock(statement)) {
return Relationship.IndirectlyRelated;
}

if (statement === goto || statement === label) {
if (first === undefined) {
// matched the first one
first = level;
} else if (first !== undefined) {
// matched the second one
if (level === first) {
return Relationship.Siblings;
} else {
return Relationship.DirectlyRelated;
}
}
}
}
}

export const Relationship = {
Siblings: "siblings",
DirectlyRelated: "related",
IndirectlyRelated: "unrelated"
};

export function prettyPrint(procedure) {
let level = 0;
return procedure.map(statement => {
if (isBlockEnd(statement)) {
level--;
}
let r = `${" ".repeat(level)}${statement.command} ${statement.target || ""}`;
if (isBlock(statement)) {
level++;
}
return r;
}).join("\n");
}
5 changes: 3 additions & 2 deletions packages/selenium-ide/src/neo/IO/legacy/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import convert from "xml-js";
import xmlescape from "xml-escape";
import xmlunescape from "unescape";
import JSZip from "jszip";
import { eliminate } from "./goto-elimination";

export function migrateProject(zippedData) {
return JSZip.loadAsync(zippedData).then(zip => {
Expand Down Expand Up @@ -100,13 +101,13 @@ export function migrateTestCase(data) {
{
id: data,
name: result.html.body.table.thead.tr.td._text,
commands: result.html.body.table.tbody.tr.filter(row => (row.td[0]._text && !/^wait/.test(row.td[0]._text))).map(row => (
commands: eliminate(result.html.body.table.tbody.tr.filter(row => (row.td[0]._text && !/^wait/.test(row.td[0]._text))).map(row => (
{
command: row.td[0]._text && row.td[0]._text.replace("AndWait", ""),
target: xmlunescape(parseTarget(row.td[1])),
value: xmlunescape(row.td[2]._text || "")
}
))
)))
}
],
suites: []
Expand Down
Loading