Skip to content

Lower boolean conditions directly (skip boxing) + thumb fast paths#11336

Open
humanapp wants to merge 2 commits into
microsoft:masterfrom
humanapp:eanders/boolean-lowering
Open

Lower boolean conditions directly (skip boxing) + thumb fast paths#11336
humanapp wants to merge 2 commits into
microsoft:masterfrom
humanapp:eanders/boolean-lowering

Conversation

@humanapp

Copy link
Copy Markdown
Contributor

Hey folks! Here is another compiler optimization for your consideration. This is a change that makes every MakeCode program smaller and faster. It reduces the MicroCode hex file size by 44k, and makes the user experience on hardware noticeably more responsive. It does come with some risk: low odds of a bug, but it touches low-level branch code everywhere, so you'll want to test it broadly before shipping.

Summary

The MakeCode compiler currently compiles booleans inefficiently. In an expression like if (!ready) or while (a || b), the expression is turned into a "boxed" boolean value that is then passed to a helper function, where it is unboxed and tested for truthiness. This PR changes the compiler to evaluate those conditions directly, and adds small fast-paths that speed up boxing/unboxing for a few common cases where boxing is still needed. The result is generated code that is both significantly smaller and measurably faster.

Testing with the microcode build, this change produces a hex file that is ~44k smaller, and a user experience that is noticeably more responsive.

What it changes

There are two layers to this change:

  • Frontend (TS --> IR)
    • !x in a condition now negates x's truth test directly, instead of producing a boxed boolean just to invert it.
    • a && b / a || b in a condition now compile to ordinary short-circuit branches that yield a plain 0/1, instead of boxing each intermediate boolean and converting it back.
    • Changes in this layer affect all platforms (ARM/microbit, JavaScript, etc.)
  • Backend (IR --> asm)
    • Small inline assembly fast paths for the boolean helpers. These handle the common values inline and defer anything unrecognized to the existing C++ helpers.
    • Changes in this layer only affect ARM/microbit.

Only code used to decide a branch is impacted by this change. Boolean expressions used as values (e.g. const flag = a && b, return !x) are untouched.

Risk assessment

I'd put this at low/moderate risk. The changes are small and not complicated, but the blast radius if a bug does surface is broad. In a way, the broad blast radius is risk-mitigating. If this regresses something, it would be caught pretty quickly.

The biggest risk here is misclassification of a candidate for fast-path. Sending a value that should have been boxed down the fast path would lead to incorrect truthiness evals.

Where do these classifications happen?

Fast-path filtering happens in _numops_toBool and _numops_toBoolDecr. These methods are extensively commented to make their logic clear.

Mitigating factors

  • Change is scoped to branch-decisions only. It cannot change the value of a boolean assigned to a value.
  • The hand-written assembly fast-paths only handle common, well-defined cases, and fallback to the preexisting C++ helper method for anything it doesn't recognize.

Tests Performed

I compiled and ran many Arcade games on hardware, including:

  • Tiny Soccer Cars
  • Tiny Wizards
  • Many of my old projects
  • MicroCode

cc: @thomasjball

@riknoll riknoll left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change looks good (thanks for all the comments!) but agree the blast radius is high. i wish we had a test suite for generated ARM code! we'll just have to bang on it a lot.

@abchatra not sure if you want this in the mbit release or not, could be risky.

@thomasjball

Copy link
Copy Markdown
Collaborator

@abchatra - where are we in the testing for next release of microbit - do we have enough runway to test this change more fully?

@thomasjball

Copy link
Copy Markdown
Collaborator

@humanapp - the fromBool function is provided just as a helper for the user; it is not needed for the optimization, correct?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes boolean handling in the PXT compiler by lowering boolean conditions directly into short-circuiting control flow (avoiding boxed booleans in branch-decision paths) and by adding Thumb inline fast paths for common boolean boxing/unboxing and truthiness checks.

Changes:

  • Update emitCondition() to lower &&/|| used as conditions into short-circuiting jumps that produce raw 0/1, and lower !expr in condition context via Boolean_::bang on the raw condition result.
  • Add Thumb assembly helpers for pxt::fromBool, Boolean_::bang, numops::toBool, and numops::toBoolDecr, and route calls to these helpers where applicable.
Show a summary per file
File Description
pxtcompiler/emitter/emitter.ts Lowers &&/`
pxtcompiler/emitter/backthumb.ts Adds Thumb fast paths for boolean helpers (fromBool, bang, toBool, toBoolDecr) and updates comparison helper path to use the new toBoolDecr fast path.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 0

@abchatra

Copy link
Copy Markdown
Collaborator

Good to see you @humanapp 😃

@humanapp

Copy link
Copy Markdown
Contributor Author

@humanapp - the fromBool function is provided just as a helper for the user; it is not needed for the optimization, correct?

@thomasjball, the presence of "pxt::fromBool" in the inlineArithmetic map changes the path taken in ThumbSnippets.call_lbl, causing it to emit a call to the new _pxt_fromBool helper instead of the C++ pxt::fromBool. It's still a call to a helper, but it drops the GC stack-save bookkeeping that wraps C++ calls.

@thomasjball

thomasjball commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

@humanapp - I don't quite follow the logic here:

lsls r1, r0, #31    ; odd => nonzero tagged int -> true
bne .true

@thomasjball

Copy link
Copy Markdown
Collaborator

I will create a MakeCode program to test the various conditions.

@humanapp

humanapp commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@humanapp - I don't quite follow the logic here:

lsls r1, r0, #31    ; odd => nonzero tagged int -> true
bne .true

This code is checking the lowest bit of r0 to see if it is set. If so, that indicates a tagged int, so branch to the .true label. How it does this is awkward to describe because lsls clears the Z flag if the result of the shift was non-zero, and bne branches to .true if the Z flag is cleared.

  • lsls r1, r0, #31 - is r0 a tagged int? if so, then clear the Z flag
  • bne .true - is the Z flag cleared? if so, then branch to .true

@thomasjball

Copy link
Copy Markdown
Collaborator

@humanapp - so my confusion is how the sequence does two checks (it is a tagged int and that int is non zero).

@humanapp

humanapp commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@humanapp - so my confusion is how the sequence does two checks (it is a tagged int and that int is non zero).

The cmp preceding the lsls instruction filters tagged-int with value zero:

    cmp r0, #1          ; integer 0 ((0<<1)|1)     -> false
    beq .false

With what possibility filtered out, lsls just needs to check whether the value is a tagged int. If it is, then we already know it is non-zero.

@thomasjball

Copy link
Copy Markdown
Collaborator

Thanks. I chatted with Jonny Austin - we will keep this PR for after new version of MakeCode for mbit ships.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants