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.
@@ -1138,6 +1138,10 @@ ${baseLabel}_nochk:
1138
1138
  }
1139
1139
  emitFieldAccess(e, store = false) {
1140
1140
  let info = e.data;
1141
+ if (!store && this.shouldSpecializeCheckedFieldLoad(info)) {
1142
+ this.emitCheckedFieldLoad(info);
1143
+ return;
1144
+ }
1141
1145
  let pref = store ? "st" : "ld";
1142
1146
  let lbl = pref + "fld_" + info.classInfo.id + "_" + info.name;
1143
1147
  if (info.needsCheck && !pxtc.target.switches.skipClassCheck) {
@@ -1156,6 +1160,32 @@ ${baseLabel}_nochk:
1156
1160
  this.write(`ldr r0, [r0, ${xoff}]`);
1157
1161
  return;
1158
1162
  }
1163
+ checkedFieldAccessCountKey(info) {
1164
+ return `${info.classInfo.id}:${info.name}:get`;
1165
+ }
1166
+ shouldSpecializeCheckedFieldLoad(info) {
1167
+ if (!pxtc.isThumb() || !this.bin.finalPass || pxtc.target.switches.noIfaceSpec)
1168
+ return false;
1169
+ if (pxtc.target.switches.skipClassCheck || !info.needsCheck)
1170
+ return false;
1171
+ return (this.bin.checkedFieldAccessCounts[this.checkedFieldAccessCountKey(info)] || 0) >= 5;
1172
+ }
1173
+ emitCheckedFieldLoad(info) {
1174
+ const helperKey = "ldfldchk_" + info.classInfo.id + "_" + info.name;
1175
+ const helper = this.ensureLabelledHelper(helperKey, () => {
1176
+ this.write(this.t.helper_prologue());
1177
+ this.emitInstanceOf(info.classInfo, "validate");
1178
+ let off = info.idx * 4 + 4;
1179
+ let xoff = "#" + off;
1180
+ if (off > 124) {
1181
+ this.write(this.t.emit_int(off, "r3"));
1182
+ xoff = "r3";
1183
+ }
1184
+ this.write(`ldr r0, [r0, ${xoff}]`);
1185
+ this.write(this.t.helper_epilogue());
1186
+ });
1187
+ this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0)));
1188
+ }
1159
1189
  writeFailBranch() {
1160
1190
  this.write(`.fail:`);
1161
1191
  this.write(`mov r1, lr`);
@@ -1376,12 +1406,50 @@ ${baseLabel}_nochk:
1376
1406
  }
1377
1407
  emitIfaceCall(procid, numargs, getset = "") {
1378
1408
  pxtc.U.assert(procid.ifaceIndex > 0);
1379
- this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
1380
- this.emitLabelledHelper("ifacecall" + numargs + "_" + getset, () => {
1409
+ const helperKey = "ifacecall" + numargs + "_" + getset;
1410
+ const helper = this.ensureLabelledHelper(helperKey, () => {
1381
1411
  this.write(`ldr r0, [sp, #0] ; ld-this`);
1382
1412
  this.loadVTable();
1383
1413
  this.ifaceCallCore(numargs, getset);
1384
1414
  });
1415
+ if (this.shouldSpecializeIfaceCall(procid.ifaceIndex, numargs, getset)) {
1416
+ const thunkKey = helperKey + "_i" + procid.ifaceIndex;
1417
+ this.emitLabelledHelper(thunkKey, () => {
1418
+ this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
1419
+ this.write(`ldlit r7, ${helper}@fn`);
1420
+ this.write(`bx r7`);
1421
+ });
1422
+ }
1423
+ else {
1424
+ this.write(this.t.emit_int(procid.ifaceIndex, "r1"));
1425
+ this.write(this.t.call_lbl(helper));
1426
+ }
1427
+ }
1428
+ ifaceCallCountKey(ifaceIndex, numargs, getset) {
1429
+ return `${ifaceIndex}:${numargs}:${getset || "call"}`;
1430
+ }
1431
+ // Count-driven specialization decisions (this method and the related
1432
+ // shouldSpecializeCheckedFieldLoad / shouldSpecializeMapSetByFieldId) assume:
1433
+ // - bin.finalPass is true, AND
1434
+ // - the count maps were populated during the same final-pass emit()
1435
+ // call that builds the IR this asm walk consumes.
1436
+ // This is currently true because the frontend populates counts during IR
1437
+ // generation and the backend reads them only during the subsequent asm walk.
1438
+ // Any future change that interleaves IR generation with asm emission would
1439
+ // silently regress these to "always false".
1440
+ //
1441
+ // Threshold of 3 is the break-even point for the labelled-thunk pattern:
1442
+ // each shared call site saves ~4 bytes vs an inline call, and the thunk
1443
+ // itself costs ~12 bytes. At 3 callers, savings break even with thunk cost.
1444
+ shouldSpecializeIfaceCall(ifaceIndex, numargs, getset) {
1445
+ if (!pxtc.isThumb() || !this.bin.finalPass || pxtc.target.switches.noIfaceSpec)
1446
+ return false;
1447
+ const count = this.bin.ifaceCallCounts[this.ifaceCallCountKey(ifaceIndex, numargs, getset)] || 0;
1448
+ if (getset == "get" && numargs == 1)
1449
+ return count >= 3;
1450
+ if (getset == "set" || !getset)
1451
+ return count >= 3;
1452
+ return false;
1385
1453
  }
1386
1454
  // vtable in r3; clobber r2
1387
1455
  checkSubtype(info, failLbl = ".fail", r2 = "r2") {
@@ -1479,6 +1547,8 @@ ${baseLabel}_nochk:
1479
1547
  }
1480
1548
  emitRtCall(topExpr, genCall = null) {
1481
1549
  let name = topExpr.data;
1550
+ const mapSetFieldId = this.mapSetByFieldId(topExpr);
1551
+ const specializeMapSetFieldId = mapSetFieldId !== undefined && this.shouldSpecializeMapSetByFieldId(mapSetFieldId);
1482
1552
  if (name == "pxt::beginTry") {
1483
1553
  return this.emitBeginTry(topExpr);
1484
1554
  }
@@ -1491,6 +1561,8 @@ ${baseLabel}_nochk:
1491
1561
  isRef: (maskInfo.refMask & (1 << i)) != 0,
1492
1562
  conv: convs.find(c => c.argIdx == i)
1493
1563
  }));
1564
+ if (specializeMapSetFieldId)
1565
+ allArgs = allArgs.filter(a => a.idx != 1);
1494
1566
  pxtc.U.assert(allArgs.length <= 4);
1495
1567
  let seenUpdate = false;
1496
1568
  for (let a of pxtc.U.reversed(allArgs)) {
@@ -1593,33 +1665,69 @@ ${baseLabel}_nochk:
1593
1665
  genCall();
1594
1666
  }
1595
1667
  else {
1596
- if (name != "langsupp::ignore")
1668
+ if (specializeMapSetFieldId)
1669
+ this.emitMapSetByFieldIdCall(mapSetFieldId);
1670
+ else if (name != "langsupp::ignore")
1597
1671
  this.alignedCall(name, "", 0, true);
1598
1672
  }
1599
1673
  if (clearStack) {
1600
1674
  this.clearArgs(complexArgs.filter(a => !a.isRef).map(a => a.expr), complexArgs.filter(a => a.isRef).map(a => a.expr));
1601
1675
  }
1602
1676
  }
1677
+ mapSetByFieldId(topExpr) {
1678
+ if (topExpr.data != "pxtrt::mapSet" || topExpr.args.length != 3)
1679
+ return undefined;
1680
+ const fieldId = topExpr.args[1];
1681
+ if (fieldId.exprKind != pxtc.ir.EK.NumberLiteral || typeof fieldId.data != "number")
1682
+ return undefined;
1683
+ return fieldId.data;
1684
+ }
1685
+ shouldSpecializeMapSetByFieldId(fieldId) {
1686
+ if (!pxtc.isThumb() || !this.bin.finalPass || pxtc.target.switches.noIfaceSpec)
1687
+ return false;
1688
+ return (this.bin.mapSetByFieldIdCounts[fieldId + ""] || 0) >= 3;
1689
+ }
1690
+ emitMapSetByFieldIdCall(fieldId) {
1691
+ const helper = this.ensureLabelledHelper("mapset_i" + fieldId, () => {
1692
+ this.write(this.t.helper_prologue());
1693
+ this.write(this.t.emit_int(fieldId, "r1"));
1694
+ this.write(this.t.callCPP("pxtrt::mapSet"));
1695
+ this.write(this.t.helper_epilogue());
1696
+ });
1697
+ this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0)));
1698
+ }
1603
1699
  alignedCall(name, cmt = "", off = 0, saveStack = false) {
1604
1700
  if (pxtc.U.startsWith(name, "_cmp_") || pxtc.U.startsWith(name, "_pxt_"))
1605
1701
  saveStack = false;
1606
1702
  this.write(this.t.call_lbl(name, saveStack, this.stackAlignmentNeeded(off)) + cmt);
1607
1703
  }
1608
- emitLabelledHelper(lbl, generate) {
1704
+ // Lazily emit a deduped helper body and return its label.
1705
+ // Use this when you want to BRANCH to (or reference) the helper without
1706
+ // emitting a `bl` at the current cursor -- e.g. when wrapping the call in
1707
+ // a thunk, or storing the label for later use.
1708
+ ensureLabelledHelper(lbl, generate) {
1609
1709
  if (!this.labelledHelpers[lbl]) {
1610
1710
  let outp = this.redirectOutput(generate);
1611
- this.emitHelper(outp, lbl);
1612
- this.labelledHelpers[lbl] = this.bin.codeHelpers[outp];
1613
- }
1614
- else {
1615
- this.write(this.t.call_lbl(this.labelledHelpers[lbl]));
1711
+ this.labelledHelpers[lbl] = this.helperLabel(outp, lbl);
1616
1712
  }
1713
+ return this.labelledHelpers[lbl];
1617
1714
  }
1618
- emitHelper(asm, baseName = "hlp") {
1715
+ // Ensure a deduped helper exists and emit a `bl` to it at the cursor.
1716
+ // Use this for the common case of "factor this sequence into a helper and call it here".
1717
+ emitLabelledHelper(lbl, generate) {
1718
+ const helper = this.ensureLabelledHelper(lbl, generate);
1719
+ this.write(this.t.call_lbl(helper));
1720
+ return helper;
1721
+ }
1722
+ helperLabel(asm, baseName = "hlp") {
1619
1723
  if (!this.bin.codeHelpers[asm]) {
1620
1724
  let len = Object.keys(this.bin.codeHelpers).length;
1621
1725
  this.bin.codeHelpers[asm] = `_${baseName}_${len}`;
1622
1726
  }
1727
+ return this.bin.codeHelpers[asm];
1728
+ }
1729
+ emitHelper(asm, baseName = "hlp") {
1730
+ this.helperLabel(asm, baseName);
1623
1731
  this.write(this.t.call_lbl(this.bin.codeHelpers[asm]));
1624
1732
  }
1625
1733
  pushToExprStack(a) {
@@ -1656,48 +1764,85 @@ ${baseLabel}_nochk:
1656
1764
  }
1657
1765
  }
1658
1766
  }
1659
- emitFieldMethods() {
1660
- for (let op of ["get", "set"]) {
1661
- this.write(`
1767
+ emitFieldMethod(op) {
1768
+ this.write(`
1662
1769
  ${this.helperObject(op)}
1663
1770
  .section code
1664
1771
  _pxt_map_${op}:
1665
1772
  `);
1666
- this.loadVTable("r4");
1667
- this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4");
1668
- this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"));
1669
- this.write(".notmap:");
1670
- let numargs = op == "set" ? 2 : 1;
1671
- let hasAlign = false;
1672
- this.write("mov r4, r3 ; save VT");
1673
- if (op == "set") {
1674
- if (pxtc.target.stackAlign) {
1675
- hasAlign = true;
1676
- this.write("push {lr} ; align");
1677
- }
1678
- this.write(`
1773
+ this.loadVTable("r4");
1774
+ this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4");
1775
+ this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString"));
1776
+ this.write(".notmap:");
1777
+ let numargs = op == "set" ? 2 : 1;
1778
+ let hasAlign = false;
1779
+ this.write("mov r4, r3 ; save VT");
1780
+ if (op == "set") {
1781
+ if (pxtc.target.stackAlign) {
1782
+ hasAlign = true;
1783
+ this.write("push {lr} ; align");
1784
+ }
1785
+ this.write(`
1679
1786
  push {r0, r2, lr}
1680
1787
  mov r0, r1
1681
1788
  `);
1682
- }
1683
- else {
1684
- this.write(`
1789
+ }
1790
+ else {
1791
+ this.write(`
1685
1792
  push {r0, lr}
1686
1793
  mov r0, r1
1687
1794
  `);
1688
- }
1689
- this.write(`
1795
+ }
1796
+ this.write(`
1690
1797
  bl pxtrt::lookupMapKey
1691
1798
  mov r1, r0 ; put key index in r1
1692
1799
  ldr r0, [sp, #0] ; restore obj pointer
1693
1800
  mov r3, r4 ; restore vt
1694
1801
  bl .dowork
1695
1802
  `);
1696
- this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)));
1697
- this.write("pop {pc}");
1698
- this.write(".dowork:");
1699
- this.ifaceCallCore(numargs, op, true);
1700
- }
1803
+ this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0)));
1804
+ this.write("pop {pc}");
1805
+ this.write(".dowork:");
1806
+ this.ifaceCallCore(numargs, op, true);
1807
+ }
1808
+ emitStringMapSetMethod() {
1809
+ // Fast path for the well-typed "map with string index signature" case.
1810
+ // - tagged-int / null receivers: unrecoverable, panic with failedCast (matches the
1811
+ // pre-optimization mapSetGeneric path, which also can't proceed on these).
1812
+ // - non-RefMap pointer receivers: tail-call _pxt_map_set, which handles
1813
+ // non-map objects via interface dispatch. Preserves back-compat for code that
1814
+ // casts non-map objects to a string-index-signature type.
1815
+ //
1816
+ // Emitted only when the final-pass IR selected mapSetByStringOnly
1817
+ // (never under `noIfaceSpec`): unreferenced helper text costs image
1818
+ // space in every program otherwise.
1819
+ if (!this.bin.usesMapSetByStringOnly)
1820
+ return;
1821
+ this.write(`
1822
+ ${this.helperObject("set string map")}
1823
+ .section code
1824
+ _pxt_map_set_by_string:
1825
+ lsls r4, r0, #30
1826
+ bne .badtype
1827
+ cmp r0, #0
1828
+ beq .badtype
1829
+ ldr r3, [r0, #0]
1830
+ `);
1831
+ this.write(`
1832
+ ldrh r4, [r3, #8]
1833
+ cmp r4, #${pxt.BuiltInType.RefMap}
1834
+ bne .fallback
1835
+ `);
1836
+ this.write(this.t.callCPPPush("pxtrt::mapSetByString"));
1837
+ this.write(`
1838
+ .fallback:
1839
+ b _pxt_map_set
1840
+ .badtype:
1841
+ mov r1, lr
1842
+ mov r7, sp
1843
+ str r7, [r6, #4]
1844
+ bl pxt::failedCast
1845
+ `);
1701
1846
  }
1702
1847
  emitArrayMethod(op, isBuffer) {
1703
1848
  this.write(`
@@ -1783,6 +1928,12 @@ ${baseLabel}_nochk:
1783
1928
  this.emitArrayMethod(op, false);
1784
1929
  }
1785
1930
  }
1931
+ emitFieldMethods() {
1932
+ for (let op of ["get", "set"]) {
1933
+ this.emitFieldMethod(op);
1934
+ }
1935
+ this.emitStringMapSetMethod();
1936
+ }
1786
1937
  emitLambdaTrampoline() {
1787
1938
  let r3 = pxtc.target.stackAlign ? "r3," : "";
1788
1939
  this.write(`
@@ -2130,6 +2281,12 @@ ${baseLabel}_nochk:
2130
2281
  this.write(`.short 0, 0 ; no captured vars`);
2131
2282
  this.write(`.word ${this.proc.label()}_args@fn`);
2132
2283
  }
2284
+ if (this.proc.useExactIfaceWrapper) {
2285
+ this.write(`${this.proc.label()}_iface:`);
2286
+ this.write(`b ${this.proc.label()}_nochk`);
2287
+ if (!this.proc.info.usedAsValue)
2288
+ return;
2289
+ }
2133
2290
  this.write(`${this.proc.label()}_args:`);
2134
2291
  let numargs = this.proc.args.length;
2135
2292
  if (numargs == 0)
@@ -2898,6 +3055,17 @@ var ts;
2898
3055
  "pxt::fromInt": "_numops_fromInt",
2899
3056
  "pxt::switch_eq": "_pxt_switch_eq",
2900
3057
  };
3058
+ // Boolean conversion fast paths, kept out of inlineArithmetic so the
3059
+ // `noBoolLower` flag can drop the group (redirects and helper bodies alike).
3060
+ const inlineBoolOps = {
3061
+ "pxt::fromBool": "_pxt_fromBool",
3062
+ "numops::toBool": "_numops_toBool",
3063
+ "numops::toBoolDecr": "_numops_toBoolDecr",
3064
+ "Boolean_::bang": "_pxt_boolean_bang",
3065
+ };
3066
+ function boolLowerEnabled() {
3067
+ return !pxtc.target.switches.noBoolLower;
3068
+ }
2901
3069
  // snippets for ARM Thumb assembly
2902
3070
  class ThumbSnippets extends pxtc.AssemblerSnippets {
2903
3071
  stackAligned() {
@@ -3005,6 +3173,8 @@ ${lbl}:`;
3005
3173
  }
3006
3174
  call_lbl(lbl, saveStack, stackAlign) {
3007
3175
  let o = pxtc.U.lookup(inlineArithmetic, lbl);
3176
+ if (!o && boolLowerEnabled())
3177
+ o = pxtc.U.lookup(inlineBoolOps, lbl);
3008
3178
  if (o) {
3009
3179
  lbl = o;
3010
3180
  saveStack = false;
@@ -3176,6 +3346,99 @@ _numops_fromInt:
3176
3346
  blx lr
3177
3347
  .over2:
3178
3348
  ${this.callCPPPush("pxt::fromInt")}
3349
+ `;
3350
+ // skipped under `noBoolLower` flag: unreferenced helper text still costs image space
3351
+ if (boolLowerEnabled())
3352
+ r += `
3353
+ ; Tag a raw 0/1 truth value into a boolean object (taggedTrue/taggedFalse).
3354
+ ; Inverse of the toBool fast paths below; matches C++ pxt::fromBool.
3355
+ _pxt_fromBool:
3356
+ @scope _pxt_fromBool
3357
+ cmp r0, #0
3358
+ beq .false
3359
+ .true:
3360
+ movs r0, #${pxtc.taggedTrue}
3361
+ blx lr
3362
+ .false:
3363
+ movs r0, #${pxtc.taggedFalse}
3364
+ blx lr
3365
+
3366
+ ; Logical NOT of a raw 0/1 truth value -> raw 0/1 (matches C++ bool bang(bool)).
3367
+ ; Callers pass an already-tested condition, so this is raw-in / raw-out; value
3368
+ ; context (x = !y) wraps the result with fromBool to re-tag it.
3369
+ _pxt_boolean_bang:
3370
+ @scope _pxt_boolean_bang
3371
+ cmp r0, #0
3372
+ beq .true
3373
+ .false:
3374
+ movs r0, #0
3375
+ blx lr
3376
+ .true:
3377
+ movs r0, #1
3378
+ blx lr
3379
+
3380
+ ; Fast-path truthiness test: tagged value in r0 -> raw 0/1 in r0.
3381
+ ; Value encodings (see taggedSpecialValue in emitter.ts): tagged int = (n<<1)|1
3382
+ ; (low bit set); special tags = (n<<2)|2 with undefined=0, null, false, NaN,
3383
+ ; true; heap objects have low 2 bits 00.
3384
+ ; Only non-pointer values are decided inline here; any heap object is handed to
3385
+ ; the C++ helper at .boxed (which also handles boxed numbers/strings like 0.0
3386
+ ; and ""). INVARIANT: the falsy set below must match C++ numops::toBool exactly
3387
+ ; -- if a new falsy tagged value is ever added, it must be added here too, or it
3388
+ ; will fall through to .true.
3389
+ _numops_toBool:
3390
+ @scope _numops_toBool
3391
+ cmp r0, #0 ; undefined (0) -> false
3392
+ beq .false
3393
+ cmp r0, #1 ; integer 0 ((0<<1)|1) -> false
3394
+ beq .false
3395
+ lsls r1, r0, #31 ; odd => nonzero tagged int -> true
3396
+ bne .true
3397
+ cmp r0, #${pxtc.taggedNull} ; null -> false
3398
+ beq .false
3399
+ cmp r0, #${pxtc.taggedFalse} ; false -> false
3400
+ beq .false
3401
+ cmp r0, #${pxtc.taggedNaN} ; NaN -> false
3402
+ beq .false
3403
+ lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++
3404
+ beq .boxed
3405
+ .true: ; remaining specials (e.g. true) are truthy
3406
+ movs r0, #1
3407
+ blx lr
3408
+ .false:
3409
+ movs r0, #0
3410
+ blx lr
3411
+ .boxed:
3412
+ ${this.callCPPPush("numops::toBool")}
3413
+
3414
+ ; Same truthiness classification as _numops_toBool (see comments above), but
3415
+ ; this variant also consumes (ref-decrements) its argument. Only heap objects
3416
+ ; are ref-counted, and those go through the C++ fallback which does the decr;
3417
+ ; the inline non-pointer paths carry no refcount, so they need none.
3418
+ _numops_toBoolDecr:
3419
+ @scope _numops_toBoolDecr
3420
+ cmp r0, #0 ; undefined (0) -> false
3421
+ beq .false
3422
+ cmp r0, #1 ; integer 0 ((0<<1)|1) -> false
3423
+ beq .false
3424
+ lsls r1, r0, #31 ; odd => nonzero tagged int -> true
3425
+ bne .true
3426
+ cmp r0, #${pxtc.taggedNull} ; null -> false
3427
+ beq .false
3428
+ cmp r0, #${pxtc.taggedFalse} ; false -> false
3429
+ beq .false
3430
+ cmp r0, #${pxtc.taggedNaN} ; NaN -> false
3431
+ beq .false
3432
+ lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++
3433
+ beq .boxed
3434
+ .true:
3435
+ movs r0, #1
3436
+ blx lr
3437
+ .false:
3438
+ movs r0, #0
3439
+ blx lr
3440
+ .boxed:
3441
+ ${this.callCPPPush("numops::toBoolDecr")}
3179
3442
  `;
3180
3443
  for (let op of Object.keys(pxtc.thumbCmpMap)) {
3181
3444
  op = op.replace(/.*::/, "");
@@ -3200,9 +3463,10 @@ _cmp_${op}:
3200
3463
  // the cmp isn't really needed, given how toBoolDecr() is compiled,
3201
3464
  // but better not rely on it
3202
3465
  // Also, cmp isn't needed when ref-counting (it ends with movs r0, r4)
3466
+ const toBoolDecr = boolLowerEnabled() ? "_numops_toBoolDecr" : "numops::toBoolDecr";
3203
3467
  r += boxedOp(`
3204
3468
  bl numops::${op}
3205
- bl numops::toBoolDecr
3469
+ bl ${toBoolDecr}
3206
3470
  cmp r0, #0`);
3207
3471
  }
3208
3472
  // switch(x){case k:} compiles to equality tests via pxt::switch_eq.
@@ -8648,6 +8912,7 @@ var ts;
8648
8912
  this.classInfo = null;
8649
8913
  this.perfCounterName = null;
8650
8914
  this.perfCounterNo = 0;
8915
+ this.useExactIfaceWrapper = false;
8651
8916
  this.body = [];
8652
8917
  this.lblNo = 0;
8653
8918
  this.action = null;
@@ -8665,6 +8930,11 @@ var ts;
8665
8930
  isGetter() {
8666
8931
  return this.action && this.action.kind == ts.SyntaxKind.GetAccessor;
8667
8932
  }
8933
+ // Returns the proc's "_args" entry label -- the full arg-shuffling
8934
+ // wrapper. Use this for any vtable-style reference where the caller
8935
+ // cannot guarantee the wrapper-skip preconditions (numargs >= proc.args.length).
8936
+ // For iface-table entries that CAN accept the optimization, the consumer
8937
+ // (hexfile.ts) inspects useExactIfaceWrapper directly and picks _iface instead.
8668
8938
  vtLabel() {
8669
8939
  return this.label() + (pxtc.isStackMachine() ? "" : "_args");
8670
8940
  }
@@ -9207,6 +9477,11 @@ var ts;
9207
9477
  argsFmt: ["T", "T", "S", "T"],
9208
9478
  value: 0
9209
9479
  },
9480
+ "pxtrt::mapSetByStringOnly": {
9481
+ name: "_pxt_map_set_by_string",
9482
+ argsFmt: ["T", "T", "S", "T"],
9483
+ value: 0
9484
+ },
9210
9485
  };
9211
9486
  let EK = pxtc.ir.EK;
9212
9487
  pxtc.SK = ts.SyntaxKind;
@@ -9940,6 +10215,87 @@ var ts;
9940
10215
  bin.breakpoints = opts.breakpoints;
9941
10216
  bin.name = opts.name;
9942
10217
  bin.target = opts.target;
10218
+ function ifaceCallKindCounts(ifaceIndex, getset) {
10219
+ const suffix = ":" + (getset || "call");
10220
+ return Object.keys(bin.ifaceCallCounts)
10221
+ .filter(k => pxtc.U.startsWith(k, ifaceIndex + ":") && pxtc.U.endsWith(k, suffix))
10222
+ .map(k => ({
10223
+ numargs: parseInt(k.split(":")[1]),
10224
+ count: bin.ifaceCallCounts[k]
10225
+ }));
10226
+ }
10227
+ // Decides whether `proc` (an iface-dispatched method) can safely skip the
10228
+ // arg-shuffling _args wrapper and have its iface vtable entry point directly
10229
+ // at _nochk. Safe iff every call site that targets this (ifaceIndex, getset)
10230
+ // bucket passes at least `proc.args.length` arguments -- that's exactly the
10231
+ // condition the wrapper would short-circuit on (`cmp r4, #numargs; bge _nochk`).
10232
+ //
10233
+ // Notes:
10234
+ // - The counts are a per-(ifaceIndex, getset) projection, not per-proc. In a
10235
+ // polymorphic iface bucket each proc is evaluated independently; eligibility
10236
+ // ends up cascading to the proc with the largest args.length in the bucket.
10237
+ // - toString is excluded because hexfile.ts:~841 wires toString's vtable
10238
+ // slot to `vtLabel()` (i.e. _args). The fixed-slot dispatch through that
10239
+ // vtable entry has no arg-count guarantees, so the wrapper is required.
10240
+ // Skipping it for a toString proc would dangle the _args reference.
10241
+ // Any future fixed-slot vtable consumer that uses vtLabel() must add a
10242
+ // matching exclusion here.
10243
+ // - dynamicIfaceCalls disqualifies any ifaceIndex ever dispatched without a
10244
+ // known decl (line ~2503). Those callers don't go through the counted path.
10245
+ // - The "no get calls in a non-get bucket" check is conservative: a "get"
10246
+ // syntax call has args.length == 1 which could under-feed a multi-arg call
10247
+ // target sharing the bucket.
10248
+ function canUseExactIfaceWrapper(proc, ifaceIndex, getset) {
10249
+ if (!isThumb() || !proc || !proc.info.usedAsIface || proc.args.length == 0)
10250
+ return false;
10251
+ if (proc.action && isToString(proc.action))
10252
+ return false;
10253
+ if (bin.dynamicIfaceCalls[ifaceIndex + ""])
10254
+ return false;
10255
+ if (!getset && ifaceCallKindCounts(ifaceIndex, "get").length)
10256
+ return false;
10257
+ const counts = ifaceCallKindCounts(ifaceIndex, getset);
10258
+ return counts.every(c => c.numargs >= proc.args.length);
10259
+ }
10260
+ // Must run AFTER the final-pass emit(rootFunction) has populated
10261
+ // bin.ifaceCallCounts and bin.dynamicIfaceCalls, and BEFORE
10262
+ // hexfile.ts emits iface vtables (which read useExactIfaceWrapper to
10263
+ // pick _iface vs _args per entry) and backbase.ts emits proc wrappers
10264
+ // (which read useExactIfaceWrapper to decide whether to emit _iface).
10265
+ function markExactIfaceWrappers() {
10266
+ bin.procs.forEach(p => p.useExactIfaceWrapper = false);
10267
+ if (!isThumb() || pxtc.target.switches.noIfaceSpec)
10268
+ return;
10269
+ const eligible = {};
10270
+ const seen = {};
10271
+ const note = (proc, ifaceIndex, getset) => {
10272
+ if (!proc || proc.args.length == 0)
10273
+ return;
10274
+ const key = proc.seqNo + "";
10275
+ seen[key] = true;
10276
+ if (eligible[key] === undefined)
10277
+ eligible[key] = true;
10278
+ if (!canUseExactIfaceWrapper(proc, ifaceIndex, getset))
10279
+ eligible[key] = false;
10280
+ };
10281
+ for (const info of bin.usedClassInfos) {
10282
+ if (!info.itable)
10283
+ continue;
10284
+ for (const entry of info.itable) {
10285
+ if (entry.proc)
10286
+ note(entry.proc, entry.idx, entry.proc.isGetter() ? "get" : "");
10287
+ if (entry.setProc)
10288
+ note(entry.setProc, entry.idx, "set");
10289
+ }
10290
+ }
10291
+ for (const proc of bin.procs) {
10292
+ const key = proc.seqNo + "";
10293
+ proc.useExactIfaceWrapper = !!seen[key] && !!eligible[key];
10294
+ }
10295
+ }
10296
+ function emitEscapedExpression(expr, reason) {
10297
+ return emitExpr(expr);
10298
+ }
9943
10299
  function reset() {
9944
10300
  bin.reset();
9945
10301
  proc = null;
@@ -10020,7 +10376,13 @@ var ts;
10020
10376
  reset();
10021
10377
  needsUsingInfo = false;
10022
10378
  bin.finalPass = true;
10379
+ bin.ifaceCallCounts = {};
10380
+ bin.mapSetByFieldIdCounts = {};
10381
+ bin.checkedFieldAccessCounts = {};
10382
+ bin.dynamicIfaceCalls = {};
10383
+ bin.usesMapSetByStringOnly = false;
10023
10384
  emit(rootFunction);
10385
+ markExactIfaceWrappers();
10024
10386
  pxtc.U.assert(usedWorkList.length == 0);
10025
10387
  res.configData = [];
10026
10388
  for (let k of Object.keys(configEntries)) {
@@ -10710,7 +11072,7 @@ ${lbl}: .short 0xffff
10710
11072
  let coll = pxtc.ir.shared(pxtc.ir.rtcall("Array_::mk", []));
10711
11073
  for (let elt of node.elements) {
10712
11074
  let mask = isRefCountedExpr(elt) ? (1 << 1) : 0;
10713
- proc.emitExpr(pxtc.ir.rtcall("Array_::push", [coll, emitExpr(elt)], mask));
11075
+ proc.emitExpr(pxtc.ir.rtcall("Array_::push", [coll, emitEscapedExpression(elt, "arrayElement")], mask));
10714
11076
  }
10715
11077
  if (node.elements.length > 0)
10716
11078
  // Make sure there is at least one use of the array, so it doesn't get GC-ed away in the last push.
@@ -10752,8 +11114,11 @@ ${lbl}: .short 0xffff
10752
11114
  if (isRefCountedExpr(p.initializer))
10753
11115
  mask |= 1 << 2;
10754
11116
  }
11117
+ const nativeFieldId = pxtc.target.isNative ? getIfaceMemberId(keyName) : undefined;
11118
+ if (nativeFieldId !== undefined)
11119
+ bin.mapSetByFieldIdCounts[nativeFieldId + ""] = (bin.mapSetByFieldIdCounts[nativeFieldId + ""] || 0) + 1;
10755
11120
  const fieldId = pxtc.target.isNative
10756
- ? pxtc.ir.numlit(getIfaceMemberId(keyName))
11121
+ ? pxtc.ir.numlit(nativeFieldId)
10757
11122
  : pxtc.ir.ptrlit(null, JSON.stringify(keyName));
10758
11123
  const args = [
10759
11124
  expr,
@@ -10813,6 +11178,9 @@ ${lbl}: .short 0xffff
10813
11178
  return emitShim(fld, decl, [node.expression]);
10814
11179
  }
10815
11180
  else {
11181
+ if (idx.needsCheck && !pxtc.target.switches.skipClassCheck) {
11182
+ bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] = (bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] || 0) + 1;
11183
+ }
10816
11184
  return pxtc.ir.op(EK.FieldAccess, [emitExpr(node.expression)], idx);
10817
11185
  }
10818
11186
  }
@@ -10866,7 +11234,17 @@ ${lbl}: .short 0xffff
10866
11234
  indexer = assign ? attrs.indexerSet : attrs.indexerGet;
10867
11235
  }
10868
11236
  if (!indexer && (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.StructuredOrTypeVariable))) {
10869
- indexer = assign ? "pxtrt::mapSetGeneric" : "pxtrt::mapGetGeneric";
11237
+ const hasIndexSignature = !!checker.getIndexTypeOfType(t, ts.IndexKind.String) || !!checker.getIndexTypeOfType(t, ts.IndexKind.Number);
11238
+ // Native-only: mapSetByStringOnly resolves to the _pxt_map_set_by_string
11239
+ // thumb helper. The JS backend has no such runtime entry, so the generic
11240
+ // setter is the only valid choice there.
11241
+ const mapOnlySet = !pxtc.target.switches.noIfaceSpec && pxtc.target.isNative
11242
+ && !!assign && !(t.flags & ts.TypeFlags.Any) && hasIndexSignature;
11243
+ if (mapOnlySet)
11244
+ bin.usesMapSetByStringOnly = true;
11245
+ indexer = assign
11246
+ ? (mapOnlySet ? "pxtrt::mapSetByStringOnly" : "pxtrt::mapSetGeneric")
11247
+ : "pxtrt::mapGetGeneric";
10870
11248
  stringOk = true;
10871
11249
  }
10872
11250
  if (indexer) {
@@ -11203,7 +11581,7 @@ ${lbl}: .short 0xffff
11203
11581
  lst.push(tracked);
11204
11582
  }
11205
11583
  function emitPlain() {
11206
- let r = mkProcCall(decl, node, args.map((x) => emitExpr(x)));
11584
+ let r = mkProcCall(decl, node, args.map(x => emitEscapedExpression(x, "callArgument")));
11207
11585
  let pp = r.data;
11208
11586
  if (args[0] && pp.proc && pp.proc.classInfo)
11209
11587
  pp.isThis = args[0].kind == pxtc.SK.ThisKeyword;
@@ -11230,7 +11608,7 @@ ${lbl}: .short 0xffff
11230
11608
  baseCtor = p.ctor;
11231
11609
  if (!baseCtor && bin.finalPass)
11232
11610
  throw userError(9280, lf("super() call requires an explicit constructor in base class"));
11233
- let ctorArgs = args.map((x) => emitExpr(x));
11611
+ let ctorArgs = args.map((x) => emitEscapedExpression(x, "callArgument"));
11234
11612
  ctorArgs.unshift(emitThis(funcExpr));
11235
11613
  return mkProcCallCore(baseCtor, node, ctorArgs);
11236
11614
  }
@@ -11246,9 +11624,11 @@ ${lbl}: .short 0xffff
11246
11624
  // TODO in VT accessor/field/method -> different
11247
11625
  pxtc.U.assert(funcExpr.kind == pxtc.SK.PropertyAccessExpression);
11248
11626
  const fieldName = funcExpr.name.text;
11627
+ const ifaceIndex = getIfaceMemberId(fieldName, true);
11628
+ bin.dynamicIfaceCalls[ifaceIndex + ""] = true;
11249
11629
  // completely dynamic dispatch
11250
- return mkMethodCall(args.map((x) => emitExpr(x)), {
11251
- ifaceIndex: getIfaceMemberId(fieldName, true),
11630
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
11631
+ ifaceIndex,
11252
11632
  callLocationIndex: markCallLocation(node),
11253
11633
  noArgs
11254
11634
  });
@@ -11272,7 +11652,7 @@ ${lbl}: .short 0xffff
11272
11652
  pxtc.U.assert(false);
11273
11653
  }
11274
11654
  pxtc.U.assert(!bin.finalPass || info.virtualIndex != null, "!bin.finalPass || info.virtualIndex != null");
11275
- return mkMethodCall(args.map((x) => emitExpr(x)), {
11655
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
11276
11656
  classInfo: info.parentClassInfo,
11277
11657
  virtualIndex: info.virtualIndex,
11278
11658
  noArgs,
@@ -11312,9 +11692,15 @@ ${lbl}: .short 0xffff
11312
11692
  return emitPlain();
11313
11693
  }
11314
11694
  else if (needsVCall || pxtc.target.switches.slowMethods || !forceMethod) {
11315
- return mkMethodCall(args.map((x) => emitExpr(x)), {
11316
- ifaceIndex: getIfaceMemberId(getName(decl), true),
11317
- isSet: noArgs && args.length == 2,
11695
+ const ifaceIndex = getIfaceMemberId(getName(decl), true);
11696
+ const isSet = noArgs && args.length == 2;
11697
+ const callKind = isSet ? "set" : noArgs ? "get" : "call";
11698
+ const helperGetSet = callKind == "call" ? "" : callKind;
11699
+ const countKey = `${ifaceIndex}:${args.length}:${helperGetSet || "call"}`;
11700
+ bin.ifaceCallCounts[countKey] = (bin.ifaceCallCounts[countKey] || 0) + 1;
11701
+ return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), {
11702
+ ifaceIndex,
11703
+ isSet,
11318
11704
  callLocationIndex: markCallLocation(node),
11319
11705
  noArgs
11320
11706
  });
@@ -11333,7 +11719,7 @@ ${lbl}: .short 0xffff
11333
11719
  // here's where we will recurse to generate funcExpr
11334
11720
  args.unshift(funcExpr);
11335
11721
  pxtc.U.assert(!noArgs);
11336
- return mkMethodCall(args.map(x => emitExpr(x)), {
11722
+ return mkMethodCall(args.map(x => emitEscapedExpression(x, "callArgument")), {
11337
11723
  virtualIndex: -1,
11338
11724
  callLocationIndex: markCallLocation(node),
11339
11725
  noArgs
@@ -11429,6 +11815,8 @@ ${lbl}: .short 0xffff
11429
11815
  }
11430
11816
  }
11431
11817
  function getCtor(decl) {
11818
+ if (!decl.members)
11819
+ return null;
11432
11820
  return decl.members.filter(m => m.kind == pxtc.SK.Constructor)[0];
11433
11821
  }
11434
11822
  function isIfaceMemberUsed(name) {
@@ -11476,7 +11864,7 @@ ${lbl}: .short 0xffff
11476
11864
  // let sig = checker.getResolvedSignature(node)
11477
11865
  // TODO: can we have overloeads?
11478
11866
  addDefaultParametersAndTypeCheck(checker.getResolvedSignature(node), args, ctorAttrs);
11479
- let compiled = args.map((x) => emitExpr(x));
11867
+ let compiled = args.map((x) => emitEscapedExpression(x, "callArgument"));
11480
11868
  if (ctorAttrs.shim) {
11481
11869
  // TODO need to deal with refMask and tagged ints here
11482
11870
  // we drop 'obj' variable
@@ -11865,7 +12253,7 @@ ${lbl}: .short 0xffff
11865
12253
  for (let f of initalizedFields) {
11866
12254
  let idx = fieldIndexCore(proc.classInfo, getFieldInfo(proc.classInfo, getName(f)), false);
11867
12255
  let trg2 = pxtc.ir.op(EK.FieldAccess, [emitLocalLoad(info.thisParameter)], idx);
11868
- proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitExpr(f.initializer)]));
12256
+ proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitEscapedExpression(f.initializer, "initializer")]));
11869
12257
  }
11870
12258
  flushHoistedFunctionDefinitions();
11871
12259
  if (node.body.kind == pxtc.SK.Block) {
@@ -12083,9 +12471,12 @@ ${lbl}: .short 0xffff
12083
12471
  isRef: true,
12084
12472
  shimName: attrs.shim,
12085
12473
  classInfo: info,
12086
- needsCheck
12474
+ needsCheck,
12087
12475
  };
12088
12476
  }
12477
+ function checkedFieldAccessCountKey(info) {
12478
+ return `${info.classInfo.id}:${info.name}:get`;
12479
+ }
12089
12480
  function fieldIndex(pacc) {
12090
12481
  const tp = typeOf(pacc.expression);
12091
12482
  if (isPossiblyGenericClassType(tp)) {
@@ -12116,7 +12507,7 @@ ${lbl}: .short 0xffff
12116
12507
  if (decl && (isGlobal || isVar(decl) || isParameter(decl))) {
12117
12508
  let l = lookupCell(decl);
12118
12509
  recordUse(decl, true);
12119
- proc.emitExpr(l.storeByRef(emitExpr(src)));
12510
+ proc.emitExpr(l.storeByRef(emitEscapedExpression(src, "assignment")));
12120
12511
  }
12121
12512
  else {
12122
12513
  unhandled(trg, lf("bad target identifier"), 9248);
@@ -12132,7 +12523,10 @@ ${lbl}: .short 0xffff
12132
12523
  }
12133
12524
  proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
12134
12525
  }
12135
- else if (decl && (decl.kind == pxtc.SK.PropertySignature || decl.kind == pxtc.SK.PropertyAssignment || isSlowField(decl))) {
12526
+ else if (decl && (decl.kind == pxtc.SK.PropertySignature || decl.kind == pxtc.SK.PropertyAssignment)) {
12527
+ proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
12528
+ }
12529
+ else if (decl && isSlowField(decl)) {
12136
12530
  proc.emitExpr(emitCallCore(trg, trg, [src], null, decl));
12137
12531
  }
12138
12532
  else {
@@ -12148,7 +12542,7 @@ ${lbl}: .short 0xffff
12148
12542
  }
12149
12543
  }
12150
12544
  let trg2 = emitExpr(trg);
12151
- proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitExpr(src)]));
12545
+ proc.emitExpr(pxtc.ir.op(EK.Store, [trg2, emitEscapedExpression(src, "assignment")]));
12152
12546
  break;
12153
12547
  }
12154
12548
  }
@@ -12559,7 +12953,7 @@ ${lbl}: .short 0xffff
12559
12953
  let mask = getMask(args);
12560
12954
  let convInfos = [];
12561
12955
  let args2 = args.map((a, i) => {
12562
- let r = emitExpr(a);
12956
+ let r = emitEscapedExpression(a, "runtimeCallArgument");
12563
12957
  if (!needsNumberConversions())
12564
12958
  return r;
12565
12959
  let f = fmt[i + 1];
@@ -12897,6 +13291,43 @@ ${lbl}: .short 0xffff
12897
13291
  emitExprAsStmt(node.expression);
12898
13292
  }
12899
13293
  function emitCondition(expr, inner = null) {
13294
+ // with `noBoolLower` flag, the generic paths below handle every condition
13295
+ const boolLower = !pxtc.target.switches.noBoolLower;
13296
+ // Lower `&&` / `||` used as a condition into short-circuiting jumps
13297
+ // that yield 0/1 directly, instead of boxing the intermediate
13298
+ // booleans and converting them back with a runtime helper.
13299
+ if (boolLower && !inner && !isStackMachine() && expr.kind == pxtc.SK.BinaryExpression) {
13300
+ const binary = expr;
13301
+ const op = binary.operatorToken.kind;
13302
+ if (op == pxtc.SK.AmpersandAmpersandToken || op == pxtc.SK.BarBarToken) {
13303
+ const shortCircuitLabel = proc.mkLabel("lazycond");
13304
+ const doneLabel = proc.mkLabel("lazycondfin");
13305
+ if (op == pxtc.SK.AmpersandAmpersandToken) {
13306
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.left), pxtc.ir.JmpMode.IfZero);
13307
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.right), pxtc.ir.JmpMode.IfZero);
13308
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(1), pxtc.ir.JmpMode.Always);
13309
+ proc.emitLbl(shortCircuitLabel);
13310
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(0), pxtc.ir.JmpMode.Always);
13311
+ }
13312
+ else {
13313
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.left), pxtc.ir.JmpMode.IfNotZero);
13314
+ proc.emitJmp(shortCircuitLabel, emitCondition(binary.right), pxtc.ir.JmpMode.IfNotZero);
13315
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(0), pxtc.ir.JmpMode.Always);
13316
+ proc.emitLbl(shortCircuitLabel);
13317
+ proc.emitJmp(doneLabel, pxtc.ir.numlit(1), pxtc.ir.JmpMode.Always);
13318
+ }
13319
+ proc.emitLbl(doneLabel);
13320
+ return captureJmpValue();
13321
+ }
13322
+ }
13323
+ // Lower `!expr` used as a condition by negating the operand's
13324
+ // condition directly, avoiding a boxed boolean just to invert it.
13325
+ if (boolLower && !inner && !isStackMachine() && expr.kind == pxtc.SK.PrefixUnaryExpression) {
13326
+ const unary = expr;
13327
+ if (unary.operator == pxtc.SK.ExclamationToken) {
13328
+ return pxtc.ir.rtcall("Boolean_::bang", [emitCondition(unary.operand)]);
13329
+ }
13330
+ }
12900
13331
  if (!inner && isThumb() && expr.kind == pxtc.SK.BinaryExpression) {
12901
13332
  let be = expr;
12902
13333
  let mapped = pxtc.U.lookup(pxtc.thumbCmpMap, simpleInstruction(be, be.operatorToken.kind));
@@ -13348,7 +13779,10 @@ ${lbl}: .short 0xffff
13348
13779
  }
13349
13780
  }
13350
13781
  typeCheckSubtoSup(node.initializer, node);
13351
- proc.emitExpr(loc.storeByRef(emitExpr(node.initializer)));
13782
+ const initializerExpr = isGlobalVar(node)
13783
+ ? emitEscapedExpression(node.initializer, "initializer")
13784
+ : emitExpr(node.initializer);
13785
+ proc.emitExpr(loc.storeByRef(initializerExpr));
13352
13786
  currJres = null;
13353
13787
  proc.stackEmpty();
13354
13788
  }
@@ -13373,8 +13807,17 @@ ${lbl}: .short 0xffff
13373
13807
  exres = pxtc.ir.op(EK.FieldAccess, [objRef], fieldIndexCore(info, getFieldInfo(info, fieldName)));
13374
13808
  }
13375
13809
  else {
13810
+ // Dynamic field access on a non-class-typed receiver (any, structural type,
13811
+ // etc). The call site supplies exactly 1 arg (the receiver) but the target
13812
+ // proc resolved via iface dispatch may be a method that expects more --
13813
+ // the _args wrapper is what normally fills in the missing args. Flag this
13814
+ // ifaceIndex so canUseExactIfaceWrapper disqualifies the wrapper-skip
13815
+ // optimization for any proc reachable here. Mirrors the dynamic-method-call
13816
+ // path above.
13817
+ const ifaceIndex = getIfaceMemberId(fieldName, true);
13818
+ bin.dynamicIfaceCalls[ifaceIndex + ""] = true;
13376
13819
  exres = mkMethodCall([objRef], {
13377
- ifaceIndex: getIfaceMemberId(fieldName, true),
13820
+ ifaceIndex,
13378
13821
  callLocationIndex: markCallLocation(node),
13379
13822
  noArgs: true
13380
13823
  });
@@ -13757,6 +14200,11 @@ ${lbl}: .short 0xffff
13757
14200
  this.usedChars = new Uint32Array(0x10000 / 32);
13758
14201
  this.explicitlyUsedIfaceMembers = {};
13759
14202
  this.ifaceMemberMap = {};
14203
+ this.ifaceCallCounts = {};
14204
+ this.mapSetByFieldIdCounts = {};
14205
+ this.checkedFieldAccessCounts = {};
14206
+ this.dynamicIfaceCalls = {};
14207
+ this.usesMapSetByStringOnly = false;
13760
14208
  this.strings = {};
13761
14209
  this.hexlits = {};
13762
14210
  this.doubles = {};
@@ -16207,9 +16655,9 @@ ${lbl}: ${snippets.obj_header("pxt::number_vt")}
16207
16655
  uint32_t ifaceHashMult;
16208
16656
  PVoid methods[2 or 4];
16209
16657
  */
16210
- const ifaceInfo = computeHashMultiplier(info.itable.map(e => e.idx));
16211
- //if (info.itable.length == 0)
16212
- // ifaceInfo.mult = 0
16658
+ const ifaceInfo = info.itable.length
16659
+ ? computeHashMultiplier(info.itable.map(e => e.idx))
16660
+ : { mult: 0, mapping: new Uint16Array(0), size: 0 };
16213
16661
  let ptrSz = pxtc.target.shortPointers ? ".short" : ".word";
16214
16662
  let s = `
16215
16663
  .object ${info.id}_VT
@@ -16243,6 +16691,11 @@ ${info.id}_IfaceVT:
16243
16691
  `;
16244
16692
  const descSize = 8;
16245
16693
  const zeroOffset = ifaceInfo.mapping.length * 2;
16694
+ // Iface table is the one consumer that can elect the wrapper-skip
16695
+ // optimization. Procs with useExactIfaceWrapper get pointed at _iface
16696
+ // (which `b _nochk`); everyone else gets the usual _args wrapper entry.
16697
+ // canUseExactIfaceWrapper guarantees this is safe for the elected procs.
16698
+ const procIfaceLabel = (p) => (p.useExactIfaceWrapper ? p.label() + "_iface" : p.vtLabel()) + "@fn";
16246
16699
  let descs = "";
16247
16700
  let offset = zeroOffset;
16248
16701
  let offsets = {};
@@ -16250,11 +16703,11 @@ ${info.id}_IfaceVT:
16250
16703
  offsets[e.idx + ""] = offset;
16251
16704
  const desc = !e.proc ? 0 : e.proc.isGetter() ? 1 : 2;
16252
16705
  descs += ` .short ${e.idx}, ${desc} ; ${e.name}\n`;
16253
- descs += ` .word ${e.proc ? e.proc.vtLabel() + "@fn" : e.info}\n`;
16706
+ descs += ` .word ${e.proc ? procIfaceLabel(e.proc) : e.info}\n`;
16254
16707
  offset += descSize;
16255
16708
  if (e.setProc) {
16256
16709
  descs += ` .short ${e.idx}, 0 ; set ${e.name}\n`;
16257
- descs += ` .word ${e.setProc.vtLabel()}@fn\n`;
16710
+ descs += ` .word ${procIfaceLabel(e.setProc)}\n`;
16258
16711
  offset += descSize;
16259
16712
  }
16260
16713
  }
@@ -16267,7 +16720,9 @@ ${info.id}_IfaceVT:
16267
16720
  bin.itFullEntries++;
16268
16721
  }
16269
16722
  // offsets are relative to the position in the array
16270
- s += " .short " + pxtc.U.toArray(map).map((e, i) => (offsets[e + ""] || zeroOffset) - (i * 2)).join(", ") + "\n";
16723
+ const hashOffsets = pxtc.U.toArray(map).map((e, i) => (offsets[e + ""] || zeroOffset) - (i * 2));
16724
+ if (hashOffsets.length)
16725
+ s += " .short " + hashOffsets.join(", ") + "\n";
16271
16726
  s += descs;
16272
16727
  s += "\n";
16273
16728
  return s;