Skip to content
96 changes: 95 additions & 1 deletion pxtcompiler/emitter/backthumb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -323,6 +327,96 @@ _numops_fromInt:
blx lr
.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
beq .false
.true:
movs r0, #${taggedTrue}
blx lr
.false:
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
beq .true
.false:
movs r0, #0
blx lr
.true:
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 ; undefined (0) -> false
beq .false
cmp r0, #1 ; integer 0 ((0<<1)|1) -> false
beq .false
lsls r1, r0, #31 ; odd => nonzero tagged int -> true
bne .true
cmp r0, #${taggedNull} ; null -> false
beq .false
cmp r0, #${taggedFalse} ; false -> false
beq .false
cmp r0, #${taggedNaN} ; NaN -> false
beq .false
lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++
beq .boxed
.true: ; remaining specials (e.g. true) are truthy
movs r0, #1
blx lr
.false:
movs r0, #0
blx lr
.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 ; undefined (0) -> false
beq .false
cmp r0, #1 ; integer 0 ((0<<1)|1) -> false
beq .false
lsls r1, r0, #31 ; odd => nonzero tagged int -> true
bne .true
cmp r0, #${taggedNull} ; null -> false
beq .false
cmp r0, #${taggedFalse} ; false -> false
beq .false
cmp r0, #${taggedNaN} ; NaN -> false
beq .false
lsls r1, r0, #30 ; low 2 bits 00 => heap object -> defer to C++
beq .boxed
.true:
movs r0, #1
blx lr
.false:
movs r0, #0
blx lr
.boxed:
${this.callCPPPush("numops::toBoolDecr")}
`


Expand Down Expand Up @@ -351,7 +445,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`)
}

Expand Down
36 changes: 35 additions & 1 deletion pxtcompiler/emitter/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down