pxt-core 13.2.1 → 13.2.3

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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/built/pxt.js +646 -137
  3. package/built/pxtcompiler.d.ts +19 -1
  4. package/built/pxtcompiler.js +519 -64
  5. package/built/pxtlib.d.ts +1 -1
  6. package/built/pxtlib.js +127 -73
  7. package/built/react-common/components/controls/DropdownModal.d.ts +6 -0
  8. package/built/react-common/components/controls/Input.d.ts +1 -1
  9. package/built/target.js +1 -1
  10. package/built/targetlight.js +1 -1
  11. package/built/tests/thumb-test/asmchecks.d.ts +73 -0
  12. package/built/tests/thumb-test/thumbrunner.d.ts +3 -0
  13. package/built/web/main.js +30 -30
  14. package/built/web/pxtapp.js +1 -1
  15. package/built/web/pxtasseteditor.js +20 -20
  16. package/built/web/pxtcompiler.js +1 -1
  17. package/built/web/pxtembed.js +4 -4
  18. package/built/web/pxtlib.js +1 -1
  19. package/built/web/pxtworker.js +1 -1
  20. package/built/web/react-common-authcode.css +1 -1
  21. package/built/web/react-common-multiplayer.css +1 -1
  22. package/built/web/react-common-skillmap.css +1 -1
  23. package/built/web/rtlreact-common-authcode.css +1 -1
  24. package/built/web/rtlreact-common-multiplayer.css +1 -1
  25. package/built/web/rtlreact-common-skillmap.css +1 -1
  26. package/built/web/rtlsemantic.css +2 -2
  27. package/built/web/runnerembed.js +5 -5
  28. package/built/web/semantic.css +2 -2
  29. package/common-docs/blocks-embed.md +2 -2
  30. package/common-docs/github/commit.md +1 -1
  31. package/common-docs/github/private-assignments.md +1 -1
  32. package/common-docs/github/test-extension.md +1 -1
  33. package/common-docs/online-learning.md +2 -2
  34. package/common-docs/reference/arrays/pick-random.md +1 -1
  35. package/common-docs/reference/text/char-code-at.md +1 -1
  36. package/common-docs/share.md +2 -2
  37. package/common-docs/types/function/define.md +1 -1
  38. package/localtypings/pxtarget.d.ts +3 -1
  39. package/package.json +3 -2
  40. package/react-common/components/controls/DropdownModal.tsx +66 -0
  41. package/react-common/components/controls/Input.tsx +1 -1
  42. package/react-common/styles/controls/DropdownModal.less +17 -0
  43. package/react-common/styles/react-common.less +1 -0
  44. package/react-common/styles/theming/ThemePickerModal.less +5 -0
  45. package/theme/monaco.less +2 -0
  46. package/theme/music-editor/gallery.less +10 -1
  47. package/theme/piano-roll/piano-roll.less +33 -2
  48. package/theme/pxtjson.less +0 -12
package/built/pxt.js CHANGED
@@ -108611,8 +108611,29 @@ var pxt;
108611
108611
  }
108612
108612
  async loadConfigAsync(repopath, tag) {
108613
108613
  if (!tag) {
108614
- pxt.debug(`dep: default to master branch`);
108615
- tag = "master";
108614
+ pxt.debug(`dep: no tag specified, try master and main`);
108615
+ let res;
108616
+ let error;
108617
+ try {
108618
+ res = await this.loadConfigAsync(repopath, "master");
108619
+ }
108620
+ catch (e) {
108621
+ error = e;
108622
+ pxt.log(`Error loading default master config for ${repopath}: ${e.message}`);
108623
+ }
108624
+ if (res)
108625
+ return res;
108626
+ try {
108627
+ res = await this.loadConfigAsync(repopath, "main");
108628
+ }
108629
+ catch (e) {
108630
+ error = e;
108631
+ pxt.log(`Error loading default main config for ${repopath}: ${e.message}`);
108632
+ }
108633
+ if (res)
108634
+ return res;
108635
+ else
108636
+ throw error;
108616
108637
  }
108617
108638
  // cache lookup
108618
108639
  const key = `${repopath}/${tag}`;
@@ -108646,8 +108667,29 @@ var pxt;
108646
108667
  }
108647
108668
  async loadPackageAsync(repopath, tag, fallbackPackageFiles) {
108648
108669
  if (!tag) {
108649
- pxt.debug(`load pkg: default to master branch`);
108650
- tag = "master";
108670
+ pxt.debug(`load pkg: no tag specified, try master and main`);
108671
+ let res;
108672
+ let error;
108673
+ try {
108674
+ res = await this.loadPackageAsync(repopath, "master", fallbackPackageFiles);
108675
+ }
108676
+ catch (e) {
108677
+ error = e;
108678
+ pxt.log(`Error loading default master package for ${repopath}: ${e.message}`);
108679
+ }
108680
+ if (res)
108681
+ return res;
108682
+ try {
108683
+ res = await this.loadPackageAsync(repopath, "main", fallbackPackageFiles);
108684
+ }
108685
+ catch (e) {
108686
+ error = e;
108687
+ pxt.log(`Error loading default main package for ${repopath}: ${e.message}`);
108688
+ }
108689
+ if (res)
108690
+ return res;
108691
+ else
108692
+ throw error;
108651
108693
  }
108652
108694
  // try using github proxy first
108653
108695
  if (hasProxy()) {
@@ -108933,16 +108975,27 @@ var pxt;
108933
108975
  });
108934
108976
  }
108935
108977
  github.mergeAsync = mergeAsync;
108936
- function getRefAsync(repopath, branch) {
108937
- branch = branch || "master";
108938
- return ghGetJsonAsync("https://api.github.com/repos/" + repopath + "/git/refs/heads/" + branch)
108939
- .then(resolveRefAsync)
108940
- .catch(err => {
108941
- if (err.statusCode == 404)
108978
+ async function getRefAsync(repopath, branch) {
108979
+ if (!branch) {
108980
+ // try master and main
108981
+ let res = await getRefAsync(repopath, "master");
108982
+ if (!res)
108983
+ res = await getRefAsync(repopath, "main");
108984
+ return res;
108985
+ }
108986
+ try {
108987
+ const ref = await ghGetJsonAsync("https://api.github.com/repos/" + repopath + "/git/refs/heads/" + branch);
108988
+ return resolveRefAsync(ref);
108989
+ }
108990
+ catch (e) {
108991
+ pxt.debug(`Error fetching ref for ${repopath} branch ${branch}: ${e.message}`);
108992
+ if (e.statusCode === 404) {
108942
108993
  return undefined;
108943
- else
108944
- Promise.reject(err);
108945
- });
108994
+ }
108995
+ else {
108996
+ throw e;
108997
+ }
108998
+ }
108946
108999
  }
108947
109000
  github.getRefAsync = getRefAsync;
108948
109001
  function generateNextRefName(res, pref) {
@@ -113528,7 +113581,7 @@ var pxt;
113528
113581
  }]
113529
113582
  },
113530
113583
  {
113531
- name: lf("booming kick"),
113584
+ name: lf("boom kick"),
113532
113585
  startFrequency: 100,
113533
113586
  startVolume: 1024,
113534
113587
  steps: [{
@@ -119033,10 +119086,12 @@ var pxt;
119033
119086
  * @param skipIDs string[] a list of string ids (block id, asset id, or file name) to ignore
119034
119087
  **/
119035
119088
  isAssetUsed(asset, files, skipIDs) {
119036
- var _a, _b, _c;
119037
- let blockIds = ((_b = (_a = asset.meta) === null || _a === void 0 ? void 0 : _a.blockIDs) === null || _b === void 0 ? void 0 : _b.filter(id => !skipIDs || (skipIDs === null || skipIDs === void 0 ? void 0 : skipIDs.indexOf(id)) < 0)) || [];
119038
- if (blockIds.length > 0)
119039
- return true;
119089
+ var _a, _b, _c, _d, _e;
119090
+ if (!((_a = asset.meta) === null || _a === void 0 ? void 0 : _a.displayName) || !files) {
119091
+ let blockIds = ((_c = (_b = asset.meta) === null || _b === void 0 ? void 0 : _b.blockIDs) === null || _c === void 0 ? void 0 : _c.filter(id => !skipIDs || (skipIDs === null || skipIDs === void 0 ? void 0 : skipIDs.indexOf(id)) < 0)) || [];
119092
+ if (blockIds.length > 0)
119093
+ return true;
119094
+ }
119040
119095
  if (asset.type == "tile" /* pxt.AssetType.Tile */) {
119041
119096
  for (const tm of this.getAssets("tilemap" /* AssetType.Tilemap */)) {
119042
119097
  if ((skipIDs === null || skipIDs === void 0 ? void 0 : skipIDs.indexOf(tm.id)) >= 0) {
@@ -119048,84 +119103,83 @@ var pxt;
119048
119103
  }
119049
119104
  }
119050
119105
  if (files) {
119051
- const shortId = pxt.Util.escapeForRegex(getShortIDForAsset(asset));
119052
- const displayName = pxt.Util.escapeForRegex((_c = asset.meta) === null || _c === void 0 ? void 0 : _c.displayName) || "";
119053
- let assetTsRefs;
119054
- switch (asset.type) {
119055
- case "tile" /* pxt.AssetType.Tile */:
119056
- assetTsRefs = `myTiles.${shortId}|assets.tile\`${shortId}\``;
119057
- if (displayName)
119058
- assetTsRefs += `|assets.tile\`${displayName}\``;
119059
- break;
119060
- case "tilemap" /* pxt.AssetType.Tilemap */:
119061
- assetTsRefs = `tilemap\`${shortId}\``;
119062
- break;
119063
- case "animation" /* pxt.AssetType.Animation */:
119064
- assetTsRefs = `assets.animation\`${shortId}\``;
119065
- if (displayName)
119066
- assetTsRefs += `|assets.animation\`${displayName}\``;
119067
- break;
119068
- case "song" /* pxt.AssetType.Song */:
119069
- assetTsRefs = `assets.song\`${shortId}\``;
119070
- if (displayName)
119071
- assetTsRefs += `|assets.song\`${displayName}\``;
119072
- break;
119073
- case "json" /* pxt.AssetType.Json */:
119074
- assetTsRefs = `assets.json\`${shortId}\``;
119075
- if (displayName)
119076
- assetTsRefs += `|assets.json\`${displayName}\``;
119077
- break;
119078
- default:
119079
- assetTsRefs = `assets.image\`${shortId}\``;
119080
- if (displayName)
119081
- assetTsRefs += `|assets.image\`${displayName}\``;
119082
- break;
119106
+ const config = pxt.U.jsonTryParse((_d = files["pxt.json"]) === null || _d === void 0 ? void 0 : _d.content);
119107
+ const filesToCheck = [];
119108
+ if (config === null || config === void 0 ? void 0 : config.files) {
119109
+ for (const file of config.files) {
119110
+ if (file.endsWith(".g.ts"))
119111
+ continue;
119112
+ if (skipIDs && (skipIDs.indexOf(file) !== -1))
119113
+ continue;
119114
+ if (file.endsWith(".ts") && file !== pxt.MAIN_TS || file.endsWith(".py") && file !== pxt.MAIN_PY) {
119115
+ filesToCheck.push(file);
119116
+ }
119117
+ }
119118
+ }
119119
+ if ((config === null || config === void 0 ? void 0 : config.preferredEditor) === pxt.BLOCKS_PROJECT_NAME) {
119120
+ filesToCheck.push(pxt.MAIN_BLOCKS);
119083
119121
  }
119084
- const assetTsRegex = new RegExp(assetTsRefs, "gm");
119085
- let assetPyRefs;
119122
+ else if ((config === null || config === void 0 ? void 0 : config.preferredEditor) === pxt.JAVASCRIPT_PROJECT_NAME) {
119123
+ filesToCheck.push(pxt.MAIN_TS);
119124
+ }
119125
+ else if ((config === null || config === void 0 ? void 0 : config.preferredEditor) === pxt.PYTHON_PROJECT_NAME) {
119126
+ filesToCheck.push(pxt.MAIN_PY);
119127
+ }
119128
+ const references = [];
119129
+ const shortId = pxt.Util.escapeForRegex(getShortIDForAsset(asset));
119130
+ const displayName = pxt.Util.escapeForRegex((_e = asset.meta) === null || _e === void 0 ? void 0 : _e.displayName) || "";
119131
+ const addNamespaceReference = (namespace, fun, id) => {
119132
+ references.push(`${namespace}\\s*\\.\\s*${fun}\`\\s*${id}\\s*\``);
119133
+ };
119086
119134
  switch (asset.type) {
119087
119135
  case "tile" /* pxt.AssetType.Tile */:
119088
- assetPyRefs = `myTiles.${shortId}|assets.tile\("""${shortId}"""\)`;
119136
+ references.push(`myTiles\\s*\\.\\s*${shortId}\\s*`);
119137
+ addNamespaceReference("assets", "tile", shortId);
119089
119138
  if (displayName)
119090
- assetPyRefs += `|assets.tile\("""${displayName}"""\)`;
119139
+ addNamespaceReference("assets", "tile", displayName);
119091
119140
  break;
119092
119141
  case "tilemap" /* pxt.AssetType.Tilemap */:
119093
- assetPyRefs = `assets.tilemap\("""${shortId}"""\)`;
119142
+ references.push(`tilemap\`\\s*${shortId}\\s*\``);
119143
+ addNamespaceReference("assets", "tilemap", shortId);
119144
+ if (displayName) {
119145
+ references.push(`tilemap\`\\s*${displayName}\\s*\``);
119146
+ addNamespaceReference("assets", "tilemap", displayName);
119147
+ }
119094
119148
  break;
119095
119149
  case "animation" /* pxt.AssetType.Animation */:
119096
- assetPyRefs = `assets.animation\("""${shortId}"""\)`;
119150
+ addNamespaceReference("assets", "animation", shortId);
119097
119151
  if (displayName)
119098
- assetPyRefs += `|assets.animation\("""${displayName}"""\)`;
119152
+ addNamespaceReference("assets", "animation", displayName);
119099
119153
  break;
119100
119154
  case "song" /* pxt.AssetType.Song */:
119101
- assetPyRefs = `assets.song\("""${shortId}"""\)`;
119155
+ addNamespaceReference("assets", "song", shortId);
119102
119156
  if (displayName)
119103
- assetPyRefs += `|assets.song\("""${displayName}"""\)`;
119157
+ addNamespaceReference("assets", "song", displayName);
119104
119158
  break;
119105
119159
  case "json" /* pxt.AssetType.Json */:
119106
- assetPyRefs = `assets.json\("""${shortId}"""\)`;
119160
+ addNamespaceReference("assets", "json", shortId);
119107
119161
  if (displayName)
119108
- assetPyRefs += `|assets.json\("""${displayName}"""\)`;
119162
+ addNamespaceReference("assets", "json", displayName);
119109
119163
  break;
119110
119164
  default:
119111
- assetPyRefs = `assets.image\("""${shortId}"""\)`;
119165
+ addNamespaceReference("assets", "image", shortId);
119112
119166
  if (displayName)
119113
- assetPyRefs += `|assets.image\("""${displayName}"""\)`;
119167
+ addNamespaceReference("assets", "image", displayName);
119114
119168
  break;
119115
119169
  }
119116
- const assetPyRegex = new RegExp(assetPyRefs, "gm");
119117
- for (let filename of Object.keys(files)) {
119118
- if ((skipIDs === null || skipIDs === void 0 ? void 0 : skipIDs.indexOf(filename)) >= 0)
119119
- continue;
119120
- const f = files[filename];
119121
- // Match .ts files that are not generated (.g.ts)
119122
- if (filename.match(/((?!\.g).{2}|^.{0,1})\.ts$/i)) {
119123
- if (f.content.match(assetTsRegex))
119170
+ const regex = new RegExp(references.map(r => `(?:${r})`).join("|"));
119171
+ const pyRegex = new RegExp(references.map(r => `(?:${r})`).join("|").replace(/`/g, '"""'));
119172
+ for (const file of filesToCheck) {
119173
+ const content = files[file].content;
119174
+ if (file.endsWith(".py")) {
119175
+ if (pyRegex.test(content)) {
119124
119176
  return true;
119177
+ }
119125
119178
  }
119126
- else if (filename.endsWith(".py")) {
119127
- if (f.content.match(assetPyRegex))
119179
+ else {
119180
+ if (regex.test(content)) {
119128
119181
  return true;
119182
+ }
119129
119183
  }
119130
119184
  }
119131
119185
  }
@@ -126068,6 +126122,10 @@ ${baseLabel}_nochk:
126068
126122
  }
126069
126123
  emitFieldAccess(e, store = false) {
126070
126124
  let info = e.data;
126125
+ if (!store && this.shouldSpecializeCheckedFieldLoad(info)) {
126126
+ this.emitCheckedFieldLoad(info);
126127
+ return;
126128
+ }
126071
126129
  let pref = store ? "st" : "ld";
126072
126130
  let lbl = pref + "fld_" + info.classInfo.id + "_" + info.name;
126073
126131
  if (info.needsCheck && !pxtc.target.switches.skipClassCheck) {
@@ -126086,6 +126144,32 @@ ${baseLabel}_nochk:
126086
126144
  this.write(`ldr r0, [r0, ${xoff}]`);
126087
126145
  return;
126088
126146
  }
126147
+ checkedFieldAccessCountKey(info) {
126148
+ return `${info.classInfo.id}:${info.name}:get`;
126149
+ }
126150
+ shouldSpecializeCheckedFieldLoad(info) {
126151
+ if (!pxtc.isThumb() || !this.bin.finalPass || pxtc.target.switches.noIfaceSpec)
126152
+ return false;
126153
+ if (pxtc.target.switches.skipClassCheck || !info.needsCheck)
126154
+ return false;
126155
+ return (this.bin.checkedFieldAccessCounts[this.checkedFieldAccessCountKey(info)] || 0) >= 5;
126156
+ }
126157
+ emitCheckedFieldLoad(info) {
126158
+ const helperKey = "ldfldchk_" + info.classInfo.id + "_" + info.name;
126159
+ const helper = this.ensureLabelledHelper(helperKey, () => {
126160
+ this.write(this.t.helper_prologue());
126161
+ this.emitInstanceOf(info.classInfo, "validate");
126162
+ let off = info.idx * 4 + 4;
126163
+ let xoff = "#" + off;
126164
+ if (off > 124) {
126165
+ this.write(this.t.emit_int(off, "r3"));
126166
+ xoff = "r3";
126167
+ }
126168
+ this.write(`ldr r0, [r0, ${xoff}]`);
126169
+ this.write(this.t.helper_epilogue());
126170
+ });
126171
+ this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0)));
126172
+ }
126089
126173
  writeFailBranch() {
126090
126174
  this.write(`.fail:`);
126091
126175
  this.write(`mov r1, lr`);
@@ -126306,12 +126390,50 @@ ${baseLabel}_nochk:
126306
126390
  }
126307
126391
  emitIfaceCall(procid, numargs, getset = "") {
126308
126392
  pxtc.U.assert(procid.ifaceIndex > 0);
126309
- this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
126310
- this.emitLabelledHelper("ifacecall" + numargs + "_" + getset, () => {
126393
+ const helperKey = "ifacecall" + numargs + "_" + getset;
126394
+ const helper = this.ensureLabelledHelper(helperKey, () => {
126311
126395
  this.write(`ldr r0, [sp, #0] ; ld-this`);
126312
126396
  this.loadVTable();
126313
126397
  this.ifaceCallCore(numargs, getset);
126314
126398
  });
126399
+ if (this.shouldSpecializeIfaceCall(procid.ifaceIndex, numargs, getset)) {
126400
+ const thunkKey = helperKey + "_i" + procid.ifaceIndex;
126401
+ this.emitLabelledHelper(thunkKey, () => {
126402
+ this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
126403
+ this.write(`ldlit r7, ${helper}@fn`);
126404
+ this.write(`bx r7`);
126405
+ });
126406
+ }
126407
+ else {
126408
+ this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
126409
+ this.write(this.t.call_lbl(helper));
126410
+ }
126411
+ }
126412
+ ifaceCallCountKey(ifaceIndex, numargs, getset) {
126413
+ return `${ifaceIndex}:${numargs}:${getset || "call"}`;
126414
+ }
126415
+ // Count-driven specialization decisions (this method and the related
126416
+ // shouldSpecializeCheckedFieldLoad / shouldSpecializeMapSetByFieldId) assume:
126417
+ // - bin.finalPass is true, AND
126418
+ // - the count maps were populated during the same final-pass emit()
126419
+ // call that builds the IR this asm walk consumes.
126420
+ // This is currently true because the frontend populates counts during IR
126421
+ // generation and the backend reads them only during the subsequent asm walk.
126422
+ // Any future change that interleaves IR generation with asm emission would
126423
+ // silently regress these to "always false".
126424
+ //
126425
+ // Threshold of 3 is the break-even point for the labelled-thunk pattern:
126426
+ // each shared call site saves ~4 bytes vs an inline call, and the thunk
126427
+ // itself costs ~12 bytes. At 3 callers, savings break even with thunk cost.
126428
+ shouldSpecializeIfaceCall(ifaceIndex, numargs, getset) {
126429
+ if (!pxtc.isThumb() || !this.bin.finalPass || pxtc.target.switches.noIfaceSpec)
126430
+ return false;
126431
+ const count = this.bin.ifaceCallCounts[this.ifaceCallCountKey(ifaceIndex, numargs, getset)] || 0;
126432
+ if (getset == "get" && numargs == 1)
126433
+ return count >= 3;
126434
+ if (getset == "set" || !getset)
126435
+ return count >= 3;
126436
+ return false;
126315
126437
  }
126316
126438
  // vtable in r3; clobber r2
126317
126439
  checkSubtype(info, failLbl = ".fail", r2 = "r2") {
@@ -126409,6 +126531,8 @@ ${baseLabel}_nochk:
126409
126531
  }
126410
126532
  emitRtCall(topExpr, genCall = null) {
126411
126533
  let name = topExpr.data;
126534
+ const mapSetFieldId = this.mapSetByFieldId(topExpr);
126535
+ const specializeMapSetFieldId = mapSetFieldId !== undefined && this.shouldSpecializeMapSetByFieldId(mapSetFieldId);
126412
126536
  if (name == "pxt::beginTry") {
126413
126537
  return this.emitBeginTry(topExpr);
126414
126538
  }
@@ -126421,6 +126545,8 @@ ${baseLabel}_nochk:
126421
126545
  isRef: (maskInfo.refMask & (1 << i)) != 0,
126422
126546
  conv: convs.find(c => c.argIdx == i)
126423
126547
  }));
126548
+ if (specializeMapSetFieldId)
126549
+ allArgs = allArgs.filter(a => a.idx != 1);
126424
126550
  pxtc.U.assert(allArgs.length <= 4);
126425
126551
  let seenUpdate = false;
126426
126552
  for (let a of pxtc.U.reversed(allArgs)) {
@@ -126523,33 +126649,69 @@ ${baseLabel}_nochk:
126523
126649
  genCall();
126524
126650
  }
126525
126651
  else {
126526
- if (name != "langsupp::ignore")
126652
+ if (specializeMapSetFieldId)
126653
+ this.emitMapSetByFieldIdCall(mapSetFieldId);
126654
+ else if (name != "langsupp::ignore")
126527
126655
  this.alignedCall(name, "", 0, true);
126528
126656
  }
126529
126657
  if (clearStack) {
126530
126658
  this.clearArgs(complexArgs.filter(a => !a.isRef).map(a => a.expr), complexArgs.filter(a => a.isRef).map(a => a.expr));
126531
126659
  }
126532
126660
  }
126661
+ mapSetByFieldId(topExpr) {
126662
+ if (topExpr.data != "pxtrt::mapSet" || topExpr.args.length != 3)
126663
+ return undefined;
126664
+ const fieldId = topExpr.args[1];
126665
+ if (fieldId.exprKind != pxtc.ir.EK.NumberLiteral || typeof fieldId.data != "number")
126666
+ return undefined;
126667
+ return fieldId.data;
126668
+ }
126669
+ shouldSpecializeMapSetByFieldId(fieldId) {
126670
+ if (!pxtc.isThumb() || !this.bin.finalPass || pxtc.target.switches.noIfaceSpec)
126671
+ return false;
126672
+ return (this.bin.mapSetByFieldIdCounts[fieldId + ""] || 0) >= 3;
126673
+ }
126674
+ emitMapSetByFieldIdCall(fieldId) {
126675
+ const helper = this.ensureLabelledHelper("mapset_i" + fieldId, () => {
126676
+ this.write(this.t.helper_prologue());
126677
+ this.write(this.t.emit_int(fieldId, "r1"));
126678
+ this.write(this.t.callCPP("pxtrt::mapSet"));
126679
+ this.write(this.t.helper_epilogue());
126680
+ });
126681
+ this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0)));
126682
+ }
126533
126683
  alignedCall(name, cmt = "", off = 0, saveStack = false) {
126534
126684
  if (pxtc.U.startsWith(name, "_cmp_") || pxtc.U.startsWith(name, "_pxt_"))
126535
126685
  saveStack = false;
126536
126686
  this.write(this.t.call_lbl(name, saveStack, this.stackAlignmentNeeded(off)) + cmt);
126537
126687
  }
126538
- emitLabelledHelper(lbl, generate) {
126688
+ // Lazily emit a deduped helper body and return its label.
126689
+ // Use this when you want to BRANCH to (or reference) the helper without
126690
+ // emitting a `bl` at the current cursor -- e.g. when wrapping the call in
126691
+ // a thunk, or storing the label for later use.
126692
+ ensureLabelledHelper(lbl, generate) {
126539
126693
  if (!this.labelledHelpers[lbl]) {
126540
126694
  let outp = this.redirectOutput(generate);
126541
- this.emitHelper(outp, lbl);
126542
- this.labelledHelpers[lbl] = this.bin.codeHelpers[outp];
126543
- }
126544
- else {
126545
- this.write(this.t.call_lbl(this.labelledHelpers[lbl]));
126695
+ this.labelledHelpers[lbl] = this.helperLabel(outp, lbl);
126546
126696
  }
126697
+ return this.labelledHelpers[lbl];
126547
126698
  }
126548
- emitHelper(asm, baseName = "hlp") {
126699
+ // Ensure a deduped helper exists and emit a `bl` to it at the cursor.
126700
+ // Use this for the common case of "factor this sequence into a helper and call it here".
126701
+ emitLabelledHelper(lbl, generate) {
126702
+ const helper = this.ensureLabelledHelper(lbl, generate);
126703
+ this.write(this.t.call_lbl(helper));
126704
+ return helper;
126705
+ }
126706
+ helperLabel(asm, baseName = "hlp") {
126549
126707
  if (!this.bin.codeHelpers[asm]) {
126550
126708
  let len = Object.keys(this.bin.codeHelpers).length;
126551
126709
  this.bin.codeHelpers[asm] = `_${baseName}_${len}`;
126552
126710
  }
126711
+ return this.bin.codeHelpers[asm];
126712
+ }
126713
+ emitHelper(asm, baseName = "hlp") {
126714
+ this.helperLabel(asm, baseName);
126553
126715
  this.write(this.t.call_lbl(this.bin.codeHelpers[asm]));
126554
126716
  }
126555
126717
  pushToExprStack(a) {
@@ -126586,48 +126748,85 @@ ${baseLabel}_nochk:
126586
126748
  }
126587
126749
  }
126588
126750
  }
126589
- emitFieldMethods() {
126590
- for (let op of ["get", "set"]) {
126591
- this.write(`
126751
+ emitFieldMethod(op) {
126752
+ this.write(`
126592
126753
  ${this.helperObject(op)}
126593
126754
  .section code
126594
126755
  _pxt_map_${op}:
126595
126756
  `);
126596
- this.loadVTable("r4");
126597
- this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4");
126598
- this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"));
126599
- this.write(".notmap:");
126600
- let numargs = op == "set" ? 2 : 1;
126601
- let hasAlign = false;
126602
- this.write("mov r4, r3 ; save VT");
126603
- if (op == "set") {
126604
- if (pxtc.target.stackAlign) {
126605
- hasAlign = true;
126606
- this.write("push {lr} ; align");
126607
- }
126608
- this.write(`
126757
+ this.loadVTable("r4");
126758
+ this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4");
126759
+ this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"));
126760
+ this.write(".notmap:");
126761
+ let numargs = op == "set" ? 2 : 1;
126762
+ let hasAlign = false;
126763
+ this.write("mov r4, r3 ; save VT");
126764
+ if (op == "set") {
126765
+ if (pxtc.target.stackAlign) {
126766
+ hasAlign = true;
126767
+ this.write("push {lr} ; align");
126768
+ }
126769
+ this.write(`
126609
126770
  push {r0, r2, lr}
126610
126771
  mov r0, r1
126611
126772
  `);
126612
- }
126613
- else {
126614
- this.write(`
126773
+ }
126774
+ else {
126775
+ this.write(`
126615
126776
  push {r0, lr}
126616
126777
  mov r0, r1
126617
126778
  `);
126618
- }
126619
- this.write(`
126779
+ }
126780
+ this.write(`
126620
126781
  bl pxtrt::lookupMapKey
126621
126782
  mov r1, r0 ; put key index in r1
126622
126783
  ldr r0, [sp, #0] ; restore obj pointer
126623
126784
  mov r3, r4 ; restore vt
126624
126785
  bl .dowork
126625
126786
  `);
126626
- this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)));
126627
- this.write("pop {pc}");
126628
- this.write(".dowork:");
126629
- this.ifaceCallCore(numargs, op, true);
126630
- }
126787
+ this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)));
126788
+ this.write("pop {pc}");
126789
+ this.write(".dowork:");
126790
+ this.ifaceCallCore(numargs, op, true);
126791
+ }
126792
+ emitStringMapSetMethod() {
126793
+ // Fast path for the well-typed "map with string index signature" case.
126794
+ // - tagged-int / null receivers: unrecoverable, panic with failedCast (matches the
126795
+ // pre-optimization mapSetGeneric path, which also can't proceed on these).
126796
+ // - non-RefMap pointer receivers: tail-call _pxt_map_set, which handles
126797
+ // non-map objects via interface dispatch. Preserves back-compat for code that
126798
+ // casts non-map objects to a string-index-signature type.
126799
+ //
126800
+ // Emitted only when the final-pass IR selected mapSetByStringOnly
126801
+ // (never under `noIfaceSpec`): unreferenced helper text costs image
126802
+ // space in every program otherwise.
126803
+ if (!this.bin.usesMapSetByStringOnly)
126804
+ return;
126805
+ this.write(`
126806
+ ${this.helperObject("set string map")}
126807
+ .section code
126808
+ _pxt_map_set_by_string:
126809
+ lsls r4, r0, #30
126810
+ bne .badtype
126811
+ cmp r0, #0
126812
+ beq .badtype
126813
+ ldr r3, [r0, #0]
126814
+ `);
126815
+ this.write(`
126816
+ ldrh r4, [r3, #8]
126817
+ cmp r4, #${pxt.BuiltInType.RefMap}
126818
+ bne .fallback
126819
+ `);
126820
+ this.write(this.t.callCPPPush("pxtrt::mapSetByString"));
126821
+ this.write(`
126822
+ .fallback:
126823
+ b _pxt_map_set
126824
+ .badtype:
126825
+ mov r1, lr
126826
+ mov r7, sp
126827
+ str r7, [r6, #4]
126828
+ bl pxt::failedCast
126829
+ `);
126631
126830
  }
126632
126831
  emitArrayMethod(op, isBuffer) {
126633
126832
  this.write(`
@@ -126713,6 +126912,12 @@ ${baseLabel}_nochk:
126713
126912
  this.emitArrayMethod(op, false);
126714
126913
  }
126715
126914
  }
126915
+ emitFieldMethods() {
126916
+ for (let op of ["get", "set"]) {
126917
+ this.emitFieldMethod(op);
126918
+ }
126919
+ this.emitStringMapSetMethod();
126920
+ }
126716
126921
  emitLambdaTrampoline() {
126717
126922
  let r3 = pxtc.target.stackAlign ? "r3," : "";
126718
126923
  this.write(`
@@ -127060,6 +127265,12 @@ ${baseLabel}_nochk:
127060
127265
  this.write(`.short 0, 0 ; no captured vars`);
127061
127266
  this.write(`.word ${this.proc.label()}_args@fn`);
127062
127267
  }
127268
+ if (this.proc.useExactIfaceWrapper) {
127269
+ this.write(`${this.proc.label()}_iface:`);
127270
+ this.write(`b ${this.proc.label()}_nochk`);
127271
+ if (!this.proc.info.usedAsValue)
127272
+ return;
127273
+ }
127063
127274
  this.write(`${this.proc.label()}_args:`);
127064
127275
  let numargs = this.proc.args.length;
127065
127276
  if (numargs == 0)
@@ -127828,6 +128039,17 @@ var ts;
127828
128039
  "pxt::fromInt": "_numops_fromInt",
127829
128040
  "pxt::switch_eq": "_pxt_switch_eq",
127830
128041
  };
128042
+ // Boolean conversion fast paths, kept out of inlineArithmetic so the
128043
+ // `noBoolLower` flag can drop the group (redirects and helper bodies alike).
128044
+ const inlineBoolOps = {
128045
+ "pxt::fromBool": "_pxt_fromBool",
128046
+ "numops::toBool": "_numops_toBool",
128047
+ "numops::toBoolDecr": "_numops_toBoolDecr",
128048
+ "Boolean_::bang": "_pxt_boolean_bang",
128049
+ };
128050
+ function boolLowerEnabled() {
128051
+ return !pxtc.target.switches.noBoolLower;
128052
+ }
127831
128053
  // snippets for ARM Thumb assembly
127832
128054
  class ThumbSnippets extends pxtc.AssemblerSnippets {
127833
128055
  stackAligned() {
@@ -127935,6 +128157,8 @@ ${lbl}:`;
127935
128157
  }
127936
128158
  call_lbl(lbl, saveStack, stackAlign) {
127937
128159
  let o = pxtc.U.lookup(inlineArithmetic, lbl);
128160
+ if (!o && boolLowerEnabled())
128161
+ o = pxtc.U.lookup(inlineBoolOps, lbl);
127938
128162
  if (o) {
127939
128163
  lbl = o;
127940
128164
  saveStack = false;
@@ -128106,6 +128330,99 @@ _numops_fromInt:
128106
128330
  blx lr
128107
128331
  .over2:
128108
128332
  ${this.callCPPPush("pxt::fromInt")}
128333
+ `;
128334
+ // skipped under `noBoolLower` flag: unreferenced helper text still costs image space
128335
+ if (boolLowerEnabled())
128336
+ r += `
128337
+ ; Tag a raw 0/1 truth value into a boolean object (taggedTrue/taggedFalse).
128338
+ ; Inverse of the toBool fast paths below; matches C++ pxt::fromBool.
128339
+ _pxt_fromBool:
128340
+ @scope _pxt_fromBool
128341
+ cmp r0, #0
128342
+ beq .false
128343
+ .true:
128344
+ movs r0, #${pxtc.taggedTrue}
128345
+ blx lr
128346
+ .false:
128347
+ movs r0, #${pxtc.taggedFalse}
128348
+ blx lr
128349
+
128350
+ ; Logical NOT of a raw 0/1 truth value -> raw 0/1 (matches C++ bool bang(bool)).
128351
+ ; Callers pass an already-tested condition, so this is raw-in / raw-out; value
128352
+ ; context (x = !y) wraps the result with fromBool to re-tag it.
128353
+ _pxt_boolean_bang:
128354
+ @scope _pxt_boolean_bang
128355
+ cmp r0, #0
128356
+ beq .true
128357
+ .false:
128358
+ movs r0, #0
128359
+ blx lr
128360
+ .true:
128361
+ movs r0, #1
128362
+ blx lr
128363
+
128364
+ ; Fast-path truthiness test: tagged value in r0 -> raw 0/1 in r0.
128365
+ ; Value encodings (see taggedSpecialValue in emitter.ts): tagged int = (n<<1)|1
128366
+ ; (low bit set); special tags = (n<<2)|2 with undefined=0, null, false, NaN,
128367
+ ; true; heap objects have low 2 bits 00.
128368
+ ; Only non-pointer values are decided inline here; any heap object is handed to
128369
+ ; the C++ helper at .boxed (which also handles boxed numbers/strings like 0.0
128370
+ ; and ""). INVARIANT: the falsy set below must match C++ numops::toBool exactly
128371
+ ; -- if a new falsy tagged value is ever added, it must be added here too, or it
128372
+ ; will fall through to .true.
128373
+ _numops_toBool:
128374
+ @scope _numops_toBool
128375
+ cmp r0, #0 ; undefined (0) -> false
128376
+ beq .false
128377
+ cmp r0, #1 ; integer 0 ((0<<1)|1) -> false
128378
+ beq .false
128379
+ lsls r1, r0, #31 ; odd => nonzero tagged int -> true
128380
+ bne .true
128381
+ cmp r0, #${pxtc.taggedNull} ; null -> false
128382
+ beq .false
128383
+ cmp r0, #${pxtc.taggedFalse} ; false -> false
128384
+ beq .false
128385
+ cmp r0, #${pxtc.taggedNaN} ; NaN -> false
128386
+ beq .false
128387
+ lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++
128388
+ beq .boxed
128389
+ .true: ; remaining specials (e.g. true) are truthy
128390
+ movs r0, #1
128391
+ blx lr
128392
+ .false:
128393
+ movs r0, #0
128394
+ blx lr
128395
+ .boxed:
128396
+ ${this.callCPPPush("numops::toBool")}
128397
+
128398
+ ; Same truthiness classification as _numops_toBool (see comments above), but
128399
+ ; this variant also consumes (ref-decrements) its argument. Only heap objects
128400
+ ; are ref-counted, and those go through the C++ fallback which does the decr;
128401
+ ; the inline non-pointer paths carry no refcount, so they need none.
128402
+ _numops_toBoolDecr:
128403
+ @scope _numops_toBoolDecr
128404
+ cmp r0, #0 ; undefined (0) -> false
128405
+ beq .false
128406
+ cmp r0, #1 ; integer 0 ((0<<1)|1) -> false
128407
+ beq .false
128408
+ lsls r1, r0, #31 ; odd => nonzero tagged int -> true
128409
+ bne .true
128410
+ cmp r0, #${pxtc.taggedNull} ; null -> false
128411
+ beq .false
128412
+ cmp r0, #${pxtc.taggedFalse} ; false -> false
128413
+ beq .false
128414
+ cmp r0, #${pxtc.taggedNaN} ; NaN -> false
128415
+ beq .false
128416
+ lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++
128417
+ beq .boxed
128418
+ .true:
128419
+ movs r0, #1
128420
+ blx lr
128421
+ .false:
128422
+ movs r0, #0
128423
+ blx lr
128424
+ .boxed:
128425
+ ${this.callCPPPush("numops::toBoolDecr")}
128109
128426
  `;
128110
128427
  for (let op of Object.keys(pxtc.thumbCmpMap)) {
128111
128428
  op = op.replace(/.*::/, "");
@@ -128130,9 +128447,10 @@ _cmp_${op}:
128130
128447
  // the cmp isn't really needed, given how toBoolDecr() is compiled,
128131
128448
  // but better not rely on it
128132
128449
  // Also, cmp isn't needed when ref-counting (it ends with movs r0, r4)
128450
+ const toBoolDecr = boolLowerEnabled() ? "_numops_toBoolDecr" : "numops::toBoolDecr";
128133
128451
  r += boxedOp(`
128134
128452
  bl numops::${op}
128135
- bl numops::toBoolDecr
128453
+ bl ${toBoolDecr}
128136
128454
  cmp r0, #0`);
128137
128455
  }
128138
128456
  // switch(x){case k:} compiles to equality tests via pxt::switch_eq.
@@ -133578,6 +133896,7 @@ var ts;
133578
133896
  this.classInfo = null;
133579
133897
  this.perfCounterName = null;
133580
133898
  this.perfCounterNo = 0;
133899
+ this.useExactIfaceWrapper = false;
133581
133900
  this.body = [];
133582
133901
  this.lblNo = 0;
133583
133902
  this.action = null;
@@ -133595,6 +133914,11 @@ var ts;
133595
133914
  isGetter() {
133596
133915
  return this.action && this.action.kind == ts.SyntaxKind.GetAccessor;
133597
133916
  }
133917
+ // Returns the proc's "_args" entry label -- the full arg-shuffling
133918
+ // wrapper. Use this for any vtable-style reference where the caller
133919
+ // cannot guarantee the wrapper-skip preconditions (numargs >= proc.args.length).
133920
+ // For iface-table entries that CAN accept the optimization, the consumer
133921
+ // (hexfile.ts) inspects useExactIfaceWrapper directly and picks _iface instead.
133598
133922
  vtLabel() {
133599
133923
  return this.label() + (pxtc.isStackMachine() ? "" : "_args");
133600
133924
  }
@@ -134137,6 +134461,11 @@ var ts;
134137
134461
  argsFmt: ["T", "T", "S", "T"],
134138
134462
  value: 0
134139
134463
  },
134464
+ "pxtrt::mapSetByStringOnly": {
134465
+ name: "_pxt_map_set_by_string",
134466
+ argsFmt: ["T", "T", "S", "T"],
134467
+ value: 0
134468
+ },
134140
134469
  };
134141
134470
  let EK = pxtc.ir.EK;
134142
134471
  pxtc.SK = ts.SyntaxKind;
@@ -134870,6 +135199,87 @@ var ts;
134870
135199
  bin.breakpoints = opts.breakpoints;
134871
135200
  bin.name = opts.name;
134872
135201
  bin.target = opts.target;
135202
+ function ifaceCallKindCounts(ifaceIndex, getset) {
135203
+ const suffix = ":" + (getset || "call");
135204
+ return Object.keys(bin.ifaceCallCounts)
135205
+ .filter(k => pxtc.U.startsWith(k, ifaceIndex + ":") && pxtc.U.endsWith(k, suffix))
135206
+ .map(k => ({
135207
+ numargs: parseInt(k.split(":")[1]),
135208
+ count: bin.ifaceCallCounts[k]
135209
+ }));
135210
+ }
135211
+ // Decides whether `proc` (an iface-dispatched method) can safely skip the
135212
+ // arg-shuffling _args wrapper and have its iface vtable entry point directly
135213
+ // at _nochk. Safe iff every call site that targets this (ifaceIndex, getset)
135214
+ // bucket passes at least `proc.args.length` arguments -- that's exactly the
135215
+ // condition the wrapper would short-circuit on (`cmp r4, #numargs; bge _nochk`).
135216
+ //
135217
+ // Notes:
135218
+ // - The counts are a per-(ifaceIndex, getset) projection, not per-proc. In a
135219
+ // polymorphic iface bucket each proc is evaluated independently; eligibility
135220
+ // ends up cascading to the proc with the largest args.length in the bucket.
135221
+ // - toString is excluded because hexfile.ts:~841 wires toString's vtable
135222
+ // slot to `vtLabel()` (i.e. _args). The fixed-slot dispatch through that
135223
+ // vtable entry has no arg-count guarantees, so the wrapper is required.
135224
+ // Skipping it for a toString proc would dangle the _args reference.
135225
+ // Any future fixed-slot vtable consumer that uses vtLabel() must add a
135226
+ // matching exclusion here.
135227
+ // - dynamicIfaceCalls disqualifies any ifaceIndex ever dispatched without a
135228
+ // known decl (line ~2503). Those callers don't go through the counted path.
135229
+ // - The "no get calls in a non-get bucket" check is conservative: a "get"
135230
+ // syntax call has args.length == 1 which could under-feed a multi-arg call
135231
+ // target sharing the bucket.
135232
+ function canUseExactIfaceWrapper(proc, ifaceIndex, getset) {
135233
+ if (!isThumb() || !proc || !proc.info.usedAsIface || proc.args.length == 0)
135234
+ return false;
135235
+ if (proc.action && isToString(proc.action))
135236
+ return false;
135237
+ if (bin.dynamicIfaceCalls[ifaceIndex + ""])
135238
+ return false;
135239
+ if (!getset && ifaceCallKindCounts(ifaceIndex, "get").length)
135240
+ return false;
135241
+ const counts = ifaceCallKindCounts(ifaceIndex, getset);
135242
+ return counts.every(c => c.numargs >= proc.args.length);
135243
+ }
135244
+ // Must run AFTER the final-pass emit(rootFunction) has populated
135245
+ // bin.ifaceCallCounts and bin.dynamicIfaceCalls, and BEFORE
135246
+ // hexfile.ts emits iface vtables (which read useExactIfaceWrapper to
135247
+ // pick _iface vs _args per entry) and backbase.ts emits proc wrappers
135248
+ // (which read useExactIfaceWrapper to decide whether to emit _iface).
135249
+ function markExactIfaceWrappers() {
135250
+ bin.procs.forEach(p => p.useExactIfaceWrapper = false);
135251
+ if (!isThumb() || pxtc.target.switches.noIfaceSpec)
135252
+ return;
135253
+ const eligible = {};
135254
+ const seen = {};
135255
+ const note = (proc, ifaceIndex, getset) => {
135256
+ if (!proc || proc.args.length == 0)
135257
+ return;
135258
+ const key = proc.seqNo + "";
135259
+ seen[key] = true;
135260
+ if (eligible[key] === undefined)
135261
+ eligible[key] = true;
135262
+ if (!canUseExactIfaceWrapper(proc, ifaceIndex, getset))
135263
+ eligible[key] = false;
135264
+ };
135265
+ for (const info of bin.usedClassInfos) {
135266
+ if (!info.itable)
135267
+ continue;
135268
+ for (const entry of info.itable) {
135269
+ if (entry.proc)
135270
+ note(entry.proc, entry.idx, entry.proc.isGetter() ? "get" : "");
135271
+ if (entry.setProc)
135272
+ note(entry.setProc, entry.idx, "set");
135273
+ }
135274
+ }
135275
+ for (const proc of bin.procs) {
135276
+ const key = proc.seqNo + "";
135277
+ proc.useExactIfaceWrapper = !!seen[key] && !!eligible[key];
135278
+ }
135279
+ }
135280
+ function emitEscapedExpression(expr, reason) {
135281
+ return emitExpr(expr);
135282
+ }
134873
135283
  function reset() {
134874
135284
  bin.reset();
134875
135285
  proc = null;
@@ -134950,7 +135360,13 @@ var ts;
134950
135360
  reset();
134951
135361
  needsUsingInfo = false;
134952
135362
  bin.finalPass = true;
135363
+ bin.ifaceCallCounts = {};
135364
+ bin.mapSetByFieldIdCounts = {};
135365
+ bin.checkedFieldAccessCounts = {};
135366
+ bin.dynamicIfaceCalls = {};
135367
+ bin.usesMapSetByStringOnly = false;
134953
135368
  emit(rootFunction);
135369
+ markExactIfaceWrappers();
134954
135370
  pxtc.U.assert(usedWorkList.length == 0);
134955
135371
  res.configData = [];
134956
135372
  for (let k of Object.keys(configEntries)) {
@@ -135640,7 +136056,7 @@ ${lbl}: .short 0xffff
135640
136056
  let coll = pxtc.ir.shared(pxtc.ir.rtcall("Array_::mk", []));
135641
136057
  for (let elt of node.elements) {
135642
136058
  let mask = isRefCountedExpr(elt) ? (1 << 1) : 0;
135643
- proc.emitExpr(pxtc.ir.rtcall("Array_::push", [coll, emitExpr(elt)], mask));
136059
+ proc.emitExpr(pxtc.ir.rtcall("Array_::push", [coll, emitEscapedExpression(elt, "arrayElement")], mask));
135644
136060
  }
135645
136061
  if (node.elements.length > 0)
135646
136062
  // Make sure there is at least one use of the array, so it doesn't get GC-ed away in the last push.
@@ -135682,8 +136098,11 @@ ${lbl}: .short 0xffff
135682
136098
  if (isRefCountedExpr(p.initializer))
135683
136099
  mask |= 1 << 2;
135684
136100
  }
136101
+ const nativeFieldId = pxtc.target.isNative ? getIfaceMemberId(keyName) : undefined;
136102
+ if (nativeFieldId !== undefined)
136103
+ bin.mapSetByFieldIdCounts[nativeFieldId + ""] = (bin.mapSetByFieldIdCounts[nativeFieldId + ""] || 0) + 1;
135685
136104
  const fieldId = pxtc.target.isNative
135686
- ? pxtc.ir.numlit(getIfaceMemberId(keyName))
136105
+ ? pxtc.ir.numlit(nativeFieldId)
135687
136106
  : pxtc.ir.ptrlit(null, JSON.stringify(keyName));
135688
136107
  const args = [
135689
136108
  expr,
@@ -135743,6 +136162,9 @@ ${lbl}: .short 0xffff
135743
136162
  return emitShim(fld, decl, [node.expression]);
135744
136163
  }
135745
136164
  else {
136165
+ if (idx.needsCheck && !pxtc.target.switches.skipClassCheck) {
136166
+ bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] = (bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] || 0) + 1;
136167
+ }
135746
136168
  return pxtc.ir.op(EK.FieldAccess, [emitExpr(node.expression)], idx);
135747
136169
  }
135748
136170
  }
@@ -135796,7 +136218,17 @@ ${lbl}: .short 0xffff
135796
136218
  indexer = assign ? attrs.indexerSet : attrs.indexerGet;
135797
136219
  }
135798
136220
  if (!indexer && (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.StructuredOrTypeVariable))) {
135799
- indexer = assign ? "pxtrt::mapSetGeneric" : "pxtrt::mapGetGeneric";
136221
+ const hasIndexSignature = !!checker.getIndexTypeOfType(t, ts.IndexKind.String) || !!checker.getIndexTypeOfType(t, ts.IndexKind.Number);
136222
+ // Native-only: mapSetByStringOnly resolves to the _pxt_map_set_by_string
136223
+ // thumb helper. The JS backend has no such runtime entry, so the generic
136224
+ // setter is the only valid choice there.
136225
+ const mapOnlySet = !pxtc.target.switches.noIfaceSpec && pxtc.target.isNative
136226
+ && !!assign && !(t.flags & ts.TypeFlags.Any) && hasIndexSignature;
136227
+ if (mapOnlySet)
136228
+ bin.usesMapSetByStringOnly = true;
136229
+ indexer = assign
136230
+ ? (mapOnlySet ? "pxtrt::mapSetByStringOnly" : "pxtrt::mapSetGeneric")
136231
+ : "pxtrt::mapGetGeneric";
135800
136232
  stringOk = true;
135801
136233
  }
135802
136234
  if (indexer) {
@@ -136133,7 +136565,7 @@ ${lbl}: .short 0xffff
136133
136565
  lst.push(tracked);
136134
136566
  }
136135
136567
  function emitPlain() {
136136
- let r = mkProcCall(decl, node, args.map((x) => emitExpr(x)));
136568
+ let r = mkProcCall(decl, node, args.map(x => emitEscapedExpression(x, "callArgument")));
136137
136569
  let pp = r.data;
136138
136570
  if (args[0] && pp.proc && pp.proc.classInfo)
136139
136571
  pp.isThis = args[0].kind == pxtc.SK.ThisKeyword;
@@ -136160,7 +136592,7 @@ ${lbl}: .short 0xffff
136160
136592
  baseCtor = p.ctor;
136161
136593
  if (!baseCtor && bin.finalPass)
136162
136594
  throw userError(9280, lf("super() call requires an explicit constructor in base class"));
136163
- let ctorArgs = args.map((x) => emitExpr(x));
136595
+ let ctorArgs = args.map((x) => emitEscapedExpression(x, "callArgument"));
136164
136596
  ctorArgs.unshift(emitThis(funcExpr));
136165
136597
  return mkProcCallCore(baseCtor, node, ctorArgs);
136166
136598
  }
@@ -136176,9 +136608,11 @@ ${lbl}: .short 0xffff
136176
136608
  // TODO in VT accessor/field/method -> different
136177
136609
  pxtc.U.assert(funcExpr.kind == pxtc.SK.PropertyAccessExpression);
136178
136610
  const fieldName = funcExpr.name.text;
136611
+ const ifaceIndex = getIfaceMemberId(fieldName, true);
136612
+ bin.dynamicIfaceCalls[ifaceIndex + ""] = true;
136179
136613
  // completely dynamic dispatch
136180
- return mkMethodCall(args.map((x) => emitExpr(x)), {
136181
- ifaceIndex: getIfaceMemberId(fieldName, true),
136614
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
136615
+ ifaceIndex,
136182
136616
  callLocationIndex: markCallLocation(node),
136183
136617
  noArgs
136184
136618
  });
@@ -136202,7 +136636,7 @@ ${lbl}: .short 0xffff
136202
136636
  pxtc.U.assert(false);
136203
136637
  }
136204
136638
  pxtc.U.assert(!bin.finalPass || info.virtualIndex != null, "!bin.finalPass || info.virtualIndex != null");
136205
- return mkMethodCall(args.map((x) => emitExpr(x)), {
136639
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
136206
136640
  classInfo: info.parentClassInfo,
136207
136641
  virtualIndex: info.virtualIndex,
136208
136642
  noArgs,
@@ -136242,9 +136676,15 @@ ${lbl}: .short 0xffff
136242
136676
  return emitPlain();
136243
136677
  }
136244
136678
  else if (needsVCall || pxtc.target.switches.slowMethods || !forceMethod) {
136245
- return mkMethodCall(args.map((x) => emitExpr(x)), {
136246
- ifaceIndex: getIfaceMemberId(getName(decl), true),
136247
- isSet: noArgs && args.length == 2,
136679
+ const ifaceIndex = getIfaceMemberId(getName(decl), true);
136680
+ const isSet = noArgs && args.length == 2;
136681
+ const callKind = isSet ? "set" : noArgs ? "get" : "call";
136682
+ const helperGetSet = callKind == "call" ? "" : callKind;
136683
+ const countKey = `${ifaceIndex}:${args.length}:${helperGetSet || "call"}`;
136684
+ bin.ifaceCallCounts[countKey] = (bin.ifaceCallCounts[countKey] || 0) + 1;
136685
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
136686
+ ifaceIndex,
136687
+ isSet,
136248
136688
  callLocationIndex: markCallLocation(node),
136249
136689
  noArgs
136250
136690
  });
@@ -136263,7 +136703,7 @@ ${lbl}: .short 0xffff
136263
136703
  // here's where we will recurse to generate funcExpr
136264
136704
  args.unshift(funcExpr);
136265
136705
  pxtc.U.assert(!noArgs);
136266
- return mkMethodCall(args.map(x => emitExpr(x)), {
136706
+ return mkMethodCall(args.map(x => emitEscapedExpression(x, "callArgument")), {
136267
136707
  virtualIndex: -1,
136268
136708
  callLocationIndex: markCallLocation(node),
136269
136709
  noArgs
@@ -136359,6 +136799,8 @@ ${lbl}: .short 0xffff
136359
136799
  }
136360
136800
  }
136361
136801
  function getCtor(decl) {
136802
+ if (!decl.members)
136803
+ return null;
136362
136804
  return decl.members.filter(m => m.kind == pxtc.SK.Constructor)[0];
136363
136805
  }
136364
136806
  function isIfaceMemberUsed(name) {
@@ -136406,7 +136848,7 @@ ${lbl}: .short 0xffff
136406
136848
  // let sig = checker.getResolvedSignature(node)
136407
136849
  // TODO: can we have overloeads?
136408
136850
  addDefaultParametersAndTypeCheck(checker.getResolvedSignature(node), args, ctorAttrs);
136409
- let compiled = args.map((x) => emitExpr(x));
136851
+ let compiled = args.map((x) => emitEscapedExpression(x, "callArgument"));
136410
136852
  if (ctorAttrs.shim) {
136411
136853
  // TODO need to deal with refMask and tagged ints here
136412
136854
  // we drop 'obj' variable
@@ -136795,7 +137237,7 @@ ${lbl}: .short 0xffff
136795
137237
  for (let f of initalizedFields) {
136796
137238
  let idx = fieldIndexCore(proc.classInfo, getFieldInfo(proc.classInfo, getName(f)), false);
136797
137239
  let trg2 = pxtc.ir.op(EK.FieldAccess, [emitLocalLoad(info.thisParameter)], idx);
136798
- proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitExpr(f.initializer)]));
137240
+ proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitEscapedExpression(f.initializer, "initializer")]));
136799
137241
  }
136800
137242
  flushHoistedFunctionDefinitions();
136801
137243
  if (node.body.kind == pxtc.SK.Block) {
@@ -137013,9 +137455,12 @@ ${lbl}: .short 0xffff
137013
137455
  isRef: true,
137014
137456
  shimName: attrs.shim,
137015
137457
  classInfo: info,
137016
- needsCheck
137458
+ needsCheck,
137017
137459
  };
137018
137460
  }
137461
+ function checkedFieldAccessCountKey(info) {
137462
+ return `${info.classInfo.id}:${info.name}:get`;
137463
+ }
137019
137464
  function fieldIndex(pacc) {
137020
137465
  const tp = typeOf(pacc.expression);
137021
137466
  if (isPossiblyGenericClassType(tp)) {
@@ -137046,7 +137491,7 @@ ${lbl}: .short 0xffff
137046
137491
  if (decl && (isGlobal || isVar(decl) || isParameter(decl))) {
137047
137492
  let l = lookupCell(decl);
137048
137493
  recordUse(decl, true);
137049
- proc.emitExpr(l.storeByRef(emitExpr(src)));
137494
+ proc.emitExpr(l.storeByRef(emitEscapedExpression(src, "assignment")));
137050
137495
  }
137051
137496
  else {
137052
137497
  unhandled(trg, lf("bad target identifier"), 9248);
@@ -137062,7 +137507,10 @@ ${lbl}: .short 0xffff
137062
137507
  }
137063
137508
  proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
137064
137509
  }
137065
- else if (decl && (decl.kind == pxtc.SK.PropertySignature || decl.kind == pxtc.SK.PropertyAssignment || isSlowField(decl))) {
137510
+ else if (decl && (decl.kind == pxtc.SK.PropertySignature || decl.kind == pxtc.SK.PropertyAssignment)) {
137511
+ proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
137512
+ }
137513
+ else if (decl && isSlowField(decl)) {
137066
137514
  proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
137067
137515
  }
137068
137516
  else {
@@ -137078,7 +137526,7 @@ ${lbl}: .short 0xffff
137078
137526
  }
137079
137527
  }
137080
137528
  let trg2 = emitExpr(trg);
137081
- proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitExpr(src)]));
137529
+ proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitEscapedExpression(src, "assignment")]));
137082
137530
  break;
137083
137531
  }
137084
137532
  }
@@ -137489,7 +137937,7 @@ ${lbl}: .short 0xffff
137489
137937
  let mask = getMask(args);
137490
137938
  let convInfos = [];
137491
137939
  let args2 = args.map((a, i) => {
137492
- let r = emitExpr(a);
137940
+ let r = emitEscapedExpression(a, "runtimeCallArgument");
137493
137941
  if (!needsNumberConversions())
137494
137942
  return r;
137495
137943
  let f = fmt[i + 1];
@@ -137827,6 +138275,43 @@ ${lbl}: .short 0xffff
137827
138275
  emitExprAsStmt(node.expression);
137828
138276
  }
137829
138277
  function emitCondition(expr, inner = null) {
138278
+ // with `noBoolLower` flag, the generic paths below handle every condition
138279
+ const boolLower = !pxtc.target.switches.noBoolLower;
138280
+ // Lower `&&` / `||` used as a condition into short-circuiting jumps
138281
+ // that yield 0/1 directly, instead of boxing the intermediate
138282
+ // booleans and converting them back with a runtime helper.
138283
+ if (boolLower && !inner && !isStackMachine() && expr.kind == pxtc.SK.BinaryExpression) {
138284
+ const binary = expr;
138285
+ const op = binary.operatorToken.kind;
138286
+ if (op == pxtc.SK.AmpersandAmpersandToken || op == pxtc.SK.BarBarToken) {
138287
+ const shortCircuitLabel = proc.mkLabel("lazycond");
138288
+ const doneLabel = proc.mkLabel("lazycondfin");
138289
+ if (op == pxtc.SK.AmpersandAmpersandToken) {
138290
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.left), pxtc.ir.JmpMode.IfZero);
138291
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.right), pxtc.ir.JmpMode.IfZero);
138292
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(1), pxtc.ir.JmpMode.Always);
138293
+ proc.emitLbl(shortCircuitLabel);
138294
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(0), pxtc.ir.JmpMode.Always);
138295
+ }
138296
+ else {
138297
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.left), pxtc.ir.JmpMode.IfNotZero);
138298
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.right), pxtc.ir.JmpMode.IfNotZero);
138299
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(0), pxtc.ir.JmpMode.Always);
138300
+ proc.emitLbl(shortCircuitLabel);
138301
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(1), pxtc.ir.JmpMode.Always);
138302
+ }
138303
+ proc.emitLbl(doneLabel);
138304
+ return captureJmpValue();
138305
+ }
138306
+ }
138307
+ // Lower `!expr` used as a condition by negating the operand's
138308
+ // condition directly, avoiding a boxed boolean just to invert it.
138309
+ if (boolLower && !inner && !isStackMachine() && expr.kind == pxtc.SK.PrefixUnaryExpression) {
138310
+ const unary = expr;
138311
+ if (unary.operator == pxtc.SK.ExclamationToken) {
138312
+ return pxtc.ir.rtcall("Boolean_::bang", [emitCondition(unary.operand)]);
138313
+ }
138314
+ }
137830
138315
  if (!inner && isThumb() && expr.kind == pxtc.SK.BinaryExpression) {
137831
138316
  let be = expr;
137832
138317
  let mapped = pxtc.U.lookup(pxtc.thumbCmpMap, simpleInstruction(be, be.operatorToken.kind));
@@ -138278,7 +138763,10 @@ ${lbl}: .short 0xffff
138278
138763
  }
138279
138764
  }
138280
138765
  typeCheckSubtoSup(node.initializer, node);
138281
- proc.emitExpr(loc.storeByRef(emitExpr(node.initializer)));
138766
+ const initializerExpr = isGlobalVar(node)
138767
+ ? emitEscapedExpression(node.initializer, "initializer")
138768
+ : emitExpr(node.initializer);
138769
+ proc.emitExpr(loc.storeByRef(initializerExpr));
138282
138770
  currJres = null;
138283
138771
  proc.stackEmpty();
138284
138772
  }
@@ -138303,8 +138791,17 @@ ${lbl}: .short 0xffff
138303
138791
  exres = pxtc.ir.op(EK.FieldAccess, [objRef], fieldIndexCore(info, getFieldInfo(info, fieldName)));
138304
138792
  }
138305
138793
  else {
138794
+ // Dynamic field access on a non-class-typed receiver (any, structural type,
138795
+ // etc). The call site supplies exactly 1 arg (the receiver) but the target
138796
+ // proc resolved via iface dispatch may be a method that expects more --
138797
+ // the _args wrapper is what normally fills in the missing args. Flag this
138798
+ // ifaceIndex so canUseExactIfaceWrapper disqualifies the wrapper-skip
138799
+ // optimization for any proc reachable here. Mirrors the dynamic-method-call
138800
+ // path above.
138801
+ const ifaceIndex = getIfaceMemberId(fieldName, true);
138802
+ bin.dynamicIfaceCalls[ifaceIndex + ""] = true;
138306
138803
  exres = mkMethodCall([objRef], {
138307
- ifaceIndex: getIfaceMemberId(fieldName, true),
138804
+ ifaceIndex,
138308
138805
  callLocationIndex: markCallLocation(node),
138309
138806
  noArgs: true
138310
138807
  });
@@ -138687,6 +139184,11 @@ ${lbl}: .short 0xffff
138687
139184
  this.usedChars = new Uint32Array(0x10000 / 32);
138688
139185
  this.explicitlyUsedIfaceMembers = {};
138689
139186
  this.ifaceMemberMap = {};
139187
+ this.ifaceCallCounts = {};
139188
+ this.mapSetByFieldIdCounts = {};
139189
+ this.checkedFieldAccessCounts = {};
139190
+ this.dynamicIfaceCalls = {};
139191
+ this.usesMapSetByStringOnly = false;
138690
139192
  this.strings = {};
138691
139193
  this.hexlits = {};
138692
139194
  this.doubles = {};
@@ -141137,9 +141639,9 @@ ${lbl}: ${snippets.obj_header("pxt::number_vt")}
141137
141639
  uint32_t ifaceHashMult;
141138
141640
  PVoid methods[2 or 4];
141139
141641
  */
141140
- const ifaceInfo = computeHashMultiplier(info.itable.map(e => e.idx));
141141
- //if (info.itable.length == 0)
141142
- // ifaceInfo.mult = 0
141642
+ const ifaceInfo = info.itable.length
141643
+ ? computeHashMultiplier(info.itable.map(e => e.idx))
141644
+ : { mult: 0, mapping: new Uint16Array(0), size: 0 };
141143
141645
  let ptrSz = pxtc.target.shortPointers ? ".short" : ".word";
141144
141646
  let s = `
141145
141647
  .object ${info.id}_VT
@@ -141173,6 +141675,11 @@ ${info.id}_IfaceVT:
141173
141675
  `;
141174
141676
  const descSize = 8;
141175
141677
  const zeroOffset = ifaceInfo.mapping.length * 2;
141678
+ // Iface table is the one consumer that can elect the wrapper-skip
141679
+ // optimization. Procs with useExactIfaceWrapper get pointed at _iface
141680
+ // (which `b _nochk`); everyone else gets the usual _args wrapper entry.
141681
+ // canUseExactIfaceWrapper guarantees this is safe for the elected procs.
141682
+ const procIfaceLabel = (p) => (p.useExactIfaceWrapper ? p.label() + "_iface" : p.vtLabel()) + "@fn";
141176
141683
  let descs = "";
141177
141684
  let offset = zeroOffset;
141178
141685
  let offsets = {};
@@ -141180,11 +141687,11 @@ ${info.id}_IfaceVT:
141180
141687
  offsets[e.idx + ""] = offset;
141181
141688
  const desc = !e.proc ? 0 : e.proc.isGetter() ? 1 : 2;
141182
141689
  descs += ` .short ${e.idx}, ${desc} ; ${e.name}\n`;
141183
- descs += ` .word ${e.proc ? e.proc.vtLabel() + "@fn" : e.info}\n`;
141690
+ descs += ` .word ${e.proc ? procIfaceLabel(e.proc) : e.info}\n`;
141184
141691
  offset += descSize;
141185
141692
  if (e.setProc) {
141186
141693
  descs += ` .short ${e.idx}, 0 ; set ${e.name}\n`;
141187
- descs += ` .word ${e.setProc.vtLabel()}@fn\n`;
141694
+ descs += ` .word ${procIfaceLabel(e.setProc)}\n`;
141188
141695
  offset += descSize;
141189
141696
  }
141190
141697
  }
@@ -141197,7 +141704,9 @@ ${info.id}_IfaceVT:
141197
141704
  bin.itFullEntries++;
141198
141705
  }
141199
141706
  // offsets are relative to the position in the array
141200
- s += " .short " + pxtc.U.toArray(map).map((e, i) => (offsets[e + ""] || zeroOffset) - (i * 2)).join(", ") + "\n";
141707
+ const hashOffsets = pxtc.U.toArray(map).map((e, i) => (offsets[e + ""] || zeroOffset) - (i * 2));
141708
+ if (hashOffsets.length)
141709
+ s += " .short " + hashOffsets.join(", ") + "\n";
141201
141710
  s += descs;
141202
141711
  s += "\n";
141203
141712
  return s;