UNPKG

18.4 kBJavaScriptView Raw
1const {
2 Expr,
3 Gte,
4 Gt,
5 Or,
6 And,
7 Invoke,
8 Not,
9 Quote,
10 Eq,
11 Cond,
12 Token,
13 Expression,
14 SetterExpression,
15 TopLevel,
16 Root,
17 Get,
18 Clone,
19 WrappedPrimitive,
20 TokenTypeData,
21 SourceTag,
22 cloneToken
23} = require('./lang');
24const {memoizeExprFunc, memoize} = require('./memoize');
25const {exprHash} = require('./expr-hash');
26const {flattenExpression, getAllFunctions, flattenExpressionWithoutInnerFunctions} = require('./expr-search');
27const {tagToSimpleFilename} = require('./expr-names');
28const {rewriteStaticsToTopLevels, rewriteLocalsToFunctions, rewriteUniqueByHash} = require('./expr-rewrite');
29const {or, and, not} = require('./expr-logic');
30let exprCounter = 1;
31let taggedExprs = {};
32
33const _ = require('lodash');
34const toposort = require('toposort');
35
36
37function printPaths(title, paths) {
38 const output = []
39 paths.forEach((cond, path) => {
40 let condValue = cond
41 if (cond instanceof Expression) {
42 condValue = JSON.parse(JSON.stringify(cond))
43 } else if (cond) {
44 condValue = [cond[0].$id, cond[1]]
45 }
46 output.push([path.map(exprHash).join(','), condValue]);
47 })
48 console.log(title, output);
49}
50
51function genUsedOnlyAsBooleanValue(expr) {
52 const parent = expr[0].$parent;
53 const indexInParent = parent ? parent.indexOf(expr) : -1;
54 if (parent && (parent[0].$type === 'and' || parent[0].$type === 'or')) {
55 return Expr(Gt, Expr(Cond, parent.$id), indexInParent)
56 }
57 if (parent && (parent[0].$type === 'ternary' && indexInParent === 1)) {
58 return true;
59 }
60 if (parent && (parent[0].$type === 'get' && indexInParent === 2)) {
61 return true;
62 }
63 if (parent && (parent[0].$type === 'not' || parent.$type === 'isUndefined')) {
64 return true;
65 }
66 return false;
67}
68
69function countPathParts(pathAsStr) {
70 return pathAsStr.split(',').length;
71}
72
73function generatePathCondExpr(pathExpressions, pathAsStr, outputCondsByPathStr) {
74 const pathPartsCnt = countPathParts(pathAsStr);
75 const nearestDeeperPaths = Object.keys(outputCondsByPathStr).filter(otherPathStr => countPathParts(otherPathStr) === pathPartsCnt + 1 &&
76 otherPathStr.substr(0, pathAsStr.length) === pathAsStr);
77 const nearestDeeperPathsCond = or(...nearestDeeperPaths.map(otherPathStr => outputCondsByPathStr[otherPathStr]))
78 const condsOfOnlyTested = [];
79 const condsOfUsed = [];
80 pathExpressions.forEach(expr => {
81 let condOfExpr = true;
82 if (expr[0].$conditional) {
83 const condId = expr[0].$conditional[0][0].$id;
84 const condBranch = expr[0].$conditional[1];
85 const condIsTernary = expr[0].$conditional[0][0].$type === 'ternary';
86 condOfExpr = Expr(condIsTernary ? Eq : Gte, Expr(Cond, condId), condBranch);
87 }
88 const usedAsBool = genUsedOnlyAsBooleanValue(expr);
89 if (usedAsBool) {
90 condsOfOnlyTested.push(condOfExpr);
91 } else {
92 condsOfUsed.push(condOfExpr)
93 }
94 });
95 const touchedButNotDeeper = and(or(...condsOfOnlyTested), not(nearestDeeperPathsCond))
96 const pathCond = or(...condsOfUsed, touchedButNotDeeper)
97 // console.log(JSON.stringify(condOfTracking, null, 2));
98 return {pathCond, used: or(...condsOfUsed, ...condsOfOnlyTested)};
99}
100
101function groupPathsThatCanBeInvalidated(paths) {
102 const groupedPaths = {};
103 paths.forEach((cond, path) => {
104 const pathAsStr = path.map(part => {
105 if (typeof part === 'string' || typeof part === 'number') {
106 return `${part}`;
107 }
108 if (part instanceof Token && part.$type === 'root' || part.$type === 'topLevel') {
109 return `***${part.$type}***`;
110 }
111 return exprHash(part);
112 }).join(',');
113 groupedPaths[pathAsStr] = groupedPaths[pathAsStr] || [];
114 groupedPaths[pathAsStr].push(path);
115 });
116 const pathStringsSortedInnerFirst = Array.from(Object.keys(groupedPaths))
117 .sort()
118 .reverse()
119 // console.log(groupedPaths,pathStringsSortedInnerFirst)
120 const outputPaths = new Map();
121 const outputCondsByPathStr = {};
122 pathStringsSortedInnerFirst
123 .forEach(pathAsStr => {
124 const similiarPaths = groupedPaths[pathAsStr];
125 const {pathCond, used} = generatePathCondExpr(similiarPaths.map(path => paths.get(path)), pathAsStr, outputCondsByPathStr)
126 outputCondsByPathStr[pathAsStr] = used;
127 if (pathCond) {
128 outputPaths.set(similiarPaths[0], pathCond);
129 }
130 });
131 return outputPaths;
132}
133
134function annotatePathsThatCanBeInvalidated(exprsByFunc) {
135 const paths = new Map();
136 const allGettersChains = exprsByFunc.filter(
137 expr => expr[0].$type === 'get' && (!expr[0].$parent || expr[0].$parent[0].$type !== 'get' || expr[0].$parent[2] !== expr)
138 );
139 const foundByType = {root: false, context: false};
140 _.forEach(allGettersChains, chainedGetter => {
141 let currentStep = chainedGetter;
142 const path = [];
143 while (currentStep instanceof Expression && currentStep[0].$type === 'get') {
144 path.unshift(currentStep[1]);
145 currentStep = currentStep[2];
146 }
147 if (currentStep instanceof Token) {
148 foundByType[currentStep.$type] = true;
149 }
150 path.unshift(currentStep);
151 paths.set(path, chainedGetter);
152 });
153 exprsByFunc.forEach(expr => {
154 expr.forEach(token => {
155 if (token instanceof Token && foundByType[token.$type] === false) {
156 foundByType[token.$type] = true;
157 paths.set([token], expr)
158 }
159 })
160 });
161 return groupPathsThatCanBeInvalidated(paths);
162}
163
164function pathFragmentToString(token) {
165 if (typeof token === 'string' || typeof token === 'number') {
166 return token;
167 } else if (token.$type === 'root' || token.$type === 'topLevel') {
168 return token.$type;
169 } else if (token instanceof Expression && token[0].$type === 'invoke') {
170 return token[1];
171 }
172 return '*';
173}
174
175function pathToString(path) {
176 return path.map(pathFragmentToString).join('.');
177}
178/*
179function exprContains(expr, str) {
180 if (expr === str) {
181 return true;
182 } else if (expr instanceof Expression) {
183 return expr.some(child => exprContains(child, str));
184 }
185}
186*/
187
188function tagExpressionFunctionsWithPathsThatCanBeInvalidated(sourceExpr) {
189 const exprFuncs = getAllFunctions(sourceExpr);
190 _.forEach(exprFuncs, func => {
191 const allExprs = flattenExpression(func);
192 const allExprsInFunc = _.filter(allExprs, expr => func[0].$funcId === expr[0].$funcId);
193 func[0].$path = annotatePathsThatCanBeInvalidated(allExprsInFunc);
194 });
195}
196
197function tagExpressions(expr, name, currentDepth, indexChain, funcType, rootName) {
198 if (expr[0].hasOwnProperty('$id')) {
199 return; //Already tagged
200 }
201 const hash = exprHash(expr);
202 if (!taggedExprs[hash]) {
203 taggedExprs[hash] = exprCounter++;
204 }
205 expr[0].$id = taggedExprs[hash];
206 expr[0].$funcId = name;
207 expr[0].$rootName = rootName;
208 expr[0].$depth = currentDepth;
209 expr[0].$funcType = funcType;
210 expr[0].$tracked = false;
211 expr[0].$parent = null;
212 // console.log(expr[0].$id, expr[0].$type)
213 if (expr[0].$tokenType === 'abstract') {
214 throw new Error(`You defined a abstract in ${expr[0].SourceTag} called ${expr[1]} but did't resolve it`);
215 }
216 expr.forEach((subExpression, childIndex) => {
217 if (subExpression instanceof Expression) {
218 if (subExpression[0].$type !== 'func') {
219 tagExpressions(subExpression, name, currentDepth, indexChain.concat(childIndex), funcType, rootName);
220 } else {
221 subExpression[0].$funcType = expr[0].$type;
222 tagExpressions(subExpression, `${name}$${expr[0].$id}`, currentDepth + 1, indexChain, expr[0].$type, rootName);
223 }
224 subExpression[0].$parent = expr;
225 } else if (subExpression instanceof Token) {
226 subExpression.$funcId = name;
227 subExpression.$rootName = rootName;
228 subExpression.$depth = currentDepth;
229 subExpression.$funcType = funcType;
230 }
231 });
232}
233
234const cloneAndHash = expr => {
235 if (expr instanceof Expression) {
236 const hash = exprHash(expr);
237 const res = new Expression(...expr.map(cloneAndHash));
238 res[0].$hash = hash;
239 return res;
240 } else if (expr instanceof Token) {
241 return cloneToken(expr);
242 }
243 return expr;
244 }
245
246
247function cloneExpressions(getters) {
248 return _.mapValues(getters, getter => Clone(getter));
249}
250
251function tagAllExpressions(getters) {
252 exprCounter = 1;
253 taggedExprs = {};
254 _.forEach(getters, (getter, name) => tagExpressions(getter, name + exprCounter++, 0, [1], getter[0].$type === 'func' ? 'helperFunc' : 'topLevel', name));
255}
256
257function tagUnconditionalExpressions(expr, cond) {
258 if (!(expr instanceof Expression)) {
259 return;
260 }
261 expr[0].$conditional = cond;
262 const $type = expr[0].$type;
263 if ($type === 'or' || $type === 'and' || $type === 'ternary') {
264 tagUnconditionalExpressions(expr[1], cond);
265 expr.slice(2).forEach((subExpr, subIndex) => tagUnconditionalExpressions(subExpr, [expr, subIndex + 2]));
266 } else if ($type === 'func') {
267 tagUnconditionalExpressions(expr[1], false);
268 } else {
269 expr.slice(1).forEach(subExpr => tagUnconditionalExpressions(subExpr, cond));
270 }
271}
272
273function parentFunction(expr) {
274 if (expr[0].$type === 'func' || !expr[0].$parent) {
275 return expr;
276 }
277 return parentFunction(expr[0].$parent);
278}
279
280function unmarkPathsThatHaveNoSetters(getters, setters) {
281 const currentSetters = Object.values(setters);
282 // const invalidatingGetters = {}
283 topologicalSortGetters(getters).forEach(name => {
284 // console.log('unmarkPathsThatHaveNoSetters', name);
285 const getter = getters[name];
286 const allExprInGetter = flattenExpression([getter])
287 const exprPathsMaps = _(allExprInGetter)
288 .filter(e => e instanceof Expression && e[0].$path)
289 .map(e => e[0].$path)
290 .value();
291 let canBeExprBeInvalidated = allExprInGetter.some(e => e[0].$type === 'invoke' && getters[e[1]][0].$invalidates);
292 const condsThatAreTracked = new Set();
293 exprPathsMaps.forEach(pathMap => {
294 // console.log('pathsTracked', JSON.stringify(Array.from(pathMap.keys())));
295 pathMap.forEach((cond, path) => {
296 let trackCond = false;
297 const matchedSetter = _.some(currentSetters, setter => pathMatches(path, setter));
298 // console.log(name, matchedSetter, JSON.stringify(path))
299 if (matchedSetter) {
300 // console.log('path can be invalidated', JSON.stringify(path))
301 canBeExprBeInvalidated = true;
302 trackCond = true;
303 } else if (path[0].$type !== 'context') {
304 pathMap.delete(path);
305 } else {
306 canBeExprBeInvalidated = true;
307 trackCond = true;
308 }
309 if (cond && trackCond) {
310 const conditionalsByPath = flattenExpression([
311 cond
312 ]).filter(e => e instanceof Expression && e[0].$type === 'cond');
313 conditionalsByPath.forEach(condPath => condsThatAreTracked.add(condPath[1]));
314 }
315 if (path[0].$type === 'topLevel' && path.length > 2 && TokenTypeData[getters[path[1]][0].$type].stable) {
316 path.splice(0, 2, Expr(Get, path[1], TopLevel))
317 }
318 });
319 });
320 // if (name.indexOf('navigation') !== -1) {
321 // console.log('unmarkPathsThatHaveNoSetters', name, canBeExprBeInvalidated, JSON.stringify(getter));
322 // }
323 if (canBeExprBeInvalidated) {
324 if (getters[name][0].$type === 'func') {
325 currentSetters.push([name]);
326 } else {
327 currentSetters.push([TopLevel, name]);
328 }
329
330 // console.log('added', name, condsThatAreTracked.size)
331 }
332 if (condsThatAreTracked.size) {
333 // console.log(condsThatAreTracked)
334 allExprInGetter.forEach(e => {
335 if (e instanceof Expression && condsThatAreTracked.has(e[0].$id)) {
336 e[0].$tracked = true;
337 const parent = parentFunction(e);
338 // console.log('TRACKED COND', JSON.stringify(parent), e[0].$id);
339 parent[0].$trackedExpr = parent[0].$trackedExpr || new Set();
340 parent[0].$trackedExpr.add(e[0].$id);
341 }
342 })
343 }
344 allExprInGetter.forEach(expr => {
345 if (expr instanceof Expression) {
346 expr[0].$invalidates = canBeExprBeInvalidated;
347 }
348 })
349 // invalidatingGetters[name] = canBeExprBeInvalidated
350 });
351 // require('fs').writeFileSync('invalidates.json', JSON.stringify(invalidatingGetters, null, 2))
352}
353
354const wrapPrimitivesInQuotes = v => {
355 if (typeof v === 'boolean' || typeof v === 'string' || typeof v === 'number') {
356 return Expr(Quote, v);
357 }
358 if (v instanceof WrappedPrimitive) {
359 return Expr(Quote, v.toJSON());
360 }
361 return v;
362};
363
364const canHaveSideEffects = memoizeExprFunc(expr => {
365 if (expr[0].$type === 'call' || expr[0].$type === 'effect') {
366 return true;
367 }
368 return expr.some(child => canHaveSideEffects(child));
369}, () => false)
370
371/*eslint no-fallthrough:0*/
372/*eslint no-case-declarations:0*/
373const deadCodeElimination = memoizeExprFunc(
374 expr => {
375 const children = expr.map((child, idx) => deadCodeElimination(child));
376 const tokenType = expr[0].$type;
377 switch (tokenType) {
378 case 'quote':
379 return children[1];
380 case 'or':
381 const firstTruthy = expr.slice(1).findIndex(t => Object(t) !== t && t);
382 if (firstTruthy === 0) {
383 return children[1];
384 } else if (firstTruthy > 0) {
385 return Expr(...children.slice(0, firstTruthy + 2));
386 }
387 case 'and':
388 const firstFalsy = expr
389 .slice(1)
390 .findIndex(t => Object(t) !== t && !t || t instanceof Token && t.$type === 'null');
391 if (firstFalsy === 0) {
392 return children[1];
393 } else if (firstFalsy > 0) {
394 return Expr(...children.slice(0, firstFalsy + 2));
395 }
396 }
397 return children;
398 },
399 token => token
400);
401
402function dedupFunctionsObjects(getters) {
403 const prevFunctions = new Map();
404 const allExpressions = flattenExpression(...Object.values(getters));
405 const allFunctions = allExpressions.filter(expr => expr[0].$type === 'func' && expr[0].$parent)
406 let duplicateFunctions = 0;
407 allFunctions
408 .forEach(expr => {
409 const hash = `${exprHash(expr)}.${expr[0].$parent[0].$type}.${expr[0].$invalidates}`;
410 if (!prevFunctions.has(hash)) {
411 expr[0].$duplicate = false;
412 prevFunctions.set(hash, expr)
413 } else {
414 const prev = prevFunctions.get(hash);
415 duplicateFunctions++;
416 expr[0].$duplicate = prev[0].$funcId;
417 }
418 });
419 const countFunctions = allFunctions.length;
420 const countGetters = Object.keys(getters).length;
421 // console.error('duplicate stats:', {
422 // duplicateFunctions, countFunctions, countGetters
423 // })
424 const prevObjectKeys = new Map();
425 const allObjects = allExpressions.filter(expr => expr[0].$type === 'object')
426 allObjects.forEach(expr => {
427 const keys = _.range(1, expr.length, 2).map(index => expr[index]).join(',');
428 if (!prevObjectKeys.has(keys)) {
429 prevObjectKeys.set(keys, expr);
430 } else {
431 const prev = prevObjectKeys.get(keys);
432 expr[0].$duplicate = prev[0].$id;
433 }
434 });
435
436 // console.log('duplicated', allFunctions.length, prevFunctions.size);
437}
438
439const allPathsInGetter = memoize(getter => _(flattenExpression([getter]))
440 .filter(e => e instanceof Expression && e[0].$path)
441 .map(e => Array.from(e[0].$path.entries()))
442 .flatten()
443 .reduce((acc, item) => {
444 acc.set(item[0], item[1]);
445 return acc;
446 }, new Map()));
447
448function pathMatches(srcPath, trgPath) {
449 // console.log('pathMatches', JSON.stringify(srcPath), JSON.stringify(trgPath));
450 return srcPath.every((part, idx) => {
451 if (typeof trgPath[idx] === 'undefined') {
452 return true;
453 }
454 const srcFrag = pathFragmentToString(part);
455 const trgFrag = pathFragmentToString(trgPath[idx]);
456 return srcFrag === trgFrag || srcFrag === '*' || trgFrag === '*';
457 });
458}
459
460function collectAllTopLevelInExpr(expr, acc) {
461 acc = acc || {};
462 if (expr[0].$type === 'get' && expr[2] instanceof Token && expr[2].$type === 'topLevel' || expr[0].$type === 'invoke') {
463 acc[expr[1]] = true;
464 } else {
465 expr.forEach(token => {
466 if (token instanceof Expression) {
467 collectAllTopLevelInExpr(token, acc);
468 }
469 });
470 }
471 return acc;
472}
473
474function topologicalSortGetters(getters) {
475 const vertices = _(getters)
476 .map((expr, name) => {
477 const usedTopLevels = collectAllTopLevelInExpr(expr);
478 return Object.keys(usedTopLevels)
479 .map(topName => [topName, name])
480 .concat([[name, '$model']]);
481 })
482 .flatten()
483 .value();
484 return toposort(vertices).filter(x => x !== '$model');
485}
486
487function splitSettersGetters(model) {
488 const setters = _.pickBy(model, v => v instanceof SetterExpression);
489 _.forEach(setters, setter => {
490 if (!(setter[0] instanceof Token) || setter[0].$type !== 'root') {
491 setter.unshift(Root);
492 }
493 });
494 const getters = _.pickBy(model, v => v instanceof Expression || v instanceof WrappedPrimitive);
495 return {
496 getters,
497 setters
498 };
499}
500
501function findFuncExpr(getters, funcId) {
502 return _(getters)
503 .map(getAllFunctions)
504 .flatten()
505 .find(e => e[0].$funcId === funcId);
506}
507
508function normalizeAndTagAllGetters(getters, setters, options) {
509 getters = rewriteUniqueByHash(getters);
510 getters = _.mapValues(getters, getter => wrapPrimitivesInQuotes(deadCodeElimination(getter)));
511 getters = rewriteStaticsToTopLevels(getters);
512 getters = rewriteUniqueByHash(getters);
513 if (!options.disableHelperFunctions) {
514 getters = rewriteLocalsToFunctions(getters);
515 }
516 getters = cloneExpressions(getters);
517 tagAllExpressions(getters);
518 _.forEach(getters, getter => tagUnconditionalExpressions(getter, false));
519 _.forEach(getters, getter => tagExpressionFunctionsWithPathsThatCanBeInvalidated(getter));
520 unmarkPathsThatHaveNoSetters(getters, setters);
521 Object.keys(getters).forEach(name => {
522 // console.log(name, getters[name][0])
523 })
524 // _.forEach(getters, (e, name) => {
525 // flattenExpression(e).forEach(t => {
526 // console.log('unmarkPathsThatHaveNoSetters', name, t[0])
527 // })
528 // })
529 dedupFunctionsObjects(getters);
530 let index = 0;
531 topologicalSortGetters(getters).forEach(name => {
532 if (getters[name][0].$type !== 'func') {// not a helper function
533 getters[name][0].$topLevelIndex = index;
534 index++
535 }
536 })
537 return getters;
538}
539
540module.exports = {
541 pathMatches,
542 topologicalSortGetters,
543 pathFragmentToString,
544 tagAllExpressions,
545 splitSettersGetters,
546 normalizeAndTagAllGetters,
547 allPathsInGetter,
548 findFuncExpr,
549 tagToSimpleFilename
550};