@markw65/monkeyc-optimizer 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,3 +161,20 @@ More fixes found via open source projects.
161
161
  - Bug fixes
162
162
  - Inject the superclass name into the classes namespace
163
163
  - Separate type vs value lookup, and use the correct one based on context.
164
+
165
+ ### 1.0.16
166
+
167
+ - Bug fixes
168
+
169
+ - Fix off-by-one in removeNodeComments
170
+ - Fix lookup to consistently lookup types or values.
171
+ - Fix lookup of superclass names
172
+
173
+ - New Features
174
+
175
+ - Add a simple inliner
176
+ - Add support for conditional inlining based on excludeAnnotations
177
+
178
+ - Testing
179
+ - Add support for @match pragmas to check the optimization results
180
+ - Add a test project, with some inlining tests
package/build/api.cjs CHANGED
@@ -233,6 +233,276 @@ const promises_namespaceObject = require("fs/promises");
233
233
  var standalone = __webpack_require__(3945);
234
234
  ;// CONCATENATED MODULE: external "./api.cjs"
235
235
  const external_api_cjs_namespaceObject = require("./api.cjs");
236
+ ;// CONCATENATED MODULE: ./src/inliner.ts
237
+
238
+ function canInline(state, func, args) {
239
+ // determine whether decl might be changed by a function call
240
+ // during the evaluation of FunctionStateNode.
241
+ const getSafety = (decl) => {
242
+ // enums are constant, they cant change
243
+ if (decl.type === "EnumStringMember")
244
+ return true;
245
+ if (decl.type === "VariableDeclarator") {
246
+ // constants also can't change
247
+ if (decl.node.kind === "const")
248
+ return true;
249
+ // if decl is a local, it also can't be changed
250
+ // by a call to another function.
251
+ for (let i = 0;; i++) {
252
+ if (!state.stack[i] || decl.stack[i] !== state.stack[i])
253
+ return false;
254
+ if (state.stack[i].type === "FunctionDeclaration")
255
+ return true;
256
+ }
257
+ }
258
+ return null;
259
+ };
260
+ const safeArgs = [];
261
+ let allSafe = true;
262
+ if (!args.every((arg) => {
263
+ switch (arg.type) {
264
+ case "Literal":
265
+ safeArgs.push(true);
266
+ return true;
267
+ case "Identifier":
268
+ case "MemberExpression": {
269
+ const [, results] = state.lookup(arg);
270
+ if (!results || results.length !== 1)
271
+ return false;
272
+ const safety = getSafety(results[0]);
273
+ if (safety === null)
274
+ return false;
275
+ if (!safety)
276
+ allSafe = false;
277
+ safeArgs.push(safety);
278
+ return true;
279
+ }
280
+ }
281
+ return false;
282
+ })) {
283
+ return false;
284
+ }
285
+ if (allSafe)
286
+ return true;
287
+ let callSeen = false;
288
+ let ok = true;
289
+ const params = Object.fromEntries(func.node.params.map((param, i) => [variableDeclarationName(param), i]));
290
+ const getLoc = (node) => (Array.isArray(node) ? node[0].start : node.start) || 0;
291
+ // look for uses of "unsafe" args that occur after a call.
292
+ // use post to do the checking, because arguments are evaluated
293
+ // prior to the call, so eg "return f(x.y);" is fine, but
294
+ // "return f()+x.y" is not.
295
+ //
296
+ // We also have to use a "pre" to ensure that child nodes are
297
+ // visited in source order (otherwise we could visit x.y before f()
298
+ // in the above example)
299
+ traverseAst(func.node.body, (node) => {
300
+ return Object.entries(node)
301
+ .filter((kv) => Array.isArray(kv[1])
302
+ ? kv[1].length !== 0 && hasProperty(kv[1][0], "type")
303
+ : hasProperty(kv[1], "type"))
304
+ .sort(([, a], [, b]) => getLoc(a) - getLoc(b))
305
+ .map(([key]) => key);
306
+ }, (node) => {
307
+ switch (node.type) {
308
+ case "CallExpression":
309
+ case "NewExpression":
310
+ callSeen = true;
311
+ break;
312
+ case "Identifier":
313
+ if (callSeen &&
314
+ hasProperty(params, node.name) &&
315
+ !safeArgs[params[node.name]]) {
316
+ ok = false;
317
+ }
318
+ }
319
+ });
320
+ return ok;
321
+ }
322
+ function inliningLooksUseful(func, node) {
323
+ while (true) {
324
+ if (node.type === "BinaryExpression" && node.operator === "as") {
325
+ node = node.left;
326
+ }
327
+ else if (node.type === "UnaryExpression" && node.operator === " as") {
328
+ node = node.argument;
329
+ }
330
+ else {
331
+ break;
332
+ }
333
+ }
334
+ if (node.type === "Literal")
335
+ return true;
336
+ if (node.type === "Identifier") {
337
+ if (func.params.length === 1 &&
338
+ variableDeclarationName(func.params[0]) === node.name) {
339
+ return 1;
340
+ }
341
+ return true;
342
+ }
343
+ return false;
344
+ }
345
+ function inliner_shouldInline(state, func, args) {
346
+ if (!func.node.body ||
347
+ func.node.body.body.length !== 1 ||
348
+ func.node.body.body[0].type !== "ReturnStatement" ||
349
+ !func.node.body.body[0].argument ||
350
+ func.node.params.length !== args.length) {
351
+ return false;
352
+ }
353
+ const autoInline = inliningLooksUseful(func.node, func.node.body.body[0].argument);
354
+ const excludeAnnotations = (func.node.loc?.source &&
355
+ state.fnMap[func.node.loc?.source].excludeAnnotations) ||
356
+ {};
357
+ return ((autoInline ||
358
+ (func.node.attrs &&
359
+ func.node.attrs.attrs &&
360
+ func.node.attrs.attrs.some((attr) => attr.type === "UnaryExpression" &&
361
+ (attr.argument.name === "inline" ||
362
+ (attr.argument.name.startsWith("inline_") &&
363
+ hasProperty(excludeAnnotations, attr.argument.name.substring(7))))))) &&
364
+ (autoInline === 1 || canInline(state, func, args)));
365
+ }
366
+ function inliner_inlineFunction(state, func, call) {
367
+ const retArg = JSON.parse(JSON.stringify(func.node.body.body[0].argument));
368
+ const params = Object.fromEntries(func.node.params.map((param, i) => [variableDeclarationName(param), i]));
369
+ try {
370
+ const result = traverseAst(retArg, (node) => {
371
+ switch (node.type) {
372
+ case "MemberExpression":
373
+ if (!node.computed) {
374
+ return ["object"];
375
+ }
376
+ break;
377
+ case "BinaryExpression":
378
+ if (node.operator === "as") {
379
+ return ["left"];
380
+ }
381
+ break;
382
+ case "UnaryExpression":
383
+ if (node.operator === " as") {
384
+ return [];
385
+ }
386
+ }
387
+ return null;
388
+ }, (node) => {
389
+ switch (node.type) {
390
+ case "Identifier": {
391
+ if (hasProperty(params, node.name)) {
392
+ return call.arguments[params[node.name]];
393
+ }
394
+ const rep = fixNodeScope(state, node, func.stack);
395
+ if (!rep) {
396
+ throw new Error(`Inliner: Couldn't fix the scope of '${node.name}`);
397
+ }
398
+ return rep;
399
+ }
400
+ }
401
+ return null;
402
+ }) || retArg;
403
+ result.loc = call.loc;
404
+ result.start = call.start;
405
+ result.end = call.end;
406
+ return result;
407
+ }
408
+ catch (ex) {
409
+ if (ex instanceof Error) {
410
+ if (ex.message.startsWith("Inliner: ")) {
411
+ return null;
412
+ }
413
+ }
414
+ throw ex;
415
+ }
416
+ }
417
+ function applyTypeIfNeeded(node) {
418
+ if ("enumType" in node && node.enumType) {
419
+ node = {
420
+ type: "BinaryExpression",
421
+ operator: "as",
422
+ left: node,
423
+ right: { type: "TypeSpecList", ts: [node.enumType] },
424
+ };
425
+ }
426
+ return node;
427
+ }
428
+ function fixNodeScope(state, lookupNode, nodeStack) {
429
+ const [, original] = state.lookup(lookupNode, null, nodeStack);
430
+ if (!original) {
431
+ return null;
432
+ }
433
+ const [, current] = state.lookup(lookupNode);
434
+ // For now, leave it alone if it already maps to the same thing.
435
+ // With a bit more work, we could find the guaranteed shortest
436
+ // reference, and then use this to optimize *all* symbols, not
437
+ // just fix inlined ones.
438
+ if (current &&
439
+ current.length === original.length &&
440
+ current.every((item, index) => item == original[index])) {
441
+ return lookupNode;
442
+ }
443
+ const node = lookupNode.type === "Identifier"
444
+ ? lookupNode
445
+ : lookupNode.property;
446
+ if (original.length === 1 && original[0].type === "EnumStringMember") {
447
+ return applyTypeIfNeeded(original[0].init);
448
+ }
449
+ const prefixes = original.map((sn) => {
450
+ if (isStateNode(sn) && sn.fullName) {
451
+ return sn.fullName;
452
+ }
453
+ return "";
454
+ });
455
+ if (prefixes.length &&
456
+ prefixes[0].startsWith("$.") &&
457
+ prefixes.every((prefix, i) => !i || prefix === prefixes[i - 1])) {
458
+ const prefix = prefixes[0].split(".").slice(0, -1).reverse();
459
+ let found = false;
460
+ return prefix.reduce((current, name) => {
461
+ if (found)
462
+ return current;
463
+ const [, results] = state.lookup(current);
464
+ if (results &&
465
+ results.length === original.length &&
466
+ results.every((result, i) => result === original[i])) {
467
+ found = true;
468
+ return current;
469
+ }
470
+ const object = typeof name === "string"
471
+ ? {
472
+ type: "Identifier",
473
+ name,
474
+ start: node.start,
475
+ end: node.end,
476
+ loc: node.loc,
477
+ }
478
+ : name;
479
+ let root = null;
480
+ let property = current;
481
+ while (property.type !== "Identifier") {
482
+ root = property;
483
+ property = property.object;
484
+ }
485
+ const mb = {
486
+ type: "MemberExpression",
487
+ object,
488
+ property,
489
+ computed: false,
490
+ start: node.start,
491
+ end: node.end,
492
+ loc: node.loc,
493
+ };
494
+ if (root) {
495
+ root.object = mb;
496
+ }
497
+ else {
498
+ current = mb;
499
+ }
500
+ return current;
501
+ }, node);
502
+ }
503
+ return null;
504
+ }
505
+
236
506
  ;// CONCATENATED MODULE: external "./util.cjs"
237
507
  const external_util_cjs_namespaceObject = require("./util.cjs");
238
508
  ;// CONCATENATED MODULE: ./src/mc-rewrite.ts
@@ -240,6 +510,7 @@ const external_util_cjs_namespaceObject = require("./util.cjs");
240
510
 
241
511
 
242
512
 
513
+
243
514
  function processImports(allImports, lookup) {
244
515
  allImports.forEach(({ node, stack }) => {
245
516
  const [name, module] = lookup(node.id, ("as" in node && node.as && node.as.name) || null, stack);
@@ -359,30 +630,34 @@ function getFileASTs(fnMap) {
359
630
  }, true));
360
631
  }
361
632
  async function analyze(fnMap) {
362
- let excludeAnnotations;
363
633
  let hasTests = false;
364
634
  const allImports = [];
365
635
  const preState = {
636
+ fnMap,
366
637
  allFunctions: [],
367
638
  allClasses: [],
368
639
  shouldExclude(node) {
369
640
  if ("attrs" in node &&
370
641
  node.attrs &&
371
642
  "attrs" in node.attrs &&
372
- node.attrs.attrs) {
373
- return node.attrs.attrs.reduce((drop, attr) => {
374
- if (attr.type != "UnaryExpression")
375
- return drop;
376
- if (attr.argument.type != "Identifier")
643
+ node.attrs.attrs &&
644
+ node.loc?.source) {
645
+ const excludeAnnotations = fnMap[node.loc.source].excludeAnnotations;
646
+ if (excludeAnnotations) {
647
+ return node.attrs.attrs.reduce((drop, attr) => {
648
+ if (attr.type != "UnaryExpression")
649
+ return drop;
650
+ if (attr.argument.type != "Identifier")
651
+ return drop;
652
+ if (hasProperty(excludeAnnotations, attr.argument.name)) {
653
+ return true;
654
+ }
655
+ if (attr.argument.name == "test") {
656
+ hasTests = true;
657
+ }
377
658
  return drop;
378
- if (hasProperty(excludeAnnotations, attr.argument.name)) {
379
- return true;
380
- }
381
- if (attr.argument.name == "test") {
382
- hasTests = true;
383
- }
384
- return drop;
385
- }, false);
659
+ }, false);
660
+ }
386
661
  }
387
662
  return false;
388
663
  },
@@ -431,7 +706,6 @@ async function analyze(fnMap) {
431
706
  if (!ast) {
432
707
  throw parserError || new Error(`Failed to parse ${name}`);
433
708
  }
434
- excludeAnnotations = value.excludeAnnotations;
435
709
  hasTests = false;
436
710
  collectNamespaces(ast, state);
437
711
  value.hasTests = hasTests;
@@ -455,8 +729,8 @@ function getLiteralFromDecls(decls) {
455
729
  let result = null;
456
730
  if (decls.every((d) => {
457
731
  if (d.type === "EnumStringMember" ||
458
- (d.type === "VariableDeclarator" && d.kind === "const")) {
459
- const init = getLiteralNode(d.init);
732
+ (d.type === "VariableDeclarator" && d.node.kind === "const")) {
733
+ const init = getLiteralNode(d.type === "EnumStringMember" ? d.init : d.node.init);
460
734
  if (!init)
461
735
  return false;
462
736
  if (!result) {
@@ -986,11 +1260,9 @@ async function optimizeMonkeyC(fnMap) {
986
1260
  }
987
1261
  return null;
988
1262
  }
989
- if (callees.length == 1) {
990
- const callee = isStateNode(callees[0]) && callees[0].node;
991
- if (callee &&
992
- callee.type == "FunctionDeclaration" &&
993
- callee.optimizable &&
1263
+ if (callees.length == 1 && callees[0].type === "FunctionDeclaration") {
1264
+ const callee = callees[0].node;
1265
+ if (callee.optimizable &&
994
1266
  !callee.hasOverride &&
995
1267
  node.arguments.every((n) => getNodeValue(n)[0] !== null)) {
996
1268
  const ret = evaluateFunction(callee, node.arguments);
@@ -999,6 +1271,13 @@ async function optimizeMonkeyC(fnMap) {
999
1271
  return null;
1000
1272
  }
1001
1273
  }
1274
+ if (shouldInline(state, callees[0], node.arguments)) {
1275
+ const ret = inlineFunction(state, callees[0], node);
1276
+ if (ret) {
1277
+ replace(node, ret);
1278
+ return null;
1279
+ }
1280
+ }
1002
1281
  }
1003
1282
  if (!hasProperty(state.calledFunctions, name)) {
1004
1283
  state.calledFunctions[name] = [];
@@ -1012,6 +1291,8 @@ async function optimizeMonkeyC(fnMap) {
1012
1291
  Object.values(fnMap).forEach((f) => {
1013
1292
  collectNamespaces(f.ast, state);
1014
1293
  });
1294
+ delete state.pre;
1295
+ delete state.post;
1015
1296
  const cleanup = (node) => {
1016
1297
  switch (node.type) {
1017
1298
  case "EnumStringBody":
@@ -1178,13 +1459,14 @@ async function api_getApiMapping(state) {
1178
1459
  filepath: "api.mir",
1179
1460
  }), state);
1180
1461
  negativeFixups.forEach((fixup) => {
1181
- const value = fixup.split(".").reduce((state, part) => {
1462
+ const vs = fixup.split(".").reduce((state, part) => {
1182
1463
  const decls = api_isStateNode(state) && state.decls?.[part];
1183
1464
  if (!Array.isArray(decls) || decls.length != 1 || !decls[0]) {
1184
1465
  throw `Failed to find and fix negative constant ${fixup}`;
1185
1466
  }
1186
1467
  return decls[0];
1187
1468
  }, result);
1469
+ const value = api_isStateNode(vs) ? vs.node : vs;
1188
1470
  if (value.type !== "EnumStringMember" &&
1189
1471
  (value.type !== "VariableDeclarator" || value.kind != "const")) {
1190
1472
  throw `Negative constant ${fixup} did not refer to a constant`;
@@ -1221,8 +1503,19 @@ function api_variableDeclarationName(node) {
1221
1503
  return ("left" in node ? node.left : node).name;
1222
1504
  }
1223
1505
  function checkOne(ns, decls, name) {
1224
- if (api_isStateNode(ns) && api_hasProperty(ns[decls], name)) {
1225
- return ns[decls][name];
1506
+ if (api_isStateNode(ns)) {
1507
+ if (api_hasProperty(ns[decls], name)) {
1508
+ return ns[decls][name];
1509
+ }
1510
+ if (ns.type == "ClassDeclaration" &&
1511
+ ns.superClass &&
1512
+ ns.superClass !== true) {
1513
+ const found = ns.superClass
1514
+ .map((cls) => checkOne(cls, decls, name))
1515
+ .filter((n) => n != null)
1516
+ .flat(1);
1517
+ return found.length ? found : null;
1518
+ }
1226
1519
  }
1227
1520
  return null;
1228
1521
  }
@@ -1232,7 +1525,7 @@ function lookup(state, decls, node, name, stack) {
1232
1525
  case "MemberExpression": {
1233
1526
  if (node.property.type != "Identifier" || node.computed)
1234
1527
  break;
1235
- const [, module, where] = state.lookup(node.object, name, stack);
1528
+ const [, module, where] = lookup(state, decls, node.object, name, stack);
1236
1529
  if (module && module.length === 1) {
1237
1530
  const result = checkOne(module[0], decls, node.property.name);
1238
1531
  if (result) {
@@ -1283,22 +1576,18 @@ function api_collectNamespaces(ast, stateIn) {
1283
1576
  state.removeNodeComments = (node, ast) => {
1284
1577
  if (node.start && node.end && ast.comments && ast.comments.length) {
1285
1578
  let low = 0, high = ast.comments.length;
1286
- while (true) {
1579
+ while (high > low) {
1287
1580
  const mid = (low + high) >> 1;
1288
- if (mid == low) {
1289
- if (ast.comments[mid].start < node.start) {
1290
- return;
1291
- }
1292
- break;
1293
- }
1294
1581
  if (ast.comments[mid].start < node.start) {
1295
- low = mid;
1582
+ low = mid + 1;
1296
1583
  }
1297
1584
  else {
1298
1585
  high = mid;
1299
1586
  }
1300
1587
  }
1301
- for (high = low; high < ast.comments.length && ast.comments[high].end < node.end; high++) { }
1588
+ while (high < ast.comments.length && ast.comments[high].end < node.end) {
1589
+ high++;
1590
+ }
1302
1591
  if (high > low) {
1303
1592
  ast.comments.splice(low, high - low);
1304
1593
  }
@@ -1401,7 +1690,17 @@ function api_collectNamespaces(ast, stateIn) {
1401
1690
  if (!api_hasProperty(parent.type_decls, name)) {
1402
1691
  parent.type_decls[name] = [];
1403
1692
  }
1404
- (0,external_util_cjs_namespaceObject.pushUnique)(parent.type_decls[name], node);
1693
+ else if (parent.type_decls[name].find((n) => (api_isStateNode(n) ? n.node : n) == node)) {
1694
+ break;
1695
+ }
1696
+ parent.type_decls[name].push(node.type === "EnumDeclaration"
1697
+ ? node
1698
+ : {
1699
+ type: "TypedefDeclaration",
1700
+ node,
1701
+ name,
1702
+ fullName: parent.fullName + "." + name,
1703
+ });
1405
1704
  break;
1406
1705
  }
1407
1706
  case "VariableDeclaration": {
@@ -1409,13 +1708,23 @@ function api_collectNamespaces(ast, stateIn) {
1409
1708
  if (!parent.decls)
1410
1709
  parent.decls = {};
1411
1710
  const decls = parent.decls;
1711
+ const stack = state.stack.slice();
1412
1712
  node.declarations.forEach((decl) => {
1413
1713
  const name = api_variableDeclarationName(decl.id);
1414
1714
  if (!api_hasProperty(decls, name)) {
1415
1715
  decls[name] = [];
1416
1716
  }
1717
+ else if (decls[name].find((n) => (api_isStateNode(n) ? n.node : n) == decl)) {
1718
+ return;
1719
+ }
1417
1720
  decl.kind = node.kind;
1418
- (0,external_util_cjs_namespaceObject.pushUnique)(decls[name], decl);
1721
+ (0,external_util_cjs_namespaceObject.pushUnique)(decls[name], {
1722
+ type: "VariableDeclarator",
1723
+ node: decl,
1724
+ name,
1725
+ fullName: parent.fullName + "." + name,
1726
+ stack,
1727
+ });
1419
1728
  if (node.kind == "const") {
1420
1729
  if (!api_hasProperty(state.index, name)) {
1421
1730
  state.index[name] = [];
@@ -10791,11 +10791,282 @@ function simulateProgram(prg, device, test) {
10791
10791
  return (0,external_sdk_util_cjs_namespaceObject.getSdkPath)().then((sdk) => (0,external_util_cjs_namespaceObject.spawnByLine)(external_path_.resolve(sdk, "bin", "monkeydo"), args, (line) => console.log(line)).then(() => { }));
10792
10792
  }
10793
10793
 
10794
+ ;// CONCATENATED MODULE: ./src/inliner.ts
10795
+
10796
+ function canInline(state, func, args) {
10797
+ // determine whether decl might be changed by a function call
10798
+ // during the evaluation of FunctionStateNode.
10799
+ const getSafety = (decl) => {
10800
+ // enums are constant, they cant change
10801
+ if (decl.type === "EnumStringMember")
10802
+ return true;
10803
+ if (decl.type === "VariableDeclarator") {
10804
+ // constants also can't change
10805
+ if (decl.node.kind === "const")
10806
+ return true;
10807
+ // if decl is a local, it also can't be changed
10808
+ // by a call to another function.
10809
+ for (let i = 0;; i++) {
10810
+ if (!state.stack[i] || decl.stack[i] !== state.stack[i])
10811
+ return false;
10812
+ if (state.stack[i].type === "FunctionDeclaration")
10813
+ return true;
10814
+ }
10815
+ }
10816
+ return null;
10817
+ };
10818
+ const safeArgs = [];
10819
+ let allSafe = true;
10820
+ if (!args.every((arg) => {
10821
+ switch (arg.type) {
10822
+ case "Literal":
10823
+ safeArgs.push(true);
10824
+ return true;
10825
+ case "Identifier":
10826
+ case "MemberExpression": {
10827
+ const [, results] = state.lookup(arg);
10828
+ if (!results || results.length !== 1)
10829
+ return false;
10830
+ const safety = getSafety(results[0]);
10831
+ if (safety === null)
10832
+ return false;
10833
+ if (!safety)
10834
+ allSafe = false;
10835
+ safeArgs.push(safety);
10836
+ return true;
10837
+ }
10838
+ }
10839
+ return false;
10840
+ })) {
10841
+ return false;
10842
+ }
10843
+ if (allSafe)
10844
+ return true;
10845
+ let callSeen = false;
10846
+ let ok = true;
10847
+ const params = Object.fromEntries(func.node.params.map((param, i) => [(0,external_api_cjs_namespaceObject.variableDeclarationName)(param), i]));
10848
+ const getLoc = (node) => (Array.isArray(node) ? node[0].start : node.start) || 0;
10849
+ // look for uses of "unsafe" args that occur after a call.
10850
+ // use post to do the checking, because arguments are evaluated
10851
+ // prior to the call, so eg "return f(x.y);" is fine, but
10852
+ // "return f()+x.y" is not.
10853
+ //
10854
+ // We also have to use a "pre" to ensure that child nodes are
10855
+ // visited in source order (otherwise we could visit x.y before f()
10856
+ // in the above example)
10857
+ (0,external_api_cjs_namespaceObject.traverseAst)(func.node.body, (node) => {
10858
+ return Object.entries(node)
10859
+ .filter((kv) => Array.isArray(kv[1])
10860
+ ? kv[1].length !== 0 && (0,external_api_cjs_namespaceObject.hasProperty)(kv[1][0], "type")
10861
+ : (0,external_api_cjs_namespaceObject.hasProperty)(kv[1], "type"))
10862
+ .sort(([, a], [, b]) => getLoc(a) - getLoc(b))
10863
+ .map(([key]) => key);
10864
+ }, (node) => {
10865
+ switch (node.type) {
10866
+ case "CallExpression":
10867
+ case "NewExpression":
10868
+ callSeen = true;
10869
+ break;
10870
+ case "Identifier":
10871
+ if (callSeen &&
10872
+ (0,external_api_cjs_namespaceObject.hasProperty)(params, node.name) &&
10873
+ !safeArgs[params[node.name]]) {
10874
+ ok = false;
10875
+ }
10876
+ }
10877
+ });
10878
+ return ok;
10879
+ }
10880
+ function inliningLooksUseful(func, node) {
10881
+ while (true) {
10882
+ if (node.type === "BinaryExpression" && node.operator === "as") {
10883
+ node = node.left;
10884
+ }
10885
+ else if (node.type === "UnaryExpression" && node.operator === " as") {
10886
+ node = node.argument;
10887
+ }
10888
+ else {
10889
+ break;
10890
+ }
10891
+ }
10892
+ if (node.type === "Literal")
10893
+ return true;
10894
+ if (node.type === "Identifier") {
10895
+ if (func.params.length === 1 &&
10896
+ (0,external_api_cjs_namespaceObject.variableDeclarationName)(func.params[0]) === node.name) {
10897
+ return 1;
10898
+ }
10899
+ return true;
10900
+ }
10901
+ return false;
10902
+ }
10903
+ function shouldInline(state, func, args) {
10904
+ if (!func.node.body ||
10905
+ func.node.body.body.length !== 1 ||
10906
+ func.node.body.body[0].type !== "ReturnStatement" ||
10907
+ !func.node.body.body[0].argument ||
10908
+ func.node.params.length !== args.length) {
10909
+ return false;
10910
+ }
10911
+ const autoInline = inliningLooksUseful(func.node, func.node.body.body[0].argument);
10912
+ const excludeAnnotations = (func.node.loc?.source &&
10913
+ state.fnMap[func.node.loc?.source].excludeAnnotations) ||
10914
+ {};
10915
+ return ((autoInline ||
10916
+ (func.node.attrs &&
10917
+ func.node.attrs.attrs &&
10918
+ func.node.attrs.attrs.some((attr) => attr.type === "UnaryExpression" &&
10919
+ (attr.argument.name === "inline" ||
10920
+ (attr.argument.name.startsWith("inline_") &&
10921
+ (0,external_api_cjs_namespaceObject.hasProperty)(excludeAnnotations, attr.argument.name.substring(7))))))) &&
10922
+ (autoInline === 1 || canInline(state, func, args)));
10923
+ }
10924
+ function inlineFunction(state, func, call) {
10925
+ const retArg = JSON.parse(JSON.stringify(func.node.body.body[0].argument));
10926
+ const params = Object.fromEntries(func.node.params.map((param, i) => [(0,external_api_cjs_namespaceObject.variableDeclarationName)(param), i]));
10927
+ try {
10928
+ const result = (0,external_api_cjs_namespaceObject.traverseAst)(retArg, (node) => {
10929
+ switch (node.type) {
10930
+ case "MemberExpression":
10931
+ if (!node.computed) {
10932
+ return ["object"];
10933
+ }
10934
+ break;
10935
+ case "BinaryExpression":
10936
+ if (node.operator === "as") {
10937
+ return ["left"];
10938
+ }
10939
+ break;
10940
+ case "UnaryExpression":
10941
+ if (node.operator === " as") {
10942
+ return [];
10943
+ }
10944
+ }
10945
+ return null;
10946
+ }, (node) => {
10947
+ switch (node.type) {
10948
+ case "Identifier": {
10949
+ if ((0,external_api_cjs_namespaceObject.hasProperty)(params, node.name)) {
10950
+ return call.arguments[params[node.name]];
10951
+ }
10952
+ const rep = fixNodeScope(state, node, func.stack);
10953
+ if (!rep) {
10954
+ throw new Error(`Inliner: Couldn't fix the scope of '${node.name}`);
10955
+ }
10956
+ return rep;
10957
+ }
10958
+ }
10959
+ return null;
10960
+ }) || retArg;
10961
+ result.loc = call.loc;
10962
+ result.start = call.start;
10963
+ result.end = call.end;
10964
+ return result;
10965
+ }
10966
+ catch (ex) {
10967
+ if (ex instanceof Error) {
10968
+ if (ex.message.startsWith("Inliner: ")) {
10969
+ return null;
10970
+ }
10971
+ }
10972
+ throw ex;
10973
+ }
10974
+ }
10975
+ function applyTypeIfNeeded(node) {
10976
+ if ("enumType" in node && node.enumType) {
10977
+ node = {
10978
+ type: "BinaryExpression",
10979
+ operator: "as",
10980
+ left: node,
10981
+ right: { type: "TypeSpecList", ts: [node.enumType] },
10982
+ };
10983
+ }
10984
+ return node;
10985
+ }
10986
+ function fixNodeScope(state, lookupNode, nodeStack) {
10987
+ const [, original] = state.lookup(lookupNode, null, nodeStack);
10988
+ if (!original) {
10989
+ return null;
10990
+ }
10991
+ const [, current] = state.lookup(lookupNode);
10992
+ // For now, leave it alone if it already maps to the same thing.
10993
+ // With a bit more work, we could find the guaranteed shortest
10994
+ // reference, and then use this to optimize *all* symbols, not
10995
+ // just fix inlined ones.
10996
+ if (current &&
10997
+ current.length === original.length &&
10998
+ current.every((item, index) => item == original[index])) {
10999
+ return lookupNode;
11000
+ }
11001
+ const node = lookupNode.type === "Identifier"
11002
+ ? lookupNode
11003
+ : lookupNode.property;
11004
+ if (original.length === 1 && original[0].type === "EnumStringMember") {
11005
+ return applyTypeIfNeeded(original[0].init);
11006
+ }
11007
+ const prefixes = original.map((sn) => {
11008
+ if ((0,external_api_cjs_namespaceObject.isStateNode)(sn) && sn.fullName) {
11009
+ return sn.fullName;
11010
+ }
11011
+ return "";
11012
+ });
11013
+ if (prefixes.length &&
11014
+ prefixes[0].startsWith("$.") &&
11015
+ prefixes.every((prefix, i) => !i || prefix === prefixes[i - 1])) {
11016
+ const prefix = prefixes[0].split(".").slice(0, -1).reverse();
11017
+ let found = false;
11018
+ return prefix.reduce((current, name) => {
11019
+ if (found)
11020
+ return current;
11021
+ const [, results] = state.lookup(current);
11022
+ if (results &&
11023
+ results.length === original.length &&
11024
+ results.every((result, i) => result === original[i])) {
11025
+ found = true;
11026
+ return current;
11027
+ }
11028
+ const object = typeof name === "string"
11029
+ ? {
11030
+ type: "Identifier",
11031
+ name,
11032
+ start: node.start,
11033
+ end: node.end,
11034
+ loc: node.loc,
11035
+ }
11036
+ : name;
11037
+ let root = null;
11038
+ let property = current;
11039
+ while (property.type !== "Identifier") {
11040
+ root = property;
11041
+ property = property.object;
11042
+ }
11043
+ const mb = {
11044
+ type: "MemberExpression",
11045
+ object,
11046
+ property,
11047
+ computed: false,
11048
+ start: node.start,
11049
+ end: node.end,
11050
+ loc: node.loc,
11051
+ };
11052
+ if (root) {
11053
+ root.object = mb;
11054
+ }
11055
+ else {
11056
+ current = mb;
11057
+ }
11058
+ return current;
11059
+ }, node);
11060
+ }
11061
+ return null;
11062
+ }
11063
+
10794
11064
  ;// CONCATENATED MODULE: ./src/mc-rewrite.ts
10795
11065
 
10796
11066
 
10797
11067
 
10798
11068
 
11069
+
10799
11070
  function processImports(allImports, lookup) {
10800
11071
  allImports.forEach(({ node, stack }) => {
10801
11072
  const [name, module] = lookup(node.id, ("as" in node && node.as && node.as.name) || null, stack);
@@ -10914,30 +11185,34 @@ function getFileASTs(fnMap) {
10914
11185
  }, true));
10915
11186
  }
10916
11187
  async function analyze(fnMap) {
10917
- let excludeAnnotations;
10918
11188
  let hasTests = false;
10919
11189
  const allImports = [];
10920
11190
  const preState = {
11191
+ fnMap,
10921
11192
  allFunctions: [],
10922
11193
  allClasses: [],
10923
11194
  shouldExclude(node) {
10924
11195
  if ("attrs" in node &&
10925
11196
  node.attrs &&
10926
11197
  "attrs" in node.attrs &&
10927
- node.attrs.attrs) {
10928
- return node.attrs.attrs.reduce((drop, attr) => {
10929
- if (attr.type != "UnaryExpression")
10930
- return drop;
10931
- if (attr.argument.type != "Identifier")
11198
+ node.attrs.attrs &&
11199
+ node.loc?.source) {
11200
+ const excludeAnnotations = fnMap[node.loc.source].excludeAnnotations;
11201
+ if (excludeAnnotations) {
11202
+ return node.attrs.attrs.reduce((drop, attr) => {
11203
+ if (attr.type != "UnaryExpression")
11204
+ return drop;
11205
+ if (attr.argument.type != "Identifier")
11206
+ return drop;
11207
+ if ((0,external_api_cjs_namespaceObject.hasProperty)(excludeAnnotations, attr.argument.name)) {
11208
+ return true;
11209
+ }
11210
+ if (attr.argument.name == "test") {
11211
+ hasTests = true;
11212
+ }
10932
11213
  return drop;
10933
- if ((0,external_api_cjs_namespaceObject.hasProperty)(excludeAnnotations, attr.argument.name)) {
10934
- return true;
10935
- }
10936
- if (attr.argument.name == "test") {
10937
- hasTests = true;
10938
- }
10939
- return drop;
10940
- }, false);
11214
+ }, false);
11215
+ }
10941
11216
  }
10942
11217
  return false;
10943
11218
  },
@@ -10986,7 +11261,6 @@ async function analyze(fnMap) {
10986
11261
  if (!ast) {
10987
11262
  throw parserError || new Error(`Failed to parse ${name}`);
10988
11263
  }
10989
- excludeAnnotations = value.excludeAnnotations;
10990
11264
  hasTests = false;
10991
11265
  (0,external_api_cjs_namespaceObject.collectNamespaces)(ast, state);
10992
11266
  value.hasTests = hasTests;
@@ -11010,8 +11284,8 @@ function getLiteralFromDecls(decls) {
11010
11284
  let result = null;
11011
11285
  if (decls.every((d) => {
11012
11286
  if (d.type === "EnumStringMember" ||
11013
- (d.type === "VariableDeclarator" && d.kind === "const")) {
11014
- const init = getLiteralNode(d.init);
11287
+ (d.type === "VariableDeclarator" && d.node.kind === "const")) {
11288
+ const init = getLiteralNode(d.type === "EnumStringMember" ? d.init : d.node.init);
11015
11289
  if (!init)
11016
11290
  return false;
11017
11291
  if (!result) {
@@ -11541,11 +11815,9 @@ async function optimizeMonkeyC(fnMap) {
11541
11815
  }
11542
11816
  return null;
11543
11817
  }
11544
- if (callees.length == 1) {
11545
- const callee = (0,external_api_cjs_namespaceObject.isStateNode)(callees[0]) && callees[0].node;
11546
- if (callee &&
11547
- callee.type == "FunctionDeclaration" &&
11548
- callee.optimizable &&
11818
+ if (callees.length == 1 && callees[0].type === "FunctionDeclaration") {
11819
+ const callee = callees[0].node;
11820
+ if (callee.optimizable &&
11549
11821
  !callee.hasOverride &&
11550
11822
  node.arguments.every((n) => getNodeValue(n)[0] !== null)) {
11551
11823
  const ret = evaluateFunction(callee, node.arguments);
@@ -11554,6 +11826,13 @@ async function optimizeMonkeyC(fnMap) {
11554
11826
  return null;
11555
11827
  }
11556
11828
  }
11829
+ if (shouldInline(state, callees[0], node.arguments)) {
11830
+ const ret = inlineFunction(state, callees[0], node);
11831
+ if (ret) {
11832
+ replace(node, ret);
11833
+ return null;
11834
+ }
11835
+ }
11557
11836
  }
11558
11837
  if (!(0,external_api_cjs_namespaceObject.hasProperty)(state.calledFunctions, name)) {
11559
11838
  state.calledFunctions[name] = [];
@@ -11567,6 +11846,8 @@ async function optimizeMonkeyC(fnMap) {
11567
11846
  Object.values(fnMap).forEach((f) => {
11568
11847
  (0,external_api_cjs_namespaceObject.collectNamespaces)(f.ast, state);
11569
11848
  });
11849
+ delete state.pre;
11850
+ delete state.post;
11570
11851
  const cleanup = (node) => {
11571
11852
  switch (node.type) {
11572
11853
  case "EnumStringBody":
@@ -12054,6 +12335,7 @@ async function generateOneConfig(buildConfig, dependencyFiles, config) {
12054
12335
  // might have altered it), and that the options we care about haven't
12055
12336
  // changed
12056
12337
  if (hasTests != null &&
12338
+ !config.checkBuildPragmas &&
12057
12339
  configOptionsToCheck.every((option) => prevOptions[option] === config[option]) &&
12058
12340
  actualOptimizedFiles.length == Object.values(fnMap).length &&
12059
12341
  Object.values(fnMap)
@@ -12064,7 +12346,7 @@ async function generateOneConfig(buildConfig, dependencyFiles, config) {
12064
12346
  // the oldest optimized file, we don't need to regenerate
12065
12347
  const source_time = await (0,external_util_cjs_namespaceObject.last_modified)(Object.keys(fnMap).concat(dependencyFiles));
12066
12348
  const opt_time = await (0,external_util_cjs_namespaceObject.first_modified)(Object.values(fnMap).map((v) => v.output));
12067
- if (source_time < opt_time && 1653954367574 < opt_time) {
12349
+ if (source_time < opt_time && 1654222986123 < opt_time) {
12068
12350
  return hasTests;
12069
12351
  }
12070
12352
  }
@@ -12076,6 +12358,14 @@ async function generateOneConfig(buildConfig, dependencyFiles, config) {
12076
12358
  const dir = external_path_.dirname(name);
12077
12359
  await promises_namespaceObject.mkdir(dir, { recursive: true });
12078
12360
  const opt_source = (0,external_api_cjs_namespaceObject.formatAst)(info.ast, info.monkeyCSource);
12361
+ if (config.checkBuildPragmas) {
12362
+ const matches = opt_source.matchAll(/^.*\/\*\s*@match\s+(\S+)\s+\*\/(.*)$/gm);
12363
+ for (const [line, needle, haystack] of matches) {
12364
+ if (!haystack.includes(needle)) {
12365
+ throw new Error(`Checking build pragmas in ${name} failed at \n\n${line}\n\n - Didn't find '${needle}'`);
12366
+ }
12367
+ }
12368
+ }
12079
12369
  await promises_namespaceObject.writeFile(name, opt_source);
12080
12370
  return info.hasTests;
12081
12371
  })).then((results) => {
@@ -12133,11 +12423,12 @@ async function generateApiMirTests(options) {
12133
12423
  return;
12134
12424
  const d = decl[0];
12135
12425
  if (d.type === "EnumStringMember" ||
12136
- (d.type === "VariableDeclarator" && d.kind === "const")) {
12137
- if (!d.init) {
12426
+ (d.type === "VariableDeclarator" && d.node.kind === "const")) {
12427
+ const init = (0,external_api_cjs_namespaceObject.isStateNode)(d) ? d.node.init : d.init;
12428
+ if (!init) {
12138
12429
  throw new Error(`Missing init for ${node.fullName}.${key}`);
12139
12430
  }
12140
- tests.push([`${node.fullName}.${key}`, (0,external_api_cjs_namespaceObject.formatAst)(d.init)]);
12431
+ tests.push([`${node.fullName}.${key}`, (0,external_api_cjs_namespaceObject.formatAst)(init)]);
12141
12432
  }
12142
12433
  else if ((0,external_api_cjs_namespaceObject.isStateNode)(d)) {
12143
12434
  findConstants(d);
@@ -0,0 +1,4 @@
1
+ import { mctree } from "@markw65/prettier-plugin-monkeyc";
2
+ export declare function shouldInline(state: ProgramStateAnalysis, func: FunctionStateNode, args: mctree.Node[]): boolean | undefined;
3
+ export declare function inlineFunction(state: ProgramStateAnalysis, func: FunctionStateNode, call: mctree.CallExpression): any;
4
+ export declare function applyTypeIfNeeded(node: mctree.Node): mctree.Node;
@@ -34,12 +34,13 @@ declare global {
34
34
  ignoredAnnotations?: string;
35
35
  ignoredSourcePaths?: string;
36
36
  returnCommand?: boolean;
37
+ checkBuildPragmas?: boolean;
37
38
  _cache?: {
38
39
  barrels?: Record<string, ResolvedJungle>;
39
40
  barrelMap?: Record<string, Record<string, ResolvedJungle>>;
40
41
  };
41
42
  };
42
- type StateNodeDecl = StateNode | mctree.EnumStringMember | mctree.TypedIdentifier | mctree.EnumDeclaration | mctree.TypedefDeclaration | mctree.VariableDeclarator;
43
+ type StateNodeDecl = StateNode | mctree.EnumStringMember | mctree.TypedIdentifier | mctree.EnumDeclaration;
43
44
  type StateNodeDecls = {
44
45
  [key: string]: StateNodeDecl[];
45
46
  };
@@ -87,11 +88,25 @@ declare global {
87
88
  node: mctree.BlockStatement;
88
89
  stack?: undefined;
89
90
  }
90
- type StateNode = ProgramStateNode | FunctionStateNode | BlockStateNode | ClassStateNode | ModuleStateNode;
91
+ interface TypedefStateNode extends BaseStateNode {
92
+ type: "TypedefDeclaration";
93
+ node: mctree.TypedefDeclaration;
94
+ name: string;
95
+ fullName: string;
96
+ }
97
+ interface VariableStateNode extends BaseStateNode {
98
+ type: "VariableDeclarator";
99
+ node: mctree.VariableDeclarator;
100
+ name: string;
101
+ fullName: string;
102
+ stack: ProgramStateStack;
103
+ }
104
+ type StateNode = ProgramStateNode | FunctionStateNode | BlockStateNode | ClassStateNode | ModuleStateNode | TypedefStateNode | VariableStateNode;
91
105
  type ProgramStateStack = StateNode[];
92
106
  export type ProgramState = {
93
107
  allFunctions?: FunctionStateNode[];
94
108
  allClasses?: ClassStateNode[];
109
+ fnMap?: FilesToOptimizeMap;
95
110
  stack?: ProgramStateStack;
96
111
  removeNodeComments?: (node: mctree.Node, ast: mctree.Program) => void;
97
112
  shouldExclude?: (node: mctree.Node) => boolean;
@@ -128,7 +143,7 @@ declare global {
128
143
  [key in Keys]-?: NonNullable<T[key]>;
129
144
  };
130
145
  export type ProgramStateLive = Finalized<ProgramState, "stack" | "lookup" | "lookupValue" | "lookupType" | "traverse" | "index" | "constants" | "removeNodeComments" | "inType">;
131
- export type ProgramStateAnalysis = Finalized<ProgramStateLive, "allClasses" | "allFunctions">;
146
+ export type ProgramStateAnalysis = Finalized<ProgramStateLive, "allClasses" | "allFunctions" | "fnMap">;
132
147
  export type ProgramStateOptimizer = Finalized<ProgramStateAnalysis, "localsStack" | "exposed" | "calledFunctions">;
133
148
  type ExcludeAnnotationsMap = {
134
149
  [key: string]: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@markw65/monkeyc-optimizer",
3
3
  "type": "module",
4
- "version": "1.0.15",
4
+ "version": "1.0.16",
5
5
  "description": "Source to source optimizer for Garmin Monkey C code",
6
6
  "main": "build/optimizer.cjs",
7
7
  "types": "build/src/optimizer.d.ts",
@@ -20,8 +20,9 @@
20
20
  "build-debug": "webpack --mode development",
21
21
  "build-release": "webpack --mode production",
22
22
  "prepack": "webpack --mode production",
23
- "test": "npm run test-remote",
24
- "test-remote": "node ./test/test.js --product=pick-one --github"
23
+ "test": "npm run test-inline && npm run test-remote",
24
+ "test-remote": "node ./test/test.js --product=pick-one --github",
25
+ "test-inline": "node test/test.js --run-tests --product=fenix5 --product=fr235 --jungle $(pwd)/test/OptimizerTests/monkey.jungle"
25
26
  },
26
27
  "files": [
27
28
  "build/optimizer.cjs",