From 2c29530372eaa70ebcea0896ddfd8d1aa3180e3a Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 28 May 2026 15:59:07 -0700 Subject: [PATCH 1/2] Lower boolean conditions directly (skip boxing) + thumb fast paths --- pxtcompiler/emitter/backthumb.ts | 78 +++++++++++++++++++++++++++++++- pxtcompiler/emitter/emitter.ts | 36 ++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/pxtcompiler/emitter/backthumb.ts b/pxtcompiler/emitter/backthumb.ts index af08bcdedae3..840333c99883 100644 --- a/pxtcompiler/emitter/backthumb.ts +++ b/pxtcompiler/emitter/backthumb.ts @@ -25,6 +25,10 @@ namespace ts.pxtc { "numops::lsrs": "_numops_lsrs", "pxt::toInt": "_numops_toInt", "pxt::fromInt": "_numops_fromInt", + "pxt::fromBool": "_pxt_fromBool", + "numops::toBool": "_numops_toBool", + "numops::toBoolDecr": "_numops_toBoolDecr", + "Boolean_::bang": "_pxt_boolean_bang", } // snippets for ARM Thumb assembly @@ -323,6 +327,78 @@ _numops_fromInt: blx lr .over2: ${this.callCPPPush("pxt::fromInt")} + +_pxt_fromBool: + @scope _pxt_fromBool + cmp r0, #0 + beq .false +.true: + movs r0, #${taggedTrue} + blx lr +.false: + movs r0, #${taggedFalse} + blx lr + +_pxt_boolean_bang: + @scope _pxt_boolean_bang + cmp r0, #0 + beq .true +.false: + movs r0, #0 + blx lr +.true: + movs r0, #1 + blx lr + +_numops_toBool: + @scope _numops_toBool + cmp r0, #0 + beq .false + cmp r0, #1 + beq .false + lsls r1, r0, #31 + bne .true + cmp r0, #${taggedNull} + beq .false + cmp r0, #${taggedFalse} + beq .false + cmp r0, #${taggedNaN} + beq .false + lsls r1, r0, #30 + beq .boxed +.true: + movs r0, #1 + blx lr +.false: + movs r0, #0 + blx lr +.boxed: + ${this.callCPPPush("numops::toBool")} + +_numops_toBoolDecr: + @scope _numops_toBoolDecr + cmp r0, #0 + beq .false + cmp r0, #1 + beq .false + lsls r1, r0, #31 + bne .true + cmp r0, #${taggedNull} + beq .false + cmp r0, #${taggedFalse} + beq .false + cmp r0, #${taggedNaN} + beq .false + lsls r1, r0, #30 + beq .boxed +.true: + movs r0, #1 + blx lr +.false: + movs r0, #0 + blx lr +.boxed: + ${this.callCPPPush("numops::toBoolDecr")} ` @@ -351,7 +427,7 @@ _cmp_${op}: // Also, cmp isn't needed when ref-counting (it ends with movs r0, r4) r += boxedOp(` bl numops::${op} - bl numops::toBoolDecr + bl _numops_toBoolDecr cmp r0, #0`) } diff --git a/pxtcompiler/emitter/emitter.ts b/pxtcompiler/emitter/emitter.ts index 461125dda37f..e3cc739f43e6 100644 --- a/pxtcompiler/emitter/emitter.ts +++ b/pxtcompiler/emitter/emitter.ts @@ -4157,7 +4157,41 @@ ${lbl}: .short 0xffff function emitExpressionStatement(node: ExpressionStatement) { emitExprAsStmt(node.expression) } - function emitCondition(expr: Expression, inner: ir.Expr = null) { + function emitCondition(expr: Expression, inner: ir.Expr = null): ir.Expr { + // Lower `&&` / `||` used as a condition into short-circuiting jumps + // that yield 0/1 directly, instead of boxing the intermediate + // booleans and converting them back with a runtime helper. + if (!inner && !isStackMachine() && expr.kind == SK.BinaryExpression) { + const binary = expr as BinaryExpression + const op = binary.operatorToken.kind + if (op == SK.AmpersandAmpersandToken || op == SK.BarBarToken) { + const shortCircuitLabel = proc.mkLabel("lazycond") + const doneLabel = proc.mkLabel("lazycondfin") + if (op == SK.AmpersandAmpersandToken) { + proc.emitJmp(shortCircuitLabel, emitCondition(binary.left), ir.JmpMode.IfZero) + proc.emitJmp(shortCircuitLabel, emitCondition(binary.right), ir.JmpMode.IfZero) + proc.emitJmp(doneLabel, ir.numlit(1), ir.JmpMode.Always) + proc.emitLbl(shortCircuitLabel) + proc.emitJmp(doneLabel, ir.numlit(0), ir.JmpMode.Always) + } else { + proc.emitJmp(shortCircuitLabel, emitCondition(binary.left), ir.JmpMode.IfNotZero) + proc.emitJmp(shortCircuitLabel, emitCondition(binary.right), ir.JmpMode.IfNotZero) + proc.emitJmp(doneLabel, ir.numlit(0), ir.JmpMode.Always) + proc.emitLbl(shortCircuitLabel) + proc.emitJmp(doneLabel, ir.numlit(1), ir.JmpMode.Always) + } + proc.emitLbl(doneLabel) + return captureJmpValue() + } + } + // Lower `!expr` used as a condition by negating the operand's + // condition directly, avoiding a boxed boolean just to invert it. + if (!inner && !isStackMachine() && expr.kind == SK.PrefixUnaryExpression) { + const unary = expr as PrefixUnaryExpression + if (unary.operator == SK.ExclamationToken) { + return ir.rtcall("Boolean_::bang", [emitCondition(unary.operand)]) + } + } if (!inner && isThumb() && expr.kind == SK.BinaryExpression) { let be = expr as BinaryExpression let mapped = U.lookup(thumbCmpMap, simpleInstruction(be, be.operatorToken.kind)) From c65095aa499684c6b9b7bf1b1e3f4c54d3f004d5 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Thu, 28 May 2026 17:11:19 -0700 Subject: [PATCH 2/2] Add comments to asm helpers --- pxtcompiler/emitter/backthumb.ts | 48 ++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/pxtcompiler/emitter/backthumb.ts b/pxtcompiler/emitter/backthumb.ts index 840333c99883..32259259322b 100644 --- a/pxtcompiler/emitter/backthumb.ts +++ b/pxtcompiler/emitter/backthumb.ts @@ -328,6 +328,8 @@ _numops_fromInt: .over2: ${this.callCPPPush("pxt::fromInt")} +; Tag a raw 0/1 truth value into a boolean object (taggedTrue/taggedFalse). +; Inverse of the toBool fast paths below; matches C++ pxt::fromBool. _pxt_fromBool: @scope _pxt_fromBool cmp r0, #0 @@ -339,6 +341,9 @@ _pxt_fromBool: movs r0, #${taggedFalse} blx lr +; Logical NOT of a raw 0/1 truth value -> raw 0/1 (matches C++ bool bang(bool)). +; Callers pass an already-tested condition, so this is raw-in / raw-out; value +; context (`x = !y`) wraps the result with fromBool to re-tag it. _pxt_boolean_bang: @scope _pxt_boolean_bang cmp r0, #0 @@ -350,23 +355,32 @@ _pxt_boolean_bang: movs r0, #1 blx lr +; Fast-path truthiness test: tagged value in r0 -> raw 0/1 in r0. +; Value encodings (see taggedSpecialValue in emitter.ts): tagged int = (n<<1)|1 +; (low bit set); special tags = (n<<2)|2 with undefined=0, null, false, NaN, +; true; heap objects have low 2 bits 00. +; Only non-pointer values are decided inline here; any heap object is handed to +; the C++ helper at .boxed (which also handles boxed numbers/strings like 0.0 +; and ""). INVARIANT: the falsy set below must match C++ numops::toBool exactly +; -- if a new falsy tagged value is ever added, it must be added here too, or it +; will fall through to .true. _numops_toBool: @scope _numops_toBool - cmp r0, #0 + cmp r0, #0 ; undefined (0) -> false beq .false - cmp r0, #1 + cmp r0, #1 ; integer 0 ((0<<1)|1) -> false beq .false - lsls r1, r0, #31 + lsls r1, r0, #31 ; odd => nonzero tagged int -> true bne .true - cmp r0, #${taggedNull} + cmp r0, #${taggedNull} ; null -> false beq .false - cmp r0, #${taggedFalse} + cmp r0, #${taggedFalse} ; false -> false beq .false - cmp r0, #${taggedNaN} + cmp r0, #${taggedNaN} ; NaN -> false beq .false - lsls r1, r0, #30 + lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++ beq .boxed -.true: +.true: ; remaining specials (e.g. true) are truthy movs r0, #1 blx lr .false: @@ -375,21 +389,25 @@ _numops_toBool: .boxed: ${this.callCPPPush("numops::toBool")} +; Same truthiness classification as _numops_toBool (see comments above), but +; this variant also consumes (ref-decrements) its argument. Only heap objects +; are ref-counted, and those go through the C++ fallback which does the decr; +; the inline non-pointer paths carry no refcount, so they need none. _numops_toBoolDecr: @scope _numops_toBoolDecr - cmp r0, #0 + cmp r0, #0 ; undefined (0) -> false beq .false - cmp r0, #1 + cmp r0, #1 ; integer 0 ((0<<1)|1) -> false beq .false - lsls r1, r0, #31 + lsls r1, r0, #31 ; odd => nonzero tagged int -> true bne .true - cmp r0, #${taggedNull} + cmp r0, #${taggedNull} ; null -> false beq .false - cmp r0, #${taggedFalse} + cmp r0, #${taggedFalse} ; false -> false beq .false - cmp r0, #${taggedNaN} + cmp r0, #${taggedNaN} ; NaN -> false beq .false - lsls r1, r0, #30 + lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++ beq .boxed .true: movs r0, #1