Release v0.1.0a2 (alpha 2) - #2
Merged
Merged
Conversation
The existing STD/LDD forwarding only fires on adjacent store/reload pairs. 16-bit values interleave their lo/hi halves (STD lo; STD hi; LDD lo; LDD hi) and call results are parked in a slot then reloaded for a compare, so the redundant reload is never adjacent to its store and survives. ForwardStores tracks, per basic block, which register still mirrors each Y+off slot and drops reloads that re-read a value the register already holds (and re-stores of an unchanged value). It is conservative by construction: STD/LDD are modeled precisely, a small set of pure readers leave tracking intact, and anything else (multi-register writers, pointer loads, calls, branches, labels, unknown mnemonics) clears all tracking -- so it never forwards a stale value across an unmodeled effect or a basic-block boundary. Cross-block churn (store/reload around a join label) is left untouched on purpose: at a join the slot value is consistent but the holding register is not, so forwarding there would be wrong. That redundancy is an IR-level copy-propagation concern, not a peephole one. Updated Float_Return_LoadsIntoR22R25: the single-Copy case now correctly elides its redundant reload (the value is already in R22:R25), so the test forces a genuine reload with an intervening float Copy that clobbers all four bytes. All 81 unit tests and 722 integration (simulator) tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eference) Backend half of passing a const[str] to a non-@inline function by reference: - LoadIntoReg(FlashStrAddr): LDI lo8/hi8(__flash_<name>) -- the string's 16-bit flash byte-address (byte address, no gs() word scaling; read via LPM). - CompileFlashLoadPtr: Dst = flash[Ptr + Index] via LPM, with the base held in a register pair (Z) instead of a fixed label (mirrors CompileArrayLoadFlash). - AvrLinearScan visits the new operands so temp liveness stays correct, and the dead-parameter-store check counts the Ptr/Index reads. Lets uart_write_str compile to one shared subroutine; dht-sensor 1362 -> 1108 B. All 81 unit + 722 integration (simulator) tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A call result is parked in its temp register's home (MOV R16/R17, R24) but the comparison that follows consumes it straight from R24, so the home-store is dead. A peephole can't see this -- the value's death is only provable across the branch that follows. EliminateDeadTempMoves runs a backward liveness restricted to the two linear-scan temporaries and drops a MOV whose destination is never read before being overwritten or before the function returns. Restricting to R16/R17 makes the exit condition exact: they are scratch (never a return register; if ever pushed, the matching POP kills the value first), so they are dead at every RET. Safety is asymmetric by construction: reads are over-approximated (unknown mnemonics and inline-asm Raw that textually mentions the register keep it live; assembler directives and register-free Raw like NOP delays stay transparent) while writes are under-approximated (only an unambiguous pure write kills liveness). Calls do not read the temps -- PyMCU passes arguments in R24/R22/R20/R18 and callees that use a temp internally push/pop it. The pass bails on computed jumps. dht-sensor 1092 -> 1074 B (eight dead home-stores removed). 81 unit + 722 integration (simulator) tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CompileBytearrayLoad/Store used R16/R17 as scratch when adding a variable (or large-constant) index to Z. Those are exactly the registers the linear-scan allocator hands out, so a register-allocated value live across a buf[i] access was silently destroyed -- a latent fragility today and the concrete blocker that broke UART readinto/readline when register allocation was extended to locals. Use R26 (X-low, never in the allocation pool) for the index and R1 (the zero register) for the carry, mirroring CompileArrayLoadFlash. Behaviour-preserving for current code (R16/R17 are not yet allocated across these sites) and it drops the redundant CLR. First prerequisite step toward A17 register allocation: codegen scratch must be disjoint from the allocator's register pool. 81 unit + 722 integration (simulator) tests pass; dht-sensor unchanged at 1074 B. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n codegen
Continues the A17 prerequisite (codegen scratch must be disjoint from the linear-
scan allocator pool). Audited every hardcoded R16/R17 use:
- ArrayLoad / ArrayStore variable-index: used CLR R16 + ADC R31,R16 as the carry
zero -> now ADC R31,R1 (the zero register), dropping the CLR and the clobber.
- TryBegin/longjmp 16-bit handler test: MOV R16; OR R16 -> R26 (X-low scratch).
Found safe (no change needed): ISR context save/restore (PUSH/POP R16/R17), the
main stack-pointer init (runs before any interval), and the MUL/MULSU paths --
those operate on R18-R25, never R16/R17, so signed multiply does not conflict.
Documented as allocator constraints (not codegen-fixable): inline-asm operand
loading uses R16-R19 by its %N convention, and GcRoot/GcUnroot use R16/R17 as
shadow-stack scratch (GC-only, and emitted at prologue / before returns where no
1-byte local is live). A future allocator must avoid R16/R17 across those.
Behaviour-preserving for current code. 81 unit + 722 integration tests pass;
dht-sensor unchanged at 1074 B. Validated with a throwaway binary; build/bin not
modified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The global register allocator excluded every name with DotCount >= 2 (the inline-expanded ZCA/HAL locals) from R4-R15. For call-heavy code like the DHT hot loop those locals — including the highest-use values (_read_byte.result, pin_pulse_in.result, ...) — were the dominant SRAM traffic. The DotCount filter was a heuristic, not a correctness requirement: R4-R15 are never used as codegen scratch and this allocator assigns a globally-unique register per name, so a register-resident value survives any call (the callee's own named vars get different registers; leaf scratch is R16-R27/R30/R31). That invariant makes dotted inline locals just as safe to registerize. Drops the exclusion and adds explicit safety filters for the names that some codegen path resolves by SRAM address instead of through _regLayout: GcRoot/GcUnroot targets and bytearray/array base names, plus GC_REF/FUNCREF pointer types. Deterministic tie-break (ordinal) keeps output stable. Net effect across examples: lm35 -10, pwm -6, adc -4, dht -2 B, 0 regressions. This converts ~40 LDD/STD into register-register MOVs in the DHT loop, which is size-neutral on AVR (both are 1 word) but cuts cycles and SRAM, and is the prerequisite for operand coalescing. 722 integration + 81 unit green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CompileBinary always staged the second operand through R18 before the reg-reg op (ADD/SUB/AND/OR/EOR), e.g. `MOV R18,Rhome; ADD R24,R18`. When the operand already lives in a home register of the matching width, use that register pair directly (`ADD R24,Rhome`), dropping one MOV per op. Guarded to stay correct: only the reg-reg ops above (variable shifts clobber R18; div/mod/mul use fixed ABI registers), only when src2's width equals the op width (no zero/sign extension needed), and never for 32-bit (no register homes from the allocator). Reading a register as the second ALU operand never clobbers it, so both the named-var pool (R4-R15) and the temporary pool (R16/R17) are safe sources. Small win on its own (the DHT's binary ops are 31/34 immediate-src2), but it is the first step of the larger in-place/operand-coalescing work needed to close the instruction-count gap vs gcc -Os. 722 integration + 81 unit green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The template codegen compiled every Binary as "stage src1 into R24, operate, store R24 to dst home" — a fixed MOV/op/MOV round-trip. For the augmented form `x = x OP c|y` where x lives in a home register, that round-trip is pure overhead: the op can run directly on the home register. Adds TryCompileBinaryInPlace, a fast path that emits the operation straight into dst's home register and returns (no R24 stage, no store-back). Restricted to the cases where it is an unambiguous win and AVR register classes allow it: - 8-bit Add/Sub/And/Or/Xor, dst == src1 (src1 already resident in the home reg); - const +/-1 -> INC/DEC (any register); - other immediates -> SUBI/ANDI/ORI only when the home is R16-R31; - register/SRAM src2 -> OP rd, <reg> (src2 staged through R18 only if in SRAM). Everything else (dst != src1, 16/32-bit, comparisons, mul/div/mod, variable shifts, float, constant src1) falls back to the existing staged path unchanged. Validation is up front so a fallback emits nothing. This is the first instruction-selection step toward closing the ~2.6x instruction-count gap vs gcc -Os (which keeps values in registers and operates in place). Measured vs the pre-A17 baseline, no regressions: dht 1074->1068, pwm 434->424, lm35 2122->2112, adc 658->654. 722 integration + 81 unit green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generalizes TryCompileBinaryInPlace to UINT16/INT16: `x = x OP y` with a 16-bit home register pair emits byte-wise in place (ADD/ADC, SUB/SBC, AND/OR/EOR on rd:rd+1) instead of staging through R24:R25 and storing back. 16-bit constant operands still fall back — they need SUBI/SBCI on an R16-R31 pair and the 16-bit homes are always R4-R15 (the temporary pool is 8-bit). No delta on the current examples (their hot 16-bit ops aren't the augmented form), but it completes the optimization rather than leaving it artificially 8-bit-only, and applies to 16-bit-heavy code. 722 integration + 81 unit green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oline)
EmitBranch lowers every "branch to target if cond" as a trampoline:
inv(cond) skip
RJMP target
skip:
so the RJMP can reach any distance — conditional branches only have ±64 words
of reach. But the vast majority of branches in compiled control flow are short,
so the trampoline is pure overhead.
Adds a ShortenBranches peephole that collapses the triple back to a single
`cond target` whenever target is within the conditional branch's reach. The
displacement is measured locally in words (CALL/JMP/LDS/STS = 2, the rest = 1);
a candidate whose span contains a Raw line or directive — anything whose size or
effect on the address counter we can't account for — is left alone. The
fixed-point loop is monotone: shortening only ever brings other branches closer,
so a candidate validated in the current layout stays in range after every later
removal. A mis-estimated displacement would be rejected by the assembler (a
loud, test-caught build failure), never silently mis-branch — and the simulator
integration tests exercise the inversions end to end.
Big, fully general win (every if/loop): dht 1068->1010, lm35 2112->2022,
eeprom 486->416, adc 658->600, uart-echo 174->146, pwm 424->398. 722
integration + 81 unit green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ShortenBranches bailed on any candidate whose span contained a Raw line (an inline-asm body is emitted as one multi-line string), because their flash size was unknown — leaving branches that jump across HAL asm (delay loops, pulse loops) on the slow RJMP-trampoline form. TryRawWordSize counts a Raw block's instruction words: comments and labels are free, recognized zero-size directives (.equ/.global/...) are skipped, and anything that emits data or moves the origin (.org/.byte/.word/unknown) still bails the candidate. This lets the branch-shortening pass reach across inline asm. Clean-build .hex: wdt-blink 292->274, dht 1116->1114. (Investigated CALL->RCALL shortening too; it is a no-op here because the GNU as/ld toolchain already performs call relaxation with -mrelax, so it was not added.) 722 integration + 81 unit green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds Ssd1306InitTests: boots the ssd1306 example with a recording I2C device at 0x3C and asserts the bytes the driver emits during oled.init(). The init now comes from a flash const[uint8[25]] table walked by a runtime loop, so these lock down its observable output: - exactly 25 commands (25 control+command pairs on the wire), - every control byte is 0x00 (command stream), - the command bytes equal the datasheet sequence, in order. Guards the table/loop against silent drift. 725 integration tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The @inline driver composition emits split constant masks like (nib & 0xF0) | _RS | _BL -> ORI Rd,1; ORI Rd,8. Folding the immediates is a flat win: Rd = (Rd op a) op b == Rd op (a op b); the second op alone fixes Rd and SREG and nothing significant reads the intermediate, so flag behaviour is preserved. Runs after EliminateDeadTempMoves so the staging MOV the template codegen parks between the two ops (MOV R16,R24) is already gone, leaving them consecutive among significant lines (comments/debug markers between are skipped; a label or raw-asm between bails). lcd-i2c example 746 -> 738 B. 725 AVR integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After `MOV d,s` both registers hold the same value, but the main alias pass
clears its facts at every conditional branch (a join-point precaution), so a
reload `MOV s,d` the codegen emits after a compare+branch survives even though
the register is unchanged on the fall-through — e.g. the decimal printer's
`MOV R9,R24; CPI R24,..; BRcc ..; MOV R24,R9`.
Adds EliminateRedundantReloads: from a `MOV d,s`, scan forward removing any
copy between {d,s} until an instruction writes d or s, or a label (a possible
alternate entry) intervenes. WritesReg over-approximates writes (calls clobber
scratch but preserve R4-R15; unrecognised mnemonics count as writes) so the
removal is always sound. Runs after ShortenBranches, which collapses the
`BRcc skip; RJMP t; skip:` lowering back to a single `BRcc t` and removes the
skip label that would otherwise stop the scan.
sensor-dashboard 1374->1350, bmp280 1192->1176, dht 992->988, ssd1306 520->514,
lcd-i2c 738->734. 725 AVR integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The template codegen parks a computed value in a home register when R24 is
needed for another argument, then reloads it:
MOV Rh, Rs ; <set up other args, clobbering Rs> ; MOV Rd, Rh ; CALL
EliminateParkRoundTrip rewrites the park to target Rd directly and drops the
reload (MOV Rd, Rs ; <args> ; CALL), when between the two MOVs nothing reads or
writes Rh or Rd and there is no call/branch/label, and Rh is dead afterward.
Soundness rests on two conservative predicates: WritesReg/ReadsReg
over-approximate effects (calls touch only the arg/scratch registers, anything
unrecognised is assumed to read and write), and RegDeadAfter bails at ANY path
divergence (branch/jump/label/RET) -- a write seen past a conditional branch
does not redefine the register on the not-taken path, which is exactly the bug
an earlier version hit on the array-min loop (`min = x` guarded by BRSH).
lcd-i2c 734->730, bmp280 1176->1172, ssd1306 514->512. 725 AVR integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Large programs (and FFI calls to distant C symbols) overflow the 13-bit PC-relative range of RCALL/RJMP (±2047 words ≈ ±4 KB): the linker reports "relocation truncated to fit: R_AVR_13_PCREL". The asm filter already upgraded RCALL->CALL; do the same for RJMP->JMP (full 22-bit absolute range) and pass -mrelax to the linker so it relaxes each CALL/JMP back to RCALL/RJMP wherever the target is in range. Small programs keep the compact encoding; large ones link. The atmega vector table (.org 4-byte slots) is filled exactly by a 4-byte JMP, and relaxes back to RJMP+pad when in range. Side benefit: calls that previously stayed CALL (no relaxation was enabled) now shrink to RCALL where possible -- dht 988->936, lcd-i2c 730->662, bmp280 1172->1130, ssd1306 512->492. A 28 KB stress program that previously failed to link now builds. 725 AVR integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports File > Examples > 03.Analog > Smoothing: a 10-sample running average of A0 printed over UART. SmoothingTests drives the ADC with a constant input and asserts the average converges to it -- using values >255 (500, 300) so the test also pins the uint16 print path (an 8-bit truncation would print 244 / 44). 727 AVR integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two accesses to the same array with the same variable index in one straight-line region each lower to a 6-instruction recipe that rebuilds base+index*scale into Z, then load/store through Z (which leaves Z intact). The second rebuild is dead work: Z already holds the address. EliminateRedundantZSetup tracks the live Z recipe and drops an identical rebuild when the index source and Z are provably untouched in between, nulling the fact at any control-flow boundary (label/jump/branch/RET/CALL) with the same path-divergence discipline as RegDeadAfter. Sliding-window/accumulator code is the canonical beneficiary: the smoothing example (readings[index] read-modify-write) drops 644->632 bytes (one Z recipe). 727 AVR integration tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
__div16 already produces both quotient (R24:R25) and remainder (R26:R27) in one pass; __mod16 just wraps it. When a basic block divides AND mods the same dividend by the same divisor -- the decimal-print digit loop's r = v % 10; v = v // 10 -- the pair was lowered to two full 16-iteration division calls. DetectDivModFusions matches the pair (same Src1/Src2, unsigned 16-bit, the second op's destination untouched between, no control flow) and EmitFusedDivMod emits a single __div16, storing the quotient and remainder from the one call. Halves the per-digit division work in every multi-digit print and shrinks the loop body: smoothing 632 -> 626 bytes. Backend-only; the IR pair is left generic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The linear scan only ever register-allocated UINT8 temporaries, so every uint16 temporary spilled to a stack slot -- each one costing a store at its definition and a reload at every use (the bulk of the STD/LDD traffic in compute-heavy code). The codegen already drives a register-homed temp's high byte via GetHighReg, so a pair-homed 16-bit temp needs no codegen change; the scan just never handed one out. Treat R16/R17 as two byte-slots: an 8-bit temp takes one, a 16-bit temp takes the pair (low in R16). Call-spanning temps stay excluded. smoothing 626->606, bmp280 1130->1122, sensor-dashboard 1316->1310, stopwatch 658->654; drivers unchanged. 727 AVR integration tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
divmod(3000, 10) -> (300, 0): a quotient above 255 that a regression to the old uint8/__div8 lowering would narrow to 44. Locks in the 16-bit divmod fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconstructing a 16-bit value from two bytes -- result = (hi << 8) | lo, or + lo -- is just "low byte = lo, high byte = hi", but the generic widen + 16-bit shift + 16-bit add lowering spent ~10 instructions on it. Every 16-bit sensor read (ADC ADCH/ADCL, BMP280, ...) pays this in its hot path. DetectBytePackFusions matches a 16-bit `hi << 8` whose result feeds an adjacent commutative Add/BitOr against a byte operand (so lo lands entirely in the low byte), with the shift result single-use and hi/lo untouched and no control flow between. EmitBytePack then loads hi into R25 and lo into R24 and stores -- three instructions instead of ten. Backend-only; the IR stays generic. smoothing 600->588, bmp280 1122->1094, sensor-dashboard 1310->1298. 730 AVR integration + 351 frontend tests green (the sensor tests check real 16-bit sample values, exercising the reconstruction). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… once Four ADC channels (four (hi<<8)|lo byte-packs), four sliding-window accumulators (variable-index array Z-CSE + heavy uint16 temp pressure), the divmod() builtin, and decimal printing (divmod fusion) in one hot loop. With all channels held at ADC count 500 the averages converge to 500 and divmod(500,10) yields (50,0) -- proving the combined optimizations preserve the arithmetic under register pressure. Output is bit-identical across rebuilds (deterministic). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read A0, map 0-1023 to a 0-255 PWM duty on D6 (OC0A) via the native pymcu HAL (UART + AnalogPin + PWM), echo the duty byte. Functional test injects ADC count 512 and asserts OCR0A == 512 >> 2 == 128, exercising the ADC byte-pack and the PWM path end to end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
machine.ADC + PWM + UART: read A0, map to a PWM duty on D6, echo it. Now builds
correctly after the inline-param stale-binding fix (ADC(Pin(14)) before
PWM(Pin("PD6")) no longer leaks the int pin id into the PWM pin). Functional test
asserts OCR0A == 512 >> 2 == 128 with A0 at ADC count 512.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_seed: uint16 = 3007 prints 3007 (seeded, not 0), then 3008 after a runtime increment in a callee (proving it is a mutable RAM cell, not a folded constant). Locks in the global-init and cross-call constant-invalidation fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exercises comma-separated and aliased module imports plus aliased constructor (m.UART/m.Pin), member (m.Pin.OUT) and method (t.sleep_ms) resolution. Reaching the 'MA' UART banner proves they all resolve to the real module. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single-byte globals flagged as isrSharedGlobals in the .mir are rewritten Variable -> MemoryAddress(GPIORn) before any allocation pass, so the existing codegen emits IN/OUT (1 cycle, always volatile) instead of LDS/STS, and SBI/CBI/SBIS/SBIC for bit ops on GPIOR0. The most-used global gets GPIOR0, the bit-addressable register. BSS shrinks by the promoted bytes. Safety: UINT8 only (1-byte MemoryAddress is typed UINT8 by GetValType, so INT8 would lose signedness in compares); globals used in inline asm, VirtualCall or GC instructions are excluded; GPIOR addresses already referenced explicitly by the program are never reassigned; chips outside the megaAVR/tinyAVR table get no promotion (SRAM is always correct). The classic ISR<->main flag now compiles from a plain global to the same code an expert writes by hand with the manual GPIOR0[n] idiom. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion Two plain uint8 globals shared between the INT0 ISR and main (flag: set/poll/clear; presses: ISR-incremented lifetime counter) with no GPIOR idiom and no manual volatile handling. Verifies behavior in the simulator (each press delivered), that both globals physically live in GPIOR0/GPIOR1 (peeking uno.Data[0x3E/0x4A]), and that the backend emits the promotion markers in the debug asm (via new PymcuCompiler.FixtureDir helper). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ain globals All 12 interrupt examples now signal between ISR and main through plain uint8 module globals. The compiler detects them as ISR-shared (volatile semantics) and auto-promotes them to GPIOR0/1/2, producing the same SBI/CBI/IN/OUT code the manual idiom wrote by hand — with zero SRAM and no chip-specific imports in user code. READMEs updated to describe the plain-global pattern. Migrated: adc-interrupt, i2c-irq, interrupt-counter, pcint-counter, pin-irq, sensor-dashboard, sleep-wakeup, soft-pwm, spi-irq, stopwatch, timer-ctc, timer-interrupt. Side fix: i2c-irq and spi-irq stored the received byte with bit-indexing (GPIOR0[0] = SPDR.value), which truncated it to bit 0 — a plain uint8 global now carries the full byte as the READMEs always documented. All 12 rebuilt and promotion-verified; integration suite 748/748 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
print(f"...") with runtime interpolations: mixed uint8/uint16 widths, a negative int16 and a float, and a constant-string (const[str]) part. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
uart.write_str(f"a={s} b={n};") and uart.println(f"line={s}") with runtime
uint8/uint16 interpolations, including the println newline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
int16(-5) < uint16(200) must be True (Python compares by value); guards against a C-style promotion to unsigned that would compare 65531 < 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign extension was only applied for size==2 (int8->int16); widening a signed value to 32 bits (int8->int32, int16->int32) fell through both the sign- and zero-extension paths and left the upper bytes as garbage, so int32(int16(-8)) read e.g. 0x0008FFF8 instead of -8. LoadIntoReg now loads only the source's real bytes in all four operand layouts and fills the remaining high bytes via a shared EmitWidenFill: zero for unsigned, the sign of the source's highest real byte for signed, at any target width. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover hex/HEX/binary/padded-decimal format specs (FStringFormatSpecs), signed decimal padding and octal (FStringFormatSignedAndOctal), and the int16->int32 sign-extension regression (SignedWidenToInt32). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The caller and callee argument loops were hard-capped at 4 (`k < 4`), so a non-inline function with 5+ parameters silently dropped args 5+ — the callee read uninitialised registers (e.g. f(a,b,c,d,e) lost e). Both loops now load and read every argument that ArgBaseRegs assigns (the virtual-call path already did). Argument registers must be R16..R25 (loading an immediate needs a high register), so ArgBaseRegs now rejects an assignment below R16 with a clear "too many/wide arguments" error pointing at @inline / a struct, instead of emitting an assembler-rejected low-register access. Five small args (or fewer wide ones) now pass correctly; more is a clean error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FiveArgsAllArrive verifies the 5th argument now arrives (the cap-at-4 silent drop). TooManyRegisterArgsRejected checks that an over-budget argument list is rejected with a clear message rather than miscompiled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build and run an LCD program using lcd.print_str(f"T={s} 0x{n:04x}") with a
runtime value and a hex format spec; assert it reaches its UART banner (the
LCD format/decompose path is valid and executes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rev_sum() builds and reads a local uint8[8] with runtime indices and is @inline-expanded into the caller; it must compute 20 for n=5 rather than failing to compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Functions with more arguments than fit in R16..R25 (about five 1-2 byte args) previously errored. They now pass the overflow arguments through a fixed SRAM region `_arg_spill` instead of reaching into R8..R15 — which is the register allocator's R2..R15 home pool, whose cross-call safety relies on the calling convention never touching it. The caller stores each spilled arg (staged through R26:R27) and the callee loads it into its home/slot in the prologue, before any nested call, so a single shared region is safe (consistent with PyMCU's existing static-frame, non-reentrant model). The spill region is sized to the largest overflow across the program and placed just above the static frame. Register args (<= 5) are unchanged. A spilled argument wider than 2 bytes is still rejected with a clear message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SixArgsViaSpill (6 uint8: one spilled), EightArgsViaSpill (8 uint8: three spilled), and SpilledArgsWith16BitMix (a uint16 spilled at a 2-byte offset) each verify every argument arrives intact past the R16..R25 register budget. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A spilled argument whose value was a temp (e.g. a nested call result) read 0: the register-arg loads ran first and overwrote the temp's home (R16..R25) before the spill store read it. CompileCall now stores all spilled arguments first — reading their sources while still intact — then loads the register arguments. Staging stays in R26:R27, which the temp pool (R16:R17 only) and home pool (R2..R15) never use, so no argument source can alias it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
f6(1,1,1,1,1, add1(s)) must return 65 — the spilled 6th argument is a call result that must survive into the spill region (regression for arg ordering). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DefaultArgViaSpill (a default value fills a spilled parameter), MethodSelfPlusFiveArgsViaSpill (self + 5 args = 6, the last spills), and ConsecutiveSpillCalls (two calls reuse the shared spill region independently). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ation BitNotUint16Width (~uint16 respects the 16-bit width -> 65530), Uint32RuntimeDivMod (uint32 % and // by a runtime divisor), and GlobalArrayMutationPersists (a module-level array mutated across function calls keeps its values). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AugAssignInstanceField (self._n += v through methods) and SliceWithStep (a[0:6:2] selects indices 0,2,4) — both behave faithfully. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RuntimeDivByZeroRaises (100 // 0 caught by except ZeroDivisionError -> 77) and RuntimeDivByNonZeroWorks (100 // 5 -> 20, no raise). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`try: r = 100 // s; except ZeroDivisionError: r = 88` (no Call in the try body) must build and catch -> 88. Regression guard for the dangling catch label / link failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ht errors The exception runtime was emitted only when a Call to __pymcu_unhandled_exn existed (an unmatched try). Uncaught-error propagation now reaches it via a BranchOnError too, so detect that target as well. Test UncaughtExceptionHalts: an uncaught runtime divide-by-zero halts the device (the code after the faulting division never runs) instead of silently continuing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se-from-callee ExceptionPropagatesThroughLevels (c->bb->run, caught at top), WrongTypeExceptionPropagatesToOuter (inner except mismatch re-propagates to the outer try), FinallyRunsOnErrorPath (except then finally), and RaiseFromCalleeCaught (explicit raise in a called function is caught). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TryElseRunsWhenNoException (else runs on success), TryElseSkippedOnException (else skipped when an exception fired), ReraiseInExceptPropagates (raise inside an except handler propagates to the caller's try), and SecondExceptClauseMatches (dispatch falls through to the matching handler). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FinallyRunsBeforePropagation (finally runs before the error propagates to the caller's handler), FinallyRunsBeforeReturn (return in a try runs finally first), and InlineRaisePropagatesToCaller (a raise inside an @inline function is caught by the caller's try). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FinallyRunsBeforeBreak (break in a try-with-finally runs finally -> 31) and FinallyRunsBeforeContinue (continue runs finally each iteration -> 58). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ErrorCodePreservedThroughFinally (R22 survives a finally that runs a uart routine, so the right handler matches), RaiseInElsePropagates (raise in else is not caught by its own try), RaiseInFinallyOverrides (a finally raise propagates), RecoverAndContinueAfterTry (code after a caught try runs), TryInLoopRecovers- EachIteration (T-flag clean per iteration), and SequentialTryBlocks (T reset between two try blocks). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReturnInExceptHandlerRunsFinally (return inside a handler runs finally -> 8,5) and RaiseFromMethodCaught (an error raised in a method is caught by the caller). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…override NestedFinallyOnReturn/OnBreak (two finally levels run innermost-first), RaiseInConstructorCaught (raise in __init__ is caught), BareReraise (bare `raise` re-raises to the caller), and FinallyExceptionOverridesPending (a finally that raises replaces the pending exception). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s R22 The handler runs a 2-arg call (2nd arg in R22) before a bare raise; the re-raised exception must still be ZeroDivisionError, caught by the caller. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ModuloByZeroRaises (% by zero raises like //), NestedBareReraise (bare raise through two handler levels), BreakInFinallySwallowsException and ReturnInFinallySwallowsException (break/return in finally discard the in-flight exception, per Python), and HandlerRaisesNewExceptionCaughtByOuter (a new exception from a handler propagates to the outer try). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PyMCU AVR backend v0.1.0a2 — second alpha. Code generation, the AVR math/exception runtime, and the integration test suite all advance significantly. 158 commits since
v0.1.0a1.post2.Calling convention & registers
LoadIntoRegoperand layouts (int16→int32no longer reads garbage); zero-extension loads the source's real high byte.IndirectCall/GcAlloc/array/bytearray ops.Exception runtime (T-flag model)
raiselowers to a plain jump (noSET/RET).SignalErroredges and countsSignalError.Codeas a use (no dangling labels / undefined references).Math & codegen
__mul32for 32-bit multiply (was truncating to 8 bits); signed floor-div/mod runtime routines; arithmetic right shift sign-extends on whole-byte shifts.FlashStrAddr+FlashLoadPtr).CALL/JMP+-mrelaxlinking fixes far-target relocations; ISR context-save now covers R20–R23 and the Z pointer; out-of-range@interruptvectors are rejected, not dropped.Testing
The integration suite (compile → simulate on an ATmega328P, check real firmware behavior) now stands at 1021 tests, covering the new arithmetic promotion model, f-strings/format-specs, the full exception model, multi-argument calls, and many fidelity regressions.
🤖 Generated with Claude Code