TouchDevelop/ast/diff.ts

1615 строки
49 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev.AST.Diff {
export interface Options
{
approxNameMatching?:boolean;
placeholderOk?:boolean;
tutorialMode?:boolean;
preciseStrings?:StringMap<boolean>;
useStableNames?:boolean;
tutorialCustomizations?: TutorialCustomizations;
}
class RandomIdSetter
extends NodeVisitor
{
private lastId:string;
private definedLocals:any = {};
public keep = false;
public makeId(defl:string)
{
if (this.keep && defl) return (this.lastId = defl);
return (this.lastId = uniqueAstId(16))
}
public visitLocalDef(l:LocalDef) {
// skip; should be handled already
}
public setDeclId(d:Decl)
{
if (d instanceof RecordDef && d.getStableName() && !/_/.test(d.getStableName())) {
d.stableId = d.getStableName();
} else if (!this.keep || !d.stableId) {
d.stableId = this.makeId(d.stableId);
}
}
public visitDecl(d:Decl)
{
this.setDeclId(d)
this.visitChildren(d);
}
public visitLibraryRef(l:LibraryRef)
{
l.getPublicActions().forEach(a => {
this.visitAction(a)
})
super.visitLibraryRef(l);
}
public visitExprHolder(eh:ExprHolder)
{
var locs:LocalDef[] = []
eh.tokens.forEach(t => {
var tt = t.getThing()
if (tt instanceof LocalDef) {
var l = <LocalDef>tt
Util.assert(!!l.stableId)
if (locs.indexOf(l) < 0 && !this.definedLocals.hasOwnProperty(l.stableId))
locs.push(l)
}
})
eh.definedLocals = locs
locs.forEach((l, i) => this.defineLocal(l, this.lastId + "L" + i))
this.makeId(null); // just in case
}
public visitStmt(s:Stmt)
{
s.stableId = this.makeId(s.stableId);
this.visitChildren(s);
}
public visitInlineActions(n:InlineActions)
{
n.stableId = this.makeId(n.stableId);
this.dispatch(n.actions)
this.dispatch(n.expr)
}
public visitInlineAction(n:InlineAction)
{
n.stableId = this.makeId(n.stableId);
this.defineLocal(n.name, n.stableId + "B0");
n.inParameters.concat(n.outParameters).forEach((p, i) => {
this.defineLocal(p, n.stableId + "P" + i);
})
this.visitChildren(n)
}
public visitForeach(n:Foreach)
{
n.stableId = this.makeId(n.stableId);
this.defineLocal(n.boundLocal, n.stableId + "B0");
this.visitChildren(n)
}
public visitFor(n:For)
{
n.stableId = this.makeId(n.stableId);
this.defineLocal(n.boundLocal, n.stableId + "B0");
this.visitChildren(n)
}
private defineLocal(l:LocalDef, id:string)
{
this.definedLocals[id] = true;
l.stableId = id
}
public visitAction(a:Action)
{
this.setDeclId(a)
this.definedLocals = {}
a.getInParameters().concat(a.getOutParameters()).forEach((p, i) => {
this.defineLocal(p.local, a.stableId + "P" + i);
})
if (a.modelParameter)
this.defineLocal(a.modelParameter.local, a.stableId + "PM")
// assign some ids
a.allLocals.forEach(l => { if (!l.stableId) l.stableId = this.makeId(null) })
this.visitChildren(a)
}
public visitRecordField(d:RecordField)
{
d.stableId = !d.getStableName() || /_/.test(d.getStableName()) ? this.makeId(d.stableId) : d.getStableName();
this.visitChildren(d);
}
}
class DiffFeatureDetector
extends FeatureDetector
{
constructor(private options:Options)
{
super();
}
static approxName(s:string)
{
return s.toLowerCase().replace(/\s/g, "")
}
visitThingRef(t:ThingRef)
{
super.visitThingRef(t)
if (t.def instanceof LocalDef)
this.use("local:" + t.def.getName())
else if (t.def instanceof SingletonDef)
this.use("singl:" + t.def.getName())
}
visitLiteral(l:Literal)
{
super.visitLiteral(l)
var d = l.data
switch (typeof d) {
case "string":
this.use("stringLiteral:" + d)
var len = (<string>d).length
this.use("stringLiteralLen10:" + Math.round(len/10))
this.use("stringLiteralLen100:" + Math.round(len/100))
break;
case "number":
this.use("numberLiteral:" + d.toString());
break;
case "boolean":
this.use(d ? "true" : "false");
break;
}
}
visitCall(c:Call)
{
super.visitCall(c)
var act = c.calledExtensionAction()
if (!act) act = c.calledAction()
if (act instanceof LibraryRefAction) {
//var lib = this.libroots[act.parentLibrary().getId()]
this.use("libact:" + act.getName())
return
}
var fld = c.referencedRecordField()
if (fld) {
if (this.options.approxNameMatching)
this.use("decl:" + DiffFeatureDetector.approxName(fld.getName()))
else
this.use("decl:" + fld.stableId)
return
}
var p = c.prop()
if (p.parentKind instanceof RecordEntryKind || p.parentKind instanceof RecordDefKind) {
this.use("recordProp:" + p.getName())
}
var decl = c.prop().forwardsTo()
if (decl) {
if (this.options.approxNameMatching)
this.use("decl:" + DiffFeatureDetector.approxName(decl.getName()))
else
this.use("decl:" + decl.stableId)
}
}
visitOperator(op:Operator)
{
this.use("op:" + op.data)
}
visitComment(c:Comment)
{
c.text.split(/\s+/).forEach(s => {
this.use("commentWord:" + s)
})
}
kind(k:Kind) : string
{
return k.toString()
}
visitRecordField(r:RecordField)
{
super.visitRecordField(r)
this.use("recordFieldName:" + r.getName())
this.use("recordFieldKind:" + this.kind(r.dataKind))
}
static run(n:AstNode, opt:Options):any
{
var d = new DiffFeatureDetector(opt)
d.dispatch(n)
var f = d.features
Object.keys(f).forEach(k => {
if (/^numberLiteral:/.test(k) || /^op:[0-9]/.test(k))
f[k] = f[k] * 0.2
})
if (opt.tutorialMode && f["var"]) {
f["commands"] = f["var"]
f["var"] = 0
}
return f
}
static cmp(a:any, b:any)
{
var ka = Object.keys(a)
var similarity = 0
var dissimilarity = 0
for (var i = 0; i < ka.length; ++i) {
var k = ka[i]
var va = a[k]
var vb = 0
if (b.hasOwnProperty(k)) {
vb = b[k]
similarity += Math.min(va, vb)
}
dissimilarity += Math.abs(va - vb)
}
var ka = Object.keys(b)
for (var i = 0; i < ka.length; ++i) {
var k = ka[i]
if (!a.hasOwnProperty(k)) {
dissimilarity += b[k]
}
}
return similarity - dissimilarity/2
}
static updateSize(a:any, b:any, tutorialMode:boolean)
{
var ka = Object.keys(a)
var similarity = 0
var dissimilarity = 0
for (var i = 0; i < ka.length; ++i) {
var k = ka[i]
var va = a[k]
var vb = 0
if (b.hasOwnProperty(k)) {
vb = b[k]
similarity += Math.min(va, vb)
}
dissimilarity += Math.abs(va - vb)
}
var ka = Object.keys(b)
for (var i = 0; i < ka.length; ++i) {
var k = ka[i]
if (!a.hasOwnProperty(k)) {
dissimilarity += b[k]
}
}
//if (!tutorialMode)
return dissimilarity
/*
if (2*dissimilarity > similarity)
return 2*dissimilarity - similarity;
else
return 1 / ((similarity - dissimilarity) + 2);
*/
}
}
function classify(d:AstNode) {
var r = d.nodeType();
switch (r) {
case "action":
var a = <Action>d;
if (a.isEvent())
return "event:" + a.eventInfo.type.category;
if (a.isPage())
return "page";
break;
case "globalDef":
return (<GlobalDef>d).isResource ? "art" : "data";
}
return r;
}
function setId(t:AstNode, id:string)
{
t.stableId = id;
if (t instanceof RecordField) {
var f = <RecordField>t
if (!f.def().persistent)
f.setStableName(id);
} else if (t instanceof RecordDef) {
if (!(<RecordDef>t).persistent)
(<RecordDef>t).setStableName(id);
}
}
function matchDecls(older:App, newer:App, opts:Options)
{
var olderByStable = Util.toDictionary(older.things, t => t.getStableName())
var olderByName = Util.toDictionary(older.things, t => t.getName())
var assignedIds:any = {}
var missing = newer.things.filter(t => {
var ot:Decl = null
if (opts.useStableNames && t.getStableName() && !newer.syntheticIds[t.getStableName()] &&
olderByStable.hasOwnProperty(t.getStableName()))
ot = olderByStable[t.getStableName()]
if (!ot && olderByName.hasOwnProperty(t.getName()))
ot = olderByName[t.getName()]
if (ot && classify(ot) == classify(t)) {
setId(t, ot.stableId);
assignedIds[ot.stableId] = true;
delete olderByName[t.getName()];
return false;
} else {
return true;
}
})
var olderClassified = Util.groupBy<Decl>(older.things.filter(t => !assignedIds[t.stableId]), classify)
var newerClassified = Util.groupBy<Decl>(missing, classify)
var getFeatures = Util.memoizeHashed((d:Decl) => d.stableId, (d) => DiffFeatureDetector.run(d, opts))
Object.keys(newerClassified).forEach(k => {
var ns:Decl[] = newerClassified[k]
var os:Decl[] = olderClassified[k]
if (!os) return;
var matches = []
ns.forEach(n => {
var fn = getFeatures(n)
os.forEach(o => {
var score = DiffFeatureDetector.cmp(getFeatures(o), fn)
if (score > 0)
matches.push({
score: score,
n: n,
o: o,
})
})
})
matches.sort((a, b) => b.score - a.score)
matches.forEach(m => {
if (assignedIds[m.o.stableId] || assignedIds[m.n.stableId])
return;
setId(m.n, m.o.stableId);
assignedIds[m.o.stableId] = true;
})
})
}
interface StmtMatch {
score: number;
o: Stmt;
n: Stmt;
}
function matchBlocks(oldStmts:Stmt[], newStmts:Stmt[], opts:Options)
{
if (oldStmts.length == 0 && newStmts.length == 0)
return
oldStmts.forEach(s => {
s.diffFeatures = DiffFeatureDetector.run(s, opts)
})
newStmts.forEach((s, i) => {
s.diffFeatures = DiffFeatureDetector.run(s, opts)
// adding statements at the bottom should be cheaper
if (opts.tutorialMode)
s.diffFeatures.positionWeight = ((newStmts.length - i) / newStmts.length) * 0.5;
else
s.diffFeatures.positionWeight = 0
})
var getFeatures = (s:Stmt) => {
if (!s) return {}
return s.diffFeatures
}
var cmp = (a:Stmt, b:Stmt) => {
var off = 0
if (a && b && classify(a) != classify(b) && (!opts.placeholderOk || !a.isPlaceholder()))
off = 1e20
var fa = getFeatures(a)
var fb = getFeatures(b)
if (!a)
off += fb.positionWeight;
return DiffFeatureDetector.updateSize(fa, fb, opts.tutorialMode) + off
};
var diff = minimalUpdateDistance(oldStmts, newStmts, null, cmp)
var firstAdded = -1;
for (var i = 0; i < diff.length; i += 2) {
if (diff[i]) {
if (diff[i].isPlaceholder() && firstAdded != -1) {
diff[firstAdded] = diff[i];
diff[i] = null;
firstAdded = -1;
}
if (diff[i+1]) firstAdded = -1;
} else {
if (firstAdded == -1) firstAdded = i;
}
}
for (var i = 0; i < diff.length; i += 2) {
if (diff[i] && diff[i+1])
setId(diff[i+1], diff[i].stableId)
}
var oldById = Util.toDictionary(oldStmts, (s) => s.stableId)
newStmts.forEach(s => {
var oldStmt = oldById[s.stableId]
if (oldStmt)
matchStmts(oldStmt, s, opts)
})
}
function matchStmts(o:AstNode, n:AstNode, opts:Options)
{
if (!n || !o) return;
if (classify(n) != classify(o)) return;
if (o instanceof Block) {
matchBlocks((<Block>o).stmts, (<Block>n).stmts, opts)
} else {
setId(n, o.stableId);
var ch0 = o.children()
var ch1 = n.children()
ch0.forEach((oo, i) => {
if (oo instanceof Stmt && ch1[i] instanceof Stmt)
matchStmts(<Stmt>oo, <Stmt>ch1[i], opts)
})
if (o instanceof LibraryRef) {
var actByName = Util.toDictionary((<LibraryRef>o).getPublicActions(), a => a.getName());
(<LibraryRef>n).getPublicActions().forEach(a => {
var aa = <LibraryRefAction> actByName[a.getName()]
if (aa) setId(a, aa.stableId)
})
}
}
}
function matchActions(older:App, newer:App, opts:Options)
{
var oldById = Util.toDictionary(older.things, t => t.stableId)
var th:Decl[] = []
newer.things.forEach(a => {
a.diffAltDecl = null
var old = oldById[a.stableId]
if (!old) return;
a.diffAltDecl = old
th.push(a)
})
th.forEach(a => {
if (!(a instanceof Action))
matchStmts(a.diffAltDecl, a, opts)
})
th.forEach(a => {
if (a instanceof Action)
matchStmts(a.diffAltDecl, a, opts)
})
}
export function setLongIds(a:AstNode)
{
var s = new RandomIdSetter()
s.dispatch(a)
}
function prepApp(a:App) {
a.isTopLevel = true;
TypeChecker.tcScript(a, true);
setLongIds(a)
}
function matchIds(older:App, newer:App, opts:Options)
{
prepApp(older)
prepApp(newer)
matchPreped(older, newer, opts)
}
function matchPreped(older:App, newer:App, opts:Options)
{
matchDecls(older, newer, opts)
matchActions(older, newer, opts)
var s = new RandomIdSetter()
s.keep = true
s.dispatch(newer) // reset ids on locals
}
export function randTest()
{
for (var i =0;i<10;++i) {
var a = Random.uint32() + ""
var b = Random.uint32() + ""
medTest(a,b)
}
}
export function medTest(s:string, t:string)
{
var res = minimalEditDistance(s.toUpperCase().split(""),
t.toLowerCase().split(""),
(a, b) => a.toUpperCase() == b.toUpperCase())
var toStr = () => {
var p = ""
for (var i = 0; i < res.length; i += 2) {
if (res[i] && res[i+1])
p += "=" + res[i] + res[i+1]
else if (res[i])
p += "-" + res[i]
else
p += "+" + res[i+1]
}
return p;
}
var r0 = toStr();
res = minimalUpdateDistance(s.toUpperCase().split(""),
t.toLowerCase().split(""),
"",
(a, b) => a=="" || b=="" ? 1 : a.toLowerCase() == b.toLowerCase() ? 0 : 2)
var r1 = toStr();
//if (r0 != r1) {
// console.log(r0)
// console.log(r1)
//}
}
// returns an array R of minimal length, such that
// even(R) without nulls == s
// odd(R) without nulls == t
// forall even i. R[i] && R[i+1] ==> eq(R[i], R[i+1])
//
export function minimalEditDistance<T>(s:T[], t:T[], eq:(a:T,b:T)=>boolean) : T[]
{
var m = s.length
var n = t.length
var d = new Array(m + 1)
for (var i = 0; i <= m; ++i) {
d[i] = new Array(n + 1)
d[i][0] = i
}
for (var j = 0; j <= n; ++j) {
d[0][j] = j;
}
for (var j = 0; j < n; ++j) {
for (var i = 0; i < m; ++i) {
if (eq(s[i], t[j])) {
d[i+1][j+1] = d[i][j];
} else {
var del = d[i][j+1] + 1
var ins = d[i+1][j] + 1
var sub = d[i][j] + 2
d[i+1][j+1] = Math.min(del, ins, sub)
}
}
}
var res:T[] = []
var i = m - 1
var j = n - 1
var lastDel = false
while (i >= 0 || j >= 0) {
var curr = d[i+1][j+1]
if (i >= 0 && j >= 0 && eq(s[i], t[j]) && curr == d[i][j]) {
res.push(t[j])
res.push(s[i])
i--;
j--;
} else {
var isDel = i >= 0 && curr == d[i][j+1] + 1
var isIns = j >= 0 && curr == d[i+1][j] + 1
if (isDel && lastDel) isIns = false;
if (isIns && !lastDel) isDel = false;
if (isDel) {
res.push(null)
res.push(s[i])
i--;
lastDel = true;
} else if (isIns) {
res.push(t[j])
res.push(null)
j--;
lastDel = false;
} else {
Util.assert(curr == d[i][j] + 2)
res.push(t[j])
res.push(null)
res.push(null)
res.push(s[i])
i--;
j--;
lastDel = true;
}
}
}
res.reverse();
return res;
}
export function minimalUpdateDistance<T>(s:T[], t:T[], zero:T, cmp:(a:T,b:T)=>number) : T[]
{
var m = s.length
var n = t.length
var d = new Array(m + 1)
var ss = s.map(e => cmp(e, zero))
var tt = t.map(e => cmp(zero, e))
for (var i = 0; i <= m; ++i) {
d[i] = new Array(n + 1)
d[i][0] = i == 0 ? 0 : (d[i-1][0] + ss[i-1])
}
for (var j = 0; j <= n; ++j) {
d[0][j] = j == 0 ? 0 : (d[0][j-1] + tt[j-1])
}
for (var j = 0; j < n; ++j) {
for (var i = 0; i < m; ++i) {
var del = d[i][j+1] + ss[i]
var ins = d[i+1][j] + tt[j]
var sub = d[i][j] + cmp(s[i], t[j])
var su2 = d[i][j] + ss[i] + tt[j]
d[i+1][j+1] = Math.min(del, ins, sub)
}
}
var res:T[] = []
var i = m - 1
var j = n - 1
var lastDel = false
while (i >= 0 || j >= 0) {
var curr = d[i+1][j+1]
if (i >= 0 && j >= 0 && curr == d[i][j] + cmp(s[i], t[j])) {
res.push(t[j])
res.push(s[i])
i--;
j--;
} else {
var isDel = i >= 0 && curr == d[i][j+1] + ss[i]
var isIns = j >= 0 && curr == d[i+1][j] + tt[j]
if (isDel && lastDel) isIns = false;
if (isIns && !lastDel) isDel = false;
if (isDel) {
res.push(null)
res.push(s[i])
i--;
lastDel = true;
} else if (isIns) {
res.push(t[j])
res.push(null)
j--;
lastDel = false;
} else {
Util.assert(curr == d[i][j] + ss[i] + tt[j])
res.push(t[j])
res.push(null)
res.push(null)
res.push(s[i])
i--;
j--;
lastDel = true;
}
}
}
res.reverse();
return res;
}
function stmtsEq(a:Stmt, b:Stmt)
{
return a.stableId && a.stableId == b.stableId; // && a.nodeType() == b.nodeType();
}
function clearDiff(s:Stmt)
{
if (s) {
s.diffAltStmt = null;
s.diffStatus = undefined;
s.diffStmts = null;
}
}
function addDiffStmt(newer:Stmt, older:Stmt)
{
if (!newer.diffStmts) newer.diffStmts = []
newer.diffStmts.push(older)
}
export function diffSize(decl:Decl):number
{
var size = 0
function desc(s:AstNode, justSize:boolean)
{
if (s instanceof Block) {
} else if (s instanceof Decl) {
} else if (s instanceof Stmt) {
var df = (<Stmt>s).diffStatus
if (df < 0) {
size += 2
return
}
if (df > 0) {
size += 1;
justSize = true;
}
} else if (s instanceof ExprHolder) {
var eh = <ExprHolder>s
if (justSize) {
size += eh.tokens.length * 2
} else if (eh.diffTokens) {
var d = eh.diffTokens
for (var i = 0; i < d.length; i += 2) {
// insert
if (d[i] == null)
size += 1
// remove
if (d[i + 1] == null)
size += 0.1;
}
}
return;
} else {
return;
}
var arr = s.children()
for (var i = 0; i < arr.length; ++i)
desc(arr[i], justSize)
}
desc(decl, false)
return size
}
/*
export function similaritySize(decl:Decl):number
{
var size = 0
function desc(s:AstNode)
{
if (s instanceof Block) {
} else if (s instanceof Decl) {
} else if (s instanceof Stmt) {
if ((<Stmt>s).diffStatus) {
size -= 0.1;
return;
}
size += 1;
} else if (s instanceof ExprHolder) {
var eh = <ExprHolder>s
if (eh.diffTokens) {
var d = eh.diffTokens
for (var i = 0; i < d.length; i += 2)
if (d[i] != null && d[i + 1] != null)
size += 1
else
size -= 0.01;
} else {
size += eh.tokens.length
}
return;
} else {
return;
}
var arr = s.children()
for (var i = 0; i < arr.length; ++i)
desc(arr[i])
}
desc(decl)
return size
}
*/
function isArt(d:Decl) {
return d instanceof GlobalDef && (<GlobalDef>d).isResource
}
function diffRatio(eh:ExprHolder)
{
function measure(t:Token)
{
return Math.min(t.getText().length, 5) + 1
}
var dt = eh.diffTokens
var lenCommon = 0
var lenDiff = 0
for (var i = 0; i < dt.length; i += 2) {
if (dt[i] && dt[i+1])
lenCommon += measure(dt[i])
else if (dt[i])
lenDiff += measure(dt[i])
else
lenDiff += measure(dt[i+1])
}
if (!lenDiff) return 0
return lenDiff / (lenDiff + lenCommon)
}
export function diffExprs(eh0:ExprHolder, eh1:ExprHolder, opts:Options, f = undefined)
{
function tokenDistance(a:Token, b:Token)
{
if (!b)
return 1;
if (!a) {
if (/^[0-9\.]$/.test(b.getText()))
// used to be "1 -"; not sure what for --MM
return 1 + (0.1 / (eh1.tokens.indexOf(b) + 2))
else
return 1 + (0.1 / (eh1.tokens.indexOf(b) + 2))
}
if (a.nodeType() != b.nodeType()) return 3;
if (opts.approxNameMatching) {
var cust = opts.tutorialCustomizations
var fa = a.getForwardedDecl()
var fb = b.getForwardedDecl()
if (isArt(fa) && isArt(fb) && fa.getKind() == fb.getKind()) {
cust.artMapping[fb.getName()] = fa.getName()
return 0;
}
var pa = a.getProperty()
var pb = b.getProperty()
if (pa && pb) {
var ka = pa.parentKind
var kb = pb.parentKind
if (ka && ka == kb && ka.getName() == "Colors" &&
/#[a-fA-F0-9]{6}/.test(pa.getDescription(true)) &&
/#[a-fA-F0-9]{6}/.test(pb.getDescription(true)))
return 0;
}
var da = a.getLiteral()
var db = b.getLiteral()
var isName = b instanceof FieldName
if (!isName && typeof da === "string" && typeof db === "string") {
if ((<Expr>b).enumVal != null || (opts.preciseStrings && opts.preciseStrings[db]))
return (da == db) ? 0 : 3;
// don't do precise matching on bitmatrix/bitframes to allow users to draw their own
// drawings: ((/^bitmatrix|bitframe$/.test((<Expr>b).languageHint)))
if (da == "" && db == "") return 0;
if (da != "" && db != "") {
if (cust)
cust.stringMapping[db] = da
return 0;
}
}
}
return a.getText() == b.getText() ? 0 : 3;
}
eh1.diffTokens = minimalUpdateDistance(eh0.tokens, eh1.tokens, null, f ? f(tokenDistance) : tokenDistance)
}
function diffDecls(decl0:Decl, decl1:Decl, opts:Options) {
var oldById = {}
var newById = {}
function clearAndIndex(idx:any, s:AstNode)
{
if (s instanceof Decl) {
} else if (s instanceof Stmt) {
idx[s.stableId] = s;
clearDiff(<Stmt>s)
} else if (s instanceof ExprHolder) {
(<ExprHolder>s).diffTokens = null;
} else {
return;
}
var arr = s.children()
for (var i = 0; i < arr.length; ++i)
clearAndIndex(idx, arr[i])
}
function linkStmts(oldStmt:Stmt, newStmt:Stmt)
{
newStmt.diffAltStmt = oldStmt;
oldStmt.diffAltStmt = newStmt;
Util.assert(oldStmt.nodeType() == newStmt.nodeType());
var oo = oldStmt.children()
var nn = newStmt.children()
Util.assert(oo.length == nn.length)
for (var i = 0; i < oo.length; ++i) {
Util.assert(oo[i].nodeType() == nn[i].nodeType());
if (oo[i] instanceof ExprHolder) {
diffExprs(<ExprHolder>oo[i], <ExprHolder>nn[i], opts)
if (!opts.tutorialMode &&
oo.length == 1 &&
newStmt instanceof ExprStmt &&
diffRatio(<ExprHolder>nn[i]) > 0.5) {
return false;
}
}
else if (oo[i] instanceof Block)
diffBlocks(<Block>oo[i], <Block>nn[i])
else if (oo[i] instanceof Stmt)
linkStmts(<Stmt>oo[i], <Stmt>nn[i])
else
Util.oops("wrong node type in diff " + oo[i].nodeType())
}
return true
}
function diffBlocks(older:Block, newer:Block)
{
var combined = minimalEditDistance(older.stmts, newer.stmts, stmtsEq)
var lastNewStmt:Stmt = null
clearDiff(newer)
for (var i = 0; i < combined.length; i += 2) {
var oldStmt = combined[i]
var newStmt = combined[i+1]
if (oldStmt && newStmt) {
if (oldStmt.isPlaceholder() && oldStmt.nodeType() != newStmt.nodeType()) {
newStmt.diffStatus = 1;
oldStmt.diffStatus = -1;
addDiffStmt(newStmt, oldStmt)
} else {
if (linkStmts(oldStmt, newStmt)) {
newStmt.diffStatus = 0;
oldStmt.diffStatus = 0;
} else {
newStmt.diffAltStmt = null;
oldStmt.diffAltStmt = null;
newStmt.diffStatus = 2;
oldStmt.diffStatus = -1;
newStmt.calcNode().diffTokens = null
addDiffStmt(newStmt, oldStmt)
}
}
} else if (newStmt) {
newStmt.diffStatus = 1;
} else {
oldStmt.diffStatus = -1;
addDiffStmt(lastNewStmt || newer, oldStmt)
}
if (newStmt)
lastNewStmt = newStmt
}
}
clearAndIndex(oldById, decl0)
clearAndIndex(newById, decl1)
linkStmts(decl0, decl1)
var diffId = 2;
Object.keys(newById).forEach(k => {
var newStmt = newById[k]
if (newStmt.diffStatus == 2) {
newStmt.diffStatus = 1
return
}
if (newStmt.diffAltStmt) return;
var oldStmt = oldById[k]
if (oldStmt) {
// moved
oldStmt.diffStatus = -1
newStmt.diffStatus = 1
if (oldStmt.nodeType() == newStmt.nodeType())
linkStmts(oldStmt, newStmt)
} else {
// really new
newStmt.diffStatus = 1
}
})
Object.keys(oldById).forEach(k => {
var oldStmt = oldById[k]
if (oldStmt.diffAltStmt && oldStmt.diffStatus == -1) {
oldStmt.diffStatus = -diffId;
diffId++;
}
if (oldStmt.diffStatus === undefined)
oldStmt.diffStatus = -1;
})
}
export function templateDiff(act:Decl, templ:Decl, opts:Options)
{
setLongIds(act)
setLongIds(templ)
matchStmts(act, templ, opts)
diffDecls(act, templ, opts)
}
export function diffApps(older:App, newer:App, opts:Options = {})
{
matchIds(older, newer, opts);
var olderD = Util.toDictionary(older.things, t => t.stableId)
newer.things.forEach(t => {
var o:Decl = olderD[t.stableId]
if (o) {
delete olderD[t.stableId]
t.diffStatus = 0
diffDecls(o, t, opts)
} else {
visitStmts(t, s => s.diffStatus = 1)
}
})
newer.diffRemovedThings = []
Object.keys(olderD).forEach(k => {
var o:Decl = olderD[k]
newer.diffRemovedThings.push(o)
visitStmts(o, s => s.diffStatus = -1)
})
}
export interface HistoryEntry
{
time: number;
scriptId: string;
historyId: string;
seqNo: number;
// one of these is present
script?: string;
ast?: any;
updates?: any;
// "idOfNode": { "field": "newValue", "anotherField": "newValue" }
// "ifOfRemovedNode": null
updateSize?:number;
scriptLines?:string[];
}
function indexIds(obj:any)
{
var oldById:any = {}
function findIds(o:any) {
if (!o) return;
if (Array.isArray(o)) {
for (var i = 0; i < o.length; ++i)
findIds(o[i])
} else if (typeof o === "object") {
if (!o.id) Util.oops("no id for " + JSON.stringify(o))
if (oldById.hasOwnProperty(o.id)) Util.oops("duplicate id " + o.id)
oldById[o.id] = o
var k = Object.keys(o)
for (var i = 0; i < k.length; ++i)
findIds(o[k[i]])
}
}
findIds(obj)
return oldById
}
export function jsonDiff(o0:any, n0:any)
{
var oldById = indexIds(o0)
var updateSet:any = {}
function compareArrays(n:any[], o:any[])
{
for (var i = 0; i < n.length; ++i) {
if (n[i].id)
compare(n[i])
}
if (!o || n.length != o.length) return true;
for (var i = 0; i < n.length; ++i) {
var id0 = o[i].id || o[i]
var id1 = n[i].id || n[i]
if (id0 !== id1) return true;
}
return false
}
function compare(n:any) {
if (!n.id) Util.oops("no id on new " + JSON.stringify(n));
var o:any;
if (oldById.hasOwnProperty(n.id)) {
var o = oldById[n.id]
if (!o)
Util.oops("duplicate new id " + n.id)
oldById[n.id] = null
} else {
o = { id: n.id } // start from empty
}
var hasUpdate = false;
var update:any = {}
var k = Object.keys(n)
for (var i = 0; i < k.length; ++i) {
var v = n[k[i]]
if (v === undefined) continue;
var ov = o[k[i]]
if (false && v.id) {
compare(v)
v = v.id
}
if (ov && ov.id) ov = ov.id
switch (typeof v) {
case "string":
case "number":
case "boolean":
if (ov !== v) update[k[i]] = v;
break;
default:
if (Array.isArray(v)) {
if (compareArrays(v, ov))
update[k[i]] = v.map(vv => vv.id || vv);
} else if (v === null) {
if (ov !== v) update[k[i]] = v;
} else
Util.oops("cannot compare " + JSON.stringify(v))
}
}
k = Object.keys(o)
for (var i = 0; i < k.length; ++i) {
if (n[k[i]] === undefined && o[k[i]] !== undefined)
update[k[i]] = null;
}
if (Object.keys(update).length > 0)
updateSet[n.id] = update
}
compare(n0)
var k = Object.keys(oldById)
for (var i = 0; i < k.length; ++i) {
var oo = oldById[k[i]]
if (oo)
updateSet[oo.id] = null
}
return updateSet
}
export var lastSeqNo = 0;
export function computeMicroEdits(entries:HistoryEntry[])
{
function getApp(s:string) {
var a = AST.Parser.parseScript(s, [])
prepApp(a)
return a
}
entries.forEach(e => {
e.updateSize = 0
})
lastSeqNo = entries[0].seqNo
var prevApp = getApp(entries[0].script)
var prevAst = Json.dumpForDiff(prevApp)
entries[0].ast = prevAst
for (var i = 1; i < entries.length; ++i) {
//console.log("compute: " + i)
lastSeqNo = entries[i].seqNo
var newApp = getApp(entries[i].script)
matchPreped(prevApp, newApp, {})
var newAst = Json.dumpForDiff(newApp)
entries[i].updates = jsonDiff(prevAst, newAst)
prevAst = newAst
prevApp = newApp
AST.reset()
newApp.things.forEach(t => t.diffAltDecl = null)
newApp.diffAltDecl = null
}
entries.forEach(e => {
e.updateSize = JSON.stringify(e.ast || e.updates).length
// e.scriptLines = e.script.split(/\r?\n/)
//delete e.script
})
}
export function applyJsonDiff(base:any, diff:any)
{
var byId = indexIds(base)
var k = Object.keys(diff)
for (var i = 0; i < k.length; ++i) {
var id = k[i]
var upd = diff[id]
if (upd === undefined) continue;
var trg = byId[id]
if (upd === null) {
if (!trg) Util.oops("apply diff: no target id " + id)
trg.__deleted = true;
continue;
}
if (!trg) {
byId[id] = trg = { id: id }
}
var kk = Object.keys(upd)
for (var j = 0; j < kk.length; ++j) {
var f = kk[j]
var v = upd[f]
if (Array.isArray(v) && typeof v[0] === "string")
v = v.map(id => {
var r = byId[id]
if (!r) { r = byId[id] = { id: id } }
return r
})
Util.assert(f != "nodeType" || !trg[f])
trg[f] = v
}
}
var newIds = indexIds(base)
k = Object.keys(newIds)
for (var i = 0; i < k.length; ++i) {
var id = k[i]
if (newIds[k[i]].__deleted)
Util.oops("dangling id after diff " + id)
}
}
export function lineDiff(a:string, b:string)
{
var res = minimalEditDistance(a.split(/\r?\n/),
b.split(/\r?\n/),
(a, b) => a == b)
var p = ""
for (var i = 0; i < res.length; i += 2) {
if (res[i] != null && res[i+1] != null)
p += " " + res[i]
else if (res[i] != null)
p += "-" + res[i]
else
p += "+" + res[i+1]
p += "\n"
}
return p;
}
export function sanityCheckEdits(entries:HistoryEntry[])
{
var ast = null
function format(s:string)
{
var app = AST.Parser.parseScript(s, [])
app.isTopLevel = true;
TypeChecker.tcScript(app, true);
app.rootId = "none";
return App.sanitizeScriptTextForCloud( app.serialize() )
}
entries.forEach(e => {
lastSeqNo = e.seqNo
//console.log("compare: " + e.seqNo)
if (e.ast) ast = JSON.parse(JSON.stringify(e.ast));
else applyJsonDiff(ast, e.updates)
var prevAst = JSON.stringify(ast)
var pre0 = e.script
var pre1 = Json.serialize(ast)
AST.reset()
var a0 = format(pre0)
var a1 = format(pre1)
if (a0 != a1) {
if (a0.split(/\n/).length < 250) {
var diff = lineDiff(a0, a1)
}
Util.oops("mismatch!", [diff || "<too big>", a0, a1])
}
if (JSON.stringify(ast) != prevAst)
Util.oops("serialize modified json", [JSON.stringify(ast), prevAst])
delete e.script;
})
}
export function scrub(entries:HistoryEntry[])
{
function desc(e:any, f) {
if (!e || typeof e != "object") return;
if (Array.isArray(e)) {
for (var i = 0; i < e.length; ++i) {
desc(e[i], f);
}
} else {
var k = Object.keys(e)
for (var i = 0; i < k.length; ++i) {
var kk = k[i]
var v0 = e[kk]
var v1 = f(kk, v0)
if (v0 !== v1) e[kk] = v1;
if (typeof v1 === "object") desc(v1, f)
}
}
}
function ehField(k:string) {
return (k == "condition" || k == "expr" || k == "bound" || k == "collection")
}
function quote(s:string) {
return s.toLowerCase().replace(/[ \r\n\t]/g, "")
}
function allowString(e:HistoryEntry) {
if (!e.scriptId) return
desc([e.ast, e.updates], (k, v) => {
if (ehField(k)) {
Json.shortToTokens(v).forEach(tok => {
if (tok.nodeType === "stringLiteral") {
allowedStrings[quote(tok.value)] = 1
// console.log("ok " + JSON.stringify(tok.value))
}
})
}
return v;
})
}
var allowedStrings:any = {};
([
"Touch Develop is cool!", "tap to create bubbles", "hello", "hello world", "hello world!"
]).forEach(s => {
allowedStrings[quote(s)] = 1
})
var disallowCount = 0;
var disallowStrings:any = {}
function checkStrings(e:HistoryEntry) {
desc([e.ast, e.updates], (k, v) => {
if (ehField(k)) {
var toks = Json.shortToTokens(v)
var numSmurf = 0
toks.forEach(tok => {
if (tok.nodeType === "stringLiteral" && tok.value.length > 3 && !allowedStrings.hasOwnProperty(quote(tok.value))) {
if (!disallowStrings.hasOwnProperty(tok.value)) {
disallowStrings[tok.value] = "scrub" + disallowCount++
}
// console.log("remove " + JSON.stringify(tok.value))
tok.value = disallowStrings[tok.value]
numSmurf++
}
})
var v1 = toks.map(Json.longToShort).join(" ")
Util.assert(numSmurf > 0 || v == v1)
return v1
}
return v;
})
}
entries.forEach(allowString)
entries.forEach(checkStrings)
}
class IdCopier
extends NodeVisitor
{
visitStmt(s:Stmt)
{
if (s.diffAltStmt && !s.getStableName())
s.setStableName(s.diffAltStmt.getStableName())
this.visitChildren(s)
}
}
export class DiffStat
extends NodeVisitor
{
stats = {
numStmts: 0,
numMatched: 0,
numAdd: 0,
numDel: 0,
numChanged: 0,
totalChangedRatio: 0,
numHighlyChanged: 0,
numArtChanges: 0,
numNumberChanges: 0,
numStringChanges: 0,
numCommentChanges: 0,
numOtherChanges: 0,
}
visitBlock(b:Block)
{
this.visitChildren(b)
}
processDiffTokens(dt:Token[])
{
var stringChanges = 0
var numChanges = 0
var generalChanges = 0
var artChanges = 0
var prevArt = false
for (var i = 0; i < dt.length; i += 2) {
if (dt[i] && dt[i+1]) {
prevArt = (dt[i] instanceof ThingRef && (<ThingRef>dt[i]).data == "art")
} else {
var tok = dt[i] || dt[i+1]
if (tok instanceof Literal && typeof (<Literal>tok).data == "string")
stringChanges++
else if (tok instanceof Operator && /^[0-9\.]$/.test((<Operator>tok).data))
numChanges++
else if (prevArt && tok instanceof PropertyRef)
artChanges++
else
generalChanges++
}
}
var st = this.stats
st.numArtChanges += artChanges
st.numStringChanges += stringChanges
if (numChanges) st.numNumberChanges++
if (generalChanges) st.numOtherChanges++
}
visitStmt(s:Stmt)
{
var st = this.stats
if (s.diffAltStmt) {
st.numMatched++
if (s instanceof GlobalDef && (<GlobalDef>s).isResource) {
if ((<GlobalDef>s.diffAltStmt).url != (<GlobalDef>s).url)
st.numArtChanges++
}
} else {
// if (s.diffStatus) console.log(s.diffStatus + " " + s.serialize())
if (s.diffStatus < 0) st.numDel++
else if (s.diffStatus > 0) st.numAdd++
if (s.diffStatus > 0) {
if (s instanceof GlobalDef && (<GlobalDef>s).isResource) {
st.numArtChanges++
} else {
st.numOtherChanges++
}
}
}
st.numStmts++
if (s.diffAltStmt &&
s instanceof Comment)
{
if ((<Comment>s).text != (<Comment>s.diffAltStmt).text) {
st.numMatched--
st.numAdd++
st.numDel++
st.numCommentChanges++
}
}
var eh = s.calcNode()
if (eh && eh.diffTokens) {
st.numChanged++
var dr = diffRatio(eh)
st.totalChangedRatio += dr
if (dr > 0.5) st.numHighlyChanged++
if (s.diffAltStmt) this.processDiffTokens(eh.diffTokens)
}
this.visitChildren(s)
}
static run(a:App)
{
var ds = new DiffStat()
ds.dispatch(a)
return ds.stats
}
}
export interface AssignResult {
text: string;
info: any;
}
export function assignIds(oldText:string, newText:string):AssignResult
{
function prep(s:string)
{
var app = AST.Parser.parseScript(s, [])
app.isTopLevel = true;
TypeChecker.tcScript(app, true);
return app;
}
var oldApp = prep(oldText)
var newApp = prep(newText)
new InitIdVisitor(false).dispatch(oldApp)
//new InitIdVisitor(false).dispatch(newApp)
diffApps(oldApp, newApp, {
useStableNames: true,
//tutorialMode: true,
})
var info = {
oldApp: DiffStat.run(oldApp),
newApp: DiffStat.run(newApp)
}
var copier = new IdCopier()
copier.dispatch(newApp)
new InitIdVisitor(false).dispatch(newApp) // and set the remaining ones
if (oldText)
newApp.rootId = oldApp.rootId
newApp.hasIds = true
var newTextIds = newApp.serialize()
var app2 = prep(newTextIds)
new InitIdVisitor(false).expectSet(app2)
return {
text: newTextIds,
info: info
}
}
}