Skip to content

py/parse: Fold anonymous string objects. - #19598

Closed
agatti wants to merge 1 commit into
micropython:masterfrom
agatti:const-folding-str
Closed

py/parse: Fold anonymous string objects.#19598
agatti wants to merge 1 commit into
micropython:masterfrom
agatti:const-folding-str

Conversation

@agatti

@agatti agatti commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds the possibility for the compiler to merge anonymous string objects that get concatenated together, so that a single object is provided to the VM instead of having to build the object.

For example, compiling statements like "Hello," + " " + "world!" would yield an opcode sequence like this:

LOAD_CONST_STRING "Hello,"  
LOAD_CONST_STRING " "  
BINARY_OP __add__  
LOAD_CONST_STRING "world!"  
BINARY_OP __add__  

whilst with folding, a single LOAD_CONST_STRING opcode will be emitted, with the concatenation of the strings involved.

This only applies if all members of the expressions are anonymous strings, otherwise the regular sequence of opcodes and partial strings will be emitted instead.
 

Testing

Besides making the test suite pass on Linux/x64 with --via-mpy, a new test, basics/str_constfolding.py has been added to provide a few more string concatenation opportunities than what is available in the test suite right now.

Trade-offs and Alternatives

The compiler/parser will obviously take up a bit more space, but I'm not sure how this can be shortened without making it more complicated than it is now.

For interactive usage, whilst folded parse nodes are removed from the tree, the QSTR backing the node leaves won't be evicted from within the parser. I'm not sure if it's a problem or not, or whether there's a way to mark now-unused QSTRs sitting in RAM as reclaimable or something like that.

Generative AI

I did not use generative AI tools when creating this PR.


I've had this in my own local tree for quite some time now, and I forgot to attempt upstreaming until now :| I've had some benefits with this for native-compiled files, however I'm not sure if this is worth of inclusion, hence why it's marked as draft.

@agatti agatti added the py-core Relates to py/ directory in source label Aug 10, 2026
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.28571% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.55%. Comparing base (aee96a0) to head (7b4a4a1).

Files with missing lines Patch % Lines
py/parse.c 94.28% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #19598      +/-   ##
==========================================
- Coverage   98.55%   98.55%   -0.01%     
==========================================
  Files         182      182              
  Lines       23316    23351      +35     
  Branches        5        5              
==========================================
+ Hits        22980    23013      +33     
- Misses        335      337       +2     
  Partials        1        1              
Flag Coverage Δ
unix-coverage-32bit 98.55% <94.28%> (-0.01%) ⬇️
unix-coverage-64bit 98.52% <94.28%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code size report:

Reference:  docs/esp32/quickref: Add config for ESP32-WROOM-32UE with KSZ8863 LAN. [aee96a0]
Comparison: py/parse: Fold anonymous string objects. [merge of 7b4a4a1]
  mpy-cross:  +352 +0.092% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +352 +0.041% standard
      stm32:  +216 +0.053% PYBV10
      esp32:  +308 +0.017% ESP32_GENERIC
     mimxrt:  +224 +0.057% TEENSY40
        rp2:  +256 +0.027% RPI_PICO_W
       samd:  +224 +0.081% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:  +240 +0.052% VIRT_RV32

This commit adds the possibility for the compiler to merge anonymous
string objects that get concatenated together, so that a single object
is provided to the VM instead of having to build the object.

For example, compiling statements like "Hello," + " " + "world!" would
yield an opcode sequence like this:

   LOAD_CONST_STRING "Hello,"
   LOAD_CONST_STRING " "
   BINARY_OP __add__
   LOAD_CONST_STRING "world!"
   BINARY_OP __add__

whilst with folding, a single `LOAD_CONST_STRING` will be emitted, with
the concatenation of the strings involved.

This only applies if all members of the expressions are anonymous
strings, otherwise the regular sequence of opcodes and partial strings
will be emitted instead.

Signed-off-by: Alessandro Gatti <a.gatti@frob.it>
@agatti
agatti force-pushed the const-folding-str branch from 67aeac4 to 7b4a4a1 Compare August 10, 2026 10:07
@octoprobe-bot

Copy link
Copy Markdown

Octoprobe PR report

Test Tests
passed
Tests
skipped
Tests
xfailed
Tests
failed
format flash 9 8
run-tests.py 22998 3325 15 6
run-tests.py --via-mpy --emit native 24363 4291 27 17
run-tests.py --via-mpy 22151 3265 15 6
run-perfbench.py 394 14
run-natmodtests.py 702 153 6
run-tests.py --test-dirs=extmod_hardware 166 252 69 3
run-tests.py --test-dirs=extmod_hardware --emit-native 158 243 66 3
Total 70941 11551 198 35
Failures

Group: run-tests.py --test-dirs=extmod_hardware

Test esp32
0c30-
ESP32_C3_DEVKIT
esp32
5d21-
ESP32_DEVKIT
esp32
472b-
ESP32_S3_DEVKIT
esp32
1830-
LOLIN_C3_MINI
esp8266
7921-
LOLIN_D1_MINI
mimxrt
1133-
TEENSY40
nrf
3c2a-
ARDUINO_NANO_33
rp2
5334-
RPI_PICO2
rp2
5334-
RPI_PICO2-
RISCV
rp2
552b-
RPI_PICO2_W
rp2
6038-
RPI_PICO_W
samd
5f2a-
ADA_ITSYBITSY_M0
stm32
2b35-
NUCLEO_WB55
stm32
7b21-
PYBV11
stm32
7b21-
PYBV11-
DP
stm32
7b21-
PYBV11-
DP_THREAD
stm32
3a21-
PYBV11-
THREAD
extmod_hardware/machine_uart_irq_rxidle.py pass pass pass FAIL FAIL FAIL pass pass pass pass pass pass skip pass pass pass skip skip skip XFAIL XFAIL XFAIL
xfail_master_478.json
XFAIL XFAIL XFAIL
xfail_master_478.json
pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass

Group: run-tests.py --test-dirs=extmod_hardware --emit-native

Test esp32
0c30-
ESP32_C3_DEVKIT
esp32
5d21-
ESP32_DEVKIT
esp32
472b-
ESP32_S3_DEVKIT
esp32
1830-
LOLIN_C3_MINI
esp8266
7921-
LOLIN_D1_MINI
mimxrt
1133-
TEENSY40
nrf
3c2a-
ARDUINO_NANO_33
rp2
5334-
RPI_PICO2
rp2
5334-
RPI_PICO2-
RISCV
rp2
552b-
RPI_PICO2_W
rp2
6038-
RPI_PICO_W
samd
5f2a-
ADA_ITSYBITSY_M0
stm32
2b35-
NUCLEO_WB55
stm32
3a21-
PYBV11
stm32
7b21-
PYBV11-
DP
stm32
7b21-
PYBV11-
DP_THREAD
stm32
3a21-
PYBV11-
THREAD
extmod_hardware/machine_uart_irq_rxidle.py pass pass pass FAIL FAIL FAIL pass pass pass pass pass pass skip pass pass pass skip skip skip XFAIL XFAIL XFAIL
xfail_master_478.json
XFAIL XFAIL XFAIL
xfail_master_478.json
pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass

Group: run-tests.py

Test esp32
0c30-
ESP32_C3_DEVKIT
esp32
5d21-
ESP32_DEVKIT
esp32
472b-
ESP32_S3_DEVKIT
esp32
1830-
LOLIN_C3_MINI
esp8266
7921-
LOLIN_D1_MINI
mimxrt
1133-
TEENSY40
nrf
3c2a-
ARDUINO_NANO_33
rp2
5334-
RPI_PICO2
rp2
5334-
RPI_PICO2-
RISCV
rp2
552b-
RPI_PICO2_W
rp2
5f2c-
RPI_PICO_W
samd
5f2a-
ADA_ITSYBITSY_M0
stm32
2b35-
NUCLEO_WB55
stm32
7b21-
PYBV11
stm32
7b21-
PYBV11-
DP
stm32
7b21-
PYBV11-
DP_THREAD
stm32
3a21-
PYBV11-
THREAD
ports/stm32/rtc.py pass pass pass FAIL FAIL pass pass pass pass
ports/stm32/servo.py skip pass pass pass FAIL pass pass pass pass
ports/stm32/spi.py pass pass pass pass FAIL pass pass pass pass
ports/stm32/switch.py pass pass pass pass FAIL pass pass pass pass
ports/stm32/timer.py pass pass pass pass FAIL pass pass pass pass

Group: run-tests.py --via-mpy --emit native

Test esp32
0c30-
ESP32_C3_DEVKIT
esp32
5d21-
ESP32_DEVKIT
esp32
472b-
ESP32_S3_DEVKIT
esp32
1830-
LOLIN_C3_MINI
esp8266
7921-
LOLIN_D1_MINI
mimxrt
1133-
TEENSY40
nrf
3c2a-
ARDUINO_NANO_33
rp2
5334-
RPI_PICO2
rp2
5334-
RPI_PICO2-
RISCV
rp2
552b-
RPI_PICO2_W
rp2
5f2c-
RPI_PICO_W
samd
5f2a-
ADA_ITSYBITSY_M0
stm32
2b35-
NUCLEO_WB55
stm32
3a21-
PYBV11
stm32
3a21-
PYBV11-
DP
stm32
7b21-
PYBV11-
DP_THREAD
stm32
3a21-
PYBV11-
THREAD
basics/builtin_setattr.py pass pass pass pass pass FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/builtin_slice.py pass pass pass pass skip FAIL skip pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/builtin_sorted.py pass pass pass pass pass FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/builtin_str_hex.py pass pass pass pass pass FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/bytearray_partition.py pass pass pass pass skip FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/bytearray_slice_assign.py pass pass pass pass skip FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/bytes.py pass pass pass pass skip FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass
basics/bytes_add.py pass pass pass pass pass FAIL pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass pass ...[Comment body truncated]

@Josverl

Josverl commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

but is it needed?
after all Python syntax allows to write this without the + .

# fmt: off
s1 = "Hello World"
s2 = "Hello" " " "World"
s3 = "Hello" \
     " " \
     "World"
s4 = (
    "Hello"
     " " 
     "World"
)

and they are all bytecode equivalent.

mpy_source_file: /tmp/example.mpy
source_file: /tmp/example.py
header: 4d:06:00:1f
arch: NONE
qstr_table[6]:
    /tmp/example.py
    <module>
    s1
    s2
    s3
    s4
obj_table: ['Hello World']
simple_name: <module>
  raw bytecode: 26 00:0c:01:40:24:24:64:20:23:00:16:02:23:00:16:03:23:00:16:04:23:00:16:05:51:63
  prelude: (1, 0, 0, 0, 0, 0)
  args: []
  line info: 40:24:24:64:20
  23:00       LOAD_CONST_OBJ 'Hello World'
  16:02       STORE_NAME s1
  23:00       LOAD_CONST_OBJ 'Hello World'
  16:03       STORE_NAME s2
  23:00       LOAD_CONST_OBJ 'Hello World'
  16:04       STORE_NAME s3
  23:00       LOAD_CONST_OBJ 'Hello World'
  16:05       STORE_NAME s4
  51          LOAD_CONST_NONE 
  63          RETURN_VALUE 
  children: []

@agatti

agatti commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

but is it needed? after all Python syntax allows to write this without the +

That's why this is marked as draft :)

I've got loads of code with strings wrapped using + instead of an escaped continuation, I'm not sure if it was PyCharm's (or the default IntelliJ Python language support) default method for breaking up long lines back then, but it was pressing enough for me to actually patch the parser. I've also moved on from IntelliJ stuff, but the patch still remained.

Now, I can probably make this more palatable by sneaking in const() string folding (ie. A = const("hello"); print(A + ", world") becoming A = const("hello"); print("hello, world")), but that will maybe have a larger impact on footprint.

@agatti

agatti commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

On second thought, maybe this is better to stay out of the main source tree.

Sorry :(

@agatti agatti closed this Aug 10, 2026
@dpgeorge

Copy link
Copy Markdown
Member

Using adjacent string concatenation is IMO a good solution, rather than + operator.

If anything this could be addressed with a small addition to the docs, saying that the former is optimised but the latter (+) is not.

@agatti

agatti commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

In the meantime I've added that to the flake8 plugin: https://codeberg.org/agatti/flake8-micropython/commit/cb06cb3084ccf11a38a33667e9a153f3eb040291.

Docs-wise, where should that paragraph be added to, docs/develop/optimizations.rst or docs/reference/speed_python.rst?

@dpgeorge

Copy link
Copy Markdown
Member

Docs-wise, where should that paragraph be added to

I think docs/reference/speed_python.rst is best, maybe under the "RAM allocation" section, or "Strings vs bytes".

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

Labels

py-core Relates to py/ directory in source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants