From 029fd54e464fa041511db80a98f66282a134a1b5 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Fri, 29 May 2026 17:18:39 -0700 Subject: [PATCH] Specialize thumb interface dispatch: skip arg-shuffling wrapper when safe, share dispatch thunks, fast-path string-map set --- pxtcompiler/emitter/backbase.ts | 234 +++++++++++++++++++++++++++----- pxtcompiler/emitter/emitter.ts | 192 +++++++++++++++++++++++--- pxtcompiler/emitter/hexfile.ts | 21 ++- pxtcompiler/emitter/ir.ts | 6 + 4 files changed, 390 insertions(+), 63 deletions(-) diff --git a/pxtcompiler/emitter/backbase.ts b/pxtcompiler/emitter/backbase.ts index a4bba8fb7bbd..5e2c414f98b4 100644 --- a/pxtcompiler/emitter/backbase.ts +++ b/pxtcompiler/emitter/backbase.ts @@ -576,6 +576,11 @@ ${baseLabel}_nochk: private emitFieldAccess(e: ir.Expr, store = false) { let info = e.data as FieldAccessInfo + if (!store && this.shouldSpecializeCheckedFieldLoad(info)) { + this.emitCheckedFieldLoad(info) + return + } + let pref = store ? "st" : "ld" let lbl = pref + "fld_" + info.classInfo.id + "_" + info.name if (info.needsCheck && !target.switches.skipClassCheck) { @@ -596,6 +601,33 @@ ${baseLabel}_nochk: return } + private checkedFieldAccessCountKey(info: FieldAccessInfo) { + return `${info.classInfo.id}:${info.name}:get` + } + + private shouldSpecializeCheckedFieldLoad(info: FieldAccessInfo) { + if (!isThumb() || !this.bin.finalPass || target.switches.skipClassCheck || !info.needsCheck) + return false + return (this.bin.checkedFieldAccessCounts[this.checkedFieldAccessCountKey(info)] || 0) >= 5 + } + + private emitCheckedFieldLoad(info: FieldAccessInfo) { + const helperKey = "ldfldchk_" + info.classInfo.id + "_" + info.name + const helper = this.ensureLabelledHelper(helperKey, () => { + this.write(this.t.helper_prologue()) + this.emitInstanceOf(info.classInfo, "validate") + let off = info.idx * 4 + 4 + let xoff = "#" + off + if (off > 124) { + this.write(this.t.emit_int(off, "r3")) + xoff = "r3" + } + this.write(`ldr r0, [r0, ${xoff}]`) + this.write(this.t.helper_epilogue()) + }) + this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0))) + } + private writeFailBranch() { this.write(`.fail:`) this.write(`mov r1, lr`) @@ -839,13 +871,52 @@ ${baseLabel}_nochk: private emitIfaceCall(procid: ir.ProcId, numargs: number, getset = "") { U.assert(procid.ifaceIndex > 0) - this.write(this.t.emit_int(procid.ifaceIndex, "r1")) - - this.emitLabelledHelper("ifacecall" + numargs + "_" + getset, () => { + const helperKey = "ifacecall" + numargs + "_" + getset + const helper = this.ensureLabelledHelper(helperKey, () => { this.write(`ldr r0, [sp, #0] ; ld-this`) this.loadVTable() this.ifaceCallCore(numargs, getset) }) + + if (this.shouldSpecializeIfaceCall(procid.ifaceIndex, numargs, getset)) { + const thunkKey = helperKey + "_i" + procid.ifaceIndex + this.emitLabelledHelper(thunkKey, () => { + this.write(this.t.emit_int(procid.ifaceIndex, "r1")) + this.write(`ldlit r7, ${helper}@fn`) + this.write(`bx r7`) + }) + } else { + this.write(this.t.emit_int(procid.ifaceIndex, "r1")) + this.write(this.t.call_lbl(helper)) + } + } + + private ifaceCallCountKey(ifaceIndex: number, numargs: number, getset: string) { + return `${ifaceIndex}:${numargs}:${getset || "call"}` + } + + // Count-driven specialization decisions (this method and the related + // shouldSpecializeCheckedFieldLoad / shouldSpecializeMapSetByFieldId) assume: + // - bin.finalPass is true, AND + // - the count maps were populated during the same final-pass emit() + // call that builds the IR this asm walk consumes. + // This is currently true because the frontend populates counts during IR + // generation and the backend reads them only during the subsequent asm walk. + // Any future change that interleaves IR generation with asm emission would + // silently regress these to "always false". + // + // Threshold of 3 is the break-even point for the labelled-thunk pattern: + // each shared call site saves ~4 bytes vs an inline call, and the thunk + // itself costs ~12 bytes. At 3 callers, savings break even with thunk cost. + private shouldSpecializeIfaceCall(ifaceIndex: number, numargs: number, getset: string) { + if (!isThumb() || !this.bin.finalPass) + return false + const count = this.bin.ifaceCallCounts[this.ifaceCallCountKey(ifaceIndex, numargs, getset)] || 0 + if (getset == "get" && numargs == 1) + return count >= 3 + if (getset == "set" || !getset) + return count >= 3 + return false } // vtable in r3; clobber r2 @@ -951,6 +1022,8 @@ ${baseLabel}_nochk: private emitRtCall(topExpr: ir.Expr, genCall: () => void = null) { let name: string = topExpr.data + const mapSetFieldId = this.mapSetByFieldId(topExpr) + const specializeMapSetFieldId = mapSetFieldId !== undefined && this.shouldSpecializeMapSetByFieldId(mapSetFieldId) if (name == "pxt::beginTry") { return this.emitBeginTry(topExpr) @@ -965,6 +1038,8 @@ ${baseLabel}_nochk: isRef: (maskInfo.refMask & (1 << i)) != 0, conv: convs.find(c => c.argIdx == i) })) + if (specializeMapSetFieldId) + allArgs = allArgs.filter(a => a.idx != 1) U.assert(allArgs.length <= 4) @@ -1071,7 +1146,9 @@ ${baseLabel}_nochk: if (genCall) { genCall() } else { - if (name != "langsupp::ignore") + if (specializeMapSetFieldId) + this.emitMapSetByFieldIdCall(mapSetFieldId) + else if (name != "langsupp::ignore") this.alignedCall(name, "", 0, true) } @@ -1081,27 +1158,67 @@ ${baseLabel}_nochk: } } + private mapSetByFieldId(topExpr: ir.Expr) { + if (topExpr.data != "pxtrt::mapSet" || topExpr.args.length != 3) + return undefined + const fieldId = topExpr.args[1] + if (fieldId.exprKind != ir.EK.NumberLiteral || typeof fieldId.data != "number") + return undefined + return fieldId.data as number + } + + private shouldSpecializeMapSetByFieldId(fieldId: number) { + if (!isThumb() || !this.bin.finalPass) + return false + return (this.bin.mapSetByFieldIdCounts[fieldId + ""] || 0) >= 3 + } + + private emitMapSetByFieldIdCall(fieldId: number) { + const helper = this.ensureLabelledHelper("mapset_i" + fieldId, () => { + this.write(this.t.helper_prologue()) + this.write(this.t.emit_int(fieldId, "r1")) + this.write(this.t.callCPP("pxtrt::mapSet")) + this.write(this.t.helper_epilogue()) + }) + this.write(this.t.call_lbl(helper, false, this.stackAlignmentNeeded(0))) + } + private alignedCall(name: string, cmt = "", off = 0, saveStack = false) { if (U.startsWith(name, "_cmp_") || U.startsWith(name, "_pxt_")) saveStack = false this.write(this.t.call_lbl(name, saveStack, this.stackAlignmentNeeded(off)) + cmt) } - private emitLabelledHelper(lbl: string, generate: () => void) { + // Lazily emit a deduped helper body and return its label. + // Use this when you want to BRANCH to (or reference) the helper without + // emitting a `bl` at the current cursor -- e.g. when wrapping the call in + // a thunk, or storing the label for later use. + private ensureLabelledHelper(lbl: string, generate: () => void) { if (!this.labelledHelpers[lbl]) { let outp = this.redirectOutput(generate) - this.emitHelper(outp, lbl) - this.labelledHelpers[lbl] = this.bin.codeHelpers[outp]; - } else { - this.write(this.t.call_lbl(this.labelledHelpers[lbl])) + this.labelledHelpers[lbl] = this.helperLabel(outp, lbl); } + return this.labelledHelpers[lbl] } - private emitHelper(asm: string, baseName = "hlp") { + // Ensure a deduped helper exists and emit a `bl` to it at the cursor. + // Use this for the common case of "factor this sequence into a helper and call it here". + private emitLabelledHelper(lbl: string, generate: () => void) { + const helper = this.ensureLabelledHelper(lbl, generate) + this.write(this.t.call_lbl(helper)) + return helper + } + + private helperLabel(asm: string, baseName = "hlp") { if (!this.bin.codeHelpers[asm]) { let len = Object.keys(this.bin.codeHelpers).length this.bin.codeHelpers[asm] = `_${baseName}_${len}` } + return this.bin.codeHelpers[asm] + } + + private emitHelper(asm: string, baseName = "hlp") { + this.helperLabel(asm, baseName) this.write(this.t.call_lbl(this.bin.codeHelpers[asm])) } @@ -1144,43 +1261,42 @@ ${baseLabel}_nochk: } } - private emitFieldMethods() { - for (let op of ["get", "set"]) { - this.write(` + private emitFieldMethod(op: string) { + this.write(` ${this.helperObject(op)} .section code _pxt_map_${op}: `) - this.loadVTable("r4") - this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4") + this.loadVTable("r4") + this.checkSubtype(this.builtInClassNo(pxt.BuiltInType.RefMap), ".notmap", "r4") - this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString")) - this.write(".notmap:") + this.write(this.t.callCPPPush(op == "set" ? "pxtrt::mapSetByString" : "pxtrt::mapGetByString")) + this.write(".notmap:") - let numargs = op == "set" ? 2 : 1 - let hasAlign = false + let numargs = op == "set" ? 2 : 1 + let hasAlign = false - this.write("mov r4, r3 ; save VT") + this.write("mov r4, r3 ; save VT") - if (op == "set") { - if (target.stackAlign) { - hasAlign = true - this.write("push {lr} ; align") - } - this.write(` + if (op == "set") { + if (target.stackAlign) { + hasAlign = true + this.write("push {lr} ; align") + } + this.write(` push {r0, r2, lr} mov r0, r1 `) - } else { - this.write(` + } else { + this.write(` push {r0, lr} mov r0, r1 `) - } + } - this.write(` + this.write(` bl pxtrt::lookupMapKey mov r1, r0 ; put key index in r1 ldr r0, [sp, #0] ; restore obj pointer @@ -1189,13 +1305,47 @@ ${baseLabel}_nochk: `) - this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0))) + this.write(this.t.pop_locals(numargs + (hasAlign ? 1 : 0))) - this.write("pop {pc}") + this.write("pop {pc}") - this.write(".dowork:") - this.ifaceCallCore(numargs, op, true) - } + this.write(".dowork:") + this.ifaceCallCore(numargs, op, true) + } + + private emitStringMapSetMethod() { + // Fast path for the well-typed "map with string index signature" case. + // - tagged-int / null receivers: unrecoverable, panic with failedCast (matches the + // pre-optimization mapSetGeneric path, which also can't proceed on these). + // - non-RefMap pointer receivers: tail-call _pxt_map_set, which handles + // non-map objects via interface dispatch. Preserves back-compat for code that + // casts non-map objects to a string-index-signature type. + this.write(` + ${this.helperObject("set string map")} + .section code + _pxt_map_set_by_string: + lsls r4, r0, #30 + bne .badtype + cmp r0, #0 + beq .badtype + ldr r3, [r0, #0] + `) + + this.write(` + ldrh r4, [r3, #8] + cmp r4, #${pxt.BuiltInType.RefMap} + bne .fallback + `) + this.write(this.t.callCPPPush("pxtrt::mapSetByString")) + this.write(` + .fallback: + b _pxt_map_set + .badtype: + mov r1, lr + mov r7, sp + str r7, [r6, #4] + bl pxt::failedCast + `) } private emitArrayMethod(op: string, isBuffer: boolean) { @@ -1292,6 +1442,13 @@ ${baseLabel}_nochk: } } + private emitFieldMethods() { + for (let op of ["get", "set"]) { + this.emitFieldMethod(op) + } + this.emitStringMapSetMethod() + } + private emitLambdaTrampoline() { let r3 = target.stackAlign ? "r3," : "" this.write(` @@ -1659,6 +1816,13 @@ ${baseLabel}_nochk: this.write(`.word ${this.proc.label()}_args@fn`) } + if (this.proc.useExactIfaceWrapper) { + this.write(`${this.proc.label()}_iface:`) + this.write(`b ${this.proc.label()}_nochk`) + if (!this.proc.info.usedAsValue) + return + } + this.write(`${this.proc.label()}_args:`) let numargs = this.proc.args.length diff --git a/pxtcompiler/emitter/emitter.ts b/pxtcompiler/emitter/emitter.ts index 461125dda37f..c52c7cee5e9d 100644 --- a/pxtcompiler/emitter/emitter.ts +++ b/pxtcompiler/emitter/emitter.ts @@ -178,6 +178,11 @@ namespace ts.pxtc { argsFmt: ["T", "T", "S", "T"], value: 0 }, + "pxtrt::mapSetByStringOnly": { + name: "_pxt_map_set_by_string", + argsFmt: ["T", "T", "S", "T"], + value: 0 + }, } let EK = ir.EK; @@ -1016,6 +1021,95 @@ namespace ts.pxtc { bin.name = opts.name; bin.target = opts.target; + function ifaceCallKindCounts(ifaceIndex: number, getset: string) { + const suffix = ":" + (getset || "call"); + return Object.keys(bin.ifaceCallCounts) + .filter(k => U.startsWith(k, ifaceIndex + ":") && U.endsWith(k, suffix)) + .map(k => ({ + numargs: parseInt(k.split(":")[1]), + count: bin.ifaceCallCounts[k] + })); + } + + // Decides whether `proc` (an iface-dispatched method) can safely skip the + // arg-shuffling _args wrapper and have its iface vtable entry point directly + // at _nochk. Safe iff every call site that targets this (ifaceIndex, getset) + // bucket passes at least `proc.args.length` arguments -- that's exactly the + // condition the wrapper would short-circuit on (`cmp r4, #numargs; bge _nochk`). + // + // Notes: + // - The counts are a per-(ifaceIndex, getset) projection, not per-proc. In a + // polymorphic iface bucket each proc is evaluated independently; eligibility + // ends up cascading to the proc with the largest args.length in the bucket. + // - toString is excluded because hexfile.ts:~841 wires toString's vtable + // slot to `vtLabel()` (i.e. _args). The fixed-slot dispatch through that + // vtable entry has no arg-count guarantees, so the wrapper is required. + // Skipping it for a toString proc would dangle the _args reference. + // Any future fixed-slot vtable consumer that uses vtLabel() must add a + // matching exclusion here. + // - dynamicIfaceCalls disqualifies any ifaceIndex ever dispatched without a + // known decl (line ~2503). Those callers don't go through the counted path. + // - The "no get calls in a non-get bucket" check is conservative: a "get" + // syntax call has args.length == 1 which could under-feed a multi-arg call + // target sharing the bucket. + function canUseExactIfaceWrapper(proc: ir.Procedure, ifaceIndex: number, getset: string) { + if (!isThumb() || !proc || !proc.info.usedAsIface || proc.args.length == 0) + return false; + if (proc.action && isToString(proc.action)) + return false; + if (bin.dynamicIfaceCalls[ifaceIndex + ""]) + return false; + if (!getset && ifaceCallKindCounts(ifaceIndex, "get").length) + return false; + + const counts = ifaceCallKindCounts(ifaceIndex, getset); + return counts.every(c => c.numargs >= proc.args.length); + } + + // Must run AFTER the final-pass emit(rootFunction) has populated + // bin.ifaceCallCounts and bin.dynamicIfaceCalls, and BEFORE + // hexfile.ts emits iface vtables (which read useExactIfaceWrapper to + // pick _iface vs _args per entry) and backbase.ts emits proc wrappers + // (which read useExactIfaceWrapper to decide whether to emit _iface). + function markExactIfaceWrappers() { + bin.procs.forEach(p => p.useExactIfaceWrapper = false); + if (!isThumb()) + return; + + const eligible: pxt.Map = {}; + const seen: pxt.Map = {}; + const note = (proc: ir.Procedure, ifaceIndex: number, getset: string) => { + if (!proc || proc.args.length == 0) + return; + const key = proc.seqNo + ""; + seen[key] = true; + if (eligible[key] === undefined) + eligible[key] = true; + if (!canUseExactIfaceWrapper(proc, ifaceIndex, getset)) + eligible[key] = false; + }; + + for (const info of bin.usedClassInfos) { + if (!info.itable) + continue; + for (const entry of info.itable) { + if (entry.proc) + note(entry.proc, entry.idx, entry.proc.isGetter() ? "get" : ""); + if (entry.setProc) + note(entry.setProc, entry.idx, "set"); + } + } + + for (const proc of bin.procs) { + const key = proc.seqNo + ""; + proc.useExactIfaceWrapper = !!seen[key] && !!eligible[key]; + } + } + + function emitEscapedExpression(expr: Expression, reason: string) { + return emitExpr(expr); + } + function reset() { bin.reset() proc = null @@ -1111,7 +1205,12 @@ namespace ts.pxtc { reset(); needsUsingInfo = false bin.finalPass = true + bin.ifaceCallCounts = {} + bin.mapSetByFieldIdCounts = {} + bin.checkedFieldAccessCounts = {} + bin.dynamicIfaceCalls = {} emit(rootFunction) + markExactIfaceWrappers() U.assert(usedWorkList.length == 0) @@ -1840,7 +1939,7 @@ ${lbl}: .short 0xffff let coll = ir.shared(ir.rtcall("Array_::mk", [])) for (let elt of node.elements) { let mask = isRefCountedExpr(elt) ? (1 << 1) : 0 - proc.emitExpr(ir.rtcall("Array_::push", [coll, emitExpr(elt)], mask)) + proc.emitExpr(ir.rtcall("Array_::push", [coll, emitEscapedExpression(elt, "arrayElement")], mask)) } if (node.elements.length > 0) // Make sure there is at least one use of the array, so it doesn't get GC-ed away in the last push. @@ -1881,8 +1980,11 @@ ${lbl}: .short 0xffff if (isRefCountedExpr(p.initializer)) mask |= 1 << 2 } + const nativeFieldId = target.isNative ? getIfaceMemberId(keyName) : undefined; + if (nativeFieldId !== undefined) + bin.mapSetByFieldIdCounts[nativeFieldId + ""] = (bin.mapSetByFieldIdCounts[nativeFieldId + ""] || 0) + 1; const fieldId = target.isNative - ? ir.numlit(getIfaceMemberId(keyName)) + ? ir.numlit(nativeFieldId) : ir.ptrlit(null, JSON.stringify(keyName)) const args = [ expr, @@ -1942,6 +2044,9 @@ ${lbl}: .short 0xffff // Reading a shimmed property return emitShim(fld, decl, [node.expression]) } else { + if (idx.needsCheck && !target.switches.skipClassCheck) { + bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] = (bin.checkedFieldAccessCounts[checkedFieldAccessCountKey(idx)] || 0) + 1; + } return ir.op(EK.FieldAccess, [emitExpr(node.expression)], idx) } } @@ -1994,7 +2099,11 @@ ${lbl}: .short 0xffff } if (!indexer && (t.flags & (TypeFlags.Any | TypeFlags.StructuredOrTypeVariable))) { - indexer = assign ? "pxtrt::mapSetGeneric" : "pxtrt::mapGetGeneric" + const hasIndexSignature = !!checker.getIndexTypeOfType(t, IndexKind.String) || !!checker.getIndexTypeOfType(t, IndexKind.Number); + const mapOnlySet = !!assign && !(t.flags & TypeFlags.Any) && hasIndexSignature; + indexer = assign + ? (mapOnlySet ? "pxtrt::mapSetByStringOnly" : "pxtrt::mapSetGeneric") + : "pxtrt::mapGetGeneric" stringOk = true } @@ -2366,7 +2475,7 @@ ${lbl}: .short 0xffff } function emitPlain() { - let r = mkProcCall(decl, node, args.map((x) => emitExpr(x))) + let r = mkProcCall(decl, node, args.map(x => emitEscapedExpression(x, "callArgument"))) let pp = r.data as ir.ProcId if (args[0] && pp.proc && pp.proc.classInfo) pp.isThis = args[0].kind == SK.ThisKeyword @@ -2397,7 +2506,7 @@ ${lbl}: .short 0xffff baseCtor = p.ctor if (!baseCtor && bin.finalPass) throw userError(9280, lf("super() call requires an explicit constructor in base class")) - let ctorArgs = args.map((x) => emitExpr(x)) + let ctorArgs = args.map((x) => emitEscapedExpression(x, "callArgument")) ctorArgs.unshift(emitThis(funcExpr)) return mkProcCallCore(baseCtor, node, ctorArgs) } @@ -2413,9 +2522,11 @@ ${lbl}: .short 0xffff // TODO in VT accessor/field/method -> different U.assert(funcExpr.kind == SK.PropertyAccessExpression); const fieldName = (funcExpr as PropertyAccessExpression).name.text + const ifaceIndex = getIfaceMemberId(fieldName, true); + bin.dynamicIfaceCalls[ifaceIndex + ""] = true; // completely dynamic dispatch - return mkMethodCall(args.map((x) => emitExpr(x)), { - ifaceIndex: getIfaceMemberId(fieldName, true), + return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), { + ifaceIndex, callLocationIndex: markCallLocation(node), noArgs }) @@ -2441,7 +2552,7 @@ ${lbl}: .short 0xffff } U.assert(!bin.finalPass || info.virtualIndex != null, "!bin.finalPass || info.virtualIndex != null") - return mkMethodCall(args.map((x) => emitExpr(x)), { + return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), { classInfo: info.parentClassInfo, virtualIndex: info.virtualIndex, noArgs, @@ -2480,9 +2591,15 @@ ${lbl}: .short 0xffff markFunctionUsed(decl) return emitPlain(); } else if (needsVCall || target.switches.slowMethods || !forceMethod) { - return mkMethodCall(args.map((x) => emitExpr(x)), { - ifaceIndex: getIfaceMemberId(getName(decl), true), - isSet: noArgs && args.length == 2, + const ifaceIndex = getIfaceMemberId(getName(decl), true); + const isSet = noArgs && args.length == 2; + const callKind = isSet ? "set" : noArgs ? "get" : "call"; + const helperGetSet = callKind == "call" ? "" : callKind; + const countKey = `${ifaceIndex}:${args.length}:${helperGetSet || "call"}`; + bin.ifaceCallCounts[countKey] = (bin.ifaceCallCounts[countKey] || 0) + 1; + return mkMethodCall(args.map((x) => emitEscapedExpression(x, "callArgument")), { + ifaceIndex, + isSet, callLocationIndex: markCallLocation(node), noArgs }) @@ -2503,7 +2620,7 @@ ${lbl}: .short 0xffff args.unshift(funcExpr) U.assert(!noArgs) - return mkMethodCall(args.map(x => emitExpr(x)), { + return mkMethodCall(args.map(x => emitEscapedExpression(x, "callArgument")), { virtualIndex: -1, callLocationIndex: markCallLocation(node), noArgs @@ -2611,6 +2728,8 @@ ${lbl}: .short 0xffff } function getCtor(decl: ClassDeclaration) { + if (!decl.members) + return null; return decl.members.filter(m => m.kind == SK.Constructor)[0] as ConstructorDeclaration } @@ -2662,7 +2781,7 @@ ${lbl}: .short 0xffff // let sig = checker.getResolvedSignature(node) // TODO: can we have overloeads? addDefaultParametersAndTypeCheck(checker.getResolvedSignature(node), args, ctorAttrs) - let compiled = args.map((x) => emitExpr(x)) + let compiled = args.map((x) => emitEscapedExpression(x, "callArgument")) if (ctorAttrs.shim) { // TODO need to deal with refMask and tagged ints here // we drop 'obj' variable @@ -3069,7 +3188,7 @@ ${lbl}: .short 0xffff let idx = fieldIndexCore(proc.classInfo, getFieldInfo(proc.classInfo, getName(f)), false) let trg2 = ir.op(EK.FieldAccess, [emitLocalLoad(info.thisParameter)], idx) - proc.emitExpr(ir.op(EK.Store, [trg2, emitExpr(f.initializer)])) + proc.emitExpr(ir.op(EK.Store, [trg2, emitEscapedExpression(f.initializer, "initializer")])) } flushHoistedFunctionDefinitions() @@ -3317,10 +3436,14 @@ ${lbl}: .short 0xffff isRef: true, shimName: attrs.shim, classInfo: info, - needsCheck + needsCheck, } } + function checkedFieldAccessCountKey(info: FieldAccessInfo) { + return `${info.classInfo.id}:${info.name}:get`; + } + function fieldIndex(pacc: PropertyAccessExpression): FieldAccessInfo { const tp = typeOf(pacc.expression) if (isPossiblyGenericClassType(tp)) { @@ -3341,6 +3464,9 @@ ${lbl}: .short 0xffff } return field; } + function tryGetFieldInfo(info: ClassInfo, fieldName: string) { + return info.allfields.filter(f => (f.name).text == fieldName)[0] || null + } function emitStore(trg: Expression, src: Expression, checkAssign: boolean = false) { if (checkAssign) { @@ -3352,7 +3478,7 @@ ${lbl}: .short 0xffff if (decl && (isGlobal || isVar(decl) || isParameter(decl))) { let l = lookupCell(decl) recordUse(decl, true) - proc.emitExpr(l.storeByRef(emitExpr(src))) + proc.emitExpr(l.storeByRef(emitEscapedExpression(src, "assignment"))) } else { unhandled(trg, lf("bad target identifier"), 9248) } @@ -3365,7 +3491,9 @@ ${lbl}: .short 0xffff unhandled(trg, lf("setter not available"), 9253) } proc.emitExpr(emitCallCore(trg, trg, [src], null, decl as FunctionLikeDeclaration)) - } else if (decl && (decl.kind == SK.PropertySignature || decl.kind == SK.PropertyAssignment || isSlowField(decl))) { + } else if (decl && (decl.kind == SK.PropertySignature || decl.kind == SK.PropertyAssignment)) { + proc.emitExpr(emitCallCore(trg, trg, [src], null, decl as FunctionLikeDeclaration)) + } else if (decl && isSlowField(decl)) { proc.emitExpr(emitCallCore(trg, trg, [src], null, decl as FunctionLikeDeclaration)) } else { for (; ;) { @@ -3380,7 +3508,7 @@ ${lbl}: .short 0xffff } } let trg2 = emitExpr(trg) - proc.emitExpr(ir.op(EK.Store, [trg2, emitExpr(src)])) + proc.emitExpr(ir.op(EK.Store, [trg2, emitEscapedExpression(src, "assignment")])) break; } } @@ -3801,7 +3929,7 @@ ${lbl}: .short 0xffff let convInfos: ir.ConvInfo[] = [] let args2 = args.map((a, i) => { - let r = emitExpr(a) + let r = emitEscapedExpression(a, "runtimeCallArgument") if (!needsNumberConversions()) return r let f = fmt[i + 1] @@ -4629,7 +4757,10 @@ ${lbl}: .short 0xffff } } typeCheckSubtoSup(node.initializer, node) - proc.emitExpr(loc.storeByRef(emitExpr(node.initializer))) + const initializerExpr = isGlobalVar(node) + ? emitEscapedExpression(node.initializer, "initializer") + : emitExpr(node.initializer); + proc.emitExpr(loc.storeByRef(initializerExpr)) currJres = null proc.stackEmpty(); } else if (inLoop(node)) { @@ -4652,10 +4783,23 @@ ${lbl}: .short 0xffff let exres: ir.Expr if (isPossiblyGenericClassType(objType)) { const info = getClassInfo(objType) - exres = ir.op(EK.FieldAccess, [objRef], fieldIndexCore(info, getFieldInfo(info, fieldName))) + const idx = fieldIndexCore(info, getFieldInfo(info, fieldName)) + if (idx.needsCheck && !target.switches.skipClassCheck) { + + } + exres = ir.op(EK.FieldAccess, [objRef], idx) } else { + // Dynamic field access on a non-class-typed receiver (any, structural type, + // etc). The call site supplies exactly 1 arg (the receiver) but the target + // proc resolved via iface dispatch may be a method that expects more -- + // the _args wrapper is what normally fills in the missing args. Flag this + // ifaceIndex so canUseExactIfaceWrapper disqualifies the wrapper-skip + // optimization for any proc reachable here. Mirrors the dynamic-method-call + // path above. + const ifaceIndex = getIfaceMemberId(fieldName, true); + bin.dynamicIfaceCalls[ifaceIndex + ""] = true; exres = mkMethodCall([objRef], { - ifaceIndex: getIfaceMemberId(fieldName, true), + ifaceIndex, callLocationIndex: markCallLocation(node), noArgs: true }) @@ -5065,6 +5209,10 @@ ${lbl}: .short 0xffff explicitlyUsedIfaceMembers: pxt.Map = {}; ifaceMemberMap: pxt.Map = {}; ifaceMembers: string[]; + ifaceCallCounts: pxt.Map = {}; + mapSetByFieldIdCounts: pxt.Map = {}; + checkedFieldAccessCounts: pxt.Map = {}; + dynamicIfaceCalls: pxt.Map = {}; strings: pxt.Map = {}; hexlits: pxt.Map = {}; doubles: pxt.Map = {}; diff --git a/pxtcompiler/emitter/hexfile.ts b/pxtcompiler/emitter/hexfile.ts index c59313d43cc7..f63e47aa99d1 100644 --- a/pxtcompiler/emitter/hexfile.ts +++ b/pxtcompiler/emitter/hexfile.ts @@ -811,9 +811,9 @@ ${lbl}: ${snippets.obj_header("pxt::number_vt")} PVoid methods[2 or 4]; */ - const ifaceInfo = computeHashMultiplier(info.itable.map(e => e.idx)) - //if (info.itable.length == 0) - // ifaceInfo.mult = 0 + const ifaceInfo = info.itable.length + ? computeHashMultiplier(info.itable.map(e => e.idx)) + : { mult: 0, mapping: new Uint16Array(0), size: 0 } let ptrSz = target.shortPointers ? ".short" : ".word" let s = ` @@ -853,6 +853,13 @@ ${info.id}_IfaceVT: const descSize = 8 const zeroOffset = ifaceInfo.mapping.length * 2 + // Iface table is the one consumer that can elect the wrapper-skip + // optimization. Procs with useExactIfaceWrapper get pointed at _iface + // (which `b _nochk`); everyone else gets the usual _args wrapper entry. + // canUseExactIfaceWrapper guarantees this is safe for the elected procs. + const procIfaceLabel = (p: ir.Procedure) => + (p.useExactIfaceWrapper ? p.label() + "_iface" : p.vtLabel()) + "@fn" + let descs = "" let offset = zeroOffset let offsets: pxt.Map = {} @@ -860,11 +867,11 @@ ${info.id}_IfaceVT: offsets[e.idx + ""] = offset const desc = !e.proc ? 0 : e.proc.isGetter() ? 1 : 2 descs += ` .short ${e.idx}, ${desc} ; ${e.name}\n` - descs += ` .word ${e.proc ? e.proc.vtLabel() + "@fn" : e.info}\n` + descs += ` .word ${e.proc ? procIfaceLabel(e.proc) : e.info}\n` offset += descSize if (e.setProc) { descs += ` .short ${e.idx}, 0 ; set ${e.name}\n` - descs += ` .word ${e.setProc.vtLabel()}@fn\n` + descs += ` .word ${procIfaceLabel(e.setProc)}\n` offset += descSize } } @@ -881,7 +888,9 @@ ${info.id}_IfaceVT: } // offsets are relative to the position in the array - s += " .short " + U.toArray(map).map((e, i) => (offsets[e + ""] || zeroOffset) - (i * 2)).join(", ") + "\n" + const hashOffsets = U.toArray(map).map((e, i) => (offsets[e + ""] || zeroOffset) - (i * 2)) + if (hashOffsets.length) + s += " .short " + hashOffsets.join(", ") + "\n" s += descs s += "\n" diff --git a/pxtcompiler/emitter/ir.ts b/pxtcompiler/emitter/ir.ts index 0968af28b0e4..08ebb80c7180 100644 --- a/pxtcompiler/emitter/ir.ts +++ b/pxtcompiler/emitter/ir.ts @@ -532,6 +532,7 @@ namespace ts.pxtc.ir { classInfo: ClassInfo = null; perfCounterName: string = null; perfCounterNo = 0; + useExactIfaceWrapper = false; body: Stmt[] = []; lblNo = 0; @@ -553,6 +554,11 @@ namespace ts.pxtc.ir { return this.action && this.action.kind == ts.SyntaxKind.GetAccessor } + // Returns the proc's "_args" entry label -- the full arg-shuffling + // wrapper. Use this for any vtable-style reference where the caller + // cannot guarantee the wrapper-skip preconditions (numargs >= proc.args.length). + // For iface-table entries that CAN accept the optimization, the consumer + // (hexfile.ts) inspects useExactIfaceWrapper directly and picks _iface instead. vtLabel() { return this.label() + (isStackMachine() ? "" : "_args") }