pxt-core 13.2.2 → 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.
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) {
@@ -126069,6 +126122,10 @@ ${baseLabel}_nochk:
126069
126122
  }
126070
126123
  emitFieldAccess(e, store = false) {
126071
126124
  let info = e.data;
126125
+ if (!store && this.shouldSpecializeCheckedFieldLoad(info)) {
126126
+ this.emitCheckedFieldLoad(info);
126127
+ return;
126128
+ }
126072
126129
  let pref = store ? "st" : "ld";
126073
126130
  let lbl = pref + "fld_" + info.classInfo.id + "_" + info.name;
126074
126131
  if (info.needsCheck && !pxtc.target.switches.skipClassCheck) {
@@ -126087,6 +126144,32 @@ ${baseLabel}_nochk:
126087
126144
  this.write(`ldr r0, [r0, ${xoff}]`);
126088
126145
  return;
126089
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
+ }
126090
126173
  writeFailBranch() {
126091
126174
  this.write(`.fail:`);
126092
126175
  this.write(`mov r1, lr`);
@@ -126307,12 +126390,50 @@ ${baseLabel}_nochk:
126307
126390
  }
126308
126391
  emitIfaceCall(procid, numargs, getset = "") {
126309
126392
  pxtc.U.assert(procid.ifaceIndex > 0);
126310
- this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
126311
- this.emitLabelledHelper("ifacecall" + numargs + "_" + getset, () => {
126393
+ const helperKey = "ifacecall" + numargs + "_" + getset;
126394
+ const helper = this.ensureLabelledHelper(helperKey, () => {
126312
126395
  this.write(`ldr r0, [sp, #0] ; ld-this`);
126313
126396
  this.loadVTable();
126314
126397
  this.ifaceCallCore(numargs, getset);
126315
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;
126316
126437
  }
126317
126438
  // vtable in r3; clobber r2
126318
126439
  checkSubtype(info, failLbl = ".fail", r2 = "r2") {
@@ -126410,6 +126531,8 @@ ${baseLabel}_nochk:
126410
126531
  }
126411
126532
  emitRtCall(topExpr, genCall = null) {
126412
126533
  let name = topExpr.data;
126534
+ const mapSetFieldId = this.mapSetByFieldId(topExpr);
126535
+ const specializeMapSetFieldId = mapSetFieldId !== undefined && this.shouldSpecializeMapSetByFieldId(mapSetFieldId);
126413
126536
  if (name == "pxt::beginTry") {
126414
126537
  return this.emitBeginTry(topExpr);
126415
126538
  }
@@ -126422,6 +126545,8 @@ ${baseLabel}_nochk:
126422
126545
  isRef: (maskInfo.refMask & (1 << i)) != 0,
126423
126546
  conv: convs.find(c => c.argIdx == i)
126424
126547
  }));
126548
+ if (specializeMapSetFieldId)
126549
+ allArgs = allArgs.filter(a => a.idx != 1);
126425
126550
  pxtc.U.assert(allArgs.length <= 4);
126426
126551
  let seenUpdate = false;
126427
126552
  for (let a of pxtc.U.reversed(allArgs)) {
@@ -126524,33 +126649,69 @@ ${baseLabel}_nochk:
126524
126649
  genCall();
126525
126650
  }
126526
126651
  else {
126527
- if (name != "langsupp::ignore")
126652
+ if (specializeMapSetFieldId)
126653
+ this.emitMapSetByFieldIdCall(mapSetFieldId);
126654
+ else if (name != "langsupp::ignore")
126528
126655
  this.alignedCall(name, "", 0, true);
126529
126656
  }
126530
126657
  if (clearStack) {
126531
126658
  this.clearArgs(complexArgs.filter(a => !a.isRef).map(a => a.expr), complexArgs.filter(a => a.isRef).map(a => a.expr));
126532
126659
  }
126533
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
+ }
126534
126683
  alignedCall(name, cmt = "", off = 0, saveStack = false) {
126535
126684
  if (pxtc.U.startsWith(name, "_cmp_") || pxtc.U.startsWith(name, "_pxt_"))
126536
126685
  saveStack = false;
126537
126686
  this.write(this.t.call_lbl(name, saveStack, this.stackAlignmentNeeded(off)) + cmt);
126538
126687
  }
126539
- 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) {
126540
126693
  if (!this.labelledHelpers[lbl]) {
126541
126694
  let outp = this.redirectOutput(generate);
126542
- this.emitHelper(outp, lbl);
126543
- this.labelledHelpers[lbl] = this.bin.codeHelpers[outp];
126544
- }
126545
- else {
126546
- this.write(this.t.call_lbl(this.labelledHelpers[lbl]));
126695
+ this.labelledHelpers[lbl] = this.helperLabel(outp, lbl);
126547
126696
  }
126697
+ return this.labelledHelpers[lbl];
126548
126698
  }
126549
- 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") {
126550
126707
  if (!this.bin.codeHelpers[asm]) {
126551
126708
  let len = Object.keys(this.bin.codeHelpers).length;
126552
126709
  this.bin.codeHelpers[asm] = `_${baseName}_${len}`;
126553
126710
  }
126711
+ return this.bin.codeHelpers[asm];
126712
+ }
126713
+ emitHelper(asm, baseName = "hlp") {
126714
+ this.helperLabel(asm, baseName);
126554
126715
  this.write(this.t.call_lbl(this.bin.codeHelpers[asm]));
126555
126716
  }
126556
126717
  pushToExprStack(a) {
@@ -126587,48 +126748,85 @@ ${baseLabel}_nochk:
126587
126748
  }
126588
126749
  }
126589
126750
  }
126590
- emitFieldMethods() {
126591
- for (let op of ["get", "set"]) {
126592
- this.write(`
126751
+ emitFieldMethod(op) {
126752
+ this.write(`
126593
126753
  ${this.helperObject(op)}
126594
126754
  .section code
126595
126755
  _pxt_map_${op}:
126596
126756
  `);
126597
- this.loadVTable("r4");
126598
- this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4");
126599
- this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"));
126600
- this.write(".notmap:");
126601
- let numargs = op == "set" ? 2 : 1;
126602
- let hasAlign = false;
126603
- this.write("mov r4, r3 ; save VT");
126604
- if (op == "set") {
126605
- if (pxtc.target.stackAlign) {
126606
- hasAlign = true;
126607
- this.write("push {lr} ; align");
126608
- }
126609
- 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(`
126610
126770
  push {r0, r2, lr}
126611
126771
  mov r0, r1
126612
126772
  `);
126613
- }
126614
- else {
126615
- this.write(`
126773
+ }
126774
+ else {
126775
+ this.write(`
126616
126776
  push {r0, lr}
126617
126777
  mov r0, r1
126618
126778
  `);
126619
- }
126620
- this.write(`
126779
+ }
126780
+ this.write(`
126621
126781
  bl pxtrt::lookupMapKey
126622
126782
  mov r1, r0 ; put key index in r1
126623
126783
  ldr r0, [sp, #0] ; restore obj pointer
126624
126784
  mov r3, r4 ; restore vt
126625
126785
  bl .dowork
126626
126786
  `);
126627
- this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)));
126628
- this.write("pop {pc}");
126629
- this.write(".dowork:");
126630
- this.ifaceCallCore(numargs, op, true);
126631
- }
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
+ `);
126632
126830
  }
126633
126831
  emitArrayMethod(op, isBuffer) {
126634
126832
  this.write(`
@@ -126714,6 +126912,12 @@ ${baseLabel}_nochk:
126714
126912
  this.emitArrayMethod(op, false);
126715
126913
  }
126716
126914
  }
126915
+ emitFieldMethods() {
126916
+ for (let op of ["get", "set"]) {
126917
+ this.emitFieldMethod(op);
126918
+ }
126919
+ this.emitStringMapSetMethod();
126920
+ }
126717
126921
  emitLambdaTrampoline() {
126718
126922
  let r3 = pxtc.target.stackAlign ? "r3," : "";
126719
126923
  this.write(`
@@ -127061,6 +127265,12 @@ ${baseLabel}_nochk:
127061
127265
  this.write(`.short 0, 0 ; no captured vars`);
127062
127266
  this.write(`.word ${this.proc.label()}_args@fn`);
127063
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
+ }
127064
127274
  this.write(`${this.proc.label()}_args:`);
127065
127275
  let numargs = this.proc.args.length;
127066
127276
  if (numargs == 0)
@@ -127829,6 +128039,17 @@ var ts;
127829
128039
  "pxt::fromInt": "_numops_fromInt",
127830
128040
  "pxt::switch_eq": "_pxt_switch_eq",
127831
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
+ }
127832
128053
  // snippets for ARM Thumb assembly
127833
128054
  class ThumbSnippets extends pxtc.AssemblerSnippets {
127834
128055
  stackAligned() {
@@ -127936,6 +128157,8 @@ ${lbl}:`;
127936
128157
  }
127937
128158
  call_lbl(lbl, saveStack, stackAlign) {
127938
128159
  let o = pxtc.U.lookup(inlineArithmetic, lbl);
128160
+ if (!o && boolLowerEnabled())
128161
+ o = pxtc.U.lookup(inlineBoolOps, lbl);
127939
128162
  if (o) {
127940
128163
  lbl = o;
127941
128164
  saveStack = false;
@@ -128107,6 +128330,99 @@ _numops_fromInt:
128107
128330
  blx lr
128108
128331
  .over2:
128109
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")}
128110
128426
  `;
128111
128427
  for (let op of Object.keys(pxtc.thumbCmpMap)) {
128112
128428
  op = op.replace(/.*::/, "");
@@ -128131,9 +128447,10 @@ _cmp_${op}:
128131
128447
  // the cmp isn't really needed, given how toBoolDecr() is compiled,
128132
128448
  // but better not rely on it
128133
128449
  // Also, cmp isn't needed when ref-counting (it ends with movs r0, r4)
128450
+ const toBoolDecr = boolLowerEnabled() ? "_numops_toBoolDecr" : "numops::toBoolDecr";
128134
128451
  r += boxedOp(`
128135
128452
  bl numops::${op}
128136
- bl numops::toBoolDecr
128453
+ bl ${toBoolDecr}
128137
128454
  cmp r0, #0`);
128138
128455
  }
128139
128456
  // switch(x){case k:} compiles to equality tests via pxt::switch_eq.
@@ -133579,6 +133896,7 @@ var ts;
133579
133896
  this.classInfo = null;
133580
133897
  this.perfCounterName = null;
133581
133898
  this.perfCounterNo = 0;
133899
+ this.useExactIfaceWrapper = false;
133582
133900
  this.body = [];
133583
133901
  this.lblNo = 0;
133584
133902
  this.action = null;
@@ -133596,6 +133914,11 @@ var ts;
133596
133914
  isGetter() {
133597
133915
  return this.action && this.action.kind == ts.SyntaxKind.GetAccessor;
133598
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.
133599
133922
  vtLabel() {
133600
133923
  return this.label() + (pxtc.isStackMachine() ? "" : "_args");
133601
133924
  }
@@ -134138,6 +134461,11 @@ var ts;
134138
134461
  argsFmt: ["T", "T", "S", "T"],
134139
134462
  value: 0
134140
134463
  },
134464
+ "pxtrt::mapSetByStringOnly": {
134465
+ name: "_pxt_map_set_by_string",
134466
+ argsFmt: ["T", "T", "S", "T"],
134467
+ value: 0
134468
+ },
134141
134469
  };
134142
134470
  let EK = pxtc.ir.EK;
134143
134471
  pxtc.SK = ts.SyntaxKind;
@@ -134871,6 +135199,87 @@ var ts;
134871
135199
  bin.breakpoints = opts.breakpoints;
134872
135200
  bin.name = opts.name;
134873
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
+ }
134874
135283
  function reset() {
134875
135284
  bin.reset();
134876
135285
  proc = null;
@@ -134951,7 +135360,13 @@ var ts;
134951
135360
  reset();
134952
135361
  needsUsingInfo = false;
134953
135362
  bin.finalPass = true;
135363
+ bin.ifaceCallCounts = {};
135364
+ bin.mapSetByFieldIdCounts = {};
135365
+ bin.checkedFieldAccessCounts = {};
135366
+ bin.dynamicIfaceCalls = {};
135367
+ bin.usesMapSetByStringOnly = false;
134954
135368
  emit(rootFunction);
135369
+ markExactIfaceWrappers();
134955
135370
  pxtc.U.assert(usedWorkList.length == 0);
134956
135371
  res.configData = [];
134957
135372
  for (let k of Object.keys(configEntries)) {
@@ -135641,7 +136056,7 @@ ${lbl}: .short 0xffff
135641
136056
  let coll = pxtc.ir.shared(pxtc.ir.rtcall("Array_::mk", []));
135642
136057
  for (let elt of node.elements) {
135643
136058
  let mask = isRefCountedExpr(elt) ? (1 << 1) : 0;
135644
- proc.emitExpr(pxtc.ir.rtcall("Array_::push", [coll, emitExpr(elt)], mask));
136059
+ proc.emitExpr(pxtc.ir.rtcall("Array_::push", [coll, emitEscapedExpression(elt, "arrayElement")], mask));
135645
136060
  }
135646
136061
  if (node.elements.length > 0)
135647
136062
  // Make sure there is at least one use of the array, so it doesn't get GC-ed away in the last push.
@@ -135683,8 +136098,11 @@ ${lbl}: .short 0xffff
135683
136098
  if (isRefCountedExpr(p.initializer))
135684
136099
  mask |= 1 << 2;
135685
136100
  }
136101
+ const nativeFieldId = pxtc.target.isNative ? getIfaceMemberId(keyName) : undefined;
136102
+ if (nativeFieldId !== undefined)
136103
+ bin.mapSetByFieldIdCounts[nativeFieldId + ""] = (bin.mapSetByFieldIdCounts[nativeFieldId + ""] || 0) + 1;
135686
136104
  const fieldId = pxtc.target.isNative
135687
- ? pxtc.ir.numlit(getIfaceMemberId(keyName))
136105
+ ? pxtc.ir.numlit(nativeFieldId)
135688
136106
  : pxtc.ir.ptrlit(null, JSON.stringify(keyName));
135689
136107
  const args = [
135690
136108
  expr,
@@ -135744,6 +136162,9 @@ ${lbl}: .short 0xffff
135744
136162
  return emitShim(fld, decl, [node.expression]);
135745
136163
  }
135746
136164
  else {
136165
+ if (idx.needsCheck && !pxtc.target.switches.skipClassCheck) {
136166
+ bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] = (bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] || 0) + 1;
136167
+ }
135747
136168
  return pxtc.ir.op(EK.FieldAccess, [emitExpr(node.expression)], idx);
135748
136169
  }
135749
136170
  }
@@ -135797,7 +136218,17 @@ ${lbl}: .short 0xffff
135797
136218
  indexer = assign ? attrs.indexerSet : attrs.indexerGet;
135798
136219
  }
135799
136220
  if (!indexer && (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.StructuredOrTypeVariable))) {
135800
- 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";
135801
136232
  stringOk = true;
135802
136233
  }
135803
136234
  if (indexer) {
@@ -136134,7 +136565,7 @@ ${lbl}: .short 0xffff
136134
136565
  lst.push(tracked);
136135
136566
  }
136136
136567
  function emitPlain() {
136137
- let r = mkProcCall(decl, node, args.map((x) => emitExpr(x)));
136568
+ let r = mkProcCall(decl, node, args.map(x => emitEscapedExpression(x, "callArgument")));
136138
136569
  let pp = r.data;
136139
136570
  if (args[0] && pp.proc && pp.proc.classInfo)
136140
136571
  pp.isThis = args[0].kind == pxtc.SK.ThisKeyword;
@@ -136161,7 +136592,7 @@ ${lbl}: .short 0xffff
136161
136592
  baseCtor = p.ctor;
136162
136593
  if (!baseCtor && bin.finalPass)
136163
136594
  throw userError(9280, lf("super() call requires an explicit constructor in base class"));
136164
- let ctorArgs = args.map((x) => emitExpr(x));
136595
+ let ctorArgs = args.map((x) => emitEscapedExpression(x, "callArgument"));
136165
136596
  ctorArgs.unshift(emitThis(funcExpr));
136166
136597
  return mkProcCallCore(baseCtor, node, ctorArgs);
136167
136598
  }
@@ -136177,9 +136608,11 @@ ${lbl}: .short 0xffff
136177
136608
  // TODO in VT accessor/field/method -> different
136178
136609
  pxtc.U.assert(funcExpr.kind == pxtc.SK.PropertyAccessExpression);
136179
136610
  const fieldName = funcExpr.name.text;
136611
+ const ifaceIndex = getIfaceMemberId(fieldName, true);
136612
+ bin.dynamicIfaceCalls[ifaceIndex + ""] = true;
136180
136613
  // completely dynamic dispatch
136181
- return mkMethodCall(args.map((x) => emitExpr(x)), {
136182
- ifaceIndex: getIfaceMemberId(fieldName, true),
136614
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
136615
+ ifaceIndex,
136183
136616
  callLocationIndex: markCallLocation(node),
136184
136617
  noArgs
136185
136618
  });
@@ -136203,7 +136636,7 @@ ${lbl}: .short 0xffff
136203
136636
  pxtc.U.assert(false);
136204
136637
  }
136205
136638
  pxtc.U.assert(!bin.finalPass || info.virtualIndex != null, "!bin.finalPass || info.virtualIndex != null");
136206
- return mkMethodCall(args.map((x) => emitExpr(x)), {
136639
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
136207
136640
  classInfo: info.parentClassInfo,
136208
136641
  virtualIndex: info.virtualIndex,
136209
136642
  noArgs,
@@ -136243,9 +136676,15 @@ ${lbl}: .short 0xffff
136243
136676
  return emitPlain();
136244
136677
  }
136245
136678
  else if (needsVCall || pxtc.target.switches.slowMethods || !forceMethod) {
136246
- return mkMethodCall(args.map((x) => emitExpr(x)), {
136247
- ifaceIndex: getIfaceMemberId(getName(decl), true),
136248
- 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,
136249
136688
  callLocationIndex: markCallLocation(node),
136250
136689
  noArgs
136251
136690
  });
@@ -136264,7 +136703,7 @@ ${lbl}: .short 0xffff
136264
136703
  // here's where we will recurse to generate funcExpr
136265
136704
  args.unshift(funcExpr);
136266
136705
  pxtc.U.assert(!noArgs);
136267
- return mkMethodCall(args.map(x => emitExpr(x)), {
136706
+ return mkMethodCall(args.map(x => emitEscapedExpression(x, "callArgument")), {
136268
136707
  virtualIndex: -1,
136269
136708
  callLocationIndex: markCallLocation(node),
136270
136709
  noArgs
@@ -136360,6 +136799,8 @@ ${lbl}: .short 0xffff
136360
136799
  }
136361
136800
  }
136362
136801
  function getCtor(decl) {
136802
+ if (!decl.members)
136803
+ return null;
136363
136804
  return decl.members.filter(m => m.kind == pxtc.SK.Constructor)[0];
136364
136805
  }
136365
136806
  function isIfaceMemberUsed(name) {
@@ -136407,7 +136848,7 @@ ${lbl}: .short 0xffff
136407
136848
  // let sig = checker.getResolvedSignature(node)
136408
136849
  // TODO: can we have overloeads?
136409
136850
  addDefaultParametersAndTypeCheck(checker.getResolvedSignature(node), args, ctorAttrs);
136410
- let compiled = args.map((x) => emitExpr(x));
136851
+ let compiled = args.map((x) => emitEscapedExpression(x, "callArgument"));
136411
136852
  if (ctorAttrs.shim) {
136412
136853
  // TODO need to deal with refMask and tagged ints here
136413
136854
  // we drop 'obj' variable
@@ -136796,7 +137237,7 @@ ${lbl}: .short 0xffff
136796
137237
  for (let f of initalizedFields) {
136797
137238
  let idx = fieldIndexCore(proc.classInfo, getFieldInfo(proc.classInfo, getName(f)), false);
136798
137239
  let trg2 = pxtc.ir.op(EK.FieldAccess, [emitLocalLoad(info.thisParameter)], idx);
136799
- proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitExpr(f.initializer)]));
137240
+ proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitEscapedExpression(f.initializer, "initializer")]));
136800
137241
  }
136801
137242
  flushHoistedFunctionDefinitions();
136802
137243
  if (node.body.kind == pxtc.SK.Block) {
@@ -137014,9 +137455,12 @@ ${lbl}: .short 0xffff
137014
137455
  isRef: true,
137015
137456
  shimName: attrs.shim,
137016
137457
  classInfo: info,
137017
- needsCheck
137458
+ needsCheck,
137018
137459
  };
137019
137460
  }
137461
+ function checkedFieldAccessCountKey(info) {
137462
+ return `${info.classInfo.id}:${info.name}:get`;
137463
+ }
137020
137464
  function fieldIndex(pacc) {
137021
137465
  const tp = typeOf(pacc.expression);
137022
137466
  if (isPossiblyGenericClassType(tp)) {
@@ -137047,7 +137491,7 @@ ${lbl}: .short 0xffff
137047
137491
  if (decl && (isGlobal || isVar(decl) || isParameter(decl))) {
137048
137492
  let l = lookupCell(decl);
137049
137493
  recordUse(decl, true);
137050
- proc.emitExpr(l.storeByRef(emitExpr(src)));
137494
+ proc.emitExpr(l.storeByRef(emitEscapedExpression(src, "assignment")));
137051
137495
  }
137052
137496
  else {
137053
137497
  unhandled(trg, lf("bad target identifier"), 9248);
@@ -137063,7 +137507,10 @@ ${lbl}: .short 0xffff
137063
137507
  }
137064
137508
  proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
137065
137509
  }
137066
- 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)) {
137067
137514
  proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
137068
137515
  }
137069
137516
  else {
@@ -137079,7 +137526,7 @@ ${lbl}: .short 0xffff
137079
137526
  }
137080
137527
  }
137081
137528
  let trg2 = emitExpr(trg);
137082
- proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitExpr(src)]));
137529
+ proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitEscapedExpression(src, "assignment")]));
137083
137530
  break;
137084
137531
  }
137085
137532
  }
@@ -137490,7 +137937,7 @@ ${lbl}: .short 0xffff
137490
137937
  let mask = getMask(args);
137491
137938
  let convInfos = [];
137492
137939
  let args2 = args.map((a, i) => {
137493
- let r = emitExpr(a);
137940
+ let r = emitEscapedExpression(a, "runtimeCallArgument");
137494
137941
  if (!needsNumberConversions())
137495
137942
  return r;
137496
137943
  let f = fmt[i + 1];
@@ -137828,6 +138275,43 @@ ${lbl}: .short 0xffff
137828
138275
  emitExprAsStmt(node.expression);
137829
138276
  }
137830
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
+ }
137831
138315
  if (!inner && isThumb() && expr.kind == pxtc.SK.BinaryExpression) {
137832
138316
  let be = expr;
137833
138317
  let mapped = pxtc.U.lookup(pxtc.thumbCmpMap, simpleInstruction(be, be.operatorToken.kind));
@@ -138279,7 +138763,10 @@ ${lbl}: .short 0xffff
138279
138763
  }
138280
138764
  }
138281
138765
  typeCheckSubtoSup(node.initializer, node);
138282
- 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));
138283
138770
  currJres = null;
138284
138771
  proc.stackEmpty();
138285
138772
  }
@@ -138304,8 +138791,17 @@ ${lbl}: .short 0xffff
138304
138791
  exres = pxtc.ir.op(EK.FieldAccess, [objRef], fieldIndexCore(info, getFieldInfo(info, fieldName)));
138305
138792
  }
138306
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;
138307
138803
  exres = mkMethodCall([objRef], {
138308
- ifaceIndex: getIfaceMemberId(fieldName, true),
138804
+ ifaceIndex,
138309
138805
  callLocationIndex: markCallLocation(node),
138310
138806
  noArgs: true
138311
138807
  });
@@ -138688,6 +139184,11 @@ ${lbl}: .short 0xffff
138688
139184
  this.usedChars = new Uint32Array(0x10000 / 32);
138689
139185
  this.explicitlyUsedIfaceMembers = {};
138690
139186
  this.ifaceMemberMap = {};
139187
+ this.ifaceCallCounts = {};
139188
+ this.mapSetByFieldIdCounts = {};
139189
+ this.checkedFieldAccessCounts = {};
139190
+ this.dynamicIfaceCalls = {};
139191
+ this.usesMapSetByStringOnly = false;
138691
139192
  this.strings = {};
138692
139193
  this.hexlits = {};
138693
139194
  this.doubles = {};
@@ -141138,9 +141639,9 @@ ${lbl}: ${snippets.obj_header("pxt::number_vt")}
141138
141639
  uint32_t ifaceHashMult;
141139
141640
  PVoid methods[2 or 4];
141140
141641
  */
141141
- const ifaceInfo = computeHashMultiplier(info.itable.map(e => e.idx));
141142
- //if (info.itable.length == 0)
141143
- // 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 };
141144
141645
  let ptrSz = pxtc.target.shortPointers ? ".short" : ".word";
141145
141646
  let s = `
141146
141647
  .object ${info.id}_VT
@@ -141174,6 +141675,11 @@ ${info.id}_IfaceVT:
141174
141675
  `;
141175
141676
  const descSize = 8;
141176
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";
141177
141683
  let descs = "";
141178
141684
  let offset = zeroOffset;
141179
141685
  let offsets = {};
@@ -141181,11 +141687,11 @@ ${info.id}_IfaceVT:
141181
141687
  offsets[e.idx + ""] = offset;
141182
141688
  const desc = !e.proc ? 0 : e.proc.isGetter() ? 1 : 2;
141183
141689
  descs += ` .short ${e.idx}, ${desc} ; ${e.name}\n`;
141184
- descs += ` .word ${e.proc ? e.proc.vtLabel() + "@fn" : e.info}\n`;
141690
+ descs += ` .word ${e.proc ? procIfaceLabel(e.proc) : e.info}\n`;
141185
141691
  offset += descSize;
141186
141692
  if (e.setProc) {
141187
141693
  descs += ` .short ${e.idx}, 0 ; set ${e.name}\n`;
141188
- descs += ` .word ${e.setProc.vtLabel()}@fn\n`;
141694
+ descs += ` .word ${procIfaceLabel(e.setProc)}\n`;
141189
141695
  offset += descSize;
141190
141696
  }
141191
141697
  }
@@ -141198,7 +141704,9 @@ ${info.id}_IfaceVT:
141198
141704
  bin.itFullEntries++;
141199
141705
  }
141200
141706
  // offsets are relative to the position in the array
141201
- 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";
141202
141710
  s += descs;
141203
141711
  s += "\n";
141204
141712
  return s;